1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-03 15:20:05 -04:00

Add new render_every_step flag to egui_kittest (#8487)

Some applications / test depend on specific gpu state and may be doing
gpu-readback. This adds a `always_render` flag, which calls `render`
every frame so that the gpu dependent logic will work correctly.
It also saves the last render result so we don't have to render the same
frame twice.
This commit is contained in:
Lucas Meurer
2026-09-03 16:54:51 +02:00
committed by GitHub
parent 9cb53feab4
commit e63fa0b314
2 changed files with 75 additions and 1 deletions

View File

@@ -18,6 +18,9 @@ pub struct HarnessBuilder<State = ()> {
pub(crate) renderer: Box<dyn TestRenderer>, pub(crate) renderer: Box<dyn TestRenderer>,
pub(crate) wait_for_pending_images: bool, pub(crate) wait_for_pending_images: bool,
#[cfg(any(feature = "wgpu", feature = "snapshot"))]
pub(crate) render_every_step: bool,
#[cfg(feature = "snapshot")] #[cfg(feature = "snapshot")]
pub(crate) default_snapshot_options: crate::SnapshotOptions, pub(crate) default_snapshot_options: crate::SnapshotOptions,
@@ -36,6 +39,9 @@ impl<State> Default for HarnessBuilder<State> {
max_steps: 4, max_steps: 4,
step_dt: 1.0 / 4.0, step_dt: 1.0 / 4.0,
wait_for_pending_images: true, wait_for_pending_images: true,
#[cfg(any(feature = "wgpu", feature = "snapshot"))]
render_every_step: false,
os: egui::os::OperatingSystem::Nix, os: egui::os::OperatingSystem::Nix,
#[cfg(feature = "snapshot")] #[cfg(feature = "snapshot")]
@@ -127,6 +133,16 @@ impl<State> HarnessBuilder<State> {
self 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. /// Configures the [`egui_wgpu::RendererOptions`] used by this harness.
/// ///
/// The default is [`egui_wgpu::RendererOptions::PREDICTABLE`]. /// The default is [`egui_wgpu::RendererOptions::PREDICTABLE`].

View File

@@ -99,6 +99,19 @@ pub struct Harness<'a, State = ()> {
response: Option<egui::Response>, response: Option<egui::Response>,
state: State, state: State,
renderer: Box<dyn TestRenderer>, renderer: Box<dyn TestRenderer>,
/// 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, max_steps: u64,
step_dt: f32, step_dt: f32,
wait_for_pending_images: bool, wait_for_pending_images: bool,
@@ -135,6 +148,9 @@ impl<'a, State> Harness<'a, State> {
mut renderer, mut renderer,
wait_for_pending_images, wait_for_pending_images,
#[cfg(any(feature = "wgpu", feature = "snapshot"))]
render_every_step,
#[cfg(feature = "snapshot")] #[cfg(feature = "snapshot")]
default_snapshot_options, default_snapshot_options,
@@ -185,6 +201,13 @@ impl<'a, State> Harness<'a, State> {
response, response,
state, state,
renderer, renderer,
#[cfg(any(feature = "wgpu", feature = "snapshot"))]
last_render: None,
#[cfg(any(feature = "wgpu", feature = "snapshot"))]
render_every_step,
max_steps, max_steps,
step_dt, step_dt,
wait_for_pending_images, wait_for_pending_images,
@@ -293,6 +316,12 @@ impl<'a, State> Harness<'a, State> {
self.renderer.handle_delta(&mut output.textures_delta); self.renderer.handle_delta(&mut output.textures_delta);
self.output = output; 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(); 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. /// This will add a [`RectShape`] to the output shapes, for the current frame.
/// Will be overwritten on the next call to [`Self::run`]. /// Will be overwritten on the next call to [`Self::run`].
pub fn mask(&mut self, rect: Rect) { 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 { self.output.shapes.push(ClippedShape {
clip_rect: Rect::EVERYTHING, clip_rect: Rect::EVERYTHING,
shape: Shape::Rect(RectShape::filled(rect, 0.0, Color32::MAGENTA)), 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. /// 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 /// # Errors
/// Returns an error if the rendering fails. /// Returns an error if the rendering fails.
#[cfg(any(feature = "wgpu", feature = "snapshot"))] #[cfg(any(feature = "wgpu", feature = "snapshot"))]
pub fn render(&mut self) -> Result<image::RgbaImage, String> { pub fn render(&mut self) -> Result<image::RgbaImage, String> {
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(); let mut output = self.output.clone();
if let Some(mouse_pos) = self.ctx.input(|i| i.pointer.hover_pos()) { 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. /// Apply the [`egui::ViewportCommand`]s the app emitted during the last frame.