Integrate KenM76 PR #5199

# Conflicts:
#	crates/render/tests/it/main.rs
#	vendor/README.md
This commit is contained in:
FredApps
2026-10-29 02:38:56 +01:00
50 changed files with 13089 additions and 2 deletions
Generated
-2
View File
@@ -7351,8 +7351,6 @@ dependencies = [
[[package]]
name = "vello_cpu"
version = "0.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d8ded630e8316bb94a55881256506d1f3b9947b5f66db8a7d32ca7ba02decd0"
dependencies = [
"bytemuck",
"glifo 0.1.1",
+1
View File
@@ -128,6 +128,7 @@ hayro-postscript = { path = "vendor/hayro-postscript" }
hayro-jbig2 = { path = "vendor/hayro-jbig2" }
lopdf = { path = "vendor/lopdf" }
vello_common = { path = "vendor/vello_common" }
vello_cpu = { path = "vendor/vello_cpu" }
# The egui and winit crates below are pinned to immutable storytold git revisions (egui
# 6d518fc799681571963842f95b55a6b68431b15a, winit a517b3846ee8457d84ee4975110bfda6797c44dc).
# Only the retained `vendor/winit` and `vendor/egui-wgpu` copies are provenance for the app's
+1
View File
@@ -7,6 +7,7 @@ mod knockout_backdrop;
mod luminosity_group_space;
mod named_form_properties;
mod open_type_cff_simple_font;
mod nonseparable_gray;
mod search;
mod separation_all;
mod stray_q_in_a_form;
@@ -0,0 +1,95 @@
//! The nonseparable blend modes keep grays gray (ISO 32000-2 §11.3.5.3). vello_cpu 0.0.8's
//! `clip_color` divided 0 by 0 for grays that rounding left just outside 0..1 and drew them black
//! under `Color`. Upstream's report also has black under `Luminosity` over a gray going wrong; that
//! case already renders correctly here, so its test guards rather than reproduces.
use std::sync::Arc;
use pdfcraft_render::{PageRenderer, RenderConfig, RenderRequest, RequestKind};
fn pdf(objects: &[String]) -> Vec<u8> {
let mut out = b"%PDF-1.7\n".to_vec();
let mut offsets = Vec::new();
for (i, body) in objects.iter().enumerate() {
offsets.push(out.len());
out.extend_from_slice(format!("{} 0 obj\n{body}\nendobj\n", i + 1).as_bytes());
}
let xref = out.len();
out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).as_bytes());
for offset in offsets {
out.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
}
out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objects.len() + 1).as_bytes());
out
}
fn stream(dict: &str, content: &str) -> String {
format!("<< {dict} /Length {} >>\nstream\n{content}\nendstream", content.len())
}
/// Renders an isolated group holding a 16 × 16 grid of 6 pt cells, one per gray level, each
/// drawn by `cell(level, "x y 6 6 re")` with `/B` (`/BM /{mode} /ca {alpha}`) available, and
/// returns each level's centre pixel.
fn gray_ramp(mode: &str, alpha: f32, cell: impl Fn(u8, &str) -> String) -> Vec<(u8, [u8; 3])> {
let mut content = String::new();
for level in 0..=255_u8 {
let (x, y) = (2 + u32::from(level % 16) * 6, 2 + u32::from(level / 16) * 6);
content.push_str(&cell(level, &format!("{x} {y} 6 6 re")));
content.push('\n');
}
let objects = [
"<< /Type /Catalog /Pages 2 0 R >>".to_string(),
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_string(),
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Contents 4 0 R /Resources << /XObject << /Fm0 5 0 R >> >> >>".to_string(),
stream("", "/Fm0 Do"),
stream(
&format!(
"/Type /XObject /Subtype /Form /BBox [0 0 100 100] /Group << /S /Transparency /I true >> /Resources << /ExtGState << /B << /BM /{mode} /ca {alpha} >> >> >>"
),
&content,
),
];
let mut renderer = PageRenderer::new(Arc::new(pdf(&objects)), RenderConfig::default());
let page = renderer.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0, preview: false });
assert!(page.error.is_none(), "{:?}", page.error);
(0..=255_u8)
.map(|level| {
let (x, y) = (5 + u32::from(level % 16) * 6, 100 - 5 - u32::from(level / 16) * 6);
let offset = usize::try_from((y * page.width + x) * 4).unwrap();
(level, [page.rgba[offset], page.rgba[offset + 1], page.rgba[offset + 2]])
})
.collect()
}
fn gray(level: u8) -> f32 {
f32::from(level) / 255.0
}
fn assert_ramp(what: &str, got: &[(u8, [u8; 3])], expected: impl Fn(u8) -> f32) {
let wrong: Vec<_> = got.iter().filter(|(level, rgb)| rgb.iter().any(|&c| (f32::from(c) - expected(*level)).abs() > 2.0)).collect();
assert!(wrong.is_empty(), "{what}: {} levels off, (level, pixel): {wrong:?}", wrong.len());
}
#[test]
fn every_gray_under_color_over_nothing_keeps_its_lightness() {
// With nothing beneath it the blend result is the source (§11.4.7): each gray at half
// opacity, over the white page.
let got = gray_ramp("Color", 0.5, |level, rect| format!("/B gs {} g {rect} f", gray(level)));
assert_ramp("Color", &got, |level| (f32::from(level) + 255.0) / 2.0);
}
#[test]
fn black_under_luminosity_halves_every_opaque_gray() {
// B(Cb, Cs) = SetLum(Cb, Lum(Cs)) is black for a black source, so at half opacity the
// result is half the backdrop's gray.
let got = gray_ramp("Luminosity", 0.5, |level, rect| format!("q {} g {rect} f /B gs 0 g {rect} f Q", gray(level)));
assert_ramp("Luminosity", &got, |level| f32::from(level) / 2.0);
}
#[test]
fn a_gray_into_an_empty_group_keeps_its_lightness_in_every_nonseparable_mode() {
for mode in ["Color", "Luminosity", "Hue", "Saturation"] {
let got = gray_ramp(mode, 1.0, |level, rect| format!("/B gs {} g {rect} f", gray(level)));
assert_ramp(mode, &got, f32::from);
}
}
+2
View File
File diff suppressed because one or more lines are too long
+6
View File
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "875f324f21da93019cae9e8e61d4abfd69893206"
},
"path_in_vcs": "sparse_strips/vello_cpu"
}
+205
View File
@@ -0,0 +1,205 @@
<!-- Instructions
This changelog follows the patterns described here: <https://keepachangelog.com/en/>.
Subheadings to categorize changes are `added, changed, deprecated, removed, fixed, security`.
-->
# Changelog
## [Unreleased]
This release has an [MSRV][] of 1.88.
## [0.0.8][] - 2026-05-15
This release has an [MSRV][] of 1.88.
### Added
- `render_decoration` on glyph run builders for rendering text decorations with skip-ink behavior. ([#1592][] by [@taj-p][])
- Support for rendering VARC glyphs in text paths. ([#1594][] by [@nicoburns][], [@oscargus][])
- Support for synthetic font emboldening in text glyph runs via `GlyphRunBuilder::font_embolden` and `glifo::FontEmbolden`. ([#1628][] by [@jrmoulton][])
### Changed
- Migrated text rendering to `glifo`; rendering, glyph, and image APIs now use an explicit `Resources` object for persistent image and glyph caches. `RenderContext::render_to_buffer`, `RenderContext::render_to_pixmap`, `RenderContext::composite_to_pixmap_at_offset`, and `RenderContext::glyph_run` now take resources, and image registration moved from `RenderContext` to `Resources`. ([#1562][] by [@LaurenzV][])
- Renamed image transparency hint APIs from `may_have_opacities` and `*_opacity_hint` to `may_have_transparency` and `*_transparency_hint`. ([#1613][] by [@upsuper][])
### Removed
- Experimental `vello_api` integration and `api` module. ([#1602][] by [@waywardmonkeys][])
- Support for recordings.
This decision was made due to a number of downsides that came with the implementation.
See the corresponding PR and Zulip thread for more information. ([#1611][] by [@LaurenzV][])
### Fixed
- Rendering of rare degenerate radial gradients with undefined regions, which should render those regions transparent. ([#1529][] by [@LaurenzV][])
- Incorrect rendering of filter layers containing nested clip layers. ([#1541][] by [@LaurenzV][])
- x86 artifacts in bicubic image rendering caused by negative interpolated color values wrapping during SIMD `f32` to `u8` conversion. ([#1563][] by [@LaurenzV][])
- Stroked glyph rendering when scaled glyph runs absorb the transform into the font size, preserving the expected stroke width. ([#1576][] by [@LaurenzV][])
- COLR glyph rendering, including glyphs using non-default blend modes. ([#1584][] by [@LaurenzV][])
- Rendering of inverted rectangles, including blurred rounded rectangles. ([#1589][] by [@LaurenzV][])
### Optimized
- CPU fine rasterization performance through improved SIMD codegen. ([#1616][] by [@LaurenzV][])
## [0.0.7][] - 2026-03-24
This release has an [MSRV][] of 1.92.
### Added
- `composite_to_pixmap_at_offset` method to `RenderContext` for compositing at specific offsets within a larger pixmap. ([#1416][] by [@grebmeg][])
- `ImageResolver` for resolving opaque image IDs at rasterization time. ([#1451][] by [@grebmeg][])
- Support for image tinting. ([#1460][] by [@grebmeg][])
### Fixed
- Rendering of blurred rounded rectangles with zero or very small blur standard deviations. ([#1422][] by [@tomcur][])
- Off-by-one error in gaussian blur decimation filter. ([#1488][] by [@LaurenzV][])
- Filter layers with zero clips. ([#1437][] by [@LaurenzV][])
### Optimized
- Bilinear image sampling in the `RenderMode::OptimizeQuality` (`f32`) pipeline. ([#1343][] by [@tomcur][])
## [0.0.6][] - 2026-01-15
This release has an [MSRV][] of 1.88.
### Added
- Support for the "offset" filter. ([#1351] by [@waywardmonkeys])
### Changed
- Breaking change: Updated Peniko to [v0.6.0](https://github.com/linebender/peniko/releases/tag/v0.6.0). ([#1349][] by [@DJMcNab][])
- This also updates Kurbo to [v0.13.0](https://github.com/linebender/kurbo/releases/tag/v0.13.0).
- Upgraded Skrifa to v0.40.0. ([#1353][] by [@waywardmonkeys][])
- Upgraded Hashbrown to v0.16.1. ([#1354][] by [@waywardmonkeys][])
- Optimized image rendering for axis-aligned images. ([#1335][] by [@grebmeg][])
See also the [vello_hybrid 0.0.6](../vello_hybrid/CHANGELOG.md#006---2026-01-15) and [vello_common 0.0.6](../vello_common/CHANGELOG.md#006---2026-01-15) releases.
## [0.0.5][] - 2026-01-08
This release has an [MSRV][] of 1.88.
### Added
- The `RenderContext` now has a `set_blend_mode` (and a corresponding `blend_mode` getter method) that can be used to support non-isolated blending. ([#1159][] by [@LaurenzV])
- The `RenderContext` now contains a `push_clip_path` and `pop_clip_path` method for performing non-isolated clipping. ([#1203][] by [@LaurenzV])
- Experimental support for image filter effects: ([#1286][] by [@grebmeg][])
- New filter API methods on `RenderContext`:
- `set_filter_effect()` - Set a filter to be applied to subsequent drawing operations.
- `push_filter_layer()` - Create a new layer with a filter effect.
- `FilterEffect` trait providing both u8 and f32 precision variants for rendering across different backends.
- Gaussian Blur filter with configurable standard deviation and edge modes (`None`, `Mirror`, `Wrap`, `Duplicate`).
Uses an optimized decimated blur algorithm with automatic downsampling for performance.
- Drop Shadow filter with customizable offset, blur radius, and shadow color.
- Flood filter for solid color fills.
- A `set_mask` method to make it possible to mask rendered paths without inducing layer isolation. ([#1237][] by [@LaurenzV])
- Support for conditionally disabling the u8 or f32 pipeline. ([#1294][] by [@nicoburns])
### Changed
- Improved performance of rendering opaque images. ([#1327][] by [@grebmeg])
### Known Limitations
- Filter effects currently support only single-primitive filters; filter graphs with multiple chained primitives are not yet supported.
- Multithreaded rendering is not supported for filter effects; filters are only applied in single-threaded mode.
See also the [vello_hybrid 0.0.5](../vello_hybrid/CHANGELOG.md#005---2026-01-08) and [vello_common 0.0.5](../vello_common/CHANGELOG.md#005---2026-01-08) releases.
## [0.0.4][] - 2025-10-17
This release has an [MSRV][] of 1.86.
No changelog was kept for this release.
See also the [vello_hybrid 0.0.4](../vello_hybrid/CHANGELOG.md#004---2025-10-17) and [vello_common 0.0.4](../vello_common/CHANGELOG.md#004---2025-10-17) releases.
## [0.0.3][] - 2025-10-04
This release has an [MSRV][] of 1.86.
No changelog was kept for this release.
See also the [vello_common 0.0.3](../vello_common/CHANGELOG.md#003---2025-10-04) release.
## [0.0.2][] - 2025-09-22
This release has an [MSRV][] of 1.85.
No changelog was kept for this release.
See also the [vello_common 0.0.2](../vello_common/CHANGELOG.md#002---2025-09-22) release.
## [0.0.1][] - 2025-05-10
This release has an [MSRV][] of 1.85.
This is the initial release. No changelog was kept for this release.
See also the [vello_common 0.0.1](../vello_common/CHANGELOG.md#001---2025-05-10) release.
[@DJMcNab]: https://github.com/DJMcNab
[@grebmeg]: https://github.com/grebmeg
[@jrmoulton]: https://github.com/jrmoulton
[@LaurenzV]: https://github.com/LaurenzV
[@nicoburns]: https://github.com/nicoburns
[@oscargus]: https://github.com/oscargus
[@taj-p]: https://github.com/taj-p
[@tomcur]: https://github.com/tomcur
[@upsuper]: https://github.com/upsuper
[@waywardmonkeys]: https://github.com/waywardmonkeys
[#1159]: https://github.com/linebender/vello/pull/1159
[#1203]: https://github.com/linebender/vello/pull/1203
[#1237]: https://github.com/linebender/vello/pull/1237
[#1286]: https://github.com/linebender/vello/pull/1286
[#1294]: https://github.com/linebender/vello/pull/1294
[#1327]: https://github.com/linebender/vello/pull/1327
[#1335]: https://github.com/linebender/vello/pull/1335
[#1343]: https://github.com/linebender/vello/pull/1343
[#1349]: https://github.com/linebender/vello/pull/1349
[#1351]: https://github.com/linebender/vello/pull/1351
[#1353]: https://github.com/linebender/vello/pull/1353
[#1354]: https://github.com/linebender/vello/pull/1354
[#1416]: https://github.com/linebender/vello/pull/1416
[#1422]: https://github.com/linebender/vello/pull/1422
[#1437]: https://github.com/linebender/vello/pull/1437
[#1451]: https://github.com/linebender/vello/pull/1451
[#1460]: https://github.com/linebender/vello/pull/1460
[#1488]: https://github.com/linebender/vello/pull/1488
[#1529]: https://github.com/linebender/vello/pull/1529
[#1541]: https://github.com/linebender/vello/pull/1541
[#1562]: https://github.com/linebender/vello/pull/1562
[#1563]: https://github.com/linebender/vello/pull/1563
[#1576]: https://github.com/linebender/vello/pull/1576
[#1584]: https://github.com/linebender/vello/pull/1584
[#1589]: https://github.com/linebender/vello/pull/1589
[#1592]: https://github.com/linebender/vello/pull/1592
[#1594]: https://github.com/linebender/vello/pull/1594
[#1602]: https://github.com/linebender/vello/pull/1602
[#1611]: https://github.com/linebender/vello/pull/1611
[#1613]: https://github.com/linebender/vello/pull/1613
[#1616]: https://github.com/linebender/vello/pull/1616
[#1628]: https://github.com/linebender/vello/pull/1628
[Unreleased]: https://github.com/linebender/vello/compare/sparse-strips-v0.0.8...HEAD
[0.0.8]: https://github.com/linebender/vello/compare/sparse-strips-v0.0.7...sparse-strips-v0.0.8
[0.0.7]: https://github.com/linebender/vello/compare/sparse-strips-v0.0.6...sparse-strips-v0.0.7
[0.0.6]: https://github.com/linebender/vello/compare/sparse-strips-v0.0.5...sparse-strips-v0.0.6
[0.0.5]: https://github.com/linebender/vello/compare/sparse-stips-v0.0.4...sparse-strips-v0.0.5
[0.0.4]: https://github.com/linebender/vello/compare/sparse-stips-v0.0.3...sparse-strips-v0.0.4
[0.0.3]: https://github.com/linebender/vello/compare/sparse-stips-v0.0.2...sparse-strips-v0.0.3
[0.0.2]: https://github.com/linebender/vello/compare/sparse-strips-v0.0.1...sparse-stips-v0.0.2
[0.0.1]: https://github.com/linebender/vello/compare/ca6b1e4c7f5b0d95953c3b524f5d3952d5669c5a...sparse-strips-v0.0.1
[MSRV]: README.md#minimum-supported-rust-version-msrv
+456
View File
@@ -0,0 +1,456 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bitflags"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
[[package]]
name = "bytemuck"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
dependencies = [
"bytemuck_derive",
]
[[package]]
name = "bytemuck_derive"
version = "1.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "color"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf"
dependencies = [
"bytemuck",
"libm",
]
[[package]]
name = "core_maths"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"
dependencies = [
"libm",
]
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "euclid"
version = "0.22.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06"
dependencies = [
"num-traits",
]
[[package]]
name = "fdeflate"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
dependencies = [
"simd-adler32",
]
[[package]]
name = "fearless_simd"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76258897e51fd156ee03b6246ea53f3e0eb395d0b327e9961c4fc4c8b2fa151a"
dependencies = [
"libm",
]
[[package]]
name = "flate2"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "font-types"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7"
dependencies = [
"bytemuck",
]
[[package]]
name = "glifo"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae9d48c6d81526ad2f9d5d6e5fddf5f6949ffbc46fcac0db0d061a6e65097019"
dependencies = [
"bytemuck",
"core_maths",
"foldhash",
"hashbrown",
"log",
"peniko",
"png",
"skrifa",
"smallvec",
"vello_common",
]
[[package]]
name = "guillotiere"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b17e70c989c36bad147b27a58d148c0741c51448aa5653436547323e524d0ab"
dependencies = [
"euclid",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
dependencies = [
"foldhash",
]
[[package]]
name = "kurbo"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2"
dependencies = [
"arrayvec",
"euclid",
"libm",
"polycool",
"smallvec",
]
[[package]]
name = "libm"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "linebender_resource_handle"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4"
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
"libm",
]
[[package]]
name = "ordered-channel"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95be4d57809897b5a7539fc15a7dfe0e84141bc3dfaa2e9b1b27caa90acf61ab"
dependencies = [
"crossbeam-channel",
]
[[package]]
name = "peniko"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "839c8299360d2e998bdb106dc0a6cd71dcc5f4df51df1b620361bf50e283cca6"
dependencies = [
"bytemuck",
"color",
"kurbo",
"linebender_resource_handle",
"smallvec",
]
[[package]]
name = "png"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
dependencies = [
"bitflags",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]]
name = "polycool"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6"
dependencies = [
"arrayvec",
"libm",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rayon"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "read-fonts"
version = "0.39.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81"
dependencies = [
"bytemuck",
"core_maths",
"font-types",
]
[[package]]
name = "simd-adler32"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "skrifa"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564"
dependencies = [
"bytemuck",
"core_maths",
"read-fonts",
]
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "vello_common"
version = "0.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3361bff7f7d82c0c496b92048db83846691f0e844cc28dee92b1c824291b55ee"
dependencies = [
"bytemuck",
"fearless_simd",
"guillotiere",
"hashbrown",
"libm",
"log",
"peniko",
"png",
"smallvec",
"thiserror",
]
[[package]]
name = "vello_cpu"
version = "0.0.8"
dependencies = [
"bytemuck",
"crossbeam-channel",
"glifo",
"hashbrown",
"ordered-channel",
"png",
"rayon",
"thread_local",
"vello_common",
]
+192
View File
@@ -0,0 +1,192 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2024"
rust-version = "1.88"
name = "vello_cpu"
version = "0.0.8"
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "A CPU-based renderer for Vello, optimized for SIMD and multithreaded execution."
readme = "README.md"
keywords = [
"2d",
"vector-graphics",
]
categories = [
"rendering",
"graphics",
]
license = "Apache-2.0 OR MIT"
repository = "https://github.com/linebender/vello"
resolver = "2"
[package.metadata.docs.rs]
all-features = true
default-target = "x86_64-unknown-linux-gnu"
targets = []
[features]
default = [
"std",
"png",
"text",
"u8_pipeline",
]
f32_pipeline = []
libm = [
"vello_common/libm",
"glifo?/libm",
]
multithreading = [
"std",
"dep:rayon",
"dep:thread_local",
"dep:ordered-channel",
"dep:crossbeam-channel",
"vello_common/multithreading",
]
png = [
"dep:png",
"vello_common/png",
"glifo?/png",
]
std = [
"vello_common/std",
"glifo?/std",
]
text = ["dep:glifo"]
u8_pipeline = []
[lib]
name = "vello_cpu"
path = "src/lib.rs"
[[example]]
name = "basic"
path = "examples/basic.rs"
required-features = [
"png",
"std",
]
[[example]]
name = "clipping"
path = "examples/clipping.rs"
required-features = [
"png",
"std",
]
[[example]]
name = "masking"
path = "examples/masking.rs"
required-features = [
"png",
"std",
]
[[example]]
name = "paints"
path = "examples/paints.rs"
required-features = [
"png",
"std",
]
[dependencies.bytemuck]
version = "1.25.0"
features = ["derive"]
[dependencies.crossbeam-channel]
version = "0.5.15"
optional = true
[dependencies.glifo]
version = "0.1.0"
optional = true
default-features = false
[dependencies.hashbrown]
version = "0.17.1"
features = ["default-hasher"]
default-features = false
[dependencies.ordered-channel]
version = "1.2.0"
features = [
"crossbeam-channel",
"crossbeam-channel",
]
optional = true
[dependencies.png]
version = "0.18.1"
optional = true
[dependencies.rayon]
version = "1.12.0"
optional = true
[dependencies.thread_local]
version = "1.1.9"
optional = true
[dependencies.vello_common]
version = "0.0.8"
default-features = false
[lints.clippy]
allow_attributes_without_reason = "warn"
cargo_common_metadata = "warn"
cast_possible_truncation = "warn"
collection_is_never_read = "warn"
dbg_macro = "warn"
debug_assert_with_mut_call = "warn"
default_trait_access = "warn"
doc_markdown = "warn"
fn_to_numeric_cast_any = "warn"
infinite_loop = "warn"
large_stack_arrays = "warn"
mismatching_type_param_order = "warn"
missing_assert_message = "warn"
missing_fields_in_debug = "warn"
negative_feature_names = "warn"
redundant_feature_names = "warn"
same_functions_in_if_condition = "warn"
semicolon_if_nothing_returned = "warn"
should_panic_without_expect = "warn"
todo = "warn"
too_many_arguments = "allow"
unseparated_literal_suffix = "warn"
use_self = "warn"
wildcard_dependencies = "warn"
[lints.rust]
elided_lifetimes_in_paths = "warn"
keyword_idents_2024 = "forbid"
missing_debug_implementations = "warn"
missing_docs = "warn"
non_ascii_idents = "forbid"
non_local_definitions = "forbid"
trivial_numeric_casts = "warn"
unnameable_types = "warn"
unreachable_pub = "warn"
unsafe_op_in_unsafe_fn = "forbid"
unused_import_braces = "warn"
unused_lifetimes = "warn"
unused_macro_rules = "warn"
unused_qualifications = "warn"
+73
View File
@@ -0,0 +1,73 @@
[package]
name = "vello_cpu"
# When moving past 0.0.x, also update caveats in the README
version = "0.0.8"
description = "A CPU-based renderer for Vello, optimized for SIMD and multithreaded execution."
categories = ["rendering", "graphics"]
keywords = ["2d", "vector-graphics"]
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
[package.metadata.docs.rs]
all-features = true
# There are no platform specific docs.
default-target = "x86_64-unknown-linux-gnu"
targets = []
[dependencies]
bytemuck = { workspace = true }
glifo = { workspace = true, default-features = false, optional = true }
vello_common = { workspace = true }
hashbrown = { workspace = true }
png = { workspace = true, optional = true }
crossbeam-channel = { workspace = true, optional = true }
ordered-channel = { workspace = true, optional = true, features = ["crossbeam-channel"] }
rayon = { workspace = true, optional = true }
thread_local = { workspace = true, optional = true }
[features]
# If adding new features, also add to `src/lib.rs`
default = ["std", "png", "text", "u8_pipeline"]
# Get floating point functions from the standard library (likely using your target’s libc).
std = ["vello_common/std", "glifo?/std"]
# Use floating point implementations from libm.
libm = ["vello_common/libm", "glifo?/libm"]
# Allow loading Pixmap from PNG, and drawing png glyphs.
png = ["dep:png", "vello_common/png", "glifo?/png"]
# Enable multi-threaded rendering.
multithreading = [
"std",
"dep:rayon",
"dep:thread_local",
"dep:ordered-channel",
"dep:crossbeam-channel",
"vello_common/multithreading",
]
# Add support for text rendering
text = ["dep:glifo"]
# Speed focussed rendering using u8 math
u8_pipeline = []
# Quality focussed rendering using f32 math
f32_pipeline = []
[lints]
workspace = true
[[example]]
name = "basic"
required-features = ["png", "std"]
[[example]]
name = "clipping"
required-features = ["png", "std"]
[[example]]
name = "masking"
required-features = ["png", "std"]
[[example]]
name = "paints"
required-features = ["png", "std"]
+176
View File
@@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
+25
View File
@@ -0,0 +1,25 @@
Copyright 2020 the Vello Authors
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
+188
View File
@@ -0,0 +1,188 @@
<div align="center">
# Vello CPU
**CPU-based renderer**
[![Latest published version.](https://img.shields.io/crates/v/vello_cpu.svg)](https://crates.io/crates/vello_cpu)
[![Documentation build status.](https://img.shields.io/docsrs/vello_cpu.svg)](https://docs.rs/vello_cpu)
[![Apache 2.0 or MIT license.](https://img.shields.io/badge/license-Apache--2.0_OR_MIT-blue.svg)](#license)
\
[![Linebender Zulip chat.](https://img.shields.io/badge/Linebender-%23vello-blue?logo=Zulip)](https://xi.zulipchat.com/#narrow/channel/197075-vello)
[![GitHub Actions CI status.](https://img.shields.io/github/actions/workflow/status/linebender/vello/ci.yml?logo=github&label=CI)](https://github.com/linebender/vello/actions)
[![Dependency staleness status.](https://deps.rs/crate/vello_cpu/latest/status.svg)](https://deps.rs/crate/vello_cpu)
</div>
<!-- We use cargo-rdme to update the README with the contents of lib.rs.
To edit the following section, update it in lib.rs, then run:
cargo rdme --workspace-project=vello_cpu
Full documentation at https://github.com/orium/cargo-rdme -->
<!-- Intra-doc links used in lib.rs should be evaluated here.
See https://linebender.org/blog/doc-include/ for related discussion. -->
[`RenderContext`]: https://docs.rs/vello_cpu/latest/vello_cpu/struct.RenderContext.html
[RenderContext::set_paint]: https://docs.rs/vello_cpu/latest/vello_cpu/struct.RenderContext.html#method.set_paint
[RenderContext::fill_path]: https://docs.rs/vello_cpu/latest/vello_cpu/struct.RenderContext.html#method.fill_path
[RenderContext::stroke_path]: https://docs.rs/vello_cpu/latest/vello_cpu/struct.RenderContext.html#method.stroke_path
[RenderContext::glyph_run]: https://docs.rs/vello_cpu/latest/vello_cpu/struct.RenderContext.html#method.glyph_run
[RenderMode::OptimizeSpeed]: https://docs.rs/vello_cpu/latest/vello_cpu/enum.RenderMode.html#variant.OptimizeSpeed
[RenderMode::OptimizeQuality]: https://docs.rs/vello_cpu/latest/vello_cpu/enum.RenderMode.html#variant.OptimizeQuality
[`RenderContext::render_to_pixmap`]: https://docs.rs/vello_cpu/latest/vello_cpu/struct.RenderContext.html#method.render_to_pixmap
[`Pixmap`]: https://docs.rs/vello_cpu/latest/vello_cpu/struct.Pixmap.html
<!-- cargo-rdme start -->
Vello CPU is a 2D graphics rendering engine written in Rust, for devices with no or underpowered GPUs.
We also develop [Vello](https://crates.io/crates/vello), which makes use of the GPU for 2D rendering and has higher performance than Vello CPU.
Vello CPU is being developed as part of work to address shortcomings in Vello.
## Usage
To use Vello CPU, you need to:
- Create a [`RenderContext`][], a 2D drawing context for a fixed-size target area.
- For each object in your scene:
- Set how the object will be painted, using [`set_paint`][RenderContext::set_paint].
- Set the shape to be drawn for that object, using methods like [`fill_path`][RenderContext::fill_path],
[`stroke_path`][RenderContext::stroke_path], or [`glyph_run`][RenderContext::glyph_run].
- Render it to an image using [`RenderContext::render_to_pixmap`][].
```rust
use vello_cpu::{RenderContext, Resources, Pixmap, RenderMode};
use vello_cpu::{color::{palette::css, PremulRgba8}, kurbo::Rect};
let width = 10;
let height = 5;
let mut context = RenderContext::new(width, height);
let mut resources = Resources::new();
context.set_paint(css::MAGENTA);
context.fill_rect(&Rect::from_points((3., 1.), (7., 4.)));
let mut target = Pixmap::new(width, height);
// While calling `flush` is only strictly necessary if you are rendering using
// multiple threads, it is recommended to always do this.
context.flush();
context.render_to_pixmap(&mut resources, &mut target);
let expected_render = b"\
0000000000\
0001111000\
0001111000\
0001111000\
0000000000";
let magenta = css::MAGENTA.premultiply().to_rgba8();
let transparent = PremulRgba8 {r: 0, g: 0, b: 0, a: 0};
let mut result = Vec::new();
for pixel in target.data() {
if *pixel == magenta {
result.push(b'1');
} else if *pixel == transparent {
result.push(b'0');
} else {
panic!("Got unexpected pixel value {pixel:?}");
}
}
assert_eq!(&result, expected_render);
```
Feel free to take a look at some further
[examples](https://github.com/linebender/vello/tree/main/sparse_strips/vello_cpu/examples)
to better understand how to interact with Vello CPU's API,
## Features
- `std` (enabled by default): Get floating point functions from the standard library
(likely using your target's libc).
- `libm`: Use floating point implementations from [libm][].
- `png`(enabled by default): Allow loading [`Pixmap`]s from PNG images.
Also required for rendering glyphs with an embedded PNG. Implies `std`.
- `multithreading`: Enable multi-threaded rendering. Implies `std`.
- `text` (enabled by default): Enables glyph rendering ([`glyph_run`][RenderContext::glyph_run]).
- `u8_pipeline` (enabled by default): Enable the u8 pipeline, for speed focused rendering using u8 math.
The `u8` pipeline will be used for [`OptimizeSpeed`][RenderMode::OptimizeSpeed], if both pipelines are enabled.
If you're using Vello CPU for application rendering, you should prefer this pipeline.
- `f32_pipeline`: Enable the `f32` pipeline, which is slower but has more accurate
results. This is espectially useful for rendering test snapshots.
The `f32` pipeline will be used for [`OptimizeQuality`][RenderMode::OptimizeQuality], if both pipelines are enabled.
At least one of `std` and `libm` is required; `std` overrides `libm`.
At least one of `u8_pipeline` and `f32_pipeline` must be enabled.
You might choose to disable one of these pipelines if your application
won't use it, so as to reduce binary size.
## Caveats
Overall, Vello CPU is already very feature-rich and should be ready for
production use cases. The main caveat at the moment is that the API is
still likely to change and not stable yet. For example, we have
known plans to change the API around how image resources are used.
Additionally, there are certain APIs that are still very much experimental,
including for example support for filters. This will be reflected in the
documentation of those APIs.
Another caveat is that multi-threading with large thread counts
(more than 4) might give diminishing returns, especially when
making heavy use of layers and clip paths.
## Performance
Performance benchmarks can be found [here](https://laurenzv.github.io/vello_chart/),
As can be seen, Vello CPU achieves compelling performance on both,
aarch64 and x86 platforms. We also have SIMD optimizations for WASM SIMD,
meaning that you can expect good performance there as well.
## Implementation
If you want to gain a better understanding of Vello CPU and the
sparse strips paradigm, you can take a look at the [accompanying
master's thesis](https://ethz.ch/content/dam/ethz/special-interest/infk/inst-pls/plf-dam/documents/StudentProjects/MasterTheses/2025-Laurenz-Thesis.pdf)
that was written on the topic. Note that parts of the descriptions might
become outdated as the implementation changes, but it should give a good
overview nevertheless.
<!-- We can't directly link to the libm crate built locally, because our feature is only a pass-through -->
[libm]: https://crates.io/crates/libm
<!-- cargo-rdme end -->
## Minimum supported Rust Version (MSRV)
This version of Vello CPU has been verified to compile with **Rust 1.92** and later.
Future versions of Vello CPU might increase the Rust version requirement.
It will not be treated as a breaking change and as such can even happen with small patch releases.
<details>
<summary>Click here if compiling fails.</summary>
As time has passed, some of Vello CPU's dependencies could have released versions with a higher Rust requirement.
If you encounter a compilation issue due to a dependency and don't want to upgrade your Rust toolchain, then you could downgrade the dependency.
```sh
# Use the problematic dependency's name and version
cargo update -p package_name --precise 0.1.1
```
</details>
## Community
Discussion of Vello CPU development happens in the [Linebender Zulip](https://xi.zulipchat.com/), specifically the [#vello channel](https://xi.zulipchat.com/#narrow/channel/197075-vello).
All public content can be read without logging in.
Contributions are welcome by pull request.
The [Rust code of conduct] applies.
## License
Licensed under either of
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>)
- MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
at your option.
[Rust code of conduct]: https://www.rust-lang.org/policies/code-of-conduct
+165
View File
@@ -0,0 +1,165 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! This example demonstrates the most basic usage of Vello CPU, including
//! fundamental features in 2D rendering like filling, stroking and applying
//! transforms.
use vello_cpu::color::palette::css::YELLOW;
use vello_cpu::kurbo::Affine;
use vello_cpu::{
Level, Pixmap, RenderContext, RenderMode, RenderSettings, Resources,
color::palette::css::{BLUE, GREEN, RED},
kurbo::{Circle, Rect, Shape},
};
fn main() {
// Vello CPU is a CPU-based 2D renderer. It takes drawing commands like
// "fill a rectangle" or "stroke a triangle" as input and rasterizes
// them into a bitmap with premultiplied RGBA pixels, which can then
// be further processed (for example by converting to PNG or displaying
// the result in a window).
// We first need to define some basic render settings.
let settings = RenderSettings {
// The `level` field indicates what SIMD level should be used. In
// the vast majority of cases, you should just use `Level::new` so that
// Vello CPU automatically uses an appropriate level that is
// available on the host system.
//
// There are very few reasons to override that value. One instance where
// it might be useful to override is for example when using Vello CPU
// to create reference images for test suites. In that case, you could
// pass `Level::fallback`, which indicates to Vello CPU that no
// platform-specific SIMD intrinsics should be used. This might be useful
// because it reduces the possibility of slight pixel differences when
// running on different platforms.
level: Level::new(),
// The number of additional threads that should be used for rendering.
// This setting only has an effect when the `multi-threading` feature
// is enabled. More threads _usually_ translates to better performance,
// but also higher CPU usage. Multi-threading can be very effective
// in situations where you need to continually redraw scenes (for example
// when rendering GUIs), but is usually less useful for one-time
// rendering operations. It should also be noted that the current
// implementation of multi-threading is still not fully optimized and
// might not work as well in certain workloads, so it's worth trying
// yourself whether it actually leads to any speedups before using it.
//
// If you enabled the multi-threading feature and leave this as 0,
// it has the exact same effect as rendering in single-threaded mode.
// According to our experiments, 2-4 threads give the best results,
// using 4+ threads might result in diminishing results, depending on
// the workload.
num_threads: 0,
// Define whether the renderer should prioritize speed or quality
// during rendering. Currently, the only difference is that
// `OptimizeSpeed` will use u8/u16 for the rasterization
// while `OptimizeQuality` uses f32. The former is much faster, but
// has the disadvantage that the overall color accuracy might be slightly
// worse due to quantization. Unless you really care about that, it is
// highly recommended to use the `OptimizeSpeed` rendering mode.
render_mode: RenderMode::OptimizeSpeed,
};
// Vello CPU embraces a slightly different paradigm than a lot of other 2D
// renderers. Many 2D renderers use _immediate mode rendering_, where
// you create a single `Pixmap` and then dispatch rendering commands into
// it. In Vello CPU, there are two components instead.
//
// The first component is the `RenderContext`, which can be thought of as
// a reusable buffer for dispatching rendering commands. The `reusable`
// part is important: In situations where you are executing multiple
// rendering passes at the same resolution, you should reset the context
// and reuse it instead of always creating a new one, as will be shown below.
// This is important because it allows Vello CPU to reuse existing memory
// allocations, leading to better performance.
//
// The second component is then the `Pixmap`, which simply acts as a storage
// for the raw RGBA pixels from the render context.
// Let's start by creating a new render context with a certain
// width and height in pixels, as well as our render settings.
let mut ctx = RenderContext::new_with(100, 100, settings);
// Vello CPU needs this to track certain resources (e.g. for glyph caching)
// across multiple frames. You need to make sure that you create on such struct
// for each render context you create, and then only use it in combination with that
// specific render context.
let mut resources = Resources::new();
// Vello CPU uses a Postscript-like API, where you can use methods like
// `set_paint` or `set_stroke` to update an internal state, and then
// dispatch commands to fill or stroke paths, using the current state.
// Using the `set_paint` method, we can change what color our shapes should
// be drawn with. You can use any RGBA color for that. Apart from that, you
// can also paint your shapes using gradients or patterns (see the `paints`
// example). In this case, we are setting the color to blue.
ctx.set_paint(BLUE);
// Now, we can dispatch a commands, for example to fill a rectangle with
// the given dimensions.
ctx.fill_path(&Rect::new(25.0, 25.0, 75.0, 75.0).to_path(0.1));
// We can then update the color to a red with 50% opacity...
ctx.set_paint(RED.with_alpha(0.5));
// ...and draw a different rectangle that overlaps the previous one. You
// can also use the `fill_rect` convenience method for that.
ctx.fill_rect(&Rect::new(50.0, 50.0, 85.0, 85.0));
ctx.set_paint(GREEN);
// As mentioned, stroking is also supported. You can update the stroke
// properties using the `set_stroke` method.
ctx.stroke_path(&Circle::new((50.0, 50.0), 30.0).to_path(0.1));
// Let's say that we have drawn everything we wanted to now. Next, it is
// recommended that you call the `flush` method. In theory, this call
// is only necessary when using multi-threaded rendering, to signal that
// no more operations will be dispatched and the render context should be
// synchronized. If you forget to call this during multi-threaded rendering,
// the application will panic. In single-threaded rendering, nothing happens.
//
// However, it is highly recommended that you always call this.
// This way, downstream consumers of your crate that might have externally
// enabled Vello CPU's multi-threading feature won't run into panics when
// running your code.
ctx.flush();
// Now the second step is to copy the results of the render context into the
// pixmap. We do this by creating a new pixmap (or reusing an existing one).
// Please note that the pixmap and the render context need to have the same
// dimensions! Otherwise, the renderer will panic.
let mut pixmap_1 = Pixmap::new(100, 100);
// Now, simply extract the results from the render context into the
// pixmap.
ctx.render_to_pixmap(&mut resources, &mut pixmap_1);
// Now you can do whatever you want with the pixmap, which provides raw
// access to the premultiplied RGBA pixels of the image. If you have enabled
// the `png` feature, you can convert it into a PNG image very easily and
// then save it to disk.
let png_1 = pixmap_1.into_png().unwrap();
std::fs::write("example_basic1.png", png_1).unwrap();
// If you have another scene you want to draw at the same resolution,
// you can simply reuse the existing render context instead of creating
// a new one.
ctx.reset();
// Vello CPU supports arbitrary affine transformations.
ctx.set_transform(Affine::scale(3.0));
ctx.set_paint(YELLOW);
// The rectangle will now actually have the dimensions 60x60 since we
// applied an affine transform that scales everything by 3x to the context.
ctx.fill_rect(&Rect::new(0.0, 0.0, 20.0, 20.0));
ctx.flush();
// Once again, we render the results into a pixmap again. If you can,
// you can just reuse existing pixmaps (assuming they have the correct
// dimension), since all previous pixels in the pixmap will be
// discarded. In our case, we need to create a new one since our call
// to `into_png` consumed the pixmap.
let mut pixmap_2 = Pixmap::new(100, 100);
ctx.render_to_pixmap(&mut resources, &mut pixmap_2);
let png_2 = pixmap_2.into_png().unwrap();
std::fs::write("example_basic2.png", png_2).unwrap();
}
+134
View File
@@ -0,0 +1,134 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Applying clip paths using Vello CPU.
use vello_cpu::Pixmap;
use vello_cpu::RenderContext;
use vello_cpu::Resources;
use vello_cpu::color::palette::css::{BLUE, RED, WHITE};
use vello_cpu::kurbo::{Circle, Rect, Shape};
fn main() {
// Clip-paths are a fundamental operation in 2D rendering and allow you
// to constrain the visible areas of all subsequently drawn paths to the
// shape of another area. Vello CPU has full support for them and actually
// provides 2 different ways of using them. Below, we will explore them
// and explain their difference and when to use which one.
let mut ctx = RenderContext::new(200, 200);
// Two example clip shapes. They have a small overlap in the center that forms
// an ellipse.
let clip_1 = Circle::new((75.0, 75.0), 50.0).to_path(0.1);
let clip_2 = Circle::new((125.0, 75.0), 50.0).to_path(0.1);
// Method 1: Non-isolated clipping using the `push_clip_path` and
// `pop_clip_path` methods:
{
// Let's first create a white background.
ctx.set_paint(WHITE);
ctx.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0));
// As mentioned, clip paths contain the area that will be affected by
// our drawing operations. By default, drawing operations will be
// visible on the whole width/height of the render context.
// After this first `push_clip_path`, all drawing operations will be
// constrained to the area of the first circle.
ctx.push_clip_path(&clip_1.to_path(0.1));
// Clip paths can be nested/stacked to arbitrary depths. By
// nesting clip paths, the drawing area will be further reduced to
// the _intersection_ of all clip paths that are currently in-place.
// Thus, after this second `push_clip_path` call, only the pixels
// that lie in the intersection of both circles will be painted.
ctx.push_clip_path(&clip_2.to_path(0.1));
ctx.set_paint(RED);
// Even though the rectangle covers the whole viewport, only the parts
// that lie in the intersection of both circles will be painted.
ctx.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0));
// By popping a clip path, the top clip-path on the element (in this case
// `clip_2`) will be removed. Thus, only `clip_1` remains in-place.
ctx.pop_clip_path();
ctx.set_paint(BLUE.with_alpha(0.2));
// This rectangle will only be constrained by the area of `clip_1`.
ctx.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0));
// This is optional. You don't strictly have to pop all clip paths
// currently in-place before rasterizing to the pixmap.
ctx.pop_clip_path();
ctx.flush();
save_pixmap(&ctx, "example_clipping1");
}
// Method 2: Isolated clipping using the `push_clip_layer` and `pop_layer`
// methods:
{
// Overall, this method works exactly the same as the previous
// one, just that the method calls are different. Instead of
// `push_clip_path`, we have `push_clip_layer`, and instead of
// `pop_clip_path`, we have `pop_clip_layer`.
ctx.set_paint(WHITE);
ctx.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0));
ctx.push_clip_layer(&clip_1.to_path(0.1));
ctx.push_clip_layer(&clip_2.to_path(0.1));
ctx.set_paint(RED);
ctx.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0));
ctx.pop_layer();
ctx.set_paint(BLUE.with_alpha(0.2));
ctx.fill_rect(&Rect::new(0.0, 0.0, 200.0, 200.0));
// Unlike the first method, THIS PART IS NOT OPTIONAL! Before
// rasterizing, you need to make sure that all previously pushed layers
// have been popped. Otherwise, the renderer will panic.
ctx.pop_layer();
ctx.flush();
save_pixmap(&ctx, "example_clipping2");
}
// If you inspect the above results, you will see that they visually yield
// the same result. So what is their difference and when should you use
// which one? The answer should become clearer when explaining how they
// differ conceptually.
// When creating a clip path using `push_clip_path`, every subsequent drawing
// operation will conceptually be stencil-masked through the intersection
// of all currently active clip paths before being drawn onto the screen.
// On the other hand, doing `push_clip_layer` will actually push a whole
// new isolated layer, and once you call
// `pop_layer`, the layer _as a whole_ will be clipped to the bounds of the
// paths and composited back into the previous layer.
//
// Which one of these two methods you should use depend on the imaging model
// you are trying to reflect. For example, in SVG, each group with a clip-path
// automatically requires creating a new isolated layer. In this case, the
// isolated clipping method fits the imaging model better. On the other hand,
// in PDF for example, clip paths and layer isolation are two completely
// separate concepts. Therefore, it makes much more sense to use the
// `push_clip_path` method, since you don't want to introduce an isolated
// layer each time a new clip path is added.
//
// Finally, it is also worth mentioning that according to your experiments,
// non-isolated clipping is usually faster than isolated clipping, especially
// on the CPU. Therefore, if you are still in doubt, it is recommended
// that you simply use the non-isolated method. If necessary, you can easily
// just mix the two different methods as well.
//
// Another small note: Clip paths can actually be emulated using alpha
// masks (see the masking example), so strictly speaking you don't need
// to use the specialized clipping methods to create clip paths. However,
// the clipping methods are _much faster_ than masking, and you should
// therefore always prefer using those over masking.
}
fn save_pixmap(ctx: &RenderContext, filename: &str) {
let mut resources = Resources::new();
let mut pixmap = Pixmap::new(ctx.width(), ctx.height());
ctx.render_to_pixmap(&mut resources, &mut pixmap);
let png = pixmap.into_png().unwrap();
std::fs::write(format!("{filename}.png"), png).unwrap();
}
+100
View File
@@ -0,0 +1,100 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Using masks with Vello CPU.
use vello_cpu::color::palette::css::{BLUE, RED, WHITE};
use vello_cpu::kurbo::Rect;
use vello_cpu::{Mask, Pixmap, RenderContext, Resources};
const SIZE: u16 = 200;
fn main() {
// Vello CPU supports applying luminance and alpha masks to your drawings.
// First, we need to create our actual mask. There are multiple ways how
// you can get to it, in our case we are going to draw our own custom mask.
let mask = {
// In this case, we are drawing the mask ourselves. Note that the
// dimensions of the final mask need to match the dimensions of our
// original render context!
let mut mask_ctx = RenderContext::new(SIZE, SIZE);
let mut mask_resources = Resources::new();
let mut pixmap = Pixmap::new(SIZE, SIZE);
mask_ctx.set_paint(RED);
mask_ctx.fill_rect(&Rect::new(30.0, 30.0, 170.0, 170.0));
mask_ctx.flush();
mask_ctx.render_to_pixmap(&mut mask_resources, &mut pixmap);
Mask::new_luminance(&pixmap)
};
// Create the main render context.
let mut ctx = RenderContext::new(SIZE, SIZE);
// Similarly to clip paths (see the clipping example), there are two
// different ways of applying them:
// The first method is by creating a new isolated layer where the mask
// will be applied once the whole layer has been drawn and is composited
// into the backdrop. The second method is by setting the mask in the
// render context, in which case the mask will be applied to each shape
// directly before being drawn. Which method you should use once again
// depends on the imaging model you are reflecting.
// Method 1: Non-isolated masking via `set_mask`.
{
ctx.set_paint(WHITE);
ctx.fill_rect(&Rect::new(0.0, 0.0, SIZE as f64, SIZE as f64));
// Once the mask is set, the mask will be applied to every path we
// are drawing individually before compositing it into the background.
ctx.set_mask(mask.clone());
// We first apply the mask to the blue rectangle and then composite it.
ctx.set_paint(BLUE);
ctx.fill_rect(&Rect::new(20.0, 20.0, 130.0, 130.0));
// Now, we yet again first apply the mask to the red rectangle only and
// then composite the result.
ctx.set_paint(RED);
ctx.fill_rect(&Rect::new(70.0, 70.0, 180.0, 180.0));
// Use this method if you want to reset the mask currently in place.
ctx.reset_mask();
ctx.flush();
save_pixmap(&ctx, "example_masking1");
}
ctx.reset();
// Method 2: Isolated masking via `push_mask_layer`.
{
ctx.set_paint(WHITE);
ctx.fill_rect(&Rect::new(0.0, 0.0, SIZE as f64, SIZE as f64));
// Using this method, we first push a new isolated layer. Apart from that,
// nothing happens so far.
ctx.push_mask_layer(mask);
// Here, the blue rectangle will be drawn first. Then, the red one is drawn
// and subsequently composited on top of the blue one,
// without any special handling.
ctx.set_paint(BLUE);
ctx.fill_rect(&Rect::new(20.0, 20.0, 130.0, 130.0));
ctx.set_paint(RED);
ctx.fill_rect(&Rect::new(70.0, 70.0, 180.0, 180.0));
// Now, the whole layer is taken, the mask is applied to all of it
// and then composited into the background.
ctx.pop_layer();
ctx.flush();
save_pixmap(&ctx, "example_masking2");
}
// As can be seen, the visual result of the two methods can be different!
}
fn save_pixmap(ctx: &RenderContext, filename: &str) {
let mut resources = Resources::new();
let mut pixmap = Pixmap::new(ctx.width(), ctx.height());
ctx.render_to_pixmap(&mut resources, &mut pixmap);
let png = pixmap.into_png().unwrap();
std::fs::write(format!("{filename}.png"), png).unwrap();
}
+145
View File
@@ -0,0 +1,145 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Using gradients and patterns paints using Vello CPU.
use std::sync::Arc;
use std::{io::Cursor, path::Path};
use vello_cpu::{
Image, ImageSource, Pixmap, RenderContext, Resources,
color::palette::css::{BLUE, CYAN, DEEP_PINK, MAGENTA, NAVY, ORANGE, PURPLE, WHITE, YELLOW},
kurbo::{Affine, Point, Rect},
peniko::{
ColorStop, ColorStops, Extend, Gradient, ImageSampler, LinearGradientPosition,
RadialGradientPosition, SweepGradientPosition,
},
};
fn main() {
// Apart from drawing shapes using a single color, you can also draw them
// using gradients and image patterns.
let mut ctx = RenderContext::new(500, 500);
let base_rect = Rect::new(25.0, 25.0, 225.0, 225.0);
// All you need to is to construct the given gradient/pattern and activate
// it by using the `set_paint` method. In this case, we are drawing a
// linear gradient.
ctx.set_paint(linear_gradient(&base_rect));
ctx.fill_rect(&base_rect);
// Since we are applying a transform to the render context, the radial
// gradient we are about to apply will also be affected to that.
ctx.set_transform(Affine::translate((250.0, 0.0)));
ctx.set_paint(radial_gradient(&base_rect));
ctx.fill_rect(&base_rect);
// Sweep gradient are also supported.
ctx.set_transform(Affine::translate((0.0, 250.0)));
ctx.set_paint(sweep_gradient(&base_rect));
ctx.fill_rect(&base_rect);
// Finally, we can use image patterns with different extend modes.
ctx.set_transform(Affine::translate((250.0, 250.0)));
ctx.set_paint(pattern());
// If you want to apply an additional transform that just applies to the
// paint instead of the shape you are drawing, you can do that using the
// `set_paint_transform` method.
ctx.set_paint_transform(Affine::scale(15.0));
ctx.fill_rect(&base_rect);
ctx.flush();
save_pixmap(&ctx, "example_paint");
}
fn linear_gradient(rect: &Rect) -> Gradient {
Gradient {
kind: LinearGradientPosition {
start: Point::new(rect.x0, rect.y0),
end: Point::new(rect.x1, rect.y1),
}
.into(),
stops: color_stops([
ColorStop::from((0.0, DEEP_PINK)),
ColorStop::from((0.55, ORANGE)),
ColorStop::from((1.0, YELLOW)),
]),
extend: Extend::Pad,
..Default::default()
}
}
fn radial_gradient(rect: &Rect) -> Gradient {
let center = rect.center();
#[allow(clippy::cast_possible_truncation, reason = "necessary for conversion")]
let radius = (rect.width().min(rect.height()) * 0.5) as f32;
Gradient {
kind: RadialGradientPosition {
start_center: center,
start_radius: radius * 0.2,
end_center: center,
end_radius: radius,
}
.into(),
stops: color_stops([
ColorStop::from((0.0, CYAN)),
ColorStop::from((0.6, BLUE)),
ColorStop::from((1.0, NAVY)),
]),
extend: Extend::Pad,
..Default::default()
}
}
fn sweep_gradient(rect: &Rect) -> Gradient {
let center = rect.center();
Gradient {
kind: SweepGradientPosition {
center,
start_angle: 0.0,
end_angle: std::f32::consts::PI * 2.0,
}
.into(),
stops: color_stops([
ColorStop::from((0.0, MAGENTA)),
ColorStop::from((0.5, PURPLE)),
ColorStop::from((1.0, WHITE)),
]),
extend: Extend::Pad,
..Default::default()
}
}
fn pattern() -> Image {
// For patterns, you need to get access to a pixmap. You can either do this
// by rendering your own pixmap or converting for example a png image into
// one.
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../vello_sparse_tests/tests/assets/rgb_image_2x2.png");
let file = std::fs::read(path).unwrap();
let pixmap = Pixmap::from_png(Cursor::new(file)).unwrap();
Image {
// Note that only ImageSource::Pixmap is currently supported. Don't
// use ImageSource::OpaqueId.
image: ImageSource::Pixmap(Arc::new(pixmap)),
sampler: ImageSampler {
x_extend: Extend::Repeat,
y_extend: Extend::Repeat,
..Default::default()
},
}
}
fn save_pixmap(ctx: &RenderContext, filename: &str) {
let mut resources = Resources::new();
let mut pixmap = Pixmap::new(ctx.width(), ctx.height());
ctx.render_to_pixmap(&mut resources, &mut pixmap);
let png = pixmap.into_png().unwrap();
std::fs::write(format!("{filename}.png"), png).unwrap();
}
fn color_stops(stops: [ColorStop; 3]) -> ColorStops {
ColorStops::from(stops.as_slice())
}
+95
View File
@@ -0,0 +1,95 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
#[cfg(feature = "multithreading")]
pub(crate) mod multi_threaded;
pub(crate) mod single_threaded;
use crate::RenderMode;
use crate::kurbo::{Affine, BezPath, Rect, Stroke};
use crate::peniko::{BlendMode, Fill};
use core::fmt::Debug;
use vello_common::coarse::Wide;
use vello_common::encode::EncodedPaint;
use vello_common::filter_effects::Filter;
use vello_common::mask::Mask;
use vello_common::paint::{ImageResolver, Paint};
pub(crate) trait Dispatcher: Debug + Send + Sync {
fn wide(&self) -> &Wide;
fn fill_path(
&mut self,
path: &BezPath,
fill_rule: Fill,
transform: Affine,
paint: Paint,
blend_mode: BlendMode,
aliasing_threshold: Option<u8>,
mask: Option<Mask>,
encoded_paints: &[EncodedPaint],
);
fn stroke_path(
&mut self,
path: &BezPath,
stroke: &Stroke,
transform: Affine,
paint: Paint,
blend_mode: BlendMode,
aliasing_threshold: Option<u8>,
mask: Option<Mask>,
encoded_paints: &[EncodedPaint],
);
/// Fill a pixel-aligned rectangle with the current paint.
fn fill_rect_fast(
&mut self,
rect: &Rect,
paint: Paint,
blend_mode: BlendMode,
mask: Option<Mask>,
encoded_paints: &[EncodedPaint],
);
fn push_clip_path(
&mut self,
path: &BezPath,
fill_rule: Fill,
transform: Affine,
aliasing_threshold: Option<u8>,
);
fn pop_clip_path(&mut self);
fn push_layer(
&mut self,
clip_path: Option<&BezPath>,
fill_rule: Fill,
clip_transform: Affine,
blend_mode: BlendMode,
opacity: f32,
aliasing_threshold: Option<u8>,
mask: Option<Mask>,
filter: Option<Filter>,
);
fn pop_layer(&mut self);
fn reset(&mut self);
fn flush(&mut self, encoded_paints: &[EncodedPaint]);
fn rasterize(
&self,
buffer: &mut [u8],
render_mode: RenderMode,
width: u16,
height: u16,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
);
fn composite_at_offset(
&self,
buffer: &mut [u8],
width: u16,
height: u16,
dst_x: u16,
dst_y: u16,
dst_buffer_width: u16,
dst_buffer_height: u16,
render_mode: RenderMode,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
);
}
+900
View File
@@ -0,0 +1,900 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use crate::RenderMode;
use crate::dispatch::Dispatcher;
use crate::dispatch::multi_threaded::cost::{COST_THRESHOLD, estimate_render_task_cost};
use crate::dispatch::multi_threaded::worker::Worker;
use crate::fine::{Fine, FineKernel};
use crate::kurbo::{Affine, BezPath, PathEl, Point, Rect, Stroke};
use crate::peniko::{BlendMode, Fill};
use crate::region::Regions;
use alloc::boxed::Box;
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
use core::fmt::{Debug, Formatter};
use crossbeam_channel::TryRecvError;
use rayon::{ThreadPool, ThreadPoolBuilder};
use std::cell::RefCell;
use std::ops::Range;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use std::sync::{Barrier, Mutex};
use thread_local::ThreadLocal;
use vello_common::clip::ClipContext;
use vello_common::coarse::{Cmd, MODE_CPU, Wide};
use vello_common::encode::EncodedPaint;
use vello_common::fearless_simd::{Level, Simd, dispatch};
use vello_common::filter_effects::Filter;
use vello_common::geometry::RectU16;
use vello_common::mask::Mask;
use vello_common::paint::{ImageResolver, Paint};
use vello_common::render_graph::RenderGraph;
use vello_common::strip::Strip;
use vello_common::strip_generator::StripGenerator;
mod cost;
mod worker;
type RenderTaskSender = crossbeam_channel::Sender<RenderTask>;
type CoarseTaskSender = ordered_channel::Sender<CoarseTask>;
type CoarseTaskReceiver = ordered_channel::Receiver<CoarseTask>;
/// A dispatcher for multi-threaded rendering.
///
/// A small note for future contributors: Unfortunately, the logic of this dispatcher as well as
/// the lifecycle of the different fields of the dispatcher can be a bit hard to grasp.
/// The reason for this is that since we have to do a lot of communication across the thread boundary,
/// we have to work with lots of `Option` and `core::mem::take` operations, to ensure that we are
/// not needlessly cloning objects.
///
/// The below comments will hopefully help with understanding the overall structure and lifecycles
/// a bit better.
pub(crate) struct MultiThreadedDispatcher {
/// The wide tile container.
wide: Wide,
clip_context: ClipContext,
/// The thread pool that is used for dispatching tasks.
thread_pool: ThreadPool,
allocation_group: AllocationGroup,
/// The cost of the current batch.
batch_cost: f32,
/// The sender used to dispatch new rendering tasks from the main thread.
///
/// This field will be set once we call the `init` method.
/// This field will be set back to `None` when running `flush` to drop the value and thus
/// indicate to receivers that no more rendering tasks will be dispatched from that point onward.
task_sender: Option<RenderTaskSender>,
/// Contains one worker object for each thread.
///
/// The workers will be initialized once when building the multi-threaded dispatcher via
/// `MultiThreadedDispatcher::new`.
workers: Arc<ThreadLocal<RefCell<Worker>>>,
/// The receiver for coarse command tasks, used to do coarse rasterization on the main thread.
///
/// Similarly to `task_sender`, this value is set to `None` initially, and will only be set once
/// we actually call the `init` method (either when creating the dispatcher for the first time, or
/// when resetting it).
coarse_task_receiver: Option<CoarseTaskReceiver>,
/// The storage for alpha values.
///
/// Similarly to the single-threaded dispatcher, we want to be able to reuse the allocation holding
/// the alpha values across multiple runs of `reset`. However, we have the problem that during path
/// rendering, each thread needs to have its own allocation. We also need to be able to move
/// the allocation back and forth between the threads (during path rendering) and the main thread
/// (during fine rasterization). Because of this, we wrap it in this `MaybePresent` struct.
///
/// During initialization, each thread will "take" the vector allocation out of its slot
/// (the vector has a length of `num_threads`, so each thread has a slot belonging to itself)
/// and will put it back to its slot after flushing. Then, during fine rasterization, we
/// take all slots out of the `MaybePresent` object so that we can easily access each buffer
/// when running the commands without having to go through the mutex. After fine rasterization,
/// the slots are put back into the `MaybePresent` object.
///
alpha_storage: MaybePresent<Vec<Vec<u8>>>,
/// The task index that will be assigned to the next rendering task.
///
/// Since we are rendering the paths on different threads, we need to make sure that they
/// come back in the right order. The `task_idx` is used to keep track of that order.
task_idx: u32,
/// The number of threads active in the thread pool.
num_threads: u16,
/// The strip generator for the main thread, used for clip path rasterization.
strip_generator: StripGenerator,
level: Level,
flushed: bool,
// So that we can reuse memory allocations across different runs.
allocations: Allocations,
/// Render graph (unused in multi-threaded, only needed for API compatibility with Wide).
render_graph: RenderGraph,
}
impl MultiThreadedDispatcher {
pub(crate) fn new(width: u16, height: u16, num_threads: u16, level: Level) -> Self {
let wide = Wide::<MODE_CPU>::new(width, height);
let thread_pool = ThreadPoolBuilder::new()
.num_threads(num_threads as usize)
.build()
.unwrap();
let alpha_storage = MaybePresent::new(vec![vec![]; usize::from(num_threads)]);
let workers = Arc::new(ThreadLocal::new());
{
let thread_ids = Arc::new(AtomicU8::new(0));
let workers = workers.clone();
// Create all workers once in `new`, so that later on we can just call`.get().unwrap()`.
thread_pool.spawn_broadcast(move |_| {
let thread_id = thread_ids.fetch_add(1, Ordering::SeqCst);
let worker = Worker::new(width, height, thread_id, level);
let _ = workers.get_or(|| RefCell::new(worker));
});
}
let task_idx = 0;
let batch_cost = 0.0;
let flushed = false;
let mut dispatcher = Self {
wide,
thread_pool,
allocations: Allocations::default(),
allocation_group: AllocationGroup::default(),
batch_cost,
task_idx,
flushed,
workers,
clip_context: ClipContext::new(),
task_sender: None,
coarse_task_receiver: None,
strip_generator: StripGenerator::new(width, height, level),
level,
alpha_storage,
num_threads,
render_graph: RenderGraph::new(),
};
dispatcher.init();
dispatcher
}
#[cfg(feature = "f32_pipeline")]
fn rasterize_f32(
&self,
buffer: &mut [u8],
width: u16,
height: u16,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
) {
use crate::fine::F32Kernel;
dispatch!(self.level, simd => self.rasterize_with::<_, F32Kernel>(simd, buffer, width, height, encoded_paints, image_resolver));
}
#[cfg(feature = "u8_pipeline")]
fn rasterize_u8(
&self,
buffer: &mut [u8],
width: u16,
height: u16,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
) {
use crate::fine::U8Kernel;
dispatch!(self.level, simd => self.rasterize_with::<_, U8Kernel>(simd, buffer, width, height, encoded_paints, image_resolver));
}
fn init(&mut self) {
let (render_task_sender, render_task_receiver) = crossbeam_channel::unbounded();
let (coarse_task_sender, coarse_task_receiver) = ordered_channel::unbounded();
let workers = self.workers.clone();
let alpha_storage = self.alpha_storage.clone();
self.task_sender = Some(render_task_sender);
self.coarse_task_receiver = Some(coarse_task_receiver);
// Spawn the loop for the worker threads.
self.thread_pool.spawn_broadcast(move |_| {
let render_task_receiver = render_task_receiver.clone();
let mut coarse_task_sender = coarse_task_sender.clone();
let worker = workers.get().unwrap();
let mut worker = worker.borrow_mut();
let thread_id = worker.thread_id();
// Take out the allocation for alphas and store it in the worker.
alpha_storage
.with_inner(|alphas| worker.init(std::mem::take(&mut alphas[thread_id as usize])));
while let Ok(task) = render_task_receiver.recv() {
worker.run_render_task(task, &mut coarse_task_sender);
}
// If we reach this point, it means the `task_sender` has been dropped by the main thread
// and no more tasks are available (since we flushed).
// So we are done, and just need to place the alphas of the worker thread back into the
// vector.
alpha_storage.with_inner(|alphas| {
alphas[thread_id as usize] = worker.finalize();
});
// Then, we drop the `coarse_task_sender`. Once all worker threads have
// dropped their `coarse_task_sender`, the main thread knows that all workers are done
// and all alphas have been placed, so it's safe to proceed.
drop(coarse_task_sender);
});
}
fn register_task(&mut self, task: RenderTaskType) {
self.flushed = false;
if self.task_sender.is_none() {
self.init();
}
let cost = estimate_render_task_cost(&task, &self.allocation_group.path);
self.allocation_group.render_tasks.push(task);
self.batch_cost += cost;
if self.batch_cost > COST_THRESHOLD {
self.flush_tasks();
}
}
fn flush_tasks(&mut self) {
self.send_pending_tasks();
self.batch_cost = 0.0;
}
fn bump_task_idx(&mut self) -> u32 {
let idx = self.task_idx;
self.task_idx += 1;
idx
}
fn send_pending_tasks(&mut self) {
let task_idx = self.bump_task_idx();
let allocation_group =
std::mem::replace(&mut self.allocation_group, self.allocations.get());
let task_sender = self.task_sender.as_mut().unwrap();
let clip_path = self.clip_context.get().map(|c| OwnedClip {
strips: c.strips.into(),
alphas: c.alphas.into(),
bbox: c.bbox,
});
let task = RenderTask {
idx: task_idx,
clip_path,
allocation_group,
};
task_sender.send(task).unwrap();
// TODO: Pass encoded_paints here to enable overdraw elimination for opaque indexed
// paints. Currently we pass an empty slice, so indexed paints render correctly but miss
// the FillHint::OpaqueImage optimization. The challenge is that encoded_paints is a
// borrowed reference that may not be valid by the time coarse processing runs asynchronously.
self.run_coarse(true, &[]);
}
// Currently, we do coarse rasterization in two phases:
//
// The first phase is when we are still processing new draw commands from the client. After each
// command, we check whether there are already any generated strips, and if so we do coarse
// rasterization for them on the main thread. In this case, we want to abort in case there are
// no more path strips available to process.
//
// The second phase is when we are flushing, in which case even if the queue is empty, we only
// want to abort once all workers have closed the channel (and thus there won't be any more
// new strips that will be generated.
//
// This is why we have the `abort_empty`flag.
fn run_coarse(&mut self, abort_empty: bool, encoded_paints: &[EncodedPaint]) {
let result_receiver = self.coarse_task_receiver.as_mut().unwrap();
loop {
match result_receiver.try_recv() {
Ok(mut task) => {
let num_tasks = task.allocation_group.coarse_tasks.len();
for cmd in task.allocation_group.coarse_tasks.drain(0..num_tasks) {
match cmd {
CoarseTaskType::RenderPath {
strips: strip_range,
paint,
blend_mode,
thread_id,
mask,
} => self.wide.generate(
&task.allocation_group.strips
[strip_range.start as usize..strip_range.end as usize],
paint.clone(),
blend_mode,
thread_id,
mask,
encoded_paints,
),
CoarseTaskType::PushLayer {
thread_id,
clip_path,
blend_mode,
mask,
opacity,
} => {
let clip_path = clip_path.map(|strip_range| {
&task.allocation_group.strips
[strip_range.start as usize..strip_range.end as usize]
});
// layer_id 0 and filter None since filters aren't supported
self.wide.push_layer(
0,
clip_path,
blend_mode,
mask,
opacity,
None,
// Transform can be IDENTITY because filters aren't supported in multi-threaded mode
Affine::IDENTITY,
&mut self.render_graph,
thread_id,
);
}
CoarseTaskType::PopLayer => self.wide.pop_layer(&mut self.render_graph),
}
}
// Put the allocation group back so it can be reused in future iterations!
self.allocations.put(task.allocation_group);
}
Err(e) => match e {
TryRecvError::Empty => {
if abort_empty {
return;
}
}
TryRecvError::Disconnected => return,
},
}
}
}
fn rasterize_with<S: Simd, F: FineKernel<S>>(
&self,
simd: S,
buffer: &mut [u8],
width: u16,
height: u16,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
) {
let mut buffer = Regions::new(width, height, buffer);
let fines = ThreadLocal::new();
let wide = &self.wide;
let alpha_slots = self.alpha_storage.take();
self.thread_pool.install(|| {
buffer.update_regions_par(|region| {
let x = region.x;
let y = region.y;
let mut fine = fines
.get_or(|| RefCell::new(Fine::<S, F>::new(simd)))
.borrow_mut();
let wtile = wide.get(x, y);
fine.set_coords(x, y);
fine.clear(wtile.bg);
for cmd in &wtile.cmds {
let thread_idx = match cmd {
Cmd::AlphaFill(a) => Some(wide.attrs.fill[a.attrs_idx as usize].thread_idx),
Cmd::ClipStrip(a) => Some(wide.attrs.clip[a.attrs_idx as usize].thread_idx),
_ => None,
};
let alphas = thread_idx
.map(|i| alpha_slots[i as usize].as_slice())
.unwrap_or(&[]);
fine.run_cmd(cmd, alphas, encoded_paints, image_resolver, &wide.attrs);
}
fine.pack(region);
});
});
// Don't forget to put back the alpha buffers, so that they can be re-used in
// the next path rendering iteration!
self.alpha_storage.init(alpha_slots);
}
}
impl Dispatcher for MultiThreadedDispatcher {
fn wide(&self) -> &Wide {
&self.wide
}
fn fill_path(
&mut self,
path: &BezPath,
fill_rule: Fill,
transform: Affine,
paint: Paint,
blend_mode: BlendMode,
aliasing_threshold: Option<u8>,
mask: Option<Mask>,
_encoded_paints: &[EncodedPaint],
) {
let start = self.allocation_group.path.len() as u32;
self.allocation_group.path.extend(path);
let end = self.allocation_group.path.len() as u32;
self.register_task(RenderTaskType::FillPath {
path_range: start..end,
transform,
paint,
fill_rule,
blend_mode,
aliasing_threshold,
mask,
});
}
fn stroke_path(
&mut self,
path: &BezPath,
stroke: &Stroke,
transform: Affine,
paint: Paint,
blend_mode: BlendMode,
aliasing_threshold: Option<u8>,
mask: Option<Mask>,
_encoded_paints: &[EncodedPaint],
) {
let start = self.allocation_group.path.len() as u32;
self.allocation_group.path.extend(path);
let end = self.allocation_group.path.len() as u32;
self.register_task(RenderTaskType::StrokePath {
path_range: start..end,
transform,
paint,
stroke: stroke.clone(),
blend_mode,
aliasing_threshold,
mask,
});
}
fn fill_rect_fast(
&mut self,
rect: &Rect,
paint: Paint,
blend_mode: BlendMode,
mask: Option<Mask>,
_encoded_paints: &[EncodedPaint],
) {
// For multi-threaded, fall back to path-based rendering.
// TODO: Implement optimized rect strip generation in worker threads.
let start = self.allocation_group.path.len() as u32;
self.allocation_group.path.extend([
PathEl::MoveTo(Point::new(rect.x0, rect.y0)),
PathEl::LineTo(Point::new(rect.x1, rect.y0)),
PathEl::LineTo(Point::new(rect.x1, rect.y1)),
PathEl::LineTo(Point::new(rect.x0, rect.y1)),
PathEl::ClosePath,
]);
let end = self.allocation_group.path.len() as u32;
self.register_task(RenderTaskType::FillPath {
path_range: start..end,
transform: Affine::IDENTITY,
paint,
fill_rule: Fill::NonZero,
blend_mode,
aliasing_threshold: None,
mask,
});
}
fn push_layer(
&mut self,
clip_path: Option<&BezPath>,
fill_rule: Fill,
clip_transform: Affine,
blend_mode: BlendMode,
opacity: f32,
aliasing_threshold: Option<u8>,
mask: Option<Mask>,
filter: Option<Filter>,
) {
// TODO: Implement filter support in multi-threaded dispatcher.
// The single-threaded dispatcher has full support via LayerManager and render graph execution,
// but multi-threaded needs additional infrastructure for cross-thread layer coordination.
if filter.is_some() {
unimplemented!("Filter effects are not yet supported in multi-threaded rendering");
}
let mapped_clip = clip_path.map(|c| {
let start = self.allocation_group.path.len() as u32;
self.allocation_group.path.extend(c);
let end = self.allocation_group.path.len() as u32;
(start..end, clip_transform)
});
self.register_task(RenderTaskType::PushLayer {
clip_path: mapped_clip,
blend_mode,
opacity,
mask,
fill_rule,
aliasing_threshold,
});
}
fn pop_layer(&mut self) {
self.register_task(RenderTaskType::PopLayer);
}
fn reset(&mut self) {
self.wide.reset();
self.clip_context.reset();
self.allocation_group.clear();
self.batch_cost = 0.0;
self.task_idx = 0;
self.flushed = false;
self.task_sender = None;
self.coarse_task_receiver = None;
self.strip_generator.reset();
self.alpha_storage.with_inner(|alphas| {
for alpha in alphas {
alpha.clear();
}
});
let workers = self.workers.clone();
// + 1 since we also wait on the main thread.
let barrier = Arc::new(Barrier::new(usize::from(self.num_threads) + 1));
let t_barrier = barrier.clone();
self.thread_pool.spawn_broadcast(move |_| {
let worker = workers.get().unwrap();
let mut borrowed = worker.borrow_mut();
borrowed.reset();
t_barrier.wait();
});
barrier.wait();
self.init();
}
fn flush(&mut self, encoded_paints: &[EncodedPaint]) {
if self.flushed {
return;
}
self.flush_tasks();
let sender = core::mem::take(&mut self.task_sender);
// Note that dropping the sender will signal to the workers that no more new paths
// can arrive.
drop(sender);
self.run_coarse(false, encoded_paints);
self.flushed = true;
}
fn rasterize(
&self,
buffer: &mut [u8],
render_mode: RenderMode,
width: u16,
height: u16,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
) {
assert!(self.flushed, "attempted to rasterize before flushing");
// Only u8 pipeline enabled
#[cfg(all(feature = "u8_pipeline", not(feature = "f32_pipeline")))]
{
let _ = render_mode;
self.rasterize_u8(buffer, width, height, encoded_paints, image_resolver);
}
// Only f32 pipeline enabled
#[cfg(all(feature = "f32_pipeline", not(feature = "u8_pipeline")))]
{
let _ = render_mode;
self.rasterize_f32(buffer, width, height, encoded_paints, image_resolver);
}
// Both pipelines enabled
#[cfg(all(feature = "f32_pipeline", feature = "u8_pipeline"))]
match render_mode {
RenderMode::OptimizeSpeed => {
self.rasterize_u8(buffer, width, height, encoded_paints, image_resolver);
}
RenderMode::OptimizeQuality => {
self.rasterize_f32(buffer, width, height, encoded_paints, image_resolver);
}
}
}
fn composite_at_offset(
&self,
_buffer: &mut [u8],
_width: u16,
_height: u16,
_dst_x: u16,
_dst_y: u16,
_dst_buffer_width: u16,
_dst_buffer_height: u16,
_render_mode: RenderMode,
_encoded_paints: &[EncodedPaint],
_image_resolver: &dyn ImageResolver,
) {
// TODO: Implement composite_at_offset for multi-threaded dispatcher.
unimplemented!("composite_at_offset is not implemented for multi-threaded dispatcher");
}
fn push_clip_path(
&mut self,
path: &BezPath,
fill_rule: Fill,
transform: Affine,
aliasing_threshold: Option<u8>,
) {
self.flush_tasks();
self.clip_context.push_clip(
path,
&mut self.strip_generator,
fill_rule,
transform,
aliasing_threshold,
);
}
fn pop_clip_path(&mut self) {
self.flush_tasks();
self.clip_context.pop_clip();
}
}
impl Debug for MultiThreadedDispatcher {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
f.write_str("MultiThreadedDispatcher { .. }")
}
}
#[derive(Debug)]
pub(crate) struct OwnedClip {
strips: Box<[Strip]>,
alphas: Box<[u8]>,
/// A coarse bounding box of the clip path in pixel coordinates.
///
/// These bounds have already been intersected with the viewport.
bbox: RectU16,
}
/// A structure that allows storing and fetching existing allocations.
struct AllocationManager<T> {
entries: Vec<Vec<T>>,
}
impl<T> AllocationManager<T> {
/// Get a new vector allocation.
///
/// The vector is guaranteed to have been cleared before.
fn get(&mut self) -> Vec<T> {
self.entries.pop().unwrap_or_default()
}
/// Insert a new allocation in the store.
fn put(&mut self, mut allocation: Vec<T>) {
allocation.clear();
self.entries.push(allocation);
}
}
impl<T> Default for AllocationManager<T> {
fn default() -> Self {
Self { entries: vec![] }
}
}
/// A structure to keep track of allocations that will be done while rendering with
/// multi-threading.
#[derive(Default)]
struct Allocations {
/// The render tasks of a batch. They will be filled by the main thread as new fill/stroke commands
/// come in and consumed by worker threads as they process them.
render_tasks: AllocationManager<RenderTaskType>,
/// The path store of a batch. It will be filled by the main thread as new commands come in
/// and be used by the worker thread to generate the strips of a path.
paths: AllocationManager<PathEl>,
/// Stores allocations that are used by the worker thread to produce strips. They will be
/// sent back to the main thread which then uses them for coarse rasterization.
strips: AllocationManager<Strip>,
/// The coarse tasks produced by a worker thread, which will be processed by the main thread.
coarse_tasks: AllocationManager<CoarseTaskType>,
}
impl Allocations {
/// Return a new allocation group.
///
/// The group is guaranteed to have been cleared.
fn get(&mut self) -> AllocationGroup {
let render_tasks = self.render_tasks.get();
let path = self.paths.get();
let strips = self.strips.get();
let coarse_tasks = self.coarse_tasks.get();
AllocationGroup {
path,
render_tasks,
coarse_tasks,
strips,
}
}
fn put(&mut self, allocation: AllocationGroup) {
self.render_tasks.put(allocation.render_tasks);
self.paths.put(allocation.path);
self.strips.put(allocation.strips);
self.coarse_tasks.put(allocation.coarse_tasks);
}
}
#[derive(Default, Debug)]
pub(crate) struct AllocationGroup {
pub(crate) path: Vec<PathEl>,
pub(crate) render_tasks: Vec<RenderTaskType>,
pub(crate) strips: Vec<Strip>,
pub(crate) coarse_tasks: Vec<CoarseTaskType>,
}
impl AllocationGroup {
fn clear(&mut self) {
self.path.clear();
self.render_tasks.clear();
self.strips.clear();
self.coarse_tasks.clear();
}
}
#[derive(Debug)]
pub(crate) struct RenderTask {
pub(crate) idx: u32,
pub(crate) clip_path: Option<OwnedClip>,
pub(crate) allocation_group: AllocationGroup,
}
#[derive(Debug, Clone)]
pub(crate) enum RenderTaskType {
FillPath {
path_range: Range<u32>,
transform: Affine,
paint: Paint,
fill_rule: Fill,
blend_mode: BlendMode,
aliasing_threshold: Option<u8>,
mask: Option<Mask>,
},
StrokePath {
path_range: Range<u32>,
transform: Affine,
paint: Paint,
stroke: Stroke,
blend_mode: BlendMode,
aliasing_threshold: Option<u8>,
mask: Option<Mask>,
},
PushLayer {
clip_path: Option<(Range<u32>, Affine)>,
blend_mode: BlendMode,
opacity: f32,
mask: Option<Mask>,
fill_rule: Fill,
aliasing_threshold: Option<u8>,
},
PopLayer,
}
pub(crate) struct CoarseTask {
allocation_group: AllocationGroup,
}
#[derive(Debug)]
pub(crate) enum CoarseTaskType {
RenderPath {
thread_id: u8,
strips: Range<u32>,
blend_mode: BlendMode,
paint: Paint,
mask: Option<Mask>,
},
PushLayer {
thread_id: u8,
clip_path: Option<Range<u32>>,
blend_mode: BlendMode,
mask: Option<Mask>,
opacity: f32,
},
PopLayer,
}
/// An object that might hold a certain value (behind a mutex), and panics if we attempt
/// to access it when it's not initialized.
#[derive(Clone)]
pub(crate) struct MaybePresent<T: Default> {
present: Arc<AtomicBool>,
value: Arc<Mutex<T>>,
}
impl<T: Default> MaybePresent<T> {
pub(crate) fn new(val: T) -> Self {
Self {
present: Arc::new(AtomicBool::new(true)),
value: Arc::new(Mutex::new(val)),
}
}
pub(crate) fn init(&self, value: T) {
let mut locked = self.value.lock().unwrap();
*locked = value;
self.present.store(true, Ordering::SeqCst);
}
pub(crate) fn with_inner(&self, mut func: impl FnMut(&mut T)) {
assert!(
self.present.load(Ordering::SeqCst),
"Tried to access `MaybePresent` before initialization."
);
let mut lock = self.value.lock().unwrap();
func(&mut lock);
}
pub(crate) fn take(&self) -> T {
assert!(
self.present.load(Ordering::SeqCst),
"Tried to access `MaybePresent` before initialization."
);
let mut locked = self.value.lock().unwrap();
self.present.store(false, Ordering::SeqCst);
std::mem::take(&mut *locked)
}
}
#[cfg(test)]
mod tests {
use crate::Level;
use crate::color::palette::css::BLUE;
use crate::dispatch::Dispatcher;
use crate::dispatch::multi_threaded::MultiThreadedDispatcher;
use crate::kurbo::{Affine, Rect, Shape};
use crate::peniko::{BlendMode, Fill};
use vello_common::paint::{Paint, PremulColor};
/// Ensure we don't cause a memory leak.
#[test]
fn allocations() {
let mut dispatcher = MultiThreadedDispatcher::new(100, 100, 4, Level::new());
for _ in 0..20 {
dispatcher.fill_path(
&Rect::new(0.0, 0.0, 50.0, 50.0).to_path(0.1),
Fill::NonZero,
Affine::IDENTITY,
Paint::Solid(PremulColor::from_alpha_color(BLUE)),
BlendMode::default(),
None,
None,
&[],
);
dispatcher.flush(&[]);
}
assert_eq!(dispatcher.allocations.paths.entries.len(), 1);
assert_eq!(dispatcher.allocations.strips.entries.len(), 1);
assert_eq!(dispatcher.allocations.render_tasks.entries.len(), 1);
assert_eq!(dispatcher.allocations.coarse_tasks.entries.len(), 1);
}
}
+130
View File
@@ -0,0 +1,130 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
/// There is not much science behind this constant. It was instead determined by doing profiling
/// and finding a constant that seems to strike a reasonable trade-off between not causing too
/// big batch sizes
pub(crate) const COST_THRESHOLD: f32 = 250.0;
use crate::dispatch::multi_threaded::RenderTaskType;
use crate::kurbo::{Affine, PathEl, PathSeg, Point, segments};
/// Try to estimate the cost of the render task.
pub(crate) fn estimate_render_task_cost(task: &RenderTaskType, paths: &[PathEl]) -> f32 {
const LAYER_COST: f32 = 10.0;
match task {
RenderTaskType::FillPath {
path_range,
transform,
..
} => {
let path = &paths[path_range.start as usize..path_range.end as usize];
estimate_path_cost(segments(path.iter().copied()), *transform, false)
}
RenderTaskType::StrokePath {
path_range,
transform,
..
} => {
let path = &paths[path_range.start as usize..path_range.end as usize];
estimate_path_cost(segments(path.iter().copied()), *transform, true)
}
RenderTaskType::PushLayer { clip_path, .. } => {
LAYER_COST
+ clip_path
.as_ref()
.map(|(path_range, transform)| {
let path = &paths[path_range.start as usize..path_range.end as usize];
estimate_path_cost(segments(path.iter().copied()), *transform, false)
})
.unwrap_or(0.0)
}
RenderTaskType::PopLayer => LAYER_COST,
}
}
/// Try to estimate an (admittedly somewhat abstract) "path cost".
///
/// The main point here is that when sending paths to a thread to convert them into sparse strip
/// representation, we might want to batch them. This is especially the case for small, line-only
/// geometries, where handling each path separately would lead to a huge overhead.
///
/// Because of this, before rendering a path, we try to estimate a very rough cost based on
/// the following attributes that (usually) have an impact on rendering times:
///
/// - Number of line segments (more line segments -> more work during strip rendering).
/// - Number of curve segments (same as line segments, plus we need to flatten them first).
/// - Path length (if the path is longer, the covered area is _likely_ to also be larger). However,
/// the path length usually grows much faster than the render time, so we only apply a very small
/// fractional value.
/// - Strokes (if we are stroking a path, there is even more overhead for stroke expansion before doing
/// flattening and strip rendering).
pub(crate) fn estimate_path_cost(
path: impl IntoIterator<Item = PathSeg>,
transform: Affine,
is_stroke: bool,
) -> f32 {
let cost_data = PathCostData::new(path, transform);
// Once again, those constants were not determined "scientifically" in any way, but
// are instead based on intuition as well as a number of experiments.
const CURVE_MULTIPLIER: f32 = 2.5;
const STROKE_MULTIPLIER: f32 = 1.5;
let mut cost = cost_data.num_line_segments as f32;
cost += cost_data.num_curve_segments as f32 * CURVE_MULTIPLIER;
cost += cost * (cost_data.path_length as f32 / 1024.0);
cost *= if is_stroke { STROKE_MULTIPLIER } else { 1.0 };
cost
}
struct PathCostData {
num_line_segments: u64,
num_curve_segments: u64,
path_length: f64,
}
impl PathCostData {
fn new(path: impl IntoIterator<Item = PathSeg>, transform: Affine) -> Self {
let mut num_line_segments = 0;
let mut num_curve_segments = 0;
let mut path_length = 0.0;
let mut register_path_length = |mut p0: Point, mut p1: Point| {
p0 = transform * p0;
p1 = transform * p1;
// We don't sqrt here because it's too expensive, we just want a rough estimate.
let dx = (p1.x - p0.x).abs();
let dy = (p1.y - p0.y).abs();
path_length += dx + dy;
};
for seg in path.into_iter() {
match seg {
PathSeg::Line(l) => {
num_line_segments += 1;
register_path_length(l.p0, l.p1);
}
PathSeg::Quad(q) => {
num_curve_segments += 1;
register_path_length(q.p0, q.p2);
}
PathSeg::Cubic(c) => {
num_curve_segments += 1;
register_path_length(c.p0, c.p3);
}
}
}
Self {
num_line_segments,
num_curve_segments,
path_length,
}
}
}
+204
View File
@@ -0,0 +1,204 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use crate::Level;
use crate::dispatch::multi_threaded::{
CoarseTask, CoarseTaskSender, CoarseTaskType, RenderTask, RenderTaskType,
};
use std::vec::Vec;
use vello_common::clip::PathDataRef;
use vello_common::strip_generator::{GenerationMode, StripGenerator, StripStorage};
#[derive(Debug)]
pub(crate) struct Worker {
strip_generator: StripGenerator,
strip_storage: StripStorage,
thread_id: u8,
}
impl Worker {
pub(crate) fn new(width: u16, height: u16, thread_id: u8, level: Level) -> Self {
let strip_generator = StripGenerator::new(width, height, level);
let strip_storage = StripStorage::default();
Self {
strip_generator,
strip_storage,
thread_id,
}
}
pub(crate) fn init(&mut self, alphas: Vec<u8>) {
self.strip_storage.alphas = alphas;
}
pub(crate) fn thread_id(&self) -> u8 {
self.thread_id
}
pub(crate) fn reset(&mut self) {
self.strip_generator.reset();
}
pub(crate) fn run_render_task(
&mut self,
mut render_task: RenderTask,
result_sender: &mut CoarseTaskSender,
) {
let num_tasks = render_task.allocation_group.render_tasks.len();
self.strip_storage.strips.clear();
self.strip_storage
.set_generation_mode(GenerationMode::Append);
let task_idx = render_task.idx;
let path_clip = render_task.clip_path.as_ref().map(|c| PathDataRef {
strips: c.strips.as_ref(),
alphas: c.alphas.as_ref(),
bbox: c.bbox,
});
for task in render_task
.allocation_group
.render_tasks
.drain(0..num_tasks)
{
match task {
RenderTaskType::FillPath {
path_range,
transform,
paint,
fill_rule,
blend_mode,
aliasing_threshold,
mask,
} => {
let start = self.strip_storage.strips.len() as u32;
let path = &render_task.allocation_group.path
[path_range.start as usize..path_range.end as usize];
self.strip_generator.generate_filled_path(
path.iter().copied(),
fill_rule,
transform,
aliasing_threshold,
&mut self.strip_storage,
path_clip,
);
let end = self.strip_storage.strips.len() as u32;
let coarse_command = CoarseTaskType::RenderPath {
thread_id: self.thread_id,
strips: start..end,
blend_mode,
paint,
mask,
};
render_task
.allocation_group
.coarse_tasks
.push(coarse_command);
}
RenderTaskType::StrokePath {
path_range,
transform,
paint,
blend_mode,
stroke,
aliasing_threshold,
mask,
} => {
let start = self.strip_storage.strips.len() as u32;
let path = &render_task.allocation_group.path
[path_range.start as usize..path_range.end as usize];
self.strip_generator.generate_stroked_path(
path.iter().copied(),
&stroke,
transform,
aliasing_threshold,
&mut self.strip_storage,
path_clip,
);
let end = self.strip_storage.strips.len() as u32;
let coarse_command = CoarseTaskType::RenderPath {
thread_id: self.thread_id,
strips: start..end,
blend_mode,
paint,
mask,
};
render_task
.allocation_group
.coarse_tasks
.push(coarse_command);
}
RenderTaskType::PushLayer {
clip_path,
blend_mode,
opacity,
mask,
fill_rule,
aliasing_threshold,
} => {
let clip = if let Some((path_range, transform)) = clip_path {
let start = self.strip_storage.strips.len() as u32;
let path = &render_task.allocation_group.path
[path_range.start as usize..path_range.end as usize];
self.strip_generator.generate_filled_path(
path.iter().copied(),
fill_rule,
transform,
aliasing_threshold,
&mut self.strip_storage,
path_clip,
);
let end = self.strip_storage.strips.len() as u32;
Some(start..end)
} else {
None
};
let coarse_command = CoarseTaskType::PushLayer {
thread_id: self.thread_id,
clip_path: clip,
blend_mode,
mask,
opacity,
};
render_task
.allocation_group
.coarse_tasks
.push(coarse_command);
}
RenderTaskType::PopLayer => {
render_task
.allocation_group
.coarse_tasks
.push(CoarseTaskType::PopLayer);
}
}
}
let taken_strips = std::mem::replace(
&mut self.strip_storage.strips,
render_task.allocation_group.strips,
);
render_task.allocation_group.strips = taken_strips;
let task = CoarseTask {
allocation_group: render_task.allocation_group,
};
result_sender.send(task_idx as usize, task).unwrap();
}
pub(crate) fn finalize(&mut self) -> Vec<u8> {
std::mem::take(&mut self.strip_storage.alphas)
}
}
+922
View File
@@ -0,0 +1,922 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use crate::RenderMode;
use crate::dispatch::Dispatcher;
use crate::fine::{Fine, FineKernel};
use crate::kurbo::{Affine, BezPath, Rect, Stroke};
use crate::layer_manager::LayerManager;
use crate::peniko::{BlendMode, Fill};
use crate::region::Regions;
use vello_common::clip::ClipContext;
use vello_common::coarse::{Cmd, LayerKind, MODE_CPU, Wide, WideTilesBbox};
use vello_common::color::palette::css::TRANSPARENT;
use vello_common::encode::EncodedPaint;
use vello_common::fearless_simd::{Level, Simd};
use vello_common::filter_effects::Filter;
use vello_common::mask::Mask;
use vello_common::paint::{ImageResolver, Paint, PremulColor};
use vello_common::pixmap::Pixmap;
use vello_common::render_graph::{RenderGraph, RenderNodeKind};
use vello_common::strip_generator::{StripGenerator, StripStorage};
/// Single-threaded implementation of the rendering dispatcher.
///
/// This dispatcher handles the entire rendering pipeline on a single thread,
/// including path rasterization, layer composition, and filter effects.
/// It maintains the coarse tile grid (`Wide`), strip generation for paths,
/// and the render graph for managing layer dependencies and filter effects.
#[derive(Debug)]
pub(crate) struct SingleThreadedDispatcher {
/// Coarse tile grid containing rendering commands for each wide tile.
wide: Wide,
/// Clip context for managing non-isolated clipping.
clip_context: ClipContext,
/// Generator for converting paths into coverage strips.
strip_generator: StripGenerator,
/// Storage for alpha coverage data from strip generation.
strip_storage: StripStorage,
/// SIMD level for fearless SIMD dispatch.
level: Level,
/// Counter for generating unique layer IDs.
layer_id_next: u32,
/// Dependency graph tracking layer relationships and filter effects.
render_graph: RenderGraph,
}
impl SingleThreadedDispatcher {
/// Creates a new single-threaded dispatcher for the given dimensions.
///
/// # Arguments
/// * `width` - Width of the rendering surface in pixels.
/// * `height` - Height of the rendering surface in pixels.
/// * `level` - SIMD level to use for rasterization.
///
/// # Notes
/// The root layer (`layer_id` 0) is created immediately and must be node 0
/// in the render graph for proper rendering order.
pub(crate) fn new(width: u16, height: u16, level: Level) -> Self {
let wide = Wide::<MODE_CPU>::new(width, height);
let strip_generator = StripGenerator::new(width, height, level);
let clip_context = ClipContext::new();
let strip_storage = StripStorage::default();
let mut render_graph = RenderGraph::new();
// Create root node (layer_id 0) as the first node (will be node 0).
// This ensures the root layer is always rendered last in the execution order.
let wtile_bbox = WideTilesBbox::new(0, 0, wide.width_tiles(), wide.height_tiles());
let root_node = render_graph.add_node(RenderNodeKind::RootLayer {
layer_id: 0,
wtile_bbox,
});
assert_eq!(root_node, 0, "Root node must be node 0");
Self {
wide,
clip_context,
strip_generator,
strip_storage,
level,
layer_id_next: 0,
render_graph,
}
}
/// Rasterizes the scene using f32 precision (high quality).
///
/// This dispatches to the appropriate SIMD implementation based on the
/// configured level, using f32 for intermediate calculations.
#[cfg(feature = "f32_pipeline")]
fn rasterize_f32(
&self,
buffer: &mut [u8],
width: u16,
height: u16,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
) {
use crate::fine::F32Kernel;
use vello_common::fearless_simd::dispatch;
dispatch!(self.level, simd => self.rasterize_with::<_, F32Kernel>(simd, buffer, width, height, encoded_paints, image_resolver));
}
/// Rasterizes the scene using u8 precision (fast).
///
/// This dispatches to the appropriate SIMD implementation based on the
/// configured level, using u8 for intermediate calculations to maximize speed.
#[cfg(feature = "u8_pipeline")]
fn rasterize_u8(
&self,
buffer: &mut [u8],
width: u16,
height: u16,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
) {
use crate::fine::U8Kernel;
use vello_common::fearless_simd::dispatch;
dispatch!(self.level, simd => self.rasterize_with::<_, U8Kernel>(simd, buffer, width, height, encoded_paints, image_resolver));
}
/// Core rasterization dispatcher that chooses between simple and filter-aware paths.
///
/// # Type Parameters
/// * `S` - SIMD implementation to use.
/// * `F` - Fine rasterization kernel (determines precision).
///
/// If the scene contains filter effects, uses the filter-aware path which maintains
/// intermediate layer buffers. Otherwise, uses the simpler direct rasterization path.
fn rasterize_with<S: Simd, F: FineKernel<S>>(
&self,
simd: S,
buffer: &mut [u8],
width: u16,
height: u16,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
) {
let mut layer_manager = LayerManager::new();
if self.has_filters() {
// Use filter-aware path that maintains layer buffers for filter effects.
self.rasterize_with_filters::<S, F>(
simd,
buffer,
width,
height,
encoded_paints,
image_resolver,
&mut layer_manager,
);
} else {
// Use simple direct rasterization for scenes without filters.
self.rasterize_simple::<S, F>(
simd,
buffer,
width,
height,
encoded_paints,
image_resolver,
);
}
}
/// Rasterizes a scene with filter effects using dependency-ordered execution.
///
/// This processes the render graph in topological order, ensuring that filtered
/// layers are rendered into intermediate buffers before being composed. Each
/// filter layer is rendered to its own pixmap, the filter is applied, and then
/// the result is stored in the layer manager for use by dependent layers.
///
/// # Render Graph Execution
/// - `FilterLayer` nodes: Render to intermediate buffer, apply filter, store result.
/// - `RootLayer` node: Final composition to output buffer.
fn rasterize_with_filters<S: Simd, F: FineKernel<S>>(
&self,
simd: S,
buffer: &mut [u8],
width: u16,
height: u16,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
layer_manager: &mut LayerManager,
) {
let mut fine = Fine::<S, F>::new(simd);
// Process nodes in dependency order (filtered layers before their consumers).
for node_id in self.render_graph.execution_order() {
let node = &self.render_graph.nodes[node_id];
match &node.kind {
RenderNodeKind::FilterLayer {
layer_id,
filter,
wtile_bbox,
transform,
} => {
// Allocate intermediate buffer for this filtered layer.
let bbox_width = wtile_bbox.width_px();
let bbox_height = wtile_bbox.height_px();
let mut pixmap = Pixmap::new(bbox_width, bbox_height);
// TODO: Re-use this allocation by adding a .configure() or similar method
// to avoid allocating the internal Vec<Region> on every filtered layer.
let mut regions =
Regions::new(bbox_width, bbox_height, pixmap.data_as_u8_slice_mut());
// Render each tile in the layer's bounding box.
regions.update_regions(|region| {
// Convert region-local coords to global wtile coords.
let x = wtile_bbox.x0() + region.x;
let y = wtile_bbox.y0() + region.y;
self.process_layer_tile(
&mut fine,
x,
y,
*layer_id,
PremulColor::from_alpha_color(TRANSPARENT),
layer_manager,
encoded_paints,
image_resolver,
);
debug_assert_eq!(
fine.blend_buf.len(),
1,
"blend buffer should contain exactly one layer after tile processing"
);
fine.pack(region);
});
// Apply the filter effect to the completed layer.
fine.filter_layer(&mut pixmap, filter, layer_manager, *transform);
// Save the filtered pixmap to disk for debugging.
// #[cfg(all(debug_assertions, feature = "std", feature = "png"))]
// save_filtered_layer_debug(&pixmap, *layer_id);
// Store the filtered result for use by dependent layers.
layer_manager.register_layer(*layer_id, *wtile_bbox, pixmap);
}
RenderNodeKind::RootLayer {
layer_id,
wtile_bbox: _,
} => {
// Final composition directly to output buffer.
let mut regions = Regions::new(width, height, buffer);
regions.update_regions(|region| {
// Use the background color from the wide tile.
let bg = self.wide.get(region.x, region.y).bg;
self.process_layer_tile(
&mut fine,
region.x,
region.y,
*layer_id,
bg,
layer_manager,
encoded_paints,
image_resolver,
);
debug_assert_eq!(
fine.blend_buf.len(),
1,
"blend buffer should contain exactly one layer after tile processing"
);
fine.pack(region);
});
}
}
}
}
/// Processes all rendering commands for a single layer within a specific tile.
///
/// This handles the complex logic of composing filtered layers by:
/// 1. Running normal rendering commands in sequence.
/// 2. When encountering a filtered layer reference, compositing its pre-rendered
/// content from the layer manager.
/// 3. Skipping the filtered layer's internal commands (already rendered separately).
///
/// # Arguments
/// * `fine` - The fine rasterizer instance.
/// * `x`, `y` - Wide tile coordinates.
/// * `layer_id` - The layer being processed.
/// * `clear_color` - Initial color for the tile.
/// * `layer_manager` - Storage for filtered layer buffers.
/// * `encoded_paints` - Paint definitions for the scene.
/// * `image_resolver` - Resolver for looking up opaque image IDs.
fn process_layer_tile<S: Simd, F: FineKernel<S>>(
&self,
fine: &mut Fine<S, F>,
x: u16,
y: u16,
layer_id: u32,
clear_color: PremulColor,
layer_manager: &mut LayerManager,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
) {
let wtile = &self.wide.get(x, y);
fine.set_coords(x, y);
fine.clear(clear_color);
// Process all commands in this layer's render range.
// It can happen that the layer has no associated ranges in this wide tile in
// case they have been cleared by setting a new wide tile background, for example
// when filling a full-tile opaque solid color.
let Some(ranges) = wtile.layer_cmd_ranges.get(&layer_id) else {
return;
};
let mut cmd_idx = ranges.render_range.start;
while cmd_idx < ranges.render_range.end {
let cmd: &Cmd = &wtile.cmds[cmd_idx];
fine.run_cmd(
cmd,
&self.strip_storage.alphas,
encoded_paints,
image_resolver,
&self.wide.attrs,
);
// Special handling for filtered layer composition.
// Filtered layers have already been rendered and stored in layer_manager.
// Here we composite them into the current buffer, with special handling for clipping.
if let Cmd::PushBuf(LayerKind::Filtered(child_layer_id), _) = cmd {
// Unlike above, the unwrap is safe here because as long as the filtered layer
// is referenced in the wide tile, it must have associated layer ranges.
let filtered_ranges = wtile.layer_cmd_ranges.get(child_layer_id).unwrap();
// Check what comes after the filtered layer push to determine clipping state
match wtile.cmds.get(cmd_idx + 1) {
// Zero-clip region: tile is completely outside the clip path.
// The layer was already rendered for filtering, but we skip compositing
// since this tile is entirely clipped out.
// (PushZeroClip only appears for clipped filter layers)
// See https://github.com/linebender/vello/pull/1541/ for why we
// add the ID check.
Some(Cmd::PushZeroClip(id)) if *id == *child_layer_id => {
// If we have a zero-clip, it means that the whole layer should not be drawn.
// Therefore, we want to skip to the very end so that only `PopBuf` will
// be run. Therefore, we jump to `filtered_ranges.full_range.end - 1`.
cmd_idx = filtered_ranges.full_range.end - 1;
continue;
}
// Partial clip: push the clip buffer, then composite the filtered layer
Some(Cmd::PushBuf(LayerKind::Clip(id), _)) if *id == *child_layer_id => {
fine.run_cmd(
&wtile.cmds[cmd_idx + 1],
&self.strip_storage.alphas,
encoded_paints,
image_resolver,
&self.wide.attrs,
);
cmd_idx += 1;
if let Some(mut region) =
layer_manager.layer_tile_region_mut(*child_layer_id, x, y)
{
fine.unpack(&mut region);
}
}
// No clip or fully inside clip: composite the filtered layer directly
_ => {
if let Some(mut region) =
layer_manager.layer_tile_region_mut(*child_layer_id, x, y)
{
fine.unpack(&mut region);
}
}
}
// Skip past the filtered layer's internal commands, as they were already
// rendered when the FilterLayer node was processed earlier.
cmd_idx = filtered_ranges.render_range.end.max(cmd_idx + 1);
} else {
cmd_idx += 1;
}
}
}
/// Simple rasterization path for scenes without filter effects.
///
/// This directly processes each tile's commands without maintaining intermediate
/// layer buffers. All rendering happens in a single pass directly to the output buffer.
/// This is more efficient than the filter-aware path when no filters are present.
fn rasterize_simple<S: Simd, F: FineKernel<S>>(
&self,
simd: S,
buffer: &mut [u8],
width: u16,
height: u16,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
) {
let mut regions = Regions::new(width, height, buffer);
let mut fine = Fine::<S, F>::new(simd);
regions.update_regions(|region| {
let x = region.x;
let y = region.y;
let wtile = self.wide.get(x, y);
fine.set_coords(x, y);
// Clear to background and process all commands in order.
fine.clear(wtile.bg);
for cmd in &wtile.cmds {
fine.run_cmd(
cmd,
&self.strip_storage.alphas,
encoded_paints,
image_resolver,
&self.wide.attrs,
);
}
fine.pack(region);
});
}
/// Returns true if the scene contains any filter effects.
fn has_filters(&self) -> bool {
self.render_graph.has_filters()
}
/// Composites at an offset using f32 precision (high quality).
#[cfg(feature = "f32_pipeline")]
fn composite_at_offset_f32(
&self,
buffer: &mut [u8],
width: u16,
height: u16,
dst_x: u16,
dst_y: u16,
dst_buffer_width: u16,
dst_buffer_height: u16,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
) {
use crate::fine::F32Kernel;
use vello_common::fearless_simd::dispatch;
dispatch!(self.level, simd => self.composite_at_offset_with::<_, F32Kernel>(
simd, buffer, width, height, dst_x, dst_y, dst_buffer_width, dst_buffer_height, encoded_paints, image_resolver
));
}
/// Composites at an offset using u8 precision (fast).
#[cfg(feature = "u8_pipeline")]
fn composite_at_offset_u8(
&self,
buffer: &mut [u8],
width: u16,
height: u16,
dst_x: u16,
dst_y: u16,
dst_buffer_width: u16,
dst_buffer_height: u16,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
) {
use crate::fine::U8Kernel;
use vello_common::fearless_simd::dispatch;
dispatch!(self.level, simd => self.composite_at_offset_with::<_, U8Kernel>(
simd, buffer, width, height, dst_x, dst_y, dst_buffer_width, dst_buffer_height, encoded_paints, image_resolver
));
}
/// Core implementation for compositing at an offset.
///
/// Composites tiles sequentially, writing directly to the destination buffer
/// at the specified offset.
fn composite_at_offset_with<S: Simd, F: FineKernel<S>>(
&self,
simd: S,
buffer: &mut [u8],
width: u16,
height: u16,
dst_x: u16,
dst_y: u16,
dst_buffer_width: u16,
dst_buffer_height: u16,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
) {
let mut regions = Regions::new_at_offset(
width,
height,
dst_x,
dst_y,
dst_buffer_width,
dst_buffer_height,
buffer,
);
let mut fine = Fine::<S, F>::new(simd);
regions.update_regions(|region| {
let x = region.x;
let y = region.y;
let wtile = self.wide.get(x, y);
fine.set_coords(x, y);
// Unpack existing pixel data from the region instead of clearing,
// so that rendering composites onto the existing pixmap contents.
fine.unpack(region);
for cmd in &wtile.cmds {
fine.run_cmd(
cmd,
&self.strip_storage.alphas,
encoded_paints,
image_resolver,
&self.wide.attrs,
);
}
fine.pack(region);
});
}
}
impl Dispatcher for SingleThreadedDispatcher {
fn wide(&self) -> &Wide {
&self.wide
}
fn fill_path(
&mut self,
path: &BezPath,
fill_rule: Fill,
transform: Affine,
paint: Paint,
blend_mode: BlendMode,
aliasing_threshold: Option<u8>,
mask: Option<Mask>,
encoded_paints: &[EncodedPaint],
) {
let wide = &mut self.wide;
// Convert path to coverage strips.
self.strip_generator.generate_filled_path(
path,
fill_rule,
transform,
aliasing_threshold,
&mut self.strip_storage,
self.clip_context.get(),
);
// Generate coarse-level commands from strips (thread_idx 0 for single-threaded).
wide.generate(
&self.strip_storage.strips,
paint,
blend_mode,
0,
mask,
encoded_paints,
);
}
fn stroke_path(
&mut self,
path: &BezPath,
stroke: &Stroke,
transform: Affine,
paint: Paint,
blend_mode: BlendMode,
aliasing_threshold: Option<u8>,
mask: Option<Mask>,
encoded_paints: &[EncodedPaint],
) {
let wide = &mut self.wide;
// Convert stroked path to coverage strips.
self.strip_generator.generate_stroked_path(
path,
stroke,
transform,
aliasing_threshold,
&mut self.strip_storage,
self.clip_context.get(),
);
// Generate coarse-level commands from strips (thread_idx 0 for single-threaded).
wide.generate(
&self.strip_storage.strips,
paint,
blend_mode,
0,
mask,
encoded_paints,
);
}
fn fill_rect_fast(
&mut self,
rect: &Rect,
paint: Paint,
blend_mode: BlendMode,
mask: Option<Mask>,
encoded_paints: &[EncodedPaint],
) {
let wide = &mut self.wide;
// Generate strips directly for the rectangle (bypasses path processing).
self.strip_generator.generate_filled_rect_fast(
rect,
&mut self.strip_storage,
self.clip_context.get(),
);
// Generate coarse-level commands from strips (thread_idx 0 for single-threaded).
wide.generate(
&self.strip_storage.strips,
paint,
blend_mode,
0,
mask,
encoded_paints,
);
}
fn push_layer(
&mut self,
clip_path: Option<&BezPath>,
fill_rule: Fill,
transform: Affine,
blend_mode: BlendMode,
opacity: f32,
aliasing_threshold: Option<u8>,
mask: Option<Mask>,
filter: Option<Filter>,
) {
// Allocate a new unique layer ID.
self.layer_id_next += 1;
// Generate clip coverage if a clip path is provided.
let clip = if let Some(c) = clip_path {
self.strip_generator.generate_filled_path(
c,
fill_rule,
transform,
aliasing_threshold,
&mut self.strip_storage,
self.clip_context.get(),
);
Some(self.strip_storage.strips.as_slice())
} else {
None
};
// Push the layer onto the coarse tile stack and update render graph.
self.wide.push_layer(
self.layer_id_next,
clip,
blend_mode,
mask,
opacity,
filter,
transform,
&mut self.render_graph,
0,
);
}
fn pop_layer(&mut self) {
// Pop the current layer and update render graph.
self.wide.pop_layer(&mut self.render_graph);
}
fn reset(&mut self) {
// Clear all rendering state to prepare for a new scene.
self.wide.reset();
self.clip_context.reset();
self.strip_generator.reset();
self.strip_storage.clear();
self.render_graph.clear();
self.layer_id_next = 0;
// Recreate root node as node 0 (required for proper execution order).
let root_node = self.render_graph.add_node(RenderNodeKind::RootLayer {
layer_id: 0,
wtile_bbox: WideTilesBbox::new(0, 0, self.wide.width_tiles(), self.wide.height_tiles()),
});
debug_assert_eq!(root_node, 0, "Root node must be node 0");
// Reset layer ID counter.
self.layer_id_next = 0;
}
fn flush(&mut self, _encoded_paints: &[EncodedPaint]) {
// No-op for single-threaded dispatcher (no work queue to flush).
}
fn rasterize(
&self,
buffer: &mut [u8],
render_mode: RenderMode,
width: u16,
height: u16,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
) {
// If only the u8 pipeline is enabled, then use it
#[cfg(all(feature = "u8_pipeline", not(feature = "f32_pipeline")))]
{
let _ = render_mode;
self.rasterize_u8(buffer, width, height, encoded_paints, image_resolver);
}
// If only the f32 pipeline is enabled, then use it
#[cfg(all(feature = "f32_pipeline", not(feature = "u8_pipeline")))]
{
let _ = render_mode;
self.rasterize_f32(buffer, width, height, encoded_paints, image_resolver);
}
// If both pipelines are enabled, select precision based on render mode parameter.
#[cfg(all(feature = "u8_pipeline", feature = "f32_pipeline"))]
match render_mode {
RenderMode::OptimizeSpeed => {
// Use u8 precision for faster rendering.
self.rasterize_u8(buffer, width, height, encoded_paints, image_resolver);
}
RenderMode::OptimizeQuality => {
// Use f32 precision for higher quality.
self.rasterize_f32(buffer, width, height, encoded_paints, image_resolver);
}
}
#[cfg(all(not(feature = "u8_pipeline"), not(feature = "f32_pipeline")))]
{
// This case never gets hit because there is a compile_error in the root.
// But have this code disables some warnings and makes the compile error easier to read
let _ = (
buffer,
render_mode,
width,
height,
encoded_paints,
image_resolver,
);
}
}
fn composite_at_offset(
&self,
buffer: &mut [u8],
width: u16,
height: u16,
dst_x: u16,
dst_y: u16,
dst_buffer_width: u16,
dst_buffer_height: u16,
render_mode: RenderMode,
encoded_paints: &[EncodedPaint],
image_resolver: &dyn ImageResolver,
) {
#[cfg(all(feature = "u8_pipeline", not(feature = "f32_pipeline")))]
{
let _ = render_mode;
self.composite_at_offset_u8(
buffer,
width,
height,
dst_x,
dst_y,
dst_buffer_width,
dst_buffer_height,
encoded_paints,
image_resolver,
);
}
#[cfg(all(feature = "f32_pipeline", not(feature = "u8_pipeline")))]
{
let _ = render_mode;
self.composite_at_offset_f32(
buffer,
width,
height,
dst_x,
dst_y,
dst_buffer_width,
dst_buffer_height,
encoded_paints,
image_resolver,
);
}
#[cfg(all(feature = "u8_pipeline", feature = "f32_pipeline"))]
match render_mode {
RenderMode::OptimizeSpeed => {
self.composite_at_offset_u8(
buffer,
width,
height,
dst_x,
dst_y,
dst_buffer_width,
dst_buffer_height,
encoded_paints,
image_resolver,
);
}
RenderMode::OptimizeQuality => {
self.composite_at_offset_f32(
buffer,
width,
height,
dst_x,
dst_y,
dst_buffer_width,
dst_buffer_height,
encoded_paints,
image_resolver,
);
}
}
#[cfg(all(not(feature = "u8_pipeline"), not(feature = "f32_pipeline")))]
{
let _ = (
buffer,
width,
height,
dst_x,
dst_y,
dst_buffer_width,
dst_buffer_height,
render_mode,
encoded_paints,
image_resolver,
);
}
}
fn push_clip_path(
&mut self,
path: &BezPath,
fill_rule: Fill,
transform: Affine,
aliasing_threshold: Option<u8>,
) {
self.clip_context.push_clip(
path,
&mut self.strip_generator,
fill_rule,
transform,
aliasing_threshold,
);
}
fn pop_clip_path(&mut self) {
self.clip_context.pop_clip();
}
}
/// Saves a filtered pixmap to disk for debugging purposes.
/// Only available in debug builds with `std` and `png` features enabled.
#[allow(
dead_code,
reason = "useful debug utility, can be enabled by uncommenting the call site"
)]
#[cfg(all(debug_assertions, feature = "std", feature = "png"))]
fn save_filtered_layer_debug(pixmap: &Pixmap, layer_id: u32) {
use std::path::PathBuf;
let diffs_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../vello_sparse_tests/diffs");
let _ = std::fs::create_dir_all(&diffs_path);
let filename = diffs_path.join(alloc::format!("filtered_layer_{}.png", layer_id));
if let Ok(png_data) = pixmap.clone().into_png() {
let _ = std::fs::write(&filename, &png_data);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kurbo::Rect;
use vello_common::color::palette::css::BLUE;
use vello_common::kurbo::Shape;
use vello_common::paint::PremulColor;
/// Verifies that `reset()` properly clears all internal buffers and state.
///
/// This is important to ensure that a dispatcher can be reused for multiple
/// rendering passes without accumulating stale data from previous frames.
#[test]
fn buffers_cleared_on_reset() {
let mut dispatcher = SingleThreadedDispatcher::new(100, 100, Level::new());
// Render a simple shape to populate internal buffers.
dispatcher.fill_path(
&Rect::new(0.0, 0.0, 50.0, 50.0).to_path(0.1),
Fill::NonZero,
Affine::IDENTITY,
Paint::Solid(PremulColor::from_alpha_color(BLUE)),
BlendMode::default(),
None,
None,
&[],
);
// Ensure there is data to clear.
assert!(!dispatcher.strip_storage.alphas.is_empty());
assert!(!dispatcher.wide.get(0, 0).cmds.is_empty());
dispatcher.reset();
// Verify all buffers are cleared.
assert!(dispatcher.strip_storage.alphas.is_empty());
assert!(dispatcher.wide.get(0, 0).cmds.is_empty());
assert_eq!(dispatcher.layer_id_next, 0);
}
}
+312
View File
@@ -0,0 +1,312 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Drop shadow filter implementation.
//!
//! This implements the feDropShadow primitive from SVG Filter Effects 2.
//! The drop shadow effect is a shorthand for a commonly used sequence of filter operations:
//! 1. Extract alpha channel
//! 2. Offset the alpha
//! 3. Blur the offset alpha
//! 4. Composite shadow color with blurred alpha
//! 5. Composite shadow with original graphic
//!
//! @see <https://drafts.fxtf.org/filter-effects-2/#feDropShadowElement>
use super::FilterEffect;
use super::gaussian_blur::apply_blur;
use super::shift::offset_pixels;
use crate::layer_manager::LayerManager;
use vello_common::color::{AlphaColor, Srgb};
use vello_common::filter::drop_shadow::DropShadow;
use vello_common::filter_effects::EdgeMode;
use vello_common::peniko::color::PremulRgba8;
#[cfg(not(feature = "std"))]
use vello_common::peniko::kurbo::common::FloatFuncs as _;
use vello_common::pixmap::Pixmap;
impl FilterEffect for DropShadow {
fn execute_lowp(&self, pixmap: &mut Pixmap, layer_manager: &mut LayerManager) {
apply_drop_shadow(
pixmap,
self.dx,
self.dy,
self.std_deviation,
self.n_decimations,
&self.kernel[..usize::from(self.kernel_size)],
self.color,
self.edge_mode,
layer_manager,
);
}
fn execute_highp(&self, pixmap: &mut Pixmap, layer_manager: &mut LayerManager) {
// TODO: Currently only lowp is implemented and used for highp as well.
// This needs to be updated to use proper high-precision arithmetic.
Self::execute_lowp(self, pixmap, layer_manager);
}
}
/// Apply drop shadow effect.
///
/// This is the main entry point that splits the drop shadow operation into well-defined steps:
/// 1. Offset the shadow pixels
/// 2. Blur the already-offset shadow
/// 3. Apply shadow color and composite with original
fn apply_drop_shadow(
pixmap: &mut Pixmap,
dx: f32,
dy: f32,
std_deviation: f32,
n_decimations: usize,
kernel: &[f32],
color: AlphaColor<Srgb>,
edge_mode: EdgeMode,
layer_manager: &mut LayerManager,
) {
// Clone pixmap to create shadow buffer
let mut shadow_pixmap = pixmap.clone();
// Step 1: Offset the shadow pixels
offset_pixels(&mut shadow_pixmap, dx, dy);
// Step 2: Blur the already-offset shadow
if std_deviation > 0.0 {
let scratch =
layer_manager.get_scratch_buffer(shadow_pixmap.width(), shadow_pixmap.height());
apply_blur(
&mut shadow_pixmap,
scratch,
n_decimations,
kernel,
edge_mode,
);
}
// Step 3: Apply shadow color and composite with original
compose_shadow_direct(&shadow_pixmap, pixmap, color);
}
/// Apply shadow color and composite with original.
///
/// The shadow has already been offset and blurred, so this simply applies
/// the shadow color to the alpha channel and composites using source-over.
fn compose_shadow_direct(shadow: &Pixmap, dst: &mut Pixmap, color: AlphaColor<Srgb>) {
let width = dst.width();
let height = dst.height();
// Precompute shadow color components
let shadow_r = (color.components[0] * 255.0).round() as u8;
let shadow_g = (color.components[1] * 255.0).round() as u8;
let shadow_b = (color.components[2] * 255.0).round() as u8;
for y in 0..height {
for x in 0..width {
// Sample alpha directly (shadow is already offset)
let alpha = shadow.sample(x, y).a;
// Apply shadow color to alpha
let shadow_alpha = (u8_to_norm(alpha) * color.components[3]).min(1.0);
let final_alpha = norm_to_u8(shadow_alpha);
// Premultiply RGB by alpha as required by PremulRgba8
let alpha_u16 = u16::from(final_alpha);
let premultiply = |channel: u8| ((u16::from(channel) * alpha_u16) / 255) as u8;
let colored_shadow = PremulRgba8 {
r: premultiply(shadow_r),
g: premultiply(shadow_g),
b: premultiply(shadow_b),
a: final_alpha,
};
// Read original and composite: original over shadow
let original_pixel = dst.sample(x, y);
let result = compose_src_over(original_pixel, colored_shadow);
dst.set_pixel(x, y, result);
}
}
}
/// Composite two pixels using Porter-Duff "source over" operator.
///
/// Composes the source pixel over the destination pixel using premultiplied
/// alpha blending. Returns the composited result.
///
/// Formula for premultiplied colors: `result = src + dst * (1 - src_alpha)`
fn compose_src_over(src: PremulRgba8, dst: PremulRgba8) -> PremulRgba8 {
let src_a = u8_to_norm(src.a);
PremulRgba8 {
r: src_over_channel(src.r, dst.r, src_a),
g: src_over_channel(src.g, dst.g, src_a),
b: src_over_channel(src.b, dst.b, src_a),
a: src_over_channel(src.a, dst.a, src_a),
}
}
/// Blend a single channel using Porter-Duff "source over" operator.
///
/// For premultiplied colors, the formula is: `result = src + dst * (1 - src_alpha)`
#[inline]
fn src_over_channel(src: u8, dst: u8, src_alpha: f32) -> u8 {
let result = u8_to_norm(src) + u8_to_norm(dst) * (1.0 - src_alpha);
norm_to_u8(result)
}
/// Convert a u8 color component (0-255) to normalized f32 (0.0-1.0).
#[inline]
fn u8_to_norm(value: u8) -> f32 {
value as f32 / 255.0
}
/// Convert a normalized f32 (0.0-1.0) to u8 color component (0-255).
#[inline]
fn norm_to_u8(value: f32) -> u8 {
(value * 255.0).round() as u8
}
#[cfg(test)]
mod tests {
use super::*;
use vello_common::color::Srgb;
/// Test `u8_to_norm` conversion.
#[test]
fn test_u8_to_norm() {
assert_eq!(u8_to_norm(0), 0.0);
assert!((u8_to_norm(255) - 1.0).abs() < 1e-6);
}
/// Test `norm_to_u8` conversion.
#[test]
fn test_norm_to_u8() {
assert_eq!(norm_to_u8(0.0), 0);
assert_eq!(norm_to_u8(1.0), 255);
assert_eq!(norm_to_u8(0.5), 128); // 0.5 * 255 = 127.5 → 128
}
/// Test round-trip conversion u8 → norm → u8.
#[test]
fn test_conversion_roundtrip() {
for value in [0, 1, 50, 127, 128, 200, 254, 255] {
let normalized = u8_to_norm(value);
let back = norm_to_u8(normalized);
assert_eq!(back, value);
}
}
/// Test Porter-Duff source-over with fully opaque source.
#[test]
fn test_compose_src_over_opaque_source() {
let src = PremulRgba8 {
r: 255,
g: 0,
b: 0,
a: 255,
}; // Opaque red
let dst = PremulRgba8 {
r: 0,
g: 255,
b: 0,
a: 255,
}; // Opaque green
let result = compose_src_over(src, dst);
// Opaque source should completely cover destination
assert_eq!(result.r, 255);
assert_eq!(result.g, 0);
assert_eq!(result.b, 0);
assert_eq!(result.a, 255);
}
/// Test Porter-Duff source-over with transparent source.
#[test]
fn test_compose_src_over_transparent_source() {
let src = PremulRgba8 {
r: 0,
g: 0,
b: 0,
a: 0,
};
let dst = PremulRgba8 {
r: 0,
g: 255,
b: 0,
a: 255,
};
let result = compose_src_over(src, dst);
// Transparent source should leave destination unchanged
assert_eq!(result.r, 0);
assert_eq!(result.g, 255);
assert_eq!(result.b, 0);
assert_eq!(result.a, 255);
}
/// Test Porter-Duff source-over with semi-transparent source.
#[test]
fn test_compose_src_over_semi_transparent() {
let src = PremulRgba8 {
r: 128,
g: 0,
b: 0,
a: 128,
}; // 50% red (premul)
let dst = PremulRgba8 {
r: 0,
g: 128,
b: 0,
a: 128,
}; // 50% green (premul)
let result = compose_src_over(src, dst);
// Result should blend src + dst*(1-src_alpha)
// r: 128 + 0*(1-0.5) = 128
// g: 0 + 128*0.5 = 64
// a: 128 + 128*0.5 = 192
assert_eq!(
result,
PremulRgba8 {
r: 128,
g: 64,
b: 0,
a: 192,
}
);
}
/// Test `compose_shadow_direct` applies color correctly.
#[test]
fn test_compose_shadow_color() {
let mut shadow_pixmap = Pixmap::new(2, 2);
let mut dst_pixmap = Pixmap::new(2, 2);
// Shadow has alpha=255 at (0,0)
shadow_pixmap.set_pixel(
0,
0,
PremulRgba8 {
r: 0,
g: 0,
b: 0,
a: 255,
},
);
let shadow_color = AlphaColor {
components: [1.0, 0.0, 0.0, 1.0], // Red
cs: std::marker::PhantomData::<Srgb>,
};
compose_shadow_direct(&shadow_pixmap, &mut dst_pixmap, shadow_color);
// Shadow at (0,0) should be red
let result = dst_pixmap.sample(0, 0);
assert_eq!(result.r, 255);
assert_eq!(result.g, 0);
assert_eq!(result.b, 0);
assert_eq!(result.a, 255);
}
}
+89
View File
@@ -0,0 +1,89 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Flood filter implementation.
use super::FilterEffect;
use crate::layer_manager::LayerManager;
use vello_common::filter::flood::Flood;
use vello_common::pixmap::Pixmap;
impl FilterEffect for Flood {
fn execute_lowp(&self, pixmap: &mut Pixmap, _layer_manager: &mut LayerManager) {
pixmap.data_mut().fill(self.color.premultiply().to_rgba8());
}
fn execute_highp(&self, pixmap: &mut Pixmap, _layer_manager: &mut LayerManager) {
pixmap.data_mut().fill(self.color.premultiply().to_rgba8());
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::AlphaColor;
use crate::layer_manager::LayerManager;
use vello_common::color::{PremulRgba8, Srgb};
/// Test flood with semi-transparent color - verifies correct premultiplication.
#[test]
fn test_flood_semi_transparent_lowp() {
let mut pixmap = Pixmap::new(2, 2);
let mut layer_manager = LayerManager::new();
// Semi-transparent white (50% alpha)
let color = AlphaColor {
components: [1.0, 1.0, 1.0, 0.5],
cs: std::marker::PhantomData::<Srgb>,
};
let flood = Flood::new(color);
flood.execute_lowp(&mut pixmap, &mut layer_manager);
// RGB should be premultiplied by alpha: 255 * 0.5 = 127-128
for y in 0..2 {
for x in 0..2 {
let pixel = pixmap.sample(x, y);
assert_eq!(
pixel,
PremulRgba8 {
r: 128,
g: 128,
b: 128,
a: 128
}
);
}
}
}
/// Test flood highp with semi-transparent color - verifies correct premultiplication.
#[test]
fn test_flood_semi_transparent_highp() {
let mut pixmap = Pixmap::new(2, 2);
let mut layer_manager = LayerManager::new();
// Semi-transparent white (50% alpha)
let color = AlphaColor {
components: [1.0, 1.0, 1.0, 0.5],
cs: std::marker::PhantomData::<Srgb>,
};
let flood = Flood::new(color);
flood.execute_highp(&mut pixmap, &mut layer_manager);
// RGB should be premultiplied by alpha: 255 * 0.5 = 127-128
for y in 0..2 {
for x in 0..2 {
let pixel = pixmap.sample(x, y);
assert_eq!(
pixel,
PremulRgba8 {
r: 128,
g: 128,
b: 128,
a: 128
}
);
}
}
}
}
File diff suppressed because it is too large Load Diff
+120
View File
@@ -0,0 +1,120 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Filter effects implementation for `vello_cpu`.
//!
//! This module provides CPU-based implementations of SVG filter effects,
//! supporting both low-precision (u8) and high-precision (f32) rendering paths.
//! Filters are applied to layers through the layer manager, which handles
//! intermediate storage.
mod drop_shadow;
mod flood;
mod gaussian_blur;
mod offset;
mod shift;
use crate::layer_manager::LayerManager;
use vello_common::filter::PreparedFilter;
use vello_common::filter_effects::Filter;
use vello_common::kurbo::Affine;
use vello_common::pixmap::Pixmap;
/// Trait for filter effects that can be applied to layers.
///
/// Each filter implements this trait with both u8 and f32 variants
/// to support different rendering backends and precision requirements.
/// The low-precision path (`execute_lowp`) uses 8-bit color channels for
/// better performance, while the high-precision path (`execute_highp`)
/// uses 32-bit floating-point for higher quality at the cost of speed.
pub(crate) trait FilterEffect {
/// Apply the low-precision (u8) version of the filter.
///
/// # Arguments
/// * `pixmap` - The target pixmap containing rendering metadata
/// * `layer_manager` - Manager for allocating and accessing intermediate layers
fn execute_lowp(&self, pixmap: &mut Pixmap, layer_manager: &mut LayerManager);
/// Apply the high-precision (f32) version of the filter.
///
/// # Arguments
/// * `pixmap` - The target pixmap containing rendering metadata
/// * `layer_manager` - Manager for allocating and accessing intermediate layers
fn execute_highp(&self, pixmap: &mut Pixmap, layer_manager: &mut LayerManager);
}
/// Apply the low-precision (u8) version of a filter effect to a layer.
///
/// This function dispatches filter primitives from a filter graph to their
/// corresponding CPU implementations using 8-bit color channels.
///
/// # Arguments
/// * `filter` - The filter containing the graph of primitives to apply
/// * `pixmap` - The target pixmap containing rendering metadata
/// * `layer_manager` - Manager for allocating and accessing intermediate layers
/// * `transform` - The transformation matrix to extract scale from for filter parameters
///
/// # Limitations
/// Currently only supports filter graphs with a single primitive.
/// Multi-primitive filter graphs are not yet implemented.
pub(crate) fn filter_lowp(
filter: &Filter,
pixmap: &mut Pixmap,
layer_manager: &mut LayerManager,
transform: Affine,
) {
let prepared_filter = PreparedFilter::new(filter, &transform);
match prepared_filter {
PreparedFilter::Flood(flood) => {
flood.execute_lowp(pixmap, layer_manager);
}
PreparedFilter::GaussianBlur(blur) => {
blur.execute_lowp(pixmap, layer_manager);
}
PreparedFilter::Offset(offset) => {
offset.execute_lowp(pixmap, layer_manager);
}
PreparedFilter::DropShadow(drop_shadow) => {
drop_shadow.execute_lowp(pixmap, layer_manager);
}
}
}
/// Apply the high-precision (f32) version of a filter effect to a layer.
///
/// This function dispatches filter primitives from a filter graph to their
/// corresponding CPU implementations using 32-bit floating-point color channels.
///
/// # Arguments
/// * `filter` - The filter containing the graph of primitives to apply
/// * `pixmap` - The target pixmap containing rendering metadata
/// * `layer_manager` - Manager for allocating and accessing intermediate layers
/// * `transform` - The transformation matrix to extract scale from for filter parameters
///
/// # Limitations
/// Currently only supports filter graphs with a single primitive.
/// Multi-primitive filter graphs are not yet implemented.
pub(crate) fn filter_highp(
filter: &Filter,
pixmap: &mut Pixmap,
layer_manager: &mut LayerManager,
transform: Affine,
) {
let prepared_filter = PreparedFilter::new(filter, &transform);
match prepared_filter {
PreparedFilter::Flood(flood) => {
flood.execute_highp(pixmap, layer_manager);
}
PreparedFilter::GaussianBlur(blur) => {
blur.execute_highp(pixmap, layer_manager);
}
PreparedFilter::Offset(offset) => {
offset.execute_highp(pixmap, layer_manager);
}
PreparedFilter::DropShadow(drop_shadow) => {
drop_shadow.execute_highp(pixmap, layer_manager);
}
}
}
+44
View File
@@ -0,0 +1,44 @@
// Copyright 2026 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! `feOffset` filter primitive implementation.
use vello_common::filter::offset::Offset;
use vello_common::pixmap::Pixmap;
use super::FilterEffect;
use super::shift::offset_pixels;
use crate::layer_manager::LayerManager;
impl FilterEffect for Offset {
fn execute_lowp(&self, pixmap: &mut Pixmap, _: &mut LayerManager) {
offset_pixels(pixmap, self.dx, self.dy);
}
fn execute_highp(&self, pixmap: &mut Pixmap, _: &mut LayerManager) {
offset_pixels(pixmap, self.dx, self.dy);
}
}
#[cfg(test)]
mod tests {
use super::Offset;
use crate::filter::FilterEffect;
use crate::layer_manager::LayerManager;
use vello_common::peniko::color::PremulRgba8;
use vello_common::pixmap::Pixmap;
#[test]
fn offset_moves_pixels_and_clears_uncovered_area() {
let mut layer_manager = LayerManager::new();
let mut pixmap = Pixmap::new(4, 3);
pixmap.set_pixel(1, 1, PremulRgba8::from_u32(0xff_00_00_ff)); // premul red, opaque
Offset::new(2.0, -1.0).execute_lowp(&mut pixmap, &mut layer_manager);
// Original pixel (1,1) moved to (3,0).
assert_eq!(pixmap.sample(3, 0), PremulRgba8::from_u32(0xff_00_00_ff));
// Original location cleared.
assert_eq!(pixmap.sample(1, 1), PremulRgba8::from_u32(0));
}
}
+253
View File
@@ -0,0 +1,253 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Shared helpers for pixel-space filter operations.
use vello_common::color::palette::css::TRANSPARENT;
#[cfg(not(feature = "std"))]
use vello_common::kurbo::common::FloatFuncs as _;
use vello_common::peniko::color::PremulRgba8;
use vello_common::pixmap::Pixmap;
/// Shift all pixels in a pixmap by the given offset.
///
/// This implements the offset operation in-place by copying pixels to their new positions.
/// The iteration order is carefully chosen based on shift direction to avoid overwriting
/// source pixels before they're read. Areas that become exposed (due to the shift) are
/// filled with transparent black. Pixels that would move outside the bounds are discarded.
pub(crate) fn offset_pixels(pixmap: &mut Pixmap, dx: f32, dy: f32) {
let dx_pixels = dx.round() as i32;
let dy_pixels = dy.round() as i32;
// Early return if no offset
if dx_pixels == 0 && dy_pixels == 0 {
return;
}
let width = pixmap.width();
let height = pixmap.height();
let transparent = TRANSPARENT.premultiply().to_rgba8();
// Process pixels in the correct order to avoid overwriting source data.
// Key insight: iterate away from the direction of movement.
// This allows us to move pixels in-place without a temporary buffer.
//
// Use match to eliminate Box<dyn Iterator> allocation overhead and enable
// better compiler optimization through static dispatch.
match (dx_pixels >= 0, dy_pixels >= 0) {
(true, true) => {
// Shift right+down: iterate bottom-to-top, right-to-left
for y in (0..height).rev() {
for x in (0..width).rev() {
process_offset_pixel(
pixmap,
x,
y,
dx_pixels,
dy_pixels,
width,
height,
transparent,
);
}
}
}
(true, false) => {
// Shift right+up: iterate top-to-bottom, right-to-left
for y in 0..height {
for x in (0..width).rev() {
process_offset_pixel(
pixmap,
x,
y,
dx_pixels,
dy_pixels,
width,
height,
transparent,
);
}
}
}
(false, true) => {
// Shift left+down: iterate bottom-to-top, left-to-right
for y in (0..height).rev() {
for x in 0..width {
process_offset_pixel(
pixmap,
x,
y,
dx_pixels,
dy_pixels,
width,
height,
transparent,
);
}
}
}
(false, false) => {
// Shift left+up: iterate top-to-bottom, left-to-right
for y in 0..height {
for x in 0..width {
process_offset_pixel(
pixmap,
x,
y,
dx_pixels,
dy_pixels,
width,
height,
transparent,
);
}
}
}
}
}
/// Process a single pixel during offset operation.
///
/// This moves the pixel to its new position (if in bounds) and clears the source
/// position if it's in the exposed region.
#[inline(always)]
fn process_offset_pixel(
pixmap: &mut Pixmap,
x: u16,
y: u16,
dx_pixels: i32,
dy_pixels: i32,
width: u16,
height: u16,
transparent: PremulRgba8,
) {
let new_x = x as i32 + dx_pixels;
let new_y = y as i32 + dy_pixels;
if new_x >= 0 && new_x < width as i32 && new_y >= 0 && new_y < height as i32 {
let pixel = pixmap.sample(x, y);
pixmap.set_pixel(new_x as u16, new_y as u16, pixel);
}
// Clear the source pixel if it's in the exposed region
let should_clear = (dx_pixels > 0 && x < dx_pixels as u16)
|| (dx_pixels < 0 && x >= (width as i32 + dx_pixels) as u16)
|| (dy_pixels > 0 && y < dy_pixels as u16)
|| (dy_pixels < 0 && y >= (height as i32 + dy_pixels) as u16);
if should_clear {
pixmap.set_pixel(x, y, transparent);
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Test `offset_pixels` with positive offset (right and down).
#[test]
fn test_offset_pixels_positive() {
let mut pixmap = Pixmap::new(4, 4);
// Set center pixel to white
pixmap.set_pixel(
1,
1,
PremulRgba8 {
r: 255,
g: 255,
b: 255,
a: 255,
},
);
offset_pixels(&mut pixmap, 1.0, 1.0);
// White pixel should have moved from (1,1) to (2,2)
let moved = pixmap.sample(2, 2);
assert_eq!(moved.a, 255);
// Original position should be cleared (in exposed region)
let cleared = pixmap.sample(1, 1);
assert_eq!(cleared.a, 0);
}
/// Test `offset_pixels` with negative offset (left and up).
#[test]
fn test_offset_pixels_negative() {
let mut pixmap = Pixmap::new(4, 4);
// Set pixel at (2,2) to white
pixmap.set_pixel(
2,
2,
PremulRgba8 {
r: 255,
g: 255,
b: 255,
a: 255,
},
);
offset_pixels(&mut pixmap, -1.0, -1.0);
// White pixel should have moved from (2,2) to (1,1)
let moved = pixmap.sample(1, 1);
assert_eq!(moved.a, 255);
// Original position should be cleared (in exposed region)
let cleared = pixmap.sample(2, 2);
assert_eq!(cleared.a, 0);
}
/// Test `offset_pixels` with fractional offset (should round).
#[test]
fn test_offset_pixels_fractional() {
let mut pixmap = Pixmap::new(4, 4);
pixmap.set_pixel(
1,
1,
PremulRgba8 {
r: 255,
g: 255,
b: 255,
a: 255,
},
);
// 0.6 rounds to 1, -0.4 rounds to 0
offset_pixels(&mut pixmap, 0.6, -0.4);
// Should move by (1, 0): from (1,1) to (2,1)
let moved = pixmap.sample(2, 1);
assert_eq!(moved.a, 255);
}
/// Test `offset_pixels` with out-of-bounds offset (should clip).
#[test]
fn test_offset_pixels_out_of_bounds() {
let mut pixmap = Pixmap::new(4, 4);
pixmap.set_pixel(
1,
1,
PremulRgba8 {
r: 255,
g: 255,
b: 255,
a: 255,
},
);
// Large offset that moves pixel outside bounds
offset_pixels(&mut pixmap, 10.0, 10.0);
// Pixel moves outside, so (1,1) should be cleared
let cleared = pixmap.sample(1, 1);
assert_eq!(cleared.a, 0);
// All pixels should be cleared (entire image is exposed region)
for y in 0..4 {
for x in 0..4 {
assert_eq!(pixmap.sample(x, y).a, 0);
}
}
}
}
+28
View File
@@ -0,0 +1,28 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use crate::fine::common::gradient::SimdGradientKind;
use core::marker::PhantomData;
use vello_common::encode::LinearKind;
use vello_common::fearless_simd::{Simd, f32x8};
#[derive(Debug)]
pub(crate) struct SimdLinearKind<S: Simd> {
// For consistency with the other gradient types.
phantom_data: PhantomData<S>,
}
impl<S: Simd> SimdLinearKind<S> {
pub(crate) fn new(_: S, _: LinearKind) -> Self {
Self {
phantom_data: PhantomData,
}
}
}
impl<S: Simd> SimdGradientKind<S> for SimdLinearKind<S> {
#[inline(always)]
fn cur_pos(&self, x_pos: f32x8<S>, _: f32x8<S>) -> f32x8<S> {
x_pos
}
}
+246
View File
@@ -0,0 +1,246 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use crate::fine::{NumericVec, PosExt};
use crate::kurbo::Point;
use crate::peniko;
use core::slice::ChunksExact;
use vello_common::encode::{EncodedGradient, GradientLut};
use vello_common::fearless_simd::*;
pub(crate) mod linear;
pub(crate) mod radial;
pub(crate) mod sweep;
const GRADIENT_INVALID_POS: u32 = u32::MAX;
pub(crate) fn calculate_t_vals<S: Simd, U: SimdGradientKind<S>>(
simd: S,
kind: U,
buf: &mut [f32],
gradient: &EncodedGradient,
start_x: f64,
start_y: f64,
) {
simd.vectorize(
#[inline(always)]
|| {
let mut cur_pos = gradient.transform * Point::new(start_x, start_y);
let x_advances = (gradient.x_advance.x as f32, gradient.x_advance.y as f32);
let y_advances = (gradient.y_advance.x as f32, gradient.y_advance.y as f32);
for buf_part in buf.chunks_exact_mut(8) {
let x_pos = f32x8::splat_pos(simd, cur_pos.x as f32, x_advances.0, y_advances.0);
let y_pos = f32x8::splat_pos(simd, cur_pos.y as f32, x_advances.1, y_advances.1);
let pos = kind.cur_pos(x_pos, y_pos);
pos.store_slice(buf_part);
cur_pos += 2.0 * gradient.x_advance;
}
},
);
}
#[derive(Debug, Clone)]
pub(crate) struct GradientPainter<'a, S: Simd> {
gradient: &'a EncodedGradient,
lut: &'a GradientLut<f32>,
t_vals: ChunksExact<'a, f32>,
has_undefined: bool,
scale_factor: f32x8<S>,
simd: S,
}
impl<'a, S: Simd> GradientPainter<'a, S> {
pub(crate) fn new(simd: S, gradient: &'a EncodedGradient, t_vals: &'a [f32]) -> Self {
let lut = gradient.f32_lut(simd);
let scale_factor: f32x8<S> = f32x8::splat(simd, lut.scale_factor());
Self {
gradient,
scale_factor,
lut,
t_vals: t_vals.chunks_exact(8),
has_undefined: gradient.has_undefined,
simd,
}
}
}
impl<S: Simd> Iterator for GradientPainter<'_, S> {
type Item = u32x8<S>;
#[inline(always)]
fn next(&mut self) -> Option<Self::Item> {
let extend = self.gradient.extend;
let pos = f32x8::from_slice(self.simd, self.t_vals.next()?);
let t_vals = apply_extend(pos, extend);
let valid = pos.simd_eq(pos);
let indices = (t_vals * self.scale_factor).to_int::<u32x8<S>>();
// In case we had any NaN's, set the index to an explicit invalid sentinel. There
// probably is architecture-specific behavior how NaN will be converted to
// f32, so we apply our own handling of that.
let indices = self.simd.select_u32x8(
valid,
indices,
u32x8::splat(self.simd, GRADIENT_INVALID_POS),
);
Some(indices)
}
}
impl<S: Simd> crate::fine::Painter for GradientPainter<'_, S> {
fn paint_u8(&mut self, buf: &mut [u8]) {
let max_index = self.lut.width() as u32 - 1;
let mut masked_iter = self.has_undefined.then_some(self.clone());
self.simd.vectorize(
#[inline(always)]
|| {
let max_index = u32x8::splat(self.simd, max_index);
for chunk in buf.chunks_exact_mut(32) {
let indices = self.next().unwrap();
let clamped_indices = indices.min(max_index);
let rgbas_1: [f32x4<S>; 4] = core::array::from_fn(|i| {
let idx = clamped_indices[i] as usize;
f32x4::from_slice(self.simd, &self.lut.get(idx))
});
let rgbas_1 = self.simd.combine_f32x8(
self.simd.combine_f32x4(rgbas_1[0], rgbas_1[1]),
self.simd.combine_f32x4(rgbas_1[2], rgbas_1[3]),
);
let rgbas_1 = u8x16::from_f32(self.simd, rgbas_1);
rgbas_1.store_slice(&mut chunk[..16]);
let rgbas_2: [f32x4<S>; 4] = core::array::from_fn(|i| {
let idx = clamped_indices[i + 4] as usize;
f32x4::from_slice(self.simd, &self.lut.get(idx))
});
let rgbas_2 = self.simd.combine_f32x8(
self.simd.combine_f32x4(rgbas_2[0], rgbas_2[1]),
self.simd.combine_f32x4(rgbas_2[2], rgbas_2[3]),
);
let rgbas_2 = u8x16::from_f32(self.simd, rgbas_2);
rgbas_2.store_slice(&mut chunk[16..]);
}
},
);
// Mask any potential NaN positions.
if let Some(mut masked_iter) = masked_iter.take() {
self.simd.vectorize(
#[inline(always)]
|| {
for chunk in buf.chunks_exact_mut(32) {
let indices = masked_iter.next().unwrap();
let invalid =
indices.simd_eq(u32x8::splat(self.simd, GRADIENT_INVALID_POS));
let (invalid_1, invalid_2) = self.simd.split_mask32x8(invalid);
let loaded_1 = self
.simd
.reinterpret_u32_u8x16(u8x16::from_slice(self.simd, &chunk[..16]));
let masked_1 =
self.simd
.select_u32x4(invalid_1, u32x4::splat(self.simd, 0), loaded_1);
self.simd
.reinterpret_u8_u32x4(masked_1)
.store_slice(&mut chunk[..16]);
let loaded_2 = self
.simd
.reinterpret_u32_u8x16(u8x16::from_slice(self.simd, &chunk[16..]));
let masked_2 =
self.simd
.select_u32x4(invalid_2, u32x4::splat(self.simd, 0), loaded_2);
self.simd
.reinterpret_u8_u32x4(masked_2)
.store_slice(&mut chunk[16..]);
}
},
);
}
}
fn paint_f32(&mut self, buf: &mut [f32]) {
let mut masked_iter = self.has_undefined.then_some(self.clone());
self.simd.vectorize(
#[inline(always)]
|| {
let max_index = self.lut.width() as u32 - 1;
let max_index = u32x8::splat(self.simd, max_index);
for chunk in buf.chunks_exact_mut(32) {
let indices = self.next().unwrap();
let clamped_indices = indices.min(max_index);
chunk[0..4].copy_from_slice(&self.lut.get(clamped_indices[0] as usize));
chunk[4..8].copy_from_slice(&self.lut.get(clamped_indices[1] as usize));
chunk[8..12].copy_from_slice(&self.lut.get(clamped_indices[2] as usize));
chunk[12..16].copy_from_slice(&self.lut.get(clamped_indices[3] as usize));
chunk[16..20].copy_from_slice(&self.lut.get(clamped_indices[4] as usize));
chunk[20..24].copy_from_slice(&self.lut.get(clamped_indices[5] as usize));
chunk[24..28].copy_from_slice(&self.lut.get(clamped_indices[6] as usize));
chunk[28..32].copy_from_slice(&self.lut.get(clamped_indices[7] as usize));
}
},
);
// Mask any potential NaN positions.
if let Some(mut masked_iter) = masked_iter.take() {
self.simd.vectorize(
#[inline(always)]
|| {
for chunk in buf.chunks_exact_mut(32) {
let indices = masked_iter.next().unwrap();
let (indices_1, indices_2) = self.simd.split_u32x8(indices);
let invalid_1 = invalid_f32_mask(self.simd, indices_1);
let loaded_1 = f32x16::from_slice(self.simd, &chunk[..16]);
let masked_1 = self.simd.select_f32x16(
invalid_1,
f32x16::splat(self.simd, 0.0),
loaded_1,
);
masked_1.store_slice(&mut chunk[..16]);
let invalid_2 = invalid_f32_mask(self.simd, indices_2);
let loaded_2 = f32x16::from_slice(self.simd, &chunk[16..]);
let masked_2 = self.simd.select_f32x16(
invalid_2,
f32x16::splat(self.simd, 0.0),
loaded_2,
);
masked_2.store_slice(&mut chunk[16..]);
}
},
);
}
}
}
#[inline(always)]
fn invalid_f32_mask<S: Simd>(simd: S, indices: u32x4<S>) -> mask32x16<S> {
let indices = indices.zip_low(indices).combine(indices.zip_high(indices));
let indices = indices.zip_low(indices).combine(indices.zip_high(indices));
indices.simd_eq(u32x16::splat(simd, GRADIENT_INVALID_POS))
}
#[inline(always)]
pub(crate) fn apply_extend<S: Simd>(val: f32x8<S>, extend: peniko::Extend) -> f32x8<S> {
match extend {
peniko::Extend::Pad => val.max(0.0).min(1.0),
peniko::Extend::Repeat => (val - val.floor()).fract(),
// See <https://github.com/google/skia/blob/220738774f7a0ce4a6c7bd17519a336e5e5dea5b/src/opts/SkRasterPipeline_opts.h#L6472-L6475>
peniko::Extend::Reflect => ((val - 1.0) - 2.0 * ((val - 1.0) * 0.5).floor() - 1.0)
.abs()
.max(0.0)
.min(1.0),
}
}
pub(crate) trait SimdGradientKind<S: Simd> {
fn cur_pos(&self, x_pos: f32x8<S>, y_pos: f32x8<S>) -> f32x8<S>;
}
+107
View File
@@ -0,0 +1,107 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use crate::fine::common::gradient::SimdGradientKind;
use vello_common::encode::{FocalData, RadialKind};
use vello_common::fearless_simd::{Simd, SimdBase, SimdFloat, f32x8};
pub(crate) enum SimdRadialKindInner<S: Simd> {
Radial {
bias: f32x8<S>,
scale: f32x8<S>,
},
Strip {
scaled_r0_squared: f32x8<S>,
},
Focal {
focal_data: FocalData,
fp0: f32x8<S>,
fp1: f32x8<S>,
},
}
pub(crate) struct SimdRadialKind<S: Simd> {
inner: SimdRadialKindInner<S>,
}
impl<S: Simd> SimdRadialKind<S> {
pub(crate) fn new(simd: S, kind: &RadialKind) -> Self {
let inner = match kind {
RadialKind::Radial { bias, scale } => SimdRadialKindInner::Radial {
bias: f32x8::splat(simd, *bias),
scale: f32x8::splat(simd, *scale),
},
RadialKind::Strip { scaled_r0_squared } => SimdRadialKindInner::Strip {
scaled_r0_squared: f32x8::splat(simd, *scaled_r0_squared),
},
RadialKind::Focal {
focal_data,
fp0,
fp1,
} => SimdRadialKindInner::Focal {
fp0: f32x8::splat(simd, *fp0),
fp1: f32x8::splat(simd, *fp1),
focal_data: *focal_data,
},
};
Self { inner }
}
}
impl<S: Simd> SimdGradientKind<S> for SimdRadialKind<S> {
#[inline(always)]
fn cur_pos(&self, x_pos: f32x8<S>, y_pos: f32x8<S>) -> f32x8<S> {
let simd = x_pos.simd;
match &self.inner {
SimdRadialKindInner::Radial { bias, scale } => {
let radius = (x_pos * x_pos + y_pos * y_pos).sqrt();
*bias + radius * *scale
}
SimdRadialKindInner::Strip { scaled_r0_squared } => {
let p1 = *scaled_r0_squared - y_pos * y_pos;
let mask = simd.simd_lt_f32x8(p1, f32x8::splat(simd, 0.0));
simd.select_f32x8(mask, f32x8::splat(simd, f32::NAN), x_pos + p1.sqrt())
}
SimdRadialKindInner::Focal {
focal_data,
fp0,
fp1,
} => {
let mut t = if focal_data.is_focal_on_circle() {
x_pos + y_pos * y_pos / x_pos
} else if focal_data.is_well_behaved() {
(x_pos * x_pos + y_pos * y_pos).sqrt() - x_pos * *fp0
} else if focal_data.is_swapped() || (1.0 - focal_data.f_focal_x < 0.0) {
f32x8::splat(simd, -1.0) * (x_pos * x_pos - y_pos * y_pos).sqrt() - x_pos * *fp0
} else {
(x_pos * x_pos - y_pos * y_pos).sqrt() - x_pos * *fp0
};
if !focal_data.is_well_behaved() {
// Radii < 0 should be masked out, too.
let is_degenerate = simd.simd_le_f32x8(t, f32x8::splat(simd, 0.0));
t = simd.select_f32x8(is_degenerate, f32x8::splat(simd, f32::NAN), t);
}
if 1.0 - focal_data.f_focal_x < 0.0 {
t = f32x8::splat(simd, -1.0) * t;
}
if !focal_data.is_natively_focal() {
t += *fp1;
}
if focal_data.is_swapped() {
t = f32x8::splat(simd, 1.0) - t;
}
t
}
}
}
}
+61
View File
@@ -0,0 +1,61 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use crate::fine::common::gradient::SimdGradientKind;
use core::f32::consts::PI;
use vello_common::encode::SweepKind;
use vello_common::fearless_simd::{Simd, SimdBase, SimdFloat, f32x8};
#[derive(Debug)]
pub(crate) struct SimdSweepKind<S: Simd> {
start_angle: f32x8<S>,
inv_angle_delta: f32x8<S>,
simd: S,
}
impl<S: Simd> SimdSweepKind<S> {
pub(crate) fn new(simd: S, kind: &SweepKind) -> Self {
Self {
start_angle: f32x8::splat(simd, kind.start_angle),
inv_angle_delta: f32x8::splat(simd, kind.inv_angle_delta),
simd,
}
}
}
impl<S: Simd> SimdGradientKind<S> for SimdSweepKind<S> {
#[inline(always)]
fn cur_pos(&self, x_pos: f32x8<S>, y_pos: f32x8<S>) -> f32x8<S> {
let angle = x_y_to_unit_angle(self.simd, x_pos, y_pos) * f32x8::splat(self.simd, 2.0 * PI);
(angle - self.start_angle) * self.inv_angle_delta
}
}
#[inline(always)]
fn x_y_to_unit_angle<S: Simd>(simd: S, x: f32x8<S>, y: f32x8<S>) -> f32x8<S> {
let c0 = f32x8::splat(simd, 0.0);
let c1 = f32x8::splat(simd, 1.0);
let c2 = f32x8::splat(simd, 1.0 / 4.0);
let c3 = f32x8::splat(simd, 1.0 / 2.0);
let x_abs = x.abs();
let y_abs = y.abs();
let slope = x_abs.min(y_abs) / x_abs.max(y_abs);
let s = slope * slope;
let a = f32x8::splat(simd, -7.054_738_2e-3).mul_add(s, f32x8::splat(simd, 2.476_102e-2));
let b = a.mul_add(s, f32x8::splat(simd, -5.185_397e-2));
let c = b.mul_add(s, f32x8::splat(simd, 0.159_121_17));
let mut phi = slope * c;
phi = simd.select_f32x8(simd.simd_lt_f32x8(x_abs, y_abs), c2 - phi, phi);
phi = simd.select_f32x8(simd.simd_lt_f32x8(x, c0), c3 - phi, phi);
phi = simd.select_f32x8(simd.simd_lt_f32x8(y, c0), c1 - phi, phi);
// Clears all NaNs, using the property that NaN != NaN.
phi = simd.select_f32x8(phi.simd_eq(phi), phi, c0);
phi
}
+568
View File
@@ -0,0 +1,568 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use crate::fine::macros::{f32x16_painter, u8x16_painter};
use crate::fine::{PosExt, Splat4thExt, u8_to_f32};
use crate::kurbo::Point;
use vello_common::encode::EncodedImage;
use vello_common::fearless_simd::{Bytes, Simd, SimdBase, SimdFloat, f32x4, f32x16, u8x16, u32x4};
use vello_common::pixmap::Pixmap;
use vello_common::simd::element_wise_splat;
/// A painter for nearest-neighbor images with no skewing.
#[derive(Debug)]
pub(crate) struct PlainNNImagePainter<'a, S: Simd> {
data: ImagePainterData<'a, S>,
y_positions: f32x4<S>,
cur_x_pos: f32x4<S>,
advance: f32,
simd: S,
}
impl<'a, S: Simd> PlainNNImagePainter<'a, S> {
pub(crate) fn new(
simd: S,
image: &'a EncodedImage,
pixmap: &'a Pixmap,
start_x: f64,
start_y: f64,
) -> Self {
let data = ImagePainterData::new(simd, image, pixmap, start_x, start_y);
let y_positions = extend(
simd,
f32x4::splat_pos(
simd,
data.cur_pos.y as f32,
data.x_advances.1,
data.y_advances.1,
),
image.sampler.y_extend,
data.height,
data.height_inv,
);
let cur_x_pos = f32x4::splat_pos(
simd,
data.cur_pos.x as f32,
data.x_advances.0,
data.y_advances.0,
);
Self {
data,
advance: image.x_advance.x as f32,
y_positions,
cur_x_pos,
simd,
}
}
}
impl<S: Simd> Iterator for PlainNNImagePainter<'_, S> {
type Item = u8x16<S>;
#[inline(always)]
fn next(&mut self) -> Option<Self::Item> {
let x_pos = extend(
self.simd,
self.cur_x_pos,
self.data.image.sampler.x_extend,
self.data.width,
self.data.width_inv,
);
let samples = sample(self.simd, &self.data, x_pos, self.y_positions);
self.cur_x_pos += self.advance;
Some(samples)
}
}
u8x16_painter!(PlainNNImagePainter<'_, S>);
/// A painter for nearest-neighbor images with arbitrary transforms.
#[derive(Debug)]
pub(crate) struct NNImagePainter<'a, S: Simd> {
data: ImagePainterData<'a, S>,
simd: S,
}
impl<'a, S: Simd> NNImagePainter<'a, S> {
pub(crate) fn new(
simd: S,
image: &'a EncodedImage,
pixmap: &'a Pixmap,
start_x: f64,
start_y: f64,
) -> Self {
let data = ImagePainterData::new(simd, image, pixmap, start_x, start_y);
Self { data, simd }
}
}
impl<S: Simd> Iterator for NNImagePainter<'_, S> {
type Item = u8x16<S>;
fn next(&mut self) -> Option<Self::Item> {
let x_positions = extend(
self.simd,
f32x4::splat_pos(
self.simd,
self.data.cur_pos.x as f32,
self.data.x_advances.0,
self.data.y_advances.0,
),
self.data.image.sampler.x_extend,
self.data.width,
self.data.width_inv,
);
let y_positions = extend(
self.simd,
f32x4::splat_pos(
self.simd,
self.data.cur_pos.y as f32,
self.data.x_advances.1,
self.data.y_advances.1,
),
self.data.image.sampler.y_extend,
self.data.height,
self.data.height_inv,
);
let samples = sample(self.simd, &self.data, x_positions, y_positions);
self.data.cur_pos += self.data.image.x_advance;
Some(samples)
}
}
u8x16_painter!(NNImagePainter<'_, S>);
/// A painter for images with bilinear or bicubic filtering.
///
/// The painter is generic over sampler quality using the const-generic `QUALITY` parameter.
///
/// - Set `QUALITY` to `1` for bilinear sampling; or
/// - set `QUALITY` to `2` for bicubic sampling.
///
/// These values for `QUALITY` are the same numeric values as defined by
/// [`crate::peniko::ImageQuality`].
#[derive(Debug)]
pub(crate) struct FilteredImagePainter<'a, S: Simd, const QUALITY: u8> {
data: ImagePainterData<'a, S>,
simd: S,
}
impl<'a, S: Simd, const QUALITY: u8> FilteredImagePainter<'a, S, QUALITY> {
pub(crate) fn new(
simd: S,
image: &'a EncodedImage,
pixmap: &'a Pixmap,
start_x: f64,
start_y: f64,
) -> Self {
let data = ImagePainterData::new(simd, image, pixmap, start_x, start_y);
Self { data, simd }
}
}
impl<S: Simd, const QUALITY: u8> Iterator for FilteredImagePainter<'_, S, QUALITY> {
type Item = f32x16<S>;
fn next(&mut self) -> Option<Self::Item> {
let x_positions = f32x4::splat_pos(
self.simd,
self.data.cur_pos.x as f32,
self.data.x_advances.0,
self.data.y_advances.0,
);
let y_positions = f32x4::splat_pos(
self.simd,
self.data.cur_pos.y as f32,
self.data.x_advances.1,
self.data.y_advances.1,
);
// We have two versions of filtering: `Medium` (bilinear filtering) and
// `High` (bicubic filtering).
// In bilinear filtering, we sample the pixels of the rectangle that spans the
// locations (-0.5, -0.5) and (0.5, 0.5), and weight them by the fractional
// x/y position using simple linear interpolation in both dimensions.
// In bicubic filtering, we instead span a 4x4 grid around the
// center of the location we are sampling, and sample those points
// using a cubic filter to weight each location's contribution.
let x_fract = fract_floor(x_positions + 0.5);
let y_fract = fract_floor(y_positions + 0.5);
let mut interpolated_color = f32x16::splat(self.simd, 0.0);
let sample = |x_pos: f32x4<S>, y_pos: f32x4<S>| {
u8_to_f32(sample(self.simd, &self.data, x_pos, y_pos))
};
macro_rules! extend_x {
($idx:expr,$offsets:expr) => {
extend(
self.simd,
x_positions + $offsets[$idx],
self.data.image.sampler.y_extend,
self.data.width,
self.data.width_inv,
)
};
}
macro_rules! extend_y {
($idx:expr,$offsets:expr) => {
extend(
self.simd,
y_positions + $offsets[$idx],
self.data.image.sampler.y_extend,
self.data.height,
self.data.height_inv,
)
};
}
match QUALITY {
// medium quality: bilinear
1 => {
// <https://github.com/google/skia/blob/220738774f7a0ce4a6c7bd17519a336e5e5dea5b/src/opts/SkRasterPipeline_opts.h#L5039-L5078>
let cx = [1.0 - x_fract, x_fract];
let cy = [1.0 - y_fract, y_fract];
// Note that the sum of all cx*cy combinations also yields 1.0 again
// (modulo some floating point number impreciseness), ensuring the
// colors stay in range.
const OFFSETS: [f32; 2] = [-0.5, 0.5];
let x_positions = [extend_x!(0, OFFSETS), extend_x!(1, OFFSETS)];
let y_positions = [extend_y!(0, OFFSETS), extend_y!(1, OFFSETS)];
// We sample the corners of rectangle that covers our current position.
for x_idx in 0..2 {
let x_positions = x_positions[x_idx];
for y_idx in 0..2 {
let y_positions = y_positions[y_idx];
let color_sample = sample(x_positions, y_positions);
let w = element_wise_splat(self.simd, cx[x_idx] * cy[y_idx]);
interpolated_color = w.mul_add(color_sample, interpolated_color);
}
}
interpolated_color *= f32x16::splat(self.simd, 1.0 / 255.0);
}
// high quality: bicubic
2 => {
// Compare to <https://github.com/google/skia/blob/84ff153b0093fc83f6c77cd10b025c06a12c5604/src/opts/SkRasterPipeline_opts.h#L5030-L5075>.
let cx = weights(self.simd, x_fract);
let cy = weights(self.simd, y_fract);
const OFFSETS: [f32; 4] = [-1.5, -0.5, 0.5, 1.5];
let x_positions = [
extend_x!(0, OFFSETS),
extend_x!(1, OFFSETS),
extend_x!(2, OFFSETS),
extend_x!(3, OFFSETS),
];
let y_positions = [
extend_y!(0, OFFSETS),
extend_y!(1, OFFSETS),
extend_y!(2, OFFSETS),
extend_y!(3, OFFSETS),
];
// Note in particular that it is guaranteed that, similarly to bilinear filtering,
// the sum of all cx*cy is 1 (modulo some edge cases).
// We sample the 4x4 grid around the position we are currently looking at.
for x_idx in 0..4 {
let x_positions = x_positions[x_idx];
for y_idx in 0..4 {
let y_positions = y_positions[y_idx];
let color_sample = sample(x_positions, y_positions);
let w = element_wise_splat(self.simd, cx[x_idx] * cy[y_idx]);
interpolated_color = w.mul_add(color_sample, interpolated_color);
}
}
interpolated_color *= f32x16::splat(self.simd, 1.0 / 255.0);
let alphas = interpolated_color.splat_4th();
// Due to the nature of the cubic filter, it can happen in certain situations
// that one of the color components ends up with a higher value than the
// alpha component, which isn't permissible because the color is
// premultiplied and would lead to overflows when doing source over
// compositing with u8-based values. Because of this, we need to clamp
// to the alpha value.
interpolated_color = interpolated_color
.min(alphas)
.min(f32x16::splat(self.simd, 1.0))
.max(f32x16::splat(self.simd, 0.0));
}
_ => panic!(
"Unknown value for `FilteredImagePainter`'s const-generic `QUALITY` parameter. Expected `1` for bilinear or `2` for bicubic, got: `{QUALITY}`."
),
}
self.data.cur_pos += self.data.image.x_advance;
Some(interpolated_color)
}
}
// Bilinear
f32x16_painter!(FilteredImagePainter<'_, S, 1>);
// Bicubic
f32x16_painter!(FilteredImagePainter<'_, S, 2>);
/// Computes the positive fractional part of a value: `val - val.floor()`.
///
/// Unlike `f32::fract()`, this always returns a value in [0, 1),
/// even for negative inputs.
#[inline(always)]
pub(crate) fn fract_floor<S: Simd>(val: f32x4<S>) -> f32x4<S> {
val - val.floor()
}
/// Common data used by different image painters
#[derive(Debug)]
pub(crate) struct ImagePainterData<'a, S: Simd> {
pub(crate) cur_pos: Point,
pub(crate) image: &'a EncodedImage,
pub(crate) pixmap: &'a Pixmap,
pub(crate) x_advances: (f32, f32),
pub(crate) y_advances: (f32, f32),
pub(crate) height: f32x4<S>,
pub(crate) height_inv: f32x4<S>,
pub(crate) width: f32x4<S>,
pub(crate) width_inv: f32x4<S>,
pub(crate) width_u32: u32x4<S>,
}
impl<'a, S: Simd> ImagePainterData<'a, S> {
pub(crate) fn new(
simd: S,
image: &'a EncodedImage,
pixmap: &'a Pixmap,
start_x: f64,
start_y: f64,
) -> Self {
let width = pixmap.width() as f32;
let height = pixmap.height() as f32;
let start_pos = image.transform * Point::new(start_x, start_y);
let width_inv = f32x4::splat(simd, 1.0 / width);
let height_inv = f32x4::splat(simd, 1.0 / height);
let width = f32x4::splat(simd, width);
let width_u32 = u32x4::splat(simd, pixmap.width() as u32);
let height = f32x4::splat(simd, height);
let x_advances = (image.x_advance.x as f32, image.x_advance.y as f32);
let y_advances = (image.y_advance.x as f32, image.y_advance.y as f32);
Self {
cur_pos: start_pos,
pixmap,
x_advances,
y_advances,
image,
width,
height,
width_u32,
width_inv,
height_inv,
}
}
}
#[inline(always)]
pub(crate) fn sample<S: Simd>(
simd: S,
data: &ImagePainterData<'_, S>,
x_positions: f32x4<S>,
y_positions: f32x4<S>,
) -> u8x16<S> {
let idx = x_positions.to_int::<u32x4<S>>() + y_positions.to_int::<u32x4<S>>() * data.width_u32;
u32x4::from_slice(
simd,
&[
data.pixmap.sample_idx(idx[0]).to_u32(),
data.pixmap.sample_idx(idx[1]).to_u32(),
data.pixmap.sample_idx(idx[2]).to_u32(),
data.pixmap.sample_idx(idx[3]).to_u32(),
],
)
.to_bytes()
}
#[inline(always)]
pub(crate) fn extend<S: Simd>(
simd: S,
val: f32x4<S>,
extend: crate::peniko::Extend,
max: f32x4<S>,
inv_max: f32x4<S>,
) -> f32x4<S> {
match extend {
// Note that max should be exclusive, so subtract one to enforce that.
// Since the maximum image dimensions we support is u16::MAX, subtracting 1 in f32
// is enough to ensure that all numbers are subtracted correctly.
crate::peniko::Extend::Pad => val.min(max - 1.0).max(f32x4::splat(simd, 0.0)),
crate::peniko::Extend::Repeat => {
// floor := (val * inv_max).floor() * max is the nearest multiple of `max` below val.
max.mul_add(-(val * inv_max).floor(), val)
// In certain edge cases, we might still end up with a higher number.
.min(max - 1.0)
}
// <https://github.com/google/skia/blob/220738774f7a0ce4a6c7bd17519a336e5e5dea5b/src/opts/SkRasterPipeline_opts.h#L3274-L3290>
crate::peniko::Extend::Reflect => {
let u = val
- (val * inv_max * f32x4::splat(simd, 0.5)).floor() * f32x4::splat(simd, 2.0) * max;
let s = (u * inv_max).floor();
let m = u - f32x4::splat(simd, 2.0) * s * (u - max);
let bias_in_ulps = s.trunc();
let m_bits = u32x4::from_bytes(m.to_bytes());
// This would yield NaN if `m` is 0 and `bias_in_ulps` > 0, but since
// our `max` is always an integer number, u and s must also be an integer number
// and thus `m_bits` must be 0.
// Note that this is a wrapping sub!
let biased_bits = m_bits - bias_in_ulps.to_int::<u32x4<S>>();
f32x4::from_bytes(biased_bits.to_bytes())
// In certain edge cases, we might still end up with a higher number.
.min(max - 1.0)
}
}
}
/// Calculate the weights for a single fractional value.
fn weights<S: Simd>(simd: S, fract: f32x4<S>) -> [f32x4<S>; 4] {
simd.vectorize(
#[inline(always)]
|| {
let s = fract.simd;
const MF: [[f32; 4]; 4] = mf_resampler();
[
single_weight(
fract,
f32x4::splat(s, MF[0][0]),
f32x4::splat(s, MF[0][1]),
f32x4::splat(s, MF[0][2]),
f32x4::splat(s, MF[0][3]),
),
single_weight(
fract,
f32x4::splat(s, MF[1][0]),
f32x4::splat(s, MF[1][1]),
f32x4::splat(s, MF[1][2]),
f32x4::splat(s, MF[1][3]),
),
single_weight(
fract,
f32x4::splat(s, MF[2][0]),
f32x4::splat(s, MF[2][1]),
f32x4::splat(s, MF[2][2]),
f32x4::splat(s, MF[2][3]),
),
single_weight(
fract,
f32x4::splat(s, MF[3][0]),
f32x4::splat(s, MF[3][1]),
f32x4::splat(s, MF[3][2]),
f32x4::splat(s, MF[3][3]),
),
]
},
)
}
/// Calculate a weight based on the fractional value t and the cubic coefficients.
#[inline(always)]
fn single_weight<S: Simd>(
t: f32x4<S>,
a: f32x4<S>,
b: f32x4<S>,
c: f32x4<S>,
d: f32x4<S>,
) -> f32x4<S> {
t.mul_add(d, c).mul_add(t, b).mul_add(t, a)
}
/// Mitchell filter with the variables B = 1/3 and C = 1/3.
const fn mf_resampler() -> [[f32; 4]; 4] {
cubic_resampler(1.0 / 3.0, 1.0 / 3.0)
}
/// Cubic resampling logic is borrowed from Skia. See
/// <https://github.com/google/skia/blob/220fef664978643a47d4559ae9e762b91aba534a/include/core/SkSamplingOptions.h#L33-L50>
/// for some links to understand how this works. In principle, this macro allows us to define a
/// resampler kernel based on two variables B and C which can be between 0 and 1, allowing to
/// change some properties of the cubic interpolation kernel.
///
/// As mentioned above, cubic resampling consists of sampling the 16 surrounding pixels of the
/// target point and interpolating them with a cubic filter.
/// The generated matrix is 4x4 and represent the coefficients of the cubic function used to
/// calculate weights based on the `x_fract` and `y_fract` of the location we are looking at.
const fn cubic_resampler(b: f32, c: f32) -> [[f32; 4]; 4] {
[
[
(1.0 / 6.0) * b,
-(3.0 / 6.0) * b - c,
(3.0 / 6.0) * b + 2.0 * c,
-(1.0 / 6.0) * b - c,
],
[
1.0 - (2.0 / 6.0) * b,
0.0,
-3.0 + (12.0 / 6.0) * b + c,
2.0 - (9.0 / 6.0) * b - c,
],
[
(1.0 / 6.0) * b,
(3.0 / 6.0) * b + c,
3.0 - (15.0 / 6.0) * b - 2.0 * c,
-2.0 + (9.0 / 6.0) * b + c,
],
[0.0, 0.0, -c, (1.0 / 6.0) * b + c],
]
}
#[cfg(test)]
mod tests {
use super::*;
use vello_common::fearless_simd::Fallback;
#[test]
fn extend_overflow() {
let simd = Fallback::new();
let max = f32x4::splat(simd, 128.0);
let max_inv = 1.0 / max;
let num = f32x4::splat(simd, 127.00001);
let res = extend(simd, num, crate::peniko::Extend::Repeat, max, max_inv);
assert!(res[0] <= 127.0);
}
}
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
pub(crate) mod gradient;
pub(crate) mod image;
pub(crate) mod rounded_blurred_rect;
+249
View File
@@ -0,0 +1,249 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Drawing blurred, rounded rectangles.
//!
//! Implementation is adapted from: <https://git.sr.ht/~raph/blurrr/tree/master/src/distfield.rs>.
use crate::fine::{NumericVec, PosExt, ShaderResultF32};
use crate::kurbo::{Point, Vec2};
use vello_common::encode::EncodedBlurredRoundedRectangle;
use vello_common::fearless_simd::{Simd, SimdBase, SimdFloat, f32x8, u8x16};
#[cfg(not(feature = "std"))]
use vello_common::kurbo::common::FloatFuncs as _;
#[derive(Debug)]
pub(crate) struct BlurredRoundedRectFiller<S: Simd> {
r: f32x8<S>,
g: f32x8<S>,
b: f32x8<S>,
a: f32x8<S>,
alpha_calculator: AlphaCalculator<S>,
}
impl<S: Simd> BlurredRoundedRectFiller<S> {
pub(crate) fn new(
simd: S,
rect: &EncodedBlurredRoundedRectangle,
start_x: f64,
start_y: f64,
) -> Self {
let start_pos = rect.transform * Point::new(start_x, start_y);
let color_components = rect.color.as_premul_f32().components;
let r = f32x8::splat(simd, color_components[0]);
let g = f32x8::splat(simd, color_components[1]);
let b = f32x8::splat(simd, color_components[2]);
let a = f32x8::splat(simd, color_components[3]);
let simd_rect = SimdRoundedBlurredRect::new(rect, simd);
let alpha_calculator =
AlphaCalculator::new(start_pos, rect.x_advance, rect.y_advance, simd_rect, simd);
Self {
alpha_calculator,
r,
g,
b,
a,
}
}
}
impl<S: Simd> Iterator for BlurredRoundedRectFiller<S> {
type Item = ShaderResultF32<S>;
fn next(&mut self) -> Option<Self::Item> {
let next = self.alpha_calculator.next().unwrap();
let r = self.r * next;
let g = self.g * next;
let b = self.b * next;
let a = self.a * next;
Some(ShaderResultF32 { r, g, b, a })
}
}
impl<S: Simd> crate::fine::Painter for BlurredRoundedRectFiller<S> {
fn paint_u8(&mut self, buf: &mut [u8]) {
for chunk in buf.chunks_exact_mut(64) {
let first = self.next().unwrap();
let simd = first.r.simd;
let second = self.next().unwrap();
let r = u8x16::from_f32(simd, simd.combine_f32x8(first.r, second.r));
let g = u8x16::from_f32(simd, simd.combine_f32x8(first.g, second.g));
let b = u8x16::from_f32(simd, simd.combine_f32x8(first.b, second.b));
let a = u8x16::from_f32(simd, simd.combine_f32x8(first.a, second.a));
let combined = simd.combine_u8x32(simd.combine_u8x16(r, g), simd.combine_u8x16(b, a));
simd.store_interleaved_128_u8x64(combined, (&mut chunk[..]).try_into().unwrap());
}
}
fn paint_f32(&mut self, buf: &mut [f32]) {
for chunk in buf.chunks_exact_mut(32) {
let (c1, c2) = self.next().unwrap().get();
c1.simd
.store_interleaved_128_f32x16(c1, (&mut chunk[..16]).try_into().unwrap());
c2.simd
.store_interleaved_128_f32x16(c2, (&mut chunk[16..]).try_into().unwrap());
}
}
}
#[derive(Debug)]
struct AlphaCalculator<S: Simd> {
cur_pos: Point,
x_advance: Vec2,
y_advance: Vec2,
r: SimdRoundedBlurredRect<S>,
simd: S,
}
impl<S: Simd> AlphaCalculator<S> {
fn new(
start_pos: Point,
x_advance: Vec2,
y_advance: Vec2,
r: SimdRoundedBlurredRect<S>,
simd: S,
) -> Self {
Self {
cur_pos: start_pos,
x_advance,
y_advance,
r,
simd,
}
}
}
impl<S: Simd> Iterator for AlphaCalculator<S> {
type Item = f32x8<S>;
fn next(&mut self) -> Option<Self::Item> {
let i = f32x8::splat_pos(
self.simd,
self.cur_pos.x as f32,
self.x_advance.x as f32,
self.y_advance.x as f32,
);
let j = f32x8::splat_pos(
self.simd,
self.cur_pos.y as f32,
self.x_advance.y as f32,
self.y_advance.y as f32,
);
let r = &self.r;
let y = j - r.v1 * r.height;
// Equivalent to r.r1 + y.abs() - (r.h * r.v1)
let y0 = r.r1 - r.h.mul_sub(r.v1, y.abs());
let y1 = y0.max(r.v0);
let x = i - r.v1 * r.width;
// Equivalent to r.r1 + x.abs() - (r.w * r.v1)
let x0 = r.r1 - r.w.mul_sub(r.v1, x.abs());
let x1 = x0.max(r.v0);
let d_pos = (x1.powf(r.exponent) + y1.powf(r.exponent)).powf(r.recip_exponent);
let d_neg = x0.max(y0).min(r.v0);
let d = d_pos + d_neg - r.r1;
let z = r.scale
* (f32x8::compute_erf7(self.simd, r.std_dev_inv * (r.min_edge + d))
- f32x8::compute_erf7(self.simd, r.std_dev_inv * d));
self.cur_pos += 2.0 * self.x_advance;
Some(z)
}
}
#[derive(Debug)]
struct SimdRoundedBlurredRect<S: Simd> {
pub exponent: f32,
pub recip_exponent: f32,
pub scale: f32x8<S>,
pub std_dev_inv: f32x8<S>,
pub min_edge: f32x8<S>,
pub w: f32x8<S>,
pub h: f32x8<S>,
pub width: f32x8<S>,
pub height: f32x8<S>,
pub r1: f32x8<S>,
pub v0: f32x8<S>,
pub v1: f32x8<S>,
}
impl<S: Simd> SimdRoundedBlurredRect<S> {
fn new(encoded: &EncodedBlurredRoundedRectangle, s: S) -> Self {
let h = f32x8::splat(s, encoded.h);
let w = f32x8::splat(s, encoded.w);
let width = f32x8::splat(s, encoded.width);
let height = f32x8::splat(s, encoded.height);
let r1 = f32x8::splat(s, encoded.r1);
let exponent = encoded.exponent;
let recip_exponent = encoded.recip_exponent;
let scale = f32x8::splat(s, encoded.scale);
let min_edge = f32x8::splat(s, encoded.min_edge);
let std_dev_inv = f32x8::splat(s, encoded.std_dev_inv);
let v0 = f32x8::splat(s, 0.0);
let v1 = f32x8::splat(s, 0.5);
Self {
exponent,
recip_exponent,
scale,
std_dev_inv,
min_edge,
w,
v0,
v1,
h,
width,
height,
r1,
}
}
}
trait FloatExt<S: Simd> {
// See https://raphlinus.github.io/audio/2018/09/05/sigmoid.html for a little
// explanation of this approximation to the erf function.
// Doing `inline(always)` seems to reduce performance for some reason.
/// Approximate the erf function.
fn compute_erf7(simd: S, x: Self) -> Self;
fn powf(self, x: f32) -> Self;
}
impl<S: Simd> FloatExt<S> for f32x8<S> {
fn compute_erf7(simd: S, x: Self) -> Self {
// Clamp `x`, because for large `x` the terms here become `inf`, causing the result to be 0 or
// `NaN`. This clamping doesn't lose any information, because `erf(±10) ≈ 1` well within `f64`
// machine precision, let alone `f32`.
let x = x.max(Self::splat(simd, -10.0)).min(Self::splat(simd, 10.0));
let x = x * Self::splat(simd, core::f32::consts::FRAC_2_SQRT_PI);
let xx = x * x;
let p1 = Self::splat(simd, 0.0104).mul_add(xx, Self::splat(simd, 0.03395));
let p2 = p1.mul_add(xx, Self::splat(simd, 0.24295));
let p3 = x * xx;
let x = p2.mul_add(p3, x);
let denom = x.mul_add(x, Self::splat(simd, 1.0)).sqrt();
x / denom
}
#[inline]
fn powf(mut self, x: f32) -> Self {
// TODO: SIMD
self[0] = self[0].powf(x);
self[1] = self[1].powf(x);
self[2] = self[2].powf(x);
self[3] = self[3].powf(x);
self[4] = self[4].powf(x);
self[5] = self[5].powf(x);
self[6] = self[6].powf(x);
self[7] = self[7].powf(x);
self
}
}
+310
View File
@@ -0,0 +1,310 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use crate::peniko::{BlendMode, Mix};
use crate::util::Premultiply;
use vello_common::fearless_simd::*;
#[derive(Copy, Clone)]
struct Channels<S: Simd> {
r: f32x4<S>,
g: f32x4<S>,
b: f32x4<S>,
}
impl<S: Simd> Channels<S> {
#[inline(always)]
fn unpremultiply(mut self, a: f32x4<S>) -> Self {
self.r = self.r.unpremultiply(a);
self.g = self.g.unpremultiply(a);
self.b = self.b.unpremultiply(a);
self
}
}
// TODO: blending is still extremely slow, investigate whether there is something obvious we are
// missing that other renderers do.
pub(crate) fn mix<S: Simd>(src_c: f32x16<S>, bg: f32x16<S>, blend_mode: BlendMode) -> f32x16<S> {
if matches!(blend_mode.mix, Mix::Normal) {
return src_c;
}
// See https://www.w3.org/TR/compositing-1/#blending
let simd = src_c.simd;
let split = |input: f32x16<S>| {
let mut storage = [0.0; 16];
simd.store_interleaved_128_f32x16(input, &mut storage);
let input_v = f32x16::from_slice(simd, &storage);
let p1 = simd.split_f32x16(input_v);
let (r, g) = simd.split_f32x8(p1.0);
let (b, a) = simd.split_f32x8(p1.1);
(Channels { r, g, b }, a)
};
let (bg_channels, bg_a) = split(bg);
let (src_channels, src_a) = split(src_c);
let unpremultiplied_bg = bg_channels.unpremultiply(bg_a);
let unpremultiplied_src = src_channels.unpremultiply(src_a);
let mut res_bg = unpremultiplied_bg;
let mix_src = blend_mode.mix(unpremultiplied_src, unpremultiplied_bg);
let apply_alpha = |unpremultiplied_src_channel: f32x4<S>,
mix_src_channel: f32x4<S>,
dest_channel: &mut f32x4<S>| {
let p1 = (1.0 - bg_a) * unpremultiplied_src_channel;
let p2 = bg_a * mix_src_channel;
*dest_channel = (p1 + p2).premultiply(src_a);
};
apply_alpha(unpremultiplied_src.r, mix_src.r, &mut res_bg.r);
apply_alpha(unpremultiplied_src.g, mix_src.g, &mut res_bg.g);
apply_alpha(unpremultiplied_src.b, mix_src.b, &mut res_bg.b);
let combined = simd.combine_f32x8(
simd.combine_f32x4(res_bg.r, res_bg.g),
simd.combine_f32x4(res_bg.b, src_a),
);
let mut storage = [0.0; 16];
simd.store_interleaved_128_f32x16(combined, &mut storage);
f32x16::from_slice(simd, &storage)
}
trait MixExt {
fn mix<S: Simd>(&self, src: Channels<S>, bg: Channels<S>) -> Channels<S>;
}
impl MixExt for BlendMode {
fn mix<S: Simd>(&self, src: Channels<S>, bg: Channels<S>) -> Channels<S> {
match self.mix {
Mix::Normal => src,
Mix::Multiply => Multiply::mix(src, bg),
Mix::Screen => Screen::mix(src, bg),
Mix::Overlay => Overlay::mix(src, bg),
Mix::Darken => Darken::mix(src, bg),
Mix::Lighten => Lighten::mix(src, bg),
Mix::ColorDodge => ColorDodge::mix(src, bg),
Mix::ColorBurn => ColorBurn::mix(src, bg),
Mix::HardLight => HardLight::mix(src, bg),
Mix::SoftLight => SoftLight::mix(src, bg),
Mix::Difference => Difference::mix(src, bg),
Mix::Exclusion => Exclusion::mix(src, bg),
Mix::Luminosity => Luminosity::mix(src, bg),
Mix::Color => Color::mix(src, bg),
Mix::Hue => Hue::mix(src, bg),
Mix::Saturation => Saturation::mix(src, bg),
}
}
}
impl Multiply {
#[inline(always)]
fn single<S: Simd>(src: f32x4<S>, bg: f32x4<S>) -> f32x4<S> {
src * bg
}
}
impl Screen {
#[inline(always)]
fn single<S: Simd>(src: f32x4<S>, bg: f32x4<S>) -> f32x4<S> {
bg + src - src * bg
}
}
impl HardLight {
fn single<S: Simd>(src: f32x4<S>, bg: f32x4<S>) -> f32x4<S> {
let two = f32x4::splat(src.simd, 2.0);
let mask = src.simd.simd_le_f32x4(src, f32x4::splat(src.simd, 0.5));
let opt1 = Multiply::single(bg, src * two);
let opt2 = Screen::single(bg, two * src - 1.0);
src.simd.select_f32x4(mask, opt1, opt2)
}
}
macro_rules! separable_mix {
($name:ident, $calc:expr) => {
pub(crate) struct $name;
impl $name {
#[inline(always)]
fn mix<S: Simd>(mut src: Channels<S>, bg: Channels<S>) -> Channels<S> {
src.r = $calc(src.r, bg.r);
src.g = $calc(src.g, bg.g);
src.b = $calc(src.b, bg.b);
src
}
}
};
}
separable_mix!(Multiply, |cs: f32x4<S>, cb: f32x4<S>| Multiply::single(
cs, cb
));
separable_mix!(Screen, |cs: f32x4<S>, cb: f32x4<S>| Screen::single(cs, cb));
separable_mix!(Overlay, |cs: f32x4<S>, cb: f32x4<S>| HardLight::single(
cb, cs
));
separable_mix!(Darken, |cs: f32x4<S>, cb: f32x4<S>| cs.min(cb));
separable_mix!(Lighten, |cs: f32x4<S>, cb: f32x4<S>| cs.max(cb));
separable_mix!(Difference, |cs: f32x4<S>, cb: f32x4<S>| {
cs.simd
.select_f32x4(cs.simd.simd_le_f32x4(cs, cb), cb - cs, cs - cb)
});
separable_mix!(HardLight, |cs: f32x4<S>, cb: f32x4<S>| HardLight::single(
cs, cb
));
separable_mix!(Exclusion, |cs: f32x4<S>, cb: f32x4<S>| {
(cs + cb) - 2.0 * (cs * cb)
});
separable_mix!(SoftLight, |cs: f32x4<S>, cb: f32x4<S>| {
let mask_1 = cs.simd.simd_le_f32x4(cb, f32x4::splat(cs.simd, 0.25));
let d = cs
.simd
.select_f32x4(mask_1, ((16.0 * cb - 12.0) * cb + 4.0) * cb, cb.sqrt());
let mask_2 = cs.simd.simd_le_f32x4(cs, f32x4::splat(cs.simd, 0.5));
cs.simd.select_f32x4(
mask_2,
cb - (1.0 - 2.0 * cs) * cb * (1.0 - cb),
cb + (2.0 * cs - 1.0) * (d - cb),
)
});
separable_mix!(ColorDodge, |cs: f32x4<S>, cb: f32x4<S>| {
let mask_1 = cb.simd.simd_eq_f32x4(cb, f32x4::splat(cb.simd, 0.0));
let mask_2 = cs.simd.simd_eq_f32x4(cs, f32x4::splat(cs.simd, 1.0));
cs.simd.select_f32x4(
// if cb == 0
mask_1,
f32x4::splat(cs.simd, 0.0),
// else if cs == 1
cs.simd.select_f32x4(
mask_2,
f32x4::splat(cs.simd, 1.0),
// else
f32x4::splat(cs.simd, 1.0).min(cb / (1.0 - cs)),
),
)
});
separable_mix!(ColorBurn, |cs: f32x4<S>, cb: f32x4<S>| {
let mask_1 = cb.simd.simd_eq_f32x4(cb, f32x4::splat(cb.simd, 1.0));
let mask_2 = cs.simd.simd_eq_f32x4(cs, f32x4::splat(cs.simd, 0.0));
cs.simd.select_f32x4(
// if cb == 1
mask_1,
f32x4::splat(cs.simd, 1.0),
// else if cs == 0
cs.simd.select_f32x4(
mask_2,
f32x4::splat(cs.simd, 0.0),
// else
1.0 - f32x4::splat(cs.simd, 1.0).min((1.0 - cb) / cs),
),
)
});
macro_rules! non_separable_mix {
($name:ident, $calc:expr) => {
pub(crate) struct $name;
impl $name {
#[inline(always)]
fn mix<S: Simd>(mut src: Channels<S>, mut bg: Channels<S>) -> Channels<S> {
$calc(&mut src, &mut bg)
}
}
};
}
non_separable_mix!(Hue, |cs: &mut Channels<S>, cb: &mut Channels<S>| {
set_sat(&mut cs.r, &mut cs.g, &mut cs.b, sat(cb.r, cb.g, cb.b));
set_lum(&mut cs.r, &mut cs.g, &mut cs.b, lum(cb.r, cb.g, cb.b));
*cs
});
non_separable_mix!(Saturation, |cs: &mut Channels<S>, cb: &mut Channels<S>| {
let lum = lum(cb.r, cb.g, cb.b);
set_sat(&mut cb.r, &mut cb.g, &mut cb.b, sat(cs.r, cs.g, cs.b));
set_lum(&mut cb.r, &mut cb.g, &mut cb.b, lum);
*cb
});
non_separable_mix!(Color, |cs: &mut Channels<S>, cb: &mut Channels<S>| {
set_lum(&mut cs.r, &mut cs.g, &mut cs.b, lum(cb.r, cb.g, cb.b));
*cs
});
non_separable_mix!(Luminosity, |cs: &mut Channels<S>, cb: &mut Channels<S>| {
set_lum(&mut cb.r, &mut cb.g, &mut cb.b, lum(cs.r, cs.g, cs.b));
*cb
});
fn lum<S: Simd>(r: f32x4<S>, g: f32x4<S>, b: f32x4<S>) -> f32x4<S> {
0.3 * r + 0.59 * g + 0.11 * b
}
fn sat<S: Simd>(r: f32x4<S>, g: f32x4<S>, b: f32x4<S>) -> f32x4<S> {
r.max(g).max(b) - r.min(g).min(b)
}
// PdfCraft patch: backport of linebender/vello#2000. A gray that rounding leaves just below 0
// or above 1 has `l == n` (or `x == l`), and the unguarded division made a NaN that drew it
// black, whatever the backdrop's alpha; a clip now needs a nonzero denominator.
fn clip_color<S: Simd>(r: &mut f32x4<S>, g: &mut f32x4<S>, b: &mut f32x4<S>) {
let simd = r.simd;
let zero = f32x4::splat(simd, 0.0);
let l = lum(*r, *g, *b);
let n = r.min(g.min(*b));
let x = r.max(g.max(*b));
let below = l - n;
let above = x - l;
let clip_low = simd.simd_lt_f32x4(n, zero) & simd.simd_gt_f32x4(below, zero);
let clip_high =
simd.simd_gt_f32x4(x, f32x4::splat(simd, 1.0)) & simd.simd_gt_f32x4(above, zero);
for c in [r, g, b] {
*c = simd.select_f32x4(clip_low, l + (((*c - l) * l) / below), *c);
*c = simd.select_f32x4(clip_high, l + (((*c - l) * (1.0 - l)) / above), *c);
}
}
fn set_lum<S: Simd>(r: &mut f32x4<S>, g: &mut f32x4<S>, b: &mut f32x4<S>, l: f32x4<S>) {
let d = l - lum(*r, *g, *b);
*r += d;
*g += d;
*b += d;
clip_color(r, g, b);
}
// Adapted from tiny-skia
fn set_sat<S: Simd>(r: &mut f32x4<S>, g: &mut f32x4<S>, b: &mut f32x4<S>, s: f32x4<S>) {
let simd = r.simd;
let zero = f32x4::splat(simd, 0.0);
let mn = r.min(g.min(*b));
let mx = r.max(g.max(*b));
let sat = mx - mn;
// Map min channel to 0, max channel to s, and scale the middle proportionally.
let scale = |c| simd.select_f32x4(simd.simd_eq_f32x4(sat, zero), zero, (c - mn) * s / sat);
*r = scale(*r);
*g = scale(*g);
*b = scale(*b);
}
+171
View File
@@ -0,0 +1,171 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use crate::fine::Splat4thExt;
use crate::peniko::{BlendMode, Compose};
use vello_common::fearless_simd::*;
pub(crate) trait ComposeExt {
fn compose<S: Simd>(
&self,
simd: S,
src_c: f32x16<S>,
bg_c: f32x16<S>,
alpha_mask: Option<f32x16<S>>,
) -> f32x16<S>;
}
impl ComposeExt for BlendMode {
fn compose<S: Simd>(
&self,
simd: S,
src_c: f32x16<S>,
bg_c: f32x16<S>,
alpha_mask: Option<f32x16<S>>,
) -> f32x16<S> {
// There some non-obvious subtleties worth highlighting here.
// We support two kinds of blending (in this case, we focus on compositing specifically):
// - Isolated blending, where layers as a whole are blended together with their backdrop.
// If we are currently performing this kind of blending, `alpha_mask` will always be `None`.
// After all, there is no concrete shape opacity associated with a layer. Instead, we are
// just compositing the RGBA values at _all_ positions of the source layer with the backdrop
// layer. For example, if the backdrop contains a green rectangle and source layer is just
// empty, if we perform blending with `Compose::Clear`, then _everything_ will be cleared,
// because we are compositing the whole source layer with the whole backdrop, and not
// just the parts of the source layer that have actually be drawn on.
// - Non-isolated blending, where a single path is blended with the backdrop. In this case,
// `alpha_mask` _might_ be `Some` and contain the alpha values of the strips we are currently
// compositing. Remember that strips always have a fixed height of 4, because of this, the
// strips might cover areas that aren't actually covered by the path (and just have an alpha
// value of 0, or a value between 0-254 for anti-aliased parts). Because of this, for
// non-isolated blending, we need to lerp the result with the backdrop using `alpha_mask`.
let mut res = match self.compose {
Compose::SrcOver => SrcOver::compose(simd, src_c, bg_c),
Compose::Clear => Clear::compose(simd, src_c, bg_c),
Compose::Copy => Copy::compose(simd, src_c, bg_c),
Compose::DestOver => DestOver::compose(simd, src_c, bg_c),
Compose::Dest => Dest::compose(simd, src_c, bg_c),
Compose::SrcIn => SrcIn::compose(simd, src_c, bg_c),
Compose::DestIn => DestIn::compose(simd, src_c, bg_c),
Compose::SrcOut => SrcOut::compose(simd, src_c, bg_c),
Compose::DestOut => DestOut::compose(simd, src_c, bg_c),
Compose::SrcAtop => SrcAtop::compose(simd, src_c, bg_c),
Compose::DestAtop => DestAtop::compose(simd, src_c, bg_c),
Compose::Xor => Xor::compose(simd, src_c, bg_c),
Compose::Plus => Plus::compose(simd, src_c, bg_c),
// Have not been able to find a formula for this, so just fallback to Plus.
Compose::PlusLighter => Plus::compose(simd, src_c, bg_c),
};
if let Some(alpha_mask) = alpha_mask {
let alpha_mask_inv = 1.0 - alpha_mask;
res = alpha_mask * res + alpha_mask_inv * bg_c;
}
res
}
}
macro_rules! compose {
($name:ident, $fa:expr, $fb:expr, $sat:expr) => {
struct $name;
impl $name {
fn compose<S: Simd>(simd: S, src_c: f32x16<S>, bg_c: f32x16<S>) -> f32x16<S> {
let al_b = bg_c.splat_4th();
let al_s = src_c.splat_4th();
let fa = $fa(simd, al_s, al_b);
let fb = $fb(simd, al_s, al_b);
if $sat {
(src_c * fa + fb * bg_c)
.min(f32x16::splat(simd, 1.0))
.max(f32x16::splat(simd, 0.0))
} else {
src_c * fa + fb * bg_c
}
}
}
};
}
compose!(
Clear,
|simd, _, _| f32x16::splat(simd, 0.0),
|simd, _, _| f32x16::splat(simd, 0.0),
false
);
compose!(
Copy,
|simd, _, _| f32x16::splat(simd, 1.0),
|simd, _, _| f32x16::splat(simd, 0.0),
false
);
compose!(
SrcOver,
|simd, _, _| f32x16::splat(simd, 1.0),
|_, al_s: f32x16<S>, _| 1.0 - al_s,
false
);
compose!(
DestOver,
|_, _, al_b: f32x16<S>| 1.0 - al_b,
|simd, _, _| f32x16::splat(simd, 1.0),
false
);
compose!(
Dest,
|simd, _, _| f32x16::splat(simd, 0.0),
|simd, _, _| f32x16::splat(simd, 1.0),
false
);
compose!(
Xor,
|_, _, al_b: f32x16<S>| 1.0 - al_b,
|_, al_s: f32x16<S>, _| 1.0 - al_s,
false
);
compose!(
SrcIn,
|_, _, al_b: f32x16<S>| al_b,
|simd, _, _| f32x16::splat(simd, 0.0),
false
);
compose!(
DestIn,
|simd, _, _| f32x16::splat(simd, 0.0),
|_, al_s: f32x16<S>, _| al_s,
false
);
compose!(
SrcOut,
|_, _, al_b: f32x16<S>| 1.0 - al_b,
|simd, _, _| f32x16::splat(simd, 0.0),
false
);
compose!(
DestOut,
|simd, _, _| f32x16::splat(simd, 0.0),
|_, al_s: f32x16<S>, _| 1.0 - al_s,
false
);
compose!(
SrcAtop,
|_, _, al_b: f32x16<S>| al_b,
|_, al_s: f32x16<S>, _| 1.0 - al_s,
false
);
compose!(
DestAtop,
|_, _, al_b: f32x16<S>| 1.0 - al_b,
|_, al_s: f32x16<S>, _| al_s,
false
);
compose!(
Plus,
|simd, _, _| f32x16::splat(simd, 1.0),
|simd, _, _| f32x16::splat(simd, 1.0),
true
);
+544
View File
@@ -0,0 +1,544 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! High-precision (f32) rendering kernel implementation.
//!
//! This module implements the fine rasterization stage using 32-bit floating-point
//! values for color components. This provides maximum precision and color accuracy,
//! at the cost of higher memory bandwidth and potentially slower performance compared
//! to the low-precision u8 kernel.
//!
//! The f32 kernel is particularly useful for:
//! - Scenes requiring high precision (e.g., gradients with subtle color transitions)
//! - Debugging and reference implementations
//! - Platforms where SIMD f32 operations are well-optimized
use crate::filter::filter_highp;
use crate::fine::FineKernel;
use crate::fine::{COLOR_COMPONENTS, Painter, Splat4thExt};
use crate::layer_manager::LayerManager;
use crate::peniko::BlendMode;
use crate::region::Region;
use vello_common::fearless_simd::*;
use vello_common::filter_effects::Filter;
use vello_common::kurbo::Affine;
use vello_common::mask::Mask;
use vello_common::paint::{PremulColor, Tint, TintMode};
use vello_common::pixmap::Pixmap;
use vello_common::tile::Tile;
pub(crate) mod blend;
pub(crate) mod compose;
/// The kernel for doing rendering using f32.
#[derive(Clone, Copy, Debug)]
pub struct F32Kernel;
impl<S: Simd> FineKernel<S> for F32Kernel {
type Numeric = f32;
type Composite = f32x16<S>;
type NumericVec = f32x16<S>;
/// Extracts RGBA color components from a premultiplied color as f32 values [0.0, 1.0].
#[inline(always)]
fn extract_color(color: PremulColor) -> [Self::Numeric; 4] {
color.as_premul_f32().components
}
/// Copies rendered pixels from the scratch buffer to the output region.
///
/// Converts f32 color values in [0.0, 1.0] range to u8 [0, 255] with rounding,
/// transforming from column-major scratch buffer to row-major region layout.
#[inline(always)]
fn pack(simd: S, region: &mut Region<'_>, blend_buf: &[Self::Numeric]) {
simd.vectorize(
#[inline(always)]
|| {
for y in 0..Tile::HEIGHT {
for (x, pixel) in region
.row_mut(y)
.chunks_exact_mut(COLOR_COMPONENTS)
.enumerate()
{
let idx =
COLOR_COMPONENTS * (usize::from(Tile::HEIGHT) * x + usize::from(y));
let start = &blend_buf[idx..];
// Convert f32 [0.0, 1.0] to u8 [0, 255] with rounding.
// TODO: Use explicit SIMD for better performance
let converted = [
(start[0] * 255.0 + 0.5) as u8,
(start[1] * 255.0 + 0.5) as u8,
(start[2] * 255.0 + 0.5) as u8,
(start[3] * 255.0 + 0.5) as u8,
];
pixel.copy_from_slice(&converted);
}
}
},
);
}
/// Copies pixels from the output region to the scratch buffer.
///
/// Converts u8 color values [0, 255] to normalized f32 [0.0, 1.0],
/// transforming from row-major region layout to column-major scratch buffer.
/// This is the inverse operation of `pack`.
#[inline(always)]
fn unpack(simd: S, region: &mut Region<'_>, blend_buf: &mut [Self::Numeric]) {
simd.vectorize(
#[inline(always)]
|| {
for y in 0..Tile::HEIGHT {
for (x, pixel) in region.row_mut(y).chunks_exact(COLOR_COMPONENTS).enumerate() {
let idx =
COLOR_COMPONENTS * (usize::from(Tile::HEIGHT) * x + usize::from(y));
let start = &mut blend_buf[idx..];
// Convert u8 [0, 255] to normalized f32 [0.0, 1.0] (reverse of pack)
start[0] = pixel[0] as f32 / 255.0;
start[1] = pixel[1] as f32 / 255.0;
start[2] = pixel[2] as f32 / 255.0;
start[3] = pixel[3] as f32 / 255.0;
}
}
},
);
}
/// Applies a filter effect to a rendered layer.
///
/// Delegates to the f32-specific filter implementation.
fn filter_layer(
pixmap: &mut Pixmap,
filter: &Filter,
layer_manager: &mut LayerManager,
transform: Affine,
) {
filter_highp(filter, pixmap, layer_manager, transform);
}
/// Fills a buffer with a solid color using SIMD operations.
///
/// Efficiently broadcasts a single RGBA color across all pixels in the destination.
#[inline(never)]
fn copy_solid(simd: S, dest: &mut [Self::Numeric], src: [Self::Numeric; 4]) {
simd.vectorize(
#[inline(always)]
|| {
let color = f32x16::block_splat(src.simd_into(simd));
for el in dest.chunks_exact_mut(16) {
color.store_slice(el);
}
},
);
}
/// Applies per-pixel mask values to a buffer by multiplying each component.
///
/// Used for anti-aliasing and clipping effects. Each pixel is multiplied by
/// its corresponding mask value (already normalized to [0.0, 1.0]).
fn apply_mask(
simd: S,
dest: &mut [Self::Numeric],
mut src: impl Iterator<Item = Self::NumericVec>,
) {
simd.vectorize(
#[inline(always)]
|| {
for el in dest.chunks_exact_mut(16) {
let loaded = f32x16::from_slice(simd, el);
let mulled = loaded * src.next().unwrap();
mulled.store_slice(el);
}
},
);
}
/// Applies a painter's output to the destination buffer.
///
/// Delegates to the painter's f32-specific implementation.
#[inline(always)]
fn apply_painter<'a>(_: S, dest: &mut [Self::Numeric], mut painter: impl Painter + 'a) {
painter.paint_f32(dest);
}
#[inline(always)]
fn apply_tint(simd: S, dest: &mut [Self::Numeric], tint: &Tint) {
let premul = tint.color.premultiply();
let [r, g, b, a] = premul.components;
let tint_v = f32x16::block_splat(f32x4::from_slice(simd, &[r, g, b, a]));
simd.vectorize(
#[inline(always)]
|| match tint.mode {
TintMode::AlphaMask => {
for chunk in dest.chunks_exact_mut(16) {
let pixel = f32x16::from_slice(simd, chunk);
let alphas = pixel.splat_4th();
let tinted = tint_v * alphas;
tinted.store_slice(chunk);
}
}
TintMode::Multiply => {
for chunk in dest.chunks_exact_mut(16) {
let pixel = f32x16::from_slice(simd, chunk);
let tinted = pixel * tint_v;
tinted.store_slice(chunk);
}
}
},
);
}
/// Composites a solid color onto a buffer using alpha blending.
///
/// Dispatches to either the masked or unmasked implementation based on the
/// presence of per-pixel alpha masks.
#[inline(always)]
fn alpha_composite_solid(
simd: S,
dest: &mut [Self::Numeric],
src: [Self::Numeric; 4],
alphas: Option<&[u8]>,
) {
if let Some(alphas) = alphas {
alpha_fill::alpha_composite_solid(
simd,
dest,
src,
bytemuck::cast_slice::<u8, [u8; 4]>(alphas).iter().copied(),
);
} else {
fill::alpha_composite_solid(simd, dest, src);
}
}
/// Composites a source buffer onto a destination buffer using alpha blending.
///
/// Dispatches to either the masked or unmasked implementation based on the
/// presence of per-pixel alpha masks. Each source pixel's alpha determines
/// the blending amount.
fn alpha_composite_buffer(
simd: S,
dest: &mut [Self::Numeric],
src: &[Self::Numeric],
alphas: Option<&[u8]>,
) {
if let Some(alphas) = alphas {
alpha_fill::alpha_composite_arbitrary(
simd,
dest,
src.chunks_exact(16).map(|el| f32x16::from_slice(simd, el)),
bytemuck::cast_slice::<u8, [u8; 4]>(alphas).iter().copied(),
);
} else {
fill::alpha_composite_arbitrary(
simd,
dest,
src.chunks_exact(16).map(|el| f32x16::from_slice(simd, el)),
);
}
}
/// Applies a blend mode to composite source pixels onto destination.
///
/// Dispatches to either the masked or unmasked blend implementation.
/// Handles both color mixing (multiply, screen, etc.) and compositing.
fn blend(
simd: S,
dest: &mut [Self::Numeric],
mut start_x: u16,
start_y: u16,
src: impl Iterator<Item = Self::Composite>,
blend_mode: BlendMode,
alphas: Option<&[u8]>,
mask: Option<&Mask>,
) {
let alpha_iter = alphas.map(|a| bytemuck::cast_slice::<u8, [u8; 4]>(a).iter().copied());
let mask_iter = mask.map(|m| {
let width = m.width();
let height = m.height();
core::iter::from_fn(move || {
let samples = if start_x < width && start_y + 3 < height {
// All in bounds, sample directly
[
m.sample(start_x, start_y),
m.sample(start_x, start_y + 1),
m.sample(start_x, start_y + 2),
m.sample(start_x, start_y + 3),
]
} else {
// Fallback: check each individually
[
if start_x < width && start_y < height {
m.sample(start_x, start_y)
} else {
255
},
if start_x < width && start_y + 1 < height {
m.sample(start_x, start_y + 1)
} else {
255
},
if start_x < width && start_y + 2 < height {
m.sample(start_x, start_y + 2)
} else {
255
},
if start_x < width && start_y + 3 < height {
m.sample(start_x, start_y + 3)
} else {
255
},
]
};
start_x += 1;
Some(samples)
})
});
match (alpha_iter, mask_iter) {
(Some(alpha_iter), Some(mut mask_iter)) => {
let iter = alpha_iter.map(|a1| {
let a2 = mask_iter.next().unwrap();
[
((a1[0] as u16 * a2[0] as u16) / 255) as u8,
((a1[1] as u16 * a2[1] as u16) / 255) as u8,
((a1[2] as u16 * a2[2] as u16) / 255) as u8,
((a1[3] as u16 * a2[3] as u16) / 255) as u8,
]
});
alpha_fill::blend(simd, dest, src, iter, blend_mode);
}
(None, Some(mask_iter)) => alpha_fill::blend(simd, dest, src, mask_iter, blend_mode),
(Some(alpha_iter), None) => alpha_fill::blend(simd, dest, src, alpha_iter, blend_mode),
(None, None) => {
fill::blend(simd, dest, src, blend_mode);
}
}
}
}
mod fill {
//! Alpha compositing and blending operations without per-pixel alpha masks.
//!
//! This module handles the case where we're compositing full opacity pixels,
//! using only the source alpha channel for compositing.
use crate::fine::Splat4thExt;
use crate::fine::highp::blend;
use crate::fine::highp::compose::ComposeExt;
use crate::peniko::BlendMode;
use vello_common::fearless_simd::*;
// IMPORTANT: The inlining attributes (#[inline(always)], #[inline(never)]) in this
// module have been carefully tuned through benchmarking. Changing them can cause
// significant performance regressions.
/// Composites a solid color onto a buffer using alpha blending.
///
/// Uses the "over" operator: `result = src + bg * (1 - src_alpha)`
#[inline(always)]
pub(super) fn alpha_composite_solid<S: Simd>(s: S, dest: &mut [f32], src: [f32; 4]) {
s.vectorize(
#[inline(always)]
|| {
let one_minus_alpha = 1.0 - f32x16::block_splat(f32x4::splat(s, src[3]));
let src_c = f32x16::block_splat(f32x4::simd_from(s, src));
for next_dest in dest.chunks_exact_mut(16) {
alpha_composite_inner(s, next_dest, src_c, one_minus_alpha);
}
},
);
}
/// Composites a buffer of colors onto another buffer using alpha blending.
///
/// Each source pixel is composited individually based on its alpha channel.
#[inline(always)]
pub(super) fn alpha_composite_arbitrary<S: Simd, T: Iterator<Item = f32x16<S>>>(
simd: S,
dest: &mut [f32],
src: T,
) {
simd.vectorize(
#[inline(always)]
|| {
for (next_dest, next_src) in dest.chunks_exact_mut(16).zip(src) {
let one_minus_alpha = 1.0 - next_src.splat_4th();
alpha_composite_inner(simd, next_dest, next_src, one_minus_alpha);
}
},
);
}
/// Applies blend mode compositing to a buffer without per-pixel masks.
pub(super) fn blend<S: Simd, T: Iterator<Item = f32x16<S>>>(
simd: S,
dest: &mut [f32],
src: T,
blend_mode: BlendMode,
) {
for (next_dest, next_src) in dest.chunks_exact_mut(16).zip(src) {
let bg_v = f32x16::from_slice(simd, next_dest);
let src_c = blend::mix(next_src, bg_v, blend_mode);
let res = blend_mode.compose(simd, src_c, bg_v, None);
res.store_slice(next_dest);
}
}
/// Performs the core alpha compositing calculation.
///
/// Formula: `result = src + bg * (1 - src_alpha)`
/// This implements the Porter-Duff "source over" operator using FMA for efficiency.
#[inline(always)]
fn alpha_composite_inner<S: Simd>(
s: S,
dest: &mut [f32],
src: f32x16<S>,
one_minus_alpha: f32x16<S>,
) {
let mut bg_c = f32x16::from_slice(s, dest);
bg_c = one_minus_alpha.mul_add(bg_c, src);
bg_c.store_slice(dest);
}
}
mod alpha_fill {
//! Alpha compositing and blending operations with per-pixel alpha masks.
//!
//! This module handles compositing when each pixel has an additional mask value
//! (e.g., from anti-aliasing or clip masks) that modulates the source alpha.
use crate::fine::Splat4thExt;
use crate::fine::highp::compose::ComposeExt;
use crate::fine::highp::{blend, extract_masks};
use crate::peniko::BlendMode;
use vello_common::fearless_simd::*;
/// Composites a solid color with per-pixel alpha masks.
///
/// Combines source alpha with mask values: `effective_alpha = src_alpha * mask`
#[inline(always)]
pub(super) fn alpha_composite_solid<S: Simd>(
s: S,
dest: &mut [f32],
src: [f32; 4],
alphas: impl Iterator<Item = [u8; 4]>,
) {
s.vectorize(
#[inline(always)]
|| {
let src_a = f32x16::splat(s, src[3]);
let src_c = f32x16::block_splat(src.simd_into(s));
let one = f32x16::splat(s, 1.0);
for (next_dest, next_mask) in dest.chunks_exact_mut(16).zip(alphas) {
alpha_composite_inner(s, next_dest, &next_mask, src_c, src_a, one);
}
},
);
}
/// Composites a buffer of colors with per-pixel alpha masks.
///
/// Each pixel's source alpha is modulated by its corresponding mask value.
pub(super) fn alpha_composite_arbitrary<S: Simd, T: Iterator<Item = f32x16<S>>>(
simd: S,
dest: &mut [f32],
src: T,
alphas: impl Iterator<Item = [u8; 4]>,
) {
simd.vectorize(
#[inline(always)]
|| {
let one = f32x16::splat(simd, 1.0);
for ((next_dest, next_mask), next_src) in
dest.chunks_exact_mut(16).zip(alphas).zip(src)
{
let src_a = next_src.splat_4th();
alpha_composite_inner(simd, next_dest, &next_mask, next_src, src_a, one);
}
},
);
}
/// Applies blend mode compositing with per-pixel alpha masks.
pub(super) fn blend<S: Simd, T: Iterator<Item = f32x16<S>>>(
simd: S,
dest: &mut [f32],
src: T,
alphas: impl Iterator<Item = [u8; 4]>,
blend_mode: BlendMode,
) {
simd.vectorize(
#[inline(always)]
|| {
for ((next_dest, next_mask), next_src) in
dest.chunks_exact_mut(16).zip(alphas).zip(src)
{
let masks = extract_masks(simd, &next_mask);
let bg = f32x16::from_slice(simd, next_dest);
let src_c = blend::mix(next_src, bg, blend_mode);
let res = blend_mode.compose(simd, src_c, bg, Some(masks));
res.store_slice(next_dest);
}
},
);
}
/// Performs alpha compositing with mask modulation.
///
/// Formula: `result = src * mask + bg * (1 - src_alpha * mask)`
/// The mask value modulates both the source contribution and the inverse alpha.
/// Uses FMA instructions for optimal performance.
#[inline(always)]
fn alpha_composite_inner<S: Simd>(
s: S,
dest: &mut [f32],
masks: &[u8; 4],
src_c: f32x16<S>,
src_a: f32x16<S>,
one: f32x16<S>,
) {
let bg_c = f32x16::from_slice(s, dest);
let mask_a = extract_masks(s, masks);
// 1 - src_a * mask_a
let inv_src_a_mask_a = src_a.mul_add(-mask_a, one);
let res = bg_c.mul_add(inv_src_a_mask_a, src_c * mask_a);
res.store_slice(dest);
}
}
/// Expands 4 mask bytes into a 16-element f32 SIMD vector with normalized values.
///
/// Converts u8 mask values to f32 in range [0.0, 1.0], then duplicates each mask
/// value across 4 consecutive elements (one per color component).
///
/// Input: [m0, m1, m2, m3] (as u8, 0-255)
/// Output: [m0/255, m0/255, m0/255, m0/255, m1/255, ..., m3/255] (as f32, 16 elements)
#[inline(always)]
fn extract_masks<S: Simd>(simd: S, masks: &[u8; 4]) -> f32x16<S> {
let mut base_mask = [
masks[0] as f32,
masks[1] as f32,
masks[2] as f32,
masks[3] as f32,
]
.simd_into(simd);
base_mask *= f32x4::splat(simd, 1.0 / 255.0);
let res = f32x16::block_splat(base_mask);
let zip_low = res.zip_low(res);
zip_low.zip_low(zip_low)
}
+161
View File
@@ -0,0 +1,161 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use crate::fine::Splat4thExt;
use crate::peniko::{BlendMode, Compose};
use crate::util::NormalizedMulExt;
use vello_common::fearless_simd::*;
use vello_common::util::Div255Ext;
pub(crate) trait ComposeExt {
fn compose<S: Simd>(
&self,
simd: S,
src_c: u8x32<S>,
bg_c: u8x32<S>,
alpha_mask: Option<u8x32<S>>,
) -> u8x32<S>;
}
impl ComposeExt for BlendMode {
fn compose<S: Simd>(
&self,
simd: S,
src_c: u8x32<S>,
bg_c: u8x32<S>,
alpha_mask: Option<u8x32<S>>,
) -> u8x32<S> {
let mut res = match self.compose {
Compose::SrcOver => SrcOver::compose(simd, src_c, bg_c),
Compose::Clear => Clear::compose(simd, src_c, bg_c),
Compose::Copy => Copy::compose(simd, src_c, bg_c),
Compose::DestOver => DestOver::compose(simd, src_c, bg_c),
Compose::Dest => Dest::compose(simd, src_c, bg_c),
Compose::SrcIn => SrcIn::compose(simd, src_c, bg_c),
Compose::DestIn => DestIn::compose(simd, src_c, bg_c),
Compose::SrcOut => SrcOut::compose(simd, src_c, bg_c),
Compose::DestOut => DestOut::compose(simd, src_c, bg_c),
Compose::SrcAtop => SrcAtop::compose(simd, src_c, bg_c),
Compose::DestAtop => DestAtop::compose(simd, src_c, bg_c),
Compose::Xor => Xor::compose(simd, src_c, bg_c),
Compose::Plus => Plus::compose(simd, src_c, bg_c),
// Have not been able to find a formula for this, so just fallback to Plus.
Compose::PlusLighter => Plus::compose(simd, src_c, bg_c),
};
if let Some(alpha_mask) = alpha_mask {
let alpha_mask_inv = 255 - alpha_mask;
let p1 = simd.widen_u8x32(alpha_mask) * simd.widen_u8x32(res);
let p2 = simd.widen_u8x32(alpha_mask_inv) * simd.widen_u8x32(bg_c);
res = simd.narrow_u16x32((p1 + p2).div_255());
}
res
}
}
macro_rules! compose {
($name:ident, $fa:expr, $fb:expr, $sat:expr) => {
struct $name;
impl $name {
fn compose<S: Simd>(simd: S, src_c: u8x32<S>, bg_c: u8x32<S>) -> u8x32<S> {
let al_b = bg_c.splat_4th();
let al_s = src_c.splat_4th();
let fa = $fa(simd, al_s, al_b);
let fb = $fb(simd, al_s, al_b);
if $sat {
simd.narrow_u16x32(
(simd.widen_u8x32(src_c.normalized_mul(fa))
+ simd.widen_u8x32(fb.normalized_mul(bg_c)))
.min(u16x32::splat(simd, 255))
.max(u16x32::splat(simd, 0)),
)
} else {
src_c.normalized_mul(fa) + fb.normalized_mul(bg_c)
}
}
}
};
}
compose!(
Clear,
|simd, _, _| u8x32::splat(simd, 0),
|simd, _, _| u8x32::splat(simd, 0),
false
);
compose!(
Copy,
|simd, _, _| u8x32::splat(simd, 255),
|simd, _, _| u8x32::splat(simd, 0),
false
);
compose!(
SrcOver,
|simd, _, _| u8x32::splat(simd, 255),
|_, al_s: u8x32<S>, _| 255 - al_s,
false
);
compose!(
DestOver,
|_, _, al_b: u8x32<S>| 255 - al_b,
|simd, _, _| u8x32::splat(simd, 255),
false
);
compose!(
Dest,
|simd, _, _| u8x32::splat(simd, 0),
|simd, _, _| u8x32::splat(simd, 255),
false
);
compose!(
Xor,
|_, _, al_b: u8x32<S>| 255 - al_b,
|_, al_s: u8x32<S>, _| 255 - al_s,
false
);
compose!(
SrcIn,
|_, _, al_b: u8x32<S>| al_b,
|simd, _, _| u8x32::splat(simd, 0),
false
);
compose!(
DestIn,
|simd, _, _| u8x32::splat(simd, 0),
|_, al_s: u8x32<S>, _| al_s,
false
);
compose!(
SrcOut,
|_, _, al_b: u8x32<S>| 255 - al_b,
|simd, _, _| u8x32::splat(simd, 0),
false
);
compose!(
DestOut,
|simd, _, _| u8x32::splat(simd, 0),
|_, al_s: u8x32<S>, _| 255 - al_s,
false
);
compose!(
SrcAtop,
|_, _, al_b: u8x32<S>| al_b,
|_, al_s: u8x32<S>, _| 255 - al_s,
false
);
compose!(
DestAtop,
|_, _, al_b: u8x32<S>| 255 - al_b,
|_, al_s: u8x32<S>, _| al_s,
false
);
compose!(
Plus,
|simd, _, _| u8x32::splat(simd, 255),
|simd, _, _| u8x32::splat(simd, 255),
true
);
+84
View File
@@ -0,0 +1,84 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use crate::peniko;
use core::slice::ChunksExact;
use vello_common::encode::EncodedGradient;
use vello_common::fearless_simd::*;
/// An accelerated gradient painter for u8.
///
/// Assumes that the gradient has no undefined positions.
#[derive(Debug)]
pub(crate) struct GradientPainter<'a, S: Simd> {
gradient: &'a EncodedGradient,
lut: &'a [[u8; 4]],
t_vals: ChunksExact<'a, f32>,
scale_factor: f32x16<S>,
simd: S,
}
impl<'a, S: Simd> GradientPainter<'a, S> {
pub(crate) fn new(simd: S, gradient: &'a EncodedGradient, t_vals: &'a [f32]) -> Self {
let lut = gradient.u8_lut(simd);
let scale_factor = f32x16::splat(simd, lut.scale_factor());
Self {
gradient,
scale_factor,
lut: lut.lut(),
t_vals: t_vals.chunks_exact(16),
simd,
}
}
}
impl<S: Simd> Iterator for GradientPainter<'_, S> {
type Item = u8x64<S>;
#[inline(always)]
fn next(&mut self) -> Option<Self::Item> {
let extend = self.gradient.extend;
let pos = f32x16::from_slice(self.simd, self.t_vals.next()?);
let t_vals = apply_extend(pos, extend);
let indices = (t_vals * self.scale_factor).to_int::<u32x16<S>>();
let mut vals = [0_u8; 64];
for (val, idx) in vals.chunks_exact_mut(4).zip(*indices) {
val.copy_from_slice(&self.lut[idx as usize]);
}
Some(u8x64::from_slice(self.simd, &vals))
}
}
impl<S: Simd> crate::fine::Painter for GradientPainter<'_, S> {
fn paint_u8(&mut self, buf: &mut [u8]) {
self.simd.vectorize(
#[inline(always)]
|| {
for chunk in buf.chunks_exact_mut(64) {
self.next().unwrap().store_slice(chunk);
}
},
);
}
fn paint_f32(&mut self, _: &mut [f32]) {
unimplemented!()
}
}
// TODO: Maybe delete this method and use `apply_extend` from highp by splitting into two f32x8.
#[inline(always)]
pub(crate) fn apply_extend<S: Simd>(val: f32x16<S>, extend: peniko::Extend) -> f32x16<S> {
match extend {
peniko::Extend::Pad => val.max(0.0).min(1.0),
peniko::Extend::Repeat => (val - val.floor()).fract(),
// See <https://github.com/google/skia/blob/220738774f7a0ce4a6c7bd17519a336e5e5dea5b/src/opts/SkRasterPipeline_opts.h#L6472-L6475>
peniko::Extend::Reflect => ((val - 1.0) - 2.0 * ((val - 1.0) * 0.5).floor() - 1.0)
.abs()
.max(0.0)
.min(1.0),
}
}
+259
View File
@@ -0,0 +1,259 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use crate::fine::PosExt;
use crate::fine::common::image::{ImagePainterData, extend, fract_floor, sample};
use crate::fine::macros::u8x16_painter;
use vello_common::encode::EncodedImage;
use vello_common::fearless_simd::{Simd, SimdBase, SimdFloat, f32x4, u8x16, u16x16};
use vello_common::pixmap::Pixmap;
use vello_common::simd::element_wise_splat;
use vello_common::util::{Div255Ext, f32_to_u8};
/// A faster bilinear image renderer for the u8 pipeline.
#[derive(Debug)]
pub(crate) struct BilinearImagePainter<'a, S: Simd> {
data: ImagePainterData<'a, S>,
simd: S,
}
impl<'a, S: Simd> BilinearImagePainter<'a, S> {
pub(crate) fn new(
simd: S,
image: &'a EncodedImage,
pixmap: &'a Pixmap,
start_x: f64,
start_y: f64,
) -> Self {
let data = ImagePainterData::new(simd, image, pixmap, start_x, start_y);
Self { data, simd }
}
}
impl<S: Simd> Iterator for BilinearImagePainter<'_, S> {
type Item = u8x16<S>;
fn next(&mut self) -> Option<Self::Item> {
let x_positions = f32x4::splat_pos(
self.simd,
self.data.cur_pos.x as f32,
self.data.x_advances.0,
self.data.y_advances.0,
);
let y_positions = f32x4::splat_pos(
self.simd,
self.data.cur_pos.y as f32,
self.data.x_advances.1,
self.data.y_advances.1,
);
let extend_x = |x_pos: f32x4<S>| {
extend(
self.simd,
x_pos,
self.data.image.sampler.x_extend,
self.data.width,
self.data.width_inv,
)
};
let extend_y = |y_pos: f32x4<S>| {
extend(
self.simd,
y_pos,
self.data.image.sampler.y_extend,
self.data.height,
self.data.height_inv,
)
};
let fx = f32_to_u8(element_wise_splat(
self.simd,
fract_floor(x_positions + 0.5).mul_add(255.0, 0.5),
));
let fy = f32_to_u8(element_wise_splat(
self.simd,
fract_floor(y_positions + 0.5).mul_add(255.0, 0.5),
));
let fx = self.simd.widen_u8x16(fx);
let fy = self.simd.widen_u8x16(fy);
let fx_inv = u16x16::splat(self.simd, 255) - fx;
let fy_inv = u16x16::splat(self.simd, 255) - fy;
let x_pos1 = extend_x(x_positions - 0.5);
let x_pos2 = extend_x(x_positions + 0.5);
let y_pos1 = extend_y(y_positions - 0.5);
let y_pos2 = extend_y(y_positions + 0.5);
let p00 = self
.simd
.widen_u8x16(sample(self.simd, &self.data, x_pos1, y_pos1));
let p10 = self
.simd
.widen_u8x16(sample(self.simd, &self.data, x_pos2, y_pos1));
let p01 = self
.simd
.widen_u8x16(sample(self.simd, &self.data, x_pos1, y_pos2));
let p11 = self
.simd
.widen_u8x16(sample(self.simd, &self.data, x_pos2, y_pos2));
let ip1 = (p00 * fx_inv + p10 * fx).div_255();
let ip2 = (p01 * fx_inv + p11 * fx).div_255();
let res = self.simd.narrow_u16x16((ip1 * fy_inv + ip2 * fy).div_255());
self.data.cur_pos += self.data.image.x_advance;
Some(res)
}
}
u8x16_painter!(BilinearImagePainter<'_, S>);
/// A faster bilinear image renderer for axis-aligned images (no skew) in the u8 pipeline.
///
/// This is an optimized version of `BilinearImagePainter` that pre-computes y positions
/// and interpolation weights since they don't change when there's no skew.
#[derive(Debug)]
pub(crate) struct PlainBilinearImagePainter<'a, S: Simd> {
data: ImagePainterData<'a, S>,
simd: S,
/// Pre-computed y sample positions (top row for bilinear grid)
y_pos1: f32x4<S>,
/// Pre-computed y sample positions (bottom row for bilinear grid)
y_pos2: f32x4<S>,
/// Pre-computed y interpolation weight
fy: u16x16<S>,
/// Pre-computed inverse y interpolation weight
fy_inv: u16x16<S>,
/// Current x position
cur_x_pos: f32x4<S>,
/// X advance per iteration
advance: f32,
}
impl<'a, S: Simd> PlainBilinearImagePainter<'a, S> {
pub(crate) fn new(
simd: S,
image: &'a EncodedImage,
pixmap: &'a Pixmap,
start_x: f64,
start_y: f64,
) -> Self {
let data = ImagePainterData::new(simd, image, pixmap, start_x, start_y);
// For axis-aligned images, y doesn't change across the strip
let y_positions = f32x4::splat_pos(
simd,
data.cur_pos.y as f32,
data.x_advances.1,
data.y_advances.1,
);
// Pre-compute y extend positions
let y_pos1 = extend(
simd,
y_positions - 0.5,
image.sampler.y_extend,
data.height,
data.height_inv,
);
let y_pos2 = extend(
simd,
y_positions + 0.5,
image.sampler.y_extend,
data.height,
data.height_inv,
);
// Pre-compute y interpolation weights
let fy = f32_to_u8(element_wise_splat(
simd,
fract_floor(y_positions + 0.5).mul_add(255.0, 0.5),
));
let fy = simd.widen_u8x16(fy);
let fy_inv = u16x16::splat(simd, 255) - fy;
let cur_x_pos = f32x4::splat_pos(
simd,
data.cur_pos.x as f32,
data.x_advances.0,
data.y_advances.0,
);
Self {
data,
y_pos1,
y_pos2,
fy,
fy_inv,
cur_x_pos,
advance: image.x_advance.x as f32,
simd,
}
}
}
impl<S: Simd> Iterator for PlainBilinearImagePainter<'_, S> {
type Item = u8x16<S>;
#[inline(always)]
fn next(&mut self) -> Option<Self::Item> {
let x_minus_half = self.cur_x_pos - 0.5;
let x_plus_half = self.cur_x_pos + 0.5;
// Only x needs to be extended per-iteration
let x_pos1 = extend(
self.simd,
x_minus_half,
self.data.image.sampler.x_extend,
self.data.width,
self.data.width_inv,
);
let x_pos2 = extend(
self.simd,
x_plus_half,
self.data.image.sampler.x_extend,
self.data.width,
self.data.width_inv,
);
// Compute x interpolation weights
let fx = f32_to_u8(element_wise_splat(
self.simd,
fract_floor(x_plus_half).mul_add(255.0, 0.5),
));
let fx = self.simd.widen_u8x16(fx);
let fx_inv = u16x16::splat(self.simd, 255) - fx;
// Sample the 4 corners using pre-computed y positions
let p00 = self
.simd
.widen_u8x16(sample(self.simd, &self.data, x_pos1, self.y_pos1));
let p10 = self
.simd
.widen_u8x16(sample(self.simd, &self.data, x_pos2, self.y_pos1));
let p01 = self
.simd
.widen_u8x16(sample(self.simd, &self.data, x_pos1, self.y_pos2));
let p11 = self
.simd
.widen_u8x16(sample(self.simd, &self.data, x_pos2, self.y_pos2));
// Bilinear interpolation
let ip1 = (p00 * fx_inv + p10 * fx).div_255();
let ip2 = (p01 * fx_inv + p11 * fx).div_255();
let res = self
.simd
.narrow_u16x16((ip1 * self.fy_inv + ip2 * self.fy).div_255());
self.cur_x_pos += self.advance;
Some(res)
}
}
u8x16_painter!(PlainBilinearImagePainter<'_, S>);
+770
View File
@@ -0,0 +1,770 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Low-precision (u8/u16) rendering kernel implementation.
//!
//! This module implements the fine rasterization stage using 8-bit unsigned integers
//! for color values and 16-bit for intermediate calculations. This provides better
//! performance on many architectures compared to floating-point operations, while
//! maintaining sufficient precision for most rendering tasks.
mod compose;
mod gradient;
mod image;
use crate::filter::filter_lowp;
use crate::fine::lowp::image::{BilinearImagePainter, PlainBilinearImagePainter};
use crate::fine::{COLOR_COMPONENTS, Painter, SCRATCH_BUF_SIZE, Splat4thExt};
use crate::fine::{FineKernel, highp, u8_to_f32};
use crate::layer_manager::LayerManager;
use crate::peniko::BlendMode;
use crate::region::Region;
use crate::util::NormalizedMulExt;
use crate::util::scalar::div_255;
use bytemuck::cast_slice;
use core::iter;
use vello_common::coarse::WideTile;
use vello_common::encode::{EncodedGradient, EncodedImage};
use vello_common::fearless_simd::*;
use vello_common::filter_effects::Filter;
use vello_common::kurbo::Affine;
use vello_common::mask::Mask;
use vello_common::paint::{PremulColor, Tint, TintMode};
use vello_common::pixmap::Pixmap;
use vello_common::tile::Tile;
use vello_common::util::{Div255Ext, f32_to_u8};
/// The kernel for doing rendering using u8/u16.
#[derive(Clone, Copy, Debug)]
pub struct U8Kernel;
impl<S: Simd> FineKernel<S> for U8Kernel {
type Numeric = u8;
type Composite = u8x32<S>;
type NumericVec = u8x16<S>;
/// Extracts RGBA color components from a premultiplied color as u8 values [0, 255].
#[inline]
fn extract_color(color: PremulColor) -> [Self::Numeric; 4] {
color.as_premul_rgba8().to_u8_array()
}
/// Copies rendered pixels from the scratch buffer to the output region.
///
/// Converts from column-major scratch buffer layout to row-major region layout,
/// using either a SIMD-optimized path for full tiles or a scalar fallback.
#[inline(always)]
fn pack(simd: S, region: &mut Region<'_>, blend_buf: &[Self::Numeric]) {
if region.width != WideTile::WIDTH || region.height != Tile::HEIGHT {
// Use scalar path for non-standard tile sizes. Wrapping this in `vectorize`
// degrades performance significantly on SSE4.2.
pack(region, blend_buf);
} else {
simd.vectorize(
#[inline(always)]
|| {
pack_block(simd, region, blend_buf);
},
);
}
}
/// Copies pixels from the output region to the scratch buffer.
///
/// Converts from row-major region layout to column-major scratch buffer layout.
/// This is the inverse operation of `pack`.
#[inline(always)]
fn unpack(simd: S, region: &mut Region<'_>, blend_buf: &mut [Self::Numeric]) {
if region.width != WideTile::WIDTH || region.height != Tile::HEIGHT {
// Use scalar path for non-standard tile sizes.
// Note that right now, this path is unused (only for benchmarking), because when
// using filters we always allocate pixmaps of the same size as a wide tile.
// Nevertheless, we still keep this function here for reference, or in case that
// changes in the future.
unpack(region, blend_buf);
} else {
simd.vectorize(
#[inline(always)]
|| {
unpack_block(simd, region, blend_buf);
},
);
}
}
/// Applies a filter effect to a rendered layer.
///
/// Delegates to the u8-specific filter implementation.
fn filter_layer(
pixmap: &mut Pixmap,
filter: &Filter,
layer_manager: &mut LayerManager,
transform: Affine,
) {
filter_lowp(filter, pixmap, layer_manager, transform);
}
/// Fills a buffer with a solid color using SIMD operations.
///
/// Efficiently broadcasts a single RGBA color across all pixels in the destination.
fn copy_solid(simd: S, dest: &mut [Self::Numeric], src: [Self::Numeric; 4]) {
simd.vectorize(
#[inline(always)]
|| {
let target: &mut [u32] = bytemuck::cast_slice_mut(dest);
target.fill(u32::from_ne_bytes(src));
},
);
}
/// Creates a painter for rendering gradients in u8 precision.
///
/// Returns a painter that evaluates the gradient at each pixel position.
fn gradient_painter<'a>(
simd: S,
gradient: &'a EncodedGradient,
t_vals: &'a [f32],
) -> impl Painter + 'a {
simd.vectorize(
#[inline(always)]
|| gradient::GradientPainter::new(simd, gradient, t_vals),
)
}
/// Creates a painter for rendering images with bilinear filtering in u8 precision.
///
/// Returns a painter that samples the image with bilinear interpolation.
fn medium_quality_image_painter<'a>(
simd: S,
image: &'a EncodedImage,
pixmap: &'a Pixmap,
start_x: f64,
start_y: f64,
) -> impl Painter + 'a {
simd.vectorize(
#[inline(always)]
|| BilinearImagePainter::new(simd, image, pixmap, start_x, start_y),
)
}
/// Creates a painter for rendering axis-aligned images with bilinear filtering in u8 precision.
///
/// Returns a painter that samples the image with bilinear interpolation.
/// This is an optimized version for images without skew transformation.
fn plain_medium_quality_image_painter<'a>(
simd: S,
image: &'a EncodedImage,
pixmap: &'a Pixmap,
start_x: f64,
start_y: f64,
) -> impl Painter + 'a {
simd.vectorize(
#[inline(always)]
|| PlainBilinearImagePainter::new(simd, image, pixmap, start_x, start_y),
)
}
/// Applies per-pixel mask values to a buffer by multiplying each component.
///
/// Used for anti-aliasing and clipping effects. Each pixel is multiplied by
/// its corresponding mask value and normalized.
fn apply_mask(
simd: S,
dest: &mut [Self::Numeric],
mut src: impl Iterator<Item = Self::NumericVec>,
) {
simd.vectorize(
#[inline(always)]
|| {
for el in dest.chunks_exact_mut(16) {
let loaded = u8x16::from_slice(simd, el);
let mulled = simd.narrow_u16x16(
(simd.widen_u8x16(loaded) * simd.widen_u8x16(src.next().unwrap()))
.div_255(),
);
mulled.store_slice(el);
}
},
);
}
/// Applies a painter's output to the destination buffer.
///
/// Delegates to the painter's u8-specific implementation.
#[inline(always)]
fn apply_painter<'a>(_: S, dest: &mut [Self::Numeric], mut painter: impl Painter + 'a) {
painter.paint_u8(dest);
}
#[inline(always)]
fn apply_tint(simd: S, dest: &mut [Self::Numeric], tint: &Tint) {
let premul = tint.color.premultiply();
let [r, g, b, a] = premul.components;
let to_u8 = |v: f32| (v * 255.0 + 0.5) as u8;
let color = u32::from_ne_bytes([to_u8(r), to_u8(g), to_u8(b), to_u8(a)]);
let tint_v = u32x8::block_splat(u32x4::splat(simd, color)).to_bytes();
simd.vectorize(
#[inline(always)]
|| match tint.mode {
TintMode::AlphaMask => {
for chunk in dest.chunks_exact_mut(32) {
let pixel = u8x32::from_slice(simd, chunk);
let alphas = pixel.splat_4th();
let tinted = tint_v.normalized_mul(alphas);
tinted.store_slice(chunk);
}
}
TintMode::Multiply => {
for chunk in dest.chunks_exact_mut(32) {
let pixel = u8x32::from_slice(simd, chunk);
let tinted = pixel.normalized_mul(tint_v);
tinted.store_slice(chunk);
}
}
},
);
}
/// Composites a solid color onto a buffer using alpha blending.
///
/// Dispatches to either the masked or unmasked implementation based on the
/// presence of per-pixel alpha masks.
#[inline(always)]
fn alpha_composite_solid(
simd: S,
dest: &mut [Self::Numeric],
src: [Self::Numeric; 4],
alphas: Option<&[u8]>,
) {
if let Some(alphas) = alphas {
alpha_fill::alpha_composite_solid(
simd,
dest,
src,
cast_slice::<u8, [u8; 8]>(alphas).iter().copied(),
);
} else {
fill::alpha_composite_solid(simd, dest, src);
}
}
/// Composites a source buffer onto a destination buffer using alpha blending.
///
/// Dispatches to either the masked or unmasked implementation based on the
/// presence of per-pixel alpha masks. Each source pixel's alpha determines
/// the blending amount.
fn alpha_composite_buffer(
simd: S,
dest: &mut [Self::Numeric],
src: &[Self::Numeric],
alphas: Option<&[u8]>,
) {
let src_iter = src.chunks_exact(32).map(|el| u8x32::from_slice(simd, el));
if let Some(alphas) = alphas {
alpha_fill::alpha_composite(
simd,
dest,
src_iter,
cast_slice::<u8, [u8; 8]>(alphas).iter().copied(),
);
} else {
fill::alpha_composite(simd, dest, src_iter);
}
}
/// Applies a blend mode to composite source pixels onto destination.
///
/// Dispatches to either the masked or unmasked blend implementation.
/// Handles both color mixing (multiply, screen, etc.) and compositing.
fn blend(
simd: S,
dest: &mut [Self::Numeric],
mut start_x: u16,
start_y: u16,
src: impl Iterator<Item = Self::Composite>,
blend_mode: BlendMode,
alphas: Option<&[u8]>,
mask: Option<&Mask>,
) {
let alpha_iter = alphas.map(|a| cast_slice::<u8, [u8; 8]>(a).iter().copied());
let mask_iter = mask.map(|m| {
iter::from_fn(|| {
let sample = |x: u16, y: u16| {
if x < m.width() && y < m.height() {
m.sample(x, y)
} else {
255
}
};
let samples = [
sample(start_x, start_y),
sample(start_x, start_y + 1),
sample(start_x, start_y + 2),
sample(start_x, start_y + 3),
sample(start_x + 1, start_y),
sample(start_x + 1, start_y + 1),
sample(start_x + 1, start_y + 2),
sample(start_x + 1, start_y + 3),
];
start_x += 2;
Some(samples)
})
});
match (alpha_iter, mask_iter) {
(Some(alpha_iter), Some(mut mask_iter)) => {
let iter = alpha_iter.map(|a1| {
let a2 = mask_iter.next().unwrap();
[
div_255(a1[0] as u16 * a2[0] as u16) as u8,
div_255(a1[1] as u16 * a2[1] as u16) as u8,
div_255(a1[2] as u16 * a2[2] as u16) as u8,
div_255(a1[3] as u16 * a2[3] as u16) as u8,
div_255(a1[4] as u16 * a2[4] as u16) as u8,
div_255(a1[5] as u16 * a2[5] as u16) as u8,
div_255(a1[6] as u16 * a2[6] as u16) as u8,
div_255(a1[7] as u16 * a2[7] as u16) as u8,
]
});
alpha_fill::blend(simd, dest, src, blend_mode, iter);
}
(None, Some(mask_iter)) => alpha_fill::blend(simd, dest, src, blend_mode, mask_iter),
(Some(alpha_iter), None) => alpha_fill::blend(simd, dest, src, blend_mode, alpha_iter),
(None, None) => {
fill::blend(simd, dest, src, blend_mode);
}
}
}
}
mod fill {
//! Alpha compositing and blending operations without per-pixel alpha masks.
//!
//! This module handles the case where we're compositing full opacity pixels,
//! using only the source alpha channel for compositing.
use crate::fine::Splat4thExt;
use crate::fine::lowp::compose::ComposeExt;
use crate::fine::lowp::mix;
use crate::peniko::{BlendMode, Mix};
use vello_common::fearless_simd::*;
use vello_common::util::normalized_mul_u8x32;
/// Applies blend mode compositing to a buffer without per-pixel masks.
pub(super) fn blend<S: Simd, T: Iterator<Item = u8x32<S>>>(
simd: S,
dest: &mut [u8],
src: T,
blend_mode: BlendMode,
) {
simd.vectorize(
#[inline(always)]
|| {
let default_mix = matches!(blend_mode.mix, Mix::Normal);
for (next_dest, next_src) in dest.chunks_exact_mut(32).zip(src) {
let bg_v = u8x32::from_slice(simd, next_dest);
let src_v = if default_mix {
next_src
} else {
mix(next_src, bg_v, blend_mode)
};
let res = blend_mode.compose(simd, src_v, bg_v, None);
res.store_slice(next_dest);
}
},
);
}
/// Composites a solid color onto a buffer using alpha blending.
///
/// Uses the "over" operator: `result = src + bg * (1 - src_alpha)`
pub(super) fn alpha_composite_solid<S: Simd>(s: S, dest: &mut [u8], src: [u8; 4]) {
s.vectorize(
#[inline(always)]
|| {
let one_minus_alpha = 255 - u8x32::splat(s, src[3]);
let src_c = u32x8::splat(s, u32::from_ne_bytes(src)).to_bytes();
for next_dest in dest.chunks_exact_mut(64) {
// We process in batches of 64 because loading/storing is much faster this way (at least on NEON),
// but since we widen to u16, we can only work with 256 bits, so we split it up.
let bg_v = u8x64::from_slice(s, next_dest);
let (bg_1, bg_2) = s.split_u8x64(bg_v);
let res_1 = alpha_composite_inner(s, bg_1, src_c, one_minus_alpha);
let res_2 = alpha_composite_inner(s, bg_2, src_c, one_minus_alpha);
let combined = s.combine_u8x32(res_1, res_2);
combined.store_slice(next_dest);
}
},
);
}
/// Composites a buffer of colors onto another buffer using alpha blending.
///
/// Each source pixel is composited individually based on its alpha channel.
pub(super) fn alpha_composite<S: Simd, T: Iterator<Item = u8x32<S>>>(
simd: S,
dest: &mut [u8],
src: T,
) {
simd.vectorize(
#[inline(always)]
|| {
for (next_dest, next_src) in dest.chunks_exact_mut(32).zip(src) {
let one_minus_alpha = 255 - next_src.splat_4th();
let bg_v = u8x32::from_slice(simd, next_dest);
let res = alpha_composite_inner(simd, bg_v, next_src, one_minus_alpha);
res.store_slice(next_dest);
}
},
);
}
/// Performs the core alpha compositing calculation.
///
/// Formula: `result = src + bg * (1 - src_alpha)`
/// This implements the Porter-Duff "source over" operator.
#[inline(always)]
fn alpha_composite_inner<S: Simd>(
s: S,
bg: u8x32<S>,
src: u8x32<S>,
one_minus_alpha: u8x32<S>,
) -> u8x32<S> {
s.narrow_u16x32(normalized_mul_u8x32(bg, one_minus_alpha)) + src
}
}
mod alpha_fill {
//! Alpha compositing and blending operations with per-pixel alpha masks.
//!
//! This module handles compositing when each pixel has an additional mask value
//! (e.g., from anti-aliasing or clip masks) that modulates the source alpha.
use crate::fine::Splat4thExt;
use crate::fine::lowp::compose::ComposeExt;
use crate::fine::lowp::{extract_masks, mix};
use crate::peniko::{BlendMode, Mix};
use vello_common::fearless_simd::*;
use vello_common::util::{Div255Ext, normalized_mul_u8x32};
/// Applies blend mode compositing with per-pixel alpha masks.
pub(super) fn blend<S: Simd, T: Iterator<Item = u8x32<S>>>(
simd: S,
dest: &mut [u8],
src: T,
blend_mode: BlendMode,
alphas: impl Iterator<Item = [u8; 8]>,
) {
simd.vectorize(
#[inline(always)]
|| {
let default_mix = matches!(blend_mode.mix, Mix::Normal);
for ((next_bg, next_mask), next_src) in
dest.chunks_exact_mut(32).zip(alphas).zip(src)
{
let bg_v = u8x32::from_slice(simd, next_bg);
let src_c = if default_mix {
next_src
} else {
mix(next_src, bg_v, blend_mode)
};
let masks = extract_masks(simd, &next_mask);
let res = blend_mode.compose(simd, src_c, bg_v, Some(masks));
res.store_slice(next_bg);
}
},
);
}
/// Composites a solid color with per-pixel alpha masks.
///
/// Combines source alpha with mask values: `effective_alpha = src_alpha * mask / 255`
#[inline(always)]
pub(super) fn alpha_composite_solid<S: Simd>(
s: S,
dest: &mut [u8],
src: [u8; 4],
alphas: impl Iterator<Item = [u8; 8]>,
) {
s.vectorize(
#[inline(always)]
|| {
let src_a = u8x32::splat(s, src[3]);
let src_c = u32x8::splat(s, u32::from_ne_bytes(src)).to_bytes();
let one = u8x32::splat(s, 255);
for (next_bg, next_mask) in dest.chunks_exact_mut(32).zip(alphas) {
alpha_composite_inner(s, next_bg, &next_mask, src_c, src_a, one);
}
},
);
}
/// Composites a buffer of colors with per-pixel alpha masks.
///
/// Each pixel's source alpha is modulated by its corresponding mask value.
#[inline(always)]
pub(super) fn alpha_composite<S: Simd, T: Iterator<Item = u8x32<S>>>(
simd: S,
dest: &mut [u8],
src: T,
alphas: impl Iterator<Item = [u8; 8]>,
) {
simd.vectorize(
#[inline(always)]
|| {
let one = u8x32::splat(simd, 255);
for ((next_dest, next_mask), next_src) in
dest.chunks_exact_mut(32).zip(alphas).zip(src)
{
let src_a = next_src.splat_4th();
alpha_composite_inner(simd, next_dest, &next_mask, next_src, src_a, one);
}
},
);
}
/// Performs alpha compositing with mask modulation.
///
/// Formula: `result = src * mask + bg * (1 - src_alpha * mask)`
/// The mask value modulates both the source contribution and the inverse alpha.
#[inline(always)]
fn alpha_composite_inner<S: Simd>(
s: S,
dest: &mut [u8],
masks: &[u8; 8],
src_c: u8x32<S>,
src_a: u8x32<S>,
one: u8x32<S>,
) {
s.vectorize(
#[inline(always)]
|| {
let bg_v = u8x32::from_slice(s, dest);
let mask_v = extract_masks(s, masks);
let inv_src_a_mask_a = one - s.narrow_u16x32(normalized_mul_u8x32(src_a, mask_v));
let p1 = s.widen_u8x32(bg_v) * s.widen_u8x32(inv_src_a_mask_a);
let p2 = s.widen_u8x32(src_c) * s.widen_u8x32(mask_v);
let res = s.narrow_u16x32((p1 + p2).div_255());
res.store_slice(dest);
},
);
}
}
/// Applies blend mode mixing by converting to f32, mixing, then converting back to u8.
///
/// TODO: Add a proper lowp mix pipeline that operates entirely in integer space
/// for better performance (currently converts through f32 which is slower).
fn mix<S: Simd>(src_c: u8x32<S>, bg_c: u8x32<S>, blend_mode: BlendMode) -> u8x32<S> {
let to_f32 = |val: u8x32<S>| {
let (a, b) = src_c.simd.split_u8x32(val);
let mut a = u8_to_f32(a);
let mut b = u8_to_f32(b);
a *= f32x16::splat(src_c.simd, 1.0 / 255.0);
b *= f32x16::splat(src_c.simd, 1.0 / 255.0);
(a, b)
};
let to_u8 = |val1: f32x16<S>, val2: f32x16<S>| {
let val1 =
f32_to_u8(f32x16::splat(val1.simd, 255.0).mul_add(val1, f32x16::splat(val1.simd, 0.5)));
let val2 =
f32_to_u8(f32x16::splat(val2.simd, 255.0).mul_add(val2, f32x16::splat(val2.simd, 0.5)));
val1.simd.combine_u8x16(val1, val2)
};
let (mut src_1, mut src_2) = to_f32(src_c);
let (bg_1, bg_2) = to_f32(bg_c);
src_1 = highp::blend::mix(src_1, bg_1, blend_mode);
src_2 = highp::blend::mix(src_2, bg_2, blend_mode);
to_u8(src_1, src_2)
}
/// Expands 8 mask bytes into a 32-byte SIMD vector where each pixel's 4 components
/// share the same mask value (each of 8 mask values is repeated 4 times).
///
/// Input: [m0, m1, m2, m3, m4, m5, m6, m7]
/// Output: [m0, m0, m0, m0, m1, m1, m1, m1, ..., m7, m7, m7, m7]
#[inline(always)]
fn extract_masks<S: Simd>(simd: S, masks: &[u8; 8]) -> u8x32<S> {
let m1 = u32x4::splat(simd, u32::from_ne_bytes(masks[0..4].try_into().unwrap())).to_bytes();
let m2 = u32x4::splat(simd, u32::from_ne_bytes(masks[4..8].try_into().unwrap())).to_bytes();
let zipped1 = m1.zip_low(m1);
let zipped1 = zipped1.zip_low(zipped1);
let zipped2 = m2.zip_low(m2);
let zipped2 = zipped2.zip_low(zipped2);
simd.combine_u8x16(zipped1, zipped2)
}
/// Copies color data from the scratch buffer to the output region (scalar fallback).
///
/// The scratch buffer stores pixels in column-major order for SIMD efficiency,
/// while the region uses row-major order for output.
#[inline(always)]
fn pack(region: &mut Region<'_>, blend_buf: &[u8]) {
for y in 0..Tile::HEIGHT {
for (x, pixel) in region
.row_mut(y)
.chunks_exact_mut(COLOR_COMPONENTS)
.enumerate()
{
let idx = COLOR_COMPONENTS * (usize::from(Tile::HEIGHT) * x + usize::from(y));
pixel.copy_from_slice(&blend_buf[idx..][..COLOR_COMPONENTS]);
}
}
}
/// Copies color data from the output region to the scratch buffer.
///
/// Converts from row-major (region) to column-major (scratch buffer) layout.
/// This is the inverse operation of `pack`.
#[inline(always)]
fn unpack(region: &mut Region<'_>, blend_buf: &mut [u8]) {
for y in 0..Tile::HEIGHT {
for (x, pixel) in region.row_mut(y).chunks_exact(COLOR_COMPONENTS).enumerate() {
let idx = COLOR_COMPONENTS * (usize::from(Tile::HEIGHT) * x + usize::from(y));
blend_buf[idx..][..COLOR_COMPONENTS].copy_from_slice(pixel);
}
}
}
/// SIMD-optimized version of `pack` for full-size tiles using interleaved loads.
///
/// Uses `load_interleaved_128` to efficiently transpose and copy data from column-major
/// scratch buffer to row-major output region. Performance characteristics are highly
/// architecture-dependent:
/// - On NEON: ~3x faster than scalar `pack`
/// - On fallback SIMD: ~3x slower than scalar `pack`
///
/// TODO: Consider runtime detection to fall back to scalar on non-NEON architectures.
#[inline(always)]
fn pack_block<S: Simd>(simd: S, region: &mut Region<'_>, mut buf: &[u8]) {
buf = &buf[..SCRATCH_BUF_SIZE];
const CHUNK_LENGTH: usize = 64;
const SLICE_WIDTH: usize = WideTile::WIDTH as usize * COLOR_COMPONENTS;
let region_areas = region.areas();
let [s1, s2, s3, s4] = region_areas;
let dest_slices: &mut [&mut [u8; SLICE_WIDTH]; 4] = &mut [
(*s1).try_into().unwrap(),
(*s2).try_into().unwrap(),
(*s3).try_into().unwrap(),
(*s4).try_into().unwrap(),
];
for (idx, col) in buf.chunks_exact(CHUNK_LENGTH).enumerate() {
let dest_idx = idx * CHUNK_LENGTH / 4;
let casted: &[u32; 16] = cast_slice::<u8, u32>(col).try_into().unwrap();
let loaded = simd.load_interleaved_128_u32x16(casted).to_bytes();
let (loaded_lo, loaded_hi) = simd.split_u8x64(loaded);
let (loaded_1, loaded_2) = simd.split_u8x32(loaded_lo);
let (loaded_3, loaded_4) = simd.split_u8x32(loaded_hi);
loaded_1.store_slice(&mut dest_slices[0][dest_idx..][..16]);
loaded_2.store_slice(&mut dest_slices[1][dest_idx..][..16]);
loaded_3.store_slice(&mut dest_slices[2][dest_idx..][..16]);
loaded_4.store_slice(&mut dest_slices[3][dest_idx..][..16]);
}
}
/// The pendant to `pack_block`, but for unpacking.
///
/// See the [`unpack`] method for more information.
#[inline(always)]
fn unpack_block<S: Simd>(simd: S, region: &mut Region<'_>, buf: &mut [u8]) {
let buf: &mut [f32] = bytemuck::cast_slice_mut(&mut buf[..SCRATCH_BUF_SIZE]);
const CHUNK_LENGTH: usize = 16;
let region_areas = region.areas();
let [s1, s2, s3, s4] = region_areas;
for (idx, col) in buf.as_chunks_mut::<CHUNK_LENGTH>().0.iter_mut().enumerate() {
let src_idx = idx * CHUNK_LENGTH;
// Note: We experimented with using u32 vs. f32 for this, but it seems like for some reason
// f32 works better on M1, while on M4 they are the same. Probably worth doing more
// benchmarks on different systems.
let r0 = f32x4::from_bytes(u8x16::from_slice(simd, &s1[src_idx..][..16]));
let r1 = f32x4::from_bytes(u8x16::from_slice(simd, &s2[src_idx..][..16]));
let r2 = f32x4::from_bytes(u8x16::from_slice(simd, &s3[src_idx..][..16]));
let r3 = f32x4::from_bytes(u8x16::from_slice(simd, &s4[src_idx..][..16]));
let combined = simd.combine_f32x8(simd.combine_f32x4(r0, r1), simd.combine_f32x4(r2, r3));
simd.store_interleaved_128_f32x16(combined, col);
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
use alloc::vec::Vec;
use vello_common::fearless_simd::dispatch;
fn test_pack_unpack_roundtrip(
pack_fn: impl FnOnce(&mut Region<'_>, &[u8]),
unpack_fn: impl FnOnce(&mut Region<'_>, &mut [u8]),
) {
let width = WideTile::WIDTH;
let height = Tile::HEIGHT;
// Just some pseudo-random numbers.
let blend_buf = (0..SCRATCH_BUF_SIZE)
.map(|n| ((n * 7 + 13) % 256) as u8)
.collect::<Vec<_>>();
let mut region_data = vec![0_u8; width as usize * height as usize * COLOR_COMPONENTS];
let row_len = width as usize * COLOR_COMPONENTS;
let (r0, rest) = region_data.split_at_mut(row_len);
let (r1, rest) = rest.split_at_mut(row_len);
let (r2, r3) = rest.split_at_mut(row_len);
let mut region = Region::new([r0, r1, r2, r3], 0, 0, width, height);
// First pack.
pack_fn(&mut region, &blend_buf);
// Now reverse the process, unpacking into a new buffer.
let mut unpacked_buf = vec![0_u8; SCRATCH_BUF_SIZE];
unpack_fn(&mut region, &mut unpacked_buf);
assert_eq!(&blend_buf, &unpacked_buf);
}
#[test]
fn pack_unpack_roundtrip() {
test_pack_unpack_roundtrip(pack, unpack);
}
#[test]
fn pack_block_unpack_block_roundtrip() {
dispatch!(Level::try_detect().unwrap_or(Level::baseline()), simd => {
test_pack_unpack_roundtrip(
|region, buf| simd.vectorize(|| pack_block(simd, region, buf)),
|region, buf| simd.vectorize(|| unpack_block(simd, region, buf)),
);
});
}
}
+1127
View File
File diff suppressed because it is too large Load Diff
+154
View File
@@ -0,0 +1,154 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Layer management for filter effects rendering.
//!
//! This module provides infrastructure for managing persistent layer buffers
//! that can be rendered to independently and then composited together. This is
//! necessary for spatial filter effects that require access to a fully-rendered
//! layer (e.g., Gaussian blur).
//!
//! Layers are stored as Pixmap instances (row-major, u8 RGBA format), which
//! allows efficient filter operations without layout conversion overhead.
//! Each layer maintains its own bounding box in wide tile coordinates, allowing
//! efficient memory usage for layers that don't span the entire render target.
use crate::region::Region;
use hashbrown::HashMap;
use vello_common::coarse::WideTilesBbox;
use vello_common::pixmap::Pixmap;
/// Manages persistent layer storage for filter effects.
///
/// Each layer is allocated as a `Pixmap` with row-major RGBA8 format.
/// This eliminates conversion overhead for filter operations.
#[derive(Debug)]
pub struct LayerManager {
/// Map of layer ID to (Pixmap, wtile bounding box).
/// The Pixmap contains the layer's pixel data, and the Bbox defines which
/// wide tiles this layer occupies (in wide tile coordinates).
layers: HashMap<u32, (Pixmap, WideTilesBbox)>,
/// Next available layer ID for automatic allocation.
next_id: u32,
/// Reusable scratch buffer for filter operations that need temporary storage.
/// Examples include separable convolution passes (e.g., Gaussian blur) or
/// intermediate compositing results. This buffer is lazily allocated and
/// automatically resized as needed to avoid repeated allocations.
scratch_buffer: Option<Pixmap>,
}
impl Default for LayerManager {
/// Creates a new, empty layer manager with no allocated layers.
///
/// Layer IDs start from 1 (ID 0 is reserved for internal use).
fn default() -> Self {
Self {
layers: HashMap::new(),
next_id: 1,
scratch_buffer: None,
}
}
}
impl LayerManager {
/// Create a new, empty layer manager.
///
/// Layers can be registered on-demand using [`register_layer`](Self::register_layer).
pub fn new() -> Self {
Self::default()
}
/// Register a layer with a specific ID, wide tile bounding box, and pixel data.
///
/// The provided `Pixmap` should already contain the layer's pixel data (e.g., the
/// result of rendering a layer or applying a filter effect).
///
/// If a layer with this ID already exists, this method does nothing (no replacement).
///
/// # Parameters
/// - `layer_id`: A unique identifier for this layer. Layer ID 0 is reserved for internal use.
/// - `wtile_bbox`: The bounding box of the layer in wide tile coordinates. This defines
/// the region of the layer that contains valid pixel data, enabling efficient memory
/// usage for layers that don't span the entire render target.
/// - `pixmap`: The pixel data for this layer in row-major RGBA8 format.
pub fn register_layer(&mut self, layer_id: u32, wtile_bbox: WideTilesBbox, pixmap: Pixmap) {
self.layers.insert(layer_id, (pixmap, wtile_bbox));
if layer_id >= self.next_id {
self.next_id = layer_id + 1;
}
}
/// Get a mutable `Region` view into a specific wide tile within a layer.
///
/// This extracts a tile-sized region from the layer's Pixmap, allowing
/// rendering or compositing operations on individual tiles without copying
/// pixel data. The returned Region provides a mutable view into the underlying
/// layer buffer.
///
/// Returns [`None`] if:
/// - The layer doesn't exist
/// - The tile position is outside the layer's bounding box
/// - The tile extraction fails (e.g., invalid coordinates)
///
/// # Parameters
/// - `layer_id`: The ID of the layer to access
/// - `tile_x`: The x-coordinate of the wide tile in global wide tile coordinates
/// - `tile_y`: The y-coordinate of the wide tile in global wide tile coordinates
///
/// # Coordinate Systems
/// The input coordinates (`tile_x`, `tile_y`) are in *global* wide tile space,
/// but they are automatically converted to *local* coordinates relative to the
/// layer's bounding box before extracting the region.
pub fn layer_tile_region_mut(
&mut self,
layer_id: u32,
tile_x: u16,
tile_y: u16,
) -> Option<Region<'_>> {
let (pixmap, bbox) = self.layers.get_mut(&layer_id)?;
// Ensure the requested tile is within the layer's allocated bounds
if !bbox.contains(tile_x, tile_y) {
return None;
}
// Convert global tile coordinates to layer-local (bbox-relative) coordinates
let local_x = tile_x - bbox.x0();
let local_y = tile_y - bbox.y0();
// Extract a mutable Region view of the tile from the underlying pixmap
Region::from_pixmap_tile(pixmap, local_x, local_y)
}
/// Get or create a scratch buffer of at least the requested dimensions.
///
/// This buffer is reused across filter operations to minimize allocations.
/// If the existing buffer is large enough, it's reused; otherwise, a new
/// (larger) buffer is allocated.
///
/// # Parameters
/// - `width`: Minimum width in pixels
/// - `height`: Minimum height in pixels
///
/// # Returns
/// A mutable reference to a `Pixmap` of at least `width × height` pixels.
/// The actual buffer may be larger than requested if it was previously allocated
/// with larger dimensions.
pub fn get_scratch_buffer(&mut self, width: u16, height: u16) -> &mut Pixmap {
match &mut self.scratch_buffer {
None => {
// No buffer exists yet, allocate a new one
self.scratch_buffer = Some(Pixmap::new(width, height));
}
Some(buf) if buf.width() < width || buf.height() < height => {
// Existing buffer is too small, resize it
buf.resize(width, height);
}
// Buffer is already large enough, reuse it without reallocation
Some(_) => {}
}
self.scratch_buffer.as_mut().unwrap()
}
}
+186
View File
@@ -0,0 +1,186 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
// After you edit the crate's doc comment, run this command, then check README.md for any missing links
// cargo rdme --workspace-project=vello_cpu
//! Vello CPU is a 2D graphics rendering engine written in Rust, for devices with no or underpowered GPUs.
//!
//! We also develop [Vello](https://crates.io/crates/vello), which makes use of the GPU for 2D rendering and has higher performance than Vello CPU.
//! Vello CPU is being developed as part of work to address shortcomings in Vello.
//!
//! # Usage
//!
//! To use Vello CPU, you need to:
//!
//! - Create a [`RenderContext`][], a 2D drawing context for a fixed-size target area.
//! - For each object in your scene:
//! - Set how the object will be painted, using [`set_paint`][RenderContext::set_paint].
//! - Set the shape to be drawn for that object, using methods like [`fill_path`][RenderContext::fill_path],
//! [`stroke_path`][RenderContext::stroke_path], or [`glyph_run`][RenderContext::glyph_run].
//! - Render it to an image using [`RenderContext::render_to_pixmap`][].
//!
//! ```rust
//! use vello_cpu::{RenderContext, Resources, Pixmap, RenderMode};
//! use vello_cpu::{color::{palette::css, PremulRgba8}, kurbo::Rect};
//! let width = 10;
//! let height = 5;
//! let mut context = RenderContext::new(width, height);
//! let mut resources = Resources::new();
//! context.set_paint(css::MAGENTA);
//! context.fill_rect(&Rect::from_points((3., 1.), (7., 4.)));
//!
//! let mut target = Pixmap::new(width, height);
//! // While calling `flush` is only strictly necessary if you are rendering using
//! // multiple threads, it is recommended to always do this.
//! context.flush();
//! context.render_to_pixmap(&mut resources, &mut target);
//!
//! let expected_render = b"\
//! 0000000000\
//! 0001111000\
//! 0001111000\
//! 0001111000\
//! 0000000000";
//! let magenta = css::MAGENTA.premultiply().to_rgba8();
//! let transparent = PremulRgba8 {r: 0, g: 0, b: 0, a: 0};
//! let mut result = Vec::new();
//! for pixel in target.data() {
//! if *pixel == magenta {
//! result.push(b'1');
//! } else if *pixel == transparent {
//! result.push(b'0');
//! } else {
//! panic!("Got unexpected pixel value {pixel:?}");
//! }
//! }
//! assert_eq!(&result, expected_render);
//! ```
//!
//! Feel free to take a look at some further
//! [examples](https://github.com/linebender/vello/tree/main/sparse_strips/vello_cpu/examples)
//! to better understand how to interact with Vello CPU's API,
//!
//! # Features
//!
//! - `std` (enabled by default): Get floating point functions from the standard library
//! (likely using your target's libc).
//! - `libm`: Use floating point implementations from [libm][].
//! - `png`(enabled by default): Allow loading [`Pixmap`]s from PNG images.
//! Also required for rendering glyphs with an embedded PNG. Implies `std`.
//! - `multithreading`: Enable multi-threaded rendering. Implies `std`.
//! - `text` (enabled by default): Enables glyph rendering ([`glyph_run`][RenderContext::glyph_run]).
//! - `u8_pipeline` (enabled by default): Enable the u8 pipeline, for speed focused rendering using u8 math.
//! The `u8` pipeline will be used for [`OptimizeSpeed`][RenderMode::OptimizeSpeed], if both pipelines are enabled.
//! If you're using Vello CPU for application rendering, you should prefer this pipeline.
//! - `f32_pipeline`: Enable the `f32` pipeline, which is slower but has more accurate
//! results. This is espectially useful for rendering test snapshots.
//! The `f32` pipeline will be used for [`OptimizeQuality`][RenderMode::OptimizeQuality], if both pipelines are enabled.
//!
//! At least one of `std` and `libm` is required; `std` overrides `libm`.
//! At least one of `u8_pipeline` and `f32_pipeline` must be enabled.
//! You might choose to disable one of these pipelines if your application
//! won't use it, so as to reduce binary size.
//!
//! # Caveats
//!
//! Overall, Vello CPU is already very feature-rich and should be ready for
//! production use cases. The main caveat at the moment is that the API is
//! still likely to change and not stable yet. For example, we have
//! known plans to change the API around how image resources are used.
//!
//! Additionally, there are certain APIs that are still very much experimental,
//! including for example support for filters. This will be reflected in the
//! documentation of those APIs.
//!
//! Another caveat is that multi-threading with large thread counts
//! (more than 4) might give diminishing returns, especially when
//! making heavy use of layers and clip paths.
//!
//! # Performance
//!
//! Performance benchmarks can be found [here](https://laurenzv.github.io/vello_chart/),
//! As can be seen, Vello CPU achieves compelling performance on both,
//! aarch64 and x86 platforms. We also have SIMD optimizations for WASM SIMD,
//! meaning that you can expect good performance there as well.
//!
//! # Implementation
//!
//! If you want to gain a better understanding of Vello CPU and the
//! sparse strips paradigm, you can take a look at the [accompanying
//! master's thesis](https://ethz.ch/content/dam/ethz/special-interest/infk/inst-pls/plf-dam/documents/StudentProjects/MasterTheses/2025-Laurenz-Thesis.pdf)
//! that was written on the topic. Note that parts of the descriptions might
//! become outdated as the implementation changes, but it should give a good
//! overview nevertheless.
//!
//! <!-- We can't directly link to the libm crate built locally, because our feature is only a pass-through -->
//! [libm]: https://crates.io/crates/libm
// LINEBENDER LINT SET - lib.rs - v3
// See https://linebender.org/wiki/canonical-lints/
// These lints shouldn't apply to examples or tests.
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
// These lints shouldn't apply to examples.
#![warn(clippy::print_stdout, clippy::print_stderr)]
// Targeting e.g. 32-bit means structs containing usize can give false positives for 64-bit.
#![cfg_attr(target_pointer_width = "64", warn(clippy::trivially_copy_pass_by_ref))]
// END LINEBENDER LINT SET
#![cfg_attr(docsrs, feature(doc_cfg))]
#![forbid(unsafe_code)]
#![expect(
clippy::cast_possible_truncation,
reason = "We cast u16s to u8 in various places where we know for sure that it's < 256"
)]
#![no_std]
extern crate alloc;
extern crate core;
// Unused in release mode because it's only used directly in the `text_debug` module
// (or transitively in vello_common).
#[cfg(feature = "png")]
use png as _;
#[cfg(feature = "std")]
extern crate std;
#[cfg(all(not(feature = "u8_pipeline"), not(feature = "f32_pipeline")))]
compile_error!("vello_cpu must have at least one of the u8 or f32 pipelines enabled");
mod render;
mod dispatch;
mod filter;
#[cfg(feature = "text")]
mod text;
#[cfg(all(feature = "text", feature = "std", debug_assertions))]
mod text_debug;
mod util;
#[doc(hidden)]
pub mod fine;
#[doc(hidden)]
pub mod layer_manager;
#[doc(hidden)]
pub mod region;
pub use render::{RenderContext, RenderSettings, Resources};
// Note: The first one is not something that should be
// exposed, but is currently needed by vello_sparse_tests.
#[cfg(feature = "text")]
pub use glifo::Glyph;
#[cfg(feature = "text")]
pub use text::{CpuGlyphRunBackend, GlyphRunBuilder};
pub use vello_common::fearless_simd::Level;
pub use vello_common::mask::Mask;
pub use vello_common::paint::{Image, ImageSource, Paint, PaintType};
pub use vello_common::pixmap::Pixmap;
pub use vello_common::{color, kurbo, peniko};
/// The selected rendering mode.
/// For using [`RenderMode::OptimizeQuality`] you also need to enable `f32_pipeline` feature.
#[derive(Copy, Clone, Debug, Default)]
pub enum RenderMode {
/// Optimize speed (by performing calculations with u8/16).
#[default]
OptimizeSpeed,
/// Optimize quality (by performing calculations with f32).
OptimizeQuality,
}
+243
View File
@@ -0,0 +1,243 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Splitting a single mutable buffer into regions that can be accessed concurrently.
use crate::fine::COLOR_COMPONENTS;
use alloc::vec::Vec;
use vello_common::coarse::WideTile;
use vello_common::pixmap::Pixmap;
use vello_common::tile::Tile;
#[derive(Debug)]
pub struct Regions<'a> {
regions: Vec<Region<'a>>,
}
impl<'a> Regions<'a> {
/// Creates regions from a buffer where the buffer dimensions match the render dimensions.
pub fn new(width: u16, height: u16, buffer: &'a mut [u8]) -> Self {
Self::new_at_offset(width, height, 0, 0, width, height, buffer)
}
/// Creates regions from a buffer at a specific offset.
///
/// This is used for rendering to a sub-region of a larger buffer. The regions
/// cover the area of size (`width` × `height`) placed at pixel offset
/// (`dst_x`, `dst_y`) in the destination buffer.
///
/// # Arguments
/// * `width` - Width of the content being rendered
/// * `height` - Height of the content being rendered
/// * `dst_x` - X offset in the destination buffer
/// * `dst_y` - Y offset in the destination buffer
/// * `dst_buffer_width` - Total width of the destination buffer
/// * `dst_buffer_height` - Total height of the destination buffer
/// * `buffer` - The destination buffer (RGBA, 4 bytes per pixel)
pub fn new_at_offset(
width: u16,
height: u16,
dst_x: u16,
dst_y: u16,
dst_buffer_width: u16,
dst_buffer_height: u16,
mut buffer: &'a mut [u8],
) -> Self {
// Calculate effective render area (clamped to destination bounds)
let effective_width = width.min(dst_buffer_width.saturating_sub(dst_x)) as usize;
let effective_height = height.min(dst_buffer_height.saturating_sub(dst_y)) as usize;
if effective_width == 0 || effective_height == 0 {
return Self {
regions: Vec::new(),
};
}
let width_regions = effective_width.div_ceil(WideTile::WIDTH as usize);
let height_regions = effective_height.div_ceil(Tile::HEIGHT as usize);
let mut regions = Vec::with_capacity(width_regions * height_regions);
let row_stride = dst_buffer_width as usize * COLOR_COMPONENTS;
let render_row_bytes = effective_width * COLOR_COMPONENTS;
// Calculate starting offset in the buffer
let start_offset = (dst_y as usize * row_stride) + (dst_x as usize * COLOR_COMPONENTS);
buffer = &mut buffer[start_offset..];
let mut next_lines: [&'a mut [u8]; Tile::HEIGHT as usize] =
[&mut [], &mut [], &mut [], &mut []];
for y in 0..height_regions {
let base_y = y * Tile::HEIGHT as usize;
let region_height = (Tile::HEIGHT as usize).min(effective_height - base_y);
// Extract Tile::HEIGHT rows from the buffer
// Each row is at row_stride intervals
for line in next_lines.iter_mut().take(region_height) {
// Take only the render area portion of this row
let (render_portion, rest) = buffer.split_at_mut(render_row_bytes);
*line = render_portion;
// Skip the remainder of this buffer row to get to the next row.
// On the last row (when clipped to pixmap edge), there may be less
// data remaining, so cap the skip to what's available.
let skip = (row_stride - render_row_bytes).min(rest.len());
buffer = &mut rest[skip..];
}
// Split each row horizontally into tile-width chunks
for x in 0..width_regions {
let mut areas: [&mut [u8]; Tile::HEIGHT as usize] =
[&mut [], &mut [], &mut [], &mut []];
let base_x = x * WideTile::WIDTH as usize;
let region_width_bytes =
((WideTile::WIDTH as usize).min(effective_width - base_x)) * COLOR_COMPONENTS;
for h in 0..region_height {
let next = core::mem::take(&mut next_lines[h]);
let (head, tail) = next.split_at_mut(region_width_bytes);
areas[h] = head;
next_lines[h] = tail;
}
regions.push(Region::new(
areas,
u16::try_from(x).unwrap(),
u16::try_from(y).unwrap(),
(region_width_bytes / COLOR_COMPONENTS) as u16,
region_height as u16,
));
}
}
Self { regions }
}
/// Apply the given function to each region. The functions will be applied
/// in parallel in the current threadpool.
#[cfg(feature = "multithreading")]
pub fn update_regions_par(&mut self, func: impl Fn(&mut Region<'_>) + Send + Sync) {
use rayon::iter::ParallelIterator;
use rayon::prelude::IntoParallelRefMutIterator;
self.regions.par_iter_mut().for_each(func);
}
/// Apply the given function to each region.
pub fn update_regions(&mut self, func: impl FnMut(&mut Region<'_>)) {
self.regions.iter_mut().for_each(func);
}
}
/// A rectangular region containing the pixels from one wide tile.
///
/// For wide tiles at the right/bottom edge, it might contain less pixels
/// than the actual wide tile, if the pixmap width/height isn't a multiple of the
/// tile width/height.
#[derive(Default, Debug)]
pub struct Region<'a> {
/// The x coordinate of the wide tile this region covers.
pub(crate) x: u16,
/// The y coordinate of the wide tile this region covers.
pub(crate) y: u16,
pub width: u16,
pub height: u16,
areas: [&'a mut [u8]; Tile::HEIGHT as usize],
}
impl<'a> Region<'a> {
pub(crate) fn new(
areas: [&'a mut [u8]; Tile::HEIGHT as usize],
x: u16,
y: u16,
width: u16,
height: u16,
) -> Self {
Self {
areas,
x,
y,
width,
height,
}
}
/// Extracts a `Region` from a pixmap at the specified tile coordinates.
///
/// The region corresponds to a wide tile area (`WideTile::WIDTH` × `Tile::HEIGHT` pixels),
/// starting at pixel coordinates `(tile_x * WideTile::WIDTH, tile_y * Tile::HEIGHT)`.
/// Regions at the right or bottom edges may be smaller if they extend beyond the pixmap bounds.
///
/// Returns `None` if the tile coordinates are completely outside the pixmap bounds.
///
/// # Arguments
/// * `pixmap` - The pixmap to extract from
/// * `tile_x` - Tile column index (in tile units, not pixels)
/// * `tile_y` - Tile row index (in tile units, not pixels)
pub(crate) fn from_pixmap_tile(
pixmap: &'a mut Pixmap,
tile_x: u16,
tile_y: u16,
) -> Option<Self> {
let pixmap_width = pixmap.width();
let pixmap_height = pixmap.height();
// Calculate pixel coordinates for this tile
let base_x = tile_x * WideTile::WIDTH;
let base_y = tile_y * Tile::HEIGHT;
// Check bounds
if base_x >= pixmap_width || base_y >= pixmap_height {
return None;
}
// Calculate actual region dimensions (might be smaller at edges)
let region_width = WideTile::WIDTH.min(pixmap_width - base_x);
let region_height = Tile::HEIGHT.min(pixmap_height - base_y);
// Get mutable access to the pixmap's buffer
let buffer = pixmap.data_as_u8_slice_mut();
// Split buffer into row slices for this tile
let row_stride = pixmap_width as usize * COLOR_COMPONENTS;
let start_offset = (base_y as usize * row_stride) + (base_x as usize * COLOR_COMPONENTS);
let region_width_bytes = region_width as usize * COLOR_COMPONENTS;
// Skip to the start of our tile region
let tile_buffer = &mut buffer[start_offset..];
// Extract individual row slices using safe split operations
let mut areas: [&mut [u8]; Tile::HEIGHT as usize] = [&mut [], &mut [], &mut [], &mut []];
// Use split_at_mut to safely extract each row
let mut remaining = tile_buffer;
for (i, area) in areas.iter_mut().take(region_height as usize).enumerate() {
if i > 0 {
// Skip rows we've already processed (advance by stride - region_width_bytes)
let skip = row_stride - region_width_bytes;
remaining = &mut remaining[skip..];
}
let (row, rest) = remaining.split_at_mut(region_width_bytes.min(remaining.len()));
*area = row;
remaining = rest;
}
Some(Self::new(
areas,
tile_x,
tile_y,
region_width,
region_height,
))
}
pub(crate) fn row_mut(&mut self, y: u16) -> &mut [u8] {
self.areas[usize::from(y)]
}
pub fn areas(&mut self) -> &mut [&'a mut [u8]; Tile::HEIGHT as usize] {
&mut self.areas
}
}
+917
View File
@@ -0,0 +1,917 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Basic render operations.
use crate::RenderMode;
use crate::dispatch::Dispatcher;
#[cfg(feature = "text")]
use crate::text::{GlyphAtlasResources, GlyphRunBuilder};
#[cfg(feature = "text")]
use glifo::GlyphPrepCache;
#[cfg(feature = "multithreading")]
use crate::dispatch::multi_threaded::MultiThreadedDispatcher;
use crate::dispatch::single_threaded::SingleThreadedDispatcher;
use crate::kurbo::{PathEl, Point};
use alloc::boxed::Box;
use alloc::sync::Arc;
use alloc::vec;
use alloc::vec::Vec;
use hashbrown::HashMap;
use vello_common::blurred_rounded_rect::BlurredRoundedRectangle;
use vello_common::encode::{EncodeExt, EncodedPaint};
use vello_common::fearless_simd::Level;
use vello_common::filter_effects::Filter;
use vello_common::kurbo::{Affine, BezPath, Rect, Stroke};
use vello_common::mask::Mask;
use vello_common::paint::{ImageId, ImageResolver, Paint, PaintType, Tint};
use vello_common::peniko::color::palette::css::BLACK;
use vello_common::peniko::{BlendMode, Fill};
use vello_common::pixmap::Pixmap;
use vello_common::render_state::RenderState;
use vello_common::util::is_axis_aligned;
#[cfg(feature = "text")]
pub(crate) const DEFAULT_GLYPH_ATLAS_SIZE: u16 = 4096;
// Why do we need this? The reason is that the way uploaded images work in Vello Hybrid
// is different from how they work in Vello CPU.
//
// In Vello Hybrid, all images, regardless of whether they are user-uploaded
// images or cached glyphs, are stored in an image atlas at a certain location. An image ID then
// uniquely resolves to an atlas page index + a location on that page. Whenever we want to
// cache a new glyph, we simply allocate a location in the image atlas and then return the image
// ID associated with that location.
//
// On Vello CPU, it works differently: An image ID is associated with a complete pixmap.
// If a user uploads an image, instead of blitting it into a bigger image atlas, we just
// store the user-provided pixmap and associate an image ID with the whole pixmap. However,
// for glyph caching to work we need the same semantics as in Vello Hybrid. Therefore, we
// use a marker to determine whether an image ID refers to a normal uploaded image or a cached
// glyph and apply special handling based on that.
//
// All IDs < than this value are reserved for normal images, all IDs >= this value are
// reserved for atlas pages.
pub(crate) const ATLAS_IMAGE_ID_BASE: u32 = u32::MAX / 2;
/// Persistent resources required by Vello CPU for rendering.
#[derive(Debug, Default)]
pub struct Resources {
pub(crate) image_registry: ImageRegistry,
#[cfg(feature = "text")]
pub(crate) glyph_prep_cache: GlyphPrepCache,
// Will be initialized lazily on first use.
#[cfg(feature = "text")]
pub(crate) glyph_resources: Option<GlyphAtlasResources>,
}
impl Resources {
/// Create a new set of renderer resources.
pub fn new() -> Self {
Self::default()
}
pub(crate) fn before_render(&mut self) {
#[cfg(feature = "text")]
self.prepare_glyph_cache();
}
pub(crate) fn after_render(&mut self) {
#[cfg(feature = "text")]
self.maintain_glyph_cache();
}
}
/// A render context for CPU-based 2D graphics rendering.
///
/// This is the main entry point for drawing operations. It maintains the current
/// rendering state (transforms, paint, stroke, etc.) and dispatches drawing commands
/// to the underlying rasterization engine.
#[derive(Debug)]
pub struct RenderContext {
/// Width of the render target in pixels.
pub(crate) width: u16,
/// Height of the render target in pixels.
pub(crate) height: u16,
/// The current rendering state.
pub(crate) state: RenderState,
/// The current mask in place.
pub(crate) mask: Option<Mask>,
/// Temporary path buffer to avoid repeated allocations.
pub(crate) temp_path: BezPath,
/// Optional threshold for aliasing.
pub(crate) aliasing_threshold: Option<u8>,
pub(crate) encoded_paints: Vec<EncodedPaint>,
pub(crate) filter: Option<Filter>,
#[cfg_attr(
not(feature = "text"),
allow(dead_code, reason = "used when the `text` feature is enabled")
)]
pub(crate) render_settings: RenderSettings,
dispatcher: Box<dyn Dispatcher>,
}
/// Settings to apply to the render context.
#[derive(Copy, Clone, Debug)]
pub struct RenderSettings {
/// The SIMD level that should be used for rendering operations.
pub level: Level,
/// The number of worker threads that should be used for rendering. Only has an effect
/// if the `multithreading` feature is active.
pub num_threads: u16,
/// Whether to prioritize speed or quality when rendering.
///
/// For most cases (especially for real-time rendering), it is highly recommended to set
/// this to `OptimizeSpeed`. If accuracy is a more significant concern (for example for visual
/// regression testing), then you can set this to `OptimizeQuality`.
///
/// Currently, the only difference this makes is that when choosing `OptimizeSpeed`, rasterization
/// will happen using u8/u16, while `OptimizeQuality` will use a f32-based pipeline.
pub render_mode: RenderMode,
}
impl Default for RenderSettings {
fn default() -> Self {
Self {
level: Level::try_detect().unwrap_or(Level::baseline()),
#[cfg(feature = "multithreading")]
num_threads: (std::thread::available_parallelism()
.unwrap()
.get()
.saturating_sub(1) as u16)
.min(8),
#[cfg(not(feature = "multithreading"))]
num_threads: 0,
render_mode: RenderMode::OptimizeSpeed,
}
}
}
impl RenderContext {
/// Create a new render context with the given width and height in pixels.
pub fn new(width: u16, height: u16) -> Self {
Self::new_with(width, height, RenderSettings::default())
}
/// Create a new render context with specific settings.
pub fn new_with(width: u16, height: u16, settings: RenderSettings) -> Self {
#[cfg(feature = "multithreading")]
let dispatcher: Box<dyn Dispatcher> = if settings.num_threads == 0 {
Box::new(SingleThreadedDispatcher::new(width, height, settings.level))
} else {
Box::new(MultiThreadedDispatcher::new(
width,
height,
settings.num_threads,
settings.level,
))
};
#[cfg(not(feature = "multithreading"))]
let dispatcher: Box<dyn Dispatcher> =
{ Box::new(SingleThreadedDispatcher::new(width, height, settings.level)) };
let encoded_paints = vec![];
let temp_path = BezPath::new();
let aliasing_threshold = None;
Self {
width,
height,
dispatcher,
state: RenderState::default(),
aliasing_threshold,
render_settings: settings,
mask: None,
temp_path,
encoded_paints,
filter: None,
}
}
fn encode_current_paint(&mut self) -> Paint {
match self.state.paint.clone() {
PaintType::Solid(s) => s.into(),
PaintType::Gradient(g) => {
// TODO: Add caching?
g.encode_into(
&mut self.encoded_paints,
self.state.transform * self.state.paint_transform,
None,
)
}
PaintType::Image(i) => i.encode_into(
&mut self.encoded_paints,
self.state.transform * self.state.paint_transform,
self.state.tint,
),
}
}
/// Fill a path.
pub fn fill_path(&mut self, path: &BezPath) {
self.with_optional_filter(|ctx| {
let paint = ctx.encode_current_paint();
ctx.dispatcher.fill_path(
path,
ctx.state.fill_rule,
ctx.state.transform,
paint,
ctx.state.blend_mode,
ctx.aliasing_threshold,
ctx.mask.clone(),
&ctx.encoded_paints,
);
});
}
/// Stroke a path.
pub fn stroke_path(&mut self, path: &BezPath) {
self.with_optional_filter(|ctx| {
let paint = ctx.encode_current_paint();
ctx.dispatcher.stroke_path(
path,
&ctx.state.stroke,
ctx.state.transform,
paint,
ctx.state.blend_mode,
ctx.aliasing_threshold,
ctx.mask.clone(),
&ctx.encoded_paints,
);
});
}
/// Fill a rectangle.
pub fn fill_rect(&mut self, rect: &Rect) {
self.with_optional_filter(|ctx| {
let paint = ctx.encode_current_paint();
// Fast path: Use optimized rect filling if we have no skew in the path transform
// and anti-aliasing is enabled.
// TODO: Maybe also support no anti-aliasing in the fast path
if is_axis_aligned(&ctx.state.transform) && ctx.aliasing_threshold.is_none() {
// Transform the rect to screen coordinates.
let transformed_rect = ctx.state.transform.transform_rect_bbox(*rect);
ctx.dispatcher.fill_rect_fast(
&transformed_rect,
paint,
ctx.state.blend_mode,
ctx.mask.clone(),
&ctx.encoded_paints,
);
} else {
// Fall back to path-based rendering for rotated/skewed transforms.
ctx.rect_to_temp_path(rect);
ctx.dispatcher.fill_path(
&ctx.temp_path,
ctx.state.fill_rule,
ctx.state.transform,
paint,
ctx.state.blend_mode,
ctx.aliasing_threshold,
ctx.mask.clone(),
&ctx.encoded_paints,
);
}
});
}
/// Stroke a rectangle.
pub fn stroke_rect(&mut self, rect: &Rect) {
self.with_optional_filter(|ctx| {
ctx.rect_to_temp_path(rect);
let paint = ctx.encode_current_paint();
ctx.dispatcher.stroke_path(
&ctx.temp_path,
&ctx.state.stroke,
ctx.state.transform,
paint,
ctx.state.blend_mode,
ctx.aliasing_threshold,
ctx.mask.clone(),
&ctx.encoded_paints,
);
});
}
fn rect_to_temp_path(&mut self, rect: &Rect) {
self.temp_path.truncate(0);
self.temp_path
.push(PathEl::MoveTo(Point::new(rect.x0, rect.y0)));
self.temp_path
.push(PathEl::LineTo(Point::new(rect.x1, rect.y0)));
self.temp_path
.push(PathEl::LineTo(Point::new(rect.x1, rect.y1)));
self.temp_path
.push(PathEl::LineTo(Point::new(rect.x0, rect.y1)));
self.temp_path.push(PathEl::ClosePath);
}
/// Fill a blurred rectangle with the given corner radius and standard deviation.
///
/// Note that this only works properly if the current paint is set to a solid color.
/// If not, it will fall back to using black as the fill color.
pub fn fill_blurred_rounded_rect(&mut self, rect: &Rect, radius: f32, std_dev: f32) {
let rect = rect.abs();
let color = match self.state.paint {
PaintType::Solid(s) => s,
// Fallback to black when attempting to blur a rectangle with an image/gradient paint
_ => BLACK,
};
let blurred_rect = BlurredRoundedRectangle {
rect,
color,
radius,
std_dev,
};
// The actual rectangle we paint needs to be larger so that the blurring effect
// is not cut off.
// The impulse response of a gaussian filter is infinite.
// For performance reason we cut off the filter at some extent where the response is close to zero.
let kernel_size = 2.5 * std_dev;
let inflated_rect = rect.inflate(f64::from(kernel_size), f64::from(kernel_size));
let transform = self.state.transform * self.state.paint_transform;
self.rect_to_temp_path(&inflated_rect);
let paint = blurred_rect.encode_into(&mut self.encoded_paints, transform, None);
self.dispatcher.fill_path(
&self.temp_path,
Fill::NonZero,
self.state.transform,
paint,
self.state.blend_mode,
self.aliasing_threshold,
self.mask.clone(),
&self.encoded_paints,
);
}
/// Creates a builder for drawing a run of glyphs that have the same attributes.
#[cfg(feature = "text")]
pub fn glyph_run<'a>(
&'a mut self,
resources: &'a mut Resources,
font: &crate::peniko::FontData,
) -> GlyphRunBuilder<'a> {
glifo::GlyphRunBuilder::new(
font.clone(),
self.state.transform,
crate::text::CpuGlyphRunBackend {
ctx: self,
resources,
atlas_cache_enabled: false,
},
)
}
/// Push a new layer with the given properties.
///
/// Note that the mask, if provided, needs to have the same size as the render context. Otherwise,
/// it will be ignored. In addition to that, the mask will not be affected by the current
/// transformation matrix in place.
pub fn push_layer(
&mut self,
clip_path: Option<&BezPath>,
blend_mode: Option<BlendMode>,
opacity: Option<f32>,
mask: Option<Mask>,
filter: Option<Filter>,
) {
let mask = mask.and_then(|m| {
if m.width() != self.width || m.height() != self.height {
None
} else {
Some(m)
}
});
let blend_mode = blend_mode.unwrap_or_default();
let opacity = opacity.unwrap_or(1.0);
self.dispatcher.push_layer(
clip_path,
self.state.fill_rule,
self.state.transform,
blend_mode,
opacity,
self.aliasing_threshold,
mask,
filter,
);
}
/// Push a new clip layer.
///
/// See the explanation in the [clipping](https://github.com/linebender/vello/tree/main/sparse_strips/vello_cpu/examples)
/// example for how this method differs from `push_clip_path`.
pub fn push_clip_layer(&mut self, path: &BezPath) {
self.push_layer(Some(path), None, None, None, None);
}
/// Push a new blend layer.
pub fn push_blend_layer(&mut self, blend_mode: BlendMode) {
self.push_layer(None, Some(blend_mode), None, None, None);
}
/// Push a new opacity layer.
pub fn push_opacity_layer(&mut self, opacity: f32) {
self.push_layer(None, None, Some(opacity), None, None);
}
/// Push a new mask layer. The mask needs to have the same dimensions as the
/// render context. The mask will not be affected by the current transform
/// in place.
///
/// See the explanation in the [masking](https://github.com/linebender/vello/tree/main/sparse_strips/masking/examples)
/// example for how this method differs from `set_mask`.
pub fn push_mask_layer(&mut self, mask: Mask) {
self.push_layer(None, None, None, Some(mask), None);
}
/// Push a filter layer that affects all subsequent drawing operations.
///
/// WARNING: Note that filters are currently incomplete and experimental. In
/// particular, they will lead to a panic when used in combination with
/// multi-threaded rendering.
pub fn push_filter_layer(&mut self, filter: Filter) {
self.push_layer(None, None, None, None, Some(filter));
}
/// Set the aliasing threshold.
///
/// If set to `None` (which is the recommended option in nearly all cases),
/// anti-aliasing will be applied.
///
/// If instead set to some value, then a pixel will be fully painted if
/// the coverage is bigger than the threshold (between 0 and 255), otherwise
/// it will not be painted at all.
///
/// Note that there is no performance benefit to disabling anti-aliasing and
/// this functionality is simply provided for compatibility.
pub fn set_aliasing_threshold(&mut self, aliasing_threshold: Option<u8>) {
self.aliasing_threshold = aliasing_threshold;
}
/// Pop the last-pushed layer.
pub fn pop_layer(&mut self) {
self.dispatcher.pop_layer();
}
/// Set the current stroke.
pub fn set_stroke(&mut self, stroke: Stroke) {
self.state.stroke = stroke;
}
/// Get the current stroke.
pub fn stroke(&self) -> &Stroke {
&self.state.stroke
}
/// Get a mutable reference to the current stroke.
#[cfg(feature = "text")]
pub(crate) fn stroke_mut(&mut self) -> &mut Stroke {
&mut self.state.stroke
}
/// Set the current paint.
///
/// If the paint is an image with `ImageSource::OpaqueId`, it will be
/// resolved to the corresponding pixmap at rasterization time.
/// Make sure to register images with [`Resources::register_image`] first.
pub fn set_paint(&mut self, paint: impl Into<PaintType>) {
self.state.paint = paint.into();
}
/// Get the current paint.
pub fn paint(&self) -> &PaintType {
&self.state.paint
}
/// Set the tint for subsequent image paint operations.
pub fn set_tint(&mut self, tint: Option<Tint>) {
self.state.tint = tint;
}
/// Clear the tint, so subsequent image paints are drawn without tinting.
pub fn reset_tint(&mut self) {
self.state.tint = None;
}
/// Set the blend mode that should be used when drawing objects.
pub fn set_blend_mode(&mut self, blend_mode: BlendMode) {
self.state.blend_mode = blend_mode;
}
/// Get the currently active blend mode.
pub fn blend_mode(&self) -> BlendMode {
self.state.blend_mode
}
/// Set the current paint transform.
///
/// The paint transform is applied to the paint after the transform of the geometry the paint
/// is drawn in, i.e., the paint transform is applied after the global transform. This allows
/// transforming the paint independently from the drawn geometry.
pub fn set_paint_transform(&mut self, paint_transform: Affine) {
self.state.paint_transform = paint_transform;
}
/// Get the current paint transform.
pub fn paint_transform(&self) -> &Affine {
&self.state.paint_transform
}
/// Reset the current paint transform.
pub fn reset_paint_transform(&mut self) {
self.state.paint_transform = Affine::IDENTITY;
}
/// Set the current fill rule.
pub fn set_fill_rule(&mut self, fill_rule: Fill) {
self.state.fill_rule = fill_rule;
}
/// Set the mask to use for path-painting operations. The mask needs to
/// have the same dimensions as the render context. The mask will not be
/// affected by the current transform in place.
///
/// See the explanation in the [masking](https://github.com/linebender/vello/tree/main/sparse_strips/masking/examples)
/// example for how this method differs from `push_mask_layer`.
pub fn set_mask(&mut self, mask: Mask) {
self.mask = Some(mask);
}
/// Reset the mask that is used for path-painting operations.
pub fn reset_mask(&mut self) {
self.mask = None;
}
/// Get the current fill rule.
pub fn fill_rule(&self) -> &Fill {
&self.state.fill_rule
}
/// Set the current transform.
pub fn set_transform(&mut self, transform: Affine) {
self.state.transform = transform;
}
/// Get the current transform.
pub fn transform(&self) -> &Affine {
&self.state.transform
}
/// Reset the current transform.
pub fn reset_transform(&mut self) {
self.state.transform = Affine::IDENTITY;
}
/// Apply filter to the current paint (affects next drawn elements).
///
/// This sets a filter that will be applied to the next drawn element.
/// To apply a filter to multiple elements, use `push_filter_layer` instead.
pub fn set_filter_effect(&mut self, filter: Filter) {
self.filter = Some(filter);
}
/// Reset the current filter effect.
pub fn reset_filter_effect(&mut self) {
self.filter = None;
}
/// Reset the render context.
pub fn reset(&mut self) {
self.dispatcher.reset();
self.encoded_paints.clear();
self.mask = None;
self.state.reset();
}
/// Push a new clip path to the clip stack.
///
/// See the explanation in the [clipping](https://github.com/linebender/vello/tree/main/sparse_strips/vello_cpu/examples)
/// example for how this method differs from `push_clip_layer`.
pub fn push_clip_path(&mut self, path: &BezPath) {
self.dispatcher.push_clip_path(
path,
self.state.fill_rule,
self.state.transform,
self.aliasing_threshold,
);
}
/// Pop a clip path from the clip stack.
///
/// Note that unlike `push_clip_layer`, it is permissible to have pending
/// pushed clip paths before finishing the rendering operation.
pub fn pop_clip_path(&mut self) {
self.dispatcher.pop_clip_path();
}
/// Flush any pending operations.
///
/// This is a no-op when using the single-threaded render mode, and can be ignored.
/// For multi-threaded rendering, you _have_ to call this before rasterizing, otherwise
/// the program will panic.
pub fn flush(&mut self) {
self.dispatcher.flush(&self.encoded_paints);
}
/// Render the current context into a buffer.
/// The buffer is expected to be in premultiplied RGBA8 format with length `width * height * 4`
pub fn render_to_buffer(
&self,
resources: &mut Resources,
buffer: &mut [u8],
width: u16,
height: u16,
render_mode: RenderMode,
) {
// TODO: Maybe we should move those checks into the dispatcher.
let wide = self.dispatcher.wide();
assert!(!wide.has_layers(), "some layers haven't been popped yet");
assert_eq!(
buffer.len(),
(width as usize) * (height as usize) * 4,
"provided width ({}) and height ({}) do not match buffer size ({})",
width,
height,
buffer.len(),
);
resources.before_render();
self.dispatcher.rasterize(
buffer,
render_mode,
width,
height,
&self.encoded_paints,
&resources.image_registry,
);
// TODO: We need to figure something out here API-wise. At the moment, the user can
// theoretically rasterize the same `RenderContext` multiple times without resetting in-between.
// However, if glyph caching is enabled, this method call could now evict that were previously
// assumed to exist in `RenderContext`, meaning that if the user rasterizes the same `RenderContext`
// again without resetting it, some of the cached glyphs might be stale and not exist anymore.
resources.after_render();
}
/// Render the current context into a pixmap.
pub fn render_to_pixmap(&self, resources: &mut Resources, pixmap: &mut Pixmap) {
let width = pixmap.width();
let height = pixmap.height();
self.render_to_buffer(
resources,
pixmap.data_as_u8_slice_mut(),
width,
height,
self.render_settings.render_mode,
);
}
/// Composite the current context into a region of a pixmap.
///
/// The context's content (sized `self.width × self.height`) is composited
/// directly to the destination pixmap starting at `(dst_x, dst_y)`.
/// If the region extends beyond the pixmap bounds, it is clipped.
///
/// Unlike [`render_to_pixmap`](Self::render_to_pixmap), this method composites on top of
/// existing pixmap content rather than clearing it first, allowing multiple
/// renders to accumulate.
///
/// This is useful for rendering individual elements (like glyphs) into
/// a spritesheet at specific coordinates.
///
/// # Panics
///
/// This method is only supported with the single-threaded dispatcher and will
/// **panic** if called on a `RenderContext` using the multi-threaded dispatcher.
pub fn composite_to_pixmap_at_offset(
&self,
resources: &Resources,
pixmap: &mut Pixmap,
dst_x: u16,
dst_y: u16,
) {
let dst_buffer_width = pixmap.width();
let dst_buffer_height = pixmap.height();
self.dispatcher.composite_at_offset(
pixmap.data_as_u8_slice_mut(),
self.width,
self.height,
dst_x,
dst_y,
dst_buffer_width,
dst_buffer_height,
self.render_settings.render_mode,
&self.encoded_paints,
&resources.image_registry,
);
}
/// Return the width of the pixmap.
pub fn width(&self) -> u16 {
self.width
}
/// Return the height of the pixmap.
pub fn height(&self) -> u16 {
self.height
}
/// Return the render settings used by the `RenderContext`.
pub fn render_settings(&self) -> &RenderSettings {
&self.render_settings
}
/// Execute a drawing operation, optionally wrapping it in a filter layer.
fn with_optional_filter<F>(&mut self, mut f: F)
where
F: FnMut(&mut Self),
{
if let Some(filter) = self.filter.clone() {
self.push_filter_layer(filter);
f(self);
self.pop_layer();
} else {
f(self);
}
}
/// Take current rendering state and reset the existing state to its default.
pub fn take_current_state(&mut self) -> RenderState {
core::mem::take(&mut self.state)
}
/// Save a copy of the current rendering state.
pub fn save_current_state(&mut self) -> RenderState {
self.state.clone()
}
/// Restore rendering state.
pub fn restore_state(&mut self, state: RenderState) {
self.state = state;
}
}
/// Image registry implementation.
impl Resources {
/// Register a pixmap in the image registry and return its [`ImageId`].
pub fn register_image(&mut self, pixmap: Arc<Pixmap>) -> ImageId {
self.image_registry.register(pixmap)
}
/// Remove an image from the registry.
pub fn destroy_image(&mut self, id: ImageId) -> bool {
self.image_registry.destroy(id)
}
/// Resolve an `ImageId` to its pixmap data.
pub fn resolve_image(&self, id: ImageId) -> Option<Arc<Pixmap>> {
self.image_registry.resolve(id)
}
/// Clear the image registry.
pub fn clear_images(&mut self) {
self.image_registry.clear();
}
}
/// Registry that maps opaque [`ImageId`]s to [`Pixmap`] data.
///
/// Used by [`RenderContext`] to resolve `ImageSource::OpaqueId` at rasterization time.
#[derive(Debug, Default)]
pub(crate) struct ImageRegistry {
images: HashMap<u32, Arc<Pixmap>>,
next_id: u32,
}
impl ImageRegistry {
fn register(&mut self, pixmap: Arc<Pixmap>) -> ImageId {
let id = self.next_id;
assert!(
id < ATLAS_IMAGE_ID_BASE,
"image registry exhausted non-atlas image IDs"
);
self.next_id += 1;
self.images.insert(id, pixmap);
ImageId::new(id)
}
#[cfg(feature = "text")]
pub(crate) fn register_atlas_page(&mut self, page_index: u32, pixmap: Arc<Pixmap>) {
self.images.insert(
ImageId::new(ATLAS_IMAGE_ID_BASE + page_index).as_u32(),
pixmap,
);
}
pub(crate) fn destroy(&mut self, id: ImageId) -> bool {
self.images.remove(&id.as_u32()).is_some()
}
#[cfg(feature = "text")]
pub(crate) fn destroy_atlas_page(&mut self, page_index: u32) -> bool {
self.destroy(ImageId::new(ATLAS_IMAGE_ID_BASE + page_index))
}
fn resolve(&self, id: ImageId) -> Option<Arc<Pixmap>> {
self.images.get(&id.as_u32()).cloned()
}
fn clear(&mut self) {
self.images.clear();
self.next_id = 0;
}
}
impl ImageResolver for ImageRegistry {
fn resolve(&self, id: ImageId) -> Option<Arc<Pixmap>> {
self.images.get(&id.as_u32()).cloned()
}
}
#[cfg(test)]
mod tests {
use crate::RenderContext;
#[cfg(feature = "text")]
use crate::peniko::{Blob, FontData};
#[cfg(feature = "text")]
use alloc::sync::Arc;
#[cfg(feature = "text")]
use glifo::Glyph;
use vello_common::kurbo::{Rect, Shape};
use vello_common::tile::Tile;
#[test]
fn clip_overflow() {
let mut ctx = RenderContext::new(100, 100);
for _ in 0..(usize::from(u16::MAX) + 1).div_ceil(usize::from(Tile::HEIGHT * Tile::WIDTH)) {
ctx.fill_rect(&Rect::new(0.0, 0.0, 1.0, 1.0));
}
ctx.push_clip_layer(&Rect::new(20.0, 20.0, 180.0, 180.0).to_path(0.1));
ctx.pop_layer();
ctx.flush();
}
#[cfg(feature = "multithreading")]
#[test]
fn multithreaded_crash_after_reset() {
use crate::{Level, RenderMode, RenderSettings};
use vello_common::pixmap::Pixmap;
let mut pixmap = Pixmap::new(200, 200);
let settings = RenderSettings {
level: Level::try_detect().unwrap_or(Level::baseline()),
num_threads: 1,
render_mode: RenderMode::OptimizeQuality,
};
let mut resources = crate::Resources::new();
let mut ctx = RenderContext::new_with(200, 200, settings);
ctx.reset();
ctx.fill_path(&Rect::new(0.0, 0.0, 100.0, 100.0).to_path(0.1));
ctx.flush();
ctx.render_to_pixmap(&mut resources, &mut pixmap);
ctx.flush();
ctx.render_to_pixmap(&mut resources, &mut pixmap);
}
#[cfg(feature = "text")]
#[test]
fn glyph_atlas_resources_are_lazy() {
const ROBOTO_FONT: &[u8] =
include_bytes!("../../../examples/assets/roboto/Roboto-Regular.ttf");
let font = FontData::new(Blob::new(Arc::new(ROBOTO_FONT)), 0);
let glyphs = [Glyph {
id: 1,
x: 0.0,
y: 0.0,
}];
let mut resources = crate::Resources::new();
let mut ctx = RenderContext::new(100, 100);
ctx.fill_rect(&Rect::new(0.0, 0.0, 10.0, 10.0));
ctx.fill_path(&Rect::new(10.0, 10.0, 20.0, 20.0).to_path(0.1));
ctx.glyph_run(&mut resources, &font)
.fill_glyphs(glyphs.into_iter());
assert!(resources.glyph_resources.is_none());
ctx.glyph_run(&mut resources, &font)
.atlas_cache(true)
.fill_glyphs(glyphs.into_iter());
assert!(resources.glyph_resources.is_some());
}
}
+498
View File
@@ -0,0 +1,498 @@
// Copyright 2025 the Vello Authors and the Parley Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Vello CPU glyph rendering backend.
//!
//! Provides [`GlyphAtlas`] (atlas backed by per-page [`Pixmap`]s) and the
//! [`GlyphRenderer`](glifo::GlyphRenderer) implementation for [`RenderContext`] that
//! rasterises glyphs into CPU-accessible pixel buffers.
//!
//! The key difference from the hybrid backend is that atlas pages are owned as
//! [`Arc<Pixmap>`]s here, so the CPU renderer can read pixels directly without
//! any GPU upload step.
use crate::render::{ATLAS_IMAGE_ID_BASE, DEFAULT_GLYPH_ATLAS_SIZE};
use crate::{
Image, ImageSource, PaintType, Pixmap, RenderContext, RenderMode, RenderSettings, Resources,
color, kurbo, peniko,
};
use alloc::boxed::Box;
use alloc::sync::Arc;
use alloc::vec::Vec;
use color::palette::css::BLACK;
use core::fmt::Debug;
use core::ops::RangeInclusive;
use glifo::atlas::{
AtlasConfig, AtlasSlot, GlyphAtlas, GlyphCacheConfig, ImageCache, PendingClearRect,
};
use glifo::{AtlasCacher, DrawSink, GlyphRunBackend};
use glifo::{Glyph, renderer};
use kurbo::{Affine, BezPath, Rect};
use peniko::BlendMode;
use peniko::color::{AlphaColor, Srgb};
use vello_common::fearless_simd::Level;
use vello_common::paint::ImageId;
fn atlas_page_image_id(page_index: u32) -> ImageId {
ImageId::new(ATLAS_IMAGE_ID_BASE + page_index)
}
#[derive(Debug)]
pub(crate) struct GlyphAtlasResources {
pub(crate) glyph_atlas: GlyphAtlas,
pub(crate) image_cache: ImageCache,
pub(crate) glyph_renderer: Box<RenderContext>,
/// One `Pixmap` per atlas page, grown on demand.
// It's a bit annoying to have this in an `Arc`, but it needs to be this way. During fine
// rasterization, we need to be able to easily clone the atlas page so that it can be shared
// across multiple threads. However, we also need to be able to mutate the pixmap to
// sync new glyphs. The way this is achieved is by calling `Arc::get_mut` when syncing
// (at this point, the pixmap isn't shared anywhere else). Before fine rasterization, we
// then share it with the image registry such that it can easily be fetched and cloned during
// fine rasterization. After that, we remove it from the image registry, such that it's uniquely
// owned again and can be mutated in the next frame.
pub(crate) pixmaps: Vec<Arc<Pixmap>>,
/// Width of each atlas page in pixels.
page_width: u16,
/// Height of each atlas page in pixels.
page_height: u16,
}
impl GlyphAtlasResources {
pub(crate) fn with_config(
page_width: u16,
page_height: u16,
level: Level,
render_mode: RenderMode,
eviction_config: GlyphCacheConfig,
) -> Self {
Self {
glyph_atlas: GlyphAtlas::with_config(eviction_config),
image_cache: ImageCache::new_with_config(AtlasConfig::default()),
glyph_renderer: Box::new(RenderContext::new_with(
page_width,
page_height,
RenderSettings {
level,
num_threads: 0,
render_mode,
},
)),
pixmaps: Vec::new(),
page_width,
page_height,
}
}
pub(crate) fn maintain(&mut self) {
self.glyph_atlas.maintain(&mut self.image_cache);
}
}
/// Ensure a pixmap exists for the given page, creating it if needed.
fn ensure_page(
pixmaps: &mut Vec<Arc<Pixmap>>,
page_width: u16,
page_height: u16,
page_index: usize,
) {
while pixmaps.len() <= page_index {
pixmaps.push(Arc::new(Pixmap::new(page_width, page_height)));
}
}
impl Resources {
pub(crate) fn prepare_glyph_cache(&mut self) {
if self.glyph_resources.is_some() {
self.sync_glyph_cache();
}
}
pub(crate) fn maintain_glyph_cache(&mut self) {
self.glyph_prep_cache.maintain();
if let Some(glyph_resources) = self.glyph_resources.as_mut() {
glyph_resources.maintain();
let page_count = glyph_resources.pixmaps.len();
for page_index in 0..page_count {
self.image_registry.destroy_atlas_page(page_index as u32);
}
self.clear_evicted_glyph_atlas_regions();
}
}
fn ensure_glyph_resources(&mut self, level: Level, render_mode: RenderMode) {
if self.glyph_resources.is_none() {
self.glyph_resources = Some(GlyphAtlasResources::with_config(
DEFAULT_GLYPH_ATLAS_SIZE,
DEFAULT_GLYPH_ATLAS_SIZE,
level,
render_mode,
GlyphCacheConfig::default(),
));
}
}
/// Upload all pending bitmaps, rasterize pending outline/COLR glyphs, etc.
fn sync_glyph_cache(&mut self) {
let glyph_resources = self
.glyph_resources
.as_mut()
.expect("glyph atlas resources must exist before syncing");
// Upload all pending bitmap glyphs to the image atlas.
for upload in glyph_resources.glyph_atlas.drain_pending_uploads() {
let page_index = upload.atlas_slot.page_index as usize;
ensure_page(
&mut glyph_resources.pixmaps,
glyph_resources.page_width,
glyph_resources.page_height,
page_index,
);
let pixmap = Arc::get_mut(&mut glyph_resources.pixmaps[page_index])
.expect("atlas pixmap should be uniquely owned during bitmap upload");
copy_pixmap_to_atlas(
&upload.pixmap,
pixmap,
upload.atlas_slot.x,
upload.atlas_slot.y,
upload.atlas_slot.width,
upload.atlas_slot.height,
);
}
// Draw all new COLR/outline glyphs into the render context, and then composite them into the
// existing atlas page.
let glyph_renderer = glyph_resources.glyph_renderer.as_mut();
glyph_resources
.glyph_atlas
.replay_pending_atlas_commands(|recorder| {
let page_index = recorder.page_index as usize;
ensure_page(
&mut glyph_resources.pixmaps,
glyph_resources.page_width,
glyph_resources.page_height,
page_index,
);
let page = Arc::get_mut(&mut glyph_resources.pixmaps[page_index])
.expect("atlas page pixmap must be uniquely owned during replay");
glyph_renderer.reset();
renderer::replay_atlas_commands(&mut recorder.commands, glyph_renderer);
glyph_renderer.flush();
glyph_renderer.composite_to_pixmap_at_offset(&Self::default(), page, 0, 0);
});
for (page_index, pixmap) in glyph_resources.pixmaps.iter().enumerate() {
self.image_registry
.register_atlas_page(page_index as u32, Arc::clone(pixmap));
}
}
fn clear_evicted_glyph_atlas_regions(&mut self) {
let glyph_resources = self
.glyph_resources
.as_mut()
.expect("glyph atlas resources must exist before clearing");
for clear in glyph_resources.glyph_atlas.drain_pending_clear_rects() {
let pixmap = Arc::get_mut(&mut glyph_resources.pixmaps[clear.page_index as usize])
.expect("atlas pixmap should be uniquely owned during region clearing");
clear_pixmap_region(pixmap, clear);
}
}
}
#[doc(hidden)]
#[derive(Debug)]
pub struct CpuGlyphRunBackend<'a> {
pub ctx: &'a mut RenderContext,
pub resources: &'a mut Resources,
pub atlas_cache_enabled: bool,
}
impl<'a> CpuGlyphRunBackend<'a> {
fn render_glyphs<Glyphs>(
self,
run: glifo::GlyphRun<'a>,
glyphs: Glyphs,
render: impl FnOnce(&mut glifo::GlyphRunRenderer<'a, 'a, Glyphs>, &mut RenderContext),
) where
Glyphs: Iterator<Item = Glyph> + Clone,
{
let atlas_cacher = if self.atlas_cache_enabled {
self.resources.ensure_glyph_resources(
self.ctx.render_settings.level,
self.ctx.render_settings.render_mode,
);
let glyph_resources = self
.resources
.glyph_resources
.as_mut()
.expect("glyph atlas resources must exist after initialization");
AtlasCacher::Enabled(
&mut glyph_resources.glyph_atlas,
&mut glyph_resources.image_cache,
)
} else {
AtlasCacher::Disabled
};
let mut glyph_run = run.build(
glyphs,
self.resources.glyph_prep_cache.as_mut(),
atlas_cacher,
);
render(&mut glyph_run, self.ctx);
}
}
impl<'a> GlyphRunBackend<'a> for CpuGlyphRunBackend<'a> {
fn atlas_cache(mut self, enabled: bool) -> Self {
self.atlas_cache_enabled = enabled;
self
}
fn fill_glyphs<Glyphs>(self, run: glifo::GlyphRun<'a>, glyphs: Glyphs)
where
Glyphs: Iterator<Item = Glyph> + Clone,
{
self.render_glyphs(run, glyphs, |glyph_run, ctx| glyph_run.fill_glyphs(ctx));
}
fn stroke_glyphs<Glyphs>(self, run: glifo::GlyphRun<'a>, glyphs: Glyphs)
where
Glyphs: Iterator<Item = Glyph> + Clone,
{
self.render_glyphs(run, glyphs, |glyph_run, ctx| {
let stroke_adjustment = glyph_run.stroke_adjustment();
let original_width = ctx.stroke().width;
ctx.stroke_mut().width *= stroke_adjustment;
glyph_run.stroke_glyphs(ctx);
ctx.stroke_mut().width = original_width;
});
}
fn render_decoration<Glyphs>(
self,
run: glifo::GlyphRun<'a>,
glyphs: Glyphs,
x_range: RangeInclusive<f32>,
baseline_y: f32,
offset: f32,
size: f32,
buffer: f32,
) where
Glyphs: Iterator<Item = Glyph> + Clone,
{
self.render_glyphs(run, glyphs, |glyph_run, ctx| {
glyph_run.render_decoration(x_range, baseline_y, offset, size, buffer, ctx);
});
}
}
/// A glyph run builder.
pub type GlyphRunBuilder<'a> = glifo::GlyphRunBuilder<'a, CpuGlyphRunBackend<'a>>;
/// Zero out a rectangular region in the atlas pixmap.
///
/// Necessary because `composite_to_pixmap_at_offset` uses `SrcOver` blending,
/// so stale pixels from evicted glyphs would bleed through if not cleared.
fn clear_pixmap_region(dst: &mut Pixmap, rect: PendingClearRect) {
let dst_stride = dst.width() as usize;
let dst_data = dst.data_as_u8_slice_mut();
let clear_width = rect.width as usize;
let clear_height = rect.height as usize;
for y in 0..clear_height {
let row_start = ((rect.y as usize + y) * dst_stride + rect.x as usize) * 4;
let row_end = row_start + clear_width * 4;
dst_data[row_start..row_end].fill(0);
}
}
/// Copy bitmap glyph pixels into a rectangular region of an atlas page.
fn copy_pixmap_to_atlas(
src: &Pixmap,
dst: &mut Pixmap,
dst_x: u16,
dst_y: u16,
width: u16,
height: u16,
) {
let copy_width = width as usize;
let copy_height = height as usize;
let src_stride = src.width() as usize;
let dst_stride = dst.width() as usize;
let src_data = src.data_as_u8_slice();
let dst_data = dst.data_as_u8_slice_mut();
for y in 0..copy_height {
let src_row_start = y * src_stride * 4;
let src_row_end = src_row_start + copy_width * 4;
let dst_row_start = ((dst_y as usize + y) * dst_stride + dst_x as usize) * 4;
let dst_row_end = dst_row_start + copy_width * 4;
dst_data[dst_row_start..dst_row_end].copy_from_slice(&src_data[src_row_start..src_row_end]);
}
}
impl DrawSink for RenderContext {
#[inline]
fn set_transform(&mut self, t: Affine) {
Self::set_transform(self, t);
}
#[inline]
fn set_paint(&mut self, paint: glifo::AtlasPaint) {
Self::set_paint(self, paint);
}
#[inline]
fn set_paint_transform(&mut self, t: Affine) {
Self::set_paint_transform(self, t);
}
#[inline]
fn fill_path(&mut self, path: &BezPath) {
Self::fill_path(self, path);
}
#[inline]
fn fill_rect(&mut self, rect: &Rect) {
Self::fill_rect(self, rect);
}
#[inline]
fn push_clip_layer(&mut self, clip: &BezPath) {
Self::push_clip_layer(self, clip);
}
#[inline]
fn push_clip_path(&mut self, clip: &BezPath) {
Self::push_clip_path(self, clip);
}
#[inline]
fn push_blend_layer(&mut self, blend_mode: BlendMode) {
Self::push_blend_layer(self, blend_mode);
}
#[inline]
fn pop_layer(&mut self) {
Self::pop_layer(self);
}
#[inline]
fn pop_clip_path(&mut self) {
Self::pop_clip_path(self);
}
#[inline]
fn width(&self) -> u16 {
Self::width(self)
}
#[inline]
fn height(&self) -> u16 {
Self::height(self)
}
}
impl glifo::GlyphRenderer for RenderContext {
type SavedState = vello_common::render_state::RenderState;
#[inline]
fn save_state(&mut self) -> Self::SavedState {
self.save_current_state()
}
#[inline]
fn restore_state(&mut self, state: Self::SavedState) {
Self::restore_state(self, state);
}
#[inline]
fn stroke_path(&mut self, path: &BezPath) {
Self::stroke_path(self, path);
}
#[inline]
fn set_paint_image(&mut self, image: Image) {
self.set_paint(image);
}
#[inline]
fn set_tint(&mut self, tint: Option<vello_common::paint::Tint>) {
Self::set_tint(self, tint);
}
#[inline]
fn get_context_color(&self) -> AlphaColor<Srgb> {
let paint = self.paint().clone();
match paint {
PaintType::Solid(s) => s,
_ => BLACK,
}
}
#[inline]
fn atlas_image_source(&self, atlas_slot: &AtlasSlot) -> ImageSource {
ImageSource::opaque_id(atlas_page_image_id(atlas_slot.page_index))
}
#[inline]
fn atlas_paint_transform(&self, atlas_slot: &AtlasSlot) -> Affine {
Affine::translate((-(atlas_slot.x as f64), -(atlas_slot.y as f64)))
}
}
/// Debug utilities for visualizing glyph bounds during rasterization.
#[cfg(debug_assertions)]
#[allow(
dead_code,
unreachable_pub,
clippy::trivially_copy_pass_by_ref,
reason = "debug-only utilities called manually during development"
)]
mod debug {
use core::sync::atomic::{AtomicUsize, Ordering};
use crate::RenderContext;
use crate::kurbo::{Affine, Rect};
use crate::peniko;
use glifo::atlas::RasterMetrics;
static COLOR_INDEX: AtomicUsize = AtomicUsize::new(0);
/// Twelve semi-transparent rotating colours for distinguishing adjacent
/// glyph bounds. Each call to [`fill_glyph_bounds`] advances the index.
const COLORS: [peniko::Color; 12] = [
peniko::Color::new([1.0, 0.0, 0.0, 0.5]), // Red
peniko::Color::new([0.0, 1.0, 0.0, 0.5]), // Green
peniko::Color::new([0.0, 0.0, 1.0, 0.5]), // Blue
peniko::Color::new([1.0, 1.0, 0.0, 0.5]), // Yellow
peniko::Color::new([1.0, 0.0, 1.0, 0.5]), // Magenta
peniko::Color::new([0.0, 1.0, 1.0, 0.5]), // Cyan
peniko::Color::new([1.0, 0.5, 0.0, 0.5]), // Orange
peniko::Color::new([0.5, 0.0, 1.0, 0.5]), // Purple
peniko::Color::new([0.0, 1.0, 0.5, 0.5]), // Mint
peniko::Color::new([1.0, 0.5, 0.5, 0.5]), // Pink
peniko::Color::new([0.5, 1.0, 0.5, 0.5]), // Light green
peniko::Color::new([0.5, 0.5, 1.0, 0.5]), // Light blue
];
/// Fill the glyph's pixel bounds with the next rotating debug colour.
/// Call before rendering the actual glyph content to visualise the extent.
pub fn fill_glyph_bounds(renderer: &mut RenderContext, raster_metrics: &RasterMetrics) {
let idx = COLOR_INDEX.fetch_add(1, Ordering::Relaxed) % COLORS.len();
renderer.set_transform(Affine::IDENTITY);
renderer.set_paint(COLORS[idx]);
renderer.fill_rect(&Rect::new(
0.0,
0.0,
raster_metrics.width as f64,
raster_metrics.height as f64,
));
}
}
+108
View File
@@ -0,0 +1,108 @@
// Copyright 2026 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Debug helpers for glyph atlas and CPU text resources.
#![allow(dead_code, reason = "only used for debugging purposes")]
#[cfg(feature = "png")]
use crate::Pixmap;
#[cfg(feature = "png")]
use crate::render::Resources;
use crate::text::GlyphAtlasResources;
#[cfg(feature = "png")]
use alloc::format;
use glifo::GlyphCacheKey;
use glifo::atlas::GlyphCacheStats;
#[cfg(feature = "png")]
impl GlyphAtlasResources {
/// Save all atlas pages to PNG files with a custom path prefix.
///
/// Files are saved as `{path_prefix}_atlas_page_{index}.png`.
pub(crate) fn save_atlas_pages_to(&self, path_prefix: &str) {
for (i, pixmap) in self.pixmaps.iter().enumerate() {
let path = format!("{path_prefix}_atlas_page_{i}.png");
let _ = save_pixmap_to_png(pixmap, std::path::Path::new(&path));
}
}
/// Save all atlas pages to PNG files for debugging.
///
/// Files are saved to `examples/_output/vello_cpu_atlas_page_{index}.png`.
pub(crate) fn save_atlas_pages(&self) {
for (i, pixmap) in self.pixmaps.iter().enumerate() {
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.pop(); // up from vello_cpu to sparse_strips
path.pop(); // up from sparse_strips to workspace root
path.push("examples");
path.push("_output");
let _ = std::fs::create_dir_all(&path);
path.push(format!("vello_cpu_atlas_page_{i}.png"));
let _ = save_pixmap_to_png(pixmap, &path);
}
}
}
impl GlyphAtlasResources {
/// Get detailed statistics about cached glyphs.
pub(crate) fn stats(&self) -> GlyphCacheStats {
self.glyph_atlas.stats(self.pixmaps.len())
}
/// Log detailed atlas statistics at info level.
pub(crate) fn log_atlas_stats(&self) {
self.glyph_atlas.log_atlas_stats(self.pixmaps.len());
}
/// Returns all cached glyph keys (for debugging).
pub(crate) fn all_keys(&self) -> impl Iterator<Item = &GlyphCacheKey> {
self.glyph_atlas.all_keys()
}
/// Log all cached keys grouped by glyph ID at info level.
pub(crate) fn log_keys_grouped(&self) {
self.glyph_atlas.log_keys_grouped();
}
}
#[cfg(feature = "png")]
impl Resources {
pub(crate) fn save_glyph_atlas_pages(&self) {
if let Some(glyph_resources) = &self.glyph_resources {
glyph_resources.save_atlas_pages();
}
}
pub(crate) fn save_glyph_atlas_pages_to(&self, path_prefix: &str) {
if let Some(glyph_resources) = &self.glyph_resources {
glyph_resources.save_atlas_pages_to(path_prefix);
}
}
}
/// Save a pixmap to a PNG file (diagnostic utility).
#[cfg(feature = "png")]
pub(crate) fn save_pixmap_to_png(pixmap: &Pixmap, path: &std::path::Path) -> std::io::Result<()> {
use std::fs::File;
use std::io::BufWriter;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let file = File::create(path)?;
let w = BufWriter::new(file);
let mut encoder = png::Encoder::new(w, pixmap.width() as u32, pixmap.height() as u32);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
let mut writer = encoder.write_header().map_err(std::io::Error::other)?;
writer
.write_image_data(pixmap.data_as_u8_slice())
.map_err(std::io::Error::other)?;
Ok(())
}
+112
View File
@@ -0,0 +1,112 @@
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use crate::peniko::ImageQuality;
use vello_common::encode::EncodedImage;
use vello_common::fearless_simd::{Simd, SimdBase, f32x4, u8x32};
use vello_common::math::FloatExt;
use vello_common::util::Div255Ext;
pub(crate) mod scalar {
/// Perform an approximate division by 255.
///
/// There are three reasons for having this method.
/// 1) Divisions are slower than shifting + adding, and the compiler does not seem to replace
/// divisions by 255 with an equivalent (this was verified by benchmarking; doing / 255 was
/// significantly slower).
/// 2) Integer divisions are usually not available in SIMD, so this provides a good baseline
/// implementation.
/// 3) There are two options for performing the division: One is to perform the division
/// in a way that completely preserves the rounding semantics of a integer division by
/// 255. This could be achieved using the implementation `(val + 1 + (val >> 8)) >> 8`.
/// The second approach (used here) has slightly different rounding behavior to a
/// normal division by 255, but is much faster (see <https://github.com/linebender/vello/issues/904>)
/// and therefore preferable for the high-performance pipeline.
///
/// Four properties worth mentioning:
/// - This actually calculates the ceiling of `val / 256`.
/// - Within the allowed range for `val`, rounding errors do not appear for values divisible by 255, i.e. any call `div_255(val * 255)` will always yield `val`.
/// - If there is a discrepancy, this division will always yield a value 1 higher than the original.
/// - This holds for values of `val` up to and including `65279`. You should not call this function with higher values.
#[inline(always)]
pub(crate) const fn div_255(val: u16) -> u16 {
debug_assert!(
val < 65280,
"the properties of `div_255` do not hold for values of `65280` or greater"
);
(val + 255) >> 8
}
#[cfg(test)]
mod tests {
use crate::util::scalar::div_255;
#[test]
fn div_255_properties() {
for i in 0_u16..256 * 255 {
let expected = i / 255;
let actual = div_255(i);
assert!(
expected <= actual,
"In case of a discrepancy, the division should yield a value higher than the original."
);
let diff = expected.abs_diff(actual);
assert!(diff <= 1, "Rounding error shouldn't be higher than 1.");
if i % 255 == 0 {
assert_eq!(diff, 0, "Division should be accurate for multiples of 255.");
}
}
}
}
}
pub(crate) trait NormalizedMulExt {
fn normalized_mul(self, other: Self) -> Self;
}
impl<S: Simd> NormalizedMulExt for u8x32<S> {
#[inline(always)]
fn normalized_mul(self, other: Self) -> Self {
let divided = (self.simd.widen_u8x32(self) * other.simd.widen_u8x32(other)).div_255();
self.simd.narrow_u16x32(divided)
}
}
pub(crate) trait EncodedImageExt {
fn has_skew(&self) -> bool;
fn nearest_neighbor(&self) -> bool;
}
impl EncodedImageExt for EncodedImage {
fn has_skew(&self) -> bool {
!(self.x_advance.y as f32).is_nearly_zero() || !(self.y_advance.x as f32).is_nearly_zero()
}
fn nearest_neighbor(&self) -> bool {
self.sampler.quality == ImageQuality::Low
}
}
pub(crate) trait Premultiply {
fn premultiply(self, alphas: Self) -> Self;
fn unpremultiply(self, alphas: Self) -> Self;
}
impl<S: Simd> Premultiply for f32x4<S> {
#[inline(always)]
fn premultiply(self, alphas: Self) -> Self {
self * alphas
}
#[inline(always)]
fn unpremultiply(self, alphas: Self) -> Self {
let zero = Self::splat(alphas.simd, 0.0);
let divided = self / alphas;
self.simd
.select_f32x4(self.simd.simd_eq_f32x4(alphas, zero), zero, divided)
}
}