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