1use crate::effect_renderer::{
4 projective_dest_bounds_rect, CompositeBatchItem, CompositeSampleMode, EffectRenderer,
5 EffectScratchTargetProvider, ProjectiveSurfaceComposite, RoundedCompositeMask,
6 ShaderCompositeBatchItem,
7};
8use crate::frame_graph::{
9 FrameCommandRecorder, FrameTextureDescriptor, WgpuFrameGraph, WgpuFrameGraphExecutor,
10};
11use crate::frame_packet::{
12 CancelReason, FramePacket, PacketRoot, PresentOutcome, RenderReturns, RootSurfacePacket,
13};
14use crate::layer_events::{
15 collect_effect_ranges, collect_layer_events, LayerEvent, LayerEventKind,
16};
17use crate::layer_surface_cache::LayerSurfaceCache;
18use crate::lazy_resource::LazyGpuResource;
19#[cfg(test)]
20use crate::normalized_scene::{
21 build_scene_window, collect_layer_contents, collect_layer_contents_with_translation_context,
22 filtered_effect_layer_index, scene_bounds, SceneWindowSource,
23};
24#[cfg(test)]
25use crate::normalized_scene::{estimate_layer_surface_rect, motion_stable_capture_bounds};
26use crate::normalized_scene::{translate_quad, ChildLayerComposite, CollectedLayer};
27use crate::offscreen::OffscreenTarget;
28use crate::rect_to_quad;
29use crate::scene::{
30 BackdropLayer, CompositorScene, DrawOp, DrawOpKind, DrawShape, EffectLayer, ImageDraw,
31 RetainedDraw, SceneBrush, ShadowDraw, SimilarityTransform, SnapAnchor, TextDraw,
32};
33use crate::shaders;
34#[cfg(test)]
35use crate::surface_executor::surface_target_size;
36use crate::surface_executor::{
37 apply_backdrop_layer_to_target as execute_apply_backdrop_layer_to_target,
38 axis_aligned_quad_rect, backdrop_underlay_is_covered_by_local_content,
39 canonicalize_device_coordinate, canonicalized_scaled_quad, canonicalized_scaled_rect,
40 composite_surface_to_view as execute_composite_surface_to_view, device_pixel_bounds_for_rect,
41 offscreen_byte_size, render_effect_layer_to_target as execute_render_effect_layer_to_target,
42 render_layer_surface as execute_render_layer_surface,
43 render_root_direct as execute_render_root_direct, root_direct_scene_events_are_supported,
44 scaled_quad, snap_delta_for_anchor, snap_motion_stable_dest_quad,
45 translation_stable_anchored_device_pixel_bounds, DevicePixelBounds, LayerSurfaceTexture,
46 SurfaceExecutionBackend,
47};
48#[cfg(test)]
49use crate::surface_executor::{clamp_effect_surface_scale, visible_layer_rect};
50#[cfg(test)]
51use crate::surface_plan::root_can_render_directly_cached;
52#[cfg(test)]
53use crate::surface_plan::{
54 composite_sample_mode_for_effect_layer, composite_sample_mode_for_requirements,
55 direct_translation, effect_layer_target_scale, layer_contains_descendant_backdrop,
56 layer_surface_requirements, layer_surface_requirements_cached, layer_surface_scale,
57 layer_surface_target_scale, layer_uses_external_backdrop_input, TranslatedContentAxes,
58};
59use crate::surface_plan::{LayerSurfaceRequest, TranslationRenderContext};
60#[cfg(test)]
61use crate::surface_requirements::SurfaceRequirement;
62use crate::surface_requirements::SurfaceRequirementSet;
63use crate::DebugCpuAllocationStats;
64use bytemuck::{Pod, Zeroable};
65#[cfg(any(not(target_arch = "wasm32"), test))]
66use cranpose_core::collections::map::HashMap;
67use cranpose_core::{hash::default as default_hash, NodeId};
68use cranpose_render_common::bounded_lru_cache::BoundedLruCache;
69use cranpose_render_common::geometry::blur_extent_margin;
70use cranpose_render_common::graph::quad_bounds;
71#[cfg(test)]
72use cranpose_render_common::graph::{
73 CachePolicy, LayerNode, PrimitiveEntry, PrimitiveNode, PrimitivePhase, ProjectiveTransform,
74 RenderNode,
75};
76use cranpose_render_common::raster_cache::LayerRasterCacheKey;
77#[cfg(test)]
78use cranpose_render_common::raster_cache::ScaleBucket;
79use cranpose_render_common::software_text_raster::{
80 collect_solid_text_atlas_run, measure_text_with_font,
81 rasterize_annotated_text_to_image_with_glyph_cache, rasterize_text_to_image_with_glyph_cache,
82 SoftwareGlyphAtlasGlyph, SoftwareGlyphAtlasKey, SoftwareGlyphAtlasPlacement,
83 SoftwareGlyphAtlasRunGlyph, SoftwareGlyphRasterCache, SoftwareTextFontSet,
84};
85#[cfg(test)]
86use cranpose_ui_graphics::GraphicsLayer;
87use cranpose_ui_graphics::{
88 BlendMode, Brush, Color, ColorFilter, FxHasher, ImageBitmap, ImageSampling, Point, Rect,
89 RenderEffect, RenderHash, RuntimeShader, StrokeCap, StrokeJoin, TileMode,
90};
91use std::borrow::Cow;
92use std::cell::Cell;
93use std::hash::{Hash, Hasher};
94use std::ops::Range;
95use std::rc::Rc;
96#[cfg(not(target_arch = "wasm32"))]
97use std::sync::atomic::{AtomicUsize, Ordering};
98use std::sync::{mpsc, Arc};
99use std::time::Duration;
100use web_time::Instant;
101
102use crate::gpu_stats;
103use crate::gpu_stats::gpu_stats_enabled;
104use crate::pipeline::push_layer_shadow;
105
106#[cfg(target_arch = "wasm32")]
111const MAX_SHAPES_PER_BATCH: usize = 102;
112#[cfg(not(target_arch = "wasm32"))]
113const MAX_SHAPES_PER_BATCH: usize = 768;
114#[cfg(target_arch = "wasm32")]
115const MAX_GRADIENT_STOPS: usize = 256;
116#[cfg(not(target_arch = "wasm32"))]
117const MAX_GRADIENT_STOPS: usize = 1024;
118
119#[cfg(not(target_arch = "wasm32"))]
126const MAX_SHAPES_PER_STORAGE_BATCH: usize = 1 << 16;
127#[cfg(not(target_arch = "wasm32"))]
128const MAX_GRADIENT_STOPS_PER_STORAGE_BATCH: usize = 1 << 16;
129
130#[cfg(not(target_arch = "wasm32"))]
136const INITIAL_STORAGE_BATCH_CAPACITY: usize = 1024;
137
138#[derive(Clone, Copy, Debug, Eq, PartialEq)]
150struct ShapeBatchLimits {
151 max_shapes_per_batch: usize,
152 max_gradient_stops: usize,
153 storage: bool,
154}
155
156impl ShapeBatchLimits {
157 fn for_device(device: &wgpu::Device) -> Self {
158 let limits = device.limits();
159 #[cfg(not(target_arch = "wasm32"))]
160 if limits.max_storage_buffers_per_shader_stage >= 2 {
161 return Self::for_storage_binding_size(limits.max_storage_buffer_binding_size);
162 }
163 Self::for_uniform_binding_size(limits.max_uniform_buffer_binding_size)
164 }
165
166 fn for_uniform_binding_size(max_uniform_buffer_binding_size: u64) -> Self {
167 let binding = max_uniform_buffer_binding_size as usize;
168 Self {
169 max_shapes_per_batch: (binding / std::mem::size_of::<ShapeData>())
170 .clamp(1, MAX_SHAPES_PER_BATCH),
171 max_gradient_stops: (binding / std::mem::size_of::<GradientStop>())
172 .clamp(1, MAX_GRADIENT_STOPS),
173 storage: false,
174 }
175 }
176
177 #[cfg(not(target_arch = "wasm32"))]
178 fn for_storage_binding_size(max_storage_buffer_binding_size: u64) -> Self {
179 let binding = max_storage_buffer_binding_size as usize;
180 Self {
181 max_shapes_per_batch: (binding / std::mem::size_of::<ShapeData>())
182 .clamp(1, MAX_SHAPES_PER_STORAGE_BATCH),
183 max_gradient_stops: (binding / std::mem::size_of::<GradientStop>())
184 .clamp(1, MAX_GRADIENT_STOPS_PER_STORAGE_BATCH),
185 storage: true,
186 }
187 }
188
189 fn initial_shape_capacity(&self) -> usize {
190 #[cfg(not(target_arch = "wasm32"))]
191 if self.storage {
192 return self
193 .max_shapes_per_batch
194 .min(INITIAL_STORAGE_BATCH_CAPACITY);
195 }
196 self.max_shapes_per_batch
197 }
198
199 fn initial_gradient_capacity(&self) -> usize {
200 #[cfg(not(target_arch = "wasm32"))]
201 if self.storage {
202 return self.max_gradient_stops.min(INITIAL_STORAGE_BATCH_CAPACITY);
203 }
204 self.max_gradient_stops
205 }
206
207 fn data_buffer_usage(&self) -> wgpu::BufferUsages {
208 if self.storage {
209 wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST
210 } else {
211 wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST
212 }
213 }
214
215 fn data_binding_type(&self) -> wgpu::BufferBindingType {
216 if self.storage {
217 wgpu::BufferBindingType::Storage { read_only: true }
218 } else {
219 wgpu::BufferBindingType::Uniform
220 }
221 }
222
223 #[cfg(test)]
224 fn desktop() -> Self {
225 Self::for_uniform_binding_size(wgpu::Limits::default().max_uniform_buffer_binding_size)
226 }
227}
228#[cfg(target_arch = "wasm32")]
229const HARD_MAX_BUFFER_MB: usize = 64; const MAX_SHADOW_SURFACE_CACHE_ITEMS: usize = 512;
231const MAX_SHADOW_SURFACE_CACHE_BYTES: u64 = 192 * 1024 * 1024;
235const MAX_TEXT_IMAGE_CACHE_ITEMS: usize = 1024;
236const MAX_TEXT_GLYPH_MASK_CACHE_ITEMS: usize = 8192;
237const MAX_TEXT_GLYPH_ATLAS_ITEMS: usize = 8192;
238const MAX_TEXT_GLYPH_RUN_CACHE_ITEMS: usize = 1024;
239#[cfg(not(target_arch = "wasm32"))]
240const MAX_TEXT_GLYPH_GPU_RUN_CACHE_ITEMS: usize = 1024;
241#[cfg(not(target_arch = "wasm32"))]
242const MIN_RETAINED_TEXT_GLYPH_QUADS: usize = 192;
243#[cfg(not(target_arch = "wasm32"))]
244const OFFSCREEN_TEXT_GLYPH_PREWARM_BUDGET_MS: f64 = 0.75;
245#[cfg(not(target_arch = "wasm32"))]
246const MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CANDIDATES: usize = 2;
247#[cfg(not(target_arch = "wasm32"))]
248const MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS: usize = 160;
249#[cfg(not(target_arch = "wasm32"))]
250const MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CACHED_GLYPHS: usize = 160;
251const TEXT_GLYPH_ATLAS_MIN_SIZE: u32 = 512;
263const TEXT_GLYPH_ATLAS_MAX_SIZE: u32 = 4096;
264const TEXT_GLYPH_ATLAS_PADDING: u32 = 1;
265const MAX_TEXT_LINE_INDEX_CACHE_ITEMS: usize = 512;
266const MIN_MULTILINE_TEXT_LINES_FOR_CLIPPED_RASTER: usize = 2;
267const MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES: usize = 128;
268const CACHE_MISS_WARMUP_FRAMES: u8 = 1;
269pub(crate) const CLEAR_COLOR: wgpu::Color = wgpu::Color {
270 r: cranpose_render_common::FRAME_CLEAR_COLOR[0] as f64,
271 g: cranpose_render_common::FRAME_CLEAR_COLOR[1] as f64,
272 b: cranpose_render_common::FRAME_CLEAR_COLOR[2] as f64,
273 a: cranpose_render_common::FRAME_CLEAR_COLOR[3] as f64,
274};
275#[cfg(not(target_arch = "wasm32"))]
276const INITIAL_UPLOAD_BUFFER_BYTES: u64 = 4 * 1024;
277#[cfg(not(target_arch = "wasm32"))]
278const INITIAL_RETAINED_GLYPH_UNIFORM_SLOTS: usize = 128;
279const MAX_TEXTURE_CACHE_ITEMS: usize = 256;
280const MAX_IMAGE_TEXTURE_CACHE_BYTES: usize = 256 * 1024 * 1024;
284const RETAINED_STAGED_UPLOAD_BYTES: usize = 256 * 1024;
285const RETAINED_STAGED_UPLOAD_COPIES: usize = 128;
286pub(crate) const RETAINED_LAYER_REQUIREMENTS_CAPACITY: usize = 512;
287const DEFAULT_WGPU_RENDER_STAGE_TELEMETRY_THRESHOLD_MS: f64 = 4.0;
288#[cfg(not(target_arch = "wasm32"))]
289static SEGMENT_DIAG_LINES: AtomicUsize = AtomicUsize::new(0);
290fn wgpu_render_stage_telemetry_threshold_ms() -> Option<f64> {
294 static THRESHOLD_MS: std::sync::OnceLock<Option<f64>> = std::sync::OnceLock::new();
295 *THRESHOLD_MS.get_or_init(|| {
296 let explicit = std::env::var("CRANPOSE_WGPU_RENDER_STAGE_TELEMETRY_MS")
297 .ok()
298 .and_then(|value| value.parse::<f64>().ok())
299 .filter(|value| value.is_finite() && *value >= 0.0);
300 explicit.or_else(|| {
301 std::env::var_os("CRANPOSE_WGPU_RENDER_STAGE_TELEMETRY")
302 .is_some()
303 .then_some(DEFAULT_WGPU_RENDER_STAGE_TELEMETRY_THRESHOLD_MS)
304 })
305 })
306}
307
308pub(crate) fn instant_ms(start: Instant, end: Instant) -> f64 {
309 end.duration_since(start).as_secs_f64() * 1000.0
310}
311
312pub(crate) fn should_log_wgpu_render_stage(start: Instant, end: Instant) -> Option<f64> {
313 let threshold_ms = wgpu_render_stage_telemetry_threshold_ms()?;
314 let total_ms = instant_ms(start, end);
315 (total_ms >= threshold_ms).then_some(total_ms)
316}
317
318fn admit_layer_surface_cache_miss_impl(
319 key: &LayerRasterCacheKey,
320 observed_scene_range_misses: &mut BoundedLruCache<LayerRasterCacheKey, ()>,
321) -> bool {
322 if !key.is_scene_range() {
323 return true;
324 }
325 if observed_scene_range_misses.contains(key) {
326 return true;
327 }
328 observed_scene_range_misses.put(*key, ());
329 false
330}
331
332#[cfg(test)]
333fn first_cache_miss_admission(key: &LayerRasterCacheKey) -> bool {
334 let mut observed_scene_range_misses =
335 BoundedLruCache::with_capacity_at_least_one(MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES);
336 admit_layer_surface_cache_miss_impl(key, &mut observed_scene_range_misses)
337}
338
339#[cfg(test)]
340fn repeated_cache_miss_admission(key: &LayerRasterCacheKey) -> bool {
341 let mut observed_scene_range_misses =
342 BoundedLruCache::with_capacity_at_least_one(MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES);
343 let _ = admit_layer_surface_cache_miss_impl(key, &mut observed_scene_range_misses);
344 admit_layer_surface_cache_miss_impl(key, &mut observed_scene_range_misses)
345}
346
347pub static PRESENTED_FRAMES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
348
349pub fn frames_presented() -> u64 {
350 PRESENTED_FRAMES.load(std::sync::atomic::Ordering::Relaxed)
351}
352
353fn frame_stats_need_warmup_frame(snapshot: &gpu_stats::FrameStatsSnapshot) -> bool {
354 snapshot.layer_cache_misses > 0
355 || snapshot.shadow_shape_cache_misses > 0
356 || snapshot.text_image_cache_misses > 0
357 || snapshot.text_glyph_atlas_misses > 0
358}
359
360fn update_frame_warmup_budget(pending_frames: &mut u8, snapshot: &gpu_stats::FrameStatsSnapshot) {
361 if *pending_frames > 0 {
362 *pending_frames = pending_frames.saturating_sub(1);
363 } else if frame_stats_need_warmup_frame(snapshot) {
364 *pending_frames = CACHE_MISS_WARMUP_FRAMES;
365 }
366}
367
368fn text_atlas_fallback_diag_enabled() -> bool {
369 cranpose_core::env_flag!("CRANPOSE_TEXT_ATLAS_FALLBACK_DIAG")
370}
371
372fn text_glyph_run_diag_enabled() -> bool {
373 cranpose_core::env_flag!("CRANPOSE_TEXT_GLYPH_RUN_DIAG")
374}
375
376fn root_direct_diag_enabled() -> bool {
377 cranpose_core::env_flag!("CRANPOSE_ROOT_DIRECT_DIAG")
378}
379
380fn scene_layer_events_precede_z(scene: &CompositorScene, z_index: usize) -> bool {
381 scene
382 .effect_layers
383 .iter()
384 .any(|layer| layer.z_start < z_index && 0 < layer.z_end)
385 || scene
386 .backdrop_layers
387 .iter()
388 .any(|layer| layer.z_index < z_index)
389}
390
391fn direct_root_child_can_be_replayed_into_later_underlay(child: &ChildLayerComposite) -> bool {
392 child.backdrop.is_none()
393 && !child.has_effect
394 && child.shadow_draws.is_empty()
395 && axis_aligned_quad_rect(child.dest_quad).is_some()
396}
397
398fn rects_overlap(a: Rect, b: Rect) -> bool {
399 let a_right = a.x + a.width;
400 let a_bottom = a.y + a.height;
401 let b_right = b.x + b.width;
402 let b_bottom = b.y + b.height;
403 a.x < b_right && b.x < a_right && a.y < b_bottom && b.y < a_bottom
404}
405
406pub(crate) fn direct_root_child_underlays_are_supported(collected: &CollectedLayer) -> bool {
407 for (child_index, child) in collected.child_layers.iter().enumerate() {
408 if child.backdrop.is_some() {
409 if root_direct_diag_enabled() {
410 log::warn!(
411 "[root-direct-diag] reject self-backdrop child node={:?}",
412 child.node_id
413 );
414 }
415 return false;
416 }
417 if child.needs_nested_underlay {
418 let Some(dest_rect) = axis_aligned_quad_rect(child.dest_quad) else {
419 if root_direct_diag_enabled() {
420 log::warn!(
421 "[root-direct-diag] reject projective underlay child node={:?}",
422 child.node_id
423 );
424 }
425 return false;
426 };
427 let translation_only = (dest_rect.width - child.logical_rect.width).abs() <= 0.001
428 && (dest_rect.height - child.logical_rect.height).abs() <= 0.001;
429 let unsupported_preceding_child_layer = collected.child_layers[..child_index]
430 .iter()
431 .any(|preceding| {
432 if direct_root_child_can_be_replayed_into_later_underlay(preceding) {
433 return false;
434 }
435 axis_aligned_quad_rect(preceding.dest_quad)
436 .is_none_or(|preceding_rect| rects_overlap(preceding_rect, dest_rect))
437 });
438 let preceding_scene_events =
439 scene_layer_events_precede_z(&collected.scene, child.z_index);
440 if unsupported_preceding_child_layer || preceding_scene_events || !translation_only {
441 if root_direct_diag_enabled() {
442 log::warn!(
443 "[root-direct-diag] reject underlay child node={:?} unsupported_preceding_child_layer={} preceding_scene_events={} translation_only={} dest=({:.1},{:.1},{:.1},{:.1}) logical=({:.1},{:.1},{:.1},{:.1})",
444 child.node_id,
445 unsupported_preceding_child_layer,
446 preceding_scene_events,
447 translation_only,
448 dest_rect.x,
449 dest_rect.y,
450 dest_rect.width,
451 dest_rect.height,
452 child.logical_rect.x,
453 child.logical_rect.y,
454 child.logical_rect.width,
455 child.logical_rect.height
456 );
457 }
458 return false;
459 }
460 }
461 }
462 true
463}
464
465#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
466struct ShadowSurfaceCacheKey {
467 content_hash: u64,
468 pixel_size: [u32; 2],
469 root_scale_bits: u32,
470 blur_radius_bits: u32,
471}
472
473struct CachedShadowSurface {
474 target: Rc<OffscreenTarget>,
475 byte_size: u64,
476}
477
478struct CachedShadowComposite {
479 source: Rc<OffscreenTarget>,
480 scissor: Option<(u32, u32, u32, u32)>,
481 rounded_mask: Option<RoundedCompositeMask>,
482 dest_viewport: Option<(f32, f32, f32, f32)>,
483}
484
485impl CachedShadowComposite {
486 fn batch_item(&self) -> CompositeBatchItem<'_> {
487 CompositeBatchItem {
488 source: &self.source,
489 alpha: 1.0,
490 scissor: self.scissor,
491 rounded_mask: self.rounded_mask,
492 blend_mode: BlendMode::SrcOver,
493 dest_viewport: self.dest_viewport,
494 source_viewport: None,
495 sample_mode: CompositeSampleMode::Nearest,
496 }
497 }
498}
499
500#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
501struct TextImageCacheKey(u64);
502
503struct CachedTextImage {
504 image: ImageBitmap,
505}
506
507#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
508struct TextGlyphRunCacheKey(u64);
509
510#[derive(Clone, Copy)]
511struct CachedTextGlyphQuad {
512 x: i32,
513 y: i32,
514 width: usize,
515 height: usize,
516 color: (f32, f32, f32, f32),
517 uv: ImageUvRect,
518}
519
520struct CachedTextGlyphRun {
521 glyphs: Rc<[SoftwareGlyphAtlasPlacement]>,
522 quads: Option<Rc<[CachedTextGlyphQuad]>>,
523 atlas_generation: u64,
524}
525
526const TEXT_GLYPH_PREWARM_VIEWPORT_MULTIPLIER: f32 = 2.0;
527
528#[cfg(not(target_arch = "wasm32"))]
529struct CachedGpuTextGlyphRun {
530 vertex_buffer: wgpu::Buffer,
531 index_buffer: wgpu::Buffer,
532 index_count: u32,
533 atlas_generation: u64,
534}
535
536#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
537struct TextLineIndexCacheKey(usize);
538
539struct CachedTextLineIndex {
540 text: std::sync::Weak<cranpose_ui::text::RenderString>,
541 len: usize,
542 starts: Rc<[usize]>,
543}
544
545struct TextLineIndexCache {
546 entries: BoundedLruCache<TextLineIndexCacheKey, CachedTextLineIndex>,
547}
548
549impl TextLineIndexCache {
550 fn new(capacity: usize) -> Self {
551 Self {
552 entries: BoundedLruCache::with_capacity_at_least_one(capacity),
553 }
554 }
555
556 fn line_starts(&mut self, text: &Arc<cranpose_ui::text::RenderString>) -> Rc<[usize]> {
557 let key = TextLineIndexCacheKey(Arc::as_ptr(text) as usize);
558 if let Some(cached) = self.entries.get(&key) {
559 if cached.len == text.text.len()
560 && cached
561 .text
562 .upgrade()
563 .is_some_and(|cached_text| Arc::ptr_eq(&cached_text, text))
564 {
565 return cached.starts.clone();
566 }
567 }
568
569 let starts = Rc::<[usize]>::from(line_start_offsets(text.text.as_str()));
570 self.entries.put(
571 key,
572 CachedTextLineIndex {
573 text: Arc::downgrade(text),
574 len: text.text.len(),
575 starts: starts.clone(),
576 },
577 );
578 starts
579 }
580}
581
582#[derive(Clone, Copy, Debug, PartialEq)]
583struct ShapeShadowSurfacePlan {
584 source_device_bounds: DevicePixelBounds,
585 processing_scissor: Option<(u32, u32, u32, u32)>,
586 pixel_radius: f32,
587}
588
589#[derive(Default)]
590struct RendererWarningState {
591 unsupported_effect_reported: Cell<bool>,
592}
593
594impl RendererWarningState {
595 fn warn_unsupported_effect_once(&self) {
596 if !self.unsupported_effect_reported.replace(true) {
597 log::warn!(
598 "WGPU renderer received an unsupported RenderEffect variant; falling back to passthrough compositing"
599 );
600 }
601 }
602}
603
604fn is_blend_mode_supported(mode: BlendMode) -> bool {
605 matches!(mode, BlendMode::SrcOver | BlendMode::DstOut)
606}
607
608fn blend_state_for_mode(mode: BlendMode) -> wgpu::BlendState {
609 match mode {
610 BlendMode::DstOut => wgpu::BlendState {
611 color: wgpu::BlendComponent {
612 src_factor: wgpu::BlendFactor::Zero,
613 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
614 operation: wgpu::BlendOperation::Add,
615 },
616 alpha: wgpu::BlendComponent {
617 src_factor: wgpu::BlendFactor::Zero,
618 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
619 operation: wgpu::BlendOperation::Add,
620 },
621 },
622 _ => wgpu::BlendState::ALPHA_BLENDING,
623 }
624}
625
626fn supported_blend_mode(mode: BlendMode) -> BlendMode {
627 if is_blend_mode_supported(mode) {
628 return mode;
629 }
630
631 BlendMode::SrcOver
632}
633
634fn direct_shader_composite_viewport(
635 alpha: f32,
636 blend_mode: BlendMode,
637 dest_viewport: Option<(f32, f32, f32, f32)>,
638 sample_mode: CompositeSampleMode,
639 source_size: (u32, u32),
640) -> Option<(f32, f32, f32, f32)> {
641 if alpha != 1.0 || supported_blend_mode(blend_mode) != BlendMode::SrcOver {
642 return None;
643 }
644 let viewport = dest_viewport?;
645 if viewport.2 <= 0.0 || viewport.3 <= 0.0 {
646 return None;
647 }
648 match sample_mode {
649 CompositeSampleMode::Linear | CompositeSampleMode::Nearest => Some(viewport),
650 CompositeSampleMode::Box4
651 if shader_composite_preserves_source_pixel_grid(viewport, source_size) =>
652 {
653 Some(viewport)
654 }
655 CompositeSampleMode::Box4 => None,
656 }
657}
658
659fn shader_composite_preserves_source_pixel_grid(
660 viewport: (f32, f32, f32, f32),
661 source_size: (u32, u32),
662) -> bool {
663 const EPSILON: f32 = 0.01;
664 let (x, y, width, height) = viewport;
665 let (source_width, source_height) = source_size;
666 (x - x.round()).abs() <= EPSILON
667 && (y - y.round()).abs() <= EPSILON
668 && (width - source_width as f32).abs() <= EPSILON
669 && (height - source_height as f32).abs() <= EPSILON
670}
671
672type DirectShaderTailComposite<'a> = (&'a RenderEffect, &'a RuntimeShader, (f32, f32, f32, f32));
673
674fn direct_shader_tail_composite(
675 effect: &RenderEffect,
676 alpha: f32,
677 blend_mode: BlendMode,
678 dest_viewport: Option<(f32, f32, f32, f32)>,
679 sample_mode: CompositeSampleMode,
680 source_size: (u32, u32),
681) -> Option<DirectShaderTailComposite<'_>> {
682 let viewport = direct_shader_composite_viewport(
683 alpha,
684 blend_mode,
685 dest_viewport,
686 sample_mode,
687 source_size,
688 )?;
689 let RenderEffect::Chain { first, second } = effect else {
690 return None;
691 };
692 let RenderEffect::Shader { shader } = second.as_ref() else {
693 return None;
694 };
695 Some((first.as_ref(), shader, viewport))
696}
697
698fn hash_f32_for_cache<H: Hasher>(value: f32, state: &mut H) {
699 value.to_bits().hash(state);
700}
701
702fn hash_text_raster_geometry_for_cache<H: Hasher>(
703 rect: Rect,
704 static_text_motion: bool,
705 state: &mut H,
706) {
707 hash_f32_for_cache(rect.width, state);
708 hash_f32_for_cache(rect.height, state);
709 static_text_motion.hash(state);
710 if !static_text_motion {
711 hash_f32_for_cache(rect.x.fract(), state);
712 hash_f32_for_cache(rect.y.fract(), state);
713 }
714}
715
716fn text_raster_geometry_for_draw(
717 text_draw: &TextDraw,
718 root_scale: f32,
719) -> Option<(Rect, Rect, Option<Rect>, f32, bool)> {
720 if text_draw.text.is_empty()
721 || text_draw.rect.width <= 0.0
722 || text_draw.rect.height <= 0.0
723 || !root_scale.is_finite()
724 || root_scale <= 0.0
725 {
726 return None;
727 }
728
729 let text_scale = text_draw.scale * root_scale;
730 if !text_scale.is_finite() || text_scale <= 0.0 {
731 return None;
732 }
733
734 let static_text_motion = text_draw
735 .text_style
736 .paragraph_style
737 .text_motion
738 .unwrap_or(cranpose_ui::text::TextMotion::Static)
739 == cranpose_ui::text::TextMotion::Static;
740 let snap_delta = text_draw
741 .snap_anchor
742 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
743 .unwrap_or_default();
744 let logical_rect = text_draw.rect.translate(snap_delta.x, snap_delta.y);
745 let clip = text_draw.clip;
748 let mut raster_rect = Rect {
749 x: logical_rect.x * root_scale,
750 y: logical_rect.y * root_scale,
751 width: logical_rect.width * root_scale,
752 height: logical_rect.height * root_scale,
753 };
754 if text_draw.snap_anchor.is_some() {
755 raster_rect.x = canonicalize_device_coordinate(raster_rect.x);
756 raster_rect.y = canonicalize_device_coordinate(raster_rect.y);
757 }
758 if static_text_motion {
759 raster_rect.x = raster_rect.x.round();
760 raster_rect.y = raster_rect.y.round();
761 }
762 raster_rect.width = raster_rect.width.ceil().max(1.0);
763 raster_rect.height = raster_rect.height.ceil().max(1.0);
764 Some((
765 logical_rect,
766 raster_rect,
767 clip,
768 text_scale,
769 static_text_motion,
770 ))
771}
772
773fn text_draw_is_visible_in_viewport(
774 logical_rect: Rect,
775 clip: Option<Rect>,
776 viewport: ViewportUniformParams,
777 root_scale: f32,
778) -> bool {
779 draw_rect_is_visible_in_viewport(logical_rect, clip, viewport, root_scale)
780}
781
782fn text_draw_should_prewarm_in_viewport(
783 logical_rect: Rect,
784 clip: Option<Rect>,
785 viewport: ViewportUniformParams,
786 root_scale: f32,
787) -> bool {
788 if !root_scale.is_finite() || root_scale <= 0.0 {
789 return false;
790 }
791 let viewport_rect = Rect {
792 x: viewport.offset[0] / root_scale,
793 y: viewport.offset[1] / root_scale,
794 width: viewport.width as f32 / root_scale,
795 height: viewport.height as f32 / root_scale,
796 };
797 let margin_x = viewport_rect.width * TEXT_GLYPH_PREWARM_VIEWPORT_MULTIPLIER;
798 let margin_y = viewport_rect.height * TEXT_GLYPH_PREWARM_VIEWPORT_MULTIPLIER;
799 let prewarm_viewport = expand_rect(viewport_rect, margin_x, margin_y);
800 let prewarm_rect = match clip {
801 Some(clip) => expand_rect(clip, margin_x, margin_y).intersect(prewarm_viewport),
802 None => Some(prewarm_viewport),
803 };
804 prewarm_rect.is_some_and(|rect| logical_rect.intersect(rect).is_some())
805}
806
807fn expand_rect(rect: Rect, margin_x: f32, margin_y: f32) -> Rect {
808 Rect {
809 x: rect.x - margin_x,
810 y: rect.y - margin_y,
811 width: rect.width + margin_x * 2.0,
812 height: rect.height + margin_y * 2.0,
813 }
814}
815
816fn draw_rect_is_visible_in_viewport(
817 rect: Rect,
818 clip: Option<Rect>,
819 viewport: ViewportUniformParams,
820 root_scale: f32,
821) -> bool {
822 if !root_scale.is_finite() || root_scale <= 0.0 {
823 return false;
824 }
825 let viewport_rect = Rect {
826 x: viewport.offset[0] / root_scale,
827 y: viewport.offset[1] / root_scale,
828 width: viewport.width as f32 / root_scale,
829 height: viewport.height as f32 / root_scale,
830 };
831 let visible_rect = match clip {
832 Some(clip) => clip.intersect(viewport_rect),
833 None => Some(viewport_rect),
834 };
835 visible_rect.is_some_and(|visible| rect.intersect(visible).is_some())
836}
837
838fn shape_draw_is_visible_in_viewport(
839 shape: &DrawShape,
840 viewport: ViewportUniformParams,
841 root_scale: f32,
842) -> bool {
843 let snap_delta = shape
844 .snap_anchor
845 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
846 .unwrap_or_default();
847 let rect = quad_bounds(translate_quad(shape.quad, snap_delta));
848 let clip = shape.clip;
849 draw_rect_is_visible_in_viewport(rect, clip, viewport, root_scale)
850}
851
852fn cached_text_glyph_quad(
853 glyph: &SoftwareGlyphAtlasPlacement,
854 entry: GlyphAtlasEntry,
855 atlas_size: u32,
856) -> CachedTextGlyphQuad {
857 CachedTextGlyphQuad {
858 x: glyph.x,
859 y: glyph.y,
860 width: glyph.width,
861 height: glyph.height,
862 color: (
863 glyph.color.0.clamp(0.0, 1.0),
864 glyph.color.1.clamp(0.0, 1.0),
865 glyph.color.2.clamp(0.0, 1.0),
866 glyph.color.3.clamp(0.0, 1.0),
867 ),
868 uv: glyph_atlas_uv_rect(entry, atlas_size),
869 }
870}
871
872fn append_cached_text_glyph_quad(
873 source_raster_rect: Rect,
874 quad: &CachedTextGlyphQuad,
875 image_vertices: &mut Vec<Vertex>,
876 image_indices: &mut Vec<u32>,
877) -> bool {
878 if quad.width == 0 || quad.height == 0 || quad.color.3 <= 0.0 {
879 return false;
880 }
881
882 let base_vertex = image_vertices.len() as u32;
883 image_indices.extend_from_slice(&[
884 base_vertex,
885 base_vertex + 1,
886 base_vertex + 2,
887 base_vertex + 2,
888 base_vertex + 1,
889 base_vertex + 3,
890 ]);
891
892 let x0 = source_raster_rect.x + quad.x as f32;
893 let y0 = source_raster_rect.y + quad.y as f32;
894 let x1 = x0 + quad.width as f32;
895 let y1 = y0 + quad.height as f32;
896 let color = [quad.color.0, quad.color.1, quad.color.2, quad.color.3];
897
898 image_vertices.extend_from_slice(&[
899 Vertex {
900 position: [x0, y0],
901 color,
902 uv: [quad.uv.min[0], quad.uv.min[1]],
903 uv_bounds: quad.uv.sample_bounds,
904 },
905 Vertex {
906 position: [x1, y0],
907 color,
908 uv: [quad.uv.max[0], quad.uv.min[1]],
909 uv_bounds: quad.uv.sample_bounds,
910 },
911 Vertex {
912 position: [x0, y1],
913 color,
914 uv: [quad.uv.min[0], quad.uv.max[1]],
915 uv_bounds: quad.uv.sample_bounds,
916 },
917 Vertex {
918 position: [x1, y1],
919 color,
920 uv: [quad.uv.max[0], quad.uv.max[1]],
921 uv_bounds: quad.uv.sample_bounds,
922 },
923 ]);
924 true
925}
926
927fn cached_text_glyph_quad_logical_rect(
928 source_raster_rect: Rect,
929 quad: &CachedTextGlyphQuad,
930 root_scale: f32,
931) -> Option<Rect> {
932 if !root_scale.is_finite() || root_scale <= 0.0 {
933 return None;
934 }
935 Some(Rect {
936 x: (source_raster_rect.x + quad.x as f32) / root_scale,
937 y: (source_raster_rect.y + quad.y as f32) / root_scale,
938 width: quad.width as f32 / root_scale,
939 height: quad.height as f32 / root_scale,
940 })
941}
942
943fn cached_text_glyph_quad_is_visible_in_viewport(
944 source_raster_rect: Rect,
945 quad: &CachedTextGlyphQuad,
946 clip: Option<Rect>,
947 viewport: ViewportUniformParams,
948 root_scale: f32,
949) -> bool {
950 cached_text_glyph_quad_logical_rect(source_raster_rect, quad, root_scale)
951 .is_some_and(|rect| draw_rect_is_visible_in_viewport(rect, clip, viewport, root_scale))
952}
953
954#[derive(Clone, Copy, Debug, Eq, PartialEq)]
955enum TextGlyphDrawAction {
956 DrawVisible,
957 PrewarmOffscreen,
958 Skip,
959}
960
961fn text_glyph_draw_action(
962 is_visible: bool,
963 is_prewarm_candidate: bool,
964 allow_offscreen_prewarm: bool,
965) -> TextGlyphDrawAction {
966 if is_visible {
967 TextGlyphDrawAction::DrawVisible
968 } else if allow_offscreen_prewarm && is_prewarm_candidate {
969 TextGlyphDrawAction::PrewarmOffscreen
970 } else {
971 TextGlyphDrawAction::Skip
972 }
973}
974
975#[cfg(not(target_arch = "wasm32"))]
976fn should_use_retained_text_glyph_run(quads_len: usize, clip: Option<Rect>) -> bool {
977 clip.is_none() && quads_len >= MIN_RETAINED_TEXT_GLYPH_QUADS
978}
979
980#[cfg(not(target_arch = "wasm32"))]
981fn offscreen_text_glyph_prewarm_work_is_bounded(
982 cached_glyphs: Option<usize>,
983 text_len: usize,
984) -> bool {
985 match cached_glyphs {
986 Some(glyphs) => glyphs <= MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CACHED_GLYPHS,
987 None => text_len <= MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS,
988 }
989}
990
991#[cfg(not(target_arch = "wasm32"))]
992fn offscreen_text_glyph_prewarm_budget_exhausted(
993 start: Instant,
994 admitted_candidates: usize,
995) -> bool {
996 admitted_candidates >= MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CANDIDATES
997 || instant_ms(start, Instant::now()) >= OFFSCREEN_TEXT_GLYPH_PREWARM_BUDGET_MS
998}
999
1000fn text_draws_for_ordered_range<'a>(
1001 ordered_items: &'a [(usize, SegmentDrawItem)],
1002 texts: &'a [TextDraw],
1003 start: usize,
1004 end: usize,
1005) -> Result<impl Iterator<Item = &'a TextDraw>, String> {
1006 let range_items = ordered_items
1007 .get(start..end)
1008 .ok_or_else(|| format!("text batch range {start}..{end} is outside ordered draw items"))?;
1009 for (_, item) in range_items {
1010 match item {
1011 SegmentDrawItem::Text(text_index) if *text_index < texts.len() => {}
1012 SegmentDrawItem::Text(text_index) => {
1013 return Err(format!(
1014 "text batch references missing text draw index: {text_index}"
1015 ));
1016 }
1017 _ => return Err(format!("text batch contains non-text draw item: {item:?}")),
1018 }
1019 }
1020
1021 Ok(range_items.iter().filter_map(move |(_, item)| match item {
1022 SegmentDrawItem::Text(text_index) => texts.get(*text_index),
1023 _ => None,
1024 }))
1025}
1026
1027const SHADOW_CACHE_DEVICE_QUANT: f32 = 16.0;
1032
1033fn hash_shadow_device_offset<H: Hasher>(value: f32, origin: f32, root_scale: f32, state: &mut H) {
1034 let quantized = ((value - origin) * root_scale * SHADOW_CACHE_DEVICE_QUANT).round();
1035 (quantized as i64).hash(state);
1036}
1037
1038fn hash_shadow_device_rect<H: Hasher>(
1039 rect: Rect,
1040 origin_x: f32,
1041 origin_y: f32,
1042 root_scale: f32,
1043 state: &mut H,
1044) {
1045 hash_shadow_device_offset(rect.x, origin_x, root_scale, state);
1046 hash_shadow_device_offset(rect.y, origin_y, root_scale, state);
1047 hash_shadow_device_offset(rect.width, 0.0, root_scale, state);
1048 hash_shadow_device_offset(rect.height, 0.0, root_scale, state);
1049}
1050
1051fn hash_shape_shadow_item<H: Hasher>(
1052 shape: &DrawShape,
1053 brushes: &[Brush],
1054 blend_mode: BlendMode,
1055 origin_x: f32,
1056 origin_y: f32,
1057 root_scale: f32,
1058 state: &mut H,
1059) {
1060 hash_shadow_device_rect(shape.rect, origin_x, origin_y, root_scale, state);
1061 hash_shadow_device_rect(shape.local_rect, origin_x, origin_y, root_scale, state);
1062 for point in shape.quad {
1063 hash_shadow_device_offset(point[0], origin_x, root_scale, state);
1064 hash_shadow_device_offset(point[1], origin_y, root_scale, state);
1065 }
1066 match shape.snap_anchor {
1067 Some(anchor) => {
1068 1u8.hash(state);
1069 hash_shadow_device_offset(anchor.origin.x, origin_x, root_scale, state);
1070 hash_shadow_device_offset(anchor.origin.y, origin_y, root_scale, state);
1071 hash_f32_for_cache(anchor.device_pixel_step, state);
1072 }
1073 None => 0u8.hash(state),
1074 }
1075 shape.brush.render_hash(brushes).hash(state);
1076 match shape.shape {
1077 Some(corner_shape) => {
1078 1u8.hash(state);
1079 corner_shape.radii().render_hash().hash(state);
1080 }
1081 None => 0u8.hash(state),
1082 }
1083 match shape.clip {
1084 Some(clip) => {
1085 1u8.hash(state);
1086 hash_shadow_device_rect(clip, origin_x, origin_y, root_scale, state);
1087 }
1088 None => 0u8.hash(state),
1089 }
1090 blend_mode.hash(state);
1091 shape.blend_mode.hash(state);
1092}
1093
1094fn shape_shadow_content_hash(
1095 shapes: &[(DrawShape, BlendMode)],
1096 brushes: &[Brush],
1097 root_scale: f32,
1098) -> u64 {
1099 let mut hasher = FxHasher::default();
1100 let origin = shape_shadow_bounds(shapes).unwrap_or(Rect {
1105 x: 0.0,
1106 y: 0.0,
1107 width: 0.0,
1108 height: 0.0,
1109 });
1110
1111 shapes.len().hash(&mut hasher);
1112 for (shape, blend_mode) in shapes {
1113 hash_shape_shadow_item(
1114 shape,
1115 brushes,
1116 *blend_mode,
1117 origin.x,
1118 origin.y,
1119 root_scale,
1120 &mut hasher,
1121 );
1122 }
1123 hasher.finish()
1124}
1125
1126fn shape_shadow_surface_cache_key(
1127 shapes: &[(DrawShape, BlendMode)],
1128 brushes: &[Brush],
1129 device_bounds: DevicePixelBounds,
1130 pixel_radius: f32,
1131 root_scale: f32,
1132) -> Option<ShadowSurfaceCacheKey> {
1133 (root_scale.is_finite() && root_scale > 0.0).then(|| ShadowSurfaceCacheKey {
1134 content_hash: shape_shadow_content_hash(shapes, brushes, root_scale),
1135 pixel_size: [device_bounds.width, device_bounds.height],
1136 root_scale_bits: root_scale.to_bits(),
1137 blur_radius_bits: pixel_radius.to_bits(),
1138 })
1139}
1140
1141fn shape_shadow_bounds(shapes: &[(DrawShape, BlendMode)]) -> Option<Rect> {
1142 shapes
1143 .iter()
1144 .map(|(shape, _)| shape.rect)
1145 .reduce(|a, b| Rect {
1146 x: a.x.min(b.x),
1147 y: a.y.min(b.y),
1148 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
1149 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
1150 })
1151}
1152
1153fn shared_shape_shadow_snap_anchor(shapes: &[(DrawShape, BlendMode)]) -> Option<SnapAnchor> {
1154 let anchor = shapes.first()?.0.snap_anchor?;
1155 shapes
1156 .iter()
1157 .all(|(shape, _)| shape.snap_anchor == Some(anchor))
1158 .then_some(anchor)
1159}
1160
1161fn shadow_draw_bounds(shadow: &ShadowDraw) -> Option<Rect> {
1162 shadow
1163 .shapes
1164 .iter()
1165 .map(|(shape, _)| shape.rect)
1166 .chain(shadow.texts.iter().map(|text| text.rect))
1167 .reduce(|a, b| Rect {
1168 x: a.x.min(b.x),
1169 y: a.y.min(b.y),
1170 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
1171 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
1172 })
1173}
1174
1175fn shadow_draw_may_render(
1176 shadow: &ShadowDraw,
1177 width: u32,
1178 height: u32,
1179 root_scale: f32,
1180 max_texture_dim: u32,
1181) -> bool {
1182 if shadow.texts.is_empty() && !shadow.shapes.is_empty() && shadow.blur_radius > 0.0 {
1183 return shape_shadow_surface_plan(
1184 &shadow.shapes,
1185 shadow.clip,
1186 shadow.blur_radius,
1187 width,
1188 height,
1189 root_scale,
1190 max_texture_dim,
1191 )
1192 .is_some();
1193 }
1194
1195 let Some(bounds) = shadow_draw_bounds(shadow) else {
1196 return false;
1197 };
1198 let blur_margin = blur_extent_margin(shadow.blur_radius);
1199 let mut visible_bounds = Rect {
1200 x: bounds.x - blur_margin,
1201 y: bounds.y - blur_margin,
1202 width: bounds.width + blur_margin * 2.0,
1203 height: bounds.height + blur_margin * 2.0,
1204 };
1205 if let Some(clip) = shadow.clip {
1206 let clip_expanded = Rect {
1207 x: clip.x - blur_margin,
1208 y: clip.y - blur_margin,
1209 width: clip.width + blur_margin * 2.0,
1210 height: clip.height + blur_margin * 2.0,
1211 };
1212 let Some(intersection) = visible_bounds.intersect(clip_expanded) else {
1213 return false;
1214 };
1215 visible_bounds = intersection;
1216 }
1217
1218 scissor_rect_for_rect(visible_bounds, root_scale, width, height).is_some()
1219}
1220
1221fn shape_shadow_surface_plan(
1222 shapes: &[(DrawShape, BlendMode)],
1223 clip: Option<Rect>,
1224 blur_radius: f32,
1225 width: u32,
1226 height: u32,
1227 root_scale: f32,
1228 max_texture_dim: u32,
1229) -> Option<ShapeShadowSurfacePlan> {
1230 let shape_bounds = shape_shadow_bounds(shapes)?;
1231 let blur_margin = blur_extent_margin(blur_radius);
1232 let source_blur_bounds = Rect {
1233 x: shape_bounds.x - blur_margin,
1234 y: shape_bounds.y - blur_margin,
1235 width: shape_bounds.width + blur_margin * 2.0,
1236 height: shape_bounds.height + blur_margin * 2.0,
1237 };
1238
1239 let mut visible_blur_bounds = source_blur_bounds;
1240 if let Some(clip) = clip {
1241 let clip_expanded = Rect {
1242 x: clip.x - blur_margin,
1243 y: clip.y - blur_margin,
1244 width: clip.width + blur_margin * 2.0,
1245 height: clip.height + blur_margin * 2.0,
1246 };
1247 visible_blur_bounds = visible_blur_bounds.intersect(clip_expanded)?;
1248 }
1249
1250 let processing_scissor = scissor_rect_for_rect(visible_blur_bounds, root_scale, width, height);
1251 processing_scissor?;
1252 let visible_device_bounds =
1253 device_pixel_bounds_for_rect(visible_blur_bounds, width, height, root_scale)?;
1254 let source_device_bounds = translation_stable_anchored_device_pixel_bounds(
1255 source_blur_bounds,
1256 shared_shape_shadow_snap_anchor(shapes),
1257 root_scale,
1258 max_texture_dim,
1259 )
1260 .unwrap_or(visible_device_bounds);
1261
1262 Some(ShapeShadowSurfacePlan {
1263 source_device_bounds,
1264 processing_scissor,
1265 pixel_radius: blur_radius * root_scale,
1266 })
1267}
1268
1269fn is_render_effect_supported(effect: &RenderEffect) -> bool {
1270 match effect {
1271 RenderEffect::Blur { .. } => true,
1272 RenderEffect::Offset { .. } => true,
1273 RenderEffect::Shader { .. } => true,
1274 RenderEffect::Chain { first, second } => {
1275 is_render_effect_supported(first) && is_render_effect_supported(second)
1276 }
1277 }
1278}
1279
1280fn resolve_gradient_point(origin: f32, extent: f32, value: f32) -> f32 {
1281 if value.is_finite() {
1282 origin + value
1283 } else if value.is_sign_positive() {
1284 origin + extent
1285 } else {
1286 origin
1287 }
1288}
1289
1290fn gradient_tile_mode_value(tile_mode: TileMode) -> u32 {
1291 match tile_mode {
1292 TileMode::Clamp => 0,
1293 TileMode::Repeated => 1,
1294 TileMode::Mirror => 2,
1295 TileMode::Decal => 3,
1296 }
1297}
1298
1299#[cfg(not(target_arch = "wasm32"))]
1300fn shape_shader_source(batch_limits: ShapeBatchLimits) -> Cow<'static, str> {
1301 if batch_limits.storage {
1305 return Cow::Owned(
1306 shaders::SHADER
1307 .replace(
1308 "var<uniform> shape_data: array<ShapeData, 102>;",
1309 "var<storage, read> shape_data: array<ShapeData>;",
1310 )
1311 .replace(
1312 "var<uniform> gradient_stops: array<GradientStop, 256>;",
1313 "var<storage, read> gradient_stops: array<GradientStop>;\n\n\
1320 @group(1) @binding(3)\n\
1321 var<storage, read> paint: array<vec4<f32>>;",
1322 )
1323 .replace(
1324 "output.color = shape.color;",
1325 "output.color = \
1326 select(shape.color, paint[shape_idx], similarity.paint_select > 0.5);",
1327 ),
1328 );
1329 }
1330 Cow::Owned(
1331 shaders::SHADER
1332 .replace(
1333 "array<ShapeData, 102>",
1334 &format!("array<ShapeData, {}>", batch_limits.max_shapes_per_batch),
1335 )
1336 .replace(
1337 "array<GradientStop, 256>",
1338 &format!("array<GradientStop, {}>", batch_limits.max_gradient_stops),
1339 ),
1340 )
1341}
1342
1343#[cfg(target_arch = "wasm32")]
1344fn shape_shader_source(_batch_limits: ShapeBatchLimits) -> Cow<'static, str> {
1345 Cow::Borrowed(shaders::SHADER)
1346}
1347
1348fn create_shape_pipeline(
1349 device: &wgpu::Device,
1350 surface_format: wgpu::TextureFormat,
1351 uniform_layout: &wgpu::BindGroupLayout,
1352 shape_layout: &wgpu::BindGroupLayout,
1353 blend_mode: BlendMode,
1354 batch_limits: ShapeBatchLimits,
1355 fragment_entry: &'static str,
1356) -> wgpu::RenderPipeline {
1357 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1358 label: Some("Shape Shader"),
1359 source: wgpu::ShaderSource::Wgsl(shape_shader_source(batch_limits)),
1360 });
1361
1362 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1363 label: Some("Render Pipeline Layout"),
1364 bind_group_layouts: &[Some(uniform_layout), Some(shape_layout)],
1365 immediate_size: 0,
1366 });
1367
1368 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1369 label: Some("Render Pipeline"),
1370 layout: Some(&pipeline_layout),
1371 vertex: wgpu::VertexState {
1372 module: &shader,
1373 entry_point: Some("vs_main"),
1374 compilation_options: wgpu::PipelineCompilationOptions::default(),
1375 buffers: &[],
1378 },
1379 fragment: Some(wgpu::FragmentState {
1380 module: &shader,
1381 entry_point: Some(fragment_entry),
1382 compilation_options: wgpu::PipelineCompilationOptions::default(),
1383 targets: &[Some(wgpu::ColorTargetState {
1384 format: surface_format,
1385 blend: Some(blend_state_for_mode(blend_mode)),
1386 write_mask: wgpu::ColorWrites::ALL,
1387 })],
1388 }),
1389 primitive: wgpu::PrimitiveState {
1390 topology: wgpu::PrimitiveTopology::TriangleList,
1391 strip_index_format: None,
1392 front_face: wgpu::FrontFace::Ccw,
1393 cull_mode: None,
1394 unclipped_depth: false,
1395 polygon_mode: wgpu::PolygonMode::Fill,
1396 conservative: false,
1397 },
1398 depth_stencil: None,
1399 multisample: wgpu::MultisampleState::default(),
1400 multiview_mask: None,
1401 cache: None,
1402 })
1403}
1404
1405#[cfg(not(target_arch = "wasm32"))]
1412fn create_mesh_shape_pipeline(
1413 device: &wgpu::Device,
1414 surface_format: wgpu::TextureFormat,
1415 uniform_layout: &wgpu::BindGroupLayout,
1416 shape_layout: &wgpu::BindGroupLayout,
1417 batch_limits: ShapeBatchLimits,
1418) -> wgpu::RenderPipeline {
1419 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1420 label: Some("Shape Mesh Shader"),
1421 source: wgpu::ShaderSource::Wgsl(shape_shader_source(batch_limits)),
1422 });
1423
1424 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1425 label: Some("Mesh Render Pipeline Layout"),
1426 bind_group_layouts: &[Some(uniform_layout), Some(shape_layout)],
1427 immediate_size: 0,
1428 });
1429
1430 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1431 label: Some("Retained Mesh Pipeline"),
1432 layout: Some(&pipeline_layout),
1433 vertex: wgpu::VertexState {
1434 module: &shader,
1435 entry_point: Some("vs_mesh"),
1436 compilation_options: wgpu::PipelineCompilationOptions::default(),
1437 buffers: &[MeshVertex::desc()],
1438 },
1439 fragment: Some(wgpu::FragmentState {
1440 module: &shader,
1441 entry_point: Some("fs_main"),
1442 compilation_options: wgpu::PipelineCompilationOptions::default(),
1443 targets: &[Some(wgpu::ColorTargetState {
1444 format: surface_format,
1445 blend: Some(blend_state_for_mode(BlendMode::SrcOver)),
1446 write_mask: wgpu::ColorWrites::ALL,
1447 })],
1448 }),
1449 primitive: wgpu::PrimitiveState {
1450 topology: wgpu::PrimitiveTopology::TriangleList,
1451 strip_index_format: None,
1452 front_face: wgpu::FrontFace::Ccw,
1453 cull_mode: None,
1454 unclipped_depth: false,
1455 polygon_mode: wgpu::PolygonMode::Fill,
1456 conservative: false,
1457 },
1458 depth_stencil: None,
1459 multisample: wgpu::MultisampleState::default(),
1460 multiview_mask: None,
1461 cache: None,
1462 })
1463}
1464
1465#[cfg(not(target_arch = "wasm32"))]
1473fn create_instanced_shape_pipeline(
1474 device: &wgpu::Device,
1475 surface_format: wgpu::TextureFormat,
1476 uniform_layout: &wgpu::BindGroupLayout,
1477 shape_layout: &wgpu::BindGroupLayout,
1478 blend_mode: BlendMode,
1479 batch_limits: ShapeBatchLimits,
1480 fragment_entry: &'static str,
1481) -> wgpu::RenderPipeline {
1482 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1483 label: Some("Shape Instanced Shader"),
1484 source: wgpu::ShaderSource::Wgsl(shape_shader_source(batch_limits)),
1485 });
1486
1487 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1488 label: Some("Instanced Render Pipeline Layout"),
1489 bind_group_layouts: &[Some(uniform_layout), Some(shape_layout)],
1490 immediate_size: 0,
1491 });
1492
1493 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1494 label: Some("Instanced Render Pipeline"),
1495 layout: Some(&pipeline_layout),
1496 vertex: wgpu::VertexState {
1497 module: &shader,
1498 entry_point: Some("vs_shape_instanced"),
1499 compilation_options: wgpu::PipelineCompilationOptions::default(),
1500 buffers: &[],
1504 },
1505 fragment: Some(wgpu::FragmentState {
1506 module: &shader,
1507 entry_point: Some(fragment_entry),
1508 compilation_options: wgpu::PipelineCompilationOptions::default(),
1509 targets: &[Some(wgpu::ColorTargetState {
1510 format: surface_format,
1511 blend: Some(blend_state_for_mode(blend_mode)),
1512 write_mask: wgpu::ColorWrites::ALL,
1513 })],
1514 }),
1515 primitive: wgpu::PrimitiveState {
1516 topology: wgpu::PrimitiveTopology::TriangleList,
1517 strip_index_format: None,
1518 front_face: wgpu::FrontFace::Ccw,
1519 cull_mode: None,
1520 unclipped_depth: false,
1521 polygon_mode: wgpu::PolygonMode::Fill,
1522 conservative: false,
1523 },
1524 depth_stencil: None,
1525 multisample: wgpu::MultisampleState::default(),
1526 multiview_mask: None,
1527 cache: None,
1528 })
1529}
1530
1531fn create_image_pipeline(
1532 device: &wgpu::Device,
1533 surface_format: wgpu::TextureFormat,
1534 uniform_layout: &wgpu::BindGroupLayout,
1535 image_layout: &wgpu::BindGroupLayout,
1536 blend_mode: BlendMode,
1537) -> wgpu::RenderPipeline {
1538 let image_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1539 label: Some("Image Shader"),
1540 source: wgpu::ShaderSource::Wgsl(shaders::IMAGE_SHADER.into()),
1541 });
1542
1543 let image_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1544 label: Some("Image Pipeline Layout"),
1545 bind_group_layouts: &[Some(uniform_layout), Some(image_layout)],
1546 immediate_size: 0,
1547 });
1548
1549 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1550 label: Some("Image Pipeline"),
1551 layout: Some(&image_pipeline_layout),
1552 vertex: wgpu::VertexState {
1553 module: &image_shader,
1554 entry_point: Some("image_vs_main"),
1555 compilation_options: wgpu::PipelineCompilationOptions::default(),
1556 buffers: &[Vertex::desc()],
1557 },
1558 fragment: Some(wgpu::FragmentState {
1559 module: &image_shader,
1560 entry_point: Some("image_fs_main"),
1561 compilation_options: wgpu::PipelineCompilationOptions::default(),
1562 targets: &[Some(wgpu::ColorTargetState {
1563 format: surface_format,
1564 blend: Some(blend_state_for_mode(blend_mode)),
1565 write_mask: wgpu::ColorWrites::ALL,
1566 })],
1567 }),
1568 primitive: wgpu::PrimitiveState {
1569 topology: wgpu::PrimitiveTopology::TriangleList,
1570 strip_index_format: None,
1571 front_face: wgpu::FrontFace::Ccw,
1572 cull_mode: None,
1573 unclipped_depth: false,
1574 polygon_mode: wgpu::PolygonMode::Fill,
1575 conservative: false,
1576 },
1577 depth_stencil: None,
1578 multisample: wgpu::MultisampleState::default(),
1579 multiview_mask: None,
1580 cache: None,
1581 })
1582}
1583
1584fn create_glyph_atlas_pipeline(
1585 device: &wgpu::Device,
1586 surface_format: wgpu::TextureFormat,
1587 uniform_layout: &wgpu::BindGroupLayout,
1588 image_layout: &wgpu::BindGroupLayout,
1589) -> wgpu::RenderPipeline {
1590 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1591 label: Some("Glyph Atlas Shader"),
1592 source: wgpu::ShaderSource::Wgsl(shaders::GLYPH_ATLAS_SHADER.into()),
1593 });
1594
1595 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1596 label: Some("Glyph Atlas Pipeline Layout"),
1597 bind_group_layouts: &[Some(uniform_layout), Some(image_layout)],
1598 immediate_size: 0,
1599 });
1600
1601 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1602 label: Some("Glyph Atlas Pipeline"),
1603 layout: Some(&pipeline_layout),
1604 vertex: wgpu::VertexState {
1605 module: &shader,
1606 entry_point: Some("glyph_atlas_vs_main"),
1607 compilation_options: wgpu::PipelineCompilationOptions::default(),
1608 buffers: &[Vertex::desc()],
1609 },
1610 fragment: Some(wgpu::FragmentState {
1611 module: &shader,
1612 entry_point: Some("glyph_atlas_fs_main"),
1613 compilation_options: wgpu::PipelineCompilationOptions::default(),
1614 targets: &[Some(wgpu::ColorTargetState {
1615 format: surface_format,
1616 blend: Some(blend_state_for_mode(BlendMode::SrcOver)),
1617 write_mask: wgpu::ColorWrites::ALL,
1618 })],
1619 }),
1620 primitive: wgpu::PrimitiveState {
1621 topology: wgpu::PrimitiveTopology::TriangleList,
1622 strip_index_format: None,
1623 front_face: wgpu::FrontFace::Ccw,
1624 cull_mode: None,
1625 unclipped_depth: false,
1626 polygon_mode: wgpu::PolygonMode::Fill,
1627 conservative: false,
1628 },
1629 depth_stencil: None,
1630 multisample: wgpu::MultisampleState::default(),
1631 multiview_mask: None,
1632 cache: None,
1633 })
1634}
1635
1636#[repr(C)]
1637#[derive(Copy, Clone, Debug, Pod, Zeroable)]
1638struct Vertex {
1639 position: [f32; 2],
1640 color: [f32; 4],
1641 uv: [f32; 2],
1642 uv_bounds: [f32; 4],
1643}
1644
1645impl Vertex {
1646 const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
1647 0 => Float32x2,
1648 1 => Float32x4,
1649 2 => Float32x2,
1650 3 => Float32x4
1651 ];
1652
1653 fn desc() -> wgpu::VertexBufferLayout<'static> {
1654 wgpu::VertexBufferLayout {
1655 array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
1656 step_mode: wgpu::VertexStepMode::Vertex,
1657 attributes: &Self::ATTRIBS,
1658 }
1659 }
1660}
1661
1662#[repr(C)]
1663#[derive(Copy, Clone, Debug, Pod, Zeroable)]
1664struct Uniforms {
1665 viewport: [f32; 2],
1666 viewport_offset: [f32; 2],
1667}
1668
1669#[repr(C)]
1675#[derive(Copy, Clone, Debug, Pod, Zeroable)]
1676struct ShapeData {
1677 rect: [f32; 4], radii: [f32; 4],
1682 gradient_params: [f32; 4], clip_rect: [f32; 4], stroke_params: [f32; 4],
1687 arc_params: [f32; 4],
1689 quad01: [f32; 4],
1691 quad23: [f32; 4],
1693 color: [f32; 4],
1695 brush_type: u32, gradient_start: u32, gradient_count: u32, gradient_tile_mode: u32, }
1700
1701const SHAPE_KIND_FILL: u32 = 0;
1703const SHAPE_KIND_STROKE: u32 = 1;
1704const SHAPE_KIND_ARC: u32 = 2;
1705
1706fn stroke_cap_code(cap: StrokeCap) -> u32 {
1707 match cap {
1708 StrokeCap::Butt => 0,
1709 StrokeCap::Round => 1,
1710 StrokeCap::Square => 2,
1711 }
1712}
1713
1714fn stroke_join_code(join: StrokeJoin) -> u32 {
1715 match join {
1716 StrokeJoin::Miter => 0,
1717 StrokeJoin::Round => 1,
1718 StrokeJoin::Bevel => 2,
1719 }
1720}
1721
1722fn pack_shape_flags(kind: u32, cap: StrokeCap, join: StrokeJoin) -> f32 {
1728 ((kind & 3) | (stroke_cap_code(cap) << 2) | (stroke_join_code(join) << 4)) as f32
1729}
1730
1731#[cfg(not(target_arch = "wasm32"))]
1739static SHAPE_CONVERT_TUNER: crate::cost_tuner::CostTuner =
1740 crate::cost_tuner::CostTuner::new("shape-convert", 256, 400_000);
1741
1742#[cfg(not(target_arch = "wasm32"))]
1743pub(crate) fn shape_convert_worker_count() -> usize {
1744 static WORKERS: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
1745 *WORKERS.get_or_init(|| {
1746 let cpus = std::thread::available_parallelism()
1747 .map(|count| count.get())
1748 .unwrap_or(1);
1749 let workers = cpus.clamp(1, 4);
1750 log::info!("[shape-convert] fan-out width {workers} (available parallelism {cpus})");
1754 workers
1755 })
1756}
1757
1758#[cfg(target_arch = "wasm32")]
1759pub(crate) fn shape_convert_worker_count() -> usize {
1760 1
1761}
1762
1763fn shape_gradient_stop_count(shape: &DrawShape, brushes: &[Brush]) -> usize {
1764 match shape.brush {
1765 SceneBrush::Solid(_) => 0,
1766 SceneBrush::Gradient(index) => match &brushes[index as usize] {
1767 Brush::Solid(_) => 0,
1768 Brush::LinearGradient { colors, .. }
1769 | Brush::RadialGradient { colors, .. }
1770 | Brush::SweepGradient { colors, .. } => colors.len(),
1771 },
1772 }
1773}
1774
1775fn convert_shape_into_slots(
1780 shape: &DrawShape,
1781 brushes: &[Brush],
1782 root_scale: f32,
1783 gradient_start: u32,
1784 shape_out: &mut ShapeData,
1785 gradient_out: &mut [GradientStop],
1786) {
1787 let snap_delta = shape
1788 .snap_anchor
1789 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
1790 .unwrap_or_default();
1791 let local_rect = shape.local_rect.translate(snap_delta.x, snap_delta.y);
1792 let quad = translate_quad(shape.quad, snap_delta);
1793 let clip = shape.clip;
1796 let canonicalize = shape.snap_anchor.is_some();
1797 let device_local_rect = if canonicalize {
1798 canonicalized_scaled_rect(local_rect, root_scale)
1799 } else {
1800 Rect {
1801 x: local_rect.x * root_scale,
1802 y: local_rect.y * root_scale,
1803 width: local_rect.width * root_scale,
1804 height: local_rect.height * root_scale,
1805 }
1806 };
1807 let device_quad = if canonicalize {
1808 canonicalized_scaled_quad(quad, root_scale)
1809 } else {
1810 scaled_quad(quad, root_scale)
1811 };
1812 let canonicalize_brush_coordinate = |value| {
1813 if canonicalize {
1814 canonicalize_device_coordinate(value)
1815 } else {
1816 value
1817 }
1818 };
1819
1820 let clip_rect = if let Some(clip) = clip {
1822 let device_clip = if canonicalize {
1823 canonicalized_scaled_rect(clip, root_scale)
1824 } else {
1825 Rect {
1826 x: clip.x * root_scale,
1827 y: clip.y * root_scale,
1828 width: clip.width * root_scale,
1829 height: clip.height * root_scale,
1830 }
1831 };
1832 [
1833 device_clip.x,
1834 device_clip.y,
1835 device_clip.width,
1836 device_clip.height,
1837 ]
1838 } else {
1839 [0.0, 0.0, 0.0, 0.0]
1840 };
1841
1842 let mut fill_gradient_entries = |colors: &[Color], stops: Option<&[f32]>| {
1844 let count = colors.len();
1845 let explicit_stops = stops.filter(|values| values.len() == count);
1846 for (index, color) in colors.iter().enumerate() {
1847 let position = explicit_stops
1848 .map(|values| values[index])
1849 .unwrap_or_else(|| {
1850 if count <= 1 {
1851 0.0
1852 } else {
1853 index as f32 / (count - 1) as f32
1854 }
1855 });
1856 gradient_out[index] = GradientStop {
1857 color: [color.r(), color.g(), color.b(), color.a()],
1858 position: [position, 0.0, 0.0, 0.0],
1859 };
1860 }
1861 count as u32
1862 };
1863 let mut gradient_params = [0.0f32; 4];
1864 let (brush_type, gradient_count, gradient_tile_mode) = match &shape.brush {
1865 SceneBrush::Solid(_) => (0u32, 0u32, gradient_tile_mode_value(TileMode::Clamp)),
1866 SceneBrush::Gradient(index) => match &brushes[*index as usize] {
1867 Brush::Solid(_) => (0u32, 0u32, gradient_tile_mode_value(TileMode::Clamp)),
1868 Brush::LinearGradient {
1869 colors,
1870 stops,
1871 start,
1872 end,
1873 tile_mode,
1874 } => {
1875 let count = fill_gradient_entries(colors, stops.as_deref());
1876 gradient_params = [
1877 canonicalize_brush_coordinate(resolve_gradient_point(
1878 device_local_rect.x,
1879 device_local_rect.width,
1880 start.x * root_scale,
1881 )),
1882 canonicalize_brush_coordinate(resolve_gradient_point(
1883 device_local_rect.y,
1884 device_local_rect.height,
1885 start.y * root_scale,
1886 )),
1887 canonicalize_brush_coordinate(resolve_gradient_point(
1888 device_local_rect.x,
1889 device_local_rect.width,
1890 end.x * root_scale,
1891 )),
1892 canonicalize_brush_coordinate(resolve_gradient_point(
1893 device_local_rect.y,
1894 device_local_rect.height,
1895 end.y * root_scale,
1896 )),
1897 ];
1898 (1u32, count, gradient_tile_mode_value(*tile_mode))
1899 }
1900 Brush::RadialGradient {
1901 colors,
1902 stops,
1903 center,
1904 radius,
1905 tile_mode,
1906 } => {
1907 let count = fill_gradient_entries(colors, stops.as_deref());
1908 gradient_params = [
1909 canonicalize_brush_coordinate(device_local_rect.x + center.x * root_scale),
1910 canonicalize_brush_coordinate(device_local_rect.y + center.y * root_scale),
1911 (radius * root_scale).max(f32::EPSILON),
1912 0.0,
1913 ];
1914 (2u32, count, gradient_tile_mode_value(*tile_mode))
1915 }
1916 Brush::SweepGradient {
1917 colors,
1918 stops,
1919 center,
1920 } => {
1921 let count = fill_gradient_entries(colors, stops.as_deref());
1922 gradient_params = [
1923 canonicalize_brush_coordinate(device_local_rect.x + center.x * root_scale),
1924 canonicalize_brush_coordinate(device_local_rect.y + center.y * root_scale),
1925 0.0,
1926 0.0,
1927 ];
1928 (3u32, count, gradient_tile_mode_value(TileMode::Clamp))
1929 }
1930 },
1931 };
1932
1933 let stroke_outset = shape
1938 .stroke
1939 .map(|stroke| stroke.half_width())
1940 .unwrap_or(0.0);
1941 let geometry_width = (local_rect.width - stroke_outset * 2.0).max(0.0);
1942 let geometry_height = (local_rect.height - stroke_outset * 2.0).max(0.0);
1943
1944 let radii = if let Some(arc) = shape.arc {
1945 if arc.sweep_angle >= cranpose_ui_graphics::TAU && arc.start_angle == 0.0 {
1955 [0.0, -1.0, 0.0, -1.0]
1956 } else {
1957 let half_sweep = arc.sweep_angle.clamp(0.0, cranpose_ui_graphics::TAU) * 0.5;
1958 let (mid_sin, mid_cos) = (arc.start_angle + half_sweep).sin_cos();
1959 let (half_sin, half_cos) = half_sweep.sin_cos();
1960 [mid_sin, mid_cos, half_sin.max(0.0), half_cos]
1961 }
1962 } else if let Some(rounded) = shape.shape {
1963 let resolved = rounded.resolve(geometry_width, geometry_height);
1964 [
1965 resolved.top_left * root_scale,
1966 resolved.top_right * root_scale,
1967 resolved.bottom_left * root_scale,
1968 resolved.bottom_right * root_scale,
1969 ]
1970 } else {
1971 [0.0, 0.0, 0.0, 0.0]
1972 };
1973
1974 let device_rect = [
1975 device_local_rect.x,
1976 device_local_rect.y,
1977 device_local_rect.width,
1978 device_local_rect.height,
1979 ];
1980
1981 let (stroke_params, arc_params) = match (shape.arc, shape.stroke) {
1985 (Some(arc), _) => (
1986 [
1987 0.0,
1988 pack_shape_flags(SHAPE_KIND_ARC, arc.cap, StrokeJoin::Miter),
1989 arc.outer_radius * root_scale,
1990 arc.inner_radius * root_scale,
1991 ],
1992 [
1993 (arc.center.x + snap_delta.x) * root_scale,
1994 (arc.center.y + snap_delta.y) * root_scale,
1995 arc.start_angle,
1996 arc.sweep_angle,
1997 ],
1998 ),
1999 (None, Some(stroke)) => (
2000 [
2001 stroke.width.max(0.0) * root_scale,
2002 pack_shape_flags(SHAPE_KIND_STROKE, stroke.cap, stroke.join),
2003 0.0,
2004 0.0,
2005 ],
2006 [0.0; 4],
2007 ),
2008 (None, None) => (
2009 [
2010 0.0,
2011 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter),
2012 0.0,
2013 0.0,
2014 ],
2015 [0.0; 4],
2016 ),
2017 };
2018
2019 let color = match &shape.brush {
2020 SceneBrush::Solid(c) => [c.r(), c.g(), c.b(), c.a()],
2021 SceneBrush::Gradient(index) => match &brushes[*index as usize] {
2022 Brush::Solid(c) => [c.r(), c.g(), c.b(), c.a()],
2023 Brush::LinearGradient { colors, .. } => {
2024 let first = colors.first().unwrap_or(&Color(1.0, 1.0, 1.0, 1.0));
2025 [first.r(), first.g(), first.b(), first.a()]
2026 }
2027 Brush::RadialGradient { colors, .. } | Brush::SweepGradient { colors, .. } => {
2028 let first = colors.first().unwrap_or(&Color(1.0, 1.0, 1.0, 1.0));
2029 [first.r(), first.g(), first.b(), first.a()]
2030 }
2031 },
2032 };
2033
2034 *shape_out = ShapeData {
2035 rect: device_rect,
2036 radii,
2037 gradient_params,
2038 clip_rect,
2039 stroke_params,
2040 arc_params,
2041 quad01: [
2042 device_quad[0][0],
2043 device_quad[0][1],
2044 device_quad[1][0],
2045 device_quad[1][1],
2046 ],
2047 quad23: [
2048 device_quad[2][0],
2049 device_quad[2][1],
2050 device_quad[3][0],
2051 device_quad[3][1],
2052 ],
2053 color,
2054 brush_type,
2055 gradient_start,
2056 gradient_count,
2057 gradient_tile_mode,
2058 };
2059}
2060
2061fn quad_area_diag_enabled() -> bool {
2068 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2069 *ENABLED.get_or_init(|| std::env::var_os("CRANPOSE_QUAD_AREA_DIAG").is_some())
2070}
2071
2072fn convert_shapes_into_outputs(
2078 shape_refs: &[&DrawShape],
2079 brushes: &[Brush],
2080 gradient_offsets: &[u32],
2081 root_scale: f32,
2082 shape_data_out: &mut [ShapeData],
2083 gradients_out: &mut [GradientStop],
2084) {
2085 let shape_count = shape_refs.len();
2086 #[cfg(not(target_arch = "wasm32"))]
2087 let convert_started = Instant::now();
2088 #[cfg(not(target_arch = "wasm32"))]
2089 let parallel =
2090 SHAPE_CONVERT_TUNER.choose_parallel(shape_count) && shape_convert_worker_count() > 1;
2091 if quad_area_diag_enabled() {
2092 let quad_area = |q: [[f32; 2]; 4]| {
2093 let poly = [q[0], q[1], q[3], q[2]];
2095 let mut twice = 0.0f64;
2096 for i in 0..4 {
2097 let a = poly[i];
2098 let b = poly[(i + 1) % 4];
2099 twice += a[0] as f64 * b[1] as f64 - b[0] as f64 * a[1] as f64;
2100 }
2101 twice.abs() * 0.5
2102 };
2103 let mut arc_quad = 0.0f64; let mut arc_band = 0.0f64; let mut arc_count = 0usize;
2106 let mut ring_count = 0usize;
2107 let mut other_quad = 0.0f64;
2108 let mut other_count = 0usize;
2109 let mut top_other: Vec<(f64, usize)> = Vec::new();
2112 for (index, shape) in shape_refs.iter().enumerate() {
2113 let area = quad_area(shape.quad);
2114 if let Some(arc) = shape.arc {
2115 arc_quad += area;
2116 arc_count += 1;
2117 if arc.sweep_angle >= cranpose_ui_graphics::TAU {
2118 ring_count += 1;
2119 }
2120 let ra = arc.mid_radius() as f64;
2121 let rb = arc.half_thickness() as f64;
2122 arc_band +=
2123 arc.sweep_angle as f64 * ra * (2.0 * rb) + std::f64::consts::PI * rb * rb;
2124 } else {
2125 other_quad += area;
2126 other_count += 1;
2127 top_other.push((area, index));
2128 }
2129 }
2130 let scale2 = (root_scale as f64) * (root_scale as f64);
2131 eprintln!(
2132 "[quad-area] arcs={arc_count} (rings={ring_count}) arc_quad_px={:.0} arc_band_px={:.0} | other={other_count} other_px={:.0}",
2133 arc_quad * scale2,
2134 arc_band * scale2,
2135 other_quad * scale2,
2136 );
2137 top_other.sort_by(|a, b| b.0.total_cmp(&a.0));
2138 for &(area, index) in top_other.iter().take(4) {
2139 let shape = shape_refs[index];
2140 let brush = match shape.brush.resolve(brushes).as_ref() {
2141 cranpose_ui_graphics::Brush::Solid(color) => format!("solid a={:.2}", color.3),
2142 cranpose_ui_graphics::Brush::LinearGradient { colors, .. } => {
2143 format!("linear n={}", colors.len())
2144 }
2145 cranpose_ui_graphics::Brush::RadialGradient { colors, .. } => {
2146 format!("radial n={}", colors.len())
2147 }
2148 cranpose_ui_graphics::Brush::SweepGradient { colors, .. } => {
2149 format!("sweep n={}", colors.len())
2150 }
2151 };
2152 eprintln!(
2153 "[quad-area] top other: {:.0}px {}x{} at ({:.0},{:.0}) {} shape={} stroke={} clip={} blend={:?} z={}",
2154 area * scale2,
2155 shape.rect.width.round(),
2156 shape.rect.height.round(),
2157 shape.rect.x,
2158 shape.rect.y,
2159 brush,
2160 shape.shape.is_some(),
2161 shape.stroke.is_some(),
2162 shape.clip.is_some(),
2163 shape.blend_mode,
2164 shape.z_index,
2165 );
2166 }
2167 }
2168 #[cfg(target_arch = "wasm32")]
2169 let parallel = false;
2170 let workers = if parallel {
2171 shape_convert_worker_count()
2172 } else {
2173 1
2174 };
2175 if workers <= 1 {
2176 for (idx, shape) in shape_refs.iter().enumerate() {
2177 let gradient_start = gradient_offsets[idx];
2178 let gradient_end = gradient_offsets[idx + 1];
2179 convert_shape_into_slots(
2180 shape,
2181 brushes,
2182 root_scale,
2183 gradient_start,
2184 &mut shape_data_out[idx],
2185 &mut gradients_out[gradient_start as usize..gradient_end as usize],
2186 );
2187 }
2188 #[cfg(not(target_arch = "wasm32"))]
2189 SHAPE_CONVERT_TUNER.record(
2190 false,
2191 shape_count,
2192 convert_started.elapsed().as_nanos() as u64,
2193 );
2194 return;
2195 }
2196
2197 let chunk_len = shape_count.div_ceil(workers);
2198 let mut shape_data_rest = shape_data_out;
2199 let mut gradients_rest = gradients_out;
2200 std::thread::scope(|scope| {
2201 let mut chunk_start = 0usize;
2202 while chunk_start < shape_count {
2203 let chunk_end = (chunk_start + chunk_len).min(shape_count);
2204 let count = chunk_end - chunk_start;
2205 let gradient_base = gradient_offsets[chunk_start];
2206 let gradient_span = (gradient_offsets[chunk_end] - gradient_base) as usize;
2207 let (shape_data_chunk, rest) = std::mem::take(&mut shape_data_rest).split_at_mut(count);
2208 shape_data_rest = rest;
2209 let (gradient_chunk, rest) =
2210 std::mem::take(&mut gradients_rest).split_at_mut(gradient_span);
2211 gradients_rest = rest;
2212 let chunk_refs = &shape_refs[chunk_start..chunk_end];
2213 let chunk_offsets = &gradient_offsets[chunk_start..=chunk_end];
2214 let mut convert_chunk = move || {
2215 for (j, shape) in chunk_refs.iter().enumerate() {
2216 let gradient_start = chunk_offsets[j];
2217 let local_start = (gradient_start - gradient_base) as usize;
2218 let local_end = (chunk_offsets[j + 1] - gradient_base) as usize;
2219 convert_shape_into_slots(
2220 shape,
2221 brushes,
2222 root_scale,
2223 gradient_start,
2224 &mut shape_data_chunk[j],
2225 &mut gradient_chunk[local_start..local_end],
2226 );
2227 }
2228 };
2229 if chunk_end == shape_count {
2230 convert_chunk();
2234 } else {
2235 scope.spawn(convert_chunk);
2236 }
2237 chunk_start = chunk_end;
2238 }
2239 });
2240 #[cfg(not(target_arch = "wasm32"))]
2241 SHAPE_CONVERT_TUNER.record(
2242 true,
2243 shape_count,
2244 convert_started.elapsed().as_nanos() as u64,
2245 );
2246}
2247
2248#[repr(C)]
2249#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2250struct GradientStop {
2251 color: [f32; 4],
2252 position: [f32; 4],
2253}
2254
2255#[cfg(not(target_arch = "wasm32"))]
2259const MAX_REPLAY_SLOTS: u32 = 128;
2260#[cfg(not(target_arch = "wasm32"))]
2261const REPLAY_TRANSFORM_STRIDE: u64 = 256;
2262
2263#[cfg(not(target_arch = "wasm32"))]
2270struct ReplaySlot {
2271 paint_buffer: wgpu::Buffer,
2276 bind_group: wgpu::BindGroup,
2277 shape_count: u32,
2278 paint_mirror: Vec<[f32; 4]>,
2284 mesh: Option<ReplaySlotMesh>,
2289 capture_epoch: u64,
2294 has_gradient: bool,
2299 fill_diag_shapes: Vec<FillDiagShapeRecord>,
2305}
2306
2307#[cfg(not(target_arch = "wasm32"))]
2312struct ReplaySlotMesh {
2313 vertex_buffer: wgpu::Buffer,
2314 index_buffer: wgpu::Buffer,
2320 index_prefix: Vec<u32>,
2325}
2326
2327#[cfg(not(target_arch = "wasm32"))]
2331#[repr(C)]
2332#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2333struct MeshVertex {
2334 position: [f32; 2],
2335 uv: [f32; 2],
2336 shape_idx: u32,
2337}
2338
2339#[cfg(not(target_arch = "wasm32"))]
2340impl MeshVertex {
2341 const ATTRIBS: [wgpu::VertexAttribute; 3] =
2342 wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Uint32];
2343
2344 fn desc() -> wgpu::VertexBufferLayout<'static> {
2345 wgpu::VertexBufferLayout {
2346 array_stride: std::mem::size_of::<MeshVertex>() as wgpu::BufferAddress,
2347 step_mode: wgpu::VertexStepMode::Vertex,
2348 attributes: &Self::ATTRIBS,
2349 }
2350 }
2351}
2352
2353#[cfg(not(target_arch = "wasm32"))]
2358fn arc_mesh_enabled() -> bool {
2359 matches!(std::env::var("CRANPOSE_ARC_MESH").as_deref(), Ok(v) if v != "0")
2367}
2368
2369#[cfg(not(target_arch = "wasm32"))]
2375const ARC_MESH_MARGIN: f32 = 1.0;
2376
2377#[cfg(not(target_arch = "wasm32"))]
2381const ARC_MESH_OVERSHOOT: f32 = 2.0;
2382
2383#[cfg(not(target_arch = "wasm32"))]
2384const ARC_MESH_MIN_SEGMENTS: usize = 4;
2385#[cfg(not(target_arch = "wasm32"))]
2386const ARC_MESH_MAX_SEGMENTS: usize = 64;
2387
2388#[cfg(not(target_arch = "wasm32"))]
2397const ARC_MESH_BUDGET_BYTES_PER_SHAPE: usize = 48 * std::mem::size_of::<MeshVertex>();
2398#[cfg(not(target_arch = "wasm32"))]
2399const ARC_MESH_BUDGET_FLOOR_BYTES: usize = 4096 * std::mem::size_of::<MeshVertex>();
2400
2401#[cfg(not(target_arch = "wasm32"))]
2404fn arc_mesh_bytes(vertices: usize, indices: usize) -> usize {
2405 vertices * std::mem::size_of::<MeshVertex>() + indices * std::mem::size_of::<u32>()
2406}
2407
2408#[cfg(not(target_arch = "wasm32"))]
2413struct ArcMeshBand {
2414 center: [f32; 2],
2415 inner: f32,
2416 outer: f32,
2417 start: f32,
2418 sweep: f32,
2419}
2420
2421#[cfg(not(target_arch = "wasm32"))]
2422fn arc_mesh_band(shape: &ShapeData) -> Option<ArcMeshBand> {
2423 let flags = shape.stroke_params[1].max(0.0) as u32;
2425 if flags & 3 != SHAPE_KIND_ARC {
2426 return None;
2427 }
2428 if shape.brush_type != 0 {
2432 return None;
2433 }
2434 if shape.clip_rect[2] > 0.0 && shape.clip_rect[3] > 0.0 {
2439 return None;
2440 }
2441 let [_, _, w, h] = shape.rect;
2442 if !(w > 0.0 && h > 0.0) {
2443 return None;
2444 }
2445 let [left, top, right, _] = shape.quad01;
2453 let [bl_x, bottom, br_x, br_y] = shape.quad23;
2454 let axis_aligned = shape.quad01[3] == top
2455 && bl_x == left
2456 && br_x == right
2457 && br_y == bottom
2458 && left < right
2459 && top < bottom;
2460 if !axis_aligned {
2461 return None;
2462 }
2463 let center = [shape.arc_params[0], shape.arc_params[1]];
2464 let start = shape.arc_params[2];
2465 let sweep = shape.arc_params[3];
2466 let outer = shape.stroke_params[2];
2467 let inner = shape.stroke_params[3];
2468 let finite = center[0].is_finite()
2469 && center[1].is_finite()
2470 && start.is_finite()
2471 && sweep.is_finite()
2472 && outer.is_finite()
2473 && inner.is_finite();
2474 if !finite || outer <= 0.0 || sweep <= 0.0 {
2475 return None;
2476 }
2477 Some(ArcMeshBand {
2478 center,
2479 inner,
2480 outer,
2481 start,
2482 sweep,
2483 })
2484}
2485
2486#[cfg(not(target_arch = "wasm32"))]
2495fn rim_mesh_enabled() -> bool {
2496 !matches!(std::env::var("CRANPOSE_RIM_MESH").as_deref(), Ok("0"))
2497}
2498
2499#[cfg(not(target_arch = "wasm32"))]
2507const RIM_MESH_VERTEX_CAPACITY: usize = 8192;
2508#[cfg(not(target_arch = "wasm32"))]
2511const RIM_MESH_INDEX_CAPACITY: usize = 32768;
2512
2513#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
2519#[derive(Clone, Copy, Debug)]
2520struct RimDraw {
2521 shape_index: u32,
2522 first_index: u32,
2523 index_count: u32,
2524}
2525
2526#[cfg(not(target_arch = "wasm32"))]
2530fn rim_mesh_capacity_warn() {
2531 use std::sync::atomic::{AtomicU64, Ordering};
2532 static OVERFLOWS: AtomicU64 = AtomicU64::new(0);
2533 let count = OVERFLOWS.fetch_add(1, Ordering::Relaxed);
2534 if count.is_multiple_of(512) {
2535 log::warn!(
2536 "[rim-mesh] transient buffers full; rim falls back to quad expansion \
2537 (lifetime overflows {})",
2538 count + 1,
2539 );
2540 }
2541}
2542
2543#[cfg(not(target_arch = "wasm32"))]
2568fn rim_mesh_band(shape: &ShapeData) -> Option<ArcMeshBand> {
2569 let flags = shape.stroke_params[1].max(0.0) as u32;
2571 if flags & 3 != SHAPE_KIND_STROKE {
2572 return None;
2573 }
2574 if shape.brush_type != 0 {
2577 return None;
2578 }
2579 if shape.clip_rect[2] > 0.0 && shape.clip_rect[3] > 0.0 {
2583 return None;
2584 }
2585 let [x, y, w, h] = shape.rect;
2586 if !(w > 0.0 && h > 0.0) {
2587 return None;
2588 }
2589 let [left, top, right, _] = shape.quad01;
2594 let [bl_x, bottom, br_x, br_y] = shape.quad23;
2595 let axis_aligned = shape.quad01[3] == top
2596 && bl_x == left
2597 && br_x == right
2598 && br_y == bottom
2599 && left < right
2600 && top < bottom;
2601 if !axis_aligned {
2602 return None;
2603 }
2604 if w * h < 65536.0 {
2607 return None;
2608 }
2609 if w.to_bits() != h.to_bits() {
2611 return None;
2612 }
2613 let [r0, r1, r2, r3] = shape.radii;
2615 if r0.to_bits() != r1.to_bits() || r0.to_bits() != r2.to_bits() || r0.to_bits() != r3.to_bits()
2616 {
2617 return None;
2618 }
2619 if !r0.is_finite() || r0 <= 0.0 {
2620 return None;
2621 }
2622 let sw = shape.stroke_params[0];
2623 if !sw.is_finite() || sw <= 0.0 {
2624 return None;
2625 }
2626 let geom_half = (w - sw) * 0.5;
2629 let center = [x + w * 0.5, y + h * 0.5];
2630 let inner = geom_half - sw * 0.5;
2631 let outer = geom_half + sw * 0.5;
2632 let finite =
2633 center[0].is_finite() && center[1].is_finite() && inner.is_finite() && outer.is_finite();
2634 if !finite || outer <= 0.0 {
2635 return None;
2636 }
2637 if (r0 - geom_half).abs() > 0.01 {
2639 return None;
2640 }
2641 Some(ArcMeshBand {
2642 center,
2643 inner,
2644 outer,
2645 start: 0.0,
2646 sweep: cranpose_ui_graphics::TAU,
2647 })
2648}
2649
2650#[cfg(not(target_arch = "wasm32"))]
2657fn static_span_enabled() -> bool {
2658 !matches!(std::env::var("CRANPOSE_STATIC_SPAN").as_deref(), Ok("0"))
2659}
2660
2661#[cfg(not(target_arch = "wasm32"))]
2665const STATIC_SPAN_MAX_SHAPES: usize = 16;
2666
2667#[cfg(not(target_arch = "wasm32"))]
2671const STATIC_SPAN_UPGRADE_FRAMES: u32 = 30;
2672
2673#[cfg(not(target_arch = "wasm32"))]
2676#[derive(Clone, Copy, Debug, PartialEq)]
2677enum StaticSpanDecision {
2678 Pass,
2680 Hit { skip: usize },
2683 Capture { len: usize, clear: wgpu::Color },
2686}
2687
2688#[cfg(not(target_arch = "wasm32"))]
2736#[derive(Default)]
2737struct StaticSpanCache {
2738 texture: Option<OffscreenTarget>,
2742 key_shapes: Vec<ShapeData>,
2744 key_gradients: Vec<GradientStop>,
2746 key_width: u32,
2747 key_height: u32,
2748 key_clear: [u64; 4],
2752 key_has_gradient: bool,
2757 prev_shapes: Vec<ShapeData>,
2760 prev_gradients: Vec<GradientStop>,
2761 extension_stable_frames: u32,
2764 armed: bool,
2769 hits: u64,
2770 recaptures: u64,
2771}
2772
2773#[cfg(not(target_arch = "wasm32"))]
2774impl StaticSpanCache {
2775 fn engage(
2781 &mut self,
2782 load_op: wgpu::LoadOp<wgpu::Color>,
2783 first_batch: Option<(usize, BlendMode, bool)>,
2784 width: u32,
2785 height: u32,
2786 shapes: &[ShapeData],
2787 gradients: &[GradientStop],
2788 ) -> StaticSpanDecision {
2789 if !self.armed || !static_span_enabled() {
2790 return StaticSpanDecision::Pass;
2791 }
2792 let wgpu::LoadOp::Clear(clear) = load_op else {
2793 return StaticSpanDecision::Pass;
2794 };
2795 if clear.a != 1.0 {
2799 return StaticSpanDecision::Pass;
2800 }
2801 self.armed = false;
2802 let Some((batch_len, blend_mode, has_gradient)) = first_batch else {
2803 self.forget_observation();
2804 return StaticSpanDecision::Pass;
2805 };
2806 if blend_mode != BlendMode::SrcOver || batch_len == 0 {
2809 self.forget_observation();
2810 return StaticSpanDecision::Pass;
2811 }
2812 let leading = &shapes[..batch_len.min(STATIC_SPAN_MAX_SHAPES).min(shapes.len())];
2813 if leading.is_empty() {
2814 self.forget_observation();
2815 return StaticSpanDecision::Pass;
2816 }
2817 if !static_span_fullscreen_opaque(&leading[0], width, height) {
2818 self.forget_observation();
2819 return StaticSpanDecision::Pass;
2820 }
2821 let mut eligible = 1;
2824 while eligible < leading.len() && rim_mesh_band(&leading[eligible]).is_none() {
2825 eligible += 1;
2826 }
2827 let leading = &leading[..eligible];
2828 let clear_key = [
2829 clear.r.to_bits(),
2830 clear.g.to_bits(),
2831 clear.b.to_bits(),
2832 clear.a.to_bits(),
2833 ];
2834
2835 let key_len = self.key_shapes.len();
2836 let valid = self.texture.is_some()
2837 && key_len > 0
2838 && key_len <= leading.len()
2839 && self.key_width == width
2840 && self.key_height == height
2841 && self.key_clear == clear_key
2842 && self.key_has_gradient == has_gradient
2843 && span_records_equal(
2844 &self.key_shapes,
2845 &leading[..key_len],
2846 &self.key_gradients,
2847 gradients,
2848 );
2849
2850 let mut stable = 0;
2854 while stable < leading.len()
2855 && stable < self.prev_shapes.len()
2856 && span_records_equal(
2857 &self.prev_shapes[stable..stable + 1],
2858 &leading[stable..stable + 1],
2859 &self.prev_gradients,
2860 gradients,
2861 )
2862 {
2863 stable += 1;
2864 }
2865 self.remember_observation(leading, gradients);
2866
2867 if valid {
2868 if stable > key_len {
2880 self.extension_stable_frames += 1;
2881 if self.extension_stable_frames >= STATIC_SPAN_UPGRADE_FRAMES {
2882 self.extension_stable_frames = 0;
2883 return StaticSpanDecision::Capture { len: stable, clear };
2884 }
2885 } else {
2886 self.extension_stable_frames = 0;
2887 }
2888 self.hits += 1;
2889 if self.hits.is_multiple_of(600) {
2890 log::debug!(
2891 "[static-span] {} hits / {} recaptures lifetime (span {} shapes, {}x{})",
2892 self.hits,
2893 self.recaptures,
2894 key_len,
2895 width,
2896 height,
2897 );
2898 }
2899 return StaticSpanDecision::Hit { skip: key_len };
2900 }
2901
2902 self.extension_stable_frames = 0;
2903 if stable == 0 || span_gradient_len(&leading[..stable]) == 0 {
2912 return StaticSpanDecision::Pass;
2913 }
2914 StaticSpanDecision::Capture { len: stable, clear }
2915 }
2916
2917 fn remember_observation(&mut self, leading: &[ShapeData], gradients: &[GradientStop]) {
2919 self.prev_shapes.clear();
2920 self.prev_shapes.extend_from_slice(leading);
2921 let stop_len = span_gradient_len(leading);
2922 self.prev_gradients.clear();
2923 self.prev_gradients
2924 .extend_from_slice(&gradients[..stop_len]);
2925 }
2926
2927 fn forget_observation(&mut self) {
2928 self.prev_shapes.clear();
2929 self.prev_gradients.clear();
2930 self.extension_stable_frames = 0;
2931 }
2932
2933 #[allow(clippy::too_many_arguments)]
2936 fn store_key(
2937 &mut self,
2938 span: &[ShapeData],
2939 gradients: &[GradientStop],
2940 width: u32,
2941 height: u32,
2942 clear: wgpu::Color,
2943 has_gradient: bool,
2944 ) {
2945 self.key_shapes.clear();
2946 self.key_shapes.extend_from_slice(span);
2947 let stop_len = span_gradient_len(span);
2948 self.key_gradients.clear();
2949 self.key_gradients.extend_from_slice(&gradients[..stop_len]);
2950 self.key_width = width;
2951 self.key_height = height;
2952 self.key_clear = [
2953 clear.r.to_bits(),
2954 clear.g.to_bits(),
2955 clear.b.to_bits(),
2956 clear.a.to_bits(),
2957 ];
2958 self.key_has_gradient = has_gradient;
2959 self.recaptures += 1;
2960 if self.recaptures.is_multiple_of(64) || self.recaptures == 1 {
2961 log::debug!(
2962 "[static-span] recapture #{} (span {} shapes, {} stops, {}x{}; {} hits lifetime)",
2963 self.recaptures,
2964 self.key_shapes.len(),
2965 self.key_gradients.len(),
2966 width,
2967 height,
2968 self.hits,
2969 );
2970 }
2971 }
2972}
2973
2974#[cfg(not(target_arch = "wasm32"))]
2978fn span_gradient_len(span: &[ShapeData]) -> usize {
2979 span.iter().map(|shape| shape.gradient_count as usize).sum()
2980}
2981
2982#[cfg(not(target_arch = "wasm32"))]
2989fn span_records_equal(
2990 expected: &[ShapeData],
2991 actual: &[ShapeData],
2992 expected_gradients: &[GradientStop],
2993 actual_gradients: &[GradientStop],
2994) -> bool {
2995 if bytemuck::cast_slice::<ShapeData, u8>(expected)
2996 != bytemuck::cast_slice::<ShapeData, u8>(actual)
2997 {
2998 return false;
2999 }
3000 for shape in expected {
3001 let start = shape.gradient_start as usize;
3002 let end = start + shape.gradient_count as usize;
3003 if end > expected_gradients.len() || end > actual_gradients.len() {
3004 return false;
3005 }
3006 if bytemuck::cast_slice::<GradientStop, u8>(&expected_gradients[start..end])
3007 != bytemuck::cast_slice::<GradientStop, u8>(&actual_gradients[start..end])
3008 {
3009 return false;
3010 }
3011 }
3012 true
3013}
3014
3015#[cfg(not(target_arch = "wasm32"))]
3024fn static_span_fullscreen_opaque(shape: &ShapeData, width: u32, height: u32) -> bool {
3025 if shape.brush_type != 0 || shape.gradient_count != 0 {
3026 return false;
3027 }
3028 if shape.color[3] != 1.0 {
3029 return false;
3030 }
3031 if shape.clip_rect != [0.0; 4] || shape.stroke_params != [0.0; 4] || shape.radii != [0.0; 4] {
3032 return false;
3033 }
3034 let [left, top, right, top_right_y] = shape.quad01;
3037 let [bl_x, bottom, br_x, br_y] = shape.quad23;
3038 let axis_aligned = top_right_y == top
3039 && bl_x == left
3040 && br_x == right
3041 && br_y == bottom
3042 && left < right
3043 && top < bottom;
3044 axis_aligned && left <= 0.0 && top <= 0.0 && right >= width as f32 && bottom >= height as f32
3045}
3046
3047#[cfg(not(target_arch = "wasm32"))]
3053fn emit_passthrough_quad(
3054 shape: &ShapeData,
3055 shape_idx: u32,
3056 vertices: &mut Vec<MeshVertex>,
3057 indices: &mut Vec<u32>,
3058) {
3059 let base = vertices.len() as u32;
3060 let corners = [
3061 ([shape.quad01[0], shape.quad01[1]], [0.0, 0.0]),
3062 ([shape.quad01[2], shape.quad01[3]], [1.0, 0.0]),
3063 ([shape.quad23[0], shape.quad23[1]], [0.0, 1.0]),
3064 ([shape.quad23[2], shape.quad23[3]], [1.0, 1.0]),
3065 ];
3066 for (position, uv) in corners {
3067 vertices.push(MeshVertex {
3068 position,
3069 uv,
3070 shape_idx,
3071 });
3072 }
3073 indices.extend([0u32, 1, 2, 2, 1, 3].map(|corner| base + corner));
3074}
3075
3076#[cfg(not(target_arch = "wasm32"))]
3087fn clip_polygon_axis(
3088 input: &[[f32; 2]],
3089 axis: usize,
3090 bound: f32,
3091 keep_at_most: bool,
3092 output: &mut Vec<[f32; 2]>,
3093) {
3094 output.clear();
3095 let inside = |p: [f32; 2]| {
3096 if keep_at_most {
3097 p[axis] <= bound
3098 } else {
3099 p[axis] >= bound
3100 }
3101 };
3102 let intersect = |a: [f32; 2], b: [f32; 2]| {
3103 let (p, q) = if (b[0], b[1]) < (a[0], a[1]) {
3104 (b, a)
3105 } else {
3106 (a, b)
3107 };
3108 let t = (bound - p[axis]) / (q[axis] - p[axis]);
3109 let mut point = [0.0f32; 2];
3110 point[axis] = bound;
3111 point[1 - axis] = p[1 - axis] + t * (q[1 - axis] - p[1 - axis]);
3112 point
3113 };
3114 for (index, ¤t) in input.iter().enumerate() {
3115 let previous = input[(index + input.len() - 1) % input.len()];
3116 match (inside(previous), inside(current)) {
3117 (true, true) => output.push(current),
3118 (true, false) => output.push(intersect(previous, current)),
3119 (false, true) => {
3120 output.push(intersect(previous, current));
3121 output.push(current);
3122 }
3123 (false, false) => {}
3124 }
3125 }
3126}
3127
3128#[cfg(not(target_arch = "wasm32"))]
3163fn emit_arc_band_mesh(
3164 shape: &ShapeData,
3165 shape_idx: u32,
3166 band: &ArcMeshBand,
3167 vertices: &mut Vec<MeshVertex>,
3168 indices: &mut Vec<u32>,
3169) -> Option<usize> {
3170 let [cx, cy] = band.center;
3171 let ra = (band.outer + band.inner) * 0.5;
3172 let rb = ((band.outer - band.inner) * 0.5).max(0.0);
3173 let rb_m = rb + ARC_MESH_MARGIN;
3174 let ro = ra + rb_m;
3175 let ri = (ra - rb_m).max(0.0);
3176 let tau = cranpose_ui_graphics::TAU;
3177
3178 let (range_start, range) = if band.sweep >= tau {
3179 (0.0, tau)
3180 } else {
3181 let pad = if rb_m < ra {
3182 (rb_m / ra).asin() + 0.05
3183 } else {
3184 std::f32::consts::PI
3187 };
3188 let padded = band.sweep + pad + pad;
3189 if padded >= tau {
3190 (0.0, tau)
3191 } else {
3192 (band.start - pad, padded)
3193 }
3194 };
3195 let closed = range >= tau;
3196
3197 let dtheta = (2.0 * (ro / (ro + ARC_MESH_OVERSHOOT)).acos()).clamp(tau / 64.0, tau / 6.0);
3198 let segments =
3199 ((range / dtheta).ceil() as usize).clamp(ARC_MESH_MIN_SEGMENTS, ARC_MESH_MAX_SEGMENTS);
3200 let step = range / segments as f32;
3201 let rc = ro / (step * 0.5).cos();
3202
3203 let boundary_count = if closed { segments } else { segments + 1 };
3207 let mut boundaries = Vec::with_capacity(boundary_count);
3208 for j in 0..boundary_count {
3209 let (sin, cos) = (range_start + step * j as f32).sin_cos();
3210 boundaries.push((
3211 [cx + cos * ri, cy + sin * ri],
3212 [cx + cos * rc, cy + sin * rc],
3213 ));
3214 }
3215
3216 let quad_min = [shape.quad01[0], shape.quad01[1]];
3217 let quad_max = [shape.quad23[2], shape.quad23[3]];
3218
3219 enum SegmentGeometry {
3224 Shared,
3225 Fan(Vec<[f32; 2]>),
3226 Empty,
3227 }
3228
3229 let mut polygon: Vec<[f32; 2]> = Vec::with_capacity(8);
3231 let mut scratch: Vec<[f32; 2]> = Vec::with_capacity(8);
3232 let mut segment_geometry = Vec::with_capacity(segments);
3233 let mut boundary_used = vec![false; boundary_count];
3234 for j in 0..segments {
3235 let jb = (j + 1) % boundary_count;
3236 let (inner_a, outer_a) = boundaries[j];
3237 let (inner_b, outer_b) = boundaries[jb];
3238 polygon.clear();
3239 polygon.extend_from_slice(&[inner_a, outer_a, outer_b, inner_b]);
3240 clip_polygon_axis(&polygon, 0, quad_min[0], false, &mut scratch);
3241 clip_polygon_axis(&scratch, 0, quad_max[0], true, &mut polygon);
3242 clip_polygon_axis(&polygon, 1, quad_min[1], false, &mut scratch);
3243 clip_polygon_axis(&scratch, 1, quad_max[1], true, &mut polygon);
3244 scratch.clear();
3247 for &point in polygon.iter() {
3248 if scratch.last() != Some(&point) {
3249 scratch.push(point);
3250 }
3251 }
3252 while scratch.len() > 1 && scratch.first() == scratch.last() {
3253 scratch.pop();
3254 }
3255 if scratch.len() < 3 {
3256 segment_geometry.push(SegmentGeometry::Empty);
3257 } else if scratch[..] == [inner_a, outer_a, outer_b, inner_b] {
3258 boundary_used[j] = true;
3259 boundary_used[jb] = true;
3260 segment_geometry.push(SegmentGeometry::Shared);
3261 } else {
3262 segment_geometry.push(SegmentGeometry::Fan(scratch.clone()));
3263 }
3264 }
3265
3266 let push_vertex = |vertices: &mut Vec<MeshVertex>, position: [f32; 2]| -> u32 {
3267 let index = vertices.len() as u32;
3268 vertices.push(MeshVertex {
3269 position,
3270 uv: [
3271 (position[0] - shape.rect[0]) / shape.rect[2],
3272 (position[1] - shape.rect[1]) / shape.rect[3],
3273 ],
3274 shape_idx,
3275 });
3276 index
3277 };
3278
3279 let mut boundary_vertex = vec![[0u32; 2]; boundary_count];
3282 for (j, used) in boundary_used.iter().enumerate() {
3283 if *used {
3284 let (inner, outer) = boundaries[j];
3285 boundary_vertex[j] = [push_vertex(vertices, inner), push_vertex(vertices, outer)];
3286 }
3287 }
3288
3289 let start_len = indices.len();
3292 for (j, geometry) in segment_geometry.iter().enumerate() {
3293 match geometry {
3294 SegmentGeometry::Empty => {}
3295 SegmentGeometry::Shared => {
3296 let jb = (j + 1) % boundary_count;
3297 let [in_a, out_a] = boundary_vertex[j];
3298 let [in_b, out_b] = boundary_vertex[jb];
3299 indices.extend_from_slice(&[in_a, out_a, out_b, in_a, out_b, in_b]);
3303 }
3304 SegmentGeometry::Fan(points) => {
3305 let base = vertices.len() as u32;
3306 for &point in points {
3307 push_vertex(vertices, point);
3308 }
3309 for i in 1..points.len() as u32 - 1 {
3310 indices.extend_from_slice(&[base, base + i, base + i + 1]);
3311 }
3312 }
3313 }
3314 }
3315 if indices.len() == start_len {
3316 return None;
3317 }
3318 Some(segments)
3319}
3320
3321#[cfg(not(target_arch = "wasm32"))]
3324fn triangles_shoelace_area(vertices: &[MeshVertex], indices: &[u32]) -> f64 {
3325 indices
3326 .as_chunks::<3>()
3327 .0
3328 .iter()
3329 .map(|tri| {
3330 let [a, b, c] = [
3331 vertices[tri[0] as usize].position,
3332 vertices[tri[1] as usize].position,
3333 vertices[tri[2] as usize].position,
3334 ];
3335 let cross = (b[0] as f64 - a[0] as f64) * (c[1] as f64 - a[1] as f64)
3336 - (b[1] as f64 - a[1] as f64) * (c[0] as f64 - a[0] as f64);
3337 cross.abs() * 0.5
3338 })
3339 .sum()
3340}
3341
3342#[cfg(not(target_arch = "wasm32"))]
3345fn quad_shoelace_area(shape: &ShapeData) -> f64 {
3346 let corners = [
3347 [shape.quad01[0] as f64, shape.quad01[1] as f64],
3348 [shape.quad01[2] as f64, shape.quad01[3] as f64],
3349 [shape.quad23[0] as f64, shape.quad23[1] as f64],
3350 [shape.quad23[2] as f64, shape.quad23[3] as f64],
3351 ];
3352 let tri = |a: [f64; 2], b: [f64; 2], c: [f64; 2]| {
3353 ((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])).abs() * 0.5
3354 };
3355 tri(corners[0], corners[1], corners[2]) + tri(corners[2], corners[1], corners[3])
3356}
3357
3358#[cfg(not(target_arch = "wasm32"))]
3363pub(crate) fn fill_area_diag_enabled() -> bool {
3364 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3365 *ENABLED.get_or_init(
3366 || matches!(std::env::var("CRANPOSE_FILL_DIAG").as_deref(), Ok(value) if value != "0"),
3367 )
3368}
3369
3370#[cfg(not(target_arch = "wasm32"))]
3372const FILL_DIAG_WINDOW_FRAMES: u32 = 120;
3373
3374#[cfg(not(target_arch = "wasm32"))]
3375const FILL_DIAG_BUCKETS: usize = 9;
3376
3377#[cfg(not(target_arch = "wasm32"))]
3384#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3385enum FillOpacityClass {
3386 Opaque = 0,
3387 Translucent = 1,
3388 NonSolid = 2,
3389}
3390
3391#[cfg(not(target_arch = "wasm32"))]
3392fn fill_opacity_class(shape: &ShapeData) -> FillOpacityClass {
3393 if shape.brush_type != 0 {
3394 FillOpacityClass::NonSolid
3395 } else if shape.color[3] == 1.0 {
3396 FillOpacityClass::Opaque
3397 } else {
3398 FillOpacityClass::Translucent
3399 }
3400}
3401
3402#[cfg(not(target_arch = "wasm32"))]
3407fn fill_diag_bucket(shape: &ShapeData) -> usize {
3408 match shape.stroke_params[1].max(0.0) as u32 & 3 {
3409 SHAPE_KIND_ARC => FillAreaDiag::ARC,
3410 SHAPE_KIND_STROKE => FillAreaDiag::RRECT_STROKE,
3411 _ if shape.radii.iter().any(|radius| *radius > 0.0) => FillAreaDiag::RRECT_FILL,
3412 _ => FillAreaDiag::RECT,
3413 }
3414}
3415
3416#[cfg(not(target_arch = "wasm32"))]
3417fn fill_diag_bucket_name(bucket: usize) -> &'static str {
3418 match bucket {
3419 FillAreaDiag::ARC => "arc",
3420 FillAreaDiag::RRECT_STROKE => "rrect-stroke",
3421 FillAreaDiag::RRECT_FILL => "rrect-fill",
3422 FillAreaDiag::RECT => "rect",
3423 FillAreaDiag::MESH => "mesh",
3424 FillAreaDiag::RETAINED => "retained",
3425 FillAreaDiag::IMAGE_GLYPH => "img+glyph",
3426 FillAreaDiag::EFFECT_COMPOSITE => "effect-comp",
3427 FillAreaDiag::OFFSCREEN_SOURCE => "offscr-src",
3428 _ => "?",
3429 }
3430}
3431
3432#[cfg(not(target_arch = "wasm32"))]
3453fn analytic_covered_area(shape: &ShapeData) -> f64 {
3454 let flags = shape.stroke_params[1].max(0.0) as u32;
3455 match flags & 3 {
3456 SHAPE_KIND_ARC => {
3457 let outer = f64::from(shape.stroke_params[2]).max(0.0);
3458 let inner = f64::from(shape.stroke_params[3]).clamp(0.0, outer);
3459 let tau = f64::from(cranpose_ui_graphics::TAU);
3460 let sweep = f64::from(shape.arc_params[3]).clamp(0.0, tau);
3461 let thickness = outer - inner;
3462 let band = sweep * 0.5 * (outer + inner) * thickness;
3463 let caps = if sweep >= tau {
3464 0.0
3465 } else {
3466 match (flags >> 2) & 3 {
3467 1 | 2 => std::f64::consts::PI * (thickness * 0.5) * (thickness * 0.5),
3471 _ => 0.0,
3472 }
3473 };
3474 band + caps
3475 }
3476 SHAPE_KIND_STROKE => {
3477 let stroke_width = f64::from(shape.stroke_params[0]).max(0.0);
3478 let geom_w = (f64::from(shape.rect[2]) - stroke_width).max(0.0);
3480 let geom_h = (f64::from(shape.rect[3]) - stroke_width).max(0.0);
3481 let max_radius = geom_w.min(geom_h) * 0.5;
3482 let radii_sum: f64 = shape
3483 .radii
3484 .iter()
3485 .map(|radius| f64::from(*radius).clamp(0.0, max_radius))
3486 .sum();
3487 let perimeter =
3488 2.0 * (geom_w + geom_h) - (2.0 - std::f64::consts::FRAC_PI_2) * radii_sum;
3489 perimeter.max(0.0) * stroke_width
3490 }
3491 _ => {
3492 let width = f64::from(shape.rect[2]).max(0.0);
3493 let height = f64::from(shape.rect[3]).max(0.0);
3494 let max_radius = width.min(height) * 0.5;
3495 let radii_sq: f64 = shape
3496 .radii
3497 .iter()
3498 .map(|radius| {
3499 let radius = f64::from(*radius).clamp(0.0, max_radius);
3500 radius * radius
3501 })
3502 .sum();
3503 width * height - (1.0 - std::f64::consts::FRAC_PI_4) * radii_sq
3504 }
3505 }
3506}
3507
3508#[cfg(not(target_arch = "wasm32"))]
3514fn aa_perimeter_allowance(shape: &ShapeData) -> f64 {
3515 let flags = shape.stroke_params[1].max(0.0) as u32;
3516 match flags & 3 {
3517 SHAPE_KIND_ARC => {
3518 let outer = f64::from(shape.stroke_params[2]).max(0.0);
3519 let inner = f64::from(shape.stroke_params[3]).clamp(0.0, outer);
3520 let tau = f64::from(cranpose_ui_graphics::TAU);
3521 let sweep = f64::from(shape.arc_params[3]).clamp(0.0, tau);
3522 let ends = if sweep >= tau {
3523 0.0
3524 } else {
3525 2.0 * (outer - inner)
3526 };
3527 sweep * (outer + inner) + ends
3528 }
3529 SHAPE_KIND_STROKE => {
3530 let stroke_width = f64::from(shape.stroke_params[0]).max(0.0);
3531 let geom_w = (f64::from(shape.rect[2]) - stroke_width).max(0.0);
3532 let geom_h = (f64::from(shape.rect[3]) - stroke_width).max(0.0);
3533 let max_radius = geom_w.min(geom_h) * 0.5;
3534 let radii_sum: f64 = shape
3535 .radii
3536 .iter()
3537 .map(|radius| f64::from(*radius).clamp(0.0, max_radius))
3538 .sum();
3539 let perimeter =
3540 2.0 * (geom_w + geom_h) - (2.0 - std::f64::consts::FRAC_PI_2) * radii_sum;
3541 2.0 * perimeter.max(0.0)
3542 }
3543 _ if shape.radii.iter().any(|radius| *radius > 0.0) => {
3544 let width = f64::from(shape.rect[2]).max(0.0);
3545 let height = f64::from(shape.rect[3]).max(0.0);
3546 let max_radius = width.min(height) * 0.5;
3547 let radii_sum: f64 = shape
3548 .radii
3549 .iter()
3550 .map(|radius| f64::from(*radius).clamp(0.0, max_radius))
3551 .sum();
3552 (2.0 * (width + height) - (2.0 - std::f64::consts::FRAC_PI_2) * radii_sum).max(0.0)
3553 }
3554 _ => 0.0,
3555 }
3556}
3557
3558#[cfg(not(target_arch = "wasm32"))]
3562fn analytic_lit_area(shape: &ShapeData) -> f64 {
3563 analytic_covered_area(shape) + aa_perimeter_allowance(shape)
3564}
3565
3566#[cfg(not(target_arch = "wasm32"))]
3568fn quad_aabb(shape: &ShapeData) -> [f64; 4] {
3569 let xs = [
3570 f64::from(shape.quad01[0]),
3571 f64::from(shape.quad01[2]),
3572 f64::from(shape.quad23[0]),
3573 f64::from(shape.quad23[2]),
3574 ];
3575 let ys = [
3576 f64::from(shape.quad01[1]),
3577 f64::from(shape.quad01[3]),
3578 f64::from(shape.quad23[1]),
3579 f64::from(shape.quad23[3]),
3580 ];
3581 let fold = |values: [f64; 4], pick: fn(f64, f64) -> f64| {
3582 values.into_iter().reduce(pick).unwrap_or(0.0)
3583 };
3584 [
3585 fold(xs, f64::min),
3586 fold(ys, f64::min),
3587 fold(xs, f64::max),
3588 fold(ys, f64::max),
3589 ]
3590}
3591
3592#[cfg(not(target_arch = "wasm32"))]
3596const CORNER_FILL_STRIPS: usize = 32;
3597
3598#[cfg(not(target_arch = "wasm32"))]
3607fn area_outside_inscribed_circle(aabb: [f64; 4], viewport: (u32, u32)) -> f64 {
3608 let viewport_w = f64::from(viewport.0);
3609 let viewport_h = f64::from(viewport.1);
3610 if viewport_w <= 0.0 || viewport_h <= 0.0 {
3611 return 0.0;
3612 }
3613 let x0 = aabb[0].max(0.0);
3614 let y0 = aabb[1].max(0.0);
3615 let x1 = aabb[2].min(viewport_w);
3616 let y1 = aabb[3].min(viewport_h);
3617 if x1 <= x0 || y1 <= y0 {
3618 return 0.0;
3619 }
3620 let center_x = viewport_w * 0.5;
3621 let center_y = viewport_h * 0.5;
3622 let radius = viewport_w.min(viewport_h) * 0.5;
3623 let strip = (x1 - x0) / CORNER_FILL_STRIPS as f64;
3624 let mut outside = 0.0;
3625 for index in 0..CORNER_FILL_STRIPS {
3626 let x = x0 + (index as f64 + 0.5) * strip;
3627 let dx = x - center_x;
3628 let chord_sq = radius * radius - dx * dx;
3629 let inside = if chord_sq > 0.0 {
3630 let half_chord = chord_sq.sqrt();
3631 (y1.min(center_y + half_chord) - y0.max(center_y - half_chord)).max(0.0)
3632 } else {
3633 0.0
3634 };
3635 outside += ((y1 - y0) - inside) * strip;
3636 }
3637 outside
3638}
3639
3640#[cfg(not(target_arch = "wasm32"))]
3644#[derive(Clone, Copy, Debug)]
3645struct FillDiagShapeRecord {
3646 drawn_px2: f64,
3649 lit_px2: f64,
3651 bucket: usize,
3653 opacity: FillOpacityClass,
3654 aabb: [f64; 4],
3656}
3657
3658#[cfg(not(target_arch = "wasm32"))]
3662fn fill_diag_capture_records(
3663 shape_data: &[ShapeData],
3664 mesh: Option<(&[MeshVertex], &[u32], &[u32])>,
3665) -> Vec<FillDiagShapeRecord> {
3666 shape_data
3667 .iter()
3668 .enumerate()
3669 .map(|(index, shape)| {
3670 let drawn_px2 = match mesh {
3671 Some((vertices, indices, index_prefix)) => {
3672 let start = index_prefix[index] as usize;
3673 let end = index_prefix[index + 1] as usize;
3674 triangles_shoelace_area(vertices, &indices[start..end])
3675 }
3676 None => quad_shoelace_area(shape),
3677 };
3678 FillDiagShapeRecord {
3679 drawn_px2,
3680 lit_px2: analytic_lit_area(shape).clamp(0.0, drawn_px2),
3681 bucket: fill_diag_bucket(shape),
3682 opacity: fill_opacity_class(shape),
3683 aabb: quad_aabb(shape),
3684 }
3685 })
3686 .collect()
3687}
3688
3689#[cfg(not(target_arch = "wasm32"))]
3692#[derive(Clone, Copy, Debug)]
3693struct FillDiagSlackEntry {
3694 slot: u32,
3695 shape: u32,
3696 bucket: usize,
3697 drawn_px2: f64,
3698 lit_px2: f64,
3699}
3700
3701#[cfg(not(target_arch = "wasm32"))]
3702const FILL_DIAG_SLACK_TOP: usize = 10;
3703
3704#[cfg(not(target_arch = "wasm32"))]
3749#[derive(Default)]
3750struct FillAreaDiag {
3751 frame: [std::cell::Cell<f64>; FILL_DIAG_BUCKETS],
3755 frame_lit: [std::cell::Cell<f64>; FILL_DIAG_BUCKETS],
3757 frame_opacity: [std::cell::Cell<f64>; 3],
3759 frame_corner: std::cell::Cell<f64>,
3761 viewport: std::cell::Cell<(u32, u32)>,
3764 window: [f64; FILL_DIAG_BUCKETS],
3766 window_lit: [f64; FILL_DIAG_BUCKETS],
3767 window_opacity: [f64; 3],
3768 window_corner: f64,
3769 window_frames: u32,
3770 slack_top: Vec<FillDiagSlackEntry>,
3773 slack_dumped: bool,
3774}
3775
3776#[cfg(not(target_arch = "wasm32"))]
3777impl FillAreaDiag {
3778 const ARC: usize = 0;
3779 const RRECT_STROKE: usize = 1;
3780 const RRECT_FILL: usize = 2;
3781 const RECT: usize = 3;
3782 const MESH: usize = 4;
3783 const RETAINED: usize = 5;
3784 const IMAGE_GLYPH: usize = 6;
3785 const EFFECT_COMPOSITE: usize = 7;
3786 const OFFSCREEN_SOURCE: usize = 8;
3787
3788 fn add(&self, bucket: usize, area_px2: f64) {
3789 let cell = &self.frame[bucket];
3790 cell.set(cell.get() + area_px2);
3791 }
3792
3793 fn add_lit(&self, bucket: usize, lit_px2: f64) {
3794 let cell = &self.frame_lit[bucket];
3795 cell.set(cell.get() + lit_px2);
3796 }
3797
3798 fn add_corner(&self, px2: f64) {
3799 self.frame_corner.set(self.frame_corner.get() + px2);
3800 }
3801
3802 fn is_full_frame(&self, viewport: ViewportUniformParams) -> bool {
3806 let (width, height) = self.viewport.get();
3807 width > 0
3808 && height > 0
3809 && viewport.width == width
3810 && viewport.height == height
3811 && viewport.offset == [0.0, 0.0]
3812 }
3813
3814 fn add_shape_quads(&self, shapes: &[ShapeData], viewport: ViewportUniformParams) {
3819 let full_frame = self.is_full_frame(viewport);
3820 let frame_viewport = self.viewport.get();
3821 let mut buckets = [0.0_f64; FILL_DIAG_BUCKETS];
3822 let mut lit_buckets = [0.0_f64; FILL_DIAG_BUCKETS];
3823 let mut opacity = [0.0_f64; 3];
3824 let mut corner = 0.0_f64;
3825 for shape in shapes {
3826 let bucket = fill_diag_bucket(shape);
3827 let quad = quad_shoelace_area(shape);
3828 let lit = analytic_lit_area(shape).clamp(0.0, quad);
3829 buckets[bucket] += quad;
3830 lit_buckets[bucket] += lit;
3831 opacity[fill_opacity_class(shape) as usize] += lit;
3832 if full_frame {
3833 corner += area_outside_inscribed_circle(quad_aabb(shape), frame_viewport);
3834 }
3835 }
3836 for (bucket, area) in buckets.into_iter().enumerate() {
3837 if area > 0.0 {
3838 self.add(bucket, area);
3839 }
3840 }
3841 for (bucket, lit) in lit_buckets.into_iter().enumerate() {
3842 if lit > 0.0 {
3843 self.add_lit(bucket, lit);
3844 }
3845 }
3846 for (class, lit) in self.frame_opacity.iter().zip(opacity) {
3847 class.set(class.get() + lit);
3848 }
3849 if corner > 0.0 {
3850 self.add_corner(corner);
3851 }
3852 }
3853
3854 fn note_static_span_skip(&self, shapes: &[ShapeData]) {
3863 for shape in shapes {
3864 let bucket = fill_diag_bucket(shape);
3865 let quad = quad_shoelace_area(shape);
3866 let lit = analytic_lit_area(shape).clamp(0.0, quad);
3867 self.add(bucket, -quad);
3868 self.add_lit(bucket, -lit);
3869 let class = &self.frame_opacity[fill_opacity_class(shape) as usize];
3870 class.set(class.get() - lit);
3871 }
3872 }
3873
3874 fn note_rim_mesh(&self, shape: &ShapeData, mesh_px2: f64) {
3881 let quad = quad_shoelace_area(shape);
3882 let lit = analytic_lit_area(shape).clamp(0.0, quad);
3883 self.add(Self::RRECT_STROKE, -quad);
3884 self.add_lit(Self::RRECT_STROKE, -lit);
3885 self.add(Self::MESH, mesh_px2);
3886 self.add_lit(Self::MESH, lit.min(mesh_px2));
3887 }
3888
3889 fn add_retained_range(
3895 &self,
3896 records: &[FillDiagShapeRecord],
3897 first: u32,
3898 last: u32,
3899 transform: &SimilarityTransform,
3900 ) {
3901 let Some(range) = records.get(first as usize..last as usize) else {
3902 return;
3903 };
3904 let scale = f64::from(transform.scale);
3905 let factor = scale * scale;
3906 let identity = transform.rot == [1.0, 0.0] && transform.scale == 1.0;
3907 let frame_viewport = self.viewport.get();
3908 let mut drawn = 0.0_f64;
3909 let mut lit = 0.0_f64;
3910 let mut opacity = [0.0_f64; 3];
3911 let mut corner = 0.0_f64;
3912 for record in range {
3913 drawn += record.drawn_px2;
3914 lit += record.lit_px2;
3915 opacity[record.opacity as usize] += record.lit_px2;
3916 if identity {
3917 corner += area_outside_inscribed_circle(record.aabb, frame_viewport);
3918 }
3919 }
3920 self.add(Self::RETAINED, drawn * factor);
3921 self.add_lit(Self::RETAINED, lit * factor);
3922 for (class, value) in self.frame_opacity.iter().zip(opacity) {
3923 class.set(class.get() + value * factor);
3924 }
3925 if corner > 0.0 {
3926 self.add_corner(corner);
3927 }
3928 }
3929
3930 fn note_retained_capture(&mut self, slot: u32, records: &[FillDiagShapeRecord]) {
3934 if self.slack_dumped {
3935 return;
3936 }
3937 for (index, record) in records.iter().enumerate() {
3938 if record.drawn_px2 - record.lit_px2 <= 0.0 {
3939 continue;
3940 }
3941 self.slack_top.push(FillDiagSlackEntry {
3942 slot,
3943 shape: index as u32,
3944 bucket: record.bucket,
3945 drawn_px2: record.drawn_px2,
3946 lit_px2: record.lit_px2,
3947 });
3948 }
3949 self.slack_top
3950 .sort_by(|a, b| (b.drawn_px2 - b.lit_px2).total_cmp(&(a.drawn_px2 - a.lit_px2)));
3951 self.slack_top.truncate(FILL_DIAG_SLACK_TOP);
3952 }
3953
3954 fn add_image_quad(&self, quad: &[[f32; 2]; 4]) {
3958 let corner = |index: usize| [f64::from(quad[index][0]), f64::from(quad[index][1])];
3959 let tri = |a: [f64; 2], b: [f64; 2], c: [f64; 2]| {
3960 ((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])).abs() * 0.5
3961 };
3962 let [a, b, c, d] = [corner(0), corner(1), corner(2), corner(3)];
3963 let area = tri(a, b, c) + tri(c, b, d);
3964 self.add(Self::IMAGE_GLYPH, area);
3965 self.add_lit(Self::IMAGE_GLYPH, area);
3966 }
3967
3968 fn add_glyph_quad(&self, quad: &CachedTextGlyphQuad) {
3970 let area = quad.width as f64 * quad.height as f64;
3971 self.add(Self::IMAGE_GLYPH, area);
3972 self.add_lit(Self::IMAGE_GLYPH, area);
3973 }
3974
3975 fn add_effect_fill(&self, composite_px2: f64, offscreen_px2: f64) {
3979 if composite_px2 > 0.0 {
3980 self.add(Self::EFFECT_COMPOSITE, composite_px2);
3981 self.add_lit(Self::EFFECT_COMPOSITE, composite_px2);
3982 }
3983 if offscreen_px2 > 0.0 {
3984 self.add(Self::OFFSCREEN_SOURCE, offscreen_px2);
3985 self.add_lit(Self::OFFSCREEN_SOURCE, offscreen_px2);
3986 }
3987 }
3988
3989 fn add_offscreen_target_fill(&self, px2: f64) {
3995 if px2 > 0.0 {
3996 self.add(Self::OFFSCREEN_SOURCE, px2);
3997 self.add_lit(Self::OFFSCREEN_SOURCE, px2);
3998 }
3999 }
4000
4001 fn reset_frame(&self, width: u32, height: u32) {
4005 for cell in &self.frame {
4006 cell.set(0.0);
4007 }
4008 for cell in &self.frame_lit {
4009 cell.set(0.0);
4010 }
4011 for cell in &self.frame_opacity {
4012 cell.set(0.0);
4013 }
4014 self.frame_corner.set(0.0);
4015 self.viewport.set((width, height));
4016 }
4017
4018 fn finish_frame(&mut self, width: u32, height: u32) {
4023 for (total, cell) in self.window.iter_mut().zip(&self.frame) {
4024 *total += cell.get();
4025 }
4026 for (total, cell) in self.window_lit.iter_mut().zip(&self.frame_lit) {
4027 *total += cell.get();
4028 }
4029 for (total, cell) in self.window_opacity.iter_mut().zip(&self.frame_opacity) {
4030 *total += cell.get();
4031 }
4032 self.window_corner += self.frame_corner.get();
4033 self.window_frames += 1;
4034 if self.window_frames < FILL_DIAG_WINDOW_FRAMES {
4035 return;
4036 }
4037 let frames = f64::from(self.window_frames);
4038 let mega = |bucket: usize| self.window[bucket] / frames / 1e6;
4039 let total_mega = self.window.iter().sum::<f64>() / frames / 1e6;
4040 let screen_mega = f64::from(width) * f64::from(height) / 1e6;
4041 let overdraw = if screen_mega > 0.0 {
4042 total_mega / screen_mega
4043 } else {
4044 0.0
4045 };
4046 log::warn!(
4047 "[fill-diag] Mpx/frame: arc {:.1}, rrect-stroke {:.1}, rrect-fill {:.1}, \
4048 rect {:.1}, mesh {:.1}, retained {:.1}, img+glyph {:.1}, \
4049 effect-comp {:.1}, offscr-src {:.1}, total {:.1} \
4050 ({:.1}x overdraw of {:.3} Mpx)",
4051 mega(Self::ARC),
4052 mega(Self::RRECT_STROKE),
4053 mega(Self::RRECT_FILL),
4054 mega(Self::RECT),
4055 mega(Self::MESH),
4056 mega(Self::RETAINED),
4057 mega(Self::IMAGE_GLYPH),
4058 mega(Self::EFFECT_COMPOSITE),
4059 mega(Self::OFFSCREEN_SOURCE),
4060 total_mega,
4061 overdraw,
4062 screen_mega,
4063 );
4064 let lit = |bucket: usize| self.window_lit[bucket] / frames / 1e6;
4068 let slack = |bucket: usize| (mega(bucket) - lit(bucket)).max(0.0);
4069 let truth = |bucket: usize| format!("{:.2}|{:.2}", lit(bucket), slack(bucket));
4070 log::warn!(
4071 "[fill-truth] Mpx/frame lit|slack: arc {}, rrect-stroke {}, rrect-fill {}, \
4072 rect {}, mesh {}, retained {}, img+glyph {}, effect-comp {}, offscr-src {}; \
4073 lit alpha Mpx: opaque {:.2}, translucent {:.2}, nonsolid {:.2}; \
4074 corner-outside {:.2}",
4075 truth(Self::ARC),
4076 truth(Self::RRECT_STROKE),
4077 truth(Self::RRECT_FILL),
4078 truth(Self::RECT),
4079 truth(Self::MESH),
4080 truth(Self::RETAINED),
4081 truth(Self::IMAGE_GLYPH),
4082 truth(Self::EFFECT_COMPOSITE),
4083 truth(Self::OFFSCREEN_SOURCE),
4084 self.window_opacity[FillOpacityClass::Opaque as usize] / frames / 1e6,
4085 self.window_opacity[FillOpacityClass::Translucent as usize] / frames / 1e6,
4086 self.window_opacity[FillOpacityClass::NonSolid as usize] / frames / 1e6,
4087 self.window_corner / frames / 1e6,
4088 );
4089 if !self.slack_dumped && !self.slack_top.is_empty() {
4090 log::warn!("[fill-truth] top retained slack (once per process, capture-space px):");
4091 for (rank, entry) in self.slack_top.iter().enumerate() {
4092 log::warn!(
4093 "[fill-truth] #{} slot {} shape {} {}: quad {:.0}, lit {:.0}, \
4094 slack {:.0}",
4095 rank + 1,
4096 entry.slot,
4097 entry.shape,
4098 fill_diag_bucket_name(entry.bucket),
4099 entry.drawn_px2,
4100 entry.lit_px2,
4101 entry.drawn_px2 - entry.lit_px2,
4102 );
4103 }
4104 self.slack_dumped = true;
4105 self.slack_top = Vec::new();
4106 }
4107 self.window = [0.0; FILL_DIAG_BUCKETS];
4108 self.window_lit = [0.0; FILL_DIAG_BUCKETS];
4109 self.window_opacity = [0.0; 3];
4110 self.window_corner = 0.0;
4111 self.window_frames = 0;
4112 }
4113}
4114
4115#[cfg(not(target_arch = "wasm32"))]
4116struct ArcMeshBuild {
4117 vertices: Vec<MeshVertex>,
4118 indices: Vec<u32>,
4120 index_prefix: Vec<u32>,
4123 meshed_arcs: usize,
4124 meshed_segments: usize,
4125 passthrough: usize,
4126 quad_area: f64,
4127 mesh_area: f64,
4128}
4129
4130#[cfg(not(target_arch = "wasm32"))]
4137fn build_arc_mesh_vertices(shape_data: &[ShapeData]) -> Option<ArcMeshBuild> {
4138 let budget_bytes =
4139 (shape_data.len() * ARC_MESH_BUDGET_BYTES_PER_SHAPE).max(ARC_MESH_BUDGET_FLOOR_BYTES);
4140 let mut build = ArcMeshBuild {
4141 vertices: Vec::new(),
4142 indices: Vec::new(),
4143 index_prefix: Vec::with_capacity(shape_data.len() + 1),
4144 meshed_arcs: 0,
4145 meshed_segments: 0,
4146 passthrough: 0,
4147 quad_area: 0.0,
4148 mesh_area: 0.0,
4149 };
4150 build.index_prefix.push(0);
4151 for (index, shape) in shape_data.iter().enumerate() {
4152 let start = build.indices.len();
4153 let meshed = arc_mesh_band(shape).and_then(|band| {
4154 emit_arc_band_mesh(
4155 shape,
4156 index as u32,
4157 &band,
4158 &mut build.vertices,
4159 &mut build.indices,
4160 )
4161 });
4162 match meshed {
4163 Some(segments) => {
4164 build.meshed_arcs += 1;
4165 build.meshed_segments += segments;
4166 }
4167 None => {
4168 emit_passthrough_quad(shape, index as u32, &mut build.vertices, &mut build.indices);
4169 build.passthrough += 1;
4170 }
4171 }
4172 if arc_mesh_bytes(build.vertices.len(), build.indices.len()) > budget_bytes {
4173 return None;
4174 }
4175 build.index_prefix.push(build.indices.len() as u32);
4176 build.quad_area += quad_shoelace_area(shape);
4177 build.mesh_area += triangles_shoelace_area(&build.vertices, &build.indices[start..]);
4178 }
4179 Some(build)
4180}
4181
4182#[cfg(not(target_arch = "wasm32"))]
4185struct ReplaySlotStore {
4186 slots: std::collections::HashMap<u32, ReplaySlot, cranpose_ui_graphics::FxBuildHasher>,
4187 transform_buffer: wgpu::Buffer,
4188 free_ids: Vec<u32>,
4189 next_capture_epoch: u64,
4193}
4194
4195#[cfg(not(target_arch = "wasm32"))]
4196impl ReplaySlotStore {
4197 fn new(device: &wgpu::Device) -> Self {
4198 let transform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4199 label: Some("Replay Transform Buffer"),
4200 size: MAX_REPLAY_SLOTS as u64 * REPLAY_TRANSFORM_STRIDE,
4201 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
4202 mapped_at_creation: false,
4203 });
4204 Self {
4205 slots: std::collections::HashMap::default(),
4206 transform_buffer,
4207 free_ids: (0..MAX_REPLAY_SLOTS).rev().collect(),
4208 next_capture_epoch: 1,
4209 }
4210 }
4211}
4212
4213#[cfg(not(target_arch = "wasm32"))]
4219fn retained_bundles_enabled() -> bool {
4220 std::env::var("CRANPOSE_RETAINED_BUNDLES").as_deref() != Ok("0")
4221}
4222
4223#[cfg(not(target_arch = "wasm32"))]
4231fn instanced_quads_enabled() -> bool {
4232 std::env::var("CRANPOSE_INSTANCED_QUADS").as_deref() != Ok("0")
4233}
4234
4235#[cfg(not(target_arch = "wasm32"))]
4239const INSTANCED_QUAD_INDICES: [u16; 6] = [0, 1, 2, 2, 1, 3];
4240
4241#[cfg(not(target_arch = "wasm32"))]
4247struct InstancedQuadPipelines {
4248 pipeline: LazyGpuResource<wgpu::RenderPipeline>,
4249 pipeline_dst_out: LazyGpuResource<wgpu::RenderPipeline>,
4250 pipeline_solid: LazyGpuResource<wgpu::RenderPipeline>,
4254 index_buffer: wgpu::Buffer,
4257}
4258
4259#[cfg(not(target_arch = "wasm32"))]
4267#[derive(Clone, Debug, PartialEq, Eq, Hash)]
4268struct RetainedBundleOpKey {
4269 slot: u32,
4270 capture_epoch: Option<u64>,
4275 first: u32,
4276 last: u32,
4277 retained_index: u32,
4278 has_mesh: bool,
4279}
4280
4281#[cfg(not(target_arch = "wasm32"))]
4285#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
4286struct RetainedBundleKey {
4287 ops: Vec<RetainedBundleOpKey>,
4288}
4289
4290#[cfg(not(target_arch = "wasm32"))]
4291struct RetainedBundleCacheEntry<B> {
4292 bundle: B,
4293 last_used_frame: u64,
4294}
4295
4296#[cfg(not(target_arch = "wasm32"))]
4305struct RetainedBundleCacheImpl<B> {
4306 entries: HashMap<RetainedBundleKey, RetainedBundleCacheEntry<B>>,
4307 frame: u64,
4308 rebuilds: u64,
4309 cached_executes: u64,
4310 window_rebuilds: u64,
4311 window_executes: u64,
4312}
4313
4314#[cfg(not(target_arch = "wasm32"))]
4315type RetainedBundleCache = RetainedBundleCacheImpl<wgpu::RenderBundle>;
4316
4317#[cfg(not(target_arch = "wasm32"))]
4318impl<B> RetainedBundleCacheImpl<B> {
4319 fn new() -> Self {
4320 Self {
4321 entries: HashMap::default(),
4322 frame: 0,
4323 rebuilds: 0,
4324 cached_executes: 0,
4325 window_rebuilds: 0,
4326 window_executes: 0,
4327 }
4328 }
4329
4330 fn hit(&mut self, key: &RetainedBundleKey) -> bool {
4333 let frame = self.frame;
4334 match self.entries.get_mut(key) {
4335 Some(entry) => {
4336 entry.last_used_frame = frame;
4337 self.cached_executes += 1;
4338 self.window_executes += 1;
4339 true
4340 }
4341 None => false,
4342 }
4343 }
4344
4345 fn insert(&mut self, key: RetainedBundleKey, bundle: B) {
4347 self.rebuilds += 1;
4348 self.window_rebuilds += 1;
4349 self.entries.insert(
4350 key,
4351 RetainedBundleCacheEntry {
4352 bundle,
4353 last_used_frame: self.frame,
4354 },
4355 );
4356 }
4357
4358 fn get(&self, key: &RetainedBundleKey) -> Option<&B> {
4359 self.entries.get(key).map(|entry| &entry.bundle)
4360 }
4361
4362 fn clear(&mut self) {
4367 self.entries.clear();
4368 }
4369
4370 fn end_frame(&mut self) {
4374 let frame = self.frame;
4375 self.entries
4376 .retain(|_, entry| entry.last_used_frame >= frame);
4377 self.frame = self.frame.wrapping_add(1);
4378 let due = self.frame.is_multiple_of(1024)
4383 || (cranpose_core::env_flag!("CRANPOSE_COMMAND_REPLAY_DIAG")
4384 && self.frame.is_multiple_of(120));
4385 if due && self.window_rebuilds + self.window_executes > 0 {
4386 log::warn!(
4387 "[retained-bundles] {} stretches, {} rebuilds, {} cached executes ({} live bundles)",
4388 self.window_rebuilds + self.window_executes,
4389 self.window_rebuilds,
4390 self.window_executes,
4391 self.entries.len(),
4392 );
4393 self.window_rebuilds = 0;
4394 self.window_executes = 0;
4395 }
4396 }
4397
4398 fn stats(&self) -> (u64, u64) {
4400 (self.rebuilds, self.cached_executes)
4401 }
4402}
4403
4404struct CachedImageTexture {
4405 _texture: wgpu::Texture,
4406 _view: wgpu::TextureView,
4407 nearest_bind_group: wgpu::BindGroup,
4408 linear_bind_group: wgpu::BindGroup,
4409 bytes: usize,
4416}
4417
4418impl CachedImageTexture {
4419 fn bind_group(&self, sampling: ImageSampling) -> &wgpu::BindGroup {
4420 match sampling {
4421 ImageSampling::Nearest => &self.nearest_bind_group,
4422 ImageSampling::Linear => &self.linear_bind_group,
4423 }
4424 }
4425}
4426
4427#[derive(Clone, Copy)]
4428struct GlyphAtlasEntry {
4429 x: u32,
4430 y: u32,
4431 width: u32,
4432 height: u32,
4433}
4434
4435fn next_glyph_atlas_size(current: u32, max: u32) -> u32 {
4442 current.saturating_mul(2).clamp(1, max.max(1))
4443}
4444
4445struct TextGlyphAtlas {
4446 texture: wgpu::Texture,
4447 _view: wgpu::TextureView,
4448 bind_group: wgpu::BindGroup,
4449 entries: BoundedLruCache<SoftwareGlyphAtlasKey, GlyphAtlasEntry>,
4450 generation: u64,
4451 size: u32,
4455 max_size: u32,
4461 cursor_x: u32,
4462 cursor_y: u32,
4463 row_height: u32,
4464 upload_scratch: Vec<u8>,
4465}
4466
4467impl TextGlyphAtlas {
4468 fn new(
4469 device: &wgpu::Device,
4470 image_layout: &wgpu::BindGroupLayout,
4471 sampler: &wgpu::Sampler,
4472 size: u32,
4473 ) -> Self {
4474 let max_size = TEXT_GLYPH_ATLAS_MAX_SIZE.min(device.limits().max_texture_dimension_2d);
4475 let size = size.clamp(TEXT_GLYPH_ATLAS_MIN_SIZE.min(max_size), max_size);
4476 let texture = Self::create_texture(device, size);
4477 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
4478 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
4479 label: Some("Text Glyph Atlas Bind Group"),
4480 layout: image_layout,
4481 entries: &[
4482 wgpu::BindGroupEntry {
4483 binding: 0,
4484 resource: wgpu::BindingResource::TextureView(&view),
4485 },
4486 wgpu::BindGroupEntry {
4487 binding: 1,
4488 resource: wgpu::BindingResource::Sampler(sampler),
4489 },
4490 ],
4491 });
4492 Self {
4493 texture,
4494 _view: view,
4495 bind_group,
4496 entries: BoundedLruCache::with_capacity_at_least_one(MAX_TEXT_GLYPH_ATLAS_ITEMS),
4497 generation: 0,
4498 size,
4499 max_size,
4500 cursor_x: TEXT_GLYPH_ATLAS_PADDING,
4501 cursor_y: TEXT_GLYPH_ATLAS_PADDING,
4502 row_height: 0,
4503 upload_scratch: Vec::new(),
4504 }
4505 }
4506
4507 fn create_texture(device: &wgpu::Device, size: u32) -> wgpu::Texture {
4508 device.create_texture(&wgpu::TextureDescriptor {
4509 label: Some("Text Glyph Atlas Texture"),
4510 size: wgpu::Extent3d {
4511 width: size,
4512 height: size,
4513 depth_or_array_layers: 1,
4514 },
4515 mip_level_count: 1,
4516 sample_count: 1,
4517 dimension: wgpu::TextureDimension::D2,
4518 format: wgpu::TextureFormat::R8Unorm,
4519 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
4520 view_formats: &[],
4521 })
4522 }
4523
4524 fn reset(
4540 &mut self,
4541 device: &wgpu::Device,
4542 image_layout: &wgpu::BindGroupLayout,
4543 sampler: &wgpu::Sampler,
4544 ) {
4545 let generation = self.generation.wrapping_add(1);
4546 let grown = next_glyph_atlas_size(self.size, self.max_size);
4547 let mut next = Self::new(device, image_layout, sampler, grown);
4548 next.generation = generation;
4549 *self = next;
4550 }
4551
4552 fn generation(&self) -> u64 {
4553 self.generation
4554 }
4555
4556 fn size(&self) -> u32 {
4557 self.size
4558 }
4559
4560 fn entry(&mut self, key: &SoftwareGlyphAtlasKey) -> Option<GlyphAtlasEntry> {
4561 self.entries.get(key).copied()
4562 }
4563
4564 fn allocate(&mut self, width: u32, height: u32) -> Option<GlyphAtlasEntry> {
4565 if width == 0
4566 || height == 0
4567 || width + TEXT_GLYPH_ATLAS_PADDING * 2 > self.size
4568 || height + TEXT_GLYPH_ATLAS_PADDING * 2 > self.size
4569 {
4570 return None;
4571 }
4572
4573 if self.cursor_x + width + TEXT_GLYPH_ATLAS_PADDING > self.size {
4574 self.cursor_x = TEXT_GLYPH_ATLAS_PADDING;
4575 self.cursor_y = self
4576 .cursor_y
4577 .saturating_add(self.row_height)
4578 .saturating_add(TEXT_GLYPH_ATLAS_PADDING);
4579 self.row_height = 0;
4580 }
4581 if self.cursor_y + height + TEXT_GLYPH_ATLAS_PADDING > self.size {
4582 return None;
4583 }
4584
4585 let entry = GlyphAtlasEntry {
4586 x: self.cursor_x,
4587 y: self.cursor_y,
4588 width,
4589 height,
4590 };
4591 self.cursor_x = self
4592 .cursor_x
4593 .saturating_add(width)
4594 .saturating_add(TEXT_GLYPH_ATLAS_PADDING);
4595 self.row_height = self.row_height.max(height);
4596 Some(entry)
4597 }
4598
4599 fn upload_glyph(
4600 &mut self,
4601 key: SoftwareGlyphAtlasKey,
4602 glyph: &SoftwareGlyphAtlasGlyph,
4603 queue: &wgpu::Queue,
4604 executor: &mut WgpuFrameGraphExecutor,
4605 frame_stats: &mut gpu_stats::FrameStats,
4606 ) -> Option<GlyphAtlasEntry> {
4607 if let Some(entry) = self.entry(&key) {
4608 frame_stats.record_text_glyph_atlas_hit();
4609 return Some(entry);
4610 }
4611
4612 let width = u32::try_from(glyph.mask.width).ok()?;
4613 let height = u32::try_from(glyph.mask.height).ok()?;
4614 let entry = self.allocate(width, height)?;
4615 self.upload_scratch.clear();
4616 self.upload_scratch.reserve(
4617 glyph
4618 .mask
4619 .alpha
4620 .len()
4621 .saturating_sub(self.upload_scratch.capacity()),
4622 );
4623 self.upload_scratch.extend(
4624 glyph
4625 .mask
4626 .alpha
4627 .iter()
4628 .map(|alpha| (alpha.clamp(0.0, 1.0) * 255.0).round() as u8),
4629 );
4630
4631 let upload_stats = executor.upload_texture(
4632 queue,
4633 wgpu::TexelCopyTextureInfo {
4634 texture: &self.texture,
4635 mip_level: 0,
4636 origin: wgpu::Origin3d {
4637 x: entry.x,
4638 y: entry.y,
4639 z: 0,
4640 },
4641 aspect: wgpu::TextureAspect::All,
4642 },
4643 &self.upload_scratch,
4644 wgpu::TexelCopyBufferLayout {
4645 offset: 0,
4646 bytes_per_row: Some(entry.width),
4647 rows_per_image: Some(entry.height),
4648 },
4649 wgpu::Extent3d {
4650 width: entry.width,
4651 height: entry.height,
4652 depth_or_array_layers: 1,
4653 },
4654 );
4655 frame_stats.record_command_stats(upload_stats);
4656 frame_stats.record_text_glyph_atlas_miss(entry.width, entry.height);
4657 self.entries.put(key, entry);
4658 Some(entry)
4659 }
4660}
4661
4662struct ImageDrawCmd {
4663 index_start: u32,
4664 scissor: (u32, u32, u32, u32),
4665 image_id: u64,
4666 sampling: ImageSampling,
4667}
4668
4669#[derive(Clone, Copy)]
4670enum GlyphDrawSource {
4671 Shared {
4672 index_start: u32,
4673 index_count: u32,
4674 },
4675 #[cfg(not(target_arch = "wasm32"))]
4676 Retained {
4677 cache_key: TextGlyphRunCacheKey,
4678 uniform_slot: usize,
4679 },
4680}
4681
4682#[derive(Clone, Copy)]
4683struct GlyphDrawCmd {
4684 source: GlyphDrawSource,
4685 scissor: (u32, u32, u32, u32),
4686}
4687
4688impl GlyphDrawCmd {
4689 fn shared(index_start: u32, index_count: u32, scissor: (u32, u32, u32, u32)) -> Self {
4690 Self {
4691 source: GlyphDrawSource::Shared {
4692 index_start,
4693 index_count,
4694 },
4695 scissor,
4696 }
4697 }
4698
4699 #[cfg(not(target_arch = "wasm32"))]
4700 fn retained(
4701 cache_key: TextGlyphRunCacheKey,
4702 uniform_slot: usize,
4703 scissor: (u32, u32, u32, u32),
4704 ) -> Self {
4705 Self {
4706 source: GlyphDrawSource::Retained {
4707 cache_key,
4708 uniform_slot,
4709 },
4710 scissor,
4711 }
4712 }
4713}
4714
4715#[derive(Clone, Copy, Debug, PartialEq)]
4716struct ImageUvRect {
4717 min: [f32; 2],
4718 max: [f32; 2],
4719 sample_bounds: [f32; 4],
4720}
4721
4722struct ShapeBatchBuffers {
4729 shape_buffer: wgpu::Buffer,
4730 gradient_buffer: wgpu::Buffer,
4731 bind_group: wgpu::BindGroup,
4732 shape_capacity: usize,
4733 gradient_capacity: usize,
4734 batch_limits: ShapeBatchLimits,
4735}
4736
4737#[cfg(target_arch = "wasm32")]
4738struct UniformBatchBuffer {
4739 buffer: wgpu::Buffer,
4740 bind_group: wgpu::BindGroup,
4741}
4742
4743#[cfg(target_arch = "wasm32")]
4744struct ImageBatchBuffers {
4745 vertex_buffer: wgpu::Buffer,
4746 index_buffer: wgpu::Buffer,
4747 vertex_capacity: usize,
4748 index_capacity: usize,
4749}
4750
4751#[derive(Clone, Copy, Debug, PartialEq)]
4752struct ViewportUniformParams {
4753 width: u32,
4754 height: u32,
4755 offset: [f32; 2],
4756}
4757
4758#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4759#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
4760enum UploadTarget {
4761 Uniform,
4762 ShapeData,
4763 ShapeGradient,
4764 ImageVertex,
4765 ImageIndex,
4766 #[cfg(not(target_arch = "wasm32"))]
4767 RetainedGlyphUniform,
4768 #[cfg(not(target_arch = "wasm32"))]
4771 ReplayTransform,
4772 #[cfg(not(target_arch = "wasm32"))]
4774 ReplayPaintData(u32),
4775}
4776
4777#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4778#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
4779struct PendingBufferCopy {
4780 source_offset: u64,
4781 target_offset: u64,
4782 size: u64,
4783 target: UploadTarget,
4784}
4785
4786#[derive(Default)]
4787struct StagedBufferUploads {
4788 bytes: Vec<u8>,
4789 copies: Vec<PendingBufferCopy>,
4790}
4791
4792impl StagedBufferUploads {
4793 fn clear(&mut self) {
4794 self.bytes.clear();
4795 self.copies.clear();
4796 }
4797
4798 fn shrink_retained_capacity(&mut self, max_bytes: usize, max_copies: usize) -> bool {
4799 let mut shrunk = false;
4800 if self.bytes.len() <= max_bytes && self.bytes.capacity() > max_bytes {
4801 self.bytes.shrink_to(max_bytes);
4802 shrunk = true;
4803 }
4804 if self.copies.len() <= max_copies && self.copies.capacity() > max_copies {
4805 self.copies.shrink_to(max_copies);
4806 shrunk = true;
4807 }
4808 shrunk
4809 }
4810
4811 fn is_empty(&self) -> bool {
4812 self.copies.is_empty()
4813 }
4814
4815 #[cfg(test)]
4816 fn payload_for_copy(&self, copy: PendingBufferCopy) -> &[u8] {
4817 let start = copy.source_offset as usize;
4818 let end = start + copy.size as usize;
4819 &self.bytes[start..end]
4820 }
4821
4822 #[cfg(not(target_arch = "wasm32"))]
4823 fn stage(&mut self, target: UploadTarget, bytes: &[u8]) {
4824 self.stage_at(target, 0, bytes);
4825 }
4826
4827 #[cfg(not(target_arch = "wasm32"))]
4832 fn record_upload_copy(
4833 &mut self,
4834 target: UploadTarget,
4835 source_offset: u64,
4836 target_offset: u64,
4837 size: u64,
4838 ) {
4839 if size == 0 {
4840 return;
4841 }
4842 self.copies.push(PendingBufferCopy {
4843 source_offset,
4844 target_offset,
4845 size,
4846 target,
4847 });
4848 }
4849
4850 #[cfg(not(target_arch = "wasm32"))]
4851 fn stage_at(&mut self, target: UploadTarget, target_offset: u64, bytes: &[u8]) {
4852 if bytes.is_empty() {
4853 return;
4854 }
4855
4856 debug_assert_eq!(
4857 bytes.len() % wgpu::COPY_BUFFER_ALIGNMENT as usize,
4858 0,
4859 "buffer uploads must be aligned to copy requirements"
4860 );
4861
4862 let aligned_offset = align_usize_to(self.bytes.len(), wgpu::COPY_BUFFER_ALIGNMENT as usize);
4863 if aligned_offset > self.bytes.len() {
4864 self.bytes.resize(aligned_offset, 0);
4865 }
4866
4867 let source_offset = self.bytes.len() as u64;
4868 self.bytes.extend_from_slice(bytes);
4869 self.copies.push(PendingBufferCopy {
4870 source_offset,
4871 target_offset,
4872 size: bytes.len() as u64,
4873 target,
4874 });
4875 }
4876
4877 fn truncate(&mut self, bytes_len: usize, copies_len: usize) {
4878 self.bytes.truncate(bytes_len);
4879 self.copies.truncate(copies_len);
4880 }
4881}
4882
4883fn shape_batch_bind_group_entries<'a>(
4888 shape_buffer: &'a wgpu::Buffer,
4889 gradient_buffer: &'a wgpu::Buffer,
4890 similarity_buffer: &'a wgpu::Buffer,
4891 paint_buffer: Option<&'a wgpu::Buffer>,
4892) -> Vec<wgpu::BindGroupEntry<'a>> {
4893 let mut entries = vec![
4894 wgpu::BindGroupEntry {
4895 binding: 0,
4896 resource: shape_buffer.as_entire_binding(),
4897 },
4898 wgpu::BindGroupEntry {
4899 binding: 1,
4900 resource: gradient_buffer.as_entire_binding(),
4901 },
4902 wgpu::BindGroupEntry {
4903 binding: 2,
4904 resource: similarity_buffer.as_entire_binding(),
4905 },
4906 ];
4907 if let Some(paint_buffer) = paint_buffer {
4908 entries.push(wgpu::BindGroupEntry {
4909 binding: 3,
4910 resource: paint_buffer.as_entire_binding(),
4911 });
4912 }
4913 entries
4914}
4915
4916impl ShapeBatchBuffers {
4917 fn new(
4918 device: &wgpu::Device,
4919 bind_group_layout: &wgpu::BindGroupLayout,
4920 similarity_buffer: &wgpu::Buffer,
4921 paint_buffer: Option<&wgpu::Buffer>,
4922 batch_limits: ShapeBatchLimits,
4923 ) -> Self {
4924 debug_assert_eq!(
4925 paint_buffer.is_some(),
4926 batch_limits.storage,
4927 "the paint binding exists exactly when the layout is in storage mode"
4928 );
4929 let initial_shape_cap = batch_limits.initial_shape_capacity();
4930 let initial_gradient_cap = batch_limits.initial_gradient_capacity();
4931
4932 let shape_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4933 label: Some("Shape Data Buffer"),
4934 size: (std::mem::size_of::<ShapeData>() * initial_shape_cap) as u64,
4935 usage: batch_limits.data_buffer_usage(),
4936 mapped_at_creation: false,
4937 });
4938
4939 let gradient_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4940 label: Some("Gradient Buffer"),
4941 size: (std::mem::size_of::<GradientStop>() * initial_gradient_cap) as u64,
4942 usage: batch_limits.data_buffer_usage(),
4943 mapped_at_creation: false,
4944 });
4945
4946 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
4947 label: Some("Shape Bind Group"),
4948 layout: bind_group_layout,
4949 entries: &shape_batch_bind_group_entries(
4950 &shape_buffer,
4951 &gradient_buffer,
4952 similarity_buffer,
4953 paint_buffer,
4954 ),
4955 });
4956
4957 Self {
4958 shape_buffer,
4959 gradient_buffer,
4960 bind_group,
4961 shape_capacity: initial_shape_cap,
4962 gradient_capacity: initial_gradient_cap,
4963 batch_limits,
4964 }
4965 }
4966
4967 fn ensure_capacity(
4970 &mut self,
4971 device: &wgpu::Device,
4972 bind_group_layout: &wgpu::BindGroupLayout,
4973 similarity_buffer: &wgpu::Buffer,
4974 paint_buffer: Option<&wgpu::Buffer>,
4975 shapes_needed: usize,
4976 gradients_needed: usize,
4977 ) {
4978 let mut need_bind_group_update = false;
4979
4980 if shapes_needed > self.shape_capacity
4984 && self.shape_capacity < self.batch_limits.max_shapes_per_batch
4985 {
4986 let new_cap = shapes_needed
4987 .next_power_of_two()
4988 .min(self.batch_limits.max_shapes_per_batch);
4989 self.shape_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4990 label: Some("Shape Data Buffer"),
4991 size: (std::mem::size_of::<ShapeData>() * new_cap) as u64,
4992 usage: self.batch_limits.data_buffer_usage(),
4993 mapped_at_creation: false,
4994 });
4995 self.shape_capacity = new_cap;
4996 need_bind_group_update = true;
4997 }
4998
4999 if gradients_needed > self.gradient_capacity
5000 && self.gradient_capacity < self.batch_limits.max_gradient_stops
5001 {
5002 let new_cap = gradients_needed
5003 .max(1)
5004 .next_power_of_two()
5005 .min(self.batch_limits.max_gradient_stops);
5006 self.gradient_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5007 label: Some("Gradient Buffer"),
5008 size: (std::mem::size_of::<GradientStop>() * new_cap) as u64,
5009 usage: self.batch_limits.data_buffer_usage(),
5010 mapped_at_creation: false,
5011 });
5012 self.gradient_capacity = new_cap;
5013 need_bind_group_update = true;
5014 }
5015
5016 if need_bind_group_update {
5017 self.bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
5018 label: Some("Shape Bind Group"),
5019 layout: bind_group_layout,
5020 entries: &shape_batch_bind_group_entries(
5021 &self.shape_buffer,
5022 &self.gradient_buffer,
5023 similarity_buffer,
5024 paint_buffer,
5025 ),
5026 });
5027 }
5028 }
5029}
5030
5031#[cfg(target_arch = "wasm32")]
5032impl UniformBatchBuffer {
5033 fn new(device: &wgpu::Device, bind_group_layout: &wgpu::BindGroupLayout) -> Self {
5034 let buffer = device.create_buffer(&wgpu::BufferDescriptor {
5035 label: Some("Viewport Uniform Batch Buffer"),
5036 size: std::mem::size_of::<Uniforms>() as u64,
5037 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
5038 mapped_at_creation: false,
5039 });
5040 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
5041 label: Some("Viewport Uniform Batch Bind Group"),
5042 layout: bind_group_layout,
5043 entries: &[wgpu::BindGroupEntry {
5044 binding: 0,
5045 resource: buffer.as_entire_binding(),
5046 }],
5047 });
5048 Self { buffer, bind_group }
5049 }
5050}
5051
5052#[cfg(target_arch = "wasm32")]
5053impl ImageBatchBuffers {
5054 fn new(device: &wgpu::Device) -> Self {
5055 let vertex_capacity = 4;
5056 let index_capacity = 6;
5057 let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5058 label: Some("Image Vertex Batch Buffer"),
5059 size: (std::mem::size_of::<Vertex>() * vertex_capacity) as u64,
5060 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
5061 mapped_at_creation: false,
5062 });
5063 let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5064 label: Some("Image Index Batch Buffer"),
5065 size: (std::mem::size_of::<u32>() * index_capacity) as u64,
5066 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
5067 mapped_at_creation: false,
5068 });
5069 Self {
5070 vertex_buffer,
5071 index_buffer,
5072 vertex_capacity,
5073 index_capacity,
5074 }
5075 }
5076
5077 fn ensure_capacity(
5078 &mut self,
5079 device: &wgpu::Device,
5080 vertices_needed: usize,
5081 indices_needed: usize,
5082 ) {
5083 let hard_max_bytes = HARD_MAX_BUFFER_MB * 1024 * 1024;
5084 if vertices_needed > self.vertex_capacity {
5085 let desired = vertices_needed.next_power_of_two();
5086 let max_count = hard_max_bytes / std::mem::size_of::<Vertex>();
5087 let new_cap = desired.min(max_count);
5088 self.vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5089 label: Some("Image Vertex Batch Buffer"),
5090 size: (std::mem::size_of::<Vertex>() * new_cap) as u64,
5091 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
5092 mapped_at_creation: false,
5093 });
5094 self.vertex_capacity = new_cap;
5095 }
5096 if indices_needed > self.index_capacity {
5097 let desired = indices_needed.next_power_of_two();
5098 let max_count = hard_max_bytes / std::mem::size_of::<u32>();
5099 let new_cap = desired.min(max_count);
5100 self.index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5101 label: Some("Image Index Batch Buffer"),
5102 size: (std::mem::size_of::<u32>() * new_cap) as u64,
5103 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
5104 mapped_at_creation: false,
5105 });
5106 self.index_capacity = new_cap;
5107 }
5108 }
5109}
5110
5111pub struct GpuRenderer {
5114 pub(crate) device: Arc<wgpu::Device>,
5115 pub(crate) queue: Arc<wgpu::Queue>,
5116 renderer_epoch: u64,
5121 #[cfg(not(target_arch = "wasm32"))]
5127 store_feed_generation: u64,
5128 surface_format: wgpu::TextureFormat,
5129 adapter_backend: wgpu::Backend,
5130 shape_batch_limits: ShapeBatchLimits,
5131 pipeline: LazyGpuResource<wgpu::RenderPipeline>,
5132 pipeline_dst_out: LazyGpuResource<wgpu::RenderPipeline>,
5133 pipeline_solid: LazyGpuResource<wgpu::RenderPipeline>,
5135 #[cfg(not(target_arch = "wasm32"))]
5139 mesh_pipeline: LazyGpuResource<wgpu::RenderPipeline>,
5140 #[cfg(not(target_arch = "wasm32"))]
5146 instanced_quads: Option<InstancedQuadPipelines>,
5147 uniform_bind_group_layout: wgpu::BindGroupLayout,
5148 shape_bind_group_layout: wgpu::BindGroupLayout,
5149 dummy_paint_buffer: Option<wgpu::Buffer>,
5152 identity_similarity_buffer: wgpu::Buffer,
5155 #[cfg(not(target_arch = "wasm32"))]
5156 replay_slots: ReplaySlotStore,
5157 image_pipeline: LazyGpuResource<wgpu::RenderPipeline>,
5158 image_pipeline_dst_out: LazyGpuResource<wgpu::RenderPipeline>,
5159 glyph_atlas_pipeline: LazyGpuResource<wgpu::RenderPipeline>,
5160 #[cfg(not(target_arch = "wasm32"))]
5161 retained_glyph_atlas_pipeline: LazyGpuResource<wgpu::RenderPipeline>,
5162 image_bind_group_layout: wgpu::BindGroupLayout,
5163 #[cfg(not(target_arch = "wasm32"))]
5164 retained_glyph_uniform_bind_group_layout: wgpu::BindGroupLayout,
5165 image_nearest_sampler: wgpu::Sampler,
5166 image_linear_sampler: wgpu::Sampler,
5167 text_fonts: SoftwareTextFontSet,
5168 #[cfg(not(target_arch = "wasm32"))]
5170 upload_buffer: wgpu::Buffer,
5171 #[cfg(not(target_arch = "wasm32"))]
5172 uniform_buffer: wgpu::Buffer,
5173 #[cfg(not(target_arch = "wasm32"))]
5174 uniform_bind_group: wgpu::BindGroup,
5175 #[cfg(not(target_arch = "wasm32"))]
5176 shape_buffers: ShapeBatchBuffers,
5177 #[cfg(not(target_arch = "wasm32"))]
5178 image_vertex_buffer: wgpu::Buffer,
5179 #[cfg(not(target_arch = "wasm32"))]
5180 image_index_buffer: wgpu::Buffer,
5181 #[cfg(not(target_arch = "wasm32"))]
5182 retained_glyph_uniform_buffer: wgpu::Buffer,
5183 #[cfg(not(target_arch = "wasm32"))]
5184 retained_glyph_uniform_bind_group: wgpu::BindGroup,
5185 #[cfg(not(target_arch = "wasm32"))]
5186 retained_glyph_uniform_stride: u64,
5187 #[cfg(not(target_arch = "wasm32"))]
5188 retained_glyph_uniform_capacity: usize,
5189 #[cfg(not(target_arch = "wasm32"))]
5190 retained_glyph_uniform_cursor: usize,
5191 #[cfg(target_arch = "wasm32")]
5192 wasm_uniform_batches: Vec<UniformBatchBuffer>,
5193 #[cfg(target_arch = "wasm32")]
5194 wasm_uniform_batch_cursor: usize,
5195 #[cfg(target_arch = "wasm32")]
5196 wasm_shape_batches: Vec<ShapeBatchBuffers>,
5197 #[cfg(target_arch = "wasm32")]
5198 wasm_shape_batch_cursor: usize,
5199 #[cfg(target_arch = "wasm32")]
5200 wasm_image_batches: Vec<ImageBatchBuffers>,
5201 #[cfg(target_arch = "wasm32")]
5202 wasm_image_batch_cursor: usize,
5203 image_texture_cache: BoundedLruCache<u64, CachedImageTexture>,
5204 image_texture_cache_bytes: usize,
5206 text_image_cache: BoundedLruCache<TextImageCacheKey, CachedTextImage>,
5207 text_glyph_atlas: TextGlyphAtlas,
5208 text_glyph_run_cache: BoundedLruCache<TextGlyphRunCacheKey, CachedTextGlyphRun>,
5209 #[cfg(not(target_arch = "wasm32"))]
5210 text_glyph_gpu_run_cache: BoundedLruCache<TextGlyphRunCacheKey, CachedGpuTextGlyphRun>,
5211 text_glyph_mask_cache: SoftwareGlyphRasterCache,
5212 text_line_index_cache: TextLineIndexCache,
5213 scratch_shape_data: Vec<ShapeData>,
5214 scratch_gradients: Vec<GradientStop>,
5215 scratch_image_vertices: Vec<Vertex>,
5216 scratch_image_indices: Vec<u32>,
5217 scratch_image_cmds: Vec<ImageDrawCmd>,
5218 scratch_glyph_cmds: Vec<GlyphDrawCmd>,
5219 scratch_text_glyph_run: Vec<SoftwareGlyphAtlasRunGlyph>,
5220 scratch_text_glyph_placements: Vec<SoftwareGlyphAtlasPlacement>,
5221 scratch_text_glyph_quads: Vec<CachedTextGlyphQuad>,
5222 scratch_segment_items: Vec<(usize, SegmentDrawItem)>,
5223 scratch_effect_ranges: Vec<Range<usize>>,
5224 scratch_layer_events: Vec<LayerEvent>,
5225 staged_uploads: StagedBufferUploads,
5226 frame_graph_executor: WgpuFrameGraphExecutor,
5227 deferred_offscreen_releases: Vec<OffscreenTarget>,
5228 effect_renderer: EffectRenderer,
5229 layer_surface_cache: LayerSurfaceCache,
5230 observed_scene_range_cache_misses: BoundedLruCache<LayerRasterCacheKey, ()>,
5231 shadow_surface_cache: BoundedLruCache<ShadowSurfaceCacheKey, CachedShadowSurface>,
5232 shadow_surface_cache_bytes: u64,
5233 frame_stats: gpu_stats::FrameStats,
5234 last_frame_stats: Option<gpu_stats::FrameStatsSnapshot>,
5235 pending_frame_warmup_frames: u8,
5236 frame_count: u64,
5237 gpu_stats_enabled: bool,
5238 warning_state: RendererWarningState,
5239 #[cfg(not(target_arch = "wasm32"))]
5240 replay_upload_stats: ReplayUploadStats,
5241 #[cfg(not(target_arch = "wasm32"))]
5248 replay_color_patches: Vec<crate::scene::ColorPatch>,
5249 #[cfg(not(target_arch = "wasm32"))]
5253 color_patch_scratch: Vec<crate::scene::ColorPatch>,
5254 #[cfg(not(target_arch = "wasm32"))]
5259 replay_ack_confirmations: Vec<crate::frame_packet::ReplayConfirmation>,
5260 #[cfg(not(target_arch = "wasm32"))]
5265 replay_generation_drops: u64,
5266 #[cfg(not(target_arch = "wasm32"))]
5269 retained_bundle_cache: RetainedBundleCache,
5270 #[cfg(not(target_arch = "wasm32"))]
5275 rim_mesh_vertices: Vec<MeshVertex>,
5276 #[cfg(not(target_arch = "wasm32"))]
5277 rim_mesh_indices: Vec<u32>,
5278 #[cfg(not(target_arch = "wasm32"))]
5284 rim_mesh_vertex_buffer: Option<wgpu::Buffer>,
5285 #[cfg(not(target_arch = "wasm32"))]
5286 rim_mesh_index_buffer: Option<wgpu::Buffer>,
5287 #[cfg(not(target_arch = "wasm32"))]
5290 rim_mesh_uploaded_vertices: usize,
5291 #[cfg(not(target_arch = "wasm32"))]
5292 rim_mesh_uploaded_indices: usize,
5293 #[cfg(not(target_arch = "wasm32"))]
5296 rim_meshes_emitted: u64,
5297 #[cfg(not(target_arch = "wasm32"))]
5300 fill_area_diag: FillAreaDiag,
5301 #[cfg(not(target_arch = "wasm32"))]
5305 static_span: StaticSpanCache,
5306}
5307
5308#[cfg(not(target_arch = "wasm32"))]
5314#[derive(Default)]
5315struct ReplayUploadStats {
5316 calls: u64,
5317 patched_calls: u64,
5318 patches: u64,
5319 slots: u64,
5320 records: u64,
5321 bytes: u64,
5322 ideal_bytes: u64,
5323 max_frame_bytes: u64,
5324}
5325
5326#[cfg(not(target_arch = "wasm32"))]
5327impl ReplayUploadStats {
5328 const REPORT_CALLS: u64 = 1024;
5338
5339 fn note_frame(&mut self, patches: u64, slots: u64, records: u64, bytes: u64, ideal: u64) {
5340 self.calls += 1;
5341 if patches > 0 {
5342 self.patched_calls += 1;
5343 self.patches += patches;
5344 self.slots += slots;
5345 self.records += records;
5346 self.bytes += bytes;
5347 self.ideal_bytes += ideal;
5348 self.max_frame_bytes = self.max_frame_bytes.max(bytes);
5349 }
5350 if self.calls >= Self::REPORT_CALLS {
5351 let patched = self.patched_calls.max(1);
5352 log::warn!(
5353 "[replay-upload] {} patched of {} drains: avg {:.1} KB/frame (max {:.1} KB), \
5354 color-only would be {:.1} KB/frame; avg {} patches over {} records in {} slots",
5355 self.patched_calls,
5356 self.calls,
5357 self.bytes as f64 / patched as f64 / 1024.0,
5358 self.max_frame_bytes as f64 / 1024.0,
5359 self.ideal_bytes as f64 / patched as f64 / 1024.0,
5360 self.patches / patched,
5361 self.records / patched,
5362 self.slots / patched,
5363 );
5364 *self = Self::default();
5365 }
5366 }
5367}
5368
5369fn image_sampler_descriptor(sampling: ImageSampling) -> wgpu::SamplerDescriptor<'static> {
5370 let filter = match sampling {
5371 ImageSampling::Nearest => wgpu::FilterMode::Nearest,
5372 ImageSampling::Linear => wgpu::FilterMode::Linear,
5373 };
5374 wgpu::SamplerDescriptor {
5375 label: Some(match sampling {
5376 ImageSampling::Nearest => "Nearest Image Sampler",
5377 ImageSampling::Linear => "Linear Image Sampler",
5378 }),
5379 address_mode_u: wgpu::AddressMode::ClampToEdge,
5380 address_mode_v: wgpu::AddressMode::ClampToEdge,
5381 address_mode_w: wgpu::AddressMode::ClampToEdge,
5382 mag_filter: filter,
5383 min_filter: filter,
5384 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
5385 ..Default::default()
5386 }
5387}
5388
5389#[cfg(test)]
5390fn layer_raster_cache_candidate(
5391 layer: &LayerNode,
5392 root_scale: f32,
5393 has_backdrop_underlay: bool,
5394 allow_runtime_cache: bool,
5395) -> Option<(LayerRasterCacheKey, Rect)> {
5396 let mut layer_surface_requirements_cache = HashMap::new();
5397 let surface_requirements =
5398 layer_surface_requirements_cached(layer, &mut layer_surface_requirements_cache);
5399 let runtime_cache_is_safe = allow_runtime_cache
5400 && surface_requirements
5401 .surface_requirements
5402 .has_isolating_requirement()
5403 && !surface_requirements.contains_runtime_shader;
5404 let cache_is_allowed = layer.cache_policy == CachePolicy::Auto
5405 || (allow_runtime_cache && surface_requirements.has_renderer_forced_surface())
5406 || runtime_cache_is_safe;
5407 if !cache_is_allowed {
5408 return None;
5409 }
5410 if layer_uses_external_backdrop_input(layer, has_backdrop_underlay) {
5411 return None;
5412 }
5413 if surface_requirements.contains_runtime_shader {
5416 return None;
5417 }
5418
5419 let logical_rect = estimate_layer_surface_rect(layer);
5420 let pixel_size = surface_target_size(logical_rect, root_scale, u32::MAX);
5421 Some((
5422 LayerRasterCacheKey::new(
5423 layer.node_id,
5424 layer.target_content_hash(),
5425 layer.effect_hash(),
5426 logical_rect,
5427 pixel_size,
5428 ScaleBucket::from_scale(root_scale),
5429 ),
5430 logical_rect,
5431 ))
5432}
5433
5434impl GpuRenderer {
5435 pub fn new(
5436 device: Arc<wgpu::Device>,
5437 queue: Arc<wgpu::Queue>,
5438 surface_format: wgpu::TextureFormat,
5439 adapter_backend: wgpu::Backend,
5440 text_fonts: SoftwareTextFontSet,
5441 renderer_epoch: u64,
5442 store_feed_generation: u64,
5443 ) -> Self {
5444 #[cfg(target_arch = "wasm32")]
5445 let _ = store_feed_generation;
5446 let construction_started = Instant::now();
5454 let shape_batch_limits = ShapeBatchLimits::for_device(&device);
5455 let uniform_bind_group_layout =
5456 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
5457 label: Some("Uniform Bind Group Layout"),
5458 entries: &[wgpu::BindGroupLayoutEntry {
5459 binding: 0,
5460 visibility: wgpu::ShaderStages::VERTEX,
5461 ty: wgpu::BindingType::Buffer {
5462 ty: wgpu::BufferBindingType::Uniform,
5463 has_dynamic_offset: false,
5464 min_binding_size: None,
5465 },
5466 count: None,
5467 }],
5468 });
5469 #[cfg(not(target_arch = "wasm32"))]
5470 let retained_glyph_uniform_bind_group_layout =
5471 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
5472 label: Some("Retained Glyph Dynamic Uniform Bind Group Layout"),
5473 entries: &[wgpu::BindGroupLayoutEntry {
5474 binding: 0,
5475 visibility: wgpu::ShaderStages::VERTEX,
5476 ty: wgpu::BindingType::Buffer {
5477 ty: wgpu::BufferBindingType::Uniform,
5478 has_dynamic_offset: true,
5479 min_binding_size: wgpu::BufferSize::new(
5480 std::mem::size_of::<Uniforms>() as u64
5481 ),
5482 },
5483 count: None,
5484 }],
5485 });
5486
5487 let mut shape_bind_group_layout_entries = vec![
5496 wgpu::BindGroupLayoutEntry {
5497 binding: 0,
5498 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
5499 ty: wgpu::BindingType::Buffer {
5500 ty: shape_batch_limits.data_binding_type(),
5501 has_dynamic_offset: false,
5502 min_binding_size: None,
5503 },
5504 count: None,
5505 },
5506 wgpu::BindGroupLayoutEntry {
5507 binding: 1,
5508 visibility: wgpu::ShaderStages::FRAGMENT,
5509 ty: wgpu::BindingType::Buffer {
5510 ty: shape_batch_limits.data_binding_type(),
5511 has_dynamic_offset: false,
5512 min_binding_size: None,
5513 },
5514 count: None,
5515 },
5516 wgpu::BindGroupLayoutEntry {
5521 binding: 2,
5522 visibility: wgpu::ShaderStages::VERTEX,
5523 ty: wgpu::BindingType::Buffer {
5524 ty: wgpu::BufferBindingType::Uniform,
5525 has_dynamic_offset: true,
5526 min_binding_size: wgpu::BufferSize::new(
5527 std::mem::size_of::<SimilarityTransform>() as u64,
5528 ),
5529 },
5530 count: None,
5531 },
5532 ];
5533 if shape_batch_limits.storage {
5539 shape_bind_group_layout_entries.push(wgpu::BindGroupLayoutEntry {
5540 binding: 3,
5541 visibility: wgpu::ShaderStages::VERTEX,
5542 ty: wgpu::BindingType::Buffer {
5543 ty: wgpu::BufferBindingType::Storage { read_only: true },
5544 has_dynamic_offset: false,
5545 min_binding_size: None,
5546 },
5547 count: None,
5548 });
5549 }
5550 let shape_bind_group_layout =
5551 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
5552 label: Some("Shape Bind Group Layout"),
5553 entries: &shape_bind_group_layout_entries,
5554 });
5555
5556 let identity_similarity_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5557 label: Some("Identity Similarity Buffer"),
5558 size: std::mem::size_of::<SimilarityTransform>() as u64,
5559 usage: wgpu::BufferUsages::UNIFORM,
5560 mapped_at_creation: true,
5561 });
5562 identity_similarity_buffer
5563 .slice(..)
5564 .get_mapped_range_mut()
5565 .copy_from_slice(bytemuck::bytes_of(&SimilarityTransform::IDENTITY));
5566 identity_similarity_buffer.unmap();
5567
5568 let dummy_paint_buffer = shape_batch_limits.storage.then(|| {
5573 device.create_buffer(&wgpu::BufferDescriptor {
5574 label: Some("Dummy Paint Buffer"),
5575 size: std::mem::size_of::<[f32; 4]>() as u64,
5576 usage: wgpu::BufferUsages::STORAGE,
5577 mapped_at_creation: false,
5578 })
5579 });
5580 #[cfg(not(target_arch = "wasm32"))]
5581 let replay_slot_store = ReplaySlotStore::new(&device);
5582
5583 let pipeline = LazyGpuResource::new("shape/src-over");
5584 let pipeline_dst_out = LazyGpuResource::new("shape/dst-out");
5585 let pipeline_solid = LazyGpuResource::new("shape/solid-src-over");
5586 #[cfg(not(target_arch = "wasm32"))]
5587 let mesh_pipeline = LazyGpuResource::new("shape/mesh");
5588 #[cfg(not(target_arch = "wasm32"))]
5594 let instanced_quads =
5595 (shape_batch_limits.storage && instanced_quads_enabled()).then(|| {
5596 let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5597 label: Some("Instanced Quad Index Buffer"),
5598 size: std::mem::size_of_val(&INSTANCED_QUAD_INDICES) as u64,
5599 usage: wgpu::BufferUsages::INDEX,
5600 mapped_at_creation: true,
5601 });
5602 index_buffer
5603 .slice(..)
5604 .get_mapped_range_mut()
5605 .copy_from_slice(bytemuck::cast_slice(&INSTANCED_QUAD_INDICES));
5606 index_buffer.unmap();
5607 InstancedQuadPipelines {
5608 pipeline: LazyGpuResource::new("shape/instanced-src-over"),
5609 pipeline_dst_out: LazyGpuResource::new("shape/instanced-dst-out"),
5610 pipeline_solid: LazyGpuResource::new("shape/instanced-solid"),
5611 index_buffer,
5612 }
5613 });
5614
5615 let image_bind_group_layout =
5616 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
5617 label: Some("Image Texture Bind Group Layout"),
5618 entries: &[
5619 wgpu::BindGroupLayoutEntry {
5620 binding: 0,
5621 visibility: wgpu::ShaderStages::FRAGMENT,
5622 ty: wgpu::BindingType::Texture {
5623 multisampled: false,
5624 view_dimension: wgpu::TextureViewDimension::D2,
5625 sample_type: wgpu::TextureSampleType::Float { filterable: true },
5626 },
5627 count: None,
5628 },
5629 wgpu::BindGroupLayoutEntry {
5630 binding: 1,
5631 visibility: wgpu::ShaderStages::FRAGMENT,
5632 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
5633 count: None,
5634 },
5635 ],
5636 });
5637
5638 let image_pipeline = LazyGpuResource::new("image/src-over");
5639 let image_pipeline_dst_out = LazyGpuResource::new("image/dst-out");
5640 let glyph_atlas_pipeline = LazyGpuResource::new("glyph/shared");
5641 #[cfg(not(target_arch = "wasm32"))]
5642 let retained_glyph_atlas_pipeline = LazyGpuResource::new("glyph/retained");
5643
5644 #[cfg(not(target_arch = "wasm32"))]
5645 let upload_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5646 label: Some("Frame Upload Buffer"),
5647 size: INITIAL_UPLOAD_BUFFER_BYTES,
5648 usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
5649 mapped_at_creation: false,
5650 });
5651
5652 #[cfg(not(target_arch = "wasm32"))]
5653 let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5654 label: Some("Uniform Buffer"),
5655 size: std::mem::size_of::<Uniforms>() as u64,
5656 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
5657 mapped_at_creation: false,
5658 });
5659
5660 #[cfg(not(target_arch = "wasm32"))]
5661 let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
5662 label: Some("Uniform Bind Group"),
5663 layout: &uniform_bind_group_layout,
5664 entries: &[wgpu::BindGroupEntry {
5665 binding: 0,
5666 resource: uniform_buffer.as_entire_binding(),
5667 }],
5668 });
5669
5670 #[cfg(not(target_arch = "wasm32"))]
5671 let shape_buffers = ShapeBatchBuffers::new(
5672 &device,
5673 &shape_bind_group_layout,
5674 &identity_similarity_buffer,
5675 dummy_paint_buffer.as_ref(),
5676 shape_batch_limits,
5677 );
5678
5679 let image_nearest_sampler =
5680 device.create_sampler(&image_sampler_descriptor(ImageSampling::Nearest));
5681 let image_linear_sampler =
5682 device.create_sampler(&image_sampler_descriptor(ImageSampling::Linear));
5683 let text_glyph_atlas = TextGlyphAtlas::new(
5684 &device,
5685 &image_bind_group_layout,
5686 &image_nearest_sampler,
5687 TEXT_GLYPH_ATLAS_MIN_SIZE,
5688 );
5689
5690 #[cfg(not(target_arch = "wasm32"))]
5691 let image_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5692 label: Some("Image Vertex Buffer"),
5693 size: (std::mem::size_of::<Vertex>() * 4) as u64,
5694 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
5695 mapped_at_creation: false,
5696 });
5697
5698 #[cfg(not(target_arch = "wasm32"))]
5699 let image_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5700 label: Some("Image Index Buffer"),
5701 size: (std::mem::size_of::<u32>() * 6) as u64,
5702 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
5703 mapped_at_creation: false,
5704 });
5705 #[cfg(not(target_arch = "wasm32"))]
5706 let retained_glyph_uniform_stride = align_usize_to(
5707 std::mem::size_of::<Uniforms>(),
5708 (device.limits().min_uniform_buffer_offset_alignment as usize)
5709 .max(wgpu::COPY_BUFFER_ALIGNMENT as usize),
5710 ) as u64;
5711 #[cfg(not(target_arch = "wasm32"))]
5712 let retained_glyph_uniform_capacity = INITIAL_RETAINED_GLYPH_UNIFORM_SLOTS;
5713 #[cfg(not(target_arch = "wasm32"))]
5714 let retained_glyph_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5715 label: Some("Retained Glyph Uniform Buffer"),
5716 size: retained_glyph_uniform_stride * retained_glyph_uniform_capacity as u64,
5717 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
5718 mapped_at_creation: false,
5719 });
5720 #[cfg(not(target_arch = "wasm32"))]
5721 let retained_glyph_uniform_bind_group =
5722 device.create_bind_group(&wgpu::BindGroupDescriptor {
5723 label: Some("Retained Glyph Uniform Bind Group"),
5724 layout: &retained_glyph_uniform_bind_group_layout,
5725 entries: &[wgpu::BindGroupEntry {
5726 binding: 0,
5727 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
5728 buffer: &retained_glyph_uniform_buffer,
5729 offset: 0,
5730 size: wgpu::BufferSize::new(std::mem::size_of::<Uniforms>() as u64),
5731 }),
5732 }],
5733 });
5734
5735 let effects_started = Instant::now();
5736 let effect_renderer = EffectRenderer::new(&device, surface_format, adapter_backend);
5737 let effects_ms = instant_ms(effects_started, Instant::now());
5738
5739 let renderer = Self {
5740 device,
5741 queue,
5742 renderer_epoch,
5743 #[cfg(not(target_arch = "wasm32"))]
5744 store_feed_generation,
5745 surface_format,
5746 adapter_backend,
5747 shape_batch_limits,
5748 pipeline,
5749 pipeline_dst_out,
5750 pipeline_solid,
5751 #[cfg(not(target_arch = "wasm32"))]
5752 mesh_pipeline,
5753 #[cfg(not(target_arch = "wasm32"))]
5754 instanced_quads,
5755 uniform_bind_group_layout,
5756 shape_bind_group_layout,
5757 dummy_paint_buffer,
5758 identity_similarity_buffer,
5759 #[cfg(not(target_arch = "wasm32"))]
5760 replay_slots: replay_slot_store,
5761 image_pipeline,
5762 image_pipeline_dst_out,
5763 glyph_atlas_pipeline,
5764 #[cfg(not(target_arch = "wasm32"))]
5765 retained_glyph_atlas_pipeline,
5766 image_bind_group_layout,
5767 #[cfg(not(target_arch = "wasm32"))]
5768 retained_glyph_uniform_bind_group_layout,
5769 image_nearest_sampler,
5770 image_linear_sampler,
5771 text_fonts,
5772 #[cfg(not(target_arch = "wasm32"))]
5773 upload_buffer,
5774 #[cfg(not(target_arch = "wasm32"))]
5775 uniform_buffer,
5776 #[cfg(not(target_arch = "wasm32"))]
5777 uniform_bind_group,
5778 #[cfg(not(target_arch = "wasm32"))]
5779 shape_buffers,
5780 #[cfg(not(target_arch = "wasm32"))]
5781 image_vertex_buffer,
5782 #[cfg(not(target_arch = "wasm32"))]
5783 image_index_buffer,
5784 #[cfg(not(target_arch = "wasm32"))]
5785 retained_glyph_uniform_buffer,
5786 #[cfg(not(target_arch = "wasm32"))]
5787 retained_glyph_uniform_bind_group,
5788 #[cfg(not(target_arch = "wasm32"))]
5789 retained_glyph_uniform_stride,
5790 #[cfg(not(target_arch = "wasm32"))]
5791 retained_glyph_uniform_capacity,
5792 #[cfg(not(target_arch = "wasm32"))]
5793 retained_glyph_uniform_cursor: 0,
5794 #[cfg(target_arch = "wasm32")]
5795 wasm_uniform_batches: Vec::new(),
5796 #[cfg(target_arch = "wasm32")]
5797 wasm_uniform_batch_cursor: 0,
5798 #[cfg(target_arch = "wasm32")]
5799 wasm_shape_batches: Vec::new(),
5800 #[cfg(target_arch = "wasm32")]
5801 wasm_shape_batch_cursor: 0,
5802 #[cfg(target_arch = "wasm32")]
5803 wasm_image_batches: Vec::new(),
5804 #[cfg(target_arch = "wasm32")]
5805 wasm_image_batch_cursor: 0,
5806 image_texture_cache: BoundedLruCache::with_capacity_at_least_one(
5807 MAX_TEXTURE_CACHE_ITEMS,
5808 ),
5809 image_texture_cache_bytes: 0,
5810 text_image_cache: BoundedLruCache::with_capacity_at_least_one(
5811 MAX_TEXT_IMAGE_CACHE_ITEMS,
5812 ),
5813 text_glyph_atlas,
5814 text_glyph_run_cache: BoundedLruCache::with_capacity_at_least_one(
5815 MAX_TEXT_GLYPH_RUN_CACHE_ITEMS,
5816 ),
5817 #[cfg(not(target_arch = "wasm32"))]
5818 text_glyph_gpu_run_cache: BoundedLruCache::with_capacity_at_least_one(
5819 MAX_TEXT_GLYPH_GPU_RUN_CACHE_ITEMS,
5820 ),
5821 text_glyph_mask_cache: SoftwareGlyphRasterCache::with_capacity_at_least_one(
5822 MAX_TEXT_GLYPH_MASK_CACHE_ITEMS,
5823 ),
5824 text_line_index_cache: TextLineIndexCache::new(MAX_TEXT_LINE_INDEX_CACHE_ITEMS),
5825 scratch_shape_data: Vec::new(),
5826 scratch_gradients: Vec::new(),
5827 scratch_image_vertices: Vec::new(),
5828 scratch_image_indices: Vec::new(),
5829 scratch_image_cmds: Vec::new(),
5830 scratch_glyph_cmds: Vec::new(),
5831 scratch_text_glyph_run: Vec::new(),
5832 scratch_text_glyph_placements: Vec::new(),
5833 scratch_text_glyph_quads: Vec::new(),
5834 scratch_segment_items: Vec::new(),
5835 scratch_effect_ranges: Vec::new(),
5836 scratch_layer_events: Vec::new(),
5837 staged_uploads: StagedBufferUploads::default(),
5838 frame_graph_executor: WgpuFrameGraphExecutor::new(),
5839 deferred_offscreen_releases: Vec::new(),
5840 effect_renderer,
5841 layer_surface_cache: LayerSurfaceCache::new(),
5842 observed_scene_range_cache_misses: BoundedLruCache::with_capacity_at_least_one(
5843 MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES,
5844 ),
5845 shadow_surface_cache: BoundedLruCache::with_capacity_at_least_one(
5846 MAX_SHADOW_SURFACE_CACHE_ITEMS,
5847 ),
5848 shadow_surface_cache_bytes: 0,
5849 frame_stats: gpu_stats::FrameStats::default(),
5850 last_frame_stats: None,
5851 pending_frame_warmup_frames: 0,
5852 frame_count: 0,
5853 gpu_stats_enabled: gpu_stats_enabled(),
5854 warning_state: RendererWarningState::default(),
5855 #[cfg(not(target_arch = "wasm32"))]
5856 replay_upload_stats: ReplayUploadStats::default(),
5857 #[cfg(not(target_arch = "wasm32"))]
5858 replay_color_patches: Vec::new(),
5859 #[cfg(not(target_arch = "wasm32"))]
5860 color_patch_scratch: Vec::new(),
5861 #[cfg(not(target_arch = "wasm32"))]
5862 replay_ack_confirmations: Vec::new(),
5863 #[cfg(not(target_arch = "wasm32"))]
5864 replay_generation_drops: 0,
5865 #[cfg(not(target_arch = "wasm32"))]
5866 retained_bundle_cache: RetainedBundleCache::new(),
5867 #[cfg(not(target_arch = "wasm32"))]
5868 rim_mesh_vertices: Vec::new(),
5869 #[cfg(not(target_arch = "wasm32"))]
5870 rim_mesh_indices: Vec::new(),
5871 #[cfg(not(target_arch = "wasm32"))]
5872 rim_mesh_vertex_buffer: None,
5873 #[cfg(not(target_arch = "wasm32"))]
5874 rim_mesh_index_buffer: None,
5875 #[cfg(not(target_arch = "wasm32"))]
5876 rim_mesh_uploaded_vertices: 0,
5877 #[cfg(not(target_arch = "wasm32"))]
5878 rim_mesh_uploaded_indices: 0,
5879 #[cfg(not(target_arch = "wasm32"))]
5880 rim_meshes_emitted: 0,
5881 #[cfg(not(target_arch = "wasm32"))]
5882 fill_area_diag: FillAreaDiag::default(),
5883 #[cfg(not(target_arch = "wasm32"))]
5884 static_span: StaticSpanCache::default(),
5885 };
5886 log::info!(
5887 "[gpu-init] {:?} renderer ready in {:.1} ms (effects {:.1} ms); \
5888 pipelines build on first use",
5889 adapter_backend,
5890 instant_ms(construction_started, Instant::now()),
5891 effects_ms,
5892 );
5893 renderer
5894 }
5895
5896 fn shape_pipeline(&self, blend_mode: BlendMode) -> &wgpu::RenderPipeline {
5897 let resource = match blend_mode {
5898 BlendMode::DstOut => &self.pipeline_dst_out,
5899 _ => &self.pipeline,
5900 };
5901 resource.get_or_init(self.adapter_backend, || {
5902 create_shape_pipeline(
5903 &self.device,
5904 self.surface_format,
5905 &self.uniform_bind_group_layout,
5906 &self.shape_bind_group_layout,
5907 blend_mode,
5908 self.shape_batch_limits,
5909 "fs_main",
5910 )
5911 })
5912 }
5913
5914 fn shape_pipeline_solid(&self) -> &wgpu::RenderPipeline {
5919 self.pipeline_solid.get_or_init(self.adapter_backend, || {
5920 create_shape_pipeline(
5921 &self.device,
5922 self.surface_format,
5923 &self.uniform_bind_group_layout,
5924 &self.shape_bind_group_layout,
5925 BlendMode::SrcOver,
5926 self.shape_batch_limits,
5927 "fs_solid",
5928 )
5929 })
5930 }
5931
5932 #[cfg(not(target_arch = "wasm32"))]
5933 fn mesh_pipeline(&self) -> &wgpu::RenderPipeline {
5934 self.mesh_pipeline.get_or_init(self.adapter_backend, || {
5935 create_mesh_shape_pipeline(
5936 &self.device,
5937 self.surface_format,
5938 &self.uniform_bind_group_layout,
5939 &self.shape_bind_group_layout,
5940 self.shape_batch_limits,
5941 )
5942 })
5943 }
5944
5945 #[cfg(not(target_arch = "wasm32"))]
5946 fn instanced_pipeline<'a>(
5947 &'a self,
5948 instanced: &'a InstancedQuadPipelines,
5949 blend_mode: BlendMode,
5950 ) -> &'a wgpu::RenderPipeline {
5951 let resource = match blend_mode {
5952 BlendMode::DstOut => &instanced.pipeline_dst_out,
5953 _ => &instanced.pipeline,
5954 };
5955 resource.get_or_init(self.adapter_backend, || {
5956 create_instanced_shape_pipeline(
5957 &self.device,
5958 self.surface_format,
5959 &self.uniform_bind_group_layout,
5960 &self.shape_bind_group_layout,
5961 blend_mode,
5962 self.shape_batch_limits,
5963 "fs_main",
5964 )
5965 })
5966 }
5967
5968 #[cfg(not(target_arch = "wasm32"))]
5970 fn instanced_pipeline_solid<'a>(
5971 &'a self,
5972 instanced: &'a InstancedQuadPipelines,
5973 ) -> &'a wgpu::RenderPipeline {
5974 instanced
5975 .pipeline_solid
5976 .get_or_init(self.adapter_backend, || {
5977 create_instanced_shape_pipeline(
5978 &self.device,
5979 self.surface_format,
5980 &self.uniform_bind_group_layout,
5981 &self.shape_bind_group_layout,
5982 BlendMode::SrcOver,
5983 self.shape_batch_limits,
5984 "fs_solid",
5985 )
5986 })
5987 }
5988
5989 fn image_pipeline(&self, blend_mode: BlendMode) -> &wgpu::RenderPipeline {
5990 let resource = match blend_mode {
5991 BlendMode::DstOut => &self.image_pipeline_dst_out,
5992 _ => &self.image_pipeline,
5993 };
5994 resource.get_or_init(self.adapter_backend, || {
5995 create_image_pipeline(
5996 &self.device,
5997 self.surface_format,
5998 &self.uniform_bind_group_layout,
5999 &self.image_bind_group_layout,
6000 blend_mode,
6001 )
6002 })
6003 }
6004
6005 fn glyph_atlas_pipeline(&self) -> &wgpu::RenderPipeline {
6006 self.glyph_atlas_pipeline
6007 .get_or_init(self.adapter_backend, || {
6008 create_glyph_atlas_pipeline(
6009 &self.device,
6010 self.surface_format,
6011 &self.uniform_bind_group_layout,
6012 &self.image_bind_group_layout,
6013 )
6014 })
6015 }
6016
6017 #[cfg(not(target_arch = "wasm32"))]
6018 fn retained_glyph_atlas_pipeline(&self) -> &wgpu::RenderPipeline {
6019 self.retained_glyph_atlas_pipeline
6020 .get_or_init(self.adapter_backend, || {
6021 create_glyph_atlas_pipeline(
6022 &self.device,
6023 self.surface_format,
6024 &self.retained_glyph_uniform_bind_group_layout,
6025 &self.image_bind_group_layout,
6026 )
6027 })
6028 }
6029
6030 fn ensure_image_cached(&mut self, image: &ImageBitmap) -> Result<(), String> {
6031 if self.image_texture_cache.get(&image.id()).is_some() {
6032 return Ok(());
6033 }
6034
6035 let size = wgpu::Extent3d {
6036 width: image.width(),
6037 height: image.height(),
6038 depth_or_array_layers: 1,
6039 };
6040
6041 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
6042 label: Some("Image Texture"),
6043 size,
6044 mip_level_count: 1,
6045 sample_count: 1,
6046 dimension: wgpu::TextureDimension::D2,
6047 format: wgpu::TextureFormat::Rgba8Unorm,
6048 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
6049 view_formats: &[],
6050 });
6051
6052 let upload_stats = self.frame_graph_executor.upload_texture(
6053 &self.queue,
6054 wgpu::TexelCopyTextureInfo {
6055 texture: &texture,
6056 mip_level: 0,
6057 origin: wgpu::Origin3d::ZERO,
6058 aspect: wgpu::TextureAspect::All,
6059 },
6060 image.pixels(),
6061 wgpu::TexelCopyBufferLayout {
6062 offset: 0,
6063 bytes_per_row: Some(4 * image.width()),
6064 rows_per_image: Some(image.height()),
6065 },
6066 size,
6067 );
6068 self.frame_stats.record_command_stats(upload_stats);
6069
6070 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
6071 let nearest_bind_group = self.image_bind_group(&view, &self.image_nearest_sampler);
6072 let linear_bind_group = self.image_bind_group(&view, &self.image_linear_sampler);
6073
6074 let bytes = image.width() as usize * image.height() as usize * 4;
6075 if let Some(replaced) = self.image_texture_cache.put(
6076 image.id(),
6077 CachedImageTexture {
6078 _texture: texture,
6079 _view: view,
6080 nearest_bind_group,
6081 linear_bind_group,
6082 bytes,
6083 },
6084 ) {
6085 self.image_texture_cache_bytes = self
6086 .image_texture_cache_bytes
6087 .saturating_sub(replaced.bytes);
6088 }
6089 self.image_texture_cache_bytes += bytes;
6090 while self.image_texture_cache_bytes > MAX_IMAGE_TEXTURE_CACHE_BYTES
6093 && self.image_texture_cache.len() > 1
6094 {
6095 let Some((_, evicted)) = self.image_texture_cache.pop_lru() else {
6096 break;
6097 };
6098 self.image_texture_cache_bytes =
6099 self.image_texture_cache_bytes.saturating_sub(evicted.bytes);
6100 }
6101 Ok(())
6102 }
6103
6104 fn image_bind_group(
6105 &self,
6106 view: &wgpu::TextureView,
6107 sampler: &wgpu::Sampler,
6108 ) -> wgpu::BindGroup {
6109 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
6110 label: Some("Image Texture Bind Group"),
6111 layout: &self.image_bind_group_layout,
6112 entries: &[
6113 wgpu::BindGroupEntry {
6114 binding: 0,
6115 resource: wgpu::BindingResource::TextureView(view),
6116 },
6117 wgpu::BindGroupEntry {
6118 binding: 1,
6119 resource: wgpu::BindingResource::Sampler(sampler),
6120 },
6121 ],
6122 })
6123 }
6124
6125 fn max_texture_dim(&self) -> u32 {
6128 self.effect_renderer.max_texture_dim()
6129 }
6130
6131 fn acquire_offscreen(&mut self, width: u32, height: u32) -> OffscreenTarget {
6132 self.effect_renderer
6133 .acquire_offscreen(&self.device, width, height, Some(&self.frame_stats))
6134 }
6135
6136 fn acquire_retained_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
6137 self.acquire_offscreen(width, height)
6138 }
6139
6140 fn transient_offscreen_descriptor(
6141 &self,
6142 label: &'static str,
6143 width: u32,
6144 height: u32,
6145 ) -> FrameTextureDescriptor {
6146 let max_texture_dim = self.max_texture_dim();
6147 FrameTextureDescriptor::render_attachment(
6148 label,
6149 width.min(max_texture_dim),
6150 height.min(max_texture_dim),
6151 self.surface_format,
6152 )
6153 }
6154
6155 fn defer_offscreen_release(&mut self, target: OffscreenTarget) {
6156 self.deferred_offscreen_releases.push(target);
6157 }
6158
6159 fn flush_deferred_offscreen_releases(&mut self) {
6160 for target in self.deferred_offscreen_releases.drain(..) {
6161 self.effect_renderer.release_offscreen(target);
6162 }
6163 }
6164
6165 fn release_layer_surface_target(&mut self, target: LayerSurfaceTexture) {
6166 if let LayerSurfaceTexture::Owned(target) = target {
6167 self.defer_offscreen_release(target);
6168 }
6169 }
6170
6171 fn cached_layer_surface(
6172 &mut self,
6173 key: &LayerRasterCacheKey,
6174 ) -> Option<(Rc<OffscreenTarget>, Rect)> {
6175 self.layer_surface_cache.get(key, &self.frame_stats)
6176 }
6177
6178 fn admit_layer_surface_cache_miss(&mut self, key: &LayerRasterCacheKey) -> bool {
6179 admit_layer_surface_cache_miss_impl(key, &mut self.observed_scene_range_cache_misses)
6180 }
6181
6182 fn insert_cached_layer_surface(
6183 &mut self,
6184 key: LayerRasterCacheKey,
6185 target: OffscreenTarget,
6186 logical_rect: Rect,
6187 ) -> Rc<OffscreenTarget> {
6188 self.layer_surface_cache
6189 .insert(key, target, logical_rect, &self.frame_stats)
6190 }
6191
6192 fn cached_shadow_surface(
6193 &mut self,
6194 key: &ShadowSurfaceCacheKey,
6195 ) -> Option<Rc<OffscreenTarget>> {
6196 self.shadow_surface_cache
6197 .get(key)
6198 .map(|cached| cached.target.clone())
6199 }
6200
6201 fn cached_shape_shadow_composite(
6202 &mut self,
6203 shadow: &ShadowDraw,
6204 width: u32,
6205 height: u32,
6206 root_scale: f32,
6207 ) -> Option<CachedShadowComposite> {
6208 if shadow.blur_radius <= 0.0 || shadow.shapes.is_empty() || !shadow.texts.is_empty() {
6209 return None;
6210 }
6211
6212 let plan = shape_shadow_surface_plan(
6213 &shadow.shapes,
6214 shadow.clip,
6215 shadow.blur_radius,
6216 width,
6217 height,
6218 root_scale,
6219 self.max_texture_dim(),
6220 )?;
6221 let key = shape_shadow_surface_cache_key(
6222 &shadow.shapes,
6223 &shadow.brushes,
6224 plan.source_device_bounds,
6225 plan.pixel_radius,
6226 root_scale,
6227 )?;
6228 let cached = self.cached_shadow_surface(&key)?;
6229 let viewport_offset = [plan.source_device_bounds.x, plan.source_device_bounds.y];
6230 self.frame_stats.record_shadow_shape_cache_hit(
6231 plan.source_device_bounds.width,
6232 plan.source_device_bounds.height,
6233 );
6234
6235 let clip_scissor = shadow
6236 .clip
6237 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
6238 let scissor = clip_scissor.or(plan.processing_scissor);
6239 let rounded_mask = inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
6240 mask.rect[0] -= viewport_offset[0];
6241 mask.rect[1] -= viewport_offset[1];
6242 mask
6243 });
6244 let dest_viewport = Some((
6245 viewport_offset[0],
6246 viewport_offset[1],
6247 plan.source_device_bounds.width as f32,
6248 plan.source_device_bounds.height as f32,
6249 ));
6250
6251 Some(CachedShadowComposite {
6252 source: cached,
6253 scissor,
6254 rounded_mask,
6255 dest_viewport,
6256 })
6257 }
6258
6259 fn insert_cached_shadow_surface(
6260 &mut self,
6261 key: ShadowSurfaceCacheKey,
6262 target: OffscreenTarget,
6263 ) {
6264 let byte_size = offscreen_byte_size(target.width, target.height);
6265 while self.shadow_surface_cache_bytes + byte_size > MAX_SHADOW_SURFACE_CACHE_BYTES {
6266 let Some((_evicted_key, evicted_entry)) = self.shadow_surface_cache.pop_lru() else {
6267 break;
6268 };
6269 self.shadow_surface_cache_bytes = self
6270 .shadow_surface_cache_bytes
6271 .saturating_sub(evicted_entry.byte_size);
6272 }
6273
6274 let cached = CachedShadowSurface {
6275 target: Rc::new(target),
6276 byte_size,
6277 };
6278 if let Some((_replaced_key, replaced_entry)) = self.shadow_surface_cache.push(key, cached) {
6279 self.shadow_surface_cache_bytes = self
6280 .shadow_surface_cache_bytes
6281 .saturating_sub(replaced_entry.byte_size);
6282 }
6283 self.shadow_surface_cache_bytes = self.shadow_surface_cache_bytes.saturating_add(byte_size);
6284 }
6285
6286 fn supports_render_effect(&self, effect: &RenderEffect) -> bool {
6287 is_render_effect_supported(effect)
6288 }
6289}
6290
6291struct RecordingSurfaceBackend<'renderer, 'recorder, C: FrameCommandRecorder> {
6292 renderer: &'renderer mut GpuRenderer,
6293 recorder: &'recorder mut C,
6294}
6295
6296impl<C: FrameCommandRecorder> RecordingSurfaceBackend<'_, '_, C> {
6297 #[allow(clippy::too_many_arguments)]
6298 fn render_range_with_layer_events_to_target_recorded(
6299 &mut self,
6300 target: &OffscreenTarget,
6301 shapes: &[DrawShape],
6302 brushes: &[Brush],
6303 images: &[ImageDraw],
6304 texts: &[TextDraw],
6305 shadow_draws: &[ShadowDraw],
6306 draw_ops: &[DrawOp],
6307 effect_layers: &[EffectLayer],
6308 backdrop_layers: &[BackdropLayer],
6309 z_start: usize,
6310 z_end: usize,
6311 excluded_effect_layer: Option<usize>,
6312 width: u32,
6313 height: u32,
6314 root_scale: f32,
6315 backdrop_underlay: Option<&OffscreenTarget>,
6316 initial_load_op: wgpu::LoadOp<wgpu::Color>,
6317 ) -> Result<(), String> {
6318 if z_start >= z_end {
6319 if matches!(initial_load_op, wgpu::LoadOp::Clear(_)) {
6320 self.clear_target_view_with_load_op(&target.view, initial_load_op);
6321 }
6322 return Ok(());
6323 }
6324
6325 let mut effect_z_ranges = std::mem::take(&mut self.renderer.scratch_effect_ranges);
6326 collect_effect_ranges(
6327 effect_layers,
6328 z_start,
6329 z_end,
6330 excluded_effect_layer,
6331 &mut effect_z_ranges,
6332 );
6333 let mut events = std::mem::take(&mut self.renderer.scratch_layer_events);
6334 collect_layer_events(
6335 effect_layers,
6336 backdrop_layers,
6337 z_start,
6338 z_end,
6339 excluded_effect_layer,
6340 &mut events,
6341 );
6342
6343 let result = (|| -> Result<(), String> {
6344 let mut next_load_op = initial_load_op;
6345 let mut cursor_z = z_start;
6346 for event in &events {
6347 if event.z_index > cursor_z {
6348 self.render_non_effect_segment(
6349 &target.view,
6350 shapes,
6351 brushes,
6352 images,
6353 texts,
6354 shadow_draws,
6355 &[],
6358 draw_ops,
6359 cursor_z,
6360 event.z_index,
6361 &effect_z_ranges,
6362 width,
6363 height,
6364 root_scale,
6365 next_load_op,
6366 )?;
6367 next_load_op = wgpu::LoadOp::Load;
6368 cursor_z = event.z_index;
6369 } else if event.z_index < cursor_z {
6370 continue;
6371 }
6372
6373 if matches!(next_load_op, wgpu::LoadOp::Clear(_)) {
6374 self.clear_target_view_with_load_op(&target.view, next_load_op);
6375 next_load_op = wgpu::LoadOp::Load;
6376 }
6377
6378 match event.kind {
6379 LayerEventKind::Backdrop(index) => {
6380 let layer = &backdrop_layers[index];
6381 let effective_backdrop_underlay = if backdrop_underlay.is_some()
6382 && backdrop_underlay_is_covered_by_local_content(
6383 shapes,
6384 brushes,
6385 images,
6386 shadow_draws,
6387 draw_ops,
6388 effect_layers,
6389 backdrop_layers,
6390 layer,
6391 ) {
6392 None
6393 } else {
6394 backdrop_underlay
6395 };
6396 execute_apply_backdrop_layer_to_target(
6397 self,
6398 target,
6399 layer,
6400 effective_backdrop_underlay,
6401 width,
6402 height,
6403 root_scale,
6404 None,
6405 )?;
6406 }
6407 LayerEventKind::Effect(index) => {
6408 let layer = &effect_layers[index];
6409 if layer.z_start < cursor_z {
6410 continue;
6411 }
6412 execute_render_effect_layer_to_target(
6413 self,
6414 target,
6415 shapes,
6416 brushes,
6417 images,
6418 texts,
6419 shadow_draws,
6420 draw_ops,
6421 effect_layers,
6422 backdrop_layers,
6423 index,
6424 backdrop_underlay,
6425 width,
6426 height,
6427 root_scale,
6428 )?;
6429 cursor_z = cursor_z.max(layer.z_end);
6430 }
6431 }
6432 }
6433
6434 if cursor_z < z_end {
6435 self.render_non_effect_segment(
6436 &target.view,
6437 shapes,
6438 brushes,
6439 images,
6440 texts,
6441 shadow_draws,
6442 &[],
6443 draw_ops,
6444 cursor_z,
6445 z_end,
6446 &effect_z_ranges,
6447 width,
6448 height,
6449 root_scale,
6450 next_load_op,
6451 )?;
6452 } else if matches!(next_load_op, wgpu::LoadOp::Clear(_)) {
6453 self.clear_target_view_with_load_op(&target.view, next_load_op);
6454 }
6455
6456 Ok(())
6457 })();
6458
6459 self.renderer.scratch_effect_ranges = effect_z_ranges;
6460 self.renderer.scratch_layer_events = events;
6461 result
6462 }
6463
6464 #[allow(clippy::too_many_arguments)]
6465 fn record_shader_composite(
6466 &mut self,
6467 source: &OffscreenTarget,
6468 shader: &RuntimeShader,
6469 effect_rect: [f32; 4],
6470 dest_view: &wgpu::TextureView,
6471 alpha: f32,
6472 load_op: wgpu::LoadOp<wgpu::Color>,
6473 scissor: Option<(u32, u32, u32, u32)>,
6474 blend_mode: BlendMode,
6475 dest_viewport: Option<(f32, f32, f32, f32)>,
6476 sample_mode: CompositeSampleMode,
6477 ) {
6478 let device = self.renderer.device.clone();
6479 if let Some(viewport) = direct_shader_composite_viewport(
6480 alpha,
6481 blend_mode,
6482 dest_viewport,
6483 sample_mode,
6484 (source.width, source.height),
6485 ) {
6486 let shader_applied = self
6487 .renderer
6488 .effect_renderer
6489 .encode_shader_src_over_to_view(
6490 self.recorder,
6491 &device,
6492 source,
6493 dest_view,
6494 shader,
6495 effect_rect,
6496 load_op,
6497 scissor,
6498 viewport,
6499 );
6500 if shader_applied {
6501 self.renderer
6502 .effect_renderer
6503 .debug_effects
6504 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
6505 self.recorder.record_pass();
6506 self.renderer.effect_renderer.record_composite_pass();
6507 return;
6508 }
6509 }
6510 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
6511 "Shader Effect Composite Scratch",
6512 source.width,
6513 source.height,
6514 );
6515 let scratch = self
6516 .recorder
6517 .acquire_transient_offscreen(&device, scratch_descriptor);
6518 let shader_applied = {
6519 self.renderer.effect_renderer.encode_shader(
6520 self.recorder,
6521 &device,
6522 source,
6523 &scratch.view,
6524 shader,
6525 effect_rect,
6526 )
6527 };
6528 let composite_source = if shader_applied {
6529 self.renderer
6530 .effect_renderer
6531 .debug_effects
6532 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
6533 self.recorder.record_pass();
6534 &scratch
6535 } else {
6536 source
6537 };
6538 {
6539 self.renderer
6540 .effect_renderer
6541 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
6542 self.recorder,
6543 &device,
6544 composite_source,
6545 dest_view,
6546 alpha,
6547 load_op,
6548 scissor,
6549 None,
6550 supported_blend_mode(blend_mode),
6551 dest_viewport,
6552 sample_mode,
6553 );
6554 }
6555 self.recorder.record_pass();
6556 self.renderer.effect_renderer.record_composite_pass();
6557 self.recorder
6558 .release_transient_offscreen(scratch_descriptor, scratch);
6559 }
6560
6561 #[allow(clippy::too_many_arguments)]
6562 fn record_shader_projective_composite(
6563 &mut self,
6564 source: &OffscreenTarget,
6565 shader: &RuntimeShader,
6566 effect_rect: [f32; 4],
6567 dest_view: &wgpu::TextureView,
6568 viewport: (u32, u32),
6569 source_size: (f32, f32),
6570 inverse_matrix: [[f32; 3]; 3],
6571 dest_bounds: [[f32; 2]; 4],
6572 alpha: f32,
6573 load_op: wgpu::LoadOp<wgpu::Color>,
6574 scissor: Option<(u32, u32, u32, u32)>,
6575 blend_mode: BlendMode,
6576 sample_mode: CompositeSampleMode,
6577 ) {
6578 if projective_dest_bounds_rect(dest_bounds).is_none() {
6579 return;
6580 }
6581 let device = self.renderer.device.clone();
6582 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
6583 "Shader Projective Composite Scratch",
6584 source.width,
6585 source.height,
6586 );
6587 let scratch = self
6588 .recorder
6589 .acquire_transient_offscreen(&device, scratch_descriptor);
6590 let shader_applied = {
6591 self.renderer.effect_renderer.encode_shader(
6592 self.recorder,
6593 &device,
6594 source,
6595 &scratch.view,
6596 shader,
6597 effect_rect,
6598 )
6599 };
6600 let composite_source = if shader_applied {
6601 self.renderer
6602 .effect_renderer
6603 .debug_effects
6604 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
6605 self.recorder.record_pass();
6606 &scratch
6607 } else {
6608 source
6609 };
6610 let composited = {
6611 self.renderer
6612 .effect_renderer
6613 .encode_composite_to_view_projective(
6614 self.recorder,
6615 &device,
6616 composite_source,
6617 dest_view,
6618 viewport,
6619 source_size,
6620 inverse_matrix,
6621 dest_bounds,
6622 alpha,
6623 load_op,
6624 scissor,
6625 supported_blend_mode(blend_mode),
6626 sample_mode,
6627 )
6628 };
6629 if composited {
6630 self.recorder.record_pass();
6631 self.renderer.effect_renderer.record_composite_pass();
6632 }
6633 self.recorder
6634 .release_transient_offscreen(scratch_descriptor, scratch);
6635 }
6636
6637 #[allow(clippy::too_many_arguments)]
6638 fn record_effect_with_direct_shader_tail_composite(
6639 &mut self,
6640 source: &OffscreenTarget,
6641 first_effect: &RenderEffect,
6642 shader: &RuntimeShader,
6643 effect_rect: [f32; 4],
6644 dest_view: &wgpu::TextureView,
6645 load_op: wgpu::LoadOp<wgpu::Color>,
6646 scissor: Option<(u32, u32, u32, u32)>,
6647 dest_viewport: (f32, f32, f32, f32),
6648 ) -> Result<bool, String> {
6649 let device = self.renderer.device.clone();
6650 let intermediate_descriptor = self.renderer.transient_offscreen_descriptor(
6651 "Render Effect Direct Shader Tail Intermediate",
6652 source.width,
6653 source.height,
6654 );
6655 let intermediate = self
6656 .recorder
6657 .acquire_transient_offscreen(&device, intermediate_descriptor);
6658 let effect_scratch_targets = self
6659 .renderer
6660 .effect_renderer
6661 .acquire_recorded_effect_scratch_targets(
6662 self.recorder,
6663 &device,
6664 first_effect,
6665 source.width,
6666 source.height,
6667 self.renderer.surface_format,
6668 );
6669 let first_passes = {
6670 let mut effect_scratch_refs = effect_scratch_targets.refs();
6671 let pass_count = self.renderer.effect_renderer.encode_effect(
6672 self.recorder,
6673 &device,
6674 source,
6675 &intermediate.view,
6676 first_effect,
6677 effect_rect,
6678 &mut effect_scratch_refs,
6679 );
6680 match pass_count {
6681 Ok(pass_count) => effect_scratch_refs.assert_consumed().map(|()| pass_count),
6682 Err(error) => Err(error),
6683 }
6684 };
6685 let first_passes = match first_passes {
6686 Ok(pass_count) => pass_count,
6687 Err(error) => {
6688 effect_scratch_targets.release_into(self.recorder);
6689 self.recorder
6690 .release_transient_offscreen(intermediate_descriptor, intermediate);
6691 return Err(error);
6692 }
6693 };
6694 let shader_applied = self
6695 .renderer
6696 .effect_renderer
6697 .encode_shader_src_over_to_view(
6698 self.recorder,
6699 &device,
6700 &intermediate,
6701 dest_view,
6702 shader,
6703 effect_rect,
6704 load_op,
6705 scissor,
6706 dest_viewport,
6707 );
6708 self.recorder
6709 .record_passes(first_passes.saturating_add(u32::from(shader_applied)));
6710 effect_scratch_targets.release_into(self.recorder);
6711 self.recorder
6712 .release_transient_offscreen(intermediate_descriptor, intermediate);
6713 if !shader_applied {
6714 return Ok(false);
6715 }
6716 self.renderer
6717 .effect_renderer
6718 .debug_effects
6719 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
6720 self.renderer.effect_renderer.record_composite_pass();
6721 Ok(true)
6722 }
6723
6724 #[allow(clippy::too_many_arguments)]
6725 fn record_effect_composite(
6726 &mut self,
6727 source: &OffscreenTarget,
6728 effect: &RenderEffect,
6729 effect_rect: [f32; 4],
6730 dest_view: &wgpu::TextureView,
6731 alpha: f32,
6732 load_op: wgpu::LoadOp<wgpu::Color>,
6733 scissor: Option<(u32, u32, u32, u32)>,
6734 blend_mode: BlendMode,
6735 dest_viewport: Option<(f32, f32, f32, f32)>,
6736 sample_mode: CompositeSampleMode,
6737 ) -> Result<(), String> {
6738 if let (
6739 RenderEffect::Chain { first, second },
6740 Some(viewport),
6741 BlendMode::SrcOver,
6742 CompositeSampleMode::Linear,
6743 ) = (
6744 effect,
6745 dest_viewport,
6746 supported_blend_mode(blend_mode),
6747 sample_mode,
6748 ) {
6749 if let (
6750 RenderEffect::Blur {
6751 radius_x,
6752 radius_y,
6753 edge_treatment,
6754 },
6755 RenderEffect::Shader { shader },
6756 ) = (first.as_ref(), second.as_ref())
6757 {
6758 if *radius_x > 0.0 || *radius_y > 0.0 {
6759 let device = self.renderer.device.clone();
6760 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
6761 "Blur Rounded Mask Scratch",
6762 source.width,
6763 source.height,
6764 );
6765 let scratch = self
6766 .recorder
6767 .acquire_transient_offscreen(&device, scratch_descriptor);
6768 let fused = self
6769 .renderer
6770 .effect_renderer
6771 .encode_blur_then_rounded_mask_src_over_to_view(
6772 self.recorder,
6773 &device,
6774 source,
6775 &scratch,
6776 dest_view,
6777 *radius_x,
6778 *radius_y,
6779 *edge_treatment,
6780 shader,
6781 effect_rect,
6782 load_op,
6783 scissor,
6784 viewport,
6785 );
6786 if fused {
6787 self.recorder.record_passes(2);
6788 self.renderer.effect_renderer.record_blur_pass();
6789 self.renderer
6790 .effect_renderer
6791 .debug_effects
6792 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
6793 self.renderer.effect_renderer.record_composite_pass();
6794 self.recorder
6795 .release_transient_offscreen(scratch_descriptor, scratch);
6796 return Ok(());
6797 }
6798 self.recorder
6799 .release_transient_offscreen(scratch_descriptor, scratch);
6800 }
6801 }
6802 }
6803 if let Some((first_effect, shader, viewport)) = direct_shader_tail_composite(
6804 effect,
6805 alpha,
6806 blend_mode,
6807 dest_viewport,
6808 sample_mode,
6809 (source.width, source.height),
6810 ) {
6811 if self.record_effect_with_direct_shader_tail_composite(
6812 source,
6813 first_effect,
6814 shader,
6815 effect_rect,
6816 dest_view,
6817 load_op,
6818 scissor,
6819 viewport,
6820 )? {
6821 return Ok(());
6822 }
6823 }
6824 let device = self.renderer.device.clone();
6825 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
6826 "Render Effect Composite Scratch",
6827 source.width,
6828 source.height,
6829 );
6830 let scratch = self
6831 .recorder
6832 .acquire_transient_offscreen(&device, scratch_descriptor);
6833 let effect_scratch_targets = self
6834 .renderer
6835 .effect_renderer
6836 .acquire_recorded_effect_scratch_targets(
6837 self.recorder,
6838 &device,
6839 effect,
6840 source.width,
6841 source.height,
6842 self.renderer.surface_format,
6843 );
6844 let effect_passes = {
6845 let mut effect_scratch_refs = effect_scratch_targets.refs();
6846 let pass_count = self.renderer.effect_renderer.encode_effect(
6847 self.recorder,
6848 &device,
6849 source,
6850 &scratch.view,
6851 effect,
6852 effect_rect,
6853 &mut effect_scratch_refs,
6854 )?;
6855 effect_scratch_refs.assert_consumed()?;
6856 Ok(pass_count)
6857 };
6858 let effect_passes = match effect_passes {
6859 Ok(pass_count) => pass_count,
6860 Err(error) => {
6861 effect_scratch_targets.release_into(self.recorder);
6862 self.recorder
6863 .release_transient_offscreen(scratch_descriptor, scratch);
6864 return Err(error);
6865 }
6866 };
6867 {
6868 self.renderer
6869 .effect_renderer
6870 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
6871 self.recorder,
6872 &device,
6873 &scratch,
6874 dest_view,
6875 alpha,
6876 load_op,
6877 scissor,
6878 None,
6879 supported_blend_mode(blend_mode),
6880 dest_viewport,
6881 sample_mode,
6882 );
6883 }
6884 self.recorder.record_passes(effect_passes.saturating_add(1));
6885 self.renderer.effect_renderer.record_composite_pass();
6886 effect_scratch_targets.release_into(self.recorder);
6887 self.recorder
6888 .release_transient_offscreen(scratch_descriptor, scratch);
6889 Ok(())
6890 }
6891
6892 #[allow(clippy::too_many_arguments)]
6893 fn record_effect_projective_composite(
6894 &mut self,
6895 source: &OffscreenTarget,
6896 effect: &RenderEffect,
6897 effect_rect: [f32; 4],
6898 dest_view: &wgpu::TextureView,
6899 viewport: (u32, u32),
6900 source_size: (f32, f32),
6901 inverse_matrix: [[f32; 3]; 3],
6902 dest_bounds: [[f32; 2]; 4],
6903 alpha: f32,
6904 load_op: wgpu::LoadOp<wgpu::Color>,
6905 scissor: Option<(u32, u32, u32, u32)>,
6906 blend_mode: BlendMode,
6907 sample_mode: CompositeSampleMode,
6908 ) -> Result<(), String> {
6909 if projective_dest_bounds_rect(dest_bounds).is_none() {
6910 return Ok(());
6911 }
6912 let device = self.renderer.device.clone();
6913 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
6914 "Render Effect Projective Composite Scratch",
6915 source.width,
6916 source.height,
6917 );
6918 let scratch = self
6919 .recorder
6920 .acquire_transient_offscreen(&device, scratch_descriptor);
6921 let effect_scratch_targets = self
6922 .renderer
6923 .effect_renderer
6924 .acquire_recorded_effect_scratch_targets(
6925 self.recorder,
6926 &device,
6927 effect,
6928 source.width,
6929 source.height,
6930 self.renderer.surface_format,
6931 );
6932 let effect_passes = {
6933 let mut effect_scratch_refs = effect_scratch_targets.refs();
6934 let pass_count = self.renderer.effect_renderer.encode_effect(
6935 self.recorder,
6936 &device,
6937 source,
6938 &scratch.view,
6939 effect,
6940 effect_rect,
6941 &mut effect_scratch_refs,
6942 )?;
6943 effect_scratch_refs.assert_consumed()?;
6944 Ok(pass_count)
6945 };
6946 let effect_passes = match effect_passes {
6947 Ok(pass_count) => pass_count,
6948 Err(error) => {
6949 effect_scratch_targets.release_into(self.recorder);
6950 self.recorder
6951 .release_transient_offscreen(scratch_descriptor, scratch);
6952 return Err(error);
6953 }
6954 };
6955 let composited = {
6956 self.renderer
6957 .effect_renderer
6958 .encode_composite_to_view_projective(
6959 self.recorder,
6960 &device,
6961 &scratch,
6962 dest_view,
6963 viewport,
6964 source_size,
6965 inverse_matrix,
6966 dest_bounds,
6967 alpha,
6968 load_op,
6969 scissor,
6970 supported_blend_mode(blend_mode),
6971 sample_mode,
6972 )
6973 };
6974 if composited {
6975 self.recorder.record_passes(effect_passes.saturating_add(1));
6976 self.renderer.effect_renderer.record_composite_pass();
6977 } else {
6978 self.recorder.record_passes(effect_passes);
6979 }
6980 effect_scratch_targets.release_into(self.recorder);
6981 self.recorder
6982 .release_transient_offscreen(scratch_descriptor, scratch);
6983 Ok(())
6984 }
6985}
6986
6987impl<C: FrameCommandRecorder> SurfaceExecutionBackend for RecordingSurfaceBackend<'_, '_, C> {
6988 fn max_texture_dim(&self) -> u32 {
6989 self.renderer.max_texture_dim()
6990 }
6991
6992 fn acquire_retained_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
6993 self.renderer.acquire_retained_surface(width, height)
6994 }
6995
6996 fn acquire_frame_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
6997 let descriptor =
6998 self.renderer
6999 .transient_offscreen_descriptor("Frame Surface", width, height);
7000 self.recorder
7001 .acquire_transient_offscreen(&self.renderer.device, descriptor)
7002 }
7003
7004 fn release_frame_surface(&mut self, target: OffscreenTarget) {
7005 let descriptor = self.renderer.transient_offscreen_descriptor(
7006 "Frame Surface",
7007 target.width,
7008 target.height,
7009 );
7010 self.recorder
7011 .release_transient_offscreen(descriptor, target);
7012 }
7013
7014 fn release_layer_surface_target(&mut self, target: LayerSurfaceTexture) {
7015 self.renderer.release_layer_surface_target(target);
7016 }
7017
7018 fn cached_layer_surface(
7019 &mut self,
7020 key: &LayerRasterCacheKey,
7021 ) -> Option<(Rc<OffscreenTarget>, Rect)> {
7022 self.renderer.cached_layer_surface(key)
7023 }
7024
7025 fn admit_layer_surface_cache_miss(&mut self, key: &LayerRasterCacheKey) -> bool {
7026 self.renderer.admit_layer_surface_cache_miss(key)
7027 }
7028
7029 fn insert_cached_layer_surface(
7030 &mut self,
7031 key: LayerRasterCacheKey,
7032 target: OffscreenTarget,
7033 logical_rect: Rect,
7034 ) -> Rc<OffscreenTarget> {
7035 self.renderer
7036 .insert_cached_layer_surface(key, target, logical_rect)
7037 }
7038
7039 fn clear_target_view_with_load_op(
7040 &mut self,
7041 target_view: &wgpu::TextureView,
7042 load_op: wgpu::LoadOp<wgpu::Color>,
7043 ) {
7044 {
7045 let _clear = self
7046 .recorder
7047 .encoder()
7048 .begin_render_pass(&wgpu::RenderPassDescriptor {
7049 label: Some("Layer Event Clear Pass"),
7050 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
7051 view: target_view,
7052 resolve_target: None,
7053 depth_slice: None,
7054 ops: wgpu::Operations {
7055 load: load_op,
7056 store: wgpu::StoreOp::Store,
7057 },
7058 })],
7059 depth_stencil_attachment: None,
7060 timestamp_writes: None,
7061 occlusion_query_set: None,
7062 multiview_mask: None,
7063 });
7064 }
7065 self.recorder.record_pass();
7066 }
7067
7068 #[allow(clippy::too_many_arguments)]
7069 fn render_non_effect_segment(
7070 &mut self,
7071 target_view: &wgpu::TextureView,
7072 shapes: &[DrawShape],
7073 brushes: &[Brush],
7074 images: &[ImageDraw],
7075 texts: &[TextDraw],
7076 shadow_draws: &[ShadowDraw],
7077 retained_draws: &[RetainedDraw],
7078 draw_ops: &[DrawOp],
7079 z_start: usize,
7080 z_end: usize,
7081 effect_z_ranges: &[Range<usize>],
7082 width: u32,
7083 height: u32,
7084 root_scale: f32,
7085 initial_load_op: wgpu::LoadOp<wgpu::Color>,
7086 ) -> Result<(), String> {
7087 self.render_non_effect_segment_with_composites(
7088 target_view,
7089 shapes,
7090 brushes,
7091 images,
7092 texts,
7093 shadow_draws,
7094 retained_draws,
7095 draw_ops,
7096 z_start,
7097 z_end,
7098 effect_z_ranges,
7099 &[],
7100 &[],
7101 width,
7102 height,
7103 root_scale,
7104 initial_load_op,
7105 )
7106 }
7107
7108 #[allow(clippy::too_many_arguments)]
7109 fn render_non_effect_segment_with_composites(
7110 &mut self,
7111 target_view: &wgpu::TextureView,
7112 shapes: &[DrawShape],
7113 brushes: &[Brush],
7114 images: &[ImageDraw],
7115 texts: &[TextDraw],
7116 shadow_draws: &[ShadowDraw],
7117 retained_draws: &[RetainedDraw],
7118 draw_ops: &[DrawOp],
7119 z_start: usize,
7120 z_end: usize,
7121 effect_z_ranges: &[Range<usize>],
7122 composites: &[(usize, CompositeBatchItem<'_>)],
7123 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
7124 width: u32,
7125 height: u32,
7126 root_scale: f32,
7127 initial_load_op: wgpu::LoadOp<wgpu::Color>,
7128 ) -> Result<(), String> {
7129 let mut ordered_items = std::mem::take(&mut self.renderer.scratch_segment_items);
7130 collect_non_effect_segment_items(
7131 shapes,
7132 images,
7133 texts,
7134 shadow_draws,
7135 draw_ops,
7136 z_start,
7137 z_end,
7138 effect_z_ranges,
7139 width,
7140 height,
7141 root_scale,
7142 &mut ordered_items,
7143 );
7144 #[cfg(not(target_arch = "wasm32"))]
7145 let raw_shadow_items = ordered_items
7146 .iter()
7147 .filter(|(_, item)| matches!(item, SegmentDrawItem::Shadow(_)))
7148 .count();
7149 let culled_shadow_items = retain_renderable_shadow_items(
7150 &mut ordered_items,
7151 shadow_draws,
7152 width,
7153 height,
7154 root_scale,
7155 self.renderer.max_texture_dim(),
7156 );
7157 #[cfg(target_arch = "wasm32")]
7158 let _ = culled_shadow_items;
7159 let mut cached_shadow_composites: Vec<(usize, CachedShadowComposite)> = Vec::new();
7160 ordered_items.extend(
7161 composites
7162 .iter()
7163 .enumerate()
7164 .map(|(index, (z_index, _))| (*z_index, SegmentDrawItem::Composite(index))),
7165 );
7166 ordered_items.extend(
7167 shader_composites
7168 .iter()
7169 .enumerate()
7170 .map(|(index, (z_index, _))| (*z_index, SegmentDrawItem::ShaderComposite(index))),
7171 );
7172 for (z_index, item) in &mut ordered_items {
7173 let SegmentDrawItem::Shadow(shadow_index) = *item else {
7174 continue;
7175 };
7176 let Some(composite) = self.renderer.cached_shape_shadow_composite(
7177 &shadow_draws[shadow_index],
7178 width,
7179 height,
7180 root_scale,
7181 ) else {
7182 continue;
7183 };
7184 let composite_index = composites.len() + cached_shadow_composites.len();
7185 cached_shadow_composites.push((*z_index, composite));
7186 *item = SegmentDrawItem::Composite(composite_index);
7187 }
7188 let mut merged_composites = Vec::with_capacity(
7189 composites
7190 .len()
7191 .saturating_add(cached_shadow_composites.len()),
7192 );
7193 merged_composites.extend(composites.iter().copied());
7194 merged_composites.extend(
7195 cached_shadow_composites
7196 .iter()
7197 .map(|(z_index, composite)| (*z_index, composite.batch_item())),
7198 );
7199 ordered_items.sort_unstable_by_key(|(z_index, _)| *z_index);
7203 #[cfg(not(target_arch = "wasm32"))]
7204 maybe_print_segment_diag(
7205 z_start..z_end,
7206 &ordered_items,
7207 shapes,
7208 brushes,
7209 images,
7210 SegmentDiagCounts {
7211 raw_shadow_items,
7212 culled_shadow_items,
7213 cached_shadow_composites: cached_shadow_composites.len(),
7214 composite_items: merged_composites.len(),
7215 shader_composite_items: shader_composites.len(),
7216 },
7217 self.renderer.shape_batch_limits,
7218 );
7219 let result = if ordered_items.is_empty() {
7220 Ok(SegmentCommandEncodeOutcome { first_batch: true })
7221 } else {
7222 self.renderer.encode_non_effect_segment_commands(
7223 self.recorder,
7224 target_view,
7225 &ordered_items,
7226 &merged_composites,
7227 shader_composites,
7228 shapes,
7229 brushes,
7230 images,
7231 texts,
7232 shadow_draws,
7233 retained_draws,
7234 initial_load_op,
7235 width,
7236 height,
7237 root_scale,
7238 )
7239 };
7240 self.renderer.scratch_segment_items = ordered_items;
7241 let outcome = result?;
7242 if outcome.first_batch && matches!(initial_load_op, wgpu::LoadOp::Clear(_)) {
7243 self.clear_target_view_with_load_op(target_view, initial_load_op);
7244 }
7245 Ok(())
7246 }
7247
7248 fn render_range_with_layer_events_to_target(
7249 &mut self,
7250 target: &OffscreenTarget,
7251 shapes: &[DrawShape],
7252 brushes: &[Brush],
7253 images: &[ImageDraw],
7254 texts: &[TextDraw],
7255 shadow_draws: &[ShadowDraw],
7256 draw_ops: &[DrawOp],
7257 effect_layers: &[EffectLayer],
7258 backdrop_layers: &[BackdropLayer],
7259 z_start: usize,
7260 z_end: usize,
7261 excluded_effect_layer: Option<usize>,
7262 width: u32,
7263 height: u32,
7264 root_scale: f32,
7265 backdrop_underlay: Option<&OffscreenTarget>,
7266 initial_load_op: wgpu::LoadOp<wgpu::Color>,
7267 ) -> Result<(), String> {
7268 self.render_range_with_layer_events_to_target_recorded(
7269 target,
7270 shapes,
7271 brushes,
7272 images,
7273 texts,
7274 shadow_draws,
7275 draw_ops,
7276 effect_layers,
7277 backdrop_layers,
7278 z_start,
7279 z_end,
7280 excluded_effect_layer,
7281 width,
7282 height,
7283 root_scale,
7284 backdrop_underlay,
7285 initial_load_op,
7286 )
7287 }
7288
7289 fn render_shadow_draw(
7290 &mut self,
7291 target_view: &wgpu::TextureView,
7292 shadow: &ShadowDraw,
7293 width: u32,
7294 height: u32,
7295 root_scale: f32,
7296 ) {
7297 self.renderer.encode_shadow_draw(
7298 self.recorder,
7299 target_view,
7300 shadow,
7301 width,
7302 height,
7303 root_scale,
7304 );
7305 }
7306
7307 fn composite_to_view_projective(
7308 &mut self,
7309 source: &OffscreenTarget,
7310 dest_view: &wgpu::TextureView,
7311 viewport: (u32, u32),
7312 source_size: (f32, f32),
7313 inverse_matrix: [[f32; 3]; 3],
7314 dest_bounds: [[f32; 2]; 4],
7315 alpha: f32,
7316 load_op: wgpu::LoadOp<wgpu::Color>,
7317 scissor: Option<(u32, u32, u32, u32)>,
7318 blend_mode: BlendMode,
7319 sample_mode: CompositeSampleMode,
7320 ) {
7321 let device = self.renderer.device.clone();
7322 let composited = {
7323 self.renderer
7324 .effect_renderer
7325 .encode_composite_to_view_projective(
7326 self.recorder,
7327 &device,
7328 source,
7329 dest_view,
7330 viewport,
7331 source_size,
7332 inverse_matrix,
7333 dest_bounds,
7334 alpha,
7335 load_op,
7336 scissor,
7337 supported_blend_mode(blend_mode),
7338 sample_mode,
7339 )
7340 };
7341 if composited {
7342 self.recorder.record_pass();
7343 self.renderer.effect_renderer.record_composite_pass();
7344 }
7345 }
7346
7347 fn composite_projective_surfaces_to_view(
7348 &mut self,
7349 dest_view: &wgpu::TextureView,
7350 viewport: (u32, u32),
7351 composites: &[ProjectiveSurfaceComposite<'_>],
7352 ) {
7353 let device = self.renderer.device.clone();
7354 let mut composite_count = 0_u32;
7355 for composite in composites
7356 .iter()
7357 .copied()
7358 .filter(|composite| projective_dest_bounds_rect(composite.dest_bounds).is_some())
7359 {
7360 let composited = {
7361 self.renderer
7362 .effect_renderer
7363 .encode_composite_to_view_projective(
7364 self.recorder,
7365 &device,
7366 composite.source,
7367 dest_view,
7368 viewport,
7369 composite.source_size,
7370 composite.inverse_matrix,
7371 composite.dest_bounds,
7372 composite.alpha,
7373 composite.load_op,
7374 composite.scissor,
7375 supported_blend_mode(composite.blend_mode),
7376 composite.sample_mode,
7377 )
7378 };
7379 if composited {
7380 composite_count = composite_count.saturating_add(1);
7381 }
7382 }
7383 if composite_count > 0 {
7384 self.recorder.record_passes(composite_count);
7385 self.renderer
7386 .effect_renderer
7387 .debug_composites
7388 .set(self.renderer.effect_renderer.debug_composites.get() + composite_count);
7389 }
7390 }
7391
7392 fn composite_surface_batch_to_view(
7393 &mut self,
7394 dest_view: &wgpu::TextureView,
7395 viewport: (u32, u32),
7396 load_op: wgpu::LoadOp<wgpu::Color>,
7397 composites: &[CompositeBatchItem<'_>],
7398 ) {
7399 if composites.is_empty() {
7400 return;
7401 }
7402 let device = self.renderer.device.clone();
7403 self.renderer
7404 .effect_renderer
7405 .encode_composite_batch_to_view_pass(
7406 self.recorder,
7407 &device,
7408 dest_view,
7409 viewport,
7410 load_op,
7411 composites,
7412 );
7413 self.recorder.record_pass();
7414 self.renderer.effect_renderer.record_composite_pass();
7415 }
7416
7417 fn copy_texture_region_to_target(
7418 &mut self,
7419 source: &OffscreenTarget,
7420 source_origin: (u32, u32),
7421 target: &OffscreenTarget,
7422 size: (u32, u32),
7423 ) -> bool {
7424 let (width, height) = size;
7425 if width == 0 || height == 0 || width > target.width || height > target.height {
7426 return false;
7427 }
7428 let Some(source_right) = source_origin.0.checked_add(width) else {
7429 return false;
7430 };
7431 let Some(source_bottom) = source_origin.1.checked_add(height) else {
7432 return false;
7433 };
7434 if source_right > source.width || source_bottom > source.height {
7435 return false;
7436 }
7437
7438 self.recorder.encoder().copy_texture_to_texture(
7439 wgpu::TexelCopyTextureInfo {
7440 texture: source.texture(),
7441 mip_level: 0,
7442 origin: wgpu::Origin3d {
7443 x: source_origin.0,
7444 y: source_origin.1,
7445 z: 0,
7446 },
7447 aspect: wgpu::TextureAspect::All,
7448 },
7449 wgpu::TexelCopyTextureInfo {
7450 texture: target.texture(),
7451 mip_level: 0,
7452 origin: wgpu::Origin3d::ZERO,
7453 aspect: wgpu::TextureAspect::All,
7454 },
7455 wgpu::Extent3d {
7456 width,
7457 height,
7458 depth_or_array_layers: 1,
7459 },
7460 );
7461 true
7462 }
7463
7464 fn shader_composite_batch_to_view(
7465 &mut self,
7466 dest_view: &wgpu::TextureView,
7467 viewport: (u32, u32),
7468 load_op: wgpu::LoadOp<wgpu::Color>,
7469 composites: &[ShaderCompositeBatchItem<'_>],
7470 ) -> bool {
7471 if composites.is_empty() {
7472 return true;
7473 }
7474 let device = self.renderer.device.clone();
7475 let encoded = self
7476 .renderer
7477 .effect_renderer
7478 .encode_shader_batch_src_over_to_view(
7479 self.recorder,
7480 &device,
7481 dest_view,
7482 viewport,
7483 load_op,
7484 composites,
7485 );
7486 if encoded {
7487 self.recorder.record_pass();
7488 self.renderer.effect_renderer.record_composite_pass();
7489 self.renderer
7490 .effect_renderer
7491 .debug_effects
7492 .set(self.renderer.effect_renderer.debug_effects.get() + composites.len() as u32);
7493 }
7494 encoded
7495 }
7496
7497 fn composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
7498 &mut self,
7499 source: &OffscreenTarget,
7500 dest_view: &wgpu::TextureView,
7501 alpha: f32,
7502 load_op: wgpu::LoadOp<wgpu::Color>,
7503 scissor: Option<(u32, u32, u32, u32)>,
7504 rounded_mask: Option<RoundedCompositeMask>,
7505 blend_mode: BlendMode,
7506 dest_viewport: Option<(f32, f32, f32, f32)>,
7507 sample_mode: CompositeSampleMode,
7508 ) {
7509 let device = self.renderer.device.clone();
7510 {
7511 self.renderer
7512 .effect_renderer
7513 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
7514 self.recorder,
7515 &device,
7516 source,
7517 dest_view,
7518 alpha,
7519 load_op,
7520 scissor,
7521 rounded_mask,
7522 supported_blend_mode(blend_mode),
7523 dest_viewport,
7524 sample_mode,
7525 );
7526 }
7527 self.recorder.record_pass();
7528 self.renderer.effect_renderer.record_composite_pass();
7529 }
7530
7531 fn apply_effect_and_composite_to_view(
7532 &mut self,
7533 source: &OffscreenTarget,
7534 effect: &RenderEffect,
7535 effect_rect: [f32; 4],
7536 dest_view: &wgpu::TextureView,
7537 alpha: f32,
7538 load_op: wgpu::LoadOp<wgpu::Color>,
7539 scissor: Option<(u32, u32, u32, u32)>,
7540 blend_mode: BlendMode,
7541 dest_viewport: Option<(f32, f32, f32, f32)>,
7542 sample_mode: CompositeSampleMode,
7543 ) -> Result<(), String> {
7544 self.record_effect_composite(
7545 source,
7546 effect,
7547 effect_rect,
7548 dest_view,
7549 alpha,
7550 load_op,
7551 scissor,
7552 blend_mode,
7553 dest_viewport,
7554 sample_mode,
7555 )
7556 }
7557
7558 fn apply_shader_and_composite_to_view(
7559 &mut self,
7560 source: &OffscreenTarget,
7561 shader: &RuntimeShader,
7562 effect_rect: [f32; 4],
7563 dest_view: &wgpu::TextureView,
7564 alpha: f32,
7565 load_op: wgpu::LoadOp<wgpu::Color>,
7566 scissor: Option<(u32, u32, u32, u32)>,
7567 blend_mode: BlendMode,
7568 dest_viewport: Option<(f32, f32, f32, f32)>,
7569 sample_mode: CompositeSampleMode,
7570 ) {
7571 self.record_shader_composite(
7572 source,
7573 shader,
7574 effect_rect,
7575 dest_view,
7576 alpha,
7577 load_op,
7578 scissor,
7579 blend_mode,
7580 dest_viewport,
7581 sample_mode,
7582 );
7583 }
7584
7585 fn apply_shader_and_composite_to_view_projective(
7586 &mut self,
7587 source: &OffscreenTarget,
7588 shader: &RuntimeShader,
7589 effect_rect: [f32; 4],
7590 dest_view: &wgpu::TextureView,
7591 viewport: (u32, u32),
7592 source_size: (f32, f32),
7593 inverse_matrix: [[f32; 3]; 3],
7594 dest_bounds: [[f32; 2]; 4],
7595 alpha: f32,
7596 load_op: wgpu::LoadOp<wgpu::Color>,
7597 scissor: Option<(u32, u32, u32, u32)>,
7598 blend_mode: BlendMode,
7599 sample_mode: CompositeSampleMode,
7600 ) {
7601 self.record_shader_projective_composite(
7602 source,
7603 shader,
7604 effect_rect,
7605 dest_view,
7606 viewport,
7607 source_size,
7608 inverse_matrix,
7609 dest_bounds,
7610 alpha,
7611 load_op,
7612 scissor,
7613 blend_mode,
7614 sample_mode,
7615 );
7616 }
7617
7618 fn apply_effect_and_composite_to_view_projective(
7619 &mut self,
7620 source: &OffscreenTarget,
7621 effect: &RenderEffect,
7622 effect_rect: [f32; 4],
7623 dest_view: &wgpu::TextureView,
7624 viewport: (u32, u32),
7625 source_size: (f32, f32),
7626 inverse_matrix: [[f32; 3]; 3],
7627 dest_bounds: [[f32; 2]; 4],
7628 alpha: f32,
7629 load_op: wgpu::LoadOp<wgpu::Color>,
7630 scissor: Option<(u32, u32, u32, u32)>,
7631 blend_mode: BlendMode,
7632 sample_mode: CompositeSampleMode,
7633 ) -> Result<(), String> {
7634 self.record_effect_projective_composite(
7635 source,
7636 effect,
7637 effect_rect,
7638 dest_view,
7639 viewport,
7640 source_size,
7641 inverse_matrix,
7642 dest_bounds,
7643 alpha,
7644 load_op,
7645 scissor,
7646 blend_mode,
7647 sample_mode,
7648 )
7649 }
7650
7651 fn is_render_effect_supported(&self, effect: &RenderEffect) -> bool {
7652 self.renderer.supports_render_effect(effect)
7653 }
7654
7655 fn warn_unsupported_effect_once(&self) {
7656 self.renderer.warning_state.warn_unsupported_effect_once();
7657 }
7658
7659 fn record_layer_cache_miss(&self, width: u32, height: u32) {
7660 self.renderer
7661 .frame_stats
7662 .record_layer_cache_miss(width, height);
7663 }
7664
7665 fn record_isolated_layer_render(
7666 &self,
7667 width: u32,
7668 height: u32,
7669 node_id: Option<NodeId>,
7670 logical_rect: Rect,
7671 requirements: SurfaceRequirementSet,
7672 ) {
7673 self.renderer.frame_stats.record_isolated_layer_render(
7674 width,
7675 height,
7676 node_id,
7677 logical_rect,
7678 requirements.into(),
7679 );
7680 }
7681}
7682
7683impl GpuRenderer {
7684 pub fn render(
7685 &mut self,
7686 view: &wgpu::TextureView,
7687 width: u32,
7688 height: u32,
7689 packet: FramePacket,
7690 surface_epoch: u64,
7691 returns: &mut RenderReturns,
7692 ) -> Result<(), String> {
7693 let cancel_reason = if packet.renderer_epoch != self.renderer_epoch {
7699 Some(CancelReason::RendererEpoch)
7700 } else if packet.surface_epoch != surface_epoch {
7701 Some(CancelReason::SurfaceEpoch)
7702 } else if packet.viewport != (width, height) {
7703 Some(CancelReason::Viewport)
7704 } else {
7705 None
7706 };
7707 if let Some(reason) = cancel_reason {
7708 return Self::cancel_packet(packet, reason, returns);
7709 }
7710 returns.frame_id = packet.frame_id;
7711 log::trace!("🎨 Rendering graph to {}x{}", width, height);
7712 let render_start = Instant::now();
7713
7714 #[cfg(target_arch = "wasm32")]
7715 {
7716 self.wasm_uniform_batch_cursor = 0;
7717 self.wasm_shape_batch_cursor = 0;
7718 self.wasm_image_batch_cursor = 0;
7719 }
7720 #[cfg(not(target_arch = "wasm32"))]
7721 {
7722 self.retained_glyph_uniform_cursor = 0;
7723 self.rim_mesh_vertices.clear();
7728 self.rim_mesh_indices.clear();
7729 self.rim_mesh_uploaded_vertices = 0;
7730 self.rim_mesh_uploaded_indices = 0;
7731 if fill_area_diag_enabled() {
7732 self.fill_area_diag.reset_frame(width, height);
7733 }
7734 self.static_span.armed = true;
7737 }
7738
7739 let text_cache_len = packet.text_cache_len;
7743 let result = self.render_graph(view, packet, returns);
7744 let after_graph = Instant::now();
7745 self.flush_deferred_offscreen_releases();
7746 #[cfg(not(target_arch = "wasm32"))]
7747 {
7748 if fill_area_diag_enabled() {
7749 let (composite_px2, offscreen_px2) = self.effect_renderer.take_fill_diag_fill_px2();
7753 self.fill_area_diag
7754 .add_effect_fill(composite_px2, offscreen_px2);
7755 self.fill_area_diag.finish_frame(width, height);
7756 }
7757 }
7758
7759 #[cfg(target_arch = "wasm32")]
7760 {
7761 const WASM_BATCH_POOL_MARGIN: usize = 4;
7762 self.wasm_uniform_batches.truncate(
7763 self.wasm_uniform_batch_cursor
7764 .saturating_add(WASM_BATCH_POOL_MARGIN),
7765 );
7766 self.wasm_shape_batches.truncate(
7767 self.wasm_shape_batch_cursor
7768 .saturating_add(WASM_BATCH_POOL_MARGIN),
7769 );
7770 self.wasm_image_batches.truncate(
7771 self.wasm_image_batch_cursor
7772 .saturating_add(WASM_BATCH_POOL_MARGIN),
7773 );
7774 }
7775 self.staged_uploads
7776 .shrink_retained_capacity(RETAINED_STAGED_UPLOAD_BYTES, RETAINED_STAGED_UPLOAD_COPIES);
7777
7778 self.layer_surface_cache.finish_frame(&self.frame_stats);
7779 #[cfg(not(target_arch = "wasm32"))]
7780 self.retained_bundle_cache.end_frame();
7781
7782 self.frame_stats.offscreen_pool_size.set(
7783 self.effect_renderer
7784 .retained_offscreen_count()
7785 .saturating_add(self.frame_graph_executor.retained_texture_count())
7786 as u32,
7787 );
7788 self.frame_stats.offscreen_pool_bytes.set(
7789 (self.effect_renderer.retained_offscreen_bytes() as u64)
7790 .saturating_add(self.frame_graph_executor.retained_texture_bytes()),
7791 );
7792 self.frame_stats
7793 .text_pool_size
7794 .set(self.text_image_cache.len() as u32);
7795 self.frame_stats
7796 .image_cache_size
7797 .set(self.image_texture_cache.len() as u32);
7798 self.frame_stats.text_cache_size.set(text_cache_len as u32);
7799 self.effect_renderer
7800 .merge_and_reset_debug_counters(&self.frame_stats);
7801 self.frame_graph_executor.reset_upload_allocators();
7802 let snapshot = self.frame_stats.snapshot();
7803 self.last_frame_stats = Some(snapshot);
7804 PRESENTED_FRAMES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
7805 update_frame_warmup_budget(&mut self.pending_frame_warmup_frames, &snapshot);
7806 self.frame_stats.maybe_print_snapshot(
7807 snapshot,
7808 &mut self.frame_count,
7809 self.gpu_stats_enabled,
7810 );
7811 if self.gpu_stats_enabled && self.frame_count.is_multiple_of(60) {
7812 gpu_stats::print_gpu_memory_report(&self.device, self.frame_count);
7813 }
7814 self.frame_stats.reset();
7815 let after_stats = Instant::now();
7816 if let Some(total_ms) = should_log_wgpu_render_stage(render_start, after_stats) {
7817 log::warn!(
7818 "[wgpu-render-stage:render] total_ms={total_ms:.2} graph_ms={:.2} cleanup_stats_ms={:.2}",
7819 instant_ms(render_start, after_graph),
7820 instant_ms(after_graph, after_stats),
7821 );
7822 }
7823 if result.is_ok() {
7824 returns.outcome = PresentOutcome::Presented;
7827 }
7828 result
7829 }
7830
7831 fn cancel_packet(
7838 packet: FramePacket,
7839 reason: CancelReason,
7840 returns: &mut RenderReturns,
7841 ) -> Result<(), String> {
7842 let FramePacket {
7843 frame_id,
7844 viewport: _,
7845 renderer_epoch: _,
7846 surface_epoch: _,
7847 root_scale: _,
7848 root,
7849 overlay: _,
7850 replay,
7851 text_cache_len: _,
7852 } = packet;
7853 match root {
7854 PacketRoot::Direct(root) => {
7855 returns.scene = Some(root.scene);
7862 #[cfg(not(target_arch = "wasm32"))]
7863 {
7864 returns.cancelled_replay = Some(replay);
7865 }
7866 }
7867 PacketRoot::Surface(_) => {}
7868 }
7869 #[cfg(target_arch = "wasm32")]
7870 let _ = replay;
7871 returns.ack = None;
7872 returns.frame_id = frame_id;
7873 returns.outcome = PresentOutcome::Cancelled(reason);
7874 Ok(())
7875 }
7876
7877 pub fn last_frame_stats(&self) -> Option<gpu_stats::FrameStatsSnapshot> {
7878 self.last_frame_stats
7879 }
7880
7881 pub fn needs_frame_warmup(&self) -> bool {
7882 self.pending_frame_warmup_frames > 0
7883 }
7884
7885 pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
7886 let layer_surface_cache_stats = self.layer_surface_cache.debug_stats();
7887 DebugCpuAllocationStats {
7888 scene_graph_node_count: 0,
7889 scene_graph_heap_bytes: 0,
7890 scene_hits_len: 0,
7891 scene_hits_cap: 0,
7892 scene_node_index_len: 0,
7893 scene_node_index_cap: 0,
7894 text_renderer_pool_len: self.text_image_cache.len(),
7895 text_renderer_pool_cap: self.text_image_cache.cap().get(),
7896 swash_image_cache_len: 0,
7897 swash_image_cache_cap: 0,
7898 swash_outline_cache_len: 0,
7899 swash_outline_cache_cap: 0,
7900 image_texture_cache_len: self.image_texture_cache.len(),
7901 image_texture_cache_cap: self.image_texture_cache.cap().get(),
7902 scratch_shape_data_cap: self.scratch_shape_data.capacity(),
7903 scratch_gradients_cap: self.scratch_gradients.capacity(),
7904 scratch_image_vertices_cap: self.scratch_image_vertices.capacity(),
7905 scratch_image_indices_cap: self.scratch_image_indices.capacity(),
7906 scratch_image_cmds_cap: self.scratch_image_cmds.capacity(),
7907 scratch_segment_items_cap: self.scratch_segment_items.capacity(),
7908 scratch_effect_ranges_cap: self.scratch_effect_ranges.capacity(),
7909 scratch_layer_events_cap: self.scratch_layer_events.capacity(),
7910 staged_upload_bytes_cap: self.staged_uploads.bytes.capacity(),
7911 staged_upload_copies_cap: self.staged_uploads.copies.capacity(),
7912 layer_surface_cache_len: layer_surface_cache_stats.entries_len,
7913 layer_surface_cache_cap: layer_surface_cache_stats.entries_cap,
7914 layer_surface_cache_identity_len: layer_surface_cache_stats.identity_len,
7915 layer_surface_cache_identity_cap: layer_surface_cache_stats.identity_cap,
7916 layer_surface_rect_cache_len: 0,
7919 layer_surface_rect_cache_cap: 0,
7920 layer_surface_requirements_cache_len: 0,
7921 layer_surface_requirements_cache_cap: 0,
7922 layer_cache_seen_this_frame_len: layer_surface_cache_stats.seen_this_frame_len,
7923 layer_cache_seen_this_frame_cap: layer_surface_cache_stats.seen_this_frame_cap,
7924 }
7925 }
7926
7927 pub fn render_to_rgba_pixels(
7928 &mut self,
7929 width: u32,
7930 height: u32,
7931 packet: FramePacket,
7932 surface_epoch: u64,
7933 returns: &mut RenderReturns,
7934 ) -> Result<Vec<u8>, String> {
7935 if width == 0 || height == 0 {
7936 return Err("Screenshot size must be non-zero".to_string());
7937 }
7938
7939 let output_texture = self.device.create_texture(&wgpu::TextureDescriptor {
7940 label: Some("Screenshot Output Texture"),
7941 size: wgpu::Extent3d {
7942 width,
7943 height,
7944 depth_or_array_layers: 1,
7945 },
7946 mip_level_count: 1,
7947 sample_count: 1,
7948 dimension: wgpu::TextureDimension::D2,
7949 format: self.surface_format,
7950 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
7951 view_formats: &[],
7952 });
7953 let output_view = output_texture.create_view(&wgpu::TextureViewDescriptor::default());
7954
7955 self.render(&output_view, width, height, packet, surface_epoch, returns)?;
7956
7957 let bytes_per_pixel = 4u32;
7958 let unpadded_bytes_per_row = width
7959 .checked_mul(bytes_per_pixel)
7960 .ok_or_else(|| "Screenshot row byte size overflow".to_string())?;
7961 let padded_bytes_per_row =
7962 align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
7963 let output_buffer_size = padded_bytes_per_row as u64 * height as u64;
7964
7965 let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
7966 label: Some("Screenshot Readback Buffer"),
7967 size: output_buffer_size,
7968 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
7969 mapped_at_creation: false,
7970 });
7971
7972 let device = self.device.clone();
7973 let queue = self.queue.clone();
7974 let mut graph = WgpuFrameGraph::new(Some("Screenshot Copy Encoder"));
7975 let source = graph.import_surface("screenshot-copy-source");
7976 graph.add_fallible_command_pass(Some("Screenshot Copy Pass"), &[source], &[], |context| {
7977 context.encoder.copy_texture_to_buffer(
7978 wgpu::TexelCopyTextureInfo {
7979 texture: &output_texture,
7980 mip_level: 0,
7981 origin: wgpu::Origin3d::ZERO,
7982 aspect: wgpu::TextureAspect::All,
7983 },
7984 wgpu::TexelCopyBufferInfo {
7985 buffer: &output_buffer,
7986 layout: wgpu::TexelCopyBufferLayout {
7987 offset: 0,
7988 bytes_per_row: Some(padded_bytes_per_row),
7989 rows_per_image: Some(height),
7990 },
7991 },
7992 wgpu::Extent3d {
7993 width,
7994 height,
7995 depth_or_array_layers: 1,
7996 },
7997 );
7998 Ok(())
7999 });
8000 let mut executor = std::mem::take(&mut self.frame_graph_executor);
8001 let execution = executor.execute_recorded_graph(&device, &queue, graph);
8002 self.frame_graph_executor = executor;
8003 let execution = execution.map_err(|error| error.to_string())?;
8004 let submission_index = execution.submission;
8005 let copy_stats = execution.stats;
8006 self.last_frame_stats = self
8007 .last_frame_stats
8008 .map(|snapshot| snapshot.with_command_stats_added(copy_stats));
8009
8010 let buffer_slice = output_buffer.slice(..);
8011 let (tx, rx) = mpsc::channel();
8012 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
8013 let _ = tx.send(result);
8014 });
8015 let _ = self.device.poll(wgpu::PollType::Wait {
8016 submission_index: Some(submission_index),
8017 timeout: None,
8018 });
8019
8020 match rx.recv_timeout(Duration::from_secs(3)) {
8021 Ok(Ok(())) => {}
8022 Ok(Err(err)) => return Err(format!("Screenshot map_async failed: {err:?}")),
8023 Err(err) => return Err(format!("Screenshot readback timed out: {err}")),
8024 }
8025
8026 let mapped = buffer_slice.get_mapped_range();
8027 let mut pixels = vec![0u8; (width as usize) * (height as usize) * 4];
8028
8029 let src_row_len = padded_bytes_per_row as usize;
8030 let dst_row_len = unpadded_bytes_per_row as usize;
8031 for row in 0..height as usize {
8032 let src_offset = row * src_row_len;
8033 let dst_offset = row * dst_row_len;
8034 pixels[dst_offset..dst_offset + dst_row_len]
8035 .copy_from_slice(&mapped[src_offset..src_offset + dst_row_len]);
8036 }
8037 drop(mapped);
8038 output_buffer.unmap();
8039
8040 self.convert_surface_pixels_to_rgba(&mut pixels)?;
8041 Ok(pixels)
8042 }
8043
8044 fn render_graph(
8045 &mut self,
8046 surface_view: &wgpu::TextureView,
8047 packet: FramePacket,
8048 returns: &mut RenderReturns,
8049 ) -> Result<(), String> {
8050 let device = self.device.clone();
8051 let queue = self.queue.clone();
8052 let graph_start = Instant::now();
8053
8054 #[cfg(not(target_arch = "wasm32"))]
8055 {
8056 let mut executor = std::mem::take(&mut self.frame_graph_executor);
8057 let mut frame_graph = WgpuFrameGraph::new(Some("Renderer Frame Graph"));
8058 let surface = frame_graph.import_surface("renderer-surface");
8059 frame_graph.add_fallible_recorded_command_pass(
8060 Some("Renderer Frame Pass"),
8061 &[],
8062 &[surface],
8063 |frame_encoder| {
8064 self.render_graph_recorded(surface_view, packet, returns, frame_encoder)
8065 },
8066 );
8067 let after_build = Instant::now();
8068 let execution = executor.execute_recorded_graph(&device, &queue, frame_graph);
8069 let after_execute = Instant::now();
8070 self.frame_graph_executor = executor;
8071 if let Some(total_ms) = should_log_wgpu_render_stage(graph_start, after_execute) {
8072 log::warn!(
8073 "[wgpu-render-stage:graph] total_ms={total_ms:.2} build_ms={:.2} execute_ms={:.2}",
8074 instant_ms(graph_start, after_build),
8075 instant_ms(after_build, after_execute),
8076 );
8077 }
8078
8079 match execution {
8080 Ok(execution) => {
8081 if execution.stats.pass_count > 0 {
8082 self.frame_stats.record_command_stats(execution.stats);
8083 }
8084 Ok(())
8085 }
8086 Err(crate::frame_graph::FrameGraphError::NoDeclaredPasses) => Ok(()),
8087 Err(error) => Err(error.to_string()),
8088 }
8089 }
8090
8091 #[cfg(target_arch = "wasm32")]
8092 {
8093 let mut executor = std::mem::take(&mut self.frame_graph_executor);
8094 let (result, execution) = {
8095 let mut frame_encoder =
8096 executor.begin(&device, &queue, Some("Renderer Frame Encoder"));
8097 let initial_pass_count = frame_encoder.recorded_pass_count();
8098 let result =
8099 self.render_graph_recorded(surface_view, packet, returns, &mut frame_encoder);
8100 let execution =
8101 if result.is_ok() && frame_encoder.recorded_pass_count() > initial_pass_count {
8102 Some(frame_encoder.finish())
8103 } else {
8104 None
8105 };
8106 (result, execution)
8107 };
8108 let after_execute = Instant::now();
8109 self.frame_graph_executor = executor;
8110 if let Some(total_ms) = should_log_wgpu_render_stage(graph_start, after_execute) {
8111 log::warn!("[wgpu-render-stage:graph] total_ms={total_ms:.2}",);
8112 }
8113 if let Some(execution) = execution {
8114 self.frame_stats.record_command_stats(execution.stats);
8115 }
8116 result
8117 }
8118 }
8119
8120 fn render_graph_recorded<C: FrameCommandRecorder>(
8121 &mut self,
8122 surface_view: &wgpu::TextureView,
8123 packet: FramePacket,
8124 returns: &mut RenderReturns,
8125 frame_encoder: &mut C,
8126 ) -> Result<(), String> {
8127 let recorded_start = Instant::now();
8128
8129 #[cfg(not(target_arch = "wasm32"))]
8141 let mut packet = packet;
8142 #[cfg(not(target_arch = "wasm32"))]
8143 if let PacketRoot::Direct(root) = &packet.root {
8144 let ops = std::mem::take(&mut packet.replay);
8145 let (ack, recycled) = self.consume_replay_ops(
8146 ops,
8147 &root.scene.shapes,
8148 &root.scene.brushes,
8149 packet.root_scale,
8150 );
8151 returns.ack = Some((ack, recycled));
8152 }
8153
8154 let FramePacket {
8155 frame_id,
8156 viewport: (width, height),
8157 renderer_epoch: _,
8158 surface_epoch: _,
8159 root_scale,
8160 root,
8161 overlay,
8162 replay: _,
8163 text_cache_len: _,
8164 } = packet;
8165
8166 let mut backend = RecordingSurfaceBackend {
8167 renderer: self,
8168 recorder: frame_encoder,
8169 };
8170
8171 let surface_packet = match root {
8172 PacketRoot::Direct(root) => {
8173 let direct_render_start = Instant::now();
8174 let result = match execute_render_root_direct(
8175 &mut backend,
8176 surface_view,
8177 *root,
8178 width,
8179 height,
8180 root_scale,
8181 wgpu::LoadOp::Clear(CLEAR_COLOR),
8182 ) {
8183 Ok(scene) => {
8188 returns.scene = Some(scene);
8189 Ok(())
8190 }
8191 Err((error, scene)) => {
8192 returns.scene = Some(scene);
8193 Err(error)
8194 }
8195 };
8196 if result.is_ok() {
8197 if let Some(overlay) = overlay {
8198 Self::render_overlay_packet(
8199 &mut backend,
8200 surface_view,
8201 overlay,
8202 width,
8203 height,
8204 root_scale,
8205 )?;
8206 }
8207 }
8208 let after_direct_render = Instant::now();
8209 if let Some(total_ms) =
8210 should_log_wgpu_render_stage(recorded_start, after_direct_render)
8211 {
8212 log::warn!(
8213 "[wgpu-render-stage:recorded-direct-root] frame={frame_id} total_ms={total_ms:.2} render_ms={:.2}",
8214 instant_ms(direct_render_start, after_direct_render),
8215 );
8216 }
8217 return result;
8218 }
8219 PacketRoot::Surface(surface_packet) => surface_packet,
8220 };
8221 let after_root_collect = Instant::now();
8222
8223 let RootSurfacePacket {
8224 lowered,
8225 source,
8226 transform_to_parent,
8227 node_id,
8228 backdrop,
8229 graphics_layer,
8230 local_bounds,
8231 clip_rect,
8232 shadow_clip,
8233 } = *surface_packet;
8234 let mut lowered = lowered;
8235 lowered.source = source;
8236
8237 let viewport_rect = Rect {
8242 x: 0.0,
8243 y: 0.0,
8244 width: width as f32 / root_scale,
8245 height: height as f32 / root_scale,
8246 };
8247 let root_surface = execute_render_layer_surface(
8248 &mut backend,
8249 &mut lowered,
8250 LayerSurfaceRequest {
8251 root_scale,
8252 backdrop_underlay: None,
8253 allow_runtime_cache: false,
8254 logical_rect_override: Some(viewport_rect),
8255 capture_clip_override: None,
8256 activates_nested_capture: false,
8257 translation_context: TranslationRenderContext::default(),
8258 },
8259 )?;
8260 let root_quad = transform_to_parent.map_rect(root_surface.logical_rect);
8261 let root_dest_quad = scaled_quad(root_quad, root_scale);
8262
8263 let needs_root_composite_target =
8264 backdrop.is_some() || graphics_layer.shadow_elevation > 0.0;
8265
8266 if needs_root_composite_target {
8267 let composite_target = backend.acquire_frame_surface(width, height);
8268 backend.clear_target_view_with_load_op(
8269 &composite_target.view,
8270 wgpu::LoadOp::Clear(CLEAR_COLOR),
8271 );
8272
8273 if let Some(backdrop) = &backdrop {
8274 execute_apply_backdrop_layer_to_target(
8275 &mut backend,
8276 &composite_target,
8277 &BackdropLayer {
8278 node_id,
8279 rect: quad_bounds(transform_to_parent.map_rect(local_bounds)),
8280 clip: clip_rect.map(|clip| quad_bounds(transform_to_parent.map_rect(clip))),
8281 snap_anchor: None,
8282 effect: backdrop.clone(),
8283 z_index: 0,
8284 },
8285 None,
8286 width,
8287 height,
8288 root_scale,
8289 None,
8290 )?;
8291 }
8292
8293 let mut root_shadow_scene = CompositorScene::new();
8294 let root_shadow_clip =
8295 shadow_clip.map(|clip| quad_bounds(transform_to_parent.map_rect(clip)));
8296 push_layer_shadow(
8297 &mut root_shadow_scene,
8298 &graphics_layer,
8299 local_bounds,
8300 quad_bounds(transform_to_parent.map_rect(local_bounds)),
8301 root_shadow_clip,
8302 );
8303 for shadow in &root_shadow_scene.shadow_draws {
8304 backend.render_shadow_draw(
8305 &composite_target.view,
8306 shadow,
8307 width,
8308 height,
8309 root_scale,
8310 );
8311 }
8312
8313 let composite_dest_quad =
8314 snap_motion_stable_dest_quad(root_dest_quad, root_surface.sample_mode);
8315 execute_composite_surface_to_view(
8316 &mut backend,
8317 root_surface.target.target(),
8318 &composite_target.view,
8319 (width, height),
8320 composite_dest_quad,
8321 root_surface.composite_alpha,
8322 wgpu::LoadOp::Load,
8323 None,
8324 root_surface.blend_mode,
8325 root_surface.sample_mode,
8326 )?;
8327 backend.composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
8328 &composite_target,
8329 surface_view,
8330 1.0,
8331 wgpu::LoadOp::Clear(CLEAR_COLOR),
8332 None,
8333 None,
8334 BlendMode::SrcOver,
8335 None,
8336 CompositeSampleMode::Linear,
8337 );
8338 backend.release_frame_surface(composite_target);
8339 } else {
8340 let composite_dest_quad =
8341 snap_motion_stable_dest_quad(root_dest_quad, root_surface.sample_mode);
8342 execute_composite_surface_to_view(
8343 &mut backend,
8344 root_surface.target.target(),
8345 surface_view,
8346 (width, height),
8347 composite_dest_quad,
8348 root_surface.composite_alpha,
8349 wgpu::LoadOp::Clear(CLEAR_COLOR),
8350 None,
8351 root_surface.blend_mode,
8352 root_surface.sample_mode,
8353 )?;
8354 }
8355 backend.release_layer_surface_target(root_surface.target);
8356 if let Some(overlay) = overlay {
8357 Self::render_overlay_packet(
8358 &mut backend,
8359 surface_view,
8360 overlay,
8361 width,
8362 height,
8363 root_scale,
8364 )?;
8365 }
8366 let after_layer_render = Instant::now();
8367 if let Some(total_ms) = should_log_wgpu_render_stage(recorded_start, after_layer_render) {
8368 log::warn!(
8369 "[wgpu-render-stage:recorded-layer-root] total_ms={total_ms:.2} collect_ms={:.2} render_ms={:.2}",
8370 instant_ms(recorded_start, after_root_collect),
8371 instant_ms(after_root_collect, after_layer_render),
8372 );
8373 }
8374 Ok(())
8375 }
8376
8377 fn render_overlay_packet<C: FrameCommandRecorder>(
8381 backend: &mut RecordingSurfaceBackend<'_, '_, C>,
8382 surface_view: &wgpu::TextureView,
8383 overlay: CollectedLayer,
8384 width: u32,
8385 height: u32,
8386 root_scale: f32,
8387 ) -> Result<(), String> {
8388 if !overlay.child_layers.is_empty()
8389 || !root_direct_scene_events_are_supported(&overlay.scene)
8390 || !direct_root_child_underlays_are_supported(&overlay)
8391 {
8392 return Err("dev overlay graph must stay directly renderable".to_string());
8393 }
8394 execute_render_root_direct(
8395 backend,
8396 surface_view,
8397 overlay,
8398 width,
8399 height,
8400 root_scale,
8401 wgpu::LoadOp::Load,
8402 )
8403 .map(|_overlay_scene| ())
8404 .map_err(|(error, _overlay_scene)| error)
8405 }
8406
8407 #[allow(clippy::too_many_arguments)]
8408 fn encode_non_effect_segment_commands<C: FrameCommandRecorder>(
8409 &mut self,
8410 frame_encoder: &mut C,
8411 target_view: &wgpu::TextureView,
8412 ordered_items: &[(usize, SegmentDrawItem)],
8413 composites: &[(usize, CompositeBatchItem<'_>)],
8414 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
8415 shapes: &[DrawShape],
8416 brushes: &[Brush],
8417 images: &[ImageDraw],
8418 texts: &[TextDraw],
8419 shadow_draws: &[ShadowDraw],
8420 retained_draws: &[RetainedDraw],
8421 initial_load_op: wgpu::LoadOp<wgpu::Color>,
8422 width: u32,
8423 height: u32,
8424 root_scale: f32,
8425 ) -> Result<SegmentCommandEncodeOutcome, String> {
8426 let mut first_batch = true;
8427 for command in
8428 SegmentCommandIter::new(ordered_items, shapes, images, self.shape_batch_limits)
8429 {
8430 match command {
8431 SegmentRenderCommand::DrawChunk(chunk) => {
8432 let load_op = if first_batch {
8433 initial_load_op
8434 } else {
8435 wgpu::LoadOp::Load
8436 };
8437 let outcome = self.render_segment_draw_chunk(
8438 frame_encoder,
8439 target_view,
8440 ordered_items,
8441 composites,
8442 shader_composites,
8443 shapes,
8444 brushes,
8445 images,
8446 texts,
8447 retained_draws,
8448 chunk,
8449 width,
8450 height,
8451 root_scale,
8452 load_op,
8453 )?;
8454 if outcome.rendered_any {
8455 frame_encoder.record_passes(outcome.pass_count);
8456 first_batch = false;
8457 }
8458 }
8459 SegmentRenderCommand::Shadow(index) => {
8460 if first_batch && matches!(initial_load_op, wgpu::LoadOp::Clear(_)) {
8461 {
8462 let _clear = frame_encoder.encoder().begin_render_pass(
8463 &wgpu::RenderPassDescriptor {
8464 label: Some("Shadow Pre-Clear"),
8465 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
8466 view: target_view,
8467 resolve_target: None,
8468 depth_slice: None,
8469 ops: wgpu::Operations {
8470 load: initial_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 }
8481 frame_encoder.record_pass();
8482 first_batch = false;
8483 }
8484 let pass_count_before = frame_encoder.recorded_pass_count();
8485 self.encode_shadow_draw(
8486 frame_encoder,
8487 target_view,
8488 &shadow_draws[index],
8489 width,
8490 height,
8491 root_scale,
8492 );
8493 if frame_encoder.recorded_pass_count() > pass_count_before {
8494 first_batch = false;
8495 }
8496 }
8497 }
8498 }
8499 Ok(SegmentCommandEncodeOutcome { first_batch })
8500 }
8501
8502 #[cfg(not(target_arch = "wasm32"))]
8503 #[allow(clippy::too_many_arguments)]
8504 fn render_segment_draw_chunk_fused_native<C: FrameCommandRecorder>(
8505 &mut self,
8506 frame_encoder: &mut C,
8507 target_view: &wgpu::TextureView,
8508 ordered_items: &[(usize, SegmentDrawItem)],
8509 composites: &[(usize, CompositeBatchItem<'_>)],
8510 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
8511 shapes: &[DrawShape],
8512 brushes: &[Brush],
8513 images: &[ImageDraw],
8514 texts: &[TextDraw],
8515 retained_draws: &[RetainedDraw],
8516 chunk: &SegmentDrawChunkPlan,
8517 width: u32,
8518 height: u32,
8519 root_scale: f32,
8520 load_op: wgpu::LoadOp<wgpu::Color>,
8521 ) -> Result<Option<SegmentRenderOutcome>, String> {
8522 let Some(partitions) = native_segment_fusion_partitions(
8523 ordered_items,
8524 shapes,
8525 brushes,
8526 chunk,
8527 self.shape_batch_limits,
8528 )?
8529 else {
8530 return Ok(None);
8531 };
8532
8533 let mut rendered_any = false;
8534 let mut pass_count = 0_u32;
8535 let mut next_load_op = load_op;
8536 for partition in partitions {
8537 let outcome = self.render_segment_draw_chunk_fused_native_partition(
8538 frame_encoder,
8539 target_view,
8540 ordered_items,
8541 composites,
8542 shader_composites,
8543 shapes,
8544 brushes,
8545 images,
8546 texts,
8547 retained_draws,
8548 &partition.chunk,
8549 partition.budget,
8550 width,
8551 height,
8552 root_scale,
8553 next_load_op,
8554 )?;
8555 if outcome.rendered_any {
8556 rendered_any = true;
8557 pass_count = pass_count.saturating_add(outcome.pass_count);
8558 next_load_op = wgpu::LoadOp::Load;
8559 }
8560 }
8561
8562 Ok(Some(SegmentRenderOutcome {
8563 rendered_any,
8564 pass_count,
8565 }))
8566 }
8567
8568 #[cfg(not(target_arch = "wasm32"))]
8569 #[allow(clippy::too_many_arguments)]
8570 fn render_segment_draw_chunk_fused_native_partition<C: FrameCommandRecorder>(
8571 &mut self,
8572 frame_encoder: &mut C,
8573 target_view: &wgpu::TextureView,
8574 ordered_items: &[(usize, SegmentDrawItem)],
8575 composites: &[(usize, CompositeBatchItem<'_>)],
8576 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
8577 shapes: &[DrawShape],
8578 brushes: &[Brush],
8579 images: &[ImageDraw],
8580 texts: &[TextDraw],
8581 retained_draws: &[RetainedDraw],
8582 chunk: &SegmentDrawChunkPlan,
8583 budget: NativeSegmentFusionBudget,
8584 width: u32,
8585 height: u32,
8586 root_scale: f32,
8587 load_op: wgpu::LoadOp<wgpu::Color>,
8588 ) -> Result<SegmentRenderOutcome, String> {
8589 let partition_start = Instant::now();
8590 let mut staged_uploads = self.take_staged_uploads();
8591 staged_uploads.clear();
8592 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
8593 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
8594 let mut image_cmds = std::mem::take(&mut self.scratch_image_cmds);
8595 let mut glyph_cmds = std::mem::take(&mut self.scratch_glyph_cmds);
8596 let mut span_cache = std::mem::take(&mut self.static_span);
8599
8600 image_vertices.clear();
8601 image_indices.clear();
8602 image_cmds.clear();
8603 glyph_cmds.clear();
8604
8605 let result = (|| {
8606 let viewport = ViewportUniformParams {
8607 width,
8608 height,
8609 offset: [0.0, 0.0],
8610 };
8611 self.prewarm_offscreen_text_glyph_draws_in_chunk(
8612 ordered_items,
8613 texts,
8614 chunk,
8615 viewport,
8616 root_scale,
8617 &mut staged_uploads,
8618 &mut image_vertices,
8619 &mut image_indices,
8620 &mut glyph_cmds,
8621 )?;
8622 let mut shape_refs = Vec::with_capacity(budget.shape_count);
8623 for batch in chunk.iter() {
8624 let SegmentBatchPlan::Shape { start, end, .. } = batch else {
8625 continue;
8626 };
8627 for (_, item) in &ordered_items[start..end] {
8628 let SegmentDrawItem::Shape(shape_index) = item else {
8629 return Err(format!(
8630 "shape batch contains non-shape draw item: {item:?}"
8631 ));
8632 };
8633 shape_refs.push(&shapes[*shape_index]);
8634 }
8635 }
8636 let after_shape_refs = Instant::now();
8637
8638 let mut direct_shape_uploads = StagedBufferUploads::default();
8639 let mut shape_upload_base = 0u64;
8640 if !shape_refs.is_empty() {
8641 let Some((_, upload_base)) = self.prepare_shapes_batch_direct(
8642 frame_encoder,
8643 shape_refs.iter().copied(),
8644 brushes,
8645 root_scale,
8646 viewport,
8647 &mut direct_shape_uploads,
8648 ) else {
8649 return Err(
8650 "native fused segment shape preparation produced no draw batch".to_string(),
8651 );
8652 };
8653 shape_upload_base = upload_base;
8654 }
8655 let after_shape_prepare = Instant::now();
8656
8657 let first_batch_info = match chunk.batches.first() {
8663 Some(&SegmentBatchPlan::Shape {
8664 start,
8665 end,
8666 blend_mode,
8667 }) => {
8668 let mut has_gradient = false;
8669 for (_, item) in &ordered_items[start..end] {
8670 if let SegmentDrawItem::Shape(shape_index) = item {
8671 has_gradient |=
8672 shape_gradient_stop_count(&shapes[*shape_index], brushes) > 0;
8673 }
8674 }
8675 Some((end - start, blend_mode, has_gradient))
8676 }
8677 _ => None,
8678 };
8679 let span_decision = span_cache.engage(
8680 load_op,
8681 first_batch_info,
8682 width,
8683 height,
8684 &self.scratch_shape_data,
8685 &self.scratch_gradients,
8686 );
8687 let span_skip = match span_decision {
8688 StaticSpanDecision::Hit { skip } => {
8689 if fill_area_diag_enabled() {
8690 self.fill_area_diag
8695 .note_static_span_skip(&self.scratch_shape_data[..skip]);
8696 }
8697 skip
8698 }
8699 _ => 0,
8700 };
8701
8702 let rim_mesh_on = rim_mesh_enabled();
8710 let mut chunk_rims: Vec<RimDraw> = Vec::new();
8711
8712 let mut fused_batches = Vec::with_capacity(chunk.batches.len());
8713 let mut shape_cursor = 0_u32;
8714 let mut composite_cursor = 0usize;
8715 let mut shader_composite_cursor = 0usize;
8716 for (batch_index, batch) in chunk.iter().enumerate() {
8717 match batch {
8718 SegmentBatchPlan::Shape {
8719 start,
8720 end,
8721 blend_mode,
8722 } => {
8723 let mut has_gradient = false;
8724 for (_, item) in &ordered_items[start..end] {
8725 let SegmentDrawItem::Shape(shape_index) = item else {
8726 return Err(format!(
8727 "shape batch contains non-shape draw item: {item:?}"
8728 ));
8729 };
8730 has_gradient |=
8731 shape_gradient_stop_count(&shapes[*shape_index], brushes) > 0;
8732 }
8733 let skip = if batch_index == 0 { span_skip } else { 0 };
8738 let shape_count = end - start;
8739 if shape_count > 0 {
8740 if rim_mesh_on
8741 && self.instanced_quads.is_some()
8742 && blend_mode == BlendMode::SrcOver
8743 {
8744 for offset in skip..shape_count {
8745 let global_index = shape_cursor + offset as u32;
8750 let converted = &self.scratch_shape_data[global_index as usize];
8751 let Some(band) = rim_mesh_band(converted) else {
8752 continue;
8753 };
8754 let vertex_mark = self.rim_mesh_vertices.len();
8755 let index_mark = self.rim_mesh_indices.len();
8756 if emit_arc_band_mesh(
8757 converted,
8758 global_index,
8759 &band,
8760 &mut self.rim_mesh_vertices,
8761 &mut self.rim_mesh_indices,
8762 )
8763 .is_none()
8764 {
8765 self.rim_mesh_vertices.truncate(vertex_mark);
8768 self.rim_mesh_indices.truncate(index_mark);
8769 continue;
8770 }
8771 if self.rim_mesh_vertices.len() > RIM_MESH_VERTEX_CAPACITY
8772 || self.rim_mesh_indices.len() > RIM_MESH_INDEX_CAPACITY
8773 {
8774 self.rim_mesh_vertices.truncate(vertex_mark);
8778 self.rim_mesh_indices.truncate(index_mark);
8779 rim_mesh_capacity_warn();
8780 continue;
8781 }
8782 chunk_rims.push(RimDraw {
8783 shape_index: global_index,
8784 first_index: index_mark as u32,
8785 index_count: (self.rim_mesh_indices.len() - index_mark)
8786 as u32,
8787 });
8788 if fill_area_diag_enabled() {
8789 self.fill_area_diag.note_rim_mesh(
8790 converted,
8791 triangles_shoelace_area(
8792 &self.rim_mesh_vertices,
8793 &self.rim_mesh_indices[index_mark..],
8794 ),
8795 );
8796 }
8797 self.rim_meshes_emitted += 1;
8798 if self.rim_meshes_emitted % 600 == 1 {
8799 log::debug!(
8800 "[rim-mesh] {} rims meshed lifetime ({} verts live this frame)",
8801 self.rim_meshes_emitted,
8802 self.rim_mesh_vertices.len(),
8803 );
8804 }
8805 }
8806 }
8807 if shape_count > skip {
8808 fused_batches.push(FusedSegmentBatch::Shape {
8809 batch: PreparedShapeBatch {
8810 vertex_start: (shape_cursor + skip as u32) * 6,
8811 vertex_count: (shape_count - skip) as u32 * 6,
8812 has_gradient,
8813 },
8814 blend_mode,
8815 });
8816 }
8817 shape_cursor += shape_count as u32;
8818 }
8819 }
8820 SegmentBatchPlan::Image {
8821 start,
8822 end,
8823 blend_mode,
8824 } => {
8825 let cmd_start = image_cmds.len();
8826 for (_, item) in &ordered_items[start..end] {
8827 let SegmentDrawItem::Image(image_index) = item else {
8828 return Err(format!(
8829 "image batch contains non-image draw item: {item:?}"
8830 ));
8831 };
8832 self.append_image_draw_cmd(
8833 &images[*image_index],
8834 viewport,
8835 root_scale,
8836 &mut image_vertices,
8837 &mut image_indices,
8838 &mut image_cmds,
8839 )?;
8840 }
8841 let cmd_end = image_cmds.len();
8842 if cmd_start < cmd_end {
8843 fused_batches.push(FusedSegmentBatch::Image {
8844 cmd_range: cmd_start..cmd_end,
8845 blend_mode,
8846 });
8847 }
8848 }
8849 SegmentBatchPlan::Text { start, end } => {
8850 let glyph_cmd_start = glyph_cmds.len();
8851 let image_cmd_start = image_cmds.len();
8852 let text_draws =
8853 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
8854 if !self.append_text_glyph_draws(
8855 text_draws,
8856 viewport,
8857 root_scale,
8858 false,
8859 &mut staged_uploads,
8860 &mut image_vertices,
8861 &mut image_indices,
8862 &mut glyph_cmds,
8863 )? {
8864 let text_draws =
8865 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
8866 self.append_text_image_draw_cmds(
8867 text_draws,
8868 viewport,
8869 root_scale,
8870 &mut image_vertices,
8871 &mut image_indices,
8872 &mut image_cmds,
8873 )?;
8874 }
8875 let image_cmd_end = image_cmds.len();
8876 let glyph_cmd_end = glyph_cmds.len();
8877 if image_cmd_start < image_cmd_end || glyph_cmd_start < glyph_cmd_end {
8878 fused_batches.push(FusedSegmentBatch::Text {
8879 image_cmd_range: image_cmd_start..image_cmd_end,
8880 glyph_cmd_range: glyph_cmd_start..glyph_cmd_end,
8881 });
8882 }
8883 }
8884 SegmentBatchPlan::Composite { start, end } => {
8885 for (_, item) in &ordered_items[start..end] {
8886 if !matches!(item, SegmentDrawItem::Composite(_)) {
8887 return Err(format!(
8888 "composite batch contains non-composite draw item: {item:?}"
8889 ));
8890 }
8891 }
8892 let draw_count = end - start;
8893 if draw_count > 0 {
8894 let draw_start = composite_cursor;
8895 composite_cursor += draw_count;
8896 fused_batches.push(FusedSegmentBatch::Composite {
8897 draw_range: draw_start..composite_cursor,
8898 });
8899 }
8900 }
8901 SegmentBatchPlan::ShaderComposite { start, end } => {
8902 for (_, item) in &ordered_items[start..end] {
8903 if !matches!(item, SegmentDrawItem::ShaderComposite(_)) {
8904 return Err(format!(
8905 "shader composite batch contains non-shader-composite draw item: {item:?}"
8906 ));
8907 }
8908 }
8909 let draw_count = end - start;
8910 if draw_count > 0 {
8911 let draw_start = shader_composite_cursor;
8912 shader_composite_cursor += draw_count;
8913 fused_batches.push(FusedSegmentBatch::ShaderComposite {
8914 draw_range: draw_start..shader_composite_cursor,
8915 });
8916 }
8917 }
8918 SegmentBatchPlan::Retained { start, end } => {
8919 self.stage_replay_patches(&mut staged_uploads);
8920 for (_, item) in &ordered_items[start..end] {
8921 let SegmentDrawItem::Retained(index) = item else {
8922 return Err(format!(
8923 "retained batch contains non-retained draw item: {item:?}"
8924 ));
8925 };
8926 let retained = retained_draws.get(*index).ok_or_else(|| {
8927 format!("retained draw index {index} out of bounds")
8928 })?;
8929 if (*index as u32) < MAX_REPLAY_SLOTS
8930 && self.replay_slots.slots.contains_key(&retained.slot)
8931 {
8932 let transform = retained.transform.with_retained_paint();
8933 staged_uploads.stage_at(
8934 UploadTarget::ReplayTransform,
8935 *index as u64 * REPLAY_TRANSFORM_STRIDE,
8936 bytemuck::bytes_of(&transform),
8937 );
8938 }
8939 }
8940 if end > start {
8941 fused_batches.push(FusedSegmentBatch::Retained {
8942 item_range: start..end,
8943 });
8944 }
8945 }
8946 }
8947 }
8948 if !chunk_rims.is_empty() {
8949 self.upload_transient_rim_meshes();
8950 }
8951 let after_batch_prepare = Instant::now();
8952
8953 if !image_indices.is_empty() {
8954 self.stage_native_image_buffers(
8955 &mut staged_uploads,
8956 viewport,
8957 &image_vertices,
8958 &image_indices,
8959 );
8960 }
8961
8962 let device = self.device.clone();
8963 let composite_items: Vec<_> = chunk
8964 .iter()
8965 .filter_map(|batch| match batch {
8966 SegmentBatchPlan::Composite { start, end } => Some((start, end)),
8967 _ => None,
8968 })
8969 .flat_map(|(start, end)| {
8970 ordered_items[start..end].iter().filter_map(|(_, item)| {
8971 let SegmentDrawItem::Composite(composite_index) = item else {
8972 return None;
8973 };
8974 composites
8975 .get(*composite_index)
8976 .map(|(_, composite)| *composite)
8977 })
8978 })
8979 .collect();
8980 let prepared_composites = self.effect_renderer.prepare_composite_batch_draws(
8981 frame_encoder,
8982 &device,
8983 load_op,
8984 &composite_items,
8985 );
8986 let shader_items: Vec<_> = chunk
8987 .iter()
8988 .filter_map(|batch| match batch {
8989 SegmentBatchPlan::ShaderComposite { start, end } => Some((start, end)),
8990 _ => None,
8991 })
8992 .flat_map(|(start, end)| {
8993 ordered_items[start..end].iter().filter_map(|(_, item)| {
8994 let SegmentDrawItem::ShaderComposite(composite_index) = item else {
8995 return None;
8996 };
8997 shader_composites
8998 .get(*composite_index)
8999 .map(|(_, composite)| *composite)
9000 })
9001 })
9002 .collect();
9003 let prepared_shaders = self
9004 .effect_renderer
9005 .prepare_shader_batch_draws(frame_encoder, &device, &shader_items)
9006 .ok_or_else(|| "shader composite batch preparation failed".to_string())?;
9007 if !shader_items.is_empty() {
9008 self.effect_renderer.record_composite_pass();
9009 self.effect_renderer
9010 .debug_effects
9011 .set(self.effect_renderer.debug_effects.get() + shader_items.len() as u32);
9012 }
9013 let span_blit_items =
9020 span_cache
9021 .texture
9022 .as_ref()
9023 .filter(|_| span_skip > 0)
9024 .map(|texture| CompositeBatchItem {
9025 source: texture,
9026 alpha: 1.0,
9027 scissor: None,
9028 rounded_mask: None,
9029 blend_mode: BlendMode::SrcOver,
9030 dest_viewport: None,
9031 source_viewport: None,
9032 sample_mode: CompositeSampleMode::Nearest,
9033 });
9034 let span_blit = match &span_blit_items {
9035 Some(item) => self.effect_renderer.prepare_composite_batch_draws(
9036 frame_encoder,
9037 &device,
9038 load_op,
9039 std::slice::from_ref(item),
9040 ),
9041 None => Vec::new(),
9042 };
9043 let after_composite_prepare = Instant::now();
9044
9045 if fused_batches.is_empty() && span_blit.is_empty() {
9046 return Ok(SegmentRenderOutcome {
9047 rendered_any: false,
9048 pass_count: 0,
9049 });
9050 }
9051
9052 self.flush_staged_uploads_at(
9057 frame_encoder.encoder(),
9058 &direct_shape_uploads,
9059 shape_upload_base,
9060 );
9061 let upload_offset =
9062 frame_encoder.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
9063 self.flush_staged_uploads_at(frame_encoder.encoder(), &staged_uploads, upload_offset);
9064 let after_upload = Instant::now();
9065
9066 let use_retained_bundles = retained_bundles_enabled();
9067 let mut retained_encode_ms = 0.0_f64;
9068 {
9069 let mut render_pass =
9070 frame_encoder
9071 .encoder()
9072 .begin_render_pass(&wgpu::RenderPassDescriptor {
9073 label: Some("Fused Segment Draw Pass"),
9074 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
9075 view: target_view,
9076 resolve_target: None,
9077 depth_slice: None,
9078 ops: wgpu::Operations {
9079 load: load_op,
9080 store: wgpu::StoreOp::Store,
9081 },
9082 })],
9083 depth_stencil_attachment: None,
9084 timestamp_writes: None,
9085 occlusion_query_set: None,
9086 multiview_mask: None,
9087 });
9088
9089 for draw in &span_blit {
9093 self.effect_renderer.draw_prepared_composite(
9094 &mut render_pass,
9095 (width, height),
9096 draw,
9097 );
9098 }
9099 for batch in &fused_batches {
9100 match batch {
9101 FusedSegmentBatch::Shape { batch, blend_mode } => {
9102 self.draw_prepared_shapes(
9103 &mut render_pass,
9104 *blend_mode,
9105 *batch,
9106 width,
9107 height,
9108 &chunk_rims,
9109 );
9110 }
9111 FusedSegmentBatch::Image {
9112 cmd_range,
9113 blend_mode,
9114 } => {
9115 self.draw_native_prepared_image_cmd_range(
9116 &mut render_pass,
9117 &image_cmds,
9118 cmd_range.clone(),
9119 *blend_mode,
9120 )?;
9121 }
9122 FusedSegmentBatch::Text {
9123 image_cmd_range,
9124 glyph_cmd_range,
9125 } => {
9126 if !image_cmd_range.is_empty() {
9127 self.draw_native_prepared_image_cmd_range(
9128 &mut render_pass,
9129 &image_cmds,
9130 image_cmd_range.clone(),
9131 BlendMode::SrcOver,
9132 )?;
9133 self.frame_stats.bump_text();
9134 }
9135 if !glyph_cmd_range.is_empty() {
9136 self.draw_native_prepared_glyph_cmd_range(
9137 &mut render_pass,
9138 &glyph_cmds,
9139 glyph_cmd_range.clone(),
9140 )?;
9141 }
9142 }
9143 FusedSegmentBatch::Composite { draw_range } => {
9144 for draw in
9145 prepared_composites.get(draw_range.clone()).ok_or_else(|| {
9146 "composite draw range is outside the prepared command buffer"
9147 .to_string()
9148 })?
9149 {
9150 self.effect_renderer.draw_prepared_composite(
9151 &mut render_pass,
9152 (width, height),
9153 draw,
9154 );
9155 }
9156 }
9157 FusedSegmentBatch::ShaderComposite { draw_range } => {
9158 for draw in prepared_shaders.get(draw_range.clone()).ok_or_else(|| {
9159 "shader composite draw range is outside the prepared command buffer"
9160 .to_string()
9161 })? {
9162 self.effect_renderer.draw_prepared_shader_src_over(
9163 &device,
9164 &mut render_pass,
9165 (width, height),
9166 draw,
9167 );
9168 }
9169 }
9170 FusedSegmentBatch::Retained { item_range } => {
9171 let retained_start = Instant::now();
9177 if use_retained_bundles {
9178 self.draw_retained_stretch_bundled(
9179 &mut render_pass,
9180 ordered_items,
9181 retained_draws,
9182 item_range.clone(),
9183 width,
9184 height,
9185 );
9186 } else {
9187 for (_, item) in &ordered_items[item_range.clone()] {
9188 if let SegmentDrawItem::Retained(index) = item {
9189 if let Some(retained) = retained_draws.get(*index) {
9190 self.draw_retained_batch(
9191 &mut render_pass,
9192 retained,
9193 *index,
9194 width,
9195 height,
9196 );
9197 }
9198 }
9199 }
9200 }
9201 retained_encode_ms += instant_ms(retained_start, Instant::now());
9202 }
9203 }
9204 }
9205 }
9206 let mut capture_passes = 0_u32;
9215 if let StaticSpanDecision::Capture { len, clear } = span_decision {
9216 let texture = match span_cache.texture.take() {
9217 Some(existing) if existing.width == width && existing.height == height => {
9218 existing
9219 }
9220 other => {
9221 if let Some(stale) = other {
9222 self.defer_offscreen_release(stale);
9223 }
9224 self.acquire_offscreen(width, height)
9225 }
9226 };
9227 {
9228 let mut capture_pass =
9229 frame_encoder
9230 .encoder()
9231 .begin_render_pass(&wgpu::RenderPassDescriptor {
9232 label: Some("Static Span Capture Pass"),
9233 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
9234 view: &texture.view,
9235 resolve_target: None,
9236 depth_slice: None,
9237 ops: wgpu::Operations {
9238 load: wgpu::LoadOp::Clear(clear),
9239 store: wgpu::StoreOp::Store,
9240 },
9241 })],
9242 depth_stencil_attachment: None,
9243 timestamp_writes: None,
9244 occlusion_query_set: None,
9245 multiview_mask: None,
9246 });
9247 let has_gradient = first_batch_info
9251 .map(|(_, _, has_gradient)| has_gradient)
9252 .unwrap_or(false);
9253 self.draw_prepared_shapes(
9254 &mut capture_pass,
9255 BlendMode::SrcOver,
9256 PreparedShapeBatch {
9257 vertex_start: 0,
9258 vertex_count: len as u32 * 6,
9259 has_gradient,
9260 },
9261 width,
9262 height,
9263 &[],
9264 );
9265 if fill_area_diag_enabled() {
9266 self.fill_area_diag
9270 .add_shape_quads(&self.scratch_shape_data[..len], viewport);
9271 }
9272 span_cache.store_key(
9273 &self.scratch_shape_data[..len],
9274 &self.scratch_gradients,
9275 width,
9276 height,
9277 clear,
9278 has_gradient,
9279 );
9280 }
9281 span_cache.texture = Some(texture);
9282 capture_passes = 1;
9283 }
9284 let after_pass = Instant::now();
9285 if let Some(total_ms) = should_log_wgpu_render_stage(partition_start, after_pass) {
9286 log::warn!(
9287 "[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={}",
9288 instant_ms(partition_start, after_shape_refs),
9289 instant_ms(after_shape_refs, after_shape_prepare),
9290 instant_ms(after_shape_prepare, after_batch_prepare),
9291 instant_ms(after_batch_prepare, after_composite_prepare),
9292 instant_ms(after_composite_prepare, after_upload),
9293 instant_ms(after_upload, after_pass),
9294 fused_batches.len(),
9295 budget.shape_count,
9296 image_cmds.len(),
9297 glyph_cmds.len(),
9298 staged_uploads.bytes.len(),
9299 );
9300 }
9301
9302 Ok(SegmentRenderOutcome {
9303 rendered_any: true,
9304 pass_count: 1 + capture_passes,
9305 })
9306 })();
9307
9308 self.scratch_image_vertices = image_vertices;
9309 self.scratch_image_indices = image_indices;
9310 self.scratch_image_cmds = image_cmds;
9311 self.scratch_glyph_cmds = glyph_cmds;
9312 self.restore_staged_uploads(staged_uploads);
9313 self.static_span = span_cache;
9314 result
9315 }
9316
9317 #[allow(clippy::too_many_arguments)]
9318 fn render_segment_draw_chunk<C: FrameCommandRecorder>(
9319 &mut self,
9320 frame_encoder: &mut C,
9321 target_view: &wgpu::TextureView,
9322 ordered_items: &[(usize, SegmentDrawItem)],
9323 composites: &[(usize, CompositeBatchItem<'_>)],
9324 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
9325 shapes: &[DrawShape],
9326 brushes: &[Brush],
9327 images: &[ImageDraw],
9328 texts: &[TextDraw],
9329 retained_draws: &[RetainedDraw],
9330 chunk: SegmentDrawChunkPlan,
9331 width: u32,
9332 height: u32,
9333 root_scale: f32,
9334 load_op: wgpu::LoadOp<wgpu::Color>,
9335 ) -> Result<SegmentRenderOutcome, String> {
9336 #[cfg(target_arch = "wasm32")]
9337 let _ = retained_draws;
9338 #[cfg(not(target_arch = "wasm32"))]
9339 if let Some(outcome) = self.render_segment_draw_chunk_fused_native(
9340 frame_encoder,
9341 target_view,
9342 ordered_items,
9343 composites,
9344 shader_composites,
9345 shapes,
9346 brushes,
9347 images,
9348 texts,
9349 retained_draws,
9350 &chunk,
9351 width,
9352 height,
9353 root_scale,
9354 load_op,
9355 )? {
9356 return Ok(outcome);
9357 }
9358
9359 let mut staged_uploads = self.take_staged_uploads();
9360 let result = (|| {
9361 let mut rendered_any = false;
9362 let mut pass_count = 0_u32;
9363 let mut next_load_op = load_op;
9364 for batch in chunk.iter() {
9365 staged_uploads.clear();
9366 match batch {
9367 SegmentBatchPlan::Shape {
9368 start,
9369 end,
9370 blend_mode,
9371 } => {
9372 let slice = &ordered_items[start..end];
9373 if slice.len() > self.shape_batch_limits.max_shapes_per_batch {
9374 return Err(format!(
9375 "shape batch contains {} shapes, exceeding the renderer limit of {}",
9376 slice.len(),
9377 self.shape_batch_limits.max_shapes_per_batch
9378 ));
9379 }
9380 let viewport = ViewportUniformParams {
9381 width,
9382 height,
9383 offset: [0.0, 0.0],
9384 };
9385 for (_, item) in slice {
9386 if !matches!(item, SegmentDrawItem::Shape(_)) {
9387 return Err(format!(
9388 "shape batch contains non-shape draw item: {item:?}"
9389 ));
9390 }
9391 }
9392 let Some(prepared) = self.prepare_shapes_batch(
9393 slice.iter().filter_map(|(_, item)| match item {
9394 SegmentDrawItem::Shape(shape_index) => Some(&shapes[*shape_index]),
9395 _ => None,
9396 }),
9397 brushes,
9398 root_scale,
9399 viewport,
9400 &mut staged_uploads,
9401 ) else {
9402 continue;
9403 };
9404 let upload_offset = frame_encoder
9405 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
9406 self.flush_staged_uploads_at(
9407 frame_encoder.encoder(),
9408 &staged_uploads,
9409 upload_offset,
9410 );
9411 {
9412 let mut render_pass = frame_encoder.encoder().begin_render_pass(
9413 &wgpu::RenderPassDescriptor {
9414 label: Some("Segment Shape Pass"),
9415 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
9416 view: target_view,
9417 resolve_target: None,
9418 depth_slice: None,
9419 ops: wgpu::Operations {
9420 load: next_load_op,
9421 store: wgpu::StoreOp::Store,
9422 },
9423 })],
9424 depth_stencil_attachment: None,
9425 timestamp_writes: None,
9426 occlusion_query_set: None,
9427 multiview_mask: None,
9428 },
9429 );
9430 self.draw_prepared_shapes(
9431 &mut render_pass,
9432 blend_mode,
9433 prepared,
9434 width,
9435 height,
9436 &[],
9437 );
9438 }
9439 pass_count = pass_count.saturating_add(1);
9440 rendered_any = true;
9441 next_load_op = wgpu::LoadOp::Load;
9442 }
9443 SegmentBatchPlan::Image {
9444 start,
9445 end,
9446 blend_mode,
9447 } => {
9448 let viewport = ViewportUniformParams {
9449 width,
9450 height,
9451 offset: [0.0, 0.0],
9452 };
9453 for (_, item) in &ordered_items[start..end] {
9454 if !matches!(item, SegmentDrawItem::Image(_)) {
9455 return Err(format!(
9456 "image batch contains non-image draw item: {item:?}"
9457 ));
9458 }
9459 }
9460 let prepared_images = self.prepare_image_draw_cmds(
9461 ordered_items[start..end]
9462 .iter()
9463 .filter_map(|(_, item)| match item {
9464 SegmentDrawItem::Image(image_index) => {
9465 Some(&images[*image_index])
9466 }
9467 _ => None,
9468 }),
9469 viewport,
9470 root_scale,
9471 &mut staged_uploads,
9472 )?;
9473 if prepared_images.is_empty() {
9474 self.scratch_image_cmds = prepared_images.into_cmds();
9475 continue;
9476 }
9477 let upload_offset = frame_encoder
9478 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
9479 self.flush_staged_uploads_at(
9480 frame_encoder.encoder(),
9481 &staged_uploads,
9482 upload_offset,
9483 );
9484 let draw_result = {
9485 let mut render_pass = frame_encoder.encoder().begin_render_pass(
9486 &wgpu::RenderPassDescriptor {
9487 label: Some("Segment Image Pass"),
9488 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
9489 view: target_view,
9490 resolve_target: None,
9491 depth_slice: None,
9492 ops: wgpu::Operations {
9493 load: next_load_op,
9494 store: wgpu::StoreOp::Store,
9495 },
9496 })],
9497 depth_stencil_attachment: None,
9498 timestamp_writes: None,
9499 occlusion_query_set: None,
9500 multiview_mask: None,
9501 },
9502 );
9503 self.draw_prepared_images(
9504 &mut render_pass,
9505 &prepared_images,
9506 blend_mode,
9507 )
9508 };
9509 pass_count = pass_count.saturating_add(1);
9510 self.scratch_image_cmds = prepared_images.into_cmds();
9511 draw_result?;
9512 rendered_any = true;
9513 next_load_op = wgpu::LoadOp::Load;
9514 }
9515 SegmentBatchPlan::Text { start, end } => {
9516 let viewport = ViewportUniformParams {
9517 width,
9518 height,
9519 offset: [0.0, 0.0],
9520 };
9521 let text_draws =
9522 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
9523 if let Some(prepared_glyphs) = self.prepare_text_glyph_draw_cmds(
9524 text_draws,
9525 viewport,
9526 root_scale,
9527 &mut staged_uploads,
9528 )? {
9529 if prepared_glyphs.is_empty() {
9530 self.scratch_glyph_cmds = prepared_glyphs.into_cmds();
9531 continue;
9532 }
9533 let upload_offset = frame_encoder
9534 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
9535 self.flush_staged_uploads_at(
9536 frame_encoder.encoder(),
9537 &staged_uploads,
9538 upload_offset,
9539 );
9540 {
9541 let mut render_pass = frame_encoder.encoder().begin_render_pass(
9542 &wgpu::RenderPassDescriptor {
9543 label: Some("Segment Text Glyph Atlas Pass"),
9544 color_attachments: &[Some(
9545 wgpu::RenderPassColorAttachment {
9546 view: target_view,
9547 resolve_target: None,
9548 depth_slice: None,
9549 ops: wgpu::Operations {
9550 load: next_load_op,
9551 store: wgpu::StoreOp::Store,
9552 },
9553 },
9554 )],
9555 depth_stencil_attachment: None,
9556 timestamp_writes: None,
9557 occlusion_query_set: None,
9558 multiview_mask: None,
9559 },
9560 );
9561 self.draw_prepared_glyphs(&mut render_pass, &prepared_glyphs)?;
9562 }
9563 pass_count = pass_count.saturating_add(1);
9564 self.scratch_glyph_cmds = prepared_glyphs.into_cmds();
9565 rendered_any = true;
9566 next_load_op = wgpu::LoadOp::Load;
9567 } else {
9568 let text_draws =
9569 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
9570 let prepared_images = self.prepare_text_image_draw_cmds(
9571 text_draws,
9572 viewport,
9573 root_scale,
9574 &mut staged_uploads,
9575 )?;
9576 if prepared_images.is_empty() {
9577 self.scratch_image_cmds = prepared_images.into_cmds();
9578 continue;
9579 }
9580 let upload_offset = frame_encoder
9581 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
9582 self.flush_staged_uploads_at(
9583 frame_encoder.encoder(),
9584 &staged_uploads,
9585 upload_offset,
9586 );
9587 {
9588 let mut render_pass = frame_encoder.encoder().begin_render_pass(
9589 &wgpu::RenderPassDescriptor {
9590 label: Some("Segment Text Pass"),
9591 color_attachments: &[Some(
9592 wgpu::RenderPassColorAttachment {
9593 view: target_view,
9594 resolve_target: None,
9595 depth_slice: None,
9596 ops: wgpu::Operations {
9597 load: next_load_op,
9598 store: wgpu::StoreOp::Store,
9599 },
9600 },
9601 )],
9602 depth_stencil_attachment: None,
9603 timestamp_writes: None,
9604 occlusion_query_set: None,
9605 multiview_mask: None,
9606 },
9607 );
9608 self.draw_prepared_images(
9609 &mut render_pass,
9610 &prepared_images,
9611 BlendMode::SrcOver,
9612 )?;
9613 }
9614 self.frame_stats.bump_text();
9615 pass_count = pass_count.saturating_add(1);
9616 self.scratch_image_cmds = prepared_images.into_cmds();
9617 rendered_any = true;
9618 next_load_op = wgpu::LoadOp::Load;
9619 }
9620 }
9621 SegmentBatchPlan::Composite { start, end } => {
9622 let batch_items: Vec<_> = ordered_items[start..end]
9623 .iter()
9624 .map(|(_, item)| match item {
9625 SegmentDrawItem::Composite(composite_index) => composites
9626 .get(*composite_index)
9627 .map(|(_, composite)| *composite)
9628 .ok_or_else(|| {
9629 "composite item index is outside the composite buffer"
9630 .to_string()
9631 }),
9632 other => Err(format!(
9633 "composite batch contains non-composite draw item: {other:?}"
9634 )),
9635 })
9636 .collect::<Result<_, _>>()?;
9637 let device = self.device.clone();
9638 self.effect_renderer.encode_composite_batch_to_view_pass(
9639 frame_encoder,
9640 &device,
9641 target_view,
9642 (width, height),
9643 next_load_op,
9644 &batch_items,
9645 );
9646 self.effect_renderer.record_composite_pass();
9647 pass_count = pass_count.saturating_add(1);
9648 rendered_any = true;
9649 next_load_op = wgpu::LoadOp::Load;
9650 }
9651 SegmentBatchPlan::ShaderComposite { start, end } => {
9652 let batch_items: Vec<_> = ordered_items[start..end]
9653 .iter()
9654 .map(|(_, item)| match item {
9655 SegmentDrawItem::ShaderComposite(composite_index) => {
9656 shader_composites
9657 .get(*composite_index)
9658 .map(|(_, composite)| *composite)
9659 .ok_or_else(|| {
9660 "shader composite item index is outside the shader composite buffer"
9661 .to_string()
9662 })
9663 }
9664 other => Err(format!(
9665 "shader composite batch contains non-shader-composite draw item: {other:?}"
9666 )),
9667 })
9668 .collect::<Result<Vec<_>, _>>()?;
9669 let device = self.device.clone();
9670 let encoded = self.effect_renderer.encode_shader_batch_src_over_to_view(
9671 frame_encoder,
9672 &device,
9673 target_view,
9674 (width, height),
9675 next_load_op,
9676 &batch_items,
9677 );
9678 if !encoded {
9679 return Err("shader composite batch failed to encode".to_string());
9680 }
9681 self.effect_renderer.record_composite_pass();
9682 self.effect_renderer.debug_effects.set(
9683 self.effect_renderer.debug_effects.get() + batch_items.len() as u32,
9684 );
9685 pass_count = pass_count.saturating_add(1);
9686 rendered_any = true;
9687 next_load_op = wgpu::LoadOp::Load;
9688 }
9689 SegmentBatchPlan::Retained { start, end } => {
9690 #[cfg(target_arch = "wasm32")]
9698 {
9699 let _ = (start, end);
9700 return Err("retained shape batches are native-only".to_string());
9701 }
9702 #[cfg(not(target_arch = "wasm32"))]
9703 {
9704 self.stage_replay_patches(&mut staged_uploads);
9705 for (_, item) in &ordered_items[start..end] {
9706 let SegmentDrawItem::Retained(index) = item else {
9707 return Err(format!(
9708 "retained batch contains non-retained draw item: {item:?}"
9709 ));
9710 };
9711 let retained = retained_draws.get(*index).ok_or_else(|| {
9712 format!("retained draw index {index} out of bounds")
9713 })?;
9714 if (*index as u32) < MAX_REPLAY_SLOTS
9715 && self.replay_slots.slots.contains_key(&retained.slot)
9716 {
9717 let transform = retained.transform.with_retained_paint();
9718 staged_uploads.stage_at(
9719 UploadTarget::ReplayTransform,
9720 *index as u64 * REPLAY_TRANSFORM_STRIDE,
9721 bytemuck::bytes_of(&transform),
9722 );
9723 }
9724 }
9725 let upload_offset = frame_encoder
9726 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
9727 self.flush_staged_uploads_at(
9728 frame_encoder.encoder(),
9729 &staged_uploads,
9730 upload_offset,
9731 );
9732 {
9733 let mut render_pass = frame_encoder.encoder().begin_render_pass(
9734 &wgpu::RenderPassDescriptor {
9735 label: Some("Segment Retained Pass"),
9736 color_attachments: &[Some(
9737 wgpu::RenderPassColorAttachment {
9738 view: target_view,
9739 resolve_target: None,
9740 depth_slice: None,
9741 ops: wgpu::Operations {
9742 load: next_load_op,
9743 store: wgpu::StoreOp::Store,
9744 },
9745 },
9746 )],
9747 depth_stencil_attachment: None,
9748 timestamp_writes: None,
9749 occlusion_query_set: None,
9750 multiview_mask: None,
9751 },
9752 );
9753 for (_, item) in &ordered_items[start..end] {
9754 if let SegmentDrawItem::Retained(index) = item {
9755 if let Some(retained) = retained_draws.get(*index) {
9756 self.draw_retained_batch(
9757 &mut render_pass,
9758 retained,
9759 *index,
9760 width,
9761 height,
9762 );
9763 }
9764 }
9765 }
9766 }
9767 pass_count = pass_count.saturating_add(1);
9768 rendered_any = true;
9769 next_load_op = wgpu::LoadOp::Load;
9770 }
9771 }
9772 }
9773 }
9774 Ok(SegmentRenderOutcome {
9775 rendered_any,
9776 pass_count,
9777 })
9778 })();
9779 self.restore_staged_uploads(staged_uploads);
9780 result
9781 }
9782
9783 fn viewport_uniforms(params: ViewportUniformParams) -> Uniforms {
9784 Uniforms {
9785 viewport: [params.width as f32, params.height as f32],
9786 viewport_offset: params.offset,
9787 }
9788 }
9789
9790 #[cfg(not(target_arch = "wasm32"))]
9791 fn stage_viewport_uniforms(
9792 &self,
9793 staged_uploads: &mut StagedBufferUploads,
9794 params: ViewportUniformParams,
9795 ) {
9796 let uniforms = Self::viewport_uniforms(params);
9797 staged_uploads.stage(UploadTarget::Uniform, bytemuck::bytes_of(&uniforms));
9798 }
9799
9800 #[cfg(not(target_arch = "wasm32"))]
9801 fn stage_retained_glyph_viewport_uniforms(
9802 &mut self,
9803 staged_uploads: &mut StagedBufferUploads,
9804 params: ViewportUniformParams,
9805 ) -> usize {
9806 let slot = self.claim_retained_glyph_uniform_slot();
9807 let uniforms = Self::viewport_uniforms(params);
9808 staged_uploads.stage_at(
9809 UploadTarget::RetainedGlyphUniform,
9810 self.retained_glyph_uniform_offset(slot),
9811 bytemuck::bytes_of(&uniforms),
9812 );
9813 slot
9814 }
9815
9816 #[cfg(not(target_arch = "wasm32"))]
9817 fn claim_retained_glyph_uniform_slot(&mut self) -> usize {
9818 let slot = self.retained_glyph_uniform_cursor;
9819 self.retained_glyph_uniform_cursor = self.retained_glyph_uniform_cursor.saturating_add(1);
9820 self.ensure_retained_glyph_uniform_capacity(slot.saturating_add(1));
9821 slot
9822 }
9823
9824 #[cfg(not(target_arch = "wasm32"))]
9825 fn retained_glyph_uniform_offset(&self, slot: usize) -> u64 {
9826 self.retained_glyph_uniform_stride * slot as u64
9827 }
9828
9829 #[cfg(not(target_arch = "wasm32"))]
9830 fn retained_glyph_uniform_dynamic_offset(&self, slot: usize) -> Result<u32, String> {
9831 let offset = self.retained_glyph_uniform_offset(slot);
9832 u32::try_from(offset).map_err(|_| {
9833 "retained glyph uniform offset exceeded WGPU dynamic offset range".to_string()
9834 })
9835 }
9836
9837 #[cfg(not(target_arch = "wasm32"))]
9838 fn ensure_retained_glyph_uniform_capacity(&mut self, required_slots: usize) {
9839 if required_slots <= self.retained_glyph_uniform_capacity {
9840 return;
9841 }
9842 let new_capacity = required_slots
9843 .next_power_of_two()
9844 .max(INITIAL_RETAINED_GLYPH_UNIFORM_SLOTS);
9845 self.retained_glyph_uniform_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
9846 label: Some("Retained Glyph Uniform Buffer"),
9847 size: self.retained_glyph_uniform_stride * new_capacity as u64,
9848 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
9849 mapped_at_creation: false,
9850 });
9851 self.retained_glyph_uniform_bind_group =
9852 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
9853 label: Some("Retained Glyph Uniform Bind Group"),
9854 layout: &self.retained_glyph_uniform_bind_group_layout,
9855 entries: &[wgpu::BindGroupEntry {
9856 binding: 0,
9857 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
9858 buffer: &self.retained_glyph_uniform_buffer,
9859 offset: 0,
9860 size: wgpu::BufferSize::new(std::mem::size_of::<Uniforms>() as u64),
9861 }),
9862 }],
9863 });
9864 self.retained_glyph_uniform_capacity = new_capacity;
9865 }
9866
9867 #[cfg(target_arch = "wasm32")]
9868 fn prepare_wasm_viewport_uniforms(&mut self, params: ViewportUniformParams) -> usize {
9869 let slot = self.claim_wasm_uniform_batch();
9870 let uniforms = Self::viewport_uniforms(params);
9871 let bytes = bytemuck::bytes_of(&uniforms);
9872 let upload_stats = self.frame_graph_executor.upload_buffer(
9873 &self.queue,
9874 &self.wasm_uniform_batches[slot].buffer,
9875 0,
9876 bytes,
9877 );
9878 self.frame_stats.record_command_stats(upload_stats);
9879 slot
9880 }
9881
9882 #[cfg(target_arch = "wasm32")]
9883 fn claim_wasm_uniform_batch(&mut self) -> usize {
9884 let slot = self.wasm_uniform_batch_cursor;
9885 self.wasm_uniform_batch_cursor += 1;
9886 while self.wasm_uniform_batches.len() <= slot {
9887 self.wasm_uniform_batches.push(UniformBatchBuffer::new(
9888 &self.device,
9889 &self.uniform_bind_group_layout,
9890 ));
9891 }
9892 slot
9893 }
9894
9895 #[cfg(target_arch = "wasm32")]
9896 fn claim_wasm_shape_batch(&mut self) -> usize {
9897 let slot = self.wasm_shape_batch_cursor;
9898 self.wasm_shape_batch_cursor += 1;
9899 while self.wasm_shape_batches.len() <= slot {
9900 self.wasm_shape_batches.push(ShapeBatchBuffers::new(
9901 &self.device,
9902 &self.shape_bind_group_layout,
9903 &self.identity_similarity_buffer,
9904 self.dummy_paint_buffer.as_ref(),
9905 self.shape_batch_limits,
9906 ));
9907 }
9908 slot
9909 }
9910
9911 #[cfg(target_arch = "wasm32")]
9912 fn claim_wasm_image_batch(&mut self) -> usize {
9913 let slot = self.wasm_image_batch_cursor;
9914 self.wasm_image_batch_cursor += 1;
9915 while self.wasm_image_batches.len() <= slot {
9916 self.wasm_image_batches
9917 .push(ImageBatchBuffers::new(&self.device));
9918 }
9919 slot
9920 }
9921
9922 #[cfg(target_arch = "wasm32")]
9923 fn write_wasm_buffer(&self, buffer: &wgpu::Buffer, bytes: &[u8]) {
9924 let upload_stats = self
9925 .frame_graph_executor
9926 .upload_buffer(&self.queue, buffer, 0, bytes);
9927 self.frame_stats.record_command_stats(upload_stats);
9928 }
9929
9930 fn take_staged_uploads(&mut self) -> StagedBufferUploads {
9931 let mut staged_uploads = std::mem::take(&mut self.staged_uploads);
9932 debug_assert!(
9933 staged_uploads.is_empty(),
9934 "renderer-owned staged uploads should be restored as empty scratch storage"
9935 );
9936 staged_uploads.clear();
9937 staged_uploads
9938 }
9939
9940 fn restore_staged_uploads(&mut self, mut staged_uploads: StagedBufferUploads) {
9941 staged_uploads.clear();
9942 self.staged_uploads = staged_uploads;
9943 }
9944
9945 #[cfg(not(target_arch = "wasm32"))]
9946 fn ensure_upload_buffer_capacity(&mut self, required_bytes: u64) {
9947 if required_bytes <= self.upload_buffer.size() {
9948 return;
9949 }
9950
9951 let new_size = required_bytes
9952 .next_power_of_two()
9953 .max(INITIAL_UPLOAD_BUFFER_BYTES);
9954 self.upload_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
9955 label: Some("Frame Upload Buffer"),
9956 size: new_size,
9957 usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
9958 mapped_at_creation: false,
9959 });
9960 }
9961
9962 fn flush_staged_uploads_at(
9963 &mut self,
9964 encoder: &mut wgpu::CommandEncoder,
9965 staged_uploads: &StagedBufferUploads,
9966 upload_buffer_offset: u64,
9967 ) {
9968 if staged_uploads.is_empty() {
9969 return;
9970 }
9971 debug_assert_eq!(
9972 upload_buffer_offset % wgpu::COPY_BUFFER_ALIGNMENT,
9973 0,
9974 "upload-buffer base offset must satisfy copy alignment"
9975 );
9976
9977 #[cfg(target_arch = "wasm32")]
9978 {
9979 let _ = upload_buffer_offset;
9980 let _ = encoder;
9981 debug_assert!(
9982 staged_uploads.is_empty(),
9983 "wasm draw uploads use retained per-batch resource slots"
9984 );
9985 return;
9986 }
9987
9988 #[cfg(not(target_arch = "wasm32"))]
9989 {
9990 self.ensure_upload_buffer_capacity(
9991 upload_buffer_offset + staged_uploads.bytes.len() as u64,
9992 );
9993 let upload_stats = self.frame_graph_executor.upload_buffer(
9994 &self.queue,
9995 &self.upload_buffer,
9996 upload_buffer_offset,
9997 &staged_uploads.bytes,
9998 );
9999 self.frame_stats.record_command_stats(upload_stats);
10000
10001 for copy in &staged_uploads.copies {
10002 let target_buffer = match copy.target {
10003 UploadTarget::Uniform => &self.uniform_buffer,
10004 UploadTarget::ShapeData => &self.shape_buffers.shape_buffer,
10005 UploadTarget::ShapeGradient => &self.shape_buffers.gradient_buffer,
10006 UploadTarget::ImageVertex => &self.image_vertex_buffer,
10007 UploadTarget::ImageIndex => &self.image_index_buffer,
10008 UploadTarget::RetainedGlyphUniform => &self.retained_glyph_uniform_buffer,
10009 UploadTarget::ReplayTransform => &self.replay_slots.transform_buffer,
10010 UploadTarget::ReplayPaintData(slot) => {
10011 let Some(entry) = self.replay_slots.slots.get(&slot) else {
10014 continue;
10015 };
10016 &entry.paint_buffer
10017 }
10018 };
10019 encoder.copy_buffer_to_buffer(
10020 &self.upload_buffer,
10021 upload_buffer_offset + copy.source_offset,
10022 target_buffer,
10023 copy.target_offset,
10024 copy.size,
10025 );
10026 }
10027 }
10028 }
10029
10030 #[allow(clippy::too_many_arguments)]
10031 fn encode_shadow_draw<C: FrameCommandRecorder>(
10032 &mut self,
10033 frame_encoder: &mut C,
10034 target_view: &wgpu::TextureView,
10035 shadow: &ShadowDraw,
10036 width: u32,
10037 height: u32,
10038 root_scale: f32,
10039 ) {
10040 if shadow.shapes.is_empty() && shadow.texts.is_empty() {
10041 return;
10042 }
10043
10044 let shape_bounds_opt = shadow
10045 .shapes
10046 .iter()
10047 .map(|(shape, _)| shape.rect)
10048 .reduce(|a, b| Rect {
10049 x: a.x.min(b.x),
10050 y: a.y.min(b.y),
10051 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
10052 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
10053 });
10054
10055 let text_bounds_opt = shadow
10056 .texts
10057 .iter()
10058 .map(|text| text.rect)
10059 .reduce(|a, b| Rect {
10060 x: a.x.min(b.x),
10061 y: a.y.min(b.y),
10062 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
10063 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
10064 });
10065
10066 let combined_bounds = match (shape_bounds_opt, text_bounds_opt) {
10067 (Some(s), Some(t)) => Some(Rect {
10068 x: s.x.min(t.x),
10069 y: s.y.min(t.y),
10070 width: (s.x + s.width).max(t.x + t.width) - s.x.min(t.x),
10071 height: (s.y + s.height).max(t.y + t.height) - s.y.min(t.y),
10072 }),
10073 (Some(s), None) => Some(s),
10074 (None, Some(t)) => Some(t),
10075 (None, None) => None,
10076 };
10077
10078 let Some(shape_bounds) = combined_bounds else {
10079 return;
10080 };
10081
10082 let blur_margin = blur_extent_margin(shadow.blur_radius);
10083 let source_blur_bounds = Rect {
10084 x: shape_bounds.x - blur_margin,
10085 y: shape_bounds.y - blur_margin,
10086 width: shape_bounds.width + blur_margin * 2.0,
10087 height: shape_bounds.height + blur_margin * 2.0,
10088 };
10089 let mut visible_blur_bounds = source_blur_bounds;
10090 if let Some(clip) = shadow.clip {
10091 let clip_expanded = Rect {
10092 x: clip.x - blur_margin,
10093 y: clip.y - blur_margin,
10094 width: clip.width + blur_margin * 2.0,
10095 height: clip.height + blur_margin * 2.0,
10096 };
10097 let Some(intersection) = visible_blur_bounds.intersect(clip_expanded) else {
10098 return;
10099 };
10100 visible_blur_bounds = intersection;
10101 }
10102 let processing_scissor =
10103 scissor_rect_for_rect(visible_blur_bounds, root_scale, width, height);
10104 if processing_scissor.is_none() {
10105 return;
10106 }
10107
10108 if shadow.blur_radius <= 0.0 {
10110 for (shape, blend_mode) in &shadow.shapes {
10111 self.encode_shapes_pass(
10112 frame_encoder,
10113 target_view,
10114 std::iter::once(shape),
10115 &shadow.brushes,
10116 *blend_mode,
10117 width,
10118 height,
10119 root_scale,
10120 wgpu::LoadOp::Load,
10121 [0.0, 0.0],
10122 );
10123 frame_encoder.record_pass();
10124 }
10125 if !shadow.texts.is_empty() {
10126 let mut staged_uploads = self.take_staged_uploads();
10127 let viewport = ViewportUniformParams {
10128 width,
10129 height,
10130 offset: [0.0, 0.0],
10131 };
10132 match self.prepare_text_image_draw_cmds(
10133 shadow.texts.iter(),
10134 viewport,
10135 root_scale,
10136 &mut staged_uploads,
10137 ) {
10138 Ok(prepared_images) if !prepared_images.is_empty() => {
10139 let upload_offset = frame_encoder
10140 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
10141 self.flush_staged_uploads_at(
10142 frame_encoder.encoder(),
10143 &staged_uploads,
10144 upload_offset,
10145 );
10146 let draw_result = {
10147 let mut render_pass = frame_encoder.encoder().begin_render_pass(
10148 &wgpu::RenderPassDescriptor {
10149 label: Some("Zero Blur Shadow Text Image Pass"),
10150 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
10151 view: target_view,
10152 resolve_target: None,
10153 depth_slice: None,
10154 ops: wgpu::Operations {
10155 load: wgpu::LoadOp::Load,
10156 store: wgpu::StoreOp::Store,
10157 },
10158 })],
10159 depth_stencil_attachment: None,
10160 timestamp_writes: None,
10161 occlusion_query_set: None,
10162 multiview_mask: None,
10163 },
10164 );
10165 self.draw_prepared_images(
10166 &mut render_pass,
10167 &prepared_images,
10168 BlendMode::SrcOver,
10169 )
10170 };
10171 self.scratch_image_cmds = prepared_images.into_cmds();
10172 if let Err(e) = draw_result {
10173 eprintln!("Failed to draw text for zero-blur shadow: {}", e);
10174 } else {
10175 self.frame_stats.bump_text();
10176 frame_encoder.record_pass();
10177 }
10178 }
10179 Ok(prepared_images) => {
10180 self.scratch_image_cmds = prepared_images.into_cmds();
10181 }
10182 Err(e) => {
10183 eprintln!("Failed to prepare text image for zero-blur shadow: {}", e);
10184 }
10185 }
10186 self.restore_staged_uploads(staged_uploads);
10187 }
10188 return;
10189 }
10190
10191 let Some(device_bounds) =
10193 device_pixel_bounds_for_rect(visible_blur_bounds, width, height, root_scale)
10194 else {
10195 return;
10196 };
10197 let bounds_x = device_bounds.x;
10198 let bounds_y = device_bounds.y;
10199 let bounds_w = device_bounds.width;
10200 let bounds_h = device_bounds.height;
10201 let pixel_radius = shadow.blur_radius * root_scale;
10202
10203 if shadow.texts.is_empty() && !shadow.shapes.is_empty() {
10204 if let Some(plan) = shape_shadow_surface_plan(
10205 &shadow.shapes,
10206 shadow.clip,
10207 shadow.blur_radius,
10208 width,
10209 height,
10210 root_scale,
10211 self.max_texture_dim(),
10212 ) {
10213 if self.encode_shape_only_blurred_shadow_draw(
10214 frame_encoder,
10215 target_view,
10216 shadow,
10217 plan.source_device_bounds,
10218 plan.pixel_radius,
10219 plan.processing_scissor,
10220 width,
10221 height,
10222 root_scale,
10223 ) {
10224 return;
10225 }
10226 }
10227 }
10228
10229 if !shadow.texts.is_empty() {
10230 self.frame_stats.record_shadow_text_blur_fallback();
10231 }
10232
10233 let device = self.device.clone();
10234 let source_descriptor =
10235 self.transient_offscreen_descriptor("Shadow Source", bounds_w, bounds_h);
10236 let source = frame_encoder.acquire_transient_offscreen(&device, source_descriptor);
10237 let viewport_offset = [bounds_x, bounds_y];
10238 let mut next_load_op = wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT);
10239 let source_outcome = self.encode_shadow_shape_source_passes(
10240 frame_encoder,
10241 &source.view,
10242 &shadow.shapes,
10243 &shadow.brushes,
10244 bounds_w,
10245 bounds_h,
10246 viewport_offset,
10247 root_scale,
10248 &mut next_load_op,
10249 );
10250 frame_encoder.record_passes(source_outcome.pass_count);
10251 let mut rendered_any = source_outcome.rendered_any;
10252
10253 if !shadow.texts.is_empty() {
10254 let mut shifted_texts = shadow.texts.clone();
10255 for text in &mut shifted_texts {
10256 text.rect.x -= viewport_offset[0] / root_scale;
10257 text.rect.y -= viewport_offset[1] / root_scale;
10258 if let Some(clip) = text.clip.as_mut() {
10259 clip.x -= viewport_offset[0] / root_scale;
10260 clip.y -= viewport_offset[1] / root_scale;
10261 }
10262 }
10263
10264 let mut staged_uploads = self.take_staged_uploads();
10265 let viewport = ViewportUniformParams {
10266 width: bounds_w,
10267 height: bounds_h,
10268 offset: [0.0, 0.0],
10269 };
10270 match self.prepare_text_image_draw_cmds(
10271 shifted_texts.iter(),
10272 viewport,
10273 root_scale,
10274 &mut staged_uploads,
10275 ) {
10276 Ok(prepared_images) if !prepared_images.is_empty() => {
10277 let upload_offset = frame_encoder
10278 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
10279 self.flush_staged_uploads_at(
10280 frame_encoder.encoder(),
10281 &staged_uploads,
10282 upload_offset,
10283 );
10284 let draw_result = {
10285 let mut render_pass = frame_encoder.encoder().begin_render_pass(
10286 &wgpu::RenderPassDescriptor {
10287 label: Some("Shadow Source Text Image Pass"),
10288 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
10289 view: &source.view,
10290 resolve_target: None,
10291 depth_slice: None,
10292 ops: wgpu::Operations {
10293 load: next_load_op,
10294 store: wgpu::StoreOp::Store,
10295 },
10296 })],
10297 depth_stencil_attachment: None,
10298 timestamp_writes: None,
10299 occlusion_query_set: None,
10300 multiview_mask: None,
10301 },
10302 );
10303 self.draw_prepared_images(
10304 &mut render_pass,
10305 &prepared_images,
10306 BlendMode::SrcOver,
10307 )
10308 };
10309 self.scratch_image_cmds = prepared_images.into_cmds();
10310 if let Err(e) = draw_result {
10311 eprintln!("Failed to draw text for shadow: {}", e);
10312 } else {
10313 self.frame_stats.bump_text();
10314 frame_encoder.record_pass();
10315 rendered_any = true;
10316 }
10317 }
10318 Ok(prepared_images) => {
10319 self.scratch_image_cmds = prepared_images.into_cmds();
10320 }
10321 Err(e) => {
10322 eprintln!("Failed to prepare text image for shadow: {}", e);
10323 }
10324 }
10325 self.restore_staged_uploads(staged_uploads);
10326 }
10327
10328 if !rendered_any {
10329 frame_encoder.release_transient_offscreen(source_descriptor, source);
10330 return;
10331 }
10332
10333 let scratch_descriptor =
10334 self.transient_offscreen_descriptor("Shadow Blur Scratch", bounds_w, bounds_h);
10335 let scratch = frame_encoder.acquire_transient_offscreen(&device, scratch_descriptor);
10336 {
10337 self.effect_renderer.encode_blur_scissored_ping_pong_passes(
10338 frame_encoder,
10339 &device,
10340 &source,
10341 &scratch,
10342 &source.view,
10343 pixel_radius,
10344 pixel_radius,
10345 TileMode::Decal,
10346 None, );
10348 }
10349 frame_encoder.record_passes(2);
10350
10351 let clip_scissor = shadow
10352 .clip
10353 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
10354 let scissor = clip_scissor.or(processing_scissor);
10355 let rounded_mask = inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
10356 mask.rect[0] -= viewport_offset[0];
10359 mask.rect[1] -= viewport_offset[1];
10360 mask
10361 });
10362 let dest_viewport = Some((
10363 viewport_offset[0],
10364 viewport_offset[1],
10365 bounds_w as f32,
10366 bounds_h as f32,
10367 ));
10368 {
10369 self.effect_renderer
10370 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
10371 frame_encoder,
10372 &device,
10373 &source,
10374 target_view,
10375 1.0,
10376 wgpu::LoadOp::Load,
10377 scissor,
10378 rounded_mask,
10379 BlendMode::SrcOver,
10380 dest_viewport,
10381 CompositeSampleMode::Linear,
10382 );
10383 }
10384 frame_encoder.record_pass();
10385 self.effect_renderer.record_blur_pass();
10386 self.effect_renderer.record_composite_pass();
10387 frame_encoder.release_transient_offscreen(scratch_descriptor, scratch);
10388 frame_encoder.release_transient_offscreen(source_descriptor, source);
10389 }
10390
10391 #[allow(clippy::too_many_arguments)]
10392 fn encode_shadow_shape_source_passes<C: FrameCommandRecorder>(
10393 &mut self,
10394 frame_encoder: &mut C,
10395 source_view: &wgpu::TextureView,
10396 shapes: &[(DrawShape, BlendMode)],
10397 brushes: &[Brush],
10398 width: u32,
10399 height: u32,
10400 viewport_offset: [f32; 2],
10401 root_scale: f32,
10402 next_load_op: &mut wgpu::LoadOp<wgpu::Color>,
10403 ) -> ShadowSourceRenderOutcome {
10404 if shapes.is_empty() {
10405 return ShadowSourceRenderOutcome {
10406 rendered_any: false,
10407 pass_count: 0,
10408 };
10409 }
10410
10411 let mut staged_uploads = self.take_staged_uploads();
10412 let mut rendered_any = false;
10413 let mut pass_count = 0_u32;
10414 let mut start = 0usize;
10415 while start < shapes.len() {
10416 let blend_mode = supported_blend_mode(shapes[start].1);
10417 let mut end = start + 1;
10418 while end < shapes.len()
10419 && end - start < self.shape_batch_limits.max_shapes_per_batch
10420 && supported_blend_mode(shapes[end].1) == blend_mode
10421 {
10422 end += 1;
10423 }
10424
10425 staged_uploads.clear();
10426 let viewport = ViewportUniformParams {
10427 width,
10428 height,
10429 offset: viewport_offset,
10430 };
10431 let Some(prepared_shape) = self.prepare_shapes_batch(
10432 shapes[start..end]
10433 .iter()
10434 .map(|(shape, _blend_mode)| shape)
10435 .filter(|shape| shape_draw_is_visible_in_viewport(shape, viewport, root_scale)),
10436 brushes,
10437 root_scale,
10438 viewport,
10439 &mut staged_uploads,
10440 ) else {
10441 start = end;
10442 continue;
10443 };
10444
10445 let upload_offset =
10446 frame_encoder.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
10447 self.flush_staged_uploads_at(frame_encoder.encoder(), &staged_uploads, upload_offset);
10448
10449 {
10450 let mut render_pass =
10451 frame_encoder
10452 .encoder()
10453 .begin_render_pass(&wgpu::RenderPassDescriptor {
10454 label: Some("Shadow Source Shape Pass"),
10455 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
10456 view: source_view,
10457 resolve_target: None,
10458 depth_slice: None,
10459 ops: wgpu::Operations {
10460 load: *next_load_op,
10461 store: wgpu::StoreOp::Store,
10462 },
10463 })],
10464 depth_stencil_attachment: None,
10465 timestamp_writes: None,
10466 occlusion_query_set: None,
10467 multiview_mask: None,
10468 });
10469 self.draw_prepared_shapes(
10470 &mut render_pass,
10471 blend_mode,
10472 prepared_shape,
10473 width,
10474 height,
10475 &[],
10476 );
10477 }
10478
10479 #[cfg(not(target_arch = "wasm32"))]
10480 {
10481 if fill_area_diag_enabled() {
10482 self.fill_area_diag
10488 .add_offscreen_target_fill(f64::from(width) * f64::from(height));
10489 }
10490 }
10491
10492 pass_count = pass_count.saturating_add(1);
10493 rendered_any = true;
10494 *next_load_op = wgpu::LoadOp::Load;
10495 start = end;
10496 }
10497
10498 self.restore_staged_uploads(staged_uploads);
10499 ShadowSourceRenderOutcome {
10500 rendered_any,
10501 pass_count,
10502 }
10503 }
10504
10505 #[allow(clippy::too_many_arguments)]
10506 fn encode_shape_only_blurred_shadow_draw<C: FrameCommandRecorder>(
10507 &mut self,
10508 frame_encoder: &mut C,
10509 target_view: &wgpu::TextureView,
10510 shadow: &ShadowDraw,
10511 device_bounds: DevicePixelBounds,
10512 pixel_radius: f32,
10513 processing_scissor: Option<(u32, u32, u32, u32)>,
10514 width: u32,
10515 height: u32,
10516 root_scale: f32,
10517 ) -> bool {
10518 let bounds_w = device_bounds.width;
10519 let bounds_h = device_bounds.height;
10520 let viewport_offset = [device_bounds.x, device_bounds.y];
10521 let cache_key = shape_shadow_surface_cache_key(
10522 &shadow.shapes,
10523 &shadow.brushes,
10524 device_bounds,
10525 pixel_radius,
10526 root_scale,
10527 );
10528
10529 if let Some(key) = cache_key {
10530 if let Some(cached) = self.cached_shadow_surface(&key) {
10531 self.frame_stats
10532 .record_shadow_shape_cache_hit(bounds_w, bounds_h);
10533 let clip_scissor = shadow
10534 .clip
10535 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
10536 let scissor = clip_scissor.or(processing_scissor);
10537 let rounded_mask =
10538 inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
10539 mask.rect[0] -= viewport_offset[0];
10540 mask.rect[1] -= viewport_offset[1];
10541 mask
10542 });
10543 let dest_viewport = Some((
10544 viewport_offset[0],
10545 viewport_offset[1],
10546 bounds_w as f32,
10547 bounds_h as f32,
10548 ));
10549 {
10550 self.effect_renderer
10551 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
10552 frame_encoder,
10553 &self.device,
10554 &cached,
10555 target_view,
10556 1.0,
10557 wgpu::LoadOp::Load,
10558 scissor,
10559 rounded_mask,
10560 BlendMode::SrcOver,
10561 dest_viewport,
10562 CompositeSampleMode::Nearest,
10563 );
10564 }
10565 frame_encoder.record_pass();
10566 self.effect_renderer.record_composite_pass();
10567 return true;
10568 }
10569 self.frame_stats
10570 .record_shadow_shape_cache_miss(bounds_w, bounds_h);
10571 self.frame_stats.maybe_print_shadow_shape_cache_miss(
10572 bounds_w,
10573 bounds_h,
10574 key.content_hash,
10575 pixel_radius,
10576 viewport_offset,
10577 shadow.shapes.len(),
10578 shadow.clip,
10579 );
10580 }
10581
10582 let device = self.device.clone();
10583 let source_descriptor =
10584 self.transient_offscreen_descriptor("Shape Shadow Source", bounds_w, bounds_h);
10585 let source_is_cacheable = cache_key.is_some();
10586 let source = if source_is_cacheable {
10587 self.acquire_retained_surface(bounds_w, bounds_h)
10588 } else {
10589 frame_encoder.acquire_transient_offscreen(&device, source_descriptor)
10590 };
10591 let scratch_descriptor =
10592 self.transient_offscreen_descriptor("Shape Shadow Blur Scratch", bounds_w, bounds_h);
10593 let scratch = frame_encoder.acquire_transient_offscreen(&device, scratch_descriptor);
10594 let mut next_load_op = wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT);
10595 let source_outcome = self.encode_shadow_shape_source_passes(
10596 frame_encoder,
10597 &source.view,
10598 &shadow.shapes,
10599 &shadow.brushes,
10600 bounds_w,
10601 bounds_h,
10602 viewport_offset,
10603 root_scale,
10604 &mut next_load_op,
10605 );
10606 frame_encoder.record_passes(source_outcome.pass_count);
10607
10608 if !source_outcome.rendered_any {
10609 frame_encoder.release_transient_offscreen(scratch_descriptor, scratch);
10610 if source_is_cacheable {
10611 self.defer_offscreen_release(source);
10612 } else {
10613 frame_encoder.release_transient_offscreen(source_descriptor, source);
10614 }
10615 return true;
10616 }
10617
10618 {
10619 self.effect_renderer.encode_blur_scissored_ping_pong_passes(
10620 frame_encoder,
10621 &device,
10622 &source,
10623 &scratch,
10624 &source.view,
10625 pixel_radius,
10626 pixel_radius,
10627 TileMode::Decal,
10628 None,
10629 );
10630 }
10631 frame_encoder.record_passes(2);
10632
10633 let clip_scissor = shadow
10634 .clip
10635 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
10636 let scissor = clip_scissor.or(processing_scissor);
10637 let rounded_mask = inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
10638 mask.rect[0] -= viewport_offset[0];
10639 mask.rect[1] -= viewport_offset[1];
10640 mask
10641 });
10642 let dest_viewport = Some((
10643 viewport_offset[0],
10644 viewport_offset[1],
10645 bounds_w as f32,
10646 bounds_h as f32,
10647 ));
10648 {
10649 self.effect_renderer
10650 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
10651 frame_encoder,
10652 &device,
10653 &source,
10654 target_view,
10655 1.0,
10656 wgpu::LoadOp::Load,
10657 scissor,
10658 rounded_mask,
10659 BlendMode::SrcOver,
10660 dest_viewport,
10661 CompositeSampleMode::Nearest,
10662 );
10663 }
10664 frame_encoder.record_pass();
10665
10666 self.effect_renderer.record_blur_pass();
10667 self.effect_renderer.record_composite_pass();
10668 frame_encoder.release_transient_offscreen(scratch_descriptor, scratch);
10669 if let Some(key) = cache_key {
10670 self.insert_cached_shadow_surface(key, source);
10671 } else {
10672 frame_encoder.release_transient_offscreen(source_descriptor, source);
10673 }
10674 true
10675 }
10676
10677 fn prepare_shapes_batch<'a, I>(
10678 &mut self,
10679 layer_shapes: I,
10680 brushes: &[Brush],
10681 root_scale: f32,
10682 viewport: ViewportUniformParams,
10683 staged_uploads: &mut StagedBufferUploads,
10684 ) -> Option<PreparedShapeBatch>
10685 where
10686 I: Iterator<Item = &'a DrawShape>,
10687 {
10688 #[cfg(target_arch = "wasm32")]
10689 let _ = staged_uploads;
10690
10691 let shape_refs: Vec<&DrawShape> = layer_shapes
10696 .take(self.shape_batch_limits.max_shapes_per_batch)
10697 .collect();
10698 let shape_count = shape_refs.len();
10699 if shape_count == 0 {
10700 return None;
10701 }
10702
10703 let mut gradient_offsets: Vec<u32> = Vec::with_capacity(shape_count + 1);
10707 let mut total_gradient_stops = 0u32;
10708 gradient_offsets.push(0);
10709 for shape in &shape_refs {
10710 total_gradient_stops += shape_gradient_stop_count(shape, brushes) as u32;
10711 gradient_offsets.push(total_gradient_stops);
10712 }
10713
10714 self.scratch_shape_data.clear();
10715 self.scratch_shape_data
10716 .resize(shape_count, ShapeData::zeroed());
10717 self.scratch_gradients.clear();
10718 self.scratch_gradients
10719 .resize(total_gradient_stops as usize, GradientStop::zeroed());
10720
10721 convert_shapes_into_outputs(
10722 &shape_refs,
10723 brushes,
10724 &gradient_offsets,
10725 root_scale,
10726 &mut self.scratch_shape_data,
10727 &mut self.scratch_gradients,
10728 );
10729 #[cfg(not(target_arch = "wasm32"))]
10730 {
10731 if fill_area_diag_enabled() {
10732 self.fill_area_diag
10733 .add_shape_quads(&self.scratch_shape_data, viewport);
10734 }
10735 }
10736
10737 #[cfg(not(target_arch = "wasm32"))]
10738 {
10739 self.shape_buffers.ensure_capacity(
10740 &self.device,
10741 &self.shape_bind_group_layout,
10742 &self.identity_similarity_buffer,
10743 self.dummy_paint_buffer.as_ref(),
10744 shape_count,
10745 self.scratch_gradients.len().max(1),
10746 );
10747 self.stage_viewport_uniforms(staged_uploads, viewport);
10748 staged_uploads.stage(
10749 UploadTarget::ShapeData,
10750 bytemuck::cast_slice(&self.scratch_shape_data),
10751 );
10752 if !self.scratch_gradients.is_empty() {
10753 staged_uploads.stage(
10754 UploadTarget::ShapeGradient,
10755 bytemuck::cast_slice(&self.scratch_gradients),
10756 );
10757 }
10758 }
10759
10760 #[cfg(target_arch = "wasm32")]
10761 let shape_slot = {
10762 let slot = self.claim_wasm_shape_batch();
10763 {
10764 let buffers = &mut self.wasm_shape_batches[slot];
10765 buffers.ensure_capacity(
10766 &self.device,
10767 &self.shape_bind_group_layout,
10768 &self.identity_similarity_buffer,
10769 self.dummy_paint_buffer.as_ref(),
10770 shape_count,
10771 self.scratch_gradients.len().max(1),
10772 );
10773 }
10774 let buffers = &self.wasm_shape_batches[slot];
10775 self.write_wasm_buffer(
10776 &buffers.shape_buffer,
10777 bytemuck::cast_slice(&self.scratch_shape_data),
10778 );
10779 if !self.scratch_gradients.is_empty() {
10780 self.write_wasm_buffer(
10781 &buffers.gradient_buffer,
10782 bytemuck::cast_slice(&self.scratch_gradients),
10783 );
10784 }
10785 slot
10786 };
10787
10788 #[cfg(target_arch = "wasm32")]
10789 let uniform_slot = self.prepare_wasm_viewport_uniforms(viewport);
10790
10791 Some(PreparedShapeBatch {
10792 vertex_start: 0,
10793 vertex_count: shape_count as u32 * 6,
10794 has_gradient: total_gradient_stops > 0,
10795 #[cfg(target_arch = "wasm32")]
10796 shape_slot,
10797 #[cfg(target_arch = "wasm32")]
10798 uniform_slot,
10799 })
10800 }
10801
10802 #[cfg(not(target_arch = "wasm32"))]
10809 fn prepare_shapes_batch_direct<'a, I, C: FrameCommandRecorder>(
10810 &mut self,
10811 frame_encoder: &mut C,
10812 layer_shapes: I,
10813 brushes: &[Brush],
10814 root_scale: f32,
10815 viewport: ViewportUniformParams,
10816 staged_uploads: &mut StagedBufferUploads,
10817 ) -> Option<(PreparedShapeBatch, u64)>
10818 where
10819 I: Iterator<Item = &'a DrawShape>,
10820 {
10821 let shape_refs: Vec<&DrawShape> = layer_shapes
10822 .take(self.shape_batch_limits.max_shapes_per_batch)
10823 .collect();
10824 let shape_count = shape_refs.len();
10825 if shape_count == 0 {
10826 return None;
10827 }
10828
10829 let mut gradient_offsets: Vec<u32> = Vec::with_capacity(shape_count + 1);
10830 let mut total_gradient_stops = 0u32;
10831 gradient_offsets.push(0);
10832 for shape in &shape_refs {
10833 total_gradient_stops += shape_gradient_stop_count(shape, brushes) as u32;
10834 gradient_offsets.push(total_gradient_stops);
10835 }
10836
10837 self.shape_buffers.ensure_capacity(
10838 &self.device,
10839 &self.shape_bind_group_layout,
10840 &self.identity_similarity_buffer,
10841 self.dummy_paint_buffer.as_ref(),
10842 shape_count,
10843 (total_gradient_stops as usize).max(1),
10844 );
10845
10846 self.scratch_shape_data.clear();
10847 self.scratch_shape_data
10848 .resize(shape_count, ShapeData::zeroed());
10849 self.scratch_gradients.clear();
10850 self.scratch_gradients
10851 .resize(total_gradient_stops as usize, GradientStop::zeroed());
10852 convert_shapes_into_outputs(
10853 &shape_refs,
10854 brushes,
10855 &gradient_offsets,
10856 root_scale,
10857 &mut self.scratch_shape_data,
10858 &mut self.scratch_gradients,
10859 );
10860 if fill_area_diag_enabled() {
10861 self.fill_area_diag
10862 .add_shape_quads(&self.scratch_shape_data, viewport);
10863 }
10864
10865 let uniform_len = std::mem::size_of::<Uniforms>() as u64;
10872 let shape_len = (shape_count * std::mem::size_of::<ShapeData>()) as u64;
10873 let gradient_len = total_gradient_stops as u64 * std::mem::size_of::<GradientStop>() as u64;
10874 let total_len = uniform_len + shape_len + gradient_len;
10875 let upload_base = frame_encoder.allocate_staged_upload_bytes(total_len);
10876 self.ensure_upload_buffer_capacity(upload_base + total_len);
10877
10878 let shape_off = uniform_len;
10879 let gradient_off = shape_off + shape_len;
10880
10881 let uniforms = Self::viewport_uniforms(viewport);
10882 let mut upload_stats = self.frame_graph_executor.upload_buffer(
10883 &self.queue,
10884 &self.upload_buffer,
10885 upload_base,
10886 bytemuck::bytes_of(&uniforms),
10887 );
10888 upload_stats.upload_bytes += self
10889 .frame_graph_executor
10890 .upload_buffer(
10891 &self.queue,
10892 &self.upload_buffer,
10893 upload_base + shape_off,
10894 bytemuck::cast_slice(&self.scratch_shape_data),
10895 )
10896 .upload_bytes;
10897 if !self.scratch_gradients.is_empty() {
10898 upload_stats.upload_bytes += self
10899 .frame_graph_executor
10900 .upload_buffer(
10901 &self.queue,
10902 &self.upload_buffer,
10903 upload_base + gradient_off,
10904 bytemuck::cast_slice(&self.scratch_gradients),
10905 )
10906 .upload_bytes;
10907 }
10908 self.frame_stats.record_command_stats(upload_stats);
10909
10910 staged_uploads.record_upload_copy(UploadTarget::Uniform, 0, 0, uniform_len);
10911 staged_uploads.record_upload_copy(UploadTarget::ShapeData, shape_off, 0, shape_len);
10912 staged_uploads.record_upload_copy(
10913 UploadTarget::ShapeGradient,
10914 gradient_off,
10915 0,
10916 gradient_len,
10917 );
10918
10919 Some((
10920 PreparedShapeBatch {
10921 vertex_start: 0,
10922 vertex_count: shape_count as u32 * 6,
10923 has_gradient: total_gradient_stops > 0,
10924 },
10925 upload_base,
10926 ))
10927 }
10928
10929 pub(crate) fn replay_supported(&self) -> bool {
10935 #[cfg(target_arch = "wasm32")]
10939 {
10940 false
10941 }
10942 #[cfg(not(target_arch = "wasm32"))]
10943 {
10944 self.shape_batch_limits.storage
10945 }
10946 }
10947
10948 pub(crate) fn restore_replay_ack_confirmations(
10954 &mut self,
10955 confirmations: Vec<crate::frame_packet::ReplayConfirmation>,
10956 ) {
10957 #[cfg(not(target_arch = "wasm32"))]
10958 {
10959 self.replay_ack_confirmations = confirmations;
10960 }
10961 #[cfg(target_arch = "wasm32")]
10962 let _ = confirmations;
10963 }
10964
10965 #[cfg(not(target_arch = "wasm32"))]
10980 fn consume_replay_ops(
10981 &mut self,
10982 mut ops: crate::frame_packet::ReplayFrameOps,
10983 shapes: &[DrawShape],
10984 brushes: &[Brush],
10985 root_scale: f32,
10986 ) -> (
10987 crate::frame_packet::ReplayAck,
10988 crate::frame_packet::ReplayFrameOps,
10989 ) {
10990 if ops.generation < self.store_feed_generation {
10991 self.replay_generation_drops += 1;
10997 log::warn!(
10998 "[command-feed] dropping replay ops of generation {} against store \
10999 generation {} ({} captures, {} patches, {} releases; lifetime drops {})",
11000 ops.generation,
11001 self.store_feed_generation,
11002 ops.captures.len(),
11003 ops.color_patches.len(),
11004 ops.releases.len(),
11005 self.replay_generation_drops,
11006 );
11007 ops.captures.clear();
11008 ops.color_patches.clear();
11009 ops.releases.clear();
11010 return (
11011 crate::frame_packet::ReplayAck {
11012 generation: self.store_feed_generation,
11013 confirmations: Vec::new(),
11014 },
11015 ops,
11016 );
11017 }
11018 if ops.generation > self.store_feed_generation {
11019 self.store_feed_generation = ops.generation;
11026 }
11027 let generation = ops.generation;
11028 for slot in ops.releases.drain(..) {
11031 self.release_replay_slot(slot);
11032 }
11033 let mut confirmations = std::mem::take(&mut self.replay_ack_confirmations);
11036 debug_assert!(confirmations.is_empty());
11037 for capture in ops.captures.drain(..) {
11038 if capture.frame != ops.frame {
11039 log::warn!(
11046 "[command-feed] dropping stale capture for slot {} of {:?} \
11047 (queued frame {}, ops frame {})",
11048 capture.key.1,
11049 capture.key.0,
11050 capture.frame,
11051 ops.frame,
11052 );
11053 continue;
11054 }
11055 let end = capture.shape_start + capture.shape_count;
11056 let Some(slice) = shapes.get(capture.shape_start..end) else {
11057 continue;
11058 };
11059 let refs: Vec<&DrawShape> = slice.iter().collect();
11060 let Some(gpu_slot) = self.capture_replay_slot(&refs, brushes, root_scale) else {
11061 continue;
11062 };
11063 confirmations.push((capture.key, gpu_slot));
11064 }
11065 self.replay_color_patches.clear();
11073 std::mem::swap(&mut self.replay_color_patches, &mut ops.color_patches);
11074 (
11075 crate::frame_packet::ReplayAck {
11076 generation,
11077 confirmations,
11078 },
11079 ops,
11080 )
11081 }
11082
11083 #[cfg(not(target_arch = "wasm32"))]
11088 pub(crate) fn replay_generation_drops(&self) -> u64 {
11089 self.replay_generation_drops
11090 }
11091
11092 #[cfg(not(target_arch = "wasm32"))]
11100 pub(crate) fn replay_ops_roundtrip_for_tests(&mut self, generation_skew: u64) -> usize {
11101 let generation = self.store_feed_generation.wrapping_add(generation_skew);
11102 let ops = crate::shape_replay::SHAPE_REPLAY
11103 .with(|state| state.borrow_mut().take_frame_ops(generation));
11104 let (ack, recycled) = self.consume_replay_ops(ops, &[], &[], 1.0);
11105 let confirmed = ack.confirmations.len();
11106 self.replay_ack_confirmations = crate::shape_replay::SHAPE_REPLAY
11107 .with(|state| state.borrow_mut().apply_ack(ack, recycled));
11108 confirmed
11109 }
11110
11111 #[cfg(not(target_arch = "wasm32"))]
11118 fn stage_replay_patches(&mut self, staged_uploads: &mut StagedBufferUploads) {
11119 std::mem::swap(
11127 &mut self.replay_color_patches,
11128 &mut self.color_patch_scratch,
11129 );
11130 let total_patches = self.color_patch_scratch.len();
11131 if total_patches == 0 {
11132 self.replay_upload_stats.note_frame(0, 0, 0, 0, 0);
11133 return;
11134 }
11135
11136 #[derive(Clone, Copy)]
11142 struct DirtySpan {
11143 paint_min: u32,
11144 paint_max: u32,
11145 }
11146 const CLEAN: DirtySpan = DirtySpan {
11147 paint_min: u32::MAX,
11148 paint_max: 0,
11149 };
11150 let mut dirty: std::collections::HashMap<
11151 u32,
11152 DirtySpan,
11153 cranpose_ui_graphics::FxBuildHasher,
11154 > = std::collections::HashMap::default();
11155
11156 for patch in &self.color_patch_scratch {
11158 let Some(slot) = self.replay_slots.slots.get_mut(&patch.slot) else {
11159 continue;
11160 };
11161 let Some(paint) = slot.paint_mirror.get_mut(patch.shape_index as usize) else {
11162 continue;
11163 };
11164 *paint = patch.color;
11165 let span = dirty.entry(patch.slot).or_insert(CLEAN);
11166 span.paint_min = span.paint_min.min(patch.shape_index);
11167 span.paint_max = span.paint_max.max(patch.shape_index);
11168 }
11169
11170 let mut uploaded_records = 0u64;
11171 let mut uploaded_bytes = 0u64;
11172 let slots_touched = dirty.len() as u64;
11173 for (slot_id, span) in dirty {
11174 let Some(slot) = self.replay_slots.slots.get(&slot_id) else {
11175 continue;
11176 };
11177 if span.paint_min <= span.paint_max {
11178 let range = span.paint_min as usize..span.paint_max as usize + 1;
11179 uploaded_records += range.len() as u64;
11180 uploaded_bytes += (range.len() * std::mem::size_of::<[f32; 4]>()) as u64;
11181 staged_uploads.stage_at(
11182 UploadTarget::ReplayPaintData(slot_id),
11183 range.start as u64 * std::mem::size_of::<[f32; 4]>() as u64,
11184 bytemuck::cast_slice(&slot.paint_mirror[range]),
11185 );
11186 }
11187 }
11188 let ideal_bytes = total_patches as u64 * 16;
11191 self.replay_upload_stats.note_frame(
11192 total_patches as u64,
11193 slots_touched,
11194 uploaded_records,
11195 uploaded_bytes,
11196 ideal_bytes,
11197 );
11198 if cranpose_core::env_flag!("CRANPOSE_COMMAND_REPLAY_DIAG") {
11199 log::warn!(
11200 "[replay-upload] frame: {} patches -> {} records / {:.1} KB staged \
11201 across {} slots (color-only {:.1} KB)",
11202 total_patches,
11203 uploaded_records,
11204 uploaded_bytes as f64 / 1024.0,
11205 slots_touched,
11206 ideal_bytes as f64 / 1024.0,
11207 );
11208 }
11209 self.color_patch_scratch.clear();
11210 }
11211
11212 #[cfg(not(target_arch = "wasm32"))]
11215 pub(crate) fn capture_replay_slot(
11216 &mut self,
11217 shape_refs: &[&DrawShape],
11218 brushes: &[Brush],
11219 root_scale: f32,
11220 ) -> Option<u32> {
11221 if !self.shape_batch_limits.storage || shape_refs.is_empty() {
11222 return None;
11223 }
11224 let id = self.replay_slots.free_ids.pop()?;
11225 let shape_count = shape_refs.len();
11226
11227 let mut gradient_offsets: Vec<u32> = Vec::with_capacity(shape_count + 1);
11228 let mut total_gradient_stops = 0u32;
11229 gradient_offsets.push(0);
11230 for shape in shape_refs {
11231 total_gradient_stops += shape_gradient_stop_count(shape, brushes) as u32;
11232 gradient_offsets.push(total_gradient_stops);
11233 }
11234
11235 let mut shape_data = vec![ShapeData::zeroed(); shape_count];
11236 let mut gradients = vec![GradientStop::zeroed(); (total_gradient_stops as usize).max(1)];
11237 convert_shapes_into_outputs(
11238 shape_refs,
11239 brushes,
11240 &gradient_offsets,
11241 root_scale,
11242 &mut shape_data,
11243 &mut gradients,
11244 );
11245
11246 let shape_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
11247 label: Some("Replay Shape Buffer"),
11248 size: (std::mem::size_of::<ShapeData>() * shape_count) as u64,
11249 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
11250 mapped_at_creation: true,
11251 });
11252 shape_buffer
11253 .slice(..)
11254 .get_mapped_range_mut()
11255 .copy_from_slice(bytemuck::cast_slice(&shape_data));
11256 shape_buffer.unmap();
11257
11258 let gradient_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
11259 label: Some("Replay Gradient Buffer"),
11260 size: (std::mem::size_of::<GradientStop>() * gradients.len()) as u64,
11261 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
11262 mapped_at_creation: true,
11263 });
11264 gradient_buffer
11265 .slice(..)
11266 .get_mapped_range_mut()
11267 .copy_from_slice(bytemuck::cast_slice(&gradients));
11268 gradient_buffer.unmap();
11269
11270 let mut mesh_fill_records: Option<Vec<FillDiagShapeRecord>> = None;
11274 let mesh = if arc_mesh_enabled() {
11275 match build_arc_mesh_vertices(&shape_data) {
11276 Some(build) => {
11277 let cut = if build.quad_area > 0.0 {
11278 (1.0 - build.mesh_area / build.quad_area) * 100.0
11279 } else {
11280 0.0
11281 };
11282 log::warn!(
11288 "[arc-mesh] slot {id}: {} arcs meshed ({} segs), {} passthrough; \
11289 {} unique verts / {} indices (quad path: {} verts); \
11290 quad_px {:.0} -> mesh_px {:.0} (-{:.1}%)",
11291 build.meshed_arcs,
11292 build.meshed_segments,
11293 build.passthrough,
11294 build.vertices.len(),
11295 build.indices.len(),
11296 shape_count * 6,
11297 build.quad_area,
11298 build.mesh_area,
11299 cut,
11300 );
11301 if build.meshed_arcs > 0 && fill_area_diag_enabled() {
11302 mesh_fill_records = Some(fill_diag_capture_records(
11303 &shape_data,
11304 Some((&build.vertices, &build.indices, &build.index_prefix)),
11305 ));
11306 }
11307 (build.meshed_arcs > 0).then(|| {
11310 let vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
11311 label: Some("Replay Mesh Vertex Buffer"),
11312 size: (std::mem::size_of::<MeshVertex>() * build.vertices.len()) as u64,
11313 usage: wgpu::BufferUsages::VERTEX,
11314 mapped_at_creation: true,
11315 });
11316 vertex_buffer
11317 .slice(..)
11318 .get_mapped_range_mut()
11319 .copy_from_slice(bytemuck::cast_slice(&build.vertices));
11320 vertex_buffer.unmap();
11321 let index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
11322 label: Some("Replay Mesh Index Buffer"),
11323 size: (std::mem::size_of::<u32>() * build.indices.len()) as u64,
11324 usage: wgpu::BufferUsages::INDEX,
11325 mapped_at_creation: true,
11326 });
11327 index_buffer
11328 .slice(..)
11329 .get_mapped_range_mut()
11330 .copy_from_slice(bytemuck::cast_slice(&build.indices));
11331 index_buffer.unmap();
11332 ReplaySlotMesh {
11333 vertex_buffer,
11334 index_buffer,
11335 index_prefix: build.index_prefix,
11336 }
11337 })
11338 }
11339 None => {
11340 log::warn!(
11341 "[arc-mesh] slot {id}: geometry byte budget overflowed for \
11342 {shape_count} shapes; whole slot falls back to quad passthrough"
11343 );
11344 None
11345 }
11346 }
11347 } else {
11348 None
11349 };
11350
11351 let fill_diag_shapes = if fill_area_diag_enabled() {
11352 let records =
11353 mesh_fill_records.unwrap_or_else(|| fill_diag_capture_records(&shape_data, None));
11354 self.fill_area_diag.note_retained_capture(id, &records);
11357 records
11358 } else {
11359 Vec::new()
11360 };
11361
11362 let paint: Vec<[f32; 4]> = shape_data.iter().map(|shape| shape.color).collect();
11365 let paint_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
11366 label: Some("Replay Paint Buffer"),
11367 size: (std::mem::size_of::<[f32; 4]>() * shape_count) as u64,
11368 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
11369 mapped_at_creation: true,
11370 });
11371 paint_buffer
11372 .slice(..)
11373 .get_mapped_range_mut()
11374 .copy_from_slice(bytemuck::cast_slice(&paint));
11375 paint_buffer.unmap();
11376
11377 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
11378 label: Some("Replay Shape Bind Group"),
11379 layout: &self.shape_bind_group_layout,
11380 entries: &[
11381 wgpu::BindGroupEntry {
11382 binding: 0,
11383 resource: shape_buffer.as_entire_binding(),
11384 },
11385 wgpu::BindGroupEntry {
11386 binding: 1,
11387 resource: gradient_buffer.as_entire_binding(),
11388 },
11389 wgpu::BindGroupEntry {
11393 binding: 2,
11394 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
11395 buffer: &self.replay_slots.transform_buffer,
11396 offset: 0,
11397 size: Some(
11398 std::num::NonZeroU64::new(
11399 std::mem::size_of::<SimilarityTransform>() as u64
11400 )
11401 .expect("similarity transform is non-empty"),
11402 ),
11403 }),
11404 },
11405 wgpu::BindGroupEntry {
11406 binding: 3,
11407 resource: paint_buffer.as_entire_binding(),
11408 },
11409 ],
11410 });
11411
11412 let capture_epoch = self.replay_slots.next_capture_epoch;
11413 self.replay_slots.next_capture_epoch += 1;
11414 self.replay_slots.slots.insert(
11415 id,
11416 ReplaySlot {
11417 paint_buffer,
11418 bind_group,
11419 shape_count: shape_count as u32,
11420 paint_mirror: paint,
11421 mesh,
11422 capture_epoch,
11423 has_gradient: total_gradient_stops > 0,
11424 fill_diag_shapes,
11425 },
11426 );
11427 Some(id)
11428 }
11429
11430 #[cfg(not(target_arch = "wasm32"))]
11432 pub(crate) fn release_replay_slot(&mut self, id: u32) {
11433 if self.replay_slots.slots.remove(&id).is_some() {
11434 self.replay_slots.free_ids.push(id);
11435 self.retained_bundle_cache.clear();
11441 }
11442 }
11443
11444 #[cfg(not(target_arch = "wasm32"))]
11447 #[doc(hidden)]
11448 pub fn instanced_quads_active(&self) -> bool {
11449 self.instanced_quads.is_some()
11450 }
11451
11452 #[cfg(not(target_arch = "wasm32"))]
11455 #[doc(hidden)]
11456 pub fn replay_slot_mesh_stats(&self) -> (usize, usize) {
11457 let meshed = self
11458 .replay_slots
11459 .slots
11460 .values()
11461 .filter(|slot| slot.mesh.is_some())
11462 .count();
11463 (meshed, self.replay_slots.slots.len())
11464 }
11465
11466 #[cfg(not(target_arch = "wasm32"))]
11470 fn draw_retained_batch(
11471 &self,
11472 render_pass: &mut wgpu::RenderPass<'_>,
11473 retained: &RetainedDraw,
11474 retained_index: usize,
11475 width: u32,
11476 height: u32,
11477 ) {
11478 let Some(slot) = self.replay_slots.slots.get(&retained.slot) else {
11479 return;
11480 };
11481 if retained_index as u32 >= MAX_REPLAY_SLOTS {
11482 return;
11483 }
11484 let first = retained.first_shape.min(slot.shape_count);
11485 let last = retained
11486 .first_shape
11487 .saturating_add(retained.shape_count)
11488 .min(slot.shape_count);
11489 if first >= last {
11490 return;
11491 }
11492 if fill_area_diag_enabled() {
11493 self.fill_area_diag.add_retained_range(
11494 &slot.fill_diag_shapes,
11495 first,
11496 last,
11497 &retained.transform,
11498 );
11499 }
11500 self.frame_stats.bump_shapes();
11501 self.frame_stats.add_draw_calls(1);
11502 render_pass.set_scissor_rect(0, 0, width, height);
11503 let mesh = slot.mesh.as_ref().map(|mesh| (mesh, self.mesh_pipeline()));
11511 match &mesh {
11512 Some((_, mesh_pipeline)) => render_pass.set_pipeline(mesh_pipeline),
11513 None => match &self.instanced_quads {
11514 Some(instanced) if !slot.has_gradient => {
11515 render_pass.set_pipeline(self.instanced_pipeline_solid(instanced))
11516 }
11517 Some(instanced) => {
11518 render_pass.set_pipeline(self.instanced_pipeline(instanced, BlendMode::SrcOver))
11519 }
11520 None if !slot.has_gradient => render_pass.set_pipeline(self.shape_pipeline_solid()),
11521 None => render_pass.set_pipeline(self.shape_pipeline(BlendMode::SrcOver)),
11522 },
11523 }
11524 render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
11525 render_pass.set_bind_group(
11526 1,
11527 &slot.bind_group,
11528 &[retained_index as u32 * REPLAY_TRANSFORM_STRIDE as u32],
11529 );
11530 match mesh {
11531 Some((mesh, _)) => {
11532 render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
11533 render_pass
11534 .set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
11535 render_pass.draw_indexed(
11536 mesh.index_prefix[first as usize]..mesh.index_prefix[last as usize],
11537 0,
11538 0..1,
11539 );
11540 }
11541 None => match &self.instanced_quads {
11542 Some(instanced) => {
11543 render_pass.set_index_buffer(
11544 instanced.index_buffer.slice(..),
11545 wgpu::IndexFormat::Uint16,
11546 );
11547 render_pass.draw_indexed(0..6, 0, first..last);
11548 }
11549 None => render_pass.draw(first * 6..last * 6, 0..1),
11550 },
11551 }
11552 }
11553
11554 #[cfg(not(target_arch = "wasm32"))]
11561 fn retained_bundle_key(
11562 &self,
11563 ordered_items: &[(usize, SegmentDrawItem)],
11564 retained_draws: &[RetainedDraw],
11565 item_range: Range<usize>,
11566 ) -> RetainedBundleKey {
11567 let mut ops = Vec::with_capacity(item_range.len());
11568 for (_, item) in &ordered_items[item_range] {
11569 let SegmentDrawItem::Retained(index) = item else {
11570 continue;
11571 };
11572 let Some(retained) = retained_draws.get(*index) else {
11573 continue;
11574 };
11575 let slot = self.replay_slots.slots.get(&retained.slot);
11576 let (first, last) = match slot {
11577 Some(slot) => (
11578 retained.first_shape.min(slot.shape_count),
11579 retained
11580 .first_shape
11581 .saturating_add(retained.shape_count)
11582 .min(slot.shape_count),
11583 ),
11584 None => (
11585 retained.first_shape,
11586 retained.first_shape.saturating_add(retained.shape_count),
11587 ),
11588 };
11589 ops.push(RetainedBundleOpKey {
11590 slot: retained.slot,
11591 capture_epoch: slot.map(|slot| slot.capture_epoch),
11592 first,
11593 last,
11594 retained_index: *index as u32,
11595 has_mesh: slot.is_some_and(|slot| slot.mesh.is_some())
11596 && self.shape_batch_limits.storage,
11597 });
11598 }
11599 RetainedBundleKey { ops }
11600 }
11601
11602 #[cfg(not(target_arch = "wasm32"))]
11609 fn build_retained_bundle(&self, key: &RetainedBundleKey) -> wgpu::RenderBundle {
11610 let mut encoder =
11611 self.device
11612 .create_render_bundle_encoder(&wgpu::RenderBundleEncoderDescriptor {
11613 label: Some("Retained Stretch Bundle"),
11614 color_formats: &[Some(self.surface_format)],
11618 depth_stencil: None,
11619 sample_count: 1,
11620 multiview: None,
11621 });
11622 for op in &key.ops {
11623 if op.capture_epoch.is_none()
11624 || op.retained_index >= MAX_REPLAY_SLOTS
11625 || op.first >= op.last
11626 {
11627 continue;
11628 }
11629 let Some(slot) = self.replay_slots.slots.get(&op.slot) else {
11630 continue;
11631 };
11632 let mesh = slot.mesh.as_ref().map(|mesh| (mesh, self.mesh_pipeline()));
11633 match &mesh {
11634 Some((_, mesh_pipeline)) => encoder.set_pipeline(mesh_pipeline),
11635 None => match &self.instanced_quads {
11639 Some(instanced) if !slot.has_gradient => {
11640 encoder.set_pipeline(self.instanced_pipeline_solid(instanced))
11641 }
11642 Some(instanced) => {
11643 encoder.set_pipeline(self.instanced_pipeline(instanced, BlendMode::SrcOver))
11644 }
11645 None if !slot.has_gradient => encoder.set_pipeline(self.shape_pipeline_solid()),
11646 None => encoder.set_pipeline(self.shape_pipeline(BlendMode::SrcOver)),
11647 },
11648 }
11649 encoder.set_bind_group(0, &self.uniform_bind_group, &[]);
11650 encoder.set_bind_group(
11651 1,
11652 &slot.bind_group,
11653 &[op.retained_index * REPLAY_TRANSFORM_STRIDE as u32],
11654 );
11655 match mesh {
11656 Some((mesh, _)) => {
11657 encoder.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
11658 encoder
11659 .set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
11660 encoder.draw_indexed(
11661 mesh.index_prefix[op.first as usize]..mesh.index_prefix[op.last as usize],
11662 0,
11663 0..1,
11664 );
11665 }
11666 None => match &self.instanced_quads {
11671 Some(instanced) => {
11672 encoder.set_index_buffer(
11673 instanced.index_buffer.slice(..),
11674 wgpu::IndexFormat::Uint16,
11675 );
11676 encoder.draw_indexed(0..6, 0, op.first..op.last);
11677 }
11678 None => encoder.draw(op.first * 6..op.last * 6, 0..1),
11679 },
11680 }
11681 }
11682 encoder.finish(&wgpu::RenderBundleDescriptor {
11683 label: Some("Retained Stretch Bundle"),
11684 })
11685 }
11686
11687 #[cfg(not(target_arch = "wasm32"))]
11695 fn draw_retained_stretch_bundled(
11696 &mut self,
11697 render_pass: &mut wgpu::RenderPass<'_>,
11698 ordered_items: &[(usize, SegmentDrawItem)],
11699 retained_draws: &[RetainedDraw],
11700 item_range: Range<usize>,
11701 width: u32,
11702 height: u32,
11703 ) {
11704 let key = self.retained_bundle_key(ordered_items, retained_draws, item_range);
11705 if !self.retained_bundle_cache.hit(&key) {
11706 let bundle = self.build_retained_bundle(&key);
11707 self.retained_bundle_cache.insert(key.clone(), bundle);
11708 }
11709 for op in &key.ops {
11712 if op.capture_epoch.is_some()
11713 && op.retained_index < MAX_REPLAY_SLOTS
11714 && op.first < op.last
11715 {
11716 self.frame_stats.bump_shapes();
11717 self.frame_stats.add_draw_calls(1);
11718 if fill_area_diag_enabled() {
11719 let slot = self.replay_slots.slots.get(&op.slot);
11721 let retained = retained_draws.get(op.retained_index as usize);
11722 if let (Some(slot), Some(retained)) = (slot, retained) {
11723 self.fill_area_diag.add_retained_range(
11724 &slot.fill_diag_shapes,
11725 op.first,
11726 op.last,
11727 &retained.transform,
11728 );
11729 }
11730 }
11731 }
11732 }
11733 render_pass.set_scissor_rect(0, 0, width, height);
11738 if let Some(bundle) = self.retained_bundle_cache.get(&key) {
11739 render_pass.execute_bundles(std::iter::once(bundle));
11740 }
11741 }
11742
11743 #[cfg(not(target_arch = "wasm32"))]
11746 #[doc(hidden)]
11747 pub fn retained_bundle_stats(&self) -> (u64, u64) {
11748 self.retained_bundle_cache.stats()
11749 }
11750
11751 #[cfg(not(target_arch = "wasm32"))]
11754 #[doc(hidden)]
11755 pub fn rim_meshes_emitted(&self) -> u64 {
11756 self.rim_meshes_emitted
11757 }
11758
11759 pub fn static_span_stats(&self) -> (u64, u64) {
11762 (self.static_span.hits, self.static_span.recaptures)
11763 }
11764
11765 #[cfg(not(target_arch = "wasm32"))]
11772 fn upload_transient_rim_meshes(&mut self) {
11773 let device = self.device.clone();
11774 let mut upload_stats = crate::frame_graph::FrameCommandStats::default();
11775 if self.rim_mesh_vertices.len() > self.rim_mesh_uploaded_vertices {
11776 let vertex_buffer = self.rim_mesh_vertex_buffer.get_or_insert_with(|| {
11777 device.create_buffer(&wgpu::BufferDescriptor {
11778 label: Some("Rim Mesh Vertex Buffer"),
11779 size: (RIM_MESH_VERTEX_CAPACITY * std::mem::size_of::<MeshVertex>()) as u64,
11780 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
11781 mapped_at_creation: false,
11782 })
11783 });
11784 upload_stats.upload_bytes += self
11785 .frame_graph_executor
11786 .upload_buffer(
11787 &self.queue,
11788 vertex_buffer,
11789 (self.rim_mesh_uploaded_vertices * std::mem::size_of::<MeshVertex>()) as u64,
11790 bytemuck::cast_slice(
11791 &self.rim_mesh_vertices[self.rim_mesh_uploaded_vertices..],
11792 ),
11793 )
11794 .upload_bytes;
11795 self.rim_mesh_uploaded_vertices = self.rim_mesh_vertices.len();
11796 }
11797 if self.rim_mesh_indices.len() > self.rim_mesh_uploaded_indices {
11798 let index_buffer = self.rim_mesh_index_buffer.get_or_insert_with(|| {
11799 device.create_buffer(&wgpu::BufferDescriptor {
11800 label: Some("Rim Mesh Index Buffer"),
11801 size: (RIM_MESH_INDEX_CAPACITY * std::mem::size_of::<u32>()) as u64,
11802 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
11803 mapped_at_creation: false,
11804 })
11805 });
11806 upload_stats.upload_bytes += self
11807 .frame_graph_executor
11808 .upload_buffer(
11809 &self.queue,
11810 index_buffer,
11811 (self.rim_mesh_uploaded_indices * std::mem::size_of::<u32>()) as u64,
11812 bytemuck::cast_slice(&self.rim_mesh_indices[self.rim_mesh_uploaded_indices..]),
11813 )
11814 .upload_bytes;
11815 self.rim_mesh_uploaded_indices = self.rim_mesh_indices.len();
11816 }
11817 if upload_stats.upload_bytes > 0 {
11818 self.frame_stats.record_command_stats(upload_stats);
11819 }
11820 }
11821
11822 fn draw_prepared_shapes(
11823 &self,
11824 render_pass: &mut wgpu::RenderPass<'_>,
11825 blend_mode: BlendMode,
11826 batch: PreparedShapeBatch,
11827 width: u32,
11828 height: u32,
11829 rims: &[RimDraw],
11830 ) {
11831 #[cfg(target_arch = "wasm32")]
11832 let _ = rims;
11833 self.frame_stats.bump_shapes();
11834 self.frame_stats.add_draw_calls(1);
11835 render_pass.set_scissor_rect(0, 0, width, height);
11836 #[cfg(not(target_arch = "wasm32"))]
11837 let (uniform_bind_group, shape_buffers) = (&self.uniform_bind_group, &self.shape_buffers);
11838 #[cfg(target_arch = "wasm32")]
11839 let (uniform_bind_group, shape_buffers) = (
11840 &self.wasm_uniform_batches[batch.uniform_slot].bind_group,
11841 &self.wasm_shape_batches[batch.shape_slot],
11842 );
11843 #[cfg(not(target_arch = "wasm32"))]
11848 if let Some(instanced) = &self.instanced_quads {
11849 assert!(
11850 batch.vertex_start.is_multiple_of(6) && batch.vertex_count.is_multiple_of(6),
11851 "shape batches are whole shapes: vertex range {}..+{} must be \
11852 six-aligned to convert to an instance range",
11853 batch.vertex_start,
11854 batch.vertex_count,
11855 );
11856 let set_instanced_pipeline = |render_pass: &mut wgpu::RenderPass<'_>| {
11859 if blend_mode == BlendMode::SrcOver && !batch.has_gradient {
11860 render_pass.set_pipeline(self.instanced_pipeline_solid(instanced));
11861 } else {
11862 render_pass.set_pipeline(self.instanced_pipeline(instanced, blend_mode));
11863 }
11864 };
11865 set_instanced_pipeline(render_pass);
11866 render_pass.set_bind_group(0, uniform_bind_group, &[]);
11867 render_pass.set_bind_group(1, &shape_buffers.bind_group, &[0]);
11870 let first_shape = batch.vertex_start / 6;
11871 let shape_count = batch.vertex_count / 6;
11872 render_pass
11873 .set_index_buffer(instanced.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
11874 debug_assert!(
11878 rims.windows(2)
11879 .all(|pair| pair[0].shape_index < pair[1].shape_index),
11880 "rim draws must arrive in ascending shape order"
11881 );
11882 let rim_start = rims.partition_point(|rim| rim.shape_index < first_shape);
11883 let rim_end = rims.partition_point(|rim| rim.shape_index < first_shape + shape_count);
11884 let batch_rims = &rims[rim_start..rim_end];
11885 let rim_buffers = match (&self.rim_mesh_vertex_buffer, &self.rim_mesh_index_buffer) {
11886 (Some(vertex_buffer), Some(index_buffer)) if !batch_rims.is_empty() => {
11887 Some((vertex_buffer, index_buffer))
11888 }
11889 _ => None,
11890 };
11891 let Some((rim_vertex_buffer, rim_index_buffer)) = rim_buffers else {
11892 render_pass.draw_indexed(0..6, 0, first_shape..first_shape + shape_count);
11893 return;
11894 };
11895 let mut draw_calls = 0u32;
11903 let mut cursor = first_shape;
11904 for rim in batch_rims {
11905 if cursor < rim.shape_index {
11906 render_pass.draw_indexed(0..6, 0, cursor..rim.shape_index);
11907 draw_calls += 1;
11908 }
11909 render_pass.set_pipeline(self.mesh_pipeline());
11910 render_pass.set_vertex_buffer(0, rim_vertex_buffer.slice(..));
11911 render_pass.set_index_buffer(rim_index_buffer.slice(..), wgpu::IndexFormat::Uint32);
11912 render_pass.draw_indexed(
11913 rim.first_index..rim.first_index + rim.index_count,
11914 0,
11915 0..1,
11916 );
11917 draw_calls += 1;
11918 set_instanced_pipeline(render_pass);
11919 render_pass
11920 .set_index_buffer(instanced.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
11921 cursor = rim.shape_index + 1;
11922 }
11923 if cursor < first_shape + shape_count {
11924 render_pass.draw_indexed(0..6, 0, cursor..first_shape + shape_count);
11925 draw_calls += 1;
11926 }
11927 self.frame_stats
11929 .add_draw_calls(draw_calls.saturating_sub(1));
11930 return;
11931 }
11932 if blend_mode == BlendMode::SrcOver && !batch.has_gradient {
11933 render_pass.set_pipeline(self.shape_pipeline_solid());
11934 } else {
11935 render_pass.set_pipeline(self.shape_pipeline(blend_mode));
11936 }
11937 render_pass.set_bind_group(0, uniform_bind_group, &[]);
11938 render_pass.set_bind_group(1, &shape_buffers.bind_group, &[0]);
11941 render_pass.draw(
11944 batch.vertex_start..batch.vertex_start + batch.vertex_count,
11945 0..1,
11946 );
11947 }
11948
11949 #[allow(clippy::too_many_arguments)]
11952 fn encode_shapes_pass<'a, I, C: FrameCommandRecorder>(
11953 &mut self,
11954 frame_encoder: &mut C,
11955 target_view: &wgpu::TextureView,
11956 layer_shapes: I,
11957 brushes: &[Brush],
11958 blend_mode: BlendMode,
11959 width: u32,
11960 height: u32,
11961 root_scale: f32,
11962 load_op: wgpu::LoadOp<wgpu::Color>,
11963 viewport_offset: [f32; 2],
11964 ) where
11965 I: Iterator<Item = &'a DrawShape>,
11966 {
11967 let mut staged_uploads = self.take_staged_uploads();
11968 let viewport = ViewportUniformParams {
11969 width,
11970 height,
11971 offset: viewport_offset,
11972 };
11973 let Some(batch) = self.prepare_shapes_batch(
11974 layer_shapes
11975 .filter(|shape| shape_draw_is_visible_in_viewport(shape, viewport, root_scale)),
11976 brushes,
11977 root_scale,
11978 viewport,
11979 &mut staged_uploads,
11980 ) else {
11981 self.restore_staged_uploads(staged_uploads);
11982 return;
11983 };
11984 let upload_offset =
11985 frame_encoder.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
11986 self.flush_staged_uploads_at(frame_encoder.encoder(), &staged_uploads, upload_offset);
11987 self.restore_staged_uploads(staged_uploads);
11988 let mut render_pass =
11989 frame_encoder
11990 .encoder()
11991 .begin_render_pass(&wgpu::RenderPassDescriptor {
11992 label: Some("Shape Pass"),
11993 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
11994 view: target_view,
11995 resolve_target: None,
11996 depth_slice: None,
11997 ops: wgpu::Operations {
11998 load: load_op,
11999 store: wgpu::StoreOp::Store,
12000 },
12001 })],
12002 depth_stencil_attachment: None,
12003 timestamp_writes: None,
12004 occlusion_query_set: None,
12005 multiview_mask: None,
12006 });
12007 self.draw_prepared_shapes(&mut render_pass, blend_mode, batch, width, height, &[]);
12008 }
12009
12010 fn draw_prepared_images(
12011 &mut self,
12012 render_pass: &mut wgpu::RenderPass<'_>,
12013 batch: &PreparedImageBatch,
12014 blend_mode: BlendMode,
12015 ) -> Result<(), String> {
12016 if batch.cmds.is_empty() {
12017 return Ok(());
12018 }
12019 self.frame_stats.bump_images();
12020 self.frame_stats.add_draw_calls(batch.cmds.len() as u32);
12021 render_pass.set_pipeline(self.image_pipeline(blend_mode));
12022 #[cfg(not(target_arch = "wasm32"))]
12023 let (uniform_bind_group, vertex_buffer, index_buffer) = (
12024 &self.uniform_bind_group,
12025 &self.image_vertex_buffer,
12026 &self.image_index_buffer,
12027 );
12028 #[cfg(target_arch = "wasm32")]
12029 let (uniform_bind_group, vertex_buffer, index_buffer) = (
12030 &self.wasm_uniform_batches[batch.uniform_slot].bind_group,
12031 &self.wasm_image_batches[batch.image_slot].vertex_buffer,
12032 &self.wasm_image_batches[batch.image_slot].index_buffer,
12033 );
12034 render_pass.set_bind_group(0, uniform_bind_group, &[]);
12035 render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
12036 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
12037
12038 for cmd in &batch.cmds {
12039 let (sx, sy, sw, sh) = cmd.scissor;
12040 render_pass.set_scissor_rect(sx, sy, sw, sh);
12041
12042 let cached = self
12043 .image_texture_cache
12044 .get(&cmd.image_id)
12045 .ok_or_else(|| "image texture missing from cache".to_string())?;
12046 render_pass.set_bind_group(1, cached.bind_group(cmd.sampling), &[]);
12047 render_pass.draw_indexed(cmd.index_start..(cmd.index_start + 6), 0, 0..1);
12048 }
12049 Ok(())
12050 }
12051
12052 fn draw_prepared_glyphs(
12053 &mut self,
12054 render_pass: &mut wgpu::RenderPass<'_>,
12055 batch: &PreparedGlyphBatch,
12056 ) -> Result<(), String> {
12057 if batch.cmds.is_empty() {
12058 return Ok(());
12059 }
12060 #[cfg(not(target_arch = "wasm32"))]
12061 {
12062 self.draw_native_prepared_glyph_cmd_range(
12063 render_pass,
12064 &batch.cmds,
12065 0..batch.cmds.len(),
12066 )?;
12067 }
12068 #[cfg(target_arch = "wasm32")]
12069 {
12070 self.frame_stats.bump_text();
12071 self.frame_stats.add_draw_calls(batch.cmds.len() as u32);
12072 render_pass.set_pipeline(self.glyph_atlas_pipeline());
12073 let (uniform_bind_group, vertex_buffer, index_buffer) = (
12074 &self.wasm_uniform_batches[batch.uniform_slot].bind_group,
12075 &self.wasm_image_batches[batch.image_slot].vertex_buffer,
12076 &self.wasm_image_batches[batch.image_slot].index_buffer,
12077 );
12078 render_pass.set_bind_group(0, uniform_bind_group, &[]);
12079 render_pass.set_bind_group(1, &self.text_glyph_atlas.bind_group, &[]);
12080 render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
12081 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
12082
12083 for cmd in &batch.cmds {
12084 let (sx, sy, sw, sh) = cmd.scissor;
12085 render_pass.set_scissor_rect(sx, sy, sw, sh);
12086 let GlyphDrawSource::Shared {
12087 index_start,
12088 index_count,
12089 } = cmd.source;
12090 render_pass.draw_indexed(index_start..(index_start + index_count), 0, 0..1);
12091 }
12092 }
12093 Ok(())
12094 }
12095
12096 #[cfg(not(target_arch = "wasm32"))]
12097 fn draw_native_prepared_image_cmd_range(
12098 &mut self,
12099 render_pass: &mut wgpu::RenderPass<'_>,
12100 cmds: &[ImageDrawCmd],
12101 cmd_range: Range<usize>,
12102 blend_mode: BlendMode,
12103 ) -> Result<(), String> {
12104 let Some(cmds) = cmds.get(cmd_range) else {
12105 return Err("image command range is outside the prepared command buffer".to_string());
12106 };
12107 if cmds.is_empty() {
12108 return Ok(());
12109 }
12110
12111 self.frame_stats.bump_images();
12112 self.frame_stats.add_draw_calls(cmds.len() as u32);
12113 render_pass.set_pipeline(self.image_pipeline(blend_mode));
12114 render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
12115 render_pass.set_index_buffer(self.image_index_buffer.slice(..), wgpu::IndexFormat::Uint32);
12116 render_pass.set_vertex_buffer(0, self.image_vertex_buffer.slice(..));
12117
12118 for cmd in cmds {
12119 let (sx, sy, sw, sh) = cmd.scissor;
12120 render_pass.set_scissor_rect(sx, sy, sw, sh);
12121
12122 let cached = self
12123 .image_texture_cache
12124 .get(&cmd.image_id)
12125 .ok_or_else(|| "image texture missing from cache".to_string())?;
12126 render_pass.set_bind_group(1, cached.bind_group(cmd.sampling), &[]);
12127 render_pass.draw_indexed(cmd.index_start..(cmd.index_start + 6), 0, 0..1);
12128 }
12129 Ok(())
12130 }
12131
12132 #[cfg(not(target_arch = "wasm32"))]
12133 fn draw_native_prepared_glyph_cmd_range(
12134 &mut self,
12135 render_pass: &mut wgpu::RenderPass<'_>,
12136 cmds: &[GlyphDrawCmd],
12137 cmd_range: Range<usize>,
12138 ) -> Result<(), String> {
12139 let Some(cmds) = cmds.get(cmd_range) else {
12140 return Err("glyph command range is outside the prepared command buffer".to_string());
12141 };
12142 if cmds.is_empty() {
12143 return Ok(());
12144 }
12145
12146 self.frame_stats.bump_text();
12147 self.frame_stats.add_draw_calls(cmds.len() as u32);
12148
12149 let mut shared_buffers_bound = false;
12150 let mut retained_pipeline_bound = false;
12151 for cmd in cmds {
12152 let (sx, sy, sw, sh) = cmd.scissor;
12153 render_pass.set_scissor_rect(sx, sy, sw, sh);
12154 match cmd.source {
12155 GlyphDrawSource::Shared {
12156 index_start,
12157 index_count,
12158 } => {
12159 if retained_pipeline_bound || !shared_buffers_bound {
12160 render_pass.set_pipeline(self.glyph_atlas_pipeline());
12161 render_pass.set_bind_group(1, &self.text_glyph_atlas.bind_group, &[]);
12162 retained_pipeline_bound = false;
12163 }
12164 if !shared_buffers_bound {
12165 render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
12166 render_pass.set_index_buffer(
12167 self.image_index_buffer.slice(..),
12168 wgpu::IndexFormat::Uint32,
12169 );
12170 render_pass.set_vertex_buffer(0, self.image_vertex_buffer.slice(..));
12171 shared_buffers_bound = true;
12172 }
12173 render_pass.draw_indexed(index_start..(index_start + index_count), 0, 0..1);
12174 }
12175 GlyphDrawSource::Retained {
12176 cache_key,
12177 uniform_slot,
12178 } => {
12179 shared_buffers_bound = false;
12180 if !retained_pipeline_bound {
12181 render_pass.set_pipeline(self.retained_glyph_atlas_pipeline());
12182 render_pass.set_bind_group(1, &self.text_glyph_atlas.bind_group, &[]);
12183 retained_pipeline_bound = true;
12184 }
12185 let cached = self
12186 .text_glyph_gpu_run_cache
12187 .peek(&cache_key)
12188 .ok_or_else(|| "retained glyph buffer missing from cache".to_string())?;
12189 let dynamic_offset =
12190 self.retained_glyph_uniform_dynamic_offset(uniform_slot)?;
12191 render_pass.set_bind_group(
12192 0,
12193 &self.retained_glyph_uniform_bind_group,
12194 &[dynamic_offset],
12195 );
12196 render_pass
12197 .set_index_buffer(cached.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
12198 render_pass.set_vertex_buffer(0, cached.vertex_buffer.slice(..));
12199 render_pass.draw_indexed(0..cached.index_count, 0, 0..1);
12200 }
12201 }
12202 }
12203 Ok(())
12204 }
12205
12206 fn append_image_draw_cmd(
12207 &mut self,
12208 image_draw: &ImageDraw,
12209 viewport: ViewportUniformParams,
12210 root_scale: f32,
12211 image_vertices: &mut Vec<Vertex>,
12212 image_indices: &mut Vec<u32>,
12213 image_cmds: &mut Vec<ImageDrawCmd>,
12214 ) -> Result<(), String> {
12215 let snap_delta = image_draw
12216 .snap_anchor
12217 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
12218 .unwrap_or_default();
12219 let rect = image_draw.rect.translate(snap_delta.x, snap_delta.y);
12220 if rect.width <= 0.0 || rect.height <= 0.0 || image_draw.alpha <= 0.0 {
12221 return Ok(());
12222 }
12223
12224 let (tint, cpu_filter) = tint_for_image(image_draw.color_filter, image_draw.alpha);
12225 if tint[3] <= 0.0 {
12226 return Ok(());
12227 }
12228
12229 let prepared_image = if let Some(filter) = cpu_filter {
12230 apply_filter_to_bitmap(&image_draw.image, filter)?
12231 } else {
12232 image_draw.image.clone()
12233 };
12234 self.ensure_image_cached(&prepared_image)?;
12235
12236 let mut adjusted_image = ImageDraw {
12237 rect,
12238 local_rect: image_draw.local_rect.translate(snap_delta.x, snap_delta.y),
12239 quad: translate_quad(image_draw.quad, snap_delta),
12240 snap_anchor: image_draw.snap_anchor,
12241 image: image_draw.image.clone(),
12242 alpha: image_draw.alpha,
12243 color_filter: image_draw.color_filter,
12244 sampling: image_draw.sampling,
12245 z_index: image_draw.z_index,
12246 clip: image_draw.clip,
12247 blend_mode: image_draw.blend_mode,
12248 src_rect: image_draw.src_rect,
12249 motion_context_animated: image_draw.motion_context_animated,
12250 };
12251 snap_nearest_image_to_device_pixels(&mut adjusted_image, root_scale);
12252 let Some(scissor) =
12253 scissor_rect_for_image(&adjusted_image, root_scale, viewport.width, viewport.height)
12254 else {
12255 return Ok(());
12256 };
12257
12258 let Some(uv_rect) = image_uv_rect(&image_draw.image, image_draw.src_rect) else {
12259 return Ok(());
12260 };
12261 let device_quad =
12262 nearest_image_device_quad(&adjusted_image, root_scale).unwrap_or_else(|| {
12263 if adjusted_image.snap_anchor.is_some() {
12264 canonicalized_scaled_quad(adjusted_image.quad, root_scale)
12265 } else {
12266 scaled_quad(adjusted_image.quad, root_scale)
12267 }
12268 });
12269 #[cfg(not(target_arch = "wasm32"))]
12270 {
12271 if fill_area_diag_enabled() {
12272 self.fill_area_diag.add_image_quad(&device_quad);
12273 }
12274 }
12275
12276 let base_vertex = image_vertices.len() as u32;
12277 let index_start = image_indices.len() as u32;
12278 image_indices.extend_from_slice(&[
12279 base_vertex,
12280 base_vertex + 1,
12281 base_vertex + 2,
12282 base_vertex + 2,
12283 base_vertex + 1,
12284 base_vertex + 3,
12285 ]);
12286 image_vertices.extend_from_slice(&[
12287 Vertex {
12288 position: device_quad[0],
12289 color: tint,
12290 uv: [uv_rect.min[0], uv_rect.min[1]],
12291 uv_bounds: uv_rect.sample_bounds,
12292 },
12293 Vertex {
12294 position: device_quad[1],
12295 color: tint,
12296 uv: [uv_rect.max[0], uv_rect.min[1]],
12297 uv_bounds: uv_rect.sample_bounds,
12298 },
12299 Vertex {
12300 position: device_quad[2],
12301 color: tint,
12302 uv: [uv_rect.min[0], uv_rect.max[1]],
12303 uv_bounds: uv_rect.sample_bounds,
12304 },
12305 Vertex {
12306 position: device_quad[3],
12307 color: tint,
12308 uv: [uv_rect.max[0], uv_rect.max[1]],
12309 uv_bounds: uv_rect.sample_bounds,
12310 },
12311 ]);
12312
12313 image_cmds.push(ImageDrawCmd {
12314 index_start,
12315 scissor,
12316 image_id: prepared_image.id(),
12317 sampling: image_draw.sampling,
12318 });
12319 Ok(())
12320 }
12321
12322 #[cfg(not(target_arch = "wasm32"))]
12323 fn stage_native_image_buffers(
12324 &mut self,
12325 staged_uploads: &mut StagedBufferUploads,
12326 viewport: ViewportUniformParams,
12327 image_vertices: &[Vertex],
12328 image_indices: &[u32],
12329 ) {
12330 if image_indices.is_empty() {
12331 return;
12332 }
12333
12334 self.stage_viewport_uniforms(staged_uploads, viewport);
12335 let needed_bytes = std::mem::size_of_val(image_vertices) as u64;
12340 if needed_bytes > self.image_vertex_buffer.size() {
12341 self.image_vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
12342 label: Some("Image Vertex Buffer"),
12343 size: needed_bytes.next_power_of_two(),
12344 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
12345 mapped_at_creation: false,
12346 });
12347 }
12348 let needed_index_bytes = std::mem::size_of_val(image_indices) as u64;
12349 if needed_index_bytes > self.image_index_buffer.size() {
12350 self.image_index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
12351 label: Some("Image Index Buffer"),
12352 size: needed_index_bytes.next_power_of_two(),
12353 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
12354 mapped_at_creation: false,
12355 });
12356 }
12357
12358 staged_uploads.stage(
12359 UploadTarget::ImageVertex,
12360 bytemuck::cast_slice(image_vertices),
12361 );
12362 staged_uploads.stage(
12363 UploadTarget::ImageIndex,
12364 bytemuck::cast_slice(image_indices),
12365 );
12366 }
12367
12368 fn prepare_image_draw_cmds<'a, I>(
12371 &mut self,
12372 layer_images: I,
12373 viewport: ViewportUniformParams,
12374 root_scale: f32,
12375 staged_uploads: &mut StagedBufferUploads,
12376 ) -> Result<PreparedImageBatch, String>
12377 where
12378 I: Iterator<Item = &'a ImageDraw>,
12379 {
12380 #[cfg(target_arch = "wasm32")]
12381 let _ = staged_uploads;
12382
12383 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
12384 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
12385 let mut image_cmds = std::mem::take(&mut self.scratch_image_cmds);
12386 image_vertices.clear();
12387 image_indices.clear();
12388 image_cmds.clear();
12389
12390 for image_draw in layer_images {
12391 self.append_image_draw_cmd(
12392 image_draw,
12393 viewport,
12394 root_scale,
12395 &mut image_vertices,
12396 &mut image_indices,
12397 &mut image_cmds,
12398 )?;
12399 }
12400
12401 #[cfg(not(target_arch = "wasm32"))]
12402 if !image_cmds.is_empty() {
12403 self.stage_native_image_buffers(
12404 staged_uploads,
12405 viewport,
12406 &image_vertices,
12407 &image_indices,
12408 );
12409 }
12410
12411 #[cfg(target_arch = "wasm32")]
12412 let image_slot = if image_cmds.is_empty() {
12413 0
12414 } else {
12415 let slot = self.claim_wasm_image_batch();
12416 {
12417 let buffers = &mut self.wasm_image_batches[slot];
12418 buffers.ensure_capacity(&self.device, image_vertices.len(), image_indices.len());
12419 }
12420 let buffers = &self.wasm_image_batches[slot];
12421 self.write_wasm_buffer(
12422 &buffers.vertex_buffer,
12423 bytemuck::cast_slice(&image_vertices),
12424 );
12425 self.write_wasm_buffer(&buffers.index_buffer, bytemuck::cast_slice(&image_indices));
12426 slot
12427 };
12428
12429 #[cfg(target_arch = "wasm32")]
12430 let uniform_slot = if image_cmds.is_empty() {
12431 0
12432 } else {
12433 self.prepare_wasm_viewport_uniforms(viewport)
12434 };
12435
12436 self.scratch_image_vertices = image_vertices;
12437 self.scratch_image_indices = image_indices;
12438 Ok(PreparedImageBatch {
12439 cmds: image_cmds,
12440 #[cfg(target_arch = "wasm32")]
12441 image_slot,
12442 #[cfg(target_arch = "wasm32")]
12443 uniform_slot,
12444 })
12445 }
12446
12447 fn glyph_atlas_entry_for(
12448 &mut self,
12449 glyph: &SoftwareGlyphAtlasGlyph,
12450 ) -> Result<GlyphAtlasEntry, String> {
12451 if let Some(entry) = self.text_glyph_atlas.upload_glyph(
12452 glyph.key,
12453 glyph,
12454 &self.queue,
12455 &mut self.frame_graph_executor,
12456 &mut self.frame_stats,
12457 ) {
12458 return Ok(entry);
12459 }
12460
12461 self.text_glyph_atlas.reset(
12462 &self.device,
12463 &self.image_bind_group_layout,
12464 &self.image_nearest_sampler,
12465 );
12466 Err("text glyph atlas filled and was reset".to_string())
12467 }
12468
12469 fn glyph_atlas_entry_for_cached(
12470 &mut self,
12471 glyph: &SoftwareGlyphAtlasPlacement,
12472 ) -> Option<GlyphAtlasEntry> {
12473 let entry = self.text_glyph_atlas.entry(&glyph.key)?;
12474 self.frame_stats.record_text_glyph_atlas_hit();
12475 Some(entry)
12476 }
12477
12478 fn glyph_atlas_entry_for_placement(
12479 &mut self,
12480 glyph: &SoftwareGlyphAtlasPlacement,
12481 ) -> Result<GlyphAtlasEntry, String> {
12482 if let Some(entry) = self.glyph_atlas_entry_for_cached(glyph) {
12483 return Ok(entry);
12484 }
12485
12486 let Some(upload_glyph) = self.text_glyph_mask_cache.atlas_glyph_for_placement(glyph) else {
12487 return Err("text glyph placement has no retained raster mask".to_string());
12488 };
12489 self.glyph_atlas_entry_for(&upload_glyph)
12490 }
12491
12492 fn prepare_text_glyph_quads(
12493 &mut self,
12494 run_key: TextGlyphRunCacheKey,
12495 atlas_generation: u64,
12496 cached_glyph_run: Option<&[SoftwareGlyphAtlasPlacement]>,
12497 collected_run: &[SoftwareGlyphAtlasRunGlyph],
12498 generated_quads: &mut Vec<CachedTextGlyphQuad>,
12499 ) -> Result<Rc<[CachedTextGlyphQuad]>, String> {
12500 generated_quads.clear();
12501 if let Some(glyph_run) = cached_glyph_run {
12502 for glyph in glyph_run {
12503 if glyph.width == 0 || glyph.height == 0 || glyph.color.3 <= 0.0 {
12504 continue;
12505 }
12506 let entry = self.glyph_atlas_entry_for_placement(glyph)?;
12507 generated_quads.push(cached_text_glyph_quad(
12512 glyph,
12513 entry,
12514 self.text_glyph_atlas.size(),
12515 ));
12516 }
12517 } else {
12518 for run_glyph in collected_run {
12519 let placement = run_glyph.placement();
12520 if placement.width == 0 || placement.height == 0 || placement.color.3 <= 0.0 {
12521 continue;
12522 }
12523 let entry = match run_glyph {
12524 SoftwareGlyphAtlasRunGlyph::Cached(placement) => {
12525 self.glyph_atlas_entry_for_placement(placement)?
12526 }
12527 SoftwareGlyphAtlasRunGlyph::New(glyph) => self.glyph_atlas_entry_for(glyph)?,
12528 };
12529 generated_quads.push(cached_text_glyph_quad(
12530 &placement,
12531 entry,
12532 self.text_glyph_atlas.size(),
12533 ));
12534 }
12535 }
12536
12537 let quads: Rc<[CachedTextGlyphQuad]> = Rc::from(generated_quads.clone().into_boxed_slice());
12538 if let Some(cached) = self.text_glyph_run_cache.get_mut(&run_key) {
12539 cached.quads = Some(Rc::clone(&quads));
12540 cached.atlas_generation = atlas_generation;
12541 }
12542 Ok(quads)
12543 }
12544
12545 #[allow(clippy::too_many_arguments)]
12546 fn append_text_glyph_quad_run(
12547 &mut self,
12548 source_raster_rect: Rect,
12549 quads: &[CachedTextGlyphQuad],
12550 clip: Option<Rect>,
12551 viewport: ViewportUniformParams,
12552 root_scale: f32,
12553 image_vertices: &mut Vec<Vertex>,
12554 image_indices: &mut Vec<u32>,
12555 record_cached_hits: bool,
12556 ) -> usize {
12557 let mut appended = 0usize;
12558 for quad in quads {
12559 if !cached_text_glyph_quad_is_visible_in_viewport(
12560 source_raster_rect,
12561 quad,
12562 clip,
12563 viewport,
12564 root_scale,
12565 ) {
12566 continue;
12567 }
12568 if append_cached_text_glyph_quad(
12569 source_raster_rect,
12570 quad,
12571 image_vertices,
12572 image_indices,
12573 ) {
12574 if record_cached_hits {
12575 self.frame_stats.record_text_glyph_atlas_hit();
12576 }
12577 #[cfg(not(target_arch = "wasm32"))]
12578 {
12579 if fill_area_diag_enabled() {
12580 self.fill_area_diag.add_glyph_quad(quad);
12581 }
12582 }
12583 appended = appended.saturating_add(1);
12584 }
12585 }
12586 appended
12587 }
12588
12589 #[cfg(not(target_arch = "wasm32"))]
12590 fn retained_glyph_viewport(
12591 viewport: ViewportUniformParams,
12592 source_raster_rect: Rect,
12593 ) -> ViewportUniformParams {
12594 ViewportUniformParams {
12595 width: viewport.width,
12596 height: viewport.height,
12597 offset: [
12598 viewport.offset[0] - source_raster_rect.x,
12599 viewport.offset[1] - source_raster_rect.y,
12600 ],
12601 }
12602 }
12603
12604 #[cfg(not(target_arch = "wasm32"))]
12605 fn retained_text_glyph_run_ready(&mut self, cache_key: TextGlyphRunCacheKey) -> bool {
12606 let atlas_generation = self.text_glyph_atlas.generation();
12607 self.text_glyph_gpu_run_cache
12608 .peek(&cache_key)
12609 .is_some_and(|cached| cached.atlas_generation == atlas_generation)
12610 }
12611
12612 #[cfg(not(target_arch = "wasm32"))]
12613 #[allow(clippy::too_many_arguments)]
12614 fn emit_retained_text_glyph_run_if_ready(
12615 &mut self,
12616 cache_key: TextGlyphRunCacheKey,
12617 quads: &[CachedTextGlyphQuad],
12618 clip: Option<Rect>,
12619 viewport: ViewportUniformParams,
12620 source_raster_rect: Rect,
12621 scissor: (u32, u32, u32, u32),
12622 staged_uploads: &mut StagedBufferUploads,
12623 glyph_cmds: &mut Vec<GlyphDrawCmd>,
12624 ) -> bool {
12625 if !should_use_retained_text_glyph_run(quads.len(), clip) {
12626 return false;
12627 }
12628 if !self.retained_text_glyph_run_ready(cache_key)
12629 && !self.ensure_retained_text_glyph_run(cache_key, quads)
12630 {
12631 return false;
12632 }
12633
12634 let uniform_slot = self.stage_retained_glyph_viewport_uniforms(
12635 staged_uploads,
12636 Self::retained_glyph_viewport(viewport, source_raster_rect),
12637 );
12638 if fill_area_diag_enabled() {
12639 for quad in quads {
12642 self.fill_area_diag.add_glyph_quad(quad);
12643 }
12644 }
12645 glyph_cmds.push(GlyphDrawCmd::retained(cache_key, uniform_slot, scissor));
12646 true
12647 }
12648
12649 #[cfg(not(target_arch = "wasm32"))]
12650 fn ensure_retained_text_glyph_run(
12651 &mut self,
12652 cache_key: TextGlyphRunCacheKey,
12653 quads: &[CachedTextGlyphQuad],
12654 ) -> bool {
12655 let atlas_generation = self.text_glyph_atlas.generation();
12656 if self
12657 .text_glyph_gpu_run_cache
12658 .peek(&cache_key)
12659 .is_some_and(|cached| cached.atlas_generation == atlas_generation)
12660 {
12661 return true;
12662 }
12663
12664 let mut vertices = Vec::with_capacity(quads.len().saturating_mul(4));
12665 let mut indices = Vec::with_capacity(quads.len().saturating_mul(6));
12666 let origin = Rect {
12667 x: 0.0,
12668 y: 0.0,
12669 width: 0.0,
12670 height: 0.0,
12671 };
12672 for quad in quads {
12673 append_cached_text_glyph_quad(origin, quad, &mut vertices, &mut indices);
12674 }
12675 if indices.is_empty() {
12676 return false;
12677 }
12678
12679 let vertex_bytes = bytemuck::cast_slice(&vertices);
12680 let index_bytes = bytemuck::cast_slice(&indices);
12681 let vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
12682 label: Some("Retained Text Glyph Vertex Buffer"),
12683 size: vertex_bytes.len() as u64,
12684 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
12685 mapped_at_creation: false,
12686 });
12687 let index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
12688 label: Some("Retained Text Glyph Index Buffer"),
12689 size: index_bytes.len() as u64,
12690 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
12691 mapped_at_creation: false,
12692 });
12693 let vertex_upload =
12694 self.frame_graph_executor
12695 .upload_buffer(&self.queue, &vertex_buffer, 0, vertex_bytes);
12696 self.frame_stats.record_command_stats(vertex_upload);
12697 let index_upload =
12698 self.frame_graph_executor
12699 .upload_buffer(&self.queue, &index_buffer, 0, index_bytes);
12700 self.frame_stats.record_command_stats(index_upload);
12701
12702 self.text_glyph_gpu_run_cache.put(
12703 cache_key,
12704 CachedGpuTextGlyphRun {
12705 vertex_buffer,
12706 index_buffer,
12707 index_count: indices.len() as u32,
12708 atlas_generation,
12709 },
12710 );
12711 true
12712 }
12713
12714 #[allow(clippy::too_many_arguments)]
12715 fn append_text_glyph_draws<'a, I>(
12716 &mut self,
12717 layer_texts: I,
12718 viewport: ViewportUniformParams,
12719 root_scale: f32,
12720 allow_offscreen_prewarm: bool,
12721 staged_uploads: &mut StagedBufferUploads,
12722 image_vertices: &mut Vec<Vertex>,
12723 image_indices: &mut Vec<u32>,
12724 glyph_cmds: &mut Vec<GlyphDrawCmd>,
12725 ) -> Result<bool, String>
12726 where
12727 I: IntoIterator<Item = &'a TextDraw>,
12728 {
12729 let append_start = Instant::now();
12730 let initial_vertex_len = image_vertices.len();
12731 let initial_index_len = image_indices.len();
12732 let initial_cmd_len = glyph_cmds.len();
12733 let initial_staged_bytes_len = staged_uploads.bytes.len();
12734 let initial_staged_copies_len = staged_uploads.copies.len();
12735 let mut collected_run = std::mem::take(&mut self.scratch_text_glyph_run);
12736 let mut collected_placements = std::mem::take(&mut self.scratch_text_glyph_placements);
12737 let mut generated_quads = std::mem::take(&mut self.scratch_text_glyph_quads);
12738 generated_quads.clear();
12739 let mut visited = 0usize;
12740 let mut emitted_glyphs = 0usize;
12741 let mut prewarmed_glyphs = 0usize;
12742 let mut run_hits = 0usize;
12743 let mut run_misses = 0usize;
12744
12745 for text_draw in layer_texts {
12746 visited = visited.saturating_add(1);
12747 let Some((logical_rect, raster_rect, clip, text_scale, static_text_motion)) =
12748 self.text_raster_geometry(text_draw, root_scale)
12749 else {
12750 continue;
12751 };
12752 if !static_text_motion {
12753 image_vertices.truncate(initial_vertex_len);
12754 image_indices.truncate(initial_index_len);
12755 glyph_cmds.truncate(initial_cmd_len);
12756 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
12757 self.scratch_text_glyph_run = collected_run;
12758 self.scratch_text_glyph_placements = collected_placements;
12759 self.scratch_text_glyph_quads = generated_quads;
12760 return Ok(false);
12761 }
12762 let is_visible =
12763 text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale);
12764 let draw_action = text_glyph_draw_action(
12765 is_visible,
12766 text_draw_should_prewarm_in_viewport(logical_rect, clip, viewport, root_scale),
12767 allow_offscreen_prewarm,
12768 );
12769 if draw_action == TextGlyphDrawAction::Skip {
12770 continue;
12771 }
12772
12773 let raster_source = text_glyph_raster_source(text_draw, raster_rect);
12774 let source_draw = raster_source.draw.as_ref();
12775 let source_raster_rect = raster_source.raster_rect;
12776
12777 let run_key = Self::text_glyph_run_cache_key(
12778 source_draw,
12779 source_raster_rect,
12780 text_scale,
12781 static_text_motion,
12782 );
12783 let atlas_generation = self.text_glyph_atlas.generation();
12784 let mut cached_quad_run = None;
12785 let mut miss_collect_ms = None;
12786 let mut miss_cached_glyphs = 0usize;
12787 let mut miss_new_glyphs = 0usize;
12788 let cached_glyph_run = if let Some(cached) = self.text_glyph_run_cache.get(&run_key) {
12789 run_hits = run_hits.saturating_add(1);
12790 if cached.atlas_generation == atlas_generation {
12791 cached_quad_run = cached.quads.as_ref().map(Rc::clone);
12792 }
12793 Some(Rc::clone(&cached.glyphs))
12794 } else {
12795 run_misses = run_misses.saturating_add(1);
12796 collected_run.clear();
12797 let collect_start = Instant::now();
12798 let collect_result = collect_solid_text_atlas_run(
12799 source_draw.text.as_ref(),
12800 source_raster_rect,
12801 &source_draw.text_style,
12802 source_draw.color,
12803 source_draw.font_size,
12804 text_scale,
12805 &self.text_fonts,
12806 &mut self.text_glyph_mask_cache,
12807 &mut collected_run,
12808 );
12809 miss_collect_ms = Some(instant_ms(collect_start, Instant::now()));
12810 if collect_result.is_none() {
12811 if text_atlas_fallback_diag_enabled() {
12812 let preview: String = source_draw.text.text.chars().take(96).collect();
12813 log::warn!(
12814 "[text-atlas-fallback] node={:?} visible={} prewarm={} spans={} links={} text_len={} preview={:?} span_style={:?} paragraph_style={:?}",
12815 source_draw.node_id,
12816 is_visible,
12817 draw_action == TextGlyphDrawAction::PrewarmOffscreen,
12818 source_draw.text.span_styles.len(),
12819 source_draw.text.links.len(),
12820 source_draw.text.text.len(),
12821 preview,
12822 source_draw.text_style.span_style,
12823 source_draw.text_style.paragraph_style,
12824 );
12825 }
12826 if draw_action == TextGlyphDrawAction::PrewarmOffscreen {
12827 continue;
12828 }
12829 image_vertices.truncate(initial_vertex_len);
12830 image_indices.truncate(initial_index_len);
12831 glyph_cmds.truncate(initial_cmd_len);
12832 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
12833 self.scratch_text_glyph_run = collected_run;
12834 self.scratch_text_glyph_placements = collected_placements;
12835 self.scratch_text_glyph_quads = generated_quads;
12836 return Ok(false);
12837 }
12838 if text_glyph_run_diag_enabled() {
12839 miss_cached_glyphs = collected_run
12840 .iter()
12841 .filter(|glyph| matches!(glyph, SoftwareGlyphAtlasRunGlyph::Cached(_)))
12842 .count();
12843 miss_new_glyphs = collected_run.len().saturating_sub(miss_cached_glyphs);
12844 }
12845 collected_placements.clear();
12846 collected_placements.extend(
12847 collected_run
12848 .iter()
12849 .map(SoftwareGlyphAtlasRunGlyph::placement),
12850 );
12851 let glyphs: Rc<[SoftwareGlyphAtlasPlacement]> =
12852 Rc::from(collected_placements.clone().into_boxed_slice());
12853 self.text_glyph_run_cache.put(
12854 run_key,
12855 CachedTextGlyphRun {
12856 glyphs,
12857 quads: None,
12858 atlas_generation: 0,
12859 },
12860 );
12861 None
12862 };
12863
12864 if draw_action == TextGlyphDrawAction::PrewarmOffscreen {
12865 let prewarm_quads = if let Some(quad_run) = cached_quad_run {
12866 quad_run
12867 } else {
12868 let prepare_start = Instant::now();
12869 match self.prepare_text_glyph_quads(
12870 run_key,
12871 atlas_generation,
12872 cached_glyph_run.as_deref(),
12873 &collected_run,
12874 &mut generated_quads,
12875 ) {
12876 Ok(quads) => {
12877 if let Some(collect_ms) = miss_collect_ms {
12878 if text_glyph_run_diag_enabled() {
12879 log::warn!(
12880 "[text-glyph-run-diag] visible=false glyphs={} cached={} new={} collect_ms={:.2} prepare_ms={:.2}",
12881 quads.len(),
12882 miss_cached_glyphs,
12883 miss_new_glyphs,
12884 collect_ms,
12885 instant_ms(prepare_start, Instant::now()),
12886 );
12887 }
12888 }
12889 quads
12890 }
12891 Err(_) => continue,
12892 }
12893 };
12894 #[cfg(not(target_arch = "wasm32"))]
12895 if should_use_retained_text_glyph_run(prewarm_quads.len(), source_draw.clip) {
12896 self.ensure_retained_text_glyph_run(run_key, prewarm_quads.as_ref());
12897 }
12898 prewarmed_glyphs = prewarmed_glyphs.saturating_add(prewarm_quads.len());
12899 continue;
12900 }
12901
12902 let draw_rect = Rect {
12903 x: source_raster_rect.x / root_scale,
12904 y: source_raster_rect.y / root_scale,
12905 width: source_raster_rect.width / root_scale,
12906 height: source_raster_rect.height / root_scale,
12907 };
12908 let Some(scissor) = scissor_rect_for_layer(
12909 draw_rect,
12910 source_draw.clip,
12911 root_scale,
12912 viewport.width,
12913 viewport.height,
12914 ) else {
12915 continue;
12916 };
12917
12918 #[cfg(not(target_arch = "wasm32"))]
12919 if let Some(quad_run) = cached_quad_run.as_ref() {
12920 if should_use_retained_text_glyph_run(quad_run.len(), source_draw.clip)
12921 && self.emit_retained_text_glyph_run_if_ready(
12922 run_key,
12923 quad_run.as_ref(),
12924 source_draw.clip,
12925 viewport,
12926 source_raster_rect,
12927 scissor,
12928 staged_uploads,
12929 glyph_cmds,
12930 )
12931 {
12932 emitted_glyphs = emitted_glyphs.saturating_add(quad_run.len());
12933 continue;
12934 }
12935 }
12936
12937 let index_start = image_indices.len() as u32;
12938 if let Some(quad_run) = cached_quad_run {
12939 emitted_glyphs = emitted_glyphs.saturating_add(self.append_text_glyph_quad_run(
12940 source_raster_rect,
12941 quad_run.as_ref(),
12942 source_draw.clip,
12943 viewport,
12944 root_scale,
12945 image_vertices,
12946 image_indices,
12947 true,
12948 ));
12949 } else {
12950 let prepare_start = Instant::now();
12951 let Ok(quad_run) = self.prepare_text_glyph_quads(
12952 run_key,
12953 atlas_generation,
12954 cached_glyph_run.as_deref(),
12955 &collected_run,
12956 &mut generated_quads,
12957 ) else {
12958 image_vertices.truncate(initial_vertex_len);
12959 image_indices.truncate(initial_index_len);
12960 glyph_cmds.truncate(initial_cmd_len);
12961 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
12962 self.scratch_text_glyph_run = collected_run;
12963 self.scratch_text_glyph_placements = collected_placements;
12964 self.scratch_text_glyph_quads = generated_quads;
12965 return Ok(false);
12966 };
12967 if let Some(collect_ms) = miss_collect_ms {
12968 if text_glyph_run_diag_enabled() {
12969 log::warn!(
12970 "[text-glyph-run-diag] visible=true glyphs={} cached={} new={} collect_ms={:.2} prepare_ms={:.2}",
12971 quad_run.len(),
12972 miss_cached_glyphs,
12973 miss_new_glyphs,
12974 collect_ms,
12975 instant_ms(prepare_start, Instant::now()),
12976 );
12977 }
12978 }
12979 emitted_glyphs = emitted_glyphs.saturating_add(self.append_text_glyph_quad_run(
12980 source_raster_rect,
12981 quad_run.as_ref(),
12982 source_draw.clip,
12983 viewport,
12984 root_scale,
12985 image_vertices,
12986 image_indices,
12987 false,
12988 ));
12989 }
12990 let index_count = image_indices.len() as u32 - index_start;
12991 if index_count > 0 {
12992 glyph_cmds.push(GlyphDrawCmd::shared(index_start, index_count, scissor));
12993 }
12994 }
12995
12996 self.scratch_text_glyph_run = collected_run;
12997 self.scratch_text_glyph_placements = collected_placements;
12998 self.scratch_text_glyph_quads = generated_quads;
12999 let append_end = Instant::now();
13000 if let Some(total_ms) = should_log_wgpu_render_stage(append_start, append_end) {
13001 log::warn!(
13002 "[wgpu-render-stage:text-glyph-atlas] total_ms={total_ms:.2} visited={} cmds={} glyphs={} prewarmed={} run_hits={} run_misses={}",
13003 visited,
13004 glyph_cmds.len().saturating_sub(initial_cmd_len),
13005 emitted_glyphs,
13006 prewarmed_glyphs,
13007 run_hits,
13008 run_misses,
13009 );
13010 }
13011 Ok(true)
13012 }
13013
13014 #[cfg(not(target_arch = "wasm32"))]
13015 fn text_glyph_prewarm_decision(
13016 &self,
13017 text_draw: &TextDraw,
13018 viewport: ViewportUniformParams,
13019 root_scale: f32,
13020 ) -> TextGlyphPrewarmDecision {
13021 let Some((logical_rect, _, clip, _, static_text_motion)) =
13022 self.text_raster_geometry(text_draw, root_scale)
13023 else {
13024 return TextGlyphPrewarmDecision::MissingGeometry;
13025 };
13026 if !static_text_motion {
13027 return TextGlyphPrewarmDecision::DynamicMotion;
13028 }
13029 if text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale) {
13030 return TextGlyphPrewarmDecision::Visible;
13031 }
13032 if text_draw_should_prewarm_in_viewport(logical_rect, clip, viewport, root_scale) {
13033 TextGlyphPrewarmDecision::Candidate
13034 } else {
13035 TextGlyphPrewarmDecision::OutsidePrewarmWindow
13036 }
13037 }
13038
13039 #[cfg(not(target_arch = "wasm32"))]
13040 #[allow(clippy::too_many_arguments)]
13041 fn prewarm_offscreen_text_glyph_draws_in_chunk(
13042 &mut self,
13043 ordered_items: &[(usize, SegmentDrawItem)],
13044 texts: &[TextDraw],
13045 chunk: &SegmentDrawChunkPlan,
13046 viewport: ViewportUniformParams,
13047 root_scale: f32,
13048 staged_uploads: &mut StagedBufferUploads,
13049 image_vertices: &mut Vec<Vertex>,
13050 image_indices: &mut Vec<u32>,
13051 glyph_cmds: &mut Vec<GlyphDrawCmd>,
13052 ) -> Result<(), String> {
13053 let prewarm_start = Instant::now();
13054 let diag_enabled = cranpose_core::env_flag!("CRANPOSE_TEXT_PREWARM_DIAG");
13055 let mut text_items = 0usize;
13056 let mut candidates = 0usize;
13057 let mut missing_geometry = 0usize;
13058 let mut dynamic_motion = 0usize;
13059 let mut visible = 0usize;
13060 let mut outside = 0usize;
13061 let mut already_prepared = 0usize;
13062 let mut admitted_candidates = 0usize;
13063 let mut skipped_unbounded = 0usize;
13064 let mut skipped_budget = 0usize;
13065 let initial_vertex_len = image_vertices.len();
13066 let initial_index_len = image_indices.len();
13067 let initial_cmd_len = glyph_cmds.len();
13068 let initial_staged_bytes_len = staged_uploads.bytes.len();
13069 let initial_staged_copies_len = staged_uploads.copies.len();
13070 'batches: for batch in chunk.iter() {
13071 let SegmentBatchPlan::Text { start, end } = batch else {
13072 continue;
13073 };
13074 for (_, item) in &ordered_items[start..end] {
13075 if offscreen_text_glyph_prewarm_budget_exhausted(prewarm_start, admitted_candidates)
13076 {
13077 skipped_budget = skipped_budget.saturating_add(1);
13078 break 'batches;
13079 }
13080 let SegmentDrawItem::Text(text_index) = item else {
13081 return Err(format!(
13082 "text prewarm batch contains non-text draw item: {item:?}"
13083 ));
13084 };
13085 let Some(text_draw) = texts.get(*text_index) else {
13086 continue;
13087 };
13088 text_items = text_items.saturating_add(1);
13089 match self.text_glyph_prewarm_decision(text_draw, viewport, root_scale) {
13090 TextGlyphPrewarmDecision::Candidate => {}
13091 TextGlyphPrewarmDecision::MissingGeometry => {
13092 missing_geometry = missing_geometry.saturating_add(1);
13093 continue;
13094 }
13095 TextGlyphPrewarmDecision::DynamicMotion => {
13096 dynamic_motion = dynamic_motion.saturating_add(1);
13097 continue;
13098 }
13099 TextGlyphPrewarmDecision::Visible => {
13100 visible = visible.saturating_add(1);
13101 continue;
13102 }
13103 TextGlyphPrewarmDecision::OutsidePrewarmWindow => {
13104 outside = outside.saturating_add(1);
13105 continue;
13106 }
13107 }
13108
13109 candidates = candidates.saturating_add(1);
13110 let Some((_, raster_rect, _, text_scale, static_text_motion)) =
13111 self.text_raster_geometry(text_draw, root_scale)
13112 else {
13113 missing_geometry = missing_geometry.saturating_add(1);
13114 continue;
13115 };
13116 let raster_source = text_glyph_raster_source(text_draw, raster_rect);
13117 let source_draw = raster_source.draw.as_ref();
13118 let run_key = Self::text_glyph_run_cache_key(
13119 source_draw,
13120 raster_source.raster_rect,
13121 text_scale,
13122 static_text_motion,
13123 );
13124 let atlas_generation = self.text_glyph_atlas.generation();
13125 let cached_glyphs = if let Some(cached) = self.text_glyph_run_cache.peek(&run_key) {
13126 if cached.atlas_generation == atlas_generation && cached.quads.is_some() {
13127 already_prepared = already_prepared.saturating_add(1);
13128 continue;
13129 }
13130 Some(cached.glyphs.len())
13131 } else {
13132 None
13133 };
13134 if !offscreen_text_glyph_prewarm_work_is_bounded(
13135 cached_glyphs,
13136 source_draw.text.text.len(),
13137 ) {
13138 skipped_unbounded = skipped_unbounded.saturating_add(1);
13139 continue;
13140 }
13141 admitted_candidates = admitted_candidates.saturating_add(1);
13142 self.append_text_glyph_draws(
13143 std::iter::once(text_draw),
13144 viewport,
13145 root_scale,
13146 true,
13147 staged_uploads,
13148 image_vertices,
13149 image_indices,
13150 glyph_cmds,
13151 )?;
13152 image_vertices.truncate(initial_vertex_len);
13153 image_indices.truncate(initial_index_len);
13154 glyph_cmds.truncate(initial_cmd_len);
13155 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
13156 }
13157 }
13158
13159 if diag_enabled && text_items > 0 {
13160 log::warn!(
13161 "[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}"
13162 );
13163 }
13164 if admitted_candidates > 0 {
13165 if let Some(total_ms) = should_log_wgpu_render_stage(prewarm_start, Instant::now()) {
13166 log::warn!(
13167 "[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}"
13168 );
13169 }
13170 }
13171 Ok(())
13172 }
13173
13174 fn prepare_text_glyph_draw_cmds<'a, I>(
13175 &mut self,
13176 layer_texts: I,
13177 viewport: ViewportUniformParams,
13178 root_scale: f32,
13179 staged_uploads: &mut StagedBufferUploads,
13180 ) -> Result<Option<PreparedGlyphBatch>, String>
13181 where
13182 I: IntoIterator<Item = &'a TextDraw>,
13183 {
13184 #[cfg(target_arch = "wasm32")]
13185 let _ = staged_uploads;
13186
13187 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
13188 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
13189 let mut glyph_cmds = std::mem::take(&mut self.scratch_glyph_cmds);
13190 image_vertices.clear();
13191 image_indices.clear();
13192 glyph_cmds.clear();
13193
13194 if !self.append_text_glyph_draws(
13195 layer_texts,
13196 viewport,
13197 root_scale,
13198 false,
13199 staged_uploads,
13200 &mut image_vertices,
13201 &mut image_indices,
13202 &mut glyph_cmds,
13203 )? {
13204 self.scratch_image_vertices = image_vertices;
13205 self.scratch_image_indices = image_indices;
13206 self.scratch_glyph_cmds = glyph_cmds;
13207 return Ok(None);
13208 }
13209
13210 #[cfg(not(target_arch = "wasm32"))]
13211 if !image_indices.is_empty() {
13212 self.stage_native_image_buffers(
13213 staged_uploads,
13214 viewport,
13215 &image_vertices,
13216 &image_indices,
13217 );
13218 }
13219
13220 #[cfg(target_arch = "wasm32")]
13221 let image_slot = if glyph_cmds.is_empty() {
13222 0
13223 } else {
13224 let slot = self.claim_wasm_image_batch();
13225 {
13226 let buffers = &mut self.wasm_image_batches[slot];
13227 buffers.ensure_capacity(&self.device, image_vertices.len(), image_indices.len());
13228 }
13229 let buffers = &self.wasm_image_batches[slot];
13230 self.write_wasm_buffer(
13231 &buffers.vertex_buffer,
13232 bytemuck::cast_slice(&image_vertices),
13233 );
13234 self.write_wasm_buffer(&buffers.index_buffer, bytemuck::cast_slice(&image_indices));
13235 slot
13236 };
13237
13238 #[cfg(target_arch = "wasm32")]
13239 let uniform_slot = if glyph_cmds.is_empty() {
13240 0
13241 } else {
13242 self.prepare_wasm_viewport_uniforms(viewport)
13243 };
13244
13245 self.scratch_image_vertices = image_vertices;
13246 self.scratch_image_indices = image_indices;
13247 Ok(Some(PreparedGlyphBatch {
13248 cmds: glyph_cmds,
13249 #[cfg(target_arch = "wasm32")]
13250 image_slot,
13251 #[cfg(target_arch = "wasm32")]
13252 uniform_slot,
13253 }))
13254 }
13255
13256 #[allow(clippy::too_many_arguments)]
13257 fn append_image_bitmap_draw_cmd(
13258 &mut self,
13259 image: &ImageBitmap,
13260 rect: Rect,
13261 clip: Option<Rect>,
13262 sampling: ImageSampling,
13263 viewport: ViewportUniformParams,
13264 root_scale: f32,
13265 image_vertices: &mut Vec<Vertex>,
13266 image_indices: &mut Vec<u32>,
13267 image_cmds: &mut Vec<ImageDrawCmd>,
13268 ) -> Result<(), String> {
13269 if rect.width <= 0.0 || rect.height <= 0.0 {
13270 return Ok(());
13271 }
13272
13273 self.ensure_image_cached(image)?;
13274
13275 let (device_quad, scissor_rect) =
13276 if sampling == ImageSampling::Nearest && root_scale.is_finite() && root_scale > 0.0 {
13277 let left_px = (rect.x * root_scale).round();
13278 let top_px = (rect.y * root_scale).round();
13279 let width_px = (rect.width * root_scale).round().max(1.0);
13280 let height_px = (rect.height * root_scale).round().max(1.0);
13281 let snapped_rect = Rect {
13282 x: left_px / root_scale,
13283 y: top_px / root_scale,
13284 width: width_px / root_scale,
13285 height: height_px / root_scale,
13286 };
13287 let right_px = left_px + width_px;
13288 let bottom_px = top_px + height_px;
13289 (
13290 [
13291 [left_px, top_px],
13292 [right_px, top_px],
13293 [left_px, bottom_px],
13294 [right_px, bottom_px],
13295 ],
13296 snapped_rect,
13297 )
13298 } else {
13299 (
13300 rect_to_quad(rect).map(|[x, y]| [x * root_scale, y * root_scale]),
13301 rect,
13302 )
13303 };
13304
13305 let Some(scissor) = scissor_rect_for_layer(
13306 scissor_rect,
13307 clip,
13308 root_scale,
13309 viewport.width,
13310 viewport.height,
13311 ) else {
13312 return Ok(());
13313 };
13314 let Some(uv_rect) = image_uv_rect(image, None) else {
13315 return Ok(());
13316 };
13317 #[cfg(not(target_arch = "wasm32"))]
13318 {
13319 if fill_area_diag_enabled() {
13320 self.fill_area_diag.add_image_quad(&device_quad);
13321 }
13322 }
13323
13324 let base_vertex = image_vertices.len() as u32;
13325 let index_start = image_indices.len() as u32;
13326 image_indices.extend_from_slice(&[
13327 base_vertex,
13328 base_vertex + 1,
13329 base_vertex + 2,
13330 base_vertex + 2,
13331 base_vertex + 1,
13332 base_vertex + 3,
13333 ]);
13334 let color = [1.0, 1.0, 1.0, 1.0];
13335 image_vertices.extend_from_slice(&[
13336 Vertex {
13337 position: device_quad[0],
13338 color,
13339 uv: [uv_rect.min[0], uv_rect.min[1]],
13340 uv_bounds: uv_rect.sample_bounds,
13341 },
13342 Vertex {
13343 position: device_quad[1],
13344 color,
13345 uv: [uv_rect.max[0], uv_rect.min[1]],
13346 uv_bounds: uv_rect.sample_bounds,
13347 },
13348 Vertex {
13349 position: device_quad[2],
13350 color,
13351 uv: [uv_rect.min[0], uv_rect.max[1]],
13352 uv_bounds: uv_rect.sample_bounds,
13353 },
13354 Vertex {
13355 position: device_quad[3],
13356 color,
13357 uv: [uv_rect.max[0], uv_rect.max[1]],
13358 uv_bounds: uv_rect.sample_bounds,
13359 },
13360 ]);
13361 image_cmds.push(ImageDrawCmd {
13362 index_start,
13363 scissor,
13364 image_id: image.id(),
13365 sampling,
13366 });
13367 Ok(())
13368 }
13369
13370 #[allow(clippy::too_many_arguments)]
13371 fn append_text_image_draw_cmds<'a, I>(
13372 &mut self,
13373 layer_texts: I,
13374 viewport: ViewportUniformParams,
13375 root_scale: f32,
13376 image_vertices: &mut Vec<Vertex>,
13377 image_indices: &mut Vec<u32>,
13378 image_cmds: &mut Vec<ImageDrawCmd>,
13379 ) -> Result<(), String>
13380 where
13381 I: Iterator<Item = &'a TextDraw>,
13382 {
13383 let append_start = Instant::now();
13384 let initial_len = image_cmds.len();
13385 let mut visited = 0usize;
13386 let mut hit_count = 0usize;
13387 let mut miss_count = 0usize;
13388 for text_draw in layer_texts {
13389 visited = visited.saturating_add(1);
13390 let _ = text_draw.node_id;
13391 let Some((logical_rect, raster_rect, clip, text_scale, static_text_motion)) =
13392 self.text_raster_geometry(text_draw, root_scale)
13393 else {
13394 continue;
13395 };
13396 if !text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale) {
13397 continue;
13398 }
13399
13400 let raster_source = self.text_image_raster_source(
13401 text_draw,
13402 logical_rect,
13403 raster_rect,
13404 clip,
13405 root_scale,
13406 static_text_motion,
13407 );
13408 let source_draw = raster_source.draw.as_ref();
13409 let source_raster_rect = raster_source.raster_rect;
13410
13411 let cache_key = Self::text_image_cache_key(
13412 source_draw,
13413 source_raster_rect,
13414 text_scale,
13415 static_text_motion,
13416 );
13417 let image = if let Some(cached) = self.text_image_cache.get(&cache_key) {
13418 self.frame_stats
13419 .record_text_image_cache_hit(cached.image.width(), cached.image.height());
13420 hit_count = hit_count.saturating_add(1);
13421 cached.image.clone()
13422 } else {
13423 let Some(image) =
13424 self.rasterize_text_draw_to_image(source_draw, source_raster_rect, text_scale)
13425 else {
13426 continue;
13427 };
13428 self.frame_stats
13429 .record_text_image_cache_miss(image.width(), image.height());
13430 miss_count = miss_count.saturating_add(1);
13431 self.text_image_cache.put(
13432 cache_key,
13433 CachedTextImage {
13434 image: image.clone(),
13435 },
13436 );
13437 image
13438 };
13439
13440 let draw_origin = if static_text_motion {
13441 Point::new(
13442 source_raster_rect.x / root_scale,
13443 source_raster_rect.y / root_scale,
13444 )
13445 } else {
13446 Point::new(logical_rect.x, logical_rect.y)
13447 };
13448 let draw_rect = Rect {
13449 x: draw_origin.x,
13450 y: draw_origin.y,
13451 width: image.width() as f32 / root_scale,
13452 height: image.height() as f32 / root_scale,
13453 };
13454 self.append_image_bitmap_draw_cmd(
13455 &image,
13456 draw_rect,
13457 clip,
13458 ImageSampling::Nearest,
13459 viewport,
13460 root_scale,
13461 image_vertices,
13462 image_indices,
13463 image_cmds,
13464 )?;
13465 }
13466 let append_end = Instant::now();
13467 if let Some(total_ms) = should_log_wgpu_render_stage(append_start, append_end) {
13468 log::warn!(
13469 "[wgpu-render-stage:text-images] total_ms={total_ms:.2} visited={} emitted={} hits={} misses={}",
13470 visited,
13471 image_cmds.len().saturating_sub(initial_len),
13472 hit_count,
13473 miss_count,
13474 );
13475 }
13476 Ok(())
13477 }
13478
13479 fn text_image_raster_source<'a>(
13480 &mut self,
13481 text_draw: &'a TextDraw,
13482 logical_rect: Rect,
13483 raster_rect: Rect,
13484 clip: Option<Rect>,
13485 root_scale: f32,
13486 static_text_motion: bool,
13487 ) -> TextRasterSource<'a> {
13488 let Some(clip) = clip else {
13489 return TextRasterSource {
13490 draw: Cow::Borrowed(text_draw),
13491 raster_rect,
13492 };
13493 };
13494 if !static_text_motion || text_draw.text.text.as_str().find('\n').is_none() {
13495 return TextRasterSource {
13496 draw: Cow::Borrowed(text_draw),
13497 raster_rect,
13498 };
13499 }
13500
13501 let line_starts = self.text_line_index_cache.line_starts(&text_draw.text);
13502 clipped_text_raster_source_with_line_starts(
13503 text_draw,
13504 logical_rect,
13505 raster_rect,
13506 clip,
13507 root_scale,
13508 line_starts.as_ref(),
13509 )
13510 }
13511
13512 fn prepare_text_image_draw_cmds<'a, I>(
13513 &mut self,
13514 layer_texts: I,
13515 viewport: ViewportUniformParams,
13516 root_scale: f32,
13517 staged_uploads: &mut StagedBufferUploads,
13518 ) -> Result<PreparedImageBatch, String>
13519 where
13520 I: Iterator<Item = &'a TextDraw>,
13521 {
13522 #[cfg(target_arch = "wasm32")]
13523 let _ = staged_uploads;
13524
13525 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
13526 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
13527 let mut image_cmds = std::mem::take(&mut self.scratch_image_cmds);
13528 image_vertices.clear();
13529 image_indices.clear();
13530 image_cmds.clear();
13531
13532 self.append_text_image_draw_cmds(
13533 layer_texts,
13534 viewport,
13535 root_scale,
13536 &mut image_vertices,
13537 &mut image_indices,
13538 &mut image_cmds,
13539 )?;
13540
13541 #[cfg(not(target_arch = "wasm32"))]
13542 if !image_cmds.is_empty() {
13543 self.stage_native_image_buffers(
13544 staged_uploads,
13545 viewport,
13546 &image_vertices,
13547 &image_indices,
13548 );
13549 }
13550
13551 #[cfg(target_arch = "wasm32")]
13552 let image_slot = if image_cmds.is_empty() {
13553 0
13554 } else {
13555 let slot = self.claim_wasm_image_batch();
13556 {
13557 let buffers = &mut self.wasm_image_batches[slot];
13558 buffers.ensure_capacity(&self.device, image_vertices.len(), image_indices.len());
13559 }
13560 let buffers = &self.wasm_image_batches[slot];
13561 self.write_wasm_buffer(
13562 &buffers.vertex_buffer,
13563 bytemuck::cast_slice(&image_vertices),
13564 );
13565 self.write_wasm_buffer(&buffers.index_buffer, bytemuck::cast_slice(&image_indices));
13566 slot
13567 };
13568
13569 #[cfg(target_arch = "wasm32")]
13570 let uniform_slot = if image_cmds.is_empty() {
13571 0
13572 } else {
13573 self.prepare_wasm_viewport_uniforms(viewport)
13574 };
13575
13576 self.scratch_image_vertices = image_vertices;
13577 self.scratch_image_indices = image_indices;
13578 Ok(PreparedImageBatch {
13579 cmds: image_cmds,
13580 #[cfg(target_arch = "wasm32")]
13581 image_slot,
13582 #[cfg(target_arch = "wasm32")]
13583 uniform_slot,
13584 })
13585 }
13586
13587 fn text_raster_geometry(
13588 &self,
13589 text_draw: &TextDraw,
13590 root_scale: f32,
13591 ) -> Option<(Rect, Rect, Option<Rect>, f32, bool)> {
13592 text_raster_geometry_for_draw(text_draw, root_scale)
13593 }
13594
13595 fn text_image_cache_key(
13596 text_draw: &TextDraw,
13597 raster_rect: Rect,
13598 text_scale: f32,
13599 static_text_motion: bool,
13600 ) -> TextImageCacheKey {
13601 let mut state = default_hash::new();
13602 text_draw.text.render_hash().hash(&mut state);
13603 text_draw.text_style.render_hash().hash(&mut state);
13604 text_draw.color.render_hash().hash(&mut state);
13605 hash_text_raster_geometry_for_cache(raster_rect, static_text_motion, &mut state);
13606 text_draw.font_size.to_bits().hash(&mut state);
13607 text_scale.to_bits().hash(&mut state);
13608 text_draw.layout_options.hash(&mut state);
13609 TextImageCacheKey(state.finish())
13610 }
13611
13612 fn text_glyph_run_cache_key(
13613 text_draw: &TextDraw,
13614 raster_rect: Rect,
13615 text_scale: f32,
13616 static_text_motion: bool,
13617 ) -> TextGlyphRunCacheKey {
13618 TextGlyphRunCacheKey(
13619 Self::text_image_cache_key(text_draw, raster_rect, text_scale, static_text_motion).0,
13620 )
13621 }
13622
13623 fn rasterize_text_draw_to_image(
13624 &mut self,
13625 text_draw: &TextDraw,
13626 raster_rect: Rect,
13627 text_scale: f32,
13628 ) -> Option<ImageBitmap> {
13629 if text_draw.text.span_styles.is_empty() {
13630 let font = self.text_fonts.resolve(&text_draw.text_style)?;
13631 return rasterize_text_to_image_with_glyph_cache(
13632 text_draw.text.text.as_str(),
13633 raster_rect,
13634 &text_draw.text_style,
13635 text_draw.color,
13636 text_draw.font_size,
13637 text_scale,
13638 font,
13639 &mut self.text_glyph_mask_cache,
13640 );
13641 }
13642
13643 if let Some(image) = rasterize_annotated_text_to_image_with_glyph_cache(
13644 text_draw.text.as_ref(),
13645 raster_rect,
13646 &text_draw.text_style,
13647 text_draw.color,
13648 text_draw.font_size,
13649 text_scale,
13650 &self.text_fonts,
13651 &mut self.text_glyph_mask_cache,
13652 ) {
13653 return Some(image);
13654 }
13655
13656 rasterize_spanned_text_to_image(
13657 text_draw,
13658 raster_rect,
13659 text_scale,
13660 &self.text_fonts,
13661 &mut self.text_glyph_mask_cache,
13662 )
13663 }
13664}
13665
13666fn rasterize_spanned_text_to_image(
13667 text_draw: &TextDraw,
13668 raster_rect: Rect,
13669 text_scale: f32,
13670 fonts: &SoftwareTextFontSet,
13671 glyph_cache: &mut SoftwareGlyphRasterCache,
13672) -> Option<ImageBitmap> {
13673 let width = raster_rect.width.ceil().max(1.0) as u32;
13674 let height = raster_rect.height.ceil().max(1.0) as u32;
13675 let mut canvas = vec![0_u8; (width as usize) * (height as usize) * 4];
13676 let boundaries = text_draw.text.span_boundaries();
13677 let base_line_height = text_draw
13678 .text_style
13679 .resolve_line_height(14.0, text_draw.font_size)
13680 .max(1.0);
13681 let mut current_line_height = base_line_height;
13682 let mut cursor_x = raster_rect.x;
13683 let mut cursor_y = raster_rect.y;
13684
13685 for window in boundaries.windows(2) {
13686 let start = window[0];
13687 let end = window[1];
13688 if start == end {
13689 continue;
13690 }
13691
13692 let chunk = &text_draw.text.text[start..end];
13693 let mut merged_span = text_draw.text_style.span_style.clone();
13694 for span in &text_draw.text.span_styles {
13695 if span.range.start <= start && span.range.end >= end {
13696 merged_span = merged_span.merge(&span.item);
13697 }
13698 }
13699
13700 let mut chunk_style = text_draw.text_style.clone();
13701 chunk_style.span_style = merged_span;
13702
13703 for part in chunk.split_inclusive('\n') {
13704 let has_newline = part.ends_with('\n');
13705 let content = if has_newline {
13706 &part[..part.len().saturating_sub(1)]
13707 } else {
13708 part
13709 };
13710
13711 if !content.is_empty() {
13712 let chunk_font_size = chunk_style.resolve_font_size(text_draw.font_size);
13713 let Some(font) = fonts.resolve(&chunk_style) else {
13714 continue;
13715 };
13716 let metrics = measure_text_with_font(content, &chunk_style, chunk_font_size, font);
13717 let segment_rect = Rect {
13718 x: cursor_x,
13719 y: cursor_y,
13720 width: (metrics.width * text_scale).ceil().max(1.0),
13721 height: (metrics.height * text_scale).ceil().max(1.0),
13722 };
13723 if let Some(segment_image) = rasterize_text_to_image_with_glyph_cache(
13724 content,
13725 segment_rect,
13726 &chunk_style,
13727 chunk_style.resolve_text_color(text_draw.color),
13728 chunk_font_size,
13729 text_scale,
13730 font,
13731 glyph_cache,
13732 ) {
13733 composite_text_segment(
13734 &mut canvas,
13735 width,
13736 height,
13737 raster_rect,
13738 segment_rect,
13739 &segment_image,
13740 );
13741 }
13742 cursor_x += metrics.width * text_scale;
13743 current_line_height = current_line_height.max(metrics.line_height.max(1.0));
13744 }
13745
13746 if has_newline {
13747 cursor_x = raster_rect.x;
13748 cursor_y += current_line_height * text_scale;
13749 current_line_height = base_line_height;
13750 }
13751 }
13752 }
13753
13754 ImageBitmap::from_rgba8(width, height, canvas).ok()
13755}
13756
13757struct TextRasterSource<'a> {
13758 draw: Cow<'a, TextDraw>,
13759 raster_rect: Rect,
13760}
13761
13762fn text_glyph_raster_source(text_draw: &TextDraw, raster_rect: Rect) -> TextRasterSource<'_> {
13763 TextRasterSource {
13764 draw: Cow::Borrowed(text_draw),
13765 raster_rect,
13766 }
13767}
13768
13769#[cfg(test)]
13770fn clipped_text_raster_source<'a>(
13771 text_draw: &'a TextDraw,
13772 logical_rect: Rect,
13773 raster_rect: Rect,
13774 clip: Option<Rect>,
13775 root_scale: f32,
13776 static_text_motion: bool,
13777) -> TextRasterSource<'a> {
13778 let Some(clip) = clip else {
13779 return TextRasterSource {
13780 draw: Cow::Borrowed(text_draw),
13781 raster_rect,
13782 };
13783 };
13784 if !static_text_motion || text_draw.text.text.as_str().find('\n').is_none() {
13785 return TextRasterSource {
13786 draw: Cow::Borrowed(text_draw),
13787 raster_rect,
13788 };
13789 }
13790 let line_starts = line_start_offsets(text_draw.text.text.as_str());
13791 clipped_text_raster_source_with_line_starts(
13792 text_draw,
13793 logical_rect,
13794 raster_rect,
13795 clip,
13796 root_scale,
13797 &line_starts,
13798 )
13799}
13800
13801fn clipped_text_raster_source_with_line_starts<'a>(
13802 text_draw: &'a TextDraw,
13803 logical_rect: Rect,
13804 raster_rect: Rect,
13805 clip: Rect,
13806 root_scale: f32,
13807 line_starts: &[usize],
13808) -> TextRasterSource<'a> {
13809 if line_starts.len() < MIN_MULTILINE_TEXT_LINES_FOR_CLIPPED_RASTER {
13810 return TextRasterSource {
13811 draw: Cow::Borrowed(text_draw),
13812 raster_rect,
13813 };
13814 }
13815
13816 let Some(visible_rect) = logical_rect.intersect(clip) else {
13817 return TextRasterSource {
13818 draw: Cow::Borrowed(text_draw),
13819 raster_rect,
13820 };
13821 };
13822
13823 let line_count = line_starts.len().max(1);
13824 let line_height = logical_rect.height / line_count as f32;
13825 if !line_height.is_finite() || line_height <= 0.0 {
13826 return TextRasterSource {
13827 draw: Cow::Borrowed(text_draw),
13828 raster_rect,
13829 };
13830 }
13831
13832 let visible_top = ((visible_rect.y - logical_rect.y) / line_height).floor() as isize;
13833 let visible_bottom =
13834 ((visible_rect.y + visible_rect.height - logical_rect.y) / line_height).ceil() as isize;
13835 let start_line = visible_top.saturating_sub(1).max(0) as usize;
13836 let end_line = (visible_bottom + 1).max(start_line as isize + 1) as usize;
13837 let end_line = end_line.min(line_count);
13838 if start_line == 0 && end_line >= line_count {
13839 return TextRasterSource {
13840 draw: Cow::Borrowed(text_draw),
13841 raster_rect,
13842 };
13843 }
13844
13845 let byte_start = line_starts[start_line];
13846 let byte_end = line_end_offset(text_draw.text.text.as_str(), line_starts, end_line - 1);
13847 if byte_start >= byte_end {
13848 return TextRasterSource {
13849 draw: Cow::Borrowed(text_draw),
13850 raster_rect,
13851 };
13852 }
13853
13854 let slice_y = logical_rect.y + start_line as f32 * line_height;
13855 let slice_height = (end_line - start_line) as f32 * line_height;
13856 let mut slice_raster_rect = Rect {
13857 x: logical_rect.x * root_scale,
13858 y: slice_y * root_scale,
13859 width: logical_rect.width * root_scale,
13860 height: slice_height * root_scale,
13861 };
13862 slice_raster_rect.x = slice_raster_rect.x.round();
13863 slice_raster_rect.y = slice_raster_rect.y.round();
13864 slice_raster_rect.width = slice_raster_rect.width.ceil().max(1.0);
13865 slice_raster_rect.height = slice_raster_rect.height.ceil().max(1.0);
13866
13867 let mut sliced_draw = text_draw.clone();
13868 sliced_draw.rect = Rect {
13869 x: logical_rect.x,
13870 y: slice_y,
13871 width: logical_rect.width,
13872 height: slice_height,
13873 };
13874 sliced_draw.text = Arc::new(text_draw.text.subsequence(byte_start..byte_end));
13875
13876 TextRasterSource {
13877 draw: Cow::Owned(sliced_draw),
13878 raster_rect: slice_raster_rect,
13879 }
13880}
13881
13882fn line_start_offsets(text: &str) -> Vec<usize> {
13883 let mut starts =
13884 Vec::with_capacity(text.as_bytes().iter().filter(|b| **b == b'\n').count() + 1);
13885 starts.push(0);
13886 starts.extend(
13887 text.char_indices()
13888 .filter_map(|(index, ch)| (ch == '\n').then_some(index + ch.len_utf8())),
13889 );
13890 starts
13891}
13892
13893fn line_end_offset(text: &str, line_starts: &[usize], line: usize) -> usize {
13894 line_starts.get(line + 1).copied().unwrap_or(text.len())
13895}
13896
13897fn composite_text_segment(
13898 canvas: &mut [u8],
13899 canvas_width: u32,
13900 canvas_height: u32,
13901 canvas_rect: Rect,
13902 segment_rect: Rect,
13903 segment_image: &ImageBitmap,
13904) {
13905 let offset_x = (segment_rect.x - canvas_rect.x).round() as i32;
13906 let offset_y = (segment_rect.y - canvas_rect.y).round() as i32;
13907 let src = segment_image.pixels();
13908 for sy in 0..segment_image.height() as i32 {
13909 let dy = offset_y + sy;
13910 if dy < 0 || dy >= canvas_height as i32 {
13911 continue;
13912 }
13913 for sx in 0..segment_image.width() as i32 {
13914 let dx = offset_x + sx;
13915 if dx < 0 || dx >= canvas_width as i32 {
13916 continue;
13917 }
13918 let src_index = ((sy as u32 * segment_image.width() + sx as u32) * 4) as usize;
13919 let dst_index = ((dy as u32 * canvas_width + dx as u32) * 4) as usize;
13920 blend_rgba_pixel(
13921 &mut canvas[dst_index..dst_index + 4],
13922 &src[src_index..src_index + 4],
13923 );
13924 }
13925 }
13926}
13927
13928fn blend_rgba_pixel(dst: &mut [u8], src: &[u8]) {
13929 let src_alpha = src[3] as f32 / 255.0;
13930 if src_alpha <= 0.0 {
13931 return;
13932 }
13933 let dst_alpha = dst[3] as f32 / 255.0;
13934 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
13935 if out_alpha <= f32::EPSILON {
13936 dst.copy_from_slice(&[0, 0, 0, 0]);
13937 return;
13938 }
13939
13940 for channel in 0..3 {
13941 let src_channel = src[channel] as f32 / 255.0;
13942 let dst_channel = dst[channel] as f32 / 255.0;
13943 let src_premult = src_channel * src_alpha;
13944 let dst_premult = dst_channel * dst_alpha;
13945 dst[channel] =
13946 (((src_premult + dst_premult * (1.0 - src_alpha)) / out_alpha).clamp(0.0, 1.0) * 255.0)
13947 .round() as u8;
13948 }
13949 dst[3] = (out_alpha.clamp(0.0, 1.0) * 255.0).round() as u8;
13950}
13951
13952fn align_to(value: u32, alignment: u32) -> u32 {
13953 debug_assert!(alignment > 0);
13954 value.div_ceil(alignment) * alignment
13955}
13956
13957#[cfg(not(target_arch = "wasm32"))]
13958fn align_usize_to(value: usize, alignment: usize) -> usize {
13959 debug_assert!(alignment > 0);
13960 value.div_ceil(alignment) * alignment
13961}
13962
13963impl GpuRenderer {
13964 fn convert_surface_pixels_to_rgba(&self, pixels: &mut [u8]) -> Result<(), String> {
13965 match self.surface_format {
13966 wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Rgba8UnormSrgb => Ok(()),
13967 wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb => {
13968 for pixel in pixels.as_chunks_mut::<4>().0 {
13969 pixel.swap(0, 2);
13970 }
13971 Ok(())
13972 }
13973 format => Err(format!(
13974 "Screenshot readback unsupported for texture format: {format:?}"
13975 )),
13976 }
13977 }
13978}
13979
13980fn is_in_effect_range(z_index: usize, effect_z_ranges: &[Range<usize>]) -> bool {
13981 effect_z_ranges.iter().any(|range| range.contains(&z_index))
13982}
13983
13984#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13985enum SegmentDrawItem {
13986 Shape(usize),
13987 Image(usize),
13988 Text(usize),
13989 Shadow(usize),
13990 Composite(usize),
13991 ShaderComposite(usize),
13992 Retained(usize),
13993}
13994
13995#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13996enum SegmentBatchPlan {
13997 Shape {
13998 start: usize,
13999 end: usize,
14000 blend_mode: BlendMode,
14001 },
14002 Image {
14003 start: usize,
14004 end: usize,
14005 blend_mode: BlendMode,
14006 },
14007 Text {
14008 start: usize,
14009 end: usize,
14010 },
14011 Composite {
14012 start: usize,
14013 end: usize,
14014 },
14015 ShaderComposite {
14016 start: usize,
14017 end: usize,
14018 },
14019 Retained {
14022 start: usize,
14023 end: usize,
14024 },
14025}
14026
14027#[derive(Clone, Debug, Default, PartialEq, Eq)]
14028struct SegmentDrawChunkPlan {
14029 batches: Vec<SegmentBatchPlan>,
14030}
14031
14032struct SegmentRenderOutcome {
14033 rendered_any: bool,
14034 pass_count: u32,
14035}
14036
14037struct SegmentCommandEncodeOutcome {
14038 first_batch: bool,
14039}
14040
14041#[cfg(not(target_arch = "wasm32"))]
14042#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14043enum TextGlyphPrewarmDecision {
14044 Candidate,
14045 MissingGeometry,
14046 DynamicMotion,
14047 Visible,
14048 OutsidePrewarmWindow,
14049}
14050
14051#[cfg(not(target_arch = "wasm32"))]
14052#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14053struct NativeSegmentFusionBudget {
14054 shape_count: usize,
14055 gradient_stop_count: usize,
14056}
14057
14058#[cfg(not(target_arch = "wasm32"))]
14059#[derive(Clone, Debug, PartialEq, Eq)]
14060struct NativeSegmentFusionPartition {
14061 chunk: SegmentDrawChunkPlan,
14062 budget: NativeSegmentFusionBudget,
14063}
14064
14065#[cfg(not(target_arch = "wasm32"))]
14066#[derive(Clone, Debug, PartialEq, Eq)]
14067enum FusedSegmentBatch {
14068 Shape {
14069 batch: PreparedShapeBatch,
14070 blend_mode: BlendMode,
14071 },
14072 Image {
14073 cmd_range: Range<usize>,
14074 blend_mode: BlendMode,
14075 },
14076 Text {
14077 image_cmd_range: Range<usize>,
14078 glyph_cmd_range: Range<usize>,
14079 },
14080 Composite {
14081 draw_range: Range<usize>,
14082 },
14083 ShaderComposite {
14084 draw_range: Range<usize>,
14085 },
14086 Retained {
14087 item_range: Range<usize>,
14088 },
14089}
14090
14091struct ShadowSourceRenderOutcome {
14092 rendered_any: bool,
14093 pass_count: u32,
14094}
14095
14096impl SegmentDrawChunkPlan {
14097 fn is_empty(&self) -> bool {
14098 self.batches.is_empty()
14099 }
14100
14101 fn push(&mut self, batch: SegmentBatchPlan) {
14102 self.batches.push(batch);
14103 }
14104
14105 fn iter(&self) -> impl Iterator<Item = SegmentBatchPlan> + '_ {
14106 self.batches.iter().copied()
14107 }
14108}
14109
14110#[derive(Clone, Debug, PartialEq, Eq)]
14111enum SegmentRenderCommand {
14112 DrawChunk(SegmentDrawChunkPlan),
14113 Shadow(usize),
14114}
14115
14116struct SegmentCommandIter<'a> {
14117 ordered_items: &'a [(usize, SegmentDrawItem)],
14118 shapes: &'a [DrawShape],
14119 images: &'a [ImageDraw],
14120 cursor: usize,
14121 batch_limits: ShapeBatchLimits,
14122}
14123
14124impl<'a> SegmentCommandIter<'a> {
14125 fn new(
14126 ordered_items: &'a [(usize, SegmentDrawItem)],
14127 shapes: &'a [DrawShape],
14128 images: &'a [ImageDraw],
14129 batch_limits: ShapeBatchLimits,
14130 ) -> Self {
14131 Self {
14132 ordered_items,
14133 shapes,
14134 images,
14135 cursor: 0,
14136 batch_limits,
14137 }
14138 }
14139}
14140
14141impl Iterator for SegmentCommandIter<'_> {
14142 type Item = SegmentRenderCommand;
14143
14144 fn next(&mut self) -> Option<Self::Item> {
14145 if self.cursor >= self.ordered_items.len() {
14146 return None;
14147 }
14148
14149 if let SegmentDrawItem::Shadow(index) = self.ordered_items[self.cursor].1 {
14150 self.cursor += 1;
14151 return Some(SegmentRenderCommand::Shadow(index));
14152 }
14153
14154 let mut chunk = SegmentDrawChunkPlan::default();
14155 while self.cursor < self.ordered_items.len() {
14156 if let SegmentDrawItem::Shadow(index) = self.ordered_items[self.cursor].1 {
14157 if chunk.is_empty() {
14158 self.cursor += 1;
14159 return Some(SegmentRenderCommand::Shadow(index));
14160 }
14161 break;
14162 }
14163
14164 let Some((batch, next_cursor)) = segment_batch_plan_at_cursor(
14165 self.ordered_items,
14166 self.shapes,
14167 self.images,
14168 self.cursor,
14169 self.batch_limits,
14170 ) else {
14171 break;
14172 };
14173 chunk.push(batch);
14174 self.cursor = next_cursor;
14175 }
14176
14177 Some(SegmentRenderCommand::DrawChunk(chunk))
14178 }
14179}
14180
14181#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14182struct PreparedShapeBatch {
14183 vertex_start: u32,
14186 vertex_count: u32,
14187 has_gradient: bool,
14190 #[cfg(target_arch = "wasm32")]
14191 shape_slot: usize,
14192 #[cfg(target_arch = "wasm32")]
14193 uniform_slot: usize,
14194}
14195
14196struct PreparedImageBatch {
14197 cmds: Vec<ImageDrawCmd>,
14198 #[cfg(target_arch = "wasm32")]
14199 image_slot: usize,
14200 #[cfg(target_arch = "wasm32")]
14201 uniform_slot: usize,
14202}
14203
14204impl PreparedImageBatch {
14205 fn is_empty(&self) -> bool {
14206 self.cmds.is_empty()
14207 }
14208
14209 fn into_cmds(self) -> Vec<ImageDrawCmd> {
14210 self.cmds
14211 }
14212}
14213
14214struct PreparedGlyphBatch {
14215 cmds: Vec<GlyphDrawCmd>,
14216 #[cfg(target_arch = "wasm32")]
14217 image_slot: usize,
14218 #[cfg(target_arch = "wasm32")]
14219 uniform_slot: usize,
14220}
14221
14222impl PreparedGlyphBatch {
14223 fn is_empty(&self) -> bool {
14224 self.cmds.is_empty()
14225 }
14226
14227 fn into_cmds(self) -> Vec<GlyphDrawCmd> {
14228 self.cmds
14229 }
14230}
14231
14232#[cfg(not(target_arch = "wasm32"))]
14233fn gradient_stop_count_for_shape(shape: &DrawShape, brushes: &[Brush]) -> usize {
14234 match shape.brush {
14235 SceneBrush::Solid(_) => 0,
14236 SceneBrush::Gradient(index) => match &brushes[index as usize] {
14237 Brush::Solid(_) => 0,
14238 Brush::LinearGradient { colors, .. }
14239 | Brush::RadialGradient { colors, .. }
14240 | Brush::SweepGradient { colors, .. } => colors.len(),
14241 },
14242 }
14243}
14244
14245#[cfg(not(target_arch = "wasm32"))]
14246fn native_segment_fusion_budget(
14247 ordered_items: &[(usize, SegmentDrawItem)],
14248 shapes: &[DrawShape],
14249 brushes: &[Brush],
14250 chunk: &SegmentDrawChunkPlan,
14251 batch_limits: ShapeBatchLimits,
14252) -> Result<Option<NativeSegmentFusionBudget>, String> {
14253 let mut shape_count = 0usize;
14254 let mut gradient_stop_count = 0usize;
14255
14256 for batch in chunk.iter() {
14257 let SegmentBatchPlan::Shape { start, end, .. } = batch else {
14258 continue;
14259 };
14260 for (_, item) in &ordered_items[start..end] {
14261 let SegmentDrawItem::Shape(shape_index) = item else {
14262 return Err(format!(
14263 "shape batch contains non-shape draw item: {item:?}"
14264 ));
14265 };
14266 let shape = &shapes[*shape_index];
14267 shape_count = shape_count.saturating_add(1);
14268 gradient_stop_count =
14269 gradient_stop_count.saturating_add(gradient_stop_count_for_shape(shape, brushes));
14270 }
14271 }
14272
14273 if shape_count > batch_limits.max_shapes_per_batch
14274 || gradient_stop_count > batch_limits.max_gradient_stops
14275 {
14276 return Ok(None);
14277 }
14278
14279 Ok(Some(NativeSegmentFusionBudget {
14280 shape_count,
14281 gradient_stop_count,
14282 }))
14283}
14284
14285#[cfg(not(target_arch = "wasm32"))]
14286fn push_native_segment_fusion_partition(
14287 partitions: &mut Vec<NativeSegmentFusionPartition>,
14288 current: &mut SegmentDrawChunkPlan,
14289 current_budget: &mut NativeSegmentFusionBudget,
14290) {
14291 if current.is_empty() {
14292 return;
14293 }
14294
14295 partitions.push(NativeSegmentFusionPartition {
14296 chunk: std::mem::take(current),
14297 budget: *current_budget,
14298 });
14299 *current_budget = NativeSegmentFusionBudget {
14300 shape_count: 0,
14301 gradient_stop_count: 0,
14302 };
14303}
14304
14305#[cfg(not(target_arch = "wasm32"))]
14306fn native_segment_fusion_partitions(
14307 ordered_items: &[(usize, SegmentDrawItem)],
14308 shapes: &[DrawShape],
14309 brushes: &[Brush],
14310 chunk: &SegmentDrawChunkPlan,
14311 batch_limits: ShapeBatchLimits,
14312) -> Result<Option<Vec<NativeSegmentFusionPartition>>, String> {
14313 if let Some(budget) =
14314 native_segment_fusion_budget(ordered_items, shapes, brushes, chunk, batch_limits)?
14315 {
14316 return Ok(Some(vec![NativeSegmentFusionPartition {
14317 chunk: chunk.clone(),
14318 budget,
14319 }]));
14320 }
14321
14322 let mut partitions = Vec::new();
14323 let mut current = SegmentDrawChunkPlan::default();
14324 let mut current_budget = NativeSegmentFusionBudget {
14325 shape_count: 0,
14326 gradient_stop_count: 0,
14327 };
14328
14329 for batch in chunk.iter() {
14330 let SegmentBatchPlan::Shape {
14331 start,
14332 end,
14333 blend_mode,
14334 } = batch
14335 else {
14336 current.push(batch);
14337 continue;
14338 };
14339
14340 let mut run_start = start;
14341 for (item_cursor, (_, item)) in ordered_items.iter().enumerate().take(end).skip(start) {
14342 let SegmentDrawItem::Shape(shape_index) = *item else {
14343 return Err(format!(
14344 "shape batch contains non-shape draw item: {:?}",
14345 item
14346 ));
14347 };
14348 let gradient_stop_count = gradient_stop_count_for_shape(&shapes[shape_index], brushes);
14349 if gradient_stop_count > batch_limits.max_gradient_stops {
14350 return Ok(None);
14351 }
14352
14353 let fits_shape_count =
14354 current_budget.shape_count.saturating_add(1) <= batch_limits.max_shapes_per_batch;
14355 let fits_gradient_count = current_budget
14356 .gradient_stop_count
14357 .saturating_add(gradient_stop_count)
14358 <= batch_limits.max_gradient_stops;
14359 if !fits_shape_count || !fits_gradient_count {
14360 if run_start < item_cursor {
14361 current.push(SegmentBatchPlan::Shape {
14362 start: run_start,
14363 end: item_cursor,
14364 blend_mode,
14365 });
14366 }
14367 push_native_segment_fusion_partition(
14368 &mut partitions,
14369 &mut current,
14370 &mut current_budget,
14371 );
14372 run_start = item_cursor;
14373 }
14374
14375 current_budget.shape_count = current_budget.shape_count.saturating_add(1);
14376 current_budget.gradient_stop_count = current_budget
14377 .gradient_stop_count
14378 .saturating_add(gradient_stop_count);
14379 }
14380
14381 if run_start < end {
14382 current.push(SegmentBatchPlan::Shape {
14383 start: run_start,
14384 end,
14385 blend_mode,
14386 });
14387 }
14388 }
14389
14390 push_native_segment_fusion_partition(&mut partitions, &mut current, &mut current_budget);
14391 Ok(Some(partitions))
14392}
14393
14394fn segment_batch_plan_at_cursor(
14395 ordered_items: &[(usize, SegmentDrawItem)],
14396 shapes: &[DrawShape],
14397 images: &[ImageDraw],
14398 start: usize,
14399 batch_limits: ShapeBatchLimits,
14400) -> Option<(SegmentBatchPlan, usize)> {
14401 match ordered_items[start].1 {
14402 SegmentDrawItem::Shape(index) => {
14403 let blend_mode = supported_blend_mode(shapes[index].blend_mode);
14404 let mut end = start + 1;
14405 let shape_limit = (start + batch_limits.max_shapes_per_batch).min(ordered_items.len());
14406 while end < shape_limit {
14407 match ordered_items[end].1 {
14408 SegmentDrawItem::Shape(next_index)
14409 if supported_blend_mode(shapes[next_index].blend_mode) == blend_mode =>
14410 {
14411 end += 1;
14412 }
14413 _ => break,
14414 }
14415 }
14416 Some((
14417 SegmentBatchPlan::Shape {
14418 start,
14419 end,
14420 blend_mode,
14421 },
14422 end,
14423 ))
14424 }
14425 SegmentDrawItem::Image(index) => {
14426 let blend_mode = supported_blend_mode(images[index].blend_mode);
14427 let mut end = start + 1;
14428 while end < ordered_items.len() {
14429 match ordered_items[end].1 {
14430 SegmentDrawItem::Image(next_index)
14431 if supported_blend_mode(images[next_index].blend_mode) == blend_mode =>
14432 {
14433 end += 1;
14434 }
14435 _ => break,
14436 }
14437 }
14438 Some((
14439 SegmentBatchPlan::Image {
14440 start,
14441 end,
14442 blend_mode,
14443 },
14444 end,
14445 ))
14446 }
14447 SegmentDrawItem::Text(_) => {
14448 let mut end = start + 1;
14449 while end < ordered_items.len() {
14450 if matches!(ordered_items[end].1, SegmentDrawItem::Text(_)) {
14451 end += 1;
14452 } else {
14453 break;
14454 }
14455 }
14456 Some((SegmentBatchPlan::Text { start, end }, end))
14457 }
14458 SegmentDrawItem::Composite(_) => {
14459 let mut end = start + 1;
14460 while end < ordered_items.len() {
14461 if matches!(ordered_items[end].1, SegmentDrawItem::Composite(_)) {
14462 end += 1;
14463 } else {
14464 break;
14465 }
14466 }
14467 Some((SegmentBatchPlan::Composite { start, end }, end))
14468 }
14469 SegmentDrawItem::ShaderComposite(_) => {
14470 let mut end = start + 1;
14471 while end < ordered_items.len() {
14472 if matches!(ordered_items[end].1, SegmentDrawItem::ShaderComposite(_)) {
14473 end += 1;
14474 } else {
14475 break;
14476 }
14477 }
14478 Some((SegmentBatchPlan::ShaderComposite { start, end }, end))
14479 }
14480 SegmentDrawItem::Retained(_) => {
14481 let mut end = start + 1;
14482 while end < ordered_items.len() {
14483 if matches!(ordered_items[end].1, SegmentDrawItem::Retained(_)) {
14484 end += 1;
14485 } else {
14486 break;
14487 }
14488 }
14489 Some((SegmentBatchPlan::Retained { start, end }, end))
14490 }
14491 SegmentDrawItem::Shadow(_) => None,
14492 }
14493}
14494
14495#[allow(clippy::too_many_arguments)]
14496fn collect_non_effect_segment_items(
14497 shapes: &[DrawShape],
14498 _images: &[ImageDraw],
14499 _texts: &[TextDraw],
14500 _shadow_draws: &[ShadowDraw],
14501 draw_ops: &[DrawOp],
14502 z_start: usize,
14503 z_end: usize,
14504 effect_z_ranges: &[Range<usize>],
14505 width: u32,
14506 height: u32,
14507 root_scale: f32,
14508 scratch: &mut Vec<(usize, SegmentDrawItem)>,
14509) {
14510 scratch.clear();
14511 let viewport = ViewportUniformParams {
14512 width,
14513 height,
14514 offset: [0.0, 0.0],
14515 };
14516
14517 scratch.extend(draw_ops.iter().filter_map(|op| {
14518 if op.z_index < z_start
14519 || op.z_index >= z_end
14520 || is_in_effect_range(op.z_index, effect_z_ranges)
14521 {
14522 return None;
14523 }
14524 let item = match op.kind {
14525 DrawOpKind::Shape(index) => {
14526 let shape = shapes.get(index)?;
14527 if !shape_draw_is_visible_in_viewport(shape, viewport, root_scale) {
14528 return None;
14529 }
14530 SegmentDrawItem::Shape(index)
14531 }
14532 DrawOpKind::Image(index) => SegmentDrawItem::Image(index),
14533 DrawOpKind::Text(index) => SegmentDrawItem::Text(index),
14534 DrawOpKind::Shadow(index) => SegmentDrawItem::Shadow(index),
14535 DrawOpKind::Retained(index) => SegmentDrawItem::Retained(index),
14536 };
14537 Some((op.z_index, item))
14538 }));
14539}
14540
14541fn retain_renderable_shadow_items(
14542 ordered_items: &mut Vec<(usize, SegmentDrawItem)>,
14543 shadow_draws: &[ShadowDraw],
14544 width: u32,
14545 height: u32,
14546 root_scale: f32,
14547 max_texture_dim: u32,
14548) -> usize {
14549 let original_len = ordered_items.len();
14550 ordered_items.retain(|(_, item)| match item {
14551 SegmentDrawItem::Shadow(index) => shadow_draws.get(*index).is_some_and(|shadow| {
14552 shadow_draw_may_render(shadow, width, height, root_scale, max_texture_dim)
14553 }),
14554 _ => true,
14555 });
14556 original_len.saturating_sub(ordered_items.len())
14557}
14558
14559#[cfg(not(target_arch = "wasm32"))]
14560#[derive(Clone, Copy)]
14561struct SegmentDiagCounts {
14562 raw_shadow_items: usize,
14563 culled_shadow_items: usize,
14564 cached_shadow_composites: usize,
14565 composite_items: usize,
14566 shader_composite_items: usize,
14567}
14568
14569#[cfg(not(target_arch = "wasm32"))]
14570fn maybe_print_segment_diag(
14571 z_range: Range<usize>,
14572 ordered_items: &[(usize, SegmentDrawItem)],
14573 shapes: &[DrawShape],
14574 brushes: &[Brush],
14575 images: &[ImageDraw],
14576 counts: SegmentDiagCounts,
14577 batch_limits: ShapeBatchLimits,
14578) {
14579 if !cranpose_core::env_flag!("CRANPOSE_SEGMENT_DIAG") {
14580 return;
14581 }
14582 let line = SEGMENT_DIAG_LINES.fetch_add(1, Ordering::Relaxed);
14583 if line >= 64 {
14584 return;
14585 }
14586
14587 let remaining_shadow_items = ordered_items
14588 .iter()
14589 .filter(|(_, item)| matches!(item, SegmentDrawItem::Shadow(_)))
14590 .count();
14591 let commands: Vec<_> =
14592 SegmentCommandIter::new(ordered_items, shapes, images, batch_limits).collect();
14593 let draw_chunks = commands
14594 .iter()
14595 .filter(|command| matches!(command, SegmentRenderCommand::DrawChunk(_)))
14596 .count();
14597 let shadow_commands = commands
14598 .iter()
14599 .filter(|command| matches!(command, SegmentRenderCommand::Shadow(_)))
14600 .count();
14601 let mut native_partitions = 0usize;
14602 let mut native_unfused_chunks = 0usize;
14603 for command in &commands {
14604 let SegmentRenderCommand::DrawChunk(chunk) = command else {
14605 continue;
14606 };
14607 match native_segment_fusion_partitions(ordered_items, shapes, brushes, chunk, batch_limits)
14608 {
14609 Ok(Some(partitions)) => native_partitions += partitions.len(),
14610 Ok(None) | Err(_) => native_unfused_chunks += 1,
14611 }
14612 }
14613
14614 eprintln!(
14615 "[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={}",
14616 z_range.start,
14617 z_range.end,
14618 ordered_items.len(),
14619 counts.raw_shadow_items,
14620 counts.culled_shadow_items,
14621 counts.cached_shadow_composites,
14622 remaining_shadow_items,
14623 counts.composite_items,
14624 counts.shader_composite_items,
14625 draw_chunks,
14626 shadow_commands,
14627 native_partitions,
14628 native_unfused_chunks,
14629 );
14630}
14631
14632pub(crate) fn has_backdrop_layer_in_range(
14633 backdrop_layers: &[BackdropLayer],
14634 z_start: usize,
14635 z_end: usize,
14636) -> bool {
14637 backdrop_layers
14638 .iter()
14639 .any(|layer| layer.z_index >= z_start && layer.z_index < z_end)
14640}
14641
14642pub(crate) fn scissor_rect_for_rect(
14643 rect: Rect,
14644 root_scale: f32,
14645 width: u32,
14646 height: u32,
14647) -> Option<(u32, u32, u32, u32)> {
14648 let mut left = canonicalize_device_coordinate(rect.x * root_scale);
14649 let mut top = canonicalize_device_coordinate(rect.y * root_scale);
14650 let mut right = canonicalize_device_coordinate((rect.x + rect.width) * root_scale);
14651 let mut bottom = canonicalize_device_coordinate((rect.y + rect.height) * root_scale);
14652
14653 left = left.max(0.0).min(width as f32).floor();
14654 top = top.max(0.0).min(height as f32).floor();
14655 right = right.max(0.0).min(width as f32).ceil();
14656 bottom = bottom.max(0.0).min(height as f32).ceil();
14657
14658 if right <= left || bottom <= top {
14659 return None;
14660 }
14661
14662 Some((
14663 left as u32,
14664 top as u32,
14665 (right - left) as u32,
14666 (bottom - top) as u32,
14667 ))
14668}
14669
14670fn scissor_rect_for_layer(
14671 rect: Rect,
14672 clip: Option<Rect>,
14673 root_scale: f32,
14674 width: u32,
14675 height: u32,
14676) -> Option<(u32, u32, u32, u32)> {
14677 let clipped_rect = match clip {
14678 Some(clip_rect) => rect.intersect(clip_rect)?,
14679 None => rect,
14680 };
14681
14682 scissor_rect_for_rect(clipped_rect, root_scale, width, height)
14683}
14684
14685fn tint_for_image(
14686 color_filter: Option<ColorFilter>,
14687 alpha: f32,
14688) -> ([f32; 4], Option<ColorFilter>) {
14689 let alpha = alpha.clamp(0.0, 1.0);
14690 match color_filter {
14691 Some(filter) if filter.supports_gpu_vertex_modulation() => {
14692 let Some(tint) = filter.gpu_vertex_tint() else {
14693 return ([1.0, 1.0, 1.0, alpha], Some(filter));
14694 };
14695 (
14696 [
14697 tint[0].clamp(0.0, 1.0),
14698 tint[1].clamp(0.0, 1.0),
14699 tint[2].clamp(0.0, 1.0),
14700 (tint[3] * alpha).clamp(0.0, 1.0),
14701 ],
14702 None,
14703 )
14704 }
14705 Some(filter) => ([1.0, 1.0, 1.0, alpha], Some(filter)),
14706 None => ([1.0, 1.0, 1.0, alpha], None),
14707 }
14708}
14709
14710fn image_uv_rect(image: &ImageBitmap, src_rect: Option<Rect>) -> Option<ImageUvRect> {
14711 let Some(src) = src_rect else {
14712 return Some(ImageUvRect {
14713 min: [0.0, 0.0],
14714 max: [1.0, 1.0],
14715 sample_bounds: [0.0, 0.0, 1.0, 1.0],
14716 });
14717 };
14718
14719 let (u_min, u_max, u_bound_min, u_bound_max) =
14720 source_axis_uv(src.x, src.width, image.width() as f32)?;
14721 let (v_min, v_max, v_bound_min, v_bound_max) =
14722 source_axis_uv(src.y, src.height, image.height() as f32)?;
14723
14724 Some(ImageUvRect {
14725 min: [u_min, v_min],
14726 max: [u_max, v_max],
14727 sample_bounds: [u_bound_min, v_bound_min, u_bound_max, v_bound_max],
14728 })
14729}
14730
14731fn glyph_atlas_uv_rect(entry: GlyphAtlasEntry, atlas_size: u32) -> ImageUvRect {
14736 let atlas_width = atlas_size as f32;
14737 let atlas_height = atlas_size as f32;
14738 let min = [entry.x as f32 / atlas_width, entry.y as f32 / atlas_height];
14739 let max = [
14740 (entry.x + entry.width) as f32 / atlas_width,
14741 (entry.y + entry.height) as f32 / atlas_height,
14742 ];
14743 let center_min = [
14744 (entry.x as f32 + 0.5) / atlas_width,
14745 (entry.y as f32 + 0.5) / atlas_height,
14746 ];
14747 let center_max = [
14748 (entry.x as f32 + entry.width as f32 - 0.5).max(entry.x as f32 + 0.5) / atlas_width,
14749 (entry.y as f32 + entry.height as f32 - 0.5).max(entry.y as f32 + 0.5) / atlas_height,
14750 ];
14751 ImageUvRect {
14752 min,
14753 max,
14754 sample_bounds: [center_min[0], center_min[1], center_max[0], center_max[1]],
14755 }
14756}
14757
14758fn snap_nearest_image_to_device_pixels(image: &mut ImageDraw, root_scale: f32) {
14759 if image.sampling != ImageSampling::Nearest || !root_scale.is_finite() || root_scale <= 0.0 {
14760 return;
14761 }
14762
14763 let Some(rect) = axis_aligned_quad_rect(image.quad) else {
14764 return;
14765 };
14766
14767 let left_px = (rect.x * root_scale).round();
14768 let top_px = (rect.y * root_scale).round();
14769 let width_px = (rect.width * root_scale).round().max(1.0);
14770 let height_px = (rect.height * root_scale).round().max(1.0);
14771 let snapped = Rect {
14772 x: left_px / root_scale,
14773 y: top_px / root_scale,
14774 width: width_px / root_scale,
14775 height: height_px / root_scale,
14776 };
14777
14778 image.rect = snapped;
14779 image.local_rect = Rect {
14780 x: image.local_rect.x + snapped.x - rect.x,
14781 y: image.local_rect.y + snapped.y - rect.y,
14782 width: snapped.width,
14783 height: snapped.height,
14784 };
14785 image.quad = crate::rect_to_quad(snapped);
14786}
14787
14788fn nearest_image_device_quad(image: &ImageDraw, root_scale: f32) -> Option<[[f32; 2]; 4]> {
14789 if image.sampling != ImageSampling::Nearest || !root_scale.is_finite() || root_scale <= 0.0 {
14790 return None;
14791 }
14792
14793 let rect = axis_aligned_quad_rect(image.quad)?;
14794 let left_px = (rect.x * root_scale).round();
14795 let top_px = (rect.y * root_scale).round();
14796 let width_px = (rect.width * root_scale).round().max(1.0);
14797 let height_px = (rect.height * root_scale).round().max(1.0);
14798 let right_px = left_px + width_px;
14799 let bottom_px = top_px + height_px;
14800 Some([
14801 [left_px, top_px],
14802 [right_px, top_px],
14803 [left_px, bottom_px],
14804 [right_px, bottom_px],
14805 ])
14806}
14807
14808fn source_axis_uv(start: f32, extent: f32, image_extent: f32) -> Option<(f32, f32, f32, f32)> {
14809 if !start.is_finite()
14810 || !extent.is_finite()
14811 || !image_extent.is_finite()
14812 || extent == 0.0
14813 || image_extent <= 0.0
14814 {
14815 return None;
14816 }
14817
14818 let end = start + extent;
14819 let edge_min = start.min(end).clamp(0.0, image_extent);
14820 let edge_max = start.max(end).clamp(0.0, image_extent);
14821 if edge_max <= edge_min {
14822 return None;
14823 }
14824
14825 let center_min = edge_min + 0.5;
14826 let center_max = edge_max - 0.5;
14827 let (bound_min, bound_max) = if center_min <= center_max {
14828 (center_min, center_max)
14829 } else {
14830 let center = (edge_min + edge_max) * 0.5;
14831 (center, center)
14832 };
14833
14834 Some((
14835 edge_min / image_extent,
14836 edge_max / image_extent,
14837 bound_min / image_extent,
14838 bound_max / image_extent,
14839 ))
14840}
14841
14842fn apply_filter_to_bitmap(image: &ImageBitmap, filter: ColorFilter) -> Result<ImageBitmap, String> {
14843 let mut filtered = Vec::with_capacity(image.pixels().len());
14844 for pixel in image.pixels().as_chunks::<4>().0 {
14845 let rgba = [
14846 pixel[0] as f32 / 255.0,
14847 pixel[1] as f32 / 255.0,
14848 pixel[2] as f32 / 255.0,
14849 pixel[3] as f32 / 255.0,
14850 ];
14851 let out = filter.apply_rgba(rgba);
14852 filtered.push((out[0].clamp(0.0, 1.0) * 255.0).round() as u8);
14853 filtered.push((out[1].clamp(0.0, 1.0) * 255.0).round() as u8);
14854 filtered.push((out[2].clamp(0.0, 1.0) * 255.0).round() as u8);
14855 filtered.push((out[3].clamp(0.0, 1.0) * 255.0).round() as u8);
14856 }
14857 ImageBitmap::from_rgba8(image.width(), image.height(), filtered)
14858 .map_err(|error| format!("failed to build filtered bitmap: {error}"))
14859}
14860
14861fn scissor_rect_for_image(
14862 image: &ImageDraw,
14863 root_scale: f32,
14864 width: u32,
14865 height: u32,
14866) -> Option<(u32, u32, u32, u32)> {
14867 scissor_rect_for_layer(image.rect, image.clip, root_scale, width, height)
14868}
14869
14870fn inner_shadow_composite_mask(
14871 shadow: &ShadowDraw,
14872 root_scale: f32,
14873) -> Option<RoundedCompositeMask> {
14874 if !shadow
14875 .shapes
14876 .iter()
14877 .any(|(_, mode)| *mode == BlendMode::DstOut)
14878 {
14879 return None;
14880 }
14881 let (fill, _) = shadow.shapes.first()?;
14882 let rect = fill.local_rect;
14883 if rect.width <= 0.0 || rect.height <= 0.0 {
14884 return None;
14885 }
14886
14887 let radii = fill.shape.map_or([0.0; 4], |rounded| {
14888 let resolved = rounded.resolve(rect.width, rect.height);
14889 [
14890 resolved.top_left * root_scale,
14891 resolved.top_right * root_scale,
14892 resolved.bottom_left * root_scale,
14893 resolved.bottom_right * root_scale,
14894 ]
14895 });
14896
14897 Some(RoundedCompositeMask {
14898 rect: [
14899 rect.x * root_scale,
14900 rect.y * root_scale,
14901 rect.width * root_scale,
14902 rect.height * root_scale,
14903 ],
14904 radii,
14905 })
14906}
14907
14908#[cfg(test)]
14909mod tests {
14910 use super::*;
14911 use crate::normalized_scene::visible_draw_rect;
14912 use cranpose_foundation::lazy::{remember_lazy_list_state, LazyListScope, LazyListState};
14913 use cranpose_render_common::graph::{DrawPrimitiveNode, IsolationReasons, TextPrimitiveNode};
14914 use cranpose_render_common::raster_cache::LayerRasterCacheHashes;
14915 use cranpose_render_common::scene_builder::build_graph_from_applier;
14916 use cranpose_ui::text::{
14917 AnnotatedString, BaselineShift, RangeStyle, Shadow, SpanStyle, TextDecoration,
14918 TextDrawStyle, TextGeometricTransform, TextMotion, TextUnit,
14919 };
14920 use cranpose_ui::{
14921 LayoutEngine, LazyColumn, LazyColumnSpec, Modifier, Size, Text, TextLayoutOptions,
14922 TextStyle,
14923 };
14924 use cranpose_ui_graphics::{
14925 Brush, Color, CornerRadii, DrawPrimitive, Rect, RenderEffect, RoundedCornerShape,
14926 RuntimeShader,
14927 };
14928
14929 fn chunk(batches: &[SegmentBatchPlan]) -> SegmentDrawChunkPlan {
14930 let mut chunk = SegmentDrawChunkPlan::default();
14931 for batch in batches {
14932 chunk.push(*batch);
14933 }
14934 chunk
14935 }
14936
14937 fn with_test_app_context<R>(block: impl FnOnce() -> R) -> R {
14938 let app_context = cranpose_ui::AppContext::new();
14939 app_context.enter(block)
14940 }
14941
14942 fn assert_snap_anchor_close(actual: Option<SnapAnchor>, expected_origin: Point, message: &str) {
14943 let Some(actual) = actual else {
14944 panic!("{message}: missing snap anchor");
14945 };
14946 let expected = SnapAnchor::rigid(expected_origin);
14947 assert_eq!(
14948 actual.device_pixel_step, expected.device_pixel_step,
14949 "{message}: device pixel step changed"
14950 );
14951 assert!(
14952 (actual.origin.x - expected.origin.x).abs() <= 1e-4
14953 && (actual.origin.y - expected.origin.y).abs() <= 1e-4,
14954 "{message}: expected origin {:?}, got {:?}",
14955 expected.origin,
14956 actual.origin
14957 );
14958 }
14959
14960 fn effect_layer(z_start: usize, z_end: usize) -> EffectLayer {
14961 EffectLayer {
14962 rect: Rect {
14963 x: 0.0,
14964 y: 0.0,
14965 width: 10.0,
14966 height: 10.0,
14967 },
14968 clip: None,
14969 snap_anchor: None,
14970 effect: Some(RenderEffect::blur(4.0)),
14971 blend_mode: BlendMode::SrcOver,
14972 composite_alpha: 1.0,
14973 z_start,
14974 z_end,
14975 requirements: SurfaceRequirementSet::default().with(SurfaceRequirement::RenderEffect),
14976 }
14977 }
14978
14979 #[test]
14980 fn direct_shader_composite_accepts_box4_when_viewport_preserves_source_pixels() {
14981 assert_eq!(
14982 direct_shader_composite_viewport(
14983 1.0,
14984 BlendMode::SrcOver,
14985 Some((12.0, 18.0, 64.0, 32.0)),
14986 CompositeSampleMode::Box4,
14987 (64, 32),
14988 ),
14989 Some((12.0, 18.0, 64.0, 32.0))
14990 );
14991 }
14992
14993 #[test]
14994 fn direct_shader_composite_rejects_box4_when_viewport_resamples_source() {
14995 assert_eq!(
14996 direct_shader_composite_viewport(
14997 1.0,
14998 BlendMode::SrcOver,
14999 Some((12.0, 18.0, 64.5, 32.0)),
15000 CompositeSampleMode::Box4,
15001 (64, 32),
15002 ),
15003 None
15004 );
15005 assert_eq!(
15006 direct_shader_composite_viewport(
15007 1.0,
15008 BlendMode::SrcOver,
15009 Some((12.25, 18.0, 64.0, 32.0)),
15010 CompositeSampleMode::Box4,
15011 (64, 32),
15012 ),
15013 None
15014 );
15015 }
15016
15017 fn test_text_draw(rect: Rect, text_motion: TextMotion) -> TextDraw {
15018 let mut text_style = TextStyle::default();
15019 text_style.paragraph_style.text_motion = Some(text_motion);
15020 TextDraw {
15021 node_id: 42,
15022 rect,
15023 snap_anchor: None,
15024 translated_content_context: false,
15025 text: Arc::new(AnnotatedString::new("stable markdown row".to_string()).render_string()),
15026 color: Color::WHITE,
15027 text_style,
15028 font_size: 14.0,
15029 scale: 1.0,
15030 layout_options: TextLayoutOptions::default(),
15031 z_index: 0,
15032 clip: None,
15033 }
15034 }
15035
15036 #[test]
15037 fn static_text_image_cache_key_ignores_absolute_scroll_position() {
15038 let base = test_text_draw(
15039 Rect {
15040 x: 12.25,
15041 y: 40.75,
15042 width: 220.0,
15043 height: 24.0,
15044 },
15045 TextMotion::Static,
15046 );
15047 let scrolled = test_text_draw(
15048 Rect {
15049 x: 12.75,
15050 y: -318.5,
15051 width: 220.0,
15052 height: 24.0,
15053 },
15054 TextMotion::Static,
15055 );
15056
15057 let base_key = GpuRenderer::text_image_cache_key(&base, base.rect, 1.0, true);
15058 let scrolled_key = GpuRenderer::text_image_cache_key(&scrolled, scrolled.rect, 1.0, true);
15059
15060 assert_eq!(
15061 base_key, scrolled_key,
15062 "scrolling static text must reuse the same raster cache entry"
15063 );
15064 }
15065
15066 #[test]
15067 fn static_text_glyph_run_cache_key_ignores_absolute_scroll_position() {
15068 let base = test_text_draw(
15069 Rect {
15070 x: 12.25,
15071 y: 40.75,
15072 width: 220.0,
15073 height: 24.0,
15074 },
15075 TextMotion::Static,
15076 );
15077 let scrolled = test_text_draw(
15078 Rect {
15079 x: 12.75,
15080 y: -318.5,
15081 width: 220.0,
15082 height: 24.0,
15083 },
15084 TextMotion::Static,
15085 );
15086
15087 let base_key = GpuRenderer::text_glyph_run_cache_key(&base, base.rect, 1.0, true);
15088 let scrolled_key =
15089 GpuRenderer::text_glyph_run_cache_key(&scrolled, scrolled.rect, 1.0, true);
15090
15091 assert_eq!(
15092 base_key, scrolled_key,
15093 "scrolling static text must reuse the same retained glyph run"
15094 );
15095 }
15096
15097 #[test]
15098 fn static_multiline_text_glyph_source_keeps_full_text_when_image_source_slices() {
15099 let rect = Rect {
15100 x: 8.0,
15101 y: 100.0,
15102 width: 240.0,
15103 height: 1_000.0,
15104 };
15105 let mut draw = test_text_draw(rect, TextMotion::Static);
15106 let lines = (0..100)
15107 .map(|line| format!("line-{line:03}"))
15108 .collect::<Vec<_>>()
15109 .join("\n");
15110 draw.text = Arc::new(AnnotatedString::from(lines).render_string());
15111
15112 let raster_rect = Rect {
15113 x: 16.0,
15114 y: 200.0,
15115 width: 480.0,
15116 height: 2_000.0,
15117 };
15118 let clipped = clipped_text_raster_source(
15119 &draw,
15120 rect,
15121 raster_rect,
15122 Some(Rect {
15123 x: 0.0,
15124 y: 610.0,
15125 width: 800.0,
15126 height: 40.0,
15127 }),
15128 2.0,
15129 true,
15130 );
15131 let glyph = text_glyph_raster_source(&draw, raster_rect);
15132
15133 assert!(
15134 matches!(clipped.draw, Cow::Owned(_)),
15135 "the image source should still slice large clipped multiline text"
15136 );
15137 assert!(
15138 matches!(glyph.draw, Cow::Borrowed(_)),
15139 "the glyph source must keep a stable full-text run key while scrolling"
15140 );
15141
15142 let clipped_key = GpuRenderer::text_glyph_run_cache_key(
15143 clipped.draw.as_ref(),
15144 clipped.raster_rect,
15145 2.0,
15146 true,
15147 );
15148 let glyph_key = GpuRenderer::text_glyph_run_cache_key(
15149 glyph.draw.as_ref(),
15150 glyph.raster_rect,
15151 2.0,
15152 true,
15153 );
15154
15155 assert_ne!(
15156 clipped_key, glyph_key,
15157 "image slicing must not force glyph rendering onto per-scroll line-window cache keys"
15158 );
15159 }
15160
15161 #[cfg(not(target_arch = "wasm32"))]
15162 #[test]
15163 fn retained_glyph_viewport_offsets_relative_vertices_by_source_origin() {
15164 let viewport = ViewportUniformParams {
15165 width: 800,
15166 height: 600,
15167 offset: [10.0, 20.0],
15168 };
15169 let source = Rect {
15170 x: 40.0,
15171 y: 90.0,
15172 width: 120.0,
15173 height: 48.0,
15174 };
15175
15176 let retained = GpuRenderer::retained_glyph_viewport(viewport, source);
15177
15178 assert_eq!(retained.width, viewport.width);
15179 assert_eq!(retained.height, viewport.height);
15180 assert_eq!(retained.offset, [-30.0, -70.0]);
15181 }
15182
15183 #[cfg(not(target_arch = "wasm32"))]
15184 #[test]
15185 fn tiny_text_glyph_runs_stay_in_shared_uploads() {
15186 assert!(
15187 !should_use_retained_text_glyph_run(8, None),
15188 "tiny labels must stay in the shared fused batch"
15189 );
15190 }
15191
15192 #[cfg(not(target_arch = "wasm32"))]
15193 #[test]
15194 fn line_sized_text_glyph_runs_stay_in_shared_uploads() {
15195 assert!(
15196 !should_use_retained_text_glyph_run(64, None),
15197 "Markdown scroll frames contain many line-sized text runs; retaining each one creates per-run buffer binds instead of one shared glyph batch"
15198 );
15199 }
15200
15201 #[cfg(not(target_arch = "wasm32"))]
15202 #[test]
15203 fn large_clipped_text_glyph_runs_stay_in_shared_uploads() {
15204 assert!(
15205 !should_use_retained_text_glyph_run(
15206 MIN_RETAINED_TEXT_GLYPH_QUADS.saturating_mul(2),
15207 Some(Rect {
15208 x: 0.0,
15209 y: 0.0,
15210 width: 200.0,
15211 height: 100.0,
15212 }),
15213 ),
15214 "clipped lazy-list text must not draw a full retained run outside the viewport"
15215 );
15216 }
15217
15218 #[test]
15219 fn normal_text_glyph_draw_skips_offscreen_prewarm_candidates() {
15220 assert_eq!(
15221 text_glyph_draw_action(false, true, false),
15222 TextGlyphDrawAction::Skip,
15223 "normal draw traversal must not prepare offscreen text"
15224 );
15225 }
15226
15227 #[test]
15228 fn bounded_text_glyph_prewarm_admits_offscreen_candidates() {
15229 assert_eq!(
15230 text_glyph_draw_action(false, true, true),
15231 TextGlyphDrawAction::PrewarmOffscreen,
15232 "only the bounded prewarm path may prepare offscreen text"
15233 );
15234 }
15235
15236 #[test]
15237 fn visible_text_glyph_draws_are_always_admitted() {
15238 assert_eq!(
15239 text_glyph_draw_action(true, false, false),
15240 TextGlyphDrawAction::DrawVisible
15241 );
15242 assert_eq!(
15243 text_glyph_draw_action(true, true, true),
15244 TextGlyphDrawAction::DrawVisible
15245 );
15246 }
15247
15248 #[cfg(not(target_arch = "wasm32"))]
15249 #[test]
15250 fn offscreen_text_prewarm_skips_large_uncached_text_runs() {
15251 assert!(
15252 !offscreen_text_glyph_prewarm_work_is_bounded(
15253 None,
15254 MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS + 1,
15255 ),
15256 "offscreen prewarm must not collect large uncached text runs in an input frame"
15257 );
15258 }
15259
15260 #[cfg(not(target_arch = "wasm32"))]
15261 #[test]
15262 fn offscreen_text_prewarm_admits_small_uncached_text_runs() {
15263 assert!(
15264 offscreen_text_glyph_prewarm_work_is_bounded(
15265 None,
15266 MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS,
15267 ),
15268 "small labels can be warmed without risking a frame-budget spike"
15269 );
15270 }
15271
15272 #[cfg(not(target_arch = "wasm32"))]
15273 #[test]
15274 fn offscreen_text_prewarm_skips_large_cached_runs_without_quads() {
15275 assert!(
15276 !offscreen_text_glyph_prewarm_work_is_bounded(
15277 Some(MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CACHED_GLYPHS + 1),
15278 0,
15279 ),
15280 "cached glyph placements can still be too large to prepare during input frames"
15281 );
15282 }
15283
15284 #[cfg(not(target_arch = "wasm32"))]
15285 #[test]
15286 fn offscreen_text_prewarm_stops_after_candidate_budget() {
15287 assert!(
15288 offscreen_text_glyph_prewarm_budget_exhausted(
15289 Instant::now(),
15290 MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CANDIDATES,
15291 ),
15292 "prewarm must be bounded by candidate count even when each candidate is cheap"
15293 );
15294 }
15295
15296 #[test]
15297 fn clipped_cached_glyph_quads_are_filtered_to_viewport() {
15298 fn quad(y: i32) -> CachedTextGlyphQuad {
15299 CachedTextGlyphQuad {
15300 x: 8,
15301 y,
15302 width: 20,
15303 height: 10,
15304 color: (1.0, 1.0, 1.0, 1.0),
15305 uv: ImageUvRect {
15306 min: [0.0, 0.0],
15307 max: [1.0, 1.0],
15308 sample_bounds: [0.0, 0.0, 1.0, 1.0],
15309 },
15310 }
15311 }
15312
15313 let source = Rect {
15314 x: 0.0,
15315 y: 0.0,
15316 width: 320.0,
15317 height: 400.0,
15318 };
15319 let clip = Some(Rect {
15320 x: 0.0,
15321 y: 0.0,
15322 width: 320.0,
15323 height: 80.0,
15324 });
15325 let viewport = ViewportUniformParams {
15326 width: 320,
15327 height: 80,
15328 offset: [0.0, 0.0],
15329 };
15330
15331 assert!(cached_text_glyph_quad_is_visible_in_viewport(
15332 source,
15333 &quad(40),
15334 clip,
15335 viewport,
15336 1.0,
15337 ));
15338 assert!(
15339 !cached_text_glyph_quad_is_visible_in_viewport(source, &quad(140), clip, viewport, 1.0,),
15340 "glyphs outside the effective clip should not enter the frame command stream"
15341 );
15342 }
15343
15344 #[test]
15345 fn small_scene_range_cache_miss_observes_first_render() {
15346 let key = LayerRasterCacheKey::scene_range(
15347 0xCACE,
15348 Rect {
15349 x: 0.0,
15350 y: 0.0,
15351 width: 120.0,
15352 height: 80.0,
15353 },
15354 (120, 80),
15355 ScaleBucket::from_scale(1.0),
15356 );
15357
15358 assert!(
15359 !first_cache_miss_admission(&key),
15360 "a small scene-range miss should render directly first instead of materializing a tiny one-frame retained target"
15361 );
15362 assert!(
15363 repeated_cache_miss_admission(&key),
15364 "a repeated small scene-range miss is stable enough to materialize into the retained cache"
15365 );
15366 }
15367
15368 #[test]
15369 fn large_scene_range_cache_miss_requires_repeated_stable_key() {
15370 let key = LayerRasterCacheKey::scene_range(
15371 0xCACE,
15372 Rect {
15373 x: 0.0,
15374 y: 0.0,
15375 width: 1200.0,
15376 height: 900.0,
15377 },
15378 (1200, 900),
15379 ScaleBucket::from_scale(1.0),
15380 );
15381
15382 assert!(
15383 !first_cache_miss_admission(&key),
15384 "a large first scene-range miss should render directly instead of materializing a multi-MB one-frame cache entry"
15385 );
15386 assert!(
15387 repeated_cache_miss_admission(&key),
15388 "a repeated scene-range miss is stable enough to materialize into the retained cache"
15389 );
15390 }
15391
15392 #[test]
15393 fn renderer_warmup_frame_is_requested_for_cache_miss_stats_only() {
15394 let stats = gpu_stats::FrameStats::default();
15395 let mut snapshot = stats.snapshot();
15396 assert!(
15397 !frame_stats_need_warmup_frame(&snapshot),
15398 "a clean frame must not keep a static scene redrawing"
15399 );
15400
15401 snapshot.layer_cache_misses = 1;
15402 assert!(frame_stats_need_warmup_frame(&snapshot));
15403 snapshot.layer_cache_misses = 0;
15404
15405 snapshot.shadow_shape_cache_misses = 1;
15406 assert!(frame_stats_need_warmup_frame(&snapshot));
15407 snapshot.shadow_shape_cache_misses = 0;
15408
15409 snapshot.text_image_cache_misses = 1;
15410 assert!(frame_stats_need_warmup_frame(&snapshot));
15411 snapshot.text_image_cache_misses = 0;
15412
15413 snapshot.text_glyph_atlas_misses = 1;
15414 assert!(frame_stats_need_warmup_frame(&snapshot));
15415 }
15416
15417 #[test]
15418 fn renderer_warmup_budget_is_consumed_by_a_repeated_cache_miss() {
15419 let stats = gpu_stats::FrameStats::default();
15420 let mut snapshot = stats.snapshot();
15421 snapshot.layer_cache_misses = 1;
15422 let mut pending_frames = 0;
15423
15424 update_frame_warmup_budget(&mut pending_frames, &snapshot);
15425 assert_eq!(pending_frames, CACHE_MISS_WARMUP_FRAMES);
15426
15427 update_frame_warmup_budget(&mut pending_frames, &snapshot);
15428 assert_eq!(
15429 pending_frames, 0,
15430 "a cache miss during the warmup frame must not replenish its budget"
15431 );
15432 }
15433
15434 #[test]
15435 fn non_scene_layer_surface_cache_miss_admits_first_render() {
15436 let key = LayerRasterCacheKey::new(
15437 Some(77),
15438 0xC0FFEE,
15439 0,
15440 Rect {
15441 x: 0.0,
15442 y: 0.0,
15443 width: 120.0,
15444 height: 80.0,
15445 },
15446 (120, 80),
15447 ScaleBucket::from_scale(1.0),
15448 );
15449
15450 assert!(
15451 first_cache_miss_admission(&key),
15452 "ordinary retained layer surfaces should still cache on first miss"
15453 );
15454 }
15455
15456 #[test]
15457 fn text_image_cache_key_is_content_addressed_not_node_addressed() {
15458 let first = test_text_draw(
15459 Rect {
15460 x: 12.25,
15461 y: 40.75,
15462 width: 220.0,
15463 height: 24.0,
15464 },
15465 TextMotion::Static,
15466 );
15467 let mut second = first.clone();
15468 second.node_id = first.node_id + 1;
15469
15470 let first_key = GpuRenderer::text_image_cache_key(&first, first.rect, 1.0, true);
15471 let second_key = GpuRenderer::text_image_cache_key(&second, second.rect, 1.0, true);
15472
15473 assert_eq!(
15474 first_key, second_key,
15475 "text raster cache keys must be based on rendered pixels, not node identity"
15476 );
15477 }
15478
15479 #[test]
15480 fn animated_text_image_cache_key_keeps_fractional_phase_only() {
15481 let base = test_text_draw(
15482 Rect {
15483 x: 12.25,
15484 y: 40.75,
15485 width: 220.0,
15486 height: 24.0,
15487 },
15488 TextMotion::Animated,
15489 );
15490 let integer_translated = test_text_draw(
15491 Rect {
15492 x: 44.25,
15493 y: 88.75,
15494 width: 220.0,
15495 height: 24.0,
15496 },
15497 TextMotion::Animated,
15498 );
15499 let phase_shifted = test_text_draw(
15500 Rect {
15501 x: 44.5,
15502 y: 88.75,
15503 width: 220.0,
15504 height: 24.0,
15505 },
15506 TextMotion::Animated,
15507 );
15508
15509 let base_key = GpuRenderer::text_image_cache_key(&base, base.rect, 1.0, false);
15510 let translated_key = GpuRenderer::text_image_cache_key(
15511 &integer_translated,
15512 integer_translated.rect,
15513 1.0,
15514 false,
15515 );
15516 let phase_shifted_key =
15517 GpuRenderer::text_image_cache_key(&phase_shifted, phase_shifted.rect, 1.0, false);
15518
15519 assert_eq!(
15520 base_key, translated_key,
15521 "integer translation should not invalidate animated text raster cache entries"
15522 );
15523 assert_ne!(
15524 base_key, phase_shifted_key,
15525 "fractional phase affects animated text rasterization and must stay in the key"
15526 );
15527 }
15528
15529 #[test]
15530 fn animated_translated_text_raster_geometry_applies_snap_anchor() {
15531 let mut base = test_text_draw(
15532 Rect {
15533 x: 14.25,
15534 y: 16.50,
15535 width: 220.0,
15536 height: 24.0,
15537 },
15538 TextMotion::Animated,
15539 );
15540 base.snap_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.50)));
15541
15542 let mut scrolled = test_text_draw(
15543 Rect {
15544 x: 14.25,
15545 y: 15.80,
15546 width: 220.0,
15547 height: 24.0,
15548 },
15549 TextMotion::Animated,
15550 );
15551 scrolled.snap_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 15.80)));
15552
15553 let (base_logical, base_raster, _, _, base_static) =
15554 text_raster_geometry_for_draw(&base, 1.0).expect("base text geometry");
15555 let (scrolled_logical, scrolled_raster, _, _, scrolled_static) =
15556 text_raster_geometry_for_draw(&scrolled, 1.0).expect("scrolled text geometry");
15557
15558 assert!(!base_static);
15559 assert!(!scrolled_static);
15560 assert!((base_logical.x - 14.0).abs() < f32::EPSILON);
15561 assert!((base_logical.y - 17.0).abs() < f32::EPSILON);
15562 assert!((scrolled_logical.x - 14.0).abs() < f32::EPSILON);
15563 assert!((scrolled_logical.y - 16.0).abs() < f32::EPSILON);
15564 assert_eq!(base_raster.x.fract(), 0.0);
15565 assert_eq!(base_raster.y.fract(), 0.0);
15566 assert_eq!(scrolled_raster.x.fract(), 0.0);
15567 assert_eq!(scrolled_raster.y.fract(), 0.0);
15568
15569 let base_key = GpuRenderer::text_image_cache_key(&base, base_raster, 1.0, false);
15570 let scrolled_key =
15571 GpuRenderer::text_image_cache_key(&scrolled, scrolled_raster, 1.0, false);
15572 assert_eq!(
15573 base_key, scrolled_key,
15574 "translated animated text should keep a stable raster phase while scrolling"
15575 );
15576 }
15577
15578 #[test]
15579 fn translated_static_text_moves_one_device_pixel_at_half_pixel_phase() {
15580 let root_scale = 1.25;
15581 let mut base = test_text_draw(
15582 Rect {
15583 x: 14.0,
15584 y: 276.0,
15585 width: 220.0,
15586 height: 24.0,
15587 },
15588 TextMotion::Static,
15589 );
15590 base.snap_anchor = Some(SnapAnchor::rigid(Point::new(0.0, 127.600_006)));
15591
15592 let mut scrolled = test_text_draw(
15593 Rect {
15594 x: 14.0,
15595 y: 275.2,
15596 width: 220.0,
15597 height: 24.0,
15598 },
15599 TextMotion::Static,
15600 );
15601 scrolled.snap_anchor = Some(SnapAnchor::rigid(Point::new(0.0, 126.799_99)));
15602
15603 let (_, base_raster, _, _, _) =
15604 text_raster_geometry_for_draw(&base, root_scale).expect("base text geometry");
15605 let (_, scrolled_raster, _, _, _) =
15606 text_raster_geometry_for_draw(&scrolled, root_scale).expect("scrolled text geometry");
15607
15608 assert_eq!(
15609 base_raster.y - scrolled_raster.y,
15610 1.0,
15611 "one physical pixel of rigid scrolling must move static text by one raster pixel"
15612 );
15613 }
15614
15615 #[test]
15616 fn translated_text_snap_does_not_move_its_fixed_ancestor_clip() {
15617 let root_scale = 1.25;
15618 let fixed_clip = Rect {
15619 x: 8.0,
15620 y: 20.0,
15621 width: 300.0,
15622 height: 680.0,
15623 };
15624 let mut draw = test_text_draw(
15625 Rect {
15626 x: 14.0,
15627 y: 276.0,
15628 width: 220.0,
15629 height: 24.0,
15630 },
15631 TextMotion::Static,
15632 );
15633 draw.snap_anchor = Some(SnapAnchor::rigid(Point::new(0.0, 127.4)));
15634 draw.clip = Some(fixed_clip);
15635
15636 let (_, _, clip, _, _) =
15637 text_raster_geometry_for_draw(&draw, root_scale).expect("clipped text geometry");
15638
15639 assert_eq!(
15640 clip,
15641 Some(fixed_clip),
15642 "content pixel snapping must not translate a fixed ancestor clip"
15643 );
15644 }
15645
15646 #[test]
15647 fn clipped_static_multiline_text_raster_source_limits_visible_line_window() {
15648 let rect = Rect {
15649 x: 8.0,
15650 y: 100.0,
15651 width: 240.0,
15652 height: 1_000.0,
15653 };
15654 let mut draw = test_text_draw(rect, TextMotion::Static);
15655 let lines = (0..100)
15656 .map(|line| format!("line-{line:03}"))
15657 .collect::<Vec<_>>()
15658 .join("\n");
15659 draw.text = Arc::new(AnnotatedString::from(lines).render_string());
15660
15661 let raster_rect = Rect {
15662 x: 16.0,
15663 y: 200.0,
15664 width: 480.0,
15665 height: 2_000.0,
15666 };
15667 let source = clipped_text_raster_source(
15668 &draw,
15669 rect,
15670 raster_rect,
15671 Some(Rect {
15672 x: 0.0,
15673 y: 610.0,
15674 width: 800.0,
15675 height: 40.0,
15676 }),
15677 2.0,
15678 true,
15679 );
15680
15681 let Cow::Owned(sliced_draw) = source.draw else {
15682 panic!("clipped static multiline text should rasterize only the visible line window");
15683 };
15684 let sliced_text = sliced_draw.text.text.as_str();
15685 assert!(sliced_text.contains("line-050"));
15686 assert!(sliced_text.contains("line-055"));
15687 assert!(!sliced_text.contains("line-000"));
15688 assert!(!sliced_text.contains("line-099"));
15689 assert_eq!(source.raster_rect.x, raster_rect.x);
15690 assert!(source.raster_rect.y > raster_rect.y);
15691 assert!(source.raster_rect.height < raster_rect.height);
15692 }
15693
15694 #[test]
15695 fn clipped_static_multiline_text_raster_source_slices_short_multiline_text() {
15696 let rect = Rect {
15697 x: 8.0,
15698 y: 100.0,
15699 width: 240.0,
15700 height: 320.0,
15701 };
15702 let mut draw = test_text_draw(rect, TextMotion::Static);
15703 let lines = (0..24)
15704 .map(|line| format!("code-line-{line:02}"))
15705 .collect::<Vec<_>>()
15706 .join("\n");
15707 draw.text = Arc::new(AnnotatedString::from(lines).render_string());
15708
15709 let raster_rect = Rect {
15710 x: 16.0,
15711 y: 200.0,
15712 width: 480.0,
15713 height: 640.0,
15714 };
15715 let source = clipped_text_raster_source(
15716 &draw,
15717 rect,
15718 raster_rect,
15719 Some(Rect {
15720 x: 0.0,
15721 y: 190.0,
15722 width: 800.0,
15723 height: 120.0,
15724 }),
15725 2.0,
15726 true,
15727 );
15728
15729 let Cow::Owned(sliced_draw) = source.draw else {
15730 panic!("clipped multiline text should rasterize only the visible line window");
15731 };
15732 assert!(sliced_draw.text.text.as_str().contains("code-line-06"));
15733 assert!(!sliced_draw.text.text.as_str().contains("code-line-00"));
15734 assert!(!sliced_draw.text.text.as_str().contains("code-line-23"));
15735 assert_eq!(source.raster_rect.x, raster_rect.x);
15736 assert!(source.raster_rect.y > raster_rect.y);
15737 assert!(source.raster_rect.height < raster_rect.height);
15738 }
15739
15740 #[test]
15741 fn text_line_index_cache_reuses_retained_index_for_same_text_instance() {
15742 let mut cache = TextLineIndexCache::new(4);
15743 let text = Arc::new(AnnotatedString::from("a\nb\nc").render_string());
15744
15745 let first = cache.line_starts(&text);
15746 let second = cache.line_starts(&text);
15747
15748 assert_eq!(first.as_ref(), &[0, 2, 4]);
15749 assert!(
15750 Rc::ptr_eq(&first, &second),
15751 "retained text should not rebuild its line index on every clipped frame"
15752 );
15753 }
15754
15755 #[test]
15756 fn text_line_index_cache_is_retained_text_instance_local() {
15757 let mut cache = TextLineIndexCache::new(4);
15758 let first_text = Arc::new(AnnotatedString::from("a\nb\nc").render_string());
15759 let second_text = Arc::new(AnnotatedString::from("a\nb\nc").render_string());
15760
15761 let first = cache.line_starts(&first_text);
15762 let second = cache.line_starts(&second_text);
15763
15764 assert_eq!(first.as_ref(), second.as_ref());
15765 assert!(
15766 !Rc::ptr_eq(&first, &second),
15767 "line index lookup should not hash large text contents to find unrelated retained nodes"
15768 );
15769 }
15770
15771 #[test]
15772 fn device_pixel_bounds_for_rect_snaps_origin_and_extents() {
15773 let bounds = device_pixel_bounds_for_rect(
15774 Rect {
15775 x: 10.25,
15776 y: 14.6,
15777 width: 20.1,
15778 height: 9.2,
15779 },
15780 200,
15781 120,
15782 2.0,
15783 )
15784 .expect("rect should intersect the viewport");
15785
15786 assert_eq!(
15787 bounds,
15788 DevicePixelBounds {
15789 x: 20.0,
15790 y: 29.0,
15791 width: 41,
15792 height: 19,
15793 }
15794 );
15795 }
15796
15797 #[test]
15798 fn visible_layer_rect_intersects_clip_and_viewport() {
15799 let visible = visible_layer_rect(
15800 Rect {
15801 x: -10.0,
15802 y: 5.0,
15803 width: 80.0,
15804 height: 40.0,
15805 },
15806 Some(Rect {
15807 x: 4.0,
15808 y: 8.0,
15809 width: 20.0,
15810 height: 50.0,
15811 }),
15812 2.0,
15813 60,
15814 40,
15815 )
15816 .expect("visible rect");
15817
15818 assert_eq!(
15819 visible,
15820 Rect {
15821 x: 4.0,
15822 y: 8.0,
15823 width: 20.0,
15824 height: 12.0,
15825 }
15826 );
15827 }
15828
15829 #[test]
15830 fn clamp_effect_surface_scale_caps_large_surfaces_but_keeps_base_scale() {
15831 let clamped = clamp_effect_surface_scale(
15832 Rect {
15833 x: 0.0,
15834 y: 0.0,
15835 width: 1200.0,
15836 height: 900.0,
15837 },
15838 1.0,
15839 8.0,
15840 16_384,
15841 );
15842
15843 assert!(
15844 clamped < 8.0,
15845 "large translated effect layers must be capped to avoid OOM, got {clamped}"
15846 );
15847 assert!(
15848 clamped >= 1.0,
15849 "effect surfaces must not fall below destination resolution, got {clamped}"
15850 );
15851 }
15852
15853 #[test]
15854 fn clamp_effect_surface_scale_keeps_decorated_text_capture_scale() {
15855 let clamped = clamp_effect_surface_scale(
15856 Rect {
15857 x: 0.0,
15858 y: 0.0,
15859 width: 446.0,
15860 height: 44.0,
15861 },
15862 1.0,
15863 9.0,
15864 16_384,
15865 );
15866
15867 assert_eq!(
15868 clamped, 9.0,
15869 "decorated text motion-stable captures must keep full scale"
15870 );
15871 }
15872
15873 fn backdrop_layer(z_index: usize) -> BackdropLayer {
15874 BackdropLayer {
15875 node_id: Some(700 + z_index),
15876 rect: Rect {
15877 x: 0.0,
15878 y: 0.0,
15879 width: 10.0,
15880 height: 10.0,
15881 },
15882 clip: None,
15883 snap_anchor: None,
15884 effect: RenderEffect::blur(2.0),
15885 z_index,
15886 }
15887 }
15888
15889 fn test_shape(z_index: usize, blend_mode: BlendMode) -> DrawShape {
15890 DrawShape {
15891 rect: Rect {
15892 x: 0.0,
15893 y: 0.0,
15894 width: 8.0,
15895 height: 8.0,
15896 },
15897 local_rect: Rect {
15898 x: 0.0,
15899 y: 0.0,
15900 width: 8.0,
15901 height: 8.0,
15902 },
15903 quad: [[0.0, 0.0], [8.0, 0.0], [0.0, 8.0], [8.0, 8.0]],
15904 snap_anchor: None,
15905 brush: SceneBrush::Solid(Color::BLACK),
15906 shape: None,
15907 stroke: None,
15908 arc: None,
15909 z_index,
15910 clip: None,
15911 blend_mode,
15912 motion_context_animated: false,
15913 }
15914 }
15915
15916 #[test]
15917 fn shape_shadow_content_hash_ignores_viewport_translation() {
15918 fn translate_shape(shape: &DrawShape, dx: f32, dy: f32) -> DrawShape {
15919 let mut translated = *shape;
15920 translated.rect.x += dx;
15921 translated.rect.y += dy;
15922 translated.local_rect.x += dx;
15923 translated.local_rect.y += dy;
15924 for point in &mut translated.quad {
15925 point[0] += dx;
15926 point[1] += dy;
15927 }
15928 translated.snap_anchor = translated.snap_anchor.map(|anchor| {
15929 SnapAnchor::rigid(Point::new(anchor.origin.x + dx, anchor.origin.y + dy))
15930 });
15931 translated.clip = translated.clip.map(|mut clip| {
15932 clip.x += dx;
15933 clip.y += dy;
15934 clip
15935 });
15936 translated
15937 }
15938
15939 let mut first = test_shape(1, BlendMode::SrcOver);
15940 first.rect = Rect {
15941 x: 10.0,
15942 y: 20.0,
15943 width: 80.0,
15944 height: 40.0,
15945 };
15946 first.local_rect = first.rect;
15947 first.quad = [[10.0, 20.0], [90.0, 20.0], [10.0, 60.0], [90.0, 60.0]];
15948 first.snap_anchor = Some(SnapAnchor::rigid(Point::new(7.0, 11.0)));
15949 first.shape = Some(RoundedCornerShape::uniform(8.0));
15950 first.clip = Some(Rect {
15951 x: 8.0,
15952 y: 18.0,
15953 width: 86.0,
15954 height: 44.0,
15955 });
15956 let mut cutout = test_shape(2, BlendMode::DstOut);
15957 cutout.rect = Rect {
15958 x: 18.0,
15959 y: 26.0,
15960 width: 62.0,
15961 height: 22.0,
15962 };
15963 cutout.local_rect = cutout.rect;
15964 cutout.quad = [[18.0, 26.0], [80.0, 26.0], [18.0, 48.0], [80.0, 48.0]];
15965 cutout.shape = Some(RoundedCornerShape::uniform(4.0));
15966
15967 let dx = 37.0;
15968 let dy = -11.5;
15969 let translated = translate_shape(&first, dx, dy);
15970 let translated_cutout = translate_shape(&cutout, dx, dy);
15971
15972 let root_scale = 1.25;
15973 let first_shapes = vec![(first, BlendMode::SrcOver), (cutout, BlendMode::DstOut)];
15974 let translated_shapes = vec![
15975 (translated, BlendMode::SrcOver),
15976 (translated_cutout, BlendMode::DstOut),
15977 ];
15978
15979 let first_hash = shape_shadow_content_hash(&first_shapes, &[], root_scale);
15980 let translated_hash = shape_shadow_content_hash(&translated_shapes, &[], root_scale);
15981
15982 assert_eq!(first_hash, translated_hash);
15983
15984 let mut changed_shapes = translated_shapes;
15985 changed_shapes[0].0.rect.width += 1.0;
15986 let changed_hash = shape_shadow_content_hash(&changed_shapes, &[], root_scale);
15987
15988 assert_ne!(first_hash, changed_hash);
15989 }
15990
15991 #[test]
15992 fn shape_shadow_content_hash_is_stable_under_fractional_scale_scroll() {
15993 fn shadow_shapes_at(y: f32) -> Vec<(DrawShape, BlendMode)> {
15999 let mut shape = test_shape(1, BlendMode::SrcOver);
16000 shape.rect = Rect {
16001 x: 24.0,
16002 y,
16003 width: 180.0,
16004 height: 90.0,
16005 };
16006 shape.local_rect = shape.rect;
16007 shape.quad = crate::rect_to_quad(shape.rect);
16008 shape.shape = Some(RoundedCornerShape::uniform(14.0));
16009 vec![(shape, BlendMode::SrcOver)]
16010 }
16011
16012 let root_scale = 130.0f32 / 96.0;
16013 let blur_radius = 18.0f32;
16014 let pixel_radius = blur_radius * root_scale;
16015
16016 let key_at = |y: f32| {
16017 let shapes = shadow_shapes_at(y);
16018 let plan =
16019 shape_shadow_surface_plan(&shapes, None, blur_radius, 1600, 1600, root_scale, 8192)
16020 .expect("surface plan");
16021 shape_shadow_surface_cache_key(
16022 &shapes,
16023 &[],
16024 plan.source_device_bounds,
16025 pixel_radius,
16026 root_scale,
16027 )
16028 .expect("cache key")
16029 };
16030
16031 let base = key_at(640.0);
16036 for step in 1..=12 {
16037 let scrolled = key_at(640.0 - step as f32 * 4.0);
16038 assert_eq!(
16039 base, scrolled,
16040 "scrolled shadow cache key must stay stable at fractional scale (step {step})"
16041 );
16042 }
16043 }
16044
16045 #[test]
16046 fn shape_shadow_cache_key_uses_unclipped_source_bounds_for_scrolled_clip() {
16047 fn translated_card_shadow(y: f32) -> Vec<(DrawShape, BlendMode)> {
16048 let mut shape = test_shape(1, BlendMode::SrcOver);
16049 shape.rect = Rect {
16050 x: 24.0,
16051 y,
16052 width: 280.0,
16053 height: 120.0,
16054 };
16055 shape.local_rect = shape.rect;
16056 shape.quad = [[24.0, y], [304.0, y], [24.0, y + 120.0], [304.0, y + 120.0]];
16057 shape.shape = Some(RoundedCornerShape::uniform(18.0));
16058 vec![(shape, BlendMode::SrcOver)]
16059 }
16060
16061 let root_scale = 1.0;
16062 let blur_radius = 18.0;
16063 let viewport_clip = Rect {
16064 x: 0.0,
16065 y: 96.0,
16066 width: 360.0,
16067 height: 720.0,
16068 };
16069 let key_for = |y: f32| {
16070 let shapes = translated_card_shadow(y);
16071 let plan = shape_shadow_surface_plan(
16072 &shapes,
16073 Some(viewport_clip),
16074 blur_radius,
16075 360,
16076 900,
16077 root_scale,
16078 4096,
16079 )
16080 .expect("surface plan");
16081 shape_shadow_surface_cache_key(
16082 &shapes,
16083 &[],
16084 plan.source_device_bounds,
16085 plan.pixel_radius,
16086 root_scale,
16087 )
16088 .expect("cache key")
16089 };
16090
16091 assert_eq!(key_for(740.0), key_for(756.0));
16094 }
16095
16096 #[test]
16097 fn shape_visibility_uses_nonzero_viewport_offset_for_cropped_offscreen() {
16098 let mut shape = test_shape(1, BlendMode::SrcOver);
16099 shape.rect = Rect {
16100 x: 24.0,
16101 y: 740.0,
16102 width: 280.0,
16103 height: 120.0,
16104 };
16105 shape.local_rect = shape.rect;
16106 shape.quad = [[24.0, 740.0], [304.0, 740.0], [24.0, 860.0], [304.0, 860.0]];
16107 let viewport = ViewportUniformParams {
16108 width: 316,
16109 height: 228,
16110 offset: [6.0, 686.0],
16111 };
16112
16113 assert!(shape_draw_is_visible_in_viewport(&shape, viewport, 1.0));
16114 }
16115
16116 #[test]
16117 fn text_prewarm_uses_nonzero_viewport_offset_for_cropped_offscreen() {
16118 let viewport = ViewportUniformParams {
16119 width: 316,
16120 height: 228,
16121 offset: [6.0, 686.0],
16122 };
16123 let text_rect = Rect {
16124 x: 24.0,
16125 y: 740.0,
16126 width: 280.0,
16127 height: 40.0,
16128 };
16129
16130 assert!(text_draw_is_visible_in_viewport(
16131 text_rect, None, viewport, 1.0
16132 ));
16133 assert!(text_draw_should_prewarm_in_viewport(
16134 text_rect, None, viewport, 1.0
16135 ));
16136 }
16137
16138 fn test_shadow_draw(shapes: Vec<(DrawShape, BlendMode)>) -> ShadowDraw {
16139 ShadowDraw {
16140 shapes,
16141 brushes: vec![],
16142 texts: vec![],
16143 blur_radius: 8.0,
16144 clip: None,
16145 z_index: 0,
16146 }
16147 }
16148
16149 fn test_image(z_index: usize, blend_mode: BlendMode) -> ImageDraw {
16150 ImageDraw {
16151 rect: Rect {
16152 x: 0.0,
16153 y: 0.0,
16154 width: 8.0,
16155 height: 8.0,
16156 },
16157 local_rect: Rect {
16158 x: 0.0,
16159 y: 0.0,
16160 width: 8.0,
16161 height: 8.0,
16162 },
16163 quad: [[0.0, 0.0], [8.0, 0.0], [0.0, 8.0], [8.0, 8.0]],
16164 snap_anchor: None,
16165 image: ImageBitmap::from_rgba8(1, 1, vec![255, 255, 255, 255]).expect("image"),
16166 alpha: 1.0,
16167 color_filter: None,
16168 sampling: ImageSampling::Nearest,
16169 z_index,
16170 clip: None,
16171 blend_mode,
16172 src_rect: None,
16173 motion_context_animated: false,
16174 }
16175 }
16176
16177 #[test]
16178 fn image_sampler_descriptors_match_requested_sampling() {
16179 let nearest = image_sampler_descriptor(ImageSampling::Nearest);
16180 assert_eq!(nearest.mag_filter, wgpu::FilterMode::Nearest);
16181 assert_eq!(nearest.min_filter, wgpu::FilterMode::Nearest);
16182
16183 let linear = image_sampler_descriptor(ImageSampling::Linear);
16184 assert_eq!(linear.mag_filter, wgpu::FilterMode::Linear);
16185 assert_eq!(linear.min_filter, wgpu::FilterMode::Linear);
16186 }
16187
16188 #[test]
16189 fn image_uv_rect_clamps_source_rect_to_texel_centers() {
16190 let image = ImageBitmap::from_rgba8(24, 16, vec![0; 24 * 16 * 4]).expect("image");
16191 let uv = image_uv_rect(
16192 &image,
16193 Some(Rect {
16194 x: 0.0,
16195 y: 0.0,
16196 width: 16.0,
16197 height: 16.0,
16198 }),
16199 )
16200 .expect("uv rect");
16201
16202 assert_eq!(uv.min, [0.0, 0.0]);
16203 assert_eq!(uv.max, [16.0 / 24.0, 1.0]);
16204 assert_eq!(
16205 uv.sample_bounds,
16206 [0.5 / 24.0, 0.5 / 16.0, 15.5 / 24.0, 15.5 / 16.0]
16207 );
16208 }
16209
16210 #[test]
16211 fn image_uv_rect_keeps_full_image_unclamped() {
16212 let image = ImageBitmap::from_rgba8(2, 2, vec![0; 16]).expect("image");
16213 let uv = image_uv_rect(&image, None).expect("uv rect");
16214
16215 assert_eq!(uv.min, [0.0, 0.0]);
16216 assert_eq!(uv.max, [1.0, 1.0]);
16217 assert_eq!(uv.sample_bounds, [0.0, 0.0, 1.0, 1.0]);
16218 }
16219
16220 fn test_text(z_index: usize) -> TextDraw {
16221 TextDraw {
16222 node_id: 0,
16223 rect: Rect {
16224 x: 0.0,
16225 y: 0.0,
16226 width: 8.0,
16227 height: 8.0,
16228 },
16229 snap_anchor: None,
16230 translated_content_context: false,
16231 text: Arc::new(cranpose_ui::text::AnnotatedString::from("t").render_string()),
16232 color: Color::WHITE,
16233 text_style: cranpose_ui::TextStyle::default(),
16234 font_size: 12.0,
16235 scale: 1.0,
16236 layout_options: cranpose_ui::TextLayoutOptions::default(),
16237 z_index,
16238 clip: None,
16239 }
16240 }
16241
16242 #[test]
16243 fn text_draw_visibility_rejects_text_outside_clip_before_rasterization() {
16244 let viewport = ViewportUniformParams {
16245 width: 320,
16246 height: 240,
16247 offset: [0.0, 0.0],
16248 };
16249 let text_rect = Rect {
16250 x: 0.0,
16251 y: 260.0,
16252 width: 200.0,
16253 height: 40.0,
16254 };
16255 let clip = Some(Rect {
16256 x: 0.0,
16257 y: 0.0,
16258 width: 320.0,
16259 height: 200.0,
16260 });
16261
16262 assert!(
16263 !text_draw_is_visible_in_viewport(text_rect, clip, viewport, 1.0),
16264 "lazy-list beyond-bound text outside the clip must not be rasterized"
16265 );
16266 }
16267
16268 #[test]
16269 fn text_draw_prewarm_accepts_clipped_text_near_viewport() {
16270 let viewport = ViewportUniformParams {
16271 width: 320,
16272 height: 240,
16273 offset: [0.0, 0.0],
16274 };
16275 let text_rect = Rect {
16276 x: 0.0,
16277 y: 260.0,
16278 width: 200.0,
16279 height: 40.0,
16280 };
16281 let clip = Some(Rect {
16282 x: 0.0,
16283 y: 0.0,
16284 width: 320.0,
16285 height: 200.0,
16286 });
16287
16288 assert!(!text_draw_is_visible_in_viewport(
16289 text_rect, clip, viewport, 1.0
16290 ));
16291 assert!(text_draw_should_prewarm_in_viewport(
16292 text_rect, clip, viewport, 1.0
16293 ));
16294 }
16295
16296 #[test]
16297 fn text_draw_prewarm_rejects_far_clipped_text() {
16298 let viewport = ViewportUniformParams {
16299 width: 320,
16300 height: 240,
16301 offset: [0.0, 0.0],
16302 };
16303 let text_rect = Rect {
16304 x: 0.0,
16305 y: 1600.0,
16306 width: 200.0,
16307 height: 40.0,
16308 };
16309 let clip = Some(Rect {
16310 x: 0.0,
16311 y: 0.0,
16312 width: 320.0,
16313 height: 200.0,
16314 });
16315
16316 assert!(!text_draw_should_prewarm_in_viewport(
16317 text_rect, clip, viewport, 1.0
16318 ));
16319 }
16320
16321 #[test]
16322 fn text_draw_visibility_rejects_unclipped_text_outside_viewport() {
16323 let viewport = ViewportUniformParams {
16324 width: 320,
16325 height: 240,
16326 offset: [0.0, 0.0],
16327 };
16328 let text_rect = Rect {
16329 x: 0.0,
16330 y: 241.0,
16331 width: 200.0,
16332 height: 40.0,
16333 };
16334
16335 assert!(
16336 !text_draw_is_visible_in_viewport(text_rect, None, viewport, 1.0),
16337 "unclipped text outside the target viewport must not be rasterized"
16338 );
16339 }
16340
16341 #[test]
16342 fn text_draw_visibility_keeps_partially_visible_text() {
16343 let viewport = ViewportUniformParams {
16344 width: 320,
16345 height: 240,
16346 offset: [0.0, 0.0],
16347 };
16348 let text_rect = Rect {
16349 x: 0.0,
16350 y: 220.0,
16351 width: 200.0,
16352 height: 40.0,
16353 };
16354
16355 assert!(text_draw_is_visible_in_viewport(
16356 text_rect, None, viewport, 1.0
16357 ));
16358 }
16359
16360 fn test_draw_ops(
16361 shapes: &[DrawShape],
16362 images: &[ImageDraw],
16363 texts: &[TextDraw],
16364 shadows: &[ShadowDraw],
16365 ) -> Vec<DrawOp> {
16366 let mut ops = Vec::new();
16367 ops.extend(shapes.iter().enumerate().map(|(index, shape)| DrawOp {
16368 z_index: shape.z_index,
16369 kind: DrawOpKind::Shape(index),
16370 }));
16371 ops.extend(images.iter().enumerate().map(|(index, image)| DrawOp {
16372 z_index: image.z_index,
16373 kind: DrawOpKind::Image(index),
16374 }));
16375 ops.extend(texts.iter().enumerate().map(|(index, text)| DrawOp {
16376 z_index: text.z_index,
16377 kind: DrawOpKind::Text(index),
16378 }));
16379 ops.extend(shadows.iter().enumerate().map(|(index, shadow)| DrawOp {
16380 z_index: shadow.z_index,
16381 kind: DrawOpKind::Shadow(index),
16382 }));
16383 ops.sort_by_key(|op| op.z_index);
16384 ops
16385 }
16386
16387 fn test_layer(local_bounds: Rect, children: Vec<RenderNode>) -> LayerNode {
16388 crate::test_support::layer_node(
16389 local_bounds,
16390 ProjectiveTransform::identity(),
16391 GraphicsLayer::default(),
16392 children,
16393 )
16394 }
16395
16396 fn cacheable_layer(
16397 node_id: cranpose_core::NodeId,
16398 local_bounds: Rect,
16399 children: Vec<RenderNode>,
16400 ) -> LayerNode {
16401 let mut layer = test_layer(local_bounds, children);
16402 layer.node_id = Some(node_id);
16403 layer.cache_policy = cranpose_render_common::graph::CachePolicy::Auto;
16404 layer.recompute_raster_cache_hashes();
16405 layer
16406 }
16407
16408 fn text_layer_with_style(text: AnnotatedString, text_style: TextStyle) -> LayerNode {
16409 test_layer(
16410 Rect {
16411 x: 0.0,
16412 y: 0.0,
16413 width: 64.0,
16414 height: 32.0,
16415 },
16416 vec![RenderNode::Primitive(PrimitiveEntry {
16417 phase: PrimitivePhase::BeforeChildren,
16418 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
16419 node_id: 1,
16420 rect: Rect {
16421 x: 2.0,
16422 y: 3.0,
16423 width: 48.0,
16424 height: 18.0,
16425 },
16426 text: std::rc::Rc::new(text),
16427 text_style,
16428 font_size: 14.0,
16429 layout_options: TextLayoutOptions::default(),
16430 clip: None,
16431 })),
16432 })],
16433 )
16434 }
16435
16436 fn snapped_text_leaf(animated: bool, translated_content_context: bool) -> LayerNode {
16437 LayerNode {
16438 node_id: Some(77),
16439 local_bounds: Rect {
16440 x: 0.0,
16441 y: 0.0,
16442 width: 48.0,
16443 height: 24.0,
16444 },
16445 transform_to_parent: ProjectiveTransform::translation(14.25, 16.5),
16446 motion_context_animated: animated,
16447 translated_content_context,
16448 translated_content_offset: Point::default(),
16449 content_offset: Point::default(),
16450 scene_children_origin: cranpose_ui_graphics::Point::default(),
16451 scene_children_layer_translation: cranpose_ui_graphics::Point::default(),
16452 graphics_layer: GraphicsLayer::default(),
16453 clip_to_bounds: false,
16454 shadow_clip: None,
16455 hit_test: None,
16456 has_hit_targets: false,
16457 isolation: IsolationReasons::default(),
16458 cache_policy: CachePolicy::None,
16459 cache_hashes: LayerRasterCacheHashes::default(),
16460 cache_hashes_valid: false,
16461 children: vec![
16462 RenderNode::Primitive(PrimitiveEntry {
16463 phase: PrimitivePhase::BeforeChildren,
16464 node: PrimitiveNode::Draw(DrawPrimitiveNode {
16465 primitive: DrawPrimitive::RoundRect {
16466 rect: Rect {
16467 x: 0.0,
16468 y: 0.0,
16469 width: 48.0,
16470 height: 24.0,
16471 },
16472 brush: Brush::solid(Color(0.28, 0.30, 0.46, 0.88)),
16473 radii: CornerRadii::uniform(6.0),
16474 stroke: None,
16475 },
16476 clip: None,
16477 }),
16478 }),
16479 RenderNode::Primitive(PrimitiveEntry {
16480 phase: PrimitivePhase::BeforeChildren,
16481 node: PrimitiveNode::Draw(DrawPrimitiveNode {
16482 primitive: DrawPrimitive::Image {
16483 rect: Rect {
16484 x: 2.0,
16485 y: 2.0,
16486 width: 12.0,
16487 height: 12.0,
16488 },
16489 image: ImageBitmap::from_rgba8(
16490 2,
16491 2,
16492 vec![
16493 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255,
16494 255,
16495 ],
16496 )
16497 .expect("image"),
16498 alpha: 1.0,
16499 color_filter: None,
16500 sampling: ImageSampling::Linear,
16501 src_rect: None,
16502 },
16503 clip: None,
16504 }),
16505 }),
16506 RenderNode::Primitive(PrimitiveEntry {
16507 phase: PrimitivePhase::BeforeChildren,
16508 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
16509 node_id: 77,
16510 rect: Rect {
16511 x: 6.0,
16512 y: 4.0,
16513 width: 36.0,
16514 height: 16.0,
16515 },
16516 text: std::rc::Rc::new(AnnotatedString::from("48 px")),
16517 text_style: TextStyle::default(),
16518 font_size: 14.0,
16519 layout_options: TextLayoutOptions::default(),
16520 clip: None,
16521 })),
16522 }),
16523 ],
16524 }
16525 }
16526
16527 fn snapped_text_leaf_root(animated: bool, translated_content_context: bool) -> LayerNode {
16528 let text_leaf = snapped_text_leaf(animated, translated_content_context);
16529 test_layer(
16530 Rect {
16531 x: 0.0,
16532 y: 0.0,
16533 width: 96.0,
16534 height: 64.0,
16535 },
16536 vec![RenderNode::Layer(Box::new(text_leaf))],
16537 )
16538 }
16539
16540 fn translated_content_local_surface_root() -> LayerNode {
16541 let mut effectful_text = text_layer_with_style(
16542 AnnotatedString::from("shadow"),
16543 TextStyle::from_span_style(SpanStyle {
16544 shadow: Some(Shadow {
16545 color: Color::BLACK,
16546 offset: Point::new(1.0, 2.0),
16547 blur_radius: 3.0,
16548 }),
16549 ..SpanStyle::default()
16550 }),
16551 );
16552 effectful_text.translated_content_context = true;
16553
16554 let translated_content = LayerNode {
16555 node_id: Some(78),
16556 local_bounds: Rect {
16557 x: 0.0,
16558 y: 0.0,
16559 width: 96.0,
16560 height: 64.0,
16561 },
16562 transform_to_parent: ProjectiveTransform::translation(14.25, 16.5),
16563 motion_context_animated: false,
16564 translated_content_context: true,
16565 translated_content_offset: Point::default(),
16566 content_offset: Point::default(),
16567 scene_children_origin: cranpose_ui_graphics::Point::default(),
16568 scene_children_layer_translation: cranpose_ui_graphics::Point::default(),
16569 graphics_layer: GraphicsLayer::default(),
16570 clip_to_bounds: false,
16571 shadow_clip: None,
16572 hit_test: None,
16573 has_hit_targets: false,
16574 isolation: IsolationReasons::default(),
16575 cache_policy: CachePolicy::None,
16576 cache_hashes: LayerRasterCacheHashes::default(),
16577 cache_hashes_valid: false,
16578 children: vec![RenderNode::Layer(Box::new(effectful_text))],
16579 };
16580
16581 test_layer(
16582 Rect {
16583 x: 0.0,
16584 y: 0.0,
16585 width: 160.0,
16586 height: 120.0,
16587 },
16588 vec![RenderNode::Layer(Box::new(translated_content))],
16589 )
16590 }
16591
16592 #[test]
16593 fn scissor_rect_for_layer_intersects_with_clip() {
16594 let rect = Rect {
16595 x: 10.0,
16596 y: 10.0,
16597 width: 30.0,
16598 height: 20.0,
16599 };
16600 let clip = Rect {
16601 x: 20.0,
16602 y: 15.0,
16603 width: 100.0,
16604 height: 100.0,
16605 };
16606
16607 let scissor = scissor_rect_for_layer(rect, Some(clip), 1.0, 200, 200);
16608 assert_eq!(scissor, Some((20, 15, 20, 15)));
16609 }
16610
16611 #[test]
16612 fn visible_draw_rect_no_clip_returns_original() {
16613 let rect = Rect {
16614 x: 100.0,
16615 y: 200.0,
16616 width: 300.0,
16617 height: 400.0,
16618 };
16619 assert_eq!(visible_draw_rect(rect, None), Some(rect));
16620 }
16621
16622 #[test]
16623 fn visible_draw_rect_with_clip_intersects() {
16624 let rect = Rect {
16625 x: 0.0,
16626 y: 0.0,
16627 width: 2000.0,
16628 height: 5000.0,
16629 };
16630 let clip = Rect {
16631 x: 0.0,
16632 y: 0.0,
16633 width: 800.0,
16634 height: 600.0,
16635 };
16636 let visible = visible_draw_rect(rect, Some(clip)).expect("should have visible area");
16637 assert_eq!(visible.width, 800.0);
16638 assert_eq!(visible.height, 600.0);
16639 }
16640
16641 #[test]
16642 fn visible_draw_rect_fully_clipped_returns_none() {
16643 let rect = Rect {
16644 x: 1000.0,
16645 y: 1000.0,
16646 width: 200.0,
16647 height: 200.0,
16648 };
16649 let clip = Rect {
16650 x: 0.0,
16651 y: 0.0,
16652 width: 800.0,
16653 height: 600.0,
16654 };
16655 assert!(visible_draw_rect(rect, Some(clip)).is_none());
16656 }
16657
16658 #[test]
16659 fn scene_bounds_respects_clip_on_shapes() {
16660 let mut scene = CompositorScene::new();
16661 scene.shapes.push(DrawShape {
16663 rect: Rect {
16664 x: 10.0,
16665 y: 10.0,
16666 width: 100.0,
16667 height: 50.0,
16668 },
16669 clip: Some(Rect {
16670 x: 0.0,
16671 y: 0.0,
16672 width: 800.0,
16673 height: 600.0,
16674 }),
16675 ..test_shape(0, BlendMode::SrcOver)
16676 });
16677 scene.shapes.push(DrawShape {
16679 rect: Rect {
16680 x: 0.0,
16681 y: 3000.0,
16682 width: 100.0,
16683 height: 50.0,
16684 },
16685 clip: Some(Rect {
16686 x: 0.0,
16687 y: 0.0,
16688 width: 800.0,
16689 height: 600.0,
16690 }),
16691 ..test_shape(1, BlendMode::SrcOver)
16692 });
16693 let bounds = scene_bounds(&scene).expect("should have bounds");
16694 assert!(bounds.y + bounds.height <= 600.0);
16697 }
16698
16699 #[test]
16700 fn scene_bounds_scroll_content_clipped_to_viewport() {
16701 let mut scene = CompositorScene::new();
16704 let viewport_clip = Rect {
16705 x: 0.0,
16706 y: 0.0,
16707 width: 800.0,
16708 height: 600.0,
16709 };
16710 for i in 0..20 {
16711 scene.shapes.push(DrawShape {
16712 rect: Rect {
16713 x: 0.0,
16714 y: i as f32 * 300.0,
16715 width: 800.0,
16716 height: 200.0,
16717 },
16718 clip: Some(viewport_clip),
16719 ..test_shape(i, BlendMode::SrcOver)
16720 });
16721 }
16722 let bounds = scene_bounds(&scene).expect("should have bounds");
16723 assert_eq!(bounds.x, 0.0);
16726 assert_eq!(bounds.y, 0.0);
16727 assert!(bounds.width <= 800.0);
16728 assert!(bounds.height <= 600.0);
16729 }
16730
16731 #[test]
16732 fn scene_bounds_stable_across_scroll_offsets() {
16733 let viewport_clip = Rect {
16736 x: 0.0,
16737 y: 0.0,
16738 width: 400.0,
16739 height: 50.0,
16740 };
16741 let compute_bounds_at_offset = |scroll_x: f32| {
16742 let mut scene = CompositorScene::new();
16743 for i in 0..10 {
16744 scene.shapes.push(DrawShape {
16745 rect: Rect {
16746 x: i as f32 * 100.0 - scroll_x,
16747 y: 0.0,
16748 width: 80.0,
16749 height: 40.0,
16750 },
16751 clip: Some(viewport_clip),
16752 ..test_shape(i, BlendMode::SrcOver)
16753 });
16754 }
16755 scene_bounds(&scene).expect("bounds")
16756 };
16757 let bounds_at_0 = compute_bounds_at_offset(0.0);
16758 let bounds_at_300 = compute_bounds_at_offset(300.0);
16759 let bounds_at_600 = compute_bounds_at_offset(600.0);
16760 assert!(
16762 (bounds_at_0.width - bounds_at_300.width).abs() < 1.0,
16763 "bounds width changed with scroll: {} vs {}",
16764 bounds_at_0.width,
16765 bounds_at_300.width
16766 );
16767 assert!(
16768 (bounds_at_0.width - bounds_at_600.width).abs() < 1.0,
16769 "bounds width changed with scroll: {} vs {}",
16770 bounds_at_0.width,
16771 bounds_at_600.width
16772 );
16773 }
16774
16775 #[test]
16776 fn collect_effect_ranges_respects_excluded_effect() {
16777 let layers = vec![effect_layer(10, 40), effect_layer(20, 30)];
16778 let mut ranges = Vec::new();
16779 collect_effect_ranges(&layers, 10, 40, Some(0), &mut ranges);
16780 assert_eq!(ranges.len(), 1);
16781 assert_eq!(ranges[0], 20..30);
16782 }
16783
16784 #[test]
16785 fn collect_layer_events_includes_nested_when_parent_excluded() {
16786 let effects = vec![effect_layer(10, 40), effect_layer(20, 30)];
16787 let backdrops = vec![backdrop_layer(25)];
16788 let mut events = Vec::new();
16789 collect_layer_events(&effects, &backdrops, 10, 40, Some(0), &mut events);
16790 assert_eq!(events.len(), 2);
16791
16792 match events[0].kind {
16793 LayerEventKind::Effect(index) => assert_eq!(index, 1),
16794 LayerEventKind::Backdrop(_) => panic!("expected nested effect as first event"),
16795 }
16796 match events[1].kind {
16797 LayerEventKind::Backdrop(index) => assert_eq!(index, 0),
16798 LayerEventKind::Effect(_) => panic!("expected backdrop as second event"),
16799 }
16800 }
16801
16802 fn pure_text_leaf(animated: bool, translated_content_context: bool) -> LayerNode {
16803 LayerNode {
16804 node_id: Some(177),
16805 local_bounds: Rect {
16806 x: 0.0,
16807 y: 0.0,
16808 width: 96.0,
16809 height: 32.0,
16810 },
16811 transform_to_parent: ProjectiveTransform::translation(11.4, 23.6),
16812 motion_context_animated: animated,
16813 translated_content_context,
16814 translated_content_offset: Point::default(),
16815 content_offset: Point::default(),
16816 scene_children_origin: cranpose_ui_graphics::Point::default(),
16817 scene_children_layer_translation: cranpose_ui_graphics::Point::default(),
16818 graphics_layer: GraphicsLayer::default(),
16819 clip_to_bounds: false,
16820 shadow_clip: None,
16821 hit_test: None,
16822 has_hit_targets: false,
16823 isolation: IsolationReasons::default(),
16824 cache_policy: CachePolicy::None,
16825 cache_hashes: LayerRasterCacheHashes::default(),
16826 cache_hashes_valid: false,
16827 children: vec![RenderNode::Primitive(PrimitiveEntry {
16828 phase: PrimitivePhase::BeforeChildren,
16829 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
16830 node_id: 177,
16831 rect: Rect {
16832 x: 0.0,
16833 y: 0.0,
16834 width: 96.0,
16835 height: 24.0,
16836 },
16837 clip: None,
16838 text: std::rc::Rc::new(AnnotatedString::from("Pure text")),
16839 text_style: TextStyle::default(),
16840 font_size: 14.0,
16841 layout_options: TextLayoutOptions::default(),
16842 })),
16843 })],
16844 }
16845 }
16846
16847 fn pure_text_leaf_root(animated: bool, translated_content_context: bool) -> LayerNode {
16848 let text_leaf = pure_text_leaf(animated, translated_content_context);
16849 test_layer(
16850 Rect {
16851 x: 0.0,
16852 y: 0.0,
16853 width: 160.0,
16854 height: 96.0,
16855 },
16856 vec![RenderNode::Layer(Box::new(text_leaf))],
16857 )
16858 }
16859
16860 #[test]
16861 fn collect_layer_events_sorts_backdrop_before_effect_at_same_z() {
16862 let effects = vec![effect_layer(10, 20)];
16863 let backdrops = vec![backdrop_layer(10)];
16864 let mut events = Vec::new();
16865 collect_layer_events(&effects, &backdrops, 0, 30, None, &mut events);
16866 assert_eq!(events.len(), 2);
16867
16868 match events[0].kind {
16869 LayerEventKind::Backdrop(_) => {}
16870 LayerEventKind::Effect(_) => panic!("expected backdrop to run before effect"),
16871 }
16872 match events[1].kind {
16873 LayerEventKind::Effect(_) => {}
16874 LayerEventKind::Backdrop(_) => panic!("expected effect as second event"),
16875 }
16876 }
16877
16878 #[test]
16879 fn collect_layer_events_prefers_outer_effect_when_same_start_z() {
16880 let effects = vec![effect_layer(10, 20), effect_layer(10, 40)];
16883 let mut events = Vec::new();
16884 collect_layer_events(&effects, &[], 0, 50, None, &mut events);
16885
16886 assert_eq!(events.len(), 2);
16887 match events[0].kind {
16888 LayerEventKind::Effect(index) => assert_eq!(index, 1),
16889 LayerEventKind::Backdrop(_) => panic!("expected outer effect first"),
16890 }
16891 match events[1].kind {
16892 LayerEventKind::Effect(index) => assert_eq!(index, 0),
16893 LayerEventKind::Backdrop(_) => panic!("expected child effect second"),
16894 }
16895 }
16896
16897 #[test]
16898 fn collect_layer_events_prefers_later_effect_when_ranges_match() {
16899 let effects = vec![effect_layer(10, 20), effect_layer(10, 20)];
16900 let mut events = Vec::new();
16901 collect_layer_events(&effects, &[], 0, 30, None, &mut events);
16902
16903 assert_eq!(events.len(), 2);
16904 match events[0].kind {
16905 LayerEventKind::Effect(index) => assert_eq!(index, 1),
16906 LayerEventKind::Backdrop(_) => panic!("expected later effect first"),
16907 }
16908 match events[1].kind {
16909 LayerEventKind::Effect(index) => assert_eq!(index, 0),
16910 LayerEventKind::Backdrop(_) => panic!("expected earlier effect second"),
16911 }
16912 }
16913
16914 #[test]
16915 fn has_backdrop_layer_in_range_detects_nested_layers() {
16916 let backdrops = vec![backdrop_layer(5), backdrop_layer(15), backdrop_layer(25)];
16917 assert!(has_backdrop_layer_in_range(&backdrops, 10, 20));
16918 assert!(has_backdrop_layer_in_range(&backdrops, 0, 6));
16919 assert!(!has_backdrop_layer_in_range(&backdrops, 20, 25));
16920 }
16921
16922 #[test]
16923 fn layer_contains_descendant_backdrop_ignores_self_backdrop() {
16924 let mut self_backdrop = test_layer(
16925 Rect {
16926 x: 0.0,
16927 y: 0.0,
16928 width: 10.0,
16929 height: 10.0,
16930 },
16931 vec![],
16932 );
16933 self_backdrop.graphics_layer.backdrop_effect = Some(RenderEffect::blur(2.0));
16934 assert!(!layer_contains_descendant_backdrop(&self_backdrop));
16935
16936 let mut child = test_layer(
16937 Rect {
16938 x: 0.0,
16939 y: 0.0,
16940 width: 8.0,
16941 height: 8.0,
16942 },
16943 vec![],
16944 );
16945 child.graphics_layer.backdrop_effect = Some(RenderEffect::blur(2.0));
16946
16947 let parent = test_layer(
16948 Rect {
16949 x: 0.0,
16950 y: 0.0,
16951 width: 20.0,
16952 height: 20.0,
16953 },
16954 vec![RenderNode::Layer(Box::new(child))],
16955 );
16956 assert!(layer_contains_descendant_backdrop(&parent));
16957 }
16958
16959 fn child_layer_composite(
16960 layer: &LayerNode,
16961 z_index: usize,
16962 rect: Rect,
16963 needs_nested_underlay: bool,
16964 ) -> crate::normalized_scene::ChildLayerComposite {
16965 let mut requirements_cache = cranpose_core::collections::map::HashMap::new();
16966 let surface_requirements =
16967 crate::surface_plan::layer_surface_requirements_cached(layer, &mut requirements_cache);
16968 crate::normalized_scene::ChildLayerComposite {
16969 z_index,
16970 logical_rect: Rect {
16971 x: 0.0,
16972 y: 0.0,
16973 width: rect.width,
16974 height: rect.height,
16975 },
16976 dest_quad: rect_to_quad(rect),
16977 snap_anchor: None,
16978 composite_snap_origin: None,
16979 backdrop_rect: rect,
16980 visual_clip: None,
16981 surface_clip: None,
16982 shadow_draws: Vec::new(),
16983 needs_nested_underlay,
16984 node_id: layer.node_id,
16985 backdrop: layer.backdrop().cloned(),
16986 has_effect: layer.effect().is_some(),
16987 effect_contains_runtime_shader: layer
16988 .effect()
16989 .is_some_and(|effect| effect.contains_runtime_shader()),
16990 target_content_hash: layer.target_content_hash(),
16991 effect_hash: layer.effect_hash(),
16992 motion_source_content_hash: Some(layer.motion_source_content_hash()),
16993 contains_descendant_backdrop: layer_contains_descendant_backdrop(layer),
16994 cache_policy: layer.cache_policy,
16995 surface_requirements,
16996 rounded_clip: crate::surface_executor::backend::LayerSurfaceRoundedClip::from_layer(
16997 layer,
16998 ),
16999 isolation: cranpose_render_common::layer_composition::effective_layer_isolation(
17000 &layer.graphics_layer,
17001 ),
17002 translated_content_context: layer.translated_content_context,
17003 own_translated_content_axes: crate::surface_plan::translated_content_axes_for_layer(
17004 layer,
17005 ),
17006 clip_rect: layer.clip_rect(),
17007 local_bounds: layer.local_bounds,
17008 surface_scale: crate::surface_plan::layer_surface_scale(layer),
17009 source: crate::normalized_scene::LoweredChildSource::default(),
17010 }
17011 }
17012
17013 #[test]
17014 fn root_direct_preflight_allows_first_translated_child_underlay() {
17015 let child = test_layer(
17016 Rect {
17017 x: 0.0,
17018 y: 0.0,
17019 width: 400.0,
17020 height: 280.0,
17021 },
17022 vec![],
17023 );
17024 let collected = CollectedLayer {
17025 scene: CompositorScene::new(),
17026 child_layers: vec![child_layer_composite(
17027 &child,
17028 3,
17029 Rect {
17030 x: 48.0,
17031 y: 96.0,
17032 width: 400.0,
17033 height: 280.0,
17034 },
17035 true,
17036 )],
17037 };
17038
17039 assert!(direct_root_child_underlays_are_supported(&collected));
17040 }
17041
17042 #[test]
17043 fn root_direct_preflight_allows_axis_aligned_prior_child_underlay() {
17044 let first = test_layer(
17045 Rect {
17046 x: 0.0,
17047 y: 0.0,
17048 width: 80.0,
17049 height: 40.0,
17050 },
17051 vec![],
17052 );
17053 let backdrop_child = test_layer(
17054 Rect {
17055 x: 0.0,
17056 y: 0.0,
17057 width: 400.0,
17058 height: 280.0,
17059 },
17060 vec![],
17061 );
17062 let collected = CollectedLayer {
17063 scene: CompositorScene::new(),
17064 child_layers: vec![
17065 child_layer_composite(
17066 &first,
17067 1,
17068 Rect {
17069 x: 8.0,
17070 y: 16.0,
17071 width: 80.0,
17072 height: 40.0,
17073 },
17074 false,
17075 ),
17076 child_layer_composite(
17077 &backdrop_child,
17078 4,
17079 Rect {
17080 x: 48.0,
17081 y: 96.0,
17082 width: 400.0,
17083 height: 280.0,
17084 },
17085 true,
17086 ),
17087 ],
17088 };
17089
17090 assert!(direct_root_child_underlays_are_supported(&collected));
17091 }
17092
17093 #[test]
17094 fn root_direct_preflight_rejects_effectful_prior_child_underlay() {
17095 let mut first = test_layer(
17096 Rect {
17097 x: 0.0,
17098 y: 0.0,
17099 width: 80.0,
17100 height: 40.0,
17101 },
17102 vec![],
17103 );
17104 first.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
17105 let backdrop_child = test_layer(
17106 Rect {
17107 x: 0.0,
17108 y: 0.0,
17109 width: 400.0,
17110 height: 280.0,
17111 },
17112 vec![],
17113 );
17114 let collected = CollectedLayer {
17115 scene: CompositorScene::new(),
17116 child_layers: vec![
17117 child_layer_composite(
17118 &first,
17119 1,
17120 Rect {
17121 x: 64.0,
17122 y: 112.0,
17123 width: 80.0,
17124 height: 40.0,
17125 },
17126 false,
17127 ),
17128 child_layer_composite(
17129 &backdrop_child,
17130 4,
17131 Rect {
17132 x: 48.0,
17133 y: 96.0,
17134 width: 400.0,
17135 height: 280.0,
17136 },
17137 true,
17138 ),
17139 ],
17140 };
17141
17142 assert!(!direct_root_child_underlays_are_supported(&collected));
17143 }
17144
17145 #[test]
17146 fn root_direct_preflight_ignores_non_overlapping_effectful_prior_child_underlay() {
17147 let mut first = test_layer(
17148 Rect {
17149 x: 0.0,
17150 y: 0.0,
17151 width: 80.0,
17152 height: 40.0,
17153 },
17154 vec![],
17155 );
17156 first.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
17157 let backdrop_child = test_layer(
17158 Rect {
17159 x: 0.0,
17160 y: 0.0,
17161 width: 400.0,
17162 height: 280.0,
17163 },
17164 vec![],
17165 );
17166 let collected = CollectedLayer {
17167 scene: CompositorScene::new(),
17168 child_layers: vec![
17169 child_layer_composite(
17170 &first,
17171 1,
17172 Rect {
17173 x: 8.0,
17174 y: 16.0,
17175 width: 80.0,
17176 height: 40.0,
17177 },
17178 false,
17179 ),
17180 child_layer_composite(
17181 &backdrop_child,
17182 4,
17183 Rect {
17184 x: 48.0,
17185 y: 96.0,
17186 width: 400.0,
17187 height: 280.0,
17188 },
17189 true,
17190 ),
17191 ],
17192 };
17193
17194 assert!(direct_root_child_underlays_are_supported(&collected));
17195 }
17196
17197 #[test]
17198 fn root_direct_preflight_rejects_underlay_that_would_replay_prior_scene_effects() {
17199 let backdrop_child = test_layer(
17200 Rect {
17201 x: 0.0,
17202 y: 0.0,
17203 width: 400.0,
17204 height: 280.0,
17205 },
17206 vec![],
17207 );
17208 let mut scene = CompositorScene::new();
17209 scene.next_z = 1;
17210 scene.push_effect_layer(
17211 Rect {
17212 x: 0.0,
17213 y: 0.0,
17214 width: 120.0,
17215 height: 120.0,
17216 },
17217 None,
17218 Some(RenderEffect::blur(2.0)),
17219 BlendMode::SrcOver,
17220 1.0,
17221 0,
17222 1,
17223 );
17224 let collected = CollectedLayer {
17225 scene,
17226 child_layers: vec![child_layer_composite(
17227 &backdrop_child,
17228 4,
17229 Rect {
17230 x: 48.0,
17231 y: 96.0,
17232 width: 400.0,
17233 height: 280.0,
17234 },
17235 true,
17236 )],
17237 };
17238
17239 assert!(!direct_root_child_underlays_are_supported(&collected));
17240 }
17241
17242 #[test]
17243 fn root_direct_eligibility_does_not_reject_descendant_backdrop() {
17244 let mut backdrop = test_layer(
17245 Rect {
17246 x: 0.0,
17247 y: 0.0,
17248 width: 40.0,
17249 height: 40.0,
17250 },
17251 vec![],
17252 );
17253 backdrop.graphics_layer.backdrop_effect = Some(RenderEffect::blur(4.0));
17254 let child = test_layer(
17255 Rect {
17256 x: 0.0,
17257 y: 0.0,
17258 width: 120.0,
17259 height: 96.0,
17260 },
17261 vec![RenderNode::Layer(Box::new(backdrop))],
17262 );
17263 let root = test_layer(
17264 Rect {
17265 x: 0.0,
17266 y: 0.0,
17267 width: 240.0,
17268 height: 160.0,
17269 },
17270 vec![RenderNode::Layer(Box::new(child))],
17271 );
17272 let mut cache = HashMap::new();
17273
17274 assert!(root_can_render_directly_cached(&root, &mut cache));
17275 }
17276
17277 #[test]
17278 fn root_direct_scene_events_allow_root_local_effects() {
17279 let mut scene = CompositorScene::new();
17280 scene.effect_layers.push(EffectLayer {
17281 rect: Rect {
17282 x: 20.0,
17283 y: 30.0,
17284 width: 120.0,
17285 height: 80.0,
17286 },
17287 clip: None,
17288 snap_anchor: None,
17289 effect: Some(RenderEffect::blur(6.0)),
17290 blend_mode: BlendMode::SrcOver,
17291 composite_alpha: 1.0,
17292 z_start: 0,
17293 z_end: 1,
17294 requirements: SurfaceRequirementSet::default().with(SurfaceRequirement::RenderEffect),
17295 });
17296
17297 assert!(root_direct_scene_events_are_supported(&scene));
17298 }
17299
17300 #[test]
17301 fn root_direct_scene_events_reject_root_local_backdrops() {
17302 let mut scene = CompositorScene::new();
17303 scene.backdrop_layers.push(BackdropLayer {
17304 node_id: Some(99),
17305 rect: Rect {
17306 x: 20.0,
17307 y: 30.0,
17308 width: 120.0,
17309 height: 80.0,
17310 },
17311 clip: None,
17312 snap_anchor: None,
17313 effect: RenderEffect::blur(6.0),
17314 z_index: 1,
17315 });
17316
17317 assert!(!root_direct_scene_events_are_supported(&scene));
17318 }
17319
17320 #[test]
17321 fn estimate_layer_surface_rect_includes_transformed_child_bounds() {
17322 let mut child = test_layer(
17323 Rect {
17324 x: 0.0,
17325 y: 0.0,
17326 width: 10.0,
17327 height: 6.0,
17328 },
17329 vec![RenderNode::Primitive(PrimitiveEntry {
17330 phase: PrimitivePhase::BeforeChildren,
17331 node: PrimitiveNode::Draw(DrawPrimitiveNode {
17332 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
17333 rect: Rect {
17334 x: 0.0,
17335 y: 0.0,
17336 width: 10.0,
17337 height: 6.0,
17338 },
17339 brush: Brush::solid(Color::WHITE),
17340 stroke: None,
17341 },
17342 clip: None,
17343 }),
17344 })],
17345 );
17346 child.transform_to_parent = ProjectiveTransform::translation(18.0, 7.0);
17347
17348 let parent = test_layer(
17349 Rect {
17350 x: 0.0,
17351 y: 0.0,
17352 width: 4.0,
17353 height: 4.0,
17354 },
17355 vec![RenderNode::Layer(Box::new(child))],
17356 );
17357
17358 assert_eq!(
17359 estimate_layer_surface_rect(&parent),
17360 Rect {
17361 x: 18.0,
17362 y: 7.0,
17363 width: 10.0,
17364 height: 6.0,
17365 }
17366 );
17367 }
17368
17369 #[test]
17370 fn estimate_layer_surface_rect_clips_translated_clip_layers_without_hidden_leading_content() {
17371 let mut layer = test_layer(
17372 Rect {
17373 x: 0.0,
17374 y: 0.0,
17375 width: 120.0,
17376 height: 72.0,
17377 },
17378 vec![RenderNode::Primitive(PrimitiveEntry {
17379 phase: PrimitivePhase::BeforeChildren,
17380 node: PrimitiveNode::Draw(DrawPrimitiveNode {
17381 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
17382 rect: Rect {
17383 x: 24.0,
17384 y: 0.0,
17385 width: 200.0,
17386 height: 480.0,
17387 },
17388 brush: Brush::solid(Color::WHITE),
17389 stroke: None,
17390 },
17391 clip: None,
17392 }),
17393 })],
17394 );
17395 layer.translated_content_context = true;
17396 layer.motion_context_animated = true;
17397 layer.clip_to_bounds = true;
17398
17399 assert_eq!(
17400 estimate_layer_surface_rect(&layer),
17401 Rect {
17402 x: 24.0,
17403 y: 0.0,
17404 width: 96.0,
17405 height: 72.0,
17406 }
17407 );
17408 }
17409
17410 #[test]
17411 fn estimate_layer_surface_rect_clips_active_horizontal_scroll_content() {
17412 let mut layer = test_layer(
17413 Rect {
17414 x: 0.0,
17415 y: 0.0,
17416 width: 120.0,
17417 height: 72.0,
17418 },
17419 vec![RenderNode::Primitive(PrimitiveEntry {
17420 phase: PrimitivePhase::BeforeChildren,
17421 node: PrimitiveNode::Draw(DrawPrimitiveNode {
17422 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
17423 rect: Rect {
17424 x: -24.0,
17425 y: 0.0,
17426 width: 200.0,
17427 height: 480.0,
17428 },
17429 brush: Brush::solid(Color::WHITE),
17430 stroke: None,
17431 },
17432 clip: None,
17433 }),
17434 })],
17435 );
17436 layer.translated_content_context = true;
17437 layer.motion_context_animated = true;
17438 layer.clip_to_bounds = true;
17439
17440 assert_eq!(
17441 estimate_layer_surface_rect(&layer),
17442 Rect {
17443 x: 0.0,
17444 y: 0.0,
17445 width: 120.0,
17446 height: 72.0,
17447 }
17448 );
17449 }
17450
17451 #[test]
17452 fn estimate_layer_surface_rect_clips_active_vertical_scroll_content() {
17453 let mut layer = test_layer(
17454 Rect {
17455 x: 0.0,
17456 y: 0.0,
17457 width: 120.0,
17458 height: 72.0,
17459 },
17460 vec![RenderNode::Primitive(PrimitiveEntry {
17461 phase: PrimitivePhase::BeforeChildren,
17462 node: PrimitiveNode::Draw(DrawPrimitiveNode {
17463 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
17464 rect: Rect {
17465 x: 0.0,
17466 y: -24.0,
17467 width: 120.0,
17468 height: 200.0,
17469 },
17470 brush: Brush::solid(Color::WHITE),
17471 stroke: None,
17472 },
17473 clip: None,
17474 }),
17475 })],
17476 );
17477 layer.translated_content_context = true;
17478 layer.motion_context_animated = true;
17479 layer.clip_to_bounds = true;
17480
17481 assert_eq!(
17482 estimate_layer_surface_rect(&layer),
17483 Rect {
17484 x: 0.0,
17485 y: 0.0,
17486 width: 120.0,
17487 height: 72.0,
17488 }
17489 );
17490 }
17491
17492 #[test]
17493 fn estimate_layer_surface_rect_keeps_shallow_scroll_capture_origin_stable() {
17494 fn shallow_scroll_surface_rect(content_y: f32) -> Rect {
17495 let mut layer = test_layer(
17496 Rect {
17497 x: 0.0,
17498 y: 0.0,
17499 width: 120.0,
17500 height: 72.0,
17501 },
17502 vec![RenderNode::Primitive(PrimitiveEntry {
17503 phase: PrimitivePhase::BeforeChildren,
17504 node: PrimitiveNode::Draw(DrawPrimitiveNode {
17505 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
17506 rect: Rect {
17507 x: 0.0,
17508 y: content_y,
17509 width: 120.0,
17510 height: 200.0,
17511 },
17512 brush: Brush::solid(Color::WHITE),
17513 stroke: None,
17514 },
17515 clip: None,
17516 }),
17517 })],
17518 );
17519 layer.translated_content_context = true;
17520 layer.motion_context_animated = true;
17521 layer.clip_to_bounds = true;
17522 estimate_layer_surface_rect(&layer)
17523 }
17524
17525 assert_eq!(
17526 shallow_scroll_surface_rect(-24.0),
17527 shallow_scroll_surface_rect(-25.0),
17528 "shallow scroll capture bounds must not move the offscreen surface origin on adjacent scroll positions"
17529 );
17530 }
17531
17532 #[test]
17533 fn estimate_layer_surface_rect_clips_active_xy_scroll_content() {
17534 let mut layer = test_layer(
17535 Rect {
17536 x: 0.0,
17537 y: 0.0,
17538 width: 120.0,
17539 height: 72.0,
17540 },
17541 vec![RenderNode::Primitive(PrimitiveEntry {
17542 phase: PrimitivePhase::BeforeChildren,
17543 node: PrimitiveNode::Draw(DrawPrimitiveNode {
17544 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
17545 rect: Rect {
17546 x: -16.0,
17547 y: -24.0,
17548 width: 180.0,
17549 height: 240.0,
17550 },
17551 brush: Brush::solid(Color::WHITE),
17552 stroke: None,
17553 },
17554 clip: None,
17555 }),
17556 })],
17557 );
17558 layer.translated_content_context = true;
17559 layer.motion_context_animated = true;
17560 layer.clip_to_bounds = true;
17561
17562 assert_eq!(
17563 estimate_layer_surface_rect(&layer),
17564 Rect {
17565 x: 0.0,
17566 y: 0.0,
17567 width: 120.0,
17568 height: 72.0,
17569 }
17570 );
17571 }
17572
17573 #[test]
17574 fn estimate_layer_surface_rect_clips_deep_hidden_active_scroll_content() {
17575 let mut layer = test_layer(
17576 Rect {
17577 x: 0.0,
17578 y: 0.0,
17579 width: 120.0,
17580 height: 72.0,
17581 },
17582 vec![RenderNode::Primitive(PrimitiveEntry {
17583 phase: PrimitivePhase::BeforeChildren,
17584 node: PrimitiveNode::Draw(DrawPrimitiveNode {
17585 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
17586 rect: Rect {
17587 x: 0.0,
17588 y: -1200.0,
17589 width: 120.0,
17590 height: 1400.0,
17591 },
17592 brush: Brush::solid(Color::WHITE),
17593 stroke: None,
17594 },
17595 clip: None,
17596 }),
17597 })],
17598 );
17599 layer.translated_content_context = true;
17600 layer.motion_context_animated = true;
17601 layer.clip_to_bounds = true;
17602
17603 assert_eq!(
17604 estimate_layer_surface_rect(&layer),
17605 Rect {
17606 x: 0.0,
17607 y: 0.0,
17608 width: 120.0,
17609 height: 72.0,
17610 }
17611 );
17612 }
17613
17614 #[test]
17615 fn estimate_layer_surface_rect_keeps_deep_scroll_capture_origin_stable() {
17616 fn deep_scroll_surface_rect(content_y: f32) -> Rect {
17617 let mut layer = test_layer(
17618 Rect {
17619 x: 0.0,
17620 y: 0.0,
17621 width: 120.0,
17622 height: 72.0,
17623 },
17624 vec![RenderNode::Primitive(PrimitiveEntry {
17625 phase: PrimitivePhase::BeforeChildren,
17626 node: PrimitiveNode::Draw(DrawPrimitiveNode {
17627 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
17628 rect: Rect {
17629 x: 0.0,
17630 y: content_y,
17631 width: 120.0,
17632 height: 1400.0,
17633 },
17634 brush: Brush::solid(Color::WHITE),
17635 stroke: None,
17636 },
17637 clip: None,
17638 }),
17639 })],
17640 );
17641 layer.translated_content_context = true;
17642 layer.motion_context_animated = true;
17643 layer.clip_to_bounds = true;
17644 estimate_layer_surface_rect(&layer)
17645 }
17646
17647 assert_eq!(
17648 deep_scroll_surface_rect(-1200.0),
17649 deep_scroll_surface_rect(-1201.0),
17650 "deep scroll capture bounds must not re-phase the offscreen surface origin on adjacent scroll positions"
17651 );
17652 }
17653
17654 #[test]
17655 fn motion_stable_capture_bounds_bounds_shadows_for_clipped_effect_layer() {
17656 let mut layer = test_layer(
17657 Rect {
17658 x: 0.0,
17659 y: 0.0,
17660 width: 120.0,
17661 height: 72.0,
17662 },
17663 vec![],
17664 );
17665 layer.clip_to_bounds = true;
17666 layer.graphics_layer.clip = true;
17667 layer.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
17668
17669 let mut shadow_shape = test_shape(0, BlendMode::SrcOver);
17670 shadow_shape.rect = Rect {
17671 x: -24.0,
17672 y: -1200.0,
17673 width: 180.0,
17674 height: 1400.0,
17675 };
17676 let mut scene = CompositorScene::new();
17677 scene
17678 .shadow_draws
17679 .push(test_shadow_draw(vec![(shadow_shape, BlendMode::SrcOver)]));
17680
17681 let requirements = SurfaceRequirementSet::default()
17682 .with(SurfaceRequirement::RenderEffect)
17683 .with(SurfaceRequirement::MotionStableCapture);
17684
17685 assert_eq!(
17686 motion_stable_capture_bounds(
17687 &layer,
17688 &scene,
17689 &[],
17690 requirements,
17691 TranslatedContentAxes::default(),
17692 None,
17693 ),
17694 Some(Rect {
17695 x: -360.0,
17696 y: -216.0,
17697 width: 480.0,
17698 height: 288.0,
17699 })
17700 );
17701 }
17702
17703 #[test]
17704 fn vertical_motion_stable_capture_uses_viewport_cross_axis_bounds() {
17705 let mut layer = test_layer(
17706 Rect {
17707 x: 0.0,
17708 y: 0.0,
17709 width: 200.0,
17710 height: 100.0,
17711 },
17712 vec![],
17713 );
17714 layer.clip_to_bounds = true;
17715 layer.graphics_layer.clip = true;
17716
17717 let mut shape = test_shape(0, BlendMode::SrcOver);
17718 shape.rect = Rect {
17719 x: 60.0,
17720 y: -80.0,
17721 width: 80.0,
17722 height: 220.0,
17723 };
17724 let mut scene = CompositorScene::new();
17725 scene.shapes.push(shape);
17726
17727 let requirements =
17728 SurfaceRequirementSet::default().with(SurfaceRequirement::MotionStableCapture);
17729
17730 assert_eq!(
17731 motion_stable_capture_bounds(
17732 &layer,
17733 &scene,
17734 &[],
17735 requirements,
17736 TranslatedContentAxes { x: false, y: true },
17737 None,
17738 ),
17739 Some(Rect {
17740 x: -96.0,
17741 y: -64.0,
17742 width: 296.0,
17743 height: 164.0,
17744 })
17745 );
17746 }
17747
17748 #[test]
17749 fn vertical_motion_stable_capture_uses_external_surface_clip() {
17750 let layer = test_layer(
17751 Rect {
17752 x: 0.0,
17753 y: 0.0,
17754 width: 200.0,
17755 height: 100.0,
17756 },
17757 vec![],
17758 );
17759
17760 let mut shape = test_shape(0, BlendMode::SrcOver);
17761 shape.rect = Rect {
17762 x: 60.0,
17763 y: -80.0,
17764 width: 80.0,
17765 height: 220.0,
17766 };
17767 let mut scene = CompositorScene::new();
17768 scene.shapes.push(shape);
17769
17770 let requirements =
17771 SurfaceRequirementSet::default().with(SurfaceRequirement::MotionStableCapture);
17772
17773 assert_eq!(
17774 motion_stable_capture_bounds(
17775 &layer,
17776 &scene,
17777 &[],
17778 requirements,
17779 TranslatedContentAxes { x: false, y: true },
17780 Some(Rect {
17781 x: 0.0,
17782 y: 0.0,
17783 width: 200.0,
17784 height: 100.0,
17785 }),
17786 ),
17787 Some(Rect {
17788 x: -96.0,
17789 y: -64.0,
17790 width: 296.0,
17791 height: 164.0,
17792 })
17793 );
17794 }
17795
17796 #[test]
17797 fn estimate_layer_surface_rect_expands_for_child_layer_shadow() {
17798 let mut child = test_layer(
17799 Rect {
17800 x: 0.0,
17801 y: 0.0,
17802 width: 12.0,
17803 height: 8.0,
17804 },
17805 vec![],
17806 );
17807 child.transform_to_parent = ProjectiveTransform::translation(20.0, 9.0);
17808 child.graphics_layer.shadow_elevation = 6.0;
17809
17810 let parent = test_layer(
17811 Rect {
17812 x: 0.0,
17813 y: 0.0,
17814 width: 4.0,
17815 height: 4.0,
17816 },
17817 vec![RenderNode::Layer(Box::new(child))],
17818 );
17819
17820 let rect = estimate_layer_surface_rect(&parent);
17821 assert!(rect.x < 20.0);
17822 assert!(rect.y < 9.0);
17823 assert!(rect.width > 12.0);
17824 assert!(rect.height > 8.0);
17825 }
17826
17827 #[test]
17828 fn estimate_layer_surface_rect_respects_local_bounds_for_effect_layers() {
17829 let mut layer = test_layer(
17830 Rect {
17831 x: 0.0,
17832 y: 0.0,
17833 width: 28.0,
17834 height: 28.0,
17835 },
17836 vec![RenderNode::Primitive(PrimitiveEntry {
17837 phase: PrimitivePhase::BeforeChildren,
17838 node: PrimitiveNode::Draw(DrawPrimitiveNode {
17839 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
17840 rect: Rect {
17841 x: 10.0,
17842 y: 10.0,
17843 width: 10.0,
17844 height: 10.0,
17845 },
17846 brush: Brush::solid(Color::WHITE),
17847 stroke: None,
17848 },
17849 clip: None,
17850 }),
17851 })],
17852 );
17853 layer.graphics_layer.render_effect = Some(RenderEffect::blur(12.0));
17854
17855 assert_eq!(
17856 estimate_layer_surface_rect(&layer),
17857 Rect {
17858 x: 0.0,
17859 y: 0.0,
17860 width: 28.0,
17861 height: 28.0,
17862 }
17863 );
17864 }
17865
17866 #[test]
17867 fn layer_raster_cache_candidate_ignores_parent_transform() {
17868 let primitive = PrimitiveEntry {
17869 phase: PrimitivePhase::BeforeChildren,
17870 node: PrimitiveNode::Draw(DrawPrimitiveNode {
17871 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
17872 rect: Rect {
17873 x: 2.0,
17874 y: 3.0,
17875 width: 6.0,
17876 height: 4.0,
17877 },
17878 brush: Brush::solid(Color::BLACK),
17879 stroke: None,
17880 },
17881 clip: None,
17882 }),
17883 };
17884 let base = cacheable_layer(
17885 41,
17886 Rect {
17887 x: 0.0,
17888 y: 0.0,
17889 width: 20.0,
17890 height: 20.0,
17891 },
17892 vec![RenderNode::Primitive(primitive.clone())],
17893 );
17894 let mut moved = base.clone();
17895 moved.transform_to_parent = ProjectiveTransform::translation(32.0, 18.0);
17896
17897 assert_eq!(
17898 layer_raster_cache_candidate(&base, 1.25, false, false),
17899 layer_raster_cache_candidate(&moved, 1.25, false, false)
17900 );
17901 }
17902
17903 #[test]
17904 fn layer_raster_cache_candidate_changes_for_translated_content_offset() {
17905 let primitive = PrimitiveEntry {
17906 phase: PrimitivePhase::BeforeChildren,
17907 node: PrimitiveNode::Draw(DrawPrimitiveNode {
17908 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
17909 rect: Rect {
17910 x: 2.0,
17911 y: 3.0,
17912 width: 6.0,
17913 height: 4.0,
17914 },
17915 brush: Brush::solid(Color::BLACK),
17916 stroke: None,
17917 },
17918 clip: None,
17919 }),
17920 };
17921 let mut base = cacheable_layer(
17922 42,
17923 Rect {
17924 x: 0.0,
17925 y: 0.0,
17926 width: 20.0,
17927 height: 20.0,
17928 },
17929 vec![RenderNode::Primitive(primitive)],
17930 );
17931 base.translated_content_context = true;
17932 base.translated_content_offset = Point::new(0.0, -8.0);
17933 base.recompute_raster_cache_hashes();
17934
17935 let mut moved = base.clone();
17936 moved.translated_content_offset = Point::new(0.0, -16.0);
17937 moved.recompute_raster_cache_hashes();
17938
17939 assert_ne!(
17940 layer_raster_cache_candidate(&base, 1.25, false, false),
17941 layer_raster_cache_candidate(&moved, 1.25, false, false),
17942 "full-surface layer cache candidates must not alias different scroll offsets"
17943 );
17944 }
17945
17946 #[test]
17947 fn layer_raster_cache_candidate_changes_for_child_transform() {
17948 let mut child = cacheable_layer(
17949 8,
17950 Rect {
17951 x: 0.0,
17952 y: 0.0,
17953 width: 12.0,
17954 height: 10.0,
17955 },
17956 vec![],
17957 );
17958 child.transform_to_parent = ProjectiveTransform::translation(4.0, 6.0);
17959 let base = cacheable_layer(
17960 7,
17961 Rect {
17962 x: 0.0,
17963 y: 0.0,
17964 width: 20.0,
17965 height: 20.0,
17966 },
17967 vec![RenderNode::Layer(Box::new(child.clone()))],
17968 );
17969 let mut moved_child = child;
17970 moved_child.transform_to_parent = ProjectiveTransform::translation(9.0, 6.0);
17971 let moved = cacheable_layer(
17972 7,
17973 Rect {
17974 x: 0.0,
17975 y: 0.0,
17976 width: 20.0,
17977 height: 20.0,
17978 },
17979 vec![RenderNode::Layer(Box::new(moved_child))],
17980 );
17981
17982 assert_ne!(
17983 layer_raster_cache_candidate(&base, 1.0, false, false),
17984 layer_raster_cache_candidate(&moved, 1.0, false, false)
17985 );
17986 }
17987
17988 #[test]
17989 fn layer_raster_cache_candidate_rejects_external_backdrop_dependency() {
17990 let mut child = cacheable_layer(
17991 12,
17992 Rect {
17993 x: 0.0,
17994 y: 0.0,
17995 width: 8.0,
17996 height: 8.0,
17997 },
17998 vec![],
17999 );
18000 child.graphics_layer.backdrop_effect = Some(RenderEffect::blur(2.0));
18001 let parent = cacheable_layer(
18002 11,
18003 Rect {
18004 x: 0.0,
18005 y: 0.0,
18006 width: 16.0,
18007 height: 16.0,
18008 },
18009 vec![RenderNode::Layer(Box::new(child))],
18010 );
18011
18012 assert!(layer_raster_cache_candidate(&parent, 1.0, false, false).is_some());
18013 assert!(layer_raster_cache_candidate(&parent, 1.0, true, false).is_none());
18014 }
18015
18016 #[test]
18017 fn layer_raster_cache_candidate_does_not_force_translation_only_text_surfaces() {
18018 let text = RenderNode::Primitive(PrimitiveEntry {
18019 phase: PrimitivePhase::BeforeChildren,
18020 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
18021 node_id: 77,
18022 rect: Rect {
18023 x: 2.0,
18024 y: 3.0,
18025 width: 48.0,
18026 height: 18.0,
18027 },
18028 text: std::rc::Rc::new(AnnotatedString::from("runtime cache")),
18029 text_style: TextStyle::default(),
18030 font_size: 14.0,
18031 layout_options: TextLayoutOptions::default(),
18032 clip: None,
18033 })),
18034 });
18035 let mut layer = test_layer(
18036 Rect {
18037 x: 0.0,
18038 y: 0.0,
18039 width: 64.0,
18040 height: 32.0,
18041 },
18042 vec![text],
18043 );
18044 layer.node_id = Some(77);
18045 layer.recompute_raster_cache_hashes();
18046
18047 assert!(
18048 layer_raster_cache_candidate(&layer, 1.0, false, false).is_none(),
18049 "root path should not isolate plain translation-only text layers"
18050 );
18051 assert!(
18052 layer_raster_cache_candidate(&layer, 1.0, false, true).is_none(),
18053 "child path should also render plain translation-only text layers directly"
18054 );
18055 }
18056
18057 #[test]
18058 fn layer_raster_cache_candidate_allows_stable_runtime_child_effect_surfaces() {
18059 let mut layer = test_layer(
18060 Rect {
18061 x: 0.0,
18062 y: 0.0,
18063 width: 64.0,
18064 height: 32.0,
18065 },
18066 vec![RenderNode::Primitive(PrimitiveEntry {
18067 phase: PrimitivePhase::BeforeChildren,
18068 node: PrimitiveNode::Draw(DrawPrimitiveNode {
18069 primitive: DrawPrimitive::Rect {
18070 rect: Rect {
18071 x: 0.0,
18072 y: 0.0,
18073 width: 64.0,
18074 height: 32.0,
18075 },
18076 brush: Brush::solid(Color::WHITE),
18077 stroke: None,
18078 },
18079 clip: None,
18080 }),
18081 })],
18082 );
18083 layer.node_id = Some(78);
18084 layer.graphics_layer.render_effect = Some(RenderEffect::blur(4.0));
18085 layer.recompute_raster_cache_hashes();
18086
18087 assert!(
18088 layer_raster_cache_candidate(&layer, 1.0, false, false).is_none(),
18089 "root direct path should not force-cache ordinary stable effects"
18090 );
18091 assert!(
18092 layer_raster_cache_candidate(&layer, 1.0, false, true).is_some(),
18093 "child surface rendering should retain stable non-runtime effects"
18094 );
18095 }
18096
18097 #[test]
18098 fn layer_raster_cache_candidate_rejects_runtime_shader_child_effect_surfaces() {
18099 let mut layer = test_layer(
18100 Rect {
18101 x: 0.0,
18102 y: 0.0,
18103 width: 64.0,
18104 height: 32.0,
18105 },
18106 vec![],
18107 );
18108 layer.node_id = Some(79);
18109 layer.graphics_layer.render_effect = Some(RenderEffect::runtime_shader(
18110 RuntimeShader::new("runtime shader"),
18111 ));
18112 layer.recompute_raster_cache_hashes();
18113
18114 assert!(
18115 layer_raster_cache_candidate(&layer, 1.0, false, true).is_none(),
18116 "runtime shaders must not fill the retained layer cache with per-frame uniform variants"
18117 );
18118 }
18119
18120 #[test]
18121 fn layer_surface_requirements_keep_plain_text_on_direct_path() {
18122 let layer = text_layer_with_style(AnnotatedString::from("plain"), TextStyle::default());
18123
18124 let requirements = layer_surface_requirements(&layer);
18125
18126 assert_eq!(requirements.direct_translation, Some(Point::default()));
18127 assert!(requirements
18128 .surface_requirements
18129 .contains(SurfaceRequirement::PixelStableComposite));
18130 assert!(!requirements
18131 .surface_requirements
18132 .has_isolating_requirement());
18133 }
18134
18135 #[test]
18136 fn layer_surface_requirements_keep_translated_plain_text_leaf_on_direct_path() {
18137 let layer = pure_text_leaf(false, true);
18138
18139 let requirements = layer_surface_requirements(&layer);
18140
18141 assert_eq!(
18142 requirements.direct_translation,
18143 Some(Point::new(11.4, 23.6))
18144 );
18145 assert!(
18146 requirements
18147 .surface_requirements
18148 .contains(SurfaceRequirement::PixelStableComposite)
18149 && !requirements
18150 .surface_requirements
18151 .has_isolating_requirement(),
18152 "translated plain text should stay on the direct path and isolate only the glyph draw"
18153 );
18154 }
18155
18156 #[test]
18157 fn layer_surface_requirements_keep_translated_text_leaf_with_background_on_direct_path() {
18158 let layer = snapped_text_leaf(false, true);
18159
18160 let requirements = layer_surface_requirements(&layer);
18161
18162 assert_eq!(
18163 requirements.direct_translation,
18164 Some(Point::new(14.25, 16.5))
18165 );
18166 assert!(
18167 requirements
18168 .surface_requirements
18169 .contains(SurfaceRequirement::PixelStableComposite)
18170 && !requirements
18171 .surface_requirements
18172 .has_isolating_requirement(),
18173 "translated text with direct sibling decoration/background should keep the layer direct"
18174 );
18175 }
18176
18177 #[test]
18178 fn translated_plain_text_uses_bounded_snap_surface() {
18179 let root = pure_text_leaf_root(true, true);
18180 let mut rect_cache = HashMap::new();
18181 let mut requirements_cache = HashMap::new();
18182 let collected =
18183 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
18184
18185 assert_eq!(collected.child_layers.len(), 1);
18186 assert!(collected.scene.texts.is_empty());
18187 assert!(collected.scene.effect_layers.is_empty());
18188 assert_snap_anchor_close(
18189 collected.child_layers[0].snap_anchor,
18190 Point::new(11.4, 23.6),
18191 "translated plain text's bounded local surface should composite at the content-origin snap phase",
18192 );
18193 }
18194
18195 #[test]
18199 #[ignore]
18200 fn shape_run_collect_timing_harness() {
18201 use cranpose_render_common::graph::DrawPrimitiveNode;
18202 use cranpose_render_common::layer_composition::local_content_layer_for;
18203 use cranpose_ui_graphics::Stroke;
18204
18205 let bounds = Rect {
18206 x: 0.0,
18207 y: 0.0,
18208 width: 1080.0,
18209 height: 2244.0,
18210 };
18211 let graphics_layer = GraphicsLayer::default();
18212
18213 let mut nodes: Vec<DrawPrimitiveNode> = Vec::new();
18216 for i in 0..3000u32 {
18217 let f = i as f32;
18218 let brush = if i % 8 == 0 {
18219 Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])
18220 } else {
18221 Brush::Solid(Color(0.5, 0.2, 0.8, 1.0))
18222 };
18223 let center = Point::new(540.0 + (f % 400.0), 1122.0 + (f % 350.0));
18224 let radius = 8.0 + (i % 23) as f32;
18225 let half = radius + 4.0;
18226 nodes.push(DrawPrimitiveNode {
18227 primitive: DrawPrimitive::Arc {
18228 rect: Rect {
18229 x: center.x - half,
18230 y: center.y - half,
18231 width: half * 2.0,
18232 height: half * 2.0,
18233 },
18234 brush,
18235 center,
18236 radius,
18237 start_angle: f * 0.07,
18238 sweep_angle: 0.5 + (i % 5) as f32,
18239 stroke: (i % 3 != 0).then(|| Stroke::new(4.0)),
18240 inner_radius: if i % 3 == 0 { radius * 0.6 } else { 0.0 },
18241 },
18242 clip: None,
18243 });
18244 }
18245
18246 let children: Vec<RenderNode> = nodes
18247 .iter()
18248 .map(|node| {
18249 RenderNode::Primitive(PrimitiveEntry {
18250 phase: PrimitivePhase::BeforeChildren,
18251 node: PrimitiveNode::Draw(node.clone()),
18252 })
18253 })
18254 .collect();
18255 let layer = crate::test_support::layer_node(
18256 bounds,
18257 ProjectiveTransform::identity(),
18258 graphics_layer,
18259 children,
18260 );
18261
18262 const ITERS: usize = 300;
18263
18264 let local_layer = local_content_layer_for(&layer.graphics_layer);
18266 let start = Instant::now();
18267 let mut sink_shapes = 0usize;
18268 for _ in 0..ITERS {
18269 let mut scene = CompositorScene::new();
18270 for node in &nodes {
18271 crate::pipeline::push_draw_primitive(
18272 &node.primitive,
18273 bounds,
18274 &local_layer,
18275 None,
18276 &mut scene,
18277 None,
18278 false,
18279 );
18280 }
18281 sink_shapes = scene.shapes.len();
18282 }
18283 let serial = start.elapsed();
18284
18285 let mut rect_cache = HashMap::new();
18286 let mut requirements_cache = HashMap::new();
18287 let start = Instant::now();
18288 let mut run_shapes = 0usize;
18289 for _ in 0..ITERS {
18290 let collected = collect_layer_contents(
18291 &layer,
18292 None,
18293 None,
18294 &mut rect_cache,
18295 &mut requirements_cache,
18296 );
18297 run_shapes = collected.scene.shapes.len();
18298 }
18299 let run = start.elapsed();
18300
18301 println!(
18302 "per-primitive: {:?}/iter ({sink_shapes} shapes) shape-run: {:?}/iter ({run_shapes} shapes)",
18303 serial / ITERS as u32,
18304 run / ITERS as u32,
18305 );
18306 }
18307
18308 fn assert_shape_run_collect_matches_per_primitive_emission() {
18310 use cranpose_render_common::graph::DrawPrimitiveNode;
18311 use cranpose_render_common::layer_composition::local_content_layer_for;
18312 use cranpose_render_common::primitive_emit::{resolve_primitive_clip, PrimitiveClipSpace};
18313 use cranpose_ui_graphics::{CornerRadii, Stroke};
18314
18315 let bounds = Rect {
18316 x: 0.0,
18317 y: 0.0,
18318 width: 800.0,
18319 height: 800.0,
18320 };
18321 let graphics_layer = GraphicsLayer {
18324 scale: 1.25,
18325 translation_x: 3.5,
18326 translation_y: -2.0,
18327 alpha: 0.9,
18328 rotation_z: 0.35,
18329 ..GraphicsLayer::default()
18330 };
18331
18332 let mut nodes: Vec<DrawPrimitiveNode> = Vec::new();
18333 for i in 0..600u32 {
18334 let f = i as f32;
18335 let brush = if i % 11 == 0 {
18336 Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])
18337 } else {
18338 Brush::Solid(Color(0.1 + (i % 7) as f32 * 0.1, 0.5, 0.9, 1.0))
18339 };
18340 let stroke = (i % 5 == 0).then(|| Stroke::new(1.0 + (i % 3) as f32));
18341 let primitive = match i % 3 {
18342 0 => DrawPrimitive::Rect {
18343 rect: Rect {
18344 x: f % 37.0,
18345 y: f % 53.0,
18346 width: 8.0 + f % 9.0,
18347 height: 6.0 + f % 5.0,
18348 },
18349 brush,
18350 stroke,
18351 },
18352 1 => DrawPrimitive::RoundRect {
18353 rect: Rect {
18354 x: f % 41.0,
18355 y: f % 43.0,
18356 width: 12.0,
18357 height: 10.0,
18358 },
18359 brush,
18360 radii: CornerRadii::uniform(2.0 + (i % 4) as f32),
18361 stroke,
18362 },
18363 _ => {
18364 let center = Point::new(60.0 + f % 71.0, 60.0 + f % 67.0);
18365 let radius = 5.0 + (i % 13) as f32;
18366 let sweep_angle = if i == 302 { 0.0 } else { 0.4 + (i % 6) as f32 };
18368 let half = radius + 4.0;
18369 DrawPrimitive::Arc {
18370 rect: Rect {
18371 x: center.x - half,
18372 y: center.y - half,
18373 width: half * 2.0,
18374 height: half * 2.0,
18375 },
18376 brush,
18377 center,
18378 radius,
18379 start_angle: f * 0.11,
18380 sweep_angle,
18381 stroke: (i % 2 == 0).then(|| Stroke::new(3.0)),
18382 inner_radius: if i % 4 == 2 { radius * 0.5 } else { 0.0 },
18383 }
18384 }
18385 };
18386 let primitive = if i == 300 {
18387 DrawPrimitive::Blend {
18391 primitive: Box::new(DrawPrimitive::Blend {
18392 primitive: Box::new(primitive),
18393 blend_mode: BlendMode::SrcOver,
18394 }),
18395 blend_mode: BlendMode::DstOut,
18396 }
18397 } else if i % 7 == 3 {
18398 DrawPrimitive::Blend {
18399 primitive: Box::new(primitive),
18400 blend_mode: BlendMode::DstOut,
18401 }
18402 } else {
18403 primitive
18404 };
18405 let clip = (i % 31 == 7).then_some(Rect {
18406 x: 0.0,
18407 y: 0.0,
18408 width: 30.0,
18409 height: 30.0,
18410 });
18411 nodes.push(DrawPrimitiveNode { primitive, clip });
18412 }
18413
18414 let children: Vec<RenderNode> = nodes
18415 .iter()
18416 .map(|node| {
18417 RenderNode::Primitive(PrimitiveEntry {
18418 phase: PrimitivePhase::BeforeChildren,
18419 node: PrimitiveNode::Draw(node.clone()),
18420 })
18421 })
18422 .collect();
18423 let layer = crate::test_support::layer_node(
18424 bounds,
18425 ProjectiveTransform::identity(),
18426 graphics_layer,
18427 children,
18428 );
18429
18430 let mut rect_cache = HashMap::new();
18431 let mut requirements_cache = HashMap::new();
18432 let collected =
18433 collect_layer_contents(&layer, None, None, &mut rect_cache, &mut requirements_cache);
18434
18435 let local_layer = local_content_layer_for(&layer.graphics_layer);
18438 let mut expected = CompositorScene::new();
18439 for node in &nodes {
18440 let clip = resolve_primitive_clip(
18441 node.clip,
18442 bounds,
18443 &local_layer,
18444 None,
18445 PrimitiveClipSpace::Local,
18446 );
18447 if node.clip.is_some() && clip.is_none() {
18448 continue;
18449 }
18450 crate::pipeline::push_draw_primitive(
18451 &node.primitive,
18452 bounds,
18453 &local_layer,
18454 clip,
18455 &mut expected,
18456 None,
18457 false,
18458 );
18459 }
18460
18461 assert!(
18462 collected.scene.shapes.len() >= 590,
18463 "the runs should engage the parallel branch: got {} shapes",
18464 collected.scene.shapes.len()
18465 );
18466 assert_eq!(collected.scene.shapes.len(), expected.shapes.len());
18467 assert_eq!(collected.scene.draw_ops, expected.draw_ops);
18468 assert_eq!(collected.scene.next_z, expected.next_z);
18469 assert!(
18470 collected
18471 .scene
18472 .shapes
18473 .iter()
18474 .all(|s| s.snap_anchor.is_none()),
18475 "a rotated layer must not rigid-snap; the reference scene assumes it"
18476 );
18477 for (index, (got, want)) in collected
18478 .scene
18479 .shapes
18480 .iter()
18481 .zip(&expected.shapes)
18482 .enumerate()
18483 {
18484 assert_eq!(got.rect, want.rect, "shape {index} rect");
18485 assert_eq!(got.local_rect, want.local_rect, "shape {index} local_rect");
18486 assert_eq!(got.quad, want.quad, "shape {index} quad");
18487 assert_eq!(got.snap_anchor, want.snap_anchor, "shape {index} snap");
18488 assert_eq!(got.brush, want.brush, "shape {index} brush");
18489 assert_eq!(got.shape, want.shape, "shape {index} shape");
18490 assert_eq!(got.stroke, want.stroke, "shape {index} stroke");
18491 assert_eq!(got.arc, want.arc, "shape {index} arc");
18492 assert_eq!(got.z_index, want.z_index, "shape {index} z");
18493 assert_eq!(got.clip, want.clip, "shape {index} clip");
18494 assert_eq!(got.blend_mode, want.blend_mode, "shape {index} blend");
18495 assert_eq!(
18496 got.motion_context_animated, want.motion_context_animated,
18497 "shape {index} motion flag"
18498 );
18499 }
18500 }
18501
18502 #[test]
18507 fn shape_run_collect_matches_per_primitive_emission_exactly() {
18508 assert_shape_run_collect_matches_per_primitive_emission();
18509 crate::normalized_scene::force_shape_run_parallel_for_tests(true);
18510 let outcome =
18511 std::panic::catch_unwind(assert_shape_run_collect_matches_per_primitive_emission);
18512 crate::normalized_scene::force_shape_run_parallel_for_tests(false);
18513 if let Err(payload) = outcome {
18514 std::panic::resume_unwind(payload);
18515 }
18516 }
18517
18518 #[test]
18519 fn non_translated_text_local_surface_keeps_linear_composite_resolve() {
18520 let layer = text_layer_with_style(
18521 AnnotatedString::from("gradient"),
18522 TextStyle::from_span_style(SpanStyle {
18523 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
18524 ..SpanStyle::default()
18525 }),
18526 );
18527 let requirements = layer_surface_requirements(&layer);
18528
18529 assert!(requirements
18530 .surface_requirements
18531 .contains(SurfaceRequirement::TextMaterialMask));
18532 assert_eq!(
18533 composite_sample_mode_for_requirements(false, false, requirements),
18534 CompositeSampleMode::Linear
18535 );
18536 }
18537
18538 #[test]
18539 fn inherited_translated_text_local_surface_uses_box4_layer_surface() {
18540 let layer = text_layer_with_style(
18541 AnnotatedString::from("shadow"),
18542 TextStyle::from_span_style(SpanStyle {
18543 shadow: Some(Shadow {
18544 color: Color::BLACK,
18545 offset: Point::new(1.0, 2.0),
18546 blur_radius: 3.0,
18547 }),
18548 ..SpanStyle::default()
18549 }),
18550 );
18551 let requirements = layer_surface_requirements(&layer);
18552
18553 assert!(requirements
18554 .surface_requirements
18555 .contains(SurfaceRequirement::TextMaterialMask));
18556 assert_eq!(
18557 composite_sample_mode_for_requirements(true, false, requirements),
18558 CompositeSampleMode::Box4
18559 );
18560 assert_eq!(
18561 layer_surface_target_scale(
18562 true,
18563 false,
18564 requirements,
18565 1.25,
18566 layer_surface_scale(&layer)
18567 ),
18568 SurfaceRequirementSet::default()
18569 .with(SurfaceRequirement::TextMaterialMask)
18570 .with(SurfaceRequirement::MotionStableCapture)
18571 .target_scale(1.25, 1.0)
18572 );
18573 }
18574
18575 #[test]
18576 fn translated_text_local_surface_inside_capture_keeps_parent_scale() {
18577 let layer = text_layer_with_style(
18578 AnnotatedString::from("shadow"),
18579 TextStyle::from_span_style(SpanStyle {
18580 shadow: Some(Shadow {
18581 color: Color::BLACK,
18582 offset: Point::new(1.0, 2.0),
18583 blur_radius: 3.0,
18584 }),
18585 ..SpanStyle::default()
18586 }),
18587 );
18588 let requirements = layer_surface_requirements(&layer);
18589
18590 assert_eq!(
18591 composite_sample_mode_for_requirements(true, true, requirements),
18592 CompositeSampleMode::Linear
18593 );
18594 assert_eq!(
18595 layer_surface_target_scale(true, true, requirements, 10.0, layer_surface_scale(&layer)),
18596 SurfaceRequirementSet::default()
18597 .with(SurfaceRequirement::TextMaterialMask)
18598 .target_scale(10.0, 1.0)
18599 );
18600 }
18601
18602 #[test]
18603 fn layer_surface_requirements_use_local_surface_for_gradient_and_stroke_text() {
18604 let cases = [
18605 (
18606 "draw_style",
18607 AnnotatedString::from("draw_style"),
18608 TextStyle::from_span_style(SpanStyle {
18609 draw_style: Some(TextDrawStyle::Stroke { width: 2.0 }),
18610 ..SpanStyle::default()
18611 }),
18612 ),
18613 (
18614 "gradient_brush",
18615 AnnotatedString::from("gradient"),
18616 TextStyle::from_span_style(SpanStyle {
18617 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
18618 ..SpanStyle::default()
18619 }),
18620 ),
18621 ];
18622
18623 for (label, text, text_style) in cases {
18624 let layer = text_layer_with_style(text, text_style);
18625 let requirements = layer_surface_requirements(&layer);
18626 assert!(
18627 requirements
18628 .surface_requirements
18629 .contains(SurfaceRequirement::TextMaterialMask),
18630 "{label} text should use a bounded local surface: {requirements:?}"
18631 );
18632 }
18633 }
18634
18635 #[test]
18636 fn layer_surface_requirements_use_local_surface_for_complex_text_effects() {
18637 let cases = [
18638 (
18639 "shadow",
18640 AnnotatedString::from("shadow"),
18641 TextStyle::from_span_style(SpanStyle {
18642 shadow: Some(Shadow {
18643 color: Color::BLACK,
18644 offset: Point::new(1.0, 2.0),
18645 blur_radius: 3.0,
18646 }),
18647 ..SpanStyle::default()
18648 }),
18649 ),
18650 (
18651 "background",
18652 AnnotatedString::from("background"),
18653 TextStyle::from_span_style(SpanStyle {
18654 background: Some(Color::BLACK),
18655 ..SpanStyle::default()
18656 }),
18657 ),
18658 (
18659 "baseline_shift",
18660 AnnotatedString::from("baseline_shift"),
18661 TextStyle::from_span_style(SpanStyle {
18662 baseline_shift: Some(BaselineShift::SUPERSCRIPT),
18663 ..SpanStyle::default()
18664 }),
18665 ),
18666 (
18667 "geometric_transform",
18668 AnnotatedString::from("geometric_transform"),
18669 TextStyle::from_span_style(SpanStyle {
18670 text_geometric_transform: Some(TextGeometricTransform {
18671 scale_x: 1.2,
18672 skew_x: 0.15,
18673 }),
18674 ..SpanStyle::default()
18675 }),
18676 ),
18677 (
18678 "letter_spacing",
18679 AnnotatedString::from("letter_spacing"),
18680 TextStyle::from_span_style(SpanStyle {
18681 letter_spacing: TextUnit::Em(0.2),
18682 ..SpanStyle::default()
18683 }),
18684 ),
18685 ];
18686
18687 for (label, text, text_style) in cases {
18688 let layer = text_layer_with_style(text, text_style);
18689 let requirements = layer_surface_requirements(&layer);
18690 assert!(
18691 requirements
18692 .surface_requirements
18693 .contains(SurfaceRequirement::TextMaterialMask),
18694 "{label} text should use a bounded local surface: {requirements:?}"
18695 );
18696 assert_eq!(
18697 requirements.direct_translation,
18698 Some(Point::default()),
18699 "{label} text should still classify as a direct translation"
18700 );
18701 }
18702 }
18703
18704 #[test]
18705 fn layer_surface_requirements_color_only_span_styles_use_direct_path() {
18706 let layer = text_layer_with_style(
18707 AnnotatedString {
18708 text: "styled".to_string(),
18709 span_styles: vec![RangeStyle {
18710 item: SpanStyle {
18711 color: Some(Color::BLACK),
18712 ..SpanStyle::default()
18713 },
18714 range: 0..3,
18715 }],
18716 ..AnnotatedString::default()
18717 },
18718 TextStyle::default(),
18719 );
18720 let requirements = layer_surface_requirements(&layer);
18721 assert!(
18722 !requirements
18723 .surface_requirements
18724 .contains(SurfaceRequirement::TextMaterialMask),
18725 "color-only span styles should render directly via software text raster colors"
18726 );
18727 }
18728
18729 #[test]
18730 fn layer_surface_requirements_keep_decoration_only_text_on_direct_path() {
18731 let layer = text_layer_with_style(
18732 AnnotatedString::from("decoration"),
18733 TextStyle::from_span_style(SpanStyle {
18734 text_decoration: Some(TextDecoration::UNDERLINE),
18735 ..SpanStyle::default()
18736 }),
18737 );
18738
18739 let requirements = layer_surface_requirements(&layer);
18740
18741 assert_eq!(requirements.direct_translation, Some(Point::default()));
18742 assert!(
18743 requirements
18744 .surface_requirements
18745 .contains(SurfaceRequirement::PixelStableComposite)
18746 && !requirements
18747 .surface_requirements
18748 .has_isolating_requirement(),
18749 "decoration-only text should not force an isolating layer surface: {requirements:?}"
18750 );
18751 }
18752
18753 #[test]
18754 fn direct_text_leaf_snaps_modifier_background_and_text_with_one_anchor() {
18755 let root = snapped_text_leaf_root(false, false);
18756 let mut rect_cache = HashMap::new();
18757 let mut requirements_cache = HashMap::new();
18758
18759 let collected =
18760 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
18761
18762 assert_eq!(collected.scene.shapes.len(), 1);
18763 assert_eq!(collected.scene.images.len(), 1);
18764 assert_eq!(collected.scene.texts.len(), 1);
18765 let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
18766 assert_eq!(collected.scene.shapes[0].snap_anchor, expected_anchor);
18767 assert_eq!(collected.scene.images[0].snap_anchor, expected_anchor);
18768 assert_eq!(collected.scene.texts[0].snap_anchor, expected_anchor);
18769 }
18770
18771 #[test]
18772 fn animated_translated_content_text_leaf_uses_bounded_content_snap() {
18773 let root = snapped_text_leaf_root(true, true);
18774 let mut rect_cache = HashMap::new();
18775 let mut requirements_cache = HashMap::new();
18776
18777 let collected =
18778 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
18779
18780 assert_eq!(collected.child_layers.len(), 1);
18781 assert!(collected.scene.shapes.is_empty());
18782 assert!(collected.scene.images.is_empty());
18783 assert!(collected.scene.texts.is_empty());
18784 assert!(collected.scene.effect_layers.is_empty());
18785 let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
18786 assert_eq!(
18787 collected.child_layers[0].snap_anchor, expected_anchor,
18788 "active translated leaf surface should keep the content-origin snap phase"
18789 );
18790 }
18791
18792 #[test]
18793 fn translated_content_assigns_motion_anchor_to_rotated_child_surface() {
18794 let mut child = snapped_text_leaf(false, false);
18795 child.graphics_layer.rotation_z = 5.0;
18796 child.transform_to_parent =
18797 cranpose_render_common::layer_transform::layer_transform_to_parent(
18798 child.local_bounds,
18799 Point::new(108.0, 3.0),
18800 &child.graphics_layer,
18801 );
18802 child.recompute_raster_cache_hashes();
18803 let mut root = test_layer(
18804 Rect {
18805 x: 0.0,
18806 y: 0.0,
18807 width: 320.0,
18808 height: 180.0,
18809 },
18810 vec![RenderNode::Layer(Box::new(child))],
18811 );
18812 root.translated_content_context = true;
18813 root.translated_content_offset = Point::new(0.0, -80.8);
18814 root.recompute_raster_cache_hashes();
18815 let mut rect_cache = HashMap::new();
18816 let mut requirements_cache = HashMap::new();
18817
18818 let collected =
18819 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
18820
18821 assert_eq!(collected.child_layers.len(), 1);
18822 assert!(
18823 collected.child_layers[0].snap_anchor.is_some(),
18824 "a projective child still translates rigidly with its scrolling parent"
18825 );
18826 }
18827
18828 #[test]
18829 fn rested_translated_content_context_text_leaf_snaps_for_crisp_scroll_rest() {
18830 let root = snapped_text_leaf_root(false, true);
18831 let mut rect_cache = HashMap::new();
18832 let mut requirements_cache = HashMap::new();
18833
18834 let collected =
18835 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
18836
18837 assert_eq!(collected.child_layers.len(), 0);
18838 assert_eq!(collected.scene.shapes.len(), 1);
18839 assert_eq!(collected.scene.images.len(), 1);
18840 assert_eq!(collected.scene.texts.len(), 1);
18841 assert_eq!(collected.scene.effect_layers.len(), 0);
18842 let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
18843 assert_eq!(
18844 collected.scene.shapes[0].snap_anchor, expected_anchor,
18845 "rested scroll content should snap back to device pixels"
18846 );
18847 assert_eq!(
18848 collected.scene.images[0].snap_anchor, expected_anchor,
18849 "rested scroll images should snap back to device pixels"
18850 );
18851 assert_eq!(
18852 collected.scene.texts[0].snap_anchor, expected_anchor,
18853 "rested scroll text should snap back to device pixels"
18854 );
18855 }
18856
18857 #[test]
18858 fn complex_text_uses_local_surface() {
18859 let root = translated_content_local_surface_root();
18860 let mut rect_cache = HashMap::new();
18861 let mut requirements_cache = HashMap::new();
18862
18863 let collected =
18864 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
18865
18866 assert!(
18867 !collected.child_layers.is_empty(),
18868 "translated-content effectful text should render through a bounded local surface"
18869 );
18870 assert!(collected.scene.texts.is_empty());
18871 assert!(collected.scene.shadow_draws.is_empty());
18872 }
18873
18874 #[test]
18875 fn translated_content_surface_composite_uses_scroll_content_snap_anchor() {
18876 let mut root = translated_content_local_surface_root();
18877 let scroll_offset = Point::new(0.0, -18.5);
18878 let Some(RenderNode::Layer(translated_content)) = root.children.get_mut(0) else {
18879 panic!("expected translated content layer");
18880 };
18881 translated_content.translated_content_offset = scroll_offset;
18882 let Some(RenderNode::Layer(effectful_text)) = translated_content.children.get_mut(0) else {
18883 panic!("expected effectful text layer");
18884 };
18885 effectful_text.transform_to_parent =
18886 effectful_text
18887 .transform_to_parent
18888 .then(ProjectiveTransform::translation(
18889 scroll_offset.x,
18890 scroll_offset.y,
18891 ));
18892
18893 let mut rect_cache = HashMap::new();
18894 let mut requirements_cache = HashMap::new();
18895 let collected =
18896 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
18897
18898 assert_eq!(collected.child_layers.len(), 1);
18899 assert_eq!(
18900 collected.child_layers[0].snap_anchor,
18901 Some(SnapAnchor::rigid(Point::new(14.25, -2.0))),
18902 "isolated scrolled descendants must composite with the same content-origin snap phase"
18903 );
18904 }
18905
18906 #[test]
18907 fn animated_translated_content_surface_composite_uses_scroll_content_snap_anchor() {
18908 let mut root = translated_content_local_surface_root();
18909 let scroll_offset = Point::new(0.0, -18.5);
18910 let Some(RenderNode::Layer(translated_content)) = root.children.get_mut(0) else {
18911 panic!("expected translated content layer");
18912 };
18913 translated_content.motion_context_animated = true;
18914 translated_content.translated_content_offset = scroll_offset;
18915 let Some(RenderNode::Layer(effectful_text)) = translated_content.children.get_mut(0) else {
18916 panic!("expected effectful text layer");
18917 };
18918 effectful_text.transform_to_parent =
18919 effectful_text
18920 .transform_to_parent
18921 .then(ProjectiveTransform::translation(
18922 scroll_offset.x,
18923 scroll_offset.y,
18924 ));
18925
18926 let mut rect_cache = HashMap::new();
18927 let mut requirements_cache = HashMap::new();
18928 let collected =
18929 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
18930
18931 assert_eq!(collected.child_layers.len(), 1);
18932 assert_eq!(
18933 collected.child_layers[0].snap_anchor,
18934 Some(SnapAnchor::rigid(Point::new(14.25, 16.5))),
18935 "animated translated content should composite the stable local surface at the viewport-origin snap phase"
18936 );
18937 }
18938
18939 #[test]
18940 fn translated_text_material_effect_layer_uses_scroll_content_snap_anchor() {
18941 let mut layer = text_layer_with_style(
18942 AnnotatedString::from("gradient"),
18943 TextStyle::from_span_style(SpanStyle {
18944 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
18945 ..SpanStyle::default()
18946 }),
18947 );
18948 layer.translated_content_context = true;
18949 layer.translated_content_offset = Point::new(0.0, -18.5);
18950 let mut rect_cache = HashMap::new();
18951 let mut requirements_cache = HashMap::new();
18952
18953 let collected =
18954 collect_layer_contents(&layer, None, None, &mut rect_cache, &mut requirements_cache);
18955
18956 assert_eq!(collected.scene.effect_layers.len(), 1);
18957 assert_eq!(
18958 composite_sample_mode_for_effect_layer(&collected.scene.effect_layers[0]),
18959 CompositeSampleMode::Box4
18960 );
18961 assert_eq!(
18962 collected.scene.effect_layers[0].snap_anchor,
18963 Some(SnapAnchor::rigid(Point::new(0.0, -18.5))),
18964 "text material surfaces must composite with the scroll content-origin snap phase"
18965 );
18966 }
18967
18968 #[test]
18969 fn translated_layer_surface_capture_does_not_restart_local_picture_for_shadow_text() {
18970 let mut layer = text_layer_with_style(
18971 AnnotatedString::from("shadow"),
18972 TextStyle::from_span_style(SpanStyle {
18973 shadow: Some(Shadow {
18974 color: Color::BLACK,
18975 offset: Point::new(1.0, 2.0),
18976 blur_radius: 3.0,
18977 }),
18978 ..SpanStyle::default()
18979 }),
18980 );
18981 layer.translated_content_context = true;
18982 let mut rect_cache = HashMap::new();
18983 let mut requirements_cache = HashMap::new();
18984
18985 let collected = collect_layer_contents_with_translation_context(
18986 &layer,
18987 None,
18988 None,
18989 TranslationRenderContext {
18990 inherited_content_translation: false,
18991 surface_capture_active: true,
18992 local_picture_capture_active: true,
18993 ..TranslationRenderContext::default()
18994 },
18995 &mut rect_cache,
18996 &mut requirements_cache,
18997 );
18998
18999 assert!(
19000 collected.scene.effect_layers.is_empty(),
19001 "a translated layer surface already provides the stable local capture"
19002 );
19003 assert_eq!(collected.scene.shadow_draws.len(), 1);
19004 assert_eq!(collected.scene.texts.len(), 1);
19005 assert!(
19006 !collected.scene.texts[0].translated_content_context,
19007 "text inside an active motion-stable capture must raster in capture-local coordinates"
19008 );
19009 }
19010
19011 #[test]
19012 fn translated_layer_surface_capture_keeps_only_material_effect_layers() {
19013 let mut layer = text_layer_with_style(
19014 AnnotatedString::from("gradient"),
19015 TextStyle::from_span_style(SpanStyle {
19016 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
19017 ..SpanStyle::default()
19018 }),
19019 );
19020 layer.translated_content_context = true;
19021 let mut rect_cache = HashMap::new();
19022 let mut requirements_cache = HashMap::new();
19023
19024 let collected = collect_layer_contents_with_translation_context(
19025 &layer,
19026 None,
19027 None,
19028 TranslationRenderContext {
19029 inherited_content_translation: false,
19030 surface_capture_active: true,
19031 local_picture_capture_active: true,
19032 ..TranslationRenderContext::default()
19033 },
19034 &mut rect_cache,
19035 &mut requirements_cache,
19036 );
19037
19038 assert_eq!(collected.scene.effect_layers.len(), 1);
19039 assert!(
19040 collected.scene.effect_layers[0]
19041 .requirements
19042 .contains(SurfaceRequirement::MotionStableCapture),
19043 "translated text materials still need motion-stable resolve semantics inside a stable capture"
19044 );
19045 assert_eq!(
19046 composite_sample_mode_for_effect_layer(&collected.scene.effect_layers[0]),
19047 CompositeSampleMode::Box4
19048 );
19049 assert_eq!(
19050 effect_layer_target_scale(&collected.scene.effect_layers[0], 10.0),
19051 10.0
19052 );
19053 assert!(collected.scene.effect_layers[0].effect.is_some());
19054 }
19055
19056 #[test]
19057 fn translated_viewport_surface_does_not_add_plain_local_picture_capture() {
19058 let mut layer = text_layer_with_style(
19059 AnnotatedString::from("shadow"),
19060 TextStyle::from_span_style(SpanStyle {
19061 shadow: Some(Shadow {
19062 color: Color::BLACK,
19063 offset: Point::new(1.0, 2.0),
19064 blur_radius: 3.0,
19065 }),
19066 ..SpanStyle::default()
19067 }),
19068 );
19069 layer.translated_content_context = true;
19070 layer.motion_context_animated = true;
19071 let mut rect_cache = HashMap::new();
19072 let mut requirements_cache = HashMap::new();
19073
19074 let collected = collect_layer_contents_with_translation_context(
19075 &layer,
19076 None,
19077 None,
19078 TranslationRenderContext {
19079 surface_capture_active: true,
19080 ..TranslationRenderContext::default()
19081 },
19082 &mut rect_cache,
19083 &mut requirements_cache,
19084 );
19085
19086 assert_eq!(
19087 collected.scene.effect_layers.len(),
19088 0,
19089 "plain translated content inside a viewport surface should not be captured again"
19090 );
19091 assert_eq!(collected.scene.shadow_draws.len(), 1);
19092 assert_eq!(collected.scene.texts.len(), 1);
19093 }
19094
19095 #[test]
19096 fn static_pure_text_leaf_snaps_without_sibling_draw_primitives() {
19097 let root = pure_text_leaf_root(false, false);
19098 let mut rect_cache = HashMap::new();
19099 let mut requirements_cache = HashMap::new();
19100
19101 let collected =
19102 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
19103
19104 assert_eq!(collected.scene.texts.len(), 1);
19105 assert!(
19106 collected.scene.texts[0].snap_anchor.is_some(),
19107 "idle pure text leaves should participate in rigid snap anchoring"
19108 );
19109 }
19110
19111 #[test]
19112 fn animated_pure_text_leaf_stays_unsnapped() {
19113 let root = pure_text_leaf_root(true, false);
19114 let mut rect_cache = HashMap::new();
19115 let mut requirements_cache = HashMap::new();
19116
19117 let collected =
19118 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
19119
19120 assert_eq!(collected.scene.texts.len(), 1);
19121 assert_eq!(collected.scene.texts[0].snap_anchor, None);
19122 }
19123
19124 #[test]
19125 fn animated_translated_pure_text_uses_bounded_content_snap() {
19126 let root = pure_text_leaf_root(true, true);
19127 let mut rect_cache = HashMap::new();
19128 let mut requirements_cache = HashMap::new();
19129
19130 let collected =
19131 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
19132
19133 assert_eq!(collected.child_layers.len(), 1);
19134 assert!(collected.scene.texts.is_empty());
19135 assert!(collected.scene.effect_layers.is_empty());
19136 assert_snap_anchor_close(
19137 collected.child_layers[0].snap_anchor,
19138 Point::new(11.4, 23.6),
19139 "animated translated pure text should use the bounded content snap phase",
19140 );
19141 }
19142
19143 #[test]
19144 fn rested_translated_pure_text_leaf_snaps_for_crisp_scroll_rest() {
19145 let root = pure_text_leaf_root(false, true);
19146 let mut rect_cache = HashMap::new();
19147 let mut requirements_cache = HashMap::new();
19148
19149 let collected =
19150 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
19151
19152 assert_eq!(collected.child_layers.len(), 0);
19153 assert_eq!(collected.scene.texts.len(), 1);
19154 assert_eq!(collected.scene.effect_layers.len(), 0);
19155 assert_snap_anchor_close(
19156 collected.scene.texts[0].snap_anchor,
19157 Point::new(11.4, 23.6),
19158 "rested translated text should snap to device pixels",
19159 );
19160 }
19161
19162 #[test]
19163 fn static_gpu_effect_text_leaf_stays_unsnapped() {
19164 let root = text_layer_with_style(
19165 AnnotatedString::from("Gradient"),
19166 TextStyle::from_span_style(SpanStyle {
19167 brush: Some(Brush::linear_gradient(vec![
19168 Color(0.2, 0.8, 1.0, 1.0),
19169 Color(1.0, 0.7, 0.4, 1.0),
19170 ])),
19171 draw_style: Some(TextDrawStyle::Stroke { width: 2.5 }),
19172 ..SpanStyle::default()
19173 }),
19174 );
19175 let mut rect_cache = HashMap::new();
19176 let mut requirements_cache = HashMap::new();
19177
19178 let collected =
19179 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
19180
19181 assert_eq!(collected.scene.texts.len(), 1);
19182 assert_eq!(
19183 collected.scene.texts[0].snap_anchor, None,
19184 "gpu text-effect leaves must not take the rigid text snap path"
19185 );
19186 assert_eq!(
19187 collected.scene.effect_layers.len(),
19188 1,
19189 "gradient stroke text should still emit a runtime shader effect layer"
19190 );
19191 }
19192
19193 #[test]
19194 fn layer_surface_requirements_keep_shape_plus_direct_child_on_direct_path() {
19195 let mut child = test_layer(
19196 Rect {
19197 x: 0.0,
19198 y: 0.0,
19199 width: 40.0,
19200 height: 20.0,
19201 },
19202 vec![RenderNode::Primitive(PrimitiveEntry {
19203 phase: PrimitivePhase::BeforeChildren,
19204 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19205 primitive: DrawPrimitive::Rect {
19206 rect: Rect {
19207 x: 0.0,
19208 y: 0.0,
19209 width: 40.0,
19210 height: 20.0,
19211 },
19212 brush: Brush::solid(Color::WHITE),
19213 stroke: None,
19214 },
19215 clip: None,
19216 }),
19217 })],
19218 );
19219 child.transform_to_parent = ProjectiveTransform::translation(8.0, 6.0);
19220
19221 let layer = test_layer(
19222 Rect {
19223 x: 0.0,
19224 y: 0.0,
19225 width: 64.0,
19226 height: 32.0,
19227 },
19228 vec![
19229 RenderNode::Primitive(PrimitiveEntry {
19230 phase: PrimitivePhase::BeforeChildren,
19231 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19232 primitive: DrawPrimitive::Rect {
19233 rect: Rect {
19234 x: 0.0,
19235 y: 0.0,
19236 width: 64.0,
19237 height: 32.0,
19238 },
19239 brush: Brush::solid(Color::BLACK),
19240 stroke: None,
19241 },
19242 clip: None,
19243 }),
19244 }),
19245 RenderNode::Layer(Box::new(child)),
19246 ],
19247 );
19248
19249 let requirements = layer_surface_requirements(&layer);
19250
19251 assert_eq!(requirements.direct_translation, Some(Point::default()));
19252 assert!(!requirements
19253 .surface_requirements
19254 .contains(SurfaceRequirement::MixedDirectContent));
19255 assert!(!requirements
19256 .surface_requirements
19257 .has_isolating_requirement());
19258 }
19259
19260 #[test]
19261 fn collect_layer_contents_translates_direct_text_rects_into_parent_space() {
19262 let mut child = text_layer_with_style(
19263 AnnotatedString::from("direct"),
19264 TextStyle::from_span_style(SpanStyle {
19265 text_decoration: Some(TextDecoration::UNDERLINE),
19266 ..SpanStyle::default()
19267 }),
19268 );
19269 child.transform_to_parent = ProjectiveTransform::translation(9.0, 7.0);
19270
19271 let parent = test_layer(
19272 Rect {
19273 x: 0.0,
19274 y: 0.0,
19275 width: 64.0,
19276 height: 32.0,
19277 },
19278 vec![RenderNode::Layer(Box::new(child))],
19279 );
19280
19281 let mut rect_cache = HashMap::new();
19282 let mut requirements_cache = HashMap::new();
19283 let collected = with_test_app_context(|| {
19284 collect_layer_contents(
19285 &parent,
19286 None,
19287 None,
19288 &mut rect_cache,
19289 &mut requirements_cache,
19290 )
19291 });
19292
19293 assert!(
19294 collected.child_layers.is_empty(),
19295 "decoration-only text child should collapse directly into the parent scene"
19296 );
19297 assert_eq!(collected.scene.texts.len(), 1, "expected one text draw");
19298 let text = &collected.scene.texts[0];
19299 assert!(
19300 text.rect.x >= 9.0 && text.rect.y >= 7.0,
19301 "collapsed text rect should be translated into parent space, got {:?}",
19302 text.rect
19303 );
19304 assert!(
19305 collected
19306 .scene
19307 .shapes
19308 .iter()
19309 .any(|shape| shape.rect.y >= 7.0),
19310 "collapsed underline geometry should also be translated into parent space"
19311 );
19312 }
19313
19314 #[test]
19315 fn normalized_scene_keeps_lazy_after_bound_text_for_prewarm() {
19316 use std::cell::RefCell;
19317
19318 fn collect_graph_text_labels(layer: &LayerNode, labels: &mut Vec<String>) {
19319 for child in &layer.children {
19320 match child {
19321 RenderNode::Primitive(PrimitiveEntry {
19322 node: PrimitiveNode::Text(text),
19323 ..
19324 }) => labels.push(text.text.text.clone()),
19325 RenderNode::Layer(child_layer) => {
19326 collect_graph_text_labels(child_layer, labels)
19327 }
19328 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => {}
19329 }
19330 }
19331 }
19332
19333 let state_holder: Rc<RefCell<Option<LazyListState>>> = Rc::new(RefCell::new(None));
19334 let state_holder_for_comp = state_holder.clone();
19335 let mut composition = cranpose_ui::run_test_composition(move || {
19336 let list_state = remember_lazy_list_state();
19337 *state_holder_for_comp.borrow_mut() = Some(list_state);
19338 let mut spec = LazyColumnSpec::new()
19339 .vertical_arrangement(cranpose_ui::LinearArrangement::SpacedBy(6.0));
19340 spec.beyond_bounds_item_count = 0;
19341 LazyColumn(Modifier::empty().height(96.0), list_state, spec, |scope| {
19342 scope.items(
19343 12,
19344 None::<fn(usize) -> u64>,
19345 None::<fn(usize) -> u64>,
19346 |index| {
19347 Text(
19348 format!("WarmRow {index}"),
19349 Modifier::empty().height(32.0),
19350 TextStyle::default(),
19351 );
19352 },
19353 );
19354 });
19355 });
19356
19357 let list_state = (*state_holder.borrow()).expect("lazy list state should be captured");
19358 list_state.scroll_to_item(4, 0.0);
19359
19360 let root = composition.root().expect("lazy column root");
19361 let handle = composition.runtime_handle();
19362 let mut applier = composition.applier_mut();
19363 applier.set_runtime_handle(handle);
19364 let _ = applier
19365 .compute_layout(
19366 root,
19367 Size {
19368 width: 240.0,
19369 height: 240.0,
19370 },
19371 )
19372 .expect("lazy column layout");
19373 let graph = build_graph_from_applier(&mut applier, root, 1.0).expect("lazy column graph");
19374 applier.clear_runtime_handle();
19375 let mut graph_labels = Vec::new();
19376 collect_graph_text_labels(&graph.root, &mut graph_labels);
19377
19378 let visible_indices: Vec<_> = list_state
19379 .layout_info()
19380 .visible_items_info
19381 .iter()
19382 .map(|item| item.index)
19383 .collect();
19384 assert_eq!(
19385 visible_indices,
19386 vec![4, 5, 6],
19387 "test setup expects exactly three viewport-visible rows"
19388 );
19389
19390 let mut rect_cache = HashMap::new();
19391 let mut requirements_cache = HashMap::new();
19392 let collected = with_test_app_context(|| {
19393 collect_layer_contents(
19394 &graph.root,
19395 None,
19396 None,
19397 &mut rect_cache,
19398 &mut requirements_cache,
19399 )
19400 });
19401 let root_text_labels: Vec<_> = collected
19402 .scene
19403 .texts
19404 .iter()
19405 .map(|text| text.text.text.clone())
19406 .collect();
19407 let child_layer_count = collected.child_layers.len();
19408 let warm_text = collected
19409 .scene
19410 .texts
19411 .iter()
19412 .find(|text| text.text.text == "WarmRow 7")
19413 .unwrap_or_else(|| {
19414 panic!(
19415 "after-bound lazy text should reach WGPU scene collection; graph_texts={graph_labels:?} root_texts={root_text_labels:?} child_layers={child_layer_count}"
19416 )
19417 });
19418
19419 assert!(
19420 warm_text.rect.y >= 96.0,
19421 "after-bound text should be below the viewport, got {:?}",
19422 warm_text.rect
19423 );
19424 assert_eq!(
19425 visible_draw_rect(warm_text.rect, warm_text.clip),
19426 None,
19427 "after-bound text should remain clipped away for drawing while staying available for glyph prewarm"
19428 );
19429 assert!(
19430 text_draw_should_prewarm_in_viewport(
19431 warm_text.rect,
19432 warm_text.clip,
19433 ViewportUniformParams {
19434 width: 240,
19435 height: 96,
19436 offset: [0.0, 0.0],
19437 },
19438 1.0,
19439 ),
19440 "after-bound text inside the warm window must be selected by WGPU prewarm"
19441 );
19442 }
19443
19444 #[test]
19445 fn direct_translation_accepts_nearly_identity_axis_scale_noise() {
19446 let local_bounds = Rect {
19447 x: 0.0,
19448 y: 0.0,
19449 width: 393.3,
19450 height: 16.8,
19451 };
19452 let quad = [
19453 [10.0, 78.399_994],
19454 [403.3, 78.399_994],
19455 [10.0, 95.2],
19456 [403.3, 95.2],
19457 ];
19458 let transform = ProjectiveTransform::from_rect_to_quad(local_bounds, quad);
19459
19460 assert_eq!(
19461 direct_translation(transform),
19462 Some(Point::new(10.0, 78.399_994)),
19463 );
19464 }
19465
19466 #[test]
19467 fn layer_surface_requirements_keep_shape_plus_isolating_child_as_mixed_content() {
19468 let mut child = test_layer(
19469 Rect {
19470 x: 0.0,
19471 y: 0.0,
19472 width: 24.0,
19473 height: 18.0,
19474 },
19475 vec![RenderNode::Primitive(PrimitiveEntry {
19476 phase: PrimitivePhase::BeforeChildren,
19477 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19478 primitive: DrawPrimitive::Rect {
19479 rect: Rect {
19480 x: 0.0,
19481 y: 0.0,
19482 width: 24.0,
19483 height: 18.0,
19484 },
19485 brush: Brush::solid(Color::WHITE),
19486 stroke: None,
19487 },
19488 clip: None,
19489 }),
19490 })],
19491 );
19492 child.transform_to_parent = ProjectiveTransform::translation(8.0, 6.0);
19493 child.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
19494
19495 let layer = test_layer(
19496 Rect {
19497 x: 0.0,
19498 y: 0.0,
19499 width: 64.0,
19500 height: 32.0,
19501 },
19502 vec![
19503 RenderNode::Primitive(PrimitiveEntry {
19504 phase: PrimitivePhase::BeforeChildren,
19505 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19506 primitive: DrawPrimitive::Rect {
19507 rect: Rect {
19508 x: 0.0,
19509 y: 0.0,
19510 width: 64.0,
19511 height: 32.0,
19512 },
19513 brush: Brush::solid(Color::BLACK),
19514 stroke: None,
19515 },
19516 clip: None,
19517 }),
19518 }),
19519 RenderNode::Layer(Box::new(child)),
19520 ],
19521 );
19522
19523 let requirements = layer_surface_requirements(&layer);
19524
19525 assert!(requirements
19526 .surface_requirements
19527 .contains(SurfaceRequirement::MixedDirectContent));
19528 assert!(!requirements
19529 .surface_requirements
19530 .has_isolating_requirement());
19531 }
19532
19533 #[test]
19534 fn build_scene_window_filters_and_translates_items() {
19535 let mut shape = test_shape(6, BlendMode::SrcOver);
19536 shape.rect.x = 12.0;
19537 shape.rect.y = 25.0;
19538 shape.local_rect.x = 12.0;
19539 shape.local_rect.y = 25.0;
19540 shape.quad = [[12.0, 25.0], [20.0, 25.0], [12.0, 33.0], [20.0, 33.0]];
19541 shape.clip = Some(Rect {
19542 x: 11.0,
19543 y: 24.0,
19544 width: 10.0,
19545 height: 10.0,
19546 });
19547
19548 let mut image = test_image(8, BlendMode::SrcOver);
19549 image.rect.x = 18.0;
19550 image.rect.y = 27.0;
19551 image.local_rect.x = 18.0;
19552 image.local_rect.y = 27.0;
19553 image.quad = [[18.0, 27.0], [26.0, 27.0], [18.0, 35.0], [26.0, 35.0]];
19554
19555 let mut text = test_text(9);
19556 text.rect.x = 16.0;
19557 text.rect.y = 29.0;
19558 text.clip = Some(Rect {
19559 x: 15.0,
19560 y: 28.0,
19561 width: 9.0,
19562 height: 6.0,
19563 });
19564
19565 let mut shadow_shape = test_shape(7, BlendMode::SrcOver);
19566 shadow_shape.rect.x = 14.0;
19567 shadow_shape.rect.y = 26.0;
19568 shadow_shape.local_rect.x = 14.0;
19569 shadow_shape.local_rect.y = 26.0;
19570 shadow_shape.quad = [[14.0, 26.0], [22.0, 26.0], [14.0, 34.0], [22.0, 34.0]];
19571 let mut shadow = test_shadow_draw(vec![(shadow_shape, BlendMode::SrcOver)]);
19572 shadow.z_index = 7;
19573
19574 let mut nested_effect = effect_layer(6, 10);
19575 nested_effect.rect.x = 13.0;
19576 nested_effect.rect.y = 24.0;
19577 nested_effect.clip = Some(Rect {
19578 x: 15.0,
19579 y: 25.0,
19580 width: 4.0,
19581 height: 5.0,
19582 });
19583
19584 let mut nested_backdrop = backdrop_layer(8);
19585 nested_backdrop.rect.x = 17.0;
19586 nested_backdrop.rect.y = 26.0;
19587 nested_backdrop.clip = Some(Rect {
19588 x: 18.0,
19589 y: 27.0,
19590 width: 3.0,
19591 height: 4.0,
19592 });
19593
19594 let window = build_scene_window(
19595 SceneWindowSource {
19596 shapes: &[test_shape(4, BlendMode::SrcOver), shape],
19597 brushes: &[],
19598 images: &[image],
19599 texts: &[text],
19600 shadow_draws: &[shadow],
19601 draw_ops: &[],
19602 effect_layers: &[effect_layer(2, 4), nested_effect.clone()],
19603 backdrop_layers: &[backdrop_layer(4), nested_backdrop.clone()],
19604 },
19605 5,
19606 10,
19607 Rect {
19608 x: 10.0,
19609 y: 20.0,
19610 width: 20.0,
19611 height: 20.0,
19612 },
19613 );
19614
19615 assert_eq!(window.shapes.len(), 1);
19616 assert_eq!(
19617 window.shapes[0].rect,
19618 Rect {
19619 x: 2.0,
19620 y: 5.0,
19621 width: 8.0,
19622 height: 8.0,
19623 }
19624 );
19625 assert_eq!(
19626 window.shapes[0].clip,
19627 Some(Rect {
19628 x: 1.0,
19629 y: 4.0,
19630 width: 10.0,
19631 height: 10.0,
19632 })
19633 );
19634 assert_eq!(window.images.len(), 1);
19635 assert_eq!(window.images[0].rect.x, 8.0);
19636 assert_eq!(window.images[0].rect.y, 7.0);
19637 assert_eq!(window.texts.len(), 1);
19638 assert_eq!(window.texts[0].rect.x, 6.0);
19639 assert_eq!(window.texts[0].rect.y, 9.0);
19640 assert_eq!(
19641 window.texts[0].clip,
19642 Some(Rect {
19643 x: 5.0,
19644 y: 8.0,
19645 width: 9.0,
19646 height: 6.0,
19647 })
19648 );
19649 assert_eq!(window.shadow_draws.len(), 1);
19650 assert_eq!(window.shadow_draws[0].shapes[0].0.rect.x, 4.0);
19651 assert_eq!(window.shadow_draws[0].shapes[0].0.rect.y, 6.0);
19652 assert_eq!(window.effect_layers.len(), 1);
19653 assert_eq!(
19654 window.effect_layers[0].rect,
19655 Rect {
19656 x: 3.0,
19657 y: 4.0,
19658 width: 10.0,
19659 height: 10.0,
19660 }
19661 );
19662 assert_eq!(
19663 window.effect_layers[0].clip,
19664 Some(Rect {
19665 x: 5.0,
19666 y: 5.0,
19667 width: 4.0,
19668 height: 5.0,
19669 })
19670 );
19671 assert_eq!(window.backdrop_layers.len(), 1);
19672 assert_eq!(
19673 window.backdrop_layers[0].rect,
19674 Rect {
19675 x: 7.0,
19676 y: 6.0,
19677 width: 10.0,
19678 height: 10.0,
19679 }
19680 );
19681 assert_eq!(
19682 window.backdrop_layers[0].clip,
19683 Some(Rect {
19684 x: 8.0,
19685 y: 7.0,
19686 width: 3.0,
19687 height: 4.0,
19688 })
19689 );
19690 }
19691
19692 #[test]
19693 fn filtered_effect_layer_index_counts_only_window_members() {
19694 let effects = vec![
19695 effect_layer(0, 2),
19696 effect_layer(5, 12),
19697 effect_layer(6, 10),
19698 effect_layer(14, 20),
19699 ];
19700
19701 assert_eq!(filtered_effect_layer_index(&effects, 1, 5, 12), Some(0));
19702 assert_eq!(filtered_effect_layer_index(&effects, 2, 5, 12), Some(1));
19703 assert_eq!(filtered_effect_layer_index(&effects, 3, 5, 12), None);
19704 }
19705
19706 #[test]
19707 fn blend_mode_support_matrix_is_explicit() {
19708 assert!(is_blend_mode_supported(BlendMode::SrcOver));
19709 assert!(is_blend_mode_supported(BlendMode::DstOut));
19710 assert!(!is_blend_mode_supported(BlendMode::Clear));
19711 assert!(!is_blend_mode_supported(BlendMode::Multiply));
19712 }
19713
19714 #[test]
19715 fn collect_non_effect_segment_items_preserves_global_z_order() {
19716 let shapes = vec![
19717 test_shape(3, BlendMode::SrcOver),
19718 test_shape(1, BlendMode::DstOut),
19719 ];
19720 let images = vec![test_image(2, BlendMode::SrcOver)];
19721 let texts = vec![test_text(0)];
19722 let shadows: Vec<ShadowDraw> = Vec::new();
19723 let draw_ops = test_draw_ops(&shapes, &images, &texts, &shadows);
19724
19725 let mut scratch = Vec::new();
19726 collect_non_effect_segment_items(
19727 &shapes,
19728 &images,
19729 &texts,
19730 &shadows,
19731 &draw_ops,
19732 0,
19733 4,
19734 &[],
19735 100,
19736 100,
19737 1.0,
19738 &mut scratch,
19739 );
19740 let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
19741 assert_eq!(
19742 items,
19743 vec![
19744 SegmentDrawItem::Text(0),
19745 SegmentDrawItem::Shape(1),
19746 SegmentDrawItem::Image(0),
19747 SegmentDrawItem::Shape(0),
19748 ]
19749 );
19750 }
19751
19752 #[test]
19753 fn collect_non_effect_segment_items_filters_effect_ranges() {
19754 let shapes = vec![
19755 test_shape(1, BlendMode::SrcOver),
19756 test_shape(3, BlendMode::DstOut),
19757 ];
19758 let images = vec![test_image(2, BlendMode::SrcOver)];
19759 let texts = vec![test_text(4)];
19760 let shadows: Vec<ShadowDraw> = Vec::new();
19761 let draw_ops = test_draw_ops(&shapes, &images, &texts, &shadows);
19762 let effect_ranges = [std::ops::Range { start: 2, end: 4 }];
19763
19764 let mut scratch = Vec::new();
19765 collect_non_effect_segment_items(
19766 &shapes,
19767 &images,
19768 &texts,
19769 &shadows,
19770 &draw_ops,
19771 0,
19772 5,
19773 &effect_ranges,
19774 100,
19775 100,
19776 1.0,
19777 &mut scratch,
19778 );
19779 let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
19780 assert_eq!(
19781 items,
19782 vec![SegmentDrawItem::Shape(0), SegmentDrawItem::Text(0)]
19783 );
19784 }
19785
19786 #[test]
19787 fn collect_non_effect_segment_items_culls_offscreen_shapes_but_keeps_text_prewarm() {
19788 let mut shape = test_shape(0, BlendMode::SrcOver);
19789 shape.rect.y = 160.0;
19790 shape.local_rect.y = 160.0;
19791 shape.quad = [[0.0, 160.0], [8.0, 160.0], [0.0, 168.0], [8.0, 168.0]];
19792
19793 let shapes = vec![shape];
19794 let images = Vec::new();
19795 let mut text = test_text(1);
19796 text.rect.y = 160.0;
19797 let texts = vec![text];
19798 let shadows: Vec<ShadowDraw> = Vec::new();
19799 let draw_ops = test_draw_ops(&shapes, &images, &texts, &shadows);
19800
19801 let mut scratch = Vec::new();
19802 collect_non_effect_segment_items(
19803 &shapes,
19804 &images,
19805 &texts,
19806 &shadows,
19807 &draw_ops,
19808 0,
19809 2,
19810 &[],
19811 100,
19812 100,
19813 1.0,
19814 &mut scratch,
19815 );
19816
19817 let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
19818 assert_eq!(items, vec![SegmentDrawItem::Text(0)]);
19819 }
19820
19821 #[test]
19822 fn segment_command_iter_merges_non_conflicting_batches_into_one_chunk() {
19823 let ordered_items = vec![
19824 (0, SegmentDrawItem::Shape(0)),
19825 (1, SegmentDrawItem::Image(0)),
19826 (2, SegmentDrawItem::Text(0)),
19827 ];
19828 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
19829 let images = vec![test_image(1, BlendMode::DstOut)];
19830
19831 let commands: Vec<_> = SegmentCommandIter::new(
19832 &ordered_items,
19833 &shapes,
19834 &images,
19835 ShapeBatchLimits::desktop(),
19836 )
19837 .collect();
19838
19839 assert_eq!(
19840 commands,
19841 vec![SegmentRenderCommand::DrawChunk(chunk(&[
19842 SegmentBatchPlan::Shape {
19843 start: 0,
19844 end: 1,
19845 blend_mode: BlendMode::SrcOver,
19846 },
19847 SegmentBatchPlan::Image {
19848 start: 1,
19849 end: 2,
19850 blend_mode: BlendMode::DstOut,
19851 },
19852 SegmentBatchPlan::Text { start: 2, end: 3 },
19853 ]))]
19854 );
19855 }
19856
19857 #[test]
19858 fn segment_command_iter_keeps_layer_composites_in_ordered_draw_chunk() {
19859 let ordered_items = vec![
19860 (0, SegmentDrawItem::Shape(0)),
19861 (1, SegmentDrawItem::Composite(0)),
19862 (2, SegmentDrawItem::Image(0)),
19863 (3, SegmentDrawItem::Composite(1)),
19864 (4, SegmentDrawItem::Text(0)),
19865 ];
19866 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
19867 let images = vec![test_image(2, BlendMode::SrcOver)];
19868
19869 let commands: Vec<_> = SegmentCommandIter::new(
19870 &ordered_items,
19871 &shapes,
19872 &images,
19873 ShapeBatchLimits::desktop(),
19874 )
19875 .collect();
19876
19877 assert_eq!(
19878 commands,
19879 vec![SegmentRenderCommand::DrawChunk(chunk(&[
19880 SegmentBatchPlan::Shape {
19881 start: 0,
19882 end: 1,
19883 blend_mode: BlendMode::SrcOver,
19884 },
19885 SegmentBatchPlan::Composite { start: 1, end: 2 },
19886 SegmentBatchPlan::Image {
19887 start: 2,
19888 end: 3,
19889 blend_mode: BlendMode::SrcOver,
19890 },
19891 SegmentBatchPlan::Composite { start: 3, end: 4 },
19892 SegmentBatchPlan::Text { start: 4, end: 5 },
19893 ]))]
19894 );
19895 }
19896
19897 #[test]
19898 fn retain_renderable_shadow_items_culls_invisible_shadow_boundaries() {
19899 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
19900 let images = vec![test_image(2, BlendMode::SrcOver)];
19901 let mut shadow_shape = test_shape(1, BlendMode::SrcOver);
19902 shadow_shape.rect = Rect {
19903 x: 500.0,
19904 y: 500.0,
19905 width: 12.0,
19906 height: 12.0,
19907 };
19908 let shadow_draws = vec![ShadowDraw {
19909 shapes: vec![(shadow_shape, BlendMode::SrcOver)],
19910 brushes: vec![],
19911 texts: Vec::new(),
19912 blur_radius: 8.0,
19913 clip: None,
19914 z_index: 1,
19915 }];
19916 let mut ordered_items = vec![
19917 (0, SegmentDrawItem::Shape(0)),
19918 (1, SegmentDrawItem::Shadow(0)),
19919 (2, SegmentDrawItem::Image(0)),
19920 ];
19921
19922 let culled =
19923 retain_renderable_shadow_items(&mut ordered_items, &shadow_draws, 100, 100, 1.0, 4096);
19924 let commands: Vec<_> = SegmentCommandIter::new(
19925 &ordered_items,
19926 &shapes,
19927 &images,
19928 ShapeBatchLimits::desktop(),
19929 )
19930 .collect();
19931
19932 assert_eq!(culled, 1);
19933 assert_eq!(
19934 commands,
19935 vec![SegmentRenderCommand::DrawChunk(chunk(&[
19936 SegmentBatchPlan::Shape {
19937 start: 0,
19938 end: 1,
19939 blend_mode: BlendMode::SrcOver,
19940 },
19941 SegmentBatchPlan::Image {
19942 start: 1,
19943 end: 2,
19944 blend_mode: BlendMode::SrcOver,
19945 },
19946 ]))]
19947 );
19948 }
19949
19950 #[test]
19951 fn retain_renderable_shadow_items_keeps_visible_shadow_boundaries() {
19952 let mut shadow_shape = test_shape(1, BlendMode::SrcOver);
19953 shadow_shape.rect = Rect {
19954 x: 20.0,
19955 y: 20.0,
19956 width: 12.0,
19957 height: 12.0,
19958 };
19959 let shadow_draws = vec![ShadowDraw {
19960 shapes: vec![(shadow_shape, BlendMode::SrcOver)],
19961 brushes: vec![],
19962 texts: Vec::new(),
19963 blur_radius: 8.0,
19964 clip: None,
19965 z_index: 1,
19966 }];
19967 let mut ordered_items = vec![(1, SegmentDrawItem::Shadow(0))];
19968
19969 let culled =
19970 retain_renderable_shadow_items(&mut ordered_items, &shadow_draws, 100, 100, 1.0, 4096);
19971
19972 assert_eq!(culled, 0);
19973 assert_eq!(ordered_items, vec![(1, SegmentDrawItem::Shadow(0))]);
19974 }
19975
19976 #[test]
19977 fn shape_data_layout_matches_the_wgsl_mirror() {
19978 assert_eq!(std::mem::size_of::<ShapeData>(), 160);
19982 assert_eq!(std::mem::size_of::<ShapeData>() % 16, 0);
19983 assert_eq!(std::mem::size_of::<GradientStop>(), 32);
19984 }
19985
19986 #[test]
19987 fn shape_flags_pack_kind_cap_and_join_without_collision() {
19988 assert_eq!(
19989 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter),
19990 0.0
19991 );
19992 assert_eq!(
19993 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Miter),
19994 1.0
19995 );
19996 assert_eq!(
19997 pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Butt, StrokeJoin::Miter),
19998 2.0
19999 );
20000 assert_eq!(
20002 pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Round, StrokeJoin::Miter),
20003 2.0 + 4.0
20004 );
20005 assert_eq!(
20006 pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Square, StrokeJoin::Miter),
20007 2.0 + 8.0
20008 );
20009 assert_eq!(
20010 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Round),
20011 1.0 + 16.0
20012 );
20013 assert_eq!(
20014 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Bevel),
20015 1.0 + 32.0
20016 );
20017 for kind in [SHAPE_KIND_FILL, SHAPE_KIND_STROKE, SHAPE_KIND_ARC] {
20019 for cap in [StrokeCap::Butt, StrokeCap::Round, StrokeCap::Square] {
20020 for join in [StrokeJoin::Miter, StrokeJoin::Round, StrokeJoin::Bevel] {
20021 let packed = pack_shape_flags(kind, cap, join);
20022 let bits = packed as u32;
20023 assert_eq!(bits & 3, kind);
20024 assert_eq!((bits >> 2) & 3, stroke_cap_code(cap));
20025 assert_eq!((bits >> 4) & 3, stroke_join_code(join));
20026 assert_eq!(packed, bits as f32, "flags must be exact in f32");
20027 }
20028 }
20029 }
20030 }
20031
20032 #[cfg(not(target_arch = "wasm32"))]
20033 #[test]
20034 fn mesh_vertex_layout_matches_the_wgsl_input() {
20035 assert_eq!(std::mem::size_of::<MeshVertex>(), 20);
20038 }
20039
20040 #[cfg(not(target_arch = "wasm32"))]
20044 #[allow(clippy::too_many_arguments)]
20045 fn sdf_arc_band_reference(
20046 p: [f32; 2],
20047 center: [f32; 2],
20048 inner: f32,
20049 outer: f32,
20050 mid_sin_cos: [f32; 2],
20051 half_sin_cos: [f32; 2],
20052 cap: u32,
20053 ) -> f32 {
20054 let ra = (outer + inner) * 0.5;
20055 let rb = ((outer - inner) * 0.5).max(0.0);
20056 let sm = mid_sin_cos[0];
20057 let cm = mid_sin_cos[1];
20058 let d = [p[0] - center[0], p[1] - center[1]];
20059 let mut q = [-sm * d[0] + cm * d[1], cm * d[0] + sm * d[1]];
20060 q[0] = q[0].abs();
20061 let sc = half_sin_cos;
20062 let mut dist = if sc[1] * q[0] > sc[0] * q[1] {
20063 let dx = q[0] - sc[0] * ra;
20064 let dy = q[1] - sc[1] * ra;
20065 (dx * dx + dy * dy).sqrt() - rb
20066 } else {
20067 ((q[0] * q[0] + q[1] * q[1]).sqrt() - ra).abs() - rb
20068 };
20069 let plane = sc[1] * q[0] - sc[0] * q[1];
20070 if cap == 0 {
20072 dist = dist.max(plane);
20073 } else if cap == 2 {
20074 dist = dist.max(plane - rb);
20075 }
20076 dist
20077 }
20078
20079 #[cfg(not(target_arch = "wasm32"))]
20080 fn point_in_triangle(p: [f64; 2], tri: &[[f64; 2]; 3]) -> bool {
20081 let side = |a: [f64; 2], b: [f64; 2]| {
20082 (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0])
20083 };
20084 let d0 = side(tri[0], tri[1]);
20085 let d1 = side(tri[1], tri[2]);
20086 let d2 = side(tri[2], tri[0]);
20087 let has_neg = d0 < 0.0 || d1 < 0.0 || d2 < 0.0;
20088 let has_pos = d0 > 0.0 || d1 > 0.0 || d2 > 0.0;
20089 !(has_neg && has_pos)
20090 }
20091
20092 #[cfg(not(target_arch = "wasm32"))]
20093 fn converted_arc_shape(arc: cranpose_ui_graphics::ArcGeometry, root_scale: f32) -> ShapeData {
20094 let bounds = arc.bounds();
20095 let mut shape = test_shape(0, BlendMode::SrcOver);
20096 shape.rect = bounds;
20097 shape.local_rect = bounds;
20098 shape.quad = [
20099 [bounds.x, bounds.y],
20100 [bounds.x + bounds.width, bounds.y],
20101 [bounds.x, bounds.y + bounds.height],
20102 [bounds.x + bounds.width, bounds.y + bounds.height],
20103 ];
20104 shape.arc = Some(arc);
20105 let mut converted = ShapeData::zeroed();
20106 convert_shape_into_slots(&shape, &[], root_scale, 0, &mut converted, &mut []);
20107 converted
20108 }
20109
20110 #[cfg(not(target_arch = "wasm32"))]
20115 #[test]
20116 fn arc_mesh_contains_every_band_pixel() {
20117 use cranpose_ui_graphics::ArcGeometry;
20118 let tau = cranpose_ui_graphics::TAU;
20119 let center = Point::new(250.0, 250.0);
20120 let cases: &[(f32, f32, f32, f32, StrokeCap)] = &[
20121 (90.0, 100.0, 0.0, tau, StrokeCap::Round),
20123 (80.0, 100.0, 1.0, 10.0, StrokeCap::Butt),
20125 (0.0, 40.0, 0.0, tau, StrokeCap::Round),
20127 (30.0, 80.0, 0.7, 2.5, StrokeCap::Butt),
20129 (30.0, 80.0, 0.7, 2.5, StrokeCap::Round),
20130 (30.0, 80.0, 0.7, 2.5, StrokeCap::Square),
20131 (99.0, 101.0, 3.0, 4.0, StrokeCap::Round),
20133 (0.6, 2.0, 0.3, 1.2, StrokeCap::Butt),
20135 (1900.0, 1904.0, 0.1, 0.35, StrokeCap::Square),
20137 (40.0, 60.0, 5.0, 1e-3, StrokeCap::Round),
20139 (40.0, 60.0, 0.2, tau - 1e-3, StrokeCap::Butt),
20141 (0.0, 3.0, 1.0, 2.0, StrokeCap::Round),
20143 (20.0, 60.0, 4.5, 1.9, StrokeCap::Butt),
20145 ];
20146 for (case, &(inner, outer, start, sweep, cap)) in cases.iter().enumerate() {
20147 for root_scale in [1.0f32, 2.0, 2.75] {
20152 let arc = ArcGeometry::new(center, inner, outer, start, sweep, cap);
20153 assert!(!arc.is_degenerate(), "case {case} must be drawable");
20154 let converted = converted_arc_shape(arc, root_scale);
20155 let band = arc_mesh_band(&converted)
20156 .unwrap_or_else(|| panic!("case {case} must qualify for meshing"));
20157 let mut vertices = Vec::new();
20158 let mut indices = Vec::new();
20159 let segments =
20160 emit_arc_band_mesh(&converted, 0, &band, &mut vertices, &mut indices)
20161 .unwrap_or_else(|| panic!("case {case} must produce a mesh"));
20162 assert!(segments >= ARC_MESH_MIN_SEGMENTS);
20163 let position = |index: u32| {
20166 let p = vertices[index as usize].position;
20167 [p[0] as f64, p[1] as f64]
20168 };
20169 let triangles: Vec<[[f64; 2]; 3]> = indices
20170 .as_chunks::<3>()
20171 .0
20172 .iter()
20173 .map(|tri| [position(tri[0]), position(tri[1]), position(tri[2])])
20174 .collect();
20175
20176 let [qx, qy, ..] = converted.quad01;
20180 let [_, _, qr, qb] = converted.quad23;
20181 let (rw, rh) = (qr - qx, qb - qy);
20182 let cap_bits = (converted.stroke_params[1].max(0.0) as u32 >> 2) & 3;
20183 let step = (rw.max(rh) / 400.0).clamp(0.25, 2.0);
20184 let mut band_points = 0usize;
20185 let mut y = qy;
20186 while y <= qb {
20187 let mut x = qx;
20188 while x <= qr {
20189 let dist = sdf_arc_band_reference(
20190 [x, y],
20191 [converted.arc_params[0], converted.arc_params[1]],
20192 converted.stroke_params[3],
20193 converted.stroke_params[2],
20194 [converted.radii[0], converted.radii[1]],
20195 [converted.radii[2], converted.radii[3]],
20196 cap_bits,
20197 );
20198 if dist <= 0.5 {
20199 band_points += 1;
20200 let p = [x as f64, y as f64];
20201 assert!(
20202 triangles.iter().any(|tri| point_in_triangle(p, tri)),
20203 "case {case} scale {root_scale}: band point ({x}, {y}) \
20204 dist {dist} escapes the mesh"
20205 );
20206 }
20207 x += step;
20208 }
20209 y += step;
20210 }
20211 assert!(
20212 band_points > 0,
20213 "case {case} scale {root_scale}: the sampling grid never hit the band"
20214 );
20215 }
20216 }
20217 }
20218
20219 #[cfg(not(target_arch = "wasm32"))]
20220 #[test]
20221 fn arc_mesh_passthrough_replicates_the_quad_expansion() {
20222 let shape = test_shape(0, BlendMode::SrcOver);
20223 let mut converted = ShapeData::zeroed();
20224 convert_shape_into_slots(&shape, &[], 1.0, 0, &mut converted, &mut []);
20225 let build =
20226 build_arc_mesh_vertices(std::slice::from_ref(&converted)).expect("within budget");
20227 assert_eq!(build.meshed_arcs, 0);
20228 assert_eq!(build.passthrough, 1);
20229 assert_eq!(build.vertices.len(), 4);
20231 assert_eq!(build.index_prefix, vec![0, 6]);
20232 assert_eq!(build.indices, vec![0, 1, 2, 2, 1, 3]);
20233 let corners = [
20234 ([converted.quad01[0], converted.quad01[1]], [0.0f32, 0.0]),
20235 ([converted.quad01[2], converted.quad01[3]], [1.0, 0.0]),
20236 ([converted.quad23[0], converted.quad23[1]], [0.0, 1.0]),
20237 ([converted.quad23[2], converted.quad23[3]], [1.0, 1.0]),
20238 ];
20239 for (vertex, corner) in build.vertices.iter().zip(corners) {
20240 assert_eq!(vertex.position, corner.0);
20241 assert_eq!(vertex.uv, corner.1);
20242 assert_eq!(vertex.shape_idx, 0);
20243 }
20244 for (index, corner) in build.indices.iter().zip([0usize, 1, 2, 2, 1, 3]) {
20247 assert_eq!(build.vertices[*index as usize].position, corners[corner].0);
20248 assert_eq!(build.vertices[*index as usize].uv, corners[corner].1);
20249 }
20250 }
20251
20252 #[cfg(not(target_arch = "wasm32"))]
20258 #[test]
20259 fn arc_mesh_indices_share_boundary_vertices_and_wrap_closed_rings() {
20260 use cranpose_ui_graphics::ArcGeometry;
20261 let tau = cranpose_ui_graphics::TAU;
20262 for (sweep, closed) in [(tau, true), (1.9f32, false)] {
20265 let arc = ArcGeometry::new(
20266 Point::new(250.0, 250.0),
20267 80.0,
20268 100.0,
20269 0.7,
20270 sweep,
20271 StrokeCap::Round,
20272 );
20273 let mut converted = converted_arc_shape(arc, 1.0);
20274 converted.rect = [0.0, 0.0, 500.0, 500.0];
20278 converted.quad01 = [0.0, 0.0, 500.0, 0.0];
20279 converted.quad23 = [0.0, 500.0, 500.0, 500.0];
20280 let band = arc_mesh_band(&converted).expect("arc must qualify");
20281 let mut vertices = Vec::new();
20282 let mut indices = Vec::new();
20283 let segments = emit_arc_band_mesh(&converted, 0, &band, &mut vertices, &mut indices)
20284 .expect("arc must mesh");
20285 let boundary_count = if closed { segments } else { segments + 1 };
20286 assert_eq!(
20287 vertices.len(),
20288 2 * boundary_count,
20289 "closed={closed}: every boundary owns exactly one (inner, outer) pair"
20290 );
20291 assert_eq!(indices.len(), 6 * segments);
20292 for j in 0..segments {
20296 let jb = (j + 1) % boundary_count;
20297 let (in_a, out_a) = (2 * j as u32, 2 * j as u32 + 1);
20298 let (in_b, out_b) = (2 * jb as u32, 2 * jb as u32 + 1);
20299 assert_eq!(
20300 indices[6 * j..6 * j + 6],
20301 [in_a, out_a, out_b, in_a, out_b, in_b],
20302 "closed={closed}: segment {j} must share its boundary pairs"
20303 );
20304 }
20305 if closed {
20306 assert_eq!(indices[6 * segments - 1], 0);
20309 }
20310 for pair in vertices.as_chunks::<2>().0 {
20314 let radius = |v: &MeshVertex| {
20315 let dx = v.position[0] - 250.0;
20316 let dy = v.position[1] - 250.0;
20317 (dx * dx + dy * dy).sqrt()
20318 };
20319 assert!(radius(&pair[0]) < radius(&pair[1]));
20320 }
20321 }
20322 }
20323
20324 #[cfg(not(target_arch = "wasm32"))]
20330 #[test]
20331 fn arc_mesh_clipped_segments_fan_over_private_vertices() {
20332 use cranpose_ui_graphics::ArcGeometry;
20333 let arc = ArcGeometry::new(
20334 Point::new(250.0, 250.0),
20335 80.0,
20336 100.0,
20337 0.0,
20338 cranpose_ui_graphics::TAU,
20339 StrokeCap::Round,
20340 );
20341 let converted = converted_arc_shape(arc, 1.0);
20342 let band = arc_mesh_band(&converted).expect("ring must qualify");
20343 let mut vertices = Vec::new();
20344 let mut indices = Vec::new();
20345 emit_arc_band_mesh(&converted, 0, &band, &mut vertices, &mut indices)
20346 .expect("ring must mesh");
20347 let mut uses = vec![0usize; vertices.len()];
20350 for &index in &indices {
20351 uses[index as usize] += 1;
20352 }
20353 assert!(
20354 uses.iter().any(|&count| count >= 3),
20355 "some boundary vertices must be shared across trapezoids"
20356 );
20357 let [left, top, ..] = converted.quad01;
20363 let [.., right, bottom] = converted.quad23;
20364 let clipped: Vec<&MeshVertex> = vertices
20365 .iter()
20366 .filter(|vertex| {
20367 let [x, y] = vertex.position;
20368 x == left || x == right || y == top || y == bottom
20369 })
20370 .collect();
20371 assert!(
20372 !clipped.is_empty(),
20373 "the tight box must clip the pushed-out chord vertices"
20374 );
20375 assert!(
20378 vertices.len() < indices.len(),
20379 "{} unique vertices should undercut {} triangle corners",
20380 vertices.len(),
20381 indices.len()
20382 );
20383 }
20384
20385 #[cfg(not(target_arch = "wasm32"))]
20386 #[test]
20387 fn arc_mesh_budget_overflow_falls_back_to_whole_slot_passthrough() {
20388 use cranpose_ui_graphics::ArcGeometry;
20389 let arc = ArcGeometry::new(
20394 Point::new(2000.0, 2000.0),
20395 1690.0,
20396 1710.0,
20397 0.0,
20398 cranpose_ui_graphics::TAU,
20399 StrokeCap::Round,
20400 );
20401 let converted = converted_arc_shape(arc, 1.0);
20402 let shapes = vec![converted; 100];
20403 assert!(build_arc_mesh_vertices(&shapes).is_none());
20404 }
20405
20406 #[cfg(not(target_arch = "wasm32"))]
20410 fn rim_test_shape_data() -> ShapeData {
20411 let mut shape = ShapeData::zeroed();
20412 shape.rect = [40.0, 40.0, 300.0, 300.0];
20413 shape.radii = [146.0; 4];
20414 shape.stroke_params = [
20415 8.0,
20416 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Miter),
20417 0.0,
20418 0.0,
20419 ];
20420 shape.quad01 = [40.0, 40.0, 340.0, 40.0];
20421 shape.quad23 = [40.0, 340.0, 340.0, 340.0];
20422 shape.color = [1.0, 1.0, 1.0, 1.0];
20423 shape
20424 }
20425
20426 #[cfg(not(target_arch = "wasm32"))]
20429 fn offscreen_test_viewport() -> ViewportUniformParams {
20430 ViewportUniformParams {
20431 width: 64,
20432 height: 64,
20433 offset: [7.0, 7.0],
20434 }
20435 }
20436
20437 #[cfg(not(target_arch = "wasm32"))]
20438 #[test]
20439 fn fill_diag_buckets_shape_quads_by_decoded_sdf_class() {
20440 let diag = FillAreaDiag::default();
20441 let mut arc = ShapeData::zeroed();
20442 arc.stroke_params[1] = pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Butt, StrokeJoin::Miter);
20443 arc.radii = [0.5; 4];
20446 arc.quad01 = [0.0, 0.0, 10.0, 0.0];
20447 arc.quad23 = [0.0, 10.0, 10.0, 10.0];
20448 let mut rounded = ShapeData::zeroed();
20449 rounded.stroke_params[1] =
20450 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
20451 rounded.radii = [2.0; 4];
20452 rounded.quad01 = [0.0, 0.0, 4.0, 0.0];
20453 rounded.quad23 = [0.0, 5.0, 4.0, 5.0];
20454 let mut plain = ShapeData::zeroed();
20455 plain.stroke_params[1] =
20456 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
20457 plain.quad01 = [0.0, 0.0, 2.0, 0.0];
20458 plain.quad23 = [0.0, 3.0, 2.0, 3.0];
20459 diag.add_shape_quads(
20460 &[rim_test_shape_data(), arc, rounded, plain],
20461 offscreen_test_viewport(),
20462 );
20463 assert_eq!(diag.frame[FillAreaDiag::RRECT_STROKE].get(), 300.0 * 300.0);
20464 assert_eq!(diag.frame[FillAreaDiag::ARC].get(), 100.0);
20465 assert_eq!(diag.frame[FillAreaDiag::RRECT_FILL].get(), 20.0);
20466 assert_eq!(diag.frame[FillAreaDiag::RECT].get(), 6.0);
20467 assert_eq!(diag.frame_corner.get(), 0.0);
20469 for (lit, quad) in diag.frame_lit.iter().zip(&diag.frame) {
20471 assert!(lit.get() <= quad.get() + 1e-9);
20472 }
20473 }
20474
20475 #[cfg(not(target_arch = "wasm32"))]
20476 #[test]
20477 fn fill_diag_rim_mesh_moves_quad_area_to_the_mesh_bucket() {
20478 let diag = FillAreaDiag::default();
20479 diag.add_shape_quads(&[rim_test_shape_data()], offscreen_test_viewport());
20480 diag.note_rim_mesh(&rim_test_shape_data(), 1234.5);
20481 assert_eq!(diag.frame[FillAreaDiag::RRECT_STROKE].get(), 0.0);
20482 assert_eq!(diag.frame[FillAreaDiag::MESH].get(), 1234.5);
20483 assert_eq!(diag.frame_lit[FillAreaDiag::RRECT_STROKE].get(), 0.0);
20486 assert!(diag.frame_lit[FillAreaDiag::MESH].get() <= 1234.5);
20487 assert!(diag.frame_lit[FillAreaDiag::MESH].get() > 0.0);
20488 }
20489
20490 #[cfg(not(target_arch = "wasm32"))]
20491 #[test]
20492 fn fill_diag_image_and_glyph_quads_share_one_bucket() {
20493 let diag = FillAreaDiag::default();
20494 diag.add_image_quad(&[[0.0, 0.0], [8.0, 0.0], [0.0, 4.0], [8.0, 4.0]]);
20495 let quad = CachedTextGlyphQuad {
20496 x: 0,
20497 y: 0,
20498 width: 5,
20499 height: 7,
20500 color: (1.0, 1.0, 1.0, 1.0),
20501 uv: ImageUvRect {
20502 min: [0.0, 0.0],
20503 max: [1.0, 1.0],
20504 sample_bounds: [0.0, 0.0, 1.0, 1.0],
20505 },
20506 };
20507 diag.add_glyph_quad(&quad);
20508 assert_eq!(diag.frame[FillAreaDiag::IMAGE_GLYPH].get(), 32.0 + 35.0);
20509 assert_eq!(diag.frame_lit[FillAreaDiag::IMAGE_GLYPH].get(), 32.0 + 35.0);
20511 }
20512
20513 #[cfg(not(target_arch = "wasm32"))]
20516 fn numeric_area(bounds: [f64; 4], steps: usize, inside: impl Fn(f64, f64) -> bool) -> f64 {
20517 let dx = (bounds[2] - bounds[0]) / steps as f64;
20518 let dy = (bounds[3] - bounds[1]) / steps as f64;
20519 let mut area = 0.0;
20520 for column in 0..steps {
20521 let x = bounds[0] + (column as f64 + 0.5) * dx;
20522 for row in 0..steps {
20523 let y = bounds[1] + (row as f64 + 0.5) * dy;
20524 if inside(x, y) {
20525 area += dx * dy;
20526 }
20527 }
20528 }
20529 area
20530 }
20531
20532 #[cfg(not(target_arch = "wasm32"))]
20535 fn sdf_rounded_rect_reference(
20536 p: [f64; 2],
20537 center: [f64; 2],
20538 half: [f64; 2],
20539 radius: f64,
20540 ) -> f64 {
20541 let qx = (p[0] - center[0]).abs() - (half[0] - radius);
20542 let qy = (p[1] - center[1]).abs() - (half[1] - radius);
20543 qx.max(0.0).hypot(qy.max(0.0)) + qx.max(qy).min(0.0) - radius
20544 }
20545
20546 #[cfg(not(target_arch = "wasm32"))]
20547 #[test]
20548 fn fill_truth_arc_lit_matches_the_sdf_covered_area() {
20549 use cranpose_ui_graphics::ArcGeometry;
20550 let tau = cranpose_ui_graphics::TAU;
20551 let center = Point::new(250.0, 250.0);
20552 let cases: &[(f32, f32, f32, f32, StrokeCap)] = &[
20555 (90.0, 100.0, 0.7, 2.5, StrokeCap::Butt),
20556 (30.0, 80.0, 0.7, 2.5, StrokeCap::Round),
20557 (30.0, 80.0, 0.7, 2.5, StrokeCap::Square),
20558 (80.0, 100.0, 0.0, tau, StrokeCap::Round),
20559 (0.0, 40.0, 0.0, tau, StrokeCap::Round),
20560 ];
20561 for (case, &(inner, outer, start, sweep, cap)) in cases.iter().enumerate() {
20562 let arc = ArcGeometry::new(center, inner, outer, start, sweep, cap);
20563 let converted = converted_arc_shape(arc, 1.0);
20564 let cap_code = (converted.stroke_params[1].max(0.0) as u32 >> 2) & 3;
20565 let arc_center = [converted.arc_params[0], converted.arc_params[1]];
20566 let mid = [converted.radii[0], converted.radii[1]];
20567 let half = [converted.radii[2], converted.radii[3]];
20568 let aabb = quad_aabb(&converted);
20569 let bounds = [aabb[0] - 2.0, aabb[1] - 2.0, aabb[2] + 2.0, aabb[3] + 2.0];
20572 let numeric = numeric_area(bounds, 1000, |x, y| {
20573 sdf_arc_band_reference(
20574 [x as f32, y as f32],
20575 arc_center,
20576 converted.stroke_params[3],
20577 converted.stroke_params[2],
20578 mid,
20579 half,
20580 cap_code,
20581 ) < 0.0
20582 });
20583 let analytic = analytic_covered_area(&converted);
20584 let error = (analytic - numeric).abs() / numeric.max(1.0);
20585 assert!(
20586 error < 0.02,
20587 "case {case}: analytic {analytic:.1} vs sdf {numeric:.1} \
20588 ({:.2}% off)",
20589 error * 100.0
20590 );
20591 }
20592 }
20593
20594 #[cfg(not(target_arch = "wasm32"))]
20595 #[test]
20596 fn fill_truth_circle_and_rrect_fill_lit_match_references() {
20597 let mut circle = ShapeData::zeroed();
20599 circle.stroke_params[1] =
20600 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
20601 circle.rect = [10.0, 10.0, 200.0, 200.0];
20602 circle.radii = [100.0; 4];
20603 let analytic = analytic_covered_area(&circle);
20604 let exact = std::f64::consts::PI * 100.0 * 100.0;
20605 assert!(
20606 (analytic - exact).abs() / exact < 1e-9,
20607 "circle: {analytic} vs {exact}"
20608 );
20609
20610 let mut rounded = ShapeData::zeroed();
20612 rounded.stroke_params[1] =
20613 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
20614 rounded.rect = [50.0, 80.0, 200.0, 120.0];
20615 rounded.radii = [40.0; 4];
20616 let numeric = numeric_area([48.0, 78.0, 252.0, 202.0], 1000, |x, y| {
20617 sdf_rounded_rect_reference([x, y], [150.0, 140.0], [100.0, 60.0], 40.0) < 0.0
20618 });
20619 let analytic = analytic_covered_area(&rounded);
20620 let error = (analytic - numeric).abs() / numeric;
20621 assert!(
20622 error < 0.02,
20623 "rrect fill: analytic {analytic:.1} vs sdf {numeric:.1}"
20624 );
20625 }
20626
20627 #[cfg(not(target_arch = "wasm32"))]
20628 #[test]
20629 fn fill_truth_stroked_rrect_lit_matches_the_band_area() {
20630 let rim = rim_test_shape_data();
20633 let analytic = analytic_covered_area(&rim);
20634 let exact = std::f64::consts::PI * (150.0 * 150.0 - 142.0 * 142.0);
20635 assert!(
20636 (analytic - exact).abs() / exact < 1e-9,
20637 "circle rim: {analytic} vs {exact}"
20638 );
20639
20640 let mut square_ring = rim_test_shape_data();
20643 square_ring.radii = [60.0; 4];
20644 let numeric = numeric_area([38.0, 38.0, 342.0, 342.0], 1000, |x, y| {
20645 sdf_rounded_rect_reference([x, y], [190.0, 190.0], [146.0, 146.0], 60.0).abs() < 4.0
20646 });
20647 let analytic = analytic_covered_area(&square_ring);
20648 let error = (analytic - numeric).abs() / numeric;
20649 assert!(
20650 error < 0.02,
20651 "square ring: analytic {analytic:.1} vs sdf {numeric:.1}"
20652 );
20653 }
20654
20655 #[cfg(not(target_arch = "wasm32"))]
20656 #[test]
20657 fn fill_truth_corner_counter_prices_the_area_outside_the_inscribed_circle() {
20658 let full = area_outside_inscribed_circle([0.0, 0.0, 454.0, 454.0], (454, 454));
20661 let exact = (1.0 - std::f64::consts::FRAC_PI_4) * 454.0 * 454.0;
20662 assert!(
20663 (full - exact).abs() / exact < 0.01,
20664 "full quad: {full} vs {exact}"
20665 );
20666 assert_eq!(
20668 area_outside_inscribed_circle([127.0, 127.0, 327.0, 327.0], (454, 454)),
20669 0.0
20670 );
20671 let corner = area_outside_inscribed_circle([0.0, 0.0, 40.0, 40.0], (454, 454));
20673 assert!((corner - 1600.0).abs() < 1e-6, "corner box: {corner}");
20674 }
20675
20676 #[cfg(not(target_arch = "wasm32"))]
20677 #[test]
20678 fn fill_truth_opacity_histogram_classifies_solid_alpha_exactly() {
20679 let diag = FillAreaDiag::default();
20680 diag.reset_frame(454, 454);
20681 let full_frame = ViewportUniformParams {
20682 width: 454,
20683 height: 454,
20684 offset: [0.0, 0.0],
20685 };
20686 let mut opaque = ShapeData::zeroed();
20687 opaque.stroke_params[1] =
20688 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
20689 opaque.rect = [0.0, 0.0, 100.0, 50.0];
20690 opaque.quad01 = [0.0, 0.0, 100.0, 0.0];
20691 opaque.quad23 = [0.0, 50.0, 100.0, 50.0];
20692 opaque.color = [1.0, 1.0, 1.0, 1.0];
20693 let mut faded = opaque;
20694 faded.color[3] = 0.82;
20695 let mut gradient = opaque;
20696 gradient.brush_type = 1;
20697 diag.add_shape_quads(&[opaque, faded, gradient], full_frame);
20698 let lit = |class: FillOpacityClass| diag.frame_opacity[class as usize].get();
20700 assert_eq!(lit(FillOpacityClass::Opaque), 5000.0);
20701 assert_eq!(lit(FillOpacityClass::Translucent), 5000.0);
20702 assert_eq!(lit(FillOpacityClass::NonSolid), 5000.0);
20703 assert!(diag.frame_corner.get() > 0.0);
20705
20706 let offscreen = FillAreaDiag::default();
20709 offscreen.reset_frame(454, 454);
20710 offscreen.add_shape_quads(&[opaque], offscreen_test_viewport());
20711 assert_eq!(offscreen.frame_corner.get(), 0.0);
20712 }
20713
20714 #[cfg(not(target_arch = "wasm32"))]
20715 #[test]
20716 fn fill_truth_retained_records_price_ranges_and_identity_corners() {
20717 let mut plain = ShapeData::zeroed();
20718 plain.stroke_params[1] =
20719 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
20720 plain.rect = [200.0, 200.0, 20.0, 10.0];
20721 plain.quad01 = [200.0, 200.0, 220.0, 200.0];
20722 plain.quad23 = [200.0, 210.0, 220.0, 210.0];
20723 plain.color = [1.0, 1.0, 1.0, 1.0];
20724 let shapes = vec![rim_test_shape_data(), plain];
20725 let records = fill_diag_capture_records(&shapes, None);
20726 assert_eq!(records.len(), 2);
20727 assert_eq!(records[0].bucket, FillAreaDiag::RRECT_STROKE);
20728 assert_eq!(records[0].drawn_px2, 300.0 * 300.0);
20729 assert!(records[0].lit_px2 < records[0].drawn_px2, "a rim has slack");
20730 assert_eq!(records[1].bucket, FillAreaDiag::RECT);
20732 assert_eq!(records[1].lit_px2, records[1].drawn_px2);
20733
20734 let diag = FillAreaDiag::default();
20735 diag.reset_frame(454, 454);
20736 let scaled = SimilarityTransform::new([0.0, 0.0], 0.0, 2.0);
20739 diag.add_retained_range(&records, 0, 2, &scaled);
20740 let drawn: f64 = records.iter().map(|record| record.drawn_px2).sum();
20741 assert!((diag.frame[FillAreaDiag::RETAINED].get() - drawn * 4.0).abs() < 1e-6);
20742 assert_eq!(diag.frame_corner.get(), 0.0);
20743
20744 let identity_diag = FillAreaDiag::default();
20747 identity_diag.reset_frame(454, 454);
20748 identity_diag.add_retained_range(&records, 0, 2, &SimilarityTransform::IDENTITY);
20749 assert!(identity_diag.frame_corner.get() > 0.0);
20750 let tail = FillAreaDiag::default();
20752 tail.reset_frame(454, 454);
20753 tail.add_retained_range(&records, 1, 2, &SimilarityTransform::IDENTITY);
20754 assert_eq!(
20755 tail.frame[FillAreaDiag::RETAINED].get(),
20756 records[1].drawn_px2
20757 );
20758 }
20759
20760 #[cfg(not(target_arch = "wasm32"))]
20761 #[test]
20762 fn fill_truth_top_slack_dump_keeps_the_worst_ten() {
20763 let mut diag = FillAreaDiag::default();
20764 let records: Vec<FillDiagShapeRecord> = (0..12)
20765 .map(|index| FillDiagShapeRecord {
20766 drawn_px2: 1000.0 * (index + 1) as f64,
20767 lit_px2: 100.0,
20768 bucket: FillAreaDiag::ARC,
20769 opacity: FillOpacityClass::Opaque,
20770 aabb: [0.0, 0.0, 10.0, 10.0],
20771 })
20772 .collect();
20773 diag.note_retained_capture(3, &records);
20774 assert_eq!(diag.slack_top.len(), FILL_DIAG_SLACK_TOP);
20775 assert_eq!(diag.slack_top[0].drawn_px2, 12000.0);
20777 assert_eq!(diag.slack_top[0].slot, 3);
20778 assert_eq!(diag.slack_top[0].shape, 11);
20779 for pair in diag.slack_top.windows(2) {
20780 assert!(pair[0].drawn_px2 - pair[0].lit_px2 >= pair[1].drawn_px2 - pair[1].lit_px2);
20781 }
20782 assert!(diag
20783 .slack_top
20784 .iter()
20785 .all(|entry| entry.drawn_px2 - entry.lit_px2 > 2000.0 - 100.0));
20786 }
20787
20788 #[cfg(not(target_arch = "wasm32"))]
20789 #[test]
20790 fn rim_mesh_band_accepts_only_huge_solid_unclipped_circle_rims() {
20791 let band = rim_mesh_band(&rim_test_shape_data()).expect("circle rim must qualify");
20792 assert_eq!(band.center, [190.0, 190.0]);
20793 assert_eq!(band.inner, 142.0);
20794 assert_eq!(band.outer, 150.0);
20795 assert_eq!(band.start, 0.0);
20796 assert!(
20797 band.sweep >= cranpose_ui_graphics::TAU,
20798 "a rim band is a closed ring"
20799 );
20800 let mut vertices = Vec::new();
20802 let mut indices = Vec::new();
20803 emit_arc_band_mesh(
20804 &rim_test_shape_data(),
20805 7,
20806 &band,
20807 &mut vertices,
20808 &mut indices,
20809 )
20810 .expect("rim must mesh");
20811 assert!(vertices.iter().all(|vertex| vertex.shape_idx == 7));
20812
20813 let mut square = rim_test_shape_data();
20817 square.radii = [100.0; 4];
20818 assert!(rim_mesh_band(&square).is_none());
20819
20820 let mut oblong = rim_test_shape_data();
20822 oblong.rect = [40.0, 40.0, 300.0, 200.0];
20823 assert!(rim_mesh_band(&oblong).is_none());
20824
20825 let mut gradient = rim_test_shape_data();
20827 gradient.brush_type = 1;
20828 assert!(rim_mesh_band(&gradient).is_none());
20829
20830 let mut clipped = rim_test_shape_data();
20832 clipped.clip_rect = [0.0, 0.0, 400.0, 400.0];
20833 assert!(rim_mesh_band(&clipped).is_none());
20834
20835 let mut small = rim_test_shape_data();
20837 small.rect = [40.0, 40.0, 100.0, 100.0];
20838 small.quad01 = [40.0, 40.0, 140.0, 40.0];
20839 small.quad23 = [40.0, 140.0, 140.0, 140.0];
20840 small.radii = [46.0; 4];
20841 assert!(rim_mesh_band(&small).is_none());
20842
20843 let mut fill = rim_test_shape_data();
20845 fill.stroke_params[1] =
20846 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
20847 assert!(rim_mesh_band(&fill).is_none());
20848
20849 let mut hairline = rim_test_shape_data();
20851 hairline.stroke_params[0] = 0.0;
20852 assert!(rim_mesh_band(&hairline).is_none());
20853
20854 let mut uneven = rim_test_shape_data();
20856 uneven.radii[2] = 145.0;
20857 assert!(rim_mesh_band(&uneven).is_none());
20858 }
20859
20860 #[cfg(not(target_arch = "wasm32"))]
20861 #[test]
20862 fn shape_batch_limits_follow_uniform_binding_size() {
20863 let desktop_shapes = 65536 / std::mem::size_of::<ShapeData>();
20866 assert_eq!(desktop_shapes, 409);
20867 assert_eq!(
20868 ShapeBatchLimits::desktop(),
20869 ShapeBatchLimits {
20870 max_shapes_per_batch: desktop_shapes.min(MAX_SHAPES_PER_BATCH),
20871 max_gradient_stops: MAX_GRADIENT_STOPS,
20872 storage: false,
20873 }
20874 );
20875
20876 let downlevel = ShapeBatchLimits::for_uniform_binding_size(16384);
20879 assert_eq!(downlevel.max_shapes_per_batch, 16384 / 160);
20880 assert_eq!(downlevel.max_shapes_per_batch, 102);
20881 assert_eq!(downlevel.max_gradient_stops, 512.min(MAX_GRADIENT_STOPS));
20882 assert!(downlevel.max_shapes_per_batch * std::mem::size_of::<ShapeData>() <= 16384);
20883 assert!(downlevel.max_gradient_stops * std::mem::size_of::<GradientStop>() <= 16384);
20884
20885 let tiny = ShapeBatchLimits::for_uniform_binding_size(1);
20887 assert_eq!(tiny.max_shapes_per_batch, 1);
20888 assert_eq!(tiny.max_gradient_stops, 1);
20889 }
20890
20891 #[test]
20892 fn storage_shape_batch_limits_uncap_the_batch_and_start_small() {
20893 let storage = ShapeBatchLimits::for_storage_binding_size(128 << 20);
20896 assert!(storage.storage);
20897 assert_eq!(storage.max_shapes_per_batch, MAX_SHAPES_PER_STORAGE_BATCH);
20898 assert_eq!(
20899 storage.max_gradient_stops,
20900 MAX_GRADIENT_STOPS_PER_STORAGE_BATCH
20901 );
20902
20903 assert_eq!(
20906 storage.initial_shape_capacity(),
20907 INITIAL_STORAGE_BATCH_CAPACITY
20908 );
20909 assert_eq!(
20910 storage.initial_gradient_capacity(),
20911 INITIAL_STORAGE_BATCH_CAPACITY
20912 );
20913 assert_eq!(
20914 storage.data_binding_type(),
20915 wgpu::BufferBindingType::Storage { read_only: true }
20916 );
20917 assert!(storage
20918 .data_buffer_usage()
20919 .contains(wgpu::BufferUsages::STORAGE));
20920
20921 let uniform = ShapeBatchLimits::desktop();
20924 assert_eq!(
20925 uniform.initial_shape_capacity(),
20926 uniform.max_shapes_per_batch
20927 );
20928 assert_eq!(
20929 uniform.initial_gradient_capacity(),
20930 uniform.max_gradient_stops
20931 );
20932 assert_eq!(
20933 uniform.data_binding_type(),
20934 wgpu::BufferBindingType::Uniform
20935 );
20936 assert!(uniform
20937 .data_buffer_usage()
20938 .contains(wgpu::BufferUsages::UNIFORM));
20939 }
20940
20941 #[test]
20942 fn storage_shape_shader_swaps_the_arrays_to_runtime_sized_storage() {
20943 let source = shape_shader_source(ShapeBatchLimits::for_storage_binding_size(128 << 20));
20944 assert!(
20945 source.contains("var<storage, read> shape_data: array<ShapeData>;"),
20946 "storage-mode shader must declare a runtime-sized shape array"
20947 );
20948 assert!(
20949 source.contains("var<storage, read> gradient_stops: array<GradientStop>;"),
20950 "storage-mode shader must declare a runtime-sized gradient array"
20951 );
20952 assert!(
20953 !source.contains("var<uniform> shape_data"),
20954 "the uniform shape declaration must be fully replaced"
20955 );
20956 assert!(
20957 !source.contains("var<uniform> gradient_stops"),
20958 "the uniform gradient declaration must be fully replaced"
20959 );
20960 assert!(
20961 source.contains("var<storage, read> paint: array<vec4<f32>>;"),
20962 "storage-mode shader must declare the retained paint array"
20963 );
20964 assert!(
20965 source.contains("select(shape.color, paint[shape_idx], similarity.paint_select > 0.5)"),
20966 "storage-mode shader must read paint under the paint_select flag"
20967 );
20968 assert!(
20969 source.contains("fn vs_mesh("),
20970 "the storage rewrite must leave the retained-mesh vertex entry intact"
20971 );
20972 assert!(
20973 source.contains("fn vs_shape_instanced("),
20974 "the storage rewrite must leave the instanced-quad vertex entry intact"
20975 );
20976 assert_eq!(
20977 source
20978 .matches("select(shape.color, paint[shape_idx], similarity.paint_select > 0.5)")
20979 .count(),
20980 3,
20981 "vs_main, vs_shape_instanced and vs_mesh must all read paint under \
20982 the paint_select flag (meshless retained draws ride the instanced \
20983 entry when the selection is latched on)"
20984 );
20985
20986 let module = naga::front::wgsl::parse_str(&source)
20989 .expect("storage-mode shape shader must parse as WGSL");
20990 naga::valid::Validator::new(
20991 naga::valid::ValidationFlags::all(),
20992 naga::valid::Capabilities::all(),
20993 )
20994 .validate(&module)
20995 .expect("storage-mode shape shader must validate for WebGPU");
20996 }
20997
20998 #[test]
20999 fn uniform_shape_shader_keeps_the_in_record_color_and_no_paint_binding() {
21000 for source in [
21004 Cow::Borrowed(shaders::SHADER),
21005 shape_shader_source(ShapeBatchLimits::desktop()),
21006 ] {
21007 assert!(
21008 !source.contains("paint: array"),
21009 "the uniform variant must not declare a paint array"
21010 );
21011 assert!(
21012 source.contains("output.color = shape.color;"),
21013 "the uniform variant must read the color from ShapeData \
21014 (this literal is also what `shape_shader_source` rewrites)"
21015 );
21016 assert!(
21017 source.contains("paint_select: f32"),
21018 "SimilarityTransform must name the flag field in both \
21019 variants; the Rust mirror is Pod and uploads raw bytes"
21020 );
21021 }
21022 }
21023
21024 #[test]
21025 fn shipped_shape_shader_array_length_fits_the_downlevel_uniform_floor() {
21026 assert!(
21030 shaders::SHADER.contains("array<ShapeData, 102>"),
21031 "shape.wgsl array length must stay in sync with \
21032 `shape_shader_source`'s replace string and MAX_SHAPES_PER_BATCH"
21033 );
21034 assert!(102 * std::mem::size_of::<ShapeData>() <= 16384);
21035 assert!(103 * std::mem::size_of::<ShapeData>() > 16384);
21036 }
21037
21038 #[test]
21039 fn glyph_atlas_doubles_on_overflow_and_stops_at_the_device_ceiling() {
21040 assert_eq!(
21043 next_glyph_atlas_size(TEXT_GLYPH_ATLAS_MIN_SIZE, TEXT_GLYPH_ATLAS_MAX_SIZE),
21044 1024
21045 );
21046 assert_eq!(
21047 next_glyph_atlas_size(2048, TEXT_GLYPH_ATLAS_MAX_SIZE),
21048 TEXT_GLYPH_ATLAS_MAX_SIZE
21049 );
21050 assert_eq!(
21051 next_glyph_atlas_size(TEXT_GLYPH_ATLAS_MAX_SIZE, TEXT_GLYPH_ATLAS_MAX_SIZE),
21052 TEXT_GLYPH_ATLAS_MAX_SIZE
21053 );
21054
21055 assert_eq!(next_glyph_atlas_size(1024, 2048), 2048);
21058 assert_eq!(next_glyph_atlas_size(2048, 2048), 2048);
21059
21060 assert_eq!(next_glyph_atlas_size(u32::MAX, 4096), 4096);
21062 assert_eq!(next_glyph_atlas_size(0, 0), 1);
21063 }
21064
21065 #[test]
21066 fn glyph_atlas_uv_rect_normalizes_against_the_atlas_it_was_placed_in() {
21067 let entry = GlyphAtlasEntry {
21071 x: 128,
21072 y: 256,
21073 width: 16,
21074 height: 32,
21075 };
21076
21077 let small = glyph_atlas_uv_rect(entry, 512);
21078 let large = glyph_atlas_uv_rect(entry, 4096);
21079
21080 assert_eq!(small.min, [128.0 / 512.0, 256.0 / 512.0]);
21081 assert_eq!(large.min, [128.0 / 4096.0, 256.0 / 4096.0]);
21082 assert_eq!(small.max, [144.0 / 512.0, 288.0 / 512.0]);
21083 assert_eq!(large.max, [144.0 / 4096.0, 288.0 / 4096.0]);
21084 }
21085
21086 #[test]
21087 fn native_shape_shader_source_uses_native_batch_limits() {
21088 let limits = ShapeBatchLimits::desktop();
21089 let source = shape_shader_source(limits);
21090
21091 assert!(source.contains(&format!(
21092 "array<ShapeData, {}>",
21093 limits.max_shapes_per_batch
21094 )));
21095 assert!(source.contains(&format!(
21096 "array<GradientStop, {}>",
21097 limits.max_gradient_stops
21098 )));
21099 assert!(!source.contains("array<ShapeData, 146>"));
21102 }
21103
21104 #[test]
21105 fn stroked_and_arc_shapes_batch_together_with_fills() {
21106 let fill = test_shape(0, BlendMode::SrcOver);
21112 let mut stroked = test_shape(1, BlendMode::SrcOver);
21113 stroked.stroke = Some(
21114 cranpose_ui_graphics::Stroke::new(3.0)
21115 .with_cap(StrokeCap::Round)
21116 .with_join(StrokeJoin::Bevel),
21117 );
21118 let mut arc = test_shape(2, BlendMode::SrcOver);
21119 arc.arc = Some(cranpose_ui_graphics::ArcGeometry::new(
21120 Point::new(4.0, 4.0),
21121 2.0,
21122 4.0,
21123 0.0,
21124 1.0,
21125 StrokeCap::Round,
21126 ));
21127 let trailing_fill = test_shape(3, BlendMode::SrcOver);
21128
21129 assert!(!fill.has_stroke_or_arc());
21130 assert!(stroked.has_stroke_or_arc());
21131 assert!(arc.has_stroke_or_arc());
21132 assert!(!trailing_fill.has_stroke_or_arc());
21133
21134 let shapes = vec![fill, stroked, arc, trailing_fill];
21135 let ordered_items: Vec<_> = (0..shapes.len())
21136 .map(|index| (index, SegmentDrawItem::Shape(index)))
21137 .collect();
21138 let images = Vec::new();
21139
21140 let commands: Vec<_> = SegmentCommandIter::new(
21141 &ordered_items,
21142 &shapes,
21143 &images,
21144 ShapeBatchLimits::desktop(),
21145 )
21146 .collect();
21147
21148 assert_eq!(
21149 commands,
21150 vec![SegmentRenderCommand::DrawChunk(chunk(&[
21151 SegmentBatchPlan::Shape {
21152 start: 0,
21153 end: 4,
21154 blend_mode: BlendMode::SrcOver,
21155 }
21156 ]))],
21157 "mixed fill/stroke/arc runs must stay one batch"
21158 );
21159 }
21160
21161 #[cfg(not(target_arch = "wasm32"))]
21162 #[test]
21163 fn native_segment_fusion_budget_allows_small_interleaved_chunks() {
21164 let ordered_items = vec![
21165 (0, SegmentDrawItem::Shape(0)),
21166 (1, SegmentDrawItem::Image(0)),
21167 (2, SegmentDrawItem::Text(0)),
21168 (3, SegmentDrawItem::Shape(1)),
21169 ];
21170 let shapes = vec![
21171 test_shape(0, BlendMode::SrcOver),
21172 test_shape(3, BlendMode::DstOut),
21173 ];
21174 let segment = chunk(&[
21175 SegmentBatchPlan::Shape {
21176 start: 0,
21177 end: 1,
21178 blend_mode: BlendMode::SrcOver,
21179 },
21180 SegmentBatchPlan::Image {
21181 start: 1,
21182 end: 2,
21183 blend_mode: BlendMode::SrcOver,
21184 },
21185 SegmentBatchPlan::Text { start: 2, end: 3 },
21186 SegmentBatchPlan::Shape {
21187 start: 3,
21188 end: 4,
21189 blend_mode: BlendMode::DstOut,
21190 },
21191 ]);
21192
21193 let budget = native_segment_fusion_budget(
21194 &ordered_items,
21195 &shapes,
21196 &[],
21197 &segment,
21198 ShapeBatchLimits::desktop(),
21199 )
21200 .expect("budget should be valid")
21201 .expect("chunk should fit native fusion budget");
21202
21203 assert_eq!(
21204 budget,
21205 NativeSegmentFusionBudget {
21206 shape_count: 2,
21207 gradient_stop_count: 0,
21208 }
21209 );
21210 }
21211
21212 #[cfg(not(target_arch = "wasm32"))]
21213 #[test]
21214 fn native_segment_fusion_budget_rejects_shape_uniform_overflow() {
21215 let ordered_items: Vec<_> = (0..=MAX_SHAPES_PER_BATCH)
21216 .map(|index| (index, SegmentDrawItem::Shape(index)))
21217 .collect();
21218 let shapes: Vec<_> = (0..=MAX_SHAPES_PER_BATCH)
21219 .map(|index| test_shape(index, BlendMode::SrcOver))
21220 .collect();
21221 let segment = chunk(&[
21222 SegmentBatchPlan::Shape {
21223 start: 0,
21224 end: MAX_SHAPES_PER_BATCH,
21225 blend_mode: BlendMode::SrcOver,
21226 },
21227 SegmentBatchPlan::Shape {
21228 start: MAX_SHAPES_PER_BATCH,
21229 end: MAX_SHAPES_PER_BATCH + 1,
21230 blend_mode: BlendMode::SrcOver,
21231 },
21232 ]);
21233
21234 let budget = native_segment_fusion_budget(
21235 &ordered_items,
21236 &shapes,
21237 &[],
21238 &segment,
21239 ShapeBatchLimits::desktop(),
21240 )
21241 .expect("valid plan");
21242
21243 assert_eq!(budget, None);
21244 }
21245
21246 #[cfg(not(target_arch = "wasm32"))]
21247 #[test]
21248 fn native_segment_fusion_budget_rejects_gradient_uniform_overflow() {
21249 let ordered_items = vec![(0, SegmentDrawItem::Shape(0))];
21250 let mut shape = test_shape(0, BlendMode::SrcOver);
21251 let brushes = vec![Brush::linear_gradient(vec![
21252 Color::BLACK;
21253 MAX_GRADIENT_STOPS + 1
21254 ])];
21255 shape.brush = SceneBrush::Gradient(0);
21256 let shapes = vec![shape];
21257 let segment = chunk(&[SegmentBatchPlan::Shape {
21258 start: 0,
21259 end: 1,
21260 blend_mode: BlendMode::SrcOver,
21261 }]);
21262
21263 let budget = native_segment_fusion_budget(
21264 &ordered_items,
21265 &shapes,
21266 &brushes,
21267 &segment,
21268 ShapeBatchLimits::desktop(),
21269 )
21270 .expect("valid plan");
21271
21272 assert_eq!(budget, None);
21273 }
21274
21275 #[cfg(not(target_arch = "wasm32"))]
21276 #[test]
21277 fn native_segment_fusion_partitions_shape_uniform_overflow() {
21278 let desktop_batch_cap = ShapeBatchLimits::desktop().max_shapes_per_batch;
21281 let ordered_items: Vec<_> = (0..=desktop_batch_cap)
21282 .map(|index| (index, SegmentDrawItem::Shape(index)))
21283 .collect();
21284 let shapes: Vec<_> = (0..=desktop_batch_cap)
21285 .map(|index| test_shape(index, BlendMode::SrcOver))
21286 .collect();
21287 let segment = chunk(&[
21288 SegmentBatchPlan::Shape {
21289 start: 0,
21290 end: desktop_batch_cap,
21291 blend_mode: BlendMode::SrcOver,
21292 },
21293 SegmentBatchPlan::Shape {
21294 start: desktop_batch_cap,
21295 end: desktop_batch_cap + 1,
21296 blend_mode: BlendMode::SrcOver,
21297 },
21298 ]);
21299
21300 let partitions = native_segment_fusion_partitions(
21301 &ordered_items,
21302 &shapes,
21303 &[],
21304 &segment,
21305 ShapeBatchLimits::desktop(),
21306 )
21307 .expect("valid plan")
21308 .expect("overflowing segment should be partitionable");
21309
21310 assert_eq!(partitions.len(), 2);
21311 assert_eq!(
21312 partitions[0],
21313 NativeSegmentFusionPartition {
21314 chunk: chunk(&[SegmentBatchPlan::Shape {
21315 start: 0,
21316 end: desktop_batch_cap,
21317 blend_mode: BlendMode::SrcOver,
21318 }]),
21319 budget: NativeSegmentFusionBudget {
21320 shape_count: desktop_batch_cap,
21321 gradient_stop_count: 0,
21322 },
21323 }
21324 );
21325 assert_eq!(
21326 partitions[1],
21327 NativeSegmentFusionPartition {
21328 chunk: chunk(&[SegmentBatchPlan::Shape {
21329 start: desktop_batch_cap,
21330 end: desktop_batch_cap + 1,
21331 blend_mode: BlendMode::SrcOver,
21332 }]),
21333 budget: NativeSegmentFusionBudget {
21334 shape_count: 1,
21335 gradient_stop_count: 0,
21336 },
21337 }
21338 );
21339 }
21340
21341 #[cfg(not(target_arch = "wasm32"))]
21342 #[test]
21343 fn native_segment_fusion_partitions_gradient_uniform_overflow() {
21344 const STOPS_PER_SHAPE: usize = MAX_GRADIENT_STOPS / 2;
21345 let ordered_items = vec![
21346 (0, SegmentDrawItem::Shape(0)),
21347 (1, SegmentDrawItem::Shape(1)),
21348 (2, SegmentDrawItem::Shape(2)),
21349 ];
21350 let mut shapes = Vec::new();
21351 let brushes = vec![Brush::linear_gradient(vec![Color::BLACK; STOPS_PER_SHAPE])];
21352 for index in 0..3 {
21353 let mut shape = test_shape(index, BlendMode::SrcOver);
21354 shape.brush = SceneBrush::Gradient(0);
21355 shapes.push(shape);
21356 }
21357 let segment = chunk(&[SegmentBatchPlan::Shape {
21358 start: 0,
21359 end: 3,
21360 blend_mode: BlendMode::SrcOver,
21361 }]);
21362
21363 let partitions = native_segment_fusion_partitions(
21364 &ordered_items,
21365 &shapes,
21366 &brushes,
21367 &segment,
21368 ShapeBatchLimits::desktop(),
21369 )
21370 .expect("valid plan")
21371 .expect("overflowing gradient segment should be partitionable");
21372
21373 assert_eq!(partitions.len(), 2);
21374 assert_eq!(
21375 partitions[0],
21376 NativeSegmentFusionPartition {
21377 chunk: chunk(&[SegmentBatchPlan::Shape {
21378 start: 0,
21379 end: 2,
21380 blend_mode: BlendMode::SrcOver,
21381 }]),
21382 budget: NativeSegmentFusionBudget {
21383 shape_count: 2,
21384 gradient_stop_count: MAX_GRADIENT_STOPS,
21385 },
21386 }
21387 );
21388 assert_eq!(
21389 partitions[1],
21390 NativeSegmentFusionPartition {
21391 chunk: chunk(&[SegmentBatchPlan::Shape {
21392 start: 2,
21393 end: 3,
21394 blend_mode: BlendMode::SrcOver,
21395 }]),
21396 budget: NativeSegmentFusionBudget {
21397 shape_count: 1,
21398 gradient_stop_count: STOPS_PER_SHAPE,
21399 },
21400 }
21401 );
21402 }
21403
21404 #[cfg(not(target_arch = "wasm32"))]
21405 #[test]
21406 fn native_segment_fusion_accepts_layer_composite_chunks() {
21407 let ordered_items = vec![
21408 (0, SegmentDrawItem::Shape(0)),
21409 (1, SegmentDrawItem::Composite(0)),
21410 (2, SegmentDrawItem::ShaderComposite(0)),
21411 (3, SegmentDrawItem::Shape(1)),
21412 ];
21413 let shapes = vec![
21414 test_shape(0, BlendMode::SrcOver),
21415 test_shape(1, BlendMode::SrcOver),
21416 ];
21417 let segment = chunk(&[
21418 SegmentBatchPlan::Shape {
21419 start: 0,
21420 end: 1,
21421 blend_mode: BlendMode::SrcOver,
21422 },
21423 SegmentBatchPlan::Composite { start: 1, end: 2 },
21424 SegmentBatchPlan::ShaderComposite { start: 2, end: 3 },
21425 SegmentBatchPlan::Shape {
21426 start: 3,
21427 end: 4,
21428 blend_mode: BlendMode::SrcOver,
21429 },
21430 ]);
21431
21432 let partitions = native_segment_fusion_partitions(
21433 &ordered_items,
21434 &shapes,
21435 &[],
21436 &segment,
21437 ShapeBatchLimits::desktop(),
21438 )
21439 .expect("valid plan")
21440 .expect("composites are drawable inside the native fused pass");
21441
21442 assert_eq!(
21443 partitions,
21444 vec![NativeSegmentFusionPartition {
21445 chunk: segment,
21446 budget: NativeSegmentFusionBudget {
21447 shape_count: 2,
21448 gradient_stop_count: 0,
21449 },
21450 }],
21451 "layer composites and shader composites must preserve order without forcing separate render passes"
21452 );
21453 }
21454
21455 #[cfg(not(target_arch = "wasm32"))]
21456 #[test]
21457 fn native_segment_fusion_partitions_preserve_non_shape_order_at_budget_boundary() {
21458 let desktop_batch_cap = ShapeBatchLimits::desktop().max_shapes_per_batch;
21461 let ordered_items: Vec<_> = (0..desktop_batch_cap)
21462 .map(|index| (index, SegmentDrawItem::Shape(index)))
21463 .chain([
21464 (desktop_batch_cap, SegmentDrawItem::Image(0)),
21465 (
21466 desktop_batch_cap + 1,
21467 SegmentDrawItem::Shape(desktop_batch_cap),
21468 ),
21469 ])
21470 .collect();
21471 let shapes: Vec<_> = (0..=desktop_batch_cap)
21472 .map(|index| test_shape(index, BlendMode::SrcOver))
21473 .collect();
21474 let segment = chunk(&[
21475 SegmentBatchPlan::Shape {
21476 start: 0,
21477 end: desktop_batch_cap,
21478 blend_mode: BlendMode::SrcOver,
21479 },
21480 SegmentBatchPlan::Image {
21481 start: desktop_batch_cap,
21482 end: desktop_batch_cap + 1,
21483 blend_mode: BlendMode::SrcOver,
21484 },
21485 SegmentBatchPlan::Shape {
21486 start: desktop_batch_cap + 1,
21487 end: desktop_batch_cap + 2,
21488 blend_mode: BlendMode::SrcOver,
21489 },
21490 ]);
21491
21492 let partitions = native_segment_fusion_partitions(
21493 &ordered_items,
21494 &shapes,
21495 &[],
21496 &segment,
21497 ShapeBatchLimits::desktop(),
21498 )
21499 .expect("valid plan")
21500 .expect("overflowing segment should be partitionable");
21501
21502 assert_eq!(partitions.len(), 2);
21503 assert_eq!(
21504 partitions[0].chunk,
21505 chunk(&[
21506 SegmentBatchPlan::Shape {
21507 start: 0,
21508 end: desktop_batch_cap,
21509 blend_mode: BlendMode::SrcOver,
21510 },
21511 SegmentBatchPlan::Image {
21512 start: desktop_batch_cap,
21513 end: desktop_batch_cap + 1,
21514 blend_mode: BlendMode::SrcOver,
21515 },
21516 ])
21517 );
21518 assert_eq!(
21519 partitions[1].chunk,
21520 chunk(&[SegmentBatchPlan::Shape {
21521 start: desktop_batch_cap + 1,
21522 end: desktop_batch_cap + 2,
21523 blend_mode: BlendMode::SrcOver,
21524 }])
21525 );
21526 }
21527
21528 #[test]
21529 fn segment_command_iter_keeps_repeated_batch_kinds_in_one_chunk() {
21530 let ordered_items = vec![
21531 (0, SegmentDrawItem::Shape(0)),
21532 (1, SegmentDrawItem::Image(0)),
21533 (2, SegmentDrawItem::Shape(1)),
21534 ];
21535 let shapes = vec![
21536 test_shape(0, BlendMode::SrcOver),
21537 test_shape(2, BlendMode::DstOut),
21538 ];
21539 let images = vec![test_image(1, BlendMode::SrcOver)];
21540
21541 let commands: Vec<_> = SegmentCommandIter::new(
21542 &ordered_items,
21543 &shapes,
21544 &images,
21545 ShapeBatchLimits::desktop(),
21546 )
21547 .collect();
21548
21549 assert_eq!(
21550 commands,
21551 vec![SegmentRenderCommand::DrawChunk(chunk(&[
21552 SegmentBatchPlan::Shape {
21553 start: 0,
21554 end: 1,
21555 blend_mode: BlendMode::SrcOver,
21556 },
21557 SegmentBatchPlan::Image {
21558 start: 1,
21559 end: 2,
21560 blend_mode: BlendMode::SrcOver,
21561 },
21562 SegmentBatchPlan::Shape {
21563 start: 2,
21564 end: 3,
21565 blend_mode: BlendMode::DstOut,
21566 },
21567 ]))]
21568 );
21569 }
21570
21571 #[test]
21572 fn segment_command_iter_splits_contiguous_shape_runs_at_uniform_batch_limit() {
21573 let desktop_batch_cap = ShapeBatchLimits::desktop().max_shapes_per_batch;
21576 let ordered_items: Vec<_> = (0..=desktop_batch_cap)
21577 .map(|index| (index, SegmentDrawItem::Shape(index)))
21578 .collect();
21579 let shapes: Vec<_> = (0..=desktop_batch_cap)
21580 .map(|index| test_shape(index, BlendMode::SrcOver))
21581 .collect();
21582 let images = Vec::new();
21583
21584 let commands: Vec<_> = SegmentCommandIter::new(
21585 &ordered_items,
21586 &shapes,
21587 &images,
21588 ShapeBatchLimits::desktop(),
21589 )
21590 .collect();
21591
21592 assert_eq!(
21593 commands,
21594 vec![SegmentRenderCommand::DrawChunk(chunk(&[
21595 SegmentBatchPlan::Shape {
21596 start: 0,
21597 end: desktop_batch_cap,
21598 blend_mode: BlendMode::SrcOver,
21599 },
21600 SegmentBatchPlan::Shape {
21601 start: desktop_batch_cap,
21602 end: desktop_batch_cap + 1,
21603 blend_mode: BlendMode::SrcOver,
21604 },
21605 ]))]
21606 );
21607 }
21608
21609 #[test]
21610 fn segment_command_iter_keeps_shadows_as_explicit_boundaries() {
21611 let ordered_items = vec![
21612 (0, SegmentDrawItem::Shape(0)),
21613 (1, SegmentDrawItem::Shadow(0)),
21614 (2, SegmentDrawItem::Image(0)),
21615 (3, SegmentDrawItem::Text(0)),
21616 ];
21617 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
21618 let images = vec![test_image(2, BlendMode::SrcOver)];
21619
21620 let commands: Vec<_> = SegmentCommandIter::new(
21621 &ordered_items,
21622 &shapes,
21623 &images,
21624 ShapeBatchLimits::desktop(),
21625 )
21626 .collect();
21627
21628 assert_eq!(
21629 commands,
21630 vec![
21631 SegmentRenderCommand::DrawChunk(chunk(&[SegmentBatchPlan::Shape {
21632 start: 0,
21633 end: 1,
21634 blend_mode: BlendMode::SrcOver,
21635 }])),
21636 SegmentRenderCommand::Shadow(0),
21637 SegmentRenderCommand::DrawChunk(chunk(&[
21638 SegmentBatchPlan::Image {
21639 start: 2,
21640 end: 3,
21641 blend_mode: BlendMode::SrcOver,
21642 },
21643 SegmentBatchPlan::Text { start: 3, end: 4 },
21644 ])),
21645 ]
21646 );
21647 }
21648
21649 #[test]
21650 fn staged_buffer_uploads_align_new_copies_to_copy_buffer_alignment() {
21651 let mut uploads = StagedBufferUploads::default();
21652 uploads.bytes.extend_from_slice(&[1, 2]);
21653
21654 uploads.stage(UploadTarget::ImageIndex, &[3, 4, 5, 6]);
21655
21656 assert_eq!(uploads.bytes, vec![1, 2, 0, 0, 3, 4, 5, 6]);
21657 assert_eq!(
21658 uploads.copies,
21659 vec![PendingBufferCopy {
21660 source_offset: 4,
21661 target_offset: 0,
21662 size: 4,
21663 target: UploadTarget::ImageIndex,
21664 }]
21665 );
21666 }
21667
21668 #[test]
21669 fn staged_buffer_uploads_ignore_empty_payloads() {
21670 let mut uploads = StagedBufferUploads::default();
21671
21672 uploads.stage(UploadTarget::Uniform, &[]);
21673
21674 assert!(uploads.is_empty());
21675 assert!(uploads.bytes.is_empty());
21676 }
21677
21678 #[test]
21679 fn staged_buffer_uploads_return_exact_payload_slice_for_copy() {
21680 let mut uploads = StagedBufferUploads::default();
21681 uploads.stage(UploadTarget::Uniform, &[1, 2, 3, 4]);
21682 uploads.stage(UploadTarget::ImageIndex, &[5, 6, 7, 8]);
21683
21684 assert_eq!(uploads.payload_for_copy(uploads.copies[0]), &[1, 2, 3, 4]);
21685 assert_eq!(uploads.payload_for_copy(uploads.copies[1]), &[5, 6, 7, 8]);
21686 }
21687
21688 #[test]
21689 fn staged_buffer_uploads_record_destination_offsets() {
21690 let mut uploads = StagedBufferUploads::default();
21691
21692 uploads.stage_at(UploadTarget::ImageIndex, 256, &[1, 2, 3, 4]);
21693
21694 assert_eq!(uploads.copies[0].target_offset, 256);
21695 assert_eq!(uploads.payload_for_copy(uploads.copies[0]), &[1, 2, 3, 4]);
21696 }
21697
21698 #[test]
21699 fn staged_buffer_uploads_truncate_restores_previous_state() {
21700 let mut uploads = StagedBufferUploads::default();
21701 uploads.stage(UploadTarget::Uniform, &[1, 2, 3, 4]);
21702 let bytes_len = uploads.bytes.len();
21703 let copies_len = uploads.copies.len();
21704 uploads.stage(UploadTarget::ImageIndex, &[5, 6, 7, 8]);
21705
21706 uploads.truncate(bytes_len, copies_len);
21707
21708 assert_eq!(uploads.bytes, vec![1, 2, 3, 4]);
21709 assert_eq!(uploads.copies.len(), 1);
21710 }
21711
21712 #[test]
21713 fn inner_shadow_composite_mask_uses_fill_shape_and_scale() {
21714 let mut fill = test_shape(0, BlendMode::SrcOver);
21715 fill.local_rect = Rect {
21716 x: 10.0,
21717 y: 12.0,
21718 width: 40.0,
21719 height: 20.0,
21720 };
21721 fill.shape = Some(RoundedCornerShape::uniform(6.0));
21722
21723 let cutout = test_shape(1, BlendMode::DstOut);
21724 let shadow = test_shadow_draw(vec![
21725 (fill, BlendMode::SrcOver),
21726 (cutout, BlendMode::DstOut),
21727 ]);
21728
21729 let mask = inner_shadow_composite_mask(&shadow, 1.5).expect("inner mask expected");
21730 assert_eq!(mask.rect, [15.0, 18.0, 60.0, 30.0]);
21731 assert_eq!(mask.radii, [9.0, 9.0, 9.0, 9.0]);
21732 }
21733
21734 #[test]
21735 fn inner_shadow_composite_mask_is_none_without_dst_out() {
21736 let fill = test_shape(0, BlendMode::SrcOver);
21737 let shadow = test_shadow_draw(vec![(fill, BlendMode::SrcOver)]);
21738 assert!(inner_shadow_composite_mask(&shadow, 1.0).is_none());
21739 }
21740
21741 #[test]
21742 fn render_effect_support_matrix_covers_all_variants() {
21743 let blur = RenderEffect::blur(4.0);
21744 let offset = RenderEffect::offset(2.0, 3.0);
21745 let shader = RenderEffect::runtime_shader(cranpose_ui_graphics::RuntimeShader::new(
21746 r#"
21747 @group(0) @binding(0) var input_texture: texture_2d<f32>;
21748 @group(0) @binding(1) var input_sampler: sampler;
21749 @group(1) @binding(0) var<uniform> u: array<vec4<f32>, 64>;
21750 struct VertexOutput {
21751 @builtin(position) position: vec4<f32>,
21752 @location(0) uv: vec2<f32>,
21753 }
21754 @vertex
21755 fn fullscreen_vs(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
21756 var output: VertexOutput;
21757 let x = f32(i32(vertex_index & 1u) * 2 - 1);
21758 let y = f32(i32(vertex_index >> 1u) * 2 - 1);
21759 output.uv = vec2<f32>(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
21760 output.position = vec4<f32>(x, y, 0.0, 1.0);
21761 return output;
21762 }
21763 @fragment
21764 fn effect_fs(input: VertexOutput) -> @location(0) vec4<f32> {
21765 return textureSample(input_texture, input_sampler, input.uv);
21766 }
21767 "#,
21768 ));
21769 let chain = blur.clone().then(offset.clone());
21770
21771 assert!(is_render_effect_supported(&blur));
21772 assert!(is_render_effect_supported(&offset));
21773 assert!(is_render_effect_supported(&shader));
21774 assert!(is_render_effect_supported(&chain));
21775 }
21776
21777 #[test]
21778 fn clip_to_bounds_propagates_visual_clip_to_all_descendant_shapes() {
21779 let container_local_bounds = Rect {
21782 x: 0.0,
21783 y: 0.0,
21784 width: 800.0,
21785 height: 500.0,
21786 };
21787 let container_clip_in_parent = Rect {
21789 x: 0.0,
21790 y: 50.0,
21791 width: 800.0,
21792 height: 500.0,
21793 };
21794
21795 let shape_above = RenderNode::Primitive(PrimitiveEntry {
21797 phase: PrimitivePhase::BeforeChildren,
21798 node: PrimitiveNode::Draw(DrawPrimitiveNode {
21799 primitive: DrawPrimitive::Rect {
21800 rect: Rect {
21801 x: 10.0,
21802 y: -30.0,
21803 width: 100.0,
21804 height: 40.0,
21805 },
21806 brush: Brush::solid(Color::WHITE),
21807 stroke: None,
21808 },
21809 clip: None,
21810 }),
21811 });
21812
21813 let shape_inside = RenderNode::Primitive(PrimitiveEntry {
21815 phase: PrimitivePhase::BeforeChildren,
21816 node: PrimitiveNode::Draw(DrawPrimitiveNode {
21817 primitive: DrawPrimitive::Rect {
21818 rect: Rect {
21819 x: 10.0,
21820 y: 100.0,
21821 width: 100.0,
21822 height: 40.0,
21823 },
21824 brush: Brush::solid(Color::WHITE),
21825 stroke: None,
21826 },
21827 clip: None,
21828 }),
21829 });
21830
21831 let shape_below = RenderNode::Primitive(PrimitiveEntry {
21833 phase: PrimitivePhase::BeforeChildren,
21834 node: PrimitiveNode::Draw(DrawPrimitiveNode {
21835 primitive: DrawPrimitive::Rect {
21836 rect: Rect {
21837 x: 10.0,
21838 y: 600.0,
21839 width: 100.0,
21840 height: 40.0,
21841 },
21842 brush: Brush::solid(Color::WHITE),
21843 stroke: None,
21844 },
21845 clip: None,
21846 }),
21847 });
21848
21849 let mut content_layer = test_layer(
21851 Rect {
21852 x: 0.0,
21853 y: 0.0,
21854 width: 800.0,
21855 height: 1000.0,
21856 },
21857 vec![shape_above, shape_inside, shape_below],
21858 );
21859 content_layer.transform_to_parent = ProjectiveTransform::translation(0.0, -30.0);
21860 content_layer.translated_content_context = true;
21861
21862 let mut clip_container = test_layer(
21864 container_local_bounds,
21865 vec![RenderNode::Layer(Box::new(content_layer))],
21866 );
21867 clip_container.clip_to_bounds = true;
21868 clip_container.transform_to_parent = ProjectiveTransform::translation(0.0, 50.0);
21869
21870 let root = test_layer(
21872 Rect {
21873 x: 0.0,
21874 y: 0.0,
21875 width: 800.0,
21876 height: 600.0,
21877 },
21878 vec![RenderNode::Layer(Box::new(clip_container))],
21879 );
21880
21881 let mut rect_cache = HashMap::new();
21882 let mut requirements_cache = HashMap::new();
21883 let collected =
21884 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21885
21886 assert_eq!(
21887 collected.scene.shapes.len(),
21888 3,
21889 "all three shapes should be flattened into the scene"
21890 );
21891
21892 for (i, shape) in collected.scene.shapes.iter().enumerate() {
21893 assert!(
21894 shape.clip.is_some(),
21895 "shape {} at rect {:?} must have a clip from clip_to_bounds container, but clip is None",
21896 i,
21897 shape.rect
21898 );
21899 let clip = shape.clip.unwrap();
21900 assert_eq!(
21901 clip, container_clip_in_parent,
21902 "shape {} clip should match the clip_to_bounds container bounds in parent space",
21903 i
21904 );
21905 }
21906 }
21907
21908 #[test]
21909 fn clip_to_bounds_culls_child_layers_outside_boundary() {
21910 let clip_container_bounds = Rect {
21918 x: 0.0,
21919 y: 0.0,
21920 width: 800.0,
21921 height: 500.0,
21922 };
21923
21924 let shape_in_card = RenderNode::Primitive(PrimitiveEntry {
21925 phase: PrimitivePhase::BeforeChildren,
21926 node: PrimitiveNode::Draw(DrawPrimitiveNode {
21927 primitive: DrawPrimitive::Rect {
21928 rect: Rect {
21929 x: 0.0,
21930 y: 0.0,
21931 width: 300.0,
21932 height: 80.0,
21933 },
21934 brush: Brush::solid(Color::WHITE),
21935 stroke: None,
21936 },
21937 clip: None,
21938 }),
21939 });
21940
21941 let mut card_outside = crate::test_support::layer_node(
21943 Rect {
21944 x: 0.0,
21945 y: 0.0,
21946 width: 300.0,
21947 height: 80.0,
21948 },
21949 ProjectiveTransform::identity(),
21950 GraphicsLayer {
21951 clip: true,
21952 ..GraphicsLayer::default()
21953 },
21954 vec![shape_in_card.clone()],
21955 );
21956 card_outside.transform_to_parent = ProjectiveTransform::translation(10.0, 600.0);
21957
21958 let mut card_inside = crate::test_support::layer_node(
21960 Rect {
21961 x: 0.0,
21962 y: 0.0,
21963 width: 300.0,
21964 height: 80.0,
21965 },
21966 ProjectiveTransform::identity(),
21967 GraphicsLayer {
21968 clip: true,
21969 ..GraphicsLayer::default()
21970 },
21971 vec![shape_in_card],
21972 );
21973 card_inside.transform_to_parent = ProjectiveTransform::translation(10.0, 100.0);
21974
21975 let content = test_layer(
21977 Rect {
21978 x: 0.0,
21979 y: 0.0,
21980 width: 800.0,
21981 height: 1000.0,
21982 },
21983 vec![
21984 RenderNode::Layer(Box::new(card_inside)),
21985 RenderNode::Layer(Box::new(card_outside)),
21986 ],
21987 );
21988
21989 let mut clip_container = test_layer(
21991 clip_container_bounds,
21992 vec![RenderNode::Layer(Box::new(content))],
21993 );
21994 clip_container.clip_to_bounds = true;
21995
21996 let root = test_layer(
21998 Rect {
21999 x: 0.0,
22000 y: 0.0,
22001 width: 800.0,
22002 height: 600.0,
22003 },
22004 vec![RenderNode::Layer(Box::new(clip_container))],
22005 );
22006
22007 let mut rect_cache = HashMap::new();
22008 let mut requirements_cache = HashMap::new();
22009 let collected =
22010 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
22011
22012 assert_eq!(
22013 collected.scene.shapes.len(),
22014 1,
22015 "only the card inside the clip boundary should produce shapes; \
22016 the card outside must be culled entirely"
22017 );
22018
22019 let shape = &collected.scene.shapes[0];
22020 assert!(
22021 shape.clip.is_some(),
22022 "the visible card's shape must have a clip from clip_to_bounds"
22023 );
22024 }
22025
22026 #[test]
22027 fn flattened_layer_shadow_z_index_is_below_content() {
22028 let shape = RenderNode::Primitive(PrimitiveEntry {
22032 phase: PrimitivePhase::BeforeChildren,
22033 node: PrimitiveNode::Draw(DrawPrimitiveNode {
22034 primitive: DrawPrimitive::Rect {
22035 rect: Rect {
22036 x: 0.0,
22037 y: 0.0,
22038 width: 100.0,
22039 height: 100.0,
22040 },
22041 brush: Brush::solid(Color::WHITE),
22042 stroke: None,
22043 },
22044 clip: None,
22045 }),
22046 });
22047
22048 let child_bounds = Rect {
22049 x: 0.0,
22050 y: 0.0,
22051 width: 100.0,
22052 height: 100.0,
22053 };
22054
22055 let child = crate::test_support::layer_node(
22056 child_bounds,
22057 ProjectiveTransform::translation(50.0, 50.0),
22058 GraphicsLayer {
22059 shadow_elevation: 20.0,
22060 ..GraphicsLayer::default()
22061 },
22062 vec![shape],
22063 );
22064
22065 let root = test_layer(
22066 Rect {
22067 x: 0.0,
22068 y: 0.0,
22069 width: 800.0,
22070 height: 600.0,
22071 },
22072 vec![RenderNode::Layer(Box::new(child))],
22073 );
22074
22075 let mut rect_cache = HashMap::new();
22076 let mut requirements_cache = HashMap::new();
22077 let collected =
22078 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
22079
22080 assert!(
22081 !collected.scene.shadow_draws.is_empty(),
22082 "shadow_elevation > 0 must produce shadow draws"
22083 );
22084 let max_shadow_z = collected
22085 .scene
22086 .shadow_draws
22087 .iter()
22088 .map(|s| s.z_index)
22089 .max()
22090 .unwrap();
22091 let min_content_z = collected
22092 .scene
22093 .shapes
22094 .iter()
22095 .map(|s| s.z_index)
22096 .min()
22097 .unwrap();
22098 assert!(
22099 max_shadow_z < min_content_z,
22100 "shadow z-index ({}) must be less than content z-index ({}); \
22101 shadows must render behind their content",
22102 max_shadow_z,
22103 min_content_z
22104 );
22105 }
22106
22107 #[cfg(not(target_arch = "wasm32"))]
22110 fn bundle_op(slot: u32, epoch: Option<u64>, first: u32, last: u32) -> RetainedBundleOpKey {
22111 RetainedBundleOpKey {
22112 slot,
22113 capture_epoch: epoch,
22114 first,
22115 last,
22116 retained_index: slot,
22117 has_mesh: false,
22118 }
22119 }
22120
22121 #[cfg(not(target_arch = "wasm32"))]
22122 fn bundle_key(ops: &[RetainedBundleOpKey]) -> RetainedBundleKey {
22123 RetainedBundleKey { ops: ops.to_vec() }
22124 }
22125
22126 #[cfg(not(target_arch = "wasm32"))]
22129 #[test]
22130 fn retained_bundle_cache_reuses_stable_keys() {
22131 let mut cache: RetainedBundleCacheImpl<u32> = RetainedBundleCacheImpl::new();
22132 let ops = [bundle_op(3, Some(7), 0, 40), bundle_op(5, Some(9), 4, 12)];
22133 let key = bundle_key(&ops);
22134
22135 assert!(!cache.hit(&key), "empty cache must miss");
22136 cache.insert(key.clone(), 111);
22137 assert_eq!(cache.get(&key), Some(&111));
22138 cache.end_frame();
22139
22140 for _ in 0..3 {
22141 assert!(cache.hit(&bundle_key(&ops)), "stable key must stay cached");
22142 cache.end_frame();
22143 }
22144 assert_eq!(cache.stats(), (1, 3), "one rebuild, three cached executes");
22145 }
22146
22147 #[cfg(not(target_arch = "wasm32"))]
22151 #[test]
22152 fn retained_bundle_cache_invalidates_on_any_op_change() {
22153 let ops = [bundle_op(3, Some(7), 0, 40), bundle_op(5, Some(9), 4, 12)];
22154 let variants: [Vec<RetainedBundleOpKey>; 5] = [
22155 vec![bundle_op(3, Some(8), 0, 40), bundle_op(5, Some(9), 4, 12)],
22157 vec![bundle_op(5, Some(9), 4, 12), bundle_op(3, Some(7), 0, 40)],
22159 vec![bundle_op(3, Some(7), 0, 40)],
22161 vec![bundle_op(3, Some(7), 0, 41), bundle_op(5, Some(9), 4, 12)],
22163 vec![bundle_op(3, Some(7), 0, 40), bundle_op(5, None, 4, 12)],
22165 ];
22166 for changed in variants {
22167 let mut cache: RetainedBundleCacheImpl<u32> = RetainedBundleCacheImpl::new();
22168 cache.insert(bundle_key(&ops), 111);
22169 cache.end_frame();
22170 assert!(
22171 !cache.hit(&RetainedBundleKey {
22172 ops: changed.clone()
22173 }),
22174 "changed key {changed:?} must not reuse the stale bundle"
22175 );
22176 }
22177 }
22178
22179 #[cfg(not(target_arch = "wasm32"))]
22183 #[test]
22184 fn retained_bundle_cache_evicts_unused_entries() {
22185 let mut cache: RetainedBundleCacheImpl<u32> = RetainedBundleCacheImpl::new();
22186 let stale = bundle_key(&[bundle_op(1, Some(1), 0, 6)]);
22187 let live = bundle_key(&[bundle_op(2, Some(2), 0, 6)]);
22188 cache.insert(stale.clone(), 1);
22189 cache.insert(live.clone(), 2);
22190 cache.end_frame();
22191
22192 assert!(cache.hit(&live));
22193 cache.end_frame();
22194
22195 assert!(
22196 !cache.hit(&stale),
22197 "entry unused for a frame must have been evicted"
22198 );
22199 assert!(cache.hit(&live), "used entry must survive eviction");
22200
22201 cache.clear();
22202 assert!(!cache.hit(&live), "clear must drop every entry");
22203 }
22204}