From 614a6249fbcaee9a8604f225c617485c42eeb25c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Rouill=C3=A9?= Date: Sun, 25 Jan 2026 16:25:58 +0100 Subject: [PATCH 01/13] New render modes - Optimize winit integration by removing a full clear and blit per frame I found out that most of the time was spent in these 3 steps: - clearing the frame - copy canvas to frame - presenting the frame (another copy) From my understanding, there is no point in doing these 3 steps, if we use the framebuffer as canvas. Which is what the winit implementation now does. `EguiSoftwareRender` now doesn't hold a canvas anymore, it's a new `EguiSoftwareRenderCanvas` struct that does this now. Another optimisation I found while doing this is to only present the dirty/damaged zone of the framebuffer. So render now returns a `DirtyRect` representing the damaged zone that needs to be presented. This improves winit frame times by a lot! - Two new caching modes `Mesh` and `TiledMesh` With this new `DirtyRect` logic, I was wondering how fast simply drawing the zone that is required to be redrawn without caching render would be. First I did the `Mesh` mode, simply caching meshes to generate the `DirtyRect` and rendering any primitive bounding box. Cache lookup is the same as before, final meshes are prepared for cache lookup. Then I was wondering if I could optimize it a bit more, with a new `TiledMesh` mode. This mode compute a set of non overlapping bounding boxes extended to tile limits so there is too many of them. And primitive are now rendered for each intersection with this set of bounding boxes. When writing this, I was wondering if seams would appear as this effectively render primitive meshes in multiple steps, but visualy it looks good on my machine at least. - `egui::Mesh::clone()` removed By changing the render api from `&[ClippedPrimitive]` to `Vec<[ClippedPrimitive]>` I was able to remove the `egui::Mesh::clone()` that was required before. In most cases render will be called with the output of `egui_context.tessellate` making it perfect. And if a clone of the whole vec is required for some reason it would be the same amount of work as before. - `SoftwareBackend` reworked I reworked winit `SoftwareBackend` exposed API. - `is_capture_frame_time` and `set_capture_frame_time` are now removed, frame_time is now always captured as it only cost 2 `Instant::now()` calls, so really not much. - `stats() -> &RenderStats ` are now exposed - `caching`, `set_caching` to read and change the caching modes live. The winit example use it. - `clear_cache`, Clear cache and reclaim memory, this will cause the next frame to redraw everything - RasterStats inner mutability, to allow &self usage when possible While doing all this work I mostly left the raster_stat feature a problem for later me. Well when I tried to activate it back, it force `&self` to `&mut self` to too many points for my taste and could found a good way to fix this. So the fix was to use inner mutability via AtomicU32 for f32 storage and egui::Mutex for rasterisation stats. I split the `RasterStats` struct in two parts: `RenderStats` that contains `RasterStats` with a "nice" API for `start_raster`. I added a few stats for the new render modes. Even if with this changes the `start_raster` would compile with rayon, as a mutex is involved there no point try to add this stats with the rayon feature, so it's still gated to `#[cfg(not(feature = "rayon"))]` --- README.md | 21 +- examples/winit.rs | 47 +- examples/winit_hello.rs | 2 - examples/winit_raw.rs | 32 +- src/dirty_rect.rs | 176 +++++ src/lib.rs | 1418 +++++++++++++++++++++++++-------------- src/raster/rect.rs | 20 +- src/raster/tri.rs | 12 +- src/render.rs | 12 +- src/stats.rs | 248 ++++--- src/test_render.rs | 22 +- src/winit.rs | 133 ++-- tests/mod.rs | 14 +- 13 files changed, 1412 insertions(+), 745 deletions(-) create mode 100644 src/dirty_rect.rs diff --git a/README.md b/README.md index 449b68b..db4461e 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,24 @@ # CPU software render backend for [egui](https://github.com/emilk/egui) -![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg) [![Crates.io](https://img.shields.io/crates/v/egui_software_backend.svg)](https://crates.io/crates/egui_software_backend) -[![Docs](https://docs.rs/egui_software_backend/badge.svg)](https://docs.rs/egui_software_backend/latest/egui_software_backend/) ![demo](demo.png) ```rs -use egui_software_backend::{BufferMutRef, ColorFieldOrder, EguiSoftwareRender}; -let buffer = &mut vec![[0u8; 4]; 512 * 512]; -let mut buffer_ref = BufferMutRef::new(buffer, 512, 512); let ctx = egui::Context::default(); let mut demo = egui_demo_lib::DemoWindows::default(); let mut sw_render = EguiSoftwareRender::new(ColorFieldOrder::Bgra); -let out = ctx.run(egui::RawInput::default(), |ctx| { +let out = ctx.run(raw_input, |ctx| { demo.ui(ctx); }); let primitives = ctx.tessellate(out.shapes, out.pixels_per_point); -sw_render.render( - &mut buffer_ref, - &primitives, - &out.textures_delta, - out.pixels_per_point, -); +sw_render.render(buffer, &primitives, &out.textures_delta, out.pixels_per_point); ``` ## winit quickstart ```rust -use egui::vec2; +use egui::Vec2; use egui_software_backend::{SoftwareBackend, SoftwareBackendAppConfiguration}; struct EguiApp {} @@ -50,7 +40,8 @@ impl egui_software_backend::App for EguiApp { fn main() { let settings = SoftwareBackendAppConfiguration::new() - .inner_size(Some(vec2(500.0, 300.0))) + .inner_size(Some(Vec2::new(500f32, 300f32))) + .resizable(Some(false)) .title(Some("Simple example".to_string())); egui_software_backend::run_app_with_software_backend(settings, EguiApp::new) @@ -62,4 +53,4 @@ fn main() { [egui_backend_selector](https://github.com/AlexanderSchuetz97/egui_backend_selector) can be used in conjunction with this crate to automatically fallback to using this software renderer at runtime. ## Other examples -- bevy + softbuffer see examples/bevy_example folder +- bevy + softbuffer see examples/bevy_example folder \ No newline at end of file diff --git a/examples/winit.rs b/examples/winit.rs index 6754f2f..4beba83 100644 --- a/examples/winit.rs +++ b/examples/winit.rs @@ -1,7 +1,9 @@ +use egui::Ui; use egui::Vec2; use egui::ViewportCommand; use egui_demo_lib::ColorTest; use egui_demo_lib::DemoWindows; +use egui_software_backend::SoftwareRenderCaching; use egui_software_backend::{SoftwareBackend, SoftwareBackendAppConfiguration}; struct EguiApp { @@ -19,12 +21,8 @@ impl EguiApp { frame_times: Vec::new(), } } -} - -impl egui_software_backend::App for EguiApp { - fn update(&mut self, ctx: &egui::Context, backend: &mut SoftwareBackend) { - backend.set_capture_frame_time(true); + fn ui(&mut self, ctx: &egui::Context) { egui::CentralPanel::default().show(ctx, |_ui| { self.demo.ui(ctx); @@ -33,6 +31,45 @@ impl egui_software_backend::App for EguiApp { self.color_test.ui(ui); }); }); + }); + } +} + +impl eframe::App for EguiApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |_ui| { + self.ui(ctx); + }); + } +} + +fn software_backend_ui(backend: &mut SoftwareBackend, ui: &mut Ui) { + let old = backend.caching(); + let mut new = old; + egui::ComboBox::from_label("SoftwareRenderCaching") + .selected_text(format!("{old:?}")) + .show_ui(ui, |ui| { + ui.selectable_value(&mut new, SoftwareRenderCaching::BlendTiled, "BlendTiled"); + ui.selectable_value(&mut new, SoftwareRenderCaching::MeshTiled, "MeshTiled"); + ui.selectable_value(&mut new, SoftwareRenderCaching::Mesh, "Mesh"); + ui.selectable_value(&mut new, SoftwareRenderCaching::Direct, "Direct"); + }); + if new != old { + backend.set_caching(new); + } +} + +impl egui_software_backend::App for EguiApp { + fn update(&mut self, ctx: &egui::Context, backend: &mut SoftwareBackend) { + egui::CentralPanel::default().show(ctx, |_ui| { + self.ui(ctx); + + #[cfg(feature = "raster_stats")] + egui::Window::new("Stats").show(ctx, |ui| { + backend.display_stats(ui); + }); + + egui::Window::new("Software Backend").show(ctx, |ui| software_backend_ui(backend, ui)); if self.frame_times.len() < 100 { self.frame_times diff --git a/examples/winit_hello.rs b/examples/winit_hello.rs index 91b7b7c..0c56707 100644 --- a/examples/winit_hello.rs +++ b/examples/winit_hello.rs @@ -12,8 +12,6 @@ impl EguiApp { impl egui_software_backend::App for EguiApp { fn update(&mut self, ctx: &egui::Context, backend: &mut SoftwareBackend) { - backend.set_capture_frame_time(true); - egui::CentralPanel::default().show(ctx, |ui| { let last_frame_time = backend.last_frame_time().unwrap_or_default(); diff --git a/examples/winit_raw.rs b/examples/winit_raw.rs index dccaf3a..c9676ba 100644 --- a/examples/winit_raw.rs +++ b/examples/winit_raw.rs @@ -47,7 +47,11 @@ fn main() { let mut egui_software_render = EguiSoftwareRender::new(ColorFieldOrder::Bgra) .with_allow_raster_opt(!args.no_opt) .with_convert_tris_to_rects(!args.no_rect) - .with_caching(!args.direct); + .with_caching(if args.direct { + egui_software_backend::SoftwareRenderCaching::Direct + } else { + egui_software_backend::SoftwareRenderCaching::BlendTiled + }); let event_loop: EventLoop<()> = EventLoop::new().unwrap(); @@ -139,7 +143,7 @@ fn main() { #[cfg(feature = "raster_stats")] egui::Window::new("Stats").show(ctx, |ui| { - egui_software_render.stats.render(ui); + egui_software_render.display_stats(ui); }); }); @@ -148,22 +152,30 @@ fn main() { .tessellate(full_output.shapes, full_output.pixels_per_point); let mut buffer = app.surface.buffer_mut().unwrap(); - buffer.fill(0); // CLEAR let buffer_ref = &mut BufferMutRef::new( bytemuck::cast_slice_mut(&mut buffer), - width as usize, - height as usize, + width, + height, ); - - egui_software_render.render( + let redraw_everything_this_frame = + egui_software_render.cached_size() != (buffer_ref.width, buffer_ref.height); + let dirty_rect = egui_software_render.render( buffer_ref, - &clipped_primitives, + redraw_everything_this_frame, + clipped_primitives, &full_output.textures_delta, full_output.pixels_per_point, ); - - buffer.present().unwrap(); + if !dirty_rect.is_empty() { + let dirty_rect = softbuffer::Rect { + x: dirty_rect.min_x, + y: dirty_rect.min_y, + width: NonZeroU32::new(dirty_rect.width()).expect("non zero rect"), + height: NonZeroU32::new(dirty_rect.height()).expect("non zero rect"), + }; + buffer.present_with_damage(&[dirty_rect]).unwrap(); + } let now = Instant::now(); if frame_times.len() < 100 { diff --git a/src/dirty_rect.rs b/src/dirty_rect.rs new file mode 100644 index 0000000..76a3853 --- /dev/null +++ b/src/dirty_rect.rs @@ -0,0 +1,176 @@ +use core::ops::Deref; + +use alloc::vec::Vec; + +use crate::TILE_SIZE; + +#[derive(Debug, Clone, Copy)] +pub struct DirtyRect { + pub min_x: u32, + pub min_y: u32, + pub max_x: u32, + pub max_y: u32, +} + +impl DirtyRect { + pub const fn new_empty() -> Self { + Self { + min_x: 0, + min_y: 0, + max_x: 0, + max_y: 0, + } + } + + #[inline] + pub const fn tiled(self) -> Self { + Self { + min_x: self.min_x / TILE_SIZE * TILE_SIZE, + min_y: self.min_y / TILE_SIZE * TILE_SIZE, + max_x: self.max_x.div_ceil(TILE_SIZE) * TILE_SIZE, + max_y: self.max_y.div_ceil(TILE_SIZE) * TILE_SIZE, + } + } + + #[inline] + pub const fn width(self) -> u32 { + self.max_x - self.min_x + } + #[inline] + pub const fn height(self) -> u32 { + self.max_y - self.min_y + } + + #[inline] + pub const fn to_egui_rect(self) -> egui::Rect { + egui::Rect { + min: egui::Pos2 { + x: self.min_x as f32, + y: self.min_y as f32, + }, + max: egui::Pos2 { + x: self.max_x as f32, + y: self.max_y as f32, + }, + } + } + + #[inline] + pub const fn is_empty(&self) -> bool { + self.min_x == self.max_x || self.min_y == self.max_y + } + + #[inline] + pub const fn intersects(self, other: Self) -> bool { + self.min_x < other.max_x && self.max_x > other.min_x + } + + #[inline] + pub fn intersection(self, other: DirtyRect) -> Self { + Self { + min_x: self.min_x.max(other.min_x), + min_y: self.min_y.max(other.min_y), + max_x: self.max_x.min(other.max_x), + max_y: self.max_y.min(other.max_y), + } + } + + #[inline] + pub fn union(&self, other: DirtyRect) -> Self { + Self { + min_x: self.min_x.min(other.min_x), + min_y: self.min_y.min(other.min_y), + max_x: self.max_x.max(other.max_x), + max_y: self.max_y.max(other.max_y), + } + } +} + +#[derive(Debug, Default)] +pub struct ComputeTiledDirtyRects { + minimal_non_overlapping_bboxes: Vec, + pub(crate) bboxes: Vec, + x_intervals: Vec<(u32, u32)>, + ys: Vec, +} + +impl Deref for ComputeTiledDirtyRects { + type Target = [DirtyRect]; + + fn deref(&self) -> &Self::Target { + &self.minimal_non_overlapping_bboxes + } +} + +impl ComputeTiledDirtyRects { + pub fn intersections(&self, other: DirtyRect) -> impl Iterator + '_ { + self.minimal_non_overlapping_bboxes + .iter() + .filter(move |bbox| bbox.intersects(other)) + .map(move |bbox| bbox.intersection(other)) + } + + pub fn set_bboxes(&mut self, boxes: impl Iterator) { + fn merge_intervals(intervals: &mut [(u32, u32)], mut f_yield: impl FnMut((u32, u32))) { + if intervals.is_empty() { + return; + } + intervals.sort_unstable_by(|a, b| a.0.cmp(&b.0)); + let mut it = intervals.iter().copied(); + if let Some(mut last) = it.next() { + for (start, end) in it { + if start <= last.1 { + last.1 = last.1.max(end); + } else { + f_yield(last); + last = (start, end); + } + } + f_yield(last); + } + } + + self.minimal_non_overlapping_bboxes.clear(); + self.bboxes.clear(); + self.bboxes.extend(boxes.map(|b| b.tiled::())); + // Step 1: collect all unique y-coordinates + self.ys.clear(); + self.ys + .extend(self.bboxes.iter().flat_map(|b| [b.min_y, b.max_y])); + self.ys.sort_unstable(); + self.ys.dedup(); + + // Step 2: iterate over horizontal strips + for strip in self.ys.windows(2) { + let min_y = strip[0]; + let max_y = strip[1]; + + // Find boxes intersecting this horizontal strip + self.x_intervals.clear(); + for b in &self.bboxes { + if b.min_y < max_y && b.max_y > min_y { + self.x_intervals.push((b.min_x, b.max_x)); + } + } + + // Merge overlapping x-intervals + merge_intervals(&mut self.x_intervals, |(min_x, max_x)| { + match self.minimal_non_overlapping_bboxes.last_mut() { + Some(rect) + if rect.min_x == min_x && rect.max_x == max_x && rect.max_y == min_y => + { + rect.max_y = max_y; + } + _ => { + self.minimal_non_overlapping_bboxes.push(DirtyRect { + min_x, + min_y, + max_x, + max_y, + }); + } + } + }); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 86311b4..e40254f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,22 +74,26 @@ extern crate alloc; #[cfg(feature = "std")] extern crate std; -use core::ops::Range; +use core::ops::{Deref, DerefMut, Range}; use alloc::{borrow::Cow, vec, vec::Vec}; use egui::{Color32, Mesh, Pos2, Vec2, ahash::HashMap, vec2}; +#[cfg(feature = "rayon")] +use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator}; #[cfg(feature = "raster_stats")] -use crate::stats::RasterStats; +use crate::stats::RenderStats; use crate::{ color::{AvailableImpl, SelectedImpl, swizzle_rgba_bgra}, + dirty_rect::{ComputeTiledDirtyRects, DirtyRect}, egui_texture::EguiTexture, hash::Hash32, render::{draw_egui_mesh, egui_orient2df}, }; pub(crate) mod color; +pub(crate) mod dirty_rect; pub(crate) mod egui_texture; pub(crate) mod hash; pub(crate) mod math; @@ -108,29 +112,8 @@ pub use winit::{ App, SoftwareBackend, SoftwareBackendAppConfiguration, run_app_with_software_backend, }; -#[inline(always)] -#[allow(dead_code)] -pub(crate) fn sse41() -> bool { - #[cfg(all(target_arch = "x86_64", feature = "std"))] - return std::arch::is_x86_feature_detected!("sse4.1"); - #[cfg(any(not(target_arch = "x86_64"), not(feature = "std")))] - return false; -} +const TILE_SIZE: u32 = 64; -#[inline(always)] -#[allow(dead_code)] -pub(crate) fn neon() -> bool { - #[cfg(all(target_arch = "aarch64", feature = "std"))] - // This should always be true on aarch64 - return std::arch::is_aarch64_feature_detected!("neon"); - #[cfg(any(not(target_arch = "aarch64"), not(feature = "std")))] - return false; -} - -const TILE_SIZE: usize = 64; - -/// Used to define the color swizzle order. Some backends require Rgba and others require Bgra. The renderer swizzles -/// textures as they are loaded so they can later be rasterized directly onto the frame buffer. #[derive(Copy, Clone, Default)] pub enum ColorFieldOrder { #[default] @@ -138,23 +121,154 @@ pub enum ColorFieldOrder { Bgra, } -/// Software render backend for egui. -pub struct EguiSoftwareRender { +/// Caching mode for the renderer +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SoftwareRenderCaching { + /// Cache primitives renders, update the dirty tiles + /// + /// This is the default mode and often the fastest mode, but it cost the most memory + /// + /// # Algorithm + /// * Prepare Mesh from primitives + /// * Hash prepared meshes for changes + /// * Render non already cached meshes to cache + /// * Mark dirty tiles + /// * Reclaim unused cached meshes renders + /// * Render dirty tiles by blending cache renders + BlendTiled, + /// Cache primitives meshes, redraw primitives intersecting a set of changed bboxes + /// + /// Primitives are rendered clipped per intersection with a non overlapping set + /// of changed tiled bounding boxes. + /// + /// # Algorithm + /// * Prepare Mesh from primitives + /// * Hash prepared meshes for changes + /// * Accumulate dirty primitives bounding boxes + /// * Reclaim unused cached meshes + /// * Generate non overlaping set of tiled bounding boxes + /// * Render primitives intersecting tiled bounding boxes. + MeshTiled, + /// Cache primitives meshes, redraw primitives in the smallest changed bbox + /// + /// Primitives are rendered clipped to the union of changed bounding boxes. + /// + /// # Algorithm + /// * Prepare Mesh from primitives + /// * Hash prepared meshes for changes + /// * Reclaim unused cached meshes + /// * Render primitives intersecting dirty rect + Mesh, + /// No cache, always redraw the whole frame (slow, for testing mostly) + Direct, +} + +struct EguiSoftwareRenderInner { + cached_size: (u32, u32), textures: HashMap, - cached_primitives: HashMap, - tiles_dim: [usize; 2], + /// Tiles grid size (cols, rows) + tiles_dim: [u32; 2], dirty_tiles: Vec, - target_size: Vec2, - prims_updated_this_frame: usize, + dirty_rects: ComputeTiledDirtyRects, output_field_order: ColorFieldOrder, - canvas: Canvas, - redraw_everything_this_frame: bool, convert_tris_to_rects: bool, allow_raster_opt: bool, - cacheing_enabled: bool, + + caching: SoftwareRenderCaching, simd_impl: AvailableImpl, #[cfg(feature = "raster_stats")] - pub stats: RasterStats, + pub stats: RenderStats, +} + +/// Software render backend for egui. +pub struct EguiSoftwareRender { + tiledcached_primitives: HashMap, + dirtycached_primitives: HashMap, + inner: EguiSoftwareRenderInner, +} + +/// Software render backend for egui with managed canvas. +pub struct EguiSoftwareRenderCanvas { + canvas: Vec<[u8; 4]>, + renderer: EguiSoftwareRender, +} + +impl Deref for EguiSoftwareRenderCanvas { + type Target = EguiSoftwareRender; + + fn deref(&self) -> &Self::Target { + &self.renderer + } +} + +impl DerefMut for EguiSoftwareRenderCanvas { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.renderer + } +} + +#[inline] +fn blit_rect( + simd_impl: impl SelectedImpl, + canvas: &BufferMutRef, + buffer: &mut BufferMutRef, + rect: DirtyRect, + canvas_row_offset: u32, +) { + for y in rect.min_y..rect.max_y { + let src_row = canvas.get_span(rect.min_x, rect.max_x, y + canvas_row_offset); + let dst_row = &mut buffer.get_mut_span(rect.min_x, rect.max_x, y); + + simd_impl.egui_blend_u8_slice(src_row, dst_row) + } +} + +impl EguiSoftwareRenderCanvas { + pub fn render( + &mut self, + buffer_ref: &mut BufferMutRef, + paint_jobs: Vec, + textures_delta: &egui::TexturesDelta, + pixels_per_point: f32, + ) { + if self.renderer.inner.caching == SoftwareRenderCaching::Direct { + self.renderer.render( + buffer_ref, + true, + paint_jobs, + textures_delta, + pixels_per_point, + ); + } else { + let redraw_everything_this_frame = + self.renderer.cached_size() != (buffer_ref.width, buffer_ref.height); + if redraw_everything_this_frame { + self.canvas.clear(); + let len = as_usize(buffer_ref.width) * as_usize(buffer_ref.height); + self.canvas.resize(len, [0; 4]); + // ^ data is now cleared in a singled memset call + } + let simd_impl = self.inner.simd_impl; + let mut canvas = + BufferMutRef::new(&mut self.canvas, buffer_ref.width, buffer_ref.height); + let dirty_rect = self.renderer.render( + &mut canvas, + redraw_everything_this_frame, + paint_jobs, + textures_delta, + pixels_per_point, + ); + if self.renderer.inner.caching == SoftwareRenderCaching::BlendTiled { + self.renderer + .inner + .blit_to_buffer_from_tiledcanvas(simd_impl, &canvas, buffer_ref); + } else { + dispatch_simd_impl!(simd_impl, |simd_impl| blit_rect( + simd_impl, &canvas, buffer_ref, dirty_rect, 0 + )); + } + } + } } impl EguiSoftwareRender { @@ -163,140 +277,289 @@ impl EguiSoftwareRender { /// output buffer order. pub fn new(output_field_order: ColorFieldOrder) -> Self { EguiSoftwareRender { - textures: Default::default(), - cached_primitives: Default::default(), - tiles_dim: Default::default(), - dirty_tiles: Default::default(), - target_size: Default::default(), - prims_updated_this_frame: Default::default(), - output_field_order, - canvas: Default::default(), - redraw_everything_this_frame: Default::default(), - convert_tris_to_rects: true, - allow_raster_opt: true, - cacheing_enabled: true, - simd_impl: Default::default(), - #[cfg(feature = "raster_stats")] - stats: Default::default(), + tiledcached_primitives: Default::default(), + dirtycached_primitives: Default::default(), + inner: EguiSoftwareRenderInner { + cached_size: (0, 0), + textures: Default::default(), + tiles_dim: Default::default(), + dirty_tiles: Default::default(), + dirty_rects: Default::default(), + output_field_order, + convert_tris_to_rects: true, + allow_raster_opt: true, + caching: SoftwareRenderCaching::BlendTiled, + simd_impl: Default::default(), + #[cfg(feature = "raster_stats")] + stats: Default::default(), + }, } } /// If true: attempts to optimize by converting suitable triangle pairs into rectangles for faster rendering. /// Things *should* look the same with this set to `true` while rendering faster. pub fn with_convert_tris_to_rects(mut self, set: bool) -> Self { - self.convert_tris_to_rects = set; + self.inner.convert_tris_to_rects = set; self } /// If false: Rasterize everything with triangles, always calculate vertex colors, uvs, use bilinear /// everywhere, etc... Things *should* look the same with this set to `true` while rendering faster. pub fn with_allow_raster_opt(mut self, set: bool) -> Self { - self.allow_raster_opt = set; + self.inner.allow_raster_opt = set; self } /// If true: rasterized ClippedPrimitives are cached and rendered to an intermediate tiled canvas. That canvas is /// then rendered over the frame buffer. If false ClippedPrimitives are rendered directly to the frame buffer. /// Rendering without caching is much slower and primarily intended for testing. - pub fn with_caching(mut self, set: bool) -> Self { - self.cacheing_enabled = set; + pub fn with_caching(mut self, set: SoftwareRenderCaching) -> Self { + self.inner.caching = set; self } + pub fn with_canvas(self) -> EguiSoftwareRenderCanvas { + EguiSoftwareRenderCanvas { + canvas: Vec::new(), + renderer: self, + } + } + + #[cfg(feature = "raster_stats")] + pub(crate) fn stats(&self) -> &RenderStats { + &self.inner.stats + } + + #[cfg(feature = "raster_stats")] + pub fn display_stats(&self, ui: &mut egui::Ui) { + self.inner.stats.render(ui); + } + + /// Get the caching mode of the renderer + pub fn caching(&self) -> SoftwareRenderCaching { + self.inner.caching + } + + /// Change the caching mode of the renderer + pub fn set_caching(&mut self, caching: SoftwareRenderCaching) { + if self.inner.caching == caching { + return; + } + self.inner.caching = caching; + self.clear_cache(); + } + + /// Clear cache and reclaim memory + /// + /// This will cause the next render to redraw everything + pub fn clear_cache(&mut self) { + self.tiledcached_primitives = Default::default(); + self.dirtycached_primitives = Default::default(); + self.inner.dirty_tiles = Default::default(); + self.inner.dirty_rects = Default::default(); + } + + /// The latest renderer `buffer_ref` width and height, if a cacheing mode is selected + pub const fn cached_size(&self) -> (u32, u32) { + self.inner.cached_size + } + /// Renders the given paint jobs to buffer_ref. Alternatively, when using caching /// EguiSoftwareRender::render_to_canvas() and subsequently EguiSoftwareRender::blit_canvas_to_buffer() can be run /// separately so that the primary rendering in render_to_canvas() can happen without a lock on the frame buffer. /// /// /// # Arguments + /// * `buffer_ref` - Buffer to render into. + /// * `redraw_everything_this_frame` - Redraw the whole buffer (ie. resize) + /// * `paint_jobs` - List of `egui::ClippedPrimitive` from egui to be rendered. /// * `paint_jobs` - List of `egui::ClippedPrimitive` from egui to be rendered. /// * `textures_delta` - The change in egui textures since last frame /// * `pixels_per_point` - The number of physical pixels for each logical point. + /// + /// # Returns + /// The smallest rect containing all updated pixels + /// + /// # Panics + /// * `buffer_ref` width or height non positive + /// * `pixels_per_point` non positive + /// * `buffer_ref` width or height must match `cached_size()` if `!redraw_everything_this_frame` pub fn render( &mut self, buffer_ref: &mut BufferMutRef, - paint_jobs: &[egui::ClippedPrimitive], + redraw_everything_this_frame: bool, + paint_jobs: Vec, textures_delta: &egui::TexturesDelta, pixels_per_point: f32, - ) { - if self.cacheing_enabled { - self.render_to_canvas( - buffer_ref.width, - buffer_ref.height, + ) -> DirtyRect { + #[cfg(feature = "raster_stats")] + self.inner.stats.clear(); + match self.inner.caching { + SoftwareRenderCaching::Direct => { + self.inner + .render_direct(buffer_ref, paint_jobs, textures_delta, pixels_per_point); + DirtyRect { + min_x: 0, + min_y: 0, + max_x: buffer_ref.width, + max_y: buffer_ref.height, + } + } + SoftwareRenderCaching::MeshTiled | SoftwareRenderCaching::Mesh => self + .render_meshmaybetiled( + buffer_ref, + redraw_everything_this_frame, + paint_jobs, + textures_delta, + pixels_per_point, + ), + SoftwareRenderCaching::BlendTiled => self.render_blendtiled( + buffer_ref, + redraw_everything_this_frame, paint_jobs, textures_delta, pixels_per_point, - ); - self.blit_canvas_to_buffer(buffer_ref); - } else { - self.render_direct(buffer_ref, paint_jobs, textures_delta, pixels_per_point); + ), } } - /// Renders the given paint jobs to an intermediate canvas. - /// - /// # Arguments - /// * `width` - The width of the output in pixels. Must match final output buffer dimensions. - /// * `height` - The height of the output in pixels. Must match final output buffer dimensions. - /// * `paint_jobs` - List of `egui::ClippedPrimitive` from egui to be rendered. - /// * `textures_delta` - The change in egui textures since last frame - /// * `pixels_per_point` - The number of physical pixels for each logical point. - pub fn render_to_canvas( + fn render_blendtiled( &mut self, - width: usize, - height: usize, - paint_jobs: &[egui::ClippedPrimitive], + canvas: &mut BufferMutRef, + redraw_everything_this_frame: bool, + paint_jobs: Vec, textures_delta: &egui::TexturesDelta, pixels_per_point: f32, - ) { + ) -> DirtyRect { // TODO: need to deal with user textures. Either make the fields of EguiUserTextures pub or need to come up with a replacement. - #[cfg(feature = "raster_stats")] - self.stats.clear(); + let dirty_rect = self.inner.prepare_render_cache( + &mut self.tiledcached_primitives, + canvas, + redraw_everything_this_frame, + paint_jobs, + textures_delta, + pixels_per_point, + EguiSoftwareRenderInner::render_prim, + EguiSoftwareRenderInner::update_dirty_tiles, + ); - assert!(width > 0); - assert!(height > 0); - assert!(pixels_per_point > 0.0); + if !dirty_rect.is_empty() { + self.inner + .render_from_tiledcache(&self.tiledcached_primitives, canvas); + } + dirty_rect + } + fn render_meshmaybetiled( + &mut self, + canvas: &mut BufferMutRef, + redraw_everything_this_frame: bool, + paint_jobs: Vec, + textures_delta: &egui::TexturesDelta, + pixels_per_point: f32, + ) -> DirtyRect { + let dirty_rect = self.inner.prepare_render_cache( + &mut self.dirtycached_primitives, + canvas, + redraw_everything_this_frame, + paint_jobs, + textures_delta, + pixels_per_point, + |_self, prim, _cropped_min, _cropped_max, clip_rect, px_mesh| MeshCachedPrimitive { + inner: prim, + px_mesh, + clip_rect, + }, + EguiSoftwareRenderInner::update_dirty_rects, + ); + if !dirty_rect.is_empty() { + self.inner + .render_from_meshcache(&self.dirtycached_primitives, canvas, dirty_rect); + } + dirty_rect + } +} + +impl EguiSoftwareRenderInner { + #[allow(clippy::too_many_arguments)] + fn prepare_render_cache( + &mut self, + cached_primitives: &mut HashMap, + canvas: &mut BufferMutRef, + redraw_everything_this_frame: bool, + paint_jobs: Vec, + textures_delta: &egui::TexturesDelta, + pixels_per_point: f32, + f_render_prims_to_cache: F, + f_update_dirty_tiles: U, + ) -> DirtyRect + where + F: Fn(&Self, CacheReuse, Vec2, Vec2, egui::Rect, Mesh) -> P + Sync + Send, + U: Fn(&mut Self, &HashMap), + P: DerefMut + Sync + Send, + { + // TODO: need to deal with user textures. Either make the fields of EguiUserTextures pub or need to come up with a replacement. - self.redraw_everything_this_frame = self.canvas.resize(width, height); + assert!(canvas.width > 0); + assert!(canvas.height > 0); + assert!(pixels_per_point > 0.0); - if self.redraw_everything_this_frame { - self.canvas.clear(); - self.cached_primitives.clear(); + if redraw_everything_this_frame { + cached_primitives.clear(); + } else { + assert_eq!(self.cached_size, (canvas.width, canvas.height)); } + self.cached_size = (canvas.width, canvas.height); - for (_hash, prim) in self.cached_primitives.iter_mut() { - prim.seen_this_frame = false; + for (_hash, prim) in cached_primitives.iter_mut() { + prim.deref_mut().seen_this_frame = false; } - self.target_size = vec2(width as f32, height as f32); - self.tiles_dim = [width.div_ceil(TILE_SIZE), height.div_ceil(TILE_SIZE)]; + self.tiles_dim = [ + canvas.width.div_ceil(TILE_SIZE), + canvas.height.div_ceil(TILE_SIZE), + ]; self.set_textures(textures_delta); - self.render_prims_to_cache(paint_jobs, pixels_per_point); - - self.update_dirty_tiles(); - self.clear_unused_cached_prims(); + self.render_prims_to_cache( + cached_primitives, + paint_jobs, + pixels_per_point, + f_render_prims_to_cache, + ); - let mut reinit_canvas = self.redraw_everything_this_frame; + let mut dirty_rect = self.update_dirty_rect(cached_primitives); - if self.prims_updated_this_frame > 0 { - // TODO use tiles - reinit_canvas = true; + if !dirty_rect.is_empty() { + f_update_dirty_tiles(self, cached_primitives); } - if reinit_canvas { - self.update_canvas_from_cached(); + // clear_unused_cached_prims + cached_primitives.retain(|_hash, prim| prim.deref().seen_this_frame); + + if redraw_everything_this_frame { + dirty_rect = DirtyRect { + min_x: 0, + min_y: 0, + max_x: canvas.width, + max_y: canvas.height, + }; } self.free_textures(textures_delta); + dirty_rect } /// Draw canvas alpha over given buffer. - /// Only run after EguiSoftwareRender::render_to_canvas(), or use EguiSoftwareRender::render() to run both. + /// Only run after EguiSoftwareRender::render() with TiledCacheing to run both. /// Only writes tile regions that contain pixels that are not fully transparent. - pub fn blit_canvas_to_buffer(&mut self, buffer: &mut BufferMutRef) { + fn blit_to_buffer_from_tiledcanvas( + &self, + simd_impl: AvailableImpl, + canvas: &BufferMutRef, + buffer: &mut BufferMutRef, + ) { #[cfg(feature = "raster_stats")] let start = std::time::Instant::now(); @@ -305,7 +568,7 @@ impl EguiSoftwareRender { // *pixel = egui_blend_u8(*src, *pixel); // }); - if self.canvas.data.is_empty() { + if canvas.data.is_empty() { #[cfg(feature = "log")] log::error!( "Canvas not initialized, call EguiSoftwareRender::blit_canvas_to_buffer() only after EguiSoftwareRender::render_to_canvas()" @@ -313,10 +576,10 @@ impl EguiSoftwareRender { return; } - let width = self.canvas.width; - let height = self.canvas.height; - assert_eq!(self.canvas.data.len(), width * height); - assert_eq!(buffer.data.len(), width * height); + let width = canvas.width; + let height = canvas.height; + assert_eq!(canvas.data.len(), as_usize(width * height)); + assert_eq!(buffer.data.len(), as_usize(width * height)); let tiles_x = self.tiles_dim[0]; @@ -329,21 +592,23 @@ impl EguiSoftwareRender { // blit rows of tiles in parallel let width = buffer.width; - let px_per_row_of_tiles = width * TILE_SIZE; + let px_per_row_of_tiles = as_usize(width) * as_usize(TILE_SIZE); buffer .data .par_chunks_mut(px_per_row_of_tiles) .enumerate() .for_each(|(tile_row, tile_height_row)| { - let height = tile_height_row.len() / width; // Might be less than TILE_SIZE + let tile_row = tile_row as u32; + let height = tile_height_row.len() as u32 / width; // Might be less than TILE_SIZE let buffer_tile_row = &mut BufferMutRef::new(tile_height_row, width, height); for (tile_idx, &mask) in self.dirty_tiles.iter().enumerate() { - if mask & Self::OCCUPIED_TILE_MASK == 0 { + if mask & EguiSoftwareRenderInner::OCCUPIED_TILE_MASK == 0 { continue; } + let tile_idx = tile_idx as u32; let tile_y = tile_idx / tiles_x; if tile_y != tile_row { continue; @@ -358,13 +623,16 @@ impl EguiSoftwareRender { let canvas_row_offset = tile_row * TILE_SIZE; - dispatch_simd_impl!(self.simd_impl, |simd_impl| self.blit_tile( + dispatch_simd_impl!(simd_impl, |simd_impl| blit_rect( simd_impl, + canvas, buffer_tile_row, - x_start, - y_start, - x_end, - y_end, + DirtyRect { + min_x: x_start, + min_y: y_start, + max_x: x_end, + max_y: y_end, + }, canvas_row_offset, )); } @@ -377,6 +645,7 @@ impl EguiSoftwareRender { continue; } + let tile_idx = tile_idx as u32; let tile_x = tile_idx % tiles_x; let tile_y = tile_idx / tiles_x; @@ -385,32 +654,24 @@ impl EguiSoftwareRender { let x_end = (x_start + TILE_SIZE).min(width); let y_end = (y_start + TILE_SIZE).min(height); - dispatch_simd_impl!(self.simd_impl, |simd_impl| self - .blit_tile(simd_impl, buffer, x_start, y_start, x_end, y_end, 0)); + dispatch_simd_impl!(simd_impl, |simd_impl| blit_rect( + simd_impl, + canvas, + buffer, + DirtyRect { + min_x: x_start, + min_y: y_start, + max_x: x_end, + max_y: y_end, + }, + 0, + )); } } #[cfg(feature = "raster_stats")] { - self.stats.blit_canvas_to_buffer = start.elapsed().as_secs_f32(); - } - } - - #[allow(clippy::too_many_arguments)] - fn blit_tile( - &self, - simd_impl: impl SelectedImpl, - buffer: &mut BufferMutRef, - x_start: usize, - y_start: usize, - x_end: usize, - y_end: usize, - canvas_row_offset: usize, - ) { - for y in y_start..y_end { - let src_row = self.canvas.get_span(x_start, x_end, y + canvas_row_offset); - let dst_row = &mut buffer.get_mut_span(x_start, x_end, y); - simd_impl.egui_blend_u8_slice(src_row, dst_row); + self.stats.blit_canvas_to_buffer.mark(start); } } @@ -418,52 +679,23 @@ impl EguiSoftwareRender { fn render_direct( &mut self, direct_draw_buffer: &mut BufferMutRef, - paint_jobs: &[egui::ClippedPrimitive], + paint_jobs: Vec, textures_delta: &egui::TexturesDelta, pixels_per_point: f32, ) { - #[cfg(feature = "raster_stats")] - self.stats.clear(); - self.set_textures(textures_delta); - self.target_size = vec2( - direct_draw_buffer.width as f32, - direct_draw_buffer.height as f32, - ); - #[cfg(feature = "raster_stats")] let start = std::time::Instant::now(); - for egui::ClippedPrimitive { - clip_rect, - primitive, - } in paint_jobs.iter() - { - let input_mesh = match primitive { - egui::epaint::Primitive::Mesh(input_mesh) => input_mesh, - egui::epaint::Primitive::Callback(_) => { - #[cfg(feature = "log")] - log::error!("egui::epaint::Primitive::Callback(PaintCallback) not supported"); - continue; - } - }; - - if input_mesh.vertices.is_empty() || input_mesh.indices.is_empty() { - continue; - } - - let clip_rect = egui::Rect { - min: clip_rect.min * pixels_per_point, - // TODO not sure why +1.5 is needed here. Occasionally things are cropped out without it. - max: clip_rect.max * pixels_per_point + egui::Vec2::splat(1.5), - }; - - let mut mesh_min = egui::Vec2::splat(f32::MAX); - let mut mesh_max = egui::Vec2::splat(-f32::MAX); - - let px_mesh = - self.prepare_px_mesh(pixels_per_point, input_mesh, &mut mesh_min, &mut mesh_max); + for paint_job in paint_jobs { + // TODO not sure why +1.5 is needed here. Occasionally things are cropped out without it. + let splat = 1.5f32; + let (clip_rect, mesh_min, mesh_max, px_mesh) = + match self.prim_prepare_px_mesh(splat, pixels_per_point, paint_job) { + Some(x) => x, + None => continue, + }; let mesh_size = mesh_max - mesh_min; if mesh_size.x > 8192.0 || mesh_size.y > 8192.0 { @@ -500,22 +732,195 @@ impl EguiSoftwareRender { ); } } - #[cfg(feature = "raster_stats")] { - self.stats.render_direct = start.elapsed().as_secs_f32(); + self.stats.render_direct.mark(start); } + self.free_textures(textures_delta); } - fn prepare_px_mesh( + fn render_prim( + &self, + prim: CacheReuse, + cropped_min: Vec2, + cropped_max: Vec2, + _clip_rect: egui::Rect, + px_mesh: Mesh, + ) -> TiledCachedPrimitive { + let (width, height) = (prim.rect.width(), prim.rect.height()); + let mut prim = TiledCachedPrimitive { + inner: prim, + buffer: vec![[0u8; 4]; as_usize(width) * as_usize(height)], + occupied_tiles: Vec::with_capacity(64), + }; + let mut buffer_ref = BufferMutRef { + data: &mut prim.buffer, + width, + height, + width_extent: width - 1, + height_extent: height - 1, + }; + + let clip_rect = egui::Rect { + min: Pos2::ZERO, + max: (cropped_max - cropped_min).to_pos2(), + }; + let offset = -vec2(cropped_min.x.floor(), cropped_min.y.floor()); + + let render_in_low_precision = width > 4096 || height > 4096; + if render_in_low_precision { + // Seems to not be an issue in direct draw? Seems like a bug. + draw_egui_mesh::<2>( + self.simd_impl, + &self.textures, + &mut buffer_ref, + &clip_rect, + &px_mesh, + offset, + self.allow_raster_opt, + self.convert_tris_to_rects, + #[cfg(all(feature = "raster_stats", not(feature = "rayon")))] + &self.stats, + ); + } else { + draw_egui_mesh::<8>( + self.simd_impl, + &self.textures, + &mut buffer_ref, + &clip_rect, + &px_mesh, + offset, + self.allow_raster_opt, + self.convert_tris_to_rects, + #[cfg(all(feature = "raster_stats", not(feature = "rayon")))] + &self.stats, + ); + } + prim.update_occupied_tiles(self.tiles_dim[0], self.tiles_dim[1]); + prim + } + + fn prim_prepare_update( + &self, + cached_primitives: &HashMap, + pixels_per_point: f32, + prim_idx: u32, + paint_job: egui::ClippedPrimitive, + f: F, + ) -> CacheUpdate

+ where + F: Fn(&Self, CacheReuse, Vec2, Vec2, egui::Rect, Mesh) -> P + Sync + Send, + P: DerefMut + Sync + Send, + { + let splat = 0.5f32; + let (clip_rect, mesh_min, mesh_max, px_mesh) = + match self.prim_prepare_px_mesh(splat, pixels_per_point, paint_job) { + Some(x) => x, + None => return CacheUpdate::None, + }; + + let cropped_min = mesh_min.max(clip_rect.min.to_vec2()); + let cropped_max = mesh_max.min(clip_rect.max.to_vec2()); + let cropped_size = (cropped_max - cropped_min).to_pos2(); + + let hash = { + let mut hasher = Hash32::new_fnv(); + + hasher.hash_wrap(cropped_size.x.to_bits()); + hasher.hash_wrap(cropped_size.y.to_bits()); + hasher.hash_wrap(match px_mesh.texture_id { + egui::TextureId::Managed(id) => id as u32, + egui::TextureId::User(id) => id as u32 + 9358476, + }); + for ind in &px_mesh.indices { + let v = px_mesh.vertices[*ind as usize]; + + // Tried to do this to avoid full redraws when moving a window but it was resulting in some + // meshes to be matches incorrectly in the ui gradient portion of the egui color test: + //let pos = v.pos - cropped_min; + + // It's much faster to not wrap for every field. General ordering should be sufficiently preserved. + hasher.hash(v.pos.x.to_bits()); + hasher.hash(v.pos.y.to_bits()); + hasher.hash(v.uv.x.to_bits()); + hasher.hash(v.uv.y.to_bits()); + hasher.hash(u32::from_le_bytes(v.color.to_array())); + hasher.fnv_wrap(); + } + hasher.hash_wrap(px_mesh.indices.len() as u32); + hasher.finalize() + }; + + let width = (cropped_max.x - cropped_min.x + 0.5) as u32; + let height = (cropped_max.y - cropped_min.y + 0.5) as u32; + let rect = DirtyRect { + min_x: cropped_min.x as u32, + min_y: cropped_min.y as u32, + max_x: cropped_min.x as u32 + width, + max_y: cropped_min.y as u32 + height, + }; + if cached_primitives.contains_key(&hash) { + CacheUpdate::CacheReuse( + hash, + CacheReuse { + z_order: prim_idx, + rect, + seen_this_frame: true, + rendered_this_frame: false, + }, + ) + } else { + if width > 8192 || height > 8192 { + // TODO it occasionally tries to make giant buffers in the first couple frames initially for some reason. + return CacheUpdate::None; + } + + if width == 0 || height == 0 { + return CacheUpdate::None; + } + + let prim = CacheReuse { + z_order: prim_idx, + rect, + seen_this_frame: true, + rendered_this_frame: true, + }; + CacheUpdate::New( + hash, + f(self, prim, cropped_min, cropped_max, clip_rect, px_mesh), + ) + } + } + + fn prim_prepare_px_mesh( &self, + splat: f32, pixels_per_point: f32, - mesh: &egui::Mesh, - mesh_min: &mut Vec2, - mesh_max: &mut Vec2, - ) -> Mesh { - let mut px_mesh = mesh.clone(); + egui::ClippedPrimitive { + clip_rect, + primitive, + }: egui::ClippedPrimitive, + ) -> Option<(egui::Rect, Vec2, Vec2, Mesh)> { + let input_mesh = match primitive { + egui::epaint::Primitive::Mesh(input_mesh) => input_mesh, + egui::epaint::Primitive::Callback(_) => { + #[cfg(feature = "log")] + log::error!("egui::epaint::Primitive::Callback(PaintCallback) not supported"); + return None; + } + }; + if input_mesh.vertices.is_empty() || input_mesh.indices.is_empty() { + return None; + } + let clip_rect = egui::Rect { + min: clip_rect.min * pixels_per_point, + max: clip_rect.max * pixels_per_point + egui::Vec2::splat(splat), + }; + let mut mesh_min = egui::Vec2::splat(f32::MAX); + let mut mesh_max = egui::Vec2::splat(-f32::MAX); + + let mut px_mesh = input_mesh; for v in px_mesh.vertices.iter_mut() { v.pos *= pixels_per_point; @@ -528,8 +933,8 @@ impl EguiSoftwareRender { } } - *mesh_min = mesh_min.min(v.pos.to_vec2()); - *mesh_max = mesh_max.max(v.pos.to_vec2()); + mesh_min = mesh_min.min(v.pos.to_vec2()); + mesh_max = mesh_max.max(v.pos.to_vec2()); } // Make all the tris face forward (ccw) to simplify rasterization. @@ -546,227 +951,138 @@ impl EguiSoftwareRender { px_mesh.indices.swap(i + 1, i + 2); } } - px_mesh + + Some((clip_rect, mesh_min, mesh_max, px_mesh)) } - fn render_prims_to_cache( - &mut self, - paint_jobs: &[egui::ClippedPrimitive], + fn render_prims_to_cache( + &self, + cached_primitives: &mut HashMap, + paint_jobs: Vec, pixels_per_point: f32, - ) { + f: F, + ) where + F: Fn(&Self, CacheReuse, Vec2, Vec2, egui::Rect, Mesh) -> P + Sync + Send, + P: DerefMut + Sync + Send, + { #[cfg(feature = "raster_stats")] let start = std::time::Instant::now(); - struct CacheReuse { - seen_this_frame: bool, - z_order: usize, - min_x: usize, - min_y: usize, - rendered_this_frame: bool, - hash: u32, - } - - enum CacheUpdate { - CacheReuse(CacheReuse), - New(u32, CachedPrimitive), - None, - } - // Render paint jobs in parallel #[cfg(feature = "rayon")] - use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; - #[cfg(feature = "rayon")] - let iter = paint_jobs.par_iter().enumerate(); + let iter = paint_jobs.into_par_iter().enumerate(); #[cfg(not(feature = "rayon"))] - let iter = paint_jobs.iter().enumerate(); - - let updates: Vec = iter - .map( - |( - prim_idx, - egui::ClippedPrimitive { - clip_rect, - primitive, - }, - )| { - let input_mesh = match primitive { - egui::epaint::Primitive::Mesh(input_mesh) => input_mesh, - egui::epaint::Primitive::Callback(_) => { - #[cfg(feature = "log")] - log::error!( - "egui::epaint::Primitive::Callback(PaintCallback) not supported" - ); - return CacheUpdate::None; - } - }; - - if input_mesh.vertices.is_empty() || input_mesh.indices.is_empty() { - return CacheUpdate::None; - } - - let clip_rect = egui::Rect { - min: clip_rect.min * pixels_per_point, - max: clip_rect.max * pixels_per_point + egui::Vec2::splat(0.5), - }; - - let mut mesh_min = egui::Vec2::splat(f32::MAX); - let mut mesh_max = egui::Vec2::splat(-f32::MAX); - - let px_mesh = self.prepare_px_mesh( - pixels_per_point, - input_mesh, - &mut mesh_min, - &mut mesh_max, - ); - - let cropped_min = mesh_min.max(clip_rect.min.to_vec2()); - let cropped_max = mesh_max.min(clip_rect.max.to_vec2()); - let clip_rect = egui::Rect { - min: Pos2::ZERO, - max: (cropped_max - cropped_min).to_pos2(), - }; - - let hash = { - let mut hasher = Hash32::new_fnv(); - - hasher.hash_wrap(clip_rect.min.x.to_bits()); - hasher.hash_wrap(clip_rect.min.y.to_bits()); - hasher.hash_wrap(clip_rect.max.x.to_bits()); - hasher.hash_wrap(clip_rect.max.y.to_bits()); - hasher.hash_wrap(match px_mesh.texture_id { - egui::TextureId::Managed(id) => id as u32, - egui::TextureId::User(id) => id as u32 + 9358476, - }); - for ind in &px_mesh.indices { - let v = px_mesh.vertices[*ind as usize]; - - // Tried to do this to avoid full redraws when moving a window but it was resulting in some - // meshes to be matches incorrectly in the ui gradient portion of the egui color test: - //let pos = v.pos - cropped_min; - - // It's much faster to not wrap for every field. General ordering should be sufficiently preserved. - hasher.hash(v.pos.x.to_bits()); - hasher.hash(v.pos.y.to_bits()); - hasher.hash(v.uv.x.to_bits()); - hasher.hash(v.uv.y.to_bits()); - hasher.hash(u32::from_le_bytes(v.color.to_array())); - hasher.fnv_wrap(); - } - hasher.hash_wrap(px_mesh.indices.len() as u32); - hasher.finalize() - }; - - if self.cached_primitives.contains_key(&hash) { - CacheUpdate::CacheReuse(CacheReuse { - hash, - seen_this_frame: true, - z_order: prim_idx, - min_x: cropped_min.x as usize, - min_y: cropped_min.y as usize, - rendered_this_frame: false, - }) - } else { - let width = (cropped_max.x - cropped_min.x + 0.5) as usize; - let height = (cropped_max.y - cropped_min.y + 0.5) as usize; - - if width > 8192 || height > 8192 { - // TODO it occasionally tries to make giant buffers in the first couple frames initially for some reason. - return CacheUpdate::None; - } - - if width == 0 || height == 0 { - return CacheUpdate::None; - } - - let render_in_low_precision = width > 4096 || height > 4096; - - let mut prim = CachedPrimitive::new( - cropped_min.x as usize, - cropped_min.y as usize, - width, - height, - prim_idx, - ); - let mut buffer_ref = BufferMutRef { - data: &mut prim.buffer, - width, - height, - width_extent: width - 1, - height_extent: height - 1, - }; - - let offset = -vec2(cropped_min.x.floor(), cropped_min.y.floor()); - - if render_in_low_precision { - // Seems to not be an issue in direct draw? Seems like a bug. - draw_egui_mesh::<2>( - self.simd_impl, - &self.textures, - &mut buffer_ref, - &clip_rect, - &px_mesh, - offset, - self.allow_raster_opt, - self.convert_tris_to_rects, - #[cfg(all(feature = "raster_stats", not(feature = "rayon")))] - &mut self.stats, - ); - } else { - draw_egui_mesh::<8>( - self.simd_impl, - &self.textures, - &mut buffer_ref, - &clip_rect, - &px_mesh, - offset, - self.allow_raster_opt, - self.convert_tris_to_rects, - #[cfg(all(feature = "raster_stats", not(feature = "rayon")))] - &mut self.stats, - ); - } - prim.update_occupied_tiles(self.tiles_dim[0], self.tiles_dim[1]); - CacheUpdate::New(hash, prim) - } - }, - ) + let iter = paint_jobs.into_iter().enumerate(); + + let updates: Vec> = iter + .map(|(prim_idx, paint_job)| { + self.prim_prepare_update( + cached_primitives, + pixels_per_point, + prim_idx as u32, + paint_job, + &f, + ) + }) .collect::>(); updates.into_iter().for_each(|update| match update { - CacheUpdate::CacheReuse(cache_reuse) => { - if let Some(cached_primitive) = self.cached_primitives.get_mut(&cache_reuse.hash) { - cached_primitive.seen_this_frame = cache_reuse.seen_this_frame; - cached_primitive.z_order = cache_reuse.z_order; - cached_primitive.min_x = cache_reuse.min_x; - cached_primitive.min_y = cache_reuse.min_y; - cached_primitive.rendered_this_frame = cache_reuse.rendered_this_frame; + CacheUpdate::CacheReuse(hash, cache_reuse) => { + if let Some(cached_primitive) = cached_primitives.get_mut(&hash) { + *cached_primitive.deref_mut() = cache_reuse; } } CacheUpdate::New(hash, prim) => { - self.prims_updated_this_frame += 1; - self.cached_primitives.insert(hash, prim); + cached_primitives.insert(hash, prim); } CacheUpdate::None => (), }); #[cfg(feature = "raster_stats")] { - self.stats.render_prims_to_cache = start.elapsed().as_secs_f32(); + self.stats.render_prims_to_cache.mark(start); } } - fn update_canvas_from_cached(&mut self) { - let simd_impl = self.simd_impl; + fn render_from_meshcache( + &self, + cached_primitives: &HashMap, + direct_draw_buffer: &mut BufferMutRef, + dirty_rect: DirtyRect, + ) { #[cfg(feature = "raster_stats")] let start = std::time::Instant::now(); - let mut sorted_prim_cache = self.cached_primitives.values().collect::>(); - sorted_prim_cache.sort_unstable_by_key(|prim| prim.z_order); + let mut sorted_prim_cache = cached_primitives.values().collect::>(); + sorted_prim_cache.sort_unstable_by_key(|prim| prim.inner.z_order); + + let mut render_from_meshcache_prim = |prim: &MeshCachedPrimitive, dirty_rect: DirtyRect| { + let clip_rect = prim.clip_rect.intersect(dirty_rect.to_egui_rect()); + let (width, height) = (prim.rect.width(), prim.rect.height()); + let render_in_low_precision = width > 4096 || height > 4096; + if render_in_low_precision { + draw_egui_mesh::<2>( + self.simd_impl, + &self.textures, + direct_draw_buffer, + &clip_rect, + &prim.px_mesh, + Vec2::ZERO, + self.allow_raster_opt, + self.convert_tris_to_rects, + #[cfg(all(feature = "raster_stats", not(feature = "rayon")))] + &self.stats, + ); + } else { + draw_egui_mesh::<8>( + self.simd_impl, + &self.textures, + direct_draw_buffer, + &clip_rect, + &prim.px_mesh, + Vec2::ZERO, + self.allow_raster_opt, + self.convert_tris_to_rects, + #[cfg(all(feature = "raster_stats", not(feature = "rayon")))] + &self.stats, + ); + } + }; - #[allow(unused_mut)] - let mut canvas = - BufferMutRef::new(&mut self.canvas.data, self.canvas.width, self.canvas.height); + match self.caching { + SoftwareRenderCaching::MeshTiled => { + for &prim in &sorted_prim_cache { + for dirty_rect in self.dirty_rects.intersections(prim.rect) { + render_from_meshcache_prim(prim, dirty_rect); + } + } + } + SoftwareRenderCaching::Mesh => { + for &prim in &sorted_prim_cache { + render_from_meshcache_prim(prim, dirty_rect); + } + } + _ => unreachable!(), + } + + #[cfg(feature = "raster_stats")] + { + self.stats.render_from_meshcache.mark(start); + } + } + + fn render_from_tiledcache( + &mut self, + cached_primitives: &HashMap, + canvas: &mut BufferMutRef, + ) { + let simd_impl = self.simd_impl; + #[cfg(feature = "raster_stats")] + let start = std::time::Instant::now(); + + let mut sorted_prim_cache = cached_primitives.values().collect::>(); + sorted_prim_cache.sort_unstable_by_key(|prim| prim.inner.z_order); #[cfg(feature = "rayon")] { @@ -776,22 +1092,23 @@ impl EguiSoftwareRender { }; // composite rows of tiles in parallel - let full_height = self.canvas.height; + let full_height = canvas.height; let width = canvas.width; - let px_per_row_of_tiles = width * TILE_SIZE; + let px_per_row_of_tiles = as_usize(width) * as_usize(TILE_SIZE); canvas .data .par_chunks_mut(px_per_row_of_tiles) .enumerate() .for_each(|(tile_row, tile_height_row)| { - let height = tile_height_row.len() / width; // Might be less than TILE_SIZE + let height = tile_height_row.len() as u32 / width; // Might be less than TILE_SIZE let canvas_tile_row = &mut BufferMutRef::new(tile_height_row, width, height); - let dirty_tile_row_start = tile_row * self.tiles_dim[0]; - let dirty_tile_row_end = dirty_tile_row_start + self.tiles_dim[0]; + let dirty_tile_row_start = tile_row * as_usize(self.tiles_dim[0]); + let dirty_tile_row_end = dirty_tile_row_start + as_usize(self.tiles_dim[0]); + let tile_row = tile_row as u32; self.dirty_tiles .iter() .enumerate() @@ -800,6 +1117,7 @@ impl EguiSoftwareRender { .filter(|(_, mask)| **mask & Self::DIRTY_TILE_MASK != 0) .map(|(idx, _)| idx) .for_each(|tile_idx| { + let tile_idx = tile_idx as u32; let tile_y = tile_idx / self.tiles_dim[0]; if tile_y != tile_row { @@ -831,13 +1149,14 @@ impl EguiSoftwareRender { .filter(|(_, mask)| **mask & Self::DIRTY_TILE_MASK != 0) .map(|(idx, _)| idx) { + let tile_idx = tile_idx as u32; let tile_x = tile_idx % self.tiles_dim[0]; let tile_y = tile_idx / self.tiles_dim[0]; let full_height = canvas.height; update_canvas_tile( simd_impl, &sorted_prim_cache, - &mut canvas, + canvas, tile_x, tile_y, full_height, @@ -845,44 +1164,87 @@ impl EguiSoftwareRender { ); } } + #[cfg(feature = "raster_stats")] { - self.stats.update_canvas_from_cached = start.elapsed().as_secs_f32(); + self.stats.render_from_tiledcache.mark(start); } } - fn clear_unused_cached_prims(&mut self) { - self.cached_primitives - .retain(|_hash, prim| prim.seen_this_frame); - } - const DIRTY_TILE_MASK: u8 = 0b00000001; const OCCUPIED_TILE_MASK: u8 = 0b000000010; - fn update_dirty_tiles(&mut self) { + fn update_dirty_tiles(&mut self, cached_primitives: &HashMap) { #[cfg(feature = "raster_stats")] let start = std::time::Instant::now(); + self.dirty_tiles - .resize(self.tiles_dim[0] * self.tiles_dim[1], 0); + .resize(as_usize(self.tiles_dim[0] * self.tiles_dim[1]), 0); self.dirty_tiles.fill(0); - for prim in self.cached_primitives.values() { + for prim in cached_primitives.values() { for tile in &prim.occupied_tiles { - let mask = - &mut self.dirty_tiles[tile[0] as usize + tile[1] as usize * self.tiles_dim[0]]; - if !prim.seen_this_frame || prim.rendered_this_frame { + let mask = &mut self.dirty_tiles + [tile[0] as usize + tile[1] as usize * self.tiles_dim[0] as usize]; + if !prim.inner.seen_this_frame || prim.inner.rendered_this_frame { *mask |= Self::DIRTY_TILE_MASK; } *mask |= Self::OCCUPIED_TILE_MASK; } } + + #[cfg(feature = "raster_stats")] + { + self.stats.update_dirty_tiles.mark(start); + } + } + + fn update_dirty_rects(&mut self, cached_primitives: &HashMap) { + #[cfg(feature = "raster_stats")] + let start = std::time::Instant::now(); + if self.caching == SoftwareRenderCaching::MeshTiled { + self.dirty_rects.set_bboxes( + cached_primitives + .values() + .filter(|prim| !prim.inner.seen_this_frame || prim.inner.rendered_this_frame) + .map(|prim| prim.rect), + ); + } + + #[cfg(feature = "raster_stats")] + { + self.stats.update_dirty_rects.mark(start); + } + } + + fn update_dirty_rect

(&mut self, cached_primitives: &HashMap) -> DirtyRect + where + P: Deref, + { + #[cfg(feature = "raster_stats")] + let start = std::time::Instant::now(); + + let mut dirty_rect = DirtyRect::new_empty(); + for prim in cached_primitives.values() { + let prim = prim.deref(); + if !prim.seen_this_frame || prim.rendered_this_frame { + if dirty_rect.is_empty() { + dirty_rect = prim.rect; + } else { + dirty_rect = dirty_rect.union(prim.rect) + } + } + } + #[cfg(feature = "raster_stats")] { - self.stats.update_dirty_tiles = start.elapsed().as_secs_f32(); + self.stats.update_dirty_rect.mark(start); } + dirty_rect } fn set_textures(&mut self, textures_delta: &egui::TexturesDelta) { #[cfg(feature = "raster_stats")] let start = std::time::Instant::now(); + for (id, delta) in &textures_delta.set { if delta.options.magnification != delta.options.minification { // Would need helper lanes to impl? @@ -920,9 +1282,10 @@ impl EguiSoftwareRender { self.textures.insert(*id, new_texture); } } + #[cfg(feature = "raster_stats")] { - self.stats.set_textures = start.elapsed().as_secs_f32(); + self.stats.set_textures.mark(start); } } @@ -935,12 +1298,12 @@ impl EguiSoftwareRender { fn update_canvas_tile( simd_impl: AvailableImpl, - sorted_prim_cache: &[&CachedPrimitive], + sorted_prim_cache: &[&TiledCachedPrimitive], canvas: &mut BufferMutRef, - tile_x: usize, - tile_y: usize, - full_height: usize, - canvas_row_offset: usize, + tile_x: u32, + tile_y: u32, + full_height: u32, + canvas_row_offset: u32, ) { let tile_x_start = tile_x * TILE_SIZE; let tile_y_start = tile_y * TILE_SIZE; @@ -952,7 +1315,7 @@ fn update_canvas_tile( let row_start = y * canvas.width; let start = row_start + tile_x_start; let end = row_start + tile_x_end; - canvas.data[start..end].fill([0; 4]); + canvas.data[as_usize(start)..as_usize(end)].fill([0; 4]); } let tile_n = [tile_x as u16, tile_y as u16]; @@ -962,10 +1325,10 @@ fn update_canvas_tile( continue; } - let mut min_x = prim.min_x; - let mut min_y = prim.min_y; - let mut max_x = min_x + prim.width; - let mut max_y = min_y + prim.height; + let mut min_x = prim.inner.rect.min_x; + let mut min_y = prim.inner.rect.min_y; + let mut max_x = prim.inner.rect.max_x; + let mut max_y = prim.inner.rect.max_y; min_x = min_x.max(tile_x_start).min(canvas.width); min_y = min_y @@ -979,20 +1342,23 @@ fn update_canvas_tile( if max_x <= min_x || max_y <= min_y { continue; } - let prim_x_min = (min_x - prim.min_x).min(prim_buf.width); - let prim_x_max = (max_x - prim.min_x).min(prim_buf.width); + let prim_x_min = (min_x - prim.inner.rect.min_x).min(prim_buf.width); + let prim_x_max = (max_x - prim.inner.rect.min_x).min(prim_buf.width); - let get_ranges = |y: usize| -> (Range, Range) { + let get_ranges = |y: u32| -> (Range, Range) { let canvas_row_start = (y - canvas_row_offset).min(canvas.height) * canvas.width; let canvas_start = canvas_row_start + min_x; let canvas_end = canvas_row_start + max_x; - let prim_y = (y - prim.min_y).min(prim_buf.height); + let prim_y = (y - prim.inner.rect.min_y).min(prim_buf.height); let prim_row_start = prim_y * prim_buf.width; let prim_start = prim_row_start + prim_x_min; let prim_end = prim_row_start + prim_x_max; - (canvas_start..canvas_end, prim_start..prim_end) + ( + as_usize(canvas_start)..as_usize(canvas_end), + as_usize(prim_start)..as_usize(prim_end), + ) }; dispatch_simd_impl!(simd_impl, |simd_impl| { @@ -1006,113 +1372,102 @@ fn update_canvas_tile( } } -#[derive(Default)] -struct Canvas { - data: Vec<[u8; 4]>, - width: usize, - height: usize, - width_extent: usize, - height_extent: usize, +enum CacheUpdate

{ + CacheReuse(u32, CacheReuse), + New(u32, P), + None, } -impl Canvas { - fn clear(&mut self) { - self.data.iter_mut().for_each(|p| *p = [0; 4]); - } +/// Common fields to both cached rendering modes +struct CacheReuse { + z_order: u32, + rect: DirtyRect, + seen_this_frame: bool, + rendered_this_frame: bool, +} - /// returns true if wasn't already the given size - fn resize(&mut self, width: usize, height: usize) -> bool { - if width != self.width || height != self.height { - self.data.resize(width * height, [0; 4]); - self.width = width; - self.height = height; - self.width_extent = width - 1; - self.height_extent = height - 1; - true - } else { - false - } - } +/// A region of cached mesh data that corresponds to a ClippedPrimitive. +struct MeshCachedPrimitive { + inner: CacheReuse, + px_mesh: Mesh, + clip_rect: egui::Rect, +} - #[inline(always)] - pub fn get_range(&self, start: usize, end: usize, y: usize) -> Range { - let row_start = y * self.width; - let start = row_start + start; - let end = row_start + end; - start..end - } +impl Deref for MeshCachedPrimitive { + type Target = CacheReuse; - #[inline(always)] - pub fn get_span(&self, start: usize, end: usize, y: usize) -> &[[u8; 4]] { - let range = self.get_range(start, end, y); - &self.data[range] + #[inline] + fn deref(&self) -> &Self::Target { + &self.inner } } +impl DerefMut for MeshCachedPrimitive { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} /// A region of cached rendered image data that corresponds to a ClippedPrimitive. -pub struct CachedPrimitive { +struct TiledCachedPrimitive { + inner: CacheReuse, buffer: Vec<[u8; 4]>, - min_x: usize, - min_y: usize, - width: usize, - height: usize, - z_order: usize, - seen_this_frame: bool, - rendered_this_frame: bool, occupied_tiles: Vec<[u16; 2]>, } +impl Deref for TiledCachedPrimitive { + type Target = CacheReuse; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.inner + } +} -impl CachedPrimitive { +impl DerefMut for TiledCachedPrimitive { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl TiledCachedPrimitive { fn get_buffer_ref(&self) -> BufferRef<'_> { BufferRef { data: &self.buffer, - width: self.width, - height: self.height, - width_extent: self.width - 1, - height_extent: self.height - 1, + width: self.inner.rect.width(), + height: self.inner.rect.height(), + width_extent: self.inner.rect.width() - 1, + height_extent: self.inner.rect.height() - 1, } } - - fn new(min_x: usize, min_y: usize, width: usize, height: usize, z_order: usize) -> Self { - CachedPrimitive { - buffer: vec![[0; 4]; width * height], - min_x, - min_y, - width, - height, - z_order, - seen_this_frame: true, - rendered_this_frame: true, - occupied_tiles: Vec::with_capacity(64), - } - } - - fn update_occupied_tiles(&mut self, tiles_wide: usize, tiles_tall: usize) { + fn update_occupied_tiles(&mut self, tiles_wide: u32, tiles_tall: u32) { // list which tiles contain a pixel with that isn't fully transparent (also containing not color info) self.occupied_tiles.clear(); - let max_x = self.min_x + self.width; - let max_y = self.min_y + self.height; - let first_tile_x = (self.min_x / TILE_SIZE).min(tiles_wide); - let first_tile_y = (self.min_y / TILE_SIZE).min(tiles_tall); + let width = self.inner.rect.width(); + let max_x = self.inner.rect.max_x; + let max_y = self.inner.rect.max_y; + let first_tile_x = (self.inner.rect.min_x / TILE_SIZE).min(tiles_wide); + let first_tile_y = (self.inner.rect.min_y / TILE_SIZE).min(tiles_tall); let last_tile_x = max_x.div_ceil(TILE_SIZE).min(tiles_wide); let last_tile_y = max_y.div_ceil(TILE_SIZE).min(tiles_tall); for tile_y in first_tile_y..last_tile_y { - let mut px_start_y = (tile_y * TILE_SIZE).max(self.min_y); + let mut px_start_y = (tile_y * TILE_SIZE).max(self.inner.rect.min_y); let mut px_end_y = (px_start_y + TILE_SIZE).min(max_y); - px_start_y -= self.min_y; - px_end_y -= self.min_y; + px_start_y -= self.inner.rect.min_y; + px_end_y -= self.inner.rect.min_y; for tile_x in first_tile_x..last_tile_x { - let mut px_start_x = (tile_x * TILE_SIZE).max(self.min_x); + let mut px_start_x = (tile_x * TILE_SIZE).max(self.inner.rect.min_x); let mut px_end_x = (px_start_x + TILE_SIZE).min(max_x); - px_start_x -= self.min_x; - px_end_x -= self.min_x; + px_start_x -= self.inner.rect.min_x; + px_end_x -= self.inner.rect.min_x; 'px_outer: for y in px_start_y..px_end_y { for x in px_start_x..px_end_x { // Purposefully panicing when out of bounds. If it's out of bounds then the math is wrong and // the tile is not being calculated correctly. - if u32::from_le_bytes(self.buffer[x + y * self.width]) > 0 { + let offset = as_usize(x) + as_usize(y) * as_usize(width); + if u32::from_le_bytes(self.buffer[offset]) > 0 { self.occupied_tiles.push([tile_x as u16, tile_y as u16]); break 'px_outer; } @@ -1127,14 +1482,14 @@ impl CachedPrimitive { #[derive(Debug)] pub struct BufferMutRef<'a> { pub data: &'a mut [[u8; 4]], - pub width: usize, - pub height: usize, - pub width_extent: usize, - pub height_extent: usize, + pub width: u32, + pub height: u32, + pub width_extent: u32, + pub height_extent: u32, } impl<'a> BufferMutRef<'a> { - pub fn new(data: &'a mut [[u8; 4]], width: usize, height: usize) -> Self { + pub fn new(data: &'a mut [[u8; 4]], width: u32, height: u32) -> Self { assert!(width > 0); assert!(height > 0); BufferMutRef { @@ -1147,29 +1502,35 @@ impl<'a> BufferMutRef<'a> { } #[inline(always)] - pub fn get_range(&self, start: usize, end: usize, y: usize) -> Range { + pub fn get_range(&self, start: u32, end: u32, y: u32) -> Range { let row_start = y * self.width; - let start = row_start + start; - let end = row_start + end; + let start = as_usize(row_start + start); + let end = as_usize(row_start + end); start..end } #[inline(always)] - pub fn get_mut_span(&mut self, start: usize, end: usize, y: usize) -> &mut [[u8; 4]] { + pub fn get_span(&self, start: u32, end: u32, y: u32) -> &[[u8; 4]] { + let range = self.get_range(start, end, y); + &self.data[range] + } + + #[inline(always)] + pub fn get_mut_span(&mut self, start: u32, end: u32, y: u32) -> &mut [[u8; 4]] { let range = self.get_range(start, end, y); &mut self.data[range] } #[inline(always)] - pub fn get_mut_clamped(&mut self, x: usize, y: usize) -> &mut [u8; 4] { + pub fn get_mut_clamped(&mut self, x: u32, y: u32) -> &mut [u8; 4] { let x = x.min(self.width_extent); let y = y.min(self.height_extent); - &mut self.data[x + y * self.width] + &mut self.data[as_usize(x) + as_usize(y) * as_usize(self.width)] } #[inline(always)] - pub fn get_mut(&mut self, x: usize, y: usize) -> &mut [u8; 4] { - &mut self.data[x + y * self.width] + pub fn get_mut(&mut self, x: u32, y: u32) -> &mut [u8; 4] { + &mut self.data[as_usize(x) + as_usize(y) * as_usize(self.width)] } } @@ -1177,22 +1538,67 @@ impl<'a> BufferMutRef<'a> { #[derive(Debug)] pub struct BufferRef<'a> { pub data: &'a [[u8; 4]], - pub width: usize, - pub height: usize, - pub width_extent: usize, - pub height_extent: usize, + pub width: u32, + pub height: u32, + pub width_extent: u32, + pub height_extent: u32, +} + +/// Lossless cast to usize +/// Prevent compilation on < 32bits platforms +#[cfg(any(target_pointer_width = "32", target_pointer_width = "64"))] +#[inline(always)] +fn as_usize(v: u32) -> usize { + v as usize } impl<'a> BufferRef<'a> { #[inline(always)] - pub fn get_ref_clamped(&self, x: usize, y: usize) -> &[u8; 4] { + pub fn get_ref_clamped(&self, x: u32, y: u32) -> &[u8; 4] { let x = x.min(self.width_extent); let y = y.min(self.height_extent); - &self.data[x + y * self.width] + &self.data[as_usize(x) + as_usize(y) * as_usize(self.width)] } #[inline(always)] - pub fn get_ref(&self, x: usize, y: usize) -> &[u8; 4] { - &self.data[x + y * self.width] + pub fn get_ref(&self, x: u32, y: u32) -> &[u8; 4] { + &self.data[as_usize(x) + as_usize(y) * as_usize(self.width)] + } +} + +#[allow(dead_code)] +fn draw_rect_border_f32( + buffer_ref: &mut BufferMutRef, + rect: egui::Rect, + border_size: f32, + color: [u8; 4], +) { + // Convert float to integer pixel coordinates + let x0 = rect.min.x.floor().max(0.0) as u32; + let y0 = rect.min.y.floor().max(0.0) as u32; + let x1 = (rect.max.x.ceil() as u32).min(buffer_ref.width); + let y1 = (rect.max.y.ceil() as u32).min(buffer_ref.height); + let border = border_size.ceil().max(0.0) as u32; + + // Helper closure: set pixel if inside buffer + let mut set_pixel = |px: u32, py: u32| { + let idx = as_usize(py * buffer_ref.width + px); + buffer_ref.data[idx] = color; + }; + + // Top & bottom borders + for dy in 0..border { + for px in x0..x1 { + set_pixel(px, y0 + dy); // top + set_pixel(px, y1.saturating_sub(1) - dy); // bottom + } + } + + // Left & right borders + for py in border..(y1.saturating_sub(y0).saturating_sub(border)) { + for dx in 0..border { + set_pixel(x0 + dx, y0 + py); // left + set_pixel(x1.saturating_sub(1) - dx, y0 + py); // right + } } } diff --git a/src/raster/rect.rs b/src/raster/rect.rs index 38220f8..028963c 100644 --- a/src/raster/rect.rs +++ b/src/raster/rect.rs @@ -1,7 +1,7 @@ use constify::constify; use egui::{Vec2, vec2}; -use crate::{BufferMutRef, color::SelectedImpl, egui_texture::EguiTexture, render::DrawInfo}; +use crate::{BufferMutRef, SelectedImpl, as_usize, egui_texture::EguiTexture, render::DrawInfo}; #[constify] pub fn draw_rect( @@ -44,10 +44,10 @@ fn draw_rect_impl( vert_offset: Vec2, allow_raster_opt: bool, convert_tris_to_rects: bool, - #[cfg(all(feature = "raster_stats", not(feature = "rayon")))] - stats: &mut crate::stats::RasterStats, + #[cfg(all(feature = "raster_stats", not(feature = "rayon")))] stats: &crate::stats::RenderStats, ) { crate::dispatch_simd_impl!(simd_impl, |simd_impl| draw_egui_mesh_impl::( simd_impl, @@ -48,8 +47,7 @@ fn draw_egui_mesh_impl( vert_offset: Vec2, allow_raster_opt: bool, convert_tris_to_rects: bool, - #[cfg(all(feature = "raster_stats", not(feature = "rayon")))] - stats: &mut crate::stats::RasterStats, + #[cfg(all(feature = "raster_stats", not(feature = "rayon")))] stats: &crate::stats::RenderStats, ) { if mesh.vertices.is_empty() || mesh.indices.is_empty() { return; @@ -215,7 +213,7 @@ fn draw_egui_mesh_impl( let rect = found_rect && !vert_col_vary; // vert_col_vary not supported by rect render #[cfg(all(feature = "raster_stats", not(feature = "rayon")))] - stats.start_raster(); + let mut stats_start = stats.start_raster(); if rect { draw_rect( simd_impl, @@ -228,7 +226,7 @@ fn draw_egui_mesh_impl( ); #[cfg(all(feature = "raster_stats", not(feature = "rayon")))] - stats.finish_rect(fsize, vert_uvs_vary, vert_col_vary, alpha_blend); + stats_start.finish_rect(fsize, vert_uvs_vary, vert_col_vary, alpha_blend); i += 6; } else { draw_tri::( @@ -242,7 +240,7 @@ fn draw_egui_mesh_impl( ); #[cfg(all(feature = "raster_stats", not(feature = "rayon")))] - stats.finish_tri(fsize, vert_uvs_vary, vert_col_vary, alpha_blend); + stats_start.finish_tri(fsize, vert_uvs_vary, vert_col_vary, alpha_blend); i += 3; } } diff --git a/src/stats.rs b/src/stats.rs index e311f0a..8a761d9 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -1,96 +1,91 @@ use crate::alloc::string::ToString; use alloc::format; use alloc::vec::Vec; +use core::sync::atomic::{self}; use egui::ahash::HashMap; +use egui::mutex::Mutex; use std::time::Instant; #[allow(unused_imports)] use egui::{Ui, Vec2, Vec2b}; #[derive(Clone, Copy)] -pub struct Stat { +pub(crate) struct Stat { pub count: u32, pub time: f32, pub sum_area: f32, } -pub struct RasterStats { - pub tri_width_buckets: HashMap, // Key is tri width - pub tri_height_buckets: HashMap, // Key is tri height - pub rect_width_buckets: HashMap, // Key is rect width - pub rect_height_buckets: HashMap, // Key is rect height - pub tri_vert_col_vary: u32, // Count of tris where the vertex colors varied - pub tri_vert_uvs_vary: u32, // Count of tris where the vertex uvs varied - pub tri_alpha_blend: u32, // Count of tris that required alpha blending - pub rect_vert_col_vary: u32, // Count of rects where the vertex colors varied - pub rect_vert_uvs_vary: u32, // Count of rects where the vertex uvs varied - pub rect_alpha_blend: u32, // Count of rects that required alpha blending - pub tris: u32, // Total tris drawn - pub rects: u32, // Total rects drawn - pub start: Instant, // Time just before latest rasterization - pub set_textures: f32, - pub update_dirty_tiles: f32, - pub update_canvas_from_cached: f32, - pub render_prims_to_cache: f32, - pub render_direct: f32, - pub blit_canvas_to_buffer: f32, +#[derive(Default)] +pub(crate) struct DurationStat { + elapsed_secs: atomic::AtomicU32, // f32 } -impl Default for RasterStats { - fn default() -> Self { - Self { - tri_width_buckets: Default::default(), - tri_height_buckets: Default::default(), - tri_vert_col_vary: Default::default(), - tri_vert_uvs_vary: Default::default(), - tri_alpha_blend: Default::default(), - rect_width_buckets: Default::default(), - rect_height_buckets: Default::default(), - rect_vert_col_vary: Default::default(), - rect_vert_uvs_vary: Default::default(), - rect_alpha_blend: Default::default(), - rects: Default::default(), - tris: Default::default(), - set_textures: Default::default(), - update_dirty_tiles: Default::default(), - update_canvas_from_cached: Default::default(), - render_prims_to_cache: Default::default(), - render_direct: Default::default(), - blit_canvas_to_buffer: Default::default(), - start: Instant::now(), - } +impl DurationStat { + pub(crate) fn mark(&self, start: Instant) { + let secs = start.elapsed().as_secs_f32(); + let secs: u32 = secs.to_bits(); + self.elapsed_secs.store(secs, atomic::Ordering::Relaxed); } -} -#[cfg(not(feature = "rayon"))] -fn insert_or_increment(long_side_size: u32, elapsed: f32, area: f32, map: &mut HashMap) { - if let Some(stat) = map.get_mut(&long_side_size) { - stat.count += 1; - stat.time += elapsed; - stat.sum_area += area; - } else { - map.insert( - long_side_size, - Stat { - count: 1, - time: elapsed, - sum_area: area, - }, - ); + pub fn elapsed_secs(&self) -> f32 { + let secs: u32 = self.elapsed_secs.load(atomic::Ordering::Relaxed); + f32::from_bits(secs) } } -impl RasterStats { - pub(crate) fn clear(&mut self) { - *self = RasterStats::default(); - } +#[derive(Default)] +pub(crate) struct RasterStats { + /// Key is tri width + pub tri_width_buckets: HashMap, + /// Key is tri height + pub tri_height_buckets: HashMap, + /// Key is rect width + pub rect_width_buckets: HashMap, + /// Key is rect height + pub rect_height_buckets: HashMap, + /// Count of tris where the vertex colors varied + pub tri_vert_col_vary: u32, + /// Count of tris where the vertex uvs varied + pub tri_vert_uvs_vary: u32, + /// Count of tris that required alpha blending + pub tri_alpha_blend: u32, + /// Count of rects where the vertex colors varied + pub rect_vert_col_vary: u32, + /// Count of rects where the vertex uvs varied + pub rect_vert_uvs_vary: u32, + /// Count of rects that required alpha blending + pub rect_alpha_blend: u32, + /// Total tris drawn + pub tris: u32, + /// Total rects drawn + pub rects: u32, +} - #[cfg(not(feature = "rayon"))] - pub(crate) fn start_raster(&mut self) { - self.start = Instant::now(); - } +#[derive(Default)] +pub(crate) struct RenderStats { + pub raster: Mutex, + pub set_textures: DurationStat, + pub render_prims_to_cache: DurationStat, + pub update_dirty_rect: DurationStat, + pub update_dirty_tiles: DurationStat, + pub update_dirty_rects: DurationStat, + pub render_from_meshcache: DurationStat, + pub render_from_tiledcache: DurationStat, + pub render_direct: DurationStat, + pub blit_canvas_to_buffer: DurationStat, + #[cfg(feature = "winit")] + pub winit_present: DurationStat, +} - #[cfg(not(feature = "rayon"))] +#[cfg(not(feature = "rayon"))] +pub(crate) struct RasterStatsStarted<'a> { + start: Instant, + stats: egui::mutex::MutexGuard<'a, RasterStats>, +} + +#[cfg(not(feature = "rayon"))] +impl<'a> RasterStatsStarted<'a> { pub(crate) fn finish_rect( &mut self, fsize: Vec2, @@ -99,26 +94,25 @@ impl RasterStats { alpha_blend: bool, ) { let elapsed = self.start.elapsed().as_secs_f32(); - self.rects += 1; + self.stats.rects += 1; let tri_area = (fsize.x * fsize.y) * 0.5; - insert_or_increment( + Self::insert_or_increment( (fsize.x as u32).max(1), elapsed, tri_area, - &mut self.rect_width_buckets, + &mut self.stats.rect_width_buckets, ); - insert_or_increment( + Self::insert_or_increment( (fsize.y as u32).max(1), elapsed, tri_area, - &mut self.rect_height_buckets, + &mut self.stats.rect_height_buckets, ); - self.rect_vert_col_vary += vert_col_vary as u32; - self.rect_vert_uvs_vary += vert_uvs_vary as u32; - self.rect_alpha_blend += alpha_blend as u32; + self.stats.rect_vert_col_vary += vert_col_vary as u32; + self.stats.rect_vert_uvs_vary += vert_uvs_vary as u32; + self.stats.rect_alpha_blend += alpha_blend as u32; } - #[cfg(not(feature = "rayon"))] pub(crate) fn finish_tri( &mut self, fsize: Vec2, @@ -127,23 +121,59 @@ impl RasterStats { alpha_blend: bool, ) { let elapsed = self.start.elapsed().as_secs_f32(); - self.tris += 1; + self.stats.tris += 1; let rect_area = fsize.x * fsize.y; - insert_or_increment( + Self::insert_or_increment( (fsize.x as u32).max(1), elapsed, rect_area, - &mut self.tri_width_buckets, + &mut self.stats.tri_width_buckets, ); - insert_or_increment( + Self::insert_or_increment( (fsize.y as u32).max(1), elapsed, rect_area, - &mut self.tri_height_buckets, + &mut self.stats.tri_height_buckets, ); - self.tri_vert_col_vary += vert_col_vary as u32; - self.tri_vert_uvs_vary += vert_uvs_vary as u32; - self.tri_alpha_blend += alpha_blend as u32; + self.stats.tri_vert_col_vary += vert_col_vary as u32; + self.stats.tri_vert_uvs_vary += vert_uvs_vary as u32; + self.stats.tri_alpha_blend += alpha_blend as u32; + } + + fn insert_or_increment( + long_side_size: u32, + elapsed: f32, + area: f32, + map: &mut HashMap, + ) { + if let Some(stat) = map.get_mut(&long_side_size) { + stat.count += 1; + stat.time += elapsed; + stat.sum_area += area; + } else { + map.insert( + long_side_size, + Stat { + count: 1, + time: elapsed, + sum_area: area, + }, + ); + } + } +} + +impl RenderStats { + pub(crate) fn clear(&mut self) { + *self = RenderStats::default(); + } + + #[cfg(not(feature = "rayon"))] + pub(crate) fn start_raster(&self) -> RasterStatsStarted<'_> { + RasterStatsStarted { + start: Instant::now(), + stats: self.raster.lock(), + } } pub fn render(&self, ui: &mut Ui) { @@ -151,18 +181,24 @@ impl RasterStats { .auto_shrink(Vec2b::new(false, false)) .min_scrolled_width(900.0) .show(ui, |ui| { + let raster = self.raster.lock(); egui::Grid::new("stats_grid").striped(true).show(ui, |ui| { - let mut stat = |label: &str, val: f32| { + let mut stat = |label: &str, val: &DurationStat| { ui.label(label); - ui.label(format!("{:.2}ms", val * 1000.0)); + ui.label(format!("{:.3}ms", val.elapsed_secs() * 1000.0)); ui.end_row(); }; - stat("set_textures", self.set_textures); - stat("render_prims_to_cache", self.render_prims_to_cache); - stat("update_dirty_tiles", self.update_dirty_tiles); - stat("update_canvas_from_cached", self.update_canvas_from_cached); - stat("blit_canvas_to_buffer", self.blit_canvas_to_buffer); - stat("render_direct", self.render_direct); + stat("set_textures", &self.set_textures); + stat("render_prims_to_cache", &self.render_prims_to_cache); + stat("update_dirty_rect", &self.update_dirty_rect); + stat("update_dirty_tiles", &self.update_dirty_tiles); + stat("update_dirty_rects", &self.update_dirty_rects); + stat("render_from_tiledcache", &self.render_from_tiledcache); + stat("render_from_meshcache", &self.render_from_meshcache); + stat("render_direct", &self.render_direct); + stat("blit_canvas_to_buffer", &self.blit_canvas_to_buffer); + #[cfg(feature = "winit")] + stat("winit_present", &self.winit_present); ui.heading(""); ui.heading("Tri"); @@ -176,18 +212,18 @@ impl RasterStats { }; stat( "Vertex colors vary", - self.tri_vert_col_vary, - self.rect_vert_col_vary, + raster.tri_vert_col_vary, + raster.rect_vert_col_vary, ); stat( "Vertex uvs vary", - self.tri_vert_uvs_vary, - self.rect_vert_uvs_vary, + raster.tri_vert_uvs_vary, + raster.rect_vert_uvs_vary, ); stat( "Requires alpha blend", - self.tri_alpha_blend, - self.rect_alpha_blend, + raster.tri_alpha_blend, + raster.rect_alpha_blend, ); }); @@ -200,10 +236,10 @@ impl RasterStats { v } - let tri_width_bucket = collect_and_sort(&self.tri_width_buckets); - let tri_height_bucket = collect_and_sort(&self.tri_height_buckets); - let rect_width_bucket = collect_and_sort(&self.rect_width_buckets); - let rect_height_bucket = collect_and_sort(&self.rect_height_buckets); + let tri_width_bucket = collect_and_sort(&raster.tri_width_buckets); + let tri_height_bucket = collect_and_sort(&raster.tri_height_buckets); + let rect_width_bucket = collect_and_sort(&raster.rect_width_buckets); + let rect_height_bucket = collect_and_sort(&raster.rect_height_buckets); let max_rows = tri_width_bucket .len() @@ -213,11 +249,11 @@ impl RasterStats { egui::Grid::new("stats_grid2").striped(true).show(ui, |ui| { ui.heading("Tris"); - ui.heading(format!("{}", self.tris)); + ui.heading(format!("{}", raster.tris)); (0..=5).for_each(|_| _ = ui.heading("")); ui.heading(" "); ui.heading("Rects"); - ui.heading(format!("{}", self.rects)); + ui.heading(format!("{}", raster.rects)); (0..=5).for_each(|_| _ = ui.heading("")); ui.end_row(); diff --git a/src/test_render.rs b/src/test_render.rs index b154ee0..13625ac 100644 --- a/src/test_render.rs +++ b/src/test_render.rs @@ -3,12 +3,12 @@ use egui::TexturesDelta; use egui_kittest::TestRenderer; use image::ImageBuffer; -use crate::{BufferMutRef, EguiSoftwareRender}; +use crate::{BufferMutRef, EguiSoftwareRenderCanvas}; -impl TestRenderer for EguiSoftwareRender { +impl TestRenderer for EguiSoftwareRenderCanvas { fn handle_delta(&mut self, delta: &TexturesDelta) { - self.set_textures(delta); - self.free_textures(delta); + self.renderer.inner.set_textures(delta); + self.renderer.inner.free_textures(delta); } fn render( @@ -18,23 +18,23 @@ impl TestRenderer for EguiSoftwareRender { ) -> Result { let paint_jobs = ctx.tessellate(output.shapes.clone(), output.pixels_per_point); - let width = (ctx.content_rect().width() * output.pixels_per_point) as usize; - let height = (ctx.content_rect().height() * output.pixels_per_point) as usize; + let width = (ctx.content_rect().width() * output.pixels_per_point) as u32; + let height = (ctx.content_rect().height() * output.pixels_per_point) as u32; - let mut buffer = vec![[0u8; 4]; width * height]; + let mut buffer = vec![[0u8; 4]; crate::as_usize(width * height)]; - let mut buffer_ref = BufferMutRef::new(&mut buffer, width as usize, height as usize); + let mut buffer_ref = BufferMutRef::new(&mut buffer, width, height); self.render( &mut buffer_ref, - &paint_jobs, + paint_jobs, &output.textures_delta, output.pixels_per_point, ); Ok(ImageBuffer::, Vec<_>>::from_raw( - width as u32, - height as u32, + width, + height, buffer.iter().flatten().cloned().collect::>(), ) .unwrap()) diff --git a/src/winit.rs b/src/winit.rs index 247e6e2..429974c 100644 --- a/src/winit.rs +++ b/src/winit.rs @@ -1,4 +1,4 @@ -use crate::{BufferMutRef, ColorFieldOrder, EguiSoftwareRender}; +use crate::{BufferMutRef, ColorFieldOrder, EguiSoftwareRender, SoftwareRenderCaching}; use egui::{ Context, CursorGrab, IconData, Pos2, SystemTheme, Vec2, ViewportBuilder, ViewportCommand, WindowLevel, X11WindowType, @@ -113,7 +113,6 @@ struct ConfiguredAppState EguiAp softbuffer_context: softbuffer::Context, /////////////////// END OF DANGER ZONE////////////////////////////////////// config: SoftwareBackendAppConfiguration, - software_backend: SoftwareBackend, renderer: EguiSoftwareRender, egui_app_factory: EguiAppFactory, } @@ -128,7 +127,6 @@ struct WindowInitializedAppState window: Rc, /////////////////// END OF DANGER ZONE////////////////////////////////////// config: SoftwareBackendAppConfiguration, - software_backend: SoftwareBackend, renderer: EguiSoftwareRender, egui_app_factory: EguiAppFactory, } @@ -144,7 +142,7 @@ struct RunningEguiAppState EguiA window: Rc, /////////////////// END OF DANGER ZONE////////////////////////////////////// config: SoftwareBackendAppConfiguration, - software_backend: SoftwareBackend, + last_frame_time: Option, renderer: EguiSoftwareRender, egui_app_factory: EguiAppFactory, softbuffer_context: softbuffer::Context, @@ -192,7 +190,6 @@ impl EguiApp> Ok(WindowInitializedAppState { config: self.config, - software_backend: self.software_backend, renderer: self.renderer, egui_context: self.egui_context, egui_app_factory: self.egui_app_factory, @@ -239,7 +236,7 @@ impl EguiApp> fullscreen, visible, input_events: Vec::new(), - software_backend: self.software_backend, + last_frame_time: None, }) } } @@ -257,10 +254,6 @@ impl EguiApp> ) -> Self { Self::Configured(ConfiguredAppState { config, - software_backend: SoftwareBackend { - capture_frame_time: false, - last_frame_time: None, - }, renderer, softbuffer_context, egui_context, @@ -423,7 +416,6 @@ impl EguiApp> pub(crate) fn suspend(self) -> WindowInitializedAppState { WindowInitializedAppState { config: self.config, - software_backend: self.software_backend, renderer: self.renderer, egui_context: self.egui_context, egui_app_factory: self.egui_app_factory, @@ -440,11 +432,7 @@ impl EguiApp> event: Event<()>, elwt: &ActiveEventLoop, ) -> Result<(), SoftwareBackendAppError> { - let start = if self.software_backend.capture_frame_time { - Some(Instant::now()) - } else { - None - }; + let start = Instant::now(); elwt.set_control_flow(ControlFlow::Wait); @@ -478,7 +466,13 @@ impl EguiApp> self.input_events.clear(); let full_output = self.egui_context.run(raw_input, |ctx| { - self.egui_app.update(ctx, &mut self.software_backend); + self.egui_app.update( + ctx, + &mut SoftwareBackend { + last_frame_time: self.last_frame_time, + renderer: &mut self.renderer, + }, + ); self.egui_context.viewport(|r| { let mut die = false; @@ -704,28 +698,41 @@ impl EguiApp> .map_err(SoftwareBackendAppError::soft_buffer( "softbuffer::Surface::buffer_mut", ))?; - buffer.fill(0); // CLEAR let buffer_ref = &mut BufferMutRef::new( bytemuck::cast_slice_mut(&mut buffer), - width.get() as usize, - height.get() as usize, + width.get(), + height.get(), ); - - self.renderer.render( + let redraw_everything_this_frame = + self.renderer.cached_size() != (buffer_ref.width, buffer_ref.height); + let dirty_rect = self.renderer.render( buffer_ref, - &clipped_primitives, + redraw_everything_this_frame, + clipped_primitives, &full_output.textures_delta, full_output.pixels_per_point, ); - buffer - .present() - .map_err(SoftwareBackendAppError::soft_buffer( - "softbuffer::Buffer::present", - ))?; + #[cfg(feature = "raster_stats")] + let present_start = std::time::Instant::now(); + if !dirty_rect.is_empty() { + let dirty_rect = softbuffer::Rect { + x: dirty_rect.min_x, + y: dirty_rect.min_y, + width: NonZeroU32::new(dirty_rect.width()).expect("non zero rect"), + height: NonZeroU32::new(dirty_rect.height()).expect("non zero rect"), + }; + buffer.present_with_damage(&[dirty_rect]).map_err( + SoftwareBackendAppError::soft_buffer("softbuffer::Buffer::present"), + )?; + } + #[cfg(feature = "raster_stats")] + { + self.renderer.stats().winit_present.mark(present_start); + } - self.software_backend.last_frame_time = start.map(|a| a.elapsed()); + self.last_frame_time = Some(start.elapsed()); } WindowEvent::CloseRequested => { @@ -760,8 +767,6 @@ impl EguiApp> /// /// impl App for MyApp { /// fn update(&mut self, ctx: &egui::Context, backend: &mut SoftwareBackend) { -/// backend.set_capture_frame_time(true); -/// /// /// egui::CentralPanel::default().show(ctx, |ui| { /// ui.label(format!( @@ -773,30 +778,38 @@ impl EguiApp> /// } /// /// ``` -pub struct SoftwareBackend { - capture_frame_time: bool, +pub struct SoftwareBackend<'a> { last_frame_time: Option, + renderer: &'a mut EguiSoftwareRender, } -impl SoftwareBackend { - /// Returns true if the frame time for the next frame is captured. - pub fn is_capture_frame_time(&self) -> bool { - self.capture_frame_time +impl<'a> SoftwareBackend<'a> { + /// Returns the rendering duration of the last frame if this information is available. + /// Returns none otherwise. + pub fn last_frame_time(&self) -> Option { + self.last_frame_time } - /// Enables or disables capturing the frame time. - /// Note that once this is called, the value persists until this function is called again. - /// Calling this with true will not affect the current frame, so once this is called with true, - /// you will need to wait for 2 more frames until you get a value. - pub fn set_capture_frame_time(&mut self, capture: bool) { - self.capture_frame_time = capture; + #[cfg(feature = "raster_stats")] + pub fn display_stats(&self, ui: &mut egui::Ui) { + self.renderer.display_stats(ui); } - /// Returns the rendering duration of the last frame if this information is available. - /// Returns none otherwise. Note that this information is only captured is `set_capture_frame_time` - /// is called with true. - pub fn last_frame_time(&self) -> Option { - self.last_frame_time + /// Get the caching mode of the renderer + pub fn caching(&self) -> SoftwareRenderCaching { + self.renderer.caching() + } + + /// Change the caching mode of the renderer + pub fn set_caching(&mut self, caching: SoftwareRenderCaching) { + self.renderer.set_caching(caching); + } + + /// Clear cache and reclaim memory + /// + /// This will cause the next frame to redraw everything + pub fn clear_cache(&mut self) { + self.renderer.clear_cache(); } } @@ -813,9 +826,8 @@ pub struct SoftwareBackendAppConfiguration { /// The underlying egui viewport builder that is used to create the window with winit. pub viewport_builder: ViewportBuilder, - /// If true: rasterized ClippedPrimitives are cached and rendered to an intermediate tiled canvas. That canvas is - /// then rendered over the frame buffer. If false ClippedPrimitives are rendered directly to the frame buffer. - /// Rendering without caching is much slower and primarily intended for testing. + /// If false: Rasterize everything with triangles, always calculate vertex colors, uvs, use bilinear + /// everywhere, etc... Things *should* look the same with this set to `true` while rendering faster. /// /// Default is true! pub allow_raster_opt: bool, @@ -826,12 +838,10 @@ pub struct SoftwareBackendAppConfiguration { /// Default is true! pub convert_tris_to_rects: bool, - /// If true: rasterized ClippedPrimitives are cached and rendered to an intermediate tiled canvas. That canvas is - /// then rendered over the frame buffer. If false ClippedPrimitives are rendered directly to the frame buffer. - /// Rendering without caching is much slower and primarily intended for testing. + /// Define the caching mode of the renderer /// - /// Default is true! - pub caching: bool, + /// Default is [`SoftwareRenderCaching::BlendTiled`]! + pub caching: SoftwareRenderCaching, } impl SoftwareBackendAppConfiguration { @@ -876,7 +886,7 @@ impl SoftwareBackendAppConfiguration { allow_raster_opt: true, convert_tris_to_rects: true, - caching: true, + caching: SoftwareRenderCaching::BlendTiled, } } @@ -1083,13 +1093,10 @@ impl SoftwareBackendAppConfiguration { self.convert_tris_to_rects = convert_tris_to_rects; self } - - /// If true: rasterized ClippedPrimitives are cached and rendered to an intermediate tiled canvas. That canvas is - /// then rendered over the frame buffer. If false ClippedPrimitives are rendered directly to the frame buffer. - /// Rendering without caching is much slower and primarily intended for testing. + /// Define the caching mode of the renderer /// - /// Default is true! - pub const fn caching(mut self, caching: bool) -> Self { + /// Default is [`SoftwareRenderCaching::BlendTiled`]! + pub const fn caching(mut self, caching: SoftwareRenderCaching) -> Self { self.caching = caching; self } diff --git a/tests/mod.rs b/tests/mod.rs index 20825df..fb073c3 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -2,7 +2,7 @@ mod tests { use egui::{Vec2, vec2}; - use egui_software_backend::{ColorFieldOrder, EguiSoftwareRender}; + use egui_software_backend::{ColorFieldOrder, EguiSoftwareRender, SoftwareRenderCaching}; use image::{ImageBuffer, Rgba}; use egui_kittest::HarnessBuilder; @@ -44,14 +44,20 @@ mod tests { .save(format!("tests/tmp/gpu_px_per_point{px_per_point}.png")) .unwrap(); - for use_cache in [false, true] { + for mode in [ + SoftwareRenderCaching::Direct, + SoftwareRenderCaching::Mesh, + SoftwareRenderCaching::MeshTiled, + SoftwareRenderCaching::BlendTiled, + ] { for allow_raster_opt in [false, true] { for convert_tris_to_rects in [false, true] { // --- Render on CPU let egui_software_render = EguiSoftwareRender::new(ColorFieldOrder::Rgba) .with_allow_raster_opt(allow_raster_opt) .with_convert_tris_to_rects(convert_tris_to_rects) - .with_caching(use_cache); + .with_caching(mode) + .with_canvas(); let mut harness = HarnessBuilder::default() .with_size(RESOLUTION) @@ -62,7 +68,7 @@ mod tests { let cpu_render_image = harness.render().unwrap(); let name = format!( - "px_per_pt {px_per_point}, use_cache {use_cache}, raster_opt {allow_raster_opt}, tris_to_rects {convert_tris_to_rects}" + "px_per_pt {px_per_point}, mode {mode:?}, raster_opt {allow_raster_opt}, tris_to_rects {convert_tris_to_rects}" ); if let Some((pixels_failed, diff_image)) = dify( From 282daaf96d9c0cff3666ab0f4c4a2d65393bdbe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Rouill=C3=A9?= Date: Mon, 23 Feb 2026 20:08:45 +0100 Subject: [PATCH 02/13] fix lib doctest example --- src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index e40254f..64f8138 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,8 @@ //! //!sw_render.render( //! &mut buffer_ref, -//! &primitives, +//! /*redraw_everything_this_frame=*/true, +//! primitives, //! &out.textures_delta, //! out.pixels_per_point, //!); From 0fb4be4353e037a2b5d5bd5045f4b90b2bc2a5da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Rouill=C3=A9?= Date: Mon, 23 Feb 2026 20:14:48 +0100 Subject: [PATCH 03/13] Fix clippy warning casting to the same type is unnecessary (`u32` -> `u32`) --- src/raster/tri.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/raster/tri.rs b/src/raster/tri.rs index fee0224..3b53566 100644 --- a/src/raster/tri.rs +++ b/src/raster/tri.rs @@ -115,7 +115,7 @@ fn draw_tri_impl< } else { draw.const_tri_color_u8x4 }; - let pixel = buffer.get_mut(ss_x as u32, ss_y as u32); + let pixel = buffer.get_mut(ss_x, ss_y as u32); *pixel = if alpha_blend { simd_impl.egui_blend_u8(src, *pixel) } else { From c2d046ad572c1c8377bcc1d5027deda0f2f7f74b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Rouill=C3=A9?= Date: Mon, 23 Feb 2026 21:25:40 +0100 Subject: [PATCH 04/13] Clear dirty rects before rendering (mesh, meshtiled, direct) --- src/dirty_rect.rs | 29 +++++++++++++------- src/lib.rs | 68 ++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 77 insertions(+), 20 deletions(-) diff --git a/src/dirty_rect.rs b/src/dirty_rect.rs index 76a3853..9eb1944 100644 --- a/src/dirty_rect.rs +++ b/src/dirty_rect.rs @@ -66,12 +66,16 @@ impl DirtyRect { } #[inline] - pub fn intersection(self, other: DirtyRect) -> Self { - Self { - min_x: self.min_x.max(other.min_x), - min_y: self.min_y.max(other.min_y), - max_x: self.max_x.min(other.max_x), - max_y: self.max_y.min(other.max_y), + pub fn intersection(self, other: DirtyRect) -> Option { + if self.intersects(other) { + Some(Self { + min_x: self.min_x.max(other.min_x), + min_y: self.min_y.max(other.min_y), + max_x: self.max_x.min(other.max_x), + max_y: self.max_y.min(other.max_y), + }) + } else { + None } } @@ -106,11 +110,12 @@ impl ComputeTiledDirtyRects { pub fn intersections(&self, other: DirtyRect) -> impl Iterator + '_ { self.minimal_non_overlapping_bboxes .iter() - .filter(move |bbox| bbox.intersects(other)) - .map(move |bbox| bbox.intersection(other)) + .filter_map(move |bbox| bbox.intersection(other)) } - pub fn set_bboxes(&mut self, boxes: impl Iterator) { + /// Compute a non overlapping set of tiled dirty rect from `boxes` iterator + /// that are within `canvas_rect` bounds + pub fn set_bboxes(&mut self, canvas_rect: DirtyRect, boxes: impl Iterator) { fn merge_intervals(intervals: &mut [(u32, u32)], mut f_yield: impl FnMut((u32, u32))) { if intervals.is_empty() { return; @@ -132,7 +137,11 @@ impl ComputeTiledDirtyRects { self.minimal_non_overlapping_bboxes.clear(); self.bboxes.clear(); - self.bboxes.extend(boxes.map(|b| b.tiled::())); + self.bboxes.extend( + boxes + .map(|b| b.tiled::()) + .filter_map(|b| b.intersection(canvas_rect)), + ); // Step 1: collect all unique y-coordinates self.ys.clear(); self.ys diff --git a/src/lib.rs b/src/lib.rs index 64f8138..211c4de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -496,7 +496,7 @@ impl EguiSoftwareRenderInner { ) -> DirtyRect where F: Fn(&Self, CacheReuse, Vec2, Vec2, egui::Rect, Mesh) -> P + Sync + Send, - U: Fn(&mut Self, &HashMap), + U: Fn(&mut Self, DirtyRect, &HashMap), P: DerefMut + Sync + Send, { // TODO: need to deal with user textures. Either make the fields of EguiUserTextures pub or need to come up with a replacement. @@ -530,10 +530,16 @@ impl EguiSoftwareRenderInner { f_render_prims_to_cache, ); - let mut dirty_rect = self.update_dirty_rect(cached_primitives); + let canvas_rect = DirtyRect { + min_x: 0, + min_y: 0, + max_x: canvas.width, + max_y: canvas.height, + }; + let mut dirty_rect = self.update_dirty_rect(canvas_rect, cached_primitives); if !dirty_rect.is_empty() { - f_update_dirty_tiles(self, cached_primitives); + f_update_dirty_tiles(self, canvas_rect, cached_primitives); } // clear_unused_cached_prims @@ -689,6 +695,8 @@ impl EguiSoftwareRenderInner { #[cfg(feature = "raster_stats")] let start = std::time::Instant::now(); + direct_draw_buffer.data.fill(Default::default()); // CLEAR + for paint_job in paint_jobs { // TODO not sure why +1.5 is needed here. Occasionally things are cropped out without it. let splat = 1.5f32; @@ -1015,6 +1023,16 @@ impl EguiSoftwareRenderInner { #[cfg(feature = "raster_stats")] let start = std::time::Instant::now(); + match self.caching { + SoftwareRenderCaching::MeshTiled => { + for &dirty_rect in self.dirty_rects.iter() { + direct_draw_buffer.clear_rect(dirty_rect) + } + } + SoftwareRenderCaching::Mesh => direct_draw_buffer.clear_rect(dirty_rect), + _ => unreachable!(), + } + let mut sorted_prim_cache = cached_primitives.values().collect::>(); sorted_prim_cache.sort_unstable_by_key(|prim| prim.inner.z_order); @@ -1174,7 +1192,11 @@ impl EguiSoftwareRenderInner { const DIRTY_TILE_MASK: u8 = 0b00000001; const OCCUPIED_TILE_MASK: u8 = 0b000000010; - fn update_dirty_tiles(&mut self, cached_primitives: &HashMap) { + fn update_dirty_tiles( + &mut self, + _canvas_rect: DirtyRect, + cached_primitives: &HashMap, + ) { #[cfg(feature = "raster_stats")] let start = std::time::Instant::now(); @@ -1198,11 +1220,18 @@ impl EguiSoftwareRenderInner { } } - fn update_dirty_rects(&mut self, cached_primitives: &HashMap) { + /// Compute a non overlapping set of tiled dirty rect from changed primitives rects + /// that are within `canvas_rect` bounds + fn update_dirty_rects( + &mut self, + canvas_rect: DirtyRect, + cached_primitives: &HashMap, + ) { #[cfg(feature = "raster_stats")] let start = std::time::Instant::now(); if self.caching == SoftwareRenderCaching::MeshTiled { self.dirty_rects.set_bboxes( + canvas_rect, cached_primitives .values() .filter(|prim| !prim.inner.seen_this_frame || prim.inner.rendered_this_frame) @@ -1216,7 +1245,14 @@ impl EguiSoftwareRenderInner { } } - fn update_dirty_rect

(&mut self, cached_primitives: &HashMap) -> DirtyRect + /// Compute the dirty rect from changed primitives rects + /// + /// Returns a dirty rect that is within `canvas_rect` bounds + fn update_dirty_rect

( + &mut self, + canvas_rect: DirtyRect, + cached_primitives: &HashMap, + ) -> DirtyRect where P: Deref, { @@ -1227,10 +1263,12 @@ impl EguiSoftwareRenderInner { for prim in cached_primitives.values() { let prim = prim.deref(); if !prim.seen_this_frame || prim.rendered_this_frame { - if dirty_rect.is_empty() { - dirty_rect = prim.rect; - } else { - dirty_rect = dirty_rect.union(prim.rect) + if let Some(prim_rect) = prim.rect.intersection(canvas_rect) { + if dirty_rect.is_empty() { + dirty_rect = prim_rect; + } else { + dirty_rect = dirty_rect.union(prim_rect) + } } } } @@ -1533,6 +1571,16 @@ impl<'a> BufferMutRef<'a> { pub fn get_mut(&mut self, x: u32, y: u32) -> &mut [u8; 4] { &mut self.data[as_usize(x) + as_usize(y) * as_usize(self.width)] } + + #[inline] + pub fn clear_rect(&mut self, rect: DirtyRect) { + for y in rect.min_y..rect.max_y { + let row_start = y * self.width; + let start = row_start + rect.min_x; + let end = row_start + rect.max_x; + self.data[as_usize(start)..as_usize(end)].fill([0; 4]); + } + } } /// A reference to a slice of image buffer data and corresponding image extents. From 1504c56e8038651f63709fe3f479638548e4916a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Rouill=C3=A9?= Date: Mon, 23 Feb 2026 21:31:07 +0100 Subject: [PATCH 05/13] Rename prim_prepare_px_mesh arg splat to padding --- src/lib.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 211c4de..bbbbf69 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -699,9 +699,9 @@ impl EguiSoftwareRenderInner { for paint_job in paint_jobs { // TODO not sure why +1.5 is needed here. Occasionally things are cropped out without it. - let splat = 1.5f32; + let padding = 1.5f32; let (clip_rect, mesh_min, mesh_max, px_mesh) = - match self.prim_prepare_px_mesh(splat, pixels_per_point, paint_job) { + match self.prim_prepare_px_mesh(padding, pixels_per_point, paint_job) { Some(x) => x, None => continue, }; @@ -822,9 +822,9 @@ impl EguiSoftwareRenderInner { F: Fn(&Self, CacheReuse, Vec2, Vec2, egui::Rect, Mesh) -> P + Sync + Send, P: DerefMut + Sync + Send, { - let splat = 0.5f32; + let padding = 0.5f32; let (clip_rect, mesh_min, mesh_max, px_mesh) = - match self.prim_prepare_px_mesh(splat, pixels_per_point, paint_job) { + match self.prim_prepare_px_mesh(padding, pixels_per_point, paint_job) { Some(x) => x, None => return CacheUpdate::None, }; @@ -904,7 +904,7 @@ impl EguiSoftwareRenderInner { fn prim_prepare_px_mesh( &self, - splat: f32, + padding: f32, pixels_per_point: f32, egui::ClippedPrimitive { clip_rect, @@ -924,7 +924,7 @@ impl EguiSoftwareRenderInner { } let clip_rect = egui::Rect { min: clip_rect.min * pixels_per_point, - max: clip_rect.max * pixels_per_point + egui::Vec2::splat(splat), + max: clip_rect.max * pixels_per_point + egui::Vec2::splat(padding), }; let mut mesh_min = egui::Vec2::splat(f32::MAX); let mut mesh_max = egui::Vec2::splat(-f32::MAX); From 441abd28be6046bc588b8e66f55f35454de6c8a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Rouill=C3=A9?= Date: Mon, 23 Feb 2026 21:40:40 +0100 Subject: [PATCH 06/13] Do pixel x,y to offset calculations in u32 --- src/lib.rs | 16 ++++++++-------- src/raster/rect.rs | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index bbbbf69..b95b14e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -599,7 +599,7 @@ impl EguiSoftwareRenderInner { // blit rows of tiles in parallel let width = buffer.width; - let px_per_row_of_tiles = as_usize(width) * as_usize(TILE_SIZE); + let px_per_row_of_tiles = as_usize(width * TILE_SIZE); buffer .data @@ -760,7 +760,7 @@ impl EguiSoftwareRenderInner { let (width, height) = (prim.rect.width(), prim.rect.height()); let mut prim = TiledCachedPrimitive { inner: prim, - buffer: vec![[0u8; 4]; as_usize(width) * as_usize(height)], + buffer: vec![[0u8; 4]; as_usize(width * height)], occupied_tiles: Vec::with_capacity(64), }; let mut buffer_ref = BufferMutRef { @@ -1114,7 +1114,7 @@ impl EguiSoftwareRenderInner { let full_height = canvas.height; let width = canvas.width; - let px_per_row_of_tiles = as_usize(width) * as_usize(TILE_SIZE); + let px_per_row_of_tiles = as_usize(width * TILE_SIZE); canvas .data @@ -1505,7 +1505,7 @@ impl TiledCachedPrimitive { for x in px_start_x..px_end_x { // Purposefully panicing when out of bounds. If it's out of bounds then the math is wrong and // the tile is not being calculated correctly. - let offset = as_usize(x) + as_usize(y) * as_usize(width); + let offset = as_usize(x + y * width); if u32::from_le_bytes(self.buffer[offset]) > 0 { self.occupied_tiles.push([tile_x as u16, tile_y as u16]); break 'px_outer; @@ -1564,12 +1564,12 @@ impl<'a> BufferMutRef<'a> { pub fn get_mut_clamped(&mut self, x: u32, y: u32) -> &mut [u8; 4] { let x = x.min(self.width_extent); let y = y.min(self.height_extent); - &mut self.data[as_usize(x) + as_usize(y) * as_usize(self.width)] + &mut self.data[as_usize(x + y * self.width)] } #[inline(always)] pub fn get_mut(&mut self, x: u32, y: u32) -> &mut [u8; 4] { - &mut self.data[as_usize(x) + as_usize(y) * as_usize(self.width)] + &mut self.data[as_usize(x + y * self.width)] } #[inline] @@ -1606,12 +1606,12 @@ impl<'a> BufferRef<'a> { pub fn get_ref_clamped(&self, x: u32, y: u32) -> &[u8; 4] { let x = x.min(self.width_extent); let y = y.min(self.height_extent); - &self.data[as_usize(x) + as_usize(y) * as_usize(self.width)] + &self.data[as_usize(x + y * self.width)] } #[inline(always)] pub fn get_ref(&self, x: u32, y: u32) -> &[u8; 4] { - &self.data[as_usize(x) + as_usize(y) * as_usize(self.width)] + &self.data[as_usize(x + y * self.width)] } } diff --git a/src/raster/rect.rs b/src/raster/rect.rs index 028963c..37a10f2 100644 --- a/src/raster/rect.rs +++ b/src/raster/rect.rs @@ -122,7 +122,7 @@ fn draw_rect_impl Date: Tue, 3 Mar 2026 00:49:53 +0100 Subject: [PATCH 07/13] Add supports for double and triple buffering and no buffering at all Previous revisions were only working in single buffering mode (win32, web, x11, orbital) Tests are run on simulated no, single, double and triple buffered canvas, there might still be differences with softbuffer. So far it handle softbuffer frame age and frame resize. --- examples/winit_raw.rs | 13 +- src/lib.rs | 553 +++++++++++++++++++++++++++++++++--------- src/test_render.rs | 69 +++++- src/winit.rs | 18 +- tests/mod.rs | 239 ++++++++++++------ 5 files changed, 688 insertions(+), 204 deletions(-) diff --git a/examples/winit_raw.rs b/examples/winit_raw.rs index c9676ba..021a413 100644 --- a/examples/winit_raw.rs +++ b/examples/winit_raw.rs @@ -52,6 +52,7 @@ fn main() { } else { egui_software_backend::SoftwareRenderCaching::BlendTiled }); + let mut buffer_states = egui_software_backend::BufferStates::new(); let event_loop: EventLoop<()> = EventLoop::new().unwrap(); @@ -152,17 +153,21 @@ fn main() { .tessellate(full_output.shapes, full_output.pixels_per_point); let mut buffer = app.surface.buffer_mut().unwrap(); - + let age = buffer.age(); let buffer_ref = &mut BufferMutRef::new( bytemuck::cast_slice_mut(&mut buffer), width, height, ); - let redraw_everything_this_frame = - egui_software_render.cached_size() != (buffer_ref.width, buffer_ref.height); + let buffer_state = buffer_states.next(age, buffer_ref.data.len()); + if buffer_state.is_new_zeroed() { + // age == 0 || resized + buffer_ref.data.fill(Default::default()); + } + let dirty_rect = egui_software_render.render( buffer_ref, - redraw_everything_this_frame, + buffer_state, clipped_primitives, &full_output.textures_delta, full_output.pixels_per_point, diff --git a/src/lib.rs b/src/lib.rs index b95b14e..b5f3ba7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ //! //! ## Basic example usage: //! ```rust -//!use egui_software_backend::{BufferMutRef, ColorFieldOrder, EguiSoftwareRender}; +//!use egui_software_backend::{BufferMutRef, BufferState, ColorFieldOrder, EguiSoftwareRender}; //!let buffer = &mut vec![[0u8; 4]; 512 * 512]; //!let mut buffer_ref = BufferMutRef::new(buffer, 512, 512); //!let ctx = egui::Context::default(); @@ -17,7 +17,7 @@ //! //!sw_render.render( //! &mut buffer_ref, -//! /*redraw_everything_this_frame=*/true, +//! BufferState::AlwaysNewZeroed, //! primitives, //! &out.textures_delta, //! out.pixels_per_point, @@ -75,7 +75,10 @@ extern crate alloc; #[cfg(feature = "std")] extern crate std; -use core::ops::{Deref, DerefMut, Range}; +use core::{ + ops::{Deref, DerefMut, Range}, + u8, +}; use alloc::{borrow::Cow, vec, vec::Vec}; @@ -165,7 +168,6 @@ pub enum SoftwareRenderCaching { } struct EguiSoftwareRenderInner { - cached_size: (u32, u32), textures: HashMap, /// Tiles grid size (cols, rows) tiles_dim: [u32; 2], @@ -181,6 +183,120 @@ struct EguiSoftwareRenderInner { pub stats: RenderStats, } +/// Manage single, double and triple buffering buffer states +pub struct BufferStates { + /// last frame + frame_1: (BufferState, usize), + /// last frame before that (for backends using double buffering). + frame_2: (BufferState, usize), + /// last frame before before that (for backends using triple buffering). + frame_3: (BufferState, usize), +} + +impl BufferStates { + pub const fn new() -> Self { + Self { + frame_1: (BufferState::Buffer1Zeroed, 0), + frame_2: (BufferState::Buffer2Zeroed, 0), + frame_3: (BufferState::Buffer3Zeroed, 0), + } + } + + /// Get the next buffer state + /// + /// * `age` is the number of frames ago this buffer was last presented (up to 3). + /// So if the value is 1, it is the same as the last frame, + /// and if it is 2, it is the same as the frame before that (for backends using double buffering), + /// and if it is 3, it is the same as the frame before before that (for backends using triple buffering), + /// If the value is 0, it is a new buffer. + /// + /// * `len` is the buffer size, if it differs the content will be marked as zeroed + /// + /// It's your responsability to ensure the provided buffer to `render` is zeroed if this returns + /// a zeroed variant! + pub fn next(&mut self, age: u8, buffer_len: usize) -> BufferState { + if cfg!(any(target_os = "macos", target_os = "android")) { + return BufferState::AlwaysZeroed; + } + if age == 1 { + // will present last frame + } else if age == 2 { + // will present last frame before that + // promote last frame before that to presenting one + core::mem::swap(&mut self.frame_1, &mut self.frame_2); + } else { + // will present last frame before before that + // promote last frame before before that to presenting one + core::mem::swap(&mut self.frame_1, &mut self.frame_3); + // promote last frame before that to last frame + core::mem::swap(&mut self.frame_2, &mut self.frame_3); + } + let (ret, len_1) = self.frame_1; + self.frame_1 = (ret.to_incremental(), buffer_len); + if age == 0 || buffer_len != len_1 { + ret.to_new_zeroed() + } else { + ret + } + } +} + +/// Decribe the state of the provided buffer before rendering +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BufferState { + /// The provided buffer will always be a new buffer filled with zeroes + /// This allows the renderer to know when to cache the last frame itself + AlwaysZeroed, + + /// A new Buffer identified as #1, filled with zeroes + Buffer1Zeroed, + /// Buffer identified as #1 that can be updated with changes since last render + Buffer1Incremental, + + /// A new Buffer identified as #1, filled with zeroes + Buffer2Zeroed, + /// Buffer identified as #1 that can be updated with changes since last render + Buffer2Incremental, + + /// A new Buffer identified as #1, filled with zeroes + Buffer3Zeroed, + /// Buffer identified as #1 that can be updated with changes since last render + Buffer3Incremental, +} + +impl BufferState { + #[inline] + pub const fn is_new_zeroed(self) -> bool { + match self { + BufferState::AlwaysZeroed + | BufferState::Buffer1Zeroed + | BufferState::Buffer2Zeroed + | BufferState::Buffer3Zeroed => true, + BufferState::Buffer1Incremental + | BufferState::Buffer2Incremental + | BufferState::Buffer3Incremental => false, + } + } + + pub fn to_incremental(self) -> Self { + match self { + Self::AlwaysZeroed => Self::AlwaysZeroed, + Self::Buffer1Zeroed | Self::Buffer1Incremental => Self::Buffer1Incremental, + Self::Buffer2Zeroed | Self::Buffer2Incremental => Self::Buffer2Incremental, + Self::Buffer3Zeroed | Self::Buffer3Incremental => Self::Buffer3Incremental, + } + } + + pub fn to_new_zeroed(self) -> Self { + match self { + Self::AlwaysZeroed => Self::AlwaysZeroed, + Self::Buffer1Zeroed | Self::Buffer1Incremental => Self::Buffer1Zeroed, + Self::Buffer2Zeroed | Self::Buffer2Incremental => Self::Buffer2Zeroed, + Self::Buffer3Zeroed | Self::Buffer3Incremental => Self::Buffer3Zeroed, + } + } +} + /// Software render backend for egui. pub struct EguiSoftwareRender { tiledcached_primitives: HashMap, @@ -235,14 +351,14 @@ impl EguiSoftwareRenderCanvas { if self.renderer.inner.caching == SoftwareRenderCaching::Direct { self.renderer.render( buffer_ref, - true, + BufferState::AlwaysZeroed, paint_jobs, textures_delta, pixels_per_point, ); } else { - let redraw_everything_this_frame = - self.renderer.cached_size() != (buffer_ref.width, buffer_ref.height); + let len = as_usize(buffer_ref.width) * as_usize(buffer_ref.height); + let redraw_everything_this_frame = self.canvas.len() != len; if redraw_everything_this_frame { self.canvas.clear(); let len = as_usize(buffer_ref.width) * as_usize(buffer_ref.height); @@ -254,7 +370,11 @@ impl EguiSoftwareRenderCanvas { BufferMutRef::new(&mut self.canvas, buffer_ref.width, buffer_ref.height); let dirty_rect = self.renderer.render( &mut canvas, - redraw_everything_this_frame, + if redraw_everything_this_frame { + BufferState::Buffer1Zeroed + } else { + BufferState::Buffer1Incremental + }, paint_jobs, textures_delta, pixels_per_point, @@ -281,7 +401,6 @@ impl EguiSoftwareRender { tiledcached_primitives: Default::default(), dirtycached_primitives: Default::default(), inner: EguiSoftwareRenderInner { - cached_size: (0, 0), textures: Default::default(), tiles_dim: Default::default(), dirty_tiles: Default::default(), @@ -360,11 +479,6 @@ impl EguiSoftwareRender { self.inner.dirty_rects = Default::default(); } - /// The latest renderer `buffer_ref` width and height, if a cacheing mode is selected - pub const fn cached_size(&self) -> (u32, u32) { - self.inner.cached_size - } - /// Renders the given paint jobs to buffer_ref. Alternatively, when using caching /// EguiSoftwareRender::render_to_canvas() and subsequently EguiSoftwareRender::blit_canvas_to_buffer() can be run /// separately so that the primary rendering in render_to_canvas() can happen without a lock on the frame buffer. @@ -372,7 +486,7 @@ impl EguiSoftwareRender { /// /// # Arguments /// * `buffer_ref` - Buffer to render into. - /// * `redraw_everything_this_frame` - Redraw the whole buffer (ie. resize) + /// * `buffer_state` - Tell the render whats the current content of `buffer_ref` /// * `paint_jobs` - List of `egui::ClippedPrimitive` from egui to be rendered. /// * `paint_jobs` - List of `egui::ClippedPrimitive` from egui to be rendered. /// * `textures_delta` - The change in egui textures since last frame @@ -388,7 +502,7 @@ impl EguiSoftwareRender { pub fn render( &mut self, buffer_ref: &mut BufferMutRef, - redraw_everything_this_frame: bool, + buffer_state: BufferState, paint_jobs: Vec, textures_delta: &egui::TexturesDelta, pixels_per_point: f32, @@ -397,8 +511,13 @@ impl EguiSoftwareRender { self.inner.stats.clear(); match self.inner.caching { SoftwareRenderCaching::Direct => { - self.inner - .render_direct(buffer_ref, paint_jobs, textures_delta, pixels_per_point); + self.inner.render_direct( + buffer_ref, + buffer_state, + paint_jobs, + textures_delta, + pixels_per_point, + ); DirtyRect { min_x: 0, min_y: 0, @@ -409,14 +528,14 @@ impl EguiSoftwareRender { SoftwareRenderCaching::MeshTiled | SoftwareRenderCaching::Mesh => self .render_meshmaybetiled( buffer_ref, - redraw_everything_this_frame, + buffer_state, paint_jobs, textures_delta, pixels_per_point, ), SoftwareRenderCaching::BlendTiled => self.render_blendtiled( buffer_ref, - redraw_everything_this_frame, + buffer_state, paint_jobs, textures_delta, pixels_per_point, @@ -426,43 +545,39 @@ impl EguiSoftwareRender { fn render_blendtiled( &mut self, - canvas: &mut BufferMutRef, - redraw_everything_this_frame: bool, + buffer_ref: &mut BufferMutRef, + buffer_state: BufferState, paint_jobs: Vec, textures_delta: &egui::TexturesDelta, pixels_per_point: f32, ) -> DirtyRect { // TODO: need to deal with user textures. Either make the fields of EguiUserTextures pub or need to come up with a replacement. - let dirty_rect = self.inner.prepare_render_cache( + let dirty_rect = self.inner.render_tiled_impl( &mut self.tiledcached_primitives, - canvas, - redraw_everything_this_frame, + buffer_ref, + buffer_state, paint_jobs, textures_delta, pixels_per_point, EguiSoftwareRenderInner::render_prim, EguiSoftwareRenderInner::update_dirty_tiles, + EguiSoftwareRenderInner::render_from_tiledcache, ); - - if !dirty_rect.is_empty() { - self.inner - .render_from_tiledcache(&self.tiledcached_primitives, canvas); - } dirty_rect } fn render_meshmaybetiled( &mut self, - canvas: &mut BufferMutRef, - redraw_everything_this_frame: bool, + buffer_ref: &mut BufferMutRef, + buffer_state: BufferState, paint_jobs: Vec, textures_delta: &egui::TexturesDelta, pixels_per_point: f32, ) -> DirtyRect { - let dirty_rect = self.inner.prepare_render_cache( + self.inner.render_tiled_impl( &mut self.dirtycached_primitives, - canvas, - redraw_everything_this_frame, + buffer_ref, + buffer_state, paint_jobs, textures_delta, pixels_per_point, @@ -472,59 +587,61 @@ impl EguiSoftwareRender { clip_rect, }, EguiSoftwareRenderInner::update_dirty_rects, - ); - if !dirty_rect.is_empty() { - self.inner - .render_from_meshcache(&self.dirtycached_primitives, canvas, dirty_rect); - } - dirty_rect + EguiSoftwareRenderInner::render_from_meshcache, + ) } } impl EguiSoftwareRenderInner { #[allow(clippy::too_many_arguments)] - fn prepare_render_cache( + fn render_tiled_impl( &mut self, cached_primitives: &mut HashMap, - canvas: &mut BufferMutRef, - redraw_everything_this_frame: bool, + buffer_ref: &mut BufferMutRef, + buffer_state: BufferState, paint_jobs: Vec, textures_delta: &egui::TexturesDelta, pixels_per_point: f32, f_render_prims_to_cache: F, f_update_dirty_tiles: U, + f_render: R, ) -> DirtyRect where - F: Fn(&Self, CacheReuse, Vec2, Vec2, egui::Rect, Mesh) -> P + Sync + Send, - U: Fn(&mut Self, DirtyRect, &HashMap), P: DerefMut + Sync + Send, + F: Fn(&Self, CacheReuse, Vec2, Vec2, egui::Rect, Mesh) -> P + Sync + Send, + U: Fn(&mut Self, BufferStateFlag, DirtyRect, &HashMap), + R: Fn(&Self, &[&P], &mut BufferMutRef, DirtyRect, bool), { // TODO: need to deal with user textures. Either make the fields of EguiUserTextures pub or need to come up with a replacement. - assert!(canvas.width > 0); - assert!(canvas.height > 0); + assert!(buffer_ref.width > 0); + assert!(buffer_ref.height > 0); assert!(pixels_per_point > 0.0); + let buffer_state_flag = buffer_state.as_flag(); + let redraw_everything_this_frame = buffer_state.is_new_zeroed(); if redraw_everything_this_frame { - cached_primitives.clear(); + for (_hash, prim) in cached_primitives.iter_mut() { + prim.seen_this_or_last_frame = prim.seen_this_frame.unmarked(buffer_state_flag); + prim.seen_this_frame.unmark(buffer_state_flag); + } } else { - assert_eq!(self.cached_size, (canvas.width, canvas.height)); - } - self.cached_size = (canvas.width, canvas.height); - - for (_hash, prim) in cached_primitives.iter_mut() { - prim.deref_mut().seen_this_frame = false; + for (_hash, prim) in cached_primitives.iter_mut() { + prim.seen_this_or_last_frame = prim.seen_this_frame; + prim.seen_this_frame.unmark(buffer_state_flag); + } } self.tiles_dim = [ - canvas.width.div_ceil(TILE_SIZE), - canvas.height.div_ceil(TILE_SIZE), + buffer_ref.width.div_ceil(TILE_SIZE), + buffer_ref.height.div_ceil(TILE_SIZE), ]; self.set_textures(textures_delta); self.render_prims_to_cache( cached_primitives, + buffer_state_flag, paint_jobs, pixels_per_point, f_render_prims_to_cache, @@ -533,28 +650,45 @@ impl EguiSoftwareRenderInner { let canvas_rect = DirtyRect { min_x: 0, min_y: 0, - max_x: canvas.width, - max_y: canvas.height, + max_x: buffer_ref.width, + max_y: buffer_ref.height, }; - let mut dirty_rect = self.update_dirty_rect(canvas_rect, cached_primitives); + let mut dirty_rect = + self.update_dirty_rect(buffer_state_flag, canvas_rect, cached_primitives); if !dirty_rect.is_empty() { - f_update_dirty_tiles(self, canvas_rect, cached_primitives); + f_update_dirty_tiles(self, buffer_state_flag, canvas_rect, cached_primitives); } // clear_unused_cached_prims - cached_primitives.retain(|_hash, prim| prim.deref().seen_this_frame); + cached_primitives.retain(|_hash, prim| !prim.seen_this_frame.all_false()); if redraw_everything_this_frame { dirty_rect = DirtyRect { min_x: 0, min_y: 0, - max_x: canvas.width, - max_y: canvas.height, + max_x: buffer_ref.width, + max_y: buffer_ref.height, }; } self.free_textures(textures_delta); + + if !dirty_rect.is_empty() { + let mut sorted_prim_cache = cached_primitives + .values() + .filter(|c| c.seen_this_frame.is_true(buffer_state_flag)) + .collect::>(); + sorted_prim_cache.sort_unstable_by_key(|prim| prim.z_order); + f_render( + self, + &sorted_prim_cache, + buffer_ref, + dirty_rect, + buffer_state.is_new_zeroed(), + ); + } + dirty_rect } @@ -686,6 +820,7 @@ impl EguiSoftwareRenderInner { fn render_direct( &mut self, direct_draw_buffer: &mut BufferMutRef, + buffer_state: BufferState, paint_jobs: Vec, textures_delta: &egui::TexturesDelta, pixels_per_point: f32, @@ -695,7 +830,9 @@ impl EguiSoftwareRenderInner { #[cfg(feature = "raster_stats")] let start = std::time::Instant::now(); - direct_draw_buffer.data.fill(Default::default()); // CLEAR + if !buffer_state.is_new_zeroed() { + direct_draw_buffer.data.fill(Default::default()); // CLEAR + } for paint_job in paint_jobs { // TODO not sure why +1.5 is needed here. Occasionally things are cropped out without it. @@ -813,6 +950,7 @@ impl EguiSoftwareRenderInner { fn prim_prepare_update( &self, cached_primitives: &HashMap, + buffer_state_flag: BufferStateFlag, pixels_per_point: f32, prim_idx: u32, paint_job: egui::ClippedPrimitive, @@ -869,16 +1007,22 @@ impl EguiSoftwareRenderInner { max_x: cropped_min.x as u32 + width, max_y: cropped_min.y as u32 + height, }; - if cached_primitives.contains_key(&hash) { - CacheUpdate::CacheReuse( - hash, - CacheReuse { - z_order: prim_idx, - rect, - seen_this_frame: true, - rendered_this_frame: false, + + if let Some(cached) = cached_primitives.get(&hash) { + let prim = CacheReuse { + z_order: prim_idx, + rect, + seen_this_frame: cached.seen_this_frame.marked(buffer_state_flag), + seen_this_or_last_frame: cached.seen_this_or_last_frame.marked(buffer_state_flag), + rendered_this_frame: { + if cached.seen_this_or_last_frame.is_true(buffer_state_flag) { + cached.rendered_this_frame.unmarked(buffer_state_flag) + } else { + cached.rendered_this_frame.marked(buffer_state_flag) + } }, - ) + }; + CacheUpdate::CacheReuse(hash, prim) } else { if width > 8192 || height > 8192 { // TODO it occasionally tries to make giant buffers in the first couple frames initially for some reason. @@ -892,8 +1036,9 @@ impl EguiSoftwareRenderInner { let prim = CacheReuse { z_order: prim_idx, rect, - seen_this_frame: true, - rendered_this_frame: true, + seen_this_frame: BufferFlags::new().marked(buffer_state_flag), + seen_this_or_last_frame: BufferFlags::new().marked(buffer_state_flag), + rendered_this_frame: BufferFlags::new().marked(buffer_state_flag), }; CacheUpdate::New( hash, @@ -967,6 +1112,7 @@ impl EguiSoftwareRenderInner { fn render_prims_to_cache( &self, cached_primitives: &mut HashMap, + buffer_state_flag: BufferStateFlag, paint_jobs: Vec, pixels_per_point: f32, f: F, @@ -988,6 +1134,7 @@ impl EguiSoftwareRenderInner { .map(|(prim_idx, paint_job)| { self.prim_prepare_update( cached_primitives, + buffer_state_flag, pixels_per_point, prim_idx as u32, paint_job, @@ -998,9 +1145,8 @@ impl EguiSoftwareRenderInner { updates.into_iter().for_each(|update| match update { CacheUpdate::CacheReuse(hash, cache_reuse) => { - if let Some(cached_primitive) = cached_primitives.get_mut(&hash) { - *cached_primitive.deref_mut() = cache_reuse; - } + let cached_primitive = cached_primitives.get_mut(&hash).expect("existing hash"); + *cached_primitive.deref_mut() = cache_reuse; } CacheUpdate::New(hash, prim) => { cached_primitives.insert(hash, prim); @@ -1016,26 +1162,26 @@ impl EguiSoftwareRenderInner { fn render_from_meshcache( &self, - cached_primitives: &HashMap, + sorted_prim_cache: &[&MeshCachedPrimitive], direct_draw_buffer: &mut BufferMutRef, dirty_rect: DirtyRect, + is_new_zeroed: bool, ) { #[cfg(feature = "raster_stats")] let start = std::time::Instant::now(); - match self.caching { - SoftwareRenderCaching::MeshTiled => { - for &dirty_rect in self.dirty_rects.iter() { - direct_draw_buffer.clear_rect(dirty_rect) + if !is_new_zeroed { + match self.caching { + SoftwareRenderCaching::MeshTiled => { + for &dirty_rect in self.dirty_rects.iter() { + direct_draw_buffer.clear_rect(dirty_rect) + } } + SoftwareRenderCaching::Mesh => direct_draw_buffer.clear_rect(dirty_rect), + _ => unreachable!(), } - SoftwareRenderCaching::Mesh => direct_draw_buffer.clear_rect(dirty_rect), - _ => unreachable!(), } - let mut sorted_prim_cache = cached_primitives.values().collect::>(); - sorted_prim_cache.sort_unstable_by_key(|prim| prim.inner.z_order); - let mut render_from_meshcache_prim = |prim: &MeshCachedPrimitive, dirty_rect: DirtyRect| { let clip_rect = prim.clip_rect.intersect(dirty_rect.to_egui_rect()); let (width, height) = (prim.rect.width(), prim.rect.height()); @@ -1071,14 +1217,14 @@ impl EguiSoftwareRenderInner { match self.caching { SoftwareRenderCaching::MeshTiled => { - for &prim in &sorted_prim_cache { + for &prim in sorted_prim_cache { for dirty_rect in self.dirty_rects.intersections(prim.rect) { render_from_meshcache_prim(prim, dirty_rect); } } } SoftwareRenderCaching::Mesh => { - for &prim in &sorted_prim_cache { + for &prim in sorted_prim_cache { render_from_meshcache_prim(prim, dirty_rect); } } @@ -1092,17 +1238,16 @@ impl EguiSoftwareRenderInner { } fn render_from_tiledcache( - &mut self, - cached_primitives: &HashMap, + &self, + sorted_prim_cache: &[&TiledCachedPrimitive], canvas: &mut BufferMutRef, + _dirty_rect: DirtyRect, + is_new_zeroed: bool, ) { let simd_impl = self.simd_impl; #[cfg(feature = "raster_stats")] let start = std::time::Instant::now(); - let mut sorted_prim_cache = cached_primitives.values().collect::>(); - sorted_prim_cache.sort_unstable_by_key(|prim| prim.inner.z_order); - #[cfg(feature = "rayon")] { use rayon::{ @@ -1154,6 +1299,7 @@ impl EguiSoftwareRenderInner { tile_y, full_height, canvas_row_offset, + is_new_zeroed, ); }); }); @@ -1174,12 +1320,13 @@ impl EguiSoftwareRenderInner { let full_height = canvas.height; update_canvas_tile( simd_impl, - &sorted_prim_cache, + sorted_prim_cache, canvas, tile_x, tile_y, full_height, 0, + is_new_zeroed, ); } } @@ -1194,6 +1341,7 @@ impl EguiSoftwareRenderInner { const OCCUPIED_TILE_MASK: u8 = 0b000000010; fn update_dirty_tiles( &mut self, + buffer_state_flag: BufferStateFlag, _canvas_rect: DirtyRect, cached_primitives: &HashMap, ) { @@ -1203,11 +1351,16 @@ impl EguiSoftwareRenderInner { self.dirty_tiles .resize(as_usize(self.tiles_dim[0] * self.tiles_dim[1]), 0); self.dirty_tiles.fill(0); - for prim in cached_primitives.values() { + for prim in cached_primitives + .values() + .filter(|prim| prim.seen_this_or_last_frame.is_true(buffer_state_flag)) + { for tile in &prim.occupied_tiles { let mask = &mut self.dirty_tiles [tile[0] as usize + tile[1] as usize * self.tiles_dim[0] as usize]; - if !prim.inner.seen_this_frame || prim.inner.rendered_this_frame { + if !prim.inner.seen_this_frame.is_true(buffer_state_flag) + || prim.inner.rendered_this_frame.is_true(buffer_state_flag) + { *mask |= Self::DIRTY_TILE_MASK; } *mask |= Self::OCCUPIED_TILE_MASK; @@ -1224,6 +1377,7 @@ impl EguiSoftwareRenderInner { /// that are within `canvas_rect` bounds fn update_dirty_rects( &mut self, + buffer_state_flag: BufferStateFlag, canvas_rect: DirtyRect, cached_primitives: &HashMap, ) { @@ -1234,7 +1388,7 @@ impl EguiSoftwareRenderInner { canvas_rect, cached_primitives .values() - .filter(|prim| !prim.inner.seen_this_frame || prim.inner.rendered_this_frame) + .filter(|prim| prim.changed_this_frame(buffer_state_flag)) .map(|prim| prim.rect), ); } @@ -1250,6 +1404,7 @@ impl EguiSoftwareRenderInner { /// Returns a dirty rect that is within `canvas_rect` bounds fn update_dirty_rect

( &mut self, + buffer_state_flag: BufferStateFlag, canvas_rect: DirtyRect, cached_primitives: &HashMap, ) -> DirtyRect @@ -1260,15 +1415,15 @@ impl EguiSoftwareRenderInner { let start = std::time::Instant::now(); let mut dirty_rect = DirtyRect::new_empty(); - for prim in cached_primitives.values() { - let prim = prim.deref(); - if !prim.seen_this_frame || prim.rendered_this_frame { - if let Some(prim_rect) = prim.rect.intersection(canvas_rect) { - if dirty_rect.is_empty() { - dirty_rect = prim_rect; - } else { - dirty_rect = dirty_rect.union(prim_rect) - } + for prim in cached_primitives + .values() + .filter(|prim| prim.changed_this_frame(buffer_state_flag)) + { + if let Some(prim_rect) = prim.rect.intersection(canvas_rect) { + if dirty_rect.is_empty() { + dirty_rect = prim_rect; + } else { + dirty_rect = dirty_rect.union(prim_rect) } } } @@ -1343,6 +1498,7 @@ fn update_canvas_tile( tile_y: u32, full_height: u32, canvas_row_offset: u32, + is_new_zeroed: bool, ) { let tile_x_start = tile_x * TILE_SIZE; let tile_y_start = tile_y * TILE_SIZE; @@ -1350,11 +1506,13 @@ fn update_canvas_tile( let tile_y_end = (tile_y_start + TILE_SIZE).min(full_height); // clear tile - for y in (tile_y_start - canvas_row_offset)..(tile_y_end - canvas_row_offset) { - let row_start = y * canvas.width; - let start = row_start + tile_x_start; - let end = row_start + tile_x_end; - canvas.data[as_usize(start)..as_usize(end)].fill([0; 4]); + if !is_new_zeroed { + canvas.clear_rect(DirtyRect { + min_x: tile_x_start, + min_y: (tile_y_start - canvas_row_offset), + max_x: tile_x_end, + max_y: (tile_y_end - canvas_row_offset), + }); } let tile_n = [tile_x as u16, tile_y as u16]; @@ -1417,12 +1575,80 @@ enum CacheUpdate

{ None, } +#[derive(Debug, Clone, Copy)] +struct BufferFlags { + flags: u8, // up to Buffer #8 +} + +#[derive(Debug, Clone, Copy)] +struct BufferStateFlag { + flag: u8, +} + +impl BufferState { + #[inline(always)] + const fn as_flag(self) -> BufferStateFlag { + BufferStateFlag { + flag: match self { + BufferState::AlwaysZeroed => 1, + BufferState::Buffer1Zeroed | BufferState::Buffer1Incremental => 1, + BufferState::Buffer2Zeroed | BufferState::Buffer2Incremental => 2, + BufferState::Buffer3Zeroed | BufferState::Buffer3Incremental => 4, + }, + } + } +} + +impl BufferFlags { + #[inline(always)] + const fn new() -> Self { + Self { flags: 0 } + } + + #[inline(always)] + const fn all_false(&self) -> bool { + self.flags == 0 + } + + #[inline(always)] + const fn is_true(&self, buffer_state: BufferStateFlag) -> bool { + self.flags & buffer_state.flag != 0 + } + + #[inline(always)] + const fn unmark(&mut self, buffer_state: BufferStateFlag) { + self.flags &= !buffer_state.flag; + } + + #[inline(always)] + const fn marked(self, buffer_state: BufferStateFlag) -> Self { + Self { + flags: self.flags | buffer_state.flag, + } + } + #[inline(always)] + const fn unmarked(self, buffer_state: BufferStateFlag) -> Self { + Self { + flags: self.flags & !buffer_state.flag, + } + } +} + /// Common fields to both cached rendering modes struct CacheReuse { z_order: u32, rect: DirtyRect, - seen_this_frame: bool, - rendered_this_frame: bool, + seen_this_or_last_frame: BufferFlags, + seen_this_frame: BufferFlags, + rendered_this_frame: BufferFlags, +} + +impl CacheReuse { + const fn changed_this_frame(&self, buffer_state_flag: BufferStateFlag) -> bool { + self.seen_this_or_last_frame.is_true(buffer_state_flag) + && (!self.seen_this_frame.is_true(buffer_state_flag) + || self.rendered_this_frame.is_true(buffer_state_flag)) + } } /// A region of cached mesh data that corresponds to a ClippedPrimitive. @@ -1651,3 +1877,98 @@ fn draw_rect_border_f32( } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ages_up_to_1() { + let mut ages = BufferStates::new(); + if cfg!(any(target_os = "macos", target_os = "android")) { + assert_eq!(ages.next(0, 10), BufferState::AlwaysZeroed); + return; + } + assert_eq!(ages.next(0, 10), BufferState::Buffer3Zeroed); + assert_eq!(ages.next(1, 10), BufferState::Buffer3Incremental); + assert_eq!(ages.next(1, 10), BufferState::Buffer3Incremental); + assert_eq!(ages.next(0, 10), BufferState::Buffer2Zeroed); + assert_eq!(ages.next(1, 10), BufferState::Buffer2Incremental); + assert_eq!(ages.next(1, 10), BufferState::Buffer2Incremental); + assert_eq!(ages.next(0, 10), BufferState::Buffer1Zeroed); + assert_eq!(ages.next(1, 10), BufferState::Buffer1Incremental); + assert_eq!(ages.next(1, 10), BufferState::Buffer1Incremental); + assert_eq!(ages.next(0, 10), BufferState::Buffer3Zeroed); + assert_eq!(ages.next(1, 10), BufferState::Buffer3Incremental); + assert_eq!(ages.next(1, 10), BufferState::Buffer3Incremental); + } + + #[test] + fn ages_up_to_2() { + let mut ages = BufferStates::new(); + if cfg!(any(target_os = "macos", target_os = "android")) { + assert_eq!(ages.next(0, 10), BufferState::AlwaysZeroed); + return; + } + assert_eq!(ages.next(0, 10), BufferState::Buffer3Zeroed); + assert_eq!(ages.next(0, 10), BufferState::Buffer2Zeroed); + assert_eq!(ages.next(2, 10), BufferState::Buffer3Incremental); + assert_eq!(ages.next(2, 10), BufferState::Buffer2Incremental); + assert_eq!(ages.next(2, 10), BufferState::Buffer3Incremental); + assert_eq!(ages.next(2, 10), BufferState::Buffer2Incremental); + assert_eq!(ages.next(2, 10), BufferState::Buffer3Incremental); + assert_eq!(ages.next(2, 10), BufferState::Buffer2Incremental); + + assert_eq!(ages.next(0, 10), BufferState::Buffer1Zeroed); + assert_eq!(ages.next(0, 10), BufferState::Buffer3Zeroed); + assert_eq!(ages.next(2, 10), BufferState::Buffer1Incremental); + assert_eq!(ages.next(2, 10), BufferState::Buffer3Incremental); + assert_eq!(ages.next(2, 10), BufferState::Buffer1Incremental); + assert_eq!(ages.next(2, 10), BufferState::Buffer3Incremental); + assert_eq!(ages.next(2, 10), BufferState::Buffer1Incremental); + assert_eq!(ages.next(2, 10), BufferState::Buffer3Incremental); + + assert_eq!(ages.next(0, 10), BufferState::Buffer2Zeroed); + assert_eq!(ages.next(0, 10), BufferState::Buffer1Zeroed); + assert_eq!(ages.next(2, 10), BufferState::Buffer2Incremental); + assert_eq!(ages.next(2, 10), BufferState::Buffer1Incremental); + assert_eq!(ages.next(2, 10), BufferState::Buffer2Incremental); + assert_eq!(ages.next(2, 10), BufferState::Buffer1Incremental); + assert_eq!(ages.next(2, 10), BufferState::Buffer2Incremental); + assert_eq!(ages.next(2, 10), BufferState::Buffer1Incremental); + } + + #[test] + fn ages_up_to_3() { + let mut ages = BufferStates::new(); + if cfg!(any(target_os = "macos", target_os = "android")) { + assert_eq!(ages.next(0, 10), BufferState::AlwaysZeroed); + return; + } + assert_eq!(ages.next(0, 10), BufferState::Buffer3Zeroed); + assert_eq!(ages.next(0, 10), BufferState::Buffer2Zeroed); + assert_eq!(ages.next(0, 10), BufferState::Buffer1Zeroed); + assert_eq!(ages.next(3, 10), BufferState::Buffer3Incremental); + assert_eq!(ages.next(3, 10), BufferState::Buffer2Incremental); + assert_eq!(ages.next(3, 10), BufferState::Buffer1Incremental); + assert_eq!(ages.next(3, 10), BufferState::Buffer3Incremental); + assert_eq!(ages.next(3, 10), BufferState::Buffer2Incremental); + assert_eq!(ages.next(3, 10), BufferState::Buffer1Incremental); + assert_eq!(ages.next(3, 10), BufferState::Buffer3Incremental); + assert_eq!(ages.next(3, 10), BufferState::Buffer2Incremental); + assert_eq!(ages.next(3, 10), BufferState::Buffer1Incremental); + + assert_eq!(ages.next(0, 10), BufferState::Buffer3Zeroed); + assert_eq!(ages.next(0, 10), BufferState::Buffer2Zeroed); + assert_eq!(ages.next(0, 10), BufferState::Buffer1Zeroed); + assert_eq!(ages.next(3, 10), BufferState::Buffer3Incremental); + assert_eq!(ages.next(3, 10), BufferState::Buffer2Incremental); + assert_eq!(ages.next(3, 10), BufferState::Buffer1Incremental); + assert_eq!(ages.next(3, 10), BufferState::Buffer3Incremental); + assert_eq!(ages.next(3, 10), BufferState::Buffer2Incremental); + assert_eq!(ages.next(3, 10), BufferState::Buffer1Incremental); + assert_eq!(ages.next(3, 10), BufferState::Buffer3Incremental); + assert_eq!(ages.next(3, 10), BufferState::Buffer2Incremental); + assert_eq!(ages.next(3, 10), BufferState::Buffer1Incremental); + } +} diff --git a/src/test_render.rs b/src/test_render.rs index 13625ac..28a844f 100644 --- a/src/test_render.rs +++ b/src/test_render.rs @@ -3,9 +3,41 @@ use egui::TexturesDelta; use egui_kittest::TestRenderer; use image::ImageBuffer; -use crate::{BufferMutRef, EguiSoftwareRenderCanvas}; +use crate::{BufferMutRef, BufferState, BufferStates, EguiSoftwareRender}; -impl TestRenderer for EguiSoftwareRenderCanvas { +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EguiSoftwareTestRenderMode { + AlwaysNewZeroed, + SimpleBuffering, + DoubleBuffering, + TripleBuffeing, +} + +pub struct EguiSoftwareTestRender { + mode: EguiSoftwareTestRenderMode, + buffer_states: BufferStates, + buffer1: Vec<[u8; 4]>, + buffer2: Vec<[u8; 4]>, + buffer3: Vec<[u8; 4]>, + counter: usize, + renderer: EguiSoftwareRender, +} + +impl EguiSoftwareTestRender { + pub fn new(mode: EguiSoftwareTestRenderMode, renderer: EguiSoftwareRender) -> Self { + Self { + mode, + buffer_states: BufferStates::new(), + buffer1: Vec::new(), + buffer2: Vec::new(), + buffer3: Vec::new(), + counter: 0, + renderer, + } + } +} + +impl TestRenderer for EguiSoftwareTestRender { fn handle_delta(&mut self, delta: &TexturesDelta) { self.renderer.inner.set_textures(delta); self.renderer.inner.free_textures(delta); @@ -20,13 +52,36 @@ impl TestRenderer for EguiSoftwareRenderCanvas { let width = (ctx.content_rect().width() * output.pixels_per_point) as u32; let height = (ctx.content_rect().height() * output.pixels_per_point) as u32; + let len = crate::as_usize(width * height); + let age = match self.mode { + EguiSoftwareTestRenderMode::SimpleBuffering if self.counter >= 1 => 1, + EguiSoftwareTestRenderMode::DoubleBuffering if self.counter >= 2 => 2, + EguiSoftwareTestRenderMode::TripleBuffeing if self.counter >= 3 => 3, + _ => 0, + }; + let buffer_state = match self.mode { + EguiSoftwareTestRenderMode::AlwaysNewZeroed => BufferState::AlwaysZeroed, + _ => self.buffer_states.next(age, len), + }; - let mut buffer = vec![[0u8; 4]; crate::as_usize(width * height)]; - - let mut buffer_ref = BufferMutRef::new(&mut buffer, width, height); - - self.render( + let buffer = match buffer_state { + BufferState::AlwaysZeroed + | BufferState::Buffer1Zeroed + | BufferState::Buffer1Incremental => &mut self.buffer1, + BufferState::Buffer2Zeroed | BufferState::Buffer2Incremental => &mut self.buffer2, + BufferState::Buffer3Zeroed | BufferState::Buffer3Incremental => &mut self.buffer3, + }; + if buffer.len() != len { + assert!(buffer_state.is_new_zeroed()); + *buffer = vec![[0u8; 4]; len]; + } else if buffer_state.is_new_zeroed() { + buffer.fill(Default::default()); + } + let mut buffer_ref = BufferMutRef::new(buffer, width, height); + self.counter += 1; + self.renderer.render( &mut buffer_ref, + buffer_state, paint_jobs, &output.textures_delta, output.pixels_per_point, diff --git a/src/winit.rs b/src/winit.rs index 429974c..8aefc31 100644 --- a/src/winit.rs +++ b/src/winit.rs @@ -1,4 +1,6 @@ -use crate::{BufferMutRef, ColorFieldOrder, EguiSoftwareRender, SoftwareRenderCaching}; +use crate::{ + BufferMutRef, BufferStates, ColorFieldOrder, EguiSoftwareRender, SoftwareRenderCaching, +}; use egui::{ Context, CursorGrab, IconData, Pos2, SystemTheme, Vec2, ViewportBuilder, ViewportCommand, WindowLevel, X11WindowType, @@ -143,6 +145,7 @@ struct RunningEguiAppState EguiA /////////////////// END OF DANGER ZONE////////////////////////////////////// config: SoftwareBackendAppConfiguration, last_frame_time: Option, + buffer_states: BufferStates, renderer: EguiSoftwareRender, egui_app_factory: EguiAppFactory, softbuffer_context: softbuffer::Context, @@ -230,6 +233,7 @@ impl EguiApp> egui_app_factory: self.egui_app_factory, softbuffer_context: self.softbuffer_context, window: self.window, + buffer_states: BufferStates::new(), surface, egui_winit, egui_app, @@ -698,17 +702,21 @@ impl EguiApp> .map_err(SoftwareBackendAppError::soft_buffer( "softbuffer::Surface::buffer_mut", ))?; - + let age = buffer.age(); let buffer_ref = &mut BufferMutRef::new( bytemuck::cast_slice_mut(&mut buffer), width.get(), height.get(), ); - let redraw_everything_this_frame = - self.renderer.cached_size() != (buffer_ref.width, buffer_ref.height); + let buffer_state = self.buffer_states.next(age, buffer_ref.data.len()); + if buffer_state.is_new_zeroed() { + // age == 0 || resized + buffer_ref.data.fill(Default::default()); + } + let dirty_rect = self.renderer.render( buffer_ref, - redraw_everything_this_frame, + buffer_state, clipped_primitives, &full_output.textures_delta, full_output.pixels_per_point, diff --git a/tests/mod.rs b/tests/mod.rs index fb073c3..6be8a97 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -1,11 +1,14 @@ #![cfg(feature = "test_render")] mod tests { + use egui::accesskit::Role; use egui::{Vec2, vec2}; + use egui_kittest::kittest::Queryable; + use egui_software_backend::test_render::{EguiSoftwareTestRender, EguiSoftwareTestRenderMode}; use egui_software_backend::{ColorFieldOrder, EguiSoftwareRender, SoftwareRenderCaching}; use image::{ImageBuffer, Rgba}; - use egui_kittest::HarnessBuilder; + use egui_kittest::{Harness, HarnessBuilder, TestRenderer}; const RESOLUTION: Vec2 = vec2(1280.0, 720.0); @@ -15,92 +18,184 @@ mod tests { // (1px for 1.0 px_per_point, 7px for 1.5 px_per_point). // Currently have some pixels that don't match perfectly due to slight rounding when px_per_point is not 1.0 pub fn compare_software_render_with_gpu() { - fn app() -> impl FnMut(&egui::Context) { - let mut egui_demo = egui_demo_lib::DemoWindows::default(); - move |ctx: &egui::Context| { - egui_demo.ui(&ctx); - - // egui::CentralPanel::default().show(ctx, |ui| { - // #[allow(const_item_mutation)] - // ui.color_edit_button_srgba(&mut egui::Color32::TRANSPARENT); - // ui.end_row(); - // }); - } - } - let _ = std::fs::create_dir("tests/tmp/"); // egui's failed_px_count_thresold default is 0 for (px_per_point, failed_px_count_thresold) in [(1.0, 8), (1.5, 15)] { // --- Render on GPU - let mut harness = HarnessBuilder::default() - .with_size(RESOLUTION) - .with_pixels_per_point(px_per_point) - .renderer(egui_kittest::LazyRenderer::default()) - .build(app()); - harness.run(); - let gpu_render_image = harness.render().unwrap(); - gpu_render_image - .save(format!("tests/tmp/gpu_px_per_point{px_per_point}.png")) - .unwrap(); + let gpu_render_images = harness_run( + app(), + egui_kittest::LazyRenderer::default(), + px_per_point, + "tests/tmp/gpu_px_per_point", + ); - for mode in [ + for caching_mode in [ SoftwareRenderCaching::Direct, - SoftwareRenderCaching::Mesh, - SoftwareRenderCaching::MeshTiled, SoftwareRenderCaching::BlendTiled, + SoftwareRenderCaching::MeshTiled, + SoftwareRenderCaching::Mesh, ] { - for allow_raster_opt in [false, true] { - for convert_tris_to_rects in [false, true] { - // --- Render on CPU - let egui_software_render = EguiSoftwareRender::new(ColorFieldOrder::Rgba) - .with_allow_raster_opt(allow_raster_opt) - .with_convert_tris_to_rects(convert_tris_to_rects) - .with_caching(mode) - .with_canvas(); - - let mut harness = HarnessBuilder::default() - .with_size(RESOLUTION) - .with_pixels_per_point(px_per_point) - .renderer(egui_software_render) - .build(app()); - harness.run(); - let cpu_render_image = harness.render().unwrap(); - - let name = format!( - "px_per_pt {px_per_point}, mode {mode:?}, raster_opt {allow_raster_opt}, tris_to_rects {convert_tris_to_rects}" - ); - - if let Some((pixels_failed, diff_image)) = dify( - &gpu_render_image, - &cpu_render_image, - 0.6, // egui's default is 0.6 - ) { - if pixels_failed > failed_px_count_thresold { - diff_image - .save(format!("tests/tmp/diff_{name} - FAIL.png")) - .unwrap(); - cpu_render_image - .save(format!("tests/tmp/cpu_{name} - FAIL.png")) - .unwrap(); - panic!("pixels_failed {pixels_failed}: {name}") - } else { - diff_image - .save(format!("tests/tmp/diff_{name}.png")) - .unwrap(); - cpu_render_image - .save(format!("tests/tmp/cpu_{name}.png")) - .unwrap(); - } - } else { - println!("excellent match, no dify diff: {name}") - }; + for buffering_mode in [ + EguiSoftwareTestRenderMode::AlwaysNewZeroed, + EguiSoftwareTestRenderMode::SimpleBuffering, + EguiSoftwareTestRenderMode::DoubleBuffering, + EguiSoftwareTestRenderMode::TripleBuffeing, + ] { + for allow_raster_opt in [false, true] { + for convert_tris_to_rects in [false, true] { + test_cpu_render( + px_per_point, + failed_px_count_thresold, + &gpu_render_images, + caching_mode, + buffering_mode, + allow_raster_opt, + convert_tris_to_rects, + ); + } } } } } } + fn test_cpu_render( + px_per_point: f32, + failed_px_count_thresold: i32, + gpu_render_images: &Vec, Vec>>, + caching_mode: SoftwareRenderCaching, + buffering_mode: EguiSoftwareTestRenderMode, + allow_raster_opt: bool, + convert_tris_to_rects: bool, + ) { + // --- Render on CPU + let egui_software_render = EguiSoftwareRender::new(ColorFieldOrder::Rgba) + .with_allow_raster_opt(allow_raster_opt) + .with_convert_tris_to_rects(convert_tris_to_rects) + .with_caching(caching_mode); + let egui_software_render = + EguiSoftwareTestRender::new(buffering_mode, egui_software_render); + + let name = format!( + "px_per_pt {}, {:?}, {:?}, raster_opt {}, tris_to_rects {}", + px_per_point, caching_mode, buffering_mode, allow_raster_opt, convert_tris_to_rects, + ); + let cpu_render_images = harness_run( + app(), + egui_software_render, + px_per_point, + &format!("tests/tmp/cpu_{name}"), + ); + + assert_eq!(gpu_render_images.len(), cpu_render_images.len()); + for (i, (gpu_render_image, cpu_render_image)) in gpu_render_images + .iter() + .zip(cpu_render_images.iter()) + .enumerate() + { + if let Some((pixels_failed, diff_image)) = dify( + &gpu_render_image, + &cpu_render_image, + 0.6, // egui's default is 0.6 + ) { + if pixels_failed > failed_px_count_thresold { + diff_image + .save(format!("tests/tmp/cpu_{name}_frame{i}_diff - FAIL.png")) + .unwrap(); + panic!("pixels_failed {pixels_failed}: {name}") + } else { + diff_image + .save(format!("tests/tmp/cpu_{name}_frame{i}_diff.png")) + .unwrap(); + } + } else { + println!("excellent match, no dify diff: {name}") + }; + } + } + + fn app() -> impl FnMut(&egui::Context) { + let mut egui_demo = egui_demo_lib::DemoWindows::default(); + let mut checked = false; + move |ctx: &egui::Context| { + if true { + egui_demo.ui(&ctx); + } else { + egui::CentralPanel::default().show(ctx, |ui| { + ui.checkbox(&mut checked, "Checkbox"); + if ui.button("✨ Misc Demos").clicked() { + checked = true; + } + if checked { + egui::Window::new("Color Test") + .current_pos((100.0, 100.0)) + .show(ctx, |ui| { + ui.label("hello"); + }); + egui::Window::new("!Checked Test") + .current_pos((200.0, 100.0)) + .show(ctx, |ui| { + ui.label("hi there"); + }); + } else { + egui::Window::new("!Checked Test") + .current_pos((150.0, 100.0)) + .show(ctx, |ui| { + ui.label("hi there"); + }); + } + }); + } + } + } + + fn harness_run( + app: impl FnMut(&egui::Context), + renderer: impl TestRenderer + 'static, + px_per_point: f32, + save_path_prefix: &str, + ) -> Vec, Vec>> { + let mut ret = Vec::new(); + let mut counter = 0; + let mut run_and_render = |harness: &mut Harness<'_>| { + harness.run(); + let gpu_render_image = harness.render().unwrap(); + gpu_render_image + .save(format!( + "{save_path_prefix}{px_per_point}_frame{counter}.png" + )) + .unwrap(); + ret.push(gpu_render_image); + counter += 1; + }; + let mut harness = HarnessBuilder::default() + .with_size(RESOLUTION) + .with_pixels_per_point(px_per_point) + .renderer(renderer) + .build(app); + run_and_render(&mut harness); + + let checkbox = harness.get_by_role_and_label(Role::Button, "✨ Misc Demos"); + checkbox.click(); + + run_and_render(&mut harness); + + //let checkbox = harness.get_by_role_and_label(Role::Button, "✨ Misc Demos"); + //checkbox.click(); + run_and_render(&mut harness); + + harness.set_size(RESOLUTION * 1.25); + + run_and_render(&mut harness); + + harness.set_size(RESOLUTION); + + run_and_render(&mut harness); + + ret + } + // Returning none indicates no diff fn dify( gpu_render_image: &ImageBuffer, Vec>, From 928717dfeb5c0ea3c168ff73a0dd5bec2b2154a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Rouill=C3=A9?= Date: Tue, 3 Mar 2026 01:15:15 +0100 Subject: [PATCH 08/13] Fix doctest --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index b5f3ba7..2a13e1f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,7 @@ //! //!sw_render.render( //! &mut buffer_ref, -//! BufferState::AlwaysNewZeroed, +//! BufferState::AlwaysZeroed, //! primitives, //! &out.textures_delta, //! out.pixels_per_point, From 79d4190bb119e8d1ca664dee71a2bac863b82392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Rouill=C3=A9?= Date: Fri, 6 Mar 2026 21:26:59 +0100 Subject: [PATCH 09/13] Fix clippy warnings --- examples/winit.rs | 12 ++++++------ src/lib.rs | 30 +++++++++++++++--------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/examples/winit.rs b/examples/winit.rs index 4beba83..b579945 100644 --- a/examples/winit.rs +++ b/examples/winit.rs @@ -23,15 +23,15 @@ impl EguiApp { } fn ui(&mut self, ctx: &egui::Context) { - egui::CentralPanel::default().show(ctx, |_ui| { - self.demo.ui(ctx); + //egui::CentralPanel::default().show(ctx, |_ui| { + self.demo.ui(ctx); - egui::Window::new("Color Test").show(ctx, |ui| { - egui::ScrollArea::both().auto_shrink(false).show(ui, |ui| { - self.color_test.ui(ui); - }); + egui::Window::new("Color Test").show(ctx, |ui| { + egui::ScrollArea::both().auto_shrink(false).show(ui, |ui| { + self.color_test.ui(ui); }); }); + //}); } } diff --git a/src/lib.rs b/src/lib.rs index 2a13e1f..cef3a28 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,10 +75,7 @@ extern crate alloc; #[cfg(feature = "std")] extern crate std; -use core::{ - ops::{Deref, DerefMut, Range}, - u8, -}; +use core::ops::{Deref, DerefMut, Range}; use alloc::{borrow::Cow, vec, vec::Vec}; @@ -193,6 +190,12 @@ pub struct BufferStates { frame_3: (BufferState, usize), } +impl Default for BufferStates { + fn default() -> Self { + Self::new() + } +} + impl BufferStates { pub const fn new() -> Self { Self { @@ -205,10 +208,10 @@ impl BufferStates { /// Get the next buffer state /// /// * `age` is the number of frames ago this buffer was last presented (up to 3). - /// So if the value is 1, it is the same as the last frame, - /// and if it is 2, it is the same as the frame before that (for backends using double buffering), - /// and if it is 3, it is the same as the frame before before that (for backends using triple buffering), - /// If the value is 0, it is a new buffer. + /// So if the value is 1, it is the same as the last frame, + /// and if it is 2, it is the same as the frame before that (for backends using double buffering), + /// and if it is 3, it is the same as the frame before before that (for backends using triple buffering), + /// If the value is 0, it is a new buffer. /// /// * `len` is the buffer size, if it differs the content will be marked as zeroed /// @@ -551,9 +554,7 @@ impl EguiSoftwareRender { textures_delta: &egui::TexturesDelta, pixels_per_point: f32, ) -> DirtyRect { - // TODO: need to deal with user textures. Either make the fields of EguiUserTextures pub or need to come up with a replacement. - - let dirty_rect = self.inner.render_tiled_impl( + self.inner.render_tiled_impl( &mut self.tiledcached_primitives, buffer_ref, buffer_state, @@ -563,8 +564,7 @@ impl EguiSoftwareRender { EguiSoftwareRenderInner::render_prim, EguiSoftwareRenderInner::update_dirty_tiles, EguiSoftwareRenderInner::render_from_tiledcache, - ); - dirty_rect + ) } fn render_meshmaybetiled( &mut self, @@ -612,8 +612,6 @@ impl EguiSoftwareRenderInner { U: Fn(&mut Self, BufferStateFlag, DirtyRect, &HashMap), R: Fn(&Self, &[&P], &mut BufferMutRef, DirtyRect, bool), { - // TODO: need to deal with user textures. Either make the fields of EguiUserTextures pub or need to come up with a replacement. - assert!(buffer_ref.width > 0); assert!(buffer_ref.height > 0); assert!(pixels_per_point > 0.0); @@ -637,6 +635,7 @@ impl EguiSoftwareRenderInner { buffer_ref.height.div_ceil(TILE_SIZE), ]; + // TODO: need to deal with user textures. Either make the fields of EguiUserTextures pub or need to come up with a replacement. self.set_textures(textures_delta); self.render_prims_to_cache( @@ -1490,6 +1489,7 @@ impl EguiSoftwareRenderInner { } } +#[allow(clippy::too_many_arguments)] fn update_canvas_tile( simd_impl: AvailableImpl, sorted_prim_cache: &[&TiledCachedPrimitive], From d5c5853118997ae437abf36419a6857eeabc437c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Rouill=C3=A9?= Date: Fri, 6 Mar 2026 22:17:49 +0100 Subject: [PATCH 10/13] 2 new internal single buffering modes AlwaysBlit and AlwaysBlend --- Cargo.toml | 3 +- src/lib.rs | 316 ++++++++++++--------------------------------- src/test_render.rs | 9 +- tests/mod.rs | 2 +- 4 files changed, 89 insertions(+), 241 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b44da62..46ce2bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ constify = { path = "constify", version = "0.0.1"} rayon = { version = "1.11.0", optional = true } log = { version = "0.4.28", optional = true } winit = {version = "0.30", optional = true } +# When upgrading `softbuffer` always check if macos or android still can't use buffered modes softbuffer = { version = "0.4", optional = true } egui-winit = { version = "0.33", default-features = false, optional = true} bytemuck = { version = "1.23", optional = true } @@ -34,7 +35,7 @@ egui_extras = { version = "0.33", features = ["all_loaders"] } egui-winit = { version = "0.33", default-features = false } epaint_default_fonts = "0.33" -softbuffer = { version = "0.4" } +softbuffer = { version = "0.4" } image = { version = "0.25", features = ["jpeg", "png"] } winit = { version = "0.30" } bytemuck = { version = "1.23" } diff --git a/src/lib.rs b/src/lib.rs index cef3a28..89db2cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -168,7 +168,9 @@ struct EguiSoftwareRenderInner { textures: HashMap, /// Tiles grid size (cols, rows) tiles_dim: [u32; 2], + /// dirty tiles for [`SoftwareRenderCaching::BlendTiled`] dirty_tiles: Vec, + /// dirty rects for [`SoftwareRenderCaching::MeshTiled`] dirty_rects: ComputeTiledDirtyRects, output_field_order: ColorFieldOrder, convert_tris_to_rects: bool, @@ -219,7 +221,7 @@ impl BufferStates { /// a zeroed variant! pub fn next(&mut self, age: u8, buffer_len: usize) -> BufferState { if cfg!(any(target_os = "macos", target_os = "android")) { - return BufferState::AlwaysZeroed; + return BufferState::AlwaysBlit; } if age == 1 { // will present last frame @@ -247,9 +249,15 @@ impl BufferStates { /// Decribe the state of the provided buffer before rendering #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BufferState { - /// The provided buffer will always be a new buffer filled with zeroes - /// This allows the renderer to know when to cache the last frame itself - AlwaysZeroed, + /// The provided buffer will always be a new buffer with unspecified contents. + /// The rendered will do single buffering internally and __blit__ (ie. memcpy) the cached content + /// to the provided buffer. + AlwaysBlit, + + /// The provided buffer will always be a new buffer with unspecified contents. + /// The rendered will do single buffering internally and __blend__ the cached content + /// to the provided buffer. + AlwaysBlend, /// A new Buffer identified as #1, filled with zeroes Buffer1Zeroed, @@ -271,7 +279,8 @@ impl BufferState { #[inline] pub const fn is_new_zeroed(self) -> bool { match self { - BufferState::AlwaysZeroed + BufferState::AlwaysBlit + | BufferState::AlwaysBlend | BufferState::Buffer1Zeroed | BufferState::Buffer2Zeroed | BufferState::Buffer3Zeroed => true, @@ -283,7 +292,8 @@ impl BufferState { pub fn to_incremental(self) -> Self { match self { - Self::AlwaysZeroed => Self::AlwaysZeroed, + Self::AlwaysBlit => Self::AlwaysBlit, + Self::AlwaysBlend => Self::AlwaysBlend, Self::Buffer1Zeroed | Self::Buffer1Incremental => Self::Buffer1Incremental, Self::Buffer2Zeroed | Self::Buffer2Incremental => Self::Buffer2Incremental, Self::Buffer3Zeroed | Self::Buffer3Incremental => Self::Buffer3Incremental, @@ -292,7 +302,8 @@ impl BufferState { pub fn to_new_zeroed(self) -> Self { match self { - Self::AlwaysZeroed => Self::AlwaysZeroed, + Self::AlwaysBlit => Self::AlwaysBlit, + Self::AlwaysBlend => Self::AlwaysBlend, Self::Buffer1Zeroed | Self::Buffer1Incremental => Self::Buffer1Zeroed, Self::Buffer2Zeroed | Self::Buffer2Incremental => Self::Buffer2Zeroed, Self::Buffer3Zeroed | Self::Buffer3Incremental => Self::Buffer3Zeroed, @@ -302,97 +313,13 @@ impl BufferState { /// Software render backend for egui. pub struct EguiSoftwareRender { + /// Cache for [`SoftwareRenderCaching::BlendTiled`] tiledcached_primitives: HashMap, + /// Cache for [`SoftwareRenderCaching::MeshTiled`] or [`SoftwareRenderCaching::Mesh`] dirtycached_primitives: HashMap, - inner: EguiSoftwareRenderInner, -} - -/// Software render backend for egui with managed canvas. -pub struct EguiSoftwareRenderCanvas { + /// Internal single buffering for [`BufferState::AlwaysBlit`] or [`BufferState::AlwaysBlend`] canvas: Vec<[u8; 4]>, - renderer: EguiSoftwareRender, -} - -impl Deref for EguiSoftwareRenderCanvas { - type Target = EguiSoftwareRender; - - fn deref(&self) -> &Self::Target { - &self.renderer - } -} - -impl DerefMut for EguiSoftwareRenderCanvas { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.renderer - } -} - -#[inline] -fn blit_rect( - simd_impl: impl SelectedImpl, - canvas: &BufferMutRef, - buffer: &mut BufferMutRef, - rect: DirtyRect, - canvas_row_offset: u32, -) { - for y in rect.min_y..rect.max_y { - let src_row = canvas.get_span(rect.min_x, rect.max_x, y + canvas_row_offset); - let dst_row = &mut buffer.get_mut_span(rect.min_x, rect.max_x, y); - - simd_impl.egui_blend_u8_slice(src_row, dst_row) - } -} - -impl EguiSoftwareRenderCanvas { - pub fn render( - &mut self, - buffer_ref: &mut BufferMutRef, - paint_jobs: Vec, - textures_delta: &egui::TexturesDelta, - pixels_per_point: f32, - ) { - if self.renderer.inner.caching == SoftwareRenderCaching::Direct { - self.renderer.render( - buffer_ref, - BufferState::AlwaysZeroed, - paint_jobs, - textures_delta, - pixels_per_point, - ); - } else { - let len = as_usize(buffer_ref.width) * as_usize(buffer_ref.height); - let redraw_everything_this_frame = self.canvas.len() != len; - if redraw_everything_this_frame { - self.canvas.clear(); - let len = as_usize(buffer_ref.width) * as_usize(buffer_ref.height); - self.canvas.resize(len, [0; 4]); - // ^ data is now cleared in a singled memset call - } - let simd_impl = self.inner.simd_impl; - let mut canvas = - BufferMutRef::new(&mut self.canvas, buffer_ref.width, buffer_ref.height); - let dirty_rect = self.renderer.render( - &mut canvas, - if redraw_everything_this_frame { - BufferState::Buffer1Zeroed - } else { - BufferState::Buffer1Incremental - }, - paint_jobs, - textures_delta, - pixels_per_point, - ); - if self.renderer.inner.caching == SoftwareRenderCaching::BlendTiled { - self.renderer - .inner - .blit_to_buffer_from_tiledcanvas(simd_impl, &canvas, buffer_ref); - } else { - dispatch_simd_impl!(simd_impl, |simd_impl| blit_rect( - simd_impl, &canvas, buffer_ref, dirty_rect, 0 - )); - } - } - } + inner: EguiSoftwareRenderInner, } impl EguiSoftwareRender { @@ -403,6 +330,7 @@ impl EguiSoftwareRender { EguiSoftwareRender { tiledcached_primitives: Default::default(), dirtycached_primitives: Default::default(), + canvas: Vec::new(), inner: EguiSoftwareRenderInner { textures: Default::default(), tiles_dim: Default::default(), @@ -441,13 +369,6 @@ impl EguiSoftwareRender { self } - pub fn with_canvas(self) -> EguiSoftwareRenderCanvas { - EguiSoftwareRenderCanvas { - canvas: Vec::new(), - renderer: self, - } - } - #[cfg(feature = "raster_stats")] pub(crate) fn stats(&self) -> &RenderStats { &self.inner.stats @@ -512,10 +433,36 @@ impl EguiSoftwareRender { ) -> DirtyRect { #[cfg(feature = "raster_stats")] self.inner.stats.clear(); - match self.inner.caching { + + let use_internal_buffer = matches!( + buffer_state, + BufferState::AlwaysBlend | BufferState::AlwaysBlit + ); + let mut internal_canvas = use_internal_buffer.then(|| { + let len = as_usize(buffer_ref.width * buffer_ref.height); + let mut canvas = std::mem::take(&mut self.canvas); + //^ take the canvas so we can satisfy borrow checker without another struct + let redraw_everything_this_frame = canvas.len() != len; + if redraw_everything_this_frame { + canvas.clear(); + canvas.resize(len, [0; 4]); + // ^ data is now cleared in a single memset call + } + canvas + }); + let render_data = match &mut internal_canvas { + Some(canvas) => canvas, + None => &mut *buffer_ref.data, + }; + let render_buffer = &mut BufferMutRef { + data: render_data, + ..*buffer_ref + }; + + let dirty_rect = match self.inner.caching { SoftwareRenderCaching::Direct => { self.inner.render_direct( - buffer_ref, + render_buffer, buffer_state, paint_jobs, textures_delta, @@ -524,26 +471,47 @@ impl EguiSoftwareRender { DirtyRect { min_x: 0, min_y: 0, - max_x: buffer_ref.width, - max_y: buffer_ref.height, + max_x: render_buffer.width, + max_y: render_buffer.height, } } SoftwareRenderCaching::MeshTiled | SoftwareRenderCaching::Mesh => self .render_meshmaybetiled( - buffer_ref, + render_buffer, buffer_state, paint_jobs, textures_delta, pixels_per_point, ), SoftwareRenderCaching::BlendTiled => self.render_blendtiled( - buffer_ref, + render_buffer, buffer_state, paint_jobs, textures_delta, pixels_per_point, ), + }; + + if let Some(canvas) = internal_canvas { + let src = &canvas; + let dst = &mut buffer_ref.data[..src.len()]; + match buffer_state { + BufferState::AlwaysBlit => { + // memcpy + dst.copy_from_slice(src); + } + BufferState::AlwaysBlend => { + dispatch_simd_impl!(self.inner.simd_impl, |simd_impl| simd_impl + .egui_blend_u8_slice(src, dst)); + } + _ => unreachable!(), + } + + self.canvas = canvas; + //^ give the canvas back } + + dirty_rect } fn render_blendtiled( @@ -691,130 +659,6 @@ impl EguiSoftwareRenderInner { dirty_rect } - /// Draw canvas alpha over given buffer. - /// Only run after EguiSoftwareRender::render() with TiledCacheing to run both. - /// Only writes tile regions that contain pixels that are not fully transparent. - fn blit_to_buffer_from_tiledcanvas( - &self, - simd_impl: AvailableImpl, - canvas: &BufferMutRef, - buffer: &mut BufferMutRef, - ) { - #[cfg(feature = "raster_stats")] - let start = std::time::Instant::now(); - - // Simple tile-less version - // buffer.data.iter_mut().zip(self.canvas.iter()).for_each(|(pixel, src)| { - // *pixel = egui_blend_u8(*src, *pixel); - // }); - - if canvas.data.is_empty() { - #[cfg(feature = "log")] - log::error!( - "Canvas not initialized, call EguiSoftwareRender::blit_canvas_to_buffer() only after EguiSoftwareRender::render_to_canvas()" - ); - return; - } - - let width = canvas.width; - let height = canvas.height; - assert_eq!(canvas.data.len(), as_usize(width * height)); - assert_eq!(buffer.data.len(), as_usize(width * height)); - - let tiles_x = self.tiles_dim[0]; - - #[cfg(feature = "rayon")] - { - use rayon::{ - iter::{IndexedParallelIterator, ParallelIterator}, - slice::ParallelSliceMut, - }; - // blit rows of tiles in parallel - - let width = buffer.width; - let px_per_row_of_tiles = as_usize(width * TILE_SIZE); - - buffer - .data - .par_chunks_mut(px_per_row_of_tiles) - .enumerate() - .for_each(|(tile_row, tile_height_row)| { - let tile_row = tile_row as u32; - let height = tile_height_row.len() as u32 / width; // Might be less than TILE_SIZE - let buffer_tile_row = &mut BufferMutRef::new(tile_height_row, width, height); - - for (tile_idx, &mask) in self.dirty_tiles.iter().enumerate() { - if mask & EguiSoftwareRenderInner::OCCUPIED_TILE_MASK == 0 { - continue; - } - - let tile_idx = tile_idx as u32; - let tile_y = tile_idx / tiles_x; - if tile_y != tile_row { - continue; - } - - let tile_x = tile_idx % tiles_x; - - let x_start = tile_x * TILE_SIZE; - let y_start = 0; - let x_end = (x_start + TILE_SIZE).min(width); - let y_end = TILE_SIZE.min(height); - - let canvas_row_offset = tile_row * TILE_SIZE; - - dispatch_simd_impl!(simd_impl, |simd_impl| blit_rect( - simd_impl, - canvas, - buffer_tile_row, - DirtyRect { - min_x: x_start, - min_y: y_start, - max_x: x_end, - max_y: y_end, - }, - canvas_row_offset, - )); - } - }); - } - #[cfg(not(feature = "rayon"))] - { - for (tile_idx, &mask) in self.dirty_tiles.iter().enumerate() { - if mask & Self::OCCUPIED_TILE_MASK == 0 { - continue; - } - - let tile_idx = tile_idx as u32; - let tile_x = tile_idx % tiles_x; - let tile_y = tile_idx / tiles_x; - - let x_start = tile_x * TILE_SIZE; - let y_start = tile_y * TILE_SIZE; - let x_end = (x_start + TILE_SIZE).min(width); - let y_end = (y_start + TILE_SIZE).min(height); - - dispatch_simd_impl!(simd_impl, |simd_impl| blit_rect( - simd_impl, - canvas, - buffer, - DirtyRect { - min_x: x_start, - min_y: y_start, - max_x: x_end, - max_y: y_end, - }, - 0, - )); - } - } - - #[cfg(feature = "raster_stats")] - { - self.stats.blit_canvas_to_buffer.mark(start); - } - } - /// Render directly into buffer without cache. This is much slower and mainly intended for testing. fn render_direct( &mut self, @@ -1590,7 +1434,7 @@ impl BufferState { const fn as_flag(self) -> BufferStateFlag { BufferStateFlag { flag: match self { - BufferState::AlwaysZeroed => 1, + BufferState::AlwaysBlit | BufferState::AlwaysBlend => 1, BufferState::Buffer1Zeroed | BufferState::Buffer1Incremental => 1, BufferState::Buffer2Zeroed | BufferState::Buffer2Incremental => 2, BufferState::Buffer3Zeroed | BufferState::Buffer3Incremental => 4, @@ -1886,7 +1730,7 @@ mod tests { fn ages_up_to_1() { let mut ages = BufferStates::new(); if cfg!(any(target_os = "macos", target_os = "android")) { - assert_eq!(ages.next(0, 10), BufferState::AlwaysZeroed); + assert_eq!(ages.next(0, 10), BufferState::AlwaysBlit); return; } assert_eq!(ages.next(0, 10), BufferState::Buffer3Zeroed); @@ -1907,7 +1751,7 @@ mod tests { fn ages_up_to_2() { let mut ages = BufferStates::new(); if cfg!(any(target_os = "macos", target_os = "android")) { - assert_eq!(ages.next(0, 10), BufferState::AlwaysZeroed); + assert_eq!(ages.next(0, 10), BufferState::AlwaysBlit); return; } assert_eq!(ages.next(0, 10), BufferState::Buffer3Zeroed); @@ -1942,7 +1786,7 @@ mod tests { fn ages_up_to_3() { let mut ages = BufferStates::new(); if cfg!(any(target_os = "macos", target_os = "android")) { - assert_eq!(ages.next(0, 10), BufferState::AlwaysZeroed); + assert_eq!(ages.next(0, 10), BufferState::AlwaysBlit); return; } assert_eq!(ages.next(0, 10), BufferState::Buffer3Zeroed); diff --git a/src/test_render.rs b/src/test_render.rs index 28a844f..22056eb 100644 --- a/src/test_render.rs +++ b/src/test_render.rs @@ -7,7 +7,8 @@ use crate::{BufferMutRef, BufferState, BufferStates, EguiSoftwareRender}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum EguiSoftwareTestRenderMode { - AlwaysNewZeroed, + AlwaysBlit, + AlwaysBlend, SimpleBuffering, DoubleBuffering, TripleBuffeing, @@ -60,12 +61,14 @@ impl TestRenderer for EguiSoftwareTestRender { _ => 0, }; let buffer_state = match self.mode { - EguiSoftwareTestRenderMode::AlwaysNewZeroed => BufferState::AlwaysZeroed, + EguiSoftwareTestRenderMode::AlwaysBlit => BufferState::AlwaysBlit, + EguiSoftwareTestRenderMode::AlwaysBlend => BufferState::AlwaysBlend, _ => self.buffer_states.next(age, len), }; let buffer = match buffer_state { - BufferState::AlwaysZeroed + BufferState::AlwaysBlit + | BufferState::AlwaysBlend | BufferState::Buffer1Zeroed | BufferState::Buffer1Incremental => &mut self.buffer1, BufferState::Buffer2Zeroed | BufferState::Buffer2Incremental => &mut self.buffer2, diff --git a/tests/mod.rs b/tests/mod.rs index 6be8a97..bc27b67 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -37,7 +37,7 @@ mod tests { SoftwareRenderCaching::Mesh, ] { for buffering_mode in [ - EguiSoftwareTestRenderMode::AlwaysNewZeroed, + EguiSoftwareTestRenderMode::AlwaysBlit, EguiSoftwareTestRenderMode::SimpleBuffering, EguiSoftwareTestRenderMode::DoubleBuffering, EguiSoftwareTestRenderMode::TripleBuffeing, From 750651ac3e995c50521d3d1908326bb2f9167485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Rouill=C3=A9?= Date: Fri, 6 Mar 2026 22:47:47 +0100 Subject: [PATCH 11/13] Remove &'a mut renderer from SoftwareBackend --- Cargo.toml | 2 +- examples/winit.rs | 20 -------------------- examples/winit_raw.rs | 36 ++++++++++++++++++++++++------------ src/lib.rs | 12 ++++++++---- src/stats.rs | 4 ---- src/winit.rs | 32 ++++++++++---------------------- 6 files changed, 43 insertions(+), 63 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 46ce2bb..17559a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ image = { version = "0.25", features = ["jpeg", "png"] } winit = { version = "0.30" } bytemuck = { version = "1.23" } dify = "0.7" -argh = "0.1" +argh = "0.1.14" # Enable optimization in debug mode diff --git a/examples/winit.rs b/examples/winit.rs index b579945..0385fd6 100644 --- a/examples/winit.rs +++ b/examples/winit.rs @@ -1,9 +1,7 @@ -use egui::Ui; use egui::Vec2; use egui::ViewportCommand; use egui_demo_lib::ColorTest; use egui_demo_lib::DemoWindows; -use egui_software_backend::SoftwareRenderCaching; use egui_software_backend::{SoftwareBackend, SoftwareBackendAppConfiguration}; struct EguiApp { @@ -43,22 +41,6 @@ impl eframe::App for EguiApp { } } -fn software_backend_ui(backend: &mut SoftwareBackend, ui: &mut Ui) { - let old = backend.caching(); - let mut new = old; - egui::ComboBox::from_label("SoftwareRenderCaching") - .selected_text(format!("{old:?}")) - .show_ui(ui, |ui| { - ui.selectable_value(&mut new, SoftwareRenderCaching::BlendTiled, "BlendTiled"); - ui.selectable_value(&mut new, SoftwareRenderCaching::MeshTiled, "MeshTiled"); - ui.selectable_value(&mut new, SoftwareRenderCaching::Mesh, "Mesh"); - ui.selectable_value(&mut new, SoftwareRenderCaching::Direct, "Direct"); - }); - if new != old { - backend.set_caching(new); - } -} - impl egui_software_backend::App for EguiApp { fn update(&mut self, ctx: &egui::Context, backend: &mut SoftwareBackend) { egui::CentralPanel::default().show(ctx, |_ui| { @@ -69,8 +51,6 @@ impl egui_software_backend::App for EguiApp { backend.display_stats(ui); }); - egui::Window::new("Software Backend").show(ctx, |ui| software_backend_ui(backend, ui)); - if self.frame_times.len() < 100 { self.frame_times .push(backend.last_frame_time().unwrap_or_default().as_secs_f32()); diff --git a/examples/winit_raw.rs b/examples/winit_raw.rs index 021a413..3354a35 100644 --- a/examples/winit_raw.rs +++ b/examples/winit_raw.rs @@ -1,9 +1,11 @@ // Based on: https://github.com/rust-windowing/softbuffer/blob/046de9228d89369151599f3f50dc4b75bd5e522b/examples/winit.rs -use argh::FromArgs; +use argh::{FromArgValue, FromArgs}; use core::num::NonZeroU32; use egui_demo_lib::ColorTest; -use egui_software_backend::{BufferMutRef, ColorFieldOrder, EguiSoftwareRender}; +use egui_software_backend::{ + BufferMutRef, ColorFieldOrder, EguiSoftwareRender, SoftwareRenderCaching, +}; use std::rc::Rc; use std::time::Instant; use winit::event::{Event, WindowEvent}; @@ -15,7 +17,15 @@ use crate::winit_app::WinitApp; #[path = "../examples/utils/winit_app.rs"] mod winit_app; -#[derive(FromArgs, Copy, Clone)] +#[derive(FromArgValue)] +enum CachingArg { + BlendTiled, + MeshTiled, + Mesh, + Direct, +} + +#[derive(FromArgs)] /// `bevy` example struct Args { /// disable raster optimizations. Rasterize everything with triangles, always calculate vertex colors, uvs, use @@ -28,9 +38,9 @@ struct Args { #[argh(switch)] no_rect: bool, - /// render directly into buffer without cache. This is much slower and mainly intended for testing. - #[argh(switch)] - direct: bool, + /// select the caching mode, defaults to BlendTiled + #[argh(option)] + caching: Option, } struct AppState { @@ -44,14 +54,16 @@ fn main() { let mut egui_demo = egui_demo_lib::DemoWindows::default(); let mut egui_color_test = ColorTest::default(); + let caching = match args.caching { + Some(CachingArg::BlendTiled) | None => SoftwareRenderCaching::BlendTiled, + Some(CachingArg::MeshTiled) => SoftwareRenderCaching::MeshTiled, + Some(CachingArg::Mesh) => SoftwareRenderCaching::Mesh, + Some(CachingArg::Direct) => SoftwareRenderCaching::Direct, + }; let mut egui_software_render = EguiSoftwareRender::new(ColorFieldOrder::Bgra) .with_allow_raster_opt(!args.no_opt) .with_convert_tris_to_rects(!args.no_rect) - .with_caching(if args.direct { - egui_software_backend::SoftwareRenderCaching::Direct - } else { - egui_software_backend::SoftwareRenderCaching::BlendTiled - }); + .with_caching(caching); let mut buffer_states = egui_software_backend::BufferStates::new(); let event_loop: EventLoop<()> = EventLoop::new().unwrap(); @@ -188,7 +200,7 @@ fn main() { } else { let avg = (frame_times.iter().sum::() / frame_times.len() as f32) * 1000.0; - window.set_title(&format!("Frame Time {avg:.2}ms")); + window.set_title(&format!("Frame Time {avg:.2}ms - {caching:?}")); frame_times.clear(); } last_frame_time = now; diff --git a/src/lib.rs b/src/lib.rs index 89db2cb..5da7796 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,6 +76,8 @@ extern crate alloc; extern crate std; use core::ops::{Deref, DerefMut, Range}; +#[cfg(feature = "raster_stats")] +use std::sync::Arc; use alloc::{borrow::Cow, vec, vec::Vec}; @@ -179,7 +181,7 @@ struct EguiSoftwareRenderInner { caching: SoftwareRenderCaching, simd_impl: AvailableImpl, #[cfg(feature = "raster_stats")] - pub stats: RenderStats, + stats: Arc, } /// Manage single, double and triple buffering buffer states @@ -370,8 +372,8 @@ impl EguiSoftwareRender { } #[cfg(feature = "raster_stats")] - pub(crate) fn stats(&self) -> &RenderStats { - &self.inner.stats + pub(crate) fn stats(&self) -> Arc { + self.inner.stats.clone() } #[cfg(feature = "raster_stats")] @@ -432,7 +434,9 @@ impl EguiSoftwareRender { pixels_per_point: f32, ) -> DirtyRect { #[cfg(feature = "raster_stats")] - self.inner.stats.clear(); + { + self.inner.stats = Default::default(); + } let use_internal_buffer = matches!( buffer_state, diff --git a/src/stats.rs b/src/stats.rs index 8a761d9..2148f71 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -164,10 +164,6 @@ impl<'a> RasterStatsStarted<'a> { } impl RenderStats { - pub(crate) fn clear(&mut self) { - *self = RenderStats::default(); - } - #[cfg(not(feature = "rayon"))] pub(crate) fn start_raster(&self) -> RasterStatsStarted<'_> { RasterStatsStarted { diff --git a/src/winit.rs b/src/winit.rs index 8aefc31..b8b577c 100644 --- a/src/winit.rs +++ b/src/winit.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "raster_stats")] +use crate::stats::RenderStats; use crate::{ BufferMutRef, BufferStates, ColorFieldOrder, EguiSoftwareRender, SoftwareRenderCaching, }; @@ -474,7 +476,8 @@ impl EguiApp> ctx, &mut SoftwareBackend { last_frame_time: self.last_frame_time, - renderer: &mut self.renderer, + #[cfg(feature = "raster_stats")] + stats: self.renderer.stats(), }, ); @@ -786,12 +789,14 @@ impl EguiApp> /// } /// /// ``` -pub struct SoftwareBackend<'a> { +pub struct SoftwareBackend { last_frame_time: Option, - renderer: &'a mut EguiSoftwareRender, + + #[cfg(feature = "raster_stats")] + stats: Arc, } -impl<'a> SoftwareBackend<'a> { +impl SoftwareBackend { /// Returns the rendering duration of the last frame if this information is available. /// Returns none otherwise. pub fn last_frame_time(&self) -> Option { @@ -800,24 +805,7 @@ impl<'a> SoftwareBackend<'a> { #[cfg(feature = "raster_stats")] pub fn display_stats(&self, ui: &mut egui::Ui) { - self.renderer.display_stats(ui); - } - - /// Get the caching mode of the renderer - pub fn caching(&self) -> SoftwareRenderCaching { - self.renderer.caching() - } - - /// Change the caching mode of the renderer - pub fn set_caching(&mut self, caching: SoftwareRenderCaching) { - self.renderer.set_caching(caching); - } - - /// Clear cache and reclaim memory - /// - /// This will cause the next frame to redraw everything - pub fn clear_cache(&mut self) { - self.renderer.clear_cache(); + self.stats.render(ui); } } From 6c3eacbbc3ef407f3fa52c6c6108ffb4eeb44238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Rouill=C3=A9?= Date: Fri, 6 Mar 2026 22:53:33 +0100 Subject: [PATCH 12/13] fix doctest --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 5da7796..add50b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,7 @@ //! //!sw_render.render( //! &mut buffer_ref, -//! BufferState::AlwaysZeroed, +//! BufferState::AlwaysBlit, //! primitives, //! &out.textures_delta, //! out.pixels_per_point, From bfe06c7f4922e2a21105d323c012a930f4227cd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vincent=20Rouill=C3=A9?= Date: Fri, 6 Mar 2026 23:03:34 +0100 Subject: [PATCH 13/13] fix no_std build --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index add50b1..d539873 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -444,7 +444,7 @@ impl EguiSoftwareRender { ); let mut internal_canvas = use_internal_buffer.then(|| { let len = as_usize(buffer_ref.width * buffer_ref.height); - let mut canvas = std::mem::take(&mut self.canvas); + let mut canvas = core::mem::take(&mut self.canvas); //^ take the canvas so we can satisfy borrow checker without another struct let redraw_everything_this_frame = canvas.len() != len; if redraw_everything_this_frame {