1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-02 14:50:03 -04:00

Prevent accidentally dropping TexturesDelta (#8356)

We had a ton of issues around `TexturesDelta` that weren't properly
applied because we early-out of some function:
* https://github.com/emilk/egui/pull/8313
* https://github.com/emilk/egui/pull/8250
* https://github.com/emilk/egui/pull/8279

This PR changes texture updates, so that we always store them after
taking them out of `FullOutput` and keep the delta around until it's
actually applied (by passing &mut refs and draining instead of
iterating). So even if we add a new early return somewhere, that can't
break texture updates.

It also optimizes `TexturesDelta::append` by dropping any previous
deltas if there's a new `whole` delta or a `free`.

It also adds a debug assert that any `TexturesDelta` is empty when
dropped, as an additional safeguard in case the bug sneaks back in.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lucas Meurer
2026-07-29 18:16:48 +02:00
committed by GitHub
parent d99b665ec0
commit 6268c84d8a
21 changed files with 291 additions and 130 deletions

View File

@@ -147,7 +147,7 @@ impl<'a, State> Harness<'a, State> {
response = app.run(ui, &mut state, false);
});
renderer.handle_delta(&output.textures_delta);
renderer.handle_delta(&mut output.textures_delta);
let mut harness = Self {
app,
@@ -269,7 +269,7 @@ impl<'a, State> Harness<'a, State> {
.take()
.expect("AccessKit was disabled"),
);
self.renderer.handle_delta(&output.textures_delta);
self.renderer.handle_delta(&mut output.textures_delta);
self.output = output;
self.handle_viewport_commands();

View File

@@ -1,4 +1,5 @@
use egui::TexturesDelta;
use std::mem;
pub trait TestRenderer {
/// We use this to pass the glow / wgpu render state to [`eframe::Frame`].
@@ -6,7 +7,7 @@ pub trait TestRenderer {
fn setup_eframe(&self, _cc: &mut eframe::CreationContext<'_>, _frame: &mut eframe::Frame) {}
/// Handle a [`TexturesDelta`] by updating the renderer's textures.
fn handle_delta(&mut self, delta: &TexturesDelta);
fn handle_delta(&mut self, delta: &mut TexturesDelta);
/// Render the [`crate::Harness`] and return the resulting image.
///
@@ -25,7 +26,7 @@ pub trait TestRenderer {
/// By default, this will create a wgpu renderer if the wgpu feature is enabled.
pub enum LazyRenderer {
Uninitialized {
texture_ops: Vec<egui::TexturesDelta>,
textures_delta: TexturesDelta,
builder: Option<Box<dyn FnOnce() -> Box<dyn TestRenderer>>>,
},
Initialized {
@@ -39,7 +40,7 @@ impl Default for LazyRenderer {
return Self::new(crate::wgpu::WgpuTestRenderer::new);
#[cfg(not(feature = "wgpu"))]
return Self::Uninitialized {
texture_ops: Vec::new(),
textures_delta: Vec::new(),
builder: None,
};
}
@@ -48,16 +49,19 @@ impl Default for LazyRenderer {
impl LazyRenderer {
pub fn new<T: TestRenderer + 'static>(create_renderer: impl FnOnce() -> T + 'static) -> Self {
Self::Uninitialized {
texture_ops: Vec::new(),
textures_delta: Default::default(),
builder: Some(Box::new(move || Box::new(create_renderer()))),
}
}
}
impl TestRenderer for LazyRenderer {
fn handle_delta(&mut self, delta: &TexturesDelta) {
fn handle_delta(&mut self, delta: &mut TexturesDelta) {
match self {
Self::Uninitialized { texture_ops, .. } => texture_ops.push(delta.clone()),
Self::Uninitialized {
textures_delta: texture_ops,
..
} => texture_ops.append(mem::take(delta)),
Self::Initialized { renderer } => renderer.handle_delta(delta),
}
}
@@ -70,15 +74,15 @@ impl TestRenderer for LazyRenderer {
) -> Result<image::RgbaImage, String> {
match self {
Self::Uninitialized {
texture_ops,
textures_delta,
builder: build,
} => {
let mut renderer = build.take().ok_or({
"No default renderer available. \
Enable the wgpu feature or set one via HarnessBuilder::renderer"
})?();
for delta in texture_ops.drain(..) {
renderer.handle_delta(&delta);
if !textures_delta.is_empty() {
renderer.handle_delta(textures_delta);
}
let image = renderer.render(ctx, output)?;
*self = Self::Initialized { renderer };
@@ -88,3 +92,14 @@ impl TestRenderer for LazyRenderer {
}
}
}
impl Drop for LazyRenderer {
fn drop(&mut self) {
match self {
Self::Uninitialized { textures_delta, .. } => {
textures_delta.clear(); // Don't panic when dropping unapplied deltas
}
Self::Initialized { .. } => {}
}
}
}

View File

@@ -137,15 +137,23 @@ impl crate::TestRenderer for WgpuTestRenderer {
frame.wgpu_render_state = Some(self.render_state.clone());
}
fn handle_delta(&mut self, delta: &TexturesDelta) {
fn handle_delta(&mut self, delta: &mut TexturesDelta) {
let mut renderer = self.render_state.renderer.write();
for (id, image) in &delta.set {
renderer.update_texture(
&self.render_state.device,
&self.render_state.queue,
*id,
image,
);
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
for (id, images) in delta.set.drain() {
for image in images {
renderer.update_texture(
&self.render_state.device,
&self.render_state.queue,
id,
&image,
);
}
}
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
for id in delta.free.drain() {
renderer.free_texture(&id);
}
}