mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-29 04:28:20 -06:00
Integrate KenM76 PR #5199
# Conflicts: # crates/render/tests/it/main.rs # vendor/README.md
This commit is contained in:
Generated
-2
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Vendored
+2
File diff suppressed because one or more lines are too long
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"git": {
|
||||
"sha1": "875f324f21da93019cae9e8e61d4abfd69893206"
|
||||
},
|
||||
"path_in_vcs": "sparse_strips/vello_cpu"
|
||||
}
|
||||
Vendored
+205
@@ -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
@@ -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",
|
||||
]
|
||||
Vendored
+192
@@ -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
@@ -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"]
|
||||
Vendored
+176
@@ -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
|
||||
Vendored
+25
@@ -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.
|
||||
Vendored
+188
@@ -0,0 +1,188 @@
|
||||
<div align="center">
|
||||
|
||||
# Vello CPU
|
||||
|
||||
**CPU-based renderer**
|
||||
|
||||
[](https://crates.io/crates/vello_cpu)
|
||||
[](https://docs.rs/vello_cpu)
|
||||
[](#license)
|
||||
\
|
||||
[](https://xi.zulipchat.com/#narrow/channel/197075-vello)
|
||||
[](https://github.com/linebender/vello/actions)
|
||||
[](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
|
||||
Vendored
+165
@@ -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
@@ -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();
|
||||
}
|
||||
Vendored
+100
@@ -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();
|
||||
}
|
||||
Vendored
+145
@@ -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())
|
||||
}
|
||||
Vendored
+95
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
Vendored
+89
@@ -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
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1147
File diff suppressed because it is too large
Load Diff
Vendored
+120
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
Vendored
+253
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
Vendored
+1127
File diff suppressed because it is too large
Load Diff
+154
@@ -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()
|
||||
}
|
||||
}
|
||||
Vendored
+186
@@ -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,
|
||||
}
|
||||
Vendored
+243
@@ -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
|
||||
}
|
||||
}
|
||||
Vendored
+917
@@ -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());
|
||||
}
|
||||
}
|
||||
Vendored
+498
@@ -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,
|
||||
));
|
||||
}
|
||||
}
|
||||
Vendored
+108
@@ -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(())
|
||||
}
|
||||
Vendored
+112
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user