Skip to content

Commit 614a624

Browse files
committed
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"))]`
1 parent e6a5f37 commit 614a624

13 files changed

Lines changed: 1412 additions & 745 deletions

File tree

README.md

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,24 @@
11
# CPU software render backend for [egui](https://github.com/emilk/egui)
2-
![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)
3-
[![Docs](https://docs.rs/egui_software_backend/badge.svg)](https://docs.rs/egui_software_backend/latest/egui_software_backend/)
42

53
![demo](demo.png)
64

75
```rs
8-
use egui_software_backend::{BufferMutRef, ColorFieldOrder, EguiSoftwareRender};
9-
let buffer = &mut vec![[0u8; 4]; 512 * 512];
10-
let mut buffer_ref = BufferMutRef::new(buffer, 512, 512);
116
let ctx = egui::Context::default();
127
let mut demo = egui_demo_lib::DemoWindows::default();
138
let mut sw_render = EguiSoftwareRender::new(ColorFieldOrder::Bgra);
149

15-
let out = ctx.run(egui::RawInput::default(), |ctx| {
10+
let out = ctx.run(raw_input, |ctx| {
1611
demo.ui(ctx);
1712
});
1813

1914
let primitives = ctx.tessellate(out.shapes, out.pixels_per_point);
2015

21-
sw_render.render(
22-
&mut buffer_ref,
23-
&primitives,
24-
&out.textures_delta,
25-
out.pixels_per_point,
26-
);
16+
sw_render.render(buffer, &primitives, &out.textures_delta, out.pixels_per_point);
2717
```
2818

2919
## winit quickstart
3020
```rust
31-
use egui::vec2;
21+
use egui::Vec2;
3222
use egui_software_backend::{SoftwareBackend, SoftwareBackendAppConfiguration};
3323

3424
struct EguiApp {}
@@ -50,7 +40,8 @@ impl egui_software_backend::App for EguiApp {
5040

5141
fn main() {
5242
let settings = SoftwareBackendAppConfiguration::new()
53-
.inner_size(Some(vec2(500.0, 300.0)))
43+
.inner_size(Some(Vec2::new(500f32, 300f32)))
44+
.resizable(Some(false))
5445
.title(Some("Simple example".to_string()));
5546

5647
egui_software_backend::run_app_with_software_backend(settings, EguiApp::new)
@@ -62,4 +53,4 @@ fn main() {
6253
[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.
6354

6455
## Other examples
65-
- bevy + softbuffer see examples/bevy_example folder
56+
- bevy + softbuffer see examples/bevy_example folder

examples/winit.rs

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
use egui::Ui;
12
use egui::Vec2;
23
use egui::ViewportCommand;
34
use egui_demo_lib::ColorTest;
45
use egui_demo_lib::DemoWindows;
6+
use egui_software_backend::SoftwareRenderCaching;
57
use egui_software_backend::{SoftwareBackend, SoftwareBackendAppConfiguration};
68

79
struct EguiApp {
@@ -19,12 +21,8 @@ impl EguiApp {
1921
frame_times: Vec::new(),
2022
}
2123
}
22-
}
23-
24-
impl egui_software_backend::App for EguiApp {
25-
fn update(&mut self, ctx: &egui::Context, backend: &mut SoftwareBackend) {
26-
backend.set_capture_frame_time(true);
2724

25+
fn ui(&mut self, ctx: &egui::Context) {
2826
egui::CentralPanel::default().show(ctx, |_ui| {
2927
self.demo.ui(ctx);
3028

@@ -33,6 +31,45 @@ impl egui_software_backend::App for EguiApp {
3331
self.color_test.ui(ui);
3432
});
3533
});
34+
});
35+
}
36+
}
37+
38+
impl eframe::App for EguiApp {
39+
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
40+
egui::CentralPanel::default().show(ctx, |_ui| {
41+
self.ui(ctx);
42+
});
43+
}
44+
}
45+
46+
fn software_backend_ui(backend: &mut SoftwareBackend, ui: &mut Ui) {
47+
let old = backend.caching();
48+
let mut new = old;
49+
egui::ComboBox::from_label("SoftwareRenderCaching")
50+
.selected_text(format!("{old:?}"))
51+
.show_ui(ui, |ui| {
52+
ui.selectable_value(&mut new, SoftwareRenderCaching::BlendTiled, "BlendTiled");
53+
ui.selectable_value(&mut new, SoftwareRenderCaching::MeshTiled, "MeshTiled");
54+
ui.selectable_value(&mut new, SoftwareRenderCaching::Mesh, "Mesh");
55+
ui.selectable_value(&mut new, SoftwareRenderCaching::Direct, "Direct");
56+
});
57+
if new != old {
58+
backend.set_caching(new);
59+
}
60+
}
61+
62+
impl egui_software_backend::App for EguiApp {
63+
fn update(&mut self, ctx: &egui::Context, backend: &mut SoftwareBackend) {
64+
egui::CentralPanel::default().show(ctx, |_ui| {
65+
self.ui(ctx);
66+
67+
#[cfg(feature = "raster_stats")]
68+
egui::Window::new("Stats").show(ctx, |ui| {
69+
backend.display_stats(ui);
70+
});
71+
72+
egui::Window::new("Software Backend").show(ctx, |ui| software_backend_ui(backend, ui));
3673

3774
if self.frame_times.len() < 100 {
3875
self.frame_times

examples/winit_hello.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ impl EguiApp {
1212

1313
impl egui_software_backend::App for EguiApp {
1414
fn update(&mut self, ctx: &egui::Context, backend: &mut SoftwareBackend) {
15-
backend.set_capture_frame_time(true);
16-
1715
egui::CentralPanel::default().show(ctx, |ui| {
1816
let last_frame_time = backend.last_frame_time().unwrap_or_default();
1917

examples/winit_raw.rs

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ fn main() {
4747
let mut egui_software_render = EguiSoftwareRender::new(ColorFieldOrder::Bgra)
4848
.with_allow_raster_opt(!args.no_opt)
4949
.with_convert_tris_to_rects(!args.no_rect)
50-
.with_caching(!args.direct);
50+
.with_caching(if args.direct {
51+
egui_software_backend::SoftwareRenderCaching::Direct
52+
} else {
53+
egui_software_backend::SoftwareRenderCaching::BlendTiled
54+
});
5155

5256
let event_loop: EventLoop<()> = EventLoop::new().unwrap();
5357

@@ -139,7 +143,7 @@ fn main() {
139143

140144
#[cfg(feature = "raster_stats")]
141145
egui::Window::new("Stats").show(ctx, |ui| {
142-
egui_software_render.stats.render(ui);
146+
egui_software_render.display_stats(ui);
143147
});
144148
});
145149

@@ -148,22 +152,30 @@ fn main() {
148152
.tessellate(full_output.shapes, full_output.pixels_per_point);
149153

150154
let mut buffer = app.surface.buffer_mut().unwrap();
151-
buffer.fill(0); // CLEAR
152155

153156
let buffer_ref = &mut BufferMutRef::new(
154157
bytemuck::cast_slice_mut(&mut buffer),
155-
width as usize,
156-
height as usize,
158+
width,
159+
height,
157160
);
158-
159-
egui_software_render.render(
161+
let redraw_everything_this_frame =
162+
egui_software_render.cached_size() != (buffer_ref.width, buffer_ref.height);
163+
let dirty_rect = egui_software_render.render(
160164
buffer_ref,
161-
&clipped_primitives,
165+
redraw_everything_this_frame,
166+
clipped_primitives,
162167
&full_output.textures_delta,
163168
full_output.pixels_per_point,
164169
);
165-
166-
buffer.present().unwrap();
170+
if !dirty_rect.is_empty() {
171+
let dirty_rect = softbuffer::Rect {
172+
x: dirty_rect.min_x,
173+
y: dirty_rect.min_y,
174+
width: NonZeroU32::new(dirty_rect.width()).expect("non zero rect"),
175+
height: NonZeroU32::new(dirty_rect.height()).expect("non zero rect"),
176+
};
177+
buffer.present_with_damage(&[dirty_rect]).unwrap();
178+
}
167179

168180
let now = Instant::now();
169181
if frame_times.len() < 100 {

src/dirty_rect.rs

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
use core::ops::Deref;
2+
3+
use alloc::vec::Vec;
4+
5+
use crate::TILE_SIZE;
6+
7+
#[derive(Debug, Clone, Copy)]
8+
pub struct DirtyRect {
9+
pub min_x: u32,
10+
pub min_y: u32,
11+
pub max_x: u32,
12+
pub max_y: u32,
13+
}
14+
15+
impl DirtyRect {
16+
pub const fn new_empty() -> Self {
17+
Self {
18+
min_x: 0,
19+
min_y: 0,
20+
max_x: 0,
21+
max_y: 0,
22+
}
23+
}
24+
25+
#[inline]
26+
pub const fn tiled<const TILE_SIZE: u32>(self) -> Self {
27+
Self {
28+
min_x: self.min_x / TILE_SIZE * TILE_SIZE,
29+
min_y: self.min_y / TILE_SIZE * TILE_SIZE,
30+
max_x: self.max_x.div_ceil(TILE_SIZE) * TILE_SIZE,
31+
max_y: self.max_y.div_ceil(TILE_SIZE) * TILE_SIZE,
32+
}
33+
}
34+
35+
#[inline]
36+
pub const fn width(self) -> u32 {
37+
self.max_x - self.min_x
38+
}
39+
#[inline]
40+
pub const fn height(self) -> u32 {
41+
self.max_y - self.min_y
42+
}
43+
44+
#[inline]
45+
pub const fn to_egui_rect(self) -> egui::Rect {
46+
egui::Rect {
47+
min: egui::Pos2 {
48+
x: self.min_x as f32,
49+
y: self.min_y as f32,
50+
},
51+
max: egui::Pos2 {
52+
x: self.max_x as f32,
53+
y: self.max_y as f32,
54+
},
55+
}
56+
}
57+
58+
#[inline]
59+
pub const fn is_empty(&self) -> bool {
60+
self.min_x == self.max_x || self.min_y == self.max_y
61+
}
62+
63+
#[inline]
64+
pub const fn intersects(self, other: Self) -> bool {
65+
self.min_x < other.max_x && self.max_x > other.min_x
66+
}
67+
68+
#[inline]
69+
pub fn intersection(self, other: DirtyRect) -> Self {
70+
Self {
71+
min_x: self.min_x.max(other.min_x),
72+
min_y: self.min_y.max(other.min_y),
73+
max_x: self.max_x.min(other.max_x),
74+
max_y: self.max_y.min(other.max_y),
75+
}
76+
}
77+
78+
#[inline]
79+
pub fn union(&self, other: DirtyRect) -> Self {
80+
Self {
81+
min_x: self.min_x.min(other.min_x),
82+
min_y: self.min_y.min(other.min_y),
83+
max_x: self.max_x.max(other.max_x),
84+
max_y: self.max_y.max(other.max_y),
85+
}
86+
}
87+
}
88+
89+
#[derive(Debug, Default)]
90+
pub struct ComputeTiledDirtyRects {
91+
minimal_non_overlapping_bboxes: Vec<DirtyRect>,
92+
pub(crate) bboxes: Vec<DirtyRect>,
93+
x_intervals: Vec<(u32, u32)>,
94+
ys: Vec<u32>,
95+
}
96+
97+
impl Deref for ComputeTiledDirtyRects {
98+
type Target = [DirtyRect];
99+
100+
fn deref(&self) -> &Self::Target {
101+
&self.minimal_non_overlapping_bboxes
102+
}
103+
}
104+
105+
impl ComputeTiledDirtyRects {
106+
pub fn intersections(&self, other: DirtyRect) -> impl Iterator<Item = DirtyRect> + '_ {
107+
self.minimal_non_overlapping_bboxes
108+
.iter()
109+
.filter(move |bbox| bbox.intersects(other))
110+
.map(move |bbox| bbox.intersection(other))
111+
}
112+
113+
pub fn set_bboxes(&mut self, boxes: impl Iterator<Item = DirtyRect>) {
114+
fn merge_intervals(intervals: &mut [(u32, u32)], mut f_yield: impl FnMut((u32, u32))) {
115+
if intervals.is_empty() {
116+
return;
117+
}
118+
intervals.sort_unstable_by(|a, b| a.0.cmp(&b.0));
119+
let mut it = intervals.iter().copied();
120+
if let Some(mut last) = it.next() {
121+
for (start, end) in it {
122+
if start <= last.1 {
123+
last.1 = last.1.max(end);
124+
} else {
125+
f_yield(last);
126+
last = (start, end);
127+
}
128+
}
129+
f_yield(last);
130+
}
131+
}
132+
133+
self.minimal_non_overlapping_bboxes.clear();
134+
self.bboxes.clear();
135+
self.bboxes.extend(boxes.map(|b| b.tiled::<TILE_SIZE>()));
136+
// Step 1: collect all unique y-coordinates
137+
self.ys.clear();
138+
self.ys
139+
.extend(self.bboxes.iter().flat_map(|b| [b.min_y, b.max_y]));
140+
self.ys.sort_unstable();
141+
self.ys.dedup();
142+
143+
// Step 2: iterate over horizontal strips
144+
for strip in self.ys.windows(2) {
145+
let min_y = strip[0];
146+
let max_y = strip[1];
147+
148+
// Find boxes intersecting this horizontal strip
149+
self.x_intervals.clear();
150+
for b in &self.bboxes {
151+
if b.min_y < max_y && b.max_y > min_y {
152+
self.x_intervals.push((b.min_x, b.max_x));
153+
}
154+
}
155+
156+
// Merge overlapping x-intervals
157+
merge_intervals(&mut self.x_intervals, |(min_x, max_x)| {
158+
match self.minimal_non_overlapping_bboxes.last_mut() {
159+
Some(rect)
160+
if rect.min_x == min_x && rect.max_x == max_x && rect.max_y == min_y =>
161+
{
162+
rect.max_y = max_y;
163+
}
164+
_ => {
165+
self.minimal_non_overlapping_bboxes.push(DirtyRect {
166+
min_x,
167+
min_y,
168+
max_x,
169+
max_y,
170+
});
171+
}
172+
}
173+
});
174+
}
175+
}
176+
}

0 commit comments

Comments
 (0)