A benchmark comparing Skia and ThorVG rendering performance.
The shared benchmark logic and Python tools can be tested without installing Skia, SDL, ThorVG, or WebGPU. A C++20 compiler, CMake, and Python 3.10+ are enough:
cmake -S . -B build-tests -DRECTBENCH_BUILD_BENCHMARKS=OFF -DBUILD_TESTING=ON
cmake --build build-tests
ctest --test-dir build-tests --output-on-failureThe suite checks statistics, seeded workloads, CLI validation, JSON output, suite orchestration, reporting, and fairness in the actual benchmark loop. A fake clock verifies warmup exclusion, exact frame budgets, and that update, draw/synchronization, and presentation stay inside the timed interval. Failed or interrupted runs must return an error without publishing partial results.
Adapter and factory tests compile selected, unchanged production method bodies
against recording API substitutes. They check both engines' submission and
presentation order and option forwarding in all 16 factories. Source-line
mapping attributes coverage to those method bodies; it does not measure the
substituted engine APIs, graphics initialization, or hardware behavior.
Use -DBUILD_TESTING=OFF to omit tests from a rendering benchmark build.
For coverage, CRAP scores, and a selected mutation sample, use GCC and its
matching gcov, with the quality tools installed in your Python environment:
python -m pip install -r tests/requirements-quality.txt
cmake -S . -B build-tests -DCMAKE_CXX_COMPILER=g++ -DCMAKE_BUILD_TYPE=Debug \
-DRECTBENCH_BUILD_BENCHMARKS=OFF -DBUILD_TESTING=ON -DRECTBENCH_TEST_COVERAGE=ON
cmake --build build-tests
python tests/quality.py --build-dir build-testsRun this quality command instead of the separate CTest command: it runs the
tests while collecting coverage. It writes reports to the ignored
build-test-quality/report/ directory. --cmake and --gcov accept alternate
tool paths; --coverage-only and --mutations-only allow separate collection
and mutation runs when investigating failures. --mutation-prefix fairness-
selects the fairness faults without repeating unrelated mutation checks.
CRAP combines cyclomatic complexity from Lizard with measured executable-line
coverage: CC² * (1 - coverage)³ + CC. Scores above 30 are reported for follow-up.
Functions without instrumentation have a null score;
they are excluded from the reported coverage denominator.
tests/mutations.json and tests/fairness_mutations.json define explicit
behavior-changing faults. Each mutant
runs against an isolated source copy after that copy passes its baseline tests.
Survivors, compile errors, and timeouts fail the quality command; only failing
tests or crashes kill a valid mutant. This selected sample is not exhaustive
mutation coverage or proof of correctness. The tests GitHub workflow runs
these checks on pushes and pull requests and uploads the reports.
git clone <this-repo>
cd thorvg.benchmark# System dependencies
brew install cmake ninja meson wgpu-native libomp python3
# vcpkg (one-time setup)
git clone https://github.com/microsoft/vcpkg.git
./vcpkg/bootstrap-vcpkg.sh
./vcpkg/vcpkg install skia sdl3 --triplet arm64-osx
# thorvg (optionally checkout to a specific tag for testing)
git clone https://github.com/thorvg/thorvg.git
meson setup thorvg/build thorvg -Dloaders=all -Dengines=all
ninja -C thorvg/build installPKG_CONFIG_PATH="/opt/homebrew/lib/pkgconfig:$PKG_CONFIG_PATH" \
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE="$HOME/vcpkg/scripts/buildsystems/vcpkg.cmake"
cmake --build build -j./build/rect_skia_sdl
./build/rect_tvg_sdl
./build/multiimage_skia_sdl
./build/multiimage_tvg_sdlThe multiimage executables draw 5,000 image instances at generated
positions and sizes, selecting each instance from a fixed set of 25 byte-distinct
PNG assets in the Kenney Animal Pack. The image sequence is generated once from
--seed and is identical across Skia and ThorVG; the assets are reused while
rendering.
The multi-image scene uses alpha values from 253 through 255.
The stroke executables draw 5,000 paths in a repeating, nearly equal mix
of everyday design shapes:
| Shape | Geometry | Typical use |
|---|---|---|
| Smooth S-connector | Two cubic curves, open | Flow diagrams and curved connectors |
| Rounded elbow connector | Three straight runs and two cubic turns, open | Routed diagram connections |
| Speech-bubble outline | Four curved corners and a triangular tail, closed | Chat and annotation icons |
Both engines use the same seeded geometry, colors, positions, and stroke widths.
Layout boxes range from 48 to 200 px on each axis, with 2–8 px strokes and round
caps and joins. Paths are built once before timing; the existing default
translation/scale and rotation animation modes still apply.
These are ordinary stroked paths rather than rectangles, ovals, rounded
rectangles, or single lines. They avoid Skia's corresponding
primitive-shape shortcuts,
while allowing normal path rendering optimizations. strokerect remains
available as the rectangle-specific workload.
Use --opacity=0..1 to set the same stroke opacity on every path (default: 1).
Both engines round this to an 8-bit color alpha: 0.5 becomes 128/255.
Changing opacity preserves geometry, colors, and animation for a given seed.
Opacity 0 is useful for a visibility check; it does not measure visible stroke
rendering work.
./build/stroke_skia_sdl --backend=cpu --opacity=1 --vsync=0
./build/stroke_tvg_sdl --backend=cpu --opacity=1 --vsync=0
./build/stroke_skia_sdl --backend=gl --scene=rotation --opacity=0.5 --vsync=0
./build/stroke_tvg_sdl --backend=gl --scene=rotation --opacity=0.5 --vsync=0Stroke result metadata includes "workload": "designer-strokes-v1" and the
requested opacity. This workload replaces the old rectangle-only stroke
benchmark; its results are not directly comparable with those historical runs.
| Option | Default | Description |
|---|---|---|
--backend=cpu|gl|webgpu |
cpu |
Rendering backend |
--scene=default|rotation |
default |
Scene mode (rotation adds per-frame rotation) |
--seed=INT |
12345 |
RNG seed for deterministic output |
--frames=INT |
1000 |
Number of measured frames (at least 1) |
--warmup=INT |
120 |
Number of warmup frames |
--width=INT |
2560 |
Window/render width |
--height=INT |
1440 |
Window/render height |
--vsync=0|1 |
0 |
Enable VSync |
--opacity=FLOAT |
1 |
Strokebench only: stroke opacity from 0 to 1 |
--output=PATH |
auto | Output JSON file path |
The frame timer includes update, drawing, and presentation. Setup and event processing are excluded, and warmup frames never enter the reported statistics.
# Quick test with fewer frames
./build/rect_skia_sdl --frames=100 --warmup=10
# Test with specific backend
./build/rect_tvg_sdl --backend=gl
# Rotation scene mode
./build/rect_tvg_sdl --scene=rotation
# Verify deterministic output (checksums should match)
./build/rect_skia_sdl --backend=cpu --seed=42 --frames=1
./build/rect_tvg_sdl --backend=cpu --seed=42 --frames=1Run a matrix of benchmarks across multiple engines, backends, and scenes:
# Basic usage
python3 tools/run_all.py --runs 3 -- --frames=200 --warmup=20
# Build before running
python3 tools/run_all.py --build --runs 3 -- --frames=200 --warmup=20
# Specific benchmarks only
python3 tools/run_all.py --benchmarks=rect,circle --runs 3
# CPU backend only
python3 tools/run_all.py --backends=cpu --runs 5
# Fixed seed for reproducibility
python3 tools/run_all.py --seed=42 --runs 3Options:
| Option | Default | Description |
|---|---|---|
--runs N |
3 |
Number of runs per variant |
--benchmarks=... |
all | Comma-separated: rect,circle,stroke,image,multiimage,lineargradient,radialgradient |
--engines=... |
skia,thorvg |
Engines to test |
--backends=... |
cpu,gl |
Backends: cpu, gl, webgpu (ThorVG only) |
--scenes=... |
default,rotation |
Scene modes |
--build |
off | Build with CMake before running |
--seed N |
- | Fixed seed for ALL runs (use for measuring variance with identical input) |
--seed-base N |
12345 |
Base for per-run seeds. Each run uses a different seed derived from this base. Same seed across all engines/backends within each run for fair comparison. |
--output-dir PATH |
auto | Results directory (default: results_suite_<timestamp>) |
--dry-run |
off | Print commands without executing |
Seed behavior: By default, run 1 uses seed derived from 12345+0, run 2 from 12345+1, etc. This ensures:
- Different random input each run
- Same input for all engines/backends within a run (fair comparison)
Output: Results are saved to results_suite_<timestamp>/ with:
- Per-run JSON files organized by benchmark/engine/backend/scene
summary.jsonwith aggregated statistics
Run the same benchmark multiple times with identical or varied seeds:
# Fixed seed (all runs identical)
python3 tools/repeat_seed.py --runs 5 --seed 42 -- \
./build/rect_skia_sdl --backend=cpu --frames=200 --warmup=20
# Random seeds per run (measure variance)
python3 tools/repeat_seed.py --runs 5 -- \
./build/rect_tvg_sdl --backend=gl --frames=200Options:
| Option | Default | Description |
|---|---|---|
--runs N |
5 |
Number of runs |
--seed N |
random | Fixed seed (omit for different seed per run) |
--output-dir PATH |
results_repeat |
Output directory |
--timeout-seconds N |
none | Kill run if exceeded |
--dry-run |
off | Print commands without executing |
Create a comparison table from benchmark results:
python3 tools/generate_report.py results_suite_*/summary.jsonOutput: Prints a Markdown table comparing FPS across engines and backends.
Benchmark results are written as JSON:
{
"stats": {
"avg_ms": 1.23,
"median_ms": 1.20,
"p95_ms": 1.50,
"p99_ms": 1.80,
"min_ms": 1.00,
"max_ms": 2.00,
"stddev_ms": 0.15,
"fps": 810.37
},
"metadata": {
"engine": "skia",
"backend": "cpu",
"seed": 12345,
"resolution": "2560x1440"
}
}Linux builds are validated via GitHub Actions. See .github/workflows/linux.yml for the CI configuration.
sudo apt-get install -y \
cmake ninja-build pkg-config python3 meson curl unzip \
autoconf autoconf-archive automake libtool libtool-bin \
libgl1-mesa-dev libegl1-mesa-dev \
libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev libxfixes-dev
# vcpkg + Skia
git clone https://github.com/microsoft/vcpkg.git
./vcpkg/bootstrap-vcpkg.sh
./vcpkg/vcpkg install skia sdl3 --triplet x64-linux
# thorvg (optionally checkout to a specific tag for testing)
git clone https://github.com/thorvg/thorvg.git
meson setup thorvg/build thorvg -Dloaders=all -Dengines=all
ninja -C thorvg/build installcmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE="$HOME/vcpkg/scripts/buildsystems/vcpkg.cmake"
cmake --build build -jrm -rf build
# Repeat the build stepThe Kenney Animal Pack provides
80 2D PNG assets (72 unique file contents) for image benchmarks. The
multiimage manifest uses 25 byte-distinct files from this pack and
selects them deterministically with the benchmark seed. These assets are CC0,
separately from this repository's MIT license; the original license
notice is included.