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

@@ -31,16 +31,17 @@ use egui::{
}; };
#[cfg(feature = "accesskit")] #[cfg(feature = "accesskit")]
use egui_winit::accesskit_winit; use egui_winit::accesskit_winit;
use log::warn;
use crate::{
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
native::{epi_integration::EpiIntegration, winit_integration::is_invisible_or_minimized},
};
use super::{ use super::{
epi_integration, event_loop_context, epi_integration, event_loop_context,
winit_integration::{EventResult, UserEvent, WinitApp, create_egui_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: // Types:
@@ -73,6 +74,16 @@ struct GlowWinitRunning<'app> {
// NOTE: one painter shared by all viewports. // NOTE: one painter shared by all viewports.
painter: Rc<RefCell<egui_glow::Painter>>, 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. /// This struct will contain both persistent and temporary glutin state.
@@ -114,6 +125,9 @@ struct Viewport {
info: ViewportInfo, info: ViewportInfo,
actions_requested: Vec<egui_winit::ActionRequested>, actions_requested: Vec<egui_winit::ActionRequested>,
/// Any not yet applied deltas for this viewport.
pending_delta: TexturesDelta,
/// The user-callback that shows the ui. /// The user-callback that shows the ui.
/// None for immediate viewports. /// None for immediate viewports.
viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>, viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
@@ -125,6 +139,13 @@ struct Viewport {
egui_winit: Option<egui_winit::State>, 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> { impl<'app> GlowWinitApp<'app> {
@@ -353,6 +374,7 @@ impl<'app> GlowWinitApp<'app> {
app, app,
glutin, glutin,
painter, painter,
pending_deltas: Default::default(),
})) }))
} }
} }
@@ -653,6 +675,7 @@ impl GlowWinitRunning<'_> {
app, app,
glutin, glutin,
painter, painter,
pending_deltas,
.. ..
} = self; } = self;
@@ -666,6 +689,7 @@ impl GlowWinitRunning<'_> {
pixels_per_point, pixels_per_point,
viewport_output, viewport_output,
} = full_output; } = full_output;
pending_deltas.append(textures_delta);
glutin.remove_viewports_not_in(&viewport_output); 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); 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 { if is_visible {
let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point); 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(); let screen_size_in_pixels: [u32; 2] = window.inner_size().into();
if !clear_before_update { if !clear_before_update {
painter.clear(screen_size_in_pixels, clear_color); 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(..) { 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); 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 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![], deferred_commands: vec![],
info: viewport_info, info: viewport_info,
actions_requested: Default::default(), actions_requested: Default::default(),
pending_delta: Default::default(),
viewport_ui_cb: None, viewport_ui_cb: None,
gl_surface: None, gl_surface: None,
window: window.map(Arc::new), window: window.map(Arc::new),
@@ -1436,6 +1454,7 @@ fn initialize_or_update_viewport(
deferred_commands: vec![], deferred_commands: vec![],
info: Default::default(), info: Default::default(),
actions_requested: Default::default(), actions_requested: Default::default(),
pending_delta: Default::default(),
viewport_ui_cb, viewport_ui_cb,
window: None, window: None,
egui_winit: None, egui_winit: None,
@@ -1584,8 +1603,10 @@ fn render_immediate_viewport(
} = &mut *glutin; } = &mut *glutin;
let Some(viewport) = viewports.get_mut(&viewport_id) else { let Some(viewport) = viewports.get_mut(&viewport_id) else {
warn!("Viewport disappeared unexpectedly!");
return; return;
}; };
viewport.pending_delta.append(textures_delta);
viewport.info.events.clear(); // they should have been processed viewport.info.events.clear(); // they should have been processed
@@ -1621,7 +1642,7 @@ fn render_immediate_viewport(
screen_size_in_pixels, screen_size_in_pixels,
pixels_per_point, pixels_per_point,
&clipped_primitives, &clipped_primitives,
&textures_delta, &mut viewport.pending_delta,
); );
{ {

View File

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

View File

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

View File

@@ -164,7 +164,7 @@ impl WebPainter for WebPainterWgpu {
clear_color: [f32; 4], clear_color: [f32; 4],
clipped_primitives: &[egui::ClippedPrimitive], clipped_primitives: &[egui::ClippedPrimitive],
pixels_per_point: f32, pixels_per_point: f32,
textures_delta: &egui::TexturesDelta, textures_delta: &mut egui::TexturesDelta,
capture_data: Vec<UserData>, capture_data: Vec<UserData>,
) -> Result<(), JsValue> { ) -> Result<(), JsValue> {
let capture = !capture_data.is_empty(); let capture = !capture_data.is_empty();
@@ -210,13 +210,16 @@ impl WebPainter for WebPainterWgpu {
let user_cmd_bufs = { let user_cmd_bufs = {
let mut renderer = render_state.renderer.write(); let mut renderer = render_state.renderer.write();
for (id, image_delta) in &textures_delta.set { #[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
renderer.update_texture( for (id, image_deltas) in textures_delta.set.drain() {
&render_state.device, for image_delta in image_deltas {
&render_state.queue, renderer.update_texture(
*id, &render_state.device,
image_delta, &render_state.queue,
); id,
&image_delta,
);
}
} }
renderer.update_buffers( 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. // 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(); let mut renderer = render_state.renderer.write();
for id in &textures_delta.free { #[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
renderer.free_texture(id); for id in textures_delta.free.drain() {
renderer.free_texture(&id);
} }
} }

View File

@@ -478,7 +478,7 @@ impl Painter {
pixels_per_point: f32, pixels_per_point: f32,
clear_color: [f32; 4], clear_color: [f32; 4],
clipped_primitives: &[epaint::ClippedPrimitive], clipped_primitives: &[epaint::ClippedPrimitive],
textures_delta: &epaint::textures::TexturesDelta, textures_delta: &mut epaint::textures::TexturesDelta,
capture_data: Vec<UserData>, capture_data: Vec<UserData>,
window: &Arc<winit::window::Window>, window: &Arc<winit::window::Window>,
) -> f32 { ) -> f32 {
@@ -545,21 +545,6 @@ impl Painter {
commands_submitted: false, commands_submitted: false,
}; };
{
// Upload textures before the surface-dependent early-returns below:
// uploads only need the device + queue, and the atlas dirty region is
// already consumed, so dropping the delta would desync the font texture.
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,
);
}
}
let Some(surface_state) = self.surfaces.get_mut(&viewport_id) else { let Some(surface_state) = self.surfaces.get_mut(&viewport_id) else {
return vsync_sec; return vsync_sec;
}; };
@@ -579,6 +564,18 @@ impl Painter {
let user_cmd_bufs = { let user_cmd_bufs = {
let mut renderer = render_state.renderer.write(); let mut renderer = render_state.renderer.write();
#[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( renderer.update_buffers(
&render_state.device, &render_state.device,
&render_state.queue, &render_state.queue,
@@ -742,8 +739,9 @@ impl Painter {
// However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live. // 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(); let mut renderer = render_state.renderer.write();
for id in &textures_delta.free { #[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
renderer.free_texture(id); for id in textures_delta.free.drain() {
renderer.free_texture(&id);
} }
} }

View File

@@ -775,6 +775,7 @@ impl Context {
/// ui.label("Hello egui!"); /// ui.label("Hello egui!");
/// }); /// });
/// // handle full_output /// // handle full_output
/// # full_output.drop_without_applying_deltas();
/// ``` /// ```
#[must_use] #[must_use]
pub fn run_ui(&self, new_input: RawInput, mut run_ui: impl FnMut(&mut Ui)) -> FullOutput { pub fn run_ui(&self, new_input: RawInput, mut run_ui: impl FnMut(&mut Ui)) -> FullOutput {
@@ -892,6 +893,7 @@ impl Context {
/// ///
/// let full_output = ctx.end_pass(); /// let full_output = ctx.end_pass();
/// // handle full_output /// // handle full_output
/// # full_output.drop_without_applying_deltas();
/// ``` /// ```
pub fn begin_pass(&self, mut new_input: RawInput) { pub fn begin_pass(&self, mut new_input: RawInput) {
profiling::function_scope!(); profiling::function_scope!();
@@ -4296,6 +4298,7 @@ mod test {
assert_eq!(num_calls, 1); assert_eq!(num_calls, 1);
assert_eq!(output.platform_output.num_completed_passes, 1); assert_eq!(output.platform_output.num_completed_passes, 1);
assert!(!output.platform_output.requested_discard()); assert!(!output.platform_output.requested_discard());
output.drop_without_applying_deltas();
} }
// A single call, with a denied request to discard: // A single call, with a denied request to discard:
@@ -4321,6 +4324,7 @@ mod test {
.reason, .reason,
"test" "test"
); );
output.drop_without_applying_deltas();
} }
} }
@@ -4341,6 +4345,7 @@ mod test {
assert_eq!(num_calls, 1); assert_eq!(num_calls, 1);
assert_eq!(output.platform_output.num_completed_passes, 1); assert_eq!(output.platform_output.num_completed_passes, 1);
assert!(!output.platform_output.requested_discard()); assert!(!output.platform_output.requested_discard());
output.drop_without_applying_deltas();
} }
// Request discard once: // Request discard once:
@@ -4363,6 +4368,7 @@ mod test {
!output.platform_output.requested_discard(), !output.platform_output.requested_discard(),
"The request should have been cleared when fulfilled" "The request should have been cleared when fulfilled"
); );
output.drop_without_applying_deltas();
} }
// Request discard twice: // Request discard twice:
@@ -4387,6 +4393,7 @@ mod test {
output.platform_output.requested_discard(), output.platform_output.requested_discard(),
"The unfulfilled request should be reported" "The unfulfilled request should be reported"
); );
output.drop_without_applying_deltas();
} }
} }
@@ -4415,6 +4422,7 @@ mod test {
!output.platform_output.requested_discard(), !output.platform_output.requested_discard(),
"The request should have been cleared when fulfilled" "The request should have been cleared when fulfilled"
); );
output.drop_without_applying_deltas();
} }
} }
} }

View File

@@ -16,7 +16,7 @@ pub struct FullOutput {
/// Texture changes since last frame (including the font texture). /// Texture changes since last frame (including the font texture).
/// ///
/// The backend needs to apply [`crate::TexturesDelta::set`] _before_ painting, /// The backend needs to apply [`crate::TexturesDelta::push`] _before_ painting,
/// and free any texture in [`crate::TexturesDelta::free`] _after_ painting. /// and free any texture in [`crate::TexturesDelta::free`] _after_ painting.
/// ///
/// It is assumed that all egui viewports share the same painter and texture namespace. /// It is assumed that all egui viewports share the same painter and texture namespace.
@@ -68,6 +68,12 @@ impl FullOutput {
} }
} }
} }
/// [`epaint::textures::TexturesDelta`] will panic when dropped with still unapplied deltas,
/// this is a helper to clear the deltas.
pub fn drop_without_applying_deltas(mut self) {
self.textures_delta.clear();
}
} }
/// Information about text being edited. /// Information about text being edited.

View File

@@ -673,18 +673,20 @@ pub enum WidgetType {
pub fn __run_test_ctx(mut run_ui: impl FnMut(&Context)) { pub fn __run_test_ctx(mut run_ui: impl FnMut(&Context)) {
let ctx = Context::default(); let ctx = Context::default();
ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time) ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time)
let _ = ctx.run_ui(Default::default(), |ui| { let output = ctx.run_ui(Default::default(), |ui| {
run_ui(ui.ctx()); run_ui(ui.ctx());
}); });
output.drop_without_applying_deltas();
} }
/// For use in tests; especially doctests. /// For use in tests; especially doctests.
pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui)) { pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui)) {
let ctx = Context::default(); let ctx = Context::default();
ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time) ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time)
let _ = ctx.run_ui(Default::default(), |ui| { let output = ctx.run_ui(Default::default(), |ui| {
add_contents(ui); add_contents(ui);
}); });
output.drop_without_applying_deltas();
} }
pub fn accesskit_root_id() -> Id { pub fn accesskit_root_id() -> Id {

View File

@@ -773,7 +773,7 @@ mod tests {
.or_default() .or_default()
.selection = Some(test_selection()); .selection = Some(test_selection());
let _ = ctx.run_ui(RawInput::default(), |_| {}); let output = ctx.run_ui(RawInput::default(), |_| {});
assert!( assert!(
plugin plugin
.lock() .lock()
@@ -782,11 +782,13 @@ mod tests {
.is_some_and(ViewportLabelSelectionState::has_selection), .is_some_and(ViewportLabelSelectionState::has_selection),
"a pass in another viewport must not clear the child viewport selection" "a pass in another viewport must not clear the child viewport selection"
); );
output.drop_without_applying_deltas();
let _ = ctx.run_ui(child_viewport_input(child_viewport_id), |_| {}); let output = ctx.run_ui(child_viewport_input(child_viewport_id), |_| {});
assert!( assert!(
!plugin.lock().has_selection(), !plugin.lock().has_selection(),
"the selection must be cleared when its labels disappear from the same viewport" "the selection must be cleared when its labels disappear from the same viewport"
); );
output.drop_without_applying_deltas();
} }
} }

View File

@@ -71,9 +71,8 @@
use std::sync::Arc; use std::sync::Arc;
use epaint::{Pos2, Vec2};
use crate::{AsId, Context, Id, Ui}; use crate::{AsId, Context, Id, Ui};
use epaint::{Pos2, Vec2};
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------

View File

@@ -28,18 +28,21 @@ pub fn criterion_benchmark(c: &mut Criterion) {
// The most end-to-end benchmark. // The most end-to-end benchmark.
c.bench_function("demo_with_tessellate__realistic", |b| { c.bench_function("demo_with_tessellate__realistic", |b| {
b.iter(|| { b.iter(|| {
let full_output = ctx.run_ui(RawInput::default(), |ui| { let mut full_output = ctx.run_ui(RawInput::default(), |ui| {
demo_windows.ui(ui); demo_windows.ui(ui);
}); });
ctx.tessellate(full_output.shapes, full_output.pixels_per_point) ctx.tessellate(full_output.shapes, full_output.pixels_per_point);
full_output.textures_delta.clear(); // Don't panic on drop with unapplied deltas
}); });
}); });
c.bench_function("demo_no_tessellate", |b| { c.bench_function("demo_no_tessellate", |b| {
b.iter(|| { b.iter(|| {
ctx.run_ui(RawInput::default(), |ui| { let output = ctx.run_ui(RawInput::default(), |ui| {
demo_windows.ui(ui); demo_windows.ui(ui);
}) });
output.drop_without_applying_deltas();
}); });
}); });
@@ -49,6 +52,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("demo_only_tessellate", |b| { c.bench_function("demo_only_tessellate", |b| {
b.iter(|| ctx.tessellate(full_output.shapes.clone(), full_output.pixels_per_point)); b.iter(|| ctx.tessellate(full_output.shapes.clone(), full_output.pixels_per_point));
}); });
full_output.drop_without_applying_deltas();
} }
if false { if false {
@@ -66,7 +70,7 @@ pub fn criterion_benchmark(c: &mut Criterion) {
{ {
let ctx = egui::Context::default(); let ctx = egui::Context::default();
let _ = ctx.run_ui(RawInput::default(), |ui| { let output = ctx.run_ui(RawInput::default(), |ui| {
c.bench_function("label &str", |b| { c.bench_function("label &str", |b| {
b.iter_batched_ref( b.iter_batched_ref(
|| create_benchmark_ui(ui), || create_benchmark_ui(ui),
@@ -86,11 +90,12 @@ pub fn criterion_benchmark(c: &mut Criterion) {
); );
}); });
}); });
output.drop_without_applying_deltas();
} }
{ {
let ctx = egui::Context::default(); let ctx = egui::Context::default();
let _ = ctx.run_ui(RawInput::default(), |ui| { let output = ctx.run_ui(RawInput::default(), |ui| {
let mut group = c.benchmark_group("button"); let mut group = c.benchmark_group("button");
// To ensure we have a valid image, let's use the font texture. The size // To ensure we have a valid image, let's use the font texture. The size
@@ -134,6 +139,8 @@ pub fn criterion_benchmark(c: &mut Criterion) {
); );
}); });
}); });
output.drop_without_applying_deltas();
} }
{ {

View File

@@ -72,11 +72,12 @@ fn test_egui_e2e() {
const NUM_FRAMES: usize = 5; const NUM_FRAMES: usize = 5;
for _ in 0..NUM_FRAMES { for _ in 0..NUM_FRAMES {
let full_output = ctx.run_ui(raw_input.clone(), |ui| { let mut full_output = ctx.run_ui(raw_input.clone(), |ui| {
demo_windows.ui(ui); demo_windows.ui(ui);
}); });
let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point); let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point);
assert!(!clipped_primitives.is_empty()); assert!(!clipped_primitives.is_empty());
full_output.textures_delta.clear(); // Don't panic on drop with unapplied deltas
} }
} }
@@ -91,7 +92,7 @@ fn test_egui_zero_window_size() {
const NUM_FRAMES: usize = 5; const NUM_FRAMES: usize = 5;
for _ in 0..NUM_FRAMES { for _ in 0..NUM_FRAMES {
let full_output = ctx.run_ui(raw_input.clone(), |ui| { let mut full_output = ctx.run_ui(raw_input.clone(), |ui| {
demo_windows.ui(ui); demo_windows.ui(ui);
}); });
let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point); let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point);
@@ -100,6 +101,7 @@ fn test_egui_zero_window_size() {
"There should be nothing to show, has at least one primitive with clip_rect: {:?}", "There should be nothing to show, has at least one primitive with clip_rect: {:?}",
clipped_primitives[0].clip_rect clipped_primitives[0].clip_rect
); );
full_output.textures_delta.clear(); // Don't panic on drop with unapplied deltas
} }
} }

View File

@@ -358,17 +358,21 @@ impl Painter {
screen_size_px: [u32; 2], screen_size_px: [u32; 2],
pixels_per_point: f32, pixels_per_point: f32,
clipped_primitives: &[egui::ClippedPrimitive], clipped_primitives: &[egui::ClippedPrimitive],
textures_delta: &egui::TexturesDelta, textures_delta: &mut egui::TexturesDelta,
) { ) {
profiling::function_scope!(); profiling::function_scope!();
for (id, image_delta) in &textures_delta.set { #[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
self.set_texture(*id, image_delta); for (id, image_deltas) in textures_delta.set.drain() {
for image_delta in image_deltas {
self.set_texture(id, &image_delta);
}
} }
self.paint_primitives(screen_size_px, pixels_per_point, clipped_primitives); self.paint_primitives(screen_size_px, pixels_per_point, clipped_primitives);
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.free_texture(id); self.free_texture(id);
} }
} }

View File

@@ -107,8 +107,11 @@ impl EguiGlow {
let shapes = std::mem::take(&mut self.shapes); let shapes = std::mem::take(&mut self.shapes);
let mut textures_delta = std::mem::take(&mut self.textures_delta); let mut textures_delta = std::mem::take(&mut self.textures_delta);
for (id, image_delta) in textures_delta.set { #[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
self.painter.set_texture(id, &image_delta); for (id, image_deltas) in textures_delta.set.drain() {
for image_delta in image_deltas {
self.painter.set_texture(id, &image_delta);
}
} }
let pixels_per_point = self.pixels_per_point; let pixels_per_point = self.pixels_per_point;
@@ -117,7 +120,8 @@ impl EguiGlow {
self.painter self.painter
.paint_primitives(dimensions, pixels_per_point, &clipped_primitives); .paint_primitives(dimensions, pixels_per_point, &clipped_primitives);
for id in textures_delta.free.drain(..) { #[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
for id in textures_delta.free.drain() {
self.painter.free_texture(id); self.painter.free_texture(id);
} }
} }

View File

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

View File

@@ -1,4 +1,5 @@
use egui::TexturesDelta; use egui::TexturesDelta;
use std::mem;
pub trait TestRenderer { pub trait TestRenderer {
/// We use this to pass the glow / wgpu render state to [`eframe::Frame`]. /// 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) {} fn setup_eframe(&self, _cc: &mut eframe::CreationContext<'_>, _frame: &mut eframe::Frame) {}
/// Handle a [`TexturesDelta`] by updating the renderer's textures. /// 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. /// 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. /// By default, this will create a wgpu renderer if the wgpu feature is enabled.
pub enum LazyRenderer { pub enum LazyRenderer {
Uninitialized { Uninitialized {
texture_ops: Vec<egui::TexturesDelta>, textures_delta: TexturesDelta,
builder: Option<Box<dyn FnOnce() -> Box<dyn TestRenderer>>>, builder: Option<Box<dyn FnOnce() -> Box<dyn TestRenderer>>>,
}, },
Initialized { Initialized {
@@ -39,7 +40,7 @@ impl Default for LazyRenderer {
return Self::new(crate::wgpu::WgpuTestRenderer::new); return Self::new(crate::wgpu::WgpuTestRenderer::new);
#[cfg(not(feature = "wgpu"))] #[cfg(not(feature = "wgpu"))]
return Self::Uninitialized { return Self::Uninitialized {
texture_ops: Vec::new(), textures_delta: Vec::new(),
builder: None, builder: None,
}; };
} }
@@ -48,16 +49,19 @@ impl Default for LazyRenderer {
impl LazyRenderer { impl LazyRenderer {
pub fn new<T: TestRenderer + 'static>(create_renderer: impl FnOnce() -> T + 'static) -> Self { pub fn new<T: TestRenderer + 'static>(create_renderer: impl FnOnce() -> T + 'static) -> Self {
Self::Uninitialized { Self::Uninitialized {
texture_ops: Vec::new(), textures_delta: Default::default(),
builder: Some(Box::new(move || Box::new(create_renderer()))), builder: Some(Box::new(move || Box::new(create_renderer()))),
} }
} }
} }
impl TestRenderer for LazyRenderer { impl TestRenderer for LazyRenderer {
fn handle_delta(&mut self, delta: &TexturesDelta) { fn handle_delta(&mut self, delta: &mut TexturesDelta) {
match self { 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), Self::Initialized { renderer } => renderer.handle_delta(delta),
} }
} }
@@ -70,15 +74,15 @@ impl TestRenderer for LazyRenderer {
) -> Result<image::RgbaImage, String> { ) -> Result<image::RgbaImage, String> {
match self { match self {
Self::Uninitialized { Self::Uninitialized {
texture_ops, textures_delta,
builder: build, builder: build,
} => { } => {
let mut renderer = build.take().ok_or({ let mut renderer = build.take().ok_or({
"No default renderer available. \ "No default renderer available. \
Enable the wgpu feature or set one via HarnessBuilder::renderer" Enable the wgpu feature or set one via HarnessBuilder::renderer"
})?(); })?();
for delta in texture_ops.drain(..) { if !textures_delta.is_empty() {
renderer.handle_delta(&delta); renderer.handle_delta(textures_delta);
} }
let image = renderer.render(ctx, output)?; let image = renderer.render(ctx, output)?;
*self = Self::Initialized { renderer }; *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()); 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(); let mut renderer = self.render_state.renderer.write();
for (id, image) in &delta.set { #[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
renderer.update_texture( for (id, images) in delta.set.drain() {
&self.render_state.device, for image in images {
&self.render_state.queue, renderer.update_texture(
*id, &self.render_state.device,
image, &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);
} }
} }

View File

@@ -144,7 +144,8 @@ fn accesskit_output_single_egui_frame(run_ui: impl FnMut(&mut Ui)) -> TreeUpdate
ctx.global_style_mut(|style| style.animation_time = 0.0); ctx.global_style_mut(|style| style.animation_time = 0.0);
ctx.enable_accesskit(); ctx.enable_accesskit();
let output = ctx.run_ui(RawInput::default(), run_ui); let mut output = ctx.run_ui(RawInput::default(), run_ui);
output.textures_delta.clear(); // Don't panic on drop with unapplied deltas
output output
.platform_output .platform_output

View File

@@ -1,4 +1,7 @@
use crate::{ImageData, ImageDelta, TextureId}; use crate::{ImageData, ImageDelta, TextureId};
use ahash::{HashMap, HashSet};
use smallvec::{SmallVec, smallvec};
use std::mem;
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@@ -40,7 +43,7 @@ impl TextureManager {
options, options,
}); });
self.delta.set.push((id, ImageDelta::full(image, options))); self.delta.push(id, ImageDelta::full(image, options));
id id
} }
@@ -58,10 +61,8 @@ impl TextureManager {
// whole update // whole update
meta.size = delta.image.size(); meta.size = delta.image.size();
meta.bytes_per_pixel = delta.image.bytes_per_pixel(); meta.bytes_per_pixel = delta.image.bytes_per_pixel();
// since we update the whole image, we can discard all old enqueued deltas
self.delta.set.retain(|(x, _)| x != &id);
} }
self.delta.set.push((id, delta)); self.delta.push(id, delta);
} else { } else {
debug_assert!(false, "Tried setting texture {id:?} which is not allocated"); debug_assert!(false, "Tried setting texture {id:?} which is not allocated");
} }
@@ -74,7 +75,7 @@ impl TextureManager {
meta.retain_count -= 1; meta.retain_count -= 1;
if meta.retain_count == 0 { if meta.retain_count == 0 {
entry.remove(); entry.remove();
self.delta.free.push(id); self.delta.free(id);
} }
} else { } else {
debug_assert!(false, "Tried freeing texture {id:?} which is not allocated"); debug_assert!(false, "Tried freeing texture {id:?} which is not allocated");
@@ -118,6 +119,12 @@ impl TextureManager {
} }
} }
impl Drop for TextureManager {
fn drop(&mut self) {
self.delta.clear(); // Prevent a debug panic on application shutdown
}
}
/// Meta-data about an allocated texture. /// Meta-data about an allocated texture.
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct TextureMeta { pub struct TextureMeta {
@@ -276,10 +283,10 @@ pub enum TextureWrapMode {
#[must_use = "The painter must take care of this"] #[must_use = "The painter must take care of this"]
pub struct TexturesDelta { pub struct TexturesDelta {
/// New or changed textures. Apply before painting. /// New or changed textures. Apply before painting.
pub set: Vec<(TextureId, ImageDelta)>, pub set: HashMap<TextureId, SmallVec<[ImageDelta; 1]>>,
/// Textures to free after painting. /// Textures to free after painting.
pub free: Vec<TextureId>, pub free: HashSet<TextureId>,
} }
impl TexturesDelta { impl TexturesDelta {
@@ -287,9 +294,36 @@ impl TexturesDelta {
self.set.is_empty() && self.free.is_empty() self.set.is_empty() && self.free.is_empty()
} }
/// Inserts a [`ImageDelta`].
///
/// If this [`TexturesDelta`] already contains this [`TextureId`], and this is a `whole` delta,
/// the previous deltas for this id are discarded.
pub fn push(&mut self, id: TextureId, delta: ImageDelta) {
if delta.is_whole() {
// It replaces the whole texture, fine to overwrite any previous deltas
self.set.insert(id, smallvec![delta]);
} else {
self.set.entry(id).or_default().push(delta);
}
}
pub fn free(&mut self, id: TextureId) {
self.free.insert(id);
}
#[expect(clippy::iter_over_hash_type)]
pub fn append(&mut self, mut newer: Self) { pub fn append(&mut self, mut newer: Self) {
self.set.extend(newer.set); // Only clear previous entries on append, not on set, since within a frame a texture might
self.free.append(&mut newer.free); // be created and immediately removed again.
for id in &newer.free {
self.set.remove(id);
}
for (id, deltas) in newer.set.drain() {
for delta in deltas {
self.push(id, delta);
}
}
self.free.extend(mem::take(&mut newer.free));
} }
pub fn clear(&mut self) { pub fn clear(&mut self) {
@@ -298,6 +332,17 @@ impl TexturesDelta {
} }
} }
impl Drop for TexturesDelta {
fn drop(&mut self) {
debug_assert!(
self.is_empty(),
"Dropped TexturesDelta with {} unapplied deltas. Deltas need to be handled. \
If you want to drop this intentionally call `clear` before dropping.",
self.free.len() + self.set.len()
);
}
}
impl std::fmt::Debug for TexturesDelta { impl std::fmt::Debug for TexturesDelta {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use std::fmt::Write as _; use std::fmt::Write as _;
@@ -305,21 +350,24 @@ impl std::fmt::Debug for TexturesDelta {
let mut debug_struct = f.debug_struct("TexturesDelta"); let mut debug_struct = f.debug_struct("TexturesDelta");
if !self.set.is_empty() { if !self.set.is_empty() {
let mut string = String::new(); let mut string = String::new();
for (tex_id, delta) in &self.set { #[expect(clippy::iter_over_hash_type)]
let size = delta.image.size(); for (tex_id, deltas) in &self.set {
if let Some(pos) = delta.pos { for delta in deltas {
write!( let size = delta.image.size();
string, if let Some(pos) = delta.pos {
"{:?} partial ([{} {}] - [{} {}]), ", write!(
tex_id, string,
pos[0], "{:?} partial ([{} {}] - [{} {}]), ",
pos[1], tex_id,
pos[0] + size[0], pos[0],
pos[1] + size[1] pos[1],
) pos[0] + size[0],
.ok(); pos[1] + size[1]
} else { )
write!(string, "{:?} full {}x{}, ", tex_id, size[0], size[1]).ok(); .ok();
} else {
write!(string, "{:?} full {}x{}, ", tex_id, size[0], size[1]).ok();
}
} }
} }
debug_struct.field("set", &string); debug_struct.field("set", &string);