Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: A software renderer #482

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions imgui-software-renderer/Cargo.toml
@@ -0,0 +1,14 @@
[package]
name = "imgui-software-renderer"
version = "0.7.0"
edition = "2018"
authors = ["The imgui-rs Developers"]
description = "Software renderer for imgui-rs"
homepage = "https://github.com/imgui-rs/imgui-rs"
repository = "https://github.com/imgui-rs/imgui-rs"
license = "MIT/Apache-2.0"
categories = ["gui", "rendering"]

[dependencies]
tiny-skia = "0.5"
imgui = { version = "0.7.0", path = "../imgui" }
28 changes: 28 additions & 0 deletions imgui-software-renderer/README.md
@@ -0,0 +1,28 @@
# Software renderer for imgui-rs

A renderer backend for imgui-rs to allow easy capture of an "dear imgui" interface to a file, without requiring any graphics hardware or complex dependencies.

Some use cases:

1. In a test case, run an application in a "headless" mode, interacting with the interface programatically. If an assertion fails, save the rendered image to a PNG file for debugging purposes.
2. In a test case, render a widget and compare it again a "known good" reference image to check a complex custom widget hasn't been altered
3. Use the renderer to automatically generate screenshots of an application or widget for use in documentation/tutorials.


## Notes/performance

Performance is not a high priority for this project.

The renderer is reasonably fast (the basic example with the "Dear ImGUI demo" nad a few other windows renders in around 6ms per frame in release mode), but this is almost entirely thanks to the speed of [`tiny_skia`](https://github.com/RazrFalcon/tiny-skia) used for rasterisation.

The renderer is inspired by [this C++ implementation](https://github.com/emilk/imgui_software_renderer/blob/master/src/imgui_sw.cpp) which contains many optimisations (e.g combining the polygons for each text character into a single square).

The primary goals of this renderer are:

1. Small, simple to follow code base
2. Consistent output - the same draw list should produce the same pixel data


## Usage

See the `examples/` directory.
99 changes: 99 additions & 0 deletions imgui-software-renderer/examples/software_renderer_basic.rs
@@ -0,0 +1,99 @@
use tiny_skia::Pixmap;
use imgui::{im_str, FontConfig, FontSource};

fn main() {
// Size of our software "display"
let width = 1000;
let height = 500;

// Create imgui Context as per usual
let mut imgui_ctx = imgui::Context::create();

// Don't save window layout etc
imgui_ctx.set_ini_filename(None);

// Tell imgui to draw a cursor, and set the cursor position
imgui_ctx.io_mut().mouse_draw_cursor = true;
imgui_ctx.io_mut().mouse_pos = [200.0, 50.0];

// Register the default font
imgui_ctx.fonts().add_font(&[FontSource::DefaultFontData {
config: Some(FontConfig {
size_pixels: 13.0,
..FontConfig::default()
}),
}]);

// Generate font atlas texture
// FIXME: Belongs as helper in lib
let font_pixmap = {
let mut font_atlas = imgui_ctx.fonts();
let font_atlas_tex = font_atlas.build_rgba32_texture();

let mut font_pixmap = Pixmap::new(font_atlas_tex.width, font_atlas_tex.height).unwrap();

{
let data = font_pixmap.pixels_mut();
for (i, src) in font_atlas_tex.data.chunks(4).enumerate() {
data[i] =
tiny_skia::ColorU8::from_rgba(src[0], src[1], src[2], src[3]).premultiply();
}
}

font_pixmap
};

// Set display size
// FIXME: Belongs as helper in lib
imgui_ctx.io_mut().display_size = [width as f32, height as f32];
imgui_ctx.io_mut().display_framebuffer_scale = [1.0, 1.0];

for frame in 0..10 {
println!("Frame {}", frame);
imgui_ctx
.io_mut()
.update_delta_time(std::time::Duration::from_millis(20));

let draw_data: &imgui::DrawData = {
// New frame
let ui = imgui_ctx.frame();

// Create an example window
imgui::Window::new(im_str!("Example"))
.size([250.0, 100.0], imgui::Condition::FirstUseEver)
.position([10.0, 200.0], imgui::Condition::FirstUseEver)
.build(&ui, || {
// Some basic widgets
ui.button(imgui::im_str!("Hi"));
ui.text("Ok");
let mut thing = 0.4;
ui.input_float(im_str!("##Test"), &mut thing).build();

// Use custom drawing API to draw useless purple box
ui.get_window_draw_list()
.add_rect([10.0, 10.0], [50.0, 50.0], [0.5, 0.0, 1.0])
.filled(true)
.rounding(6.0)
.build();
});

// Show built-in example windows
ui.show_demo_window(&mut true);
ui.show_metrics_window(&mut true);

// Done, get draw list data
ui.render()
};

// Create empty pixmap
let mut px = Pixmap::new(width, height).unwrap();
px.fill(tiny_skia::Color::from_rgba8(89, 89, 89, 255));

// Render imgui data
let r = imgui_software_renderer::Renderer::new();
r.render(&mut px, draw_data, font_pixmap.as_ref());

// Save output
px.save_png(format!("test_{}.png", frame)).unwrap();
}
}
88 changes: 88 additions & 0 deletions imgui-software-renderer/src/copypaste.rs
@@ -0,0 +1,88 @@
//! The tiny_skia::Transform::invert method is useful but private in
//! tiny_skia currently, so duplicated here for now.

mod copypaste {
use tiny_skia::Transform;

fn dcross(a: f64, b: f64, c: f64, d: f64) -> f64 {
a * b - c * d
}

fn dcross_dscale(a: f32, b: f32, c: f32, d: f32, scale: f64) -> f32 {
(dcross(a as f64, b as f64, c as f64, d as f64) * scale) as f32
}

pub fn compute_inv(ts: &Transform, inv_det: f64) -> Transform {
Transform::from_row(
(ts.sy as f64 * inv_det) as f32,
(-ts.ky as f64 * inv_det) as f32,
(-ts.kx as f64 * inv_det) as f32,
(ts.sx as f64 * inv_det) as f32,
dcross_dscale(ts.kx, ts.ty, ts.sy, ts.tx, inv_det),
dcross_dscale(ts.ky, ts.tx, ts.sx, ts.ty, inv_det),
)
}

fn is_nearly_zero_within_tolerance(value: f32, tolerance: f32) -> bool {
debug_assert!(tolerance >= 0.0);
value.abs() <= tolerance
}

fn inv_determinant(ts: &Transform) -> Option<f64> {
let det = dcross(ts.sx as f64, ts.sy as f64, ts.kx as f64, ts.ky as f64);

// Since the determinant is on the order of the cube of the matrix members,
// compare to the cube of the default nearly-zero constant (although an
// estimate of the condition number would be better if it wasn't so expensive).
const SCALAR_NEARLY_ZERO: f32 = 1.0 / (1 << 12) as f32;

let tolerance = SCALAR_NEARLY_ZERO * SCALAR_NEARLY_ZERO * SCALAR_NEARLY_ZERO;
if is_nearly_zero_within_tolerance(det as f32, tolerance) {
None
} else {
Some(1.0 / det)
}
}

fn is_finite(x: &Transform) -> bool {
x.sx.is_finite()
&& x.ky.is_finite()
&& x.kx.is_finite()
&& x.sy.is_finite()
&& x.tx.is_finite()
&& x.ty.is_finite()
}

pub fn invert(ts: &Transform) -> Option<Transform> {
debug_assert!(!ts.is_identity());

if ts.is_scale_translate() {
if ts.has_scale() {
let inv_x = 1.0 / ts.sx;
let inv_y = 1.0 / ts.sy;
Some(Transform::from_row(
inv_x,
0.0,
0.0,
inv_y,
-ts.tx * inv_x,
-ts.ty * inv_y,
))
} else {
// translate only
Some(Transform::from_translate(-ts.tx, -ts.ty))
}
} else {
let inv_det = inv_determinant(ts)?;
let inv_ts = compute_inv(ts, inv_det);

if is_finite(&inv_ts) {
Some(inv_ts)
} else {
None
}
}
}
}

pub(crate) use copypaste::invert;