Skip to content

Commit

Permalink
Optimize tessellation of filled circles (#1616)
Browse files Browse the repository at this point in the history
When painting a scatter plot we sometimes want to paint hundreds of thousands of points (filled circles) on screen every frame.

In this PR the font texture atlas is pre-populated with some filled circled of various radii. These are then used when painting (small) filled circled, which means A LOT less triangles and vertices are generated for them.

In a new benchmark we can see a 10x speedup in circle tessellation, but the the real benefit comes in the painting of these circles: since we generate a lot less vertices, the backend painter has less to do.

In a real-life scenario with a lot of things being painted (including around 100k points) I saw tessellation go from 35ms -> 7ms and painting go from 45ms -> 1ms. This means the total frame time went from 80ms to 8ms, or a 10x speedup.
  • Loading branch information
emilk authored May 10, 2022
1 parent 28efc0e commit 7b18fab
Show file tree
Hide file tree
Showing 12 changed files with 232 additions and 23 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w


## Unreleased
* Add `*_released` & `*_clicked` methods for `PointerState`.
* Add `*_released` & `*_clicked` methods for `PointerState` ([#1582](https://github.com/emilk/egui/pull/1582)).
* Optimize painting of filled circles (e.g. for scatter plots) by 10x or more ([#1616](https://github.com/emilk/egui/pull/1616)).


## 0.18.1 - 2022-05-01
* Change `Shape::Callback` from `&dyn Any` to `&mut dyn Any` to support more backends.
Expand Down
7 changes: 5 additions & 2 deletions egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -830,14 +830,17 @@ impl Context {

let pixels_per_point = self.pixels_per_point();
let tessellation_options = *self.tessellation_options();
let font_image_size = self.fonts().font_image_size();
let texture_atlas = self.fonts().texture_atlas();
let font_tex_size = texture_atlas.lock().size();
let prepared_discs = texture_atlas.lock().prepared_discs();

let paint_stats = PaintStats::from_shapes(&shapes);
let clipped_primitives = tessellator::tessellate_shapes(
pixels_per_point,
tessellation_options,
font_tex_size,
prepared_discs,
shapes,
font_image_size,
);
self.write().paint_stats = paint_stats.with_clipped_primitives(&clipped_primitives);
clipped_primitives
Expand Down
3 changes: 3 additions & 0 deletions egui/src/introspection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ impl Widget for &mut epaint::TessellationOptions {
feathering,
feathering_size_in_pixels,
coarse_tessellation_culling,
prerasterized_discs,
round_text_to_pixels,
debug_paint_clip_rects,
debug_paint_text_rects,
Expand All @@ -158,6 +159,8 @@ impl Widget for &mut epaint::TessellationOptions {
.text("Feathering size in pixels");
ui.add_enabled(*feathering, feathering_slider);

ui.checkbox(prerasterized_discs, "Speed up filled circles with pre-rasterization");

ui.add(
crate::widgets::Slider::new(bezier_tolerance, 0.0001..=10.0)
.logarithmic(true)
Expand Down
9 changes: 7 additions & 2 deletions egui_demo_lib/benches/benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,13 @@ pub fn criterion_benchmark(c: &mut Criterion) {

let galley = fonts.layout(LOREM_IPSUM_LONG.to_owned(), font_id, color, wrap_width);
let font_image_size = fonts.font_image_size();
let mut tessellator =
egui::epaint::Tessellator::new(1.0, Default::default(), font_image_size);
let prepared_discs = fonts.texture_atlas().lock().prepared_discs();
let mut tessellator = egui::epaint::Tessellator::new(
1.0,
Default::default(),
font_image_size,
prepared_discs,
);
let mut mesh = egui::epaint::Mesh::default();
let text_shape = TextShape::new(egui::Pos2::ZERO, galley);
c.bench_function("tessellate_text", |b| {
Expand Down
14 changes: 14 additions & 0 deletions egui_demo_lib/src/demo/misc_demo_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,20 @@ impl View for MiscDemoWindow {
painter.line_segment([c, c + r * Vec2::angled(TAU * 3.0 / 8.0)], stroke);
});
});

CollapsingHeader::new("Many circles of different sizes")
.default_open(false)
.show(ui, |ui| {
ui.horizontal_wrapped(|ui| {
for i in 0..100 {
let r = i as f32 * 0.5;
let size = Vec2::splat(2.0 * r + 5.0);
let (rect, _response) = ui.allocate_at_least(size, Sense::hover());
ui.painter()
.circle_filled(rect.center(), r, ui.visuals().text_color());
}
});
});
}
}

Expand Down
1 change: 1 addition & 0 deletions epaint/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ All notable changes to the epaint crate will be documented in this file.


## Unreleased
* Optimize tessellation of filled circles by 10x or more ([#1616](https://github.com/emilk/egui/pull/1616)).


## 0.18.1 - 2022-05-01
Expand Down
42 changes: 40 additions & 2 deletions epaint/benches/benchmark.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};

use epaint::{pos2, Color32, Shape, Stroke};
use epaint::*;

fn single_dashed_lines(c: &mut Criterion) {
c.bench_function("single_dashed_lines", move |b| {
Expand Down Expand Up @@ -39,5 +39,43 @@ fn many_dashed_lines(c: &mut Criterion) {
});
}

criterion_group!(benches, single_dashed_lines, many_dashed_lines);
fn tessellate_circles(c: &mut Criterion) {
c.bench_function("tessellate_circles_100k", move |b| {
let radii: [f32; 10] = [1.0, 2.0, 3.6, 4.0, 5.7, 8.0, 10.0, 13.0, 15.0, 17.0];
let mut clipped_shapes = vec![];
for r in radii {
for _ in 0..10_000 {
let clip_rect = Rect::from_min_size(Pos2::ZERO, Vec2::splat(1024.0));
let shape = Shape::circle_filled(Pos2::new(10.0, 10.0), r, Color32::WHITE);
clipped_shapes.push(ClippedShape(clip_rect, shape));
}
}
assert_eq!(clipped_shapes.len(), 100_000);

let pixels_per_point = 2.0;
let options = TessellationOptions::default();

let atlas = TextureAtlas::new([4096, 256]);
let font_tex_size = atlas.size();
let prepared_discs = atlas.prepared_discs();

b.iter(|| {
let clipped_primitive = tessellate_shapes(
pixels_per_point,
options,
font_tex_size,
prepared_discs.clone(),
clipped_shapes.clone(),
);
black_box(clipped_primitive);
});
});
}

criterion_group!(
benches,
single_dashed_lines,
many_dashed_lines,
tessellate_circles
);
criterion_main!(benches);
1 change: 1 addition & 0 deletions epaint/src/shadow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ impl Shadow {
..Default::default()
},
font_tex_size,
vec![],
);
let mut mesh = Mesh::default();
tessellator.tessellate_rect(&rect, &mut mesh);
Expand Down
57 changes: 52 additions & 5 deletions epaint/src/tessellator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#![allow(clippy::identity_op)]

use crate::texture_atlas::PreparedDisc;
use crate::*;
use emath::*;

Expand Down Expand Up @@ -618,6 +619,10 @@ pub struct TessellationOptions {
/// This likely makes
pub coarse_tessellation_culling: bool,

/// If `true`, small filled circled will be optimized by using pre-rasterized circled
/// from the font atlas.
pub prerasterized_discs: bool,

/// If `true` (default) align text to mesh grid.
/// This makes the text sharper on most platforms.
pub round_text_to_pixels: bool,
Expand All @@ -644,6 +649,7 @@ impl Default for TessellationOptions {
feathering: true,
feathering_size_in_pixels: 1.0,
coarse_tessellation_culling: true,
prerasterized_discs: true,
round_text_to_pixels: true,
debug_paint_text_rects: false,
debug_paint_clip_rects: false,
Expand Down Expand Up @@ -968,6 +974,8 @@ pub struct Tessellator {
pixels_per_point: f32,
options: TessellationOptions,
font_tex_size: [usize; 2],
/// See [`TextureAtlas::prepared_discs`].
prepared_discs: Vec<PreparedDisc>,
/// size of feathering in points. normally the size of a physical pixel. 0.0 if disabled
feathering: f32,
/// Only used for culling
Expand All @@ -980,10 +988,12 @@ impl Tessellator {
/// Create a new [`Tessellator`].
///
/// * `font_tex_size`: size of the font texture. Required to normalize glyph uv rectangles when tessellating text.
/// * `prepared_discs`: What [`TextureAtlas::prepared_discs`] returns. Can safely be set to an empty vec.
pub fn new(
pixels_per_point: f32,
options: TessellationOptions,
font_tex_size: [usize; 2],
prepared_discs: Vec<PreparedDisc>,
) -> Self {
let feathering = if options.feathering {
let pixel_size = 1.0 / pixels_per_point;
Expand All @@ -995,6 +1005,7 @@ impl Tessellator {
pixels_per_point,
options,
font_tex_size,
prepared_discs,
feathering,
clip_rect: Rect::EVERYTHING,
scratchpad_points: Default::default(),
Expand Down Expand Up @@ -1137,7 +1148,7 @@ impl Tessellator {
let CircleShape {
center,
radius,
fill,
mut fill,
stroke,
} = shape;

Expand All @@ -1154,6 +1165,30 @@ impl Tessellator {
return;
}

if self.options.prerasterized_discs && fill != Color32::TRANSPARENT {
let radius_px = radius * self.pixels_per_point;
// strike the right balance between some circles becoming too blurry, and some too sharp.
let cutoff_radius = radius_px * 2.0_f32.powf(0.25);

// Find the right disc radius for a crisp edge:
// TODO: perhaps we can do something faster than this linear search.
for disc in &self.prepared_discs {
if cutoff_radius <= disc.r {
let side = radius_px * disc.w / (self.pixels_per_point * disc.r);
let rect = Rect::from_center_size(center, Vec2::splat(side));
out.add_rect_with_uv(rect, disc.uv, fill);

if stroke.is_empty() {
return; // we are done
} else {
// we still need to do the stroke
fill = Color32::TRANSPARENT; // don't fill again below
break;
}
}
}
}

self.scratchpad_path.clear();
self.scratchpad_path.add_circle(center, radius);
self.scratchpad_path.fill(self.feathering, fill, out);
Expand Down Expand Up @@ -1476,7 +1511,8 @@ impl Tessellator {
/// * `pixels_per_point`: number of physical pixels to each logical point
/// * `options`: tessellation quality
/// * `shapes`: what to tessellate
/// * `font_tex_size`: size of the font texture (required to normalize glyph uv rectangles)
/// * `font_tex_size`: size of the font texture. Required to normalize glyph uv rectangles when tessellating text.
/// * `prepared_discs`: What [`TextureAtlas::prepared_discs`] returns. Can safely be set to an empty vec.
///
/// The implementation uses a [`Tessellator`].
///
Expand All @@ -1485,10 +1521,12 @@ impl Tessellator {
pub fn tessellate_shapes(
pixels_per_point: f32,
options: TessellationOptions,
shapes: Vec<ClippedShape>,
font_tex_size: [usize; 2],
prepared_discs: Vec<PreparedDisc>,
shapes: Vec<ClippedShape>,
) -> Vec<ClippedPrimitive> {
let mut tessellator = Tessellator::new(pixels_per_point, options, font_tex_size);
let mut tessellator =
Tessellator::new(pixels_per_point, options, font_tex_size, prepared_discs);

let mut clipped_primitives: Vec<ClippedPrimitive> = Vec::default();

Expand Down Expand Up @@ -1562,6 +1600,15 @@ fn test_tessellator() {
let shape = Shape::Vec(shapes);
let clipped_shapes = vec![ClippedShape(rect, shape)];

let primitives = tessellate_shapes(1.0, Default::default(), clipped_shapes, [100, 100]);
let font_tex_size = [1024, 1024]; // unused
let prepared_discs = vec![]; // unused

let primitives = tessellate_shapes(
1.0,
Default::default(),
font_tex_size,
prepared_discs,
clipped_shapes,
);
assert_eq!(primitives.len(), 2);
}
15 changes: 7 additions & 8 deletions epaint/src/text/fonts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,12 @@ impl Fonts {
self.lock().fonts.max_texture_side
}

/// The font atlas.
/// Pass this to [`crate::Tessellator`].
pub fn texture_atlas(&self) -> Arc<Mutex<TextureAtlas>> {
self.lock().fonts.atlas.clone()
}

/// Current size of the font image.
/// Pass this to [`crate::Tessellator`].
pub fn font_image_size(&self) -> [usize; 2] {
Expand Down Expand Up @@ -535,14 +541,7 @@ impl FontsImpl {

let texture_width = max_texture_side.at_most(8 * 1024);
let initial_height = 64;
let mut atlas = TextureAtlas::new([texture_width, initial_height]);

{
// Make the top left pixel fully white:
let (pos, image) = atlas.allocate((1, 1));
assert_eq!(pos, (0, 0));
image[pos] = 1.0;
}
let atlas = TextureAtlas::new([texture_width, initial_height]);

let atlas = Arc::new(Mutex::new(atlas));

Expand Down
2 changes: 1 addition & 1 deletion epaint/src/text/text_layout_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ pub struct Glyph {
pub pos: Pos2,
/// Advance width and font row height.
pub size: Vec2,
/// Position of the glyph in the font texture.
/// Position of the glyph in the font texture, in texels.
pub uv_rect: UvRect,
/// Index into [`LayoutJob::sections`]. Decides color etc.
pub section_index: u32,
Expand Down
Loading

0 comments on commit 7b18fab

Please sign in to comment.