Skip to main content

tessera_ui/renderer/core/
frame.rs

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
20/// Scene pixels available to draws that sample the scene.
21enum SceneState {
22    /// Draws sample a copy of the offscreen target.
23    Offscreen,
24    /// A compute pass filtered the scene inside `rect`.
25    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 position of the target's first texel.
59    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    /// Renders the frame and presents it to the window surface, or keeps it in
97    /// the offscreen copy when the core has no surface.
98    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    /// Reads the last offscreen-rendered frame back as tightly packed RGBA8
406    /// pixels in row-major order with a top-left origin.
407    ///
408    /// The returned buffer has `width * height * 4` bytes and no row padding.
409    /// RGBA8 presentation bytes are sRGB-encoded even when the output texture
410    /// uses a non-sRGB format, matching PNG and most image consumers.
411    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    /// Copies the scene pixels that a sampling pass reads.
593    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    /// Runs compute items over the scene region they read and write. Each batch
629    /// starts from a copy of the previous result, so pixels a batch does not
630    /// write keep their prior values.
631    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            // Items in one batch read the same input, so their reads must not
705            // depend on each other's writes.
706            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                    // Replace the destination. Blending against an undefined
792                    // or stale target turns opaque scene colors black.
793                    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        // Mapping the whole source onto a shifted viewport copies texels one to
804        // one starting at `source_origin`.
805        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    // Draws that do not sample the scene never read this placeholder.
934    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
1076/// Apply clip_stack to current_batch_draw_rect. Returns false if intersection
1077/// yields nothing (meaning there is nothing to submit), true otherwise.
1078///
1079/// Also returns the current clipping rectangle (if any) for potential use by
1080/// the caller.
1081fn 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
1125/// Determine whether `next_type_id` (with potential clipping) can be merged
1126/// into the current batch. Equivalent to the negation of the original flush
1127/// condition: merge allowed when last_command_type_id == Some(next_type_id) or
1128/// last_command_type_id is None.
1129fn 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
1136/// Merge the existing optional batch rect with a new command rect.
1137fn 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}