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;
18#[cfg(test)]
19use crate::normalized_scene::{
20 build_scene_window, collect_layer_contents, collect_layer_contents_with_translation_context,
21 filtered_effect_layer_index, scene_bounds, SceneWindowSource,
22};
23#[cfg(test)]
24use crate::normalized_scene::{estimate_layer_surface_rect, motion_stable_capture_bounds};
25use crate::normalized_scene::{translate_quad, ChildLayerComposite, CollectedLayer};
26use crate::offscreen::OffscreenTarget;
27use crate::rect_to_quad;
28use crate::scene::{
29 BackdropLayer, CompositorScene, DrawOp, DrawOpKind, DrawShape, EffectLayer, ImageDraw,
30 RetainedDraw, ShadowDraw, SimilarityTransform, SnapAnchor, TextDraw,
31};
32use crate::shaders;
33#[cfg(test)]
34use crate::surface_executor::surface_target_size;
35use crate::surface_executor::{
36 apply_backdrop_layer_to_target as execute_apply_backdrop_layer_to_target,
37 axis_aligned_quad_rect, backdrop_underlay_is_covered_by_local_content,
38 canonicalize_device_coordinate, canonicalized_scaled_quad, canonicalized_scaled_rect,
39 composite_surface_to_view as execute_composite_surface_to_view, device_pixel_bounds_for_rect,
40 offscreen_byte_size, render_effect_layer_to_target as execute_render_effect_layer_to_target,
41 render_layer_surface as execute_render_layer_surface,
42 render_root_direct as execute_render_root_direct, root_direct_scene_events_are_supported,
43 scaled_quad, snap_delta_for_anchor, snap_motion_stable_dest_quad,
44 translation_stable_anchored_device_pixel_bounds, DevicePixelBounds, LayerSurfaceTexture,
45 SurfaceExecutionBackend,
46};
47#[cfg(test)]
48use crate::surface_executor::{clamp_effect_surface_scale, visible_layer_rect};
49#[cfg(test)]
50use crate::surface_plan::root_can_render_directly_cached;
51#[cfg(test)]
52use crate::surface_plan::{
53 composite_sample_mode_for_effect_layer, composite_sample_mode_for_requirements,
54 direct_translation, effect_layer_target_scale, layer_contains_descendant_backdrop,
55 layer_surface_requirements, layer_surface_requirements_cached, layer_surface_scale,
56 layer_surface_target_scale, layer_uses_external_backdrop_input, TranslatedContentAxes,
57};
58use crate::surface_plan::{LayerSurfaceRequest, TranslationRenderContext};
59#[cfg(test)]
60use crate::surface_requirements::SurfaceRequirement;
61use crate::surface_requirements::SurfaceRequirementSet;
62use crate::DebugCpuAllocationStats;
63use bytemuck::{Pod, Zeroable};
64#[cfg(any(not(target_arch = "wasm32"), test))]
65use cranpose_core::collections::map::HashMap;
66use cranpose_core::{hash::default as default_hash, NodeId};
67use cranpose_render_common::bounded_lru_cache::BoundedLruCache;
68use cranpose_render_common::geometry::blur_extent_margin;
69use cranpose_render_common::graph::quad_bounds;
70#[cfg(test)]
71use cranpose_render_common::graph::{
72 CachePolicy, LayerNode, PrimitiveEntry, PrimitiveNode, PrimitivePhase, ProjectiveTransform,
73 RenderNode,
74};
75use cranpose_render_common::raster_cache::LayerRasterCacheKey;
76#[cfg(test)]
77use cranpose_render_common::raster_cache::ScaleBucket;
78use cranpose_render_common::software_text_raster::{
79 collect_solid_text_atlas_run, measure_text_with_font,
80 rasterize_annotated_text_to_image_with_glyph_cache, rasterize_text_to_image_with_glyph_cache,
81 SoftwareGlyphAtlasGlyph, SoftwareGlyphAtlasKey, SoftwareGlyphAtlasPlacement,
82 SoftwareGlyphAtlasRunGlyph, SoftwareGlyphRasterCache, SoftwareTextFontSet,
83};
84#[cfg(test)]
85use cranpose_ui_graphics::GraphicsLayer;
86use cranpose_ui_graphics::{
87 BlendMode, Brush, Color, ColorFilter, FxHasher, ImageBitmap, ImageSampling, Point, Rect,
88 RenderEffect, RenderHash, RuntimeShader, StrokeCap, StrokeJoin, TileMode,
89};
90use std::borrow::Cow;
91use std::cell::Cell;
92use std::hash::{Hash, Hasher};
93use std::ops::Range;
94use std::rc::Rc;
95#[cfg(not(target_arch = "wasm32"))]
96use std::sync::atomic::{AtomicUsize, Ordering};
97use std::sync::{mpsc, Arc};
98use std::time::Duration;
99use web_time::Instant;
100
101use crate::gpu_stats;
102use crate::gpu_stats::gpu_stats_enabled;
103use crate::pipeline::push_layer_shadow;
104
105#[cfg(target_arch = "wasm32")]
110const MAX_SHAPES_PER_BATCH: usize = 102;
111#[cfg(not(target_arch = "wasm32"))]
112const MAX_SHAPES_PER_BATCH: usize = 768;
113#[cfg(target_arch = "wasm32")]
114const MAX_GRADIENT_STOPS: usize = 256;
115#[cfg(not(target_arch = "wasm32"))]
116const MAX_GRADIENT_STOPS: usize = 1024;
117
118#[cfg(not(target_arch = "wasm32"))]
125const MAX_SHAPES_PER_STORAGE_BATCH: usize = 1 << 16;
126#[cfg(not(target_arch = "wasm32"))]
127const MAX_GRADIENT_STOPS_PER_STORAGE_BATCH: usize = 1 << 16;
128
129#[cfg(not(target_arch = "wasm32"))]
135const INITIAL_STORAGE_BATCH_CAPACITY: usize = 1024;
136
137#[derive(Clone, Copy, Debug, Eq, PartialEq)]
149struct ShapeBatchLimits {
150 max_shapes_per_batch: usize,
151 max_gradient_stops: usize,
152 storage: bool,
153}
154
155impl ShapeBatchLimits {
156 fn for_device(device: &wgpu::Device) -> Self {
157 let limits = device.limits();
158 #[cfg(not(target_arch = "wasm32"))]
159 if limits.max_storage_buffers_per_shader_stage >= 2 {
160 return Self::for_storage_binding_size(limits.max_storage_buffer_binding_size);
161 }
162 Self::for_uniform_binding_size(limits.max_uniform_buffer_binding_size)
163 }
164
165 fn for_uniform_binding_size(max_uniform_buffer_binding_size: u64) -> Self {
166 let binding = max_uniform_buffer_binding_size as usize;
167 Self {
168 max_shapes_per_batch: (binding / std::mem::size_of::<ShapeData>())
169 .clamp(1, MAX_SHAPES_PER_BATCH),
170 max_gradient_stops: (binding / std::mem::size_of::<GradientStop>())
171 .clamp(1, MAX_GRADIENT_STOPS),
172 storage: false,
173 }
174 }
175
176 #[cfg(not(target_arch = "wasm32"))]
177 fn for_storage_binding_size(max_storage_buffer_binding_size: u64) -> Self {
178 let binding = max_storage_buffer_binding_size as usize;
179 Self {
180 max_shapes_per_batch: (binding / std::mem::size_of::<ShapeData>())
181 .clamp(1, MAX_SHAPES_PER_STORAGE_BATCH),
182 max_gradient_stops: (binding / std::mem::size_of::<GradientStop>())
183 .clamp(1, MAX_GRADIENT_STOPS_PER_STORAGE_BATCH),
184 storage: true,
185 }
186 }
187
188 fn initial_shape_capacity(&self) -> usize {
189 #[cfg(not(target_arch = "wasm32"))]
190 if self.storage {
191 return self
192 .max_shapes_per_batch
193 .min(INITIAL_STORAGE_BATCH_CAPACITY);
194 }
195 self.max_shapes_per_batch
196 }
197
198 fn initial_gradient_capacity(&self) -> usize {
199 #[cfg(not(target_arch = "wasm32"))]
200 if self.storage {
201 return self.max_gradient_stops.min(INITIAL_STORAGE_BATCH_CAPACITY);
202 }
203 self.max_gradient_stops
204 }
205
206 fn data_buffer_usage(&self) -> wgpu::BufferUsages {
207 if self.storage {
208 wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST
209 } else {
210 wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST
211 }
212 }
213
214 fn data_binding_type(&self) -> wgpu::BufferBindingType {
215 if self.storage {
216 wgpu::BufferBindingType::Storage { read_only: true }
217 } else {
218 wgpu::BufferBindingType::Uniform
219 }
220 }
221
222 #[cfg(test)]
223 fn desktop() -> Self {
224 Self::for_uniform_binding_size(wgpu::Limits::default().max_uniform_buffer_binding_size)
225 }
226}
227#[cfg(target_arch = "wasm32")]
228const HARD_MAX_BUFFER_MB: usize = 64; const MAX_SHADOW_SURFACE_CACHE_ITEMS: usize = 512;
230const MAX_SHADOW_SURFACE_CACHE_BYTES: u64 = 192 * 1024 * 1024;
234const MAX_TEXT_IMAGE_CACHE_ITEMS: usize = 1024;
235const MAX_TEXT_GLYPH_MASK_CACHE_ITEMS: usize = 8192;
236const MAX_TEXT_GLYPH_ATLAS_ITEMS: usize = 8192;
237const MAX_TEXT_GLYPH_RUN_CACHE_ITEMS: usize = 1024;
238#[cfg(not(target_arch = "wasm32"))]
239const MAX_TEXT_GLYPH_GPU_RUN_CACHE_ITEMS: usize = 1024;
240#[cfg(not(target_arch = "wasm32"))]
241const MIN_RETAINED_TEXT_GLYPH_QUADS: usize = 192;
242#[cfg(not(target_arch = "wasm32"))]
243const OFFSCREEN_TEXT_GLYPH_PREWARM_BUDGET_MS: f64 = 0.75;
244#[cfg(not(target_arch = "wasm32"))]
245const MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CANDIDATES: usize = 2;
246#[cfg(not(target_arch = "wasm32"))]
247const MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS: usize = 160;
248#[cfg(not(target_arch = "wasm32"))]
249const MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CACHED_GLYPHS: usize = 160;
250const TEXT_GLYPH_ATLAS_MIN_SIZE: u32 = 512;
262const TEXT_GLYPH_ATLAS_MAX_SIZE: u32 = 4096;
263const TEXT_GLYPH_ATLAS_PADDING: u32 = 1;
264const MAX_TEXT_LINE_INDEX_CACHE_ITEMS: usize = 512;
265const MIN_MULTILINE_TEXT_LINES_FOR_CLIPPED_RASTER: usize = 2;
266const MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES: usize = 128;
267const CACHE_MISS_WARMUP_FRAMES: u8 = 1;
268pub(crate) const CLEAR_COLOR: wgpu::Color = wgpu::Color {
269 r: cranpose_render_common::FRAME_CLEAR_COLOR[0] as f64,
270 g: cranpose_render_common::FRAME_CLEAR_COLOR[1] as f64,
271 b: cranpose_render_common::FRAME_CLEAR_COLOR[2] as f64,
272 a: cranpose_render_common::FRAME_CLEAR_COLOR[3] as f64,
273};
274#[cfg(not(target_arch = "wasm32"))]
275const INITIAL_UPLOAD_BUFFER_BYTES: u64 = 4 * 1024;
276#[cfg(not(target_arch = "wasm32"))]
277const INITIAL_RETAINED_GLYPH_UNIFORM_SLOTS: usize = 128;
278const MAX_TEXTURE_CACHE_ITEMS: usize = 256;
279const MAX_IMAGE_TEXTURE_CACHE_BYTES: usize = 256 * 1024 * 1024;
283const RETAINED_STAGED_UPLOAD_BYTES: usize = 256 * 1024;
284const RETAINED_STAGED_UPLOAD_COPIES: usize = 128;
285pub(crate) const RETAINED_LAYER_REQUIREMENTS_CAPACITY: usize = 512;
286const DEFAULT_WGPU_RENDER_STAGE_TELEMETRY_THRESHOLD_MS: f64 = 4.0;
287#[cfg(not(target_arch = "wasm32"))]
288static SEGMENT_DIAG_LINES: AtomicUsize = AtomicUsize::new(0);
289fn wgpu_render_stage_telemetry_threshold_ms() -> Option<f64> {
293 static THRESHOLD_MS: std::sync::OnceLock<Option<f64>> = std::sync::OnceLock::new();
294 *THRESHOLD_MS.get_or_init(|| {
295 let explicit = std::env::var("CRANPOSE_WGPU_RENDER_STAGE_TELEMETRY_MS")
296 .ok()
297 .and_then(|value| value.parse::<f64>().ok())
298 .filter(|value| value.is_finite() && *value >= 0.0);
299 explicit.or_else(|| {
300 std::env::var_os("CRANPOSE_WGPU_RENDER_STAGE_TELEMETRY")
301 .is_some()
302 .then_some(DEFAULT_WGPU_RENDER_STAGE_TELEMETRY_THRESHOLD_MS)
303 })
304 })
305}
306
307pub(crate) fn instant_ms(start: Instant, end: Instant) -> f64 {
308 end.duration_since(start).as_secs_f64() * 1000.0
309}
310
311pub(crate) fn should_log_wgpu_render_stage(start: Instant, end: Instant) -> Option<f64> {
312 let threshold_ms = wgpu_render_stage_telemetry_threshold_ms()?;
313 let total_ms = instant_ms(start, end);
314 (total_ms >= threshold_ms).then_some(total_ms)
315}
316
317fn admit_layer_surface_cache_miss_impl(
318 key: &LayerRasterCacheKey,
319 observed_scene_range_misses: &mut BoundedLruCache<LayerRasterCacheKey, ()>,
320) -> bool {
321 if !key.is_scene_range() {
322 return true;
323 }
324 if observed_scene_range_misses.contains(key) {
325 return true;
326 }
327 observed_scene_range_misses.put(*key, ());
328 false
329}
330
331#[cfg(test)]
332fn first_cache_miss_admission(key: &LayerRasterCacheKey) -> bool {
333 let mut observed_scene_range_misses =
334 BoundedLruCache::with_capacity_at_least_one(MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES);
335 admit_layer_surface_cache_miss_impl(key, &mut observed_scene_range_misses)
336}
337
338#[cfg(test)]
339fn repeated_cache_miss_admission(key: &LayerRasterCacheKey) -> bool {
340 let mut observed_scene_range_misses =
341 BoundedLruCache::with_capacity_at_least_one(MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES);
342 let _ = admit_layer_surface_cache_miss_impl(key, &mut observed_scene_range_misses);
343 admit_layer_surface_cache_miss_impl(key, &mut observed_scene_range_misses)
344}
345
346pub static PRESENTED_FRAMES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
347
348pub fn frames_presented() -> u64 {
349 PRESENTED_FRAMES.load(std::sync::atomic::Ordering::Relaxed)
350}
351
352fn frame_stats_need_warmup_frame(snapshot: &gpu_stats::FrameStatsSnapshot) -> bool {
353 snapshot.layer_cache_misses > 0
354 || snapshot.shadow_shape_cache_misses > 0
355 || snapshot.text_image_cache_misses > 0
356 || snapshot.text_glyph_atlas_misses > 0
357}
358
359fn update_frame_warmup_budget(pending_frames: &mut u8, snapshot: &gpu_stats::FrameStatsSnapshot) {
360 if *pending_frames > 0 {
361 *pending_frames = pending_frames.saturating_sub(1);
362 } else if frame_stats_need_warmup_frame(snapshot) {
363 *pending_frames = CACHE_MISS_WARMUP_FRAMES;
364 }
365}
366
367fn text_atlas_fallback_diag_enabled() -> bool {
368 cranpose_core::env_flag!("CRANPOSE_TEXT_ATLAS_FALLBACK_DIAG")
369}
370
371fn text_glyph_run_diag_enabled() -> bool {
372 cranpose_core::env_flag!("CRANPOSE_TEXT_GLYPH_RUN_DIAG")
373}
374
375fn root_direct_diag_enabled() -> bool {
376 cranpose_core::env_flag!("CRANPOSE_ROOT_DIRECT_DIAG")
377}
378
379fn scene_layer_events_precede_z(scene: &CompositorScene, z_index: usize) -> bool {
380 scene
381 .effect_layers
382 .iter()
383 .any(|layer| layer.z_start < z_index && 0 < layer.z_end)
384 || scene
385 .backdrop_layers
386 .iter()
387 .any(|layer| layer.z_index < z_index)
388}
389
390fn direct_root_child_can_be_replayed_into_later_underlay(child: &ChildLayerComposite) -> bool {
391 child.backdrop.is_none()
392 && !child.has_effect
393 && child.shadow_draws.is_empty()
394 && axis_aligned_quad_rect(child.dest_quad).is_some()
395}
396
397fn rects_overlap(a: Rect, b: Rect) -> bool {
398 let a_right = a.x + a.width;
399 let a_bottom = a.y + a.height;
400 let b_right = b.x + b.width;
401 let b_bottom = b.y + b.height;
402 a.x < b_right && b.x < a_right && a.y < b_bottom && b.y < a_bottom
403}
404
405pub(crate) fn direct_root_child_underlays_are_supported(collected: &CollectedLayer) -> bool {
406 for (child_index, child) in collected.child_layers.iter().enumerate() {
407 if child.backdrop.is_some() {
408 if root_direct_diag_enabled() {
409 log::warn!(
410 "[root-direct-diag] reject self-backdrop child node={:?}",
411 child.node_id
412 );
413 }
414 return false;
415 }
416 if child.needs_nested_underlay {
417 let Some(dest_rect) = axis_aligned_quad_rect(child.dest_quad) else {
418 if root_direct_diag_enabled() {
419 log::warn!(
420 "[root-direct-diag] reject projective underlay child node={:?}",
421 child.node_id
422 );
423 }
424 return false;
425 };
426 let translation_only = (dest_rect.width - child.logical_rect.width).abs() <= 0.001
427 && (dest_rect.height - child.logical_rect.height).abs() <= 0.001;
428 let unsupported_preceding_child_layer = collected.child_layers[..child_index]
429 .iter()
430 .any(|preceding| {
431 if direct_root_child_can_be_replayed_into_later_underlay(preceding) {
432 return false;
433 }
434 axis_aligned_quad_rect(preceding.dest_quad)
435 .is_none_or(|preceding_rect| rects_overlap(preceding_rect, dest_rect))
436 });
437 let preceding_scene_events =
438 scene_layer_events_precede_z(&collected.scene, child.z_index);
439 if unsupported_preceding_child_layer || preceding_scene_events || !translation_only {
440 if root_direct_diag_enabled() {
441 log::warn!(
442 "[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})",
443 child.node_id,
444 unsupported_preceding_child_layer,
445 preceding_scene_events,
446 translation_only,
447 dest_rect.x,
448 dest_rect.y,
449 dest_rect.width,
450 dest_rect.height,
451 child.logical_rect.x,
452 child.logical_rect.y,
453 child.logical_rect.width,
454 child.logical_rect.height
455 );
456 }
457 return false;
458 }
459 }
460 }
461 true
462}
463
464#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
465struct ShadowSurfaceCacheKey {
466 content_hash: u64,
467 pixel_size: [u32; 2],
468 root_scale_bits: u32,
469 blur_radius_bits: u32,
470}
471
472struct CachedShadowSurface {
473 target: Rc<OffscreenTarget>,
474 byte_size: u64,
475}
476
477struct CachedShadowComposite {
478 source: Rc<OffscreenTarget>,
479 scissor: Option<(u32, u32, u32, u32)>,
480 rounded_mask: Option<RoundedCompositeMask>,
481 dest_viewport: Option<(f32, f32, f32, f32)>,
482}
483
484impl CachedShadowComposite {
485 fn batch_item(&self) -> CompositeBatchItem<'_> {
486 CompositeBatchItem {
487 source: &self.source,
488 alpha: 1.0,
489 scissor: self.scissor,
490 rounded_mask: self.rounded_mask,
491 blend_mode: BlendMode::SrcOver,
492 dest_viewport: self.dest_viewport,
493 source_viewport: None,
494 sample_mode: CompositeSampleMode::Nearest,
495 }
496 }
497}
498
499#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
500struct TextImageCacheKey(u64);
501
502struct CachedTextImage {
503 image: ImageBitmap,
504}
505
506#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
507struct TextGlyphRunCacheKey(u64);
508
509#[derive(Clone, Copy)]
510struct CachedTextGlyphQuad {
511 x: i32,
512 y: i32,
513 width: usize,
514 height: usize,
515 color: (f32, f32, f32, f32),
516 uv: ImageUvRect,
517}
518
519struct CachedTextGlyphRun {
520 glyphs: Rc<[SoftwareGlyphAtlasPlacement]>,
521 quads: Option<Rc<[CachedTextGlyphQuad]>>,
522 atlas_generation: u64,
523}
524
525const TEXT_GLYPH_PREWARM_VIEWPORT_MULTIPLIER: f32 = 2.0;
526
527#[cfg(not(target_arch = "wasm32"))]
528struct CachedGpuTextGlyphRun {
529 vertex_buffer: wgpu::Buffer,
530 index_buffer: wgpu::Buffer,
531 index_count: u32,
532 atlas_generation: u64,
533}
534
535#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
536struct TextLineIndexCacheKey(usize);
537
538struct CachedTextLineIndex {
539 text: std::sync::Weak<cranpose_ui::text::RenderString>,
540 len: usize,
541 starts: Rc<[usize]>,
542}
543
544struct TextLineIndexCache {
545 entries: BoundedLruCache<TextLineIndexCacheKey, CachedTextLineIndex>,
546}
547
548impl TextLineIndexCache {
549 fn new(capacity: usize) -> Self {
550 Self {
551 entries: BoundedLruCache::with_capacity_at_least_one(capacity),
552 }
553 }
554
555 fn line_starts(&mut self, text: &Arc<cranpose_ui::text::RenderString>) -> Rc<[usize]> {
556 let key = TextLineIndexCacheKey(Arc::as_ptr(text) as usize);
557 if let Some(cached) = self.entries.get(&key) {
558 if cached.len == text.text.len()
559 && cached
560 .text
561 .upgrade()
562 .is_some_and(|cached_text| Arc::ptr_eq(&cached_text, text))
563 {
564 return cached.starts.clone();
565 }
566 }
567
568 let starts = Rc::<[usize]>::from(line_start_offsets(text.text.as_str()));
569 self.entries.put(
570 key,
571 CachedTextLineIndex {
572 text: Arc::downgrade(text),
573 len: text.text.len(),
574 starts: starts.clone(),
575 },
576 );
577 starts
578 }
579}
580
581#[derive(Clone, Copy, Debug, PartialEq)]
582struct ShapeShadowSurfacePlan {
583 source_device_bounds: DevicePixelBounds,
584 processing_scissor: Option<(u32, u32, u32, u32)>,
585 pixel_radius: f32,
586}
587
588#[derive(Default)]
589struct RendererWarningState {
590 unsupported_effect_reported: Cell<bool>,
591}
592
593impl RendererWarningState {
594 fn warn_unsupported_effect_once(&self) {
595 if !self.unsupported_effect_reported.replace(true) {
596 log::warn!(
597 "WGPU renderer received an unsupported RenderEffect variant; falling back to passthrough compositing"
598 );
599 }
600 }
601}
602
603fn is_blend_mode_supported(mode: BlendMode) -> bool {
604 matches!(mode, BlendMode::SrcOver | BlendMode::DstOut)
605}
606
607fn blend_state_for_mode(mode: BlendMode) -> wgpu::BlendState {
608 match mode {
609 BlendMode::DstOut => wgpu::BlendState {
610 color: wgpu::BlendComponent {
611 src_factor: wgpu::BlendFactor::Zero,
612 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
613 operation: wgpu::BlendOperation::Add,
614 },
615 alpha: wgpu::BlendComponent {
616 src_factor: wgpu::BlendFactor::Zero,
617 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
618 operation: wgpu::BlendOperation::Add,
619 },
620 },
621 _ => wgpu::BlendState::ALPHA_BLENDING,
622 }
623}
624
625fn supported_blend_mode(mode: BlendMode) -> BlendMode {
626 if is_blend_mode_supported(mode) {
627 return mode;
628 }
629
630 BlendMode::SrcOver
631}
632
633fn direct_shader_composite_viewport(
634 alpha: f32,
635 blend_mode: BlendMode,
636 dest_viewport: Option<(f32, f32, f32, f32)>,
637 sample_mode: CompositeSampleMode,
638 source_size: (u32, u32),
639) -> Option<(f32, f32, f32, f32)> {
640 if alpha != 1.0 || supported_blend_mode(blend_mode) != BlendMode::SrcOver {
641 return None;
642 }
643 let viewport = dest_viewport?;
644 if viewport.2 <= 0.0 || viewport.3 <= 0.0 {
645 return None;
646 }
647 match sample_mode {
648 CompositeSampleMode::Linear | CompositeSampleMode::Nearest => Some(viewport),
649 CompositeSampleMode::Box4
650 if shader_composite_preserves_source_pixel_grid(viewport, source_size) =>
651 {
652 Some(viewport)
653 }
654 CompositeSampleMode::Box4 => None,
655 }
656}
657
658fn shader_composite_preserves_source_pixel_grid(
659 viewport: (f32, f32, f32, f32),
660 source_size: (u32, u32),
661) -> bool {
662 const EPSILON: f32 = 0.01;
663 let (x, y, width, height) = viewport;
664 let (source_width, source_height) = source_size;
665 (x - x.round()).abs() <= EPSILON
666 && (y - y.round()).abs() <= EPSILON
667 && (width - source_width as f32).abs() <= EPSILON
668 && (height - source_height as f32).abs() <= EPSILON
669}
670
671type DirectShaderTailComposite<'a> = (&'a RenderEffect, &'a RuntimeShader, (f32, f32, f32, f32));
672
673fn direct_shader_tail_composite(
674 effect: &RenderEffect,
675 alpha: f32,
676 blend_mode: BlendMode,
677 dest_viewport: Option<(f32, f32, f32, f32)>,
678 sample_mode: CompositeSampleMode,
679 source_size: (u32, u32),
680) -> Option<DirectShaderTailComposite<'_>> {
681 let viewport = direct_shader_composite_viewport(
682 alpha,
683 blend_mode,
684 dest_viewport,
685 sample_mode,
686 source_size,
687 )?;
688 let RenderEffect::Chain { first, second } = effect else {
689 return None;
690 };
691 let RenderEffect::Shader { shader } = second.as_ref() else {
692 return None;
693 };
694 Some((first.as_ref(), shader, viewport))
695}
696
697fn hash_f32_for_cache<H: Hasher>(value: f32, state: &mut H) {
698 value.to_bits().hash(state);
699}
700
701fn hash_text_raster_geometry_for_cache<H: Hasher>(
702 rect: Rect,
703 static_text_motion: bool,
704 state: &mut H,
705) {
706 hash_f32_for_cache(rect.width, state);
707 hash_f32_for_cache(rect.height, state);
708 static_text_motion.hash(state);
709 if !static_text_motion {
710 hash_f32_for_cache(rect.x.fract(), state);
711 hash_f32_for_cache(rect.y.fract(), state);
712 }
713}
714
715fn text_raster_geometry_for_draw(
716 text_draw: &TextDraw,
717 root_scale: f32,
718) -> Option<(Rect, Rect, Option<Rect>, f32, bool)> {
719 if text_draw.text.is_empty()
720 || text_draw.rect.width <= 0.0
721 || text_draw.rect.height <= 0.0
722 || !root_scale.is_finite()
723 || root_scale <= 0.0
724 {
725 return None;
726 }
727
728 let text_scale = text_draw.scale * root_scale;
729 if !text_scale.is_finite() || text_scale <= 0.0 {
730 return None;
731 }
732
733 let static_text_motion = text_draw
734 .text_style
735 .paragraph_style
736 .text_motion
737 .unwrap_or(cranpose_ui::text::TextMotion::Static)
738 == cranpose_ui::text::TextMotion::Static;
739 let snap_delta = text_draw
740 .snap_anchor
741 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
742 .unwrap_or_default();
743 let logical_rect = text_draw.rect.translate(snap_delta.x, snap_delta.y);
744 let clip = text_draw.clip;
747 let mut raster_rect = Rect {
748 x: logical_rect.x * root_scale,
749 y: logical_rect.y * root_scale,
750 width: logical_rect.width * root_scale,
751 height: logical_rect.height * root_scale,
752 };
753 if text_draw.snap_anchor.is_some() {
754 raster_rect.x = canonicalize_device_coordinate(raster_rect.x);
755 raster_rect.y = canonicalize_device_coordinate(raster_rect.y);
756 }
757 if static_text_motion {
758 raster_rect.x = raster_rect.x.round();
759 raster_rect.y = raster_rect.y.round();
760 }
761 raster_rect.width = raster_rect.width.ceil().max(1.0);
762 raster_rect.height = raster_rect.height.ceil().max(1.0);
763 Some((
764 logical_rect,
765 raster_rect,
766 clip,
767 text_scale,
768 static_text_motion,
769 ))
770}
771
772fn text_draw_is_visible_in_viewport(
773 logical_rect: Rect,
774 clip: Option<Rect>,
775 viewport: ViewportUniformParams,
776 root_scale: f32,
777) -> bool {
778 draw_rect_is_visible_in_viewport(logical_rect, clip, viewport, root_scale)
779}
780
781fn text_draw_should_prewarm_in_viewport(
782 logical_rect: Rect,
783 clip: Option<Rect>,
784 viewport: ViewportUniformParams,
785 root_scale: f32,
786) -> bool {
787 if !root_scale.is_finite() || root_scale <= 0.0 {
788 return false;
789 }
790 let viewport_rect = Rect {
791 x: viewport.offset[0] / root_scale,
792 y: viewport.offset[1] / root_scale,
793 width: viewport.width as f32 / root_scale,
794 height: viewport.height as f32 / root_scale,
795 };
796 let margin_x = viewport_rect.width * TEXT_GLYPH_PREWARM_VIEWPORT_MULTIPLIER;
797 let margin_y = viewport_rect.height * TEXT_GLYPH_PREWARM_VIEWPORT_MULTIPLIER;
798 let prewarm_viewport = expand_rect(viewport_rect, margin_x, margin_y);
799 let prewarm_rect = match clip {
800 Some(clip) => expand_rect(clip, margin_x, margin_y).intersect(prewarm_viewport),
801 None => Some(prewarm_viewport),
802 };
803 prewarm_rect.is_some_and(|rect| logical_rect.intersect(rect).is_some())
804}
805
806fn expand_rect(rect: Rect, margin_x: f32, margin_y: f32) -> Rect {
807 Rect {
808 x: rect.x - margin_x,
809 y: rect.y - margin_y,
810 width: rect.width + margin_x * 2.0,
811 height: rect.height + margin_y * 2.0,
812 }
813}
814
815fn draw_rect_is_visible_in_viewport(
816 rect: Rect,
817 clip: Option<Rect>,
818 viewport: ViewportUniformParams,
819 root_scale: f32,
820) -> bool {
821 if !root_scale.is_finite() || root_scale <= 0.0 {
822 return false;
823 }
824 let viewport_rect = Rect {
825 x: viewport.offset[0] / root_scale,
826 y: viewport.offset[1] / root_scale,
827 width: viewport.width as f32 / root_scale,
828 height: viewport.height as f32 / root_scale,
829 };
830 let visible_rect = match clip {
831 Some(clip) => clip.intersect(viewport_rect),
832 None => Some(viewport_rect),
833 };
834 visible_rect.is_some_and(|visible| rect.intersect(visible).is_some())
835}
836
837fn shape_draw_is_visible_in_viewport(
838 shape: &DrawShape,
839 viewport: ViewportUniformParams,
840 root_scale: f32,
841) -> bool {
842 let snap_delta = shape
843 .snap_anchor
844 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
845 .unwrap_or_default();
846 let rect = quad_bounds(translate_quad(shape.quad, snap_delta));
847 let clip = shape.clip;
848 draw_rect_is_visible_in_viewport(rect, clip, viewport, root_scale)
849}
850
851fn cached_text_glyph_quad(
852 glyph: &SoftwareGlyphAtlasPlacement,
853 entry: GlyphAtlasEntry,
854 atlas_size: u32,
855) -> CachedTextGlyphQuad {
856 CachedTextGlyphQuad {
857 x: glyph.x,
858 y: glyph.y,
859 width: glyph.width,
860 height: glyph.height,
861 color: (
862 glyph.color.0.clamp(0.0, 1.0),
863 glyph.color.1.clamp(0.0, 1.0),
864 glyph.color.2.clamp(0.0, 1.0),
865 glyph.color.3.clamp(0.0, 1.0),
866 ),
867 uv: glyph_atlas_uv_rect(entry, atlas_size),
868 }
869}
870
871fn append_cached_text_glyph_quad(
872 source_raster_rect: Rect,
873 quad: &CachedTextGlyphQuad,
874 image_vertices: &mut Vec<Vertex>,
875 image_indices: &mut Vec<u32>,
876) -> bool {
877 if quad.width == 0 || quad.height == 0 || quad.color.3 <= 0.0 {
878 return false;
879 }
880
881 let base_vertex = image_vertices.len() as u32;
882 image_indices.extend_from_slice(&[
883 base_vertex,
884 base_vertex + 1,
885 base_vertex + 2,
886 base_vertex + 2,
887 base_vertex + 1,
888 base_vertex + 3,
889 ]);
890
891 let x0 = source_raster_rect.x + quad.x as f32;
892 let y0 = source_raster_rect.y + quad.y as f32;
893 let x1 = x0 + quad.width as f32;
894 let y1 = y0 + quad.height as f32;
895 let color = [quad.color.0, quad.color.1, quad.color.2, quad.color.3];
896
897 image_vertices.extend_from_slice(&[
898 Vertex {
899 position: [x0, y0],
900 color,
901 uv: [quad.uv.min[0], quad.uv.min[1]],
902 uv_bounds: quad.uv.sample_bounds,
903 },
904 Vertex {
905 position: [x1, y0],
906 color,
907 uv: [quad.uv.max[0], quad.uv.min[1]],
908 uv_bounds: quad.uv.sample_bounds,
909 },
910 Vertex {
911 position: [x0, y1],
912 color,
913 uv: [quad.uv.min[0], quad.uv.max[1]],
914 uv_bounds: quad.uv.sample_bounds,
915 },
916 Vertex {
917 position: [x1, y1],
918 color,
919 uv: [quad.uv.max[0], quad.uv.max[1]],
920 uv_bounds: quad.uv.sample_bounds,
921 },
922 ]);
923 true
924}
925
926fn cached_text_glyph_quad_logical_rect(
927 source_raster_rect: Rect,
928 quad: &CachedTextGlyphQuad,
929 root_scale: f32,
930) -> Option<Rect> {
931 if !root_scale.is_finite() || root_scale <= 0.0 {
932 return None;
933 }
934 Some(Rect {
935 x: (source_raster_rect.x + quad.x as f32) / root_scale,
936 y: (source_raster_rect.y + quad.y as f32) / root_scale,
937 width: quad.width as f32 / root_scale,
938 height: quad.height as f32 / root_scale,
939 })
940}
941
942fn cached_text_glyph_quad_is_visible_in_viewport(
943 source_raster_rect: Rect,
944 quad: &CachedTextGlyphQuad,
945 clip: Option<Rect>,
946 viewport: ViewportUniformParams,
947 root_scale: f32,
948) -> bool {
949 cached_text_glyph_quad_logical_rect(source_raster_rect, quad, root_scale)
950 .is_some_and(|rect| draw_rect_is_visible_in_viewport(rect, clip, viewport, root_scale))
951}
952
953#[derive(Clone, Copy, Debug, Eq, PartialEq)]
954enum TextGlyphDrawAction {
955 DrawVisible,
956 PrewarmOffscreen,
957 Skip,
958}
959
960fn text_glyph_draw_action(
961 is_visible: bool,
962 is_prewarm_candidate: bool,
963 allow_offscreen_prewarm: bool,
964) -> TextGlyphDrawAction {
965 if is_visible {
966 TextGlyphDrawAction::DrawVisible
967 } else if allow_offscreen_prewarm && is_prewarm_candidate {
968 TextGlyphDrawAction::PrewarmOffscreen
969 } else {
970 TextGlyphDrawAction::Skip
971 }
972}
973
974#[cfg(not(target_arch = "wasm32"))]
975fn should_use_retained_text_glyph_run(quads_len: usize, clip: Option<Rect>) -> bool {
976 clip.is_none() && quads_len >= MIN_RETAINED_TEXT_GLYPH_QUADS
977}
978
979#[cfg(not(target_arch = "wasm32"))]
980fn offscreen_text_glyph_prewarm_work_is_bounded(
981 cached_glyphs: Option<usize>,
982 text_len: usize,
983) -> bool {
984 match cached_glyphs {
985 Some(glyphs) => glyphs <= MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CACHED_GLYPHS,
986 None => text_len <= MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS,
987 }
988}
989
990#[cfg(not(target_arch = "wasm32"))]
991fn offscreen_text_glyph_prewarm_budget_exhausted(
992 start: Instant,
993 admitted_candidates: usize,
994) -> bool {
995 admitted_candidates >= MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CANDIDATES
996 || instant_ms(start, Instant::now()) >= OFFSCREEN_TEXT_GLYPH_PREWARM_BUDGET_MS
997}
998
999fn text_draws_for_ordered_range<'a>(
1000 ordered_items: &'a [(usize, SegmentDrawItem)],
1001 texts: &'a [TextDraw],
1002 start: usize,
1003 end: usize,
1004) -> Result<impl Iterator<Item = &'a TextDraw>, String> {
1005 let range_items = ordered_items
1006 .get(start..end)
1007 .ok_or_else(|| format!("text batch range {start}..{end} is outside ordered draw items"))?;
1008 for (_, item) in range_items {
1009 match item {
1010 SegmentDrawItem::Text(text_index) if *text_index < texts.len() => {}
1011 SegmentDrawItem::Text(text_index) => {
1012 return Err(format!(
1013 "text batch references missing text draw index: {text_index}"
1014 ));
1015 }
1016 _ => return Err(format!("text batch contains non-text draw item: {item:?}")),
1017 }
1018 }
1019
1020 Ok(range_items.iter().filter_map(move |(_, item)| match item {
1021 SegmentDrawItem::Text(text_index) => texts.get(*text_index),
1022 _ => None,
1023 }))
1024}
1025
1026const SHADOW_CACHE_DEVICE_QUANT: f32 = 16.0;
1031
1032fn hash_shadow_device_offset<H: Hasher>(value: f32, origin: f32, root_scale: f32, state: &mut H) {
1033 let quantized = ((value - origin) * root_scale * SHADOW_CACHE_DEVICE_QUANT).round();
1034 (quantized as i64).hash(state);
1035}
1036
1037fn hash_shadow_device_rect<H: Hasher>(
1038 rect: Rect,
1039 origin_x: f32,
1040 origin_y: f32,
1041 root_scale: f32,
1042 state: &mut H,
1043) {
1044 hash_shadow_device_offset(rect.x, origin_x, root_scale, state);
1045 hash_shadow_device_offset(rect.y, origin_y, root_scale, state);
1046 hash_shadow_device_offset(rect.width, 0.0, root_scale, state);
1047 hash_shadow_device_offset(rect.height, 0.0, root_scale, state);
1048}
1049
1050fn hash_shape_shadow_item<H: Hasher>(
1051 shape: &DrawShape,
1052 blend_mode: BlendMode,
1053 origin_x: f32,
1054 origin_y: f32,
1055 root_scale: f32,
1056 state: &mut H,
1057) {
1058 hash_shadow_device_rect(shape.rect, origin_x, origin_y, root_scale, state);
1059 hash_shadow_device_rect(shape.local_rect, origin_x, origin_y, root_scale, state);
1060 for point in shape.quad {
1061 hash_shadow_device_offset(point[0], origin_x, root_scale, state);
1062 hash_shadow_device_offset(point[1], origin_y, root_scale, state);
1063 }
1064 match shape.snap_anchor {
1065 Some(anchor) => {
1066 1u8.hash(state);
1067 hash_shadow_device_offset(anchor.origin.x, origin_x, root_scale, state);
1068 hash_shadow_device_offset(anchor.origin.y, origin_y, root_scale, state);
1069 hash_f32_for_cache(anchor.device_pixel_step, state);
1070 }
1071 None => 0u8.hash(state),
1072 }
1073 shape.brush.render_hash().hash(state);
1074 match shape.shape {
1075 Some(corner_shape) => {
1076 1u8.hash(state);
1077 corner_shape.radii().render_hash().hash(state);
1078 }
1079 None => 0u8.hash(state),
1080 }
1081 match shape.clip {
1082 Some(clip) => {
1083 1u8.hash(state);
1084 hash_shadow_device_rect(clip, origin_x, origin_y, root_scale, state);
1085 }
1086 None => 0u8.hash(state),
1087 }
1088 blend_mode.hash(state);
1089 shape.blend_mode.hash(state);
1090}
1091
1092fn shape_shadow_content_hash(shapes: &[(DrawShape, BlendMode)], root_scale: f32) -> u64 {
1093 let mut hasher = FxHasher::default();
1094 let origin = shape_shadow_bounds(shapes).unwrap_or(Rect {
1099 x: 0.0,
1100 y: 0.0,
1101 width: 0.0,
1102 height: 0.0,
1103 });
1104
1105 shapes.len().hash(&mut hasher);
1106 for (shape, blend_mode) in shapes {
1107 hash_shape_shadow_item(
1108 shape,
1109 *blend_mode,
1110 origin.x,
1111 origin.y,
1112 root_scale,
1113 &mut hasher,
1114 );
1115 }
1116 hasher.finish()
1117}
1118
1119fn shape_shadow_surface_cache_key(
1120 shapes: &[(DrawShape, BlendMode)],
1121 device_bounds: DevicePixelBounds,
1122 pixel_radius: f32,
1123 root_scale: f32,
1124) -> Option<ShadowSurfaceCacheKey> {
1125 (root_scale.is_finite() && root_scale > 0.0).then(|| ShadowSurfaceCacheKey {
1126 content_hash: shape_shadow_content_hash(shapes, root_scale),
1127 pixel_size: [device_bounds.width, device_bounds.height],
1128 root_scale_bits: root_scale.to_bits(),
1129 blur_radius_bits: pixel_radius.to_bits(),
1130 })
1131}
1132
1133fn shape_shadow_bounds(shapes: &[(DrawShape, BlendMode)]) -> Option<Rect> {
1134 shapes
1135 .iter()
1136 .map(|(shape, _)| shape.rect)
1137 .reduce(|a, b| Rect {
1138 x: a.x.min(b.x),
1139 y: a.y.min(b.y),
1140 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
1141 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
1142 })
1143}
1144
1145fn shared_shape_shadow_snap_anchor(shapes: &[(DrawShape, BlendMode)]) -> Option<SnapAnchor> {
1146 let anchor = shapes.first()?.0.snap_anchor?;
1147 shapes
1148 .iter()
1149 .all(|(shape, _)| shape.snap_anchor == Some(anchor))
1150 .then_some(anchor)
1151}
1152
1153fn shadow_draw_bounds(shadow: &ShadowDraw) -> Option<Rect> {
1154 shadow
1155 .shapes
1156 .iter()
1157 .map(|(shape, _)| shape.rect)
1158 .chain(shadow.texts.iter().map(|text| text.rect))
1159 .reduce(|a, b| Rect {
1160 x: a.x.min(b.x),
1161 y: a.y.min(b.y),
1162 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
1163 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
1164 })
1165}
1166
1167fn shadow_draw_may_render(
1168 shadow: &ShadowDraw,
1169 width: u32,
1170 height: u32,
1171 root_scale: f32,
1172 max_texture_dim: u32,
1173) -> bool {
1174 if shadow.texts.is_empty() && !shadow.shapes.is_empty() && shadow.blur_radius > 0.0 {
1175 return shape_shadow_surface_plan(
1176 &shadow.shapes,
1177 shadow.clip,
1178 shadow.blur_radius,
1179 width,
1180 height,
1181 root_scale,
1182 max_texture_dim,
1183 )
1184 .is_some();
1185 }
1186
1187 let Some(bounds) = shadow_draw_bounds(shadow) else {
1188 return false;
1189 };
1190 let blur_margin = blur_extent_margin(shadow.blur_radius);
1191 let mut visible_bounds = Rect {
1192 x: bounds.x - blur_margin,
1193 y: bounds.y - blur_margin,
1194 width: bounds.width + blur_margin * 2.0,
1195 height: bounds.height + blur_margin * 2.0,
1196 };
1197 if let Some(clip) = shadow.clip {
1198 let clip_expanded = Rect {
1199 x: clip.x - blur_margin,
1200 y: clip.y - blur_margin,
1201 width: clip.width + blur_margin * 2.0,
1202 height: clip.height + blur_margin * 2.0,
1203 };
1204 let Some(intersection) = visible_bounds.intersect(clip_expanded) else {
1205 return false;
1206 };
1207 visible_bounds = intersection;
1208 }
1209
1210 scissor_rect_for_rect(visible_bounds, root_scale, width, height).is_some()
1211}
1212
1213fn shape_shadow_surface_plan(
1214 shapes: &[(DrawShape, BlendMode)],
1215 clip: Option<Rect>,
1216 blur_radius: f32,
1217 width: u32,
1218 height: u32,
1219 root_scale: f32,
1220 max_texture_dim: u32,
1221) -> Option<ShapeShadowSurfacePlan> {
1222 let shape_bounds = shape_shadow_bounds(shapes)?;
1223 let blur_margin = blur_extent_margin(blur_radius);
1224 let source_blur_bounds = Rect {
1225 x: shape_bounds.x - blur_margin,
1226 y: shape_bounds.y - blur_margin,
1227 width: shape_bounds.width + blur_margin * 2.0,
1228 height: shape_bounds.height + blur_margin * 2.0,
1229 };
1230
1231 let mut visible_blur_bounds = source_blur_bounds;
1232 if let Some(clip) = clip {
1233 let clip_expanded = Rect {
1234 x: clip.x - blur_margin,
1235 y: clip.y - blur_margin,
1236 width: clip.width + blur_margin * 2.0,
1237 height: clip.height + blur_margin * 2.0,
1238 };
1239 visible_blur_bounds = visible_blur_bounds.intersect(clip_expanded)?;
1240 }
1241
1242 let processing_scissor = scissor_rect_for_rect(visible_blur_bounds, root_scale, width, height);
1243 processing_scissor?;
1244 let visible_device_bounds =
1245 device_pixel_bounds_for_rect(visible_blur_bounds, width, height, root_scale)?;
1246 let source_device_bounds = translation_stable_anchored_device_pixel_bounds(
1247 source_blur_bounds,
1248 shared_shape_shadow_snap_anchor(shapes),
1249 root_scale,
1250 max_texture_dim,
1251 )
1252 .unwrap_or(visible_device_bounds);
1253
1254 Some(ShapeShadowSurfacePlan {
1255 source_device_bounds,
1256 processing_scissor,
1257 pixel_radius: blur_radius * root_scale,
1258 })
1259}
1260
1261fn is_render_effect_supported(effect: &RenderEffect) -> bool {
1262 match effect {
1263 RenderEffect::Blur { .. } => true,
1264 RenderEffect::Offset { .. } => true,
1265 RenderEffect::Shader { .. } => true,
1266 RenderEffect::Chain { first, second } => {
1267 is_render_effect_supported(first) && is_render_effect_supported(second)
1268 }
1269 }
1270}
1271
1272fn resolve_gradient_point(origin: f32, extent: f32, value: f32) -> f32 {
1273 if value.is_finite() {
1274 origin + value
1275 } else if value.is_sign_positive() {
1276 origin + extent
1277 } else {
1278 origin
1279 }
1280}
1281
1282fn gradient_tile_mode_value(tile_mode: TileMode) -> u32 {
1283 match tile_mode {
1284 TileMode::Clamp => 0,
1285 TileMode::Repeated => 1,
1286 TileMode::Mirror => 2,
1287 TileMode::Decal => 3,
1288 }
1289}
1290
1291#[cfg(not(target_arch = "wasm32"))]
1292fn shape_shader_source(batch_limits: ShapeBatchLimits) -> Cow<'static, str> {
1293 if batch_limits.storage {
1297 return Cow::Owned(
1298 shaders::SHADER
1299 .replace(
1300 "var<uniform> shape_data: array<ShapeData, 102>;",
1301 "var<storage, read> shape_data: array<ShapeData>;",
1302 )
1303 .replace(
1304 "var<uniform> gradient_stops: array<GradientStop, 256>;",
1305 "var<storage, read> gradient_stops: array<GradientStop>;\n\n\
1312 @group(1) @binding(3)\n\
1313 var<storage, read> paint: array<vec4<f32>>;",
1314 )
1315 .replace(
1316 "output.color = shape.color;",
1317 "output.color = \
1318 select(shape.color, paint[shape_idx], similarity.paint_select > 0.5);",
1319 ),
1320 );
1321 }
1322 Cow::Owned(
1323 shaders::SHADER
1324 .replace(
1325 "array<ShapeData, 102>",
1326 &format!("array<ShapeData, {}>", batch_limits.max_shapes_per_batch),
1327 )
1328 .replace(
1329 "array<GradientStop, 256>",
1330 &format!("array<GradientStop, {}>", batch_limits.max_gradient_stops),
1331 ),
1332 )
1333}
1334
1335#[cfg(target_arch = "wasm32")]
1336fn shape_shader_source(_batch_limits: ShapeBatchLimits) -> Cow<'static, str> {
1337 Cow::Borrowed(shaders::SHADER)
1338}
1339
1340fn create_shape_pipeline(
1341 device: &wgpu::Device,
1342 surface_format: wgpu::TextureFormat,
1343 uniform_layout: &wgpu::BindGroupLayout,
1344 shape_layout: &wgpu::BindGroupLayout,
1345 blend_mode: BlendMode,
1346 batch_limits: ShapeBatchLimits,
1347) -> wgpu::RenderPipeline {
1348 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1349 label: Some("Shape Shader"),
1350 source: wgpu::ShaderSource::Wgsl(shape_shader_source(batch_limits)),
1351 });
1352
1353 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1354 label: Some("Render Pipeline Layout"),
1355 bind_group_layouts: &[Some(uniform_layout), Some(shape_layout)],
1356 immediate_size: 0,
1357 });
1358
1359 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1360 label: Some("Render Pipeline"),
1361 layout: Some(&pipeline_layout),
1362 vertex: wgpu::VertexState {
1363 module: &shader,
1364 entry_point: Some("vs_main"),
1365 compilation_options: wgpu::PipelineCompilationOptions::default(),
1366 buffers: &[],
1369 },
1370 fragment: Some(wgpu::FragmentState {
1371 module: &shader,
1372 entry_point: Some("fs_main"),
1373 compilation_options: wgpu::PipelineCompilationOptions::default(),
1374 targets: &[Some(wgpu::ColorTargetState {
1375 format: surface_format,
1376 blend: Some(blend_state_for_mode(blend_mode)),
1377 write_mask: wgpu::ColorWrites::ALL,
1378 })],
1379 }),
1380 primitive: wgpu::PrimitiveState {
1381 topology: wgpu::PrimitiveTopology::TriangleList,
1382 strip_index_format: None,
1383 front_face: wgpu::FrontFace::Ccw,
1384 cull_mode: None,
1385 unclipped_depth: false,
1386 polygon_mode: wgpu::PolygonMode::Fill,
1387 conservative: false,
1388 },
1389 depth_stencil: None,
1390 multisample: wgpu::MultisampleState::default(),
1391 multiview_mask: None,
1392 cache: None,
1393 })
1394}
1395
1396#[cfg(not(target_arch = "wasm32"))]
1403fn create_mesh_shape_pipeline(
1404 device: &wgpu::Device,
1405 surface_format: wgpu::TextureFormat,
1406 uniform_layout: &wgpu::BindGroupLayout,
1407 shape_layout: &wgpu::BindGroupLayout,
1408 batch_limits: ShapeBatchLimits,
1409) -> wgpu::RenderPipeline {
1410 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1411 label: Some("Shape Mesh Shader"),
1412 source: wgpu::ShaderSource::Wgsl(shape_shader_source(batch_limits)),
1413 });
1414
1415 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1416 label: Some("Mesh Render Pipeline Layout"),
1417 bind_group_layouts: &[Some(uniform_layout), Some(shape_layout)],
1418 immediate_size: 0,
1419 });
1420
1421 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1422 label: Some("Retained Mesh Pipeline"),
1423 layout: Some(&pipeline_layout),
1424 vertex: wgpu::VertexState {
1425 module: &shader,
1426 entry_point: Some("vs_mesh"),
1427 compilation_options: wgpu::PipelineCompilationOptions::default(),
1428 buffers: &[MeshVertex::desc()],
1429 },
1430 fragment: Some(wgpu::FragmentState {
1431 module: &shader,
1432 entry_point: Some("fs_main"),
1433 compilation_options: wgpu::PipelineCompilationOptions::default(),
1434 targets: &[Some(wgpu::ColorTargetState {
1435 format: surface_format,
1436 blend: Some(blend_state_for_mode(BlendMode::SrcOver)),
1437 write_mask: wgpu::ColorWrites::ALL,
1438 })],
1439 }),
1440 primitive: wgpu::PrimitiveState {
1441 topology: wgpu::PrimitiveTopology::TriangleList,
1442 strip_index_format: None,
1443 front_face: wgpu::FrontFace::Ccw,
1444 cull_mode: None,
1445 unclipped_depth: false,
1446 polygon_mode: wgpu::PolygonMode::Fill,
1447 conservative: false,
1448 },
1449 depth_stencil: None,
1450 multisample: wgpu::MultisampleState::default(),
1451 multiview_mask: None,
1452 cache: None,
1453 })
1454}
1455
1456#[cfg(not(target_arch = "wasm32"))]
1464fn create_instanced_shape_pipeline(
1465 device: &wgpu::Device,
1466 surface_format: wgpu::TextureFormat,
1467 uniform_layout: &wgpu::BindGroupLayout,
1468 shape_layout: &wgpu::BindGroupLayout,
1469 blend_mode: BlendMode,
1470 batch_limits: ShapeBatchLimits,
1471) -> wgpu::RenderPipeline {
1472 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1473 label: Some("Shape Instanced Shader"),
1474 source: wgpu::ShaderSource::Wgsl(shape_shader_source(batch_limits)),
1475 });
1476
1477 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1478 label: Some("Instanced Render Pipeline Layout"),
1479 bind_group_layouts: &[Some(uniform_layout), Some(shape_layout)],
1480 immediate_size: 0,
1481 });
1482
1483 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1484 label: Some("Instanced Render Pipeline"),
1485 layout: Some(&pipeline_layout),
1486 vertex: wgpu::VertexState {
1487 module: &shader,
1488 entry_point: Some("vs_shape_instanced"),
1489 compilation_options: wgpu::PipelineCompilationOptions::default(),
1490 buffers: &[],
1494 },
1495 fragment: Some(wgpu::FragmentState {
1496 module: &shader,
1497 entry_point: Some("fs_main"),
1498 compilation_options: wgpu::PipelineCompilationOptions::default(),
1499 targets: &[Some(wgpu::ColorTargetState {
1500 format: surface_format,
1501 blend: Some(blend_state_for_mode(blend_mode)),
1502 write_mask: wgpu::ColorWrites::ALL,
1503 })],
1504 }),
1505 primitive: wgpu::PrimitiveState {
1506 topology: wgpu::PrimitiveTopology::TriangleList,
1507 strip_index_format: None,
1508 front_face: wgpu::FrontFace::Ccw,
1509 cull_mode: None,
1510 unclipped_depth: false,
1511 polygon_mode: wgpu::PolygonMode::Fill,
1512 conservative: false,
1513 },
1514 depth_stencil: None,
1515 multisample: wgpu::MultisampleState::default(),
1516 multiview_mask: None,
1517 cache: None,
1518 })
1519}
1520
1521fn create_image_pipeline(
1522 device: &wgpu::Device,
1523 surface_format: wgpu::TextureFormat,
1524 uniform_layout: &wgpu::BindGroupLayout,
1525 image_layout: &wgpu::BindGroupLayout,
1526 blend_mode: BlendMode,
1527) -> wgpu::RenderPipeline {
1528 let image_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1529 label: Some("Image Shader"),
1530 source: wgpu::ShaderSource::Wgsl(shaders::IMAGE_SHADER.into()),
1531 });
1532
1533 let image_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1534 label: Some("Image Pipeline Layout"),
1535 bind_group_layouts: &[Some(uniform_layout), Some(image_layout)],
1536 immediate_size: 0,
1537 });
1538
1539 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1540 label: Some("Image Pipeline"),
1541 layout: Some(&image_pipeline_layout),
1542 vertex: wgpu::VertexState {
1543 module: &image_shader,
1544 entry_point: Some("image_vs_main"),
1545 compilation_options: wgpu::PipelineCompilationOptions::default(),
1546 buffers: &[Vertex::desc()],
1547 },
1548 fragment: Some(wgpu::FragmentState {
1549 module: &image_shader,
1550 entry_point: Some("image_fs_main"),
1551 compilation_options: wgpu::PipelineCompilationOptions::default(),
1552 targets: &[Some(wgpu::ColorTargetState {
1553 format: surface_format,
1554 blend: Some(blend_state_for_mode(blend_mode)),
1555 write_mask: wgpu::ColorWrites::ALL,
1556 })],
1557 }),
1558 primitive: wgpu::PrimitiveState {
1559 topology: wgpu::PrimitiveTopology::TriangleList,
1560 strip_index_format: None,
1561 front_face: wgpu::FrontFace::Ccw,
1562 cull_mode: None,
1563 unclipped_depth: false,
1564 polygon_mode: wgpu::PolygonMode::Fill,
1565 conservative: false,
1566 },
1567 depth_stencil: None,
1568 multisample: wgpu::MultisampleState::default(),
1569 multiview_mask: None,
1570 cache: None,
1571 })
1572}
1573
1574fn create_glyph_atlas_pipeline(
1575 device: &wgpu::Device,
1576 surface_format: wgpu::TextureFormat,
1577 uniform_layout: &wgpu::BindGroupLayout,
1578 image_layout: &wgpu::BindGroupLayout,
1579) -> wgpu::RenderPipeline {
1580 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1581 label: Some("Glyph Atlas Shader"),
1582 source: wgpu::ShaderSource::Wgsl(shaders::GLYPH_ATLAS_SHADER.into()),
1583 });
1584
1585 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1586 label: Some("Glyph Atlas Pipeline Layout"),
1587 bind_group_layouts: &[Some(uniform_layout), Some(image_layout)],
1588 immediate_size: 0,
1589 });
1590
1591 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1592 label: Some("Glyph Atlas Pipeline"),
1593 layout: Some(&pipeline_layout),
1594 vertex: wgpu::VertexState {
1595 module: &shader,
1596 entry_point: Some("glyph_atlas_vs_main"),
1597 compilation_options: wgpu::PipelineCompilationOptions::default(),
1598 buffers: &[Vertex::desc()],
1599 },
1600 fragment: Some(wgpu::FragmentState {
1601 module: &shader,
1602 entry_point: Some("glyph_atlas_fs_main"),
1603 compilation_options: wgpu::PipelineCompilationOptions::default(),
1604 targets: &[Some(wgpu::ColorTargetState {
1605 format: surface_format,
1606 blend: Some(blend_state_for_mode(BlendMode::SrcOver)),
1607 write_mask: wgpu::ColorWrites::ALL,
1608 })],
1609 }),
1610 primitive: wgpu::PrimitiveState {
1611 topology: wgpu::PrimitiveTopology::TriangleList,
1612 strip_index_format: None,
1613 front_face: wgpu::FrontFace::Ccw,
1614 cull_mode: None,
1615 unclipped_depth: false,
1616 polygon_mode: wgpu::PolygonMode::Fill,
1617 conservative: false,
1618 },
1619 depth_stencil: None,
1620 multisample: wgpu::MultisampleState::default(),
1621 multiview_mask: None,
1622 cache: None,
1623 })
1624}
1625
1626#[repr(C)]
1627#[derive(Copy, Clone, Debug, Pod, Zeroable)]
1628struct Vertex {
1629 position: [f32; 2],
1630 color: [f32; 4],
1631 uv: [f32; 2],
1632 uv_bounds: [f32; 4],
1633}
1634
1635impl Vertex {
1636 const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
1637 0 => Float32x2,
1638 1 => Float32x4,
1639 2 => Float32x2,
1640 3 => Float32x4
1641 ];
1642
1643 fn desc() -> wgpu::VertexBufferLayout<'static> {
1644 wgpu::VertexBufferLayout {
1645 array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
1646 step_mode: wgpu::VertexStepMode::Vertex,
1647 attributes: &Self::ATTRIBS,
1648 }
1649 }
1650}
1651
1652#[repr(C)]
1653#[derive(Copy, Clone, Debug, Pod, Zeroable)]
1654struct Uniforms {
1655 viewport: [f32; 2],
1656 viewport_offset: [f32; 2],
1657}
1658
1659#[repr(C)]
1665#[derive(Copy, Clone, Debug, Pod, Zeroable)]
1666struct ShapeData {
1667 rect: [f32; 4], radii: [f32; 4],
1672 gradient_params: [f32; 4], clip_rect: [f32; 4], stroke_params: [f32; 4],
1677 arc_params: [f32; 4],
1679 quad01: [f32; 4],
1681 quad23: [f32; 4],
1683 color: [f32; 4],
1685 brush_type: u32, gradient_start: u32, gradient_count: u32, gradient_tile_mode: u32, }
1690
1691const SHAPE_KIND_FILL: u32 = 0;
1693const SHAPE_KIND_STROKE: u32 = 1;
1694const SHAPE_KIND_ARC: u32 = 2;
1695
1696fn stroke_cap_code(cap: StrokeCap) -> u32 {
1697 match cap {
1698 StrokeCap::Butt => 0,
1699 StrokeCap::Round => 1,
1700 StrokeCap::Square => 2,
1701 }
1702}
1703
1704fn stroke_join_code(join: StrokeJoin) -> u32 {
1705 match join {
1706 StrokeJoin::Miter => 0,
1707 StrokeJoin::Round => 1,
1708 StrokeJoin::Bevel => 2,
1709 }
1710}
1711
1712fn pack_shape_flags(kind: u32, cap: StrokeCap, join: StrokeJoin) -> f32 {
1718 ((kind & 3) | (stroke_cap_code(cap) << 2) | (stroke_join_code(join) << 4)) as f32
1719}
1720
1721#[cfg(not(target_arch = "wasm32"))]
1729static SHAPE_CONVERT_TUNER: crate::cost_tuner::CostTuner =
1730 crate::cost_tuner::CostTuner::new("shape-convert", 256, 400_000);
1731
1732#[cfg(not(target_arch = "wasm32"))]
1733pub(crate) fn shape_convert_worker_count() -> usize {
1734 static WORKERS: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
1735 *WORKERS.get_or_init(|| {
1736 let cpus = std::thread::available_parallelism()
1737 .map(|count| count.get())
1738 .unwrap_or(1);
1739 let workers = cpus.clamp(1, 4);
1740 log::info!("[shape-convert] fan-out width {workers} (available parallelism {cpus})");
1744 workers
1745 })
1746}
1747
1748#[cfg(target_arch = "wasm32")]
1749pub(crate) fn shape_convert_worker_count() -> usize {
1750 1
1751}
1752
1753fn shape_gradient_stop_count(shape: &DrawShape) -> usize {
1754 match &shape.brush {
1755 Brush::Solid(_) => 0,
1756 Brush::LinearGradient { colors, .. }
1757 | Brush::RadialGradient { colors, .. }
1758 | Brush::SweepGradient { colors, .. } => colors.len(),
1759 }
1760}
1761
1762fn convert_shape_into_slots(
1767 shape: &DrawShape,
1768 root_scale: f32,
1769 gradient_start: u32,
1770 shape_out: &mut ShapeData,
1771 gradient_out: &mut [GradientStop],
1772) {
1773 let snap_delta = shape
1774 .snap_anchor
1775 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
1776 .unwrap_or_default();
1777 let local_rect = shape.local_rect.translate(snap_delta.x, snap_delta.y);
1778 let quad = translate_quad(shape.quad, snap_delta);
1779 let clip = shape.clip;
1782 let canonicalize = shape.snap_anchor.is_some();
1783 let device_local_rect = if canonicalize {
1784 canonicalized_scaled_rect(local_rect, root_scale)
1785 } else {
1786 Rect {
1787 x: local_rect.x * root_scale,
1788 y: local_rect.y * root_scale,
1789 width: local_rect.width * root_scale,
1790 height: local_rect.height * root_scale,
1791 }
1792 };
1793 let device_quad = if canonicalize {
1794 canonicalized_scaled_quad(quad, root_scale)
1795 } else {
1796 scaled_quad(quad, root_scale)
1797 };
1798 let canonicalize_brush_coordinate = |value| {
1799 if canonicalize {
1800 canonicalize_device_coordinate(value)
1801 } else {
1802 value
1803 }
1804 };
1805
1806 let clip_rect = if let Some(clip) = clip {
1808 let device_clip = if canonicalize {
1809 canonicalized_scaled_rect(clip, root_scale)
1810 } else {
1811 Rect {
1812 x: clip.x * root_scale,
1813 y: clip.y * root_scale,
1814 width: clip.width * root_scale,
1815 height: clip.height * root_scale,
1816 }
1817 };
1818 [
1819 device_clip.x,
1820 device_clip.y,
1821 device_clip.width,
1822 device_clip.height,
1823 ]
1824 } else {
1825 [0.0, 0.0, 0.0, 0.0]
1826 };
1827
1828 let mut fill_gradient_entries = |colors: &[Color], stops: Option<&[f32]>| {
1830 let count = colors.len();
1831 let explicit_stops = stops.filter(|values| values.len() == count);
1832 for (index, color) in colors.iter().enumerate() {
1833 let position = explicit_stops
1834 .map(|values| values[index])
1835 .unwrap_or_else(|| {
1836 if count <= 1 {
1837 0.0
1838 } else {
1839 index as f32 / (count - 1) as f32
1840 }
1841 });
1842 gradient_out[index] = GradientStop {
1843 color: [color.r(), color.g(), color.b(), color.a()],
1844 position: [position, 0.0, 0.0, 0.0],
1845 };
1846 }
1847 count as u32
1848 };
1849 let mut gradient_params = [0.0f32; 4];
1850 let (brush_type, gradient_count, gradient_tile_mode) = match &shape.brush {
1851 Brush::Solid(_) => (0u32, 0u32, gradient_tile_mode_value(TileMode::Clamp)),
1852 Brush::LinearGradient {
1853 colors,
1854 stops,
1855 start,
1856 end,
1857 tile_mode,
1858 } => {
1859 let count = fill_gradient_entries(colors, stops.as_deref());
1860 gradient_params = [
1861 canonicalize_brush_coordinate(resolve_gradient_point(
1862 device_local_rect.x,
1863 device_local_rect.width,
1864 start.x * root_scale,
1865 )),
1866 canonicalize_brush_coordinate(resolve_gradient_point(
1867 device_local_rect.y,
1868 device_local_rect.height,
1869 start.y * root_scale,
1870 )),
1871 canonicalize_brush_coordinate(resolve_gradient_point(
1872 device_local_rect.x,
1873 device_local_rect.width,
1874 end.x * root_scale,
1875 )),
1876 canonicalize_brush_coordinate(resolve_gradient_point(
1877 device_local_rect.y,
1878 device_local_rect.height,
1879 end.y * root_scale,
1880 )),
1881 ];
1882 (1u32, count, gradient_tile_mode_value(*tile_mode))
1883 }
1884 Brush::RadialGradient {
1885 colors,
1886 stops,
1887 center,
1888 radius,
1889 tile_mode,
1890 } => {
1891 let count = fill_gradient_entries(colors, stops.as_deref());
1892 gradient_params = [
1893 canonicalize_brush_coordinate(device_local_rect.x + center.x * root_scale),
1894 canonicalize_brush_coordinate(device_local_rect.y + center.y * root_scale),
1895 (radius * root_scale).max(f32::EPSILON),
1896 0.0,
1897 ];
1898 (2u32, count, gradient_tile_mode_value(*tile_mode))
1899 }
1900 Brush::SweepGradient {
1901 colors,
1902 stops,
1903 center,
1904 } => {
1905 let count = fill_gradient_entries(colors, stops.as_deref());
1906 gradient_params = [
1907 canonicalize_brush_coordinate(device_local_rect.x + center.x * root_scale),
1908 canonicalize_brush_coordinate(device_local_rect.y + center.y * root_scale),
1909 0.0,
1910 0.0,
1911 ];
1912 (3u32, count, gradient_tile_mode_value(TileMode::Clamp))
1913 }
1914 };
1915
1916 let stroke_outset = shape
1921 .stroke
1922 .map(|stroke| stroke.half_width())
1923 .unwrap_or(0.0);
1924 let geometry_width = (local_rect.width - stroke_outset * 2.0).max(0.0);
1925 let geometry_height = (local_rect.height - stroke_outset * 2.0).max(0.0);
1926
1927 let radii = if let Some(arc) = shape.arc {
1928 if arc.sweep_angle >= cranpose_ui_graphics::TAU && arc.start_angle == 0.0 {
1938 [0.0, -1.0, 0.0, -1.0]
1939 } else {
1940 let half_sweep = arc.sweep_angle.clamp(0.0, cranpose_ui_graphics::TAU) * 0.5;
1941 let (mid_sin, mid_cos) = (arc.start_angle + half_sweep).sin_cos();
1942 let (half_sin, half_cos) = half_sweep.sin_cos();
1943 [mid_sin, mid_cos, half_sin.max(0.0), half_cos]
1944 }
1945 } else if let Some(rounded) = shape.shape {
1946 let resolved = rounded.resolve(geometry_width, geometry_height);
1947 [
1948 resolved.top_left * root_scale,
1949 resolved.top_right * root_scale,
1950 resolved.bottom_left * root_scale,
1951 resolved.bottom_right * root_scale,
1952 ]
1953 } else {
1954 [0.0, 0.0, 0.0, 0.0]
1955 };
1956
1957 let device_rect = [
1958 device_local_rect.x,
1959 device_local_rect.y,
1960 device_local_rect.width,
1961 device_local_rect.height,
1962 ];
1963
1964 let (stroke_params, arc_params) = match (shape.arc, shape.stroke) {
1968 (Some(arc), _) => (
1969 [
1970 0.0,
1971 pack_shape_flags(SHAPE_KIND_ARC, arc.cap, StrokeJoin::Miter),
1972 arc.outer_radius * root_scale,
1973 arc.inner_radius * root_scale,
1974 ],
1975 [
1976 (arc.center.x + snap_delta.x) * root_scale,
1977 (arc.center.y + snap_delta.y) * root_scale,
1978 arc.start_angle,
1979 arc.sweep_angle,
1980 ],
1981 ),
1982 (None, Some(stroke)) => (
1983 [
1984 stroke.width.max(0.0) * root_scale,
1985 pack_shape_flags(SHAPE_KIND_STROKE, stroke.cap, stroke.join),
1986 0.0,
1987 0.0,
1988 ],
1989 [0.0; 4],
1990 ),
1991 (None, None) => (
1992 [
1993 0.0,
1994 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter),
1995 0.0,
1996 0.0,
1997 ],
1998 [0.0; 4],
1999 ),
2000 };
2001
2002 let color = match &shape.brush {
2003 Brush::Solid(c) => [c.r(), c.g(), c.b(), c.a()],
2004 Brush::LinearGradient { colors, .. } => {
2005 let first = colors.first().unwrap_or(&Color(1.0, 1.0, 1.0, 1.0));
2006 [first.r(), first.g(), first.b(), first.a()]
2007 }
2008 Brush::RadialGradient { colors, .. } | Brush::SweepGradient { colors, .. } => {
2009 let first = colors.first().unwrap_or(&Color(1.0, 1.0, 1.0, 1.0));
2010 [first.r(), first.g(), first.b(), first.a()]
2011 }
2012 };
2013
2014 *shape_out = ShapeData {
2015 rect: device_rect,
2016 radii,
2017 gradient_params,
2018 clip_rect,
2019 stroke_params,
2020 arc_params,
2021 quad01: [
2022 device_quad[0][0],
2023 device_quad[0][1],
2024 device_quad[1][0],
2025 device_quad[1][1],
2026 ],
2027 quad23: [
2028 device_quad[2][0],
2029 device_quad[2][1],
2030 device_quad[3][0],
2031 device_quad[3][1],
2032 ],
2033 color,
2034 brush_type,
2035 gradient_start,
2036 gradient_count,
2037 gradient_tile_mode,
2038 };
2039}
2040
2041fn quad_area_diag_enabled() -> bool {
2048 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2049 *ENABLED.get_or_init(|| std::env::var_os("CRANPOSE_QUAD_AREA_DIAG").is_some())
2050}
2051
2052fn convert_shapes_into_outputs(
2058 shape_refs: &[&DrawShape],
2059 gradient_offsets: &[u32],
2060 root_scale: f32,
2061 shape_data_out: &mut [ShapeData],
2062 gradients_out: &mut [GradientStop],
2063) {
2064 let shape_count = shape_refs.len();
2065 #[cfg(not(target_arch = "wasm32"))]
2066 let convert_started = Instant::now();
2067 #[cfg(not(target_arch = "wasm32"))]
2068 let parallel =
2069 SHAPE_CONVERT_TUNER.choose_parallel(shape_count) && shape_convert_worker_count() > 1;
2070 if quad_area_diag_enabled() {
2071 let quad_area = |q: [[f32; 2]; 4]| {
2072 let poly = [q[0], q[1], q[3], q[2]];
2074 let mut twice = 0.0f64;
2075 for i in 0..4 {
2076 let a = poly[i];
2077 let b = poly[(i + 1) % 4];
2078 twice += a[0] as f64 * b[1] as f64 - b[0] as f64 * a[1] as f64;
2079 }
2080 twice.abs() * 0.5
2081 };
2082 let mut arc_quad = 0.0f64; let mut arc_band = 0.0f64; let mut arc_count = 0usize;
2085 let mut ring_count = 0usize;
2086 let mut other_quad = 0.0f64;
2087 let mut other_count = 0usize;
2088 let mut top_other: Vec<(f64, usize)> = Vec::new();
2091 for (index, shape) in shape_refs.iter().enumerate() {
2092 let area = quad_area(shape.quad);
2093 if let Some(arc) = shape.arc {
2094 arc_quad += area;
2095 arc_count += 1;
2096 if arc.sweep_angle >= cranpose_ui_graphics::TAU {
2097 ring_count += 1;
2098 }
2099 let ra = arc.mid_radius() as f64;
2100 let rb = arc.half_thickness() as f64;
2101 arc_band +=
2102 arc.sweep_angle as f64 * ra * (2.0 * rb) + std::f64::consts::PI * rb * rb;
2103 } else {
2104 other_quad += area;
2105 other_count += 1;
2106 top_other.push((area, index));
2107 }
2108 }
2109 let scale2 = (root_scale as f64) * (root_scale as f64);
2110 eprintln!(
2111 "[quad-area] arcs={arc_count} (rings={ring_count}) arc_quad_px={:.0} arc_band_px={:.0} | other={other_count} other_px={:.0}",
2112 arc_quad * scale2,
2113 arc_band * scale2,
2114 other_quad * scale2,
2115 );
2116 top_other.sort_by(|a, b| b.0.total_cmp(&a.0));
2117 for &(area, index) in top_other.iter().take(4) {
2118 let shape = shape_refs[index];
2119 let brush = match &shape.brush {
2120 cranpose_ui_graphics::Brush::Solid(color) => format!("solid a={:.2}", color.3),
2121 cranpose_ui_graphics::Brush::LinearGradient { colors, .. } => {
2122 format!("linear n={}", colors.len())
2123 }
2124 cranpose_ui_graphics::Brush::RadialGradient { colors, .. } => {
2125 format!("radial n={}", colors.len())
2126 }
2127 cranpose_ui_graphics::Brush::SweepGradient { colors, .. } => {
2128 format!("sweep n={}", colors.len())
2129 }
2130 };
2131 eprintln!(
2132 "[quad-area] top other: {:.0}px {}x{} at ({:.0},{:.0}) {} shape={} stroke={} clip={} blend={:?} z={}",
2133 area * scale2,
2134 shape.rect.width.round(),
2135 shape.rect.height.round(),
2136 shape.rect.x,
2137 shape.rect.y,
2138 brush,
2139 shape.shape.is_some(),
2140 shape.stroke.is_some(),
2141 shape.clip.is_some(),
2142 shape.blend_mode,
2143 shape.z_index,
2144 );
2145 }
2146 }
2147 #[cfg(target_arch = "wasm32")]
2148 let parallel = false;
2149 let workers = if parallel {
2150 shape_convert_worker_count()
2151 } else {
2152 1
2153 };
2154 if workers <= 1 {
2155 for (idx, shape) in shape_refs.iter().enumerate() {
2156 let gradient_start = gradient_offsets[idx];
2157 let gradient_end = gradient_offsets[idx + 1];
2158 convert_shape_into_slots(
2159 shape,
2160 root_scale,
2161 gradient_start,
2162 &mut shape_data_out[idx],
2163 &mut gradients_out[gradient_start as usize..gradient_end as usize],
2164 );
2165 }
2166 #[cfg(not(target_arch = "wasm32"))]
2167 SHAPE_CONVERT_TUNER.record(
2168 false,
2169 shape_count,
2170 convert_started.elapsed().as_nanos() as u64,
2171 );
2172 return;
2173 }
2174
2175 let chunk_len = shape_count.div_ceil(workers);
2176 let mut shape_data_rest = shape_data_out;
2177 let mut gradients_rest = gradients_out;
2178 std::thread::scope(|scope| {
2179 let mut chunk_start = 0usize;
2180 while chunk_start < shape_count {
2181 let chunk_end = (chunk_start + chunk_len).min(shape_count);
2182 let count = chunk_end - chunk_start;
2183 let gradient_base = gradient_offsets[chunk_start];
2184 let gradient_span = (gradient_offsets[chunk_end] - gradient_base) as usize;
2185 let (shape_data_chunk, rest) = std::mem::take(&mut shape_data_rest).split_at_mut(count);
2186 shape_data_rest = rest;
2187 let (gradient_chunk, rest) =
2188 std::mem::take(&mut gradients_rest).split_at_mut(gradient_span);
2189 gradients_rest = rest;
2190 let chunk_refs = &shape_refs[chunk_start..chunk_end];
2191 let chunk_offsets = &gradient_offsets[chunk_start..=chunk_end];
2192 let mut convert_chunk = move || {
2193 for (j, shape) in chunk_refs.iter().enumerate() {
2194 let gradient_start = chunk_offsets[j];
2195 let local_start = (gradient_start - gradient_base) as usize;
2196 let local_end = (chunk_offsets[j + 1] - gradient_base) as usize;
2197 convert_shape_into_slots(
2198 shape,
2199 root_scale,
2200 gradient_start,
2201 &mut shape_data_chunk[j],
2202 &mut gradient_chunk[local_start..local_end],
2203 );
2204 }
2205 };
2206 if chunk_end == shape_count {
2207 convert_chunk();
2211 } else {
2212 scope.spawn(convert_chunk);
2213 }
2214 chunk_start = chunk_end;
2215 }
2216 });
2217 #[cfg(not(target_arch = "wasm32"))]
2218 SHAPE_CONVERT_TUNER.record(
2219 true,
2220 shape_count,
2221 convert_started.elapsed().as_nanos() as u64,
2222 );
2223}
2224
2225#[repr(C)]
2226#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2227struct GradientStop {
2228 color: [f32; 4],
2229 position: [f32; 4],
2230}
2231
2232#[cfg(not(target_arch = "wasm32"))]
2236const MAX_REPLAY_SLOTS: u32 = 128;
2237#[cfg(not(target_arch = "wasm32"))]
2238const REPLAY_TRANSFORM_STRIDE: u64 = 256;
2239
2240#[cfg(not(target_arch = "wasm32"))]
2247struct ReplaySlot {
2248 paint_buffer: wgpu::Buffer,
2253 bind_group: wgpu::BindGroup,
2254 shape_count: u32,
2255 paint_mirror: Vec<[f32; 4]>,
2261 mesh: Option<ReplaySlotMesh>,
2266 capture_epoch: u64,
2271}
2272
2273#[cfg(not(target_arch = "wasm32"))]
2278struct ReplaySlotMesh {
2279 vertex_buffer: wgpu::Buffer,
2280 index_buffer: wgpu::Buffer,
2286 index_prefix: Vec<u32>,
2291}
2292
2293#[cfg(not(target_arch = "wasm32"))]
2297#[repr(C)]
2298#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2299struct MeshVertex {
2300 position: [f32; 2],
2301 uv: [f32; 2],
2302 shape_idx: u32,
2303}
2304
2305#[cfg(not(target_arch = "wasm32"))]
2306impl MeshVertex {
2307 const ATTRIBS: [wgpu::VertexAttribute; 3] =
2308 wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Uint32];
2309
2310 fn desc() -> wgpu::VertexBufferLayout<'static> {
2311 wgpu::VertexBufferLayout {
2312 array_stride: std::mem::size_of::<MeshVertex>() as wgpu::BufferAddress,
2313 step_mode: wgpu::VertexStepMode::Vertex,
2314 attributes: &Self::ATTRIBS,
2315 }
2316 }
2317}
2318
2319#[cfg(not(target_arch = "wasm32"))]
2324fn arc_mesh_enabled() -> bool {
2325 matches!(std::env::var("CRANPOSE_ARC_MESH").as_deref(), Ok(v) if v != "0")
2333}
2334
2335#[cfg(not(target_arch = "wasm32"))]
2341const ARC_MESH_MARGIN: f32 = 1.0;
2342
2343#[cfg(not(target_arch = "wasm32"))]
2347const ARC_MESH_OVERSHOOT: f32 = 2.0;
2348
2349#[cfg(not(target_arch = "wasm32"))]
2350const ARC_MESH_MIN_SEGMENTS: usize = 4;
2351#[cfg(not(target_arch = "wasm32"))]
2352const ARC_MESH_MAX_SEGMENTS: usize = 64;
2353
2354#[cfg(not(target_arch = "wasm32"))]
2363const ARC_MESH_BUDGET_BYTES_PER_SHAPE: usize = 48 * std::mem::size_of::<MeshVertex>();
2364#[cfg(not(target_arch = "wasm32"))]
2365const ARC_MESH_BUDGET_FLOOR_BYTES: usize = 4096 * std::mem::size_of::<MeshVertex>();
2366
2367#[cfg(not(target_arch = "wasm32"))]
2370fn arc_mesh_bytes(vertices: usize, indices: usize) -> usize {
2371 vertices * std::mem::size_of::<MeshVertex>() + indices * std::mem::size_of::<u32>()
2372}
2373
2374#[cfg(not(target_arch = "wasm32"))]
2379struct ArcMeshBand {
2380 center: [f32; 2],
2381 inner: f32,
2382 outer: f32,
2383 start: f32,
2384 sweep: f32,
2385}
2386
2387#[cfg(not(target_arch = "wasm32"))]
2388fn arc_mesh_band(shape: &ShapeData) -> Option<ArcMeshBand> {
2389 let flags = shape.stroke_params[1].max(0.0) as u32;
2391 if flags & 3 != SHAPE_KIND_ARC {
2392 return None;
2393 }
2394 if shape.brush_type != 0 {
2398 return None;
2399 }
2400 if shape.clip_rect[2] > 0.0 && shape.clip_rect[3] > 0.0 {
2405 return None;
2406 }
2407 let [_, _, w, h] = shape.rect;
2408 if !(w > 0.0 && h > 0.0) {
2409 return None;
2410 }
2411 let [left, top, right, _] = shape.quad01;
2419 let [bl_x, bottom, br_x, br_y] = shape.quad23;
2420 let axis_aligned = shape.quad01[3] == top
2421 && bl_x == left
2422 && br_x == right
2423 && br_y == bottom
2424 && left < right
2425 && top < bottom;
2426 if !axis_aligned {
2427 return None;
2428 }
2429 let center = [shape.arc_params[0], shape.arc_params[1]];
2430 let start = shape.arc_params[2];
2431 let sweep = shape.arc_params[3];
2432 let outer = shape.stroke_params[2];
2433 let inner = shape.stroke_params[3];
2434 let finite = center[0].is_finite()
2435 && center[1].is_finite()
2436 && start.is_finite()
2437 && sweep.is_finite()
2438 && outer.is_finite()
2439 && inner.is_finite();
2440 if !finite || outer <= 0.0 || sweep <= 0.0 {
2441 return None;
2442 }
2443 Some(ArcMeshBand {
2444 center,
2445 inner,
2446 outer,
2447 start,
2448 sweep,
2449 })
2450}
2451
2452#[cfg(not(target_arch = "wasm32"))]
2458fn emit_passthrough_quad(
2459 shape: &ShapeData,
2460 shape_idx: u32,
2461 vertices: &mut Vec<MeshVertex>,
2462 indices: &mut Vec<u32>,
2463) {
2464 let base = vertices.len() as u32;
2465 let corners = [
2466 ([shape.quad01[0], shape.quad01[1]], [0.0, 0.0]),
2467 ([shape.quad01[2], shape.quad01[3]], [1.0, 0.0]),
2468 ([shape.quad23[0], shape.quad23[1]], [0.0, 1.0]),
2469 ([shape.quad23[2], shape.quad23[3]], [1.0, 1.0]),
2470 ];
2471 for (position, uv) in corners {
2472 vertices.push(MeshVertex {
2473 position,
2474 uv,
2475 shape_idx,
2476 });
2477 }
2478 indices.extend([0u32, 1, 2, 2, 1, 3].map(|corner| base + corner));
2479}
2480
2481#[cfg(not(target_arch = "wasm32"))]
2492fn clip_polygon_axis(
2493 input: &[[f32; 2]],
2494 axis: usize,
2495 bound: f32,
2496 keep_at_most: bool,
2497 output: &mut Vec<[f32; 2]>,
2498) {
2499 output.clear();
2500 let inside = |p: [f32; 2]| {
2501 if keep_at_most {
2502 p[axis] <= bound
2503 } else {
2504 p[axis] >= bound
2505 }
2506 };
2507 let intersect = |a: [f32; 2], b: [f32; 2]| {
2508 let (p, q) = if (b[0], b[1]) < (a[0], a[1]) {
2509 (b, a)
2510 } else {
2511 (a, b)
2512 };
2513 let t = (bound - p[axis]) / (q[axis] - p[axis]);
2514 let mut point = [0.0f32; 2];
2515 point[axis] = bound;
2516 point[1 - axis] = p[1 - axis] + t * (q[1 - axis] - p[1 - axis]);
2517 point
2518 };
2519 for (index, ¤t) in input.iter().enumerate() {
2520 let previous = input[(index + input.len() - 1) % input.len()];
2521 match (inside(previous), inside(current)) {
2522 (true, true) => output.push(current),
2523 (true, false) => output.push(intersect(previous, current)),
2524 (false, true) => {
2525 output.push(intersect(previous, current));
2526 output.push(current);
2527 }
2528 (false, false) => {}
2529 }
2530 }
2531}
2532
2533#[cfg(not(target_arch = "wasm32"))]
2568fn emit_arc_band_mesh(
2569 shape: &ShapeData,
2570 shape_idx: u32,
2571 band: &ArcMeshBand,
2572 vertices: &mut Vec<MeshVertex>,
2573 indices: &mut Vec<u32>,
2574) -> Option<usize> {
2575 let [cx, cy] = band.center;
2576 let ra = (band.outer + band.inner) * 0.5;
2577 let rb = ((band.outer - band.inner) * 0.5).max(0.0);
2578 let rb_m = rb + ARC_MESH_MARGIN;
2579 let ro = ra + rb_m;
2580 let ri = (ra - rb_m).max(0.0);
2581 let tau = cranpose_ui_graphics::TAU;
2582
2583 let (range_start, range) = if band.sweep >= tau {
2584 (0.0, tau)
2585 } else {
2586 let pad = if rb_m < ra {
2587 (rb_m / ra).asin() + 0.05
2588 } else {
2589 std::f32::consts::PI
2592 };
2593 let padded = band.sweep + pad + pad;
2594 if padded >= tau {
2595 (0.0, tau)
2596 } else {
2597 (band.start - pad, padded)
2598 }
2599 };
2600 let closed = range >= tau;
2601
2602 let dtheta = (2.0 * (ro / (ro + ARC_MESH_OVERSHOOT)).acos()).clamp(tau / 64.0, tau / 6.0);
2603 let segments =
2604 ((range / dtheta).ceil() as usize).clamp(ARC_MESH_MIN_SEGMENTS, ARC_MESH_MAX_SEGMENTS);
2605 let step = range / segments as f32;
2606 let rc = ro / (step * 0.5).cos();
2607
2608 let boundary_count = if closed { segments } else { segments + 1 };
2612 let mut boundaries = Vec::with_capacity(boundary_count);
2613 for j in 0..boundary_count {
2614 let (sin, cos) = (range_start + step * j as f32).sin_cos();
2615 boundaries.push((
2616 [cx + cos * ri, cy + sin * ri],
2617 [cx + cos * rc, cy + sin * rc],
2618 ));
2619 }
2620
2621 let quad_min = [shape.quad01[0], shape.quad01[1]];
2622 let quad_max = [shape.quad23[2], shape.quad23[3]];
2623
2624 enum SegmentGeometry {
2629 Shared,
2630 Fan(Vec<[f32; 2]>),
2631 Empty,
2632 }
2633
2634 let mut polygon: Vec<[f32; 2]> = Vec::with_capacity(8);
2636 let mut scratch: Vec<[f32; 2]> = Vec::with_capacity(8);
2637 let mut segment_geometry = Vec::with_capacity(segments);
2638 let mut boundary_used = vec![false; boundary_count];
2639 for j in 0..segments {
2640 let jb = (j + 1) % boundary_count;
2641 let (inner_a, outer_a) = boundaries[j];
2642 let (inner_b, outer_b) = boundaries[jb];
2643 polygon.clear();
2644 polygon.extend_from_slice(&[inner_a, outer_a, outer_b, inner_b]);
2645 clip_polygon_axis(&polygon, 0, quad_min[0], false, &mut scratch);
2646 clip_polygon_axis(&scratch, 0, quad_max[0], true, &mut polygon);
2647 clip_polygon_axis(&polygon, 1, quad_min[1], false, &mut scratch);
2648 clip_polygon_axis(&scratch, 1, quad_max[1], true, &mut polygon);
2649 scratch.clear();
2652 for &point in polygon.iter() {
2653 if scratch.last() != Some(&point) {
2654 scratch.push(point);
2655 }
2656 }
2657 while scratch.len() > 1 && scratch.first() == scratch.last() {
2658 scratch.pop();
2659 }
2660 if scratch.len() < 3 {
2661 segment_geometry.push(SegmentGeometry::Empty);
2662 } else if scratch[..] == [inner_a, outer_a, outer_b, inner_b] {
2663 boundary_used[j] = true;
2664 boundary_used[jb] = true;
2665 segment_geometry.push(SegmentGeometry::Shared);
2666 } else {
2667 segment_geometry.push(SegmentGeometry::Fan(scratch.clone()));
2668 }
2669 }
2670
2671 let push_vertex = |vertices: &mut Vec<MeshVertex>, position: [f32; 2]| -> u32 {
2672 let index = vertices.len() as u32;
2673 vertices.push(MeshVertex {
2674 position,
2675 uv: [
2676 (position[0] - shape.rect[0]) / shape.rect[2],
2677 (position[1] - shape.rect[1]) / shape.rect[3],
2678 ],
2679 shape_idx,
2680 });
2681 index
2682 };
2683
2684 let mut boundary_vertex = vec![[0u32; 2]; boundary_count];
2687 for (j, used) in boundary_used.iter().enumerate() {
2688 if *used {
2689 let (inner, outer) = boundaries[j];
2690 boundary_vertex[j] = [push_vertex(vertices, inner), push_vertex(vertices, outer)];
2691 }
2692 }
2693
2694 let start_len = indices.len();
2697 for (j, geometry) in segment_geometry.iter().enumerate() {
2698 match geometry {
2699 SegmentGeometry::Empty => {}
2700 SegmentGeometry::Shared => {
2701 let jb = (j + 1) % boundary_count;
2702 let [in_a, out_a] = boundary_vertex[j];
2703 let [in_b, out_b] = boundary_vertex[jb];
2704 indices.extend_from_slice(&[in_a, out_a, out_b, in_a, out_b, in_b]);
2708 }
2709 SegmentGeometry::Fan(points) => {
2710 let base = vertices.len() as u32;
2711 for &point in points {
2712 push_vertex(vertices, point);
2713 }
2714 for i in 1..points.len() as u32 - 1 {
2715 indices.extend_from_slice(&[base, base + i, base + i + 1]);
2716 }
2717 }
2718 }
2719 }
2720 if indices.len() == start_len {
2721 return None;
2722 }
2723 Some(segments)
2724}
2725
2726#[cfg(not(target_arch = "wasm32"))]
2729fn triangles_shoelace_area(vertices: &[MeshVertex], indices: &[u32]) -> f64 {
2730 indices
2731 .chunks_exact(3)
2732 .map(|tri| {
2733 let [a, b, c] = [
2734 vertices[tri[0] as usize].position,
2735 vertices[tri[1] as usize].position,
2736 vertices[tri[2] as usize].position,
2737 ];
2738 let cross = (b[0] as f64 - a[0] as f64) * (c[1] as f64 - a[1] as f64)
2739 - (b[1] as f64 - a[1] as f64) * (c[0] as f64 - a[0] as f64);
2740 cross.abs() * 0.5
2741 })
2742 .sum()
2743}
2744
2745#[cfg(not(target_arch = "wasm32"))]
2748fn quad_shoelace_area(shape: &ShapeData) -> f64 {
2749 let corners = [
2750 [shape.quad01[0] as f64, shape.quad01[1] as f64],
2751 [shape.quad01[2] as f64, shape.quad01[3] as f64],
2752 [shape.quad23[0] as f64, shape.quad23[1] as f64],
2753 [shape.quad23[2] as f64, shape.quad23[3] as f64],
2754 ];
2755 let tri = |a: [f64; 2], b: [f64; 2], c: [f64; 2]| {
2756 ((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])).abs() * 0.5
2757 };
2758 tri(corners[0], corners[1], corners[2]) + tri(corners[2], corners[1], corners[3])
2759}
2760
2761#[cfg(not(target_arch = "wasm32"))]
2762struct ArcMeshBuild {
2763 vertices: Vec<MeshVertex>,
2764 indices: Vec<u32>,
2766 index_prefix: Vec<u32>,
2769 meshed_arcs: usize,
2770 meshed_segments: usize,
2771 passthrough: usize,
2772 quad_area: f64,
2773 mesh_area: f64,
2774}
2775
2776#[cfg(not(target_arch = "wasm32"))]
2783fn build_arc_mesh_vertices(shape_data: &[ShapeData]) -> Option<ArcMeshBuild> {
2784 let budget_bytes =
2785 (shape_data.len() * ARC_MESH_BUDGET_BYTES_PER_SHAPE).max(ARC_MESH_BUDGET_FLOOR_BYTES);
2786 let mut build = ArcMeshBuild {
2787 vertices: Vec::new(),
2788 indices: Vec::new(),
2789 index_prefix: Vec::with_capacity(shape_data.len() + 1),
2790 meshed_arcs: 0,
2791 meshed_segments: 0,
2792 passthrough: 0,
2793 quad_area: 0.0,
2794 mesh_area: 0.0,
2795 };
2796 build.index_prefix.push(0);
2797 for (index, shape) in shape_data.iter().enumerate() {
2798 let start = build.indices.len();
2799 let meshed = arc_mesh_band(shape).and_then(|band| {
2800 emit_arc_band_mesh(
2801 shape,
2802 index as u32,
2803 &band,
2804 &mut build.vertices,
2805 &mut build.indices,
2806 )
2807 });
2808 match meshed {
2809 Some(segments) => {
2810 build.meshed_arcs += 1;
2811 build.meshed_segments += segments;
2812 }
2813 None => {
2814 emit_passthrough_quad(shape, index as u32, &mut build.vertices, &mut build.indices);
2815 build.passthrough += 1;
2816 }
2817 }
2818 if arc_mesh_bytes(build.vertices.len(), build.indices.len()) > budget_bytes {
2819 return None;
2820 }
2821 build.index_prefix.push(build.indices.len() as u32);
2822 build.quad_area += quad_shoelace_area(shape);
2823 build.mesh_area += triangles_shoelace_area(&build.vertices, &build.indices[start..]);
2824 }
2825 Some(build)
2826}
2827
2828#[cfg(not(target_arch = "wasm32"))]
2831struct ReplaySlotStore {
2832 slots: std::collections::HashMap<u32, ReplaySlot, cranpose_ui_graphics::FxBuildHasher>,
2833 transform_buffer: wgpu::Buffer,
2834 free_ids: Vec<u32>,
2835 next_capture_epoch: u64,
2839}
2840
2841#[cfg(not(target_arch = "wasm32"))]
2842impl ReplaySlotStore {
2843 fn new(device: &wgpu::Device) -> Self {
2844 let transform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
2845 label: Some("Replay Transform Buffer"),
2846 size: MAX_REPLAY_SLOTS as u64 * REPLAY_TRANSFORM_STRIDE,
2847 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
2848 mapped_at_creation: false,
2849 });
2850 Self {
2851 slots: std::collections::HashMap::default(),
2852 transform_buffer,
2853 free_ids: (0..MAX_REPLAY_SLOTS).rev().collect(),
2854 next_capture_epoch: 1,
2855 }
2856 }
2857}
2858
2859#[cfg(not(target_arch = "wasm32"))]
2865fn retained_bundles_enabled() -> bool {
2866 std::env::var("CRANPOSE_RETAINED_BUNDLES").as_deref() != Ok("0")
2867}
2868
2869#[cfg(not(target_arch = "wasm32"))]
2877fn instanced_quads_enabled() -> bool {
2878 std::env::var("CRANPOSE_INSTANCED_QUADS").as_deref() != Ok("0")
2879}
2880
2881#[cfg(not(target_arch = "wasm32"))]
2885const INSTANCED_QUAD_INDICES: [u16; 6] = [0, 1, 2, 2, 1, 3];
2886
2887#[cfg(not(target_arch = "wasm32"))]
2893struct InstancedQuadPipelines {
2894 pipeline: wgpu::RenderPipeline,
2895 pipeline_dst_out: wgpu::RenderPipeline,
2896 index_buffer: wgpu::Buffer,
2899}
2900
2901#[cfg(not(target_arch = "wasm32"))]
2909#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2910struct RetainedBundleOpKey {
2911 slot: u32,
2912 capture_epoch: Option<u64>,
2917 first: u32,
2918 last: u32,
2919 retained_index: u32,
2920 has_mesh: bool,
2921}
2922
2923#[cfg(not(target_arch = "wasm32"))]
2927#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
2928struct RetainedBundleKey {
2929 ops: Vec<RetainedBundleOpKey>,
2930}
2931
2932#[cfg(not(target_arch = "wasm32"))]
2933struct RetainedBundleCacheEntry<B> {
2934 bundle: B,
2935 last_used_frame: u64,
2936}
2937
2938#[cfg(not(target_arch = "wasm32"))]
2947struct RetainedBundleCacheImpl<B> {
2948 entries: HashMap<RetainedBundleKey, RetainedBundleCacheEntry<B>>,
2949 frame: u64,
2950 rebuilds: u64,
2951 cached_executes: u64,
2952 window_rebuilds: u64,
2953 window_executes: u64,
2954}
2955
2956#[cfg(not(target_arch = "wasm32"))]
2957type RetainedBundleCache = RetainedBundleCacheImpl<wgpu::RenderBundle>;
2958
2959#[cfg(not(target_arch = "wasm32"))]
2960impl<B> RetainedBundleCacheImpl<B> {
2961 fn new() -> Self {
2962 Self {
2963 entries: HashMap::default(),
2964 frame: 0,
2965 rebuilds: 0,
2966 cached_executes: 0,
2967 window_rebuilds: 0,
2968 window_executes: 0,
2969 }
2970 }
2971
2972 fn hit(&mut self, key: &RetainedBundleKey) -> bool {
2975 let frame = self.frame;
2976 match self.entries.get_mut(key) {
2977 Some(entry) => {
2978 entry.last_used_frame = frame;
2979 self.cached_executes += 1;
2980 self.window_executes += 1;
2981 true
2982 }
2983 None => false,
2984 }
2985 }
2986
2987 fn insert(&mut self, key: RetainedBundleKey, bundle: B) {
2989 self.rebuilds += 1;
2990 self.window_rebuilds += 1;
2991 self.entries.insert(
2992 key,
2993 RetainedBundleCacheEntry {
2994 bundle,
2995 last_used_frame: self.frame,
2996 },
2997 );
2998 }
2999
3000 fn get(&self, key: &RetainedBundleKey) -> Option<&B> {
3001 self.entries.get(key).map(|entry| &entry.bundle)
3002 }
3003
3004 fn clear(&mut self) {
3009 self.entries.clear();
3010 }
3011
3012 fn end_frame(&mut self) {
3016 let frame = self.frame;
3017 self.entries
3018 .retain(|_, entry| entry.last_used_frame >= frame);
3019 self.frame = self.frame.wrapping_add(1);
3020 let due = self.frame.is_multiple_of(1024)
3025 || (cranpose_core::env_flag!("CRANPOSE_COMMAND_REPLAY_DIAG")
3026 && self.frame.is_multiple_of(120));
3027 if due && self.window_rebuilds + self.window_executes > 0 {
3028 log::warn!(
3029 "[retained-bundles] {} stretches, {} rebuilds, {} cached executes ({} live bundles)",
3030 self.window_rebuilds + self.window_executes,
3031 self.window_rebuilds,
3032 self.window_executes,
3033 self.entries.len(),
3034 );
3035 self.window_rebuilds = 0;
3036 self.window_executes = 0;
3037 }
3038 }
3039
3040 fn stats(&self) -> (u64, u64) {
3042 (self.rebuilds, self.cached_executes)
3043 }
3044}
3045
3046struct CachedImageTexture {
3047 _texture: wgpu::Texture,
3048 _view: wgpu::TextureView,
3049 nearest_bind_group: wgpu::BindGroup,
3050 linear_bind_group: wgpu::BindGroup,
3051 bytes: usize,
3058}
3059
3060impl CachedImageTexture {
3061 fn bind_group(&self, sampling: ImageSampling) -> &wgpu::BindGroup {
3062 match sampling {
3063 ImageSampling::Nearest => &self.nearest_bind_group,
3064 ImageSampling::Linear => &self.linear_bind_group,
3065 }
3066 }
3067}
3068
3069#[derive(Clone, Copy)]
3070struct GlyphAtlasEntry {
3071 x: u32,
3072 y: u32,
3073 width: u32,
3074 height: u32,
3075}
3076
3077fn next_glyph_atlas_size(current: u32, max: u32) -> u32 {
3084 current.saturating_mul(2).clamp(1, max.max(1))
3085}
3086
3087struct TextGlyphAtlas {
3088 texture: wgpu::Texture,
3089 _view: wgpu::TextureView,
3090 bind_group: wgpu::BindGroup,
3091 entries: BoundedLruCache<SoftwareGlyphAtlasKey, GlyphAtlasEntry>,
3092 generation: u64,
3093 size: u32,
3097 max_size: u32,
3103 cursor_x: u32,
3104 cursor_y: u32,
3105 row_height: u32,
3106 upload_scratch: Vec<u8>,
3107}
3108
3109impl TextGlyphAtlas {
3110 fn new(
3111 device: &wgpu::Device,
3112 image_layout: &wgpu::BindGroupLayout,
3113 sampler: &wgpu::Sampler,
3114 size: u32,
3115 ) -> Self {
3116 let max_size = TEXT_GLYPH_ATLAS_MAX_SIZE.min(device.limits().max_texture_dimension_2d);
3117 let size = size.clamp(TEXT_GLYPH_ATLAS_MIN_SIZE.min(max_size), max_size);
3118 let texture = Self::create_texture(device, size);
3119 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
3120 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3121 label: Some("Text Glyph Atlas Bind Group"),
3122 layout: image_layout,
3123 entries: &[
3124 wgpu::BindGroupEntry {
3125 binding: 0,
3126 resource: wgpu::BindingResource::TextureView(&view),
3127 },
3128 wgpu::BindGroupEntry {
3129 binding: 1,
3130 resource: wgpu::BindingResource::Sampler(sampler),
3131 },
3132 ],
3133 });
3134 Self {
3135 texture,
3136 _view: view,
3137 bind_group,
3138 entries: BoundedLruCache::with_capacity_at_least_one(MAX_TEXT_GLYPH_ATLAS_ITEMS),
3139 generation: 0,
3140 size,
3141 max_size,
3142 cursor_x: TEXT_GLYPH_ATLAS_PADDING,
3143 cursor_y: TEXT_GLYPH_ATLAS_PADDING,
3144 row_height: 0,
3145 upload_scratch: Vec::new(),
3146 }
3147 }
3148
3149 fn create_texture(device: &wgpu::Device, size: u32) -> wgpu::Texture {
3150 device.create_texture(&wgpu::TextureDescriptor {
3151 label: Some("Text Glyph Atlas Texture"),
3152 size: wgpu::Extent3d {
3153 width: size,
3154 height: size,
3155 depth_or_array_layers: 1,
3156 },
3157 mip_level_count: 1,
3158 sample_count: 1,
3159 dimension: wgpu::TextureDimension::D2,
3160 format: wgpu::TextureFormat::R8Unorm,
3161 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
3162 view_formats: &[],
3163 })
3164 }
3165
3166 fn reset(
3182 &mut self,
3183 device: &wgpu::Device,
3184 image_layout: &wgpu::BindGroupLayout,
3185 sampler: &wgpu::Sampler,
3186 ) {
3187 let generation = self.generation.wrapping_add(1);
3188 let grown = next_glyph_atlas_size(self.size, self.max_size);
3189 let mut next = Self::new(device, image_layout, sampler, grown);
3190 next.generation = generation;
3191 *self = next;
3192 }
3193
3194 fn generation(&self) -> u64 {
3195 self.generation
3196 }
3197
3198 fn size(&self) -> u32 {
3199 self.size
3200 }
3201
3202 fn entry(&mut self, key: &SoftwareGlyphAtlasKey) -> Option<GlyphAtlasEntry> {
3203 self.entries.get(key).copied()
3204 }
3205
3206 fn allocate(&mut self, width: u32, height: u32) -> Option<GlyphAtlasEntry> {
3207 if width == 0
3208 || height == 0
3209 || width + TEXT_GLYPH_ATLAS_PADDING * 2 > self.size
3210 || height + TEXT_GLYPH_ATLAS_PADDING * 2 > self.size
3211 {
3212 return None;
3213 }
3214
3215 if self.cursor_x + width + TEXT_GLYPH_ATLAS_PADDING > self.size {
3216 self.cursor_x = TEXT_GLYPH_ATLAS_PADDING;
3217 self.cursor_y = self
3218 .cursor_y
3219 .saturating_add(self.row_height)
3220 .saturating_add(TEXT_GLYPH_ATLAS_PADDING);
3221 self.row_height = 0;
3222 }
3223 if self.cursor_y + height + TEXT_GLYPH_ATLAS_PADDING > self.size {
3224 return None;
3225 }
3226
3227 let entry = GlyphAtlasEntry {
3228 x: self.cursor_x,
3229 y: self.cursor_y,
3230 width,
3231 height,
3232 };
3233 self.cursor_x = self
3234 .cursor_x
3235 .saturating_add(width)
3236 .saturating_add(TEXT_GLYPH_ATLAS_PADDING);
3237 self.row_height = self.row_height.max(height);
3238 Some(entry)
3239 }
3240
3241 fn upload_glyph(
3242 &mut self,
3243 key: SoftwareGlyphAtlasKey,
3244 glyph: &SoftwareGlyphAtlasGlyph,
3245 queue: &wgpu::Queue,
3246 executor: &mut WgpuFrameGraphExecutor,
3247 frame_stats: &mut gpu_stats::FrameStats,
3248 ) -> Option<GlyphAtlasEntry> {
3249 if let Some(entry) = self.entry(&key) {
3250 frame_stats.record_text_glyph_atlas_hit();
3251 return Some(entry);
3252 }
3253
3254 let width = u32::try_from(glyph.mask.width).ok()?;
3255 let height = u32::try_from(glyph.mask.height).ok()?;
3256 let entry = self.allocate(width, height)?;
3257 self.upload_scratch.clear();
3258 self.upload_scratch.reserve(
3259 glyph
3260 .mask
3261 .alpha
3262 .len()
3263 .saturating_sub(self.upload_scratch.capacity()),
3264 );
3265 self.upload_scratch.extend(
3266 glyph
3267 .mask
3268 .alpha
3269 .iter()
3270 .map(|alpha| (alpha.clamp(0.0, 1.0) * 255.0).round() as u8),
3271 );
3272
3273 let upload_stats = executor.upload_texture(
3274 queue,
3275 wgpu::TexelCopyTextureInfo {
3276 texture: &self.texture,
3277 mip_level: 0,
3278 origin: wgpu::Origin3d {
3279 x: entry.x,
3280 y: entry.y,
3281 z: 0,
3282 },
3283 aspect: wgpu::TextureAspect::All,
3284 },
3285 &self.upload_scratch,
3286 wgpu::TexelCopyBufferLayout {
3287 offset: 0,
3288 bytes_per_row: Some(entry.width),
3289 rows_per_image: Some(entry.height),
3290 },
3291 wgpu::Extent3d {
3292 width: entry.width,
3293 height: entry.height,
3294 depth_or_array_layers: 1,
3295 },
3296 );
3297 frame_stats.record_command_stats(upload_stats);
3298 frame_stats.record_text_glyph_atlas_miss(entry.width, entry.height);
3299 self.entries.put(key, entry);
3300 Some(entry)
3301 }
3302}
3303
3304struct ImageDrawCmd {
3305 index_start: u32,
3306 scissor: (u32, u32, u32, u32),
3307 image_id: u64,
3308 sampling: ImageSampling,
3309}
3310
3311#[derive(Clone, Copy)]
3312enum GlyphDrawSource {
3313 Shared {
3314 index_start: u32,
3315 index_count: u32,
3316 },
3317 #[cfg(not(target_arch = "wasm32"))]
3318 Retained {
3319 cache_key: TextGlyphRunCacheKey,
3320 uniform_slot: usize,
3321 },
3322}
3323
3324#[derive(Clone, Copy)]
3325struct GlyphDrawCmd {
3326 source: GlyphDrawSource,
3327 scissor: (u32, u32, u32, u32),
3328}
3329
3330impl GlyphDrawCmd {
3331 fn shared(index_start: u32, index_count: u32, scissor: (u32, u32, u32, u32)) -> Self {
3332 Self {
3333 source: GlyphDrawSource::Shared {
3334 index_start,
3335 index_count,
3336 },
3337 scissor,
3338 }
3339 }
3340
3341 #[cfg(not(target_arch = "wasm32"))]
3342 fn retained(
3343 cache_key: TextGlyphRunCacheKey,
3344 uniform_slot: usize,
3345 scissor: (u32, u32, u32, u32),
3346 ) -> Self {
3347 Self {
3348 source: GlyphDrawSource::Retained {
3349 cache_key,
3350 uniform_slot,
3351 },
3352 scissor,
3353 }
3354 }
3355}
3356
3357#[derive(Clone, Copy, Debug, PartialEq)]
3358struct ImageUvRect {
3359 min: [f32; 2],
3360 max: [f32; 2],
3361 sample_bounds: [f32; 4],
3362}
3363
3364struct ShapeBatchBuffers {
3371 shape_buffer: wgpu::Buffer,
3372 gradient_buffer: wgpu::Buffer,
3373 bind_group: wgpu::BindGroup,
3374 shape_capacity: usize,
3375 gradient_capacity: usize,
3376 batch_limits: ShapeBatchLimits,
3377}
3378
3379#[cfg(target_arch = "wasm32")]
3380struct UniformBatchBuffer {
3381 buffer: wgpu::Buffer,
3382 bind_group: wgpu::BindGroup,
3383}
3384
3385#[cfg(target_arch = "wasm32")]
3386struct ImageBatchBuffers {
3387 vertex_buffer: wgpu::Buffer,
3388 index_buffer: wgpu::Buffer,
3389 vertex_capacity: usize,
3390 index_capacity: usize,
3391}
3392
3393#[derive(Clone, Copy, Debug, PartialEq)]
3394struct ViewportUniformParams {
3395 width: u32,
3396 height: u32,
3397 offset: [f32; 2],
3398}
3399
3400#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3401#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
3402enum UploadTarget {
3403 Uniform,
3404 ShapeData,
3405 ShapeGradient,
3406 ImageVertex,
3407 ImageIndex,
3408 #[cfg(not(target_arch = "wasm32"))]
3409 RetainedGlyphUniform,
3410 #[cfg(not(target_arch = "wasm32"))]
3413 ReplayTransform,
3414 #[cfg(not(target_arch = "wasm32"))]
3416 ReplayPaintData(u32),
3417}
3418
3419#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3420#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
3421struct PendingBufferCopy {
3422 source_offset: u64,
3423 target_offset: u64,
3424 size: u64,
3425 target: UploadTarget,
3426}
3427
3428#[derive(Default)]
3429struct StagedBufferUploads {
3430 bytes: Vec<u8>,
3431 copies: Vec<PendingBufferCopy>,
3432}
3433
3434impl StagedBufferUploads {
3435 fn clear(&mut self) {
3436 self.bytes.clear();
3437 self.copies.clear();
3438 }
3439
3440 fn shrink_retained_capacity(&mut self, max_bytes: usize, max_copies: usize) -> bool {
3441 let mut shrunk = false;
3442 if self.bytes.len() <= max_bytes && self.bytes.capacity() > max_bytes {
3443 self.bytes.shrink_to(max_bytes);
3444 shrunk = true;
3445 }
3446 if self.copies.len() <= max_copies && self.copies.capacity() > max_copies {
3447 self.copies.shrink_to(max_copies);
3448 shrunk = true;
3449 }
3450 shrunk
3451 }
3452
3453 fn is_empty(&self) -> bool {
3454 self.copies.is_empty()
3455 }
3456
3457 #[cfg(test)]
3458 fn payload_for_copy(&self, copy: PendingBufferCopy) -> &[u8] {
3459 let start = copy.source_offset as usize;
3460 let end = start + copy.size as usize;
3461 &self.bytes[start..end]
3462 }
3463
3464 #[cfg(not(target_arch = "wasm32"))]
3465 fn stage(&mut self, target: UploadTarget, bytes: &[u8]) {
3466 self.stage_at(target, 0, bytes);
3467 }
3468
3469 #[cfg(not(target_arch = "wasm32"))]
3474 fn record_upload_copy(
3475 &mut self,
3476 target: UploadTarget,
3477 source_offset: u64,
3478 target_offset: u64,
3479 size: u64,
3480 ) {
3481 if size == 0 {
3482 return;
3483 }
3484 self.copies.push(PendingBufferCopy {
3485 source_offset,
3486 target_offset,
3487 size,
3488 target,
3489 });
3490 }
3491
3492 #[cfg(not(target_arch = "wasm32"))]
3493 fn stage_at(&mut self, target: UploadTarget, target_offset: u64, bytes: &[u8]) {
3494 if bytes.is_empty() {
3495 return;
3496 }
3497
3498 debug_assert_eq!(
3499 bytes.len() % wgpu::COPY_BUFFER_ALIGNMENT as usize,
3500 0,
3501 "buffer uploads must be aligned to copy requirements"
3502 );
3503
3504 let aligned_offset = align_usize_to(self.bytes.len(), wgpu::COPY_BUFFER_ALIGNMENT as usize);
3505 if aligned_offset > self.bytes.len() {
3506 self.bytes.resize(aligned_offset, 0);
3507 }
3508
3509 let source_offset = self.bytes.len() as u64;
3510 self.bytes.extend_from_slice(bytes);
3511 self.copies.push(PendingBufferCopy {
3512 source_offset,
3513 target_offset,
3514 size: bytes.len() as u64,
3515 target,
3516 });
3517 }
3518
3519 fn truncate(&mut self, bytes_len: usize, copies_len: usize) {
3520 self.bytes.truncate(bytes_len);
3521 self.copies.truncate(copies_len);
3522 }
3523}
3524
3525fn shape_batch_bind_group_entries<'a>(
3530 shape_buffer: &'a wgpu::Buffer,
3531 gradient_buffer: &'a wgpu::Buffer,
3532 similarity_buffer: &'a wgpu::Buffer,
3533 paint_buffer: Option<&'a wgpu::Buffer>,
3534) -> Vec<wgpu::BindGroupEntry<'a>> {
3535 let mut entries = vec![
3536 wgpu::BindGroupEntry {
3537 binding: 0,
3538 resource: shape_buffer.as_entire_binding(),
3539 },
3540 wgpu::BindGroupEntry {
3541 binding: 1,
3542 resource: gradient_buffer.as_entire_binding(),
3543 },
3544 wgpu::BindGroupEntry {
3545 binding: 2,
3546 resource: similarity_buffer.as_entire_binding(),
3547 },
3548 ];
3549 if let Some(paint_buffer) = paint_buffer {
3550 entries.push(wgpu::BindGroupEntry {
3551 binding: 3,
3552 resource: paint_buffer.as_entire_binding(),
3553 });
3554 }
3555 entries
3556}
3557
3558impl ShapeBatchBuffers {
3559 fn new(
3560 device: &wgpu::Device,
3561 bind_group_layout: &wgpu::BindGroupLayout,
3562 similarity_buffer: &wgpu::Buffer,
3563 paint_buffer: Option<&wgpu::Buffer>,
3564 batch_limits: ShapeBatchLimits,
3565 ) -> Self {
3566 debug_assert_eq!(
3567 paint_buffer.is_some(),
3568 batch_limits.storage,
3569 "the paint binding exists exactly when the layout is in storage mode"
3570 );
3571 let initial_shape_cap = batch_limits.initial_shape_capacity();
3572 let initial_gradient_cap = batch_limits.initial_gradient_capacity();
3573
3574 let shape_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3575 label: Some("Shape Data Buffer"),
3576 size: (std::mem::size_of::<ShapeData>() * initial_shape_cap) as u64,
3577 usage: batch_limits.data_buffer_usage(),
3578 mapped_at_creation: false,
3579 });
3580
3581 let gradient_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3582 label: Some("Gradient Buffer"),
3583 size: (std::mem::size_of::<GradientStop>() * initial_gradient_cap) as u64,
3584 usage: batch_limits.data_buffer_usage(),
3585 mapped_at_creation: false,
3586 });
3587
3588 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3589 label: Some("Shape Bind Group"),
3590 layout: bind_group_layout,
3591 entries: &shape_batch_bind_group_entries(
3592 &shape_buffer,
3593 &gradient_buffer,
3594 similarity_buffer,
3595 paint_buffer,
3596 ),
3597 });
3598
3599 Self {
3600 shape_buffer,
3601 gradient_buffer,
3602 bind_group,
3603 shape_capacity: initial_shape_cap,
3604 gradient_capacity: initial_gradient_cap,
3605 batch_limits,
3606 }
3607 }
3608
3609 fn ensure_capacity(
3612 &mut self,
3613 device: &wgpu::Device,
3614 bind_group_layout: &wgpu::BindGroupLayout,
3615 similarity_buffer: &wgpu::Buffer,
3616 paint_buffer: Option<&wgpu::Buffer>,
3617 shapes_needed: usize,
3618 gradients_needed: usize,
3619 ) {
3620 let mut need_bind_group_update = false;
3621
3622 if shapes_needed > self.shape_capacity
3626 && self.shape_capacity < self.batch_limits.max_shapes_per_batch
3627 {
3628 let new_cap = shapes_needed
3629 .next_power_of_two()
3630 .min(self.batch_limits.max_shapes_per_batch);
3631 self.shape_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3632 label: Some("Shape Data Buffer"),
3633 size: (std::mem::size_of::<ShapeData>() * new_cap) as u64,
3634 usage: self.batch_limits.data_buffer_usage(),
3635 mapped_at_creation: false,
3636 });
3637 self.shape_capacity = new_cap;
3638 need_bind_group_update = true;
3639 }
3640
3641 if gradients_needed > self.gradient_capacity
3642 && self.gradient_capacity < self.batch_limits.max_gradient_stops
3643 {
3644 let new_cap = gradients_needed
3645 .max(1)
3646 .next_power_of_two()
3647 .min(self.batch_limits.max_gradient_stops);
3648 self.gradient_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3649 label: Some("Gradient Buffer"),
3650 size: (std::mem::size_of::<GradientStop>() * new_cap) as u64,
3651 usage: self.batch_limits.data_buffer_usage(),
3652 mapped_at_creation: false,
3653 });
3654 self.gradient_capacity = new_cap;
3655 need_bind_group_update = true;
3656 }
3657
3658 if need_bind_group_update {
3659 self.bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3660 label: Some("Shape Bind Group"),
3661 layout: bind_group_layout,
3662 entries: &shape_batch_bind_group_entries(
3663 &self.shape_buffer,
3664 &self.gradient_buffer,
3665 similarity_buffer,
3666 paint_buffer,
3667 ),
3668 });
3669 }
3670 }
3671}
3672
3673#[cfg(target_arch = "wasm32")]
3674impl UniformBatchBuffer {
3675 fn new(device: &wgpu::Device, bind_group_layout: &wgpu::BindGroupLayout) -> Self {
3676 let buffer = device.create_buffer(&wgpu::BufferDescriptor {
3677 label: Some("Viewport Uniform Batch Buffer"),
3678 size: std::mem::size_of::<Uniforms>() as u64,
3679 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
3680 mapped_at_creation: false,
3681 });
3682 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3683 label: Some("Viewport Uniform Batch Bind Group"),
3684 layout: bind_group_layout,
3685 entries: &[wgpu::BindGroupEntry {
3686 binding: 0,
3687 resource: buffer.as_entire_binding(),
3688 }],
3689 });
3690 Self { buffer, bind_group }
3691 }
3692}
3693
3694#[cfg(target_arch = "wasm32")]
3695impl ImageBatchBuffers {
3696 fn new(device: &wgpu::Device) -> Self {
3697 let vertex_capacity = 4;
3698 let index_capacity = 6;
3699 let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3700 label: Some("Image Vertex Batch Buffer"),
3701 size: (std::mem::size_of::<Vertex>() * vertex_capacity) as u64,
3702 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
3703 mapped_at_creation: false,
3704 });
3705 let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3706 label: Some("Image Index Batch Buffer"),
3707 size: (std::mem::size_of::<u32>() * index_capacity) as u64,
3708 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
3709 mapped_at_creation: false,
3710 });
3711 Self {
3712 vertex_buffer,
3713 index_buffer,
3714 vertex_capacity,
3715 index_capacity,
3716 }
3717 }
3718
3719 fn ensure_capacity(
3720 &mut self,
3721 device: &wgpu::Device,
3722 vertices_needed: usize,
3723 indices_needed: usize,
3724 ) {
3725 let hard_max_bytes = HARD_MAX_BUFFER_MB * 1024 * 1024;
3726 if vertices_needed > self.vertex_capacity {
3727 let desired = vertices_needed.next_power_of_two();
3728 let max_count = hard_max_bytes / std::mem::size_of::<Vertex>();
3729 let new_cap = desired.min(max_count);
3730 self.vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3731 label: Some("Image Vertex Batch Buffer"),
3732 size: (std::mem::size_of::<Vertex>() * new_cap) as u64,
3733 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
3734 mapped_at_creation: false,
3735 });
3736 self.vertex_capacity = new_cap;
3737 }
3738 if indices_needed > self.index_capacity {
3739 let desired = indices_needed.next_power_of_two();
3740 let max_count = hard_max_bytes / std::mem::size_of::<u32>();
3741 let new_cap = desired.min(max_count);
3742 self.index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3743 label: Some("Image Index Batch Buffer"),
3744 size: (std::mem::size_of::<u32>() * new_cap) as u64,
3745 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
3746 mapped_at_creation: false,
3747 });
3748 self.index_capacity = new_cap;
3749 }
3750 }
3751}
3752
3753pub struct GpuRenderer {
3756 pub(crate) device: Arc<wgpu::Device>,
3757 pub(crate) queue: Arc<wgpu::Queue>,
3758 renderer_epoch: u64,
3763 #[cfg(not(target_arch = "wasm32"))]
3769 store_feed_generation: u64,
3770 surface_format: wgpu::TextureFormat,
3771 shape_batch_limits: ShapeBatchLimits,
3772 pipeline: wgpu::RenderPipeline,
3773 pipeline_dst_out: wgpu::RenderPipeline,
3774 #[cfg(not(target_arch = "wasm32"))]
3778 mesh_pipeline: Option<wgpu::RenderPipeline>,
3779 #[cfg(not(target_arch = "wasm32"))]
3785 instanced_quads: Option<InstancedQuadPipelines>,
3786 #[cfg(target_arch = "wasm32")]
3787 uniform_bind_group_layout: wgpu::BindGroupLayout,
3788 shape_bind_group_layout: wgpu::BindGroupLayout,
3789 dummy_paint_buffer: Option<wgpu::Buffer>,
3792 identity_similarity_buffer: wgpu::Buffer,
3795 #[cfg(not(target_arch = "wasm32"))]
3796 replay_slots: ReplaySlotStore,
3797 image_pipeline: wgpu::RenderPipeline,
3798 image_pipeline_dst_out: wgpu::RenderPipeline,
3799 glyph_atlas_pipeline: wgpu::RenderPipeline,
3800 #[cfg(not(target_arch = "wasm32"))]
3801 retained_glyph_atlas_pipeline: wgpu::RenderPipeline,
3802 image_bind_group_layout: wgpu::BindGroupLayout,
3803 #[cfg(not(target_arch = "wasm32"))]
3804 retained_glyph_uniform_bind_group_layout: wgpu::BindGroupLayout,
3805 image_nearest_sampler: wgpu::Sampler,
3806 image_linear_sampler: wgpu::Sampler,
3807 text_fonts: SoftwareTextFontSet,
3808 #[cfg(not(target_arch = "wasm32"))]
3810 upload_buffer: wgpu::Buffer,
3811 #[cfg(not(target_arch = "wasm32"))]
3812 uniform_buffer: wgpu::Buffer,
3813 #[cfg(not(target_arch = "wasm32"))]
3814 uniform_bind_group: wgpu::BindGroup,
3815 #[cfg(not(target_arch = "wasm32"))]
3816 shape_buffers: ShapeBatchBuffers,
3817 #[cfg(not(target_arch = "wasm32"))]
3818 image_vertex_buffer: wgpu::Buffer,
3819 #[cfg(not(target_arch = "wasm32"))]
3820 image_index_buffer: wgpu::Buffer,
3821 #[cfg(not(target_arch = "wasm32"))]
3822 retained_glyph_uniform_buffer: wgpu::Buffer,
3823 #[cfg(not(target_arch = "wasm32"))]
3824 retained_glyph_uniform_bind_group: wgpu::BindGroup,
3825 #[cfg(not(target_arch = "wasm32"))]
3826 retained_glyph_uniform_stride: u64,
3827 #[cfg(not(target_arch = "wasm32"))]
3828 retained_glyph_uniform_capacity: usize,
3829 #[cfg(not(target_arch = "wasm32"))]
3830 retained_glyph_uniform_cursor: usize,
3831 #[cfg(target_arch = "wasm32")]
3832 wasm_uniform_batches: Vec<UniformBatchBuffer>,
3833 #[cfg(target_arch = "wasm32")]
3834 wasm_uniform_batch_cursor: usize,
3835 #[cfg(target_arch = "wasm32")]
3836 wasm_shape_batches: Vec<ShapeBatchBuffers>,
3837 #[cfg(target_arch = "wasm32")]
3838 wasm_shape_batch_cursor: usize,
3839 #[cfg(target_arch = "wasm32")]
3840 wasm_image_batches: Vec<ImageBatchBuffers>,
3841 #[cfg(target_arch = "wasm32")]
3842 wasm_image_batch_cursor: usize,
3843 image_texture_cache: BoundedLruCache<u64, CachedImageTexture>,
3844 image_texture_cache_bytes: usize,
3846 text_image_cache: BoundedLruCache<TextImageCacheKey, CachedTextImage>,
3847 text_glyph_atlas: TextGlyphAtlas,
3848 text_glyph_run_cache: BoundedLruCache<TextGlyphRunCacheKey, CachedTextGlyphRun>,
3849 #[cfg(not(target_arch = "wasm32"))]
3850 text_glyph_gpu_run_cache: BoundedLruCache<TextGlyphRunCacheKey, CachedGpuTextGlyphRun>,
3851 text_glyph_mask_cache: SoftwareGlyphRasterCache,
3852 text_line_index_cache: TextLineIndexCache,
3853 scratch_shape_data: Vec<ShapeData>,
3854 scratch_gradients: Vec<GradientStop>,
3855 scratch_image_vertices: Vec<Vertex>,
3856 scratch_image_indices: Vec<u32>,
3857 scratch_image_cmds: Vec<ImageDrawCmd>,
3858 scratch_glyph_cmds: Vec<GlyphDrawCmd>,
3859 scratch_text_glyph_run: Vec<SoftwareGlyphAtlasRunGlyph>,
3860 scratch_text_glyph_placements: Vec<SoftwareGlyphAtlasPlacement>,
3861 scratch_text_glyph_quads: Vec<CachedTextGlyphQuad>,
3862 scratch_segment_items: Vec<(usize, SegmentDrawItem)>,
3863 scratch_effect_ranges: Vec<Range<usize>>,
3864 scratch_layer_events: Vec<LayerEvent>,
3865 staged_uploads: StagedBufferUploads,
3866 frame_graph_executor: WgpuFrameGraphExecutor,
3867 deferred_offscreen_releases: Vec<OffscreenTarget>,
3868 effect_renderer: EffectRenderer,
3869 layer_surface_cache: LayerSurfaceCache,
3870 observed_scene_range_cache_misses: BoundedLruCache<LayerRasterCacheKey, ()>,
3871 shadow_surface_cache: BoundedLruCache<ShadowSurfaceCacheKey, CachedShadowSurface>,
3872 shadow_surface_cache_bytes: u64,
3873 frame_stats: gpu_stats::FrameStats,
3874 last_frame_stats: Option<gpu_stats::FrameStatsSnapshot>,
3875 pending_frame_warmup_frames: u8,
3876 frame_count: u64,
3877 gpu_stats_enabled: bool,
3878 warning_state: RendererWarningState,
3879 #[cfg(not(target_arch = "wasm32"))]
3880 replay_upload_stats: ReplayUploadStats,
3881 #[cfg(not(target_arch = "wasm32"))]
3888 replay_color_patches: Vec<crate::scene::ColorPatch>,
3889 #[cfg(not(target_arch = "wasm32"))]
3893 color_patch_scratch: Vec<crate::scene::ColorPatch>,
3894 #[cfg(not(target_arch = "wasm32"))]
3899 replay_ack_confirmations: Vec<crate::frame_packet::ReplayConfirmation>,
3900 #[cfg(not(target_arch = "wasm32"))]
3905 replay_generation_drops: u64,
3906 #[cfg(not(target_arch = "wasm32"))]
3909 retained_bundle_cache: RetainedBundleCache,
3910}
3911
3912#[cfg(not(target_arch = "wasm32"))]
3918#[derive(Default)]
3919struct ReplayUploadStats {
3920 calls: u64,
3921 patched_calls: u64,
3922 patches: u64,
3923 slots: u64,
3924 records: u64,
3925 bytes: u64,
3926 ideal_bytes: u64,
3927 max_frame_bytes: u64,
3928}
3929
3930#[cfg(not(target_arch = "wasm32"))]
3931impl ReplayUploadStats {
3932 const REPORT_CALLS: u64 = 1024;
3942
3943 fn note_frame(&mut self, patches: u64, slots: u64, records: u64, bytes: u64, ideal: u64) {
3944 self.calls += 1;
3945 if patches > 0 {
3946 self.patched_calls += 1;
3947 self.patches += patches;
3948 self.slots += slots;
3949 self.records += records;
3950 self.bytes += bytes;
3951 self.ideal_bytes += ideal;
3952 self.max_frame_bytes = self.max_frame_bytes.max(bytes);
3953 }
3954 if self.calls >= Self::REPORT_CALLS {
3955 let patched = self.patched_calls.max(1);
3956 log::warn!(
3957 "[replay-upload] {} patched of {} drains: avg {:.1} KB/frame (max {:.1} KB), \
3958 color-only would be {:.1} KB/frame; avg {} patches over {} records in {} slots",
3959 self.patched_calls,
3960 self.calls,
3961 self.bytes as f64 / patched as f64 / 1024.0,
3962 self.max_frame_bytes as f64 / 1024.0,
3963 self.ideal_bytes as f64 / patched as f64 / 1024.0,
3964 self.patches / patched,
3965 self.records / patched,
3966 self.slots / patched,
3967 );
3968 *self = Self::default();
3969 }
3970 }
3971}
3972
3973fn image_sampler_descriptor(sampling: ImageSampling) -> wgpu::SamplerDescriptor<'static> {
3974 let filter = match sampling {
3975 ImageSampling::Nearest => wgpu::FilterMode::Nearest,
3976 ImageSampling::Linear => wgpu::FilterMode::Linear,
3977 };
3978 wgpu::SamplerDescriptor {
3979 label: Some(match sampling {
3980 ImageSampling::Nearest => "Nearest Image Sampler",
3981 ImageSampling::Linear => "Linear Image Sampler",
3982 }),
3983 address_mode_u: wgpu::AddressMode::ClampToEdge,
3984 address_mode_v: wgpu::AddressMode::ClampToEdge,
3985 address_mode_w: wgpu::AddressMode::ClampToEdge,
3986 mag_filter: filter,
3987 min_filter: filter,
3988 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
3989 ..Default::default()
3990 }
3991}
3992
3993#[cfg(test)]
3994fn layer_raster_cache_candidate(
3995 layer: &LayerNode,
3996 root_scale: f32,
3997 has_backdrop_underlay: bool,
3998 allow_runtime_cache: bool,
3999) -> Option<(LayerRasterCacheKey, Rect)> {
4000 let mut layer_surface_requirements_cache = HashMap::new();
4001 let surface_requirements =
4002 layer_surface_requirements_cached(layer, &mut layer_surface_requirements_cache);
4003 let runtime_cache_is_safe = allow_runtime_cache
4004 && surface_requirements
4005 .surface_requirements
4006 .has_isolating_requirement()
4007 && !layer
4008 .effect()
4009 .is_some_and(RenderEffect::contains_runtime_shader);
4010 let cache_is_allowed = layer.cache_policy == CachePolicy::Auto
4011 || (allow_runtime_cache && surface_requirements.has_renderer_forced_surface())
4012 || runtime_cache_is_safe;
4013 if !cache_is_allowed {
4014 return None;
4015 }
4016 if layer_uses_external_backdrop_input(layer, has_backdrop_underlay) {
4017 return None;
4018 }
4019 if layer
4020 .effect()
4021 .is_some_and(RenderEffect::contains_runtime_shader)
4022 {
4023 return None;
4024 }
4025
4026 let logical_rect = estimate_layer_surface_rect(layer);
4027 let pixel_size = surface_target_size(logical_rect, root_scale, u32::MAX);
4028 Some((
4029 LayerRasterCacheKey::new(
4030 layer.node_id,
4031 layer.target_content_hash(),
4032 layer.effect_hash(),
4033 logical_rect,
4034 pixel_size,
4035 ScaleBucket::from_scale(root_scale),
4036 ),
4037 logical_rect,
4038 ))
4039}
4040
4041impl GpuRenderer {
4042 pub fn new(
4043 device: Arc<wgpu::Device>,
4044 queue: Arc<wgpu::Queue>,
4045 surface_format: wgpu::TextureFormat,
4046 adapter_backend: wgpu::Backend,
4047 text_fonts: SoftwareTextFontSet,
4048 renderer_epoch: u64,
4049 store_feed_generation: u64,
4050 ) -> Self {
4051 #[cfg(target_arch = "wasm32")]
4052 let _ = store_feed_generation;
4053 let shape_batch_limits = ShapeBatchLimits::for_device(&device);
4054 let uniform_bind_group_layout =
4055 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
4056 label: Some("Uniform Bind Group Layout"),
4057 entries: &[wgpu::BindGroupLayoutEntry {
4058 binding: 0,
4059 visibility: wgpu::ShaderStages::VERTEX,
4060 ty: wgpu::BindingType::Buffer {
4061 ty: wgpu::BufferBindingType::Uniform,
4062 has_dynamic_offset: false,
4063 min_binding_size: None,
4064 },
4065 count: None,
4066 }],
4067 });
4068 #[cfg(not(target_arch = "wasm32"))]
4069 let retained_glyph_uniform_bind_group_layout =
4070 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
4071 label: Some("Retained Glyph Dynamic Uniform Bind Group Layout"),
4072 entries: &[wgpu::BindGroupLayoutEntry {
4073 binding: 0,
4074 visibility: wgpu::ShaderStages::VERTEX,
4075 ty: wgpu::BindingType::Buffer {
4076 ty: wgpu::BufferBindingType::Uniform,
4077 has_dynamic_offset: true,
4078 min_binding_size: wgpu::BufferSize::new(
4079 std::mem::size_of::<Uniforms>() as u64
4080 ),
4081 },
4082 count: None,
4083 }],
4084 });
4085
4086 let mut shape_bind_group_layout_entries = vec![
4095 wgpu::BindGroupLayoutEntry {
4096 binding: 0,
4097 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
4098 ty: wgpu::BindingType::Buffer {
4099 ty: shape_batch_limits.data_binding_type(),
4100 has_dynamic_offset: false,
4101 min_binding_size: None,
4102 },
4103 count: None,
4104 },
4105 wgpu::BindGroupLayoutEntry {
4106 binding: 1,
4107 visibility: wgpu::ShaderStages::FRAGMENT,
4108 ty: wgpu::BindingType::Buffer {
4109 ty: shape_batch_limits.data_binding_type(),
4110 has_dynamic_offset: false,
4111 min_binding_size: None,
4112 },
4113 count: None,
4114 },
4115 wgpu::BindGroupLayoutEntry {
4120 binding: 2,
4121 visibility: wgpu::ShaderStages::VERTEX,
4122 ty: wgpu::BindingType::Buffer {
4123 ty: wgpu::BufferBindingType::Uniform,
4124 has_dynamic_offset: true,
4125 min_binding_size: wgpu::BufferSize::new(
4126 std::mem::size_of::<SimilarityTransform>() as u64,
4127 ),
4128 },
4129 count: None,
4130 },
4131 ];
4132 if shape_batch_limits.storage {
4138 shape_bind_group_layout_entries.push(wgpu::BindGroupLayoutEntry {
4139 binding: 3,
4140 visibility: wgpu::ShaderStages::VERTEX,
4141 ty: wgpu::BindingType::Buffer {
4142 ty: wgpu::BufferBindingType::Storage { read_only: true },
4143 has_dynamic_offset: false,
4144 min_binding_size: None,
4145 },
4146 count: None,
4147 });
4148 }
4149 let shape_bind_group_layout =
4150 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
4151 label: Some("Shape Bind Group Layout"),
4152 entries: &shape_bind_group_layout_entries,
4153 });
4154
4155 let identity_similarity_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4156 label: Some("Identity Similarity Buffer"),
4157 size: std::mem::size_of::<SimilarityTransform>() as u64,
4158 usage: wgpu::BufferUsages::UNIFORM,
4159 mapped_at_creation: true,
4160 });
4161 identity_similarity_buffer
4162 .slice(..)
4163 .get_mapped_range_mut()
4164 .copy_from_slice(bytemuck::bytes_of(&SimilarityTransform::IDENTITY));
4165 identity_similarity_buffer.unmap();
4166
4167 let dummy_paint_buffer = shape_batch_limits.storage.then(|| {
4172 device.create_buffer(&wgpu::BufferDescriptor {
4173 label: Some("Dummy Paint Buffer"),
4174 size: std::mem::size_of::<[f32; 4]>() as u64,
4175 usage: wgpu::BufferUsages::STORAGE,
4176 mapped_at_creation: false,
4177 })
4178 });
4179 #[cfg(not(target_arch = "wasm32"))]
4180 let replay_slot_store = ReplaySlotStore::new(&device);
4181
4182 let pipeline = create_shape_pipeline(
4183 &device,
4184 surface_format,
4185 &uniform_bind_group_layout,
4186 &shape_bind_group_layout,
4187 BlendMode::SrcOver,
4188 shape_batch_limits,
4189 );
4190 let pipeline_dst_out = create_shape_pipeline(
4191 &device,
4192 surface_format,
4193 &uniform_bind_group_layout,
4194 &shape_bind_group_layout,
4195 BlendMode::DstOut,
4196 shape_batch_limits,
4197 );
4198 #[cfg(not(target_arch = "wasm32"))]
4199 let mesh_pipeline = shape_batch_limits.storage.then(|| {
4200 create_mesh_shape_pipeline(
4201 &device,
4202 surface_format,
4203 &uniform_bind_group_layout,
4204 &shape_bind_group_layout,
4205 shape_batch_limits,
4206 )
4207 });
4208 #[cfg(not(target_arch = "wasm32"))]
4214 let instanced_quads =
4215 (shape_batch_limits.storage && instanced_quads_enabled()).then(|| {
4216 let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4217 label: Some("Instanced Quad Index Buffer"),
4218 size: std::mem::size_of_val(&INSTANCED_QUAD_INDICES) as u64,
4219 usage: wgpu::BufferUsages::INDEX,
4220 mapped_at_creation: true,
4221 });
4222 index_buffer
4223 .slice(..)
4224 .get_mapped_range_mut()
4225 .copy_from_slice(bytemuck::cast_slice(&INSTANCED_QUAD_INDICES));
4226 index_buffer.unmap();
4227 InstancedQuadPipelines {
4228 pipeline: create_instanced_shape_pipeline(
4229 &device,
4230 surface_format,
4231 &uniform_bind_group_layout,
4232 &shape_bind_group_layout,
4233 BlendMode::SrcOver,
4234 shape_batch_limits,
4235 ),
4236 pipeline_dst_out: create_instanced_shape_pipeline(
4237 &device,
4238 surface_format,
4239 &uniform_bind_group_layout,
4240 &shape_bind_group_layout,
4241 BlendMode::DstOut,
4242 shape_batch_limits,
4243 ),
4244 index_buffer,
4245 }
4246 });
4247
4248 let image_bind_group_layout =
4249 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
4250 label: Some("Image Texture Bind Group Layout"),
4251 entries: &[
4252 wgpu::BindGroupLayoutEntry {
4253 binding: 0,
4254 visibility: wgpu::ShaderStages::FRAGMENT,
4255 ty: wgpu::BindingType::Texture {
4256 multisampled: false,
4257 view_dimension: wgpu::TextureViewDimension::D2,
4258 sample_type: wgpu::TextureSampleType::Float { filterable: true },
4259 },
4260 count: None,
4261 },
4262 wgpu::BindGroupLayoutEntry {
4263 binding: 1,
4264 visibility: wgpu::ShaderStages::FRAGMENT,
4265 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
4266 count: None,
4267 },
4268 ],
4269 });
4270
4271 let image_pipeline = create_image_pipeline(
4272 &device,
4273 surface_format,
4274 &uniform_bind_group_layout,
4275 &image_bind_group_layout,
4276 BlendMode::SrcOver,
4277 );
4278 let image_pipeline_dst_out = create_image_pipeline(
4279 &device,
4280 surface_format,
4281 &uniform_bind_group_layout,
4282 &image_bind_group_layout,
4283 BlendMode::DstOut,
4284 );
4285 let glyph_atlas_pipeline = create_glyph_atlas_pipeline(
4286 &device,
4287 surface_format,
4288 &uniform_bind_group_layout,
4289 &image_bind_group_layout,
4290 );
4291 #[cfg(not(target_arch = "wasm32"))]
4292 let retained_glyph_atlas_pipeline = create_glyph_atlas_pipeline(
4293 &device,
4294 surface_format,
4295 &retained_glyph_uniform_bind_group_layout,
4296 &image_bind_group_layout,
4297 );
4298
4299 #[cfg(not(target_arch = "wasm32"))]
4300 let upload_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4301 label: Some("Frame Upload Buffer"),
4302 size: INITIAL_UPLOAD_BUFFER_BYTES,
4303 usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
4304 mapped_at_creation: false,
4305 });
4306
4307 #[cfg(not(target_arch = "wasm32"))]
4308 let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4309 label: Some("Uniform Buffer"),
4310 size: std::mem::size_of::<Uniforms>() as u64,
4311 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
4312 mapped_at_creation: false,
4313 });
4314
4315 #[cfg(not(target_arch = "wasm32"))]
4316 let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
4317 label: Some("Uniform Bind Group"),
4318 layout: &uniform_bind_group_layout,
4319 entries: &[wgpu::BindGroupEntry {
4320 binding: 0,
4321 resource: uniform_buffer.as_entire_binding(),
4322 }],
4323 });
4324
4325 #[cfg(not(target_arch = "wasm32"))]
4326 let shape_buffers = ShapeBatchBuffers::new(
4327 &device,
4328 &shape_bind_group_layout,
4329 &identity_similarity_buffer,
4330 dummy_paint_buffer.as_ref(),
4331 shape_batch_limits,
4332 );
4333
4334 let image_nearest_sampler =
4335 device.create_sampler(&image_sampler_descriptor(ImageSampling::Nearest));
4336 let image_linear_sampler =
4337 device.create_sampler(&image_sampler_descriptor(ImageSampling::Linear));
4338 let text_glyph_atlas = TextGlyphAtlas::new(
4339 &device,
4340 &image_bind_group_layout,
4341 &image_nearest_sampler,
4342 TEXT_GLYPH_ATLAS_MIN_SIZE,
4343 );
4344
4345 #[cfg(not(target_arch = "wasm32"))]
4346 let image_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4347 label: Some("Image Vertex Buffer"),
4348 size: (std::mem::size_of::<Vertex>() * 4) as u64,
4349 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
4350 mapped_at_creation: false,
4351 });
4352
4353 #[cfg(not(target_arch = "wasm32"))]
4354 let image_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4355 label: Some("Image Index Buffer"),
4356 size: (std::mem::size_of::<u32>() * 6) as u64,
4357 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
4358 mapped_at_creation: false,
4359 });
4360 #[cfg(not(target_arch = "wasm32"))]
4361 let retained_glyph_uniform_stride = align_usize_to(
4362 std::mem::size_of::<Uniforms>(),
4363 (device.limits().min_uniform_buffer_offset_alignment as usize)
4364 .max(wgpu::COPY_BUFFER_ALIGNMENT as usize),
4365 ) as u64;
4366 #[cfg(not(target_arch = "wasm32"))]
4367 let retained_glyph_uniform_capacity = INITIAL_RETAINED_GLYPH_UNIFORM_SLOTS;
4368 #[cfg(not(target_arch = "wasm32"))]
4369 let retained_glyph_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4370 label: Some("Retained Glyph Uniform Buffer"),
4371 size: retained_glyph_uniform_stride * retained_glyph_uniform_capacity as u64,
4372 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
4373 mapped_at_creation: false,
4374 });
4375 #[cfg(not(target_arch = "wasm32"))]
4376 let retained_glyph_uniform_bind_group =
4377 device.create_bind_group(&wgpu::BindGroupDescriptor {
4378 label: Some("Retained Glyph Uniform Bind Group"),
4379 layout: &retained_glyph_uniform_bind_group_layout,
4380 entries: &[wgpu::BindGroupEntry {
4381 binding: 0,
4382 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
4383 buffer: &retained_glyph_uniform_buffer,
4384 offset: 0,
4385 size: wgpu::BufferSize::new(std::mem::size_of::<Uniforms>() as u64),
4386 }),
4387 }],
4388 });
4389
4390 let effect_renderer = EffectRenderer::new(&device, surface_format, adapter_backend);
4391
4392 Self {
4393 device,
4394 queue,
4395 renderer_epoch,
4396 #[cfg(not(target_arch = "wasm32"))]
4397 store_feed_generation,
4398 surface_format,
4399 shape_batch_limits,
4400 pipeline,
4401 pipeline_dst_out,
4402 #[cfg(not(target_arch = "wasm32"))]
4403 mesh_pipeline,
4404 #[cfg(not(target_arch = "wasm32"))]
4405 instanced_quads,
4406 #[cfg(target_arch = "wasm32")]
4407 uniform_bind_group_layout,
4408 shape_bind_group_layout,
4409 dummy_paint_buffer,
4410 identity_similarity_buffer,
4411 #[cfg(not(target_arch = "wasm32"))]
4412 replay_slots: replay_slot_store,
4413 image_pipeline,
4414 image_pipeline_dst_out,
4415 glyph_atlas_pipeline,
4416 #[cfg(not(target_arch = "wasm32"))]
4417 retained_glyph_atlas_pipeline,
4418 image_bind_group_layout,
4419 #[cfg(not(target_arch = "wasm32"))]
4420 retained_glyph_uniform_bind_group_layout,
4421 image_nearest_sampler,
4422 image_linear_sampler,
4423 text_fonts,
4424 #[cfg(not(target_arch = "wasm32"))]
4425 upload_buffer,
4426 #[cfg(not(target_arch = "wasm32"))]
4427 uniform_buffer,
4428 #[cfg(not(target_arch = "wasm32"))]
4429 uniform_bind_group,
4430 #[cfg(not(target_arch = "wasm32"))]
4431 shape_buffers,
4432 #[cfg(not(target_arch = "wasm32"))]
4433 image_vertex_buffer,
4434 #[cfg(not(target_arch = "wasm32"))]
4435 image_index_buffer,
4436 #[cfg(not(target_arch = "wasm32"))]
4437 retained_glyph_uniform_buffer,
4438 #[cfg(not(target_arch = "wasm32"))]
4439 retained_glyph_uniform_bind_group,
4440 #[cfg(not(target_arch = "wasm32"))]
4441 retained_glyph_uniform_stride,
4442 #[cfg(not(target_arch = "wasm32"))]
4443 retained_glyph_uniform_capacity,
4444 #[cfg(not(target_arch = "wasm32"))]
4445 retained_glyph_uniform_cursor: 0,
4446 #[cfg(target_arch = "wasm32")]
4447 wasm_uniform_batches: Vec::new(),
4448 #[cfg(target_arch = "wasm32")]
4449 wasm_uniform_batch_cursor: 0,
4450 #[cfg(target_arch = "wasm32")]
4451 wasm_shape_batches: Vec::new(),
4452 #[cfg(target_arch = "wasm32")]
4453 wasm_shape_batch_cursor: 0,
4454 #[cfg(target_arch = "wasm32")]
4455 wasm_image_batches: Vec::new(),
4456 #[cfg(target_arch = "wasm32")]
4457 wasm_image_batch_cursor: 0,
4458 image_texture_cache: BoundedLruCache::with_capacity_at_least_one(
4459 MAX_TEXTURE_CACHE_ITEMS,
4460 ),
4461 image_texture_cache_bytes: 0,
4462 text_image_cache: BoundedLruCache::with_capacity_at_least_one(
4463 MAX_TEXT_IMAGE_CACHE_ITEMS,
4464 ),
4465 text_glyph_atlas,
4466 text_glyph_run_cache: BoundedLruCache::with_capacity_at_least_one(
4467 MAX_TEXT_GLYPH_RUN_CACHE_ITEMS,
4468 ),
4469 #[cfg(not(target_arch = "wasm32"))]
4470 text_glyph_gpu_run_cache: BoundedLruCache::with_capacity_at_least_one(
4471 MAX_TEXT_GLYPH_GPU_RUN_CACHE_ITEMS,
4472 ),
4473 text_glyph_mask_cache: SoftwareGlyphRasterCache::with_capacity_at_least_one(
4474 MAX_TEXT_GLYPH_MASK_CACHE_ITEMS,
4475 ),
4476 text_line_index_cache: TextLineIndexCache::new(MAX_TEXT_LINE_INDEX_CACHE_ITEMS),
4477 scratch_shape_data: Vec::new(),
4478 scratch_gradients: Vec::new(),
4479 scratch_image_vertices: Vec::new(),
4480 scratch_image_indices: Vec::new(),
4481 scratch_image_cmds: Vec::new(),
4482 scratch_glyph_cmds: Vec::new(),
4483 scratch_text_glyph_run: Vec::new(),
4484 scratch_text_glyph_placements: Vec::new(),
4485 scratch_text_glyph_quads: Vec::new(),
4486 scratch_segment_items: Vec::new(),
4487 scratch_effect_ranges: Vec::new(),
4488 scratch_layer_events: Vec::new(),
4489 staged_uploads: StagedBufferUploads::default(),
4490 frame_graph_executor: WgpuFrameGraphExecutor::new(),
4491 deferred_offscreen_releases: Vec::new(),
4492 effect_renderer,
4493 layer_surface_cache: LayerSurfaceCache::new(),
4494 observed_scene_range_cache_misses: BoundedLruCache::with_capacity_at_least_one(
4495 MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES,
4496 ),
4497 shadow_surface_cache: BoundedLruCache::with_capacity_at_least_one(
4498 MAX_SHADOW_SURFACE_CACHE_ITEMS,
4499 ),
4500 shadow_surface_cache_bytes: 0,
4501 frame_stats: gpu_stats::FrameStats::default(),
4502 last_frame_stats: None,
4503 pending_frame_warmup_frames: 0,
4504 frame_count: 0,
4505 gpu_stats_enabled: gpu_stats_enabled(),
4506 warning_state: RendererWarningState::default(),
4507 #[cfg(not(target_arch = "wasm32"))]
4508 replay_upload_stats: ReplayUploadStats::default(),
4509 #[cfg(not(target_arch = "wasm32"))]
4510 replay_color_patches: Vec::new(),
4511 #[cfg(not(target_arch = "wasm32"))]
4512 color_patch_scratch: Vec::new(),
4513 #[cfg(not(target_arch = "wasm32"))]
4514 replay_ack_confirmations: Vec::new(),
4515 #[cfg(not(target_arch = "wasm32"))]
4516 replay_generation_drops: 0,
4517 #[cfg(not(target_arch = "wasm32"))]
4518 retained_bundle_cache: RetainedBundleCache::new(),
4519 }
4520 }
4521
4522 fn ensure_image_cached(&mut self, image: &ImageBitmap) -> Result<(), String> {
4523 if self.image_texture_cache.get(&image.id()).is_some() {
4524 return Ok(());
4525 }
4526
4527 let size = wgpu::Extent3d {
4528 width: image.width(),
4529 height: image.height(),
4530 depth_or_array_layers: 1,
4531 };
4532
4533 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
4534 label: Some("Image Texture"),
4535 size,
4536 mip_level_count: 1,
4537 sample_count: 1,
4538 dimension: wgpu::TextureDimension::D2,
4539 format: wgpu::TextureFormat::Rgba8Unorm,
4540 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
4541 view_formats: &[],
4542 });
4543
4544 let upload_stats = self.frame_graph_executor.upload_texture(
4545 &self.queue,
4546 wgpu::TexelCopyTextureInfo {
4547 texture: &texture,
4548 mip_level: 0,
4549 origin: wgpu::Origin3d::ZERO,
4550 aspect: wgpu::TextureAspect::All,
4551 },
4552 image.pixels(),
4553 wgpu::TexelCopyBufferLayout {
4554 offset: 0,
4555 bytes_per_row: Some(4 * image.width()),
4556 rows_per_image: Some(image.height()),
4557 },
4558 size,
4559 );
4560 self.frame_stats.record_command_stats(upload_stats);
4561
4562 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
4563 let nearest_bind_group = self.image_bind_group(&view, &self.image_nearest_sampler);
4564 let linear_bind_group = self.image_bind_group(&view, &self.image_linear_sampler);
4565
4566 let bytes = image.width() as usize * image.height() as usize * 4;
4567 if let Some(replaced) = self.image_texture_cache.put(
4568 image.id(),
4569 CachedImageTexture {
4570 _texture: texture,
4571 _view: view,
4572 nearest_bind_group,
4573 linear_bind_group,
4574 bytes,
4575 },
4576 ) {
4577 self.image_texture_cache_bytes = self
4578 .image_texture_cache_bytes
4579 .saturating_sub(replaced.bytes);
4580 }
4581 self.image_texture_cache_bytes += bytes;
4582 while self.image_texture_cache_bytes > MAX_IMAGE_TEXTURE_CACHE_BYTES
4585 && self.image_texture_cache.len() > 1
4586 {
4587 let Some((_, evicted)) = self.image_texture_cache.pop_lru() else {
4588 break;
4589 };
4590 self.image_texture_cache_bytes =
4591 self.image_texture_cache_bytes.saturating_sub(evicted.bytes);
4592 }
4593 Ok(())
4594 }
4595
4596 fn image_bind_group(
4597 &self,
4598 view: &wgpu::TextureView,
4599 sampler: &wgpu::Sampler,
4600 ) -> wgpu::BindGroup {
4601 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
4602 label: Some("Image Texture Bind Group"),
4603 layout: &self.image_bind_group_layout,
4604 entries: &[
4605 wgpu::BindGroupEntry {
4606 binding: 0,
4607 resource: wgpu::BindingResource::TextureView(view),
4608 },
4609 wgpu::BindGroupEntry {
4610 binding: 1,
4611 resource: wgpu::BindingResource::Sampler(sampler),
4612 },
4613 ],
4614 })
4615 }
4616
4617 fn max_texture_dim(&self) -> u32 {
4620 self.effect_renderer.max_texture_dim()
4621 }
4622
4623 fn acquire_offscreen(&mut self, width: u32, height: u32) -> OffscreenTarget {
4624 self.effect_renderer
4625 .acquire_offscreen(&self.device, width, height, Some(&self.frame_stats))
4626 }
4627
4628 fn acquire_retained_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
4629 self.acquire_offscreen(width, height)
4630 }
4631
4632 fn transient_offscreen_descriptor(
4633 &self,
4634 label: &'static str,
4635 width: u32,
4636 height: u32,
4637 ) -> FrameTextureDescriptor {
4638 let max_texture_dim = self.max_texture_dim();
4639 FrameTextureDescriptor::render_attachment(
4640 label,
4641 width.min(max_texture_dim),
4642 height.min(max_texture_dim),
4643 self.surface_format,
4644 )
4645 }
4646
4647 fn defer_offscreen_release(&mut self, target: OffscreenTarget) {
4648 self.deferred_offscreen_releases.push(target);
4649 }
4650
4651 fn flush_deferred_offscreen_releases(&mut self) {
4652 for target in self.deferred_offscreen_releases.drain(..) {
4653 self.effect_renderer.release_offscreen(target);
4654 }
4655 }
4656
4657 fn release_layer_surface_target(&mut self, target: LayerSurfaceTexture) {
4658 if let LayerSurfaceTexture::Owned(target) = target {
4659 self.defer_offscreen_release(target);
4660 }
4661 }
4662
4663 fn cached_layer_surface(
4664 &mut self,
4665 key: &LayerRasterCacheKey,
4666 ) -> Option<(Rc<OffscreenTarget>, Rect)> {
4667 self.layer_surface_cache.get(key, &self.frame_stats)
4668 }
4669
4670 fn admit_layer_surface_cache_miss(&mut self, key: &LayerRasterCacheKey) -> bool {
4671 admit_layer_surface_cache_miss_impl(key, &mut self.observed_scene_range_cache_misses)
4672 }
4673
4674 fn insert_cached_layer_surface(
4675 &mut self,
4676 key: LayerRasterCacheKey,
4677 target: OffscreenTarget,
4678 logical_rect: Rect,
4679 ) -> Rc<OffscreenTarget> {
4680 self.layer_surface_cache
4681 .insert(key, target, logical_rect, &self.frame_stats)
4682 }
4683
4684 fn cached_shadow_surface(
4685 &mut self,
4686 key: &ShadowSurfaceCacheKey,
4687 ) -> Option<Rc<OffscreenTarget>> {
4688 self.shadow_surface_cache
4689 .get(key)
4690 .map(|cached| cached.target.clone())
4691 }
4692
4693 fn cached_shape_shadow_composite(
4694 &mut self,
4695 shadow: &ShadowDraw,
4696 width: u32,
4697 height: u32,
4698 root_scale: f32,
4699 ) -> Option<CachedShadowComposite> {
4700 if shadow.blur_radius <= 0.0 || shadow.shapes.is_empty() || !shadow.texts.is_empty() {
4701 return None;
4702 }
4703
4704 let plan = shape_shadow_surface_plan(
4705 &shadow.shapes,
4706 shadow.clip,
4707 shadow.blur_radius,
4708 width,
4709 height,
4710 root_scale,
4711 self.max_texture_dim(),
4712 )?;
4713 let key = shape_shadow_surface_cache_key(
4714 &shadow.shapes,
4715 plan.source_device_bounds,
4716 plan.pixel_radius,
4717 root_scale,
4718 )?;
4719 let cached = self.cached_shadow_surface(&key)?;
4720 let viewport_offset = [plan.source_device_bounds.x, plan.source_device_bounds.y];
4721 self.frame_stats.record_shadow_shape_cache_hit(
4722 plan.source_device_bounds.width,
4723 plan.source_device_bounds.height,
4724 );
4725
4726 let clip_scissor = shadow
4727 .clip
4728 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
4729 let scissor = clip_scissor.or(plan.processing_scissor);
4730 let rounded_mask = inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
4731 mask.rect[0] -= viewport_offset[0];
4732 mask.rect[1] -= viewport_offset[1];
4733 mask
4734 });
4735 let dest_viewport = Some((
4736 viewport_offset[0],
4737 viewport_offset[1],
4738 plan.source_device_bounds.width as f32,
4739 plan.source_device_bounds.height as f32,
4740 ));
4741
4742 Some(CachedShadowComposite {
4743 source: cached,
4744 scissor,
4745 rounded_mask,
4746 dest_viewport,
4747 })
4748 }
4749
4750 fn insert_cached_shadow_surface(
4751 &mut self,
4752 key: ShadowSurfaceCacheKey,
4753 target: OffscreenTarget,
4754 ) {
4755 let byte_size = offscreen_byte_size(target.width, target.height);
4756 while self.shadow_surface_cache_bytes + byte_size > MAX_SHADOW_SURFACE_CACHE_BYTES {
4757 let Some((_evicted_key, evicted_entry)) = self.shadow_surface_cache.pop_lru() else {
4758 break;
4759 };
4760 self.shadow_surface_cache_bytes = self
4761 .shadow_surface_cache_bytes
4762 .saturating_sub(evicted_entry.byte_size);
4763 }
4764
4765 let cached = CachedShadowSurface {
4766 target: Rc::new(target),
4767 byte_size,
4768 };
4769 if let Some((_replaced_key, replaced_entry)) = self.shadow_surface_cache.push(key, cached) {
4770 self.shadow_surface_cache_bytes = self
4771 .shadow_surface_cache_bytes
4772 .saturating_sub(replaced_entry.byte_size);
4773 }
4774 self.shadow_surface_cache_bytes = self.shadow_surface_cache_bytes.saturating_add(byte_size);
4775 }
4776
4777 fn supports_render_effect(&self, effect: &RenderEffect) -> bool {
4778 is_render_effect_supported(effect)
4779 }
4780}
4781
4782struct RecordingSurfaceBackend<'renderer, 'recorder, C: FrameCommandRecorder> {
4783 renderer: &'renderer mut GpuRenderer,
4784 recorder: &'recorder mut C,
4785}
4786
4787impl<C: FrameCommandRecorder> RecordingSurfaceBackend<'_, '_, C> {
4788 #[allow(clippy::too_many_arguments)]
4789 fn render_range_with_layer_events_to_target_recorded(
4790 &mut self,
4791 target: &OffscreenTarget,
4792 shapes: &[DrawShape],
4793 images: &[ImageDraw],
4794 texts: &[TextDraw],
4795 shadow_draws: &[ShadowDraw],
4796 draw_ops: &[DrawOp],
4797 effect_layers: &[EffectLayer],
4798 backdrop_layers: &[BackdropLayer],
4799 z_start: usize,
4800 z_end: usize,
4801 excluded_effect_layer: Option<usize>,
4802 width: u32,
4803 height: u32,
4804 root_scale: f32,
4805 backdrop_underlay: Option<&OffscreenTarget>,
4806 initial_load_op: wgpu::LoadOp<wgpu::Color>,
4807 ) -> Result<(), String> {
4808 if z_start >= z_end {
4809 if matches!(initial_load_op, wgpu::LoadOp::Clear(_)) {
4810 self.clear_target_view_with_load_op(&target.view, initial_load_op);
4811 }
4812 return Ok(());
4813 }
4814
4815 let mut effect_z_ranges = std::mem::take(&mut self.renderer.scratch_effect_ranges);
4816 collect_effect_ranges(
4817 effect_layers,
4818 z_start,
4819 z_end,
4820 excluded_effect_layer,
4821 &mut effect_z_ranges,
4822 );
4823 let mut events = std::mem::take(&mut self.renderer.scratch_layer_events);
4824 collect_layer_events(
4825 effect_layers,
4826 backdrop_layers,
4827 z_start,
4828 z_end,
4829 excluded_effect_layer,
4830 &mut events,
4831 );
4832
4833 let result = (|| -> Result<(), String> {
4834 let mut next_load_op = initial_load_op;
4835 let mut cursor_z = z_start;
4836 for event in &events {
4837 if event.z_index > cursor_z {
4838 self.render_non_effect_segment(
4839 &target.view,
4840 shapes,
4841 images,
4842 texts,
4843 shadow_draws,
4844 &[],
4847 draw_ops,
4848 cursor_z,
4849 event.z_index,
4850 &effect_z_ranges,
4851 width,
4852 height,
4853 root_scale,
4854 next_load_op,
4855 )?;
4856 next_load_op = wgpu::LoadOp::Load;
4857 cursor_z = event.z_index;
4858 } else if event.z_index < cursor_z {
4859 continue;
4860 }
4861
4862 if matches!(next_load_op, wgpu::LoadOp::Clear(_)) {
4863 self.clear_target_view_with_load_op(&target.view, next_load_op);
4864 next_load_op = wgpu::LoadOp::Load;
4865 }
4866
4867 match event.kind {
4868 LayerEventKind::Backdrop(index) => {
4869 let layer = &backdrop_layers[index];
4870 let effective_backdrop_underlay = if backdrop_underlay.is_some()
4871 && backdrop_underlay_is_covered_by_local_content(
4872 shapes,
4873 images,
4874 shadow_draws,
4875 draw_ops,
4876 effect_layers,
4877 backdrop_layers,
4878 layer,
4879 ) {
4880 None
4881 } else {
4882 backdrop_underlay
4883 };
4884 execute_apply_backdrop_layer_to_target(
4885 self,
4886 target,
4887 layer,
4888 effective_backdrop_underlay,
4889 width,
4890 height,
4891 root_scale,
4892 None,
4893 )?;
4894 }
4895 LayerEventKind::Effect(index) => {
4896 let layer = &effect_layers[index];
4897 if layer.z_start < cursor_z {
4898 continue;
4899 }
4900 execute_render_effect_layer_to_target(
4901 self,
4902 target,
4903 shapes,
4904 images,
4905 texts,
4906 shadow_draws,
4907 draw_ops,
4908 effect_layers,
4909 backdrop_layers,
4910 index,
4911 backdrop_underlay,
4912 width,
4913 height,
4914 root_scale,
4915 )?;
4916 cursor_z = cursor_z.max(layer.z_end);
4917 }
4918 }
4919 }
4920
4921 if cursor_z < z_end {
4922 self.render_non_effect_segment(
4923 &target.view,
4924 shapes,
4925 images,
4926 texts,
4927 shadow_draws,
4928 &[],
4929 draw_ops,
4930 cursor_z,
4931 z_end,
4932 &effect_z_ranges,
4933 width,
4934 height,
4935 root_scale,
4936 next_load_op,
4937 )?;
4938 } else if matches!(next_load_op, wgpu::LoadOp::Clear(_)) {
4939 self.clear_target_view_with_load_op(&target.view, next_load_op);
4940 }
4941
4942 Ok(())
4943 })();
4944
4945 self.renderer.scratch_effect_ranges = effect_z_ranges;
4946 self.renderer.scratch_layer_events = events;
4947 result
4948 }
4949
4950 #[allow(clippy::too_many_arguments)]
4951 fn record_shader_composite(
4952 &mut self,
4953 source: &OffscreenTarget,
4954 shader: &RuntimeShader,
4955 effect_rect: [f32; 4],
4956 dest_view: &wgpu::TextureView,
4957 alpha: f32,
4958 load_op: wgpu::LoadOp<wgpu::Color>,
4959 scissor: Option<(u32, u32, u32, u32)>,
4960 blend_mode: BlendMode,
4961 dest_viewport: Option<(f32, f32, f32, f32)>,
4962 sample_mode: CompositeSampleMode,
4963 ) {
4964 let device = self.renderer.device.clone();
4965 if let Some(viewport) = direct_shader_composite_viewport(
4966 alpha,
4967 blend_mode,
4968 dest_viewport,
4969 sample_mode,
4970 (source.width, source.height),
4971 ) {
4972 let shader_applied = self
4973 .renderer
4974 .effect_renderer
4975 .encode_shader_src_over_to_view(
4976 self.recorder,
4977 &device,
4978 source,
4979 dest_view,
4980 shader,
4981 effect_rect,
4982 load_op,
4983 scissor,
4984 viewport,
4985 );
4986 if shader_applied {
4987 self.renderer
4988 .effect_renderer
4989 .debug_effects
4990 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
4991 self.recorder.record_pass();
4992 self.renderer.effect_renderer.record_composite_pass();
4993 return;
4994 }
4995 }
4996 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
4997 "Shader Effect Composite Scratch",
4998 source.width,
4999 source.height,
5000 );
5001 let scratch = self
5002 .recorder
5003 .acquire_transient_offscreen(&device, scratch_descriptor);
5004 let shader_applied = {
5005 self.renderer.effect_renderer.encode_shader(
5006 self.recorder,
5007 &device,
5008 source,
5009 &scratch.view,
5010 shader,
5011 effect_rect,
5012 )
5013 };
5014 let composite_source = if shader_applied {
5015 self.renderer
5016 .effect_renderer
5017 .debug_effects
5018 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
5019 self.recorder.record_pass();
5020 &scratch
5021 } else {
5022 source
5023 };
5024 {
5025 self.renderer
5026 .effect_renderer
5027 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
5028 self.recorder,
5029 &device,
5030 composite_source,
5031 dest_view,
5032 alpha,
5033 load_op,
5034 scissor,
5035 None,
5036 supported_blend_mode(blend_mode),
5037 dest_viewport,
5038 sample_mode,
5039 );
5040 }
5041 self.recorder.record_pass();
5042 self.renderer.effect_renderer.record_composite_pass();
5043 self.recorder
5044 .release_transient_offscreen(scratch_descriptor, scratch);
5045 }
5046
5047 #[allow(clippy::too_many_arguments)]
5048 fn record_shader_projective_composite(
5049 &mut self,
5050 source: &OffscreenTarget,
5051 shader: &RuntimeShader,
5052 effect_rect: [f32; 4],
5053 dest_view: &wgpu::TextureView,
5054 viewport: (u32, u32),
5055 source_size: (f32, f32),
5056 inverse_matrix: [[f32; 3]; 3],
5057 dest_bounds: [[f32; 2]; 4],
5058 alpha: f32,
5059 load_op: wgpu::LoadOp<wgpu::Color>,
5060 scissor: Option<(u32, u32, u32, u32)>,
5061 blend_mode: BlendMode,
5062 sample_mode: CompositeSampleMode,
5063 ) {
5064 if projective_dest_bounds_rect(dest_bounds).is_none() {
5065 return;
5066 }
5067 let device = self.renderer.device.clone();
5068 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
5069 "Shader Projective Composite Scratch",
5070 source.width,
5071 source.height,
5072 );
5073 let scratch = self
5074 .recorder
5075 .acquire_transient_offscreen(&device, scratch_descriptor);
5076 let shader_applied = {
5077 self.renderer.effect_renderer.encode_shader(
5078 self.recorder,
5079 &device,
5080 source,
5081 &scratch.view,
5082 shader,
5083 effect_rect,
5084 )
5085 };
5086 let composite_source = if shader_applied {
5087 self.renderer
5088 .effect_renderer
5089 .debug_effects
5090 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
5091 self.recorder.record_pass();
5092 &scratch
5093 } else {
5094 source
5095 };
5096 let composited = {
5097 self.renderer
5098 .effect_renderer
5099 .encode_composite_to_view_projective(
5100 self.recorder,
5101 &device,
5102 composite_source,
5103 dest_view,
5104 viewport,
5105 source_size,
5106 inverse_matrix,
5107 dest_bounds,
5108 alpha,
5109 load_op,
5110 scissor,
5111 supported_blend_mode(blend_mode),
5112 sample_mode,
5113 )
5114 };
5115 if composited {
5116 self.recorder.record_pass();
5117 self.renderer.effect_renderer.record_composite_pass();
5118 }
5119 self.recorder
5120 .release_transient_offscreen(scratch_descriptor, scratch);
5121 }
5122
5123 #[allow(clippy::too_many_arguments)]
5124 fn record_effect_with_direct_shader_tail_composite(
5125 &mut self,
5126 source: &OffscreenTarget,
5127 first_effect: &RenderEffect,
5128 shader: &RuntimeShader,
5129 effect_rect: [f32; 4],
5130 dest_view: &wgpu::TextureView,
5131 load_op: wgpu::LoadOp<wgpu::Color>,
5132 scissor: Option<(u32, u32, u32, u32)>,
5133 dest_viewport: (f32, f32, f32, f32),
5134 ) -> Result<bool, String> {
5135 let device = self.renderer.device.clone();
5136 let intermediate_descriptor = self.renderer.transient_offscreen_descriptor(
5137 "Render Effect Direct Shader Tail Intermediate",
5138 source.width,
5139 source.height,
5140 );
5141 let intermediate = self
5142 .recorder
5143 .acquire_transient_offscreen(&device, intermediate_descriptor);
5144 let effect_scratch_targets = self
5145 .renderer
5146 .effect_renderer
5147 .acquire_recorded_effect_scratch_targets(
5148 self.recorder,
5149 &device,
5150 first_effect,
5151 source.width,
5152 source.height,
5153 self.renderer.surface_format,
5154 );
5155 let first_passes = {
5156 let mut effect_scratch_refs = effect_scratch_targets.refs();
5157 let pass_count = self.renderer.effect_renderer.encode_effect(
5158 self.recorder,
5159 &device,
5160 source,
5161 &intermediate.view,
5162 first_effect,
5163 effect_rect,
5164 &mut effect_scratch_refs,
5165 );
5166 match pass_count {
5167 Ok(pass_count) => effect_scratch_refs.assert_consumed().map(|()| pass_count),
5168 Err(error) => Err(error),
5169 }
5170 };
5171 let first_passes = match first_passes {
5172 Ok(pass_count) => pass_count,
5173 Err(error) => {
5174 effect_scratch_targets.release_into(self.recorder);
5175 self.recorder
5176 .release_transient_offscreen(intermediate_descriptor, intermediate);
5177 return Err(error);
5178 }
5179 };
5180 let shader_applied = self
5181 .renderer
5182 .effect_renderer
5183 .encode_shader_src_over_to_view(
5184 self.recorder,
5185 &device,
5186 &intermediate,
5187 dest_view,
5188 shader,
5189 effect_rect,
5190 load_op,
5191 scissor,
5192 dest_viewport,
5193 );
5194 self.recorder
5195 .record_passes(first_passes.saturating_add(u32::from(shader_applied)));
5196 effect_scratch_targets.release_into(self.recorder);
5197 self.recorder
5198 .release_transient_offscreen(intermediate_descriptor, intermediate);
5199 if !shader_applied {
5200 return Ok(false);
5201 }
5202 self.renderer
5203 .effect_renderer
5204 .debug_effects
5205 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
5206 self.renderer.effect_renderer.record_composite_pass();
5207 Ok(true)
5208 }
5209
5210 #[allow(clippy::too_many_arguments)]
5211 fn record_effect_composite(
5212 &mut self,
5213 source: &OffscreenTarget,
5214 effect: &RenderEffect,
5215 effect_rect: [f32; 4],
5216 dest_view: &wgpu::TextureView,
5217 alpha: f32,
5218 load_op: wgpu::LoadOp<wgpu::Color>,
5219 scissor: Option<(u32, u32, u32, u32)>,
5220 blend_mode: BlendMode,
5221 dest_viewport: Option<(f32, f32, f32, f32)>,
5222 sample_mode: CompositeSampleMode,
5223 ) -> Result<(), String> {
5224 if let (
5225 RenderEffect::Chain { first, second },
5226 Some(viewport),
5227 BlendMode::SrcOver,
5228 CompositeSampleMode::Linear,
5229 ) = (
5230 effect,
5231 dest_viewport,
5232 supported_blend_mode(blend_mode),
5233 sample_mode,
5234 ) {
5235 if let (
5236 RenderEffect::Blur {
5237 radius_x,
5238 radius_y,
5239 edge_treatment,
5240 },
5241 RenderEffect::Shader { shader },
5242 ) = (first.as_ref(), second.as_ref())
5243 {
5244 if *radius_x > 0.0 || *radius_y > 0.0 {
5245 let device = self.renderer.device.clone();
5246 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
5247 "Blur Rounded Mask Scratch",
5248 source.width,
5249 source.height,
5250 );
5251 let scratch = self
5252 .recorder
5253 .acquire_transient_offscreen(&device, scratch_descriptor);
5254 let fused = self
5255 .renderer
5256 .effect_renderer
5257 .encode_blur_then_rounded_mask_src_over_to_view(
5258 self.recorder,
5259 &device,
5260 source,
5261 &scratch,
5262 dest_view,
5263 *radius_x,
5264 *radius_y,
5265 *edge_treatment,
5266 shader,
5267 effect_rect,
5268 load_op,
5269 scissor,
5270 viewport,
5271 );
5272 if fused {
5273 self.recorder.record_passes(2);
5274 self.renderer.effect_renderer.record_blur_pass();
5275 self.renderer
5276 .effect_renderer
5277 .debug_effects
5278 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
5279 self.renderer.effect_renderer.record_composite_pass();
5280 self.recorder
5281 .release_transient_offscreen(scratch_descriptor, scratch);
5282 return Ok(());
5283 }
5284 self.recorder
5285 .release_transient_offscreen(scratch_descriptor, scratch);
5286 }
5287 }
5288 }
5289 if let Some((first_effect, shader, viewport)) = direct_shader_tail_composite(
5290 effect,
5291 alpha,
5292 blend_mode,
5293 dest_viewport,
5294 sample_mode,
5295 (source.width, source.height),
5296 ) {
5297 if self.record_effect_with_direct_shader_tail_composite(
5298 source,
5299 first_effect,
5300 shader,
5301 effect_rect,
5302 dest_view,
5303 load_op,
5304 scissor,
5305 viewport,
5306 )? {
5307 return Ok(());
5308 }
5309 }
5310 let device = self.renderer.device.clone();
5311 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
5312 "Render Effect Composite Scratch",
5313 source.width,
5314 source.height,
5315 );
5316 let scratch = self
5317 .recorder
5318 .acquire_transient_offscreen(&device, scratch_descriptor);
5319 let effect_scratch_targets = self
5320 .renderer
5321 .effect_renderer
5322 .acquire_recorded_effect_scratch_targets(
5323 self.recorder,
5324 &device,
5325 effect,
5326 source.width,
5327 source.height,
5328 self.renderer.surface_format,
5329 );
5330 let effect_passes = {
5331 let mut effect_scratch_refs = effect_scratch_targets.refs();
5332 let pass_count = self.renderer.effect_renderer.encode_effect(
5333 self.recorder,
5334 &device,
5335 source,
5336 &scratch.view,
5337 effect,
5338 effect_rect,
5339 &mut effect_scratch_refs,
5340 )?;
5341 effect_scratch_refs.assert_consumed()?;
5342 Ok(pass_count)
5343 };
5344 let effect_passes = match effect_passes {
5345 Ok(pass_count) => pass_count,
5346 Err(error) => {
5347 effect_scratch_targets.release_into(self.recorder);
5348 self.recorder
5349 .release_transient_offscreen(scratch_descriptor, scratch);
5350 return Err(error);
5351 }
5352 };
5353 {
5354 self.renderer
5355 .effect_renderer
5356 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
5357 self.recorder,
5358 &device,
5359 &scratch,
5360 dest_view,
5361 alpha,
5362 load_op,
5363 scissor,
5364 None,
5365 supported_blend_mode(blend_mode),
5366 dest_viewport,
5367 sample_mode,
5368 );
5369 }
5370 self.recorder.record_passes(effect_passes.saturating_add(1));
5371 self.renderer.effect_renderer.record_composite_pass();
5372 effect_scratch_targets.release_into(self.recorder);
5373 self.recorder
5374 .release_transient_offscreen(scratch_descriptor, scratch);
5375 Ok(())
5376 }
5377
5378 #[allow(clippy::too_many_arguments)]
5379 fn record_effect_projective_composite(
5380 &mut self,
5381 source: &OffscreenTarget,
5382 effect: &RenderEffect,
5383 effect_rect: [f32; 4],
5384 dest_view: &wgpu::TextureView,
5385 viewport: (u32, u32),
5386 source_size: (f32, f32),
5387 inverse_matrix: [[f32; 3]; 3],
5388 dest_bounds: [[f32; 2]; 4],
5389 alpha: f32,
5390 load_op: wgpu::LoadOp<wgpu::Color>,
5391 scissor: Option<(u32, u32, u32, u32)>,
5392 blend_mode: BlendMode,
5393 sample_mode: CompositeSampleMode,
5394 ) -> Result<(), String> {
5395 if projective_dest_bounds_rect(dest_bounds).is_none() {
5396 return Ok(());
5397 }
5398 let device = self.renderer.device.clone();
5399 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
5400 "Render Effect Projective Composite Scratch",
5401 source.width,
5402 source.height,
5403 );
5404 let scratch = self
5405 .recorder
5406 .acquire_transient_offscreen(&device, scratch_descriptor);
5407 let effect_scratch_targets = self
5408 .renderer
5409 .effect_renderer
5410 .acquire_recorded_effect_scratch_targets(
5411 self.recorder,
5412 &device,
5413 effect,
5414 source.width,
5415 source.height,
5416 self.renderer.surface_format,
5417 );
5418 let effect_passes = {
5419 let mut effect_scratch_refs = effect_scratch_targets.refs();
5420 let pass_count = self.renderer.effect_renderer.encode_effect(
5421 self.recorder,
5422 &device,
5423 source,
5424 &scratch.view,
5425 effect,
5426 effect_rect,
5427 &mut effect_scratch_refs,
5428 )?;
5429 effect_scratch_refs.assert_consumed()?;
5430 Ok(pass_count)
5431 };
5432 let effect_passes = match effect_passes {
5433 Ok(pass_count) => pass_count,
5434 Err(error) => {
5435 effect_scratch_targets.release_into(self.recorder);
5436 self.recorder
5437 .release_transient_offscreen(scratch_descriptor, scratch);
5438 return Err(error);
5439 }
5440 };
5441 let composited = {
5442 self.renderer
5443 .effect_renderer
5444 .encode_composite_to_view_projective(
5445 self.recorder,
5446 &device,
5447 &scratch,
5448 dest_view,
5449 viewport,
5450 source_size,
5451 inverse_matrix,
5452 dest_bounds,
5453 alpha,
5454 load_op,
5455 scissor,
5456 supported_blend_mode(blend_mode),
5457 sample_mode,
5458 )
5459 };
5460 if composited {
5461 self.recorder.record_passes(effect_passes.saturating_add(1));
5462 self.renderer.effect_renderer.record_composite_pass();
5463 } else {
5464 self.recorder.record_passes(effect_passes);
5465 }
5466 effect_scratch_targets.release_into(self.recorder);
5467 self.recorder
5468 .release_transient_offscreen(scratch_descriptor, scratch);
5469 Ok(())
5470 }
5471}
5472
5473impl<C: FrameCommandRecorder> SurfaceExecutionBackend for RecordingSurfaceBackend<'_, '_, C> {
5474 fn max_texture_dim(&self) -> u32 {
5475 self.renderer.max_texture_dim()
5476 }
5477
5478 fn acquire_retained_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
5479 self.renderer.acquire_retained_surface(width, height)
5480 }
5481
5482 fn acquire_frame_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
5483 let descriptor =
5484 self.renderer
5485 .transient_offscreen_descriptor("Frame Surface", width, height);
5486 self.recorder
5487 .acquire_transient_offscreen(&self.renderer.device, descriptor)
5488 }
5489
5490 fn release_frame_surface(&mut self, target: OffscreenTarget) {
5491 let descriptor = self.renderer.transient_offscreen_descriptor(
5492 "Frame Surface",
5493 target.width,
5494 target.height,
5495 );
5496 self.recorder
5497 .release_transient_offscreen(descriptor, target);
5498 }
5499
5500 fn release_layer_surface_target(&mut self, target: LayerSurfaceTexture) {
5501 self.renderer.release_layer_surface_target(target);
5502 }
5503
5504 fn cached_layer_surface(
5505 &mut self,
5506 key: &LayerRasterCacheKey,
5507 ) -> Option<(Rc<OffscreenTarget>, Rect)> {
5508 self.renderer.cached_layer_surface(key)
5509 }
5510
5511 fn admit_layer_surface_cache_miss(&mut self, key: &LayerRasterCacheKey) -> bool {
5512 self.renderer.admit_layer_surface_cache_miss(key)
5513 }
5514
5515 fn insert_cached_layer_surface(
5516 &mut self,
5517 key: LayerRasterCacheKey,
5518 target: OffscreenTarget,
5519 logical_rect: Rect,
5520 ) -> Rc<OffscreenTarget> {
5521 self.renderer
5522 .insert_cached_layer_surface(key, target, logical_rect)
5523 }
5524
5525 fn clear_target_view_with_load_op(
5526 &mut self,
5527 target_view: &wgpu::TextureView,
5528 load_op: wgpu::LoadOp<wgpu::Color>,
5529 ) {
5530 {
5531 let _clear = self
5532 .recorder
5533 .encoder()
5534 .begin_render_pass(&wgpu::RenderPassDescriptor {
5535 label: Some("Layer Event Clear Pass"),
5536 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
5537 view: target_view,
5538 resolve_target: None,
5539 depth_slice: None,
5540 ops: wgpu::Operations {
5541 load: load_op,
5542 store: wgpu::StoreOp::Store,
5543 },
5544 })],
5545 depth_stencil_attachment: None,
5546 timestamp_writes: None,
5547 occlusion_query_set: None,
5548 multiview_mask: None,
5549 });
5550 }
5551 self.recorder.record_pass();
5552 }
5553
5554 #[allow(clippy::too_many_arguments)]
5555 fn render_non_effect_segment(
5556 &mut self,
5557 target_view: &wgpu::TextureView,
5558 shapes: &[DrawShape],
5559 images: &[ImageDraw],
5560 texts: &[TextDraw],
5561 shadow_draws: &[ShadowDraw],
5562 retained_draws: &[RetainedDraw],
5563 draw_ops: &[DrawOp],
5564 z_start: usize,
5565 z_end: usize,
5566 effect_z_ranges: &[Range<usize>],
5567 width: u32,
5568 height: u32,
5569 root_scale: f32,
5570 initial_load_op: wgpu::LoadOp<wgpu::Color>,
5571 ) -> Result<(), String> {
5572 self.render_non_effect_segment_with_composites(
5573 target_view,
5574 shapes,
5575 images,
5576 texts,
5577 shadow_draws,
5578 retained_draws,
5579 draw_ops,
5580 z_start,
5581 z_end,
5582 effect_z_ranges,
5583 &[],
5584 &[],
5585 width,
5586 height,
5587 root_scale,
5588 initial_load_op,
5589 )
5590 }
5591
5592 #[allow(clippy::too_many_arguments)]
5593 fn render_non_effect_segment_with_composites(
5594 &mut self,
5595 target_view: &wgpu::TextureView,
5596 shapes: &[DrawShape],
5597 images: &[ImageDraw],
5598 texts: &[TextDraw],
5599 shadow_draws: &[ShadowDraw],
5600 retained_draws: &[RetainedDraw],
5601 draw_ops: &[DrawOp],
5602 z_start: usize,
5603 z_end: usize,
5604 effect_z_ranges: &[Range<usize>],
5605 composites: &[(usize, CompositeBatchItem<'_>)],
5606 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
5607 width: u32,
5608 height: u32,
5609 root_scale: f32,
5610 initial_load_op: wgpu::LoadOp<wgpu::Color>,
5611 ) -> Result<(), String> {
5612 let mut ordered_items = std::mem::take(&mut self.renderer.scratch_segment_items);
5613 collect_non_effect_segment_items(
5614 shapes,
5615 images,
5616 texts,
5617 shadow_draws,
5618 draw_ops,
5619 z_start,
5620 z_end,
5621 effect_z_ranges,
5622 width,
5623 height,
5624 root_scale,
5625 &mut ordered_items,
5626 );
5627 #[cfg(not(target_arch = "wasm32"))]
5628 let raw_shadow_items = ordered_items
5629 .iter()
5630 .filter(|(_, item)| matches!(item, SegmentDrawItem::Shadow(_)))
5631 .count();
5632 let culled_shadow_items = retain_renderable_shadow_items(
5633 &mut ordered_items,
5634 shadow_draws,
5635 width,
5636 height,
5637 root_scale,
5638 self.renderer.max_texture_dim(),
5639 );
5640 #[cfg(target_arch = "wasm32")]
5641 let _ = culled_shadow_items;
5642 let mut cached_shadow_composites: Vec<(usize, CachedShadowComposite)> = Vec::new();
5643 ordered_items.extend(
5644 composites
5645 .iter()
5646 .enumerate()
5647 .map(|(index, (z_index, _))| (*z_index, SegmentDrawItem::Composite(index))),
5648 );
5649 ordered_items.extend(
5650 shader_composites
5651 .iter()
5652 .enumerate()
5653 .map(|(index, (z_index, _))| (*z_index, SegmentDrawItem::ShaderComposite(index))),
5654 );
5655 for (z_index, item) in &mut ordered_items {
5656 let SegmentDrawItem::Shadow(shadow_index) = *item else {
5657 continue;
5658 };
5659 let Some(composite) = self.renderer.cached_shape_shadow_composite(
5660 &shadow_draws[shadow_index],
5661 width,
5662 height,
5663 root_scale,
5664 ) else {
5665 continue;
5666 };
5667 let composite_index = composites.len() + cached_shadow_composites.len();
5668 cached_shadow_composites.push((*z_index, composite));
5669 *item = SegmentDrawItem::Composite(composite_index);
5670 }
5671 let mut merged_composites = Vec::with_capacity(
5672 composites
5673 .len()
5674 .saturating_add(cached_shadow_composites.len()),
5675 );
5676 merged_composites.extend(composites.iter().copied());
5677 merged_composites.extend(
5678 cached_shadow_composites
5679 .iter()
5680 .map(|(z_index, composite)| (*z_index, composite.batch_item())),
5681 );
5682 ordered_items.sort_unstable_by_key(|(z_index, _)| *z_index);
5686 #[cfg(not(target_arch = "wasm32"))]
5687 maybe_print_segment_diag(
5688 z_start..z_end,
5689 &ordered_items,
5690 shapes,
5691 images,
5692 SegmentDiagCounts {
5693 raw_shadow_items,
5694 culled_shadow_items,
5695 cached_shadow_composites: cached_shadow_composites.len(),
5696 composite_items: merged_composites.len(),
5697 shader_composite_items: shader_composites.len(),
5698 },
5699 self.renderer.shape_batch_limits,
5700 );
5701 let result = if ordered_items.is_empty() {
5702 Ok(SegmentCommandEncodeOutcome { first_batch: true })
5703 } else {
5704 self.renderer.encode_non_effect_segment_commands(
5705 self.recorder,
5706 target_view,
5707 &ordered_items,
5708 &merged_composites,
5709 shader_composites,
5710 shapes,
5711 images,
5712 texts,
5713 shadow_draws,
5714 retained_draws,
5715 initial_load_op,
5716 width,
5717 height,
5718 root_scale,
5719 )
5720 };
5721 self.renderer.scratch_segment_items = ordered_items;
5722 let outcome = result?;
5723 if outcome.first_batch && matches!(initial_load_op, wgpu::LoadOp::Clear(_)) {
5724 self.clear_target_view_with_load_op(target_view, initial_load_op);
5725 }
5726 Ok(())
5727 }
5728
5729 fn render_range_with_layer_events_to_target(
5730 &mut self,
5731 target: &OffscreenTarget,
5732 shapes: &[DrawShape],
5733 images: &[ImageDraw],
5734 texts: &[TextDraw],
5735 shadow_draws: &[ShadowDraw],
5736 draw_ops: &[DrawOp],
5737 effect_layers: &[EffectLayer],
5738 backdrop_layers: &[BackdropLayer],
5739 z_start: usize,
5740 z_end: usize,
5741 excluded_effect_layer: Option<usize>,
5742 width: u32,
5743 height: u32,
5744 root_scale: f32,
5745 backdrop_underlay: Option<&OffscreenTarget>,
5746 initial_load_op: wgpu::LoadOp<wgpu::Color>,
5747 ) -> Result<(), String> {
5748 self.render_range_with_layer_events_to_target_recorded(
5749 target,
5750 shapes,
5751 images,
5752 texts,
5753 shadow_draws,
5754 draw_ops,
5755 effect_layers,
5756 backdrop_layers,
5757 z_start,
5758 z_end,
5759 excluded_effect_layer,
5760 width,
5761 height,
5762 root_scale,
5763 backdrop_underlay,
5764 initial_load_op,
5765 )
5766 }
5767
5768 fn render_shadow_draw(
5769 &mut self,
5770 target_view: &wgpu::TextureView,
5771 shadow: &ShadowDraw,
5772 width: u32,
5773 height: u32,
5774 root_scale: f32,
5775 ) {
5776 self.renderer.encode_shadow_draw(
5777 self.recorder,
5778 target_view,
5779 shadow,
5780 width,
5781 height,
5782 root_scale,
5783 );
5784 }
5785
5786 fn composite_to_view_projective(
5787 &mut self,
5788 source: &OffscreenTarget,
5789 dest_view: &wgpu::TextureView,
5790 viewport: (u32, u32),
5791 source_size: (f32, f32),
5792 inverse_matrix: [[f32; 3]; 3],
5793 dest_bounds: [[f32; 2]; 4],
5794 alpha: f32,
5795 load_op: wgpu::LoadOp<wgpu::Color>,
5796 scissor: Option<(u32, u32, u32, u32)>,
5797 blend_mode: BlendMode,
5798 sample_mode: CompositeSampleMode,
5799 ) {
5800 let device = self.renderer.device.clone();
5801 let composited = {
5802 self.renderer
5803 .effect_renderer
5804 .encode_composite_to_view_projective(
5805 self.recorder,
5806 &device,
5807 source,
5808 dest_view,
5809 viewport,
5810 source_size,
5811 inverse_matrix,
5812 dest_bounds,
5813 alpha,
5814 load_op,
5815 scissor,
5816 supported_blend_mode(blend_mode),
5817 sample_mode,
5818 )
5819 };
5820 if composited {
5821 self.recorder.record_pass();
5822 self.renderer.effect_renderer.record_composite_pass();
5823 }
5824 }
5825
5826 fn composite_projective_surfaces_to_view(
5827 &mut self,
5828 dest_view: &wgpu::TextureView,
5829 viewport: (u32, u32),
5830 composites: &[ProjectiveSurfaceComposite<'_>],
5831 ) {
5832 let device = self.renderer.device.clone();
5833 let mut composite_count = 0_u32;
5834 for composite in composites
5835 .iter()
5836 .copied()
5837 .filter(|composite| projective_dest_bounds_rect(composite.dest_bounds).is_some())
5838 {
5839 let composited = {
5840 self.renderer
5841 .effect_renderer
5842 .encode_composite_to_view_projective(
5843 self.recorder,
5844 &device,
5845 composite.source,
5846 dest_view,
5847 viewport,
5848 composite.source_size,
5849 composite.inverse_matrix,
5850 composite.dest_bounds,
5851 composite.alpha,
5852 composite.load_op,
5853 composite.scissor,
5854 supported_blend_mode(composite.blend_mode),
5855 composite.sample_mode,
5856 )
5857 };
5858 if composited {
5859 composite_count = composite_count.saturating_add(1);
5860 }
5861 }
5862 if composite_count > 0 {
5863 self.recorder.record_passes(composite_count);
5864 self.renderer
5865 .effect_renderer
5866 .debug_composites
5867 .set(self.renderer.effect_renderer.debug_composites.get() + composite_count);
5868 }
5869 }
5870
5871 fn composite_surface_batch_to_view(
5872 &mut self,
5873 dest_view: &wgpu::TextureView,
5874 viewport: (u32, u32),
5875 load_op: wgpu::LoadOp<wgpu::Color>,
5876 composites: &[CompositeBatchItem<'_>],
5877 ) {
5878 if composites.is_empty() {
5879 return;
5880 }
5881 let device = self.renderer.device.clone();
5882 self.renderer
5883 .effect_renderer
5884 .encode_composite_batch_to_view_pass(
5885 self.recorder,
5886 &device,
5887 dest_view,
5888 viewport,
5889 load_op,
5890 composites,
5891 );
5892 self.recorder.record_pass();
5893 self.renderer.effect_renderer.record_composite_pass();
5894 }
5895
5896 fn copy_texture_region_to_target(
5897 &mut self,
5898 source: &OffscreenTarget,
5899 source_origin: (u32, u32),
5900 target: &OffscreenTarget,
5901 size: (u32, u32),
5902 ) -> bool {
5903 let (width, height) = size;
5904 if width == 0 || height == 0 || width > target.width || height > target.height {
5905 return false;
5906 }
5907 let Some(source_right) = source_origin.0.checked_add(width) else {
5908 return false;
5909 };
5910 let Some(source_bottom) = source_origin.1.checked_add(height) else {
5911 return false;
5912 };
5913 if source_right > source.width || source_bottom > source.height {
5914 return false;
5915 }
5916
5917 self.recorder.encoder().copy_texture_to_texture(
5918 wgpu::TexelCopyTextureInfo {
5919 texture: source.texture(),
5920 mip_level: 0,
5921 origin: wgpu::Origin3d {
5922 x: source_origin.0,
5923 y: source_origin.1,
5924 z: 0,
5925 },
5926 aspect: wgpu::TextureAspect::All,
5927 },
5928 wgpu::TexelCopyTextureInfo {
5929 texture: target.texture(),
5930 mip_level: 0,
5931 origin: wgpu::Origin3d::ZERO,
5932 aspect: wgpu::TextureAspect::All,
5933 },
5934 wgpu::Extent3d {
5935 width,
5936 height,
5937 depth_or_array_layers: 1,
5938 },
5939 );
5940 true
5941 }
5942
5943 fn shader_composite_batch_to_view(
5944 &mut self,
5945 dest_view: &wgpu::TextureView,
5946 viewport: (u32, u32),
5947 load_op: wgpu::LoadOp<wgpu::Color>,
5948 composites: &[ShaderCompositeBatchItem<'_>],
5949 ) -> bool {
5950 if composites.is_empty() {
5951 return true;
5952 }
5953 let device = self.renderer.device.clone();
5954 let encoded = self
5955 .renderer
5956 .effect_renderer
5957 .encode_shader_batch_src_over_to_view(
5958 self.recorder,
5959 &device,
5960 dest_view,
5961 viewport,
5962 load_op,
5963 composites,
5964 );
5965 if encoded {
5966 self.recorder.record_pass();
5967 self.renderer.effect_renderer.record_composite_pass();
5968 self.renderer
5969 .effect_renderer
5970 .debug_effects
5971 .set(self.renderer.effect_renderer.debug_effects.get() + composites.len() as u32);
5972 }
5973 encoded
5974 }
5975
5976 fn composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
5977 &mut self,
5978 source: &OffscreenTarget,
5979 dest_view: &wgpu::TextureView,
5980 alpha: f32,
5981 load_op: wgpu::LoadOp<wgpu::Color>,
5982 scissor: Option<(u32, u32, u32, u32)>,
5983 rounded_mask: Option<RoundedCompositeMask>,
5984 blend_mode: BlendMode,
5985 dest_viewport: Option<(f32, f32, f32, f32)>,
5986 sample_mode: CompositeSampleMode,
5987 ) {
5988 let device = self.renderer.device.clone();
5989 {
5990 self.renderer
5991 .effect_renderer
5992 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
5993 self.recorder,
5994 &device,
5995 source,
5996 dest_view,
5997 alpha,
5998 load_op,
5999 scissor,
6000 rounded_mask,
6001 supported_blend_mode(blend_mode),
6002 dest_viewport,
6003 sample_mode,
6004 );
6005 }
6006 self.recorder.record_pass();
6007 self.renderer.effect_renderer.record_composite_pass();
6008 }
6009
6010 fn apply_effect_and_composite_to_view(
6011 &mut self,
6012 source: &OffscreenTarget,
6013 effect: &RenderEffect,
6014 effect_rect: [f32; 4],
6015 dest_view: &wgpu::TextureView,
6016 alpha: f32,
6017 load_op: wgpu::LoadOp<wgpu::Color>,
6018 scissor: Option<(u32, u32, u32, u32)>,
6019 blend_mode: BlendMode,
6020 dest_viewport: Option<(f32, f32, f32, f32)>,
6021 sample_mode: CompositeSampleMode,
6022 ) -> Result<(), String> {
6023 self.record_effect_composite(
6024 source,
6025 effect,
6026 effect_rect,
6027 dest_view,
6028 alpha,
6029 load_op,
6030 scissor,
6031 blend_mode,
6032 dest_viewport,
6033 sample_mode,
6034 )
6035 }
6036
6037 fn apply_shader_and_composite_to_view(
6038 &mut self,
6039 source: &OffscreenTarget,
6040 shader: &RuntimeShader,
6041 effect_rect: [f32; 4],
6042 dest_view: &wgpu::TextureView,
6043 alpha: f32,
6044 load_op: wgpu::LoadOp<wgpu::Color>,
6045 scissor: Option<(u32, u32, u32, u32)>,
6046 blend_mode: BlendMode,
6047 dest_viewport: Option<(f32, f32, f32, f32)>,
6048 sample_mode: CompositeSampleMode,
6049 ) {
6050 self.record_shader_composite(
6051 source,
6052 shader,
6053 effect_rect,
6054 dest_view,
6055 alpha,
6056 load_op,
6057 scissor,
6058 blend_mode,
6059 dest_viewport,
6060 sample_mode,
6061 );
6062 }
6063
6064 fn apply_shader_and_composite_to_view_projective(
6065 &mut self,
6066 source: &OffscreenTarget,
6067 shader: &RuntimeShader,
6068 effect_rect: [f32; 4],
6069 dest_view: &wgpu::TextureView,
6070 viewport: (u32, u32),
6071 source_size: (f32, f32),
6072 inverse_matrix: [[f32; 3]; 3],
6073 dest_bounds: [[f32; 2]; 4],
6074 alpha: f32,
6075 load_op: wgpu::LoadOp<wgpu::Color>,
6076 scissor: Option<(u32, u32, u32, u32)>,
6077 blend_mode: BlendMode,
6078 sample_mode: CompositeSampleMode,
6079 ) {
6080 self.record_shader_projective_composite(
6081 source,
6082 shader,
6083 effect_rect,
6084 dest_view,
6085 viewport,
6086 source_size,
6087 inverse_matrix,
6088 dest_bounds,
6089 alpha,
6090 load_op,
6091 scissor,
6092 blend_mode,
6093 sample_mode,
6094 );
6095 }
6096
6097 fn apply_effect_and_composite_to_view_projective(
6098 &mut self,
6099 source: &OffscreenTarget,
6100 effect: &RenderEffect,
6101 effect_rect: [f32; 4],
6102 dest_view: &wgpu::TextureView,
6103 viewport: (u32, u32),
6104 source_size: (f32, f32),
6105 inverse_matrix: [[f32; 3]; 3],
6106 dest_bounds: [[f32; 2]; 4],
6107 alpha: f32,
6108 load_op: wgpu::LoadOp<wgpu::Color>,
6109 scissor: Option<(u32, u32, u32, u32)>,
6110 blend_mode: BlendMode,
6111 sample_mode: CompositeSampleMode,
6112 ) -> Result<(), String> {
6113 self.record_effect_projective_composite(
6114 source,
6115 effect,
6116 effect_rect,
6117 dest_view,
6118 viewport,
6119 source_size,
6120 inverse_matrix,
6121 dest_bounds,
6122 alpha,
6123 load_op,
6124 scissor,
6125 blend_mode,
6126 sample_mode,
6127 )
6128 }
6129
6130 fn is_render_effect_supported(&self, effect: &RenderEffect) -> bool {
6131 self.renderer.supports_render_effect(effect)
6132 }
6133
6134 fn warn_unsupported_effect_once(&self) {
6135 self.renderer.warning_state.warn_unsupported_effect_once();
6136 }
6137
6138 fn record_layer_cache_miss(&self, width: u32, height: u32) {
6139 self.renderer
6140 .frame_stats
6141 .record_layer_cache_miss(width, height);
6142 }
6143
6144 fn record_isolated_layer_render(
6145 &self,
6146 width: u32,
6147 height: u32,
6148 node_id: Option<NodeId>,
6149 logical_rect: Rect,
6150 requirements: SurfaceRequirementSet,
6151 ) {
6152 self.renderer.frame_stats.record_isolated_layer_render(
6153 width,
6154 height,
6155 node_id,
6156 logical_rect,
6157 requirements.into(),
6158 );
6159 }
6160}
6161
6162impl GpuRenderer {
6163 pub fn render(
6164 &mut self,
6165 view: &wgpu::TextureView,
6166 width: u32,
6167 height: u32,
6168 packet: FramePacket,
6169 surface_epoch: u64,
6170 returns: &mut RenderReturns,
6171 ) -> Result<(), String> {
6172 let cancel_reason = if packet.renderer_epoch != self.renderer_epoch {
6178 Some(CancelReason::RendererEpoch)
6179 } else if packet.surface_epoch != surface_epoch {
6180 Some(CancelReason::SurfaceEpoch)
6181 } else if packet.viewport != (width, height) {
6182 Some(CancelReason::Viewport)
6183 } else {
6184 None
6185 };
6186 if let Some(reason) = cancel_reason {
6187 return Self::cancel_packet(packet, reason, returns);
6188 }
6189 returns.frame_id = packet.frame_id;
6190 log::trace!("🎨 Rendering graph to {}x{}", width, height);
6191 let render_start = Instant::now();
6192
6193 #[cfg(target_arch = "wasm32")]
6194 {
6195 self.wasm_uniform_batch_cursor = 0;
6196 self.wasm_shape_batch_cursor = 0;
6197 self.wasm_image_batch_cursor = 0;
6198 }
6199 #[cfg(not(target_arch = "wasm32"))]
6200 {
6201 self.retained_glyph_uniform_cursor = 0;
6202 }
6203
6204 let text_cache_len = packet.text_cache_len;
6208 let result = self.render_graph(view, packet, returns);
6209 let after_graph = Instant::now();
6210 self.flush_deferred_offscreen_releases();
6211
6212 #[cfg(target_arch = "wasm32")]
6213 {
6214 const WASM_BATCH_POOL_MARGIN: usize = 4;
6215 self.wasm_uniform_batches.truncate(
6216 self.wasm_uniform_batch_cursor
6217 .saturating_add(WASM_BATCH_POOL_MARGIN),
6218 );
6219 self.wasm_shape_batches.truncate(
6220 self.wasm_shape_batch_cursor
6221 .saturating_add(WASM_BATCH_POOL_MARGIN),
6222 );
6223 self.wasm_image_batches.truncate(
6224 self.wasm_image_batch_cursor
6225 .saturating_add(WASM_BATCH_POOL_MARGIN),
6226 );
6227 }
6228 self.staged_uploads
6229 .shrink_retained_capacity(RETAINED_STAGED_UPLOAD_BYTES, RETAINED_STAGED_UPLOAD_COPIES);
6230
6231 self.layer_surface_cache.finish_frame(&self.frame_stats);
6232 #[cfg(not(target_arch = "wasm32"))]
6233 self.retained_bundle_cache.end_frame();
6234
6235 self.frame_stats.offscreen_pool_size.set(
6236 self.effect_renderer
6237 .retained_offscreen_count()
6238 .saturating_add(self.frame_graph_executor.retained_texture_count())
6239 as u32,
6240 );
6241 self.frame_stats.offscreen_pool_bytes.set(
6242 (self.effect_renderer.retained_offscreen_bytes() as u64)
6243 .saturating_add(self.frame_graph_executor.retained_texture_bytes()),
6244 );
6245 self.frame_stats
6246 .text_pool_size
6247 .set(self.text_image_cache.len() as u32);
6248 self.frame_stats
6249 .image_cache_size
6250 .set(self.image_texture_cache.len() as u32);
6251 self.frame_stats.text_cache_size.set(text_cache_len as u32);
6252 self.effect_renderer
6253 .merge_and_reset_debug_counters(&self.frame_stats);
6254 self.frame_graph_executor.reset_upload_allocators();
6255 let snapshot = self.frame_stats.snapshot();
6256 self.last_frame_stats = Some(snapshot);
6257 PRESENTED_FRAMES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
6258 update_frame_warmup_budget(&mut self.pending_frame_warmup_frames, &snapshot);
6259 self.frame_stats.maybe_print_snapshot(
6260 snapshot,
6261 &mut self.frame_count,
6262 self.gpu_stats_enabled,
6263 );
6264 if self.gpu_stats_enabled && self.frame_count.is_multiple_of(60) {
6265 gpu_stats::print_gpu_memory_report(&self.device, self.frame_count);
6266 }
6267 self.frame_stats.reset();
6268 let after_stats = Instant::now();
6269 if let Some(total_ms) = should_log_wgpu_render_stage(render_start, after_stats) {
6270 log::warn!(
6271 "[wgpu-render-stage:render] total_ms={total_ms:.2} graph_ms={:.2} cleanup_stats_ms={:.2}",
6272 instant_ms(render_start, after_graph),
6273 instant_ms(after_graph, after_stats),
6274 );
6275 }
6276 if result.is_ok() {
6277 returns.outcome = PresentOutcome::Presented;
6280 }
6281 result
6282 }
6283
6284 fn cancel_packet(
6291 packet: FramePacket,
6292 reason: CancelReason,
6293 returns: &mut RenderReturns,
6294 ) -> Result<(), String> {
6295 let FramePacket {
6296 frame_id,
6297 viewport: _,
6298 renderer_epoch: _,
6299 surface_epoch: _,
6300 root_scale: _,
6301 root,
6302 overlay: _,
6303 replay,
6304 text_cache_len: _,
6305 } = packet;
6306 match root {
6307 PacketRoot::Direct(root) => {
6308 returns.scene = Some(root.scene);
6315 #[cfg(not(target_arch = "wasm32"))]
6316 {
6317 returns.cancelled_replay = Some(replay);
6318 }
6319 }
6320 PacketRoot::Surface(_) => {}
6321 }
6322 #[cfg(target_arch = "wasm32")]
6323 let _ = replay;
6324 returns.ack = None;
6325 returns.frame_id = frame_id;
6326 returns.outcome = PresentOutcome::Cancelled(reason);
6327 Ok(())
6328 }
6329
6330 pub fn last_frame_stats(&self) -> Option<gpu_stats::FrameStatsSnapshot> {
6331 self.last_frame_stats
6332 }
6333
6334 pub fn needs_frame_warmup(&self) -> bool {
6335 self.pending_frame_warmup_frames > 0
6336 }
6337
6338 pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
6339 let layer_surface_cache_stats = self.layer_surface_cache.debug_stats();
6340 DebugCpuAllocationStats {
6341 scene_graph_node_count: 0,
6342 scene_graph_heap_bytes: 0,
6343 scene_hits_len: 0,
6344 scene_hits_cap: 0,
6345 scene_node_index_len: 0,
6346 scene_node_index_cap: 0,
6347 text_renderer_pool_len: self.text_image_cache.len(),
6348 text_renderer_pool_cap: self.text_image_cache.cap().get(),
6349 swash_image_cache_len: 0,
6350 swash_image_cache_cap: 0,
6351 swash_outline_cache_len: 0,
6352 swash_outline_cache_cap: 0,
6353 image_texture_cache_len: self.image_texture_cache.len(),
6354 image_texture_cache_cap: self.image_texture_cache.cap().get(),
6355 scratch_shape_data_cap: self.scratch_shape_data.capacity(),
6356 scratch_gradients_cap: self.scratch_gradients.capacity(),
6357 scratch_image_vertices_cap: self.scratch_image_vertices.capacity(),
6358 scratch_image_indices_cap: self.scratch_image_indices.capacity(),
6359 scratch_image_cmds_cap: self.scratch_image_cmds.capacity(),
6360 scratch_segment_items_cap: self.scratch_segment_items.capacity(),
6361 scratch_effect_ranges_cap: self.scratch_effect_ranges.capacity(),
6362 scratch_layer_events_cap: self.scratch_layer_events.capacity(),
6363 staged_upload_bytes_cap: self.staged_uploads.bytes.capacity(),
6364 staged_upload_copies_cap: self.staged_uploads.copies.capacity(),
6365 layer_surface_cache_len: layer_surface_cache_stats.entries_len,
6366 layer_surface_cache_cap: layer_surface_cache_stats.entries_cap,
6367 layer_surface_cache_identity_len: layer_surface_cache_stats.identity_len,
6368 layer_surface_cache_identity_cap: layer_surface_cache_stats.identity_cap,
6369 layer_surface_rect_cache_len: 0,
6372 layer_surface_rect_cache_cap: 0,
6373 layer_surface_requirements_cache_len: 0,
6374 layer_surface_requirements_cache_cap: 0,
6375 layer_cache_seen_this_frame_len: layer_surface_cache_stats.seen_this_frame_len,
6376 layer_cache_seen_this_frame_cap: layer_surface_cache_stats.seen_this_frame_cap,
6377 }
6378 }
6379
6380 pub fn render_to_rgba_pixels(
6381 &mut self,
6382 width: u32,
6383 height: u32,
6384 packet: FramePacket,
6385 surface_epoch: u64,
6386 returns: &mut RenderReturns,
6387 ) -> Result<Vec<u8>, String> {
6388 if width == 0 || height == 0 {
6389 return Err("Screenshot size must be non-zero".to_string());
6390 }
6391
6392 let output_texture = self.device.create_texture(&wgpu::TextureDescriptor {
6393 label: Some("Screenshot Output Texture"),
6394 size: wgpu::Extent3d {
6395 width,
6396 height,
6397 depth_or_array_layers: 1,
6398 },
6399 mip_level_count: 1,
6400 sample_count: 1,
6401 dimension: wgpu::TextureDimension::D2,
6402 format: self.surface_format,
6403 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
6404 view_formats: &[],
6405 });
6406 let output_view = output_texture.create_view(&wgpu::TextureViewDescriptor::default());
6407
6408 self.render(&output_view, width, height, packet, surface_epoch, returns)?;
6409
6410 let bytes_per_pixel = 4u32;
6411 let unpadded_bytes_per_row = width
6412 .checked_mul(bytes_per_pixel)
6413 .ok_or_else(|| "Screenshot row byte size overflow".to_string())?;
6414 let padded_bytes_per_row =
6415 align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
6416 let output_buffer_size = padded_bytes_per_row as u64 * height as u64;
6417
6418 let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
6419 label: Some("Screenshot Readback Buffer"),
6420 size: output_buffer_size,
6421 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
6422 mapped_at_creation: false,
6423 });
6424
6425 let device = self.device.clone();
6426 let queue = self.queue.clone();
6427 let mut graph = WgpuFrameGraph::new(Some("Screenshot Copy Encoder"));
6428 let source = graph.import_surface("screenshot-copy-source");
6429 graph.add_fallible_command_pass(Some("Screenshot Copy Pass"), &[source], &[], |context| {
6430 context.encoder.copy_texture_to_buffer(
6431 wgpu::TexelCopyTextureInfo {
6432 texture: &output_texture,
6433 mip_level: 0,
6434 origin: wgpu::Origin3d::ZERO,
6435 aspect: wgpu::TextureAspect::All,
6436 },
6437 wgpu::TexelCopyBufferInfo {
6438 buffer: &output_buffer,
6439 layout: wgpu::TexelCopyBufferLayout {
6440 offset: 0,
6441 bytes_per_row: Some(padded_bytes_per_row),
6442 rows_per_image: Some(height),
6443 },
6444 },
6445 wgpu::Extent3d {
6446 width,
6447 height,
6448 depth_or_array_layers: 1,
6449 },
6450 );
6451 Ok(())
6452 });
6453 let mut executor = std::mem::take(&mut self.frame_graph_executor);
6454 let execution = executor.execute_recorded_graph(&device, &queue, graph);
6455 self.frame_graph_executor = executor;
6456 let execution = execution.map_err(|error| error.to_string())?;
6457 let submission_index = execution.submission;
6458 let copy_stats = execution.stats;
6459 self.last_frame_stats = self
6460 .last_frame_stats
6461 .map(|snapshot| snapshot.with_command_stats_added(copy_stats));
6462
6463 let buffer_slice = output_buffer.slice(..);
6464 let (tx, rx) = mpsc::channel();
6465 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
6466 let _ = tx.send(result);
6467 });
6468 let _ = self.device.poll(wgpu::PollType::Wait {
6469 submission_index: Some(submission_index),
6470 timeout: None,
6471 });
6472
6473 match rx.recv_timeout(Duration::from_secs(3)) {
6474 Ok(Ok(())) => {}
6475 Ok(Err(err)) => return Err(format!("Screenshot map_async failed: {err:?}")),
6476 Err(err) => return Err(format!("Screenshot readback timed out: {err}")),
6477 }
6478
6479 let mapped = buffer_slice.get_mapped_range();
6480 let mut pixels = vec![0u8; (width as usize) * (height as usize) * 4];
6481
6482 let src_row_len = padded_bytes_per_row as usize;
6483 let dst_row_len = unpadded_bytes_per_row as usize;
6484 for row in 0..height as usize {
6485 let src_offset = row * src_row_len;
6486 let dst_offset = row * dst_row_len;
6487 pixels[dst_offset..dst_offset + dst_row_len]
6488 .copy_from_slice(&mapped[src_offset..src_offset + dst_row_len]);
6489 }
6490 drop(mapped);
6491 output_buffer.unmap();
6492
6493 self.convert_surface_pixels_to_rgba(&mut pixels)?;
6494 Ok(pixels)
6495 }
6496
6497 fn render_graph(
6498 &mut self,
6499 surface_view: &wgpu::TextureView,
6500 packet: FramePacket,
6501 returns: &mut RenderReturns,
6502 ) -> Result<(), String> {
6503 let device = self.device.clone();
6504 let queue = self.queue.clone();
6505 let graph_start = Instant::now();
6506
6507 #[cfg(not(target_arch = "wasm32"))]
6508 {
6509 let mut executor = std::mem::take(&mut self.frame_graph_executor);
6510 let mut frame_graph = WgpuFrameGraph::new(Some("Renderer Frame Graph"));
6511 let surface = frame_graph.import_surface("renderer-surface");
6512 frame_graph.add_fallible_recorded_command_pass(
6513 Some("Renderer Frame Pass"),
6514 &[],
6515 &[surface],
6516 |frame_encoder| {
6517 self.render_graph_recorded(surface_view, packet, returns, frame_encoder)
6518 },
6519 );
6520 let after_build = Instant::now();
6521 let execution = executor.execute_recorded_graph(&device, &queue, frame_graph);
6522 let after_execute = Instant::now();
6523 self.frame_graph_executor = executor;
6524 if let Some(total_ms) = should_log_wgpu_render_stage(graph_start, after_execute) {
6525 log::warn!(
6526 "[wgpu-render-stage:graph] total_ms={total_ms:.2} build_ms={:.2} execute_ms={:.2}",
6527 instant_ms(graph_start, after_build),
6528 instant_ms(after_build, after_execute),
6529 );
6530 }
6531
6532 match execution {
6533 Ok(execution) => {
6534 if execution.stats.pass_count > 0 {
6535 self.frame_stats.record_command_stats(execution.stats);
6536 }
6537 Ok(())
6538 }
6539 Err(crate::frame_graph::FrameGraphError::NoDeclaredPasses) => Ok(()),
6540 Err(error) => Err(error.to_string()),
6541 }
6542 }
6543
6544 #[cfg(target_arch = "wasm32")]
6545 {
6546 let mut executor = std::mem::take(&mut self.frame_graph_executor);
6547 let (result, execution) = {
6548 let mut frame_encoder =
6549 executor.begin(&device, &queue, Some("Renderer Frame Encoder"));
6550 let initial_pass_count = frame_encoder.recorded_pass_count();
6551 let result =
6552 self.render_graph_recorded(surface_view, packet, returns, &mut frame_encoder);
6553 let execution =
6554 if result.is_ok() && frame_encoder.recorded_pass_count() > initial_pass_count {
6555 Some(frame_encoder.finish())
6556 } else {
6557 None
6558 };
6559 (result, execution)
6560 };
6561 let after_execute = Instant::now();
6562 self.frame_graph_executor = executor;
6563 if let Some(total_ms) = should_log_wgpu_render_stage(graph_start, after_execute) {
6564 log::warn!("[wgpu-render-stage:graph] total_ms={total_ms:.2}",);
6565 }
6566 if let Some(execution) = execution {
6567 self.frame_stats.record_command_stats(execution.stats);
6568 }
6569 result
6570 }
6571 }
6572
6573 fn render_graph_recorded<C: FrameCommandRecorder>(
6574 &mut self,
6575 surface_view: &wgpu::TextureView,
6576 packet: FramePacket,
6577 returns: &mut RenderReturns,
6578 frame_encoder: &mut C,
6579 ) -> Result<(), String> {
6580 let recorded_start = Instant::now();
6581
6582 #[cfg(not(target_arch = "wasm32"))]
6594 let mut packet = packet;
6595 #[cfg(not(target_arch = "wasm32"))]
6596 if let PacketRoot::Direct(root) = &packet.root {
6597 let ops = std::mem::take(&mut packet.replay);
6598 let (ack, recycled) =
6599 self.consume_replay_ops(ops, &root.scene.shapes, packet.root_scale);
6600 returns.ack = Some((ack, recycled));
6601 }
6602
6603 let FramePacket {
6604 frame_id,
6605 viewport: (width, height),
6606 renderer_epoch: _,
6607 surface_epoch: _,
6608 root_scale,
6609 root,
6610 overlay,
6611 replay: _,
6612 text_cache_len: _,
6613 } = packet;
6614
6615 let mut backend = RecordingSurfaceBackend {
6616 renderer: self,
6617 recorder: frame_encoder,
6618 };
6619
6620 let surface_packet = match root {
6621 PacketRoot::Direct(root) => {
6622 let direct_render_start = Instant::now();
6623 let result = match execute_render_root_direct(
6624 &mut backend,
6625 surface_view,
6626 *root,
6627 width,
6628 height,
6629 root_scale,
6630 wgpu::LoadOp::Clear(CLEAR_COLOR),
6631 ) {
6632 Ok(scene) => {
6637 returns.scene = Some(scene);
6638 Ok(())
6639 }
6640 Err((error, scene)) => {
6641 returns.scene = Some(scene);
6642 Err(error)
6643 }
6644 };
6645 if result.is_ok() {
6646 if let Some(overlay) = overlay {
6647 Self::render_overlay_packet(
6648 &mut backend,
6649 surface_view,
6650 overlay,
6651 width,
6652 height,
6653 root_scale,
6654 )?;
6655 }
6656 }
6657 let after_direct_render = Instant::now();
6658 if let Some(total_ms) =
6659 should_log_wgpu_render_stage(recorded_start, after_direct_render)
6660 {
6661 log::warn!(
6662 "[wgpu-render-stage:recorded-direct-root] frame={frame_id} total_ms={total_ms:.2} render_ms={:.2}",
6663 instant_ms(direct_render_start, after_direct_render),
6664 );
6665 }
6666 return result;
6667 }
6668 PacketRoot::Surface(surface_packet) => surface_packet,
6669 };
6670 let after_root_collect = Instant::now();
6671
6672 let RootSurfacePacket {
6673 lowered,
6674 source,
6675 transform_to_parent,
6676 node_id,
6677 backdrop,
6678 graphics_layer,
6679 local_bounds,
6680 clip_rect,
6681 shadow_clip,
6682 } = *surface_packet;
6683 let mut lowered = lowered;
6684 lowered.source = source;
6685
6686 let viewport_rect = Rect {
6691 x: 0.0,
6692 y: 0.0,
6693 width: width as f32 / root_scale,
6694 height: height as f32 / root_scale,
6695 };
6696 let root_surface = execute_render_layer_surface(
6697 &mut backend,
6698 &mut lowered,
6699 LayerSurfaceRequest {
6700 root_scale,
6701 backdrop_underlay: None,
6702 allow_runtime_cache: false,
6703 logical_rect_override: Some(viewport_rect),
6704 capture_clip_override: None,
6705 activates_nested_capture: false,
6706 translation_context: TranslationRenderContext::default(),
6707 },
6708 )?;
6709 let root_quad = transform_to_parent.map_rect(root_surface.logical_rect);
6710 let root_dest_quad = scaled_quad(root_quad, root_scale);
6711
6712 let needs_root_composite_target =
6713 backdrop.is_some() || graphics_layer.shadow_elevation > 0.0;
6714
6715 if needs_root_composite_target {
6716 let composite_target = backend.acquire_frame_surface(width, height);
6717 backend.clear_target_view_with_load_op(
6718 &composite_target.view,
6719 wgpu::LoadOp::Clear(CLEAR_COLOR),
6720 );
6721
6722 if let Some(backdrop) = &backdrop {
6723 execute_apply_backdrop_layer_to_target(
6724 &mut backend,
6725 &composite_target,
6726 &BackdropLayer {
6727 node_id,
6728 rect: quad_bounds(transform_to_parent.map_rect(local_bounds)),
6729 clip: clip_rect.map(|clip| quad_bounds(transform_to_parent.map_rect(clip))),
6730 snap_anchor: None,
6731 effect: backdrop.clone(),
6732 z_index: 0,
6733 },
6734 None,
6735 width,
6736 height,
6737 root_scale,
6738 None,
6739 )?;
6740 }
6741
6742 let mut root_shadow_scene = CompositorScene::new();
6743 let root_shadow_clip =
6744 shadow_clip.map(|clip| quad_bounds(transform_to_parent.map_rect(clip)));
6745 push_layer_shadow(
6746 &mut root_shadow_scene,
6747 &graphics_layer,
6748 local_bounds,
6749 quad_bounds(transform_to_parent.map_rect(local_bounds)),
6750 root_shadow_clip,
6751 );
6752 for shadow in &root_shadow_scene.shadow_draws {
6753 backend.render_shadow_draw(
6754 &composite_target.view,
6755 shadow,
6756 width,
6757 height,
6758 root_scale,
6759 );
6760 }
6761
6762 let composite_dest_quad =
6763 snap_motion_stable_dest_quad(root_dest_quad, root_surface.sample_mode);
6764 execute_composite_surface_to_view(
6765 &mut backend,
6766 root_surface.target.target(),
6767 &composite_target.view,
6768 (width, height),
6769 composite_dest_quad,
6770 root_surface.composite_alpha,
6771 wgpu::LoadOp::Load,
6772 None,
6773 root_surface.blend_mode,
6774 root_surface.sample_mode,
6775 )?;
6776 backend.composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
6777 &composite_target,
6778 surface_view,
6779 1.0,
6780 wgpu::LoadOp::Clear(CLEAR_COLOR),
6781 None,
6782 None,
6783 BlendMode::SrcOver,
6784 None,
6785 CompositeSampleMode::Linear,
6786 );
6787 backend.release_frame_surface(composite_target);
6788 } else {
6789 let composite_dest_quad =
6790 snap_motion_stable_dest_quad(root_dest_quad, root_surface.sample_mode);
6791 execute_composite_surface_to_view(
6792 &mut backend,
6793 root_surface.target.target(),
6794 surface_view,
6795 (width, height),
6796 composite_dest_quad,
6797 root_surface.composite_alpha,
6798 wgpu::LoadOp::Clear(CLEAR_COLOR),
6799 None,
6800 root_surface.blend_mode,
6801 root_surface.sample_mode,
6802 )?;
6803 }
6804 backend.release_layer_surface_target(root_surface.target);
6805 if let Some(overlay) = overlay {
6806 Self::render_overlay_packet(
6807 &mut backend,
6808 surface_view,
6809 overlay,
6810 width,
6811 height,
6812 root_scale,
6813 )?;
6814 }
6815 let after_layer_render = Instant::now();
6816 if let Some(total_ms) = should_log_wgpu_render_stage(recorded_start, after_layer_render) {
6817 log::warn!(
6818 "[wgpu-render-stage:recorded-layer-root] total_ms={total_ms:.2} collect_ms={:.2} render_ms={:.2}",
6819 instant_ms(recorded_start, after_root_collect),
6820 instant_ms(after_root_collect, after_layer_render),
6821 );
6822 }
6823 Ok(())
6824 }
6825
6826 fn render_overlay_packet<C: FrameCommandRecorder>(
6830 backend: &mut RecordingSurfaceBackend<'_, '_, C>,
6831 surface_view: &wgpu::TextureView,
6832 overlay: CollectedLayer,
6833 width: u32,
6834 height: u32,
6835 root_scale: f32,
6836 ) -> Result<(), String> {
6837 if !overlay.child_layers.is_empty()
6838 || !root_direct_scene_events_are_supported(&overlay.scene)
6839 || !direct_root_child_underlays_are_supported(&overlay)
6840 {
6841 return Err("dev overlay graph must stay directly renderable".to_string());
6842 }
6843 execute_render_root_direct(
6844 backend,
6845 surface_view,
6846 overlay,
6847 width,
6848 height,
6849 root_scale,
6850 wgpu::LoadOp::Load,
6851 )
6852 .map(|_overlay_scene| ())
6853 .map_err(|(error, _overlay_scene)| error)
6854 }
6855
6856 #[allow(clippy::too_many_arguments)]
6857 fn encode_non_effect_segment_commands<C: FrameCommandRecorder>(
6858 &mut self,
6859 frame_encoder: &mut C,
6860 target_view: &wgpu::TextureView,
6861 ordered_items: &[(usize, SegmentDrawItem)],
6862 composites: &[(usize, CompositeBatchItem<'_>)],
6863 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
6864 shapes: &[DrawShape],
6865 images: &[ImageDraw],
6866 texts: &[TextDraw],
6867 shadow_draws: &[ShadowDraw],
6868 retained_draws: &[RetainedDraw],
6869 initial_load_op: wgpu::LoadOp<wgpu::Color>,
6870 width: u32,
6871 height: u32,
6872 root_scale: f32,
6873 ) -> Result<SegmentCommandEncodeOutcome, String> {
6874 let mut first_batch = true;
6875 for command in
6876 SegmentCommandIter::new(ordered_items, shapes, images, self.shape_batch_limits)
6877 {
6878 match command {
6879 SegmentRenderCommand::DrawChunk(chunk) => {
6880 let load_op = if first_batch {
6881 initial_load_op
6882 } else {
6883 wgpu::LoadOp::Load
6884 };
6885 let outcome = self.render_segment_draw_chunk(
6886 frame_encoder,
6887 target_view,
6888 ordered_items,
6889 composites,
6890 shader_composites,
6891 shapes,
6892 images,
6893 texts,
6894 retained_draws,
6895 chunk,
6896 width,
6897 height,
6898 root_scale,
6899 load_op,
6900 )?;
6901 if outcome.rendered_any {
6902 frame_encoder.record_passes(outcome.pass_count);
6903 first_batch = false;
6904 }
6905 }
6906 SegmentRenderCommand::Shadow(index) => {
6907 if first_batch && matches!(initial_load_op, wgpu::LoadOp::Clear(_)) {
6908 {
6909 let _clear = frame_encoder.encoder().begin_render_pass(
6910 &wgpu::RenderPassDescriptor {
6911 label: Some("Shadow Pre-Clear"),
6912 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
6913 view: target_view,
6914 resolve_target: None,
6915 depth_slice: None,
6916 ops: wgpu::Operations {
6917 load: initial_load_op,
6918 store: wgpu::StoreOp::Store,
6919 },
6920 })],
6921 depth_stencil_attachment: None,
6922 timestamp_writes: None,
6923 occlusion_query_set: None,
6924 multiview_mask: None,
6925 },
6926 );
6927 }
6928 frame_encoder.record_pass();
6929 first_batch = false;
6930 }
6931 let pass_count_before = frame_encoder.recorded_pass_count();
6932 self.encode_shadow_draw(
6933 frame_encoder,
6934 target_view,
6935 &shadow_draws[index],
6936 width,
6937 height,
6938 root_scale,
6939 );
6940 if frame_encoder.recorded_pass_count() > pass_count_before {
6941 first_batch = false;
6942 }
6943 }
6944 }
6945 }
6946 Ok(SegmentCommandEncodeOutcome { first_batch })
6947 }
6948
6949 #[cfg(not(target_arch = "wasm32"))]
6950 #[allow(clippy::too_many_arguments)]
6951 fn render_segment_draw_chunk_fused_native<C: FrameCommandRecorder>(
6952 &mut self,
6953 frame_encoder: &mut C,
6954 target_view: &wgpu::TextureView,
6955 ordered_items: &[(usize, SegmentDrawItem)],
6956 composites: &[(usize, CompositeBatchItem<'_>)],
6957 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
6958 shapes: &[DrawShape],
6959 images: &[ImageDraw],
6960 texts: &[TextDraw],
6961 retained_draws: &[RetainedDraw],
6962 chunk: &SegmentDrawChunkPlan,
6963 width: u32,
6964 height: u32,
6965 root_scale: f32,
6966 load_op: wgpu::LoadOp<wgpu::Color>,
6967 ) -> Result<Option<SegmentRenderOutcome>, String> {
6968 let Some(partitions) = native_segment_fusion_partitions(
6969 ordered_items,
6970 shapes,
6971 chunk,
6972 self.shape_batch_limits,
6973 )?
6974 else {
6975 return Ok(None);
6976 };
6977
6978 let mut rendered_any = false;
6979 let mut pass_count = 0_u32;
6980 let mut next_load_op = load_op;
6981 for partition in partitions {
6982 let outcome = self.render_segment_draw_chunk_fused_native_partition(
6983 frame_encoder,
6984 target_view,
6985 ordered_items,
6986 composites,
6987 shader_composites,
6988 shapes,
6989 images,
6990 texts,
6991 retained_draws,
6992 &partition.chunk,
6993 partition.budget,
6994 width,
6995 height,
6996 root_scale,
6997 next_load_op,
6998 )?;
6999 if outcome.rendered_any {
7000 rendered_any = true;
7001 pass_count = pass_count.saturating_add(outcome.pass_count);
7002 next_load_op = wgpu::LoadOp::Load;
7003 }
7004 }
7005
7006 Ok(Some(SegmentRenderOutcome {
7007 rendered_any,
7008 pass_count,
7009 }))
7010 }
7011
7012 #[cfg(not(target_arch = "wasm32"))]
7013 #[allow(clippy::too_many_arguments)]
7014 fn render_segment_draw_chunk_fused_native_partition<C: FrameCommandRecorder>(
7015 &mut self,
7016 frame_encoder: &mut C,
7017 target_view: &wgpu::TextureView,
7018 ordered_items: &[(usize, SegmentDrawItem)],
7019 composites: &[(usize, CompositeBatchItem<'_>)],
7020 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
7021 shapes: &[DrawShape],
7022 images: &[ImageDraw],
7023 texts: &[TextDraw],
7024 retained_draws: &[RetainedDraw],
7025 chunk: &SegmentDrawChunkPlan,
7026 budget: NativeSegmentFusionBudget,
7027 width: u32,
7028 height: u32,
7029 root_scale: f32,
7030 load_op: wgpu::LoadOp<wgpu::Color>,
7031 ) -> Result<SegmentRenderOutcome, String> {
7032 let partition_start = Instant::now();
7033 let mut staged_uploads = self.take_staged_uploads();
7034 staged_uploads.clear();
7035 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
7036 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
7037 let mut image_cmds = std::mem::take(&mut self.scratch_image_cmds);
7038 let mut glyph_cmds = std::mem::take(&mut self.scratch_glyph_cmds);
7039
7040 image_vertices.clear();
7041 image_indices.clear();
7042 image_cmds.clear();
7043 glyph_cmds.clear();
7044
7045 let result = (|| {
7046 let viewport = ViewportUniformParams {
7047 width,
7048 height,
7049 offset: [0.0, 0.0],
7050 };
7051 self.prewarm_offscreen_text_glyph_draws_in_chunk(
7052 ordered_items,
7053 texts,
7054 chunk,
7055 viewport,
7056 root_scale,
7057 &mut staged_uploads,
7058 &mut image_vertices,
7059 &mut image_indices,
7060 &mut glyph_cmds,
7061 )?;
7062 let mut shape_refs = Vec::with_capacity(budget.shape_count);
7063 for batch in chunk.iter() {
7064 let SegmentBatchPlan::Shape { start, end, .. } = batch else {
7065 continue;
7066 };
7067 for (_, item) in &ordered_items[start..end] {
7068 let SegmentDrawItem::Shape(shape_index) = item else {
7069 return Err(format!(
7070 "shape batch contains non-shape draw item: {item:?}"
7071 ));
7072 };
7073 shape_refs.push(&shapes[*shape_index]);
7074 }
7075 }
7076 let after_shape_refs = Instant::now();
7077
7078 let mut direct_shape_uploads = StagedBufferUploads::default();
7079 let mut shape_upload_base = 0u64;
7080 if !shape_refs.is_empty() {
7081 let Some((_, upload_base)) = self.prepare_shapes_batch_direct(
7082 frame_encoder,
7083 shape_refs.iter().copied(),
7084 root_scale,
7085 viewport,
7086 &mut direct_shape_uploads,
7087 ) else {
7088 return Err(
7089 "native fused segment shape preparation produced no draw batch".to_string(),
7090 );
7091 };
7092 shape_upload_base = upload_base;
7093 }
7094 let after_shape_prepare = Instant::now();
7095
7096 let mut fused_batches = Vec::with_capacity(chunk.batches.len());
7097 let mut shape_cursor = 0_u32;
7098 let mut composite_cursor = 0usize;
7099 let mut shader_composite_cursor = 0usize;
7100 for batch in chunk.iter() {
7101 match batch {
7102 SegmentBatchPlan::Shape {
7103 start,
7104 end,
7105 blend_mode,
7106 } => {
7107 for (_, item) in &ordered_items[start..end] {
7108 if !matches!(item, SegmentDrawItem::Shape(_)) {
7109 return Err(format!(
7110 "shape batch contains non-shape draw item: {item:?}"
7111 ));
7112 }
7113 }
7114 let shape_count = end - start;
7115 if shape_count > 0 {
7116 fused_batches.push(FusedSegmentBatch::Shape {
7117 batch: PreparedShapeBatch {
7118 vertex_start: shape_cursor * 6,
7119 vertex_count: shape_count as u32 * 6,
7120 },
7121 blend_mode,
7122 });
7123 shape_cursor += shape_count as u32;
7124 }
7125 }
7126 SegmentBatchPlan::Image {
7127 start,
7128 end,
7129 blend_mode,
7130 } => {
7131 let cmd_start = image_cmds.len();
7132 for (_, item) in &ordered_items[start..end] {
7133 let SegmentDrawItem::Image(image_index) = item else {
7134 return Err(format!(
7135 "image batch contains non-image draw item: {item:?}"
7136 ));
7137 };
7138 self.append_image_draw_cmd(
7139 &images[*image_index],
7140 viewport,
7141 root_scale,
7142 &mut image_vertices,
7143 &mut image_indices,
7144 &mut image_cmds,
7145 )?;
7146 }
7147 let cmd_end = image_cmds.len();
7148 if cmd_start < cmd_end {
7149 fused_batches.push(FusedSegmentBatch::Image {
7150 cmd_range: cmd_start..cmd_end,
7151 blend_mode,
7152 });
7153 }
7154 }
7155 SegmentBatchPlan::Text { start, end } => {
7156 let glyph_cmd_start = glyph_cmds.len();
7157 let image_cmd_start = image_cmds.len();
7158 let text_draws =
7159 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
7160 if !self.append_text_glyph_draws(
7161 text_draws,
7162 viewport,
7163 root_scale,
7164 false,
7165 &mut staged_uploads,
7166 &mut image_vertices,
7167 &mut image_indices,
7168 &mut glyph_cmds,
7169 )? {
7170 let text_draws =
7171 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
7172 self.append_text_image_draw_cmds(
7173 text_draws,
7174 viewport,
7175 root_scale,
7176 &mut image_vertices,
7177 &mut image_indices,
7178 &mut image_cmds,
7179 )?;
7180 }
7181 let image_cmd_end = image_cmds.len();
7182 let glyph_cmd_end = glyph_cmds.len();
7183 if image_cmd_start < image_cmd_end || glyph_cmd_start < glyph_cmd_end {
7184 fused_batches.push(FusedSegmentBatch::Text {
7185 image_cmd_range: image_cmd_start..image_cmd_end,
7186 glyph_cmd_range: glyph_cmd_start..glyph_cmd_end,
7187 });
7188 }
7189 }
7190 SegmentBatchPlan::Composite { start, end } => {
7191 for (_, item) in &ordered_items[start..end] {
7192 if !matches!(item, SegmentDrawItem::Composite(_)) {
7193 return Err(format!(
7194 "composite batch contains non-composite draw item: {item:?}"
7195 ));
7196 }
7197 }
7198 let draw_count = end - start;
7199 if draw_count > 0 {
7200 let draw_start = composite_cursor;
7201 composite_cursor += draw_count;
7202 fused_batches.push(FusedSegmentBatch::Composite {
7203 draw_range: draw_start..composite_cursor,
7204 });
7205 }
7206 }
7207 SegmentBatchPlan::ShaderComposite { start, end } => {
7208 for (_, item) in &ordered_items[start..end] {
7209 if !matches!(item, SegmentDrawItem::ShaderComposite(_)) {
7210 return Err(format!(
7211 "shader composite batch contains non-shader-composite draw item: {item:?}"
7212 ));
7213 }
7214 }
7215 let draw_count = end - start;
7216 if draw_count > 0 {
7217 let draw_start = shader_composite_cursor;
7218 shader_composite_cursor += draw_count;
7219 fused_batches.push(FusedSegmentBatch::ShaderComposite {
7220 draw_range: draw_start..shader_composite_cursor,
7221 });
7222 }
7223 }
7224 SegmentBatchPlan::Retained { start, end } => {
7225 self.stage_replay_patches(&mut staged_uploads);
7226 for (_, item) in &ordered_items[start..end] {
7227 let SegmentDrawItem::Retained(index) = item else {
7228 return Err(format!(
7229 "retained batch contains non-retained draw item: {item:?}"
7230 ));
7231 };
7232 let retained = retained_draws.get(*index).ok_or_else(|| {
7233 format!("retained draw index {index} out of bounds")
7234 })?;
7235 if (*index as u32) < MAX_REPLAY_SLOTS
7236 && self.replay_slots.slots.contains_key(&retained.slot)
7237 {
7238 let transform = retained.transform.with_retained_paint();
7239 staged_uploads.stage_at(
7240 UploadTarget::ReplayTransform,
7241 *index as u64 * REPLAY_TRANSFORM_STRIDE,
7242 bytemuck::bytes_of(&transform),
7243 );
7244 }
7245 }
7246 if end > start {
7247 fused_batches.push(FusedSegmentBatch::Retained {
7248 item_range: start..end,
7249 });
7250 }
7251 }
7252 }
7253 }
7254 let after_batch_prepare = Instant::now();
7255
7256 if !image_indices.is_empty() {
7257 self.stage_native_image_buffers(
7258 &mut staged_uploads,
7259 viewport,
7260 &image_vertices,
7261 &image_indices,
7262 );
7263 }
7264
7265 let device = self.device.clone();
7266 let composite_items: Vec<_> = chunk
7267 .iter()
7268 .filter_map(|batch| match batch {
7269 SegmentBatchPlan::Composite { start, end } => Some((start, end)),
7270 _ => None,
7271 })
7272 .flat_map(|(start, end)| {
7273 ordered_items[start..end].iter().filter_map(|(_, item)| {
7274 let SegmentDrawItem::Composite(composite_index) = item else {
7275 return None;
7276 };
7277 composites
7278 .get(*composite_index)
7279 .map(|(_, composite)| *composite)
7280 })
7281 })
7282 .collect();
7283 let prepared_composites = self.effect_renderer.prepare_composite_batch_draws(
7284 frame_encoder,
7285 &device,
7286 load_op,
7287 &composite_items,
7288 );
7289 let shader_items: Vec<_> = chunk
7290 .iter()
7291 .filter_map(|batch| match batch {
7292 SegmentBatchPlan::ShaderComposite { start, end } => Some((start, end)),
7293 _ => None,
7294 })
7295 .flat_map(|(start, end)| {
7296 ordered_items[start..end].iter().filter_map(|(_, item)| {
7297 let SegmentDrawItem::ShaderComposite(composite_index) = item else {
7298 return None;
7299 };
7300 shader_composites
7301 .get(*composite_index)
7302 .map(|(_, composite)| *composite)
7303 })
7304 })
7305 .collect();
7306 let prepared_shaders = self
7307 .effect_renderer
7308 .prepare_shader_batch_draws(frame_encoder, &device, &shader_items)
7309 .ok_or_else(|| "shader composite batch preparation failed".to_string())?;
7310 if !shader_items.is_empty() {
7311 self.effect_renderer.record_composite_pass();
7312 self.effect_renderer
7313 .debug_effects
7314 .set(self.effect_renderer.debug_effects.get() + shader_items.len() as u32);
7315 }
7316 let after_composite_prepare = Instant::now();
7317
7318 if fused_batches.is_empty() {
7319 return Ok(SegmentRenderOutcome {
7320 rendered_any: false,
7321 pass_count: 0,
7322 });
7323 }
7324
7325 self.flush_staged_uploads_at(
7330 frame_encoder.encoder(),
7331 &direct_shape_uploads,
7332 shape_upload_base,
7333 );
7334 let upload_offset =
7335 frame_encoder.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
7336 self.flush_staged_uploads_at(frame_encoder.encoder(), &staged_uploads, upload_offset);
7337 let after_upload = Instant::now();
7338
7339 let use_retained_bundles = retained_bundles_enabled();
7340 let mut retained_encode_ms = 0.0_f64;
7341 {
7342 let mut render_pass =
7343 frame_encoder
7344 .encoder()
7345 .begin_render_pass(&wgpu::RenderPassDescriptor {
7346 label: Some("Fused Segment Draw Pass"),
7347 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
7348 view: target_view,
7349 resolve_target: None,
7350 depth_slice: None,
7351 ops: wgpu::Operations {
7352 load: load_op,
7353 store: wgpu::StoreOp::Store,
7354 },
7355 })],
7356 depth_stencil_attachment: None,
7357 timestamp_writes: None,
7358 occlusion_query_set: None,
7359 multiview_mask: None,
7360 });
7361
7362 for batch in &fused_batches {
7363 match batch {
7364 FusedSegmentBatch::Shape { batch, blend_mode } => {
7365 self.draw_prepared_shapes(
7366 &mut render_pass,
7367 *blend_mode,
7368 *batch,
7369 width,
7370 height,
7371 );
7372 }
7373 FusedSegmentBatch::Image {
7374 cmd_range,
7375 blend_mode,
7376 } => {
7377 self.draw_native_prepared_image_cmd_range(
7378 &mut render_pass,
7379 &image_cmds,
7380 cmd_range.clone(),
7381 *blend_mode,
7382 )?;
7383 }
7384 FusedSegmentBatch::Text {
7385 image_cmd_range,
7386 glyph_cmd_range,
7387 } => {
7388 if !image_cmd_range.is_empty() {
7389 self.draw_native_prepared_image_cmd_range(
7390 &mut render_pass,
7391 &image_cmds,
7392 image_cmd_range.clone(),
7393 BlendMode::SrcOver,
7394 )?;
7395 self.frame_stats.bump_text();
7396 }
7397 if !glyph_cmd_range.is_empty() {
7398 self.draw_native_prepared_glyph_cmd_range(
7399 &mut render_pass,
7400 &glyph_cmds,
7401 glyph_cmd_range.clone(),
7402 )?;
7403 }
7404 }
7405 FusedSegmentBatch::Composite { draw_range } => {
7406 for draw in
7407 prepared_composites.get(draw_range.clone()).ok_or_else(|| {
7408 "composite draw range is outside the prepared command buffer"
7409 .to_string()
7410 })?
7411 {
7412 self.effect_renderer.draw_prepared_composite(
7413 &mut render_pass,
7414 (width, height),
7415 draw,
7416 );
7417 }
7418 }
7419 FusedSegmentBatch::ShaderComposite { draw_range } => {
7420 for draw in prepared_shaders.get(draw_range.clone()).ok_or_else(|| {
7421 "shader composite draw range is outside the prepared command buffer"
7422 .to_string()
7423 })? {
7424 self.effect_renderer.draw_prepared_shader_src_over(
7425 &device,
7426 &mut render_pass,
7427 (width, height),
7428 draw,
7429 );
7430 }
7431 }
7432 FusedSegmentBatch::Retained { item_range } => {
7433 let retained_start = Instant::now();
7439 if use_retained_bundles {
7440 self.draw_retained_stretch_bundled(
7441 &mut render_pass,
7442 ordered_items,
7443 retained_draws,
7444 item_range.clone(),
7445 width,
7446 height,
7447 );
7448 } else {
7449 for (_, item) in &ordered_items[item_range.clone()] {
7450 if let SegmentDrawItem::Retained(index) = item {
7451 if let Some(retained) = retained_draws.get(*index) {
7452 self.draw_retained_batch(
7453 &mut render_pass,
7454 retained,
7455 *index,
7456 width,
7457 height,
7458 );
7459 }
7460 }
7461 }
7462 }
7463 retained_encode_ms += instant_ms(retained_start, Instant::now());
7464 }
7465 }
7466 }
7467 }
7468 let after_pass = Instant::now();
7469 if let Some(total_ms) = should_log_wgpu_render_stage(partition_start, after_pass) {
7470 log::warn!(
7471 "[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={}",
7472 instant_ms(partition_start, after_shape_refs),
7473 instant_ms(after_shape_refs, after_shape_prepare),
7474 instant_ms(after_shape_prepare, after_batch_prepare),
7475 instant_ms(after_batch_prepare, after_composite_prepare),
7476 instant_ms(after_composite_prepare, after_upload),
7477 instant_ms(after_upload, after_pass),
7478 fused_batches.len(),
7479 budget.shape_count,
7480 image_cmds.len(),
7481 glyph_cmds.len(),
7482 staged_uploads.bytes.len(),
7483 );
7484 }
7485
7486 Ok(SegmentRenderOutcome {
7487 rendered_any: true,
7488 pass_count: 1,
7489 })
7490 })();
7491
7492 self.scratch_image_vertices = image_vertices;
7493 self.scratch_image_indices = image_indices;
7494 self.scratch_image_cmds = image_cmds;
7495 self.scratch_glyph_cmds = glyph_cmds;
7496 self.restore_staged_uploads(staged_uploads);
7497 result
7498 }
7499
7500 #[allow(clippy::too_many_arguments)]
7501 fn render_segment_draw_chunk<C: FrameCommandRecorder>(
7502 &mut self,
7503 frame_encoder: &mut C,
7504 target_view: &wgpu::TextureView,
7505 ordered_items: &[(usize, SegmentDrawItem)],
7506 composites: &[(usize, CompositeBatchItem<'_>)],
7507 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
7508 shapes: &[DrawShape],
7509 images: &[ImageDraw],
7510 texts: &[TextDraw],
7511 retained_draws: &[RetainedDraw],
7512 chunk: SegmentDrawChunkPlan,
7513 width: u32,
7514 height: u32,
7515 root_scale: f32,
7516 load_op: wgpu::LoadOp<wgpu::Color>,
7517 ) -> Result<SegmentRenderOutcome, String> {
7518 #[cfg(target_arch = "wasm32")]
7519 let _ = retained_draws;
7520 #[cfg(not(target_arch = "wasm32"))]
7521 if let Some(outcome) = self.render_segment_draw_chunk_fused_native(
7522 frame_encoder,
7523 target_view,
7524 ordered_items,
7525 composites,
7526 shader_composites,
7527 shapes,
7528 images,
7529 texts,
7530 retained_draws,
7531 &chunk,
7532 width,
7533 height,
7534 root_scale,
7535 load_op,
7536 )? {
7537 return Ok(outcome);
7538 }
7539
7540 let mut staged_uploads = self.take_staged_uploads();
7541 let result = (|| {
7542 let mut rendered_any = false;
7543 let mut pass_count = 0_u32;
7544 let mut next_load_op = load_op;
7545 for batch in chunk.iter() {
7546 staged_uploads.clear();
7547 match batch {
7548 SegmentBatchPlan::Shape {
7549 start,
7550 end,
7551 blend_mode,
7552 } => {
7553 let slice = &ordered_items[start..end];
7554 if slice.len() > self.shape_batch_limits.max_shapes_per_batch {
7555 return Err(format!(
7556 "shape batch contains {} shapes, exceeding the renderer limit of {}",
7557 slice.len(),
7558 self.shape_batch_limits.max_shapes_per_batch
7559 ));
7560 }
7561 let viewport = ViewportUniformParams {
7562 width,
7563 height,
7564 offset: [0.0, 0.0],
7565 };
7566 for (_, item) in slice {
7567 if !matches!(item, SegmentDrawItem::Shape(_)) {
7568 return Err(format!(
7569 "shape batch contains non-shape draw item: {item:?}"
7570 ));
7571 }
7572 }
7573 let Some(prepared) = self.prepare_shapes_batch(
7574 slice.iter().filter_map(|(_, item)| match item {
7575 SegmentDrawItem::Shape(shape_index) => Some(&shapes[*shape_index]),
7576 _ => None,
7577 }),
7578 root_scale,
7579 viewport,
7580 &mut staged_uploads,
7581 ) else {
7582 continue;
7583 };
7584 let upload_offset = frame_encoder
7585 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
7586 self.flush_staged_uploads_at(
7587 frame_encoder.encoder(),
7588 &staged_uploads,
7589 upload_offset,
7590 );
7591 {
7592 let mut render_pass = frame_encoder.encoder().begin_render_pass(
7593 &wgpu::RenderPassDescriptor {
7594 label: Some("Segment Shape Pass"),
7595 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
7596 view: target_view,
7597 resolve_target: None,
7598 depth_slice: None,
7599 ops: wgpu::Operations {
7600 load: next_load_op,
7601 store: wgpu::StoreOp::Store,
7602 },
7603 })],
7604 depth_stencil_attachment: None,
7605 timestamp_writes: None,
7606 occlusion_query_set: None,
7607 multiview_mask: None,
7608 },
7609 );
7610 self.draw_prepared_shapes(
7611 &mut render_pass,
7612 blend_mode,
7613 prepared,
7614 width,
7615 height,
7616 );
7617 }
7618 pass_count = pass_count.saturating_add(1);
7619 rendered_any = true;
7620 next_load_op = wgpu::LoadOp::Load;
7621 }
7622 SegmentBatchPlan::Image {
7623 start,
7624 end,
7625 blend_mode,
7626 } => {
7627 let viewport = ViewportUniformParams {
7628 width,
7629 height,
7630 offset: [0.0, 0.0],
7631 };
7632 for (_, item) in &ordered_items[start..end] {
7633 if !matches!(item, SegmentDrawItem::Image(_)) {
7634 return Err(format!(
7635 "image batch contains non-image draw item: {item:?}"
7636 ));
7637 }
7638 }
7639 let prepared_images = self.prepare_image_draw_cmds(
7640 ordered_items[start..end]
7641 .iter()
7642 .filter_map(|(_, item)| match item {
7643 SegmentDrawItem::Image(image_index) => {
7644 Some(&images[*image_index])
7645 }
7646 _ => None,
7647 }),
7648 viewport,
7649 root_scale,
7650 &mut staged_uploads,
7651 )?;
7652 if prepared_images.is_empty() {
7653 self.scratch_image_cmds = prepared_images.into_cmds();
7654 continue;
7655 }
7656 let upload_offset = frame_encoder
7657 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
7658 self.flush_staged_uploads_at(
7659 frame_encoder.encoder(),
7660 &staged_uploads,
7661 upload_offset,
7662 );
7663 let draw_result = {
7664 let mut render_pass = frame_encoder.encoder().begin_render_pass(
7665 &wgpu::RenderPassDescriptor {
7666 label: Some("Segment Image Pass"),
7667 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
7668 view: target_view,
7669 resolve_target: None,
7670 depth_slice: None,
7671 ops: wgpu::Operations {
7672 load: next_load_op,
7673 store: wgpu::StoreOp::Store,
7674 },
7675 })],
7676 depth_stencil_attachment: None,
7677 timestamp_writes: None,
7678 occlusion_query_set: None,
7679 multiview_mask: None,
7680 },
7681 );
7682 self.draw_prepared_images(
7683 &mut render_pass,
7684 &prepared_images,
7685 blend_mode,
7686 )
7687 };
7688 pass_count = pass_count.saturating_add(1);
7689 self.scratch_image_cmds = prepared_images.into_cmds();
7690 draw_result?;
7691 rendered_any = true;
7692 next_load_op = wgpu::LoadOp::Load;
7693 }
7694 SegmentBatchPlan::Text { start, end } => {
7695 let viewport = ViewportUniformParams {
7696 width,
7697 height,
7698 offset: [0.0, 0.0],
7699 };
7700 let text_draws =
7701 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
7702 if let Some(prepared_glyphs) = self.prepare_text_glyph_draw_cmds(
7703 text_draws,
7704 viewport,
7705 root_scale,
7706 &mut staged_uploads,
7707 )? {
7708 if prepared_glyphs.is_empty() {
7709 self.scratch_glyph_cmds = prepared_glyphs.into_cmds();
7710 continue;
7711 }
7712 let upload_offset = frame_encoder
7713 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
7714 self.flush_staged_uploads_at(
7715 frame_encoder.encoder(),
7716 &staged_uploads,
7717 upload_offset,
7718 );
7719 {
7720 let mut render_pass = frame_encoder.encoder().begin_render_pass(
7721 &wgpu::RenderPassDescriptor {
7722 label: Some("Segment Text Glyph Atlas Pass"),
7723 color_attachments: &[Some(
7724 wgpu::RenderPassColorAttachment {
7725 view: target_view,
7726 resolve_target: None,
7727 depth_slice: None,
7728 ops: wgpu::Operations {
7729 load: next_load_op,
7730 store: wgpu::StoreOp::Store,
7731 },
7732 },
7733 )],
7734 depth_stencil_attachment: None,
7735 timestamp_writes: None,
7736 occlusion_query_set: None,
7737 multiview_mask: None,
7738 },
7739 );
7740 self.draw_prepared_glyphs(&mut render_pass, &prepared_glyphs)?;
7741 }
7742 pass_count = pass_count.saturating_add(1);
7743 self.scratch_glyph_cmds = prepared_glyphs.into_cmds();
7744 rendered_any = true;
7745 next_load_op = wgpu::LoadOp::Load;
7746 } else {
7747 let text_draws =
7748 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
7749 let prepared_images = self.prepare_text_image_draw_cmds(
7750 text_draws,
7751 viewport,
7752 root_scale,
7753 &mut staged_uploads,
7754 )?;
7755 if prepared_images.is_empty() {
7756 self.scratch_image_cmds = prepared_images.into_cmds();
7757 continue;
7758 }
7759 let upload_offset = frame_encoder
7760 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
7761 self.flush_staged_uploads_at(
7762 frame_encoder.encoder(),
7763 &staged_uploads,
7764 upload_offset,
7765 );
7766 {
7767 let mut render_pass = frame_encoder.encoder().begin_render_pass(
7768 &wgpu::RenderPassDescriptor {
7769 label: Some("Segment Text Pass"),
7770 color_attachments: &[Some(
7771 wgpu::RenderPassColorAttachment {
7772 view: target_view,
7773 resolve_target: None,
7774 depth_slice: None,
7775 ops: wgpu::Operations {
7776 load: next_load_op,
7777 store: wgpu::StoreOp::Store,
7778 },
7779 },
7780 )],
7781 depth_stencil_attachment: None,
7782 timestamp_writes: None,
7783 occlusion_query_set: None,
7784 multiview_mask: None,
7785 },
7786 );
7787 self.draw_prepared_images(
7788 &mut render_pass,
7789 &prepared_images,
7790 BlendMode::SrcOver,
7791 )?;
7792 }
7793 self.frame_stats.bump_text();
7794 pass_count = pass_count.saturating_add(1);
7795 self.scratch_image_cmds = prepared_images.into_cmds();
7796 rendered_any = true;
7797 next_load_op = wgpu::LoadOp::Load;
7798 }
7799 }
7800 SegmentBatchPlan::Composite { start, end } => {
7801 let batch_items: Vec<_> = ordered_items[start..end]
7802 .iter()
7803 .map(|(_, item)| match item {
7804 SegmentDrawItem::Composite(composite_index) => composites
7805 .get(*composite_index)
7806 .map(|(_, composite)| *composite)
7807 .ok_or_else(|| {
7808 "composite item index is outside the composite buffer"
7809 .to_string()
7810 }),
7811 other => Err(format!(
7812 "composite batch contains non-composite draw item: {other:?}"
7813 )),
7814 })
7815 .collect::<Result<_, _>>()?;
7816 let device = self.device.clone();
7817 self.effect_renderer.encode_composite_batch_to_view_pass(
7818 frame_encoder,
7819 &device,
7820 target_view,
7821 (width, height),
7822 next_load_op,
7823 &batch_items,
7824 );
7825 self.effect_renderer.record_composite_pass();
7826 pass_count = pass_count.saturating_add(1);
7827 rendered_any = true;
7828 next_load_op = wgpu::LoadOp::Load;
7829 }
7830 SegmentBatchPlan::ShaderComposite { start, end } => {
7831 let batch_items: Vec<_> = ordered_items[start..end]
7832 .iter()
7833 .map(|(_, item)| match item {
7834 SegmentDrawItem::ShaderComposite(composite_index) => {
7835 shader_composites
7836 .get(*composite_index)
7837 .map(|(_, composite)| *composite)
7838 .ok_or_else(|| {
7839 "shader composite item index is outside the shader composite buffer"
7840 .to_string()
7841 })
7842 }
7843 other => Err(format!(
7844 "shader composite batch contains non-shader-composite draw item: {other:?}"
7845 )),
7846 })
7847 .collect::<Result<Vec<_>, _>>()?;
7848 let device = self.device.clone();
7849 let encoded = self.effect_renderer.encode_shader_batch_src_over_to_view(
7850 frame_encoder,
7851 &device,
7852 target_view,
7853 (width, height),
7854 next_load_op,
7855 &batch_items,
7856 );
7857 if !encoded {
7858 return Err("shader composite batch failed to encode".to_string());
7859 }
7860 self.effect_renderer.record_composite_pass();
7861 self.effect_renderer.debug_effects.set(
7862 self.effect_renderer.debug_effects.get() + batch_items.len() as u32,
7863 );
7864 pass_count = pass_count.saturating_add(1);
7865 rendered_any = true;
7866 next_load_op = wgpu::LoadOp::Load;
7867 }
7868 SegmentBatchPlan::Retained { start, end } => {
7869 #[cfg(target_arch = "wasm32")]
7877 {
7878 let _ = (start, end);
7879 return Err("retained shape batches are native-only".to_string());
7880 }
7881 #[cfg(not(target_arch = "wasm32"))]
7882 {
7883 self.stage_replay_patches(&mut staged_uploads);
7884 for (_, item) in &ordered_items[start..end] {
7885 let SegmentDrawItem::Retained(index) = item else {
7886 return Err(format!(
7887 "retained batch contains non-retained draw item: {item:?}"
7888 ));
7889 };
7890 let retained = retained_draws.get(*index).ok_or_else(|| {
7891 format!("retained draw index {index} out of bounds")
7892 })?;
7893 if (*index as u32) < MAX_REPLAY_SLOTS
7894 && self.replay_slots.slots.contains_key(&retained.slot)
7895 {
7896 let transform = retained.transform.with_retained_paint();
7897 staged_uploads.stage_at(
7898 UploadTarget::ReplayTransform,
7899 *index as u64 * REPLAY_TRANSFORM_STRIDE,
7900 bytemuck::bytes_of(&transform),
7901 );
7902 }
7903 }
7904 let upload_offset = frame_encoder
7905 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
7906 self.flush_staged_uploads_at(
7907 frame_encoder.encoder(),
7908 &staged_uploads,
7909 upload_offset,
7910 );
7911 {
7912 let mut render_pass = frame_encoder.encoder().begin_render_pass(
7913 &wgpu::RenderPassDescriptor {
7914 label: Some("Segment Retained Pass"),
7915 color_attachments: &[Some(
7916 wgpu::RenderPassColorAttachment {
7917 view: target_view,
7918 resolve_target: None,
7919 depth_slice: None,
7920 ops: wgpu::Operations {
7921 load: next_load_op,
7922 store: wgpu::StoreOp::Store,
7923 },
7924 },
7925 )],
7926 depth_stencil_attachment: None,
7927 timestamp_writes: None,
7928 occlusion_query_set: None,
7929 multiview_mask: None,
7930 },
7931 );
7932 for (_, item) in &ordered_items[start..end] {
7933 if let SegmentDrawItem::Retained(index) = item {
7934 if let Some(retained) = retained_draws.get(*index) {
7935 self.draw_retained_batch(
7936 &mut render_pass,
7937 retained,
7938 *index,
7939 width,
7940 height,
7941 );
7942 }
7943 }
7944 }
7945 }
7946 pass_count = pass_count.saturating_add(1);
7947 rendered_any = true;
7948 next_load_op = wgpu::LoadOp::Load;
7949 }
7950 }
7951 }
7952 }
7953 Ok(SegmentRenderOutcome {
7954 rendered_any,
7955 pass_count,
7956 })
7957 })();
7958 self.restore_staged_uploads(staged_uploads);
7959 result
7960 }
7961
7962 fn viewport_uniforms(params: ViewportUniformParams) -> Uniforms {
7963 Uniforms {
7964 viewport: [params.width as f32, params.height as f32],
7965 viewport_offset: params.offset,
7966 }
7967 }
7968
7969 #[cfg(not(target_arch = "wasm32"))]
7970 fn stage_viewport_uniforms(
7971 &self,
7972 staged_uploads: &mut StagedBufferUploads,
7973 params: ViewportUniformParams,
7974 ) {
7975 let uniforms = Self::viewport_uniforms(params);
7976 staged_uploads.stage(UploadTarget::Uniform, bytemuck::bytes_of(&uniforms));
7977 }
7978
7979 #[cfg(not(target_arch = "wasm32"))]
7980 fn stage_retained_glyph_viewport_uniforms(
7981 &mut self,
7982 staged_uploads: &mut StagedBufferUploads,
7983 params: ViewportUniformParams,
7984 ) -> usize {
7985 let slot = self.claim_retained_glyph_uniform_slot();
7986 let uniforms = Self::viewport_uniforms(params);
7987 staged_uploads.stage_at(
7988 UploadTarget::RetainedGlyphUniform,
7989 self.retained_glyph_uniform_offset(slot),
7990 bytemuck::bytes_of(&uniforms),
7991 );
7992 slot
7993 }
7994
7995 #[cfg(not(target_arch = "wasm32"))]
7996 fn claim_retained_glyph_uniform_slot(&mut self) -> usize {
7997 let slot = self.retained_glyph_uniform_cursor;
7998 self.retained_glyph_uniform_cursor = self.retained_glyph_uniform_cursor.saturating_add(1);
7999 self.ensure_retained_glyph_uniform_capacity(slot.saturating_add(1));
8000 slot
8001 }
8002
8003 #[cfg(not(target_arch = "wasm32"))]
8004 fn retained_glyph_uniform_offset(&self, slot: usize) -> u64 {
8005 self.retained_glyph_uniform_stride * slot as u64
8006 }
8007
8008 #[cfg(not(target_arch = "wasm32"))]
8009 fn retained_glyph_uniform_dynamic_offset(&self, slot: usize) -> Result<u32, String> {
8010 let offset = self.retained_glyph_uniform_offset(slot);
8011 u32::try_from(offset).map_err(|_| {
8012 "retained glyph uniform offset exceeded WGPU dynamic offset range".to_string()
8013 })
8014 }
8015
8016 #[cfg(not(target_arch = "wasm32"))]
8017 fn ensure_retained_glyph_uniform_capacity(&mut self, required_slots: usize) {
8018 if required_slots <= self.retained_glyph_uniform_capacity {
8019 return;
8020 }
8021 let new_capacity = required_slots
8022 .next_power_of_two()
8023 .max(INITIAL_RETAINED_GLYPH_UNIFORM_SLOTS);
8024 self.retained_glyph_uniform_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
8025 label: Some("Retained Glyph Uniform Buffer"),
8026 size: self.retained_glyph_uniform_stride * new_capacity as u64,
8027 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
8028 mapped_at_creation: false,
8029 });
8030 self.retained_glyph_uniform_bind_group =
8031 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
8032 label: Some("Retained Glyph Uniform Bind Group"),
8033 layout: &self.retained_glyph_uniform_bind_group_layout,
8034 entries: &[wgpu::BindGroupEntry {
8035 binding: 0,
8036 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
8037 buffer: &self.retained_glyph_uniform_buffer,
8038 offset: 0,
8039 size: wgpu::BufferSize::new(std::mem::size_of::<Uniforms>() as u64),
8040 }),
8041 }],
8042 });
8043 self.retained_glyph_uniform_capacity = new_capacity;
8044 }
8045
8046 #[cfg(target_arch = "wasm32")]
8047 fn prepare_wasm_viewport_uniforms(&mut self, params: ViewportUniformParams) -> usize {
8048 let slot = self.claim_wasm_uniform_batch();
8049 let uniforms = Self::viewport_uniforms(params);
8050 let bytes = bytemuck::bytes_of(&uniforms);
8051 let upload_stats = self.frame_graph_executor.upload_buffer(
8052 &self.queue,
8053 &self.wasm_uniform_batches[slot].buffer,
8054 0,
8055 bytes,
8056 );
8057 self.frame_stats.record_command_stats(upload_stats);
8058 slot
8059 }
8060
8061 #[cfg(target_arch = "wasm32")]
8062 fn claim_wasm_uniform_batch(&mut self) -> usize {
8063 let slot = self.wasm_uniform_batch_cursor;
8064 self.wasm_uniform_batch_cursor += 1;
8065 while self.wasm_uniform_batches.len() <= slot {
8066 self.wasm_uniform_batches.push(UniformBatchBuffer::new(
8067 &self.device,
8068 &self.uniform_bind_group_layout,
8069 ));
8070 }
8071 slot
8072 }
8073
8074 #[cfg(target_arch = "wasm32")]
8075 fn claim_wasm_shape_batch(&mut self) -> usize {
8076 let slot = self.wasm_shape_batch_cursor;
8077 self.wasm_shape_batch_cursor += 1;
8078 while self.wasm_shape_batches.len() <= slot {
8079 self.wasm_shape_batches.push(ShapeBatchBuffers::new(
8080 &self.device,
8081 &self.shape_bind_group_layout,
8082 &self.identity_similarity_buffer,
8083 self.dummy_paint_buffer.as_ref(),
8084 self.shape_batch_limits,
8085 ));
8086 }
8087 slot
8088 }
8089
8090 #[cfg(target_arch = "wasm32")]
8091 fn claim_wasm_image_batch(&mut self) -> usize {
8092 let slot = self.wasm_image_batch_cursor;
8093 self.wasm_image_batch_cursor += 1;
8094 while self.wasm_image_batches.len() <= slot {
8095 self.wasm_image_batches
8096 .push(ImageBatchBuffers::new(&self.device));
8097 }
8098 slot
8099 }
8100
8101 #[cfg(target_arch = "wasm32")]
8102 fn write_wasm_buffer(&self, buffer: &wgpu::Buffer, bytes: &[u8]) {
8103 let upload_stats = self
8104 .frame_graph_executor
8105 .upload_buffer(&self.queue, buffer, 0, bytes);
8106 self.frame_stats.record_command_stats(upload_stats);
8107 }
8108
8109 fn take_staged_uploads(&mut self) -> StagedBufferUploads {
8110 let mut staged_uploads = std::mem::take(&mut self.staged_uploads);
8111 debug_assert!(
8112 staged_uploads.is_empty(),
8113 "renderer-owned staged uploads should be restored as empty scratch storage"
8114 );
8115 staged_uploads.clear();
8116 staged_uploads
8117 }
8118
8119 fn restore_staged_uploads(&mut self, mut staged_uploads: StagedBufferUploads) {
8120 staged_uploads.clear();
8121 self.staged_uploads = staged_uploads;
8122 }
8123
8124 #[cfg(not(target_arch = "wasm32"))]
8125 fn ensure_upload_buffer_capacity(&mut self, required_bytes: u64) {
8126 if required_bytes <= self.upload_buffer.size() {
8127 return;
8128 }
8129
8130 let new_size = required_bytes
8131 .next_power_of_two()
8132 .max(INITIAL_UPLOAD_BUFFER_BYTES);
8133 self.upload_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
8134 label: Some("Frame Upload Buffer"),
8135 size: new_size,
8136 usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
8137 mapped_at_creation: false,
8138 });
8139 }
8140
8141 fn flush_staged_uploads_at(
8142 &mut self,
8143 encoder: &mut wgpu::CommandEncoder,
8144 staged_uploads: &StagedBufferUploads,
8145 upload_buffer_offset: u64,
8146 ) {
8147 if staged_uploads.is_empty() {
8148 return;
8149 }
8150 debug_assert_eq!(
8151 upload_buffer_offset % wgpu::COPY_BUFFER_ALIGNMENT,
8152 0,
8153 "upload-buffer base offset must satisfy copy alignment"
8154 );
8155
8156 #[cfg(target_arch = "wasm32")]
8157 {
8158 let _ = upload_buffer_offset;
8159 let _ = encoder;
8160 debug_assert!(
8161 staged_uploads.is_empty(),
8162 "wasm draw uploads use retained per-batch resource slots"
8163 );
8164 return;
8165 }
8166
8167 #[cfg(not(target_arch = "wasm32"))]
8168 {
8169 self.ensure_upload_buffer_capacity(
8170 upload_buffer_offset + staged_uploads.bytes.len() as u64,
8171 );
8172 let upload_stats = self.frame_graph_executor.upload_buffer(
8173 &self.queue,
8174 &self.upload_buffer,
8175 upload_buffer_offset,
8176 &staged_uploads.bytes,
8177 );
8178 self.frame_stats.record_command_stats(upload_stats);
8179
8180 for copy in &staged_uploads.copies {
8181 let target_buffer = match copy.target {
8182 UploadTarget::Uniform => &self.uniform_buffer,
8183 UploadTarget::ShapeData => &self.shape_buffers.shape_buffer,
8184 UploadTarget::ShapeGradient => &self.shape_buffers.gradient_buffer,
8185 UploadTarget::ImageVertex => &self.image_vertex_buffer,
8186 UploadTarget::ImageIndex => &self.image_index_buffer,
8187 UploadTarget::RetainedGlyphUniform => &self.retained_glyph_uniform_buffer,
8188 UploadTarget::ReplayTransform => &self.replay_slots.transform_buffer,
8189 UploadTarget::ReplayPaintData(slot) => {
8190 let Some(entry) = self.replay_slots.slots.get(&slot) else {
8193 continue;
8194 };
8195 &entry.paint_buffer
8196 }
8197 };
8198 encoder.copy_buffer_to_buffer(
8199 &self.upload_buffer,
8200 upload_buffer_offset + copy.source_offset,
8201 target_buffer,
8202 copy.target_offset,
8203 copy.size,
8204 );
8205 }
8206 }
8207 }
8208
8209 #[allow(clippy::too_many_arguments)]
8210 fn encode_shadow_draw<C: FrameCommandRecorder>(
8211 &mut self,
8212 frame_encoder: &mut C,
8213 target_view: &wgpu::TextureView,
8214 shadow: &ShadowDraw,
8215 width: u32,
8216 height: u32,
8217 root_scale: f32,
8218 ) {
8219 if shadow.shapes.is_empty() && shadow.texts.is_empty() {
8220 return;
8221 }
8222
8223 let shape_bounds_opt = shadow
8224 .shapes
8225 .iter()
8226 .map(|(shape, _)| shape.rect)
8227 .reduce(|a, b| Rect {
8228 x: a.x.min(b.x),
8229 y: a.y.min(b.y),
8230 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
8231 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
8232 });
8233
8234 let text_bounds_opt = shadow
8235 .texts
8236 .iter()
8237 .map(|text| text.rect)
8238 .reduce(|a, b| Rect {
8239 x: a.x.min(b.x),
8240 y: a.y.min(b.y),
8241 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
8242 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
8243 });
8244
8245 let combined_bounds = match (shape_bounds_opt, text_bounds_opt) {
8246 (Some(s), Some(t)) => Some(Rect {
8247 x: s.x.min(t.x),
8248 y: s.y.min(t.y),
8249 width: (s.x + s.width).max(t.x + t.width) - s.x.min(t.x),
8250 height: (s.y + s.height).max(t.y + t.height) - s.y.min(t.y),
8251 }),
8252 (Some(s), None) => Some(s),
8253 (None, Some(t)) => Some(t),
8254 (None, None) => None,
8255 };
8256
8257 let Some(shape_bounds) = combined_bounds else {
8258 return;
8259 };
8260
8261 let blur_margin = blur_extent_margin(shadow.blur_radius);
8262 let source_blur_bounds = Rect {
8263 x: shape_bounds.x - blur_margin,
8264 y: shape_bounds.y - blur_margin,
8265 width: shape_bounds.width + blur_margin * 2.0,
8266 height: shape_bounds.height + blur_margin * 2.0,
8267 };
8268 let mut visible_blur_bounds = source_blur_bounds;
8269 if let Some(clip) = shadow.clip {
8270 let clip_expanded = Rect {
8271 x: clip.x - blur_margin,
8272 y: clip.y - blur_margin,
8273 width: clip.width + blur_margin * 2.0,
8274 height: clip.height + blur_margin * 2.0,
8275 };
8276 let Some(intersection) = visible_blur_bounds.intersect(clip_expanded) else {
8277 return;
8278 };
8279 visible_blur_bounds = intersection;
8280 }
8281 let processing_scissor =
8282 scissor_rect_for_rect(visible_blur_bounds, root_scale, width, height);
8283 if processing_scissor.is_none() {
8284 return;
8285 }
8286
8287 if shadow.blur_radius <= 0.0 {
8289 for (shape, blend_mode) in &shadow.shapes {
8290 self.encode_shapes_pass(
8291 frame_encoder,
8292 target_view,
8293 std::iter::once(shape),
8294 *blend_mode,
8295 width,
8296 height,
8297 root_scale,
8298 wgpu::LoadOp::Load,
8299 [0.0, 0.0],
8300 );
8301 frame_encoder.record_pass();
8302 }
8303 if !shadow.texts.is_empty() {
8304 let mut staged_uploads = self.take_staged_uploads();
8305 let viewport = ViewportUniformParams {
8306 width,
8307 height,
8308 offset: [0.0, 0.0],
8309 };
8310 match self.prepare_text_image_draw_cmds(
8311 shadow.texts.iter(),
8312 viewport,
8313 root_scale,
8314 &mut staged_uploads,
8315 ) {
8316 Ok(prepared_images) if !prepared_images.is_empty() => {
8317 let upload_offset = frame_encoder
8318 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
8319 self.flush_staged_uploads_at(
8320 frame_encoder.encoder(),
8321 &staged_uploads,
8322 upload_offset,
8323 );
8324 let draw_result = {
8325 let mut render_pass = frame_encoder.encoder().begin_render_pass(
8326 &wgpu::RenderPassDescriptor {
8327 label: Some("Zero Blur Shadow Text Image Pass"),
8328 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
8329 view: target_view,
8330 resolve_target: None,
8331 depth_slice: None,
8332 ops: wgpu::Operations {
8333 load: wgpu::LoadOp::Load,
8334 store: wgpu::StoreOp::Store,
8335 },
8336 })],
8337 depth_stencil_attachment: None,
8338 timestamp_writes: None,
8339 occlusion_query_set: None,
8340 multiview_mask: None,
8341 },
8342 );
8343 self.draw_prepared_images(
8344 &mut render_pass,
8345 &prepared_images,
8346 BlendMode::SrcOver,
8347 )
8348 };
8349 self.scratch_image_cmds = prepared_images.into_cmds();
8350 if let Err(e) = draw_result {
8351 eprintln!("Failed to draw text for zero-blur shadow: {}", e);
8352 } else {
8353 self.frame_stats.bump_text();
8354 frame_encoder.record_pass();
8355 }
8356 }
8357 Ok(prepared_images) => {
8358 self.scratch_image_cmds = prepared_images.into_cmds();
8359 }
8360 Err(e) => {
8361 eprintln!("Failed to prepare text image for zero-blur shadow: {}", e);
8362 }
8363 }
8364 self.restore_staged_uploads(staged_uploads);
8365 }
8366 return;
8367 }
8368
8369 let Some(device_bounds) =
8371 device_pixel_bounds_for_rect(visible_blur_bounds, width, height, root_scale)
8372 else {
8373 return;
8374 };
8375 let bounds_x = device_bounds.x;
8376 let bounds_y = device_bounds.y;
8377 let bounds_w = device_bounds.width;
8378 let bounds_h = device_bounds.height;
8379 let pixel_radius = shadow.blur_radius * root_scale;
8380
8381 if shadow.texts.is_empty() && !shadow.shapes.is_empty() {
8382 if let Some(plan) = shape_shadow_surface_plan(
8383 &shadow.shapes,
8384 shadow.clip,
8385 shadow.blur_radius,
8386 width,
8387 height,
8388 root_scale,
8389 self.max_texture_dim(),
8390 ) {
8391 if self.encode_shape_only_blurred_shadow_draw(
8392 frame_encoder,
8393 target_view,
8394 shadow,
8395 plan.source_device_bounds,
8396 plan.pixel_radius,
8397 plan.processing_scissor,
8398 width,
8399 height,
8400 root_scale,
8401 ) {
8402 return;
8403 }
8404 }
8405 }
8406
8407 if !shadow.texts.is_empty() {
8408 self.frame_stats.record_shadow_text_blur_fallback();
8409 }
8410
8411 let device = self.device.clone();
8412 let source_descriptor =
8413 self.transient_offscreen_descriptor("Shadow Source", bounds_w, bounds_h);
8414 let source = frame_encoder.acquire_transient_offscreen(&device, source_descriptor);
8415 let viewport_offset = [bounds_x, bounds_y];
8416 let mut next_load_op = wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT);
8417 let source_outcome = self.encode_shadow_shape_source_passes(
8418 frame_encoder,
8419 &source.view,
8420 &shadow.shapes,
8421 bounds_w,
8422 bounds_h,
8423 viewport_offset,
8424 root_scale,
8425 &mut next_load_op,
8426 );
8427 frame_encoder.record_passes(source_outcome.pass_count);
8428 let mut rendered_any = source_outcome.rendered_any;
8429
8430 if !shadow.texts.is_empty() {
8431 let mut shifted_texts = shadow.texts.clone();
8432 for text in &mut shifted_texts {
8433 text.rect.x -= viewport_offset[0] / root_scale;
8434 text.rect.y -= viewport_offset[1] / root_scale;
8435 if let Some(clip) = text.clip.as_mut() {
8436 clip.x -= viewport_offset[0] / root_scale;
8437 clip.y -= viewport_offset[1] / root_scale;
8438 }
8439 }
8440
8441 let mut staged_uploads = self.take_staged_uploads();
8442 let viewport = ViewportUniformParams {
8443 width: bounds_w,
8444 height: bounds_h,
8445 offset: [0.0, 0.0],
8446 };
8447 match self.prepare_text_image_draw_cmds(
8448 shifted_texts.iter(),
8449 viewport,
8450 root_scale,
8451 &mut staged_uploads,
8452 ) {
8453 Ok(prepared_images) if !prepared_images.is_empty() => {
8454 let upload_offset = frame_encoder
8455 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
8456 self.flush_staged_uploads_at(
8457 frame_encoder.encoder(),
8458 &staged_uploads,
8459 upload_offset,
8460 );
8461 let draw_result = {
8462 let mut render_pass = frame_encoder.encoder().begin_render_pass(
8463 &wgpu::RenderPassDescriptor {
8464 label: Some("Shadow Source Text Image Pass"),
8465 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
8466 view: &source.view,
8467 resolve_target: None,
8468 depth_slice: None,
8469 ops: wgpu::Operations {
8470 load: next_load_op,
8471 store: wgpu::StoreOp::Store,
8472 },
8473 })],
8474 depth_stencil_attachment: None,
8475 timestamp_writes: None,
8476 occlusion_query_set: None,
8477 multiview_mask: None,
8478 },
8479 );
8480 self.draw_prepared_images(
8481 &mut render_pass,
8482 &prepared_images,
8483 BlendMode::SrcOver,
8484 )
8485 };
8486 self.scratch_image_cmds = prepared_images.into_cmds();
8487 if let Err(e) = draw_result {
8488 eprintln!("Failed to draw text for shadow: {}", e);
8489 } else {
8490 self.frame_stats.bump_text();
8491 frame_encoder.record_pass();
8492 rendered_any = true;
8493 }
8494 }
8495 Ok(prepared_images) => {
8496 self.scratch_image_cmds = prepared_images.into_cmds();
8497 }
8498 Err(e) => {
8499 eprintln!("Failed to prepare text image for shadow: {}", e);
8500 }
8501 }
8502 self.restore_staged_uploads(staged_uploads);
8503 }
8504
8505 if !rendered_any {
8506 frame_encoder.release_transient_offscreen(source_descriptor, source);
8507 return;
8508 }
8509
8510 let scratch_descriptor =
8511 self.transient_offscreen_descriptor("Shadow Blur Scratch", bounds_w, bounds_h);
8512 let scratch = frame_encoder.acquire_transient_offscreen(&device, scratch_descriptor);
8513 {
8514 self.effect_renderer.encode_blur_scissored_ping_pong_passes(
8515 frame_encoder,
8516 &device,
8517 &source,
8518 &scratch,
8519 &source.view,
8520 pixel_radius,
8521 pixel_radius,
8522 TileMode::Decal,
8523 None, );
8525 }
8526 frame_encoder.record_passes(2);
8527
8528 let clip_scissor = shadow
8529 .clip
8530 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
8531 let scissor = clip_scissor.or(processing_scissor);
8532 let rounded_mask = inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
8533 mask.rect[0] -= viewport_offset[0];
8536 mask.rect[1] -= viewport_offset[1];
8537 mask
8538 });
8539 let dest_viewport = Some((
8540 viewport_offset[0],
8541 viewport_offset[1],
8542 bounds_w as f32,
8543 bounds_h as f32,
8544 ));
8545 {
8546 self.effect_renderer
8547 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
8548 frame_encoder,
8549 &device,
8550 &source,
8551 target_view,
8552 1.0,
8553 wgpu::LoadOp::Load,
8554 scissor,
8555 rounded_mask,
8556 BlendMode::SrcOver,
8557 dest_viewport,
8558 CompositeSampleMode::Linear,
8559 );
8560 }
8561 frame_encoder.record_pass();
8562 self.effect_renderer.record_blur_pass();
8563 self.effect_renderer.record_composite_pass();
8564 frame_encoder.release_transient_offscreen(scratch_descriptor, scratch);
8565 frame_encoder.release_transient_offscreen(source_descriptor, source);
8566 }
8567
8568 #[allow(clippy::too_many_arguments)]
8569 fn encode_shadow_shape_source_passes<C: FrameCommandRecorder>(
8570 &mut self,
8571 frame_encoder: &mut C,
8572 source_view: &wgpu::TextureView,
8573 shapes: &[(DrawShape, BlendMode)],
8574 width: u32,
8575 height: u32,
8576 viewport_offset: [f32; 2],
8577 root_scale: f32,
8578 next_load_op: &mut wgpu::LoadOp<wgpu::Color>,
8579 ) -> ShadowSourceRenderOutcome {
8580 if shapes.is_empty() {
8581 return ShadowSourceRenderOutcome {
8582 rendered_any: false,
8583 pass_count: 0,
8584 };
8585 }
8586
8587 let mut staged_uploads = self.take_staged_uploads();
8588 let mut rendered_any = false;
8589 let mut pass_count = 0_u32;
8590 let mut start = 0usize;
8591 while start < shapes.len() {
8592 let blend_mode = supported_blend_mode(shapes[start].1);
8593 let mut end = start + 1;
8594 while end < shapes.len()
8595 && end - start < self.shape_batch_limits.max_shapes_per_batch
8596 && supported_blend_mode(shapes[end].1) == blend_mode
8597 {
8598 end += 1;
8599 }
8600
8601 staged_uploads.clear();
8602 let viewport = ViewportUniformParams {
8603 width,
8604 height,
8605 offset: viewport_offset,
8606 };
8607 let Some(prepared_shape) = self.prepare_shapes_batch(
8608 shapes[start..end]
8609 .iter()
8610 .map(|(shape, _blend_mode)| shape)
8611 .filter(|shape| shape_draw_is_visible_in_viewport(shape, viewport, root_scale)),
8612 root_scale,
8613 viewport,
8614 &mut staged_uploads,
8615 ) else {
8616 start = end;
8617 continue;
8618 };
8619
8620 let upload_offset =
8621 frame_encoder.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
8622 self.flush_staged_uploads_at(frame_encoder.encoder(), &staged_uploads, upload_offset);
8623
8624 {
8625 let mut render_pass =
8626 frame_encoder
8627 .encoder()
8628 .begin_render_pass(&wgpu::RenderPassDescriptor {
8629 label: Some("Shadow Source Shape Pass"),
8630 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
8631 view: source_view,
8632 resolve_target: None,
8633 depth_slice: None,
8634 ops: wgpu::Operations {
8635 load: *next_load_op,
8636 store: wgpu::StoreOp::Store,
8637 },
8638 })],
8639 depth_stencil_attachment: None,
8640 timestamp_writes: None,
8641 occlusion_query_set: None,
8642 multiview_mask: None,
8643 });
8644 self.draw_prepared_shapes(
8645 &mut render_pass,
8646 blend_mode,
8647 prepared_shape,
8648 width,
8649 height,
8650 );
8651 }
8652
8653 pass_count = pass_count.saturating_add(1);
8654 rendered_any = true;
8655 *next_load_op = wgpu::LoadOp::Load;
8656 start = end;
8657 }
8658
8659 self.restore_staged_uploads(staged_uploads);
8660 ShadowSourceRenderOutcome {
8661 rendered_any,
8662 pass_count,
8663 }
8664 }
8665
8666 #[allow(clippy::too_many_arguments)]
8667 fn encode_shape_only_blurred_shadow_draw<C: FrameCommandRecorder>(
8668 &mut self,
8669 frame_encoder: &mut C,
8670 target_view: &wgpu::TextureView,
8671 shadow: &ShadowDraw,
8672 device_bounds: DevicePixelBounds,
8673 pixel_radius: f32,
8674 processing_scissor: Option<(u32, u32, u32, u32)>,
8675 width: u32,
8676 height: u32,
8677 root_scale: f32,
8678 ) -> bool {
8679 let bounds_w = device_bounds.width;
8680 let bounds_h = device_bounds.height;
8681 let viewport_offset = [device_bounds.x, device_bounds.y];
8682 let cache_key =
8683 shape_shadow_surface_cache_key(&shadow.shapes, device_bounds, pixel_radius, root_scale);
8684
8685 if let Some(key) = cache_key {
8686 if let Some(cached) = self.cached_shadow_surface(&key) {
8687 self.frame_stats
8688 .record_shadow_shape_cache_hit(bounds_w, bounds_h);
8689 let clip_scissor = shadow
8690 .clip
8691 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
8692 let scissor = clip_scissor.or(processing_scissor);
8693 let rounded_mask =
8694 inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
8695 mask.rect[0] -= viewport_offset[0];
8696 mask.rect[1] -= viewport_offset[1];
8697 mask
8698 });
8699 let dest_viewport = Some((
8700 viewport_offset[0],
8701 viewport_offset[1],
8702 bounds_w as f32,
8703 bounds_h as f32,
8704 ));
8705 {
8706 self.effect_renderer
8707 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
8708 frame_encoder,
8709 &self.device,
8710 &cached,
8711 target_view,
8712 1.0,
8713 wgpu::LoadOp::Load,
8714 scissor,
8715 rounded_mask,
8716 BlendMode::SrcOver,
8717 dest_viewport,
8718 CompositeSampleMode::Nearest,
8719 );
8720 }
8721 frame_encoder.record_pass();
8722 self.effect_renderer.record_composite_pass();
8723 return true;
8724 }
8725 self.frame_stats
8726 .record_shadow_shape_cache_miss(bounds_w, bounds_h);
8727 self.frame_stats.maybe_print_shadow_shape_cache_miss(
8728 bounds_w,
8729 bounds_h,
8730 key.content_hash,
8731 pixel_radius,
8732 viewport_offset,
8733 shadow.shapes.len(),
8734 shadow.clip,
8735 );
8736 }
8737
8738 let device = self.device.clone();
8739 let source_descriptor =
8740 self.transient_offscreen_descriptor("Shape Shadow Source", bounds_w, bounds_h);
8741 let source_is_cacheable = cache_key.is_some();
8742 let source = if source_is_cacheable {
8743 self.acquire_retained_surface(bounds_w, bounds_h)
8744 } else {
8745 frame_encoder.acquire_transient_offscreen(&device, source_descriptor)
8746 };
8747 let scratch_descriptor =
8748 self.transient_offscreen_descriptor("Shape Shadow Blur Scratch", bounds_w, bounds_h);
8749 let scratch = frame_encoder.acquire_transient_offscreen(&device, scratch_descriptor);
8750 let mut next_load_op = wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT);
8751 let source_outcome = self.encode_shadow_shape_source_passes(
8752 frame_encoder,
8753 &source.view,
8754 &shadow.shapes,
8755 bounds_w,
8756 bounds_h,
8757 viewport_offset,
8758 root_scale,
8759 &mut next_load_op,
8760 );
8761 frame_encoder.record_passes(source_outcome.pass_count);
8762
8763 if !source_outcome.rendered_any {
8764 frame_encoder.release_transient_offscreen(scratch_descriptor, scratch);
8765 if source_is_cacheable {
8766 self.defer_offscreen_release(source);
8767 } else {
8768 frame_encoder.release_transient_offscreen(source_descriptor, source);
8769 }
8770 return true;
8771 }
8772
8773 {
8774 self.effect_renderer.encode_blur_scissored_ping_pong_passes(
8775 frame_encoder,
8776 &device,
8777 &source,
8778 &scratch,
8779 &source.view,
8780 pixel_radius,
8781 pixel_radius,
8782 TileMode::Decal,
8783 None,
8784 );
8785 }
8786 frame_encoder.record_passes(2);
8787
8788 let clip_scissor = shadow
8789 .clip
8790 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
8791 let scissor = clip_scissor.or(processing_scissor);
8792 let rounded_mask = inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
8793 mask.rect[0] -= viewport_offset[0];
8794 mask.rect[1] -= viewport_offset[1];
8795 mask
8796 });
8797 let dest_viewport = Some((
8798 viewport_offset[0],
8799 viewport_offset[1],
8800 bounds_w as f32,
8801 bounds_h as f32,
8802 ));
8803 {
8804 self.effect_renderer
8805 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
8806 frame_encoder,
8807 &device,
8808 &source,
8809 target_view,
8810 1.0,
8811 wgpu::LoadOp::Load,
8812 scissor,
8813 rounded_mask,
8814 BlendMode::SrcOver,
8815 dest_viewport,
8816 CompositeSampleMode::Nearest,
8817 );
8818 }
8819 frame_encoder.record_pass();
8820
8821 self.effect_renderer.record_blur_pass();
8822 self.effect_renderer.record_composite_pass();
8823 frame_encoder.release_transient_offscreen(scratch_descriptor, scratch);
8824 if let Some(key) = cache_key {
8825 self.insert_cached_shadow_surface(key, source);
8826 } else {
8827 frame_encoder.release_transient_offscreen(source_descriptor, source);
8828 }
8829 true
8830 }
8831
8832 fn prepare_shapes_batch<'a, I>(
8833 &mut self,
8834 layer_shapes: I,
8835 root_scale: f32,
8836 viewport: ViewportUniformParams,
8837 staged_uploads: &mut StagedBufferUploads,
8838 ) -> Option<PreparedShapeBatch>
8839 where
8840 I: Iterator<Item = &'a DrawShape>,
8841 {
8842 #[cfg(target_arch = "wasm32")]
8843 let _ = staged_uploads;
8844
8845 let shape_refs: Vec<&DrawShape> = layer_shapes
8850 .take(self.shape_batch_limits.max_shapes_per_batch)
8851 .collect();
8852 let shape_count = shape_refs.len();
8853 if shape_count == 0 {
8854 return None;
8855 }
8856
8857 let mut gradient_offsets: Vec<u32> = Vec::with_capacity(shape_count + 1);
8861 let mut total_gradient_stops = 0u32;
8862 gradient_offsets.push(0);
8863 for shape in &shape_refs {
8864 total_gradient_stops += shape_gradient_stop_count(shape) as u32;
8865 gradient_offsets.push(total_gradient_stops);
8866 }
8867
8868 self.scratch_shape_data.clear();
8869 self.scratch_shape_data
8870 .resize(shape_count, ShapeData::zeroed());
8871 self.scratch_gradients.clear();
8872 self.scratch_gradients
8873 .resize(total_gradient_stops as usize, GradientStop::zeroed());
8874
8875 convert_shapes_into_outputs(
8876 &shape_refs,
8877 &gradient_offsets,
8878 root_scale,
8879 &mut self.scratch_shape_data,
8880 &mut self.scratch_gradients,
8881 );
8882
8883 #[cfg(not(target_arch = "wasm32"))]
8884 {
8885 self.shape_buffers.ensure_capacity(
8886 &self.device,
8887 &self.shape_bind_group_layout,
8888 &self.identity_similarity_buffer,
8889 self.dummy_paint_buffer.as_ref(),
8890 shape_count,
8891 self.scratch_gradients.len().max(1),
8892 );
8893 self.stage_viewport_uniforms(staged_uploads, viewport);
8894 staged_uploads.stage(
8895 UploadTarget::ShapeData,
8896 bytemuck::cast_slice(&self.scratch_shape_data),
8897 );
8898 if !self.scratch_gradients.is_empty() {
8899 staged_uploads.stage(
8900 UploadTarget::ShapeGradient,
8901 bytemuck::cast_slice(&self.scratch_gradients),
8902 );
8903 }
8904 }
8905
8906 #[cfg(target_arch = "wasm32")]
8907 let shape_slot = {
8908 let slot = self.claim_wasm_shape_batch();
8909 {
8910 let buffers = &mut self.wasm_shape_batches[slot];
8911 buffers.ensure_capacity(
8912 &self.device,
8913 &self.shape_bind_group_layout,
8914 &self.identity_similarity_buffer,
8915 self.dummy_paint_buffer.as_ref(),
8916 shape_count,
8917 self.scratch_gradients.len().max(1),
8918 );
8919 }
8920 let buffers = &self.wasm_shape_batches[slot];
8921 self.write_wasm_buffer(
8922 &buffers.shape_buffer,
8923 bytemuck::cast_slice(&self.scratch_shape_data),
8924 );
8925 if !self.scratch_gradients.is_empty() {
8926 self.write_wasm_buffer(
8927 &buffers.gradient_buffer,
8928 bytemuck::cast_slice(&self.scratch_gradients),
8929 );
8930 }
8931 slot
8932 };
8933
8934 #[cfg(target_arch = "wasm32")]
8935 let uniform_slot = self.prepare_wasm_viewport_uniforms(viewport);
8936
8937 Some(PreparedShapeBatch {
8938 vertex_start: 0,
8939 vertex_count: shape_count as u32 * 6,
8940 #[cfg(target_arch = "wasm32")]
8941 shape_slot,
8942 #[cfg(target_arch = "wasm32")]
8943 uniform_slot,
8944 })
8945 }
8946
8947 #[cfg(not(target_arch = "wasm32"))]
8954 fn prepare_shapes_batch_direct<'a, I, C: FrameCommandRecorder>(
8955 &mut self,
8956 frame_encoder: &mut C,
8957 layer_shapes: I,
8958 root_scale: f32,
8959 viewport: ViewportUniformParams,
8960 staged_uploads: &mut StagedBufferUploads,
8961 ) -> Option<(PreparedShapeBatch, u64)>
8962 where
8963 I: Iterator<Item = &'a DrawShape>,
8964 {
8965 let shape_refs: Vec<&DrawShape> = layer_shapes
8966 .take(self.shape_batch_limits.max_shapes_per_batch)
8967 .collect();
8968 let shape_count = shape_refs.len();
8969 if shape_count == 0 {
8970 return None;
8971 }
8972
8973 let mut gradient_offsets: Vec<u32> = Vec::with_capacity(shape_count + 1);
8974 let mut total_gradient_stops = 0u32;
8975 gradient_offsets.push(0);
8976 for shape in &shape_refs {
8977 total_gradient_stops += shape_gradient_stop_count(shape) as u32;
8978 gradient_offsets.push(total_gradient_stops);
8979 }
8980
8981 self.shape_buffers.ensure_capacity(
8982 &self.device,
8983 &self.shape_bind_group_layout,
8984 &self.identity_similarity_buffer,
8985 self.dummy_paint_buffer.as_ref(),
8986 shape_count,
8987 (total_gradient_stops as usize).max(1),
8988 );
8989
8990 self.scratch_shape_data.clear();
8991 self.scratch_shape_data
8992 .resize(shape_count, ShapeData::zeroed());
8993 self.scratch_gradients.clear();
8994 self.scratch_gradients
8995 .resize(total_gradient_stops as usize, GradientStop::zeroed());
8996 convert_shapes_into_outputs(
8997 &shape_refs,
8998 &gradient_offsets,
8999 root_scale,
9000 &mut self.scratch_shape_data,
9001 &mut self.scratch_gradients,
9002 );
9003
9004 let uniform_len = std::mem::size_of::<Uniforms>() as u64;
9011 let shape_len = (shape_count * std::mem::size_of::<ShapeData>()) as u64;
9012 let gradient_len = total_gradient_stops as u64 * std::mem::size_of::<GradientStop>() as u64;
9013 let total_len = uniform_len + shape_len + gradient_len;
9014 let upload_base = frame_encoder.allocate_staged_upload_bytes(total_len);
9015 self.ensure_upload_buffer_capacity(upload_base + total_len);
9016
9017 let shape_off = uniform_len;
9018 let gradient_off = shape_off + shape_len;
9019
9020 let uniforms = Self::viewport_uniforms(viewport);
9021 let mut upload_stats = self.frame_graph_executor.upload_buffer(
9022 &self.queue,
9023 &self.upload_buffer,
9024 upload_base,
9025 bytemuck::bytes_of(&uniforms),
9026 );
9027 upload_stats.upload_bytes += self
9028 .frame_graph_executor
9029 .upload_buffer(
9030 &self.queue,
9031 &self.upload_buffer,
9032 upload_base + shape_off,
9033 bytemuck::cast_slice(&self.scratch_shape_data),
9034 )
9035 .upload_bytes;
9036 if !self.scratch_gradients.is_empty() {
9037 upload_stats.upload_bytes += self
9038 .frame_graph_executor
9039 .upload_buffer(
9040 &self.queue,
9041 &self.upload_buffer,
9042 upload_base + gradient_off,
9043 bytemuck::cast_slice(&self.scratch_gradients),
9044 )
9045 .upload_bytes;
9046 }
9047 self.frame_stats.record_command_stats(upload_stats);
9048
9049 staged_uploads.record_upload_copy(UploadTarget::Uniform, 0, 0, uniform_len);
9050 staged_uploads.record_upload_copy(UploadTarget::ShapeData, shape_off, 0, shape_len);
9051 staged_uploads.record_upload_copy(
9052 UploadTarget::ShapeGradient,
9053 gradient_off,
9054 0,
9055 gradient_len,
9056 );
9057
9058 Some((
9059 PreparedShapeBatch {
9060 vertex_start: 0,
9061 vertex_count: shape_count as u32 * 6,
9062 },
9063 upload_base,
9064 ))
9065 }
9066
9067 pub(crate) fn replay_supported(&self) -> bool {
9073 #[cfg(target_arch = "wasm32")]
9077 {
9078 false
9079 }
9080 #[cfg(not(target_arch = "wasm32"))]
9081 {
9082 self.shape_batch_limits.storage
9083 }
9084 }
9085
9086 pub(crate) fn restore_replay_ack_confirmations(
9092 &mut self,
9093 confirmations: Vec<crate::frame_packet::ReplayConfirmation>,
9094 ) {
9095 #[cfg(not(target_arch = "wasm32"))]
9096 {
9097 self.replay_ack_confirmations = confirmations;
9098 }
9099 #[cfg(target_arch = "wasm32")]
9100 let _ = confirmations;
9101 }
9102
9103 #[cfg(not(target_arch = "wasm32"))]
9118 fn consume_replay_ops(
9119 &mut self,
9120 mut ops: crate::frame_packet::ReplayFrameOps,
9121 shapes: &[DrawShape],
9122 root_scale: f32,
9123 ) -> (
9124 crate::frame_packet::ReplayAck,
9125 crate::frame_packet::ReplayFrameOps,
9126 ) {
9127 if ops.generation < self.store_feed_generation {
9128 self.replay_generation_drops += 1;
9134 log::warn!(
9135 "[command-feed] dropping replay ops of generation {} against store \
9136 generation {} ({} captures, {} patches, {} releases; lifetime drops {})",
9137 ops.generation,
9138 self.store_feed_generation,
9139 ops.captures.len(),
9140 ops.color_patches.len(),
9141 ops.releases.len(),
9142 self.replay_generation_drops,
9143 );
9144 ops.captures.clear();
9145 ops.color_patches.clear();
9146 ops.releases.clear();
9147 return (
9148 crate::frame_packet::ReplayAck {
9149 generation: self.store_feed_generation,
9150 confirmations: Vec::new(),
9151 },
9152 ops,
9153 );
9154 }
9155 if ops.generation > self.store_feed_generation {
9156 self.store_feed_generation = ops.generation;
9163 }
9164 let generation = ops.generation;
9165 for slot in ops.releases.drain(..) {
9168 self.release_replay_slot(slot);
9169 }
9170 let mut confirmations = std::mem::take(&mut self.replay_ack_confirmations);
9173 debug_assert!(confirmations.is_empty());
9174 for capture in ops.captures.drain(..) {
9175 if capture.frame != ops.frame {
9176 log::warn!(
9183 "[command-feed] dropping stale capture for slot {} of {:?} \
9184 (queued frame {}, ops frame {})",
9185 capture.key.1,
9186 capture.key.0,
9187 capture.frame,
9188 ops.frame,
9189 );
9190 continue;
9191 }
9192 let end = capture.shape_start + capture.shape_count;
9193 let Some(slice) = shapes.get(capture.shape_start..end) else {
9194 continue;
9195 };
9196 let refs: Vec<&DrawShape> = slice.iter().collect();
9197 let Some(gpu_slot) = self.capture_replay_slot(&refs, root_scale) else {
9198 continue;
9199 };
9200 confirmations.push((capture.key, gpu_slot));
9201 }
9202 self.replay_color_patches.clear();
9210 std::mem::swap(&mut self.replay_color_patches, &mut ops.color_patches);
9211 (
9212 crate::frame_packet::ReplayAck {
9213 generation,
9214 confirmations,
9215 },
9216 ops,
9217 )
9218 }
9219
9220 #[cfg(not(target_arch = "wasm32"))]
9225 pub(crate) fn replay_generation_drops(&self) -> u64 {
9226 self.replay_generation_drops
9227 }
9228
9229 #[cfg(not(target_arch = "wasm32"))]
9237 pub(crate) fn replay_ops_roundtrip_for_tests(&mut self, generation_skew: u64) -> usize {
9238 let generation = self.store_feed_generation.wrapping_add(generation_skew);
9239 let ops = crate::shape_replay::SHAPE_REPLAY
9240 .with(|state| state.borrow_mut().take_frame_ops(generation));
9241 let (ack, recycled) = self.consume_replay_ops(ops, &[], 1.0);
9242 let confirmed = ack.confirmations.len();
9243 self.replay_ack_confirmations = crate::shape_replay::SHAPE_REPLAY
9244 .with(|state| state.borrow_mut().apply_ack(ack, recycled));
9245 confirmed
9246 }
9247
9248 #[cfg(not(target_arch = "wasm32"))]
9255 fn stage_replay_patches(&mut self, staged_uploads: &mut StagedBufferUploads) {
9256 std::mem::swap(
9264 &mut self.replay_color_patches,
9265 &mut self.color_patch_scratch,
9266 );
9267 let total_patches = self.color_patch_scratch.len();
9268 if total_patches == 0 {
9269 self.replay_upload_stats.note_frame(0, 0, 0, 0, 0);
9270 return;
9271 }
9272
9273 #[derive(Clone, Copy)]
9279 struct DirtySpan {
9280 paint_min: u32,
9281 paint_max: u32,
9282 }
9283 const CLEAN: DirtySpan = DirtySpan {
9284 paint_min: u32::MAX,
9285 paint_max: 0,
9286 };
9287 let mut dirty: std::collections::HashMap<
9288 u32,
9289 DirtySpan,
9290 cranpose_ui_graphics::FxBuildHasher,
9291 > = std::collections::HashMap::default();
9292
9293 for patch in &self.color_patch_scratch {
9295 let Some(slot) = self.replay_slots.slots.get_mut(&patch.slot) else {
9296 continue;
9297 };
9298 let Some(paint) = slot.paint_mirror.get_mut(patch.shape_index as usize) else {
9299 continue;
9300 };
9301 *paint = patch.color;
9302 let span = dirty.entry(patch.slot).or_insert(CLEAN);
9303 span.paint_min = span.paint_min.min(patch.shape_index);
9304 span.paint_max = span.paint_max.max(patch.shape_index);
9305 }
9306
9307 let mut uploaded_records = 0u64;
9308 let mut uploaded_bytes = 0u64;
9309 let slots_touched = dirty.len() as u64;
9310 for (slot_id, span) in dirty {
9311 let Some(slot) = self.replay_slots.slots.get(&slot_id) else {
9312 continue;
9313 };
9314 if span.paint_min <= span.paint_max {
9315 let range = span.paint_min as usize..span.paint_max as usize + 1;
9316 uploaded_records += range.len() as u64;
9317 uploaded_bytes += (range.len() * std::mem::size_of::<[f32; 4]>()) as u64;
9318 staged_uploads.stage_at(
9319 UploadTarget::ReplayPaintData(slot_id),
9320 range.start as u64 * std::mem::size_of::<[f32; 4]>() as u64,
9321 bytemuck::cast_slice(&slot.paint_mirror[range]),
9322 );
9323 }
9324 }
9325 let ideal_bytes = total_patches as u64 * 16;
9328 self.replay_upload_stats.note_frame(
9329 total_patches as u64,
9330 slots_touched,
9331 uploaded_records,
9332 uploaded_bytes,
9333 ideal_bytes,
9334 );
9335 if cranpose_core::env_flag!("CRANPOSE_COMMAND_REPLAY_DIAG") {
9336 log::warn!(
9337 "[replay-upload] frame: {} patches -> {} records / {:.1} KB staged \
9338 across {} slots (color-only {:.1} KB)",
9339 total_patches,
9340 uploaded_records,
9341 uploaded_bytes as f64 / 1024.0,
9342 slots_touched,
9343 ideal_bytes as f64 / 1024.0,
9344 );
9345 }
9346 self.color_patch_scratch.clear();
9347 }
9348
9349 #[cfg(not(target_arch = "wasm32"))]
9352 pub(crate) fn capture_replay_slot(
9353 &mut self,
9354 shape_refs: &[&DrawShape],
9355 root_scale: f32,
9356 ) -> Option<u32> {
9357 if !self.shape_batch_limits.storage || shape_refs.is_empty() {
9358 return None;
9359 }
9360 let id = self.replay_slots.free_ids.pop()?;
9361 let shape_count = shape_refs.len();
9362
9363 let mut gradient_offsets: Vec<u32> = Vec::with_capacity(shape_count + 1);
9364 let mut total_gradient_stops = 0u32;
9365 gradient_offsets.push(0);
9366 for shape in shape_refs {
9367 total_gradient_stops += shape_gradient_stop_count(shape) as u32;
9368 gradient_offsets.push(total_gradient_stops);
9369 }
9370
9371 let mut shape_data = vec![ShapeData::zeroed(); shape_count];
9372 let mut gradients = vec![GradientStop::zeroed(); (total_gradient_stops as usize).max(1)];
9373 convert_shapes_into_outputs(
9374 shape_refs,
9375 &gradient_offsets,
9376 root_scale,
9377 &mut shape_data,
9378 &mut gradients,
9379 );
9380
9381 let shape_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
9382 label: Some("Replay Shape Buffer"),
9383 size: (std::mem::size_of::<ShapeData>() * shape_count) as u64,
9384 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
9385 mapped_at_creation: true,
9386 });
9387 shape_buffer
9388 .slice(..)
9389 .get_mapped_range_mut()
9390 .copy_from_slice(bytemuck::cast_slice(&shape_data));
9391 shape_buffer.unmap();
9392
9393 let gradient_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
9394 label: Some("Replay Gradient Buffer"),
9395 size: (std::mem::size_of::<GradientStop>() * gradients.len()) as u64,
9396 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
9397 mapped_at_creation: true,
9398 });
9399 gradient_buffer
9400 .slice(..)
9401 .get_mapped_range_mut()
9402 .copy_from_slice(bytemuck::cast_slice(&gradients));
9403 gradient_buffer.unmap();
9404
9405 let mesh = if arc_mesh_enabled() {
9406 match build_arc_mesh_vertices(&shape_data) {
9407 Some(build) => {
9408 let cut = if build.quad_area > 0.0 {
9409 (1.0 - build.mesh_area / build.quad_area) * 100.0
9410 } else {
9411 0.0
9412 };
9413 log::warn!(
9419 "[arc-mesh] slot {id}: {} arcs meshed ({} segs), {} passthrough; \
9420 {} unique verts / {} indices (quad path: {} verts); \
9421 quad_px {:.0} -> mesh_px {:.0} (-{:.1}%)",
9422 build.meshed_arcs,
9423 build.meshed_segments,
9424 build.passthrough,
9425 build.vertices.len(),
9426 build.indices.len(),
9427 shape_count * 6,
9428 build.quad_area,
9429 build.mesh_area,
9430 cut,
9431 );
9432 (build.meshed_arcs > 0).then(|| {
9435 let vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
9436 label: Some("Replay Mesh Vertex Buffer"),
9437 size: (std::mem::size_of::<MeshVertex>() * build.vertices.len()) as u64,
9438 usage: wgpu::BufferUsages::VERTEX,
9439 mapped_at_creation: true,
9440 });
9441 vertex_buffer
9442 .slice(..)
9443 .get_mapped_range_mut()
9444 .copy_from_slice(bytemuck::cast_slice(&build.vertices));
9445 vertex_buffer.unmap();
9446 let index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
9447 label: Some("Replay Mesh Index Buffer"),
9448 size: (std::mem::size_of::<u32>() * build.indices.len()) as u64,
9449 usage: wgpu::BufferUsages::INDEX,
9450 mapped_at_creation: true,
9451 });
9452 index_buffer
9453 .slice(..)
9454 .get_mapped_range_mut()
9455 .copy_from_slice(bytemuck::cast_slice(&build.indices));
9456 index_buffer.unmap();
9457 ReplaySlotMesh {
9458 vertex_buffer,
9459 index_buffer,
9460 index_prefix: build.index_prefix,
9461 }
9462 })
9463 }
9464 None => {
9465 log::warn!(
9466 "[arc-mesh] slot {id}: geometry byte budget overflowed for \
9467 {shape_count} shapes; whole slot falls back to quad passthrough"
9468 );
9469 None
9470 }
9471 }
9472 } else {
9473 None
9474 };
9475
9476 let paint: Vec<[f32; 4]> = shape_data.iter().map(|shape| shape.color).collect();
9479 let paint_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
9480 label: Some("Replay Paint Buffer"),
9481 size: (std::mem::size_of::<[f32; 4]>() * shape_count) as u64,
9482 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
9483 mapped_at_creation: true,
9484 });
9485 paint_buffer
9486 .slice(..)
9487 .get_mapped_range_mut()
9488 .copy_from_slice(bytemuck::cast_slice(&paint));
9489 paint_buffer.unmap();
9490
9491 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
9492 label: Some("Replay Shape Bind Group"),
9493 layout: &self.shape_bind_group_layout,
9494 entries: &[
9495 wgpu::BindGroupEntry {
9496 binding: 0,
9497 resource: shape_buffer.as_entire_binding(),
9498 },
9499 wgpu::BindGroupEntry {
9500 binding: 1,
9501 resource: gradient_buffer.as_entire_binding(),
9502 },
9503 wgpu::BindGroupEntry {
9507 binding: 2,
9508 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
9509 buffer: &self.replay_slots.transform_buffer,
9510 offset: 0,
9511 size: Some(
9512 std::num::NonZeroU64::new(
9513 std::mem::size_of::<SimilarityTransform>() as u64
9514 )
9515 .expect("similarity transform is non-empty"),
9516 ),
9517 }),
9518 },
9519 wgpu::BindGroupEntry {
9520 binding: 3,
9521 resource: paint_buffer.as_entire_binding(),
9522 },
9523 ],
9524 });
9525
9526 let capture_epoch = self.replay_slots.next_capture_epoch;
9527 self.replay_slots.next_capture_epoch += 1;
9528 self.replay_slots.slots.insert(
9529 id,
9530 ReplaySlot {
9531 paint_buffer,
9532 bind_group,
9533 shape_count: shape_count as u32,
9534 paint_mirror: paint,
9535 mesh,
9536 capture_epoch,
9537 },
9538 );
9539 Some(id)
9540 }
9541
9542 #[cfg(not(target_arch = "wasm32"))]
9544 pub(crate) fn release_replay_slot(&mut self, id: u32) {
9545 if self.replay_slots.slots.remove(&id).is_some() {
9546 self.replay_slots.free_ids.push(id);
9547 self.retained_bundle_cache.clear();
9553 }
9554 }
9555
9556 #[cfg(not(target_arch = "wasm32"))]
9559 #[doc(hidden)]
9560 pub fn instanced_quads_active(&self) -> bool {
9561 self.instanced_quads.is_some()
9562 }
9563
9564 #[cfg(not(target_arch = "wasm32"))]
9567 #[doc(hidden)]
9568 pub fn replay_slot_mesh_stats(&self) -> (usize, usize) {
9569 let meshed = self
9570 .replay_slots
9571 .slots
9572 .values()
9573 .filter(|slot| slot.mesh.is_some())
9574 .count();
9575 (meshed, self.replay_slots.slots.len())
9576 }
9577
9578 #[cfg(not(target_arch = "wasm32"))]
9582 fn draw_retained_batch(
9583 &self,
9584 render_pass: &mut wgpu::RenderPass<'_>,
9585 retained: &RetainedDraw,
9586 retained_index: usize,
9587 width: u32,
9588 height: u32,
9589 ) {
9590 let Some(slot) = self.replay_slots.slots.get(&retained.slot) else {
9591 return;
9592 };
9593 if retained_index as u32 >= MAX_REPLAY_SLOTS {
9594 return;
9595 }
9596 let first = retained.first_shape.min(slot.shape_count);
9597 let last = retained
9598 .first_shape
9599 .saturating_add(retained.shape_count)
9600 .min(slot.shape_count);
9601 if first >= last {
9602 return;
9603 }
9604 self.frame_stats.bump_shapes();
9605 self.frame_stats.add_draw_calls(1);
9606 render_pass.set_scissor_rect(0, 0, width, height);
9607 let mesh = slot.mesh.as_ref().zip(self.mesh_pipeline.as_ref());
9615 match &mesh {
9616 Some((_, mesh_pipeline)) => render_pass.set_pipeline(mesh_pipeline),
9617 None => match &self.instanced_quads {
9618 Some(instanced) => render_pass.set_pipeline(&instanced.pipeline),
9619 None => render_pass.set_pipeline(&self.pipeline),
9620 },
9621 }
9622 render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
9623 render_pass.set_bind_group(
9624 1,
9625 &slot.bind_group,
9626 &[retained_index as u32 * REPLAY_TRANSFORM_STRIDE as u32],
9627 );
9628 match mesh {
9629 Some((mesh, _)) => {
9630 render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
9631 render_pass
9632 .set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
9633 render_pass.draw_indexed(
9634 mesh.index_prefix[first as usize]..mesh.index_prefix[last as usize],
9635 0,
9636 0..1,
9637 );
9638 }
9639 None => match &self.instanced_quads {
9640 Some(instanced) => {
9641 render_pass.set_index_buffer(
9642 instanced.index_buffer.slice(..),
9643 wgpu::IndexFormat::Uint16,
9644 );
9645 render_pass.draw_indexed(0..6, 0, first..last);
9646 }
9647 None => render_pass.draw(first * 6..last * 6, 0..1),
9648 },
9649 }
9650 }
9651
9652 #[cfg(not(target_arch = "wasm32"))]
9659 fn retained_bundle_key(
9660 &self,
9661 ordered_items: &[(usize, SegmentDrawItem)],
9662 retained_draws: &[RetainedDraw],
9663 item_range: Range<usize>,
9664 ) -> RetainedBundleKey {
9665 let mut ops = Vec::with_capacity(item_range.len());
9666 for (_, item) in &ordered_items[item_range] {
9667 let SegmentDrawItem::Retained(index) = item else {
9668 continue;
9669 };
9670 let Some(retained) = retained_draws.get(*index) else {
9671 continue;
9672 };
9673 let slot = self.replay_slots.slots.get(&retained.slot);
9674 let (first, last) = match slot {
9675 Some(slot) => (
9676 retained.first_shape.min(slot.shape_count),
9677 retained
9678 .first_shape
9679 .saturating_add(retained.shape_count)
9680 .min(slot.shape_count),
9681 ),
9682 None => (
9683 retained.first_shape,
9684 retained.first_shape.saturating_add(retained.shape_count),
9685 ),
9686 };
9687 ops.push(RetainedBundleOpKey {
9688 slot: retained.slot,
9689 capture_epoch: slot.map(|slot| slot.capture_epoch),
9690 first,
9691 last,
9692 retained_index: *index as u32,
9693 has_mesh: slot.is_some_and(|slot| slot.mesh.is_some())
9694 && self.mesh_pipeline.is_some(),
9695 });
9696 }
9697 RetainedBundleKey { ops }
9698 }
9699
9700 #[cfg(not(target_arch = "wasm32"))]
9707 fn build_retained_bundle(&self, key: &RetainedBundleKey) -> wgpu::RenderBundle {
9708 let mut encoder =
9709 self.device
9710 .create_render_bundle_encoder(&wgpu::RenderBundleEncoderDescriptor {
9711 label: Some("Retained Stretch Bundle"),
9712 color_formats: &[Some(self.surface_format)],
9716 depth_stencil: None,
9717 sample_count: 1,
9718 multiview: None,
9719 });
9720 for op in &key.ops {
9721 if op.capture_epoch.is_none()
9722 || op.retained_index >= MAX_REPLAY_SLOTS
9723 || op.first >= op.last
9724 {
9725 continue;
9726 }
9727 let Some(slot) = self.replay_slots.slots.get(&op.slot) else {
9728 continue;
9729 };
9730 let mesh = slot.mesh.as_ref().zip(self.mesh_pipeline.as_ref());
9731 match &mesh {
9732 Some((_, mesh_pipeline)) => encoder.set_pipeline(mesh_pipeline),
9733 None => match &self.instanced_quads {
9734 Some(instanced) => encoder.set_pipeline(&instanced.pipeline),
9735 None => encoder.set_pipeline(&self.pipeline),
9736 },
9737 }
9738 encoder.set_bind_group(0, &self.uniform_bind_group, &[]);
9739 encoder.set_bind_group(
9740 1,
9741 &slot.bind_group,
9742 &[op.retained_index * REPLAY_TRANSFORM_STRIDE as u32],
9743 );
9744 match mesh {
9745 Some((mesh, _)) => {
9746 encoder.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
9747 encoder
9748 .set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
9749 encoder.draw_indexed(
9750 mesh.index_prefix[op.first as usize]..mesh.index_prefix[op.last as usize],
9751 0,
9752 0..1,
9753 );
9754 }
9755 None => match &self.instanced_quads {
9760 Some(instanced) => {
9761 encoder.set_index_buffer(
9762 instanced.index_buffer.slice(..),
9763 wgpu::IndexFormat::Uint16,
9764 );
9765 encoder.draw_indexed(0..6, 0, op.first..op.last);
9766 }
9767 None => encoder.draw(op.first * 6..op.last * 6, 0..1),
9768 },
9769 }
9770 }
9771 encoder.finish(&wgpu::RenderBundleDescriptor {
9772 label: Some("Retained Stretch Bundle"),
9773 })
9774 }
9775
9776 #[cfg(not(target_arch = "wasm32"))]
9784 fn draw_retained_stretch_bundled(
9785 &mut self,
9786 render_pass: &mut wgpu::RenderPass<'_>,
9787 ordered_items: &[(usize, SegmentDrawItem)],
9788 retained_draws: &[RetainedDraw],
9789 item_range: Range<usize>,
9790 width: u32,
9791 height: u32,
9792 ) {
9793 let key = self.retained_bundle_key(ordered_items, retained_draws, item_range);
9794 if !self.retained_bundle_cache.hit(&key) {
9795 let bundle = self.build_retained_bundle(&key);
9796 self.retained_bundle_cache.insert(key.clone(), bundle);
9797 }
9798 for op in &key.ops {
9801 if op.capture_epoch.is_some()
9802 && op.retained_index < MAX_REPLAY_SLOTS
9803 && op.first < op.last
9804 {
9805 self.frame_stats.bump_shapes();
9806 self.frame_stats.add_draw_calls(1);
9807 }
9808 }
9809 render_pass.set_scissor_rect(0, 0, width, height);
9814 if let Some(bundle) = self.retained_bundle_cache.get(&key) {
9815 render_pass.execute_bundles(std::iter::once(bundle));
9816 }
9817 }
9818
9819 #[cfg(not(target_arch = "wasm32"))]
9822 #[doc(hidden)]
9823 pub fn retained_bundle_stats(&self) -> (u64, u64) {
9824 self.retained_bundle_cache.stats()
9825 }
9826
9827 fn draw_prepared_shapes(
9828 &self,
9829 render_pass: &mut wgpu::RenderPass<'_>,
9830 blend_mode: BlendMode,
9831 batch: PreparedShapeBatch,
9832 width: u32,
9833 height: u32,
9834 ) {
9835 self.frame_stats.bump_shapes();
9836 self.frame_stats.add_draw_calls(1);
9837 render_pass.set_scissor_rect(0, 0, width, height);
9838 #[cfg(not(target_arch = "wasm32"))]
9839 let (uniform_bind_group, shape_buffers) = (&self.uniform_bind_group, &self.shape_buffers);
9840 #[cfg(target_arch = "wasm32")]
9841 let (uniform_bind_group, shape_buffers) = (
9842 &self.wasm_uniform_batches[batch.uniform_slot].bind_group,
9843 &self.wasm_shape_batches[batch.shape_slot],
9844 );
9845 #[cfg(not(target_arch = "wasm32"))]
9850 if let Some(instanced) = &self.instanced_quads {
9851 assert!(
9852 batch.vertex_start.is_multiple_of(6) && batch.vertex_count.is_multiple_of(6),
9853 "shape batches are whole shapes: vertex range {}..+{} must be \
9854 six-aligned to convert to an instance range",
9855 batch.vertex_start,
9856 batch.vertex_count,
9857 );
9858 render_pass.set_pipeline(match blend_mode {
9859 BlendMode::DstOut => &instanced.pipeline_dst_out,
9860 _ => &instanced.pipeline,
9861 });
9862 render_pass.set_bind_group(0, uniform_bind_group, &[]);
9863 render_pass.set_bind_group(1, &shape_buffers.bind_group, &[0]);
9866 let first_shape = batch.vertex_start / 6;
9867 let shape_count = batch.vertex_count / 6;
9868 render_pass
9869 .set_index_buffer(instanced.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
9870 render_pass.draw_indexed(0..6, 0, first_shape..first_shape + shape_count);
9871 return;
9872 }
9873 render_pass.set_pipeline(match blend_mode {
9874 BlendMode::DstOut => &self.pipeline_dst_out,
9875 _ => &self.pipeline,
9876 });
9877 render_pass.set_bind_group(0, uniform_bind_group, &[]);
9878 render_pass.set_bind_group(1, &shape_buffers.bind_group, &[0]);
9881 render_pass.draw(
9884 batch.vertex_start..batch.vertex_start + batch.vertex_count,
9885 0..1,
9886 );
9887 }
9888
9889 #[allow(clippy::too_many_arguments)]
9892 fn encode_shapes_pass<'a, I, C: FrameCommandRecorder>(
9893 &mut self,
9894 frame_encoder: &mut C,
9895 target_view: &wgpu::TextureView,
9896 layer_shapes: I,
9897 blend_mode: BlendMode,
9898 width: u32,
9899 height: u32,
9900 root_scale: f32,
9901 load_op: wgpu::LoadOp<wgpu::Color>,
9902 viewport_offset: [f32; 2],
9903 ) where
9904 I: Iterator<Item = &'a DrawShape>,
9905 {
9906 let mut staged_uploads = self.take_staged_uploads();
9907 let viewport = ViewportUniformParams {
9908 width,
9909 height,
9910 offset: viewport_offset,
9911 };
9912 let Some(batch) = self.prepare_shapes_batch(
9913 layer_shapes
9914 .filter(|shape| shape_draw_is_visible_in_viewport(shape, viewport, root_scale)),
9915 root_scale,
9916 viewport,
9917 &mut staged_uploads,
9918 ) else {
9919 self.restore_staged_uploads(staged_uploads);
9920 return;
9921 };
9922 let upload_offset =
9923 frame_encoder.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
9924 self.flush_staged_uploads_at(frame_encoder.encoder(), &staged_uploads, upload_offset);
9925 self.restore_staged_uploads(staged_uploads);
9926 let mut render_pass =
9927 frame_encoder
9928 .encoder()
9929 .begin_render_pass(&wgpu::RenderPassDescriptor {
9930 label: Some("Shape Pass"),
9931 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
9932 view: target_view,
9933 resolve_target: None,
9934 depth_slice: None,
9935 ops: wgpu::Operations {
9936 load: load_op,
9937 store: wgpu::StoreOp::Store,
9938 },
9939 })],
9940 depth_stencil_attachment: None,
9941 timestamp_writes: None,
9942 occlusion_query_set: None,
9943 multiview_mask: None,
9944 });
9945 self.draw_prepared_shapes(&mut render_pass, blend_mode, batch, width, height);
9946 }
9947
9948 fn draw_prepared_images(
9949 &mut self,
9950 render_pass: &mut wgpu::RenderPass<'_>,
9951 batch: &PreparedImageBatch,
9952 blend_mode: BlendMode,
9953 ) -> Result<(), String> {
9954 if batch.cmds.is_empty() {
9955 return Ok(());
9956 }
9957 self.frame_stats.bump_images();
9958 self.frame_stats.add_draw_calls(batch.cmds.len() as u32);
9959 render_pass.set_pipeline(match blend_mode {
9960 BlendMode::DstOut => &self.image_pipeline_dst_out,
9961 _ => &self.image_pipeline,
9962 });
9963 #[cfg(not(target_arch = "wasm32"))]
9964 let (uniform_bind_group, vertex_buffer, index_buffer) = (
9965 &self.uniform_bind_group,
9966 &self.image_vertex_buffer,
9967 &self.image_index_buffer,
9968 );
9969 #[cfg(target_arch = "wasm32")]
9970 let (uniform_bind_group, vertex_buffer, index_buffer) = (
9971 &self.wasm_uniform_batches[batch.uniform_slot].bind_group,
9972 &self.wasm_image_batches[batch.image_slot].vertex_buffer,
9973 &self.wasm_image_batches[batch.image_slot].index_buffer,
9974 );
9975 render_pass.set_bind_group(0, uniform_bind_group, &[]);
9976 render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
9977 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
9978
9979 for cmd in &batch.cmds {
9980 let (sx, sy, sw, sh) = cmd.scissor;
9981 render_pass.set_scissor_rect(sx, sy, sw, sh);
9982
9983 let cached = self
9984 .image_texture_cache
9985 .get(&cmd.image_id)
9986 .ok_or_else(|| "image texture missing from cache".to_string())?;
9987 render_pass.set_bind_group(1, cached.bind_group(cmd.sampling), &[]);
9988 render_pass.draw_indexed(cmd.index_start..(cmd.index_start + 6), 0, 0..1);
9989 }
9990 Ok(())
9991 }
9992
9993 fn draw_prepared_glyphs(
9994 &mut self,
9995 render_pass: &mut wgpu::RenderPass<'_>,
9996 batch: &PreparedGlyphBatch,
9997 ) -> Result<(), String> {
9998 if batch.cmds.is_empty() {
9999 return Ok(());
10000 }
10001 #[cfg(not(target_arch = "wasm32"))]
10002 {
10003 self.draw_native_prepared_glyph_cmd_range(
10004 render_pass,
10005 &batch.cmds,
10006 0..batch.cmds.len(),
10007 )?;
10008 }
10009 #[cfg(target_arch = "wasm32")]
10010 {
10011 self.frame_stats.bump_text();
10012 self.frame_stats.add_draw_calls(batch.cmds.len() as u32);
10013 render_pass.set_pipeline(&self.glyph_atlas_pipeline);
10014 let (uniform_bind_group, vertex_buffer, index_buffer) = (
10015 &self.wasm_uniform_batches[batch.uniform_slot].bind_group,
10016 &self.wasm_image_batches[batch.image_slot].vertex_buffer,
10017 &self.wasm_image_batches[batch.image_slot].index_buffer,
10018 );
10019 render_pass.set_bind_group(0, uniform_bind_group, &[]);
10020 render_pass.set_bind_group(1, &self.text_glyph_atlas.bind_group, &[]);
10021 render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
10022 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
10023
10024 for cmd in &batch.cmds {
10025 let (sx, sy, sw, sh) = cmd.scissor;
10026 render_pass.set_scissor_rect(sx, sy, sw, sh);
10027 let GlyphDrawSource::Shared {
10028 index_start,
10029 index_count,
10030 } = cmd.source;
10031 render_pass.draw_indexed(index_start..(index_start + index_count), 0, 0..1);
10032 }
10033 }
10034 Ok(())
10035 }
10036
10037 #[cfg(not(target_arch = "wasm32"))]
10038 fn draw_native_prepared_image_cmd_range(
10039 &mut self,
10040 render_pass: &mut wgpu::RenderPass<'_>,
10041 cmds: &[ImageDrawCmd],
10042 cmd_range: Range<usize>,
10043 blend_mode: BlendMode,
10044 ) -> Result<(), String> {
10045 let Some(cmds) = cmds.get(cmd_range) else {
10046 return Err("image command range is outside the prepared command buffer".to_string());
10047 };
10048 if cmds.is_empty() {
10049 return Ok(());
10050 }
10051
10052 self.frame_stats.bump_images();
10053 self.frame_stats.add_draw_calls(cmds.len() as u32);
10054 render_pass.set_pipeline(match blend_mode {
10055 BlendMode::DstOut => &self.image_pipeline_dst_out,
10056 _ => &self.image_pipeline,
10057 });
10058 render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
10059 render_pass.set_index_buffer(self.image_index_buffer.slice(..), wgpu::IndexFormat::Uint32);
10060 render_pass.set_vertex_buffer(0, self.image_vertex_buffer.slice(..));
10061
10062 for cmd in cmds {
10063 let (sx, sy, sw, sh) = cmd.scissor;
10064 render_pass.set_scissor_rect(sx, sy, sw, sh);
10065
10066 let cached = self
10067 .image_texture_cache
10068 .get(&cmd.image_id)
10069 .ok_or_else(|| "image texture missing from cache".to_string())?;
10070 render_pass.set_bind_group(1, cached.bind_group(cmd.sampling), &[]);
10071 render_pass.draw_indexed(cmd.index_start..(cmd.index_start + 6), 0, 0..1);
10072 }
10073 Ok(())
10074 }
10075
10076 #[cfg(not(target_arch = "wasm32"))]
10077 fn draw_native_prepared_glyph_cmd_range(
10078 &mut self,
10079 render_pass: &mut wgpu::RenderPass<'_>,
10080 cmds: &[GlyphDrawCmd],
10081 cmd_range: Range<usize>,
10082 ) -> Result<(), String> {
10083 let Some(cmds) = cmds.get(cmd_range) else {
10084 return Err("glyph command range is outside the prepared command buffer".to_string());
10085 };
10086 if cmds.is_empty() {
10087 return Ok(());
10088 }
10089
10090 self.frame_stats.bump_text();
10091 self.frame_stats.add_draw_calls(cmds.len() as u32);
10092
10093 let mut shared_buffers_bound = false;
10094 let mut retained_pipeline_bound = false;
10095 for cmd in cmds {
10096 let (sx, sy, sw, sh) = cmd.scissor;
10097 render_pass.set_scissor_rect(sx, sy, sw, sh);
10098 match cmd.source {
10099 GlyphDrawSource::Shared {
10100 index_start,
10101 index_count,
10102 } => {
10103 if retained_pipeline_bound || !shared_buffers_bound {
10104 render_pass.set_pipeline(&self.glyph_atlas_pipeline);
10105 render_pass.set_bind_group(1, &self.text_glyph_atlas.bind_group, &[]);
10106 retained_pipeline_bound = false;
10107 }
10108 if !shared_buffers_bound {
10109 render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
10110 render_pass.set_index_buffer(
10111 self.image_index_buffer.slice(..),
10112 wgpu::IndexFormat::Uint32,
10113 );
10114 render_pass.set_vertex_buffer(0, self.image_vertex_buffer.slice(..));
10115 shared_buffers_bound = true;
10116 }
10117 render_pass.draw_indexed(index_start..(index_start + index_count), 0, 0..1);
10118 }
10119 GlyphDrawSource::Retained {
10120 cache_key,
10121 uniform_slot,
10122 } => {
10123 shared_buffers_bound = false;
10124 if !retained_pipeline_bound {
10125 render_pass.set_pipeline(&self.retained_glyph_atlas_pipeline);
10126 render_pass.set_bind_group(1, &self.text_glyph_atlas.bind_group, &[]);
10127 retained_pipeline_bound = true;
10128 }
10129 let cached = self
10130 .text_glyph_gpu_run_cache
10131 .peek(&cache_key)
10132 .ok_or_else(|| "retained glyph buffer missing from cache".to_string())?;
10133 let dynamic_offset =
10134 self.retained_glyph_uniform_dynamic_offset(uniform_slot)?;
10135 render_pass.set_bind_group(
10136 0,
10137 &self.retained_glyph_uniform_bind_group,
10138 &[dynamic_offset],
10139 );
10140 render_pass
10141 .set_index_buffer(cached.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
10142 render_pass.set_vertex_buffer(0, cached.vertex_buffer.slice(..));
10143 render_pass.draw_indexed(0..cached.index_count, 0, 0..1);
10144 }
10145 }
10146 }
10147 Ok(())
10148 }
10149
10150 fn append_image_draw_cmd(
10151 &mut self,
10152 image_draw: &ImageDraw,
10153 viewport: ViewportUniformParams,
10154 root_scale: f32,
10155 image_vertices: &mut Vec<Vertex>,
10156 image_indices: &mut Vec<u32>,
10157 image_cmds: &mut Vec<ImageDrawCmd>,
10158 ) -> Result<(), String> {
10159 let snap_delta = image_draw
10160 .snap_anchor
10161 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
10162 .unwrap_or_default();
10163 let rect = image_draw.rect.translate(snap_delta.x, snap_delta.y);
10164 if rect.width <= 0.0 || rect.height <= 0.0 || image_draw.alpha <= 0.0 {
10165 return Ok(());
10166 }
10167
10168 let (tint, cpu_filter) = tint_for_image(image_draw.color_filter, image_draw.alpha);
10169 if tint[3] <= 0.0 {
10170 return Ok(());
10171 }
10172
10173 let prepared_image = if let Some(filter) = cpu_filter {
10174 apply_filter_to_bitmap(&image_draw.image, filter)?
10175 } else {
10176 image_draw.image.clone()
10177 };
10178 self.ensure_image_cached(&prepared_image)?;
10179
10180 let mut adjusted_image = ImageDraw {
10181 rect,
10182 local_rect: image_draw.local_rect.translate(snap_delta.x, snap_delta.y),
10183 quad: translate_quad(image_draw.quad, snap_delta),
10184 snap_anchor: image_draw.snap_anchor,
10185 image: image_draw.image.clone(),
10186 alpha: image_draw.alpha,
10187 color_filter: image_draw.color_filter,
10188 sampling: image_draw.sampling,
10189 z_index: image_draw.z_index,
10190 clip: image_draw.clip,
10191 blend_mode: image_draw.blend_mode,
10192 src_rect: image_draw.src_rect,
10193 motion_context_animated: image_draw.motion_context_animated,
10194 };
10195 snap_nearest_image_to_device_pixels(&mut adjusted_image, root_scale);
10196 let Some(scissor) =
10197 scissor_rect_for_image(&adjusted_image, root_scale, viewport.width, viewport.height)
10198 else {
10199 return Ok(());
10200 };
10201
10202 let Some(uv_rect) = image_uv_rect(&image_draw.image, image_draw.src_rect) else {
10203 return Ok(());
10204 };
10205 let device_quad =
10206 nearest_image_device_quad(&adjusted_image, root_scale).unwrap_or_else(|| {
10207 if adjusted_image.snap_anchor.is_some() {
10208 canonicalized_scaled_quad(adjusted_image.quad, root_scale)
10209 } else {
10210 scaled_quad(adjusted_image.quad, root_scale)
10211 }
10212 });
10213
10214 let base_vertex = image_vertices.len() as u32;
10215 let index_start = image_indices.len() as u32;
10216 image_indices.extend_from_slice(&[
10217 base_vertex,
10218 base_vertex + 1,
10219 base_vertex + 2,
10220 base_vertex + 2,
10221 base_vertex + 1,
10222 base_vertex + 3,
10223 ]);
10224 image_vertices.extend_from_slice(&[
10225 Vertex {
10226 position: device_quad[0],
10227 color: tint,
10228 uv: [uv_rect.min[0], uv_rect.min[1]],
10229 uv_bounds: uv_rect.sample_bounds,
10230 },
10231 Vertex {
10232 position: device_quad[1],
10233 color: tint,
10234 uv: [uv_rect.max[0], uv_rect.min[1]],
10235 uv_bounds: uv_rect.sample_bounds,
10236 },
10237 Vertex {
10238 position: device_quad[2],
10239 color: tint,
10240 uv: [uv_rect.min[0], uv_rect.max[1]],
10241 uv_bounds: uv_rect.sample_bounds,
10242 },
10243 Vertex {
10244 position: device_quad[3],
10245 color: tint,
10246 uv: [uv_rect.max[0], uv_rect.max[1]],
10247 uv_bounds: uv_rect.sample_bounds,
10248 },
10249 ]);
10250
10251 image_cmds.push(ImageDrawCmd {
10252 index_start,
10253 scissor,
10254 image_id: prepared_image.id(),
10255 sampling: image_draw.sampling,
10256 });
10257 Ok(())
10258 }
10259
10260 #[cfg(not(target_arch = "wasm32"))]
10261 fn stage_native_image_buffers(
10262 &mut self,
10263 staged_uploads: &mut StagedBufferUploads,
10264 viewport: ViewportUniformParams,
10265 image_vertices: &[Vertex],
10266 image_indices: &[u32],
10267 ) {
10268 if image_indices.is_empty() {
10269 return;
10270 }
10271
10272 self.stage_viewport_uniforms(staged_uploads, viewport);
10273 let needed_bytes = std::mem::size_of_val(image_vertices) as u64;
10278 if needed_bytes > self.image_vertex_buffer.size() {
10279 self.image_vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
10280 label: Some("Image Vertex Buffer"),
10281 size: needed_bytes.next_power_of_two(),
10282 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
10283 mapped_at_creation: false,
10284 });
10285 }
10286 let needed_index_bytes = std::mem::size_of_val(image_indices) as u64;
10287 if needed_index_bytes > self.image_index_buffer.size() {
10288 self.image_index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
10289 label: Some("Image Index Buffer"),
10290 size: needed_index_bytes.next_power_of_two(),
10291 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
10292 mapped_at_creation: false,
10293 });
10294 }
10295
10296 staged_uploads.stage(
10297 UploadTarget::ImageVertex,
10298 bytemuck::cast_slice(image_vertices),
10299 );
10300 staged_uploads.stage(
10301 UploadTarget::ImageIndex,
10302 bytemuck::cast_slice(image_indices),
10303 );
10304 }
10305
10306 fn prepare_image_draw_cmds<'a, I>(
10309 &mut self,
10310 layer_images: I,
10311 viewport: ViewportUniformParams,
10312 root_scale: f32,
10313 staged_uploads: &mut StagedBufferUploads,
10314 ) -> Result<PreparedImageBatch, String>
10315 where
10316 I: Iterator<Item = &'a ImageDraw>,
10317 {
10318 #[cfg(target_arch = "wasm32")]
10319 let _ = staged_uploads;
10320
10321 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
10322 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
10323 let mut image_cmds = std::mem::take(&mut self.scratch_image_cmds);
10324 image_vertices.clear();
10325 image_indices.clear();
10326 image_cmds.clear();
10327
10328 for image_draw in layer_images {
10329 self.append_image_draw_cmd(
10330 image_draw,
10331 viewport,
10332 root_scale,
10333 &mut image_vertices,
10334 &mut image_indices,
10335 &mut image_cmds,
10336 )?;
10337 }
10338
10339 #[cfg(not(target_arch = "wasm32"))]
10340 if !image_cmds.is_empty() {
10341 self.stage_native_image_buffers(
10342 staged_uploads,
10343 viewport,
10344 &image_vertices,
10345 &image_indices,
10346 );
10347 }
10348
10349 #[cfg(target_arch = "wasm32")]
10350 let image_slot = if image_cmds.is_empty() {
10351 0
10352 } else {
10353 let slot = self.claim_wasm_image_batch();
10354 {
10355 let buffers = &mut self.wasm_image_batches[slot];
10356 buffers.ensure_capacity(&self.device, image_vertices.len(), image_indices.len());
10357 }
10358 let buffers = &self.wasm_image_batches[slot];
10359 self.write_wasm_buffer(
10360 &buffers.vertex_buffer,
10361 bytemuck::cast_slice(&image_vertices),
10362 );
10363 self.write_wasm_buffer(&buffers.index_buffer, bytemuck::cast_slice(&image_indices));
10364 slot
10365 };
10366
10367 #[cfg(target_arch = "wasm32")]
10368 let uniform_slot = if image_cmds.is_empty() {
10369 0
10370 } else {
10371 self.prepare_wasm_viewport_uniforms(viewport)
10372 };
10373
10374 self.scratch_image_vertices = image_vertices;
10375 self.scratch_image_indices = image_indices;
10376 Ok(PreparedImageBatch {
10377 cmds: image_cmds,
10378 #[cfg(target_arch = "wasm32")]
10379 image_slot,
10380 #[cfg(target_arch = "wasm32")]
10381 uniform_slot,
10382 })
10383 }
10384
10385 fn glyph_atlas_entry_for(
10386 &mut self,
10387 glyph: &SoftwareGlyphAtlasGlyph,
10388 ) -> Result<GlyphAtlasEntry, String> {
10389 if let Some(entry) = self.text_glyph_atlas.upload_glyph(
10390 glyph.key,
10391 glyph,
10392 &self.queue,
10393 &mut self.frame_graph_executor,
10394 &mut self.frame_stats,
10395 ) {
10396 return Ok(entry);
10397 }
10398
10399 self.text_glyph_atlas.reset(
10400 &self.device,
10401 &self.image_bind_group_layout,
10402 &self.image_nearest_sampler,
10403 );
10404 Err("text glyph atlas filled and was reset".to_string())
10405 }
10406
10407 fn glyph_atlas_entry_for_cached(
10408 &mut self,
10409 glyph: &SoftwareGlyphAtlasPlacement,
10410 ) -> Option<GlyphAtlasEntry> {
10411 let entry = self.text_glyph_atlas.entry(&glyph.key)?;
10412 self.frame_stats.record_text_glyph_atlas_hit();
10413 Some(entry)
10414 }
10415
10416 fn glyph_atlas_entry_for_placement(
10417 &mut self,
10418 glyph: &SoftwareGlyphAtlasPlacement,
10419 ) -> Result<GlyphAtlasEntry, String> {
10420 if let Some(entry) = self.glyph_atlas_entry_for_cached(glyph) {
10421 return Ok(entry);
10422 }
10423
10424 let Some(upload_glyph) = self.text_glyph_mask_cache.atlas_glyph_for_placement(glyph) else {
10425 return Err("text glyph placement has no retained raster mask".to_string());
10426 };
10427 self.glyph_atlas_entry_for(&upload_glyph)
10428 }
10429
10430 fn prepare_text_glyph_quads(
10431 &mut self,
10432 run_key: TextGlyphRunCacheKey,
10433 atlas_generation: u64,
10434 cached_glyph_run: Option<&[SoftwareGlyphAtlasPlacement]>,
10435 collected_run: &[SoftwareGlyphAtlasRunGlyph],
10436 generated_quads: &mut Vec<CachedTextGlyphQuad>,
10437 ) -> Result<Rc<[CachedTextGlyphQuad]>, String> {
10438 generated_quads.clear();
10439 if let Some(glyph_run) = cached_glyph_run {
10440 for glyph in glyph_run {
10441 if glyph.width == 0 || glyph.height == 0 || glyph.color.3 <= 0.0 {
10442 continue;
10443 }
10444 let entry = self.glyph_atlas_entry_for_placement(glyph)?;
10445 generated_quads.push(cached_text_glyph_quad(
10450 glyph,
10451 entry,
10452 self.text_glyph_atlas.size(),
10453 ));
10454 }
10455 } else {
10456 for run_glyph in collected_run {
10457 let placement = run_glyph.placement();
10458 if placement.width == 0 || placement.height == 0 || placement.color.3 <= 0.0 {
10459 continue;
10460 }
10461 let entry = match run_glyph {
10462 SoftwareGlyphAtlasRunGlyph::Cached(placement) => {
10463 self.glyph_atlas_entry_for_placement(placement)?
10464 }
10465 SoftwareGlyphAtlasRunGlyph::New(glyph) => self.glyph_atlas_entry_for(glyph)?,
10466 };
10467 generated_quads.push(cached_text_glyph_quad(
10468 &placement,
10469 entry,
10470 self.text_glyph_atlas.size(),
10471 ));
10472 }
10473 }
10474
10475 let quads: Rc<[CachedTextGlyphQuad]> = Rc::from(generated_quads.clone().into_boxed_slice());
10476 if let Some(cached) = self.text_glyph_run_cache.get_mut(&run_key) {
10477 cached.quads = Some(Rc::clone(&quads));
10478 cached.atlas_generation = atlas_generation;
10479 }
10480 Ok(quads)
10481 }
10482
10483 #[allow(clippy::too_many_arguments)]
10484 fn append_text_glyph_quad_run(
10485 &mut self,
10486 source_raster_rect: Rect,
10487 quads: &[CachedTextGlyphQuad],
10488 clip: Option<Rect>,
10489 viewport: ViewportUniformParams,
10490 root_scale: f32,
10491 image_vertices: &mut Vec<Vertex>,
10492 image_indices: &mut Vec<u32>,
10493 record_cached_hits: bool,
10494 ) -> usize {
10495 let mut appended = 0usize;
10496 for quad in quads {
10497 if !cached_text_glyph_quad_is_visible_in_viewport(
10498 source_raster_rect,
10499 quad,
10500 clip,
10501 viewport,
10502 root_scale,
10503 ) {
10504 continue;
10505 }
10506 if append_cached_text_glyph_quad(
10507 source_raster_rect,
10508 quad,
10509 image_vertices,
10510 image_indices,
10511 ) {
10512 if record_cached_hits {
10513 self.frame_stats.record_text_glyph_atlas_hit();
10514 }
10515 appended = appended.saturating_add(1);
10516 }
10517 }
10518 appended
10519 }
10520
10521 #[cfg(not(target_arch = "wasm32"))]
10522 fn retained_glyph_viewport(
10523 viewport: ViewportUniformParams,
10524 source_raster_rect: Rect,
10525 ) -> ViewportUniformParams {
10526 ViewportUniformParams {
10527 width: viewport.width,
10528 height: viewport.height,
10529 offset: [
10530 viewport.offset[0] - source_raster_rect.x,
10531 viewport.offset[1] - source_raster_rect.y,
10532 ],
10533 }
10534 }
10535
10536 #[cfg(not(target_arch = "wasm32"))]
10537 fn retained_text_glyph_run_ready(&mut self, cache_key: TextGlyphRunCacheKey) -> bool {
10538 let atlas_generation = self.text_glyph_atlas.generation();
10539 self.text_glyph_gpu_run_cache
10540 .peek(&cache_key)
10541 .is_some_and(|cached| cached.atlas_generation == atlas_generation)
10542 }
10543
10544 #[cfg(not(target_arch = "wasm32"))]
10545 #[allow(clippy::too_many_arguments)]
10546 fn emit_retained_text_glyph_run_if_ready(
10547 &mut self,
10548 cache_key: TextGlyphRunCacheKey,
10549 quads: &[CachedTextGlyphQuad],
10550 clip: Option<Rect>,
10551 viewport: ViewportUniformParams,
10552 source_raster_rect: Rect,
10553 scissor: (u32, u32, u32, u32),
10554 staged_uploads: &mut StagedBufferUploads,
10555 glyph_cmds: &mut Vec<GlyphDrawCmd>,
10556 ) -> bool {
10557 if !should_use_retained_text_glyph_run(quads.len(), clip) {
10558 return false;
10559 }
10560 if !self.retained_text_glyph_run_ready(cache_key)
10561 && !self.ensure_retained_text_glyph_run(cache_key, quads)
10562 {
10563 return false;
10564 }
10565
10566 let uniform_slot = self.stage_retained_glyph_viewport_uniforms(
10567 staged_uploads,
10568 Self::retained_glyph_viewport(viewport, source_raster_rect),
10569 );
10570 glyph_cmds.push(GlyphDrawCmd::retained(cache_key, uniform_slot, scissor));
10571 true
10572 }
10573
10574 #[cfg(not(target_arch = "wasm32"))]
10575 fn ensure_retained_text_glyph_run(
10576 &mut self,
10577 cache_key: TextGlyphRunCacheKey,
10578 quads: &[CachedTextGlyphQuad],
10579 ) -> bool {
10580 let atlas_generation = self.text_glyph_atlas.generation();
10581 if self
10582 .text_glyph_gpu_run_cache
10583 .peek(&cache_key)
10584 .is_some_and(|cached| cached.atlas_generation == atlas_generation)
10585 {
10586 return true;
10587 }
10588
10589 let mut vertices = Vec::with_capacity(quads.len().saturating_mul(4));
10590 let mut indices = Vec::with_capacity(quads.len().saturating_mul(6));
10591 let origin = Rect {
10592 x: 0.0,
10593 y: 0.0,
10594 width: 0.0,
10595 height: 0.0,
10596 };
10597 for quad in quads {
10598 append_cached_text_glyph_quad(origin, quad, &mut vertices, &mut indices);
10599 }
10600 if indices.is_empty() {
10601 return false;
10602 }
10603
10604 let vertex_bytes = bytemuck::cast_slice(&vertices);
10605 let index_bytes = bytemuck::cast_slice(&indices);
10606 let vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
10607 label: Some("Retained Text Glyph Vertex Buffer"),
10608 size: vertex_bytes.len() as u64,
10609 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
10610 mapped_at_creation: false,
10611 });
10612 let index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
10613 label: Some("Retained Text Glyph Index Buffer"),
10614 size: index_bytes.len() as u64,
10615 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
10616 mapped_at_creation: false,
10617 });
10618 let vertex_upload =
10619 self.frame_graph_executor
10620 .upload_buffer(&self.queue, &vertex_buffer, 0, vertex_bytes);
10621 self.frame_stats.record_command_stats(vertex_upload);
10622 let index_upload =
10623 self.frame_graph_executor
10624 .upload_buffer(&self.queue, &index_buffer, 0, index_bytes);
10625 self.frame_stats.record_command_stats(index_upload);
10626
10627 self.text_glyph_gpu_run_cache.put(
10628 cache_key,
10629 CachedGpuTextGlyphRun {
10630 vertex_buffer,
10631 index_buffer,
10632 index_count: indices.len() as u32,
10633 atlas_generation,
10634 },
10635 );
10636 true
10637 }
10638
10639 #[allow(clippy::too_many_arguments)]
10640 fn append_text_glyph_draws<'a, I>(
10641 &mut self,
10642 layer_texts: I,
10643 viewport: ViewportUniformParams,
10644 root_scale: f32,
10645 allow_offscreen_prewarm: bool,
10646 staged_uploads: &mut StagedBufferUploads,
10647 image_vertices: &mut Vec<Vertex>,
10648 image_indices: &mut Vec<u32>,
10649 glyph_cmds: &mut Vec<GlyphDrawCmd>,
10650 ) -> Result<bool, String>
10651 where
10652 I: IntoIterator<Item = &'a TextDraw>,
10653 {
10654 let append_start = Instant::now();
10655 let initial_vertex_len = image_vertices.len();
10656 let initial_index_len = image_indices.len();
10657 let initial_cmd_len = glyph_cmds.len();
10658 let initial_staged_bytes_len = staged_uploads.bytes.len();
10659 let initial_staged_copies_len = staged_uploads.copies.len();
10660 let mut collected_run = std::mem::take(&mut self.scratch_text_glyph_run);
10661 let mut collected_placements = std::mem::take(&mut self.scratch_text_glyph_placements);
10662 let mut generated_quads = std::mem::take(&mut self.scratch_text_glyph_quads);
10663 generated_quads.clear();
10664 let mut visited = 0usize;
10665 let mut emitted_glyphs = 0usize;
10666 let mut prewarmed_glyphs = 0usize;
10667 let mut run_hits = 0usize;
10668 let mut run_misses = 0usize;
10669
10670 for text_draw in layer_texts {
10671 visited = visited.saturating_add(1);
10672 let Some((logical_rect, raster_rect, clip, text_scale, static_text_motion)) =
10673 self.text_raster_geometry(text_draw, root_scale)
10674 else {
10675 continue;
10676 };
10677 if !static_text_motion {
10678 image_vertices.truncate(initial_vertex_len);
10679 image_indices.truncate(initial_index_len);
10680 glyph_cmds.truncate(initial_cmd_len);
10681 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
10682 self.scratch_text_glyph_run = collected_run;
10683 self.scratch_text_glyph_placements = collected_placements;
10684 self.scratch_text_glyph_quads = generated_quads;
10685 return Ok(false);
10686 }
10687 let is_visible =
10688 text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale);
10689 let draw_action = text_glyph_draw_action(
10690 is_visible,
10691 text_draw_should_prewarm_in_viewport(logical_rect, clip, viewport, root_scale),
10692 allow_offscreen_prewarm,
10693 );
10694 if draw_action == TextGlyphDrawAction::Skip {
10695 continue;
10696 }
10697
10698 let raster_source = text_glyph_raster_source(text_draw, raster_rect);
10699 let source_draw = raster_source.draw.as_ref();
10700 let source_raster_rect = raster_source.raster_rect;
10701
10702 let run_key = Self::text_glyph_run_cache_key(
10703 source_draw,
10704 source_raster_rect,
10705 text_scale,
10706 static_text_motion,
10707 );
10708 let atlas_generation = self.text_glyph_atlas.generation();
10709 let mut cached_quad_run = None;
10710 let mut miss_collect_ms = None;
10711 let mut miss_cached_glyphs = 0usize;
10712 let mut miss_new_glyphs = 0usize;
10713 let cached_glyph_run = if let Some(cached) = self.text_glyph_run_cache.get(&run_key) {
10714 run_hits = run_hits.saturating_add(1);
10715 if cached.atlas_generation == atlas_generation {
10716 cached_quad_run = cached.quads.as_ref().map(Rc::clone);
10717 }
10718 Some(Rc::clone(&cached.glyphs))
10719 } else {
10720 run_misses = run_misses.saturating_add(1);
10721 collected_run.clear();
10722 let collect_start = Instant::now();
10723 let collect_result = collect_solid_text_atlas_run(
10724 source_draw.text.as_ref(),
10725 source_raster_rect,
10726 &source_draw.text_style,
10727 source_draw.color,
10728 source_draw.font_size,
10729 text_scale,
10730 &self.text_fonts,
10731 &mut self.text_glyph_mask_cache,
10732 &mut collected_run,
10733 );
10734 miss_collect_ms = Some(instant_ms(collect_start, Instant::now()));
10735 if collect_result.is_none() {
10736 if text_atlas_fallback_diag_enabled() {
10737 let preview: String = source_draw.text.text.chars().take(96).collect();
10738 log::warn!(
10739 "[text-atlas-fallback] node={:?} visible={} prewarm={} spans={} links={} text_len={} preview={:?} span_style={:?} paragraph_style={:?}",
10740 source_draw.node_id,
10741 is_visible,
10742 draw_action == TextGlyphDrawAction::PrewarmOffscreen,
10743 source_draw.text.span_styles.len(),
10744 source_draw.text.links.len(),
10745 source_draw.text.text.len(),
10746 preview,
10747 source_draw.text_style.span_style,
10748 source_draw.text_style.paragraph_style,
10749 );
10750 }
10751 if draw_action == TextGlyphDrawAction::PrewarmOffscreen {
10752 continue;
10753 }
10754 image_vertices.truncate(initial_vertex_len);
10755 image_indices.truncate(initial_index_len);
10756 glyph_cmds.truncate(initial_cmd_len);
10757 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
10758 self.scratch_text_glyph_run = collected_run;
10759 self.scratch_text_glyph_placements = collected_placements;
10760 self.scratch_text_glyph_quads = generated_quads;
10761 return Ok(false);
10762 }
10763 if text_glyph_run_diag_enabled() {
10764 miss_cached_glyphs = collected_run
10765 .iter()
10766 .filter(|glyph| matches!(glyph, SoftwareGlyphAtlasRunGlyph::Cached(_)))
10767 .count();
10768 miss_new_glyphs = collected_run.len().saturating_sub(miss_cached_glyphs);
10769 }
10770 collected_placements.clear();
10771 collected_placements.extend(
10772 collected_run
10773 .iter()
10774 .map(SoftwareGlyphAtlasRunGlyph::placement),
10775 );
10776 let glyphs: Rc<[SoftwareGlyphAtlasPlacement]> =
10777 Rc::from(collected_placements.clone().into_boxed_slice());
10778 self.text_glyph_run_cache.put(
10779 run_key,
10780 CachedTextGlyphRun {
10781 glyphs,
10782 quads: None,
10783 atlas_generation: 0,
10784 },
10785 );
10786 None
10787 };
10788
10789 if draw_action == TextGlyphDrawAction::PrewarmOffscreen {
10790 let prewarm_quads = if let Some(quad_run) = cached_quad_run {
10791 quad_run
10792 } else {
10793 let prepare_start = Instant::now();
10794 match self.prepare_text_glyph_quads(
10795 run_key,
10796 atlas_generation,
10797 cached_glyph_run.as_deref(),
10798 &collected_run,
10799 &mut generated_quads,
10800 ) {
10801 Ok(quads) => {
10802 if let Some(collect_ms) = miss_collect_ms {
10803 if text_glyph_run_diag_enabled() {
10804 log::warn!(
10805 "[text-glyph-run-diag] visible=false glyphs={} cached={} new={} collect_ms={:.2} prepare_ms={:.2}",
10806 quads.len(),
10807 miss_cached_glyphs,
10808 miss_new_glyphs,
10809 collect_ms,
10810 instant_ms(prepare_start, Instant::now()),
10811 );
10812 }
10813 }
10814 quads
10815 }
10816 Err(_) => continue,
10817 }
10818 };
10819 #[cfg(not(target_arch = "wasm32"))]
10820 if should_use_retained_text_glyph_run(prewarm_quads.len(), source_draw.clip) {
10821 self.ensure_retained_text_glyph_run(run_key, prewarm_quads.as_ref());
10822 }
10823 prewarmed_glyphs = prewarmed_glyphs.saturating_add(prewarm_quads.len());
10824 continue;
10825 }
10826
10827 let draw_rect = Rect {
10828 x: source_raster_rect.x / root_scale,
10829 y: source_raster_rect.y / root_scale,
10830 width: source_raster_rect.width / root_scale,
10831 height: source_raster_rect.height / root_scale,
10832 };
10833 let Some(scissor) = scissor_rect_for_layer(
10834 draw_rect,
10835 source_draw.clip,
10836 root_scale,
10837 viewport.width,
10838 viewport.height,
10839 ) else {
10840 continue;
10841 };
10842
10843 #[cfg(not(target_arch = "wasm32"))]
10844 if let Some(quad_run) = cached_quad_run.as_ref() {
10845 if should_use_retained_text_glyph_run(quad_run.len(), source_draw.clip)
10846 && self.emit_retained_text_glyph_run_if_ready(
10847 run_key,
10848 quad_run.as_ref(),
10849 source_draw.clip,
10850 viewport,
10851 source_raster_rect,
10852 scissor,
10853 staged_uploads,
10854 glyph_cmds,
10855 )
10856 {
10857 emitted_glyphs = emitted_glyphs.saturating_add(quad_run.len());
10858 continue;
10859 }
10860 }
10861
10862 let index_start = image_indices.len() as u32;
10863 if let Some(quad_run) = cached_quad_run {
10864 emitted_glyphs = emitted_glyphs.saturating_add(self.append_text_glyph_quad_run(
10865 source_raster_rect,
10866 quad_run.as_ref(),
10867 source_draw.clip,
10868 viewport,
10869 root_scale,
10870 image_vertices,
10871 image_indices,
10872 true,
10873 ));
10874 } else {
10875 let prepare_start = Instant::now();
10876 let Ok(quad_run) = self.prepare_text_glyph_quads(
10877 run_key,
10878 atlas_generation,
10879 cached_glyph_run.as_deref(),
10880 &collected_run,
10881 &mut generated_quads,
10882 ) else {
10883 image_vertices.truncate(initial_vertex_len);
10884 image_indices.truncate(initial_index_len);
10885 glyph_cmds.truncate(initial_cmd_len);
10886 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
10887 self.scratch_text_glyph_run = collected_run;
10888 self.scratch_text_glyph_placements = collected_placements;
10889 self.scratch_text_glyph_quads = generated_quads;
10890 return Ok(false);
10891 };
10892 if let Some(collect_ms) = miss_collect_ms {
10893 if text_glyph_run_diag_enabled() {
10894 log::warn!(
10895 "[text-glyph-run-diag] visible=true glyphs={} cached={} new={} collect_ms={:.2} prepare_ms={:.2}",
10896 quad_run.len(),
10897 miss_cached_glyphs,
10898 miss_new_glyphs,
10899 collect_ms,
10900 instant_ms(prepare_start, Instant::now()),
10901 );
10902 }
10903 }
10904 emitted_glyphs = emitted_glyphs.saturating_add(self.append_text_glyph_quad_run(
10905 source_raster_rect,
10906 quad_run.as_ref(),
10907 source_draw.clip,
10908 viewport,
10909 root_scale,
10910 image_vertices,
10911 image_indices,
10912 false,
10913 ));
10914 }
10915 let index_count = image_indices.len() as u32 - index_start;
10916 if index_count > 0 {
10917 glyph_cmds.push(GlyphDrawCmd::shared(index_start, index_count, scissor));
10918 }
10919 }
10920
10921 self.scratch_text_glyph_run = collected_run;
10922 self.scratch_text_glyph_placements = collected_placements;
10923 self.scratch_text_glyph_quads = generated_quads;
10924 let append_end = Instant::now();
10925 if let Some(total_ms) = should_log_wgpu_render_stage(append_start, append_end) {
10926 log::warn!(
10927 "[wgpu-render-stage:text-glyph-atlas] total_ms={total_ms:.2} visited={} cmds={} glyphs={} prewarmed={} run_hits={} run_misses={}",
10928 visited,
10929 glyph_cmds.len().saturating_sub(initial_cmd_len),
10930 emitted_glyphs,
10931 prewarmed_glyphs,
10932 run_hits,
10933 run_misses,
10934 );
10935 }
10936 Ok(true)
10937 }
10938
10939 #[cfg(not(target_arch = "wasm32"))]
10940 fn text_glyph_prewarm_decision(
10941 &self,
10942 text_draw: &TextDraw,
10943 viewport: ViewportUniformParams,
10944 root_scale: f32,
10945 ) -> TextGlyphPrewarmDecision {
10946 let Some((logical_rect, _, clip, _, static_text_motion)) =
10947 self.text_raster_geometry(text_draw, root_scale)
10948 else {
10949 return TextGlyphPrewarmDecision::MissingGeometry;
10950 };
10951 if !static_text_motion {
10952 return TextGlyphPrewarmDecision::DynamicMotion;
10953 }
10954 if text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale) {
10955 return TextGlyphPrewarmDecision::Visible;
10956 }
10957 if text_draw_should_prewarm_in_viewport(logical_rect, clip, viewport, root_scale) {
10958 TextGlyphPrewarmDecision::Candidate
10959 } else {
10960 TextGlyphPrewarmDecision::OutsidePrewarmWindow
10961 }
10962 }
10963
10964 #[cfg(not(target_arch = "wasm32"))]
10965 #[allow(clippy::too_many_arguments)]
10966 fn prewarm_offscreen_text_glyph_draws_in_chunk(
10967 &mut self,
10968 ordered_items: &[(usize, SegmentDrawItem)],
10969 texts: &[TextDraw],
10970 chunk: &SegmentDrawChunkPlan,
10971 viewport: ViewportUniformParams,
10972 root_scale: f32,
10973 staged_uploads: &mut StagedBufferUploads,
10974 image_vertices: &mut Vec<Vertex>,
10975 image_indices: &mut Vec<u32>,
10976 glyph_cmds: &mut Vec<GlyphDrawCmd>,
10977 ) -> Result<(), String> {
10978 let prewarm_start = Instant::now();
10979 let diag_enabled = cranpose_core::env_flag!("CRANPOSE_TEXT_PREWARM_DIAG");
10980 let mut text_items = 0usize;
10981 let mut candidates = 0usize;
10982 let mut missing_geometry = 0usize;
10983 let mut dynamic_motion = 0usize;
10984 let mut visible = 0usize;
10985 let mut outside = 0usize;
10986 let mut already_prepared = 0usize;
10987 let mut admitted_candidates = 0usize;
10988 let mut skipped_unbounded = 0usize;
10989 let mut skipped_budget = 0usize;
10990 let initial_vertex_len = image_vertices.len();
10991 let initial_index_len = image_indices.len();
10992 let initial_cmd_len = glyph_cmds.len();
10993 let initial_staged_bytes_len = staged_uploads.bytes.len();
10994 let initial_staged_copies_len = staged_uploads.copies.len();
10995 'batches: for batch in chunk.iter() {
10996 let SegmentBatchPlan::Text { start, end } = batch else {
10997 continue;
10998 };
10999 for (_, item) in &ordered_items[start..end] {
11000 if offscreen_text_glyph_prewarm_budget_exhausted(prewarm_start, admitted_candidates)
11001 {
11002 skipped_budget = skipped_budget.saturating_add(1);
11003 break 'batches;
11004 }
11005 let SegmentDrawItem::Text(text_index) = item else {
11006 return Err(format!(
11007 "text prewarm batch contains non-text draw item: {item:?}"
11008 ));
11009 };
11010 let Some(text_draw) = texts.get(*text_index) else {
11011 continue;
11012 };
11013 text_items = text_items.saturating_add(1);
11014 match self.text_glyph_prewarm_decision(text_draw, viewport, root_scale) {
11015 TextGlyphPrewarmDecision::Candidate => {}
11016 TextGlyphPrewarmDecision::MissingGeometry => {
11017 missing_geometry = missing_geometry.saturating_add(1);
11018 continue;
11019 }
11020 TextGlyphPrewarmDecision::DynamicMotion => {
11021 dynamic_motion = dynamic_motion.saturating_add(1);
11022 continue;
11023 }
11024 TextGlyphPrewarmDecision::Visible => {
11025 visible = visible.saturating_add(1);
11026 continue;
11027 }
11028 TextGlyphPrewarmDecision::OutsidePrewarmWindow => {
11029 outside = outside.saturating_add(1);
11030 continue;
11031 }
11032 }
11033
11034 candidates = candidates.saturating_add(1);
11035 let Some((_, raster_rect, _, text_scale, static_text_motion)) =
11036 self.text_raster_geometry(text_draw, root_scale)
11037 else {
11038 missing_geometry = missing_geometry.saturating_add(1);
11039 continue;
11040 };
11041 let raster_source = text_glyph_raster_source(text_draw, raster_rect);
11042 let source_draw = raster_source.draw.as_ref();
11043 let run_key = Self::text_glyph_run_cache_key(
11044 source_draw,
11045 raster_source.raster_rect,
11046 text_scale,
11047 static_text_motion,
11048 );
11049 let atlas_generation = self.text_glyph_atlas.generation();
11050 let cached_glyphs = if let Some(cached) = self.text_glyph_run_cache.peek(&run_key) {
11051 if cached.atlas_generation == atlas_generation && cached.quads.is_some() {
11052 already_prepared = already_prepared.saturating_add(1);
11053 continue;
11054 }
11055 Some(cached.glyphs.len())
11056 } else {
11057 None
11058 };
11059 if !offscreen_text_glyph_prewarm_work_is_bounded(
11060 cached_glyphs,
11061 source_draw.text.text.len(),
11062 ) {
11063 skipped_unbounded = skipped_unbounded.saturating_add(1);
11064 continue;
11065 }
11066 admitted_candidates = admitted_candidates.saturating_add(1);
11067 self.append_text_glyph_draws(
11068 std::iter::once(text_draw),
11069 viewport,
11070 root_scale,
11071 true,
11072 staged_uploads,
11073 image_vertices,
11074 image_indices,
11075 glyph_cmds,
11076 )?;
11077 image_vertices.truncate(initial_vertex_len);
11078 image_indices.truncate(initial_index_len);
11079 glyph_cmds.truncate(initial_cmd_len);
11080 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
11081 }
11082 }
11083
11084 if diag_enabled && text_items > 0 {
11085 log::warn!(
11086 "[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}"
11087 );
11088 }
11089 if admitted_candidates > 0 {
11090 if let Some(total_ms) = should_log_wgpu_render_stage(prewarm_start, Instant::now()) {
11091 log::warn!(
11092 "[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}"
11093 );
11094 }
11095 }
11096 Ok(())
11097 }
11098
11099 fn prepare_text_glyph_draw_cmds<'a, I>(
11100 &mut self,
11101 layer_texts: I,
11102 viewport: ViewportUniformParams,
11103 root_scale: f32,
11104 staged_uploads: &mut StagedBufferUploads,
11105 ) -> Result<Option<PreparedGlyphBatch>, String>
11106 where
11107 I: IntoIterator<Item = &'a TextDraw>,
11108 {
11109 #[cfg(target_arch = "wasm32")]
11110 let _ = staged_uploads;
11111
11112 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
11113 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
11114 let mut glyph_cmds = std::mem::take(&mut self.scratch_glyph_cmds);
11115 image_vertices.clear();
11116 image_indices.clear();
11117 glyph_cmds.clear();
11118
11119 if !self.append_text_glyph_draws(
11120 layer_texts,
11121 viewport,
11122 root_scale,
11123 false,
11124 staged_uploads,
11125 &mut image_vertices,
11126 &mut image_indices,
11127 &mut glyph_cmds,
11128 )? {
11129 self.scratch_image_vertices = image_vertices;
11130 self.scratch_image_indices = image_indices;
11131 self.scratch_glyph_cmds = glyph_cmds;
11132 return Ok(None);
11133 }
11134
11135 #[cfg(not(target_arch = "wasm32"))]
11136 if !image_indices.is_empty() {
11137 self.stage_native_image_buffers(
11138 staged_uploads,
11139 viewport,
11140 &image_vertices,
11141 &image_indices,
11142 );
11143 }
11144
11145 #[cfg(target_arch = "wasm32")]
11146 let image_slot = if glyph_cmds.is_empty() {
11147 0
11148 } else {
11149 let slot = self.claim_wasm_image_batch();
11150 {
11151 let buffers = &mut self.wasm_image_batches[slot];
11152 buffers.ensure_capacity(&self.device, image_vertices.len(), image_indices.len());
11153 }
11154 let buffers = &self.wasm_image_batches[slot];
11155 self.write_wasm_buffer(
11156 &buffers.vertex_buffer,
11157 bytemuck::cast_slice(&image_vertices),
11158 );
11159 self.write_wasm_buffer(&buffers.index_buffer, bytemuck::cast_slice(&image_indices));
11160 slot
11161 };
11162
11163 #[cfg(target_arch = "wasm32")]
11164 let uniform_slot = if glyph_cmds.is_empty() {
11165 0
11166 } else {
11167 self.prepare_wasm_viewport_uniforms(viewport)
11168 };
11169
11170 self.scratch_image_vertices = image_vertices;
11171 self.scratch_image_indices = image_indices;
11172 Ok(Some(PreparedGlyphBatch {
11173 cmds: glyph_cmds,
11174 #[cfg(target_arch = "wasm32")]
11175 image_slot,
11176 #[cfg(target_arch = "wasm32")]
11177 uniform_slot,
11178 }))
11179 }
11180
11181 #[allow(clippy::too_many_arguments)]
11182 fn append_image_bitmap_draw_cmd(
11183 &mut self,
11184 image: &ImageBitmap,
11185 rect: Rect,
11186 clip: Option<Rect>,
11187 sampling: ImageSampling,
11188 viewport: ViewportUniformParams,
11189 root_scale: f32,
11190 image_vertices: &mut Vec<Vertex>,
11191 image_indices: &mut Vec<u32>,
11192 image_cmds: &mut Vec<ImageDrawCmd>,
11193 ) -> Result<(), String> {
11194 if rect.width <= 0.0 || rect.height <= 0.0 {
11195 return Ok(());
11196 }
11197
11198 self.ensure_image_cached(image)?;
11199
11200 let (device_quad, scissor_rect) =
11201 if sampling == ImageSampling::Nearest && root_scale.is_finite() && root_scale > 0.0 {
11202 let left_px = (rect.x * root_scale).round();
11203 let top_px = (rect.y * root_scale).round();
11204 let width_px = (rect.width * root_scale).round().max(1.0);
11205 let height_px = (rect.height * root_scale).round().max(1.0);
11206 let snapped_rect = Rect {
11207 x: left_px / root_scale,
11208 y: top_px / root_scale,
11209 width: width_px / root_scale,
11210 height: height_px / root_scale,
11211 };
11212 let right_px = left_px + width_px;
11213 let bottom_px = top_px + height_px;
11214 (
11215 [
11216 [left_px, top_px],
11217 [right_px, top_px],
11218 [left_px, bottom_px],
11219 [right_px, bottom_px],
11220 ],
11221 snapped_rect,
11222 )
11223 } else {
11224 (
11225 rect_to_quad(rect).map(|[x, y]| [x * root_scale, y * root_scale]),
11226 rect,
11227 )
11228 };
11229
11230 let Some(scissor) = scissor_rect_for_layer(
11231 scissor_rect,
11232 clip,
11233 root_scale,
11234 viewport.width,
11235 viewport.height,
11236 ) else {
11237 return Ok(());
11238 };
11239 let Some(uv_rect) = image_uv_rect(image, None) else {
11240 return Ok(());
11241 };
11242
11243 let base_vertex = image_vertices.len() as u32;
11244 let index_start = image_indices.len() as u32;
11245 image_indices.extend_from_slice(&[
11246 base_vertex,
11247 base_vertex + 1,
11248 base_vertex + 2,
11249 base_vertex + 2,
11250 base_vertex + 1,
11251 base_vertex + 3,
11252 ]);
11253 let color = [1.0, 1.0, 1.0, 1.0];
11254 image_vertices.extend_from_slice(&[
11255 Vertex {
11256 position: device_quad[0],
11257 color,
11258 uv: [uv_rect.min[0], uv_rect.min[1]],
11259 uv_bounds: uv_rect.sample_bounds,
11260 },
11261 Vertex {
11262 position: device_quad[1],
11263 color,
11264 uv: [uv_rect.max[0], uv_rect.min[1]],
11265 uv_bounds: uv_rect.sample_bounds,
11266 },
11267 Vertex {
11268 position: device_quad[2],
11269 color,
11270 uv: [uv_rect.min[0], uv_rect.max[1]],
11271 uv_bounds: uv_rect.sample_bounds,
11272 },
11273 Vertex {
11274 position: device_quad[3],
11275 color,
11276 uv: [uv_rect.max[0], uv_rect.max[1]],
11277 uv_bounds: uv_rect.sample_bounds,
11278 },
11279 ]);
11280 image_cmds.push(ImageDrawCmd {
11281 index_start,
11282 scissor,
11283 image_id: image.id(),
11284 sampling,
11285 });
11286 Ok(())
11287 }
11288
11289 #[allow(clippy::too_many_arguments)]
11290 fn append_text_image_draw_cmds<'a, I>(
11291 &mut self,
11292 layer_texts: I,
11293 viewport: ViewportUniformParams,
11294 root_scale: f32,
11295 image_vertices: &mut Vec<Vertex>,
11296 image_indices: &mut Vec<u32>,
11297 image_cmds: &mut Vec<ImageDrawCmd>,
11298 ) -> Result<(), String>
11299 where
11300 I: Iterator<Item = &'a TextDraw>,
11301 {
11302 let append_start = Instant::now();
11303 let initial_len = image_cmds.len();
11304 let mut visited = 0usize;
11305 let mut hit_count = 0usize;
11306 let mut miss_count = 0usize;
11307 for text_draw in layer_texts {
11308 visited = visited.saturating_add(1);
11309 let _ = text_draw.node_id;
11310 let Some((logical_rect, raster_rect, clip, text_scale, static_text_motion)) =
11311 self.text_raster_geometry(text_draw, root_scale)
11312 else {
11313 continue;
11314 };
11315 if !text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale) {
11316 continue;
11317 }
11318
11319 let raster_source = self.text_image_raster_source(
11320 text_draw,
11321 logical_rect,
11322 raster_rect,
11323 clip,
11324 root_scale,
11325 static_text_motion,
11326 );
11327 let source_draw = raster_source.draw.as_ref();
11328 let source_raster_rect = raster_source.raster_rect;
11329
11330 let cache_key = Self::text_image_cache_key(
11331 source_draw,
11332 source_raster_rect,
11333 text_scale,
11334 static_text_motion,
11335 );
11336 let image = if let Some(cached) = self.text_image_cache.get(&cache_key) {
11337 self.frame_stats
11338 .record_text_image_cache_hit(cached.image.width(), cached.image.height());
11339 hit_count = hit_count.saturating_add(1);
11340 cached.image.clone()
11341 } else {
11342 let Some(image) =
11343 self.rasterize_text_draw_to_image(source_draw, source_raster_rect, text_scale)
11344 else {
11345 continue;
11346 };
11347 self.frame_stats
11348 .record_text_image_cache_miss(image.width(), image.height());
11349 miss_count = miss_count.saturating_add(1);
11350 self.text_image_cache.put(
11351 cache_key,
11352 CachedTextImage {
11353 image: image.clone(),
11354 },
11355 );
11356 image
11357 };
11358
11359 let draw_origin = if static_text_motion {
11360 Point::new(
11361 source_raster_rect.x / root_scale,
11362 source_raster_rect.y / root_scale,
11363 )
11364 } else {
11365 Point::new(logical_rect.x, logical_rect.y)
11366 };
11367 let draw_rect = Rect {
11368 x: draw_origin.x,
11369 y: draw_origin.y,
11370 width: image.width() as f32 / root_scale,
11371 height: image.height() as f32 / root_scale,
11372 };
11373 self.append_image_bitmap_draw_cmd(
11374 &image,
11375 draw_rect,
11376 clip,
11377 ImageSampling::Nearest,
11378 viewport,
11379 root_scale,
11380 image_vertices,
11381 image_indices,
11382 image_cmds,
11383 )?;
11384 }
11385 let append_end = Instant::now();
11386 if let Some(total_ms) = should_log_wgpu_render_stage(append_start, append_end) {
11387 log::warn!(
11388 "[wgpu-render-stage:text-images] total_ms={total_ms:.2} visited={} emitted={} hits={} misses={}",
11389 visited,
11390 image_cmds.len().saturating_sub(initial_len),
11391 hit_count,
11392 miss_count,
11393 );
11394 }
11395 Ok(())
11396 }
11397
11398 fn text_image_raster_source<'a>(
11399 &mut self,
11400 text_draw: &'a TextDraw,
11401 logical_rect: Rect,
11402 raster_rect: Rect,
11403 clip: Option<Rect>,
11404 root_scale: f32,
11405 static_text_motion: bool,
11406 ) -> TextRasterSource<'a> {
11407 let Some(clip) = clip else {
11408 return TextRasterSource {
11409 draw: Cow::Borrowed(text_draw),
11410 raster_rect,
11411 };
11412 };
11413 if !static_text_motion || text_draw.text.text.as_str().find('\n').is_none() {
11414 return TextRasterSource {
11415 draw: Cow::Borrowed(text_draw),
11416 raster_rect,
11417 };
11418 }
11419
11420 let line_starts = self.text_line_index_cache.line_starts(&text_draw.text);
11421 clipped_text_raster_source_with_line_starts(
11422 text_draw,
11423 logical_rect,
11424 raster_rect,
11425 clip,
11426 root_scale,
11427 line_starts.as_ref(),
11428 )
11429 }
11430
11431 fn prepare_text_image_draw_cmds<'a, I>(
11432 &mut self,
11433 layer_texts: I,
11434 viewport: ViewportUniformParams,
11435 root_scale: f32,
11436 staged_uploads: &mut StagedBufferUploads,
11437 ) -> Result<PreparedImageBatch, String>
11438 where
11439 I: Iterator<Item = &'a TextDraw>,
11440 {
11441 #[cfg(target_arch = "wasm32")]
11442 let _ = staged_uploads;
11443
11444 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
11445 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
11446 let mut image_cmds = std::mem::take(&mut self.scratch_image_cmds);
11447 image_vertices.clear();
11448 image_indices.clear();
11449 image_cmds.clear();
11450
11451 self.append_text_image_draw_cmds(
11452 layer_texts,
11453 viewport,
11454 root_scale,
11455 &mut image_vertices,
11456 &mut image_indices,
11457 &mut image_cmds,
11458 )?;
11459
11460 #[cfg(not(target_arch = "wasm32"))]
11461 if !image_cmds.is_empty() {
11462 self.stage_native_image_buffers(
11463 staged_uploads,
11464 viewport,
11465 &image_vertices,
11466 &image_indices,
11467 );
11468 }
11469
11470 #[cfg(target_arch = "wasm32")]
11471 let image_slot = if image_cmds.is_empty() {
11472 0
11473 } else {
11474 let slot = self.claim_wasm_image_batch();
11475 {
11476 let buffers = &mut self.wasm_image_batches[slot];
11477 buffers.ensure_capacity(&self.device, image_vertices.len(), image_indices.len());
11478 }
11479 let buffers = &self.wasm_image_batches[slot];
11480 self.write_wasm_buffer(
11481 &buffers.vertex_buffer,
11482 bytemuck::cast_slice(&image_vertices),
11483 );
11484 self.write_wasm_buffer(&buffers.index_buffer, bytemuck::cast_slice(&image_indices));
11485 slot
11486 };
11487
11488 #[cfg(target_arch = "wasm32")]
11489 let uniform_slot = if image_cmds.is_empty() {
11490 0
11491 } else {
11492 self.prepare_wasm_viewport_uniforms(viewport)
11493 };
11494
11495 self.scratch_image_vertices = image_vertices;
11496 self.scratch_image_indices = image_indices;
11497 Ok(PreparedImageBatch {
11498 cmds: image_cmds,
11499 #[cfg(target_arch = "wasm32")]
11500 image_slot,
11501 #[cfg(target_arch = "wasm32")]
11502 uniform_slot,
11503 })
11504 }
11505
11506 fn text_raster_geometry(
11507 &self,
11508 text_draw: &TextDraw,
11509 root_scale: f32,
11510 ) -> Option<(Rect, Rect, Option<Rect>, f32, bool)> {
11511 text_raster_geometry_for_draw(text_draw, root_scale)
11512 }
11513
11514 fn text_image_cache_key(
11515 text_draw: &TextDraw,
11516 raster_rect: Rect,
11517 text_scale: f32,
11518 static_text_motion: bool,
11519 ) -> TextImageCacheKey {
11520 let mut state = default_hash::new();
11521 text_draw.text.render_hash().hash(&mut state);
11522 text_draw.text_style.render_hash().hash(&mut state);
11523 text_draw.color.render_hash().hash(&mut state);
11524 hash_text_raster_geometry_for_cache(raster_rect, static_text_motion, &mut state);
11525 text_draw.font_size.to_bits().hash(&mut state);
11526 text_scale.to_bits().hash(&mut state);
11527 text_draw.layout_options.hash(&mut state);
11528 TextImageCacheKey(state.finish())
11529 }
11530
11531 fn text_glyph_run_cache_key(
11532 text_draw: &TextDraw,
11533 raster_rect: Rect,
11534 text_scale: f32,
11535 static_text_motion: bool,
11536 ) -> TextGlyphRunCacheKey {
11537 TextGlyphRunCacheKey(
11538 Self::text_image_cache_key(text_draw, raster_rect, text_scale, static_text_motion).0,
11539 )
11540 }
11541
11542 fn rasterize_text_draw_to_image(
11543 &mut self,
11544 text_draw: &TextDraw,
11545 raster_rect: Rect,
11546 text_scale: f32,
11547 ) -> Option<ImageBitmap> {
11548 if text_draw.text.span_styles.is_empty() {
11549 let font = self.text_fonts.resolve(&text_draw.text_style)?;
11550 return rasterize_text_to_image_with_glyph_cache(
11551 text_draw.text.text.as_str(),
11552 raster_rect,
11553 &text_draw.text_style,
11554 text_draw.color,
11555 text_draw.font_size,
11556 text_scale,
11557 font,
11558 &mut self.text_glyph_mask_cache,
11559 );
11560 }
11561
11562 if let Some(image) = rasterize_annotated_text_to_image_with_glyph_cache(
11563 text_draw.text.as_ref(),
11564 raster_rect,
11565 &text_draw.text_style,
11566 text_draw.color,
11567 text_draw.font_size,
11568 text_scale,
11569 &self.text_fonts,
11570 &mut self.text_glyph_mask_cache,
11571 ) {
11572 return Some(image);
11573 }
11574
11575 rasterize_spanned_text_to_image(
11576 text_draw,
11577 raster_rect,
11578 text_scale,
11579 &self.text_fonts,
11580 &mut self.text_glyph_mask_cache,
11581 )
11582 }
11583}
11584
11585fn rasterize_spanned_text_to_image(
11586 text_draw: &TextDraw,
11587 raster_rect: Rect,
11588 text_scale: f32,
11589 fonts: &SoftwareTextFontSet,
11590 glyph_cache: &mut SoftwareGlyphRasterCache,
11591) -> Option<ImageBitmap> {
11592 let width = raster_rect.width.ceil().max(1.0) as u32;
11593 let height = raster_rect.height.ceil().max(1.0) as u32;
11594 let mut canvas = vec![0_u8; (width as usize) * (height as usize) * 4];
11595 let boundaries = text_draw.text.span_boundaries();
11596 let base_line_height = text_draw
11597 .text_style
11598 .resolve_line_height(14.0, text_draw.font_size)
11599 .max(1.0);
11600 let mut current_line_height = base_line_height;
11601 let mut cursor_x = raster_rect.x;
11602 let mut cursor_y = raster_rect.y;
11603
11604 for window in boundaries.windows(2) {
11605 let start = window[0];
11606 let end = window[1];
11607 if start == end {
11608 continue;
11609 }
11610
11611 let chunk = &text_draw.text.text[start..end];
11612 let mut merged_span = text_draw.text_style.span_style.clone();
11613 for span in &text_draw.text.span_styles {
11614 if span.range.start <= start && span.range.end >= end {
11615 merged_span = merged_span.merge(&span.item);
11616 }
11617 }
11618
11619 let mut chunk_style = text_draw.text_style.clone();
11620 chunk_style.span_style = merged_span;
11621
11622 for part in chunk.split_inclusive('\n') {
11623 let has_newline = part.ends_with('\n');
11624 let content = if has_newline {
11625 &part[..part.len().saturating_sub(1)]
11626 } else {
11627 part
11628 };
11629
11630 if !content.is_empty() {
11631 let chunk_font_size = chunk_style.resolve_font_size(text_draw.font_size);
11632 let Some(font) = fonts.resolve(&chunk_style) else {
11633 continue;
11634 };
11635 let metrics = measure_text_with_font(content, &chunk_style, chunk_font_size, font);
11636 let segment_rect = Rect {
11637 x: cursor_x,
11638 y: cursor_y,
11639 width: (metrics.width * text_scale).ceil().max(1.0),
11640 height: (metrics.height * text_scale).ceil().max(1.0),
11641 };
11642 if let Some(segment_image) = rasterize_text_to_image_with_glyph_cache(
11643 content,
11644 segment_rect,
11645 &chunk_style,
11646 chunk_style.resolve_text_color(text_draw.color),
11647 chunk_font_size,
11648 text_scale,
11649 font,
11650 glyph_cache,
11651 ) {
11652 composite_text_segment(
11653 &mut canvas,
11654 width,
11655 height,
11656 raster_rect,
11657 segment_rect,
11658 &segment_image,
11659 );
11660 }
11661 cursor_x += metrics.width * text_scale;
11662 current_line_height = current_line_height.max(metrics.line_height.max(1.0));
11663 }
11664
11665 if has_newline {
11666 cursor_x = raster_rect.x;
11667 cursor_y += current_line_height * text_scale;
11668 current_line_height = base_line_height;
11669 }
11670 }
11671 }
11672
11673 ImageBitmap::from_rgba8(width, height, canvas).ok()
11674}
11675
11676struct TextRasterSource<'a> {
11677 draw: Cow<'a, TextDraw>,
11678 raster_rect: Rect,
11679}
11680
11681fn text_glyph_raster_source(text_draw: &TextDraw, raster_rect: Rect) -> TextRasterSource<'_> {
11682 TextRasterSource {
11683 draw: Cow::Borrowed(text_draw),
11684 raster_rect,
11685 }
11686}
11687
11688#[cfg(test)]
11689fn clipped_text_raster_source<'a>(
11690 text_draw: &'a TextDraw,
11691 logical_rect: Rect,
11692 raster_rect: Rect,
11693 clip: Option<Rect>,
11694 root_scale: f32,
11695 static_text_motion: bool,
11696) -> TextRasterSource<'a> {
11697 let Some(clip) = clip else {
11698 return TextRasterSource {
11699 draw: Cow::Borrowed(text_draw),
11700 raster_rect,
11701 };
11702 };
11703 if !static_text_motion || text_draw.text.text.as_str().find('\n').is_none() {
11704 return TextRasterSource {
11705 draw: Cow::Borrowed(text_draw),
11706 raster_rect,
11707 };
11708 }
11709 let line_starts = line_start_offsets(text_draw.text.text.as_str());
11710 clipped_text_raster_source_with_line_starts(
11711 text_draw,
11712 logical_rect,
11713 raster_rect,
11714 clip,
11715 root_scale,
11716 &line_starts,
11717 )
11718}
11719
11720fn clipped_text_raster_source_with_line_starts<'a>(
11721 text_draw: &'a TextDraw,
11722 logical_rect: Rect,
11723 raster_rect: Rect,
11724 clip: Rect,
11725 root_scale: f32,
11726 line_starts: &[usize],
11727) -> TextRasterSource<'a> {
11728 if line_starts.len() < MIN_MULTILINE_TEXT_LINES_FOR_CLIPPED_RASTER {
11729 return TextRasterSource {
11730 draw: Cow::Borrowed(text_draw),
11731 raster_rect,
11732 };
11733 }
11734
11735 let Some(visible_rect) = logical_rect.intersect(clip) else {
11736 return TextRasterSource {
11737 draw: Cow::Borrowed(text_draw),
11738 raster_rect,
11739 };
11740 };
11741
11742 let line_count = line_starts.len().max(1);
11743 let line_height = logical_rect.height / line_count as f32;
11744 if !line_height.is_finite() || line_height <= 0.0 {
11745 return TextRasterSource {
11746 draw: Cow::Borrowed(text_draw),
11747 raster_rect,
11748 };
11749 }
11750
11751 let visible_top = ((visible_rect.y - logical_rect.y) / line_height).floor() as isize;
11752 let visible_bottom =
11753 ((visible_rect.y + visible_rect.height - logical_rect.y) / line_height).ceil() as isize;
11754 let start_line = visible_top.saturating_sub(1).max(0) as usize;
11755 let end_line = (visible_bottom + 1).max(start_line as isize + 1) as usize;
11756 let end_line = end_line.min(line_count);
11757 if start_line == 0 && end_line >= line_count {
11758 return TextRasterSource {
11759 draw: Cow::Borrowed(text_draw),
11760 raster_rect,
11761 };
11762 }
11763
11764 let byte_start = line_starts[start_line];
11765 let byte_end = line_end_offset(text_draw.text.text.as_str(), line_starts, end_line - 1);
11766 if byte_start >= byte_end {
11767 return TextRasterSource {
11768 draw: Cow::Borrowed(text_draw),
11769 raster_rect,
11770 };
11771 }
11772
11773 let slice_y = logical_rect.y + start_line as f32 * line_height;
11774 let slice_height = (end_line - start_line) as f32 * line_height;
11775 let mut slice_raster_rect = Rect {
11776 x: logical_rect.x * root_scale,
11777 y: slice_y * root_scale,
11778 width: logical_rect.width * root_scale,
11779 height: slice_height * root_scale,
11780 };
11781 slice_raster_rect.x = slice_raster_rect.x.round();
11782 slice_raster_rect.y = slice_raster_rect.y.round();
11783 slice_raster_rect.width = slice_raster_rect.width.ceil().max(1.0);
11784 slice_raster_rect.height = slice_raster_rect.height.ceil().max(1.0);
11785
11786 let mut sliced_draw = text_draw.clone();
11787 sliced_draw.rect = Rect {
11788 x: logical_rect.x,
11789 y: slice_y,
11790 width: logical_rect.width,
11791 height: slice_height,
11792 };
11793 sliced_draw.text = Arc::new(text_draw.text.subsequence(byte_start..byte_end));
11794
11795 TextRasterSource {
11796 draw: Cow::Owned(sliced_draw),
11797 raster_rect: slice_raster_rect,
11798 }
11799}
11800
11801fn line_start_offsets(text: &str) -> Vec<usize> {
11802 let mut starts =
11803 Vec::with_capacity(text.as_bytes().iter().filter(|b| **b == b'\n').count() + 1);
11804 starts.push(0);
11805 starts.extend(
11806 text.char_indices()
11807 .filter_map(|(index, ch)| (ch == '\n').then_some(index + ch.len_utf8())),
11808 );
11809 starts
11810}
11811
11812fn line_end_offset(text: &str, line_starts: &[usize], line: usize) -> usize {
11813 line_starts.get(line + 1).copied().unwrap_or(text.len())
11814}
11815
11816fn composite_text_segment(
11817 canvas: &mut [u8],
11818 canvas_width: u32,
11819 canvas_height: u32,
11820 canvas_rect: Rect,
11821 segment_rect: Rect,
11822 segment_image: &ImageBitmap,
11823) {
11824 let offset_x = (segment_rect.x - canvas_rect.x).round() as i32;
11825 let offset_y = (segment_rect.y - canvas_rect.y).round() as i32;
11826 let src = segment_image.pixels();
11827 for sy in 0..segment_image.height() as i32 {
11828 let dy = offset_y + sy;
11829 if dy < 0 || dy >= canvas_height as i32 {
11830 continue;
11831 }
11832 for sx in 0..segment_image.width() as i32 {
11833 let dx = offset_x + sx;
11834 if dx < 0 || dx >= canvas_width as i32 {
11835 continue;
11836 }
11837 let src_index = ((sy as u32 * segment_image.width() + sx as u32) * 4) as usize;
11838 let dst_index = ((dy as u32 * canvas_width + dx as u32) * 4) as usize;
11839 blend_rgba_pixel(
11840 &mut canvas[dst_index..dst_index + 4],
11841 &src[src_index..src_index + 4],
11842 );
11843 }
11844 }
11845}
11846
11847fn blend_rgba_pixel(dst: &mut [u8], src: &[u8]) {
11848 let src_alpha = src[3] as f32 / 255.0;
11849 if src_alpha <= 0.0 {
11850 return;
11851 }
11852 let dst_alpha = dst[3] as f32 / 255.0;
11853 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
11854 if out_alpha <= f32::EPSILON {
11855 dst.copy_from_slice(&[0, 0, 0, 0]);
11856 return;
11857 }
11858
11859 for channel in 0..3 {
11860 let src_channel = src[channel] as f32 / 255.0;
11861 let dst_channel = dst[channel] as f32 / 255.0;
11862 let src_premult = src_channel * src_alpha;
11863 let dst_premult = dst_channel * dst_alpha;
11864 dst[channel] =
11865 (((src_premult + dst_premult * (1.0 - src_alpha)) / out_alpha).clamp(0.0, 1.0) * 255.0)
11866 .round() as u8;
11867 }
11868 dst[3] = (out_alpha.clamp(0.0, 1.0) * 255.0).round() as u8;
11869}
11870
11871fn align_to(value: u32, alignment: u32) -> u32 {
11872 debug_assert!(alignment > 0);
11873 value.div_ceil(alignment) * alignment
11874}
11875
11876#[cfg(not(target_arch = "wasm32"))]
11877fn align_usize_to(value: usize, alignment: usize) -> usize {
11878 debug_assert!(alignment > 0);
11879 value.div_ceil(alignment) * alignment
11880}
11881
11882impl GpuRenderer {
11883 fn convert_surface_pixels_to_rgba(&self, pixels: &mut [u8]) -> Result<(), String> {
11884 match self.surface_format {
11885 wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Rgba8UnormSrgb => Ok(()),
11886 wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb => {
11887 for pixel in pixels.chunks_exact_mut(4) {
11888 pixel.swap(0, 2);
11889 }
11890 Ok(())
11891 }
11892 format => Err(format!(
11893 "Screenshot readback unsupported for texture format: {format:?}"
11894 )),
11895 }
11896 }
11897}
11898
11899fn is_in_effect_range(z_index: usize, effect_z_ranges: &[Range<usize>]) -> bool {
11900 effect_z_ranges.iter().any(|range| range.contains(&z_index))
11901}
11902
11903#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11904enum SegmentDrawItem {
11905 Shape(usize),
11906 Image(usize),
11907 Text(usize),
11908 Shadow(usize),
11909 Composite(usize),
11910 ShaderComposite(usize),
11911 Retained(usize),
11912}
11913
11914#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11915enum SegmentBatchPlan {
11916 Shape {
11917 start: usize,
11918 end: usize,
11919 blend_mode: BlendMode,
11920 },
11921 Image {
11922 start: usize,
11923 end: usize,
11924 blend_mode: BlendMode,
11925 },
11926 Text {
11927 start: usize,
11928 end: usize,
11929 },
11930 Composite {
11931 start: usize,
11932 end: usize,
11933 },
11934 ShaderComposite {
11935 start: usize,
11936 end: usize,
11937 },
11938 Retained {
11941 start: usize,
11942 end: usize,
11943 },
11944}
11945
11946#[derive(Clone, Debug, Default, PartialEq, Eq)]
11947struct SegmentDrawChunkPlan {
11948 batches: Vec<SegmentBatchPlan>,
11949}
11950
11951struct SegmentRenderOutcome {
11952 rendered_any: bool,
11953 pass_count: u32,
11954}
11955
11956struct SegmentCommandEncodeOutcome {
11957 first_batch: bool,
11958}
11959
11960#[cfg(not(target_arch = "wasm32"))]
11961#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11962enum TextGlyphPrewarmDecision {
11963 Candidate,
11964 MissingGeometry,
11965 DynamicMotion,
11966 Visible,
11967 OutsidePrewarmWindow,
11968}
11969
11970#[cfg(not(target_arch = "wasm32"))]
11971#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11972struct NativeSegmentFusionBudget {
11973 shape_count: usize,
11974 gradient_stop_count: usize,
11975}
11976
11977#[cfg(not(target_arch = "wasm32"))]
11978#[derive(Clone, Debug, PartialEq, Eq)]
11979struct NativeSegmentFusionPartition {
11980 chunk: SegmentDrawChunkPlan,
11981 budget: NativeSegmentFusionBudget,
11982}
11983
11984#[cfg(not(target_arch = "wasm32"))]
11985#[derive(Clone, Debug, PartialEq, Eq)]
11986enum FusedSegmentBatch {
11987 Shape {
11988 batch: PreparedShapeBatch,
11989 blend_mode: BlendMode,
11990 },
11991 Image {
11992 cmd_range: Range<usize>,
11993 blend_mode: BlendMode,
11994 },
11995 Text {
11996 image_cmd_range: Range<usize>,
11997 glyph_cmd_range: Range<usize>,
11998 },
11999 Composite {
12000 draw_range: Range<usize>,
12001 },
12002 ShaderComposite {
12003 draw_range: Range<usize>,
12004 },
12005 Retained {
12006 item_range: Range<usize>,
12007 },
12008}
12009
12010struct ShadowSourceRenderOutcome {
12011 rendered_any: bool,
12012 pass_count: u32,
12013}
12014
12015impl SegmentDrawChunkPlan {
12016 fn is_empty(&self) -> bool {
12017 self.batches.is_empty()
12018 }
12019
12020 fn push(&mut self, batch: SegmentBatchPlan) {
12021 self.batches.push(batch);
12022 }
12023
12024 fn iter(&self) -> impl Iterator<Item = SegmentBatchPlan> + '_ {
12025 self.batches.iter().copied()
12026 }
12027}
12028
12029#[derive(Clone, Debug, PartialEq, Eq)]
12030enum SegmentRenderCommand {
12031 DrawChunk(SegmentDrawChunkPlan),
12032 Shadow(usize),
12033}
12034
12035struct SegmentCommandIter<'a> {
12036 ordered_items: &'a [(usize, SegmentDrawItem)],
12037 shapes: &'a [DrawShape],
12038 images: &'a [ImageDraw],
12039 cursor: usize,
12040 batch_limits: ShapeBatchLimits,
12041}
12042
12043impl<'a> SegmentCommandIter<'a> {
12044 fn new(
12045 ordered_items: &'a [(usize, SegmentDrawItem)],
12046 shapes: &'a [DrawShape],
12047 images: &'a [ImageDraw],
12048 batch_limits: ShapeBatchLimits,
12049 ) -> Self {
12050 Self {
12051 ordered_items,
12052 shapes,
12053 images,
12054 cursor: 0,
12055 batch_limits,
12056 }
12057 }
12058}
12059
12060impl Iterator for SegmentCommandIter<'_> {
12061 type Item = SegmentRenderCommand;
12062
12063 fn next(&mut self) -> Option<Self::Item> {
12064 if self.cursor >= self.ordered_items.len() {
12065 return None;
12066 }
12067
12068 if let SegmentDrawItem::Shadow(index) = self.ordered_items[self.cursor].1 {
12069 self.cursor += 1;
12070 return Some(SegmentRenderCommand::Shadow(index));
12071 }
12072
12073 let mut chunk = SegmentDrawChunkPlan::default();
12074 while self.cursor < self.ordered_items.len() {
12075 if let SegmentDrawItem::Shadow(index) = self.ordered_items[self.cursor].1 {
12076 if chunk.is_empty() {
12077 self.cursor += 1;
12078 return Some(SegmentRenderCommand::Shadow(index));
12079 }
12080 break;
12081 }
12082
12083 let Some((batch, next_cursor)) = segment_batch_plan_at_cursor(
12084 self.ordered_items,
12085 self.shapes,
12086 self.images,
12087 self.cursor,
12088 self.batch_limits,
12089 ) else {
12090 break;
12091 };
12092 chunk.push(batch);
12093 self.cursor = next_cursor;
12094 }
12095
12096 Some(SegmentRenderCommand::DrawChunk(chunk))
12097 }
12098}
12099
12100#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12101struct PreparedShapeBatch {
12102 vertex_start: u32,
12105 vertex_count: u32,
12106 #[cfg(target_arch = "wasm32")]
12107 shape_slot: usize,
12108 #[cfg(target_arch = "wasm32")]
12109 uniform_slot: usize,
12110}
12111
12112struct PreparedImageBatch {
12113 cmds: Vec<ImageDrawCmd>,
12114 #[cfg(target_arch = "wasm32")]
12115 image_slot: usize,
12116 #[cfg(target_arch = "wasm32")]
12117 uniform_slot: usize,
12118}
12119
12120impl PreparedImageBatch {
12121 fn is_empty(&self) -> bool {
12122 self.cmds.is_empty()
12123 }
12124
12125 fn into_cmds(self) -> Vec<ImageDrawCmd> {
12126 self.cmds
12127 }
12128}
12129
12130struct PreparedGlyphBatch {
12131 cmds: Vec<GlyphDrawCmd>,
12132 #[cfg(target_arch = "wasm32")]
12133 image_slot: usize,
12134 #[cfg(target_arch = "wasm32")]
12135 uniform_slot: usize,
12136}
12137
12138impl PreparedGlyphBatch {
12139 fn is_empty(&self) -> bool {
12140 self.cmds.is_empty()
12141 }
12142
12143 fn into_cmds(self) -> Vec<GlyphDrawCmd> {
12144 self.cmds
12145 }
12146}
12147
12148#[cfg(not(target_arch = "wasm32"))]
12149fn gradient_stop_count_for_shape(shape: &DrawShape) -> usize {
12150 match &shape.brush {
12151 Brush::Solid(_) => 0,
12152 Brush::LinearGradient { colors, .. }
12153 | Brush::RadialGradient { colors, .. }
12154 | Brush::SweepGradient { colors, .. } => colors.len(),
12155 }
12156}
12157
12158#[cfg(not(target_arch = "wasm32"))]
12159fn native_segment_fusion_budget(
12160 ordered_items: &[(usize, SegmentDrawItem)],
12161 shapes: &[DrawShape],
12162 chunk: &SegmentDrawChunkPlan,
12163 batch_limits: ShapeBatchLimits,
12164) -> Result<Option<NativeSegmentFusionBudget>, String> {
12165 let mut shape_count = 0usize;
12166 let mut gradient_stop_count = 0usize;
12167
12168 for batch in chunk.iter() {
12169 let SegmentBatchPlan::Shape { start, end, .. } = batch else {
12170 continue;
12171 };
12172 for (_, item) in &ordered_items[start..end] {
12173 let SegmentDrawItem::Shape(shape_index) = item else {
12174 return Err(format!(
12175 "shape batch contains non-shape draw item: {item:?}"
12176 ));
12177 };
12178 let shape = &shapes[*shape_index];
12179 shape_count = shape_count.saturating_add(1);
12180 gradient_stop_count =
12181 gradient_stop_count.saturating_add(gradient_stop_count_for_shape(shape));
12182 }
12183 }
12184
12185 if shape_count > batch_limits.max_shapes_per_batch
12186 || gradient_stop_count > batch_limits.max_gradient_stops
12187 {
12188 return Ok(None);
12189 }
12190
12191 Ok(Some(NativeSegmentFusionBudget {
12192 shape_count,
12193 gradient_stop_count,
12194 }))
12195}
12196
12197#[cfg(not(target_arch = "wasm32"))]
12198fn push_native_segment_fusion_partition(
12199 partitions: &mut Vec<NativeSegmentFusionPartition>,
12200 current: &mut SegmentDrawChunkPlan,
12201 current_budget: &mut NativeSegmentFusionBudget,
12202) {
12203 if current.is_empty() {
12204 return;
12205 }
12206
12207 partitions.push(NativeSegmentFusionPartition {
12208 chunk: std::mem::take(current),
12209 budget: *current_budget,
12210 });
12211 *current_budget = NativeSegmentFusionBudget {
12212 shape_count: 0,
12213 gradient_stop_count: 0,
12214 };
12215}
12216
12217#[cfg(not(target_arch = "wasm32"))]
12218fn native_segment_fusion_partitions(
12219 ordered_items: &[(usize, SegmentDrawItem)],
12220 shapes: &[DrawShape],
12221 chunk: &SegmentDrawChunkPlan,
12222 batch_limits: ShapeBatchLimits,
12223) -> Result<Option<Vec<NativeSegmentFusionPartition>>, String> {
12224 if let Some(budget) = native_segment_fusion_budget(ordered_items, shapes, chunk, batch_limits)?
12225 {
12226 return Ok(Some(vec![NativeSegmentFusionPartition {
12227 chunk: chunk.clone(),
12228 budget,
12229 }]));
12230 }
12231
12232 let mut partitions = Vec::new();
12233 let mut current = SegmentDrawChunkPlan::default();
12234 let mut current_budget = NativeSegmentFusionBudget {
12235 shape_count: 0,
12236 gradient_stop_count: 0,
12237 };
12238
12239 for batch in chunk.iter() {
12240 let SegmentBatchPlan::Shape {
12241 start,
12242 end,
12243 blend_mode,
12244 } = batch
12245 else {
12246 current.push(batch);
12247 continue;
12248 };
12249
12250 let mut run_start = start;
12251 for (item_cursor, (_, item)) in ordered_items.iter().enumerate().take(end).skip(start) {
12252 let SegmentDrawItem::Shape(shape_index) = *item else {
12253 return Err(format!(
12254 "shape batch contains non-shape draw item: {:?}",
12255 item
12256 ));
12257 };
12258 let gradient_stop_count = gradient_stop_count_for_shape(&shapes[shape_index]);
12259 if gradient_stop_count > batch_limits.max_gradient_stops {
12260 return Ok(None);
12261 }
12262
12263 let fits_shape_count =
12264 current_budget.shape_count.saturating_add(1) <= batch_limits.max_shapes_per_batch;
12265 let fits_gradient_count = current_budget
12266 .gradient_stop_count
12267 .saturating_add(gradient_stop_count)
12268 <= batch_limits.max_gradient_stops;
12269 if !fits_shape_count || !fits_gradient_count {
12270 if run_start < item_cursor {
12271 current.push(SegmentBatchPlan::Shape {
12272 start: run_start,
12273 end: item_cursor,
12274 blend_mode,
12275 });
12276 }
12277 push_native_segment_fusion_partition(
12278 &mut partitions,
12279 &mut current,
12280 &mut current_budget,
12281 );
12282 run_start = item_cursor;
12283 }
12284
12285 current_budget.shape_count = current_budget.shape_count.saturating_add(1);
12286 current_budget.gradient_stop_count = current_budget
12287 .gradient_stop_count
12288 .saturating_add(gradient_stop_count);
12289 }
12290
12291 if run_start < end {
12292 current.push(SegmentBatchPlan::Shape {
12293 start: run_start,
12294 end,
12295 blend_mode,
12296 });
12297 }
12298 }
12299
12300 push_native_segment_fusion_partition(&mut partitions, &mut current, &mut current_budget);
12301 Ok(Some(partitions))
12302}
12303
12304fn segment_batch_plan_at_cursor(
12305 ordered_items: &[(usize, SegmentDrawItem)],
12306 shapes: &[DrawShape],
12307 images: &[ImageDraw],
12308 start: usize,
12309 batch_limits: ShapeBatchLimits,
12310) -> Option<(SegmentBatchPlan, usize)> {
12311 match ordered_items[start].1 {
12312 SegmentDrawItem::Shape(index) => {
12313 let blend_mode = supported_blend_mode(shapes[index].blend_mode);
12314 let mut end = start + 1;
12315 let shape_limit = (start + batch_limits.max_shapes_per_batch).min(ordered_items.len());
12316 while end < shape_limit {
12317 match ordered_items[end].1 {
12318 SegmentDrawItem::Shape(next_index)
12319 if supported_blend_mode(shapes[next_index].blend_mode) == blend_mode =>
12320 {
12321 end += 1;
12322 }
12323 _ => break,
12324 }
12325 }
12326 Some((
12327 SegmentBatchPlan::Shape {
12328 start,
12329 end,
12330 blend_mode,
12331 },
12332 end,
12333 ))
12334 }
12335 SegmentDrawItem::Image(index) => {
12336 let blend_mode = supported_blend_mode(images[index].blend_mode);
12337 let mut end = start + 1;
12338 while end < ordered_items.len() {
12339 match ordered_items[end].1 {
12340 SegmentDrawItem::Image(next_index)
12341 if supported_blend_mode(images[next_index].blend_mode) == blend_mode =>
12342 {
12343 end += 1;
12344 }
12345 _ => break,
12346 }
12347 }
12348 Some((
12349 SegmentBatchPlan::Image {
12350 start,
12351 end,
12352 blend_mode,
12353 },
12354 end,
12355 ))
12356 }
12357 SegmentDrawItem::Text(_) => {
12358 let mut end = start + 1;
12359 while end < ordered_items.len() {
12360 if matches!(ordered_items[end].1, SegmentDrawItem::Text(_)) {
12361 end += 1;
12362 } else {
12363 break;
12364 }
12365 }
12366 Some((SegmentBatchPlan::Text { start, end }, end))
12367 }
12368 SegmentDrawItem::Composite(_) => {
12369 let mut end = start + 1;
12370 while end < ordered_items.len() {
12371 if matches!(ordered_items[end].1, SegmentDrawItem::Composite(_)) {
12372 end += 1;
12373 } else {
12374 break;
12375 }
12376 }
12377 Some((SegmentBatchPlan::Composite { start, end }, end))
12378 }
12379 SegmentDrawItem::ShaderComposite(_) => {
12380 let mut end = start + 1;
12381 while end < ordered_items.len() {
12382 if matches!(ordered_items[end].1, SegmentDrawItem::ShaderComposite(_)) {
12383 end += 1;
12384 } else {
12385 break;
12386 }
12387 }
12388 Some((SegmentBatchPlan::ShaderComposite { start, end }, end))
12389 }
12390 SegmentDrawItem::Retained(_) => {
12391 let mut end = start + 1;
12392 while end < ordered_items.len() {
12393 if matches!(ordered_items[end].1, SegmentDrawItem::Retained(_)) {
12394 end += 1;
12395 } else {
12396 break;
12397 }
12398 }
12399 Some((SegmentBatchPlan::Retained { start, end }, end))
12400 }
12401 SegmentDrawItem::Shadow(_) => None,
12402 }
12403}
12404
12405#[allow(clippy::too_many_arguments)]
12406fn collect_non_effect_segment_items(
12407 shapes: &[DrawShape],
12408 _images: &[ImageDraw],
12409 _texts: &[TextDraw],
12410 _shadow_draws: &[ShadowDraw],
12411 draw_ops: &[DrawOp],
12412 z_start: usize,
12413 z_end: usize,
12414 effect_z_ranges: &[Range<usize>],
12415 width: u32,
12416 height: u32,
12417 root_scale: f32,
12418 scratch: &mut Vec<(usize, SegmentDrawItem)>,
12419) {
12420 scratch.clear();
12421 let viewport = ViewportUniformParams {
12422 width,
12423 height,
12424 offset: [0.0, 0.0],
12425 };
12426
12427 scratch.extend(draw_ops.iter().filter_map(|op| {
12428 if op.z_index < z_start
12429 || op.z_index >= z_end
12430 || is_in_effect_range(op.z_index, effect_z_ranges)
12431 {
12432 return None;
12433 }
12434 let item = match op.kind {
12435 DrawOpKind::Shape(index) => {
12436 let shape = shapes.get(index)?;
12437 if !shape_draw_is_visible_in_viewport(shape, viewport, root_scale) {
12438 return None;
12439 }
12440 SegmentDrawItem::Shape(index)
12441 }
12442 DrawOpKind::Image(index) => SegmentDrawItem::Image(index),
12443 DrawOpKind::Text(index) => SegmentDrawItem::Text(index),
12444 DrawOpKind::Shadow(index) => SegmentDrawItem::Shadow(index),
12445 DrawOpKind::Retained(index) => SegmentDrawItem::Retained(index),
12446 };
12447 Some((op.z_index, item))
12448 }));
12449}
12450
12451fn retain_renderable_shadow_items(
12452 ordered_items: &mut Vec<(usize, SegmentDrawItem)>,
12453 shadow_draws: &[ShadowDraw],
12454 width: u32,
12455 height: u32,
12456 root_scale: f32,
12457 max_texture_dim: u32,
12458) -> usize {
12459 let original_len = ordered_items.len();
12460 ordered_items.retain(|(_, item)| match item {
12461 SegmentDrawItem::Shadow(index) => shadow_draws.get(*index).is_some_and(|shadow| {
12462 shadow_draw_may_render(shadow, width, height, root_scale, max_texture_dim)
12463 }),
12464 _ => true,
12465 });
12466 original_len.saturating_sub(ordered_items.len())
12467}
12468
12469#[cfg(not(target_arch = "wasm32"))]
12470#[derive(Clone, Copy)]
12471struct SegmentDiagCounts {
12472 raw_shadow_items: usize,
12473 culled_shadow_items: usize,
12474 cached_shadow_composites: usize,
12475 composite_items: usize,
12476 shader_composite_items: usize,
12477}
12478
12479#[cfg(not(target_arch = "wasm32"))]
12480fn maybe_print_segment_diag(
12481 z_range: Range<usize>,
12482 ordered_items: &[(usize, SegmentDrawItem)],
12483 shapes: &[DrawShape],
12484 images: &[ImageDraw],
12485 counts: SegmentDiagCounts,
12486 batch_limits: ShapeBatchLimits,
12487) {
12488 if !cranpose_core::env_flag!("CRANPOSE_SEGMENT_DIAG") {
12489 return;
12490 }
12491 let line = SEGMENT_DIAG_LINES.fetch_add(1, Ordering::Relaxed);
12492 if line >= 64 {
12493 return;
12494 }
12495
12496 let remaining_shadow_items = ordered_items
12497 .iter()
12498 .filter(|(_, item)| matches!(item, SegmentDrawItem::Shadow(_)))
12499 .count();
12500 let commands: Vec<_> =
12501 SegmentCommandIter::new(ordered_items, shapes, images, batch_limits).collect();
12502 let draw_chunks = commands
12503 .iter()
12504 .filter(|command| matches!(command, SegmentRenderCommand::DrawChunk(_)))
12505 .count();
12506 let shadow_commands = commands
12507 .iter()
12508 .filter(|command| matches!(command, SegmentRenderCommand::Shadow(_)))
12509 .count();
12510 let mut native_partitions = 0usize;
12511 let mut native_unfused_chunks = 0usize;
12512 for command in &commands {
12513 let SegmentRenderCommand::DrawChunk(chunk) = command else {
12514 continue;
12515 };
12516 match native_segment_fusion_partitions(ordered_items, shapes, chunk, batch_limits) {
12517 Ok(Some(partitions)) => native_partitions += partitions.len(),
12518 Ok(None) | Err(_) => native_unfused_chunks += 1,
12519 }
12520 }
12521
12522 eprintln!(
12523 "[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={}",
12524 z_range.start,
12525 z_range.end,
12526 ordered_items.len(),
12527 counts.raw_shadow_items,
12528 counts.culled_shadow_items,
12529 counts.cached_shadow_composites,
12530 remaining_shadow_items,
12531 counts.composite_items,
12532 counts.shader_composite_items,
12533 draw_chunks,
12534 shadow_commands,
12535 native_partitions,
12536 native_unfused_chunks,
12537 );
12538}
12539
12540pub(crate) fn has_backdrop_layer_in_range(
12541 backdrop_layers: &[BackdropLayer],
12542 z_start: usize,
12543 z_end: usize,
12544) -> bool {
12545 backdrop_layers
12546 .iter()
12547 .any(|layer| layer.z_index >= z_start && layer.z_index < z_end)
12548}
12549
12550pub(crate) fn scissor_rect_for_rect(
12551 rect: Rect,
12552 root_scale: f32,
12553 width: u32,
12554 height: u32,
12555) -> Option<(u32, u32, u32, u32)> {
12556 let mut left = canonicalize_device_coordinate(rect.x * root_scale);
12557 let mut top = canonicalize_device_coordinate(rect.y * root_scale);
12558 let mut right = canonicalize_device_coordinate((rect.x + rect.width) * root_scale);
12559 let mut bottom = canonicalize_device_coordinate((rect.y + rect.height) * root_scale);
12560
12561 left = left.max(0.0).min(width as f32).floor();
12562 top = top.max(0.0).min(height as f32).floor();
12563 right = right.max(0.0).min(width as f32).ceil();
12564 bottom = bottom.max(0.0).min(height as f32).ceil();
12565
12566 if right <= left || bottom <= top {
12567 return None;
12568 }
12569
12570 Some((
12571 left as u32,
12572 top as u32,
12573 (right - left) as u32,
12574 (bottom - top) as u32,
12575 ))
12576}
12577
12578fn scissor_rect_for_layer(
12579 rect: Rect,
12580 clip: Option<Rect>,
12581 root_scale: f32,
12582 width: u32,
12583 height: u32,
12584) -> Option<(u32, u32, u32, u32)> {
12585 let clipped_rect = match clip {
12586 Some(clip_rect) => rect.intersect(clip_rect)?,
12587 None => rect,
12588 };
12589
12590 scissor_rect_for_rect(clipped_rect, root_scale, width, height)
12591}
12592
12593fn tint_for_image(
12594 color_filter: Option<ColorFilter>,
12595 alpha: f32,
12596) -> ([f32; 4], Option<ColorFilter>) {
12597 let alpha = alpha.clamp(0.0, 1.0);
12598 match color_filter {
12599 Some(filter) if filter.supports_gpu_vertex_modulation() => {
12600 let Some(tint) = filter.gpu_vertex_tint() else {
12601 return ([1.0, 1.0, 1.0, alpha], Some(filter));
12602 };
12603 (
12604 [
12605 tint[0].clamp(0.0, 1.0),
12606 tint[1].clamp(0.0, 1.0),
12607 tint[2].clamp(0.0, 1.0),
12608 (tint[3] * alpha).clamp(0.0, 1.0),
12609 ],
12610 None,
12611 )
12612 }
12613 Some(filter) => ([1.0, 1.0, 1.0, alpha], Some(filter)),
12614 None => ([1.0, 1.0, 1.0, alpha], None),
12615 }
12616}
12617
12618fn image_uv_rect(image: &ImageBitmap, src_rect: Option<Rect>) -> Option<ImageUvRect> {
12619 let Some(src) = src_rect else {
12620 return Some(ImageUvRect {
12621 min: [0.0, 0.0],
12622 max: [1.0, 1.0],
12623 sample_bounds: [0.0, 0.0, 1.0, 1.0],
12624 });
12625 };
12626
12627 let (u_min, u_max, u_bound_min, u_bound_max) =
12628 source_axis_uv(src.x, src.width, image.width() as f32)?;
12629 let (v_min, v_max, v_bound_min, v_bound_max) =
12630 source_axis_uv(src.y, src.height, image.height() as f32)?;
12631
12632 Some(ImageUvRect {
12633 min: [u_min, v_min],
12634 max: [u_max, v_max],
12635 sample_bounds: [u_bound_min, v_bound_min, u_bound_max, v_bound_max],
12636 })
12637}
12638
12639fn glyph_atlas_uv_rect(entry: GlyphAtlasEntry, atlas_size: u32) -> ImageUvRect {
12644 let atlas_width = atlas_size as f32;
12645 let atlas_height = atlas_size as f32;
12646 let min = [entry.x as f32 / atlas_width, entry.y as f32 / atlas_height];
12647 let max = [
12648 (entry.x + entry.width) as f32 / atlas_width,
12649 (entry.y + entry.height) as f32 / atlas_height,
12650 ];
12651 let center_min = [
12652 (entry.x as f32 + 0.5) / atlas_width,
12653 (entry.y as f32 + 0.5) / atlas_height,
12654 ];
12655 let center_max = [
12656 (entry.x as f32 + entry.width as f32 - 0.5).max(entry.x as f32 + 0.5) / atlas_width,
12657 (entry.y as f32 + entry.height as f32 - 0.5).max(entry.y as f32 + 0.5) / atlas_height,
12658 ];
12659 ImageUvRect {
12660 min,
12661 max,
12662 sample_bounds: [center_min[0], center_min[1], center_max[0], center_max[1]],
12663 }
12664}
12665
12666fn snap_nearest_image_to_device_pixels(image: &mut ImageDraw, root_scale: f32) {
12667 if image.sampling != ImageSampling::Nearest || !root_scale.is_finite() || root_scale <= 0.0 {
12668 return;
12669 }
12670
12671 let Some(rect) = axis_aligned_quad_rect(image.quad) else {
12672 return;
12673 };
12674
12675 let left_px = (rect.x * root_scale).round();
12676 let top_px = (rect.y * root_scale).round();
12677 let width_px = (rect.width * root_scale).round().max(1.0);
12678 let height_px = (rect.height * root_scale).round().max(1.0);
12679 let snapped = Rect {
12680 x: left_px / root_scale,
12681 y: top_px / root_scale,
12682 width: width_px / root_scale,
12683 height: height_px / root_scale,
12684 };
12685
12686 image.rect = snapped;
12687 image.local_rect = Rect {
12688 x: image.local_rect.x + snapped.x - rect.x,
12689 y: image.local_rect.y + snapped.y - rect.y,
12690 width: snapped.width,
12691 height: snapped.height,
12692 };
12693 image.quad = crate::rect_to_quad(snapped);
12694}
12695
12696fn nearest_image_device_quad(image: &ImageDraw, root_scale: f32) -> Option<[[f32; 2]; 4]> {
12697 if image.sampling != ImageSampling::Nearest || !root_scale.is_finite() || root_scale <= 0.0 {
12698 return None;
12699 }
12700
12701 let rect = axis_aligned_quad_rect(image.quad)?;
12702 let left_px = (rect.x * root_scale).round();
12703 let top_px = (rect.y * root_scale).round();
12704 let width_px = (rect.width * root_scale).round().max(1.0);
12705 let height_px = (rect.height * root_scale).round().max(1.0);
12706 let right_px = left_px + width_px;
12707 let bottom_px = top_px + height_px;
12708 Some([
12709 [left_px, top_px],
12710 [right_px, top_px],
12711 [left_px, bottom_px],
12712 [right_px, bottom_px],
12713 ])
12714}
12715
12716fn source_axis_uv(start: f32, extent: f32, image_extent: f32) -> Option<(f32, f32, f32, f32)> {
12717 if !start.is_finite()
12718 || !extent.is_finite()
12719 || !image_extent.is_finite()
12720 || extent == 0.0
12721 || image_extent <= 0.0
12722 {
12723 return None;
12724 }
12725
12726 let end = start + extent;
12727 let edge_min = start.min(end).clamp(0.0, image_extent);
12728 let edge_max = start.max(end).clamp(0.0, image_extent);
12729 if edge_max <= edge_min {
12730 return None;
12731 }
12732
12733 let center_min = edge_min + 0.5;
12734 let center_max = edge_max - 0.5;
12735 let (bound_min, bound_max) = if center_min <= center_max {
12736 (center_min, center_max)
12737 } else {
12738 let center = (edge_min + edge_max) * 0.5;
12739 (center, center)
12740 };
12741
12742 Some((
12743 edge_min / image_extent,
12744 edge_max / image_extent,
12745 bound_min / image_extent,
12746 bound_max / image_extent,
12747 ))
12748}
12749
12750fn apply_filter_to_bitmap(image: &ImageBitmap, filter: ColorFilter) -> Result<ImageBitmap, String> {
12751 let mut filtered = Vec::with_capacity(image.pixels().len());
12752 for pixel in image.pixels().chunks_exact(4) {
12753 let rgba = [
12754 pixel[0] as f32 / 255.0,
12755 pixel[1] as f32 / 255.0,
12756 pixel[2] as f32 / 255.0,
12757 pixel[3] as f32 / 255.0,
12758 ];
12759 let out = filter.apply_rgba(rgba);
12760 filtered.push((out[0].clamp(0.0, 1.0) * 255.0).round() as u8);
12761 filtered.push((out[1].clamp(0.0, 1.0) * 255.0).round() as u8);
12762 filtered.push((out[2].clamp(0.0, 1.0) * 255.0).round() as u8);
12763 filtered.push((out[3].clamp(0.0, 1.0) * 255.0).round() as u8);
12764 }
12765 ImageBitmap::from_rgba8(image.width(), image.height(), filtered)
12766 .map_err(|error| format!("failed to build filtered bitmap: {error}"))
12767}
12768
12769fn scissor_rect_for_image(
12770 image: &ImageDraw,
12771 root_scale: f32,
12772 width: u32,
12773 height: u32,
12774) -> Option<(u32, u32, u32, u32)> {
12775 scissor_rect_for_layer(image.rect, image.clip, root_scale, width, height)
12776}
12777
12778fn inner_shadow_composite_mask(
12779 shadow: &ShadowDraw,
12780 root_scale: f32,
12781) -> Option<RoundedCompositeMask> {
12782 if !shadow
12783 .shapes
12784 .iter()
12785 .any(|(_, mode)| *mode == BlendMode::DstOut)
12786 {
12787 return None;
12788 }
12789 let (fill, _) = shadow.shapes.first()?;
12790 let rect = fill.local_rect;
12791 if rect.width <= 0.0 || rect.height <= 0.0 {
12792 return None;
12793 }
12794
12795 let radii = fill.shape.map_or([0.0; 4], |rounded| {
12796 let resolved = rounded.resolve(rect.width, rect.height);
12797 [
12798 resolved.top_left * root_scale,
12799 resolved.top_right * root_scale,
12800 resolved.bottom_left * root_scale,
12801 resolved.bottom_right * root_scale,
12802 ]
12803 });
12804
12805 Some(RoundedCompositeMask {
12806 rect: [
12807 rect.x * root_scale,
12808 rect.y * root_scale,
12809 rect.width * root_scale,
12810 rect.height * root_scale,
12811 ],
12812 radii,
12813 })
12814}
12815
12816#[cfg(test)]
12817mod tests {
12818 use super::*;
12819 use crate::normalized_scene::visible_draw_rect;
12820 use cranpose_foundation::lazy::{remember_lazy_list_state, LazyListScope, LazyListState};
12821 use cranpose_render_common::graph::{DrawPrimitiveNode, IsolationReasons, TextPrimitiveNode};
12822 use cranpose_render_common::raster_cache::LayerRasterCacheHashes;
12823 use cranpose_render_common::scene_builder::build_graph_from_applier;
12824 use cranpose_ui::text::{
12825 AnnotatedString, BaselineShift, RangeStyle, Shadow, SpanStyle, TextDecoration,
12826 TextDrawStyle, TextGeometricTransform, TextMotion, TextUnit,
12827 };
12828 use cranpose_ui::{
12829 LayoutEngine, LazyColumn, LazyColumnSpec, Modifier, Size, Text, TextLayoutOptions,
12830 TextStyle,
12831 };
12832 use cranpose_ui_graphics::{
12833 Brush, Color, CornerRadii, DrawPrimitive, Rect, RenderEffect, RoundedCornerShape,
12834 RuntimeShader,
12835 };
12836
12837 fn chunk(batches: &[SegmentBatchPlan]) -> SegmentDrawChunkPlan {
12838 let mut chunk = SegmentDrawChunkPlan::default();
12839 for batch in batches {
12840 chunk.push(*batch);
12841 }
12842 chunk
12843 }
12844
12845 fn with_test_app_context<R>(block: impl FnOnce() -> R) -> R {
12846 let app_context = cranpose_ui::AppContext::new();
12847 app_context.enter(block)
12848 }
12849
12850 fn assert_snap_anchor_close(actual: Option<SnapAnchor>, expected_origin: Point, message: &str) {
12851 let Some(actual) = actual else {
12852 panic!("{message}: missing snap anchor");
12853 };
12854 let expected = SnapAnchor::rigid(expected_origin);
12855 assert_eq!(
12856 actual.device_pixel_step, expected.device_pixel_step,
12857 "{message}: device pixel step changed"
12858 );
12859 assert!(
12860 (actual.origin.x - expected.origin.x).abs() <= 1e-4
12861 && (actual.origin.y - expected.origin.y).abs() <= 1e-4,
12862 "{message}: expected origin {:?}, got {:?}",
12863 expected.origin,
12864 actual.origin
12865 );
12866 }
12867
12868 fn effect_layer(z_start: usize, z_end: usize) -> EffectLayer {
12869 EffectLayer {
12870 rect: Rect {
12871 x: 0.0,
12872 y: 0.0,
12873 width: 10.0,
12874 height: 10.0,
12875 },
12876 clip: None,
12877 snap_anchor: None,
12878 effect: Some(RenderEffect::blur(4.0)),
12879 blend_mode: BlendMode::SrcOver,
12880 composite_alpha: 1.0,
12881 z_start,
12882 z_end,
12883 requirements: SurfaceRequirementSet::default().with(SurfaceRequirement::RenderEffect),
12884 }
12885 }
12886
12887 #[test]
12888 fn direct_shader_composite_accepts_box4_when_viewport_preserves_source_pixels() {
12889 assert_eq!(
12890 direct_shader_composite_viewport(
12891 1.0,
12892 BlendMode::SrcOver,
12893 Some((12.0, 18.0, 64.0, 32.0)),
12894 CompositeSampleMode::Box4,
12895 (64, 32),
12896 ),
12897 Some((12.0, 18.0, 64.0, 32.0))
12898 );
12899 }
12900
12901 #[test]
12902 fn direct_shader_composite_rejects_box4_when_viewport_resamples_source() {
12903 assert_eq!(
12904 direct_shader_composite_viewport(
12905 1.0,
12906 BlendMode::SrcOver,
12907 Some((12.0, 18.0, 64.5, 32.0)),
12908 CompositeSampleMode::Box4,
12909 (64, 32),
12910 ),
12911 None
12912 );
12913 assert_eq!(
12914 direct_shader_composite_viewport(
12915 1.0,
12916 BlendMode::SrcOver,
12917 Some((12.25, 18.0, 64.0, 32.0)),
12918 CompositeSampleMode::Box4,
12919 (64, 32),
12920 ),
12921 None
12922 );
12923 }
12924
12925 fn test_text_draw(rect: Rect, text_motion: TextMotion) -> TextDraw {
12926 let mut text_style = TextStyle::default();
12927 text_style.paragraph_style.text_motion = Some(text_motion);
12928 TextDraw {
12929 node_id: 42,
12930 rect,
12931 snap_anchor: None,
12932 translated_content_context: false,
12933 text: Arc::new(AnnotatedString::new("stable markdown row".to_string()).render_string()),
12934 color: Color::WHITE,
12935 text_style,
12936 font_size: 14.0,
12937 scale: 1.0,
12938 layout_options: TextLayoutOptions::default(),
12939 z_index: 0,
12940 clip: None,
12941 }
12942 }
12943
12944 #[test]
12945 fn static_text_image_cache_key_ignores_absolute_scroll_position() {
12946 let base = test_text_draw(
12947 Rect {
12948 x: 12.25,
12949 y: 40.75,
12950 width: 220.0,
12951 height: 24.0,
12952 },
12953 TextMotion::Static,
12954 );
12955 let scrolled = test_text_draw(
12956 Rect {
12957 x: 12.75,
12958 y: -318.5,
12959 width: 220.0,
12960 height: 24.0,
12961 },
12962 TextMotion::Static,
12963 );
12964
12965 let base_key = GpuRenderer::text_image_cache_key(&base, base.rect, 1.0, true);
12966 let scrolled_key = GpuRenderer::text_image_cache_key(&scrolled, scrolled.rect, 1.0, true);
12967
12968 assert_eq!(
12969 base_key, scrolled_key,
12970 "scrolling static text must reuse the same raster cache entry"
12971 );
12972 }
12973
12974 #[test]
12975 fn static_text_glyph_run_cache_key_ignores_absolute_scroll_position() {
12976 let base = test_text_draw(
12977 Rect {
12978 x: 12.25,
12979 y: 40.75,
12980 width: 220.0,
12981 height: 24.0,
12982 },
12983 TextMotion::Static,
12984 );
12985 let scrolled = test_text_draw(
12986 Rect {
12987 x: 12.75,
12988 y: -318.5,
12989 width: 220.0,
12990 height: 24.0,
12991 },
12992 TextMotion::Static,
12993 );
12994
12995 let base_key = GpuRenderer::text_glyph_run_cache_key(&base, base.rect, 1.0, true);
12996 let scrolled_key =
12997 GpuRenderer::text_glyph_run_cache_key(&scrolled, scrolled.rect, 1.0, true);
12998
12999 assert_eq!(
13000 base_key, scrolled_key,
13001 "scrolling static text must reuse the same retained glyph run"
13002 );
13003 }
13004
13005 #[test]
13006 fn static_multiline_text_glyph_source_keeps_full_text_when_image_source_slices() {
13007 let rect = Rect {
13008 x: 8.0,
13009 y: 100.0,
13010 width: 240.0,
13011 height: 1_000.0,
13012 };
13013 let mut draw = test_text_draw(rect, TextMotion::Static);
13014 let lines = (0..100)
13015 .map(|line| format!("line-{line:03}"))
13016 .collect::<Vec<_>>()
13017 .join("\n");
13018 draw.text = Arc::new(AnnotatedString::from(lines).render_string());
13019
13020 let raster_rect = Rect {
13021 x: 16.0,
13022 y: 200.0,
13023 width: 480.0,
13024 height: 2_000.0,
13025 };
13026 let clipped = clipped_text_raster_source(
13027 &draw,
13028 rect,
13029 raster_rect,
13030 Some(Rect {
13031 x: 0.0,
13032 y: 610.0,
13033 width: 800.0,
13034 height: 40.0,
13035 }),
13036 2.0,
13037 true,
13038 );
13039 let glyph = text_glyph_raster_source(&draw, raster_rect);
13040
13041 assert!(
13042 matches!(clipped.draw, Cow::Owned(_)),
13043 "the image source should still slice large clipped multiline text"
13044 );
13045 assert!(
13046 matches!(glyph.draw, Cow::Borrowed(_)),
13047 "the glyph source must keep a stable full-text run key while scrolling"
13048 );
13049
13050 let clipped_key = GpuRenderer::text_glyph_run_cache_key(
13051 clipped.draw.as_ref(),
13052 clipped.raster_rect,
13053 2.0,
13054 true,
13055 );
13056 let glyph_key = GpuRenderer::text_glyph_run_cache_key(
13057 glyph.draw.as_ref(),
13058 glyph.raster_rect,
13059 2.0,
13060 true,
13061 );
13062
13063 assert_ne!(
13064 clipped_key, glyph_key,
13065 "image slicing must not force glyph rendering onto per-scroll line-window cache keys"
13066 );
13067 }
13068
13069 #[cfg(not(target_arch = "wasm32"))]
13070 #[test]
13071 fn retained_glyph_viewport_offsets_relative_vertices_by_source_origin() {
13072 let viewport = ViewportUniformParams {
13073 width: 800,
13074 height: 600,
13075 offset: [10.0, 20.0],
13076 };
13077 let source = Rect {
13078 x: 40.0,
13079 y: 90.0,
13080 width: 120.0,
13081 height: 48.0,
13082 };
13083
13084 let retained = GpuRenderer::retained_glyph_viewport(viewport, source);
13085
13086 assert_eq!(retained.width, viewport.width);
13087 assert_eq!(retained.height, viewport.height);
13088 assert_eq!(retained.offset, [-30.0, -70.0]);
13089 }
13090
13091 #[cfg(not(target_arch = "wasm32"))]
13092 #[test]
13093 fn tiny_text_glyph_runs_stay_in_shared_uploads() {
13094 assert!(
13095 !should_use_retained_text_glyph_run(8, None),
13096 "tiny labels must stay in the shared fused batch"
13097 );
13098 }
13099
13100 #[cfg(not(target_arch = "wasm32"))]
13101 #[test]
13102 fn line_sized_text_glyph_runs_stay_in_shared_uploads() {
13103 assert!(
13104 !should_use_retained_text_glyph_run(64, None),
13105 "Markdown scroll frames contain many line-sized text runs; retaining each one creates per-run buffer binds instead of one shared glyph batch"
13106 );
13107 }
13108
13109 #[cfg(not(target_arch = "wasm32"))]
13110 #[test]
13111 fn large_clipped_text_glyph_runs_stay_in_shared_uploads() {
13112 assert!(
13113 !should_use_retained_text_glyph_run(
13114 MIN_RETAINED_TEXT_GLYPH_QUADS.saturating_mul(2),
13115 Some(Rect {
13116 x: 0.0,
13117 y: 0.0,
13118 width: 200.0,
13119 height: 100.0,
13120 }),
13121 ),
13122 "clipped lazy-list text must not draw a full retained run outside the viewport"
13123 );
13124 }
13125
13126 #[test]
13127 fn normal_text_glyph_draw_skips_offscreen_prewarm_candidates() {
13128 assert_eq!(
13129 text_glyph_draw_action(false, true, false),
13130 TextGlyphDrawAction::Skip,
13131 "normal draw traversal must not prepare offscreen text"
13132 );
13133 }
13134
13135 #[test]
13136 fn bounded_text_glyph_prewarm_admits_offscreen_candidates() {
13137 assert_eq!(
13138 text_glyph_draw_action(false, true, true),
13139 TextGlyphDrawAction::PrewarmOffscreen,
13140 "only the bounded prewarm path may prepare offscreen text"
13141 );
13142 }
13143
13144 #[test]
13145 fn visible_text_glyph_draws_are_always_admitted() {
13146 assert_eq!(
13147 text_glyph_draw_action(true, false, false),
13148 TextGlyphDrawAction::DrawVisible
13149 );
13150 assert_eq!(
13151 text_glyph_draw_action(true, true, true),
13152 TextGlyphDrawAction::DrawVisible
13153 );
13154 }
13155
13156 #[cfg(not(target_arch = "wasm32"))]
13157 #[test]
13158 fn offscreen_text_prewarm_skips_large_uncached_text_runs() {
13159 assert!(
13160 !offscreen_text_glyph_prewarm_work_is_bounded(
13161 None,
13162 MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS + 1,
13163 ),
13164 "offscreen prewarm must not collect large uncached text runs in an input frame"
13165 );
13166 }
13167
13168 #[cfg(not(target_arch = "wasm32"))]
13169 #[test]
13170 fn offscreen_text_prewarm_admits_small_uncached_text_runs() {
13171 assert!(
13172 offscreen_text_glyph_prewarm_work_is_bounded(
13173 None,
13174 MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS,
13175 ),
13176 "small labels can be warmed without risking a frame-budget spike"
13177 );
13178 }
13179
13180 #[cfg(not(target_arch = "wasm32"))]
13181 #[test]
13182 fn offscreen_text_prewarm_skips_large_cached_runs_without_quads() {
13183 assert!(
13184 !offscreen_text_glyph_prewarm_work_is_bounded(
13185 Some(MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CACHED_GLYPHS + 1),
13186 0,
13187 ),
13188 "cached glyph placements can still be too large to prepare during input frames"
13189 );
13190 }
13191
13192 #[cfg(not(target_arch = "wasm32"))]
13193 #[test]
13194 fn offscreen_text_prewarm_stops_after_candidate_budget() {
13195 assert!(
13196 offscreen_text_glyph_prewarm_budget_exhausted(
13197 Instant::now(),
13198 MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CANDIDATES,
13199 ),
13200 "prewarm must be bounded by candidate count even when each candidate is cheap"
13201 );
13202 }
13203
13204 #[test]
13205 fn clipped_cached_glyph_quads_are_filtered_to_viewport() {
13206 fn quad(y: i32) -> CachedTextGlyphQuad {
13207 CachedTextGlyphQuad {
13208 x: 8,
13209 y,
13210 width: 20,
13211 height: 10,
13212 color: (1.0, 1.0, 1.0, 1.0),
13213 uv: ImageUvRect {
13214 min: [0.0, 0.0],
13215 max: [1.0, 1.0],
13216 sample_bounds: [0.0, 0.0, 1.0, 1.0],
13217 },
13218 }
13219 }
13220
13221 let source = Rect {
13222 x: 0.0,
13223 y: 0.0,
13224 width: 320.0,
13225 height: 400.0,
13226 };
13227 let clip = Some(Rect {
13228 x: 0.0,
13229 y: 0.0,
13230 width: 320.0,
13231 height: 80.0,
13232 });
13233 let viewport = ViewportUniformParams {
13234 width: 320,
13235 height: 80,
13236 offset: [0.0, 0.0],
13237 };
13238
13239 assert!(cached_text_glyph_quad_is_visible_in_viewport(
13240 source,
13241 &quad(40),
13242 clip,
13243 viewport,
13244 1.0,
13245 ));
13246 assert!(
13247 !cached_text_glyph_quad_is_visible_in_viewport(source, &quad(140), clip, viewport, 1.0,),
13248 "glyphs outside the effective clip should not enter the frame command stream"
13249 );
13250 }
13251
13252 #[test]
13253 fn small_scene_range_cache_miss_observes_first_render() {
13254 let key = LayerRasterCacheKey::scene_range(
13255 0xCACE,
13256 Rect {
13257 x: 0.0,
13258 y: 0.0,
13259 width: 120.0,
13260 height: 80.0,
13261 },
13262 (120, 80),
13263 ScaleBucket::from_scale(1.0),
13264 );
13265
13266 assert!(
13267 !first_cache_miss_admission(&key),
13268 "a small scene-range miss should render directly first instead of materializing a tiny one-frame retained target"
13269 );
13270 assert!(
13271 repeated_cache_miss_admission(&key),
13272 "a repeated small scene-range miss is stable enough to materialize into the retained cache"
13273 );
13274 }
13275
13276 #[test]
13277 fn large_scene_range_cache_miss_requires_repeated_stable_key() {
13278 let key = LayerRasterCacheKey::scene_range(
13279 0xCACE,
13280 Rect {
13281 x: 0.0,
13282 y: 0.0,
13283 width: 1200.0,
13284 height: 900.0,
13285 },
13286 (1200, 900),
13287 ScaleBucket::from_scale(1.0),
13288 );
13289
13290 assert!(
13291 !first_cache_miss_admission(&key),
13292 "a large first scene-range miss should render directly instead of materializing a multi-MB one-frame cache entry"
13293 );
13294 assert!(
13295 repeated_cache_miss_admission(&key),
13296 "a repeated scene-range miss is stable enough to materialize into the retained cache"
13297 );
13298 }
13299
13300 #[test]
13301 fn renderer_warmup_frame_is_requested_for_cache_miss_stats_only() {
13302 let stats = gpu_stats::FrameStats::default();
13303 let mut snapshot = stats.snapshot();
13304 assert!(
13305 !frame_stats_need_warmup_frame(&snapshot),
13306 "a clean frame must not keep a static scene redrawing"
13307 );
13308
13309 snapshot.layer_cache_misses = 1;
13310 assert!(frame_stats_need_warmup_frame(&snapshot));
13311 snapshot.layer_cache_misses = 0;
13312
13313 snapshot.shadow_shape_cache_misses = 1;
13314 assert!(frame_stats_need_warmup_frame(&snapshot));
13315 snapshot.shadow_shape_cache_misses = 0;
13316
13317 snapshot.text_image_cache_misses = 1;
13318 assert!(frame_stats_need_warmup_frame(&snapshot));
13319 snapshot.text_image_cache_misses = 0;
13320
13321 snapshot.text_glyph_atlas_misses = 1;
13322 assert!(frame_stats_need_warmup_frame(&snapshot));
13323 }
13324
13325 #[test]
13326 fn renderer_warmup_budget_is_consumed_by_a_repeated_cache_miss() {
13327 let stats = gpu_stats::FrameStats::default();
13328 let mut snapshot = stats.snapshot();
13329 snapshot.layer_cache_misses = 1;
13330 let mut pending_frames = 0;
13331
13332 update_frame_warmup_budget(&mut pending_frames, &snapshot);
13333 assert_eq!(pending_frames, CACHE_MISS_WARMUP_FRAMES);
13334
13335 update_frame_warmup_budget(&mut pending_frames, &snapshot);
13336 assert_eq!(
13337 pending_frames, 0,
13338 "a cache miss during the warmup frame must not replenish its budget"
13339 );
13340 }
13341
13342 #[test]
13343 fn non_scene_layer_surface_cache_miss_admits_first_render() {
13344 let key = LayerRasterCacheKey::new(
13345 Some(77),
13346 0xC0FFEE,
13347 0,
13348 Rect {
13349 x: 0.0,
13350 y: 0.0,
13351 width: 120.0,
13352 height: 80.0,
13353 },
13354 (120, 80),
13355 ScaleBucket::from_scale(1.0),
13356 );
13357
13358 assert!(
13359 first_cache_miss_admission(&key),
13360 "ordinary retained layer surfaces should still cache on first miss"
13361 );
13362 }
13363
13364 #[test]
13365 fn text_image_cache_key_is_content_addressed_not_node_addressed() {
13366 let first = test_text_draw(
13367 Rect {
13368 x: 12.25,
13369 y: 40.75,
13370 width: 220.0,
13371 height: 24.0,
13372 },
13373 TextMotion::Static,
13374 );
13375 let mut second = first.clone();
13376 second.node_id = first.node_id + 1;
13377
13378 let first_key = GpuRenderer::text_image_cache_key(&first, first.rect, 1.0, true);
13379 let second_key = GpuRenderer::text_image_cache_key(&second, second.rect, 1.0, true);
13380
13381 assert_eq!(
13382 first_key, second_key,
13383 "text raster cache keys must be based on rendered pixels, not node identity"
13384 );
13385 }
13386
13387 #[test]
13388 fn animated_text_image_cache_key_keeps_fractional_phase_only() {
13389 let base = test_text_draw(
13390 Rect {
13391 x: 12.25,
13392 y: 40.75,
13393 width: 220.0,
13394 height: 24.0,
13395 },
13396 TextMotion::Animated,
13397 );
13398 let integer_translated = test_text_draw(
13399 Rect {
13400 x: 44.25,
13401 y: 88.75,
13402 width: 220.0,
13403 height: 24.0,
13404 },
13405 TextMotion::Animated,
13406 );
13407 let phase_shifted = test_text_draw(
13408 Rect {
13409 x: 44.5,
13410 y: 88.75,
13411 width: 220.0,
13412 height: 24.0,
13413 },
13414 TextMotion::Animated,
13415 );
13416
13417 let base_key = GpuRenderer::text_image_cache_key(&base, base.rect, 1.0, false);
13418 let translated_key = GpuRenderer::text_image_cache_key(
13419 &integer_translated,
13420 integer_translated.rect,
13421 1.0,
13422 false,
13423 );
13424 let phase_shifted_key =
13425 GpuRenderer::text_image_cache_key(&phase_shifted, phase_shifted.rect, 1.0, false);
13426
13427 assert_eq!(
13428 base_key, translated_key,
13429 "integer translation should not invalidate animated text raster cache entries"
13430 );
13431 assert_ne!(
13432 base_key, phase_shifted_key,
13433 "fractional phase affects animated text rasterization and must stay in the key"
13434 );
13435 }
13436
13437 #[test]
13438 fn animated_translated_text_raster_geometry_applies_snap_anchor() {
13439 let mut base = test_text_draw(
13440 Rect {
13441 x: 14.25,
13442 y: 16.50,
13443 width: 220.0,
13444 height: 24.0,
13445 },
13446 TextMotion::Animated,
13447 );
13448 base.snap_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.50)));
13449
13450 let mut scrolled = test_text_draw(
13451 Rect {
13452 x: 14.25,
13453 y: 15.80,
13454 width: 220.0,
13455 height: 24.0,
13456 },
13457 TextMotion::Animated,
13458 );
13459 scrolled.snap_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 15.80)));
13460
13461 let (base_logical, base_raster, _, _, base_static) =
13462 text_raster_geometry_for_draw(&base, 1.0).expect("base text geometry");
13463 let (scrolled_logical, scrolled_raster, _, _, scrolled_static) =
13464 text_raster_geometry_for_draw(&scrolled, 1.0).expect("scrolled text geometry");
13465
13466 assert!(!base_static);
13467 assert!(!scrolled_static);
13468 assert!((base_logical.x - 14.0).abs() < f32::EPSILON);
13469 assert!((base_logical.y - 17.0).abs() < f32::EPSILON);
13470 assert!((scrolled_logical.x - 14.0).abs() < f32::EPSILON);
13471 assert!((scrolled_logical.y - 16.0).abs() < f32::EPSILON);
13472 assert_eq!(base_raster.x.fract(), 0.0);
13473 assert_eq!(base_raster.y.fract(), 0.0);
13474 assert_eq!(scrolled_raster.x.fract(), 0.0);
13475 assert_eq!(scrolled_raster.y.fract(), 0.0);
13476
13477 let base_key = GpuRenderer::text_image_cache_key(&base, base_raster, 1.0, false);
13478 let scrolled_key =
13479 GpuRenderer::text_image_cache_key(&scrolled, scrolled_raster, 1.0, false);
13480 assert_eq!(
13481 base_key, scrolled_key,
13482 "translated animated text should keep a stable raster phase while scrolling"
13483 );
13484 }
13485
13486 #[test]
13487 fn translated_static_text_moves_one_device_pixel_at_half_pixel_phase() {
13488 let root_scale = 1.25;
13489 let mut base = test_text_draw(
13490 Rect {
13491 x: 14.0,
13492 y: 276.0,
13493 width: 220.0,
13494 height: 24.0,
13495 },
13496 TextMotion::Static,
13497 );
13498 base.snap_anchor = Some(SnapAnchor::rigid(Point::new(0.0, 127.600_006)));
13499
13500 let mut scrolled = test_text_draw(
13501 Rect {
13502 x: 14.0,
13503 y: 275.2,
13504 width: 220.0,
13505 height: 24.0,
13506 },
13507 TextMotion::Static,
13508 );
13509 scrolled.snap_anchor = Some(SnapAnchor::rigid(Point::new(0.0, 126.799_99)));
13510
13511 let (_, base_raster, _, _, _) =
13512 text_raster_geometry_for_draw(&base, root_scale).expect("base text geometry");
13513 let (_, scrolled_raster, _, _, _) =
13514 text_raster_geometry_for_draw(&scrolled, root_scale).expect("scrolled text geometry");
13515
13516 assert_eq!(
13517 base_raster.y - scrolled_raster.y,
13518 1.0,
13519 "one physical pixel of rigid scrolling must move static text by one raster pixel"
13520 );
13521 }
13522
13523 #[test]
13524 fn translated_text_snap_does_not_move_its_fixed_ancestor_clip() {
13525 let root_scale = 1.25;
13526 let fixed_clip = Rect {
13527 x: 8.0,
13528 y: 20.0,
13529 width: 300.0,
13530 height: 680.0,
13531 };
13532 let mut draw = test_text_draw(
13533 Rect {
13534 x: 14.0,
13535 y: 276.0,
13536 width: 220.0,
13537 height: 24.0,
13538 },
13539 TextMotion::Static,
13540 );
13541 draw.snap_anchor = Some(SnapAnchor::rigid(Point::new(0.0, 127.4)));
13542 draw.clip = Some(fixed_clip);
13543
13544 let (_, _, clip, _, _) =
13545 text_raster_geometry_for_draw(&draw, root_scale).expect("clipped text geometry");
13546
13547 assert_eq!(
13548 clip,
13549 Some(fixed_clip),
13550 "content pixel snapping must not translate a fixed ancestor clip"
13551 );
13552 }
13553
13554 #[test]
13555 fn clipped_static_multiline_text_raster_source_limits_visible_line_window() {
13556 let rect = Rect {
13557 x: 8.0,
13558 y: 100.0,
13559 width: 240.0,
13560 height: 1_000.0,
13561 };
13562 let mut draw = test_text_draw(rect, TextMotion::Static);
13563 let lines = (0..100)
13564 .map(|line| format!("line-{line:03}"))
13565 .collect::<Vec<_>>()
13566 .join("\n");
13567 draw.text = Arc::new(AnnotatedString::from(lines).render_string());
13568
13569 let raster_rect = Rect {
13570 x: 16.0,
13571 y: 200.0,
13572 width: 480.0,
13573 height: 2_000.0,
13574 };
13575 let source = clipped_text_raster_source(
13576 &draw,
13577 rect,
13578 raster_rect,
13579 Some(Rect {
13580 x: 0.0,
13581 y: 610.0,
13582 width: 800.0,
13583 height: 40.0,
13584 }),
13585 2.0,
13586 true,
13587 );
13588
13589 let Cow::Owned(sliced_draw) = source.draw else {
13590 panic!("clipped static multiline text should rasterize only the visible line window");
13591 };
13592 let sliced_text = sliced_draw.text.text.as_str();
13593 assert!(sliced_text.contains("line-050"));
13594 assert!(sliced_text.contains("line-055"));
13595 assert!(!sliced_text.contains("line-000"));
13596 assert!(!sliced_text.contains("line-099"));
13597 assert_eq!(source.raster_rect.x, raster_rect.x);
13598 assert!(source.raster_rect.y > raster_rect.y);
13599 assert!(source.raster_rect.height < raster_rect.height);
13600 }
13601
13602 #[test]
13603 fn clipped_static_multiline_text_raster_source_slices_short_multiline_text() {
13604 let rect = Rect {
13605 x: 8.0,
13606 y: 100.0,
13607 width: 240.0,
13608 height: 320.0,
13609 };
13610 let mut draw = test_text_draw(rect, TextMotion::Static);
13611 let lines = (0..24)
13612 .map(|line| format!("code-line-{line:02}"))
13613 .collect::<Vec<_>>()
13614 .join("\n");
13615 draw.text = Arc::new(AnnotatedString::from(lines).render_string());
13616
13617 let raster_rect = Rect {
13618 x: 16.0,
13619 y: 200.0,
13620 width: 480.0,
13621 height: 640.0,
13622 };
13623 let source = clipped_text_raster_source(
13624 &draw,
13625 rect,
13626 raster_rect,
13627 Some(Rect {
13628 x: 0.0,
13629 y: 190.0,
13630 width: 800.0,
13631 height: 120.0,
13632 }),
13633 2.0,
13634 true,
13635 );
13636
13637 let Cow::Owned(sliced_draw) = source.draw else {
13638 panic!("clipped multiline text should rasterize only the visible line window");
13639 };
13640 assert!(sliced_draw.text.text.as_str().contains("code-line-06"));
13641 assert!(!sliced_draw.text.text.as_str().contains("code-line-00"));
13642 assert!(!sliced_draw.text.text.as_str().contains("code-line-23"));
13643 assert_eq!(source.raster_rect.x, raster_rect.x);
13644 assert!(source.raster_rect.y > raster_rect.y);
13645 assert!(source.raster_rect.height < raster_rect.height);
13646 }
13647
13648 #[test]
13649 fn text_line_index_cache_reuses_retained_index_for_same_text_instance() {
13650 let mut cache = TextLineIndexCache::new(4);
13651 let text = Arc::new(AnnotatedString::from("a\nb\nc").render_string());
13652
13653 let first = cache.line_starts(&text);
13654 let second = cache.line_starts(&text);
13655
13656 assert_eq!(first.as_ref(), &[0, 2, 4]);
13657 assert!(
13658 Rc::ptr_eq(&first, &second),
13659 "retained text should not rebuild its line index on every clipped frame"
13660 );
13661 }
13662
13663 #[test]
13664 fn text_line_index_cache_is_retained_text_instance_local() {
13665 let mut cache = TextLineIndexCache::new(4);
13666 let first_text = Arc::new(AnnotatedString::from("a\nb\nc").render_string());
13667 let second_text = Arc::new(AnnotatedString::from("a\nb\nc").render_string());
13668
13669 let first = cache.line_starts(&first_text);
13670 let second = cache.line_starts(&second_text);
13671
13672 assert_eq!(first.as_ref(), second.as_ref());
13673 assert!(
13674 !Rc::ptr_eq(&first, &second),
13675 "line index lookup should not hash large text contents to find unrelated retained nodes"
13676 );
13677 }
13678
13679 #[test]
13680 fn device_pixel_bounds_for_rect_snaps_origin_and_extents() {
13681 let bounds = device_pixel_bounds_for_rect(
13682 Rect {
13683 x: 10.25,
13684 y: 14.6,
13685 width: 20.1,
13686 height: 9.2,
13687 },
13688 200,
13689 120,
13690 2.0,
13691 )
13692 .expect("rect should intersect the viewport");
13693
13694 assert_eq!(
13695 bounds,
13696 DevicePixelBounds {
13697 x: 20.0,
13698 y: 29.0,
13699 width: 41,
13700 height: 19,
13701 }
13702 );
13703 }
13704
13705 #[test]
13706 fn visible_layer_rect_intersects_clip_and_viewport() {
13707 let visible = visible_layer_rect(
13708 Rect {
13709 x: -10.0,
13710 y: 5.0,
13711 width: 80.0,
13712 height: 40.0,
13713 },
13714 Some(Rect {
13715 x: 4.0,
13716 y: 8.0,
13717 width: 20.0,
13718 height: 50.0,
13719 }),
13720 2.0,
13721 60,
13722 40,
13723 )
13724 .expect("visible rect");
13725
13726 assert_eq!(
13727 visible,
13728 Rect {
13729 x: 4.0,
13730 y: 8.0,
13731 width: 20.0,
13732 height: 12.0,
13733 }
13734 );
13735 }
13736
13737 #[test]
13738 fn clamp_effect_surface_scale_caps_large_surfaces_but_keeps_base_scale() {
13739 let clamped = clamp_effect_surface_scale(
13740 Rect {
13741 x: 0.0,
13742 y: 0.0,
13743 width: 1200.0,
13744 height: 900.0,
13745 },
13746 1.0,
13747 8.0,
13748 16_384,
13749 );
13750
13751 assert!(
13752 clamped < 8.0,
13753 "large translated effect layers must be capped to avoid OOM, got {clamped}"
13754 );
13755 assert!(
13756 clamped >= 1.0,
13757 "effect surfaces must not fall below destination resolution, got {clamped}"
13758 );
13759 }
13760
13761 #[test]
13762 fn clamp_effect_surface_scale_keeps_decorated_text_capture_scale() {
13763 let clamped = clamp_effect_surface_scale(
13764 Rect {
13765 x: 0.0,
13766 y: 0.0,
13767 width: 446.0,
13768 height: 44.0,
13769 },
13770 1.0,
13771 9.0,
13772 16_384,
13773 );
13774
13775 assert_eq!(
13776 clamped, 9.0,
13777 "decorated text motion-stable captures must keep full scale"
13778 );
13779 }
13780
13781 fn backdrop_layer(z_index: usize) -> BackdropLayer {
13782 BackdropLayer {
13783 node_id: Some(700 + z_index),
13784 rect: Rect {
13785 x: 0.0,
13786 y: 0.0,
13787 width: 10.0,
13788 height: 10.0,
13789 },
13790 clip: None,
13791 snap_anchor: None,
13792 effect: RenderEffect::blur(2.0),
13793 z_index,
13794 }
13795 }
13796
13797 fn test_shape(z_index: usize, blend_mode: BlendMode) -> DrawShape {
13798 DrawShape {
13799 rect: Rect {
13800 x: 0.0,
13801 y: 0.0,
13802 width: 8.0,
13803 height: 8.0,
13804 },
13805 local_rect: Rect {
13806 x: 0.0,
13807 y: 0.0,
13808 width: 8.0,
13809 height: 8.0,
13810 },
13811 quad: [[0.0, 0.0], [8.0, 0.0], [0.0, 8.0], [8.0, 8.0]],
13812 snap_anchor: None,
13813 brush: Brush::solid(Color::BLACK),
13814 shape: None,
13815 stroke: None,
13816 arc: None,
13817 z_index,
13818 clip: None,
13819 blend_mode,
13820 motion_context_animated: false,
13821 }
13822 }
13823
13824 #[test]
13825 fn shape_shadow_content_hash_ignores_viewport_translation() {
13826 fn translate_shape(shape: &DrawShape, dx: f32, dy: f32) -> DrawShape {
13827 let mut translated = shape.clone();
13828 translated.rect.x += dx;
13829 translated.rect.y += dy;
13830 translated.local_rect.x += dx;
13831 translated.local_rect.y += dy;
13832 for point in &mut translated.quad {
13833 point[0] += dx;
13834 point[1] += dy;
13835 }
13836 translated.snap_anchor = translated.snap_anchor.map(|anchor| {
13837 SnapAnchor::rigid(Point::new(anchor.origin.x + dx, anchor.origin.y + dy))
13838 });
13839 translated.clip = translated.clip.map(|mut clip| {
13840 clip.x += dx;
13841 clip.y += dy;
13842 clip
13843 });
13844 translated
13845 }
13846
13847 let mut first = test_shape(1, BlendMode::SrcOver);
13848 first.rect = Rect {
13849 x: 10.0,
13850 y: 20.0,
13851 width: 80.0,
13852 height: 40.0,
13853 };
13854 first.local_rect = first.rect;
13855 first.quad = [[10.0, 20.0], [90.0, 20.0], [10.0, 60.0], [90.0, 60.0]];
13856 first.snap_anchor = Some(SnapAnchor::rigid(Point::new(7.0, 11.0)));
13857 first.shape = Some(RoundedCornerShape::uniform(8.0));
13858 first.clip = Some(Rect {
13859 x: 8.0,
13860 y: 18.0,
13861 width: 86.0,
13862 height: 44.0,
13863 });
13864 let mut cutout = test_shape(2, BlendMode::DstOut);
13865 cutout.rect = Rect {
13866 x: 18.0,
13867 y: 26.0,
13868 width: 62.0,
13869 height: 22.0,
13870 };
13871 cutout.local_rect = cutout.rect;
13872 cutout.quad = [[18.0, 26.0], [80.0, 26.0], [18.0, 48.0], [80.0, 48.0]];
13873 cutout.shape = Some(RoundedCornerShape::uniform(4.0));
13874
13875 let dx = 37.0;
13876 let dy = -11.5;
13877 let translated = translate_shape(&first, dx, dy);
13878 let translated_cutout = translate_shape(&cutout, dx, dy);
13879
13880 let root_scale = 1.25;
13881 let first_shapes = vec![
13882 (first.clone(), BlendMode::SrcOver),
13883 (cutout, BlendMode::DstOut),
13884 ];
13885 let translated_shapes = vec![
13886 (translated.clone(), BlendMode::SrcOver),
13887 (translated_cutout, BlendMode::DstOut),
13888 ];
13889
13890 let first_hash = shape_shadow_content_hash(&first_shapes, root_scale);
13891 let translated_hash = shape_shadow_content_hash(&translated_shapes, root_scale);
13892
13893 assert_eq!(first_hash, translated_hash);
13894
13895 let mut changed_shapes = translated_shapes;
13896 changed_shapes[0].0.rect.width += 1.0;
13897 let changed_hash = shape_shadow_content_hash(&changed_shapes, root_scale);
13898
13899 assert_ne!(first_hash, changed_hash);
13900 }
13901
13902 #[test]
13903 fn shape_shadow_content_hash_is_stable_under_fractional_scale_scroll() {
13904 fn shadow_shapes_at(y: f32) -> Vec<(DrawShape, BlendMode)> {
13910 let mut shape = test_shape(1, BlendMode::SrcOver);
13911 shape.rect = Rect {
13912 x: 24.0,
13913 y,
13914 width: 180.0,
13915 height: 90.0,
13916 };
13917 shape.local_rect = shape.rect;
13918 shape.quad = crate::rect_to_quad(shape.rect);
13919 shape.shape = Some(RoundedCornerShape::uniform(14.0));
13920 vec![(shape, BlendMode::SrcOver)]
13921 }
13922
13923 let root_scale = 130.0f32 / 96.0;
13924 let blur_radius = 18.0f32;
13925 let pixel_radius = blur_radius * root_scale;
13926
13927 let key_at = |y: f32| {
13928 let shapes = shadow_shapes_at(y);
13929 let plan =
13930 shape_shadow_surface_plan(&shapes, None, blur_radius, 1600, 1600, root_scale, 8192)
13931 .expect("surface plan");
13932 shape_shadow_surface_cache_key(
13933 &shapes,
13934 plan.source_device_bounds,
13935 pixel_radius,
13936 root_scale,
13937 )
13938 .expect("cache key")
13939 };
13940
13941 let base = key_at(640.0);
13946 for step in 1..=12 {
13947 let scrolled = key_at(640.0 - step as f32 * 4.0);
13948 assert_eq!(
13949 base, scrolled,
13950 "scrolled shadow cache key must stay stable at fractional scale (step {step})"
13951 );
13952 }
13953 }
13954
13955 #[test]
13956 fn shape_shadow_cache_key_uses_unclipped_source_bounds_for_scrolled_clip() {
13957 fn translated_card_shadow(y: f32) -> Vec<(DrawShape, BlendMode)> {
13958 let mut shape = test_shape(1, BlendMode::SrcOver);
13959 shape.rect = Rect {
13960 x: 24.0,
13961 y,
13962 width: 280.0,
13963 height: 120.0,
13964 };
13965 shape.local_rect = shape.rect;
13966 shape.quad = [[24.0, y], [304.0, y], [24.0, y + 120.0], [304.0, y + 120.0]];
13967 shape.shape = Some(RoundedCornerShape::uniform(18.0));
13968 vec![(shape, BlendMode::SrcOver)]
13969 }
13970
13971 let root_scale = 1.0;
13972 let blur_radius = 18.0;
13973 let viewport_clip = Rect {
13974 x: 0.0,
13975 y: 96.0,
13976 width: 360.0,
13977 height: 720.0,
13978 };
13979 let key_for = |y: f32| {
13980 let shapes = translated_card_shadow(y);
13981 let plan = shape_shadow_surface_plan(
13982 &shapes,
13983 Some(viewport_clip),
13984 blur_radius,
13985 360,
13986 900,
13987 root_scale,
13988 4096,
13989 )
13990 .expect("surface plan");
13991 shape_shadow_surface_cache_key(
13992 &shapes,
13993 plan.source_device_bounds,
13994 plan.pixel_radius,
13995 root_scale,
13996 )
13997 .expect("cache key")
13998 };
13999
14000 assert_eq!(key_for(740.0), key_for(756.0));
14003 }
14004
14005 #[test]
14006 fn shape_visibility_uses_nonzero_viewport_offset_for_cropped_offscreen() {
14007 let mut shape = test_shape(1, BlendMode::SrcOver);
14008 shape.rect = Rect {
14009 x: 24.0,
14010 y: 740.0,
14011 width: 280.0,
14012 height: 120.0,
14013 };
14014 shape.local_rect = shape.rect;
14015 shape.quad = [[24.0, 740.0], [304.0, 740.0], [24.0, 860.0], [304.0, 860.0]];
14016 let viewport = ViewportUniformParams {
14017 width: 316,
14018 height: 228,
14019 offset: [6.0, 686.0],
14020 };
14021
14022 assert!(shape_draw_is_visible_in_viewport(&shape, viewport, 1.0));
14023 }
14024
14025 #[test]
14026 fn text_prewarm_uses_nonzero_viewport_offset_for_cropped_offscreen() {
14027 let viewport = ViewportUniformParams {
14028 width: 316,
14029 height: 228,
14030 offset: [6.0, 686.0],
14031 };
14032 let text_rect = Rect {
14033 x: 24.0,
14034 y: 740.0,
14035 width: 280.0,
14036 height: 40.0,
14037 };
14038
14039 assert!(text_draw_is_visible_in_viewport(
14040 text_rect, None, viewport, 1.0
14041 ));
14042 assert!(text_draw_should_prewarm_in_viewport(
14043 text_rect, None, viewport, 1.0
14044 ));
14045 }
14046
14047 fn test_shadow_draw(shapes: Vec<(DrawShape, BlendMode)>) -> ShadowDraw {
14048 ShadowDraw {
14049 shapes,
14050 texts: vec![],
14051 blur_radius: 8.0,
14052 clip: None,
14053 z_index: 0,
14054 }
14055 }
14056
14057 fn test_image(z_index: usize, blend_mode: BlendMode) -> ImageDraw {
14058 ImageDraw {
14059 rect: Rect {
14060 x: 0.0,
14061 y: 0.0,
14062 width: 8.0,
14063 height: 8.0,
14064 },
14065 local_rect: Rect {
14066 x: 0.0,
14067 y: 0.0,
14068 width: 8.0,
14069 height: 8.0,
14070 },
14071 quad: [[0.0, 0.0], [8.0, 0.0], [0.0, 8.0], [8.0, 8.0]],
14072 snap_anchor: None,
14073 image: ImageBitmap::from_rgba8(1, 1, vec![255, 255, 255, 255]).expect("image"),
14074 alpha: 1.0,
14075 color_filter: None,
14076 sampling: ImageSampling::Nearest,
14077 z_index,
14078 clip: None,
14079 blend_mode,
14080 src_rect: None,
14081 motion_context_animated: false,
14082 }
14083 }
14084
14085 #[test]
14086 fn image_sampler_descriptors_match_requested_sampling() {
14087 let nearest = image_sampler_descriptor(ImageSampling::Nearest);
14088 assert_eq!(nearest.mag_filter, wgpu::FilterMode::Nearest);
14089 assert_eq!(nearest.min_filter, wgpu::FilterMode::Nearest);
14090
14091 let linear = image_sampler_descriptor(ImageSampling::Linear);
14092 assert_eq!(linear.mag_filter, wgpu::FilterMode::Linear);
14093 assert_eq!(linear.min_filter, wgpu::FilterMode::Linear);
14094 }
14095
14096 #[test]
14097 fn image_uv_rect_clamps_source_rect_to_texel_centers() {
14098 let image = ImageBitmap::from_rgba8(24, 16, vec![0; 24 * 16 * 4]).expect("image");
14099 let uv = image_uv_rect(
14100 &image,
14101 Some(Rect {
14102 x: 0.0,
14103 y: 0.0,
14104 width: 16.0,
14105 height: 16.0,
14106 }),
14107 )
14108 .expect("uv rect");
14109
14110 assert_eq!(uv.min, [0.0, 0.0]);
14111 assert_eq!(uv.max, [16.0 / 24.0, 1.0]);
14112 assert_eq!(
14113 uv.sample_bounds,
14114 [0.5 / 24.0, 0.5 / 16.0, 15.5 / 24.0, 15.5 / 16.0]
14115 );
14116 }
14117
14118 #[test]
14119 fn image_uv_rect_keeps_full_image_unclamped() {
14120 let image = ImageBitmap::from_rgba8(2, 2, vec![0; 16]).expect("image");
14121 let uv = image_uv_rect(&image, None).expect("uv rect");
14122
14123 assert_eq!(uv.min, [0.0, 0.0]);
14124 assert_eq!(uv.max, [1.0, 1.0]);
14125 assert_eq!(uv.sample_bounds, [0.0, 0.0, 1.0, 1.0]);
14126 }
14127
14128 fn test_text(z_index: usize) -> TextDraw {
14129 TextDraw {
14130 node_id: 0,
14131 rect: Rect {
14132 x: 0.0,
14133 y: 0.0,
14134 width: 8.0,
14135 height: 8.0,
14136 },
14137 snap_anchor: None,
14138 translated_content_context: false,
14139 text: Arc::new(cranpose_ui::text::AnnotatedString::from("t").render_string()),
14140 color: Color::WHITE,
14141 text_style: cranpose_ui::TextStyle::default(),
14142 font_size: 12.0,
14143 scale: 1.0,
14144 layout_options: cranpose_ui::TextLayoutOptions::default(),
14145 z_index,
14146 clip: None,
14147 }
14148 }
14149
14150 #[test]
14151 fn text_draw_visibility_rejects_text_outside_clip_before_rasterization() {
14152 let viewport = ViewportUniformParams {
14153 width: 320,
14154 height: 240,
14155 offset: [0.0, 0.0],
14156 };
14157 let text_rect = Rect {
14158 x: 0.0,
14159 y: 260.0,
14160 width: 200.0,
14161 height: 40.0,
14162 };
14163 let clip = Some(Rect {
14164 x: 0.0,
14165 y: 0.0,
14166 width: 320.0,
14167 height: 200.0,
14168 });
14169
14170 assert!(
14171 !text_draw_is_visible_in_viewport(text_rect, clip, viewport, 1.0),
14172 "lazy-list beyond-bound text outside the clip must not be rasterized"
14173 );
14174 }
14175
14176 #[test]
14177 fn text_draw_prewarm_accepts_clipped_text_near_viewport() {
14178 let viewport = ViewportUniformParams {
14179 width: 320,
14180 height: 240,
14181 offset: [0.0, 0.0],
14182 };
14183 let text_rect = Rect {
14184 x: 0.0,
14185 y: 260.0,
14186 width: 200.0,
14187 height: 40.0,
14188 };
14189 let clip = Some(Rect {
14190 x: 0.0,
14191 y: 0.0,
14192 width: 320.0,
14193 height: 200.0,
14194 });
14195
14196 assert!(!text_draw_is_visible_in_viewport(
14197 text_rect, clip, viewport, 1.0
14198 ));
14199 assert!(text_draw_should_prewarm_in_viewport(
14200 text_rect, clip, viewport, 1.0
14201 ));
14202 }
14203
14204 #[test]
14205 fn text_draw_prewarm_rejects_far_clipped_text() {
14206 let viewport = ViewportUniformParams {
14207 width: 320,
14208 height: 240,
14209 offset: [0.0, 0.0],
14210 };
14211 let text_rect = Rect {
14212 x: 0.0,
14213 y: 1600.0,
14214 width: 200.0,
14215 height: 40.0,
14216 };
14217 let clip = Some(Rect {
14218 x: 0.0,
14219 y: 0.0,
14220 width: 320.0,
14221 height: 200.0,
14222 });
14223
14224 assert!(!text_draw_should_prewarm_in_viewport(
14225 text_rect, clip, viewport, 1.0
14226 ));
14227 }
14228
14229 #[test]
14230 fn text_draw_visibility_rejects_unclipped_text_outside_viewport() {
14231 let viewport = ViewportUniformParams {
14232 width: 320,
14233 height: 240,
14234 offset: [0.0, 0.0],
14235 };
14236 let text_rect = Rect {
14237 x: 0.0,
14238 y: 241.0,
14239 width: 200.0,
14240 height: 40.0,
14241 };
14242
14243 assert!(
14244 !text_draw_is_visible_in_viewport(text_rect, None, viewport, 1.0),
14245 "unclipped text outside the target viewport must not be rasterized"
14246 );
14247 }
14248
14249 #[test]
14250 fn text_draw_visibility_keeps_partially_visible_text() {
14251 let viewport = ViewportUniformParams {
14252 width: 320,
14253 height: 240,
14254 offset: [0.0, 0.0],
14255 };
14256 let text_rect = Rect {
14257 x: 0.0,
14258 y: 220.0,
14259 width: 200.0,
14260 height: 40.0,
14261 };
14262
14263 assert!(text_draw_is_visible_in_viewport(
14264 text_rect, None, viewport, 1.0
14265 ));
14266 }
14267
14268 fn test_draw_ops(
14269 shapes: &[DrawShape],
14270 images: &[ImageDraw],
14271 texts: &[TextDraw],
14272 shadows: &[ShadowDraw],
14273 ) -> Vec<DrawOp> {
14274 let mut ops = Vec::new();
14275 ops.extend(shapes.iter().enumerate().map(|(index, shape)| DrawOp {
14276 z_index: shape.z_index,
14277 kind: DrawOpKind::Shape(index),
14278 }));
14279 ops.extend(images.iter().enumerate().map(|(index, image)| DrawOp {
14280 z_index: image.z_index,
14281 kind: DrawOpKind::Image(index),
14282 }));
14283 ops.extend(texts.iter().enumerate().map(|(index, text)| DrawOp {
14284 z_index: text.z_index,
14285 kind: DrawOpKind::Text(index),
14286 }));
14287 ops.extend(shadows.iter().enumerate().map(|(index, shadow)| DrawOp {
14288 z_index: shadow.z_index,
14289 kind: DrawOpKind::Shadow(index),
14290 }));
14291 ops.sort_by_key(|op| op.z_index);
14292 ops
14293 }
14294
14295 fn test_layer(local_bounds: Rect, children: Vec<RenderNode>) -> LayerNode {
14296 crate::test_support::layer_node(
14297 local_bounds,
14298 ProjectiveTransform::identity(),
14299 GraphicsLayer::default(),
14300 children,
14301 )
14302 }
14303
14304 fn cacheable_layer(
14305 node_id: cranpose_core::NodeId,
14306 local_bounds: Rect,
14307 children: Vec<RenderNode>,
14308 ) -> LayerNode {
14309 let mut layer = test_layer(local_bounds, children);
14310 layer.node_id = Some(node_id);
14311 layer.cache_policy = cranpose_render_common::graph::CachePolicy::Auto;
14312 layer.recompute_raster_cache_hashes();
14313 layer
14314 }
14315
14316 fn text_layer_with_style(text: AnnotatedString, text_style: TextStyle) -> LayerNode {
14317 test_layer(
14318 Rect {
14319 x: 0.0,
14320 y: 0.0,
14321 width: 64.0,
14322 height: 32.0,
14323 },
14324 vec![RenderNode::Primitive(PrimitiveEntry {
14325 phase: PrimitivePhase::BeforeChildren,
14326 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
14327 node_id: 1,
14328 rect: Rect {
14329 x: 2.0,
14330 y: 3.0,
14331 width: 48.0,
14332 height: 18.0,
14333 },
14334 text: std::rc::Rc::new(text),
14335 text_style,
14336 font_size: 14.0,
14337 layout_options: TextLayoutOptions::default(),
14338 clip: None,
14339 })),
14340 })],
14341 )
14342 }
14343
14344 fn snapped_text_leaf(animated: bool, translated_content_context: bool) -> LayerNode {
14345 LayerNode {
14346 node_id: Some(77),
14347 local_bounds: Rect {
14348 x: 0.0,
14349 y: 0.0,
14350 width: 48.0,
14351 height: 24.0,
14352 },
14353 transform_to_parent: ProjectiveTransform::translation(14.25, 16.5),
14354 motion_context_animated: animated,
14355 translated_content_context,
14356 translated_content_offset: Point::default(),
14357 content_offset: Point::default(),
14358 scene_children_origin: cranpose_ui_graphics::Point::default(),
14359 scene_children_layer_translation: cranpose_ui_graphics::Point::default(),
14360 graphics_layer: GraphicsLayer::default(),
14361 clip_to_bounds: false,
14362 shadow_clip: None,
14363 hit_test: None,
14364 has_hit_targets: false,
14365 isolation: IsolationReasons::default(),
14366 cache_policy: CachePolicy::None,
14367 cache_hashes: LayerRasterCacheHashes::default(),
14368 cache_hashes_valid: false,
14369 children: vec![
14370 RenderNode::Primitive(PrimitiveEntry {
14371 phase: PrimitivePhase::BeforeChildren,
14372 node: PrimitiveNode::Draw(DrawPrimitiveNode {
14373 primitive: DrawPrimitive::RoundRect {
14374 rect: Rect {
14375 x: 0.0,
14376 y: 0.0,
14377 width: 48.0,
14378 height: 24.0,
14379 },
14380 brush: Brush::solid(Color(0.28, 0.30, 0.46, 0.88)),
14381 radii: CornerRadii::uniform(6.0),
14382 stroke: None,
14383 },
14384 clip: None,
14385 }),
14386 }),
14387 RenderNode::Primitive(PrimitiveEntry {
14388 phase: PrimitivePhase::BeforeChildren,
14389 node: PrimitiveNode::Draw(DrawPrimitiveNode {
14390 primitive: DrawPrimitive::Image {
14391 rect: Rect {
14392 x: 2.0,
14393 y: 2.0,
14394 width: 12.0,
14395 height: 12.0,
14396 },
14397 image: ImageBitmap::from_rgba8(
14398 2,
14399 2,
14400 vec![
14401 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255,
14402 255,
14403 ],
14404 )
14405 .expect("image"),
14406 alpha: 1.0,
14407 color_filter: None,
14408 sampling: ImageSampling::Linear,
14409 src_rect: None,
14410 },
14411 clip: None,
14412 }),
14413 }),
14414 RenderNode::Primitive(PrimitiveEntry {
14415 phase: PrimitivePhase::BeforeChildren,
14416 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
14417 node_id: 77,
14418 rect: Rect {
14419 x: 6.0,
14420 y: 4.0,
14421 width: 36.0,
14422 height: 16.0,
14423 },
14424 text: std::rc::Rc::new(AnnotatedString::from("48 px")),
14425 text_style: TextStyle::default(),
14426 font_size: 14.0,
14427 layout_options: TextLayoutOptions::default(),
14428 clip: None,
14429 })),
14430 }),
14431 ],
14432 }
14433 }
14434
14435 fn snapped_text_leaf_root(animated: bool, translated_content_context: bool) -> LayerNode {
14436 let text_leaf = snapped_text_leaf(animated, translated_content_context);
14437 test_layer(
14438 Rect {
14439 x: 0.0,
14440 y: 0.0,
14441 width: 96.0,
14442 height: 64.0,
14443 },
14444 vec![RenderNode::Layer(Box::new(text_leaf))],
14445 )
14446 }
14447
14448 fn translated_content_local_surface_root() -> LayerNode {
14449 let mut effectful_text = text_layer_with_style(
14450 AnnotatedString::from("shadow"),
14451 TextStyle::from_span_style(SpanStyle {
14452 shadow: Some(Shadow {
14453 color: Color::BLACK,
14454 offset: Point::new(1.0, 2.0),
14455 blur_radius: 3.0,
14456 }),
14457 ..SpanStyle::default()
14458 }),
14459 );
14460 effectful_text.translated_content_context = true;
14461
14462 let translated_content = LayerNode {
14463 node_id: Some(78),
14464 local_bounds: Rect {
14465 x: 0.0,
14466 y: 0.0,
14467 width: 96.0,
14468 height: 64.0,
14469 },
14470 transform_to_parent: ProjectiveTransform::translation(14.25, 16.5),
14471 motion_context_animated: false,
14472 translated_content_context: true,
14473 translated_content_offset: Point::default(),
14474 content_offset: Point::default(),
14475 scene_children_origin: cranpose_ui_graphics::Point::default(),
14476 scene_children_layer_translation: cranpose_ui_graphics::Point::default(),
14477 graphics_layer: GraphicsLayer::default(),
14478 clip_to_bounds: false,
14479 shadow_clip: None,
14480 hit_test: None,
14481 has_hit_targets: false,
14482 isolation: IsolationReasons::default(),
14483 cache_policy: CachePolicy::None,
14484 cache_hashes: LayerRasterCacheHashes::default(),
14485 cache_hashes_valid: false,
14486 children: vec![RenderNode::Layer(Box::new(effectful_text))],
14487 };
14488
14489 test_layer(
14490 Rect {
14491 x: 0.0,
14492 y: 0.0,
14493 width: 160.0,
14494 height: 120.0,
14495 },
14496 vec![RenderNode::Layer(Box::new(translated_content))],
14497 )
14498 }
14499
14500 #[test]
14501 fn scissor_rect_for_layer_intersects_with_clip() {
14502 let rect = Rect {
14503 x: 10.0,
14504 y: 10.0,
14505 width: 30.0,
14506 height: 20.0,
14507 };
14508 let clip = Rect {
14509 x: 20.0,
14510 y: 15.0,
14511 width: 100.0,
14512 height: 100.0,
14513 };
14514
14515 let scissor = scissor_rect_for_layer(rect, Some(clip), 1.0, 200, 200);
14516 assert_eq!(scissor, Some((20, 15, 20, 15)));
14517 }
14518
14519 #[test]
14520 fn visible_draw_rect_no_clip_returns_original() {
14521 let rect = Rect {
14522 x: 100.0,
14523 y: 200.0,
14524 width: 300.0,
14525 height: 400.0,
14526 };
14527 assert_eq!(visible_draw_rect(rect, None), Some(rect));
14528 }
14529
14530 #[test]
14531 fn visible_draw_rect_with_clip_intersects() {
14532 let rect = Rect {
14533 x: 0.0,
14534 y: 0.0,
14535 width: 2000.0,
14536 height: 5000.0,
14537 };
14538 let clip = Rect {
14539 x: 0.0,
14540 y: 0.0,
14541 width: 800.0,
14542 height: 600.0,
14543 };
14544 let visible = visible_draw_rect(rect, Some(clip)).expect("should have visible area");
14545 assert_eq!(visible.width, 800.0);
14546 assert_eq!(visible.height, 600.0);
14547 }
14548
14549 #[test]
14550 fn visible_draw_rect_fully_clipped_returns_none() {
14551 let rect = Rect {
14552 x: 1000.0,
14553 y: 1000.0,
14554 width: 200.0,
14555 height: 200.0,
14556 };
14557 let clip = Rect {
14558 x: 0.0,
14559 y: 0.0,
14560 width: 800.0,
14561 height: 600.0,
14562 };
14563 assert!(visible_draw_rect(rect, Some(clip)).is_none());
14564 }
14565
14566 #[test]
14567 fn scene_bounds_respects_clip_on_shapes() {
14568 let mut scene = CompositorScene::new();
14569 scene.shapes.push(DrawShape {
14571 rect: Rect {
14572 x: 10.0,
14573 y: 10.0,
14574 width: 100.0,
14575 height: 50.0,
14576 },
14577 clip: Some(Rect {
14578 x: 0.0,
14579 y: 0.0,
14580 width: 800.0,
14581 height: 600.0,
14582 }),
14583 ..test_shape(0, BlendMode::SrcOver)
14584 });
14585 scene.shapes.push(DrawShape {
14587 rect: Rect {
14588 x: 0.0,
14589 y: 3000.0,
14590 width: 100.0,
14591 height: 50.0,
14592 },
14593 clip: Some(Rect {
14594 x: 0.0,
14595 y: 0.0,
14596 width: 800.0,
14597 height: 600.0,
14598 }),
14599 ..test_shape(1, BlendMode::SrcOver)
14600 });
14601 let bounds = scene_bounds(&scene).expect("should have bounds");
14602 assert!(bounds.y + bounds.height <= 600.0);
14605 }
14606
14607 #[test]
14608 fn scene_bounds_scroll_content_clipped_to_viewport() {
14609 let mut scene = CompositorScene::new();
14612 let viewport_clip = Rect {
14613 x: 0.0,
14614 y: 0.0,
14615 width: 800.0,
14616 height: 600.0,
14617 };
14618 for i in 0..20 {
14619 scene.shapes.push(DrawShape {
14620 rect: Rect {
14621 x: 0.0,
14622 y: i as f32 * 300.0,
14623 width: 800.0,
14624 height: 200.0,
14625 },
14626 clip: Some(viewport_clip),
14627 ..test_shape(i, BlendMode::SrcOver)
14628 });
14629 }
14630 let bounds = scene_bounds(&scene).expect("should have bounds");
14631 assert_eq!(bounds.x, 0.0);
14634 assert_eq!(bounds.y, 0.0);
14635 assert!(bounds.width <= 800.0);
14636 assert!(bounds.height <= 600.0);
14637 }
14638
14639 #[test]
14640 fn scene_bounds_stable_across_scroll_offsets() {
14641 let viewport_clip = Rect {
14644 x: 0.0,
14645 y: 0.0,
14646 width: 400.0,
14647 height: 50.0,
14648 };
14649 let compute_bounds_at_offset = |scroll_x: f32| {
14650 let mut scene = CompositorScene::new();
14651 for i in 0..10 {
14652 scene.shapes.push(DrawShape {
14653 rect: Rect {
14654 x: i as f32 * 100.0 - scroll_x,
14655 y: 0.0,
14656 width: 80.0,
14657 height: 40.0,
14658 },
14659 clip: Some(viewport_clip),
14660 ..test_shape(i, BlendMode::SrcOver)
14661 });
14662 }
14663 scene_bounds(&scene).expect("bounds")
14664 };
14665 let bounds_at_0 = compute_bounds_at_offset(0.0);
14666 let bounds_at_300 = compute_bounds_at_offset(300.0);
14667 let bounds_at_600 = compute_bounds_at_offset(600.0);
14668 assert!(
14670 (bounds_at_0.width - bounds_at_300.width).abs() < 1.0,
14671 "bounds width changed with scroll: {} vs {}",
14672 bounds_at_0.width,
14673 bounds_at_300.width
14674 );
14675 assert!(
14676 (bounds_at_0.width - bounds_at_600.width).abs() < 1.0,
14677 "bounds width changed with scroll: {} vs {}",
14678 bounds_at_0.width,
14679 bounds_at_600.width
14680 );
14681 }
14682
14683 #[test]
14684 fn collect_effect_ranges_respects_excluded_effect() {
14685 let layers = vec![effect_layer(10, 40), effect_layer(20, 30)];
14686 let mut ranges = Vec::new();
14687 collect_effect_ranges(&layers, 10, 40, Some(0), &mut ranges);
14688 assert_eq!(ranges.len(), 1);
14689 assert_eq!(ranges[0], 20..30);
14690 }
14691
14692 #[test]
14693 fn collect_layer_events_includes_nested_when_parent_excluded() {
14694 let effects = vec![effect_layer(10, 40), effect_layer(20, 30)];
14695 let backdrops = vec![backdrop_layer(25)];
14696 let mut events = Vec::new();
14697 collect_layer_events(&effects, &backdrops, 10, 40, Some(0), &mut events);
14698 assert_eq!(events.len(), 2);
14699
14700 match events[0].kind {
14701 LayerEventKind::Effect(index) => assert_eq!(index, 1),
14702 LayerEventKind::Backdrop(_) => panic!("expected nested effect as first event"),
14703 }
14704 match events[1].kind {
14705 LayerEventKind::Backdrop(index) => assert_eq!(index, 0),
14706 LayerEventKind::Effect(_) => panic!("expected backdrop as second event"),
14707 }
14708 }
14709
14710 fn pure_text_leaf(animated: bool, translated_content_context: bool) -> LayerNode {
14711 LayerNode {
14712 node_id: Some(177),
14713 local_bounds: Rect {
14714 x: 0.0,
14715 y: 0.0,
14716 width: 96.0,
14717 height: 32.0,
14718 },
14719 transform_to_parent: ProjectiveTransform::translation(11.4, 23.6),
14720 motion_context_animated: animated,
14721 translated_content_context,
14722 translated_content_offset: Point::default(),
14723 content_offset: Point::default(),
14724 scene_children_origin: cranpose_ui_graphics::Point::default(),
14725 scene_children_layer_translation: cranpose_ui_graphics::Point::default(),
14726 graphics_layer: GraphicsLayer::default(),
14727 clip_to_bounds: false,
14728 shadow_clip: None,
14729 hit_test: None,
14730 has_hit_targets: false,
14731 isolation: IsolationReasons::default(),
14732 cache_policy: CachePolicy::None,
14733 cache_hashes: LayerRasterCacheHashes::default(),
14734 cache_hashes_valid: false,
14735 children: vec![RenderNode::Primitive(PrimitiveEntry {
14736 phase: PrimitivePhase::BeforeChildren,
14737 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
14738 node_id: 177,
14739 rect: Rect {
14740 x: 0.0,
14741 y: 0.0,
14742 width: 96.0,
14743 height: 24.0,
14744 },
14745 clip: None,
14746 text: std::rc::Rc::new(AnnotatedString::from("Pure text")),
14747 text_style: TextStyle::default(),
14748 font_size: 14.0,
14749 layout_options: TextLayoutOptions::default(),
14750 })),
14751 })],
14752 }
14753 }
14754
14755 fn pure_text_leaf_root(animated: bool, translated_content_context: bool) -> LayerNode {
14756 let text_leaf = pure_text_leaf(animated, translated_content_context);
14757 test_layer(
14758 Rect {
14759 x: 0.0,
14760 y: 0.0,
14761 width: 160.0,
14762 height: 96.0,
14763 },
14764 vec![RenderNode::Layer(Box::new(text_leaf))],
14765 )
14766 }
14767
14768 #[test]
14769 fn collect_layer_events_sorts_backdrop_before_effect_at_same_z() {
14770 let effects = vec![effect_layer(10, 20)];
14771 let backdrops = vec![backdrop_layer(10)];
14772 let mut events = Vec::new();
14773 collect_layer_events(&effects, &backdrops, 0, 30, None, &mut events);
14774 assert_eq!(events.len(), 2);
14775
14776 match events[0].kind {
14777 LayerEventKind::Backdrop(_) => {}
14778 LayerEventKind::Effect(_) => panic!("expected backdrop to run before effect"),
14779 }
14780 match events[1].kind {
14781 LayerEventKind::Effect(_) => {}
14782 LayerEventKind::Backdrop(_) => panic!("expected effect as second event"),
14783 }
14784 }
14785
14786 #[test]
14787 fn collect_layer_events_prefers_outer_effect_when_same_start_z() {
14788 let effects = vec![effect_layer(10, 20), effect_layer(10, 40)];
14791 let mut events = Vec::new();
14792 collect_layer_events(&effects, &[], 0, 50, None, &mut events);
14793
14794 assert_eq!(events.len(), 2);
14795 match events[0].kind {
14796 LayerEventKind::Effect(index) => assert_eq!(index, 1),
14797 LayerEventKind::Backdrop(_) => panic!("expected outer effect first"),
14798 }
14799 match events[1].kind {
14800 LayerEventKind::Effect(index) => assert_eq!(index, 0),
14801 LayerEventKind::Backdrop(_) => panic!("expected child effect second"),
14802 }
14803 }
14804
14805 #[test]
14806 fn collect_layer_events_prefers_later_effect_when_ranges_match() {
14807 let effects = vec![effect_layer(10, 20), effect_layer(10, 20)];
14808 let mut events = Vec::new();
14809 collect_layer_events(&effects, &[], 0, 30, None, &mut events);
14810
14811 assert_eq!(events.len(), 2);
14812 match events[0].kind {
14813 LayerEventKind::Effect(index) => assert_eq!(index, 1),
14814 LayerEventKind::Backdrop(_) => panic!("expected later effect first"),
14815 }
14816 match events[1].kind {
14817 LayerEventKind::Effect(index) => assert_eq!(index, 0),
14818 LayerEventKind::Backdrop(_) => panic!("expected earlier effect second"),
14819 }
14820 }
14821
14822 #[test]
14823 fn has_backdrop_layer_in_range_detects_nested_layers() {
14824 let backdrops = vec![backdrop_layer(5), backdrop_layer(15), backdrop_layer(25)];
14825 assert!(has_backdrop_layer_in_range(&backdrops, 10, 20));
14826 assert!(has_backdrop_layer_in_range(&backdrops, 0, 6));
14827 assert!(!has_backdrop_layer_in_range(&backdrops, 20, 25));
14828 }
14829
14830 #[test]
14831 fn layer_contains_descendant_backdrop_ignores_self_backdrop() {
14832 let mut self_backdrop = test_layer(
14833 Rect {
14834 x: 0.0,
14835 y: 0.0,
14836 width: 10.0,
14837 height: 10.0,
14838 },
14839 vec![],
14840 );
14841 self_backdrop.graphics_layer.backdrop_effect = Some(RenderEffect::blur(2.0));
14842 assert!(!layer_contains_descendant_backdrop(&self_backdrop));
14843
14844 let mut child = test_layer(
14845 Rect {
14846 x: 0.0,
14847 y: 0.0,
14848 width: 8.0,
14849 height: 8.0,
14850 },
14851 vec![],
14852 );
14853 child.graphics_layer.backdrop_effect = Some(RenderEffect::blur(2.0));
14854
14855 let parent = test_layer(
14856 Rect {
14857 x: 0.0,
14858 y: 0.0,
14859 width: 20.0,
14860 height: 20.0,
14861 },
14862 vec![RenderNode::Layer(Box::new(child))],
14863 );
14864 assert!(layer_contains_descendant_backdrop(&parent));
14865 }
14866
14867 fn child_layer_composite(
14868 layer: &LayerNode,
14869 z_index: usize,
14870 rect: Rect,
14871 needs_nested_underlay: bool,
14872 ) -> crate::normalized_scene::ChildLayerComposite {
14873 let mut requirements_cache = cranpose_core::collections::map::HashMap::new();
14874 let surface_requirements =
14875 crate::surface_plan::layer_surface_requirements_cached(layer, &mut requirements_cache);
14876 crate::normalized_scene::ChildLayerComposite {
14877 z_index,
14878 logical_rect: Rect {
14879 x: 0.0,
14880 y: 0.0,
14881 width: rect.width,
14882 height: rect.height,
14883 },
14884 dest_quad: rect_to_quad(rect),
14885 snap_anchor: None,
14886 composite_snap_origin: None,
14887 backdrop_rect: rect,
14888 visual_clip: None,
14889 surface_clip: None,
14890 shadow_draws: Vec::new(),
14891 needs_nested_underlay,
14892 node_id: layer.node_id,
14893 backdrop: layer.backdrop().cloned(),
14894 has_effect: layer.effect().is_some(),
14895 effect_contains_runtime_shader: layer
14896 .effect()
14897 .is_some_and(|effect| effect.contains_runtime_shader()),
14898 target_content_hash: layer.target_content_hash(),
14899 effect_hash: layer.effect_hash(),
14900 motion_source_content_hash: Some(layer.motion_source_content_hash()),
14901 contains_descendant_backdrop: layer_contains_descendant_backdrop(layer),
14902 cache_policy: layer.cache_policy,
14903 surface_requirements,
14904 rounded_clip: crate::surface_executor::backend::LayerSurfaceRoundedClip::from_layer(
14905 layer,
14906 ),
14907 isolation: cranpose_render_common::layer_composition::effective_layer_isolation(
14908 &layer.graphics_layer,
14909 ),
14910 translated_content_context: layer.translated_content_context,
14911 own_translated_content_axes: crate::surface_plan::translated_content_axes_for_layer(
14912 layer,
14913 ),
14914 clip_rect: layer.clip_rect(),
14915 local_bounds: layer.local_bounds,
14916 surface_scale: crate::surface_plan::layer_surface_scale(layer),
14917 source: crate::normalized_scene::LoweredChildSource::default(),
14918 }
14919 }
14920
14921 #[test]
14922 fn root_direct_preflight_allows_first_translated_child_underlay() {
14923 let child = test_layer(
14924 Rect {
14925 x: 0.0,
14926 y: 0.0,
14927 width: 400.0,
14928 height: 280.0,
14929 },
14930 vec![],
14931 );
14932 let collected = CollectedLayer {
14933 scene: CompositorScene::new(),
14934 child_layers: vec![child_layer_composite(
14935 &child,
14936 3,
14937 Rect {
14938 x: 48.0,
14939 y: 96.0,
14940 width: 400.0,
14941 height: 280.0,
14942 },
14943 true,
14944 )],
14945 };
14946
14947 assert!(direct_root_child_underlays_are_supported(&collected));
14948 }
14949
14950 #[test]
14951 fn root_direct_preflight_allows_axis_aligned_prior_child_underlay() {
14952 let first = test_layer(
14953 Rect {
14954 x: 0.0,
14955 y: 0.0,
14956 width: 80.0,
14957 height: 40.0,
14958 },
14959 vec![],
14960 );
14961 let backdrop_child = test_layer(
14962 Rect {
14963 x: 0.0,
14964 y: 0.0,
14965 width: 400.0,
14966 height: 280.0,
14967 },
14968 vec![],
14969 );
14970 let collected = CollectedLayer {
14971 scene: CompositorScene::new(),
14972 child_layers: vec![
14973 child_layer_composite(
14974 &first,
14975 1,
14976 Rect {
14977 x: 8.0,
14978 y: 16.0,
14979 width: 80.0,
14980 height: 40.0,
14981 },
14982 false,
14983 ),
14984 child_layer_composite(
14985 &backdrop_child,
14986 4,
14987 Rect {
14988 x: 48.0,
14989 y: 96.0,
14990 width: 400.0,
14991 height: 280.0,
14992 },
14993 true,
14994 ),
14995 ],
14996 };
14997
14998 assert!(direct_root_child_underlays_are_supported(&collected));
14999 }
15000
15001 #[test]
15002 fn root_direct_preflight_rejects_effectful_prior_child_underlay() {
15003 let mut first = test_layer(
15004 Rect {
15005 x: 0.0,
15006 y: 0.0,
15007 width: 80.0,
15008 height: 40.0,
15009 },
15010 vec![],
15011 );
15012 first.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
15013 let backdrop_child = test_layer(
15014 Rect {
15015 x: 0.0,
15016 y: 0.0,
15017 width: 400.0,
15018 height: 280.0,
15019 },
15020 vec![],
15021 );
15022 let collected = CollectedLayer {
15023 scene: CompositorScene::new(),
15024 child_layers: vec![
15025 child_layer_composite(
15026 &first,
15027 1,
15028 Rect {
15029 x: 64.0,
15030 y: 112.0,
15031 width: 80.0,
15032 height: 40.0,
15033 },
15034 false,
15035 ),
15036 child_layer_composite(
15037 &backdrop_child,
15038 4,
15039 Rect {
15040 x: 48.0,
15041 y: 96.0,
15042 width: 400.0,
15043 height: 280.0,
15044 },
15045 true,
15046 ),
15047 ],
15048 };
15049
15050 assert!(!direct_root_child_underlays_are_supported(&collected));
15051 }
15052
15053 #[test]
15054 fn root_direct_preflight_ignores_non_overlapping_effectful_prior_child_underlay() {
15055 let mut first = test_layer(
15056 Rect {
15057 x: 0.0,
15058 y: 0.0,
15059 width: 80.0,
15060 height: 40.0,
15061 },
15062 vec![],
15063 );
15064 first.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
15065 let backdrop_child = test_layer(
15066 Rect {
15067 x: 0.0,
15068 y: 0.0,
15069 width: 400.0,
15070 height: 280.0,
15071 },
15072 vec![],
15073 );
15074 let collected = CollectedLayer {
15075 scene: CompositorScene::new(),
15076 child_layers: vec![
15077 child_layer_composite(
15078 &first,
15079 1,
15080 Rect {
15081 x: 8.0,
15082 y: 16.0,
15083 width: 80.0,
15084 height: 40.0,
15085 },
15086 false,
15087 ),
15088 child_layer_composite(
15089 &backdrop_child,
15090 4,
15091 Rect {
15092 x: 48.0,
15093 y: 96.0,
15094 width: 400.0,
15095 height: 280.0,
15096 },
15097 true,
15098 ),
15099 ],
15100 };
15101
15102 assert!(direct_root_child_underlays_are_supported(&collected));
15103 }
15104
15105 #[test]
15106 fn root_direct_preflight_rejects_underlay_that_would_replay_prior_scene_effects() {
15107 let backdrop_child = test_layer(
15108 Rect {
15109 x: 0.0,
15110 y: 0.0,
15111 width: 400.0,
15112 height: 280.0,
15113 },
15114 vec![],
15115 );
15116 let mut scene = CompositorScene::new();
15117 scene.next_z = 1;
15118 scene.push_effect_layer(
15119 Rect {
15120 x: 0.0,
15121 y: 0.0,
15122 width: 120.0,
15123 height: 120.0,
15124 },
15125 None,
15126 Some(RenderEffect::blur(2.0)),
15127 BlendMode::SrcOver,
15128 1.0,
15129 0,
15130 1,
15131 );
15132 let collected = CollectedLayer {
15133 scene,
15134 child_layers: vec![child_layer_composite(
15135 &backdrop_child,
15136 4,
15137 Rect {
15138 x: 48.0,
15139 y: 96.0,
15140 width: 400.0,
15141 height: 280.0,
15142 },
15143 true,
15144 )],
15145 };
15146
15147 assert!(!direct_root_child_underlays_are_supported(&collected));
15148 }
15149
15150 #[test]
15151 fn root_direct_eligibility_does_not_reject_descendant_backdrop() {
15152 let mut backdrop = test_layer(
15153 Rect {
15154 x: 0.0,
15155 y: 0.0,
15156 width: 40.0,
15157 height: 40.0,
15158 },
15159 vec![],
15160 );
15161 backdrop.graphics_layer.backdrop_effect = Some(RenderEffect::blur(4.0));
15162 let child = test_layer(
15163 Rect {
15164 x: 0.0,
15165 y: 0.0,
15166 width: 120.0,
15167 height: 96.0,
15168 },
15169 vec![RenderNode::Layer(Box::new(backdrop))],
15170 );
15171 let root = test_layer(
15172 Rect {
15173 x: 0.0,
15174 y: 0.0,
15175 width: 240.0,
15176 height: 160.0,
15177 },
15178 vec![RenderNode::Layer(Box::new(child))],
15179 );
15180 let mut cache = HashMap::new();
15181
15182 assert!(root_can_render_directly_cached(&root, &mut cache));
15183 }
15184
15185 #[test]
15186 fn root_direct_scene_events_allow_root_local_effects() {
15187 let mut scene = CompositorScene::new();
15188 scene.effect_layers.push(EffectLayer {
15189 rect: Rect {
15190 x: 20.0,
15191 y: 30.0,
15192 width: 120.0,
15193 height: 80.0,
15194 },
15195 clip: None,
15196 snap_anchor: None,
15197 effect: Some(RenderEffect::blur(6.0)),
15198 blend_mode: BlendMode::SrcOver,
15199 composite_alpha: 1.0,
15200 z_start: 0,
15201 z_end: 1,
15202 requirements: SurfaceRequirementSet::default().with(SurfaceRequirement::RenderEffect),
15203 });
15204
15205 assert!(root_direct_scene_events_are_supported(&scene));
15206 }
15207
15208 #[test]
15209 fn root_direct_scene_events_reject_root_local_backdrops() {
15210 let mut scene = CompositorScene::new();
15211 scene.backdrop_layers.push(BackdropLayer {
15212 node_id: Some(99),
15213 rect: Rect {
15214 x: 20.0,
15215 y: 30.0,
15216 width: 120.0,
15217 height: 80.0,
15218 },
15219 clip: None,
15220 snap_anchor: None,
15221 effect: RenderEffect::blur(6.0),
15222 z_index: 1,
15223 });
15224
15225 assert!(!root_direct_scene_events_are_supported(&scene));
15226 }
15227
15228 #[test]
15229 fn estimate_layer_surface_rect_includes_transformed_child_bounds() {
15230 let mut child = test_layer(
15231 Rect {
15232 x: 0.0,
15233 y: 0.0,
15234 width: 10.0,
15235 height: 6.0,
15236 },
15237 vec![RenderNode::Primitive(PrimitiveEntry {
15238 phase: PrimitivePhase::BeforeChildren,
15239 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15240 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15241 rect: Rect {
15242 x: 0.0,
15243 y: 0.0,
15244 width: 10.0,
15245 height: 6.0,
15246 },
15247 brush: Brush::solid(Color::WHITE),
15248 stroke: None,
15249 },
15250 clip: None,
15251 }),
15252 })],
15253 );
15254 child.transform_to_parent = ProjectiveTransform::translation(18.0, 7.0);
15255
15256 let parent = test_layer(
15257 Rect {
15258 x: 0.0,
15259 y: 0.0,
15260 width: 4.0,
15261 height: 4.0,
15262 },
15263 vec![RenderNode::Layer(Box::new(child))],
15264 );
15265
15266 assert_eq!(
15267 estimate_layer_surface_rect(&parent),
15268 Rect {
15269 x: 18.0,
15270 y: 7.0,
15271 width: 10.0,
15272 height: 6.0,
15273 }
15274 );
15275 }
15276
15277 #[test]
15278 fn estimate_layer_surface_rect_clips_translated_clip_layers_without_hidden_leading_content() {
15279 let mut layer = test_layer(
15280 Rect {
15281 x: 0.0,
15282 y: 0.0,
15283 width: 120.0,
15284 height: 72.0,
15285 },
15286 vec![RenderNode::Primitive(PrimitiveEntry {
15287 phase: PrimitivePhase::BeforeChildren,
15288 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15289 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15290 rect: Rect {
15291 x: 24.0,
15292 y: 0.0,
15293 width: 200.0,
15294 height: 480.0,
15295 },
15296 brush: Brush::solid(Color::WHITE),
15297 stroke: None,
15298 },
15299 clip: None,
15300 }),
15301 })],
15302 );
15303 layer.translated_content_context = true;
15304 layer.motion_context_animated = true;
15305 layer.clip_to_bounds = true;
15306
15307 assert_eq!(
15308 estimate_layer_surface_rect(&layer),
15309 Rect {
15310 x: 24.0,
15311 y: 0.0,
15312 width: 96.0,
15313 height: 72.0,
15314 }
15315 );
15316 }
15317
15318 #[test]
15319 fn estimate_layer_surface_rect_clips_active_horizontal_scroll_content() {
15320 let mut layer = test_layer(
15321 Rect {
15322 x: 0.0,
15323 y: 0.0,
15324 width: 120.0,
15325 height: 72.0,
15326 },
15327 vec![RenderNode::Primitive(PrimitiveEntry {
15328 phase: PrimitivePhase::BeforeChildren,
15329 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15330 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15331 rect: Rect {
15332 x: -24.0,
15333 y: 0.0,
15334 width: 200.0,
15335 height: 480.0,
15336 },
15337 brush: Brush::solid(Color::WHITE),
15338 stroke: None,
15339 },
15340 clip: None,
15341 }),
15342 })],
15343 );
15344 layer.translated_content_context = true;
15345 layer.motion_context_animated = true;
15346 layer.clip_to_bounds = true;
15347
15348 assert_eq!(
15349 estimate_layer_surface_rect(&layer),
15350 Rect {
15351 x: 0.0,
15352 y: 0.0,
15353 width: 120.0,
15354 height: 72.0,
15355 }
15356 );
15357 }
15358
15359 #[test]
15360 fn estimate_layer_surface_rect_clips_active_vertical_scroll_content() {
15361 let mut layer = test_layer(
15362 Rect {
15363 x: 0.0,
15364 y: 0.0,
15365 width: 120.0,
15366 height: 72.0,
15367 },
15368 vec![RenderNode::Primitive(PrimitiveEntry {
15369 phase: PrimitivePhase::BeforeChildren,
15370 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15371 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15372 rect: Rect {
15373 x: 0.0,
15374 y: -24.0,
15375 width: 120.0,
15376 height: 200.0,
15377 },
15378 brush: Brush::solid(Color::WHITE),
15379 stroke: None,
15380 },
15381 clip: None,
15382 }),
15383 })],
15384 );
15385 layer.translated_content_context = true;
15386 layer.motion_context_animated = true;
15387 layer.clip_to_bounds = true;
15388
15389 assert_eq!(
15390 estimate_layer_surface_rect(&layer),
15391 Rect {
15392 x: 0.0,
15393 y: 0.0,
15394 width: 120.0,
15395 height: 72.0,
15396 }
15397 );
15398 }
15399
15400 #[test]
15401 fn estimate_layer_surface_rect_keeps_shallow_scroll_capture_origin_stable() {
15402 fn shallow_scroll_surface_rect(content_y: f32) -> Rect {
15403 let mut layer = test_layer(
15404 Rect {
15405 x: 0.0,
15406 y: 0.0,
15407 width: 120.0,
15408 height: 72.0,
15409 },
15410 vec![RenderNode::Primitive(PrimitiveEntry {
15411 phase: PrimitivePhase::BeforeChildren,
15412 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15413 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15414 rect: Rect {
15415 x: 0.0,
15416 y: content_y,
15417 width: 120.0,
15418 height: 200.0,
15419 },
15420 brush: Brush::solid(Color::WHITE),
15421 stroke: None,
15422 },
15423 clip: None,
15424 }),
15425 })],
15426 );
15427 layer.translated_content_context = true;
15428 layer.motion_context_animated = true;
15429 layer.clip_to_bounds = true;
15430 estimate_layer_surface_rect(&layer)
15431 }
15432
15433 assert_eq!(
15434 shallow_scroll_surface_rect(-24.0),
15435 shallow_scroll_surface_rect(-25.0),
15436 "shallow scroll capture bounds must not move the offscreen surface origin on adjacent scroll positions"
15437 );
15438 }
15439
15440 #[test]
15441 fn estimate_layer_surface_rect_clips_active_xy_scroll_content() {
15442 let mut layer = test_layer(
15443 Rect {
15444 x: 0.0,
15445 y: 0.0,
15446 width: 120.0,
15447 height: 72.0,
15448 },
15449 vec![RenderNode::Primitive(PrimitiveEntry {
15450 phase: PrimitivePhase::BeforeChildren,
15451 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15452 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15453 rect: Rect {
15454 x: -16.0,
15455 y: -24.0,
15456 width: 180.0,
15457 height: 240.0,
15458 },
15459 brush: Brush::solid(Color::WHITE),
15460 stroke: None,
15461 },
15462 clip: None,
15463 }),
15464 })],
15465 );
15466 layer.translated_content_context = true;
15467 layer.motion_context_animated = true;
15468 layer.clip_to_bounds = true;
15469
15470 assert_eq!(
15471 estimate_layer_surface_rect(&layer),
15472 Rect {
15473 x: 0.0,
15474 y: 0.0,
15475 width: 120.0,
15476 height: 72.0,
15477 }
15478 );
15479 }
15480
15481 #[test]
15482 fn estimate_layer_surface_rect_clips_deep_hidden_active_scroll_content() {
15483 let mut layer = test_layer(
15484 Rect {
15485 x: 0.0,
15486 y: 0.0,
15487 width: 120.0,
15488 height: 72.0,
15489 },
15490 vec![RenderNode::Primitive(PrimitiveEntry {
15491 phase: PrimitivePhase::BeforeChildren,
15492 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15493 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15494 rect: Rect {
15495 x: 0.0,
15496 y: -1200.0,
15497 width: 120.0,
15498 height: 1400.0,
15499 },
15500 brush: Brush::solid(Color::WHITE),
15501 stroke: None,
15502 },
15503 clip: None,
15504 }),
15505 })],
15506 );
15507 layer.translated_content_context = true;
15508 layer.motion_context_animated = true;
15509 layer.clip_to_bounds = true;
15510
15511 assert_eq!(
15512 estimate_layer_surface_rect(&layer),
15513 Rect {
15514 x: 0.0,
15515 y: 0.0,
15516 width: 120.0,
15517 height: 72.0,
15518 }
15519 );
15520 }
15521
15522 #[test]
15523 fn estimate_layer_surface_rect_keeps_deep_scroll_capture_origin_stable() {
15524 fn deep_scroll_surface_rect(content_y: f32) -> Rect {
15525 let mut layer = test_layer(
15526 Rect {
15527 x: 0.0,
15528 y: 0.0,
15529 width: 120.0,
15530 height: 72.0,
15531 },
15532 vec![RenderNode::Primitive(PrimitiveEntry {
15533 phase: PrimitivePhase::BeforeChildren,
15534 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15535 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15536 rect: Rect {
15537 x: 0.0,
15538 y: content_y,
15539 width: 120.0,
15540 height: 1400.0,
15541 },
15542 brush: Brush::solid(Color::WHITE),
15543 stroke: None,
15544 },
15545 clip: None,
15546 }),
15547 })],
15548 );
15549 layer.translated_content_context = true;
15550 layer.motion_context_animated = true;
15551 layer.clip_to_bounds = true;
15552 estimate_layer_surface_rect(&layer)
15553 }
15554
15555 assert_eq!(
15556 deep_scroll_surface_rect(-1200.0),
15557 deep_scroll_surface_rect(-1201.0),
15558 "deep scroll capture bounds must not re-phase the offscreen surface origin on adjacent scroll positions"
15559 );
15560 }
15561
15562 #[test]
15563 fn motion_stable_capture_bounds_bounds_shadows_for_clipped_effect_layer() {
15564 let mut layer = test_layer(
15565 Rect {
15566 x: 0.0,
15567 y: 0.0,
15568 width: 120.0,
15569 height: 72.0,
15570 },
15571 vec![],
15572 );
15573 layer.clip_to_bounds = true;
15574 layer.graphics_layer.clip = true;
15575 layer.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
15576
15577 let mut shadow_shape = test_shape(0, BlendMode::SrcOver);
15578 shadow_shape.rect = Rect {
15579 x: -24.0,
15580 y: -1200.0,
15581 width: 180.0,
15582 height: 1400.0,
15583 };
15584 let mut scene = CompositorScene::new();
15585 scene
15586 .shadow_draws
15587 .push(test_shadow_draw(vec![(shadow_shape, BlendMode::SrcOver)]));
15588
15589 let requirements = SurfaceRequirementSet::default()
15590 .with(SurfaceRequirement::RenderEffect)
15591 .with(SurfaceRequirement::MotionStableCapture);
15592
15593 assert_eq!(
15594 motion_stable_capture_bounds(
15595 &layer,
15596 &scene,
15597 &[],
15598 requirements,
15599 TranslatedContentAxes::default(),
15600 None,
15601 ),
15602 Some(Rect {
15603 x: -360.0,
15604 y: -216.0,
15605 width: 480.0,
15606 height: 288.0,
15607 })
15608 );
15609 }
15610
15611 #[test]
15612 fn vertical_motion_stable_capture_uses_viewport_cross_axis_bounds() {
15613 let mut layer = test_layer(
15614 Rect {
15615 x: 0.0,
15616 y: 0.0,
15617 width: 200.0,
15618 height: 100.0,
15619 },
15620 vec![],
15621 );
15622 layer.clip_to_bounds = true;
15623 layer.graphics_layer.clip = true;
15624
15625 let mut shape = test_shape(0, BlendMode::SrcOver);
15626 shape.rect = Rect {
15627 x: 60.0,
15628 y: -80.0,
15629 width: 80.0,
15630 height: 220.0,
15631 };
15632 let mut scene = CompositorScene::new();
15633 scene.shapes.push(shape);
15634
15635 let requirements =
15636 SurfaceRequirementSet::default().with(SurfaceRequirement::MotionStableCapture);
15637
15638 assert_eq!(
15639 motion_stable_capture_bounds(
15640 &layer,
15641 &scene,
15642 &[],
15643 requirements,
15644 TranslatedContentAxes { x: false, y: true },
15645 None,
15646 ),
15647 Some(Rect {
15648 x: -96.0,
15649 y: -64.0,
15650 width: 296.0,
15651 height: 164.0,
15652 })
15653 );
15654 }
15655
15656 #[test]
15657 fn vertical_motion_stable_capture_uses_external_surface_clip() {
15658 let layer = test_layer(
15659 Rect {
15660 x: 0.0,
15661 y: 0.0,
15662 width: 200.0,
15663 height: 100.0,
15664 },
15665 vec![],
15666 );
15667
15668 let mut shape = test_shape(0, BlendMode::SrcOver);
15669 shape.rect = Rect {
15670 x: 60.0,
15671 y: -80.0,
15672 width: 80.0,
15673 height: 220.0,
15674 };
15675 let mut scene = CompositorScene::new();
15676 scene.shapes.push(shape);
15677
15678 let requirements =
15679 SurfaceRequirementSet::default().with(SurfaceRequirement::MotionStableCapture);
15680
15681 assert_eq!(
15682 motion_stable_capture_bounds(
15683 &layer,
15684 &scene,
15685 &[],
15686 requirements,
15687 TranslatedContentAxes { x: false, y: true },
15688 Some(Rect {
15689 x: 0.0,
15690 y: 0.0,
15691 width: 200.0,
15692 height: 100.0,
15693 }),
15694 ),
15695 Some(Rect {
15696 x: -96.0,
15697 y: -64.0,
15698 width: 296.0,
15699 height: 164.0,
15700 })
15701 );
15702 }
15703
15704 #[test]
15705 fn estimate_layer_surface_rect_expands_for_child_layer_shadow() {
15706 let mut child = test_layer(
15707 Rect {
15708 x: 0.0,
15709 y: 0.0,
15710 width: 12.0,
15711 height: 8.0,
15712 },
15713 vec![],
15714 );
15715 child.transform_to_parent = ProjectiveTransform::translation(20.0, 9.0);
15716 child.graphics_layer.shadow_elevation = 6.0;
15717
15718 let parent = test_layer(
15719 Rect {
15720 x: 0.0,
15721 y: 0.0,
15722 width: 4.0,
15723 height: 4.0,
15724 },
15725 vec![RenderNode::Layer(Box::new(child))],
15726 );
15727
15728 let rect = estimate_layer_surface_rect(&parent);
15729 assert!(rect.x < 20.0);
15730 assert!(rect.y < 9.0);
15731 assert!(rect.width > 12.0);
15732 assert!(rect.height > 8.0);
15733 }
15734
15735 #[test]
15736 fn estimate_layer_surface_rect_respects_local_bounds_for_effect_layers() {
15737 let mut layer = test_layer(
15738 Rect {
15739 x: 0.0,
15740 y: 0.0,
15741 width: 28.0,
15742 height: 28.0,
15743 },
15744 vec![RenderNode::Primitive(PrimitiveEntry {
15745 phase: PrimitivePhase::BeforeChildren,
15746 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15747 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15748 rect: Rect {
15749 x: 10.0,
15750 y: 10.0,
15751 width: 10.0,
15752 height: 10.0,
15753 },
15754 brush: Brush::solid(Color::WHITE),
15755 stroke: None,
15756 },
15757 clip: None,
15758 }),
15759 })],
15760 );
15761 layer.graphics_layer.render_effect = Some(RenderEffect::blur(12.0));
15762
15763 assert_eq!(
15764 estimate_layer_surface_rect(&layer),
15765 Rect {
15766 x: 0.0,
15767 y: 0.0,
15768 width: 28.0,
15769 height: 28.0,
15770 }
15771 );
15772 }
15773
15774 #[test]
15775 fn layer_raster_cache_candidate_ignores_parent_transform() {
15776 let primitive = PrimitiveEntry {
15777 phase: PrimitivePhase::BeforeChildren,
15778 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15779 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15780 rect: Rect {
15781 x: 2.0,
15782 y: 3.0,
15783 width: 6.0,
15784 height: 4.0,
15785 },
15786 brush: Brush::solid(Color::BLACK),
15787 stroke: None,
15788 },
15789 clip: None,
15790 }),
15791 };
15792 let base = cacheable_layer(
15793 41,
15794 Rect {
15795 x: 0.0,
15796 y: 0.0,
15797 width: 20.0,
15798 height: 20.0,
15799 },
15800 vec![RenderNode::Primitive(primitive.clone())],
15801 );
15802 let mut moved = base.clone();
15803 moved.transform_to_parent = ProjectiveTransform::translation(32.0, 18.0);
15804
15805 assert_eq!(
15806 layer_raster_cache_candidate(&base, 1.25, false, false),
15807 layer_raster_cache_candidate(&moved, 1.25, false, false)
15808 );
15809 }
15810
15811 #[test]
15812 fn layer_raster_cache_candidate_changes_for_translated_content_offset() {
15813 let primitive = PrimitiveEntry {
15814 phase: PrimitivePhase::BeforeChildren,
15815 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15816 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15817 rect: Rect {
15818 x: 2.0,
15819 y: 3.0,
15820 width: 6.0,
15821 height: 4.0,
15822 },
15823 brush: Brush::solid(Color::BLACK),
15824 stroke: None,
15825 },
15826 clip: None,
15827 }),
15828 };
15829 let mut base = cacheable_layer(
15830 42,
15831 Rect {
15832 x: 0.0,
15833 y: 0.0,
15834 width: 20.0,
15835 height: 20.0,
15836 },
15837 vec![RenderNode::Primitive(primitive)],
15838 );
15839 base.translated_content_context = true;
15840 base.translated_content_offset = Point::new(0.0, -8.0);
15841 base.recompute_raster_cache_hashes();
15842
15843 let mut moved = base.clone();
15844 moved.translated_content_offset = Point::new(0.0, -16.0);
15845 moved.recompute_raster_cache_hashes();
15846
15847 assert_ne!(
15848 layer_raster_cache_candidate(&base, 1.25, false, false),
15849 layer_raster_cache_candidate(&moved, 1.25, false, false),
15850 "full-surface layer cache candidates must not alias different scroll offsets"
15851 );
15852 }
15853
15854 #[test]
15855 fn layer_raster_cache_candidate_changes_for_child_transform() {
15856 let mut child = cacheable_layer(
15857 8,
15858 Rect {
15859 x: 0.0,
15860 y: 0.0,
15861 width: 12.0,
15862 height: 10.0,
15863 },
15864 vec![],
15865 );
15866 child.transform_to_parent = ProjectiveTransform::translation(4.0, 6.0);
15867 let base = cacheable_layer(
15868 7,
15869 Rect {
15870 x: 0.0,
15871 y: 0.0,
15872 width: 20.0,
15873 height: 20.0,
15874 },
15875 vec![RenderNode::Layer(Box::new(child.clone()))],
15876 );
15877 let mut moved_child = child;
15878 moved_child.transform_to_parent = ProjectiveTransform::translation(9.0, 6.0);
15879 let moved = cacheable_layer(
15880 7,
15881 Rect {
15882 x: 0.0,
15883 y: 0.0,
15884 width: 20.0,
15885 height: 20.0,
15886 },
15887 vec![RenderNode::Layer(Box::new(moved_child))],
15888 );
15889
15890 assert_ne!(
15891 layer_raster_cache_candidate(&base, 1.0, false, false),
15892 layer_raster_cache_candidate(&moved, 1.0, false, false)
15893 );
15894 }
15895
15896 #[test]
15897 fn layer_raster_cache_candidate_rejects_external_backdrop_dependency() {
15898 let mut child = cacheable_layer(
15899 12,
15900 Rect {
15901 x: 0.0,
15902 y: 0.0,
15903 width: 8.0,
15904 height: 8.0,
15905 },
15906 vec![],
15907 );
15908 child.graphics_layer.backdrop_effect = Some(RenderEffect::blur(2.0));
15909 let parent = cacheable_layer(
15910 11,
15911 Rect {
15912 x: 0.0,
15913 y: 0.0,
15914 width: 16.0,
15915 height: 16.0,
15916 },
15917 vec![RenderNode::Layer(Box::new(child))],
15918 );
15919
15920 assert!(layer_raster_cache_candidate(&parent, 1.0, false, false).is_some());
15921 assert!(layer_raster_cache_candidate(&parent, 1.0, true, false).is_none());
15922 }
15923
15924 #[test]
15925 fn layer_raster_cache_candidate_does_not_force_translation_only_text_surfaces() {
15926 let text = RenderNode::Primitive(PrimitiveEntry {
15927 phase: PrimitivePhase::BeforeChildren,
15928 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
15929 node_id: 77,
15930 rect: Rect {
15931 x: 2.0,
15932 y: 3.0,
15933 width: 48.0,
15934 height: 18.0,
15935 },
15936 text: std::rc::Rc::new(AnnotatedString::from("runtime cache")),
15937 text_style: TextStyle::default(),
15938 font_size: 14.0,
15939 layout_options: TextLayoutOptions::default(),
15940 clip: None,
15941 })),
15942 });
15943 let mut layer = test_layer(
15944 Rect {
15945 x: 0.0,
15946 y: 0.0,
15947 width: 64.0,
15948 height: 32.0,
15949 },
15950 vec![text],
15951 );
15952 layer.node_id = Some(77);
15953 layer.recompute_raster_cache_hashes();
15954
15955 assert!(
15956 layer_raster_cache_candidate(&layer, 1.0, false, false).is_none(),
15957 "root path should not isolate plain translation-only text layers"
15958 );
15959 assert!(
15960 layer_raster_cache_candidate(&layer, 1.0, false, true).is_none(),
15961 "child path should also render plain translation-only text layers directly"
15962 );
15963 }
15964
15965 #[test]
15966 fn layer_raster_cache_candidate_allows_stable_runtime_child_effect_surfaces() {
15967 let mut layer = test_layer(
15968 Rect {
15969 x: 0.0,
15970 y: 0.0,
15971 width: 64.0,
15972 height: 32.0,
15973 },
15974 vec![RenderNode::Primitive(PrimitiveEntry {
15975 phase: PrimitivePhase::BeforeChildren,
15976 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15977 primitive: DrawPrimitive::Rect {
15978 rect: Rect {
15979 x: 0.0,
15980 y: 0.0,
15981 width: 64.0,
15982 height: 32.0,
15983 },
15984 brush: Brush::solid(Color::WHITE),
15985 stroke: None,
15986 },
15987 clip: None,
15988 }),
15989 })],
15990 );
15991 layer.node_id = Some(78);
15992 layer.graphics_layer.render_effect = Some(RenderEffect::blur(4.0));
15993 layer.recompute_raster_cache_hashes();
15994
15995 assert!(
15996 layer_raster_cache_candidate(&layer, 1.0, false, false).is_none(),
15997 "root direct path should not force-cache ordinary stable effects"
15998 );
15999 assert!(
16000 layer_raster_cache_candidate(&layer, 1.0, false, true).is_some(),
16001 "child surface rendering should retain stable non-runtime effects"
16002 );
16003 }
16004
16005 #[test]
16006 fn layer_raster_cache_candidate_rejects_runtime_shader_child_effect_surfaces() {
16007 let mut layer = test_layer(
16008 Rect {
16009 x: 0.0,
16010 y: 0.0,
16011 width: 64.0,
16012 height: 32.0,
16013 },
16014 vec![],
16015 );
16016 layer.node_id = Some(79);
16017 layer.graphics_layer.render_effect = Some(RenderEffect::runtime_shader(
16018 RuntimeShader::new("runtime shader"),
16019 ));
16020 layer.recompute_raster_cache_hashes();
16021
16022 assert!(
16023 layer_raster_cache_candidate(&layer, 1.0, false, true).is_none(),
16024 "runtime shaders must not fill the retained layer cache with per-frame uniform variants"
16025 );
16026 }
16027
16028 #[test]
16029 fn layer_surface_requirements_keep_plain_text_on_direct_path() {
16030 let layer = text_layer_with_style(AnnotatedString::from("plain"), TextStyle::default());
16031
16032 let requirements = layer_surface_requirements(&layer);
16033
16034 assert_eq!(requirements.direct_translation, Some(Point::default()));
16035 assert!(requirements
16036 .surface_requirements
16037 .contains(SurfaceRequirement::PixelStableComposite));
16038 assert!(!requirements
16039 .surface_requirements
16040 .has_isolating_requirement());
16041 }
16042
16043 #[test]
16044 fn layer_surface_requirements_keep_translated_plain_text_leaf_on_direct_path() {
16045 let layer = pure_text_leaf(false, true);
16046
16047 let requirements = layer_surface_requirements(&layer);
16048
16049 assert_eq!(
16050 requirements.direct_translation,
16051 Some(Point::new(11.4, 23.6))
16052 );
16053 assert!(
16054 requirements
16055 .surface_requirements
16056 .contains(SurfaceRequirement::PixelStableComposite)
16057 && !requirements
16058 .surface_requirements
16059 .has_isolating_requirement(),
16060 "translated plain text should stay on the direct path and isolate only the glyph draw"
16061 );
16062 }
16063
16064 #[test]
16065 fn layer_surface_requirements_keep_translated_text_leaf_with_background_on_direct_path() {
16066 let layer = snapped_text_leaf(false, true);
16067
16068 let requirements = layer_surface_requirements(&layer);
16069
16070 assert_eq!(
16071 requirements.direct_translation,
16072 Some(Point::new(14.25, 16.5))
16073 );
16074 assert!(
16075 requirements
16076 .surface_requirements
16077 .contains(SurfaceRequirement::PixelStableComposite)
16078 && !requirements
16079 .surface_requirements
16080 .has_isolating_requirement(),
16081 "translated text with direct sibling decoration/background should keep the layer direct"
16082 );
16083 }
16084
16085 #[test]
16086 fn translated_plain_text_uses_bounded_snap_surface() {
16087 let root = pure_text_leaf_root(true, true);
16088 let mut rect_cache = HashMap::new();
16089 let mut requirements_cache = HashMap::new();
16090 let collected =
16091 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
16092
16093 assert_eq!(collected.child_layers.len(), 1);
16094 assert!(collected.scene.texts.is_empty());
16095 assert!(collected.scene.effect_layers.is_empty());
16096 assert_snap_anchor_close(
16097 collected.child_layers[0].snap_anchor,
16098 Point::new(11.4, 23.6),
16099 "translated plain text's bounded local surface should composite at the content-origin snap phase",
16100 );
16101 }
16102
16103 #[test]
16107 #[ignore]
16108 fn shape_run_collect_timing_harness() {
16109 use cranpose_render_common::graph::DrawPrimitiveNode;
16110 use cranpose_render_common::layer_composition::local_content_layer_for;
16111 use cranpose_ui_graphics::Stroke;
16112
16113 let bounds = Rect {
16114 x: 0.0,
16115 y: 0.0,
16116 width: 1080.0,
16117 height: 2244.0,
16118 };
16119 let graphics_layer = GraphicsLayer::default();
16120
16121 let mut nodes: Vec<DrawPrimitiveNode> = Vec::new();
16124 for i in 0..3000u32 {
16125 let f = i as f32;
16126 let brush = if i % 8 == 0 {
16127 Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])
16128 } else {
16129 Brush::Solid(Color(0.5, 0.2, 0.8, 1.0))
16130 };
16131 let center = Point::new(540.0 + (f % 400.0), 1122.0 + (f % 350.0));
16132 let radius = 8.0 + (i % 23) as f32;
16133 let half = radius + 4.0;
16134 nodes.push(DrawPrimitiveNode {
16135 primitive: DrawPrimitive::Arc {
16136 rect: Rect {
16137 x: center.x - half,
16138 y: center.y - half,
16139 width: half * 2.0,
16140 height: half * 2.0,
16141 },
16142 brush,
16143 center,
16144 radius,
16145 start_angle: f * 0.07,
16146 sweep_angle: 0.5 + (i % 5) as f32,
16147 stroke: (i % 3 != 0).then(|| Stroke::new(4.0)),
16148 inner_radius: if i % 3 == 0 { radius * 0.6 } else { 0.0 },
16149 },
16150 clip: None,
16151 });
16152 }
16153
16154 let children: Vec<RenderNode> = nodes
16155 .iter()
16156 .map(|node| {
16157 RenderNode::Primitive(PrimitiveEntry {
16158 phase: PrimitivePhase::BeforeChildren,
16159 node: PrimitiveNode::Draw(node.clone()),
16160 })
16161 })
16162 .collect();
16163 let layer = crate::test_support::layer_node(
16164 bounds,
16165 ProjectiveTransform::identity(),
16166 graphics_layer,
16167 children,
16168 );
16169
16170 const ITERS: usize = 300;
16171
16172 let local_layer = local_content_layer_for(&layer.graphics_layer);
16174 let start = Instant::now();
16175 let mut sink_shapes = 0usize;
16176 for _ in 0..ITERS {
16177 let mut scene = CompositorScene::new();
16178 for node in &nodes {
16179 crate::pipeline::push_draw_primitive(
16180 &node.primitive,
16181 bounds,
16182 &local_layer,
16183 None,
16184 &mut scene,
16185 None,
16186 false,
16187 );
16188 }
16189 sink_shapes = scene.shapes.len();
16190 }
16191 let serial = start.elapsed();
16192
16193 let mut rect_cache = HashMap::new();
16194 let mut requirements_cache = HashMap::new();
16195 let start = Instant::now();
16196 let mut run_shapes = 0usize;
16197 for _ in 0..ITERS {
16198 let collected = collect_layer_contents(
16199 &layer,
16200 None,
16201 None,
16202 &mut rect_cache,
16203 &mut requirements_cache,
16204 );
16205 run_shapes = collected.scene.shapes.len();
16206 }
16207 let run = start.elapsed();
16208
16209 println!(
16210 "per-primitive: {:?}/iter ({sink_shapes} shapes) shape-run: {:?}/iter ({run_shapes} shapes)",
16211 serial / ITERS as u32,
16212 run / ITERS as u32,
16213 );
16214 }
16215
16216 fn assert_shape_run_collect_matches_per_primitive_emission() {
16218 use cranpose_render_common::graph::DrawPrimitiveNode;
16219 use cranpose_render_common::layer_composition::local_content_layer_for;
16220 use cranpose_render_common::primitive_emit::{resolve_primitive_clip, PrimitiveClipSpace};
16221 use cranpose_ui_graphics::{CornerRadii, Stroke};
16222
16223 let bounds = Rect {
16224 x: 0.0,
16225 y: 0.0,
16226 width: 800.0,
16227 height: 800.0,
16228 };
16229 let graphics_layer = GraphicsLayer {
16232 scale: 1.25,
16233 translation_x: 3.5,
16234 translation_y: -2.0,
16235 alpha: 0.9,
16236 rotation_z: 0.35,
16237 ..GraphicsLayer::default()
16238 };
16239
16240 let mut nodes: Vec<DrawPrimitiveNode> = Vec::new();
16241 for i in 0..600u32 {
16242 let f = i as f32;
16243 let brush = if i % 11 == 0 {
16244 Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])
16245 } else {
16246 Brush::Solid(Color(0.1 + (i % 7) as f32 * 0.1, 0.5, 0.9, 1.0))
16247 };
16248 let stroke = (i % 5 == 0).then(|| Stroke::new(1.0 + (i % 3) as f32));
16249 let primitive = match i % 3 {
16250 0 => DrawPrimitive::Rect {
16251 rect: Rect {
16252 x: f % 37.0,
16253 y: f % 53.0,
16254 width: 8.0 + f % 9.0,
16255 height: 6.0 + f % 5.0,
16256 },
16257 brush,
16258 stroke,
16259 },
16260 1 => DrawPrimitive::RoundRect {
16261 rect: Rect {
16262 x: f % 41.0,
16263 y: f % 43.0,
16264 width: 12.0,
16265 height: 10.0,
16266 },
16267 brush,
16268 radii: CornerRadii::uniform(2.0 + (i % 4) as f32),
16269 stroke,
16270 },
16271 _ => {
16272 let center = Point::new(60.0 + f % 71.0, 60.0 + f % 67.0);
16273 let radius = 5.0 + (i % 13) as f32;
16274 let sweep_angle = if i == 302 { 0.0 } else { 0.4 + (i % 6) as f32 };
16276 let half = radius + 4.0;
16277 DrawPrimitive::Arc {
16278 rect: Rect {
16279 x: center.x - half,
16280 y: center.y - half,
16281 width: half * 2.0,
16282 height: half * 2.0,
16283 },
16284 brush,
16285 center,
16286 radius,
16287 start_angle: f * 0.11,
16288 sweep_angle,
16289 stroke: (i % 2 == 0).then(|| Stroke::new(3.0)),
16290 inner_radius: if i % 4 == 2 { radius * 0.5 } else { 0.0 },
16291 }
16292 }
16293 };
16294 let primitive = if i == 300 {
16295 DrawPrimitive::Blend {
16299 primitive: Box::new(DrawPrimitive::Blend {
16300 primitive: Box::new(primitive),
16301 blend_mode: BlendMode::SrcOver,
16302 }),
16303 blend_mode: BlendMode::DstOut,
16304 }
16305 } else if i % 7 == 3 {
16306 DrawPrimitive::Blend {
16307 primitive: Box::new(primitive),
16308 blend_mode: BlendMode::DstOut,
16309 }
16310 } else {
16311 primitive
16312 };
16313 let clip = (i % 31 == 7).then_some(Rect {
16314 x: 0.0,
16315 y: 0.0,
16316 width: 30.0,
16317 height: 30.0,
16318 });
16319 nodes.push(DrawPrimitiveNode { primitive, clip });
16320 }
16321
16322 let children: Vec<RenderNode> = nodes
16323 .iter()
16324 .map(|node| {
16325 RenderNode::Primitive(PrimitiveEntry {
16326 phase: PrimitivePhase::BeforeChildren,
16327 node: PrimitiveNode::Draw(node.clone()),
16328 })
16329 })
16330 .collect();
16331 let layer = crate::test_support::layer_node(
16332 bounds,
16333 ProjectiveTransform::identity(),
16334 graphics_layer,
16335 children,
16336 );
16337
16338 let mut rect_cache = HashMap::new();
16339 let mut requirements_cache = HashMap::new();
16340 let collected =
16341 collect_layer_contents(&layer, None, None, &mut rect_cache, &mut requirements_cache);
16342
16343 let local_layer = local_content_layer_for(&layer.graphics_layer);
16346 let mut expected = CompositorScene::new();
16347 for node in &nodes {
16348 let clip = resolve_primitive_clip(
16349 node.clip,
16350 bounds,
16351 &local_layer,
16352 None,
16353 PrimitiveClipSpace::Local,
16354 );
16355 if node.clip.is_some() && clip.is_none() {
16356 continue;
16357 }
16358 crate::pipeline::push_draw_primitive(
16359 &node.primitive,
16360 bounds,
16361 &local_layer,
16362 clip,
16363 &mut expected,
16364 None,
16365 false,
16366 );
16367 }
16368
16369 assert!(
16370 collected.scene.shapes.len() >= 590,
16371 "the runs should engage the parallel branch: got {} shapes",
16372 collected.scene.shapes.len()
16373 );
16374 assert_eq!(collected.scene.shapes.len(), expected.shapes.len());
16375 assert_eq!(collected.scene.draw_ops, expected.draw_ops);
16376 assert_eq!(collected.scene.next_z, expected.next_z);
16377 assert!(
16378 collected
16379 .scene
16380 .shapes
16381 .iter()
16382 .all(|s| s.snap_anchor.is_none()),
16383 "a rotated layer must not rigid-snap; the reference scene assumes it"
16384 );
16385 for (index, (got, want)) in collected
16386 .scene
16387 .shapes
16388 .iter()
16389 .zip(&expected.shapes)
16390 .enumerate()
16391 {
16392 assert_eq!(got.rect, want.rect, "shape {index} rect");
16393 assert_eq!(got.local_rect, want.local_rect, "shape {index} local_rect");
16394 assert_eq!(got.quad, want.quad, "shape {index} quad");
16395 assert_eq!(got.snap_anchor, want.snap_anchor, "shape {index} snap");
16396 assert_eq!(got.brush, want.brush, "shape {index} brush");
16397 assert_eq!(got.shape, want.shape, "shape {index} shape");
16398 assert_eq!(got.stroke, want.stroke, "shape {index} stroke");
16399 assert_eq!(got.arc, want.arc, "shape {index} arc");
16400 assert_eq!(got.z_index, want.z_index, "shape {index} z");
16401 assert_eq!(got.clip, want.clip, "shape {index} clip");
16402 assert_eq!(got.blend_mode, want.blend_mode, "shape {index} blend");
16403 assert_eq!(
16404 got.motion_context_animated, want.motion_context_animated,
16405 "shape {index} motion flag"
16406 );
16407 }
16408 }
16409
16410 #[test]
16415 fn shape_run_collect_matches_per_primitive_emission_exactly() {
16416 assert_shape_run_collect_matches_per_primitive_emission();
16417 crate::normalized_scene::force_shape_run_parallel_for_tests(true);
16418 let outcome =
16419 std::panic::catch_unwind(assert_shape_run_collect_matches_per_primitive_emission);
16420 crate::normalized_scene::force_shape_run_parallel_for_tests(false);
16421 if let Err(payload) = outcome {
16422 std::panic::resume_unwind(payload);
16423 }
16424 }
16425
16426 #[test]
16427 fn non_translated_text_local_surface_keeps_linear_composite_resolve() {
16428 let layer = text_layer_with_style(
16429 AnnotatedString::from("gradient"),
16430 TextStyle::from_span_style(SpanStyle {
16431 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
16432 ..SpanStyle::default()
16433 }),
16434 );
16435 let requirements = layer_surface_requirements(&layer);
16436
16437 assert!(requirements
16438 .surface_requirements
16439 .contains(SurfaceRequirement::TextMaterialMask));
16440 assert_eq!(
16441 composite_sample_mode_for_requirements(false, false, requirements),
16442 CompositeSampleMode::Linear
16443 );
16444 }
16445
16446 #[test]
16447 fn inherited_translated_text_local_surface_uses_box4_layer_surface() {
16448 let layer = text_layer_with_style(
16449 AnnotatedString::from("shadow"),
16450 TextStyle::from_span_style(SpanStyle {
16451 shadow: Some(Shadow {
16452 color: Color::BLACK,
16453 offset: Point::new(1.0, 2.0),
16454 blur_radius: 3.0,
16455 }),
16456 ..SpanStyle::default()
16457 }),
16458 );
16459 let requirements = layer_surface_requirements(&layer);
16460
16461 assert!(requirements
16462 .surface_requirements
16463 .contains(SurfaceRequirement::TextMaterialMask));
16464 assert_eq!(
16465 composite_sample_mode_for_requirements(true, false, requirements),
16466 CompositeSampleMode::Box4
16467 );
16468 assert_eq!(
16469 layer_surface_target_scale(
16470 true,
16471 false,
16472 requirements,
16473 1.25,
16474 layer_surface_scale(&layer)
16475 ),
16476 SurfaceRequirementSet::default()
16477 .with(SurfaceRequirement::TextMaterialMask)
16478 .with(SurfaceRequirement::MotionStableCapture)
16479 .target_scale(1.25, 1.0)
16480 );
16481 }
16482
16483 #[test]
16484 fn translated_text_local_surface_inside_capture_keeps_parent_scale() {
16485 let layer = text_layer_with_style(
16486 AnnotatedString::from("shadow"),
16487 TextStyle::from_span_style(SpanStyle {
16488 shadow: Some(Shadow {
16489 color: Color::BLACK,
16490 offset: Point::new(1.0, 2.0),
16491 blur_radius: 3.0,
16492 }),
16493 ..SpanStyle::default()
16494 }),
16495 );
16496 let requirements = layer_surface_requirements(&layer);
16497
16498 assert_eq!(
16499 composite_sample_mode_for_requirements(true, true, requirements),
16500 CompositeSampleMode::Linear
16501 );
16502 assert_eq!(
16503 layer_surface_target_scale(true, true, requirements, 10.0, layer_surface_scale(&layer)),
16504 SurfaceRequirementSet::default()
16505 .with(SurfaceRequirement::TextMaterialMask)
16506 .target_scale(10.0, 1.0)
16507 );
16508 }
16509
16510 #[test]
16511 fn layer_surface_requirements_use_local_surface_for_gradient_and_stroke_text() {
16512 let cases = [
16513 (
16514 "draw_style",
16515 AnnotatedString::from("draw_style"),
16516 TextStyle::from_span_style(SpanStyle {
16517 draw_style: Some(TextDrawStyle::Stroke { width: 2.0 }),
16518 ..SpanStyle::default()
16519 }),
16520 ),
16521 (
16522 "gradient_brush",
16523 AnnotatedString::from("gradient"),
16524 TextStyle::from_span_style(SpanStyle {
16525 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
16526 ..SpanStyle::default()
16527 }),
16528 ),
16529 ];
16530
16531 for (label, text, text_style) in cases {
16532 let layer = text_layer_with_style(text, text_style);
16533 let requirements = layer_surface_requirements(&layer);
16534 assert!(
16535 requirements
16536 .surface_requirements
16537 .contains(SurfaceRequirement::TextMaterialMask),
16538 "{label} text should use a bounded local surface: {requirements:?}"
16539 );
16540 }
16541 }
16542
16543 #[test]
16544 fn layer_surface_requirements_use_local_surface_for_complex_text_effects() {
16545 let cases = [
16546 (
16547 "shadow",
16548 AnnotatedString::from("shadow"),
16549 TextStyle::from_span_style(SpanStyle {
16550 shadow: Some(Shadow {
16551 color: Color::BLACK,
16552 offset: Point::new(1.0, 2.0),
16553 blur_radius: 3.0,
16554 }),
16555 ..SpanStyle::default()
16556 }),
16557 ),
16558 (
16559 "background",
16560 AnnotatedString::from("background"),
16561 TextStyle::from_span_style(SpanStyle {
16562 background: Some(Color::BLACK),
16563 ..SpanStyle::default()
16564 }),
16565 ),
16566 (
16567 "baseline_shift",
16568 AnnotatedString::from("baseline_shift"),
16569 TextStyle::from_span_style(SpanStyle {
16570 baseline_shift: Some(BaselineShift::SUPERSCRIPT),
16571 ..SpanStyle::default()
16572 }),
16573 ),
16574 (
16575 "geometric_transform",
16576 AnnotatedString::from("geometric_transform"),
16577 TextStyle::from_span_style(SpanStyle {
16578 text_geometric_transform: Some(TextGeometricTransform {
16579 scale_x: 1.2,
16580 skew_x: 0.15,
16581 }),
16582 ..SpanStyle::default()
16583 }),
16584 ),
16585 (
16586 "letter_spacing",
16587 AnnotatedString::from("letter_spacing"),
16588 TextStyle::from_span_style(SpanStyle {
16589 letter_spacing: TextUnit::Em(0.2),
16590 ..SpanStyle::default()
16591 }),
16592 ),
16593 ];
16594
16595 for (label, text, text_style) in cases {
16596 let layer = text_layer_with_style(text, text_style);
16597 let requirements = layer_surface_requirements(&layer);
16598 assert!(
16599 requirements
16600 .surface_requirements
16601 .contains(SurfaceRequirement::TextMaterialMask),
16602 "{label} text should use a bounded local surface: {requirements:?}"
16603 );
16604 assert_eq!(
16605 requirements.direct_translation,
16606 Some(Point::default()),
16607 "{label} text should still classify as a direct translation"
16608 );
16609 }
16610 }
16611
16612 #[test]
16613 fn layer_surface_requirements_color_only_span_styles_use_direct_path() {
16614 let layer = text_layer_with_style(
16615 AnnotatedString {
16616 text: "styled".to_string(),
16617 span_styles: vec![RangeStyle {
16618 item: SpanStyle {
16619 color: Some(Color::BLACK),
16620 ..SpanStyle::default()
16621 },
16622 range: 0..3,
16623 }],
16624 ..AnnotatedString::default()
16625 },
16626 TextStyle::default(),
16627 );
16628 let requirements = layer_surface_requirements(&layer);
16629 assert!(
16630 !requirements
16631 .surface_requirements
16632 .contains(SurfaceRequirement::TextMaterialMask),
16633 "color-only span styles should render directly via software text raster colors"
16634 );
16635 }
16636
16637 #[test]
16638 fn layer_surface_requirements_keep_decoration_only_text_on_direct_path() {
16639 let layer = text_layer_with_style(
16640 AnnotatedString::from("decoration"),
16641 TextStyle::from_span_style(SpanStyle {
16642 text_decoration: Some(TextDecoration::UNDERLINE),
16643 ..SpanStyle::default()
16644 }),
16645 );
16646
16647 let requirements = layer_surface_requirements(&layer);
16648
16649 assert_eq!(requirements.direct_translation, Some(Point::default()));
16650 assert!(
16651 requirements
16652 .surface_requirements
16653 .contains(SurfaceRequirement::PixelStableComposite)
16654 && !requirements
16655 .surface_requirements
16656 .has_isolating_requirement(),
16657 "decoration-only text should not force an isolating layer surface: {requirements:?}"
16658 );
16659 }
16660
16661 #[test]
16662 fn direct_text_leaf_snaps_modifier_background_and_text_with_one_anchor() {
16663 let root = snapped_text_leaf_root(false, false);
16664 let mut rect_cache = HashMap::new();
16665 let mut requirements_cache = HashMap::new();
16666
16667 let collected =
16668 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
16669
16670 assert_eq!(collected.scene.shapes.len(), 1);
16671 assert_eq!(collected.scene.images.len(), 1);
16672 assert_eq!(collected.scene.texts.len(), 1);
16673 let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
16674 assert_eq!(collected.scene.shapes[0].snap_anchor, expected_anchor);
16675 assert_eq!(collected.scene.images[0].snap_anchor, expected_anchor);
16676 assert_eq!(collected.scene.texts[0].snap_anchor, expected_anchor);
16677 }
16678
16679 #[test]
16680 fn animated_translated_content_text_leaf_uses_bounded_content_snap() {
16681 let root = snapped_text_leaf_root(true, true);
16682 let mut rect_cache = HashMap::new();
16683 let mut requirements_cache = HashMap::new();
16684
16685 let collected =
16686 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
16687
16688 assert_eq!(collected.child_layers.len(), 1);
16689 assert!(collected.scene.shapes.is_empty());
16690 assert!(collected.scene.images.is_empty());
16691 assert!(collected.scene.texts.is_empty());
16692 assert!(collected.scene.effect_layers.is_empty());
16693 let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
16694 assert_eq!(
16695 collected.child_layers[0].snap_anchor, expected_anchor,
16696 "active translated leaf surface should keep the content-origin snap phase"
16697 );
16698 }
16699
16700 #[test]
16701 fn translated_content_assigns_motion_anchor_to_rotated_child_surface() {
16702 let mut child = snapped_text_leaf(false, false);
16703 child.graphics_layer.rotation_z = 5.0;
16704 child.transform_to_parent =
16705 cranpose_render_common::layer_transform::layer_transform_to_parent(
16706 child.local_bounds,
16707 Point::new(108.0, 3.0),
16708 &child.graphics_layer,
16709 );
16710 child.recompute_raster_cache_hashes();
16711 let mut root = test_layer(
16712 Rect {
16713 x: 0.0,
16714 y: 0.0,
16715 width: 320.0,
16716 height: 180.0,
16717 },
16718 vec![RenderNode::Layer(Box::new(child))],
16719 );
16720 root.translated_content_context = true;
16721 root.translated_content_offset = Point::new(0.0, -80.8);
16722 root.recompute_raster_cache_hashes();
16723 let mut rect_cache = HashMap::new();
16724 let mut requirements_cache = HashMap::new();
16725
16726 let collected =
16727 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
16728
16729 assert_eq!(collected.child_layers.len(), 1);
16730 assert!(
16731 collected.child_layers[0].snap_anchor.is_some(),
16732 "a projective child still translates rigidly with its scrolling parent"
16733 );
16734 }
16735
16736 #[test]
16737 fn rested_translated_content_context_text_leaf_snaps_for_crisp_scroll_rest() {
16738 let root = snapped_text_leaf_root(false, true);
16739 let mut rect_cache = HashMap::new();
16740 let mut requirements_cache = HashMap::new();
16741
16742 let collected =
16743 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
16744
16745 assert_eq!(collected.child_layers.len(), 0);
16746 assert_eq!(collected.scene.shapes.len(), 1);
16747 assert_eq!(collected.scene.images.len(), 1);
16748 assert_eq!(collected.scene.texts.len(), 1);
16749 assert_eq!(collected.scene.effect_layers.len(), 0);
16750 let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
16751 assert_eq!(
16752 collected.scene.shapes[0].snap_anchor, expected_anchor,
16753 "rested scroll content should snap back to device pixels"
16754 );
16755 assert_eq!(
16756 collected.scene.images[0].snap_anchor, expected_anchor,
16757 "rested scroll images should snap back to device pixels"
16758 );
16759 assert_eq!(
16760 collected.scene.texts[0].snap_anchor, expected_anchor,
16761 "rested scroll text should snap back to device pixels"
16762 );
16763 }
16764
16765 #[test]
16766 fn complex_text_uses_local_surface() {
16767 let root = translated_content_local_surface_root();
16768 let mut rect_cache = HashMap::new();
16769 let mut requirements_cache = HashMap::new();
16770
16771 let collected =
16772 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
16773
16774 assert!(
16775 !collected.child_layers.is_empty(),
16776 "translated-content effectful text should render through a bounded local surface"
16777 );
16778 assert!(collected.scene.texts.is_empty());
16779 assert!(collected.scene.shadow_draws.is_empty());
16780 }
16781
16782 #[test]
16783 fn translated_content_surface_composite_uses_scroll_content_snap_anchor() {
16784 let mut root = translated_content_local_surface_root();
16785 let scroll_offset = Point::new(0.0, -18.5);
16786 let Some(RenderNode::Layer(translated_content)) = root.children.get_mut(0) else {
16787 panic!("expected translated content layer");
16788 };
16789 translated_content.translated_content_offset = scroll_offset;
16790 let Some(RenderNode::Layer(effectful_text)) = translated_content.children.get_mut(0) else {
16791 panic!("expected effectful text layer");
16792 };
16793 effectful_text.transform_to_parent =
16794 effectful_text
16795 .transform_to_parent
16796 .then(ProjectiveTransform::translation(
16797 scroll_offset.x,
16798 scroll_offset.y,
16799 ));
16800
16801 let mut rect_cache = HashMap::new();
16802 let mut requirements_cache = HashMap::new();
16803 let collected =
16804 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
16805
16806 assert_eq!(collected.child_layers.len(), 1);
16807 assert_eq!(
16808 collected.child_layers[0].snap_anchor,
16809 Some(SnapAnchor::rigid(Point::new(14.25, -2.0))),
16810 "isolated scrolled descendants must composite with the same content-origin snap phase"
16811 );
16812 }
16813
16814 #[test]
16815 fn animated_translated_content_surface_composite_uses_scroll_content_snap_anchor() {
16816 let mut root = translated_content_local_surface_root();
16817 let scroll_offset = Point::new(0.0, -18.5);
16818 let Some(RenderNode::Layer(translated_content)) = root.children.get_mut(0) else {
16819 panic!("expected translated content layer");
16820 };
16821 translated_content.motion_context_animated = true;
16822 translated_content.translated_content_offset = scroll_offset;
16823 let Some(RenderNode::Layer(effectful_text)) = translated_content.children.get_mut(0) else {
16824 panic!("expected effectful text layer");
16825 };
16826 effectful_text.transform_to_parent =
16827 effectful_text
16828 .transform_to_parent
16829 .then(ProjectiveTransform::translation(
16830 scroll_offset.x,
16831 scroll_offset.y,
16832 ));
16833
16834 let mut rect_cache = HashMap::new();
16835 let mut requirements_cache = HashMap::new();
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_eq!(
16841 collected.child_layers[0].snap_anchor,
16842 Some(SnapAnchor::rigid(Point::new(14.25, 16.5))),
16843 "animated translated content should composite the stable local surface at the viewport-origin snap phase"
16844 );
16845 }
16846
16847 #[test]
16848 fn translated_text_material_effect_layer_uses_scroll_content_snap_anchor() {
16849 let mut layer = text_layer_with_style(
16850 AnnotatedString::from("gradient"),
16851 TextStyle::from_span_style(SpanStyle {
16852 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
16853 ..SpanStyle::default()
16854 }),
16855 );
16856 layer.translated_content_context = true;
16857 layer.translated_content_offset = Point::new(0.0, -18.5);
16858 let mut rect_cache = HashMap::new();
16859 let mut requirements_cache = HashMap::new();
16860
16861 let collected =
16862 collect_layer_contents(&layer, None, None, &mut rect_cache, &mut requirements_cache);
16863
16864 assert_eq!(collected.scene.effect_layers.len(), 1);
16865 assert_eq!(
16866 composite_sample_mode_for_effect_layer(&collected.scene.effect_layers[0]),
16867 CompositeSampleMode::Box4
16868 );
16869 assert_eq!(
16870 collected.scene.effect_layers[0].snap_anchor,
16871 Some(SnapAnchor::rigid(Point::new(0.0, -18.5))),
16872 "text material surfaces must composite with the scroll content-origin snap phase"
16873 );
16874 }
16875
16876 #[test]
16877 fn translated_layer_surface_capture_does_not_restart_local_picture_for_shadow_text() {
16878 let mut layer = text_layer_with_style(
16879 AnnotatedString::from("shadow"),
16880 TextStyle::from_span_style(SpanStyle {
16881 shadow: Some(Shadow {
16882 color: Color::BLACK,
16883 offset: Point::new(1.0, 2.0),
16884 blur_radius: 3.0,
16885 }),
16886 ..SpanStyle::default()
16887 }),
16888 );
16889 layer.translated_content_context = true;
16890 let mut rect_cache = HashMap::new();
16891 let mut requirements_cache = HashMap::new();
16892
16893 let collected = collect_layer_contents_with_translation_context(
16894 &layer,
16895 None,
16896 None,
16897 TranslationRenderContext {
16898 inherited_content_translation: false,
16899 surface_capture_active: true,
16900 local_picture_capture_active: true,
16901 ..TranslationRenderContext::default()
16902 },
16903 &mut rect_cache,
16904 &mut requirements_cache,
16905 );
16906
16907 assert!(
16908 collected.scene.effect_layers.is_empty(),
16909 "a translated layer surface already provides the stable local capture"
16910 );
16911 assert_eq!(collected.scene.shadow_draws.len(), 1);
16912 assert_eq!(collected.scene.texts.len(), 1);
16913 assert!(
16914 !collected.scene.texts[0].translated_content_context,
16915 "text inside an active motion-stable capture must raster in capture-local coordinates"
16916 );
16917 }
16918
16919 #[test]
16920 fn translated_layer_surface_capture_keeps_only_material_effect_layers() {
16921 let mut layer = text_layer_with_style(
16922 AnnotatedString::from("gradient"),
16923 TextStyle::from_span_style(SpanStyle {
16924 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
16925 ..SpanStyle::default()
16926 }),
16927 );
16928 layer.translated_content_context = true;
16929 let mut rect_cache = HashMap::new();
16930 let mut requirements_cache = HashMap::new();
16931
16932 let collected = collect_layer_contents_with_translation_context(
16933 &layer,
16934 None,
16935 None,
16936 TranslationRenderContext {
16937 inherited_content_translation: false,
16938 surface_capture_active: true,
16939 local_picture_capture_active: true,
16940 ..TranslationRenderContext::default()
16941 },
16942 &mut rect_cache,
16943 &mut requirements_cache,
16944 );
16945
16946 assert_eq!(collected.scene.effect_layers.len(), 1);
16947 assert!(
16948 collected.scene.effect_layers[0]
16949 .requirements
16950 .contains(SurfaceRequirement::MotionStableCapture),
16951 "translated text materials still need motion-stable resolve semantics inside a stable capture"
16952 );
16953 assert_eq!(
16954 composite_sample_mode_for_effect_layer(&collected.scene.effect_layers[0]),
16955 CompositeSampleMode::Box4
16956 );
16957 assert_eq!(
16958 effect_layer_target_scale(&collected.scene.effect_layers[0], 10.0),
16959 10.0
16960 );
16961 assert!(collected.scene.effect_layers[0].effect.is_some());
16962 }
16963
16964 #[test]
16965 fn translated_viewport_surface_does_not_add_plain_local_picture_capture() {
16966 let mut layer = text_layer_with_style(
16967 AnnotatedString::from("shadow"),
16968 TextStyle::from_span_style(SpanStyle {
16969 shadow: Some(Shadow {
16970 color: Color::BLACK,
16971 offset: Point::new(1.0, 2.0),
16972 blur_radius: 3.0,
16973 }),
16974 ..SpanStyle::default()
16975 }),
16976 );
16977 layer.translated_content_context = true;
16978 layer.motion_context_animated = true;
16979 let mut rect_cache = HashMap::new();
16980 let mut requirements_cache = HashMap::new();
16981
16982 let collected = collect_layer_contents_with_translation_context(
16983 &layer,
16984 None,
16985 None,
16986 TranslationRenderContext {
16987 surface_capture_active: true,
16988 ..TranslationRenderContext::default()
16989 },
16990 &mut rect_cache,
16991 &mut requirements_cache,
16992 );
16993
16994 assert_eq!(
16995 collected.scene.effect_layers.len(),
16996 0,
16997 "plain translated content inside a viewport surface should not be captured again"
16998 );
16999 assert_eq!(collected.scene.shadow_draws.len(), 1);
17000 assert_eq!(collected.scene.texts.len(), 1);
17001 }
17002
17003 #[test]
17004 fn static_pure_text_leaf_snaps_without_sibling_draw_primitives() {
17005 let root = pure_text_leaf_root(false, false);
17006 let mut rect_cache = HashMap::new();
17007 let mut requirements_cache = HashMap::new();
17008
17009 let collected =
17010 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
17011
17012 assert_eq!(collected.scene.texts.len(), 1);
17013 assert!(
17014 collected.scene.texts[0].snap_anchor.is_some(),
17015 "idle pure text leaves should participate in rigid snap anchoring"
17016 );
17017 }
17018
17019 #[test]
17020 fn animated_pure_text_leaf_stays_unsnapped() {
17021 let root = pure_text_leaf_root(true, false);
17022 let mut rect_cache = HashMap::new();
17023 let mut requirements_cache = HashMap::new();
17024
17025 let collected =
17026 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
17027
17028 assert_eq!(collected.scene.texts.len(), 1);
17029 assert_eq!(collected.scene.texts[0].snap_anchor, None);
17030 }
17031
17032 #[test]
17033 fn animated_translated_pure_text_uses_bounded_content_snap() {
17034 let root = pure_text_leaf_root(true, true);
17035 let mut rect_cache = HashMap::new();
17036 let mut requirements_cache = HashMap::new();
17037
17038 let collected =
17039 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
17040
17041 assert_eq!(collected.child_layers.len(), 1);
17042 assert!(collected.scene.texts.is_empty());
17043 assert!(collected.scene.effect_layers.is_empty());
17044 assert_snap_anchor_close(
17045 collected.child_layers[0].snap_anchor,
17046 Point::new(11.4, 23.6),
17047 "animated translated pure text should use the bounded content snap phase",
17048 );
17049 }
17050
17051 #[test]
17052 fn rested_translated_pure_text_leaf_snaps_for_crisp_scroll_rest() {
17053 let root = pure_text_leaf_root(false, true);
17054 let mut rect_cache = HashMap::new();
17055 let mut requirements_cache = HashMap::new();
17056
17057 let collected =
17058 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
17059
17060 assert_eq!(collected.child_layers.len(), 0);
17061 assert_eq!(collected.scene.texts.len(), 1);
17062 assert_eq!(collected.scene.effect_layers.len(), 0);
17063 assert_snap_anchor_close(
17064 collected.scene.texts[0].snap_anchor,
17065 Point::new(11.4, 23.6),
17066 "rested translated text should snap to device pixels",
17067 );
17068 }
17069
17070 #[test]
17071 fn static_gpu_effect_text_leaf_stays_unsnapped() {
17072 let root = text_layer_with_style(
17073 AnnotatedString::from("Gradient"),
17074 TextStyle::from_span_style(SpanStyle {
17075 brush: Some(Brush::linear_gradient(vec![
17076 Color(0.2, 0.8, 1.0, 1.0),
17077 Color(1.0, 0.7, 0.4, 1.0),
17078 ])),
17079 draw_style: Some(TextDrawStyle::Stroke { width: 2.5 }),
17080 ..SpanStyle::default()
17081 }),
17082 );
17083 let mut rect_cache = HashMap::new();
17084 let mut requirements_cache = HashMap::new();
17085
17086 let collected =
17087 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
17088
17089 assert_eq!(collected.scene.texts.len(), 1);
17090 assert_eq!(
17091 collected.scene.texts[0].snap_anchor, None,
17092 "gpu text-effect leaves must not take the rigid text snap path"
17093 );
17094 assert_eq!(
17095 collected.scene.effect_layers.len(),
17096 1,
17097 "gradient stroke text should still emit a runtime shader effect layer"
17098 );
17099 }
17100
17101 #[test]
17102 fn layer_surface_requirements_keep_shape_plus_direct_child_on_direct_path() {
17103 let mut child = test_layer(
17104 Rect {
17105 x: 0.0,
17106 y: 0.0,
17107 width: 40.0,
17108 height: 20.0,
17109 },
17110 vec![RenderNode::Primitive(PrimitiveEntry {
17111 phase: PrimitivePhase::BeforeChildren,
17112 node: PrimitiveNode::Draw(DrawPrimitiveNode {
17113 primitive: DrawPrimitive::Rect {
17114 rect: Rect {
17115 x: 0.0,
17116 y: 0.0,
17117 width: 40.0,
17118 height: 20.0,
17119 },
17120 brush: Brush::solid(Color::WHITE),
17121 stroke: None,
17122 },
17123 clip: None,
17124 }),
17125 })],
17126 );
17127 child.transform_to_parent = ProjectiveTransform::translation(8.0, 6.0);
17128
17129 let layer = test_layer(
17130 Rect {
17131 x: 0.0,
17132 y: 0.0,
17133 width: 64.0,
17134 height: 32.0,
17135 },
17136 vec![
17137 RenderNode::Primitive(PrimitiveEntry {
17138 phase: PrimitivePhase::BeforeChildren,
17139 node: PrimitiveNode::Draw(DrawPrimitiveNode {
17140 primitive: DrawPrimitive::Rect {
17141 rect: Rect {
17142 x: 0.0,
17143 y: 0.0,
17144 width: 64.0,
17145 height: 32.0,
17146 },
17147 brush: Brush::solid(Color::BLACK),
17148 stroke: None,
17149 },
17150 clip: None,
17151 }),
17152 }),
17153 RenderNode::Layer(Box::new(child)),
17154 ],
17155 );
17156
17157 let requirements = layer_surface_requirements(&layer);
17158
17159 assert_eq!(requirements.direct_translation, Some(Point::default()));
17160 assert!(!requirements
17161 .surface_requirements
17162 .contains(SurfaceRequirement::MixedDirectContent));
17163 assert!(!requirements
17164 .surface_requirements
17165 .has_isolating_requirement());
17166 }
17167
17168 #[test]
17169 fn collect_layer_contents_translates_direct_text_rects_into_parent_space() {
17170 let mut child = text_layer_with_style(
17171 AnnotatedString::from("direct"),
17172 TextStyle::from_span_style(SpanStyle {
17173 text_decoration: Some(TextDecoration::UNDERLINE),
17174 ..SpanStyle::default()
17175 }),
17176 );
17177 child.transform_to_parent = ProjectiveTransform::translation(9.0, 7.0);
17178
17179 let parent = test_layer(
17180 Rect {
17181 x: 0.0,
17182 y: 0.0,
17183 width: 64.0,
17184 height: 32.0,
17185 },
17186 vec![RenderNode::Layer(Box::new(child))],
17187 );
17188
17189 let mut rect_cache = HashMap::new();
17190 let mut requirements_cache = HashMap::new();
17191 let collected = with_test_app_context(|| {
17192 collect_layer_contents(
17193 &parent,
17194 None,
17195 None,
17196 &mut rect_cache,
17197 &mut requirements_cache,
17198 )
17199 });
17200
17201 assert!(
17202 collected.child_layers.is_empty(),
17203 "decoration-only text child should collapse directly into the parent scene"
17204 );
17205 assert_eq!(collected.scene.texts.len(), 1, "expected one text draw");
17206 let text = &collected.scene.texts[0];
17207 assert!(
17208 text.rect.x >= 9.0 && text.rect.y >= 7.0,
17209 "collapsed text rect should be translated into parent space, got {:?}",
17210 text.rect
17211 );
17212 assert!(
17213 collected
17214 .scene
17215 .shapes
17216 .iter()
17217 .any(|shape| shape.rect.y >= 7.0),
17218 "collapsed underline geometry should also be translated into parent space"
17219 );
17220 }
17221
17222 #[test]
17223 fn normalized_scene_keeps_lazy_after_bound_text_for_prewarm() {
17224 use std::cell::RefCell;
17225
17226 fn collect_graph_text_labels(layer: &LayerNode, labels: &mut Vec<String>) {
17227 for child in &layer.children {
17228 match child {
17229 RenderNode::Primitive(PrimitiveEntry {
17230 node: PrimitiveNode::Text(text),
17231 ..
17232 }) => labels.push(text.text.text.clone()),
17233 RenderNode::Layer(child_layer) => {
17234 collect_graph_text_labels(child_layer, labels)
17235 }
17236 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => {}
17237 }
17238 }
17239 }
17240
17241 let state_holder: Rc<RefCell<Option<LazyListState>>> = Rc::new(RefCell::new(None));
17242 let state_holder_for_comp = state_holder.clone();
17243 let mut composition = cranpose_ui::run_test_composition(move || {
17244 let list_state = remember_lazy_list_state();
17245 *state_holder_for_comp.borrow_mut() = Some(list_state);
17246 let mut spec = LazyColumnSpec::new()
17247 .vertical_arrangement(cranpose_ui::LinearArrangement::SpacedBy(6.0));
17248 spec.beyond_bounds_item_count = 0;
17249 LazyColumn(Modifier::empty().height(96.0), list_state, spec, |scope| {
17250 scope.items(
17251 12,
17252 None::<fn(usize) -> u64>,
17253 None::<fn(usize) -> u64>,
17254 |index| {
17255 Text(
17256 format!("WarmRow {index}"),
17257 Modifier::empty().height(32.0),
17258 TextStyle::default(),
17259 );
17260 },
17261 );
17262 });
17263 });
17264
17265 let list_state = (*state_holder.borrow()).expect("lazy list state should be captured");
17266 list_state.scroll_to_item(4, 0.0);
17267
17268 let root = composition.root().expect("lazy column root");
17269 let handle = composition.runtime_handle();
17270 let mut applier = composition.applier_mut();
17271 applier.set_runtime_handle(handle);
17272 let _ = applier
17273 .compute_layout(
17274 root,
17275 Size {
17276 width: 240.0,
17277 height: 240.0,
17278 },
17279 )
17280 .expect("lazy column layout");
17281 let graph = build_graph_from_applier(&mut applier, root, 1.0).expect("lazy column graph");
17282 applier.clear_runtime_handle();
17283 let mut graph_labels = Vec::new();
17284 collect_graph_text_labels(&graph.root, &mut graph_labels);
17285
17286 let visible_indices: Vec<_> = list_state
17287 .layout_info()
17288 .visible_items_info
17289 .iter()
17290 .map(|item| item.index)
17291 .collect();
17292 assert_eq!(
17293 visible_indices,
17294 vec![4, 5, 6],
17295 "test setup expects exactly three viewport-visible rows"
17296 );
17297
17298 let mut rect_cache = HashMap::new();
17299 let mut requirements_cache = HashMap::new();
17300 let collected = with_test_app_context(|| {
17301 collect_layer_contents(
17302 &graph.root,
17303 None,
17304 None,
17305 &mut rect_cache,
17306 &mut requirements_cache,
17307 )
17308 });
17309 let root_text_labels: Vec<_> = collected
17310 .scene
17311 .texts
17312 .iter()
17313 .map(|text| text.text.text.clone())
17314 .collect();
17315 let child_layer_count = collected.child_layers.len();
17316 let warm_text = collected
17317 .scene
17318 .texts
17319 .iter()
17320 .find(|text| text.text.text == "WarmRow 7")
17321 .unwrap_or_else(|| {
17322 panic!(
17323 "after-bound lazy text should reach WGPU scene collection; graph_texts={graph_labels:?} root_texts={root_text_labels:?} child_layers={child_layer_count}"
17324 )
17325 });
17326
17327 assert!(
17328 warm_text.rect.y >= 96.0,
17329 "after-bound text should be below the viewport, got {:?}",
17330 warm_text.rect
17331 );
17332 assert_eq!(
17333 visible_draw_rect(warm_text.rect, warm_text.clip),
17334 None,
17335 "after-bound text should remain clipped away for drawing while staying available for glyph prewarm"
17336 );
17337 assert!(
17338 text_draw_should_prewarm_in_viewport(
17339 warm_text.rect,
17340 warm_text.clip,
17341 ViewportUniformParams {
17342 width: 240,
17343 height: 96,
17344 offset: [0.0, 0.0],
17345 },
17346 1.0,
17347 ),
17348 "after-bound text inside the warm window must be selected by WGPU prewarm"
17349 );
17350 }
17351
17352 #[test]
17353 fn direct_translation_accepts_nearly_identity_axis_scale_noise() {
17354 let local_bounds = Rect {
17355 x: 0.0,
17356 y: 0.0,
17357 width: 393.3,
17358 height: 16.8,
17359 };
17360 let quad = [
17361 [10.0, 78.399_994],
17362 [403.3, 78.399_994],
17363 [10.0, 95.2],
17364 [403.3, 95.2],
17365 ];
17366 let transform = ProjectiveTransform::from_rect_to_quad(local_bounds, quad);
17367
17368 assert_eq!(
17369 direct_translation(transform),
17370 Some(Point::new(10.0, 78.399_994)),
17371 );
17372 }
17373
17374 #[test]
17375 fn layer_surface_requirements_keep_shape_plus_isolating_child_as_mixed_content() {
17376 let mut child = test_layer(
17377 Rect {
17378 x: 0.0,
17379 y: 0.0,
17380 width: 24.0,
17381 height: 18.0,
17382 },
17383 vec![RenderNode::Primitive(PrimitiveEntry {
17384 phase: PrimitivePhase::BeforeChildren,
17385 node: PrimitiveNode::Draw(DrawPrimitiveNode {
17386 primitive: DrawPrimitive::Rect {
17387 rect: Rect {
17388 x: 0.0,
17389 y: 0.0,
17390 width: 24.0,
17391 height: 18.0,
17392 },
17393 brush: Brush::solid(Color::WHITE),
17394 stroke: None,
17395 },
17396 clip: None,
17397 }),
17398 })],
17399 );
17400 child.transform_to_parent = ProjectiveTransform::translation(8.0, 6.0);
17401 child.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
17402
17403 let layer = test_layer(
17404 Rect {
17405 x: 0.0,
17406 y: 0.0,
17407 width: 64.0,
17408 height: 32.0,
17409 },
17410 vec![
17411 RenderNode::Primitive(PrimitiveEntry {
17412 phase: PrimitivePhase::BeforeChildren,
17413 node: PrimitiveNode::Draw(DrawPrimitiveNode {
17414 primitive: DrawPrimitive::Rect {
17415 rect: Rect {
17416 x: 0.0,
17417 y: 0.0,
17418 width: 64.0,
17419 height: 32.0,
17420 },
17421 brush: Brush::solid(Color::BLACK),
17422 stroke: None,
17423 },
17424 clip: None,
17425 }),
17426 }),
17427 RenderNode::Layer(Box::new(child)),
17428 ],
17429 );
17430
17431 let requirements = layer_surface_requirements(&layer);
17432
17433 assert!(requirements
17434 .surface_requirements
17435 .contains(SurfaceRequirement::MixedDirectContent));
17436 assert!(!requirements
17437 .surface_requirements
17438 .has_isolating_requirement());
17439 }
17440
17441 #[test]
17442 fn build_scene_window_filters_and_translates_items() {
17443 let mut shape = test_shape(6, BlendMode::SrcOver);
17444 shape.rect.x = 12.0;
17445 shape.rect.y = 25.0;
17446 shape.local_rect.x = 12.0;
17447 shape.local_rect.y = 25.0;
17448 shape.quad = [[12.0, 25.0], [20.0, 25.0], [12.0, 33.0], [20.0, 33.0]];
17449 shape.clip = Some(Rect {
17450 x: 11.0,
17451 y: 24.0,
17452 width: 10.0,
17453 height: 10.0,
17454 });
17455
17456 let mut image = test_image(8, BlendMode::SrcOver);
17457 image.rect.x = 18.0;
17458 image.rect.y = 27.0;
17459 image.local_rect.x = 18.0;
17460 image.local_rect.y = 27.0;
17461 image.quad = [[18.0, 27.0], [26.0, 27.0], [18.0, 35.0], [26.0, 35.0]];
17462
17463 let mut text = test_text(9);
17464 text.rect.x = 16.0;
17465 text.rect.y = 29.0;
17466 text.clip = Some(Rect {
17467 x: 15.0,
17468 y: 28.0,
17469 width: 9.0,
17470 height: 6.0,
17471 });
17472
17473 let mut shadow_shape = test_shape(7, BlendMode::SrcOver);
17474 shadow_shape.rect.x = 14.0;
17475 shadow_shape.rect.y = 26.0;
17476 shadow_shape.local_rect.x = 14.0;
17477 shadow_shape.local_rect.y = 26.0;
17478 shadow_shape.quad = [[14.0, 26.0], [22.0, 26.0], [14.0, 34.0], [22.0, 34.0]];
17479 let mut shadow = test_shadow_draw(vec![(shadow_shape, BlendMode::SrcOver)]);
17480 shadow.z_index = 7;
17481
17482 let mut nested_effect = effect_layer(6, 10);
17483 nested_effect.rect.x = 13.0;
17484 nested_effect.rect.y = 24.0;
17485 nested_effect.clip = Some(Rect {
17486 x: 15.0,
17487 y: 25.0,
17488 width: 4.0,
17489 height: 5.0,
17490 });
17491
17492 let mut nested_backdrop = backdrop_layer(8);
17493 nested_backdrop.rect.x = 17.0;
17494 nested_backdrop.rect.y = 26.0;
17495 nested_backdrop.clip = Some(Rect {
17496 x: 18.0,
17497 y: 27.0,
17498 width: 3.0,
17499 height: 4.0,
17500 });
17501
17502 let window = build_scene_window(
17503 SceneWindowSource {
17504 shapes: &[test_shape(4, BlendMode::SrcOver), shape],
17505 images: &[image],
17506 texts: &[text],
17507 shadow_draws: &[shadow],
17508 draw_ops: &[],
17509 effect_layers: &[effect_layer(2, 4), nested_effect.clone()],
17510 backdrop_layers: &[backdrop_layer(4), nested_backdrop.clone()],
17511 },
17512 5,
17513 10,
17514 Rect {
17515 x: 10.0,
17516 y: 20.0,
17517 width: 20.0,
17518 height: 20.0,
17519 },
17520 );
17521
17522 assert_eq!(window.shapes.len(), 1);
17523 assert_eq!(
17524 window.shapes[0].rect,
17525 Rect {
17526 x: 2.0,
17527 y: 5.0,
17528 width: 8.0,
17529 height: 8.0,
17530 }
17531 );
17532 assert_eq!(
17533 window.shapes[0].clip,
17534 Some(Rect {
17535 x: 1.0,
17536 y: 4.0,
17537 width: 10.0,
17538 height: 10.0,
17539 })
17540 );
17541 assert_eq!(window.images.len(), 1);
17542 assert_eq!(window.images[0].rect.x, 8.0);
17543 assert_eq!(window.images[0].rect.y, 7.0);
17544 assert_eq!(window.texts.len(), 1);
17545 assert_eq!(window.texts[0].rect.x, 6.0);
17546 assert_eq!(window.texts[0].rect.y, 9.0);
17547 assert_eq!(
17548 window.texts[0].clip,
17549 Some(Rect {
17550 x: 5.0,
17551 y: 8.0,
17552 width: 9.0,
17553 height: 6.0,
17554 })
17555 );
17556 assert_eq!(window.shadow_draws.len(), 1);
17557 assert_eq!(window.shadow_draws[0].shapes[0].0.rect.x, 4.0);
17558 assert_eq!(window.shadow_draws[0].shapes[0].0.rect.y, 6.0);
17559 assert_eq!(window.effect_layers.len(), 1);
17560 assert_eq!(
17561 window.effect_layers[0].rect,
17562 Rect {
17563 x: 3.0,
17564 y: 4.0,
17565 width: 10.0,
17566 height: 10.0,
17567 }
17568 );
17569 assert_eq!(
17570 window.effect_layers[0].clip,
17571 Some(Rect {
17572 x: 5.0,
17573 y: 5.0,
17574 width: 4.0,
17575 height: 5.0,
17576 })
17577 );
17578 assert_eq!(window.backdrop_layers.len(), 1);
17579 assert_eq!(
17580 window.backdrop_layers[0].rect,
17581 Rect {
17582 x: 7.0,
17583 y: 6.0,
17584 width: 10.0,
17585 height: 10.0,
17586 }
17587 );
17588 assert_eq!(
17589 window.backdrop_layers[0].clip,
17590 Some(Rect {
17591 x: 8.0,
17592 y: 7.0,
17593 width: 3.0,
17594 height: 4.0,
17595 })
17596 );
17597 }
17598
17599 #[test]
17600 fn filtered_effect_layer_index_counts_only_window_members() {
17601 let effects = vec![
17602 effect_layer(0, 2),
17603 effect_layer(5, 12),
17604 effect_layer(6, 10),
17605 effect_layer(14, 20),
17606 ];
17607
17608 assert_eq!(filtered_effect_layer_index(&effects, 1, 5, 12), Some(0));
17609 assert_eq!(filtered_effect_layer_index(&effects, 2, 5, 12), Some(1));
17610 assert_eq!(filtered_effect_layer_index(&effects, 3, 5, 12), None);
17611 }
17612
17613 #[test]
17614 fn blend_mode_support_matrix_is_explicit() {
17615 assert!(is_blend_mode_supported(BlendMode::SrcOver));
17616 assert!(is_blend_mode_supported(BlendMode::DstOut));
17617 assert!(!is_blend_mode_supported(BlendMode::Clear));
17618 assert!(!is_blend_mode_supported(BlendMode::Multiply));
17619 }
17620
17621 #[test]
17622 fn collect_non_effect_segment_items_preserves_global_z_order() {
17623 let shapes = vec![
17624 test_shape(3, BlendMode::SrcOver),
17625 test_shape(1, BlendMode::DstOut),
17626 ];
17627 let images = vec![test_image(2, BlendMode::SrcOver)];
17628 let texts = vec![test_text(0)];
17629 let shadows: Vec<ShadowDraw> = Vec::new();
17630 let draw_ops = test_draw_ops(&shapes, &images, &texts, &shadows);
17631
17632 let mut scratch = Vec::new();
17633 collect_non_effect_segment_items(
17634 &shapes,
17635 &images,
17636 &texts,
17637 &shadows,
17638 &draw_ops,
17639 0,
17640 4,
17641 &[],
17642 100,
17643 100,
17644 1.0,
17645 &mut scratch,
17646 );
17647 let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
17648 assert_eq!(
17649 items,
17650 vec![
17651 SegmentDrawItem::Text(0),
17652 SegmentDrawItem::Shape(1),
17653 SegmentDrawItem::Image(0),
17654 SegmentDrawItem::Shape(0),
17655 ]
17656 );
17657 }
17658
17659 #[test]
17660 fn collect_non_effect_segment_items_filters_effect_ranges() {
17661 let shapes = vec![
17662 test_shape(1, BlendMode::SrcOver),
17663 test_shape(3, BlendMode::DstOut),
17664 ];
17665 let images = vec![test_image(2, BlendMode::SrcOver)];
17666 let texts = vec![test_text(4)];
17667 let shadows: Vec<ShadowDraw> = Vec::new();
17668 let draw_ops = test_draw_ops(&shapes, &images, &texts, &shadows);
17669 let effect_ranges = [std::ops::Range { start: 2, end: 4 }];
17670
17671 let mut scratch = Vec::new();
17672 collect_non_effect_segment_items(
17673 &shapes,
17674 &images,
17675 &texts,
17676 &shadows,
17677 &draw_ops,
17678 0,
17679 5,
17680 &effect_ranges,
17681 100,
17682 100,
17683 1.0,
17684 &mut scratch,
17685 );
17686 let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
17687 assert_eq!(
17688 items,
17689 vec![SegmentDrawItem::Shape(0), SegmentDrawItem::Text(0)]
17690 );
17691 }
17692
17693 #[test]
17694 fn collect_non_effect_segment_items_culls_offscreen_shapes_but_keeps_text_prewarm() {
17695 let mut shape = test_shape(0, BlendMode::SrcOver);
17696 shape.rect.y = 160.0;
17697 shape.local_rect.y = 160.0;
17698 shape.quad = [[0.0, 160.0], [8.0, 160.0], [0.0, 168.0], [8.0, 168.0]];
17699
17700 let shapes = vec![shape];
17701 let images = Vec::new();
17702 let mut text = test_text(1);
17703 text.rect.y = 160.0;
17704 let texts = vec![text];
17705 let shadows: Vec<ShadowDraw> = Vec::new();
17706 let draw_ops = test_draw_ops(&shapes, &images, &texts, &shadows);
17707
17708 let mut scratch = Vec::new();
17709 collect_non_effect_segment_items(
17710 &shapes,
17711 &images,
17712 &texts,
17713 &shadows,
17714 &draw_ops,
17715 0,
17716 2,
17717 &[],
17718 100,
17719 100,
17720 1.0,
17721 &mut scratch,
17722 );
17723
17724 let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
17725 assert_eq!(items, vec![SegmentDrawItem::Text(0)]);
17726 }
17727
17728 #[test]
17729 fn segment_command_iter_merges_non_conflicting_batches_into_one_chunk() {
17730 let ordered_items = vec![
17731 (0, SegmentDrawItem::Shape(0)),
17732 (1, SegmentDrawItem::Image(0)),
17733 (2, SegmentDrawItem::Text(0)),
17734 ];
17735 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
17736 let images = vec![test_image(1, BlendMode::DstOut)];
17737
17738 let commands: Vec<_> = SegmentCommandIter::new(
17739 &ordered_items,
17740 &shapes,
17741 &images,
17742 ShapeBatchLimits::desktop(),
17743 )
17744 .collect();
17745
17746 assert_eq!(
17747 commands,
17748 vec![SegmentRenderCommand::DrawChunk(chunk(&[
17749 SegmentBatchPlan::Shape {
17750 start: 0,
17751 end: 1,
17752 blend_mode: BlendMode::SrcOver,
17753 },
17754 SegmentBatchPlan::Image {
17755 start: 1,
17756 end: 2,
17757 blend_mode: BlendMode::DstOut,
17758 },
17759 SegmentBatchPlan::Text { start: 2, end: 3 },
17760 ]))]
17761 );
17762 }
17763
17764 #[test]
17765 fn segment_command_iter_keeps_layer_composites_in_ordered_draw_chunk() {
17766 let ordered_items = vec![
17767 (0, SegmentDrawItem::Shape(0)),
17768 (1, SegmentDrawItem::Composite(0)),
17769 (2, SegmentDrawItem::Image(0)),
17770 (3, SegmentDrawItem::Composite(1)),
17771 (4, SegmentDrawItem::Text(0)),
17772 ];
17773 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
17774 let images = vec![test_image(2, BlendMode::SrcOver)];
17775
17776 let commands: Vec<_> = SegmentCommandIter::new(
17777 &ordered_items,
17778 &shapes,
17779 &images,
17780 ShapeBatchLimits::desktop(),
17781 )
17782 .collect();
17783
17784 assert_eq!(
17785 commands,
17786 vec![SegmentRenderCommand::DrawChunk(chunk(&[
17787 SegmentBatchPlan::Shape {
17788 start: 0,
17789 end: 1,
17790 blend_mode: BlendMode::SrcOver,
17791 },
17792 SegmentBatchPlan::Composite { start: 1, end: 2 },
17793 SegmentBatchPlan::Image {
17794 start: 2,
17795 end: 3,
17796 blend_mode: BlendMode::SrcOver,
17797 },
17798 SegmentBatchPlan::Composite { start: 3, end: 4 },
17799 SegmentBatchPlan::Text { start: 4, end: 5 },
17800 ]))]
17801 );
17802 }
17803
17804 #[test]
17805 fn retain_renderable_shadow_items_culls_invisible_shadow_boundaries() {
17806 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
17807 let images = vec![test_image(2, BlendMode::SrcOver)];
17808 let mut shadow_shape = test_shape(1, BlendMode::SrcOver);
17809 shadow_shape.rect = Rect {
17810 x: 500.0,
17811 y: 500.0,
17812 width: 12.0,
17813 height: 12.0,
17814 };
17815 let shadow_draws = vec![ShadowDraw {
17816 shapes: vec![(shadow_shape, BlendMode::SrcOver)],
17817 texts: Vec::new(),
17818 blur_radius: 8.0,
17819 clip: None,
17820 z_index: 1,
17821 }];
17822 let mut ordered_items = vec![
17823 (0, SegmentDrawItem::Shape(0)),
17824 (1, SegmentDrawItem::Shadow(0)),
17825 (2, SegmentDrawItem::Image(0)),
17826 ];
17827
17828 let culled =
17829 retain_renderable_shadow_items(&mut ordered_items, &shadow_draws, 100, 100, 1.0, 4096);
17830 let commands: Vec<_> = SegmentCommandIter::new(
17831 &ordered_items,
17832 &shapes,
17833 &images,
17834 ShapeBatchLimits::desktop(),
17835 )
17836 .collect();
17837
17838 assert_eq!(culled, 1);
17839 assert_eq!(
17840 commands,
17841 vec![SegmentRenderCommand::DrawChunk(chunk(&[
17842 SegmentBatchPlan::Shape {
17843 start: 0,
17844 end: 1,
17845 blend_mode: BlendMode::SrcOver,
17846 },
17847 SegmentBatchPlan::Image {
17848 start: 1,
17849 end: 2,
17850 blend_mode: BlendMode::SrcOver,
17851 },
17852 ]))]
17853 );
17854 }
17855
17856 #[test]
17857 fn retain_renderable_shadow_items_keeps_visible_shadow_boundaries() {
17858 let mut shadow_shape = test_shape(1, BlendMode::SrcOver);
17859 shadow_shape.rect = Rect {
17860 x: 20.0,
17861 y: 20.0,
17862 width: 12.0,
17863 height: 12.0,
17864 };
17865 let shadow_draws = vec![ShadowDraw {
17866 shapes: vec![(shadow_shape, BlendMode::SrcOver)],
17867 texts: Vec::new(),
17868 blur_radius: 8.0,
17869 clip: None,
17870 z_index: 1,
17871 }];
17872 let mut ordered_items = vec![(1, SegmentDrawItem::Shadow(0))];
17873
17874 let culled =
17875 retain_renderable_shadow_items(&mut ordered_items, &shadow_draws, 100, 100, 1.0, 4096);
17876
17877 assert_eq!(culled, 0);
17878 assert_eq!(ordered_items, vec![(1, SegmentDrawItem::Shadow(0))]);
17879 }
17880
17881 #[test]
17882 fn shape_data_layout_matches_the_wgsl_mirror() {
17883 assert_eq!(std::mem::size_of::<ShapeData>(), 160);
17887 assert_eq!(std::mem::size_of::<ShapeData>() % 16, 0);
17888 assert_eq!(std::mem::size_of::<GradientStop>(), 32);
17889 }
17890
17891 #[test]
17892 fn shape_flags_pack_kind_cap_and_join_without_collision() {
17893 assert_eq!(
17894 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter),
17895 0.0
17896 );
17897 assert_eq!(
17898 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Miter),
17899 1.0
17900 );
17901 assert_eq!(
17902 pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Butt, StrokeJoin::Miter),
17903 2.0
17904 );
17905 assert_eq!(
17907 pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Round, StrokeJoin::Miter),
17908 2.0 + 4.0
17909 );
17910 assert_eq!(
17911 pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Square, StrokeJoin::Miter),
17912 2.0 + 8.0
17913 );
17914 assert_eq!(
17915 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Round),
17916 1.0 + 16.0
17917 );
17918 assert_eq!(
17919 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Bevel),
17920 1.0 + 32.0
17921 );
17922 for kind in [SHAPE_KIND_FILL, SHAPE_KIND_STROKE, SHAPE_KIND_ARC] {
17924 for cap in [StrokeCap::Butt, StrokeCap::Round, StrokeCap::Square] {
17925 for join in [StrokeJoin::Miter, StrokeJoin::Round, StrokeJoin::Bevel] {
17926 let packed = pack_shape_flags(kind, cap, join);
17927 let bits = packed as u32;
17928 assert_eq!(bits & 3, kind);
17929 assert_eq!((bits >> 2) & 3, stroke_cap_code(cap));
17930 assert_eq!((bits >> 4) & 3, stroke_join_code(join));
17931 assert_eq!(packed, bits as f32, "flags must be exact in f32");
17932 }
17933 }
17934 }
17935 }
17936
17937 #[cfg(not(target_arch = "wasm32"))]
17938 #[test]
17939 fn mesh_vertex_layout_matches_the_wgsl_input() {
17940 assert_eq!(std::mem::size_of::<MeshVertex>(), 20);
17943 }
17944
17945 #[cfg(not(target_arch = "wasm32"))]
17949 #[allow(clippy::too_many_arguments)]
17950 fn sdf_arc_band_reference(
17951 p: [f32; 2],
17952 center: [f32; 2],
17953 inner: f32,
17954 outer: f32,
17955 mid_sin_cos: [f32; 2],
17956 half_sin_cos: [f32; 2],
17957 cap: u32,
17958 ) -> f32 {
17959 let ra = (outer + inner) * 0.5;
17960 let rb = ((outer - inner) * 0.5).max(0.0);
17961 let sm = mid_sin_cos[0];
17962 let cm = mid_sin_cos[1];
17963 let d = [p[0] - center[0], p[1] - center[1]];
17964 let mut q = [-sm * d[0] + cm * d[1], cm * d[0] + sm * d[1]];
17965 q[0] = q[0].abs();
17966 let sc = half_sin_cos;
17967 let mut dist = if sc[1] * q[0] > sc[0] * q[1] {
17968 let dx = q[0] - sc[0] * ra;
17969 let dy = q[1] - sc[1] * ra;
17970 (dx * dx + dy * dy).sqrt() - rb
17971 } else {
17972 ((q[0] * q[0] + q[1] * q[1]).sqrt() - ra).abs() - rb
17973 };
17974 let plane = sc[1] * q[0] - sc[0] * q[1];
17975 if cap == 0 {
17977 dist = dist.max(plane);
17978 } else if cap == 2 {
17979 dist = dist.max(plane - rb);
17980 }
17981 dist
17982 }
17983
17984 #[cfg(not(target_arch = "wasm32"))]
17985 fn point_in_triangle(p: [f64; 2], tri: &[[f64; 2]; 3]) -> bool {
17986 let side = |a: [f64; 2], b: [f64; 2]| {
17987 (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0])
17988 };
17989 let d0 = side(tri[0], tri[1]);
17990 let d1 = side(tri[1], tri[2]);
17991 let d2 = side(tri[2], tri[0]);
17992 let has_neg = d0 < 0.0 || d1 < 0.0 || d2 < 0.0;
17993 let has_pos = d0 > 0.0 || d1 > 0.0 || d2 > 0.0;
17994 !(has_neg && has_pos)
17995 }
17996
17997 #[cfg(not(target_arch = "wasm32"))]
17998 fn converted_arc_shape(arc: cranpose_ui_graphics::ArcGeometry, root_scale: f32) -> ShapeData {
17999 let bounds = arc.bounds();
18000 let mut shape = test_shape(0, BlendMode::SrcOver);
18001 shape.rect = bounds;
18002 shape.local_rect = bounds;
18003 shape.quad = [
18004 [bounds.x, bounds.y],
18005 [bounds.x + bounds.width, bounds.y],
18006 [bounds.x, bounds.y + bounds.height],
18007 [bounds.x + bounds.width, bounds.y + bounds.height],
18008 ];
18009 shape.arc = Some(arc);
18010 let mut converted = ShapeData::zeroed();
18011 convert_shape_into_slots(&shape, root_scale, 0, &mut converted, &mut []);
18012 converted
18013 }
18014
18015 #[cfg(not(target_arch = "wasm32"))]
18020 #[test]
18021 fn arc_mesh_contains_every_band_pixel() {
18022 use cranpose_ui_graphics::ArcGeometry;
18023 let tau = cranpose_ui_graphics::TAU;
18024 let center = Point::new(250.0, 250.0);
18025 let cases: &[(f32, f32, f32, f32, StrokeCap)] = &[
18026 (90.0, 100.0, 0.0, tau, StrokeCap::Round),
18028 (80.0, 100.0, 1.0, 10.0, StrokeCap::Butt),
18030 (0.0, 40.0, 0.0, tau, StrokeCap::Round),
18032 (30.0, 80.0, 0.7, 2.5, StrokeCap::Butt),
18034 (30.0, 80.0, 0.7, 2.5, StrokeCap::Round),
18035 (30.0, 80.0, 0.7, 2.5, StrokeCap::Square),
18036 (99.0, 101.0, 3.0, 4.0, StrokeCap::Round),
18038 (0.6, 2.0, 0.3, 1.2, StrokeCap::Butt),
18040 (1900.0, 1904.0, 0.1, 0.35, StrokeCap::Square),
18042 (40.0, 60.0, 5.0, 1e-3, StrokeCap::Round),
18044 (40.0, 60.0, 0.2, tau - 1e-3, StrokeCap::Butt),
18046 (0.0, 3.0, 1.0, 2.0, StrokeCap::Round),
18048 (20.0, 60.0, 4.5, 1.9, StrokeCap::Butt),
18050 ];
18051 for (case, &(inner, outer, start, sweep, cap)) in cases.iter().enumerate() {
18052 for root_scale in [1.0f32, 2.0, 2.75] {
18057 let arc = ArcGeometry::new(center, inner, outer, start, sweep, cap);
18058 assert!(!arc.is_degenerate(), "case {case} must be drawable");
18059 let converted = converted_arc_shape(arc, root_scale);
18060 let band = arc_mesh_band(&converted)
18061 .unwrap_or_else(|| panic!("case {case} must qualify for meshing"));
18062 let mut vertices = Vec::new();
18063 let mut indices = Vec::new();
18064 let segments =
18065 emit_arc_band_mesh(&converted, 0, &band, &mut vertices, &mut indices)
18066 .unwrap_or_else(|| panic!("case {case} must produce a mesh"));
18067 assert!(segments >= ARC_MESH_MIN_SEGMENTS);
18068 let position = |index: u32| {
18071 let p = vertices[index as usize].position;
18072 [p[0] as f64, p[1] as f64]
18073 };
18074 let triangles: Vec<[[f64; 2]; 3]> = indices
18075 .chunks_exact(3)
18076 .map(|tri| [position(tri[0]), position(tri[1]), position(tri[2])])
18077 .collect();
18078
18079 let [qx, qy, ..] = converted.quad01;
18083 let [_, _, qr, qb] = converted.quad23;
18084 let (rw, rh) = (qr - qx, qb - qy);
18085 let cap_bits = (converted.stroke_params[1].max(0.0) as u32 >> 2) & 3;
18086 let step = (rw.max(rh) / 400.0).clamp(0.25, 2.0);
18087 let mut band_points = 0usize;
18088 let mut y = qy;
18089 while y <= qb {
18090 let mut x = qx;
18091 while x <= qr {
18092 let dist = sdf_arc_band_reference(
18093 [x, y],
18094 [converted.arc_params[0], converted.arc_params[1]],
18095 converted.stroke_params[3],
18096 converted.stroke_params[2],
18097 [converted.radii[0], converted.radii[1]],
18098 [converted.radii[2], converted.radii[3]],
18099 cap_bits,
18100 );
18101 if dist <= 0.5 {
18102 band_points += 1;
18103 let p = [x as f64, y as f64];
18104 assert!(
18105 triangles.iter().any(|tri| point_in_triangle(p, tri)),
18106 "case {case} scale {root_scale}: band point ({x}, {y}) \
18107 dist {dist} escapes the mesh"
18108 );
18109 }
18110 x += step;
18111 }
18112 y += step;
18113 }
18114 assert!(
18115 band_points > 0,
18116 "case {case} scale {root_scale}: the sampling grid never hit the band"
18117 );
18118 }
18119 }
18120 }
18121
18122 #[cfg(not(target_arch = "wasm32"))]
18123 #[test]
18124 fn arc_mesh_passthrough_replicates_the_quad_expansion() {
18125 let shape = test_shape(0, BlendMode::SrcOver);
18126 let mut converted = ShapeData::zeroed();
18127 convert_shape_into_slots(&shape, 1.0, 0, &mut converted, &mut []);
18128 let build =
18129 build_arc_mesh_vertices(std::slice::from_ref(&converted)).expect("within budget");
18130 assert_eq!(build.meshed_arcs, 0);
18131 assert_eq!(build.passthrough, 1);
18132 assert_eq!(build.vertices.len(), 4);
18134 assert_eq!(build.index_prefix, vec![0, 6]);
18135 assert_eq!(build.indices, vec![0, 1, 2, 2, 1, 3]);
18136 let corners = [
18137 ([converted.quad01[0], converted.quad01[1]], [0.0f32, 0.0]),
18138 ([converted.quad01[2], converted.quad01[3]], [1.0, 0.0]),
18139 ([converted.quad23[0], converted.quad23[1]], [0.0, 1.0]),
18140 ([converted.quad23[2], converted.quad23[3]], [1.0, 1.0]),
18141 ];
18142 for (vertex, corner) in build.vertices.iter().zip(corners) {
18143 assert_eq!(vertex.position, corner.0);
18144 assert_eq!(vertex.uv, corner.1);
18145 assert_eq!(vertex.shape_idx, 0);
18146 }
18147 for (index, corner) in build.indices.iter().zip([0usize, 1, 2, 2, 1, 3]) {
18150 assert_eq!(build.vertices[*index as usize].position, corners[corner].0);
18151 assert_eq!(build.vertices[*index as usize].uv, corners[corner].1);
18152 }
18153 }
18154
18155 #[cfg(not(target_arch = "wasm32"))]
18161 #[test]
18162 fn arc_mesh_indices_share_boundary_vertices_and_wrap_closed_rings() {
18163 use cranpose_ui_graphics::ArcGeometry;
18164 let tau = cranpose_ui_graphics::TAU;
18165 for (sweep, closed) in [(tau, true), (1.9f32, false)] {
18168 let arc = ArcGeometry::new(
18169 Point::new(250.0, 250.0),
18170 80.0,
18171 100.0,
18172 0.7,
18173 sweep,
18174 StrokeCap::Round,
18175 );
18176 let mut converted = converted_arc_shape(arc, 1.0);
18177 converted.rect = [0.0, 0.0, 500.0, 500.0];
18181 converted.quad01 = [0.0, 0.0, 500.0, 0.0];
18182 converted.quad23 = [0.0, 500.0, 500.0, 500.0];
18183 let band = arc_mesh_band(&converted).expect("arc must qualify");
18184 let mut vertices = Vec::new();
18185 let mut indices = Vec::new();
18186 let segments = emit_arc_band_mesh(&converted, 0, &band, &mut vertices, &mut indices)
18187 .expect("arc must mesh");
18188 let boundary_count = if closed { segments } else { segments + 1 };
18189 assert_eq!(
18190 vertices.len(),
18191 2 * boundary_count,
18192 "closed={closed}: every boundary owns exactly one (inner, outer) pair"
18193 );
18194 assert_eq!(indices.len(), 6 * segments);
18195 for j in 0..segments {
18199 let jb = (j + 1) % boundary_count;
18200 let (in_a, out_a) = (2 * j as u32, 2 * j as u32 + 1);
18201 let (in_b, out_b) = (2 * jb as u32, 2 * jb as u32 + 1);
18202 assert_eq!(
18203 indices[6 * j..6 * j + 6],
18204 [in_a, out_a, out_b, in_a, out_b, in_b],
18205 "closed={closed}: segment {j} must share its boundary pairs"
18206 );
18207 }
18208 if closed {
18209 assert_eq!(indices[6 * segments - 1], 0);
18212 }
18213 for pair in vertices.chunks_exact(2) {
18217 let radius = |v: &MeshVertex| {
18218 let dx = v.position[0] - 250.0;
18219 let dy = v.position[1] - 250.0;
18220 (dx * dx + dy * dy).sqrt()
18221 };
18222 assert!(radius(&pair[0]) < radius(&pair[1]));
18223 }
18224 }
18225 }
18226
18227 #[cfg(not(target_arch = "wasm32"))]
18233 #[test]
18234 fn arc_mesh_clipped_segments_fan_over_private_vertices() {
18235 use cranpose_ui_graphics::ArcGeometry;
18236 let arc = ArcGeometry::new(
18237 Point::new(250.0, 250.0),
18238 80.0,
18239 100.0,
18240 0.0,
18241 cranpose_ui_graphics::TAU,
18242 StrokeCap::Round,
18243 );
18244 let converted = converted_arc_shape(arc, 1.0);
18245 let band = arc_mesh_band(&converted).expect("ring must qualify");
18246 let mut vertices = Vec::new();
18247 let mut indices = Vec::new();
18248 emit_arc_band_mesh(&converted, 0, &band, &mut vertices, &mut indices)
18249 .expect("ring must mesh");
18250 let mut uses = vec![0usize; vertices.len()];
18253 for &index in &indices {
18254 uses[index as usize] += 1;
18255 }
18256 assert!(
18257 uses.iter().any(|&count| count >= 3),
18258 "some boundary vertices must be shared across trapezoids"
18259 );
18260 let [left, top, ..] = converted.quad01;
18266 let [.., right, bottom] = converted.quad23;
18267 let clipped: Vec<&MeshVertex> = vertices
18268 .iter()
18269 .filter(|vertex| {
18270 let [x, y] = vertex.position;
18271 x == left || x == right || y == top || y == bottom
18272 })
18273 .collect();
18274 assert!(
18275 !clipped.is_empty(),
18276 "the tight box must clip the pushed-out chord vertices"
18277 );
18278 assert!(
18281 vertices.len() < indices.len(),
18282 "{} unique vertices should undercut {} triangle corners",
18283 vertices.len(),
18284 indices.len()
18285 );
18286 }
18287
18288 #[cfg(not(target_arch = "wasm32"))]
18289 #[test]
18290 fn arc_mesh_budget_overflow_falls_back_to_whole_slot_passthrough() {
18291 use cranpose_ui_graphics::ArcGeometry;
18292 let arc = ArcGeometry::new(
18297 Point::new(2000.0, 2000.0),
18298 1690.0,
18299 1710.0,
18300 0.0,
18301 cranpose_ui_graphics::TAU,
18302 StrokeCap::Round,
18303 );
18304 let converted = converted_arc_shape(arc, 1.0);
18305 let shapes = vec![converted; 100];
18306 assert!(build_arc_mesh_vertices(&shapes).is_none());
18307 }
18308
18309 #[cfg(not(target_arch = "wasm32"))]
18310 #[test]
18311 fn shape_batch_limits_follow_uniform_binding_size() {
18312 let desktop_shapes = 65536 / std::mem::size_of::<ShapeData>();
18315 assert_eq!(desktop_shapes, 409);
18316 assert_eq!(
18317 ShapeBatchLimits::desktop(),
18318 ShapeBatchLimits {
18319 max_shapes_per_batch: desktop_shapes.min(MAX_SHAPES_PER_BATCH),
18320 max_gradient_stops: MAX_GRADIENT_STOPS,
18321 storage: false,
18322 }
18323 );
18324
18325 let downlevel = ShapeBatchLimits::for_uniform_binding_size(16384);
18328 assert_eq!(downlevel.max_shapes_per_batch, 16384 / 160);
18329 assert_eq!(downlevel.max_shapes_per_batch, 102);
18330 assert_eq!(downlevel.max_gradient_stops, 512.min(MAX_GRADIENT_STOPS));
18331 assert!(downlevel.max_shapes_per_batch * std::mem::size_of::<ShapeData>() <= 16384);
18332 assert!(downlevel.max_gradient_stops * std::mem::size_of::<GradientStop>() <= 16384);
18333
18334 let tiny = ShapeBatchLimits::for_uniform_binding_size(1);
18336 assert_eq!(tiny.max_shapes_per_batch, 1);
18337 assert_eq!(tiny.max_gradient_stops, 1);
18338 }
18339
18340 #[test]
18341 fn storage_shape_batch_limits_uncap_the_batch_and_start_small() {
18342 let storage = ShapeBatchLimits::for_storage_binding_size(128 << 20);
18345 assert!(storage.storage);
18346 assert_eq!(storage.max_shapes_per_batch, MAX_SHAPES_PER_STORAGE_BATCH);
18347 assert_eq!(
18348 storage.max_gradient_stops,
18349 MAX_GRADIENT_STOPS_PER_STORAGE_BATCH
18350 );
18351
18352 assert_eq!(
18355 storage.initial_shape_capacity(),
18356 INITIAL_STORAGE_BATCH_CAPACITY
18357 );
18358 assert_eq!(
18359 storage.initial_gradient_capacity(),
18360 INITIAL_STORAGE_BATCH_CAPACITY
18361 );
18362 assert_eq!(
18363 storage.data_binding_type(),
18364 wgpu::BufferBindingType::Storage { read_only: true }
18365 );
18366 assert!(storage
18367 .data_buffer_usage()
18368 .contains(wgpu::BufferUsages::STORAGE));
18369
18370 let uniform = ShapeBatchLimits::desktop();
18373 assert_eq!(
18374 uniform.initial_shape_capacity(),
18375 uniform.max_shapes_per_batch
18376 );
18377 assert_eq!(
18378 uniform.initial_gradient_capacity(),
18379 uniform.max_gradient_stops
18380 );
18381 assert_eq!(
18382 uniform.data_binding_type(),
18383 wgpu::BufferBindingType::Uniform
18384 );
18385 assert!(uniform
18386 .data_buffer_usage()
18387 .contains(wgpu::BufferUsages::UNIFORM));
18388 }
18389
18390 #[test]
18391 fn storage_shape_shader_swaps_the_arrays_to_runtime_sized_storage() {
18392 let source = shape_shader_source(ShapeBatchLimits::for_storage_binding_size(128 << 20));
18393 assert!(
18394 source.contains("var<storage, read> shape_data: array<ShapeData>;"),
18395 "storage-mode shader must declare a runtime-sized shape array"
18396 );
18397 assert!(
18398 source.contains("var<storage, read> gradient_stops: array<GradientStop>;"),
18399 "storage-mode shader must declare a runtime-sized gradient array"
18400 );
18401 assert!(
18402 !source.contains("var<uniform> shape_data"),
18403 "the uniform shape declaration must be fully replaced"
18404 );
18405 assert!(
18406 !source.contains("var<uniform> gradient_stops"),
18407 "the uniform gradient declaration must be fully replaced"
18408 );
18409 assert!(
18410 source.contains("var<storage, read> paint: array<vec4<f32>>;"),
18411 "storage-mode shader must declare the retained paint array"
18412 );
18413 assert!(
18414 source.contains("select(shape.color, paint[shape_idx], similarity.paint_select > 0.5)"),
18415 "storage-mode shader must read paint under the paint_select flag"
18416 );
18417 assert!(
18418 source.contains("fn vs_mesh("),
18419 "the storage rewrite must leave the retained-mesh vertex entry intact"
18420 );
18421 assert!(
18422 source.contains("fn vs_shape_instanced("),
18423 "the storage rewrite must leave the instanced-quad vertex entry intact"
18424 );
18425 assert_eq!(
18426 source
18427 .matches("select(shape.color, paint[shape_idx], similarity.paint_select > 0.5)")
18428 .count(),
18429 3,
18430 "vs_main, vs_shape_instanced and vs_mesh must all read paint under \
18431 the paint_select flag (meshless retained draws ride the instanced \
18432 entry when the selection is latched on)"
18433 );
18434
18435 let module = naga::front::wgsl::parse_str(&source)
18438 .expect("storage-mode shape shader must parse as WGSL");
18439 naga::valid::Validator::new(
18440 naga::valid::ValidationFlags::all(),
18441 naga::valid::Capabilities::all(),
18442 )
18443 .validate(&module)
18444 .expect("storage-mode shape shader must validate for WebGPU");
18445 }
18446
18447 #[test]
18448 fn uniform_shape_shader_keeps_the_in_record_color_and_no_paint_binding() {
18449 for source in [
18453 Cow::Borrowed(shaders::SHADER),
18454 shape_shader_source(ShapeBatchLimits::desktop()),
18455 ] {
18456 assert!(
18457 !source.contains("paint: array"),
18458 "the uniform variant must not declare a paint array"
18459 );
18460 assert!(
18461 source.contains("output.color = shape.color;"),
18462 "the uniform variant must read the color from ShapeData \
18463 (this literal is also what `shape_shader_source` rewrites)"
18464 );
18465 assert!(
18466 source.contains("paint_select: f32"),
18467 "SimilarityTransform must name the flag field in both \
18468 variants; the Rust mirror is Pod and uploads raw bytes"
18469 );
18470 }
18471 }
18472
18473 #[test]
18474 fn shipped_shape_shader_array_length_fits_the_downlevel_uniform_floor() {
18475 assert!(
18479 shaders::SHADER.contains("array<ShapeData, 102>"),
18480 "shape.wgsl array length must stay in sync with \
18481 `shape_shader_source`'s replace string and MAX_SHAPES_PER_BATCH"
18482 );
18483 assert!(102 * std::mem::size_of::<ShapeData>() <= 16384);
18484 assert!(103 * std::mem::size_of::<ShapeData>() > 16384);
18485 }
18486
18487 #[test]
18488 fn glyph_atlas_doubles_on_overflow_and_stops_at_the_device_ceiling() {
18489 assert_eq!(
18492 next_glyph_atlas_size(TEXT_GLYPH_ATLAS_MIN_SIZE, TEXT_GLYPH_ATLAS_MAX_SIZE),
18493 1024
18494 );
18495 assert_eq!(
18496 next_glyph_atlas_size(2048, TEXT_GLYPH_ATLAS_MAX_SIZE),
18497 TEXT_GLYPH_ATLAS_MAX_SIZE
18498 );
18499 assert_eq!(
18500 next_glyph_atlas_size(TEXT_GLYPH_ATLAS_MAX_SIZE, TEXT_GLYPH_ATLAS_MAX_SIZE),
18501 TEXT_GLYPH_ATLAS_MAX_SIZE
18502 );
18503
18504 assert_eq!(next_glyph_atlas_size(1024, 2048), 2048);
18507 assert_eq!(next_glyph_atlas_size(2048, 2048), 2048);
18508
18509 assert_eq!(next_glyph_atlas_size(u32::MAX, 4096), 4096);
18511 assert_eq!(next_glyph_atlas_size(0, 0), 1);
18512 }
18513
18514 #[test]
18515 fn glyph_atlas_uv_rect_normalizes_against_the_atlas_it_was_placed_in() {
18516 let entry = GlyphAtlasEntry {
18520 x: 128,
18521 y: 256,
18522 width: 16,
18523 height: 32,
18524 };
18525
18526 let small = glyph_atlas_uv_rect(entry, 512);
18527 let large = glyph_atlas_uv_rect(entry, 4096);
18528
18529 assert_eq!(small.min, [128.0 / 512.0, 256.0 / 512.0]);
18530 assert_eq!(large.min, [128.0 / 4096.0, 256.0 / 4096.0]);
18531 assert_eq!(small.max, [144.0 / 512.0, 288.0 / 512.0]);
18532 assert_eq!(large.max, [144.0 / 4096.0, 288.0 / 4096.0]);
18533 }
18534
18535 #[test]
18536 fn native_shape_shader_source_uses_native_batch_limits() {
18537 let limits = ShapeBatchLimits::desktop();
18538 let source = shape_shader_source(limits);
18539
18540 assert!(source.contains(&format!(
18541 "array<ShapeData, {}>",
18542 limits.max_shapes_per_batch
18543 )));
18544 assert!(source.contains(&format!(
18545 "array<GradientStop, {}>",
18546 limits.max_gradient_stops
18547 )));
18548 assert!(!source.contains("array<ShapeData, 146>"));
18551 }
18552
18553 #[test]
18554 fn stroked_and_arc_shapes_batch_together_with_fills() {
18555 let fill = test_shape(0, BlendMode::SrcOver);
18561 let mut stroked = test_shape(1, BlendMode::SrcOver);
18562 stroked.stroke = Some(
18563 cranpose_ui_graphics::Stroke::new(3.0)
18564 .with_cap(StrokeCap::Round)
18565 .with_join(StrokeJoin::Bevel),
18566 );
18567 let mut arc = test_shape(2, BlendMode::SrcOver);
18568 arc.arc = Some(cranpose_ui_graphics::ArcGeometry::new(
18569 Point::new(4.0, 4.0),
18570 2.0,
18571 4.0,
18572 0.0,
18573 1.0,
18574 StrokeCap::Round,
18575 ));
18576 let trailing_fill = test_shape(3, BlendMode::SrcOver);
18577
18578 assert!(!fill.has_stroke_or_arc());
18579 assert!(stroked.has_stroke_or_arc());
18580 assert!(arc.has_stroke_or_arc());
18581 assert!(!trailing_fill.has_stroke_or_arc());
18582
18583 let shapes = vec![fill, stroked, arc, trailing_fill];
18584 let ordered_items: Vec<_> = (0..shapes.len())
18585 .map(|index| (index, SegmentDrawItem::Shape(index)))
18586 .collect();
18587 let images = Vec::new();
18588
18589 let commands: Vec<_> = SegmentCommandIter::new(
18590 &ordered_items,
18591 &shapes,
18592 &images,
18593 ShapeBatchLimits::desktop(),
18594 )
18595 .collect();
18596
18597 assert_eq!(
18598 commands,
18599 vec![SegmentRenderCommand::DrawChunk(chunk(&[
18600 SegmentBatchPlan::Shape {
18601 start: 0,
18602 end: 4,
18603 blend_mode: BlendMode::SrcOver,
18604 }
18605 ]))],
18606 "mixed fill/stroke/arc runs must stay one batch"
18607 );
18608 }
18609
18610 #[cfg(not(target_arch = "wasm32"))]
18611 #[test]
18612 fn native_segment_fusion_budget_allows_small_interleaved_chunks() {
18613 let ordered_items = vec![
18614 (0, SegmentDrawItem::Shape(0)),
18615 (1, SegmentDrawItem::Image(0)),
18616 (2, SegmentDrawItem::Text(0)),
18617 (3, SegmentDrawItem::Shape(1)),
18618 ];
18619 let shapes = vec![
18620 test_shape(0, BlendMode::SrcOver),
18621 test_shape(3, BlendMode::DstOut),
18622 ];
18623 let segment = chunk(&[
18624 SegmentBatchPlan::Shape {
18625 start: 0,
18626 end: 1,
18627 blend_mode: BlendMode::SrcOver,
18628 },
18629 SegmentBatchPlan::Image {
18630 start: 1,
18631 end: 2,
18632 blend_mode: BlendMode::SrcOver,
18633 },
18634 SegmentBatchPlan::Text { start: 2, end: 3 },
18635 SegmentBatchPlan::Shape {
18636 start: 3,
18637 end: 4,
18638 blend_mode: BlendMode::DstOut,
18639 },
18640 ]);
18641
18642 let budget = native_segment_fusion_budget(
18643 &ordered_items,
18644 &shapes,
18645 &segment,
18646 ShapeBatchLimits::desktop(),
18647 )
18648 .expect("budget should be valid")
18649 .expect("chunk should fit native fusion budget");
18650
18651 assert_eq!(
18652 budget,
18653 NativeSegmentFusionBudget {
18654 shape_count: 2,
18655 gradient_stop_count: 0,
18656 }
18657 );
18658 }
18659
18660 #[cfg(not(target_arch = "wasm32"))]
18661 #[test]
18662 fn native_segment_fusion_budget_rejects_shape_uniform_overflow() {
18663 let ordered_items: Vec<_> = (0..=MAX_SHAPES_PER_BATCH)
18664 .map(|index| (index, SegmentDrawItem::Shape(index)))
18665 .collect();
18666 let shapes: Vec<_> = (0..=MAX_SHAPES_PER_BATCH)
18667 .map(|index| test_shape(index, BlendMode::SrcOver))
18668 .collect();
18669 let segment = chunk(&[
18670 SegmentBatchPlan::Shape {
18671 start: 0,
18672 end: MAX_SHAPES_PER_BATCH,
18673 blend_mode: BlendMode::SrcOver,
18674 },
18675 SegmentBatchPlan::Shape {
18676 start: MAX_SHAPES_PER_BATCH,
18677 end: MAX_SHAPES_PER_BATCH + 1,
18678 blend_mode: BlendMode::SrcOver,
18679 },
18680 ]);
18681
18682 let budget = native_segment_fusion_budget(
18683 &ordered_items,
18684 &shapes,
18685 &segment,
18686 ShapeBatchLimits::desktop(),
18687 )
18688 .expect("valid plan");
18689
18690 assert_eq!(budget, None);
18691 }
18692
18693 #[cfg(not(target_arch = "wasm32"))]
18694 #[test]
18695 fn native_segment_fusion_budget_rejects_gradient_uniform_overflow() {
18696 let ordered_items = vec![(0, SegmentDrawItem::Shape(0))];
18697 let mut shape = test_shape(0, BlendMode::SrcOver);
18698 shape.brush = Brush::linear_gradient(vec![Color::BLACK; MAX_GRADIENT_STOPS + 1]);
18699 let shapes = vec![shape];
18700 let segment = chunk(&[SegmentBatchPlan::Shape {
18701 start: 0,
18702 end: 1,
18703 blend_mode: BlendMode::SrcOver,
18704 }]);
18705
18706 let budget = native_segment_fusion_budget(
18707 &ordered_items,
18708 &shapes,
18709 &segment,
18710 ShapeBatchLimits::desktop(),
18711 )
18712 .expect("valid plan");
18713
18714 assert_eq!(budget, None);
18715 }
18716
18717 #[cfg(not(target_arch = "wasm32"))]
18718 #[test]
18719 fn native_segment_fusion_partitions_shape_uniform_overflow() {
18720 let desktop_batch_cap = ShapeBatchLimits::desktop().max_shapes_per_batch;
18723 let ordered_items: Vec<_> = (0..=desktop_batch_cap)
18724 .map(|index| (index, SegmentDrawItem::Shape(index)))
18725 .collect();
18726 let shapes: Vec<_> = (0..=desktop_batch_cap)
18727 .map(|index| test_shape(index, BlendMode::SrcOver))
18728 .collect();
18729 let segment = chunk(&[
18730 SegmentBatchPlan::Shape {
18731 start: 0,
18732 end: desktop_batch_cap,
18733 blend_mode: BlendMode::SrcOver,
18734 },
18735 SegmentBatchPlan::Shape {
18736 start: desktop_batch_cap,
18737 end: desktop_batch_cap + 1,
18738 blend_mode: BlendMode::SrcOver,
18739 },
18740 ]);
18741
18742 let partitions = native_segment_fusion_partitions(
18743 &ordered_items,
18744 &shapes,
18745 &segment,
18746 ShapeBatchLimits::desktop(),
18747 )
18748 .expect("valid plan")
18749 .expect("overflowing segment should be partitionable");
18750
18751 assert_eq!(partitions.len(), 2);
18752 assert_eq!(
18753 partitions[0],
18754 NativeSegmentFusionPartition {
18755 chunk: chunk(&[SegmentBatchPlan::Shape {
18756 start: 0,
18757 end: desktop_batch_cap,
18758 blend_mode: BlendMode::SrcOver,
18759 }]),
18760 budget: NativeSegmentFusionBudget {
18761 shape_count: desktop_batch_cap,
18762 gradient_stop_count: 0,
18763 },
18764 }
18765 );
18766 assert_eq!(
18767 partitions[1],
18768 NativeSegmentFusionPartition {
18769 chunk: chunk(&[SegmentBatchPlan::Shape {
18770 start: desktop_batch_cap,
18771 end: desktop_batch_cap + 1,
18772 blend_mode: BlendMode::SrcOver,
18773 }]),
18774 budget: NativeSegmentFusionBudget {
18775 shape_count: 1,
18776 gradient_stop_count: 0,
18777 },
18778 }
18779 );
18780 }
18781
18782 #[cfg(not(target_arch = "wasm32"))]
18783 #[test]
18784 fn native_segment_fusion_partitions_gradient_uniform_overflow() {
18785 const STOPS_PER_SHAPE: usize = MAX_GRADIENT_STOPS / 2;
18786 let ordered_items = vec![
18787 (0, SegmentDrawItem::Shape(0)),
18788 (1, SegmentDrawItem::Shape(1)),
18789 (2, SegmentDrawItem::Shape(2)),
18790 ];
18791 let mut shapes = Vec::new();
18792 for index in 0..3 {
18793 let mut shape = test_shape(index, BlendMode::SrcOver);
18794 shape.brush = Brush::linear_gradient(vec![Color::BLACK; STOPS_PER_SHAPE]);
18795 shapes.push(shape);
18796 }
18797 let segment = chunk(&[SegmentBatchPlan::Shape {
18798 start: 0,
18799 end: 3,
18800 blend_mode: BlendMode::SrcOver,
18801 }]);
18802
18803 let partitions = native_segment_fusion_partitions(
18804 &ordered_items,
18805 &shapes,
18806 &segment,
18807 ShapeBatchLimits::desktop(),
18808 )
18809 .expect("valid plan")
18810 .expect("overflowing gradient segment should be partitionable");
18811
18812 assert_eq!(partitions.len(), 2);
18813 assert_eq!(
18814 partitions[0],
18815 NativeSegmentFusionPartition {
18816 chunk: chunk(&[SegmentBatchPlan::Shape {
18817 start: 0,
18818 end: 2,
18819 blend_mode: BlendMode::SrcOver,
18820 }]),
18821 budget: NativeSegmentFusionBudget {
18822 shape_count: 2,
18823 gradient_stop_count: MAX_GRADIENT_STOPS,
18824 },
18825 }
18826 );
18827 assert_eq!(
18828 partitions[1],
18829 NativeSegmentFusionPartition {
18830 chunk: chunk(&[SegmentBatchPlan::Shape {
18831 start: 2,
18832 end: 3,
18833 blend_mode: BlendMode::SrcOver,
18834 }]),
18835 budget: NativeSegmentFusionBudget {
18836 shape_count: 1,
18837 gradient_stop_count: STOPS_PER_SHAPE,
18838 },
18839 }
18840 );
18841 }
18842
18843 #[cfg(not(target_arch = "wasm32"))]
18844 #[test]
18845 fn native_segment_fusion_accepts_layer_composite_chunks() {
18846 let ordered_items = vec![
18847 (0, SegmentDrawItem::Shape(0)),
18848 (1, SegmentDrawItem::Composite(0)),
18849 (2, SegmentDrawItem::ShaderComposite(0)),
18850 (3, SegmentDrawItem::Shape(1)),
18851 ];
18852 let shapes = vec![
18853 test_shape(0, BlendMode::SrcOver),
18854 test_shape(1, BlendMode::SrcOver),
18855 ];
18856 let segment = chunk(&[
18857 SegmentBatchPlan::Shape {
18858 start: 0,
18859 end: 1,
18860 blend_mode: BlendMode::SrcOver,
18861 },
18862 SegmentBatchPlan::Composite { start: 1, end: 2 },
18863 SegmentBatchPlan::ShaderComposite { start: 2, end: 3 },
18864 SegmentBatchPlan::Shape {
18865 start: 3,
18866 end: 4,
18867 blend_mode: BlendMode::SrcOver,
18868 },
18869 ]);
18870
18871 let partitions = native_segment_fusion_partitions(
18872 &ordered_items,
18873 &shapes,
18874 &segment,
18875 ShapeBatchLimits::desktop(),
18876 )
18877 .expect("valid plan")
18878 .expect("composites are drawable inside the native fused pass");
18879
18880 assert_eq!(
18881 partitions,
18882 vec![NativeSegmentFusionPartition {
18883 chunk: segment,
18884 budget: NativeSegmentFusionBudget {
18885 shape_count: 2,
18886 gradient_stop_count: 0,
18887 },
18888 }],
18889 "layer composites and shader composites must preserve order without forcing separate render passes"
18890 );
18891 }
18892
18893 #[cfg(not(target_arch = "wasm32"))]
18894 #[test]
18895 fn native_segment_fusion_partitions_preserve_non_shape_order_at_budget_boundary() {
18896 let desktop_batch_cap = ShapeBatchLimits::desktop().max_shapes_per_batch;
18899 let ordered_items: Vec<_> = (0..desktop_batch_cap)
18900 .map(|index| (index, SegmentDrawItem::Shape(index)))
18901 .chain([
18902 (desktop_batch_cap, SegmentDrawItem::Image(0)),
18903 (
18904 desktop_batch_cap + 1,
18905 SegmentDrawItem::Shape(desktop_batch_cap),
18906 ),
18907 ])
18908 .collect();
18909 let shapes: Vec<_> = (0..=desktop_batch_cap)
18910 .map(|index| test_shape(index, BlendMode::SrcOver))
18911 .collect();
18912 let segment = chunk(&[
18913 SegmentBatchPlan::Shape {
18914 start: 0,
18915 end: desktop_batch_cap,
18916 blend_mode: BlendMode::SrcOver,
18917 },
18918 SegmentBatchPlan::Image {
18919 start: desktop_batch_cap,
18920 end: desktop_batch_cap + 1,
18921 blend_mode: BlendMode::SrcOver,
18922 },
18923 SegmentBatchPlan::Shape {
18924 start: desktop_batch_cap + 1,
18925 end: desktop_batch_cap + 2,
18926 blend_mode: BlendMode::SrcOver,
18927 },
18928 ]);
18929
18930 let partitions = native_segment_fusion_partitions(
18931 &ordered_items,
18932 &shapes,
18933 &segment,
18934 ShapeBatchLimits::desktop(),
18935 )
18936 .expect("valid plan")
18937 .expect("overflowing segment should be partitionable");
18938
18939 assert_eq!(partitions.len(), 2);
18940 assert_eq!(
18941 partitions[0].chunk,
18942 chunk(&[
18943 SegmentBatchPlan::Shape {
18944 start: 0,
18945 end: desktop_batch_cap,
18946 blend_mode: BlendMode::SrcOver,
18947 },
18948 SegmentBatchPlan::Image {
18949 start: desktop_batch_cap,
18950 end: desktop_batch_cap + 1,
18951 blend_mode: BlendMode::SrcOver,
18952 },
18953 ])
18954 );
18955 assert_eq!(
18956 partitions[1].chunk,
18957 chunk(&[SegmentBatchPlan::Shape {
18958 start: desktop_batch_cap + 1,
18959 end: desktop_batch_cap + 2,
18960 blend_mode: BlendMode::SrcOver,
18961 }])
18962 );
18963 }
18964
18965 #[test]
18966 fn segment_command_iter_keeps_repeated_batch_kinds_in_one_chunk() {
18967 let ordered_items = vec![
18968 (0, SegmentDrawItem::Shape(0)),
18969 (1, SegmentDrawItem::Image(0)),
18970 (2, SegmentDrawItem::Shape(1)),
18971 ];
18972 let shapes = vec![
18973 test_shape(0, BlendMode::SrcOver),
18974 test_shape(2, BlendMode::DstOut),
18975 ];
18976 let images = vec![test_image(1, BlendMode::SrcOver)];
18977
18978 let commands: Vec<_> = SegmentCommandIter::new(
18979 &ordered_items,
18980 &shapes,
18981 &images,
18982 ShapeBatchLimits::desktop(),
18983 )
18984 .collect();
18985
18986 assert_eq!(
18987 commands,
18988 vec![SegmentRenderCommand::DrawChunk(chunk(&[
18989 SegmentBatchPlan::Shape {
18990 start: 0,
18991 end: 1,
18992 blend_mode: BlendMode::SrcOver,
18993 },
18994 SegmentBatchPlan::Image {
18995 start: 1,
18996 end: 2,
18997 blend_mode: BlendMode::SrcOver,
18998 },
18999 SegmentBatchPlan::Shape {
19000 start: 2,
19001 end: 3,
19002 blend_mode: BlendMode::DstOut,
19003 },
19004 ]))]
19005 );
19006 }
19007
19008 #[test]
19009 fn segment_command_iter_splits_contiguous_shape_runs_at_uniform_batch_limit() {
19010 let desktop_batch_cap = ShapeBatchLimits::desktop().max_shapes_per_batch;
19013 let ordered_items: Vec<_> = (0..=desktop_batch_cap)
19014 .map(|index| (index, SegmentDrawItem::Shape(index)))
19015 .collect();
19016 let shapes: Vec<_> = (0..=desktop_batch_cap)
19017 .map(|index| test_shape(index, BlendMode::SrcOver))
19018 .collect();
19019 let images = Vec::new();
19020
19021 let commands: Vec<_> = SegmentCommandIter::new(
19022 &ordered_items,
19023 &shapes,
19024 &images,
19025 ShapeBatchLimits::desktop(),
19026 )
19027 .collect();
19028
19029 assert_eq!(
19030 commands,
19031 vec![SegmentRenderCommand::DrawChunk(chunk(&[
19032 SegmentBatchPlan::Shape {
19033 start: 0,
19034 end: desktop_batch_cap,
19035 blend_mode: BlendMode::SrcOver,
19036 },
19037 SegmentBatchPlan::Shape {
19038 start: desktop_batch_cap,
19039 end: desktop_batch_cap + 1,
19040 blend_mode: BlendMode::SrcOver,
19041 },
19042 ]))]
19043 );
19044 }
19045
19046 #[test]
19047 fn segment_command_iter_keeps_shadows_as_explicit_boundaries() {
19048 let ordered_items = vec![
19049 (0, SegmentDrawItem::Shape(0)),
19050 (1, SegmentDrawItem::Shadow(0)),
19051 (2, SegmentDrawItem::Image(0)),
19052 (3, SegmentDrawItem::Text(0)),
19053 ];
19054 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
19055 let images = vec![test_image(2, BlendMode::SrcOver)];
19056
19057 let commands: Vec<_> = SegmentCommandIter::new(
19058 &ordered_items,
19059 &shapes,
19060 &images,
19061 ShapeBatchLimits::desktop(),
19062 )
19063 .collect();
19064
19065 assert_eq!(
19066 commands,
19067 vec![
19068 SegmentRenderCommand::DrawChunk(chunk(&[SegmentBatchPlan::Shape {
19069 start: 0,
19070 end: 1,
19071 blend_mode: BlendMode::SrcOver,
19072 }])),
19073 SegmentRenderCommand::Shadow(0),
19074 SegmentRenderCommand::DrawChunk(chunk(&[
19075 SegmentBatchPlan::Image {
19076 start: 2,
19077 end: 3,
19078 blend_mode: BlendMode::SrcOver,
19079 },
19080 SegmentBatchPlan::Text { start: 3, end: 4 },
19081 ])),
19082 ]
19083 );
19084 }
19085
19086 #[test]
19087 fn staged_buffer_uploads_align_new_copies_to_copy_buffer_alignment() {
19088 let mut uploads = StagedBufferUploads::default();
19089 uploads.bytes.extend_from_slice(&[1, 2]);
19090
19091 uploads.stage(UploadTarget::ImageIndex, &[3, 4, 5, 6]);
19092
19093 assert_eq!(uploads.bytes, vec![1, 2, 0, 0, 3, 4, 5, 6]);
19094 assert_eq!(
19095 uploads.copies,
19096 vec![PendingBufferCopy {
19097 source_offset: 4,
19098 target_offset: 0,
19099 size: 4,
19100 target: UploadTarget::ImageIndex,
19101 }]
19102 );
19103 }
19104
19105 #[test]
19106 fn staged_buffer_uploads_ignore_empty_payloads() {
19107 let mut uploads = StagedBufferUploads::default();
19108
19109 uploads.stage(UploadTarget::Uniform, &[]);
19110
19111 assert!(uploads.is_empty());
19112 assert!(uploads.bytes.is_empty());
19113 }
19114
19115 #[test]
19116 fn staged_buffer_uploads_return_exact_payload_slice_for_copy() {
19117 let mut uploads = StagedBufferUploads::default();
19118 uploads.stage(UploadTarget::Uniform, &[1, 2, 3, 4]);
19119 uploads.stage(UploadTarget::ImageIndex, &[5, 6, 7, 8]);
19120
19121 assert_eq!(uploads.payload_for_copy(uploads.copies[0]), &[1, 2, 3, 4]);
19122 assert_eq!(uploads.payload_for_copy(uploads.copies[1]), &[5, 6, 7, 8]);
19123 }
19124
19125 #[test]
19126 fn staged_buffer_uploads_record_destination_offsets() {
19127 let mut uploads = StagedBufferUploads::default();
19128
19129 uploads.stage_at(UploadTarget::ImageIndex, 256, &[1, 2, 3, 4]);
19130
19131 assert_eq!(uploads.copies[0].target_offset, 256);
19132 assert_eq!(uploads.payload_for_copy(uploads.copies[0]), &[1, 2, 3, 4]);
19133 }
19134
19135 #[test]
19136 fn staged_buffer_uploads_truncate_restores_previous_state() {
19137 let mut uploads = StagedBufferUploads::default();
19138 uploads.stage(UploadTarget::Uniform, &[1, 2, 3, 4]);
19139 let bytes_len = uploads.bytes.len();
19140 let copies_len = uploads.copies.len();
19141 uploads.stage(UploadTarget::ImageIndex, &[5, 6, 7, 8]);
19142
19143 uploads.truncate(bytes_len, copies_len);
19144
19145 assert_eq!(uploads.bytes, vec![1, 2, 3, 4]);
19146 assert_eq!(uploads.copies.len(), 1);
19147 }
19148
19149 #[test]
19150 fn inner_shadow_composite_mask_uses_fill_shape_and_scale() {
19151 let mut fill = test_shape(0, BlendMode::SrcOver);
19152 fill.local_rect = Rect {
19153 x: 10.0,
19154 y: 12.0,
19155 width: 40.0,
19156 height: 20.0,
19157 };
19158 fill.shape = Some(RoundedCornerShape::uniform(6.0));
19159
19160 let cutout = test_shape(1, BlendMode::DstOut);
19161 let shadow = test_shadow_draw(vec![
19162 (fill, BlendMode::SrcOver),
19163 (cutout, BlendMode::DstOut),
19164 ]);
19165
19166 let mask = inner_shadow_composite_mask(&shadow, 1.5).expect("inner mask expected");
19167 assert_eq!(mask.rect, [15.0, 18.0, 60.0, 30.0]);
19168 assert_eq!(mask.radii, [9.0, 9.0, 9.0, 9.0]);
19169 }
19170
19171 #[test]
19172 fn inner_shadow_composite_mask_is_none_without_dst_out() {
19173 let fill = test_shape(0, BlendMode::SrcOver);
19174 let shadow = test_shadow_draw(vec![(fill, BlendMode::SrcOver)]);
19175 assert!(inner_shadow_composite_mask(&shadow, 1.0).is_none());
19176 }
19177
19178 #[test]
19179 fn render_effect_support_matrix_covers_all_variants() {
19180 let blur = RenderEffect::blur(4.0);
19181 let offset = RenderEffect::offset(2.0, 3.0);
19182 let shader = RenderEffect::runtime_shader(cranpose_ui_graphics::RuntimeShader::new(
19183 r#"
19184 @group(0) @binding(0) var input_texture: texture_2d<f32>;
19185 @group(0) @binding(1) var input_sampler: sampler;
19186 @group(1) @binding(0) var<uniform> u: array<vec4<f32>, 64>;
19187 struct VertexOutput {
19188 @builtin(position) position: vec4<f32>,
19189 @location(0) uv: vec2<f32>,
19190 }
19191 @vertex
19192 fn fullscreen_vs(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
19193 var output: VertexOutput;
19194 let x = f32(i32(vertex_index & 1u) * 2 - 1);
19195 let y = f32(i32(vertex_index >> 1u) * 2 - 1);
19196 output.uv = vec2<f32>(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
19197 output.position = vec4<f32>(x, y, 0.0, 1.0);
19198 return output;
19199 }
19200 @fragment
19201 fn effect_fs(input: VertexOutput) -> @location(0) vec4<f32> {
19202 return textureSample(input_texture, input_sampler, input.uv);
19203 }
19204 "#,
19205 ));
19206 let chain = blur.clone().then(offset.clone());
19207
19208 assert!(is_render_effect_supported(&blur));
19209 assert!(is_render_effect_supported(&offset));
19210 assert!(is_render_effect_supported(&shader));
19211 assert!(is_render_effect_supported(&chain));
19212 }
19213
19214 #[test]
19215 fn clip_to_bounds_propagates_visual_clip_to_all_descendant_shapes() {
19216 let container_local_bounds = Rect {
19219 x: 0.0,
19220 y: 0.0,
19221 width: 800.0,
19222 height: 500.0,
19223 };
19224 let container_clip_in_parent = Rect {
19226 x: 0.0,
19227 y: 50.0,
19228 width: 800.0,
19229 height: 500.0,
19230 };
19231
19232 let shape_above = RenderNode::Primitive(PrimitiveEntry {
19234 phase: PrimitivePhase::BeforeChildren,
19235 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19236 primitive: DrawPrimitive::Rect {
19237 rect: Rect {
19238 x: 10.0,
19239 y: -30.0,
19240 width: 100.0,
19241 height: 40.0,
19242 },
19243 brush: Brush::solid(Color::WHITE),
19244 stroke: None,
19245 },
19246 clip: None,
19247 }),
19248 });
19249
19250 let shape_inside = RenderNode::Primitive(PrimitiveEntry {
19252 phase: PrimitivePhase::BeforeChildren,
19253 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19254 primitive: DrawPrimitive::Rect {
19255 rect: Rect {
19256 x: 10.0,
19257 y: 100.0,
19258 width: 100.0,
19259 height: 40.0,
19260 },
19261 brush: Brush::solid(Color::WHITE),
19262 stroke: None,
19263 },
19264 clip: None,
19265 }),
19266 });
19267
19268 let shape_below = RenderNode::Primitive(PrimitiveEntry {
19270 phase: PrimitivePhase::BeforeChildren,
19271 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19272 primitive: DrawPrimitive::Rect {
19273 rect: Rect {
19274 x: 10.0,
19275 y: 600.0,
19276 width: 100.0,
19277 height: 40.0,
19278 },
19279 brush: Brush::solid(Color::WHITE),
19280 stroke: None,
19281 },
19282 clip: None,
19283 }),
19284 });
19285
19286 let mut content_layer = test_layer(
19288 Rect {
19289 x: 0.0,
19290 y: 0.0,
19291 width: 800.0,
19292 height: 1000.0,
19293 },
19294 vec![shape_above, shape_inside, shape_below],
19295 );
19296 content_layer.transform_to_parent = ProjectiveTransform::translation(0.0, -30.0);
19297 content_layer.translated_content_context = true;
19298
19299 let mut clip_container = test_layer(
19301 container_local_bounds,
19302 vec![RenderNode::Layer(Box::new(content_layer))],
19303 );
19304 clip_container.clip_to_bounds = true;
19305 clip_container.transform_to_parent = ProjectiveTransform::translation(0.0, 50.0);
19306
19307 let root = test_layer(
19309 Rect {
19310 x: 0.0,
19311 y: 0.0,
19312 width: 800.0,
19313 height: 600.0,
19314 },
19315 vec![RenderNode::Layer(Box::new(clip_container))],
19316 );
19317
19318 let mut rect_cache = HashMap::new();
19319 let mut requirements_cache = HashMap::new();
19320 let collected =
19321 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
19322
19323 assert_eq!(
19324 collected.scene.shapes.len(),
19325 3,
19326 "all three shapes should be flattened into the scene"
19327 );
19328
19329 for (i, shape) in collected.scene.shapes.iter().enumerate() {
19330 assert!(
19331 shape.clip.is_some(),
19332 "shape {} at rect {:?} must have a clip from clip_to_bounds container, but clip is None",
19333 i,
19334 shape.rect
19335 );
19336 let clip = shape.clip.unwrap();
19337 assert_eq!(
19338 clip, container_clip_in_parent,
19339 "shape {} clip should match the clip_to_bounds container bounds in parent space",
19340 i
19341 );
19342 }
19343 }
19344
19345 #[test]
19346 fn clip_to_bounds_culls_child_layers_outside_boundary() {
19347 let clip_container_bounds = Rect {
19355 x: 0.0,
19356 y: 0.0,
19357 width: 800.0,
19358 height: 500.0,
19359 };
19360
19361 let shape_in_card = RenderNode::Primitive(PrimitiveEntry {
19362 phase: PrimitivePhase::BeforeChildren,
19363 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19364 primitive: DrawPrimitive::Rect {
19365 rect: Rect {
19366 x: 0.0,
19367 y: 0.0,
19368 width: 300.0,
19369 height: 80.0,
19370 },
19371 brush: Brush::solid(Color::WHITE),
19372 stroke: None,
19373 },
19374 clip: None,
19375 }),
19376 });
19377
19378 let mut card_outside = crate::test_support::layer_node(
19380 Rect {
19381 x: 0.0,
19382 y: 0.0,
19383 width: 300.0,
19384 height: 80.0,
19385 },
19386 ProjectiveTransform::identity(),
19387 GraphicsLayer {
19388 clip: true,
19389 ..GraphicsLayer::default()
19390 },
19391 vec![shape_in_card.clone()],
19392 );
19393 card_outside.transform_to_parent = ProjectiveTransform::translation(10.0, 600.0);
19394
19395 let mut card_inside = crate::test_support::layer_node(
19397 Rect {
19398 x: 0.0,
19399 y: 0.0,
19400 width: 300.0,
19401 height: 80.0,
19402 },
19403 ProjectiveTransform::identity(),
19404 GraphicsLayer {
19405 clip: true,
19406 ..GraphicsLayer::default()
19407 },
19408 vec![shape_in_card],
19409 );
19410 card_inside.transform_to_parent = ProjectiveTransform::translation(10.0, 100.0);
19411
19412 let content = test_layer(
19414 Rect {
19415 x: 0.0,
19416 y: 0.0,
19417 width: 800.0,
19418 height: 1000.0,
19419 },
19420 vec![
19421 RenderNode::Layer(Box::new(card_inside)),
19422 RenderNode::Layer(Box::new(card_outside)),
19423 ],
19424 );
19425
19426 let mut clip_container = test_layer(
19428 clip_container_bounds,
19429 vec![RenderNode::Layer(Box::new(content))],
19430 );
19431 clip_container.clip_to_bounds = true;
19432
19433 let root = test_layer(
19435 Rect {
19436 x: 0.0,
19437 y: 0.0,
19438 width: 800.0,
19439 height: 600.0,
19440 },
19441 vec![RenderNode::Layer(Box::new(clip_container))],
19442 );
19443
19444 let mut rect_cache = HashMap::new();
19445 let mut requirements_cache = HashMap::new();
19446 let collected =
19447 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
19448
19449 assert_eq!(
19450 collected.scene.shapes.len(),
19451 1,
19452 "only the card inside the clip boundary should produce shapes; \
19453 the card outside must be culled entirely"
19454 );
19455
19456 let shape = &collected.scene.shapes[0];
19457 assert!(
19458 shape.clip.is_some(),
19459 "the visible card's shape must have a clip from clip_to_bounds"
19460 );
19461 }
19462
19463 #[test]
19464 fn flattened_layer_shadow_z_index_is_below_content() {
19465 let shape = RenderNode::Primitive(PrimitiveEntry {
19469 phase: PrimitivePhase::BeforeChildren,
19470 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19471 primitive: DrawPrimitive::Rect {
19472 rect: Rect {
19473 x: 0.0,
19474 y: 0.0,
19475 width: 100.0,
19476 height: 100.0,
19477 },
19478 brush: Brush::solid(Color::WHITE),
19479 stroke: None,
19480 },
19481 clip: None,
19482 }),
19483 });
19484
19485 let child_bounds = Rect {
19486 x: 0.0,
19487 y: 0.0,
19488 width: 100.0,
19489 height: 100.0,
19490 };
19491
19492 let child = crate::test_support::layer_node(
19493 child_bounds,
19494 ProjectiveTransform::translation(50.0, 50.0),
19495 GraphicsLayer {
19496 shadow_elevation: 20.0,
19497 ..GraphicsLayer::default()
19498 },
19499 vec![shape],
19500 );
19501
19502 let root = test_layer(
19503 Rect {
19504 x: 0.0,
19505 y: 0.0,
19506 width: 800.0,
19507 height: 600.0,
19508 },
19509 vec![RenderNode::Layer(Box::new(child))],
19510 );
19511
19512 let mut rect_cache = HashMap::new();
19513 let mut requirements_cache = HashMap::new();
19514 let collected =
19515 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
19516
19517 assert!(
19518 !collected.scene.shadow_draws.is_empty(),
19519 "shadow_elevation > 0 must produce shadow draws"
19520 );
19521 let max_shadow_z = collected
19522 .scene
19523 .shadow_draws
19524 .iter()
19525 .map(|s| s.z_index)
19526 .max()
19527 .unwrap();
19528 let min_content_z = collected
19529 .scene
19530 .shapes
19531 .iter()
19532 .map(|s| s.z_index)
19533 .min()
19534 .unwrap();
19535 assert!(
19536 max_shadow_z < min_content_z,
19537 "shadow z-index ({}) must be less than content z-index ({}); \
19538 shadows must render behind their content",
19539 max_shadow_z,
19540 min_content_z
19541 );
19542 }
19543
19544 #[cfg(not(target_arch = "wasm32"))]
19547 fn bundle_op(slot: u32, epoch: Option<u64>, first: u32, last: u32) -> RetainedBundleOpKey {
19548 RetainedBundleOpKey {
19549 slot,
19550 capture_epoch: epoch,
19551 first,
19552 last,
19553 retained_index: slot,
19554 has_mesh: false,
19555 }
19556 }
19557
19558 #[cfg(not(target_arch = "wasm32"))]
19559 fn bundle_key(ops: &[RetainedBundleOpKey]) -> RetainedBundleKey {
19560 RetainedBundleKey { ops: ops.to_vec() }
19561 }
19562
19563 #[cfg(not(target_arch = "wasm32"))]
19566 #[test]
19567 fn retained_bundle_cache_reuses_stable_keys() {
19568 let mut cache: RetainedBundleCacheImpl<u32> = RetainedBundleCacheImpl::new();
19569 let ops = [bundle_op(3, Some(7), 0, 40), bundle_op(5, Some(9), 4, 12)];
19570 let key = bundle_key(&ops);
19571
19572 assert!(!cache.hit(&key), "empty cache must miss");
19573 cache.insert(key.clone(), 111);
19574 assert_eq!(cache.get(&key), Some(&111));
19575 cache.end_frame();
19576
19577 for _ in 0..3 {
19578 assert!(cache.hit(&bundle_key(&ops)), "stable key must stay cached");
19579 cache.end_frame();
19580 }
19581 assert_eq!(cache.stats(), (1, 3), "one rebuild, three cached executes");
19582 }
19583
19584 #[cfg(not(target_arch = "wasm32"))]
19588 #[test]
19589 fn retained_bundle_cache_invalidates_on_any_op_change() {
19590 let ops = [bundle_op(3, Some(7), 0, 40), bundle_op(5, Some(9), 4, 12)];
19591 let variants: [Vec<RetainedBundleOpKey>; 5] = [
19592 vec![bundle_op(3, Some(8), 0, 40), bundle_op(5, Some(9), 4, 12)],
19594 vec![bundle_op(5, Some(9), 4, 12), bundle_op(3, Some(7), 0, 40)],
19596 vec![bundle_op(3, Some(7), 0, 40)],
19598 vec![bundle_op(3, Some(7), 0, 41), bundle_op(5, Some(9), 4, 12)],
19600 vec![bundle_op(3, Some(7), 0, 40), bundle_op(5, None, 4, 12)],
19602 ];
19603 for changed in variants {
19604 let mut cache: RetainedBundleCacheImpl<u32> = RetainedBundleCacheImpl::new();
19605 cache.insert(bundle_key(&ops), 111);
19606 cache.end_frame();
19607 assert!(
19608 !cache.hit(&RetainedBundleKey {
19609 ops: changed.clone()
19610 }),
19611 "changed key {changed:?} must not reuse the stale bundle"
19612 );
19613 }
19614 }
19615
19616 #[cfg(not(target_arch = "wasm32"))]
19620 #[test]
19621 fn retained_bundle_cache_evicts_unused_entries() {
19622 let mut cache: RetainedBundleCacheImpl<u32> = RetainedBundleCacheImpl::new();
19623 let stale = bundle_key(&[bundle_op(1, Some(1), 0, 6)]);
19624 let live = bundle_key(&[bundle_op(2, Some(2), 0, 6)]);
19625 cache.insert(stale.clone(), 1);
19626 cache.insert(live.clone(), 2);
19627 cache.end_frame();
19628
19629 assert!(cache.hit(&live));
19630 cache.end_frame();
19631
19632 assert!(
19633 !cache.hit(&stale),
19634 "entry unused for a frame must have been evicted"
19635 );
19636 assert!(cache.hit(&live), "used entry must survive eviction");
19637
19638 cache.clear();
19639 assert!(!cache.hit(&live), "clear must drop every entry");
19640 }
19641}