Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 6 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 {}
Expand All @@ -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)
Expand All @@ -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
47 changes: 42 additions & 5 deletions examples/winit.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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);

Expand All @@ -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
Expand Down
2 changes: 0 additions & 2 deletions examples/winit_hello.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
32 changes: 22 additions & 10 deletions examples/winit_raw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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);
});
});

Expand All @@ -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 {
Expand Down
176 changes: 176 additions & 0 deletions src/dirty_rect.rs
Original file line number Diff line number Diff line change
@@ -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<const TILE_SIZE: u32>(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<DirtyRect>,
pub(crate) bboxes: Vec<DirtyRect>,
x_intervals: Vec<(u32, u32)>,
ys: Vec<u32>,
}

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<Item = DirtyRect> + '_ {
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<Item = DirtyRect>) {
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::<TILE_SIZE>()));
// 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,
});
}
}
});
}
}
}
Loading