1use super::layer_surface::{
2 replay_batches_by_directional_priority, surface_intersects_coverage_bounds, LayerSurfaceEntry,
3 LayerSurfaceLayout, LayoutChangeKind, ReplayPriorityEntry, SurfaceReplayCoordinator,
4};
5#[cfg(debug_assertions)]
6use super::layer_surface::{visible_surface_escape, VisibleSurfaceEscape};
7use kurbo::{BezPath, PathEl, Rect, Shape};
8use pax_gpu::{
9 point, Box2D, DrawRange as PixelDrawRange, Image, LightShape as PixelLightShape,
10 Material as PixelMaterial, Path, ResourceChurnStats, SceneLight as PixelSceneLight,
11 SceneLighting as PixelSceneLighting, Stroke as PixelStroke, StrokeCap as PixelStrokeCap,
12 StrokeJoin as PixelStrokeJoin, Transform2D, WgpuRenderer,
13};
14use pax_runtime_api::{
15 Axis, LayerSurfaceScreenshotData, Material, PathSmoothing, RenderContext,
16 ReplayCanvasLayerUpdate, SceneLighting, ScreenshotData, Stroke, StrokeCap, StrokeJoin,
17};
18use std::{
19 cell::{Cell, RefCell},
20 collections::{HashMap, HashSet, VecDeque},
21 future::Future,
22 pin::Pin,
23 rc::Rc,
24};
25
26#[cfg(not(target_arch = "wasm32"))]
27use pollster;
28
29pub struct LayerRenderer {
31 key: String,
32 host_signature: String,
33 renderer: WgpuRenderer<'static>,
34 origin_x: f32,
35 origin_y: f32,
36 logical_width: f32,
37 logical_height: f32,
38 surface_width: u32,
39 surface_height: u32,
40 dpr: [f32; 2],
41}
42
43pub struct LayerTarget {
45 renderers: Vec<LayerRenderer>,
46 active: bool,
47 needs_replay: bool,
48}
49
50#[cfg(debug_assertions)]
51#[derive(Clone, Copy, Default)]
52struct TileCullStats {
53 nodes_considered: u64,
54 selected_surfaces: u64,
55 skipped_surfaces: u64,
56 stale_surface_removal_attempts: u64,
57 origin_only_retargets: u64,
58 targeted_replay_batches: u64,
59 targeted_replay_surfaces: u64,
60 targeted_replay_visible_batches: u64,
61 targeted_replay_warm_batches: u64,
62 targeted_replay_visible_surfaces: u64,
63 targeted_replay_warm_surfaces: u64,
64 full_layer_replays: u64,
65 resize_resets: u64,
66}
67
68#[cfg(debug_assertions)]
69fn replay_priority_stats(
70 entries: &[ReplayPriorityEntry],
71 batches: &[Vec<usize>],
72) -> (usize, usize, usize, usize) {
73 let visible_surfaces = entries.iter().filter(|entry| entry.priority <= 0).count();
74 let warm_surfaces = entries.len().saturating_sub(visible_surfaces);
75 let priority_by_index: HashMap<usize, i32> = entries
76 .iter()
77 .map(|entry| (entry.index, entry.priority))
78 .collect();
79 let visible_batches = batches
80 .iter()
81 .filter(|batch| {
82 batch.iter().any(|index| {
83 priority_by_index
84 .get(index)
85 .map_or(false, |priority| *priority <= 0)
86 })
87 })
88 .count();
89 let warm_batches = batches.len().saturating_sub(visible_batches);
90
91 (
92 visible_batches,
93 warm_batches,
94 visible_surfaces,
95 warm_surfaces,
96 )
97}
98
99#[cfg(debug_assertions)]
100fn log_visible_surface_escape(layer: usize, escape: VisibleSurfaceEscape) {
101 log::warn!(
102 "[pax-tile-window-escape] backend=wgpu layer={} visible_surfaces={} escaped_visible_surfaces={} max_gap_x={:.1} max_gap_y={:.1} previous_bounds={} visible_bounds={}",
103 layer,
104 escape.visible_surfaces,
105 escape.escaped_visible_surfaces,
106 escape.max_gap_x,
107 escape.max_gap_y,
108 format_rect(escape.previous_bounds),
109 format_rect(escape.visible_bounds),
110 );
111}
112
113#[cfg(debug_assertions)]
114fn format_rect(bounds: Rect) -> String {
115 format!(
116 "{:.1},{:.1}-{:.1},{:.1}",
117 bounds.x0, bounds.y0, bounds.x1, bounds.y1
118 )
119}
120
121#[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
122fn should_print_gpu_profile_logs() -> bool {
123 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
124 *ENABLED.get_or_init(|| {
125 std::env::var("PAX_GPU_PROFILE")
126 .map(|value| {
127 let value = value.trim().to_ascii_lowercase();
128 !(value.is_empty() || value == "0" || value == "false" || value == "off")
129 })
130 .unwrap_or(false)
131 })
132}
133
134type LayerDef = (LayerTarget, Pin<Box<dyn Fn() -> LayerSurfaceLayout>>);
135
136impl LayerRenderer {
137 pub fn new(
139 key: String,
140 host_signature: String,
141 renderer: WgpuRenderer<'static>,
142 origin_x: f32,
143 origin_y: f32,
144 logical_width: f32,
145 logical_height: f32,
146 surface_width: u32,
147 surface_height: u32,
148 dpr: [f32; 2],
149 ) -> Self {
150 Self {
151 key,
152 host_signature,
153 renderer,
154 origin_x,
155 origin_y,
156 logical_width,
157 logical_height,
158 surface_width,
159 surface_height,
160 dpr,
161 }
162 }
163
164 pub fn renderer_mut(&mut self) -> &mut WgpuRenderer<'static> {
166 &mut self.renderer
167 }
168
169 fn intersects_coverage_bounds(&self, bounds: &Rect) -> bool {
170 surface_intersects_coverage_bounds(
171 bounds,
172 self.origin_x as f64,
173 self.origin_y as f64,
174 self.logical_width as f64,
175 self.logical_height as f64,
176 )
177 }
178
179 fn coverage_bounds(&self) -> Rect {
180 Rect::new(
181 self.origin_x as f64,
182 self.origin_y as f64,
183 self.origin_x as f64 + self.logical_width as f64,
184 self.origin_y as f64 + self.logical_height as f64,
185 )
186 }
187
188 fn update_layout(&mut self, surface: &LayerSurfaceEntry) -> LayoutChangeKind {
189 let origin_changed = (self.origin_x - surface.origin_x).abs() > f32::EPSILON
190 || (self.origin_y - surface.origin_y).abs() > f32::EPSILON;
191 let size_changed = self.logical_width != surface.surface.logical_width
192 || self.logical_height != surface.surface.logical_height
193 || self.surface_width != surface.surface.surface_width
194 || self.surface_height != surface.surface.surface_height
195 || self.dpr != surface.surface.dpr;
196
197 self.origin_x = surface.origin_x;
198 self.origin_y = surface.origin_y;
199 self.logical_width = surface.surface.logical_width;
200 self.logical_height = surface.surface.logical_height;
201 self.surface_width = surface.surface.surface_width;
202 self.surface_height = surface.surface.surface_height;
203 self.dpr = surface.surface.dpr;
204
205 if size_changed {
206 LayoutChangeKind::Resized
207 } else if origin_changed {
208 LayoutChangeKind::OriginOnly
209 } else {
210 LayoutChangeKind::Unchanged
211 }
212 }
213
214 pub fn sync_layout_metadata(&mut self, surface: &LayerSurfaceEntry) {
217 self.update_layout(surface);
218 }
219}
220
221impl LayerTarget {
222 pub fn new(mut renderers: Vec<LayerRenderer>, active: bool) -> Self {
224 if let Some((first, rest)) = renderers.split_first_mut() {
227 for renderer in rest {
228 renderer.renderer.share_vector_caches_from(&first.renderer);
229 }
230 }
231 Self {
232 renderers,
233 active,
234 needs_replay: false,
235 }
236 }
237
238 pub fn renderers_mut(&mut self) -> &mut [LayerRenderer] {
240 &mut self.renderers
241 }
242
243 fn activate(&mut self) {
244 if !self.active {
245 self.active = true;
246 self.needs_replay = true;
247 }
248 }
249
250 fn deactivate(&mut self) {
251 self.active = false;
252 }
253
254 fn prepare_for_render(&mut self) {
255 if !self.needs_replay {
256 return;
257 }
258 for renderer in &mut self.renderers {
259 renderer.renderer.reset_retained_scene();
260 }
261 self.needs_replay = false;
262 }
263}
264
265fn layer_layout_matches_target(target: &LayerTarget, layout: &LayerSurfaceLayout) -> bool {
266 layout.surfaces.len() == target.renderers.len()
270 && layout
271 .surfaces
272 .iter()
273 .zip(target.renderers.iter())
274 .all(|(surface, renderer)| {
275 surface.key == renderer.key && surface.host_signature == renderer.host_signature
276 })
277}
278
279fn layer_layout_matches_bootstrapped_target(
280 target: &LayerTarget,
281 layout: &LayerSurfaceLayout,
282) -> bool {
283 target.active == layout.active
287 && layer_layout_matches_target(target, layout)
288 && layout
289 .surfaces
290 .iter()
291 .zip(target.renderers.iter())
292 .all(|(surface, renderer)| {
293 (surface.origin_x - renderer.origin_x).abs() <= f32::EPSILON
294 && (surface.origin_y - renderer.origin_y).abs() <= f32::EPSILON
295 && (surface.surface.logical_width - renderer.logical_width).abs()
296 <= f32::EPSILON
297 && (surface.surface.logical_height - renderer.logical_height).abs()
298 <= f32::EPSILON
299 && surface.surface.surface_width == renderer.surface_width
300 && surface.surface.surface_height == renderer.surface_height
301 && (surface.surface.dpr[0] - renderer.dpr[0]).abs() <= f32::EPSILON
302 && (surface.surface.dpr[1] - renderer.dpr[1]).abs() <= f32::EPSILON
303 })
304}
305
306const MAX_CONCURRENT_LAYER_INITIALIZATIONS: usize = 4;
307
308fn pump_layer_initialization_queue(
309 factory: Rc<dyn Fn(usize) -> Pin<Box<dyn Future<Output = Option<LayerDef>>>>>,
310 backends: Rc<RefCell<Vec<RenderLayerState>>>,
311 queue: Rc<RefCell<VecDeque<usize>>>,
312 scheduled: Rc<RefCell<HashSet<usize>>>,
313 in_flight: Rc<Cell<usize>>,
314 ready_layers: Rc<RefCell<Vec<usize>>>,
315) {
316 while in_flight.get() < MAX_CONCURRENT_LAYER_INITIALIZATIONS {
317 let next_layer = queue.borrow_mut().pop_front();
318 let Some(layer_index) = next_layer else {
319 break;
320 };
321
322 in_flight.set(in_flight.get() + 1);
323 let factory = Rc::clone(&factory);
324 let backends = Rc::clone(&backends);
325 let queue = Rc::clone(&queue);
326 let scheduled = Rc::clone(&scheduled);
327 let in_flight_count = Rc::clone(&in_flight);
328 let ready_layers = Rc::clone(&ready_layers);
329
330 let task = async move {
331 let backend = (factory)(layer_index).await;
332 let mut should_requeue = false;
333 match backend {
334 Some(layer_def) => {
335 let current_layout = layer_def.1();
336 let layout_matches =
337 layer_layout_matches_bootstrapped_target(&layer_def.0, ¤t_layout);
338 let mut backend_states = backends.borrow_mut();
339 match backend_states.get_mut(layer_index) {
340 Some(change) if layout_matches => {
341 *change = RenderLayerState::Ready(layer_def);
342 ready_layers.borrow_mut().push(layer_index);
343 }
344 Some(change) => {
345 *change = RenderLayerState::Pending;
350 should_requeue = true;
351 }
352 None => {
353 log::warn!(
354 "failed to set poll state to ready: layer {} doesn't exist anymore",
355 layer_index
356 );
357 }
358 }
359 }
360 None => match backends.borrow_mut().get_mut(layer_index) {
361 Some(change) => {
362 *change = RenderLayerState::Failed;
363 log::warn!(
364 "failed to initialize render backend for layer {}",
365 layer_index
366 );
367 }
368 None => log::warn!(
369 "failed to set poll state to ready: layer {} doesn't exist and backend failed to initialize",
370 layer_index
371 ),
372 },
373 }
374
375 scheduled.borrow_mut().remove(&layer_index);
376 if should_requeue && scheduled.borrow_mut().insert(layer_index) {
377 queue.borrow_mut().push_back(layer_index);
378 }
379
380 in_flight_count.set(in_flight_count.get().saturating_sub(1));
381 pump_layer_initialization_queue(
382 factory,
383 backends,
384 queue,
385 scheduled,
386 in_flight_count,
387 ready_layers,
388 );
389 };
390
391 #[cfg(target_arch = "wasm32")]
392 wasm_bindgen_futures::spawn_local(task);
393 #[cfg(not(target_arch = "wasm32"))]
394 {
395 pollster::block_on(task);
396 }
397 }
398}
399pub struct PaxGpuRenderer {
401 backends: Rc<RefCell<Vec<RenderLayerState>>>,
402 layer_factory: Rc<dyn Fn(usize) -> Pin<Box<dyn Future<Output = Option<LayerDef>>>>>,
403 image_map: HashMap<String, Image>,
404 image_versions: HashMap<String, u64>,
405 failed_context_gets: RefCell<Vec<bool>>,
406 ready_layers: Rc<RefCell<Vec<usize>>>,
407 replay_layers: Rc<RefCell<Vec<usize>>>,
408 pending_layer_initializations: Rc<RefCell<VecDeque<usize>>>,
409 scheduled_layer_initializations: Rc<RefCell<HashSet<usize>>>,
410 layer_initializations_in_flight: Rc<Cell<usize>>,
411 active_render_scopes: RefCell<Vec<Vec<Vec<usize>>>>,
412 dirty_render_surfaces: RefCell<Vec<HashSet<usize>>>,
413 last_scene_lighting: RefCell<Vec<Option<PixelSceneLighting>>>,
414 surface_replay: RefCell<SurfaceReplayCoordinator>,
415 clean_skipped_canvas_nodes: RefCell<HashSet<(usize, u32)>>,
416 #[cfg(debug_assertions)]
417 tile_cull_stats: RefCell<Vec<TileCullStats>>,
418}
419
420pub enum RenderLayerState {
422 Pending,
423 Failed,
424 Ready(LayerDef),
425}
426
427impl PaxGpuRenderer {
428 pub fn new(
430 layer_factory: impl Fn(usize) -> Pin<Box<dyn Future<Output = Option<LayerDef>>>> + 'static,
431 ) -> Self {
432 Self {
433 backends: Default::default(),
434 layer_factory: Rc::new(layer_factory),
435 image_map: Default::default(),
436 image_versions: Default::default(),
437 failed_context_gets: RefCell::new(vec![]),
438 ready_layers: Default::default(),
439 replay_layers: Default::default(),
440 pending_layer_initializations: Rc::new(RefCell::new(VecDeque::new())),
441 scheduled_layer_initializations: Rc::new(RefCell::new(HashSet::new())),
442 layer_initializations_in_flight: Rc::new(Cell::new(0)),
443 active_render_scopes: Default::default(),
444 dirty_render_surfaces: Default::default(),
445 last_scene_lighting: Default::default(),
446 surface_replay: Default::default(),
447 clean_skipped_canvas_nodes: Default::default(),
448 #[cfg(debug_assertions)]
449 tile_cull_stats: Default::default(),
450 }
451 }
452}
453
454impl PaxGpuRenderer {
455 fn queue_layer_initialization(&self, layer_index: usize) {
456 if self
457 .scheduled_layer_initializations
458 .borrow_mut()
459 .insert(layer_index)
460 {
461 self.pending_layer_initializations
462 .borrow_mut()
463 .push_back(layer_index);
464 }
465 }
466
467 fn enqueue_layer_initialization(&self, layer_index: usize) {
468 self.queue_layer_initialization(layer_index);
469 self.schedule_layer_initialization();
470 }
471
472 fn with_layer_context(&self, layer: usize, mut f: impl FnMut(&mut WgpuRenderer)) {
473 let mut backends = self.backends.borrow_mut();
474 match backends.get_mut(layer) {
475 Some(layer_state) => match layer_state {
476 RenderLayerState::Pending => {
477 let mut failed_context_gets = self.failed_context_gets.borrow_mut();
478 if failed_context_gets.len() <= layer {
479 failed_context_gets.resize(layer + 1, false);
480 }
481 failed_context_gets[layer] = true;
482 }
483 RenderLayerState::Failed => {}
484 RenderLayerState::Ready((target, _)) => {
485 if !target.active {
486 return;
487 }
488 target.prepare_for_render();
489 let scoped_indices = self.active_render_scope(layer).or_else(|| {
490 self.targeted_replay_scope(layer)
493 });
494 if let Some(indices) = scoped_indices {
495 for index in indices {
496 if let Some(renderer) = target.renderers.get_mut(index) {
497 f(&mut renderer.renderer);
498 }
499 }
500 } else {
501 for renderer in &mut target.renderers {
502 f(&mut renderer.renderer);
503 }
504 }
505 }
506 },
507 None => log::warn!(
508 "tried to retrieve layer {} context for non-existent layer",
509 layer
510 ),
511 }
512 }
513
514 fn active_render_scope(&self, layer: usize) -> Option<Vec<usize>> {
515 self.active_render_scopes
516 .borrow()
517 .get(layer)
518 .and_then(|scopes| scopes.last().cloned())
519 }
520
521 fn push_render_scope(&self, layer: usize, renderer_indices: Vec<usize>) {
522 let mut scopes = self.active_render_scopes.borrow_mut();
523 if scopes.len() <= layer {
524 scopes.resize_with(layer + 1, Vec::new);
525 }
526 scopes[layer].push(renderer_indices);
527 }
528
529 fn pop_render_scope(&self, layer: usize) {
530 if let Some(scopes) = self.active_render_scopes.borrow_mut().get_mut(layer) {
531 scopes.pop();
532 }
533 }
534
535 fn mark_render_surfaces_dirty(
536 &self,
537 layer: usize,
538 renderer_indices: impl IntoIterator<Item = usize>,
539 ) {
540 let mut dirty_surfaces = self.dirty_render_surfaces.borrow_mut();
541 if dirty_surfaces.len() <= layer {
542 dirty_surfaces.resize_with(layer + 1, HashSet::new);
543 }
544 dirty_surfaces[layer].extend(renderer_indices);
545 }
546
547 fn dirty_render_surface_scope(&self, layer: usize) -> Option<Vec<usize>> {
548 let mut indices: Vec<_> = self
549 .dirty_render_surfaces
550 .borrow()
551 .get(layer)?
552 .iter()
553 .copied()
554 .collect();
555 if indices.is_empty() {
556 return None;
557 }
558 indices.sort_unstable();
559 Some(indices)
560 }
561
562 fn clear_dirty_render_surfaces(&self, layer: usize, renderer_indices: &[usize]) {
563 if let Some(dirty_surfaces) = self.dirty_render_surfaces.borrow_mut().get_mut(layer) {
564 for index in renderer_indices {
565 dirty_surfaces.remove(index);
566 }
567 }
568 }
569
570 fn update_scene_lighting_cache(&self, layer: usize, lighting: &PixelSceneLighting) -> bool {
571 let mut last_scene_lighting = self.last_scene_lighting.borrow_mut();
572 if last_scene_lighting.len() <= layer {
573 last_scene_lighting.resize_with(layer + 1, || None);
574 }
575 let changed = last_scene_lighting[layer].as_ref() != Some(lighting);
576 if changed {
577 last_scene_lighting[layer] = Some(lighting.clone());
578 }
579 changed
580 }
581
582 fn targeted_replay_scope(&self, layer: usize) -> Option<Vec<usize>> {
583 self.surface_replay.borrow().targeted_replay_scope(layer)
584 }
585
586 fn set_targeted_replay_batches(
587 &self,
588 layer: usize,
589 batches: Vec<Vec<usize>>,
590 bounds_by_surface: HashMap<usize, Vec<Rect>>,
591 ) {
592 self.surface_replay
593 .borrow_mut()
594 .set_targeted_replay_batches(layer, batches, bounds_by_surface);
595 }
596
597 fn advance_targeted_replay_queue(&self, layer: usize) -> bool {
598 self.surface_replay
599 .borrow_mut()
600 .advance_targeted_replay_queue(layer)
601 }
602
603 fn flush_targeted_or_dirty(&self, layer: usize, target: &mut LayerTarget) -> bool {
604 let targeted_indices = self.targeted_replay_scope(layer);
605 let dirty_indices = self.dirty_render_surface_scope(layer);
606 let used_targeted_replay = targeted_indices.is_some();
607 #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
608 let targeted_surface_count = targeted_indices.as_ref().map(Vec::len).unwrap_or(0);
609 #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
610 let dirty_surface_count = dirty_indices.as_ref().map(Vec::len).unwrap_or(0);
611 let mut indices = targeted_indices.unwrap_or_default();
612 if let Some(dirty_indices) = dirty_indices {
613 indices.extend(dirty_indices);
614 }
615 indices.sort_unstable();
616 indices.dedup();
617
618 if !indices.is_empty() {
619 #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
620 let flush_start = std::time::Instant::now();
621 #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
622 let mut encode_us = 0u128;
623 #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
624 let mut submit_us = 0u128;
625 #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
626 let mut cleanup_us = 0u128;
627 #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
628 let mut present_us = 0u128;
629 let mut resource_stats = ResourceChurnStats::default();
630 let mut command_buffers = Vec::new();
631 let mut submitter_index = None;
632 let mut cleanup_indices = Vec::new();
633 let mut present_indices = Vec::new();
634 for index in &indices {
635 if let Some(renderer) = target.renderers.get_mut(*index) {
636 #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
637 let encode_start = std::time::Instant::now();
638 renderer.renderer.flush_deferred();
639 #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
640 {
641 encode_us += encode_start.elapsed().as_micros();
642 }
643 let mut renderer_command_buffers =
644 renderer.renderer.take_pending_command_buffers();
645 if !renderer_command_buffers.is_empty() {
646 if submitter_index.is_none() {
647 submitter_index = Some(*index);
648 }
649 command_buffers.append(&mut renderer_command_buffers);
650 }
651 if renderer.renderer.has_pending_submission_cleanup() {
652 cleanup_indices.push(*index);
653 }
654 present_indices.push(*index);
655 resource_stats.merge(renderer.renderer.take_resource_churn_stats());
656 }
657 }
658 let command_buffer_count = command_buffers.len();
659 if command_buffer_count != 0 {
660 if let Some(index) = submitter_index {
661 if let Some(renderer) = target.renderers.get(index) {
662 #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
663 let submit_start = std::time::Instant::now();
664 renderer.renderer.submit_command_buffers(command_buffers);
665 #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
666 {
667 submit_us += submit_start.elapsed().as_micros();
668 }
669 log::trace!(
670 "[pax-gpu-submit] layer={} surfaces={} command_buffers={} shared_submit=1",
671 layer,
672 indices.len(),
673 command_buffer_count
674 );
675 }
676 }
677 }
678 for index in &cleanup_indices {
679 if let Some(renderer) = target.renderers.get_mut(*index) {
680 #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
681 let cleanup_start = std::time::Instant::now();
682 renderer.renderer.complete_submitted_work();
683 #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
684 {
685 cleanup_us += cleanup_start.elapsed().as_micros();
686 }
687 }
688 }
689 for index in &present_indices {
690 if let Some(renderer) = target.renderers.get_mut(*index) {
691 #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
692 let present_start = std::time::Instant::now();
693 renderer.renderer.present_deferred_frame();
694 #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
695 {
696 present_us += present_start.elapsed().as_micros();
697 }
698 }
699 }
700 #[cfg(all(debug_assertions, not(target_arch = "wasm32")))]
701 {
702 let total_us = flush_start.elapsed().as_micros();
703 let line = format!(
704 "[pax-gpu-flush] layer={} surfaces={} targeted={} targeted_surfaces={} dirty_surfaces={} command_buffers={} cleanup_surfaces={} present_surfaces={} total_us={} encode_us={} submit_us={} cleanup_us={} present_us={} flushes={} retained_nodes={}/{} retained_batches={} retained_draws={} vector_draws={} image_draws={} batch_flushes={} geometry_rebuilds={} geometry_cache_hits={} geometry_cache_misses={} geometry_cache_evictions={} geometry_cache_bytes={} tessellated_vertices={} tessellated_indices={} cached_vertices_reused={} cached_indices_reused={} resource_creates={} resource_updates={} resource_recreates={} create_bytes={} update_bytes={} cache_hits={} cache_misses={} cache_evictions={} cache_bytes={} texture_creates={} texture_bytes={}",
705 layer,
706 indices.len(),
707 used_targeted_replay,
708 targeted_surface_count,
709 dirty_surface_count,
710 command_buffer_count,
711 cleanup_indices.len(),
712 present_indices.len(),
713 total_us,
714 encode_us,
715 submit_us,
716 cleanup_us,
717 present_us,
718 resource_stats.flushes,
719 resource_stats.retained_nodes_visible,
720 resource_stats.retained_nodes_considered,
721 resource_stats.retained_draw_batches,
722 resource_stats.retained_draws,
723 resource_stats.retained_vector_draws,
724 resource_stats.retained_image_draws,
725 resource_stats.vector_batch_flushes,
726 resource_stats.vector_geometry_rebuilds,
727 resource_stats.vector_geometry_cache_hits,
728 resource_stats.vector_geometry_cache_misses,
729 resource_stats.vector_geometry_cache_evictions,
730 resource_stats.vector_geometry_cache_bytes,
731 resource_stats.tessellated_vertices,
732 resource_stats.tessellated_indices,
733 resource_stats.cached_vertices_reused,
734 resource_stats.cached_indices_reused,
735 resource_stats.vector_resource_creates,
736 resource_stats.vector_resource_updates,
737 resource_stats.vector_resource_recreates,
738 resource_stats.vector_resource_create_bytes,
739 resource_stats.vector_resource_update_bytes,
740 resource_stats.vector_resource_cache_hits,
741 resource_stats.vector_resource_cache_misses,
742 resource_stats.vector_resource_cache_evictions,
743 resource_stats.vector_resource_cache_bytes,
744 resource_stats.texture_creates,
745 resource_stats.texture_upload_bytes,
746 );
747 log::trace!("{}", line);
748 #[cfg(not(target_arch = "wasm32"))]
749 if should_print_gpu_profile_logs()
750 && (used_targeted_replay
751 || total_us >= 8_000
752 || resource_stats.has_resource_churn())
753 {
754 println!("{}", line);
755 }
756 }
757 if resource_stats.has_resource_churn() {
758 log::trace!(
759 "[pax-gpu-resources] layer={} surfaces={} targeted={} {:?}",
760 layer,
761 indices.len(),
762 used_targeted_replay,
763 resource_stats
764 );
765 }
766 self.clear_dirty_render_surfaces(layer, &indices);
767 }
768
769 used_targeted_replay
770 }
771
772 fn clear_targeted_replay_scope(&self, layer: usize) {
773 self.surface_replay
774 .borrow_mut()
775 .clear_targeted_replay_scope(layer);
776 }
777
778 fn targeted_or_all_indices(&self, layer: usize, renderer_count: usize) -> Vec<usize> {
779 self.surface_replay
780 .borrow()
781 .targeted_or_all_indices(layer, renderer_count)
782 }
783
784 fn remember_canvas_node_coverage(&self, layer: usize, node_id: u32, coverage_bounds: Rect) {
785 self.surface_replay
786 .borrow_mut()
787 .remember_canvas_node_coverage(layer, node_id, coverage_bounds);
788 }
789
790 fn forget_canvas_node_coverage(&self, layer: usize, node_id: u32) {
791 self.surface_replay
792 .borrow_mut()
793 .forget_canvas_node_coverage(layer, node_id);
794 }
795
796 fn mark_clean_skipped_node(&self, layer: usize, node_id: u32) {
797 self.clean_skipped_canvas_nodes
798 .borrow_mut()
799 .insert((layer, node_id));
800 }
801
802 fn targeted_replay_surface_bounds(
803 &self,
804 layer: usize,
805 target: &LayerTarget,
806 ) -> Option<Vec<Rect>> {
807 self.surface_replay
808 .borrow()
809 .targeted_replay_surface_bounds(layer, |index| {
810 let renderer = target.renderers.get(index)?;
811 Some(Rect::new(
812 renderer.origin_x as f64,
813 renderer.origin_y as f64,
814 renderer.origin_x as f64 + renderer.logical_width as f64,
815 renderer.origin_y as f64 + renderer.logical_height as f64,
816 ))
817 })
818 }
819
820 fn spatial_replay_node_ids(&self, layer: usize) -> Option<Vec<u32>> {
821 let backends = self.backends.borrow();
822 let Some(RenderLayerState::Ready((target, _))) = backends.get(layer) else {
823 return None;
824 };
825 let surface_bounds = self.targeted_replay_surface_bounds(layer, target)?;
826 self.surface_replay
827 .borrow()
828 .spatial_replay_node_ids_for_surface_bounds(layer, &surface_bounds)
829 }
830
831 fn take_replay_layer_ids(&mut self) -> Vec<usize> {
832 let mut replay_layers = self.replay_layers.borrow_mut();
833 let mut replay = std::mem::take(&mut *replay_layers);
834 replay.sort_unstable();
835 replay.dedup();
836 replay
837 }
838
839 #[cfg(debug_assertions)]
840 fn update_tile_cull_stats(&self, layer: usize, update: impl FnOnce(&mut TileCullStats)) {
841 let mut stats = self.tile_cull_stats.borrow_mut();
842 if stats.len() <= layer {
843 stats.resize_with(layer + 1, TileCullStats::default);
844 }
845 update(&mut stats[layer]);
846 }
847
848 #[cfg(debug_assertions)]
849 fn emit_tile_cull_stats(&self, layer: usize) {
850 let stats = self
851 .tile_cull_stats
852 .borrow_mut()
853 .get_mut(layer)
854 .map(std::mem::take);
855 if let Some(stats) = stats {
856 if stats.origin_only_retargets > 0
857 || stats.targeted_replay_batches > 0
858 || stats.full_layer_replays > 0
859 || stats.resize_resets > 0
860 {
861 let line = format!(
862 "[pax-tile-cull] layer={} nodes={} selected_surfaces={} skipped_surfaces={} stale_removal_attempts={} origin_only_retargets={} targeted_replay_batches={} targeted_replay_surfaces={} visible_batches={} warm_batches={} visible_surfaces={} warm_surfaces={} full_layer_replays={} resize_resets={}",
863 layer,
864 stats.nodes_considered,
865 stats.selected_surfaces,
866 stats.skipped_surfaces,
867 stats.stale_surface_removal_attempts,
868 stats.origin_only_retargets,
869 stats.targeted_replay_batches,
870 stats.targeted_replay_surfaces,
871 stats.targeted_replay_visible_batches,
872 stats.targeted_replay_warm_batches,
873 stats.targeted_replay_visible_surfaces,
874 stats.targeted_replay_warm_surfaces,
875 stats.full_layer_replays,
876 stats.resize_resets
877 );
878 log::trace!("{}", line);
879 #[cfg(not(target_arch = "wasm32"))]
880 if should_print_gpu_profile_logs() {
881 println!("{}", line);
882 }
883 }
884 }
885 }
886
887 fn schedule_layer_initialization(&self) {
888 pump_layer_initialization_queue(
889 Rc::clone(&self.layer_factory),
890 Rc::clone(&self.backends),
891 Rc::clone(&self.pending_layer_initializations),
892 Rc::clone(&self.scheduled_layer_initializations),
893 Rc::clone(&self.layer_initializations_in_flight),
894 Rc::clone(&self.ready_layers),
895 );
896 }
897
898 fn refresh_layer_layouts<I>(&mut self, layer_indices: I)
899 where
900 I: IntoIterator<Item = usize>,
901 {
902 let mut needs_reinitialization = false;
903 let mut backends = self.backends.borrow_mut();
904 for layer_index in layer_indices {
905 let Some(backend) = backends.get_mut(layer_index) else {
906 continue;
907 };
908 match backend {
909 RenderLayerState::Pending => {}
910 RenderLayerState::Failed => {}
911 RenderLayerState::Ready((target, layout_provider)) => {
912 let layout = (layout_provider)();
913 let layout_matches = layer_layout_matches_target(target, &layout);
914
915 if layout.active {
916 target.activate();
917 } else {
918 target.deactivate();
919 self.clear_targeted_replay_scope(layer_index);
920 }
921
922 if !layout_matches {
923 self.clear_targeted_replay_scope(layer_index);
927 *backend = RenderLayerState::Pending;
928 self.queue_layer_initialization(layer_index);
929 needs_reinitialization = true;
930 continue;
931 }
932
933 let mut targeted_replay_entries = Vec::new();
934 let mut targeted_replay_bounds = HashMap::new();
935 let mut needs_full_layer_replay = false;
936 #[cfg(debug_assertions)]
937 {
938 let previous_surface_bounds: Vec<_> = target
939 .renderers
940 .iter()
941 .map(LayerRenderer::coverage_bounds)
942 .collect();
943 if let Some(escape) =
944 visible_surface_escape(&previous_surface_bounds, &layout.surfaces)
945 {
946 log_visible_surface_escape(layer_index, escape);
947 }
948 }
949 for (index, (surface, renderer)) in layout
950 .surfaces
951 .iter()
952 .zip(target.renderers.iter_mut())
953 .enumerate()
954 {
955 let previous_bounds = renderer.coverage_bounds();
956 let layout_change = renderer.update_layout(surface);
957 let current_bounds = renderer.coverage_bounds();
958 match layout_change {
959 LayoutChangeKind::Unchanged => {}
960 LayoutChangeKind::OriginOnly => {
961 targeted_replay_entries.push(ReplayPriorityEntry::new(
962 index,
963 surface.replay_priority,
964 previous_bounds,
965 current_bounds,
966 ));
967 targeted_replay_bounds
971 .insert(index, vec![previous_bounds, current_bounds]);
972 renderer.renderer.reset_retained_scene();
973 renderer
974 .renderer
975 .set_surface_transform(Transform2D::from_array([
976 1.0,
977 0.0,
978 0.0,
979 1.0,
980 -surface.origin_x,
981 -surface.origin_y,
982 ]));
983 self.replay_layers.borrow_mut().push(layer_index);
984 }
985 LayoutChangeKind::Resized => {
986 #[cfg(debug_assertions)]
987 self.update_tile_cull_stats(layer_index, |stats| {
988 stats.resize_resets += 1;
989 });
990 renderer.renderer.reset_retained_scene();
991 renderer
992 .renderer
993 .set_surface_transform(Transform2D::from_array([
994 1.0,
995 0.0,
996 0.0,
997 1.0,
998 -surface.origin_x,
999 -surface.origin_y,
1000 ]));
1001 renderer.renderer.resize_surface(
1002 surface.surface.surface_width as f32,
1003 surface.surface.surface_height as f32,
1004 );
1005 renderer.renderer.set_viewport(
1006 surface.surface.logical_width,
1007 surface.surface.logical_height,
1008 surface.surface.dpr,
1009 );
1010 needs_full_layer_replay = true;
1011 }
1012 }
1013 }
1014 if needs_full_layer_replay {
1015 self.clear_targeted_replay_scope(layer_index);
1016 #[cfg(debug_assertions)]
1017 self.update_tile_cull_stats(layer_index, |stats| {
1018 stats.full_layer_replays += 1;
1019 });
1020 self.mark_render_surfaces_dirty(layer_index, 0..target.renderers.len());
1021 self.replay_layers.borrow_mut().push(layer_index);
1022 } else if !targeted_replay_entries.is_empty() {
1023 let replay_batches =
1024 replay_batches_by_directional_priority(&targeted_replay_entries);
1025 #[cfg(debug_assertions)]
1026 let replay_priority_stats =
1027 replay_priority_stats(&targeted_replay_entries, &replay_batches);
1028 #[cfg(debug_assertions)]
1029 self.update_tile_cull_stats(layer_index, |stats| {
1030 stats.origin_only_retargets +=
1031 replay_batches.iter().map(Vec::len).sum::<usize>() as u64;
1032 stats.targeted_replay_batches += replay_batches.len() as u64;
1033 stats.targeted_replay_surfaces +=
1034 replay_batches.iter().map(Vec::len).sum::<usize>() as u64;
1035 stats.targeted_replay_visible_batches += replay_priority_stats.0 as u64;
1036 stats.targeted_replay_warm_batches += replay_priority_stats.1 as u64;
1037 stats.targeted_replay_visible_surfaces +=
1038 replay_priority_stats.2 as u64;
1039 stats.targeted_replay_warm_surfaces += replay_priority_stats.3 as u64;
1040 });
1041 self.set_targeted_replay_batches(
1042 layer_index,
1043 replay_batches,
1044 targeted_replay_bounds,
1045 );
1046 self.replay_layers.borrow_mut().push(layer_index);
1047 }
1048 }
1049 }
1050 }
1051 drop(backends);
1052 if needs_reinitialization {
1053 self.schedule_layer_initialization();
1054 }
1055 }
1056}
1057
1058impl RenderContext for PaxGpuRenderer {
1059 fn fill_with_opacity(
1060 &mut self,
1061 layer: usize,
1062 path: kurbo::BezPath,
1063 fill: &pax_runtime_api::Fill,
1064 opacity: f64,
1065 ) {
1066 self.with_layer_context(layer, |context| {
1067 let bounds = path.bounding_box();
1068 let path = convert_kurbo_to_lyon_path(&path);
1069 let fill = to_pax_gpu_fill(fill, bounds, context.current_transform());
1070 context.fill_path_with_opacity(path, fill, opacity as f32);
1071 });
1072 }
1073
1074 fn fill_with_material_and_opacity(
1075 &mut self,
1076 layer: usize,
1077 path: kurbo::BezPath,
1078 fill: &pax_runtime_api::Fill,
1079 material: &Material,
1080 opacity: f64,
1081 ) {
1082 self.with_layer_context(layer, |context| {
1083 let bounds = path.bounding_box();
1084 let path = convert_kurbo_to_lyon_path(&path);
1085 let fill = to_pax_gpu_fill(fill, bounds, context.current_transform());
1086 let material = to_pax_gpu_material(material);
1087 context.fill_path_with_material_and_opacity(path, fill, material, opacity as f32);
1088 });
1089 }
1090
1091 fn fill_with_material_and_opacity_and_smoothing(
1092 &mut self,
1093 layer: usize,
1094 path: kurbo::BezPath,
1095 fill: &pax_runtime_api::Fill,
1096 material: &Material,
1097 opacity: f64,
1098 smoothing: PathSmoothing,
1099 ) {
1100 self.with_layer_context(layer, |context| {
1101 let bounds = path.bounding_box();
1102 let path = convert_kurbo_to_lyon_path(&path);
1103 let fill = to_pax_gpu_fill(fill, bounds, context.current_transform());
1104 let material = to_pax_gpu_material(material);
1105 context.fill_path_with_material_and_opacity_and_smoothing(
1106 path,
1107 fill,
1108 material,
1109 opacity as f32,
1110 smoothing,
1111 );
1112 });
1113 }
1114
1115 fn stroke_with_opacity(
1116 &mut self,
1117 layer: usize,
1118 path: kurbo::BezPath,
1119 stroke: &Stroke,
1120 opacity: f64,
1121 ) {
1122 self.with_layer_context(layer, |context| {
1123 let bounds = path.bounding_box();
1124 context.stroke_path_with_opacity(
1125 convert_kurbo_to_lyon_path(&path),
1126 PixelStroke {
1127 fill: to_pax_gpu_fill(
1128 &pax_runtime_api::Fill::Solid(stroke.color.get()),
1129 bounds,
1130 context.current_transform(),
1131 ),
1132 weight: stroke.width.get().expect_pixels().to_float() as f32,
1133 cap: match stroke.cap.get() {
1134 StrokeCap::Butt => PixelStrokeCap::Butt,
1135 StrokeCap::Round => PixelStrokeCap::Round,
1136 StrokeCap::Square => PixelStrokeCap::Square,
1137 },
1138 join: match stroke.join.get() {
1139 StrokeJoin::Miter => PixelStrokeJoin::Miter,
1140 StrokeJoin::Round => PixelStrokeJoin::Round,
1141 StrokeJoin::Bevel => PixelStrokeJoin::Bevel,
1142 },
1143 },
1144 opacity as f32,
1145 );
1146 });
1147 }
1148
1149 fn stroke_with_material_and_opacity(
1150 &mut self,
1151 layer: usize,
1152 path: kurbo::BezPath,
1153 stroke: &Stroke,
1154 material: &Material,
1155 opacity: f64,
1156 ) {
1157 self.with_layer_context(layer, |context| {
1158 let bounds = path.bounding_box();
1159 context.stroke_path_with_material_and_opacity(
1160 convert_kurbo_to_lyon_path(&path),
1161 PixelStroke {
1162 fill: to_pax_gpu_fill(
1163 &pax_runtime_api::Fill::Solid(stroke.color.get()),
1164 bounds,
1165 context.current_transform(),
1166 ),
1167 weight: stroke.width.get().expect_pixels().to_float() as f32,
1168 cap: match stroke.cap.get() {
1169 StrokeCap::Butt => PixelStrokeCap::Butt,
1170 StrokeCap::Round => PixelStrokeCap::Round,
1171 StrokeCap::Square => PixelStrokeCap::Square,
1172 },
1173 join: match stroke.join.get() {
1174 StrokeJoin::Miter => PixelStrokeJoin::Miter,
1175 StrokeJoin::Round => PixelStrokeJoin::Round,
1176 StrokeJoin::Bevel => PixelStrokeJoin::Bevel,
1177 },
1178 },
1179 to_pax_gpu_material(material),
1180 opacity as f32,
1181 );
1182 });
1183 }
1184
1185 fn stroke_with_material_and_opacity_and_smoothing(
1186 &mut self,
1187 layer: usize,
1188 path: kurbo::BezPath,
1189 stroke: &Stroke,
1190 material: &Material,
1191 opacity: f64,
1192 smoothing: PathSmoothing,
1193 ) {
1194 self.with_layer_context(layer, |context| {
1195 let bounds = path.bounding_box();
1196 context.stroke_path_with_material_and_opacity_and_smoothing(
1197 convert_kurbo_to_lyon_path(&path),
1198 PixelStroke {
1199 fill: to_pax_gpu_fill(
1200 &pax_runtime_api::Fill::Solid(stroke.color.get()),
1201 bounds,
1202 context.current_transform(),
1203 ),
1204 weight: stroke.width.get().expect_pixels().to_float() as f32,
1205 cap: match stroke.cap.get() {
1206 StrokeCap::Butt => PixelStrokeCap::Butt,
1207 StrokeCap::Round => PixelStrokeCap::Round,
1208 StrokeCap::Square => PixelStrokeCap::Square,
1209 },
1210 join: match stroke.join.get() {
1211 StrokeJoin::Miter => PixelStrokeJoin::Miter,
1212 StrokeJoin::Round => PixelStrokeJoin::Round,
1213 StrokeJoin::Bevel => PixelStrokeJoin::Bevel,
1214 },
1215 },
1216 to_pax_gpu_material(material),
1217 opacity as f32,
1218 smoothing,
1219 );
1220 });
1221 }
1222
1223 fn stroke_with_draw_range_and_material_and_opacity(
1224 &mut self,
1225 layer: usize,
1226 path: kurbo::BezPath,
1227 stroke: &Stroke,
1228 material: &Material,
1229 opacity: f64,
1230 draw_start: f64,
1231 draw_end: f64,
1232 ) {
1233 self.with_layer_context(layer, |context| {
1234 let bounds = path.bounding_box();
1235 context.stroke_path_with_draw_range_and_material_and_opacity(
1236 convert_kurbo_to_lyon_path(&path),
1237 PixelStroke {
1238 fill: to_pax_gpu_fill(
1239 &pax_runtime_api::Fill::Solid(stroke.color.get()),
1240 bounds,
1241 context.current_transform(),
1242 ),
1243 weight: stroke.width.get().expect_pixels().to_float() as f32,
1244 cap: match stroke.cap.get() {
1245 StrokeCap::Butt => PixelStrokeCap::Butt,
1246 StrokeCap::Round => PixelStrokeCap::Round,
1247 StrokeCap::Square => PixelStrokeCap::Square,
1248 },
1249 join: match stroke.join.get() {
1250 StrokeJoin::Miter => PixelStrokeJoin::Miter,
1251 StrokeJoin::Round => PixelStrokeJoin::Round,
1252 StrokeJoin::Bevel => PixelStrokeJoin::Bevel,
1253 },
1254 },
1255 to_pax_gpu_material(material),
1256 opacity as f32,
1257 PixelDrawRange::enabled(draw_start as f32, draw_end as f32),
1258 );
1259 });
1260 }
1261
1262 fn stroke_with_draw_range_and_material_and_opacity_and_smoothing(
1263 &mut self,
1264 layer: usize,
1265 path: kurbo::BezPath,
1266 stroke: &Stroke,
1267 material: &Material,
1268 opacity: f64,
1269 draw_start: f64,
1270 draw_end: f64,
1271 smoothing: PathSmoothing,
1272 ) {
1273 self.with_layer_context(layer, |context| {
1274 let bounds = path.bounding_box();
1275 context.stroke_path_with_draw_range_and_material_and_opacity_and_smoothing(
1276 convert_kurbo_to_lyon_path(&path),
1277 PixelStroke {
1278 fill: to_pax_gpu_fill(
1279 &pax_runtime_api::Fill::Solid(stroke.color.get()),
1280 bounds,
1281 context.current_transform(),
1282 ),
1283 weight: stroke.width.get().expect_pixels().to_float() as f32,
1284 cap: match stroke.cap.get() {
1285 StrokeCap::Butt => PixelStrokeCap::Butt,
1286 StrokeCap::Round => PixelStrokeCap::Round,
1287 StrokeCap::Square => PixelStrokeCap::Square,
1288 },
1289 join: match stroke.join.get() {
1290 StrokeJoin::Miter => PixelStrokeJoin::Miter,
1291 StrokeJoin::Round => PixelStrokeJoin::Round,
1292 StrokeJoin::Bevel => PixelStrokeJoin::Bevel,
1293 },
1294 },
1295 to_pax_gpu_material(material),
1296 opacity as f32,
1297 PixelDrawRange::enabled(draw_start as f32, draw_end as f32),
1298 smoothing,
1299 );
1300 });
1301 }
1302
1303 fn save(&mut self, layer: usize) {
1304 self.with_layer_context(layer, |context| {
1305 context.save();
1306 });
1307 }
1308 fn restore(&mut self, layer: usize) {
1309 self.with_layer_context(layer, |context| {
1310 context.restore();
1311 });
1312 }
1313 fn clip(&mut self, layer: usize, path: kurbo::BezPath) {
1314 self.with_layer_context(layer, |context| {
1315 let path = convert_kurbo_to_lyon_path(&path);
1316 context.clip(path);
1317 });
1318 }
1319 fn clip_alpha(
1320 &mut self,
1321 layer: usize,
1322 paints: &[pax_runtime_api::AlphaMaskPaint],
1323 feather: f64,
1324 ) {
1325 self.with_layer_context(layer, |context| {
1326 let paints = paints
1327 .iter()
1328 .map(|paint| {
1329 let transform =
1330 Transform2D::from_array(paint.transform.as_coeffs().map(|v| v as f32));
1331 pax_gpu::AlphaMaskPaint {
1332 path: convert_kurbo_to_lyon_path(&paint.path),
1333 fill: to_pax_gpu_alpha_fill(
1334 &paint.fill,
1335 paint.path.bounding_box(),
1336 transform.then(&context.current_transform()),
1337 ),
1338 transform,
1339 opacity: paint.opacity.clamp(0.0, 1.0) as f32,
1340 }
1341 })
1342 .collect();
1343 context.clip_alpha(paints, feather.max(0.0) as f32);
1344 });
1345 }
1346
1347 fn transform(&mut self, layer: usize, affine: kurbo::Affine) {
1348 self.with_layer_context(layer, |context| {
1349 context.transform(Transform2D::from_array(
1350 affine.as_coeffs().map(|v| v as f32),
1351 ))
1352 });
1353 }
1354
1355 fn load_image(&mut self, identifier: &str, image: &[u8], width: usize, height: usize) {
1356 self.image_map.insert(
1357 identifier.to_string(),
1358 Image {
1359 rgba: image.into(),
1360 pixel_width: width as u32,
1361 pixel_height: height as u32,
1362 },
1363 );
1364 *self
1365 .image_versions
1366 .entry(identifier.to_string())
1367 .or_insert(0) += 1;
1368 }
1369
1370 fn draw_image(&mut self, layer: usize, image_path: &str, rect: kurbo::Rect) {
1371 self.with_layer_context(layer, |context| {
1372 if let Some(image) = self.image_map.get(image_path) {
1373 let version = *self.image_versions.get(image_path).unwrap_or(&0);
1374 context.draw_image(
1375 image_path,
1376 version,
1377 image,
1378 Box2D {
1379 min: point(rect.x0 as f32, rect.y0 as f32),
1380 max: point(rect.x1 as f32, rect.y1 as f32),
1381 },
1382 );
1383 }
1384 });
1385 }
1386
1387 fn get_image_size(&mut self, image_path: &str) -> Option<(usize, usize)> {
1388 self.image_map
1389 .get(image_path)
1390 .map(|img| (img.pixel_width as usize, img.pixel_height as usize))
1391 }
1392
1393 fn image_loaded(&self, image_path: &str) -> bool {
1394 self.image_map.contains_key(image_path)
1395 }
1396
1397 fn layers(&self) -> usize {
1398 self.backends.borrow().len()
1399 }
1400
1401 fn resize_layers_to(&mut self, layer_count: usize, _dirty_canvases: Rc<RefCell<Vec<bool>>>) {
1402 let current_len = self.backends.borrow().len();
1403 match layer_count.cmp(¤t_len) {
1404 std::cmp::Ordering::Less => {
1405 self.backends.borrow_mut().truncate(layer_count);
1406 }
1407 std::cmp::Ordering::Equal => return,
1408 std::cmp::Ordering::Greater => {
1409 for i in current_len..layer_count {
1410 self.backends.borrow_mut().push(RenderLayerState::Pending);
1411 self.enqueue_layer_initialization(i);
1412 }
1413 }
1414 }
1415 }
1416
1417 fn clear(&mut self, layer: usize) {
1418 let mut backends = self.backends.borrow_mut();
1419 match backends.get_mut(layer) {
1420 Some(RenderLayerState::Pending) => {
1421 let mut failed_context_gets = self.failed_context_gets.borrow_mut();
1422 if failed_context_gets.len() <= layer {
1423 failed_context_gets.resize(layer + 1, false);
1424 }
1425 failed_context_gets[layer] = true;
1426 }
1427 Some(RenderLayerState::Failed) => {}
1428 Some(RenderLayerState::Ready((target, _))) => {
1429 if !target.active {
1430 return;
1431 }
1432 target.prepare_for_render();
1433 let indices = self.targeted_or_all_indices(layer, target.renderers.len());
1434 for index in &indices {
1435 if let Some(renderer) = target.renderers.get_mut(*index) {
1436 renderer.renderer.clear();
1437 }
1438 }
1439 self.mark_render_surfaces_dirty(layer, indices);
1440 }
1441 None => log::warn!(
1442 "tried to clear layer {} context for non-existent layer",
1443 layer
1444 ),
1445 }
1446 }
1447
1448 fn flush(&mut self, layer: usize, dirty_canvases: Rc<RefCell<Vec<bool>>>) {
1449 #[cfg(debug_assertions)]
1450 self.emit_tile_cull_stats(layer);
1451
1452 let mut flushed_targeted_batch = false;
1453 let mut backends = self.backends.borrow_mut();
1454 match backends.get_mut(layer) {
1455 Some(RenderLayerState::Pending) => {}
1456 Some(RenderLayerState::Failed) => {}
1457 Some(RenderLayerState::Ready((target, _))) => {
1458 if !target.active {
1459 return;
1460 }
1461 if let Some(failed) = self.failed_context_gets.borrow_mut().get_mut(layer) {
1462 if *failed {
1463 if let Some(dirty_bit) = dirty_canvases.borrow_mut().get_mut(layer) {
1464 *dirty_bit = true;
1467 }
1468 *failed = false;
1469 }
1470 }
1471 flushed_targeted_batch = self.flush_targeted_or_dirty(layer, target);
1472 }
1473 None => log::warn!(
1474 "tried to flush layer {} context for non-existent layer",
1475 layer
1476 ),
1477 }
1478 drop(backends);
1479
1480 if flushed_targeted_batch && self.advance_targeted_replay_queue(layer) {
1481 self.replay_layers.borrow_mut().push(layer);
1482 }
1483 }
1484
1485 fn resize(&mut self, _width: usize, _height: usize) {
1486 let layer_count = self.backends.borrow().len();
1487 self.refresh_layer_layouts(0..layer_count);
1488 }
1489
1490 fn refresh_layers(&mut self, layers: &[usize]) {
1491 self.refresh_layer_layouts(layers.iter().copied());
1492 }
1493
1494 fn set_scene_lighting(&mut self, layer: usize, lighting: &SceneLighting) {
1495 let lighting = to_pax_gpu_scene_lighting(lighting);
1496 let lighting_changed = self.update_scene_lighting_cache(layer, &lighting);
1497 let mut backends = self.backends.borrow_mut();
1498 match backends.get_mut(layer) {
1499 Some(RenderLayerState::Pending) => {}
1500 Some(RenderLayerState::Failed) => {}
1501 Some(RenderLayerState::Ready((target, _))) => {
1502 if !target.active {
1503 return;
1504 }
1505 if lighting_changed {
1506 self.mark_render_surfaces_dirty(layer, 0..target.renderers.len());
1507 }
1508 for renderer in &mut target.renderers {
1509 renderer
1510 .renderer
1511 .set_scene_lighting(translate_scene_lighting_for_surface(
1512 &lighting,
1513 renderer.origin_x,
1514 renderer.origin_y,
1515 ));
1516 }
1517 }
1518 None => log::warn!(
1519 "tried to set lighting for layer {} context for non-existent layer",
1520 layer
1521 ),
1522 }
1523 }
1524
1525 fn clear_targeted_replay(&mut self, layer: usize) {
1526 self.clear_targeted_replay_scope(layer);
1527 }
1528
1529 fn take_ready_canvas_layers(&mut self) -> Vec<usize> {
1530 let mut ready_layers = self.ready_layers.borrow_mut();
1531 let mut ready = std::mem::take(&mut *ready_layers);
1532 drop(ready_layers);
1533 ready.sort_unstable();
1534 ready.dedup();
1535 if !ready.is_empty() {
1536 self.refresh_layer_layouts(ready.iter().copied());
1540 }
1541 ready
1542 }
1543
1544 fn take_replay_canvas_layers(&mut self) -> Vec<usize> {
1545 self.take_replay_layer_ids()
1546 }
1547
1548 fn take_replay_canvas_layer_updates(&mut self) -> Vec<ReplayCanvasLayerUpdate> {
1549 self.take_replay_layer_ids()
1550 .into_iter()
1551 .map(|layer| ReplayCanvasLayerUpdate {
1552 layer,
1553 node_ids: self.spatial_replay_node_ids(layer),
1554 })
1555 .collect()
1556 }
1557
1558 fn request_layer_screenshot(&mut self, layer: usize, request_id: u32) {
1559 self.with_layer_context(layer, |context| {
1560 context.request_screenshot_capture(request_id);
1561 });
1562 self.replay_layers.borrow_mut().push(layer);
1563 }
1564
1565 fn take_layer_screenshot(&mut self, layer: usize, request_id: u32) -> Option<ScreenshotData> {
1566 let mut screenshot = None;
1567 self.with_layer_context(layer, |context| {
1568 screenshot =
1569 context
1570 .take_screenshot_capture(request_id)
1571 .map(|capture| ScreenshotData {
1572 id: request_id,
1573 data: capture.rgba,
1574 width: capture.width as usize,
1575 height: capture.height as usize,
1576 });
1577 });
1578 screenshot
1579 }
1580
1581 fn take_layer_surface_screenshots(
1582 &mut self,
1583 layer: usize,
1584 request_id: u32,
1585 ) -> Vec<LayerSurfaceScreenshotData> {
1586 let mut screenshots = Vec::new();
1587 let mut backends = self.backends.borrow_mut();
1588 let Some(RenderLayerState::Ready((target, _))) = backends.get_mut(layer) else {
1589 return screenshots;
1590 };
1591 if !target.active {
1592 return screenshots;
1593 }
1594
1595 for renderer in target.renderers_mut() {
1596 let Some(capture) = renderer.renderer_mut().take_screenshot_capture(request_id) else {
1597 continue;
1598 };
1599 screenshots.push(LayerSurfaceScreenshotData {
1600 id: request_id,
1601 key: renderer.key.clone(),
1602 data: capture.rgba,
1603 width: capture.width as usize,
1604 height: capture.height as usize,
1605 origin_x: renderer.origin_x,
1606 origin_y: renderer.origin_y,
1607 logical_width: renderer.logical_width,
1608 logical_height: renderer.logical_height,
1609 });
1610 }
1611 screenshots
1612 }
1613
1614 fn begin_node(&mut self, layer: usize, node_id: u32, z_index: i32, light_mask: u32) -> bool {
1615 let mut backends = self.backends.borrow_mut();
1616 match backends.get_mut(layer) {
1617 Some(RenderLayerState::Pending) => {
1618 let mut failed_context_gets = self.failed_context_gets.borrow_mut();
1619 if failed_context_gets.len() <= layer {
1620 failed_context_gets.resize(layer + 1, false);
1621 }
1622 failed_context_gets[layer] = true;
1623 false
1624 }
1625 Some(RenderLayerState::Failed) => false,
1626 Some(RenderLayerState::Ready((target, _))) => {
1627 if !target.active {
1628 return false;
1629 }
1630 self.remember_canvas_node_coverage(
1631 layer,
1632 node_id,
1633 Rect::new(
1634 f64::NEG_INFINITY,
1635 f64::NEG_INFINITY,
1636 f64::INFINITY,
1637 f64::INFINITY,
1638 ),
1639 );
1640 target.prepare_for_render();
1641 let candidate_indices = self.targeted_or_all_indices(layer, target.renderers.len());
1642 let mut selected = Vec::new();
1643 for index in candidate_indices {
1644 let Some(renderer) = target.renderers.get_mut(index) else {
1645 continue;
1646 };
1647 if renderer.renderer.begin_node(node_id, z_index, light_mask) {
1648 selected.push(index);
1649 }
1650 }
1651 let began = !selected.is_empty();
1652 if began {
1653 self.mark_render_surfaces_dirty(layer, selected.iter().copied());
1654 self.push_render_scope(layer, selected);
1655 }
1656 began
1657 }
1658 None => {
1659 log::warn!(
1660 "tried to retrieve layer {} context for non-existent layer",
1661 layer
1662 );
1663 false
1664 }
1665 }
1666 }
1667
1668 fn begin_node_with_bounds(
1669 &mut self,
1670 layer: usize,
1671 node_id: u32,
1672 z_index: i32,
1673 coverage_bounds: Rect,
1674 light_mask: u32,
1675 ) -> bool {
1676 let mut backends = self.backends.borrow_mut();
1677 match backends.get_mut(layer) {
1678 Some(RenderLayerState::Pending) => {
1679 let mut failed_context_gets = self.failed_context_gets.borrow_mut();
1680 if failed_context_gets.len() <= layer {
1681 failed_context_gets.resize(layer + 1, false);
1682 }
1683 failed_context_gets[layer] = true;
1684 false
1685 }
1686 Some(RenderLayerState::Failed) => false,
1687 Some(RenderLayerState::Ready((target, _))) => {
1688 if !target.active {
1689 return false;
1690 }
1691 self.remember_canvas_node_coverage(layer, node_id, coverage_bounds);
1692 target.prepare_for_render();
1693 let renderer_count = target.renderers.len();
1694 let candidate_indices = self.targeted_or_all_indices(layer, renderer_count);
1695 let candidate_indices: HashSet<_> = candidate_indices.into_iter().collect();
1696 #[cfg(debug_assertions)]
1697 self.update_tile_cull_stats(layer, |stats| {
1698 stats.nodes_considered += 1;
1699 });
1700 let mut selected = Vec::new();
1701 let mut removed = Vec::new();
1702 #[cfg(debug_assertions)]
1703 let mut skipped_surfaces = 0u64;
1704 #[cfg(debug_assertions)]
1705 let mut stale_surface_removal_attempts = 0u64;
1706 for (index, renderer) in target.renderers.iter_mut().enumerate() {
1707 let intersects = renderer.intersects_coverage_bounds(&coverage_bounds);
1708 if !intersects {
1709 #[cfg(debug_assertions)]
1710 {
1711 skipped_surfaces += 1;
1712 stale_surface_removal_attempts += 1;
1713 }
1714 if renderer.renderer.remove_node(node_id) {
1717 removed.push(index);
1718 }
1719 continue;
1720 }
1721 if !candidate_indices.contains(&index) {
1722 #[cfg(debug_assertions)]
1723 {
1724 skipped_surfaces += 1;
1725 }
1726 continue;
1727 }
1728 if renderer.renderer.begin_node(node_id, z_index, light_mask) {
1729 selected.push(index);
1730 }
1731 }
1732 let began = !selected.is_empty();
1733 self.mark_render_surfaces_dirty(
1734 layer,
1735 selected.iter().chain(removed.iter()).copied(),
1736 );
1737 #[cfg(debug_assertions)]
1738 self.update_tile_cull_stats(layer, |stats| {
1739 let selected_count = selected.len() as u64;
1740 stats.selected_surfaces += selected_count;
1741 stats.skipped_surfaces += skipped_surfaces;
1742 stats.stale_surface_removal_attempts += stale_surface_removal_attempts;
1743 });
1744 if began {
1745 self.push_render_scope(layer, selected);
1746 } else {
1747 self.mark_clean_skipped_node(layer, node_id);
1748 }
1749 began
1750 }
1751 None => {
1752 log::warn!(
1753 "tried to retrieve layer {} context for non-existent layer",
1754 layer
1755 );
1756 false
1757 }
1758 }
1759 }
1760
1761 fn take_clean_skipped_node(&mut self, layer: usize, node_id: u32) -> bool {
1762 self.clean_skipped_canvas_nodes
1763 .borrow_mut()
1764 .remove(&(layer, node_id))
1765 }
1766
1767 fn end_node(&mut self, layer: usize, node_id: u32) -> bool {
1768 let mut ended = false;
1769 self.with_layer_context(layer, |context| {
1770 ended = context.end_node(node_id);
1771 });
1772 self.pop_render_scope(layer);
1773 ended
1774 }
1775
1776 fn remove_node(&mut self, layer: usize, node_id: u32) -> bool {
1777 let mut backends = self.backends.borrow_mut();
1778 match backends.get_mut(layer) {
1779 Some(RenderLayerState::Pending) => {
1780 let mut failed_context_gets = self.failed_context_gets.borrow_mut();
1781 if failed_context_gets.len() <= layer {
1782 failed_context_gets.resize(layer + 1, false);
1783 }
1784 failed_context_gets[layer] = true;
1785 false
1786 }
1787 Some(RenderLayerState::Failed) => false,
1788 Some(RenderLayerState::Ready((target, _))) => {
1789 if !target.active {
1790 return false;
1791 }
1792 self.forget_canvas_node_coverage(layer, node_id);
1793 target.prepare_for_render();
1794 let candidate_indices = 0..target.renderers.len();
1795 let mut removed_indices = Vec::new();
1796 for index in candidate_indices {
1797 let Some(renderer) = target.renderers.get_mut(index) else {
1798 continue;
1799 };
1800 if renderer.renderer.remove_node(node_id) {
1801 removed_indices.push(index);
1802 }
1803 }
1804 let removed = !removed_indices.is_empty();
1805 if removed {
1806 self.mark_render_surfaces_dirty(layer, removed_indices);
1807 }
1808 true
1809 }
1810 None => {
1811 log::warn!(
1812 "tried to remove node from layer {} context for non-existent layer",
1813 layer
1814 );
1815 false
1816 }
1817 }
1818 }
1819}
1820
1821fn to_pax_gpu_fill(
1822 fill: &pax_runtime_api::Fill,
1823 rect: kurbo::Rect,
1824 transform: pax_gpu::Transform2D,
1825) -> pax_gpu::Fill {
1826 let bounds = (rect.width(), rect.height());
1827 let orig = rect.origin();
1828 match fill {
1829 pax_runtime_api::Fill::Solid(color) => pax_gpu::Fill::Solid(to_pax_gpu_color(color)),
1830 pax_runtime_api::Fill::LinearGradient(gradient) => {
1831 let start_x = gradient.start.0.evaluate(bounds, Axis::X);
1832 let start_y = gradient.start.1.evaluate(bounds, Axis::Y);
1833 let end_x = gradient.end.0.evaluate(bounds, Axis::X);
1834 let end_y = gradient.end.1.evaluate(bounds, Axis::Y);
1835 let local_pos =
1836 pax_gpu::Point2D::new((orig.x + start_x) as f32, (orig.y + start_y) as f32);
1837 let local_end = pax_gpu::Point2D::new((orig.x + end_x) as f32, (orig.y + end_y) as f32);
1838 let world_pos = transform.transform_point(local_pos);
1839 let world_end = transform.transform_point(local_end);
1840 let main_axis = world_end - world_pos;
1841 pax_gpu::Fill::Gradient {
1842 stops: gradient
1843 .stops
1844 .iter()
1845 .map(|g| pax_gpu::GradientStop {
1846 color: to_pax_gpu_color(&g.color),
1847 stop: g
1848 .position
1849 .evaluate((main_axis.length() as f64, 0.0), Axis::X)
1850 as f32,
1851 })
1852 .collect(),
1853 gradient_type: pax_gpu::GradientType::Linear,
1854 pos: world_pos,
1855 main_axis,
1856 off_axis: pax_gpu::Vector2D::zero(), }
1858 }
1859 pax_runtime_api::Fill::RadialGradient(gradient) => {
1860 let start_x = gradient.start.0.evaluate(bounds, Axis::X);
1861 let start_y = gradient.start.1.evaluate(bounds, Axis::Y);
1862 let end_x = gradient.end.0.evaluate(bounds, Axis::X);
1863 let end_y = gradient.end.1.evaluate(bounds, Axis::Y);
1864 let r = gradient.radius as f32;
1865 let local_pos =
1866 pax_gpu::Point2D::new((orig.x + start_x) as f32, (orig.y + start_y) as f32);
1867 let local_main_axis =
1868 pax_gpu::Vector2D::new(r * (end_x - start_x) as f32, r * (end_y - start_y) as f32);
1869 let local_off_axis = pax_gpu::Vector2D::new(-local_main_axis.y, local_main_axis.x);
1870 let world_pos = transform.transform_point(local_pos);
1871 let world_main_axis = transform.transform_point(pax_gpu::Point2D::new(
1872 local_pos.x + local_main_axis.x,
1873 local_pos.y + local_main_axis.y,
1874 )) - world_pos;
1875 let world_off_axis = transform.transform_point(pax_gpu::Point2D::new(
1876 local_pos.x + local_off_axis.x,
1877 local_pos.y + local_off_axis.y,
1878 )) - world_pos;
1879 pax_gpu::Fill::Gradient {
1880 gradient_type: pax_gpu::GradientType::Radial,
1881 pos: world_pos,
1882 main_axis: world_main_axis,
1883 off_axis: world_off_axis,
1884 stops: gradient
1885 .stops
1886 .iter()
1887 .map(|g| pax_gpu::GradientStop {
1888 color: to_pax_gpu_color(&g.color),
1889 stop: g
1890 .position
1891 .evaluate((world_main_axis.length() as f64, 0.0), Axis::X)
1892 as f32,
1893 })
1894 .collect(),
1895 }
1896 }
1897 }
1898}
1899
1900fn to_pax_gpu_alpha_fill(
1901 fill: &pax_runtime_api::Fill,
1902 rect: Rect,
1903 transform: Transform2D,
1904) -> pax_gpu::Fill {
1905 let bounds = (rect.width(), rect.height());
1909 match fill {
1910 pax_runtime_api::Fill::RadialGradient(g) => {
1911 let center = point(
1912 (rect.x0 + g.start.0.evaluate(bounds, Axis::X)) as f32,
1913 (rect.y0 + g.start.1.evaluate(bounds, Axis::Y)) as f32,
1914 );
1915 let direction = pax_gpu::Vector2D::new(
1916 (g.end.0.evaluate(bounds, Axis::X) - g.start.0.evaluate(bounds, Axis::X)) as f32,
1917 (g.end.1.evaluate(bounds, Axis::Y) - g.start.1.evaluate(bounds, Axis::Y)) as f32,
1918 );
1919 let unit = if direction.length() > 0.0001 {
1920 direction / direction.length()
1921 } else {
1922 pax_gpu::Vector2D::new(1.0, 0.0)
1923 };
1924 let axis = unit * (g.radius as f32).max(0.0001);
1925 let off = pax_gpu::Vector2D::new(-axis.y, axis.x);
1926 let pos = transform.transform_point(center);
1927 let main_axis = transform.transform_point(center + axis) - pos;
1928 let off_axis = transform.transform_point(center + off) - pos;
1929 pax_gpu::Fill::Gradient {
1930 gradient_type: pax_gpu::GradientType::Radial,
1931 pos,
1932 main_axis,
1933 off_axis,
1934 stops: g
1935 .stops
1936 .iter()
1937 .map(|s| pax_gpu::GradientStop {
1938 color: to_pax_gpu_color(&s.color),
1939 stop: s
1940 .position
1941 .evaluate((main_axis.length() as f64, 0.0), Axis::X)
1942 as f32,
1943 })
1944 .collect(),
1945 }
1946 }
1947 pax_runtime_api::Fill::LinearGradient(g) => {
1948 let mut resolved = to_pax_gpu_fill(fill, rect, transform);
1949 if let pax_gpu::Fill::Gradient { off_axis, .. } = &mut resolved {
1950 let dx = (g.end.0.evaluate(bounds, Axis::X) - g.start.0.evaluate(bounds, Axis::X))
1951 as f32;
1952 let dy = (g.end.1.evaluate(bounds, Axis::Y) - g.start.1.evaluate(bounds, Axis::Y))
1953 as f32;
1954 *off_axis = transform.transform_vector(pax_gpu::Vector2D::new(-dy, dx));
1955 }
1956 resolved
1957 }
1958 _ => to_pax_gpu_fill(fill, rect, transform),
1959 }
1960}
1961
1962fn to_pax_gpu_material(material: &pax_runtime_api::Material) -> PixelMaterial {
1963 match material {
1964 pax_runtime_api::Material::Unlit => PixelMaterial::unlit(),
1965 pax_runtime_api::Material::Lit(params) => {
1966 let metallic = params.metallic.get().clamp(0.0, 1.0) as f32;
1967 let diffuse = params.diffuse.get().max(0.0) as f32 * (1.0 - metallic * 0.55);
1968 let specular = params.specular.get().max(0.0) as f32 + metallic * 0.45;
1969 let roughness = (params.roughness.get().clamp(0.0, 1.0) as f32
1970 * (1.0 - metallic * 0.35))
1971 .clamp(0.0, 1.0);
1972 PixelMaterial::custom(
1973 params.ambient.get().max(0.0) as f32,
1974 diffuse,
1975 specular,
1976 roughness,
1977 to_pax_gpu_color(¶ms.emissive.get()),
1978 params.emissive_intensity.get().max(0.0) as f32,
1979 )
1980 }
1981 }
1982}
1983
1984fn to_pax_gpu_scene_lighting(lighting: &SceneLighting) -> PixelSceneLighting {
1985 PixelSceneLighting {
1986 active: lighting.active,
1987 ambient_is_authored: lighting.ambient_is_authored,
1988 ambient_color: to_pax_gpu_color(&lighting.ambient.color),
1989 ambient_intensity: lighting.ambient.intensity.max(0.0) as f32,
1990 lights: lighting
1991 .lights
1992 .iter()
1993 .map(|light| PixelSceneLight {
1994 shape: match light.shape {
1995 pax_runtime_api::LightShape::Point => PixelLightShape::Point,
1996 pax_runtime_api::LightShape::Directional => PixelLightShape::Directional,
1997 },
1998 position: [
1999 light.position.x as f32,
2000 light.position.y as f32,
2001 light.position.z as f32,
2002 ],
2003 direction: [
2004 light.direction.x as f32,
2005 light.direction.y as f32,
2006 light.direction.z as f32,
2007 ],
2008 color: to_pax_gpu_color(&light.color),
2009 intensity: light.intensity.max(0.0) as f32,
2010 radius: light.radius.max(0.0) as f32,
2011 })
2012 .collect(),
2013 }
2014}
2015
2016fn translate_scene_lighting_for_surface(
2017 lighting: &PixelSceneLighting,
2018 origin_x: f32,
2019 origin_y: f32,
2020) -> PixelSceneLighting {
2021 let mut lighting = lighting.clone();
2022 for light in &mut lighting.lights {
2023 if matches!(light.shape, PixelLightShape::Point) {
2024 light.position[0] -= origin_x;
2025 light.position[1] -= origin_y;
2026 }
2027 }
2028 lighting
2029}
2030
2031pub fn to_pax_gpu_color(color: &pax_runtime_api::Color) -> pax_gpu::Color {
2033 let [r, g, b, a] = color.to_rgba_0_1();
2034 pax_gpu::Color::rgba(r as f32, g as f32, b as f32, a as f32)
2035}
2036
2037pub fn convert_kurbo_to_lyon_path(kurbo_path: &BezPath) -> Path {
2039 let mut builder = Path::builder();
2040 let mut has_open_subpath = false;
2041 for el in kurbo_path.elements() {
2042 match el {
2043 PathEl::MoveTo(p) => {
2044 if has_open_subpath {
2045 builder.end(false);
2046 }
2047 builder.begin(point(p.x as f32, p.y as f32));
2048 has_open_subpath = true;
2049 }
2050 PathEl::LineTo(p) => {
2051 if has_open_subpath {
2052 builder.line_to(point(p.x as f32, p.y as f32));
2053 }
2054 }
2055 PathEl::QuadTo(p1, p2) => {
2056 if has_open_subpath {
2057 builder.quadratic_bezier_to(
2058 point(p1.x as f32, p1.y as f32),
2059 point(p2.x as f32, p2.y as f32),
2060 );
2061 }
2062 }
2063 PathEl::CurveTo(p1, p2, p3) => {
2064 if has_open_subpath {
2065 builder.cubic_bezier_to(
2066 point(p1.x as f32, p1.y as f32),
2067 point(p2.x as f32, p2.y as f32),
2068 point(p3.x as f32, p3.y as f32),
2069 );
2070 }
2071 }
2072 PathEl::ClosePath => {
2073 if has_open_subpath {
2074 builder.end(true);
2075 has_open_subpath = false;
2076 }
2077 }
2078 }
2079 }
2080 if has_open_subpath {
2081 builder.end(false);
2082 }
2083
2084 builder.build()
2085}
2086
2087#[cfg(test)]
2088mod tests {
2089 use super::*;
2090
2091 #[test]
2092 fn alpha_radial_gradient_retains_radius_with_coincident_endpoints() {
2093 use pax_runtime_api::{Fill, RadialGradient, Size};
2094 let fill = Fill::RadialGradient(RadialGradient {
2095 start: (Size::Percent(50.into()), Size::Percent(50.into())),
2096 end: (Size::Percent(50.into()), Size::Percent(50.into())),
2097 radius: 25.0,
2098 stops: vec![],
2099 });
2100 let pax_gpu::Fill::Gradient {
2101 pos,
2102 main_axis,
2103 off_axis,
2104 ..
2105 } = to_pax_gpu_alpha_fill(
2106 &fill,
2107 Rect::new(0.0, 0.0, 100.0, 100.0),
2108 Transform2D::new(2.0, 0.0, 1.0, 3.0, 0.0, 0.0),
2109 )
2110 else {
2111 panic!("expected a radial gradient")
2112 };
2113 assert_eq!(pos, point(150.0, 150.0));
2114 assert_eq!(main_axis, pax_gpu::Vector2D::new(50.0, 0.0));
2115 assert_eq!(off_axis, pax_gpu::Vector2D::new(25.0, 75.0));
2116 }
2117
2118 #[test]
2119 fn alpha_linear_gradient_retains_sheared_basis() {
2120 use pax_runtime_api::{Fill, LinearGradient, Size};
2121 let fill = Fill::LinearGradient(LinearGradient {
2122 start: (Size::Percent(0.into()), Size::Percent(0.into())),
2123 end: (Size::Percent(100.into()), Size::Percent(0.into())),
2124 stops: vec![],
2125 });
2126 let pax_gpu::Fill::Gradient {
2127 main_axis,
2128 off_axis,
2129 ..
2130 } = to_pax_gpu_alpha_fill(
2131 &fill,
2132 Rect::new(0.0, 0.0, 100.0, 100.0),
2133 Transform2D::new(1.0, 0.0, 0.5, 1.0, 0.0, 0.0),
2134 )
2135 else {
2136 panic!("expected a linear gradient")
2137 };
2138 assert_eq!(main_axis, pax_gpu::Vector2D::new(100.0, 0.0));
2139 assert_eq!(off_axis, pax_gpu::Vector2D::new(50.0, 100.0));
2140 }
2141
2142 #[test]
2143 fn surface_intersection_includes_edges() {
2144 let bounds = Rect::new(100.0, 100.0, 200.0, 200.0);
2145
2146 assert!(surface_intersects_coverage_bounds(
2147 &bounds, 200.0, 100.0, 100.0, 100.0
2148 ));
2149 assert!(surface_intersects_coverage_bounds(
2150 &bounds, 0.0, 0.0, 100.0, 100.0
2151 ));
2152 }
2153
2154 #[test]
2155 fn surface_intersection_rejects_disjoint_surfaces() {
2156 let bounds = Rect::new(100.0, 100.0, 200.0, 200.0);
2157
2158 assert!(!surface_intersects_coverage_bounds(
2159 &bounds, 201.0, 100.0, 100.0, 100.0
2160 ));
2161 assert!(!surface_intersects_coverage_bounds(
2162 &bounds, 100.0, 201.0, 100.0, 100.0
2163 ));
2164 }
2165
2166 #[test]
2167 fn surface_intersection_keeps_non_finite_bounds_conservative() {
2168 let bounds = Rect::new(f64::NAN, 0.0, 100.0, 100.0);
2169
2170 assert!(surface_intersects_coverage_bounds(
2171 &bounds, 500.0, 500.0, 100.0, 100.0
2172 ));
2173 }
2174
2175 #[test]
2176 fn converts_multiple_kurbo_subpaths_to_lyon_path() {
2177 let mut path = BezPath::new();
2178 path.move_to((0.0, 0.0));
2179 path.line_to((10.0, 0.0));
2180 path.move_to((20.0, 0.0));
2181 path.line_to((30.0, 0.0));
2182 path.close_path();
2183
2184 let lyon_path = convert_kurbo_to_lyon_path(&path);
2185
2186 assert_eq!(lyon_path.iter().count(), 6);
2187 }
2188}