Skip to content

Commit 0599adb

Browse files
authored
Add basic benchmarking harness for vello_common + vello_cpu (linebender#867)
1 parent 74f45f1 commit 0599adb

File tree

13 files changed

+778
-7
lines changed

13 files changed

+778
-7
lines changed

Cargo.lock

Lines changed: 326 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ members = [
1414
"examples/with_winit",
1515

1616
"sparse_strips/vello_api",
17+
"sparse_strips/vello_bench",
1718
"sparse_strips/vello_common",
1819
"sparse_strips/vello_cpu",
1920
"sparse_strips/vello_hybrid",
@@ -122,3 +123,7 @@ web-time = "1.1.0"
122123
wgpu-profiler = "0.21.0"
123124
scenes = { path = "examples/scenes" }
124125
svg = "0.18.0"
126+
criterion = { version = "0.5.1", default-features = false }
127+
rand = { version = "0.9.0", default-features = false, features = ["std_rng"] }
128+
usvg = { version = "0.45.0" }
129+
once_cell = "1.21.1"

sparse_strips/vello_bench/Cargo.toml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[package]
2+
name = "vello_bench"
3+
description = "Benchmarking harness for sparse strips."
4+
categories = ["rendering", "graphics"]
5+
keywords = ["2d", "vector-graphics"]
6+
edition.workspace = true
7+
rust-version.workspace = true
8+
license.workspace = true
9+
repository.workspace = true
10+
publish = false
11+
12+
[dependencies]
13+
vello_common = { workspace = true }
14+
vello_cpu = { workspace = true }
15+
criterion = { workspace = true }
16+
rand = { workspace = true }
17+
usvg = { workspace = true }
18+
19+
[[bench]]
20+
name = "main"
21+
harness = false
22+
23+
[lints]
24+
workspace = true

sparse_strips/vello_bench/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Setup
2+
3+
In order to run the integration benchmarks with custom SVGs, you need to add the SVGs you want to run into the `data` folder. For each SVG file in that folder, a corresponding integration test will be generated automatically.
4+
5+
If you don't add any SVGs, the benchmarking harness will only use the ghostscript tiger by default.
6+
7+
In order to run the benches, you can simply run `cargo bench`. However, in most cases you probably don't
8+
want to rerun all benchmarks, in which case you can also provide a filter for the name of the benchmarks
9+
you want to run, like `cargo bench -- fine/fill`
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright 2025 the Vello Authors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
#![allow(missing_docs, reason = "Not needed for benchmarks")]
5+
6+
use criterion::{criterion_group, criterion_main};
7+
use vello_bench::{cpu_fine, strip, tile};
8+
9+
criterion_group!(ff, cpu_fine::fill);
10+
criterion_group!(fs, cpu_fine::strip);
11+
criterion_group!(tt, tile::tile);
12+
criterion_group!(srs, strip::render_strips);
13+
criterion_main!(tt, srs, ff, fs);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.svg
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright 2025 the Vello Authors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
use crate::SEED;
5+
use criterion::Criterion;
6+
use rand::prelude::StdRng;
7+
use rand::{Rng, SeedableRng};
8+
use vello_common::coarse::WideTile;
9+
use vello_common::color::palette::css::ROYAL_BLUE;
10+
use vello_common::tile::Tile;
11+
use vello_cpu::fine::Fine;
12+
13+
pub fn fill(c: &mut Criterion) {
14+
let mut g = c.benchmark_group("fine/fill");
15+
16+
macro_rules! fill_single {
17+
($name:ident, $paint:expr) => {
18+
g.bench_function(stringify!($name), |b| {
19+
let mut out = vec![];
20+
let mut fine = Fine::new(WideTile::WIDTH, Tile::HEIGHT, &mut out);
21+
22+
b.iter(|| {
23+
fine.fill(0, WideTile::WIDTH as usize, $paint);
24+
25+
std::hint::black_box(&fine);
26+
})
27+
});
28+
};
29+
}
30+
31+
fill_single!(opaque, &ROYAL_BLUE.into());
32+
fill_single!(transparent, &ROYAL_BLUE.with_alpha(0.2).into());
33+
}
34+
35+
pub fn strip(c: &mut Criterion) {
36+
let mut g = c.benchmark_group("fine/strip");
37+
let mut rng = StdRng::from_seed(SEED);
38+
39+
let mut alphas = vec![];
40+
41+
for _ in 0..WideTile::WIDTH * Tile::HEIGHT {
42+
alphas.push(rng.random());
43+
}
44+
45+
macro_rules! strip_single {
46+
($name:ident, $paint:expr) => {
47+
g.bench_function(stringify!($name), |b| {
48+
let mut out = vec![];
49+
let mut fine = Fine::new(WideTile::WIDTH, Tile::HEIGHT, &mut out);
50+
51+
b.iter(|| {
52+
fine.strip(0, WideTile::WIDTH as usize, &alphas, $paint);
53+
54+
std::hint::black_box(&fine);
55+
})
56+
});
57+
};
58+
}
59+
60+
strip_single!(basic, &ROYAL_BLUE.into());
61+
}

sparse_strips/vello_bench/src/data.rs

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
// Copyright 2025 the Vello Authors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
use std::path::Path;
5+
use std::sync::OnceLock;
6+
use usvg::tiny_skia_path::PathSegment;
7+
use usvg::{Group, Node};
8+
use vello_common::flatten::Line;
9+
use vello_common::kurbo::{Affine, BezPath, Stroke};
10+
use vello_common::peniko::Fill;
11+
use vello_common::strip::Strip;
12+
use vello_common::tile::Tiles;
13+
use vello_common::{flatten, strip};
14+
15+
static DATA: OnceLock<Vec<DataItem>> = OnceLock::new();
16+
17+
pub fn get_data_items() -> &'static [DataItem] {
18+
DATA.get_or_init(|| {
19+
let data_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("data");
20+
let mut data = vec![];
21+
22+
// Always use ghostscript tiger.
23+
data.push(DataItem::from_path(
24+
&Path::new(env!("CARGO_MANIFEST_DIR"))
25+
.join("../../examples/assets/Ghostscript_Tiger.svg"),
26+
));
27+
28+
for entry in std::fs::read_dir(&data_dir).unwrap() {
29+
let entry = entry.unwrap();
30+
let path = entry.path();
31+
32+
if path.extension().and_then(|e| e.to_str()) == Some("svg") {
33+
data.push(DataItem::from_path(&path));
34+
}
35+
}
36+
37+
data
38+
})
39+
}
40+
41+
#[derive(Clone, Debug)]
42+
pub struct DataItem {
43+
pub name: String,
44+
pub fills: Vec<BezPath>,
45+
pub strokes: Vec<BezPath>,
46+
pub width: u16,
47+
pub height: u16,
48+
}
49+
50+
impl DataItem {
51+
fn from_path(path: &Path) -> Self {
52+
let file_name = { path.file_stem().unwrap().to_string_lossy().to_string() };
53+
54+
let data = std::fs::read(path).unwrap();
55+
let tree = usvg::Tree::from_data(&data, &usvg::Options::default()).unwrap();
56+
let mut ctx = ConversionContext::new();
57+
convert(&mut ctx, tree.root());
58+
59+
Self {
60+
name: file_name,
61+
fills: ctx.fills,
62+
strokes: ctx.strokes,
63+
#[allow(
64+
clippy::cast_possible_truncation,
65+
reason = "It's okay to ignore for benchmarking."
66+
)]
67+
width: tree.size().width() as u16,
68+
#[allow(
69+
clippy::cast_possible_truncation,
70+
reason = "It's okay to ignore for benchmarking."
71+
)]
72+
height: tree.size().height() as u16,
73+
}
74+
}
75+
76+
/// Get the raw flattened lines of both fills and strokes.
77+
///
78+
/// A stroke width of 2.0 is assumed.
79+
pub fn lines(&self) -> Vec<Line> {
80+
let mut line_buf = vec![];
81+
let mut temp_buf = vec![];
82+
83+
for path in &self.fills {
84+
flatten::fill(path, Affine::default(), &mut temp_buf);
85+
line_buf.extend(&temp_buf);
86+
}
87+
88+
let stroke = Stroke {
89+
// Obviously not all strokes have that width, but it should be good enough
90+
// for benchmarking.
91+
width: 2.0,
92+
..Default::default()
93+
};
94+
95+
for path in &self.strokes {
96+
flatten::stroke(path, &stroke, Affine::default(), &mut temp_buf);
97+
line_buf.extend(&temp_buf);
98+
}
99+
100+
line_buf
101+
}
102+
103+
/// Get the unsorted tiles.
104+
pub fn unsorted_tiles(&self) -> Tiles {
105+
let mut tiles = Tiles::new();
106+
let lines = self.lines();
107+
tiles.make_tiles(&lines, self.width, self.height);
108+
109+
tiles
110+
}
111+
112+
/// Get the sorted tiles.
113+
pub fn sorted_tiles(&self) -> Tiles {
114+
let mut tiles = self.unsorted_tiles();
115+
tiles.sort_tiles();
116+
117+
tiles
118+
}
119+
120+
/// Get the alpha buffer and rendered strips.
121+
pub fn strips(&self) -> (Vec<u8>, Vec<Strip>) {
122+
let mut strip_buf = vec![];
123+
let mut alpha_buf = vec![];
124+
let lines = self.lines();
125+
let tiles = self.sorted_tiles();
126+
127+
strip::render(
128+
&tiles,
129+
&mut strip_buf,
130+
&mut alpha_buf,
131+
Fill::NonZero,
132+
&lines,
133+
);
134+
135+
(alpha_buf, strip_buf)
136+
}
137+
}
138+
139+
fn convert(ctx: &mut ConversionContext, g: &Group) {
140+
ctx.push(convert_transform(&g.transform()));
141+
142+
for child in g.children() {
143+
match child {
144+
Node::Group(group) => {
145+
convert(ctx, group);
146+
}
147+
Node::Path(p) => {
148+
let converted = convert_path_data(p);
149+
150+
if p.fill().is_some() {
151+
ctx.add_filled_path(converted.clone());
152+
}
153+
154+
if p.stroke().is_some() {
155+
ctx.add_stroked_path(converted);
156+
}
157+
}
158+
Node::Image(_) => {}
159+
Node::Text(_) => {}
160+
}
161+
}
162+
163+
ctx.pop();
164+
}
165+
166+
#[derive(Debug)]
167+
struct ConversionContext {
168+
stack: Vec<Affine>,
169+
fills: Vec<BezPath>,
170+
strokes: Vec<BezPath>,
171+
}
172+
173+
impl ConversionContext {
174+
fn new() -> Self {
175+
Self {
176+
stack: vec![],
177+
fills: vec![],
178+
strokes: vec![],
179+
}
180+
}
181+
182+
fn push(&mut self, transform: Affine) {
183+
let new = *self.stack.last().unwrap_or(&Affine::IDENTITY) * transform;
184+
self.stack.push(new);
185+
}
186+
187+
fn add_filled_path(&mut self, path: BezPath) {
188+
self.fills.push(self.get() * path);
189+
}
190+
191+
fn add_stroked_path(&mut self, path: BezPath) {
192+
self.strokes.push(self.get() * path);
193+
}
194+
195+
fn get(&self) -> Affine {
196+
*self.stack.last().unwrap_or(&Affine::IDENTITY)
197+
}
198+
199+
fn pop(&mut self) {
200+
self.stack.pop();
201+
}
202+
}
203+
204+
fn convert_transform(transform: &usvg::Transform) -> Affine {
205+
Affine::new([
206+
transform.sx as f64,
207+
transform.ky as f64,
208+
transform.kx as f64,
209+
transform.sy as f64,
210+
transform.tx as f64,
211+
transform.ty as f64,
212+
])
213+
}
214+
215+
fn convert_path_data(path: &usvg::Path) -> BezPath {
216+
let mut bez_path = BezPath::new();
217+
218+
for e in path.data().segments() {
219+
match e {
220+
PathSegment::MoveTo(p) => {
221+
bez_path.move_to((p.x, p.y));
222+
}
223+
PathSegment::LineTo(p) => {
224+
bez_path.line_to((p.x, p.y));
225+
}
226+
PathSegment::QuadTo(p1, p2) => {
227+
bez_path.quad_to((p1.x, p1.y), (p2.x, p2.y));
228+
}
229+
PathSegment::CubicTo(p1, p2, p3) => {
230+
bez_path.curve_to((p1.x, p1.y), (p2.x, p2.y), (p3.x, p3.y));
231+
}
232+
PathSegment::Close => {
233+
bez_path.close_path();
234+
}
235+
}
236+
}
237+
238+
bez_path
239+
}

sparse_strips/vello_bench/src/lib.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright 2025 the Vello Authors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
#![allow(missing_docs, reason = "Not needed for benchmarks")]
5+
6+
use std::path::PathBuf;
7+
use std::sync::LazyLock;
8+
9+
pub mod cpu_fine;
10+
pub mod data;
11+
pub mod strip;
12+
pub mod tile;
13+
14+
pub(crate) const SEED: [u8; 32] = [0; 32];
15+
pub static DATA_PATH: LazyLock<PathBuf> =
16+
LazyLock::new(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("data"));

0 commit comments

Comments
 (0)