1use crate::display_clip;
4#[cfg(not(target_arch = "wasm32"))]
5use crate::display_clip::DisplayVisibleRegion;
6use crate::effect_renderer::{
7 projective_dest_bounds_rect, CompositeBatchItem, CompositeSampleMode, EffectRenderer,
8 EffectScratchTargetProvider, ProjectiveSurfaceComposite, RoundedCompositeMask,
9 ShaderCompositeBatchItem,
10};
11#[cfg(not(target_arch = "wasm32"))]
12use crate::effect_renderer::{PreparedProjectiveComposite, ProjectiveCompositeItem};
13use crate::frame_graph::{
14 FrameCommandRecorder, FrameTextureDescriptor, WgpuFrameGraph, WgpuFrameGraphExecutor,
15};
16use crate::frame_packet::{
17 CancelReason, FramePacket, PacketRoot, PresentOutcome, RenderReturns, RootSurfacePacket,
18};
19use crate::layer_events::{
20 collect_effect_ranges, collect_layer_events, LayerEvent, LayerEventKind,
21};
22use crate::layer_surface_cache::LayerSurfaceCache;
23#[cfg(not(target_arch = "wasm32"))]
24use crate::lazy_resource::LazyGpuResource;
25use crate::lazy_resource::PassPipeline;
26#[cfg(test)]
27use crate::normalized_scene::{
28 build_scene_window, collect_layer_contents, collect_layer_contents_with_translation_context,
29 filtered_effect_layer_index, scene_bounds, SceneWindowSource,
30};
31#[cfg(test)]
32use crate::normalized_scene::{estimate_layer_surface_rect, motion_stable_capture_bounds};
33use crate::normalized_scene::{translate_quad, ChildLayerComposite, CollectedLayer};
34use crate::offscreen::{composition_bytes_per_pixel, OffscreenTarget, COMPOSITION_FORMAT};
35use crate::output_conversion::OutputConverter;
36use crate::rect_to_quad;
37use crate::scene::{
38 BackdropLayer, CompositorScene, DrawOp, DrawOpKind, DrawShape, EffectLayer, ImageDraw,
39 RetainedDraw, SceneBrush, ShadowDraw, SimilarityTransform, SnapAnchor, TextDraw,
40};
41#[cfg(not(target_arch = "wasm32"))]
42use crate::segment_surface::{
43 Affine2, CaptureRect, SegmentSurfaceCache, SegmentSurfaceDecision, SegmentSurfaceKey,
44 SEGMENT_CAPTURE_SLOTS, SEGMENT_CAPTURE_UNIFORM_STRIDE,
45};
46use crate::shaders;
47#[cfg(test)]
48use crate::surface_executor::surface_target_size;
49use crate::surface_executor::{
50 apply_backdrop_layer_to_target as execute_apply_backdrop_layer_to_target,
51 axis_aligned_quad_rect, backdrop_underlay_is_covered_by_local_content,
52 canonicalize_device_coordinate, canonicalized_scaled_quad, canonicalized_scaled_rect,
53 composite_surface_to_view as execute_composite_surface_to_view, device_pixel_bounds_for_rect,
54 offscreen_byte_size, render_effect_layer_to_target as execute_render_effect_layer_to_target,
55 render_layer_surface as execute_render_layer_surface,
56 render_root_direct as execute_render_root_direct, root_direct_scene_events_are_supported,
57 scaled_quad, snap_delta_for_anchor, snap_motion_stable_dest_quad,
58 translation_stable_anchored_device_pixel_bounds, DevicePixelBounds, LayerSurfaceTexture,
59 SurfaceExecutionBackend,
60};
61#[cfg(test)]
62use crate::surface_executor::{clamp_effect_surface_scale, visible_layer_rect};
63#[cfg(test)]
64use crate::surface_plan::root_can_render_directly_cached;
65#[cfg(test)]
66use crate::surface_plan::{
67 composite_sample_mode_for_effect_layer, composite_sample_mode_for_requirements,
68 direct_translation, effect_layer_target_scale, layer_contains_descendant_backdrop,
69 layer_surface_requirements, layer_surface_requirements_cached, layer_surface_scale,
70 layer_surface_target_scale, layer_uses_external_backdrop_input, TranslatedContentAxes,
71};
72use crate::surface_plan::{LayerSurfaceRequest, TranslationRenderContext};
73#[cfg(test)]
74use crate::surface_requirements::SurfaceRequirement;
75use crate::surface_requirements::SurfaceRequirementSet;
76use crate::DebugCpuAllocationStats;
77use bytemuck::{Pod, Zeroable};
78#[cfg(any(not(target_arch = "wasm32"), test))]
79use cranpose_core::collections::map::HashMap;
80use cranpose_core::{hash::default as default_hash, NodeId};
81use cranpose_render_common::bounded_lru_cache::BoundedLruCache;
82use cranpose_render_common::geometry::blur_extent_margin;
83use cranpose_render_common::graph::quad_bounds;
84#[cfg(test)]
85use cranpose_render_common::graph::{
86 CachePolicy, LayerNode, PrimitiveEntry, PrimitiveNode, PrimitivePhase, ProjectiveTransform,
87 RenderNode,
88};
89use cranpose_render_common::raster_cache::LayerRasterCacheKey;
90#[cfg(test)]
91use cranpose_render_common::raster_cache::ScaleBucket;
92use cranpose_render_common::software_text_raster::{
93 collect_solid_text_atlas_run, measure_text_with_font,
94 rasterize_annotated_text_to_image_with_glyph_cache, rasterize_text_to_image_with_glyph_cache,
95 SoftwareGlyphAtlasGlyph, SoftwareGlyphAtlasKey, SoftwareGlyphAtlasPlacement,
96 SoftwareGlyphAtlasRunGlyph, SoftwareGlyphRasterCache, SoftwareTextFontSet,
97};
98#[cfg(test)]
99use cranpose_ui_graphics::GraphicsLayer;
100use cranpose_ui_graphics::{
101 BlendMode, Brush, Color, ColorFilter, FxHasher, ImageBitmap, ImageSampling, Point, Rect,
102 RenderEffect, RenderHash, RuntimeShader, StrokeCap, StrokeJoin, TileMode,
103};
104use std::borrow::Cow;
105use std::cell::Cell;
106use std::hash::{Hash, Hasher};
107use std::ops::Range;
108use std::rc::Rc;
109#[cfg(not(target_arch = "wasm32"))]
110use std::sync::atomic::{AtomicUsize, Ordering};
111use std::sync::{mpsc, Arc};
112use std::time::Duration;
113use web_time::Instant;
114
115use crate::gpu_stats;
116use crate::gpu_stats::gpu_stats_enabled;
117use crate::pipeline::push_layer_shadow;
118
119#[cfg(target_arch = "wasm32")]
124const MAX_SHAPES_PER_BATCH: usize = 102;
125#[cfg(not(target_arch = "wasm32"))]
126const MAX_SHAPES_PER_BATCH: usize = 768;
127#[cfg(target_arch = "wasm32")]
128const MAX_GRADIENT_STOPS: usize = 256;
129#[cfg(not(target_arch = "wasm32"))]
130const MAX_GRADIENT_STOPS: usize = 1024;
131
132#[cfg(not(target_arch = "wasm32"))]
139const MAX_SHAPES_PER_STORAGE_BATCH: usize = 1 << 16;
140#[cfg(not(target_arch = "wasm32"))]
141const MAX_GRADIENT_STOPS_PER_STORAGE_BATCH: usize = 1 << 16;
142
143#[cfg(not(target_arch = "wasm32"))]
149const INITIAL_STORAGE_BATCH_CAPACITY: usize = 1024;
150
151#[derive(Clone, Copy, Debug, Eq, PartialEq)]
163struct ShapeBatchLimits {
164 max_shapes_per_batch: usize,
165 max_gradient_stops: usize,
166 storage: bool,
167}
168
169impl ShapeBatchLimits {
170 fn for_device(device: &wgpu::Device, downlevel: wgpu::DownlevelFlags) -> Self {
171 Self::select(&device.limits(), downlevel)
172 }
173
174 fn select(limits: &wgpu::Limits, _downlevel: wgpu::DownlevelFlags) -> Self {
181 #[cfg(not(target_arch = "wasm32"))]
182 if limits.max_storage_buffers_per_shader_stage >= 2
183 && _downlevel.contains(wgpu::DownlevelFlags::VERTEX_STORAGE)
203 {
204 return Self::for_storage_binding_size(limits.max_storage_buffer_binding_size);
205 }
206 Self::for_uniform_binding_size(limits.max_uniform_buffer_binding_size)
207 }
208
209 fn for_uniform_binding_size(max_uniform_buffer_binding_size: u64) -> Self {
210 let binding = max_uniform_buffer_binding_size as usize;
211 Self {
212 max_shapes_per_batch: (binding / std::mem::size_of::<ShapeData>())
213 .clamp(1, MAX_SHAPES_PER_BATCH),
214 max_gradient_stops: (binding / std::mem::size_of::<GradientStop>())
215 .clamp(1, MAX_GRADIENT_STOPS),
216 storage: false,
217 }
218 }
219
220 #[cfg(not(target_arch = "wasm32"))]
221 fn for_storage_binding_size(max_storage_buffer_binding_size: u64) -> Self {
222 let binding = max_storage_buffer_binding_size as usize;
223 Self {
224 max_shapes_per_batch: (binding / std::mem::size_of::<ShapeData>())
225 .clamp(1, MAX_SHAPES_PER_STORAGE_BATCH),
226 max_gradient_stops: (binding / std::mem::size_of::<GradientStop>())
227 .clamp(1, MAX_GRADIENT_STOPS_PER_STORAGE_BATCH),
228 storage: true,
229 }
230 }
231
232 fn initial_shape_capacity(&self) -> usize {
233 #[cfg(not(target_arch = "wasm32"))]
234 if self.storage {
235 return self
236 .max_shapes_per_batch
237 .min(INITIAL_STORAGE_BATCH_CAPACITY);
238 }
239 self.max_shapes_per_batch
240 }
241
242 fn initial_gradient_capacity(&self) -> usize {
243 #[cfg(not(target_arch = "wasm32"))]
244 if self.storage {
245 return self.max_gradient_stops.min(INITIAL_STORAGE_BATCH_CAPACITY);
246 }
247 self.max_gradient_stops
248 }
249
250 fn data_buffer_usage(&self) -> wgpu::BufferUsages {
251 if self.storage {
252 wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST
253 } else {
254 wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST
255 }
256 }
257
258 fn data_binding_type(&self) -> wgpu::BufferBindingType {
259 if self.storage {
260 wgpu::BufferBindingType::Storage { read_only: true }
261 } else {
262 wgpu::BufferBindingType::Uniform
263 }
264 }
265
266 #[cfg(test)]
267 fn desktop() -> Self {
268 Self::for_uniform_binding_size(wgpu::Limits::default().max_uniform_buffer_binding_size)
269 }
270}
271#[cfg(target_arch = "wasm32")]
272const HARD_MAX_BUFFER_MB: usize = 64; const MAX_SHADOW_SURFACE_CACHE_ITEMS: usize = 512;
274const MAX_SHADOW_SURFACE_CACHE_BYTES: u64 = 384 * 1024 * 1024;
278const MAX_TEXT_IMAGE_CACHE_ITEMS: usize = 1024;
279const MAX_TEXT_GLYPH_MASK_CACHE_ITEMS: usize = 8192;
280const MAX_TEXT_GLYPH_ATLAS_ITEMS: usize = 8192;
281const MAX_TEXT_GLYPH_RUN_CACHE_ITEMS: usize = 1024;
282#[cfg(not(target_arch = "wasm32"))]
283const MAX_TEXT_GLYPH_GPU_RUN_CACHE_ITEMS: usize = 1024;
284#[cfg(not(target_arch = "wasm32"))]
285const MIN_RETAINED_TEXT_GLYPH_QUADS: usize = 192;
286#[cfg(not(target_arch = "wasm32"))]
287const OFFSCREEN_TEXT_GLYPH_PREWARM_BUDGET_MS: f64 = 0.75;
288#[cfg(not(target_arch = "wasm32"))]
289const MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CANDIDATES: usize = 2;
290#[cfg(not(target_arch = "wasm32"))]
291const MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS: usize = 160;
292#[cfg(not(target_arch = "wasm32"))]
293const MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CACHED_GLYPHS: usize = 160;
294const TEXT_GLYPH_ATLAS_MIN_SIZE: u32 = 512;
306const TEXT_GLYPH_ATLAS_MAX_SIZE: u32 = 4096;
307const TEXT_GLYPH_ATLAS_PADDING: u32 = 1;
308const MAX_TEXT_LINE_INDEX_CACHE_ITEMS: usize = 512;
309const MIN_MULTILINE_TEXT_LINES_FOR_CLIPPED_RASTER: usize = 2;
310const MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES: usize = 128;
311const CACHE_MISS_WARMUP_FRAMES: u8 = 1;
312pub(crate) const CLEAR_COLOR: wgpu::Color = wgpu::Color {
313 r: cranpose_render_common::FRAME_CLEAR_COLOR[0] as f64,
314 g: cranpose_render_common::FRAME_CLEAR_COLOR[1] as f64,
315 b: cranpose_render_common::FRAME_CLEAR_COLOR[2] as f64,
316 a: cranpose_render_common::FRAME_CLEAR_COLOR[3] as f64,
317};
318#[cfg(not(target_arch = "wasm32"))]
319const INITIAL_UPLOAD_BUFFER_BYTES: u64 = 4 * 1024;
320#[cfg(not(target_arch = "wasm32"))]
321const INITIAL_RETAINED_GLYPH_UNIFORM_SLOTS: usize = 128;
322const MAX_TEXTURE_CACHE_ITEMS: usize = 256;
323const MAX_IMAGE_TEXTURE_CACHE_BYTES: usize = 256 * 1024 * 1024;
327const RETAINED_STAGED_UPLOAD_BYTES: usize = 256 * 1024;
328const RETAINED_STAGED_UPLOAD_COPIES: usize = 128;
329pub(crate) const RETAINED_LAYER_REQUIREMENTS_CAPACITY: usize = 512;
330const DEFAULT_WGPU_RENDER_STAGE_TELEMETRY_THRESHOLD_MS: f64 = 4.0;
331#[cfg(not(target_arch = "wasm32"))]
332static SEGMENT_DIAG_LINES: AtomicUsize = AtomicUsize::new(0);
333fn wgpu_render_stage_telemetry_threshold_ms() -> Option<f64> {
337 static THRESHOLD_MS: std::sync::OnceLock<Option<f64>> = std::sync::OnceLock::new();
338 *THRESHOLD_MS.get_or_init(|| {
339 let explicit = std::env::var("CRANPOSE_WGPU_RENDER_STAGE_TELEMETRY_MS")
340 .ok()
341 .and_then(|value| value.parse::<f64>().ok())
342 .filter(|value| value.is_finite() && *value >= 0.0);
343 explicit.or_else(|| {
344 std::env::var_os("CRANPOSE_WGPU_RENDER_STAGE_TELEMETRY")
345 .is_some()
346 .then_some(DEFAULT_WGPU_RENDER_STAGE_TELEMETRY_THRESHOLD_MS)
347 })
348 })
349}
350
351pub(crate) fn instant_ms(start: Instant, end: Instant) -> f64 {
352 end.duration_since(start).as_secs_f64() * 1000.0
353}
354
355pub(crate) fn should_log_wgpu_render_stage(start: Instant, end: Instant) -> Option<f64> {
356 let threshold_ms = wgpu_render_stage_telemetry_threshold_ms()?;
357 let total_ms = instant_ms(start, end);
358 (total_ms >= threshold_ms).then_some(total_ms)
359}
360
361fn admit_layer_surface_cache_miss_impl(
362 key: &LayerRasterCacheKey,
363 observed_scene_range_misses: &mut BoundedLruCache<LayerRasterCacheKey, ()>,
364) -> bool {
365 if !key.is_scene_range() {
366 return true;
367 }
368 if observed_scene_range_misses.contains(key) {
369 return true;
370 }
371 observed_scene_range_misses.put(*key, ());
372 false
373}
374
375#[cfg(test)]
376fn first_cache_miss_admission(key: &LayerRasterCacheKey) -> bool {
377 let mut observed_scene_range_misses =
378 BoundedLruCache::with_capacity_at_least_one(MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES);
379 admit_layer_surface_cache_miss_impl(key, &mut observed_scene_range_misses)
380}
381
382#[cfg(test)]
383fn repeated_cache_miss_admission(key: &LayerRasterCacheKey) -> bool {
384 let mut observed_scene_range_misses =
385 BoundedLruCache::with_capacity_at_least_one(MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES);
386 let _ = admit_layer_surface_cache_miss_impl(key, &mut observed_scene_range_misses);
387 admit_layer_surface_cache_miss_impl(key, &mut observed_scene_range_misses)
388}
389
390pub static PRESENTED_FRAMES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
391
392pub fn frames_presented() -> u64 {
393 PRESENTED_FRAMES.load(std::sync::atomic::Ordering::Relaxed)
394}
395
396fn frame_stats_need_warmup_frame(snapshot: &gpu_stats::FrameStatsSnapshot) -> bool {
397 snapshot.layer_cache_misses > 0
398 || snapshot.shadow_shape_cache_misses > 0
399 || snapshot.text_image_cache_misses > 0
400 || snapshot.text_glyph_atlas_misses > 0
401}
402
403fn update_frame_warmup_budget(pending_frames: &mut u8, snapshot: &gpu_stats::FrameStatsSnapshot) {
404 if *pending_frames > 0 {
405 *pending_frames = pending_frames.saturating_sub(1);
406 } else if frame_stats_need_warmup_frame(snapshot) {
407 *pending_frames = CACHE_MISS_WARMUP_FRAMES;
408 }
409}
410
411fn text_atlas_fallback_diag_enabled() -> bool {
412 cranpose_core::env_flag!("CRANPOSE_TEXT_ATLAS_FALLBACK_DIAG")
413}
414
415fn text_glyph_run_diag_enabled() -> bool {
416 cranpose_core::env_flag!("CRANPOSE_TEXT_GLYPH_RUN_DIAG")
417}
418
419fn root_direct_diag_enabled() -> bool {
420 cranpose_core::env_flag!("CRANPOSE_ROOT_DIRECT_DIAG")
421}
422
423fn scene_layer_events_precede_z(scene: &CompositorScene, z_index: usize) -> bool {
424 scene
425 .effect_layers
426 .iter()
427 .any(|layer| layer.z_start < z_index && 0 < layer.z_end)
428 || scene
429 .backdrop_layers
430 .iter()
431 .any(|layer| layer.z_index < z_index)
432}
433
434fn direct_root_child_can_be_replayed_into_later_underlay(child: &ChildLayerComposite) -> bool {
435 child.backdrop.is_none()
436 && !child.has_effect
437 && child.shadow_draws.is_empty()
438 && axis_aligned_quad_rect(child.dest_quad).is_some()
439}
440
441fn rects_overlap(a: Rect, b: Rect) -> bool {
442 let a_right = a.x + a.width;
443 let a_bottom = a.y + a.height;
444 let b_right = b.x + b.width;
445 let b_bottom = b.y + b.height;
446 a.x < b_right && b.x < a_right && a.y < b_bottom && b.y < a_bottom
447}
448
449pub(crate) fn direct_root_child_underlays_are_supported(
450 collected: &CollectedLayer,
451 root_target_reads: bool,
452) -> bool {
453 for (child_index, child) in collected.child_layers.iter().enumerate() {
454 if child.backdrop.is_some() && !root_target_reads {
455 if root_direct_diag_enabled() {
456 log::warn!(
457 "[root-direct-diag] reject self-backdrop child node={:?}",
458 child.node_id
459 );
460 }
461 return false;
462 }
463 if child.needs_nested_underlay {
464 let Some(dest_rect) = axis_aligned_quad_rect(child.dest_quad) else {
465 if root_direct_diag_enabled() {
466 log::warn!(
467 "[root-direct-diag] reject projective underlay child node={:?}",
468 child.node_id
469 );
470 }
471 return false;
472 };
473 let translation_only = (dest_rect.width - child.logical_rect.width).abs() <= 0.001
474 && (dest_rect.height - child.logical_rect.height).abs() <= 0.001;
475 let unsupported_preceding_child_layer = collected.child_layers[..child_index]
476 .iter()
477 .any(|preceding| {
478 if direct_root_child_can_be_replayed_into_later_underlay(preceding) {
479 return false;
480 }
481 axis_aligned_quad_rect(preceding.dest_quad)
482 .is_none_or(|preceding_rect| rects_overlap(preceding_rect, dest_rect))
483 });
484 let preceding_scene_events =
485 scene_layer_events_precede_z(&collected.scene, child.z_index);
486 if unsupported_preceding_child_layer || preceding_scene_events || !translation_only {
487 if root_direct_diag_enabled() {
488 log::warn!(
489 "[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})",
490 child.node_id,
491 unsupported_preceding_child_layer,
492 preceding_scene_events,
493 translation_only,
494 dest_rect.x,
495 dest_rect.y,
496 dest_rect.width,
497 dest_rect.height,
498 child.logical_rect.x,
499 child.logical_rect.y,
500 child.logical_rect.width,
501 child.logical_rect.height
502 );
503 }
504 return false;
505 }
506 }
507 }
508 true
509}
510
511#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
512struct ShadowSurfaceCacheKey {
513 content_hash: u64,
514 pixel_size: [u32; 2],
515 root_scale_bits: u32,
516 blur_radius_bits: u32,
517}
518
519struct CachedShadowSurface {
520 target: Rc<OffscreenTarget>,
521 byte_size: u64,
522}
523
524struct CachedShadowComposite {
525 source: Rc<OffscreenTarget>,
526 scissor: Option<(u32, u32, u32, u32)>,
527 rounded_mask: Option<RoundedCompositeMask>,
528 dest_viewport: Option<(f32, f32, f32, f32)>,
529}
530
531impl CachedShadowComposite {
532 fn batch_item(&self) -> CompositeBatchItem<'_> {
533 CompositeBatchItem {
534 source: &self.source,
535 alpha: 1.0,
536 scissor: self.scissor,
537 rounded_mask: self.rounded_mask,
538 blend_mode: BlendMode::SrcOver,
539 dest_viewport: self.dest_viewport,
540 source_viewport: None,
541 sample_mode: CompositeSampleMode::Nearest,
542 }
543 }
544}
545
546#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
547struct TextImageCacheKey(u64);
548
549struct CachedTextImage {
550 image: ImageBitmap,
551}
552
553#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
554struct TextGlyphRunCacheKey(u64);
555
556#[derive(Clone, Copy)]
557struct CachedTextGlyphQuad {
558 x: i32,
559 y: i32,
560 width: usize,
561 height: usize,
562 color: (f32, f32, f32, f32),
563 uv: ImageUvRect,
564}
565
566struct CachedTextGlyphRun {
567 glyphs: Rc<[SoftwareGlyphAtlasPlacement]>,
568 quads: Option<Rc<[CachedTextGlyphQuad]>>,
569 atlas_generation: u64,
570}
571
572const TEXT_GLYPH_PREWARM_VIEWPORT_MULTIPLIER: f32 = 2.0;
573
574#[cfg(not(target_arch = "wasm32"))]
575struct CachedGpuTextGlyphRun {
576 vertex_buffer: wgpu::Buffer,
577 index_buffer: wgpu::Buffer,
578 index_count: u32,
579 atlas_generation: u64,
580}
581
582#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
583struct TextLineIndexCacheKey(usize);
584
585struct CachedTextLineIndex {
586 text: std::sync::Weak<cranpose_ui::text::RenderString>,
587 len: usize,
588 starts: Rc<[usize]>,
589}
590
591struct TextLineIndexCache {
592 entries: BoundedLruCache<TextLineIndexCacheKey, CachedTextLineIndex>,
593}
594
595impl TextLineIndexCache {
596 fn new(capacity: usize) -> Self {
597 Self {
598 entries: BoundedLruCache::with_capacity_at_least_one(capacity),
599 }
600 }
601
602 fn line_starts(&mut self, text: &Arc<cranpose_ui::text::RenderString>) -> Rc<[usize]> {
603 let key = TextLineIndexCacheKey(Arc::as_ptr(text) as usize);
604 if let Some(cached) = self.entries.get(&key) {
605 if cached.len == text.text.len()
606 && cached
607 .text
608 .upgrade()
609 .is_some_and(|cached_text| Arc::ptr_eq(&cached_text, text))
610 {
611 return cached.starts.clone();
612 }
613 }
614
615 let starts = Rc::<[usize]>::from(line_start_offsets(text.text.as_str()));
616 self.entries.put(
617 key,
618 CachedTextLineIndex {
619 text: Arc::downgrade(text),
620 len: text.text.len(),
621 starts: starts.clone(),
622 },
623 );
624 starts
625 }
626}
627
628#[derive(Clone, Copy, Debug, PartialEq)]
629struct ShapeShadowSurfacePlan {
630 source_device_bounds: DevicePixelBounds,
631 processing_scissor: Option<(u32, u32, u32, u32)>,
632 pixel_radius: f32,
633}
634
635#[derive(Default)]
651struct DeviceErrorSentry {
652 errors: std::sync::atomic::AtomicU64,
654 poisoned: std::sync::atomic::AtomicBool,
656}
657
658impl DeviceErrorSentry {
659 fn record(&self, error: &wgpu::Error) {
662 use std::sync::atomic::Ordering;
663 self.poisoned.store(true, Ordering::Release);
664 let count = self.errors.fetch_add(1, Ordering::Relaxed) + 1;
665 if count.is_power_of_two() {
671 log::error!("[gpu-device] uncaptured wgpu error #{count}: {error}");
672 }
673 }
674
675 fn take_poison(&self) -> bool {
676 self.poisoned
677 .swap(false, std::sync::atomic::Ordering::AcqRel)
678 }
679
680 fn error_count(&self) -> u64 {
681 self.errors.load(std::sync::atomic::Ordering::Relaxed)
682 }
683}
684
685#[derive(Default)]
686struct RendererWarningState {
687 unsupported_effect_reported: Cell<bool>,
688}
689
690impl RendererWarningState {
691 fn warn_unsupported_effect_once(&self) {
692 if !self.unsupported_effect_reported.replace(true) {
693 log::warn!(
694 "WGPU renderer received an unsupported RenderEffect variant; falling back to passthrough compositing"
695 );
696 }
697 }
698}
699
700fn is_blend_mode_supported(mode: BlendMode) -> bool {
701 matches!(
702 mode,
703 BlendMode::Src | BlendMode::SrcOver | BlendMode::DstOut
704 )
705}
706
707fn blend_state_for_mode(mode: BlendMode) -> wgpu::BlendState {
708 match mode {
709 BlendMode::Src => wgpu::BlendState::REPLACE,
710 BlendMode::DstOut => wgpu::BlendState {
711 color: wgpu::BlendComponent {
712 src_factor: wgpu::BlendFactor::Zero,
713 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
714 operation: wgpu::BlendOperation::Add,
715 },
716 alpha: wgpu::BlendComponent {
717 src_factor: wgpu::BlendFactor::Zero,
718 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
719 operation: wgpu::BlendOperation::Add,
720 },
721 },
722 _ => wgpu::BlendState::ALPHA_BLENDING,
723 }
724}
725
726fn supported_blend_mode(mode: BlendMode) -> BlendMode {
727 if is_blend_mode_supported(mode) {
728 return mode;
729 }
730
731 BlendMode::SrcOver
732}
733
734fn direct_shader_composite_viewport(
735 alpha: f32,
736 blend_mode: BlendMode,
737 dest_viewport: Option<(f32, f32, f32, f32)>,
738 sample_mode: CompositeSampleMode,
739 source_size: (u32, u32),
740) -> Option<(f32, f32, f32, f32)> {
741 if alpha != 1.0 || supported_blend_mode(blend_mode) != BlendMode::SrcOver {
742 return None;
743 }
744 let viewport = dest_viewport?;
745 if viewport.2 <= 0.0 || viewport.3 <= 0.0 {
746 return None;
747 }
748 match sample_mode {
749 CompositeSampleMode::Linear | CompositeSampleMode::Nearest => Some(viewport),
750 CompositeSampleMode::Box4
751 if shader_composite_preserves_source_pixel_grid(viewport, source_size) =>
752 {
753 Some(viewport)
754 }
755 CompositeSampleMode::Box4 => None,
756 }
757}
758
759fn shader_composite_preserves_source_pixel_grid(
760 viewport: (f32, f32, f32, f32),
761 source_size: (u32, u32),
762) -> bool {
763 const EPSILON: f32 = 0.01;
764 let (x, y, width, height) = viewport;
765 let (source_width, source_height) = source_size;
766 (x - x.round()).abs() <= EPSILON
767 && (y - y.round()).abs() <= EPSILON
768 && (width - source_width as f32).abs() <= EPSILON
769 && (height - source_height as f32).abs() <= EPSILON
770}
771
772type DirectShaderTailComposite<'a> = (&'a RenderEffect, &'a RuntimeShader, (f32, f32, f32, f32));
773
774fn direct_shader_tail_composite(
775 effect: &RenderEffect,
776 alpha: f32,
777 blend_mode: BlendMode,
778 dest_viewport: Option<(f32, f32, f32, f32)>,
779 sample_mode: CompositeSampleMode,
780 source_size: (u32, u32),
781) -> Option<DirectShaderTailComposite<'_>> {
782 let viewport = direct_shader_composite_viewport(
783 alpha,
784 blend_mode,
785 dest_viewport,
786 sample_mode,
787 source_size,
788 )?;
789 let RenderEffect::Chain { first, second } = effect else {
790 return None;
791 };
792 let RenderEffect::Shader { shader } = second.as_ref() else {
793 return None;
794 };
795 Some((first.as_ref(), shader, viewport))
796}
797
798fn hash_f32_for_cache<H: Hasher>(value: f32, state: &mut H) {
799 value.to_bits().hash(state);
800}
801
802fn hash_text_raster_geometry_for_cache<H: Hasher>(
803 rect: Rect,
804 static_text_motion: bool,
805 state: &mut H,
806) {
807 hash_f32_for_cache(rect.width, state);
808 hash_f32_for_cache(rect.height, state);
809 static_text_motion.hash(state);
810 if !static_text_motion {
811 hash_f32_for_cache(rect.x.fract(), state);
812 hash_f32_for_cache(rect.y.fract(), state);
813 }
814}
815
816fn text_raster_geometry_for_draw(
817 text_draw: &TextDraw,
818 root_scale: f32,
819) -> Option<(Rect, Rect, Option<Rect>, f32, bool)> {
820 if text_draw.text.is_empty()
821 || text_draw.rect.width <= 0.0
822 || text_draw.rect.height <= 0.0
823 || !root_scale.is_finite()
824 || root_scale <= 0.0
825 {
826 return None;
827 }
828
829 let text_scale = text_draw.scale * root_scale;
830 if !text_scale.is_finite() || text_scale <= 0.0 {
831 return None;
832 }
833
834 let static_text_motion = text_draw
835 .text_style
836 .paragraph_style
837 .text_motion
838 .unwrap_or(cranpose_ui::text::TextMotion::Static)
839 == cranpose_ui::text::TextMotion::Static;
840 let snap_delta = text_draw
841 .snap_anchor
842 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
843 .unwrap_or_default();
844 let logical_rect = text_draw.rect.translate(snap_delta.x, snap_delta.y);
845 let clip = text_draw.clip;
848 let mut raster_rect = Rect {
849 x: logical_rect.x * root_scale,
850 y: logical_rect.y * root_scale,
851 width: logical_rect.width * root_scale,
852 height: logical_rect.height * root_scale,
853 };
854 if text_draw.snap_anchor.is_some() {
855 raster_rect.x = canonicalize_device_coordinate(raster_rect.x);
856 raster_rect.y = canonicalize_device_coordinate(raster_rect.y);
857 }
858 if static_text_motion {
859 raster_rect.x = raster_rect.x.round();
860 raster_rect.y = raster_rect.y.round();
861 }
862 raster_rect.width = raster_rect.width.ceil().max(1.0);
863 raster_rect.height = raster_rect.height.ceil().max(1.0);
864 Some((
865 logical_rect,
866 raster_rect,
867 clip,
868 text_scale,
869 static_text_motion,
870 ))
871}
872
873fn text_draw_is_visible_in_viewport(
874 logical_rect: Rect,
875 clip: Option<Rect>,
876 viewport: ViewportUniformParams,
877 root_scale: f32,
878) -> bool {
879 draw_rect_is_visible_in_viewport(logical_rect, clip, viewport, root_scale)
880}
881
882fn text_draw_should_prewarm_in_viewport(
883 logical_rect: Rect,
884 clip: Option<Rect>,
885 viewport: ViewportUniformParams,
886 root_scale: f32,
887) -> bool {
888 if !root_scale.is_finite() || root_scale <= 0.0 {
889 return false;
890 }
891 let viewport_rect = Rect {
892 x: viewport.offset[0] / root_scale,
893 y: viewport.offset[1] / root_scale,
894 width: viewport.width as f32 / root_scale,
895 height: viewport.height as f32 / root_scale,
896 };
897 let margin_x = viewport_rect.width * TEXT_GLYPH_PREWARM_VIEWPORT_MULTIPLIER;
898 let margin_y = viewport_rect.height * TEXT_GLYPH_PREWARM_VIEWPORT_MULTIPLIER;
899 let prewarm_viewport = expand_rect(viewport_rect, margin_x, margin_y);
900 let prewarm_rect = match clip {
901 Some(clip) => expand_rect(clip, margin_x, margin_y).intersect(prewarm_viewport),
902 None => Some(prewarm_viewport),
903 };
904 prewarm_rect.is_some_and(|rect| logical_rect.intersect(rect).is_some())
905}
906
907fn expand_rect(rect: Rect, margin_x: f32, margin_y: f32) -> Rect {
908 Rect {
909 x: rect.x - margin_x,
910 y: rect.y - margin_y,
911 width: rect.width + margin_x * 2.0,
912 height: rect.height + margin_y * 2.0,
913 }
914}
915
916fn draw_rect_is_visible_in_viewport(
917 rect: Rect,
918 clip: Option<Rect>,
919 viewport: ViewportUniformParams,
920 root_scale: f32,
921) -> bool {
922 if !root_scale.is_finite() || root_scale <= 0.0 {
923 return false;
924 }
925 let viewport_rect = Rect {
926 x: viewport.offset[0] / root_scale,
927 y: viewport.offset[1] / root_scale,
928 width: viewport.width as f32 / root_scale,
929 height: viewport.height as f32 / root_scale,
930 };
931 let visible_rect = match clip {
932 Some(clip) => clip.intersect(viewport_rect),
933 None => Some(viewport_rect),
934 };
935 visible_rect.is_some_and(|visible| rect.intersect(visible).is_some())
936}
937
938fn shape_draw_is_visible_in_viewport(
939 shape: &DrawShape,
940 viewport: ViewportUniformParams,
941 root_scale: f32,
942) -> bool {
943 let Some(viewport_rect) = viewport_rect_in_logical(viewport, root_scale) else {
944 return false;
945 };
946 shape_draw_is_visible_in_rect(shape, viewport_rect, root_scale)
947}
948
949fn viewport_rect_in_logical(viewport: ViewportUniformParams, root_scale: f32) -> Option<Rect> {
953 if !root_scale.is_finite() || root_scale <= 0.0 {
954 return None;
955 }
956 Some(Rect {
957 x: viewport.offset[0] / root_scale,
958 y: viewport.offset[1] / root_scale,
959 width: viewport.width as f32 / root_scale,
960 height: viewport.height as f32 / root_scale,
961 })
962}
963
964fn shape_draw_is_visible_in_rect(shape: &DrawShape, viewport_rect: Rect, root_scale: f32) -> bool {
967 let snap_delta = shape
968 .snap_anchor
969 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
970 .unwrap_or_default();
971 let rect = quad_bounds(translate_quad(shape.quad, snap_delta));
972 let visible_rect = match shape.clip {
973 Some(clip) => clip.intersect(viewport_rect),
974 None => Some(viewport_rect),
975 };
976 visible_rect.is_some_and(|visible| rect.intersect(visible).is_some())
977}
978
979fn cached_text_glyph_quad(
980 glyph: &SoftwareGlyphAtlasPlacement,
981 entry: GlyphAtlasEntry,
982 atlas_size: u32,
983) -> CachedTextGlyphQuad {
984 CachedTextGlyphQuad {
985 x: glyph.x,
986 y: glyph.y,
987 width: glyph.width,
988 height: glyph.height,
989 color: (
990 glyph.color.0.clamp(0.0, 1.0),
991 glyph.color.1.clamp(0.0, 1.0),
992 glyph.color.2.clamp(0.0, 1.0),
993 glyph.color.3.clamp(0.0, 1.0),
994 ),
995 uv: glyph_atlas_uv_rect(entry, atlas_size),
996 }
997}
998
999fn append_cached_text_glyph_quad(
1000 source_raster_rect: Rect,
1001 quad: &CachedTextGlyphQuad,
1002 image_vertices: &mut Vec<Vertex>,
1003 image_indices: &mut Vec<u32>,
1004) -> bool {
1005 if quad.width == 0 || quad.height == 0 || quad.color.3 <= 0.0 {
1006 return false;
1007 }
1008
1009 let base_vertex = image_vertices.len() as u32;
1010 image_indices.extend_from_slice(&[
1011 base_vertex,
1012 base_vertex + 1,
1013 base_vertex + 2,
1014 base_vertex + 2,
1015 base_vertex + 1,
1016 base_vertex + 3,
1017 ]);
1018
1019 let x0 = source_raster_rect.x + quad.x as f32;
1020 let y0 = source_raster_rect.y + quad.y as f32;
1021 let x1 = x0 + quad.width as f32;
1022 let y1 = y0 + quad.height as f32;
1023 let color = [quad.color.0, quad.color.1, quad.color.2, quad.color.3];
1024
1025 image_vertices.extend_from_slice(&[
1026 Vertex {
1027 position: [x0, y0],
1028 color,
1029 uv: [quad.uv.min[0], quad.uv.min[1]],
1030 uv_bounds: quad.uv.sample_bounds,
1031 },
1032 Vertex {
1033 position: [x1, y0],
1034 color,
1035 uv: [quad.uv.max[0], quad.uv.min[1]],
1036 uv_bounds: quad.uv.sample_bounds,
1037 },
1038 Vertex {
1039 position: [x0, y1],
1040 color,
1041 uv: [quad.uv.min[0], quad.uv.max[1]],
1042 uv_bounds: quad.uv.sample_bounds,
1043 },
1044 Vertex {
1045 position: [x1, y1],
1046 color,
1047 uv: [quad.uv.max[0], quad.uv.max[1]],
1048 uv_bounds: quad.uv.sample_bounds,
1049 },
1050 ]);
1051 true
1052}
1053
1054fn cached_text_glyph_quad_logical_rect(
1055 source_raster_rect: Rect,
1056 quad: &CachedTextGlyphQuad,
1057 root_scale: f32,
1058) -> Option<Rect> {
1059 if !root_scale.is_finite() || root_scale <= 0.0 {
1060 return None;
1061 }
1062 Some(Rect {
1063 x: (source_raster_rect.x + quad.x as f32) / root_scale,
1064 y: (source_raster_rect.y + quad.y as f32) / root_scale,
1065 width: quad.width as f32 / root_scale,
1066 height: quad.height as f32 / root_scale,
1067 })
1068}
1069
1070fn cached_text_glyph_quad_is_visible_in_viewport(
1071 source_raster_rect: Rect,
1072 quad: &CachedTextGlyphQuad,
1073 clip: Option<Rect>,
1074 viewport: ViewportUniformParams,
1075 root_scale: f32,
1076) -> bool {
1077 cached_text_glyph_quad_logical_rect(source_raster_rect, quad, root_scale)
1078 .is_some_and(|rect| draw_rect_is_visible_in_viewport(rect, clip, viewport, root_scale))
1079}
1080
1081#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1082enum TextGlyphDrawAction {
1083 DrawVisible,
1084 PrewarmOffscreen,
1085 Skip,
1086}
1087
1088fn text_glyph_draw_action(
1089 is_visible: bool,
1090 is_prewarm_candidate: bool,
1091 allow_offscreen_prewarm: bool,
1092) -> TextGlyphDrawAction {
1093 if is_visible {
1094 TextGlyphDrawAction::DrawVisible
1095 } else if allow_offscreen_prewarm && is_prewarm_candidate {
1096 TextGlyphDrawAction::PrewarmOffscreen
1097 } else {
1098 TextGlyphDrawAction::Skip
1099 }
1100}
1101
1102#[cfg(not(target_arch = "wasm32"))]
1103fn should_use_retained_text_glyph_run(quads_len: usize, clip: Option<Rect>) -> bool {
1104 clip.is_none() && quads_len >= MIN_RETAINED_TEXT_GLYPH_QUADS
1105}
1106
1107#[cfg(not(target_arch = "wasm32"))]
1108fn offscreen_text_glyph_prewarm_work_is_bounded(
1109 cached_glyphs: Option<usize>,
1110 text_len: usize,
1111) -> bool {
1112 match cached_glyphs {
1113 Some(glyphs) => glyphs <= MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CACHED_GLYPHS,
1114 None => text_len <= MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS,
1115 }
1116}
1117
1118#[cfg(not(target_arch = "wasm32"))]
1119fn offscreen_text_glyph_prewarm_budget_exhausted(
1120 start: Instant,
1121 admitted_candidates: usize,
1122) -> bool {
1123 admitted_candidates >= MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CANDIDATES
1124 || instant_ms(start, Instant::now()) >= OFFSCREEN_TEXT_GLYPH_PREWARM_BUDGET_MS
1125}
1126
1127fn text_draws_for_ordered_range<'a>(
1128 ordered_items: &'a [(usize, SegmentDrawItem)],
1129 texts: &'a [TextDraw],
1130 start: usize,
1131 end: usize,
1132) -> Result<impl Iterator<Item = &'a TextDraw>, String> {
1133 let range_items = ordered_items
1134 .get(start..end)
1135 .ok_or_else(|| format!("text batch range {start}..{end} is outside ordered draw items"))?;
1136 for (_, item) in range_items {
1137 match item {
1138 SegmentDrawItem::Text(text_index) if *text_index < texts.len() => {}
1139 SegmentDrawItem::Text(text_index) => {
1140 return Err(format!(
1141 "text batch references missing text draw index: {text_index}"
1142 ));
1143 }
1144 _ => return Err(format!("text batch contains non-text draw item: {item:?}")),
1145 }
1146 }
1147
1148 Ok(range_items.iter().filter_map(move |(_, item)| match item {
1149 SegmentDrawItem::Text(text_index) => texts.get(*text_index),
1150 _ => None,
1151 }))
1152}
1153
1154const SHADOW_CACHE_DEVICE_QUANT: f32 = 16.0;
1159
1160fn hash_shadow_device_offset<H: Hasher>(value: f32, origin: f32, root_scale: f32, state: &mut H) {
1161 let quantized = ((value - origin) * root_scale * SHADOW_CACHE_DEVICE_QUANT).round();
1162 (quantized as i64).hash(state);
1163}
1164
1165fn hash_shadow_device_rect<H: Hasher>(
1166 rect: Rect,
1167 origin_x: f32,
1168 origin_y: f32,
1169 root_scale: f32,
1170 state: &mut H,
1171) {
1172 hash_shadow_device_offset(rect.x, origin_x, root_scale, state);
1173 hash_shadow_device_offset(rect.y, origin_y, root_scale, state);
1174 hash_shadow_device_offset(rect.width, 0.0, root_scale, state);
1175 hash_shadow_device_offset(rect.height, 0.0, root_scale, state);
1176}
1177
1178fn hash_shape_shadow_item<H: Hasher>(
1179 shape: &DrawShape,
1180 brushes: &[Brush],
1181 blend_mode: BlendMode,
1182 origin_x: f32,
1183 origin_y: f32,
1184 root_scale: f32,
1185 state: &mut H,
1186) {
1187 hash_shadow_device_rect(shape.rect, origin_x, origin_y, root_scale, state);
1188 hash_shadow_device_rect(shape.local_rect, origin_x, origin_y, root_scale, state);
1189 for point in shape.quad {
1190 hash_shadow_device_offset(point[0], origin_x, root_scale, state);
1191 hash_shadow_device_offset(point[1], origin_y, root_scale, state);
1192 }
1193 match shape.snap_anchor {
1194 Some(anchor) => {
1195 1u8.hash(state);
1196 hash_shadow_device_offset(anchor.origin.x, origin_x, root_scale, state);
1197 hash_shadow_device_offset(anchor.origin.y, origin_y, root_scale, state);
1198 hash_f32_for_cache(anchor.device_pixel_step, state);
1199 }
1200 None => 0u8.hash(state),
1201 }
1202 shape.brush.render_hash(brushes).hash(state);
1203 match shape.shape {
1204 Some(corner_shape) => {
1205 1u8.hash(state);
1206 corner_shape.radii().render_hash().hash(state);
1207 }
1208 None => 0u8.hash(state),
1209 }
1210 match shape.clip {
1211 Some(clip) => {
1212 1u8.hash(state);
1213 hash_shadow_device_rect(clip, origin_x, origin_y, root_scale, state);
1214 }
1215 None => 0u8.hash(state),
1216 }
1217 blend_mode.hash(state);
1218 shape.blend_mode.hash(state);
1219}
1220
1221fn shape_shadow_content_hash(
1222 shapes: &[(DrawShape, BlendMode)],
1223 brushes: &[Brush],
1224 root_scale: f32,
1225) -> u64 {
1226 let mut hasher = FxHasher::default();
1227 let origin = shape_shadow_bounds(shapes).unwrap_or(Rect {
1232 x: 0.0,
1233 y: 0.0,
1234 width: 0.0,
1235 height: 0.0,
1236 });
1237
1238 shapes.len().hash(&mut hasher);
1239 for (shape, blend_mode) in shapes {
1240 hash_shape_shadow_item(
1241 shape,
1242 brushes,
1243 *blend_mode,
1244 origin.x,
1245 origin.y,
1246 root_scale,
1247 &mut hasher,
1248 );
1249 }
1250 hasher.finish()
1251}
1252
1253fn shape_shadow_surface_cache_key(
1254 shapes: &[(DrawShape, BlendMode)],
1255 brushes: &[Brush],
1256 device_bounds: DevicePixelBounds,
1257 pixel_radius: f32,
1258 root_scale: f32,
1259) -> Option<ShadowSurfaceCacheKey> {
1260 (root_scale.is_finite() && root_scale > 0.0).then(|| ShadowSurfaceCacheKey {
1261 content_hash: shape_shadow_content_hash(shapes, brushes, root_scale),
1262 pixel_size: [device_bounds.width, device_bounds.height],
1263 root_scale_bits: root_scale.to_bits(),
1264 blur_radius_bits: pixel_radius.to_bits(),
1265 })
1266}
1267
1268fn shape_shadow_bounds(shapes: &[(DrawShape, BlendMode)]) -> Option<Rect> {
1269 shapes
1270 .iter()
1271 .map(|(shape, _)| shape.rect)
1272 .reduce(|a, b| Rect {
1273 x: a.x.min(b.x),
1274 y: a.y.min(b.y),
1275 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
1276 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
1277 })
1278}
1279
1280fn shared_shape_shadow_snap_anchor(shapes: &[(DrawShape, BlendMode)]) -> Option<SnapAnchor> {
1281 let anchor = shapes.first()?.0.snap_anchor?;
1282 shapes
1283 .iter()
1284 .all(|(shape, _)| shape.snap_anchor == Some(anchor))
1285 .then_some(anchor)
1286}
1287
1288fn shadow_draw_bounds(shadow: &ShadowDraw) -> Option<Rect> {
1289 shadow
1290 .shapes
1291 .iter()
1292 .map(|(shape, _)| shape.rect)
1293 .chain(shadow.texts.iter().map(|text| text.rect))
1294 .reduce(|a, b| Rect {
1295 x: a.x.min(b.x),
1296 y: a.y.min(b.y),
1297 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
1298 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
1299 })
1300}
1301
1302fn shadow_draw_may_render(
1303 shadow: &ShadowDraw,
1304 width: u32,
1305 height: u32,
1306 root_scale: f32,
1307 max_texture_dim: u32,
1308) -> bool {
1309 if shadow.texts.is_empty() && !shadow.shapes.is_empty() && shadow.blur_radius > 0.0 {
1310 return shape_shadow_surface_plan(
1311 &shadow.shapes,
1312 shadow.clip,
1313 shadow.blur_radius,
1314 width,
1315 height,
1316 root_scale,
1317 max_texture_dim,
1318 )
1319 .is_some();
1320 }
1321
1322 let Some(bounds) = shadow_draw_bounds(shadow) else {
1323 return false;
1324 };
1325 let blur_margin = blur_extent_margin(shadow.blur_radius);
1326 let mut visible_bounds = Rect {
1327 x: bounds.x - blur_margin,
1328 y: bounds.y - blur_margin,
1329 width: bounds.width + blur_margin * 2.0,
1330 height: bounds.height + blur_margin * 2.0,
1331 };
1332 if let Some(clip) = shadow.clip {
1333 let clip_expanded = Rect {
1334 x: clip.x - blur_margin,
1335 y: clip.y - blur_margin,
1336 width: clip.width + blur_margin * 2.0,
1337 height: clip.height + blur_margin * 2.0,
1338 };
1339 let Some(intersection) = visible_bounds.intersect(clip_expanded) else {
1340 return false;
1341 };
1342 visible_bounds = intersection;
1343 }
1344
1345 scissor_rect_for_rect(visible_bounds, root_scale, width, height).is_some()
1346}
1347
1348fn shape_shadow_surface_plan(
1349 shapes: &[(DrawShape, BlendMode)],
1350 clip: Option<Rect>,
1351 blur_radius: f32,
1352 width: u32,
1353 height: u32,
1354 root_scale: f32,
1355 max_texture_dim: u32,
1356) -> Option<ShapeShadowSurfacePlan> {
1357 let shape_bounds = shape_shadow_bounds(shapes)?;
1358 let blur_margin = blur_extent_margin(blur_radius);
1359 let source_blur_bounds = Rect {
1360 x: shape_bounds.x - blur_margin,
1361 y: shape_bounds.y - blur_margin,
1362 width: shape_bounds.width + blur_margin * 2.0,
1363 height: shape_bounds.height + blur_margin * 2.0,
1364 };
1365
1366 let mut visible_blur_bounds = source_blur_bounds;
1367 if let Some(clip) = clip {
1368 let clip_expanded = Rect {
1369 x: clip.x - blur_margin,
1370 y: clip.y - blur_margin,
1371 width: clip.width + blur_margin * 2.0,
1372 height: clip.height + blur_margin * 2.0,
1373 };
1374 visible_blur_bounds = visible_blur_bounds.intersect(clip_expanded)?;
1375 }
1376
1377 let processing_scissor = scissor_rect_for_rect(visible_blur_bounds, root_scale, width, height);
1378 processing_scissor?;
1379 let visible_device_bounds =
1380 device_pixel_bounds_for_rect(visible_blur_bounds, width, height, root_scale)?;
1381 let source_device_bounds = translation_stable_anchored_device_pixel_bounds(
1382 source_blur_bounds,
1383 shared_shape_shadow_snap_anchor(shapes),
1384 root_scale,
1385 max_texture_dim,
1386 )
1387 .unwrap_or(visible_device_bounds);
1388
1389 Some(ShapeShadowSurfacePlan {
1390 source_device_bounds,
1391 processing_scissor,
1392 pixel_radius: blur_radius * root_scale,
1393 })
1394}
1395
1396fn is_render_effect_supported(effect: &RenderEffect) -> bool {
1397 match effect {
1398 RenderEffect::Blur { .. } => true,
1399 RenderEffect::Offset { .. } => true,
1400 RenderEffect::Shader { .. } => true,
1401 RenderEffect::Chain { first, second } => {
1402 is_render_effect_supported(first) && is_render_effect_supported(second)
1403 }
1404 }
1405}
1406
1407fn resolve_gradient_point(origin: f32, extent: f32, value: f32) -> f32 {
1408 if value.is_finite() {
1409 origin + value
1410 } else if value.is_sign_positive() {
1411 origin + extent
1412 } else {
1413 origin
1414 }
1415}
1416
1417fn gradient_tile_mode_value(tile_mode: TileMode) -> u32 {
1418 match tile_mode {
1419 TileMode::Clamp => 0,
1420 TileMode::Repeated => 1,
1421 TileMode::Mirror => 2,
1422 TileMode::Decal => 3,
1423 }
1424}
1425
1426fn shape_shader_base(solid_trim: bool) -> Cow<'static, str> {
1434 if solid_trim {
1435 return Cow::Owned(format!(
1436 "{}\n{}",
1437 shaders::SHADER,
1438 shaders::SOLID_TRIM_APPENDIX
1439 ));
1440 }
1441 Cow::Borrowed(shaders::SHADER)
1442}
1443
1444#[cfg(not(target_arch = "wasm32"))]
1445fn shape_shader_source(batch_limits: ShapeBatchLimits, solid_trim: bool) -> Cow<'static, str> {
1446 let base = shape_shader_base(solid_trim);
1447 if batch_limits.storage {
1451 return Cow::Owned(
1452 base.replace(
1453 "var<uniform> shape_data: array<ShapeData, 102>;",
1454 "var<storage, read> shape_data: array<ShapeData>;",
1455 )
1456 .replace(
1457 "var<uniform> gradient_stops: array<GradientStop, 256>;",
1458 "var<storage, read> gradient_stops: array<GradientStop>;\n\n\
1465 @group(1) @binding(3)\n\
1466 var<storage, read> paint: array<vec4<f32>>;",
1467 )
1468 .replace(
1469 "output.color = shape.color;",
1470 "output.color = \
1471 select(shape.color, paint[shape_idx], similarity.paint_select > 0.5);",
1472 ),
1473 );
1474 }
1475 Cow::Owned(
1476 base.replace(
1477 "array<ShapeData, 102>",
1478 &format!("array<ShapeData, {}>", batch_limits.max_shapes_per_batch),
1479 )
1480 .replace(
1481 "array<GradientStop, 256>",
1482 &format!("array<GradientStop, {}>", batch_limits.max_gradient_stops),
1483 ),
1484 )
1485}
1486
1487#[cfg(target_arch = "wasm32")]
1488fn shape_shader_source(_batch_limits: ShapeBatchLimits, solid_trim: bool) -> Cow<'static, str> {
1489 shape_shader_base(solid_trim)
1493}
1494
1495pub(crate) fn create_render_pipeline_logged<'a>(
1500 device: &wgpu::Device,
1501 cache: Option<&'a wgpu::PipelineCache>,
1502 tag: &str,
1503 mut descriptor: wgpu::RenderPipelineDescriptor<'a>,
1504) -> wgpu::RenderPipeline {
1505 descriptor.cache = cache;
1506 let started = Instant::now();
1507 let pipeline = device.create_render_pipeline(&descriptor);
1508 log::info!(
1509 "[pipeline-create] {tag} {:.1}ms",
1510 instant_ms(started, Instant::now())
1511 );
1512 pipeline
1513}
1514
1515#[cfg(not(target_arch = "wasm32"))]
1518fn pipeline_prewarm_enabled() -> bool {
1519 std::env::var("CRANPOSE_PIPELINE_PREWARM").as_deref() != Ok("0")
1520}
1521
1522#[cfg(not(target_arch = "wasm32"))]
1523struct PipelinePrewarmInputs {
1524 device: Arc<wgpu::Device>,
1525 cache: Option<wgpu::PipelineCache>,
1526 surface_format: wgpu::TextureFormat,
1527 uniform_layout: wgpu::BindGroupLayout,
1528 shape_layout: wgpu::BindGroupLayout,
1529 image_layout: wgpu::BindGroupLayout,
1530 batch_limits: ShapeBatchLimits,
1531 instanced: bool,
1532}
1533
1534#[cfg(not(target_arch = "wasm32"))]
1545fn spawn_pipeline_prewarm(inputs: PipelinePrewarmInputs) {
1546 if !pipeline_prewarm_enabled() {
1547 return;
1548 }
1549 let spawned = std::thread::Builder::new()
1550 .name("cranpose-pl-warm".into())
1551 .spawn(move || {
1552 let started = Instant::now();
1553 let cache = inputs.cache.as_ref();
1554 let device = &inputs.device;
1555 let solid_trim = solid_trim_varyings_enabled();
1556 let mut built = 0_u32;
1557 if inputs.instanced {
1558 let (vertex_entry, fragment_entry) = if solid_trim {
1559 ("vs_solid_instanced", "fs_solid_trim")
1560 } else {
1561 ("vs_shape_instanced", "fs_solid")
1562 };
1563 drop(create_instanced_shape_pipeline(
1564 device,
1565 cache,
1566 inputs.surface_format,
1567 &inputs.uniform_layout,
1568 &inputs.shape_layout,
1569 BlendMode::SrcOver,
1570 inputs.batch_limits,
1571 solid_trim,
1572 vertex_entry,
1573 fragment_entry,
1574 false,
1575 ));
1576 drop(create_instanced_shape_pipeline(
1577 device,
1578 cache,
1579 inputs.surface_format,
1580 &inputs.uniform_layout,
1581 &inputs.shape_layout,
1582 BlendMode::SrcOver,
1583 inputs.batch_limits,
1584 false,
1585 "vs_shape_instanced",
1586 "fs_main",
1587 false,
1588 ));
1589 } else {
1590 let (vertex_entry, fragment_entry) = if solid_trim {
1591 ("vs_solid", "fs_solid_trim")
1592 } else {
1593 ("vs_main", "fs_solid")
1594 };
1595 drop(create_shape_pipeline(
1596 device,
1597 cache,
1598 inputs.surface_format,
1599 &inputs.uniform_layout,
1600 &inputs.shape_layout,
1601 BlendMode::SrcOver,
1602 inputs.batch_limits,
1603 solid_trim,
1604 vertex_entry,
1605 fragment_entry,
1606 false,
1607 ));
1608 drop(create_shape_pipeline(
1609 device,
1610 cache,
1611 inputs.surface_format,
1612 &inputs.uniform_layout,
1613 &inputs.shape_layout,
1614 BlendMode::SrcOver,
1615 inputs.batch_limits,
1616 false,
1617 "vs_main",
1618 "fs_main",
1619 false,
1620 ));
1621 }
1622 built += 2;
1623 if inputs.batch_limits.storage {
1624 drop(create_mesh_shape_pipeline(
1625 device,
1626 cache,
1627 inputs.surface_format,
1628 &inputs.uniform_layout,
1629 &inputs.shape_layout,
1630 inputs.batch_limits,
1631 false,
1632 ));
1633 built += 1;
1634 }
1635 drop(create_glyph_atlas_pipeline(
1636 device,
1637 cache,
1638 inputs.surface_format,
1639 &inputs.uniform_layout,
1640 &inputs.image_layout,
1641 false,
1642 ));
1643 built += 1;
1644 log::info!(
1645 "[pipeline-prewarm] {built} pipelines in {:.1} ms",
1646 instant_ms(started, Instant::now())
1647 );
1648 });
1649 if let Err(error) = spawned {
1650 log::warn!("[pipeline-prewarm] thread failed to spawn: {error}");
1651 }
1652}
1653
1654#[allow(clippy::too_many_arguments)]
1655fn create_shape_pipeline(
1656 device: &wgpu::Device,
1657 cache: Option<&wgpu::PipelineCache>,
1658 surface_format: wgpu::TextureFormat,
1659 uniform_layout: &wgpu::BindGroupLayout,
1660 shape_layout: &wgpu::BindGroupLayout,
1661 blend_mode: BlendMode,
1662 batch_limits: ShapeBatchLimits,
1663 solid_trim: bool,
1664 vertex_entry: &'static str,
1665 fragment_entry: &'static str,
1666 depth: bool,
1667) -> wgpu::RenderPipeline {
1668 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1669 label: Some("Shape Shader"),
1670 source: wgpu::ShaderSource::Wgsl(display_clip::with_content_z(
1671 shape_shader_source(batch_limits, solid_trim),
1672 depth,
1673 )),
1674 });
1675
1676 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1677 label: Some("Render Pipeline Layout"),
1678 bind_group_layouts: &[Some(uniform_layout), Some(shape_layout)],
1679 immediate_size: 0,
1680 });
1681
1682 create_render_pipeline_logged(
1683 device,
1684 cache,
1685 &format!("shape entry={fragment_entry} blend={blend_mode:?} depth={depth}"),
1686 wgpu::RenderPipelineDescriptor {
1687 label: Some("Render Pipeline"),
1688 layout: Some(&pipeline_layout),
1689 vertex: wgpu::VertexState {
1690 module: &shader,
1691 entry_point: Some(vertex_entry),
1692 compilation_options: wgpu::PipelineCompilationOptions::default(),
1693 buffers: &[],
1696 },
1697 fragment: Some(wgpu::FragmentState {
1698 module: &shader,
1699 entry_point: Some(fragment_entry),
1700 compilation_options: wgpu::PipelineCompilationOptions::default(),
1701 targets: &[Some(wgpu::ColorTargetState {
1702 format: surface_format,
1703 blend: Some(blend_state_for_mode(blend_mode)),
1704 write_mask: wgpu::ColorWrites::ALL,
1705 })],
1706 }),
1707 primitive: wgpu::PrimitiveState {
1708 topology: wgpu::PrimitiveTopology::TriangleList,
1709 strip_index_format: None,
1710 front_face: wgpu::FrontFace::Ccw,
1711 cull_mode: None,
1712 unclipped_depth: false,
1713 polygon_mode: wgpu::PolygonMode::Fill,
1714 conservative: false,
1715 },
1716 depth_stencil: display_clip::content_depth_state(depth),
1717 multisample: wgpu::MultisampleState::default(),
1718 multiview_mask: None,
1719 cache: None,
1720 },
1721 )
1722}
1723
1724#[cfg(not(target_arch = "wasm32"))]
1731fn create_mesh_shape_pipeline(
1732 device: &wgpu::Device,
1733 cache: Option<&wgpu::PipelineCache>,
1734 surface_format: wgpu::TextureFormat,
1735 uniform_layout: &wgpu::BindGroupLayout,
1736 shape_layout: &wgpu::BindGroupLayout,
1737 batch_limits: ShapeBatchLimits,
1738 depth: bool,
1739) -> wgpu::RenderPipeline {
1740 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1741 label: Some("Shape Mesh Shader"),
1742 source: wgpu::ShaderSource::Wgsl(display_clip::with_content_z(
1745 shape_shader_source(batch_limits, false),
1746 depth,
1747 )),
1748 });
1749
1750 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1751 label: Some("Mesh Render Pipeline Layout"),
1752 bind_group_layouts: &[Some(uniform_layout), Some(shape_layout)],
1753 immediate_size: 0,
1754 });
1755
1756 create_render_pipeline_logged(
1757 device,
1758 cache,
1759 &format!("mesh depth={depth}"),
1760 wgpu::RenderPipelineDescriptor {
1761 label: Some("Retained Mesh Pipeline"),
1762 layout: Some(&pipeline_layout),
1763 vertex: wgpu::VertexState {
1764 module: &shader,
1765 entry_point: Some("vs_mesh"),
1766 compilation_options: wgpu::PipelineCompilationOptions::default(),
1767 buffers: &[MeshVertex::desc()],
1768 },
1769 fragment: Some(wgpu::FragmentState {
1770 module: &shader,
1771 entry_point: Some("fs_main"),
1772 compilation_options: wgpu::PipelineCompilationOptions::default(),
1773 targets: &[Some(wgpu::ColorTargetState {
1774 format: surface_format,
1775 blend: Some(blend_state_for_mode(BlendMode::SrcOver)),
1776 write_mask: wgpu::ColorWrites::ALL,
1777 })],
1778 }),
1779 primitive: wgpu::PrimitiveState {
1780 topology: wgpu::PrimitiveTopology::TriangleList,
1781 strip_index_format: None,
1782 front_face: wgpu::FrontFace::Ccw,
1783 cull_mode: None,
1784 unclipped_depth: false,
1785 polygon_mode: wgpu::PolygonMode::Fill,
1786 conservative: false,
1787 },
1788 depth_stencil: display_clip::content_depth_state(depth),
1789 multisample: wgpu::MultisampleState::default(),
1790 multiview_mask: None,
1791 cache: None,
1792 },
1793 )
1794}
1795
1796#[cfg(not(target_arch = "wasm32"))]
1804#[allow(clippy::too_many_arguments)]
1805fn create_instanced_shape_pipeline(
1806 device: &wgpu::Device,
1807 cache: Option<&wgpu::PipelineCache>,
1808 surface_format: wgpu::TextureFormat,
1809 uniform_layout: &wgpu::BindGroupLayout,
1810 shape_layout: &wgpu::BindGroupLayout,
1811 blend_mode: BlendMode,
1812 batch_limits: ShapeBatchLimits,
1813 solid_trim: bool,
1814 vertex_entry: &'static str,
1815 fragment_entry: &'static str,
1816 depth: bool,
1817) -> wgpu::RenderPipeline {
1818 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1819 label: Some("Shape Instanced Shader"),
1820 source: wgpu::ShaderSource::Wgsl(display_clip::with_content_z(
1821 shape_shader_source(batch_limits, solid_trim),
1822 depth,
1823 )),
1824 });
1825
1826 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1827 label: Some("Instanced Render Pipeline Layout"),
1828 bind_group_layouts: &[Some(uniform_layout), Some(shape_layout)],
1829 immediate_size: 0,
1830 });
1831
1832 create_render_pipeline_logged(
1833 device,
1834 cache,
1835 &format!("instanced entry={fragment_entry} blend={blend_mode:?} depth={depth}"),
1836 wgpu::RenderPipelineDescriptor {
1837 label: Some("Instanced Render Pipeline"),
1838 layout: Some(&pipeline_layout),
1839 vertex: wgpu::VertexState {
1840 module: &shader,
1841 entry_point: Some(vertex_entry),
1842 compilation_options: wgpu::PipelineCompilationOptions::default(),
1843 buffers: &[],
1847 },
1848 fragment: Some(wgpu::FragmentState {
1849 module: &shader,
1850 entry_point: Some(fragment_entry),
1851 compilation_options: wgpu::PipelineCompilationOptions::default(),
1852 targets: &[Some(wgpu::ColorTargetState {
1853 format: surface_format,
1854 blend: Some(blend_state_for_mode(blend_mode)),
1855 write_mask: wgpu::ColorWrites::ALL,
1856 })],
1857 }),
1858 primitive: wgpu::PrimitiveState {
1859 topology: wgpu::PrimitiveTopology::TriangleList,
1860 strip_index_format: None,
1861 front_face: wgpu::FrontFace::Ccw,
1862 cull_mode: None,
1863 unclipped_depth: false,
1864 polygon_mode: wgpu::PolygonMode::Fill,
1865 conservative: false,
1866 },
1867 depth_stencil: display_clip::content_depth_state(depth),
1868 multisample: wgpu::MultisampleState::default(),
1869 multiview_mask: None,
1870 cache: None,
1871 },
1872 )
1873}
1874
1875fn create_image_pipeline(
1876 device: &wgpu::Device,
1877 cache: Option<&wgpu::PipelineCache>,
1878 surface_format: wgpu::TextureFormat,
1879 uniform_layout: &wgpu::BindGroupLayout,
1880 image_layout: &wgpu::BindGroupLayout,
1881 blend_mode: BlendMode,
1882 depth: bool,
1883) -> wgpu::RenderPipeline {
1884 let image_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1885 label: Some("Image Shader"),
1886 source: wgpu::ShaderSource::Wgsl(display_clip::with_content_z(
1887 shaders::IMAGE_SHADER.into(),
1888 depth,
1889 )),
1890 });
1891
1892 let image_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1893 label: Some("Image Pipeline Layout"),
1894 bind_group_layouts: &[Some(uniform_layout), Some(image_layout)],
1895 immediate_size: 0,
1896 });
1897
1898 create_render_pipeline_logged(
1899 device,
1900 cache,
1901 &format!("image blend={blend_mode:?} depth={depth}"),
1902 wgpu::RenderPipelineDescriptor {
1903 label: Some("Image Pipeline"),
1904 layout: Some(&image_pipeline_layout),
1905 vertex: wgpu::VertexState {
1906 module: &image_shader,
1907 entry_point: Some("image_vs_main"),
1908 compilation_options: wgpu::PipelineCompilationOptions::default(),
1909 buffers: &[Vertex::desc()],
1910 },
1911 fragment: Some(wgpu::FragmentState {
1912 module: &image_shader,
1913 entry_point: Some("image_fs_main"),
1914 compilation_options: wgpu::PipelineCompilationOptions::default(),
1915 targets: &[Some(wgpu::ColorTargetState {
1916 format: surface_format,
1917 blend: Some(blend_state_for_mode(blend_mode)),
1918 write_mask: wgpu::ColorWrites::ALL,
1919 })],
1920 }),
1921 primitive: wgpu::PrimitiveState {
1922 topology: wgpu::PrimitiveTopology::TriangleList,
1923 strip_index_format: None,
1924 front_face: wgpu::FrontFace::Ccw,
1925 cull_mode: None,
1926 unclipped_depth: false,
1927 polygon_mode: wgpu::PolygonMode::Fill,
1928 conservative: false,
1929 },
1930 depth_stencil: display_clip::content_depth_state(depth),
1931 multisample: wgpu::MultisampleState::default(),
1932 multiview_mask: None,
1933 cache: None,
1934 },
1935 )
1936}
1937
1938fn create_glyph_atlas_pipeline(
1939 device: &wgpu::Device,
1940 cache: Option<&wgpu::PipelineCache>,
1941 surface_format: wgpu::TextureFormat,
1942 uniform_layout: &wgpu::BindGroupLayout,
1943 image_layout: &wgpu::BindGroupLayout,
1944 depth: bool,
1945) -> wgpu::RenderPipeline {
1946 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1947 label: Some("Glyph Atlas Shader"),
1948 source: wgpu::ShaderSource::Wgsl(display_clip::with_content_z(
1949 shaders::GLYPH_ATLAS_SHADER.into(),
1950 depth,
1951 )),
1952 });
1953
1954 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1955 label: Some("Glyph Atlas Pipeline Layout"),
1956 bind_group_layouts: &[Some(uniform_layout), Some(image_layout)],
1957 immediate_size: 0,
1958 });
1959
1960 create_render_pipeline_logged(
1961 device,
1962 cache,
1963 &format!("glyph-atlas depth={depth}"),
1964 wgpu::RenderPipelineDescriptor {
1965 label: Some("Glyph Atlas Pipeline"),
1966 layout: Some(&pipeline_layout),
1967 vertex: wgpu::VertexState {
1968 module: &shader,
1969 entry_point: Some("glyph_atlas_vs_main"),
1970 compilation_options: wgpu::PipelineCompilationOptions::default(),
1971 buffers: &[Vertex::desc()],
1972 },
1973 fragment: Some(wgpu::FragmentState {
1974 module: &shader,
1975 entry_point: Some("glyph_atlas_fs_main"),
1976 compilation_options: wgpu::PipelineCompilationOptions::default(),
1977 targets: &[Some(wgpu::ColorTargetState {
1978 format: surface_format,
1979 blend: Some(blend_state_for_mode(BlendMode::SrcOver)),
1980 write_mask: wgpu::ColorWrites::ALL,
1981 })],
1982 }),
1983 primitive: wgpu::PrimitiveState {
1984 topology: wgpu::PrimitiveTopology::TriangleList,
1985 strip_index_format: None,
1986 front_face: wgpu::FrontFace::Ccw,
1987 cull_mode: None,
1988 unclipped_depth: false,
1989 polygon_mode: wgpu::PolygonMode::Fill,
1990 conservative: false,
1991 },
1992 depth_stencil: display_clip::content_depth_state(depth),
1993 multisample: wgpu::MultisampleState::default(),
1994 multiview_mask: None,
1995 cache: None,
1996 },
1997 )
1998}
1999
2000#[cfg(not(target_arch = "wasm32"))]
2008fn create_display_clip_occluder_pipeline(
2009 device: &wgpu::Device,
2010 cache: Option<&wgpu::PipelineCache>,
2011 surface_format: wgpu::TextureFormat,
2012) -> wgpu::RenderPipeline {
2013 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
2014 label: Some("Display Clip Occluder Shader"),
2015 source: wgpu::ShaderSource::Wgsl(display_clip::OCCLUDER_SHADER.into()),
2016 });
2017 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2018 label: Some("Display Clip Occluder Pipeline Layout"),
2019 bind_group_layouts: &[],
2020 immediate_size: 0,
2021 });
2022 create_render_pipeline_logged(
2023 device,
2024 cache,
2025 "occluder",
2026 wgpu::RenderPipelineDescriptor {
2027 label: Some("Display Clip Occluder Pipeline"),
2028 layout: Some(&pipeline_layout),
2029 vertex: wgpu::VertexState {
2030 module: &shader,
2031 entry_point: Some("mask_vs"),
2032 compilation_options: wgpu::PipelineCompilationOptions::default(),
2033 buffers: &[wgpu::VertexBufferLayout {
2034 array_stride: (std::mem::size_of::<[f32; 2]>()) as wgpu::BufferAddress,
2035 step_mode: wgpu::VertexStepMode::Vertex,
2036 attributes: &[wgpu::VertexAttribute {
2037 offset: 0,
2038 shader_location: 0,
2039 format: wgpu::VertexFormat::Float32x2,
2040 }],
2041 }],
2042 },
2043 fragment: Some(wgpu::FragmentState {
2044 module: &shader,
2045 entry_point: Some("mask_fs"),
2046 compilation_options: wgpu::PipelineCompilationOptions::default(),
2047 targets: &[Some(wgpu::ColorTargetState {
2048 format: surface_format,
2049 blend: None,
2050 write_mask: wgpu::ColorWrites::empty(),
2051 })],
2052 }),
2053 primitive: wgpu::PrimitiveState {
2054 topology: wgpu::PrimitiveTopology::TriangleList,
2055 strip_index_format: None,
2056 front_face: wgpu::FrontFace::Ccw,
2057 cull_mode: None,
2058 unclipped_depth: false,
2059 polygon_mode: wgpu::PolygonMode::Fill,
2060 conservative: false,
2061 },
2062 depth_stencil: Some(wgpu::DepthStencilState {
2063 format: display_clip::DISPLAY_CLIP_DEPTH_FORMAT,
2064 depth_write_enabled: Some(true),
2065 depth_compare: Some(wgpu::CompareFunction::Always),
2066 stencil: wgpu::StencilState::default(),
2067 bias: wgpu::DepthBiasState::default(),
2068 }),
2069 multisample: wgpu::MultisampleState::default(),
2070 multiview_mask: None,
2071 cache: None,
2072 },
2073 )
2074}
2075
2076#[repr(C)]
2077#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2078struct Vertex {
2079 position: [f32; 2],
2080 color: [f32; 4],
2081 uv: [f32; 2],
2082 uv_bounds: [f32; 4],
2083}
2084
2085impl Vertex {
2086 const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
2087 0 => Float32x2,
2088 1 => Float32x4,
2089 2 => Float32x2,
2090 3 => Float32x4
2091 ];
2092
2093 fn desc() -> wgpu::VertexBufferLayout<'static> {
2094 wgpu::VertexBufferLayout {
2095 array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
2096 step_mode: wgpu::VertexStepMode::Vertex,
2097 attributes: &Self::ATTRIBS,
2098 }
2099 }
2100}
2101
2102#[repr(C)]
2103#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2104struct Uniforms {
2105 viewport: [f32; 2],
2106 viewport_offset: [f32; 2],
2107}
2108
2109#[repr(C)]
2115#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2116struct ShapeData {
2117 rect: [f32; 4], radii: [f32; 4],
2122 gradient_params: [f32; 4], clip_rect: [f32; 4], stroke_params: [f32; 4],
2127 arc_params: [f32; 4],
2129 quad01: [f32; 4],
2131 quad23: [f32; 4],
2133 color: [f32; 4],
2135 brush_type: u32, gradient_start: u32, gradient_count: u32, gradient_tile_mode: u32, }
2140
2141const SHAPE_KIND_FILL: u32 = 0;
2143const SHAPE_KIND_STROKE: u32 = 1;
2144const SHAPE_KIND_ARC: u32 = 2;
2145
2146fn stroke_cap_code(cap: StrokeCap) -> u32 {
2147 match cap {
2148 StrokeCap::Butt => 0,
2149 StrokeCap::Round => 1,
2150 StrokeCap::Square => 2,
2151 }
2152}
2153
2154fn stroke_join_code(join: StrokeJoin) -> u32 {
2155 match join {
2156 StrokeJoin::Miter => 0,
2157 StrokeJoin::Round => 1,
2158 StrokeJoin::Bevel => 2,
2159 }
2160}
2161
2162fn pack_shape_flags(kind: u32, cap: StrokeCap, join: StrokeJoin) -> f32 {
2168 ((kind & 3) | (stroke_cap_code(cap) << 2) | (stroke_join_code(join) << 4)) as f32
2169}
2170
2171#[cfg(not(target_arch = "wasm32"))]
2179static SHAPE_CONVERT_TUNER: crate::cost_tuner::CostTuner =
2180 crate::cost_tuner::CostTuner::new("shape-convert", 256, 400_000);
2181
2182#[cfg(not(target_arch = "wasm32"))]
2183pub(crate) fn shape_convert_worker_count() -> usize {
2184 static WORKERS: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
2185 *WORKERS.get_or_init(|| {
2186 let cpus = std::thread::available_parallelism()
2187 .map(|count| count.get())
2188 .unwrap_or(1);
2189 let workers = cpus.clamp(1, 4);
2190 log::info!("[shape-convert] fan-out width {workers} (available parallelism {cpus})");
2194 workers
2195 })
2196}
2197
2198#[cfg(target_arch = "wasm32")]
2199pub(crate) fn shape_convert_worker_count() -> usize {
2200 1
2201}
2202
2203fn shape_gradient_stop_count(shape: &DrawShape, brushes: &[Brush]) -> usize {
2204 match shape.brush {
2205 SceneBrush::Solid(_) => 0,
2206 SceneBrush::Gradient(index) => match &brushes[index as usize] {
2207 Brush::Solid(_) => 0,
2208 Brush::LinearGradient { colors, .. }
2209 | Brush::RadialGradient { colors, .. }
2210 | Brush::SweepGradient { colors, .. } => colors.len(),
2211 },
2212 }
2213}
2214
2215fn convert_shape_into_slots(
2220 shape: &DrawShape,
2221 brushes: &[Brush],
2222 root_scale: f32,
2223 gradient_start: u32,
2224 shape_out: &mut ShapeData,
2225 gradient_out: &mut [GradientStop],
2226) {
2227 let snap_delta = shape
2228 .snap_anchor
2229 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
2230 .unwrap_or_default();
2231 let local_rect = shape.local_rect.translate(snap_delta.x, snap_delta.y);
2232 let quad = translate_quad(shape.quad, snap_delta);
2233 let clip = shape.clip;
2236 let canonicalize = shape.snap_anchor.is_some();
2237 let device_local_rect = if canonicalize {
2238 canonicalized_scaled_rect(local_rect, root_scale)
2239 } else {
2240 Rect {
2241 x: local_rect.x * root_scale,
2242 y: local_rect.y * root_scale,
2243 width: local_rect.width * root_scale,
2244 height: local_rect.height * root_scale,
2245 }
2246 };
2247 let device_quad = if canonicalize {
2248 canonicalized_scaled_quad(quad, root_scale)
2249 } else {
2250 scaled_quad(quad, root_scale)
2251 };
2252 let canonicalize_brush_coordinate = |value| {
2253 if canonicalize {
2254 canonicalize_device_coordinate(value)
2255 } else {
2256 value
2257 }
2258 };
2259
2260 let clip_rect = if let Some(clip) = clip {
2262 let device_clip = if canonicalize {
2263 canonicalized_scaled_rect(clip, root_scale)
2264 } else {
2265 Rect {
2266 x: clip.x * root_scale,
2267 y: clip.y * root_scale,
2268 width: clip.width * root_scale,
2269 height: clip.height * root_scale,
2270 }
2271 };
2272 [
2273 device_clip.x,
2274 device_clip.y,
2275 device_clip.width,
2276 device_clip.height,
2277 ]
2278 } else {
2279 [0.0, 0.0, 0.0, 0.0]
2280 };
2281
2282 let mut fill_gradient_entries = |colors: &[Color], stops: Option<&[f32]>| {
2284 let count = colors.len();
2285 let explicit_stops = stops.filter(|values| values.len() == count);
2286 for (index, color) in colors.iter().enumerate() {
2287 let position = explicit_stops
2288 .map(|values| values[index])
2289 .unwrap_or_else(|| {
2290 if count <= 1 {
2291 0.0
2292 } else {
2293 index as f32 / (count - 1) as f32
2294 }
2295 });
2296 gradient_out[index] = GradientStop {
2297 color: [color.r(), color.g(), color.b(), color.a()],
2298 position: [position, 0.0, 0.0, 0.0],
2299 };
2300 }
2301 count as u32
2302 };
2303 let mut gradient_params = [0.0f32; 4];
2304 let (brush_type, gradient_count, gradient_tile_mode) = match &shape.brush {
2305 SceneBrush::Solid(_) => (0u32, 0u32, gradient_tile_mode_value(TileMode::Clamp)),
2306 SceneBrush::Gradient(index) => match &brushes[*index as usize] {
2307 Brush::Solid(_) => (0u32, 0u32, gradient_tile_mode_value(TileMode::Clamp)),
2308 Brush::LinearGradient {
2309 colors,
2310 stops,
2311 start,
2312 end,
2313 tile_mode,
2314 } => {
2315 let count = fill_gradient_entries(colors, stops.as_deref());
2316 gradient_params = [
2317 canonicalize_brush_coordinate(resolve_gradient_point(
2318 device_local_rect.x,
2319 device_local_rect.width,
2320 start.x * root_scale,
2321 )),
2322 canonicalize_brush_coordinate(resolve_gradient_point(
2323 device_local_rect.y,
2324 device_local_rect.height,
2325 start.y * root_scale,
2326 )),
2327 canonicalize_brush_coordinate(resolve_gradient_point(
2328 device_local_rect.x,
2329 device_local_rect.width,
2330 end.x * root_scale,
2331 )),
2332 canonicalize_brush_coordinate(resolve_gradient_point(
2333 device_local_rect.y,
2334 device_local_rect.height,
2335 end.y * root_scale,
2336 )),
2337 ];
2338 (1u32, count, gradient_tile_mode_value(*tile_mode))
2339 }
2340 Brush::RadialGradient {
2341 colors,
2342 stops,
2343 center,
2344 radius,
2345 tile_mode,
2346 } => {
2347 let count = fill_gradient_entries(colors, stops.as_deref());
2348 gradient_params = [
2349 canonicalize_brush_coordinate(device_local_rect.x + center.x * root_scale),
2350 canonicalize_brush_coordinate(device_local_rect.y + center.y * root_scale),
2351 (radius * root_scale).max(f32::EPSILON),
2352 0.0,
2353 ];
2354 (2u32, count, gradient_tile_mode_value(*tile_mode))
2355 }
2356 Brush::SweepGradient {
2357 colors,
2358 stops,
2359 center,
2360 } => {
2361 let count = fill_gradient_entries(colors, stops.as_deref());
2362 gradient_params = [
2363 canonicalize_brush_coordinate(device_local_rect.x + center.x * root_scale),
2364 canonicalize_brush_coordinate(device_local_rect.y + center.y * root_scale),
2365 0.0,
2366 0.0,
2367 ];
2368 (3u32, count, gradient_tile_mode_value(TileMode::Clamp))
2369 }
2370 },
2371 };
2372
2373 let stroke_outset = shape
2378 .stroke
2379 .map(|stroke| stroke.half_width())
2380 .unwrap_or(0.0);
2381 let geometry_width = (local_rect.width - stroke_outset * 2.0).max(0.0);
2382 let geometry_height = (local_rect.height - stroke_outset * 2.0).max(0.0);
2383
2384 let radii = if let Some(arc) = shape.arc {
2385 if arc.sweep_angle >= cranpose_ui_graphics::TAU && arc.start_angle == 0.0 {
2395 [0.0, -1.0, 0.0, -1.0]
2396 } else {
2397 let half_sweep = arc.sweep_angle.clamp(0.0, cranpose_ui_graphics::TAU) * 0.5;
2398 let (mid_sin, mid_cos) = (arc.start_angle + half_sweep).sin_cos();
2399 let (half_sin, half_cos) = half_sweep.sin_cos();
2400 [mid_sin, mid_cos, half_sin.max(0.0), half_cos]
2401 }
2402 } else if let Some(rounded) = shape.shape {
2403 let resolved = rounded.resolve(geometry_width, geometry_height);
2404 [
2405 resolved.top_left * root_scale,
2406 resolved.top_right * root_scale,
2407 resolved.bottom_left * root_scale,
2408 resolved.bottom_right * root_scale,
2409 ]
2410 } else {
2411 [0.0, 0.0, 0.0, 0.0]
2412 };
2413
2414 let device_rect = [
2415 device_local_rect.x,
2416 device_local_rect.y,
2417 device_local_rect.width,
2418 device_local_rect.height,
2419 ];
2420
2421 let (stroke_params, arc_params) = match (shape.arc, shape.stroke) {
2425 (Some(arc), _) => (
2426 [
2427 0.0,
2428 pack_shape_flags(SHAPE_KIND_ARC, arc.cap, StrokeJoin::Miter),
2429 arc.outer_radius * root_scale,
2430 arc.inner_radius * root_scale,
2431 ],
2432 [
2433 (arc.center.x + snap_delta.x) * root_scale,
2434 (arc.center.y + snap_delta.y) * root_scale,
2435 arc.start_angle,
2436 arc.sweep_angle,
2437 ],
2438 ),
2439 (None, Some(stroke)) => (
2440 [
2441 stroke.width.max(0.0) * root_scale,
2442 pack_shape_flags(SHAPE_KIND_STROKE, stroke.cap, stroke.join),
2443 0.0,
2444 0.0,
2445 ],
2446 [0.0; 4],
2447 ),
2448 (None, None) => (
2449 [
2450 0.0,
2451 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter),
2452 0.0,
2453 0.0,
2454 ],
2455 [0.0; 4],
2456 ),
2457 };
2458
2459 let color = match &shape.brush {
2460 SceneBrush::Solid(c) => [c.r(), c.g(), c.b(), c.a()],
2461 SceneBrush::Gradient(index) => match &brushes[*index as usize] {
2462 Brush::Solid(c) => [c.r(), c.g(), c.b(), c.a()],
2463 Brush::LinearGradient { colors, .. } => {
2464 let first = colors.first().unwrap_or(&Color(1.0, 1.0, 1.0, 1.0));
2465 [first.r(), first.g(), first.b(), first.a()]
2466 }
2467 Brush::RadialGradient { colors, .. } | Brush::SweepGradient { colors, .. } => {
2468 let first = colors.first().unwrap_or(&Color(1.0, 1.0, 1.0, 1.0));
2469 [first.r(), first.g(), first.b(), first.a()]
2470 }
2471 },
2472 };
2473
2474 *shape_out = ShapeData {
2475 rect: device_rect,
2476 radii,
2477 gradient_params,
2478 clip_rect,
2479 stroke_params,
2480 arc_params,
2481 quad01: [
2482 device_quad[0][0],
2483 device_quad[0][1],
2484 device_quad[1][0],
2485 device_quad[1][1],
2486 ],
2487 quad23: [
2488 device_quad[2][0],
2489 device_quad[2][1],
2490 device_quad[3][0],
2491 device_quad[3][1],
2492 ],
2493 color,
2494 brush_type,
2495 gradient_start,
2496 gradient_count,
2497 gradient_tile_mode,
2498 };
2499}
2500
2501fn quad_area_diag_enabled() -> bool {
2508 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2509 *ENABLED.get_or_init(|| std::env::var_os("CRANPOSE_QUAD_AREA_DIAG").is_some())
2510}
2511
2512fn convert_shapes_into_outputs(
2518 shape_refs: &[&DrawShape],
2519 brushes: &[Brush],
2520 gradient_offsets: &[u32],
2521 root_scale: f32,
2522 shape_data_out: &mut [ShapeData],
2523 gradients_out: &mut [GradientStop],
2524) {
2525 let shape_count = shape_refs.len();
2526 #[cfg(not(target_arch = "wasm32"))]
2527 let convert_started = Instant::now();
2528 #[cfg(not(target_arch = "wasm32"))]
2529 let parallel =
2530 SHAPE_CONVERT_TUNER.choose_parallel(shape_count) && shape_convert_worker_count() > 1;
2531 if quad_area_diag_enabled() {
2532 let quad_area = |q: [[f32; 2]; 4]| {
2533 let poly = [q[0], q[1], q[3], q[2]];
2535 let mut twice = 0.0f64;
2536 for i in 0..4 {
2537 let a = poly[i];
2538 let b = poly[(i + 1) % 4];
2539 twice += a[0] as f64 * b[1] as f64 - b[0] as f64 * a[1] as f64;
2540 }
2541 twice.abs() * 0.5
2542 };
2543 let mut arc_quad = 0.0f64; let mut arc_band = 0.0f64; let mut arc_count = 0usize;
2546 let mut ring_count = 0usize;
2547 let mut other_quad = 0.0f64;
2548 let mut other_count = 0usize;
2549 let mut top_other: Vec<(f64, usize)> = Vec::new();
2552 for (index, shape) in shape_refs.iter().enumerate() {
2553 let area = quad_area(shape.quad);
2554 if let Some(arc) = shape.arc {
2555 arc_quad += area;
2556 arc_count += 1;
2557 if arc.sweep_angle >= cranpose_ui_graphics::TAU {
2558 ring_count += 1;
2559 }
2560 let ra = arc.mid_radius() as f64;
2561 let rb = arc.half_thickness() as f64;
2562 arc_band +=
2563 arc.sweep_angle as f64 * ra * (2.0 * rb) + std::f64::consts::PI * rb * rb;
2564 } else {
2565 other_quad += area;
2566 other_count += 1;
2567 top_other.push((area, index));
2568 }
2569 }
2570 let scale2 = (root_scale as f64) * (root_scale as f64);
2571 eprintln!(
2572 "[quad-area] arcs={arc_count} (rings={ring_count}) arc_quad_px={:.0} arc_band_px={:.0} | other={other_count} other_px={:.0}",
2573 arc_quad * scale2,
2574 arc_band * scale2,
2575 other_quad * scale2,
2576 );
2577 top_other.sort_by(|a, b| b.0.total_cmp(&a.0));
2578 for &(area, index) in top_other.iter().take(4) {
2579 let shape = shape_refs[index];
2580 let brush = match shape.brush.resolve(brushes).as_ref() {
2581 cranpose_ui_graphics::Brush::Solid(color) => format!("solid a={:.2}", color.3),
2582 cranpose_ui_graphics::Brush::LinearGradient { colors, .. } => {
2583 format!("linear n={}", colors.len())
2584 }
2585 cranpose_ui_graphics::Brush::RadialGradient { colors, .. } => {
2586 format!("radial n={}", colors.len())
2587 }
2588 cranpose_ui_graphics::Brush::SweepGradient { colors, .. } => {
2589 format!("sweep n={}", colors.len())
2590 }
2591 };
2592 eprintln!(
2593 "[quad-area] top other: {:.0}px {}x{} at ({:.0},{:.0}) {} shape={} stroke={} clip={} blend={:?} z={}",
2594 area * scale2,
2595 shape.rect.width.round(),
2596 shape.rect.height.round(),
2597 shape.rect.x,
2598 shape.rect.y,
2599 brush,
2600 shape.shape.is_some(),
2601 shape.stroke.is_some(),
2602 shape.clip.is_some(),
2603 shape.blend_mode,
2604 shape.z_index,
2605 );
2606 }
2607 }
2608 #[cfg(target_arch = "wasm32")]
2609 let parallel = false;
2610 let workers = if parallel {
2611 shape_convert_worker_count()
2612 } else {
2613 1
2614 };
2615 if workers <= 1 {
2616 for (idx, shape) in shape_refs.iter().enumerate() {
2617 let gradient_start = gradient_offsets[idx];
2618 let gradient_end = gradient_offsets[idx + 1];
2619 convert_shape_into_slots(
2620 shape,
2621 brushes,
2622 root_scale,
2623 gradient_start,
2624 &mut shape_data_out[idx],
2625 &mut gradients_out[gradient_start as usize..gradient_end as usize],
2626 );
2627 }
2628 #[cfg(not(target_arch = "wasm32"))]
2629 SHAPE_CONVERT_TUNER.record(
2630 false,
2631 shape_count,
2632 convert_started.elapsed().as_nanos() as u64,
2633 );
2634 return;
2635 }
2636
2637 let chunk_len = shape_count.div_ceil(workers);
2638 let mut shape_data_rest = shape_data_out;
2639 let mut gradients_rest = gradients_out;
2640 std::thread::scope(|scope| {
2641 let mut chunk_start = 0usize;
2642 while chunk_start < shape_count {
2643 let chunk_end = (chunk_start + chunk_len).min(shape_count);
2644 let count = chunk_end - chunk_start;
2645 let gradient_base = gradient_offsets[chunk_start];
2646 let gradient_span = (gradient_offsets[chunk_end] - gradient_base) as usize;
2647 let (shape_data_chunk, rest) = std::mem::take(&mut shape_data_rest).split_at_mut(count);
2648 shape_data_rest = rest;
2649 let (gradient_chunk, rest) =
2650 std::mem::take(&mut gradients_rest).split_at_mut(gradient_span);
2651 gradients_rest = rest;
2652 let chunk_refs = &shape_refs[chunk_start..chunk_end];
2653 let chunk_offsets = &gradient_offsets[chunk_start..=chunk_end];
2654 let mut convert_chunk = move || {
2655 for (j, shape) in chunk_refs.iter().enumerate() {
2656 let gradient_start = chunk_offsets[j];
2657 let local_start = (gradient_start - gradient_base) as usize;
2658 let local_end = (chunk_offsets[j + 1] - gradient_base) as usize;
2659 convert_shape_into_slots(
2660 shape,
2661 brushes,
2662 root_scale,
2663 gradient_start,
2664 &mut shape_data_chunk[j],
2665 &mut gradient_chunk[local_start..local_end],
2666 );
2667 }
2668 };
2669 if chunk_end == shape_count {
2670 convert_chunk();
2674 } else {
2675 scope.spawn(convert_chunk);
2676 }
2677 chunk_start = chunk_end;
2678 }
2679 });
2680 #[cfg(not(target_arch = "wasm32"))]
2681 SHAPE_CONVERT_TUNER.record(
2682 true,
2683 shape_count,
2684 convert_started.elapsed().as_nanos() as u64,
2685 );
2686}
2687
2688#[repr(C)]
2689#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2690struct GradientStop {
2691 color: [f32; 4],
2692 position: [f32; 4],
2693}
2694
2695#[cfg(not(target_arch = "wasm32"))]
2699const MAX_REPLAY_SLOTS: u32 = 128;
2700#[cfg(not(target_arch = "wasm32"))]
2701const REPLAY_TRANSFORM_STRIDE: u64 = 256;
2702
2703#[cfg(not(target_arch = "wasm32"))]
2710struct ReplaySlot {
2711 paint_buffer: wgpu::Buffer,
2716 bind_group: wgpu::BindGroup,
2717 shape_count: u32,
2718 paint_mirror: Vec<[f32; 4]>,
2724 mesh: Option<ReplaySlotMesh>,
2729 capture_epoch: u64,
2734 has_gradient: bool,
2739 fill_diag_shapes: Vec<FillDiagShapeRecord>,
2745 shape_aabbs: Vec<[f32; 4]>,
2751 area_prefix: Vec<f32>,
2755 submitted_area_scale: f32,
2759}
2760
2761#[cfg(not(target_arch = "wasm32"))]
2771struct ReplaySlotMesh {
2772 vertex_buffer: wgpu::Buffer,
2773 index_buffer: wgpu::Buffer,
2779 index_prefix: Vec<u32>,
2785 meshed_arcs: usize,
2790 meshed_rims: usize,
2791 passthrough: usize,
2792}
2793
2794#[cfg(not(target_arch = "wasm32"))]
2798#[repr(C)]
2799#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2800struct MeshVertex {
2801 position: [f32; 2],
2802 uv: [f32; 2],
2803 shape_idx: u32,
2804}
2805
2806#[cfg(not(target_arch = "wasm32"))]
2807impl MeshVertex {
2808 const ATTRIBS: [wgpu::VertexAttribute; 3] =
2809 wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Uint32];
2810
2811 fn desc() -> wgpu::VertexBufferLayout<'static> {
2812 wgpu::VertexBufferLayout {
2813 array_stride: std::mem::size_of::<MeshVertex>() as wgpu::BufferAddress,
2814 step_mode: wgpu::VertexStepMode::Vertex,
2815 attributes: &Self::ATTRIBS,
2816 }
2817 }
2818}
2819
2820#[cfg(not(target_arch = "wasm32"))]
2825fn arc_mesh_enabled() -> bool {
2826 matches!(std::env::var("CRANPOSE_ARC_MESH").as_deref(), Ok("1"))
2842}
2843
2844#[cfg(not(target_arch = "wasm32"))]
2850const ARC_MESH_MARGIN: f32 = 1.0;
2851
2852#[cfg(not(target_arch = "wasm32"))]
2856const ARC_MESH_OVERSHOOT: f32 = 2.0;
2857
2858#[cfg(not(target_arch = "wasm32"))]
2859const ARC_MESH_MIN_SEGMENTS: usize = 4;
2860#[cfg(not(target_arch = "wasm32"))]
2861const ARC_MESH_MAX_SEGMENTS: usize = 64;
2862
2863#[cfg(not(target_arch = "wasm32"))]
2872const ARC_MESH_BUDGET_BYTES_PER_SHAPE: usize = 48 * std::mem::size_of::<MeshVertex>();
2873#[cfg(not(target_arch = "wasm32"))]
2874const ARC_MESH_BUDGET_FLOOR_BYTES: usize = 4096 * std::mem::size_of::<MeshVertex>();
2875
2876#[cfg(not(target_arch = "wasm32"))]
2879fn arc_mesh_bytes(vertices: usize, indices: usize) -> usize {
2880 vertices * std::mem::size_of::<MeshVertex>() + indices * std::mem::size_of::<u32>()
2881}
2882
2883#[cfg(not(target_arch = "wasm32"))]
2894const MESH_SLOT_MAX_STRETCHES: usize = 8;
2895
2896#[cfg(not(target_arch = "wasm32"))]
2919const RETAINED_MESH_MIN_PX2_DEFAULT: usize = 16384;
2920#[cfg(not(target_arch = "wasm32"))]
2925const RETAINED_MESH_MIN_PX2_RANGE: std::ops::RangeInclusive<usize> = 1024..=262144;
2926
2927#[cfg(not(target_arch = "wasm32"))]
2933fn retained_mesh_min_px2() -> f64 {
2934 parse_retained_mesh_min_px2(std::env::var("CRANPOSE_RETAINED_MESH_PX2").ok().as_deref())
2935}
2936
2937#[cfg(not(target_arch = "wasm32"))]
2938fn parse_retained_mesh_min_px2(value: Option<&str>) -> f64 {
2939 value
2940 .and_then(|value| value.trim().parse::<usize>().ok())
2941 .map(|px2| {
2942 px2.clamp(
2943 *RETAINED_MESH_MIN_PX2_RANGE.start(),
2944 *RETAINED_MESH_MIN_PX2_RANGE.end(),
2945 )
2946 })
2947 .unwrap_or(RETAINED_MESH_MIN_PX2_DEFAULT) as f64
2948}
2949
2950#[cfg(not(target_arch = "wasm32"))]
2955struct ArcMeshBand {
2956 center: [f32; 2],
2957 inner: f32,
2958 outer: f32,
2959 start: f32,
2960 sweep: f32,
2961}
2962
2963#[cfg(not(target_arch = "wasm32"))]
2964fn arc_mesh_band(shape: &ShapeData) -> Option<ArcMeshBand> {
2965 let flags = shape.stroke_params[1].max(0.0) as u32;
2967 if flags & 3 != SHAPE_KIND_ARC {
2968 return None;
2969 }
2970 if shape.brush_type != 0 {
2974 return None;
2975 }
2976 if shape.clip_rect[2] > 0.0 && shape.clip_rect[3] > 0.0 {
2981 return None;
2982 }
2983 let [_, _, w, h] = shape.rect;
2984 if !(w > 0.0 && h > 0.0) {
2985 return None;
2986 }
2987 let [left, top, right, _] = shape.quad01;
2995 let [bl_x, bottom, br_x, br_y] = shape.quad23;
2996 let axis_aligned = shape.quad01[3] == top
2997 && bl_x == left
2998 && br_x == right
2999 && br_y == bottom
3000 && left < right
3001 && top < bottom;
3002 if !axis_aligned {
3003 return None;
3004 }
3005 let center = [shape.arc_params[0], shape.arc_params[1]];
3006 let start = shape.arc_params[2];
3007 let sweep = shape.arc_params[3];
3008 let outer = shape.stroke_params[2];
3009 let inner = shape.stroke_params[3];
3010 let finite = center[0].is_finite()
3011 && center[1].is_finite()
3012 && start.is_finite()
3013 && sweep.is_finite()
3014 && outer.is_finite()
3015 && inner.is_finite();
3016 if !finite || outer <= 0.0 || sweep <= 0.0 {
3017 return None;
3018 }
3019 Some(ArcMeshBand {
3020 center,
3021 inner,
3022 outer,
3023 start,
3024 sweep,
3025 })
3026}
3027
3028#[cfg(not(target_arch = "wasm32"))]
3037fn rim_mesh_enabled() -> bool {
3038 !matches!(std::env::var("CRANPOSE_RIM_MESH").as_deref(), Ok("0"))
3039}
3040
3041#[cfg(not(target_arch = "wasm32"))]
3049const RIM_MESH_VERTEX_CAPACITY: usize = 8192;
3050#[cfg(not(target_arch = "wasm32"))]
3053const RIM_MESH_INDEX_CAPACITY: usize = 32768;
3054
3055#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
3061#[derive(Clone, Copy, Debug)]
3062struct RimDraw {
3063 shape_index: u32,
3064 first_index: u32,
3065 index_count: u32,
3066}
3067
3068#[cfg(not(target_arch = "wasm32"))]
3072fn rim_mesh_capacity_warn() {
3073 use std::sync::atomic::{AtomicU64, Ordering};
3074 static OVERFLOWS: AtomicU64 = AtomicU64::new(0);
3075 let count = OVERFLOWS.fetch_add(1, Ordering::Relaxed);
3076 if count.is_multiple_of(512) {
3077 log::warn!(
3078 "[rim-mesh] transient buffers full; rim falls back to quad expansion \
3079 (lifetime overflows {})",
3080 count + 1,
3081 );
3082 }
3083}
3084
3085#[cfg(not(target_arch = "wasm32"))]
3114fn rim_band_geometry(shape: &ShapeData) -> Option<ArcMeshBand> {
3115 let flags = shape.stroke_params[1].max(0.0) as u32;
3117 if flags & 3 != SHAPE_KIND_STROKE {
3118 return None;
3119 }
3120 if shape.brush_type != 0 {
3123 return None;
3124 }
3125 if shape.clip_rect[2] > 0.0 && shape.clip_rect[3] > 0.0 {
3129 return None;
3130 }
3131 let [x, y, w, h] = shape.rect;
3132 if !(w > 0.0 && h > 0.0) {
3133 return None;
3134 }
3135 let [left, top, right, _] = shape.quad01;
3140 let [bl_x, bottom, br_x, br_y] = shape.quad23;
3141 let axis_aligned = shape.quad01[3] == top
3142 && bl_x == left
3143 && br_x == right
3144 && br_y == bottom
3145 && left < right
3146 && top < bottom;
3147 if !axis_aligned {
3148 return None;
3149 }
3150 if w.to_bits() != h.to_bits() {
3152 return None;
3153 }
3154 let [r0, r1, r2, r3] = shape.radii;
3156 if r0.to_bits() != r1.to_bits() || r0.to_bits() != r2.to_bits() || r0.to_bits() != r3.to_bits()
3157 {
3158 return None;
3159 }
3160 if !r0.is_finite() || r0 <= 0.0 {
3161 return None;
3162 }
3163 let sw = shape.stroke_params[0];
3164 if !sw.is_finite() || sw <= 0.0 {
3165 return None;
3166 }
3167 let geom_half = (w - sw) * 0.5;
3170 let center = [x + w * 0.5, y + h * 0.5];
3171 let inner = geom_half - sw * 0.5;
3172 let outer = geom_half + sw * 0.5;
3173 let finite =
3174 center[0].is_finite() && center[1].is_finite() && inner.is_finite() && outer.is_finite();
3175 if !finite || outer <= 0.0 {
3176 return None;
3177 }
3178 if (r0 - geom_half).abs() > 0.01 {
3180 return None;
3181 }
3182 Some(ArcMeshBand {
3183 center,
3184 inner,
3185 outer,
3186 start: 0.0,
3187 sweep: cranpose_ui_graphics::TAU,
3188 })
3189}
3190
3191#[cfg(not(target_arch = "wasm32"))]
3195fn rim_mesh_band(shape: &ShapeData) -> Option<ArcMeshBand> {
3196 let [_, _, w, h] = shape.rect;
3197 if w * h < 65536.0 {
3198 return None;
3199 }
3200 rim_band_geometry(shape)
3201}
3202
3203#[cfg(not(target_arch = "wasm32"))]
3210fn static_span_enabled() -> bool {
3211 !matches!(std::env::var("CRANPOSE_STATIC_SPAN").as_deref(), Ok("0"))
3212}
3213
3214#[cfg(not(target_arch = "wasm32"))]
3218const STATIC_SPAN_MAX_SHAPES: usize = 16;
3219
3220#[cfg(not(target_arch = "wasm32"))]
3224const STATIC_SPAN_UPGRADE_FRAMES: u32 = 30;
3225
3226#[cfg(not(target_arch = "wasm32"))]
3229#[derive(Clone, Copy, Debug, PartialEq)]
3230enum StaticSpanDecision {
3231 Pass,
3233 Hit { skip: usize },
3236 Capture { len: usize, clear: wgpu::Color },
3239}
3240
3241#[cfg(not(target_arch = "wasm32"))]
3289#[derive(Default)]
3290struct StaticSpanCache {
3291 texture: Option<OffscreenTarget>,
3295 key_shapes: Vec<ShapeData>,
3297 key_gradients: Vec<GradientStop>,
3299 key_width: u32,
3300 key_height: u32,
3301 key_clear: [u64; 4],
3305 key_has_gradient: bool,
3310 prev_shapes: Vec<ShapeData>,
3313 prev_gradients: Vec<GradientStop>,
3314 extension_stable_frames: u32,
3317 armed: bool,
3322 hits: u64,
3323 recaptures: u64,
3324}
3325
3326#[cfg(not(target_arch = "wasm32"))]
3327impl StaticSpanCache {
3328 fn engage(
3334 &mut self,
3335 load_op: wgpu::LoadOp<wgpu::Color>,
3336 first_batch: Option<(usize, BlendMode, bool)>,
3337 width: u32,
3338 height: u32,
3339 shapes: &[ShapeData],
3340 gradients: &[GradientStop],
3341 ) -> StaticSpanDecision {
3342 if !self.armed || !static_span_enabled() {
3343 return StaticSpanDecision::Pass;
3344 }
3345 let wgpu::LoadOp::Clear(clear) = load_op else {
3346 return StaticSpanDecision::Pass;
3347 };
3348 if clear.a != 1.0 {
3352 return StaticSpanDecision::Pass;
3353 }
3354 self.armed = false;
3355 let Some((batch_len, blend_mode, has_gradient)) = first_batch else {
3356 self.forget_observation();
3357 return StaticSpanDecision::Pass;
3358 };
3359 if blend_mode != BlendMode::SrcOver || batch_len == 0 {
3362 self.forget_observation();
3363 return StaticSpanDecision::Pass;
3364 }
3365 let leading = &shapes[..batch_len.min(STATIC_SPAN_MAX_SHAPES).min(shapes.len())];
3366 if leading.is_empty() {
3367 self.forget_observation();
3368 return StaticSpanDecision::Pass;
3369 }
3370 if !static_span_fullscreen_opaque(&leading[0], width, height) {
3371 self.forget_observation();
3372 return StaticSpanDecision::Pass;
3373 }
3374 let mut eligible = 1;
3377 while eligible < leading.len() && rim_mesh_band(&leading[eligible]).is_none() {
3378 eligible += 1;
3379 }
3380 let leading = &leading[..eligible];
3381 let clear_key = [
3382 clear.r.to_bits(),
3383 clear.g.to_bits(),
3384 clear.b.to_bits(),
3385 clear.a.to_bits(),
3386 ];
3387
3388 let key_len = self.key_shapes.len();
3389 let valid = self.texture.is_some()
3390 && key_len > 0
3391 && key_len <= leading.len()
3392 && self.key_width == width
3393 && self.key_height == height
3394 && self.key_clear == clear_key
3395 && self.key_has_gradient == has_gradient
3396 && span_records_equal(
3397 &self.key_shapes,
3398 &leading[..key_len],
3399 &self.key_gradients,
3400 gradients,
3401 );
3402
3403 let mut stable = 0;
3407 while stable < leading.len()
3408 && stable < self.prev_shapes.len()
3409 && span_records_equal(
3410 &self.prev_shapes[stable..stable + 1],
3411 &leading[stable..stable + 1],
3412 &self.prev_gradients,
3413 gradients,
3414 )
3415 {
3416 stable += 1;
3417 }
3418 self.remember_observation(leading, gradients);
3419
3420 if valid {
3421 if stable > key_len {
3433 self.extension_stable_frames += 1;
3434 if self.extension_stable_frames >= STATIC_SPAN_UPGRADE_FRAMES {
3435 self.extension_stable_frames = 0;
3436 return StaticSpanDecision::Capture { len: stable, clear };
3437 }
3438 } else {
3439 self.extension_stable_frames = 0;
3440 }
3441 self.hits += 1;
3442 if self.hits.is_multiple_of(600) {
3443 log::debug!(
3444 "[static-span] {} hits / {} recaptures lifetime (span {} shapes, {}x{})",
3445 self.hits,
3446 self.recaptures,
3447 key_len,
3448 width,
3449 height,
3450 );
3451 }
3452 return StaticSpanDecision::Hit { skip: key_len };
3453 }
3454
3455 self.extension_stable_frames = 0;
3456 if stable == 0 || span_gradient_len(&leading[..stable]) == 0 {
3465 return StaticSpanDecision::Pass;
3466 }
3467 StaticSpanDecision::Capture { len: stable, clear }
3468 }
3469
3470 fn remember_observation(&mut self, leading: &[ShapeData], gradients: &[GradientStop]) {
3472 self.prev_shapes.clear();
3473 self.prev_shapes.extend_from_slice(leading);
3474 let stop_len = span_gradient_len(leading);
3475 self.prev_gradients.clear();
3476 self.prev_gradients
3477 .extend_from_slice(&gradients[..stop_len]);
3478 }
3479
3480 fn forget_observation(&mut self) {
3481 self.prev_shapes.clear();
3482 self.prev_gradients.clear();
3483 self.extension_stable_frames = 0;
3484 }
3485
3486 #[allow(clippy::too_many_arguments)]
3489 fn store_key(
3490 &mut self,
3491 span: &[ShapeData],
3492 gradients: &[GradientStop],
3493 width: u32,
3494 height: u32,
3495 clear: wgpu::Color,
3496 has_gradient: bool,
3497 ) {
3498 self.key_shapes.clear();
3499 self.key_shapes.extend_from_slice(span);
3500 let stop_len = span_gradient_len(span);
3501 self.key_gradients.clear();
3502 self.key_gradients.extend_from_slice(&gradients[..stop_len]);
3503 self.key_width = width;
3504 self.key_height = height;
3505 self.key_clear = [
3506 clear.r.to_bits(),
3507 clear.g.to_bits(),
3508 clear.b.to_bits(),
3509 clear.a.to_bits(),
3510 ];
3511 self.key_has_gradient = has_gradient;
3512 self.recaptures += 1;
3513 if self.recaptures.is_multiple_of(64) || self.recaptures == 1 {
3514 log::debug!(
3515 "[static-span] recapture #{} (span {} shapes, {} stops, {}x{}; {} hits lifetime)",
3516 self.recaptures,
3517 self.key_shapes.len(),
3518 self.key_gradients.len(),
3519 width,
3520 height,
3521 self.hits,
3522 );
3523 }
3524 }
3525}
3526
3527#[cfg(not(target_arch = "wasm32"))]
3531fn span_gradient_len(span: &[ShapeData]) -> usize {
3532 span.iter().map(|shape| shape.gradient_count as usize).sum()
3533}
3534
3535#[cfg(not(target_arch = "wasm32"))]
3542fn span_records_equal(
3543 expected: &[ShapeData],
3544 actual: &[ShapeData],
3545 expected_gradients: &[GradientStop],
3546 actual_gradients: &[GradientStop],
3547) -> bool {
3548 if bytemuck::cast_slice::<ShapeData, u8>(expected)
3549 != bytemuck::cast_slice::<ShapeData, u8>(actual)
3550 {
3551 return false;
3552 }
3553 for shape in expected {
3554 let start = shape.gradient_start as usize;
3555 let end = start + shape.gradient_count as usize;
3556 if end > expected_gradients.len() || end > actual_gradients.len() {
3557 return false;
3558 }
3559 if bytemuck::cast_slice::<GradientStop, u8>(&expected_gradients[start..end])
3560 != bytemuck::cast_slice::<GradientStop, u8>(&actual_gradients[start..end])
3561 {
3562 return false;
3563 }
3564 }
3565 true
3566}
3567
3568#[cfg(not(target_arch = "wasm32"))]
3577fn static_span_fullscreen_opaque(shape: &ShapeData, width: u32, height: u32) -> bool {
3578 if shape.brush_type != 0 || shape.gradient_count != 0 {
3579 return false;
3580 }
3581 if shape.color[3] != 1.0 {
3582 return false;
3583 }
3584 if shape.clip_rect != [0.0; 4] || shape.stroke_params != [0.0; 4] || shape.radii != [0.0; 4] {
3585 return false;
3586 }
3587 let [left, top, right, top_right_y] = shape.quad01;
3590 let [bl_x, bottom, br_x, br_y] = shape.quad23;
3591 let axis_aligned = top_right_y == top
3592 && bl_x == left
3593 && br_x == right
3594 && br_y == bottom
3595 && left < right
3596 && top < bottom;
3597 axis_aligned && left <= 0.0 && top <= 0.0 && right >= width as f32 && bottom >= height as f32
3598}
3599
3600#[cfg(not(target_arch = "wasm32"))]
3611fn clip_polygon_axis(
3612 input: &[[f32; 2]],
3613 axis: usize,
3614 bound: f32,
3615 keep_at_most: bool,
3616 output: &mut Vec<[f32; 2]>,
3617) {
3618 output.clear();
3619 let inside = |p: [f32; 2]| {
3620 if keep_at_most {
3621 p[axis] <= bound
3622 } else {
3623 p[axis] >= bound
3624 }
3625 };
3626 let intersect = |a: [f32; 2], b: [f32; 2]| {
3627 let (p, q) = if (b[0], b[1]) < (a[0], a[1]) {
3628 (b, a)
3629 } else {
3630 (a, b)
3631 };
3632 let t = (bound - p[axis]) / (q[axis] - p[axis]);
3633 let mut point = [0.0f32; 2];
3634 point[axis] = bound;
3635 point[1 - axis] = p[1 - axis] + t * (q[1 - axis] - p[1 - axis]);
3636 point
3637 };
3638 for (index, ¤t) in input.iter().enumerate() {
3639 let previous = input[(index + input.len() - 1) % input.len()];
3640 match (inside(previous), inside(current)) {
3641 (true, true) => output.push(current),
3642 (true, false) => output.push(intersect(previous, current)),
3643 (false, true) => {
3644 output.push(intersect(previous, current));
3645 output.push(current);
3646 }
3647 (false, false) => {}
3648 }
3649 }
3650}
3651
3652#[cfg(not(target_arch = "wasm32"))]
3687fn emit_arc_band_mesh(
3688 shape: &ShapeData,
3689 shape_idx: u32,
3690 band: &ArcMeshBand,
3691 vertices: &mut Vec<MeshVertex>,
3692 indices: &mut Vec<u32>,
3693) -> Option<usize> {
3694 let [cx, cy] = band.center;
3695 let ra = (band.outer + band.inner) * 0.5;
3696 let rb = ((band.outer - band.inner) * 0.5).max(0.0);
3697 let rb_m = rb + ARC_MESH_MARGIN;
3698 let ro = ra + rb_m;
3699 let ri = (ra - rb_m).max(0.0);
3700 let tau = cranpose_ui_graphics::TAU;
3701
3702 let (range_start, range) = if band.sweep >= tau {
3703 (0.0, tau)
3704 } else {
3705 let pad = if rb_m < ra {
3706 (rb_m / ra).asin() + 0.05
3707 } else {
3708 std::f32::consts::PI
3711 };
3712 let padded = band.sweep + pad + pad;
3713 if padded >= tau {
3714 (0.0, tau)
3715 } else {
3716 (band.start - pad, padded)
3717 }
3718 };
3719 let closed = range >= tau;
3720
3721 let dtheta = (2.0 * (ro / (ro + ARC_MESH_OVERSHOOT)).acos()).clamp(tau / 64.0, tau / 6.0);
3722 let segments =
3723 ((range / dtheta).ceil() as usize).clamp(ARC_MESH_MIN_SEGMENTS, ARC_MESH_MAX_SEGMENTS);
3724 let step = range / segments as f32;
3725 let rc = ro / (step * 0.5).cos();
3726
3727 let boundary_count = if closed { segments } else { segments + 1 };
3731 let mut boundaries = Vec::with_capacity(boundary_count);
3732 for j in 0..boundary_count {
3733 let (sin, cos) = (range_start + step * j as f32).sin_cos();
3734 boundaries.push((
3735 [cx + cos * ri, cy + sin * ri],
3736 [cx + cos * rc, cy + sin * rc],
3737 ));
3738 }
3739
3740 let quad_min = [shape.quad01[0], shape.quad01[1]];
3741 let quad_max = [shape.quad23[2], shape.quad23[3]];
3742
3743 enum SegmentGeometry {
3748 Shared,
3749 Fan(Vec<[f32; 2]>),
3750 Empty,
3751 }
3752
3753 let mut polygon: Vec<[f32; 2]> = Vec::with_capacity(8);
3755 let mut scratch: Vec<[f32; 2]> = Vec::with_capacity(8);
3756 let mut segment_geometry = Vec::with_capacity(segments);
3757 let mut boundary_used = vec![false; boundary_count];
3758 for j in 0..segments {
3759 let jb = (j + 1) % boundary_count;
3760 let (inner_a, outer_a) = boundaries[j];
3761 let (inner_b, outer_b) = boundaries[jb];
3762 polygon.clear();
3763 polygon.extend_from_slice(&[inner_a, outer_a, outer_b, inner_b]);
3764 clip_polygon_axis(&polygon, 0, quad_min[0], false, &mut scratch);
3765 clip_polygon_axis(&scratch, 0, quad_max[0], true, &mut polygon);
3766 clip_polygon_axis(&polygon, 1, quad_min[1], false, &mut scratch);
3767 clip_polygon_axis(&scratch, 1, quad_max[1], true, &mut polygon);
3768 scratch.clear();
3771 for &point in polygon.iter() {
3772 if scratch.last() != Some(&point) {
3773 scratch.push(point);
3774 }
3775 }
3776 while scratch.len() > 1 && scratch.first() == scratch.last() {
3777 scratch.pop();
3778 }
3779 if scratch.len() < 3 {
3780 segment_geometry.push(SegmentGeometry::Empty);
3781 } else if scratch[..] == [inner_a, outer_a, outer_b, inner_b] {
3782 boundary_used[j] = true;
3783 boundary_used[jb] = true;
3784 segment_geometry.push(SegmentGeometry::Shared);
3785 } else {
3786 segment_geometry.push(SegmentGeometry::Fan(scratch.clone()));
3787 }
3788 }
3789
3790 let push_vertex = |vertices: &mut Vec<MeshVertex>, position: [f32; 2]| -> u32 {
3791 let index = vertices.len() as u32;
3792 vertices.push(MeshVertex {
3793 position,
3794 uv: [
3795 (position[0] - shape.rect[0]) / shape.rect[2],
3796 (position[1] - shape.rect[1]) / shape.rect[3],
3797 ],
3798 shape_idx,
3799 });
3800 index
3801 };
3802
3803 let mut boundary_vertex = vec![[0u32; 2]; boundary_count];
3806 for (j, used) in boundary_used.iter().enumerate() {
3807 if *used {
3808 let (inner, outer) = boundaries[j];
3809 boundary_vertex[j] = [push_vertex(vertices, inner), push_vertex(vertices, outer)];
3810 }
3811 }
3812
3813 let start_len = indices.len();
3816 for (j, geometry) in segment_geometry.iter().enumerate() {
3817 match geometry {
3818 SegmentGeometry::Empty => {}
3819 SegmentGeometry::Shared => {
3820 let jb = (j + 1) % boundary_count;
3821 let [in_a, out_a] = boundary_vertex[j];
3822 let [in_b, out_b] = boundary_vertex[jb];
3823 indices.extend_from_slice(&[in_a, out_a, out_b, in_a, out_b, in_b]);
3827 }
3828 SegmentGeometry::Fan(points) => {
3829 let base = vertices.len() as u32;
3830 for &point in points {
3831 push_vertex(vertices, point);
3832 }
3833 for i in 1..points.len() as u32 - 1 {
3834 indices.extend_from_slice(&[base, base + i, base + i + 1]);
3835 }
3836 }
3837 }
3838 }
3839 if indices.len() == start_len {
3840 return None;
3841 }
3842 Some(segments)
3843}
3844
3845#[cfg(not(target_arch = "wasm32"))]
3848fn triangles_shoelace_area(vertices: &[MeshVertex], indices: &[u32]) -> f64 {
3849 indices
3850 .as_chunks::<3>()
3851 .0
3852 .iter()
3853 .map(|tri| {
3854 let [a, b, c] = [
3855 vertices[tri[0] as usize].position,
3856 vertices[tri[1] as usize].position,
3857 vertices[tri[2] as usize].position,
3858 ];
3859 let cross = (b[0] as f64 - a[0] as f64) * (c[1] as f64 - a[1] as f64)
3860 - (b[1] as f64 - a[1] as f64) * (c[0] as f64 - a[0] as f64);
3861 cross.abs() * 0.5
3862 })
3863 .sum()
3864}
3865
3866#[cfg(not(target_arch = "wasm32"))]
3869fn quad_shoelace_area(shape: &ShapeData) -> f64 {
3870 let corners = [
3871 [shape.quad01[0] as f64, shape.quad01[1] as f64],
3872 [shape.quad01[2] as f64, shape.quad01[3] as f64],
3873 [shape.quad23[0] as f64, shape.quad23[1] as f64],
3874 [shape.quad23[2] as f64, shape.quad23[3] as f64],
3875 ];
3876 let tri = |a: [f64; 2], b: [f64; 2], c: [f64; 2]| {
3877 ((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])).abs() * 0.5
3878 };
3879 tri(corners[0], corners[1], corners[2]) + tri(corners[2], corners[1], corners[3])
3880}
3881
3882#[cfg(not(target_arch = "wasm32"))]
3887pub(crate) fn fill_area_diag_enabled() -> bool {
3888 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3889 *ENABLED.get_or_init(
3890 || matches!(std::env::var("CRANPOSE_FILL_DIAG").as_deref(), Ok(value) if value != "0"),
3891 )
3892}
3893
3894#[cfg(not(target_arch = "wasm32"))]
3896const FILL_DIAG_WINDOW_FRAMES: u32 = 120;
3897
3898#[cfg(not(target_arch = "wasm32"))]
3899const FILL_DIAG_BUCKETS: usize = 9;
3900
3901#[cfg(not(target_arch = "wasm32"))]
3908#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3909enum FillOpacityClass {
3910 Opaque = 0,
3911 Translucent = 1,
3912 NonSolid = 2,
3913}
3914
3915#[cfg(not(target_arch = "wasm32"))]
3916fn fill_opacity_class(shape: &ShapeData) -> FillOpacityClass {
3917 if shape.brush_type != 0 {
3918 FillOpacityClass::NonSolid
3919 } else if shape.color[3] == 1.0 {
3920 FillOpacityClass::Opaque
3921 } else {
3922 FillOpacityClass::Translucent
3923 }
3924}
3925
3926#[cfg(not(target_arch = "wasm32"))]
3931fn fill_diag_bucket(shape: &ShapeData) -> usize {
3932 match shape.stroke_params[1].max(0.0) as u32 & 3 {
3933 SHAPE_KIND_ARC => FillAreaDiag::ARC,
3934 SHAPE_KIND_STROKE => FillAreaDiag::RRECT_STROKE,
3935 _ if shape.radii.iter().any(|radius| *radius > 0.0) => FillAreaDiag::RRECT_FILL,
3936 _ => FillAreaDiag::RECT,
3937 }
3938}
3939
3940#[cfg(not(target_arch = "wasm32"))]
3941fn fill_diag_bucket_name(bucket: usize) -> &'static str {
3942 match bucket {
3943 FillAreaDiag::ARC => "arc",
3944 FillAreaDiag::RRECT_STROKE => "rrect-stroke",
3945 FillAreaDiag::RRECT_FILL => "rrect-fill",
3946 FillAreaDiag::RECT => "rect",
3947 FillAreaDiag::MESH => "mesh",
3948 FillAreaDiag::RETAINED => "retained",
3949 FillAreaDiag::IMAGE_GLYPH => "img+glyph",
3950 FillAreaDiag::EFFECT_COMPOSITE => "effect-comp",
3951 FillAreaDiag::OFFSCREEN_SOURCE => "offscr-src",
3952 _ => "?",
3953 }
3954}
3955
3956#[cfg(not(target_arch = "wasm32"))]
3977fn analytic_covered_area(shape: &ShapeData) -> f64 {
3978 let flags = shape.stroke_params[1].max(0.0) as u32;
3979 match flags & 3 {
3980 SHAPE_KIND_ARC => {
3981 let outer = f64::from(shape.stroke_params[2]).max(0.0);
3982 let inner = f64::from(shape.stroke_params[3]).clamp(0.0, outer);
3983 let tau = f64::from(cranpose_ui_graphics::TAU);
3984 let sweep = f64::from(shape.arc_params[3]).clamp(0.0, tau);
3985 let thickness = outer - inner;
3986 let band = sweep * 0.5 * (outer + inner) * thickness;
3987 let caps = if sweep >= tau {
3988 0.0
3989 } else {
3990 match (flags >> 2) & 3 {
3991 1 | 2 => std::f64::consts::PI * (thickness * 0.5) * (thickness * 0.5),
3995 _ => 0.0,
3996 }
3997 };
3998 band + caps
3999 }
4000 SHAPE_KIND_STROKE => {
4001 let stroke_width = f64::from(shape.stroke_params[0]).max(0.0);
4002 let geom_w = (f64::from(shape.rect[2]) - stroke_width).max(0.0);
4004 let geom_h = (f64::from(shape.rect[3]) - stroke_width).max(0.0);
4005 let max_radius = geom_w.min(geom_h) * 0.5;
4006 let radii_sum: f64 = shape
4007 .radii
4008 .iter()
4009 .map(|radius| f64::from(*radius).clamp(0.0, max_radius))
4010 .sum();
4011 let perimeter =
4012 2.0 * (geom_w + geom_h) - (2.0 - std::f64::consts::FRAC_PI_2) * radii_sum;
4013 perimeter.max(0.0) * stroke_width
4014 }
4015 _ => {
4016 let width = f64::from(shape.rect[2]).max(0.0);
4017 let height = f64::from(shape.rect[3]).max(0.0);
4018 let max_radius = width.min(height) * 0.5;
4019 let radii_sq: f64 = shape
4020 .radii
4021 .iter()
4022 .map(|radius| {
4023 let radius = f64::from(*radius).clamp(0.0, max_radius);
4024 radius * radius
4025 })
4026 .sum();
4027 width * height - (1.0 - std::f64::consts::FRAC_PI_4) * radii_sq
4028 }
4029 }
4030}
4031
4032#[cfg(not(target_arch = "wasm32"))]
4038fn aa_perimeter_allowance(shape: &ShapeData) -> f64 {
4039 let flags = shape.stroke_params[1].max(0.0) as u32;
4040 match flags & 3 {
4041 SHAPE_KIND_ARC => {
4042 let outer = f64::from(shape.stroke_params[2]).max(0.0);
4043 let inner = f64::from(shape.stroke_params[3]).clamp(0.0, outer);
4044 let tau = f64::from(cranpose_ui_graphics::TAU);
4045 let sweep = f64::from(shape.arc_params[3]).clamp(0.0, tau);
4046 let ends = if sweep >= tau {
4047 0.0
4048 } else {
4049 2.0 * (outer - inner)
4050 };
4051 sweep * (outer + inner) + ends
4052 }
4053 SHAPE_KIND_STROKE => {
4054 let stroke_width = f64::from(shape.stroke_params[0]).max(0.0);
4055 let geom_w = (f64::from(shape.rect[2]) - stroke_width).max(0.0);
4056 let geom_h = (f64::from(shape.rect[3]) - stroke_width).max(0.0);
4057 let max_radius = geom_w.min(geom_h) * 0.5;
4058 let radii_sum: f64 = shape
4059 .radii
4060 .iter()
4061 .map(|radius| f64::from(*radius).clamp(0.0, max_radius))
4062 .sum();
4063 let perimeter =
4064 2.0 * (geom_w + geom_h) - (2.0 - std::f64::consts::FRAC_PI_2) * radii_sum;
4065 2.0 * perimeter.max(0.0)
4066 }
4067 _ if shape.radii.iter().any(|radius| *radius > 0.0) => {
4068 let width = f64::from(shape.rect[2]).max(0.0);
4069 let height = f64::from(shape.rect[3]).max(0.0);
4070 let max_radius = width.min(height) * 0.5;
4071 let radii_sum: f64 = shape
4072 .radii
4073 .iter()
4074 .map(|radius| f64::from(*radius).clamp(0.0, max_radius))
4075 .sum();
4076 (2.0 * (width + height) - (2.0 - std::f64::consts::FRAC_PI_2) * radii_sum).max(0.0)
4077 }
4078 _ => 0.0,
4079 }
4080}
4081
4082#[cfg(not(target_arch = "wasm32"))]
4086fn analytic_lit_area(shape: &ShapeData) -> f64 {
4087 analytic_covered_area(shape) + aa_perimeter_allowance(shape)
4088}
4089
4090#[cfg(not(target_arch = "wasm32"))]
4092fn quad_aabb(shape: &ShapeData) -> [f64; 4] {
4093 let xs = [
4094 f64::from(shape.quad01[0]),
4095 f64::from(shape.quad01[2]),
4096 f64::from(shape.quad23[0]),
4097 f64::from(shape.quad23[2]),
4098 ];
4099 let ys = [
4100 f64::from(shape.quad01[1]),
4101 f64::from(shape.quad01[3]),
4102 f64::from(shape.quad23[1]),
4103 f64::from(shape.quad23[3]),
4104 ];
4105 let fold = |values: [f64; 4], pick: fn(f64, f64) -> f64| {
4106 values.into_iter().reduce(pick).unwrap_or(0.0)
4107 };
4108 [
4109 fold(xs, f64::min),
4110 fold(ys, f64::min),
4111 fold(xs, f64::max),
4112 fold(ys, f64::max),
4113 ]
4114}
4115
4116#[cfg(not(target_arch = "wasm32"))]
4120const CORNER_FILL_STRIPS: usize = 32;
4121
4122#[cfg(not(target_arch = "wasm32"))]
4131fn area_outside_inscribed_circle(aabb: [f64; 4], viewport: (u32, u32)) -> f64 {
4132 let viewport_w = f64::from(viewport.0);
4133 let viewport_h = f64::from(viewport.1);
4134 if viewport_w <= 0.0 || viewport_h <= 0.0 {
4135 return 0.0;
4136 }
4137 let x0 = aabb[0].max(0.0);
4138 let y0 = aabb[1].max(0.0);
4139 let x1 = aabb[2].min(viewport_w);
4140 let y1 = aabb[3].min(viewport_h);
4141 if x1 <= x0 || y1 <= y0 {
4142 return 0.0;
4143 }
4144 let center_x = viewport_w * 0.5;
4145 let center_y = viewport_h * 0.5;
4146 let radius = viewport_w.min(viewport_h) * 0.5;
4147 let strip = (x1 - x0) / CORNER_FILL_STRIPS as f64;
4148 let mut outside = 0.0;
4149 for index in 0..CORNER_FILL_STRIPS {
4150 let x = x0 + (index as f64 + 0.5) * strip;
4151 let dx = x - center_x;
4152 let chord_sq = radius * radius - dx * dx;
4153 let inside = if chord_sq > 0.0 {
4154 let half_chord = chord_sq.sqrt();
4155 (y1.min(center_y + half_chord) - y0.max(center_y - half_chord)).max(0.0)
4156 } else {
4157 0.0
4158 };
4159 outside += ((y1 - y0) - inside) * strip;
4160 }
4161 outside
4162}
4163
4164#[cfg(not(target_arch = "wasm32"))]
4168#[derive(Clone, Copy, Debug)]
4169struct FillDiagShapeRecord {
4170 drawn_px2: f64,
4174 lit_px2: f64,
4176 bucket: usize,
4178 opacity: FillOpacityClass,
4179 aabb: [f64; 4],
4181}
4182
4183#[cfg(not(target_arch = "wasm32"))]
4187fn fill_diag_capture_records(
4188 shape_data: &[ShapeData],
4189 mesh: Option<(&[MeshVertex], &[u32], &[u32])>,
4190) -> Vec<FillDiagShapeRecord> {
4191 shape_data
4192 .iter()
4193 .enumerate()
4194 .map(|(index, shape)| {
4195 let drawn_px2 = match mesh {
4196 Some((vertices, indices, index_prefix))
4200 if index_prefix[index + 1] > index_prefix[index] =>
4201 {
4202 let start = index_prefix[index] as usize;
4203 let end = index_prefix[index + 1] as usize;
4204 triangles_shoelace_area(vertices, &indices[start..end])
4205 }
4206 _ => quad_shoelace_area(shape),
4207 };
4208 FillDiagShapeRecord {
4209 drawn_px2,
4210 lit_px2: analytic_lit_area(shape).clamp(0.0, drawn_px2),
4211 bucket: fill_diag_bucket(shape),
4212 opacity: fill_opacity_class(shape),
4213 aabb: quad_aabb(shape),
4214 }
4215 })
4216 .collect()
4217}
4218
4219#[cfg(not(target_arch = "wasm32"))]
4222#[derive(Clone, Copy, Debug)]
4223struct FillDiagSlackEntry {
4224 slot: u32,
4225 shape: u32,
4226 bucket: usize,
4227 drawn_px2: f64,
4228 lit_px2: f64,
4229}
4230
4231#[cfg(not(target_arch = "wasm32"))]
4232const FILL_DIAG_SLACK_TOP: usize = 10;
4233
4234#[cfg(not(target_arch = "wasm32"))]
4279#[derive(Default)]
4280struct FillAreaDiag {
4281 frame: [std::cell::Cell<f64>; FILL_DIAG_BUCKETS],
4285 frame_lit: [std::cell::Cell<f64>; FILL_DIAG_BUCKETS],
4287 frame_opacity: [std::cell::Cell<f64>; 3],
4289 frame_corner: std::cell::Cell<f64>,
4291 viewport: std::cell::Cell<(u32, u32)>,
4294 window: [f64; FILL_DIAG_BUCKETS],
4296 window_lit: [f64; FILL_DIAG_BUCKETS],
4297 window_opacity: [f64; 3],
4298 window_corner: f64,
4299 window_frames: u32,
4300 slack_top: Vec<FillDiagSlackEntry>,
4303 slack_dumped: bool,
4304}
4305
4306#[cfg(not(target_arch = "wasm32"))]
4307impl FillAreaDiag {
4308 const ARC: usize = 0;
4309 const RRECT_STROKE: usize = 1;
4310 const RRECT_FILL: usize = 2;
4311 const RECT: usize = 3;
4312 const MESH: usize = 4;
4313 const RETAINED: usize = 5;
4314 const IMAGE_GLYPH: usize = 6;
4315 const EFFECT_COMPOSITE: usize = 7;
4316 const OFFSCREEN_SOURCE: usize = 8;
4317
4318 fn add(&self, bucket: usize, area_px2: f64) {
4319 let cell = &self.frame[bucket];
4320 cell.set(cell.get() + area_px2);
4321 }
4322
4323 fn add_lit(&self, bucket: usize, lit_px2: f64) {
4324 let cell = &self.frame_lit[bucket];
4325 cell.set(cell.get() + lit_px2);
4326 }
4327
4328 fn add_corner(&self, px2: f64) {
4329 self.frame_corner.set(self.frame_corner.get() + px2);
4330 }
4331
4332 fn is_full_frame(&self, viewport: ViewportUniformParams) -> bool {
4336 let (width, height) = self.viewport.get();
4337 width > 0
4338 && height > 0
4339 && viewport.width == width
4340 && viewport.height == height
4341 && viewport.offset == [0.0, 0.0]
4342 }
4343
4344 fn add_shape_quads(&self, shapes: &[ShapeData], viewport: ViewportUniformParams) {
4349 let full_frame = self.is_full_frame(viewport);
4350 let frame_viewport = self.viewport.get();
4351 let mut buckets = [0.0_f64; FILL_DIAG_BUCKETS];
4352 let mut lit_buckets = [0.0_f64; FILL_DIAG_BUCKETS];
4353 let mut opacity = [0.0_f64; 3];
4354 let mut corner = 0.0_f64;
4355 for shape in shapes {
4356 let bucket = fill_diag_bucket(shape);
4357 let quad = quad_shoelace_area(shape);
4358 let lit = analytic_lit_area(shape).clamp(0.0, quad);
4359 buckets[bucket] += quad;
4360 lit_buckets[bucket] += lit;
4361 opacity[fill_opacity_class(shape) as usize] += lit;
4362 if full_frame {
4363 corner += area_outside_inscribed_circle(quad_aabb(shape), frame_viewport);
4364 }
4365 }
4366 for (bucket, area) in buckets.into_iter().enumerate() {
4367 if area > 0.0 {
4368 self.add(bucket, area);
4369 }
4370 }
4371 for (bucket, lit) in lit_buckets.into_iter().enumerate() {
4372 if lit > 0.0 {
4373 self.add_lit(bucket, lit);
4374 }
4375 }
4376 for (class, lit) in self.frame_opacity.iter().zip(opacity) {
4377 class.set(class.get() + lit);
4378 }
4379 if corner > 0.0 {
4380 self.add_corner(corner);
4381 }
4382 }
4383
4384 fn note_static_span_skip(&self, shapes: &[ShapeData]) {
4393 for shape in shapes {
4394 let bucket = fill_diag_bucket(shape);
4395 let quad = quad_shoelace_area(shape);
4396 let lit = analytic_lit_area(shape).clamp(0.0, quad);
4397 self.add(bucket, -quad);
4398 self.add_lit(bucket, -lit);
4399 let class = &self.frame_opacity[fill_opacity_class(shape) as usize];
4400 class.set(class.get() - lit);
4401 }
4402 }
4403
4404 fn note_rim_mesh(&self, shape: &ShapeData, mesh_px2: f64) {
4411 let quad = quad_shoelace_area(shape);
4412 let lit = analytic_lit_area(shape).clamp(0.0, quad);
4413 self.add(Self::RRECT_STROKE, -quad);
4414 self.add_lit(Self::RRECT_STROKE, -lit);
4415 self.add(Self::MESH, mesh_px2);
4416 self.add_lit(Self::MESH, lit.min(mesh_px2));
4417 }
4418
4419 fn add_retained_range(
4425 &self,
4426 records: &[FillDiagShapeRecord],
4427 first: u32,
4428 last: u32,
4429 transform: &SimilarityTransform,
4430 ) {
4431 let Some(range) = records.get(first as usize..last as usize) else {
4432 return;
4433 };
4434 let scale = f64::from(transform.scale);
4435 let factor = scale * scale;
4436 let identity = transform.rot == [1.0, 0.0] && transform.scale == 1.0;
4437 let frame_viewport = self.viewport.get();
4438 let mut drawn = 0.0_f64;
4439 let mut lit = 0.0_f64;
4440 let mut opacity = [0.0_f64; 3];
4441 let mut corner = 0.0_f64;
4442 for record in range {
4443 drawn += record.drawn_px2;
4444 lit += record.lit_px2;
4445 opacity[record.opacity as usize] += record.lit_px2;
4446 if identity {
4447 corner += area_outside_inscribed_circle(record.aabb, frame_viewport);
4448 }
4449 }
4450 self.add(Self::RETAINED, drawn * factor);
4451 self.add_lit(Self::RETAINED, lit * factor);
4452 for (class, value) in self.frame_opacity.iter().zip(opacity) {
4453 class.set(class.get() + value * factor);
4454 }
4455 if corner > 0.0 {
4456 self.add_corner(corner);
4457 }
4458 }
4459
4460 fn note_retained_capture(&mut self, slot: u32, records: &[FillDiagShapeRecord]) {
4464 if self.slack_dumped {
4465 return;
4466 }
4467 for (index, record) in records.iter().enumerate() {
4468 if record.drawn_px2 - record.lit_px2 <= 0.0 {
4469 continue;
4470 }
4471 self.slack_top.push(FillDiagSlackEntry {
4472 slot,
4473 shape: index as u32,
4474 bucket: record.bucket,
4475 drawn_px2: record.drawn_px2,
4476 lit_px2: record.lit_px2,
4477 });
4478 }
4479 self.slack_top
4480 .sort_by(|a, b| (b.drawn_px2 - b.lit_px2).total_cmp(&(a.drawn_px2 - a.lit_px2)));
4481 self.slack_top.truncate(FILL_DIAG_SLACK_TOP);
4482 }
4483
4484 fn add_image_quad(&self, quad: &[[f32; 2]; 4]) {
4488 let corner = |index: usize| [f64::from(quad[index][0]), f64::from(quad[index][1])];
4489 let tri = |a: [f64; 2], b: [f64; 2], c: [f64; 2]| {
4490 ((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])).abs() * 0.5
4491 };
4492 let [a, b, c, d] = [corner(0), corner(1), corner(2), corner(3)];
4493 let area = tri(a, b, c) + tri(c, b, d);
4494 self.add(Self::IMAGE_GLYPH, area);
4495 self.add_lit(Self::IMAGE_GLYPH, area);
4496 }
4497
4498 fn add_glyph_quad(&self, quad: &CachedTextGlyphQuad) {
4500 let area = quad.width as f64 * quad.height as f64;
4501 self.add(Self::IMAGE_GLYPH, area);
4502 self.add_lit(Self::IMAGE_GLYPH, area);
4503 }
4504
4505 fn add_effect_fill(&self, composite_px2: f64, offscreen_px2: f64) {
4509 if composite_px2 > 0.0 {
4510 self.add(Self::EFFECT_COMPOSITE, composite_px2);
4511 self.add_lit(Self::EFFECT_COMPOSITE, composite_px2);
4512 }
4513 if offscreen_px2 > 0.0 {
4514 self.add(Self::OFFSCREEN_SOURCE, offscreen_px2);
4515 self.add_lit(Self::OFFSCREEN_SOURCE, offscreen_px2);
4516 }
4517 }
4518
4519 fn add_offscreen_target_fill(&self, px2: f64) {
4525 if px2 > 0.0 {
4526 self.add(Self::OFFSCREEN_SOURCE, px2);
4527 self.add_lit(Self::OFFSCREEN_SOURCE, px2);
4528 }
4529 }
4530
4531 fn reset_frame(&self, width: u32, height: u32) {
4535 for cell in &self.frame {
4536 cell.set(0.0);
4537 }
4538 for cell in &self.frame_lit {
4539 cell.set(0.0);
4540 }
4541 for cell in &self.frame_opacity {
4542 cell.set(0.0);
4543 }
4544 self.frame_corner.set(0.0);
4545 self.viewport.set((width, height));
4546 }
4547
4548 fn finish_frame(&mut self, width: u32, height: u32) {
4553 for (total, cell) in self.window.iter_mut().zip(&self.frame) {
4554 *total += cell.get();
4555 }
4556 for (total, cell) in self.window_lit.iter_mut().zip(&self.frame_lit) {
4557 *total += cell.get();
4558 }
4559 for (total, cell) in self.window_opacity.iter_mut().zip(&self.frame_opacity) {
4560 *total += cell.get();
4561 }
4562 self.window_corner += self.frame_corner.get();
4563 self.window_frames += 1;
4564 if self.window_frames < FILL_DIAG_WINDOW_FRAMES {
4565 return;
4566 }
4567 let frames = f64::from(self.window_frames);
4568 let mega = |bucket: usize| self.window[bucket] / frames / 1e6;
4569 let total_mega = self.window.iter().sum::<f64>() / frames / 1e6;
4570 let screen_mega = f64::from(width) * f64::from(height) / 1e6;
4571 let overdraw = if screen_mega > 0.0 {
4572 total_mega / screen_mega
4573 } else {
4574 0.0
4575 };
4576 log::warn!(
4577 "[fill-diag] Mpx/frame: arc {:.1}, rrect-stroke {:.1}, rrect-fill {:.1}, \
4578 rect {:.1}, mesh {:.1}, retained {:.1}, img+glyph {:.1}, \
4579 effect-comp {:.1}, offscr-src {:.1}, total {:.1} \
4580 ({:.1}x overdraw of {:.3} Mpx)",
4581 mega(Self::ARC),
4582 mega(Self::RRECT_STROKE),
4583 mega(Self::RRECT_FILL),
4584 mega(Self::RECT),
4585 mega(Self::MESH),
4586 mega(Self::RETAINED),
4587 mega(Self::IMAGE_GLYPH),
4588 mega(Self::EFFECT_COMPOSITE),
4589 mega(Self::OFFSCREEN_SOURCE),
4590 total_mega,
4591 overdraw,
4592 screen_mega,
4593 );
4594 let lit = |bucket: usize| self.window_lit[bucket] / frames / 1e6;
4598 let slack = |bucket: usize| (mega(bucket) - lit(bucket)).max(0.0);
4599 let truth = |bucket: usize| format!("{:.2}|{:.2}", lit(bucket), slack(bucket));
4600 log::warn!(
4601 "[fill-truth] Mpx/frame lit|slack: arc {}, rrect-stroke {}, rrect-fill {}, \
4602 rect {}, mesh {}, retained {}, img+glyph {}, effect-comp {}, offscr-src {}; \
4603 lit alpha Mpx: opaque {:.2}, translucent {:.2}, nonsolid {:.2}; \
4604 corner-outside {:.2}",
4605 truth(Self::ARC),
4606 truth(Self::RRECT_STROKE),
4607 truth(Self::RRECT_FILL),
4608 truth(Self::RECT),
4609 truth(Self::MESH),
4610 truth(Self::RETAINED),
4611 truth(Self::IMAGE_GLYPH),
4612 truth(Self::EFFECT_COMPOSITE),
4613 truth(Self::OFFSCREEN_SOURCE),
4614 self.window_opacity[FillOpacityClass::Opaque as usize] / frames / 1e6,
4615 self.window_opacity[FillOpacityClass::Translucent as usize] / frames / 1e6,
4616 self.window_opacity[FillOpacityClass::NonSolid as usize] / frames / 1e6,
4617 self.window_corner / frames / 1e6,
4618 );
4619 if !self.slack_dumped && !self.slack_top.is_empty() {
4620 log::warn!("[fill-truth] top retained slack (once per process, capture-space px):");
4621 for (rank, entry) in self.slack_top.iter().enumerate() {
4622 log::warn!(
4623 "[fill-truth] #{} slot {} shape {} {}: quad {:.0}, lit {:.0}, \
4624 slack {:.0}",
4625 rank + 1,
4626 entry.slot,
4627 entry.shape,
4628 fill_diag_bucket_name(entry.bucket),
4629 entry.drawn_px2,
4630 entry.lit_px2,
4631 entry.drawn_px2 - entry.lit_px2,
4632 );
4633 }
4634 self.slack_dumped = true;
4635 self.slack_top = Vec::new();
4636 }
4637 self.window = [0.0; FILL_DIAG_BUCKETS];
4638 self.window_lit = [0.0; FILL_DIAG_BUCKETS];
4639 self.window_opacity = [0.0; 3];
4640 self.window_corner = 0.0;
4641 self.window_frames = 0;
4642 }
4643}
4644
4645#[cfg(not(target_arch = "wasm32"))]
4646struct ArcMeshBuild {
4647 vertices: Vec<MeshVertex>,
4648 indices: Vec<u32>,
4650 index_prefix: Vec<u32>,
4656 meshed_arcs: usize,
4657 meshed_rims: usize,
4658 meshed_segments: usize,
4659 passthrough: usize,
4660 meshed_stretches: usize,
4664 quad_area: f64,
4665 mesh_area: f64,
4668}
4669
4670#[cfg(not(target_arch = "wasm32"))]
4684fn build_arc_mesh_vertices(shape_data: &[ShapeData], min_mesh_px2: f64) -> Option<ArcMeshBuild> {
4685 let budget_bytes =
4686 (shape_data.len() * ARC_MESH_BUDGET_BYTES_PER_SHAPE).max(ARC_MESH_BUDGET_FLOOR_BYTES);
4687 let mut build = ArcMeshBuild {
4688 vertices: Vec::new(),
4689 indices: Vec::new(),
4690 index_prefix: Vec::with_capacity(shape_data.len() + 1),
4691 meshed_arcs: 0,
4692 meshed_rims: 0,
4693 meshed_segments: 0,
4694 passthrough: 0,
4695 meshed_stretches: 0,
4696 quad_area: 0.0,
4697 mesh_area: 0.0,
4698 };
4699 build.index_prefix.push(0);
4700 let mut previous_meshed = false;
4701 for (index, shape) in shape_data.iter().enumerate() {
4702 let start = build.indices.len();
4703 let quad_px2 = quad_shoelace_area(shape);
4704 let band = if quad_px2 >= min_mesh_px2 {
4713 arc_mesh_band(shape)
4714 .map(|band| (band, false))
4715 .or_else(|| rim_band_geometry(shape).map(|band| (band, true)))
4716 } else {
4717 None
4718 };
4719 let meshed = band.and_then(|(band, is_rim)| {
4720 emit_arc_band_mesh(
4721 shape,
4722 index as u32,
4723 &band,
4724 &mut build.vertices,
4725 &mut build.indices,
4726 )
4727 .map(|segments| (segments, is_rim))
4728 });
4729 match meshed {
4730 Some((segments, is_rim)) => {
4731 if is_rim {
4732 build.meshed_rims += 1;
4733 } else {
4734 build.meshed_arcs += 1;
4735 }
4736 build.meshed_segments += segments;
4737 if !previous_meshed {
4738 build.meshed_stretches += 1;
4739 }
4740 previous_meshed = true;
4741 build.mesh_area +=
4742 triangles_shoelace_area(&build.vertices, &build.indices[start..]);
4743 }
4744 None => {
4745 build.passthrough += 1;
4746 previous_meshed = false;
4747 build.mesh_area += quad_px2;
4748 }
4749 }
4750 if arc_mesh_bytes(build.vertices.len(), build.indices.len()) > budget_bytes {
4751 return None;
4752 }
4753 build.index_prefix.push(build.indices.len() as u32);
4754 build.quad_area += quad_px2;
4755 }
4756 Some(build)
4757}
4758
4759#[cfg(not(target_arch = "wasm32"))]
4762struct ReplaySlotStore {
4763 slots: std::collections::HashMap<u32, ReplaySlot, cranpose_ui_graphics::FxBuildHasher>,
4764 transform_buffer: wgpu::Buffer,
4765 free_ids: Vec<u32>,
4766 next_capture_epoch: u64,
4770}
4771
4772#[cfg(not(target_arch = "wasm32"))]
4773impl ReplaySlotStore {
4774 fn new(device: &wgpu::Device) -> Self {
4775 let transform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4776 label: Some("Replay Transform Buffer"),
4777 size: (MAX_REPLAY_SLOTS + SEGMENT_CAPTURE_SLOTS) as u64 * REPLAY_TRANSFORM_STRIDE,
4784 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
4785 mapped_at_creation: false,
4786 });
4787 Self {
4788 slots: std::collections::HashMap::default(),
4789 transform_buffer,
4790 free_ids: (0..MAX_REPLAY_SLOTS).rev().collect(),
4791 next_capture_epoch: 1,
4792 }
4793 }
4794}
4795
4796#[cfg(not(target_arch = "wasm32"))]
4802fn retained_bundles_enabled() -> bool {
4803 std::env::var("CRANPOSE_RETAINED_BUNDLES").as_deref() != Ok("0")
4804}
4805
4806#[cfg(not(target_arch = "wasm32"))]
4814fn instanced_quads_enabled() -> bool {
4815 std::env::var("CRANPOSE_INSTANCED_QUADS").as_deref() != Ok("0")
4816}
4817
4818fn solid_trim_varyings_enabled() -> bool {
4831 std::env::var("CRANPOSE_SOLID_TRIM_VARYINGS").as_deref() == Ok("1")
4832}
4833
4834fn survive_gpu_errors_enabled() -> bool {
4843 std::env::var("CRANPOSE_SURVIVE_GPU_ERRORS").as_deref() != Ok("0")
4844}
4845
4846#[cfg(not(target_arch = "wasm32"))]
4866fn display_clip_cull_enabled() -> bool {
4867 std::env::var("CRANPOSE_ROUND_CULL").as_deref() == Ok("1")
4868}
4869
4870#[cfg(not(target_arch = "wasm32"))]
4874const INSTANCED_QUAD_INDICES: [u16; 6] = [0, 1, 2, 2, 1, 3];
4875
4876#[cfg(not(target_arch = "wasm32"))]
4882struct InstancedQuadPipelines {
4883 pipeline: PassPipeline,
4884 pipeline_dst_out: PassPipeline,
4885 pipeline_solid: PassPipeline,
4889 index_buffer: wgpu::Buffer,
4892}
4893
4894#[cfg(not(target_arch = "wasm32"))]
4895struct SegmentCapturePipelines {
4896 expanded: PassPipeline,
4897 expanded_solid: PassPipeline,
4898 mesh: PassPipeline,
4899 instanced: PassPipeline,
4900 instanced_solid: PassPipeline,
4901}
4902
4903#[cfg(not(target_arch = "wasm32"))]
4904#[derive(Clone, Copy)]
4905enum RetainedPipelineKind {
4906 Expanded,
4907 ExpandedSolid,
4908 Mesh,
4909 Instanced,
4910 InstancedSolid,
4911}
4912
4913#[cfg(not(target_arch = "wasm32"))]
4925enum RetainedCmd<'r> {
4926 Pipeline(RetainedPipelineKind),
4927 Uniforms(&'r wgpu::BindGroup),
4929 SlotBindings(&'r wgpu::BindGroup, u32),
4931 MeshVertices(&'r wgpu::Buffer),
4933 Index(&'r wgpu::Buffer, wgpu::IndexFormat),
4934 Draw(Range<u32>),
4936 DrawIndexed(Range<u32>, Range<u32>),
4938}
4939
4940#[cfg(not(target_arch = "wasm32"))]
4951#[derive(Clone, Debug, PartialEq, Eq, Hash)]
4952struct RetainedBundleOpKey {
4953 slot: u32,
4954 capture_epoch: Option<u64>,
4959 first: u32,
4960 last: u32,
4961 retained_index: u32,
4962 has_mesh: bool,
4963}
4964
4965#[cfg(not(target_arch = "wasm32"))]
4969#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
4970struct RetainedBundleKey {
4971 depth: bool,
4976 ops: Vec<RetainedBundleOpKey>,
4977}
4978
4979#[cfg(not(target_arch = "wasm32"))]
4980struct RetainedBundleCacheEntry<B> {
4981 bundle: B,
4982 last_used_frame: u64,
4983}
4984
4985#[cfg(not(target_arch = "wasm32"))]
4994struct RetainedBundleCacheImpl<B> {
4995 entries: HashMap<RetainedBundleKey, RetainedBundleCacheEntry<B>>,
4996 frame: u64,
4997 rebuilds: u64,
4998 cached_executes: u64,
4999 window_rebuilds: u64,
5000 window_executes: u64,
5001}
5002
5003#[cfg(not(target_arch = "wasm32"))]
5004type RetainedBundleCache = RetainedBundleCacheImpl<wgpu::RenderBundle>;
5005
5006#[cfg(not(target_arch = "wasm32"))]
5007impl<B> RetainedBundleCacheImpl<B> {
5008 fn new() -> Self {
5009 Self {
5010 entries: HashMap::default(),
5011 frame: 0,
5012 rebuilds: 0,
5013 cached_executes: 0,
5014 window_rebuilds: 0,
5015 window_executes: 0,
5016 }
5017 }
5018
5019 fn hit(&mut self, key: &RetainedBundleKey) -> bool {
5022 let frame = self.frame;
5023 match self.entries.get_mut(key) {
5024 Some(entry) => {
5025 entry.last_used_frame = frame;
5026 self.cached_executes += 1;
5027 self.window_executes += 1;
5028 true
5029 }
5030 None => false,
5031 }
5032 }
5033
5034 fn insert(&mut self, key: RetainedBundleKey, bundle: B) {
5036 self.rebuilds += 1;
5037 self.window_rebuilds += 1;
5038 self.entries.insert(
5039 key,
5040 RetainedBundleCacheEntry {
5041 bundle,
5042 last_used_frame: self.frame,
5043 },
5044 );
5045 }
5046
5047 fn get(&self, key: &RetainedBundleKey) -> Option<&B> {
5048 self.entries.get(key).map(|entry| &entry.bundle)
5049 }
5050
5051 fn clear(&mut self) {
5056 self.entries.clear();
5057 }
5058
5059 fn end_frame(&mut self) {
5063 let frame = self.frame;
5064 self.entries
5065 .retain(|_, entry| entry.last_used_frame >= frame);
5066 self.frame = self.frame.wrapping_add(1);
5067 let due = self.frame.is_multiple_of(1024)
5072 || (cranpose_core::env_flag!("CRANPOSE_COMMAND_REPLAY_DIAG")
5073 && self.frame.is_multiple_of(120));
5074 if due && self.window_rebuilds + self.window_executes > 0 {
5075 log::warn!(
5076 "[retained-bundles] {} stretches, {} rebuilds, {} cached executes ({} live bundles)",
5077 self.window_rebuilds + self.window_executes,
5078 self.window_rebuilds,
5079 self.window_executes,
5080 self.entries.len(),
5081 );
5082 self.window_rebuilds = 0;
5083 self.window_executes = 0;
5084 }
5085 }
5086
5087 fn stats(&self) -> (u64, u64) {
5089 (self.rebuilds, self.cached_executes)
5090 }
5091}
5092
5093struct CachedImageTexture {
5094 _texture: wgpu::Texture,
5095 _view: wgpu::TextureView,
5096 nearest_bind_group: wgpu::BindGroup,
5097 linear_bind_group: wgpu::BindGroup,
5098 bytes: usize,
5105}
5106
5107impl CachedImageTexture {
5108 fn bind_group(&self, sampling: ImageSampling) -> &wgpu::BindGroup {
5109 match sampling {
5110 ImageSampling::Nearest => &self.nearest_bind_group,
5111 ImageSampling::Linear => &self.linear_bind_group,
5112 }
5113 }
5114}
5115
5116#[derive(Clone, Copy)]
5117struct GlyphAtlasEntry {
5118 x: u32,
5119 y: u32,
5120 width: u32,
5121 height: u32,
5122}
5123
5124fn next_glyph_atlas_size(current: u32, max: u32) -> u32 {
5131 current.saturating_mul(2).clamp(1, max.max(1))
5132}
5133
5134struct TextGlyphAtlas {
5135 texture: wgpu::Texture,
5136 _view: wgpu::TextureView,
5137 bind_group: wgpu::BindGroup,
5138 entries: BoundedLruCache<SoftwareGlyphAtlasKey, GlyphAtlasEntry>,
5139 generation: u64,
5140 size: u32,
5144 max_size: u32,
5150 cursor_x: u32,
5151 cursor_y: u32,
5152 row_height: u32,
5153 upload_scratch: Vec<u8>,
5154}
5155
5156impl TextGlyphAtlas {
5157 fn new(
5158 device: &wgpu::Device,
5159 image_layout: &wgpu::BindGroupLayout,
5160 sampler: &wgpu::Sampler,
5161 size: u32,
5162 ) -> Self {
5163 let max_size = TEXT_GLYPH_ATLAS_MAX_SIZE.min(device.limits().max_texture_dimension_2d);
5164 let size = size.clamp(TEXT_GLYPH_ATLAS_MIN_SIZE.min(max_size), max_size);
5165 let texture = Self::create_texture(device, size);
5166 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
5167 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
5168 label: Some("Text Glyph Atlas Bind Group"),
5169 layout: image_layout,
5170 entries: &[
5171 wgpu::BindGroupEntry {
5172 binding: 0,
5173 resource: wgpu::BindingResource::TextureView(&view),
5174 },
5175 wgpu::BindGroupEntry {
5176 binding: 1,
5177 resource: wgpu::BindingResource::Sampler(sampler),
5178 },
5179 ],
5180 });
5181 Self {
5182 texture,
5183 _view: view,
5184 bind_group,
5185 entries: BoundedLruCache::with_capacity_at_least_one(MAX_TEXT_GLYPH_ATLAS_ITEMS),
5186 generation: 0,
5187 size,
5188 max_size,
5189 cursor_x: TEXT_GLYPH_ATLAS_PADDING,
5190 cursor_y: TEXT_GLYPH_ATLAS_PADDING,
5191 row_height: 0,
5192 upload_scratch: Vec::new(),
5193 }
5194 }
5195
5196 fn create_texture(device: &wgpu::Device, size: u32) -> wgpu::Texture {
5197 device.create_texture(&wgpu::TextureDescriptor {
5198 label: Some("Text Glyph Atlas Texture"),
5199 size: wgpu::Extent3d {
5200 width: size,
5201 height: size,
5202 depth_or_array_layers: 1,
5203 },
5204 mip_level_count: 1,
5205 sample_count: 1,
5206 dimension: wgpu::TextureDimension::D2,
5207 format: wgpu::TextureFormat::R8Unorm,
5208 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
5209 view_formats: &[],
5210 })
5211 }
5212
5213 fn reset(
5229 &mut self,
5230 device: &wgpu::Device,
5231 image_layout: &wgpu::BindGroupLayout,
5232 sampler: &wgpu::Sampler,
5233 ) {
5234 let generation = self.generation.wrapping_add(1);
5235 let grown = next_glyph_atlas_size(self.size, self.max_size);
5236 let mut next = Self::new(device, image_layout, sampler, grown);
5237 next.generation = generation;
5238 *self = next;
5239 }
5240
5241 fn generation(&self) -> u64 {
5242 self.generation
5243 }
5244
5245 fn size(&self) -> u32 {
5246 self.size
5247 }
5248
5249 fn entry(&mut self, key: &SoftwareGlyphAtlasKey) -> Option<GlyphAtlasEntry> {
5250 self.entries.get(key).copied()
5251 }
5252
5253 fn allocate(&mut self, width: u32, height: u32) -> Option<GlyphAtlasEntry> {
5254 if width == 0
5255 || height == 0
5256 || width + TEXT_GLYPH_ATLAS_PADDING * 2 > self.size
5257 || height + TEXT_GLYPH_ATLAS_PADDING * 2 > self.size
5258 {
5259 return None;
5260 }
5261
5262 if self.cursor_x + width + TEXT_GLYPH_ATLAS_PADDING > self.size {
5263 self.cursor_x = TEXT_GLYPH_ATLAS_PADDING;
5264 self.cursor_y = self
5265 .cursor_y
5266 .saturating_add(self.row_height)
5267 .saturating_add(TEXT_GLYPH_ATLAS_PADDING);
5268 self.row_height = 0;
5269 }
5270 if self.cursor_y + height + TEXT_GLYPH_ATLAS_PADDING > self.size {
5271 return None;
5272 }
5273
5274 let entry = GlyphAtlasEntry {
5275 x: self.cursor_x,
5276 y: self.cursor_y,
5277 width,
5278 height,
5279 };
5280 self.cursor_x = self
5281 .cursor_x
5282 .saturating_add(width)
5283 .saturating_add(TEXT_GLYPH_ATLAS_PADDING);
5284 self.row_height = self.row_height.max(height);
5285 Some(entry)
5286 }
5287
5288 fn upload_glyph(
5289 &mut self,
5290 key: SoftwareGlyphAtlasKey,
5291 glyph: &SoftwareGlyphAtlasGlyph,
5292 queue: &wgpu::Queue,
5293 executor: &mut WgpuFrameGraphExecutor,
5294 frame_stats: &mut gpu_stats::FrameStats,
5295 ) -> Option<GlyphAtlasEntry> {
5296 if let Some(entry) = self.entry(&key) {
5297 frame_stats.record_text_glyph_atlas_hit();
5298 return Some(entry);
5299 }
5300
5301 let width = u32::try_from(glyph.mask.width).ok()?;
5302 let height = u32::try_from(glyph.mask.height).ok()?;
5303 let entry = self.allocate(width, height)?;
5304 self.upload_scratch.clear();
5305 self.upload_scratch.reserve(
5306 glyph
5307 .mask
5308 .alpha
5309 .len()
5310 .saturating_sub(self.upload_scratch.capacity()),
5311 );
5312 self.upload_scratch.extend(
5313 glyph
5314 .mask
5315 .alpha
5316 .iter()
5317 .map(|alpha| (alpha.clamp(0.0, 1.0) * 255.0).round() as u8),
5318 );
5319
5320 let upload_stats = executor.upload_texture(
5321 queue,
5322 wgpu::TexelCopyTextureInfo {
5323 texture: &self.texture,
5324 mip_level: 0,
5325 origin: wgpu::Origin3d {
5326 x: entry.x,
5327 y: entry.y,
5328 z: 0,
5329 },
5330 aspect: wgpu::TextureAspect::All,
5331 },
5332 &self.upload_scratch,
5333 wgpu::TexelCopyBufferLayout {
5334 offset: 0,
5335 bytes_per_row: Some(entry.width),
5336 rows_per_image: Some(entry.height),
5337 },
5338 wgpu::Extent3d {
5339 width: entry.width,
5340 height: entry.height,
5341 depth_or_array_layers: 1,
5342 },
5343 );
5344 frame_stats.record_command_stats(upload_stats);
5345 frame_stats.record_text_glyph_atlas_miss(entry.width, entry.height);
5346 self.entries.put(key, entry);
5347 Some(entry)
5348 }
5349}
5350
5351struct ImageDrawCmd {
5352 index_start: u32,
5353 scissor: (u32, u32, u32, u32),
5354 image_id: u64,
5355 sampling: ImageSampling,
5356}
5357
5358#[derive(Clone, Copy)]
5359enum GlyphDrawSource {
5360 Shared {
5361 index_start: u32,
5362 index_count: u32,
5363 },
5364 #[cfg(not(target_arch = "wasm32"))]
5365 Retained {
5366 cache_key: TextGlyphRunCacheKey,
5367 uniform_slot: usize,
5368 },
5369}
5370
5371#[derive(Clone, Copy)]
5372struct GlyphDrawCmd {
5373 source: GlyphDrawSource,
5374 scissor: (u32, u32, u32, u32),
5375}
5376
5377impl GlyphDrawCmd {
5378 fn shared(index_start: u32, index_count: u32, scissor: (u32, u32, u32, u32)) -> Self {
5379 Self {
5380 source: GlyphDrawSource::Shared {
5381 index_start,
5382 index_count,
5383 },
5384 scissor,
5385 }
5386 }
5387
5388 #[cfg(not(target_arch = "wasm32"))]
5389 fn retained(
5390 cache_key: TextGlyphRunCacheKey,
5391 uniform_slot: usize,
5392 scissor: (u32, u32, u32, u32),
5393 ) -> Self {
5394 Self {
5395 source: GlyphDrawSource::Retained {
5396 cache_key,
5397 uniform_slot,
5398 },
5399 scissor,
5400 }
5401 }
5402}
5403
5404#[derive(Clone, Copy, Debug, PartialEq)]
5405struct ImageUvRect {
5406 min: [f32; 2],
5407 max: [f32; 2],
5408 sample_bounds: [f32; 4],
5409}
5410
5411struct ShapeBatchBuffers {
5418 shape_buffer: wgpu::Buffer,
5419 gradient_buffer: wgpu::Buffer,
5420 bind_group: wgpu::BindGroup,
5421 shape_capacity: usize,
5422 gradient_capacity: usize,
5423 batch_limits: ShapeBatchLimits,
5424}
5425
5426#[cfg(target_arch = "wasm32")]
5427struct UniformBatchBuffer {
5428 buffer: wgpu::Buffer,
5429 bind_group: wgpu::BindGroup,
5430}
5431
5432#[cfg(target_arch = "wasm32")]
5433struct ImageBatchBuffers {
5434 vertex_buffer: wgpu::Buffer,
5435 index_buffer: wgpu::Buffer,
5436 vertex_capacity: usize,
5437 index_capacity: usize,
5438}
5439
5440#[derive(Clone, Copy, Debug, PartialEq)]
5441struct ViewportUniformParams {
5442 width: u32,
5443 height: u32,
5444 offset: [f32; 2],
5445}
5446
5447#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5448#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
5449enum UploadTarget {
5450 Uniform,
5451 ShapeData,
5452 ShapeGradient,
5453 ImageVertex,
5454 ImageIndex,
5455 #[cfg(not(target_arch = "wasm32"))]
5456 RetainedGlyphUniform,
5457 #[cfg(not(target_arch = "wasm32"))]
5460 ReplayTransform,
5461 #[cfg(not(target_arch = "wasm32"))]
5463 ReplayPaintData(u32),
5464}
5465
5466#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5467#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
5468struct PendingBufferCopy {
5469 source_offset: u64,
5470 target_offset: u64,
5471 size: u64,
5472 target: UploadTarget,
5473}
5474
5475#[derive(Default)]
5476struct StagedBufferUploads {
5477 bytes: Vec<u8>,
5478 copies: Vec<PendingBufferCopy>,
5479}
5480
5481impl StagedBufferUploads {
5482 fn clear(&mut self) {
5483 self.bytes.clear();
5484 self.copies.clear();
5485 }
5486
5487 fn shrink_retained_capacity(&mut self, max_bytes: usize, max_copies: usize) -> bool {
5488 let mut shrunk = false;
5489 if self.bytes.len() <= max_bytes && self.bytes.capacity() > max_bytes {
5490 self.bytes.shrink_to(max_bytes);
5491 shrunk = true;
5492 }
5493 if self.copies.len() <= max_copies && self.copies.capacity() > max_copies {
5494 self.copies.shrink_to(max_copies);
5495 shrunk = true;
5496 }
5497 shrunk
5498 }
5499
5500 fn is_empty(&self) -> bool {
5501 self.copies.is_empty()
5502 }
5503
5504 #[cfg(test)]
5505 fn payload_for_copy(&self, copy: PendingBufferCopy) -> &[u8] {
5506 let start = copy.source_offset as usize;
5507 let end = start + copy.size as usize;
5508 &self.bytes[start..end]
5509 }
5510
5511 #[cfg(not(target_arch = "wasm32"))]
5512 fn stage(&mut self, target: UploadTarget, bytes: &[u8]) {
5513 self.stage_at(target, 0, bytes);
5514 }
5515
5516 #[cfg(not(target_arch = "wasm32"))]
5521 fn record_upload_copy(
5522 &mut self,
5523 target: UploadTarget,
5524 source_offset: u64,
5525 target_offset: u64,
5526 size: u64,
5527 ) {
5528 if size == 0 {
5529 return;
5530 }
5531 self.copies.push(PendingBufferCopy {
5532 source_offset,
5533 target_offset,
5534 size,
5535 target,
5536 });
5537 }
5538
5539 #[cfg(not(target_arch = "wasm32"))]
5540 fn stage_at(&mut self, target: UploadTarget, target_offset: u64, bytes: &[u8]) {
5541 if bytes.is_empty() {
5542 return;
5543 }
5544
5545 debug_assert_eq!(
5546 bytes.len() % wgpu::COPY_BUFFER_ALIGNMENT as usize,
5547 0,
5548 "buffer uploads must be aligned to copy requirements"
5549 );
5550
5551 let aligned_offset = align_usize_to(self.bytes.len(), wgpu::COPY_BUFFER_ALIGNMENT as usize);
5552 if aligned_offset > self.bytes.len() {
5553 self.bytes.resize(aligned_offset, 0);
5554 }
5555
5556 let source_offset = self.bytes.len() as u64;
5557 self.bytes.extend_from_slice(bytes);
5558 self.copies.push(PendingBufferCopy {
5559 source_offset,
5560 target_offset,
5561 size: bytes.len() as u64,
5562 target,
5563 });
5564 }
5565
5566 fn truncate(&mut self, bytes_len: usize, copies_len: usize) {
5567 self.bytes.truncate(bytes_len);
5568 self.copies.truncate(copies_len);
5569 }
5570}
5571
5572fn shape_batch_bind_group_entries<'a>(
5577 shape_buffer: &'a wgpu::Buffer,
5578 gradient_buffer: &'a wgpu::Buffer,
5579 similarity_buffer: &'a wgpu::Buffer,
5580 paint_buffer: Option<&'a wgpu::Buffer>,
5581) -> Vec<wgpu::BindGroupEntry<'a>> {
5582 let mut entries = vec![
5583 wgpu::BindGroupEntry {
5584 binding: 0,
5585 resource: shape_buffer.as_entire_binding(),
5586 },
5587 wgpu::BindGroupEntry {
5588 binding: 1,
5589 resource: gradient_buffer.as_entire_binding(),
5590 },
5591 wgpu::BindGroupEntry {
5592 binding: 2,
5593 resource: similarity_buffer.as_entire_binding(),
5594 },
5595 ];
5596 if let Some(paint_buffer) = paint_buffer {
5597 entries.push(wgpu::BindGroupEntry {
5598 binding: 3,
5599 resource: paint_buffer.as_entire_binding(),
5600 });
5601 }
5602 entries
5603}
5604
5605impl ShapeBatchBuffers {
5606 fn new(
5607 device: &wgpu::Device,
5608 bind_group_layout: &wgpu::BindGroupLayout,
5609 similarity_buffer: &wgpu::Buffer,
5610 paint_buffer: Option<&wgpu::Buffer>,
5611 batch_limits: ShapeBatchLimits,
5612 ) -> Self {
5613 debug_assert_eq!(
5614 paint_buffer.is_some(),
5615 batch_limits.storage,
5616 "the paint binding exists exactly when the layout is in storage mode"
5617 );
5618 let initial_shape_cap = batch_limits.initial_shape_capacity();
5619 let initial_gradient_cap = batch_limits.initial_gradient_capacity();
5620
5621 let shape_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5622 label: Some("Shape Data Buffer"),
5623 size: (std::mem::size_of::<ShapeData>() * initial_shape_cap) as u64,
5624 usage: batch_limits.data_buffer_usage(),
5625 mapped_at_creation: false,
5626 });
5627
5628 let gradient_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5629 label: Some("Gradient Buffer"),
5630 size: (std::mem::size_of::<GradientStop>() * initial_gradient_cap) as u64,
5631 usage: batch_limits.data_buffer_usage(),
5632 mapped_at_creation: false,
5633 });
5634
5635 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
5636 label: Some("Shape Bind Group"),
5637 layout: bind_group_layout,
5638 entries: &shape_batch_bind_group_entries(
5639 &shape_buffer,
5640 &gradient_buffer,
5641 similarity_buffer,
5642 paint_buffer,
5643 ),
5644 });
5645
5646 Self {
5647 shape_buffer,
5648 gradient_buffer,
5649 bind_group,
5650 shape_capacity: initial_shape_cap,
5651 gradient_capacity: initial_gradient_cap,
5652 batch_limits,
5653 }
5654 }
5655
5656 fn ensure_capacity(
5659 &mut self,
5660 device: &wgpu::Device,
5661 bind_group_layout: &wgpu::BindGroupLayout,
5662 similarity_buffer: &wgpu::Buffer,
5663 paint_buffer: Option<&wgpu::Buffer>,
5664 shapes_needed: usize,
5665 gradients_needed: usize,
5666 ) {
5667 let mut need_bind_group_update = false;
5668
5669 if shapes_needed > self.shape_capacity
5673 && self.shape_capacity < self.batch_limits.max_shapes_per_batch
5674 {
5675 let new_cap = shapes_needed
5676 .next_power_of_two()
5677 .min(self.batch_limits.max_shapes_per_batch);
5678 self.shape_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5679 label: Some("Shape Data Buffer"),
5680 size: (std::mem::size_of::<ShapeData>() * new_cap) as u64,
5681 usage: self.batch_limits.data_buffer_usage(),
5682 mapped_at_creation: false,
5683 });
5684 self.shape_capacity = new_cap;
5685 need_bind_group_update = true;
5686 }
5687
5688 if gradients_needed > self.gradient_capacity
5689 && self.gradient_capacity < self.batch_limits.max_gradient_stops
5690 {
5691 let new_cap = gradients_needed
5692 .max(1)
5693 .next_power_of_two()
5694 .min(self.batch_limits.max_gradient_stops);
5695 self.gradient_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5696 label: Some("Gradient Buffer"),
5697 size: (std::mem::size_of::<GradientStop>() * new_cap) as u64,
5698 usage: self.batch_limits.data_buffer_usage(),
5699 mapped_at_creation: false,
5700 });
5701 self.gradient_capacity = new_cap;
5702 need_bind_group_update = true;
5703 }
5704
5705 if need_bind_group_update {
5706 self.bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
5707 label: Some("Shape Bind Group"),
5708 layout: bind_group_layout,
5709 entries: &shape_batch_bind_group_entries(
5710 &self.shape_buffer,
5711 &self.gradient_buffer,
5712 similarity_buffer,
5713 paint_buffer,
5714 ),
5715 });
5716 }
5717 }
5718}
5719
5720#[cfg(target_arch = "wasm32")]
5721impl UniformBatchBuffer {
5722 fn new(device: &wgpu::Device, bind_group_layout: &wgpu::BindGroupLayout) -> Self {
5723 let buffer = device.create_buffer(&wgpu::BufferDescriptor {
5724 label: Some("Viewport Uniform Batch Buffer"),
5725 size: std::mem::size_of::<Uniforms>() as u64,
5726 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
5727 mapped_at_creation: false,
5728 });
5729 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
5730 label: Some("Viewport Uniform Batch Bind Group"),
5731 layout: bind_group_layout,
5732 entries: &[wgpu::BindGroupEntry {
5733 binding: 0,
5734 resource: buffer.as_entire_binding(),
5735 }],
5736 });
5737 Self { buffer, bind_group }
5738 }
5739}
5740
5741#[cfg(target_arch = "wasm32")]
5742impl ImageBatchBuffers {
5743 fn new(device: &wgpu::Device) -> Self {
5744 let vertex_capacity = 4;
5745 let index_capacity = 6;
5746 let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5747 label: Some("Image Vertex Batch Buffer"),
5748 size: (std::mem::size_of::<Vertex>() * vertex_capacity) as u64,
5749 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
5750 mapped_at_creation: false,
5751 });
5752 let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5753 label: Some("Image Index Batch Buffer"),
5754 size: (std::mem::size_of::<u32>() * index_capacity) as u64,
5755 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
5756 mapped_at_creation: false,
5757 });
5758 Self {
5759 vertex_buffer,
5760 index_buffer,
5761 vertex_capacity,
5762 index_capacity,
5763 }
5764 }
5765
5766 fn ensure_capacity(
5767 &mut self,
5768 device: &wgpu::Device,
5769 vertices_needed: usize,
5770 indices_needed: usize,
5771 ) {
5772 let hard_max_bytes = HARD_MAX_BUFFER_MB * 1024 * 1024;
5773 if vertices_needed > self.vertex_capacity {
5774 let desired = vertices_needed.next_power_of_two();
5775 let max_count = hard_max_bytes / std::mem::size_of::<Vertex>();
5776 let new_cap = desired.min(max_count);
5777 self.vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5778 label: Some("Image Vertex Batch Buffer"),
5779 size: (std::mem::size_of::<Vertex>() * new_cap) as u64,
5780 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
5781 mapped_at_creation: false,
5782 });
5783 self.vertex_capacity = new_cap;
5784 }
5785 if indices_needed > self.index_capacity {
5786 let desired = indices_needed.next_power_of_two();
5787 let max_count = hard_max_bytes / std::mem::size_of::<u32>();
5788 let new_cap = desired.min(max_count);
5789 self.index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5790 label: Some("Image Index Batch Buffer"),
5791 size: (std::mem::size_of::<u32>() * new_cap) as u64,
5792 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
5793 mapped_at_creation: false,
5794 });
5795 self.index_capacity = new_cap;
5796 }
5797 }
5798}
5799
5800struct CompositionTarget {
5803 target: OffscreenTarget,
5804 output_bind_group: wgpu::BindGroup,
5805}
5806
5807#[derive(Clone, Copy)]
5808enum OutputMode {
5809 Display,
5810 Screenshot,
5811}
5812
5813pub struct GpuRenderer {
5814 pub(crate) device: Arc<wgpu::Device>,
5815 pub(crate) queue: Arc<wgpu::Queue>,
5816 device_errors: Arc<DeviceErrorSentry>,
5822 renderer_epoch: u64,
5827 #[cfg(not(target_arch = "wasm32"))]
5833 store_feed_generation: u64,
5834 composition_format: wgpu::TextureFormat,
5835 #[cfg(not(target_arch = "wasm32"))]
5836 display_format: wgpu::TextureFormat,
5837 composition_target: Option<CompositionTarget>,
5838 output_converter: OutputConverter,
5839 screenshot_converter: OutputConverter,
5840 adapter_backend: wgpu::Backend,
5841 shape_batch_limits: ShapeBatchLimits,
5842 pipeline_cache: Option<wgpu::PipelineCache>,
5848 pipeline: PassPipeline,
5849 pipeline_dst_out: PassPipeline,
5850 pipeline_solid: PassPipeline,
5852 #[cfg(not(target_arch = "wasm32"))]
5856 mesh_pipeline: PassPipeline,
5857 #[cfg(not(target_arch = "wasm32"))]
5863 instanced_quads: Option<InstancedQuadPipelines>,
5864 #[cfg(not(target_arch = "wasm32"))]
5865 segment_capture_pipelines: SegmentCapturePipelines,
5866 uniform_bind_group_layout: wgpu::BindGroupLayout,
5867 shape_bind_group_layout: wgpu::BindGroupLayout,
5868 dummy_paint_buffer: Option<wgpu::Buffer>,
5871 identity_similarity_buffer: wgpu::Buffer,
5874 #[cfg(not(target_arch = "wasm32"))]
5875 replay_slots: ReplaySlotStore,
5876 image_pipeline: PassPipeline,
5877 image_pipeline_dst_out: PassPipeline,
5878 glyph_atlas_pipeline: PassPipeline,
5879 #[cfg(not(target_arch = "wasm32"))]
5880 retained_glyph_atlas_pipeline: PassPipeline,
5881 image_bind_group_layout: wgpu::BindGroupLayout,
5882 #[cfg(not(target_arch = "wasm32"))]
5883 retained_glyph_uniform_bind_group_layout: wgpu::BindGroupLayout,
5884 image_nearest_sampler: wgpu::Sampler,
5885 image_linear_sampler: wgpu::Sampler,
5886 text_fonts: SoftwareTextFontSet,
5887 #[cfg(not(target_arch = "wasm32"))]
5889 upload_buffer: wgpu::Buffer,
5890 #[cfg(not(target_arch = "wasm32"))]
5891 uniform_buffer: wgpu::Buffer,
5892 #[cfg(not(target_arch = "wasm32"))]
5893 uniform_bind_group: wgpu::BindGroup,
5894 #[cfg(not(target_arch = "wasm32"))]
5895 shape_buffers: ShapeBatchBuffers,
5896 #[cfg(not(target_arch = "wasm32"))]
5897 image_vertex_buffer: wgpu::Buffer,
5898 #[cfg(not(target_arch = "wasm32"))]
5899 image_index_buffer: wgpu::Buffer,
5900 #[cfg(not(target_arch = "wasm32"))]
5901 retained_glyph_uniform_buffer: wgpu::Buffer,
5902 #[cfg(not(target_arch = "wasm32"))]
5903 retained_glyph_uniform_bind_group: wgpu::BindGroup,
5904 #[cfg(not(target_arch = "wasm32"))]
5905 retained_glyph_uniform_stride: u64,
5906 #[cfg(not(target_arch = "wasm32"))]
5907 retained_glyph_uniform_capacity: usize,
5908 #[cfg(not(target_arch = "wasm32"))]
5909 retained_glyph_uniform_cursor: usize,
5910 #[cfg(target_arch = "wasm32")]
5911 wasm_uniform_batches: Vec<UniformBatchBuffer>,
5912 #[cfg(target_arch = "wasm32")]
5913 wasm_uniform_batch_cursor: usize,
5914 #[cfg(target_arch = "wasm32")]
5915 wasm_shape_batches: Vec<ShapeBatchBuffers>,
5916 #[cfg(target_arch = "wasm32")]
5917 wasm_shape_batch_cursor: usize,
5918 #[cfg(target_arch = "wasm32")]
5919 wasm_image_batches: Vec<ImageBatchBuffers>,
5920 #[cfg(target_arch = "wasm32")]
5921 wasm_image_batch_cursor: usize,
5922 image_texture_cache: BoundedLruCache<u64, CachedImageTexture>,
5923 image_texture_cache_bytes: usize,
5925 text_image_cache: BoundedLruCache<TextImageCacheKey, CachedTextImage>,
5926 text_glyph_atlas: TextGlyphAtlas,
5927 text_glyph_run_cache: BoundedLruCache<TextGlyphRunCacheKey, CachedTextGlyphRun>,
5928 #[cfg(not(target_arch = "wasm32"))]
5929 text_glyph_gpu_run_cache: BoundedLruCache<TextGlyphRunCacheKey, CachedGpuTextGlyphRun>,
5930 text_glyph_mask_cache: SoftwareGlyphRasterCache,
5931 text_line_index_cache: TextLineIndexCache,
5932 scratch_shape_data: Vec<ShapeData>,
5933 scratch_gradients: Vec<GradientStop>,
5934 scratch_image_vertices: Vec<Vertex>,
5935 scratch_image_indices: Vec<u32>,
5936 scratch_image_cmds: Vec<ImageDrawCmd>,
5937 scratch_glyph_cmds: Vec<GlyphDrawCmd>,
5938 scratch_text_glyph_run: Vec<SoftwareGlyphAtlasRunGlyph>,
5939 scratch_text_glyph_placements: Vec<SoftwareGlyphAtlasPlacement>,
5940 scratch_text_glyph_quads: Vec<CachedTextGlyphQuad>,
5941 scratch_segment_items: Vec<(usize, SegmentDrawItem)>,
5942 scratch_effect_ranges: Vec<Range<usize>>,
5943 scratch_layer_events: Vec<LayerEvent>,
5944 staged_uploads: StagedBufferUploads,
5945 frame_graph_executor: WgpuFrameGraphExecutor,
5946 deferred_offscreen_releases: Vec<OffscreenTarget>,
5947 effect_renderer: EffectRenderer,
5948 layer_surface_cache: LayerSurfaceCache,
5949 observed_scene_range_cache_misses: BoundedLruCache<LayerRasterCacheKey, ()>,
5950 shadow_surface_cache: BoundedLruCache<ShadowSurfaceCacheKey, CachedShadowSurface>,
5951 shadow_surface_cache_bytes: u64,
5952 frame_stats: gpu_stats::FrameStats,
5953 last_frame_stats: Option<gpu_stats::FrameStatsSnapshot>,
5954 pending_frame_warmup_frames: u8,
5955 frame_count: u64,
5956 gpu_stats_enabled: bool,
5957 warning_state: RendererWarningState,
5958 #[cfg(not(target_arch = "wasm32"))]
5959 replay_upload_stats: ReplayUploadStats,
5960 #[cfg(not(target_arch = "wasm32"))]
5961 segment_encode_stats: SegmentEncodeStats,
5962 #[cfg(not(target_arch = "wasm32"))]
5969 replay_color_patches: Vec<crate::scene::ColorPatch>,
5970 #[cfg(not(target_arch = "wasm32"))]
5974 color_patch_scratch: Vec<crate::scene::ColorPatch>,
5975 #[cfg(not(target_arch = "wasm32"))]
5981 replay_capture_shape_scratch: Vec<ShapeData>,
5982 #[cfg(not(target_arch = "wasm32"))]
5984 replay_capture_gradient_scratch: Vec<GradientStop>,
5985 #[cfg(not(target_arch = "wasm32"))]
5990 replay_ack_confirmations: Vec<crate::frame_packet::ReplayConfirmation>,
5991 #[cfg(not(target_arch = "wasm32"))]
5996 replay_generation_drops: u64,
5997 #[cfg(not(target_arch = "wasm32"))]
6000 retained_bundle_cache: RetainedBundleCache,
6001 #[cfg(not(target_arch = "wasm32"))]
6006 rim_mesh_vertices: Vec<MeshVertex>,
6007 #[cfg(not(target_arch = "wasm32"))]
6008 rim_mesh_indices: Vec<u32>,
6009 #[cfg(not(target_arch = "wasm32"))]
6015 rim_mesh_vertex_buffer: Option<wgpu::Buffer>,
6016 #[cfg(not(target_arch = "wasm32"))]
6017 rim_mesh_index_buffer: Option<wgpu::Buffer>,
6018 #[cfg(not(target_arch = "wasm32"))]
6021 rim_mesh_uploaded_vertices: usize,
6022 #[cfg(not(target_arch = "wasm32"))]
6023 rim_mesh_uploaded_indices: usize,
6024 #[cfg(not(target_arch = "wasm32"))]
6027 rim_meshes_emitted: u64,
6028 #[cfg(not(target_arch = "wasm32"))]
6031 fill_area_diag: FillAreaDiag,
6032 #[cfg(not(target_arch = "wasm32"))]
6036 static_span: StaticSpanCache,
6037 #[cfg(not(target_arch = "wasm32"))]
6042 segment_surfaces: SegmentSurfaceCache,
6043 #[cfg(not(target_arch = "wasm32"))]
6048 display_clip: DisplayClipState,
6049}
6050
6051#[cfg(not(target_arch = "wasm32"))]
6054type DisplayClipResourceKey = ((u32, u32), DisplayVisibleRegion);
6055
6056#[cfg(not(target_arch = "wasm32"))]
6058struct DisplayClipState {
6059 visible_region: DisplayVisibleRegion,
6065 frame_root_view: Option<wgpu::TextureView>,
6071 pass_depth: Cell<bool>,
6076 resources: Option<(DisplayClipResourceKey, Option<DisplayClipResources>)>,
6081 occluder_pipeline: LazyGpuResource<wgpu::RenderPipeline>,
6082}
6083
6084#[cfg(not(target_arch = "wasm32"))]
6085impl DisplayClipState {
6086 fn new() -> Self {
6087 Self {
6088 visible_region: DisplayVisibleRegion::Full,
6089 frame_root_view: None,
6090 pass_depth: Cell::new(false),
6091 resources: None,
6092 occluder_pipeline: LazyGpuResource::new("display-clip/occluder"),
6093 }
6094 }
6095}
6096
6097#[cfg(not(target_arch = "wasm32"))]
6099struct DisplayClipResources {
6100 depth_view: wgpu::TextureView,
6103 occluder_vertex_buffer: wgpu::Buffer,
6106 occluder_vertex_count: u32,
6107}
6108
6109#[cfg(not(target_arch = "wasm32"))]
6115#[derive(Default)]
6116struct ReplayUploadStats {
6117 calls: u64,
6118 patched_calls: u64,
6119 patches: u64,
6120 slots: u64,
6121 records: u64,
6122 bytes: u64,
6123 ideal_bytes: u64,
6124 max_frame_bytes: u64,
6125}
6126
6127#[cfg(not(target_arch = "wasm32"))]
6128impl ReplayUploadStats {
6129 const REPORT_CALLS: u64 = 1024;
6139
6140 fn note_frame(&mut self, patches: u64, slots: u64, records: u64, bytes: u64, ideal: u64) {
6141 self.calls += 1;
6142 if patches > 0 {
6143 self.patched_calls += 1;
6144 self.patches += patches;
6145 self.slots += slots;
6146 self.records += records;
6147 self.bytes += bytes;
6148 self.ideal_bytes += ideal;
6149 self.max_frame_bytes = self.max_frame_bytes.max(bytes);
6150 }
6151 if self.calls >= Self::REPORT_CALLS {
6152 let patched = self.patched_calls.max(1);
6153 log::warn!(
6154 "[replay-upload] {} patched of {} drains: avg {:.1} KB/frame (max {:.1} KB), \
6155 color-only would be {:.1} KB/frame; avg {} patches over {} records in {} slots",
6156 self.patched_calls,
6157 self.calls,
6158 self.bytes as f64 / patched as f64 / 1024.0,
6159 self.max_frame_bytes as f64 / 1024.0,
6160 self.ideal_bytes as f64 / patched as f64 / 1024.0,
6161 self.patches / patched,
6162 self.records / patched,
6163 self.slots / patched,
6164 );
6165 *self = Self::default();
6166 }
6167 }
6168}
6169
6170#[cfg(not(target_arch = "wasm32"))]
6177#[derive(Default)]
6178struct SegmentEncodeStats {
6179 calls: u64,
6180 partitions: u64,
6181 max_partitions: u64,
6182 encode_micros: u64,
6183 max_call_micros: u64,
6184}
6185
6186#[cfg(not(target_arch = "wasm32"))]
6187impl SegmentEncodeStats {
6188 const REPORT_CALLS: u64 = 1024;
6189
6190 fn note_call(&mut self, partitions: u64, micros: u64) {
6191 self.calls += 1;
6192 self.partitions += partitions;
6193 self.max_partitions = self.max_partitions.max(partitions);
6194 self.encode_micros += micros;
6195 self.max_call_micros = self.max_call_micros.max(micros);
6196 if self.calls >= Self::REPORT_CALLS {
6197 log::warn!(
6198 "[segment-encode] {} chunks: avg {:.1} partitions (max {}), \
6199 avg {:.2} ms encode (max {:.2})",
6200 self.calls,
6201 self.partitions as f64 / self.calls as f64,
6202 self.max_partitions,
6203 self.encode_micros as f64 / self.calls as f64 / 1000.0,
6204 self.max_call_micros as f64 / 1000.0,
6205 );
6206 *self = Self::default();
6207 }
6208 }
6209}
6210
6211fn image_sampler_descriptor(sampling: ImageSampling) -> wgpu::SamplerDescriptor<'static> {
6212 let filter = match sampling {
6213 ImageSampling::Nearest => wgpu::FilterMode::Nearest,
6214 ImageSampling::Linear => wgpu::FilterMode::Linear,
6215 };
6216 wgpu::SamplerDescriptor {
6217 label: Some(match sampling {
6218 ImageSampling::Nearest => "Nearest Image Sampler",
6219 ImageSampling::Linear => "Linear Image Sampler",
6220 }),
6221 address_mode_u: wgpu::AddressMode::ClampToEdge,
6222 address_mode_v: wgpu::AddressMode::ClampToEdge,
6223 address_mode_w: wgpu::AddressMode::ClampToEdge,
6224 mag_filter: filter,
6225 min_filter: filter,
6226 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
6227 ..Default::default()
6228 }
6229}
6230
6231#[cfg(test)]
6232fn layer_raster_cache_candidate(
6233 layer: &LayerNode,
6234 root_scale: f32,
6235 has_backdrop_underlay: bool,
6236 allow_runtime_cache: bool,
6237) -> Option<(LayerRasterCacheKey, Rect)> {
6238 let mut layer_surface_requirements_cache = HashMap::new();
6239 let surface_requirements =
6240 layer_surface_requirements_cached(layer, &mut layer_surface_requirements_cache);
6241 let runtime_cache_is_safe = allow_runtime_cache
6242 && surface_requirements
6243 .surface_requirements
6244 .has_isolating_requirement()
6245 && !surface_requirements.contains_runtime_shader;
6246 let cache_is_allowed = layer.cache_policy == CachePolicy::Auto
6247 || (allow_runtime_cache && surface_requirements.has_renderer_forced_surface())
6248 || runtime_cache_is_safe;
6249 if !cache_is_allowed {
6250 return None;
6251 }
6252 if layer_uses_external_backdrop_input(layer, has_backdrop_underlay) {
6253 return None;
6254 }
6255 if surface_requirements.contains_runtime_shader {
6258 return None;
6259 }
6260
6261 let logical_rect = estimate_layer_surface_rect(layer);
6262 let pixel_size = surface_target_size(logical_rect, root_scale, u32::MAX);
6263 Some((
6264 LayerRasterCacheKey::new(
6265 layer.node_id,
6266 layer.target_content_hash(),
6267 layer.effect_hash(),
6268 logical_rect,
6269 pixel_size,
6270 ScaleBucket::from_scale(root_scale),
6271 ),
6272 logical_rect,
6273 ))
6274}
6275
6276impl GpuRenderer {
6277 #[allow(clippy::too_many_arguments)]
6278 pub fn new(
6279 device: Arc<wgpu::Device>,
6280 queue: Arc<wgpu::Queue>,
6281 surface_format: wgpu::TextureFormat,
6282 adapter_backend: wgpu::Backend,
6283 adapter_downlevel: wgpu::DownlevelFlags,
6289 text_fonts: SoftwareTextFontSet,
6290 renderer_epoch: u64,
6291 store_feed_generation: u64,
6292 ) -> Self {
6293 #[cfg(target_arch = "wasm32")]
6294 let _ = store_feed_generation;
6295 let display_format = surface_format;
6296 let composition_format = COMPOSITION_FORMAT;
6297 let construction_started = Instant::now();
6305 let device_errors = Arc::new(DeviceErrorSentry::default());
6311 if survive_gpu_errors_enabled() {
6312 let sentry = Arc::clone(&device_errors);
6313 device.on_uncaptured_error(Arc::new(move |error| sentry.record(&error)));
6314 }
6315 let shape_batch_limits = ShapeBatchLimits::for_device(&device, adapter_downlevel);
6316 let uniform_bind_group_layout =
6317 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
6318 label: Some("Uniform Bind Group Layout"),
6319 entries: &[wgpu::BindGroupLayoutEntry {
6320 binding: 0,
6321 visibility: wgpu::ShaderStages::VERTEX,
6322 ty: wgpu::BindingType::Buffer {
6323 ty: wgpu::BufferBindingType::Uniform,
6324 has_dynamic_offset: false,
6325 min_binding_size: None,
6326 },
6327 count: None,
6328 }],
6329 });
6330 #[cfg(not(target_arch = "wasm32"))]
6331 let retained_glyph_uniform_bind_group_layout =
6332 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
6333 label: Some("Retained Glyph Dynamic Uniform Bind Group Layout"),
6334 entries: &[wgpu::BindGroupLayoutEntry {
6335 binding: 0,
6336 visibility: wgpu::ShaderStages::VERTEX,
6337 ty: wgpu::BindingType::Buffer {
6338 ty: wgpu::BufferBindingType::Uniform,
6339 has_dynamic_offset: true,
6340 min_binding_size: wgpu::BufferSize::new(
6341 std::mem::size_of::<Uniforms>() as u64
6342 ),
6343 },
6344 count: None,
6345 }],
6346 });
6347
6348 let mut shape_bind_group_layout_entries = vec![
6358 wgpu::BindGroupLayoutEntry {
6359 binding: 0,
6360 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
6361 ty: wgpu::BindingType::Buffer {
6362 ty: shape_batch_limits.data_binding_type(),
6363 has_dynamic_offset: false,
6364 min_binding_size: None,
6365 },
6366 count: None,
6367 },
6368 wgpu::BindGroupLayoutEntry {
6369 binding: 1,
6370 visibility: wgpu::ShaderStages::FRAGMENT,
6371 ty: wgpu::BindingType::Buffer {
6372 ty: shape_batch_limits.data_binding_type(),
6373 has_dynamic_offset: false,
6374 min_binding_size: None,
6375 },
6376 count: None,
6377 },
6378 wgpu::BindGroupLayoutEntry {
6383 binding: 2,
6384 visibility: wgpu::ShaderStages::VERTEX,
6385 ty: wgpu::BindingType::Buffer {
6386 ty: wgpu::BufferBindingType::Uniform,
6387 has_dynamic_offset: true,
6388 min_binding_size: wgpu::BufferSize::new(
6389 std::mem::size_of::<SimilarityTransform>() as u64,
6390 ),
6391 },
6392 count: None,
6393 },
6394 ];
6395 if shape_batch_limits.storage {
6401 shape_bind_group_layout_entries.push(wgpu::BindGroupLayoutEntry {
6402 binding: 3,
6403 visibility: wgpu::ShaderStages::VERTEX,
6404 ty: wgpu::BindingType::Buffer {
6405 ty: wgpu::BufferBindingType::Storage { read_only: true },
6406 has_dynamic_offset: false,
6407 min_binding_size: None,
6408 },
6409 count: None,
6410 });
6411 }
6412 let shape_bind_group_layout =
6413 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
6414 label: Some("Shape Bind Group Layout"),
6415 entries: &shape_bind_group_layout_entries,
6416 });
6417
6418 let identity_similarity_buffer = device.create_buffer(&wgpu::BufferDescriptor {
6419 label: Some("Identity Similarity Buffer"),
6420 size: std::mem::size_of::<SimilarityTransform>() as u64,
6421 usage: wgpu::BufferUsages::UNIFORM,
6422 mapped_at_creation: true,
6423 });
6424 identity_similarity_buffer
6425 .slice(..)
6426 .get_mapped_range_mut()
6427 .copy_from_slice(bytemuck::bytes_of(&SimilarityTransform::IDENTITY));
6428 identity_similarity_buffer.unmap();
6429
6430 let dummy_paint_buffer = shape_batch_limits.storage.then(|| {
6435 device.create_buffer(&wgpu::BufferDescriptor {
6436 label: Some("Dummy Paint Buffer"),
6437 size: std::mem::size_of::<[f32; 4]>() as u64,
6438 usage: wgpu::BufferUsages::STORAGE,
6439 mapped_at_creation: false,
6440 })
6441 });
6442 #[cfg(not(target_arch = "wasm32"))]
6443 let replay_slot_store = ReplaySlotStore::new(&device);
6444
6445 let pipeline = PassPipeline::new("shape/src-over", "shape/src-over-depth");
6446 let pipeline_dst_out = PassPipeline::new("shape/dst-out", "shape/dst-out-depth");
6447 let pipeline_solid =
6448 PassPipeline::new("shape/solid-src-over", "shape/solid-src-over-depth");
6449 #[cfg(not(target_arch = "wasm32"))]
6450 let mesh_pipeline = PassPipeline::new("shape/mesh", "shape/mesh-depth");
6451 #[cfg(not(target_arch = "wasm32"))]
6457 let instanced_quads =
6458 (shape_batch_limits.storage && instanced_quads_enabled()).then(|| {
6459 let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
6460 label: Some("Instanced Quad Index Buffer"),
6461 size: std::mem::size_of_val(&INSTANCED_QUAD_INDICES) as u64,
6462 usage: wgpu::BufferUsages::INDEX,
6463 mapped_at_creation: true,
6464 });
6465 index_buffer
6466 .slice(..)
6467 .get_mapped_range_mut()
6468 .copy_from_slice(bytemuck::cast_slice(&INSTANCED_QUAD_INDICES));
6469 index_buffer.unmap();
6470 InstancedQuadPipelines {
6471 pipeline: PassPipeline::new(
6472 "shape/instanced-src-over",
6473 "shape/instanced-src-over-depth",
6474 ),
6475 pipeline_dst_out: PassPipeline::new(
6476 "shape/instanced-dst-out",
6477 "shape/instanced-dst-out-depth",
6478 ),
6479 pipeline_solid: PassPipeline::new(
6480 "shape/instanced-solid",
6481 "shape/instanced-solid-depth",
6482 ),
6483 index_buffer,
6484 }
6485 });
6486 #[cfg(not(target_arch = "wasm32"))]
6487 let segment_capture_pipelines = SegmentCapturePipelines {
6488 expanded: PassPipeline::new("segment/expanded", "segment/expanded-depth"),
6489 expanded_solid: PassPipeline::new(
6490 "segment/expanded-solid",
6491 "segment/expanded-solid-depth",
6492 ),
6493 mesh: PassPipeline::new("segment/mesh", "segment/mesh-depth"),
6494 instanced: PassPipeline::new("segment/instanced", "segment/instanced-depth"),
6495 instanced_solid: PassPipeline::new(
6496 "segment/instanced-solid",
6497 "segment/instanced-solid-depth",
6498 ),
6499 };
6500
6501 let image_bind_group_layout =
6502 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
6503 label: Some("Image Texture Bind Group Layout"),
6504 entries: &[
6505 wgpu::BindGroupLayoutEntry {
6506 binding: 0,
6507 visibility: wgpu::ShaderStages::FRAGMENT,
6508 ty: wgpu::BindingType::Texture {
6509 multisampled: false,
6510 view_dimension: wgpu::TextureViewDimension::D2,
6511 sample_type: wgpu::TextureSampleType::Float { filterable: true },
6512 },
6513 count: None,
6514 },
6515 wgpu::BindGroupLayoutEntry {
6516 binding: 1,
6517 visibility: wgpu::ShaderStages::FRAGMENT,
6518 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
6519 count: None,
6520 },
6521 ],
6522 });
6523
6524 let image_pipeline = PassPipeline::new("image/src-over", "image/src-over-depth");
6525 let image_pipeline_dst_out = PassPipeline::new("image/dst-out", "image/dst-out-depth");
6526 let glyph_atlas_pipeline = PassPipeline::new("glyph/shared", "glyph/shared-depth");
6527 #[cfg(not(target_arch = "wasm32"))]
6528 let retained_glyph_atlas_pipeline =
6529 PassPipeline::new("glyph/retained", "glyph/retained-depth");
6530
6531 #[cfg(not(target_arch = "wasm32"))]
6532 let upload_buffer = device.create_buffer(&wgpu::BufferDescriptor {
6533 label: Some("Frame Upload Buffer"),
6534 size: INITIAL_UPLOAD_BUFFER_BYTES,
6535 usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
6536 mapped_at_creation: false,
6537 });
6538
6539 #[cfg(not(target_arch = "wasm32"))]
6540 let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
6541 label: Some("Uniform Buffer"),
6542 size: std::mem::size_of::<Uniforms>() as u64,
6543 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
6544 mapped_at_creation: false,
6545 });
6546
6547 #[cfg(not(target_arch = "wasm32"))]
6548 let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
6549 label: Some("Uniform Bind Group"),
6550 layout: &uniform_bind_group_layout,
6551 entries: &[wgpu::BindGroupEntry {
6552 binding: 0,
6553 resource: uniform_buffer.as_entire_binding(),
6554 }],
6555 });
6556
6557 #[cfg(not(target_arch = "wasm32"))]
6558 let shape_buffers = ShapeBatchBuffers::new(
6559 &device,
6560 &shape_bind_group_layout,
6561 &identity_similarity_buffer,
6562 dummy_paint_buffer.as_ref(),
6563 shape_batch_limits,
6564 );
6565
6566 let image_nearest_sampler =
6567 device.create_sampler(&image_sampler_descriptor(ImageSampling::Nearest));
6568 let image_linear_sampler =
6569 device.create_sampler(&image_sampler_descriptor(ImageSampling::Linear));
6570 let text_glyph_atlas = TextGlyphAtlas::new(
6571 &device,
6572 &image_bind_group_layout,
6573 &image_nearest_sampler,
6574 TEXT_GLYPH_ATLAS_MIN_SIZE,
6575 );
6576
6577 #[cfg(not(target_arch = "wasm32"))]
6578 let image_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
6579 label: Some("Image Vertex Buffer"),
6580 size: (std::mem::size_of::<Vertex>() * 4) as u64,
6581 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
6582 mapped_at_creation: false,
6583 });
6584
6585 #[cfg(not(target_arch = "wasm32"))]
6586 let image_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
6587 label: Some("Image Index Buffer"),
6588 size: (std::mem::size_of::<u32>() * 6) as u64,
6589 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
6590 mapped_at_creation: false,
6591 });
6592 #[cfg(not(target_arch = "wasm32"))]
6593 let retained_glyph_uniform_stride = align_usize_to(
6594 std::mem::size_of::<Uniforms>(),
6595 (device.limits().min_uniform_buffer_offset_alignment as usize)
6596 .max(wgpu::COPY_BUFFER_ALIGNMENT as usize),
6597 ) as u64;
6598 #[cfg(not(target_arch = "wasm32"))]
6599 let retained_glyph_uniform_capacity = INITIAL_RETAINED_GLYPH_UNIFORM_SLOTS;
6600 #[cfg(not(target_arch = "wasm32"))]
6601 let retained_glyph_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
6602 label: Some("Retained Glyph Uniform Buffer"),
6603 size: retained_glyph_uniform_stride * retained_glyph_uniform_capacity as u64,
6604 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
6605 mapped_at_creation: false,
6606 });
6607 #[cfg(not(target_arch = "wasm32"))]
6608 let retained_glyph_uniform_bind_group =
6609 device.create_bind_group(&wgpu::BindGroupDescriptor {
6610 label: Some("Retained Glyph Uniform Bind Group"),
6611 layout: &retained_glyph_uniform_bind_group_layout,
6612 entries: &[wgpu::BindGroupEntry {
6613 binding: 0,
6614 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
6615 buffer: &retained_glyph_uniform_buffer,
6616 offset: 0,
6617 size: wgpu::BufferSize::new(std::mem::size_of::<Uniforms>() as u64),
6618 }),
6619 }],
6620 });
6621
6622 #[cfg(not(target_arch = "wasm32"))]
6629 let pipeline_cache = crate::pipeline_disk_cache::load(&device);
6630 #[cfg(target_arch = "wasm32")]
6631 let pipeline_cache: Option<wgpu::PipelineCache> = None;
6632 #[cfg(not(target_arch = "wasm32"))]
6633 if let Some(cache) = pipeline_cache.clone() {
6634 crate::pipeline_disk_cache::spawn_persist_schedule(cache);
6635 spawn_pipeline_prewarm(PipelinePrewarmInputs {
6636 device: Arc::clone(&device),
6637 cache: pipeline_cache.clone(),
6638 surface_format: composition_format,
6639 uniform_layout: uniform_bind_group_layout.clone(),
6640 shape_layout: shape_bind_group_layout.clone(),
6641 image_layout: image_bind_group_layout.clone(),
6642 batch_limits: shape_batch_limits,
6643 instanced: instanced_quads.is_some(),
6644 });
6645 }
6646
6647 let effects_started = Instant::now();
6648 let effect_renderer = EffectRenderer::new(
6649 &device,
6650 pipeline_cache.clone(),
6651 composition_format,
6652 adapter_backend,
6653 );
6654 let output_converter = OutputConverter::new(&device, display_format);
6655 let screenshot_converter = OutputConverter::new(&device, wgpu::TextureFormat::Rgba8Unorm);
6656 let effects_ms = instant_ms(effects_started, Instant::now());
6657
6658 let renderer = Self {
6659 device,
6660 queue,
6661 device_errors,
6662 renderer_epoch,
6663 #[cfg(not(target_arch = "wasm32"))]
6664 store_feed_generation,
6665 composition_format,
6666 #[cfg(not(target_arch = "wasm32"))]
6667 display_format,
6668 composition_target: None,
6669 output_converter,
6670 screenshot_converter,
6671 adapter_backend,
6672 shape_batch_limits,
6673 pipeline_cache,
6674 pipeline,
6675 pipeline_dst_out,
6676 pipeline_solid,
6677 #[cfg(not(target_arch = "wasm32"))]
6678 mesh_pipeline,
6679 #[cfg(not(target_arch = "wasm32"))]
6680 instanced_quads,
6681 #[cfg(not(target_arch = "wasm32"))]
6682 segment_capture_pipelines,
6683 uniform_bind_group_layout,
6684 shape_bind_group_layout,
6685 dummy_paint_buffer,
6686 identity_similarity_buffer,
6687 #[cfg(not(target_arch = "wasm32"))]
6688 replay_slots: replay_slot_store,
6689 image_pipeline,
6690 image_pipeline_dst_out,
6691 glyph_atlas_pipeline,
6692 #[cfg(not(target_arch = "wasm32"))]
6693 retained_glyph_atlas_pipeline,
6694 image_bind_group_layout,
6695 #[cfg(not(target_arch = "wasm32"))]
6696 retained_glyph_uniform_bind_group_layout,
6697 image_nearest_sampler,
6698 image_linear_sampler,
6699 text_fonts,
6700 #[cfg(not(target_arch = "wasm32"))]
6701 upload_buffer,
6702 #[cfg(not(target_arch = "wasm32"))]
6703 uniform_buffer,
6704 #[cfg(not(target_arch = "wasm32"))]
6705 uniform_bind_group,
6706 #[cfg(not(target_arch = "wasm32"))]
6707 shape_buffers,
6708 #[cfg(not(target_arch = "wasm32"))]
6709 image_vertex_buffer,
6710 #[cfg(not(target_arch = "wasm32"))]
6711 image_index_buffer,
6712 #[cfg(not(target_arch = "wasm32"))]
6713 retained_glyph_uniform_buffer,
6714 #[cfg(not(target_arch = "wasm32"))]
6715 retained_glyph_uniform_bind_group,
6716 #[cfg(not(target_arch = "wasm32"))]
6717 retained_glyph_uniform_stride,
6718 #[cfg(not(target_arch = "wasm32"))]
6719 retained_glyph_uniform_capacity,
6720 #[cfg(not(target_arch = "wasm32"))]
6721 retained_glyph_uniform_cursor: 0,
6722 #[cfg(target_arch = "wasm32")]
6723 wasm_uniform_batches: Vec::new(),
6724 #[cfg(target_arch = "wasm32")]
6725 wasm_uniform_batch_cursor: 0,
6726 #[cfg(target_arch = "wasm32")]
6727 wasm_shape_batches: Vec::new(),
6728 #[cfg(target_arch = "wasm32")]
6729 wasm_shape_batch_cursor: 0,
6730 #[cfg(target_arch = "wasm32")]
6731 wasm_image_batches: Vec::new(),
6732 #[cfg(target_arch = "wasm32")]
6733 wasm_image_batch_cursor: 0,
6734 image_texture_cache: BoundedLruCache::with_capacity_at_least_one(
6735 MAX_TEXTURE_CACHE_ITEMS,
6736 ),
6737 image_texture_cache_bytes: 0,
6738 text_image_cache: BoundedLruCache::with_capacity_at_least_one(
6739 MAX_TEXT_IMAGE_CACHE_ITEMS,
6740 ),
6741 text_glyph_atlas,
6742 text_glyph_run_cache: BoundedLruCache::with_capacity_at_least_one(
6743 MAX_TEXT_GLYPH_RUN_CACHE_ITEMS,
6744 ),
6745 #[cfg(not(target_arch = "wasm32"))]
6746 text_glyph_gpu_run_cache: BoundedLruCache::with_capacity_at_least_one(
6747 MAX_TEXT_GLYPH_GPU_RUN_CACHE_ITEMS,
6748 ),
6749 text_glyph_mask_cache: SoftwareGlyphRasterCache::with_capacity_at_least_one(
6750 MAX_TEXT_GLYPH_MASK_CACHE_ITEMS,
6751 ),
6752 text_line_index_cache: TextLineIndexCache::new(MAX_TEXT_LINE_INDEX_CACHE_ITEMS),
6753 scratch_shape_data: Vec::new(),
6754 scratch_gradients: Vec::new(),
6755 scratch_image_vertices: Vec::new(),
6756 scratch_image_indices: Vec::new(),
6757 scratch_image_cmds: Vec::new(),
6758 scratch_glyph_cmds: Vec::new(),
6759 scratch_text_glyph_run: Vec::new(),
6760 scratch_text_glyph_placements: Vec::new(),
6761 scratch_text_glyph_quads: Vec::new(),
6762 scratch_segment_items: Vec::new(),
6763 scratch_effect_ranges: Vec::new(),
6764 scratch_layer_events: Vec::new(),
6765 staged_uploads: StagedBufferUploads::default(),
6766 frame_graph_executor: WgpuFrameGraphExecutor::new(),
6767 deferred_offscreen_releases: Vec::new(),
6768 effect_renderer,
6769 layer_surface_cache: LayerSurfaceCache::new(),
6770 observed_scene_range_cache_misses: BoundedLruCache::with_capacity_at_least_one(
6771 MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES,
6772 ),
6773 shadow_surface_cache: BoundedLruCache::with_capacity_at_least_one(
6774 MAX_SHADOW_SURFACE_CACHE_ITEMS,
6775 ),
6776 shadow_surface_cache_bytes: 0,
6777 frame_stats: gpu_stats::FrameStats::default(),
6778 last_frame_stats: None,
6779 pending_frame_warmup_frames: 0,
6780 frame_count: 0,
6781 gpu_stats_enabled: gpu_stats_enabled(),
6782 warning_state: RendererWarningState::default(),
6783 #[cfg(not(target_arch = "wasm32"))]
6784 replay_upload_stats: ReplayUploadStats::default(),
6785 #[cfg(not(target_arch = "wasm32"))]
6786 segment_encode_stats: SegmentEncodeStats::default(),
6787 #[cfg(not(target_arch = "wasm32"))]
6788 replay_color_patches: Vec::new(),
6789 #[cfg(not(target_arch = "wasm32"))]
6790 color_patch_scratch: Vec::new(),
6791 #[cfg(not(target_arch = "wasm32"))]
6792 replay_capture_shape_scratch: Vec::new(),
6793 #[cfg(not(target_arch = "wasm32"))]
6794 replay_capture_gradient_scratch: Vec::new(),
6795 #[cfg(not(target_arch = "wasm32"))]
6796 replay_ack_confirmations: Vec::new(),
6797 #[cfg(not(target_arch = "wasm32"))]
6798 replay_generation_drops: 0,
6799 #[cfg(not(target_arch = "wasm32"))]
6800 retained_bundle_cache: RetainedBundleCache::new(),
6801 #[cfg(not(target_arch = "wasm32"))]
6802 rim_mesh_vertices: Vec::new(),
6803 #[cfg(not(target_arch = "wasm32"))]
6804 rim_mesh_indices: Vec::new(),
6805 #[cfg(not(target_arch = "wasm32"))]
6806 rim_mesh_vertex_buffer: None,
6807 #[cfg(not(target_arch = "wasm32"))]
6808 rim_mesh_index_buffer: None,
6809 #[cfg(not(target_arch = "wasm32"))]
6810 rim_mesh_uploaded_vertices: 0,
6811 #[cfg(not(target_arch = "wasm32"))]
6812 rim_mesh_uploaded_indices: 0,
6813 #[cfg(not(target_arch = "wasm32"))]
6814 rim_meshes_emitted: 0,
6815 #[cfg(not(target_arch = "wasm32"))]
6816 fill_area_diag: FillAreaDiag::default(),
6817 #[cfg(not(target_arch = "wasm32"))]
6818 static_span: StaticSpanCache::default(),
6819 #[cfg(not(target_arch = "wasm32"))]
6820 segment_surfaces: SegmentSurfaceCache::default(),
6821 #[cfg(not(target_arch = "wasm32"))]
6822 display_clip: DisplayClipState::new(),
6823 };
6824 log::info!(
6825 "[gpu-init] {:?} renderer ready in {:.1} ms (effects {:.1} ms); \
6826 pipelines build on first use",
6827 adapter_backend,
6828 instant_ms(construction_started, Instant::now()),
6829 effects_ms,
6830 );
6831 renderer
6832 }
6833
6834 #[cfg(not(target_arch = "wasm32"))]
6840 pub fn set_display_visible_region(&mut self, region: DisplayVisibleRegion) {
6841 self.display_clip.visible_region = region;
6842 }
6843
6844 #[cfg(not(target_arch = "wasm32"))]
6848 fn pass_depth(&self) -> bool {
6849 self.display_clip.pass_depth.get()
6850 }
6851
6852 #[cfg(target_arch = "wasm32")]
6853 fn pass_depth(&self) -> bool {
6854 false
6855 }
6856
6857 #[cfg(not(target_arch = "wasm32"))]
6865 fn display_clip_pass_depth_view(
6866 &mut self,
6867 target_view: &wgpu::TextureView,
6868 width: u32,
6869 height: u32,
6870 ) -> Option<wgpu::TextureView> {
6871 if !self.display_clip.visible_region.cullable() {
6872 return None;
6873 }
6874 if self.display_clip.frame_root_view.as_ref() != Some(target_view) {
6875 return None;
6876 }
6877 if !display_clip_cull_enabled() {
6878 return None;
6879 }
6880 self.ensure_display_clip_resources(width, height)
6881 }
6882
6883 #[cfg(not(target_arch = "wasm32"))]
6889 fn ensure_display_clip_resources(
6890 &mut self,
6891 width: u32,
6892 height: u32,
6893 ) -> Option<wgpu::TextureView> {
6894 let region = self.display_clip.visible_region;
6895 let key = ((width, height), region);
6896 if let Some((cached_key, resources)) = &self.display_clip.resources {
6897 if *cached_key == key {
6898 return resources
6899 .as_ref()
6900 .map(|resources| resources.depth_view.clone());
6901 }
6902 }
6903 let built = display_clip::tessellate_complement(region, width, height).map(|mesh| {
6904 let occluder_vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
6905 label: Some("Display Clip Occluder Vertices"),
6906 size: std::mem::size_of_val(mesh.vertices.as_slice()) as u64,
6907 usage: wgpu::BufferUsages::VERTEX,
6908 mapped_at_creation: true,
6909 });
6910 occluder_vertex_buffer
6911 .slice(..)
6912 .get_mapped_range_mut()
6913 .copy_from_slice(bytemuck::cast_slice(&mesh.vertices));
6914 occluder_vertex_buffer.unmap();
6915 let depth_texture = self.device.create_texture(&wgpu::TextureDescriptor {
6916 label: Some("Display Clip Depth"),
6917 size: wgpu::Extent3d {
6918 width,
6919 height,
6920 depth_or_array_layers: 1,
6921 },
6922 mip_level_count: 1,
6923 sample_count: 1,
6924 dimension: wgpu::TextureDimension::D2,
6925 format: display_clip::DISPLAY_CLIP_DEPTH_FORMAT,
6926 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
6927 view_formats: &[],
6928 });
6929 match region {
6933 DisplayVisibleRegion::InscribedCircle => log::info!(
6934 "[display-clip] round display: corner cull active ({} px masked) at {width}x{height}",
6935 mesh.masked_px,
6936 ),
6937 _ => log::info!(
6938 "[display-clip] visible-region cull active for {region:?} ({} px masked) at {width}x{height}",
6939 mesh.masked_px,
6940 ),
6941 }
6942 DisplayClipResources {
6943 depth_view: depth_texture.create_view(&wgpu::TextureViewDescriptor::default()),
6944 occluder_vertex_buffer,
6945 occluder_vertex_count: mesh.vertices.len() as u32,
6946 }
6947 });
6948 let view = built.as_ref().map(|resources| resources.depth_view.clone());
6949 self.display_clip.resources = Some((key, built));
6950 view
6951 }
6952
6953 #[cfg(not(target_arch = "wasm32"))]
6957 fn draw_display_clip_occluder(
6958 &self,
6959 render_pass: &mut wgpu::RenderPass<'_>,
6960 width: u32,
6961 height: u32,
6962 ) {
6963 let Some((((size_w, size_h), _), Some(resources))) = &self.display_clip.resources else {
6964 return;
6965 };
6966 debug_assert_eq!((*size_w, *size_h), (width, height));
6967 let pipeline =
6968 self.display_clip
6969 .occluder_pipeline
6970 .get_or_init(self.adapter_backend, || {
6971 create_display_clip_occluder_pipeline(
6972 &self.device,
6973 self.pipeline_cache.as_ref(),
6974 self.composition_format,
6975 )
6976 });
6977 render_pass.set_scissor_rect(0, 0, width, height);
6978 render_pass.set_pipeline(pipeline);
6979 render_pass.set_vertex_buffer(0, resources.occluder_vertex_buffer.slice(..));
6980 render_pass.draw(0..resources.occluder_vertex_count, 0..1);
6981 self.frame_stats.add_draw_calls(1);
6982 }
6983
6984 fn shape_pipeline(&self, blend_mode: BlendMode) -> &wgpu::RenderPipeline {
6985 let resource = match blend_mode {
6986 BlendMode::DstOut => &self.pipeline_dst_out,
6987 _ => &self.pipeline,
6988 };
6989 resource.get_or_init(self.adapter_backend, self.pass_depth(), |depth| {
6990 create_shape_pipeline(
6991 &self.device,
6992 self.pipeline_cache.as_ref(),
6993 self.composition_format,
6994 &self.uniform_bind_group_layout,
6995 &self.shape_bind_group_layout,
6996 blend_mode,
6997 self.shape_batch_limits,
6998 false,
6999 "vs_main",
7000 "fs_main",
7001 depth,
7002 )
7003 })
7004 }
7005
7006 fn shape_pipeline_solid(&self) -> &wgpu::RenderPipeline {
7015 self.pipeline_solid
7016 .get_or_init(self.adapter_backend, self.pass_depth(), |depth| {
7017 let solid_trim = solid_trim_varyings_enabled();
7018 let (vertex_entry, fragment_entry) = if solid_trim {
7019 ("vs_solid", "fs_solid_trim")
7020 } else {
7021 ("vs_main", "fs_solid")
7022 };
7023 create_shape_pipeline(
7024 &self.device,
7025 self.pipeline_cache.as_ref(),
7026 self.composition_format,
7027 &self.uniform_bind_group_layout,
7028 &self.shape_bind_group_layout,
7029 BlendMode::SrcOver,
7030 self.shape_batch_limits,
7031 solid_trim,
7032 vertex_entry,
7033 fragment_entry,
7034 depth,
7035 )
7036 })
7037 }
7038
7039 #[cfg(not(target_arch = "wasm32"))]
7040 fn mesh_pipeline(&self) -> &wgpu::RenderPipeline {
7041 self.mesh_pipeline
7042 .get_or_init(self.adapter_backend, self.pass_depth(), |depth| {
7043 create_mesh_shape_pipeline(
7044 &self.device,
7045 self.pipeline_cache.as_ref(),
7046 self.composition_format,
7047 &self.uniform_bind_group_layout,
7048 &self.shape_bind_group_layout,
7049 self.shape_batch_limits,
7050 depth,
7051 )
7052 })
7053 }
7054
7055 #[cfg(not(target_arch = "wasm32"))]
7056 fn instanced_pipeline<'a>(
7057 &'a self,
7058 instanced: &'a InstancedQuadPipelines,
7059 blend_mode: BlendMode,
7060 ) -> &'a wgpu::RenderPipeline {
7061 let resource = match blend_mode {
7062 BlendMode::DstOut => &instanced.pipeline_dst_out,
7063 _ => &instanced.pipeline,
7064 };
7065 resource.get_or_init(self.adapter_backend, self.pass_depth(), |depth| {
7066 create_instanced_shape_pipeline(
7067 &self.device,
7068 self.pipeline_cache.as_ref(),
7069 self.composition_format,
7070 &self.uniform_bind_group_layout,
7071 &self.shape_bind_group_layout,
7072 blend_mode,
7073 self.shape_batch_limits,
7074 false,
7075 "vs_shape_instanced",
7076 "fs_main",
7077 depth,
7078 )
7079 })
7080 }
7081
7082 #[cfg(not(target_arch = "wasm32"))]
7086 fn instanced_pipeline_solid<'a>(
7087 &'a self,
7088 instanced: &'a InstancedQuadPipelines,
7089 ) -> &'a wgpu::RenderPipeline {
7090 instanced
7091 .pipeline_solid
7092 .get_or_init(self.adapter_backend, self.pass_depth(), |depth| {
7093 let solid_trim = solid_trim_varyings_enabled();
7094 let (vertex_entry, fragment_entry) = if solid_trim {
7095 ("vs_solid_instanced", "fs_solid_trim")
7096 } else {
7097 ("vs_shape_instanced", "fs_solid")
7098 };
7099 create_instanced_shape_pipeline(
7100 &self.device,
7101 self.pipeline_cache.as_ref(),
7102 self.composition_format,
7103 &self.uniform_bind_group_layout,
7104 &self.shape_bind_group_layout,
7105 BlendMode::SrcOver,
7106 self.shape_batch_limits,
7107 solid_trim,
7108 vertex_entry,
7109 fragment_entry,
7110 depth,
7111 )
7112 })
7113 }
7114
7115 #[cfg(not(target_arch = "wasm32"))]
7116 fn segment_capture_pipeline(&self, kind: RetainedPipelineKind) -> &wgpu::RenderPipeline {
7117 let format = COMPOSITION_FORMAT;
7118 match kind {
7119 RetainedPipelineKind::Mesh => {
7120 self.segment_capture_pipelines
7121 .mesh
7122 .get_or_init(self.adapter_backend, false, |_| {
7123 create_mesh_shape_pipeline(
7124 &self.device,
7125 self.pipeline_cache.as_ref(),
7126 format,
7127 &self.uniform_bind_group_layout,
7128 &self.shape_bind_group_layout,
7129 self.shape_batch_limits,
7130 false,
7131 )
7132 })
7133 }
7134 RetainedPipelineKind::Expanded => self.segment_capture_pipelines.expanded.get_or_init(
7135 self.adapter_backend,
7136 false,
7137 |_| {
7138 create_shape_pipeline(
7139 &self.device,
7140 self.pipeline_cache.as_ref(),
7141 format,
7142 &self.uniform_bind_group_layout,
7143 &self.shape_bind_group_layout,
7144 BlendMode::SrcOver,
7145 self.shape_batch_limits,
7146 false,
7147 "vs_main",
7148 "fs_main",
7149 false,
7150 )
7151 },
7152 ),
7153 RetainedPipelineKind::ExpandedSolid => self
7154 .segment_capture_pipelines
7155 .expanded_solid
7156 .get_or_init(self.adapter_backend, false, |_| {
7157 let solid_trim = solid_trim_varyings_enabled();
7158 let (vertex_entry, fragment_entry) = if solid_trim {
7159 ("vs_solid", "fs_solid_trim")
7160 } else {
7161 ("vs_main", "fs_solid")
7162 };
7163 create_shape_pipeline(
7164 &self.device,
7165 self.pipeline_cache.as_ref(),
7166 format,
7167 &self.uniform_bind_group_layout,
7168 &self.shape_bind_group_layout,
7169 BlendMode::SrcOver,
7170 self.shape_batch_limits,
7171 solid_trim,
7172 vertex_entry,
7173 fragment_entry,
7174 false,
7175 )
7176 }),
7177 RetainedPipelineKind::Instanced | RetainedPipelineKind::InstancedSolid => {
7178 let Some(_) = self.instanced_quads.as_ref() else {
7179 return self.segment_capture_pipeline(match kind {
7180 RetainedPipelineKind::Instanced => RetainedPipelineKind::Expanded,
7181 RetainedPipelineKind::InstancedSolid => RetainedPipelineKind::ExpandedSolid,
7182 _ => unreachable!(),
7183 });
7184 };
7185 let (resource, solid_trim, vertex_entry, fragment_entry) = match kind {
7186 RetainedPipelineKind::Instanced => (
7187 &self.segment_capture_pipelines.instanced,
7188 false,
7189 "vs_shape_instanced",
7190 "fs_main",
7191 ),
7192 RetainedPipelineKind::InstancedSolid => {
7193 let solid_trim = solid_trim_varyings_enabled();
7194 (
7195 &self.segment_capture_pipelines.instanced_solid,
7196 solid_trim,
7197 if solid_trim {
7198 "vs_solid_instanced"
7199 } else {
7200 "vs_shape_instanced"
7201 },
7202 if solid_trim {
7203 "fs_solid_trim"
7204 } else {
7205 "fs_solid"
7206 },
7207 )
7208 }
7209 _ => unreachable!(),
7210 };
7211 resource.get_or_init(self.adapter_backend, false, |_| {
7212 create_instanced_shape_pipeline(
7213 &self.device,
7214 self.pipeline_cache.as_ref(),
7215 format,
7216 &self.uniform_bind_group_layout,
7217 &self.shape_bind_group_layout,
7218 BlendMode::SrcOver,
7219 self.shape_batch_limits,
7220 solid_trim,
7221 vertex_entry,
7222 fragment_entry,
7223 false,
7224 )
7225 })
7226 }
7227 }
7228 }
7229
7230 #[cfg(not(target_arch = "wasm32"))]
7231 fn retained_pipeline(&self, kind: RetainedPipelineKind) -> &wgpu::RenderPipeline {
7232 match kind {
7233 RetainedPipelineKind::Mesh => self.mesh_pipeline(),
7234 RetainedPipelineKind::Expanded => self.shape_pipeline(BlendMode::SrcOver),
7235 RetainedPipelineKind::ExpandedSolid => self.shape_pipeline_solid(),
7236 RetainedPipelineKind::Instanced => self
7237 .instanced_quads
7238 .as_ref()
7239 .map(|instanced| self.instanced_pipeline(instanced, BlendMode::SrcOver))
7240 .unwrap_or_else(|| self.shape_pipeline(BlendMode::SrcOver)),
7241 RetainedPipelineKind::InstancedSolid => self
7242 .instanced_quads
7243 .as_ref()
7244 .map(|instanced| self.instanced_pipeline_solid(instanced))
7245 .unwrap_or_else(|| self.shape_pipeline_solid()),
7246 }
7247 }
7248
7249 fn image_pipeline(&self, blend_mode: BlendMode) -> &wgpu::RenderPipeline {
7250 let resource = match blend_mode {
7251 BlendMode::DstOut => &self.image_pipeline_dst_out,
7252 _ => &self.image_pipeline,
7253 };
7254 resource.get_or_init(self.adapter_backend, self.pass_depth(), |depth| {
7255 create_image_pipeline(
7256 &self.device,
7257 self.pipeline_cache.as_ref(),
7258 self.composition_format,
7259 &self.uniform_bind_group_layout,
7260 &self.image_bind_group_layout,
7261 blend_mode,
7262 depth,
7263 )
7264 })
7265 }
7266
7267 fn glyph_atlas_pipeline(&self) -> &wgpu::RenderPipeline {
7268 self.glyph_atlas_pipeline
7269 .get_or_init(self.adapter_backend, self.pass_depth(), |depth| {
7270 create_glyph_atlas_pipeline(
7271 &self.device,
7272 self.pipeline_cache.as_ref(),
7273 self.composition_format,
7274 &self.uniform_bind_group_layout,
7275 &self.image_bind_group_layout,
7276 depth,
7277 )
7278 })
7279 }
7280
7281 #[cfg(not(target_arch = "wasm32"))]
7282 fn retained_glyph_atlas_pipeline(&self) -> &wgpu::RenderPipeline {
7283 self.retained_glyph_atlas_pipeline.get_or_init(
7284 self.adapter_backend,
7285 self.pass_depth(),
7286 |depth| {
7287 create_glyph_atlas_pipeline(
7288 &self.device,
7289 self.pipeline_cache.as_ref(),
7290 self.composition_format,
7291 &self.retained_glyph_uniform_bind_group_layout,
7292 &self.image_bind_group_layout,
7293 depth,
7294 )
7295 },
7296 )
7297 }
7298
7299 fn ensure_image_cached(&mut self, image: &ImageBitmap) -> Result<(), String> {
7300 if self.image_texture_cache.get(&image.id()).is_some() {
7301 return Ok(());
7302 }
7303
7304 let size = wgpu::Extent3d {
7305 width: image.width(),
7306 height: image.height(),
7307 depth_or_array_layers: 1,
7308 };
7309
7310 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
7311 label: Some("Image Texture"),
7312 size,
7313 mip_level_count: 1,
7314 sample_count: 1,
7315 dimension: wgpu::TextureDimension::D2,
7316 format: wgpu::TextureFormat::Rgba8Unorm,
7317 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
7318 view_formats: &[],
7319 });
7320
7321 let upload_stats = self.frame_graph_executor.upload_texture(
7322 &self.queue,
7323 wgpu::TexelCopyTextureInfo {
7324 texture: &texture,
7325 mip_level: 0,
7326 origin: wgpu::Origin3d::ZERO,
7327 aspect: wgpu::TextureAspect::All,
7328 },
7329 image.pixels(),
7330 wgpu::TexelCopyBufferLayout {
7331 offset: 0,
7332 bytes_per_row: Some(4 * image.width()),
7333 rows_per_image: Some(image.height()),
7334 },
7335 size,
7336 );
7337 self.frame_stats.record_command_stats(upload_stats);
7338
7339 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
7340 let nearest_bind_group = self.image_bind_group(&view, &self.image_nearest_sampler);
7341 let linear_bind_group = self.image_bind_group(&view, &self.image_linear_sampler);
7342
7343 let bytes = image.width() as usize * image.height() as usize * 4;
7344 if let Some(replaced) = self.image_texture_cache.put(
7345 image.id(),
7346 CachedImageTexture {
7347 _texture: texture,
7348 _view: view,
7349 nearest_bind_group,
7350 linear_bind_group,
7351 bytes,
7352 },
7353 ) {
7354 self.image_texture_cache_bytes = self
7355 .image_texture_cache_bytes
7356 .saturating_sub(replaced.bytes);
7357 }
7358 self.image_texture_cache_bytes += bytes;
7359 while self.image_texture_cache_bytes > MAX_IMAGE_TEXTURE_CACHE_BYTES
7362 && self.image_texture_cache.len() > 1
7363 {
7364 let Some((_, evicted)) = self.image_texture_cache.pop_lru() else {
7365 break;
7366 };
7367 self.image_texture_cache_bytes =
7368 self.image_texture_cache_bytes.saturating_sub(evicted.bytes);
7369 }
7370 Ok(())
7371 }
7372
7373 fn image_bind_group(
7374 &self,
7375 view: &wgpu::TextureView,
7376 sampler: &wgpu::Sampler,
7377 ) -> wgpu::BindGroup {
7378 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
7379 label: Some("Image Texture Bind Group"),
7380 layout: &self.image_bind_group_layout,
7381 entries: &[
7382 wgpu::BindGroupEntry {
7383 binding: 0,
7384 resource: wgpu::BindingResource::TextureView(view),
7385 },
7386 wgpu::BindGroupEntry {
7387 binding: 1,
7388 resource: wgpu::BindingResource::Sampler(sampler),
7389 },
7390 ],
7391 })
7392 }
7393
7394 fn max_texture_dim(&self) -> u32 {
7397 self.effect_renderer.max_texture_dim()
7398 }
7399
7400 fn acquire_offscreen(&mut self, width: u32, height: u32) -> OffscreenTarget {
7401 self.effect_renderer
7402 .acquire_offscreen(&self.device, width, height, Some(&self.frame_stats))
7403 }
7404
7405 fn acquire_retained_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
7406 self.acquire_offscreen(width, height)
7407 }
7408
7409 fn take_composition_target(&mut self, width: u32, height: u32) -> CompositionTarget {
7410 if let Some(target) = self.composition_target.take() {
7411 if target.target.width == width && target.target.height == height {
7412 return target;
7413 }
7414 }
7415 let target = OffscreenTarget::new(&self.device, self.composition_format, width, height);
7416 let output_bind_group = self.output_converter.bind_group(&self.device, &target.view);
7417 CompositionTarget {
7418 target,
7419 output_bind_group,
7420 }
7421 }
7422
7423 #[cfg(not(target_arch = "wasm32"))]
7424 fn acquire_segment_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
7425 let max_texture_dim = self.max_texture_dim();
7426 OffscreenTarget::new(
7427 &self.device,
7428 COMPOSITION_FORMAT,
7429 width.min(max_texture_dim).max(1),
7430 height.min(max_texture_dim).max(1),
7431 )
7432 }
7433
7434 fn transient_offscreen_descriptor(
7435 &self,
7436 label: &'static str,
7437 width: u32,
7438 height: u32,
7439 ) -> FrameTextureDescriptor {
7440 let max_texture_dim = self.max_texture_dim();
7441 FrameTextureDescriptor::render_attachment(
7442 label,
7443 width.min(max_texture_dim),
7444 height.min(max_texture_dim),
7445 self.composition_format,
7446 )
7447 }
7448
7449 fn defer_offscreen_release(&mut self, target: OffscreenTarget) {
7450 self.deferred_offscreen_releases.push(target);
7451 }
7452
7453 fn flush_deferred_offscreen_releases(&mut self) {
7454 for target in self.deferred_offscreen_releases.drain(..) {
7455 self.effect_renderer.release_offscreen(target);
7456 }
7457 }
7458
7459 fn release_layer_surface_target(&mut self, target: LayerSurfaceTexture) {
7460 if let LayerSurfaceTexture::Owned(target) = target {
7461 self.defer_offscreen_release(target);
7462 }
7463 }
7464
7465 fn cached_layer_surface(
7466 &mut self,
7467 key: &LayerRasterCacheKey,
7468 ) -> Option<(Rc<OffscreenTarget>, Rect)> {
7469 self.layer_surface_cache.get(key, &self.frame_stats)
7470 }
7471
7472 fn admit_layer_surface_cache_miss(&mut self, key: &LayerRasterCacheKey) -> bool {
7473 admit_layer_surface_cache_miss_impl(key, &mut self.observed_scene_range_cache_misses)
7474 }
7475
7476 fn insert_cached_layer_surface(
7477 &mut self,
7478 key: LayerRasterCacheKey,
7479 target: OffscreenTarget,
7480 logical_rect: Rect,
7481 ) -> Rc<OffscreenTarget> {
7482 self.layer_surface_cache
7483 .insert(key, target, logical_rect, &self.frame_stats)
7484 }
7485
7486 fn cached_shadow_surface(
7487 &mut self,
7488 key: &ShadowSurfaceCacheKey,
7489 ) -> Option<Rc<OffscreenTarget>> {
7490 self.shadow_surface_cache
7491 .get(key)
7492 .map(|cached| cached.target.clone())
7493 }
7494
7495 fn cached_shape_shadow_composite(
7496 &mut self,
7497 shadow: &ShadowDraw,
7498 width: u32,
7499 height: u32,
7500 root_scale: f32,
7501 ) -> Option<CachedShadowComposite> {
7502 if shadow.blur_radius <= 0.0 || shadow.shapes.is_empty() || !shadow.texts.is_empty() {
7503 return None;
7504 }
7505
7506 let plan = shape_shadow_surface_plan(
7507 &shadow.shapes,
7508 shadow.clip,
7509 shadow.blur_radius,
7510 width,
7511 height,
7512 root_scale,
7513 self.max_texture_dim(),
7514 )?;
7515 let key = shape_shadow_surface_cache_key(
7516 &shadow.shapes,
7517 &shadow.brushes,
7518 plan.source_device_bounds,
7519 plan.pixel_radius,
7520 root_scale,
7521 )?;
7522 let cached = self.cached_shadow_surface(&key)?;
7523 let viewport_offset = [plan.source_device_bounds.x, plan.source_device_bounds.y];
7524 self.frame_stats.record_shadow_shape_cache_hit(
7525 plan.source_device_bounds.width,
7526 plan.source_device_bounds.height,
7527 );
7528
7529 let clip_scissor = shadow
7530 .clip
7531 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
7532 let scissor = clip_scissor.or(plan.processing_scissor);
7533 let rounded_mask = inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
7534 mask.rect[0] -= viewport_offset[0];
7535 mask.rect[1] -= viewport_offset[1];
7536 mask
7537 });
7538 let dest_viewport = Some((
7539 viewport_offset[0],
7540 viewport_offset[1],
7541 plan.source_device_bounds.width as f32,
7542 plan.source_device_bounds.height as f32,
7543 ));
7544
7545 Some(CachedShadowComposite {
7546 source: cached,
7547 scissor,
7548 rounded_mask,
7549 dest_viewport,
7550 })
7551 }
7552
7553 fn insert_cached_shadow_surface(
7554 &mut self,
7555 key: ShadowSurfaceCacheKey,
7556 target: OffscreenTarget,
7557 ) {
7558 let byte_size = offscreen_byte_size(target.width, target.height);
7559 while self.shadow_surface_cache_bytes + byte_size > MAX_SHADOW_SURFACE_CACHE_BYTES {
7560 let Some((_evicted_key, evicted_entry)) = self.shadow_surface_cache.pop_lru() else {
7561 break;
7562 };
7563 self.shadow_surface_cache_bytes = self
7564 .shadow_surface_cache_bytes
7565 .saturating_sub(evicted_entry.byte_size);
7566 }
7567
7568 let cached = CachedShadowSurface {
7569 target: Rc::new(target),
7570 byte_size,
7571 };
7572 if let Some((_replaced_key, replaced_entry)) = self.shadow_surface_cache.push(key, cached) {
7573 self.shadow_surface_cache_bytes = self
7574 .shadow_surface_cache_bytes
7575 .saturating_sub(replaced_entry.byte_size);
7576 }
7577 self.shadow_surface_cache_bytes = self.shadow_surface_cache_bytes.saturating_add(byte_size);
7578 }
7579
7580 fn supports_render_effect(&self, effect: &RenderEffect) -> bool {
7581 is_render_effect_supported(effect)
7582 }
7583}
7584
7585struct RecordingSurfaceBackend<'renderer, 'recorder, C: FrameCommandRecorder> {
7586 renderer: &'renderer mut GpuRenderer,
7587 recorder: &'recorder mut C,
7588}
7589
7590impl<C: FrameCommandRecorder> RecordingSurfaceBackend<'_, '_, C> {
7591 #[allow(clippy::too_many_arguments)]
7592 fn render_range_with_layer_events_to_target_recorded(
7593 &mut self,
7594 target: &OffscreenTarget,
7595 shapes: &[DrawShape],
7596 brushes: &[Brush],
7597 images: &[ImageDraw],
7598 texts: &[TextDraw],
7599 shadow_draws: &[ShadowDraw],
7600 retained_draws: &[RetainedDraw],
7601 draw_ops: &[DrawOp],
7602 effect_layers: &[EffectLayer],
7603 backdrop_layers: &[BackdropLayer],
7604 backdrop_input_hashes: &[u64],
7605 z_start: usize,
7606 z_end: usize,
7607 excluded_effect_layer: Option<usize>,
7608 width: u32,
7609 height: u32,
7610 root_scale: f32,
7611 backdrop_underlay: Option<&OffscreenTarget>,
7612 initial_load_op: wgpu::LoadOp<wgpu::Color>,
7613 ) -> Result<(), String> {
7614 if z_start >= z_end {
7615 if matches!(initial_load_op, wgpu::LoadOp::Clear(_)) {
7616 self.clear_target_view_with_load_op(&target.view, initial_load_op);
7617 }
7618 return Ok(());
7619 }
7620
7621 let mut effect_z_ranges = std::mem::take(&mut self.renderer.scratch_effect_ranges);
7622 collect_effect_ranges(
7623 effect_layers,
7624 z_start,
7625 z_end,
7626 excluded_effect_layer,
7627 &mut effect_z_ranges,
7628 );
7629 let mut events = std::mem::take(&mut self.renderer.scratch_layer_events);
7630 collect_layer_events(
7631 effect_layers,
7632 backdrop_layers,
7633 z_start,
7634 z_end,
7635 excluded_effect_layer,
7636 &mut events,
7637 );
7638
7639 let result = (|| -> Result<(), String> {
7640 let mut next_load_op = initial_load_op;
7641 let mut cursor_z = z_start;
7642 for event in &events {
7643 if event.z_index > cursor_z {
7644 self.render_non_effect_segment(
7645 &target.view,
7646 shapes,
7647 brushes,
7648 images,
7649 texts,
7650 shadow_draws,
7651 retained_draws,
7652 draw_ops,
7653 cursor_z,
7654 event.z_index,
7655 &effect_z_ranges,
7656 width,
7657 height,
7658 root_scale,
7659 next_load_op,
7660 )?;
7661 next_load_op = wgpu::LoadOp::Load;
7662 cursor_z = event.z_index;
7663 } else if event.z_index < cursor_z {
7664 continue;
7665 }
7666
7667 if matches!(next_load_op, wgpu::LoadOp::Clear(_)) {
7668 self.clear_target_view_with_load_op(&target.view, next_load_op);
7669 next_load_op = wgpu::LoadOp::Load;
7670 }
7671
7672 match event.kind {
7673 LayerEventKind::Backdrop(index) => {
7674 let layer = &backdrop_layers[index];
7675 let effective_backdrop_underlay = if backdrop_underlay.is_some()
7676 && backdrop_underlay_is_covered_by_local_content(
7677 shapes,
7678 brushes,
7679 images,
7680 shadow_draws,
7681 draw_ops,
7682 effect_layers,
7683 backdrop_layers,
7684 layer,
7685 ) {
7686 None
7687 } else {
7688 backdrop_underlay
7689 };
7690 execute_apply_backdrop_layer_to_target(
7691 self,
7692 target,
7693 layer,
7694 effective_backdrop_underlay,
7695 width,
7696 height,
7697 root_scale,
7698 backdrop_input_hashes.get(index).copied(),
7699 )?;
7700 }
7701 LayerEventKind::Effect(index) => {
7702 let layer = &effect_layers[index];
7703 if layer.z_start < cursor_z {
7704 continue;
7705 }
7706 execute_render_effect_layer_to_target(
7707 self,
7708 target,
7709 shapes,
7710 brushes,
7711 images,
7712 texts,
7713 shadow_draws,
7714 draw_ops,
7715 effect_layers,
7716 backdrop_layers,
7717 index,
7718 backdrop_underlay,
7719 width,
7720 height,
7721 root_scale,
7722 )?;
7723 cursor_z = cursor_z.max(layer.z_end);
7724 }
7725 }
7726 }
7727
7728 if cursor_z < z_end {
7729 self.render_non_effect_segment(
7730 &target.view,
7731 shapes,
7732 brushes,
7733 images,
7734 texts,
7735 shadow_draws,
7736 retained_draws,
7737 draw_ops,
7738 cursor_z,
7739 z_end,
7740 &effect_z_ranges,
7741 width,
7742 height,
7743 root_scale,
7744 next_load_op,
7745 )?;
7746 } else if matches!(next_load_op, wgpu::LoadOp::Clear(_)) {
7747 self.clear_target_view_with_load_op(&target.view, next_load_op);
7748 }
7749
7750 Ok(())
7751 })();
7752
7753 self.renderer.scratch_effect_ranges = effect_z_ranges;
7754 self.renderer.scratch_layer_events = events;
7755 result
7756 }
7757
7758 #[allow(clippy::too_many_arguments)]
7759 fn record_shader_composite(
7760 &mut self,
7761 source: &OffscreenTarget,
7762 shader: &RuntimeShader,
7763 effect_rect: [f32; 4],
7764 dest_view: &wgpu::TextureView,
7765 alpha: f32,
7766 load_op: wgpu::LoadOp<wgpu::Color>,
7767 scissor: Option<(u32, u32, u32, u32)>,
7768 blend_mode: BlendMode,
7769 dest_viewport: Option<(f32, f32, f32, f32)>,
7770 sample_mode: CompositeSampleMode,
7771 ) {
7772 let device = self.renderer.device.clone();
7773 if let Some(viewport) = direct_shader_composite_viewport(
7774 alpha,
7775 blend_mode,
7776 dest_viewport,
7777 sample_mode,
7778 (source.width, source.height),
7779 ) {
7780 let shader_applied = self
7781 .renderer
7782 .effect_renderer
7783 .encode_shader_src_over_to_view(
7784 self.recorder,
7785 &device,
7786 source,
7787 dest_view,
7788 shader,
7789 effect_rect,
7790 load_op,
7791 scissor,
7792 viewport,
7793 );
7794 if shader_applied {
7795 self.renderer
7796 .effect_renderer
7797 .debug_effects
7798 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
7799 self.recorder.record_pass();
7800 self.renderer.effect_renderer.record_composite_pass();
7801 return;
7802 }
7803 }
7804 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
7805 "Shader Effect Composite Scratch",
7806 source.width,
7807 source.height,
7808 );
7809 let scratch = self
7810 .recorder
7811 .acquire_transient_offscreen(&device, scratch_descriptor);
7812 let shader_applied = {
7813 self.renderer.effect_renderer.encode_shader(
7814 self.recorder,
7815 &device,
7816 source,
7817 &scratch.view,
7818 shader,
7819 effect_rect,
7820 )
7821 };
7822 let composite_source = if shader_applied {
7823 self.renderer
7824 .effect_renderer
7825 .debug_effects
7826 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
7827 self.recorder.record_pass();
7828 &scratch
7829 } else {
7830 source
7831 };
7832 {
7833 self.renderer
7834 .effect_renderer
7835 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
7836 self.recorder,
7837 &device,
7838 composite_source,
7839 dest_view,
7840 alpha,
7841 load_op,
7842 scissor,
7843 None,
7844 supported_blend_mode(blend_mode),
7845 dest_viewport,
7846 sample_mode,
7847 );
7848 }
7849 self.recorder.record_pass();
7850 self.renderer.effect_renderer.record_composite_pass();
7851 self.recorder
7852 .release_transient_offscreen(scratch_descriptor, scratch);
7853 }
7854
7855 #[allow(clippy::too_many_arguments)]
7856 fn record_shader_projective_composite(
7857 &mut self,
7858 source: &OffscreenTarget,
7859 shader: &RuntimeShader,
7860 effect_rect: [f32; 4],
7861 dest_view: &wgpu::TextureView,
7862 viewport: (u32, u32),
7863 source_size: (f32, f32),
7864 inverse_matrix: [[f32; 3]; 3],
7865 dest_bounds: [[f32; 2]; 4],
7866 alpha: f32,
7867 load_op: wgpu::LoadOp<wgpu::Color>,
7868 scissor: Option<(u32, u32, u32, u32)>,
7869 blend_mode: BlendMode,
7870 sample_mode: CompositeSampleMode,
7871 ) {
7872 if projective_dest_bounds_rect(dest_bounds).is_none() {
7873 return;
7874 }
7875 let device = self.renderer.device.clone();
7876 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
7877 "Shader Projective Composite Scratch",
7878 source.width,
7879 source.height,
7880 );
7881 let scratch = self
7882 .recorder
7883 .acquire_transient_offscreen(&device, scratch_descriptor);
7884 let shader_applied = {
7885 self.renderer.effect_renderer.encode_shader(
7886 self.recorder,
7887 &device,
7888 source,
7889 &scratch.view,
7890 shader,
7891 effect_rect,
7892 )
7893 };
7894 let composite_source = if shader_applied {
7895 self.renderer
7896 .effect_renderer
7897 .debug_effects
7898 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
7899 self.recorder.record_pass();
7900 &scratch
7901 } else {
7902 source
7903 };
7904 let composited = {
7905 self.renderer
7906 .effect_renderer
7907 .encode_composite_to_view_projective(
7908 self.recorder,
7909 &device,
7910 composite_source,
7911 dest_view,
7912 viewport,
7913 source_size,
7914 inverse_matrix,
7915 dest_bounds,
7916 alpha,
7917 load_op,
7918 scissor,
7919 supported_blend_mode(blend_mode),
7920 sample_mode,
7921 )
7922 };
7923 if composited {
7924 self.recorder.record_pass();
7925 self.renderer.effect_renderer.record_composite_pass();
7926 }
7927 self.recorder
7928 .release_transient_offscreen(scratch_descriptor, scratch);
7929 }
7930
7931 #[allow(clippy::too_many_arguments)]
7932 fn record_effect_with_direct_shader_tail_composite(
7933 &mut self,
7934 source: &OffscreenTarget,
7935 first_effect: &RenderEffect,
7936 shader: &RuntimeShader,
7937 effect_rect: [f32; 4],
7938 dest_view: &wgpu::TextureView,
7939 load_op: wgpu::LoadOp<wgpu::Color>,
7940 scissor: Option<(u32, u32, u32, u32)>,
7941 dest_viewport: (f32, f32, f32, f32),
7942 ) -> Result<bool, String> {
7943 let device = self.renderer.device.clone();
7944 let intermediate_descriptor = self.renderer.transient_offscreen_descriptor(
7945 "Render Effect Direct Shader Tail Intermediate",
7946 source.width,
7947 source.height,
7948 );
7949 let intermediate = self
7950 .recorder
7951 .acquire_transient_offscreen(&device, intermediate_descriptor);
7952 let effect_scratch_targets = self
7953 .renderer
7954 .effect_renderer
7955 .acquire_recorded_effect_scratch_targets(
7956 self.recorder,
7957 &device,
7958 first_effect,
7959 source.width,
7960 source.height,
7961 self.renderer.composition_format,
7962 );
7963 let first_passes = {
7964 let mut effect_scratch_refs = effect_scratch_targets.refs();
7965 let pass_count = self.renderer.effect_renderer.encode_effect(
7966 self.recorder,
7967 &device,
7968 source,
7969 &intermediate.view,
7970 first_effect,
7971 effect_rect,
7972 &mut effect_scratch_refs,
7973 );
7974 match pass_count {
7975 Ok(pass_count) => effect_scratch_refs.assert_consumed().map(|()| pass_count),
7976 Err(error) => Err(error),
7977 }
7978 };
7979 let first_passes = match first_passes {
7980 Ok(pass_count) => pass_count,
7981 Err(error) => {
7982 effect_scratch_targets.release_into(self.recorder);
7983 self.recorder
7984 .release_transient_offscreen(intermediate_descriptor, intermediate);
7985 return Err(error);
7986 }
7987 };
7988 let shader_applied = self
7989 .renderer
7990 .effect_renderer
7991 .encode_shader_src_over_to_view(
7992 self.recorder,
7993 &device,
7994 &intermediate,
7995 dest_view,
7996 shader,
7997 effect_rect,
7998 load_op,
7999 scissor,
8000 dest_viewport,
8001 );
8002 self.recorder
8003 .record_passes(first_passes.saturating_add(u32::from(shader_applied)));
8004 effect_scratch_targets.release_into(self.recorder);
8005 self.recorder
8006 .release_transient_offscreen(intermediate_descriptor, intermediate);
8007 if !shader_applied {
8008 return Ok(false);
8009 }
8010 self.renderer
8011 .effect_renderer
8012 .debug_effects
8013 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
8014 self.renderer.effect_renderer.record_composite_pass();
8015 Ok(true)
8016 }
8017
8018 #[allow(clippy::too_many_arguments)]
8019 fn record_effect_composite(
8020 &mut self,
8021 source: &OffscreenTarget,
8022 effect: &RenderEffect,
8023 effect_rect: [f32; 4],
8024 dest_view: &wgpu::TextureView,
8025 alpha: f32,
8026 load_op: wgpu::LoadOp<wgpu::Color>,
8027 scissor: Option<(u32, u32, u32, u32)>,
8028 blend_mode: BlendMode,
8029 dest_viewport: Option<(f32, f32, f32, f32)>,
8030 sample_mode: CompositeSampleMode,
8031 ) -> Result<(), String> {
8032 if let (
8033 RenderEffect::Chain { first, second },
8034 Some(viewport),
8035 BlendMode::SrcOver,
8036 CompositeSampleMode::Linear,
8037 ) = (
8038 effect,
8039 dest_viewport,
8040 supported_blend_mode(blend_mode),
8041 sample_mode,
8042 ) {
8043 if let (
8044 RenderEffect::Blur {
8045 radius_x,
8046 radius_y,
8047 edge_treatment,
8048 },
8049 RenderEffect::Shader { shader },
8050 ) = (first.as_ref(), second.as_ref())
8051 {
8052 if *radius_x > 0.0 || *radius_y > 0.0 {
8053 let device = self.renderer.device.clone();
8054 let (scratch_width, scratch_height) = crate::effect_renderer::blur_scratch_size(
8055 *radius_x,
8056 *radius_y,
8057 source.width,
8058 source.height,
8059 );
8060 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
8061 "Blur Rounded Mask Scratch",
8062 scratch_width,
8063 scratch_height,
8064 );
8065 let scratch = self
8066 .recorder
8067 .acquire_transient_offscreen(&device, scratch_descriptor);
8068 let fused = self
8069 .renderer
8070 .effect_renderer
8071 .encode_blur_then_rounded_mask_src_over_to_view(
8072 self.recorder,
8073 &device,
8074 source,
8075 &scratch,
8076 dest_view,
8077 *radius_x,
8078 *radius_y,
8079 *edge_treatment,
8080 shader,
8081 effect_rect,
8082 load_op,
8083 scissor,
8084 viewport,
8085 );
8086 if fused {
8087 self.recorder.record_passes(2);
8088 self.renderer.effect_renderer.record_blur_pass();
8089 self.renderer
8090 .effect_renderer
8091 .debug_effects
8092 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
8093 self.renderer.effect_renderer.record_composite_pass();
8094 self.recorder
8095 .release_transient_offscreen(scratch_descriptor, scratch);
8096 return Ok(());
8097 }
8098 self.recorder
8099 .release_transient_offscreen(scratch_descriptor, scratch);
8100 }
8101 }
8102 }
8103 if let Some((first_effect, shader, viewport)) = direct_shader_tail_composite(
8104 effect,
8105 alpha,
8106 blend_mode,
8107 dest_viewport,
8108 sample_mode,
8109 (source.width, source.height),
8110 ) {
8111 if self.record_effect_with_direct_shader_tail_composite(
8112 source,
8113 first_effect,
8114 shader,
8115 effect_rect,
8116 dest_view,
8117 load_op,
8118 scissor,
8119 viewport,
8120 )? {
8121 return Ok(());
8122 }
8123 }
8124 let device = self.renderer.device.clone();
8125 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
8126 "Render Effect Composite Scratch",
8127 source.width,
8128 source.height,
8129 );
8130 let scratch = self
8131 .recorder
8132 .acquire_transient_offscreen(&device, scratch_descriptor);
8133 let effect_scratch_targets = self
8134 .renderer
8135 .effect_renderer
8136 .acquire_recorded_effect_scratch_targets(
8137 self.recorder,
8138 &device,
8139 effect,
8140 source.width,
8141 source.height,
8142 self.renderer.composition_format,
8143 );
8144 let effect_passes = {
8145 let mut effect_scratch_refs = effect_scratch_targets.refs();
8146 let pass_count = self.renderer.effect_renderer.encode_effect(
8147 self.recorder,
8148 &device,
8149 source,
8150 &scratch.view,
8151 effect,
8152 effect_rect,
8153 &mut effect_scratch_refs,
8154 )?;
8155 effect_scratch_refs.assert_consumed()?;
8156 Ok(pass_count)
8157 };
8158 let effect_passes = match effect_passes {
8159 Ok(pass_count) => pass_count,
8160 Err(error) => {
8161 effect_scratch_targets.release_into(self.recorder);
8162 self.recorder
8163 .release_transient_offscreen(scratch_descriptor, scratch);
8164 return Err(error);
8165 }
8166 };
8167 {
8168 self.renderer
8169 .effect_renderer
8170 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
8171 self.recorder,
8172 &device,
8173 &scratch,
8174 dest_view,
8175 alpha,
8176 load_op,
8177 scissor,
8178 None,
8179 supported_blend_mode(blend_mode),
8180 dest_viewport,
8181 sample_mode,
8182 );
8183 }
8184 self.recorder.record_passes(effect_passes.saturating_add(1));
8185 self.renderer.effect_renderer.record_composite_pass();
8186 effect_scratch_targets.release_into(self.recorder);
8187 self.recorder
8188 .release_transient_offscreen(scratch_descriptor, scratch);
8189 Ok(())
8190 }
8191
8192 #[allow(clippy::too_many_arguments)]
8193 fn record_effect_projective_composite(
8194 &mut self,
8195 source: &OffscreenTarget,
8196 effect: &RenderEffect,
8197 effect_rect: [f32; 4],
8198 dest_view: &wgpu::TextureView,
8199 viewport: (u32, u32),
8200 source_size: (f32, f32),
8201 inverse_matrix: [[f32; 3]; 3],
8202 dest_bounds: [[f32; 2]; 4],
8203 alpha: f32,
8204 load_op: wgpu::LoadOp<wgpu::Color>,
8205 scissor: Option<(u32, u32, u32, u32)>,
8206 blend_mode: BlendMode,
8207 sample_mode: CompositeSampleMode,
8208 ) -> Result<(), String> {
8209 if projective_dest_bounds_rect(dest_bounds).is_none() {
8210 return Ok(());
8211 }
8212 let device = self.renderer.device.clone();
8213 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
8214 "Render Effect Projective Composite Scratch",
8215 source.width,
8216 source.height,
8217 );
8218 let scratch = self
8219 .recorder
8220 .acquire_transient_offscreen(&device, scratch_descriptor);
8221 let effect_scratch_targets = self
8222 .renderer
8223 .effect_renderer
8224 .acquire_recorded_effect_scratch_targets(
8225 self.recorder,
8226 &device,
8227 effect,
8228 source.width,
8229 source.height,
8230 self.renderer.composition_format,
8231 );
8232 let effect_passes = {
8233 let mut effect_scratch_refs = effect_scratch_targets.refs();
8234 let pass_count = self.renderer.effect_renderer.encode_effect(
8235 self.recorder,
8236 &device,
8237 source,
8238 &scratch.view,
8239 effect,
8240 effect_rect,
8241 &mut effect_scratch_refs,
8242 )?;
8243 effect_scratch_refs.assert_consumed()?;
8244 Ok(pass_count)
8245 };
8246 let effect_passes = match effect_passes {
8247 Ok(pass_count) => pass_count,
8248 Err(error) => {
8249 effect_scratch_targets.release_into(self.recorder);
8250 self.recorder
8251 .release_transient_offscreen(scratch_descriptor, scratch);
8252 return Err(error);
8253 }
8254 };
8255 let composited = {
8256 self.renderer
8257 .effect_renderer
8258 .encode_composite_to_view_projective(
8259 self.recorder,
8260 &device,
8261 &scratch,
8262 dest_view,
8263 viewport,
8264 source_size,
8265 inverse_matrix,
8266 dest_bounds,
8267 alpha,
8268 load_op,
8269 scissor,
8270 supported_blend_mode(blend_mode),
8271 sample_mode,
8272 )
8273 };
8274 if composited {
8275 self.recorder.record_passes(effect_passes.saturating_add(1));
8276 self.renderer.effect_renderer.record_composite_pass();
8277 } else {
8278 self.recorder.record_passes(effect_passes);
8279 }
8280 effect_scratch_targets.release_into(self.recorder);
8281 self.recorder
8282 .release_transient_offscreen(scratch_descriptor, scratch);
8283 Ok(())
8284 }
8285}
8286
8287impl<C: FrameCommandRecorder> SurfaceExecutionBackend for RecordingSurfaceBackend<'_, '_, C> {
8288 fn max_texture_dim(&self) -> u32 {
8289 self.renderer.max_texture_dim()
8290 }
8291
8292 fn acquire_retained_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
8293 self.renderer.acquire_retained_surface(width, height)
8294 }
8295
8296 fn acquire_frame_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
8297 let descriptor =
8298 self.renderer
8299 .transient_offscreen_descriptor("Frame Surface", width, height);
8300 self.recorder
8301 .acquire_transient_offscreen(&self.renderer.device, descriptor)
8302 }
8303
8304 fn release_frame_surface(&mut self, target: OffscreenTarget) {
8305 let descriptor = self.renderer.transient_offscreen_descriptor(
8306 "Frame Surface",
8307 target.width,
8308 target.height,
8309 );
8310 self.recorder
8311 .release_transient_offscreen(descriptor, target);
8312 }
8313
8314 fn release_layer_surface_target(&mut self, target: LayerSurfaceTexture) {
8315 self.renderer.release_layer_surface_target(target);
8316 }
8317
8318 fn cached_layer_surface(
8319 &mut self,
8320 key: &LayerRasterCacheKey,
8321 ) -> Option<(Rc<OffscreenTarget>, Rect)> {
8322 self.renderer.cached_layer_surface(key)
8323 }
8324
8325 fn admit_layer_surface_cache_miss(&mut self, key: &LayerRasterCacheKey) -> bool {
8326 self.renderer.admit_layer_surface_cache_miss(key)
8327 }
8328
8329 fn insert_cached_layer_surface(
8330 &mut self,
8331 key: LayerRasterCacheKey,
8332 target: OffscreenTarget,
8333 logical_rect: Rect,
8334 ) -> Rc<OffscreenTarget> {
8335 self.renderer
8336 .insert_cached_layer_surface(key, target, logical_rect)
8337 }
8338
8339 fn clear_target_view_with_load_op(
8340 &mut self,
8341 target_view: &wgpu::TextureView,
8342 load_op: wgpu::LoadOp<wgpu::Color>,
8343 ) {
8344 {
8345 let _clear = self
8346 .recorder
8347 .encoder()
8348 .begin_render_pass(&wgpu::RenderPassDescriptor {
8349 label: Some("Layer Event Clear Pass"),
8350 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
8351 view: target_view,
8352 resolve_target: None,
8353 depth_slice: None,
8354 ops: wgpu::Operations {
8355 load: load_op,
8356 store: wgpu::StoreOp::Store,
8357 },
8358 })],
8359 depth_stencil_attachment: None,
8360 timestamp_writes: None,
8361 occlusion_query_set: None,
8362 multiview_mask: None,
8363 });
8364 }
8365 self.recorder.record_pass();
8366 }
8367
8368 #[allow(clippy::too_many_arguments)]
8369 fn render_non_effect_segment(
8370 &mut self,
8371 target_view: &wgpu::TextureView,
8372 shapes: &[DrawShape],
8373 brushes: &[Brush],
8374 images: &[ImageDraw],
8375 texts: &[TextDraw],
8376 shadow_draws: &[ShadowDraw],
8377 retained_draws: &[RetainedDraw],
8378 draw_ops: &[DrawOp],
8379 z_start: usize,
8380 z_end: usize,
8381 effect_z_ranges: &[Range<usize>],
8382 width: u32,
8383 height: u32,
8384 root_scale: f32,
8385 initial_load_op: wgpu::LoadOp<wgpu::Color>,
8386 ) -> Result<(), String> {
8387 self.render_non_effect_segment_with_composites(
8388 target_view,
8389 shapes,
8390 brushes,
8391 images,
8392 texts,
8393 shadow_draws,
8394 retained_draws,
8395 draw_ops,
8396 z_start,
8397 z_end,
8398 effect_z_ranges,
8399 &[],
8400 &[],
8401 width,
8402 height,
8403 root_scale,
8404 initial_load_op,
8405 )
8406 }
8407
8408 #[allow(clippy::too_many_arguments)]
8409 fn render_non_effect_segment_with_composites(
8410 &mut self,
8411 target_view: &wgpu::TextureView,
8412 shapes: &[DrawShape],
8413 brushes: &[Brush],
8414 images: &[ImageDraw],
8415 texts: &[TextDraw],
8416 shadow_draws: &[ShadowDraw],
8417 retained_draws: &[RetainedDraw],
8418 draw_ops: &[DrawOp],
8419 z_start: usize,
8420 z_end: usize,
8421 effect_z_ranges: &[Range<usize>],
8422 composites: &[(usize, CompositeBatchItem<'_>)],
8423 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
8424 width: u32,
8425 height: u32,
8426 root_scale: f32,
8427 initial_load_op: wgpu::LoadOp<wgpu::Color>,
8428 ) -> Result<(), String> {
8429 let mut ordered_items = std::mem::take(&mut self.renderer.scratch_segment_items);
8430 collect_non_effect_segment_items(
8431 shapes,
8432 images,
8433 texts,
8434 shadow_draws,
8435 draw_ops,
8436 z_start,
8437 z_end,
8438 effect_z_ranges,
8439 width,
8440 height,
8441 root_scale,
8442 &mut ordered_items,
8443 );
8444 #[cfg(not(target_arch = "wasm32"))]
8445 let raw_shadow_items = ordered_items
8446 .iter()
8447 .filter(|(_, item)| matches!(item, SegmentDrawItem::Shadow(_)))
8448 .count();
8449 let culled_shadow_items = retain_renderable_shadow_items(
8450 &mut ordered_items,
8451 shadow_draws,
8452 width,
8453 height,
8454 root_scale,
8455 self.renderer.max_texture_dim(),
8456 );
8457 #[cfg(target_arch = "wasm32")]
8458 let _ = culled_shadow_items;
8459 let mut cached_shadow_composites: Vec<(usize, CachedShadowComposite)> = Vec::new();
8460 ordered_items.extend(
8461 composites
8462 .iter()
8463 .enumerate()
8464 .map(|(index, (z_index, _))| (*z_index, SegmentDrawItem::Composite(index))),
8465 );
8466 ordered_items.extend(
8467 shader_composites
8468 .iter()
8469 .enumerate()
8470 .map(|(index, (z_index, _))| (*z_index, SegmentDrawItem::ShaderComposite(index))),
8471 );
8472 for (z_index, item) in &mut ordered_items {
8473 let SegmentDrawItem::Shadow(shadow_index) = *item else {
8474 continue;
8475 };
8476 let Some(composite) = self.renderer.cached_shape_shadow_composite(
8477 &shadow_draws[shadow_index],
8478 width,
8479 height,
8480 root_scale,
8481 ) else {
8482 continue;
8483 };
8484 let composite_index = composites.len() + cached_shadow_composites.len();
8485 cached_shadow_composites.push((*z_index, composite));
8486 *item = SegmentDrawItem::Composite(composite_index);
8487 }
8488 let mut merged_composites = Vec::with_capacity(
8489 composites
8490 .len()
8491 .saturating_add(cached_shadow_composites.len()),
8492 );
8493 merged_composites.extend(composites.iter().copied());
8494 merged_composites.extend(
8495 cached_shadow_composites
8496 .iter()
8497 .map(|(z_index, composite)| (*z_index, composite.batch_item())),
8498 );
8499 ordered_items.sort_unstable_by_key(|(z_index, _)| *z_index);
8503 #[cfg(not(target_arch = "wasm32"))]
8504 maybe_print_segment_diag(
8505 z_start..z_end,
8506 &ordered_items,
8507 shapes,
8508 brushes,
8509 images,
8510 SegmentDiagCounts {
8511 raw_shadow_items,
8512 culled_shadow_items,
8513 cached_shadow_composites: cached_shadow_composites.len(),
8514 composite_items: merged_composites.len(),
8515 shader_composite_items: shader_composites.len(),
8516 },
8517 self.renderer.shape_batch_limits,
8518 );
8519 let result = if ordered_items.is_empty() {
8520 Ok(SegmentCommandEncodeOutcome { first_batch: true })
8521 } else {
8522 self.renderer.encode_non_effect_segment_commands(
8523 self.recorder,
8524 target_view,
8525 &ordered_items,
8526 &merged_composites,
8527 shader_composites,
8528 shapes,
8529 brushes,
8530 images,
8531 texts,
8532 shadow_draws,
8533 retained_draws,
8534 initial_load_op,
8535 width,
8536 height,
8537 root_scale,
8538 )
8539 };
8540 self.renderer.scratch_segment_items = ordered_items;
8541 let outcome = result?;
8542 if outcome.first_batch && matches!(initial_load_op, wgpu::LoadOp::Clear(_)) {
8543 self.clear_target_view_with_load_op(target_view, initial_load_op);
8544 }
8545 Ok(())
8546 }
8547
8548 fn render_range_with_layer_events_to_target(
8549 &mut self,
8550 target: &OffscreenTarget,
8551 shapes: &[DrawShape],
8552 brushes: &[Brush],
8553 images: &[ImageDraw],
8554 texts: &[TextDraw],
8555 shadow_draws: &[ShadowDraw],
8556 retained_draws: &[RetainedDraw],
8557 draw_ops: &[DrawOp],
8558 effect_layers: &[EffectLayer],
8559 backdrop_layers: &[BackdropLayer],
8560 backdrop_input_hashes: &[u64],
8561 z_start: usize,
8562 z_end: usize,
8563 excluded_effect_layer: Option<usize>,
8564 width: u32,
8565 height: u32,
8566 root_scale: f32,
8567 backdrop_underlay: Option<&OffscreenTarget>,
8568 initial_load_op: wgpu::LoadOp<wgpu::Color>,
8569 ) -> Result<(), String> {
8570 self.render_range_with_layer_events_to_target_recorded(
8571 target,
8572 shapes,
8573 brushes,
8574 images,
8575 texts,
8576 shadow_draws,
8577 retained_draws,
8578 draw_ops,
8579 effect_layers,
8580 backdrop_layers,
8581 backdrop_input_hashes,
8582 z_start,
8583 z_end,
8584 excluded_effect_layer,
8585 width,
8586 height,
8587 root_scale,
8588 backdrop_underlay,
8589 initial_load_op,
8590 )
8591 }
8592
8593 fn render_shadow_draw(
8594 &mut self,
8595 target_view: &wgpu::TextureView,
8596 shadow: &ShadowDraw,
8597 width: u32,
8598 height: u32,
8599 root_scale: f32,
8600 ) {
8601 self.renderer.encode_shadow_draw(
8602 self.recorder,
8603 target_view,
8604 shadow,
8605 width,
8606 height,
8607 root_scale,
8608 );
8609 }
8610
8611 fn composite_to_view_projective(
8612 &mut self,
8613 source: &OffscreenTarget,
8614 dest_view: &wgpu::TextureView,
8615 viewport: (u32, u32),
8616 source_size: (f32, f32),
8617 inverse_matrix: [[f32; 3]; 3],
8618 dest_bounds: [[f32; 2]; 4],
8619 alpha: f32,
8620 load_op: wgpu::LoadOp<wgpu::Color>,
8621 scissor: Option<(u32, u32, u32, u32)>,
8622 blend_mode: BlendMode,
8623 sample_mode: CompositeSampleMode,
8624 ) {
8625 let device = self.renderer.device.clone();
8626 let composited = {
8627 self.renderer
8628 .effect_renderer
8629 .encode_composite_to_view_projective(
8630 self.recorder,
8631 &device,
8632 source,
8633 dest_view,
8634 viewport,
8635 source_size,
8636 inverse_matrix,
8637 dest_bounds,
8638 alpha,
8639 load_op,
8640 scissor,
8641 supported_blend_mode(blend_mode),
8642 sample_mode,
8643 )
8644 };
8645 if composited {
8646 self.recorder.record_pass();
8647 self.renderer.effect_renderer.record_composite_pass();
8648 }
8649 }
8650
8651 fn composite_projective_surfaces_to_view(
8652 &mut self,
8653 dest_view: &wgpu::TextureView,
8654 viewport: (u32, u32),
8655 composites: &[ProjectiveSurfaceComposite<'_>],
8656 ) {
8657 let device = self.renderer.device.clone();
8658 let mut composite_count = 0_u32;
8659 for composite in composites
8660 .iter()
8661 .copied()
8662 .filter(|composite| projective_dest_bounds_rect(composite.dest_bounds).is_some())
8663 {
8664 let composited = {
8665 self.renderer
8666 .effect_renderer
8667 .encode_composite_to_view_projective(
8668 self.recorder,
8669 &device,
8670 composite.source,
8671 dest_view,
8672 viewport,
8673 composite.source_size,
8674 composite.inverse_matrix,
8675 composite.dest_bounds,
8676 composite.alpha,
8677 composite.load_op,
8678 composite.scissor,
8679 supported_blend_mode(composite.blend_mode),
8680 composite.sample_mode,
8681 )
8682 };
8683 if composited {
8684 composite_count = composite_count.saturating_add(1);
8685 }
8686 }
8687 if composite_count > 0 {
8688 self.recorder.record_passes(composite_count);
8689 self.renderer
8690 .effect_renderer
8691 .debug_composites
8692 .set(self.renderer.effect_renderer.debug_composites.get() + composite_count);
8693 }
8694 }
8695
8696 fn composite_surface_batch_to_view(
8697 &mut self,
8698 dest_view: &wgpu::TextureView,
8699 viewport: (u32, u32),
8700 load_op: wgpu::LoadOp<wgpu::Color>,
8701 composites: &[CompositeBatchItem<'_>],
8702 ) {
8703 if composites.is_empty() {
8704 return;
8705 }
8706 let device = self.renderer.device.clone();
8707 self.renderer
8708 .effect_renderer
8709 .encode_composite_batch_to_view_pass(
8710 self.recorder,
8711 &device,
8712 dest_view,
8713 viewport,
8714 load_op,
8715 composites,
8716 );
8717 self.recorder.record_pass();
8718 self.renderer.effect_renderer.record_composite_pass();
8719 }
8720
8721 fn copy_texture_region_to_target(
8722 &mut self,
8723 source: &OffscreenTarget,
8724 source_origin: (u32, u32),
8725 target: &OffscreenTarget,
8726 size: (u32, u32),
8727 ) -> bool {
8728 let (width, height) = size;
8729 if width == 0 || height == 0 || width > target.width || height > target.height {
8730 return false;
8731 }
8732 let Some(source_right) = source_origin.0.checked_add(width) else {
8733 return false;
8734 };
8735 let Some(source_bottom) = source_origin.1.checked_add(height) else {
8736 return false;
8737 };
8738 if source_right > source.width || source_bottom > source.height {
8739 return false;
8740 }
8741
8742 self.recorder.encoder().copy_texture_to_texture(
8743 wgpu::TexelCopyTextureInfo {
8744 texture: source.texture(),
8745 mip_level: 0,
8746 origin: wgpu::Origin3d {
8747 x: source_origin.0,
8748 y: source_origin.1,
8749 z: 0,
8750 },
8751 aspect: wgpu::TextureAspect::All,
8752 },
8753 wgpu::TexelCopyTextureInfo {
8754 texture: target.texture(),
8755 mip_level: 0,
8756 origin: wgpu::Origin3d::ZERO,
8757 aspect: wgpu::TextureAspect::All,
8758 },
8759 wgpu::Extent3d {
8760 width,
8761 height,
8762 depth_or_array_layers: 1,
8763 },
8764 );
8765 true
8766 }
8767
8768 fn shader_composite_batch_to_view(
8769 &mut self,
8770 dest_view: &wgpu::TextureView,
8771 viewport: (u32, u32),
8772 load_op: wgpu::LoadOp<wgpu::Color>,
8773 composites: &[ShaderCompositeBatchItem<'_>],
8774 ) -> bool {
8775 if composites.is_empty() {
8776 return true;
8777 }
8778 let device = self.renderer.device.clone();
8779 let encoded = self
8780 .renderer
8781 .effect_renderer
8782 .encode_shader_batch_src_over_to_view(
8783 self.recorder,
8784 &device,
8785 dest_view,
8786 viewport,
8787 load_op,
8788 composites,
8789 );
8790 if encoded {
8791 self.recorder.record_pass();
8792 self.renderer.effect_renderer.record_composite_pass();
8793 self.renderer
8794 .effect_renderer
8795 .debug_effects
8796 .set(self.renderer.effect_renderer.debug_effects.get() + composites.len() as u32);
8797 }
8798 encoded
8799 }
8800
8801 fn composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
8802 &mut self,
8803 source: &OffscreenTarget,
8804 dest_view: &wgpu::TextureView,
8805 alpha: f32,
8806 load_op: wgpu::LoadOp<wgpu::Color>,
8807 scissor: Option<(u32, u32, u32, u32)>,
8808 rounded_mask: Option<RoundedCompositeMask>,
8809 blend_mode: BlendMode,
8810 dest_viewport: Option<(f32, f32, f32, f32)>,
8811 sample_mode: CompositeSampleMode,
8812 ) {
8813 let device = self.renderer.device.clone();
8814 {
8815 self.renderer
8816 .effect_renderer
8817 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
8818 self.recorder,
8819 &device,
8820 source,
8821 dest_view,
8822 alpha,
8823 load_op,
8824 scissor,
8825 rounded_mask,
8826 supported_blend_mode(blend_mode),
8827 dest_viewport,
8828 sample_mode,
8829 );
8830 }
8831 self.recorder.record_pass();
8832 self.renderer.effect_renderer.record_composite_pass();
8833 }
8834
8835 fn apply_effect_and_composite_to_view(
8836 &mut self,
8837 source: &OffscreenTarget,
8838 effect: &RenderEffect,
8839 effect_rect: [f32; 4],
8840 dest_view: &wgpu::TextureView,
8841 alpha: f32,
8842 load_op: wgpu::LoadOp<wgpu::Color>,
8843 scissor: Option<(u32, u32, u32, u32)>,
8844 blend_mode: BlendMode,
8845 dest_viewport: Option<(f32, f32, f32, f32)>,
8846 sample_mode: CompositeSampleMode,
8847 ) -> Result<(), String> {
8848 self.record_effect_composite(
8849 source,
8850 effect,
8851 effect_rect,
8852 dest_view,
8853 alpha,
8854 load_op,
8855 scissor,
8856 blend_mode,
8857 dest_viewport,
8858 sample_mode,
8859 )
8860 }
8861
8862 fn apply_shader_and_composite_to_view(
8863 &mut self,
8864 source: &OffscreenTarget,
8865 shader: &RuntimeShader,
8866 effect_rect: [f32; 4],
8867 dest_view: &wgpu::TextureView,
8868 alpha: f32,
8869 load_op: wgpu::LoadOp<wgpu::Color>,
8870 scissor: Option<(u32, u32, u32, u32)>,
8871 blend_mode: BlendMode,
8872 dest_viewport: Option<(f32, f32, f32, f32)>,
8873 sample_mode: CompositeSampleMode,
8874 ) {
8875 self.record_shader_composite(
8876 source,
8877 shader,
8878 effect_rect,
8879 dest_view,
8880 alpha,
8881 load_op,
8882 scissor,
8883 blend_mode,
8884 dest_viewport,
8885 sample_mode,
8886 );
8887 }
8888
8889 fn apply_shader_and_composite_to_view_projective(
8890 &mut self,
8891 source: &OffscreenTarget,
8892 shader: &RuntimeShader,
8893 effect_rect: [f32; 4],
8894 dest_view: &wgpu::TextureView,
8895 viewport: (u32, u32),
8896 source_size: (f32, f32),
8897 inverse_matrix: [[f32; 3]; 3],
8898 dest_bounds: [[f32; 2]; 4],
8899 alpha: f32,
8900 load_op: wgpu::LoadOp<wgpu::Color>,
8901 scissor: Option<(u32, u32, u32, u32)>,
8902 blend_mode: BlendMode,
8903 sample_mode: CompositeSampleMode,
8904 ) {
8905 self.record_shader_projective_composite(
8906 source,
8907 shader,
8908 effect_rect,
8909 dest_view,
8910 viewport,
8911 source_size,
8912 inverse_matrix,
8913 dest_bounds,
8914 alpha,
8915 load_op,
8916 scissor,
8917 blend_mode,
8918 sample_mode,
8919 );
8920 }
8921
8922 fn apply_effect_and_composite_to_view_projective(
8923 &mut self,
8924 source: &OffscreenTarget,
8925 effect: &RenderEffect,
8926 effect_rect: [f32; 4],
8927 dest_view: &wgpu::TextureView,
8928 viewport: (u32, u32),
8929 source_size: (f32, f32),
8930 inverse_matrix: [[f32; 3]; 3],
8931 dest_bounds: [[f32; 2]; 4],
8932 alpha: f32,
8933 load_op: wgpu::LoadOp<wgpu::Color>,
8934 scissor: Option<(u32, u32, u32, u32)>,
8935 blend_mode: BlendMode,
8936 sample_mode: CompositeSampleMode,
8937 ) -> Result<(), String> {
8938 self.record_effect_projective_composite(
8939 source,
8940 effect,
8941 effect_rect,
8942 dest_view,
8943 viewport,
8944 source_size,
8945 inverse_matrix,
8946 dest_bounds,
8947 alpha,
8948 load_op,
8949 scissor,
8950 blend_mode,
8951 sample_mode,
8952 )
8953 }
8954
8955 fn is_render_effect_supported(&self, effect: &RenderEffect) -> bool {
8956 self.renderer.supports_render_effect(effect)
8957 }
8958
8959 fn warn_unsupported_effect_once(&self) {
8960 self.renderer.warning_state.warn_unsupported_effect_once();
8961 }
8962
8963 fn record_layer_cache_miss(&self, width: u32, height: u32) {
8964 self.renderer
8965 .frame_stats
8966 .record_layer_cache_miss(width, height);
8967 }
8968
8969 fn record_isolated_layer_render(
8970 &self,
8971 width: u32,
8972 height: u32,
8973 node_id: Option<NodeId>,
8974 logical_rect: Rect,
8975 requirements: SurfaceRequirementSet,
8976 ) {
8977 self.renderer.frame_stats.record_isolated_layer_render(
8978 width,
8979 height,
8980 node_id,
8981 logical_rect,
8982 requirements.into(),
8983 );
8984 }
8985}
8986
8987impl GpuRenderer {
8988 #[allow(clippy::too_many_arguments)]
8989 pub fn render(
8990 &mut self,
8991 view: &wgpu::TextureView,
8992 width: u32,
8993 height: u32,
8994 packet: FramePacket,
8995 surface_epoch: u64,
8996 returns: &mut RenderReturns,
8997 ) -> Result<(), String> {
8998 self.render_internal(
8999 width,
9000 height,
9001 packet,
9002 surface_epoch,
9003 returns,
9004 OutputMode::Display,
9005 Some(view),
9006 )
9007 }
9008
9009 #[allow(clippy::too_many_arguments)]
9010 fn render_internal(
9011 &mut self,
9012 width: u32,
9013 height: u32,
9014 mut packet: FramePacket,
9015 surface_epoch: u64,
9016 returns: &mut RenderReturns,
9017 output_mode: OutputMode,
9018 output_view: Option<&wgpu::TextureView>,
9019 ) -> Result<(), String> {
9020 if let Some(confirmations) = packet.recycled_confirmations.take() {
9025 self.restore_replay_ack_confirmations(confirmations);
9026 }
9027 let cancel_reason = if packet.renderer_epoch != self.renderer_epoch {
9033 Some(CancelReason::RendererEpoch)
9034 } else if packet.surface_epoch != surface_epoch {
9035 Some(CancelReason::SurfaceEpoch)
9036 } else if packet.viewport != (width, height) {
9037 Some(CancelReason::Viewport)
9038 } else {
9039 None
9040 };
9041 if let Some(reason) = cancel_reason {
9042 return Self::cancel_packet(packet, reason, returns);
9043 }
9044 if self.device_errors.take_poison() {
9051 return Self::cancel_packet(packet, CancelReason::DeviceError, returns);
9052 }
9053 returns.frame_id = packet.frame_id;
9054 log::trace!("🎨 Rendering graph to {}x{}", width, height);
9055 let render_start = Instant::now();
9056
9057 #[cfg(target_arch = "wasm32")]
9058 {
9059 self.wasm_uniform_batch_cursor = 0;
9060 self.wasm_shape_batch_cursor = 0;
9061 self.wasm_image_batch_cursor = 0;
9062 }
9063 #[cfg(not(target_arch = "wasm32"))]
9064 {
9065 self.retained_glyph_uniform_cursor = 0;
9066 self.rim_mesh_vertices.clear();
9071 self.rim_mesh_indices.clear();
9072 self.rim_mesh_uploaded_vertices = 0;
9073 self.rim_mesh_uploaded_indices = 0;
9074 if fill_area_diag_enabled() {
9075 self.fill_area_diag.reset_frame(width, height);
9076 }
9077 self.static_span.armed = true;
9080 self.segment_surfaces.begin_frame();
9083 }
9087
9088 let text_cache_len = packet.text_cache_len;
9092 let composition = self.take_composition_target(width.max(1), height.max(1));
9093 #[cfg(not(target_arch = "wasm32"))]
9094 {
9095 self.display_clip.frame_root_view = Some(composition.target.view.clone());
9096 }
9097 let composition_root = Some(&composition.target);
9098 let screenshot_bind_group = output_view.and_then(|_| {
9099 matches!(output_mode, OutputMode::Screenshot).then(|| {
9100 self.screenshot_converter
9101 .bind_group(&self.device, &composition.target.view)
9102 })
9103 });
9104 let output = output_view.map(|view| {
9105 let bind_group = screenshot_bind_group
9106 .as_ref()
9107 .unwrap_or(&composition.output_bind_group);
9108 (view, bind_group)
9109 });
9110 let result = self.render_graph(
9111 &composition.target.view,
9112 composition_root,
9113 packet,
9114 returns,
9115 output_mode,
9116 output,
9117 );
9118 self.composition_target = Some(composition);
9119 #[cfg(not(target_arch = "wasm32"))]
9120 {
9121 self.display_clip.frame_root_view = None;
9122 }
9123 let after_graph = Instant::now();
9124 self.flush_deferred_offscreen_releases();
9125 #[cfg(not(target_arch = "wasm32"))]
9126 {
9127 if fill_area_diag_enabled() {
9128 let (composite_px2, offscreen_px2) = self.effect_renderer.take_fill_diag_fill_px2();
9132 self.fill_area_diag
9133 .add_effect_fill(composite_px2, offscreen_px2);
9134 self.fill_area_diag.finish_frame(width, height);
9135 }
9136 }
9137
9138 #[cfg(target_arch = "wasm32")]
9139 {
9140 const WASM_BATCH_POOL_MARGIN: usize = 4;
9141 self.wasm_uniform_batches.truncate(
9142 self.wasm_uniform_batch_cursor
9143 .saturating_add(WASM_BATCH_POOL_MARGIN),
9144 );
9145 self.wasm_shape_batches.truncate(
9146 self.wasm_shape_batch_cursor
9147 .saturating_add(WASM_BATCH_POOL_MARGIN),
9148 );
9149 self.wasm_image_batches.truncate(
9150 self.wasm_image_batch_cursor
9151 .saturating_add(WASM_BATCH_POOL_MARGIN),
9152 );
9153 }
9154 self.staged_uploads
9155 .shrink_retained_capacity(RETAINED_STAGED_UPLOAD_BYTES, RETAINED_STAGED_UPLOAD_COPIES);
9156
9157 self.layer_surface_cache.finish_frame(&self.frame_stats);
9158 for target in self.layer_surface_cache.take_recycled() {
9159 self.defer_offscreen_release(target);
9160 }
9161 #[cfg(not(target_arch = "wasm32"))]
9162 self.retained_bundle_cache.end_frame();
9163
9164 self.frame_stats.offscreen_pool_size.set(
9165 self.effect_renderer
9166 .retained_offscreen_count()
9167 .saturating_add(self.frame_graph_executor.retained_texture_count())
9168 .saturating_add(usize::from(self.composition_target.is_some())) as u32,
9169 );
9170 self.frame_stats.offscreen_pool_bytes.set(
9171 (self.effect_renderer.retained_offscreen_bytes() as u64)
9172 .saturating_add(self.frame_graph_executor.retained_texture_bytes())
9173 .saturating_add(
9174 self.composition_target
9175 .as_ref()
9176 .map(|target| {
9177 u64::from(target.target.width)
9178 .saturating_mul(u64::from(target.target.height))
9179 .saturating_mul(composition_bytes_per_pixel())
9180 })
9181 .unwrap_or(0),
9182 ),
9183 );
9184 self.frame_stats
9185 .text_pool_size
9186 .set(self.text_image_cache.len() as u32);
9187 self.frame_stats
9188 .image_cache_size
9189 .set(self.image_texture_cache.len() as u32);
9190 self.frame_stats.text_cache_size.set(text_cache_len as u32);
9191 self.effect_renderer
9192 .merge_and_reset_debug_counters(&self.frame_stats);
9193 self.frame_graph_executor.reset_upload_allocators();
9194 let snapshot = self.frame_stats.snapshot();
9195 self.last_frame_stats = Some(snapshot);
9196 PRESENTED_FRAMES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
9197 update_frame_warmup_budget(&mut self.pending_frame_warmup_frames, &snapshot);
9198 self.frame_stats.maybe_print_snapshot(
9199 snapshot,
9200 &mut self.frame_count,
9201 self.gpu_stats_enabled,
9202 );
9203 if self.gpu_stats_enabled && self.frame_count.is_multiple_of(60) {
9204 gpu_stats::print_gpu_memory_report(&self.device, self.frame_count);
9205 }
9206 self.frame_stats.reset();
9207 let after_stats = Instant::now();
9208 if let Some(total_ms) = should_log_wgpu_render_stage(render_start, after_stats) {
9209 log::warn!(
9210 "[wgpu-render-stage:render] total_ms={total_ms:.2} graph_ms={:.2} cleanup_stats_ms={:.2}",
9211 instant_ms(render_start, after_graph),
9212 instant_ms(after_graph, after_stats),
9213 );
9214 }
9215 if result.is_ok() {
9216 returns.outcome = PresentOutcome::Presented;
9219 }
9220 result
9221 }
9222
9223 pub(crate) fn cancel_packet(
9236 packet: FramePacket,
9237 reason: CancelReason,
9238 returns: &mut RenderReturns,
9239 ) -> Result<(), String> {
9240 let FramePacket {
9241 frame_id,
9242 viewport: _,
9243 renderer_epoch: _,
9244 surface_epoch: _,
9245 root_scale: _,
9246 root,
9247 overlay: _,
9248 replay,
9249 text_cache_len: _,
9250 recycled_confirmations: _,
9251 replay_preconsumed,
9252 } = packet;
9253 match root {
9254 PacketRoot::Direct(root) => {
9255 returns.scene = Some(root.scene);
9265 #[cfg(not(target_arch = "wasm32"))]
9266 if !replay_preconsumed {
9267 returns.cancelled_replay = Some(replay);
9268 }
9269 }
9270 PacketRoot::Surface(_) => {}
9271 }
9272 #[cfg(target_arch = "wasm32")]
9273 let _ = (replay, replay_preconsumed);
9274 returns.ack = None;
9275 returns.frame_id = frame_id;
9276 returns.outcome = PresentOutcome::Cancelled(reason);
9277 Ok(())
9278 }
9279
9280 pub fn last_frame_stats(&self) -> Option<gpu_stats::FrameStatsSnapshot> {
9281 self.last_frame_stats
9282 }
9283
9284 pub fn needs_frame_warmup(&self) -> bool {
9285 self.pending_frame_warmup_frames > 0
9286 }
9287
9288 pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
9289 let layer_surface_cache_stats = self.layer_surface_cache.debug_stats();
9290 DebugCpuAllocationStats {
9291 scene_graph_node_count: 0,
9292 scene_graph_heap_bytes: 0,
9293 scene_hits_len: 0,
9294 scene_hits_cap: 0,
9295 scene_node_index_len: 0,
9296 scene_node_index_cap: 0,
9297 text_renderer_pool_len: self.text_image_cache.len(),
9298 text_renderer_pool_cap: self.text_image_cache.cap().get(),
9299 swash_image_cache_len: 0,
9300 swash_image_cache_cap: 0,
9301 swash_outline_cache_len: 0,
9302 swash_outline_cache_cap: 0,
9303 image_texture_cache_len: self.image_texture_cache.len(),
9304 image_texture_cache_cap: self.image_texture_cache.cap().get(),
9305 scratch_shape_data_cap: self.scratch_shape_data.capacity(),
9306 scratch_gradients_cap: self.scratch_gradients.capacity(),
9307 scratch_image_vertices_cap: self.scratch_image_vertices.capacity(),
9308 scratch_image_indices_cap: self.scratch_image_indices.capacity(),
9309 scratch_image_cmds_cap: self.scratch_image_cmds.capacity(),
9310 scratch_segment_items_cap: self.scratch_segment_items.capacity(),
9311 scratch_effect_ranges_cap: self.scratch_effect_ranges.capacity(),
9312 scratch_layer_events_cap: self.scratch_layer_events.capacity(),
9313 staged_upload_bytes_cap: self.staged_uploads.bytes.capacity(),
9314 staged_upload_copies_cap: self.staged_uploads.copies.capacity(),
9315 layer_surface_cache_len: layer_surface_cache_stats.entries_len,
9316 layer_surface_cache_cap: layer_surface_cache_stats.entries_cap,
9317 layer_surface_cache_identity_len: layer_surface_cache_stats.identity_len,
9318 layer_surface_cache_identity_cap: layer_surface_cache_stats.identity_cap,
9319 layer_surface_rect_cache_len: 0,
9322 layer_surface_rect_cache_cap: 0,
9323 layer_surface_requirements_cache_len: 0,
9324 layer_surface_requirements_cache_cap: 0,
9325 layer_cache_seen_this_frame_len: layer_surface_cache_stats.seen_this_frame_len,
9326 layer_cache_seen_this_frame_cap: layer_surface_cache_stats.seen_this_frame_cap,
9327 }
9328 }
9329
9330 pub fn render_to_rgba_pixels(
9331 &mut self,
9332 width: u32,
9333 height: u32,
9334 packet: FramePacket,
9335 surface_epoch: u64,
9336 returns: &mut RenderReturns,
9337 ) -> Result<Vec<u8>, String> {
9338 if width == 0 || height == 0 {
9339 return Err("Screenshot size must be non-zero".to_string());
9340 }
9341
9342 let output_texture = self.device.create_texture(&wgpu::TextureDescriptor {
9343 label: Some("Screenshot Output Texture"),
9344 size: wgpu::Extent3d {
9345 width,
9346 height,
9347 depth_or_array_layers: 1,
9348 },
9349 mip_level_count: 1,
9350 sample_count: 1,
9351 dimension: wgpu::TextureDimension::D2,
9352 format: wgpu::TextureFormat::Rgba8Unorm,
9353 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
9354 view_formats: &[],
9355 });
9356 let output_view = output_texture.create_view(&wgpu::TextureViewDescriptor::default());
9357 self.render_internal(
9358 width,
9359 height,
9360 packet,
9361 surface_epoch,
9362 returns,
9363 OutputMode::Screenshot,
9364 Some(&output_view),
9365 )?;
9366
9367 let bytes_per_pixel = 4u32;
9368 let unpadded_bytes_per_row = width
9369 .checked_mul(bytes_per_pixel)
9370 .ok_or_else(|| "Screenshot row byte size overflow".to_string())?;
9371 let padded_bytes_per_row =
9372 align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
9373 let output_buffer_size = padded_bytes_per_row as u64 * height as u64;
9374
9375 let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
9376 label: Some("Screenshot Readback Buffer"),
9377 size: output_buffer_size,
9378 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
9379 mapped_at_creation: false,
9380 });
9381
9382 let device = self.device.clone();
9383 let queue = self.queue.clone();
9384 let mut graph = WgpuFrameGraph::new(Some("Screenshot Copy Encoder"));
9385 let source = graph.import_surface("screenshot-copy-source");
9386 graph.add_fallible_command_pass(Some("Screenshot Copy Pass"), &[source], &[], |context| {
9387 context.encoder.copy_texture_to_buffer(
9388 wgpu::TexelCopyTextureInfo {
9389 texture: &output_texture,
9390 mip_level: 0,
9391 origin: wgpu::Origin3d::ZERO,
9392 aspect: wgpu::TextureAspect::All,
9393 },
9394 wgpu::TexelCopyBufferInfo {
9395 buffer: &output_buffer,
9396 layout: wgpu::TexelCopyBufferLayout {
9397 offset: 0,
9398 bytes_per_row: Some(padded_bytes_per_row),
9399 rows_per_image: Some(height),
9400 },
9401 },
9402 wgpu::Extent3d {
9403 width,
9404 height,
9405 depth_or_array_layers: 1,
9406 },
9407 );
9408 Ok(())
9409 });
9410 let mut executor = std::mem::take(&mut self.frame_graph_executor);
9411 let execution = executor.execute_recorded_graph(&device, &queue, graph);
9412 self.frame_graph_executor = executor;
9413 let execution = execution.map_err(|error| error.to_string())?;
9414 let submission_index = execution.submission;
9415 let copy_stats = execution.stats;
9416 self.last_frame_stats = self
9417 .last_frame_stats
9418 .map(|snapshot| snapshot.with_command_stats_added(copy_stats));
9419
9420 let buffer_slice = output_buffer.slice(..);
9421 let (tx, rx) = mpsc::channel();
9422 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
9423 let _ = tx.send(result);
9424 });
9425 let _ = self.device.poll(wgpu::PollType::Wait {
9426 submission_index: Some(submission_index),
9427 timeout: None,
9428 });
9429
9430 match rx.recv_timeout(Duration::from_secs(3)) {
9431 Ok(Ok(())) => {}
9432 Ok(Err(err)) => return Err(format!("Screenshot map_async failed: {err:?}")),
9433 Err(err) => return Err(format!("Screenshot readback timed out: {err}")),
9434 }
9435
9436 let mapped = buffer_slice.get_mapped_range();
9437 let mut pixels = vec![0u8; (width as usize) * (height as usize) * 4];
9438
9439 let src_row_len = padded_bytes_per_row as usize;
9440 let dst_row_len = unpadded_bytes_per_row as usize;
9441 for row in 0..height as usize {
9442 let src_offset = row * src_row_len;
9443 let dst_offset = row * dst_row_len;
9444 pixels[dst_offset..dst_offset + dst_row_len]
9445 .copy_from_slice(&mapped[src_offset..src_offset + dst_row_len]);
9446 }
9447 drop(mapped);
9448 output_buffer.unmap();
9449
9450 self.convert_surface_pixels_to_rgba(&pixels)
9451 }
9452
9453 fn render_graph(
9454 &mut self,
9455 surface_view: &wgpu::TextureView,
9456 root_target: Option<&OffscreenTarget>,
9457 packet: FramePacket,
9458 returns: &mut RenderReturns,
9459 output_mode: OutputMode,
9460 output: Option<(&wgpu::TextureView, &wgpu::BindGroup)>,
9461 ) -> Result<(), String> {
9462 let device = self.device.clone();
9463 let queue = self.queue.clone();
9464 let graph_start = Instant::now();
9465
9466 #[cfg(not(target_arch = "wasm32"))]
9467 {
9468 let mut executor = std::mem::take(&mut self.frame_graph_executor);
9469 let mut frame_graph = WgpuFrameGraph::new(Some("Renderer Frame Graph"));
9470 let surface = frame_graph.import_surface("renderer-surface");
9471 frame_graph.add_fallible_recorded_command_pass(
9472 Some("Renderer Frame Pass"),
9473 &[],
9474 &[surface],
9475 |frame_encoder| {
9476 self.render_graph_recorded(
9477 surface_view,
9478 root_target,
9479 packet,
9480 returns,
9481 frame_encoder,
9482 )?;
9483 if let Some((output_view, bind_group)) = output {
9484 match output_mode {
9485 OutputMode::Display => &self.output_converter,
9486 OutputMode::Screenshot => &self.screenshot_converter,
9487 }
9488 .encode(
9489 &self.device,
9490 frame_encoder.encoder(),
9491 output_view,
9492 bind_group,
9493 self.adapter_backend,
9494 );
9495 frame_encoder.record_pass();
9496 }
9497 Ok(())
9498 },
9499 );
9500 let after_build = Instant::now();
9501 let execution = executor.execute_recorded_graph(&device, &queue, frame_graph);
9502 let after_execute = Instant::now();
9503 self.frame_graph_executor = executor;
9504 if let Some(total_ms) = should_log_wgpu_render_stage(graph_start, after_execute) {
9505 log::warn!(
9506 "[wgpu-render-stage:graph] total_ms={total_ms:.2} build_ms={:.2} execute_ms={:.2}",
9507 instant_ms(graph_start, after_build),
9508 instant_ms(after_build, after_execute),
9509 );
9510 }
9511
9512 match execution {
9513 Ok(execution) => {
9514 if execution.stats.pass_count > 0 {
9515 self.frame_stats.record_command_stats(execution.stats);
9516 }
9517 Ok(())
9518 }
9519 Err(crate::frame_graph::FrameGraphError::NoDeclaredPasses) => Ok(()),
9520 Err(error) => Err(error.to_string()),
9521 }
9522 }
9523
9524 #[cfg(target_arch = "wasm32")]
9525 {
9526 let mut executor = std::mem::take(&mut self.frame_graph_executor);
9527 let (result, execution) = {
9528 let mut frame_encoder =
9529 executor.begin(&device, &queue, Some("Renderer Frame Encoder"));
9530 let initial_pass_count = frame_encoder.recorded_pass_count();
9531 let result = self.render_graph_recorded(
9532 surface_view,
9533 root_target,
9534 packet,
9535 returns,
9536 &mut frame_encoder,
9537 );
9538 if result.is_ok() {
9539 if let Some((output_view, bind_group)) = output {
9540 match output_mode {
9541 OutputMode::Display => &self.output_converter,
9542 OutputMode::Screenshot => &self.screenshot_converter,
9543 }
9544 .encode(
9545 &self.device,
9546 frame_encoder.encoder(),
9547 output_view,
9548 bind_group,
9549 self.adapter_backend,
9550 );
9551 frame_encoder.record_pass();
9552 }
9553 }
9554 let execution =
9555 if result.is_ok() && frame_encoder.recorded_pass_count() > initial_pass_count {
9556 Some(frame_encoder.finish())
9557 } else {
9558 None
9559 };
9560 (result, execution)
9561 };
9562 let after_execute = Instant::now();
9563 self.frame_graph_executor = executor;
9564 if let Some(total_ms) = should_log_wgpu_render_stage(graph_start, after_execute) {
9565 log::warn!("[wgpu-render-stage:graph] total_ms={total_ms:.2}",);
9566 }
9567 if let Some(execution) = execution {
9568 self.frame_stats.record_command_stats(execution.stats);
9569 }
9570 result
9571 }
9572 }
9573
9574 fn render_graph_recorded<C: FrameCommandRecorder>(
9575 &mut self,
9576 surface_view: &wgpu::TextureView,
9577 root_target: Option<&OffscreenTarget>,
9578 packet: FramePacket,
9579 returns: &mut RenderReturns,
9580 frame_encoder: &mut C,
9581 ) -> Result<(), String> {
9582 let recorded_start = Instant::now();
9583
9584 #[cfg(not(target_arch = "wasm32"))]
9599 let mut packet = packet;
9600 #[cfg(not(target_arch = "wasm32"))]
9601 if !packet.replay_preconsumed {
9602 if let PacketRoot::Direct(root) = &packet.root {
9603 let ops = std::mem::take(&mut packet.replay);
9604 let (ack, recycled) = self.consume_replay_ops(
9605 ops,
9606 &root.scene.shapes,
9607 &root.scene.brushes,
9608 packet.root_scale,
9609 );
9610 returns.ack = Some((ack, recycled));
9611 }
9612 }
9613
9614 let FramePacket {
9615 frame_id,
9616 viewport: (width, height),
9617 renderer_epoch: _,
9618 surface_epoch: _,
9619 root_scale,
9620 root,
9621 overlay,
9622 replay: _,
9623 text_cache_len: _,
9624 recycled_confirmations: _,
9625 replay_preconsumed: _,
9626 } = packet;
9627
9628 let mut backend = RecordingSurfaceBackend {
9629 renderer: self,
9630 recorder: frame_encoder,
9631 };
9632
9633 let surface_packet = match root {
9634 PacketRoot::Direct(root) => {
9635 let direct_render_start = Instant::now();
9636 let result = match execute_render_root_direct(
9637 &mut backend,
9638 surface_view,
9639 root_target,
9640 *root,
9641 width,
9642 height,
9643 root_scale,
9644 wgpu::LoadOp::Clear(CLEAR_COLOR),
9645 ) {
9646 Ok(scene) => {
9651 returns.scene = Some(scene);
9652 Ok(())
9653 }
9654 Err((error, scene)) => {
9655 returns.scene = Some(scene);
9656 Err(error)
9657 }
9658 };
9659 if result.is_ok() {
9660 if let Some(overlay) = overlay {
9661 Self::render_overlay_packet(
9662 &mut backend,
9663 surface_view,
9664 overlay,
9665 width,
9666 height,
9667 root_scale,
9668 )?;
9669 }
9670 }
9671 let after_direct_render = Instant::now();
9672 if let Some(total_ms) =
9673 should_log_wgpu_render_stage(recorded_start, after_direct_render)
9674 {
9675 log::warn!(
9676 "[wgpu-render-stage:recorded-direct-root] frame={frame_id} total_ms={total_ms:.2} render_ms={:.2}",
9677 instant_ms(direct_render_start, after_direct_render),
9678 );
9679 }
9680 return result;
9681 }
9682 PacketRoot::Surface(surface_packet) => surface_packet,
9683 };
9684 let after_root_collect = Instant::now();
9685
9686 let RootSurfacePacket {
9687 lowered,
9688 source,
9689 transform_to_parent,
9690 node_id,
9691 backdrop,
9692 graphics_layer,
9693 local_bounds,
9694 clip_rect,
9695 shadow_clip,
9696 } = *surface_packet;
9697 let mut lowered = lowered;
9698 lowered.source = source;
9699
9700 let viewport_rect = Rect {
9705 x: 0.0,
9706 y: 0.0,
9707 width: width as f32 / root_scale,
9708 height: height as f32 / root_scale,
9709 };
9710 let root_surface = execute_render_layer_surface(
9711 &mut backend,
9712 &mut lowered,
9713 LayerSurfaceRequest {
9714 root_scale,
9715 backdrop_underlay: None,
9716 backdrop_underlay_color: None,
9717 allow_runtime_cache: false,
9718 logical_rect_override: Some(viewport_rect),
9719 capture_clip_override: None,
9720 activates_nested_capture: false,
9721 translation_context: TranslationRenderContext::default(),
9722 },
9723 )?;
9724 let root_quad = transform_to_parent.map_rect(root_surface.logical_rect);
9725 let root_dest_quad = scaled_quad(root_quad, root_scale);
9726
9727 let needs_root_composite_target =
9728 backdrop.is_some() || graphics_layer.shadow_elevation > 0.0;
9729
9730 if needs_root_composite_target {
9731 let composite_target = backend.acquire_frame_surface(width, height);
9732 backend.clear_target_view_with_load_op(
9733 &composite_target.view,
9734 wgpu::LoadOp::Clear(CLEAR_COLOR),
9735 );
9736
9737 if let Some(backdrop) = &backdrop {
9738 execute_apply_backdrop_layer_to_target(
9739 &mut backend,
9740 &composite_target,
9741 &BackdropLayer {
9742 node_id,
9743 rect: quad_bounds(transform_to_parent.map_rect(local_bounds)),
9744 clip: clip_rect.map(|clip| quad_bounds(transform_to_parent.map_rect(clip))),
9745 snap_anchor: None,
9746 effect: backdrop.clone(),
9747 z_index: 0,
9748 },
9749 None,
9750 width,
9751 height,
9752 root_scale,
9753 None,
9754 )?;
9755 }
9756
9757 let mut root_shadow_scene = CompositorScene::new();
9758 let root_shadow_clip =
9759 shadow_clip.map(|clip| quad_bounds(transform_to_parent.map_rect(clip)));
9760 push_layer_shadow(
9761 &mut root_shadow_scene,
9762 &graphics_layer,
9763 local_bounds,
9764 quad_bounds(transform_to_parent.map_rect(local_bounds)),
9765 root_shadow_clip,
9766 );
9767 for shadow in &root_shadow_scene.shadow_draws {
9768 backend.render_shadow_draw(
9769 &composite_target.view,
9770 shadow,
9771 width,
9772 height,
9773 root_scale,
9774 );
9775 }
9776
9777 let composite_dest_quad =
9778 snap_motion_stable_dest_quad(root_dest_quad, root_surface.sample_mode);
9779 execute_composite_surface_to_view(
9780 &mut backend,
9781 root_surface.target.target(),
9782 &composite_target.view,
9783 (width, height),
9784 composite_dest_quad,
9785 root_surface.composite_alpha,
9786 wgpu::LoadOp::Load,
9787 None,
9788 root_surface.blend_mode,
9789 root_surface.sample_mode,
9790 )?;
9791 backend.composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
9792 &composite_target,
9793 surface_view,
9794 1.0,
9795 wgpu::LoadOp::Clear(CLEAR_COLOR),
9796 None,
9797 None,
9798 BlendMode::SrcOver,
9799 None,
9800 CompositeSampleMode::Linear,
9801 );
9802 backend.release_frame_surface(composite_target);
9803 } else {
9804 let composite_dest_quad =
9805 snap_motion_stable_dest_quad(root_dest_quad, root_surface.sample_mode);
9806 execute_composite_surface_to_view(
9807 &mut backend,
9808 root_surface.target.target(),
9809 surface_view,
9810 (width, height),
9811 composite_dest_quad,
9812 root_surface.composite_alpha,
9813 wgpu::LoadOp::Clear(CLEAR_COLOR),
9814 None,
9815 root_surface.blend_mode,
9816 root_surface.sample_mode,
9817 )?;
9818 }
9819 backend.release_layer_surface_target(root_surface.target);
9820 if let Some(overlay) = overlay {
9821 Self::render_overlay_packet(
9822 &mut backend,
9823 surface_view,
9824 overlay,
9825 width,
9826 height,
9827 root_scale,
9828 )?;
9829 }
9830 let after_layer_render = Instant::now();
9831 if let Some(total_ms) = should_log_wgpu_render_stage(recorded_start, after_layer_render) {
9832 log::warn!(
9833 "[wgpu-render-stage:recorded-layer-root] total_ms={total_ms:.2} collect_ms={:.2} render_ms={:.2}",
9834 instant_ms(recorded_start, after_root_collect),
9835 instant_ms(after_root_collect, after_layer_render),
9836 );
9837 }
9838 Ok(())
9839 }
9840
9841 fn render_overlay_packet<C: FrameCommandRecorder>(
9845 backend: &mut RecordingSurfaceBackend<'_, '_, C>,
9846 surface_view: &wgpu::TextureView,
9847 overlay: CollectedLayer,
9848 width: u32,
9849 height: u32,
9850 root_scale: f32,
9851 ) -> Result<(), String> {
9852 if !overlay.child_layers.is_empty()
9853 || !root_direct_scene_events_are_supported(&overlay.scene, false)
9854 || !direct_root_child_underlays_are_supported(&overlay, false)
9855 {
9856 return Err("dev overlay graph must stay directly renderable".to_string());
9857 }
9858 execute_render_root_direct(
9859 backend,
9860 surface_view,
9861 None,
9862 overlay,
9863 width,
9864 height,
9865 root_scale,
9866 wgpu::LoadOp::Load,
9867 )
9868 .map(|_overlay_scene| ())
9869 .map_err(|(error, _overlay_scene)| error)
9870 }
9871
9872 #[allow(clippy::too_many_arguments)]
9873 fn encode_non_effect_segment_commands<C: FrameCommandRecorder>(
9874 &mut self,
9875 frame_encoder: &mut C,
9876 target_view: &wgpu::TextureView,
9877 ordered_items: &[(usize, SegmentDrawItem)],
9878 composites: &[(usize, CompositeBatchItem<'_>)],
9879 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
9880 shapes: &[DrawShape],
9881 brushes: &[Brush],
9882 images: &[ImageDraw],
9883 texts: &[TextDraw],
9884 shadow_draws: &[ShadowDraw],
9885 retained_draws: &[RetainedDraw],
9886 initial_load_op: wgpu::LoadOp<wgpu::Color>,
9887 width: u32,
9888 height: u32,
9889 root_scale: f32,
9890 ) -> Result<SegmentCommandEncodeOutcome, String> {
9891 let mut first_batch = true;
9892 for command in
9893 SegmentCommandIter::new(ordered_items, shapes, images, self.shape_batch_limits)
9894 {
9895 match command {
9896 SegmentRenderCommand::DrawChunk(chunk) => {
9897 let load_op = if first_batch {
9898 initial_load_op
9899 } else {
9900 wgpu::LoadOp::Load
9901 };
9902 let outcome = self.render_segment_draw_chunk(
9903 frame_encoder,
9904 target_view,
9905 ordered_items,
9906 composites,
9907 shader_composites,
9908 shapes,
9909 brushes,
9910 images,
9911 texts,
9912 retained_draws,
9913 chunk,
9914 width,
9915 height,
9916 root_scale,
9917 load_op,
9918 )?;
9919 if outcome.rendered_any {
9920 frame_encoder.record_passes(outcome.pass_count);
9921 first_batch = false;
9922 }
9923 }
9924 SegmentRenderCommand::Shadow(index) => {
9925 if first_batch && matches!(initial_load_op, wgpu::LoadOp::Clear(_)) {
9926 {
9927 let _clear = frame_encoder.encoder().begin_render_pass(
9928 &wgpu::RenderPassDescriptor {
9929 label: Some("Shadow Pre-Clear"),
9930 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
9931 view: target_view,
9932 resolve_target: None,
9933 depth_slice: None,
9934 ops: wgpu::Operations {
9935 load: initial_load_op,
9936 store: wgpu::StoreOp::Store,
9937 },
9938 })],
9939 depth_stencil_attachment: None,
9940 timestamp_writes: None,
9941 occlusion_query_set: None,
9942 multiview_mask: None,
9943 },
9944 );
9945 }
9946 frame_encoder.record_pass();
9947 first_batch = false;
9948 }
9949 let pass_count_before = frame_encoder.recorded_pass_count();
9950 self.encode_shadow_draw(
9951 frame_encoder,
9952 target_view,
9953 &shadow_draws[index],
9954 width,
9955 height,
9956 root_scale,
9957 );
9958 if frame_encoder.recorded_pass_count() > pass_count_before {
9959 first_batch = false;
9960 }
9961 }
9962 }
9963 }
9964 Ok(SegmentCommandEncodeOutcome { first_batch })
9965 }
9966
9967 #[cfg(not(target_arch = "wasm32"))]
9968 #[allow(clippy::too_many_arguments)]
9969 fn render_segment_draw_chunk_fused_native<C: FrameCommandRecorder>(
9970 &mut self,
9971 frame_encoder: &mut C,
9972 target_view: &wgpu::TextureView,
9973 ordered_items: &[(usize, SegmentDrawItem)],
9974 composites: &[(usize, CompositeBatchItem<'_>)],
9975 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
9976 shapes: &[DrawShape],
9977 brushes: &[Brush],
9978 images: &[ImageDraw],
9979 texts: &[TextDraw],
9980 retained_draws: &[RetainedDraw],
9981 chunk: &SegmentDrawChunkPlan,
9982 width: u32,
9983 height: u32,
9984 root_scale: f32,
9985 load_op: wgpu::LoadOp<wgpu::Color>,
9986 ) -> Result<Option<SegmentRenderOutcome>, String> {
9987 let Some(partitions) = native_segment_fusion_partitions(
9988 ordered_items,
9989 shapes,
9990 brushes,
9991 chunk,
9992 self.shape_batch_limits,
9993 )?
9994 else {
9995 return Ok(None);
9996 };
9997
9998 let mut rendered_any = false;
9999 let mut pass_count = 0_u32;
10000 let mut next_load_op = load_op;
10001 let encode_started = Instant::now();
10002 let mut partition_count = 0_u64;
10003 for partition in partitions {
10004 partition_count += 1;
10005 let outcome = self.render_segment_draw_chunk_fused_native_partition(
10006 frame_encoder,
10007 target_view,
10008 ordered_items,
10009 composites,
10010 shader_composites,
10011 shapes,
10012 brushes,
10013 images,
10014 texts,
10015 retained_draws,
10016 &partition.chunk,
10017 partition.budget,
10018 width,
10019 height,
10020 root_scale,
10021 next_load_op,
10022 )?;
10023 if outcome.rendered_any {
10024 rendered_any = true;
10025 pass_count = pass_count.saturating_add(outcome.pass_count);
10026 next_load_op = wgpu::LoadOp::Load;
10027 }
10028 }
10029
10030 self.segment_encode_stats
10031 .note_call(partition_count, encode_started.elapsed().as_micros() as u64);
10032
10033 Ok(Some(SegmentRenderOutcome {
10034 rendered_any,
10035 pass_count,
10036 }))
10037 }
10038
10039 #[cfg(not(target_arch = "wasm32"))]
10040 #[allow(clippy::too_many_arguments)]
10041 fn render_segment_draw_chunk_fused_native_partition<C: FrameCommandRecorder>(
10042 &mut self,
10043 frame_encoder: &mut C,
10044 target_view: &wgpu::TextureView,
10045 ordered_items: &[(usize, SegmentDrawItem)],
10046 composites: &[(usize, CompositeBatchItem<'_>)],
10047 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
10048 shapes: &[DrawShape],
10049 brushes: &[Brush],
10050 images: &[ImageDraw],
10051 texts: &[TextDraw],
10052 retained_draws: &[RetainedDraw],
10053 chunk: &SegmentDrawChunkPlan,
10054 budget: NativeSegmentFusionBudget,
10055 width: u32,
10056 height: u32,
10057 root_scale: f32,
10058 load_op: wgpu::LoadOp<wgpu::Color>,
10059 ) -> Result<SegmentRenderOutcome, String> {
10060 let partition_start = Instant::now();
10061 let mut staged_uploads = self.take_staged_uploads();
10062 staged_uploads.clear();
10063 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
10064 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
10065 let mut image_cmds = std::mem::take(&mut self.scratch_image_cmds);
10066 let mut glyph_cmds = std::mem::take(&mut self.scratch_glyph_cmds);
10067 let mut span_cache = std::mem::take(&mut self.static_span);
10070 let mut segment_surfaces = std::mem::take(&mut self.segment_surfaces);
10073
10074 image_vertices.clear();
10075 image_indices.clear();
10076 image_cmds.clear();
10077 glyph_cmds.clear();
10078
10079 let result = (|| {
10080 let viewport = ViewportUniformParams {
10081 width,
10082 height,
10083 offset: [0.0, 0.0],
10084 };
10085 self.prewarm_offscreen_text_glyph_draws_in_chunk(
10086 ordered_items,
10087 texts,
10088 chunk,
10089 viewport,
10090 root_scale,
10091 &mut staged_uploads,
10092 &mut image_vertices,
10093 &mut image_indices,
10094 &mut glyph_cmds,
10095 )?;
10096 let mut shape_refs = Vec::with_capacity(budget.shape_count);
10097 for batch in chunk.iter() {
10098 let SegmentBatchPlan::Shape { start, end, .. } = batch else {
10099 continue;
10100 };
10101 for (_, item) in &ordered_items[start..end] {
10102 let SegmentDrawItem::Shape(shape_index) = item else {
10103 return Err(format!(
10104 "shape batch contains non-shape draw item: {item:?}"
10105 ));
10106 };
10107 shape_refs.push(&shapes[*shape_index]);
10108 }
10109 }
10110 let after_shape_refs = Instant::now();
10111
10112 let mut direct_shape_uploads = StagedBufferUploads::default();
10113 let mut shape_upload_base = 0u64;
10114 if !shape_refs.is_empty() {
10115 let Some((_, upload_base)) = self.prepare_shapes_batch_direct(
10116 frame_encoder,
10117 shape_refs.iter().copied(),
10118 brushes,
10119 root_scale,
10120 viewport,
10121 &mut direct_shape_uploads,
10122 ) else {
10123 return Err(
10124 "native fused segment shape preparation produced no draw batch".to_string(),
10125 );
10126 };
10127 shape_upload_base = upload_base;
10128 }
10129 let after_shape_prepare = Instant::now();
10130
10131 let mut segment_captures: Vec<SegmentCaptureJob> = Vec::new();
10139 let mut segment_composite_plans: Vec<(usize, SegmentCompositePlan)> = Vec::new();
10140 if segment_surfaces.enabled() {
10141 self.plan_segment_surfaces(
10142 &mut segment_surfaces,
10143 ordered_items,
10144 chunk,
10145 retained_draws,
10146 &mut staged_uploads,
10147 &mut segment_captures,
10148 &mut segment_composite_plans,
10149 );
10150 }
10151
10152 let first_batch_info = match chunk.batches.first() {
10158 Some(&SegmentBatchPlan::Shape {
10159 start,
10160 end,
10161 blend_mode,
10162 }) => {
10163 let mut has_gradient = false;
10164 for (_, item) in &ordered_items[start..end] {
10165 if let SegmentDrawItem::Shape(shape_index) = item {
10166 has_gradient |=
10167 shape_gradient_stop_count(&shapes[*shape_index], brushes) > 0;
10168 }
10169 }
10170 Some((end - start, blend_mode, has_gradient))
10171 }
10172 _ => None,
10173 };
10174 let span_decision = span_cache.engage(
10175 load_op,
10176 first_batch_info,
10177 width,
10178 height,
10179 &self.scratch_shape_data,
10180 &self.scratch_gradients,
10181 );
10182 let span_skip = match span_decision {
10183 StaticSpanDecision::Hit { skip } => {
10184 if fill_area_diag_enabled() {
10185 self.fill_area_diag
10190 .note_static_span_skip(&self.scratch_shape_data[..skip]);
10191 }
10192 skip
10193 }
10194 _ => 0,
10195 };
10196
10197 let rim_mesh_on = rim_mesh_enabled();
10205 let mut chunk_rims: Vec<RimDraw> = Vec::new();
10206
10207 let mut fused_batches = Vec::with_capacity(chunk.batches.len());
10208 let mut shape_cursor = 0_u32;
10209 let mut composite_cursor = 0usize;
10210 let mut shader_composite_cursor = 0usize;
10211 for (batch_index, batch) in chunk.iter().enumerate() {
10212 match batch {
10213 SegmentBatchPlan::Shape {
10214 start,
10215 end,
10216 blend_mode,
10217 } => {
10218 let mut has_gradient = false;
10219 for (_, item) in &ordered_items[start..end] {
10220 let SegmentDrawItem::Shape(shape_index) = item else {
10221 return Err(format!(
10222 "shape batch contains non-shape draw item: {item:?}"
10223 ));
10224 };
10225 has_gradient |=
10226 shape_gradient_stop_count(&shapes[*shape_index], brushes) > 0;
10227 }
10228 let skip = if batch_index == 0 { span_skip } else { 0 };
10233 let shape_count = end - start;
10234 if shape_count > 0 {
10235 if rim_mesh_on
10236 && self.instanced_quads.is_some()
10237 && blend_mode == BlendMode::SrcOver
10238 {
10239 for offset in skip..shape_count {
10240 let global_index = shape_cursor + offset as u32;
10245 let converted = &self.scratch_shape_data[global_index as usize];
10246 let Some(band) = rim_mesh_band(converted) else {
10247 continue;
10248 };
10249 let vertex_mark = self.rim_mesh_vertices.len();
10250 let index_mark = self.rim_mesh_indices.len();
10251 if emit_arc_band_mesh(
10252 converted,
10253 global_index,
10254 &band,
10255 &mut self.rim_mesh_vertices,
10256 &mut self.rim_mesh_indices,
10257 )
10258 .is_none()
10259 {
10260 self.rim_mesh_vertices.truncate(vertex_mark);
10263 self.rim_mesh_indices.truncate(index_mark);
10264 continue;
10265 }
10266 if self.rim_mesh_vertices.len() > RIM_MESH_VERTEX_CAPACITY
10267 || self.rim_mesh_indices.len() > RIM_MESH_INDEX_CAPACITY
10268 {
10269 self.rim_mesh_vertices.truncate(vertex_mark);
10273 self.rim_mesh_indices.truncate(index_mark);
10274 rim_mesh_capacity_warn();
10275 continue;
10276 }
10277 chunk_rims.push(RimDraw {
10278 shape_index: global_index,
10279 first_index: index_mark as u32,
10280 index_count: (self.rim_mesh_indices.len() - index_mark)
10281 as u32,
10282 });
10283 if fill_area_diag_enabled() {
10284 self.fill_area_diag.note_rim_mesh(
10285 converted,
10286 triangles_shoelace_area(
10287 &self.rim_mesh_vertices,
10288 &self.rim_mesh_indices[index_mark..],
10289 ),
10290 );
10291 }
10292 self.rim_meshes_emitted += 1;
10293 if self.rim_meshes_emitted % 600 == 1 {
10294 log::debug!(
10295 "[rim-mesh] {} rims meshed lifetime ({} verts live this frame)",
10296 self.rim_meshes_emitted,
10297 self.rim_mesh_vertices.len(),
10298 );
10299 }
10300 }
10301 }
10302 if shape_count > skip {
10303 fused_batches.push(FusedSegmentBatch::Shape {
10304 batch: PreparedShapeBatch {
10305 vertex_start: (shape_cursor + skip as u32) * 6,
10306 vertex_count: (shape_count - skip) as u32 * 6,
10307 has_gradient,
10308 },
10309 blend_mode,
10310 });
10311 }
10312 shape_cursor += shape_count as u32;
10313 }
10314 }
10315 SegmentBatchPlan::Image {
10316 start,
10317 end,
10318 blend_mode,
10319 } => {
10320 let cmd_start = image_cmds.len();
10321 for (_, item) in &ordered_items[start..end] {
10322 let SegmentDrawItem::Image(image_index) = item else {
10323 return Err(format!(
10324 "image batch contains non-image draw item: {item:?}"
10325 ));
10326 };
10327 self.append_image_draw_cmd(
10328 &images[*image_index],
10329 viewport,
10330 root_scale,
10331 &mut image_vertices,
10332 &mut image_indices,
10333 &mut image_cmds,
10334 )?;
10335 }
10336 let cmd_end = image_cmds.len();
10337 if cmd_start < cmd_end {
10338 fused_batches.push(FusedSegmentBatch::Image {
10339 cmd_range: cmd_start..cmd_end,
10340 blend_mode,
10341 });
10342 }
10343 }
10344 SegmentBatchPlan::Text { start, end } => {
10345 let glyph_cmd_start = glyph_cmds.len();
10346 let image_cmd_start = image_cmds.len();
10347 let text_draws =
10348 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
10349 if !self.append_text_glyph_draws(
10350 text_draws,
10351 viewport,
10352 root_scale,
10353 false,
10354 &mut staged_uploads,
10355 &mut image_vertices,
10356 &mut image_indices,
10357 &mut glyph_cmds,
10358 )? {
10359 let text_draws =
10360 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
10361 self.append_text_image_draw_cmds(
10362 text_draws,
10363 viewport,
10364 root_scale,
10365 &mut image_vertices,
10366 &mut image_indices,
10367 &mut image_cmds,
10368 )?;
10369 }
10370 let image_cmd_end = image_cmds.len();
10371 let glyph_cmd_end = glyph_cmds.len();
10372 if image_cmd_start < image_cmd_end || glyph_cmd_start < glyph_cmd_end {
10373 fused_batches.push(FusedSegmentBatch::Text {
10374 image_cmd_range: image_cmd_start..image_cmd_end,
10375 glyph_cmd_range: glyph_cmd_start..glyph_cmd_end,
10376 });
10377 }
10378 }
10379 SegmentBatchPlan::Composite { start, end } => {
10380 for (_, item) in &ordered_items[start..end] {
10381 if !matches!(item, SegmentDrawItem::Composite(_)) {
10382 return Err(format!(
10383 "composite batch contains non-composite draw item: {item:?}"
10384 ));
10385 }
10386 }
10387 let draw_count = end - start;
10388 if draw_count > 0 {
10389 let draw_start = composite_cursor;
10390 composite_cursor += draw_count;
10391 fused_batches.push(FusedSegmentBatch::Composite {
10392 draw_range: draw_start..composite_cursor,
10393 });
10394 }
10395 }
10396 SegmentBatchPlan::ShaderComposite { start, end } => {
10397 for (_, item) in &ordered_items[start..end] {
10398 if !matches!(item, SegmentDrawItem::ShaderComposite(_)) {
10399 return Err(format!(
10400 "shader composite batch contains non-shader-composite draw item: {item:?}"
10401 ));
10402 }
10403 }
10404 let draw_count = end - start;
10405 if draw_count > 0 {
10406 let draw_start = shader_composite_cursor;
10407 shader_composite_cursor += draw_count;
10408 fused_batches.push(FusedSegmentBatch::ShaderComposite {
10409 draw_range: draw_start..shader_composite_cursor,
10410 });
10411 }
10412 }
10413 SegmentBatchPlan::Retained { start, end } => {
10414 self.stage_replay_patches(&mut staged_uploads);
10415 for (_, item) in &ordered_items[start..end] {
10416 let SegmentDrawItem::Retained(index) = item else {
10417 return Err(format!(
10418 "retained batch contains non-retained draw item: {item:?}"
10419 ));
10420 };
10421 let retained = retained_draws.get(*index).ok_or_else(|| {
10422 format!("retained draw index {index} out of bounds")
10423 })?;
10424 if (*index as u32) < MAX_REPLAY_SLOTS
10425 && self.replay_slots.slots.contains_key(&retained.slot)
10426 {
10427 let transform = retained.transform.with_retained_paint();
10428 staged_uploads.stage_at(
10429 UploadTarget::ReplayTransform,
10430 *index as u64 * REPLAY_TRANSFORM_STRIDE,
10431 bytemuck::bytes_of(&transform),
10432 );
10433 }
10434 }
10435 if end > start {
10436 fused_batches.push(FusedSegmentBatch::Retained {
10437 item_range: start..end,
10438 });
10439 }
10440 }
10441 }
10442 }
10443 if !chunk_rims.is_empty() {
10444 self.upload_transient_rim_meshes();
10445 }
10446 let after_batch_prepare = Instant::now();
10447
10448 if !image_indices.is_empty() {
10449 self.stage_native_image_buffers(
10450 &mut staged_uploads,
10451 viewport,
10452 &image_vertices,
10453 &image_indices,
10454 );
10455 }
10456
10457 let display_clip_depth_view =
10466 self.display_clip_pass_depth_view(target_view, width, height);
10467 let pass_depth = display_clip_depth_view.is_some();
10468
10469 let device = self.device.clone();
10470 let composite_items: Vec<_> = chunk
10471 .iter()
10472 .filter_map(|batch| match batch {
10473 SegmentBatchPlan::Composite { start, end } => Some((start, end)),
10474 _ => None,
10475 })
10476 .flat_map(|(start, end)| {
10477 ordered_items[start..end].iter().filter_map(|(_, item)| {
10478 let SegmentDrawItem::Composite(composite_index) = item else {
10479 return None;
10480 };
10481 composites
10482 .get(*composite_index)
10483 .map(|(_, composite)| *composite)
10484 })
10485 })
10486 .collect();
10487 let prepared_composites = self.effect_renderer.prepare_composite_batch_draws(
10488 frame_encoder,
10489 &device,
10490 load_op,
10491 &composite_items,
10492 pass_depth,
10493 );
10494 let shader_items: Vec<_> = chunk
10495 .iter()
10496 .filter_map(|batch| match batch {
10497 SegmentBatchPlan::ShaderComposite { start, end } => Some((start, end)),
10498 _ => None,
10499 })
10500 .flat_map(|(start, end)| {
10501 ordered_items[start..end].iter().filter_map(|(_, item)| {
10502 let SegmentDrawItem::ShaderComposite(composite_index) = item else {
10503 return None;
10504 };
10505 shader_composites
10506 .get(*composite_index)
10507 .map(|(_, composite)| *composite)
10508 })
10509 })
10510 .collect();
10511 let prepared_shaders = self
10512 .effect_renderer
10513 .prepare_shader_batch_draws(frame_encoder, &device, &shader_items, pass_depth)
10514 .ok_or_else(|| "shader composite batch preparation failed".to_string())?;
10515 if !shader_items.is_empty() {
10516 self.effect_renderer.record_composite_pass();
10517 self.effect_renderer
10518 .debug_effects
10519 .set(self.effect_renderer.debug_effects.get() + shader_items.len() as u32);
10520 }
10521 let span_blit_items =
10528 span_cache
10529 .texture
10530 .as_ref()
10531 .filter(|_| span_skip > 0)
10532 .map(|texture| CompositeBatchItem {
10533 source: texture,
10534 alpha: 1.0,
10535 scissor: None,
10536 rounded_mask: None,
10537 blend_mode: BlendMode::Src,
10538 dest_viewport: None,
10539 source_viewport: None,
10540 sample_mode: CompositeSampleMode::Nearest,
10541 });
10542 let span_blit = match &span_blit_items {
10543 Some(item) => self.effect_renderer.prepare_composite_batch_draws(
10544 frame_encoder,
10545 &device,
10546 load_op,
10547 std::slice::from_ref(item),
10548 pass_depth,
10549 ),
10550 None => Vec::new(),
10551 };
10552 let mut prepared_segment_composites: Vec<(usize, PreparedProjectiveComposite<'_>)> =
10557 Vec::with_capacity(segment_composite_plans.len());
10558 for (index, plan) in &segment_composite_plans {
10559 let Some(entry) = segment_surfaces.entry(&plan.key) else {
10560 continue;
10561 };
10562 let item = ProjectiveCompositeItem {
10563 source: &entry.texture,
10564 viewport: (width, height),
10565 dest_quad: plan.dest_quad,
10566 inverse: plan.inverse,
10567 alpha: 1.0,
10568 blend_mode: BlendMode::SrcOver,
10569 sample_mode: if plan.identity || plan.integer_translation {
10570 CompositeSampleMode::Nearest
10571 } else {
10572 CompositeSampleMode::Linear
10573 },
10574 };
10575 let prepared = self.effect_renderer.prepare_projective_composite_draw(
10576 frame_encoder,
10577 &device,
10578 &item,
10579 pass_depth,
10580 );
10581 prepared_segment_composites.push((*index, prepared));
10582 }
10583 let after_composite_prepare = Instant::now();
10584
10585 if fused_batches.is_empty() && span_blit.is_empty() {
10586 return Ok(SegmentRenderOutcome {
10587 rendered_any: false,
10588 pass_count: 0,
10589 });
10590 }
10591
10592 self.flush_staged_uploads_at(
10597 frame_encoder.encoder(),
10598 &direct_shape_uploads,
10599 shape_upload_base,
10600 );
10601 let upload_offset =
10602 frame_encoder.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
10603 self.flush_staged_uploads_at(frame_encoder.encoder(), &staged_uploads, upload_offset);
10604 let after_upload = Instant::now();
10605
10606 let mut segment_capture_passes = 0u32;
10615 for job in &segment_captures {
10616 let Some(entry) = segment_surfaces.entry(&job.key) else {
10617 continue;
10618 };
10619 let Some(slot) = self.replay_slots.slots.get(&job.key.slot) else {
10620 continue;
10621 };
10622 let Some(uniform_group) =
10623 segment_surfaces.capture_uniform_bind_group(job.capture_index)
10624 else {
10625 continue;
10626 };
10627 let mut capture_pass =
10628 frame_encoder
10629 .encoder()
10630 .begin_render_pass(&wgpu::RenderPassDescriptor {
10631 label: Some("Segment Surface Capture Pass"),
10632 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
10633 view: &entry.texture.view,
10634 resolve_target: None,
10635 depth_slice: None,
10636 ops: wgpu::Operations {
10637 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
10641 store: wgpu::StoreOp::Store,
10642 },
10643 })],
10644 depth_stencil_attachment: None,
10645 timestamp_writes: None,
10646 occlusion_query_set: None,
10647 multiview_mask: None,
10648 });
10649 let draws = self.encode_retained_op(
10650 slot,
10651 job.first,
10652 job.last,
10653 MAX_REPLAY_SLOTS + job.capture_index,
10654 &mut |cmd| match cmd {
10655 RetainedCmd::Uniforms(_) => {
10660 capture_pass.set_bind_group(0, uniform_group, &[])
10661 }
10662 RetainedCmd::Pipeline(pipeline) => {
10663 capture_pass.set_pipeline(self.segment_capture_pipeline(pipeline))
10664 }
10665 RetainedCmd::SlotBindings(group, offset) => {
10666 capture_pass.set_bind_group(1, group, &[offset])
10667 }
10668 RetainedCmd::MeshVertices(buffer) => {
10669 capture_pass.set_vertex_buffer(0, buffer.slice(..))
10670 }
10671 RetainedCmd::Index(buffer, format) => {
10672 capture_pass.set_index_buffer(buffer.slice(..), format)
10673 }
10674 RetainedCmd::Draw(vertices) => capture_pass.draw(vertices, 0..1),
10675 RetainedCmd::DrawIndexed(indices, instances) => {
10676 capture_pass.draw_indexed(indices, 0, instances)
10677 }
10678 },
10679 );
10680 self.frame_stats.add_draw_calls(draws);
10681 segment_capture_passes += 1;
10682 }
10683
10684 let use_retained_bundles = retained_bundles_enabled();
10685 let mut retained_encode_ms = 0.0_f64;
10686 {
10687 let mut render_pass =
10688 frame_encoder
10689 .encoder()
10690 .begin_render_pass(&wgpu::RenderPassDescriptor {
10691 label: Some("Fused Segment Draw Pass"),
10692 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
10693 view: target_view,
10694 resolve_target: None,
10695 depth_slice: None,
10696 ops: wgpu::Operations {
10697 load: load_op,
10698 store: wgpu::StoreOp::Store,
10699 },
10700 })],
10701 depth_stencil_attachment: display_clip_depth_view.as_ref().map(
10705 |view| wgpu::RenderPassDepthStencilAttachment {
10706 view,
10707 depth_ops: Some(wgpu::Operations {
10708 load: wgpu::LoadOp::Clear(
10709 crate::display_clip::DISPLAY_CLIP_DEPTH_CLEAR,
10710 ),
10711 store: wgpu::StoreOp::Discard,
10712 }),
10713 stencil_ops: None,
10714 },
10715 ),
10716 timestamp_writes: None,
10717 occlusion_query_set: None,
10718 multiview_mask: None,
10719 });
10720
10721 for draw in &span_blit {
10725 self.effect_renderer.draw_prepared_composite(
10726 &mut render_pass,
10727 (width, height),
10728 draw,
10729 pass_depth,
10730 );
10731 }
10732 if pass_depth {
10733 self.draw_display_clip_occluder(&mut render_pass, width, height);
10736 self.display_clip.pass_depth.set(true);
10737 }
10738
10739 for batch in &fused_batches {
10740 match batch {
10741 FusedSegmentBatch::Shape { batch, blend_mode } => {
10742 self.draw_prepared_shapes(
10743 &mut render_pass,
10744 *blend_mode,
10745 *batch,
10746 width,
10747 height,
10748 &chunk_rims,
10749 );
10750 }
10751 FusedSegmentBatch::Image {
10752 cmd_range,
10753 blend_mode,
10754 } => {
10755 self.draw_native_prepared_image_cmd_range(
10756 &mut render_pass,
10757 &image_cmds,
10758 cmd_range.clone(),
10759 *blend_mode,
10760 )?;
10761 }
10762 FusedSegmentBatch::Text {
10763 image_cmd_range,
10764 glyph_cmd_range,
10765 } => {
10766 if !image_cmd_range.is_empty() {
10767 self.draw_native_prepared_image_cmd_range(
10768 &mut render_pass,
10769 &image_cmds,
10770 image_cmd_range.clone(),
10771 BlendMode::SrcOver,
10772 )?;
10773 self.frame_stats.bump_text();
10774 }
10775 if !glyph_cmd_range.is_empty() {
10776 self.draw_native_prepared_glyph_cmd_range(
10777 &mut render_pass,
10778 &glyph_cmds,
10779 glyph_cmd_range.clone(),
10780 )?;
10781 }
10782 }
10783 FusedSegmentBatch::Composite { draw_range } => {
10784 for draw in
10785 prepared_composites.get(draw_range.clone()).ok_or_else(|| {
10786 "composite draw range is outside the prepared command buffer"
10787 .to_string()
10788 })?
10789 {
10790 self.effect_renderer.draw_prepared_composite(
10791 &mut render_pass,
10792 (width, height),
10793 draw,
10794 pass_depth,
10795 );
10796 }
10797 }
10798 FusedSegmentBatch::ShaderComposite { draw_range } => {
10799 for draw in prepared_shaders.get(draw_range.clone()).ok_or_else(|| {
10800 "shader composite draw range is outside the prepared command buffer"
10801 .to_string()
10802 })? {
10803 self.effect_renderer.draw_prepared_shader_src_over(
10804 &device,
10805 &mut render_pass,
10806 (width, height),
10807 draw,
10808 pass_depth,
10809 );
10810 }
10811 }
10812 FusedSegmentBatch::Retained { item_range } => {
10813 let retained_start = Instant::now();
10819 let stretch_has_composites = !prepared_segment_composites.is_empty()
10825 && ordered_items[item_range.clone()].iter().any(|(_, item)| {
10826 matches!(
10827 item,
10828 SegmentDrawItem::Retained(index)
10829 if prepared_segment_composites
10830 .iter()
10831 .any(|(prepared_index, _)| prepared_index == index)
10832 )
10833 });
10834 if use_retained_bundles && !stretch_has_composites {
10835 self.draw_retained_stretch_bundled(
10836 &mut render_pass,
10837 ordered_items,
10838 retained_draws,
10839 item_range.clone(),
10840 width,
10841 height,
10842 );
10843 } else {
10844 for (_, item) in &ordered_items[item_range.clone()] {
10845 if let SegmentDrawItem::Retained(index) = item {
10846 if let Some((_, prepared)) = prepared_segment_composites
10847 .iter()
10848 .find(|(prepared_index, _)| prepared_index == index)
10849 {
10850 self.effect_renderer
10858 .draw_prepared_projective_composite(
10859 &mut render_pass,
10860 (width, height),
10861 prepared,
10862 pass_depth,
10863 );
10864 self.frame_stats.add_draw_calls(1);
10865 } else if let Some(retained) = retained_draws.get(*index) {
10866 self.draw_retained_batch(
10867 &mut render_pass,
10868 retained,
10869 *index,
10870 width,
10871 height,
10872 );
10873 }
10874 }
10875 }
10876 }
10877 retained_encode_ms += instant_ms(retained_start, Instant::now());
10878 }
10879 }
10880 }
10881 }
10882 self.display_clip.pass_depth.set(false);
10888 let mut capture_passes = 0_u32;
10897 if let StaticSpanDecision::Capture { len, clear } = span_decision {
10898 let texture = match span_cache.texture.take() {
10899 Some(existing) if existing.width == width && existing.height == height => {
10900 existing
10901 }
10902 other => {
10903 if let Some(stale) = other {
10904 self.defer_offscreen_release(stale);
10905 }
10906 self.acquire_offscreen(width, height)
10907 }
10908 };
10909 {
10910 let mut capture_pass =
10911 frame_encoder
10912 .encoder()
10913 .begin_render_pass(&wgpu::RenderPassDescriptor {
10914 label: Some("Static Span Capture Pass"),
10915 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
10916 view: &texture.view,
10917 resolve_target: None,
10918 depth_slice: None,
10919 ops: wgpu::Operations {
10920 load: wgpu::LoadOp::Clear(clear),
10921 store: wgpu::StoreOp::Store,
10922 },
10923 })],
10924 depth_stencil_attachment: None,
10925 timestamp_writes: None,
10926 occlusion_query_set: None,
10927 multiview_mask: None,
10928 });
10929 let has_gradient = first_batch_info
10933 .map(|(_, _, has_gradient)| has_gradient)
10934 .unwrap_or(false);
10935 self.draw_prepared_shapes(
10936 &mut capture_pass,
10937 BlendMode::SrcOver,
10938 PreparedShapeBatch {
10939 vertex_start: 0,
10940 vertex_count: len as u32 * 6,
10941 has_gradient,
10942 },
10943 width,
10944 height,
10945 &[],
10946 );
10947 if fill_area_diag_enabled() {
10948 self.fill_area_diag
10952 .add_shape_quads(&self.scratch_shape_data[..len], viewport);
10953 }
10954 span_cache.store_key(
10955 &self.scratch_shape_data[..len],
10956 &self.scratch_gradients,
10957 width,
10958 height,
10959 clear,
10960 has_gradient,
10961 );
10962 }
10963 span_cache.texture = Some(texture);
10964 capture_passes = 1;
10965 }
10966 let after_pass = Instant::now();
10967 if let Some(total_ms) = should_log_wgpu_render_stage(partition_start, after_pass) {
10968 log::warn!(
10969 "[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={}",
10970 instant_ms(partition_start, after_shape_refs),
10971 instant_ms(after_shape_refs, after_shape_prepare),
10972 instant_ms(after_shape_prepare, after_batch_prepare),
10973 instant_ms(after_batch_prepare, after_composite_prepare),
10974 instant_ms(after_composite_prepare, after_upload),
10975 instant_ms(after_upload, after_pass),
10976 fused_batches.len(),
10977 budget.shape_count,
10978 image_cmds.len(),
10979 glyph_cmds.len(),
10980 staged_uploads.bytes.len(),
10981 );
10982 }
10983
10984 Ok(SegmentRenderOutcome {
10985 rendered_any: true,
10986 pass_count: 1 + capture_passes + segment_capture_passes,
10987 })
10988 })();
10989
10990 self.display_clip.pass_depth.set(false);
10994 self.scratch_image_vertices = image_vertices;
10995 self.scratch_image_indices = image_indices;
10996 self.scratch_image_cmds = image_cmds;
10997 self.scratch_glyph_cmds = glyph_cmds;
10998 self.restore_staged_uploads(staged_uploads);
10999 self.static_span = span_cache;
11000 if result.is_err() {
11001 segment_surfaces.clear();
11004 }
11005 self.segment_surfaces = segment_surfaces;
11006 result
11007 }
11008
11009 #[allow(clippy::too_many_arguments)]
11010 fn render_segment_draw_chunk<C: FrameCommandRecorder>(
11011 &mut self,
11012 frame_encoder: &mut C,
11013 target_view: &wgpu::TextureView,
11014 ordered_items: &[(usize, SegmentDrawItem)],
11015 composites: &[(usize, CompositeBatchItem<'_>)],
11016 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
11017 shapes: &[DrawShape],
11018 brushes: &[Brush],
11019 images: &[ImageDraw],
11020 texts: &[TextDraw],
11021 retained_draws: &[RetainedDraw],
11022 chunk: SegmentDrawChunkPlan,
11023 width: u32,
11024 height: u32,
11025 root_scale: f32,
11026 load_op: wgpu::LoadOp<wgpu::Color>,
11027 ) -> Result<SegmentRenderOutcome, String> {
11028 #[cfg(target_arch = "wasm32")]
11029 let _ = retained_draws;
11030 #[cfg(not(target_arch = "wasm32"))]
11031 if let Some(outcome) = self.render_segment_draw_chunk_fused_native(
11032 frame_encoder,
11033 target_view,
11034 ordered_items,
11035 composites,
11036 shader_composites,
11037 shapes,
11038 brushes,
11039 images,
11040 texts,
11041 retained_draws,
11042 &chunk,
11043 width,
11044 height,
11045 root_scale,
11046 load_op,
11047 )? {
11048 return Ok(outcome);
11049 }
11050
11051 let mut staged_uploads = self.take_staged_uploads();
11052 let result = (|| {
11053 let mut rendered_any = false;
11054 let mut pass_count = 0_u32;
11055 let mut next_load_op = load_op;
11056 for batch in chunk.iter() {
11057 staged_uploads.clear();
11058 match batch {
11059 SegmentBatchPlan::Shape {
11060 start,
11061 end,
11062 blend_mode,
11063 } => {
11064 let slice = &ordered_items[start..end];
11065 if slice.len() > self.shape_batch_limits.max_shapes_per_batch {
11066 return Err(format!(
11067 "shape batch contains {} shapes, exceeding the renderer limit of {}",
11068 slice.len(),
11069 self.shape_batch_limits.max_shapes_per_batch
11070 ));
11071 }
11072 let viewport = ViewportUniformParams {
11073 width,
11074 height,
11075 offset: [0.0, 0.0],
11076 };
11077 for (_, item) in slice {
11078 if !matches!(item, SegmentDrawItem::Shape(_)) {
11079 return Err(format!(
11080 "shape batch contains non-shape draw item: {item:?}"
11081 ));
11082 }
11083 }
11084 let Some(prepared) = self.prepare_shapes_batch(
11085 slice.iter().filter_map(|(_, item)| match item {
11086 SegmentDrawItem::Shape(shape_index) => Some(&shapes[*shape_index]),
11087 _ => None,
11088 }),
11089 brushes,
11090 root_scale,
11091 viewport,
11092 &mut staged_uploads,
11093 ) else {
11094 continue;
11095 };
11096 let upload_offset = frame_encoder
11097 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
11098 self.flush_staged_uploads_at(
11099 frame_encoder.encoder(),
11100 &staged_uploads,
11101 upload_offset,
11102 );
11103 {
11104 let mut render_pass = frame_encoder.encoder().begin_render_pass(
11105 &wgpu::RenderPassDescriptor {
11106 label: Some("Segment Shape Pass"),
11107 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
11108 view: target_view,
11109 resolve_target: None,
11110 depth_slice: None,
11111 ops: wgpu::Operations {
11112 load: next_load_op,
11113 store: wgpu::StoreOp::Store,
11114 },
11115 })],
11116 depth_stencil_attachment: None,
11117 timestamp_writes: None,
11118 occlusion_query_set: None,
11119 multiview_mask: None,
11120 },
11121 );
11122 self.draw_prepared_shapes(
11123 &mut render_pass,
11124 blend_mode,
11125 prepared,
11126 width,
11127 height,
11128 &[],
11129 );
11130 }
11131 pass_count = pass_count.saturating_add(1);
11132 rendered_any = true;
11133 next_load_op = wgpu::LoadOp::Load;
11134 }
11135 SegmentBatchPlan::Image {
11136 start,
11137 end,
11138 blend_mode,
11139 } => {
11140 let viewport = ViewportUniformParams {
11141 width,
11142 height,
11143 offset: [0.0, 0.0],
11144 };
11145 for (_, item) in &ordered_items[start..end] {
11146 if !matches!(item, SegmentDrawItem::Image(_)) {
11147 return Err(format!(
11148 "image batch contains non-image draw item: {item:?}"
11149 ));
11150 }
11151 }
11152 let prepared_images = self.prepare_image_draw_cmds(
11153 ordered_items[start..end]
11154 .iter()
11155 .filter_map(|(_, item)| match item {
11156 SegmentDrawItem::Image(image_index) => {
11157 Some(&images[*image_index])
11158 }
11159 _ => None,
11160 }),
11161 viewport,
11162 root_scale,
11163 &mut staged_uploads,
11164 )?;
11165 if prepared_images.is_empty() {
11166 self.scratch_image_cmds = prepared_images.into_cmds();
11167 continue;
11168 }
11169 let upload_offset = frame_encoder
11170 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
11171 self.flush_staged_uploads_at(
11172 frame_encoder.encoder(),
11173 &staged_uploads,
11174 upload_offset,
11175 );
11176 let draw_result = {
11177 let mut render_pass = frame_encoder.encoder().begin_render_pass(
11178 &wgpu::RenderPassDescriptor {
11179 label: Some("Segment Image Pass"),
11180 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
11181 view: target_view,
11182 resolve_target: None,
11183 depth_slice: None,
11184 ops: wgpu::Operations {
11185 load: next_load_op,
11186 store: wgpu::StoreOp::Store,
11187 },
11188 })],
11189 depth_stencil_attachment: None,
11190 timestamp_writes: None,
11191 occlusion_query_set: None,
11192 multiview_mask: None,
11193 },
11194 );
11195 self.draw_prepared_images(
11196 &mut render_pass,
11197 &prepared_images,
11198 blend_mode,
11199 )
11200 };
11201 pass_count = pass_count.saturating_add(1);
11202 self.scratch_image_cmds = prepared_images.into_cmds();
11203 draw_result?;
11204 rendered_any = true;
11205 next_load_op = wgpu::LoadOp::Load;
11206 }
11207 SegmentBatchPlan::Text { start, end } => {
11208 let viewport = ViewportUniformParams {
11209 width,
11210 height,
11211 offset: [0.0, 0.0],
11212 };
11213 let text_draws =
11214 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
11215 if let Some(prepared_glyphs) = self.prepare_text_glyph_draw_cmds(
11216 text_draws,
11217 viewport,
11218 root_scale,
11219 &mut staged_uploads,
11220 )? {
11221 if prepared_glyphs.is_empty() {
11222 self.scratch_glyph_cmds = prepared_glyphs.into_cmds();
11223 continue;
11224 }
11225 let upload_offset = frame_encoder
11226 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
11227 self.flush_staged_uploads_at(
11228 frame_encoder.encoder(),
11229 &staged_uploads,
11230 upload_offset,
11231 );
11232 {
11233 let mut render_pass = frame_encoder.encoder().begin_render_pass(
11234 &wgpu::RenderPassDescriptor {
11235 label: Some("Segment Text Glyph Atlas Pass"),
11236 color_attachments: &[Some(
11237 wgpu::RenderPassColorAttachment {
11238 view: target_view,
11239 resolve_target: None,
11240 depth_slice: None,
11241 ops: wgpu::Operations {
11242 load: next_load_op,
11243 store: wgpu::StoreOp::Store,
11244 },
11245 },
11246 )],
11247 depth_stencil_attachment: None,
11248 timestamp_writes: None,
11249 occlusion_query_set: None,
11250 multiview_mask: None,
11251 },
11252 );
11253 self.draw_prepared_glyphs(&mut render_pass, &prepared_glyphs)?;
11254 }
11255 pass_count = pass_count.saturating_add(1);
11256 self.scratch_glyph_cmds = prepared_glyphs.into_cmds();
11257 rendered_any = true;
11258 next_load_op = wgpu::LoadOp::Load;
11259 } else {
11260 let text_draws =
11261 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
11262 let prepared_images = self.prepare_text_image_draw_cmds(
11263 text_draws,
11264 viewport,
11265 root_scale,
11266 &mut staged_uploads,
11267 )?;
11268 if prepared_images.is_empty() {
11269 self.scratch_image_cmds = prepared_images.into_cmds();
11270 continue;
11271 }
11272 let upload_offset = frame_encoder
11273 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
11274 self.flush_staged_uploads_at(
11275 frame_encoder.encoder(),
11276 &staged_uploads,
11277 upload_offset,
11278 );
11279 {
11280 let mut render_pass = frame_encoder.encoder().begin_render_pass(
11281 &wgpu::RenderPassDescriptor {
11282 label: Some("Segment Text Pass"),
11283 color_attachments: &[Some(
11284 wgpu::RenderPassColorAttachment {
11285 view: target_view,
11286 resolve_target: None,
11287 depth_slice: None,
11288 ops: wgpu::Operations {
11289 load: next_load_op,
11290 store: wgpu::StoreOp::Store,
11291 },
11292 },
11293 )],
11294 depth_stencil_attachment: None,
11295 timestamp_writes: None,
11296 occlusion_query_set: None,
11297 multiview_mask: None,
11298 },
11299 );
11300 self.draw_prepared_images(
11301 &mut render_pass,
11302 &prepared_images,
11303 BlendMode::SrcOver,
11304 )?;
11305 }
11306 self.frame_stats.bump_text();
11307 pass_count = pass_count.saturating_add(1);
11308 self.scratch_image_cmds = prepared_images.into_cmds();
11309 rendered_any = true;
11310 next_load_op = wgpu::LoadOp::Load;
11311 }
11312 }
11313 SegmentBatchPlan::Composite { start, end } => {
11314 let batch_items: Vec<_> = ordered_items[start..end]
11315 .iter()
11316 .map(|(_, item)| match item {
11317 SegmentDrawItem::Composite(composite_index) => composites
11318 .get(*composite_index)
11319 .map(|(_, composite)| *composite)
11320 .ok_or_else(|| {
11321 "composite item index is outside the composite buffer"
11322 .to_string()
11323 }),
11324 other => Err(format!(
11325 "composite batch contains non-composite draw item: {other:?}"
11326 )),
11327 })
11328 .collect::<Result<_, _>>()?;
11329 let device = self.device.clone();
11330 self.effect_renderer.encode_composite_batch_to_view_pass(
11331 frame_encoder,
11332 &device,
11333 target_view,
11334 (width, height),
11335 next_load_op,
11336 &batch_items,
11337 );
11338 self.effect_renderer.record_composite_pass();
11339 pass_count = pass_count.saturating_add(1);
11340 rendered_any = true;
11341 next_load_op = wgpu::LoadOp::Load;
11342 }
11343 SegmentBatchPlan::ShaderComposite { start, end } => {
11344 let batch_items: Vec<_> = ordered_items[start..end]
11345 .iter()
11346 .map(|(_, item)| match item {
11347 SegmentDrawItem::ShaderComposite(composite_index) => {
11348 shader_composites
11349 .get(*composite_index)
11350 .map(|(_, composite)| *composite)
11351 .ok_or_else(|| {
11352 "shader composite item index is outside the shader composite buffer"
11353 .to_string()
11354 })
11355 }
11356 other => Err(format!(
11357 "shader composite batch contains non-shader-composite draw item: {other:?}"
11358 )),
11359 })
11360 .collect::<Result<Vec<_>, _>>()?;
11361 let device = self.device.clone();
11362 let encoded = self.effect_renderer.encode_shader_batch_src_over_to_view(
11363 frame_encoder,
11364 &device,
11365 target_view,
11366 (width, height),
11367 next_load_op,
11368 &batch_items,
11369 );
11370 if !encoded {
11371 return Err("shader composite batch failed to encode".to_string());
11372 }
11373 self.effect_renderer.record_composite_pass();
11374 self.effect_renderer.debug_effects.set(
11375 self.effect_renderer.debug_effects.get() + batch_items.len() as u32,
11376 );
11377 pass_count = pass_count.saturating_add(1);
11378 rendered_any = true;
11379 next_load_op = wgpu::LoadOp::Load;
11380 }
11381 SegmentBatchPlan::Retained { start, end } => {
11382 #[cfg(target_arch = "wasm32")]
11391 {
11392 let _ = (start, end);
11393 return Err("retained shape batches are native-only".to_string());
11394 }
11395 #[cfg(not(target_arch = "wasm32"))]
11396 {
11397 self.stage_replay_patches(&mut staged_uploads);
11398 for (_, item) in &ordered_items[start..end] {
11399 let SegmentDrawItem::Retained(index) = item else {
11400 return Err(format!(
11401 "retained batch contains non-retained draw item: {item:?}"
11402 ));
11403 };
11404 let retained = retained_draws.get(*index).ok_or_else(|| {
11405 format!("retained draw index {index} out of bounds")
11406 })?;
11407 if (*index as u32) < MAX_REPLAY_SLOTS
11408 && self.replay_slots.slots.contains_key(&retained.slot)
11409 {
11410 let transform = retained.transform.with_retained_paint();
11411 staged_uploads.stage_at(
11412 UploadTarget::ReplayTransform,
11413 *index as u64 * REPLAY_TRANSFORM_STRIDE,
11414 bytemuck::bytes_of(&transform),
11415 );
11416 }
11417 }
11418 let upload_offset = frame_encoder
11419 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
11420 self.flush_staged_uploads_at(
11421 frame_encoder.encoder(),
11422 &staged_uploads,
11423 upload_offset,
11424 );
11425 {
11426 let mut render_pass = frame_encoder.encoder().begin_render_pass(
11427 &wgpu::RenderPassDescriptor {
11428 label: Some("Segment Retained Pass"),
11429 color_attachments: &[Some(
11430 wgpu::RenderPassColorAttachment {
11431 view: target_view,
11432 resolve_target: None,
11433 depth_slice: None,
11434 ops: wgpu::Operations {
11435 load: next_load_op,
11436 store: wgpu::StoreOp::Store,
11437 },
11438 },
11439 )],
11440 depth_stencil_attachment: None,
11441 timestamp_writes: None,
11442 occlusion_query_set: None,
11443 multiview_mask: None,
11444 },
11445 );
11446 for (_, item) in &ordered_items[start..end] {
11447 if let SegmentDrawItem::Retained(index) = item {
11448 if let Some(retained) = retained_draws.get(*index) {
11449 self.draw_retained_batch(
11450 &mut render_pass,
11451 retained,
11452 *index,
11453 width,
11454 height,
11455 );
11456 }
11457 }
11458 }
11459 }
11460 pass_count = pass_count.saturating_add(1);
11461 rendered_any = true;
11462 next_load_op = wgpu::LoadOp::Load;
11463 }
11464 }
11465 }
11466 }
11467 Ok(SegmentRenderOutcome {
11468 rendered_any,
11469 pass_count,
11470 })
11471 })();
11472 self.restore_staged_uploads(staged_uploads);
11473 result
11474 }
11475
11476 fn viewport_uniforms(params: ViewportUniformParams) -> Uniforms {
11477 Uniforms {
11478 viewport: [params.width as f32, params.height as f32],
11479 viewport_offset: params.offset,
11480 }
11481 }
11482
11483 #[cfg(not(target_arch = "wasm32"))]
11484 fn stage_viewport_uniforms(
11485 &self,
11486 staged_uploads: &mut StagedBufferUploads,
11487 params: ViewportUniformParams,
11488 ) {
11489 let uniforms = Self::viewport_uniforms(params);
11490 staged_uploads.stage(UploadTarget::Uniform, bytemuck::bytes_of(&uniforms));
11491 }
11492
11493 #[cfg(not(target_arch = "wasm32"))]
11494 fn stage_retained_glyph_viewport_uniforms(
11495 &mut self,
11496 staged_uploads: &mut StagedBufferUploads,
11497 params: ViewportUniformParams,
11498 ) -> usize {
11499 let slot = self.claim_retained_glyph_uniform_slot();
11500 let uniforms = Self::viewport_uniforms(params);
11501 staged_uploads.stage_at(
11502 UploadTarget::RetainedGlyphUniform,
11503 self.retained_glyph_uniform_offset(slot),
11504 bytemuck::bytes_of(&uniforms),
11505 );
11506 slot
11507 }
11508
11509 #[cfg(not(target_arch = "wasm32"))]
11510 fn claim_retained_glyph_uniform_slot(&mut self) -> usize {
11511 let slot = self.retained_glyph_uniform_cursor;
11512 self.retained_glyph_uniform_cursor = self.retained_glyph_uniform_cursor.saturating_add(1);
11513 self.ensure_retained_glyph_uniform_capacity(slot.saturating_add(1));
11514 slot
11515 }
11516
11517 #[cfg(not(target_arch = "wasm32"))]
11518 fn retained_glyph_uniform_offset(&self, slot: usize) -> u64 {
11519 self.retained_glyph_uniform_stride * slot as u64
11520 }
11521
11522 #[cfg(not(target_arch = "wasm32"))]
11523 fn retained_glyph_uniform_dynamic_offset(&self, slot: usize) -> Result<u32, String> {
11524 let offset = self.retained_glyph_uniform_offset(slot);
11525 u32::try_from(offset).map_err(|_| {
11526 "retained glyph uniform offset exceeded WGPU dynamic offset range".to_string()
11527 })
11528 }
11529
11530 #[cfg(not(target_arch = "wasm32"))]
11531 fn ensure_retained_glyph_uniform_capacity(&mut self, required_slots: usize) {
11532 if required_slots <= self.retained_glyph_uniform_capacity {
11533 return;
11534 }
11535 let new_capacity = required_slots
11536 .next_power_of_two()
11537 .max(INITIAL_RETAINED_GLYPH_UNIFORM_SLOTS);
11538 self.retained_glyph_uniform_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
11539 label: Some("Retained Glyph Uniform Buffer"),
11540 size: self.retained_glyph_uniform_stride * new_capacity as u64,
11541 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
11542 mapped_at_creation: false,
11543 });
11544 self.retained_glyph_uniform_bind_group =
11545 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
11546 label: Some("Retained Glyph Uniform Bind Group"),
11547 layout: &self.retained_glyph_uniform_bind_group_layout,
11548 entries: &[wgpu::BindGroupEntry {
11549 binding: 0,
11550 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
11551 buffer: &self.retained_glyph_uniform_buffer,
11552 offset: 0,
11553 size: wgpu::BufferSize::new(std::mem::size_of::<Uniforms>() as u64),
11554 }),
11555 }],
11556 });
11557 self.retained_glyph_uniform_capacity = new_capacity;
11558 }
11559
11560 #[cfg(target_arch = "wasm32")]
11561 fn prepare_wasm_viewport_uniforms(&mut self, params: ViewportUniformParams) -> usize {
11562 let slot = self.claim_wasm_uniform_batch();
11563 let uniforms = Self::viewport_uniforms(params);
11564 let bytes = bytemuck::bytes_of(&uniforms);
11565 let upload_stats = self.frame_graph_executor.upload_buffer(
11566 &self.queue,
11567 &self.wasm_uniform_batches[slot].buffer,
11568 0,
11569 bytes,
11570 );
11571 self.frame_stats.record_command_stats(upload_stats);
11572 slot
11573 }
11574
11575 #[cfg(target_arch = "wasm32")]
11576 fn claim_wasm_uniform_batch(&mut self) -> usize {
11577 let slot = self.wasm_uniform_batch_cursor;
11578 self.wasm_uniform_batch_cursor += 1;
11579 while self.wasm_uniform_batches.len() <= slot {
11580 self.wasm_uniform_batches.push(UniformBatchBuffer::new(
11581 &self.device,
11582 &self.uniform_bind_group_layout,
11583 ));
11584 }
11585 slot
11586 }
11587
11588 #[cfg(target_arch = "wasm32")]
11589 fn claim_wasm_shape_batch(&mut self) -> usize {
11590 let slot = self.wasm_shape_batch_cursor;
11591 self.wasm_shape_batch_cursor += 1;
11592 while self.wasm_shape_batches.len() <= slot {
11593 self.wasm_shape_batches.push(ShapeBatchBuffers::new(
11594 &self.device,
11595 &self.shape_bind_group_layout,
11596 &self.identity_similarity_buffer,
11597 self.dummy_paint_buffer.as_ref(),
11598 self.shape_batch_limits,
11599 ));
11600 }
11601 slot
11602 }
11603
11604 #[cfg(target_arch = "wasm32")]
11605 fn claim_wasm_image_batch(&mut self) -> usize {
11606 let slot = self.wasm_image_batch_cursor;
11607 self.wasm_image_batch_cursor += 1;
11608 while self.wasm_image_batches.len() <= slot {
11609 self.wasm_image_batches
11610 .push(ImageBatchBuffers::new(&self.device));
11611 }
11612 slot
11613 }
11614
11615 #[cfg(target_arch = "wasm32")]
11616 fn write_wasm_buffer(&self, buffer: &wgpu::Buffer, bytes: &[u8]) {
11617 let upload_stats = self
11618 .frame_graph_executor
11619 .upload_buffer(&self.queue, buffer, 0, bytes);
11620 self.frame_stats.record_command_stats(upload_stats);
11621 }
11622
11623 fn take_staged_uploads(&mut self) -> StagedBufferUploads {
11624 let mut staged_uploads = std::mem::take(&mut self.staged_uploads);
11625 debug_assert!(
11626 staged_uploads.is_empty(),
11627 "renderer-owned staged uploads should be restored as empty scratch storage"
11628 );
11629 staged_uploads.clear();
11630 staged_uploads
11631 }
11632
11633 fn restore_staged_uploads(&mut self, mut staged_uploads: StagedBufferUploads) {
11634 staged_uploads.clear();
11635 self.staged_uploads = staged_uploads;
11636 }
11637
11638 #[cfg(not(target_arch = "wasm32"))]
11639 fn ensure_upload_buffer_capacity(&mut self, required_bytes: u64) {
11640 if required_bytes <= self.upload_buffer.size() {
11641 return;
11642 }
11643
11644 let new_size = required_bytes
11645 .next_power_of_two()
11646 .max(INITIAL_UPLOAD_BUFFER_BYTES);
11647 self.upload_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
11648 label: Some("Frame Upload Buffer"),
11649 size: new_size,
11650 usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
11651 mapped_at_creation: false,
11652 });
11653 }
11654
11655 fn flush_staged_uploads_at(
11656 &mut self,
11657 encoder: &mut wgpu::CommandEncoder,
11658 staged_uploads: &StagedBufferUploads,
11659 upload_buffer_offset: u64,
11660 ) {
11661 if staged_uploads.is_empty() {
11662 return;
11663 }
11664 debug_assert_eq!(
11665 upload_buffer_offset % wgpu::COPY_BUFFER_ALIGNMENT,
11666 0,
11667 "upload-buffer base offset must satisfy copy alignment"
11668 );
11669
11670 #[cfg(target_arch = "wasm32")]
11671 {
11672 let _ = upload_buffer_offset;
11673 let _ = encoder;
11674 debug_assert!(
11675 staged_uploads.is_empty(),
11676 "wasm draw uploads use retained per-batch resource slots"
11677 );
11678 return;
11679 }
11680
11681 #[cfg(not(target_arch = "wasm32"))]
11682 {
11683 self.ensure_upload_buffer_capacity(
11684 upload_buffer_offset + staged_uploads.bytes.len() as u64,
11685 );
11686 let upload_stats = self.frame_graph_executor.upload_buffer(
11687 &self.queue,
11688 &self.upload_buffer,
11689 upload_buffer_offset,
11690 &staged_uploads.bytes,
11691 );
11692 self.frame_stats.record_command_stats(upload_stats);
11693
11694 for copy in &staged_uploads.copies {
11695 let target_buffer = match copy.target {
11696 UploadTarget::Uniform => &self.uniform_buffer,
11697 UploadTarget::ShapeData => &self.shape_buffers.shape_buffer,
11698 UploadTarget::ShapeGradient => &self.shape_buffers.gradient_buffer,
11699 UploadTarget::ImageVertex => &self.image_vertex_buffer,
11700 UploadTarget::ImageIndex => &self.image_index_buffer,
11701 UploadTarget::RetainedGlyphUniform => &self.retained_glyph_uniform_buffer,
11702 UploadTarget::ReplayTransform => &self.replay_slots.transform_buffer,
11703 UploadTarget::ReplayPaintData(slot) => {
11704 let Some(entry) = self.replay_slots.slots.get(&slot) else {
11707 continue;
11708 };
11709 &entry.paint_buffer
11710 }
11711 };
11712 encoder.copy_buffer_to_buffer(
11713 &self.upload_buffer,
11714 upload_buffer_offset + copy.source_offset,
11715 target_buffer,
11716 copy.target_offset,
11717 copy.size,
11718 );
11719 }
11720 }
11721 }
11722
11723 #[allow(clippy::too_many_arguments)]
11724 fn encode_shadow_draw<C: FrameCommandRecorder>(
11725 &mut self,
11726 frame_encoder: &mut C,
11727 target_view: &wgpu::TextureView,
11728 shadow: &ShadowDraw,
11729 width: u32,
11730 height: u32,
11731 root_scale: f32,
11732 ) {
11733 if shadow.shapes.is_empty() && shadow.texts.is_empty() {
11734 return;
11735 }
11736
11737 let shape_bounds_opt = shadow
11738 .shapes
11739 .iter()
11740 .map(|(shape, _)| shape.rect)
11741 .reduce(|a, b| Rect {
11742 x: a.x.min(b.x),
11743 y: a.y.min(b.y),
11744 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
11745 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
11746 });
11747
11748 let text_bounds_opt = shadow
11749 .texts
11750 .iter()
11751 .map(|text| text.rect)
11752 .reduce(|a, b| Rect {
11753 x: a.x.min(b.x),
11754 y: a.y.min(b.y),
11755 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
11756 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
11757 });
11758
11759 let combined_bounds = match (shape_bounds_opt, text_bounds_opt) {
11760 (Some(s), Some(t)) => Some(Rect {
11761 x: s.x.min(t.x),
11762 y: s.y.min(t.y),
11763 width: (s.x + s.width).max(t.x + t.width) - s.x.min(t.x),
11764 height: (s.y + s.height).max(t.y + t.height) - s.y.min(t.y),
11765 }),
11766 (Some(s), None) => Some(s),
11767 (None, Some(t)) => Some(t),
11768 (None, None) => None,
11769 };
11770
11771 let Some(shape_bounds) = combined_bounds else {
11772 return;
11773 };
11774
11775 let blur_margin = blur_extent_margin(shadow.blur_radius);
11776 let source_blur_bounds = Rect {
11777 x: shape_bounds.x - blur_margin,
11778 y: shape_bounds.y - blur_margin,
11779 width: shape_bounds.width + blur_margin * 2.0,
11780 height: shape_bounds.height + blur_margin * 2.0,
11781 };
11782 let mut visible_blur_bounds = source_blur_bounds;
11783 if let Some(clip) = shadow.clip {
11784 let clip_expanded = Rect {
11785 x: clip.x - blur_margin,
11786 y: clip.y - blur_margin,
11787 width: clip.width + blur_margin * 2.0,
11788 height: clip.height + blur_margin * 2.0,
11789 };
11790 let Some(intersection) = visible_blur_bounds.intersect(clip_expanded) else {
11791 return;
11792 };
11793 visible_blur_bounds = intersection;
11794 }
11795 let processing_scissor =
11796 scissor_rect_for_rect(visible_blur_bounds, root_scale, width, height);
11797 if processing_scissor.is_none() {
11798 return;
11799 }
11800
11801 if shadow.blur_radius <= 0.0 {
11803 for (shape, blend_mode) in &shadow.shapes {
11804 self.encode_shapes_pass(
11805 frame_encoder,
11806 target_view,
11807 std::iter::once(shape),
11808 &shadow.brushes,
11809 *blend_mode,
11810 width,
11811 height,
11812 root_scale,
11813 wgpu::LoadOp::Load,
11814 [0.0, 0.0],
11815 );
11816 frame_encoder.record_pass();
11817 }
11818 if !shadow.texts.is_empty() {
11819 let mut staged_uploads = self.take_staged_uploads();
11820 let viewport = ViewportUniformParams {
11821 width,
11822 height,
11823 offset: [0.0, 0.0],
11824 };
11825 match self.prepare_text_image_draw_cmds(
11826 shadow.texts.iter(),
11827 viewport,
11828 root_scale,
11829 &mut staged_uploads,
11830 ) {
11831 Ok(prepared_images) if !prepared_images.is_empty() => {
11832 let upload_offset = frame_encoder
11833 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
11834 self.flush_staged_uploads_at(
11835 frame_encoder.encoder(),
11836 &staged_uploads,
11837 upload_offset,
11838 );
11839 let draw_result = {
11840 let mut render_pass = frame_encoder.encoder().begin_render_pass(
11841 &wgpu::RenderPassDescriptor {
11842 label: Some("Zero Blur Shadow Text Image Pass"),
11843 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
11844 view: target_view,
11845 resolve_target: None,
11846 depth_slice: None,
11847 ops: wgpu::Operations {
11848 load: wgpu::LoadOp::Load,
11849 store: wgpu::StoreOp::Store,
11850 },
11851 })],
11852 depth_stencil_attachment: None,
11853 timestamp_writes: None,
11854 occlusion_query_set: None,
11855 multiview_mask: None,
11856 },
11857 );
11858 self.draw_prepared_images(
11859 &mut render_pass,
11860 &prepared_images,
11861 BlendMode::SrcOver,
11862 )
11863 };
11864 self.scratch_image_cmds = prepared_images.into_cmds();
11865 if let Err(e) = draw_result {
11866 eprintln!("Failed to draw text for zero-blur shadow: {}", e);
11867 } else {
11868 self.frame_stats.bump_text();
11869 frame_encoder.record_pass();
11870 }
11871 }
11872 Ok(prepared_images) => {
11873 self.scratch_image_cmds = prepared_images.into_cmds();
11874 }
11875 Err(e) => {
11876 eprintln!("Failed to prepare text image for zero-blur shadow: {}", e);
11877 }
11878 }
11879 self.restore_staged_uploads(staged_uploads);
11880 }
11881 return;
11882 }
11883
11884 let Some(device_bounds) =
11886 device_pixel_bounds_for_rect(visible_blur_bounds, width, height, root_scale)
11887 else {
11888 return;
11889 };
11890 let bounds_x = device_bounds.x;
11891 let bounds_y = device_bounds.y;
11892 let bounds_w = device_bounds.width;
11893 let bounds_h = device_bounds.height;
11894 let pixel_radius = shadow.blur_radius * root_scale;
11895
11896 if shadow.texts.is_empty() && !shadow.shapes.is_empty() {
11897 if let Some(plan) = shape_shadow_surface_plan(
11898 &shadow.shapes,
11899 shadow.clip,
11900 shadow.blur_radius,
11901 width,
11902 height,
11903 root_scale,
11904 self.max_texture_dim(),
11905 ) {
11906 if self.encode_shape_only_blurred_shadow_draw(
11907 frame_encoder,
11908 target_view,
11909 shadow,
11910 plan.source_device_bounds,
11911 plan.pixel_radius,
11912 plan.processing_scissor,
11913 width,
11914 height,
11915 root_scale,
11916 ) {
11917 return;
11918 }
11919 }
11920 }
11921
11922 if !shadow.texts.is_empty() {
11923 self.frame_stats.record_shadow_text_blur_fallback();
11924 }
11925
11926 let device = self.device.clone();
11927 let source_descriptor =
11928 self.transient_offscreen_descriptor("Shadow Source", bounds_w, bounds_h);
11929 let source = frame_encoder.acquire_transient_offscreen(&device, source_descriptor);
11930 let viewport_offset = [bounds_x, bounds_y];
11931 let mut next_load_op = wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT);
11932 let source_outcome = self.encode_shadow_shape_source_passes(
11933 frame_encoder,
11934 &source.view,
11935 &shadow.shapes,
11936 &shadow.brushes,
11937 bounds_w,
11938 bounds_h,
11939 viewport_offset,
11940 root_scale,
11941 &mut next_load_op,
11942 );
11943 frame_encoder.record_passes(source_outcome.pass_count);
11944 let mut rendered_any = source_outcome.rendered_any;
11945
11946 if !shadow.texts.is_empty() {
11947 let mut shifted_texts = shadow.texts.clone();
11948 for text in &mut shifted_texts {
11949 text.rect.x -= viewport_offset[0] / root_scale;
11950 text.rect.y -= viewport_offset[1] / root_scale;
11951 if let Some(clip) = text.clip.as_mut() {
11952 clip.x -= viewport_offset[0] / root_scale;
11953 clip.y -= viewport_offset[1] / root_scale;
11954 }
11955 }
11956
11957 let mut staged_uploads = self.take_staged_uploads();
11958 let viewport = ViewportUniformParams {
11959 width: bounds_w,
11960 height: bounds_h,
11961 offset: [0.0, 0.0],
11962 };
11963 match self.prepare_text_image_draw_cmds(
11964 shifted_texts.iter(),
11965 viewport,
11966 root_scale,
11967 &mut staged_uploads,
11968 ) {
11969 Ok(prepared_images) if !prepared_images.is_empty() => {
11970 let upload_offset = frame_encoder
11971 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
11972 self.flush_staged_uploads_at(
11973 frame_encoder.encoder(),
11974 &staged_uploads,
11975 upload_offset,
11976 );
11977 let draw_result = {
11978 let mut render_pass = frame_encoder.encoder().begin_render_pass(
11979 &wgpu::RenderPassDescriptor {
11980 label: Some("Shadow Source Text Image Pass"),
11981 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
11982 view: &source.view,
11983 resolve_target: None,
11984 depth_slice: None,
11985 ops: wgpu::Operations {
11986 load: next_load_op,
11987 store: wgpu::StoreOp::Store,
11988 },
11989 })],
11990 depth_stencil_attachment: None,
11991 timestamp_writes: None,
11992 occlusion_query_set: None,
11993 multiview_mask: None,
11994 },
11995 );
11996 self.draw_prepared_images(
11997 &mut render_pass,
11998 &prepared_images,
11999 BlendMode::SrcOver,
12000 )
12001 };
12002 self.scratch_image_cmds = prepared_images.into_cmds();
12003 if let Err(e) = draw_result {
12004 eprintln!("Failed to draw text for shadow: {}", e);
12005 } else {
12006 self.frame_stats.bump_text();
12007 frame_encoder.record_pass();
12008 rendered_any = true;
12009 }
12010 }
12011 Ok(prepared_images) => {
12012 self.scratch_image_cmds = prepared_images.into_cmds();
12013 }
12014 Err(e) => {
12015 eprintln!("Failed to prepare text image for shadow: {}", e);
12016 }
12017 }
12018 self.restore_staged_uploads(staged_uploads);
12019 }
12020
12021 if !rendered_any {
12022 frame_encoder.release_transient_offscreen(source_descriptor, source);
12023 return;
12024 }
12025
12026 let (scratch_w, scratch_h) = crate::effect_renderer::blur_scratch_size(
12027 pixel_radius,
12028 pixel_radius,
12029 bounds_w,
12030 bounds_h,
12031 );
12032 let scratch_descriptor =
12033 self.transient_offscreen_descriptor("Shadow Blur Scratch", scratch_w, scratch_h);
12034 let scratch = frame_encoder.acquire_transient_offscreen(&device, scratch_descriptor);
12035 {
12036 self.effect_renderer.encode_blur_scissored_ping_pong_passes(
12037 frame_encoder,
12038 &device,
12039 &source,
12040 &scratch,
12041 &source.view,
12042 pixel_radius,
12043 pixel_radius,
12044 TileMode::Decal,
12045 None, );
12047 }
12048 frame_encoder.record_passes(2);
12049
12050 let clip_scissor = shadow
12051 .clip
12052 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
12053 let scissor = clip_scissor.or(processing_scissor);
12054 let rounded_mask = inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
12055 mask.rect[0] -= viewport_offset[0];
12058 mask.rect[1] -= viewport_offset[1];
12059 mask
12060 });
12061 let dest_viewport = Some((
12062 viewport_offset[0],
12063 viewport_offset[1],
12064 bounds_w as f32,
12065 bounds_h as f32,
12066 ));
12067 {
12068 self.effect_renderer
12069 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
12070 frame_encoder,
12071 &device,
12072 &source,
12073 target_view,
12074 1.0,
12075 wgpu::LoadOp::Load,
12076 scissor,
12077 rounded_mask,
12078 BlendMode::SrcOver,
12079 dest_viewport,
12080 CompositeSampleMode::Linear,
12081 );
12082 }
12083 frame_encoder.record_pass();
12084 self.effect_renderer.record_blur_pass();
12085 self.effect_renderer.record_composite_pass();
12086 frame_encoder.release_transient_offscreen(scratch_descriptor, scratch);
12087 frame_encoder.release_transient_offscreen(source_descriptor, source);
12088 }
12089
12090 #[allow(clippy::too_many_arguments)]
12091 fn encode_shadow_shape_source_passes<C: FrameCommandRecorder>(
12092 &mut self,
12093 frame_encoder: &mut C,
12094 source_view: &wgpu::TextureView,
12095 shapes: &[(DrawShape, BlendMode)],
12096 brushes: &[Brush],
12097 width: u32,
12098 height: u32,
12099 viewport_offset: [f32; 2],
12100 root_scale: f32,
12101 next_load_op: &mut wgpu::LoadOp<wgpu::Color>,
12102 ) -> ShadowSourceRenderOutcome {
12103 if shapes.is_empty() {
12104 return ShadowSourceRenderOutcome {
12105 rendered_any: false,
12106 pass_count: 0,
12107 };
12108 }
12109
12110 let mut staged_uploads = self.take_staged_uploads();
12111 let mut rendered_any = false;
12112 let mut pass_count = 0_u32;
12113 let mut start = 0usize;
12114 while start < shapes.len() {
12115 let blend_mode = supported_blend_mode(shapes[start].1);
12116 let mut end = start + 1;
12117 while end < shapes.len()
12118 && end - start < self.shape_batch_limits.max_shapes_per_batch
12119 && supported_blend_mode(shapes[end].1) == blend_mode
12120 {
12121 end += 1;
12122 }
12123
12124 staged_uploads.clear();
12125 let viewport = ViewportUniformParams {
12126 width,
12127 height,
12128 offset: viewport_offset,
12129 };
12130 let viewport_rect_logical = viewport_rect_in_logical(viewport, root_scale);
12131 let Some(prepared_shape) = self.prepare_shapes_batch(
12132 shapes[start..end]
12133 .iter()
12134 .map(|(shape, _blend_mode)| shape)
12135 .filter(|shape| match viewport_rect_logical {
12136 Some(rect) => shape_draw_is_visible_in_rect(shape, rect, root_scale),
12137 None => false,
12138 }),
12139 brushes,
12140 root_scale,
12141 viewport,
12142 &mut staged_uploads,
12143 ) else {
12144 start = end;
12145 continue;
12146 };
12147
12148 let upload_offset =
12149 frame_encoder.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
12150 self.flush_staged_uploads_at(frame_encoder.encoder(), &staged_uploads, upload_offset);
12151
12152 {
12153 let mut render_pass =
12154 frame_encoder
12155 .encoder()
12156 .begin_render_pass(&wgpu::RenderPassDescriptor {
12157 label: Some("Shadow Source Shape Pass"),
12158 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
12159 view: source_view,
12160 resolve_target: None,
12161 depth_slice: None,
12162 ops: wgpu::Operations {
12163 load: *next_load_op,
12164 store: wgpu::StoreOp::Store,
12165 },
12166 })],
12167 depth_stencil_attachment: None,
12168 timestamp_writes: None,
12169 occlusion_query_set: None,
12170 multiview_mask: None,
12171 });
12172 self.draw_prepared_shapes(
12173 &mut render_pass,
12174 blend_mode,
12175 prepared_shape,
12176 width,
12177 height,
12178 &[],
12179 );
12180 }
12181
12182 #[cfg(not(target_arch = "wasm32"))]
12183 {
12184 if fill_area_diag_enabled() {
12185 self.fill_area_diag
12191 .add_offscreen_target_fill(f64::from(width) * f64::from(height));
12192 }
12193 }
12194
12195 pass_count = pass_count.saturating_add(1);
12196 rendered_any = true;
12197 *next_load_op = wgpu::LoadOp::Load;
12198 start = end;
12199 }
12200
12201 self.restore_staged_uploads(staged_uploads);
12202 ShadowSourceRenderOutcome {
12203 rendered_any,
12204 pass_count,
12205 }
12206 }
12207
12208 #[allow(clippy::too_many_arguments)]
12209 fn encode_shape_only_blurred_shadow_draw<C: FrameCommandRecorder>(
12210 &mut self,
12211 frame_encoder: &mut C,
12212 target_view: &wgpu::TextureView,
12213 shadow: &ShadowDraw,
12214 device_bounds: DevicePixelBounds,
12215 pixel_radius: f32,
12216 processing_scissor: Option<(u32, u32, u32, u32)>,
12217 width: u32,
12218 height: u32,
12219 root_scale: f32,
12220 ) -> bool {
12221 let bounds_w = device_bounds.width;
12222 let bounds_h = device_bounds.height;
12223 let viewport_offset = [device_bounds.x, device_bounds.y];
12224 let cache_key = shape_shadow_surface_cache_key(
12225 &shadow.shapes,
12226 &shadow.brushes,
12227 device_bounds,
12228 pixel_radius,
12229 root_scale,
12230 );
12231
12232 if let Some(key) = cache_key {
12233 if let Some(cached) = self.cached_shadow_surface(&key) {
12234 self.frame_stats
12235 .record_shadow_shape_cache_hit(bounds_w, bounds_h);
12236 let clip_scissor = shadow
12237 .clip
12238 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
12239 let scissor = clip_scissor.or(processing_scissor);
12240 let rounded_mask =
12241 inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
12242 mask.rect[0] -= viewport_offset[0];
12243 mask.rect[1] -= viewport_offset[1];
12244 mask
12245 });
12246 let dest_viewport = Some((
12247 viewport_offset[0],
12248 viewport_offset[1],
12249 bounds_w as f32,
12250 bounds_h as f32,
12251 ));
12252 {
12253 self.effect_renderer
12254 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
12255 frame_encoder,
12256 &self.device,
12257 &cached,
12258 target_view,
12259 1.0,
12260 wgpu::LoadOp::Load,
12261 scissor,
12262 rounded_mask,
12263 BlendMode::SrcOver,
12264 dest_viewport,
12265 CompositeSampleMode::Nearest,
12266 );
12267 }
12268 frame_encoder.record_pass();
12269 self.effect_renderer.record_composite_pass();
12270 return true;
12271 }
12272 self.frame_stats
12273 .record_shadow_shape_cache_miss(bounds_w, bounds_h);
12274 self.frame_stats.maybe_print_shadow_shape_cache_miss(
12275 bounds_w,
12276 bounds_h,
12277 key.content_hash,
12278 pixel_radius,
12279 viewport_offset,
12280 shadow.shapes.len(),
12281 shadow.clip,
12282 );
12283 }
12284
12285 let device = self.device.clone();
12286 let source_descriptor =
12287 self.transient_offscreen_descriptor("Shape Shadow Source", bounds_w, bounds_h);
12288 let source_is_cacheable = cache_key.is_some();
12289 let source = if source_is_cacheable {
12290 self.acquire_retained_surface(bounds_w, bounds_h)
12291 } else {
12292 frame_encoder.acquire_transient_offscreen(&device, source_descriptor)
12293 };
12294 let (scratch_w, scratch_h) = crate::effect_renderer::blur_scratch_size(
12295 pixel_radius,
12296 pixel_radius,
12297 bounds_w,
12298 bounds_h,
12299 );
12300 let scratch_descriptor =
12301 self.transient_offscreen_descriptor("Shape Shadow Blur Scratch", scratch_w, scratch_h);
12302 let scratch = frame_encoder.acquire_transient_offscreen(&device, scratch_descriptor);
12303 let mut next_load_op = wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT);
12304 let source_outcome = self.encode_shadow_shape_source_passes(
12305 frame_encoder,
12306 &source.view,
12307 &shadow.shapes,
12308 &shadow.brushes,
12309 bounds_w,
12310 bounds_h,
12311 viewport_offset,
12312 root_scale,
12313 &mut next_load_op,
12314 );
12315 frame_encoder.record_passes(source_outcome.pass_count);
12316
12317 if !source_outcome.rendered_any {
12318 frame_encoder.release_transient_offscreen(scratch_descriptor, scratch);
12319 if source_is_cacheable {
12320 self.defer_offscreen_release(source);
12321 } else {
12322 frame_encoder.release_transient_offscreen(source_descriptor, source);
12323 }
12324 return true;
12325 }
12326
12327 {
12328 self.effect_renderer.encode_blur_scissored_ping_pong_passes(
12329 frame_encoder,
12330 &device,
12331 &source,
12332 &scratch,
12333 &source.view,
12334 pixel_radius,
12335 pixel_radius,
12336 TileMode::Decal,
12337 None,
12338 );
12339 }
12340 frame_encoder.record_passes(2);
12341
12342 let clip_scissor = shadow
12343 .clip
12344 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
12345 let scissor = clip_scissor.or(processing_scissor);
12346 let rounded_mask = inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
12347 mask.rect[0] -= viewport_offset[0];
12348 mask.rect[1] -= viewport_offset[1];
12349 mask
12350 });
12351 let dest_viewport = Some((
12352 viewport_offset[0],
12353 viewport_offset[1],
12354 bounds_w as f32,
12355 bounds_h as f32,
12356 ));
12357 {
12358 self.effect_renderer
12359 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
12360 frame_encoder,
12361 &device,
12362 &source,
12363 target_view,
12364 1.0,
12365 wgpu::LoadOp::Load,
12366 scissor,
12367 rounded_mask,
12368 BlendMode::SrcOver,
12369 dest_viewport,
12370 CompositeSampleMode::Nearest,
12371 );
12372 }
12373 frame_encoder.record_pass();
12374
12375 self.effect_renderer.record_blur_pass();
12376 self.effect_renderer.record_composite_pass();
12377 frame_encoder.release_transient_offscreen(scratch_descriptor, scratch);
12378 if let Some(key) = cache_key {
12379 self.insert_cached_shadow_surface(key, source);
12380 } else {
12381 frame_encoder.release_transient_offscreen(source_descriptor, source);
12382 }
12383 true
12384 }
12385
12386 fn prepare_shapes_batch<'a, I>(
12387 &mut self,
12388 layer_shapes: I,
12389 brushes: &[Brush],
12390 root_scale: f32,
12391 viewport: ViewportUniformParams,
12392 staged_uploads: &mut StagedBufferUploads,
12393 ) -> Option<PreparedShapeBatch>
12394 where
12395 I: Iterator<Item = &'a DrawShape>,
12396 {
12397 #[cfg(target_arch = "wasm32")]
12398 let _ = staged_uploads;
12399
12400 let shape_refs: Vec<&DrawShape> = layer_shapes
12405 .take(self.shape_batch_limits.max_shapes_per_batch)
12406 .collect();
12407 let shape_count = shape_refs.len();
12408 if shape_count == 0 {
12409 return None;
12410 }
12411
12412 let mut gradient_offsets: Vec<u32> = Vec::with_capacity(shape_count + 1);
12416 let mut total_gradient_stops = 0u32;
12417 gradient_offsets.push(0);
12418 for shape in &shape_refs {
12419 total_gradient_stops += shape_gradient_stop_count(shape, brushes) as u32;
12420 gradient_offsets.push(total_gradient_stops);
12421 }
12422
12423 self.scratch_shape_data.clear();
12424 self.scratch_shape_data
12425 .resize(shape_count, ShapeData::zeroed());
12426 self.scratch_gradients.clear();
12427 self.scratch_gradients
12428 .resize(total_gradient_stops as usize, GradientStop::zeroed());
12429
12430 convert_shapes_into_outputs(
12431 &shape_refs,
12432 brushes,
12433 &gradient_offsets,
12434 root_scale,
12435 &mut self.scratch_shape_data,
12436 &mut self.scratch_gradients,
12437 );
12438 #[cfg(not(target_arch = "wasm32"))]
12439 {
12440 if fill_area_diag_enabled() {
12441 self.fill_area_diag
12442 .add_shape_quads(&self.scratch_shape_data, viewport);
12443 }
12444 }
12445
12446 #[cfg(not(target_arch = "wasm32"))]
12447 {
12448 self.shape_buffers.ensure_capacity(
12449 &self.device,
12450 &self.shape_bind_group_layout,
12451 &self.identity_similarity_buffer,
12452 self.dummy_paint_buffer.as_ref(),
12453 shape_count,
12454 self.scratch_gradients.len().max(1),
12455 );
12456 self.stage_viewport_uniforms(staged_uploads, viewport);
12457 staged_uploads.stage(
12458 UploadTarget::ShapeData,
12459 bytemuck::cast_slice(&self.scratch_shape_data),
12460 );
12461 if !self.scratch_gradients.is_empty() {
12462 staged_uploads.stage(
12463 UploadTarget::ShapeGradient,
12464 bytemuck::cast_slice(&self.scratch_gradients),
12465 );
12466 }
12467 }
12468
12469 #[cfg(target_arch = "wasm32")]
12470 let shape_slot = {
12471 let slot = self.claim_wasm_shape_batch();
12472 {
12473 let buffers = &mut self.wasm_shape_batches[slot];
12474 buffers.ensure_capacity(
12475 &self.device,
12476 &self.shape_bind_group_layout,
12477 &self.identity_similarity_buffer,
12478 self.dummy_paint_buffer.as_ref(),
12479 shape_count,
12480 self.scratch_gradients.len().max(1),
12481 );
12482 }
12483 let buffers = &self.wasm_shape_batches[slot];
12484 self.write_wasm_buffer(
12485 &buffers.shape_buffer,
12486 bytemuck::cast_slice(&self.scratch_shape_data),
12487 );
12488 if !self.scratch_gradients.is_empty() {
12489 self.write_wasm_buffer(
12490 &buffers.gradient_buffer,
12491 bytemuck::cast_slice(&self.scratch_gradients),
12492 );
12493 }
12494 slot
12495 };
12496
12497 #[cfg(target_arch = "wasm32")]
12498 let uniform_slot = self.prepare_wasm_viewport_uniforms(viewport);
12499
12500 Some(PreparedShapeBatch {
12501 vertex_start: 0,
12502 vertex_count: shape_count as u32 * 6,
12503 has_gradient: total_gradient_stops > 0,
12504 #[cfg(target_arch = "wasm32")]
12505 shape_slot,
12506 #[cfg(target_arch = "wasm32")]
12507 uniform_slot,
12508 })
12509 }
12510
12511 #[cfg(not(target_arch = "wasm32"))]
12518 fn prepare_shapes_batch_direct<'a, I, C: FrameCommandRecorder>(
12519 &mut self,
12520 frame_encoder: &mut C,
12521 layer_shapes: I,
12522 brushes: &[Brush],
12523 root_scale: f32,
12524 viewport: ViewportUniformParams,
12525 staged_uploads: &mut StagedBufferUploads,
12526 ) -> Option<(PreparedShapeBatch, u64)>
12527 where
12528 I: Iterator<Item = &'a DrawShape>,
12529 {
12530 let shape_refs: Vec<&DrawShape> = layer_shapes
12531 .take(self.shape_batch_limits.max_shapes_per_batch)
12532 .collect();
12533 let shape_count = shape_refs.len();
12534 if shape_count == 0 {
12535 return None;
12536 }
12537
12538 let mut gradient_offsets: Vec<u32> = Vec::with_capacity(shape_count + 1);
12539 let mut total_gradient_stops = 0u32;
12540 gradient_offsets.push(0);
12541 for shape in &shape_refs {
12542 total_gradient_stops += shape_gradient_stop_count(shape, brushes) as u32;
12543 gradient_offsets.push(total_gradient_stops);
12544 }
12545
12546 self.shape_buffers.ensure_capacity(
12547 &self.device,
12548 &self.shape_bind_group_layout,
12549 &self.identity_similarity_buffer,
12550 self.dummy_paint_buffer.as_ref(),
12551 shape_count,
12552 (total_gradient_stops as usize).max(1),
12553 );
12554
12555 self.scratch_shape_data.clear();
12556 self.scratch_shape_data
12557 .resize(shape_count, ShapeData::zeroed());
12558 self.scratch_gradients.clear();
12559 self.scratch_gradients
12560 .resize(total_gradient_stops as usize, GradientStop::zeroed());
12561 convert_shapes_into_outputs(
12562 &shape_refs,
12563 brushes,
12564 &gradient_offsets,
12565 root_scale,
12566 &mut self.scratch_shape_data,
12567 &mut self.scratch_gradients,
12568 );
12569 if fill_area_diag_enabled() {
12570 self.fill_area_diag
12571 .add_shape_quads(&self.scratch_shape_data, viewport);
12572 }
12573
12574 let uniform_len = std::mem::size_of::<Uniforms>() as u64;
12581 let shape_len = (shape_count * std::mem::size_of::<ShapeData>()) as u64;
12582 let gradient_len = total_gradient_stops as u64 * std::mem::size_of::<GradientStop>() as u64;
12583 let total_len = uniform_len + shape_len + gradient_len;
12584 let upload_base = frame_encoder.allocate_staged_upload_bytes(total_len);
12585 self.ensure_upload_buffer_capacity(upload_base + total_len);
12586
12587 let shape_off = uniform_len;
12588 let gradient_off = shape_off + shape_len;
12589
12590 let uniforms = Self::viewport_uniforms(viewport);
12591 let mut upload_stats = self.frame_graph_executor.upload_buffer(
12592 &self.queue,
12593 &self.upload_buffer,
12594 upload_base,
12595 bytemuck::bytes_of(&uniforms),
12596 );
12597 upload_stats.upload_bytes += self
12598 .frame_graph_executor
12599 .upload_buffer(
12600 &self.queue,
12601 &self.upload_buffer,
12602 upload_base + shape_off,
12603 bytemuck::cast_slice(&self.scratch_shape_data),
12604 )
12605 .upload_bytes;
12606 if !self.scratch_gradients.is_empty() {
12607 upload_stats.upload_bytes += self
12608 .frame_graph_executor
12609 .upload_buffer(
12610 &self.queue,
12611 &self.upload_buffer,
12612 upload_base + gradient_off,
12613 bytemuck::cast_slice(&self.scratch_gradients),
12614 )
12615 .upload_bytes;
12616 }
12617 self.frame_stats.record_command_stats(upload_stats);
12618
12619 staged_uploads.record_upload_copy(UploadTarget::Uniform, 0, 0, uniform_len);
12620 staged_uploads.record_upload_copy(UploadTarget::ShapeData, shape_off, 0, shape_len);
12621 staged_uploads.record_upload_copy(
12622 UploadTarget::ShapeGradient,
12623 gradient_off,
12624 0,
12625 gradient_len,
12626 );
12627
12628 Some((
12629 PreparedShapeBatch {
12630 vertex_start: 0,
12631 vertex_count: shape_count as u32 * 6,
12632 has_gradient: total_gradient_stops > 0,
12633 },
12634 upload_base,
12635 ))
12636 }
12637
12638 pub(crate) fn replay_supported(&self) -> bool {
12644 #[cfg(target_arch = "wasm32")]
12648 {
12649 false
12650 }
12651 #[cfg(not(target_arch = "wasm32"))]
12652 {
12653 self.shape_batch_limits.storage
12654 }
12655 }
12656
12657 pub(crate) fn restore_replay_ack_confirmations(
12663 &mut self,
12664 confirmations: Vec<crate::frame_packet::ReplayConfirmation>,
12665 ) {
12666 #[cfg(not(target_arch = "wasm32"))]
12667 {
12668 self.replay_ack_confirmations = confirmations;
12669 }
12670 #[cfg(target_arch = "wasm32")]
12671 let _ = confirmations;
12672 }
12673
12674 #[cfg(not(target_arch = "wasm32"))]
12677 pub(crate) fn surface_format(&self) -> wgpu::TextureFormat {
12678 self.display_format
12679 }
12680
12681 #[cfg(not(target_arch = "wasm32"))]
12684 pub(crate) fn replay_ack_confirmations_capacity(&self) -> usize {
12685 self.replay_ack_confirmations.capacity()
12686 }
12687
12688 #[cfg(not(target_arch = "wasm32"))]
12700 pub(crate) fn take_replay_ack_early(
12701 &mut self,
12702 packet: &mut FramePacket,
12703 ) -> Option<(
12704 crate::frame_packet::ReplayAck,
12705 crate::frame_packet::ReplayFrameOps,
12706 )> {
12707 if packet.replay_preconsumed {
12708 return None;
12709 }
12710 let PacketRoot::Direct(root) = &packet.root else {
12711 return None;
12712 };
12713 let ops = std::mem::take(&mut packet.replay);
12714 let root_scale = packet.root_scale;
12715 let (ack, recycled) =
12716 self.consume_replay_ops(ops, &root.scene.shapes, &root.scene.brushes, root_scale);
12717 packet.replay_preconsumed = true;
12718 Some((ack, recycled))
12719 }
12720
12721 #[cfg(not(target_arch = "wasm32"))]
12736 fn consume_replay_ops(
12737 &mut self,
12738 mut ops: crate::frame_packet::ReplayFrameOps,
12739 shapes: &[DrawShape],
12740 brushes: &[Brush],
12741 root_scale: f32,
12742 ) -> (
12743 crate::frame_packet::ReplayAck,
12744 crate::frame_packet::ReplayFrameOps,
12745 ) {
12746 let acked_frame = ops.frame;
12750 if ops.generation < self.store_feed_generation {
12751 self.replay_generation_drops += 1;
12757 log::warn!(
12758 "[command-feed] dropping replay ops of generation {} against store \
12759 generation {} ({} captures, {} patches, {} releases; lifetime drops {})",
12760 ops.generation,
12761 self.store_feed_generation,
12762 ops.captures.len(),
12763 ops.color_patches.len(),
12764 ops.releases.len(),
12765 self.replay_generation_drops,
12766 );
12767 ops.captures.clear();
12768 ops.color_patches.clear();
12769 ops.releases.clear();
12770 return (
12771 crate::frame_packet::ReplayAck {
12772 generation: self.store_feed_generation,
12773 frame: acked_frame,
12774 confirmations: Vec::new(),
12775 },
12776 ops,
12777 );
12778 }
12779 if ops.generation > self.store_feed_generation {
12780 self.store_feed_generation = ops.generation;
12787 }
12788 let generation = ops.generation;
12789 for slot in ops.releases.drain(..) {
12792 self.release_replay_slot(slot);
12793 }
12794 let mut confirmations = std::mem::take(&mut self.replay_ack_confirmations);
12797 debug_assert!(confirmations.is_empty());
12798 let mut refs: Vec<&DrawShape> = Vec::new();
12802 for capture in ops.captures.drain(..) {
12803 if capture.frame != ops.frame {
12804 log::warn!(
12811 "[command-feed] dropping stale capture for slot {} of {:?} \
12812 (queued frame {}, ops frame {})",
12813 capture.key.1,
12814 capture.key.0,
12815 capture.frame,
12816 ops.frame,
12817 );
12818 continue;
12819 }
12820 let end = capture.shape_start + capture.shape_count;
12821 let Some(slice) = shapes.get(capture.shape_start..end) else {
12822 continue;
12823 };
12824 refs.clear();
12825 refs.extend(slice.iter());
12826 let Some(gpu_slot) = self.capture_replay_slot(&refs, brushes, root_scale) else {
12827 continue;
12828 };
12829 confirmations.push((capture.key, gpu_slot));
12830 }
12831 self.replay_color_patches.clear();
12839 std::mem::swap(&mut self.replay_color_patches, &mut ops.color_patches);
12840 (
12841 crate::frame_packet::ReplayAck {
12842 generation,
12843 frame: acked_frame,
12844 confirmations,
12845 },
12846 ops,
12847 )
12848 }
12849
12850 #[cfg(not(target_arch = "wasm32"))]
12855 pub(crate) fn replay_generation_drops(&self) -> u64 {
12856 self.replay_generation_drops
12857 }
12858
12859 #[cfg(not(target_arch = "wasm32"))]
12867 pub(crate) fn replay_ops_roundtrip_for_tests(&mut self, generation_skew: u64) -> usize {
12868 let generation = self.store_feed_generation.wrapping_add(generation_skew);
12869 let ops = crate::shape_replay::SHAPE_REPLAY
12870 .with(|state| state.borrow_mut().take_frame_ops(generation));
12871 let (ack, recycled) = self.consume_replay_ops(ops, &[], &[], 1.0);
12872 let confirmed = ack.confirmations.len();
12873 self.replay_ack_confirmations = crate::shape_replay::SHAPE_REPLAY
12874 .with(|state| state.borrow_mut().apply_ack(ack, recycled));
12875 confirmed
12876 }
12877
12878 #[cfg(not(target_arch = "wasm32"))]
12885 fn stage_replay_patches(&mut self, staged_uploads: &mut StagedBufferUploads) {
12886 std::mem::swap(
12894 &mut self.replay_color_patches,
12895 &mut self.color_patch_scratch,
12896 );
12897 let total_patches = self.color_patch_scratch.len();
12898 if total_patches == 0 {
12899 self.replay_upload_stats.note_frame(0, 0, 0, 0, 0);
12900 return;
12901 }
12902
12903 #[derive(Clone, Copy)]
12909 struct DirtySpan {
12910 paint_min: u32,
12911 paint_max: u32,
12912 }
12913 const CLEAN: DirtySpan = DirtySpan {
12914 paint_min: u32::MAX,
12915 paint_max: 0,
12916 };
12917 let mut dirty: std::collections::HashMap<
12918 u32,
12919 DirtySpan,
12920 cranpose_ui_graphics::FxBuildHasher,
12921 > = std::collections::HashMap::default();
12922
12923 for patch in &self.color_patch_scratch {
12925 let Some(slot) = self.replay_slots.slots.get_mut(&patch.slot) else {
12926 continue;
12927 };
12928 let Some(paint) = slot.paint_mirror.get_mut(patch.shape_index as usize) else {
12929 continue;
12930 };
12931 *paint = patch.color;
12932 let span = dirty.entry(patch.slot).or_insert(CLEAN);
12933 span.paint_min = span.paint_min.min(patch.shape_index);
12934 span.paint_max = span.paint_max.max(patch.shape_index);
12935 }
12936
12937 let mut uploaded_records = 0u64;
12938 let mut uploaded_bytes = 0u64;
12939 let slots_touched = dirty.len() as u64;
12940 for (slot_id, span) in dirty {
12941 let Some(slot) = self.replay_slots.slots.get(&slot_id) else {
12942 continue;
12943 };
12944 if span.paint_min <= span.paint_max {
12945 let range = span.paint_min as usize..span.paint_max as usize + 1;
12946 uploaded_records += range.len() as u64;
12947 uploaded_bytes += (range.len() * std::mem::size_of::<[f32; 4]>()) as u64;
12948 staged_uploads.stage_at(
12949 UploadTarget::ReplayPaintData(slot_id),
12950 range.start as u64 * std::mem::size_of::<[f32; 4]>() as u64,
12951 bytemuck::cast_slice(&slot.paint_mirror[range]),
12952 );
12953 }
12954 }
12955 let ideal_bytes = total_patches as u64 * 16;
12958 self.replay_upload_stats.note_frame(
12959 total_patches as u64,
12960 slots_touched,
12961 uploaded_records,
12962 uploaded_bytes,
12963 ideal_bytes,
12964 );
12965 if cranpose_core::env_flag!("CRANPOSE_COMMAND_REPLAY_DIAG") {
12966 log::warn!(
12967 "[replay-upload] frame: {} patches -> {} records / {:.1} KB staged \
12968 across {} slots (color-only {:.1} KB)",
12969 total_patches,
12970 uploaded_records,
12971 uploaded_bytes as f64 / 1024.0,
12972 slots_touched,
12973 ideal_bytes as f64 / 1024.0,
12974 );
12975 }
12976 self.color_patch_scratch.clear();
12977 }
12978
12979 #[cfg(not(target_arch = "wasm32"))]
12982 pub(crate) fn capture_replay_slot(
12983 &mut self,
12984 shape_refs: &[&DrawShape],
12985 brushes: &[Brush],
12986 root_scale: f32,
12987 ) -> Option<u32> {
12988 if !self.shape_batch_limits.storage || shape_refs.is_empty() {
12989 return None;
12990 }
12991 let id = self.replay_slots.free_ids.pop()?;
12992 let shape_count = shape_refs.len();
12993
12994 let mut gradient_offsets: Vec<u32> = Vec::with_capacity(shape_count + 1);
12995 let mut total_gradient_stops = 0u32;
12996 gradient_offsets.push(0);
12997 for shape in shape_refs {
12998 total_gradient_stops += shape_gradient_stop_count(shape, brushes) as u32;
12999 gradient_offsets.push(total_gradient_stops);
13000 }
13001
13002 let mut shape_data = std::mem::take(&mut self.replay_capture_shape_scratch);
13005 shape_data.clear();
13006 shape_data.resize(shape_count, ShapeData::zeroed());
13007 let mut gradients = std::mem::take(&mut self.replay_capture_gradient_scratch);
13008 gradients.clear();
13009 gradients.resize(
13010 (total_gradient_stops as usize).max(1),
13011 GradientStop::zeroed(),
13012 );
13013 convert_shapes_into_outputs(
13014 shape_refs,
13015 brushes,
13016 &gradient_offsets,
13017 root_scale,
13018 &mut shape_data,
13019 &mut gradients,
13020 );
13021
13022 let shape_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
13023 label: Some("Replay Shape Buffer"),
13024 size: (std::mem::size_of::<ShapeData>() * shape_count) as u64,
13025 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
13026 mapped_at_creation: true,
13027 });
13028 shape_buffer
13029 .slice(..)
13030 .get_mapped_range_mut()
13031 .copy_from_slice(bytemuck::cast_slice(&shape_data));
13032 shape_buffer.unmap();
13033
13034 let gradient_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
13035 label: Some("Replay Gradient Buffer"),
13036 size: (std::mem::size_of::<GradientStop>() * gradients.len()) as u64,
13037 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
13038 mapped_at_creation: true,
13039 });
13040 gradient_buffer
13041 .slice(..)
13042 .get_mapped_range_mut()
13043 .copy_from_slice(bytemuck::cast_slice(&gradients));
13044 gradient_buffer.unmap();
13045
13046 let mut mesh_fill_records: Option<Vec<FillDiagShapeRecord>> = None;
13050 let mut submitted_area_scale = 1.0f32;
13051 let mesh = if arc_mesh_enabled() {
13052 match build_arc_mesh_vertices(&shape_data, retained_mesh_min_px2()) {
13053 Some(build) => {
13054 let meshed_shapes = build.meshed_arcs + build.meshed_rims;
13055 let within_stretch_cap = build.meshed_stretches <= MESH_SLOT_MAX_STRETCHES;
13060 let cut = if build.quad_area > 0.0 {
13061 (1.0 - build.mesh_area / build.quad_area) * 100.0
13062 } else {
13063 0.0
13064 };
13065 log::warn!(
13072 "[arc-mesh] slot {id}: {} arcs + {} rims meshed ({} segs, \
13073 {} stretches), {} instanced; {} unique verts / {} indices; \
13074 quad_px {:.0} -> submit_px {:.0} (-{:.1}%)",
13075 build.meshed_arcs,
13076 build.meshed_rims,
13077 build.meshed_segments,
13078 build.meshed_stretches,
13079 build.passthrough,
13080 build.vertices.len(),
13081 build.indices.len(),
13082 build.quad_area,
13083 build.mesh_area,
13084 cut,
13085 );
13086 if !within_stretch_cap {
13087 log::warn!(
13088 "[arc-mesh] slot {id}: {} meshed stretches exceed the \
13089 {MESH_SLOT_MAX_STRETCHES}-stretch switch cap; slot stays instanced",
13090 build.meshed_stretches,
13091 );
13092 }
13093 let keep_mesh = meshed_shapes > 0 && within_stretch_cap;
13094 if keep_mesh && build.quad_area > 0.0 {
13095 submitted_area_scale =
13101 (build.mesh_area / build.quad_area).clamp(0.05, 1.0) as f32;
13102 }
13103 if keep_mesh && fill_area_diag_enabled() {
13104 mesh_fill_records = Some(fill_diag_capture_records(
13105 &shape_data,
13106 Some((&build.vertices, &build.indices, &build.index_prefix)),
13107 ));
13108 }
13109 keep_mesh.then(|| {
13112 let vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
13113 label: Some("Replay Mesh Vertex Buffer"),
13114 size: (std::mem::size_of::<MeshVertex>() * build.vertices.len()) as u64,
13115 usage: wgpu::BufferUsages::VERTEX,
13116 mapped_at_creation: true,
13117 });
13118 vertex_buffer
13119 .slice(..)
13120 .get_mapped_range_mut()
13121 .copy_from_slice(bytemuck::cast_slice(&build.vertices));
13122 vertex_buffer.unmap();
13123 let index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
13124 label: Some("Replay Mesh Index Buffer"),
13125 size: (std::mem::size_of::<u32>() * build.indices.len()) as u64,
13126 usage: wgpu::BufferUsages::INDEX,
13127 mapped_at_creation: true,
13128 });
13129 index_buffer
13130 .slice(..)
13131 .get_mapped_range_mut()
13132 .copy_from_slice(bytemuck::cast_slice(&build.indices));
13133 index_buffer.unmap();
13134 ReplaySlotMesh {
13135 vertex_buffer,
13136 index_buffer,
13137 index_prefix: build.index_prefix,
13138 meshed_arcs: build.meshed_arcs,
13139 meshed_rims: build.meshed_rims,
13140 passthrough: build.passthrough,
13141 }
13142 })
13143 }
13144 None => {
13145 log::warn!(
13146 "[arc-mesh] slot {id}: geometry byte budget overflowed for \
13147 {shape_count} shapes; whole slot stays instanced"
13148 );
13149 None
13150 }
13151 }
13152 } else {
13153 None
13154 };
13155
13156 let fill_diag_shapes = if fill_area_diag_enabled() {
13157 let records =
13158 mesh_fill_records.unwrap_or_else(|| fill_diag_capture_records(&shape_data, None));
13159 self.fill_area_diag.note_retained_capture(id, &records);
13162 records
13163 } else {
13164 Vec::new()
13165 };
13166
13167 let mut shape_aabbs = Vec::with_capacity(shape_count);
13172 let mut area_prefix = Vec::with_capacity(shape_count + 1);
13173 area_prefix.push(0.0f32);
13174 for shape in &shape_data {
13175 let corners = [
13176 [shape.quad01[0], shape.quad01[1]],
13177 [shape.quad01[2], shape.quad01[3]],
13178 [shape.quad23[0], shape.quad23[1]],
13179 [shape.quad23[2], shape.quad23[3]],
13180 ];
13181 let mut min_x = f32::INFINITY;
13182 let mut min_y = f32::INFINITY;
13183 let mut max_x = f32::NEG_INFINITY;
13184 let mut max_y = f32::NEG_INFINITY;
13185 for corner in corners {
13186 min_x = min_x.min(corner[0]);
13187 min_y = min_y.min(corner[1]);
13188 max_x = max_x.max(corner[0]);
13189 max_y = max_y.max(corner[1]);
13190 }
13191 shape_aabbs.push([min_x, min_y, max_x, max_y]);
13192 let ring = [corners[0], corners[1], corners[3], corners[2]];
13195 let mut doubled = 0.0f32;
13196 for i in 0..4 {
13197 let a = ring[i];
13198 let b = ring[(i + 1) % 4];
13199 doubled += a[0] * b[1] - b[0] * a[1];
13200 }
13201 let area = (doubled * 0.5).abs();
13202 let running = *area_prefix.last().expect("prefix seeded with 0.0");
13203 area_prefix.push(running + area);
13204 }
13205
13206 let paint: Vec<[f32; 4]> = shape_data.iter().map(|shape| shape.color).collect();
13209 let paint_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
13210 label: Some("Replay Paint Buffer"),
13211 size: (std::mem::size_of::<[f32; 4]>() * shape_count) as u64,
13212 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
13213 mapped_at_creation: true,
13214 });
13215 paint_buffer
13216 .slice(..)
13217 .get_mapped_range_mut()
13218 .copy_from_slice(bytemuck::cast_slice(&paint));
13219 paint_buffer.unmap();
13220
13221 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
13222 label: Some("Replay Shape Bind Group"),
13223 layout: &self.shape_bind_group_layout,
13224 entries: &[
13225 wgpu::BindGroupEntry {
13226 binding: 0,
13227 resource: shape_buffer.as_entire_binding(),
13228 },
13229 wgpu::BindGroupEntry {
13230 binding: 1,
13231 resource: gradient_buffer.as_entire_binding(),
13232 },
13233 wgpu::BindGroupEntry {
13237 binding: 2,
13238 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
13239 buffer: &self.replay_slots.transform_buffer,
13240 offset: 0,
13241 size: Some(
13242 std::num::NonZeroU64::new(
13243 std::mem::size_of::<SimilarityTransform>() as u64
13244 )
13245 .expect("similarity transform is non-empty"),
13246 ),
13247 }),
13248 },
13249 wgpu::BindGroupEntry {
13250 binding: 3,
13251 resource: paint_buffer.as_entire_binding(),
13252 },
13253 ],
13254 });
13255
13256 let capture_epoch = self.replay_slots.next_capture_epoch;
13257 self.replay_slots.next_capture_epoch += 1;
13258 self.replay_slots.slots.insert(
13259 id,
13260 ReplaySlot {
13261 paint_buffer,
13262 bind_group,
13263 shape_count: shape_count as u32,
13264 paint_mirror: paint,
13265 mesh,
13266 capture_epoch,
13267 has_gradient: total_gradient_stops > 0,
13268 fill_diag_shapes,
13269 shape_aabbs,
13270 area_prefix,
13271 submitted_area_scale,
13272 },
13273 );
13274 self.replay_capture_shape_scratch = shape_data;
13277 self.replay_capture_gradient_scratch = gradients;
13278 Some(id)
13279 }
13280
13281 #[cfg(not(target_arch = "wasm32"))]
13283 pub(crate) fn release_replay_slot(&mut self, id: u32) {
13284 if self.replay_slots.slots.remove(&id).is_some() {
13285 self.replay_slots.free_ids.push(id);
13286 self.retained_bundle_cache.clear();
13292 self.segment_surfaces.drop_slot(id);
13295 }
13296 }
13297
13298 #[cfg(not(target_arch = "wasm32"))]
13302 #[doc(hidden)]
13303 pub fn segment_surface_stats(&self) -> (u64, u64, u64, u64, u64) {
13304 let stats = &self.segment_surfaces.stats;
13305 (
13306 stats.captures,
13307 stats.composites,
13308 stats.dirty_recaptures,
13309 stats.rejected_churn,
13310 stats.rejected_economics,
13311 )
13312 }
13313
13314 #[cfg(not(target_arch = "wasm32"))]
13317 #[doc(hidden)]
13318 pub fn instanced_quads_active(&self) -> bool {
13319 self.instanced_quads.is_some()
13320 }
13321
13322 #[cfg(not(target_arch = "wasm32"))]
13325 #[doc(hidden)]
13326 pub fn replay_slot_mesh_stats(&self) -> (usize, usize) {
13327 let meshed = self
13328 .replay_slots
13329 .slots
13330 .values()
13331 .filter(|slot| slot.mesh.is_some())
13332 .count();
13333 (meshed, self.replay_slots.slots.len())
13334 }
13335
13336 #[cfg(not(target_arch = "wasm32"))]
13340 #[doc(hidden)]
13341 pub fn replay_slot_mesh_engagement(&self) -> (usize, usize, usize) {
13342 self.replay_slots
13343 .slots
13344 .values()
13345 .filter_map(|slot| slot.mesh.as_ref())
13346 .fold((0, 0, 0), |(arcs, rims, passthrough), mesh| {
13347 (
13348 arcs + mesh.meshed_arcs,
13349 rims + mesh.meshed_rims,
13350 passthrough + mesh.passthrough,
13351 )
13352 })
13353 }
13354
13355 #[cfg(not(target_arch = "wasm32"))]
13361 #[allow(clippy::too_many_arguments)]
13362 fn plan_segment_surfaces(
13363 &mut self,
13364 segment_surfaces: &mut SegmentSurfaceCache,
13365 ordered_items: &[(usize, SegmentDrawItem)],
13366 chunk: &SegmentDrawChunkPlan,
13367 retained_draws: &[RetainedDraw],
13368 staged_uploads: &mut StagedBufferUploads,
13369 captures: &mut Vec<SegmentCaptureJob>,
13370 composites: &mut Vec<(usize, SegmentCompositePlan)>,
13371 ) {
13372 segment_surfaces.ensure_dirty_map(
13373 self.replay_color_patches
13374 .iter()
13375 .map(|patch| (patch.slot, patch.shape_index)),
13376 );
13377 let max_texture_dim = self.effect_renderer.max_texture_dim();
13378 for batch in chunk.iter() {
13379 let SegmentBatchPlan::Retained { start, end } = batch else {
13380 continue;
13381 };
13382 for (_, item) in &ordered_items[start..end] {
13383 let SegmentDrawItem::Retained(index) = item else {
13384 continue;
13385 };
13386 if (*index as u32) >= MAX_REPLAY_SLOTS {
13389 continue;
13390 }
13391 let Some(retained) = retained_draws.get(*index) else {
13392 continue;
13393 };
13394 let transform = retained.transform;
13395 let (key, capture_epoch, dirty) = {
13396 let Some(slot) = self.replay_slots.slots.get(&retained.slot) else {
13397 continue;
13398 };
13399 let first = retained.first_shape.min(slot.shape_count);
13400 let last = retained
13401 .first_shape
13402 .saturating_add(retained.shape_count)
13403 .min(slot.shape_count);
13404 if first >= last {
13405 continue;
13406 }
13407 (
13408 SegmentSurfaceKey {
13409 slot: retained.slot,
13410 first_shape: first,
13411 shape_count: last - first,
13412 },
13413 slot.capture_epoch,
13414 segment_surfaces.range_dirty(retained.slot, first, last),
13415 )
13416 };
13417 let first = key.first_shape;
13418 let last = key.first_shape + key.shape_count;
13419 let slots = &self.replay_slots.slots;
13420 let decision =
13421 segment_surfaces.decide(key, capture_epoch, dirty, transform.scale, || {
13422 let slot = slots.get(&key.slot)?;
13423 plan_segment_capture_geometry(slot, first, last, transform, max_texture_dim)
13424 });
13425 let SegmentSurfaceDecision::Composite { capture } = decision else {
13426 continue;
13427 };
13428 if let Some(plan) = capture {
13429 let texture = segment_surfaces
13430 .take_texture_for_recapture(&key, &plan.rect)
13431 .unwrap_or_else(|| {
13432 self.acquire_segment_surface(plan.rect.width, plan.rect.height)
13433 });
13434 segment_surfaces.install_entry(
13435 key,
13436 capture_epoch,
13437 transform.center,
13438 transform.rot,
13439 transform.scale,
13440 plan.rect,
13441 texture,
13442 );
13443 staged_uploads.stage_at(
13447 UploadTarget::ReplayTransform,
13448 (MAX_REPLAY_SLOTS + plan.index) as u64 * REPLAY_TRANSFORM_STRIDE,
13449 bytemuck::bytes_of(&transform.with_retained_paint()),
13450 );
13451 let uniforms = Self::viewport_uniforms(ViewportUniformParams {
13458 width: plan.rect.width,
13459 height: plan.rect.height,
13460 offset: plan.rect.origin,
13461 });
13462 let device = self.device.clone();
13463 let capture_uniforms =
13464 segment_surfaces.capture_uniforms(&device, &self.uniform_bind_group_layout);
13465 let upload_stats = self.frame_graph_executor.upload_buffer(
13466 &self.queue,
13467 &capture_uniforms.buffer,
13468 plan.index as u64 * SEGMENT_CAPTURE_UNIFORM_STRIDE,
13469 bytemuck::bytes_of(&uniforms),
13470 );
13471 self.frame_stats.record_command_stats(upload_stats);
13472 captures.push(SegmentCaptureJob {
13473 key,
13474 first,
13475 last,
13476 capture_index: plan.index,
13477 });
13478 composites.push((
13482 *index,
13483 SegmentCompositePlan {
13484 key,
13485 dest_quad: segment_identity_quad(&plan.rect),
13486 inverse: segment_identity_inverse(&plan.rect),
13487 identity: true,
13488 integer_translation: true,
13489 },
13490 ));
13491 } else {
13492 let Some(entry) = segment_surfaces.entry(&key) else {
13493 continue;
13494 };
13495 let t_now =
13496 Affine2::from_similarity(transform.center, transform.rot, transform.scale);
13497 let t_cap =
13498 Affine2::from_similarity(entry.cap_center, entry.cap_rot, entry.cap_scale);
13499 let Some(cap_inverse) = t_cap.invert() else {
13500 segment_surfaces.remove(&key);
13501 continue;
13502 };
13503 let effective = t_now.compose(&cap_inverse);
13504 let rect = entry.rect;
13505 let plan = if effective.is_identity_for_sampling() {
13506 SegmentCompositePlan {
13509 key,
13510 dest_quad: segment_identity_quad(&rect),
13511 inverse: segment_identity_inverse(&rect),
13512 identity: true,
13513 integer_translation: true,
13514 }
13515 } else {
13516 let Some(inverse) = effective.invert() else {
13517 segment_surfaces.remove(&key);
13518 continue;
13519 };
13520 SegmentCompositePlan {
13521 key,
13522 dest_quad: segment_identity_quad(&rect).map(|c| effective.apply(c)),
13523 inverse: [
13524 [
13525 inverse.l[0][0],
13526 inverse.l[0][1],
13527 inverse.t[0] - rect.origin[0],
13528 ],
13529 [
13530 inverse.l[1][0],
13531 inverse.l[1][1],
13532 inverse.t[1] - rect.origin[1],
13533 ],
13534 [0.0, 0.0, 1.0],
13535 ],
13536 identity: false,
13537 integer_translation: effective.is_integer_translation_for_sampling(),
13538 }
13539 };
13540 composites.push((*index, plan));
13541 }
13542 }
13543 }
13544 }
13545
13546 #[cfg(not(target_arch = "wasm32"))]
13550 fn draw_retained_batch(
13551 &self,
13552 render_pass: &mut wgpu::RenderPass<'_>,
13553 retained: &RetainedDraw,
13554 retained_index: usize,
13555 width: u32,
13556 height: u32,
13557 ) {
13558 let Some(slot) = self.replay_slots.slots.get(&retained.slot) else {
13559 return;
13560 };
13561 if retained_index as u32 >= MAX_REPLAY_SLOTS {
13562 return;
13563 }
13564 let first = retained.first_shape.min(slot.shape_count);
13565 let last = retained
13566 .first_shape
13567 .saturating_add(retained.shape_count)
13568 .min(slot.shape_count);
13569 if first >= last {
13570 return;
13571 }
13572 if fill_area_diag_enabled() {
13573 self.fill_area_diag.add_retained_range(
13574 &slot.fill_diag_shapes,
13575 first,
13576 last,
13577 &retained.transform,
13578 );
13579 }
13580 self.frame_stats.bump_shapes();
13581 render_pass.set_scissor_rect(0, 0, width, height);
13582 let draws =
13583 self.encode_retained_op(
13584 slot,
13585 first,
13586 last,
13587 retained_index as u32,
13588 &mut |cmd| match cmd {
13589 RetainedCmd::Pipeline(pipeline) => {
13590 render_pass.set_pipeline(self.retained_pipeline(pipeline))
13591 }
13592 RetainedCmd::Uniforms(group) => render_pass.set_bind_group(0, group, &[]),
13593 RetainedCmd::SlotBindings(group, offset) => {
13594 render_pass.set_bind_group(1, group, &[offset])
13595 }
13596 RetainedCmd::MeshVertices(buffer) => {
13597 render_pass.set_vertex_buffer(0, buffer.slice(..))
13598 }
13599 RetainedCmd::Index(buffer, format) => {
13600 render_pass.set_index_buffer(buffer.slice(..), format)
13601 }
13602 RetainedCmd::Draw(vertices) => render_pass.draw(vertices, 0..1),
13603 RetainedCmd::DrawIndexed(indices, instances) => {
13604 render_pass.draw_indexed(indices, 0, instances)
13605 }
13606 },
13607 );
13608 self.frame_stats.add_draw_calls(draws);
13609 }
13610
13611 #[cfg(not(target_arch = "wasm32"))]
13637 fn encode_retained_op<'r>(
13638 &'r self,
13639 slot: &'r ReplaySlot,
13640 first: u32,
13641 last: u32,
13642 retained_index: u32,
13643 sink: &mut impl FnMut(RetainedCmd<'r>),
13644 ) -> u32 {
13645 sink(RetainedCmd::Uniforms(&self.uniform_bind_group));
13646 sink(RetainedCmd::SlotBindings(
13647 &slot.bind_group,
13648 retained_index * REPLAY_TRANSFORM_STRIDE as u32,
13649 ));
13650 let Some(mesh) = slot.mesh.as_ref() else {
13651 self.encode_retained_instanced(slot, first..last, sink);
13652 return 1;
13653 };
13654 sink(RetainedCmd::MeshVertices(&mesh.vertex_buffer));
13655 let prefix = &mesh.index_prefix;
13656 let meshed_at = |shape: u32| prefix[shape as usize + 1] > prefix[shape as usize];
13657 let mut draws = 0;
13658 let mut cursor = first;
13659 while cursor < last {
13660 let run_meshed = meshed_at(cursor);
13661 let mut end = cursor + 1;
13662 while end < last && meshed_at(end) == run_meshed {
13663 end += 1;
13664 }
13665 if run_meshed {
13666 sink(RetainedCmd::Pipeline(RetainedPipelineKind::Mesh));
13667 sink(RetainedCmd::Index(
13668 &mesh.index_buffer,
13669 wgpu::IndexFormat::Uint32,
13670 ));
13671 sink(RetainedCmd::DrawIndexed(
13672 prefix[cursor as usize]..prefix[end as usize],
13673 0..1,
13674 ));
13675 } else {
13676 self.encode_retained_instanced(slot, cursor..end, sink);
13677 }
13678 draws += 1;
13679 cursor = end;
13680 }
13681 draws
13682 }
13683
13684 #[cfg(not(target_arch = "wasm32"))]
13690 fn encode_retained_instanced<'r>(
13691 &'r self,
13692 slot: &ReplaySlot,
13693 range: Range<u32>,
13694 sink: &mut impl FnMut(RetainedCmd<'r>),
13695 ) {
13696 match &self.instanced_quads {
13697 Some(instanced) => {
13698 if slot.has_gradient {
13699 sink(RetainedCmd::Pipeline(RetainedPipelineKind::Instanced));
13700 } else {
13701 sink(RetainedCmd::Pipeline(RetainedPipelineKind::InstancedSolid));
13702 }
13703 sink(RetainedCmd::Index(
13704 &instanced.index_buffer,
13705 wgpu::IndexFormat::Uint16,
13706 ));
13707 sink(RetainedCmd::DrawIndexed(0..6, range));
13708 }
13709 None => {
13710 if slot.has_gradient {
13711 sink(RetainedCmd::Pipeline(RetainedPipelineKind::Expanded));
13712 } else {
13713 sink(RetainedCmd::Pipeline(RetainedPipelineKind::ExpandedSolid));
13714 }
13715 sink(RetainedCmd::Draw(range.start * 6..range.end * 6));
13716 }
13717 }
13718 }
13719
13720 #[cfg(not(target_arch = "wasm32"))]
13728 fn retained_bundle_key(
13729 &self,
13730 ordered_items: &[(usize, SegmentDrawItem)],
13731 retained_draws: &[RetainedDraw],
13732 item_range: Range<usize>,
13733 ) -> RetainedBundleKey {
13734 let mut ops = Vec::with_capacity(item_range.len());
13735 for (_, item) in &ordered_items[item_range] {
13736 let SegmentDrawItem::Retained(index) = item else {
13737 continue;
13738 };
13739 let Some(retained) = retained_draws.get(*index) else {
13740 continue;
13741 };
13742 let slot = self.replay_slots.slots.get(&retained.slot);
13743 let (first, last) = match slot {
13744 Some(slot) => (
13745 retained.first_shape.min(slot.shape_count),
13746 retained
13747 .first_shape
13748 .saturating_add(retained.shape_count)
13749 .min(slot.shape_count),
13750 ),
13751 None => (
13752 retained.first_shape,
13753 retained.first_shape.saturating_add(retained.shape_count),
13754 ),
13755 };
13756 ops.push(RetainedBundleOpKey {
13757 slot: retained.slot,
13758 capture_epoch: slot.map(|slot| slot.capture_epoch),
13759 first,
13760 last,
13761 retained_index: *index as u32,
13762 has_mesh: slot.is_some_and(|slot| slot.mesh.is_some())
13763 && self.shape_batch_limits.storage,
13764 });
13765 }
13766 RetainedBundleKey {
13767 depth: self.pass_depth(),
13768 ops,
13769 }
13770 }
13771
13772 #[cfg(not(target_arch = "wasm32"))]
13779 fn build_retained_bundle(&self, key: &RetainedBundleKey) -> wgpu::RenderBundle {
13780 let mut encoder =
13781 self.device
13782 .create_render_bundle_encoder(&wgpu::RenderBundleEncoderDescriptor {
13783 label: Some("Retained Stretch Bundle"),
13784 color_formats: &[Some(self.composition_format)],
13785 depth_stencil: key.depth.then_some(wgpu::RenderBundleDepthStencil {
13790 format: display_clip::DISPLAY_CLIP_DEPTH_FORMAT,
13791 depth_read_only: true,
13792 stencil_read_only: true,
13793 }),
13794 sample_count: 1,
13795 multiview: None,
13796 });
13797 for op in &key.ops {
13798 if op.capture_epoch.is_none()
13799 || op.retained_index >= MAX_REPLAY_SLOTS
13800 || op.first >= op.last
13801 {
13802 continue;
13803 }
13804 let Some(slot) = self.replay_slots.slots.get(&op.slot) else {
13805 continue;
13806 };
13807 self.encode_retained_op(
13812 slot,
13813 op.first,
13814 op.last,
13815 op.retained_index,
13816 &mut |cmd| match cmd {
13817 RetainedCmd::Pipeline(pipeline) => {
13818 encoder.set_pipeline(self.retained_pipeline(pipeline))
13819 }
13820 RetainedCmd::Uniforms(group) => encoder.set_bind_group(0, group, &[]),
13821 RetainedCmd::SlotBindings(group, offset) => {
13822 encoder.set_bind_group(1, group, &[offset])
13823 }
13824 RetainedCmd::MeshVertices(buffer) => {
13825 encoder.set_vertex_buffer(0, buffer.slice(..))
13826 }
13827 RetainedCmd::Index(buffer, format) => {
13828 encoder.set_index_buffer(buffer.slice(..), format)
13829 }
13830 RetainedCmd::Draw(vertices) => encoder.draw(vertices, 0..1),
13831 RetainedCmd::DrawIndexed(indices, instances) => {
13832 encoder.draw_indexed(indices, 0, instances)
13833 }
13834 },
13835 );
13836 }
13837 encoder.finish(&wgpu::RenderBundleDescriptor {
13838 label: Some("Retained Stretch Bundle"),
13839 })
13840 }
13841
13842 #[cfg(not(target_arch = "wasm32"))]
13850 fn draw_retained_stretch_bundled(
13851 &mut self,
13852 render_pass: &mut wgpu::RenderPass<'_>,
13853 ordered_items: &[(usize, SegmentDrawItem)],
13854 retained_draws: &[RetainedDraw],
13855 item_range: Range<usize>,
13856 width: u32,
13857 height: u32,
13858 ) {
13859 let key = self.retained_bundle_key(ordered_items, retained_draws, item_range);
13860 if !self.retained_bundle_cache.hit(&key) {
13861 let bundle = self.build_retained_bundle(&key);
13862 self.retained_bundle_cache.insert(key.clone(), bundle);
13863 }
13864 for op in &key.ops {
13867 if op.capture_epoch.is_some()
13868 && op.retained_index < MAX_REPLAY_SLOTS
13869 && op.first < op.last
13870 {
13871 self.frame_stats.bump_shapes();
13872 self.frame_stats.add_draw_calls(1);
13873 if fill_area_diag_enabled() {
13874 let slot = self.replay_slots.slots.get(&op.slot);
13876 let retained = retained_draws.get(op.retained_index as usize);
13877 if let (Some(slot), Some(retained)) = (slot, retained) {
13878 self.fill_area_diag.add_retained_range(
13879 &slot.fill_diag_shapes,
13880 op.first,
13881 op.last,
13882 &retained.transform,
13883 );
13884 }
13885 }
13886 }
13887 }
13888 render_pass.set_scissor_rect(0, 0, width, height);
13893 if let Some(bundle) = self.retained_bundle_cache.get(&key) {
13894 render_pass.execute_bundles(std::iter::once(bundle));
13895 }
13896 }
13897
13898 #[cfg(not(target_arch = "wasm32"))]
13901 #[doc(hidden)]
13902 pub fn retained_bundle_stats(&self) -> (u64, u64) {
13903 self.retained_bundle_cache.stats()
13904 }
13905
13906 #[cfg(not(target_arch = "wasm32"))]
13909 #[doc(hidden)]
13910 pub fn rim_meshes_emitted(&self) -> u64 {
13911 self.rim_meshes_emitted
13912 }
13913
13914 #[doc(hidden)]
13918 pub fn device_error_count(&self) -> u64 {
13919 self.device_errors.error_count()
13920 }
13921
13922 #[cfg(not(target_arch = "wasm32"))]
13929 #[doc(hidden)]
13930 pub fn static_span_stats(&self) -> (u64, u64) {
13931 (self.static_span.hits, self.static_span.recaptures)
13932 }
13933
13934 #[cfg(not(target_arch = "wasm32"))]
13941 fn upload_transient_rim_meshes(&mut self) {
13942 let device = self.device.clone();
13943 let mut upload_stats = crate::frame_graph::FrameCommandStats::default();
13944 if self.rim_mesh_vertices.len() > self.rim_mesh_uploaded_vertices {
13945 let vertex_buffer = self.rim_mesh_vertex_buffer.get_or_insert_with(|| {
13946 device.create_buffer(&wgpu::BufferDescriptor {
13947 label: Some("Rim Mesh Vertex Buffer"),
13948 size: (RIM_MESH_VERTEX_CAPACITY * std::mem::size_of::<MeshVertex>()) as u64,
13949 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
13950 mapped_at_creation: false,
13951 })
13952 });
13953 upload_stats.upload_bytes += self
13954 .frame_graph_executor
13955 .upload_buffer(
13956 &self.queue,
13957 vertex_buffer,
13958 (self.rim_mesh_uploaded_vertices * std::mem::size_of::<MeshVertex>()) as u64,
13959 bytemuck::cast_slice(
13960 &self.rim_mesh_vertices[self.rim_mesh_uploaded_vertices..],
13961 ),
13962 )
13963 .upload_bytes;
13964 self.rim_mesh_uploaded_vertices = self.rim_mesh_vertices.len();
13965 }
13966 if self.rim_mesh_indices.len() > self.rim_mesh_uploaded_indices {
13967 let index_buffer = self.rim_mesh_index_buffer.get_or_insert_with(|| {
13968 device.create_buffer(&wgpu::BufferDescriptor {
13969 label: Some("Rim Mesh Index Buffer"),
13970 size: (RIM_MESH_INDEX_CAPACITY * std::mem::size_of::<u32>()) as u64,
13971 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
13972 mapped_at_creation: false,
13973 })
13974 });
13975 upload_stats.upload_bytes += self
13976 .frame_graph_executor
13977 .upload_buffer(
13978 &self.queue,
13979 index_buffer,
13980 (self.rim_mesh_uploaded_indices * std::mem::size_of::<u32>()) as u64,
13981 bytemuck::cast_slice(&self.rim_mesh_indices[self.rim_mesh_uploaded_indices..]),
13982 )
13983 .upload_bytes;
13984 self.rim_mesh_uploaded_indices = self.rim_mesh_indices.len();
13985 }
13986 if upload_stats.upload_bytes > 0 {
13987 self.frame_stats.record_command_stats(upload_stats);
13988 }
13989 }
13990
13991 fn draw_prepared_shapes(
13992 &self,
13993 render_pass: &mut wgpu::RenderPass<'_>,
13994 blend_mode: BlendMode,
13995 batch: PreparedShapeBatch,
13996 width: u32,
13997 height: u32,
13998 rims: &[RimDraw],
13999 ) {
14000 #[cfg(target_arch = "wasm32")]
14001 let _ = rims;
14002 self.frame_stats.bump_shapes();
14003 self.frame_stats.add_draw_calls(1);
14004 render_pass.set_scissor_rect(0, 0, width, height);
14005 #[cfg(not(target_arch = "wasm32"))]
14006 let (uniform_bind_group, shape_buffers) = (&self.uniform_bind_group, &self.shape_buffers);
14007 #[cfg(target_arch = "wasm32")]
14008 let (uniform_bind_group, shape_buffers) = (
14009 &self.wasm_uniform_batches[batch.uniform_slot].bind_group,
14010 &self.wasm_shape_batches[batch.shape_slot],
14011 );
14012 #[cfg(not(target_arch = "wasm32"))]
14017 if let Some(instanced) = &self.instanced_quads {
14018 assert!(
14019 batch.vertex_start.is_multiple_of(6) && batch.vertex_count.is_multiple_of(6),
14020 "shape batches are whole shapes: vertex range {}..+{} must be \
14021 six-aligned to convert to an instance range",
14022 batch.vertex_start,
14023 batch.vertex_count,
14024 );
14025 let set_instanced_pipeline = |render_pass: &mut wgpu::RenderPass<'_>| {
14028 if blend_mode == BlendMode::SrcOver && !batch.has_gradient {
14029 render_pass.set_pipeline(self.instanced_pipeline_solid(instanced));
14030 } else {
14031 render_pass.set_pipeline(self.instanced_pipeline(instanced, blend_mode));
14032 }
14033 };
14034 set_instanced_pipeline(render_pass);
14035 render_pass.set_bind_group(0, uniform_bind_group, &[]);
14036 render_pass.set_bind_group(1, &shape_buffers.bind_group, &[0]);
14039 let first_shape = batch.vertex_start / 6;
14040 let shape_count = batch.vertex_count / 6;
14041 render_pass
14042 .set_index_buffer(instanced.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
14043 debug_assert!(
14047 rims.windows(2)
14048 .all(|pair| pair[0].shape_index < pair[1].shape_index),
14049 "rim draws must arrive in ascending shape order"
14050 );
14051 let rim_start = rims.partition_point(|rim| rim.shape_index < first_shape);
14052 let rim_end = rims.partition_point(|rim| rim.shape_index < first_shape + shape_count);
14053 let batch_rims = &rims[rim_start..rim_end];
14054 let rim_buffers = match (&self.rim_mesh_vertex_buffer, &self.rim_mesh_index_buffer) {
14055 (Some(vertex_buffer), Some(index_buffer)) if !batch_rims.is_empty() => {
14056 Some((vertex_buffer, index_buffer))
14057 }
14058 _ => None,
14059 };
14060 let Some((rim_vertex_buffer, rim_index_buffer)) = rim_buffers else {
14061 render_pass.draw_indexed(0..6, 0, first_shape..first_shape + shape_count);
14062 return;
14063 };
14064 let mut draw_calls = 0u32;
14072 let mut cursor = first_shape;
14073 for rim in batch_rims {
14074 if cursor < rim.shape_index {
14075 render_pass.draw_indexed(0..6, 0, cursor..rim.shape_index);
14076 draw_calls += 1;
14077 }
14078 render_pass.set_pipeline(self.mesh_pipeline());
14079 render_pass.set_vertex_buffer(0, rim_vertex_buffer.slice(..));
14080 render_pass.set_index_buffer(rim_index_buffer.slice(..), wgpu::IndexFormat::Uint32);
14081 render_pass.draw_indexed(
14082 rim.first_index..rim.first_index + rim.index_count,
14083 0,
14084 0..1,
14085 );
14086 draw_calls += 1;
14087 set_instanced_pipeline(render_pass);
14088 render_pass
14089 .set_index_buffer(instanced.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
14090 cursor = rim.shape_index + 1;
14091 }
14092 if cursor < first_shape + shape_count {
14093 render_pass.draw_indexed(0..6, 0, cursor..first_shape + shape_count);
14094 draw_calls += 1;
14095 }
14096 self.frame_stats
14098 .add_draw_calls(draw_calls.saturating_sub(1));
14099 return;
14100 }
14101 if blend_mode == BlendMode::SrcOver && !batch.has_gradient {
14102 render_pass.set_pipeline(self.shape_pipeline_solid());
14103 } else {
14104 render_pass.set_pipeline(self.shape_pipeline(blend_mode));
14105 }
14106 render_pass.set_bind_group(0, uniform_bind_group, &[]);
14107 render_pass.set_bind_group(1, &shape_buffers.bind_group, &[0]);
14110 render_pass.draw(
14113 batch.vertex_start..batch.vertex_start + batch.vertex_count,
14114 0..1,
14115 );
14116 }
14117
14118 #[allow(clippy::too_many_arguments)]
14121 fn encode_shapes_pass<'a, I, C: FrameCommandRecorder>(
14122 &mut self,
14123 frame_encoder: &mut C,
14124 target_view: &wgpu::TextureView,
14125 layer_shapes: I,
14126 brushes: &[Brush],
14127 blend_mode: BlendMode,
14128 width: u32,
14129 height: u32,
14130 root_scale: f32,
14131 load_op: wgpu::LoadOp<wgpu::Color>,
14132 viewport_offset: [f32; 2],
14133 ) where
14134 I: Iterator<Item = &'a DrawShape>,
14135 {
14136 let mut staged_uploads = self.take_staged_uploads();
14137 let viewport = ViewportUniformParams {
14138 width,
14139 height,
14140 offset: viewport_offset,
14141 };
14142 let viewport_rect_logical = viewport_rect_in_logical(viewport, root_scale);
14143 let Some(batch) = self.prepare_shapes_batch(
14144 layer_shapes.filter(|shape| match viewport_rect_logical {
14145 Some(rect) => shape_draw_is_visible_in_rect(shape, rect, root_scale),
14146 None => false,
14147 }),
14148 brushes,
14149 root_scale,
14150 viewport,
14151 &mut staged_uploads,
14152 ) else {
14153 self.restore_staged_uploads(staged_uploads);
14154 return;
14155 };
14156 let upload_offset =
14157 frame_encoder.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
14158 self.flush_staged_uploads_at(frame_encoder.encoder(), &staged_uploads, upload_offset);
14159 self.restore_staged_uploads(staged_uploads);
14160 let mut render_pass =
14161 frame_encoder
14162 .encoder()
14163 .begin_render_pass(&wgpu::RenderPassDescriptor {
14164 label: Some("Shape Pass"),
14165 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
14166 view: target_view,
14167 resolve_target: None,
14168 depth_slice: None,
14169 ops: wgpu::Operations {
14170 load: load_op,
14171 store: wgpu::StoreOp::Store,
14172 },
14173 })],
14174 depth_stencil_attachment: None,
14175 timestamp_writes: None,
14176 occlusion_query_set: None,
14177 multiview_mask: None,
14178 });
14179 self.draw_prepared_shapes(&mut render_pass, blend_mode, batch, width, height, &[]);
14180 }
14181
14182 fn draw_prepared_images(
14183 &mut self,
14184 render_pass: &mut wgpu::RenderPass<'_>,
14185 batch: &PreparedImageBatch,
14186 blend_mode: BlendMode,
14187 ) -> Result<(), String> {
14188 if batch.cmds.is_empty() {
14189 return Ok(());
14190 }
14191 self.frame_stats.bump_images();
14192 self.frame_stats.add_draw_calls(batch.cmds.len() as u32);
14193 render_pass.set_pipeline(self.image_pipeline(blend_mode));
14194 #[cfg(not(target_arch = "wasm32"))]
14195 let (uniform_bind_group, vertex_buffer, index_buffer) = (
14196 &self.uniform_bind_group,
14197 &self.image_vertex_buffer,
14198 &self.image_index_buffer,
14199 );
14200 #[cfg(target_arch = "wasm32")]
14201 let (uniform_bind_group, vertex_buffer, index_buffer) = (
14202 &self.wasm_uniform_batches[batch.uniform_slot].bind_group,
14203 &self.wasm_image_batches[batch.image_slot].vertex_buffer,
14204 &self.wasm_image_batches[batch.image_slot].index_buffer,
14205 );
14206 render_pass.set_bind_group(0, uniform_bind_group, &[]);
14207 render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
14208 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
14209
14210 for cmd in &batch.cmds {
14211 let (sx, sy, sw, sh) = cmd.scissor;
14212 render_pass.set_scissor_rect(sx, sy, sw, sh);
14213
14214 let cached = self
14215 .image_texture_cache
14216 .get(&cmd.image_id)
14217 .ok_or_else(|| "image texture missing from cache".to_string())?;
14218 render_pass.set_bind_group(1, cached.bind_group(cmd.sampling), &[]);
14219 render_pass.draw_indexed(cmd.index_start..(cmd.index_start + 6), 0, 0..1);
14220 }
14221 Ok(())
14222 }
14223
14224 fn draw_prepared_glyphs(
14225 &mut self,
14226 render_pass: &mut wgpu::RenderPass<'_>,
14227 batch: &PreparedGlyphBatch,
14228 ) -> Result<(), String> {
14229 if batch.cmds.is_empty() {
14230 return Ok(());
14231 }
14232 #[cfg(not(target_arch = "wasm32"))]
14233 {
14234 self.draw_native_prepared_glyph_cmd_range(
14235 render_pass,
14236 &batch.cmds,
14237 0..batch.cmds.len(),
14238 )?;
14239 }
14240 #[cfg(target_arch = "wasm32")]
14241 {
14242 self.frame_stats.bump_text();
14243 self.frame_stats.add_draw_calls(batch.cmds.len() as u32);
14244 render_pass.set_pipeline(self.glyph_atlas_pipeline());
14245 let (uniform_bind_group, vertex_buffer, index_buffer) = (
14246 &self.wasm_uniform_batches[batch.uniform_slot].bind_group,
14247 &self.wasm_image_batches[batch.image_slot].vertex_buffer,
14248 &self.wasm_image_batches[batch.image_slot].index_buffer,
14249 );
14250 render_pass.set_bind_group(0, uniform_bind_group, &[]);
14251 render_pass.set_bind_group(1, &self.text_glyph_atlas.bind_group, &[]);
14252 render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
14253 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
14254
14255 for cmd in &batch.cmds {
14256 let (sx, sy, sw, sh) = cmd.scissor;
14257 render_pass.set_scissor_rect(sx, sy, sw, sh);
14258 let GlyphDrawSource::Shared {
14259 index_start,
14260 index_count,
14261 } = cmd.source;
14262 render_pass.draw_indexed(index_start..(index_start + index_count), 0, 0..1);
14263 }
14264 }
14265 Ok(())
14266 }
14267
14268 #[cfg(not(target_arch = "wasm32"))]
14269 fn draw_native_prepared_image_cmd_range(
14270 &mut self,
14271 render_pass: &mut wgpu::RenderPass<'_>,
14272 cmds: &[ImageDrawCmd],
14273 cmd_range: Range<usize>,
14274 blend_mode: BlendMode,
14275 ) -> Result<(), String> {
14276 let Some(cmds) = cmds.get(cmd_range) else {
14277 return Err("image command range is outside the prepared command buffer".to_string());
14278 };
14279 if cmds.is_empty() {
14280 return Ok(());
14281 }
14282
14283 self.frame_stats.bump_images();
14284 self.frame_stats.add_draw_calls(cmds.len() as u32);
14285 render_pass.set_pipeline(self.image_pipeline(blend_mode));
14286 render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
14287 render_pass.set_index_buffer(self.image_index_buffer.slice(..), wgpu::IndexFormat::Uint32);
14288 render_pass.set_vertex_buffer(0, self.image_vertex_buffer.slice(..));
14289
14290 for cmd in cmds {
14291 let (sx, sy, sw, sh) = cmd.scissor;
14292 render_pass.set_scissor_rect(sx, sy, sw, sh);
14293
14294 let cached = self
14295 .image_texture_cache
14296 .get(&cmd.image_id)
14297 .ok_or_else(|| "image texture missing from cache".to_string())?;
14298 render_pass.set_bind_group(1, cached.bind_group(cmd.sampling), &[]);
14299 render_pass.draw_indexed(cmd.index_start..(cmd.index_start + 6), 0, 0..1);
14300 }
14301 Ok(())
14302 }
14303
14304 #[cfg(not(target_arch = "wasm32"))]
14305 fn draw_native_prepared_glyph_cmd_range(
14306 &mut self,
14307 render_pass: &mut wgpu::RenderPass<'_>,
14308 cmds: &[GlyphDrawCmd],
14309 cmd_range: Range<usize>,
14310 ) -> Result<(), String> {
14311 let Some(cmds) = cmds.get(cmd_range) else {
14312 return Err("glyph command range is outside the prepared command buffer".to_string());
14313 };
14314 if cmds.is_empty() {
14315 return Ok(());
14316 }
14317
14318 self.frame_stats.bump_text();
14319 self.frame_stats.add_draw_calls(cmds.len() as u32);
14320
14321 let mut shared_buffers_bound = false;
14322 let mut retained_pipeline_bound = false;
14323 for cmd in cmds {
14324 let (sx, sy, sw, sh) = cmd.scissor;
14325 render_pass.set_scissor_rect(sx, sy, sw, sh);
14326 match cmd.source {
14327 GlyphDrawSource::Shared {
14328 index_start,
14329 index_count,
14330 } => {
14331 if retained_pipeline_bound || !shared_buffers_bound {
14332 render_pass.set_pipeline(self.glyph_atlas_pipeline());
14333 render_pass.set_bind_group(1, &self.text_glyph_atlas.bind_group, &[]);
14334 retained_pipeline_bound = false;
14335 }
14336 if !shared_buffers_bound {
14337 render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
14338 render_pass.set_index_buffer(
14339 self.image_index_buffer.slice(..),
14340 wgpu::IndexFormat::Uint32,
14341 );
14342 render_pass.set_vertex_buffer(0, self.image_vertex_buffer.slice(..));
14343 shared_buffers_bound = true;
14344 }
14345 render_pass.draw_indexed(index_start..(index_start + index_count), 0, 0..1);
14346 }
14347 GlyphDrawSource::Retained {
14348 cache_key,
14349 uniform_slot,
14350 } => {
14351 shared_buffers_bound = false;
14352 if !retained_pipeline_bound {
14353 render_pass.set_pipeline(self.retained_glyph_atlas_pipeline());
14354 render_pass.set_bind_group(1, &self.text_glyph_atlas.bind_group, &[]);
14355 retained_pipeline_bound = true;
14356 }
14357 let cached = self
14358 .text_glyph_gpu_run_cache
14359 .peek(&cache_key)
14360 .ok_or_else(|| "retained glyph buffer missing from cache".to_string())?;
14361 let dynamic_offset =
14362 self.retained_glyph_uniform_dynamic_offset(uniform_slot)?;
14363 render_pass.set_bind_group(
14364 0,
14365 &self.retained_glyph_uniform_bind_group,
14366 &[dynamic_offset],
14367 );
14368 render_pass
14369 .set_index_buffer(cached.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
14370 render_pass.set_vertex_buffer(0, cached.vertex_buffer.slice(..));
14371 render_pass.draw_indexed(0..cached.index_count, 0, 0..1);
14372 }
14373 }
14374 }
14375 Ok(())
14376 }
14377
14378 fn append_image_draw_cmd(
14379 &mut self,
14380 image_draw: &ImageDraw,
14381 viewport: ViewportUniformParams,
14382 root_scale: f32,
14383 image_vertices: &mut Vec<Vertex>,
14384 image_indices: &mut Vec<u32>,
14385 image_cmds: &mut Vec<ImageDrawCmd>,
14386 ) -> Result<(), String> {
14387 let snap_delta = image_draw
14388 .snap_anchor
14389 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
14390 .unwrap_or_default();
14391 let rect = image_draw.rect.translate(snap_delta.x, snap_delta.y);
14392 if rect.width <= 0.0 || rect.height <= 0.0 || image_draw.alpha <= 0.0 {
14393 return Ok(());
14394 }
14395
14396 let (tint, cpu_filter) = tint_for_image(image_draw.color_filter, image_draw.alpha);
14397 if tint[3] <= 0.0 {
14398 return Ok(());
14399 }
14400
14401 let prepared_image = if let Some(filter) = cpu_filter {
14402 apply_filter_to_bitmap(&image_draw.image, filter)?
14403 } else {
14404 image_draw.image.clone()
14405 };
14406 self.ensure_image_cached(&prepared_image)?;
14407
14408 let mut adjusted_image = ImageDraw {
14409 rect,
14410 local_rect: image_draw.local_rect.translate(snap_delta.x, snap_delta.y),
14411 quad: translate_quad(image_draw.quad, snap_delta),
14412 snap_anchor: image_draw.snap_anchor,
14413 image: image_draw.image.clone(),
14414 alpha: image_draw.alpha,
14415 color_filter: image_draw.color_filter,
14416 sampling: image_draw.sampling,
14417 z_index: image_draw.z_index,
14418 clip: image_draw.clip,
14419 blend_mode: image_draw.blend_mode,
14420 src_rect: image_draw.src_rect,
14421 motion_context_animated: image_draw.motion_context_animated,
14422 };
14423 snap_nearest_image_to_device_pixels(&mut adjusted_image, root_scale);
14424 let Some(scissor) =
14425 scissor_rect_for_image(&adjusted_image, root_scale, viewport.width, viewport.height)
14426 else {
14427 return Ok(());
14428 };
14429
14430 let Some(uv_rect) = image_uv_rect(&image_draw.image, image_draw.src_rect) else {
14431 return Ok(());
14432 };
14433 let device_quad =
14434 nearest_image_device_quad(&adjusted_image, root_scale).unwrap_or_else(|| {
14435 if adjusted_image.snap_anchor.is_some() {
14436 canonicalized_scaled_quad(adjusted_image.quad, root_scale)
14437 } else {
14438 scaled_quad(adjusted_image.quad, root_scale)
14439 }
14440 });
14441 #[cfg(not(target_arch = "wasm32"))]
14442 {
14443 if fill_area_diag_enabled() {
14444 self.fill_area_diag.add_image_quad(&device_quad);
14445 }
14446 }
14447
14448 let base_vertex = image_vertices.len() as u32;
14449 let index_start = image_indices.len() as u32;
14450 image_indices.extend_from_slice(&[
14451 base_vertex,
14452 base_vertex + 1,
14453 base_vertex + 2,
14454 base_vertex + 2,
14455 base_vertex + 1,
14456 base_vertex + 3,
14457 ]);
14458 image_vertices.extend_from_slice(&[
14459 Vertex {
14460 position: device_quad[0],
14461 color: tint,
14462 uv: [uv_rect.min[0], uv_rect.min[1]],
14463 uv_bounds: uv_rect.sample_bounds,
14464 },
14465 Vertex {
14466 position: device_quad[1],
14467 color: tint,
14468 uv: [uv_rect.max[0], uv_rect.min[1]],
14469 uv_bounds: uv_rect.sample_bounds,
14470 },
14471 Vertex {
14472 position: device_quad[2],
14473 color: tint,
14474 uv: [uv_rect.min[0], uv_rect.max[1]],
14475 uv_bounds: uv_rect.sample_bounds,
14476 },
14477 Vertex {
14478 position: device_quad[3],
14479 color: tint,
14480 uv: [uv_rect.max[0], uv_rect.max[1]],
14481 uv_bounds: uv_rect.sample_bounds,
14482 },
14483 ]);
14484
14485 image_cmds.push(ImageDrawCmd {
14486 index_start,
14487 scissor,
14488 image_id: prepared_image.id(),
14489 sampling: image_draw.sampling,
14490 });
14491 Ok(())
14492 }
14493
14494 #[cfg(not(target_arch = "wasm32"))]
14495 fn stage_native_image_buffers(
14496 &mut self,
14497 staged_uploads: &mut StagedBufferUploads,
14498 viewport: ViewportUniformParams,
14499 image_vertices: &[Vertex],
14500 image_indices: &[u32],
14501 ) {
14502 if image_indices.is_empty() {
14503 return;
14504 }
14505
14506 self.stage_viewport_uniforms(staged_uploads, viewport);
14507 let needed_bytes = std::mem::size_of_val(image_vertices) as u64;
14512 if needed_bytes > self.image_vertex_buffer.size() {
14513 self.image_vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
14514 label: Some("Image Vertex Buffer"),
14515 size: needed_bytes.next_power_of_two(),
14516 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
14517 mapped_at_creation: false,
14518 });
14519 }
14520 let needed_index_bytes = std::mem::size_of_val(image_indices) as u64;
14521 if needed_index_bytes > self.image_index_buffer.size() {
14522 self.image_index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
14523 label: Some("Image Index Buffer"),
14524 size: needed_index_bytes.next_power_of_two(),
14525 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
14526 mapped_at_creation: false,
14527 });
14528 }
14529
14530 staged_uploads.stage(
14531 UploadTarget::ImageVertex,
14532 bytemuck::cast_slice(image_vertices),
14533 );
14534 staged_uploads.stage(
14535 UploadTarget::ImageIndex,
14536 bytemuck::cast_slice(image_indices),
14537 );
14538 }
14539
14540 fn prepare_image_draw_cmds<'a, I>(
14543 &mut self,
14544 layer_images: I,
14545 viewport: ViewportUniformParams,
14546 root_scale: f32,
14547 staged_uploads: &mut StagedBufferUploads,
14548 ) -> Result<PreparedImageBatch, String>
14549 where
14550 I: Iterator<Item = &'a ImageDraw>,
14551 {
14552 #[cfg(target_arch = "wasm32")]
14553 let _ = staged_uploads;
14554
14555 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
14556 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
14557 let mut image_cmds = std::mem::take(&mut self.scratch_image_cmds);
14558 image_vertices.clear();
14559 image_indices.clear();
14560 image_cmds.clear();
14561
14562 for image_draw in layer_images {
14563 self.append_image_draw_cmd(
14564 image_draw,
14565 viewport,
14566 root_scale,
14567 &mut image_vertices,
14568 &mut image_indices,
14569 &mut image_cmds,
14570 )?;
14571 }
14572
14573 #[cfg(not(target_arch = "wasm32"))]
14574 if !image_cmds.is_empty() {
14575 self.stage_native_image_buffers(
14576 staged_uploads,
14577 viewport,
14578 &image_vertices,
14579 &image_indices,
14580 );
14581 }
14582
14583 #[cfg(target_arch = "wasm32")]
14584 let image_slot = if image_cmds.is_empty() {
14585 0
14586 } else {
14587 let slot = self.claim_wasm_image_batch();
14588 {
14589 let buffers = &mut self.wasm_image_batches[slot];
14590 buffers.ensure_capacity(&self.device, image_vertices.len(), image_indices.len());
14591 }
14592 let buffers = &self.wasm_image_batches[slot];
14593 self.write_wasm_buffer(
14594 &buffers.vertex_buffer,
14595 bytemuck::cast_slice(&image_vertices),
14596 );
14597 self.write_wasm_buffer(&buffers.index_buffer, bytemuck::cast_slice(&image_indices));
14598 slot
14599 };
14600
14601 #[cfg(target_arch = "wasm32")]
14602 let uniform_slot = if image_cmds.is_empty() {
14603 0
14604 } else {
14605 self.prepare_wasm_viewport_uniforms(viewport)
14606 };
14607
14608 self.scratch_image_vertices = image_vertices;
14609 self.scratch_image_indices = image_indices;
14610 Ok(PreparedImageBatch {
14611 cmds: image_cmds,
14612 #[cfg(target_arch = "wasm32")]
14613 image_slot,
14614 #[cfg(target_arch = "wasm32")]
14615 uniform_slot,
14616 })
14617 }
14618
14619 fn glyph_atlas_entry_for(
14620 &mut self,
14621 glyph: &SoftwareGlyphAtlasGlyph,
14622 ) -> Result<GlyphAtlasEntry, String> {
14623 if let Some(entry) = self.text_glyph_atlas.upload_glyph(
14624 glyph.key,
14625 glyph,
14626 &self.queue,
14627 &mut self.frame_graph_executor,
14628 &mut self.frame_stats,
14629 ) {
14630 return Ok(entry);
14631 }
14632
14633 self.text_glyph_atlas.reset(
14634 &self.device,
14635 &self.image_bind_group_layout,
14636 &self.image_nearest_sampler,
14637 );
14638 Err("text glyph atlas filled and was reset".to_string())
14639 }
14640
14641 fn glyph_atlas_entry_for_cached(
14642 &mut self,
14643 glyph: &SoftwareGlyphAtlasPlacement,
14644 ) -> Option<GlyphAtlasEntry> {
14645 let entry = self.text_glyph_atlas.entry(&glyph.key)?;
14646 self.frame_stats.record_text_glyph_atlas_hit();
14647 Some(entry)
14648 }
14649
14650 fn glyph_atlas_entry_for_placement(
14651 &mut self,
14652 glyph: &SoftwareGlyphAtlasPlacement,
14653 ) -> Result<GlyphAtlasEntry, String> {
14654 if let Some(entry) = self.glyph_atlas_entry_for_cached(glyph) {
14655 return Ok(entry);
14656 }
14657
14658 let Some(upload_glyph) = self.text_glyph_mask_cache.atlas_glyph_for_placement(glyph) else {
14659 return Err("text glyph placement has no retained raster mask".to_string());
14660 };
14661 self.glyph_atlas_entry_for(&upload_glyph)
14662 }
14663
14664 fn prepare_text_glyph_quads(
14665 &mut self,
14666 run_key: TextGlyphRunCacheKey,
14667 atlas_generation: u64,
14668 cached_glyph_run: Option<&[SoftwareGlyphAtlasPlacement]>,
14669 collected_run: &[SoftwareGlyphAtlasRunGlyph],
14670 generated_quads: &mut Vec<CachedTextGlyphQuad>,
14671 ) -> Result<Rc<[CachedTextGlyphQuad]>, String> {
14672 generated_quads.clear();
14673 if let Some(glyph_run) = cached_glyph_run {
14674 for glyph in glyph_run {
14675 if glyph.width == 0 || glyph.height == 0 || glyph.color.3 <= 0.0 {
14676 continue;
14677 }
14678 let entry = self.glyph_atlas_entry_for_placement(glyph)?;
14679 generated_quads.push(cached_text_glyph_quad(
14684 glyph,
14685 entry,
14686 self.text_glyph_atlas.size(),
14687 ));
14688 }
14689 } else {
14690 for run_glyph in collected_run {
14691 let placement = run_glyph.placement();
14692 if placement.width == 0 || placement.height == 0 || placement.color.3 <= 0.0 {
14693 continue;
14694 }
14695 let entry = match run_glyph {
14696 SoftwareGlyphAtlasRunGlyph::Cached(placement) => {
14697 self.glyph_atlas_entry_for_placement(placement)?
14698 }
14699 SoftwareGlyphAtlasRunGlyph::New(glyph) => self.glyph_atlas_entry_for(glyph)?,
14700 };
14701 generated_quads.push(cached_text_glyph_quad(
14702 &placement,
14703 entry,
14704 self.text_glyph_atlas.size(),
14705 ));
14706 }
14707 }
14708
14709 let quads: Rc<[CachedTextGlyphQuad]> = Rc::from(generated_quads.clone().into_boxed_slice());
14710 if let Some(cached) = self.text_glyph_run_cache.get_mut(&run_key) {
14711 cached.quads = Some(Rc::clone(&quads));
14712 cached.atlas_generation = atlas_generation;
14713 }
14714 Ok(quads)
14715 }
14716
14717 #[allow(clippy::too_many_arguments)]
14718 fn append_text_glyph_quad_run(
14719 &mut self,
14720 source_raster_rect: Rect,
14721 quads: &[CachedTextGlyphQuad],
14722 clip: Option<Rect>,
14723 viewport: ViewportUniformParams,
14724 root_scale: f32,
14725 image_vertices: &mut Vec<Vertex>,
14726 image_indices: &mut Vec<u32>,
14727 record_cached_hits: bool,
14728 ) -> usize {
14729 let mut appended = 0usize;
14730 for quad in quads {
14731 if !cached_text_glyph_quad_is_visible_in_viewport(
14732 source_raster_rect,
14733 quad,
14734 clip,
14735 viewport,
14736 root_scale,
14737 ) {
14738 continue;
14739 }
14740 if append_cached_text_glyph_quad(
14741 source_raster_rect,
14742 quad,
14743 image_vertices,
14744 image_indices,
14745 ) {
14746 if record_cached_hits {
14747 self.frame_stats.record_text_glyph_atlas_hit();
14748 }
14749 #[cfg(not(target_arch = "wasm32"))]
14750 {
14751 if fill_area_diag_enabled() {
14752 self.fill_area_diag.add_glyph_quad(quad);
14753 }
14754 }
14755 appended = appended.saturating_add(1);
14756 }
14757 }
14758 appended
14759 }
14760
14761 #[cfg(not(target_arch = "wasm32"))]
14762 fn retained_glyph_viewport(
14763 viewport: ViewportUniformParams,
14764 source_raster_rect: Rect,
14765 ) -> ViewportUniformParams {
14766 ViewportUniformParams {
14767 width: viewport.width,
14768 height: viewport.height,
14769 offset: [
14770 viewport.offset[0] - source_raster_rect.x,
14771 viewport.offset[1] - source_raster_rect.y,
14772 ],
14773 }
14774 }
14775
14776 #[cfg(not(target_arch = "wasm32"))]
14777 fn retained_text_glyph_run_ready(&mut self, cache_key: TextGlyphRunCacheKey) -> bool {
14778 let atlas_generation = self.text_glyph_atlas.generation();
14779 self.text_glyph_gpu_run_cache
14780 .peek(&cache_key)
14781 .is_some_and(|cached| cached.atlas_generation == atlas_generation)
14782 }
14783
14784 #[cfg(not(target_arch = "wasm32"))]
14785 #[allow(clippy::too_many_arguments)]
14786 fn emit_retained_text_glyph_run_if_ready(
14787 &mut self,
14788 cache_key: TextGlyphRunCacheKey,
14789 quads: &[CachedTextGlyphQuad],
14790 clip: Option<Rect>,
14791 viewport: ViewportUniformParams,
14792 source_raster_rect: Rect,
14793 scissor: (u32, u32, u32, u32),
14794 staged_uploads: &mut StagedBufferUploads,
14795 glyph_cmds: &mut Vec<GlyphDrawCmd>,
14796 ) -> bool {
14797 if !should_use_retained_text_glyph_run(quads.len(), clip) {
14798 return false;
14799 }
14800 if !self.retained_text_glyph_run_ready(cache_key)
14801 && !self.ensure_retained_text_glyph_run(cache_key, quads)
14802 {
14803 return false;
14804 }
14805
14806 let uniform_slot = self.stage_retained_glyph_viewport_uniforms(
14807 staged_uploads,
14808 Self::retained_glyph_viewport(viewport, source_raster_rect),
14809 );
14810 if fill_area_diag_enabled() {
14811 for quad in quads {
14814 self.fill_area_diag.add_glyph_quad(quad);
14815 }
14816 }
14817 glyph_cmds.push(GlyphDrawCmd::retained(cache_key, uniform_slot, scissor));
14818 true
14819 }
14820
14821 #[cfg(not(target_arch = "wasm32"))]
14822 fn ensure_retained_text_glyph_run(
14823 &mut self,
14824 cache_key: TextGlyphRunCacheKey,
14825 quads: &[CachedTextGlyphQuad],
14826 ) -> bool {
14827 let atlas_generation = self.text_glyph_atlas.generation();
14828 if self
14829 .text_glyph_gpu_run_cache
14830 .peek(&cache_key)
14831 .is_some_and(|cached| cached.atlas_generation == atlas_generation)
14832 {
14833 return true;
14834 }
14835
14836 let mut vertices = Vec::with_capacity(quads.len().saturating_mul(4));
14837 let mut indices = Vec::with_capacity(quads.len().saturating_mul(6));
14838 let origin = Rect {
14839 x: 0.0,
14840 y: 0.0,
14841 width: 0.0,
14842 height: 0.0,
14843 };
14844 for quad in quads {
14845 append_cached_text_glyph_quad(origin, quad, &mut vertices, &mut indices);
14846 }
14847 if indices.is_empty() {
14848 return false;
14849 }
14850
14851 let vertex_bytes = bytemuck::cast_slice(&vertices);
14852 let index_bytes = bytemuck::cast_slice(&indices);
14853 let vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
14854 label: Some("Retained Text Glyph Vertex Buffer"),
14855 size: vertex_bytes.len() as u64,
14856 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
14857 mapped_at_creation: false,
14858 });
14859 let index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
14860 label: Some("Retained Text Glyph Index Buffer"),
14861 size: index_bytes.len() as u64,
14862 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
14863 mapped_at_creation: false,
14864 });
14865 let vertex_upload =
14866 self.frame_graph_executor
14867 .upload_buffer(&self.queue, &vertex_buffer, 0, vertex_bytes);
14868 self.frame_stats.record_command_stats(vertex_upload);
14869 let index_upload =
14870 self.frame_graph_executor
14871 .upload_buffer(&self.queue, &index_buffer, 0, index_bytes);
14872 self.frame_stats.record_command_stats(index_upload);
14873
14874 self.text_glyph_gpu_run_cache.put(
14875 cache_key,
14876 CachedGpuTextGlyphRun {
14877 vertex_buffer,
14878 index_buffer,
14879 index_count: indices.len() as u32,
14880 atlas_generation,
14881 },
14882 );
14883 true
14884 }
14885
14886 #[allow(clippy::too_many_arguments)]
14887 fn append_text_glyph_draws<'a, I>(
14888 &mut self,
14889 layer_texts: I,
14890 viewport: ViewportUniformParams,
14891 root_scale: f32,
14892 allow_offscreen_prewarm: bool,
14893 staged_uploads: &mut StagedBufferUploads,
14894 image_vertices: &mut Vec<Vertex>,
14895 image_indices: &mut Vec<u32>,
14896 glyph_cmds: &mut Vec<GlyphDrawCmd>,
14897 ) -> Result<bool, String>
14898 where
14899 I: IntoIterator<Item = &'a TextDraw>,
14900 {
14901 let append_start = Instant::now();
14902 let initial_vertex_len = image_vertices.len();
14903 let initial_index_len = image_indices.len();
14904 let initial_cmd_len = glyph_cmds.len();
14905 let initial_staged_bytes_len = staged_uploads.bytes.len();
14906 let initial_staged_copies_len = staged_uploads.copies.len();
14907 let mut collected_run = std::mem::take(&mut self.scratch_text_glyph_run);
14908 let mut collected_placements = std::mem::take(&mut self.scratch_text_glyph_placements);
14909 let mut generated_quads = std::mem::take(&mut self.scratch_text_glyph_quads);
14910 generated_quads.clear();
14911 let mut visited = 0usize;
14912 let mut emitted_glyphs = 0usize;
14913 let mut prewarmed_glyphs = 0usize;
14914 let mut run_hits = 0usize;
14915 let mut run_misses = 0usize;
14916
14917 for text_draw in layer_texts {
14918 visited = visited.saturating_add(1);
14919 let Some((logical_rect, raster_rect, clip, text_scale, static_text_motion)) =
14920 self.text_raster_geometry(text_draw, root_scale)
14921 else {
14922 continue;
14923 };
14924 if !static_text_motion {
14925 image_vertices.truncate(initial_vertex_len);
14926 image_indices.truncate(initial_index_len);
14927 glyph_cmds.truncate(initial_cmd_len);
14928 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
14929 self.scratch_text_glyph_run = collected_run;
14930 self.scratch_text_glyph_placements = collected_placements;
14931 self.scratch_text_glyph_quads = generated_quads;
14932 return Ok(false);
14933 }
14934 let is_visible =
14935 text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale);
14936 let draw_action = text_glyph_draw_action(
14937 is_visible,
14938 text_draw_should_prewarm_in_viewport(logical_rect, clip, viewport, root_scale),
14939 allow_offscreen_prewarm,
14940 );
14941 if draw_action == TextGlyphDrawAction::Skip {
14942 continue;
14943 }
14944
14945 let raster_source = text_glyph_raster_source(text_draw, raster_rect);
14946 let source_draw = raster_source.draw.as_ref();
14947 let source_raster_rect = raster_source.raster_rect;
14948
14949 let run_key = Self::text_glyph_run_cache_key(
14950 source_draw,
14951 source_raster_rect,
14952 text_scale,
14953 static_text_motion,
14954 );
14955 let atlas_generation = self.text_glyph_atlas.generation();
14956 let mut cached_quad_run = None;
14957 let mut miss_collect_ms = None;
14958 let mut miss_cached_glyphs = 0usize;
14959 let mut miss_new_glyphs = 0usize;
14960 let cached_glyph_run = if let Some(cached) = self.text_glyph_run_cache.get(&run_key) {
14961 run_hits = run_hits.saturating_add(1);
14962 if cached.atlas_generation == atlas_generation {
14963 cached_quad_run = cached.quads.as_ref().map(Rc::clone);
14964 }
14965 Some(Rc::clone(&cached.glyphs))
14966 } else {
14967 run_misses = run_misses.saturating_add(1);
14968 collected_run.clear();
14969 let collect_start = Instant::now();
14970 let collect_result = collect_solid_text_atlas_run(
14971 source_draw.text.as_ref(),
14972 source_raster_rect,
14973 &source_draw.text_style,
14974 source_draw.color,
14975 source_draw.font_size,
14976 text_scale,
14977 &self.text_fonts,
14978 &mut self.text_glyph_mask_cache,
14979 &mut collected_run,
14980 );
14981 miss_collect_ms = Some(instant_ms(collect_start, Instant::now()));
14982 if collect_result.is_none() {
14983 if text_atlas_fallback_diag_enabled() {
14984 let preview: String = source_draw.text.text.chars().take(96).collect();
14985 log::warn!(
14986 "[text-atlas-fallback] node={:?} visible={} prewarm={} spans={} links={} text_len={} preview={:?} span_style={:?} paragraph_style={:?}",
14987 source_draw.node_id,
14988 is_visible,
14989 draw_action == TextGlyphDrawAction::PrewarmOffscreen,
14990 source_draw.text.span_styles.len(),
14991 source_draw.text.links.len(),
14992 source_draw.text.text.len(),
14993 preview,
14994 source_draw.text_style.span_style,
14995 source_draw.text_style.paragraph_style,
14996 );
14997 }
14998 if draw_action == TextGlyphDrawAction::PrewarmOffscreen {
14999 continue;
15000 }
15001 image_vertices.truncate(initial_vertex_len);
15002 image_indices.truncate(initial_index_len);
15003 glyph_cmds.truncate(initial_cmd_len);
15004 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
15005 self.scratch_text_glyph_run = collected_run;
15006 self.scratch_text_glyph_placements = collected_placements;
15007 self.scratch_text_glyph_quads = generated_quads;
15008 return Ok(false);
15009 }
15010 if text_glyph_run_diag_enabled() {
15011 miss_cached_glyphs = collected_run
15012 .iter()
15013 .filter(|glyph| matches!(glyph, SoftwareGlyphAtlasRunGlyph::Cached(_)))
15014 .count();
15015 miss_new_glyphs = collected_run.len().saturating_sub(miss_cached_glyphs);
15016 }
15017 collected_placements.clear();
15018 collected_placements.extend(
15019 collected_run
15020 .iter()
15021 .map(SoftwareGlyphAtlasRunGlyph::placement),
15022 );
15023 let glyphs: Rc<[SoftwareGlyphAtlasPlacement]> =
15024 Rc::from(collected_placements.clone().into_boxed_slice());
15025 self.text_glyph_run_cache.put(
15026 run_key,
15027 CachedTextGlyphRun {
15028 glyphs,
15029 quads: None,
15030 atlas_generation: 0,
15031 },
15032 );
15033 None
15034 };
15035
15036 if draw_action == TextGlyphDrawAction::PrewarmOffscreen {
15037 let prewarm_quads = if let Some(quad_run) = cached_quad_run {
15038 quad_run
15039 } else {
15040 let prepare_start = Instant::now();
15041 match self.prepare_text_glyph_quads(
15042 run_key,
15043 atlas_generation,
15044 cached_glyph_run.as_deref(),
15045 &collected_run,
15046 &mut generated_quads,
15047 ) {
15048 Ok(quads) => {
15049 if let Some(collect_ms) = miss_collect_ms {
15050 if text_glyph_run_diag_enabled() {
15051 log::warn!(
15052 "[text-glyph-run-diag] visible=false glyphs={} cached={} new={} collect_ms={:.2} prepare_ms={:.2}",
15053 quads.len(),
15054 miss_cached_glyphs,
15055 miss_new_glyphs,
15056 collect_ms,
15057 instant_ms(prepare_start, Instant::now()),
15058 );
15059 }
15060 }
15061 quads
15062 }
15063 Err(_) => continue,
15064 }
15065 };
15066 #[cfg(not(target_arch = "wasm32"))]
15067 if should_use_retained_text_glyph_run(prewarm_quads.len(), source_draw.clip) {
15068 self.ensure_retained_text_glyph_run(run_key, prewarm_quads.as_ref());
15069 }
15070 prewarmed_glyphs = prewarmed_glyphs.saturating_add(prewarm_quads.len());
15071 continue;
15072 }
15073
15074 let draw_rect = Rect {
15075 x: source_raster_rect.x / root_scale,
15076 y: source_raster_rect.y / root_scale,
15077 width: source_raster_rect.width / root_scale,
15078 height: source_raster_rect.height / root_scale,
15079 };
15080 let Some(scissor) = scissor_rect_for_layer(
15081 draw_rect,
15082 source_draw.clip,
15083 root_scale,
15084 viewport.width,
15085 viewport.height,
15086 ) else {
15087 continue;
15088 };
15089
15090 #[cfg(not(target_arch = "wasm32"))]
15091 if let Some(quad_run) = cached_quad_run.as_ref() {
15092 if should_use_retained_text_glyph_run(quad_run.len(), source_draw.clip)
15093 && self.emit_retained_text_glyph_run_if_ready(
15094 run_key,
15095 quad_run.as_ref(),
15096 source_draw.clip,
15097 viewport,
15098 source_raster_rect,
15099 scissor,
15100 staged_uploads,
15101 glyph_cmds,
15102 )
15103 {
15104 emitted_glyphs = emitted_glyphs.saturating_add(quad_run.len());
15105 continue;
15106 }
15107 }
15108
15109 let index_start = image_indices.len() as u32;
15110 if let Some(quad_run) = cached_quad_run {
15111 emitted_glyphs = emitted_glyphs.saturating_add(self.append_text_glyph_quad_run(
15112 source_raster_rect,
15113 quad_run.as_ref(),
15114 source_draw.clip,
15115 viewport,
15116 root_scale,
15117 image_vertices,
15118 image_indices,
15119 true,
15120 ));
15121 } else {
15122 let prepare_start = Instant::now();
15123 let Ok(quad_run) = self.prepare_text_glyph_quads(
15124 run_key,
15125 atlas_generation,
15126 cached_glyph_run.as_deref(),
15127 &collected_run,
15128 &mut generated_quads,
15129 ) else {
15130 image_vertices.truncate(initial_vertex_len);
15131 image_indices.truncate(initial_index_len);
15132 glyph_cmds.truncate(initial_cmd_len);
15133 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
15134 self.scratch_text_glyph_run = collected_run;
15135 self.scratch_text_glyph_placements = collected_placements;
15136 self.scratch_text_glyph_quads = generated_quads;
15137 return Ok(false);
15138 };
15139 if let Some(collect_ms) = miss_collect_ms {
15140 if text_glyph_run_diag_enabled() {
15141 log::warn!(
15142 "[text-glyph-run-diag] visible=true glyphs={} cached={} new={} collect_ms={:.2} prepare_ms={:.2}",
15143 quad_run.len(),
15144 miss_cached_glyphs,
15145 miss_new_glyphs,
15146 collect_ms,
15147 instant_ms(prepare_start, Instant::now()),
15148 );
15149 }
15150 }
15151 emitted_glyphs = emitted_glyphs.saturating_add(self.append_text_glyph_quad_run(
15152 source_raster_rect,
15153 quad_run.as_ref(),
15154 source_draw.clip,
15155 viewport,
15156 root_scale,
15157 image_vertices,
15158 image_indices,
15159 false,
15160 ));
15161 }
15162 let index_count = image_indices.len() as u32 - index_start;
15163 if index_count > 0 {
15164 glyph_cmds.push(GlyphDrawCmd::shared(index_start, index_count, scissor));
15165 }
15166 }
15167
15168 self.scratch_text_glyph_run = collected_run;
15169 self.scratch_text_glyph_placements = collected_placements;
15170 self.scratch_text_glyph_quads = generated_quads;
15171 let append_end = Instant::now();
15172 if let Some(total_ms) = should_log_wgpu_render_stage(append_start, append_end) {
15173 log::warn!(
15174 "[wgpu-render-stage:text-glyph-atlas] total_ms={total_ms:.2} visited={} cmds={} glyphs={} prewarmed={} run_hits={} run_misses={}",
15175 visited,
15176 glyph_cmds.len().saturating_sub(initial_cmd_len),
15177 emitted_glyphs,
15178 prewarmed_glyphs,
15179 run_hits,
15180 run_misses,
15181 );
15182 }
15183 Ok(true)
15184 }
15185
15186 #[cfg(not(target_arch = "wasm32"))]
15187 fn text_glyph_prewarm_decision(
15188 &self,
15189 text_draw: &TextDraw,
15190 viewport: ViewportUniformParams,
15191 root_scale: f32,
15192 ) -> TextGlyphPrewarmDecision {
15193 let Some((logical_rect, _, clip, _, static_text_motion)) =
15194 self.text_raster_geometry(text_draw, root_scale)
15195 else {
15196 return TextGlyphPrewarmDecision::MissingGeometry;
15197 };
15198 if !static_text_motion {
15199 return TextGlyphPrewarmDecision::DynamicMotion;
15200 }
15201 if text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale) {
15202 return TextGlyphPrewarmDecision::Visible;
15203 }
15204 if text_draw_should_prewarm_in_viewport(logical_rect, clip, viewport, root_scale) {
15205 TextGlyphPrewarmDecision::Candidate
15206 } else {
15207 TextGlyphPrewarmDecision::OutsidePrewarmWindow
15208 }
15209 }
15210
15211 #[cfg(not(target_arch = "wasm32"))]
15212 #[allow(clippy::too_many_arguments)]
15213 fn prewarm_offscreen_text_glyph_draws_in_chunk(
15214 &mut self,
15215 ordered_items: &[(usize, SegmentDrawItem)],
15216 texts: &[TextDraw],
15217 chunk: &SegmentDrawChunkPlan,
15218 viewport: ViewportUniformParams,
15219 root_scale: f32,
15220 staged_uploads: &mut StagedBufferUploads,
15221 image_vertices: &mut Vec<Vertex>,
15222 image_indices: &mut Vec<u32>,
15223 glyph_cmds: &mut Vec<GlyphDrawCmd>,
15224 ) -> Result<(), String> {
15225 let prewarm_start = Instant::now();
15226 let diag_enabled = cranpose_core::env_flag!("CRANPOSE_TEXT_PREWARM_DIAG");
15227 let mut text_items = 0usize;
15228 let mut candidates = 0usize;
15229 let mut missing_geometry = 0usize;
15230 let mut dynamic_motion = 0usize;
15231 let mut visible = 0usize;
15232 let mut outside = 0usize;
15233 let mut already_prepared = 0usize;
15234 let mut admitted_candidates = 0usize;
15235 let mut skipped_unbounded = 0usize;
15236 let mut skipped_budget = 0usize;
15237 let initial_vertex_len = image_vertices.len();
15238 let initial_index_len = image_indices.len();
15239 let initial_cmd_len = glyph_cmds.len();
15240 let initial_staged_bytes_len = staged_uploads.bytes.len();
15241 let initial_staged_copies_len = staged_uploads.copies.len();
15242 'batches: for batch in chunk.iter() {
15243 let SegmentBatchPlan::Text { start, end } = batch else {
15244 continue;
15245 };
15246 for (_, item) in &ordered_items[start..end] {
15247 if offscreen_text_glyph_prewarm_budget_exhausted(prewarm_start, admitted_candidates)
15248 {
15249 skipped_budget = skipped_budget.saturating_add(1);
15250 break 'batches;
15251 }
15252 let SegmentDrawItem::Text(text_index) = item else {
15253 return Err(format!(
15254 "text prewarm batch contains non-text draw item: {item:?}"
15255 ));
15256 };
15257 let Some(text_draw) = texts.get(*text_index) else {
15258 continue;
15259 };
15260 text_items = text_items.saturating_add(1);
15261 match self.text_glyph_prewarm_decision(text_draw, viewport, root_scale) {
15262 TextGlyphPrewarmDecision::Candidate => {}
15263 TextGlyphPrewarmDecision::MissingGeometry => {
15264 missing_geometry = missing_geometry.saturating_add(1);
15265 continue;
15266 }
15267 TextGlyphPrewarmDecision::DynamicMotion => {
15268 dynamic_motion = dynamic_motion.saturating_add(1);
15269 continue;
15270 }
15271 TextGlyphPrewarmDecision::Visible => {
15272 visible = visible.saturating_add(1);
15273 continue;
15274 }
15275 TextGlyphPrewarmDecision::OutsidePrewarmWindow => {
15276 outside = outside.saturating_add(1);
15277 continue;
15278 }
15279 }
15280
15281 candidates = candidates.saturating_add(1);
15282 let Some((_, raster_rect, _, text_scale, static_text_motion)) =
15283 self.text_raster_geometry(text_draw, root_scale)
15284 else {
15285 missing_geometry = missing_geometry.saturating_add(1);
15286 continue;
15287 };
15288 let raster_source = text_glyph_raster_source(text_draw, raster_rect);
15289 let source_draw = raster_source.draw.as_ref();
15290 let run_key = Self::text_glyph_run_cache_key(
15291 source_draw,
15292 raster_source.raster_rect,
15293 text_scale,
15294 static_text_motion,
15295 );
15296 let atlas_generation = self.text_glyph_atlas.generation();
15297 let cached_glyphs = if let Some(cached) = self.text_glyph_run_cache.peek(&run_key) {
15298 if cached.atlas_generation == atlas_generation && cached.quads.is_some() {
15299 already_prepared = already_prepared.saturating_add(1);
15300 continue;
15301 }
15302 Some(cached.glyphs.len())
15303 } else {
15304 None
15305 };
15306 if !offscreen_text_glyph_prewarm_work_is_bounded(
15307 cached_glyphs,
15308 source_draw.text.text.len(),
15309 ) {
15310 skipped_unbounded = skipped_unbounded.saturating_add(1);
15311 continue;
15312 }
15313 admitted_candidates = admitted_candidates.saturating_add(1);
15314 self.append_text_glyph_draws(
15315 std::iter::once(text_draw),
15316 viewport,
15317 root_scale,
15318 true,
15319 staged_uploads,
15320 image_vertices,
15321 image_indices,
15322 glyph_cmds,
15323 )?;
15324 image_vertices.truncate(initial_vertex_len);
15325 image_indices.truncate(initial_index_len);
15326 glyph_cmds.truncate(initial_cmd_len);
15327 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
15328 }
15329 }
15330
15331 if diag_enabled && text_items > 0 {
15332 log::warn!(
15333 "[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}"
15334 );
15335 }
15336 if admitted_candidates > 0 {
15337 if let Some(total_ms) = should_log_wgpu_render_stage(prewarm_start, Instant::now()) {
15338 log::warn!(
15339 "[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}"
15340 );
15341 }
15342 }
15343 Ok(())
15344 }
15345
15346 fn prepare_text_glyph_draw_cmds<'a, I>(
15347 &mut self,
15348 layer_texts: I,
15349 viewport: ViewportUniformParams,
15350 root_scale: f32,
15351 staged_uploads: &mut StagedBufferUploads,
15352 ) -> Result<Option<PreparedGlyphBatch>, String>
15353 where
15354 I: IntoIterator<Item = &'a TextDraw>,
15355 {
15356 #[cfg(target_arch = "wasm32")]
15357 let _ = staged_uploads;
15358
15359 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
15360 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
15361 let mut glyph_cmds = std::mem::take(&mut self.scratch_glyph_cmds);
15362 image_vertices.clear();
15363 image_indices.clear();
15364 glyph_cmds.clear();
15365
15366 if !self.append_text_glyph_draws(
15367 layer_texts,
15368 viewport,
15369 root_scale,
15370 false,
15371 staged_uploads,
15372 &mut image_vertices,
15373 &mut image_indices,
15374 &mut glyph_cmds,
15375 )? {
15376 self.scratch_image_vertices = image_vertices;
15377 self.scratch_image_indices = image_indices;
15378 self.scratch_glyph_cmds = glyph_cmds;
15379 return Ok(None);
15380 }
15381
15382 #[cfg(not(target_arch = "wasm32"))]
15383 if !image_indices.is_empty() {
15384 self.stage_native_image_buffers(
15385 staged_uploads,
15386 viewport,
15387 &image_vertices,
15388 &image_indices,
15389 );
15390 }
15391
15392 #[cfg(target_arch = "wasm32")]
15393 let image_slot = if glyph_cmds.is_empty() {
15394 0
15395 } else {
15396 let slot = self.claim_wasm_image_batch();
15397 {
15398 let buffers = &mut self.wasm_image_batches[slot];
15399 buffers.ensure_capacity(&self.device, image_vertices.len(), image_indices.len());
15400 }
15401 let buffers = &self.wasm_image_batches[slot];
15402 self.write_wasm_buffer(
15403 &buffers.vertex_buffer,
15404 bytemuck::cast_slice(&image_vertices),
15405 );
15406 self.write_wasm_buffer(&buffers.index_buffer, bytemuck::cast_slice(&image_indices));
15407 slot
15408 };
15409
15410 #[cfg(target_arch = "wasm32")]
15411 let uniform_slot = if glyph_cmds.is_empty() {
15412 0
15413 } else {
15414 self.prepare_wasm_viewport_uniforms(viewport)
15415 };
15416
15417 self.scratch_image_vertices = image_vertices;
15418 self.scratch_image_indices = image_indices;
15419 Ok(Some(PreparedGlyphBatch {
15420 cmds: glyph_cmds,
15421 #[cfg(target_arch = "wasm32")]
15422 image_slot,
15423 #[cfg(target_arch = "wasm32")]
15424 uniform_slot,
15425 }))
15426 }
15427
15428 #[allow(clippy::too_many_arguments)]
15429 fn append_image_bitmap_draw_cmd(
15430 &mut self,
15431 image: &ImageBitmap,
15432 rect: Rect,
15433 clip: Option<Rect>,
15434 sampling: ImageSampling,
15435 viewport: ViewportUniformParams,
15436 root_scale: f32,
15437 image_vertices: &mut Vec<Vertex>,
15438 image_indices: &mut Vec<u32>,
15439 image_cmds: &mut Vec<ImageDrawCmd>,
15440 ) -> Result<(), String> {
15441 if rect.width <= 0.0 || rect.height <= 0.0 {
15442 return Ok(());
15443 }
15444
15445 self.ensure_image_cached(image)?;
15446
15447 let (device_quad, scissor_rect) =
15448 if sampling == ImageSampling::Nearest && root_scale.is_finite() && root_scale > 0.0 {
15449 let left_px = (rect.x * root_scale).round();
15450 let top_px = (rect.y * root_scale).round();
15451 let width_px = (rect.width * root_scale).round().max(1.0);
15452 let height_px = (rect.height * root_scale).round().max(1.0);
15453 let snapped_rect = Rect {
15454 x: left_px / root_scale,
15455 y: top_px / root_scale,
15456 width: width_px / root_scale,
15457 height: height_px / root_scale,
15458 };
15459 let right_px = left_px + width_px;
15460 let bottom_px = top_px + height_px;
15461 (
15462 [
15463 [left_px, top_px],
15464 [right_px, top_px],
15465 [left_px, bottom_px],
15466 [right_px, bottom_px],
15467 ],
15468 snapped_rect,
15469 )
15470 } else {
15471 (
15472 rect_to_quad(rect).map(|[x, y]| [x * root_scale, y * root_scale]),
15473 rect,
15474 )
15475 };
15476
15477 let Some(scissor) = scissor_rect_for_layer(
15478 scissor_rect,
15479 clip,
15480 root_scale,
15481 viewport.width,
15482 viewport.height,
15483 ) else {
15484 return Ok(());
15485 };
15486 let Some(uv_rect) = image_uv_rect(image, None) else {
15487 return Ok(());
15488 };
15489 #[cfg(not(target_arch = "wasm32"))]
15490 {
15491 if fill_area_diag_enabled() {
15492 self.fill_area_diag.add_image_quad(&device_quad);
15493 }
15494 }
15495
15496 let base_vertex = image_vertices.len() as u32;
15497 let index_start = image_indices.len() as u32;
15498 image_indices.extend_from_slice(&[
15499 base_vertex,
15500 base_vertex + 1,
15501 base_vertex + 2,
15502 base_vertex + 2,
15503 base_vertex + 1,
15504 base_vertex + 3,
15505 ]);
15506 let color = [1.0, 1.0, 1.0, 1.0];
15507 image_vertices.extend_from_slice(&[
15508 Vertex {
15509 position: device_quad[0],
15510 color,
15511 uv: [uv_rect.min[0], uv_rect.min[1]],
15512 uv_bounds: uv_rect.sample_bounds,
15513 },
15514 Vertex {
15515 position: device_quad[1],
15516 color,
15517 uv: [uv_rect.max[0], uv_rect.min[1]],
15518 uv_bounds: uv_rect.sample_bounds,
15519 },
15520 Vertex {
15521 position: device_quad[2],
15522 color,
15523 uv: [uv_rect.min[0], uv_rect.max[1]],
15524 uv_bounds: uv_rect.sample_bounds,
15525 },
15526 Vertex {
15527 position: device_quad[3],
15528 color,
15529 uv: [uv_rect.max[0], uv_rect.max[1]],
15530 uv_bounds: uv_rect.sample_bounds,
15531 },
15532 ]);
15533 image_cmds.push(ImageDrawCmd {
15534 index_start,
15535 scissor,
15536 image_id: image.id(),
15537 sampling,
15538 });
15539 Ok(())
15540 }
15541
15542 #[allow(clippy::too_many_arguments)]
15543 fn append_text_image_draw_cmds<'a, I>(
15544 &mut self,
15545 layer_texts: I,
15546 viewport: ViewportUniformParams,
15547 root_scale: f32,
15548 image_vertices: &mut Vec<Vertex>,
15549 image_indices: &mut Vec<u32>,
15550 image_cmds: &mut Vec<ImageDrawCmd>,
15551 ) -> Result<(), String>
15552 where
15553 I: Iterator<Item = &'a TextDraw>,
15554 {
15555 let append_start = Instant::now();
15556 let initial_len = image_cmds.len();
15557 let mut visited = 0usize;
15558 let mut hit_count = 0usize;
15559 let mut miss_count = 0usize;
15560 for text_draw in layer_texts {
15561 visited = visited.saturating_add(1);
15562 let _ = text_draw.node_id;
15563 let Some((logical_rect, raster_rect, clip, text_scale, static_text_motion)) =
15564 self.text_raster_geometry(text_draw, root_scale)
15565 else {
15566 continue;
15567 };
15568 if !text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale) {
15569 continue;
15570 }
15571
15572 let raster_source = self.text_image_raster_source(
15573 text_draw,
15574 logical_rect,
15575 raster_rect,
15576 clip,
15577 root_scale,
15578 static_text_motion,
15579 );
15580 let source_draw = raster_source.draw.as_ref();
15581 let source_raster_rect = raster_source.raster_rect;
15582
15583 let cache_key = Self::text_image_cache_key(
15584 source_draw,
15585 source_raster_rect,
15586 text_scale,
15587 static_text_motion,
15588 );
15589 let image = if let Some(cached) = self.text_image_cache.get(&cache_key) {
15590 self.frame_stats
15591 .record_text_image_cache_hit(cached.image.width(), cached.image.height());
15592 hit_count = hit_count.saturating_add(1);
15593 cached.image.clone()
15594 } else {
15595 let Some(image) =
15596 self.rasterize_text_draw_to_image(source_draw, source_raster_rect, text_scale)
15597 else {
15598 continue;
15599 };
15600 self.frame_stats
15601 .record_text_image_cache_miss(image.width(), image.height());
15602 miss_count = miss_count.saturating_add(1);
15603 self.text_image_cache.put(
15604 cache_key,
15605 CachedTextImage {
15606 image: image.clone(),
15607 },
15608 );
15609 image
15610 };
15611
15612 let draw_origin = if static_text_motion {
15613 Point::new(
15614 source_raster_rect.x / root_scale,
15615 source_raster_rect.y / root_scale,
15616 )
15617 } else {
15618 Point::new(logical_rect.x, logical_rect.y)
15619 };
15620 let draw_rect = Rect {
15621 x: draw_origin.x,
15622 y: draw_origin.y,
15623 width: image.width() as f32 / root_scale,
15624 height: image.height() as f32 / root_scale,
15625 };
15626 self.append_image_bitmap_draw_cmd(
15627 &image,
15628 draw_rect,
15629 clip,
15630 ImageSampling::Nearest,
15631 viewport,
15632 root_scale,
15633 image_vertices,
15634 image_indices,
15635 image_cmds,
15636 )?;
15637 }
15638 let append_end = Instant::now();
15639 if let Some(total_ms) = should_log_wgpu_render_stage(append_start, append_end) {
15640 log::warn!(
15641 "[wgpu-render-stage:text-images] total_ms={total_ms:.2} visited={} emitted={} hits={} misses={}",
15642 visited,
15643 image_cmds.len().saturating_sub(initial_len),
15644 hit_count,
15645 miss_count,
15646 );
15647 }
15648 Ok(())
15649 }
15650
15651 fn text_image_raster_source<'a>(
15652 &mut self,
15653 text_draw: &'a TextDraw,
15654 logical_rect: Rect,
15655 raster_rect: Rect,
15656 clip: Option<Rect>,
15657 root_scale: f32,
15658 static_text_motion: bool,
15659 ) -> TextRasterSource<'a> {
15660 let Some(clip) = clip else {
15661 return TextRasterSource {
15662 draw: Cow::Borrowed(text_draw),
15663 raster_rect,
15664 };
15665 };
15666 if !static_text_motion || text_draw.text.text.as_str().find('\n').is_none() {
15667 return TextRasterSource {
15668 draw: Cow::Borrowed(text_draw),
15669 raster_rect,
15670 };
15671 }
15672
15673 let line_starts = self.text_line_index_cache.line_starts(&text_draw.text);
15674 clipped_text_raster_source_with_line_starts(
15675 text_draw,
15676 logical_rect,
15677 raster_rect,
15678 clip,
15679 root_scale,
15680 line_starts.as_ref(),
15681 )
15682 }
15683
15684 fn prepare_text_image_draw_cmds<'a, I>(
15685 &mut self,
15686 layer_texts: I,
15687 viewport: ViewportUniformParams,
15688 root_scale: f32,
15689 staged_uploads: &mut StagedBufferUploads,
15690 ) -> Result<PreparedImageBatch, String>
15691 where
15692 I: Iterator<Item = &'a TextDraw>,
15693 {
15694 #[cfg(target_arch = "wasm32")]
15695 let _ = staged_uploads;
15696
15697 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
15698 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
15699 let mut image_cmds = std::mem::take(&mut self.scratch_image_cmds);
15700 image_vertices.clear();
15701 image_indices.clear();
15702 image_cmds.clear();
15703
15704 self.append_text_image_draw_cmds(
15705 layer_texts,
15706 viewport,
15707 root_scale,
15708 &mut image_vertices,
15709 &mut image_indices,
15710 &mut image_cmds,
15711 )?;
15712
15713 #[cfg(not(target_arch = "wasm32"))]
15714 if !image_cmds.is_empty() {
15715 self.stage_native_image_buffers(
15716 staged_uploads,
15717 viewport,
15718 &image_vertices,
15719 &image_indices,
15720 );
15721 }
15722
15723 #[cfg(target_arch = "wasm32")]
15724 let image_slot = if image_cmds.is_empty() {
15725 0
15726 } else {
15727 let slot = self.claim_wasm_image_batch();
15728 {
15729 let buffers = &mut self.wasm_image_batches[slot];
15730 buffers.ensure_capacity(&self.device, image_vertices.len(), image_indices.len());
15731 }
15732 let buffers = &self.wasm_image_batches[slot];
15733 self.write_wasm_buffer(
15734 &buffers.vertex_buffer,
15735 bytemuck::cast_slice(&image_vertices),
15736 );
15737 self.write_wasm_buffer(&buffers.index_buffer, bytemuck::cast_slice(&image_indices));
15738 slot
15739 };
15740
15741 #[cfg(target_arch = "wasm32")]
15742 let uniform_slot = if image_cmds.is_empty() {
15743 0
15744 } else {
15745 self.prepare_wasm_viewport_uniforms(viewport)
15746 };
15747
15748 self.scratch_image_vertices = image_vertices;
15749 self.scratch_image_indices = image_indices;
15750 Ok(PreparedImageBatch {
15751 cmds: image_cmds,
15752 #[cfg(target_arch = "wasm32")]
15753 image_slot,
15754 #[cfg(target_arch = "wasm32")]
15755 uniform_slot,
15756 })
15757 }
15758
15759 fn text_raster_geometry(
15760 &self,
15761 text_draw: &TextDraw,
15762 root_scale: f32,
15763 ) -> Option<(Rect, Rect, Option<Rect>, f32, bool)> {
15764 text_raster_geometry_for_draw(text_draw, root_scale)
15765 }
15766
15767 fn text_image_cache_key(
15768 text_draw: &TextDraw,
15769 raster_rect: Rect,
15770 text_scale: f32,
15771 static_text_motion: bool,
15772 ) -> TextImageCacheKey {
15773 let mut state = default_hash::new();
15774 text_draw.text.render_hash().hash(&mut state);
15775 text_draw.text_style.render_hash().hash(&mut state);
15776 text_draw.color.render_hash().hash(&mut state);
15777 hash_text_raster_geometry_for_cache(raster_rect, static_text_motion, &mut state);
15778 text_draw.font_size.to_bits().hash(&mut state);
15779 text_scale.to_bits().hash(&mut state);
15780 text_draw.layout_options.hash(&mut state);
15781 TextImageCacheKey(state.finish())
15782 }
15783
15784 fn text_glyph_run_cache_key(
15785 text_draw: &TextDraw,
15786 raster_rect: Rect,
15787 text_scale: f32,
15788 static_text_motion: bool,
15789 ) -> TextGlyphRunCacheKey {
15790 TextGlyphRunCacheKey(
15791 Self::text_image_cache_key(text_draw, raster_rect, text_scale, static_text_motion).0,
15792 )
15793 }
15794
15795 fn rasterize_text_draw_to_image(
15796 &mut self,
15797 text_draw: &TextDraw,
15798 raster_rect: Rect,
15799 text_scale: f32,
15800 ) -> Option<ImageBitmap> {
15801 if text_draw.text.span_styles.is_empty() {
15802 let font = self.text_fonts.resolve(&text_draw.text_style)?;
15803 return rasterize_text_to_image_with_glyph_cache(
15804 text_draw.text.text.as_str(),
15805 raster_rect,
15806 &text_draw.text_style,
15807 text_draw.color,
15808 text_draw.font_size,
15809 text_scale,
15810 font,
15811 &mut self.text_glyph_mask_cache,
15812 );
15813 }
15814
15815 if let Some(image) = rasterize_annotated_text_to_image_with_glyph_cache(
15816 text_draw.text.as_ref(),
15817 raster_rect,
15818 &text_draw.text_style,
15819 text_draw.color,
15820 text_draw.font_size,
15821 text_scale,
15822 &self.text_fonts,
15823 &mut self.text_glyph_mask_cache,
15824 ) {
15825 return Some(image);
15826 }
15827
15828 rasterize_spanned_text_to_image(
15829 text_draw,
15830 raster_rect,
15831 text_scale,
15832 &self.text_fonts,
15833 &mut self.text_glyph_mask_cache,
15834 )
15835 }
15836}
15837
15838fn rasterize_spanned_text_to_image(
15839 text_draw: &TextDraw,
15840 raster_rect: Rect,
15841 text_scale: f32,
15842 fonts: &SoftwareTextFontSet,
15843 glyph_cache: &mut SoftwareGlyphRasterCache,
15844) -> Option<ImageBitmap> {
15845 let width = raster_rect.width.ceil().max(1.0) as u32;
15846 let height = raster_rect.height.ceil().max(1.0) as u32;
15847 let mut canvas = vec![0_u8; (width as usize) * (height as usize) * 4];
15848 let boundaries = text_draw.text.span_boundaries();
15849 let base_line_height = text_draw
15850 .text_style
15851 .resolve_line_height(14.0, text_draw.font_size)
15852 .max(1.0);
15853 let mut current_line_height = base_line_height;
15854 let mut cursor_x = raster_rect.x;
15855 let mut cursor_y = raster_rect.y;
15856
15857 for window in boundaries.windows(2) {
15858 let start = window[0];
15859 let end = window[1];
15860 if start == end {
15861 continue;
15862 }
15863
15864 let chunk = &text_draw.text.text[start..end];
15865 let mut merged_span = text_draw.text_style.span_style.clone();
15866 for span in &text_draw.text.span_styles {
15867 if span.range.start <= start && span.range.end >= end {
15868 merged_span = merged_span.merge(&span.item);
15869 }
15870 }
15871
15872 let mut chunk_style = text_draw.text_style.clone();
15873 chunk_style.span_style = merged_span;
15874
15875 for part in chunk.split_inclusive('\n') {
15876 let has_newline = part.ends_with('\n');
15877 let content = if has_newline {
15878 &part[..part.len().saturating_sub(1)]
15879 } else {
15880 part
15881 };
15882
15883 if !content.is_empty() {
15884 let chunk_font_size = chunk_style.resolve_font_size(text_draw.font_size);
15885 let Some(font) = fonts.resolve(&chunk_style) else {
15886 continue;
15887 };
15888 let metrics = measure_text_with_font(content, &chunk_style, chunk_font_size, font);
15889 let segment_rect = Rect {
15890 x: cursor_x,
15891 y: cursor_y,
15892 width: (metrics.width * text_scale).ceil().max(1.0),
15893 height: (metrics.height * text_scale).ceil().max(1.0),
15894 };
15895 if let Some(segment_image) = rasterize_text_to_image_with_glyph_cache(
15896 content,
15897 segment_rect,
15898 &chunk_style,
15899 chunk_style.resolve_text_color(text_draw.color),
15900 chunk_font_size,
15901 text_scale,
15902 font,
15903 glyph_cache,
15904 ) {
15905 composite_text_segment(
15906 &mut canvas,
15907 width,
15908 height,
15909 raster_rect,
15910 segment_rect,
15911 &segment_image,
15912 );
15913 }
15914 cursor_x += metrics.width * text_scale;
15915 current_line_height = current_line_height.max(metrics.line_height.max(1.0));
15916 }
15917
15918 if has_newline {
15919 cursor_x = raster_rect.x;
15920 cursor_y += current_line_height * text_scale;
15921 current_line_height = base_line_height;
15922 }
15923 }
15924 }
15925
15926 ImageBitmap::from_rgba8(width, height, canvas).ok()
15927}
15928
15929struct TextRasterSource<'a> {
15930 draw: Cow<'a, TextDraw>,
15931 raster_rect: Rect,
15932}
15933
15934fn text_glyph_raster_source(text_draw: &TextDraw, raster_rect: Rect) -> TextRasterSource<'_> {
15935 TextRasterSource {
15936 draw: Cow::Borrowed(text_draw),
15937 raster_rect,
15938 }
15939}
15940
15941#[cfg(test)]
15942fn clipped_text_raster_source<'a>(
15943 text_draw: &'a TextDraw,
15944 logical_rect: Rect,
15945 raster_rect: Rect,
15946 clip: Option<Rect>,
15947 root_scale: f32,
15948 static_text_motion: bool,
15949) -> TextRasterSource<'a> {
15950 let Some(clip) = clip else {
15951 return TextRasterSource {
15952 draw: Cow::Borrowed(text_draw),
15953 raster_rect,
15954 };
15955 };
15956 if !static_text_motion || text_draw.text.text.as_str().find('\n').is_none() {
15957 return TextRasterSource {
15958 draw: Cow::Borrowed(text_draw),
15959 raster_rect,
15960 };
15961 }
15962 let line_starts = line_start_offsets(text_draw.text.text.as_str());
15963 clipped_text_raster_source_with_line_starts(
15964 text_draw,
15965 logical_rect,
15966 raster_rect,
15967 clip,
15968 root_scale,
15969 &line_starts,
15970 )
15971}
15972
15973fn clipped_text_raster_source_with_line_starts<'a>(
15974 text_draw: &'a TextDraw,
15975 logical_rect: Rect,
15976 raster_rect: Rect,
15977 clip: Rect,
15978 root_scale: f32,
15979 line_starts: &[usize],
15980) -> TextRasterSource<'a> {
15981 if line_starts.len() < MIN_MULTILINE_TEXT_LINES_FOR_CLIPPED_RASTER {
15982 return TextRasterSource {
15983 draw: Cow::Borrowed(text_draw),
15984 raster_rect,
15985 };
15986 }
15987
15988 let Some(visible_rect) = logical_rect.intersect(clip) else {
15989 return TextRasterSource {
15990 draw: Cow::Borrowed(text_draw),
15991 raster_rect,
15992 };
15993 };
15994
15995 let line_count = line_starts.len().max(1);
15996 let line_height = logical_rect.height / line_count as f32;
15997 if !line_height.is_finite() || line_height <= 0.0 {
15998 return TextRasterSource {
15999 draw: Cow::Borrowed(text_draw),
16000 raster_rect,
16001 };
16002 }
16003
16004 let visible_top = ((visible_rect.y - logical_rect.y) / line_height).floor() as isize;
16005 let visible_bottom =
16006 ((visible_rect.y + visible_rect.height - logical_rect.y) / line_height).ceil() as isize;
16007 let start_line = visible_top.saturating_sub(1).max(0) as usize;
16008 let end_line = (visible_bottom + 1).max(start_line as isize + 1) as usize;
16009 let end_line = end_line.min(line_count);
16010 if start_line == 0 && end_line >= line_count {
16011 return TextRasterSource {
16012 draw: Cow::Borrowed(text_draw),
16013 raster_rect,
16014 };
16015 }
16016
16017 let byte_start = line_starts[start_line];
16018 let byte_end = line_end_offset(text_draw.text.text.as_str(), line_starts, end_line - 1);
16019 if byte_start >= byte_end {
16020 return TextRasterSource {
16021 draw: Cow::Borrowed(text_draw),
16022 raster_rect,
16023 };
16024 }
16025
16026 let slice_y = logical_rect.y + start_line as f32 * line_height;
16027 let slice_height = (end_line - start_line) as f32 * line_height;
16028 let mut slice_raster_rect = Rect {
16029 x: logical_rect.x * root_scale,
16030 y: slice_y * root_scale,
16031 width: logical_rect.width * root_scale,
16032 height: slice_height * root_scale,
16033 };
16034 slice_raster_rect.x = slice_raster_rect.x.round();
16035 slice_raster_rect.y = slice_raster_rect.y.round();
16036 slice_raster_rect.width = slice_raster_rect.width.ceil().max(1.0);
16037 slice_raster_rect.height = slice_raster_rect.height.ceil().max(1.0);
16038
16039 let mut sliced_draw = text_draw.clone();
16040 sliced_draw.rect = Rect {
16041 x: logical_rect.x,
16042 y: slice_y,
16043 width: logical_rect.width,
16044 height: slice_height,
16045 };
16046 sliced_draw.text = Arc::new(text_draw.text.subsequence(byte_start..byte_end));
16047
16048 TextRasterSource {
16049 draw: Cow::Owned(sliced_draw),
16050 raster_rect: slice_raster_rect,
16051 }
16052}
16053
16054fn line_start_offsets(text: &str) -> Vec<usize> {
16055 let mut starts =
16056 Vec::with_capacity(text.as_bytes().iter().filter(|b| **b == b'\n').count() + 1);
16057 starts.push(0);
16058 starts.extend(
16059 text.char_indices()
16060 .filter_map(|(index, ch)| (ch == '\n').then_some(index + ch.len_utf8())),
16061 );
16062 starts
16063}
16064
16065fn line_end_offset(text: &str, line_starts: &[usize], line: usize) -> usize {
16066 line_starts.get(line + 1).copied().unwrap_or(text.len())
16067}
16068
16069fn composite_text_segment(
16070 canvas: &mut [u8],
16071 canvas_width: u32,
16072 canvas_height: u32,
16073 canvas_rect: Rect,
16074 segment_rect: Rect,
16075 segment_image: &ImageBitmap,
16076) {
16077 let offset_x = (segment_rect.x - canvas_rect.x).round() as i32;
16078 let offset_y = (segment_rect.y - canvas_rect.y).round() as i32;
16079 let src = segment_image.pixels();
16080 for sy in 0..segment_image.height() as i32 {
16081 let dy = offset_y + sy;
16082 if dy < 0 || dy >= canvas_height as i32 {
16083 continue;
16084 }
16085 for sx in 0..segment_image.width() as i32 {
16086 let dx = offset_x + sx;
16087 if dx < 0 || dx >= canvas_width as i32 {
16088 continue;
16089 }
16090 let src_index = ((sy as u32 * segment_image.width() + sx as u32) * 4) as usize;
16091 let dst_index = ((dy as u32 * canvas_width + dx as u32) * 4) as usize;
16092 blend_rgba_pixel(
16093 &mut canvas[dst_index..dst_index + 4],
16094 &src[src_index..src_index + 4],
16095 );
16096 }
16097 }
16098}
16099
16100fn blend_rgba_pixel(dst: &mut [u8], src: &[u8]) {
16101 let src_alpha = src[3] as f32 / 255.0;
16102 if src_alpha <= 0.0 {
16103 return;
16104 }
16105 let dst_alpha = dst[3] as f32 / 255.0;
16106 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
16107 if out_alpha <= f32::EPSILON {
16108 dst.copy_from_slice(&[0, 0, 0, 0]);
16109 return;
16110 }
16111
16112 for channel in 0..3 {
16113 let src_channel = src[channel] as f32 / 255.0;
16114 let dst_channel = dst[channel] as f32 / 255.0;
16115 let src_premult = src_channel * src_alpha;
16116 let dst_premult = dst_channel * dst_alpha;
16117 dst[channel] =
16118 (((src_premult + dst_premult * (1.0 - src_alpha)) / out_alpha).clamp(0.0, 1.0) * 255.0)
16119 .round() as u8;
16120 }
16121 dst[3] = (out_alpha.clamp(0.0, 1.0) * 255.0).round() as u8;
16122}
16123
16124fn align_to(value: u32, alignment: u32) -> u32 {
16125 debug_assert!(alignment > 0);
16126 value.div_ceil(alignment) * alignment
16127}
16128
16129#[cfg(not(target_arch = "wasm32"))]
16130fn align_usize_to(value: usize, alignment: usize) -> usize {
16131 debug_assert!(alignment > 0);
16132 value.div_ceil(alignment) * alignment
16133}
16134
16135impl GpuRenderer {
16136 fn convert_surface_pixels_to_rgba(&self, pixels: &[u8]) -> Result<Vec<u8>, String> {
16137 if !pixels.len().is_multiple_of(4) {
16138 return Err("Screenshot readback has an incomplete pixel".to_string());
16139 }
16140 Ok(pixels.to_vec())
16141 }
16142}
16143
16144fn is_in_effect_range(z_index: usize, effect_z_ranges: &[Range<usize>]) -> bool {
16145 effect_z_ranges.iter().any(|range| range.contains(&z_index))
16146}
16147
16148#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16149enum SegmentDrawItem {
16150 Shape(usize),
16151 Image(usize),
16152 Text(usize),
16153 Shadow(usize),
16154 Composite(usize),
16155 ShaderComposite(usize),
16156 Retained(usize),
16157}
16158
16159#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16160enum SegmentBatchPlan {
16161 Shape {
16162 start: usize,
16163 end: usize,
16164 blend_mode: BlendMode,
16165 },
16166 Image {
16167 start: usize,
16168 end: usize,
16169 blend_mode: BlendMode,
16170 },
16171 Text {
16172 start: usize,
16173 end: usize,
16174 },
16175 Composite {
16176 start: usize,
16177 end: usize,
16178 },
16179 ShaderComposite {
16180 start: usize,
16181 end: usize,
16182 },
16183 Retained {
16186 start: usize,
16187 end: usize,
16188 },
16189}
16190
16191#[derive(Clone, Debug, Default, PartialEq, Eq)]
16192struct SegmentDrawChunkPlan {
16193 batches: Vec<SegmentBatchPlan>,
16194}
16195
16196struct SegmentRenderOutcome {
16197 rendered_any: bool,
16198 pass_count: u32,
16199}
16200
16201struct SegmentCommandEncodeOutcome {
16202 first_batch: bool,
16203}
16204
16205#[cfg(not(target_arch = "wasm32"))]
16206#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16207enum TextGlyphPrewarmDecision {
16208 Candidate,
16209 MissingGeometry,
16210 DynamicMotion,
16211 Visible,
16212 OutsidePrewarmWindow,
16213}
16214
16215#[cfg(not(target_arch = "wasm32"))]
16216#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16217struct NativeSegmentFusionBudget {
16218 shape_count: usize,
16219 gradient_stop_count: usize,
16220}
16221
16222#[cfg(not(target_arch = "wasm32"))]
16223#[derive(Clone, Debug, PartialEq, Eq)]
16224struct NativeSegmentFusionPartition {
16225 chunk: SegmentDrawChunkPlan,
16226 budget: NativeSegmentFusionBudget,
16227}
16228
16229#[cfg(not(target_arch = "wasm32"))]
16230#[derive(Clone, Debug, PartialEq, Eq)]
16231enum FusedSegmentBatch {
16232 Shape {
16233 batch: PreparedShapeBatch,
16234 blend_mode: BlendMode,
16235 },
16236 Image {
16237 cmd_range: Range<usize>,
16238 blend_mode: BlendMode,
16239 },
16240 Text {
16241 image_cmd_range: Range<usize>,
16242 glyph_cmd_range: Range<usize>,
16243 },
16244 Composite {
16245 draw_range: Range<usize>,
16246 },
16247 ShaderComposite {
16248 draw_range: Range<usize>,
16249 },
16250 Retained {
16251 item_range: Range<usize>,
16252 },
16253}
16254
16255struct ShadowSourceRenderOutcome {
16256 rendered_any: bool,
16257 pass_count: u32,
16258}
16259
16260#[cfg(not(target_arch = "wasm32"))]
16264struct SegmentCaptureJob {
16265 key: SegmentSurfaceKey,
16266 first: u32,
16267 last: u32,
16268 capture_index: u32,
16269}
16270
16271#[cfg(not(target_arch = "wasm32"))]
16275struct SegmentCompositePlan {
16276 key: SegmentSurfaceKey,
16277 dest_quad: [[f32; 2]; 4],
16278 inverse: [[f32; 3]; 3],
16279 identity: bool,
16280 integer_translation: bool,
16281}
16282
16283#[cfg(not(target_arch = "wasm32"))]
16286fn segment_identity_quad(rect: &CaptureRect) -> [[f32; 2]; 4] {
16287 let [x, y] = rect.origin;
16288 let width = rect.width as f32;
16289 let height = rect.height as f32;
16290 [
16291 [x, y],
16292 [x + width, y],
16293 [x, y + height],
16294 [x + width, y + height],
16295 ]
16296}
16297
16298#[cfg(not(target_arch = "wasm32"))]
16301fn segment_identity_inverse(rect: &CaptureRect) -> [[f32; 3]; 3] {
16302 [
16303 [1.0, 0.0, -rect.origin[0]],
16304 [0.0, 1.0, -rect.origin[1]],
16305 [0.0, 0.0, 1.0],
16306 ]
16307}
16308
16309#[cfg(not(target_arch = "wasm32"))]
16314fn plan_segment_capture_geometry(
16315 slot: &ReplaySlot,
16316 first: u32,
16317 last: u32,
16318 transform: SimilarityTransform,
16319 max_texture_dim: u32,
16320) -> Option<(CaptureRect, f32)> {
16321 let range = first as usize..last as usize;
16322 let aabbs = slot.shape_aabbs.get(range)?;
16323 if aabbs.is_empty() {
16324 return None;
16325 }
16326 let affine = Affine2::from_similarity(transform.center, transform.rot, transform.scale);
16327 let mut min = [f32::INFINITY; 2];
16328 let mut max = [f32::NEG_INFINITY; 2];
16329 for aabb in aabbs {
16330 for corner in [
16331 [aabb[0], aabb[1]],
16332 [aabb[2], aabb[1]],
16333 [aabb[0], aabb[3]],
16334 [aabb[2], aabb[3]],
16335 ] {
16336 let p = affine.apply(corner);
16337 min[0] = min[0].min(p[0]);
16338 min[1] = min[1].min(p[1]);
16339 max[0] = max[0].max(p[0]);
16340 max[1] = max[1].max(p[1]);
16341 }
16342 }
16343 let rect = crate::segment_surface::snap_capture_rect(min, max, max_texture_dim)?;
16344 let base_area = slot.area_prefix.get(last as usize).copied()?
16345 - slot.area_prefix.get(first as usize).copied()?;
16346 let member_px = base_area * transform.scale * transform.scale * slot.submitted_area_scale;
16347 Some((rect, member_px))
16348}
16349
16350impl SegmentDrawChunkPlan {
16351 fn is_empty(&self) -> bool {
16352 self.batches.is_empty()
16353 }
16354
16355 fn push(&mut self, batch: SegmentBatchPlan) {
16356 self.batches.push(batch);
16357 }
16358
16359 fn iter(&self) -> impl Iterator<Item = SegmentBatchPlan> + '_ {
16360 self.batches.iter().copied()
16361 }
16362}
16363
16364#[derive(Clone, Debug, PartialEq, Eq)]
16365enum SegmentRenderCommand {
16366 DrawChunk(SegmentDrawChunkPlan),
16367 Shadow(usize),
16368}
16369
16370struct SegmentCommandIter<'a> {
16371 ordered_items: &'a [(usize, SegmentDrawItem)],
16372 shapes: &'a [DrawShape],
16373 images: &'a [ImageDraw],
16374 cursor: usize,
16375 batch_limits: ShapeBatchLimits,
16376}
16377
16378impl<'a> SegmentCommandIter<'a> {
16379 fn new(
16380 ordered_items: &'a [(usize, SegmentDrawItem)],
16381 shapes: &'a [DrawShape],
16382 images: &'a [ImageDraw],
16383 batch_limits: ShapeBatchLimits,
16384 ) -> Self {
16385 Self {
16386 ordered_items,
16387 shapes,
16388 images,
16389 cursor: 0,
16390 batch_limits,
16391 }
16392 }
16393}
16394
16395impl Iterator for SegmentCommandIter<'_> {
16396 type Item = SegmentRenderCommand;
16397
16398 fn next(&mut self) -> Option<Self::Item> {
16399 if self.cursor >= self.ordered_items.len() {
16400 return None;
16401 }
16402
16403 if let SegmentDrawItem::Shadow(index) = self.ordered_items[self.cursor].1 {
16404 self.cursor += 1;
16405 return Some(SegmentRenderCommand::Shadow(index));
16406 }
16407
16408 let mut chunk = SegmentDrawChunkPlan::default();
16409 while self.cursor < self.ordered_items.len() {
16410 if let SegmentDrawItem::Shadow(index) = self.ordered_items[self.cursor].1 {
16411 if chunk.is_empty() {
16412 self.cursor += 1;
16413 return Some(SegmentRenderCommand::Shadow(index));
16414 }
16415 break;
16416 }
16417
16418 let Some((batch, next_cursor)) = segment_batch_plan_at_cursor(
16419 self.ordered_items,
16420 self.shapes,
16421 self.images,
16422 self.cursor,
16423 self.batch_limits,
16424 ) else {
16425 break;
16426 };
16427 chunk.push(batch);
16428 self.cursor = next_cursor;
16429 }
16430
16431 Some(SegmentRenderCommand::DrawChunk(chunk))
16432 }
16433}
16434
16435#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16436struct PreparedShapeBatch {
16437 vertex_start: u32,
16440 vertex_count: u32,
16441 has_gradient: bool,
16444 #[cfg(target_arch = "wasm32")]
16445 shape_slot: usize,
16446 #[cfg(target_arch = "wasm32")]
16447 uniform_slot: usize,
16448}
16449
16450struct PreparedImageBatch {
16451 cmds: Vec<ImageDrawCmd>,
16452 #[cfg(target_arch = "wasm32")]
16453 image_slot: usize,
16454 #[cfg(target_arch = "wasm32")]
16455 uniform_slot: usize,
16456}
16457
16458impl PreparedImageBatch {
16459 fn is_empty(&self) -> bool {
16460 self.cmds.is_empty()
16461 }
16462
16463 fn into_cmds(self) -> Vec<ImageDrawCmd> {
16464 self.cmds
16465 }
16466}
16467
16468struct PreparedGlyphBatch {
16469 cmds: Vec<GlyphDrawCmd>,
16470 #[cfg(target_arch = "wasm32")]
16471 image_slot: usize,
16472 #[cfg(target_arch = "wasm32")]
16473 uniform_slot: usize,
16474}
16475
16476impl PreparedGlyphBatch {
16477 fn is_empty(&self) -> bool {
16478 self.cmds.is_empty()
16479 }
16480
16481 fn into_cmds(self) -> Vec<GlyphDrawCmd> {
16482 self.cmds
16483 }
16484}
16485
16486#[cfg(not(target_arch = "wasm32"))]
16487fn gradient_stop_count_for_shape(shape: &DrawShape, brushes: &[Brush]) -> usize {
16488 match shape.brush {
16489 SceneBrush::Solid(_) => 0,
16490 SceneBrush::Gradient(index) => match &brushes[index as usize] {
16491 Brush::Solid(_) => 0,
16492 Brush::LinearGradient { colors, .. }
16493 | Brush::RadialGradient { colors, .. }
16494 | Brush::SweepGradient { colors, .. } => colors.len(),
16495 },
16496 }
16497}
16498
16499#[cfg(not(target_arch = "wasm32"))]
16500fn native_segment_fusion_budget(
16501 ordered_items: &[(usize, SegmentDrawItem)],
16502 shapes: &[DrawShape],
16503 brushes: &[Brush],
16504 chunk: &SegmentDrawChunkPlan,
16505 batch_limits: ShapeBatchLimits,
16506) -> Result<Option<NativeSegmentFusionBudget>, String> {
16507 let mut shape_count = 0usize;
16508 let mut gradient_stop_count = 0usize;
16509
16510 for batch in chunk.iter() {
16511 let SegmentBatchPlan::Shape { start, end, .. } = batch else {
16512 continue;
16513 };
16514 for (_, item) in &ordered_items[start..end] {
16515 let SegmentDrawItem::Shape(shape_index) = item else {
16516 return Err(format!(
16517 "shape batch contains non-shape draw item: {item:?}"
16518 ));
16519 };
16520 let shape = &shapes[*shape_index];
16521 shape_count = shape_count.saturating_add(1);
16522 gradient_stop_count =
16523 gradient_stop_count.saturating_add(gradient_stop_count_for_shape(shape, brushes));
16524 }
16525 }
16526
16527 if shape_count > batch_limits.max_shapes_per_batch
16528 || gradient_stop_count > batch_limits.max_gradient_stops
16529 {
16530 return Ok(None);
16531 }
16532
16533 Ok(Some(NativeSegmentFusionBudget {
16534 shape_count,
16535 gradient_stop_count,
16536 }))
16537}
16538
16539#[cfg(not(target_arch = "wasm32"))]
16540fn push_native_segment_fusion_partition(
16541 partitions: &mut Vec<NativeSegmentFusionPartition>,
16542 current: &mut SegmentDrawChunkPlan,
16543 current_budget: &mut NativeSegmentFusionBudget,
16544) {
16545 if current.is_empty() {
16546 return;
16547 }
16548
16549 partitions.push(NativeSegmentFusionPartition {
16550 chunk: std::mem::take(current),
16551 budget: *current_budget,
16552 });
16553 *current_budget = NativeSegmentFusionBudget {
16554 shape_count: 0,
16555 gradient_stop_count: 0,
16556 };
16557}
16558
16559#[cfg(not(target_arch = "wasm32"))]
16560fn native_segment_fusion_partitions(
16561 ordered_items: &[(usize, SegmentDrawItem)],
16562 shapes: &[DrawShape],
16563 brushes: &[Brush],
16564 chunk: &SegmentDrawChunkPlan,
16565 batch_limits: ShapeBatchLimits,
16566) -> Result<Option<Vec<NativeSegmentFusionPartition>>, String> {
16567 if let Some(budget) =
16568 native_segment_fusion_budget(ordered_items, shapes, brushes, chunk, batch_limits)?
16569 {
16570 return Ok(Some(vec![NativeSegmentFusionPartition {
16571 chunk: chunk.clone(),
16572 budget,
16573 }]));
16574 }
16575
16576 let mut partitions = Vec::new();
16577 let mut current = SegmentDrawChunkPlan::default();
16578 let mut current_budget = NativeSegmentFusionBudget {
16579 shape_count: 0,
16580 gradient_stop_count: 0,
16581 };
16582
16583 for batch in chunk.iter() {
16584 let SegmentBatchPlan::Shape {
16585 start,
16586 end,
16587 blend_mode,
16588 } = batch
16589 else {
16590 current.push(batch);
16591 continue;
16592 };
16593
16594 let mut run_start = start;
16595 for (item_cursor, (_, item)) in ordered_items.iter().enumerate().take(end).skip(start) {
16596 let SegmentDrawItem::Shape(shape_index) = *item else {
16597 return Err(format!(
16598 "shape batch contains non-shape draw item: {:?}",
16599 item
16600 ));
16601 };
16602 let gradient_stop_count = gradient_stop_count_for_shape(&shapes[shape_index], brushes);
16603 if gradient_stop_count > batch_limits.max_gradient_stops {
16604 return Ok(None);
16605 }
16606
16607 let fits_shape_count =
16608 current_budget.shape_count.saturating_add(1) <= batch_limits.max_shapes_per_batch;
16609 let fits_gradient_count = current_budget
16610 .gradient_stop_count
16611 .saturating_add(gradient_stop_count)
16612 <= batch_limits.max_gradient_stops;
16613 if !fits_shape_count || !fits_gradient_count {
16614 if run_start < item_cursor {
16615 current.push(SegmentBatchPlan::Shape {
16616 start: run_start,
16617 end: item_cursor,
16618 blend_mode,
16619 });
16620 }
16621 push_native_segment_fusion_partition(
16622 &mut partitions,
16623 &mut current,
16624 &mut current_budget,
16625 );
16626 run_start = item_cursor;
16627 }
16628
16629 current_budget.shape_count = current_budget.shape_count.saturating_add(1);
16630 current_budget.gradient_stop_count = current_budget
16631 .gradient_stop_count
16632 .saturating_add(gradient_stop_count);
16633 }
16634
16635 if run_start < end {
16636 current.push(SegmentBatchPlan::Shape {
16637 start: run_start,
16638 end,
16639 blend_mode,
16640 });
16641 }
16642 }
16643
16644 push_native_segment_fusion_partition(&mut partitions, &mut current, &mut current_budget);
16645 Ok(Some(partitions))
16646}
16647
16648fn segment_batch_plan_at_cursor(
16649 ordered_items: &[(usize, SegmentDrawItem)],
16650 shapes: &[DrawShape],
16651 images: &[ImageDraw],
16652 start: usize,
16653 batch_limits: ShapeBatchLimits,
16654) -> Option<(SegmentBatchPlan, usize)> {
16655 match ordered_items[start].1 {
16656 SegmentDrawItem::Shape(index) => {
16657 let blend_mode = supported_blend_mode(shapes[index].blend_mode);
16658 let mut end = start + 1;
16659 let shape_limit = (start + batch_limits.max_shapes_per_batch).min(ordered_items.len());
16660 while end < shape_limit {
16661 match ordered_items[end].1 {
16662 SegmentDrawItem::Shape(next_index)
16663 if supported_blend_mode(shapes[next_index].blend_mode) == blend_mode =>
16664 {
16665 end += 1;
16666 }
16667 _ => break,
16668 }
16669 }
16670 Some((
16671 SegmentBatchPlan::Shape {
16672 start,
16673 end,
16674 blend_mode,
16675 },
16676 end,
16677 ))
16678 }
16679 SegmentDrawItem::Image(index) => {
16680 let blend_mode = supported_blend_mode(images[index].blend_mode);
16681 let mut end = start + 1;
16682 while end < ordered_items.len() {
16683 match ordered_items[end].1 {
16684 SegmentDrawItem::Image(next_index)
16685 if supported_blend_mode(images[next_index].blend_mode) == blend_mode =>
16686 {
16687 end += 1;
16688 }
16689 _ => break,
16690 }
16691 }
16692 Some((
16693 SegmentBatchPlan::Image {
16694 start,
16695 end,
16696 blend_mode,
16697 },
16698 end,
16699 ))
16700 }
16701 SegmentDrawItem::Text(_) => {
16702 let mut end = start + 1;
16703 while end < ordered_items.len() {
16704 if matches!(ordered_items[end].1, SegmentDrawItem::Text(_)) {
16705 end += 1;
16706 } else {
16707 break;
16708 }
16709 }
16710 Some((SegmentBatchPlan::Text { start, end }, end))
16711 }
16712 SegmentDrawItem::Composite(_) => {
16713 let mut end = start + 1;
16714 while end < ordered_items.len() {
16715 if matches!(ordered_items[end].1, SegmentDrawItem::Composite(_)) {
16716 end += 1;
16717 } else {
16718 break;
16719 }
16720 }
16721 Some((SegmentBatchPlan::Composite { start, end }, end))
16722 }
16723 SegmentDrawItem::ShaderComposite(_) => {
16724 let mut end = start + 1;
16725 while end < ordered_items.len() {
16726 if matches!(ordered_items[end].1, SegmentDrawItem::ShaderComposite(_)) {
16727 end += 1;
16728 } else {
16729 break;
16730 }
16731 }
16732 Some((SegmentBatchPlan::ShaderComposite { start, end }, end))
16733 }
16734 SegmentDrawItem::Retained(_) => {
16735 let mut end = start + 1;
16736 while end < ordered_items.len() {
16737 if matches!(ordered_items[end].1, SegmentDrawItem::Retained(_)) {
16738 end += 1;
16739 } else {
16740 break;
16741 }
16742 }
16743 Some((SegmentBatchPlan::Retained { start, end }, end))
16744 }
16745 SegmentDrawItem::Shadow(_) => None,
16746 }
16747}
16748
16749#[allow(clippy::too_many_arguments)]
16750fn collect_non_effect_segment_items(
16751 shapes: &[DrawShape],
16752 _images: &[ImageDraw],
16753 _texts: &[TextDraw],
16754 _shadow_draws: &[ShadowDraw],
16755 draw_ops: &[DrawOp],
16756 z_start: usize,
16757 z_end: usize,
16758 effect_z_ranges: &[Range<usize>],
16759 width: u32,
16760 height: u32,
16761 root_scale: f32,
16762 scratch: &mut Vec<(usize, SegmentDrawItem)>,
16763) {
16764 scratch.clear();
16765 let viewport = ViewportUniformParams {
16766 width,
16767 height,
16768 offset: [0.0, 0.0],
16769 };
16770
16771 scratch.extend(draw_ops.iter().filter_map(|op| {
16772 if op.z_index < z_start
16773 || op.z_index >= z_end
16774 || is_in_effect_range(op.z_index, effect_z_ranges)
16775 {
16776 return None;
16777 }
16778 let item = match op.kind {
16779 DrawOpKind::Shape(index) => {
16780 let shape = shapes.get(index)?;
16781 if !shape_draw_is_visible_in_viewport(shape, viewport, root_scale) {
16782 return None;
16783 }
16784 SegmentDrawItem::Shape(index)
16785 }
16786 DrawOpKind::Image(index) => SegmentDrawItem::Image(index),
16787 DrawOpKind::Text(index) => SegmentDrawItem::Text(index),
16788 DrawOpKind::Shadow(index) => SegmentDrawItem::Shadow(index),
16789 DrawOpKind::Retained(index) => SegmentDrawItem::Retained(index),
16790 };
16791 Some((op.z_index, item))
16792 }));
16793}
16794
16795fn retain_renderable_shadow_items(
16796 ordered_items: &mut Vec<(usize, SegmentDrawItem)>,
16797 shadow_draws: &[ShadowDraw],
16798 width: u32,
16799 height: u32,
16800 root_scale: f32,
16801 max_texture_dim: u32,
16802) -> usize {
16803 let original_len = ordered_items.len();
16804 ordered_items.retain(|(_, item)| match item {
16805 SegmentDrawItem::Shadow(index) => shadow_draws.get(*index).is_some_and(|shadow| {
16806 shadow_draw_may_render(shadow, width, height, root_scale, max_texture_dim)
16807 }),
16808 _ => true,
16809 });
16810 original_len.saturating_sub(ordered_items.len())
16811}
16812
16813#[cfg(not(target_arch = "wasm32"))]
16814#[derive(Clone, Copy)]
16815struct SegmentDiagCounts {
16816 raw_shadow_items: usize,
16817 culled_shadow_items: usize,
16818 cached_shadow_composites: usize,
16819 composite_items: usize,
16820 shader_composite_items: usize,
16821}
16822
16823#[cfg(not(target_arch = "wasm32"))]
16824fn maybe_print_segment_diag(
16825 z_range: Range<usize>,
16826 ordered_items: &[(usize, SegmentDrawItem)],
16827 shapes: &[DrawShape],
16828 brushes: &[Brush],
16829 images: &[ImageDraw],
16830 counts: SegmentDiagCounts,
16831 batch_limits: ShapeBatchLimits,
16832) {
16833 if !cranpose_core::env_flag!("CRANPOSE_SEGMENT_DIAG") {
16834 return;
16835 }
16836 let line = SEGMENT_DIAG_LINES.fetch_add(1, Ordering::Relaxed);
16837 if line >= 64 {
16838 return;
16839 }
16840
16841 let remaining_shadow_items = ordered_items
16842 .iter()
16843 .filter(|(_, item)| matches!(item, SegmentDrawItem::Shadow(_)))
16844 .count();
16845 let commands: Vec<_> =
16846 SegmentCommandIter::new(ordered_items, shapes, images, batch_limits).collect();
16847 let draw_chunks = commands
16848 .iter()
16849 .filter(|command| matches!(command, SegmentRenderCommand::DrawChunk(_)))
16850 .count();
16851 let shadow_commands = commands
16852 .iter()
16853 .filter(|command| matches!(command, SegmentRenderCommand::Shadow(_)))
16854 .count();
16855 let mut native_partitions = 0usize;
16856 let mut native_unfused_chunks = 0usize;
16857 for command in &commands {
16858 let SegmentRenderCommand::DrawChunk(chunk) = command else {
16859 continue;
16860 };
16861 match native_segment_fusion_partitions(ordered_items, shapes, brushes, chunk, batch_limits)
16862 {
16863 Ok(Some(partitions)) => native_partitions += partitions.len(),
16864 Ok(None) | Err(_) => native_unfused_chunks += 1,
16865 }
16866 }
16867
16868 eprintln!(
16869 "[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={}",
16870 z_range.start,
16871 z_range.end,
16872 ordered_items.len(),
16873 counts.raw_shadow_items,
16874 counts.culled_shadow_items,
16875 counts.cached_shadow_composites,
16876 remaining_shadow_items,
16877 counts.composite_items,
16878 counts.shader_composite_items,
16879 draw_chunks,
16880 shadow_commands,
16881 native_partitions,
16882 native_unfused_chunks,
16883 );
16884}
16885
16886pub(crate) fn has_backdrop_layer_in_range(
16887 backdrop_layers: &[BackdropLayer],
16888 z_start: usize,
16889 z_end: usize,
16890) -> bool {
16891 backdrop_layers
16892 .iter()
16893 .any(|layer| layer.z_index >= z_start && layer.z_index < z_end)
16894}
16895
16896pub(crate) fn scissor_rect_for_rect(
16897 rect: Rect,
16898 root_scale: f32,
16899 width: u32,
16900 height: u32,
16901) -> Option<(u32, u32, u32, u32)> {
16902 let mut left = canonicalize_device_coordinate(rect.x * root_scale);
16903 let mut top = canonicalize_device_coordinate(rect.y * root_scale);
16904 let mut right = canonicalize_device_coordinate((rect.x + rect.width) * root_scale);
16905 let mut bottom = canonicalize_device_coordinate((rect.y + rect.height) * root_scale);
16906
16907 left = left.max(0.0).min(width as f32).floor();
16908 top = top.max(0.0).min(height as f32).floor();
16909 right = right.max(0.0).min(width as f32).ceil();
16910 bottom = bottom.max(0.0).min(height as f32).ceil();
16911
16912 if right <= left || bottom <= top {
16913 return None;
16914 }
16915
16916 Some((
16917 left as u32,
16918 top as u32,
16919 (right - left) as u32,
16920 (bottom - top) as u32,
16921 ))
16922}
16923
16924fn scissor_rect_for_layer(
16925 rect: Rect,
16926 clip: Option<Rect>,
16927 root_scale: f32,
16928 width: u32,
16929 height: u32,
16930) -> Option<(u32, u32, u32, u32)> {
16931 let clipped_rect = match clip {
16932 Some(clip_rect) => rect.intersect(clip_rect)?,
16933 None => rect,
16934 };
16935
16936 scissor_rect_for_rect(clipped_rect, root_scale, width, height)
16937}
16938
16939fn tint_for_image(
16940 color_filter: Option<ColorFilter>,
16941 alpha: f32,
16942) -> ([f32; 4], Option<ColorFilter>) {
16943 let alpha = alpha.clamp(0.0, 1.0);
16944 match color_filter {
16945 Some(filter) if filter.supports_gpu_vertex_modulation() => {
16946 let Some(tint) = filter.gpu_vertex_tint() else {
16947 return ([1.0, 1.0, 1.0, alpha], Some(filter));
16948 };
16949 (
16950 [
16951 tint[0].clamp(0.0, 1.0),
16952 tint[1].clamp(0.0, 1.0),
16953 tint[2].clamp(0.0, 1.0),
16954 (tint[3] * alpha).clamp(0.0, 1.0),
16955 ],
16956 None,
16957 )
16958 }
16959 Some(filter) => ([1.0, 1.0, 1.0, alpha], Some(filter)),
16960 None => ([1.0, 1.0, 1.0, alpha], None),
16961 }
16962}
16963
16964fn image_uv_rect(image: &ImageBitmap, src_rect: Option<Rect>) -> Option<ImageUvRect> {
16965 let Some(src) = src_rect else {
16966 return Some(ImageUvRect {
16967 min: [0.0, 0.0],
16968 max: [1.0, 1.0],
16969 sample_bounds: [0.0, 0.0, 1.0, 1.0],
16970 });
16971 };
16972
16973 let (u_min, u_max, u_bound_min, u_bound_max) =
16974 source_axis_uv(src.x, src.width, image.width() as f32)?;
16975 let (v_min, v_max, v_bound_min, v_bound_max) =
16976 source_axis_uv(src.y, src.height, image.height() as f32)?;
16977
16978 Some(ImageUvRect {
16979 min: [u_min, v_min],
16980 max: [u_max, v_max],
16981 sample_bounds: [u_bound_min, v_bound_min, u_bound_max, v_bound_max],
16982 })
16983}
16984
16985fn glyph_atlas_uv_rect(entry: GlyphAtlasEntry, atlas_size: u32) -> ImageUvRect {
16990 let atlas_width = atlas_size as f32;
16991 let atlas_height = atlas_size as f32;
16992 let min = [entry.x as f32 / atlas_width, entry.y as f32 / atlas_height];
16993 let max = [
16994 (entry.x + entry.width) as f32 / atlas_width,
16995 (entry.y + entry.height) as f32 / atlas_height,
16996 ];
16997 let center_min = [
16998 (entry.x as f32 + 0.5) / atlas_width,
16999 (entry.y as f32 + 0.5) / atlas_height,
17000 ];
17001 let center_max = [
17002 (entry.x as f32 + entry.width as f32 - 0.5).max(entry.x as f32 + 0.5) / atlas_width,
17003 (entry.y as f32 + entry.height as f32 - 0.5).max(entry.y as f32 + 0.5) / atlas_height,
17004 ];
17005 ImageUvRect {
17006 min,
17007 max,
17008 sample_bounds: [center_min[0], center_min[1], center_max[0], center_max[1]],
17009 }
17010}
17011
17012fn snap_nearest_image_to_device_pixels(image: &mut ImageDraw, root_scale: f32) {
17013 if image.sampling != ImageSampling::Nearest || !root_scale.is_finite() || root_scale <= 0.0 {
17014 return;
17015 }
17016
17017 let Some(rect) = axis_aligned_quad_rect(image.quad) else {
17018 return;
17019 };
17020
17021 let left_px = (rect.x * root_scale).round();
17022 let top_px = (rect.y * root_scale).round();
17023 let width_px = (rect.width * root_scale).round().max(1.0);
17024 let height_px = (rect.height * root_scale).round().max(1.0);
17025 let snapped = Rect {
17026 x: left_px / root_scale,
17027 y: top_px / root_scale,
17028 width: width_px / root_scale,
17029 height: height_px / root_scale,
17030 };
17031
17032 image.rect = snapped;
17033 image.local_rect = Rect {
17034 x: image.local_rect.x + snapped.x - rect.x,
17035 y: image.local_rect.y + snapped.y - rect.y,
17036 width: snapped.width,
17037 height: snapped.height,
17038 };
17039 image.quad = crate::rect_to_quad(snapped);
17040}
17041
17042fn nearest_image_device_quad(image: &ImageDraw, root_scale: f32) -> Option<[[f32; 2]; 4]> {
17043 if image.sampling != ImageSampling::Nearest || !root_scale.is_finite() || root_scale <= 0.0 {
17044 return None;
17045 }
17046
17047 let rect = axis_aligned_quad_rect(image.quad)?;
17048 let left_px = (rect.x * root_scale).round();
17049 let top_px = (rect.y * root_scale).round();
17050 let width_px = (rect.width * root_scale).round().max(1.0);
17051 let height_px = (rect.height * root_scale).round().max(1.0);
17052 let right_px = left_px + width_px;
17053 let bottom_px = top_px + height_px;
17054 Some([
17055 [left_px, top_px],
17056 [right_px, top_px],
17057 [left_px, bottom_px],
17058 [right_px, bottom_px],
17059 ])
17060}
17061
17062fn source_axis_uv(start: f32, extent: f32, image_extent: f32) -> Option<(f32, f32, f32, f32)> {
17063 if !start.is_finite()
17064 || !extent.is_finite()
17065 || !image_extent.is_finite()
17066 || extent == 0.0
17067 || image_extent <= 0.0
17068 {
17069 return None;
17070 }
17071
17072 let end = start + extent;
17073 let edge_min = start.min(end).clamp(0.0, image_extent);
17074 let edge_max = start.max(end).clamp(0.0, image_extent);
17075 if edge_max <= edge_min {
17076 return None;
17077 }
17078
17079 let center_min = edge_min + 0.5;
17080 let center_max = edge_max - 0.5;
17081 let (bound_min, bound_max) = if center_min <= center_max {
17082 (center_min, center_max)
17083 } else {
17084 let center = (edge_min + edge_max) * 0.5;
17085 (center, center)
17086 };
17087
17088 Some((
17089 edge_min / image_extent,
17090 edge_max / image_extent,
17091 bound_min / image_extent,
17092 bound_max / image_extent,
17093 ))
17094}
17095
17096fn apply_filter_to_bitmap(image: &ImageBitmap, filter: ColorFilter) -> Result<ImageBitmap, String> {
17097 let mut filtered = Vec::with_capacity(image.pixels().len());
17098 for pixel in image.pixels().as_chunks::<4>().0 {
17099 let rgba = [
17100 pixel[0] as f32 / 255.0,
17101 pixel[1] as f32 / 255.0,
17102 pixel[2] as f32 / 255.0,
17103 pixel[3] as f32 / 255.0,
17104 ];
17105 let out = filter.apply_rgba(rgba);
17106 filtered.push((out[0].clamp(0.0, 1.0) * 255.0).round() as u8);
17107 filtered.push((out[1].clamp(0.0, 1.0) * 255.0).round() as u8);
17108 filtered.push((out[2].clamp(0.0, 1.0) * 255.0).round() as u8);
17109 filtered.push((out[3].clamp(0.0, 1.0) * 255.0).round() as u8);
17110 }
17111 ImageBitmap::from_rgba8(image.width(), image.height(), filtered)
17112 .map_err(|error| format!("failed to build filtered bitmap: {error}"))
17113}
17114
17115fn scissor_rect_for_image(
17116 image: &ImageDraw,
17117 root_scale: f32,
17118 width: u32,
17119 height: u32,
17120) -> Option<(u32, u32, u32, u32)> {
17121 scissor_rect_for_layer(image.rect, image.clip, root_scale, width, height)
17122}
17123
17124fn inner_shadow_composite_mask(
17125 shadow: &ShadowDraw,
17126 root_scale: f32,
17127) -> Option<RoundedCompositeMask> {
17128 if !shadow
17129 .shapes
17130 .iter()
17131 .any(|(_, mode)| *mode == BlendMode::DstOut)
17132 {
17133 return None;
17134 }
17135 let (fill, _) = shadow.shapes.first()?;
17136 let rect = fill.local_rect;
17137 if rect.width <= 0.0 || rect.height <= 0.0 {
17138 return None;
17139 }
17140
17141 let radii = fill.shape.map_or([0.0; 4], |rounded| {
17142 let resolved = rounded.resolve(rect.width, rect.height);
17143 [
17144 resolved.top_left * root_scale,
17145 resolved.top_right * root_scale,
17146 resolved.bottom_left * root_scale,
17147 resolved.bottom_right * root_scale,
17148 ]
17149 });
17150
17151 Some(RoundedCompositeMask {
17152 rect: [
17153 rect.x * root_scale,
17154 rect.y * root_scale,
17155 rect.width * root_scale,
17156 rect.height * root_scale,
17157 ],
17158 radii,
17159 })
17160}
17161
17162#[cfg(test)]
17163mod shape_batch_limits_tests {
17164 use super::*;
17165
17166 fn generous_limits() -> wgpu::Limits {
17169 wgpu::Limits {
17170 max_storage_buffers_per_shader_stage: 8,
17171 max_storage_buffer_binding_size: 128 << 20,
17172 max_uniform_buffer_binding_size: 16 << 10,
17173 ..wgpu::Limits::default()
17174 }
17175 }
17176
17177 #[test]
17178 fn a_device_without_vertex_storage_takes_the_uniform_path() {
17179 let limits = ShapeBatchLimits::select(&generous_limits(), wgpu::DownlevelFlags::empty());
17185 assert!(
17186 !limits.storage,
17187 "no VERTEX_STORAGE must mean uniform mode, whatever the limit says"
17188 );
17189 }
17190
17191 #[test]
17192 fn a_device_with_vertex_storage_still_takes_the_storage_path() {
17193 let limits = ShapeBatchLimits::select(&generous_limits(), wgpu::DownlevelFlags::all());
17194 assert!(
17195 limits.storage,
17196 "the flag must not cost storage mode on a device that has it"
17197 );
17198 }
17199
17200 #[test]
17201 fn the_limit_still_gates_storage_when_the_flag_is_present() {
17202 let mut limits = generous_limits();
17203 limits.max_storage_buffers_per_shader_stage = 1;
17204 let limits = ShapeBatchLimits::select(&limits, wgpu::DownlevelFlags::all());
17205 assert!(!limits.storage, "two bindings are needed, not one");
17206 }
17207}
17208
17209#[cfg(test)]
17210mod tests {
17211 use super::*;
17212 use crate::normalized_scene::visible_draw_rect;
17213 use cranpose_foundation::lazy::{remember_lazy_list_state, LazyListScope, LazyListState};
17214 use cranpose_render_common::graph::{DrawPrimitiveNode, IsolationReasons, TextPrimitiveNode};
17215 use cranpose_render_common::raster_cache::LayerRasterCacheHashes;
17216 use cranpose_render_common::scene_builder::build_graph_from_applier;
17217 use cranpose_ui::text::{
17218 AnnotatedString, BaselineShift, RangeStyle, Shadow, SpanStyle, TextDecoration,
17219 TextDrawStyle, TextGeometricTransform, TextMotion, TextUnit,
17220 };
17221 use cranpose_ui::{
17222 LayoutEngine, LazyColumn, LazyColumnSpec, Modifier, Size, Text, TextLayoutOptions,
17223 TextStyle,
17224 };
17225 use cranpose_ui_graphics::{
17226 Brush, Color, CornerRadii, DrawPrimitive, Rect, RenderEffect, RoundedCornerShape,
17227 RuntimeShader,
17228 };
17229
17230 fn chunk(batches: &[SegmentBatchPlan]) -> SegmentDrawChunkPlan {
17231 let mut chunk = SegmentDrawChunkPlan::default();
17232 for batch in batches {
17233 chunk.push(*batch);
17234 }
17235 chunk
17236 }
17237
17238 fn with_test_app_context<R>(block: impl FnOnce() -> R) -> R {
17239 let app_context = cranpose_ui::AppContext::new();
17240 app_context.enter(block)
17241 }
17242
17243 fn assert_snap_anchor_close(actual: Option<SnapAnchor>, expected_origin: Point, message: &str) {
17244 let Some(actual) = actual else {
17245 panic!("{message}: missing snap anchor");
17246 };
17247 let expected = SnapAnchor::rigid(expected_origin);
17248 assert_eq!(
17249 actual.device_pixel_step, expected.device_pixel_step,
17250 "{message}: device pixel step changed"
17251 );
17252 assert!(
17253 (actual.origin.x - expected.origin.x).abs() <= 1e-4
17254 && (actual.origin.y - expected.origin.y).abs() <= 1e-4,
17255 "{message}: expected origin {:?}, got {:?}",
17256 expected.origin,
17257 actual.origin
17258 );
17259 }
17260
17261 fn effect_layer(z_start: usize, z_end: usize) -> EffectLayer {
17262 EffectLayer {
17263 rect: Rect {
17264 x: 0.0,
17265 y: 0.0,
17266 width: 10.0,
17267 height: 10.0,
17268 },
17269 clip: None,
17270 snap_anchor: None,
17271 effect: Some(RenderEffect::blur(4.0)),
17272 blend_mode: BlendMode::SrcOver,
17273 composite_alpha: 1.0,
17274 z_start,
17275 z_end,
17276 requirements: SurfaceRequirementSet::default().with(SurfaceRequirement::RenderEffect),
17277 }
17278 }
17279
17280 #[test]
17281 fn direct_shader_composite_accepts_box4_when_viewport_preserves_source_pixels() {
17282 assert_eq!(
17283 direct_shader_composite_viewport(
17284 1.0,
17285 BlendMode::SrcOver,
17286 Some((12.0, 18.0, 64.0, 32.0)),
17287 CompositeSampleMode::Box4,
17288 (64, 32),
17289 ),
17290 Some((12.0, 18.0, 64.0, 32.0))
17291 );
17292 }
17293
17294 #[test]
17295 fn direct_shader_composite_rejects_box4_when_viewport_resamples_source() {
17296 assert_eq!(
17297 direct_shader_composite_viewport(
17298 1.0,
17299 BlendMode::SrcOver,
17300 Some((12.0, 18.0, 64.5, 32.0)),
17301 CompositeSampleMode::Box4,
17302 (64, 32),
17303 ),
17304 None
17305 );
17306 assert_eq!(
17307 direct_shader_composite_viewport(
17308 1.0,
17309 BlendMode::SrcOver,
17310 Some((12.25, 18.0, 64.0, 32.0)),
17311 CompositeSampleMode::Box4,
17312 (64, 32),
17313 ),
17314 None
17315 );
17316 }
17317
17318 fn test_text_draw(rect: Rect, text_motion: TextMotion) -> TextDraw {
17319 let mut text_style = TextStyle::default();
17320 text_style.paragraph_style.text_motion = Some(text_motion);
17321 TextDraw {
17322 node_id: 42,
17323 rect,
17324 snap_anchor: None,
17325 translated_content_context: false,
17326 text: Arc::new(AnnotatedString::new("stable markdown row".to_string()).render_string()),
17327 color: Color::WHITE,
17328 text_style,
17329 font_size: 14.0,
17330 scale: 1.0,
17331 layout_options: TextLayoutOptions::default(),
17332 z_index: 0,
17333 clip: None,
17334 }
17335 }
17336
17337 #[test]
17338 fn static_text_image_cache_key_ignores_absolute_scroll_position() {
17339 let base = test_text_draw(
17340 Rect {
17341 x: 12.25,
17342 y: 40.75,
17343 width: 220.0,
17344 height: 24.0,
17345 },
17346 TextMotion::Static,
17347 );
17348 let scrolled = test_text_draw(
17349 Rect {
17350 x: 12.75,
17351 y: -318.5,
17352 width: 220.0,
17353 height: 24.0,
17354 },
17355 TextMotion::Static,
17356 );
17357
17358 let base_key = GpuRenderer::text_image_cache_key(&base, base.rect, 1.0, true);
17359 let scrolled_key = GpuRenderer::text_image_cache_key(&scrolled, scrolled.rect, 1.0, true);
17360
17361 assert_eq!(
17362 base_key, scrolled_key,
17363 "scrolling static text must reuse the same raster cache entry"
17364 );
17365 }
17366
17367 #[test]
17368 fn static_text_glyph_run_cache_key_ignores_absolute_scroll_position() {
17369 let base = test_text_draw(
17370 Rect {
17371 x: 12.25,
17372 y: 40.75,
17373 width: 220.0,
17374 height: 24.0,
17375 },
17376 TextMotion::Static,
17377 );
17378 let scrolled = test_text_draw(
17379 Rect {
17380 x: 12.75,
17381 y: -318.5,
17382 width: 220.0,
17383 height: 24.0,
17384 },
17385 TextMotion::Static,
17386 );
17387
17388 let base_key = GpuRenderer::text_glyph_run_cache_key(&base, base.rect, 1.0, true);
17389 let scrolled_key =
17390 GpuRenderer::text_glyph_run_cache_key(&scrolled, scrolled.rect, 1.0, true);
17391
17392 assert_eq!(
17393 base_key, scrolled_key,
17394 "scrolling static text must reuse the same retained glyph run"
17395 );
17396 }
17397
17398 #[test]
17399 fn static_multiline_text_glyph_source_keeps_full_text_when_image_source_slices() {
17400 let rect = Rect {
17401 x: 8.0,
17402 y: 100.0,
17403 width: 240.0,
17404 height: 1_000.0,
17405 };
17406 let mut draw = test_text_draw(rect, TextMotion::Static);
17407 let lines = (0..100)
17408 .map(|line| format!("line-{line:03}"))
17409 .collect::<Vec<_>>()
17410 .join("\n");
17411 draw.text = Arc::new(AnnotatedString::from(lines).render_string());
17412
17413 let raster_rect = Rect {
17414 x: 16.0,
17415 y: 200.0,
17416 width: 480.0,
17417 height: 2_000.0,
17418 };
17419 let clipped = clipped_text_raster_source(
17420 &draw,
17421 rect,
17422 raster_rect,
17423 Some(Rect {
17424 x: 0.0,
17425 y: 610.0,
17426 width: 800.0,
17427 height: 40.0,
17428 }),
17429 2.0,
17430 true,
17431 );
17432 let glyph = text_glyph_raster_source(&draw, raster_rect);
17433
17434 assert!(
17435 matches!(clipped.draw, Cow::Owned(_)),
17436 "the image source should still slice large clipped multiline text"
17437 );
17438 assert!(
17439 matches!(glyph.draw, Cow::Borrowed(_)),
17440 "the glyph source must keep a stable full-text run key while scrolling"
17441 );
17442
17443 let clipped_key = GpuRenderer::text_glyph_run_cache_key(
17444 clipped.draw.as_ref(),
17445 clipped.raster_rect,
17446 2.0,
17447 true,
17448 );
17449 let glyph_key = GpuRenderer::text_glyph_run_cache_key(
17450 glyph.draw.as_ref(),
17451 glyph.raster_rect,
17452 2.0,
17453 true,
17454 );
17455
17456 assert_ne!(
17457 clipped_key, glyph_key,
17458 "image slicing must not force glyph rendering onto per-scroll line-window cache keys"
17459 );
17460 }
17461
17462 #[cfg(not(target_arch = "wasm32"))]
17463 #[test]
17464 fn retained_glyph_viewport_offsets_relative_vertices_by_source_origin() {
17465 let viewport = ViewportUniformParams {
17466 width: 800,
17467 height: 600,
17468 offset: [10.0, 20.0],
17469 };
17470 let source = Rect {
17471 x: 40.0,
17472 y: 90.0,
17473 width: 120.0,
17474 height: 48.0,
17475 };
17476
17477 let retained = GpuRenderer::retained_glyph_viewport(viewport, source);
17478
17479 assert_eq!(retained.width, viewport.width);
17480 assert_eq!(retained.height, viewport.height);
17481 assert_eq!(retained.offset, [-30.0, -70.0]);
17482 }
17483
17484 #[cfg(not(target_arch = "wasm32"))]
17485 #[test]
17486 fn tiny_text_glyph_runs_stay_in_shared_uploads() {
17487 assert!(
17488 !should_use_retained_text_glyph_run(8, None),
17489 "tiny labels must stay in the shared fused batch"
17490 );
17491 }
17492
17493 #[cfg(not(target_arch = "wasm32"))]
17494 #[test]
17495 fn line_sized_text_glyph_runs_stay_in_shared_uploads() {
17496 assert!(
17497 !should_use_retained_text_glyph_run(64, None),
17498 "Markdown scroll frames contain many line-sized text runs; retaining each one creates per-run buffer binds instead of one shared glyph batch"
17499 );
17500 }
17501
17502 #[cfg(not(target_arch = "wasm32"))]
17503 #[test]
17504 fn large_clipped_text_glyph_runs_stay_in_shared_uploads() {
17505 assert!(
17506 !should_use_retained_text_glyph_run(
17507 MIN_RETAINED_TEXT_GLYPH_QUADS.saturating_mul(2),
17508 Some(Rect {
17509 x: 0.0,
17510 y: 0.0,
17511 width: 200.0,
17512 height: 100.0,
17513 }),
17514 ),
17515 "clipped lazy-list text must not draw a full retained run outside the viewport"
17516 );
17517 }
17518
17519 #[test]
17520 fn normal_text_glyph_draw_skips_offscreen_prewarm_candidates() {
17521 assert_eq!(
17522 text_glyph_draw_action(false, true, false),
17523 TextGlyphDrawAction::Skip,
17524 "normal draw traversal must not prepare offscreen text"
17525 );
17526 }
17527
17528 #[test]
17529 fn bounded_text_glyph_prewarm_admits_offscreen_candidates() {
17530 assert_eq!(
17531 text_glyph_draw_action(false, true, true),
17532 TextGlyphDrawAction::PrewarmOffscreen,
17533 "only the bounded prewarm path may prepare offscreen text"
17534 );
17535 }
17536
17537 #[test]
17538 fn visible_text_glyph_draws_are_always_admitted() {
17539 assert_eq!(
17540 text_glyph_draw_action(true, false, false),
17541 TextGlyphDrawAction::DrawVisible
17542 );
17543 assert_eq!(
17544 text_glyph_draw_action(true, true, true),
17545 TextGlyphDrawAction::DrawVisible
17546 );
17547 }
17548
17549 #[cfg(not(target_arch = "wasm32"))]
17550 #[test]
17551 fn offscreen_text_prewarm_skips_large_uncached_text_runs() {
17552 assert!(
17553 !offscreen_text_glyph_prewarm_work_is_bounded(
17554 None,
17555 MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS + 1,
17556 ),
17557 "offscreen prewarm must not collect large uncached text runs in an input frame"
17558 );
17559 }
17560
17561 #[cfg(not(target_arch = "wasm32"))]
17562 #[test]
17563 fn offscreen_text_prewarm_admits_small_uncached_text_runs() {
17564 assert!(
17565 offscreen_text_glyph_prewarm_work_is_bounded(
17566 None,
17567 MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS,
17568 ),
17569 "small labels can be warmed without risking a frame-budget spike"
17570 );
17571 }
17572
17573 #[cfg(not(target_arch = "wasm32"))]
17574 #[test]
17575 fn offscreen_text_prewarm_skips_large_cached_runs_without_quads() {
17576 assert!(
17577 !offscreen_text_glyph_prewarm_work_is_bounded(
17578 Some(MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CACHED_GLYPHS + 1),
17579 0,
17580 ),
17581 "cached glyph placements can still be too large to prepare during input frames"
17582 );
17583 }
17584
17585 #[cfg(not(target_arch = "wasm32"))]
17586 #[test]
17587 fn offscreen_text_prewarm_stops_after_candidate_budget() {
17588 assert!(
17589 offscreen_text_glyph_prewarm_budget_exhausted(
17590 Instant::now(),
17591 MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CANDIDATES,
17592 ),
17593 "prewarm must be bounded by candidate count even when each candidate is cheap"
17594 );
17595 }
17596
17597 #[test]
17598 fn clipped_cached_glyph_quads_are_filtered_to_viewport() {
17599 fn quad(y: i32) -> CachedTextGlyphQuad {
17600 CachedTextGlyphQuad {
17601 x: 8,
17602 y,
17603 width: 20,
17604 height: 10,
17605 color: (1.0, 1.0, 1.0, 1.0),
17606 uv: ImageUvRect {
17607 min: [0.0, 0.0],
17608 max: [1.0, 1.0],
17609 sample_bounds: [0.0, 0.0, 1.0, 1.0],
17610 },
17611 }
17612 }
17613
17614 let source = Rect {
17615 x: 0.0,
17616 y: 0.0,
17617 width: 320.0,
17618 height: 400.0,
17619 };
17620 let clip = Some(Rect {
17621 x: 0.0,
17622 y: 0.0,
17623 width: 320.0,
17624 height: 80.0,
17625 });
17626 let viewport = ViewportUniformParams {
17627 width: 320,
17628 height: 80,
17629 offset: [0.0, 0.0],
17630 };
17631
17632 assert!(cached_text_glyph_quad_is_visible_in_viewport(
17633 source,
17634 &quad(40),
17635 clip,
17636 viewport,
17637 1.0,
17638 ));
17639 assert!(
17640 !cached_text_glyph_quad_is_visible_in_viewport(source, &quad(140), clip, viewport, 1.0,),
17641 "glyphs outside the effective clip should not enter the frame command stream"
17642 );
17643 }
17644
17645 #[test]
17646 fn small_scene_range_cache_miss_observes_first_render() {
17647 let key = LayerRasterCacheKey::scene_range(
17648 0xCACE,
17649 Rect {
17650 x: 0.0,
17651 y: 0.0,
17652 width: 120.0,
17653 height: 80.0,
17654 },
17655 (120, 80),
17656 ScaleBucket::from_scale(1.0),
17657 );
17658
17659 assert!(
17660 !first_cache_miss_admission(&key),
17661 "a small scene-range miss should render directly first instead of materializing a tiny one-frame retained target"
17662 );
17663 assert!(
17664 repeated_cache_miss_admission(&key),
17665 "a repeated small scene-range miss is stable enough to materialize into the retained cache"
17666 );
17667 }
17668
17669 #[test]
17670 fn large_scene_range_cache_miss_requires_repeated_stable_key() {
17671 let key = LayerRasterCacheKey::scene_range(
17672 0xCACE,
17673 Rect {
17674 x: 0.0,
17675 y: 0.0,
17676 width: 1200.0,
17677 height: 900.0,
17678 },
17679 (1200, 900),
17680 ScaleBucket::from_scale(1.0),
17681 );
17682
17683 assert!(
17684 !first_cache_miss_admission(&key),
17685 "a large first scene-range miss should render directly instead of materializing a multi-MB one-frame cache entry"
17686 );
17687 assert!(
17688 repeated_cache_miss_admission(&key),
17689 "a repeated scene-range miss is stable enough to materialize into the retained cache"
17690 );
17691 }
17692
17693 #[test]
17694 fn renderer_warmup_frame_is_requested_for_cache_miss_stats_only() {
17695 let stats = gpu_stats::FrameStats::default();
17696 let mut snapshot = stats.snapshot();
17697 assert!(
17698 !frame_stats_need_warmup_frame(&snapshot),
17699 "a clean frame must not keep a static scene redrawing"
17700 );
17701
17702 snapshot.layer_cache_misses = 1;
17703 assert!(frame_stats_need_warmup_frame(&snapshot));
17704 snapshot.layer_cache_misses = 0;
17705
17706 snapshot.shadow_shape_cache_misses = 1;
17707 assert!(frame_stats_need_warmup_frame(&snapshot));
17708 snapshot.shadow_shape_cache_misses = 0;
17709
17710 snapshot.text_image_cache_misses = 1;
17711 assert!(frame_stats_need_warmup_frame(&snapshot));
17712 snapshot.text_image_cache_misses = 0;
17713
17714 snapshot.text_glyph_atlas_misses = 1;
17715 assert!(frame_stats_need_warmup_frame(&snapshot));
17716 }
17717
17718 #[test]
17719 fn renderer_warmup_budget_is_consumed_by_a_repeated_cache_miss() {
17720 let stats = gpu_stats::FrameStats::default();
17721 let mut snapshot = stats.snapshot();
17722 snapshot.layer_cache_misses = 1;
17723 let mut pending_frames = 0;
17724
17725 update_frame_warmup_budget(&mut pending_frames, &snapshot);
17726 assert_eq!(pending_frames, CACHE_MISS_WARMUP_FRAMES);
17727
17728 update_frame_warmup_budget(&mut pending_frames, &snapshot);
17729 assert_eq!(
17730 pending_frames, 0,
17731 "a cache miss during the warmup frame must not replenish its budget"
17732 );
17733 }
17734
17735 #[test]
17736 fn non_scene_layer_surface_cache_miss_admits_first_render() {
17737 let key = LayerRasterCacheKey::new(
17738 Some(77),
17739 0xC0FFEE,
17740 0,
17741 Rect {
17742 x: 0.0,
17743 y: 0.0,
17744 width: 120.0,
17745 height: 80.0,
17746 },
17747 (120, 80),
17748 ScaleBucket::from_scale(1.0),
17749 );
17750
17751 assert!(
17752 first_cache_miss_admission(&key),
17753 "ordinary retained layer surfaces should still cache on first miss"
17754 );
17755 }
17756
17757 #[test]
17758 fn text_image_cache_key_is_content_addressed_not_node_addressed() {
17759 let first = test_text_draw(
17760 Rect {
17761 x: 12.25,
17762 y: 40.75,
17763 width: 220.0,
17764 height: 24.0,
17765 },
17766 TextMotion::Static,
17767 );
17768 let mut second = first.clone();
17769 second.node_id = first.node_id + 1;
17770
17771 let first_key = GpuRenderer::text_image_cache_key(&first, first.rect, 1.0, true);
17772 let second_key = GpuRenderer::text_image_cache_key(&second, second.rect, 1.0, true);
17773
17774 assert_eq!(
17775 first_key, second_key,
17776 "text raster cache keys must be based on rendered pixels, not node identity"
17777 );
17778 }
17779
17780 #[test]
17781 fn animated_text_image_cache_key_keeps_fractional_phase_only() {
17782 let base = test_text_draw(
17783 Rect {
17784 x: 12.25,
17785 y: 40.75,
17786 width: 220.0,
17787 height: 24.0,
17788 },
17789 TextMotion::Animated,
17790 );
17791 let integer_translated = test_text_draw(
17792 Rect {
17793 x: 44.25,
17794 y: 88.75,
17795 width: 220.0,
17796 height: 24.0,
17797 },
17798 TextMotion::Animated,
17799 );
17800 let phase_shifted = test_text_draw(
17801 Rect {
17802 x: 44.5,
17803 y: 88.75,
17804 width: 220.0,
17805 height: 24.0,
17806 },
17807 TextMotion::Animated,
17808 );
17809
17810 let base_key = GpuRenderer::text_image_cache_key(&base, base.rect, 1.0, false);
17811 let translated_key = GpuRenderer::text_image_cache_key(
17812 &integer_translated,
17813 integer_translated.rect,
17814 1.0,
17815 false,
17816 );
17817 let phase_shifted_key =
17818 GpuRenderer::text_image_cache_key(&phase_shifted, phase_shifted.rect, 1.0, false);
17819
17820 assert_eq!(
17821 base_key, translated_key,
17822 "integer translation should not invalidate animated text raster cache entries"
17823 );
17824 assert_ne!(
17825 base_key, phase_shifted_key,
17826 "fractional phase affects animated text rasterization and must stay in the key"
17827 );
17828 }
17829
17830 #[test]
17831 fn animated_translated_text_raster_geometry_applies_snap_anchor() {
17832 let mut base = test_text_draw(
17833 Rect {
17834 x: 14.25,
17835 y: 16.50,
17836 width: 220.0,
17837 height: 24.0,
17838 },
17839 TextMotion::Animated,
17840 );
17841 base.snap_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.50)));
17842
17843 let mut scrolled = test_text_draw(
17844 Rect {
17845 x: 14.25,
17846 y: 15.80,
17847 width: 220.0,
17848 height: 24.0,
17849 },
17850 TextMotion::Animated,
17851 );
17852 scrolled.snap_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 15.80)));
17853
17854 let (base_logical, base_raster, _, _, base_static) =
17855 text_raster_geometry_for_draw(&base, 1.0).expect("base text geometry");
17856 let (scrolled_logical, scrolled_raster, _, _, scrolled_static) =
17857 text_raster_geometry_for_draw(&scrolled, 1.0).expect("scrolled text geometry");
17858
17859 assert!(!base_static);
17860 assert!(!scrolled_static);
17861 assert!((base_logical.x - 14.0).abs() < f32::EPSILON);
17862 assert!((base_logical.y - 17.0).abs() < f32::EPSILON);
17863 assert!((scrolled_logical.x - 14.0).abs() < f32::EPSILON);
17864 assert!((scrolled_logical.y - 16.0).abs() < f32::EPSILON);
17865 assert_eq!(base_raster.x.fract(), 0.0);
17866 assert_eq!(base_raster.y.fract(), 0.0);
17867 assert_eq!(scrolled_raster.x.fract(), 0.0);
17868 assert_eq!(scrolled_raster.y.fract(), 0.0);
17869
17870 let base_key = GpuRenderer::text_image_cache_key(&base, base_raster, 1.0, false);
17871 let scrolled_key =
17872 GpuRenderer::text_image_cache_key(&scrolled, scrolled_raster, 1.0, false);
17873 assert_eq!(
17874 base_key, scrolled_key,
17875 "translated animated text should keep a stable raster phase while scrolling"
17876 );
17877 }
17878
17879 #[test]
17880 fn translated_static_text_moves_one_device_pixel_at_half_pixel_phase() {
17881 let root_scale = 1.25;
17882 let mut base = test_text_draw(
17883 Rect {
17884 x: 14.0,
17885 y: 276.0,
17886 width: 220.0,
17887 height: 24.0,
17888 },
17889 TextMotion::Static,
17890 );
17891 base.snap_anchor = Some(SnapAnchor::rigid(Point::new(0.0, 127.600_006)));
17892
17893 let mut scrolled = test_text_draw(
17894 Rect {
17895 x: 14.0,
17896 y: 275.2,
17897 width: 220.0,
17898 height: 24.0,
17899 },
17900 TextMotion::Static,
17901 );
17902 scrolled.snap_anchor = Some(SnapAnchor::rigid(Point::new(0.0, 126.799_99)));
17903
17904 let (_, base_raster, _, _, _) =
17905 text_raster_geometry_for_draw(&base, root_scale).expect("base text geometry");
17906 let (_, scrolled_raster, _, _, _) =
17907 text_raster_geometry_for_draw(&scrolled, root_scale).expect("scrolled text geometry");
17908
17909 assert_eq!(
17910 base_raster.y - scrolled_raster.y,
17911 1.0,
17912 "one physical pixel of rigid scrolling must move static text by one raster pixel"
17913 );
17914 }
17915
17916 #[test]
17917 fn translated_text_snap_does_not_move_its_fixed_ancestor_clip() {
17918 let root_scale = 1.25;
17919 let fixed_clip = Rect {
17920 x: 8.0,
17921 y: 20.0,
17922 width: 300.0,
17923 height: 680.0,
17924 };
17925 let mut draw = test_text_draw(
17926 Rect {
17927 x: 14.0,
17928 y: 276.0,
17929 width: 220.0,
17930 height: 24.0,
17931 },
17932 TextMotion::Static,
17933 );
17934 draw.snap_anchor = Some(SnapAnchor::rigid(Point::new(0.0, 127.4)));
17935 draw.clip = Some(fixed_clip);
17936
17937 let (_, _, clip, _, _) =
17938 text_raster_geometry_for_draw(&draw, root_scale).expect("clipped text geometry");
17939
17940 assert_eq!(
17941 clip,
17942 Some(fixed_clip),
17943 "content pixel snapping must not translate a fixed ancestor clip"
17944 );
17945 }
17946
17947 #[test]
17948 fn clipped_static_multiline_text_raster_source_limits_visible_line_window() {
17949 let rect = Rect {
17950 x: 8.0,
17951 y: 100.0,
17952 width: 240.0,
17953 height: 1_000.0,
17954 };
17955 let mut draw = test_text_draw(rect, TextMotion::Static);
17956 let lines = (0..100)
17957 .map(|line| format!("line-{line:03}"))
17958 .collect::<Vec<_>>()
17959 .join("\n");
17960 draw.text = Arc::new(AnnotatedString::from(lines).render_string());
17961
17962 let raster_rect = Rect {
17963 x: 16.0,
17964 y: 200.0,
17965 width: 480.0,
17966 height: 2_000.0,
17967 };
17968 let source = clipped_text_raster_source(
17969 &draw,
17970 rect,
17971 raster_rect,
17972 Some(Rect {
17973 x: 0.0,
17974 y: 610.0,
17975 width: 800.0,
17976 height: 40.0,
17977 }),
17978 2.0,
17979 true,
17980 );
17981
17982 let Cow::Owned(sliced_draw) = source.draw else {
17983 panic!("clipped static multiline text should rasterize only the visible line window");
17984 };
17985 let sliced_text = sliced_draw.text.text.as_str();
17986 assert!(sliced_text.contains("line-050"));
17987 assert!(sliced_text.contains("line-055"));
17988 assert!(!sliced_text.contains("line-000"));
17989 assert!(!sliced_text.contains("line-099"));
17990 assert_eq!(source.raster_rect.x, raster_rect.x);
17991 assert!(source.raster_rect.y > raster_rect.y);
17992 assert!(source.raster_rect.height < raster_rect.height);
17993 }
17994
17995 #[test]
17996 fn clipped_static_multiline_text_raster_source_slices_short_multiline_text() {
17997 let rect = Rect {
17998 x: 8.0,
17999 y: 100.0,
18000 width: 240.0,
18001 height: 320.0,
18002 };
18003 let mut draw = test_text_draw(rect, TextMotion::Static);
18004 let lines = (0..24)
18005 .map(|line| format!("code-line-{line:02}"))
18006 .collect::<Vec<_>>()
18007 .join("\n");
18008 draw.text = Arc::new(AnnotatedString::from(lines).render_string());
18009
18010 let raster_rect = Rect {
18011 x: 16.0,
18012 y: 200.0,
18013 width: 480.0,
18014 height: 640.0,
18015 };
18016 let source = clipped_text_raster_source(
18017 &draw,
18018 rect,
18019 raster_rect,
18020 Some(Rect {
18021 x: 0.0,
18022 y: 190.0,
18023 width: 800.0,
18024 height: 120.0,
18025 }),
18026 2.0,
18027 true,
18028 );
18029
18030 let Cow::Owned(sliced_draw) = source.draw else {
18031 panic!("clipped multiline text should rasterize only the visible line window");
18032 };
18033 assert!(sliced_draw.text.text.as_str().contains("code-line-06"));
18034 assert!(!sliced_draw.text.text.as_str().contains("code-line-00"));
18035 assert!(!sliced_draw.text.text.as_str().contains("code-line-23"));
18036 assert_eq!(source.raster_rect.x, raster_rect.x);
18037 assert!(source.raster_rect.y > raster_rect.y);
18038 assert!(source.raster_rect.height < raster_rect.height);
18039 }
18040
18041 #[test]
18042 fn text_line_index_cache_reuses_retained_index_for_same_text_instance() {
18043 let mut cache = TextLineIndexCache::new(4);
18044 let text = Arc::new(AnnotatedString::from("a\nb\nc").render_string());
18045
18046 let first = cache.line_starts(&text);
18047 let second = cache.line_starts(&text);
18048
18049 assert_eq!(first.as_ref(), &[0, 2, 4]);
18050 assert!(
18051 Rc::ptr_eq(&first, &second),
18052 "retained text should not rebuild its line index on every clipped frame"
18053 );
18054 }
18055
18056 #[test]
18057 fn text_line_index_cache_is_retained_text_instance_local() {
18058 let mut cache = TextLineIndexCache::new(4);
18059 let first_text = Arc::new(AnnotatedString::from("a\nb\nc").render_string());
18060 let second_text = Arc::new(AnnotatedString::from("a\nb\nc").render_string());
18061
18062 let first = cache.line_starts(&first_text);
18063 let second = cache.line_starts(&second_text);
18064
18065 assert_eq!(first.as_ref(), second.as_ref());
18066 assert!(
18067 !Rc::ptr_eq(&first, &second),
18068 "line index lookup should not hash large text contents to find unrelated retained nodes"
18069 );
18070 }
18071
18072 #[test]
18073 fn device_pixel_bounds_for_rect_snaps_origin_and_extents() {
18074 let bounds = device_pixel_bounds_for_rect(
18075 Rect {
18076 x: 10.25,
18077 y: 14.6,
18078 width: 20.1,
18079 height: 9.2,
18080 },
18081 200,
18082 120,
18083 2.0,
18084 )
18085 .expect("rect should intersect the viewport");
18086
18087 assert_eq!(
18088 bounds,
18089 DevicePixelBounds {
18090 x: 20.0,
18091 y: 29.0,
18092 width: 41,
18093 height: 19,
18094 }
18095 );
18096 }
18097
18098 #[test]
18099 fn visible_layer_rect_intersects_clip_and_viewport() {
18100 let visible = visible_layer_rect(
18101 Rect {
18102 x: -10.0,
18103 y: 5.0,
18104 width: 80.0,
18105 height: 40.0,
18106 },
18107 Some(Rect {
18108 x: 4.0,
18109 y: 8.0,
18110 width: 20.0,
18111 height: 50.0,
18112 }),
18113 2.0,
18114 60,
18115 40,
18116 )
18117 .expect("visible rect");
18118
18119 assert_eq!(
18120 visible,
18121 Rect {
18122 x: 4.0,
18123 y: 8.0,
18124 width: 20.0,
18125 height: 12.0,
18126 }
18127 );
18128 }
18129
18130 #[test]
18131 fn clamp_effect_surface_scale_caps_large_surfaces_but_keeps_base_scale() {
18132 let clamped = clamp_effect_surface_scale(
18133 Rect {
18134 x: 0.0,
18135 y: 0.0,
18136 width: 1200.0,
18137 height: 900.0,
18138 },
18139 1.0,
18140 8.0,
18141 16_384,
18142 );
18143
18144 assert!(
18145 clamped < 8.0,
18146 "large translated effect layers must be capped to avoid OOM, got {clamped}"
18147 );
18148 assert!(
18149 clamped >= 1.0,
18150 "effect surfaces must not fall below destination resolution, got {clamped}"
18151 );
18152 }
18153
18154 #[test]
18155 fn clamp_effect_surface_scale_keeps_decorated_text_capture_scale() {
18156 let clamped = clamp_effect_surface_scale(
18157 Rect {
18158 x: 0.0,
18159 y: 0.0,
18160 width: 446.0,
18161 height: 44.0,
18162 },
18163 1.0,
18164 9.0,
18165 16_384,
18166 );
18167
18168 assert_eq!(
18169 clamped, 9.0,
18170 "decorated text motion-stable captures must keep full scale"
18171 );
18172 }
18173
18174 fn backdrop_layer(z_index: usize) -> BackdropLayer {
18175 BackdropLayer {
18176 node_id: Some(700 + z_index),
18177 rect: Rect {
18178 x: 0.0,
18179 y: 0.0,
18180 width: 10.0,
18181 height: 10.0,
18182 },
18183 clip: None,
18184 snap_anchor: None,
18185 effect: RenderEffect::blur(2.0),
18186 z_index,
18187 }
18188 }
18189
18190 fn test_shape(z_index: usize, blend_mode: BlendMode) -> DrawShape {
18191 DrawShape {
18192 rect: Rect {
18193 x: 0.0,
18194 y: 0.0,
18195 width: 8.0,
18196 height: 8.0,
18197 },
18198 local_rect: Rect {
18199 x: 0.0,
18200 y: 0.0,
18201 width: 8.0,
18202 height: 8.0,
18203 },
18204 quad: [[0.0, 0.0], [8.0, 0.0], [0.0, 8.0], [8.0, 8.0]],
18205 snap_anchor: None,
18206 brush: SceneBrush::Solid(Color::BLACK),
18207 shape: None,
18208 stroke: None,
18209 arc: None,
18210 z_index,
18211 clip: None,
18212 blend_mode,
18213 motion_context_animated: false,
18214 }
18215 }
18216
18217 #[test]
18218 fn shape_shadow_content_hash_ignores_viewport_translation() {
18219 fn translate_shape(shape: &DrawShape, dx: f32, dy: f32) -> DrawShape {
18220 let mut translated = *shape;
18221 translated.rect.x += dx;
18222 translated.rect.y += dy;
18223 translated.local_rect.x += dx;
18224 translated.local_rect.y += dy;
18225 for point in &mut translated.quad {
18226 point[0] += dx;
18227 point[1] += dy;
18228 }
18229 translated.snap_anchor = translated.snap_anchor.map(|anchor| {
18230 SnapAnchor::rigid(Point::new(anchor.origin.x + dx, anchor.origin.y + dy))
18231 });
18232 translated.clip = translated.clip.map(|mut clip| {
18233 clip.x += dx;
18234 clip.y += dy;
18235 clip
18236 });
18237 translated
18238 }
18239
18240 let mut first = test_shape(1, BlendMode::SrcOver);
18241 first.rect = Rect {
18242 x: 10.0,
18243 y: 20.0,
18244 width: 80.0,
18245 height: 40.0,
18246 };
18247 first.local_rect = first.rect;
18248 first.quad = [[10.0, 20.0], [90.0, 20.0], [10.0, 60.0], [90.0, 60.0]];
18249 first.snap_anchor = Some(SnapAnchor::rigid(Point::new(7.0, 11.0)));
18250 first.shape = Some(RoundedCornerShape::uniform(8.0));
18251 first.clip = Some(Rect {
18252 x: 8.0,
18253 y: 18.0,
18254 width: 86.0,
18255 height: 44.0,
18256 });
18257 let mut cutout = test_shape(2, BlendMode::DstOut);
18258 cutout.rect = Rect {
18259 x: 18.0,
18260 y: 26.0,
18261 width: 62.0,
18262 height: 22.0,
18263 };
18264 cutout.local_rect = cutout.rect;
18265 cutout.quad = [[18.0, 26.0], [80.0, 26.0], [18.0, 48.0], [80.0, 48.0]];
18266 cutout.shape = Some(RoundedCornerShape::uniform(4.0));
18267
18268 let dx = 37.0;
18269 let dy = -11.5;
18270 let translated = translate_shape(&first, dx, dy);
18271 let translated_cutout = translate_shape(&cutout, dx, dy);
18272
18273 let root_scale = 1.25;
18274 let first_shapes = vec![(first, BlendMode::SrcOver), (cutout, BlendMode::DstOut)];
18275 let translated_shapes = vec![
18276 (translated, BlendMode::SrcOver),
18277 (translated_cutout, BlendMode::DstOut),
18278 ];
18279
18280 let first_hash = shape_shadow_content_hash(&first_shapes, &[], root_scale);
18281 let translated_hash = shape_shadow_content_hash(&translated_shapes, &[], root_scale);
18282
18283 assert_eq!(first_hash, translated_hash);
18284
18285 let mut changed_shapes = translated_shapes;
18286 changed_shapes[0].0.rect.width += 1.0;
18287 let changed_hash = shape_shadow_content_hash(&changed_shapes, &[], root_scale);
18288
18289 assert_ne!(first_hash, changed_hash);
18290 }
18291
18292 #[test]
18293 fn shape_shadow_content_hash_is_stable_under_fractional_scale_scroll() {
18294 fn shadow_shapes_at(y: f32) -> Vec<(DrawShape, BlendMode)> {
18300 let mut shape = test_shape(1, BlendMode::SrcOver);
18301 shape.rect = Rect {
18302 x: 24.0,
18303 y,
18304 width: 180.0,
18305 height: 90.0,
18306 };
18307 shape.local_rect = shape.rect;
18308 shape.quad = crate::rect_to_quad(shape.rect);
18309 shape.shape = Some(RoundedCornerShape::uniform(14.0));
18310 vec![(shape, BlendMode::SrcOver)]
18311 }
18312
18313 let root_scale = 130.0f32 / 96.0;
18314 let blur_radius = 18.0f32;
18315 let pixel_radius = blur_radius * root_scale;
18316
18317 let key_at = |y: f32| {
18318 let shapes = shadow_shapes_at(y);
18319 let plan =
18320 shape_shadow_surface_plan(&shapes, None, blur_radius, 1600, 1600, root_scale, 8192)
18321 .expect("surface plan");
18322 shape_shadow_surface_cache_key(
18323 &shapes,
18324 &[],
18325 plan.source_device_bounds,
18326 pixel_radius,
18327 root_scale,
18328 )
18329 .expect("cache key")
18330 };
18331
18332 let base = key_at(640.0);
18337 for step in 1..=12 {
18338 let scrolled = key_at(640.0 - step as f32 * 4.0);
18339 assert_eq!(
18340 base, scrolled,
18341 "scrolled shadow cache key must stay stable at fractional scale (step {step})"
18342 );
18343 }
18344 }
18345
18346 #[test]
18347 fn shape_shadow_cache_key_uses_unclipped_source_bounds_for_scrolled_clip() {
18348 fn translated_card_shadow(y: f32) -> Vec<(DrawShape, BlendMode)> {
18349 let mut shape = test_shape(1, BlendMode::SrcOver);
18350 shape.rect = Rect {
18351 x: 24.0,
18352 y,
18353 width: 280.0,
18354 height: 120.0,
18355 };
18356 shape.local_rect = shape.rect;
18357 shape.quad = [[24.0, y], [304.0, y], [24.0, y + 120.0], [304.0, y + 120.0]];
18358 shape.shape = Some(RoundedCornerShape::uniform(18.0));
18359 vec![(shape, BlendMode::SrcOver)]
18360 }
18361
18362 let root_scale = 1.0;
18363 let blur_radius = 18.0;
18364 let viewport_clip = Rect {
18365 x: 0.0,
18366 y: 96.0,
18367 width: 360.0,
18368 height: 720.0,
18369 };
18370 let key_for = |y: f32| {
18371 let shapes = translated_card_shadow(y);
18372 let plan = shape_shadow_surface_plan(
18373 &shapes,
18374 Some(viewport_clip),
18375 blur_radius,
18376 360,
18377 900,
18378 root_scale,
18379 4096,
18380 )
18381 .expect("surface plan");
18382 shape_shadow_surface_cache_key(
18383 &shapes,
18384 &[],
18385 plan.source_device_bounds,
18386 plan.pixel_radius,
18387 root_scale,
18388 )
18389 .expect("cache key")
18390 };
18391
18392 assert_eq!(key_for(740.0), key_for(756.0));
18395 }
18396
18397 #[test]
18398 fn shape_visibility_uses_nonzero_viewport_offset_for_cropped_offscreen() {
18399 let mut shape = test_shape(1, BlendMode::SrcOver);
18400 shape.rect = Rect {
18401 x: 24.0,
18402 y: 740.0,
18403 width: 280.0,
18404 height: 120.0,
18405 };
18406 shape.local_rect = shape.rect;
18407 shape.quad = [[24.0, 740.0], [304.0, 740.0], [24.0, 860.0], [304.0, 860.0]];
18408 let viewport = ViewportUniformParams {
18409 width: 316,
18410 height: 228,
18411 offset: [6.0, 686.0],
18412 };
18413
18414 assert!(shape_draw_is_visible_in_viewport(&shape, viewport, 1.0));
18415 }
18416
18417 #[test]
18418 fn text_prewarm_uses_nonzero_viewport_offset_for_cropped_offscreen() {
18419 let viewport = ViewportUniformParams {
18420 width: 316,
18421 height: 228,
18422 offset: [6.0, 686.0],
18423 };
18424 let text_rect = Rect {
18425 x: 24.0,
18426 y: 740.0,
18427 width: 280.0,
18428 height: 40.0,
18429 };
18430
18431 assert!(text_draw_is_visible_in_viewport(
18432 text_rect, None, viewport, 1.0
18433 ));
18434 assert!(text_draw_should_prewarm_in_viewport(
18435 text_rect, None, viewport, 1.0
18436 ));
18437 }
18438
18439 fn test_shadow_draw(shapes: Vec<(DrawShape, BlendMode)>) -> ShadowDraw {
18440 ShadowDraw {
18441 shapes,
18442 brushes: vec![],
18443 texts: vec![],
18444 blur_radius: 8.0,
18445 clip: None,
18446 z_index: 0,
18447 }
18448 }
18449
18450 fn test_image(z_index: usize, blend_mode: BlendMode) -> ImageDraw {
18451 ImageDraw {
18452 rect: Rect {
18453 x: 0.0,
18454 y: 0.0,
18455 width: 8.0,
18456 height: 8.0,
18457 },
18458 local_rect: Rect {
18459 x: 0.0,
18460 y: 0.0,
18461 width: 8.0,
18462 height: 8.0,
18463 },
18464 quad: [[0.0, 0.0], [8.0, 0.0], [0.0, 8.0], [8.0, 8.0]],
18465 snap_anchor: None,
18466 image: ImageBitmap::from_rgba8(1, 1, vec![255, 255, 255, 255]).expect("image"),
18467 alpha: 1.0,
18468 color_filter: None,
18469 sampling: ImageSampling::Nearest,
18470 z_index,
18471 clip: None,
18472 blend_mode,
18473 src_rect: None,
18474 motion_context_animated: false,
18475 }
18476 }
18477
18478 #[test]
18479 fn image_sampler_descriptors_match_requested_sampling() {
18480 let nearest = image_sampler_descriptor(ImageSampling::Nearest);
18481 assert_eq!(nearest.mag_filter, wgpu::FilterMode::Nearest);
18482 assert_eq!(nearest.min_filter, wgpu::FilterMode::Nearest);
18483
18484 let linear = image_sampler_descriptor(ImageSampling::Linear);
18485 assert_eq!(linear.mag_filter, wgpu::FilterMode::Linear);
18486 assert_eq!(linear.min_filter, wgpu::FilterMode::Linear);
18487 }
18488
18489 #[test]
18490 fn image_uv_rect_clamps_source_rect_to_texel_centers() {
18491 let image = ImageBitmap::from_rgba8(24, 16, vec![0; 24 * 16 * 4]).expect("image");
18492 let uv = image_uv_rect(
18493 &image,
18494 Some(Rect {
18495 x: 0.0,
18496 y: 0.0,
18497 width: 16.0,
18498 height: 16.0,
18499 }),
18500 )
18501 .expect("uv rect");
18502
18503 assert_eq!(uv.min, [0.0, 0.0]);
18504 assert_eq!(uv.max, [16.0 / 24.0, 1.0]);
18505 assert_eq!(
18506 uv.sample_bounds,
18507 [0.5 / 24.0, 0.5 / 16.0, 15.5 / 24.0, 15.5 / 16.0]
18508 );
18509 }
18510
18511 #[test]
18512 fn image_uv_rect_keeps_full_image_unclamped() {
18513 let image = ImageBitmap::from_rgba8(2, 2, vec![0; 16]).expect("image");
18514 let uv = image_uv_rect(&image, None).expect("uv rect");
18515
18516 assert_eq!(uv.min, [0.0, 0.0]);
18517 assert_eq!(uv.max, [1.0, 1.0]);
18518 assert_eq!(uv.sample_bounds, [0.0, 0.0, 1.0, 1.0]);
18519 }
18520
18521 fn test_text(z_index: usize) -> TextDraw {
18522 TextDraw {
18523 node_id: 0,
18524 rect: Rect {
18525 x: 0.0,
18526 y: 0.0,
18527 width: 8.0,
18528 height: 8.0,
18529 },
18530 snap_anchor: None,
18531 translated_content_context: false,
18532 text: Arc::new(cranpose_ui::text::AnnotatedString::from("t").render_string()),
18533 color: Color::WHITE,
18534 text_style: cranpose_ui::TextStyle::default(),
18535 font_size: 12.0,
18536 scale: 1.0,
18537 layout_options: cranpose_ui::TextLayoutOptions::default(),
18538 z_index,
18539 clip: None,
18540 }
18541 }
18542
18543 #[test]
18544 fn text_draw_visibility_rejects_text_outside_clip_before_rasterization() {
18545 let viewport = ViewportUniformParams {
18546 width: 320,
18547 height: 240,
18548 offset: [0.0, 0.0],
18549 };
18550 let text_rect = Rect {
18551 x: 0.0,
18552 y: 260.0,
18553 width: 200.0,
18554 height: 40.0,
18555 };
18556 let clip = Some(Rect {
18557 x: 0.0,
18558 y: 0.0,
18559 width: 320.0,
18560 height: 200.0,
18561 });
18562
18563 assert!(
18564 !text_draw_is_visible_in_viewport(text_rect, clip, viewport, 1.0),
18565 "lazy-list beyond-bound text outside the clip must not be rasterized"
18566 );
18567 }
18568
18569 #[test]
18570 fn text_draw_prewarm_accepts_clipped_text_near_viewport() {
18571 let viewport = ViewportUniformParams {
18572 width: 320,
18573 height: 240,
18574 offset: [0.0, 0.0],
18575 };
18576 let text_rect = Rect {
18577 x: 0.0,
18578 y: 260.0,
18579 width: 200.0,
18580 height: 40.0,
18581 };
18582 let clip = Some(Rect {
18583 x: 0.0,
18584 y: 0.0,
18585 width: 320.0,
18586 height: 200.0,
18587 });
18588
18589 assert!(!text_draw_is_visible_in_viewport(
18590 text_rect, clip, viewport, 1.0
18591 ));
18592 assert!(text_draw_should_prewarm_in_viewport(
18593 text_rect, clip, viewport, 1.0
18594 ));
18595 }
18596
18597 #[test]
18598 fn text_draw_prewarm_rejects_far_clipped_text() {
18599 let viewport = ViewportUniformParams {
18600 width: 320,
18601 height: 240,
18602 offset: [0.0, 0.0],
18603 };
18604 let text_rect = Rect {
18605 x: 0.0,
18606 y: 1600.0,
18607 width: 200.0,
18608 height: 40.0,
18609 };
18610 let clip = Some(Rect {
18611 x: 0.0,
18612 y: 0.0,
18613 width: 320.0,
18614 height: 200.0,
18615 });
18616
18617 assert!(!text_draw_should_prewarm_in_viewport(
18618 text_rect, clip, viewport, 1.0
18619 ));
18620 }
18621
18622 #[test]
18623 fn text_draw_visibility_rejects_unclipped_text_outside_viewport() {
18624 let viewport = ViewportUniformParams {
18625 width: 320,
18626 height: 240,
18627 offset: [0.0, 0.0],
18628 };
18629 let text_rect = Rect {
18630 x: 0.0,
18631 y: 241.0,
18632 width: 200.0,
18633 height: 40.0,
18634 };
18635
18636 assert!(
18637 !text_draw_is_visible_in_viewport(text_rect, None, viewport, 1.0),
18638 "unclipped text outside the target viewport must not be rasterized"
18639 );
18640 }
18641
18642 #[test]
18643 fn text_draw_visibility_keeps_partially_visible_text() {
18644 let viewport = ViewportUniformParams {
18645 width: 320,
18646 height: 240,
18647 offset: [0.0, 0.0],
18648 };
18649 let text_rect = Rect {
18650 x: 0.0,
18651 y: 220.0,
18652 width: 200.0,
18653 height: 40.0,
18654 };
18655
18656 assert!(text_draw_is_visible_in_viewport(
18657 text_rect, None, viewport, 1.0
18658 ));
18659 }
18660
18661 fn test_draw_ops(
18662 shapes: &[DrawShape],
18663 images: &[ImageDraw],
18664 texts: &[TextDraw],
18665 shadows: &[ShadowDraw],
18666 ) -> Vec<DrawOp> {
18667 let mut ops = Vec::new();
18668 ops.extend(shapes.iter().enumerate().map(|(index, shape)| DrawOp {
18669 z_index: shape.z_index,
18670 kind: DrawOpKind::Shape(index),
18671 }));
18672 ops.extend(images.iter().enumerate().map(|(index, image)| DrawOp {
18673 z_index: image.z_index,
18674 kind: DrawOpKind::Image(index),
18675 }));
18676 ops.extend(texts.iter().enumerate().map(|(index, text)| DrawOp {
18677 z_index: text.z_index,
18678 kind: DrawOpKind::Text(index),
18679 }));
18680 ops.extend(shadows.iter().enumerate().map(|(index, shadow)| DrawOp {
18681 z_index: shadow.z_index,
18682 kind: DrawOpKind::Shadow(index),
18683 }));
18684 ops.sort_by_key(|op| op.z_index);
18685 ops
18686 }
18687
18688 fn test_layer(local_bounds: Rect, children: Vec<RenderNode>) -> LayerNode {
18689 crate::test_support::layer_node(
18690 local_bounds,
18691 ProjectiveTransform::identity(),
18692 GraphicsLayer::default(),
18693 children,
18694 )
18695 }
18696
18697 fn cacheable_layer(
18698 node_id: cranpose_core::NodeId,
18699 local_bounds: Rect,
18700 children: Vec<RenderNode>,
18701 ) -> LayerNode {
18702 let mut layer = test_layer(local_bounds, children);
18703 layer.node_id = Some(node_id);
18704 layer.cache_policy = cranpose_render_common::graph::CachePolicy::Auto;
18705 layer.recompute_raster_cache_hashes();
18706 layer
18707 }
18708
18709 fn text_layer_with_style(text: AnnotatedString, text_style: TextStyle) -> LayerNode {
18710 test_layer(
18711 Rect {
18712 x: 0.0,
18713 y: 0.0,
18714 width: 64.0,
18715 height: 32.0,
18716 },
18717 vec![RenderNode::Primitive(PrimitiveEntry {
18718 phase: PrimitivePhase::BeforeChildren,
18719 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
18720 node_id: 1,
18721 rect: Rect {
18722 x: 2.0,
18723 y: 3.0,
18724 width: 48.0,
18725 height: 18.0,
18726 },
18727 text: std::rc::Rc::new(text),
18728 text_style,
18729 font_size: 14.0,
18730 layout_options: TextLayoutOptions::default(),
18731 clip: None,
18732 })),
18733 })],
18734 )
18735 }
18736
18737 fn snapped_text_leaf(animated: bool, translated_content_context: bool) -> LayerNode {
18738 LayerNode {
18739 node_id: Some(77),
18740 local_bounds: Rect {
18741 x: 0.0,
18742 y: 0.0,
18743 width: 48.0,
18744 height: 24.0,
18745 },
18746 transform_to_parent: ProjectiveTransform::translation(14.25, 16.5),
18747 motion_context_animated: animated,
18748 translated_content_context,
18749 translated_content_offset: Point::default(),
18750 content_offset: Point::default(),
18751 scene_children_origin: cranpose_ui_graphics::Point::default(),
18752 scene_children_layer_translation: cranpose_ui_graphics::Point::default(),
18753 graphics_layer: GraphicsLayer::default(),
18754 clip_to_bounds: false,
18755 shadow_clip: None,
18756 hit_test: None,
18757 has_hit_targets: false,
18758 isolation: IsolationReasons::default(),
18759 cache_policy: CachePolicy::None,
18760 cache_hashes: LayerRasterCacheHashes::default(),
18761 cache_hashes_valid: false,
18762 children: vec![
18763 RenderNode::Primitive(PrimitiveEntry {
18764 phase: PrimitivePhase::BeforeChildren,
18765 node: PrimitiveNode::Draw(DrawPrimitiveNode {
18766 primitive: DrawPrimitive::RoundRect {
18767 rect: Rect {
18768 x: 0.0,
18769 y: 0.0,
18770 width: 48.0,
18771 height: 24.0,
18772 },
18773 brush: Brush::solid(Color(0.28, 0.30, 0.46, 0.88)),
18774 radii: CornerRadii::uniform(6.0),
18775 stroke: None,
18776 },
18777 clip: None,
18778 }),
18779 }),
18780 RenderNode::Primitive(PrimitiveEntry {
18781 phase: PrimitivePhase::BeforeChildren,
18782 node: PrimitiveNode::Draw(DrawPrimitiveNode {
18783 primitive: DrawPrimitive::Image {
18784 rect: Rect {
18785 x: 2.0,
18786 y: 2.0,
18787 width: 12.0,
18788 height: 12.0,
18789 },
18790 image: ImageBitmap::from_rgba8(
18791 2,
18792 2,
18793 vec![
18794 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255,
18795 255,
18796 ],
18797 )
18798 .expect("image"),
18799 alpha: 1.0,
18800 color_filter: None,
18801 sampling: ImageSampling::Linear,
18802 src_rect: None,
18803 },
18804 clip: None,
18805 }),
18806 }),
18807 RenderNode::Primitive(PrimitiveEntry {
18808 phase: PrimitivePhase::BeforeChildren,
18809 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
18810 node_id: 77,
18811 rect: Rect {
18812 x: 6.0,
18813 y: 4.0,
18814 width: 36.0,
18815 height: 16.0,
18816 },
18817 text: std::rc::Rc::new(AnnotatedString::from("48 px")),
18818 text_style: TextStyle::default(),
18819 font_size: 14.0,
18820 layout_options: TextLayoutOptions::default(),
18821 clip: None,
18822 })),
18823 }),
18824 ],
18825 }
18826 }
18827
18828 fn snapped_text_leaf_root(animated: bool, translated_content_context: bool) -> LayerNode {
18829 let text_leaf = snapped_text_leaf(animated, translated_content_context);
18830 test_layer(
18831 Rect {
18832 x: 0.0,
18833 y: 0.0,
18834 width: 96.0,
18835 height: 64.0,
18836 },
18837 vec![RenderNode::Layer(Box::new(text_leaf))],
18838 )
18839 }
18840
18841 fn translated_content_local_surface_root() -> LayerNode {
18842 let mut effectful_text = text_layer_with_style(
18843 AnnotatedString::from("shadow"),
18844 TextStyle::from_span_style(SpanStyle {
18845 shadow: Some(Shadow {
18846 color: Color::BLACK,
18847 offset: Point::new(1.0, 2.0),
18848 blur_radius: 3.0,
18849 }),
18850 ..SpanStyle::default()
18851 }),
18852 );
18853 effectful_text.translated_content_context = true;
18854
18855 let translated_content = LayerNode {
18856 node_id: Some(78),
18857 local_bounds: Rect {
18858 x: 0.0,
18859 y: 0.0,
18860 width: 96.0,
18861 height: 64.0,
18862 },
18863 transform_to_parent: ProjectiveTransform::translation(14.25, 16.5),
18864 motion_context_animated: false,
18865 translated_content_context: true,
18866 translated_content_offset: Point::default(),
18867 content_offset: Point::default(),
18868 scene_children_origin: cranpose_ui_graphics::Point::default(),
18869 scene_children_layer_translation: cranpose_ui_graphics::Point::default(),
18870 graphics_layer: GraphicsLayer::default(),
18871 clip_to_bounds: false,
18872 shadow_clip: None,
18873 hit_test: None,
18874 has_hit_targets: false,
18875 isolation: IsolationReasons::default(),
18876 cache_policy: CachePolicy::None,
18877 cache_hashes: LayerRasterCacheHashes::default(),
18878 cache_hashes_valid: false,
18879 children: vec![RenderNode::Layer(Box::new(effectful_text))],
18880 };
18881
18882 test_layer(
18883 Rect {
18884 x: 0.0,
18885 y: 0.0,
18886 width: 160.0,
18887 height: 120.0,
18888 },
18889 vec![RenderNode::Layer(Box::new(translated_content))],
18890 )
18891 }
18892
18893 #[test]
18894 fn scissor_rect_for_layer_intersects_with_clip() {
18895 let rect = Rect {
18896 x: 10.0,
18897 y: 10.0,
18898 width: 30.0,
18899 height: 20.0,
18900 };
18901 let clip = Rect {
18902 x: 20.0,
18903 y: 15.0,
18904 width: 100.0,
18905 height: 100.0,
18906 };
18907
18908 let scissor = scissor_rect_for_layer(rect, Some(clip), 1.0, 200, 200);
18909 assert_eq!(scissor, Some((20, 15, 20, 15)));
18910 }
18911
18912 #[test]
18913 fn visible_draw_rect_no_clip_returns_original() {
18914 let rect = Rect {
18915 x: 100.0,
18916 y: 200.0,
18917 width: 300.0,
18918 height: 400.0,
18919 };
18920 assert_eq!(visible_draw_rect(rect, None), Some(rect));
18921 }
18922
18923 #[test]
18924 fn visible_draw_rect_with_clip_intersects() {
18925 let rect = Rect {
18926 x: 0.0,
18927 y: 0.0,
18928 width: 2000.0,
18929 height: 5000.0,
18930 };
18931 let clip = Rect {
18932 x: 0.0,
18933 y: 0.0,
18934 width: 800.0,
18935 height: 600.0,
18936 };
18937 let visible = visible_draw_rect(rect, Some(clip)).expect("should have visible area");
18938 assert_eq!(visible.width, 800.0);
18939 assert_eq!(visible.height, 600.0);
18940 }
18941
18942 #[test]
18943 fn visible_draw_rect_fully_clipped_returns_none() {
18944 let rect = Rect {
18945 x: 1000.0,
18946 y: 1000.0,
18947 width: 200.0,
18948 height: 200.0,
18949 };
18950 let clip = Rect {
18951 x: 0.0,
18952 y: 0.0,
18953 width: 800.0,
18954 height: 600.0,
18955 };
18956 assert!(visible_draw_rect(rect, Some(clip)).is_none());
18957 }
18958
18959 #[test]
18960 fn scene_bounds_respects_clip_on_shapes() {
18961 let mut scene = CompositorScene::new();
18962 scene.shapes.push(DrawShape {
18964 rect: Rect {
18965 x: 10.0,
18966 y: 10.0,
18967 width: 100.0,
18968 height: 50.0,
18969 },
18970 clip: Some(Rect {
18971 x: 0.0,
18972 y: 0.0,
18973 width: 800.0,
18974 height: 600.0,
18975 }),
18976 ..test_shape(0, BlendMode::SrcOver)
18977 });
18978 scene.shapes.push(DrawShape {
18980 rect: Rect {
18981 x: 0.0,
18982 y: 3000.0,
18983 width: 100.0,
18984 height: 50.0,
18985 },
18986 clip: Some(Rect {
18987 x: 0.0,
18988 y: 0.0,
18989 width: 800.0,
18990 height: 600.0,
18991 }),
18992 ..test_shape(1, BlendMode::SrcOver)
18993 });
18994 let bounds = scene_bounds(&scene).expect("should have bounds");
18995 assert!(bounds.y + bounds.height <= 600.0);
18998 }
18999
19000 #[test]
19001 fn scene_bounds_scroll_content_clipped_to_viewport() {
19002 let mut scene = CompositorScene::new();
19005 let viewport_clip = Rect {
19006 x: 0.0,
19007 y: 0.0,
19008 width: 800.0,
19009 height: 600.0,
19010 };
19011 for i in 0..20 {
19012 scene.shapes.push(DrawShape {
19013 rect: Rect {
19014 x: 0.0,
19015 y: i as f32 * 300.0,
19016 width: 800.0,
19017 height: 200.0,
19018 },
19019 clip: Some(viewport_clip),
19020 ..test_shape(i, BlendMode::SrcOver)
19021 });
19022 }
19023 let bounds = scene_bounds(&scene).expect("should have bounds");
19024 assert_eq!(bounds.x, 0.0);
19027 assert_eq!(bounds.y, 0.0);
19028 assert!(bounds.width <= 800.0);
19029 assert!(bounds.height <= 600.0);
19030 }
19031
19032 #[test]
19033 fn scene_bounds_stable_across_scroll_offsets() {
19034 let viewport_clip = Rect {
19037 x: 0.0,
19038 y: 0.0,
19039 width: 400.0,
19040 height: 50.0,
19041 };
19042 let compute_bounds_at_offset = |scroll_x: f32| {
19043 let mut scene = CompositorScene::new();
19044 for i in 0..10 {
19045 scene.shapes.push(DrawShape {
19046 rect: Rect {
19047 x: i as f32 * 100.0 - scroll_x,
19048 y: 0.0,
19049 width: 80.0,
19050 height: 40.0,
19051 },
19052 clip: Some(viewport_clip),
19053 ..test_shape(i, BlendMode::SrcOver)
19054 });
19055 }
19056 scene_bounds(&scene).expect("bounds")
19057 };
19058 let bounds_at_0 = compute_bounds_at_offset(0.0);
19059 let bounds_at_300 = compute_bounds_at_offset(300.0);
19060 let bounds_at_600 = compute_bounds_at_offset(600.0);
19061 assert!(
19063 (bounds_at_0.width - bounds_at_300.width).abs() < 1.0,
19064 "bounds width changed with scroll: {} vs {}",
19065 bounds_at_0.width,
19066 bounds_at_300.width
19067 );
19068 assert!(
19069 (bounds_at_0.width - bounds_at_600.width).abs() < 1.0,
19070 "bounds width changed with scroll: {} vs {}",
19071 bounds_at_0.width,
19072 bounds_at_600.width
19073 );
19074 }
19075
19076 #[test]
19077 fn collect_effect_ranges_respects_excluded_effect() {
19078 let layers = vec![effect_layer(10, 40), effect_layer(20, 30)];
19079 let mut ranges = Vec::new();
19080 collect_effect_ranges(&layers, 10, 40, Some(0), &mut ranges);
19081 assert_eq!(ranges.len(), 1);
19082 assert_eq!(ranges[0], 20..30);
19083 }
19084
19085 #[test]
19086 fn collect_layer_events_includes_nested_when_parent_excluded() {
19087 let effects = vec![effect_layer(10, 40), effect_layer(20, 30)];
19088 let backdrops = vec![backdrop_layer(25)];
19089 let mut events = Vec::new();
19090 collect_layer_events(&effects, &backdrops, 10, 40, Some(0), &mut events);
19091 assert_eq!(events.len(), 2);
19092
19093 match events[0].kind {
19094 LayerEventKind::Effect(index) => assert_eq!(index, 1),
19095 LayerEventKind::Backdrop(_) => panic!("expected nested effect as first event"),
19096 }
19097 match events[1].kind {
19098 LayerEventKind::Backdrop(index) => assert_eq!(index, 0),
19099 LayerEventKind::Effect(_) => panic!("expected backdrop as second event"),
19100 }
19101 }
19102
19103 fn pure_text_leaf(animated: bool, translated_content_context: bool) -> LayerNode {
19104 LayerNode {
19105 node_id: Some(177),
19106 local_bounds: Rect {
19107 x: 0.0,
19108 y: 0.0,
19109 width: 96.0,
19110 height: 32.0,
19111 },
19112 transform_to_parent: ProjectiveTransform::translation(11.4, 23.6),
19113 motion_context_animated: animated,
19114 translated_content_context,
19115 translated_content_offset: Point::default(),
19116 content_offset: Point::default(),
19117 scene_children_origin: cranpose_ui_graphics::Point::default(),
19118 scene_children_layer_translation: cranpose_ui_graphics::Point::default(),
19119 graphics_layer: GraphicsLayer::default(),
19120 clip_to_bounds: false,
19121 shadow_clip: None,
19122 hit_test: None,
19123 has_hit_targets: false,
19124 isolation: IsolationReasons::default(),
19125 cache_policy: CachePolicy::None,
19126 cache_hashes: LayerRasterCacheHashes::default(),
19127 cache_hashes_valid: false,
19128 children: vec![RenderNode::Primitive(PrimitiveEntry {
19129 phase: PrimitivePhase::BeforeChildren,
19130 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
19131 node_id: 177,
19132 rect: Rect {
19133 x: 0.0,
19134 y: 0.0,
19135 width: 96.0,
19136 height: 24.0,
19137 },
19138 clip: None,
19139 text: std::rc::Rc::new(AnnotatedString::from("Pure text")),
19140 text_style: TextStyle::default(),
19141 font_size: 14.0,
19142 layout_options: TextLayoutOptions::default(),
19143 })),
19144 })],
19145 }
19146 }
19147
19148 fn pure_text_leaf_root(animated: bool, translated_content_context: bool) -> LayerNode {
19149 let text_leaf = pure_text_leaf(animated, translated_content_context);
19150 test_layer(
19151 Rect {
19152 x: 0.0,
19153 y: 0.0,
19154 width: 160.0,
19155 height: 96.0,
19156 },
19157 vec![RenderNode::Layer(Box::new(text_leaf))],
19158 )
19159 }
19160
19161 #[test]
19162 fn collect_layer_events_sorts_backdrop_before_effect_at_same_z() {
19163 let effects = vec![effect_layer(10, 20)];
19164 let backdrops = vec![backdrop_layer(10)];
19165 let mut events = Vec::new();
19166 collect_layer_events(&effects, &backdrops, 0, 30, None, &mut events);
19167 assert_eq!(events.len(), 2);
19168
19169 match events[0].kind {
19170 LayerEventKind::Backdrop(_) => {}
19171 LayerEventKind::Effect(_) => panic!("expected backdrop to run before effect"),
19172 }
19173 match events[1].kind {
19174 LayerEventKind::Effect(_) => {}
19175 LayerEventKind::Backdrop(_) => panic!("expected effect as second event"),
19176 }
19177 }
19178
19179 #[test]
19180 fn collect_layer_events_prefers_outer_effect_when_same_start_z() {
19181 let effects = vec![effect_layer(10, 20), effect_layer(10, 40)];
19184 let mut events = Vec::new();
19185 collect_layer_events(&effects, &[], 0, 50, None, &mut events);
19186
19187 assert_eq!(events.len(), 2);
19188 match events[0].kind {
19189 LayerEventKind::Effect(index) => assert_eq!(index, 1),
19190 LayerEventKind::Backdrop(_) => panic!("expected outer effect first"),
19191 }
19192 match events[1].kind {
19193 LayerEventKind::Effect(index) => assert_eq!(index, 0),
19194 LayerEventKind::Backdrop(_) => panic!("expected child effect second"),
19195 }
19196 }
19197
19198 #[test]
19199 fn collect_layer_events_prefers_later_effect_when_ranges_match() {
19200 let effects = vec![effect_layer(10, 20), effect_layer(10, 20)];
19201 let mut events = Vec::new();
19202 collect_layer_events(&effects, &[], 0, 30, None, &mut events);
19203
19204 assert_eq!(events.len(), 2);
19205 match events[0].kind {
19206 LayerEventKind::Effect(index) => assert_eq!(index, 1),
19207 LayerEventKind::Backdrop(_) => panic!("expected later effect first"),
19208 }
19209 match events[1].kind {
19210 LayerEventKind::Effect(index) => assert_eq!(index, 0),
19211 LayerEventKind::Backdrop(_) => panic!("expected earlier effect second"),
19212 }
19213 }
19214
19215 #[test]
19216 fn has_backdrop_layer_in_range_detects_nested_layers() {
19217 let backdrops = vec![backdrop_layer(5), backdrop_layer(15), backdrop_layer(25)];
19218 assert!(has_backdrop_layer_in_range(&backdrops, 10, 20));
19219 assert!(has_backdrop_layer_in_range(&backdrops, 0, 6));
19220 assert!(!has_backdrop_layer_in_range(&backdrops, 20, 25));
19221 }
19222
19223 #[test]
19224 fn layer_contains_descendant_backdrop_ignores_self_backdrop() {
19225 let mut self_backdrop = test_layer(
19226 Rect {
19227 x: 0.0,
19228 y: 0.0,
19229 width: 10.0,
19230 height: 10.0,
19231 },
19232 vec![],
19233 );
19234 self_backdrop.graphics_layer.backdrop_effect = Some(RenderEffect::blur(2.0));
19235 assert!(!layer_contains_descendant_backdrop(&self_backdrop));
19236
19237 let mut child = test_layer(
19238 Rect {
19239 x: 0.0,
19240 y: 0.0,
19241 width: 8.0,
19242 height: 8.0,
19243 },
19244 vec![],
19245 );
19246 child.graphics_layer.backdrop_effect = Some(RenderEffect::blur(2.0));
19247
19248 let parent = test_layer(
19249 Rect {
19250 x: 0.0,
19251 y: 0.0,
19252 width: 20.0,
19253 height: 20.0,
19254 },
19255 vec![RenderNode::Layer(Box::new(child))],
19256 );
19257 assert!(layer_contains_descendant_backdrop(&parent));
19258 }
19259
19260 fn child_layer_composite(
19261 layer: &LayerNode,
19262 z_index: usize,
19263 rect: Rect,
19264 needs_nested_underlay: bool,
19265 ) -> crate::normalized_scene::ChildLayerComposite {
19266 let mut requirements_cache = cranpose_core::collections::map::HashMap::new();
19267 let surface_requirements =
19268 crate::surface_plan::layer_surface_requirements_cached(layer, &mut requirements_cache);
19269 crate::normalized_scene::ChildLayerComposite {
19270 z_index,
19271 logical_rect: Rect {
19272 x: 0.0,
19273 y: 0.0,
19274 width: rect.width,
19275 height: rect.height,
19276 },
19277 dest_quad: rect_to_quad(rect),
19278 snap_anchor: None,
19279 composite_snap_origin: None,
19280 backdrop_rect: rect,
19281 visual_clip: None,
19282 surface_clip: None,
19283 shadow_draws: Vec::new(),
19284 needs_nested_underlay,
19285 node_id: layer.node_id,
19286 backdrop: layer.backdrop().cloned(),
19287 has_effect: layer.effect().is_some(),
19288 effect_contains_runtime_shader: layer
19289 .effect()
19290 .is_some_and(|effect| effect.contains_runtime_shader()),
19291 target_content_hash: layer.target_content_hash(),
19292 effect_hash: layer.effect_hash(),
19293 motion_source_content_hash: Some(layer.motion_source_content_hash()),
19294 contains_descendant_backdrop: layer_contains_descendant_backdrop(layer),
19295 cache_policy: layer.cache_policy,
19296 surface_requirements,
19297 rounded_clip: crate::surface_executor::backend::LayerSurfaceRoundedClip::from_layer(
19298 layer,
19299 ),
19300 isolation: cranpose_render_common::layer_composition::effective_layer_isolation(
19301 &layer.graphics_layer,
19302 ),
19303 translated_content_context: layer.translated_content_context,
19304 own_translated_content_axes: crate::surface_plan::translated_content_axes_for_layer(
19305 layer,
19306 ),
19307 clip_rect: layer.clip_rect(),
19308 local_bounds: layer.local_bounds,
19309 surface_scale: crate::surface_plan::layer_surface_scale(layer),
19310 source: crate::normalized_scene::LoweredChildSource::default(),
19311 }
19312 }
19313
19314 #[test]
19315 fn root_direct_preflight_allows_first_translated_child_underlay() {
19316 let child = test_layer(
19317 Rect {
19318 x: 0.0,
19319 y: 0.0,
19320 width: 400.0,
19321 height: 280.0,
19322 },
19323 vec![],
19324 );
19325 let collected = CollectedLayer {
19326 scene: CompositorScene::new(),
19327 child_layers: vec![child_layer_composite(
19328 &child,
19329 3,
19330 Rect {
19331 x: 48.0,
19332 y: 96.0,
19333 width: 400.0,
19334 height: 280.0,
19335 },
19336 true,
19337 )],
19338 };
19339
19340 assert!(direct_root_child_underlays_are_supported(&collected, false));
19341 }
19342
19343 #[test]
19344 fn root_direct_preflight_allows_axis_aligned_prior_child_underlay() {
19345 let first = test_layer(
19346 Rect {
19347 x: 0.0,
19348 y: 0.0,
19349 width: 80.0,
19350 height: 40.0,
19351 },
19352 vec![],
19353 );
19354 let backdrop_child = test_layer(
19355 Rect {
19356 x: 0.0,
19357 y: 0.0,
19358 width: 400.0,
19359 height: 280.0,
19360 },
19361 vec![],
19362 );
19363 let collected = CollectedLayer {
19364 scene: CompositorScene::new(),
19365 child_layers: vec![
19366 child_layer_composite(
19367 &first,
19368 1,
19369 Rect {
19370 x: 8.0,
19371 y: 16.0,
19372 width: 80.0,
19373 height: 40.0,
19374 },
19375 false,
19376 ),
19377 child_layer_composite(
19378 &backdrop_child,
19379 4,
19380 Rect {
19381 x: 48.0,
19382 y: 96.0,
19383 width: 400.0,
19384 height: 280.0,
19385 },
19386 true,
19387 ),
19388 ],
19389 };
19390
19391 assert!(direct_root_child_underlays_are_supported(&collected, false));
19392 }
19393
19394 #[test]
19395 fn root_direct_preflight_rejects_effectful_prior_child_underlay() {
19396 let mut first = test_layer(
19397 Rect {
19398 x: 0.0,
19399 y: 0.0,
19400 width: 80.0,
19401 height: 40.0,
19402 },
19403 vec![],
19404 );
19405 first.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
19406 let backdrop_child = test_layer(
19407 Rect {
19408 x: 0.0,
19409 y: 0.0,
19410 width: 400.0,
19411 height: 280.0,
19412 },
19413 vec![],
19414 );
19415 let collected = CollectedLayer {
19416 scene: CompositorScene::new(),
19417 child_layers: vec![
19418 child_layer_composite(
19419 &first,
19420 1,
19421 Rect {
19422 x: 64.0,
19423 y: 112.0,
19424 width: 80.0,
19425 height: 40.0,
19426 },
19427 false,
19428 ),
19429 child_layer_composite(
19430 &backdrop_child,
19431 4,
19432 Rect {
19433 x: 48.0,
19434 y: 96.0,
19435 width: 400.0,
19436 height: 280.0,
19437 },
19438 true,
19439 ),
19440 ],
19441 };
19442
19443 assert!(!direct_root_child_underlays_are_supported(
19444 &collected, false
19445 ));
19446 }
19447
19448 #[test]
19449 fn root_direct_preflight_ignores_non_overlapping_effectful_prior_child_underlay() {
19450 let mut first = test_layer(
19451 Rect {
19452 x: 0.0,
19453 y: 0.0,
19454 width: 80.0,
19455 height: 40.0,
19456 },
19457 vec![],
19458 );
19459 first.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
19460 let backdrop_child = test_layer(
19461 Rect {
19462 x: 0.0,
19463 y: 0.0,
19464 width: 400.0,
19465 height: 280.0,
19466 },
19467 vec![],
19468 );
19469 let collected = CollectedLayer {
19470 scene: CompositorScene::new(),
19471 child_layers: vec![
19472 child_layer_composite(
19473 &first,
19474 1,
19475 Rect {
19476 x: 8.0,
19477 y: 16.0,
19478 width: 80.0,
19479 height: 40.0,
19480 },
19481 false,
19482 ),
19483 child_layer_composite(
19484 &backdrop_child,
19485 4,
19486 Rect {
19487 x: 48.0,
19488 y: 96.0,
19489 width: 400.0,
19490 height: 280.0,
19491 },
19492 true,
19493 ),
19494 ],
19495 };
19496
19497 assert!(direct_root_child_underlays_are_supported(&collected, false));
19498 }
19499
19500 #[test]
19501 fn root_direct_preflight_rejects_underlay_that_would_replay_prior_scene_effects() {
19502 let backdrop_child = test_layer(
19503 Rect {
19504 x: 0.0,
19505 y: 0.0,
19506 width: 400.0,
19507 height: 280.0,
19508 },
19509 vec![],
19510 );
19511 let mut scene = CompositorScene::new();
19512 scene.next_z = 1;
19513 scene.push_effect_layer(
19514 Rect {
19515 x: 0.0,
19516 y: 0.0,
19517 width: 120.0,
19518 height: 120.0,
19519 },
19520 None,
19521 Some(RenderEffect::blur(2.0)),
19522 BlendMode::SrcOver,
19523 1.0,
19524 0,
19525 1,
19526 );
19527 let collected = CollectedLayer {
19528 scene,
19529 child_layers: vec![child_layer_composite(
19530 &backdrop_child,
19531 4,
19532 Rect {
19533 x: 48.0,
19534 y: 96.0,
19535 width: 400.0,
19536 height: 280.0,
19537 },
19538 true,
19539 )],
19540 };
19541
19542 assert!(!direct_root_child_underlays_are_supported(
19543 &collected, false
19544 ));
19545 }
19546
19547 #[test]
19548 fn root_direct_eligibility_does_not_reject_descendant_backdrop() {
19549 let mut backdrop = test_layer(
19550 Rect {
19551 x: 0.0,
19552 y: 0.0,
19553 width: 40.0,
19554 height: 40.0,
19555 },
19556 vec![],
19557 );
19558 backdrop.graphics_layer.backdrop_effect = Some(RenderEffect::blur(4.0));
19559 let child = test_layer(
19560 Rect {
19561 x: 0.0,
19562 y: 0.0,
19563 width: 120.0,
19564 height: 96.0,
19565 },
19566 vec![RenderNode::Layer(Box::new(backdrop))],
19567 );
19568 let root = test_layer(
19569 Rect {
19570 x: 0.0,
19571 y: 0.0,
19572 width: 240.0,
19573 height: 160.0,
19574 },
19575 vec![RenderNode::Layer(Box::new(child))],
19576 );
19577 let mut cache = HashMap::new();
19578
19579 assert!(root_can_render_directly_cached(&root, &mut cache));
19580 }
19581
19582 #[test]
19583 fn root_direct_scene_events_allow_root_local_effects() {
19584 let mut scene = CompositorScene::new();
19585 scene.effect_layers.push(EffectLayer {
19586 rect: Rect {
19587 x: 20.0,
19588 y: 30.0,
19589 width: 120.0,
19590 height: 80.0,
19591 },
19592 clip: None,
19593 snap_anchor: None,
19594 effect: Some(RenderEffect::blur(6.0)),
19595 blend_mode: BlendMode::SrcOver,
19596 composite_alpha: 1.0,
19597 z_start: 0,
19598 z_end: 1,
19599 requirements: SurfaceRequirementSet::default().with(SurfaceRequirement::RenderEffect),
19600 });
19601
19602 assert!(root_direct_scene_events_are_supported(&scene, false));
19603 }
19604
19605 #[test]
19606 fn root_direct_scene_events_reject_root_local_backdrops() {
19607 let mut scene = CompositorScene::new();
19608 scene.backdrop_layers.push(BackdropLayer {
19609 node_id: Some(99),
19610 rect: Rect {
19611 x: 20.0,
19612 y: 30.0,
19613 width: 120.0,
19614 height: 80.0,
19615 },
19616 clip: None,
19617 snap_anchor: None,
19618 effect: RenderEffect::blur(6.0),
19619 z_index: 1,
19620 });
19621
19622 assert!(!root_direct_scene_events_are_supported(&scene, false));
19623 }
19624
19625 fn scene_with_root_local_backdrop(z_index: usize) -> CompositorScene {
19626 let mut scene = CompositorScene::new();
19627 scene.next_z = z_index + 1;
19628 scene.backdrop_layers.push(BackdropLayer {
19629 node_id: Some(99),
19630 rect: Rect {
19631 x: 20.0,
19632 y: 30.0,
19633 width: 120.0,
19634 height: 80.0,
19635 },
19636 clip: None,
19637 snap_anchor: None,
19638 effect: RenderEffect::blur(6.0),
19639 z_index,
19640 });
19641 scene
19642 }
19643
19644 #[test]
19645 fn a_root_local_backdrop_takes_the_direct_road_when_the_target_reads() {
19646 let scene = scene_with_root_local_backdrop(1);
19647 assert!(root_direct_scene_events_are_supported(&scene, true));
19648 }
19649
19650 #[test]
19651 fn a_backdrop_inside_an_effect_layer_stays_off_the_direct_road() {
19652 let mut scene = scene_with_root_local_backdrop(1);
19653 scene.next_z = 3;
19654 scene.effect_layers.push(EffectLayer {
19655 rect: Rect {
19656 x: 0.0,
19657 y: 0.0,
19658 width: 200.0,
19659 height: 200.0,
19660 },
19661 clip: None,
19662 snap_anchor: None,
19663 effect: Some(RenderEffect::blur(6.0)),
19664 blend_mode: BlendMode::SrcOver,
19665 composite_alpha: 1.0,
19666 z_start: 0,
19667 z_end: 3,
19668 requirements: SurfaceRequirementSet::default().with(SurfaceRequirement::RenderEffect),
19669 });
19670
19671 assert!(!root_direct_scene_events_are_supported(&scene, true));
19672 assert!(!root_direct_scene_events_are_supported(&scene, false));
19673 }
19674
19675 #[test]
19676 fn a_child_that_carries_a_backdrop_takes_the_direct_road_when_the_target_reads() {
19677 let mut backdrop_child = test_layer(
19678 Rect {
19679 x: 0.0,
19680 y: 0.0,
19681 width: 400.0,
19682 height: 280.0,
19683 },
19684 vec![],
19685 );
19686 backdrop_child.graphics_layer.backdrop_effect = Some(RenderEffect::blur(4.0));
19687 let collected = CollectedLayer {
19688 scene: CompositorScene::new(),
19689 child_layers: vec![child_layer_composite(
19690 &backdrop_child,
19691 1,
19692 Rect {
19693 x: 48.0,
19694 y: 96.0,
19695 width: 400.0,
19696 height: 280.0,
19697 },
19698 false,
19699 )],
19700 };
19701
19702 assert!(collected.child_layers[0].backdrop.is_some());
19703 assert!(direct_root_child_underlays_are_supported(&collected, true));
19704 assert!(!direct_root_child_underlays_are_supported(
19705 &collected, false
19706 ));
19707 }
19708
19709 fn frosted_layer(bounds: Rect, offset: Point) -> LayerNode {
19710 let mut layer = test_layer(
19711 bounds,
19712 vec![RenderNode::Primitive(PrimitiveEntry {
19713 phase: PrimitivePhase::BeforeChildren,
19714 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19715 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19716 rect: bounds,
19717 brush: Brush::solid(Color::from_rgba_u8(255, 255, 255, 60)),
19718 stroke: None,
19719 },
19720 clip: None,
19721 }),
19722 })],
19723 );
19724 layer.transform_to_parent = ProjectiveTransform::translation(offset.x, offset.y);
19725 layer.graphics_layer.backdrop_effect = Some(RenderEffect::blur(8.0));
19726 layer
19727 }
19728
19729 #[test]
19730 fn a_frosted_layer_keeps_its_own_surface() {
19731 let frosted = frosted_layer(
19732 Rect {
19733 x: 0.0,
19734 y: 0.0,
19735 width: 40.0,
19736 height: 20.0,
19737 },
19738 Point::new(10.0, 6.0),
19739 );
19740 let root = test_layer(
19741 Rect {
19742 x: 0.0,
19743 y: 0.0,
19744 width: 200.0,
19745 height: 100.0,
19746 },
19747 vec![RenderNode::Layer(Box::new(frosted))],
19748 );
19749 let mut rect_cache = HashMap::new();
19750 let mut requirements_cache = HashMap::new();
19751
19752 let collected =
19753 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
19754
19755 assert_eq!(collected.child_layers.len(), 1);
19756 assert!(
19757 collected.scene.backdrop_layers.is_empty(),
19758 "a layer that keeps its surface carries its backdrop on the composite"
19759 );
19760 assert!(collected.child_layers[0].backdrop.is_some());
19761 }
19762
19763 fn row_with_clipped_glass(row_background: Color) -> LayerNode {
19764 let mut glass = frosted_layer(
19765 Rect {
19766 x: 0.0,
19767 y: 0.0,
19768 width: 40.0,
19769 height: 20.0,
19770 },
19771 Point::new(10.0, 6.0),
19772 );
19773 glass.isolation.shape_clip = true;
19774 let row_bounds = Rect {
19775 x: 0.0,
19776 y: 0.0,
19777 width: 200.0,
19778 height: 40.0,
19779 };
19780 let mut row = test_layer(
19781 row_bounds,
19782 vec![
19783 RenderNode::Primitive(PrimitiveEntry {
19784 phase: PrimitivePhase::BeforeChildren,
19785 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19786 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19787 rect: row_bounds,
19788 brush: Brush::solid(row_background),
19789 stroke: None,
19790 },
19791 clip: None,
19792 }),
19793 }),
19794 RenderNode::Layer(Box::new(glass)),
19795 ],
19796 );
19797 row.isolation.shape_clip = true;
19798 row
19799 }
19800
19801 #[test]
19802 fn a_backdrop_covered_by_its_own_row_asks_for_no_underlay() {
19803 let root = test_layer(
19804 Rect {
19805 x: 0.0,
19806 y: 0.0,
19807 width: 400.0,
19808 height: 200.0,
19809 },
19810 vec![RenderNode::Layer(Box::new(row_with_clipped_glass(
19811 Color::WHITE,
19812 )))],
19813 );
19814 let mut rect_cache = HashMap::new();
19815 let mut requirements_cache = HashMap::new();
19816
19817 let collected =
19818 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
19819
19820 assert_eq!(collected.child_layers.len(), 1);
19821 assert!(collected.child_layers[0].contains_descendant_backdrop);
19822 assert!(
19823 !collected.child_layers[0].needs_nested_underlay,
19824 "an opaque row draw under the glass is all the blur reads, so no picture of the scene behind the row is needed"
19825 );
19826 }
19827
19828 #[test]
19829 fn a_backdrop_over_a_see_through_row_still_asks_for_an_underlay() {
19830 let root = test_layer(
19831 Rect {
19832 x: 0.0,
19833 y: 0.0,
19834 width: 400.0,
19835 height: 200.0,
19836 },
19837 vec![RenderNode::Layer(Box::new(row_with_clipped_glass(
19838 Color::from_rgba_u8(255, 255, 255, 40),
19839 )))],
19840 );
19841 let mut rect_cache = HashMap::new();
19842 let mut requirements_cache = HashMap::new();
19843
19844 let collected =
19845 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
19846
19847 assert_eq!(collected.child_layers.len(), 1);
19848 assert!(collected.child_layers[0].needs_nested_underlay);
19849 }
19850
19851 #[test]
19852 fn estimate_layer_surface_rect_includes_transformed_child_bounds() {
19853 let mut child = test_layer(
19854 Rect {
19855 x: 0.0,
19856 y: 0.0,
19857 width: 10.0,
19858 height: 6.0,
19859 },
19860 vec![RenderNode::Primitive(PrimitiveEntry {
19861 phase: PrimitivePhase::BeforeChildren,
19862 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19863 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19864 rect: Rect {
19865 x: 0.0,
19866 y: 0.0,
19867 width: 10.0,
19868 height: 6.0,
19869 },
19870 brush: Brush::solid(Color::WHITE),
19871 stroke: None,
19872 },
19873 clip: None,
19874 }),
19875 })],
19876 );
19877 child.transform_to_parent = ProjectiveTransform::translation(18.0, 7.0);
19878
19879 let parent = test_layer(
19880 Rect {
19881 x: 0.0,
19882 y: 0.0,
19883 width: 4.0,
19884 height: 4.0,
19885 },
19886 vec![RenderNode::Layer(Box::new(child))],
19887 );
19888
19889 assert_eq!(
19890 estimate_layer_surface_rect(&parent),
19891 Rect {
19892 x: 18.0,
19893 y: 7.0,
19894 width: 10.0,
19895 height: 6.0,
19896 }
19897 );
19898 }
19899
19900 #[test]
19901 fn estimate_layer_surface_rect_clips_translated_clip_layers_without_hidden_leading_content() {
19902 let mut layer = test_layer(
19903 Rect {
19904 x: 0.0,
19905 y: 0.0,
19906 width: 120.0,
19907 height: 72.0,
19908 },
19909 vec![RenderNode::Primitive(PrimitiveEntry {
19910 phase: PrimitivePhase::BeforeChildren,
19911 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19912 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19913 rect: Rect {
19914 x: 24.0,
19915 y: 0.0,
19916 width: 200.0,
19917 height: 480.0,
19918 },
19919 brush: Brush::solid(Color::WHITE),
19920 stroke: None,
19921 },
19922 clip: None,
19923 }),
19924 })],
19925 );
19926 layer.translated_content_context = true;
19927 layer.motion_context_animated = true;
19928 layer.clip_to_bounds = true;
19929
19930 assert_eq!(
19931 estimate_layer_surface_rect(&layer),
19932 Rect {
19933 x: 24.0,
19934 y: 0.0,
19935 width: 96.0,
19936 height: 72.0,
19937 }
19938 );
19939 }
19940
19941 #[test]
19942 fn estimate_layer_surface_rect_clips_active_horizontal_scroll_content() {
19943 let mut layer = test_layer(
19944 Rect {
19945 x: 0.0,
19946 y: 0.0,
19947 width: 120.0,
19948 height: 72.0,
19949 },
19950 vec![RenderNode::Primitive(PrimitiveEntry {
19951 phase: PrimitivePhase::BeforeChildren,
19952 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19953 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19954 rect: Rect {
19955 x: -24.0,
19956 y: 0.0,
19957 width: 200.0,
19958 height: 480.0,
19959 },
19960 brush: Brush::solid(Color::WHITE),
19961 stroke: None,
19962 },
19963 clip: None,
19964 }),
19965 })],
19966 );
19967 layer.translated_content_context = true;
19968 layer.motion_context_animated = true;
19969 layer.clip_to_bounds = true;
19970
19971 assert_eq!(
19972 estimate_layer_surface_rect(&layer),
19973 Rect {
19974 x: 0.0,
19975 y: 0.0,
19976 width: 120.0,
19977 height: 72.0,
19978 }
19979 );
19980 }
19981
19982 #[test]
19983 fn estimate_layer_surface_rect_clips_active_vertical_scroll_content() {
19984 let mut layer = test_layer(
19985 Rect {
19986 x: 0.0,
19987 y: 0.0,
19988 width: 120.0,
19989 height: 72.0,
19990 },
19991 vec![RenderNode::Primitive(PrimitiveEntry {
19992 phase: PrimitivePhase::BeforeChildren,
19993 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19994 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19995 rect: Rect {
19996 x: 0.0,
19997 y: -24.0,
19998 width: 120.0,
19999 height: 200.0,
20000 },
20001 brush: Brush::solid(Color::WHITE),
20002 stroke: None,
20003 },
20004 clip: None,
20005 }),
20006 })],
20007 );
20008 layer.translated_content_context = true;
20009 layer.motion_context_animated = true;
20010 layer.clip_to_bounds = true;
20011
20012 assert_eq!(
20013 estimate_layer_surface_rect(&layer),
20014 Rect {
20015 x: 0.0,
20016 y: 0.0,
20017 width: 120.0,
20018 height: 72.0,
20019 }
20020 );
20021 }
20022
20023 #[test]
20024 fn estimate_layer_surface_rect_keeps_shallow_scroll_capture_origin_stable() {
20025 fn shallow_scroll_surface_rect(content_y: f32) -> Rect {
20026 let mut layer = test_layer(
20027 Rect {
20028 x: 0.0,
20029 y: 0.0,
20030 width: 120.0,
20031 height: 72.0,
20032 },
20033 vec![RenderNode::Primitive(PrimitiveEntry {
20034 phase: PrimitivePhase::BeforeChildren,
20035 node: PrimitiveNode::Draw(DrawPrimitiveNode {
20036 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
20037 rect: Rect {
20038 x: 0.0,
20039 y: content_y,
20040 width: 120.0,
20041 height: 200.0,
20042 },
20043 brush: Brush::solid(Color::WHITE),
20044 stroke: None,
20045 },
20046 clip: None,
20047 }),
20048 })],
20049 );
20050 layer.translated_content_context = true;
20051 layer.motion_context_animated = true;
20052 layer.clip_to_bounds = true;
20053 estimate_layer_surface_rect(&layer)
20054 }
20055
20056 assert_eq!(
20057 shallow_scroll_surface_rect(-24.0),
20058 shallow_scroll_surface_rect(-25.0),
20059 "shallow scroll capture bounds must not move the offscreen surface origin on adjacent scroll positions"
20060 );
20061 }
20062
20063 #[test]
20064 fn estimate_layer_surface_rect_clips_active_xy_scroll_content() {
20065 let mut layer = test_layer(
20066 Rect {
20067 x: 0.0,
20068 y: 0.0,
20069 width: 120.0,
20070 height: 72.0,
20071 },
20072 vec![RenderNode::Primitive(PrimitiveEntry {
20073 phase: PrimitivePhase::BeforeChildren,
20074 node: PrimitiveNode::Draw(DrawPrimitiveNode {
20075 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
20076 rect: Rect {
20077 x: -16.0,
20078 y: -24.0,
20079 width: 180.0,
20080 height: 240.0,
20081 },
20082 brush: Brush::solid(Color::WHITE),
20083 stroke: None,
20084 },
20085 clip: None,
20086 }),
20087 })],
20088 );
20089 layer.translated_content_context = true;
20090 layer.motion_context_animated = true;
20091 layer.clip_to_bounds = true;
20092
20093 assert_eq!(
20094 estimate_layer_surface_rect(&layer),
20095 Rect {
20096 x: 0.0,
20097 y: 0.0,
20098 width: 120.0,
20099 height: 72.0,
20100 }
20101 );
20102 }
20103
20104 #[test]
20105 fn estimate_layer_surface_rect_clips_deep_hidden_active_scroll_content() {
20106 let mut layer = test_layer(
20107 Rect {
20108 x: 0.0,
20109 y: 0.0,
20110 width: 120.0,
20111 height: 72.0,
20112 },
20113 vec![RenderNode::Primitive(PrimitiveEntry {
20114 phase: PrimitivePhase::BeforeChildren,
20115 node: PrimitiveNode::Draw(DrawPrimitiveNode {
20116 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
20117 rect: Rect {
20118 x: 0.0,
20119 y: -1200.0,
20120 width: 120.0,
20121 height: 1400.0,
20122 },
20123 brush: Brush::solid(Color::WHITE),
20124 stroke: None,
20125 },
20126 clip: None,
20127 }),
20128 })],
20129 );
20130 layer.translated_content_context = true;
20131 layer.motion_context_animated = true;
20132 layer.clip_to_bounds = true;
20133
20134 assert_eq!(
20135 estimate_layer_surface_rect(&layer),
20136 Rect {
20137 x: 0.0,
20138 y: 0.0,
20139 width: 120.0,
20140 height: 72.0,
20141 }
20142 );
20143 }
20144
20145 #[test]
20146 fn estimate_layer_surface_rect_keeps_deep_scroll_capture_origin_stable() {
20147 fn deep_scroll_surface_rect(content_y: f32) -> Rect {
20148 let mut layer = test_layer(
20149 Rect {
20150 x: 0.0,
20151 y: 0.0,
20152 width: 120.0,
20153 height: 72.0,
20154 },
20155 vec![RenderNode::Primitive(PrimitiveEntry {
20156 phase: PrimitivePhase::BeforeChildren,
20157 node: PrimitiveNode::Draw(DrawPrimitiveNode {
20158 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
20159 rect: Rect {
20160 x: 0.0,
20161 y: content_y,
20162 width: 120.0,
20163 height: 1400.0,
20164 },
20165 brush: Brush::solid(Color::WHITE),
20166 stroke: None,
20167 },
20168 clip: None,
20169 }),
20170 })],
20171 );
20172 layer.translated_content_context = true;
20173 layer.motion_context_animated = true;
20174 layer.clip_to_bounds = true;
20175 estimate_layer_surface_rect(&layer)
20176 }
20177
20178 assert_eq!(
20179 deep_scroll_surface_rect(-1200.0),
20180 deep_scroll_surface_rect(-1201.0),
20181 "deep scroll capture bounds must not re-phase the offscreen surface origin on adjacent scroll positions"
20182 );
20183 }
20184
20185 #[test]
20186 fn motion_stable_capture_bounds_bounds_shadows_for_clipped_effect_layer() {
20187 let mut layer = test_layer(
20188 Rect {
20189 x: 0.0,
20190 y: 0.0,
20191 width: 120.0,
20192 height: 72.0,
20193 },
20194 vec![],
20195 );
20196 layer.clip_to_bounds = true;
20197 layer.graphics_layer.clip = true;
20198 layer.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
20199
20200 let mut shadow_shape = test_shape(0, BlendMode::SrcOver);
20201 shadow_shape.rect = Rect {
20202 x: -24.0,
20203 y: -1200.0,
20204 width: 180.0,
20205 height: 1400.0,
20206 };
20207 let mut scene = CompositorScene::new();
20208 scene
20209 .shadow_draws
20210 .push(test_shadow_draw(vec![(shadow_shape, BlendMode::SrcOver)]));
20211
20212 let requirements = SurfaceRequirementSet::default()
20213 .with(SurfaceRequirement::RenderEffect)
20214 .with(SurfaceRequirement::MotionStableCapture);
20215
20216 assert_eq!(
20217 motion_stable_capture_bounds(
20218 &layer,
20219 &scene,
20220 &[],
20221 requirements,
20222 TranslatedContentAxes::default(),
20223 None,
20224 ),
20225 Some(Rect {
20226 x: -360.0,
20227 y: -216.0,
20228 width: 480.0,
20229 height: 288.0,
20230 })
20231 );
20232 }
20233
20234 #[test]
20235 fn vertical_motion_stable_capture_uses_viewport_cross_axis_bounds() {
20236 let mut layer = test_layer(
20237 Rect {
20238 x: 0.0,
20239 y: 0.0,
20240 width: 200.0,
20241 height: 100.0,
20242 },
20243 vec![],
20244 );
20245 layer.clip_to_bounds = true;
20246 layer.graphics_layer.clip = true;
20247
20248 let mut shape = test_shape(0, BlendMode::SrcOver);
20249 shape.rect = Rect {
20250 x: 60.0,
20251 y: -80.0,
20252 width: 80.0,
20253 height: 220.0,
20254 };
20255 let mut scene = CompositorScene::new();
20256 scene.shapes.push(shape);
20257
20258 let requirements =
20259 SurfaceRequirementSet::default().with(SurfaceRequirement::MotionStableCapture);
20260
20261 assert_eq!(
20262 motion_stable_capture_bounds(
20263 &layer,
20264 &scene,
20265 &[],
20266 requirements,
20267 TranslatedContentAxes { x: false, y: true },
20268 None,
20269 ),
20270 Some(Rect {
20271 x: -96.0,
20272 y: -64.0,
20273 width: 296.0,
20274 height: 164.0,
20275 })
20276 );
20277 }
20278
20279 #[test]
20280 fn vertical_motion_stable_capture_uses_external_surface_clip() {
20281 let layer = test_layer(
20282 Rect {
20283 x: 0.0,
20284 y: 0.0,
20285 width: 200.0,
20286 height: 100.0,
20287 },
20288 vec![],
20289 );
20290
20291 let mut shape = test_shape(0, BlendMode::SrcOver);
20292 shape.rect = Rect {
20293 x: 60.0,
20294 y: -80.0,
20295 width: 80.0,
20296 height: 220.0,
20297 };
20298 let mut scene = CompositorScene::new();
20299 scene.shapes.push(shape);
20300
20301 let requirements =
20302 SurfaceRequirementSet::default().with(SurfaceRequirement::MotionStableCapture);
20303
20304 assert_eq!(
20305 motion_stable_capture_bounds(
20306 &layer,
20307 &scene,
20308 &[],
20309 requirements,
20310 TranslatedContentAxes { x: false, y: true },
20311 Some(Rect {
20312 x: 0.0,
20313 y: 0.0,
20314 width: 200.0,
20315 height: 100.0,
20316 }),
20317 ),
20318 Some(Rect {
20319 x: -96.0,
20320 y: -64.0,
20321 width: 296.0,
20322 height: 164.0,
20323 })
20324 );
20325 }
20326
20327 #[test]
20328 fn estimate_layer_surface_rect_expands_for_child_layer_shadow() {
20329 let mut child = test_layer(
20330 Rect {
20331 x: 0.0,
20332 y: 0.0,
20333 width: 12.0,
20334 height: 8.0,
20335 },
20336 vec![],
20337 );
20338 child.transform_to_parent = ProjectiveTransform::translation(20.0, 9.0);
20339 child.graphics_layer.shadow_elevation = 6.0;
20340
20341 let parent = test_layer(
20342 Rect {
20343 x: 0.0,
20344 y: 0.0,
20345 width: 4.0,
20346 height: 4.0,
20347 },
20348 vec![RenderNode::Layer(Box::new(child))],
20349 );
20350
20351 let rect = estimate_layer_surface_rect(&parent);
20352 assert!(rect.x < 20.0);
20353 assert!(rect.y < 9.0);
20354 assert!(rect.width > 12.0);
20355 assert!(rect.height > 8.0);
20356 }
20357
20358 #[test]
20359 fn estimate_layer_surface_rect_respects_local_bounds_for_effect_layers() {
20360 let mut layer = test_layer(
20361 Rect {
20362 x: 0.0,
20363 y: 0.0,
20364 width: 28.0,
20365 height: 28.0,
20366 },
20367 vec![RenderNode::Primitive(PrimitiveEntry {
20368 phase: PrimitivePhase::BeforeChildren,
20369 node: PrimitiveNode::Draw(DrawPrimitiveNode {
20370 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
20371 rect: Rect {
20372 x: 10.0,
20373 y: 10.0,
20374 width: 10.0,
20375 height: 10.0,
20376 },
20377 brush: Brush::solid(Color::WHITE),
20378 stroke: None,
20379 },
20380 clip: None,
20381 }),
20382 })],
20383 );
20384 layer.graphics_layer.render_effect = Some(RenderEffect::blur(12.0));
20385
20386 assert_eq!(
20387 estimate_layer_surface_rect(&layer),
20388 Rect {
20389 x: 0.0,
20390 y: 0.0,
20391 width: 28.0,
20392 height: 28.0,
20393 }
20394 );
20395 }
20396
20397 #[test]
20398 fn layer_raster_cache_candidate_ignores_parent_transform() {
20399 let primitive = PrimitiveEntry {
20400 phase: PrimitivePhase::BeforeChildren,
20401 node: PrimitiveNode::Draw(DrawPrimitiveNode {
20402 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
20403 rect: Rect {
20404 x: 2.0,
20405 y: 3.0,
20406 width: 6.0,
20407 height: 4.0,
20408 },
20409 brush: Brush::solid(Color::BLACK),
20410 stroke: None,
20411 },
20412 clip: None,
20413 }),
20414 };
20415 let base = cacheable_layer(
20416 41,
20417 Rect {
20418 x: 0.0,
20419 y: 0.0,
20420 width: 20.0,
20421 height: 20.0,
20422 },
20423 vec![RenderNode::Primitive(primitive.clone())],
20424 );
20425 let mut moved = base.clone();
20426 moved.transform_to_parent = ProjectiveTransform::translation(32.0, 18.0);
20427
20428 assert_eq!(
20429 layer_raster_cache_candidate(&base, 1.25, false, false),
20430 layer_raster_cache_candidate(&moved, 1.25, false, false)
20431 );
20432 }
20433
20434 #[test]
20435 fn layer_raster_cache_candidate_changes_for_translated_content_offset() {
20436 let primitive = PrimitiveEntry {
20437 phase: PrimitivePhase::BeforeChildren,
20438 node: PrimitiveNode::Draw(DrawPrimitiveNode {
20439 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
20440 rect: Rect {
20441 x: 2.0,
20442 y: 3.0,
20443 width: 6.0,
20444 height: 4.0,
20445 },
20446 brush: Brush::solid(Color::BLACK),
20447 stroke: None,
20448 },
20449 clip: None,
20450 }),
20451 };
20452 let mut base = cacheable_layer(
20453 42,
20454 Rect {
20455 x: 0.0,
20456 y: 0.0,
20457 width: 20.0,
20458 height: 20.0,
20459 },
20460 vec![RenderNode::Primitive(primitive)],
20461 );
20462 base.translated_content_context = true;
20463 base.translated_content_offset = Point::new(0.0, -8.0);
20464 base.recompute_raster_cache_hashes();
20465
20466 let mut moved = base.clone();
20467 moved.translated_content_offset = Point::new(0.0, -16.0);
20468 moved.recompute_raster_cache_hashes();
20469
20470 assert_ne!(
20471 layer_raster_cache_candidate(&base, 1.25, false, false),
20472 layer_raster_cache_candidate(&moved, 1.25, false, false),
20473 "full-surface layer cache candidates must not alias different scroll offsets"
20474 );
20475 }
20476
20477 #[test]
20478 fn layer_raster_cache_candidate_changes_for_child_transform() {
20479 let mut child = cacheable_layer(
20480 8,
20481 Rect {
20482 x: 0.0,
20483 y: 0.0,
20484 width: 12.0,
20485 height: 10.0,
20486 },
20487 vec![],
20488 );
20489 child.transform_to_parent = ProjectiveTransform::translation(4.0, 6.0);
20490 let base = cacheable_layer(
20491 7,
20492 Rect {
20493 x: 0.0,
20494 y: 0.0,
20495 width: 20.0,
20496 height: 20.0,
20497 },
20498 vec![RenderNode::Layer(Box::new(child.clone()))],
20499 );
20500 let mut moved_child = child;
20501 moved_child.transform_to_parent = ProjectiveTransform::translation(9.0, 6.0);
20502 let moved = cacheable_layer(
20503 7,
20504 Rect {
20505 x: 0.0,
20506 y: 0.0,
20507 width: 20.0,
20508 height: 20.0,
20509 },
20510 vec![RenderNode::Layer(Box::new(moved_child))],
20511 );
20512
20513 assert_ne!(
20514 layer_raster_cache_candidate(&base, 1.0, false, false),
20515 layer_raster_cache_candidate(&moved, 1.0, false, false)
20516 );
20517 }
20518
20519 #[test]
20520 fn layer_raster_cache_candidate_rejects_external_backdrop_dependency() {
20521 let mut child = cacheable_layer(
20522 12,
20523 Rect {
20524 x: 0.0,
20525 y: 0.0,
20526 width: 8.0,
20527 height: 8.0,
20528 },
20529 vec![],
20530 );
20531 child.graphics_layer.backdrop_effect = Some(RenderEffect::blur(2.0));
20532 let parent = cacheable_layer(
20533 11,
20534 Rect {
20535 x: 0.0,
20536 y: 0.0,
20537 width: 16.0,
20538 height: 16.0,
20539 },
20540 vec![RenderNode::Layer(Box::new(child))],
20541 );
20542
20543 assert!(layer_raster_cache_candidate(&parent, 1.0, false, false).is_some());
20544 assert!(layer_raster_cache_candidate(&parent, 1.0, true, false).is_none());
20545 }
20546
20547 #[test]
20548 fn layer_raster_cache_candidate_does_not_force_translation_only_text_surfaces() {
20549 let text = RenderNode::Primitive(PrimitiveEntry {
20550 phase: PrimitivePhase::BeforeChildren,
20551 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
20552 node_id: 77,
20553 rect: Rect {
20554 x: 2.0,
20555 y: 3.0,
20556 width: 48.0,
20557 height: 18.0,
20558 },
20559 text: std::rc::Rc::new(AnnotatedString::from("runtime cache")),
20560 text_style: TextStyle::default(),
20561 font_size: 14.0,
20562 layout_options: TextLayoutOptions::default(),
20563 clip: None,
20564 })),
20565 });
20566 let mut layer = test_layer(
20567 Rect {
20568 x: 0.0,
20569 y: 0.0,
20570 width: 64.0,
20571 height: 32.0,
20572 },
20573 vec![text],
20574 );
20575 layer.node_id = Some(77);
20576 layer.recompute_raster_cache_hashes();
20577
20578 assert!(
20579 layer_raster_cache_candidate(&layer, 1.0, false, false).is_none(),
20580 "root path should not isolate plain translation-only text layers"
20581 );
20582 assert!(
20583 layer_raster_cache_candidate(&layer, 1.0, false, true).is_none(),
20584 "child path should also render plain translation-only text layers directly"
20585 );
20586 }
20587
20588 #[test]
20589 fn layer_raster_cache_candidate_allows_stable_runtime_child_effect_surfaces() {
20590 let mut layer = test_layer(
20591 Rect {
20592 x: 0.0,
20593 y: 0.0,
20594 width: 64.0,
20595 height: 32.0,
20596 },
20597 vec![RenderNode::Primitive(PrimitiveEntry {
20598 phase: PrimitivePhase::BeforeChildren,
20599 node: PrimitiveNode::Draw(DrawPrimitiveNode {
20600 primitive: DrawPrimitive::Rect {
20601 rect: Rect {
20602 x: 0.0,
20603 y: 0.0,
20604 width: 64.0,
20605 height: 32.0,
20606 },
20607 brush: Brush::solid(Color::WHITE),
20608 stroke: None,
20609 },
20610 clip: None,
20611 }),
20612 })],
20613 );
20614 layer.node_id = Some(78);
20615 layer.graphics_layer.render_effect = Some(RenderEffect::blur(4.0));
20616 layer.recompute_raster_cache_hashes();
20617
20618 assert!(
20619 layer_raster_cache_candidate(&layer, 1.0, false, false).is_none(),
20620 "root direct path should not force-cache ordinary stable effects"
20621 );
20622 assert!(
20623 layer_raster_cache_candidate(&layer, 1.0, false, true).is_some(),
20624 "child surface rendering should retain stable non-runtime effects"
20625 );
20626 }
20627
20628 #[test]
20629 fn layer_raster_cache_candidate_rejects_runtime_shader_child_effect_surfaces() {
20630 let mut layer = test_layer(
20631 Rect {
20632 x: 0.0,
20633 y: 0.0,
20634 width: 64.0,
20635 height: 32.0,
20636 },
20637 vec![],
20638 );
20639 layer.node_id = Some(79);
20640 layer.graphics_layer.render_effect = Some(RenderEffect::runtime_shader(
20641 RuntimeShader::new("runtime shader"),
20642 ));
20643 layer.recompute_raster_cache_hashes();
20644
20645 assert!(
20646 layer_raster_cache_candidate(&layer, 1.0, false, true).is_none(),
20647 "runtime shaders must not fill the retained layer cache with per-frame uniform variants"
20648 );
20649 }
20650
20651 #[test]
20652 fn layer_surface_requirements_keep_plain_text_on_direct_path() {
20653 let layer = text_layer_with_style(AnnotatedString::from("plain"), TextStyle::default());
20654
20655 let requirements = layer_surface_requirements(&layer);
20656
20657 assert_eq!(requirements.direct_translation, Some(Point::default()));
20658 assert!(requirements
20659 .surface_requirements
20660 .contains(SurfaceRequirement::PixelStableComposite));
20661 assert!(!requirements
20662 .surface_requirements
20663 .has_isolating_requirement());
20664 }
20665
20666 #[test]
20667 fn layer_surface_requirements_keep_translated_plain_text_leaf_on_direct_path() {
20668 let layer = pure_text_leaf(false, true);
20669
20670 let requirements = layer_surface_requirements(&layer);
20671
20672 assert_eq!(
20673 requirements.direct_translation,
20674 Some(Point::new(11.4, 23.6))
20675 );
20676 assert!(
20677 requirements
20678 .surface_requirements
20679 .contains(SurfaceRequirement::PixelStableComposite)
20680 && !requirements
20681 .surface_requirements
20682 .has_isolating_requirement(),
20683 "translated plain text should stay on the direct path and isolate only the glyph draw"
20684 );
20685 }
20686
20687 #[test]
20688 fn layer_surface_requirements_keep_translated_text_leaf_with_background_on_direct_path() {
20689 let layer = snapped_text_leaf(false, true);
20690
20691 let requirements = layer_surface_requirements(&layer);
20692
20693 assert_eq!(
20694 requirements.direct_translation,
20695 Some(Point::new(14.25, 16.5))
20696 );
20697 assert!(
20698 requirements
20699 .surface_requirements
20700 .contains(SurfaceRequirement::PixelStableComposite)
20701 && !requirements
20702 .surface_requirements
20703 .has_isolating_requirement(),
20704 "translated text with direct sibling decoration/background should keep the layer direct"
20705 );
20706 }
20707
20708 #[test]
20709 fn translated_plain_text_uses_bounded_snap_surface() {
20710 let root = pure_text_leaf_root(true, true);
20711 let mut rect_cache = HashMap::new();
20712 let mut requirements_cache = HashMap::new();
20713 let collected =
20714 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
20715
20716 assert_eq!(collected.child_layers.len(), 1);
20717 assert!(collected.scene.texts.is_empty());
20718 assert!(collected.scene.effect_layers.is_empty());
20719 assert_snap_anchor_close(
20720 collected.child_layers[0].snap_anchor,
20721 Point::new(11.4, 23.6),
20722 "translated plain text's bounded local surface should composite at the content-origin snap phase",
20723 );
20724 }
20725
20726 #[test]
20730 #[ignore]
20731 fn shape_run_collect_timing_harness() {
20732 use cranpose_render_common::graph::DrawPrimitiveNode;
20733 use cranpose_render_common::layer_composition::local_content_layer_for;
20734 use cranpose_ui_graphics::Stroke;
20735
20736 let bounds = Rect {
20737 x: 0.0,
20738 y: 0.0,
20739 width: 1080.0,
20740 height: 2244.0,
20741 };
20742 let graphics_layer = GraphicsLayer::default();
20743
20744 let mut nodes: Vec<DrawPrimitiveNode> = Vec::new();
20747 for i in 0..3000u32 {
20748 let f = i as f32;
20749 let brush = if i % 8 == 0 {
20750 Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])
20751 } else {
20752 Brush::Solid(Color(0.5, 0.2, 0.8, 1.0))
20753 };
20754 let center = Point::new(540.0 + (f % 400.0), 1122.0 + (f % 350.0));
20755 let radius = 8.0 + (i % 23) as f32;
20756 let half = radius + 4.0;
20757 nodes.push(DrawPrimitiveNode {
20758 primitive: DrawPrimitive::Arc {
20759 rect: Rect {
20760 x: center.x - half,
20761 y: center.y - half,
20762 width: half * 2.0,
20763 height: half * 2.0,
20764 },
20765 brush,
20766 center,
20767 radius,
20768 start_angle: f * 0.07,
20769 sweep_angle: 0.5 + (i % 5) as f32,
20770 stroke: (i % 3 != 0).then(|| Stroke::new(4.0)),
20771 inner_radius: if i % 3 == 0 { radius * 0.6 } else { 0.0 },
20772 },
20773 clip: None,
20774 });
20775 }
20776
20777 let children: Vec<RenderNode> = nodes
20778 .iter()
20779 .map(|node| {
20780 RenderNode::Primitive(PrimitiveEntry {
20781 phase: PrimitivePhase::BeforeChildren,
20782 node: PrimitiveNode::Draw(node.clone()),
20783 })
20784 })
20785 .collect();
20786 let layer = crate::test_support::layer_node(
20787 bounds,
20788 ProjectiveTransform::identity(),
20789 graphics_layer,
20790 children,
20791 );
20792
20793 const ITERS: usize = 300;
20794
20795 let local_layer = local_content_layer_for(&layer.graphics_layer);
20797 let start = Instant::now();
20798 let mut sink_shapes = 0usize;
20799 for _ in 0..ITERS {
20800 let mut scene = CompositorScene::new();
20801 for node in &nodes {
20802 crate::pipeline::push_draw_primitive(
20803 &node.primitive,
20804 bounds,
20805 &local_layer,
20806 None,
20807 &mut scene,
20808 None,
20809 false,
20810 );
20811 }
20812 sink_shapes = scene.shapes.len();
20813 }
20814 let serial = start.elapsed();
20815
20816 let mut rect_cache = HashMap::new();
20817 let mut requirements_cache = HashMap::new();
20818 let start = Instant::now();
20819 let mut run_shapes = 0usize;
20820 for _ in 0..ITERS {
20821 let collected = collect_layer_contents(
20822 &layer,
20823 None,
20824 None,
20825 &mut rect_cache,
20826 &mut requirements_cache,
20827 );
20828 run_shapes = collected.scene.shapes.len();
20829 }
20830 let run = start.elapsed();
20831
20832 println!(
20833 "per-primitive: {:?}/iter ({sink_shapes} shapes) shape-run: {:?}/iter ({run_shapes} shapes)",
20834 serial / ITERS as u32,
20835 run / ITERS as u32,
20836 );
20837 }
20838
20839 fn assert_shape_run_collect_matches_per_primitive_emission() {
20841 use cranpose_render_common::graph::DrawPrimitiveNode;
20842 use cranpose_render_common::layer_composition::local_content_layer_for;
20843 use cranpose_render_common::primitive_emit::{resolve_primitive_clip, PrimitiveClipSpace};
20844 use cranpose_ui_graphics::{CornerRadii, Stroke};
20845
20846 let bounds = Rect {
20847 x: 0.0,
20848 y: 0.0,
20849 width: 800.0,
20850 height: 800.0,
20851 };
20852 let graphics_layer = GraphicsLayer {
20855 scale: 1.25,
20856 translation_x: 3.5,
20857 translation_y: -2.0,
20858 alpha: 0.9,
20859 rotation_z: 0.35,
20860 ..GraphicsLayer::default()
20861 };
20862
20863 let mut nodes: Vec<DrawPrimitiveNode> = Vec::new();
20864 for i in 0..600u32 {
20865 let f = i as f32;
20866 let brush = if i % 11 == 0 {
20867 Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])
20868 } else {
20869 Brush::Solid(Color(0.1 + (i % 7) as f32 * 0.1, 0.5, 0.9, 1.0))
20870 };
20871 let stroke = (i % 5 == 0).then(|| Stroke::new(1.0 + (i % 3) as f32));
20872 let primitive = match i % 3 {
20873 0 => DrawPrimitive::Rect {
20874 rect: Rect {
20875 x: f % 37.0,
20876 y: f % 53.0,
20877 width: 8.0 + f % 9.0,
20878 height: 6.0 + f % 5.0,
20879 },
20880 brush,
20881 stroke,
20882 },
20883 1 => DrawPrimitive::RoundRect {
20884 rect: Rect {
20885 x: f % 41.0,
20886 y: f % 43.0,
20887 width: 12.0,
20888 height: 10.0,
20889 },
20890 brush,
20891 radii: CornerRadii::uniform(2.0 + (i % 4) as f32),
20892 stroke,
20893 },
20894 _ => {
20895 let center = Point::new(60.0 + f % 71.0, 60.0 + f % 67.0);
20896 let radius = 5.0 + (i % 13) as f32;
20897 let sweep_angle = if i == 302 { 0.0 } else { 0.4 + (i % 6) as f32 };
20899 let half = radius + 4.0;
20900 DrawPrimitive::Arc {
20901 rect: Rect {
20902 x: center.x - half,
20903 y: center.y - half,
20904 width: half * 2.0,
20905 height: half * 2.0,
20906 },
20907 brush,
20908 center,
20909 radius,
20910 start_angle: f * 0.11,
20911 sweep_angle,
20912 stroke: (i % 2 == 0).then(|| Stroke::new(3.0)),
20913 inner_radius: if i % 4 == 2 { radius * 0.5 } else { 0.0 },
20914 }
20915 }
20916 };
20917 let primitive = if i == 300 {
20918 DrawPrimitive::Blend {
20922 primitive: Box::new(DrawPrimitive::Blend {
20923 primitive: Box::new(primitive),
20924 blend_mode: BlendMode::SrcOver,
20925 }),
20926 blend_mode: BlendMode::DstOut,
20927 }
20928 } else if i % 7 == 3 {
20929 DrawPrimitive::Blend {
20930 primitive: Box::new(primitive),
20931 blend_mode: BlendMode::DstOut,
20932 }
20933 } else {
20934 primitive
20935 };
20936 let clip = (i % 31 == 7).then_some(Rect {
20937 x: 0.0,
20938 y: 0.0,
20939 width: 30.0,
20940 height: 30.0,
20941 });
20942 nodes.push(DrawPrimitiveNode { primitive, clip });
20943 }
20944
20945 let children: Vec<RenderNode> = nodes
20946 .iter()
20947 .map(|node| {
20948 RenderNode::Primitive(PrimitiveEntry {
20949 phase: PrimitivePhase::BeforeChildren,
20950 node: PrimitiveNode::Draw(node.clone()),
20951 })
20952 })
20953 .collect();
20954 let layer = crate::test_support::layer_node(
20955 bounds,
20956 ProjectiveTransform::identity(),
20957 graphics_layer,
20958 children,
20959 );
20960
20961 let mut rect_cache = HashMap::new();
20962 let mut requirements_cache = HashMap::new();
20963 let collected =
20964 collect_layer_contents(&layer, None, None, &mut rect_cache, &mut requirements_cache);
20965
20966 let local_layer = local_content_layer_for(&layer.graphics_layer);
20969 let mut expected = CompositorScene::new();
20970 for node in &nodes {
20971 let clip = resolve_primitive_clip(
20972 node.clip,
20973 bounds,
20974 &local_layer,
20975 None,
20976 PrimitiveClipSpace::Local,
20977 );
20978 if node.clip.is_some() && clip.is_none() {
20979 continue;
20980 }
20981 crate::pipeline::push_draw_primitive(
20982 &node.primitive,
20983 bounds,
20984 &local_layer,
20985 clip,
20986 &mut expected,
20987 None,
20988 false,
20989 );
20990 }
20991
20992 assert!(
20993 collected.scene.shapes.len() >= 590,
20994 "the runs should engage the parallel branch: got {} shapes",
20995 collected.scene.shapes.len()
20996 );
20997 assert_eq!(collected.scene.shapes.len(), expected.shapes.len());
20998 assert_eq!(collected.scene.draw_ops, expected.draw_ops);
20999 assert_eq!(collected.scene.next_z, expected.next_z);
21000 assert!(
21001 collected
21002 .scene
21003 .shapes
21004 .iter()
21005 .all(|s| s.snap_anchor.is_none()),
21006 "a rotated layer must not rigid-snap; the reference scene assumes it"
21007 );
21008 for (index, (got, want)) in collected
21009 .scene
21010 .shapes
21011 .iter()
21012 .zip(&expected.shapes)
21013 .enumerate()
21014 {
21015 assert_eq!(got.rect, want.rect, "shape {index} rect");
21016 assert_eq!(got.local_rect, want.local_rect, "shape {index} local_rect");
21017 assert_eq!(got.quad, want.quad, "shape {index} quad");
21018 assert_eq!(got.snap_anchor, want.snap_anchor, "shape {index} snap");
21019 assert_eq!(got.brush, want.brush, "shape {index} brush");
21020 assert_eq!(got.shape, want.shape, "shape {index} shape");
21021 assert_eq!(got.stroke, want.stroke, "shape {index} stroke");
21022 assert_eq!(got.arc, want.arc, "shape {index} arc");
21023 assert_eq!(got.z_index, want.z_index, "shape {index} z");
21024 assert_eq!(got.clip, want.clip, "shape {index} clip");
21025 assert_eq!(got.blend_mode, want.blend_mode, "shape {index} blend");
21026 assert_eq!(
21027 got.motion_context_animated, want.motion_context_animated,
21028 "shape {index} motion flag"
21029 );
21030 }
21031 }
21032
21033 #[test]
21038 fn shape_run_collect_matches_per_primitive_emission_exactly() {
21039 assert_shape_run_collect_matches_per_primitive_emission();
21040 crate::normalized_scene::force_shape_run_parallel_for_tests(true);
21041 let outcome =
21042 std::panic::catch_unwind(assert_shape_run_collect_matches_per_primitive_emission);
21043 crate::normalized_scene::force_shape_run_parallel_for_tests(false);
21044 if let Err(payload) = outcome {
21045 std::panic::resume_unwind(payload);
21046 }
21047 }
21048
21049 #[test]
21050 fn non_translated_text_local_surface_keeps_linear_composite_resolve() {
21051 let layer = text_layer_with_style(
21052 AnnotatedString::from("gradient"),
21053 TextStyle::from_span_style(SpanStyle {
21054 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
21055 ..SpanStyle::default()
21056 }),
21057 );
21058 let requirements = layer_surface_requirements(&layer);
21059
21060 assert!(requirements
21061 .surface_requirements
21062 .contains(SurfaceRequirement::TextMaterialMask));
21063 assert_eq!(
21064 composite_sample_mode_for_requirements(false, false, requirements),
21065 CompositeSampleMode::Linear
21066 );
21067 }
21068
21069 #[test]
21070 fn inherited_translated_text_local_surface_uses_box4_layer_surface() {
21071 let layer = text_layer_with_style(
21072 AnnotatedString::from("shadow"),
21073 TextStyle::from_span_style(SpanStyle {
21074 shadow: Some(Shadow {
21075 color: Color::BLACK,
21076 offset: Point::new(1.0, 2.0),
21077 blur_radius: 3.0,
21078 }),
21079 ..SpanStyle::default()
21080 }),
21081 );
21082 let requirements = layer_surface_requirements(&layer);
21083
21084 assert!(requirements
21085 .surface_requirements
21086 .contains(SurfaceRequirement::TextMaterialMask));
21087 assert_eq!(
21088 composite_sample_mode_for_requirements(true, false, requirements),
21089 CompositeSampleMode::Box4
21090 );
21091 assert_eq!(
21092 layer_surface_target_scale(
21093 true,
21094 false,
21095 requirements,
21096 1.25,
21097 layer_surface_scale(&layer)
21098 ),
21099 SurfaceRequirementSet::default()
21100 .with(SurfaceRequirement::TextMaterialMask)
21101 .with(SurfaceRequirement::MotionStableCapture)
21102 .target_scale(1.25, 1.0)
21103 );
21104 }
21105
21106 #[test]
21107 fn translated_text_local_surface_inside_capture_keeps_parent_scale() {
21108 let layer = text_layer_with_style(
21109 AnnotatedString::from("shadow"),
21110 TextStyle::from_span_style(SpanStyle {
21111 shadow: Some(Shadow {
21112 color: Color::BLACK,
21113 offset: Point::new(1.0, 2.0),
21114 blur_radius: 3.0,
21115 }),
21116 ..SpanStyle::default()
21117 }),
21118 );
21119 let requirements = layer_surface_requirements(&layer);
21120
21121 assert_eq!(
21122 composite_sample_mode_for_requirements(true, true, requirements),
21123 CompositeSampleMode::Linear
21124 );
21125 assert_eq!(
21126 layer_surface_target_scale(true, true, requirements, 10.0, layer_surface_scale(&layer)),
21127 SurfaceRequirementSet::default()
21128 .with(SurfaceRequirement::TextMaterialMask)
21129 .target_scale(10.0, 1.0)
21130 );
21131 }
21132
21133 #[test]
21134 fn layer_surface_requirements_use_local_surface_for_gradient_and_stroke_text() {
21135 let cases = [
21136 (
21137 "draw_style",
21138 AnnotatedString::from("draw_style"),
21139 TextStyle::from_span_style(SpanStyle {
21140 draw_style: Some(TextDrawStyle::Stroke { width: 2.0 }),
21141 ..SpanStyle::default()
21142 }),
21143 ),
21144 (
21145 "gradient_brush",
21146 AnnotatedString::from("gradient"),
21147 TextStyle::from_span_style(SpanStyle {
21148 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
21149 ..SpanStyle::default()
21150 }),
21151 ),
21152 ];
21153
21154 for (label, text, text_style) in cases {
21155 let layer = text_layer_with_style(text, text_style);
21156 let requirements = layer_surface_requirements(&layer);
21157 assert!(
21158 requirements
21159 .surface_requirements
21160 .contains(SurfaceRequirement::TextMaterialMask),
21161 "{label} text should use a bounded local surface: {requirements:?}"
21162 );
21163 }
21164 }
21165
21166 #[test]
21167 fn layer_surface_requirements_use_local_surface_for_complex_text_effects() {
21168 let cases = [
21169 (
21170 "shadow",
21171 AnnotatedString::from("shadow"),
21172 TextStyle::from_span_style(SpanStyle {
21173 shadow: Some(Shadow {
21174 color: Color::BLACK,
21175 offset: Point::new(1.0, 2.0),
21176 blur_radius: 3.0,
21177 }),
21178 ..SpanStyle::default()
21179 }),
21180 ),
21181 (
21182 "background",
21183 AnnotatedString::from("background"),
21184 TextStyle::from_span_style(SpanStyle {
21185 background: Some(Color::BLACK),
21186 ..SpanStyle::default()
21187 }),
21188 ),
21189 (
21190 "baseline_shift",
21191 AnnotatedString::from("baseline_shift"),
21192 TextStyle::from_span_style(SpanStyle {
21193 baseline_shift: Some(BaselineShift::SUPERSCRIPT),
21194 ..SpanStyle::default()
21195 }),
21196 ),
21197 (
21198 "geometric_transform",
21199 AnnotatedString::from("geometric_transform"),
21200 TextStyle::from_span_style(SpanStyle {
21201 text_geometric_transform: Some(TextGeometricTransform {
21202 scale_x: 1.2,
21203 skew_x: 0.15,
21204 }),
21205 ..SpanStyle::default()
21206 }),
21207 ),
21208 (
21209 "letter_spacing",
21210 AnnotatedString::from("letter_spacing"),
21211 TextStyle::from_span_style(SpanStyle {
21212 letter_spacing: TextUnit::Em(0.2),
21213 ..SpanStyle::default()
21214 }),
21215 ),
21216 ];
21217
21218 for (label, text, text_style) in cases {
21219 let layer = text_layer_with_style(text, text_style);
21220 let requirements = layer_surface_requirements(&layer);
21221 assert!(
21222 requirements
21223 .surface_requirements
21224 .contains(SurfaceRequirement::TextMaterialMask),
21225 "{label} text should use a bounded local surface: {requirements:?}"
21226 );
21227 assert_eq!(
21228 requirements.direct_translation,
21229 Some(Point::default()),
21230 "{label} text should still classify as a direct translation"
21231 );
21232 }
21233 }
21234
21235 #[test]
21236 fn layer_surface_requirements_color_only_span_styles_use_direct_path() {
21237 let layer = text_layer_with_style(
21238 AnnotatedString {
21239 text: "styled".to_string(),
21240 span_styles: vec![RangeStyle {
21241 item: SpanStyle {
21242 color: Some(Color::BLACK),
21243 ..SpanStyle::default()
21244 },
21245 range: 0..3,
21246 }],
21247 ..AnnotatedString::default()
21248 },
21249 TextStyle::default(),
21250 );
21251 let requirements = layer_surface_requirements(&layer);
21252 assert!(
21253 !requirements
21254 .surface_requirements
21255 .contains(SurfaceRequirement::TextMaterialMask),
21256 "color-only span styles should render directly via software text raster colors"
21257 );
21258 }
21259
21260 #[test]
21261 fn layer_surface_requirements_keep_decoration_only_text_on_direct_path() {
21262 let layer = text_layer_with_style(
21263 AnnotatedString::from("decoration"),
21264 TextStyle::from_span_style(SpanStyle {
21265 text_decoration: Some(TextDecoration::UNDERLINE),
21266 ..SpanStyle::default()
21267 }),
21268 );
21269
21270 let requirements = layer_surface_requirements(&layer);
21271
21272 assert_eq!(requirements.direct_translation, Some(Point::default()));
21273 assert!(
21274 requirements
21275 .surface_requirements
21276 .contains(SurfaceRequirement::PixelStableComposite)
21277 && !requirements
21278 .surface_requirements
21279 .has_isolating_requirement(),
21280 "decoration-only text should not force an isolating layer surface: {requirements:?}"
21281 );
21282 }
21283
21284 #[test]
21285 fn direct_text_leaf_snaps_modifier_background_and_text_with_one_anchor() {
21286 let root = snapped_text_leaf_root(false, false);
21287 let mut rect_cache = HashMap::new();
21288 let mut requirements_cache = HashMap::new();
21289
21290 let collected =
21291 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21292
21293 assert_eq!(collected.scene.shapes.len(), 1);
21294 assert_eq!(collected.scene.images.len(), 1);
21295 assert_eq!(collected.scene.texts.len(), 1);
21296 let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
21297 assert_eq!(collected.scene.shapes[0].snap_anchor, expected_anchor);
21298 assert_eq!(collected.scene.images[0].snap_anchor, expected_anchor);
21299 assert_eq!(collected.scene.texts[0].snap_anchor, expected_anchor);
21300 }
21301
21302 #[test]
21303 fn animated_translated_content_text_leaf_uses_bounded_content_snap() {
21304 let root = snapped_text_leaf_root(true, true);
21305 let mut rect_cache = HashMap::new();
21306 let mut requirements_cache = HashMap::new();
21307
21308 let collected =
21309 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21310
21311 assert_eq!(collected.child_layers.len(), 1);
21312 assert!(collected.scene.shapes.is_empty());
21313 assert!(collected.scene.images.is_empty());
21314 assert!(collected.scene.texts.is_empty());
21315 assert!(collected.scene.effect_layers.is_empty());
21316 let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
21317 assert_eq!(
21318 collected.child_layers[0].snap_anchor, expected_anchor,
21319 "active translated leaf surface should keep the content-origin snap phase"
21320 );
21321 }
21322
21323 #[test]
21324 fn translated_content_assigns_motion_anchor_to_rotated_child_surface() {
21325 let mut child = snapped_text_leaf(false, false);
21326 child.graphics_layer.rotation_z = 5.0;
21327 child.transform_to_parent =
21328 cranpose_render_common::layer_transform::layer_transform_to_parent(
21329 child.local_bounds,
21330 Point::new(108.0, 3.0),
21331 &child.graphics_layer,
21332 );
21333 child.recompute_raster_cache_hashes();
21334 let mut root = test_layer(
21335 Rect {
21336 x: 0.0,
21337 y: 0.0,
21338 width: 320.0,
21339 height: 180.0,
21340 },
21341 vec![RenderNode::Layer(Box::new(child))],
21342 );
21343 root.translated_content_context = true;
21344 root.translated_content_offset = Point::new(0.0, -80.8);
21345 root.recompute_raster_cache_hashes();
21346 let mut rect_cache = HashMap::new();
21347 let mut requirements_cache = HashMap::new();
21348
21349 let collected =
21350 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21351
21352 assert_eq!(collected.child_layers.len(), 1);
21353 assert!(
21354 collected.child_layers[0].snap_anchor.is_some(),
21355 "a projective child still translates rigidly with its scrolling parent"
21356 );
21357 }
21358
21359 #[test]
21360 fn rested_translated_content_context_text_leaf_snaps_for_crisp_scroll_rest() {
21361 let root = snapped_text_leaf_root(false, true);
21362 let mut rect_cache = HashMap::new();
21363 let mut requirements_cache = HashMap::new();
21364
21365 let collected =
21366 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21367
21368 assert_eq!(collected.child_layers.len(), 0);
21369 assert_eq!(collected.scene.shapes.len(), 1);
21370 assert_eq!(collected.scene.images.len(), 1);
21371 assert_eq!(collected.scene.texts.len(), 1);
21372 assert_eq!(collected.scene.effect_layers.len(), 0);
21373 let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
21374 assert_eq!(
21375 collected.scene.shapes[0].snap_anchor, expected_anchor,
21376 "rested scroll content should snap back to device pixels"
21377 );
21378 assert_eq!(
21379 collected.scene.images[0].snap_anchor, expected_anchor,
21380 "rested scroll images should snap back to device pixels"
21381 );
21382 assert_eq!(
21383 collected.scene.texts[0].snap_anchor, expected_anchor,
21384 "rested scroll text should snap back to device pixels"
21385 );
21386 }
21387
21388 #[test]
21389 fn complex_text_uses_local_surface() {
21390 let root = translated_content_local_surface_root();
21391 let mut rect_cache = HashMap::new();
21392 let mut requirements_cache = HashMap::new();
21393
21394 let collected =
21395 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21396
21397 assert!(
21398 !collected.child_layers.is_empty(),
21399 "translated-content effectful text should render through a bounded local surface"
21400 );
21401 assert!(collected.scene.texts.is_empty());
21402 assert!(collected.scene.shadow_draws.is_empty());
21403 }
21404
21405 #[test]
21406 fn translated_content_surface_composite_uses_scroll_content_snap_anchor() {
21407 let mut root = translated_content_local_surface_root();
21408 let scroll_offset = Point::new(0.0, -18.5);
21409 let Some(RenderNode::Layer(translated_content)) = root.children.get_mut(0) else {
21410 panic!("expected translated content layer");
21411 };
21412 translated_content.translated_content_offset = scroll_offset;
21413 let Some(RenderNode::Layer(effectful_text)) = translated_content.children.get_mut(0) else {
21414 panic!("expected effectful text layer");
21415 };
21416 effectful_text.transform_to_parent =
21417 effectful_text
21418 .transform_to_parent
21419 .then(ProjectiveTransform::translation(
21420 scroll_offset.x,
21421 scroll_offset.y,
21422 ));
21423
21424 let mut rect_cache = HashMap::new();
21425 let mut requirements_cache = HashMap::new();
21426 let collected =
21427 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21428
21429 assert_eq!(collected.child_layers.len(), 1);
21430 assert_eq!(
21431 collected.child_layers[0].snap_anchor,
21432 Some(SnapAnchor::rigid(Point::new(14.25, -2.0))),
21433 "isolated scrolled descendants must composite with the same content-origin snap phase"
21434 );
21435 }
21436
21437 #[test]
21438 fn animated_translated_content_surface_composite_uses_scroll_content_snap_anchor() {
21439 let mut root = translated_content_local_surface_root();
21440 let scroll_offset = Point::new(0.0, -18.5);
21441 let Some(RenderNode::Layer(translated_content)) = root.children.get_mut(0) else {
21442 panic!("expected translated content layer");
21443 };
21444 translated_content.motion_context_animated = true;
21445 translated_content.translated_content_offset = scroll_offset;
21446 let Some(RenderNode::Layer(effectful_text)) = translated_content.children.get_mut(0) else {
21447 panic!("expected effectful text layer");
21448 };
21449 effectful_text.transform_to_parent =
21450 effectful_text
21451 .transform_to_parent
21452 .then(ProjectiveTransform::translation(
21453 scroll_offset.x,
21454 scroll_offset.y,
21455 ));
21456
21457 let mut rect_cache = HashMap::new();
21458 let mut requirements_cache = HashMap::new();
21459 let collected =
21460 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21461
21462 assert_eq!(collected.child_layers.len(), 1);
21463 assert_eq!(
21464 collected.child_layers[0].snap_anchor,
21465 Some(SnapAnchor::rigid(Point::new(14.25, 16.5))),
21466 "animated translated content should composite the stable local surface at the viewport-origin snap phase"
21467 );
21468 }
21469
21470 #[test]
21471 fn translated_text_material_effect_layer_uses_scroll_content_snap_anchor() {
21472 let mut layer = text_layer_with_style(
21473 AnnotatedString::from("gradient"),
21474 TextStyle::from_span_style(SpanStyle {
21475 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
21476 ..SpanStyle::default()
21477 }),
21478 );
21479 layer.translated_content_context = true;
21480 layer.translated_content_offset = Point::new(0.0, -18.5);
21481 let mut rect_cache = HashMap::new();
21482 let mut requirements_cache = HashMap::new();
21483
21484 let collected =
21485 collect_layer_contents(&layer, None, None, &mut rect_cache, &mut requirements_cache);
21486
21487 assert_eq!(collected.scene.effect_layers.len(), 1);
21488 assert_eq!(
21489 composite_sample_mode_for_effect_layer(&collected.scene.effect_layers[0]),
21490 CompositeSampleMode::Box4
21491 );
21492 assert_eq!(
21493 collected.scene.effect_layers[0].snap_anchor,
21494 Some(SnapAnchor::rigid(Point::new(0.0, -18.5))),
21495 "text material surfaces must composite with the scroll content-origin snap phase"
21496 );
21497 }
21498
21499 #[test]
21500 fn translated_layer_surface_capture_does_not_restart_local_picture_for_shadow_text() {
21501 let mut layer = text_layer_with_style(
21502 AnnotatedString::from("shadow"),
21503 TextStyle::from_span_style(SpanStyle {
21504 shadow: Some(Shadow {
21505 color: Color::BLACK,
21506 offset: Point::new(1.0, 2.0),
21507 blur_radius: 3.0,
21508 }),
21509 ..SpanStyle::default()
21510 }),
21511 );
21512 layer.translated_content_context = true;
21513 let mut rect_cache = HashMap::new();
21514 let mut requirements_cache = HashMap::new();
21515
21516 let collected = collect_layer_contents_with_translation_context(
21517 &layer,
21518 None,
21519 None,
21520 TranslationRenderContext {
21521 inherited_content_translation: false,
21522 surface_capture_active: true,
21523 local_picture_capture_active: true,
21524 ..TranslationRenderContext::default()
21525 },
21526 &mut rect_cache,
21527 &mut requirements_cache,
21528 );
21529
21530 assert!(
21531 collected.scene.effect_layers.is_empty(),
21532 "a translated layer surface already provides the stable local capture"
21533 );
21534 assert_eq!(collected.scene.shadow_draws.len(), 1);
21535 assert_eq!(collected.scene.texts.len(), 1);
21536 assert!(
21537 !collected.scene.texts[0].translated_content_context,
21538 "text inside an active motion-stable capture must raster in capture-local coordinates"
21539 );
21540 }
21541
21542 #[test]
21543 fn translated_layer_surface_capture_keeps_only_material_effect_layers() {
21544 let mut layer = text_layer_with_style(
21545 AnnotatedString::from("gradient"),
21546 TextStyle::from_span_style(SpanStyle {
21547 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
21548 ..SpanStyle::default()
21549 }),
21550 );
21551 layer.translated_content_context = true;
21552 let mut rect_cache = HashMap::new();
21553 let mut requirements_cache = HashMap::new();
21554
21555 let collected = collect_layer_contents_with_translation_context(
21556 &layer,
21557 None,
21558 None,
21559 TranslationRenderContext {
21560 inherited_content_translation: false,
21561 surface_capture_active: true,
21562 local_picture_capture_active: true,
21563 ..TranslationRenderContext::default()
21564 },
21565 &mut rect_cache,
21566 &mut requirements_cache,
21567 );
21568
21569 assert_eq!(collected.scene.effect_layers.len(), 1);
21570 assert!(
21571 collected.scene.effect_layers[0]
21572 .requirements
21573 .contains(SurfaceRequirement::MotionStableCapture),
21574 "translated text materials still need motion-stable resolve semantics inside a stable capture"
21575 );
21576 assert_eq!(
21577 composite_sample_mode_for_effect_layer(&collected.scene.effect_layers[0]),
21578 CompositeSampleMode::Box4
21579 );
21580 assert_eq!(
21581 effect_layer_target_scale(&collected.scene.effect_layers[0], 10.0),
21582 10.0
21583 );
21584 assert!(collected.scene.effect_layers[0].effect.is_some());
21585 }
21586
21587 #[test]
21588 fn translated_viewport_surface_does_not_add_plain_local_picture_capture() {
21589 let mut layer = text_layer_with_style(
21590 AnnotatedString::from("shadow"),
21591 TextStyle::from_span_style(SpanStyle {
21592 shadow: Some(Shadow {
21593 color: Color::BLACK,
21594 offset: Point::new(1.0, 2.0),
21595 blur_radius: 3.0,
21596 }),
21597 ..SpanStyle::default()
21598 }),
21599 );
21600 layer.translated_content_context = true;
21601 layer.motion_context_animated = true;
21602 let mut rect_cache = HashMap::new();
21603 let mut requirements_cache = HashMap::new();
21604
21605 let collected = collect_layer_contents_with_translation_context(
21606 &layer,
21607 None,
21608 None,
21609 TranslationRenderContext {
21610 surface_capture_active: true,
21611 ..TranslationRenderContext::default()
21612 },
21613 &mut rect_cache,
21614 &mut requirements_cache,
21615 );
21616
21617 assert_eq!(
21618 collected.scene.effect_layers.len(),
21619 0,
21620 "plain translated content inside a viewport surface should not be captured again"
21621 );
21622 assert_eq!(collected.scene.shadow_draws.len(), 1);
21623 assert_eq!(collected.scene.texts.len(), 1);
21624 }
21625
21626 #[test]
21627 fn static_pure_text_leaf_snaps_without_sibling_draw_primitives() {
21628 let root = pure_text_leaf_root(false, false);
21629 let mut rect_cache = HashMap::new();
21630 let mut requirements_cache = HashMap::new();
21631
21632 let collected =
21633 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21634
21635 assert_eq!(collected.scene.texts.len(), 1);
21636 assert!(
21637 collected.scene.texts[0].snap_anchor.is_some(),
21638 "idle pure text leaves should participate in rigid snap anchoring"
21639 );
21640 }
21641
21642 #[test]
21643 fn animated_pure_text_leaf_stays_unsnapped() {
21644 let root = pure_text_leaf_root(true, false);
21645 let mut rect_cache = HashMap::new();
21646 let mut requirements_cache = HashMap::new();
21647
21648 let collected =
21649 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21650
21651 assert_eq!(collected.scene.texts.len(), 1);
21652 assert_eq!(collected.scene.texts[0].snap_anchor, None);
21653 }
21654
21655 #[test]
21656 fn animated_translated_pure_text_uses_bounded_content_snap() {
21657 let root = pure_text_leaf_root(true, true);
21658 let mut rect_cache = HashMap::new();
21659 let mut requirements_cache = HashMap::new();
21660
21661 let collected =
21662 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21663
21664 assert_eq!(collected.child_layers.len(), 1);
21665 assert!(collected.scene.texts.is_empty());
21666 assert!(collected.scene.effect_layers.is_empty());
21667 assert_snap_anchor_close(
21668 collected.child_layers[0].snap_anchor,
21669 Point::new(11.4, 23.6),
21670 "animated translated pure text should use the bounded content snap phase",
21671 );
21672 }
21673
21674 #[test]
21675 fn rested_translated_pure_text_leaf_snaps_for_crisp_scroll_rest() {
21676 let root = pure_text_leaf_root(false, true);
21677 let mut rect_cache = HashMap::new();
21678 let mut requirements_cache = HashMap::new();
21679
21680 let collected =
21681 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21682
21683 assert_eq!(collected.child_layers.len(), 0);
21684 assert_eq!(collected.scene.texts.len(), 1);
21685 assert_eq!(collected.scene.effect_layers.len(), 0);
21686 assert_snap_anchor_close(
21687 collected.scene.texts[0].snap_anchor,
21688 Point::new(11.4, 23.6),
21689 "rested translated text should snap to device pixels",
21690 );
21691 }
21692
21693 #[test]
21694 fn static_gpu_effect_text_leaf_stays_unsnapped() {
21695 let root = text_layer_with_style(
21696 AnnotatedString::from("Gradient"),
21697 TextStyle::from_span_style(SpanStyle {
21698 brush: Some(Brush::linear_gradient(vec![
21699 Color(0.2, 0.8, 1.0, 1.0),
21700 Color(1.0, 0.7, 0.4, 1.0),
21701 ])),
21702 draw_style: Some(TextDrawStyle::Stroke { width: 2.5 }),
21703 ..SpanStyle::default()
21704 }),
21705 );
21706 let mut rect_cache = HashMap::new();
21707 let mut requirements_cache = HashMap::new();
21708
21709 let collected =
21710 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21711
21712 assert_eq!(collected.scene.texts.len(), 1);
21713 assert_eq!(
21714 collected.scene.texts[0].snap_anchor, None,
21715 "gpu text-effect leaves must not take the rigid text snap path"
21716 );
21717 assert_eq!(
21718 collected.scene.effect_layers.len(),
21719 1,
21720 "gradient stroke text should still emit a runtime shader effect layer"
21721 );
21722 }
21723
21724 #[test]
21725 fn layer_surface_requirements_keep_shape_plus_direct_child_on_direct_path() {
21726 let mut child = test_layer(
21727 Rect {
21728 x: 0.0,
21729 y: 0.0,
21730 width: 40.0,
21731 height: 20.0,
21732 },
21733 vec![RenderNode::Primitive(PrimitiveEntry {
21734 phase: PrimitivePhase::BeforeChildren,
21735 node: PrimitiveNode::Draw(DrawPrimitiveNode {
21736 primitive: DrawPrimitive::Rect {
21737 rect: Rect {
21738 x: 0.0,
21739 y: 0.0,
21740 width: 40.0,
21741 height: 20.0,
21742 },
21743 brush: Brush::solid(Color::WHITE),
21744 stroke: None,
21745 },
21746 clip: None,
21747 }),
21748 })],
21749 );
21750 child.transform_to_parent = ProjectiveTransform::translation(8.0, 6.0);
21751
21752 let layer = test_layer(
21753 Rect {
21754 x: 0.0,
21755 y: 0.0,
21756 width: 64.0,
21757 height: 32.0,
21758 },
21759 vec![
21760 RenderNode::Primitive(PrimitiveEntry {
21761 phase: PrimitivePhase::BeforeChildren,
21762 node: PrimitiveNode::Draw(DrawPrimitiveNode {
21763 primitive: DrawPrimitive::Rect {
21764 rect: Rect {
21765 x: 0.0,
21766 y: 0.0,
21767 width: 64.0,
21768 height: 32.0,
21769 },
21770 brush: Brush::solid(Color::BLACK),
21771 stroke: None,
21772 },
21773 clip: None,
21774 }),
21775 }),
21776 RenderNode::Layer(Box::new(child)),
21777 ],
21778 );
21779
21780 let requirements = layer_surface_requirements(&layer);
21781
21782 assert_eq!(requirements.direct_translation, Some(Point::default()));
21783 assert!(!requirements
21784 .surface_requirements
21785 .contains(SurfaceRequirement::MixedDirectContent));
21786 assert!(!requirements
21787 .surface_requirements
21788 .has_isolating_requirement());
21789 }
21790
21791 #[test]
21792 fn collect_layer_contents_translates_direct_text_rects_into_parent_space() {
21793 let mut child = text_layer_with_style(
21794 AnnotatedString::from("direct"),
21795 TextStyle::from_span_style(SpanStyle {
21796 text_decoration: Some(TextDecoration::UNDERLINE),
21797 ..SpanStyle::default()
21798 }),
21799 );
21800 child.transform_to_parent = ProjectiveTransform::translation(9.0, 7.0);
21801
21802 let parent = test_layer(
21803 Rect {
21804 x: 0.0,
21805 y: 0.0,
21806 width: 64.0,
21807 height: 32.0,
21808 },
21809 vec![RenderNode::Layer(Box::new(child))],
21810 );
21811
21812 let mut rect_cache = HashMap::new();
21813 let mut requirements_cache = HashMap::new();
21814 let collected = with_test_app_context(|| {
21815 collect_layer_contents(
21816 &parent,
21817 None,
21818 None,
21819 &mut rect_cache,
21820 &mut requirements_cache,
21821 )
21822 });
21823
21824 assert!(
21825 collected.child_layers.is_empty(),
21826 "decoration-only text child should collapse directly into the parent scene"
21827 );
21828 assert_eq!(collected.scene.texts.len(), 1, "expected one text draw");
21829 let text = &collected.scene.texts[0];
21830 assert!(
21831 text.rect.x >= 9.0 && text.rect.y >= 7.0,
21832 "collapsed text rect should be translated into parent space, got {:?}",
21833 text.rect
21834 );
21835 assert!(
21836 collected
21837 .scene
21838 .shapes
21839 .iter()
21840 .any(|shape| shape.rect.y >= 7.0),
21841 "collapsed underline geometry should also be translated into parent space"
21842 );
21843 }
21844
21845 #[test]
21846 fn normalized_scene_keeps_lazy_after_bound_text_for_prewarm() {
21847 use std::cell::RefCell;
21848
21849 fn collect_graph_text_labels(layer: &LayerNode, labels: &mut Vec<String>) {
21850 for child in &layer.children {
21851 match child {
21852 RenderNode::Primitive(PrimitiveEntry {
21853 node: PrimitiveNode::Text(text),
21854 ..
21855 }) => labels.push(text.text.text.clone()),
21856 RenderNode::Layer(child_layer) => {
21857 collect_graph_text_labels(child_layer, labels)
21858 }
21859 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => {}
21860 }
21861 }
21862 }
21863
21864 let state_holder: Rc<RefCell<Option<LazyListState>>> = Rc::new(RefCell::new(None));
21865 let state_holder_for_comp = state_holder.clone();
21866 let mut composition = cranpose_ui::run_test_composition(move || {
21867 let list_state = remember_lazy_list_state();
21868 *state_holder_for_comp.borrow_mut() = Some(list_state);
21869 let mut spec = LazyColumnSpec::new()
21870 .vertical_arrangement(cranpose_ui::LinearArrangement::SpacedBy(6.0));
21871 spec.beyond_bounds_item_count = 0;
21872 LazyColumn(Modifier::empty().height(96.0), list_state, spec, |scope| {
21873 scope.items(
21874 12,
21875 None::<fn(usize) -> u64>,
21876 None::<fn(usize) -> u64>,
21877 |index| {
21878 Text(
21879 format!("WarmRow {index}"),
21880 Modifier::empty().height(32.0),
21881 TextStyle::default(),
21882 );
21883 },
21884 );
21885 });
21886 });
21887
21888 let list_state = (*state_holder.borrow()).expect("lazy list state should be captured");
21889 list_state.scroll_to_item(4, 0.0);
21890
21891 let root = composition.root().expect("lazy column root");
21892 let handle = composition.runtime_handle();
21893 let mut applier = composition.applier_mut();
21894 applier.set_runtime_handle(handle);
21895 let _ = applier
21896 .compute_layout(
21897 root,
21898 Size {
21899 width: 240.0,
21900 height: 240.0,
21901 },
21902 )
21903 .expect("lazy column layout");
21904 let graph = build_graph_from_applier(&mut applier, root, 1.0).expect("lazy column graph");
21905 applier.clear_runtime_handle();
21906 let mut graph_labels = Vec::new();
21907 collect_graph_text_labels(&graph.root, &mut graph_labels);
21908
21909 let visible_indices: Vec<_> = list_state
21910 .layout_info()
21911 .visible_items_info
21912 .iter()
21913 .map(|item| item.index)
21914 .collect();
21915 assert_eq!(
21916 visible_indices,
21917 vec![4, 5, 6],
21918 "test setup expects exactly three viewport-visible rows"
21919 );
21920
21921 let mut rect_cache = HashMap::new();
21922 let mut requirements_cache = HashMap::new();
21923 let collected = with_test_app_context(|| {
21924 collect_layer_contents(
21925 &graph.root,
21926 None,
21927 None,
21928 &mut rect_cache,
21929 &mut requirements_cache,
21930 )
21931 });
21932 let root_text_labels: Vec<_> = collected
21933 .scene
21934 .texts
21935 .iter()
21936 .map(|text| text.text.text.clone())
21937 .collect();
21938 let child_layer_count = collected.child_layers.len();
21939 let warm_text = collected
21940 .scene
21941 .texts
21942 .iter()
21943 .find(|text| text.text.text == "WarmRow 7")
21944 .unwrap_or_else(|| {
21945 panic!(
21946 "after-bound lazy text should reach WGPU scene collection; graph_texts={graph_labels:?} root_texts={root_text_labels:?} child_layers={child_layer_count}"
21947 )
21948 });
21949
21950 assert!(
21951 warm_text.rect.y >= 96.0,
21952 "after-bound text should be below the viewport, got {:?}",
21953 warm_text.rect
21954 );
21955 assert_eq!(
21956 visible_draw_rect(warm_text.rect, warm_text.clip),
21957 None,
21958 "after-bound text should remain clipped away for drawing while staying available for glyph prewarm"
21959 );
21960 assert!(
21961 text_draw_should_prewarm_in_viewport(
21962 warm_text.rect,
21963 warm_text.clip,
21964 ViewportUniformParams {
21965 width: 240,
21966 height: 96,
21967 offset: [0.0, 0.0],
21968 },
21969 1.0,
21970 ),
21971 "after-bound text inside the warm window must be selected by WGPU prewarm"
21972 );
21973 }
21974
21975 #[test]
21976 fn direct_translation_accepts_nearly_identity_axis_scale_noise() {
21977 let local_bounds = Rect {
21978 x: 0.0,
21979 y: 0.0,
21980 width: 393.3,
21981 height: 16.8,
21982 };
21983 let quad = [
21984 [10.0, 78.399_994],
21985 [403.3, 78.399_994],
21986 [10.0, 95.2],
21987 [403.3, 95.2],
21988 ];
21989 let transform = ProjectiveTransform::from_rect_to_quad(local_bounds, quad);
21990
21991 assert_eq!(
21992 direct_translation(transform),
21993 Some(Point::new(10.0, 78.399_994)),
21994 );
21995 }
21996
21997 #[test]
21998 fn layer_surface_requirements_keep_shape_plus_isolating_child_as_mixed_content() {
21999 let mut child = test_layer(
22000 Rect {
22001 x: 0.0,
22002 y: 0.0,
22003 width: 24.0,
22004 height: 18.0,
22005 },
22006 vec![RenderNode::Primitive(PrimitiveEntry {
22007 phase: PrimitivePhase::BeforeChildren,
22008 node: PrimitiveNode::Draw(DrawPrimitiveNode {
22009 primitive: DrawPrimitive::Rect {
22010 rect: Rect {
22011 x: 0.0,
22012 y: 0.0,
22013 width: 24.0,
22014 height: 18.0,
22015 },
22016 brush: Brush::solid(Color::WHITE),
22017 stroke: None,
22018 },
22019 clip: None,
22020 }),
22021 })],
22022 );
22023 child.transform_to_parent = ProjectiveTransform::translation(8.0, 6.0);
22024 child.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
22025
22026 let layer = test_layer(
22027 Rect {
22028 x: 0.0,
22029 y: 0.0,
22030 width: 64.0,
22031 height: 32.0,
22032 },
22033 vec![
22034 RenderNode::Primitive(PrimitiveEntry {
22035 phase: PrimitivePhase::BeforeChildren,
22036 node: PrimitiveNode::Draw(DrawPrimitiveNode {
22037 primitive: DrawPrimitive::Rect {
22038 rect: Rect {
22039 x: 0.0,
22040 y: 0.0,
22041 width: 64.0,
22042 height: 32.0,
22043 },
22044 brush: Brush::solid(Color::BLACK),
22045 stroke: None,
22046 },
22047 clip: None,
22048 }),
22049 }),
22050 RenderNode::Layer(Box::new(child)),
22051 ],
22052 );
22053
22054 let requirements = layer_surface_requirements(&layer);
22055
22056 assert!(requirements
22057 .surface_requirements
22058 .contains(SurfaceRequirement::MixedDirectContent));
22059 assert!(!requirements
22060 .surface_requirements
22061 .has_isolating_requirement());
22062 }
22063
22064 #[test]
22065 fn build_scene_window_filters_and_translates_items() {
22066 let mut shape = test_shape(6, BlendMode::SrcOver);
22067 shape.rect.x = 12.0;
22068 shape.rect.y = 25.0;
22069 shape.local_rect.x = 12.0;
22070 shape.local_rect.y = 25.0;
22071 shape.quad = [[12.0, 25.0], [20.0, 25.0], [12.0, 33.0], [20.0, 33.0]];
22072 shape.clip = Some(Rect {
22073 x: 11.0,
22074 y: 24.0,
22075 width: 10.0,
22076 height: 10.0,
22077 });
22078
22079 let mut image = test_image(8, BlendMode::SrcOver);
22080 image.rect.x = 18.0;
22081 image.rect.y = 27.0;
22082 image.local_rect.x = 18.0;
22083 image.local_rect.y = 27.0;
22084 image.quad = [[18.0, 27.0], [26.0, 27.0], [18.0, 35.0], [26.0, 35.0]];
22085
22086 let mut text = test_text(9);
22087 text.rect.x = 16.0;
22088 text.rect.y = 29.0;
22089 text.clip = Some(Rect {
22090 x: 15.0,
22091 y: 28.0,
22092 width: 9.0,
22093 height: 6.0,
22094 });
22095
22096 let mut shadow_shape = test_shape(7, BlendMode::SrcOver);
22097 shadow_shape.rect.x = 14.0;
22098 shadow_shape.rect.y = 26.0;
22099 shadow_shape.local_rect.x = 14.0;
22100 shadow_shape.local_rect.y = 26.0;
22101 shadow_shape.quad = [[14.0, 26.0], [22.0, 26.0], [14.0, 34.0], [22.0, 34.0]];
22102 let mut shadow = test_shadow_draw(vec![(shadow_shape, BlendMode::SrcOver)]);
22103 shadow.z_index = 7;
22104
22105 let mut nested_effect = effect_layer(6, 10);
22106 nested_effect.rect.x = 13.0;
22107 nested_effect.rect.y = 24.0;
22108 nested_effect.clip = Some(Rect {
22109 x: 15.0,
22110 y: 25.0,
22111 width: 4.0,
22112 height: 5.0,
22113 });
22114
22115 let mut nested_backdrop = backdrop_layer(8);
22116 nested_backdrop.rect.x = 17.0;
22117 nested_backdrop.rect.y = 26.0;
22118 nested_backdrop.clip = Some(Rect {
22119 x: 18.0,
22120 y: 27.0,
22121 width: 3.0,
22122 height: 4.0,
22123 });
22124
22125 let window = build_scene_window(
22126 SceneWindowSource {
22127 shapes: &[test_shape(4, BlendMode::SrcOver), shape],
22128 brushes: &[],
22129 images: &[image],
22130 texts: &[text],
22131 shadow_draws: &[shadow],
22132 draw_ops: &[],
22133 effect_layers: &[effect_layer(2, 4), nested_effect.clone()],
22134 backdrop_layers: &[backdrop_layer(4), nested_backdrop.clone()],
22135 },
22136 5,
22137 10,
22138 Rect {
22139 x: 10.0,
22140 y: 20.0,
22141 width: 20.0,
22142 height: 20.0,
22143 },
22144 );
22145
22146 assert_eq!(window.shapes.len(), 1);
22147 assert_eq!(
22148 window.shapes[0].rect,
22149 Rect {
22150 x: 2.0,
22151 y: 5.0,
22152 width: 8.0,
22153 height: 8.0,
22154 }
22155 );
22156 assert_eq!(
22157 window.shapes[0].clip,
22158 Some(Rect {
22159 x: 1.0,
22160 y: 4.0,
22161 width: 10.0,
22162 height: 10.0,
22163 })
22164 );
22165 assert_eq!(window.images.len(), 1);
22166 assert_eq!(window.images[0].rect.x, 8.0);
22167 assert_eq!(window.images[0].rect.y, 7.0);
22168 assert_eq!(window.texts.len(), 1);
22169 assert_eq!(window.texts[0].rect.x, 6.0);
22170 assert_eq!(window.texts[0].rect.y, 9.0);
22171 assert_eq!(
22172 window.texts[0].clip,
22173 Some(Rect {
22174 x: 5.0,
22175 y: 8.0,
22176 width: 9.0,
22177 height: 6.0,
22178 })
22179 );
22180 assert_eq!(window.shadow_draws.len(), 1);
22181 assert_eq!(window.shadow_draws[0].shapes[0].0.rect.x, 4.0);
22182 assert_eq!(window.shadow_draws[0].shapes[0].0.rect.y, 6.0);
22183 assert_eq!(window.effect_layers.len(), 1);
22184 assert_eq!(
22185 window.effect_layers[0].rect,
22186 Rect {
22187 x: 3.0,
22188 y: 4.0,
22189 width: 10.0,
22190 height: 10.0,
22191 }
22192 );
22193 assert_eq!(
22194 window.effect_layers[0].clip,
22195 Some(Rect {
22196 x: 5.0,
22197 y: 5.0,
22198 width: 4.0,
22199 height: 5.0,
22200 })
22201 );
22202 assert_eq!(window.backdrop_layers.len(), 1);
22203 assert_eq!(
22204 window.backdrop_layers[0].rect,
22205 Rect {
22206 x: 7.0,
22207 y: 6.0,
22208 width: 10.0,
22209 height: 10.0,
22210 }
22211 );
22212 assert_eq!(
22213 window.backdrop_layers[0].clip,
22214 Some(Rect {
22215 x: 8.0,
22216 y: 7.0,
22217 width: 3.0,
22218 height: 4.0,
22219 })
22220 );
22221 }
22222
22223 #[test]
22224 fn filtered_effect_layer_index_counts_only_window_members() {
22225 let effects = vec![
22226 effect_layer(0, 2),
22227 effect_layer(5, 12),
22228 effect_layer(6, 10),
22229 effect_layer(14, 20),
22230 ];
22231
22232 assert_eq!(filtered_effect_layer_index(&effects, 1, 5, 12), Some(0));
22233 assert_eq!(filtered_effect_layer_index(&effects, 2, 5, 12), Some(1));
22234 assert_eq!(filtered_effect_layer_index(&effects, 3, 5, 12), None);
22235 }
22236
22237 #[test]
22238 fn blend_mode_support_matrix_is_explicit() {
22239 assert!(is_blend_mode_supported(BlendMode::Src));
22240 assert!(is_blend_mode_supported(BlendMode::SrcOver));
22241 assert!(is_blend_mode_supported(BlendMode::DstOut));
22242 assert!(!is_blend_mode_supported(BlendMode::Clear));
22243 assert!(!is_blend_mode_supported(BlendMode::Multiply));
22244 }
22245
22246 #[test]
22247 fn collect_non_effect_segment_items_preserves_global_z_order() {
22248 let shapes = vec![
22249 test_shape(3, BlendMode::SrcOver),
22250 test_shape(1, BlendMode::DstOut),
22251 ];
22252 let images = vec![test_image(2, BlendMode::SrcOver)];
22253 let texts = vec![test_text(0)];
22254 let shadows: Vec<ShadowDraw> = Vec::new();
22255 let draw_ops = test_draw_ops(&shapes, &images, &texts, &shadows);
22256
22257 let mut scratch = Vec::new();
22258 collect_non_effect_segment_items(
22259 &shapes,
22260 &images,
22261 &texts,
22262 &shadows,
22263 &draw_ops,
22264 0,
22265 4,
22266 &[],
22267 100,
22268 100,
22269 1.0,
22270 &mut scratch,
22271 );
22272 let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
22273 assert_eq!(
22274 items,
22275 vec![
22276 SegmentDrawItem::Text(0),
22277 SegmentDrawItem::Shape(1),
22278 SegmentDrawItem::Image(0),
22279 SegmentDrawItem::Shape(0),
22280 ]
22281 );
22282 }
22283
22284 #[test]
22285 fn collect_non_effect_segment_items_filters_effect_ranges() {
22286 let shapes = vec![
22287 test_shape(1, BlendMode::SrcOver),
22288 test_shape(3, BlendMode::DstOut),
22289 ];
22290 let images = vec![test_image(2, BlendMode::SrcOver)];
22291 let texts = vec![test_text(4)];
22292 let shadows: Vec<ShadowDraw> = Vec::new();
22293 let draw_ops = test_draw_ops(&shapes, &images, &texts, &shadows);
22294 let effect_ranges = [std::ops::Range { start: 2, end: 4 }];
22295
22296 let mut scratch = Vec::new();
22297 collect_non_effect_segment_items(
22298 &shapes,
22299 &images,
22300 &texts,
22301 &shadows,
22302 &draw_ops,
22303 0,
22304 5,
22305 &effect_ranges,
22306 100,
22307 100,
22308 1.0,
22309 &mut scratch,
22310 );
22311 let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
22312 assert_eq!(
22313 items,
22314 vec![SegmentDrawItem::Shape(0), SegmentDrawItem::Text(0)]
22315 );
22316 }
22317
22318 #[test]
22319 fn collect_non_effect_segment_items_culls_offscreen_shapes_but_keeps_text_prewarm() {
22320 let mut shape = test_shape(0, BlendMode::SrcOver);
22321 shape.rect.y = 160.0;
22322 shape.local_rect.y = 160.0;
22323 shape.quad = [[0.0, 160.0], [8.0, 160.0], [0.0, 168.0], [8.0, 168.0]];
22324
22325 let shapes = vec![shape];
22326 let images = Vec::new();
22327 let mut text = test_text(1);
22328 text.rect.y = 160.0;
22329 let texts = vec![text];
22330 let shadows: Vec<ShadowDraw> = Vec::new();
22331 let draw_ops = test_draw_ops(&shapes, &images, &texts, &shadows);
22332
22333 let mut scratch = Vec::new();
22334 collect_non_effect_segment_items(
22335 &shapes,
22336 &images,
22337 &texts,
22338 &shadows,
22339 &draw_ops,
22340 0,
22341 2,
22342 &[],
22343 100,
22344 100,
22345 1.0,
22346 &mut scratch,
22347 );
22348
22349 let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
22350 assert_eq!(items, vec![SegmentDrawItem::Text(0)]);
22351 }
22352
22353 #[test]
22354 fn segment_command_iter_merges_non_conflicting_batches_into_one_chunk() {
22355 let ordered_items = vec![
22356 (0, SegmentDrawItem::Shape(0)),
22357 (1, SegmentDrawItem::Image(0)),
22358 (2, SegmentDrawItem::Text(0)),
22359 ];
22360 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
22361 let images = vec![test_image(1, BlendMode::DstOut)];
22362
22363 let commands: Vec<_> = SegmentCommandIter::new(
22364 &ordered_items,
22365 &shapes,
22366 &images,
22367 ShapeBatchLimits::desktop(),
22368 )
22369 .collect();
22370
22371 assert_eq!(
22372 commands,
22373 vec![SegmentRenderCommand::DrawChunk(chunk(&[
22374 SegmentBatchPlan::Shape {
22375 start: 0,
22376 end: 1,
22377 blend_mode: BlendMode::SrcOver,
22378 },
22379 SegmentBatchPlan::Image {
22380 start: 1,
22381 end: 2,
22382 blend_mode: BlendMode::DstOut,
22383 },
22384 SegmentBatchPlan::Text { start: 2, end: 3 },
22385 ]))]
22386 );
22387 }
22388
22389 #[test]
22390 fn segment_command_iter_keeps_layer_composites_in_ordered_draw_chunk() {
22391 let ordered_items = vec![
22392 (0, SegmentDrawItem::Shape(0)),
22393 (1, SegmentDrawItem::Composite(0)),
22394 (2, SegmentDrawItem::Image(0)),
22395 (3, SegmentDrawItem::Composite(1)),
22396 (4, SegmentDrawItem::Text(0)),
22397 ];
22398 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
22399 let images = vec![test_image(2, BlendMode::SrcOver)];
22400
22401 let commands: Vec<_> = SegmentCommandIter::new(
22402 &ordered_items,
22403 &shapes,
22404 &images,
22405 ShapeBatchLimits::desktop(),
22406 )
22407 .collect();
22408
22409 assert_eq!(
22410 commands,
22411 vec![SegmentRenderCommand::DrawChunk(chunk(&[
22412 SegmentBatchPlan::Shape {
22413 start: 0,
22414 end: 1,
22415 blend_mode: BlendMode::SrcOver,
22416 },
22417 SegmentBatchPlan::Composite { start: 1, end: 2 },
22418 SegmentBatchPlan::Image {
22419 start: 2,
22420 end: 3,
22421 blend_mode: BlendMode::SrcOver,
22422 },
22423 SegmentBatchPlan::Composite { start: 3, end: 4 },
22424 SegmentBatchPlan::Text { start: 4, end: 5 },
22425 ]))]
22426 );
22427 }
22428
22429 #[test]
22430 fn retain_renderable_shadow_items_culls_invisible_shadow_boundaries() {
22431 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
22432 let images = vec![test_image(2, BlendMode::SrcOver)];
22433 let mut shadow_shape = test_shape(1, BlendMode::SrcOver);
22434 shadow_shape.rect = Rect {
22435 x: 500.0,
22436 y: 500.0,
22437 width: 12.0,
22438 height: 12.0,
22439 };
22440 let shadow_draws = vec![ShadowDraw {
22441 shapes: vec![(shadow_shape, BlendMode::SrcOver)],
22442 brushes: vec![],
22443 texts: Vec::new(),
22444 blur_radius: 8.0,
22445 clip: None,
22446 z_index: 1,
22447 }];
22448 let mut ordered_items = vec![
22449 (0, SegmentDrawItem::Shape(0)),
22450 (1, SegmentDrawItem::Shadow(0)),
22451 (2, SegmentDrawItem::Image(0)),
22452 ];
22453
22454 let culled =
22455 retain_renderable_shadow_items(&mut ordered_items, &shadow_draws, 100, 100, 1.0, 4096);
22456 let commands: Vec<_> = SegmentCommandIter::new(
22457 &ordered_items,
22458 &shapes,
22459 &images,
22460 ShapeBatchLimits::desktop(),
22461 )
22462 .collect();
22463
22464 assert_eq!(culled, 1);
22465 assert_eq!(
22466 commands,
22467 vec![SegmentRenderCommand::DrawChunk(chunk(&[
22468 SegmentBatchPlan::Shape {
22469 start: 0,
22470 end: 1,
22471 blend_mode: BlendMode::SrcOver,
22472 },
22473 SegmentBatchPlan::Image {
22474 start: 1,
22475 end: 2,
22476 blend_mode: BlendMode::SrcOver,
22477 },
22478 ]))]
22479 );
22480 }
22481
22482 #[test]
22483 fn retain_renderable_shadow_items_keeps_visible_shadow_boundaries() {
22484 let mut shadow_shape = test_shape(1, BlendMode::SrcOver);
22485 shadow_shape.rect = Rect {
22486 x: 20.0,
22487 y: 20.0,
22488 width: 12.0,
22489 height: 12.0,
22490 };
22491 let shadow_draws = vec![ShadowDraw {
22492 shapes: vec![(shadow_shape, BlendMode::SrcOver)],
22493 brushes: vec![],
22494 texts: Vec::new(),
22495 blur_radius: 8.0,
22496 clip: None,
22497 z_index: 1,
22498 }];
22499 let mut ordered_items = vec![(1, SegmentDrawItem::Shadow(0))];
22500
22501 let culled =
22502 retain_renderable_shadow_items(&mut ordered_items, &shadow_draws, 100, 100, 1.0, 4096);
22503
22504 assert_eq!(culled, 0);
22505 assert_eq!(ordered_items, vec![(1, SegmentDrawItem::Shadow(0))]);
22506 }
22507
22508 #[test]
22509 fn shape_data_layout_matches_the_wgsl_mirror() {
22510 assert_eq!(std::mem::size_of::<ShapeData>(), 160);
22514 assert_eq!(std::mem::size_of::<ShapeData>() % 16, 0);
22515 assert_eq!(std::mem::size_of::<GradientStop>(), 32);
22516 }
22517
22518 #[test]
22519 fn shape_flags_pack_kind_cap_and_join_without_collision() {
22520 assert_eq!(
22521 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter),
22522 0.0
22523 );
22524 assert_eq!(
22525 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Miter),
22526 1.0
22527 );
22528 assert_eq!(
22529 pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Butt, StrokeJoin::Miter),
22530 2.0
22531 );
22532 assert_eq!(
22534 pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Round, StrokeJoin::Miter),
22535 2.0 + 4.0
22536 );
22537 assert_eq!(
22538 pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Square, StrokeJoin::Miter),
22539 2.0 + 8.0
22540 );
22541 assert_eq!(
22542 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Round),
22543 1.0 + 16.0
22544 );
22545 assert_eq!(
22546 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Bevel),
22547 1.0 + 32.0
22548 );
22549 for kind in [SHAPE_KIND_FILL, SHAPE_KIND_STROKE, SHAPE_KIND_ARC] {
22551 for cap in [StrokeCap::Butt, StrokeCap::Round, StrokeCap::Square] {
22552 for join in [StrokeJoin::Miter, StrokeJoin::Round, StrokeJoin::Bevel] {
22553 let packed = pack_shape_flags(kind, cap, join);
22554 let bits = packed as u32;
22555 assert_eq!(bits & 3, kind);
22556 assert_eq!((bits >> 2) & 3, stroke_cap_code(cap));
22557 assert_eq!((bits >> 4) & 3, stroke_join_code(join));
22558 assert_eq!(packed, bits as f32, "flags must be exact in f32");
22559 }
22560 }
22561 }
22562 }
22563
22564 #[cfg(not(target_arch = "wasm32"))]
22565 #[test]
22566 fn mesh_vertex_layout_matches_the_wgsl_input() {
22567 assert_eq!(std::mem::size_of::<MeshVertex>(), 20);
22570 }
22571
22572 #[cfg(not(target_arch = "wasm32"))]
22576 #[allow(clippy::too_many_arguments)]
22577 fn sdf_arc_band_reference(
22578 p: [f32; 2],
22579 center: [f32; 2],
22580 inner: f32,
22581 outer: f32,
22582 mid_sin_cos: [f32; 2],
22583 half_sin_cos: [f32; 2],
22584 cap: u32,
22585 ) -> f32 {
22586 let ra = (outer + inner) * 0.5;
22587 let rb = ((outer - inner) * 0.5).max(0.0);
22588 let sm = mid_sin_cos[0];
22589 let cm = mid_sin_cos[1];
22590 let d = [p[0] - center[0], p[1] - center[1]];
22591 let mut q = [-sm * d[0] + cm * d[1], cm * d[0] + sm * d[1]];
22592 q[0] = q[0].abs();
22593 let sc = half_sin_cos;
22594 let mut dist = if sc[1] * q[0] > sc[0] * q[1] {
22595 let dx = q[0] - sc[0] * ra;
22596 let dy = q[1] - sc[1] * ra;
22597 (dx * dx + dy * dy).sqrt() - rb
22598 } else {
22599 ((q[0] * q[0] + q[1] * q[1]).sqrt() - ra).abs() - rb
22600 };
22601 let plane = sc[1] * q[0] - sc[0] * q[1];
22602 if cap == 0 {
22604 dist = dist.max(plane);
22605 } else if cap == 2 {
22606 dist = dist.max(plane - rb);
22607 }
22608 dist
22609 }
22610
22611 #[cfg(not(target_arch = "wasm32"))]
22612 fn point_in_triangle(p: [f64; 2], tri: &[[f64; 2]; 3]) -> bool {
22613 let side = |a: [f64; 2], b: [f64; 2]| {
22614 (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0])
22615 };
22616 let d0 = side(tri[0], tri[1]);
22617 let d1 = side(tri[1], tri[2]);
22618 let d2 = side(tri[2], tri[0]);
22619 let has_neg = d0 < 0.0 || d1 < 0.0 || d2 < 0.0;
22620 let has_pos = d0 > 0.0 || d1 > 0.0 || d2 > 0.0;
22621 !(has_neg && has_pos)
22622 }
22623
22624 #[cfg(not(target_arch = "wasm32"))]
22625 fn converted_arc_shape(arc: cranpose_ui_graphics::ArcGeometry, root_scale: f32) -> ShapeData {
22626 let bounds = arc.bounds();
22627 let mut shape = test_shape(0, BlendMode::SrcOver);
22628 shape.rect = bounds;
22629 shape.local_rect = bounds;
22630 shape.quad = [
22631 [bounds.x, bounds.y],
22632 [bounds.x + bounds.width, bounds.y],
22633 [bounds.x, bounds.y + bounds.height],
22634 [bounds.x + bounds.width, bounds.y + bounds.height],
22635 ];
22636 shape.arc = Some(arc);
22637 let mut converted = ShapeData::zeroed();
22638 convert_shape_into_slots(&shape, &[], root_scale, 0, &mut converted, &mut []);
22639 converted
22640 }
22641
22642 #[cfg(not(target_arch = "wasm32"))]
22647 #[test]
22648 fn arc_mesh_contains_every_band_pixel() {
22649 use cranpose_ui_graphics::ArcGeometry;
22650 let tau = cranpose_ui_graphics::TAU;
22651 let center = Point::new(250.0, 250.0);
22652 let cases: &[(f32, f32, f32, f32, StrokeCap)] = &[
22653 (90.0, 100.0, 0.0, tau, StrokeCap::Round),
22655 (80.0, 100.0, 1.0, 10.0, StrokeCap::Butt),
22657 (0.0, 40.0, 0.0, tau, StrokeCap::Round),
22659 (30.0, 80.0, 0.7, 2.5, StrokeCap::Butt),
22661 (30.0, 80.0, 0.7, 2.5, StrokeCap::Round),
22662 (30.0, 80.0, 0.7, 2.5, StrokeCap::Square),
22663 (99.0, 101.0, 3.0, 4.0, StrokeCap::Round),
22665 (0.6, 2.0, 0.3, 1.2, StrokeCap::Butt),
22667 (1900.0, 1904.0, 0.1, 0.35, StrokeCap::Square),
22669 (40.0, 60.0, 5.0, 1e-3, StrokeCap::Round),
22671 (40.0, 60.0, 0.2, tau - 1e-3, StrokeCap::Butt),
22673 (0.0, 3.0, 1.0, 2.0, StrokeCap::Round),
22675 (20.0, 60.0, 4.5, 1.9, StrokeCap::Butt),
22677 ];
22678 for (case, &(inner, outer, start, sweep, cap)) in cases.iter().enumerate() {
22679 for root_scale in [1.0f32, 2.0, 2.75] {
22684 let arc = ArcGeometry::new(center, inner, outer, start, sweep, cap);
22685 assert!(!arc.is_degenerate(), "case {case} must be drawable");
22686 let converted = converted_arc_shape(arc, root_scale);
22687 let band = arc_mesh_band(&converted)
22688 .unwrap_or_else(|| panic!("case {case} must qualify for meshing"));
22689 let mut vertices = Vec::new();
22690 let mut indices = Vec::new();
22691 let segments =
22692 emit_arc_band_mesh(&converted, 0, &band, &mut vertices, &mut indices)
22693 .unwrap_or_else(|| panic!("case {case} must produce a mesh"));
22694 assert!(segments >= ARC_MESH_MIN_SEGMENTS);
22695 let position = |index: u32| {
22698 let p = vertices[index as usize].position;
22699 [p[0] as f64, p[1] as f64]
22700 };
22701 let triangles: Vec<[[f64; 2]; 3]> = indices
22702 .as_chunks::<3>()
22703 .0
22704 .iter()
22705 .map(|tri| [position(tri[0]), position(tri[1]), position(tri[2])])
22706 .collect();
22707
22708 let [qx, qy, ..] = converted.quad01;
22712 let [_, _, qr, qb] = converted.quad23;
22713 let (rw, rh) = (qr - qx, qb - qy);
22714 let cap_bits = (converted.stroke_params[1].max(0.0) as u32 >> 2) & 3;
22715 let step = (rw.max(rh) / 400.0).clamp(0.25, 2.0);
22716 let mut band_points = 0usize;
22717 let mut y = qy;
22718 while y <= qb {
22719 let mut x = qx;
22720 while x <= qr {
22721 let dist = sdf_arc_band_reference(
22722 [x, y],
22723 [converted.arc_params[0], converted.arc_params[1]],
22724 converted.stroke_params[3],
22725 converted.stroke_params[2],
22726 [converted.radii[0], converted.radii[1]],
22727 [converted.radii[2], converted.radii[3]],
22728 cap_bits,
22729 );
22730 if dist <= 0.5 {
22731 band_points += 1;
22732 let p = [x as f64, y as f64];
22733 assert!(
22734 triangles.iter().any(|tri| point_in_triangle(p, tri)),
22735 "case {case} scale {root_scale}: band point ({x}, {y}) \
22736 dist {dist} escapes the mesh"
22737 );
22738 }
22739 x += step;
22740 }
22741 y += step;
22742 }
22743 assert!(
22744 band_points > 0,
22745 "case {case} scale {root_scale}: the sampling grid never hit the band"
22746 );
22747 }
22748 }
22749 }
22750
22751 #[cfg(not(target_arch = "wasm32"))]
22757 #[test]
22758 fn unmeshed_shapes_leave_no_geometry_and_empty_index_ranges() {
22759 let shape = test_shape(0, BlendMode::SrcOver);
22760 let mut converted = ShapeData::zeroed();
22761 convert_shape_into_slots(&shape, &[], 1.0, 0, &mut converted, &mut []);
22762 let build = build_arc_mesh_vertices(
22763 std::slice::from_ref(&converted),
22764 RETAINED_MESH_MIN_PX2_DEFAULT as f64,
22765 )
22766 .expect("within budget");
22767 assert_eq!(build.meshed_arcs, 0);
22768 assert_eq!(build.meshed_rims, 0);
22769 assert_eq!(build.passthrough, 1);
22770 assert_eq!(build.meshed_stretches, 0);
22771 assert!(build.vertices.is_empty());
22772 assert!(build.indices.is_empty());
22773 assert_eq!(build.index_prefix, vec![0, 0]);
22774 assert_eq!(build.mesh_area, build.quad_area);
22777 }
22778
22779 #[cfg(not(target_arch = "wasm32"))]
22785 #[test]
22786 fn arc_mesh_indices_share_boundary_vertices_and_wrap_closed_rings() {
22787 use cranpose_ui_graphics::ArcGeometry;
22788 let tau = cranpose_ui_graphics::TAU;
22789 for (sweep, closed) in [(tau, true), (1.9f32, false)] {
22792 let arc = ArcGeometry::new(
22793 Point::new(250.0, 250.0),
22794 80.0,
22795 100.0,
22796 0.7,
22797 sweep,
22798 StrokeCap::Round,
22799 );
22800 let mut converted = converted_arc_shape(arc, 1.0);
22801 converted.rect = [0.0, 0.0, 500.0, 500.0];
22805 converted.quad01 = [0.0, 0.0, 500.0, 0.0];
22806 converted.quad23 = [0.0, 500.0, 500.0, 500.0];
22807 let band = arc_mesh_band(&converted).expect("arc must qualify");
22808 let mut vertices = Vec::new();
22809 let mut indices = Vec::new();
22810 let segments = emit_arc_band_mesh(&converted, 0, &band, &mut vertices, &mut indices)
22811 .expect("arc must mesh");
22812 let boundary_count = if closed { segments } else { segments + 1 };
22813 assert_eq!(
22814 vertices.len(),
22815 2 * boundary_count,
22816 "closed={closed}: every boundary owns exactly one (inner, outer) pair"
22817 );
22818 assert_eq!(indices.len(), 6 * segments);
22819 for j in 0..segments {
22823 let jb = (j + 1) % boundary_count;
22824 let (in_a, out_a) = (2 * j as u32, 2 * j as u32 + 1);
22825 let (in_b, out_b) = (2 * jb as u32, 2 * jb as u32 + 1);
22826 assert_eq!(
22827 indices[6 * j..6 * j + 6],
22828 [in_a, out_a, out_b, in_a, out_b, in_b],
22829 "closed={closed}: segment {j} must share its boundary pairs"
22830 );
22831 }
22832 if closed {
22833 assert_eq!(indices[6 * segments - 1], 0);
22836 }
22837 for pair in vertices.as_chunks::<2>().0 {
22841 let radius = |v: &MeshVertex| {
22842 let dx = v.position[0] - 250.0;
22843 let dy = v.position[1] - 250.0;
22844 (dx * dx + dy * dy).sqrt()
22845 };
22846 assert!(radius(&pair[0]) < radius(&pair[1]));
22847 }
22848 }
22849 }
22850
22851 #[cfg(not(target_arch = "wasm32"))]
22857 #[test]
22858 fn arc_mesh_clipped_segments_fan_over_private_vertices() {
22859 use cranpose_ui_graphics::ArcGeometry;
22860 let arc = ArcGeometry::new(
22861 Point::new(250.0, 250.0),
22862 80.0,
22863 100.0,
22864 0.0,
22865 cranpose_ui_graphics::TAU,
22866 StrokeCap::Round,
22867 );
22868 let converted = converted_arc_shape(arc, 1.0);
22869 let band = arc_mesh_band(&converted).expect("ring must qualify");
22870 let mut vertices = Vec::new();
22871 let mut indices = Vec::new();
22872 emit_arc_band_mesh(&converted, 0, &band, &mut vertices, &mut indices)
22873 .expect("ring must mesh");
22874 let mut uses = vec![0usize; vertices.len()];
22877 for &index in &indices {
22878 uses[index as usize] += 1;
22879 }
22880 assert!(
22881 uses.iter().any(|&count| count >= 3),
22882 "some boundary vertices must be shared across trapezoids"
22883 );
22884 let [left, top, ..] = converted.quad01;
22890 let [.., right, bottom] = converted.quad23;
22891 let clipped: Vec<&MeshVertex> = vertices
22892 .iter()
22893 .filter(|vertex| {
22894 let [x, y] = vertex.position;
22895 x == left || x == right || y == top || y == bottom
22896 })
22897 .collect();
22898 assert!(
22899 !clipped.is_empty(),
22900 "the tight box must clip the pushed-out chord vertices"
22901 );
22902 assert!(
22905 vertices.len() < indices.len(),
22906 "{} unique vertices should undercut {} triangle corners",
22907 vertices.len(),
22908 indices.len()
22909 );
22910 }
22911
22912 #[cfg(not(target_arch = "wasm32"))]
22913 #[test]
22914 fn arc_mesh_budget_overflow_falls_back_to_whole_slot_passthrough() {
22915 use cranpose_ui_graphics::ArcGeometry;
22916 let arc = ArcGeometry::new(
22921 Point::new(2000.0, 2000.0),
22922 1690.0,
22923 1710.0,
22924 0.0,
22925 cranpose_ui_graphics::TAU,
22926 StrokeCap::Round,
22927 );
22928 let converted = converted_arc_shape(arc, 1.0);
22929 let shapes = vec![converted; 100];
22930 assert!(build_arc_mesh_vertices(&shapes, RETAINED_MESH_MIN_PX2_DEFAULT as f64).is_none());
22931 }
22932
22933 #[cfg(not(target_arch = "wasm32"))]
22938 #[test]
22939 fn retained_mesh_size_gate_engages_exactly_per_threshold() {
22940 use cranpose_ui_graphics::ArcGeometry;
22941 let big_ring = converted_arc_shape(
22945 ArcGeometry::new(
22946 Point::new(204.0, 204.0),
22947 140.0,
22948 160.0,
22949 0.0,
22950 cranpose_ui_graphics::TAU,
22951 StrokeCap::Butt,
22952 ),
22953 1.0,
22954 );
22955 let small_arc = converted_arc_shape(
22956 ArcGeometry::new(
22957 Point::new(204.0, 204.0),
22958 12.0,
22959 18.0,
22960 0.3,
22961 0.5,
22962 StrokeCap::Butt,
22963 ),
22964 1.0,
22965 );
22966 let rim = rim_test_shape_data();
22967 let shapes = [big_ring, small_arc, rim];
22968 let big_px2 = quad_shoelace_area(&shapes[0]);
22969 let small_px2 = quad_shoelace_area(&shapes[1]);
22970 let rim_px2 = quad_shoelace_area(&shapes[2]);
22971 assert!(small_px2 < 1024.0 && big_px2 > rim_px2 && rim_px2 > 16384.0);
22972
22973 let build = build_arc_mesh_vertices(&shapes, RETAINED_MESH_MIN_PX2_DEFAULT as f64)
22977 .expect("within budget");
22978 assert_eq!(
22979 (build.meshed_arcs, build.meshed_rims, build.passthrough),
22980 (1, 1, 1)
22981 );
22982 assert_eq!(build.meshed_stretches, 2);
22983 assert_eq!(build.index_prefix[1], build.index_prefix[2]);
22986 assert!(build.index_prefix[1] > build.index_prefix[0]);
22987 assert!(build.index_prefix[3] > build.index_prefix[2]);
22988
22989 let build = build_arc_mesh_vertices(&shapes, big_px2).expect("within budget");
22992 assert_eq!(
22993 (build.meshed_arcs, build.meshed_rims, build.passthrough),
22994 (1, 0, 2)
22995 );
22996 let build =
22998 build_arc_mesh_vertices(&shapes, big_px2 + big_px2 * f64::EPSILON).expect("budget");
22999 assert_eq!(
23000 (build.meshed_arcs, build.meshed_rims, build.passthrough),
23001 (0, 0, 3)
23002 );
23003
23004 let build = build_arc_mesh_vertices(&shapes, (rim_px2 + big_px2) * 0.5).expect("budget");
23006 assert_eq!(
23007 (build.meshed_arcs, build.meshed_rims, build.passthrough),
23008 (1, 0, 2)
23009 );
23010
23011 let everything_gated =
23014 build_arc_mesh_vertices(&shapes, big_px2 * 2.0).expect("within budget");
23015 assert_eq!(everything_gated.passthrough, 3);
23016 assert_eq!(everything_gated.meshed_stretches, 0);
23017 assert!(everything_gated.vertices.is_empty());
23018 assert_eq!(everything_gated.index_prefix, vec![0, 0, 0, 0]);
23019 }
23020
23021 #[cfg(not(target_arch = "wasm32"))]
23026 #[test]
23027 fn meshed_stretches_count_maximal_runs_of_consecutive_meshed_shapes() {
23028 use cranpose_ui_graphics::ArcGeometry;
23029 let big = converted_arc_shape(
23030 ArcGeometry::new(
23031 Point::new(204.0, 204.0),
23032 140.0,
23033 160.0,
23034 0.0,
23035 cranpose_ui_graphics::TAU,
23036 StrokeCap::Butt,
23037 ),
23038 1.0,
23039 );
23040 let small = converted_arc_shape(
23041 ArcGeometry::new(
23042 Point::new(204.0, 204.0),
23043 12.0,
23044 18.0,
23045 0.3,
23046 0.5,
23047 StrokeCap::Butt,
23048 ),
23049 1.0,
23050 );
23051 let shapes = [big, big, small, big, small, small, big, big];
23053 let build = build_arc_mesh_vertices(&shapes, RETAINED_MESH_MIN_PX2_DEFAULT as f64)
23054 .expect("within budget");
23055 assert_eq!(build.meshed_arcs, 5);
23056 assert_eq!(build.passthrough, 3);
23057 assert_eq!(build.meshed_stretches, 3);
23058 assert!(build.meshed_stretches <= MESH_SLOT_MAX_STRETCHES);
23061 }
23062
23063 #[cfg(not(target_arch = "wasm32"))]
23066 #[test]
23067 fn retained_mesh_px2_override_parses_and_clamps() {
23068 assert_eq!(
23069 parse_retained_mesh_min_px2(None),
23070 RETAINED_MESH_MIN_PX2_DEFAULT as f64
23071 );
23072 assert_eq!(
23073 parse_retained_mesh_min_px2(Some("not a number")),
23074 RETAINED_MESH_MIN_PX2_DEFAULT as f64
23075 );
23076 assert_eq!(
23077 parse_retained_mesh_min_px2(Some("-5")),
23078 RETAINED_MESH_MIN_PX2_DEFAULT as f64
23079 );
23080 assert_eq!(parse_retained_mesh_min_px2(Some(" 40000 ")), 40000.0);
23081 assert_eq!(
23082 parse_retained_mesh_min_px2(Some("0")),
23083 *RETAINED_MESH_MIN_PX2_RANGE.start() as f64
23084 );
23085 assert_eq!(
23086 parse_retained_mesh_min_px2(Some("99999999")),
23087 *RETAINED_MESH_MIN_PX2_RANGE.end() as f64
23088 );
23089 }
23090
23091 #[cfg(not(target_arch = "wasm32"))]
23096 #[test]
23097 fn retained_capture_meshes_big_stroked_circle_rims_as_annuli() {
23098 let rim = rim_test_shape_data();
23099 let build = build_arc_mesh_vertices(
23100 std::slice::from_ref(&rim),
23101 RETAINED_MESH_MIN_PX2_DEFAULT as f64,
23102 )
23103 .expect("within budget");
23104 assert_eq!(
23105 (build.meshed_arcs, build.meshed_rims, build.passthrough),
23106 (0, 1, 0)
23107 );
23108 assert!(build.meshed_segments >= ARC_MESH_MIN_SEGMENTS);
23109 assert!(build.mesh_area < 0.2 * build.quad_area);
23114 let band = rim_band_geometry(&rim).expect("rim must qualify");
23115 for vertex in &build.vertices {
23116 let dx = vertex.position[0] - band.center[0];
23117 let dy = vertex.position[1] - band.center[1];
23118 let radius = (dx * dx + dy * dy).sqrt();
23119 assert!(
23120 radius >= band.inner - ARC_MESH_MARGIN - 1e-3,
23121 "vertex at radius {radius} fell inside the annulus hole"
23122 );
23123 }
23124 }
23125
23126 #[cfg(not(target_arch = "wasm32"))]
23130 fn rim_test_shape_data() -> ShapeData {
23131 let mut shape = ShapeData::zeroed();
23132 shape.rect = [40.0, 40.0, 300.0, 300.0];
23133 shape.radii = [146.0; 4];
23134 shape.stroke_params = [
23135 8.0,
23136 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Miter),
23137 0.0,
23138 0.0,
23139 ];
23140 shape.quad01 = [40.0, 40.0, 340.0, 40.0];
23141 shape.quad23 = [40.0, 340.0, 340.0, 340.0];
23142 shape.color = [1.0, 1.0, 1.0, 1.0];
23143 shape
23144 }
23145
23146 #[cfg(not(target_arch = "wasm32"))]
23149 fn offscreen_test_viewport() -> ViewportUniformParams {
23150 ViewportUniformParams {
23151 width: 64,
23152 height: 64,
23153 offset: [7.0, 7.0],
23154 }
23155 }
23156
23157 #[cfg(not(target_arch = "wasm32"))]
23158 #[test]
23159 fn fill_diag_buckets_shape_quads_by_decoded_sdf_class() {
23160 let diag = FillAreaDiag::default();
23161 let mut arc = ShapeData::zeroed();
23162 arc.stroke_params[1] = pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Butt, StrokeJoin::Miter);
23163 arc.radii = [0.5; 4];
23166 arc.quad01 = [0.0, 0.0, 10.0, 0.0];
23167 arc.quad23 = [0.0, 10.0, 10.0, 10.0];
23168 let mut rounded = ShapeData::zeroed();
23169 rounded.stroke_params[1] =
23170 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
23171 rounded.radii = [2.0; 4];
23172 rounded.quad01 = [0.0, 0.0, 4.0, 0.0];
23173 rounded.quad23 = [0.0, 5.0, 4.0, 5.0];
23174 let mut plain = ShapeData::zeroed();
23175 plain.stroke_params[1] =
23176 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
23177 plain.quad01 = [0.0, 0.0, 2.0, 0.0];
23178 plain.quad23 = [0.0, 3.0, 2.0, 3.0];
23179 diag.add_shape_quads(
23180 &[rim_test_shape_data(), arc, rounded, plain],
23181 offscreen_test_viewport(),
23182 );
23183 assert_eq!(diag.frame[FillAreaDiag::RRECT_STROKE].get(), 300.0 * 300.0);
23184 assert_eq!(diag.frame[FillAreaDiag::ARC].get(), 100.0);
23185 assert_eq!(diag.frame[FillAreaDiag::RRECT_FILL].get(), 20.0);
23186 assert_eq!(diag.frame[FillAreaDiag::RECT].get(), 6.0);
23187 assert_eq!(diag.frame_corner.get(), 0.0);
23189 for (lit, quad) in diag.frame_lit.iter().zip(&diag.frame) {
23191 assert!(lit.get() <= quad.get() + 1e-9);
23192 }
23193 }
23194
23195 #[cfg(not(target_arch = "wasm32"))]
23196 #[test]
23197 fn fill_diag_rim_mesh_moves_quad_area_to_the_mesh_bucket() {
23198 let diag = FillAreaDiag::default();
23199 diag.add_shape_quads(&[rim_test_shape_data()], offscreen_test_viewport());
23200 diag.note_rim_mesh(&rim_test_shape_data(), 1234.5);
23201 assert_eq!(diag.frame[FillAreaDiag::RRECT_STROKE].get(), 0.0);
23202 assert_eq!(diag.frame[FillAreaDiag::MESH].get(), 1234.5);
23203 assert_eq!(diag.frame_lit[FillAreaDiag::RRECT_STROKE].get(), 0.0);
23206 assert!(diag.frame_lit[FillAreaDiag::MESH].get() <= 1234.5);
23207 assert!(diag.frame_lit[FillAreaDiag::MESH].get() > 0.0);
23208 }
23209
23210 #[cfg(not(target_arch = "wasm32"))]
23211 #[test]
23212 fn fill_diag_image_and_glyph_quads_share_one_bucket() {
23213 let diag = FillAreaDiag::default();
23214 diag.add_image_quad(&[[0.0, 0.0], [8.0, 0.0], [0.0, 4.0], [8.0, 4.0]]);
23215 let quad = CachedTextGlyphQuad {
23216 x: 0,
23217 y: 0,
23218 width: 5,
23219 height: 7,
23220 color: (1.0, 1.0, 1.0, 1.0),
23221 uv: ImageUvRect {
23222 min: [0.0, 0.0],
23223 max: [1.0, 1.0],
23224 sample_bounds: [0.0, 0.0, 1.0, 1.0],
23225 },
23226 };
23227 diag.add_glyph_quad(&quad);
23228 assert_eq!(diag.frame[FillAreaDiag::IMAGE_GLYPH].get(), 32.0 + 35.0);
23229 assert_eq!(diag.frame_lit[FillAreaDiag::IMAGE_GLYPH].get(), 32.0 + 35.0);
23231 }
23232
23233 #[cfg(not(target_arch = "wasm32"))]
23236 fn numeric_area(bounds: [f64; 4], steps: usize, inside: impl Fn(f64, f64) -> bool) -> f64 {
23237 let dx = (bounds[2] - bounds[0]) / steps as f64;
23238 let dy = (bounds[3] - bounds[1]) / steps as f64;
23239 let mut area = 0.0;
23240 for column in 0..steps {
23241 let x = bounds[0] + (column as f64 + 0.5) * dx;
23242 for row in 0..steps {
23243 let y = bounds[1] + (row as f64 + 0.5) * dy;
23244 if inside(x, y) {
23245 area += dx * dy;
23246 }
23247 }
23248 }
23249 area
23250 }
23251
23252 #[cfg(not(target_arch = "wasm32"))]
23255 fn sdf_rounded_rect_reference(
23256 p: [f64; 2],
23257 center: [f64; 2],
23258 half: [f64; 2],
23259 radius: f64,
23260 ) -> f64 {
23261 let qx = (p[0] - center[0]).abs() - (half[0] - radius);
23262 let qy = (p[1] - center[1]).abs() - (half[1] - radius);
23263 qx.max(0.0).hypot(qy.max(0.0)) + qx.max(qy).min(0.0) - radius
23264 }
23265
23266 #[cfg(not(target_arch = "wasm32"))]
23267 #[test]
23268 fn fill_truth_arc_lit_matches_the_sdf_covered_area() {
23269 use cranpose_ui_graphics::ArcGeometry;
23270 let tau = cranpose_ui_graphics::TAU;
23271 let center = Point::new(250.0, 250.0);
23272 let cases: &[(f32, f32, f32, f32, StrokeCap)] = &[
23275 (90.0, 100.0, 0.7, 2.5, StrokeCap::Butt),
23276 (30.0, 80.0, 0.7, 2.5, StrokeCap::Round),
23277 (30.0, 80.0, 0.7, 2.5, StrokeCap::Square),
23278 (80.0, 100.0, 0.0, tau, StrokeCap::Round),
23279 (0.0, 40.0, 0.0, tau, StrokeCap::Round),
23280 ];
23281 for (case, &(inner, outer, start, sweep, cap)) in cases.iter().enumerate() {
23282 let arc = ArcGeometry::new(center, inner, outer, start, sweep, cap);
23283 let converted = converted_arc_shape(arc, 1.0);
23284 let cap_code = (converted.stroke_params[1].max(0.0) as u32 >> 2) & 3;
23285 let arc_center = [converted.arc_params[0], converted.arc_params[1]];
23286 let mid = [converted.radii[0], converted.radii[1]];
23287 let half = [converted.radii[2], converted.radii[3]];
23288 let aabb = quad_aabb(&converted);
23289 let bounds = [aabb[0] - 2.0, aabb[1] - 2.0, aabb[2] + 2.0, aabb[3] + 2.0];
23292 let numeric = numeric_area(bounds, 1000, |x, y| {
23293 sdf_arc_band_reference(
23294 [x as f32, y as f32],
23295 arc_center,
23296 converted.stroke_params[3],
23297 converted.stroke_params[2],
23298 mid,
23299 half,
23300 cap_code,
23301 ) < 0.0
23302 });
23303 let analytic = analytic_covered_area(&converted);
23304 let error = (analytic - numeric).abs() / numeric.max(1.0);
23305 assert!(
23306 error < 0.02,
23307 "case {case}: analytic {analytic:.1} vs sdf {numeric:.1} \
23308 ({:.2}% off)",
23309 error * 100.0
23310 );
23311 }
23312 }
23313
23314 #[cfg(not(target_arch = "wasm32"))]
23315 #[test]
23316 fn fill_truth_circle_and_rrect_fill_lit_match_references() {
23317 let mut circle = ShapeData::zeroed();
23319 circle.stroke_params[1] =
23320 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
23321 circle.rect = [10.0, 10.0, 200.0, 200.0];
23322 circle.radii = [100.0; 4];
23323 let analytic = analytic_covered_area(&circle);
23324 let exact = std::f64::consts::PI * 100.0 * 100.0;
23325 assert!(
23326 (analytic - exact).abs() / exact < 1e-9,
23327 "circle: {analytic} vs {exact}"
23328 );
23329
23330 let mut rounded = ShapeData::zeroed();
23332 rounded.stroke_params[1] =
23333 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
23334 rounded.rect = [50.0, 80.0, 200.0, 120.0];
23335 rounded.radii = [40.0; 4];
23336 let numeric = numeric_area([48.0, 78.0, 252.0, 202.0], 1000, |x, y| {
23337 sdf_rounded_rect_reference([x, y], [150.0, 140.0], [100.0, 60.0], 40.0) < 0.0
23338 });
23339 let analytic = analytic_covered_area(&rounded);
23340 let error = (analytic - numeric).abs() / numeric;
23341 assert!(
23342 error < 0.02,
23343 "rrect fill: analytic {analytic:.1} vs sdf {numeric:.1}"
23344 );
23345 }
23346
23347 #[cfg(not(target_arch = "wasm32"))]
23348 #[test]
23349 fn fill_truth_stroked_rrect_lit_matches_the_band_area() {
23350 let rim = rim_test_shape_data();
23353 let analytic = analytic_covered_area(&rim);
23354 let exact = std::f64::consts::PI * (150.0 * 150.0 - 142.0 * 142.0);
23355 assert!(
23356 (analytic - exact).abs() / exact < 1e-9,
23357 "circle rim: {analytic} vs {exact}"
23358 );
23359
23360 let mut square_ring = rim_test_shape_data();
23363 square_ring.radii = [60.0; 4];
23364 let numeric = numeric_area([38.0, 38.0, 342.0, 342.0], 1000, |x, y| {
23365 sdf_rounded_rect_reference([x, y], [190.0, 190.0], [146.0, 146.0], 60.0).abs() < 4.0
23366 });
23367 let analytic = analytic_covered_area(&square_ring);
23368 let error = (analytic - numeric).abs() / numeric;
23369 assert!(
23370 error < 0.02,
23371 "square ring: analytic {analytic:.1} vs sdf {numeric:.1}"
23372 );
23373 }
23374
23375 #[cfg(not(target_arch = "wasm32"))]
23376 #[test]
23377 fn fill_truth_corner_counter_prices_the_area_outside_the_inscribed_circle() {
23378 let full = area_outside_inscribed_circle([0.0, 0.0, 454.0, 454.0], (454, 454));
23381 let exact = (1.0 - std::f64::consts::FRAC_PI_4) * 454.0 * 454.0;
23382 assert!(
23383 (full - exact).abs() / exact < 0.01,
23384 "full quad: {full} vs {exact}"
23385 );
23386 assert_eq!(
23388 area_outside_inscribed_circle([127.0, 127.0, 327.0, 327.0], (454, 454)),
23389 0.0
23390 );
23391 let corner = area_outside_inscribed_circle([0.0, 0.0, 40.0, 40.0], (454, 454));
23393 assert!((corner - 1600.0).abs() < 1e-6, "corner box: {corner}");
23394 }
23395
23396 #[cfg(not(target_arch = "wasm32"))]
23397 #[test]
23398 fn fill_truth_opacity_histogram_classifies_solid_alpha_exactly() {
23399 let diag = FillAreaDiag::default();
23400 diag.reset_frame(454, 454);
23401 let full_frame = ViewportUniformParams {
23402 width: 454,
23403 height: 454,
23404 offset: [0.0, 0.0],
23405 };
23406 let mut opaque = ShapeData::zeroed();
23407 opaque.stroke_params[1] =
23408 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
23409 opaque.rect = [0.0, 0.0, 100.0, 50.0];
23410 opaque.quad01 = [0.0, 0.0, 100.0, 0.0];
23411 opaque.quad23 = [0.0, 50.0, 100.0, 50.0];
23412 opaque.color = [1.0, 1.0, 1.0, 1.0];
23413 let mut faded = opaque;
23414 faded.color[3] = 0.82;
23415 let mut gradient = opaque;
23416 gradient.brush_type = 1;
23417 diag.add_shape_quads(&[opaque, faded, gradient], full_frame);
23418 let lit = |class: FillOpacityClass| diag.frame_opacity[class as usize].get();
23420 assert_eq!(lit(FillOpacityClass::Opaque), 5000.0);
23421 assert_eq!(lit(FillOpacityClass::Translucent), 5000.0);
23422 assert_eq!(lit(FillOpacityClass::NonSolid), 5000.0);
23423 assert!(diag.frame_corner.get() > 0.0);
23425
23426 let offscreen = FillAreaDiag::default();
23429 offscreen.reset_frame(454, 454);
23430 offscreen.add_shape_quads(&[opaque], offscreen_test_viewport());
23431 assert_eq!(offscreen.frame_corner.get(), 0.0);
23432 }
23433
23434 #[cfg(not(target_arch = "wasm32"))]
23435 #[test]
23436 fn fill_truth_retained_records_price_ranges_and_identity_corners() {
23437 let mut plain = ShapeData::zeroed();
23438 plain.stroke_params[1] =
23439 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
23440 plain.rect = [200.0, 200.0, 20.0, 10.0];
23441 plain.quad01 = [200.0, 200.0, 220.0, 200.0];
23442 plain.quad23 = [200.0, 210.0, 220.0, 210.0];
23443 plain.color = [1.0, 1.0, 1.0, 1.0];
23444 let shapes = vec![rim_test_shape_data(), plain];
23445 let records = fill_diag_capture_records(&shapes, None);
23446 assert_eq!(records.len(), 2);
23447 assert_eq!(records[0].bucket, FillAreaDiag::RRECT_STROKE);
23448 assert_eq!(records[0].drawn_px2, 300.0 * 300.0);
23449 assert!(records[0].lit_px2 < records[0].drawn_px2, "a rim has slack");
23450 assert_eq!(records[1].bucket, FillAreaDiag::RECT);
23452 assert_eq!(records[1].lit_px2, records[1].drawn_px2);
23453
23454 let diag = FillAreaDiag::default();
23455 diag.reset_frame(454, 454);
23456 let scaled = SimilarityTransform::new([0.0, 0.0], 0.0, 2.0);
23459 diag.add_retained_range(&records, 0, 2, &scaled);
23460 let drawn: f64 = records.iter().map(|record| record.drawn_px2).sum();
23461 assert!((diag.frame[FillAreaDiag::RETAINED].get() - drawn * 4.0).abs() < 1e-6);
23462 assert_eq!(diag.frame_corner.get(), 0.0);
23463
23464 let identity_diag = FillAreaDiag::default();
23467 identity_diag.reset_frame(454, 454);
23468 identity_diag.add_retained_range(&records, 0, 2, &SimilarityTransform::IDENTITY);
23469 assert!(identity_diag.frame_corner.get() > 0.0);
23470 let tail = FillAreaDiag::default();
23472 tail.reset_frame(454, 454);
23473 tail.add_retained_range(&records, 1, 2, &SimilarityTransform::IDENTITY);
23474 assert_eq!(
23475 tail.frame[FillAreaDiag::RETAINED].get(),
23476 records[1].drawn_px2
23477 );
23478 }
23479
23480 #[cfg(not(target_arch = "wasm32"))]
23481 #[test]
23482 fn fill_truth_top_slack_dump_keeps_the_worst_ten() {
23483 let mut diag = FillAreaDiag::default();
23484 let records: Vec<FillDiagShapeRecord> = (0..12)
23485 .map(|index| FillDiagShapeRecord {
23486 drawn_px2: 1000.0 * (index + 1) as f64,
23487 lit_px2: 100.0,
23488 bucket: FillAreaDiag::ARC,
23489 opacity: FillOpacityClass::Opaque,
23490 aabb: [0.0, 0.0, 10.0, 10.0],
23491 })
23492 .collect();
23493 diag.note_retained_capture(3, &records);
23494 assert_eq!(diag.slack_top.len(), FILL_DIAG_SLACK_TOP);
23495 assert_eq!(diag.slack_top[0].drawn_px2, 12000.0);
23497 assert_eq!(diag.slack_top[0].slot, 3);
23498 assert_eq!(diag.slack_top[0].shape, 11);
23499 for pair in diag.slack_top.windows(2) {
23500 assert!(pair[0].drawn_px2 - pair[0].lit_px2 >= pair[1].drawn_px2 - pair[1].lit_px2);
23501 }
23502 assert!(diag
23503 .slack_top
23504 .iter()
23505 .all(|entry| entry.drawn_px2 - entry.lit_px2 > 2000.0 - 100.0));
23506 }
23507
23508 #[cfg(not(target_arch = "wasm32"))]
23509 #[test]
23510 fn rim_mesh_band_accepts_only_huge_solid_unclipped_circle_rims() {
23511 let band = rim_mesh_band(&rim_test_shape_data()).expect("circle rim must qualify");
23512 assert_eq!(band.center, [190.0, 190.0]);
23513 assert_eq!(band.inner, 142.0);
23514 assert_eq!(band.outer, 150.0);
23515 assert_eq!(band.start, 0.0);
23516 assert!(
23517 band.sweep >= cranpose_ui_graphics::TAU,
23518 "a rim band is a closed ring"
23519 );
23520 let mut vertices = Vec::new();
23522 let mut indices = Vec::new();
23523 emit_arc_band_mesh(
23524 &rim_test_shape_data(),
23525 7,
23526 &band,
23527 &mut vertices,
23528 &mut indices,
23529 )
23530 .expect("rim must mesh");
23531 assert!(vertices.iter().all(|vertex| vertex.shape_idx == 7));
23532
23533 let mut square = rim_test_shape_data();
23537 square.radii = [100.0; 4];
23538 assert!(rim_mesh_band(&square).is_none());
23539
23540 let mut oblong = rim_test_shape_data();
23542 oblong.rect = [40.0, 40.0, 300.0, 200.0];
23543 assert!(rim_mesh_band(&oblong).is_none());
23544
23545 let mut gradient = rim_test_shape_data();
23547 gradient.brush_type = 1;
23548 assert!(rim_mesh_band(&gradient).is_none());
23549
23550 let mut clipped = rim_test_shape_data();
23552 clipped.clip_rect = [0.0, 0.0, 400.0, 400.0];
23553 assert!(rim_mesh_band(&clipped).is_none());
23554
23555 let mut small = rim_test_shape_data();
23557 small.rect = [40.0, 40.0, 100.0, 100.0];
23558 small.quad01 = [40.0, 40.0, 140.0, 40.0];
23559 small.quad23 = [40.0, 140.0, 140.0, 140.0];
23560 small.radii = [46.0; 4];
23561 assert!(rim_mesh_band(&small).is_none());
23562
23563 let mut fill = rim_test_shape_data();
23565 fill.stroke_params[1] =
23566 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
23567 assert!(rim_mesh_band(&fill).is_none());
23568
23569 let mut hairline = rim_test_shape_data();
23571 hairline.stroke_params[0] = 0.0;
23572 assert!(rim_mesh_band(&hairline).is_none());
23573
23574 let mut uneven = rim_test_shape_data();
23576 uneven.radii[2] = 145.0;
23577 assert!(rim_mesh_band(&uneven).is_none());
23578 }
23579
23580 #[cfg(not(target_arch = "wasm32"))]
23581 #[test]
23582 fn shape_batch_limits_follow_uniform_binding_size() {
23583 let desktop_shapes = 65536 / std::mem::size_of::<ShapeData>();
23586 assert_eq!(desktop_shapes, 409);
23587 assert_eq!(
23588 ShapeBatchLimits::desktop(),
23589 ShapeBatchLimits {
23590 max_shapes_per_batch: desktop_shapes.min(MAX_SHAPES_PER_BATCH),
23591 max_gradient_stops: MAX_GRADIENT_STOPS,
23592 storage: false,
23593 }
23594 );
23595
23596 let downlevel = ShapeBatchLimits::for_uniform_binding_size(16384);
23599 assert_eq!(downlevel.max_shapes_per_batch, 16384 / 160);
23600 assert_eq!(downlevel.max_shapes_per_batch, 102);
23601 assert_eq!(downlevel.max_gradient_stops, 512.min(MAX_GRADIENT_STOPS));
23602 assert!(downlevel.max_shapes_per_batch * std::mem::size_of::<ShapeData>() <= 16384);
23603 assert!(downlevel.max_gradient_stops * std::mem::size_of::<GradientStop>() <= 16384);
23604
23605 let tiny = ShapeBatchLimits::for_uniform_binding_size(1);
23607 assert_eq!(tiny.max_shapes_per_batch, 1);
23608 assert_eq!(tiny.max_gradient_stops, 1);
23609 }
23610
23611 #[test]
23612 fn storage_shape_batch_limits_uncap_the_batch_and_start_small() {
23613 let storage = ShapeBatchLimits::for_storage_binding_size(128 << 20);
23616 assert!(storage.storage);
23617 assert_eq!(storage.max_shapes_per_batch, MAX_SHAPES_PER_STORAGE_BATCH);
23618 assert_eq!(
23619 storage.max_gradient_stops,
23620 MAX_GRADIENT_STOPS_PER_STORAGE_BATCH
23621 );
23622
23623 assert_eq!(
23626 storage.initial_shape_capacity(),
23627 INITIAL_STORAGE_BATCH_CAPACITY
23628 );
23629 assert_eq!(
23630 storage.initial_gradient_capacity(),
23631 INITIAL_STORAGE_BATCH_CAPACITY
23632 );
23633 assert_eq!(
23634 storage.data_binding_type(),
23635 wgpu::BufferBindingType::Storage { read_only: true }
23636 );
23637 assert!(storage
23638 .data_buffer_usage()
23639 .contains(wgpu::BufferUsages::STORAGE));
23640
23641 let uniform = ShapeBatchLimits::desktop();
23644 assert_eq!(
23645 uniform.initial_shape_capacity(),
23646 uniform.max_shapes_per_batch
23647 );
23648 assert_eq!(
23649 uniform.initial_gradient_capacity(),
23650 uniform.max_gradient_stops
23651 );
23652 assert_eq!(
23653 uniform.data_binding_type(),
23654 wgpu::BufferBindingType::Uniform
23655 );
23656 assert!(uniform
23657 .data_buffer_usage()
23658 .contains(wgpu::BufferUsages::UNIFORM));
23659 }
23660
23661 #[test]
23662 fn storage_shape_shader_swaps_the_arrays_to_runtime_sized_storage() {
23663 let source =
23664 shape_shader_source(ShapeBatchLimits::for_storage_binding_size(128 << 20), false);
23665 assert!(
23666 source.contains("var<storage, read> shape_data: array<ShapeData>;"),
23667 "storage-mode shader must declare a runtime-sized shape array"
23668 );
23669 assert!(
23670 source.contains("var<storage, read> gradient_stops: array<GradientStop>;"),
23671 "storage-mode shader must declare a runtime-sized gradient array"
23672 );
23673 assert!(
23674 !source.contains("var<uniform> shape_data"),
23675 "the uniform shape declaration must be fully replaced"
23676 );
23677 assert!(
23678 !source.contains("var<uniform> gradient_stops"),
23679 "the uniform gradient declaration must be fully replaced"
23680 );
23681 assert!(
23682 source.contains("var<storage, read> paint: array<vec4<f32>>;"),
23683 "storage-mode shader must declare the retained paint array"
23684 );
23685 assert!(
23686 source.contains("select(shape.color, paint[shape_idx], similarity.paint_select > 0.5)"),
23687 "storage-mode shader must read paint under the paint_select flag"
23688 );
23689 assert!(
23690 source.contains("fn vs_mesh("),
23691 "the storage rewrite must leave the retained-mesh vertex entry intact"
23692 );
23693 assert!(
23694 source.contains("fn vs_shape_instanced("),
23695 "the storage rewrite must leave the instanced-quad vertex entry intact"
23696 );
23697 assert_eq!(
23698 source
23699 .matches("select(shape.color, paint[shape_idx], similarity.paint_select > 0.5)")
23700 .count(),
23701 3,
23702 "vs_main, vs_shape_instanced and vs_mesh must all read paint under \
23703 the paint_select flag (meshless retained draws ride the instanced \
23704 entry when the selection is latched on)"
23705 );
23706
23707 let module = naga::front::wgsl::parse_str(&source)
23710 .expect("storage-mode shape shader must parse as WGSL");
23711 naga::valid::Validator::new(
23712 naga::valid::ValidationFlags::all(),
23713 naga::valid::Capabilities::all(),
23714 )
23715 .validate(&module)
23716 .expect("storage-mode shape shader must validate for WebGPU");
23717 }
23718
23719 #[test]
23720 fn solid_trim_keeps_the_full_struct_locations_with_the_dropped_slots_vacant() {
23721 let appendix = shaders::SOLID_TRIM_APPENDIX;
23727 for line in [
23728 "@location(0) color: vec4<f32>,",
23729 "@location(1) uv: vec2<f32>,",
23730 "@location(2) world_pos: vec2<f32>,",
23731 "@location(3) @interpolate(flat) rect: vec4<f32>,",
23732 "@location(4) @interpolate(flat) radii: vec4<f32>,",
23733 "@location(6) @interpolate(flat) clip_rect: vec4<f32>,",
23734 "@location(7) @interpolate(flat) stroke_params: vec4<f32>,",
23735 "@location(8) @interpolate(flat) arc_params: vec4<f32>,",
23736 ] {
23737 assert!(
23738 shaders::SHADER.contains(line),
23739 "`{line}` drifted out of VertexOutput; realign the trimmed \
23740 struct line for line before touching anything else"
23741 );
23742 assert!(
23743 appendix.contains(line),
23744 "`{line}` must appear verbatim in VertexOutputSolid — the \
23745 surviving varyings keep the full struct's location indices"
23746 );
23747 }
23748 assert!(
23749 !appendix.contains("@location(5)"),
23750 "location 5 is gradient_params' slot and must stay VACANT — \
23751 dense renumbering is the reverted attempt's suspect #1"
23752 );
23753 assert!(
23754 !appendix.contains("@location(9)"),
23755 "location 9 is brush's slot and must stay VACANT — dense \
23756 renumbering is the reverted attempt's suspect #1"
23757 );
23758 assert!(
23759 !appendix.contains("output.gradient_params") && !appendix.contains("output.brush"),
23760 "the trimmed vertex entries must not write the dropped varyings"
23761 );
23762 }
23763
23764 #[test]
23765 fn solid_trim_source_reaches_every_injection_and_validates() {
23766 let storage =
23771 shape_shader_source(ShapeBatchLimits::for_storage_binding_size(128 << 20), true);
23772 for entry in [
23773 "fn vs_solid(",
23774 "fn vs_solid_instanced(",
23775 "fn fs_solid_trim(",
23776 ] {
23777 assert!(
23778 storage.contains(entry),
23779 "trimmed storage source must carry `{entry}`"
23780 );
23781 }
23782 assert_eq!(
23783 storage
23784 .matches("select(shape.color, paint[shape_idx], similarity.paint_select > 0.5)")
23785 .count(),
23786 5,
23787 "vs_main, vs_shape_instanced, vs_mesh, vs_solid and \
23788 vs_solid_instanced must all read paint under the paint_select \
23789 flag"
23790 );
23791
23792 let uniform = shape_shader_source(ShapeBatchLimits::desktop(), true);
23795 for source in [&storage, &uniform] {
23796 for depth in [false, true] {
23797 let text = display_clip::with_content_z(Cow::Owned(source.to_string()), depth);
23798 let module = naga::front::wgsl::parse_str(&text)
23799 .expect("trimmed shape shader must parse as WGSL");
23800 naga::valid::Validator::new(
23801 naga::valid::ValidationFlags::all(),
23802 naga::valid::Capabilities::all(),
23803 )
23804 .validate(&module)
23805 .expect("trimmed shape shader must validate for WebGPU");
23806 }
23807 }
23808 }
23809
23810 #[test]
23811 fn solid_trim_flag_reads_the_documented_variable() {
23812 std::env::remove_var("CRANPOSE_SOLID_TRIM_VARYINGS");
23815 assert!(!solid_trim_varyings_enabled(), "the trim must default OFF");
23816 std::env::set_var("CRANPOSE_SOLID_TRIM_VARYINGS", "1");
23817 assert!(solid_trim_varyings_enabled());
23818 std::env::set_var("CRANPOSE_SOLID_TRIM_VARYINGS", "0");
23819 assert!(!solid_trim_varyings_enabled());
23820 std::env::remove_var("CRANPOSE_SOLID_TRIM_VARYINGS");
23821 }
23822
23823 #[test]
23824 fn uniform_shape_shader_keeps_the_in_record_color_and_no_paint_binding() {
23825 for source in [
23829 Cow::Borrowed(shaders::SHADER),
23830 shape_shader_source(ShapeBatchLimits::desktop(), false),
23831 ] {
23832 assert!(
23833 !source.contains("paint: array"),
23834 "the uniform variant must not declare a paint array"
23835 );
23836 assert!(
23837 source.contains("output.color = shape.color;"),
23838 "the uniform variant must read the color from ShapeData \
23839 (this literal is also what `shape_shader_source` rewrites)"
23840 );
23841 assert!(
23842 source.contains("paint_select: f32"),
23843 "SimilarityTransform must name the flag field in both \
23844 variants; the Rust mirror is Pod and uploads raw bytes"
23845 );
23846 }
23847 }
23848
23849 #[test]
23850 fn shipped_shape_shader_array_length_fits_the_downlevel_uniform_floor() {
23851 assert!(
23855 shaders::SHADER.contains("array<ShapeData, 102>"),
23856 "shape.wgsl array length must stay in sync with \
23857 `shape_shader_source`'s replace string and MAX_SHAPES_PER_BATCH"
23858 );
23859 assert!(102 * std::mem::size_of::<ShapeData>() <= 16384);
23860 assert!(103 * std::mem::size_of::<ShapeData>() > 16384);
23861 }
23862
23863 #[test]
23864 fn glyph_atlas_doubles_on_overflow_and_stops_at_the_device_ceiling() {
23865 assert_eq!(
23868 next_glyph_atlas_size(TEXT_GLYPH_ATLAS_MIN_SIZE, TEXT_GLYPH_ATLAS_MAX_SIZE),
23869 1024
23870 );
23871 assert_eq!(
23872 next_glyph_atlas_size(2048, TEXT_GLYPH_ATLAS_MAX_SIZE),
23873 TEXT_GLYPH_ATLAS_MAX_SIZE
23874 );
23875 assert_eq!(
23876 next_glyph_atlas_size(TEXT_GLYPH_ATLAS_MAX_SIZE, TEXT_GLYPH_ATLAS_MAX_SIZE),
23877 TEXT_GLYPH_ATLAS_MAX_SIZE
23878 );
23879
23880 assert_eq!(next_glyph_atlas_size(1024, 2048), 2048);
23883 assert_eq!(next_glyph_atlas_size(2048, 2048), 2048);
23884
23885 assert_eq!(next_glyph_atlas_size(u32::MAX, 4096), 4096);
23887 assert_eq!(next_glyph_atlas_size(0, 0), 1);
23888 }
23889
23890 #[test]
23891 fn glyph_atlas_uv_rect_normalizes_against_the_atlas_it_was_placed_in() {
23892 let entry = GlyphAtlasEntry {
23896 x: 128,
23897 y: 256,
23898 width: 16,
23899 height: 32,
23900 };
23901
23902 let small = glyph_atlas_uv_rect(entry, 512);
23903 let large = glyph_atlas_uv_rect(entry, 4096);
23904
23905 assert_eq!(small.min, [128.0 / 512.0, 256.0 / 512.0]);
23906 assert_eq!(large.min, [128.0 / 4096.0, 256.0 / 4096.0]);
23907 assert_eq!(small.max, [144.0 / 512.0, 288.0 / 512.0]);
23908 assert_eq!(large.max, [144.0 / 4096.0, 288.0 / 4096.0]);
23909 }
23910
23911 #[test]
23912 fn native_shape_shader_source_uses_native_batch_limits() {
23913 let limits = ShapeBatchLimits::desktop();
23914 let source = shape_shader_source(limits, false);
23915
23916 assert!(source.contains(&format!(
23917 "array<ShapeData, {}>",
23918 limits.max_shapes_per_batch
23919 )));
23920 assert!(source.contains(&format!(
23921 "array<GradientStop, {}>",
23922 limits.max_gradient_stops
23923 )));
23924 assert!(!source.contains("array<ShapeData, 146>"));
23927 }
23928
23929 #[test]
23930 fn stroked_and_arc_shapes_batch_together_with_fills() {
23931 let fill = test_shape(0, BlendMode::SrcOver);
23937 let mut stroked = test_shape(1, BlendMode::SrcOver);
23938 stroked.stroke = Some(
23939 cranpose_ui_graphics::Stroke::new(3.0)
23940 .with_cap(StrokeCap::Round)
23941 .with_join(StrokeJoin::Bevel),
23942 );
23943 let mut arc = test_shape(2, BlendMode::SrcOver);
23944 arc.arc = Some(cranpose_ui_graphics::ArcGeometry::new(
23945 Point::new(4.0, 4.0),
23946 2.0,
23947 4.0,
23948 0.0,
23949 1.0,
23950 StrokeCap::Round,
23951 ));
23952 let trailing_fill = test_shape(3, BlendMode::SrcOver);
23953
23954 assert!(!fill.has_stroke_or_arc());
23955 assert!(stroked.has_stroke_or_arc());
23956 assert!(arc.has_stroke_or_arc());
23957 assert!(!trailing_fill.has_stroke_or_arc());
23958
23959 let shapes = vec![fill, stroked, arc, trailing_fill];
23960 let ordered_items: Vec<_> = (0..shapes.len())
23961 .map(|index| (index, SegmentDrawItem::Shape(index)))
23962 .collect();
23963 let images = Vec::new();
23964
23965 let commands: Vec<_> = SegmentCommandIter::new(
23966 &ordered_items,
23967 &shapes,
23968 &images,
23969 ShapeBatchLimits::desktop(),
23970 )
23971 .collect();
23972
23973 assert_eq!(
23974 commands,
23975 vec![SegmentRenderCommand::DrawChunk(chunk(&[
23976 SegmentBatchPlan::Shape {
23977 start: 0,
23978 end: 4,
23979 blend_mode: BlendMode::SrcOver,
23980 }
23981 ]))],
23982 "mixed fill/stroke/arc runs must stay one batch"
23983 );
23984 }
23985
23986 #[cfg(not(target_arch = "wasm32"))]
23987 #[test]
23988 fn native_segment_fusion_budget_allows_small_interleaved_chunks() {
23989 let ordered_items = vec![
23990 (0, SegmentDrawItem::Shape(0)),
23991 (1, SegmentDrawItem::Image(0)),
23992 (2, SegmentDrawItem::Text(0)),
23993 (3, SegmentDrawItem::Shape(1)),
23994 ];
23995 let shapes = vec![
23996 test_shape(0, BlendMode::SrcOver),
23997 test_shape(3, BlendMode::DstOut),
23998 ];
23999 let segment = chunk(&[
24000 SegmentBatchPlan::Shape {
24001 start: 0,
24002 end: 1,
24003 blend_mode: BlendMode::SrcOver,
24004 },
24005 SegmentBatchPlan::Image {
24006 start: 1,
24007 end: 2,
24008 blend_mode: BlendMode::SrcOver,
24009 },
24010 SegmentBatchPlan::Text { start: 2, end: 3 },
24011 SegmentBatchPlan::Shape {
24012 start: 3,
24013 end: 4,
24014 blend_mode: BlendMode::DstOut,
24015 },
24016 ]);
24017
24018 let budget = native_segment_fusion_budget(
24019 &ordered_items,
24020 &shapes,
24021 &[],
24022 &segment,
24023 ShapeBatchLimits::desktop(),
24024 )
24025 .expect("budget should be valid")
24026 .expect("chunk should fit native fusion budget");
24027
24028 assert_eq!(
24029 budget,
24030 NativeSegmentFusionBudget {
24031 shape_count: 2,
24032 gradient_stop_count: 0,
24033 }
24034 );
24035 }
24036
24037 #[cfg(not(target_arch = "wasm32"))]
24038 #[test]
24039 fn native_segment_fusion_budget_rejects_shape_uniform_overflow() {
24040 let ordered_items: Vec<_> = (0..=MAX_SHAPES_PER_BATCH)
24041 .map(|index| (index, SegmentDrawItem::Shape(index)))
24042 .collect();
24043 let shapes: Vec<_> = (0..=MAX_SHAPES_PER_BATCH)
24044 .map(|index| test_shape(index, BlendMode::SrcOver))
24045 .collect();
24046 let segment = chunk(&[
24047 SegmentBatchPlan::Shape {
24048 start: 0,
24049 end: MAX_SHAPES_PER_BATCH,
24050 blend_mode: BlendMode::SrcOver,
24051 },
24052 SegmentBatchPlan::Shape {
24053 start: MAX_SHAPES_PER_BATCH,
24054 end: MAX_SHAPES_PER_BATCH + 1,
24055 blend_mode: BlendMode::SrcOver,
24056 },
24057 ]);
24058
24059 let budget = native_segment_fusion_budget(
24060 &ordered_items,
24061 &shapes,
24062 &[],
24063 &segment,
24064 ShapeBatchLimits::desktop(),
24065 )
24066 .expect("valid plan");
24067
24068 assert_eq!(budget, None);
24069 }
24070
24071 #[cfg(not(target_arch = "wasm32"))]
24072 #[test]
24073 fn native_segment_fusion_budget_rejects_gradient_uniform_overflow() {
24074 let ordered_items = vec![(0, SegmentDrawItem::Shape(0))];
24075 let mut shape = test_shape(0, BlendMode::SrcOver);
24076 let brushes = vec![Brush::linear_gradient(vec![
24077 Color::BLACK;
24078 MAX_GRADIENT_STOPS + 1
24079 ])];
24080 shape.brush = SceneBrush::Gradient(0);
24081 let shapes = vec![shape];
24082 let segment = chunk(&[SegmentBatchPlan::Shape {
24083 start: 0,
24084 end: 1,
24085 blend_mode: BlendMode::SrcOver,
24086 }]);
24087
24088 let budget = native_segment_fusion_budget(
24089 &ordered_items,
24090 &shapes,
24091 &brushes,
24092 &segment,
24093 ShapeBatchLimits::desktop(),
24094 )
24095 .expect("valid plan");
24096
24097 assert_eq!(budget, None);
24098 }
24099
24100 #[cfg(not(target_arch = "wasm32"))]
24101 #[test]
24102 fn native_segment_fusion_partitions_shape_uniform_overflow() {
24103 let desktop_batch_cap = ShapeBatchLimits::desktop().max_shapes_per_batch;
24106 let ordered_items: Vec<_> = (0..=desktop_batch_cap)
24107 .map(|index| (index, SegmentDrawItem::Shape(index)))
24108 .collect();
24109 let shapes: Vec<_> = (0..=desktop_batch_cap)
24110 .map(|index| test_shape(index, BlendMode::SrcOver))
24111 .collect();
24112 let segment = chunk(&[
24113 SegmentBatchPlan::Shape {
24114 start: 0,
24115 end: desktop_batch_cap,
24116 blend_mode: BlendMode::SrcOver,
24117 },
24118 SegmentBatchPlan::Shape {
24119 start: desktop_batch_cap,
24120 end: desktop_batch_cap + 1,
24121 blend_mode: BlendMode::SrcOver,
24122 },
24123 ]);
24124
24125 let partitions = native_segment_fusion_partitions(
24126 &ordered_items,
24127 &shapes,
24128 &[],
24129 &segment,
24130 ShapeBatchLimits::desktop(),
24131 )
24132 .expect("valid plan")
24133 .expect("overflowing segment should be partitionable");
24134
24135 assert_eq!(partitions.len(), 2);
24136 assert_eq!(
24137 partitions[0],
24138 NativeSegmentFusionPartition {
24139 chunk: chunk(&[SegmentBatchPlan::Shape {
24140 start: 0,
24141 end: desktop_batch_cap,
24142 blend_mode: BlendMode::SrcOver,
24143 }]),
24144 budget: NativeSegmentFusionBudget {
24145 shape_count: desktop_batch_cap,
24146 gradient_stop_count: 0,
24147 },
24148 }
24149 );
24150 assert_eq!(
24151 partitions[1],
24152 NativeSegmentFusionPartition {
24153 chunk: chunk(&[SegmentBatchPlan::Shape {
24154 start: desktop_batch_cap,
24155 end: desktop_batch_cap + 1,
24156 blend_mode: BlendMode::SrcOver,
24157 }]),
24158 budget: NativeSegmentFusionBudget {
24159 shape_count: 1,
24160 gradient_stop_count: 0,
24161 },
24162 }
24163 );
24164 }
24165
24166 #[cfg(not(target_arch = "wasm32"))]
24167 #[test]
24168 fn native_segment_fusion_partitions_gradient_uniform_overflow() {
24169 const STOPS_PER_SHAPE: usize = MAX_GRADIENT_STOPS / 2;
24170 let ordered_items = vec![
24171 (0, SegmentDrawItem::Shape(0)),
24172 (1, SegmentDrawItem::Shape(1)),
24173 (2, SegmentDrawItem::Shape(2)),
24174 ];
24175 let mut shapes = Vec::new();
24176 let brushes = vec![Brush::linear_gradient(vec![Color::BLACK; STOPS_PER_SHAPE])];
24177 for index in 0..3 {
24178 let mut shape = test_shape(index, BlendMode::SrcOver);
24179 shape.brush = SceneBrush::Gradient(0);
24180 shapes.push(shape);
24181 }
24182 let segment = chunk(&[SegmentBatchPlan::Shape {
24183 start: 0,
24184 end: 3,
24185 blend_mode: BlendMode::SrcOver,
24186 }]);
24187
24188 let partitions = native_segment_fusion_partitions(
24189 &ordered_items,
24190 &shapes,
24191 &brushes,
24192 &segment,
24193 ShapeBatchLimits::desktop(),
24194 )
24195 .expect("valid plan")
24196 .expect("overflowing gradient segment should be partitionable");
24197
24198 assert_eq!(partitions.len(), 2);
24199 assert_eq!(
24200 partitions[0],
24201 NativeSegmentFusionPartition {
24202 chunk: chunk(&[SegmentBatchPlan::Shape {
24203 start: 0,
24204 end: 2,
24205 blend_mode: BlendMode::SrcOver,
24206 }]),
24207 budget: NativeSegmentFusionBudget {
24208 shape_count: 2,
24209 gradient_stop_count: MAX_GRADIENT_STOPS,
24210 },
24211 }
24212 );
24213 assert_eq!(
24214 partitions[1],
24215 NativeSegmentFusionPartition {
24216 chunk: chunk(&[SegmentBatchPlan::Shape {
24217 start: 2,
24218 end: 3,
24219 blend_mode: BlendMode::SrcOver,
24220 }]),
24221 budget: NativeSegmentFusionBudget {
24222 shape_count: 1,
24223 gradient_stop_count: STOPS_PER_SHAPE,
24224 },
24225 }
24226 );
24227 }
24228
24229 #[cfg(not(target_arch = "wasm32"))]
24230 #[test]
24231 fn native_segment_fusion_accepts_layer_composite_chunks() {
24232 let ordered_items = vec![
24233 (0, SegmentDrawItem::Shape(0)),
24234 (1, SegmentDrawItem::Composite(0)),
24235 (2, SegmentDrawItem::ShaderComposite(0)),
24236 (3, SegmentDrawItem::Shape(1)),
24237 ];
24238 let shapes = vec![
24239 test_shape(0, BlendMode::SrcOver),
24240 test_shape(1, BlendMode::SrcOver),
24241 ];
24242 let segment = chunk(&[
24243 SegmentBatchPlan::Shape {
24244 start: 0,
24245 end: 1,
24246 blend_mode: BlendMode::SrcOver,
24247 },
24248 SegmentBatchPlan::Composite { start: 1, end: 2 },
24249 SegmentBatchPlan::ShaderComposite { start: 2, end: 3 },
24250 SegmentBatchPlan::Shape {
24251 start: 3,
24252 end: 4,
24253 blend_mode: BlendMode::SrcOver,
24254 },
24255 ]);
24256
24257 let partitions = native_segment_fusion_partitions(
24258 &ordered_items,
24259 &shapes,
24260 &[],
24261 &segment,
24262 ShapeBatchLimits::desktop(),
24263 )
24264 .expect("valid plan")
24265 .expect("composites are drawable inside the native fused pass");
24266
24267 assert_eq!(
24268 partitions,
24269 vec![NativeSegmentFusionPartition {
24270 chunk: segment,
24271 budget: NativeSegmentFusionBudget {
24272 shape_count: 2,
24273 gradient_stop_count: 0,
24274 },
24275 }],
24276 "layer composites and shader composites must preserve order without forcing separate render passes"
24277 );
24278 }
24279
24280 #[cfg(not(target_arch = "wasm32"))]
24281 #[test]
24282 fn native_segment_fusion_partitions_preserve_non_shape_order_at_budget_boundary() {
24283 let desktop_batch_cap = ShapeBatchLimits::desktop().max_shapes_per_batch;
24286 let ordered_items: Vec<_> = (0..desktop_batch_cap)
24287 .map(|index| (index, SegmentDrawItem::Shape(index)))
24288 .chain([
24289 (desktop_batch_cap, SegmentDrawItem::Image(0)),
24290 (
24291 desktop_batch_cap + 1,
24292 SegmentDrawItem::Shape(desktop_batch_cap),
24293 ),
24294 ])
24295 .collect();
24296 let shapes: Vec<_> = (0..=desktop_batch_cap)
24297 .map(|index| test_shape(index, BlendMode::SrcOver))
24298 .collect();
24299 let segment = chunk(&[
24300 SegmentBatchPlan::Shape {
24301 start: 0,
24302 end: desktop_batch_cap,
24303 blend_mode: BlendMode::SrcOver,
24304 },
24305 SegmentBatchPlan::Image {
24306 start: desktop_batch_cap,
24307 end: desktop_batch_cap + 1,
24308 blend_mode: BlendMode::SrcOver,
24309 },
24310 SegmentBatchPlan::Shape {
24311 start: desktop_batch_cap + 1,
24312 end: desktop_batch_cap + 2,
24313 blend_mode: BlendMode::SrcOver,
24314 },
24315 ]);
24316
24317 let partitions = native_segment_fusion_partitions(
24318 &ordered_items,
24319 &shapes,
24320 &[],
24321 &segment,
24322 ShapeBatchLimits::desktop(),
24323 )
24324 .expect("valid plan")
24325 .expect("overflowing segment should be partitionable");
24326
24327 assert_eq!(partitions.len(), 2);
24328 assert_eq!(
24329 partitions[0].chunk,
24330 chunk(&[
24331 SegmentBatchPlan::Shape {
24332 start: 0,
24333 end: desktop_batch_cap,
24334 blend_mode: BlendMode::SrcOver,
24335 },
24336 SegmentBatchPlan::Image {
24337 start: desktop_batch_cap,
24338 end: desktop_batch_cap + 1,
24339 blend_mode: BlendMode::SrcOver,
24340 },
24341 ])
24342 );
24343 assert_eq!(
24344 partitions[1].chunk,
24345 chunk(&[SegmentBatchPlan::Shape {
24346 start: desktop_batch_cap + 1,
24347 end: desktop_batch_cap + 2,
24348 blend_mode: BlendMode::SrcOver,
24349 }])
24350 );
24351 }
24352
24353 #[test]
24354 fn segment_command_iter_keeps_repeated_batch_kinds_in_one_chunk() {
24355 let ordered_items = vec![
24356 (0, SegmentDrawItem::Shape(0)),
24357 (1, SegmentDrawItem::Image(0)),
24358 (2, SegmentDrawItem::Shape(1)),
24359 ];
24360 let shapes = vec![
24361 test_shape(0, BlendMode::SrcOver),
24362 test_shape(2, BlendMode::DstOut),
24363 ];
24364 let images = vec![test_image(1, BlendMode::SrcOver)];
24365
24366 let commands: Vec<_> = SegmentCommandIter::new(
24367 &ordered_items,
24368 &shapes,
24369 &images,
24370 ShapeBatchLimits::desktop(),
24371 )
24372 .collect();
24373
24374 assert_eq!(
24375 commands,
24376 vec![SegmentRenderCommand::DrawChunk(chunk(&[
24377 SegmentBatchPlan::Shape {
24378 start: 0,
24379 end: 1,
24380 blend_mode: BlendMode::SrcOver,
24381 },
24382 SegmentBatchPlan::Image {
24383 start: 1,
24384 end: 2,
24385 blend_mode: BlendMode::SrcOver,
24386 },
24387 SegmentBatchPlan::Shape {
24388 start: 2,
24389 end: 3,
24390 blend_mode: BlendMode::DstOut,
24391 },
24392 ]))]
24393 );
24394 }
24395
24396 #[test]
24397 fn segment_command_iter_splits_contiguous_shape_runs_at_uniform_batch_limit() {
24398 let desktop_batch_cap = ShapeBatchLimits::desktop().max_shapes_per_batch;
24401 let ordered_items: Vec<_> = (0..=desktop_batch_cap)
24402 .map(|index| (index, SegmentDrawItem::Shape(index)))
24403 .collect();
24404 let shapes: Vec<_> = (0..=desktop_batch_cap)
24405 .map(|index| test_shape(index, BlendMode::SrcOver))
24406 .collect();
24407 let images = Vec::new();
24408
24409 let commands: Vec<_> = SegmentCommandIter::new(
24410 &ordered_items,
24411 &shapes,
24412 &images,
24413 ShapeBatchLimits::desktop(),
24414 )
24415 .collect();
24416
24417 assert_eq!(
24418 commands,
24419 vec![SegmentRenderCommand::DrawChunk(chunk(&[
24420 SegmentBatchPlan::Shape {
24421 start: 0,
24422 end: desktop_batch_cap,
24423 blend_mode: BlendMode::SrcOver,
24424 },
24425 SegmentBatchPlan::Shape {
24426 start: desktop_batch_cap,
24427 end: desktop_batch_cap + 1,
24428 blend_mode: BlendMode::SrcOver,
24429 },
24430 ]))]
24431 );
24432 }
24433
24434 #[test]
24435 fn segment_command_iter_keeps_shadows_as_explicit_boundaries() {
24436 let ordered_items = vec![
24437 (0, SegmentDrawItem::Shape(0)),
24438 (1, SegmentDrawItem::Shadow(0)),
24439 (2, SegmentDrawItem::Image(0)),
24440 (3, SegmentDrawItem::Text(0)),
24441 ];
24442 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
24443 let images = vec![test_image(2, BlendMode::SrcOver)];
24444
24445 let commands: Vec<_> = SegmentCommandIter::new(
24446 &ordered_items,
24447 &shapes,
24448 &images,
24449 ShapeBatchLimits::desktop(),
24450 )
24451 .collect();
24452
24453 assert_eq!(
24454 commands,
24455 vec![
24456 SegmentRenderCommand::DrawChunk(chunk(&[SegmentBatchPlan::Shape {
24457 start: 0,
24458 end: 1,
24459 blend_mode: BlendMode::SrcOver,
24460 }])),
24461 SegmentRenderCommand::Shadow(0),
24462 SegmentRenderCommand::DrawChunk(chunk(&[
24463 SegmentBatchPlan::Image {
24464 start: 2,
24465 end: 3,
24466 blend_mode: BlendMode::SrcOver,
24467 },
24468 SegmentBatchPlan::Text { start: 3, end: 4 },
24469 ])),
24470 ]
24471 );
24472 }
24473
24474 #[test]
24475 fn staged_buffer_uploads_align_new_copies_to_copy_buffer_alignment() {
24476 let mut uploads = StagedBufferUploads::default();
24477 uploads.bytes.extend_from_slice(&[1, 2]);
24478
24479 uploads.stage(UploadTarget::ImageIndex, &[3, 4, 5, 6]);
24480
24481 assert_eq!(uploads.bytes, vec![1, 2, 0, 0, 3, 4, 5, 6]);
24482 assert_eq!(
24483 uploads.copies,
24484 vec![PendingBufferCopy {
24485 source_offset: 4,
24486 target_offset: 0,
24487 size: 4,
24488 target: UploadTarget::ImageIndex,
24489 }]
24490 );
24491 }
24492
24493 #[test]
24494 fn staged_buffer_uploads_ignore_empty_payloads() {
24495 let mut uploads = StagedBufferUploads::default();
24496
24497 uploads.stage(UploadTarget::Uniform, &[]);
24498
24499 assert!(uploads.is_empty());
24500 assert!(uploads.bytes.is_empty());
24501 }
24502
24503 #[test]
24504 fn staged_buffer_uploads_return_exact_payload_slice_for_copy() {
24505 let mut uploads = StagedBufferUploads::default();
24506 uploads.stage(UploadTarget::Uniform, &[1, 2, 3, 4]);
24507 uploads.stage(UploadTarget::ImageIndex, &[5, 6, 7, 8]);
24508
24509 assert_eq!(uploads.payload_for_copy(uploads.copies[0]), &[1, 2, 3, 4]);
24510 assert_eq!(uploads.payload_for_copy(uploads.copies[1]), &[5, 6, 7, 8]);
24511 }
24512
24513 #[test]
24514 fn staged_buffer_uploads_record_destination_offsets() {
24515 let mut uploads = StagedBufferUploads::default();
24516
24517 uploads.stage_at(UploadTarget::ImageIndex, 256, &[1, 2, 3, 4]);
24518
24519 assert_eq!(uploads.copies[0].target_offset, 256);
24520 assert_eq!(uploads.payload_for_copy(uploads.copies[0]), &[1, 2, 3, 4]);
24521 }
24522
24523 #[test]
24524 fn staged_buffer_uploads_truncate_restores_previous_state() {
24525 let mut uploads = StagedBufferUploads::default();
24526 uploads.stage(UploadTarget::Uniform, &[1, 2, 3, 4]);
24527 let bytes_len = uploads.bytes.len();
24528 let copies_len = uploads.copies.len();
24529 uploads.stage(UploadTarget::ImageIndex, &[5, 6, 7, 8]);
24530
24531 uploads.truncate(bytes_len, copies_len);
24532
24533 assert_eq!(uploads.bytes, vec![1, 2, 3, 4]);
24534 assert_eq!(uploads.copies.len(), 1);
24535 }
24536
24537 #[test]
24538 fn inner_shadow_composite_mask_uses_fill_shape_and_scale() {
24539 let mut fill = test_shape(0, BlendMode::SrcOver);
24540 fill.local_rect = Rect {
24541 x: 10.0,
24542 y: 12.0,
24543 width: 40.0,
24544 height: 20.0,
24545 };
24546 fill.shape = Some(RoundedCornerShape::uniform(6.0));
24547
24548 let cutout = test_shape(1, BlendMode::DstOut);
24549 let shadow = test_shadow_draw(vec![
24550 (fill, BlendMode::SrcOver),
24551 (cutout, BlendMode::DstOut),
24552 ]);
24553
24554 let mask = inner_shadow_composite_mask(&shadow, 1.5).expect("inner mask expected");
24555 assert_eq!(mask.rect, [15.0, 18.0, 60.0, 30.0]);
24556 assert_eq!(mask.radii, [9.0, 9.0, 9.0, 9.0]);
24557 }
24558
24559 #[test]
24560 fn inner_shadow_composite_mask_is_none_without_dst_out() {
24561 let fill = test_shape(0, BlendMode::SrcOver);
24562 let shadow = test_shadow_draw(vec![(fill, BlendMode::SrcOver)]);
24563 assert!(inner_shadow_composite_mask(&shadow, 1.0).is_none());
24564 }
24565
24566 #[test]
24567 fn render_effect_support_matrix_covers_all_variants() {
24568 let blur = RenderEffect::blur(4.0);
24569 let offset = RenderEffect::offset(2.0, 3.0);
24570 let shader = RenderEffect::runtime_shader(cranpose_ui_graphics::RuntimeShader::new(
24571 r#"
24572 @group(0) @binding(0) var input_texture: texture_2d<f32>;
24573 @group(0) @binding(1) var input_sampler: sampler;
24574 @group(1) @binding(0) var<uniform> u: array<vec4<f32>, 64>;
24575 struct VertexOutput {
24576 @builtin(position) position: vec4<f32>,
24577 @location(0) uv: vec2<f32>,
24578 }
24579 @vertex
24580 fn fullscreen_vs(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
24581 var output: VertexOutput;
24582 let x = f32(i32(vertex_index & 1u) * 2 - 1);
24583 let y = f32(i32(vertex_index >> 1u) * 2 - 1);
24584 output.uv = vec2<f32>(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
24585 output.position = vec4<f32>(x, y, 0.0, 1.0);
24586 return output;
24587 }
24588 @fragment
24589 fn effect_fs(input: VertexOutput) -> @location(0) vec4<f32> {
24590 return textureSample(input_texture, input_sampler, input.uv);
24591 }
24592 "#,
24593 ));
24594 let chain = blur.clone().then(offset.clone());
24595
24596 assert!(is_render_effect_supported(&blur));
24597 assert!(is_render_effect_supported(&offset));
24598 assert!(is_render_effect_supported(&shader));
24599 assert!(is_render_effect_supported(&chain));
24600 }
24601
24602 #[test]
24603 fn clip_to_bounds_propagates_visual_clip_to_all_descendant_shapes() {
24604 let container_local_bounds = Rect {
24607 x: 0.0,
24608 y: 0.0,
24609 width: 800.0,
24610 height: 500.0,
24611 };
24612 let container_clip_in_parent = Rect {
24614 x: 0.0,
24615 y: 50.0,
24616 width: 800.0,
24617 height: 500.0,
24618 };
24619
24620 let shape_above = RenderNode::Primitive(PrimitiveEntry {
24622 phase: PrimitivePhase::BeforeChildren,
24623 node: PrimitiveNode::Draw(DrawPrimitiveNode {
24624 primitive: DrawPrimitive::Rect {
24625 rect: Rect {
24626 x: 10.0,
24627 y: -30.0,
24628 width: 100.0,
24629 height: 40.0,
24630 },
24631 brush: Brush::solid(Color::WHITE),
24632 stroke: None,
24633 },
24634 clip: None,
24635 }),
24636 });
24637
24638 let shape_inside = RenderNode::Primitive(PrimitiveEntry {
24640 phase: PrimitivePhase::BeforeChildren,
24641 node: PrimitiveNode::Draw(DrawPrimitiveNode {
24642 primitive: DrawPrimitive::Rect {
24643 rect: Rect {
24644 x: 10.0,
24645 y: 100.0,
24646 width: 100.0,
24647 height: 40.0,
24648 },
24649 brush: Brush::solid(Color::WHITE),
24650 stroke: None,
24651 },
24652 clip: None,
24653 }),
24654 });
24655
24656 let shape_below = RenderNode::Primitive(PrimitiveEntry {
24658 phase: PrimitivePhase::BeforeChildren,
24659 node: PrimitiveNode::Draw(DrawPrimitiveNode {
24660 primitive: DrawPrimitive::Rect {
24661 rect: Rect {
24662 x: 10.0,
24663 y: 600.0,
24664 width: 100.0,
24665 height: 40.0,
24666 },
24667 brush: Brush::solid(Color::WHITE),
24668 stroke: None,
24669 },
24670 clip: None,
24671 }),
24672 });
24673
24674 let mut content_layer = test_layer(
24676 Rect {
24677 x: 0.0,
24678 y: 0.0,
24679 width: 800.0,
24680 height: 1000.0,
24681 },
24682 vec![shape_above, shape_inside, shape_below],
24683 );
24684 content_layer.transform_to_parent = ProjectiveTransform::translation(0.0, -30.0);
24685 content_layer.translated_content_context = true;
24686
24687 let mut clip_container = test_layer(
24689 container_local_bounds,
24690 vec![RenderNode::Layer(Box::new(content_layer))],
24691 );
24692 clip_container.clip_to_bounds = true;
24693 clip_container.transform_to_parent = ProjectiveTransform::translation(0.0, 50.0);
24694
24695 let root = test_layer(
24697 Rect {
24698 x: 0.0,
24699 y: 0.0,
24700 width: 800.0,
24701 height: 600.0,
24702 },
24703 vec![RenderNode::Layer(Box::new(clip_container))],
24704 );
24705
24706 let mut rect_cache = HashMap::new();
24707 let mut requirements_cache = HashMap::new();
24708 let collected =
24709 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
24710
24711 assert_eq!(
24712 collected.scene.shapes.len(),
24713 3,
24714 "all three shapes should be flattened into the scene"
24715 );
24716
24717 for (i, shape) in collected.scene.shapes.iter().enumerate() {
24718 assert!(
24719 shape.clip.is_some(),
24720 "shape {} at rect {:?} must have a clip from clip_to_bounds container, but clip is None",
24721 i,
24722 shape.rect
24723 );
24724 let clip = shape.clip.unwrap();
24725 assert_eq!(
24726 clip, container_clip_in_parent,
24727 "shape {} clip should match the clip_to_bounds container bounds in parent space",
24728 i
24729 );
24730 }
24731 }
24732
24733 #[test]
24734 fn clip_to_bounds_culls_child_layers_outside_boundary() {
24735 let clip_container_bounds = Rect {
24743 x: 0.0,
24744 y: 0.0,
24745 width: 800.0,
24746 height: 500.0,
24747 };
24748
24749 let shape_in_card = RenderNode::Primitive(PrimitiveEntry {
24750 phase: PrimitivePhase::BeforeChildren,
24751 node: PrimitiveNode::Draw(DrawPrimitiveNode {
24752 primitive: DrawPrimitive::Rect {
24753 rect: Rect {
24754 x: 0.0,
24755 y: 0.0,
24756 width: 300.0,
24757 height: 80.0,
24758 },
24759 brush: Brush::solid(Color::WHITE),
24760 stroke: None,
24761 },
24762 clip: None,
24763 }),
24764 });
24765
24766 let mut card_outside = crate::test_support::layer_node(
24768 Rect {
24769 x: 0.0,
24770 y: 0.0,
24771 width: 300.0,
24772 height: 80.0,
24773 },
24774 ProjectiveTransform::identity(),
24775 GraphicsLayer {
24776 clip: true,
24777 ..GraphicsLayer::default()
24778 },
24779 vec![shape_in_card.clone()],
24780 );
24781 card_outside.transform_to_parent = ProjectiveTransform::translation(10.0, 600.0);
24782
24783 let mut card_inside = crate::test_support::layer_node(
24785 Rect {
24786 x: 0.0,
24787 y: 0.0,
24788 width: 300.0,
24789 height: 80.0,
24790 },
24791 ProjectiveTransform::identity(),
24792 GraphicsLayer {
24793 clip: true,
24794 ..GraphicsLayer::default()
24795 },
24796 vec![shape_in_card],
24797 );
24798 card_inside.transform_to_parent = ProjectiveTransform::translation(10.0, 100.0);
24799
24800 let content = test_layer(
24802 Rect {
24803 x: 0.0,
24804 y: 0.0,
24805 width: 800.0,
24806 height: 1000.0,
24807 },
24808 vec![
24809 RenderNode::Layer(Box::new(card_inside)),
24810 RenderNode::Layer(Box::new(card_outside)),
24811 ],
24812 );
24813
24814 let mut clip_container = test_layer(
24816 clip_container_bounds,
24817 vec![RenderNode::Layer(Box::new(content))],
24818 );
24819 clip_container.clip_to_bounds = true;
24820
24821 let root = test_layer(
24823 Rect {
24824 x: 0.0,
24825 y: 0.0,
24826 width: 800.0,
24827 height: 600.0,
24828 },
24829 vec![RenderNode::Layer(Box::new(clip_container))],
24830 );
24831
24832 let mut rect_cache = HashMap::new();
24833 let mut requirements_cache = HashMap::new();
24834 let collected =
24835 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
24836
24837 assert_eq!(
24838 collected.scene.shapes.len(),
24839 1,
24840 "only the card inside the clip boundary should produce shapes; \
24841 the card outside must be culled entirely"
24842 );
24843
24844 let shape = &collected.scene.shapes[0];
24845 assert!(
24846 shape.clip.is_some(),
24847 "the visible card's shape must have a clip from clip_to_bounds"
24848 );
24849 }
24850
24851 #[test]
24852 fn flattened_layer_shadow_z_index_is_below_content() {
24853 let shape = RenderNode::Primitive(PrimitiveEntry {
24857 phase: PrimitivePhase::BeforeChildren,
24858 node: PrimitiveNode::Draw(DrawPrimitiveNode {
24859 primitive: DrawPrimitive::Rect {
24860 rect: Rect {
24861 x: 0.0,
24862 y: 0.0,
24863 width: 100.0,
24864 height: 100.0,
24865 },
24866 brush: Brush::solid(Color::WHITE),
24867 stroke: None,
24868 },
24869 clip: None,
24870 }),
24871 });
24872
24873 let child_bounds = Rect {
24874 x: 0.0,
24875 y: 0.0,
24876 width: 100.0,
24877 height: 100.0,
24878 };
24879
24880 let child = crate::test_support::layer_node(
24881 child_bounds,
24882 ProjectiveTransform::translation(50.0, 50.0),
24883 GraphicsLayer {
24884 shadow_elevation: 20.0,
24885 ..GraphicsLayer::default()
24886 },
24887 vec![shape],
24888 );
24889
24890 let root = test_layer(
24891 Rect {
24892 x: 0.0,
24893 y: 0.0,
24894 width: 800.0,
24895 height: 600.0,
24896 },
24897 vec![RenderNode::Layer(Box::new(child))],
24898 );
24899
24900 let mut rect_cache = HashMap::new();
24901 let mut requirements_cache = HashMap::new();
24902 let collected =
24903 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
24904
24905 assert!(
24906 !collected.scene.shadow_draws.is_empty(),
24907 "shadow_elevation > 0 must produce shadow draws"
24908 );
24909 let max_shadow_z = collected
24910 .scene
24911 .shadow_draws
24912 .iter()
24913 .map(|s| s.z_index)
24914 .max()
24915 .unwrap();
24916 let min_content_z = collected
24917 .scene
24918 .shapes
24919 .iter()
24920 .map(|s| s.z_index)
24921 .min()
24922 .unwrap();
24923 assert!(
24924 max_shadow_z < min_content_z,
24925 "shadow z-index ({}) must be less than content z-index ({}); \
24926 shadows must render behind their content",
24927 max_shadow_z,
24928 min_content_z
24929 );
24930 }
24931
24932 #[cfg(not(target_arch = "wasm32"))]
24935 fn bundle_op(slot: u32, epoch: Option<u64>, first: u32, last: u32) -> RetainedBundleOpKey {
24936 RetainedBundleOpKey {
24937 slot,
24938 capture_epoch: epoch,
24939 first,
24940 last,
24941 retained_index: slot,
24942 has_mesh: false,
24943 }
24944 }
24945
24946 #[cfg(not(target_arch = "wasm32"))]
24947 fn bundle_key(ops: &[RetainedBundleOpKey]) -> RetainedBundleKey {
24948 RetainedBundleKey {
24949 depth: false,
24950 ops: ops.to_vec(),
24951 }
24952 }
24953
24954 #[cfg(not(target_arch = "wasm32"))]
24957 #[test]
24958 fn retained_bundle_cache_reuses_stable_keys() {
24959 let mut cache: RetainedBundleCacheImpl<u32> = RetainedBundleCacheImpl::new();
24960 let ops = [bundle_op(3, Some(7), 0, 40), bundle_op(5, Some(9), 4, 12)];
24961 let key = bundle_key(&ops);
24962
24963 assert!(!cache.hit(&key), "empty cache must miss");
24964 cache.insert(key.clone(), 111);
24965 assert_eq!(cache.get(&key), Some(&111));
24966 cache.end_frame();
24967
24968 for _ in 0..3 {
24969 assert!(cache.hit(&bundle_key(&ops)), "stable key must stay cached");
24970 cache.end_frame();
24971 }
24972 assert_eq!(cache.stats(), (1, 3), "one rebuild, three cached executes");
24973 }
24974
24975 #[cfg(not(target_arch = "wasm32"))]
24979 #[test]
24980 fn retained_bundle_cache_invalidates_on_any_op_change() {
24981 let ops = [bundle_op(3, Some(7), 0, 40), bundle_op(5, Some(9), 4, 12)];
24982 let variants: [Vec<RetainedBundleOpKey>; 5] = [
24983 vec![bundle_op(3, Some(8), 0, 40), bundle_op(5, Some(9), 4, 12)],
24985 vec![bundle_op(5, Some(9), 4, 12), bundle_op(3, Some(7), 0, 40)],
24987 vec![bundle_op(3, Some(7), 0, 40)],
24989 vec![bundle_op(3, Some(7), 0, 41), bundle_op(5, Some(9), 4, 12)],
24991 vec![bundle_op(3, Some(7), 0, 40), bundle_op(5, None, 4, 12)],
24993 ];
24994 for changed in variants {
24995 let mut cache: RetainedBundleCacheImpl<u32> = RetainedBundleCacheImpl::new();
24996 cache.insert(bundle_key(&ops), 111);
24997 cache.end_frame();
24998 assert!(
24999 !cache.hit(&RetainedBundleKey {
25000 depth: false,
25001 ops: changed.clone()
25002 }),
25003 "changed key {changed:?} must not reuse the stale bundle"
25004 );
25005 }
25006 }
25007
25008 #[cfg(not(target_arch = "wasm32"))]
25012 #[test]
25013 fn retained_bundle_cache_keys_depth_variants_apart() {
25014 let mut cache: RetainedBundleCacheImpl<u32> = RetainedBundleCacheImpl::new();
25015 let ops = vec![bundle_op(3, Some(7), 0, 40)];
25016 cache.insert(
25017 RetainedBundleKey {
25018 depth: false,
25019 ops: ops.clone(),
25020 },
25021 111,
25022 );
25023 cache.end_frame();
25024 assert!(
25025 !cache.hit(&RetainedBundleKey { depth: true, ops }),
25026 "a flat bundle must not replay into the display-clip culled pass"
25027 );
25028 }
25029
25030 #[cfg(not(target_arch = "wasm32"))]
25034 #[test]
25035 fn retained_bundle_cache_evicts_unused_entries() {
25036 let mut cache: RetainedBundleCacheImpl<u32> = RetainedBundleCacheImpl::new();
25037 let stale = bundle_key(&[bundle_op(1, Some(1), 0, 6)]);
25038 let live = bundle_key(&[bundle_op(2, Some(2), 0, 6)]);
25039 cache.insert(stale.clone(), 1);
25040 cache.insert(live.clone(), 2);
25041 cache.end_frame();
25042
25043 assert!(cache.hit(&live));
25044 cache.end_frame();
25045
25046 assert!(
25047 !cache.hit(&stale),
25048 "entry unused for a frame must have been evicted"
25049 );
25050 assert!(cache.hit(&live), "used entry must survive eviction");
25051
25052 cache.clear();
25053 assert!(!cache.hit(&live), "clear must drop every entry");
25054 }
25055}