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:
@@ -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,
|
||||
);
|
||||
|
||||
{
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user