1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-29 12:50:04 -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

@@ -31,16 +31,17 @@ use egui::{
};
#[cfg(feature = "accesskit")]
use egui_winit::accesskit_winit;
use crate::{
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
native::{epi_integration::EpiIntegration, winit_integration::is_invisible_or_minimized},
};
use log::warn;
use super::{
epi_integration, event_loop_context,
winit_integration::{EventResult, UserEvent, WinitApp, create_egui_context},
};
use crate::epaint::textures::TexturesDelta;
use crate::{
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
native::{epi_integration::EpiIntegration, winit_integration::is_invisible_or_minimized},
};
// ----------------------------------------------------------------------------
// Types:
@@ -73,6 +74,16 @@ struct GlowWinitRunning<'app> {
// NOTE: one painter shared by all viewports.
painter: Rc<RefCell<egui_glow::Painter>>,
/// Any not yet applied deltas for this app.
pending_deltas: TexturesDelta,
}
impl Drop for GlowWinitRunning<'_> {
fn drop(&mut self) {
// Avoid debug panic when dropping unapplied deltas on teardown
self.pending_deltas.clear();
}
}
/// This struct will contain both persistent and temporary glutin state.
@@ -114,6 +125,9 @@ struct Viewport {
info: ViewportInfo,
actions_requested: Vec<egui_winit::ActionRequested>,
/// Any not yet applied deltas for this viewport.
pending_delta: TexturesDelta,
/// The user-callback that shows the ui.
/// None for immediate viewports.
viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
@@ -125,6 +139,13 @@ struct Viewport {
egui_winit: Option<egui_winit::State>,
}
impl Drop for Viewport {
fn drop(&mut self) {
// Avoid debug panic when dropping unapplied deltas on teardown
self.pending_delta.clear();
}
}
// ----------------------------------------------------------------------------
impl<'app> GlowWinitApp<'app> {
@@ -353,6 +374,7 @@ impl<'app> GlowWinitApp<'app> {
app,
glutin,
painter,
pending_deltas: Default::default(),
}))
}
}
@@ -653,6 +675,7 @@ impl GlowWinitRunning<'_> {
app,
glutin,
painter,
pending_deltas,
..
} = self;
@@ -666,6 +689,7 @@ impl GlowWinitRunning<'_> {
pixels_per_point,
viewport_output,
} = full_output;
pending_deltas.append(textures_delta);
glutin.remove_viewports_not_in(&viewport_output);
@@ -687,30 +711,28 @@ impl GlowWinitRunning<'_> {
egui_winit.handle_platform_output_with_event_loop(&window, event_loop, platform_output);
// Upload textures even when not visible: the atlas dirty region is already
// consumed, so dropping the delta would desync the font texture.
let has_texture_updates = !textures_delta.set.is_empty() || !textures_delta.free.is_empty();
if is_visible || has_texture_updates {
// We may need to switch contexts again, because of immediate viewports:
frame_timer.pause();
change_gl_context(current_gl_context, not_current_gl_context, gl_surface);
frame_timer.resume();
}
for (id, image_delta) in &textures_delta.set {
painter.set_texture(*id, image_delta);
}
if is_visible {
let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point);
{
// We may need to switch contexts again, because of immediate viewports:
frame_timer.pause();
change_gl_context(current_gl_context, not_current_gl_context, gl_surface);
frame_timer.resume();
}
let screen_size_in_pixels: [u32; 2] = window.inner_size().into();
if !clear_before_update {
painter.clear(screen_size_in_pixels, clear_color);
}
painter.paint_primitives(screen_size_in_pixels, pixels_per_point, &clipped_primitives);
painter.paint_and_update_textures(
screen_size_in_pixels,
pixels_per_point,
&clipped_primitives,
pending_deltas,
);
{
for action in viewport.actions_requested.drain(..) {
@@ -772,11 +794,6 @@ impl GlowWinitRunning<'_> {
}
}
// Free textures *after* painting, since they may still be used in the frame we just drew.
for id in &textures_delta.free {
painter.free_texture(*id);
}
glutin.handle_viewport_output(event_loop, &integration.egui_ctx, &viewport_output);
integration.report_frame_time(frame_timer.total_time_sec()); // don't count auto-save time as part of regular frame time
@@ -1120,6 +1137,7 @@ impl GlutinWindowContext {
deferred_commands: vec![],
info: viewport_info,
actions_requested: Default::default(),
pending_delta: Default::default(),
viewport_ui_cb: None,
gl_surface: None,
window: window.map(Arc::new),
@@ -1436,6 +1454,7 @@ fn initialize_or_update_viewport(
deferred_commands: vec![],
info: Default::default(),
actions_requested: Default::default(),
pending_delta: Default::default(),
viewport_ui_cb,
window: None,
egui_winit: None,
@@ -1584,8 +1603,10 @@ fn render_immediate_viewport(
} = &mut *glutin;
let Some(viewport) = viewports.get_mut(&viewport_id) else {
warn!("Viewport disappeared unexpectedly!");
return;
};
viewport.pending_delta.append(textures_delta);
viewport.info.events.clear(); // they should have been processed
@@ -1621,7 +1642,7 @@ fn render_immediate_viewport(
screen_size_in_pixels,
pixels_per_point,
&clipped_primitives,
&textures_delta,
&mut viewport.pending_delta,
);
{

View File

@@ -17,12 +17,13 @@ use winit::{
use ahash::HashMap;
use egui::{
DeferredViewportUiCallback, FullOutput, ImmediateViewport, OrderedViewportIdMap,
DeferredViewportUiCallback, FullOutput, ImmediateViewport, OrderedViewportIdMap, TexturesDelta,
ViewportBuilder, ViewportClass, ViewportId, ViewportIdPair, ViewportIdSet, ViewportInfo,
ViewportOutput,
};
#[cfg(feature = "accesskit")]
use egui_winit::accesskit_winit;
use log::warn;
use winit_integration::UserEvent;
use crate::{
@@ -65,6 +66,15 @@ struct WgpuWinitRunning<'app> {
/// Wrapped in an `Rc<RefCell<…>>` so it can be re-entrantly shared via a weak-pointer.
shared: Rc<RefCell<SharedState>>,
pending_deltas: TexturesDelta,
}
impl Drop for WgpuWinitRunning<'_> {
fn drop(&mut self) {
// Avoid debug panic when dropping unapplied deltas on teardown
self.pending_deltas.clear();
}
}
/// Everything needed by the immediate viewport renderer.\
@@ -91,6 +101,9 @@ pub struct Viewport {
info: ViewportInfo,
actions_requested: Vec<ActionRequested>,
/// Any not yet applied deltas for this viewport.
pending_delta: TexturesDelta,
/// `None` for sync viewports.
viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
@@ -102,6 +115,13 @@ pub struct Viewport {
egui_winit: Option<egui_winit::State>,
}
impl Drop for Viewport {
fn drop(&mut self) {
// Avoid debug panic when dropping unapplied deltas on teardown
self.pending_delta.clear();
}
}
// ----------------------------------------------------------------------------
impl<'app> WgpuWinitApp<'app> {
@@ -328,6 +348,7 @@ impl<'app> WgpuWinitApp<'app> {
viewport_ui_cb: None,
window: Some(window),
egui_winit: Some(egui_winit),
pending_delta: Default::default(),
},
);
@@ -358,6 +379,7 @@ impl<'app> WgpuWinitApp<'app> {
integration,
app,
shared,
pending_deltas: Default::default(),
}))
}
}
@@ -596,6 +618,7 @@ impl WgpuWinitRunning<'_> {
app,
integration,
shared,
pending_deltas,
} = self;
let mut frame_timer = crate::stopwatch::Stopwatch::new();
@@ -699,6 +722,8 @@ impl WgpuWinitRunning<'_> {
viewport_output,
} = full_output;
pending_deltas.append(textures_delta);
remove_viewports_not_in(viewports, painter, viewport_from_window, &viewport_output);
let Some(viewport) = viewports.get_mut(&viewport_id) else {
@@ -735,7 +760,7 @@ impl WgpuWinitRunning<'_> {
pixels_per_point,
app.clear_color(&egui_ctx.global_style().visuals),
&clipped_primitives,
&textures_delta,
pending_deltas,
screenshot_commands,
window,
);
@@ -1125,8 +1150,11 @@ fn render_immediate_viewport(
} = &mut *shared_mut;
let Some(viewport) = viewports.get_mut(&ids.this) else {
warn!("Viewport disappeared unexpectedly!");
return;
};
viewport.pending_delta.append(textures_delta);
viewport.info.events.clear(); // they should have been processed
let (Some(egui_winit), Some(window)) = (&mut viewport.egui_winit, &viewport.window) else {
return;
@@ -1149,7 +1177,7 @@ fn render_immediate_viewport(
pixels_per_point,
[0.0, 0.0, 0.0, 0.0],
&clipped_primitives,
&textures_delta,
&mut viewport.pending_delta,
vec![],
window,
);
@@ -1268,6 +1296,7 @@ fn initialize_or_update_viewport<'a>(
viewport_ui_cb,
window: None,
egui_winit: None,
pending_delta: Default::default(),
})
}

View File

@@ -324,7 +324,6 @@ impl AppRunner {
/// Paint the results of the last call to [`Self::logic`].
pub fn paint(&mut self) {
let textures_delta = std::mem::take(&mut self.textures_delta);
let clipped_primitives = std::mem::take(&mut self.clipped_primitives);
if let Some(clipped_primitives) = clipped_primitives {
@@ -347,7 +346,7 @@ impl AppRunner {
self.app.clear_color(&self.egui_ctx.global_style().visuals),
&clipped_primitives,
self.egui_ctx.pixels_per_point(),
&textures_delta,
&mut self.textures_delta,
screenshot_commands,
) {
log::error!("Failed to paint: {}", super::string_from_js_value(&err));

View File

@@ -24,7 +24,7 @@ pub(crate) trait WebPainter {
clear_color: [f32; 4],
clipped_primitives: &[egui::ClippedPrimitive],
pixels_per_point: f32,
textures_delta: &egui::TexturesDelta,
textures_delta: &mut egui::TexturesDelta,
capture: Vec<UserData>,
) -> Result<(), JsValue>;

View File

@@ -61,13 +61,16 @@ impl WebPainter for WebPainterGlow {
clear_color: [f32; 4],
clipped_primitives: &[egui::ClippedPrimitive],
pixels_per_point: f32,
textures_delta: &egui::TexturesDelta,
textures_delta: &mut egui::TexturesDelta,
capture: Vec<UserData>,
) -> Result<(), JsValue> {
let canvas_dimension = [self.canvas.width(), self.canvas.height()];
for (id, image_delta) in &textures_delta.set {
self.painter.set_texture(*id, image_delta);
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
for (id, image_deltas) in textures_delta.set.drain() {
for image_delta in image_deltas {
self.painter.set_texture(id, &image_delta);
}
}
egui_glow::painter::clear(self.painter.gl(), canvas_dimension, clear_color);
@@ -79,7 +82,8 @@ impl WebPainter for WebPainterGlow {
self.screenshots.push((image, capture));
}
for &id in &textures_delta.free {
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
for id in textures_delta.free.drain() {
self.painter.free_texture(id);
}

View File

@@ -164,7 +164,7 @@ impl WebPainter for WebPainterWgpu {
clear_color: [f32; 4],
clipped_primitives: &[egui::ClippedPrimitive],
pixels_per_point: f32,
textures_delta: &egui::TexturesDelta,
textures_delta: &mut egui::TexturesDelta,
capture_data: Vec<UserData>,
) -> Result<(), JsValue> {
let capture = !capture_data.is_empty();
@@ -210,13 +210,16 @@ impl WebPainter for WebPainterWgpu {
let user_cmd_bufs = {
let mut renderer = render_state.renderer.write();
for (id, image_delta) in &textures_delta.set {
renderer.update_texture(
&render_state.device,
&render_state.queue,
*id,
image_delta,
);
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
for (id, image_deltas) in textures_delta.set.drain() {
for image_delta in image_deltas {
renderer.update_texture(
&render_state.device,
&render_state.queue,
id,
&image_delta,
);
}
}
renderer.update_buffers(
@@ -388,8 +391,9 @@ impl WebPainter for WebPainterWgpu {
// However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live.
{
let mut renderer = render_state.renderer.write();
for id in &textures_delta.free {
renderer.free_texture(id);
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
for id in textures_delta.free.drain() {
renderer.free_texture(&id);
}
}