1use std::any::TypeId;
2
3use smallvec::SmallVec;
4use tracing::{debug, error, warn};
5
6use crate::{
7 DrawCommand, LocalToWindow, Px, PxPosition, PxRect, PxSize, SampleRegion, ScratchFit,
8 ScratchTexture, ScratchTextureDescriptor,
9 render_graph::{DrawGeometry, RenderGraph},
10 render_pass::{ClipOps, ComputePlanItem, DrawOrClip, RenderPassGraph, RenderPassPlan},
11 renderer::{
12 compute::{ErasedComputeBatchItem, pipeline::ErasedDispatchContext},
13 drawer::ErasedDrawContext,
14 },
15 time::Instant,
16};
17
18use super::*;
19
20enum SceneState {
22 Offscreen,
24 Filtered {
26 texture: ScratchTexture,
27 rect: PxRect,
28 },
29}
30
31struct RenderPassParams<'a, 'commands> {
32 msaa_view: Option<&'a wgpu::TextureView>,
33 stencil_view: &'a wgpu::TextureView,
34 clipper: &'a mut super::clip::ShapeClipper,
35 clear_target: bool,
36 damage: PxRect,
37 encoder: &'a mut wgpu::CommandEncoder,
38 write_target: &'a wgpu::TextureView,
39 commands_in_pass: &'a mut SmallVec<[DrawOrClip<'commands>; 32]>,
40 scene: Option<(&'a wgpu::TextureView, PxRect)>,
41 drawer: &'a mut Drawer,
42 graphics: &'a crate::GraphicsDevice,
43 device: &'a wgpu::Device,
44 queue: &'a wgpu::Queue,
45 config: &'a wgpu::SurfaceConfiguration,
46 target_size: PxSize,
47 clip_stack: &'a mut SmallVec<[PxRect; 16]>,
48}
49
50struct BlitParams<'a> {
51 encoder: &'a mut wgpu::CommandEncoder,
52 device: &'a wgpu::Device,
53 source: &'a wgpu::TextureView,
54 target: &'a wgpu::TextureView,
55 bind_group_layout: &'a wgpu::BindGroupLayout,
56 sampler: &'a wgpu::Sampler,
57 pipeline: &'a wgpu::RenderPipeline,
58 source_origin: PxPosition,
60}
61
62struct RenderCoreFrameState<'a> {
63 graphics: &'a crate::GraphicsDevice,
64 device: &'a wgpu::Device,
65 queue: &'a wgpu::Queue,
66 config: &'a wgpu::SurfaceConfiguration,
67 targets: &'a FrameTargets,
68 pipelines: &'a mut RenderPipelines,
69 blit: &'a BlitState,
70}
71
72impl RenderCoreFrameState<'_> {
73 fn target_size(&self) -> PxSize {
74 PxSize::new(Px(self.config.width as i32), Px(self.config.height as i32))
75 }
76}
77
78struct SubmitContext<'a> {
79 graphics: &'a crate::GraphicsDevice,
80 device: &'a wgpu::Device,
81 queue: &'a wgpu::Queue,
82 config: &'a wgpu::SurfaceConfiguration,
83}
84
85struct SubmitBatchParams<'a, 'b> {
86 drawer: &'a mut Drawer,
87 resources: SubmitContext<'a>,
88 scene: (&'a wgpu::TextureView, PxRect),
89 target_size: PxSize,
90 clip_stack: &'b [PxRect],
91 current_batch_draw_rect: &'a mut Option<PxRect>,
92 damage: PxRect,
93}
94
95impl RenderCore {
96 pub(crate) fn render(
99 &mut self,
100 graph: RenderGraph,
101 #[cfg(feature = "debug-dirty-overlay")] dirty_overlay_rects: &[PxRect],
102 ) {
103 if self.surface.is_none() {
104 let render_start = Instant::now();
105 let output_view = self.offscreen_copy().clone();
106 self.render_into_view(
107 graph,
108 &output_view,
109 #[cfg(feature = "debug-dirty-overlay")]
110 dirty_overlay_rects,
111 render_start,
112 std::time::Duration::ZERO,
113 );
114 return;
115 }
116
117 let render_start = Instant::now();
118 self.last_render_breakdown = None;
119 let acquire_start = Instant::now();
120 let (output_frame, reconfigure_after_present) = {
121 let surface = self
122 .surface
123 .as_ref()
124 .expect("surface presence checked above");
125 match surface.get_current_texture() {
126 wgpu::CurrentSurfaceTexture::Success(frame) => (frame, false),
127 wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
128 debug!("Surface suboptimal, rendering frame before reconfiguring...");
129 (frame, true)
130 }
131 wgpu::CurrentSurfaceTexture::Timeout => {
132 warn!("Surface timeout. Frame will be dropped.");
133 return;
134 }
135 wgpu::CurrentSurfaceTexture::Occluded => {
136 warn!("Surface occluded. Frame will be dropped.");
137 return;
138 }
139 wgpu::CurrentSurfaceTexture::Outdated => {
140 debug!("Surface outdated, reconfiguring...");
141 self.resize_surface();
142 return;
143 }
144 wgpu::CurrentSurfaceTexture::Lost => {
145 warn!("Surface lost, recreating surface...");
146 self.recreate_surface();
147 return;
148 }
149 wgpu::CurrentSurfaceTexture::Validation => {
150 error!("Surface validation error. Frame will be dropped.");
151 return;
152 }
153 }
154 };
155 let acquire = acquire_start.elapsed();
156 let output_view = output_frame
157 .texture
158 .create_view(&wgpu::TextureViewDescriptor::default());
159
160 self.render_into_view(
161 graph,
162 &output_view,
163 #[cfg(feature = "debug-dirty-overlay")]
164 dirty_overlay_rects,
165 render_start,
166 acquire,
167 );
168
169 let present_start = Instant::now();
170 self.queue.present(output_frame);
171 if reconfigure_after_present {
172 self.resize_surface();
173 }
174 if let Some(breakdown) = self.last_render_breakdown.as_mut() {
175 breakdown.present = present_start.elapsed();
176 }
177 }
178
179 fn render_into_view(
180 &mut self,
181 graph: RenderGraph,
182 output_view: &wgpu::TextureView,
183 #[cfg(feature = "debug-dirty-overlay")] dirty_overlay_rects: &[PxRect],
184 render_start: Instant,
185 acquire: std::time::Duration,
186 ) {
187 let current_frame = self.frame_index;
188 self.last_render_breakdown = None;
189
190 let texture_size = wgpu::Extent3d {
191 width: self.draw_config.width,
192 height: self.draw_config.height,
193 depth_or_array_layers: 1,
194 };
195 let build_start = Instant::now();
196 let target_rect = PxRect::new(
197 Px::ZERO,
198 Px::ZERO,
199 Px(texture_size.width as i32),
200 Px(texture_size.height as i32),
201 );
202 let damage = graph.damage_since(self.previous_graph.as_ref());
203 let local_graph = damage
204 .and_then(|damage| damage.intersection(&target_rect))
205 .and_then(|damage| graph.for_damage(damage));
206 let damage = damage.filter(|_| {
207 local_graph
208 .as_ref()
209 .is_none_or(RenderGraph::supports_partial_redraw)
210 });
211 let full_redraw = damage.is_none();
212 let damage = damage
213 .unwrap_or(target_rect)
214 .intersection(&target_rect)
215 .unwrap_or(PxRect::ZERO);
216 let local_graph = local_graph.as_ref().filter(|_| !full_redraw);
217 let mut passes = if damage.width > Px::ZERO && damage.height > Px::ZERO {
218 RenderPassGraph::build(
219 local_graph.unwrap_or(&graph).ordered_ops(
220 self.previous_graph
221 .as_ref()
222 .filter(|_| local_graph.is_none()),
223 ),
224 texture_size,
225 (!full_redraw).then_some(damage),
226 )
227 .into_passes()
228 } else {
229 Vec::new()
230 };
231 self.initialize_used_pipelines(&passes);
232 self.pipelines.clipper.prepare(
233 &self.device,
234 &self.queue,
235 &mut passes,
236 [
237 self.draw_config.width as f32,
238 self.draw_config.height as f32,
239 ],
240 );
241 let build_passes = build_start.elapsed();
242
243 let mut encoder = self
244 .device
245 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
246 label: Some("Render Encoder"),
247 });
248
249 let encode_start = Instant::now();
250 #[cfg(all(feature = "profiling", not(target_arch = "wasm32")))]
251 if let Some(timing) = &mut self.gpu_timing {
252 timing.begin(&self.device, &mut encoder, passes.len(), current_frame);
253 }
254
255 let mut state = RenderCoreFrameState {
256 graphics: &self.graphics,
257 device: &self.device,
258 queue: &self.queue,
259 config: &self.draw_config,
260 targets: &self.targets,
261 pipelines: &mut self.pipelines,
262 blit: &self.blit,
263 };
264 if (!full_redraw && damage.width > Px::ZERO && damage.height > Px::ZERO)
265 || (full_redraw && passes.is_empty())
266 {
267 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
268 label: Some("Damage Clear Pass"),
269 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
270 view: state
271 .targets
272 .msaa_view
273 .as_ref()
274 .unwrap_or(&state.targets.offscreen),
275 resolve_target: state
276 .targets
277 .msaa_view
278 .as_ref()
279 .map(|_| &state.targets.offscreen),
280 depth_slice: None,
281 ops: wgpu::Operations {
282 load: if full_redraw {
283 wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT)
284 } else {
285 wgpu::LoadOp::Load
286 },
287 store: wgpu::StoreOp::Store,
288 },
289 })],
290 ..Default::default()
291 });
292 if !full_redraw {
293 pass.set_pipeline(&state.blit.clear_pipeline);
294 set_scissor_rect_from_pxrect(&mut pass, damage);
295 pass.draw(0..3, 0..1);
296 }
297 }
298 state.pipelines.drawer.pipeline_registry.begin_all_frames(
299 state.device,
300 state.queue,
301 state.config,
302 state.graphics,
303 );
304
305 let mut scene = SceneState::Offscreen;
306 let mut scene_written = !full_redraw;
307 let mut clip_stack: SmallVec<[PxRect; 16]> = SmallVec::new();
308 for mut pass in passes {
309 let timing = tracing::enabled!(tracing::Level::DEBUG).then(Instant::now);
310 Self::execute_render_pass(
311 &mut state,
312 &mut encoder,
313 &mut scene,
314 &mut scene_written,
315 &mut pass,
316 &mut clip_stack,
317 damage,
318 );
319 #[cfg(all(feature = "profiling", not(target_arch = "wasm32")))]
320 if let Some(timing) = &mut self.gpu_timing {
321 timing.mark(&mut encoder, || match &pass {
322 RenderPassPlan::Draw { reads_scene, .. } => {
323 format!("Draw reads_scene={reads_scene}")
324 }
325 RenderPassPlan::Compute(_) => "Compute".to_owned(),
326 });
327 }
328 if let Some(timing) = timing {
329 debug!(
330 elapsed_us = timing.elapsed().as_micros(),
331 frame_nanos = crate::current_frame_nanos(),
332 "render_pass_cpu"
333 );
334 }
335 }
336 drop(scene);
337
338 RenderCore::blit_to_view(BlitParams {
339 encoder: &mut encoder,
340 device: state.device,
341 source: &state.targets.offscreen,
342 target: output_view,
343 bind_group_layout: &state.blit.bind_group_layout,
344 sampler: &state.blit.sampler,
345 pipeline: &state.blit.present_pipeline,
346 source_origin: PxPosition::ZERO,
347 });
348 #[cfg(feature = "debug-dirty-overlay")]
349 Self::render_dirty_overlay(
350 &mut encoder,
351 output_view,
352 &state.blit.dirty_overlay_pipeline,
353 state.target_size(),
354 dirty_overlay_rects,
355 );
356
357 state.pipelines.drawer.pipeline_registry.end_all_frames(
358 state.device,
359 state.queue,
360 state.config,
361 state.graphics,
362 );
363 let encode = encode_start.elapsed();
364
365 #[cfg(all(feature = "profiling", not(target_arch = "wasm32")))]
366 if let Some(timing) = &mut self.gpu_timing {
367 timing.finish(&mut encoder);
368 }
369
370 let submit_start = Instant::now();
371 self.graphics.upload_frame_buffers();
372 let command_buffer = encoder.finish();
373 let finish_time =
374 tracing::enabled!(target: "tessera_ui::frame_cost", tracing::Level::DEBUG)
375 .then(|| submit_start.elapsed());
376 self.queue.submit(
377 self.graphics
378 .finish_prelude()
379 .into_iter()
380 .chain([command_buffer]),
381 );
382 let submit = submit_start.elapsed();
383 if let Some(finish_time) = finish_time {
384 debug!(
385 target: "tessera_ui::frame_cost",
386 frame_index = current_frame,
387 encoder_finish_ns = finish_time.as_nanos(),
388 queue_submit_ns = submit.saturating_sub(finish_time).as_nanos(),
389 "render_submit_cpu"
390 );
391 }
392
393 self.frame_index = self.frame_index.wrapping_add(1);
394 self.previous_graph = Some(graph);
395 self.last_render_breakdown = Some(RenderTimingBreakdown {
396 acquire,
397 build_passes,
398 encode,
399 submit,
400 present: std::time::Duration::ZERO,
401 total: render_start.elapsed(),
402 });
403 }
404
405 pub fn read_offscreen_rgba(&self) -> Result<Vec<u8>, OffscreenReadbackError> {
412 self.read_texture_rgba(self.offscreen_copy().texture())
413 }
414
415 fn offscreen_copy(&self) -> &wgpu::TextureView {
416 self.targets
417 .offscreen_copy
418 .as_ref()
419 .expect("offscreen cores own a copy target")
420 }
421
422 fn read_texture_rgba(
423 &self,
424 texture: &wgpu::Texture,
425 ) -> Result<Vec<u8>, OffscreenReadbackError> {
426 let width = self.config.width;
427 let height = self.config.height;
428 if width == 0 || height == 0 {
429 return Err(OffscreenReadbackError::EmptyTarget);
430 }
431
432 let unpadded_bytes_per_row = width * 4;
433 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
434 let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
435 let buffer_size = (padded_bytes_per_row as u64) * (height as u64);
436
437 let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
438 label: Some("Offscreen Readback Buffer"),
439 size: buffer_size,
440 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
441 mapped_at_creation: false,
442 });
443
444 let mut encoder = self
445 .device
446 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
447 label: Some("Offscreen Readback Encoder"),
448 });
449 encoder.copy_texture_to_buffer(
450 wgpu::TexelCopyTextureInfo {
451 texture,
452 mip_level: 0,
453 origin: wgpu::Origin3d::ZERO,
454 aspect: wgpu::TextureAspect::All,
455 },
456 wgpu::TexelCopyBufferInfo {
457 buffer: &buffer,
458 layout: wgpu::TexelCopyBufferLayout {
459 offset: 0,
460 bytes_per_row: Some(padded_bytes_per_row),
461 rows_per_image: Some(height),
462 },
463 },
464 wgpu::Extent3d {
465 width,
466 height,
467 depth_or_array_layers: 1,
468 },
469 );
470 self.queue.submit(Some(encoder.finish()));
471
472 let slice = buffer.slice(..);
473 let (tx, rx) = std::sync::mpsc::channel();
474 slice.map_async(wgpu::MapMode::Read, move |result| {
475 let _ = tx.send(result);
476 });
477 self.device
478 .poll(wgpu::PollType::wait_indefinitely())
479 .map_err(|err| OffscreenReadbackError::Poll(err.to_string()))?;
480 match rx.recv() {
481 Ok(Ok(())) => {}
482 Ok(Err(err)) => return Err(OffscreenReadbackError::Map(err.to_string())),
483 Err(err) => return Err(OffscreenReadbackError::Map(err.to_string())),
484 }
485
486 let mut pixels = Vec::with_capacity((unpadded_bytes_per_row * height) as usize);
487 {
488 let mapped = slice
489 .get_mapped_range()
490 .map_err(|err| OffscreenReadbackError::Map(err.to_string()))?;
491 for row in 0..height {
492 let start = (row * padded_bytes_per_row) as usize;
493 let end = start + unpadded_bytes_per_row as usize;
494 pixels.extend_from_slice(&mapped[start..end]);
495 }
496 }
497 buffer.unmap();
498 Ok(pixels)
499 }
500
501 fn execute_render_pass(
502 state: &mut RenderCoreFrameState<'_>,
503 encoder: &mut wgpu::CommandEncoder,
504 scene: &mut SceneState,
505 scene_written: &mut bool,
506 pass: &mut RenderPassPlan<'_>,
507 clip_stack: &mut SmallVec<[PxRect; 16]>,
508 damage: PxRect,
509 ) {
510 match pass {
511 RenderPassPlan::Compute(items) => {
512 if let Some(filtered) = Self::filter_scene(state, encoder, std::mem::take(items)) {
513 *scene = filtered;
514 }
515 }
516 RenderPassPlan::Draw { draws, reads_scene } => {
517 if draws.is_empty() {
518 return;
519 }
520 let target_size = state.target_size();
521 let sample_copy;
522 let sampled = if *reads_scene {
523 Some(match &*scene {
524 SceneState::Filtered { texture, rect } => (&**texture, *rect),
525 SceneState::Offscreen => {
526 let rect =
527 scene_sampling_rect(draws, target_size).unwrap_or_else(|| {
528 PxRect::from_position_size(PxPosition::ZERO, target_size)
529 });
530 sample_copy = Self::copy_scene_region(state, encoder, rect);
531 (&*sample_copy, rect)
532 }
533 })
534 } else {
535 None
536 };
537 render_current_pass(RenderPassParams {
538 msaa_view: state.targets.msaa_view.as_ref(),
539 stencil_view: &state.targets.stencil,
540 clipper: &mut state.pipelines.clipper,
541 clear_target: !*scene_written,
542 damage,
543 encoder,
544 write_target: &state.targets.offscreen,
545 commands_in_pass: draws,
546 scene: sampled,
547 drawer: &mut state.pipelines.drawer,
548 graphics: state.graphics,
549 device: state.device,
550 queue: state.queue,
551 config: state.config,
552 target_size,
553 clip_stack,
554 });
555 *scene_written = true;
556 *scene = SceneState::Offscreen;
557 }
558 }
559 }
560
561 fn initialize_used_pipelines(&mut self, passes: &[RenderPassPlan<'_>]) {
562 if self.pipeline_factories.is_empty() {
563 return;
564 }
565 let mut last = None;
566 let mut initialize = |key| {
567 if last != Some(key) {
568 last = Some(key);
569 if let Some(factory) = self.pipeline_factories.remove(&key) {
570 factory(self);
571 }
572 }
573 };
574 for pass in passes {
575 match pass {
576 RenderPassPlan::Draw { draws, .. } => {
577 for op in draws {
578 if let DrawOrClip::Draw(draw) = op {
579 initialize((PipelineKind::Draw, draw.type_id));
580 }
581 }
582 }
583 RenderPassPlan::Compute(items) => {
584 for item in items {
585 initialize((PipelineKind::Compute, item.command.as_any().type_id()));
586 }
587 }
588 }
589 }
590 }
591
592 fn copy_scene_region(
594 state: &RenderCoreFrameState<'_>,
595 encoder: &mut wgpu::CommandEncoder,
596 rect: PxRect,
597 ) -> ScratchTexture {
598 let size = wgpu::Extent3d {
599 width: rect.width.positive(),
600 height: rect.height.positive(),
601 depth_or_array_layers: 1,
602 };
603 let texture = state.graphics.scratch_texture(ScratchTextureDescriptor {
604 width: size.width,
605 height: size.height,
606 format: state.config.format,
607 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
608 sample_count: 1,
609 fit: ScratchFit::Approx,
610 });
611 encoder.copy_texture_to_texture(
612 wgpu::TexelCopyTextureInfo {
613 texture: state.targets.offscreen.texture(),
614 mip_level: 0,
615 origin: wgpu::Origin3d {
616 x: rect.x.positive(),
617 y: rect.y.positive(),
618 z: 0,
619 },
620 aspect: wgpu::TextureAspect::All,
621 },
622 texture.texture().as_image_copy(),
623 size,
624 );
625 texture
626 }
627
628 fn filter_scene(
632 state: &mut RenderCoreFrameState<'_>,
633 encoder: &mut wgpu::CommandEncoder,
634 items: Vec<ComputePlanItem<'_>>,
635 ) -> Option<SceneState> {
636 let target_size = state.target_size();
637 let region = items
638 .iter()
639 .map(|item| item.sampling_rect.union(&item.target_rect))
640 .reduce(|a, b| a.union(&b))?
641 .intersection(&PxRect::from_position_size(PxPosition::ZERO, target_size))?;
642 if region.width <= Px::ZERO || region.height <= Px::ZERO {
643 return None;
644 }
645 let origin = PxPosition::new(region.x, region.y);
646 let descriptor = ScratchTextureDescriptor {
647 width: region.width.positive(),
648 height: region.height.positive(),
649 format: wgpu::TextureFormat::Rgba8Unorm,
650 usage: wgpu::TextureUsages::STORAGE_BINDING
651 | wgpu::TextureUsages::TEXTURE_BINDING
652 | wgpu::TextureUsages::RENDER_ATTACHMENT
653 | wgpu::TextureUsages::COPY_SRC
654 | wgpu::TextureUsages::COPY_DST,
655 sample_count: 1,
656 fit: ScratchFit::Approx,
657 };
658 let mut front = state.graphics.scratch_texture(descriptor);
659 let mut back = state.graphics.scratch_texture(descriptor);
660 Self::blit_to_view(BlitParams {
661 encoder,
662 device: state.device,
663 source: &state.targets.offscreen,
664 target: &front,
665 bind_group_layout: &state.blit.bind_group_layout,
666 sampler: &state.blit.sampler,
667 pipeline: &state.blit.pipeline_rgba,
668 source_origin: origin,
669 });
670
671 let translate = |rect: PxRect| PxRect {
672 x: rect.x - region.x,
673 y: rect.y - region.y,
674 ..rect
675 };
676 let items: Vec<ComputePlanItem<'_>> = items
677 .into_iter()
678 .map(|item| ComputePlanItem {
679 geometry: DrawGeometry::new(
680 item.geometry.local_size,
681 LocalToWindow::new(
682 item.geometry.local_to_window.scale(),
683 item.geometry.local_to_window.translation() - origin,
684 )
685 .expect("finite compute geometry"),
686 ),
687 target_rect: translate(item.target_rect),
688 sampling_rect: translate(item.sampling_rect),
689 ..item
690 })
691 .collect();
692 let extent = wgpu::Extent3d {
693 width: descriptor.width,
694 height: descriptor.height,
695 depth_or_array_layers: 1,
696 };
697
698 let mut index = 0;
699 while index < items.len() {
700 let type_id = items[index].command.as_any().type_id();
701 let mut batch: SmallVec<[ErasedComputeBatchItem<'_>; 8]> = SmallVec::new();
702 let mut sampling: SmallVec<[PxRect; 8]> = SmallVec::new();
703 let mut cursor = index;
704 while let Some(item) = items.get(cursor) {
707 if item.command.as_any().type_id() != type_id
708 || (!batch.is_empty()
709 && sampling
710 .iter()
711 .any(|rect| rects_overlap(*rect, item.sampling_rect)))
712 {
713 break;
714 }
715 sampling.push(item.sampling_rect);
716 batch.push(ErasedComputeBatchItem {
717 command: item.command,
718 geometry: item.geometry,
719 target_area: item.target_rect,
720 });
721 cursor += 1;
722 }
723
724 encoder.copy_texture_to_texture(
725 front.texture().as_image_copy(),
726 back.texture().as_image_copy(),
727 extent,
728 );
729 {
730 let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
731 label: Some("Compute Pass"),
732 timestamp_writes: None,
733 });
734 state.pipelines.compute_registry.dispatch_erased(
735 ErasedDispatchContext {
736 graphics: state.graphics,
737 device: state.device,
738 queue: state.queue,
739 config: state.config,
740 target_size: PxSize::new(region.width, region.height),
741 compute_pass: &mut compute_pass,
742 input_view: &front,
743 output_view: &back,
744 },
745 &batch,
746 );
747 }
748 std::mem::swap(&mut front, &mut back);
749 index = cursor;
750 }
751
752 Some(SceneState::Filtered {
753 texture: front,
754 rect: region,
755 })
756 }
757
758 fn blit_to_view(params: BlitParams<'_>) {
759 let BlitParams {
760 encoder,
761 device,
762 source,
763 target,
764 bind_group_layout,
765 sampler,
766 pipeline,
767 source_origin,
768 } = params;
769 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
770 layout: bind_group_layout,
771 entries: &[
772 wgpu::BindGroupEntry {
773 binding: 0,
774 resource: wgpu::BindingResource::TextureView(source),
775 },
776 wgpu::BindGroupEntry {
777 binding: 1,
778 resource: wgpu::BindingResource::Sampler(sampler),
779 },
780 ],
781 label: Some("Blit Bind Group"),
782 });
783
784 let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
785 label: Some("Blit Pass"),
786 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
787 view: target,
788 resolve_target: None,
789 depth_slice: None,
790 ops: wgpu::Operations {
791 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
794 store: wgpu::StoreOp::Store,
795 },
796 })],
797 depth_stencil_attachment: None,
798 ..Default::default()
799 });
800
801 rpass.set_pipeline(pipeline);
802 rpass.set_bind_group(0, &bind_group, &[]);
803 rpass.set_viewport(
806 -source_origin.x,
807 -source_origin.y,
808 source.texture().width() as f32,
809 source.texture().height() as f32,
810 0.0,
811 1.0,
812 );
813 rpass.draw(0..3, 0..1);
814 }
815
816 #[cfg(feature = "debug-dirty-overlay")]
817 fn render_dirty_overlay(
818 encoder: &mut wgpu::CommandEncoder,
819 target: &wgpu::TextureView,
820 pipeline: &wgpu::RenderPipeline,
821 target_size: PxSize,
822 dirty_rects: &[PxRect],
823 ) {
824 if dirty_rects.is_empty() {
825 return;
826 }
827
828 let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
829 label: Some("Dirty Overlay Pass"),
830 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
831 view: target,
832 resolve_target: None,
833 depth_slice: None,
834 ops: wgpu::Operations {
835 load: wgpu::LoadOp::Load,
836 store: wgpu::StoreOp::Store,
837 },
838 })],
839 depth_stencil_attachment: None,
840 ..Default::default()
841 });
842 rpass.set_pipeline(pipeline);
843 for rect in dirty_rects {
844 let Some(clamped) = clamp_rect_to_target(*rect, target_size) else {
845 continue;
846 };
847 if clamped.width <= Px::ZERO || clamped.height <= Px::ZERO {
848 continue;
849 }
850 set_scissor_rect_from_pxrect(&mut rpass, clamped);
851 rpass.draw(0..3, 0..1);
852 }
853 }
854}
855
856fn rects_overlap(a: PxRect, b: PxRect) -> bool {
857 let a_left = a.x.0;
858 let a_top = a.y.0;
859 let a_right = a_left + a.width.0;
860 let a_bottom = a_top + a.height.0;
861
862 let b_left = b.x.0;
863 let b_top = b.y.0;
864 let b_right = b_left + b.width.0;
865 let b_bottom = b_top + b.height.0;
866
867 !(a_right <= b_left || b_right <= a_left || a_bottom <= b_top || b_bottom <= a_top)
868}
869
870fn render_current_pass(params: RenderPassParams<'_, '_>) {
871 let RenderPassParams {
872 msaa_view,
873 stencil_view,
874 clipper,
875 clear_target,
876 damage,
877 encoder,
878 write_target,
879 commands_in_pass,
880 scene,
881 drawer,
882 graphics,
883 device,
884 queue,
885 config,
886 target_size,
887 clip_stack,
888 } = params;
889
890 let (view, resolve_target) = match msaa_view {
891 Some(msaa_view) => (msaa_view, Some(write_target)),
892 None => (write_target, None),
893 };
894 let load_ops = if clear_target {
895 wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT)
896 } else {
897 wgpu::LoadOp::Load
898 };
899
900 let timing = tracing::enabled!(tracing::Level::DEBUG).then(Instant::now);
901 let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
902 label: Some("Render Pass"),
903 depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
904 view: stencil_view,
905 depth_ops: None,
906 stencil_ops: Some(wgpu::Operations {
907 load: wgpu::LoadOp::Clear(0),
908 store: wgpu::StoreOp::Discard,
909 }),
910 }),
911 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
912 view,
913 depth_slice: None,
914 resolve_target,
915 ops: wgpu::Operations {
916 load: load_ops,
917 store: wgpu::StoreOp::Store,
918 },
919 })],
920 ..Default::default()
921 });
922
923 rpass.set_viewport(
924 0.0,
925 0.0,
926 target_size.width.to_f32(),
927 target_size.height.to_f32(),
928 0.0,
929 1.0,
930 );
931 clipper.restore(&mut rpass, target_size);
932
933 let scene = scene.unwrap_or((
935 write_target,
936 PxRect::from_position_size(PxPosition::ZERO, target_size),
937 ));
938 let mut buffer: SmallVec<[(&dyn DrawCommand, DrawGeometry); 16]> = SmallVec::new();
939 let mut last_command_type_id = None;
940 let mut current_batch_draw_rect: Option<PxRect> = None;
941 macro_rules! flush {
942 () => {
943 submit_buffered_commands(
944 &mut rpass,
945 SubmitBatchParams {
946 drawer: &mut *drawer,
947 resources: SubmitContext {
948 graphics,
949 device,
950 queue,
951 config,
952 },
953 scene,
954 target_size,
955 clip_stack: clip_stack.as_slice(),
956 current_batch_draw_rect: &mut current_batch_draw_rect,
957 damage,
958 },
959 &mut buffer,
960 )
961 };
962 }
963 for cmd in commands_in_pass.iter() {
964 let cmd = match cmd {
965 DrawOrClip::Clip(clip_ops) => {
966 if !buffer.is_empty() {
967 flush!();
968 last_command_type_id = None;
969 }
970 match *clip_ops {
971 ClipOps::Push(rect) => {
972 clip_stack.push(rect);
973 clipper.push(&mut rpass, target_size, None);
974 }
975 ClipOps::ShapePush { region, index } => {
976 clip_stack.push(region.bounds());
977 clipper.push(&mut rpass, target_size, Some(index));
978 }
979 ClipOps::Pop => {
980 clip_stack.pop();
981 clipper.pop(&mut rpass, target_size);
982 }
983 }
984 continue;
985 }
986 DrawOrClip::Draw(cmd) => cmd,
987 };
988
989 if cmd.draw_rect.intersection(&damage).is_none() {
990 continue;
991 }
992
993 if !can_merge_into_batch(&last_command_type_id, cmd.type_id) && !buffer.is_empty() {
994 flush!();
995 }
996 buffer.push((cmd.command, cmd.geometry));
997 last_command_type_id = Some(cmd.type_id);
998 current_batch_draw_rect = Some(merge_batch_rect(current_batch_draw_rect, cmd.draw_rect));
999 }
1000 if !buffer.is_empty() {
1001 flush!();
1002 }
1003
1004 drop(buffer);
1005 commands_in_pass.clear();
1006 let record_us = timing.map(|start| start.elapsed().as_micros());
1007 let finalize_start = timing.map(|_| Instant::now());
1008 drop(rpass);
1009 if let Some(finalize_start) = finalize_start {
1010 debug!(
1011 record_us,
1012 finalize_us = finalize_start.elapsed().as_micros(),
1013 frame_nanos = crate::current_frame_nanos(),
1014 "draw_pass_cpu"
1015 );
1016 }
1017}
1018
1019fn submit_buffered_commands(
1020 rpass: &mut wgpu::RenderPass<'_>,
1021 params: SubmitBatchParams<'_, '_>,
1022 buffer: &mut SmallVec<[(&dyn DrawCommand, DrawGeometry); 16]>,
1023) {
1024 let SubmitBatchParams {
1025 drawer,
1026 resources,
1027 scene,
1028 target_size,
1029 clip_stack,
1030 current_batch_draw_rect,
1031 damage,
1032 } = params;
1033 let (current_clip_rect, anything_to_submit) =
1034 apply_clip_to_batch_rect(clip_stack, current_batch_draw_rect, target_size);
1035 if anything_to_submit
1036 && let Some(rect) = current_batch_draw_rect.and_then(|rect| rect.intersection(&damage))
1037 {
1038 set_scissor_rect_from_pxrect(rpass, rect);
1039 drawer.submit(
1040 ErasedDrawContext {
1041 graphics: resources.graphics,
1042 device: resources.device,
1043 queue: resources.queue,
1044 config: resources.config,
1045 target_size,
1046 render_pass: rpass,
1047 scene_texture_view: scene.0,
1048 scene_rect: scene.1,
1049 clip_rect: Some(
1050 current_clip_rect
1051 .and_then(|clip| clip.intersection(&damage))
1052 .unwrap_or(damage),
1053 ),
1054 },
1055 buffer.as_slice(),
1056 );
1057 }
1058 buffer.clear();
1059 *current_batch_draw_rect = None;
1060}
1061
1062fn set_scissor_rect_from_pxrect(rpass: &mut wgpu::RenderPass<'_>, rect: PxRect) {
1063 rpass.set_scissor_rect(
1064 rect.x.positive(),
1065 rect.y.positive(),
1066 rect.width.positive(),
1067 rect.height.positive(),
1068 );
1069}
1070
1071fn clamp_rect_to_target(rect: PxRect, target_size: PxSize) -> Option<PxRect> {
1072 let target_rect = PxRect::from_position_size(PxPosition::ZERO, target_size);
1073 rect.intersection(&target_rect)
1074}
1075
1076fn apply_clip_to_batch_rect(
1082 clip_stack: &[PxRect],
1083 current_batch_draw_rect: &mut Option<PxRect>,
1084 target_size: PxSize,
1085) -> (Option<PxRect>, bool) {
1086 let clipped_rect = if clip_stack.is_empty() {
1087 None
1088 } else {
1089 clip_stack.iter().copied().try_fold(
1090 PxRect::from_position_size(PxPosition::ZERO, target_size),
1091 |current, rect| {
1092 let clamped = clamp_rect_to_target(rect, target_size)?;
1093 current.intersection(&clamped)
1094 },
1095 )
1096 };
1097
1098 let Some(current_rect) = current_batch_draw_rect.as_ref() else {
1099 return (clipped_rect, false);
1100 };
1101
1102 let Some(mut final_rect) = clamp_rect_to_target(*current_rect, target_size) else {
1103 *current_batch_draw_rect = None;
1104 return (clipped_rect, false);
1105 };
1106
1107 if !clip_stack.is_empty() && clipped_rect.is_none() {
1108 *current_batch_draw_rect = None;
1109 return (None, false);
1110 }
1111
1112 if let Some(clip_rect) = clipped_rect {
1113 if let Some(intersection) = final_rect.intersection(&clip_rect) {
1114 final_rect = intersection;
1115 } else {
1116 *current_batch_draw_rect = None;
1117 return (Some(clip_rect), false);
1118 }
1119 }
1120
1121 *current_batch_draw_rect = Some(final_rect);
1122 (clipped_rect, true)
1123}
1124
1125fn can_merge_into_batch(last_command_type_id: &Option<TypeId>, next_type_id: TypeId) -> bool {
1130 match last_command_type_id {
1131 Some(l) => *l == next_type_id,
1132 None => true,
1133 }
1134}
1135
1136fn merge_batch_rect(current: Option<PxRect>, next: PxRect) -> PxRect {
1138 current.map(|dr| dr.union(&next)).unwrap_or(next)
1139}
1140
1141fn scene_sampling_rect(commands: &[DrawOrClip<'_>], target_size: PxSize) -> Option<PxRect> {
1142 let target = PxRect::new(Px::ZERO, Px::ZERO, target_size.width, target_size.height);
1143 let mut captured = None;
1144 for command in commands {
1145 let DrawOrClip::Draw(command) = command else {
1146 continue;
1147 };
1148 let rect = match command.command.sample_region()? {
1149 SampleRegion::Global => return Some(target),
1150 SampleRegion::Absolute(rect) => rect,
1151 SampleRegion::PaddedLocal(padding) => {
1152 command.geometry.padded_window_bounds(padding).outward()
1153 }
1154 };
1155 if let Some(rect) = rect.intersection(&target) {
1156 captured = Some(merge_batch_rect(captured, rect));
1157 }
1158 }
1159 Some(captured.unwrap_or(PxRect::ZERO))
1160}
1161
1162#[cfg(test)]
1163mod tests {
1164 use super::*;
1165
1166 #[cfg(all(feature = "headless", not(target_family = "wasm")))]
1167 #[test]
1168 #[ignore = "requires a GPU adapter"]
1169 fn nearby_resizes_reuse_attachments_without_scaling_output_or_skipping_recovery() {
1170 let _runtime_session = crate::RuntimeSession::new();
1171 let _runtime_scope = _runtime_session.enter();
1172 for sample_count in [1, 4] {
1173 let mut core = pollster::block_on(RenderCore::new_offscreen(
1174 winit::dpi::PhysicalSize::new(256, 256),
1175 wgpu::TextureFormat::Rgba8UnormSrgb,
1176 sample_count,
1177 crate::renderer::DEFAULT_MAX_NON_SAMPLER_BINDINGS,
1178 ));
1179 core.resize_surface();
1180 let scene = core.targets.offscreen.texture().clone();
1181 let stencil = core.targets.stencil.texture().clone();
1182 let msaa = core.targets.msaa_texture.clone();
1183 for (width, height) in [(133, 139), (251, 253), (129, 135)] {
1184 core.resize(winit::dpi::PhysicalSize::new(width, height));
1185 core.resize_if_needed();
1186 assert_eq!(core.targets.offscreen.texture(), &scene);
1187 assert_eq!(core.targets.stencil.texture(), &stencil);
1188 assert_eq!(core.targets.msaa_texture, msaa);
1189 assert_eq!(
1190 (core.draw_config.width, core.draw_config.height),
1191 (width, height)
1192 );
1193 assert_eq!(
1194 core.offscreen_copy().texture().size(),
1195 wgpu::Extent3d {
1196 width,
1197 height,
1198 depth_or_array_layers: 1,
1199 }
1200 );
1201
1202 let extent = scene.size();
1203 let mut pixels = vec![0; (extent.width * extent.height * 4) as usize];
1204 for y in 0..extent.height {
1205 for x in 0..extent.width {
1206 let start = ((y * extent.width + x) * 4) as usize;
1207 pixels[start..start + 4].copy_from_slice(&[x as u8, y as u8, 0, 255]);
1208 }
1209 }
1210 core.queue.write_texture(
1211 scene.as_image_copy(),
1212 &pixels,
1213 wgpu::TexelCopyBufferLayout {
1214 offset: 0,
1215 bytes_per_row: Some(extent.width * 4),
1216 rows_per_image: Some(extent.height),
1217 },
1218 extent,
1219 );
1220 let mut encoder = core.device.create_command_encoder(&Default::default());
1221 RenderCore::blit_to_view(BlitParams {
1222 encoder: &mut encoder,
1223 device: &core.device,
1224 source: &core.targets.offscreen,
1225 target: core.offscreen_copy(),
1226 bind_group_layout: &core.blit.bind_group_layout,
1227 sampler: &core.blit.sampler,
1228 pipeline: &core.blit.present_pipeline,
1229 source_origin: PxPosition::ZERO,
1230 });
1231 core.queue.submit(Some(encoder.finish()));
1232 let output = core.read_offscreen_rgba().unwrap();
1233 assert_eq!(output.len(), (width * height * 4) as usize);
1234 for (index, pixel) in output.chunks_exact(4).enumerate() {
1235 let x = index as u32 % width;
1236 let y = index as u32 / width;
1237 assert_eq!(pixel, &[x as u8, y as u8, 0, 255]);
1238 }
1239 }
1240 core.render(
1241 RenderGraph::default(),
1242 #[cfg(feature = "debug-dirty-overlay")]
1243 &[],
1244 );
1245 assert!(core.previous_graph.is_some());
1246 core.resize_surface();
1247 assert_ne!(core.targets.offscreen.texture(), &scene);
1248 assert_ne!(core.targets.stencil.texture(), &stencil);
1249 assert!(core.previous_graph.is_none());
1250 core.resize(winit::dpi::PhysicalSize::new(17, 19));
1251 core.resize_if_needed();
1252 assert_eq!(core.targets.offscreen.texture().width(), 128);
1253 assert_eq!(core.targets.offscreen.texture().height(), 128);
1254 }
1255 }
1256
1257 #[cfg(all(feature = "headless", not(target_family = "wasm")))]
1258 #[test]
1259 #[ignore = "requires a GPU adapter"]
1260 fn unorm_output_encodes_srgb_and_internal_linear_copies_remain_linear() {
1261 let _runtime_session = crate::RuntimeSession::new();
1262 let _runtime_scope = _runtime_session.enter();
1263 let core = pollster::block_on(RenderCore::new_offscreen(
1264 winit::dpi::PhysicalSize::new(1, 1),
1265 wgpu::TextureFormat::Rgba8Unorm,
1266 1,
1267 crate::renderer::DEFAULT_MAX_NON_SAMPLER_BINDINGS,
1268 ));
1269 let filtered = core.graphics.scratch_texture(ScratchTextureDescriptor {
1270 width: 1,
1271 height: 1,
1272 format: wgpu::TextureFormat::Rgba8Unorm,
1273 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
1274 sample_count: 1,
1275 fit: ScratchFit::Exact,
1276 });
1277 for (target, pipeline, expected) in [
1278 (&*filtered, &core.blit.pipeline_rgba, 55_u8),
1279 (core.offscreen_copy(), &core.blit.present_pipeline, 128),
1280 ] {
1281 let mut encoder = core.device.create_command_encoder(&Default::default());
1282 {
1283 let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1284 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1285 view: &core.targets.offscreen,
1286 resolve_target: None,
1287 depth_slice: None,
1288 ops: wgpu::Operations {
1289 load: wgpu::LoadOp::Clear(wgpu::Color {
1290 r: 0.21404114,
1291 g: 0.21404114,
1292 b: 0.21404114,
1293 a: 1.0,
1294 }),
1295 store: wgpu::StoreOp::Store,
1296 },
1297 })],
1298 ..Default::default()
1299 });
1300 }
1301 RenderCore::blit_to_view(BlitParams {
1302 encoder: &mut encoder,
1303 device: &core.device,
1304 source: &core.targets.offscreen,
1305 target,
1306 bind_group_layout: &core.blit.bind_group_layout,
1307 sampler: &core.blit.sampler,
1308 pipeline,
1309 source_origin: PxPosition::ZERO,
1310 });
1311 core.queue.submit(Some(encoder.finish()));
1312 let pixels = core
1313 .read_texture_rgba(target.texture())
1314 .expect("GPU pixels");
1315 for channel in &pixels[..3] {
1316 assert!(
1317 channel.abs_diff(expected) <= 1,
1318 "{pixels:?}, expected {expected}"
1319 );
1320 }
1321 assert_eq!(pixels[3], 255);
1322 }
1323 }
1324
1325 #[derive(Clone)]
1326 struct SampleCommand(Option<SampleRegion>);
1327
1328 impl DrawCommand for SampleCommand {
1329 fn sample_region(&self) -> Option<SampleRegion> {
1330 self.0
1331 }
1332
1333 fn apply_opacity(&mut self, _opacity: f32) {}
1334 }
1335
1336 #[cfg(all(feature = "headless", not(target_family = "wasm")))]
1337 #[test]
1338 #[ignore = "requires a GPU adapter"]
1339 fn deferred_pipelines_start_on_first_use_and_draw_receives_the_whole_frame() {
1340 use std::sync::{Arc, Mutex};
1341
1342 use crate::renderer::{
1343 compute::pipeline::ComputeContext,
1344 drawer::pipeline::{DrawContext, DrawablePipeline, FrameContext},
1345 };
1346
1347 struct Probe(Arc<Mutex<Vec<&'static str>>>);
1348
1349 #[derive(Clone)]
1350 struct ComputeProbe;
1351
1352 impl ComputeCommand for ComputeProbe {
1353 fn barrier(&self) -> SampleRegion {
1354 SampleRegion::Global
1355 }
1356 }
1357
1358 impl ComputablePipeline<ComputeProbe> for Probe {
1359 fn dispatch(&mut self, _: &mut ComputeContext<'_, '_, '_, ComputeProbe>) {
1360 self.0.lock().unwrap().push("compute");
1361 }
1362 }
1363
1364 impl DrawablePipeline<SampleCommand> for Probe {
1365 fn begin_frame(&mut self, _: &FrameContext<'_>) {
1366 self.0.lock().unwrap().push("begin");
1367 }
1368
1369 fn draw(&mut self, _: &mut DrawContext<'_, '_, '_, SampleCommand>) {
1370 self.0.lock().unwrap().push("draw");
1371 }
1372
1373 fn end_frame(&mut self, _: &FrameContext<'_>) {
1374 self.0.lock().unwrap().push("end");
1375 }
1376 }
1377
1378 let _runtime_session = crate::RuntimeSession::new();
1379 let _runtime_scope = _runtime_session.enter();
1380 let mut core = pollster::block_on(RenderCore::new_offscreen(
1381 winit::dpi::PhysicalSize::new(16, 16),
1382 wgpu::TextureFormat::Rgba8UnormSrgb,
1383 1,
1384 crate::renderer::DEFAULT_MAX_NON_SAMPLER_BINDINGS,
1385 ));
1386 let events = Arc::new(Mutex::new(Vec::new()));
1387 core.register_draw_pipeline_factory({
1388 let events = events.clone();
1389 move |_| {
1390 events.lock().unwrap().push("create");
1391 Probe(events)
1392 }
1393 });
1394 core.register_compute_pipeline_factory({
1395 let events = events.clone();
1396 move |_| {
1397 events.lock().unwrap().push("create compute");
1398 Probe(events)
1399 }
1400 });
1401 let render = |core: &mut RenderCore, graph| {
1402 core.render(
1403 graph,
1404 #[cfg(feature = "debug-dirty-overlay")]
1405 &[],
1406 );
1407 };
1408 render(&mut core, RenderGraph::default());
1409 assert!(events.lock().unwrap().is_empty());
1410 let mut fragment = crate::render_graph::RenderFragment::default();
1411 fragment.push_draw_command(SampleCommand(None));
1412 let mut graph = crate::render_graph::RenderGraphBuilder::new();
1413 graph.append_fragment(
1414 &fragment,
1415 DrawGeometry::from_position_size(PxPosition::ZERO, PxSize::new(Px(16), Px(16))),
1416 1.0,
1417 );
1418 let graph = graph.finish();
1419 render(&mut core, graph.clone());
1420 render(&mut core, graph);
1421 assert_eq!(
1422 *events.lock().unwrap(),
1423 ["create", "begin", "draw", "end", "begin", "draw", "end"]
1424 );
1425 events.lock().unwrap().clear();
1426 let mut fragment = crate::render_graph::RenderFragment::default();
1427 fragment.push_compute_command(ComputeProbe);
1428 let mut graph = crate::render_graph::RenderGraphBuilder::new();
1429 graph.append_fragment(
1430 &fragment,
1431 DrawGeometry::from_position_size(PxPosition::ZERO, PxSize::new(Px(16), Px(16))),
1432 1.0,
1433 );
1434 render(&mut core, graph.finish());
1435 assert_eq!(
1436 *events.lock().unwrap(),
1437 ["create compute", "begin", "compute", "end"]
1438 );
1439 }
1440
1441 fn sampled_draw(command: &SampleCommand, position: PxPosition) -> DrawOrClip<'_> {
1442 let size = PxSize::new(Px(50), Px(50));
1443 DrawOrClip::Draw(crate::render_pass::DrawCommandWithMetadata {
1444 command,
1445 type_id: std::any::TypeId::of::<SampleCommand>(),
1446 geometry: DrawGeometry::from_position_size(position, size),
1447 draw_rect: PxRect::from_position_size(position, size),
1448 })
1449 }
1450
1451 #[test]
1452 fn scene_capture_unions_sampling_regions_without_applying_draw_clips() {
1453 let _runtime_session = crate::RuntimeSession::new();
1454 let _runtime_scope = _runtime_session.enter();
1455 let local = SampleCommand(Some(SampleRegion::uniform_padding_local(Px(10))));
1456 let absolute = SampleCommand(Some(SampleRegion::Absolute(PxRect::new(
1457 Px(300),
1458 Px(100),
1459 Px(20),
1460 Px(20),
1461 ))));
1462 let commands = [
1463 DrawOrClip::Clip(ClipOps::Push(PxRect::new(Px(100), Px(100), Px(1), Px(1)))),
1464 sampled_draw(&local, PxPosition::new(Px(100), Px(100))),
1465 sampled_draw(&absolute, PxPosition::ZERO),
1466 ];
1467 assert_eq!(
1468 scene_sampling_rect(&commands, PxSize::new(Px(400), Px(400))),
1469 Some(PxRect::new(Px(90), Px(90), Px(230), Px(70)))
1470 );
1471 }
1472
1473 #[test]
1474 fn scene_capture_rounds_fractional_sampling_edges_outward() {
1475 let _runtime_session = crate::RuntimeSession::new();
1476 let _runtime_scope = _runtime_session.enter();
1477 let local = SampleCommand(Some(SampleRegion::uniform_padding_local(Px(10))));
1478 let commands = [sampled_draw(
1479 &local,
1480 PxPosition::from_f32_xy(100.25, 200.75),
1481 )];
1482 assert_eq!(
1483 scene_sampling_rect(&commands, PxSize::new(Px(400), Px(400))),
1484 Some(PxRect::new(Px(90), Px(190), Px(71), Px(71)))
1485 );
1486 }
1487
1488 #[test]
1489 fn scene_capture_clamps_regions_at_screen_edges() {
1490 let _runtime_session = crate::RuntimeSession::new();
1491 let _runtime_scope = _runtime_session.enter();
1492 let local = SampleCommand(Some(SampleRegion::uniform_padding_local(Px(10))));
1493 let commands = [sampled_draw(&local, PxPosition::from_f32_xy(-5.25, -6.75))];
1494 assert_eq!(
1495 scene_sampling_rect(&commands, PxSize::new(Px(400), Px(400))),
1496 Some(PxRect::new(Px::ZERO, Px::ZERO, Px(55), Px(54)))
1497 );
1498 }
1499
1500 #[test]
1501 fn scene_capture_preserves_global_and_undeclared_scene_reads() {
1502 let _runtime_session = crate::RuntimeSession::new();
1503 let _runtime_scope = _runtime_session.enter();
1504 let target_size = PxSize::new(Px(400), Px(400));
1505 assert_eq!(
1506 scene_sampling_rect(
1507 &[sampled_draw(
1508 &SampleCommand(Some(SampleRegion::Global)),
1509 PxPosition::ZERO
1510 )],
1511 target_size
1512 ),
1513 Some(PxRect::new(Px::ZERO, Px::ZERO, Px(400), Px(400)))
1514 );
1515 assert_eq!(
1516 scene_sampling_rect(
1517 &[sampled_draw(&SampleCommand(None), PxPosition::ZERO)],
1518 target_size
1519 ),
1520 None
1521 );
1522 }
1523
1524 #[test]
1525 fn apply_clip_to_batch_rect_intersects_full_clip_stack() {
1526 let _runtime_session = crate::RuntimeSession::new();
1527 let _runtime_scope = _runtime_session.enter();
1528 let target_size = PxSize::new(Px(400), Px(400));
1529 let clip_stack = [
1530 PxRect::new(Px(0), Px(0), Px(200), Px(200)),
1531 PxRect::new(Px(50), Px(50), Px(200), Px(200)),
1532 ];
1533 let mut batch_rect = Some(PxRect::new(Px(25), Px(25), Px(200), Px(200)));
1534
1535 let (clip_rect, should_submit) =
1536 apply_clip_to_batch_rect(&clip_stack, &mut batch_rect, target_size);
1537
1538 assert!(should_submit);
1539 assert_eq!(
1540 clip_rect,
1541 Some(PxRect::new(Px(50), Px(50), Px(150), Px(150)))
1542 );
1543 assert_eq!(
1544 batch_rect,
1545 Some(PxRect::new(Px(50), Px(50), Px(150), Px(150)))
1546 );
1547 }
1548
1549 #[test]
1550 fn apply_clip_to_batch_rect_drops_batch_when_nested_clips_do_not_overlap() {
1551 let _runtime_session = crate::RuntimeSession::new();
1552 let _runtime_scope = _runtime_session.enter();
1553 let target_size = PxSize::new(Px(400), Px(400));
1554 let clip_stack = [
1555 PxRect::new(Px(0), Px(0), Px(100), Px(100)),
1556 PxRect::new(Px(200), Px(200), Px(100), Px(100)),
1557 ];
1558 let mut batch_rect = Some(PxRect::new(Px(0), Px(0), Px(300), Px(300)));
1559
1560 let (clip_rect, should_submit) =
1561 apply_clip_to_batch_rect(&clip_stack, &mut batch_rect, target_size);
1562
1563 assert!(!should_submit);
1564 assert_eq!(clip_rect, None);
1565 assert_eq!(batch_rect, None);
1566 }
1567}