mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-26 19:19:10 -06:00
M2: clip intersection starts its row walk at the first shared row (#2026)
* M2: clip intersection starts its row walk at the first shared row clip::intersect in vello_common, the step every clipped path passes through, began its walk over strip rows at the earlier of the two paths' first rows. Each path therefore visited every row above it, although rows where one path has no strips produce no output. Every page is clipped (the crop box is a clip), so this ran for every stroke of every render: on the A1 engineering sample it was the hottest function in a full-page render. Start at the later first row, as vello_common 0.1 does. The strips and the pixels are unchanged; a test counts the rows the walk visits and fails without the change. vello_common 0.0.8 is vendored with that one-line PdfCraft patch (marked in the source), its unit test, the vendor/README.md row and an ATTRIBUTION.toml entry for its reference image. Cargo.lock records it as a path crate. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> * Refresh catalog hashes after merging main --------- Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Claude Haiku 5.5
Brandon Thomas
parent
e6e65d0292
commit
572870eca8
+2
-1
@@ -4,7 +4,7 @@
|
||||
|
||||
Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE).
|
||||
|
||||
## In this repository (271)
|
||||
## In this repository (272)
|
||||
|
||||
| Asset | Title | Author | Licence | Source | Used for |
|
||||
|---|---|---|---|---|---|
|
||||
@@ -279,6 +279,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material,
|
||||
| `crates/sign/tests/data/x509-rsa-sha1.pdf` | One-page PDF with a legacy adbe.x509.rsa_sha1 signature by the RSA test key | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: hand-written PDF objects; signature made with `openssl dgst -sha1 -sign` over its byte ranges (see crates/sign/tests/data/README.md) | Signature validation tests (crates/sign/tests/pdf.rs): the legacy adbe.x509.rsa_sha1 format |
|
||||
| `assets/icons/monitor.svg` | Lucide icon "monitor" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/monitor.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
||||
| `crates/ui-egui/src/i18n/sl.tsv` | Slovene interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `vendor/vello_common/assets/probe.png` | vello_common probe reference image (rendered from probe.rgba) | Linebender vello contributors | MIT OR Apache-2.0 | vello_common 0.0.8 (crates.io), vendored in vendor/vello_common; regenerated by its vello_sparse_tests regenerate_probe_reference binary | Reference picture of the vendored vello_common probe module, for people only; the build reads probe.rgba, not this file |
|
||||
|
||||
## Compiled in through dependencies (6)
|
||||
|
||||
|
||||
@@ -3364,3 +3364,14 @@ licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "dddc56fe8c320dd807c56a5d6450eeb5cdaecc81d9e517b6c1adfe986d688a41"
|
||||
|
||||
[[asset]]
|
||||
path = "vendor/vello_common/assets/probe.png"
|
||||
title = "vello_common probe reference image (rendered from probe.rgba)"
|
||||
author = "Linebender vello contributors"
|
||||
source = "vello_common 0.0.8 (crates.io), vendored in vendor/vello_common; regenerated by its vello_sparse_tests regenerate_probe_reference binary"
|
||||
licence = "MIT OR Apache-2.0"
|
||||
licence_file = "vendor/vello_common/LICENSE-MIT"
|
||||
kind = "image"
|
||||
usage = "Reference picture of the vendored vello_common probe module, for people only; the build reads probe.rgba, not this file"
|
||||
sha256 = "78c59a0cfd2c829909d77006f94e34dac1ae5d1f90e7f10f72e84de541fef697"
|
||||
|
||||
Generated
-2
@@ -6883,8 +6883,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "vello_common"
|
||||
version = "0.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3361bff7f7d82c0c496b92048db83846691f0e844cc28dee92b1c824291b55ee"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"fearless_simd",
|
||||
|
||||
@@ -112,3 +112,4 @@ hayro-jbig2 = { path = "vendor/hayro-jbig2" }
|
||||
lopdf = { path = "vendor/lopdf" }
|
||||
winit = { path = "vendor/winit" }
|
||||
egui-wgpu = { path = "vendor/egui-wgpu" }
|
||||
vello_common = { path = "vendor/vello_common" }
|
||||
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+246
@@ -0,0 +1,246 @@
|
||||
<!-- 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
|
||||
|
||||
- `TextureId`, a handle for user-provided textures resolved at render-time, and `EncodedExternalTexture` for encoding draws that sample from them. ([#1552][] by [@tomcur][])
|
||||
- `RectU16`, a shared axis-aligned rectangle type for `u16` coordinate bounding boxes. ([#1559][] by [@tomcur][])
|
||||
|
||||
### Changed
|
||||
|
||||
- `WideTilesBbox` now uses `RectU16` internally, its `bbox` field is private, and `WideTilesBbox::new` now takes four `u16` coordinates instead of a `[u16; 4]`. ([#1559][] by [@tomcur][])
|
||||
- Renamed image transparency hint APIs from `may_have_opacities` and `*_opacity_hint` to `may_have_transparency` and `*_transparency_hint`. ([#1613][] by [@upsuper][])
|
||||
- Gradient LUTs no longer reserve an extra transparent entry for undefined radial-gradient regions; `EncodedGradient` now tracks whether those regions are possible. ([#1529][] by [@LaurenzV][])
|
||||
|
||||
### Removed
|
||||
|
||||
- Public `glyph` and `colr` modules and the `text` feature; sparse-strips text rendering now lives in `glifo` and the renderer crates. ([#1562][] by [@LaurenzV][])
|
||||
- Support for recordings, including the shared recording API.
|
||||
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
|
||||
|
||||
- Handling of zero-area rectangles in the rectangle fast path, avoiding out-of-viewport strips and downstream assertions. ([#1537][] by [@LaurenzV][])
|
||||
- Invalid gradients with NaN or out-of-range final stop offsets now fall back to the first color instead of passing validation. ([#1531][] by [@LaurenzV][])
|
||||
- Implicit subpaths that continue after `ClosePath` without an explicit `MoveTo`. ([#1544][] by [@tomcur][])
|
||||
- Negative-sized rectangles during strip generation. ([#1589][] by [@LaurenzV][])
|
||||
|
||||
### Optimized
|
||||
|
||||
- Rendering performance for paths with line segments fully or partly left of the viewport by culling invisible left-of-viewport line work while preserving winding contributions. ([#1368][] by [@b0nes164][])
|
||||
- Performance of clipped drawing by culling geometry outside the clip bounding box during flattening. ([#1519][] by [@tomcur][])
|
||||
- Flattening and tiling performance through codegen improvements in the sparse strip pipeline. ([#1600][], [#1616][], [#1634][] by [@LaurenzV][])
|
||||
|
||||
## [0.0.7][] - 2026-03-24
|
||||
|
||||
This release has an [MSRV][] of 1.92.
|
||||
|
||||
### Added
|
||||
|
||||
- Support for image tinting. ([#1460][] by [@grebmeg][])
|
||||
- `RenderState` abstraction. ([#1486][] by [@LaurenzV][])
|
||||
- Helpers for rendering filters. ([#1446][] by [@LaurenzV][])
|
||||
- `ImageCache` and `MultiAtlasManager`. ([#1457][] by [@grebmeg][])
|
||||
|
||||
### Changed
|
||||
|
||||
- Replaced custom blend mode representation with peniko `Mix` for filter effects. ([#1410][] by [@LaurenzV][])
|
||||
|
||||
### Removed
|
||||
|
||||
- Process Row Incremental Macro and Perfect Bit. ([#1384][] by [@b0nes164][])
|
||||
- `tolerance` parameter from the flattening method. ([#1399][] by [@LaurenzV][])
|
||||
- Command annotation logic. ([#1438][] by [@LaurenzV][])
|
||||
|
||||
### Fixed
|
||||
|
||||
- Double-scaling of non-hinted COLR glyphs. ([#1370][] by [@conor-93][])
|
||||
- Handling of `pop_clip` commands and properly resetting wide tiles. ([#1443][] by [@LaurenzV][], [#1447][] by [@tomcur][])
|
||||
- Layer ranges not being updated when a wide tile is reset with a background color. ([#1478][] by [@LaurenzV][])
|
||||
|
||||
### Optimized
|
||||
|
||||
- Bézier flattening performance by catching more Béziers whose chords are immediately within rendering tolerance. ([#1216][] by [@tomcur][])
|
||||
- Flattening and tiling performance by culling out-of-viewport Béziers before flattening. ([#1341][] by [@tomcur][])
|
||||
- Rendering performance of scenes including blend layers by ensuring no commands are generated for wide tiles without layer content. ([#1403][] by [@tomcur][])
|
||||
- Scenes including blend layers by only allocating layer scratch buffers for a wide tile upon draw commands being performed in that wide tile. ([#1414][] by [@tomcur][])
|
||||
- Analytic anti-aliasing. ([#1426][], [#1442][] by [@tomcur][])
|
||||
- Filter application to layers. ([#1444][] by [@LaurenzV][])
|
||||
- Rendering pixel-aligned rectangles. ([#1453][] by [@grebmeg][])
|
||||
- Coarse rasterization and scheduling of scenes without layers. ([#1454][] by [@LaurenzV][])
|
||||
- Resetting wide tiles. ([#1484][] by [@LaurenzV][])
|
||||
|
||||
## [0.0.6][] - 2026-01-15
|
||||
|
||||
This release has an [MSRV][] of 1.88.
|
||||
|
||||
### 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
|
||||
|
||||
- Alpha blending for fully opaque image fills. ([#1329][] by [@grebmeg][])
|
||||
|
||||
See also the [vello_hybrid 0.0.6](../vello_hybrid/CHANGELOG.md#006---2026-01-15) and [vello_cpu 0.0.6](../vello_cpu/CHANGELOG.md#006---2026-01-15) releases.
|
||||
|
||||
## [0.0.5][] - 2026-01-08
|
||||
|
||||
This release has an [MSRV][] of 1.88.
|
||||
|
||||
### Added
|
||||
|
||||
- A new module `clip` allowing for the possibility to intersect two strips to create a new strip representing their intersection. ([#1203][] by [@LaurenzV])
|
||||
- An `extend` method to `StripStorage` to extend its alphas/strips from another `StripStorage`. ([#1203][] by [@LaurenzV])
|
||||
- A new trait for approximate integer division by 255. ([#1203][] by [@LaurenzV])
|
||||
- A `from_parts` method for masks. ([#1237][] by [@LaurenzV])
|
||||
- Initial support for image filters. ([#1286][] by [@grebmeg])
|
||||
|
||||
### Changed
|
||||
|
||||
- `WideTile::generate` now takes an additional `BlendMode` as a parameter. ([#1159][] by [@LaurenzV])
|
||||
- `CmdFill` and `CmdAlphaFill` now store a `BlendMode` instead of `Option<BlendMode>`. ([#1159][] by [@LaurenzV])
|
||||
- `Strip` now implements `PartialEq` and `Eq`. ([#1203][] by [@LaurenzV])
|
||||
- `Strip` now has a `is_sentinel` method. ([#1203][] by [@LaurenzV])
|
||||
- `StripStorage` now implements `PartialEq` and `Eq`. ([#1203][] by [@LaurenzV])
|
||||
- The `generate_filled_path` method of `StripGenerator` now takes an optional clip path as input. ([#1203][] by [@LaurenzV])
|
||||
- The `generate` method of `Wide` now takes an optional mask as an additional argument. ([#1237][] by [@LaurenzV])
|
||||
- `CmdFill` and `CmdAlphaFill` now store an optional mask. ([#1237][] by [@LaurenzV])
|
||||
- Various changes to the logic for computing tile intersections and representation of tiles. ([#1293][], [#1317][], [#1318][] by [@b0nes164])
|
||||
- Support for computing data necessary to implement multi-sampled anti-aliasing. ([#1319][], by [@b0nes164])
|
||||
|
||||
### Optimized
|
||||
|
||||
- Gradient rendering. ([#1301][] by [@valadaptive])
|
||||
- Numerous performance and memory-efficiency improvements. ([#1325][] by [@LaurenzV], [#1327][] by [@grebmeg], [#1336][] by [@tomcur], [#1338][] by [@taj-p])
|
||||
|
||||
See also the [vello_hybrid 0.0.5](../vello_hybrid/CHANGELOG.md#005---2026-01-08) and [vello_cpu 0.0.5](../vello_cpu/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_cpu 0.0.4](../vello_cpu/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_cpu 0.0.3](../vello_cpu/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_cpu 0.0.2](../vello_cpu/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_cpu 0.0.1](../vello_cpu/CHANGELOG.md#001---2025-05-10) release.
|
||||
|
||||
[@b0nes164]: https://github.com/b0nes164
|
||||
[@conor-93]: https://github.com/conor-93
|
||||
[@DJMcNab]: https://github.com/waywardmonkeys
|
||||
[@grebmeg]: https://github.com/grebmeg
|
||||
[@LaurenzV]: https://github.com/LaurenzV
|
||||
[@taj-p]: https://github.com/taj-p
|
||||
[@tomcur]: https://github.com/tomcur
|
||||
[@upsuper]: https://github.com/upsuper
|
||||
[@valadaptive]: https://github.com/valadaptive
|
||||
[@waywardmonkeys]: https://github.com/waywardmonkeys
|
||||
|
||||
[#1159]: https://github.com/linebender/vello/pull/1159
|
||||
[#1203]: https://github.com/linebender/vello/pull/1203
|
||||
[#1216]: https://github.com/linebender/vello/pull/1216
|
||||
[#1237]: https://github.com/linebender/vello/pull/1237
|
||||
[#1286]: https://github.com/linebender/vello/pull/1286
|
||||
[#1293]: https://github.com/linebender/vello/pull/1293
|
||||
[#1301]: https://github.com/linebender/vello/pull/1301
|
||||
[#1317]: https://github.com/linebender/vello/pull/1317
|
||||
[#1318]: https://github.com/linebender/vello/pull/1318
|
||||
[#1319]: https://github.com/linebender/vello/pull/1319
|
||||
[#1325]: https://github.com/linebender/vello/pull/1325
|
||||
[#1327]: https://github.com/linebender/vello/pull/1327
|
||||
[#1329]: https://github.com/linebender/vello/pull/1329
|
||||
[#1336]: https://github.com/linebender/vello/pull/1327
|
||||
[#1338]: https://github.com/linebender/vello/pull/1327
|
||||
[#1341]: https://github.com/linebender/vello/pull/1341
|
||||
[#1349]: https://github.com/linebender/vello/pull/1349
|
||||
[#1353]: https://github.com/linebender/vello/pull/1353
|
||||
[#1354]: https://github.com/linebender/vello/pull/1354
|
||||
[#1368]: https://github.com/linebender/vello/pull/1368
|
||||
[#1399]: https://github.com/linebender/vello/pull/1399
|
||||
[#1403]: https://github.com/linebender/vello/pull/1403
|
||||
[#1414]: https://github.com/linebender/vello/pull/1414
|
||||
[#1370]: https://github.com/linebender/vello/pull/1370
|
||||
[#1384]: https://github.com/linebender/vello/pull/1384
|
||||
[#1410]: https://github.com/linebender/vello/pull/1410
|
||||
[#1426]: https://github.com/linebender/vello/pull/1426
|
||||
[#1438]: https://github.com/linebender/vello/pull/1438
|
||||
[#1442]: https://github.com/linebender/vello/pull/1442
|
||||
[#1443]: https://github.com/linebender/vello/pull/1443
|
||||
[#1444]: https://github.com/linebender/vello/pull/1444
|
||||
[#1446]: https://github.com/linebender/vello/pull/1446
|
||||
[#1447]: https://github.com/linebender/vello/pull/1447
|
||||
[#1453]: https://github.com/linebender/vello/pull/1453
|
||||
[#1454]: https://github.com/linebender/vello/pull/1454
|
||||
[#1457]: https://github.com/linebender/vello/pull/1457
|
||||
[#1460]: https://github.com/linebender/vello/pull/1460
|
||||
[#1478]: https://github.com/linebender/vello/pull/1478
|
||||
[#1484]: https://github.com/linebender/vello/pull/1484
|
||||
[#1486]: https://github.com/linebender/vello/pull/1486
|
||||
[#1519]: https://github.com/linebender/vello/pull/1519
|
||||
[#1529]: https://github.com/linebender/vello/pull/1529
|
||||
[#1531]: https://github.com/linebender/vello/pull/1531
|
||||
[#1537]: https://github.com/linebender/vello/pull/1537
|
||||
[#1544]: https://github.com/linebender/vello/pull/1544
|
||||
[#1552]: https://github.com/linebender/vello/pull/1552
|
||||
[#1559]: https://github.com/linebender/vello/pull/1559
|
||||
[#1562]: https://github.com/linebender/vello/pull/1562
|
||||
[#1589]: https://github.com/linebender/vello/pull/1589
|
||||
[#1600]: https://github.com/linebender/vello/pull/1600
|
||||
[#1611]: https://github.com/linebender/vello/pull/1611
|
||||
[#1613]: https://github.com/linebender/vello/pull/1613
|
||||
[#1616]: https://github.com/linebender/vello/pull/1616
|
||||
[#1634]: https://github.com/linebender/vello/pull/1634
|
||||
|
||||
[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
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
# 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 = "crc32fast"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[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 = "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 = "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 = "roxmltree"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||
|
||||
[[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 = "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"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"fearless_simd",
|
||||
"guillotiere",
|
||||
"hashbrown",
|
||||
"libm",
|
||||
"log",
|
||||
"peniko",
|
||||
"png",
|
||||
"roxmltree",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
]
|
||||
Vendored
+158
@@ -0,0 +1,158 @@
|
||||
# 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_common"
|
||||
version = "0.0.8"
|
||||
build = false
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "Core data structures and utilities shared across the Vello rendering, including geometry processing and tiling logic."
|
||||
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",
|
||||
]
|
||||
libm = [
|
||||
"peniko/libm",
|
||||
"dep:libm",
|
||||
"fearless_simd/libm",
|
||||
]
|
||||
multithreading = ["std"]
|
||||
pico_svg = ["dep:roxmltree"]
|
||||
png = [
|
||||
"std",
|
||||
"dep:png",
|
||||
]
|
||||
probe = []
|
||||
std = [
|
||||
"peniko/std",
|
||||
"fearless_simd/std",
|
||||
]
|
||||
|
||||
[lib]
|
||||
name = "vello_common"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies.bytemuck]
|
||||
version = "1.25.0"
|
||||
features = [
|
||||
"derive",
|
||||
"derive",
|
||||
]
|
||||
|
||||
[dependencies.fearless_simd]
|
||||
version = "0.4.0"
|
||||
default-features = false
|
||||
|
||||
[dependencies.guillotiere]
|
||||
version = "0.7.0"
|
||||
default-features = false
|
||||
|
||||
[dependencies.hashbrown]
|
||||
version = "0.17.1"
|
||||
features = [
|
||||
"default-hasher",
|
||||
"raw-entry",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[dependencies.libm]
|
||||
version = "0.2.16"
|
||||
optional = true
|
||||
|
||||
[dependencies.log]
|
||||
version = "0.4.29"
|
||||
|
||||
[dependencies.peniko]
|
||||
version = "0.6.1"
|
||||
features = ["bytemuck"]
|
||||
default-features = false
|
||||
|
||||
[dependencies.png]
|
||||
version = "0.18.1"
|
||||
optional = true
|
||||
|
||||
[dependencies.roxmltree]
|
||||
version = "0.20.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.smallvec]
|
||||
version = "1.15.1"
|
||||
|
||||
[dependencies.thiserror]
|
||||
version = "2.0.18"
|
||||
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"
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
[package]
|
||||
name = "vello_common"
|
||||
# When updating, also update the version in the workspace dependency in the root Cargo.toml
|
||||
version = "0.0.8"
|
||||
description = "Core data structures and utilities shared across the Vello rendering, including geometry processing and tiling logic."
|
||||
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, features = ["derive"] }
|
||||
peniko = { workspace = true, features = ["bytemuck"] }
|
||||
fearless_simd = { workspace = true }
|
||||
hashbrown = { workspace = true, features = ["raw-entry"] }
|
||||
png = { workspace = true, optional = true }
|
||||
roxmltree = { version = "0.20.0", optional = true }
|
||||
smallvec = { workspace = true }
|
||||
thiserror = { workspace = true, default-features = false }
|
||||
guillotiere = { workspace = true }
|
||||
libm = { version = "0.2.16", optional = true }
|
||||
log = { workspace = true }
|
||||
|
||||
[features]
|
||||
# If adding new features, also document in `src/lib.rs`
|
||||
default = ["std", "png"]
|
||||
# Get floating point functions from the standard library (likely using your target's libc).
|
||||
std = ["peniko/std", "fearless_simd/std"]
|
||||
# Use floating point implementations from libm.
|
||||
libm = ["peniko/libm", "dep:libm", "fearless_simd/libm"]
|
||||
# Allow loading Pixmap from PNG, and drawing png glyphs.
|
||||
png = ["std", "dep:png"]
|
||||
# Add helpers for drawing a probe scene.
|
||||
probe = []
|
||||
# Enable multi-threaded rendering.
|
||||
multithreading = ["std"]
|
||||
# Development only features
|
||||
|
||||
# Enable a simple version of SVG drawing.
|
||||
# This is only intended for development of Vello CPU itself,
|
||||
# and is likely to be removed/moved.
|
||||
pico_svg = ["dep:roxmltree"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
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
+100
@@ -0,0 +1,100 @@
|
||||
<div align="center">
|
||||
|
||||
# Vello Common
|
||||
|
||||
**Shared data structures**
|
||||
|
||||
[](https://crates.io/crates/vello_common)
|
||||
[](https://docs.rs/vello_common)
|
||||
[](#license)
|
||||
\
|
||||
[](https://xi.zulipchat.com/#narrow/channel/197075-vello)
|
||||
[](https://github.com/linebender/vello/actions)
|
||||
[](https://deps.rs/crate/vello_common)
|
||||
|
||||
</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_common
|
||||
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. -->
|
||||
|
||||
[libm]: https://crates.io/crates/libm
|
||||
[crate::pixmap::Pixmap]: https://docs.rs/vello_common/latest/vello_common/pixmap/struct.Pixmap.html
|
||||
<!-- cargo-rdme start -->
|
||||
|
||||
This crate includes common geometry representations, tiling logic, and other fundamental components used by both [Vello CPU][vello_cpu] and Vello Hybrid.
|
||||
|
||||
## Usage
|
||||
|
||||
This crate should not be used on its own, and you should instead use one of the renderers which use it.
|
||||
At the moment, only [Vello CPU][vello_cpu] is published, and you probably want to use that.
|
||||
|
||||
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.
|
||||
Vello does not use this crate.
|
||||
|
||||
## 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`][crate::pixmap::Pixmap]s from PNG images.
|
||||
Also required for rendering glyphs with an embedded PNG.
|
||||
Implies `std`.
|
||||
|
||||
At least one of `std` and `libm` is required; `std` overrides `libm`.
|
||||
|
||||
## Contents
|
||||
|
||||
- Shared data structures for paths, tiles, and strips
|
||||
- Geometry processing utilities
|
||||
- Common logic for rendering stages
|
||||
|
||||
This crate acts as a foundation for `vello_cpu` and `vello_hybrid`, providing essential components to minimize duplication.
|
||||
|
||||
[vello_cpu]: https://crates.io/crates/vello_cpu
|
||||
|
||||
<!-- cargo-rdme end -->
|
||||
|
||||
## Minimum supported Rust Version (MSRV)
|
||||
|
||||
This version of Vello Common has been verified to compile with **Rust 1.92** and later.
|
||||
|
||||
Future versions of Vello Common 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 Common'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 Common 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
+2
@@ -0,0 +1,2 @@
|
||||
In order to regenerate `probe.png` and `probe.rgba`, please run
|
||||
`cargo run --bin regenerate_probe_reference` inside of `vello_sparse_tests`.
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
BIN
Binary file not shown.
+19
@@ -0,0 +1,19 @@
|
||||
// Copyright 2025 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Blurred, rounded rectangles.
|
||||
use crate::color::{AlphaColor, Srgb};
|
||||
use crate::kurbo::Rect;
|
||||
|
||||
/// A blurred, rounded rectangle.
|
||||
#[derive(Debug)]
|
||||
pub struct BlurredRoundedRectangle {
|
||||
/// The base rectangle to use for the blur effect.
|
||||
pub rect: Rect,
|
||||
/// The color of the blurred rectangle.
|
||||
pub color: AlphaColor<Srgb>,
|
||||
/// The radius of the rounded rectangle's corners.
|
||||
pub radius: f32,
|
||||
/// The standard deviation of the blur effect.
|
||||
pub std_dev: f32,
|
||||
}
|
||||
Vendored
+996
@@ -0,0 +1,996 @@
|
||||
// Copyright 2025 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Managing clipping state.
|
||||
|
||||
use crate::geometry::RectU16;
|
||||
use crate::kurbo::{Affine, BezPath, PathEl, Rect};
|
||||
use crate::strip::Strip;
|
||||
use crate::strip_generator::{GenerationMode, StripGenerator, StripStorage};
|
||||
use crate::tile::Tile;
|
||||
use crate::util::normalized_mul_u8x16;
|
||||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
use fearless_simd::{Level, Simd, SimdBase, dispatch, u8x16};
|
||||
use peniko::Fill;
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
use peniko::kurbo::common::FloatFuncs as _;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ClipData {
|
||||
alpha_start: u32,
|
||||
strip_start: u32,
|
||||
|
||||
/// A coarse bounding box of the clip path in pixel coordinates.
|
||||
///
|
||||
/// These bounds have already been intersected with the viewport.
|
||||
bbox: RectU16,
|
||||
}
|
||||
|
||||
impl ClipData {
|
||||
fn to_path_data_ref<'a>(&self, storage: &'a StripStorage) -> PathDataRef<'a> {
|
||||
PathDataRef {
|
||||
strips: storage
|
||||
.strips
|
||||
.get(self.strip_start as usize..)
|
||||
.unwrap_or(&[]),
|
||||
alphas: storage
|
||||
.alphas
|
||||
.get(self.alpha_start as usize..)
|
||||
.unwrap_or(&[]),
|
||||
bbox: self.bbox,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A context for managing clip stacks.
|
||||
#[derive(Debug)]
|
||||
pub struct ClipContext {
|
||||
storage: StripStorage,
|
||||
temp_storage: StripStorage,
|
||||
clip_stack: Vec<ClipData>,
|
||||
}
|
||||
|
||||
impl Default for ClipContext {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ClipContext {
|
||||
/// Create a new clip context.
|
||||
#[inline]
|
||||
pub fn new() -> Self {
|
||||
let mut main_storage = StripStorage::default();
|
||||
main_storage.set_generation_mode(GenerationMode::Append);
|
||||
Self {
|
||||
storage: main_storage,
|
||||
temp_storage: StripStorage::default(),
|
||||
clip_stack: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the clip context.
|
||||
#[inline]
|
||||
pub fn reset(&mut self) {
|
||||
self.clip_stack.clear();
|
||||
self.storage.clear();
|
||||
self.temp_storage.clear();
|
||||
}
|
||||
|
||||
/// Get the data of the current clip path.
|
||||
#[inline]
|
||||
pub fn get(&self) -> Option<PathDataRef<'_>> {
|
||||
self.clip_stack
|
||||
.last()
|
||||
.map(|c| c.to_path_data_ref(&self.storage))
|
||||
}
|
||||
|
||||
/// Push a new clip path to the stack.
|
||||
#[inline]
|
||||
pub fn push_clip(
|
||||
&mut self,
|
||||
clip_path: &BezPath,
|
||||
strip_generator: &mut StripGenerator,
|
||||
fill_rule: Fill,
|
||||
transform: Affine,
|
||||
aliasing_threshold: Option<u8>,
|
||||
) {
|
||||
self.temp_storage.clear();
|
||||
|
||||
let alpha_start = self.storage.alphas.len() as u32;
|
||||
let strip_start = self.storage.strips.len() as u32;
|
||||
|
||||
// Calculate a coarse bounding box of the path. If the path is empty, the bounding box is
|
||||
// an infinite and inversed `kurbo::Rect`. This is harmless in practice: an empty path does
|
||||
// not produce any strips.
|
||||
//
|
||||
// Note this iterates `clip_path`, which means we iterate it twice: once here, and once in
|
||||
// flattening. If we ever take an iterator instead, or want to prevent iterating twice, we
|
||||
// could move this calculation into flattening (perhaps with a const-generic as to not
|
||||
// pessimize calls that don't require the bbox).
|
||||
let path_bbox = control_point_bbox(clip_path, transform);
|
||||
let mut bbox = RectU16::new(
|
||||
path_bbox.x0 as u16,
|
||||
path_bbox.y0 as u16,
|
||||
path_bbox.x1.ceil() as u16,
|
||||
path_bbox.y1.ceil() as u16,
|
||||
);
|
||||
|
||||
// Intersect with the existing clip bounding box, or the viewport if this is the outermost
|
||||
// clip.
|
||||
if let Some(existing) = self.clip_stack.last() {
|
||||
bbox = bbox.intersect(existing.bbox);
|
||||
} else {
|
||||
bbox.x1 = bbox.x1.min(strip_generator.width());
|
||||
bbox.y1 = bbox.y1.min(strip_generator.height());
|
||||
}
|
||||
|
||||
let clip_data = ClipData {
|
||||
alpha_start,
|
||||
strip_start,
|
||||
bbox,
|
||||
};
|
||||
|
||||
let existing_clip = self
|
||||
.clip_stack
|
||||
.last()
|
||||
.map(|c| c.to_path_data_ref(&self.storage));
|
||||
|
||||
strip_generator.generate_filled_path(
|
||||
clip_path,
|
||||
fill_rule,
|
||||
transform,
|
||||
aliasing_threshold,
|
||||
&mut self.temp_storage,
|
||||
existing_clip,
|
||||
);
|
||||
|
||||
self.storage.extend(&self.temp_storage);
|
||||
self.clip_stack.push(clip_data);
|
||||
}
|
||||
|
||||
/// Pop the least recent clip path.
|
||||
#[inline]
|
||||
pub fn pop_clip(&mut self) {
|
||||
let data = self.clip_stack.pop().expect("clip stack underflowed");
|
||||
self.storage.strips.truncate(data.strip_start as usize);
|
||||
self.storage.alphas.truncate(data.alpha_start as usize);
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute a conservative bounding box for the transformed path by computing the bounding box of
|
||||
/// the transformed control points.
|
||||
///
|
||||
/// If `path` is empty, this returns an infinite, inversed [`Rect`] (`left` > `right` and `top` > `bottom`).
|
||||
fn control_point_bbox(path: &BezPath, transform: Affine) -> Rect {
|
||||
// Start with an infinite, inversed rectangle. Adding the first point immediately collapses it
|
||||
// without branching.
|
||||
let mut bbox = Rect::new(
|
||||
f64::INFINITY,
|
||||
f64::INFINITY,
|
||||
f64::NEG_INFINITY,
|
||||
f64::NEG_INFINITY,
|
||||
);
|
||||
for el in path.iter() {
|
||||
match el {
|
||||
PathEl::MoveTo(p) | PathEl::LineTo(p) => {
|
||||
bbox = bbox.union_pt(transform * p);
|
||||
}
|
||||
PathEl::QuadTo(p1, p2) => {
|
||||
bbox = bbox.union_pt(transform * p1);
|
||||
bbox = bbox.union_pt(transform * p2);
|
||||
}
|
||||
PathEl::CurveTo(p1, p2, p3) => {
|
||||
bbox = bbox.union_pt(transform * p1);
|
||||
bbox = bbox.union_pt(transform * p2);
|
||||
bbox = bbox.union_pt(transform * p3);
|
||||
}
|
||||
PathEl::ClosePath => {}
|
||||
}
|
||||
}
|
||||
bbox
|
||||
}
|
||||
|
||||
/// Borrowed data of a stripped path.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct PathDataRef<'a> {
|
||||
/// The strips.
|
||||
pub strips: &'a [Strip],
|
||||
/// The alpha buffer.
|
||||
pub alphas: &'a [u8],
|
||||
|
||||
/// A coarse bounding box of the clip path in pixel coordinates.
|
||||
///
|
||||
/// These bounds have already been intersected with the viewport.
|
||||
pub bbox: RectU16,
|
||||
}
|
||||
|
||||
/// Compute the sparse strips representation of a path that results
|
||||
/// from intersecting the two input paths. This can be used to implement
|
||||
/// clip paths.
|
||||
pub fn intersect(
|
||||
level: Level,
|
||||
path_1: PathDataRef<'_>,
|
||||
path_2: PathDataRef<'_>,
|
||||
target: &mut StripStorage,
|
||||
) {
|
||||
dispatch!(level, simd => intersect_impl(simd, path_1, path_2, target));
|
||||
}
|
||||
|
||||
// PdfCraft patch: rows walked by `intersect_impl` on this thread (tests only).
|
||||
#[cfg(test)]
|
||||
std::thread_local! {
|
||||
static ROWS_WALKED: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// The implementation of the clipping algorithm using sparse strips. Conceptually, it is relatively
|
||||
/// simple: We iterate over each strip and fill region of the two paths in lock step and determine
|
||||
/// all overlaps between the two. For each overlap, we proceed depending on what kind of region
|
||||
/// we have in the first path and the second one.
|
||||
/// - In case we have two fill regions, the overlap region will also be filled.
|
||||
/// - In case we have one strip and one fill region, the overlap region will copy the alpha mask of the strip region.
|
||||
/// - Finally, if we have two strip regions, we combine the alpha masks of both.
|
||||
/// - All regions that are not filled in either path are simply ignored.
|
||||
///
|
||||
/// This is all that this method does. It just looks more complicated as the logic for iterating
|
||||
/// in lock step is a bit tricky.
|
||||
fn intersect_impl<S: Simd>(
|
||||
simd: S,
|
||||
path_1: PathDataRef<'_>,
|
||||
path_2: PathDataRef<'_>,
|
||||
target: &mut StripStorage,
|
||||
) {
|
||||
// In case either path is empty, the clip path should be empty.
|
||||
if path_1.strips.is_empty() || path_2.strips.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore any y values that are outside the bounding box of either of the two paths, as
|
||||
// those are guaranteed to have neither fill nor strip regions.
|
||||
// PdfCraft patch: start at the later first row. Above it one path has no strips, so no row
|
||||
// there yields output, but the walk visited each of them, from the top of the clip, for every
|
||||
// path drawn (on the A1 sample's full-page canvas, about half its strip rows per stroke).
|
||||
let mut cur_y = path_1.strips[0].strip_y().max(path_2.strips[0].strip_y());
|
||||
let end_y = path_1.strips[path_1.strips.len() - 1]
|
||||
.strip_y()
|
||||
.min(path_2.strips[path_2.strips.len() - 1].strip_y());
|
||||
|
||||
let mut path_1_idx = 0;
|
||||
let mut path_2_idx = 0;
|
||||
let mut strip_state = None;
|
||||
|
||||
// Iterate over each strip row and handle them.
|
||||
while cur_y <= end_y {
|
||||
// PdfCraft patch: counts the rows walked, for the test that pins where the walk starts.
|
||||
#[cfg(test)]
|
||||
ROWS_WALKED.with(|rows| rows.set(rows.get() + 1));
|
||||
// For each row, we create two iterators that alternatingly yield the strips and fill
|
||||
// regions in that row, until the last strip has been reached.
|
||||
let mut p1_iter = RowIterator::new(path_1, &mut path_1_idx, cur_y);
|
||||
let mut p2_iter = RowIterator::new(path_2, &mut path_2_idx, cur_y);
|
||||
|
||||
let mut p1_region = p1_iter.next();
|
||||
let mut p2_region = p2_iter.next();
|
||||
|
||||
// If at least one region is none, it means that we reached the end of the row
|
||||
// for that path, meaning that we exceeded the bounding box of that path and no
|
||||
// additional strips should be generated for that row, even if the other path might
|
||||
// still have more strips left. They will all be clipped away. So only consider it
|
||||
// if both paths have a region left.
|
||||
while let (Some(region_1), Some(region_2)) = (p1_region, p2_region) {
|
||||
match region_1.overlap_relationship(®ion_2) {
|
||||
// This means there is no overlap between the regions, so we need to advance
|
||||
// the iterator of the region that is further behind.
|
||||
OverlapRelationship::Advance(advance) => {
|
||||
match advance {
|
||||
Advance::Left => p1_region = p1_iter.next(),
|
||||
Advance::Right => p2_region = p2_iter.next(),
|
||||
};
|
||||
|
||||
continue;
|
||||
}
|
||||
// We have an overlap!
|
||||
OverlapRelationship::Overlap(overlap) => {
|
||||
match (region_1, region_2) {
|
||||
// Both regions are a fill. Flush the current strip and start a new
|
||||
// one at the end of the overlap region setting `fill_gap` to true,
|
||||
// so that the whole area before that will be filled with a sparse
|
||||
// fill.
|
||||
(Region::Fill(_), Region::Fill(_)) => {
|
||||
flush_strip(&mut strip_state, &mut target.strips, cur_y);
|
||||
start_strip(&mut strip_state, &target.alphas, overlap.end, true);
|
||||
}
|
||||
// One fill one strip, so we simply use the alpha mask from the strip region.
|
||||
(Region::Strip(s), Region::Fill(_))
|
||||
| (Region::Fill(_), Region::Strip(s)) => {
|
||||
// If possible, don't create a new strip but just extend the current one.
|
||||
if should_create_new_strip(&strip_state, &target.alphas, overlap.start)
|
||||
{
|
||||
flush_strip(&mut strip_state, &mut target.strips, cur_y);
|
||||
start_strip(&mut strip_state, &target.alphas, overlap.start, false);
|
||||
}
|
||||
|
||||
let s_alphas = &s.alphas[(overlap.start - s.start) as usize * 4..]
|
||||
[..overlap.width() as usize * 4];
|
||||
target.alphas.extend_from_slice(s_alphas);
|
||||
}
|
||||
// Two strips, we need to multiply the opacity masks from both paths.
|
||||
(Region::Strip(s_region_1), Region::Strip(s_region_2)) => {
|
||||
// Once again, only create a new strip if we can't extend the current one.
|
||||
if should_create_new_strip(&strip_state, &target.alphas, overlap.start)
|
||||
{
|
||||
flush_strip(&mut strip_state, &mut target.strips, cur_y);
|
||||
start_strip(&mut strip_state, &target.alphas, overlap.start, false);
|
||||
}
|
||||
|
||||
let num_blocks = overlap.width() / Tile::HEIGHT;
|
||||
|
||||
// Get the right alpha values for the specific position.
|
||||
let s1_alphas = s_region_1.alphas
|
||||
[(overlap.start - s_region_1.start) as usize * 4..]
|
||||
.chunks_exact(16)
|
||||
.take(num_blocks as usize);
|
||||
let s2_alphas = s_region_2.alphas
|
||||
[(overlap.start - s_region_2.start) as usize * 4..]
|
||||
.chunks_exact(16)
|
||||
.take(num_blocks as usize);
|
||||
|
||||
for (s1_alpha, s2_alpha) in s1_alphas.zip(s2_alphas) {
|
||||
let s1 = u8x16::from_slice(simd, s1_alpha);
|
||||
let s2 = u8x16::from_slice(simd, s2_alpha);
|
||||
|
||||
// Combine them.
|
||||
let res = simd.narrow_u16x16(normalized_mul_u8x16(s1, s2));
|
||||
target.alphas.extend(res.as_slice());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Advance the iterator of the path whose region's end is further behind.
|
||||
match overlap.advance {
|
||||
Advance::Left => p1_region = p1_iter.next(),
|
||||
Advance::Right => p2_region = p2_iter.next(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flush the strip before advancing to the next strip row.
|
||||
flush_strip(&mut strip_state, &mut target.strips, cur_y);
|
||||
cur_y += 1;
|
||||
}
|
||||
|
||||
// Push the sentinel strip, if one wasn't already pushed.
|
||||
if !target.strips.last().is_some_and(Strip::is_sentinel) {
|
||||
target.strips.push(Strip::new(
|
||||
u16::MAX,
|
||||
end_y * Tile::HEIGHT,
|
||||
target.alphas.len() as u32,
|
||||
false,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// An overlap between two regions.
|
||||
struct Overlap {
|
||||
/// The start x coordinate.
|
||||
start: u16,
|
||||
/// The end x coordinate.
|
||||
end: u16,
|
||||
/// Whether the left or right region iterator should be advanced next.
|
||||
advance: Advance,
|
||||
}
|
||||
|
||||
impl Overlap {
|
||||
fn width(&self) -> u16 {
|
||||
self.end - self.start
|
||||
}
|
||||
}
|
||||
|
||||
enum Advance {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
/// The relationship between two regions.
|
||||
enum OverlapRelationship {
|
||||
/// There is no overlap between the regions, advance the region iterator on the given side.
|
||||
Advance(Advance),
|
||||
/// There is an overlap between the regions.
|
||||
Overlap(Overlap),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct FillRegion {
|
||||
start: u16,
|
||||
width: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct StripRegion<'a> {
|
||||
start: u16,
|
||||
width: u16,
|
||||
alphas: &'a [u8],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Region<'a> {
|
||||
Fill(FillRegion),
|
||||
Strip(StripRegion<'a>),
|
||||
}
|
||||
|
||||
impl Region<'_> {
|
||||
#[inline(always)]
|
||||
fn start(&self) -> u16 {
|
||||
match self {
|
||||
Region::Fill(fill) => fill.start,
|
||||
Region::Strip(strip) => strip.start,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn width(&self) -> u16 {
|
||||
match self {
|
||||
Region::Fill(fill) => fill.width,
|
||||
Region::Strip(strip) => strip.width,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn end(&self) -> u16 {
|
||||
self.start() + self.width()
|
||||
}
|
||||
|
||||
fn overlap_relationship(&self, other: &Region<'_>) -> OverlapRelationship {
|
||||
if self.end() <= other.start() {
|
||||
OverlapRelationship::Advance(Advance::Left)
|
||||
} else if self.start() >= other.end() {
|
||||
OverlapRelationship::Advance(Advance::Right)
|
||||
} else {
|
||||
let start = self.start().max(other.start());
|
||||
let end = self.end().min(other.end());
|
||||
|
||||
let shift = if self.end() <= other.end() {
|
||||
Advance::Left
|
||||
} else {
|
||||
Advance::Right
|
||||
};
|
||||
|
||||
OverlapRelationship::Overlap(Overlap {
|
||||
advance: shift,
|
||||
start,
|
||||
end,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator of strip and fill regions of a single strip row.
|
||||
struct RowIterator<'a> {
|
||||
/// The path in question.
|
||||
input: PathDataRef<'a>,
|
||||
/// The strip row we want to iterate over.
|
||||
strip_y: u16,
|
||||
/// The index of the current strip.
|
||||
cur_idx: &'a mut usize,
|
||||
/// Whether the iterator should yield a strip next or not.
|
||||
/// When iterating over a row, we alternate between emitting strips and filled regions (unless
|
||||
/// the region between two strips is not filled), so this flag acts as a toggle to store what
|
||||
/// should be yielded next.
|
||||
on_strip: bool,
|
||||
}
|
||||
|
||||
impl<'a> RowIterator<'a> {
|
||||
fn new(input: PathDataRef<'a>, cur_idx: &'a mut usize, strip_y: u16) -> Self {
|
||||
// Forward the index until we have found the right strip.
|
||||
while input.strips[*cur_idx].strip_y() < strip_y {
|
||||
*cur_idx += 1;
|
||||
}
|
||||
|
||||
Self {
|
||||
input,
|
||||
cur_idx,
|
||||
strip_y,
|
||||
on_strip: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn cur_strip(&self) -> &Strip {
|
||||
&self.input.strips[*self.cur_idx]
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn next_strip(&self) -> &Strip {
|
||||
&self.input.strips[*self.cur_idx + 1]
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn cur_strip_width(&self) -> u16 {
|
||||
let cur = self.cur_strip();
|
||||
let next = self.next_strip();
|
||||
((next.alpha_idx() - cur.alpha_idx()) / Tile::HEIGHT as u32) as u16
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn cur_strip_alphas(&self) -> &'a [u8] {
|
||||
let cur = self.cur_strip();
|
||||
let next = self.next_strip();
|
||||
&self.input.alphas[cur.alpha_idx() as usize..next.alpha_idx() as usize]
|
||||
}
|
||||
|
||||
fn cur_strip_fill_area(&self) -> Option<FillRegion> {
|
||||
let next = self.next_strip();
|
||||
|
||||
// Note that if the next strip happens to be on the next line, it will always have
|
||||
// zero winding so we don't need to special case this.
|
||||
if next.fill_gap() {
|
||||
let cur = self.cur_strip();
|
||||
let x = cur.x + self.cur_strip_width();
|
||||
let width = next.x - x;
|
||||
|
||||
(width > 0).then_some(FillRegion { start: x, width })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for RowIterator<'a> {
|
||||
type Item = Region<'a>;
|
||||
|
||||
#[inline(always)]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
// If we are currently not on a strip, we want to yield a filled region in case there is one.
|
||||
if !self.on_strip {
|
||||
// Flip boolean flag so we will yield a strip in the next iteration.
|
||||
self.on_strip = true;
|
||||
|
||||
// if we have a filled area, yield it and return. Otherwise, do nothing and we will
|
||||
// instead yield the next strip below. In any case, we need to advance the current index
|
||||
// so that we point to the next strip now.
|
||||
if let Some(fill_area) = self.cur_strip_fill_area() {
|
||||
*self.cur_idx += 1;
|
||||
|
||||
return Some(Region::Fill(fill_area));
|
||||
} else {
|
||||
*self.cur_idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// If we reached this point, we will yield a strip this iteration, so toggle the flag
|
||||
// so that in the next iteration, we yield a filled region instead.
|
||||
self.on_strip = false;
|
||||
|
||||
// If the current strip is sentinel or not within our target row, terminate.
|
||||
if self.cur_strip().is_sentinel() || self.cur_strip().strip_y() != self.strip_y {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Calculate the dimensions of the strip and yield it.
|
||||
let x = self.cur_strip().x;
|
||||
let width = self.cur_strip_width();
|
||||
let alphas = self.cur_strip_alphas();
|
||||
|
||||
Some(Region::Strip(StripRegion {
|
||||
start: x,
|
||||
width,
|
||||
alphas,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// The data of the current strip we are building.
|
||||
struct StripState {
|
||||
x: u16,
|
||||
alpha_idx: u32,
|
||||
fill_gap: bool,
|
||||
}
|
||||
|
||||
fn flush_strip(strip_state: &mut Option<StripState>, strips: &mut Vec<Strip>, cur_y: u16) {
|
||||
if let Some(state) = core::mem::take(strip_state) {
|
||||
strips.push(Strip::new(
|
||||
state.x,
|
||||
cur_y * Tile::HEIGHT,
|
||||
state.alpha_idx,
|
||||
state.fill_gap,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn start_strip(strip_data: &mut Option<StripState>, alphas: &[u8], x: u16, fill_gap: bool) {
|
||||
*strip_data = Some(StripState {
|
||||
x,
|
||||
alpha_idx: alphas.len() as u32,
|
||||
fill_gap,
|
||||
});
|
||||
}
|
||||
|
||||
fn should_create_new_strip(
|
||||
strip_state: &Option<StripState>,
|
||||
alphas: &[u8],
|
||||
overlap_start: u16,
|
||||
) -> bool {
|
||||
// Returns false in case we can append to the currently built strip.
|
||||
strip_state.as_ref().is_none_or(|state| {
|
||||
let width = ((alphas.len() as u32 - state.alpha_idx) / Tile::HEIGHT as u32) as u16;
|
||||
let strip_end = state.x + width;
|
||||
|
||||
strip_end < overlap_start - 1
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::clip::{PathDataRef, Region, RowIterator, intersect};
|
||||
use crate::geometry::RectU16;
|
||||
use crate::strip::Strip;
|
||||
use crate::strip_generator::StripStorage;
|
||||
use crate::tile::Tile;
|
||||
use fearless_simd::Level;
|
||||
use std::vec;
|
||||
|
||||
#[test]
|
||||
fn intersect_partly_overlapping_strips() {
|
||||
let path_1 = StripBuilder::new().add_strip(0, 0, 32, false).finish();
|
||||
|
||||
let path_2 = StripBuilder::new().add_strip(8, 0, 44, false).finish();
|
||||
|
||||
let expected = StripBuilder::new().add_strip(8, 0, 32, false).finish();
|
||||
|
||||
run_test(expected, path_1, path_2);
|
||||
}
|
||||
|
||||
// PdfCraft patch: the walk starts at the first row both paths have strips in. Starting at the
|
||||
// earlier first row walked every row between the top of the clip and this one for each path.
|
||||
#[test]
|
||||
fn intersect_walks_only_rows_both_paths_have_strips() {
|
||||
let path_1 = StripBuilder::new().add_strip(0, 200, 8, false).finish();
|
||||
let path_2 = StripBuilder::new()
|
||||
.add_strip(0, 0, 8, false)
|
||||
.add_strip(0, 200, 8, false)
|
||||
.finish();
|
||||
let expected = StripBuilder::new().add_strip(0, 200, 8, false).finish();
|
||||
|
||||
super::ROWS_WALKED.with(|rows| rows.set(0));
|
||||
run_test(expected, path_1, path_2);
|
||||
let walked = super::ROWS_WALKED.with(core::cell::Cell::get);
|
||||
assert_eq!(walked, 1, "the walk visited {walked} rows; it should visit only row 200");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_multiple_overlapping_strips() {
|
||||
let path_1 = StripBuilder::new()
|
||||
.add_strip(0, 1, 4, false)
|
||||
.add_strip(12, 1, 20, true)
|
||||
.add_strip(28, 1, 32, false)
|
||||
.add_strip(44, 1, 52, true)
|
||||
.finish();
|
||||
|
||||
let path_2 = StripBuilder::new()
|
||||
.add_strip(4, 1, 8, false)
|
||||
.add_strip(16, 1, 20, true)
|
||||
.add_strip(24, 1, 28, false)
|
||||
.add_strip(32, 1, 36, false)
|
||||
.add_strip(44, 1, 48, true)
|
||||
.finish();
|
||||
|
||||
let expected = StripBuilder::new()
|
||||
.add_strip(4, 1, 8, false)
|
||||
.add_strip(12, 1, 20, true)
|
||||
.add_strip(32, 1, 36, false)
|
||||
.add_strip(44, 1, 48, true)
|
||||
.finish();
|
||||
|
||||
run_test(expected, path_1, path_2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_rows() {
|
||||
let path_1 = StripBuilder::new()
|
||||
.add_strip(0, 0, 4, false)
|
||||
.add_strip(16, 0, 20, true)
|
||||
.add_strip(4, 1, 8, false)
|
||||
.add_strip(12, 1, 24, true)
|
||||
.add_strip(4, 2, 8, false)
|
||||
.add_strip(16, 2, 32, true)
|
||||
.finish();
|
||||
|
||||
let path_2 = StripBuilder::new()
|
||||
.add_strip(0, 2, 4, false)
|
||||
.add_strip(16, 2, 24, true)
|
||||
.add_strip(8, 3, 12, false)
|
||||
.add_strip(16, 3, 28, true)
|
||||
.finish();
|
||||
|
||||
let expected = StripBuilder::new()
|
||||
.add_strip(4, 2, 8, false)
|
||||
.add_strip(16, 2, 24, true)
|
||||
.finish();
|
||||
|
||||
run_test(expected, path_1, path_2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alpha_buffer_correct_width() {
|
||||
let path_1 = StripBuilder::new()
|
||||
.add_strip(0, 0, 4, false)
|
||||
.add_strip(0, 1, 12, false)
|
||||
.finish();
|
||||
|
||||
let path_2 = StripBuilder::new()
|
||||
.add_strip(4, 0, 8, false)
|
||||
.add_strip(0, 1, 4, false)
|
||||
.add_strip(12, 1, 16, true)
|
||||
.finish();
|
||||
|
||||
let expected = StripBuilder::new().add_strip(0, 1, 12, false).finish();
|
||||
|
||||
run_test(expected, path_1, path_2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_iterator_abort_next_line() {
|
||||
let path_1 = StripBuilder::new()
|
||||
.add_strip(0, 0, 4, false)
|
||||
.add_strip(0, 1, 4, false)
|
||||
.finish();
|
||||
|
||||
let path_ref = PathDataRef {
|
||||
strips: &path_1.strips,
|
||||
alphas: &path_1.alphas,
|
||||
bbox: RectU16::new(0, 0, u16::MAX, u16::MAX),
|
||||
};
|
||||
|
||||
let mut idx = 0;
|
||||
let mut iter = RowIterator::new(path_ref, &mut idx, 0);
|
||||
|
||||
assert!(iter.next().is_some());
|
||||
assert!(iter.next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_iterator_sentinel_fill_gap() {
|
||||
let path = StripBuilder::new()
|
||||
.add_strip(0, 0, Tile::WIDTH, false)
|
||||
.finish_with_fill_gap_sentinel();
|
||||
let path_ref = path_ref(&path);
|
||||
|
||||
let mut idx = 0;
|
||||
let mut iter = RowIterator::new(path_ref, &mut idx, 0);
|
||||
|
||||
assert_strip_region(iter.next(), 0, Tile::WIDTH);
|
||||
assert_fill_region(iter.next(), Tile::WIDTH, u16::MAX - Tile::WIDTH);
|
||||
assert!(iter.next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_strip_with_sentinel_fill_gap() {
|
||||
let path_1 = StripBuilder::new()
|
||||
.add_strip(0, 0, Tile::WIDTH, false)
|
||||
.finish_with_fill_gap_sentinel();
|
||||
let path_2 = StripBuilder::new().add_strip(8, 0, 12, false).finish();
|
||||
let expected = StripBuilder::new().add_strip(8, 0, 12, false).finish();
|
||||
|
||||
run_test(expected, path_1, path_2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_two_sentinel_fill_gaps() {
|
||||
let path_1 = StripBuilder::new()
|
||||
.add_strip(0, 0, 8, false)
|
||||
.finish_with_fill_gap_sentinel();
|
||||
let path_2 = StripBuilder::new()
|
||||
.add_strip(4, 0, 12, false)
|
||||
.finish_with_fill_gap_sentinel();
|
||||
let expected = StripBuilder::new()
|
||||
.add_strip(4, 0, 12, false)
|
||||
.finish_with_fill_gap_sentinel();
|
||||
|
||||
run_test(expected, path_1, path_2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_iterator_fill_gap_stops_at_row_boundary() {
|
||||
let mut path = StripBuilder::new()
|
||||
.add_strip(0, 0, 4, false)
|
||||
.finish_with_fill_gap_sentinel();
|
||||
let idx = path.alphas.len();
|
||||
path.strips
|
||||
.push(Strip::new(0, Tile::HEIGHT, idx as u32, false));
|
||||
path.alphas
|
||||
.extend([0; Tile::HEIGHT as usize * Tile::WIDTH as usize]);
|
||||
path.strips.push(Strip::new(
|
||||
u16::MAX,
|
||||
Tile::HEIGHT,
|
||||
path.alphas.len() as u32,
|
||||
false,
|
||||
));
|
||||
|
||||
let path_ref = path_ref(&path);
|
||||
let mut idx = 0;
|
||||
let mut iter = RowIterator::new(path_ref, &mut idx, 0);
|
||||
|
||||
assert_strip_region(iter.next(), 0, 4);
|
||||
assert_fill_region(iter.next(), 4, u16::MAX - 4);
|
||||
assert!(iter.next().is_none());
|
||||
|
||||
let mut iter = RowIterator::new(path_ref, &mut idx, 1);
|
||||
|
||||
assert_strip_region(iter.next(), 0, Tile::WIDTH);
|
||||
assert!(iter.next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_iterator_adjacent_unmerged_strips_no_fill() {
|
||||
let path = StripBuilder::new()
|
||||
.add_strip(0, 0, 4, false)
|
||||
.add_strip(4, 0, 8, false)
|
||||
.finish();
|
||||
let path_ref = path_ref(&path);
|
||||
|
||||
let mut idx = 0;
|
||||
let mut iter = RowIterator::new(path_ref, &mut idx, 0);
|
||||
|
||||
assert_strip_region(iter.next(), 0, 4);
|
||||
assert_strip_region(iter.next(), 4, 4);
|
||||
assert!(iter.next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_iterator_adjacent_unmerged_strips_with_fill_gap() {
|
||||
let path = StripBuilder::new()
|
||||
.add_strip(0, 0, 4, false)
|
||||
.add_strip(4, 0, 8, true)
|
||||
.finish();
|
||||
let path_ref = path_ref(&path);
|
||||
|
||||
let mut idx = 0;
|
||||
let mut iter = RowIterator::new(path_ref, &mut idx, 0);
|
||||
|
||||
assert_strip_region(iter.next(), 0, 4);
|
||||
assert_strip_region(iter.next(), 4, 4);
|
||||
assert!(iter.next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_adjacent_unmerged_strips() {
|
||||
let path = StripBuilder::new()
|
||||
.add_strip(0, 0, 4, false)
|
||||
.add_strip(4, 0, 8, true)
|
||||
.finish();
|
||||
let cover = StripBuilder::new().add_strip(0, 0, 8, false).finish();
|
||||
let expected = StripBuilder::new().add_strip(0, 0, 8, false).finish();
|
||||
|
||||
run_test(expected, path, cover);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_iterator_zero_width_alpha_region() {
|
||||
let mut path = StripStorage::default();
|
||||
path.strips.push(Strip::new(8, 0, 0, false));
|
||||
path.strips.push(Strip::new(16, 0, 0, true));
|
||||
path.strips.push(Strip::new(u16::MAX, 0, 0, false));
|
||||
let path_ref = path_ref(&path);
|
||||
|
||||
let mut idx = 0;
|
||||
let iter = RowIterator::new(path_ref, &mut idx, 0);
|
||||
|
||||
assert_eq!(iter.cur_strip_width(), 0);
|
||||
let fill = iter.cur_strip_fill_area().unwrap();
|
||||
assert_eq!(fill.start, 8);
|
||||
assert_eq!(fill.width, 8);
|
||||
|
||||
let cover = StripBuilder::new().add_strip(0, 0, 20, false).finish();
|
||||
let expected = StripBuilder::new().add_strip(8, 0, 16, false).finish();
|
||||
|
||||
run_test(expected, path, cover);
|
||||
}
|
||||
|
||||
fn run_test(expected: StripStorage, path_1: StripStorage, path_2: StripStorage) {
|
||||
let mut write_target = StripStorage::default();
|
||||
|
||||
let path_1 = path_ref(&path_1);
|
||||
let path_2 = path_ref(&path_2);
|
||||
|
||||
intersect(Level::new(), path_1, path_2, &mut write_target);
|
||||
|
||||
assert_eq!(write_target, expected);
|
||||
}
|
||||
|
||||
fn path_ref(path: &StripStorage) -> PathDataRef<'_> {
|
||||
PathDataRef {
|
||||
strips: &path.strips,
|
||||
alphas: &path.alphas,
|
||||
bbox: RectU16::new(0, 0, u16::MAX, u16::MAX),
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_strip_region(region: Option<Region<'_>>, start: u16, width: u16) {
|
||||
match region {
|
||||
Some(Region::Strip(strip)) => {
|
||||
assert_eq!(strip.start, start);
|
||||
assert_eq!(strip.width, width);
|
||||
assert_eq!(strip.alphas.len(), (width * Tile::HEIGHT) as usize);
|
||||
}
|
||||
other => panic!("expected strip region, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_fill_region(region: Option<Region<'_>>, start: u16, width: u16) {
|
||||
match region {
|
||||
Some(Region::Fill(fill)) => {
|
||||
assert_eq!(fill.start, start);
|
||||
assert_eq!(fill.width, width);
|
||||
}
|
||||
other => panic!("expected fill region, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
struct StripBuilder {
|
||||
storage: StripStorage,
|
||||
}
|
||||
|
||||
impl StripBuilder {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
storage: StripStorage::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_strip(self, x: u16, strip_y: u16, end: u16, fill_gap: bool) -> Self {
|
||||
let width = end - x;
|
||||
self.add_strip_with(
|
||||
x,
|
||||
strip_y,
|
||||
end,
|
||||
fill_gap,
|
||||
&vec![0; (width * Tile::HEIGHT) as usize],
|
||||
)
|
||||
}
|
||||
|
||||
fn add_strip_with(
|
||||
mut self,
|
||||
x: u16,
|
||||
strip_y: u16,
|
||||
end: u16,
|
||||
fill_gap: bool,
|
||||
alphas: &[u8],
|
||||
) -> Self {
|
||||
let width = end - x;
|
||||
assert_eq!(alphas.len(), (width * Tile::HEIGHT) as usize);
|
||||
let idx = self.storage.alphas.len();
|
||||
self.storage
|
||||
.strips
|
||||
.push(Strip::new(x, strip_y * Tile::HEIGHT, idx as u32, fill_gap));
|
||||
self.storage.alphas.extend_from_slice(alphas);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
fn finish(mut self) -> StripStorage {
|
||||
let last_y = self.storage.strips.last().unwrap().y;
|
||||
let idx = self.storage.alphas.len();
|
||||
|
||||
self.storage
|
||||
.strips
|
||||
.push(Strip::new(u16::MAX, last_y, idx as u32, false));
|
||||
|
||||
self.storage
|
||||
}
|
||||
|
||||
fn finish_with_fill_gap_sentinel(mut self) -> StripStorage {
|
||||
let last_y = self.storage.strips.last().unwrap().y;
|
||||
let idx = self.storage.alphas.len();
|
||||
|
||||
self.storage
|
||||
.strips
|
||||
.push(Strip::new(u16::MAX, last_y, idx as u32, true));
|
||||
|
||||
self.storage
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+2385
File diff suppressed because it is too large
Load Diff
Vendored
+1354
File diff suppressed because it is too large
Load Diff
+86
@@ -0,0 +1,86 @@
|
||||
// Copyright 2026 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! The drop shadow filter.
|
||||
|
||||
use crate::color::{AlphaColor, Srgb};
|
||||
use crate::filter::gaussian_blur::{MAX_KERNEL_SIZE, plan_decimated_blur, transform_blur_params};
|
||||
use crate::filter::transform_offset_params;
|
||||
use crate::filter_effects::EdgeMode;
|
||||
use crate::kurbo::Affine;
|
||||
|
||||
/// A drop shadow filter.
|
||||
#[derive(Debug)]
|
||||
pub struct DropShadow {
|
||||
/// The x-offset of the shadow.
|
||||
pub dx: f32,
|
||||
/// The y-offset of the shadow.
|
||||
pub dy: f32,
|
||||
/// The color of the shadow.
|
||||
pub color: AlphaColor<Srgb>,
|
||||
/// Standard deviation for the blur (for reference/debugging).
|
||||
pub std_deviation: f32,
|
||||
/// Edge mode for blur sampling.
|
||||
pub edge_mode: EdgeMode,
|
||||
/// Number of 2x2 decimation levels to use (0 means direct convolution).
|
||||
pub n_decimations: usize,
|
||||
/// Pre-computed Gaussian kernel weights for the reduced blur.
|
||||
/// Only the first `kernel_size` elements are valid.
|
||||
pub kernel: [f32; MAX_KERNEL_SIZE],
|
||||
/// Actual length of the kernel (kernel is padded to `MAX_KERNEL_SIZE`).
|
||||
pub kernel_size: u8,
|
||||
}
|
||||
|
||||
impl DropShadow {
|
||||
/// Create a new drop shadow filter with the specified parameters.
|
||||
///
|
||||
/// This precomputes the blur decimation plan and kernel for optimal performance.
|
||||
pub fn new(
|
||||
dx: f32,
|
||||
dy: f32,
|
||||
std_deviation: f32,
|
||||
edge_mode: EdgeMode,
|
||||
color: AlphaColor<Srgb>,
|
||||
) -> Self {
|
||||
// Precompute blur plan (same logic as GaussianBlur::new)
|
||||
let (n_decimations, kernel, kernel_size) = plan_decimated_blur(std_deviation);
|
||||
|
||||
Self {
|
||||
dx,
|
||||
dy,
|
||||
color,
|
||||
std_deviation,
|
||||
edge_mode,
|
||||
n_decimations,
|
||||
kernel,
|
||||
kernel_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Transform a drop shadow's offset and standard deviation using the affine transformation.
|
||||
///
|
||||
/// Applies the full linear transformation (rotation, scale, and shear) to the offset vector,
|
||||
/// and scales the blur standard deviation uniformly.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `dx` - Horizontal offset in user space
|
||||
/// * `dy` - Vertical offset in user space
|
||||
/// * `std_deviation` - Blur standard deviation in user space
|
||||
/// * `transform` - The transformation matrix to apply
|
||||
///
|
||||
/// # Returns
|
||||
/// A tuple of (`scaled_dx`, `scaled_dy`, `scaled_std_dev`) in device space
|
||||
pub(crate) fn transform_shadow_params(
|
||||
dx: f32,
|
||||
dy: f32,
|
||||
std_deviation: f32,
|
||||
transform: &Affine,
|
||||
) -> (f32, f32, f32) {
|
||||
let (scaled_dx, scaled_dy) = transform_offset_params(dx, dy, transform);
|
||||
|
||||
// Scale the blur radius uniformly
|
||||
let scaled_std_dev = transform_blur_params(std_deviation, transform);
|
||||
|
||||
(scaled_dx, scaled_dy, scaled_std_dev)
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// Copyright 2026 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! The flood filter.
|
||||
|
||||
use crate::color::{AlphaColor, Srgb};
|
||||
|
||||
/// A flood filter.
|
||||
#[derive(Debug)]
|
||||
pub struct Flood {
|
||||
/// The flood color.
|
||||
pub color: AlphaColor<Srgb>,
|
||||
}
|
||||
|
||||
impl Flood {
|
||||
/// Create a new flood filter with the specified color.
|
||||
pub fn new(color: AlphaColor<Srgb>) -> Self {
|
||||
Self { color }
|
||||
}
|
||||
}
|
||||
+334
@@ -0,0 +1,334 @@
|
||||
// Copyright 2026 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! The gaussian blur filter.
|
||||
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::filter_effects::EdgeMode;
|
||||
use crate::kurbo::Affine;
|
||||
use crate::util::extract_scales;
|
||||
use core::f32::consts::E;
|
||||
#[cfg(not(feature = "std"))]
|
||||
use peniko::kurbo::common::FloatFuncs as _;
|
||||
|
||||
/// Scale a blur's standard deviation uniformly based on the transformation.
|
||||
///
|
||||
/// Extracts the scale factors from the transformation matrix using SVD and
|
||||
/// averages them to get a uniform scale factor for the blur radius.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `std_deviation` - The blur standard deviation in user space
|
||||
/// * `transform` - The transformation matrix to extract scale from
|
||||
///
|
||||
/// # Returns
|
||||
/// The scaled standard deviation in device space
|
||||
pub(crate) fn transform_blur_params(std_deviation: f32, transform: &Affine) -> f32 {
|
||||
let (scale_x, scale_y) = extract_scales(transform);
|
||||
let uniform_scale = (scale_x + scale_y) / 2.0;
|
||||
// TODO: Support separate std_deviation for x and y axes (std_deviation_x, std_deviation_y)
|
||||
// to properly handle non-uniform scaling. This would eliminate the need for uniform_scale
|
||||
// and allow blur to scale independently along each axis.
|
||||
std_deviation * uniform_scale
|
||||
}
|
||||
|
||||
/// Maximum size of the Gaussian kernel (must be odd and equal to or smaller than [`u8::MAX`]).
|
||||
///
|
||||
/// The multi-scale decimation algorithm guarantees that kernel size never exceeds this value.
|
||||
/// Decimation stops when remaining variance ≤ 4.0 (σ ≤ 2.0), which produces kernels of size
|
||||
/// at most 13 (radius = ceil(3σ) = 6, size = 1 + 2×6 = 13).
|
||||
// Keep in sync with MAX_KERNEL_SIZE in vello_sparse_shaders/shaders/filters.wgsl
|
||||
pub const MAX_KERNEL_SIZE: usize = 13;
|
||||
|
||||
#[cfg(test)]
|
||||
const _: () = const {
|
||||
if MAX_KERNEL_SIZE.is_multiple_of(2) {
|
||||
panic!("`MAX_KERNEL_SIZE` must be odd");
|
||||
}
|
||||
if MAX_KERNEL_SIZE > u8::MAX as usize {
|
||||
panic!("`MAX_KERNEL_SIZE` must be less than or equal to `u8::MAX`");
|
||||
}
|
||||
};
|
||||
|
||||
/// A gaussian blur.
|
||||
#[derive(Debug)]
|
||||
pub struct GaussianBlur {
|
||||
/// The standard deviation.
|
||||
pub std_deviation: f32,
|
||||
/// Number of 2× decimation levels to use (0 means no decimation, direct convolution).
|
||||
pub n_decimations: usize,
|
||||
/// Pre-computed Gaussian kernel weights for the reduced blur.
|
||||
/// Only the first `kernel_size` elements are valid.
|
||||
pub kernel: [f32; MAX_KERNEL_SIZE],
|
||||
/// Actual length of the kernel (rest is padding up to `MAX_KERNEL_SIZE`).
|
||||
pub kernel_size: u8,
|
||||
/// Edge mode for handling out-of-bounds sampling.
|
||||
pub edge_mode: EdgeMode,
|
||||
}
|
||||
|
||||
impl GaussianBlur {
|
||||
/// Create a new Gaussian blur filter with the specified standard deviation.
|
||||
///
|
||||
/// This precomputes the decimation plan, kernel, and radius for optimal performance.
|
||||
pub fn new(std_deviation: f32, edge_mode: EdgeMode) -> Self {
|
||||
let (n_decimations, kernel, kernel_size) = plan_decimated_blur(std_deviation);
|
||||
|
||||
Self {
|
||||
std_deviation,
|
||||
edge_mode,
|
||||
n_decimations,
|
||||
kernel,
|
||||
kernel_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the blur execution plan based on standard deviation.
|
||||
///
|
||||
/// Returns (`n_decimations`, `kernel`, `kernel_size`):
|
||||
/// - `n_decimations`: Number of 2× downsampling steps to perform (per axis)
|
||||
/// - `kernel`: Pre-computed Gaussian kernel weights (fixed-size array)
|
||||
/// - `kernel_size`: Actual length of the kernel (rest is zero-padded)
|
||||
pub fn plan_decimated_blur(std_deviation: f32) -> (usize, [f32; MAX_KERNEL_SIZE], u8) {
|
||||
if std_deviation <= 0.0 {
|
||||
// Invalid standard deviation, return identity kernel (no blur)
|
||||
let mut kernel = [0.0; MAX_KERNEL_SIZE];
|
||||
kernel[0] = 1.0;
|
||||
return (0, kernel, 1);
|
||||
}
|
||||
|
||||
// Compute decimation plan using variance analysis.
|
||||
// Variance (σ²) has the additive property: applying two blurs sequentially
|
||||
// adds their variances together. We use this to decompose the blur.
|
||||
//
|
||||
// Mathematical Foundation: From probability theory, convolving two Gaussians
|
||||
// G(σ₁) ⊗ G(σ₂) = G(√(σ₁² + σ₂²)). This means variance is additive: σ²_total = σ²_1 + σ²_2.
|
||||
// Rearranging: σ²_2 = σ²_total - σ²_1, allowing us to decompose the target blur.
|
||||
let variance = std_deviation * std_deviation;
|
||||
let mut n_decimations = 0;
|
||||
let mut remaining_variance = variance;
|
||||
|
||||
// Each decimation step:
|
||||
// 1. Applies [1,3,3,1] blur which adds variance = 0.75 per axis
|
||||
// 2. Downsamples by 2× per axis, adding variance = 0.75 × 4 = 3.0 total
|
||||
// 3. The 2× downsampling scales the remaining variance by 0.25 (= 1/2²)
|
||||
// Stop when remaining variance ≤ 4.0 (σ ≈ 2), as further decimation isn't beneficial.
|
||||
while remaining_variance > 4.0 {
|
||||
remaining_variance = (remaining_variance - 3.0) * 0.25;
|
||||
n_decimations += 1;
|
||||
}
|
||||
// Compute the reduced standard deviation to apply at the decimated resolution
|
||||
let remaining_sigma = remaining_variance.sqrt();
|
||||
// Compute Gaussian kernel for the reduced blur
|
||||
let (kernel, kernel_size) = compute_gaussian_kernel(remaining_sigma);
|
||||
|
||||
(n_decimations, kernel, kernel_size)
|
||||
}
|
||||
|
||||
/// Compute 1D Gaussian kernel weights for separable convolution.
|
||||
///
|
||||
/// Returns (`kernel_weights`, `kernel_size`) where `kernel_size = 2×radius + 1`.
|
||||
/// The kernel is stored in a fixed-size array to avoid heap allocation.
|
||||
/// Uses the standard Gaussian formula: G(x) = exp(-x² / (2σ²)), normalized to sum to 1.
|
||||
pub fn compute_gaussian_kernel(std_deviation: f32) -> ([f32; MAX_KERNEL_SIZE], u8) {
|
||||
// Use radius = 3σ to capture 99.7% of the Gaussian distribution.
|
||||
// Beyond ±3σ, the Gaussian values are negligible (<0.3%).
|
||||
let radius = (3.0 * std_deviation).ceil() as usize;
|
||||
let kernel_size = (1 + radius * 2).min(MAX_KERNEL_SIZE) as u8;
|
||||
|
||||
let mut kernel = [0.0; MAX_KERNEL_SIZE];
|
||||
// Compute Gaussian weights using the formula: G(x) = exp(-x² / (2σ²))
|
||||
// This creates a symmetric bell curve centered at the middle of the kernel.
|
||||
let gaussian_denominator = 2.0 * std_deviation * std_deviation;
|
||||
let mut sum = 0.0;
|
||||
let kernel_center = (kernel_size / 2) as f32;
|
||||
for (i, weight) in kernel.iter_mut().enumerate().take(usize::from(kernel_size)) {
|
||||
// Compute distance from center (0 at center, increases outward)
|
||||
let x = (i as f32) - kernel_center;
|
||||
// Apply Gaussian formula: weight decreases exponentially with squared distance
|
||||
*weight = E.powf(-x * x / gaussian_denominator);
|
||||
sum += *weight;
|
||||
}
|
||||
|
||||
// Normalize weights to sum to 1.0, ensuring the blur doesn't change overall brightness.
|
||||
// Without normalization, blurring a uniform gray area could make it brighter/darker.
|
||||
let scale = 1.0 / sum;
|
||||
for weight in kernel.iter_mut().take(usize::from(kernel_size)) {
|
||||
*weight *= scale;
|
||||
}
|
||||
|
||||
(kernel, kernel_size)
|
||||
}
|
||||
|
||||
/// Tracks dimensions through a chain of downscale/upscale operations.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DecimationSizer {
|
||||
width: u16,
|
||||
height: u16,
|
||||
dim_stack: Vec<(u16, u16)>,
|
||||
}
|
||||
|
||||
impl DecimationSizer {
|
||||
/// Create a new sizer with the given initial dimensions.
|
||||
#[inline]
|
||||
pub fn new(width: u16, height: u16) -> Self {
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
dim_stack: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the sizer so it can be reused.
|
||||
#[inline]
|
||||
pub fn reset(&mut self, width: u16, height: u16) {
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
self.dim_stack.clear();
|
||||
}
|
||||
|
||||
/// Returns the current logical dimensions.
|
||||
#[inline]
|
||||
pub fn current(&self) -> (u16, u16) {
|
||||
(self.width, self.height)
|
||||
}
|
||||
|
||||
/// Apply a new downscale operation.
|
||||
#[inline]
|
||||
pub fn downscale(&mut self) -> (u16, u16) {
|
||||
self.dim_stack.push((self.width, self.height));
|
||||
self.width = self.width.div_ceil(2);
|
||||
self.height = self.height.div_ceil(2);
|
||||
(self.width, self.height)
|
||||
}
|
||||
|
||||
/// Apply a new upscale operation.
|
||||
#[inline]
|
||||
pub fn upscale(&mut self) -> (u16, u16) {
|
||||
let (target_w, target_h) = self.dim_stack.pop().unwrap();
|
||||
// Clamp because upscale can exceed target on odd dimensions (e.g., 5→3→6 > 5)
|
||||
self.width = (self.width * 2).min(target_w);
|
||||
self.height = (self.height * 2).min(target_h);
|
||||
(self.width, self.height)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::filter::gaussian_blur::{
|
||||
DecimationSizer, compute_gaussian_kernel, plan_decimated_blur,
|
||||
};
|
||||
|
||||
/// Test Gaussian kernel computation for small σ.
|
||||
#[test]
|
||||
fn test_gaussian_kernel_small_sigma() {
|
||||
let (kernel, size) = compute_gaussian_kernel(1.0);
|
||||
// For σ=1.0, radius = ceil(3.0) = 3, size = 2*3+1 = 7
|
||||
assert_eq!(size, 7);
|
||||
|
||||
// Kernel should be symmetric
|
||||
for i in 0..size / 2 {
|
||||
assert!((kernel[usize::from(i)] - kernel[usize::from(size - 1 - i)]).abs() < 1e-6);
|
||||
}
|
||||
|
||||
// Kernel should sum to 1.0 (normalized)
|
||||
let sum: f32 = kernel.iter().take(usize::from(size)).sum();
|
||||
assert!((sum - 1.0).abs() < 1e-6);
|
||||
|
||||
// Center should be the largest weight
|
||||
let center_idx = size / 2;
|
||||
for i in 0..size {
|
||||
if i != center_idx {
|
||||
assert!(kernel[usize::from(center_idx)] >= kernel[usize::from(i)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test Gaussian kernel computation for very small σ (near-zero).
|
||||
#[test]
|
||||
fn test_gaussian_kernel_very_small_sigma() {
|
||||
let (kernel, size) = compute_gaussian_kernel(0.1);
|
||||
// For σ=0.1, radius = ceil(0.3) = 1, size = 3
|
||||
assert_eq!(size, 3);
|
||||
// Should sum to 1.0
|
||||
let sum: f32 = kernel.iter().take(usize::from(size)).sum();
|
||||
assert!((sum - 1.0).abs() < 1e-6);
|
||||
// Center weight should be dominant for very small σ
|
||||
assert!(kernel[1] > 0.9); // Center is highly weighted
|
||||
}
|
||||
|
||||
/// Test Gaussian kernel for fractional σ.
|
||||
#[test]
|
||||
fn test_gaussian_kernel_fractional_sigma() {
|
||||
let (kernel, size) = compute_gaussian_kernel(0.5);
|
||||
// For σ=0.5, radius = ceil(1.5) = 2, size = 5
|
||||
assert_eq!(size, 5);
|
||||
|
||||
// Should still sum to 1.0
|
||||
let sum: f32 = kernel.iter().take(usize::from(size)).sum();
|
||||
assert!((sum - 1.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
/// Test decimation plan for small blur (no decimation).
|
||||
#[test]
|
||||
fn test_plan_no_decimation() {
|
||||
let (n_decimations, _kernel, _size) = plan_decimated_blur(1.0);
|
||||
// σ=1.0 → variance=1.0, should not decimate
|
||||
assert_eq!(n_decimations, 0);
|
||||
}
|
||||
|
||||
/// Test decimation plan for medium blur (some decimation).
|
||||
#[test]
|
||||
fn test_plan_with_decimation() {
|
||||
let (n_decimations, _kernel, _size) = plan_decimated_blur(5.0);
|
||||
// σ=5.0 → variance=25.0, should decimate
|
||||
assert_eq!(n_decimations, 2);
|
||||
}
|
||||
|
||||
/// Test decimation plan at boundary (σ=2.0).
|
||||
#[test]
|
||||
fn test_plan_decimation_boundary() {
|
||||
let (n_decimations, _kernel, _size) = plan_decimated_blur(2.0);
|
||||
// σ=2.0 → variance=4.0, right at the boundary
|
||||
assert_eq!(n_decimations, 0);
|
||||
}
|
||||
|
||||
/// Test decimation plan for negative σ (invalid, should return identity).
|
||||
#[test]
|
||||
fn test_plan_negative_sigma() {
|
||||
let (n_decimations, kernel, size) = plan_decimated_blur(-1.0);
|
||||
assert_eq!(n_decimations, 0);
|
||||
assert_eq!(size, 1);
|
||||
assert!((kernel[0] - 1.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decimation_sizer_even() {
|
||||
let mut sizer = DecimationSizer::new(8, 8);
|
||||
assert_eq!(sizer.current(), (8, 8));
|
||||
|
||||
assert_eq!(sizer.downscale(), (4, 4));
|
||||
assert_eq!(sizer.downscale(), (2, 2));
|
||||
|
||||
assert_eq!(sizer.upscale(), (4, 4));
|
||||
assert_eq!(sizer.upscale(), (8, 8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decimation_sizer_odd() {
|
||||
let mut sizer = DecimationSizer::new(5, 7);
|
||||
assert_eq!(sizer.downscale(), (3, 4));
|
||||
assert_eq!(sizer.downscale(), (2, 2));
|
||||
|
||||
// Upscale clamps to the pre-downscale target
|
||||
assert_eq!(sizer.upscale(), (3, 4));
|
||||
assert_eq!(sizer.upscale(), (5, 7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decimation_sizer_single_level() {
|
||||
let mut sizer = DecimationSizer::new(100, 50);
|
||||
assert_eq!(sizer.downscale(), (50, 25));
|
||||
assert_eq!(sizer.upscale(), (100, 50));
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
// Copyright 2026 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Common filter helper functions.
|
||||
//!
|
||||
//! Unlike the filters defines in [`crate::filter_effects`], the filters in this module
|
||||
//! represent a special representation of each filter to be used as the basis for rendering in
|
||||
//! `vello_hybrid` and `vello_cpu`.
|
||||
|
||||
use crate::filter::drop_shadow::{DropShadow, transform_shadow_params};
|
||||
use crate::filter::flood::Flood;
|
||||
use crate::filter::gaussian_blur::{GaussianBlur, transform_blur_params};
|
||||
use crate::filter::offset::Offset;
|
||||
use crate::filter_effects::{Filter, FilterPrimitive};
|
||||
use crate::kurbo::{Affine, Vec2};
|
||||
|
||||
pub mod drop_shadow;
|
||||
pub mod flood;
|
||||
pub mod gaussian_blur;
|
||||
pub mod offset;
|
||||
|
||||
/// A filter that has been prepared for rendering.
|
||||
#[derive(Debug)]
|
||||
pub enum PreparedFilter {
|
||||
/// A flood filter.
|
||||
Flood(Flood),
|
||||
/// A gaussian blur filter.
|
||||
GaussianBlur(GaussianBlur),
|
||||
/// An offset filter.
|
||||
Offset(Offset),
|
||||
/// A drop shadow filter.
|
||||
DropShadow(DropShadow),
|
||||
}
|
||||
|
||||
impl PreparedFilter {
|
||||
/// Build a new prepared filter for the given transform.
|
||||
pub fn new(filter: &Filter, transform: &Affine) -> Self {
|
||||
// Multi-primitive filter graphs are not yet implemented.
|
||||
if filter.graph.primitives.len() != 1 {
|
||||
unimplemented!("Multi-primitive filter graphs are not yet supported");
|
||||
}
|
||||
|
||||
match &filter.graph.primitives[0] {
|
||||
FilterPrimitive::Flood { color } => {
|
||||
let flood = Flood::new(*color);
|
||||
Self::Flood(flood)
|
||||
}
|
||||
FilterPrimitive::GaussianBlur {
|
||||
std_deviation,
|
||||
edge_mode,
|
||||
} => {
|
||||
let scaled_std_dev = transform_blur_params(*std_deviation, transform);
|
||||
let blur = GaussianBlur::new(scaled_std_dev, *edge_mode);
|
||||
Self::GaussianBlur(blur)
|
||||
}
|
||||
FilterPrimitive::DropShadow {
|
||||
dx,
|
||||
dy,
|
||||
std_deviation,
|
||||
color,
|
||||
edge_mode,
|
||||
} => {
|
||||
let (scaled_dx, scaled_dy, scaled_std_dev) =
|
||||
transform_shadow_params(*dx, *dy, *std_deviation, transform);
|
||||
let drop_shadow =
|
||||
DropShadow::new(scaled_dx, scaled_dy, scaled_std_dev, *edge_mode, *color);
|
||||
|
||||
Self::DropShadow(drop_shadow)
|
||||
}
|
||||
FilterPrimitive::Offset { dx, dy } => {
|
||||
let (scaled_dx, scaled_dy) = transform_offset_params(*dx, *dy, transform);
|
||||
let offset = Offset::new(scaled_dx, scaled_dy);
|
||||
|
||||
Self::Offset(offset)
|
||||
}
|
||||
_ => {
|
||||
// Other primitives like Blend, ColorMatrix, ComponentTransfer, etc.
|
||||
// are not yet implemented
|
||||
unimplemented!("Other filter primitives not yet implemented");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Transform an offset's dx/dy using the affine transformation's linear part.
|
||||
///
|
||||
/// # Returns
|
||||
/// A tuple of (`scaled_dx`, `scaled_dy`) in device space.
|
||||
fn transform_offset_params(dx: f32, dy: f32, transform: &Affine) -> (f32, f32) {
|
||||
let offset = Vec2::new(dx as f64, dy as f64);
|
||||
let [a, b, c, d, _, _] = transform.as_coeffs();
|
||||
let transformed_offset = Vec2::new(a * offset.x + c * offset.y, b * offset.x + d * offset.y);
|
||||
(transformed_offset.x as f32, transformed_offset.y as f32)
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
// Copyright 2026 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! The offset filter.
|
||||
|
||||
/// Translation/shift filter.
|
||||
///
|
||||
/// This shifts the input image by `(dx, dy)` in device pixel space.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Offset {
|
||||
/// The x-offset that should be applied.
|
||||
pub dx: f32,
|
||||
/// The y-offset that should be applied.
|
||||
pub dy: f32,
|
||||
}
|
||||
|
||||
impl Offset {
|
||||
/// Create a new offset filter.
|
||||
pub fn new(dx: f32, dy: f32) -> Self {
|
||||
Self { dx, dy }
|
||||
}
|
||||
}
|
||||
+1089
File diff suppressed because it is too large
Load Diff
Vendored
+286
@@ -0,0 +1,286 @@
|
||||
// Copyright 2025 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Flattening filled and stroked paths.
|
||||
|
||||
use crate::flatten_simd::{Callback, LinePathEl};
|
||||
use crate::geometry::RectU16;
|
||||
use crate::kurbo::{self, Affine, PathEl, Stroke, StrokeCtx, StrokeOpts};
|
||||
use alloc::vec::Vec;
|
||||
use fearless_simd::{Level, Simd, dispatch};
|
||||
use log::warn;
|
||||
|
||||
pub use crate::flatten_simd::FlattenCtx;
|
||||
|
||||
// The current tolerance is set to 0.25. Since `sqrt` doesn't work in const contexts, we instead
|
||||
// hardcode the squared tolerance and derive the others from that.
|
||||
pub(crate) const SQRT_TOL: f64 = 0.5;
|
||||
pub(crate) const TOL: f64 = SQRT_TOL * SQRT_TOL;
|
||||
pub(crate) const TOL_2: f64 = TOL * TOL;
|
||||
|
||||
/// A point.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Point {
|
||||
/// The x coordinate of the point.
|
||||
pub x: f32,
|
||||
/// The y coordinate of the point.
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
impl Point {
|
||||
/// The point `(0, 0)`.
|
||||
pub const ZERO: Self = Self::new(0., 0.);
|
||||
|
||||
/// Create a new point.
|
||||
pub const fn new(x: f32, y: f32) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<kurbo::Point> for Point {
|
||||
#[inline(always)]
|
||||
fn from(value: kurbo::Point) -> Self {
|
||||
Self {
|
||||
x: value.x as f32,
|
||||
y: value.y as f32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::Add for Point {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: Self) -> Self {
|
||||
Self::new(self.x + rhs.x, self.y + rhs.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::Sub for Point {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, rhs: Self) -> Self {
|
||||
Self::new(self.x - rhs.x, self.y - rhs.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::Mul<f32> for Point {
|
||||
type Output = Self;
|
||||
|
||||
fn mul(self, rhs: f32) -> Self {
|
||||
Self::new(self.x * rhs, self.y * rhs)
|
||||
}
|
||||
}
|
||||
|
||||
/// A line.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Line {
|
||||
/// The start point of the line.
|
||||
pub p0: Point,
|
||||
/// The end point of the line.
|
||||
pub p1: Point,
|
||||
}
|
||||
|
||||
impl Line {
|
||||
/// Create a new line.
|
||||
pub fn new(p0: Point, p1: Point) -> Self {
|
||||
Self { p0, p1 }
|
||||
}
|
||||
}
|
||||
|
||||
/// Flatten a filled Bézier path into line segments.
|
||||
///
|
||||
/// # Open subpaths and culling
|
||||
///
|
||||
/// Open subpaths in the input path get closed by connecting the last endpoint in the subpath to
|
||||
/// the starting point. The output lines in `line_buf` describe the flattened path, but these lines
|
||||
/// may describe open subpaths, as some path elements may have been culled.
|
||||
///
|
||||
/// For example, consider the following, where the box describes the viewport, a path is marked by
|
||||
/// `*`, and the region to be filled in the viewport is shaded. For ease of drawing the ASCII art,
|
||||
/// the path elements are all lines ([`PathEl::LineTo`]), but the same also holds for Bézier path
|
||||
/// elements.
|
||||
///
|
||||
/// ```text
|
||||
/// ---> winding scan direction
|
||||
///
|
||||
/// * * * * *
|
||||
/// * *
|
||||
/// ---------- * ----- *
|
||||
/// | *░░░░░░░| *
|
||||
/// | *░░░░░░░░░| *
|
||||
/// | *░░░░░░░░░░░| *
|
||||
/// | *░░░░░░░░░░░░░| *
|
||||
/// |*░░░░░░░░░░░░░░░| *
|
||||
/// *|░░░░░░░░░░░░░░░░| *
|
||||
/// * |░░░░░░░░░░░░░░░░| *
|
||||
/// * |░░░░░░░░░░░░░░░░| *
|
||||
/// * ------------------ *
|
||||
/// * *
|
||||
/// * *
|
||||
/// * *
|
||||
/// * *
|
||||
/// * *
|
||||
/// * *
|
||||
/// * *
|
||||
/// * * * * * * * * * * * * * *
|
||||
/// ```
|
||||
///
|
||||
/// Because the winding scan direction is from left to right, only the left-of-viewport and
|
||||
/// diagonal lines matter in later stages of rendering for the winding number and pixel coverage.
|
||||
/// The other three lines can be culled.
|
||||
///
|
||||
/// ```text
|
||||
/// *
|
||||
/// *
|
||||
/// ---------- * -----
|
||||
/// | *░░░░░░░|
|
||||
/// | *░░░░░░░░░|
|
||||
/// | *░░░░░░░░░░░|
|
||||
/// | *░░░░░░░░░░░░░|
|
||||
/// |*░░░░░░░░░░░░░░░|
|
||||
/// *|░░░░░░░░░░░░░░░░|
|
||||
/// * |░░░░░░░░░░░░░░░░|
|
||||
/// * |░░░░░░░░░░░░░░░░|
|
||||
/// * ------------------
|
||||
/// *
|
||||
/// *
|
||||
/// *
|
||||
/// *
|
||||
/// *
|
||||
/// *
|
||||
/// *
|
||||
/// ```
|
||||
///
|
||||
/// It is important to keep these flattened subpaths open after culling, as closing the subpaths
|
||||
/// might yield different geometry like the following.
|
||||
///
|
||||
/// ```text
|
||||
/// *
|
||||
/// **
|
||||
/// ---------- * *----
|
||||
/// | *░░* |
|
||||
/// | *░░░* |
|
||||
/// | *░░░░* |
|
||||
/// | *░░░░░* |
|
||||
/// |*░░░░░░* |
|
||||
/// *|░░░░░░* |
|
||||
/// * |░░░░░* |
|
||||
/// * |░░░░* |
|
||||
/// * --- * ------------
|
||||
/// * *
|
||||
/// * *
|
||||
/// * *
|
||||
/// * *
|
||||
/// * *
|
||||
/// **
|
||||
/// *
|
||||
/// ```
|
||||
pub fn fill(
|
||||
level: Level,
|
||||
path: impl IntoIterator<Item = PathEl>,
|
||||
affine: Affine,
|
||||
line_buf: &mut Vec<Line>,
|
||||
ctx: &mut FlattenCtx,
|
||||
cull_bbox: RectU16,
|
||||
) {
|
||||
dispatch!(level, simd => fill_impl(simd, path, affine, line_buf, ctx, cull_bbox));
|
||||
}
|
||||
|
||||
/// Flatten a filled bezier path into line segments.
|
||||
///
|
||||
/// See the note about open subpaths and culling on [`fill`].
|
||||
#[inline(always)]
|
||||
pub fn fill_impl<S: Simd>(
|
||||
simd: S,
|
||||
path: impl IntoIterator<Item = PathEl>,
|
||||
affine: Affine,
|
||||
line_buf: &mut Vec<Line>,
|
||||
flatten_ctx: &mut FlattenCtx,
|
||||
cull_bbox: RectU16,
|
||||
) {
|
||||
line_buf.clear();
|
||||
let mut lb = FlattenerCallback {
|
||||
line_buf,
|
||||
start: Point::ZERO,
|
||||
p0: Point::ZERO,
|
||||
is_nan: false,
|
||||
};
|
||||
|
||||
crate::flatten_simd::flatten(simd, path, affine, &mut lb, flatten_ctx, cull_bbox);
|
||||
|
||||
// A path that contains NaN is ill-defined, so ignore it.
|
||||
if lb.is_nan {
|
||||
warn!("A path contains NaN, ignoring it.");
|
||||
|
||||
line_buf.clear();
|
||||
}
|
||||
}
|
||||
/// Flatten a stroked Bézier path into line segments.
|
||||
///
|
||||
/// See the note about open subpaths and culling on [`fill`].
|
||||
pub fn stroke(
|
||||
level: Level,
|
||||
path: impl IntoIterator<Item = PathEl>,
|
||||
style: &Stroke,
|
||||
affine: Affine,
|
||||
line_buf: &mut Vec<Line>,
|
||||
flatten_ctx: &mut FlattenCtx,
|
||||
stroke_ctx: &mut StrokeCtx,
|
||||
cull_bbox: RectU16,
|
||||
) {
|
||||
// TODO: Temporary hack to ensure that strokes are scaled properly by the transform.
|
||||
let tolerance = TOL
|
||||
/ affine.as_coeffs()[0]
|
||||
.abs()
|
||||
.max(affine.as_coeffs()[3].abs())
|
||||
.max(1.);
|
||||
|
||||
expand_stroke(path, style, tolerance, stroke_ctx);
|
||||
fill(
|
||||
level,
|
||||
stroke_ctx.output(),
|
||||
affine,
|
||||
line_buf,
|
||||
flatten_ctx,
|
||||
cull_bbox,
|
||||
);
|
||||
}
|
||||
|
||||
/// Expand a stroked path to a filled path.
|
||||
pub fn expand_stroke(
|
||||
path: impl IntoIterator<Item = PathEl>,
|
||||
style: &Stroke,
|
||||
tolerance: f64,
|
||||
stroke_ctx: &mut StrokeCtx,
|
||||
) {
|
||||
kurbo::stroke_with(path, style, &StrokeOpts::default(), tolerance, stroke_ctx);
|
||||
}
|
||||
|
||||
struct FlattenerCallback<'a> {
|
||||
line_buf: &'a mut Vec<Line>,
|
||||
start: Point,
|
||||
p0: Point,
|
||||
is_nan: bool,
|
||||
}
|
||||
|
||||
impl Callback for FlattenerCallback<'_> {
|
||||
#[inline(always)]
|
||||
fn callback(&mut self, el: LinePathEl) {
|
||||
match el {
|
||||
LinePathEl::MoveTo(p) => {
|
||||
self.is_nan |= p.is_nan();
|
||||
|
||||
let p = p.into();
|
||||
self.start = p;
|
||||
self.p0 = p;
|
||||
}
|
||||
LinePathEl::LineTo(p) => {
|
||||
self.is_nan |= p.is_nan();
|
||||
|
||||
let p = p.into();
|
||||
self.line_buf.push(Line::new(self.p0, p));
|
||||
self.p0 = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+687
@@ -0,0 +1,687 @@
|
||||
// Copyright 2025 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! This is a temporary module that contains a SIMD version of flattening of cubic curves, as
|
||||
//! well as some code that was copied from kurbo, which is needed to reimplement the
|
||||
//! full `flatten` method.
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
use crate::kurbo::common::FloatFuncs as _;
|
||||
use crate::kurbo::{CubicBez, Line, ParamCurve, ParamCurveNearest, PathEl, Point, QuadBez};
|
||||
use crate::{
|
||||
flatten::{SQRT_TOL, TOL, TOL_2},
|
||||
geometry::RectU16,
|
||||
kurbo::Affine,
|
||||
tile::Tile,
|
||||
};
|
||||
use alloc::vec::Vec;
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use fearless_simd::*;
|
||||
|
||||
/// The element of a path made of lines.
|
||||
///
|
||||
/// Each subpath must start with a `MoveTo`. Closing of subpaths is not supported, and subpaths are
|
||||
/// not closed implicitly when a new subpath (with `MoveTo`) is started. It is expected that closed
|
||||
/// subpaths are watertight in the sense that the last `LineTo` matches exactly with the first
|
||||
/// `MoveTo`.
|
||||
///
|
||||
/// This intentionally allows for non-watertight subpaths, as, e.g., lines that are fully outside
|
||||
/// of the viewport do not need to be drawn.
|
||||
///
|
||||
/// See [`PathEl`] for a more general-purpose path element type.
|
||||
pub(crate) enum LinePathEl {
|
||||
MoveTo(Point),
|
||||
LineTo(Point),
|
||||
}
|
||||
|
||||
// Unlike kurbo, which takes a closure with a callback for outputting the lines, we use a trait
|
||||
// instead. The reason is that this way the callback can be inlined, which is not possible with
|
||||
// a closure and turned out to have a noticeable overhead.
|
||||
pub(crate) trait Callback {
|
||||
fn callback(&mut self, el: LinePathEl);
|
||||
}
|
||||
|
||||
/// See the docs for the kurbo implementation of flattening:
|
||||
/// <https://docs.rs/kurbo/latest/kurbo/fn.flatten.html>
|
||||
///
|
||||
/// This version works using a similar approach but using f32x4/f32x8 SIMD instead.
|
||||
#[inline(always)]
|
||||
pub(crate) fn flatten<S: Simd>(
|
||||
simd: S,
|
||||
path: impl IntoIterator<Item = PathEl>,
|
||||
// Note: we explicitly pass the `Affine` here instead of using `map` on
|
||||
// the iterator because it seems like the `map` function isn't always inlined
|
||||
// properly, even when annotating the closure with `#[inline(always)]`.
|
||||
// See https://github.com/linebender/vello/pull/1600.
|
||||
affine: Affine,
|
||||
callback: &mut impl Callback,
|
||||
flatten_ctx: &mut FlattenCtx,
|
||||
cull_bbox: RectU16,
|
||||
) {
|
||||
flatten_ctx.flattened_cubics.clear();
|
||||
|
||||
// For the culling performed here to be correct, the top y coordinate of the cull bbox must be
|
||||
// aligned to strip row boundaries. Consider the alternative: for example, a strip row starting
|
||||
// at y=8, a cull bbox starting at y=10, and (nearly) horizontal geometry at y=9 that does not
|
||||
// cross into the cull bbox and does not extend above y=8 (and note this is all in a y-down
|
||||
// coordinate space).
|
||||
//
|
||||
// The culling performed here would remove that geometry. However, as it does not extend above
|
||||
// the strip row, it does not add coarse winding to the strip, and does not produce a sparse
|
||||
// fill. Yet, if this is the top part of, say, a rectangle that is partially visible, the
|
||||
// geometry is necessary for tiling to emit intermediate tiles that are necessary for the
|
||||
// bottom part of the strip to get filled.
|
||||
//
|
||||
// Therefore, we align `top` to strip row boundaries, such that all intermediate tiles are
|
||||
// produced.
|
||||
//
|
||||
// This is not necessary for the bottom. Consider a path where a segment extends just below the
|
||||
// cull bbox, but remains in the same strip row. The path is closed, so if anything is to be
|
||||
// rendered at all, there will be other geometry above that edge of the cull bbox. If that
|
||||
// geometry extends above the row, there will be coarse winding for a sparse fill. If not, it
|
||||
// there will be geometry to generate the intermediate tiles.
|
||||
let left = cull_bbox.x0 as f64;
|
||||
let top = ((cull_bbox.y0 / Tile::HEIGHT) * Tile::HEIGHT) as f64;
|
||||
let right = cull_bbox.x1 as f64;
|
||||
let bottom = cull_bbox.y1 as f64;
|
||||
|
||||
let mut path = path.into_iter();
|
||||
let Some(first_el) = path.next() else {
|
||||
return;
|
||||
};
|
||||
let first_el = affine * first_el;
|
||||
let PathEl::MoveTo(start_pt) = first_el else {
|
||||
debug_assert!(
|
||||
matches!(first_el, PathEl::MoveTo(_)),
|
||||
"Non-empty paths must begin with `PathEl::MoveTo`, got {first_el:?}"
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let mut start_pt = start_pt;
|
||||
let mut last_pt = start_pt;
|
||||
callback.callback(LinePathEl::MoveTo(start_pt));
|
||||
|
||||
for el in path {
|
||||
match affine * el {
|
||||
PathEl::MoveTo(p) => {
|
||||
if last_pt != start_pt {
|
||||
callback.callback(LinePathEl::LineTo(start_pt));
|
||||
}
|
||||
last_pt = p;
|
||||
start_pt = p;
|
||||
callback.callback(LinePathEl::MoveTo(p));
|
||||
}
|
||||
PathEl::LineTo(p) => {
|
||||
last_pt = p;
|
||||
callback.callback(LinePathEl::LineTo(p));
|
||||
}
|
||||
PathEl::QuadTo(p1, p2) => {
|
||||
let p0 = last_pt;
|
||||
let line = Line::new(p0, p2);
|
||||
// If the quadratic Bézier is fully to the right, top, or bottom of the culling
|
||||
// bbox, it does not impact pixel coverage or winding. We can ignore it. The
|
||||
// following checks that conservatively by checking whether the bounding box of the
|
||||
// Bézier's control points is fully outside the culling bbox.
|
||||
if [p0, p1, p2].into_iter().all(|p| p.x > right)
|
||||
|| [p0, p1, p2].into_iter().all(|p| p.y < top)
|
||||
|| [p0, p1, p2].into_iter().all(|p| p.y > bottom)
|
||||
{
|
||||
callback.callback(LinePathEl::MoveTo(p2));
|
||||
}
|
||||
// The following checks two things. First, if the quadratic Bézier is fully to the
|
||||
// left of the culling bbox, it may affect pixel coverage and winding, but its
|
||||
// exact shape does not matter. It can be emitted as a line segment [p0, p2].
|
||||
//
|
||||
// Second, an upper bound on the shortest distance of any point on the quadratic
|
||||
// Bézier curve to the line segment [p0, p2] is 1/2 of the control-point-to-line-segment
|
||||
// distance.
|
||||
//
|
||||
// The derivation is similar to that for the cubic Bézier (see below). In
|
||||
// short:
|
||||
//
|
||||
// q(t) = B0(t) p0 + B1(t) p1 + B2(t) p2
|
||||
// dist(q(t), [p0, p1]) <= B1(t) dist(p1, [p0, p1])
|
||||
// = 2 (1-t)t dist(p1, [p0, p1]).
|
||||
//
|
||||
// The maximum occurs at t=1/2, hence
|
||||
// max(dist(q(t), [p0, p1] <= 1/2 dist(p1, [p0, p1])).
|
||||
//
|
||||
// The following takes the square to elide the square root of the Euclidean
|
||||
// distance.
|
||||
else if [p0, p1, p2].into_iter().all(|p| p.x < left)
|
||||
|| line.nearest(p1, 0.).distance_sq <= 4. * TOL_2
|
||||
{
|
||||
callback.callback(LinePathEl::LineTo(p2));
|
||||
} else {
|
||||
let q = QuadBez::new(p0, p1, p2);
|
||||
let params = q.estimate_subdiv(SQRT_TOL);
|
||||
let n = ((0.5 / SQRT_TOL * params.val).ceil() as usize).max(1);
|
||||
let step = 1.0 / (n as f64);
|
||||
for i in 1..n {
|
||||
let u = (i as f64) * step;
|
||||
let t = q.determine_subdiv_t(¶ms, u);
|
||||
let p = q.eval(t);
|
||||
callback.callback(LinePathEl::LineTo(p));
|
||||
}
|
||||
callback.callback(LinePathEl::LineTo(p2));
|
||||
}
|
||||
last_pt = p2;
|
||||
}
|
||||
PathEl::CurveTo(p1, p2, p3) => {
|
||||
let p0 = last_pt;
|
||||
let line = Line::new(p0, p3);
|
||||
// If the cubic Bézier is fully to the right, top, or bottom of the culling bbox,
|
||||
// it does not impact pixel coverage or winding. We can ignore it. The following
|
||||
// checks that conservatively by checking whether the bounding box of the Bézier's
|
||||
// control points is fully outside the culling bbox.
|
||||
if [p0, p1, p2, p3].into_iter().all(|p| p.x > right)
|
||||
|| [p0, p1, p2, p3].into_iter().all(|p| p.y < top)
|
||||
|| [p0, p1, p2, p3].into_iter().all(|p| p.y > bottom)
|
||||
{
|
||||
callback.callback(LinePathEl::MoveTo(p3));
|
||||
}
|
||||
// The following checks two things. First, if the cubic Bézier is fully to the left
|
||||
// of the culling bbox, it may affect pixel coverage and winding, but its exact
|
||||
// shape does not matter. It can be emitted as a line segment [p0, p3].
|
||||
//
|
||||
// Second, an upper bound on the shortest distance of any point on the cubic Bézier
|
||||
// curve to the line segment [p0, p3] is 3/4 of the maximum of the
|
||||
// control-point-to-line-segment distances.
|
||||
//
|
||||
// With Bernstein weights Bi(t), we have
|
||||
// c(t) = B0(t) p0 + B1(t) p1 + B2(t) p2 + B3(t) p3
|
||||
// with t from 0 to 1 (inclusive).
|
||||
//
|
||||
// Through convexivity of the Euclidean distance function and the line segment,
|
||||
// we have
|
||||
// dist(c(t), [p0, p3]) <= B1(t) dist(p1, [p0, p3]) + B2(t) dist(p2, [p0, p3])
|
||||
// <= (B1(t) + B2(t)) max(dist(p1, [p0, p3]), dist(p2, [p0, p3]))
|
||||
// = 3 ((1-t)t^2 + (1-t)^2t) max(dist(p1, [p0, p3]), dist(p2, [p0, p3])).
|
||||
//
|
||||
// The inner polynomial has its maximum of 1/4 at t=1/2, hence
|
||||
// max(dist(c(t), [p0, p3])) <= 3/4 max(dist(p1, [p0, p3]), dist(p2, [p0, p3])).
|
||||
//
|
||||
// The following takes the square to elide the square root of the Euclidean
|
||||
// distance.
|
||||
else if [p0, p1, p2, p3].into_iter().all(|p| p.x < left)
|
||||
|| f64::max(
|
||||
line.nearest(p1, 0.).distance_sq,
|
||||
line.nearest(p2, 0.).distance_sq,
|
||||
) <= 16. / 9. * TOL_2
|
||||
{
|
||||
callback.callback(LinePathEl::LineTo(p3));
|
||||
} else {
|
||||
let c = CubicBez::new(p0, p1, p2, p3);
|
||||
let max = flatten_cubic_simd(simd, c, flatten_ctx);
|
||||
|
||||
for p in &flatten_ctx.flattened_cubics[1..max] {
|
||||
callback.callback(LinePathEl::LineTo(Point::new(p.x as f64, p.y as f64)));
|
||||
}
|
||||
}
|
||||
last_pt = p3;
|
||||
}
|
||||
PathEl::ClosePath => {
|
||||
if last_pt != start_pt {
|
||||
callback.callback(LinePathEl::LineTo(start_pt));
|
||||
|
||||
// Kurbo says: "If `quad_to` [or another drawing op] is called immediately
|
||||
// after `close_path` then the current subpath starts at the initial point of
|
||||
// the previous subpath."
|
||||
//
|
||||
// Hence, we set `last_pt` back to the just-closed subpath's `start_pt`.
|
||||
last_pt = start_pt;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if last_pt != start_pt {
|
||||
callback.callback(LinePathEl::LineTo(start_pt));
|
||||
}
|
||||
}
|
||||
|
||||
// The below methods are copied from kurbo and needed to implement flattening of normal quad curves.
|
||||
|
||||
/// An approximation to $\int (1 + 4x^2) ^ -0.25 dx$
|
||||
///
|
||||
/// This is used for flattening curves.
|
||||
fn approx_parabola_integral(x: f64) -> f64 {
|
||||
const D: f64 = 0.67;
|
||||
x / (1.0 - D + (D.powi(4) + 0.25 * x * x).sqrt().sqrt())
|
||||
}
|
||||
|
||||
/// An approximation to the inverse parabola integral.
|
||||
fn approx_parabola_inv_integral(x: f64) -> f64 {
|
||||
const B: f64 = 0.39;
|
||||
x * (1.0 - B + (B * B + 0.25 * x * x).sqrt())
|
||||
}
|
||||
|
||||
impl FlattenParamsExt for QuadBez {
|
||||
#[inline(always)]
|
||||
fn estimate_subdiv(&self, sqrt_tol: f64) -> FlattenParams {
|
||||
// Determine transformation to $y = x^2$ parabola.
|
||||
let d01 = self.p1 - self.p0;
|
||||
let d12 = self.p2 - self.p1;
|
||||
let dd = d01 - d12;
|
||||
let cross = (self.p2 - self.p0).cross(dd);
|
||||
let x0 = d01.dot(dd) * cross.recip();
|
||||
let x2 = d12.dot(dd) * cross.recip();
|
||||
let scale = (cross / (dd.hypot() * (x2 - x0))).abs();
|
||||
|
||||
// Compute number of subdivisions needed.
|
||||
let a0 = approx_parabola_integral(x0);
|
||||
let a2 = approx_parabola_integral(x2);
|
||||
let val = if scale.is_finite() {
|
||||
let da = (a2 - a0).abs();
|
||||
let sqrt_scale = scale.sqrt();
|
||||
if x0.signum() == x2.signum() {
|
||||
da * sqrt_scale
|
||||
} else {
|
||||
// Handle cusp case (segment contains curvature maximum)
|
||||
let xmin = sqrt_tol / sqrt_scale;
|
||||
sqrt_tol * da / approx_parabola_integral(xmin)
|
||||
}
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let u0 = approx_parabola_inv_integral(a0);
|
||||
let u2 = approx_parabola_inv_integral(a2);
|
||||
let uscale = (u2 - u0).recip();
|
||||
FlattenParams {
|
||||
a0,
|
||||
a2,
|
||||
u0,
|
||||
uscale,
|
||||
val,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn determine_subdiv_t(&self, params: &FlattenParams, x: f64) -> f64 {
|
||||
let a = params.a0 + (params.a2 - params.a0) * x;
|
||||
let u = approx_parabola_inv_integral(a);
|
||||
(u - params.u0) * params.uscale
|
||||
}
|
||||
}
|
||||
|
||||
trait FlattenParamsExt {
|
||||
fn estimate_subdiv(&self, sqrt_tol: f64) -> FlattenParams;
|
||||
fn determine_subdiv_t(&self, params: &FlattenParams, x: f64) -> f64;
|
||||
}
|
||||
|
||||
// Everything below is a SIMD implementation of flattening of cubic curves.
|
||||
// It's a combination of https://gist.github.com/raphlinus/5f4e9feb85fd79bafc72da744571ec0e
|
||||
// and https://gist.github.com/raphlinus/44e114fef2fd33b889383a60ced0129b.
|
||||
|
||||
// TODO(laurenz): Perhaps we should get rid of this in the future and work directly with f32,
|
||||
// as it's the only reason we have to pull in proc_macros via the `derive` feature of bytemuck.
|
||||
#[derive(Clone, Copy, Debug, Default, Zeroable, Pod)]
|
||||
#[repr(C)]
|
||||
struct Point32 {
|
||||
x: f32,
|
||||
y: f32,
|
||||
}
|
||||
|
||||
struct FlattenParams {
|
||||
a0: f64,
|
||||
a2: f64,
|
||||
u0: f64,
|
||||
uscale: f64,
|
||||
/// The number of `subdivisions * 2 * sqrt_tol`.
|
||||
val: f64,
|
||||
}
|
||||
|
||||
/// This limit was chosen based on the pre-existing GitHub gist.
|
||||
/// This limit should not be hit in normal operation, but _might_ be hit for very large
|
||||
/// transforms.
|
||||
const MAX_QUADS: usize = 16;
|
||||
|
||||
/// The context needed for flattening curves.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct FlattenCtx {
|
||||
// The +4 is to encourage alignment; might be better to be explicit
|
||||
even_pts: [Point32; MAX_QUADS + 4],
|
||||
odd_pts: [Point32; MAX_QUADS],
|
||||
a0: [f32; MAX_QUADS],
|
||||
da: [f32; MAX_QUADS],
|
||||
u0: [f32; MAX_QUADS],
|
||||
uscale: [f32; MAX_QUADS],
|
||||
val: [f32; MAX_QUADS],
|
||||
n_quads: usize,
|
||||
/// Reusable buffer for flattened cubic points.
|
||||
flattened_cubics: Vec<Point32>,
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn is_finite_simd<S: Simd>(x: f32x4<S>) -> mask32x4<S> {
|
||||
let simd = x.simd;
|
||||
|
||||
let x_abs = x.abs();
|
||||
let reinterpreted = u32x4::from_bytes(x_abs.to_bytes());
|
||||
simd.simd_lt_u32x4(reinterpreted, u32x4::splat(simd, 0x7f80_0000))
|
||||
}
|
||||
|
||||
/// An approximation to $\int (1 + 4x^2) ^ -0.25 dx$
|
||||
///
|
||||
/// This is used for flattening curves.
|
||||
///
|
||||
/// SIMD version of [`approx_parabola_integral`].
|
||||
#[inline(always)]
|
||||
fn approx_parabola_integral_simd<S: Simd, F: SimdFloat<S, Element = f32>>(x: F) -> F {
|
||||
let simd = x.witness();
|
||||
|
||||
const D: f32 = 0.67;
|
||||
const D_POWI_4: f32 = 0.201_511_2;
|
||||
|
||||
let temp = F::splat(x.witness(), 0.25)
|
||||
.mul_add(x * x, F::splat(simd, D_POWI_4))
|
||||
.sqrt()
|
||||
.sqrt();
|
||||
let denom = temp + (1. - D);
|
||||
x / denom
|
||||
}
|
||||
|
||||
/// An approximation to the inverse parabola integral.
|
||||
///
|
||||
/// SIMD version of [`approx_parabola_inv_integral`].
|
||||
#[inline(always)]
|
||||
fn approx_parabola_inv_integral_simd<S: Simd>(x: f32x8<S>) -> f32x8<S> {
|
||||
let simd = x.simd;
|
||||
|
||||
const B: f32 = 0.39;
|
||||
const ONE_MINUS_B: f32 = 1.0 - B;
|
||||
|
||||
let temp = f32x8::splat(simd, 0.25).mul_add(x * x, B * B).sqrt();
|
||||
let factor = f32x8::splat(simd, ONE_MINUS_B) + temp;
|
||||
x * factor
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn pt_splat_simd<S: Simd>(simd: S, pt: Point32) -> f32x8<S> {
|
||||
let p_f64: f64 = bytemuck::cast(pt);
|
||||
simd.reinterpret_f32_f64x4(f64x4::splat(simd, p_f64))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn eval_cubics_simd<S: Simd>(simd: S, c: &CubicBez, n: usize, result: &mut FlattenCtx) {
|
||||
result.n_quads = n;
|
||||
let dt = 0.5 / n as f32;
|
||||
|
||||
// TODO(laurenz): Perhaps we can SIMDify this better
|
||||
let p0p1 = f32x4::from_slice(
|
||||
simd,
|
||||
&[c.p0.x as f32, c.p0.y as f32, c.p1.x as f32, c.p1.y as f32],
|
||||
);
|
||||
let p2p3 = f32x4::from_slice(
|
||||
simd,
|
||||
&[c.p2.x as f32, c.p2.y as f32, c.p3.x as f32, c.p3.y as f32],
|
||||
);
|
||||
|
||||
let split_single = |input: f32x4<S>| {
|
||||
let t1 = simd.reinterpret_f64_f32x4(input);
|
||||
let p0 = simd.zip_low_f64x2(t1, t1);
|
||||
let p1 = simd.zip_high_f64x2(t1, t1);
|
||||
|
||||
let p0 = simd.reinterpret_f32_f64x2(p0);
|
||||
let p1 = simd.reinterpret_f32_f64x2(p1);
|
||||
|
||||
(f32x8::block_splat(p0), f32x8::block_splat(p1))
|
||||
};
|
||||
|
||||
let (p0_128, p1_128) = split_single(p0p1);
|
||||
let (p2_128, p3_128) = split_single(p2p3);
|
||||
|
||||
// Use Horner's method to evaluate p(t) as ((a*t + b)*t + c)*t + d. This allows hoisting
|
||||
// coefficients out of the loop, and then evaluating as three sequential FMAs. Estrin's method
|
||||
// would expose more ILP, but the increase in work on such small polynomials (cubic here and
|
||||
// quad below) makes it a net slowdown.
|
||||
let coeff_a = (p1_128 - p2_128).mul_add(3.0, p3_128 - p0_128);
|
||||
let coeff_b = p1_128.mul_add(-2.0, p0_128 + p2_128) * 3.0;
|
||||
let coeff_c = (p1_128 - p0_128) * 3.0;
|
||||
let coeff_d = p0_128;
|
||||
|
||||
let iota = f32x8::from_slice(simd, &[0.0, 0.0, 2.0, 2.0, 1.0, 1.0, 3.0, 3.0]);
|
||||
let step = iota * dt;
|
||||
let mut t = step;
|
||||
let t_inc = f32x8::splat(simd, 4.0 * dt);
|
||||
|
||||
let even_pts: &mut [f32] = bytemuck::cast_slice_mut(&mut result.even_pts);
|
||||
let odd_pts: &mut [f32] = bytemuck::cast_slice_mut(&mut result.odd_pts);
|
||||
|
||||
for i in 0..n.div_ceil(2) {
|
||||
let evaluated = (coeff_a.mul_add(t, coeff_b))
|
||||
.mul_add(t, coeff_c)
|
||||
.mul_add(t, coeff_d);
|
||||
|
||||
let (low, high) = simd.split_f32x8(evaluated);
|
||||
|
||||
low.store_slice(&mut even_pts[i * 4..][..4]);
|
||||
high.store_slice(&mut odd_pts[i * 4..][..4]);
|
||||
|
||||
t += t_inc;
|
||||
}
|
||||
|
||||
p3_128.store_slice(&mut even_pts[n * 2..][..8]);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn estimate_subdiv_simd<S: Simd>(simd: S, sqrt_tol: f32, ctx: &mut FlattenCtx) {
|
||||
let n = ctx.n_quads;
|
||||
|
||||
let even_pts: &mut [f32] = bytemuck::cast_slice_mut(&mut ctx.even_pts);
|
||||
let odd_pts: &mut [f32] = bytemuck::cast_slice_mut(&mut ctx.odd_pts);
|
||||
|
||||
for i in 0..n.div_ceil(4) {
|
||||
let p0 = f32x8::from_slice(simd, &even_pts[i * 8..][..8]);
|
||||
let p_onehalf = f32x8::from_slice(simd, &odd_pts[i * 8..][..8]);
|
||||
let p2 = f32x8::from_slice(simd, &even_pts[(i * 8 + 2)..][..8]);
|
||||
let x = p0 * -0.5;
|
||||
let x1 = p_onehalf.mul_add(2.0, x);
|
||||
let p1 = p2.mul_add(-0.5, x1);
|
||||
|
||||
p1.store_slice(&mut odd_pts[(i * 8)..][..8]);
|
||||
|
||||
let d01 = p1 - p0;
|
||||
let d12 = p2 - p1;
|
||||
let d01x = simd.unzip_low_f32x8(d01, d01);
|
||||
let d01y = simd.unzip_high_f32x8(d01, d01);
|
||||
let d12x = simd.unzip_low_f32x8(d12, d12);
|
||||
let d12y = simd.unzip_high_f32x8(d12, d12);
|
||||
let ddx = d01x - d12x;
|
||||
let ddy = d01y - d12y;
|
||||
let d02x = d01x + d12x;
|
||||
let d02y = d01y + d12y;
|
||||
// (d02x * ddy) - (d02y * ddx)
|
||||
let cross = ddx.mul_add(-d02y, d02x * ddy);
|
||||
|
||||
let x0_x2_a = {
|
||||
let (d01x_low, _) = simd.split_f32x8(d01x);
|
||||
let (d12x_low, _) = simd.split_f32x8(d12x);
|
||||
|
||||
simd.combine_f32x4(d12x_low, d01x_low) * ddx
|
||||
};
|
||||
let temp1 = {
|
||||
let (d12y_low, _) = simd.split_f32x8(d12y);
|
||||
let (d01y_low, _) = simd.split_f32x8(d01y);
|
||||
|
||||
simd.combine_f32x4(d12y_low, d01y_low)
|
||||
};
|
||||
let x0_x2_num = temp1.mul_add(ddy, x0_x2_a);
|
||||
let x0_x2 = x0_x2_num / cross;
|
||||
let (ddx_low, _) = simd.split_f32x8(ddx);
|
||||
let (ddy_low, _) = simd.split_f32x8(ddy);
|
||||
let dd_hypot = ddy_low.mul_add(ddy_low, ddx_low * ddx_low).sqrt();
|
||||
let (x0, x2) = simd.split_f32x8(x0_x2);
|
||||
let scale_denom = dd_hypot * (x2 - x0);
|
||||
let (temp2, _) = simd.split_f32x8(cross);
|
||||
let scale = (temp2 / scale_denom).abs();
|
||||
let a0_a2 = approx_parabola_integral_simd(x0_x2);
|
||||
let (a0, a2) = simd.split_f32x8(a0_a2);
|
||||
let da = a2 - a0;
|
||||
let da_abs = da.abs();
|
||||
let sqrt_scale = scale.sqrt();
|
||||
let temp3 = simd.or_i32x4(x0.bitcast(), x2.bitcast());
|
||||
let mask = simd.simd_ge_i32x4(temp3, i32x4::splat(simd, 0));
|
||||
let noncusp = da_abs * sqrt_scale;
|
||||
// TODO: should we skip this if neither is a cusp? Maybe not worth branch prediction cost
|
||||
let xmin = sqrt_tol / sqrt_scale;
|
||||
let approxint = approx_parabola_integral_simd(xmin);
|
||||
let cusp = (sqrt_tol * da_abs) / approxint;
|
||||
let val_raw = simd.select_f32x4(mask, noncusp, cusp);
|
||||
let finite_mask = is_finite_simd(val_raw);
|
||||
let val = simd.select_f32x4(finite_mask, val_raw, f32x4::splat(simd, 0.0));
|
||||
let u0_u2 = approx_parabola_inv_integral_simd(a0_a2);
|
||||
let (u0, u2) = simd.split_f32x8(u0_u2);
|
||||
let uscale_a = u2 - u0;
|
||||
let uscale = 1.0 / uscale_a;
|
||||
|
||||
a0.store_slice(&mut ctx.a0[i * 4..][..4]);
|
||||
da.store_slice(&mut ctx.da[i * 4..][..4]);
|
||||
u0.store_slice(&mut ctx.u0[i * 4..][..4]);
|
||||
uscale.store_slice(&mut ctx.uscale[i * 4..][..4]);
|
||||
val.store_slice(&mut ctx.val[i * 4..][..4]);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn output_lines_simd<S: Simd>(
|
||||
simd: S,
|
||||
ctx: &mut FlattenCtx,
|
||||
i: usize,
|
||||
x0: f32,
|
||||
dx: f32,
|
||||
n: usize,
|
||||
start_idx: usize,
|
||||
) {
|
||||
let p0 = pt_splat_simd(simd, ctx.even_pts[i]);
|
||||
let p1 = pt_splat_simd(simd, ctx.odd_pts[i]);
|
||||
let p2 = pt_splat_simd(simd, ctx.even_pts[i + 1]);
|
||||
|
||||
const IOTA2: [f32; 8] = [0., 0., 1., 1., 2., 2., 3., 3.];
|
||||
let iota2 = f32x8::from_slice(simd, IOTA2.as_ref());
|
||||
let x = iota2.mul_add(dx, f32x8::splat(simd, x0));
|
||||
let da = f32x8::splat(simd, ctx.da[i]);
|
||||
let mut a = da.mul_add(x, f32x8::splat(simd, ctx.a0[i]));
|
||||
let a_inc = 4.0 * dx * da;
|
||||
let uscale = f32x8::splat(simd, ctx.uscale[i]);
|
||||
let u0 = f32x8::splat(simd, ctx.u0[i]);
|
||||
|
||||
// See Horner comment above
|
||||
let coeff_a = p1.mul_add(-2.0, p0) + p2;
|
||||
let coeff_b = (p1 - p0) * 2.0;
|
||||
let coeff_c = p0;
|
||||
|
||||
let out: &mut [f32] = bytemuck::cast_slice_mut(&mut ctx.flattened_cubics[start_idx..]);
|
||||
|
||||
for j in 0..n.div_ceil(4) {
|
||||
let u = approx_parabola_inv_integral_simd(a);
|
||||
let t = (u - u0) * uscale;
|
||||
let p = coeff_a.mul_add(t, coeff_b).mul_add(t, coeff_c);
|
||||
p.store_slice(&mut out[j * 8..][..8]);
|
||||
a += a_inc;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn flatten_cubic_simd<S: Simd>(simd: S, c: CubicBez, ctx: &mut FlattenCtx) -> usize {
|
||||
let n_quads = estimate_num_quads(c, TOL as f32);
|
||||
eval_cubics_simd(simd, &c, n_quads, ctx);
|
||||
let tol = (TOL as f32) * (1.0 - TO_QUAD_TOL);
|
||||
let sqrt_tol = tol.sqrt();
|
||||
estimate_subdiv_simd(simd, sqrt_tol, ctx);
|
||||
let sum: f32 = ctx.val[..n_quads].iter().sum();
|
||||
let n = ((0.5 * sum / sqrt_tol).ceil() as usize).max(1);
|
||||
let target_len = n + 4;
|
||||
if target_len > ctx.flattened_cubics.len() {
|
||||
ctx.flattened_cubics.resize(target_len, Point32::default());
|
||||
}
|
||||
|
||||
let step = sum / (n as f32);
|
||||
let step_recip = 1.0 / step;
|
||||
let mut val_sum = 0.0;
|
||||
let mut last_n = 0;
|
||||
let mut x0base = 0.0;
|
||||
|
||||
for i in 0..n_quads {
|
||||
let val = ctx.val[i];
|
||||
val_sum += val;
|
||||
let this_n = val_sum * step_recip;
|
||||
let this_n_next = 1.0 + this_n.floor();
|
||||
let dn = this_n_next as usize - last_n;
|
||||
if dn > 0 {
|
||||
let dx = step / val;
|
||||
let x0 = x0base * dx;
|
||||
output_lines_simd(simd, ctx, i, x0, dx, dn, last_n);
|
||||
}
|
||||
x0base = this_n_next - this_n;
|
||||
last_n = this_n_next as usize;
|
||||
}
|
||||
|
||||
ctx.flattened_cubics[n] = ctx.even_pts[n_quads];
|
||||
|
||||
n + 1
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn estimate_num_quads(c: CubicBez, accuracy: f32) -> usize {
|
||||
let q_accuracy = (accuracy * TO_QUAD_TOL) as f64;
|
||||
let max_hypot2 = 432.0 * q_accuracy * q_accuracy;
|
||||
let p1x2 = c.p1.to_vec2() * 3.0 - c.p0.to_vec2();
|
||||
let p2x2 = c.p2.to_vec2() * 3.0 - c.p3.to_vec2();
|
||||
let err = (p2x2 - p1x2).hypot2();
|
||||
let err_div = err / max_hypot2;
|
||||
|
||||
estimate(err_div)
|
||||
}
|
||||
|
||||
const TO_QUAD_TOL: f32 = 0.1;
|
||||
|
||||
#[inline(always)]
|
||||
fn estimate(err_div: f64) -> usize {
|
||||
// The original version of this method was:
|
||||
// let n_quads = (err_div.powf(1. / 6.0).ceil() as usize).max(1);
|
||||
// n_quads.min(MAX_QUADS)
|
||||
//
|
||||
// Note how we always round up and clamp to the range [1, max_quads]. Since we don't
|
||||
// care about the actual fractional value resulting from the powf call we can simply
|
||||
// compute this using a precomputed lookup table evaluating 1^6, 2^6, 3^6, etc. and simply
|
||||
// comparing if the value is less than or equal to each threshold.
|
||||
|
||||
const LUT: [f64; MAX_QUADS] = [
|
||||
1.0, 64.0, 729.0, 4096.0, 15625.0, 46656.0, 117649.0, 262144.0, 531441.0, 1000000.0,
|
||||
1771561.0, 2985984.0, 4826809.0, 7529536.0, 11390625.0, 16777216.0,
|
||||
];
|
||||
|
||||
#[expect(clippy::needless_range_loop, reason = "better clarity")]
|
||||
for i in 0..MAX_QUADS {
|
||||
if err_div <= LUT[i] {
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
MAX_QUADS
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::flatten_simd::{MAX_QUADS, estimate};
|
||||
|
||||
fn old_estimate(err_div: f64) -> usize {
|
||||
let n_quads = (err_div.powf(1. / 6.0).ceil() as usize).max(1);
|
||||
n_quads.min(MAX_QUADS)
|
||||
}
|
||||
|
||||
// Test is disabled by default since it takes 10-20 seconds to run, even in release mode.
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn accuracy() {
|
||||
for i in 0..u32::MAX {
|
||||
let num = f32::from_bits(i);
|
||||
|
||||
if num.is_finite() {
|
||||
assert_eq!(old_estimate(num as f64), estimate(num as f64), "{num}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+108
@@ -0,0 +1,108 @@
|
||||
// Copyright 2025 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Geometry utilities.
|
||||
|
||||
/// An axis-aligned rectangle with `u16` coordinates, stored as two corners `(x0, y0)` and
|
||||
/// `(x1, y1)`.
|
||||
///
|
||||
/// `(x0, y0)` is the top-left (minimum) corner and `(x1, y1)` is the bottom-right (maximum) corner.
|
||||
/// The rectangle is considered to be empty when `x0 >= x1` or `y0 >= y1`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct RectU16 {
|
||||
/// The minimum x coordinate (left edge).
|
||||
pub x0: u16,
|
||||
/// The minimum y coordinate (top edge).
|
||||
pub y0: u16,
|
||||
/// The maximum x coordinate (right edge, exclusive).
|
||||
pub x1: u16,
|
||||
/// The maximum y coordinate (bottom edge, exclusive).
|
||||
pub y1: u16,
|
||||
}
|
||||
|
||||
impl RectU16 {
|
||||
/// A rectangle with all coordinates set to zero.
|
||||
pub const ZERO: Self = Self {
|
||||
x0: 0,
|
||||
y0: 0,
|
||||
x1: 0,
|
||||
y1: 0,
|
||||
};
|
||||
|
||||
/// An empty, maximally inverted rectangle, useful as a starting value for incremental union
|
||||
/// operations.
|
||||
///
|
||||
/// Has `(x0, y0) = (u16::MAX, u16::MAX)` and `(x1, y1) = (0, 0)`.
|
||||
pub const INVERTED: Self = Self {
|
||||
x0: u16::MAX,
|
||||
y0: u16::MAX,
|
||||
x1: 0,
|
||||
y1: 0,
|
||||
};
|
||||
|
||||
/// Create a new rectangle from its corner coordinates.
|
||||
#[inline(always)]
|
||||
pub const fn new(x0: u16, y0: u16, x1: u16, y1: u16) -> Self {
|
||||
Self { x0, y0, x1, y1 }
|
||||
}
|
||||
|
||||
/// The width of the rectangle (`x1 - x0`), saturating at zero.
|
||||
#[inline(always)]
|
||||
pub const fn width(self) -> u16 {
|
||||
self.x1.saturating_sub(self.x0)
|
||||
}
|
||||
|
||||
/// The height of the rectangle (`y1 - y0`), saturating at zero.
|
||||
#[inline(always)]
|
||||
pub const fn height(self) -> u16 {
|
||||
self.y1.saturating_sub(self.y0)
|
||||
}
|
||||
|
||||
/// Returns `true` if the rectangle has zero area (`x0 >= x1` or `y0 >= y1`).
|
||||
#[inline(always)]
|
||||
pub const fn is_empty(self) -> bool {
|
||||
self.x0 >= self.x1 || self.y0 >= self.y1
|
||||
}
|
||||
|
||||
/// Check if a point `(x, y)` is contained within this rectangle.
|
||||
///
|
||||
/// Returns `true` if `x0 <= x < x1` and `y0 <= y < y1`.
|
||||
#[inline(always)]
|
||||
pub const fn contains(self, x: u16, y: u16) -> bool {
|
||||
(x >= self.x0) & (x < self.x1) & (y >= self.y0) & (y < self.y1)
|
||||
}
|
||||
|
||||
/// Compute the intersection of two rectangles.
|
||||
///
|
||||
/// The result may be empty if the rectangles do not overlap.
|
||||
#[inline(always)]
|
||||
pub const fn intersect(self, other: Self) -> Self {
|
||||
Self {
|
||||
x0: const_max(self.x0, other.x0),
|
||||
y0: const_max(self.y0, other.y0),
|
||||
x1: const_min(self.x1, other.x1),
|
||||
y1: const_min(self.y1, other.y1),
|
||||
}
|
||||
}
|
||||
|
||||
/// Expand this rectangle to also cover `other` (union in place).
|
||||
///
|
||||
/// The union of `self` with a [`Self::INVERTED`] returns `self`.
|
||||
#[inline(always)]
|
||||
pub const fn union(&mut self, other: Self) {
|
||||
self.x0 = const_min(self.x0, other.x0);
|
||||
self.y0 = const_min(self.y0, other.y0);
|
||||
self.x1 = const_max(self.x1, other.x1);
|
||||
self.y1 = const_max(self.y1, other.y1);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
const fn const_max(a: u16, b: u16) -> u16 {
|
||||
if a > b { a } else { b }
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
const fn const_min(a: u16, b: u16) -> u16 {
|
||||
if a < b { a } else { b }
|
||||
}
|
||||
+342
@@ -0,0 +1,342 @@
|
||||
// Copyright 2025 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Image resource caching with multi-atlas allocation.
|
||||
//!
|
||||
//! This module provides an [`ImageCache`] that manages image resources across multiple texture
|
||||
//! atlases, supporting allocation, deallocation, and slot reuse.
|
||||
|
||||
use crate::multi_atlas::{
|
||||
AllocId, AllocationStrategy, AtlasConfig, AtlasError, AtlasId, MultiAtlasManager,
|
||||
};
|
||||
use crate::paint::ImageId;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
/// Represents an image resource for rendering.
|
||||
#[derive(Debug)]
|
||||
pub struct ImageResource {
|
||||
/// The width of the image.
|
||||
pub width: u16,
|
||||
/// The height of the image.
|
||||
pub height: u16,
|
||||
/// The Id of the atlas containing this image.
|
||||
pub atlas_id: AtlasId,
|
||||
/// The offset of the image within its atlas (does not include padding, i.e. it points to the
|
||||
/// position of the first actual top-left pixel).
|
||||
pub offset: [u16; 2],
|
||||
/// The number of transparent padding pixels around the image in the atlas.
|
||||
pub padding: u16,
|
||||
/// The atlas allocation ID for deallocation.
|
||||
atlas_alloc_id: AllocId,
|
||||
}
|
||||
|
||||
impl ImageResource {
|
||||
/// Returns the offset as `[u32; 2]`.
|
||||
pub fn offsets(&self) -> [u32; 2] {
|
||||
[self.offset[0] as u32, self.offset[1] as u32]
|
||||
}
|
||||
|
||||
/// Returns the size as `[u32; 2]`.
|
||||
pub fn size(&self) -> [u32; 2] {
|
||||
[self.width as u32, self.height as u32]
|
||||
}
|
||||
}
|
||||
|
||||
/// Manages image resources for the renderer.
|
||||
pub struct ImageCache {
|
||||
/// Multi-atlas manager for handling multiple texture atlases.
|
||||
atlas_manager: MultiAtlasManager,
|
||||
/// Vector of optional image resources (None = free slot).
|
||||
slots: Vec<Option<ImageResource>>,
|
||||
/// Stack of free indices.
|
||||
free_idxs: Vec<usize>,
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for ImageCache {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
let atlas_stats = self.atlas_manager.atlas_stats();
|
||||
|
||||
f.debug_struct("ImageCache")
|
||||
.field("slots", &self.slots)
|
||||
.field("free_idxs", &self.free_idxs)
|
||||
.field("atlas_count", &self.atlas_manager.atlas_count())
|
||||
.field("atlas_stats", &atlas_stats)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl ImageCache {
|
||||
/// Create a new image cache with custom atlas configuration.
|
||||
pub fn new_with_config(config: AtlasConfig) -> Self {
|
||||
Self {
|
||||
atlas_manager: MultiAtlasManager::new(config),
|
||||
slots: Vec::new(),
|
||||
free_idxs: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new dummy image atlas that is supposed to act as a stub.
|
||||
pub fn new_dummy() -> Self {
|
||||
Self::new_with_config(AtlasConfig {
|
||||
initial_atlas_count: 1,
|
||||
max_atlases: 1,
|
||||
atlas_size: (1, 1),
|
||||
auto_grow: false,
|
||||
allocation_strategy: AllocationStrategy::FirstFit,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get an image resource by its Id.
|
||||
pub fn get(&self, id: ImageId) -> Option<&ImageResource> {
|
||||
self.slots.get(id.as_u32() as usize)?.as_ref()
|
||||
}
|
||||
|
||||
/// Allocate an image in the cache, with optional transparent padding.
|
||||
pub fn allocate(
|
||||
&mut self,
|
||||
width: u32,
|
||||
height: u32,
|
||||
padding: u16,
|
||||
) -> Result<ImageId, AtlasError> {
|
||||
self.allocate_excluding(width, height, padding, None)
|
||||
}
|
||||
|
||||
/// Allocate an image in the cache, with optional transparency padding
|
||||
/// and optionally excluding a specific atlas.
|
||||
#[expect(
|
||||
clippy::cast_possible_truncation,
|
||||
reason = "u16 is enough for the offset and width/height"
|
||||
)]
|
||||
pub fn allocate_excluding(
|
||||
&mut self,
|
||||
width: u32,
|
||||
height: u32,
|
||||
padding: u16,
|
||||
exclude_atlas_id: Option<AtlasId>,
|
||||
) -> Result<ImageId, AtlasError> {
|
||||
let padded_width = width + u32::from(padding) * 2;
|
||||
let padded_height = height + u32::from(padding) * 2;
|
||||
let atlas_alloc = self.atlas_manager.try_allocate_excluding(
|
||||
padded_width,
|
||||
padded_height,
|
||||
exclude_atlas_id,
|
||||
)?;
|
||||
|
||||
let slot_idx = self.free_idxs.pop().unwrap_or_else(|| {
|
||||
// No free slots, append to vector
|
||||
let index = self.slots.len();
|
||||
// Placeholder, will be replaced
|
||||
self.slots.push(None);
|
||||
index
|
||||
});
|
||||
|
||||
let image_id = ImageId::new(slot_idx as u32);
|
||||
let image_resource = ImageResource {
|
||||
width: width as u16,
|
||||
height: height as u16,
|
||||
atlas_id: atlas_alloc.atlas_id,
|
||||
offset: [
|
||||
atlas_alloc.allocation.x as u16 + padding,
|
||||
atlas_alloc.allocation.y as u16 + padding,
|
||||
],
|
||||
padding,
|
||||
atlas_alloc_id: atlas_alloc.allocation.id,
|
||||
};
|
||||
self.slots[slot_idx] = Some(image_resource);
|
||||
|
||||
Ok(image_id)
|
||||
}
|
||||
|
||||
/// Deallocate an image from the cache, returning the image resource if it existed.
|
||||
pub fn deallocate(&mut self, id: ImageId) -> Option<ImageResource> {
|
||||
let index = id.as_u32() as usize;
|
||||
if let Some(image_resource) = self.slots.get_mut(index).and_then(Option::take) {
|
||||
// Deallocate from the appropriate atlas
|
||||
let padded_width = image_resource.width as u32 + u32::from(image_resource.padding) * 2;
|
||||
let padded_height =
|
||||
image_resource.height as u32 + u32::from(image_resource.padding) * 2;
|
||||
self.atlas_manager
|
||||
.deallocate(
|
||||
image_resource.atlas_id,
|
||||
image_resource.atlas_alloc_id,
|
||||
padded_width,
|
||||
padded_height,
|
||||
)
|
||||
.unwrap();
|
||||
self.free_idxs.push(index);
|
||||
Some(image_resource)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Get access to the atlas manager.
|
||||
pub fn atlas_manager(&self) -> &MultiAtlasManager {
|
||||
&self.atlas_manager
|
||||
}
|
||||
|
||||
/// Get the number of atlases.
|
||||
pub fn atlas_count(&self) -> usize {
|
||||
self.atlas_manager.atlas_count()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const ATLAS_SIZE: u32 = 1024;
|
||||
|
||||
#[test]
|
||||
fn test_insert_single_image() {
|
||||
let mut cache = ImageCache::new_with_config(AtlasConfig {
|
||||
atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let id = cache.allocate(100, 100, 0).unwrap();
|
||||
|
||||
assert_eq!(id.as_u32(), 0);
|
||||
let resource = cache.get(id).unwrap();
|
||||
assert_eq!(resource.width, 100);
|
||||
assert_eq!(resource.height, 100);
|
||||
// First image should be at origin
|
||||
assert_eq!(resource.offset, [0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_single_image_with_padding() {
|
||||
let mut cache = ImageCache::new_with_config(AtlasConfig {
|
||||
atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let id = cache.allocate(100, 100, 4).unwrap();
|
||||
|
||||
assert_eq!(id.as_u32(), 0);
|
||||
let resource = cache.get(id).unwrap();
|
||||
assert_eq!(resource.width, 100);
|
||||
assert_eq!(resource.height, 100);
|
||||
assert_eq!(resource.padding, 4);
|
||||
// Offset should be shifted inward by padding.
|
||||
assert_eq!(resource.offset, [4, 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_multiple_images() {
|
||||
let mut cache = ImageCache::new_with_config(AtlasConfig {
|
||||
atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let id1 = cache.allocate(50, 50, 0).unwrap();
|
||||
let id2 = cache.allocate(75, 75, 0).unwrap();
|
||||
|
||||
assert_eq!(id1.as_u32(), 0);
|
||||
assert_eq!(id2.as_u32(), 1);
|
||||
|
||||
let resource1 = cache.get(id1).unwrap();
|
||||
let resource2 = cache.get(id2).unwrap();
|
||||
|
||||
assert_eq!(resource1.width, 50);
|
||||
assert_eq!(resource2.width, 75);
|
||||
|
||||
// Second image should be placed adjacent to first
|
||||
assert_ne!(resource1.offset, resource2.offset);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_nonexistent_image() {
|
||||
let cache: ImageCache = ImageCache::new_with_config(AtlasConfig {
|
||||
atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
assert!(cache.get(ImageId::new(0)).is_none());
|
||||
assert!(cache.get(ImageId::new(999)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_image() {
|
||||
let mut cache = ImageCache::new_with_config(AtlasConfig {
|
||||
atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let id = cache.allocate(100, 100, 0).unwrap();
|
||||
assert!(cache.get(id).is_some());
|
||||
|
||||
cache.deallocate(id);
|
||||
assert!(cache.get(id).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_nonexistent_image() {
|
||||
let mut cache: ImageCache = ImageCache::new_with_config(AtlasConfig {
|
||||
atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Should not panic when unregistering non-existent image
|
||||
cache.deallocate(ImageId::new(0));
|
||||
cache.deallocate(ImageId::new(999));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_slot_reuse_after_remove() {
|
||||
let mut cache = ImageCache::new_with_config(AtlasConfig {
|
||||
atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Register three images
|
||||
let id1 = cache.allocate(50, 50, 0).unwrap();
|
||||
let id2 = cache.allocate(60, 60, 0).unwrap();
|
||||
let id3 = cache.allocate(70, 70, 0).unwrap();
|
||||
|
||||
assert_eq!(id1.as_u32(), 0);
|
||||
assert_eq!(id2.as_u32(), 1);
|
||||
assert_eq!(id3.as_u32(), 2);
|
||||
|
||||
// Unregister the middle one
|
||||
cache.deallocate(id2);
|
||||
assert!(cache.get(id2).is_none());
|
||||
|
||||
// Register a new image - should reuse slot 1
|
||||
let id4 = cache.allocate(80, 80, 0).unwrap();
|
||||
// Reused slot 1
|
||||
assert_eq!(id4.as_u32(), 1);
|
||||
|
||||
// Verify other images are still there
|
||||
assert!(cache.get(id1).is_some());
|
||||
assert!(cache.get(id3).is_some());
|
||||
assert!(cache.get(id4).is_some());
|
||||
assert_eq!(cache.get(id4).unwrap().width, 80);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_remove_and_reuse() {
|
||||
let mut cache = ImageCache::new_with_config(AtlasConfig {
|
||||
atlas_size: (ATLAS_SIZE, ATLAS_SIZE),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Register several images
|
||||
let ids: Vec<_> = (0..5)
|
||||
.map(|i| cache.allocate(100 + i * 10, 100 + i * 10, 0).unwrap())
|
||||
.collect();
|
||||
|
||||
// Unregister some in the middle
|
||||
cache.deallocate(ids[1]);
|
||||
cache.deallocate(ids[3]);
|
||||
|
||||
// Register new images - should reuse the freed slots
|
||||
let new_id1 = cache.allocate(200, 200, 0).unwrap();
|
||||
let new_id2 = cache.allocate(300, 300, 0).unwrap();
|
||||
|
||||
// Should have reused slots 3 and 1 (in reverse order due to stack behavior)
|
||||
assert_eq!(new_id1.as_u32(), 3);
|
||||
assert_eq!(new_id2.as_u32(), 1);
|
||||
assert_ne!(new_id1.as_u32(), new_id2.as_u32());
|
||||
}
|
||||
}
|
||||
Vendored
+106
@@ -0,0 +1,106 @@
|
||||
// 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_common
|
||||
|
||||
//! This crate includes common geometry representations, tiling logic, and other fundamental components used by both [Vello CPU][vello_cpu] and Vello Hybrid.
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! This crate should not be used on its own, and you should instead use one of the renderers which use it.
|
||||
//! At the moment, only [Vello CPU][vello_cpu] is published, and you probably want to use that.
|
||||
//!
|
||||
//! 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.
|
||||
//! Vello does not use this crate.
|
||||
//!
|
||||
//! # 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`][crate::pixmap::Pixmap]s from PNG images.
|
||||
//! Also required for rendering glyphs with an embedded PNG.
|
||||
//! Implies `std`.
|
||||
//!
|
||||
//! At least one of `std` and `libm` is required; `std` overrides `libm`.
|
||||
//!
|
||||
//! # Contents
|
||||
//!
|
||||
//! - Shared data structures for paths, tiles, and strips
|
||||
//! - Geometry processing utilities
|
||||
//! - Common logic for rendering stages
|
||||
//!
|
||||
//! This crate acts as a foundation for `vello_cpu` and `vello_hybrid`, providing essential components to minimize duplication.
|
||||
//!
|
||||
//! [vello_cpu]: https://crates.io/crates/vello_cpu
|
||||
#![cfg_attr(feature = "libm", doc = "[libm]: libm")]
|
||||
#![cfg_attr(not(feature = "libm"), doc = "[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 temporarily ignore those because the casts\
|
||||
only break in edge cases, and some of them are also only related to conversions from f64 to f32."
|
||||
)]
|
||||
#![no_std]
|
||||
|
||||
// Suppress the unused_crate_dependencies lint when both std and libm are specified.
|
||||
#[cfg(all(feature = "std", feature = "libm"))]
|
||||
use libm as _;
|
||||
|
||||
extern crate alloc;
|
||||
#[cfg(feature = "std")]
|
||||
extern crate std;
|
||||
|
||||
pub mod blurred_rounded_rect;
|
||||
pub mod clip;
|
||||
pub mod coarse;
|
||||
pub mod encode;
|
||||
pub mod filter;
|
||||
pub mod filter_effects;
|
||||
pub mod flatten;
|
||||
pub(crate) mod flatten_simd;
|
||||
pub mod geometry;
|
||||
pub mod image_cache;
|
||||
pub mod mask;
|
||||
pub mod math;
|
||||
pub mod multi_atlas;
|
||||
pub mod paint;
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "pico_svg")]
|
||||
pub mod pico_svg;
|
||||
pub mod pixmap;
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "probe")]
|
||||
pub mod probe;
|
||||
pub mod rect;
|
||||
pub mod render_graph;
|
||||
pub mod render_state;
|
||||
pub mod simd;
|
||||
pub mod strip;
|
||||
pub mod strip_generator;
|
||||
pub mod tile;
|
||||
pub mod util;
|
||||
|
||||
pub use fearless_simd;
|
||||
pub use peniko;
|
||||
pub use peniko::color;
|
||||
pub use peniko::kurbo;
|
||||
|
||||
/// A handle to an external, user-provided texture.
|
||||
///
|
||||
/// This is resolved at render time by passing in a mapping of handles to textures, but is
|
||||
/// otherwise opaque to the renderer.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct TextureId(pub u64);
|
||||
Vendored
+116
@@ -0,0 +1,116 @@
|
||||
// Copyright 2025 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Alpha and luminance masks.
|
||||
|
||||
use crate::pixmap::Pixmap;
|
||||
use alloc::sync::Arc;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct MaskRepr {
|
||||
data: Vec<u8>,
|
||||
width: u16,
|
||||
height: u16,
|
||||
}
|
||||
|
||||
// Note that we are on purpose storing width and height inside the `Arc`
|
||||
// to reduce the memory footprint of the struct.
|
||||
/// A mask.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Mask(Arc<MaskRepr>);
|
||||
|
||||
impl Mask {
|
||||
/// Create a new alpha mask from the pixmap.
|
||||
pub fn new_alpha(pixmap: &Pixmap) -> Self {
|
||||
Self::new_with(pixmap, true)
|
||||
}
|
||||
|
||||
/// Create a new luminance mask from the pixmap.
|
||||
pub fn new_luminance(pixmap: &Pixmap) -> Self {
|
||||
Self::new_with(pixmap, false)
|
||||
}
|
||||
|
||||
/// Create a new mask from the given alpha data.
|
||||
///
|
||||
/// The `data` vector must be of length `width * height` exactly.
|
||||
///
|
||||
/// The pixels are in row-major order.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the `data` vector is not of length `width * height`.
|
||||
pub fn from_parts(data: Vec<u8>, width: u16, height: u16) -> Self {
|
||||
assert_eq!(
|
||||
data.len(),
|
||||
usize::from(width) * usize::from(height),
|
||||
"`data` should have `width * height` length"
|
||||
);
|
||||
|
||||
Self(Arc::new(MaskRepr {
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
}))
|
||||
}
|
||||
|
||||
fn new_with(pixmap: &Pixmap, alpha_mask: bool) -> Self {
|
||||
let data = pixmap
|
||||
.data()
|
||||
.iter()
|
||||
.map(|pixel| {
|
||||
if alpha_mask {
|
||||
pixel.a
|
||||
} else {
|
||||
let r = f32::from(pixel.r) / 255.;
|
||||
let g = f32::from(pixel.g) / 255.;
|
||||
let b = f32::from(pixel.b) / 255.;
|
||||
|
||||
// See CSS Masking Module Level 1 § 7.10.1
|
||||
// <https://www.w3.org/TR/css-masking-1/#MaskValues>
|
||||
// and Filter Effects Module Level 1 § 9.6
|
||||
// <https://www.w3.org/TR/filter-effects-1/#elementdef-fecolormatrix>.
|
||||
// Note r, g and b are premultiplied by alpha.
|
||||
let luma = r * 0.2126 + g * 0.7152 + b * 0.0722;
|
||||
#[expect(clippy::cast_possible_truncation, reason = "This cannot overflow")]
|
||||
{
|
||||
(luma * 255.0 + 0.5) as u8
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<u8>>();
|
||||
|
||||
Self(Arc::new(MaskRepr {
|
||||
data,
|
||||
width: pixmap.width(),
|
||||
height: pixmap.height(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Return the width of the mask.
|
||||
#[inline(always)]
|
||||
pub fn width(&self) -> u16 {
|
||||
self.0.width
|
||||
}
|
||||
|
||||
/// Return the height of the mask.
|
||||
#[inline(always)]
|
||||
pub fn height(&self) -> u16 {
|
||||
self.0.height
|
||||
}
|
||||
|
||||
/// Sample the value at a specific location.
|
||||
///
|
||||
/// This function might panic or yield a wrong result if the location
|
||||
/// is out-of-bounds.
|
||||
#[inline(always)]
|
||||
pub fn sample(&self, x: u16, y: u16) -> u8 {
|
||||
let repr = &*self.0;
|
||||
debug_assert!(
|
||||
x < repr.width && y < repr.height,
|
||||
"cannot sample mask outside of its range"
|
||||
);
|
||||
|
||||
repr.data[y as usize * repr.width as usize + x as usize]
|
||||
}
|
||||
}
|
||||
Vendored
+60
@@ -0,0 +1,60 @@
|
||||
// Copyright 2025 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Mathematical helper functions.
|
||||
|
||||
use core::ops::Sub;
|
||||
#[cfg(not(feature = "std"))]
|
||||
use peniko::kurbo::common::FloatFuncs as _;
|
||||
|
||||
// See https://raphlinus.github.io/audio/2018/09/05/sigmoid.html for a little
|
||||
// explanation of this approximation to the erf function.
|
||||
/// Approximate the erf function.
|
||||
pub fn compute_erf7(x: f32) -> f32 {
|
||||
// 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.clamp(-10., 10.);
|
||||
let x = x * core::f32::consts::FRAC_2_SQRT_PI;
|
||||
let xx = x * x;
|
||||
let x = x + (0.24295 + (0.03395 + 0.0104 * xx) * xx) * (x * xx);
|
||||
x / (1.0 + x * x).sqrt()
|
||||
}
|
||||
|
||||
// From <https://github.com/linebender/tiny-skia/blob/68b198a7210a6bbf752b43d6bc4db62445730313/path/src/scalar.rs#L12>
|
||||
// Note: If this value changes, also update NEARLY_ZERO_TOLERANCE in render_strips.wgsl
|
||||
// @see {@link https://github.com/linebender/vello/blob/58b80d660e2fc5aef3bd32b24af3f95e973aab95/sparse_strips/vello_sparse_shaders/shaders/render_strips.wgsl#L63}
|
||||
const SCALAR_NEARLY_ZERO: f32 = 1.0 / (1 << 12) as f32;
|
||||
|
||||
/// A number of useful methods for floating-point numbers.
|
||||
pub trait FloatExt: Sized + Sub<Self, Output = Self> {
|
||||
/// Whether the number is approximately 0.
|
||||
fn is_nearly_zero(&self) -> bool;
|
||||
|
||||
/// Whether the number is approximately 0, with a given tolerance.
|
||||
fn is_nearly_zero_within_tolerance(&self, tolerance: f32) -> bool;
|
||||
}
|
||||
|
||||
impl FloatExt for f32 {
|
||||
fn is_nearly_zero(&self) -> bool {
|
||||
self.is_nearly_zero_within_tolerance(SCALAR_NEARLY_ZERO)
|
||||
}
|
||||
|
||||
fn is_nearly_zero_within_tolerance(&self, tolerance: f32) -> bool {
|
||||
debug_assert!(tolerance >= 0.0, "tolerance must be positive");
|
||||
|
||||
self.abs() <= tolerance
|
||||
}
|
||||
}
|
||||
|
||||
impl FloatExt for f64 {
|
||||
fn is_nearly_zero(&self) -> bool {
|
||||
self.is_nearly_zero_within_tolerance(SCALAR_NEARLY_ZERO)
|
||||
}
|
||||
|
||||
fn is_nearly_zero_within_tolerance(&self, tolerance: f32) -> bool {
|
||||
debug_assert!(tolerance >= 0.0, "tolerance must be positive");
|
||||
|
||||
self.abs() <= tolerance as Self
|
||||
}
|
||||
}
|
||||
+760
@@ -0,0 +1,760 @@
|
||||
// Copyright 2025 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Multi-atlas management for texture atlases.
|
||||
//!
|
||||
//! This module provides support for managing multiple texture atlases, allowing for handling of
|
||||
//! large numbers of images.
|
||||
//!
|
||||
//! The allocator backend is [guillotiere](https://github.com/nical/guillotiere)'s tree-based
|
||||
//! guillotine algorithm, providing O(1) neighbor lookup during deallocation and automatic
|
||||
//! free-rect coalescing.
|
||||
|
||||
use alloc::vec::Vec;
|
||||
pub use guillotiere::AllocId;
|
||||
use guillotiere::AtlasAllocator;
|
||||
use thiserror::Error;
|
||||
|
||||
/// The result of a successful rectangle allocation within a single atlas.
|
||||
#[derive(Debug)]
|
||||
pub struct Allocation {
|
||||
/// Opaque handle used for deallocation.
|
||||
pub id: AllocId,
|
||||
/// X coordinate of the top-left corner of the allocated rectangle.
|
||||
pub x: u32,
|
||||
/// Y coordinate of the top-left corner of the allocated rectangle.
|
||||
pub y: u32,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Unified Atlas type
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Represents a single atlas in the multi-atlas system.
|
||||
pub struct Atlas {
|
||||
/// Unique identifier for this atlas.
|
||||
pub id: AtlasId,
|
||||
/// Rectangle allocator backend.
|
||||
allocator: AtlasAllocator,
|
||||
/// Current usage statistics.
|
||||
stats: AtlasUsageStats,
|
||||
/// Allocation counter.
|
||||
allocation_counter: u32,
|
||||
}
|
||||
|
||||
impl Atlas {
|
||||
/// Create a new atlas with the given ID and size.
|
||||
pub fn new(id: AtlasId, width: u32, height: u32) -> Self {
|
||||
Self {
|
||||
id,
|
||||
allocator: AtlasAllocator::new(guillotiere::size2(width as i32, height as i32)),
|
||||
stats: AtlasUsageStats {
|
||||
allocated_area: 0,
|
||||
total_area: width * height,
|
||||
allocated_count: 0,
|
||||
},
|
||||
allocation_counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to allocate an image in this atlas.
|
||||
#[expect(
|
||||
clippy::cast_sign_loss,
|
||||
reason = "coordinates are always non-negative for valid allocations"
|
||||
)]
|
||||
pub fn allocate(&mut self, width: u32, height: u32) -> Option<Allocation> {
|
||||
let alloc = self
|
||||
.allocator
|
||||
.allocate(guillotiere::size2(width as i32, height as i32))?;
|
||||
self.stats.allocated_area += width * height;
|
||||
self.stats.allocated_count += 1;
|
||||
self.allocation_counter += 1;
|
||||
Some(Allocation {
|
||||
id: alloc.id,
|
||||
x: alloc.rectangle.min.x as u32,
|
||||
y: alloc.rectangle.min.y as u32,
|
||||
})
|
||||
}
|
||||
|
||||
/// Deallocate an image from this atlas.
|
||||
pub fn deallocate(&mut self, alloc_id: AllocId, width: u32, height: u32) {
|
||||
self.allocator.deallocate(alloc_id);
|
||||
self.stats.allocated_area = self.stats.allocated_area.saturating_sub(width * height);
|
||||
self.stats.allocated_count = self.stats.allocated_count.saturating_sub(1);
|
||||
}
|
||||
|
||||
/// Get current usage statistics.
|
||||
pub fn stats(&self) -> &AtlasUsageStats {
|
||||
&self.stats
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for Atlas {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("Atlas")
|
||||
.field("id", &self.id)
|
||||
.field("stats", &self.stats)
|
||||
.field("allocation_counter", &self.allocation_counter)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MultiAtlasManager
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Manages multiple texture atlases.
|
||||
pub struct MultiAtlasManager {
|
||||
/// All atlases managed by this instance.
|
||||
atlases: Vec<Atlas>,
|
||||
/// Configuration for atlas management.
|
||||
config: AtlasConfig,
|
||||
/// Round-robin counter for allocation strategy.
|
||||
round_robin_counter: usize,
|
||||
}
|
||||
|
||||
impl MultiAtlasManager {
|
||||
/// Create a new multi-atlas manager with the given configuration.
|
||||
pub fn new(config: AtlasConfig) -> Self {
|
||||
let mut manager = Self {
|
||||
atlases: Vec::new(),
|
||||
config,
|
||||
round_robin_counter: 0,
|
||||
};
|
||||
|
||||
for _ in 0..config.initial_atlas_count {
|
||||
manager
|
||||
.create_atlas()
|
||||
.expect("Failed to create initial atlas");
|
||||
}
|
||||
|
||||
manager
|
||||
}
|
||||
|
||||
/// Get the current configuration.
|
||||
pub fn config(&self) -> &AtlasConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Create a new atlas and return its ID.
|
||||
pub fn create_atlas(&mut self) -> Result<AtlasId, AtlasError> {
|
||||
if self.atlases.len() >= self.config.max_atlases {
|
||||
return Err(AtlasError::AtlasLimitReached);
|
||||
}
|
||||
|
||||
let atlas_id = AtlasId::new(self.next_atlas_id());
|
||||
|
||||
let atlas = Atlas::new(atlas_id, self.config.atlas_size.0, self.config.atlas_size.1);
|
||||
self.atlases.push(atlas);
|
||||
|
||||
Ok(atlas_id)
|
||||
}
|
||||
|
||||
/// Get the next available atlas ID.
|
||||
pub fn next_atlas_id(&self) -> u32 {
|
||||
u32::try_from(self.atlases.len()).unwrap()
|
||||
}
|
||||
|
||||
/// Try to allocate space for an image with the given dimensions.
|
||||
pub fn try_allocate(&mut self, width: u32, height: u32) -> Result<AtlasAllocation, AtlasError> {
|
||||
self.try_allocate_excluding(width, height, None)
|
||||
}
|
||||
|
||||
/// Try to allocate space for an image with the given dimensions,
|
||||
/// optionally excluding a specific atlas.
|
||||
pub fn try_allocate_excluding(
|
||||
&mut self,
|
||||
width: u32,
|
||||
height: u32,
|
||||
exclude_atlas_id: Option<AtlasId>,
|
||||
) -> Result<AtlasAllocation, AtlasError> {
|
||||
// Check if the image is too large for any atlas
|
||||
if width > self.config.atlas_size.0 || height > self.config.atlas_size.1 {
|
||||
return Err(AtlasError::TextureTooLarge { width, height });
|
||||
}
|
||||
|
||||
// Try allocation based on strategy
|
||||
match self.config.allocation_strategy {
|
||||
AllocationStrategy::FirstFit => {
|
||||
self.allocate_first_fit(width, height, exclude_atlas_id)
|
||||
}
|
||||
AllocationStrategy::BestFit => self.allocate_best_fit(width, height, exclude_atlas_id),
|
||||
AllocationStrategy::LeastUsed => {
|
||||
self.allocate_least_used(width, height, exclude_atlas_id)
|
||||
}
|
||||
AllocationStrategy::RoundRobin => {
|
||||
self.allocate_round_robin(width, height, exclude_atlas_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Allocate using first-fit strategy: try atlases in order until one has space.
|
||||
fn allocate_first_fit(
|
||||
&mut self,
|
||||
width: u32,
|
||||
height: u32,
|
||||
exclude_atlas_id: Option<AtlasId>,
|
||||
) -> Result<AtlasAllocation, AtlasError> {
|
||||
for atlas in &mut self.atlases {
|
||||
if Some(atlas.id) == exclude_atlas_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(allocation) = atlas.allocate(width, height) {
|
||||
return Ok(AtlasAllocation {
|
||||
atlas_id: atlas.id,
|
||||
allocation,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Try creating a new atlas if auto-grow is enabled
|
||||
if self.config.auto_grow {
|
||||
let atlas_id = self.create_atlas()?;
|
||||
let atlas = self.atlases.last_mut().unwrap();
|
||||
if let Some(allocation) = atlas.allocate(width, height) {
|
||||
return Ok(AtlasAllocation {
|
||||
atlas_id,
|
||||
allocation,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Err(AtlasError::NoSpaceAvailable)
|
||||
}
|
||||
|
||||
/// Allocate using best-fit strategy: choose the atlas with the smallest remaining space that
|
||||
/// can fit the image.
|
||||
fn allocate_best_fit(
|
||||
&mut self,
|
||||
width: u32,
|
||||
height: u32,
|
||||
exclude_atlas_id: Option<AtlasId>,
|
||||
) -> Result<AtlasAllocation, AtlasError> {
|
||||
let mut best_atlas_idx = None;
|
||||
let mut best_remaining_space = u32::MAX;
|
||||
|
||||
// Find the atlas with the least remaining space that can fit the image
|
||||
for (idx, atlas) in self.atlases.iter().enumerate() {
|
||||
if Some(atlas.id) == exclude_atlas_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
let stats = atlas.stats();
|
||||
let remaining_space = stats.total_area - stats.allocated_area;
|
||||
|
||||
if remaining_space >= width * height && remaining_space < best_remaining_space {
|
||||
best_remaining_space = remaining_space;
|
||||
best_atlas_idx = Some(idx);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(idx) = best_atlas_idx {
|
||||
let atlas = &mut self.atlases[idx];
|
||||
if let Some(allocation) = atlas.allocate(width, height) {
|
||||
return Ok(AtlasAllocation {
|
||||
atlas_id: atlas.id,
|
||||
allocation,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to first-fit if best-fit didn't work
|
||||
self.allocate_first_fit(width, height, exclude_atlas_id)
|
||||
}
|
||||
|
||||
/// Allocate using least-used strategy: prefer the atlas with the lowest usage percentage.
|
||||
fn allocate_least_used(
|
||||
&mut self,
|
||||
width: u32,
|
||||
height: u32,
|
||||
exclude_atlas_id: Option<AtlasId>,
|
||||
) -> Result<AtlasAllocation, AtlasError> {
|
||||
let mut best_atlas_idx = None;
|
||||
let mut lowest_usage = f32::MAX;
|
||||
|
||||
// Find the atlas with the lowest usage percentage
|
||||
for (idx, atlas) in self.atlases.iter().enumerate() {
|
||||
if Some(atlas.id) == exclude_atlas_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
let usage = atlas.stats().usage_percentage();
|
||||
if usage < lowest_usage {
|
||||
lowest_usage = usage;
|
||||
best_atlas_idx = Some(idx);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(idx) = best_atlas_idx
|
||||
&& let Some(allocation) = self.atlases[idx].allocate(width, height)
|
||||
{
|
||||
let atlas_id = self.atlases[idx].id;
|
||||
return Ok(AtlasAllocation {
|
||||
atlas_id,
|
||||
allocation,
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback to first-fit if least-used didn't work
|
||||
self.allocate_first_fit(width, height, exclude_atlas_id)
|
||||
}
|
||||
|
||||
/// Allocate using round-robin strategy: cycle through atlases using a round-robin counter.
|
||||
fn allocate_round_robin(
|
||||
&mut self,
|
||||
width: u32,
|
||||
height: u32,
|
||||
exclude_atlas_id: Option<AtlasId>,
|
||||
) -> Result<AtlasAllocation, AtlasError> {
|
||||
if self.atlases.is_empty() {
|
||||
return self.allocate_first_fit(width, height, exclude_atlas_id);
|
||||
}
|
||||
|
||||
let start_idx = self.round_robin_counter % self.atlases.len();
|
||||
|
||||
// Try starting from the round-robin position
|
||||
for i in 0..self.atlases.len() {
|
||||
let idx = (start_idx + i) % self.atlases.len();
|
||||
|
||||
if Some(self.atlases[idx].id) == exclude_atlas_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(allocation) = self.atlases[idx].allocate(width, height) {
|
||||
let atlas_id = self.atlases[idx].id;
|
||||
self.round_robin_counter = (idx + 1) % self.atlases.len();
|
||||
return Ok(AtlasAllocation {
|
||||
atlas_id,
|
||||
allocation,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Try creating a new atlas if auto-grow is enabled
|
||||
if self.config.auto_grow {
|
||||
let atlas_id = self.create_atlas()?;
|
||||
let atlas = self.atlases.last_mut().unwrap();
|
||||
if let Some(allocation) = atlas.allocate(width, height) {
|
||||
self.round_robin_counter = self.atlases.len() - 1;
|
||||
return Ok(AtlasAllocation {
|
||||
atlas_id,
|
||||
allocation,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Err(AtlasError::NoSpaceAvailable)
|
||||
}
|
||||
|
||||
/// Deallocate space in the specified atlas.
|
||||
pub fn deallocate(
|
||||
&mut self,
|
||||
atlas_id: AtlasId,
|
||||
alloc_id: AllocId,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<(), AtlasError> {
|
||||
// Since atlases only grow (never deallocate) and id is the index into the atlases vec,
|
||||
// we can do a lookup instead of a linear search
|
||||
let atlas = self
|
||||
.atlases
|
||||
.get_mut(atlas_id.0 as usize)
|
||||
.ok_or(AtlasError::AtlasNotFound(atlas_id))?;
|
||||
atlas.deallocate(alloc_id, width, height);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get statistics for all atlases.
|
||||
pub fn atlas_stats(&self) -> Vec<(AtlasId, &AtlasUsageStats)> {
|
||||
self.atlases
|
||||
.iter()
|
||||
.map(|atlas| (atlas.id, atlas.stats()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get the number of atlases.
|
||||
pub fn atlas_count(&self) -> usize {
|
||||
self.atlases.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for MultiAtlasManager {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("MultiAtlasManager")
|
||||
.field("atlas_count", &self.atlases.len())
|
||||
.field("config", &self.config)
|
||||
.field("next_atlas_id", &self.next_atlas_id())
|
||||
.field("round_robin_counter", &self.round_robin_counter)
|
||||
.field("atlases", &self.atlases)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that can occur during atlas operations.
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum AtlasError {
|
||||
/// No space available in any atlas.
|
||||
#[error("No space available in any atlas")]
|
||||
NoSpaceAvailable,
|
||||
/// Maximum number of atlases reached.
|
||||
#[error("Maximum number of atlases reached")]
|
||||
AtlasLimitReached,
|
||||
/// The requested texture size is too large for any atlas.
|
||||
#[error("Texture too large ({width}x{height}) for atlas")]
|
||||
TextureTooLarge {
|
||||
/// The width of the requested texture.
|
||||
width: u32,
|
||||
/// The height of the requested texture.
|
||||
height: u32,
|
||||
},
|
||||
/// The specified atlas was not found.
|
||||
#[error("Atlas with Id {0:?} not found")]
|
||||
AtlasNotFound(AtlasId),
|
||||
}
|
||||
|
||||
/// Unique identifier for an atlas.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct AtlasId(pub u32);
|
||||
|
||||
impl AtlasId {
|
||||
/// Create a new atlas ID.
|
||||
pub fn new(id: u32) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
|
||||
/// Get the raw ID value.
|
||||
pub fn as_u32(self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Usage statistics for an atlas.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AtlasUsageStats {
|
||||
/// Total allocated area in pixels.
|
||||
pub allocated_area: u32,
|
||||
/// Total available area in pixels.
|
||||
pub total_area: u32,
|
||||
/// Number of allocated images.
|
||||
pub allocated_count: u32,
|
||||
}
|
||||
|
||||
impl AtlasUsageStats {
|
||||
/// Calculate usage percentage (0.0 to 1.0).
|
||||
pub fn usage_percentage(&self) -> f32 {
|
||||
if self.total_area == 0 {
|
||||
0.0
|
||||
} else {
|
||||
self.allocated_area as f32 / self.total_area as f32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of an atlas allocation attempt.
|
||||
#[derive(Debug)]
|
||||
pub struct AtlasAllocation {
|
||||
/// The atlas where the allocation was made.
|
||||
pub atlas_id: AtlasId,
|
||||
/// The allocation details.
|
||||
pub allocation: Allocation,
|
||||
}
|
||||
|
||||
/// Configuration for multiple atlas support.
|
||||
///
|
||||
/// Note that any values provided here are recommendations and might not be fully
|
||||
/// honored depending on the capabilities of the backend. For example, if you define
|
||||
/// the atlas size to be 8192x8192 but the device only supports texture sizes up to 4096x4096,
|
||||
/// the backend will likely decide to instead use the value that is compatible with the device.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct AtlasConfig {
|
||||
/// Initial number of atlases to create.
|
||||
pub initial_atlas_count: usize,
|
||||
/// Maximum number of atlases to create.
|
||||
pub max_atlases: usize,
|
||||
// TODO: Make those u16 instead?
|
||||
/// Size of each atlas texture.
|
||||
pub atlas_size: (u32, u32),
|
||||
/// Whether to automatically create new atlases when needed.
|
||||
pub auto_grow: bool,
|
||||
/// Strategy for allocating images across atlases.
|
||||
pub allocation_strategy: AllocationStrategy,
|
||||
}
|
||||
|
||||
impl Default for AtlasConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
initial_atlas_count: 1,
|
||||
max_atlases: 8,
|
||||
atlas_size: (4096, 4096),
|
||||
auto_grow: true,
|
||||
allocation_strategy: AllocationStrategy::FirstFit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Strategy for allocating images across multiple atlases.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum AllocationStrategy {
|
||||
/// Try atlases in order until one has space.
|
||||
#[default]
|
||||
FirstFit,
|
||||
/// Choose the atlas with the smallest remaining space that can fit the image.
|
||||
BestFit,
|
||||
/// Prefer the atlas with the lowest usage percentage.
|
||||
LeastUsed,
|
||||
/// Cycle through atlases in round-robin fashion.
|
||||
RoundRobin,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_atlas_creation() {
|
||||
let mut manager = MultiAtlasManager::new(AtlasConfig {
|
||||
initial_atlas_count: 0,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let atlas_id = manager.create_atlas().unwrap();
|
||||
assert_eq!(atlas_id.as_u32(), 0);
|
||||
assert_eq!(manager.atlas_count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allocation_strategies() {
|
||||
let mut manager = MultiAtlasManager::new(AtlasConfig {
|
||||
initial_atlas_count: 1,
|
||||
max_atlases: 3,
|
||||
atlas_size: (256, 256),
|
||||
allocation_strategy: AllocationStrategy::FirstFit,
|
||||
auto_grow: true,
|
||||
});
|
||||
|
||||
// Should create atlas automatically
|
||||
let allocation = manager.try_allocate(100, 100).unwrap();
|
||||
assert_eq!(allocation.atlas_id.as_u32(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_atlas_limit() {
|
||||
let mut manager = MultiAtlasManager::new(AtlasConfig {
|
||||
initial_atlas_count: 1,
|
||||
max_atlases: 1,
|
||||
atlas_size: (256, 256),
|
||||
allocation_strategy: AllocationStrategy::FirstFit,
|
||||
auto_grow: false,
|
||||
});
|
||||
|
||||
assert!(manager.create_atlas().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_texture_too_large() {
|
||||
let mut manager = MultiAtlasManager::new(AtlasConfig {
|
||||
atlas_size: (256, 256),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let result = manager.try_allocate(300, 300);
|
||||
assert!(matches!(result, Err(AtlasError::TextureTooLarge { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_first_fit_allocation_strategy() {
|
||||
let mut manager = MultiAtlasManager::new(AtlasConfig {
|
||||
initial_atlas_count: 3,
|
||||
max_atlases: 3,
|
||||
atlas_size: (256, 256),
|
||||
allocation_strategy: AllocationStrategy::FirstFit,
|
||||
auto_grow: false,
|
||||
});
|
||||
|
||||
// First allocation should go to atlas 0
|
||||
let allocation0 = manager.try_allocate(100, 100).unwrap();
|
||||
assert_eq!(allocation0.atlas_id.as_u32(), 0);
|
||||
|
||||
// Second allocation should also go to atlas 0 (first fit)
|
||||
let allocation1 = manager.try_allocate(50, 50).unwrap();
|
||||
assert_eq!(allocation1.atlas_id.as_u32(), 0);
|
||||
|
||||
// Third allocation should still go to atlas 0 (first fit continues to use same atlas)
|
||||
let allocation2 = manager.try_allocate(80, 80).unwrap();
|
||||
assert_eq!(allocation2.atlas_id.as_u32(), 0);
|
||||
|
||||
// Try to allocate something very large that definitely won't fit in atlas 0's remaining space
|
||||
// This should force it to go to atlas 1
|
||||
let allocation3 = manager.try_allocate(200, 200).unwrap();
|
||||
assert_eq!(allocation3.atlas_id.as_u32(), 1);
|
||||
|
||||
// Next small allocation should go back to atlas 0 (first fit tries atlas 0 first)
|
||||
let allocation4 = manager.try_allocate(20, 20).unwrap();
|
||||
assert_eq!(allocation4.atlas_id.as_u32(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_best_fit_allocation_strategy() {
|
||||
let mut manager = MultiAtlasManager::new(AtlasConfig {
|
||||
initial_atlas_count: 3,
|
||||
max_atlases: 3,
|
||||
atlas_size: (256, 256),
|
||||
allocation_strategy: AllocationStrategy::BestFit,
|
||||
auto_grow: false,
|
||||
});
|
||||
|
||||
// All atlases start empty, so first allocation goes to atlas 0 (first available)
|
||||
let allocation0 = manager.try_allocate(150, 150).unwrap();
|
||||
assert_eq!(allocation0.atlas_id.as_u32(), 0);
|
||||
|
||||
// Second allocation should also go to atlas 0 since it still has the least remaining space
|
||||
// that can fit the image (all atlases have same remaining space, so it picks the first)
|
||||
let allocation1 = manager.try_allocate(100, 100).unwrap();
|
||||
assert_eq!(allocation1.atlas_id.as_u32(), 0);
|
||||
|
||||
// Now atlas 0 has less remaining space than atlases 1 and 2
|
||||
// For a small allocation, it should still go to atlas 0 (best fit - least remaining space)
|
||||
let allocation2 = manager.try_allocate(100, 100).unwrap();
|
||||
assert_eq!(allocation2.atlas_id.as_u32(), 0);
|
||||
|
||||
// Now try to allocate something very large that won't fit in atlas 0's remaining space
|
||||
// This should force it to go to atlas 1 (which has the most remaining space)
|
||||
let allocation3 = manager.try_allocate(200, 200).unwrap();
|
||||
assert_eq!(allocation3.atlas_id.as_u32(), 1);
|
||||
|
||||
// Now atlas 1 has less remaining space
|
||||
// A small allocation should go to atlas 0 as it can
|
||||
let allocation4 = manager.try_allocate(80, 80).unwrap();
|
||||
assert_eq!(allocation4.atlas_id.as_u32(), 0);
|
||||
|
||||
// Now atlas 1 has less remaining space but it can't fit the allocation
|
||||
// It should go to atlas 2 (best fit - least remaining space)
|
||||
let allocation5 = manager.try_allocate(80, 80).unwrap();
|
||||
assert_eq!(allocation5.atlas_id.as_u32(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_least_used_allocation_strategy() {
|
||||
let mut manager = MultiAtlasManager::new(AtlasConfig {
|
||||
initial_atlas_count: 3,
|
||||
max_atlases: 3,
|
||||
atlas_size: (256, 256),
|
||||
allocation_strategy: AllocationStrategy::LeastUsed,
|
||||
auto_grow: false,
|
||||
});
|
||||
|
||||
// First allocation goes to atlas 0 (all atlases have 0% usage, picks first)
|
||||
let allocation0 = manager.try_allocate(100, 100).unwrap();
|
||||
assert_eq!(allocation0.atlas_id.as_u32(), 0);
|
||||
|
||||
// Second allocation should go to atlas 1 (least used among remaining)
|
||||
let allocation1 = manager.try_allocate(50, 50).unwrap();
|
||||
assert_eq!(allocation1.atlas_id.as_u32(), 1);
|
||||
|
||||
// Third allocation should go to atlas 2 (least used)
|
||||
let allocation2 = manager.try_allocate(30, 30).unwrap();
|
||||
assert_eq!(allocation2.atlas_id.as_u32(), 2);
|
||||
|
||||
// Fourth allocation should go to atlas 2 again (still least used)
|
||||
let allocation3 = manager.try_allocate(30, 30).unwrap();
|
||||
assert_eq!(allocation3.atlas_id.as_u32(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_robin_allocation_strategy() {
|
||||
let mut manager = MultiAtlasManager::new(AtlasConfig {
|
||||
initial_atlas_count: 3,
|
||||
max_atlases: 3,
|
||||
atlas_size: (256, 256),
|
||||
allocation_strategy: AllocationStrategy::RoundRobin,
|
||||
auto_grow: false,
|
||||
});
|
||||
|
||||
// Allocations should cycle through atlases in order
|
||||
let allocation0 = manager.try_allocate(50, 50).unwrap();
|
||||
assert_eq!(allocation0.atlas_id.as_u32(), 0);
|
||||
|
||||
let allocation1 = manager.try_allocate(50, 50).unwrap();
|
||||
assert_eq!(allocation1.atlas_id.as_u32(), 1);
|
||||
|
||||
let allocation2 = manager.try_allocate(50, 50).unwrap();
|
||||
assert_eq!(allocation2.atlas_id.as_u32(), 2);
|
||||
|
||||
// Should wrap back to atlas 0
|
||||
let allocation3 = manager.try_allocate(50, 50).unwrap();
|
||||
assert_eq!(allocation3.atlas_id.as_u32(), 0);
|
||||
|
||||
// Continue the cycle
|
||||
let allocation4 = manager.try_allocate(50, 50).unwrap();
|
||||
assert_eq!(allocation4.atlas_id.as_u32(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auto_grow() {
|
||||
let mut manager = MultiAtlasManager::new(AtlasConfig {
|
||||
initial_atlas_count: 1,
|
||||
max_atlases: 3,
|
||||
atlas_size: (256, 256),
|
||||
allocation_strategy: AllocationStrategy::FirstFit,
|
||||
auto_grow: true,
|
||||
});
|
||||
|
||||
let allocation0 = manager.try_allocate(256, 256).unwrap();
|
||||
assert_eq!(allocation0.atlas_id.as_u32(), 0);
|
||||
|
||||
let allocation1 = manager.try_allocate(256, 256).unwrap();
|
||||
assert_eq!(allocation1.atlas_id.as_u32(), 1);
|
||||
|
||||
let allocation2 = manager.try_allocate(256, 256).unwrap();
|
||||
assert_eq!(allocation2.atlas_id.as_u32(), 2);
|
||||
}
|
||||
|
||||
fn test_allocate_excluding_with_strategy(strategy: AllocationStrategy) {
|
||||
let mut manager = MultiAtlasManager::new(AtlasConfig {
|
||||
initial_atlas_count: 3,
|
||||
max_atlases: 3,
|
||||
atlas_size: (256, 256),
|
||||
allocation_strategy: strategy,
|
||||
auto_grow: false,
|
||||
});
|
||||
|
||||
let allocation0 = manager.try_allocate(100, 100).unwrap();
|
||||
let first_atlas = allocation0.atlas_id;
|
||||
let allocation1 = manager
|
||||
.try_allocate_excluding(256, 256, Some(first_atlas))
|
||||
.unwrap();
|
||||
assert_ne!(allocation1.atlas_id, first_atlas);
|
||||
|
||||
let second_atlas = allocation1.atlas_id;
|
||||
let allocation2 = manager
|
||||
.try_allocate_excluding(100, 100, Some(second_atlas))
|
||||
.unwrap();
|
||||
assert_ne!(allocation2.atlas_id, second_atlas);
|
||||
|
||||
let allocation3 = manager
|
||||
.try_allocate_excluding(100, 100, Some(first_atlas))
|
||||
.unwrap();
|
||||
assert_ne!(allocation3.atlas_id, first_atlas);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allocate_excluding_first_fit() {
|
||||
test_allocate_excluding_with_strategy(AllocationStrategy::FirstFit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allocate_excluding_best_fit() {
|
||||
test_allocate_excluding_with_strategy(AllocationStrategy::BestFit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allocate_excluding_least_used() {
|
||||
test_allocate_excluding_with_strategy(AllocationStrategy::LeastUsed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allocate_excluding_round_robin() {
|
||||
test_allocate_excluding_with_strategy(AllocationStrategy::RoundRobin);
|
||||
}
|
||||
}
|
||||
Vendored
+276
@@ -0,0 +1,276 @@
|
||||
// Copyright 2025 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Types for paints.
|
||||
|
||||
use crate::pixmap::Pixmap;
|
||||
use alloc::sync::Arc;
|
||||
pub use peniko::Color;
|
||||
use peniko::{
|
||||
Gradient,
|
||||
color::{AlphaColor, PremulRgba8, Srgb},
|
||||
};
|
||||
|
||||
/// A paint that needs to be resolved via its index.
|
||||
// In the future, we might add additional flags, that's why we have
|
||||
// this thin wrapper around u32, so we can change the underlying
|
||||
// representation without breaking the API.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IndexedPaint(u32);
|
||||
|
||||
impl IndexedPaint {
|
||||
/// Create a new indexed paint from an index.
|
||||
pub fn new(index: usize) -> Self {
|
||||
Self(u32::try_from(index).expect("exceeded the maximum number of paints"))
|
||||
}
|
||||
|
||||
/// Return the index of the paint.
|
||||
pub fn index(&self) -> usize {
|
||||
usize::try_from(self.0).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// A paint that is used internally by a rendering frontend to store how a wide tile command
|
||||
/// should be painted. There are only two types of paint:
|
||||
///
|
||||
/// 1) Simple solid colors, which are stored in premultiplied representation so that
|
||||
/// each wide tile doesn't have to recompute it.
|
||||
/// 2) Indexed paints, which can represent any arbitrary, more complex paint that is
|
||||
/// determined by the frontend. The intended way of using this is to store a vector
|
||||
/// of paints and store its index inside `IndexedPaint`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Paint {
|
||||
/// A premultiplied RGBA8 color.
|
||||
Solid(PremulColor),
|
||||
/// A paint that needs to be resolved via an index.
|
||||
Indexed(IndexedPaint),
|
||||
}
|
||||
|
||||
impl From<AlphaColor<Srgb>> for Paint {
|
||||
fn from(value: AlphaColor<Srgb>) -> Self {
|
||||
Self::Solid(PremulColor::from_alpha_color(value))
|
||||
}
|
||||
}
|
||||
|
||||
/// Opaque image handle
|
||||
#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)]
|
||||
pub struct ImageId(u32);
|
||||
|
||||
impl ImageId {
|
||||
// TODO: make this private in future
|
||||
/// Create a new image id from a u32.
|
||||
pub fn new(value: u32) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
|
||||
/// Return the image id as a u32.
|
||||
pub fn as_u32(&self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Bitmap source used by `Image`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ImageSource {
|
||||
/// Pixmap pixels travel with the scene packet.
|
||||
Pixmap(Arc<Pixmap>),
|
||||
/// Pixmap pixels were registered earlier; this is just a handle.
|
||||
OpaqueId {
|
||||
/// The image handle.
|
||||
id: ImageId,
|
||||
/// Whether the image may contain non-opaque pixels.
|
||||
may_have_transparency: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl ImageSource {
|
||||
/// Create an [`ImageSource`] from a pre-registered image handle.
|
||||
///
|
||||
/// Conservatively assumes the image may have non-opaque pixels.
|
||||
/// Use [`Self::opaque_id_with_transparency_hint`] when you know the image is fully opaque.
|
||||
pub fn opaque_id(id: ImageId) -> Self {
|
||||
Self::OpaqueId {
|
||||
id,
|
||||
may_have_transparency: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an [`ImageSource`] from a pre-registered image handle,
|
||||
/// with an explicit hint about whether the image may have non-opaque pixels.
|
||||
pub fn opaque_id_with_transparency_hint(id: ImageId, may_have_transparency: bool) -> Self {
|
||||
Self::OpaqueId {
|
||||
id,
|
||||
may_have_transparency,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether this image source may contain non-opaque pixels.
|
||||
pub fn may_have_transparency(&self) -> bool {
|
||||
match self {
|
||||
Self::Pixmap(p) => p.may_have_transparency(),
|
||||
Self::OpaqueId {
|
||||
may_have_transparency,
|
||||
..
|
||||
} => *may_have_transparency,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a [`peniko::ImageData`] to an [`ImageSource`].
|
||||
///
|
||||
/// This is a somewhat lossy conversion, as the image data data is transformed to
|
||||
/// [premultiplied RGBA8](`PremulRgba8`).
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This panics if `image` has a `width` or `height` greater than `u16::MAX`.
|
||||
pub fn from_peniko_image_data(image: &peniko::ImageData) -> Self {
|
||||
// TODO: how do we deal with `peniko::ImageFormat` growing? See also
|
||||
// <https://github.com/linebender/vello/pull/996#discussion_r2080510863>.
|
||||
let do_alpha_multiply = image.alpha_type != peniko::ImageAlphaType::AlphaPremultiplied;
|
||||
|
||||
assert!(
|
||||
image.width <= u16::MAX as u32 && image.height <= u16::MAX as u32,
|
||||
"The image is too big. Its width and height can be no larger than {} pixels.",
|
||||
u16::MAX,
|
||||
);
|
||||
let width = image.width.try_into().unwrap();
|
||||
let height = image.height.try_into().unwrap();
|
||||
|
||||
// TODO: SIMD
|
||||
#[expect(clippy::cast_possible_truncation, reason = "This cannot overflow.")]
|
||||
let pixels = image
|
||||
.data
|
||||
.data()
|
||||
.chunks_exact(4)
|
||||
.map(|pixel| {
|
||||
let rgba: [u8; 4] = match image.format {
|
||||
peniko::ImageFormat::Rgba8 => pixel.try_into().unwrap(),
|
||||
peniko::ImageFormat::Bgra8 => [pixel[2], pixel[1], pixel[0], pixel[3]],
|
||||
format => unimplemented!("Unsupported image format: {format:?}"),
|
||||
};
|
||||
let alpha = u16::from(rgba[3]);
|
||||
let multiply = |component| ((alpha * u16::from(component)) / 255) as u8;
|
||||
if do_alpha_multiply {
|
||||
PremulRgba8 {
|
||||
r: multiply(rgba[0]),
|
||||
g: multiply(rgba[1]),
|
||||
b: multiply(rgba[2]),
|
||||
a: rgba[3],
|
||||
}
|
||||
} else {
|
||||
PremulRgba8 {
|
||||
r: rgba[0],
|
||||
g: rgba[1],
|
||||
b: rgba[2],
|
||||
a: rgba[3],
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let pixmap = Pixmap::from_parts(pixels, width, height);
|
||||
|
||||
Self::Pixmap(Arc::new(pixmap))
|
||||
}
|
||||
}
|
||||
|
||||
/// An image.
|
||||
pub type Image = peniko::ImageBrush<ImageSource>;
|
||||
|
||||
/// Trait for resolving opaque image IDs to pixmaps at rasterization time.
|
||||
///
|
||||
/// This allows delaying the resolution of `ImageSource::OpaqueId` until the
|
||||
/// image is actually needed during rasterization, enabling patterns like
|
||||
/// dynamic sprite atlases where the image data may be updated between
|
||||
/// encoding and rendering.
|
||||
pub trait ImageResolver: Send + Sync {
|
||||
/// Resolve an `ImageId` to its pixmap data.
|
||||
///
|
||||
/// This method may be called repeatedly (dozens or even hundreds of times
|
||||
/// per frame) and should therefore be very fast.
|
||||
///
|
||||
/// Returns `None` if the image ID is not found in the registry.
|
||||
fn resolve(&self, id: ImageId) -> Option<Arc<Pixmap>>;
|
||||
}
|
||||
|
||||
/// A no-op image resolver that always returns `None`.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct NoOpImageResolver;
|
||||
|
||||
impl ImageResolver for NoOpImageResolver {
|
||||
fn resolve(&self, _id: ImageId) -> Option<Arc<Pixmap>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A premultiplied color.
|
||||
#[derive(Debug, Clone, PartialEq, Copy)]
|
||||
pub struct PremulColor {
|
||||
premul_u8: PremulRgba8,
|
||||
premul_f32: peniko::color::PremulColor<Srgb>,
|
||||
}
|
||||
|
||||
impl PremulColor {
|
||||
/// Create a new premultiplied color.
|
||||
pub fn from_alpha_color(color: AlphaColor<Srgb>) -> Self {
|
||||
Self::from_premul_color(color.premultiply())
|
||||
}
|
||||
|
||||
/// Create a new premultiplied color from `peniko::PremulColor`.
|
||||
pub fn from_premul_color(color: peniko::color::PremulColor<Srgb>) -> Self {
|
||||
Self {
|
||||
premul_u8: color.to_rgba8(),
|
||||
premul_f32: color,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the color as a premultiplied RGBA8 color.
|
||||
pub fn as_premul_rgba8(&self) -> PremulRgba8 {
|
||||
self.premul_u8
|
||||
}
|
||||
|
||||
/// Return the color as a premultiplied RGBAF32 color.
|
||||
pub fn as_premul_f32(&self) -> peniko::color::PremulColor<Srgb> {
|
||||
self.premul_f32
|
||||
}
|
||||
|
||||
/// Return whether the color is opaque (i.e. doesn't have transparency).
|
||||
pub fn is_opaque(&self) -> bool {
|
||||
self.premul_f32.components[3] == 1.0
|
||||
}
|
||||
}
|
||||
|
||||
/// How tint color is applied to an image.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum TintMode {
|
||||
/// Alpha-mask tinting: `tint_premul * source.alpha`.
|
||||
///
|
||||
/// The source image's alpha channel is used as a coverage mask,
|
||||
/// and the result is filled with the premultiplied tint color.
|
||||
/// This is the standard approach for glyph / monochrome image tinting.
|
||||
AlphaMask = 0,
|
||||
/// Component-wise multiply: `source * tint`.
|
||||
///
|
||||
/// Each channel of the source pixel is multiplied by the corresponding
|
||||
/// channel of the tint color. This works well for full-color images.
|
||||
Multiply = 1,
|
||||
}
|
||||
|
||||
impl TintMode {
|
||||
/// Return the discriminant as a `u32`.
|
||||
pub fn as_u32(self) -> u32 {
|
||||
self as u32
|
||||
}
|
||||
}
|
||||
|
||||
/// A tint applied to image paints.
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub struct Tint {
|
||||
/// The tint color.
|
||||
pub color: Color,
|
||||
/// How the tint is applied.
|
||||
pub mode: TintMode,
|
||||
}
|
||||
|
||||
/// A kind of paint that can be used for filling and stroking shapes.
|
||||
pub type PaintType = peniko::Brush<Image, Gradient>;
|
||||
Vendored
+356
@@ -0,0 +1,356 @@
|
||||
// Copyright 2024 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
// Below is copied, lightly adapted, from Vello.
|
||||
|
||||
//! A minimal SVG parser for rendering examples
|
||||
//!
|
||||
//! This module provides a simple SVG parser to load and render SVG files
|
||||
//! for demonstration purposes. It supports basic SVG features like paths,
|
||||
//! fill, stroke, and grouping.
|
||||
|
||||
#![expect(
|
||||
clippy::print_stderr,
|
||||
reason = "We don't have a better way to handle these errors, plus this code is not public API"
|
||||
)]
|
||||
|
||||
extern crate std;
|
||||
|
||||
use crate::color::{AlphaColor, DynamicColor, Srgb, palette};
|
||||
use crate::kurbo::{Affine, BezPath, Point, Size, Vec2};
|
||||
use alloc::boxed::Box;
|
||||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
use core::str::FromStr;
|
||||
use roxmltree::{Document, Node};
|
||||
use std::eprintln;
|
||||
|
||||
/// A simplified representation of an SVG document
|
||||
#[derive(Debug)]
|
||||
pub struct PicoSvg {
|
||||
/// The items (shapes, groups) contained in the SVG
|
||||
pub items: Vec<Item>,
|
||||
/// The size of the SVG document
|
||||
pub size: Size,
|
||||
}
|
||||
|
||||
/// Represents a single item in an SVG document
|
||||
#[derive(Debug)]
|
||||
pub enum Item {
|
||||
/// A filled shape
|
||||
Fill(FillItem),
|
||||
/// A stroked shape
|
||||
Stroke(StrokeItem),
|
||||
/// A group of items
|
||||
Group(GroupItem),
|
||||
}
|
||||
|
||||
/// A stroke item with styling information
|
||||
#[derive(Debug)]
|
||||
pub struct StrokeItem {
|
||||
/// The width of the stroke
|
||||
pub width: f64,
|
||||
/// The color of the stroke
|
||||
pub color: AlphaColor<Srgb>,
|
||||
/// The path to be stroked
|
||||
pub path: BezPath,
|
||||
}
|
||||
|
||||
/// A fill item with styling information
|
||||
#[derive(Debug)]
|
||||
pub struct FillItem {
|
||||
/// The color to fill with
|
||||
pub color: AlphaColor<Srgb>,
|
||||
/// The path to be filled
|
||||
pub path: BezPath,
|
||||
}
|
||||
|
||||
/// A group of items that can be transformed together
|
||||
#[derive(Debug)]
|
||||
pub struct GroupItem {
|
||||
/// The affine transformation to apply to all children
|
||||
pub affine: Affine,
|
||||
/// The child items in this group
|
||||
pub children: Vec<Item>,
|
||||
}
|
||||
|
||||
struct Parser {
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
impl PicoSvg {
|
||||
/// Load an SVG document from a string
|
||||
pub fn load(xml_string: &str, scale: f64) -> Result<Self, Box<dyn core::error::Error>> {
|
||||
let doc = Document::parse(xml_string)?;
|
||||
let root = doc.root_element();
|
||||
let mut parser = Parser::new(scale);
|
||||
let root_width = root.attribute("width").and_then(|s| f64::from_str(s).ok());
|
||||
let root_height = root.attribute("height").and_then(|s| f64::from_str(s).ok());
|
||||
let (origin, viewbox_size) = root
|
||||
.attribute("viewBox")
|
||||
.and_then(|vb_attr| {
|
||||
let vs: Vec<f64> = vb_attr
|
||||
.split(' ')
|
||||
.map(|s| f64::from_str(s).unwrap())
|
||||
.collect();
|
||||
if let &[x, y, width, height] = vs.as_slice() {
|
||||
Some((Point { x, y }, Size { width, height }))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unzip();
|
||||
|
||||
let mut transform = if let Some(origin) = origin {
|
||||
Affine::translate(origin.to_vec2() * -1.0)
|
||||
} else {
|
||||
Affine::IDENTITY
|
||||
};
|
||||
|
||||
transform *= match (root_width, root_height, viewbox_size) {
|
||||
(None, None, Some(_)) => Affine::IDENTITY,
|
||||
(Some(w), Some(h), Some(s)) => {
|
||||
Affine::scale_non_uniform(1.0 / s.width * w, 1.0 / s.height * h)
|
||||
}
|
||||
(Some(w), None, Some(s)) => Affine::scale(1.0 / s.width * w),
|
||||
(None, Some(h), Some(s)) => Affine::scale(1.0 / s.height * h),
|
||||
_ => Affine::IDENTITY,
|
||||
};
|
||||
|
||||
let size = match (root_width, root_height, viewbox_size) {
|
||||
(None, None, Some(s)) => s,
|
||||
(mw, mh, None) => Size {
|
||||
width: mw.unwrap_or(300_f64),
|
||||
height: mh.unwrap_or(150_f64),
|
||||
},
|
||||
(Some(w), None, Some(s)) => Size {
|
||||
width: w,
|
||||
height: 1.0 / w * s.width * s.height,
|
||||
},
|
||||
(None, Some(h), Some(s)) => Size {
|
||||
width: 1.0 / h * s.height * s.width,
|
||||
height: h,
|
||||
},
|
||||
(Some(width), Some(height), Some(_)) => Size { width, height },
|
||||
};
|
||||
|
||||
transform *= if scale >= 0.0 {
|
||||
Affine::scale(scale)
|
||||
} else {
|
||||
Affine::new([-scale, 0.0, 0.0, scale, 0.0, 0.0])
|
||||
};
|
||||
let props = RecursiveProperties {
|
||||
fill: Some(palette::css::BLACK),
|
||||
};
|
||||
// The root element is the svg document element, which we don't care about
|
||||
let mut items = Vec::new();
|
||||
for node in root.children() {
|
||||
parser.rec_parse(node, &props, &mut items)?;
|
||||
}
|
||||
let root_group = Item::Group(GroupItem {
|
||||
affine: transform,
|
||||
children: items,
|
||||
});
|
||||
Ok(Self {
|
||||
items: vec![root_group],
|
||||
size,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RecursiveProperties {
|
||||
fill: Option<AlphaColor<Srgb>>,
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
fn new(scale: f64) -> Self {
|
||||
Self { scale }
|
||||
}
|
||||
|
||||
fn rec_parse(
|
||||
&mut self,
|
||||
node: Node<'_, '_>,
|
||||
properties: &RecursiveProperties,
|
||||
items: &mut Vec<Item>,
|
||||
) -> Result<(), Box<dyn core::error::Error>> {
|
||||
if node.is_element() {
|
||||
let mut properties = properties.clone();
|
||||
if let Some(fill_color) = node.attribute("fill") {
|
||||
if fill_color == "none" {
|
||||
properties.fill = None;
|
||||
} else {
|
||||
let color = parse_color(fill_color);
|
||||
let color = modify_opacity(color, "fill-opacity", node);
|
||||
// TODO: Handle recursive opacity properly
|
||||
let color = modify_opacity(color, "opacity", node);
|
||||
properties.fill = Some(color);
|
||||
}
|
||||
}
|
||||
match node.tag_name().name() {
|
||||
"g" => {
|
||||
let mut children = Vec::new();
|
||||
let mut affine = Affine::default();
|
||||
if let Some(transform) = node.attribute("transform") {
|
||||
affine = parse_transform(transform);
|
||||
}
|
||||
for child in node.children() {
|
||||
self.rec_parse(child, &properties, &mut children)?;
|
||||
}
|
||||
items.push(Item::Group(GroupItem { affine, children }));
|
||||
}
|
||||
"path" => {
|
||||
let d = node.attribute("d").ok_or("missing 'd' attribute")?;
|
||||
let bp = BezPath::from_svg(d)?;
|
||||
let path = bp;
|
||||
if let Some(color) = properties.fill {
|
||||
items.push(Item::Fill(FillItem {
|
||||
color,
|
||||
path: path.clone(),
|
||||
}));
|
||||
}
|
||||
if let Some(stroke_color) = node.attribute("stroke")
|
||||
&& stroke_color != "none"
|
||||
{
|
||||
let width = node
|
||||
.attribute("stroke-width")
|
||||
.map(|a| f64::from_str(a).unwrap_or(1.0))
|
||||
.unwrap_or(1.0)
|
||||
* self.scale.abs();
|
||||
let color = parse_color(stroke_color);
|
||||
let color = modify_opacity(color, "stroke-opacity", node);
|
||||
// TODO: Handle recursive opacity properly
|
||||
let color = modify_opacity(color, "opacity", node);
|
||||
items.push(Item::Stroke(StrokeItem { width, color, path }));
|
||||
}
|
||||
}
|
||||
other => eprintln!("Unhandled node type {other}"),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_transform(transform: &str) -> Affine {
|
||||
let mut nt = Affine::IDENTITY;
|
||||
for ts in transform.split(')').map(str::trim) {
|
||||
nt *= if let Some(s) = ts.strip_prefix("matrix(") {
|
||||
let vals = s
|
||||
.split([',', ' '])
|
||||
.map(str::parse)
|
||||
.collect::<Result<Vec<f64>, _>>()
|
||||
.expect("Could parse all values of 'matrix' as floats");
|
||||
Affine::new(
|
||||
vals.try_into()
|
||||
.expect("Should be six arguments to `matrix`"),
|
||||
)
|
||||
} else if let Some(s) = ts.strip_prefix("translate(") {
|
||||
if let Ok(vals) = s
|
||||
.split([',', ' '])
|
||||
.map(str::trim)
|
||||
.map(str::parse)
|
||||
.collect::<Result<Vec<f64>, _>>()
|
||||
{
|
||||
match vals.as_slice() {
|
||||
&[x, y] => Affine::translate(Vec2 { x, y }),
|
||||
_ => Affine::IDENTITY,
|
||||
}
|
||||
} else {
|
||||
Affine::IDENTITY
|
||||
}
|
||||
} else if let Some(s) = ts.strip_prefix("scale(") {
|
||||
if let Ok(vals) = s
|
||||
.split([',', ' '])
|
||||
.map(str::trim)
|
||||
.map(str::parse)
|
||||
.collect::<Result<Vec<f64>, _>>()
|
||||
{
|
||||
match *vals.as_slice() {
|
||||
[x, y] => Affine::scale_non_uniform(x, y),
|
||||
[x] => Affine::scale(x),
|
||||
_ => Affine::IDENTITY,
|
||||
}
|
||||
} else {
|
||||
Affine::IDENTITY
|
||||
}
|
||||
} else if let Some(s) = ts.strip_prefix("scaleX(") {
|
||||
s.trim()
|
||||
.parse()
|
||||
.ok()
|
||||
.map(|x| Affine::scale_non_uniform(x, 1.0))
|
||||
.unwrap_or(Affine::IDENTITY)
|
||||
} else if let Some(s) = ts.strip_prefix("scaleY(") {
|
||||
s.trim()
|
||||
.parse()
|
||||
.ok()
|
||||
.map(|y| Affine::scale_non_uniform(1.0, y))
|
||||
.unwrap_or(Affine::IDENTITY)
|
||||
} else {
|
||||
if !ts.is_empty() {
|
||||
eprintln!("Did not understand transform attribute {ts:?})");
|
||||
}
|
||||
Affine::IDENTITY
|
||||
};
|
||||
}
|
||||
nt
|
||||
}
|
||||
|
||||
fn parse_color(color: &str) -> AlphaColor<Srgb> {
|
||||
let color = color.trim();
|
||||
crate::color::parse_color(color.trim())
|
||||
.map(DynamicColor::to_alpha_color)
|
||||
.unwrap_or(palette::css::FUCHSIA.with_alpha(0.5))
|
||||
}
|
||||
|
||||
fn modify_opacity(
|
||||
color: AlphaColor<Srgb>,
|
||||
attr_name: &str,
|
||||
node: Node<'_, '_>,
|
||||
) -> AlphaColor<Srgb> {
|
||||
if let Some(opacity) = node.attribute(attr_name) {
|
||||
let alpha: f32 = if let Some(o) = opacity.strip_suffix('%') {
|
||||
let pctg = o.parse().unwrap_or(100.0);
|
||||
pctg * 0.01
|
||||
} else {
|
||||
opacity.parse().unwrap_or(1.0)
|
||||
};
|
||||
color.with_alpha(alpha.clamp(0., 1.))
|
||||
} else {
|
||||
color
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_color;
|
||||
use crate::color::{AlphaColor, Srgb, palette};
|
||||
|
||||
fn assert_close_color(c1: AlphaColor<Srgb>, c2: AlphaColor<Srgb>) {
|
||||
const EPSILON: f32 = 1e-4;
|
||||
assert_eq!(c1.cs, c2.cs);
|
||||
for i in 0..4 {
|
||||
assert!((c1.components[i] - c2.components[i]).abs() < EPSILON);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_colors() {
|
||||
let lime = palette::css::LIME;
|
||||
let lime_a = lime.with_alpha(0.4);
|
||||
|
||||
let named = parse_color("lime");
|
||||
assert_close_color(lime, named);
|
||||
|
||||
let hex = parse_color("#00ff00");
|
||||
assert_close_color(lime, hex);
|
||||
|
||||
let rgb = parse_color("rgb(0, 255, 0)");
|
||||
assert_close_color(lime, rgb);
|
||||
|
||||
let modern = parse_color("color(srgb 0 1 0)");
|
||||
assert_close_color(lime, modern);
|
||||
|
||||
let modern_a = parse_color("color(srgb 0 1 0 / 0.4)");
|
||||
assert_close_color(lime_a, modern_a);
|
||||
}
|
||||
}
|
||||
Vendored
+378
@@ -0,0 +1,378 @@
|
||||
// Copyright 2025 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! A simple pixmap type.
|
||||
|
||||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
#[cfg(feature = "png")]
|
||||
use std::io::{BufRead, Seek};
|
||||
|
||||
use crate::peniko::color::{PremulRgba8, Rgba8};
|
||||
|
||||
#[cfg(feature = "png")]
|
||||
extern crate std;
|
||||
|
||||
/// A pixmap of premultiplied RGBA8 values backed by [`u8`][core::u8].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Pixmap {
|
||||
/// Width of the pixmap in pixels.
|
||||
width: u16,
|
||||
/// Height of the pixmap in pixels.
|
||||
height: u16,
|
||||
/// Buffer of the pixmap in RGBA8 format.
|
||||
buf: Vec<PremulRgba8>,
|
||||
/// Whether the pixmap may have non-opaque pixels.
|
||||
///
|
||||
/// Note: This may become stale if pixels are modified via [`data_mut()`](Self::data_mut),
|
||||
/// [`data_as_u8_slice_mut()`](Self::data_as_u8_slice_mut), or [`set_pixel()`](Self::set_pixel).
|
||||
may_have_transparency: bool,
|
||||
}
|
||||
|
||||
impl Pixmap {
|
||||
/// Create a new pixmap with the given width and height in pixels.
|
||||
///
|
||||
/// All pixels are initialized to transparent black.
|
||||
pub fn new(width: u16, height: u16) -> Self {
|
||||
let buf = vec![PremulRgba8::from_u32(0); width as usize * height as usize];
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
buf,
|
||||
may_have_transparency: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new pixmap with the given premultiplied RGBA8 data.
|
||||
///
|
||||
/// The `data` vector must be of length `width * height` exactly.
|
||||
///
|
||||
/// The pixels are in row-major order.
|
||||
///
|
||||
/// This assumes the image may have transparent pixels. Use
|
||||
/// [`from_parts_with_opacity`](Self::from_parts_with_opacity) if you already
|
||||
/// know the opacity status to enable optimizations.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the `data` vector is not of length `width * height`.
|
||||
pub fn from_parts(data: Vec<PremulRgba8>, width: u16, height: u16) -> Self {
|
||||
Self::from_parts_with_opacity(data, width, height, true)
|
||||
}
|
||||
|
||||
/// Create a new pixmap with the given premultiplied RGBA8 data and precomputed opacity flag.
|
||||
///
|
||||
/// The `data` vector must be of length `width * height` exactly.
|
||||
///
|
||||
/// The pixels are in row-major order.
|
||||
///
|
||||
/// Use this when you've already determined whether the data contains
|
||||
/// non-opaque pixels to avoid redundant scanning.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the `data` vector is not of length `width * height`.
|
||||
pub fn from_parts_with_opacity(
|
||||
data: Vec<PremulRgba8>,
|
||||
width: u16,
|
||||
height: u16,
|
||||
may_have_transparency: bool,
|
||||
) -> Self {
|
||||
assert_eq!(
|
||||
data.len(),
|
||||
usize::from(width) * usize::from(height),
|
||||
"Expected `data` to have length of exactly `width * height`"
|
||||
);
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
buf: data,
|
||||
may_have_transparency,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resizes the pixmap container to the given width and height; this does not resize the
|
||||
/// contained image.
|
||||
///
|
||||
/// If the pixmap buffer has to grow to fit the new size, those pixels are set to transparent
|
||||
/// black. If the pixmap buffer is larger than required, the buffer is truncated and its
|
||||
/// reserved capacity is unchanged.
|
||||
pub fn resize(&mut self, width: u16, height: u16) {
|
||||
let new_len = usize::from(width) * usize::from(height);
|
||||
// If we're growing, new pixels are transparent black
|
||||
if new_len > self.buf.len() {
|
||||
self.may_have_transparency = true;
|
||||
}
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
self.buf.resize(new_len, PremulRgba8::from_u32(0));
|
||||
}
|
||||
|
||||
/// Shrink the capacity of the pixmap buffer to fit the pixmap's current size.
|
||||
pub fn shrink_to_fit(&mut self) {
|
||||
self.buf.shrink_to_fit();
|
||||
}
|
||||
|
||||
/// The reserved capacity (in pixels) of this pixmap.
|
||||
///
|
||||
/// When calling [`Pixmap::resize`] with a `width * height` smaller than this value, the pixmap
|
||||
/// does not need to reallocate.
|
||||
pub fn capacity(&self) -> usize {
|
||||
self.buf.capacity()
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Returns whether the pixmap may have non-opaque pixels.
|
||||
///
|
||||
/// This value is computed at construction time. It may become stale if pixels are
|
||||
/// modified directly via [`data_mut()`](Self::data_mut),
|
||||
/// [`data_as_u8_slice_mut()`](Self::data_as_u8_slice_mut), or [`set_pixel()`](Self::set_pixel).
|
||||
///
|
||||
/// Use [`set_may_have_transparency()`](Self::set_may_have_transparency) to manually update the flag,
|
||||
/// or [`recompute_may_have_transparency()`](Self::recompute_may_have_transparency) to recalculate it
|
||||
/// by scanning all pixels.
|
||||
pub fn may_have_transparency(&self) -> bool {
|
||||
self.may_have_transparency
|
||||
}
|
||||
|
||||
/// Manually set the `may_have_transparency` flag.
|
||||
///
|
||||
/// Use this after modifying pixels via [`data_mut()`](Self::data_mut) or
|
||||
/// [`set_pixel()`](Self::set_pixel) when you know whether the image has
|
||||
/// non-opaque pixels.
|
||||
pub fn set_may_have_transparency(&mut self, may_have_transparency: bool) {
|
||||
self.may_have_transparency = may_have_transparency;
|
||||
}
|
||||
|
||||
/// Recalculate `may_have_transparency` by scanning all pixels.
|
||||
///
|
||||
/// Use this after modifying pixels via [`data_mut()`](Self::data_mut) or
|
||||
/// [`set_pixel()`](Self::set_pixel) when you need accurate opacity information.
|
||||
pub fn recompute_may_have_transparency(&mut self) {
|
||||
self.may_have_transparency = self.buf.iter().any(|pixel| pixel.a != 255);
|
||||
}
|
||||
|
||||
/// Apply an alpha value to the whole pixmap.
|
||||
pub fn multiply_alpha(&mut self, alpha: u8) {
|
||||
#[expect(
|
||||
clippy::cast_possible_truncation,
|
||||
reason = "cannot overflow in this case"
|
||||
)]
|
||||
let multiply = |component| ((u16::from(alpha) * u16::from(component)) / 255) as u8;
|
||||
|
||||
for pixel in self.data_mut() {
|
||||
*pixel = PremulRgba8 {
|
||||
r: multiply(pixel.r),
|
||||
g: multiply(pixel.g),
|
||||
b: multiply(pixel.b),
|
||||
a: multiply(pixel.a),
|
||||
};
|
||||
}
|
||||
|
||||
// If we applied a non-opaque alpha, the image now has transparency
|
||||
if alpha != 255 {
|
||||
self.may_have_transparency = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a pixmap from a PNG file.
|
||||
#[cfg(feature = "png")]
|
||||
pub fn from_png(data: impl BufRead + Seek) -> Result<Self, png::DecodingError> {
|
||||
let mut decoder = png::Decoder::new(data);
|
||||
decoder.set_transformations(
|
||||
png::Transformations::normalize_to_color8() | png::Transformations::ALPHA,
|
||||
);
|
||||
|
||||
let mut reader = decoder.read_info()?;
|
||||
let mut pixmap = {
|
||||
let info = reader.info();
|
||||
let width: u16 = info
|
||||
.width
|
||||
.try_into()
|
||||
.map_err(|_| png::DecodingError::LimitsExceeded)?;
|
||||
let height: u16 = info
|
||||
.height
|
||||
.try_into()
|
||||
.map_err(|_| png::DecodingError::LimitsExceeded)?;
|
||||
Self::new(width, height)
|
||||
};
|
||||
|
||||
// Note `reader.info()` returns the pre-transformation color type output, whereas
|
||||
// `reader.output_color_type()` takes the transformation into account.
|
||||
let (color_type, bit_depth) = reader.output_color_type();
|
||||
debug_assert_eq!(
|
||||
bit_depth,
|
||||
png::BitDepth::Eight,
|
||||
"normalize_to_color8 means the bit depth is always 8."
|
||||
);
|
||||
|
||||
match color_type {
|
||||
png::ColorType::Rgb | png::ColorType::Grayscale => {
|
||||
unreachable!("We set a transformation to always convert to alpha")
|
||||
}
|
||||
png::ColorType::Indexed => {
|
||||
unreachable!("Transformation should have expanded indexed images")
|
||||
}
|
||||
png::ColorType::Rgba => {
|
||||
debug_assert_eq!(
|
||||
Some(pixmap.data_as_u8_slice().len()),
|
||||
reader.output_buffer_size(),
|
||||
"The pixmap buffer should have the same number of bytes as the image."
|
||||
);
|
||||
reader.next_frame(pixmap.data_as_u8_slice_mut())?;
|
||||
}
|
||||
png::ColorType::GrayscaleAlpha => {
|
||||
debug_assert_eq!(
|
||||
Some(pixmap.data().len() * 2),
|
||||
reader.output_buffer_size(),
|
||||
"The pixmap buffer should have twice the number of bytes of the grayscale image."
|
||||
);
|
||||
let mut grayscale_data = vec![0; reader.output_buffer_size().unwrap_or_default()];
|
||||
reader.next_frame(&mut grayscale_data)?;
|
||||
|
||||
for (grayscale_pixel, pixmap_pixel) in
|
||||
grayscale_data.chunks_exact(2).zip(pixmap.data_mut())
|
||||
{
|
||||
let [gray, alpha] = grayscale_pixel.try_into().unwrap();
|
||||
*pixmap_pixel = PremulRgba8 {
|
||||
r: gray,
|
||||
g: gray,
|
||||
b: gray,
|
||||
a: alpha,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut may_have_transparency = false;
|
||||
for pixel in pixmap.data_mut() {
|
||||
let alpha = pixel.a;
|
||||
if alpha != 255 {
|
||||
may_have_transparency = true;
|
||||
}
|
||||
let alpha_u16 = u16::from(alpha);
|
||||
#[expect(
|
||||
clippy::cast_possible_truncation,
|
||||
reason = "Overflow should be impossible."
|
||||
)]
|
||||
let premultiply = |e: u8| ((u16::from(e) * alpha_u16) / 255) as u8;
|
||||
pixel.r = premultiply(pixel.r);
|
||||
pixel.g = premultiply(pixel.g);
|
||||
pixel.b = premultiply(pixel.b);
|
||||
}
|
||||
pixmap.may_have_transparency = may_have_transparency;
|
||||
|
||||
Ok(pixmap)
|
||||
}
|
||||
|
||||
/// Return the current content of the pixmap as a PNG.
|
||||
#[cfg(feature = "png")]
|
||||
pub fn into_png(self) -> Result<Vec<u8>, png::EncodingError> {
|
||||
let mut data = Vec::new();
|
||||
let mut encoder = png::Encoder::new(&mut data, self.width as u32, self.height as u32);
|
||||
encoder.set_color(png::ColorType::Rgba);
|
||||
encoder.set_depth(png::BitDepth::Eight);
|
||||
let mut writer = encoder.write_header()?;
|
||||
writer.write_image_data(bytemuck::cast_slice(&self.take_unpremultiplied()))?;
|
||||
writer.finish().map(|_| data)
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying data as premultiplied RGBA8.
|
||||
///
|
||||
/// The pixels are in row-major order.
|
||||
pub fn data(&self) -> &[PremulRgba8] {
|
||||
&self.buf
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the underlying data as premultiplied RGBA8.
|
||||
///
|
||||
/// The pixels are in row-major order.
|
||||
pub fn data_mut(&mut self) -> &mut [PremulRgba8] {
|
||||
&mut self.buf
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying data as premultiplied RGBA8.
|
||||
///
|
||||
/// The pixels are in row-major order. Each pixel consists of four bytes in the order
|
||||
/// `[r, g, b, a]`.
|
||||
pub fn data_as_u8_slice(&self) -> &[u8] {
|
||||
bytemuck::cast_slice(&self.buf)
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the underlying data as premultiplied RGBA8.
|
||||
///
|
||||
/// The pixels are in row-major order. Each pixel consists of four bytes in the order
|
||||
/// `[r, g, b, a]`.
|
||||
pub fn data_as_u8_slice_mut(&mut self) -> &mut [u8] {
|
||||
bytemuck::cast_slice_mut(&mut self.buf)
|
||||
}
|
||||
|
||||
/// Sample a pixel from the pixmap.
|
||||
///
|
||||
/// The pixel data is [premultiplied RGBA8][PremulRgba8].
|
||||
#[inline(always)]
|
||||
pub fn sample(&self, x: u16, y: u16) -> PremulRgba8 {
|
||||
let idx = self.width as usize * y as usize + x as usize;
|
||||
self.buf[idx]
|
||||
}
|
||||
|
||||
/// Sample a pixel from a custom-calculated index. This index should be calculated assuming that
|
||||
/// the data is stored in row-major order.
|
||||
#[inline(always)]
|
||||
pub fn sample_idx(&self, idx: u32) -> PremulRgba8 {
|
||||
self.buf[idx as usize]
|
||||
}
|
||||
|
||||
/// Set a pixel in the pixmap at the given coordinates.
|
||||
///
|
||||
/// The pixel data should be [premultiplied RGBA8][PremulRgba8]. The coordinate system has
|
||||
/// its origin at the top-left corner, with `x` increasing to the right and `y` increasing
|
||||
/// downward.
|
||||
#[inline(always)]
|
||||
pub fn set_pixel(&mut self, x: u16, y: u16, pixel: PremulRgba8) {
|
||||
let idx = self.width as usize * y as usize + x as usize;
|
||||
self.buf[idx] = pixel;
|
||||
}
|
||||
|
||||
/// Consume the pixmap, returning the data as the underlying [`Vec`] of premultiplied RGBA8.
|
||||
///
|
||||
/// The pixels are in row-major order.
|
||||
pub fn take(self) -> Vec<PremulRgba8> {
|
||||
self.buf
|
||||
}
|
||||
|
||||
/// Consume the pixmap, returning the data as (unpremultiplied) RGBA8.
|
||||
///
|
||||
/// Not fast, but useful for saving to PNG etc.
|
||||
///
|
||||
/// The pixels are in row-major order.
|
||||
pub fn take_unpremultiplied(self) -> Vec<Rgba8> {
|
||||
self.buf
|
||||
.into_iter()
|
||||
.map(|PremulRgba8 { r, g, b, a }| {
|
||||
let alpha = 255.0 / f32::from(a);
|
||||
if a != 0 {
|
||||
#[expect(clippy::cast_possible_truncation, reason = "deliberate quantization")]
|
||||
let unpremultiply = |component| (f32::from(component) * alpha + 0.5) as u8;
|
||||
Rgba8 {
|
||||
r: unpremultiply(r),
|
||||
g: unpremultiply(g),
|
||||
b: unpremultiply(b),
|
||||
a,
|
||||
}
|
||||
} else {
|
||||
Rgba8 { r, g, b, a }
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
Vendored
+419
@@ -0,0 +1,419 @@
|
||||
// Copyright 2026 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Helpers for performing probing to verify the basic capabilities of the device we are
|
||||
//! running on.
|
||||
|
||||
use crate::color::{AlphaColor, palette::css};
|
||||
use crate::filter_effects::{EdgeMode, Filter, FilterPrimitive};
|
||||
use crate::kurbo::{Affine, BezPath, Circle, Point, Rect, Shape};
|
||||
use crate::paint::{Image, ImageSource, PaintType};
|
||||
use crate::peniko::{
|
||||
BlendMode, ColorStop, ColorStops, Compose, Extend, Gradient, ImageQuality, ImageSampler,
|
||||
LinearGradientPosition, Mix,
|
||||
};
|
||||
use crate::pixmap::Pixmap;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
const REFERENCE_RGBA: &[u8] = include_bytes!("../assets/probe.rgba");
|
||||
|
||||
const ELEMENTS_PER_ROW: usize = 3;
|
||||
const ELEMENT_MARGIN: f64 = 1.0;
|
||||
|
||||
const RECT_SIZE: f64 = 10.0;
|
||||
const CIRCLE_RADIUS: f64 = 5.0;
|
||||
const CIRCLE_CENTER_OFFSET_X: f64 = 1.5;
|
||||
const IMAGE_SOURCE_SIZE: f64 = 5.0;
|
||||
const PATH_TOLERANCE: f64 = 0.1;
|
||||
|
||||
const ELEMENTS: [ProbeElement; 9] = [
|
||||
ProbeElement::SolidRect,
|
||||
ProbeElement::AlphaBlending,
|
||||
ProbeElement::Gradient,
|
||||
ProbeElement::ImageNearest,
|
||||
ProbeElement::Filter,
|
||||
ProbeElement::ImageBilinear,
|
||||
ProbeElement::OpacityLayer,
|
||||
ProbeElement::Blending,
|
||||
ProbeElement::Transformed,
|
||||
];
|
||||
/// Per-channel absolute tolerance used when comparing probe pixels.
|
||||
const CHANNEL_TOLERANCE: u8 = 3;
|
||||
|
||||
/// Result of running the renderer probe.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Probe<E> {
|
||||
/// The probe matched the bundled reference image.
|
||||
Success,
|
||||
/// The probe did not match the bundled reference image.
|
||||
Error(ProbeResult),
|
||||
/// Rendering the probe scene produces an error.
|
||||
RenderError(E),
|
||||
}
|
||||
|
||||
/// Probe failure output.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProbeResult {
|
||||
/// The expected probe image.
|
||||
pub expected: ProbeImage,
|
||||
/// The actual probe image.
|
||||
pub actual: ProbeImage,
|
||||
}
|
||||
|
||||
/// A probe image stored as RGBA8 bytes.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProbeImage {
|
||||
/// Width of the image in pixels.
|
||||
pub width: u16,
|
||||
/// Height of the image in pixels.
|
||||
pub height: u16,
|
||||
/// The image data as RGBA8 bytes.
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl<E> Probe<E> {
|
||||
/// Returns `true` when the probe matched the bundled reference image.
|
||||
pub fn is_success(&self) -> bool {
|
||||
matches!(self, Self::Success)
|
||||
}
|
||||
|
||||
/// Construct a new probe result by inspecting the provided pixmap and comparing it
|
||||
/// against the reference output.
|
||||
pub fn from_actual(actual: Pixmap) -> Self {
|
||||
let (width, height) = canvas_size();
|
||||
let expected = ProbeImage {
|
||||
width,
|
||||
height,
|
||||
data: REFERENCE_RGBA.to_vec(),
|
||||
};
|
||||
let actual = ProbeImage::from_pixmap(actual);
|
||||
let matches_reference = expected.width == actual.width
|
||||
&& expected.height == actual.height
|
||||
&& expected.data.len() == actual.data.len()
|
||||
&& expected
|
||||
.data
|
||||
.chunks_exact(4)
|
||||
.zip(actual.data.chunks_exact(4))
|
||||
.all(|(expected, actual)| {
|
||||
pixels_within_tolerance(expected, actual, CHANNEL_TOLERANCE)
|
||||
});
|
||||
|
||||
if matches_reference {
|
||||
Self::Success
|
||||
} else {
|
||||
Self::Error(ProbeResult { expected, actual })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProbeImage {
|
||||
fn from_pixmap(pixmap: Pixmap) -> Self {
|
||||
Self {
|
||||
width: pixmap.width(),
|
||||
height: pixmap.height(),
|
||||
data: bytemuck::cast_slice(&pixmap.take_unpremultiplied()).to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// API necessary to draw the probe scene.
|
||||
pub trait ProbeRenderer {
|
||||
fn set_transform(&mut self, transform: Affine);
|
||||
fn set_paint(&mut self, paint: PaintType);
|
||||
fn fill_path(&mut self, path: &BezPath);
|
||||
fn fill_rect(&mut self, rect: &Rect);
|
||||
fn push_layer(&mut self, blend_mode: Option<BlendMode>, opacity: Option<f32>);
|
||||
fn push_filter_layer(&mut self, filter: Filter);
|
||||
fn pop_layer(&mut self);
|
||||
fn set_paint_transform(&mut self, paint_transform: Affine);
|
||||
fn reset_paint_transform(&mut self);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum ProbeElement {
|
||||
SolidRect,
|
||||
Transformed,
|
||||
AlphaBlending,
|
||||
Gradient,
|
||||
ImageNearest,
|
||||
Filter,
|
||||
ImageBilinear,
|
||||
OpacityLayer,
|
||||
Blending,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct GridLayout {
|
||||
columns: usize,
|
||||
rows: usize,
|
||||
cell_width: f64,
|
||||
cell_height: f64,
|
||||
}
|
||||
|
||||
impl GridLayout {
|
||||
fn from_elements(elements: &[ProbeElement]) -> Self {
|
||||
let columns = ELEMENTS_PER_ROW.min(elements.len());
|
||||
let rows = elements.len().div_ceil(columns);
|
||||
let (cell_width, cell_height) = elements
|
||||
.iter()
|
||||
.copied()
|
||||
.map(ProbeElement::bounds)
|
||||
.fold((0.0_f64, 0.0_f64), |(max_w, max_h), (w, h)| {
|
||||
(max_w.max(w), max_h.max(h))
|
||||
});
|
||||
|
||||
Self {
|
||||
columns,
|
||||
rows,
|
||||
cell_width,
|
||||
cell_height,
|
||||
}
|
||||
}
|
||||
|
||||
fn canvas_size(self) -> (u16, u16) {
|
||||
let width = self.columns as f64 * self.cell_width
|
||||
+ self.columns.saturating_sub(1) as f64 * ELEMENT_MARGIN;
|
||||
let height = self.rows as f64 * self.cell_height
|
||||
+ self.rows.saturating_sub(1) as f64 * ELEMENT_MARGIN;
|
||||
(width.ceil() as u16, height.ceil() as u16)
|
||||
}
|
||||
|
||||
fn canvas_rect(self) -> Rect {
|
||||
let (width, height) = self.canvas_size();
|
||||
Rect::new(0.0, 0.0, f64::from(width), f64::from(height))
|
||||
}
|
||||
|
||||
fn cell_rect(self, index: usize) -> Rect {
|
||||
let column = index % self.columns;
|
||||
let row = index / self.columns;
|
||||
let x0 = column as f64 * (self.cell_width + ELEMENT_MARGIN);
|
||||
let y0 = row as f64 * (self.cell_height + ELEMENT_MARGIN);
|
||||
Rect::new(x0, y0, x0 + self.cell_width, y0 + self.cell_height)
|
||||
}
|
||||
}
|
||||
|
||||
impl ProbeElement {
|
||||
fn bounds(self) -> (f64, f64) {
|
||||
let (width, height) = match self {
|
||||
Self::SolidRect
|
||||
| Self::Gradient
|
||||
| Self::ImageNearest
|
||||
| Self::ImageBilinear
|
||||
| Self::Filter
|
||||
| Self::OpacityLayer => (RECT_SIZE, RECT_SIZE),
|
||||
Self::Transformed => (
|
||||
RECT_SIZE * core::f64::consts::SQRT_2,
|
||||
RECT_SIZE * core::f64::consts::SQRT_2,
|
||||
),
|
||||
Self::AlphaBlending | Self::Blending => (
|
||||
CIRCLE_RADIUS * 2.0 + CIRCLE_CENTER_OFFSET_X * 2.0,
|
||||
CIRCLE_RADIUS * 2.0,
|
||||
),
|
||||
};
|
||||
(width + ELEMENT_MARGIN * 2.0, height + ELEMENT_MARGIN * 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the canvas size of the shared probe scene.
|
||||
pub fn canvas_size() -> (u16, u16) {
|
||||
GridLayout::from_elements(&ELEMENTS).canvas_size()
|
||||
}
|
||||
|
||||
/// Return the pixmap that is referenced when drawing images in the scene.
|
||||
pub fn probe_image_pixmap() -> Pixmap {
|
||||
let mut pixmap = Pixmap::new(IMAGE_SOURCE_SIZE as u16, IMAGE_SOURCE_SIZE as u16);
|
||||
for y in 0..pixmap.height() {
|
||||
for x in 0..pixmap.width() {
|
||||
pixmap.set_pixel(
|
||||
x,
|
||||
y,
|
||||
AlphaColor::from_rgba8(255, 0, 0, 255)
|
||||
.premultiply()
|
||||
.to_rgba8(),
|
||||
);
|
||||
}
|
||||
}
|
||||
pixmap.set_may_have_transparency(false);
|
||||
pixmap
|
||||
}
|
||||
|
||||
fn image_paint(image: ImageSource, quality: ImageQuality) -> PaintType {
|
||||
Image {
|
||||
image,
|
||||
sampler: ImageSampler {
|
||||
x_extend: Extend::Pad,
|
||||
y_extend: Extend::Pad,
|
||||
quality,
|
||||
alpha: 1.0,
|
||||
},
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Draw the full shared probe scene into a rendering context.
|
||||
pub fn draw_scene<T: ProbeRenderer>(ctx: &mut T, image: ImageSource) {
|
||||
let layout = GridLayout::from_elements(&ELEMENTS);
|
||||
let image_nearest = image_paint(image.clone(), ImageQuality::Low);
|
||||
let image_bilinear = image_paint(image, ImageQuality::Medium);
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
ctx.set_paint(css::WHITE.into());
|
||||
ctx.fill_rect(&layout.canvas_rect());
|
||||
|
||||
for (index, element) in ELEMENTS.iter().copied().enumerate() {
|
||||
draw_probe_element(
|
||||
ctx,
|
||||
layout.cell_rect(index),
|
||||
element,
|
||||
&image_nearest,
|
||||
&image_bilinear,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn pixels_within_tolerance(expected: &[u8], actual: &[u8], channel_tolerance: u8) -> bool {
|
||||
if expected[3] == 0 && actual[3] == 0 {
|
||||
return true;
|
||||
}
|
||||
|
||||
expected
|
||||
.iter()
|
||||
.zip(actual)
|
||||
.all(|(expected, actual)| expected.abs_diff(*actual) <= channel_tolerance)
|
||||
}
|
||||
|
||||
fn draw_probe_element(
|
||||
ctx: &mut impl ProbeRenderer,
|
||||
cell: Rect,
|
||||
element: ProbeElement,
|
||||
image_nearest: &PaintType,
|
||||
image_bilinear: &PaintType,
|
||||
) {
|
||||
match element {
|
||||
ProbeElement::SolidRect => {
|
||||
ctx.set_paint(css::BLUE.into());
|
||||
ctx.fill_rect(¢ered_rect(cell, RECT_SIZE, RECT_SIZE));
|
||||
}
|
||||
ProbeElement::Transformed => {
|
||||
draw_transformed_rect(ctx, centered_rect(cell, RECT_SIZE, RECT_SIZE));
|
||||
}
|
||||
ProbeElement::AlphaBlending => {
|
||||
let center = cell.center();
|
||||
ctx.set_paint(css::YELLOW.with_alpha(0.5).into());
|
||||
ctx.fill_path(
|
||||
&Circle::new((center.x - CIRCLE_CENTER_OFFSET_X, center.y), CIRCLE_RADIUS)
|
||||
.to_path(PATH_TOLERANCE),
|
||||
);
|
||||
ctx.set_paint(css::GREEN.with_alpha(0.5).into());
|
||||
ctx.fill_path(
|
||||
&Circle::new((center.x + CIRCLE_CENTER_OFFSET_X, center.y), CIRCLE_RADIUS)
|
||||
.to_path(PATH_TOLERANCE),
|
||||
);
|
||||
}
|
||||
ProbeElement::Gradient => {
|
||||
let rect = centered_rect(cell, RECT_SIZE, RECT_SIZE);
|
||||
ctx.set_paint(linear_gradient(&rect).into());
|
||||
ctx.fill_rect(&rect);
|
||||
}
|
||||
ProbeElement::ImageNearest => draw_centered_padded_image(ctx, cell, image_nearest),
|
||||
ProbeElement::Filter => draw_blurred_rect(ctx, centered_rect(cell, RECT_SIZE, RECT_SIZE)),
|
||||
ProbeElement::ImageBilinear => draw_centered_padded_image(ctx, cell, image_bilinear),
|
||||
ProbeElement::OpacityLayer => {
|
||||
draw_opacity_layer_rect(ctx, centered_rect(cell, RECT_SIZE, RECT_SIZE));
|
||||
}
|
||||
ProbeElement::Blending => draw_layered_difference_circles(ctx, cell),
|
||||
}
|
||||
}
|
||||
|
||||
fn centered_rect(cell: Rect, width: f64, height: f64) -> Rect {
|
||||
let center = cell.center();
|
||||
Rect::new(
|
||||
center.x - width * 0.5,
|
||||
center.y - height * 0.5,
|
||||
center.x + width * 0.5,
|
||||
center.y + height * 0.5,
|
||||
)
|
||||
}
|
||||
|
||||
fn draw_centered_padded_image(ctx: &mut impl ProbeRenderer, cell: Rect, image_paint: &PaintType) {
|
||||
let dst_rect = centered_rect(cell, RECT_SIZE, RECT_SIZE);
|
||||
let image_origin = (
|
||||
dst_rect.x0 + (RECT_SIZE - IMAGE_SOURCE_SIZE) * 0.5,
|
||||
dst_rect.y0 + (RECT_SIZE - IMAGE_SOURCE_SIZE) * 0.5,
|
||||
);
|
||||
ctx.set_paint(image_paint.clone());
|
||||
ctx.set_paint_transform(Affine::translate(image_origin));
|
||||
ctx.fill_rect(&dst_rect);
|
||||
ctx.reset_paint_transform();
|
||||
}
|
||||
|
||||
fn draw_transformed_rect(ctx: &mut impl ProbeRenderer, rect: Rect) {
|
||||
let center = rect.center();
|
||||
ctx.set_transform(
|
||||
Affine::translate((center.x, center.y))
|
||||
* Affine::rotate(core::f64::consts::FRAC_PI_4)
|
||||
* Affine::translate((-center.x, -center.y)),
|
||||
);
|
||||
ctx.set_paint(css::BLUE.into());
|
||||
ctx.fill_rect(&rect);
|
||||
ctx.set_transform(Affine::IDENTITY);
|
||||
}
|
||||
|
||||
fn draw_blurred_rect(ctx: &mut impl ProbeRenderer, rect: Rect) {
|
||||
let blur = Filter::from_primitive(FilterPrimitive::GaussianBlur {
|
||||
std_deviation: 0.5,
|
||||
edge_mode: EdgeMode::None,
|
||||
});
|
||||
ctx.push_filter_layer(blur);
|
||||
ctx.set_paint(css::REBECCA_PURPLE.into());
|
||||
ctx.fill_rect(&rect);
|
||||
ctx.pop_layer();
|
||||
}
|
||||
|
||||
fn draw_opacity_layer_rect(ctx: &mut impl ProbeRenderer, rect: Rect) {
|
||||
ctx.push_layer(None, Some(0.5));
|
||||
ctx.set_paint(css::ORANGE_RED.into());
|
||||
ctx.fill_rect(&rect);
|
||||
ctx.pop_layer();
|
||||
}
|
||||
|
||||
fn draw_layered_difference_circles(ctx: &mut impl ProbeRenderer, cell: Rect) {
|
||||
let center = cell.center();
|
||||
|
||||
ctx.push_layer(None, None);
|
||||
ctx.set_paint(css::YELLOW.with_alpha(0.5).into());
|
||||
ctx.fill_path(
|
||||
&Circle::new((center.x - CIRCLE_CENTER_OFFSET_X, center.y), CIRCLE_RADIUS)
|
||||
.to_path(PATH_TOLERANCE),
|
||||
);
|
||||
|
||||
ctx.push_layer(
|
||||
Some(BlendMode::new(Mix::Difference, Compose::SrcOver)),
|
||||
None,
|
||||
);
|
||||
ctx.set_paint(css::GREEN.with_alpha(0.5).into());
|
||||
ctx.fill_path(
|
||||
&Circle::new((center.x + CIRCLE_CENTER_OFFSET_X, center.y), CIRCLE_RADIUS)
|
||||
.to_path(PATH_TOLERANCE),
|
||||
);
|
||||
ctx.pop_layer();
|
||||
ctx.pop_layer();
|
||||
}
|
||||
|
||||
fn linear_gradient(rect: &Rect) -> Gradient {
|
||||
Gradient {
|
||||
kind: LinearGradientPosition {
|
||||
start: Point::new(rect.x0, rect.y0),
|
||||
end: Point::new(rect.x1, rect.y0),
|
||||
}
|
||||
.into(),
|
||||
stops: ColorStops::from(
|
||||
[
|
||||
ColorStop::from((0.0, css::BLUE)),
|
||||
ColorStop::from((1.0, css::RED)),
|
||||
]
|
||||
.as_slice(),
|
||||
),
|
||||
extend: Extend::Pad,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
Vendored
+180
@@ -0,0 +1,180 @@
|
||||
// Copyright 2025 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Fast pixel-aligned rectangle rendering directly into strips.
|
||||
|
||||
use crate::kurbo::Rect;
|
||||
#[cfg(not(feature = "std"))]
|
||||
use crate::kurbo::common::FloatFuncs as _;
|
||||
use crate::strip::Strip;
|
||||
use crate::tile::Tile;
|
||||
use alloc::vec::Vec;
|
||||
use fearless_simd::*;
|
||||
|
||||
/// Render a pixel-aligned rectangle directly into strips.
|
||||
///
|
||||
/// This bypasses the full path processing pipeline (flatten → tiles → strips)
|
||||
/// by directly creating strip coverage data for the rectangle.
|
||||
///
|
||||
/// The rect bounds should already be clamped to the viewport.
|
||||
pub fn render(level: Level, rect: Rect, strip_buf: &mut Vec<Strip>, alpha_buf: &mut Vec<u8>) {
|
||||
dispatch!(level, simd => render_impl(simd, rect, strip_buf, alpha_buf));
|
||||
}
|
||||
|
||||
/// Generates strip data for an axis-aligned rectangle.
|
||||
///
|
||||
/// # Strip layout strategy
|
||||
///
|
||||
/// Tile rows are classified into two kinds:
|
||||
///
|
||||
/// - **Edge rows** (top/bottom of rect): the rect boundary crosses partway
|
||||
/// through the tile vertically, so individual pixels need per-cell alpha.
|
||||
/// We emit a *single wide strip* spanning all tile columns, with alpha =
|
||||
/// `x_alpha` * `y_alpha` (so the intersection of the alpha mask in each direction).
|
||||
///
|
||||
/// - **Interior rows**: every pixel in the tile has full vertical coverage,
|
||||
/// so we only need to handle the left and right partial-column edges.
|
||||
/// We emit a **left edge strip** (with its x-alpha mask) and, when the rect
|
||||
/// spans more than one tile column, a **right edge strip** with `fill_gap =
|
||||
/// true` so the renderer fills solid 0xFF between them.
|
||||
///
|
||||
/// The x-alpha masks for the left/right edge tiles are y-independent, so they
|
||||
/// are precomputed once and reused across all interior rows.
|
||||
fn render_impl<S: Simd>(s: S, rect: Rect, strip_buf: &mut Vec<Strip>, alpha_buf: &mut Vec<u8>) {
|
||||
if rect.is_zero_area() {
|
||||
return;
|
||||
}
|
||||
|
||||
let rect_x0 = rect.x0 as f32;
|
||||
let rect_y0 = rect.y0 as f32;
|
||||
let rect_x1 = rect.x1 as f32;
|
||||
let rect_y1 = rect.y1 as f32;
|
||||
|
||||
// Integer pixel bounds.
|
||||
let px_x0 = rect_x0.floor() as u16;
|
||||
let px_y0 = rect_y0.floor() as u16;
|
||||
let px_y1 = rect_y1.ceil() as u16;
|
||||
|
||||
let left_tile_x = (px_x0 / Tile::WIDTH) * Tile::WIDTH;
|
||||
// Inclusive, so don't use `ceil` here but just `rect_x1` directly.
|
||||
let right_tile_x = (rect_x1 as u16 / Tile::WIDTH) * Tile::WIDTH;
|
||||
|
||||
let y0 = (px_y0 / Tile::HEIGHT) * Tile::HEIGHT;
|
||||
// Note: y1 is exclusive, but it's gonna break for the very last tile if we have a height of u16::MAX.
|
||||
let y1 = (px_y1.saturating_add(Tile::HEIGHT - 1) / Tile::HEIGHT) * Tile::HEIGHT;
|
||||
// Include one tile past the right edge so the right-edge tile column is
|
||||
// covered by the edge-row wide-strip loop.
|
||||
let x_end = right_tile_x.saturating_add(Tile::WIDTH);
|
||||
|
||||
if x_end <= left_tile_x || y1 <= y0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let tile_start_y = y0 / Tile::HEIGHT;
|
||||
let tile_end_y = y1 / Tile::HEIGHT;
|
||||
|
||||
// A right strip is only needed when the rect spans more than one tile column.
|
||||
let needs_right_strip = right_tile_x > left_tile_x;
|
||||
|
||||
let left_x_cov = coverage(left_tile_x, rect_x0, rect_x1);
|
||||
let right_x_cov = coverage(right_tile_x, rect_x0, rect_x1);
|
||||
let left_x_mask = alpha_mask_from_x_coverage(s, &left_x_cov);
|
||||
let right_x_mask = alpha_mask_from_x_coverage(s, &right_x_cov);
|
||||
|
||||
for tile_y in tile_start_y..tile_end_y {
|
||||
let strip_y = tile_y * Tile::HEIGHT;
|
||||
let strip_y_f = strip_y as f32;
|
||||
let strip_y_end_f = strip_y as f32 + Tile::HEIGHT as f32;
|
||||
|
||||
// A row is an "edge" if the rect's top or bottom boundary falls
|
||||
// *inside* it (i.e. partial vertical coverage).
|
||||
let is_top_edge = strip_y_f < rect_y0 && rect_y0 < strip_y_end_f;
|
||||
let is_bottom_edge = strip_y_f < rect_y1 && rect_y1 < strip_y_end_f;
|
||||
|
||||
if is_top_edge || is_bottom_edge {
|
||||
let alpha_start = alpha_buf.len() as u32;
|
||||
|
||||
let y_cov = coverage(strip_y, rect_y0, rect_y1);
|
||||
let mut col = left_tile_x;
|
||||
// TODO: Can this result in an infinite loop in case x_end == u16::MAX?
|
||||
while col + Tile::WIDTH <= x_end {
|
||||
// TODO: We could optimize this so this is only computed for the left-most and right-most
|
||||
// tile of the edge, all intermediate tiles have full horizontal coverage.
|
||||
let x_cov = coverage(col, rect_x0, rect_x1);
|
||||
let combined = combined_tile_alpha(s, &x_cov, &y_cov);
|
||||
alpha_buf.extend_from_slice(combined.as_slice());
|
||||
col += Tile::WIDTH;
|
||||
}
|
||||
|
||||
strip_buf.push(Strip::new(left_tile_x, strip_y, alpha_start, false));
|
||||
} else {
|
||||
let alpha_start = alpha_buf.len() as u32;
|
||||
alpha_buf.extend_from_slice(left_x_mask.as_slice());
|
||||
strip_buf.push(Strip::new(left_tile_x, strip_y, alpha_start, false));
|
||||
|
||||
if needs_right_strip {
|
||||
// `fill_gap = true` tells the renderer to fill solid 0xFF
|
||||
// between the previous strip's end and this strip's start.
|
||||
let alpha_start = alpha_buf.len() as u32;
|
||||
alpha_buf.extend_from_slice(right_x_mask.as_slice());
|
||||
strip_buf.push(Strip::new(right_tile_x, strip_y, alpha_start, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sentinel strip: marks the end of the strip list for this shape.
|
||||
let last_strip_y = (tile_end_y - 1) * Tile::HEIGHT;
|
||||
strip_buf.push(Strip::new(
|
||||
u16::MAX,
|
||||
last_strip_y,
|
||||
alpha_buf.len() as u32,
|
||||
false,
|
||||
));
|
||||
}
|
||||
|
||||
/// Compute fractional pixel coverage for `N` consecutive pixels starting at `start`.
|
||||
#[inline(always)]
|
||||
fn coverage<const N: usize>(start: u16, rect_lo: f32, rect_hi: f32) -> [f32; N] {
|
||||
let mut cov = [0.0_f32; N];
|
||||
|
||||
#[allow(clippy::needless_range_loop, reason = "better clarity")]
|
||||
for i in 0..N {
|
||||
let px = (start as usize + i) as f32;
|
||||
cov[i] = (rect_hi.min(px + 1.0) - rect_lo.max(px)).clamp(0.0, 1.0);
|
||||
}
|
||||
cov
|
||||
}
|
||||
|
||||
/// Build an alpha mask for the 4x4 tile from the given horizontal coverages,
|
||||
/// splatting them across the other dimension.
|
||||
#[inline(always)]
|
||||
fn alpha_mask_from_x_coverage<S: Simd>(s: S, cov: &[f32; Tile::WIDTH as usize]) -> u8x16<S> {
|
||||
let mut buf = [0_u8; 16];
|
||||
|
||||
#[allow(clippy::needless_range_loop, reason = "better clarity")]
|
||||
for col in 0..Tile::WIDTH as usize {
|
||||
let alpha = (cov[col] * 255.0 + 0.5) as u8;
|
||||
let base = col * Tile::HEIGHT as usize;
|
||||
buf[base..base + Tile::HEIGHT as usize].fill(alpha);
|
||||
}
|
||||
|
||||
u8x16::from_slice(s, &buf)
|
||||
}
|
||||
|
||||
/// Compute the alphas for a single 4x4 tile, taking horizontal as well as vertical coverage
|
||||
/// of the rectangle into account.
|
||||
#[inline(always)]
|
||||
fn combined_tile_alpha<S: Simd>(
|
||||
s: S,
|
||||
x_cov: &[f32; Tile::WIDTH as usize],
|
||||
y_cov: &[f32; Tile::HEIGHT as usize],
|
||||
) -> u8x16<S> {
|
||||
let mut buf = [0_u8; 16];
|
||||
for (col, xc) in x_cov.iter().copied().enumerate() {
|
||||
for (row, yc) in y_cov.iter().copied().enumerate() {
|
||||
buf[col * Tile::HEIGHT as usize + row] = (xc * yc * 255.0 + 0.5) as u8;
|
||||
}
|
||||
}
|
||||
|
||||
u8x16::from_slice(s, &buf)
|
||||
}
|
||||
+616
@@ -0,0 +1,616 @@
|
||||
// Copyright 2025 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Render graph for multi-pass rendering with filter effects.
|
||||
//!
|
||||
//! This module provides a directed acyclic graph (DAG) representation of the rendering pipeline,
|
||||
//! enabling efficient multi-pass rendering when layers have filter effects applied.
|
||||
//!
|
||||
//! # Overview
|
||||
//!
|
||||
//! The render graph tracks dependencies between rendering operations (nodes) and ensures they
|
||||
//! execute in the correct order. Each node represents either:
|
||||
//! - The root (backdrop) layer that forms the base for compositing
|
||||
//! - A filtered layer that renders geometry, applies effects, and blends the result
|
||||
//!
|
||||
//! # Key Features
|
||||
//!
|
||||
//! - **Incremental execution order**: The graph builds its execution order as layers are popped
|
||||
//! during scene encoding, avoiding the need for a full topological sort at render time.
|
||||
//! - **Data dependencies**: Edges represent dependencies where one layer's output is consumed
|
||||
//! as input to another operation (e.g., a filter reading from a previous layer's buffer).
|
||||
//! - **Efficient traversal**: The pre-computed execution order allows O(1) iteration over nodes
|
||||
//! in dependency order, with children always processed before their parents.
|
||||
//!
|
||||
//! # Usage Pattern
|
||||
//!
|
||||
//! 1. During scene encoding, nodes are added via [`RenderGraph::add_node`] as layers are created
|
||||
//! 2. When layers with dependencies are popped, edges are added via [`RenderGraph::add_edge`]
|
||||
//! 3. As each layer completes, it's recorded in execution order via [`RenderGraph::record_node_for_execution`]
|
||||
//! 4. At render time, iterate nodes in order using [`RenderGraph::execution_order`]
|
||||
//!
|
||||
//! # Example Structure
|
||||
//!
|
||||
//! ```text
|
||||
//! RootLayer (id=0) ← backdrop
|
||||
//! │
|
||||
//! └─→ FilterLayer (id=1, blur filter)
|
||||
//! │
|
||||
//! └─→ FilterLayer (id=2, drop shadow filter)
|
||||
//!
|
||||
//! Execution order: [2, 1, 0] (deepest children first, then parents)
|
||||
//! ```
|
||||
//!
|
||||
//! In this example, `FilterLayer 2` (drop shadow) is the most deeply nested child, so it
|
||||
//! executes first. Then `FilterLayer 1` (blur) executes and can reference `FilterLayer 2`'s output
|
||||
//! if needed. Finally, the `RootLayer` composites all filtered results into the final image.
|
||||
//!
|
||||
//! # Future Improvements
|
||||
//!
|
||||
//! TODO: Consider using `render_graph` for `FilterGraph` execution for filters. The render graph
|
||||
//! infrastructure could potentially be extended to handle the internal DAG structure within
|
||||
//! individual filter effects, not just layer-to-layer dependencies.
|
||||
|
||||
use crate::coarse::WideTilesBbox;
|
||||
use crate::filter_effects::Filter;
|
||||
use crate::kurbo::Affine;
|
||||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
use hashbrown::HashMap;
|
||||
|
||||
/// A render graph containing nodes and edges representing the rendering pipeline.
|
||||
///
|
||||
/// This graph tracks layers with filter effects and their dependencies, enabling
|
||||
/// multi-pass rendering where filter outputs can be used as inputs to subsequent operations.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenderGraph {
|
||||
/// All render operations (layers) in the graph.
|
||||
pub nodes: Vec<RenderNode>,
|
||||
/// Dependencies between nodes, defining execution order.
|
||||
pub edges: Vec<RenderEdge>,
|
||||
/// Counter for generating unique node IDs.
|
||||
next_node_id: NodeId,
|
||||
/// Pre-computed execution order collected during `pop_layer` calls.
|
||||
/// This is built incrementally as layers are popped (children before parents),
|
||||
/// avoiding the need for topological sort at render time.
|
||||
node_execution_order: Vec<NodeId>,
|
||||
/// The root layer node ID, tracked separately since it's never popped.
|
||||
/// This is appended to the execution order when iterating.
|
||||
root_node: Option<NodeId>,
|
||||
/// Flag indicating whether the graph contains any filter layers.
|
||||
/// Set to true when a `FilterLayer` node is added.
|
||||
has_filters: bool,
|
||||
}
|
||||
|
||||
/// The type of dependency between nodes.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DependencyKind {
|
||||
/// Sequential execution: the `from` node must execute before the `to` node.
|
||||
///
|
||||
/// The `to` node consumes output from the `from` node via the specified layer.
|
||||
/// For example, a parent filter reads from a child layer buffer produced by a previous operation.
|
||||
Sequential {
|
||||
/// The layer that connects the two nodes, whose output flows from `from` to `to`.
|
||||
layer_id: LayerId,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for RenderGraph {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderGraph {
|
||||
/// Creates a new empty render graph.
|
||||
///
|
||||
/// The graph starts with no nodes or edges. Nodes are added via [`add_node`](Self::add_node)
|
||||
/// as layers are created during scene encoding.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
nodes: Vec::new(),
|
||||
edges: Vec::new(),
|
||||
next_node_id: 0,
|
||||
node_execution_order: Vec::new(),
|
||||
root_node: None,
|
||||
has_filters: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears the render graph state while preserving allocated capacity.
|
||||
///
|
||||
/// This resets the graph to an empty state (equivalent to [`new`](Self::new))
|
||||
/// but reuses the existing memory allocations, avoiding the need to reallocate
|
||||
/// when building a new scene.
|
||||
///
|
||||
/// After calling `clear()`, the graph will have no nodes or edges, and counters
|
||||
/// will be reset to their initial state.
|
||||
pub fn clear(&mut self) {
|
||||
self.nodes.clear();
|
||||
self.edges.clear();
|
||||
self.next_node_id = 0;
|
||||
self.node_execution_order.clear();
|
||||
self.root_node = None;
|
||||
self.has_filters = false;
|
||||
}
|
||||
|
||||
/// Add a node to the graph and return its ID.
|
||||
///
|
||||
/// This is called during scene encoding when a new layer is created. The returned ID
|
||||
/// should be stored with the layer to establish edges later when dependencies are known.
|
||||
pub fn add_node(&mut self, kind: RenderNodeKind) -> NodeId {
|
||||
let id = self.next_node_id;
|
||||
self.next_node_id += 1;
|
||||
|
||||
// Track root node separately since it's never popped and executes last
|
||||
if matches!(kind, RenderNodeKind::RootLayer { .. }) {
|
||||
self.root_node = Some(id);
|
||||
}
|
||||
|
||||
// Track if we have any filters to avoid scanning nodes later
|
||||
if matches!(kind, RenderNodeKind::FilterLayer { .. }) {
|
||||
self.has_filters = true;
|
||||
}
|
||||
|
||||
self.nodes.push(RenderNode { id, kind });
|
||||
id
|
||||
}
|
||||
|
||||
/// Add an edge between two nodes, establishing a dependency relationship.
|
||||
///
|
||||
/// This is called when a layer with dependencies is popped during scene encoding.
|
||||
/// The edge ensures that `from` node executes before `to` node, typically because
|
||||
/// `to` needs to read data produced by `from` (e.g., a filter reading from a layer buffer).
|
||||
pub fn add_edge(&mut self, from: NodeId, to: NodeId, kind: DependencyKind) {
|
||||
self.edges.push(RenderEdge { from, to, kind });
|
||||
}
|
||||
|
||||
/// Record a node as ready for execution in topological order.
|
||||
///
|
||||
/// This is called from `pop_layer` to incrementally build the execution order
|
||||
/// as layers are popped. Since layers are popped in a depth-first manner (children
|
||||
/// before parents), this naturally builds the correct dependency order without
|
||||
/// requiring a separate topological sort at render time.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// For nested layers A → B → C (where C is the deepest child):
|
||||
/// - C is popped first → recorded first
|
||||
/// - B is popped second → recorded second
|
||||
/// - A is popped last → recorded last
|
||||
///
|
||||
/// This gives execution order [C, B, A], which respects dependencies.
|
||||
pub fn record_node_for_execution(&mut self, node_id: NodeId) {
|
||||
self.node_execution_order.push(node_id);
|
||||
}
|
||||
|
||||
/// Check if the graph contains any filter passes.
|
||||
///
|
||||
/// Returns `true` if any layers have filter effects that need processing.
|
||||
/// This is useful for determining whether to use the multi-pass rendering
|
||||
/// pipeline (with filter support) or a simpler single-pass approach.
|
||||
///
|
||||
/// This is set when filter nodes are added.
|
||||
pub fn has_filters(&self) -> bool {
|
||||
self.has_filters
|
||||
}
|
||||
|
||||
/// Get an iterator over nodes in execution order.
|
||||
///
|
||||
/// Returns filter layers followed by the root layer, in the order they should be executed.
|
||||
/// This order is built incrementally during `pop_layer` calls (children before parents),
|
||||
/// avoiding the need for topological sort at render time. Returns an iterator to avoid allocation.
|
||||
pub fn execution_order(&self) -> impl Iterator<Item = NodeId> + '_ {
|
||||
self.node_execution_order
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(self.root_node)
|
||||
}
|
||||
|
||||
/// Topologically sort nodes based on their dependencies using Kahn's algorithm.
|
||||
///
|
||||
/// Returns nodes in execution order, ensuring all dependencies are satisfied before
|
||||
/// each node is processed. This implements a standard topological sort with cycle detection.
|
||||
///
|
||||
/// # Performance
|
||||
///
|
||||
/// This is primarily used for validation and debugging. For normal rendering,
|
||||
/// prefer [`execution_order`](Self::execution_order) which is pre-computed during
|
||||
/// scene encoding and avoids the O(V+E) sorting overhead.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the graph contains a cycle, which should never happen in a valid render graph.
|
||||
#[allow(
|
||||
dead_code,
|
||||
reason = "we don't use currently topological sort but will in the future"
|
||||
)]
|
||||
pub fn topological_sort(&self) -> Vec<NodeId> {
|
||||
// Track how many incoming edges each node has (dependencies it's waiting for)
|
||||
let mut in_degree = vec![0; self.nodes.len()];
|
||||
|
||||
// Map each node to its dependents (nodes that depend on it)
|
||||
let mut adj_list: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
|
||||
|
||||
// Build adjacency list and count incoming edges for each node
|
||||
for edge in &self.edges {
|
||||
adj_list.entry(edge.from).or_default().push(edge.to);
|
||||
in_degree[edge.to] += 1;
|
||||
}
|
||||
|
||||
// Start with nodes that have no dependencies (in-degree = 0)
|
||||
// These are safe to execute immediately
|
||||
let mut queue: Vec<NodeId> = (0..self.nodes.len())
|
||||
.filter(|&i| in_degree[i] == 0)
|
||||
.collect();
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
// Process nodes in dependency order (Kahn's algorithm)
|
||||
while let Some(node_id) = queue.pop() {
|
||||
result.push(node_id);
|
||||
|
||||
// For each node that depends on this one, mark this dependency as satisfied
|
||||
if let Some(neighbors) = adj_list.get(&node_id) {
|
||||
for &neighbor in neighbors {
|
||||
in_degree[neighbor] -= 1;
|
||||
// If all dependencies are now satisfied, this node is ready to execute
|
||||
if in_degree[neighbor] == 0 {
|
||||
queue.push(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't process all nodes, there's a cycle (which should never happen)
|
||||
assert_eq!(
|
||||
result.len(),
|
||||
self.nodes.len(),
|
||||
"Cycle detected in render graph"
|
||||
);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// A node in the render graph representing a single render operation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenderNode {
|
||||
/// Unique identifier for this node.
|
||||
pub id: NodeId,
|
||||
/// The type of render operation this node performs.
|
||||
pub kind: RenderNodeKind,
|
||||
}
|
||||
|
||||
impl RenderNode {
|
||||
/// Returns `true` if this node has an empty (zero-area) bounding box.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match &self.kind {
|
||||
RenderNodeKind::RootLayer { wtile_bbox, .. } => wtile_bbox.is_empty(),
|
||||
RenderNodeKind::FilterLayer { wtile_bbox, .. } => wtile_bbox.is_empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An edge representing a dependency between two nodes.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenderEdge {
|
||||
/// The source node that must complete first.
|
||||
pub from: NodeId,
|
||||
/// The destination node that depends on the source.
|
||||
pub to: NodeId,
|
||||
/// The type of dependency relationship.
|
||||
pub kind: DependencyKind,
|
||||
}
|
||||
|
||||
/// The type of operation a render node performs.
|
||||
///
|
||||
/// Each variant represents a different kind of rendering pass in the pipeline.
|
||||
/// Nodes are executed in dependency order to ensure correct compositing and filtering.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RenderNodeKind {
|
||||
/// The root (backdrop) layer that serves as the base for compositing.
|
||||
///
|
||||
/// This layer is always present and is rendered last, after all filter layers have
|
||||
/// been processed. It contains all non-filtered geometry and serves as the final
|
||||
/// compositing target.
|
||||
RootLayer {
|
||||
/// ID of the root layer.
|
||||
layer_id: LayerId,
|
||||
/// Bounding box in wide tile coordinates covered by this layer.
|
||||
wtile_bbox: WideTilesBbox,
|
||||
},
|
||||
/// A layer with filter effects applied.
|
||||
///
|
||||
/// This combines multiple passes: render geometry → apply filter → blend result.
|
||||
/// Layers are added to the render graph only if they have filters. The filter
|
||||
/// output may be consumed by other filter layers or composited into the root layer.
|
||||
FilterLayer {
|
||||
/// ID of this filtered layer.
|
||||
layer_id: LayerId,
|
||||
/// The filter effect to apply.
|
||||
filter: Filter,
|
||||
/// Bounding box in wide tile coordinates containing geometry for this layer.
|
||||
wtile_bbox: WideTilesBbox,
|
||||
/// Transform that was active when the layer was created.
|
||||
/// Used to scale filter parameters based on the current scale/zoom level.
|
||||
transform: Affine,
|
||||
},
|
||||
}
|
||||
|
||||
/// Unique identifier for a layer in the rendering system.
|
||||
///
|
||||
/// Layer IDs are assigned by the encoding system and used to track layer buffers
|
||||
/// across multiple render passes. A layer may or may not have a corresponding node
|
||||
/// in the render graph (only layers with filters create graph nodes).
|
||||
pub type LayerId = u32;
|
||||
|
||||
/// Unique identifier for a node in the render graph.
|
||||
///
|
||||
/// Node IDs are assigned sequentially as nodes are added during scene encoding.
|
||||
/// They are used to reference nodes when adding edges and during execution order traversal.
|
||||
pub type NodeId = usize;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::color::AlphaColor;
|
||||
|
||||
#[test]
|
||||
fn new_creates_empty_graph() {
|
||||
let graph = RenderGraph::new();
|
||||
assert_eq!(graph.nodes.len(), 0);
|
||||
assert_eq!(graph.edges.len(), 0);
|
||||
assert!(!graph.has_filters());
|
||||
assert!(graph.root_node.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_resets_to_empty_state() {
|
||||
let mut graph = RenderGraph::new();
|
||||
graph.add_node(RenderNodeKind::RootLayer {
|
||||
layer_id: 0,
|
||||
wtile_bbox: dummy_bbox(),
|
||||
});
|
||||
graph.add_node(RenderNodeKind::FilterLayer {
|
||||
layer_id: 1,
|
||||
filter: dummy_filter(),
|
||||
wtile_bbox: dummy_bbox(),
|
||||
transform: Affine::IDENTITY,
|
||||
});
|
||||
graph.record_node_for_execution(1);
|
||||
|
||||
graph.clear();
|
||||
|
||||
assert_eq!(graph.nodes.len(), 0);
|
||||
assert_eq!(graph.edges.len(), 0);
|
||||
assert_eq!(graph.next_node_id, 0);
|
||||
assert!(graph.root_node.is_none());
|
||||
assert!(!graph.has_filters());
|
||||
assert_eq!(graph.execution_order().count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_preserves_capacity() {
|
||||
let mut graph = RenderGraph::new();
|
||||
let from = graph.add_node(RenderNodeKind::RootLayer {
|
||||
layer_id: 0,
|
||||
wtile_bbox: dummy_bbox(),
|
||||
});
|
||||
let to = graph.add_node(RenderNodeKind::RootLayer {
|
||||
layer_id: 1,
|
||||
wtile_bbox: dummy_bbox(),
|
||||
});
|
||||
graph.add_edge(from, to, DependencyKind::Sequential { layer_id: 1 });
|
||||
|
||||
let nodes_capacity = graph.nodes.capacity();
|
||||
let edges_capacity = graph.edges.capacity();
|
||||
|
||||
graph.clear();
|
||||
|
||||
assert!(graph.nodes.capacity() >= nodes_capacity);
|
||||
assert!(graph.edges.capacity() >= edges_capacity);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_node_returns_sequential_ids() {
|
||||
let mut graph = RenderGraph::new();
|
||||
let id0 = graph.add_node(RenderNodeKind::RootLayer {
|
||||
layer_id: 0,
|
||||
wtile_bbox: dummy_bbox(),
|
||||
});
|
||||
let id1 = graph.add_node(RenderNodeKind::FilterLayer {
|
||||
layer_id: 1,
|
||||
filter: dummy_filter(),
|
||||
wtile_bbox: dummy_bbox(),
|
||||
transform: Affine::IDENTITY,
|
||||
});
|
||||
|
||||
assert_eq!(id0, 0);
|
||||
assert_eq!(id1, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_node_tracks_root() {
|
||||
let mut graph = RenderGraph::new();
|
||||
let root_id = graph.add_node(RenderNodeKind::RootLayer {
|
||||
layer_id: 0,
|
||||
wtile_bbox: dummy_bbox(),
|
||||
});
|
||||
|
||||
assert_eq!(graph.root_node, Some(root_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_node_filter_sets_has_filters() {
|
||||
let mut graph = RenderGraph::new();
|
||||
|
||||
graph.add_node(RenderNodeKind::FilterLayer {
|
||||
layer_id: 1,
|
||||
filter: dummy_filter(),
|
||||
wtile_bbox: dummy_bbox(),
|
||||
transform: Affine::IDENTITY,
|
||||
});
|
||||
|
||||
assert!(graph.has_filters());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_node_root_does_not_set_has_filters() {
|
||||
let mut graph = RenderGraph::new();
|
||||
|
||||
graph.add_node(RenderNodeKind::RootLayer {
|
||||
layer_id: 0,
|
||||
wtile_bbox: dummy_bbox(),
|
||||
});
|
||||
|
||||
assert!(!graph.has_filters());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_edge_creates_dependency() {
|
||||
let mut graph = RenderGraph::new();
|
||||
let from = graph.add_node(RenderNodeKind::FilterLayer {
|
||||
layer_id: 1,
|
||||
filter: dummy_filter(),
|
||||
wtile_bbox: dummy_bbox(),
|
||||
transform: Affine::IDENTITY,
|
||||
});
|
||||
let to = graph.add_node(RenderNodeKind::RootLayer {
|
||||
layer_id: 0,
|
||||
wtile_bbox: dummy_bbox(),
|
||||
});
|
||||
|
||||
graph.add_edge(from, to, DependencyKind::Sequential { layer_id: 1 });
|
||||
|
||||
assert_eq!(graph.edges.len(), 1);
|
||||
assert_eq!(graph.edges[0].from, from);
|
||||
assert_eq!(graph.edges[0].to, to);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_edge_multiple_from_same_node() {
|
||||
let mut graph = RenderGraph::new();
|
||||
let from = graph.add_node(RenderNodeKind::FilterLayer {
|
||||
layer_id: 1,
|
||||
filter: dummy_filter(),
|
||||
wtile_bbox: dummy_bbox(),
|
||||
transform: Affine::IDENTITY,
|
||||
});
|
||||
let to1 = graph.add_node(RenderNodeKind::FilterLayer {
|
||||
layer_id: 2,
|
||||
filter: dummy_filter(),
|
||||
wtile_bbox: dummy_bbox(),
|
||||
transform: Affine::IDENTITY,
|
||||
});
|
||||
let to2 = graph.add_node(RenderNodeKind::RootLayer {
|
||||
layer_id: 0,
|
||||
wtile_bbox: dummy_bbox(),
|
||||
});
|
||||
|
||||
graph.add_edge(from, to1, DependencyKind::Sequential { layer_id: 1 });
|
||||
graph.add_edge(from, to2, DependencyKind::Sequential { layer_id: 1 });
|
||||
|
||||
assert_eq!(graph.edges.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execution_order_includes_recorded_nodes() {
|
||||
let mut graph = RenderGraph::new();
|
||||
let filter = graph.add_node(RenderNodeKind::FilterLayer {
|
||||
layer_id: 1,
|
||||
filter: dummy_filter(),
|
||||
wtile_bbox: dummy_bbox(),
|
||||
transform: Affine::IDENTITY,
|
||||
});
|
||||
|
||||
graph.record_node_for_execution(filter);
|
||||
|
||||
let order: Vec<_> = graph.execution_order().collect();
|
||||
assert_eq!(order, vec![filter]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execution_order_includes_only_root_when_present() {
|
||||
let mut graph = RenderGraph::new();
|
||||
let root = graph.add_node(RenderNodeKind::RootLayer {
|
||||
layer_id: 0,
|
||||
wtile_bbox: dummy_bbox(),
|
||||
});
|
||||
|
||||
let order: Vec<_> = graph.execution_order().collect();
|
||||
assert_eq!(order, vec![root]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execution_order_root_comes_last() {
|
||||
let mut graph = RenderGraph::new();
|
||||
let filter1 = graph.add_node(RenderNodeKind::FilterLayer {
|
||||
layer_id: 1,
|
||||
filter: dummy_filter(),
|
||||
wtile_bbox: dummy_bbox(),
|
||||
transform: Affine::IDENTITY,
|
||||
});
|
||||
let filter2 = graph.add_node(RenderNodeKind::FilterLayer {
|
||||
layer_id: 2,
|
||||
filter: dummy_filter(),
|
||||
wtile_bbox: dummy_bbox(),
|
||||
transform: Affine::IDENTITY,
|
||||
});
|
||||
let root = graph.add_node(RenderNodeKind::RootLayer {
|
||||
layer_id: 0,
|
||||
wtile_bbox: dummy_bbox(),
|
||||
});
|
||||
|
||||
graph.record_node_for_execution(filter2);
|
||||
graph.record_node_for_execution(filter1);
|
||||
|
||||
let order: Vec<_> = graph.execution_order().collect();
|
||||
assert_eq!(order, vec![filter2, filter1, root]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn topological_sort_diamond_dependency() {
|
||||
let mut graph = RenderGraph::new();
|
||||
let bottom = graph.add_node(RenderNodeKind::FilterLayer {
|
||||
layer_id: 3,
|
||||
filter: dummy_filter(),
|
||||
wtile_bbox: dummy_bbox(),
|
||||
transform: Affine::IDENTITY,
|
||||
});
|
||||
let left = graph.add_node(RenderNodeKind::FilterLayer {
|
||||
layer_id: 1,
|
||||
filter: dummy_filter(),
|
||||
wtile_bbox: dummy_bbox(),
|
||||
transform: Affine::IDENTITY,
|
||||
});
|
||||
let right = graph.add_node(RenderNodeKind::FilterLayer {
|
||||
layer_id: 2,
|
||||
filter: dummy_filter(),
|
||||
wtile_bbox: dummy_bbox(),
|
||||
transform: Affine::IDENTITY,
|
||||
});
|
||||
let top = graph.add_node(RenderNodeKind::RootLayer {
|
||||
layer_id: 0,
|
||||
wtile_bbox: dummy_bbox(),
|
||||
});
|
||||
|
||||
// Diamond: bottom -> left -> top, bottom -> right -> top
|
||||
graph.add_edge(bottom, left, DependencyKind::Sequential { layer_id: 3 });
|
||||
graph.add_edge(bottom, right, DependencyKind::Sequential { layer_id: 3 });
|
||||
graph.add_edge(left, top, DependencyKind::Sequential { layer_id: 1 });
|
||||
graph.add_edge(right, top, DependencyKind::Sequential { layer_id: 2 });
|
||||
|
||||
let sorted = graph.topological_sort();
|
||||
assert_eq!(sorted, vec![bottom, right, left, top]);
|
||||
}
|
||||
|
||||
fn dummy_filter() -> Filter {
|
||||
Filter::from_primitive(crate::filter_effects::FilterPrimitive::Flood {
|
||||
color: AlphaColor::from_rgba8(0, 0, 0, 255),
|
||||
})
|
||||
}
|
||||
|
||||
fn dummy_bbox() -> WideTilesBbox {
|
||||
WideTilesBbox::ZERO
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
// Copyright 2025 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Shared render state.
|
||||
|
||||
use crate::kurbo::{Affine, Cap, Join, Stroke};
|
||||
use crate::paint::{PaintType, Tint};
|
||||
use crate::peniko::color::palette::css::BLACK;
|
||||
use crate::peniko::{BlendMode, Compose, Fill, Mix};
|
||||
|
||||
/// A render state which contains the style properties for path rendering and
|
||||
/// the current transform.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenderState {
|
||||
/// The paint type (solid color, gradient, or image).
|
||||
pub paint: PaintType,
|
||||
/// Transform applied to the paint coordinates.
|
||||
pub paint_transform: Affine,
|
||||
/// Stroke style for path stroking operations.
|
||||
pub stroke: Stroke,
|
||||
/// Transform applied to geometry.
|
||||
pub transform: Affine,
|
||||
/// Fill rule for path filling operations.
|
||||
pub fill_rule: Fill,
|
||||
/// Blend mode for compositing.
|
||||
pub blend_mode: BlendMode,
|
||||
/// The tint for image painting.
|
||||
pub tint: Option<Tint>,
|
||||
}
|
||||
|
||||
impl Default for RenderState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
paint: BLACK.into(),
|
||||
paint_transform: Affine::IDENTITY,
|
||||
stroke: Stroke {
|
||||
width: 1.0,
|
||||
join: Join::Bevel,
|
||||
start_cap: Cap::Butt,
|
||||
end_cap: Cap::Butt,
|
||||
..Default::default()
|
||||
},
|
||||
transform: Affine::IDENTITY,
|
||||
fill_rule: Fill::NonZero,
|
||||
blend_mode: BlendMode::new(Mix::Normal, Compose::SrcOver),
|
||||
tint: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderState {
|
||||
/// Reset to default state.
|
||||
pub fn reset(&mut self) {
|
||||
*self = Self::default();
|
||||
}
|
||||
}
|
||||
Vendored
+76
@@ -0,0 +1,76 @@
|
||||
// Copyright 2025 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! A number of SIMD extension traits.
|
||||
|
||||
use fearless_simd::*;
|
||||
|
||||
/// Splatting every 4th element in the vector, used for splatting the alpha value of
|
||||
/// a color to all lanes.
|
||||
pub trait Splat4thExt<S> {
|
||||
/// Splat every 4th element of the vector.
|
||||
fn splat_4th(self) -> Self;
|
||||
}
|
||||
|
||||
impl<S: Simd> Splat4thExt<S> for f32x4<S> {
|
||||
#[inline(always)]
|
||||
fn splat_4th(self) -> Self {
|
||||
// TODO: Explore whether it's just faster to manually access the 4th element and splat it.
|
||||
let zip1 = self.zip_high(self);
|
||||
zip1.zip_high(zip1)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Simd> Splat4thExt<S> for f32x8<S> {
|
||||
#[inline(always)]
|
||||
fn splat_4th(self) -> Self {
|
||||
let (mut p1, mut p2) = self.simd.split_f32x8(self);
|
||||
p1 = p1.splat_4th();
|
||||
p2 = p2.splat_4th();
|
||||
|
||||
self.simd.combine_f32x4(p1, p2)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Simd> Splat4thExt<S> for f32x16<S> {
|
||||
#[inline(always)]
|
||||
fn splat_4th(self) -> Self {
|
||||
let (mut p1, mut p2) = self.simd.split_f32x16(self);
|
||||
p1 = p1.splat_4th();
|
||||
p2 = p2.splat_4th();
|
||||
|
||||
self.simd.combine_f32x8(p1, p2)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Simd> Splat4thExt<S> for u8x16<S> {
|
||||
#[inline(always)]
|
||||
fn splat_4th(self) -> Self {
|
||||
// TODO: SIMDify
|
||||
[
|
||||
self[3], self[3], self[3], self[3], self[7], self[7], self[7], self[7], self[11],
|
||||
self[11], self[11], self[11], self[15], self[15], self[15], self[15],
|
||||
]
|
||||
.simd_into(self.simd)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Simd> Splat4thExt<S> for u8x32<S> {
|
||||
#[inline(always)]
|
||||
fn splat_4th(self) -> Self {
|
||||
let (mut p1, mut p2) = self.simd.split_u8x32(self);
|
||||
p1 = p1.splat_4th();
|
||||
p2 = p2.splat_4th();
|
||||
|
||||
self.simd.combine_u8x16(p1, p2)
|
||||
}
|
||||
}
|
||||
|
||||
/// Splat each single element in the vector to 4 lanes.
|
||||
#[inline(always)]
|
||||
pub fn element_wise_splat<S: Simd>(simd: S, input: f32x4<S>) -> f32x16<S> {
|
||||
simd.combine_f32x8(
|
||||
simd.combine_f32x4(f32x4::splat(simd, input[0]), f32x4::splat(simd, input[1])),
|
||||
simd.combine_f32x4(f32x4::splat(simd, input[2]), f32x4::splat(simd, input[3])),
|
||||
)
|
||||
}
|
||||
Vendored
+535
@@ -0,0 +1,535 @@
|
||||
// Copyright 2025 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Rendering strips.
|
||||
|
||||
use crate::flatten::Line;
|
||||
use crate::peniko::Fill;
|
||||
use crate::tile::{Tile, Tiles};
|
||||
use crate::util::f32_to_u8;
|
||||
use alloc::vec::Vec;
|
||||
use fearless_simd::*;
|
||||
|
||||
/// A strip.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Strip {
|
||||
/// The x coordinate of the strip, in user coordinates.
|
||||
pub x: u16,
|
||||
/// The y coordinate of the strip, in user coordinates.
|
||||
pub y: u16,
|
||||
/// Packed alpha index and fill gap flag.
|
||||
///
|
||||
/// Bit layout (u32):
|
||||
/// - bit 31: `fill_gap` (See `Strip::fill_gap()`).
|
||||
/// - bits 0..=30: `alpha_idx` (See `Strip::alpha_idx()`).
|
||||
packed_alpha_idx_fill_gap: u32,
|
||||
}
|
||||
|
||||
impl Strip {
|
||||
/// The bit mask for `fill_gap` packed into `packed_alpha_idx_fill_gap`.
|
||||
const FILL_GAP_MASK: u32 = 1 << 31;
|
||||
|
||||
/// Creates a new strip.
|
||||
pub fn new(x: u16, y: u16, alpha_idx: u32, fill_gap: bool) -> Self {
|
||||
// Ensure `alpha_idx` does not collide with the fill flag bit.
|
||||
assert!(
|
||||
alpha_idx & Self::FILL_GAP_MASK == 0,
|
||||
"`alpha_idx` too large"
|
||||
);
|
||||
let fill_gap = u32::from(fill_gap) << 31;
|
||||
Self {
|
||||
x,
|
||||
y,
|
||||
packed_alpha_idx_fill_gap: alpha_idx | fill_gap,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return whether the strip is a sentinel strip.
|
||||
pub fn is_sentinel(&self) -> bool {
|
||||
self.x == u16::MAX
|
||||
}
|
||||
|
||||
/// Return the y coordinate of the strip, in strip units.
|
||||
pub fn strip_y(&self) -> u16 {
|
||||
self.y / Tile::HEIGHT
|
||||
}
|
||||
|
||||
/// Returns the alpha index.
|
||||
#[inline(always)]
|
||||
pub fn alpha_idx(&self) -> u32 {
|
||||
self.packed_alpha_idx_fill_gap & !Self::FILL_GAP_MASK
|
||||
}
|
||||
|
||||
/// Sets the alpha index.
|
||||
///
|
||||
/// Note that the largest value that can be stored in the alpha index is `u32::MAX << 1`, as the
|
||||
/// highest bit is reserved for `fill_gap`.
|
||||
#[inline(always)]
|
||||
pub fn set_alpha_idx(&mut self, alpha_idx: u32) {
|
||||
// Ensure `alpha_idx` does not collide with the fill flag bit.
|
||||
assert!(
|
||||
alpha_idx & Self::FILL_GAP_MASK == 0,
|
||||
"`alpha_idx` too large"
|
||||
);
|
||||
let fill_gap = self.packed_alpha_idx_fill_gap & Self::FILL_GAP_MASK;
|
||||
self.packed_alpha_idx_fill_gap = alpha_idx | fill_gap;
|
||||
}
|
||||
|
||||
/// Returns whether the gap that lies between this strip and the previous in the same row should be filled.
|
||||
#[inline(always)]
|
||||
pub fn fill_gap(&self) -> bool {
|
||||
(self.packed_alpha_idx_fill_gap & Self::FILL_GAP_MASK) != 0
|
||||
}
|
||||
|
||||
/// Sets whether the gap that lies between this strip and the previous in the same row should be filled.
|
||||
#[inline(always)]
|
||||
pub fn set_fill_gap(&mut self, fill: bool) {
|
||||
let fill = u32::from(fill) << 31;
|
||||
self.packed_alpha_idx_fill_gap =
|
||||
(self.packed_alpha_idx_fill_gap & !Self::FILL_GAP_MASK) | fill;
|
||||
}
|
||||
|
||||
/// When early culling is active, geometry fully to the left of the viewport creates no tiles.
|
||||
/// However, if that geometry has a non-zero winding (e.g. a large shape surrounding the
|
||||
/// viewport), then we must output strips for those fills.
|
||||
///
|
||||
/// We reconstruct this "background" fill using `row_windings` (the winding at x=0) to emit solid
|
||||
/// strips for:
|
||||
/// 1. All rows vertically above the first visible tile.
|
||||
/// 2. 'Captive' rows between two tile-containing rows.
|
||||
/// 3. All rows vertically below the last visible tile.
|
||||
#[inline(always)]
|
||||
fn emit_culled_background<F>(
|
||||
start: u16,
|
||||
end: u16,
|
||||
strips: &mut Vec<Self>,
|
||||
alphas: &mut Vec<u8>,
|
||||
windings: &crate::tile::CulledWindings,
|
||||
mut should_fill: F,
|
||||
) where
|
||||
F: FnMut(i32) -> bool,
|
||||
{
|
||||
windings.for_active_rows_in_range(start as usize, end as usize, |row| {
|
||||
if should_fill(windings.coarse[row] as i32) {
|
||||
let y_pos = row as u16 * Tile::HEIGHT;
|
||||
strips.push(Self::new(0, y_pos, alphas.len() as u32, false));
|
||||
alphas.extend([255_u8; Tile::HEIGHT as usize * Tile::WIDTH as usize]);
|
||||
// TODO: Clamp to the scene width instead of u16::MAX; in the future there might
|
||||
// not be clamping on the x.
|
||||
strips.push(Self::new(u16::MAX, y_pos, alphas.len() as u32, true));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the tiles stored in `tiles` into the strip and alpha buffer.
|
||||
pub fn render(
|
||||
level: Level,
|
||||
tiles: &Tiles,
|
||||
strip_buf: &mut Vec<Strip>,
|
||||
alpha_buf: &mut Vec<u8>,
|
||||
fill_rule: Fill,
|
||||
aliasing_threshold: Option<u8>,
|
||||
lines: &[Line],
|
||||
) {
|
||||
dispatch!(level, simd => render_impl(simd,
|
||||
tiles,
|
||||
strip_buf,
|
||||
alpha_buf,
|
||||
fill_rule,
|
||||
aliasing_threshold,
|
||||
lines));
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn render_impl<S: Simd>(
|
||||
s: S,
|
||||
tiles: &Tiles,
|
||||
strip_buf: &mut Vec<Strip>,
|
||||
alpha_buf: &mut Vec<u8>,
|
||||
fill_rule: Fill,
|
||||
aliasing_threshold: Option<u8>,
|
||||
lines: &[Line],
|
||||
) {
|
||||
let row_windings = &tiles.windings.coarse;
|
||||
let has_culled_tiles = tiles.has_culled_tiles();
|
||||
|
||||
// If no tiles were culled and the tile buffer is empty, we can simply exit. If tiles were
|
||||
// culled, the tile buffer may be empty but there may be winding produced by culled geometry
|
||||
// left of the viewport that must be checked for filling.
|
||||
if !has_culled_tiles && tiles.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let should_fill = |winding: i32| match fill_rule {
|
||||
Fill::NonZero => winding != 0,
|
||||
Fill::EvenOdd => winding % 2 != 0,
|
||||
};
|
||||
|
||||
// Helper to handle "captive strips". When a row has tiles, but the first tile
|
||||
// is not at the left edge of the viewport (x != 0), we must emit a solid strip
|
||||
// from x=0 to that tile if the coarse winding dictates a fill.
|
||||
let emit_captive_strip =
|
||||
|y: u16, is_left_viewport: bool, strips: &mut Vec<Strip>, alphas: &mut Vec<u8>| {
|
||||
let coarse_wd = tiles.windings.coarse[y as usize] as i32;
|
||||
|
||||
if should_fill(coarse_wd) && !is_left_viewport {
|
||||
strips.push(Strip::new(0, y * Tile::HEIGHT, alphas.len() as u32, false));
|
||||
alphas.extend([255_u8; Tile::HEIGHT as usize * Tile::WIDTH as usize]);
|
||||
}
|
||||
|
||||
let mut acc = f32x4::splat(s, coarse_wd as f32);
|
||||
if is_left_viewport {
|
||||
let fine_winding: f32x4<_> = tiles.windings.partial[y as usize].simd_into(s);
|
||||
acc += fine_winding;
|
||||
}
|
||||
|
||||
(coarse_wd, acc)
|
||||
};
|
||||
|
||||
// The accumulated tile winding delta. A line that crosses the top edge of a tile
|
||||
// increments the delta if the line is directed upwards, and decrements it if goes
|
||||
// downwards. Horizontal lines leave it unchanged.
|
||||
let mut winding_delta: i32 = 0;
|
||||
|
||||
// The previous tile visited.
|
||||
let mut prev_tile = if has_culled_tiles && tiles.is_empty() {
|
||||
Tile::SENTINEL
|
||||
} else {
|
||||
*tiles.get(0)
|
||||
};
|
||||
|
||||
// The accumulated (fractional) winding of the tile-sized location we're currently at.
|
||||
// Note multiple tiles can be at the same location.
|
||||
// Note that we are also implicitly assuming here that the tile height exactly fits into a
|
||||
// SIMD vector (i.e. 128 bits).
|
||||
let mut location_winding = [f32x4::splat(s, 0.0); Tile::WIDTH as usize];
|
||||
// The accumulated (fractional) windings at this location's right edge. When we move to the
|
||||
// next location, this is splatted to that location's starting winding.
|
||||
let mut accumulated_winding = f32x4::splat(s, 0.0);
|
||||
|
||||
let left_viewport = prev_tile.x == 0;
|
||||
if has_culled_tiles {
|
||||
let row_max = prev_tile.y.min(row_windings.len() as u16);
|
||||
Strip::emit_culled_background(
|
||||
0,
|
||||
row_max,
|
||||
strip_buf,
|
||||
alpha_buf,
|
||||
&tiles.windings,
|
||||
should_fill,
|
||||
);
|
||||
if tiles.is_empty() {
|
||||
return;
|
||||
}
|
||||
let (wd, acc) = emit_captive_strip(prev_tile.y, left_viewport, strip_buf, alpha_buf);
|
||||
winding_delta = wd;
|
||||
accumulated_winding = acc;
|
||||
location_winding = [accumulated_winding; Tile::WIDTH as usize];
|
||||
}
|
||||
|
||||
// The strip we're building.
|
||||
let mut strip = Strip::new(
|
||||
prev_tile.x * Tile::WIDTH,
|
||||
prev_tile.y * Tile::HEIGHT,
|
||||
alpha_buf.len() as u32,
|
||||
should_fill(winding_delta) && !left_viewport,
|
||||
);
|
||||
|
||||
for (tile_idx, tile) in tiles.iter().copied().chain([Tile::SENTINEL]).enumerate() {
|
||||
let line = lines[tile.line_idx() as usize];
|
||||
let tile_left_x = f32::from(tile.x) * f32::from(Tile::WIDTH);
|
||||
let tile_top_y = f32::from(tile.y) * f32::from(Tile::HEIGHT);
|
||||
let p0_x = line.p0.x - tile_left_x;
|
||||
let p0_y = line.p0.y - tile_top_y;
|
||||
let p1_x = line.p1.x - tile_left_x;
|
||||
let p1_y = line.p1.y - tile_top_y;
|
||||
|
||||
// Push out the winding as an alpha mask when we move to the next location (i.e., a tile
|
||||
// without the same location).
|
||||
if !prev_tile.same_loc(&tile) {
|
||||
match fill_rule {
|
||||
Fill::NonZero => {
|
||||
let p1 = f32x4::splat(s, 0.5);
|
||||
let p2 = f32x4::splat(s, 255.0);
|
||||
|
||||
#[expect(clippy::needless_range_loop, reason = "dimension clarity")]
|
||||
for x in 0..Tile::WIDTH as usize {
|
||||
let area = location_winding[x];
|
||||
let coverage = area.abs();
|
||||
let mulled = coverage.mul_add(p2, p1);
|
||||
// Note that we are not storing the location winding here but the actual
|
||||
// alpha value as f32, so we reuse the variable as a temporary storage.
|
||||
// Also note that we need the `min` here because the winding can be > 1
|
||||
// and thus the calculated alpha value need to be clamped to 255.
|
||||
location_winding[x] = mulled.min(p2);
|
||||
}
|
||||
}
|
||||
Fill::EvenOdd => {
|
||||
let p1 = f32x4::splat(s, 0.5);
|
||||
let p2 = f32x4::splat(s, -2.0);
|
||||
let p3 = f32x4::splat(s, 255.0);
|
||||
|
||||
#[expect(clippy::needless_range_loop, reason = "dimension clarity")]
|
||||
for x in 0..Tile::WIDTH as usize {
|
||||
let area = location_winding[x];
|
||||
let im1 = area.mul_add(p1, p1).floor();
|
||||
let coverage = p2.mul_add(im1, area).abs();
|
||||
let mulled = p3.mul_add(coverage, p1);
|
||||
// TODO: It is possible that, unlike for `NonZero`, we don't need the `min`
|
||||
// here.
|
||||
location_winding[x] = mulled.min(p3);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let p1 = s.combine_f32x4(location_winding[0], location_winding[1]);
|
||||
let p2 = s.combine_f32x4(location_winding[2], location_winding[3]);
|
||||
|
||||
let mut u8_vals = f32_to_u8(s.combine_f32x8(p1, p2));
|
||||
|
||||
if let Some(aliasing_threshold) = aliasing_threshold {
|
||||
u8_vals = s.select_u8x16(
|
||||
u8_vals.simd_ge(u8x16::splat(s, aliasing_threshold)),
|
||||
u8x16::splat(s, 255),
|
||||
u8x16::splat(s, 0),
|
||||
);
|
||||
}
|
||||
|
||||
alpha_buf.extend_from_slice(u8_vals.as_slice());
|
||||
|
||||
#[expect(clippy::needless_range_loop, reason = "dimension clarity")]
|
||||
for x in 0..Tile::WIDTH as usize {
|
||||
location_winding[x] = accumulated_winding;
|
||||
}
|
||||
}
|
||||
|
||||
// Push out the strip if we're moving to a next strip.
|
||||
if !prev_tile.same_loc(&tile) && !prev_tile.prev_loc(&tile) {
|
||||
debug_assert_eq!(
|
||||
(prev_tile.x as u32 + 1) * Tile::WIDTH as u32 - strip.x as u32,
|
||||
((alpha_buf.len() - strip.alpha_idx() as usize) / usize::from(Tile::HEIGHT)) as u32,
|
||||
"The number of columns written to the alpha buffer should equal the number of columns spanned by this strip."
|
||||
);
|
||||
strip_buf.push(strip);
|
||||
|
||||
let is_sentinel = tile_idx == tiles.len() as usize;
|
||||
let left_viewport = tile.x == 0;
|
||||
if !prev_tile.same_row(&tile) {
|
||||
// Emit a final strip in the row if there is non-zero winding for the sparse fill,
|
||||
// or unconditionally if we've reached the sentinel tile to end the path (the
|
||||
// `alpha_idx` field is used for width calculations).
|
||||
if winding_delta != 0 || is_sentinel {
|
||||
strip_buf.push(Strip::new(
|
||||
u16::MAX,
|
||||
prev_tile.y * Tile::HEIGHT,
|
||||
alpha_buf.len() as u32,
|
||||
should_fill(winding_delta),
|
||||
));
|
||||
}
|
||||
|
||||
// Logic identical to the start (see above): fill any vertical gaps (empty rows)
|
||||
// between the previous and current tile using the row windings.
|
||||
if has_culled_tiles && !is_sentinel {
|
||||
Strip::emit_culled_background(
|
||||
prev_tile.y + 1,
|
||||
tile.y,
|
||||
strip_buf,
|
||||
alpha_buf,
|
||||
&tiles.windings,
|
||||
should_fill,
|
||||
);
|
||||
|
||||
let (wd, acc) = emit_captive_strip(tile.y, left_viewport, strip_buf, alpha_buf);
|
||||
winding_delta = wd;
|
||||
accumulated_winding = acc;
|
||||
} else {
|
||||
winding_delta = 0;
|
||||
accumulated_winding = f32x4::splat(s, 0.0);
|
||||
};
|
||||
|
||||
#[expect(clippy::needless_range_loop, reason = "dimension clarity")]
|
||||
for x in 0..Tile::WIDTH as usize {
|
||||
location_winding[x] = accumulated_winding;
|
||||
}
|
||||
} else {
|
||||
// Note: this fill is mathematically not necessary. It provides a way to reduce
|
||||
// accumulation of float rounding errors.
|
||||
accumulated_winding = f32x4::splat(s, winding_delta as f32);
|
||||
}
|
||||
|
||||
if is_sentinel {
|
||||
break;
|
||||
}
|
||||
|
||||
strip = Strip::new(
|
||||
tile.x * Tile::WIDTH,
|
||||
tile.y * Tile::HEIGHT,
|
||||
alpha_buf.len() as u32,
|
||||
should_fill(winding_delta) && !left_viewport,
|
||||
);
|
||||
}
|
||||
prev_tile = tile;
|
||||
|
||||
// TODO: horizontal geometry has no impact on winding. This branch will be removed when
|
||||
// horizontal geometry is culled at the tile-generation stage.
|
||||
if p0_y == p1_y {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Lines moving upwards (in a y-down coordinate system) add to winding; lines moving
|
||||
// downwards subtract from winding.
|
||||
let sign = (p0_y - p1_y).signum();
|
||||
|
||||
// Calculate winding / pixel area coverage.
|
||||
//
|
||||
// Conceptually, horizontal rays are shot from left to right. Every time the ray crosses a
|
||||
// line that is directed upwards (decreasing `y`), the winding is incremented. Every time
|
||||
// the ray crosses a line moving downwards (increasing `y`), the winding is decremented.
|
||||
// The fractional area coverage of a pixel is the integral of the winding within it.
|
||||
//
|
||||
// Practically, to calculate this, each pixel is considered individually, and we determine
|
||||
// whether the line moves through this pixel. The line's y-delta within this pixel is
|
||||
// accumulated and added to the area coverage of pixels to the right. Within the pixel
|
||||
// itself, the area to the right of the line segment forms a trapezoid (or a triangle in
|
||||
// the degenerate case). The area of this trapezoid is added to the pixel's area coverage.
|
||||
//
|
||||
// For example, consider the following pixel square, with a line indicated by asterisks
|
||||
// starting inside the pixel and crossing its bottom edge. The area covered is the
|
||||
// trapezoid on the bottom-right enclosed by the line and the pixel square. The area is
|
||||
// positive if the line moves down, and negative otherwise.
|
||||
//
|
||||
// __________________
|
||||
// | |
|
||||
// | *------|
|
||||
// | * |
|
||||
// | * |
|
||||
// | * |
|
||||
// | * |
|
||||
// | * |
|
||||
// |___*____________|
|
||||
// *
|
||||
// *
|
||||
|
||||
let (line_top_y, line_top_x, line_bottom_y, line_bottom_x) = if p0_y < p1_y {
|
||||
(p0_y, p0_x, p1_y, p1_x)
|
||||
} else {
|
||||
(p1_y, p1_x, p0_y, p0_x)
|
||||
};
|
||||
|
||||
let y_slope = (line_bottom_y - line_top_y) / (line_bottom_x - line_top_x);
|
||||
let x_slope = 1. / y_slope;
|
||||
|
||||
winding_delta += sign as i32 * i32::from(tile.winding());
|
||||
|
||||
let line_top_y = f32x4::splat(s, line_top_y);
|
||||
let line_bottom_y = f32x4::splat(s, line_bottom_y);
|
||||
|
||||
// See the explanation of this term on the `line_px_left_yx` and `line_px_right_yx`
|
||||
// variables below.
|
||||
let line_px_base_yx = line_top_y.mul_add(-x_slope, line_top_x);
|
||||
|
||||
let px_top_y = f32x4::simd_from(s, [0., 1., 2., 3.]);
|
||||
let px_bottom_y = 1. + px_top_y;
|
||||
|
||||
let ymin = line_top_y.max(px_top_y);
|
||||
let ymax = line_bottom_y.min(px_bottom_y);
|
||||
|
||||
let mut acc = f32x4::splat(s, 0.0);
|
||||
|
||||
for x_idx in 0..Tile::WIDTH {
|
||||
let x_idx_s = f32x4::splat(s, x_idx as f32);
|
||||
let px_left_x = x_idx_s;
|
||||
let px_right_x = 1.0 + x_idx_s;
|
||||
|
||||
// The y-coordinate of the intersections between the line and the pixel's left and
|
||||
// right edges respectively.
|
||||
//
|
||||
// There is some subtlety going on here: `y_slope` will usually be finite, but will
|
||||
// be `inf` for purely vertical lines (`p0_x == p1_x`).
|
||||
//
|
||||
// In the case of `inf`, the resulting slope calculation will be `-inf` or `inf`
|
||||
// depending on whether the pixel edge is left or right of the line, respectively
|
||||
// (from the viewport's coordinate system perspective). The `min` and `max`
|
||||
// y-clamping logic generalizes nicely, as a pixel edge to the left of the line is
|
||||
// clamped to `ymin`, and a pixel edge to the right is clamped to `ymax`.
|
||||
//
|
||||
// In the special case where a vertical line and pixel edge are at the exact same
|
||||
// x-position (collinear), the line belongs to the pixel on whose _left_ edge it is
|
||||
// situated. The resulting slope calculation for the edge the line is situated on
|
||||
// will be NaN, as `0 * inf` results in NaN. This is true for both the left and
|
||||
// right edge.
|
||||
//
|
||||
// We know `ymin` and `ymax` are finite. We require the `max` operation to pick `ymin`
|
||||
// if its first operand is NaN. On x86, that maps to the semantics of `_mm_max_ps`,
|
||||
// which `f32x4::max` emits: that instruction takes element-wise
|
||||
// `if first > second { first } else { second }`. For AArch64, we do require the
|
||||
// `f32x4::max_precise` semantics (as `vmax_f32` returns NaN if either operand is NaN);
|
||||
// however, for AArch64 the precise version should be comparatively less expensive than
|
||||
// on x86. For `min`, we then know both operands are finite, so we can unambiguously
|
||||
// use the relaxed version. If this ever breaks, tests should fail loudly, because NaNs
|
||||
// happen a lot here!
|
||||
trait F32x4MaxExt {
|
||||
fn max_if_first_nan_take_second(self, rhs: Self) -> Self;
|
||||
}
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
impl<S: Simd> F32x4MaxExt for f32x4<S> {
|
||||
#[inline(always)]
|
||||
fn max_if_first_nan_take_second(self, rhs: Self) -> Self {
|
||||
self.max(rhs)
|
||||
}
|
||||
}
|
||||
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
|
||||
impl<S: Simd> F32x4MaxExt for f32x4<S> {
|
||||
#[inline(always)]
|
||||
fn max_if_first_nan_take_second(self, rhs: Self) -> Self {
|
||||
self.max_precise(rhs)
|
||||
}
|
||||
}
|
||||
let line_px_left_y = (px_left_x - line_top_x)
|
||||
.mul_add(y_slope, line_top_y)
|
||||
.max_if_first_nan_take_second(ymin)
|
||||
.min(ymax);
|
||||
let line_px_right_y = (px_right_x - line_top_x)
|
||||
.mul_add(y_slope, line_top_y)
|
||||
.max_if_first_nan_take_second(ymin)
|
||||
.min(ymax);
|
||||
|
||||
// For each pixel we calculate the x-coordinates of the left- and rightmost points on
|
||||
// the line segment within that pixel. We do this based on the y-offsets of those two
|
||||
// points from the top of the line. This can be calculated as, e.g.,
|
||||
// `(line_px_left_y - line_top_y) * x_slope + line_top_x`.
|
||||
//
|
||||
// Rather than calculating that y-offset twice for each pixel within the loop through
|
||||
// subtracting from the points' y-coordinates, we get rid of that subtraction by baking
|
||||
// it into the algebraic "base" x-coordinate `line_px_base_yx` calculated above the
|
||||
// loop. When adding that term to `y * x_slope` it gives the x-coordinate of the point
|
||||
// along the line.
|
||||
//
|
||||
// Note `x_slope` is always finite, as horizontal geometry is elided.
|
||||
let line_px_left_yx = line_px_left_y.mul_add(x_slope, line_px_base_yx);
|
||||
let line_px_right_yx = line_px_right_y.mul_add(x_slope, line_px_base_yx);
|
||||
let h = (line_px_right_y - line_px_left_y).abs();
|
||||
|
||||
// The trapezoidal area enclosed between the line and the right edge of the pixel
|
||||
// square. More straightforwardly written as follows, but the `madd` is faster.
|
||||
// 0.5 * h * (2. * px_right_x - line_px_right_yx - line_px_left_yx).
|
||||
let area = h * (line_px_right_yx + line_px_left_yx).mul_add(-0.5, px_right_x);
|
||||
location_winding[x_idx as usize] += area.mul_add(sign, acc);
|
||||
acc = h.mul_add(sign, acc);
|
||||
}
|
||||
|
||||
accumulated_winding += acc;
|
||||
}
|
||||
|
||||
if has_culled_tiles {
|
||||
Strip::emit_culled_background(
|
||||
(prev_tile.y + 1).min(row_windings.len() as u16),
|
||||
row_windings.len() as u16,
|
||||
strip_buf,
|
||||
alpha_buf,
|
||||
&tiles.windings,
|
||||
should_fill,
|
||||
);
|
||||
}
|
||||
}
|
||||
+440
@@ -0,0 +1,440 @@
|
||||
// Copyright 2025 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Abstraction for generating strips from paths.
|
||||
|
||||
use crate::clip::{PathDataRef, intersect};
|
||||
use crate::fearless_simd::Level;
|
||||
use crate::flatten::{FlattenCtx, Line};
|
||||
use crate::geometry::RectU16;
|
||||
use crate::kurbo::{Affine, PathEl, Rect, Stroke};
|
||||
use crate::peniko::Fill;
|
||||
use crate::strip::Strip;
|
||||
use crate::tile::Tiles;
|
||||
use crate::{flatten, rect, strip};
|
||||
use alloc::vec::Vec;
|
||||
use peniko::kurbo::StrokeCtx;
|
||||
|
||||
/// A storage for storing strip-related data.
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub struct StripStorage {
|
||||
/// The strips in the storage.
|
||||
pub strips: Vec<Strip>,
|
||||
/// The alphas in the storage.
|
||||
pub alphas: Vec<u8>,
|
||||
generation_mode: GenerationMode,
|
||||
}
|
||||
|
||||
/// The generation mode of the strip storage.
|
||||
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum GenerationMode {
|
||||
#[default]
|
||||
/// Clear strips before generating the new ones.
|
||||
Replace,
|
||||
/// Don't clear strips, append to the existing buffer.
|
||||
Append,
|
||||
/// Truncate strips to the given index before generating new ones,
|
||||
/// preserving strips in `[0..n]`.
|
||||
ReplaceAfter(usize),
|
||||
}
|
||||
|
||||
impl StripStorage {
|
||||
/// Create a new strip storage with the given generation mode.
|
||||
pub fn new(generation_mode: GenerationMode) -> Self {
|
||||
Self {
|
||||
strips: Vec::new(),
|
||||
alphas: Vec::new(),
|
||||
generation_mode,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the storage.
|
||||
pub fn clear(&mut self) {
|
||||
self.strips.clear();
|
||||
self.alphas.clear();
|
||||
}
|
||||
|
||||
/// Get the current generation mode.
|
||||
pub fn generation_mode(&self) -> GenerationMode {
|
||||
self.generation_mode
|
||||
}
|
||||
|
||||
/// Set the generation mode of the storage.
|
||||
pub fn set_generation_mode(&mut self, mode: GenerationMode) {
|
||||
self.generation_mode = mode;
|
||||
}
|
||||
|
||||
/// Whether the strip storage is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.strips.is_empty() && self.alphas.is_empty()
|
||||
}
|
||||
|
||||
/// Extend the current strip storage with the data from another storage.
|
||||
pub fn extend(&mut self, other: &Self) {
|
||||
self.strips.extend(&other.strips);
|
||||
self.alphas.extend(&other.alphas);
|
||||
}
|
||||
}
|
||||
|
||||
/// An object for easily generating strips for a filled/stroked path.
|
||||
#[derive(Debug)]
|
||||
pub struct StripGenerator {
|
||||
pub(crate) level: Level,
|
||||
line_buf: Vec<Line>,
|
||||
flatten_ctx: FlattenCtx,
|
||||
stroke_ctx: StrokeCtx,
|
||||
temp_storage: StripStorage,
|
||||
tiles: Tiles,
|
||||
width: u16,
|
||||
height: u16,
|
||||
}
|
||||
|
||||
impl StripGenerator {
|
||||
/// Create a new strip generator.
|
||||
pub fn new(width: u16, height: u16, level: Level) -> Self {
|
||||
Self {
|
||||
level,
|
||||
line_buf: Vec::new(),
|
||||
tiles: Tiles::new(level, height),
|
||||
flatten_ctx: FlattenCtx::default(),
|
||||
stroke_ctx: StrokeCtx::default(),
|
||||
temp_storage: StripStorage::default(),
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get this strip generator's viewport width.
|
||||
#[inline(always)]
|
||||
pub fn width(&self) -> u16 {
|
||||
self.width
|
||||
}
|
||||
|
||||
/// Get this strip generator's viewport height.
|
||||
#[inline(always)]
|
||||
pub fn height(&self) -> u16 {
|
||||
self.height
|
||||
}
|
||||
|
||||
/// Generate the strips for a filled path.
|
||||
pub fn generate_filled_path(
|
||||
&mut self,
|
||||
path: impl IntoIterator<Item = PathEl>,
|
||||
fill_rule: Fill,
|
||||
transform: Affine,
|
||||
aliasing_threshold: Option<u8>,
|
||||
strip_storage: &mut StripStorage,
|
||||
clip_path: Option<PathDataRef<'_>>,
|
||||
) {
|
||||
let cull_bbox = clip_path
|
||||
.map(|clip_path| clip_path.bbox)
|
||||
.unwrap_or(RectU16::new(0, 0, self.width, self.height));
|
||||
flatten::fill(
|
||||
self.level,
|
||||
path,
|
||||
transform,
|
||||
&mut self.line_buf,
|
||||
&mut self.flatten_ctx,
|
||||
cull_bbox,
|
||||
);
|
||||
|
||||
self.generate_with_clip(aliasing_threshold, strip_storage, fill_rule, clip_path);
|
||||
}
|
||||
|
||||
/// Generate the strips for a stroked path.
|
||||
pub fn generate_stroked_path(
|
||||
&mut self,
|
||||
path: impl IntoIterator<Item = PathEl>,
|
||||
stroke: &Stroke,
|
||||
transform: Affine,
|
||||
aliasing_threshold: Option<u8>,
|
||||
strip_storage: &mut StripStorage,
|
||||
clip_path: Option<PathDataRef<'_>>,
|
||||
) {
|
||||
let cull_bbox = clip_path
|
||||
.map(|clip_path| clip_path.bbox)
|
||||
.unwrap_or(RectU16::new(0, 0, self.width, self.height));
|
||||
flatten::stroke(
|
||||
self.level,
|
||||
path,
|
||||
stroke,
|
||||
transform,
|
||||
&mut self.line_buf,
|
||||
&mut self.flatten_ctx,
|
||||
&mut self.stroke_ctx,
|
||||
cull_bbox,
|
||||
);
|
||||
self.generate_with_clip(aliasing_threshold, strip_storage, Fill::NonZero, clip_path);
|
||||
}
|
||||
|
||||
fn generate_with_clip(
|
||||
&mut self,
|
||||
aliasing_threshold: Option<u8>,
|
||||
strip_storage: &mut StripStorage,
|
||||
fill_rule: Fill,
|
||||
clip_path: Option<PathDataRef<'_>>,
|
||||
) {
|
||||
self.tiles
|
||||
.make_tiles_analytic_aa(self.level, &self.line_buf, self.width, self.height);
|
||||
|
||||
self.tiles.sort_tiles();
|
||||
|
||||
let level = self.level;
|
||||
let tiles = &self.tiles;
|
||||
let line_buf = &self.line_buf;
|
||||
render_with_clip(
|
||||
level,
|
||||
&mut self.temp_storage,
|
||||
strip_storage,
|
||||
clip_path,
|
||||
|strips, alphas| {
|
||||
strip::render(
|
||||
level,
|
||||
tiles,
|
||||
strips,
|
||||
alphas,
|
||||
fill_rule,
|
||||
aliasing_threshold,
|
||||
line_buf,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Generate strips directly for a pixel-aligned rectangle.
|
||||
///
|
||||
/// This bypasses the full path processing pipeline (flatten -> tiles -> strips)
|
||||
/// by directly creating strip coverage data for the rectangle.
|
||||
pub fn generate_filled_rect_fast(
|
||||
&mut self,
|
||||
rect: &Rect,
|
||||
strip_storage: &mut StripStorage,
|
||||
clip_path: Option<PathDataRef<'_>>,
|
||||
) {
|
||||
let viewport = Rect::new(0.0, 0.0, self.width as f64, self.height as f64);
|
||||
let clip_bbox = clip_path
|
||||
.map(|clip| {
|
||||
// Clip bbox is always guaranteed to be within viewport bounds, so no need to
|
||||
// intersect again.
|
||||
Rect::new(
|
||||
f64::from(clip.bbox.x0),
|
||||
f64::from(clip.bbox.y0),
|
||||
f64::from(clip.bbox.x1),
|
||||
f64::from(clip.bbox.y1),
|
||||
)
|
||||
})
|
||||
.unwrap_or(viewport);
|
||||
let clamped = rect.abs().intersect(clip_bbox);
|
||||
|
||||
let level = self.level;
|
||||
render_with_clip(
|
||||
level,
|
||||
&mut self.temp_storage,
|
||||
strip_storage,
|
||||
clip_path,
|
||||
|strips, alphas| {
|
||||
rect::render(level, clamped, strips, alphas);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Reset the strip generator.
|
||||
pub fn reset(&mut self) {
|
||||
self.line_buf.clear();
|
||||
self.tiles.reset();
|
||||
self.temp_storage.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Render strips via `render_fn` with optional clip intersection.
|
||||
///
|
||||
/// When `clip_path` is `Some`, strips are rendered into `temp_storage` first, then
|
||||
/// intersected with the clip mask into `strip_storage`. Otherwise strips are rendered
|
||||
/// directly into `strip_storage`.
|
||||
fn render_with_clip(
|
||||
level: Level,
|
||||
temp_storage: &mut StripStorage,
|
||||
strip_storage: &mut StripStorage,
|
||||
clip_path: Option<PathDataRef<'_>>,
|
||||
render_fn: impl FnOnce(&mut Vec<Strip>, &mut Vec<u8>),
|
||||
) {
|
||||
match strip_storage.generation_mode {
|
||||
GenerationMode::Replace => strip_storage.strips.clear(),
|
||||
GenerationMode::Append => {}
|
||||
GenerationMode::ReplaceAfter(n) => strip_storage.strips.truncate(n),
|
||||
}
|
||||
|
||||
if let Some(clip_path) = clip_path {
|
||||
temp_storage.clear();
|
||||
|
||||
render_fn(&mut temp_storage.strips, &mut temp_storage.alphas);
|
||||
|
||||
let path_data = PathDataRef {
|
||||
strips: &temp_storage.strips,
|
||||
alphas: &temp_storage.alphas,
|
||||
bbox: RectU16::new(0, 0, u16::MAX, u16::MAX),
|
||||
};
|
||||
intersect(level, clip_path, path_data, strip_storage);
|
||||
} else {
|
||||
render_fn(&mut strip_storage.strips, &mut strip_storage.alphas);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use alloc::format;
|
||||
|
||||
use crate::fearless_simd::Level;
|
||||
use crate::kurbo::{Affine, Rect, Shape};
|
||||
use crate::peniko::Fill;
|
||||
use crate::strip_generator::{StripGenerator, StripStorage};
|
||||
|
||||
#[test]
|
||||
fn reset() {
|
||||
let mut generator = StripGenerator::new(100, 100, Level::baseline());
|
||||
let mut storage = StripStorage::default();
|
||||
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
|
||||
|
||||
generator.generate_filled_path(
|
||||
rect.to_path(0.1),
|
||||
Fill::NonZero,
|
||||
Affine::IDENTITY,
|
||||
None,
|
||||
&mut storage,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(!generator.line_buf.is_empty());
|
||||
assert!(!storage.is_empty());
|
||||
|
||||
generator.reset();
|
||||
storage.clear();
|
||||
|
||||
assert!(generator.line_buf.is_empty());
|
||||
assert!(storage.is_empty());
|
||||
}
|
||||
|
||||
/// Assert that `generate_filled_rect_fast` produces the same strips as the
|
||||
/// path-based pipeline for the given rectangle.
|
||||
fn assert_rect_fast_eq_path(rect: Rect, test_name: &str) {
|
||||
let mut generator = StripGenerator::new(100, 100, Level::baseline());
|
||||
let mut storage_path = StripStorage::default();
|
||||
let mut storage_rect = StripStorage::default();
|
||||
|
||||
generator.generate_filled_path(
|
||||
rect.to_path(0.1),
|
||||
Fill::NonZero,
|
||||
Affine::IDENTITY,
|
||||
None,
|
||||
&mut storage_path,
|
||||
None,
|
||||
);
|
||||
generator.reset();
|
||||
|
||||
generator.generate_filled_rect_fast(&rect, &mut storage_rect, None);
|
||||
|
||||
assert_eq!(
|
||||
storage_path.strips, storage_rect.strips,
|
||||
"{test_name}: strips mismatch",
|
||||
);
|
||||
assert_eq!(
|
||||
storage_path.alphas, storage_rect.alphas,
|
||||
"{test_name}: alphas mismatch",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rect_small_single_tile() {
|
||||
assert_rect_fast_eq_path(Rect::new(1.0, 1.0, 3.0, 3.0), "small_single_tile");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rect_spanning_multiple_tiles_horizontally() {
|
||||
assert_rect_fast_eq_path(Rect::new(2.0, 1.0, 14.0, 3.0), "spanning_horizontal");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rect_spanning_multiple_tiles_vertically() {
|
||||
assert_rect_fast_eq_path(Rect::new(1.0, 2.0, 3.0, 14.0), "spanning_vertical");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rect_spanning_multiple_tiles_both_directions() {
|
||||
assert_rect_fast_eq_path(Rect::new(2.0, 2.0, 18.0, 18.0), "spanning_both");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rect_tile_aligned() {
|
||||
assert_rect_fast_eq_path(Rect::new(0.0, 0.0, 8.0, 8.0), "tile_aligned");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rect_one_pixel_wide() {
|
||||
assert_rect_fast_eq_path(Rect::new(5.0, 2.0, 6.0, 12.0), "one_pixel_wide");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rect_one_pixel_tall() {
|
||||
assert_rect_fast_eq_path(Rect::new(2.0, 5.0, 12.0, 6.0), "one_pixel_tall");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rect_fractional_within_single_tile() {
|
||||
let cases: &[(f64, f64, f64, f64)] = &[
|
||||
(0.25, 0.75, 2.5, 3.5),
|
||||
(1.2, 1.3, 1.8, 1.7),
|
||||
(0.1, 0.1, 3.9, 3.9),
|
||||
(2.5, 2.5, 2.6, 2.6),
|
||||
(0.01, 0.99, 3.99, 3.01),
|
||||
];
|
||||
for (i, &(x0, y0, x1, y1)) in cases.iter().enumerate() {
|
||||
assert_rect_fast_eq_path(Rect::new(x0, y0, x1, y1), &format!("single_tile_{i}"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rect_fractional_multi_tile() {
|
||||
let cases: &[(f64, f64, f64, f64)] = &[
|
||||
(1.5, 2.3, 10.7, 8.9),
|
||||
(0.5, 0.5, 8.5, 8.5),
|
||||
(2.3, 5.1, 15.7, 5.9),
|
||||
(5.1, 2.3, 5.9, 15.7),
|
||||
(0.25, 0.25, 12.75, 12.75),
|
||||
(1.0 / 3.0, 2.0 / 3.0, 10.33, 8.67),
|
||||
(1.99, 2.01, 9.01, 7.99),
|
||||
(3.9, 3.9, 8.1, 8.1),
|
||||
(3.2, 6.3, 14.8, 6.7),
|
||||
(6.3, 3.2, 6.7, 14.8),
|
||||
(0.1, 0.9, 49.9, 49.1),
|
||||
(4.0, 2.7, 12.0, 9.3),
|
||||
(2.7, 4.0, 9.3, 12.0),
|
||||
(1.5, 1.2, 10.5, 2.8),
|
||||
(1.5, 2.5, 14.5, 18.5),
|
||||
(0.7, 0.3, 30.2, 25.8),
|
||||
(7.9, 7.9, 8.1, 8.1),
|
||||
(3.5, 0.5, 4.5, 0.9),
|
||||
(0.01, 0.01, 99.99, 99.99),
|
||||
(10.0, 10.0, 10.1, 10.1),
|
||||
];
|
||||
for (i, &(x0, y0, x1, y1)) in cases.iter().enumerate() {
|
||||
assert_rect_fast_eq_path(Rect::new(x0, y0, x1, y1), &format!("multi_tile_{i}"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rect_fractional_exhaustive() {
|
||||
for xi in 0..100_u32 {
|
||||
for yi in 0..100_u32 {
|
||||
let dx = xi as f64 * 0.01;
|
||||
let dy = yi as f64 * 0.01;
|
||||
let rect = Rect::new(dx, dy, 50.0 + dx, 50.0 + dy);
|
||||
assert_rect_fast_eq_path(rect, &format!("exhaustive_{dx}_{dy}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rect_inverted_both_axes() {
|
||||
assert_rect_fast_eq_path(Rect::new(18.0, 18.0, 2.0, 2.0), "inverted_both_axes");
|
||||
}
|
||||
}
|
||||
Vendored
+2426
File diff suppressed because it is too large
Load Diff
Vendored
+126
@@ -0,0 +1,126 @@
|
||||
// Copyright 2025 the Vello Authors
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
//! Utility functions.
|
||||
|
||||
use crate::math::FloatExt;
|
||||
use fearless_simd::{
|
||||
Bytes, Simd, SimdBase, SimdFloat, f32x16, u8x16, u8x32, u16x16, u16x32, u32x16,
|
||||
};
|
||||
use peniko::kurbo::Affine;
|
||||
#[cfg(not(feature = "std"))]
|
||||
use peniko::kurbo::common::FloatFuncs as _;
|
||||
|
||||
/// Convert f32x16 to u8x16.
|
||||
///
|
||||
/// **Important note: The values need to be between 0.0 and 1.0, otherwise you might
|
||||
/// get inconsistent results across different platforms.**
|
||||
// We can't guarantee correctness for values < 0.0 due to a restriction in fearless_simd:
|
||||
// https://github.com/linebender/fearless_simd/blob/3f4489389940b7c3c6ee1847a2d007a22494eeff/fearless_simd/src/generated/simd_types.rs#L1623
|
||||
#[inline(always)]
|
||||
pub fn f32_to_u8<S: Simd>(val: f32x16<S>) -> u8x16<S> {
|
||||
let simd = val.simd;
|
||||
let converted = val.to_int::<u32x16<S>>().to_bytes();
|
||||
|
||||
let (x8_1, x8_2) = simd.split_u8x64(converted);
|
||||
let (p1, p2) = simd.split_u8x32(x8_1);
|
||||
let (p3, p4) = simd.split_u8x32(x8_2);
|
||||
|
||||
let uzp1 = simd.unzip_low_u8x16(p1, p2);
|
||||
let uzp2 = simd.unzip_low_u8x16(p3, p4);
|
||||
simd.unzip_low_u8x16(uzp1, uzp2)
|
||||
}
|
||||
|
||||
/// A trait for implementing a fast approximal division by 255 for integers.
|
||||
pub trait Div255Ext {
|
||||
/// Divide by 255.
|
||||
fn div_255(self) -> Self;
|
||||
}
|
||||
|
||||
impl<S: Simd> Div255Ext for u16x32<S> {
|
||||
#[inline(always)]
|
||||
fn div_255(self) -> Self {
|
||||
let p1 = Self::splat(self.simd, 255);
|
||||
let p2 = self + p1;
|
||||
p2 >> 8
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Simd> Div255Ext for u16x16<S> {
|
||||
#[inline(always)]
|
||||
fn div_255(self) -> Self {
|
||||
let p1 = Self::splat(self.simd, 255);
|
||||
let p2 = self + p1;
|
||||
p2 >> 8
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform a normalized multiplication for u8x32.
|
||||
#[inline(always)]
|
||||
pub fn normalized_mul_u8x32<S: Simd>(a: u8x32<S>, b: u8x32<S>) -> u16x32<S> {
|
||||
(S::widen_u8x32(a.simd, a) * S::widen_u8x32(b.simd, b)).div_255()
|
||||
}
|
||||
|
||||
/// Perform a normalized multiplication for u8x16.
|
||||
#[inline(always)]
|
||||
pub fn normalized_mul_u8x16<S: Simd>(a: u8x16<S>, b: u8x16<S>) -> u16x16<S> {
|
||||
(S::widen_u8x16(a.simd, a) * S::widen_u8x16(b.simd, b)).div_255()
|
||||
}
|
||||
|
||||
/// Check if an affine transform is a pure integer translation.
|
||||
///
|
||||
/// Returns true if the transform only contains integer translation (no rotation,
|
||||
/// skew, or scaling), meaning rectangles will remain pixel-aligned after transformation.
|
||||
#[inline]
|
||||
pub fn is_integer_translation(transform: &Affine) -> bool {
|
||||
let [a, b, c, d, e, f] = transform.as_coeffs();
|
||||
(a - 1.0).is_nearly_zero()
|
||||
&& b.is_nearly_zero()
|
||||
&& c.is_nearly_zero()
|
||||
&& (d - 1.0).is_nearly_zero()
|
||||
&& (e - e.round()).is_nearly_zero()
|
||||
&& (f - f.round()).is_nearly_zero()
|
||||
}
|
||||
/// Check if an affine transform has no skewing (i.e. preserves axis alignment).
|
||||
#[inline]
|
||||
pub fn is_axis_aligned(transform: &Affine) -> bool {
|
||||
let [_, b, c, ..] = transform.as_coeffs();
|
||||
b.is_nearly_zero() && c.is_nearly_zero()
|
||||
}
|
||||
|
||||
/// Extract scale factors from an affine transform using singular value decomposition.
|
||||
///
|
||||
/// Returns a tuple of (`scale_x`, `scale_y`) representing the scale along each axis.
|
||||
/// This uses the same algorithm as kurbo's internal `svd()` method.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `transform` - The affine transformation to extract scales from.
|
||||
///
|
||||
/// # Returns
|
||||
/// A tuple `(scale_x, scale_y)` with minimum values clamped to 1e-6 to avoid division by zero.
|
||||
///
|
||||
/// # Note
|
||||
/// TODO: Consider making `Affine::svd()` public in kurbo to avoid duplicating this code.
|
||||
/// This implementation mirrors kurbo's internal SVD calculation for extracting scale factors
|
||||
/// from arbitrary affine transformations.
|
||||
#[inline]
|
||||
pub fn extract_scales(transform: &Affine) -> (f32, f32) {
|
||||
let [a, b, c, d, _, _] = transform.as_coeffs();
|
||||
let a = a as f32;
|
||||
let b = b as f32;
|
||||
let c = c as f32;
|
||||
let d = d as f32;
|
||||
|
||||
// Compute singular values using the same formula as kurbo's svd()
|
||||
let a2 = a * a;
|
||||
let b2 = b * b;
|
||||
let c2 = c * c;
|
||||
let d2 = d * d;
|
||||
let s1 = a2 + b2 + c2 + d2;
|
||||
let s2 = ((a2 - b2 + c2 - d2).powi(2) + 4.0 * (a * b + c * d).powi(2)).sqrt();
|
||||
|
||||
let scale_x = (0.5 * (s1 + s2)).sqrt();
|
||||
let scale_y = (0.5 * (s1 - s2)).sqrt();
|
||||
|
||||
(scale_x.max(1e-6), scale_y.max(1e-6))
|
||||
}
|
||||
Reference in New Issue
Block a user