diff --git a/crates/egui_kittest/src/builder.rs b/crates/egui_kittest/src/builder.rs index 23109544b..dfaa7af36 100644 --- a/crates/egui_kittest/src/builder.rs +++ b/crates/egui_kittest/src/builder.rs @@ -18,6 +18,9 @@ pub struct HarnessBuilder { pub(crate) renderer: Box, pub(crate) wait_for_pending_images: bool, + #[cfg(any(feature = "wgpu", feature = "snapshot"))] + pub(crate) render_every_step: bool, + #[cfg(feature = "snapshot")] pub(crate) default_snapshot_options: crate::SnapshotOptions, @@ -36,6 +39,9 @@ impl Default for HarnessBuilder { max_steps: 4, step_dt: 1.0 / 4.0, wait_for_pending_images: true, + + #[cfg(any(feature = "wgpu", feature = "snapshot"))] + render_every_step: false, os: egui::os::OperatingSystem::Nix, #[cfg(feature = "snapshot")] @@ -127,6 +133,16 @@ impl HarnessBuilder { self } + /// Should every step be rendered? + /// + /// Useful when test logic requires some specific gpu logic, e.g. reading data back from the gpu. + #[cfg(any(feature = "wgpu", feature = "snapshot"))] + #[inline] + pub fn with_render_every_step(mut self, render_every_step: bool) -> Self { + self.render_every_step = render_every_step; + self + } + /// Configures the [`egui_wgpu::RendererOptions`] used by this harness. /// /// The default is [`egui_wgpu::RendererOptions::PREDICTABLE`]. diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index 3bbc61822..474f02ae8 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -99,6 +99,19 @@ pub struct Harness<'a, State = ()> { response: Option, state: State, renderer: Box, + + /// The image of a pass we already rendered, keyed by [`egui::Context::cumulative_pass_nr`]. + /// + /// A pass should be rendered at most once. A paint callback can do GPU work of its own — + /// schedule a readback, say — and running it a second time for the same pass corrupts that + /// work. + #[cfg(any(feature = "wgpu", feature = "snapshot"))] + last_render: Option<(u64, image::RgbaImage)>, + + /// Render every pass. See [`HarnessBuilder::with_render_every_step`]. + #[cfg(any(feature = "wgpu", feature = "snapshot"))] + render_every_step: bool, + max_steps: u64, step_dt: f32, wait_for_pending_images: bool, @@ -135,6 +148,9 @@ impl<'a, State> Harness<'a, State> { mut renderer, wait_for_pending_images, + #[cfg(any(feature = "wgpu", feature = "snapshot"))] + render_every_step, + #[cfg(feature = "snapshot")] default_snapshot_options, @@ -185,6 +201,13 @@ impl<'a, State> Harness<'a, State> { response, state, renderer, + + #[cfg(any(feature = "wgpu", feature = "snapshot"))] + last_render: None, + + #[cfg(any(feature = "wgpu", feature = "snapshot"))] + render_every_step, + max_steps, step_dt, wait_for_pending_images, @@ -293,6 +316,12 @@ impl<'a, State> Harness<'a, State> { self.renderer.handle_delta(&mut output.textures_delta); self.output = output; + #[cfg(any(feature = "wgpu", feature = "snapshot"))] + if self.render_every_step { + self.render() + .expect("Failed to render during `render_every_step`"); + } + self.handle_viewport_commands(); } @@ -683,18 +712,45 @@ impl<'a, State> Harness<'a, State> { /// This will add a [`RectShape`] to the output shapes, for the current frame. /// Will be overwritten on the next call to [`Self::run`]. pub fn mask(&mut self, rect: Rect) { + #[cfg(any(feature = "wgpu", feature = "snapshot"))] + { + // This changes what a render of this pass looks like. + self.last_render = None; + } + self.output.shapes.push(ClippedShape { clip_rect: Rect::EVERYTHING, shape: Shape::Rect(RectShape::filled(rect, 0.0, Color32::MAGENTA)), }); } + /// Should every step be rendered? + /// + /// Useful when test logic requires some specific gpu logic, e.g. reading data back from the gpu. + #[cfg(any(feature = "wgpu", feature = "snapshot"))] + #[inline] + pub fn set_render_every_step(&mut self, render_every_step: bool) { + self.render_every_step = render_every_step; + } + /// Render the last output to an image. /// + /// When calling this multiple times on the same frame, or when [`Self::set_render_every_step`] is + /// true, this will return the already-rendered frame. + /// /// # Errors /// Returns an error if the rendering fails. #[cfg(any(feature = "wgpu", feature = "snapshot"))] pub fn render(&mut self) -> Result { + let pass_nr = self.ctx.cumulative_pass_nr(); + + // Rendering a pass twice would run its paint callbacks twice. See `last_render`. + if let Some((rendered_pass_nr, image)) = &self.last_render + && *rendered_pass_nr == pass_nr + { + return Ok(image.clone()); + } + let mut output = self.output.clone(); if let Some(mouse_pos) = self.ctx.input(|i| i.pointer.hover_pos()) { @@ -716,7 +772,9 @@ impl<'a, State> Harness<'a, State> { }); } - self.renderer.render(&self.ctx, &output) + let image = self.renderer.render(&self.ctx, &output)?; + self.last_render = Some((pass_nr, image.clone())); + Ok(image) } /// Apply the [`egui::ViewportCommand`]s the app emitted during the last frame.