diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index d35b66320..c9acb0512 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -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) diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 57944f400..fcf3570cc 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -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" diff --git a/Cargo.lock b/Cargo.lock index b881689e5..e8b3345fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 84b2ed1f2..41c9f90e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/vendor/README.md b/vendor/README.md index d622802db..b1166afa3 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -25,6 +25,7 @@ A PdfCraft patch in `hayro-syntax::Page::base_dimensions` expresses the existing | hayro-syntax | 0.7.2 | Apache-2.0 OR MIT | (1) Page-tree cycle guard in `resolve_pages` (visited set + depth cap): a `/Kids` loop among compressed objects overflowed the stack and aborted the process; (2) `ccitt_size_ok`: CCITT images wider than 2^20 columns or over 2^28 pixels are not decoded (a fuzzed `/Columns 4294967295` allocated 4 GiB); (3) Flate/LZW predictor row widths are saturating `usize`, a zero width is refused and a row longer than the data is not allocated (a fuzzed `/Columns 9223372036854775807` wrapped to a 2^61-byte allocation and aborted); (4) the inline-image end-of-data check (`EI`) doesn't nest inside its own look-ahead and examines at most 256 candidates and steps (50 images with "EI" in their data ran for over ten minutes); (5) a stream whose `/Length` is wrong is recovered only when `stream` is the next token — scanning ahead to a later object's `stream` made an indirect appearance-state dictionary (Quartz checkboxes: `/AP /N` → `<< /Yes … /Off … >>`) render as a non-form, so the widget was skipped; (6) JPX rescaling uses an exact integer sample range for 1–32-bit components (32 shifted past u32, and floating-point maxima at 25–31 bits wrapped to zero), and refuses zero-sized rows; (7) `decode_limit`: Flate (through flate2 and the fallback inflater), LZW and RunLength never write past `MAX_DECODED_STREAM` (256 MiB), or, for the last filter of an image of known size, past twice its pixel bytes (a predictor tag byte per byte), at most 1 GiB; `Stream::decoded_within` for budgeted callers; a page's `/Contents` array shares one `MAX_DECODED_STREAM` (a 2 MB page whose content stream or 8 × 8 image inflated to 2 GB took the renderer past 3 GB in under a second; naming the stream twenty times in `/Contents`, 5 GB); (8) high-precision numbers (jsPDF MediaBox coordinates with over 19 digits) parse without overflowing the u64 mantissa: fractional digits past it are dropped, integer digits past it scale the value (a real, kept finite); (9) large dictionary indexes are built on first use and shared across repeated reads within each PDF (#307); the validated-byte cache is bounded, reader contexts remain separate, repairs and inline/content dictionaries bypass sharing, and small dictionaries stay eager; (10) decoded object-stream slots publish once under contention, cold loading rejects recursive dependencies, and xref repair serializes without holding recursive read guards; this permits one parser per native render pool with private worker caches; (11) `sync.rs`: a poisoned lock is recovered instead of unwrapped (a worker panic no longer cascades into every reader of the same PDF), and xref repair takes a blocking write lock instead of asserting a non-blocking one; (12) `SegmentList` (`util.rs`) returns `None` for an out-of-range index instead of panicking; (13) a page's `/UserUnit` scales `render_dimensions` and `initial_transform`, so the page and everything on it are sized in points, as Acrobat shows them; it is read as Acrobat reads it (the page's own entry, not inherited; a number from 1 up, at most 75,000; anything else counts as 1), and a page without it keeps exactly the transform it had; (14) direct arrays and dictionaries share a reader-local nesting cap of 128, counted while skipping and while parsing a dictionary (its lenient repair reads, rather than skips, an object found where a key belongs, so `<< << …` recursed through that read); deeper objects return `None` instead of overflowing the stack (including unused trailer values, trailer entries, content operands and marked-content properties); a chain of 65 direct stitching functions now stops at this cap, before the 64-function guard | (1) `cos/tests/writer.rs::page_tree_loops_in_object_streams_do_not_crash_readers`; (2) `raster::tests::absurd_mask_and_fax_sizes_are_refused_before_decoding`; (3) `raster::tests::absurd_predictor_columns_and_line_widths_render`; (4) `raster::tests::inline_images_with_ei_in_their_data_parse_in_linear_time`; (5) `raster::tests::indirect_appearance_state_dict_is_not_another_stream`; (6) `raster::tests::jpx_stream_bit_depth_boundaries_do_not_panic`, `raster::tests::jpx_32_bit_image_does_not_prevent_other_page_content_from_rendering`, `raster::tests::jpx_gray_rgb_and_alpha_keep_eight_bit_samples`, `raster::tests::jpx_streams_reject_out_of_range_bit_depths`; (7) `raster::tests::decompression_bombs_stop_at_the_stream_limit`, `raster::tests::page_contents_and_nested_forms_share_one_budget`; (8) `inspect::tests::jspdf_high_precision_mediabox_does_not_overflow_dimensions`, `object::number::tests::high_precision_float_does_not_overflow_mantissa` (hayro-syntax); (9) `object::dict::lazy_index_tests`, `raster::tests::large_resource_indexes_preserve_pixels_and_text`; (10) `data::tests`, `xref::tests::concurrent_cold_object_stream_reads_all_succeed`, `raster::tests::shared_parser_is_lazy_and_initializes_once_under_contention`; (11) `data::tests::panicking_initializer_wakes_waiters_and_prevents_retries`, `xref::tests::concurrent_cold_object_stream_reads_all_succeed`; (12) `util::tests::segment_list_rejects_out_of_range_indices`; (13) `raster::tests::user_unit_scales_the_page_and_everything_on_it`; (14) `raster::tests::deeply_nested_direct_objects_do_not_overflow_the_stack`, `raster::tests::dictionaries_nested_without_keys_in_content_do_not_overflow_the_stack`, `raster::tests::dictionaries_nested_without_keys_in_the_trailer_do_not_overflow_the_stack`, `raster::tests::dictionaries_nested_without_keys_as_marked_content_properties_do_not_overflow_the_stack`, `raster::tests::dictionaries_nested_without_keys_are_read_up_to_the_nesting_cap`, `raster::tests::nesting_at_the_cap_fits_a_512_kib_stack` (native only), `raster::tests::function_limits_depth_guard_refuses_a_chain_of_references` | | winit | 0.30.13 | Apache-2.0 | (1) on Windows 11 (build 22000 and later), `WM_DPICHANGED` applies the rect Windows suggests. It used to nudge the window onto the monitor `MonitorFromWindow` names, which during a drag is still the monitor being left; the nudge sent another `WM_DPICHANGED`, so a window dragged to a monitor with another scale factor grew and stayed on the wrong monitor at the wrong scale (#324). Backport of winit master 488c036a ([winit#4341](https://github.com/rust-windowing/winit/pull/4341)), which no 0.30 release has; Windows 10 keeps the old adjustment, as on winit master. (2) on Wayland, a seat's text input is destroyed when the seat goes away, not whenever it loses a capability: losing any capability (a touch screen or tablet unplugged, a remote-desktop session ending) destroyed it, and only an added capability recreated it, so IME stopped working until restart. Port of [winit#4747](https://github.com/rust-windowing/winit/pull/4747); (3) on Windows, an IME composition reports its cursor from `GCS_CURSORPOS` only when `WM_IME_COMPOSITION` says it is set, and at the end of the composition otherwise: the Microsoft Korean IME never sets it, so the cursor read as 0 (egui then drew no caret while composing; it now draws it after the syllable). Port of [winit#4746](https://github.com/rust-windowing/winit/pull/4746); (4) on Windows, a hidden window is not maximized until it is shown, and a hidden window shown while maximized gets one `ShowWindow(SW_MAXIMIZE)`: `SW_MAXIMIZE` shows a hidden window, so PdfCraft, which eframe keeps hidden until its first frame is drawn, appeared unpainted (white) when it restored a maximized window, and could lose its focus. Port of [winit#4587](https://github.com/rust-windowing/winit/pull/4587) (the later, focus-safe form of [winit#4575](https://github.com/rust-windowing/winit/pull/4575)); (5) on X11, when allowing or disallowing IME replaces the focused window's input context (egui does it each time a text field gains or loses focus), the new context is focused: no focus event follows, so it stayed unfocused until the window lost and regained focus. Port of [winit#4727](https://github.com/rust-windowing/winit/pull/4727); (6) on macOS, `insertText` commits through IME when a composition was started (`pending_commit`, set by `setMarkedText`) rather than when marked text is left: input methods that end a composition with `unmarkText` before `insertText` lost the text, and Apple Korean dropped the key typed after a syllable (`한` then `5` or `;`); other `insertText` calls stay `KeyboardInput`; a commit outside a key press (a candidate clicked with the mouse, dictation) clears the marked text and leaves no key to swallow, as in [winit#4748](https://github.com/rust-windowing/winit/pull/4748) (without it the next key, such as Backspace, was lost). `validAttributesForMarkedText` lists the underline and marked-clause attributes, without which some input methods don't compose. Port of [winit#4650](https://github.com/rust-windowing/winit/pull/4650), the 0.30 backport of [winit#4651](https://github.com/rust-windowing/winit/pull/4651) (merged on winit master), with the underline attribute spelled as AppKit's `NSUnderlineStyleAttributeName` (`NSUnderline`); (7) on macOS, disallowing IME with a composition in progress (focus leaving a text field) tells the input method to discard its marked text (`discardMarkedText`): it kept composing it once IME was allowed again, so the abandoned syllable came back (`하`, then `가` typed in the next field, became `학`, then `하` + `가`; Japanese `か` then `さ` became `傘`). Port of [winit#4745](https://github.com/rust-windowing/winit/pull/4745); (8) on macOS, text inserted outside a key press without a composition (the emoji picker) is reported as `Ime::Commit`; it was dropped. Port of the emoji-picker part of [winit#4749](https://github.com/rust-windowing/winit/pull/4749) (0.30 backport of [winit#4748](https://github.com/rust-windowing/winit/pull/4748)); its `selectedRange` change for dictation is left out because it switches on press-and-hold for every held key (an accent menu instead of key repeat, and the chosen accent added after the repeated letters); (9) on macOS, a key whose text was committed through IME isn't also sent as `KeyboardInput` unless AppKit forwards it as a command (Enter): Apple Korean answers Space with no syllable in progress with `setMarkedText(" ")`, `insertText(" ")`, `setMarkedText("")`, and the last call reset winit's state, so the space arrived twice. Same goal as [winit#4478](https://github.com/rust-windowing/winit/pull/4478), whose changes don't reach this call order (macOS 14); to be proposed upstream there; (10) on macOS, when the first key in a fresh app reaches Apple Korean's `insertText` as a raw compatibility jamo instead of starting a composition, that key is interpreted once more, synchronously, before it is sent as `KeyboardInput` (g k s typed ㅎㅏㄴ instead of 한); only an unmodified, non-repeated first key whose one callback inserts that same jamo, with IME enabled and nothing composed, focus and input source unchanged, is retried (`ime_startup.rs`). Port of [winit#4744](https://github.com/rust-windowing/winit/pull/4744), with its policy tests.; (11) on X11 without `Xft.dpi`, the scale factor comes from the X screen's configured DPI (its size in pixels and millimetres) before the output's physical density from RandR: Xorg can deliberately run a dense panel at 96 DPI, and only winit applications then looked zoomed; `Xft.dpi` still wins, `WINIT_X11_SCALE_FACTOR` still overrides, and a screen reporting no size keeps the RandR fallback. PdfCraft patch (#1150, replacing the launcher pin proposed in #908). The published crate's asset files (`docs/res/*.svg`, `examples/data/*.png`) are left out, so its `window` example doesn't build | (1) `pdfcraft` `tests::winit_carries_the_windows_11_monitor_scale_fix` keeps the patch in the build; the drag itself needs two monitors with different scale factors on Windows 11; (2) `pdfcraft` `tests/winit_wayland_text_input.rs`; the capability change itself needs a Wayland session and an input method; (3) `pdfcraft` `tests/winit_windows_ime_cursor.rs`; composing itself needs the Microsoft Korean IME; (4) `pdfcraft` `tests/winit_windows_hidden_maximized.rs`; the start-up itself needs Windows and a window that was closed maximized; (5) `pdfcraft` `tests/winit_x11_ime_focus.rs`; switching fields itself needs an X11 session and an input method (IBus, fcitx5); (6) `pdfcraft` `tests/winit_macos_ime_commit.rs`; composing itself needs macOS and an input method; (7) `pdfcraft` `tests/winit_macos_ime_discard.rs`; composing itself needs macOS and an input method; (8) `pdfcraft` `tests/winit_macos_emoji_picker.rs`; the picker itself needs macOS; (9) `pdfcraft` `tests/winit_macos_korean_space.rs`; composing itself needs macOS and Apple Korean; (10) `pdfcraft` `tests/winit_macos_korean_first_key.rs` and `ime_startup.rs`'s own tests (run them with `rustc --test`); typing itself needs macOS and Apple Korean; (11) `pdfcraft` `tests/winit_x11_dpi.rs` keeps the patch in the build, and `util::randr` tests the policy (run them in the vendored crate); the scale itself needs an X11 session | | egui-wgpu | 0.36.2 | MIT OR Apache-2.0 | (1) `winit::surface_fit`: a window's surface is configured within the device's `max_texture_dimension_2d`, both sides shrunk by one factor that egui then draws at (`pixels_per_point × render_scale`), so the whole window is drawn into the surface (on DX12 it is stretched over the window and lines up with the pointer; wgpu's GL backend copies it unscaled into a corner); the longer side becomes exactly the limit; the depth and MSAA textures follow, and a recreated surface starts again from the window's own size. egui-wgpu configured the window's size as is, and `Surface::configure` panics beyond the limit: a window restored at another monitor's scale (opening an Outlook attachment on a mixed-DPI laptop asked an 8192-pixel device for 8600 × 3423, #577) or stretched across monitors closed PdfCraft, before its window appeared when it happened at start-up ([emilk/egui#8361](https://github.com/emilk/egui/issues/8361)). The published crate leaves out egui's licence files; `LICENSE-APACHE` and `LICENSE-MIT` are from the 0.36.2 tag; (2) `winit::catch_errors`: a new window's surface is configured inside wgpu error scopes (validation, out of memory, internal), and a failure is returned as `WgpuError::ConfigureSurface` (for the root window at start-up eframe returns it as `Error::Wgpu`, which PdfCraft retries with OpenGL); nothing is kept for the window. wgpu's default handler panicked instead, and eframe configures the root surface before the app is created, so a driver that can't configure one closed PdfCraft at launch (a 2015 Intel GPU through wgpu's GL backend timed out waiting for the GPU to go idle, `GpuWaitTimeout`, #519). Unchanged, so still a panic: an error in a later reconfigure (resize, a lost surface), a device lost during the first configure (wgpu reports it only to a callback), and a window shown at zero size (first configured on resize) | (1) `pdfcraft` `tests::a_window_larger_than_the_gpu_limit_gets_a_surface_within_it`, `tests::surface_fit_never_empties_or_overflows_a_surface`, and `tests::egui_wgpu_carries_the_surface_size_fit`, which keeps the patch in the build and in the paths that size a surface; (2) `pdfcraft` `tests::a_gpu_error_while_a_window_is_set_up_is_returned_not_a_panic` (a real device: an error inside `catch_errors` is returned, not a panic) and `tests::egui_wgpu_returns_a_surface_it_cannot_configure_as_an_error`, which keeps it around `add_surface`'s configure | +| vello_common | 0.0.8 | Apache-2.0 OR MIT | (1) `clip::intersect` (the clip step every clipped path passes through) starts its row walk at the first row both paths have strips in, not the earlier of the two first rows. The walk visited every row from the top of the clip for each path (on the A1 sample's full-page canvas, about half its strip rows per stroke), and rows where one path has no strips produce no output, so the strips are unchanged (vello_common 0.1 starts at the later row too). Its cost grew with the page, and a full-page render of the A1 sample was about 30% faster; the output is byte-identical | (1) `clip::tests::intersect_walks_only_rows_both_paths_have_strips` counts the rows the walk visits (`cargo test --manifest-path vendor/vello_common/Cargo.toml`) and fails without the patch; the render crate's pixel tests run through the patched crate | Renderer startup errors: `egui-wgpu::RenderState::create` also wraps `Renderer::new` in `winit::catch_errors` when the winit feature is enabled. A rejected shader or render pipeline diff --git a/vendor/vello_common/CHANGELOG.md b/vendor/vello_common/CHANGELOG.md new file mode 100644 index 000000000..912b7648a --- /dev/null +++ b/vendor/vello_common/CHANGELOG.md @@ -0,0 +1,246 @@ + + +# 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`. ([#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 diff --git a/vendor/vello_common/Cargo.lock b/vendor/vello_common/Cargo.lock new file mode 100644 index 000000000..72d0743ff --- /dev/null +++ b/vendor/vello_common/Cargo.lock @@ -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", +] diff --git a/vendor/vello_common/Cargo.toml b/vendor/vello_common/Cargo.toml new file mode 100644 index 000000000..988ffb939 --- /dev/null +++ b/vendor/vello_common/Cargo.toml @@ -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" diff --git a/vendor/vello_common/Cargo.toml.orig b/vendor/vello_common/Cargo.toml.orig new file mode 100644 index 000000000..fa356c16d --- /dev/null +++ b/vendor/vello_common/Cargo.toml.orig @@ -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 diff --git a/vendor/vello_common/LICENSE-APACHE b/vendor/vello_common/LICENSE-APACHE new file mode 100644 index 000000000..d9a10c0d8 --- /dev/null +++ b/vendor/vello_common/LICENSE-APACHE @@ -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 diff --git a/vendor/vello_common/LICENSE-MIT b/vendor/vello_common/LICENSE-MIT new file mode 100644 index 000000000..48ff8e414 --- /dev/null +++ b/vendor/vello_common/LICENSE-MIT @@ -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. diff --git a/vendor/vello_common/README.md b/vendor/vello_common/README.md new file mode 100644 index 000000000..27fa3f770 --- /dev/null +++ b/vendor/vello_common/README.md @@ -0,0 +1,100 @@ +
+ +# Vello Common + +**Shared data structures** + +[![Latest published version.](https://img.shields.io/crates/v/vello_common.svg)](https://crates.io/crates/vello_common) +[![Documentation build status.](https://img.shields.io/docsrs/vello_common.svg)](https://docs.rs/vello_common) +[![Apache 2.0 or MIT license.](https://img.shields.io/badge/license-Apache--2.0_OR_MIT-blue.svg)](#license) +\ +[![Linebender Zulip chat.](https://img.shields.io/badge/Linebender-%23vello-blue?logo=Zulip)](https://xi.zulipchat.com/#narrow/channel/197075-vello) +[![GitHub Actions CI status.](https://img.shields.io/github/actions/workflow/status/linebender/vello/ci.yml?logo=github&label=CI)](https://github.com/linebender/vello/actions) +[![Dependency staleness status.](https://deps.rs/crate/vello_common/latest/status.svg)](https://deps.rs/crate/vello_common) + +
+ + + + + +[libm]: https://crates.io/crates/libm +[crate::pixmap::Pixmap]: https://docs.rs/vello_common/latest/vello_common/pixmap/struct.Pixmap.html + + +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 + + + +## 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. + +
+Click here if compiling fails. + +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 +``` + +
+ +## 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 ) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or ) + +at your option. + +[Rust code of conduct]: https://www.rust-lang.org/policies/code-of-conduct diff --git a/vendor/vello_common/assets/README.md b/vendor/vello_common/assets/README.md new file mode 100644 index 000000000..f7fc3bace --- /dev/null +++ b/vendor/vello_common/assets/README.md @@ -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`. diff --git a/vendor/vello_common/assets/probe.png b/vendor/vello_common/assets/probe.png new file mode 100644 index 000000000..8464880a0 Binary files /dev/null and b/vendor/vello_common/assets/probe.png differ diff --git a/vendor/vello_common/assets/probe.rgba b/vendor/vello_common/assets/probe.rgba new file mode 100644 index 000000000..35238209a Binary files /dev/null and b/vendor/vello_common/assets/probe.rgba differ diff --git a/vendor/vello_common/src/blurred_rounded_rect.rs b/vendor/vello_common/src/blurred_rounded_rect.rs new file mode 100644 index 000000000..13f62e33f --- /dev/null +++ b/vendor/vello_common/src/blurred_rounded_rect.rs @@ -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, + /// The radius of the rounded rectangle's corners. + pub radius: f32, + /// The standard deviation of the blur effect. + pub std_dev: f32, +} diff --git a/vendor/vello_common/src/clip.rs b/vendor/vello_common/src/clip.rs new file mode 100644 index 000000000..c50a0ff9d --- /dev/null +++ b/vendor/vello_common/src/clip.rs @@ -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, +} + +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> { + 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, + ) { + 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 = 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( + 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 { + 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 { + // 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, strips: &mut Vec, 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, 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, + 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>, 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>, 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 + } + } +} diff --git a/vendor/vello_common/src/coarse.rs b/vendor/vello_common/src/coarse.rs new file mode 100644 index 000000000..14f9b3a9a --- /dev/null +++ b/vendor/vello_common/src/coarse.rs @@ -0,0 +1,2385 @@ +// Copyright 2025 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Generating and processing wide tiles. + +use crate::color::palette::css::TRANSPARENT; +use crate::encode::EncodedPaint; +use crate::filter_effects::Filter; +use crate::geometry::RectU16; +use crate::kurbo::{Affine, Rect}; +use crate::mask::Mask; +use crate::paint::{Paint, PremulColor}; +use crate::peniko::{BlendMode, Compose, Mix}; +use crate::render_graph::{DependencyKind, LayerId, RenderGraph, RenderNodeKind}; +use crate::{strip::Strip, tile::Tile}; +use alloc::vec; +use alloc::{boxed::Box, vec::Vec}; +#[cfg(debug_assertions)] +use alloc::{format, string::String}; +use core::ops::Range; +use hashbrown::HashMap; +#[cfg(not(feature = "std"))] +use peniko::kurbo::common::FloatFuncs as _; + +#[derive(Debug)] +struct Layer { + /// The layer's ID. + layer_id: LayerId, + /// Whether the layer has a clip associated with it. + clip: bool, + /// The blend mode with which this layer should be blended into + /// the previous layer. + blend_mode: BlendMode, + /// An opacity to apply to the whole layer before blending it + /// into the backdrop. + opacity: f32, + /// A mask to apply to the layer before blending it back into + /// the backdrop. + mask: Option, + /// A filter effect to apply to the layer before other operations. + filter: Option, + /// Bounding box of wide tiles containing geometry. + /// Starts with inverted bounds, shrinks to actual content during drawing. + wtile_bbox: WideTilesBbox, +} + +impl Layer { + /// Whether the layer actually requires allocating a new scratch buffer + /// for drawing its contents. + fn needs_buf(&self) -> bool { + self.blend_mode.mix != Mix::Normal + || self.blend_mode.compose != Compose::SrcOver + || self.opacity != 1.0 + || self.mask.is_some() + || self.filter.is_some() + || !self.clip + } +} + +/// `MODE_CPU` allows compile time optimizations to be applied to wide tile draw command generation +/// specific to `vello_cpu`. +pub const MODE_CPU: u8 = 0; +/// `MODE_HYBRID` allows compile time optimizations to be applied to wide tile draw command +/// generation specific for `vello_hybrid`. +pub const MODE_HYBRID: u8 = 1; + +/// The index that in `push_buf_indices` is reserved for indicating the target is the final +/// destination surface. +const TARGET_SURFACE_PUSH_BUF_IDX: usize = usize::MAX; + +#[derive(Debug)] +struct LayerKindAndOccupiedTiles { + /// The kind of layer needing a scratch buffer. This is tracked to allow lazy buffer pushing to + /// know for which layer kind to push. + kind: LayerKind, + /// The indices into [`Wide::tiles`] of wide tiles that occupy this layer. These are the wide + /// tiles that have pushed a buffer and need to be popped when the layer is popped. + occupied_tiles: Vec, +} + +/// For layers that require allocating a new scratch buffer for drawing its contents, this stores +/// the layer kind and the occupied wide tiles. +/// +/// The wide tile push buffer command [`WideTile::push_buf`] is performed lazily upon actually +/// being drawn into. +/// +/// See [`LayerKindAndOccupiedTiles`]. +/// +/// This is a small wrapper around [`Vec`] so we can keep the +/// [`LayerKindAndOccupiedTiles::occupied_tiles`] allocations around. +#[derive(Debug, Default)] +struct NeedsBufLayerStack { + stack: Vec, + /// The actual length of [`Self::stack`], as we never actually shrink it (see the note above + /// about keeping allocations around). + len: usize, +} + +impl NeedsBufLayerStack { + #[inline] + fn clear(&mut self) { + self.len = 0; + } + + #[inline] + fn push(&mut self, kind: LayerKind) { + if self.len == self.stack.len() { + self.stack.push(LayerKindAndOccupiedTiles { + kind, + occupied_tiles: vec![], + }); + } else { + self.stack[self.len].occupied_tiles.clear(); + self.stack[self.len].kind = kind; + } + self.len += 1; + } + + #[inline] + fn pop(&mut self) { + debug_assert!(self.len > 0, "Called `pop` at the root"); + self.len -= 1; + } + + #[inline] + fn last(&self) -> Option<&LayerKindAndOccupiedTiles> { + self.len.checked_sub(1).map(|idx| &self.stack[idx]) + } +} + +/// A container for wide tiles. +#[derive(Debug)] +pub struct Wide { + /// The width of the container. + width: u16, + /// The height of the container. + height: u16, + /// The wide tiles in the container. + tiles: Vec>, + /// Shared command properties, referenced by index from fill and clip commands. + pub attrs: CommandAttrs, + /// The stack of layers. + layer_stack: Vec, + /// The stack of layer kinds and occupied tiles for layers that require buffers. + /// + /// This contains exactly one entry for each layer with [`Layer::needs_buf`] in + /// [`Self::layer_stack`]. + layers_needing_buf_stack: NeedsBufLayerStack, + /// The stack of active clip regions. + clip_stack: Vec, + /// Stack of filter layer node IDs for render graph dependency tracking. + /// Initialized with node 0 (the root node representing the final output). + /// As layers with filters are pushed, their node IDs are added to this stack. + filter_node_stack: Vec, + /// Count of nested filtered layers with clip paths. + /// When > 0, command generation uses full viewport bounds instead of clip bounds + /// to ensure filter effects can process the full layer before applying the clip. + clipped_filter_layer_depth: u32, + /// Global batch counter, incremented each time we transition from fast strip rendering + /// to a set of strips passing through coarse rasterization. + batch_count: u32, + /// Whether to enable the optimization that allows fills with an opaque color to clear + /// all previous commands if it spans the whole wide tile. + /// + /// This needs to be disabled for the interleaved rendering path in `vello_hybrid`, because + /// the background is applied as the very first operation, and we have no way of clearing + /// strips in the fast path that would affect the area of the wide tile. + enable_bg_optimization: bool, + /// Whether at least one of the wide tiles has been mutated and thus they + /// need to be reset. + tiles_dirty: bool, +} + +/// A clip region. +#[derive(Debug)] +struct Clip { + /// The intersected bounding box after clip + pub clip_bbox: WideTilesBbox, + /// The rendered path in sparse strip representation + pub strips: Box<[Strip]>, + /// The index of the thread that owns the alpha buffer. + /// Always 0 in single-threaded mode. + pub thread_idx: u8, +} + +/// An axis-aligned bounding box in wide tile coordinates. +/// +/// This is a wrapper around [`RectU16`], adding some utilities useful for converting between +/// wide-tile space and pixel-space. `(x0, y0)` is the top-left corner and `(x1, y1)` is the +/// bottom-right corner, both in wide tile units. +#[derive(Debug, Clone, Copy)] +pub struct WideTilesBbox { + /// The bounding box. + bbox: RectU16, +} + +impl WideTilesBbox { + /// A empty rectangle with all coordinates set to zero. + pub const ZERO: Self = Self { + bbox: RectU16::ZERO, + }; + + /// 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)`. + const INVERTED: Self = Self { + bbox: RectU16::INVERTED, + }; + + /// Create a new bounding box from wide tile coordinates. + pub fn new(x0: u16, y0: u16, x1: u16, y1: u16) -> Self { + Self { + bbox: RectU16::new(x0, y0, x1, y1), + } + } + + /// Get the x0 coordinate of the bounding box. + #[inline(always)] + pub fn x0(&self) -> u16 { + self.bbox.x0 + } + + /// Get the y0 coordinate of the bounding box. + #[inline(always)] + pub fn y0(&self) -> u16 { + self.bbox.y0 + } + + /// Get the x1 coordinate of the bounding box. + #[inline(always)] + pub fn x1(&self) -> u16 { + self.bbox.x1 + } + + /// Get the y1 coordinate of the bounding box. + #[inline(always)] + pub fn y1(&self) -> u16 { + self.bbox.y1 + } + + /// Returns `true` if the bounding box has zero area. + #[inline(always)] + pub fn is_empty(&self) -> bool { + self.bbox.is_empty() + } + + /// Get the width of the bounding box in tiles (x1 - x0). + #[inline(always)] + pub fn width_tiles(&self) -> u16 { + self.bbox.width() + } + + /// Get the width of the bounding box in pixels. + #[inline(always)] + pub fn width_px(&self) -> u16 { + self.width_tiles() * WideTile::WIDTH + } + + /// Get the height of the bounding box in tiles (y1 - y0). + #[inline(always)] + pub fn height_tiles(&self) -> u16 { + self.bbox.height() + } + + /// Get the height of the bounding box in pixels. + #[inline(always)] + pub fn height_px(&self) -> u16 { + self.height_tiles() * Tile::HEIGHT + } + + /// Check if a point (x, y) is contained within this bounding box. + /// + /// Returns `true` if x0 <= x < x1 and y0 <= y < y1. + #[inline(always)] + pub fn contains(&self, x: u16, y: u16) -> bool { + self.bbox.contains(x, y) + } + + /// Calculate the intersection of this bounding box with another. + #[inline(always)] + pub(crate) fn intersect(self, other: Self) -> Self { + Self { + bbox: self.bbox.intersect(other.bbox), + } + } + + /// Update this bounding box to include another bounding box (union in place). + #[inline(always)] + pub(crate) fn union(&mut self, other: Self) { + self.bbox.union(other.bbox); + } + + /// Update the bbox to include the given tile coordinates. + #[inline(always)] + pub(crate) fn include_tile(&mut self, wtile_x: u16, wtile_y: u16) { + self.bbox.x0 = self.bbox.x0.min(wtile_x); + self.bbox.y0 = self.bbox.y0.min(wtile_y); + self.bbox.x1 = self.bbox.x1.max(wtile_x + 1); + self.bbox.y1 = self.bbox.y1.max(wtile_y + 1); + } + + /// Scale this bounding box by the given scale factors. + /// + /// Multiplies each coordinate by the corresponding scale factor to convert + /// from one coordinate system to another. + #[inline(always)] + pub fn scale(&self, scale_x: u16, scale_y: u16) -> [u32; 4] { + [ + u32::from(self.x0()) * u32::from(scale_x), + u32::from(self.y0()) * u32::from(scale_y), + u32::from(self.x1()) * u32::from(scale_x), + u32::from(self.y1()) * u32::from(scale_y), + ] + } + + /// Expands the bounding box outward by the given pixel amounts in each direction. + /// + /// Pixel values are converted to tile coordinates (rounding up) and clamped to the + /// valid range `[0, max_x)` × `[0, max_y)`. The result is a new bounding box in + /// wide tile coordinates. + pub fn expand_by_pixels(&self, expansion: Rect, max_x: u16, max_y: u16) -> Self { + // The expansion rect is centered at origin: + // - Negative coordinates (x0, y0) represent left/top expansion + // - Positive coordinates (x1, y1) represent right/bottom expansion + let left_px = (-expansion.x0).max(0.0).ceil() as u16; + let top_px = (-expansion.y0).max(0.0).ceil() as u16; + let right_px = expansion.x1.max(0.0).ceil() as u16; + let bottom_px = expansion.y1.max(0.0).ceil() as u16; + + // Convert pixel expansion to tile expansion (round up) + let left_tiles = left_px.div_ceil(WideTile::WIDTH); + let top_tiles = top_px.div_ceil(Tile::HEIGHT); + let right_tiles = right_px.div_ceil(WideTile::WIDTH); + let bottom_tiles = bottom_px.div_ceil(Tile::HEIGHT); + + Self::new( + self.x0().saturating_sub(left_tiles), + self.y0().saturating_sub(top_tiles), + (self.x1() + right_tiles).min(max_x), + (self.y1() + bottom_tiles).min(max_y), + ) + } +} + +impl Wide { + /// Create a new container for wide tiles. + pub fn new(width: u16, height: u16) -> Self { + Self::new_internal(width, height, true) + } +} + +impl Wide { + /// Create a new container for wide tiles. + pub fn new(width: u16, height: u16, enable_bg_optimization: bool) -> Self { + Self::new_internal(width, height, enable_bg_optimization) + } + + /// Record a coarse batch boundary. + /// + /// Each tile lazily emits the pending `BatchEnd` markers the next + /// time it receives a command. + #[inline(always)] + pub fn end_batch(&mut self) { + self.batch_count += 1; + } +} + +impl Wide { + /// Create a new container for wide tiles. + fn new_internal(width: u16, height: u16, enable_bg_optimization: bool) -> Self { + let width_tiles = width.div_ceil(WideTile::WIDTH); + let height_tiles = height.div_ceil(Tile::HEIGHT); + let mut tiles = Vec::with_capacity(usize::from(width_tiles) * usize::from(height_tiles)); + + for h in 0..height_tiles { + for w in 0..width_tiles { + tiles.push(WideTile::::new_internal( + w * WideTile::WIDTH, + h * Tile::HEIGHT, + )); + } + } + + Self { + tiles, + width, + height, + attrs: CommandAttrs::default(), + enable_bg_optimization, + layer_stack: vec![], + clip_stack: vec![], + // Start with root node 0. + filter_node_stack: vec![0], + clipped_filter_layer_depth: 0, + layers_needing_buf_stack: NeedsBufLayerStack::default(), + batch_count: 0, + tiles_dirty: false, + } + } + + /// Whether there are any existing layers that haven't been popped yet. + pub fn has_layers(&self) -> bool { + !self.layer_stack.is_empty() + } + + /// Reset all tiles in the container. + pub fn reset(&mut self) { + if self.tiles_dirty { + for tile in &mut self.tiles { + tile.reset(); + } + + self.tiles_dirty = false; + } + + self.attrs.clear(); + self.layer_stack.clear(); + self.layers_needing_buf_stack.clear(); + self.clip_stack.clear(); + self.filter_node_stack.truncate(1); + self.clipped_filter_layer_depth = 0; + self.batch_count = 0; + } + + /// Return the number of horizontal tiles. + pub fn width_tiles(&self) -> u16 { + self.width.div_ceil(WideTile::WIDTH) + } + + /// Return the number of vertical tiles. + pub fn height_tiles(&self) -> u16 { + self.height.div_ceil(Tile::HEIGHT) + } + + /// Get the index of the wide tile at the given coordinates. + /// + /// Panics if the coordinates are out-of-range. + #[inline] + pub fn get_idx(&self, x: u16, y: u16) -> usize { + assert!( + x < self.width_tiles() && y < self.height_tiles(), + "attempted to access out-of-bounds wide tile" + ); + + usize::from(y) * usize::from(self.width_tiles()) + usize::from(x) + } + + /// Get the index of the wide tile at the given coordinates. + /// + /// Panics if the coordinates are out-of-range. + pub fn get(&self, x: u16, y: u16) -> &WideTile { + let idx = self.get_idx(x, y); + &self.tiles[idx] + } + + /// Get mutable access to the wide tile at the given coordinates. + /// + /// Panics if the coordinates are out-of-range. + fn get_mut(&mut self, x: u16, y: u16) -> &mut WideTile { + let idx = self.get_idx(x, y); + &mut self.tiles[idx] + } + + /// Return a reference to all wide tiles. + pub fn tiles(&self) -> &[WideTile] { + self.tiles.as_slice() + } + + /// Get the current layer Id. + #[inline(always)] + pub fn get_current_layer_id(&self) -> LayerId { + self.layer_stack.last().map_or(0, |l| l.layer_id) + } + + /// Update the bounding box of the current layer to include the given tile. + /// Should be called whenever a command is generated for a tile. + #[inline] + fn update_current_layer_bbox(&mut self, wtile_x: u16, wtile_y: u16) { + if let Some(layer) = self.layer_stack.last_mut() { + layer.wtile_bbox.include_tile(wtile_x, wtile_y); + } + } + + /// Generate wide tile commands from the strip buffer. + /// + /// This method processes a buffer of strips that represent a path, applies the fill rule, + /// and generates appropriate drawing commands for each affected wide tile. + /// + /// # Algorithm overview: + /// 1. For each strip in the buffer: + /// - Calculate its position and width in pixels + /// - Determine which wide tiles the strip intersects + /// - Generate alpha fill commands for the intersected wide tiles + /// 2. For active fill regions (determined by fill rule): + /// - Generate solid fill commands for the regions between strips + pub fn generate( + &mut self, + strip_buf: &[Strip], + paint: Paint, + blend_mode: BlendMode, + thread_idx: u8, + mask: Option, + encoded_paints: &[EncodedPaint], + ) { + if strip_buf.is_empty() { + return; + } + + self.tiles_dirty = true; + let alpha_base_idx = strip_buf[0].alpha_idx(); + + // Create shared attributes for all commands from this path + let attrs_idx = self.attrs.fill.len() as u32; + self.attrs.fill.push(FillAttrs { + thread_idx, + paint, + blend_mode, + mask, + alpha_base_idx, + }); + + // Get current clip bounding box or full viewport if no clip is active + let bbox = self.active_bbox(); + + // Save current_layer_id and batch_count to avoid borrowing issues + let current_layer_id = self.get_current_layer_id(); + let batch_count = self.batch_count; + + for i in 0..strip_buf.len() - 1 { + let strip = &strip_buf[i]; + + debug_assert!( + strip.y < self.height, + "Strips below the viewport should have been culled prior to this stage." + ); + + // Don't render strips that are outside the viewport width + if strip.x >= self.width { + continue; + } + + let next_strip = &strip_buf[i + 1]; + let x0 = strip.x; + let strip_y = strip.strip_y(); + + // Skip strips outside the current clip bounding box + if strip_y < bbox.y0() { + continue; + } + if strip_y >= bbox.y1() { + // The rest of our strips must be outside the clip, so we can break early. + break; + } + + // Calculate the width of the strip in columns + let mut col = strip.alpha_idx() / u32::from(Tile::HEIGHT); + let next_col = next_strip.alpha_idx() / u32::from(Tile::HEIGHT); + // Can potentially be 0 if strip only changes winding without covering pixels + let strip_width = next_col.saturating_sub(col) as u16; + let x1 = x0.saturating_add(strip_width); + + // Calculate which wide tiles this strip intersects + let wtile_x0 = (x0 / WideTile::WIDTH).max(bbox.x0()); + // It's possible that a strip extends into a new wide tile, but we don't actually + // have as many wide tiles (e.g. because the pixmap width is only 512, but + // strip ends at 513), so take the minimum between the rounded values and `width_tiles`. + let wtile_x1 = x1 + .div_ceil(WideTile::WIDTH) + .min(bbox.x1()) + .min(WideTile::MAX_WIDE_TILE_COORD); + + // Adjust column starting position if needed to respect clip boundaries + let mut x = x0; + let clip_x = bbox.x0() * WideTile::WIDTH; + if clip_x > x { + col += u32::from(clip_x - x); + x = clip_x; + } + + // Generate alpha fill commands for each wide tile intersected by this strip + for wtile_x in wtile_x0..wtile_x1 { + let x_wtile_rel = x % WideTile::WIDTH; + // Restrict the width of the fill to the width of the wide tile + let width = x1.min((wtile_x + 1) * WideTile::WIDTH) - x; + let cmd = CmdAlphaFill { + x: x_wtile_rel, + width, + alpha_offset: col * u32::from(Tile::HEIGHT) - alpha_base_idx, + attrs_idx, + }; + x += width; + col += u32::from(width); + let idx = self.get_idx(wtile_x, strip_y); + self.tiles[idx].strip( + batch_count, + idx, + &mut self.layers_needing_buf_stack, + cmd, + current_layer_id, + ); + self.update_current_layer_bbox(wtile_x, strip_y); + } + + // Determine if the region between this strip and the next should be filled. + let active_fill = next_strip.fill_gap(); + + // If region should be filled and both strips are on the same row, + // generate fill commands for the region between them + if active_fill && strip_y == next_strip.strip_y() { + // Clamp the fill to the clip bounding box + x = x1.max(bbox.x0() * WideTile::WIDTH); + let x2 = next_strip.x.min( + self.width + .checked_next_multiple_of(WideTile::WIDTH) + .unwrap_or(u16::MAX), + ); + let wfxt0 = (x1 / WideTile::WIDTH).max(bbox.x0()); + let wfxt1 = x2 + .div_ceil(WideTile::WIDTH) + .min(bbox.x1()) + .min(WideTile::MAX_WIDE_TILE_COORD); + + // Compute fill hint based on paint type + let fill_attrs = &self.attrs.fill[attrs_idx as usize]; + let fill_hint = if fill_attrs.mask.is_none() && self.enable_bg_optimization { + match &fill_attrs.paint { + Paint::Solid(s) if s.is_opaque() => FillHint::OpaqueSolid(*s), + Paint::Indexed(idx) => match encoded_paints.get(idx.index()) { + Some(EncodedPaint::Image(img)) + if !img.may_have_transparency + && img.sampler.alpha == 1.0 + && img.tint.is_none_or(|t| t.color.components[3] >= 1.0) => + { + FillHint::OpaqueImage + } + Some(EncodedPaint::ExternalTexture(img)) + if !img.may_have_transparency + && img.sampler.alpha == 1.0 + && img.tint.is_none_or(|t| t.color.components[3] >= 1.0) => + { + FillHint::OpaqueImage + } + _ => FillHint::None, + }, + _ => FillHint::None, + } + } else { + FillHint::None + }; + + // Generate fill commands for each wide tile in the fill region + for wtile_x in wfxt0..wfxt1 { + let x_wtile_rel = x % WideTile::WIDTH; + let width = x2.min( + (wtile_x + .checked_add(1) + .unwrap_or(WideTile::MAX_WIDE_TILE_COORD)) + * WideTile::WIDTH, + ) - x; + x += width; + let idx = self.get_idx(wtile_x, strip_y); + self.tiles[idx].fill( + batch_count, + idx, + &mut self.layers_needing_buf_stack, + x_wtile_rel, + width, + attrs_idx, + current_layer_id, + fill_hint, + ); + // TODO: This bbox update might be redundant since filled regions are always + // bounded by strip regions (which already update the bbox). Consider removing + // this in a follow-up with proper benchmarks to verify correctness. + self.update_current_layer_bbox(wtile_x, strip_y); + } + } + } + } + + /// Push a new layer with the given properties. + /// + /// Rendering will be directed to the layer storage identified by `layer_id`. + /// This is used for filter effects that require access to a fully-rendered layer. + /// + /// If `filter` is Some, builds render graph nodes for filter effects. + pub fn push_layer( + &mut self, + layer_id: LayerId, + clip_path: Option>>, + blend_mode: BlendMode, + mask: Option, + opacity: f32, + filter: Option, + transform: Affine, + render_graph: &mut RenderGraph, + thread_idx: u8, + ) { + self.tiles_dirty = true; + + // Some explanations about what is going on here: We support the concept of + // layers, where a user can push a new layer (with certain properties), draw some + // stuff, and finally pop the layer, as part of which the layer as a whole will be + // blended into the previous layer. + // There are 3 "straightforward" properties that can be set for each layer: + // 1) The blend mode that should be used to blend the layer into the backdrop. + // 2) A mask that will be applied to the whole layer in the very end before blending. + // 3) An optional opacity that will be applied to the whole layer before blending (this + // could in theory be simulated with an alpha mask, but since it's a common operation and + // we only have a single opacity, this can easily be optimized. + // + // Finally, you can also add a clip path to the layer. However, clipping has its own + // more complicated logic for pushing/popping buffers where drawing is also suppressed + // in clipped-out wide tiles. Because of this, in case we have one of the above properties + // AND a clipping path, we will actually end up pushing two buffers, the first one handles + // the three properties and the second one is just for clip paths. That is a bit wasteful + // and I believe it should be possible to process them all in just one go, but for now + // this is good enough, and it allows us to implement blending without too deep changes to + // the original clipping implementation. + + // Build render graph node ONLY if we have a filter. + // The render graph tracks dependencies and execution order for filter effects. + if let Some(filter) = &filter { + // Create a FilterLayer node that combines render + filter + other operations + let child_node = render_graph.add_node(RenderNodeKind::FilterLayer { + layer_id, + filter: filter.clone(), + // Bounding box starts inverted and will be updated in pop_layer with actual bounds + wtile_bbox: WideTilesBbox::INVERTED, + transform, + }); + + // Connect to parent node if there is one + if let Some(&parent_node) = self.filter_node_stack.last() { + render_graph.add_edge( + child_node, + parent_node, + DependencyKind::Sequential { layer_id }, + ); + } + + // Push this filter node onto the stack so subsequent filters depend on it + self.filter_node_stack.push(child_node); + } + + let has_filter = filter.is_some(); + let has_clip = clip_path.is_some(); + let layer_kind = if has_filter { + LayerKind::Filtered(layer_id) + } else { + LayerKind::Regular(layer_id) + }; + + // Filtered layers with clipping require special handling: normally, tiles with + // zero-winding clips suppress all drawing. However, filters need the full layer + // content rendered (including zero-clipped areas) before applying the clip as a mask. + // When this flag is true, we generate explicit drawing commands instead of just counters. + let in_clipped_filter_layer = has_filter && has_clip; + + // Increment the depth counter so that active_bbox() returns the full viewport + // instead of the clipped bbox. This ensures command generation covers all tiles, + // allowing the filter to process the entire layer before the clip is applied. + if in_clipped_filter_layer { + self.clipped_filter_layer_depth += 1; + } + + let layer = Layer { + layer_id, + clip: has_clip, + blend_mode, + opacity, + mask, + filter, + wtile_bbox: WideTilesBbox::INVERTED, + }; + + // In case we do blending, masking, opacity, or filtering, push one buffer per wide tile. + // + // Layers require buffers for different reasons: + // - Blending, opacity, and masking: Need to composite results with the backdrop + // - Filtering: Content must be rendered to a buffer before applying filter effects + // + // The layer_kind parameter distinguishes how buffers are managed: + // - Regular layers use the local blend_buf stack + // - Filtered layers are materialized in persistent layer storage for filter processing + // - Clip layers have special handling for clipping operations + if layer.needs_buf() { + let batch_count = self.batch_count; + + self.layers_needing_buf_stack.push(layer_kind); + + // We eagerly push buffers for the current active bbox if this is a destructive blend + // or the entire viewport if we are (nested in) a clipped filter layer. + // + // TODO: We may be able to do away with some or all of this eager pushing in the + // future, but for now these types of layers need buffers for all wide tiles regardless + // of whether they're drawn on. + if layer.blend_mode.is_destructive() || self.clipped_filter_layer_depth > 0 { + let active_bbox = if self.clipped_filter_layer_depth > 0 { + self.full_viewport_bbox() + } else { + self.active_bbox() + }; + + for y in active_bbox.y0()..active_bbox.y1() { + for x in active_bbox.x0()..active_bbox.x1() { + let idx = self.get_idx(x, y); + self.tiles[idx].ensure_layer_stack_bufs( + idx, + &mut self.layers_needing_buf_stack, + batch_count, + ); + // Mark tiles that are in a clipped filter layer so they generate + // explicit clip commands for proper filter processing. + self.tiles[idx].in_clipped_filter_layer = in_clipped_filter_layer; + } + } + } + } + + // If we have a clip path, push another buffer in the affected wide tiles. + // Note that it is important that we FIRST push the buffer for blending etc. and + // only then for clipping, otherwise we will use the empty clip buffer as the backdrop + // for blending! + if let Some(clip) = clip_path { + self.push_clip(clip, layer_id, thread_idx); + } + + self.layer_stack.push(layer); + } + + /// Pop a previously pushed layer. + /// + /// This method finalizes the layer by: + /// - Expanding the bounding box if filter effects are present + /// - Updating the parent layer's bounding box to include this layer's bounds + /// - Completing render graph nodes for filter effects + /// - Generating filter commands for each tile + /// - Popping any associated clip + /// - Applying mask, opacity, and blend mode operations if needed + pub fn pop_layer(&mut self, render_graph: &mut RenderGraph) { + self.tiles_dirty = true; + + // This method basically unwinds everything we did in `push_layer`. + let mut layer = self.layer_stack.pop().unwrap(); + let batch_count = self.batch_count; + + if let Some(filter) = &layer.filter { + let mut final_bbox = WideTilesBbox::INVERTED; + + // Update render graph node with final bounding box + if let Some(node_id) = self.filter_node_stack.pop() { + // Get the transform from the FilterLayer node and scale the expansion by it + if let Some(node) = render_graph.nodes.get_mut(node_id) + && let RenderNodeKind::FilterLayer { + wtile_bbox, + transform, + .. + } = &mut node.kind + { + // Calculate expansion in device/pixel space, accounting for the full transform. + // This ensures that rotated filters (e.g., drop shadows) have correct bounds. + let expansion = filter.bounds_expansion(transform); + let expanded_bbox = layer.wtile_bbox.expand_by_pixels( + expansion, + self.width_tiles(), + self.height_tiles(), + ); + let clip_bbox = self.active_bbox(); + final_bbox = expanded_bbox.intersect(clip_bbox); + + // Update both the local layer and the render graph node + layer.wtile_bbox = final_bbox; + *wtile_bbox = final_bbox; + } + // Record this node in execution order (children before parents) + render_graph.record_node_for_execution(node_id); + } + + // Generate filter commands for each tile (used for non-graph path rendering) + // Apply filter BEFORE clipping (per SVG spec: filter → clip → mask → opacity → blend) + // Also ensure that each wide tile in the filter bbox (out of which some might not + // have been drawn on) has a `PushBuf` command. + for x in final_bbox.x0()..final_bbox.x1() { + for y in final_bbox.y0()..final_bbox.y1() { + let idx = self.get_idx(x, y); + self.tiles[idx].ensure_layer_stack_bufs( + idx, + &mut self.layers_needing_buf_stack, + batch_count, + ); + + // Note that unlike commands like "mask" or "opacity" which only need to be applied + // to wide tiles with drawing commands, filter commands always need to be applied + // to all wide tiles in the filter bounding-box, because it's possible that after + // applying a filter (like gaussian blur), wide tiles will take on a new color + // even though nothing had been drawn there originally. + self.tiles[idx].filter(layer.layer_id, filter.clone()); + } + } + } + + // Union this layer's bbox into the parent layer's bbox. + // This ensures the parent knows about all tiles used by this child layer, + // which is important for filter effects that may expand beyond the original content bounds. + if let Some(parent_layer) = self.layer_stack.last_mut() { + parent_layer.wtile_bbox.union(layer.wtile_bbox); + } + + if layer.clip { + self.pop_clip(); + } + + if layer.needs_buf() { + for &tile_idx in self + .layers_needing_buf_stack + .last() + .unwrap() + .occupied_tiles + .iter() + { + let tile = &mut self.tiles[tile_idx]; + + // Optimization: If no drawing happened since the last `PushBuf`, then we don't + // need to do any masking or buffer-wide opacity work. Even though we push buffers + // lazily, this can still happen: e.g., filter layers with clips and destructive blends + // currently push layers eagerly. + let has_draw_commands = !matches!(tile.cmds.last().unwrap(), &Cmd::PushBuf(..)); + if has_draw_commands { + if let Some(mask) = layer.mask.clone() { + tile.mask(mask); + } + tile.opacity(layer.opacity); + } + // We only need to blend if there are draw commands, unless this is destructive + // blending, in which case we always blend. + if has_draw_commands || layer.blend_mode.is_destructive() { + tile.blend(layer.blend_mode); + } + tile.pop_buf(); + } + self.layers_needing_buf_stack.pop(); + } + + let in_clipped_filter_layer = layer.filter.is_some() && layer.clip; + // Decrement the depth counter after popping a filtered layer with clip + if in_clipped_filter_layer { + self.clipped_filter_layer_depth -= 1; + } + } + + /// Adds a clipping region defined by the provided strips. + /// + /// This method takes a vector of strips representing a clip path, calculates the + /// intersection with the current clip region, and updates the clip stack. + /// + /// # Algorithm overview: + /// 1. Calculate bounding box of the clip path + /// 2. Intersect with current clip bounding box + /// 3. For each tile in the intersected bounding box: + /// - If covered by zero winding: `push_zero_clip` + /// - If fully covered by non-zero winding: do nothing (clip is a no-op) + /// - If partially covered: `push_clip` + fn push_clip(&mut self, strips: impl Into>, layer_id: LayerId, thread_idx: u8) { + let strips = strips.into(); + let n_strips = strips.len(); + + // Calculate the bounding box of the clip path in strip coordinates + let path_bbox = if n_strips <= 1 { + WideTilesBbox::ZERO + } else { + // Calculate the y range from first to last strip in wide tile coordinates + let wtile_y0 = strips[0].strip_y(); + let wtile_y1 = strips[n_strips.saturating_sub(1)].strip_y() + 1; + + // Calculate the x range by examining all strips in wide tile coordinates + let mut wtile_x0 = strips[0].x / WideTile::WIDTH; + let mut wtile_x1 = wtile_x0; + for i in 0..n_strips.saturating_sub(1) { + let strip = &strips[i]; + let next_strip = &strips[i + 1]; + let width = + ((next_strip.alpha_idx() - strip.alpha_idx()) / u32::from(Tile::HEIGHT)) as u16; + let x = strip.x; + wtile_x0 = wtile_x0.min(x / WideTile::WIDTH); + wtile_x1 = wtile_x1.max((x + width).div_ceil(WideTile::WIDTH)); + } + WideTilesBbox::new(wtile_x0, wtile_y0, wtile_x1, wtile_y1) + }; + + let parent_bbox = self.active_bbox(); + // Determine which tiles need clip processing: + // - For clipped filter layers: active_bbox() returns the full viewport, so parent_bbox + // already covers all tiles. We need to process all of them because the filter needs + // the entire layer rendered, and tiles outside the clip path must get `PushZeroClip` + // commands to properly suppress their content after filtering. + // - For normal clips: Intersect with the path bounds to only process tiles that are + // actually affected by the clip path, avoiding unnecessary work. + let clip_bbox = if self.clipped_filter_layer_depth > 0 { + // Use parent_bbox as-is (full viewport) to process all tiles + parent_bbox + } else { + // Optimize by processing only the intersection of parent and path bounds + parent_bbox.intersect(path_bbox) + }; + + let mut cur_wtile_x = clip_bbox.x0(); + let mut cur_wtile_y = clip_bbox.y0(); + let batch_count = self.batch_count; + + // Process strips to determine the clipping state for each wide tile + for i in 0..n_strips.saturating_sub(1) { + let strip = &strips[i]; + let strip_y = strip.strip_y(); + + // Skip strips before current wide tile row + if strip_y < cur_wtile_y { + continue; + } + + // Process wide tiles in rows before this strip's row + // These wide tiles are all zero-winding (outside the path) + while cur_wtile_y < strip_y.min(clip_bbox.y1()) { + for wtile_x in cur_wtile_x..clip_bbox.x1() { + self.get_mut(wtile_x, cur_wtile_y).push_zero_clip(layer_id); + } + // Reset x to the left edge of the clip bounding box + cur_wtile_x = clip_bbox.x0(); + // Move to the next row + cur_wtile_y += 1; + } + + // If we've reached the bottom of the clip bounding box, stop processing. + // Note that we are explicitly checking >= instead of ==, so that we abort if the clipping box + // is zero-area (see issue 1072). + if cur_wtile_y >= clip_bbox.y1() { + break; + } + + // Process wide tiles to the left of this strip in the same row + let x = strip.x; + let wtile_x_clamped = (x / WideTile::WIDTH).min(clip_bbox.x1()); + if cur_wtile_x < wtile_x_clamped { + // If winding is zero or doesn't match fill rule, these wide tiles are outside the path + let is_inside = strip.fill_gap(); + if !is_inside { + for wtile_x in cur_wtile_x..wtile_x_clamped { + self.get_mut(wtile_x, cur_wtile_y).push_zero_clip(layer_id); + } + } + // If winding is nonzero, then wide tiles covered entirely + // by sparse fill are no-op (no clipping is applied). + cur_wtile_x = wtile_x_clamped; + } + + // Process wide tiles covered by the strip - these need actual clipping + let next_strip = &strips[i + 1]; + let width = + ((next_strip.alpha_idx() - strip.alpha_idx()) / u32::from(Tile::HEIGHT)) as u16; + let wtile_x1 = (x + width).div_ceil(WideTile::WIDTH).min(clip_bbox.x1()); + if cur_wtile_x < wtile_x1 { + for wtile_x in cur_wtile_x..wtile_x1 { + let idx = self.get_idx(wtile_x, cur_wtile_y); + self.tiles[idx].push_clip( + idx, + &mut self.layers_needing_buf_stack, + layer_id, + batch_count, + ); + } + cur_wtile_x = wtile_x1; + } + } + + // Process any remaining wide tiles in the bounding box (all zero-winding) + while cur_wtile_y < clip_bbox.y1() { + for wtile_x in cur_wtile_x..clip_bbox.x1() { + self.get_mut(wtile_x, cur_wtile_y).push_zero_clip(layer_id); + } + cur_wtile_x = clip_bbox.x0(); + cur_wtile_y += 1; + } + + self.clip_stack.push(Clip { + clip_bbox, + strips, + thread_idx, + }); + } + + /// Get the bounding box of the current clip region or the entire viewport if no clip regions are active. + fn active_bbox(&self) -> WideTilesBbox { + // When in a clipped filter layer, use full viewport to allow + // filter to process the complete layer before applying clip as mask + if self.clipped_filter_layer_depth > 0 { + return self.full_viewport_bbox(); + } + + self.clip_stack + .last() + .map(|top| top.clip_bbox) + .unwrap_or_else(|| self.full_viewport_bbox()) + } + + /// Returns the bounding box covering the entire viewport in wide tile coordinates. + fn full_viewport_bbox(&self) -> WideTilesBbox { + WideTilesBbox::new(0, 0, self.width_tiles(), self.height_tiles()) + } + + /// Removes the most recently added clip region. + /// + /// This is the inverse operation of `push_clip`, carefully undoing all the clipping + /// operations while also handling any rendering needed for the clip region itself. + /// + /// # Algorithm overview: + /// 1. Retrieve the top clip from the stack + /// 2. For each wide tile in the clip's bounding box: + /// - If covered by zero winding: `pop_zero_clip` + /// - If fully covered by non-zero winding: do nothing (was no-op) + /// - If partially covered: render the clip and `pop_clip` + /// + /// This operation must be symmetric with `push_clip` to maintain a balanced clip stack. + fn pop_clip(&mut self) { + let Clip { + clip_bbox, + strips, + thread_idx, + } = self.clip_stack.pop().unwrap(); + let n_strips = strips.len(); + + // Compute base alpha index and create shared clip attributes + // Note: It's possible that the clip-path has zero strips. However, we cannot exit early + // in this case because we need to potentially pop zero clips further below. Therefore, + // we simply use a dummy alpha index of 0 in this case. + let clip_attrs_idx = self.attrs.clip.len() as u32; + let alpha_base_idx; + if n_strips == 0 { + alpha_base_idx = 0; + } else { + alpha_base_idx = strips[0].alpha_idx(); + self.attrs.clip.push(ClipAttrs { + thread_idx, + alpha_base_idx, + }); + }; + + let mut cur_wtile_x = clip_bbox.x0(); + let mut cur_wtile_y = clip_bbox.y0(); + let mut pop_pending = false; + + // Process each strip to determine the clipping state for each tile + for i in 0..n_strips.saturating_sub(1) { + let strip = &strips[i]; + let strip_y = strip.strip_y(); + + // Skip strips before current tile row + if strip_y < cur_wtile_y { + continue; + } + + // Process tiles in rows before this strip's row + // These tiles all had zero-winding clips + while cur_wtile_y < strip_y.min(clip_bbox.y1()) { + // Handle any pending clip pop from previous iteration + if core::mem::take(&mut pop_pending) { + self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip(); + cur_wtile_x += 1; + } + + // Pop zero clips for all remaining tiles in this row + for wtile_x in cur_wtile_x..clip_bbox.x1() { + self.get_mut(wtile_x, cur_wtile_y).pop_zero_clip(); + } + cur_wtile_x = clip_bbox.x0(); + cur_wtile_y += 1; + } + + // If we've reached the bottom of the clip bounding box, stop processing + // Note that we are explicitly checking >= instead of ==, so that we abort if the clipping box + // is zero-area (see issue 1072). + if cur_wtile_y >= clip_bbox.y1() { + break; + } + + // Process tiles to the left of this strip in the same row + let x0 = strip.x; + let wtile_x_clamped = (x0 / WideTile::WIDTH).min(clip_bbox.x1()); + if cur_wtile_x < wtile_x_clamped { + // Handle any pending clip pop from previous iteration + if core::mem::take(&mut pop_pending) { + self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip(); + cur_wtile_x += 1; + } + + // Pop zero clips for tiles that had zero winding or didn't match fill rule + // TODO: The winding check is probably not needed; if there was a fill, + // the logic below should have advanced wtile_x. + let is_inside = strip.fill_gap(); + if !is_inside { + for wtile_x in cur_wtile_x..wtile_x_clamped { + self.get_mut(wtile_x, cur_wtile_y).pop_zero_clip(); + } + } + cur_wtile_x = wtile_x_clamped; + } + + // Process tiles covered by the strip - render clip content and pop + let next_strip = &strips[i + 1]; + let strip_width = + ((next_strip.alpha_idx() - strip.alpha_idx()) / u32::from(Tile::HEIGHT)) as u16; + let mut clipped_x1 = x0 + strip_width; + let wtile_x0 = (x0 / WideTile::WIDTH).max(clip_bbox.x0()); + let wtile_x1 = clipped_x1.div_ceil(WideTile::WIDTH).min(clip_bbox.x1()); + + // Calculate starting position and column for alpha mask + let mut x = x0; + let mut col = strip.alpha_idx() / u32::from(Tile::HEIGHT); + let clip_x = clip_bbox.x0() * WideTile::WIDTH; + if clip_x > x { + col += u32::from(clip_x - x); + x = clip_x; + clipped_x1 = clip_x.max(clipped_x1); + } + + // Render clip strips for each affected tile and mark for popping + for wtile_x in wtile_x0..wtile_x1 { + // If we've moved past tile_x and have a pending pop, do it now + if cur_wtile_x < wtile_x && core::mem::take(&mut pop_pending) { + self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip(); + } + + // Calculate the portion of the strip that affects this tile + let x_rel = x % WideTile::WIDTH; + let width = clipped_x1.min((wtile_x + 1) * WideTile::WIDTH) - x; + + // Create clip strip command for rendering the partial coverage + let cmd = CmdClipAlphaFill { + x: x_rel, + width, + alpha_offset: col * u32::from(Tile::HEIGHT) - alpha_base_idx, + attrs_idx: clip_attrs_idx, + }; + x += width; + col += u32::from(width); + + // Apply the clip strip command and update state + self.get_mut(wtile_x, cur_wtile_y).clip_strip(cmd); + cur_wtile_x = wtile_x; + + // Only request a pop if the x coordinate is actually inside the bounds. + if cur_wtile_x < clip_bbox.x1() { + pop_pending = true; + } + } + + // Handle fill regions between strips based on fill rule + let is_inside = next_strip.fill_gap(); + if is_inside && strip_y == next_strip.strip_y() { + if cur_wtile_x >= clip_bbox.x1() { + continue; + } + + let x2 = next_strip.x; + let clipped_x2 = x2.min((cur_wtile_x + 1) * WideTile::WIDTH); + let width = clipped_x2.saturating_sub(clipped_x1); + + // If there's a gap, fill it. Only do this if the fill wouldn't cover the + // whole tile, as such clips are skipped by the `push_clip` function. See + // + if width > 0 && width < WideTile::WIDTH { + let x_rel = clipped_x1 % WideTile::WIDTH; + self.get_mut(cur_wtile_x, cur_wtile_y) + .clip_fill(x_rel, width); + } + + // If the next strip is a sentinel, skip the fill + // It's a sentinel in the row if there is non-zero winding for the sparse fill + // Look more into this in the strip.rs render function + if x2 == u16::MAX { + continue; + } + + // If fill extends to next tile, pop current and handle next + if x2 > (cur_wtile_x + 1) * WideTile::WIDTH { + if core::mem::take(&mut pop_pending) { + self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip(); + } + + let width2 = x2 % WideTile::WIDTH; + cur_wtile_x = x2 / WideTile::WIDTH; + + // If the strip is outside the clipping box, we don't need to do any + // filling, so we continue (also to prevent out-of-bounds access). + if cur_wtile_x >= clip_bbox.x1() { + continue; + } + + if width2 > 0 { + // An important thing to note: Note that we are only applying + // `clip_fill` to the wide tile that is actually covered by the next + // strip, and not the ones in-between! For example, if the first strip + // is in wide tile 1 and the second in wide tile 4, we will do a clip + // fill in wide tile 1 and 4, but not in 2 and 3. The reason for this is + // that any tile in-between is fully covered and thus no clipping is + // necessary at all. See also the `push_clip` function, where we don't + // push a new buffer for such tiles. + self.get_mut(cur_wtile_x, cur_wtile_y).clip_fill(0, width2); + } + } + } + } + + // Handle any pending clip pop from the last iteration + if core::mem::take(&mut pop_pending) { + self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip(); + cur_wtile_x += 1; + } + + // Process any remaining tiles in the bounding box (all zero-winding) + while cur_wtile_y < clip_bbox.y1() { + for wtile_x in cur_wtile_x..clip_bbox.x1() { + self.get_mut(wtile_x, cur_wtile_y).pop_zero_clip(); + } + cur_wtile_x = clip_bbox.x0(); + cur_wtile_y += 1; + } + } +} + +/// A wide tile. +#[derive(Debug)] +pub struct WideTile { + /// The x coordinate of the wide tile. + pub x: u16, + /// The y coordinate of the wide tile. + pub y: u16, + /// The background of the tile. + pub bg: PremulColor, + /// The draw commands of the tile. + pub cmds: Vec, + /// The number of zero-winding clips. + n_zero_clip: usize, + /// The number of non-zero-winding clips. + n_clip: usize, + /// The number of pushed buffers, including buffers for clips. + /// + /// Note not all layers require their own buffers; see [`Layer::needs_buf`]. + n_bufs: usize, + /// True when this tile is in a filtered layer with clipping applied. + /// When set, clip operations generate explicit commands instead of just + /// tracking counters, allowing filters to process clipped content correctly. + in_clipped_filter_layer: bool, + /// Maps layer Id to command ranges for this tile. + pub layer_cmd_ranges: HashMap, + /// Vector of layer IDs this tile participates in. + layer_ids: Vec, + /// Tracks the index into `cmds` of each `Start`/`PushBuf` command on the current stack. + /// + /// Only used in `HYBRID` mode. + push_buf_indices: Vec, + /// Whether at least one filter layer has been pushed on this tile. + had_filter_layer: bool, + /// Indicates whether the main target surface is used as a blend target for a non + /// src-over blending operation. + /// + /// This will only be set in `HYBRID` mode. + surface_is_blend_target: bool, + /// Watermark: the batch count at which this tile last emitted `BatchEnd` markers. + /// Only meaningful in `MODE_HYBRID`. + last_batch_end: u32, +} + +impl WideTile { + /// The width of a wide tile in pixels. + pub const WIDTH: u16 = 256; + /// The maximum coordinate of a wide tile. + pub const MAX_WIDE_TILE_COORD: u16 = u16::MAX / Self::WIDTH; +} + +impl WideTile { + /// Create a new wide tile. + pub fn new(width: u16, height: u16) -> Self { + Self::new_internal(width, height) + } +} + +impl WideTile { + /// Create a new wide tile. + pub fn new(width: u16, height: u16) -> Self { + Self::new_internal(width, height) + } +} + +impl WideTile { + /// Create a new wide tile. + fn new_internal(x: u16, y: u16) -> Self { + let mut layer_cmd_ranges = HashMap::new(); + layer_cmd_ranges.insert(0, LayerCommandRanges::default()); + Self { + x, + y, + bg: PremulColor::from_alpha_color(TRANSPARENT), + cmds: vec![], + n_zero_clip: 0, + n_clip: 0, + n_bufs: 0, + in_clipped_filter_layer: false, + had_filter_layer: false, + layer_cmd_ranges, + layer_ids: vec![LayerKind::Regular(0)], + push_buf_indices: vec![TARGET_SURFACE_PUSH_BUF_IDX], + surface_is_blend_target: false, + last_batch_end: 0, + } + } + + #[inline] + fn reset(&mut self) { + self.bg = PremulColor::from_alpha_color(TRANSPARENT); + self.cmds.clear(); + self.n_zero_clip = 0; + self.n_clip = 0; + self.n_bufs = 0; + self.in_clipped_filter_layer = false; + self.had_filter_layer = false; + self.layer_ids.truncate(1); + self.layer_cmd_ranges.clear(); + self.layer_cmd_ranges + .insert(0, LayerCommandRanges::default()); + self.push_buf_indices.clear(); + // We can't use 0 here, because then we have no way of distinguishing it from a + // user-supplied `PushBuf` at position 0. + self.push_buf_indices.push(TARGET_SURFACE_PUSH_BUF_IDX); + self.surface_is_blend_target = false; + self.last_batch_end = 0; + } + + /// Push all layer buffers that have not yet been pushed for this tile. + /// Also emit `BatchEnd` commands in case we are in HYBRID mode. + /// + /// We push wide tile layer buffers lazily. This is called by wide tile draw methods to ensure + /// all layer stack buffers are pushed for the wide tile. Calling this multiple times won't + /// push additional buffers beyond the first call (until the layer stack changes). + /// + /// The `tile_idx` parameter is this tile's index in [`Wide::tiles`]. + #[inline(always)] + fn ensure_layer_stack_bufs( + &mut self, + tile_idx: usize, + layers: &mut NeedsBufLayerStack, + batch_count: u32, + ) { + // First emit `BatchEnd` commands if necessary. + if MODE == MODE_HYBRID && self.last_batch_end < batch_count { + let count = (batch_count - self.last_batch_end) as usize; + self.cmds.extend(core::iter::repeat_n(Cmd::BatchEnd, count)); + self.last_batch_end = batch_count; + } + + // Then, take care of emitting `PushBuf` commands that haven't been emitted yet. + + // `layers` tracks the number of layers that require scratch buffers, excluding those + // required for clips: clip buffers are handled separately. The scratch buffer stack for + // this tile is `self.n_bufs`, of which `self.n_clip` are for clips. + let layer_bufs = self.n_bufs - self.n_clip; + debug_assert!( + layer_bufs <= layers.len, + "tile `layer_buf_depth` exceeds active layer stack" + ); + // It may be quite likely that no buffers need to be pushed: e.g. a tile that has content + // may well have more than one call to `ensure_layer_stack_bufs`, and layers needing + // buffers are not necessarily the most common case. As such, we keep this check inlined + // (because the function itself is inlined), and if buffers are needed do an explicit + // function call that is not inlined. That keeps the generated code size small at our call + // sites. + (layer_bufs < layers.len).then( + #[inline(never)] + || { + for layer in &mut layers.stack[layer_bufs..layers.len] { + layer.occupied_tiles.push(tile_idx); + self.push_buf(layer.kind); + } + }, + ); + } + + /// Fill a rectangular region with a paint. + /// + /// Generates fill commands unless the tile is in a zero-clip region (fully clipped out). + /// For clipped filter layers, commands are always generated since filters need the full + /// layer content rendered before applying the clip as a mask. + /// + /// The `fill_hint` parameter is pre-computed by the caller based on paint type: + /// - `OpaqueSolid(color)`: Paint is an opaque solid color, can replace background + /// - `OpaqueImage`: Paint is an opaque image, can clear previous commands + /// - `None`: No optimization available + fn fill( + &mut self, + batch_count: u32, + tile_idx: usize, + layers: &mut NeedsBufLayerStack, + x: u16, + width: u16, + attrs_idx: u32, + current_layer_id: LayerId, + fill_hint: FillHint, + ) { + if !self.is_zero_clip() || self.in_clipped_filter_layer { + self.ensure_layer_stack_bufs(tile_idx, layers, batch_count); + + // Check if we can apply overdraw elimination optimization. + // This requires filling the entire tile width with no clip/buffer stack. + // + // Note that we could be more aggressive in optimizing a whole-tile opaque fill + // even with a clip stack. It would be valid to elide all drawing commands from + // the enclosing clip push up to the fill. Further, we could extend the clip + // push command to include a background color, rather than always starting with + // a transparent buffer. Lastly, a sequence of push(bg); strip/fill; pop could + // be replaced with strip/fill with the color (the latter is true even with a + // non-opaque color). + // + // However, the extra cost of tracking such optimizations may outweigh the + // benefit, especially in hybrid mode with GPU painting. + + let can_override = x == 0 + && width == WideTile::WIDTH + && self.n_clip == 0 + && self.n_bufs == 0 + // TODO: Relax this condition to only trigger for filter graphs that contain at + // least 1 primitive filter that has non-local effects (see + // https://github.com/linebender/vello/pull/1526#discussion_r3007377869) + && !self.had_filter_layer; + + if can_override { + match fill_hint { + FillHint::OpaqueSolid(color) => { + self.cmds.clear(); + self.bg = color; + + // We need to invalidate the ranges of all layers that have been drawn so far + // in that wide tile. + self.layer_cmd_ranges.clear(); + self.layer_cmd_ranges + .insert(current_layer_id, LayerCommandRanges::default()); + return; + } + FillHint::OpaqueImage => { + // Opaque image: clear previous commands but still emit the fill. + self.cmds.clear(); + self.bg = PremulColor::from_alpha_color(TRANSPARENT); + + // We need to invalidate the ranges of all layers that have been drawn so far + // in that wide tile. + self.layer_cmd_ranges.clear(); + self.layer_cmd_ranges + .insert(current_layer_id, LayerCommandRanges::default()); + // Fall through to emit the fill command below, as opposed to + // solid paints where we have a return statement. + } + FillHint::None => {} + } + } + + self.record_fill_cmd(current_layer_id, self.cmds.len()); + self.cmds.push(Cmd::Fill(CmdFill { + x, + width, + attrs_idx, + })); + } + } + + /// Fill a region using an alpha mask from a strip. + /// + /// Generates alpha fill commands unless the tile is in a zero-clip region (fully clipped out). + /// For clipped filter layers, commands are always generated since filters need the full + /// layer content rendered before applying the clip as a mask. + fn strip( + &mut self, + batch_count: u32, + tile_idx: usize, + layers: &mut NeedsBufLayerStack, + cmd_strip: CmdAlphaFill, + current_layer_id: LayerId, + ) { + if !self.is_zero_clip() || self.in_clipped_filter_layer { + self.ensure_layer_stack_bufs(tile_idx, layers, batch_count); + + self.record_fill_cmd(current_layer_id, self.cmds.len()); + self.cmds.push(Cmd::AlphaFill(cmd_strip)); + } + } + + /// Adds a new clip region to the current wide tile. + /// + /// Pushes a clip buffer unless the tile is in a zero-clip region (fully clipped out). + /// For clipped filter layers, clip buffers are always pushed since filters need explicit + /// clip state to process the full layer before applying the clip as a mask. + fn push_clip( + &mut self, + tile_idx: usize, + layers: &mut NeedsBufLayerStack, + layer_id: LayerId, + batch_count: u32, + ) { + if !self.is_zero_clip() || self.in_clipped_filter_layer { + self.ensure_layer_stack_bufs(tile_idx, layers, batch_count); + self.push_buf(LayerKind::Clip(layer_id)); + self.n_clip += 1; + } + } + + /// Removes the most recently added clip region from the current wide tile. + /// + /// Pops a clip buffer unless the tile is in a zero-clip region (fully clipped out). + /// For clipped filter layers, clip buffers are always popped since filters need explicit + /// clip state to process the full layer before applying the clip as a mask. + fn pop_clip(&mut self) { + if !self.is_zero_clip() || self.in_clipped_filter_layer { + self.pop_buf(); + self.n_clip -= 1; + } + } + + /// Adds a zero-winding clip region to the stack. + /// + /// Zero-winding clips represent tiles completely outside the clip path. + /// Normally these just increment a counter to suppress drawing, but for + /// clipped filter layers we generate explicit commands so filters can + /// process the entire layer before applying the clip as a mask. + fn push_zero_clip(&mut self, layer_id: LayerId) { + if self.in_clipped_filter_layer { + // Generate explicit command for filter processing + self.cmds.push(Cmd::PushZeroClip(layer_id)); + } + self.n_zero_clip += 1; + } + + /// Removes the most recently added zero-winding clip region. + fn pop_zero_clip(&mut self) { + if self.in_clipped_filter_layer { + // Generate explicit command for filter processing + self.cmds.push(Cmd::PopZeroClip); + } + self.n_zero_clip -= 1; + } + + /// Checks if the current clip region is a zero-winding clip. + fn is_zero_clip(&mut self) -> bool { + self.n_zero_clip > 0 + } + + /// Applies a clip strip operation with the given parameters. + /// + /// Note: Unlike content operations (`strip`, `push_clip`, etc.), clip operations don't need + /// the `|| self.in_clipped_filter_layer` check. Filter effects need full layer *content* + /// rendered (even in zero-clip areas). + fn clip_strip(&mut self, cmd_clip_strip: CmdClipAlphaFill) { + if (!self.is_zero_clip()) && !matches!(self.cmds.last(), Some(Cmd::PushBuf(..))) { + self.cmds.push(Cmd::ClipStrip(cmd_clip_strip)); + } + } + + /// Applies a clip fill operation at the specified position and width. + fn clip_fill(&mut self, x: u16, width: u16) { + if (!self.is_zero_clip()) && !matches!(self.cmds.last(), Some(Cmd::PushBuf(..))) { + self.cmds.push(Cmd::ClipFill(CmdClipFill { x, width })); + } + } + + /// Records the fill command for a specific layer. + fn record_fill_cmd(&mut self, layer_id: LayerId, cmd_idx: usize) { + self.layer_cmd_ranges.entry(layer_id).and_modify(|ranges| { + ranges.full_range.end = cmd_idx + 1; + if ranges.render_range.is_empty() { + ranges.render_range = cmd_idx..cmd_idx + 1; + } else { + ranges.render_range.end = cmd_idx + 1; + } + }); + } + + /// Whether the set of pushed commands so far indicates that the surface is used as + /// a blend target. + pub fn surface_is_blend_target(&self) -> bool { + self.surface_is_blend_target + } + + /// Push a buffer for a new layer. + /// + /// Different layer kinds are handled differently: + /// - Regular layers: Use local `blend_buf` stack for temporary storage + /// - Filtered layers: Materialized in persistent layer storage for filter processing + /// - Clip layers: Special handling for clipping operations + #[inline(always)] + fn push_buf(&mut self, layer_kind: LayerKind) { + let top_layer = layer_kind.id(); + if matches!(layer_kind, LayerKind::Filtered(_)) { + self.layer_cmd_ranges.insert( + top_layer, + LayerCommandRanges { + full_range: self.cmds.len()..self.cmds.len() + 1, + // Start with empty render_range; will be updated by `record_fill_cmd` and `pop_buf`. + render_range: self.cmds.len() + 1..self.cmds.len() + 1, + }, + ); + } else if matches!(layer_kind, LayerKind::Clip(_)) { + self.layer_cmd_ranges.entry(top_layer).and_modify(|ranges| { + ranges.full_range.end = self.cmds.len() + 1; + // Start with empty render_range; will be updated by `record_fill_cmd` and `pop_buf`. + ranges.render_range = self.cmds.len() + 1..self.cmds.len() + 1; + }); + } + + if MODE == MODE_HYBRID { + self.push_buf_indices.push(self.cmds.len()); + } + + self.cmds.push(Cmd::PushBuf(layer_kind, false)); + self.layer_ids.push(layer_kind); + self.n_bufs += 1; + } + + /// Pop the most recent buffer. + #[inline(always)] + fn pop_buf(&mut self) { + if MODE == MODE_HYBRID { + self.push_buf_indices.pop(); + } + + let top_layer = self.layer_ids.pop().unwrap(); + let mut next_layer = *self.layer_ids.last().unwrap(); + + if matches!(self.cmds.last(), Some(&Cmd::PushBuf(..))) { + // Optimization: If no drawing happened between the last `PushBuf`, + // we can just pop it instead. + self.cmds.pop(); + } else { + self.layer_cmd_ranges + .entry(top_layer.id()) + .and_modify(|ranges| { + ranges.full_range.end = self.cmds.len() + 1; + }); + if top_layer.id() == next_layer.id() { + next_layer = *self + .layer_ids + .get(self.layer_ids.len().saturating_sub(2)) + .unwrap(); + } + + self.layer_cmd_ranges + .entry(next_layer.id()) + .and_modify(|ranges| { + ranges.full_range.end = self.cmds.len() + 1; + ranges.render_range.end = self.cmds.len() + 1; + }); + self.cmds.push(Cmd::PopBuf); + } + self.n_bufs -= 1; + } + + /// Apply an opacity to the whole buffer. + fn opacity(&mut self, opacity: f32) { + if opacity != 1.0 { + self.cmds.push(Cmd::Opacity(opacity)); + } + } + + /// Apply a filter effect to the whole buffer. + pub fn filter(&mut self, layer_id: LayerId, filter: Filter) { + self.had_filter_layer = true; + self.cmds.push(Cmd::Filter(layer_id, filter)); + } + + /// Apply a mask to the whole buffer. + fn mask(&mut self, mask: Mask) { + self.cmds.push(Cmd::Mask(mask)); + } + + /// Blend the current buffer into the previous buffer in the stack. + fn blend(&mut self, blend_mode: BlendMode) { + #[allow(clippy::collapsible_if, reason = "better expresses intent")] + if MODE == MODE_HYBRID { + // Whether we use a non-default blend mode to blend into the destination. + let blends_into_dest = + blend_mode.mix != Mix::Normal || blend_mode.compose != Compose::SrcOver; + + if blends_into_dest && self.push_buf_indices.len() >= 2 { + let nos_idx = self.push_buf_indices[self.push_buf_indices.len() - 2]; + + if nos_idx == TARGET_SURFACE_PUSH_BUF_IDX { + self.surface_is_blend_target = true; + } else { + match &mut self.cmds[nos_idx] { + Cmd::PushBuf(_, is_blend_target) => *is_blend_target = true, + // Anything else shouldn't be possible. + _ => unreachable!(), + } + } + } + } + + self.cmds.push(Cmd::Blend(blend_mode)); + } +} + +/// Debug utilities for wide tiles. +/// +/// These methods are only available in debug builds (`debug_assertions`). +/// They provide introspection into the command buffer for debugging and logging purposes. +#[cfg(debug_assertions)] +impl WideTile { + /// Lists all commands in this wide tile with their indices and names. + /// + /// Returns a formatted string with each command on a new line, showing its index + /// and human-readable name. This is useful for debugging and understanding the + /// command sequence. + /// + /// # Example + /// + /// ```ignore + /// let commands = wide_tile.list_commands(); + /// println!("{}", commands); + /// // Output: + /// // 0: PushBuf(Regular) + /// // 1: FillPath + /// // 2: PushZeroClip + /// // 3: FillPath + /// // 4: PopBuf + /// ``` + #[allow(dead_code, reason = "useful for debugging")] + pub fn list_commands(&self) -> String { + self.cmds + .iter() + .enumerate() + .map(|(i, cmd)| format!("{}: {}", i, cmd.name())) + .collect::>() + .join("\n") + } +} + +/// Optimization hint for fill operations, computed in `Wide::generate` and passed to `WideTile::fill`. +/// +/// This enum communicates whether a fill operation can benefit from overdraw elimination: +/// - For opaque solid colors: we can set the background color directly and skip the fill +/// - For opaque images: we can clear previous commands but still need to emit the fill +#[derive(Debug, Clone, Copy)] +pub enum FillHint { + /// No optimization possible, emit fill command normally. + None, + /// Paint is an opaque solid color - can replace background if conditions are met. + OpaqueSolid(PremulColor), + /// Paint is an opaque image - can clear previous commands if conditions are met. + OpaqueImage, +} + +/// Distinguishes between different types of layers and their storage strategies. +/// +/// Each layer kind determines how the layer's content is stored and processed: +/// - Regular layers are blended on-the-fly using a temporary buffer stack +/// - Filtered layers are materialized in persistent storage for filter processing +/// - Clip layers are special buffers used for clipping operations +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LayerKind { + /// Regular layer using local `blend_buf` stack for temporary storage. + Regular(LayerId), + /// Filtered layer materialized in persistent `layer_manager` storage. + Filtered(LayerId), + /// Clip layer for clipping operations. + Clip(LayerId), +} + +impl LayerKind { + /// Get the underlying layer ID. + /// + /// All layer kinds contain a layer ID that uniquely identifies the layer. + pub fn id(&self) -> LayerId { + match self { + Self::Regular(id) | Self::Filtered(id) | Self::Clip(id) => *id, + } + } +} + +/// A drawing command for wide tiles. +/// +/// Commands are executed in order to render the final image. They include +/// drawing operations (`Fill`, `AlphaFill`), layer management (`PushBuf`, `PopBuf`), +/// clipping operations (`ClipFill`, `ClipStrip`), and post-processing effects +/// (`Filter`, `Blend`, `Opacity`, `Mask`). +#[derive(Clone, Debug, PartialEq)] +pub enum Cmd { + /// Fill a rectangular region with a solid color or paint. + Fill(CmdFill), + /// Fill a region with a paint, modulated by an alpha mask. + AlphaFill(CmdAlphaFill), + /// Pushes a new buffer for drawing. + /// Regular layers use the local `blend_buf` stack. + /// Filtered layers are materialized in persistent layer storage. + /// + /// The second argument indicates whether the layer that is about to be pushed + /// will be used as a destination for a blending operation with a non-default blend mode. + /// This information is only needed by `vello_hybrid` for scheduling purposes. + PushBuf(LayerKind, bool), + /// Pops the most recent buffer and blends it into the previous buffer. + PopBuf, + /// A fill command within a clipping region. + /// + /// This command will blend the contents of the current buffer within the clip fill region + /// into the previous buffer in the stack. + ClipFill(CmdClipFill), + /// A fill command with alpha mask within a clipping region. + /// + /// This command will blend the contents of the current buffer within the clip fill region + /// into the previous buffer in the stack, with an additional alpha mask. + ClipStrip(CmdClipAlphaFill), + /// Marks entry into a zero-winding clip region for a clipped filter layer. + /// + /// Zero-winding clips represent tiles completely outside the clip path. For clipped + /// filter layers, this command allows the filter to process the full layer content + /// before applying the clip as a mask (per SVG spec: filter → clip → mask → blend). + PushZeroClip(LayerId), + /// Marks exit from a zero-winding clip region for a clipped filter layer. + PopZeroClip, + /// Apply a filter effect to a layer's contents. + /// + /// This command applies a filter (e.g., blur, drop shadow) to the specified layer's + /// rendered content. Per the SVG specification, filters are applied before clipping, + /// masking, blending, and opacity operations. + Filter(LayerId, Filter), + /// Blend the current buffer into the previous buffer. + /// + /// This command blends the contents of the current buffer into the previous buffer + /// using the specified blend mode (e.g., multiply, screen, overlay). + Blend(BlendMode), + /// Apply uniform opacity to the current buffer. + /// + /// Multiplies the alpha channel of all pixels in the buffer by the given opacity value. + Opacity(f32), + /// Apply a mask to the current buffer. + /// + /// Modulates the alpha channel of the buffer using the provided mask. + Mask(Mask), + /// Marks a boundary between rendering fast path strips and coarse rasterized strips. + /// + /// Only meaningful in `MODE_HYBRID`. + // TODO: Consider removing this, see https://github.com/linebender/vello/pull/1479#discussion_r2869917343. + BatchEnd, +} + +#[cfg(debug_assertions)] +impl Cmd { + /// Returns a human-readable name for this command. + /// + /// This is useful for debugging, logging, and displaying command information + /// in a user-friendly format. To get detailed paint information, use `name_with_attrs` + /// which can look up the paint from the command attributes. + /// + /// **Note:** This method is only available in debug builds (`debug_assertions`). + pub fn name(&self) -> &'static str { + match self { + Self::Fill(_) => "FillPath", + Self::AlphaFill(_) => "AlphaFillPath", + Self::PushBuf(layer_kind, needs_temp) => match (layer_kind, needs_temp) { + (LayerKind::Regular(_), false) => "PushBuf(Regular, false)", + (LayerKind::Regular(_), true) => "PushBuf(Regular, true)", + (LayerKind::Filtered(_), false) => "PushBuf(Filtered, false)", + (LayerKind::Filtered(_), true) => "PushBuf(Filtered, true)", + (LayerKind::Clip(_), false) => "PushBuf(Clip, false)", + (LayerKind::Clip(_), true) => "PushBuf(Clip, true)", + }, + Self::PopBuf => "PopBuf", + Self::ClipFill(_) => "ClipPathFill", + Self::ClipStrip(_) => "ClipPathStrip", + Self::PushZeroClip(_) => "PushZeroClip", + Self::PopZeroClip => "PopZeroClip", + Self::Filter(_, _) => "Filter", + Self::Blend(_) => "Blend", + Self::Opacity(_) => "Opacity", + Self::Mask(_) => "Mask", + Self::BatchEnd => "BatchEnd", + } + } + + /// Returns a human-readable name for this command with detailed paint information. + /// + /// This variant looks up paint details from the command attributes for fill commands. + /// + /// **Note:** This method is only available in debug builds (`debug_assertions`). + pub fn name_with_attrs( + &self, + fill_attrs: &[FillAttrs], + encoded_paints: &[EncodedPaint], + ) -> String { + match self { + Self::Fill(cmd) => { + if let Some(attrs) = fill_attrs.get(cmd.attrs_idx as usize) { + format!("FillPath({})", paint_name(&attrs.paint, encoded_paints)) + } else { + format!("FillPath(attrs_idx={})", cmd.attrs_idx) + } + } + Self::AlphaFill(cmd) => { + if let Some(attrs) = fill_attrs.get(cmd.attrs_idx as usize) { + format!( + "AlphaFillPath({})", + paint_name(&attrs.paint, encoded_paints) + ) + } else { + format!("AlphaFillPath(attrs_idx={})", cmd.attrs_idx) + } + } + _ => self.name().into(), + } + } +} + +/// Returns a human-readable description of a paint. +#[cfg(debug_assertions)] +fn paint_name(paint: &Paint, encoded_paints: &[EncodedPaint]) -> String { + match paint { + Paint::Solid(color) => { + let rgba = color.as_premul_rgba8(); + format!( + "Solid(#{:02x}{:02x}{:02x}{:02x})", + rgba.r, rgba.g, rgba.b, rgba.a + ) + } + Paint::Indexed(idx) => { + let index = idx.index(); + if let Some(encoded) = encoded_paints.get(index) { + let kind = match encoded { + EncodedPaint::Gradient(g) => match &g.kind { + crate::encode::EncodedKind::Linear(_) => "LinearGradient", + crate::encode::EncodedKind::Radial(_) => "RadialGradient", + crate::encode::EncodedKind::Sweep(_) => "SweepGradient", + }, + EncodedPaint::Image(_) => "Image", + EncodedPaint::ExternalTexture(_) => "ExternalTexture", + EncodedPaint::BlurredRoundedRect(_) => "BlurredRoundedRect", + }; + format!("{}[{}]", kind, index) + } else { + format!("Indexed({})", index) + } + } + } +} + +/// Shared attributes for alpha fill commands. +#[derive(Debug, Clone, PartialEq)] +pub struct FillAttrs { + /// The index of the thread that owns the alpha buffer + /// containing the mask values at `alpha_idx`. + /// Always 0 in single-threaded mode. + pub thread_idx: u8, + /// The paint (color, gradient, etc.) to fill the region with. + // TODO: Store premultiplied colors as indexed paints as well, to reduce + // memory overhead? Or get rid of indexed paints and inline all paints? + pub paint: Paint, + /// The blend mode to apply before drawing the contents. + pub blend_mode: BlendMode, + /// A mask to apply to the command. + pub mask: Option, + /// Base index into the alpha buffer for this path's commands. + /// Commands store a relative offset that is added to this base. + alpha_base_idx: u32, +} + +impl FillAttrs { + /// Compute the absolute alpha buffer index from a relative offset. + pub fn alpha_idx(&self, offset: u32) -> u32 { + self.alpha_base_idx + offset + } +} + +/// Shared attributes for clip alpha fill commands. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClipAttrs { + /// The index of the thread that owns the alpha buffer + /// containing the mask values at `alpha_idx`. + /// Always 0 in single-threaded mode. + pub thread_idx: u8, + /// Base index into the alpha buffer for this clip path's commands. + /// Commands store a relative offset that is added to this base. + alpha_base_idx: u32, +} + +impl ClipAttrs { + /// Compute the absolute alpha buffer index from a relative offset. + pub fn alpha_idx(&self, offset: u32) -> u32 { + self.alpha_base_idx + offset + } +} + +/// Container for shared command attributes. +/// +/// This struct holds the shared attributes for fill and clip commands, +/// allowing them to be passed together to functions that need both. +#[derive(Debug, Default, Clone)] +pub struct CommandAttrs { + /// Shared attributes for fill commands, indexed by `attrs_idx` in `CmdFill`/`CmdAlphaFill`. + pub fill: Vec, + /// Shared attributes for clip commands, indexed by `attrs_idx` in `CmdClipAlphaFill`. + pub clip: Vec, +} + +impl CommandAttrs { + /// Clear all attributes. + pub fn clear(&mut self) { + self.fill.clear(); + self.clip.clear(); + } +} + +/// Fill a consecutive horizontal region of a wide tile. +/// +/// This command fills a rectangular region with the specified paint. +/// The region starts at x-coordinate `x` and extends for `width` pixels +/// horizontally, spanning the full height of the wide tile. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CmdFill { + /// The horizontal start position relative to the wide tile's left edge, in pixels. + pub x: u16, + /// The width of the filled region in pixels. + pub width: u16, + /// Index into the command attributes array. + pub attrs_idx: u32, +} + +/// Fill a consecutive horizontal region with an alpha mask. +/// +/// Similar to `CmdFill`, but modulates the paint by an alpha mask stored +/// in a separate buffer. This is used for anti-aliased edges and partial +/// coverage from path rasterization. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CmdAlphaFill { + /// The horizontal start position relative to the wide tile's left edge, in pixels. + pub x: u16, + /// The width of the filled region in pixels. + pub width: u16, + /// Relative offset to the alpha buffer location. + /// Use `FillAttrs::alpha_idx(alpha_offset)` to compute the absolute index. + pub alpha_offset: u32, + /// Index into the command attributes array. + pub attrs_idx: u32, +} + +/// Fill operation within a clipping region. +/// +/// This command copies a horizontal region from the top of the clip buffer stack +/// to the next buffer on the stack, effectively rendering the clipped content. +/// Unlike `CmdFill`, this doesn't fill with a paint but transfers existing content. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CmdClipFill { + /// The horizontal start position relative to the wide tile's left edge, in pixels. + pub x: u16, + /// The width of the region to copy in pixels. + pub width: u16, +} + +/// Alpha-masked fill operation within a clipping region. +/// +/// This command composites a horizontal region from the top of the clip buffer stack +/// to the next buffer, modulated by an alpha mask. This is used for anti-aliased +/// clip edges. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CmdClipAlphaFill { + /// The horizontal start position relative to the wide tile's left edge, in pixels. + pub x: u16, + /// The width of the region to composite in pixels. + pub width: u16, + /// Relative offset to the alpha buffer location. + /// Use `ClipAttrs::alpha_idx(alpha_offset)` to compute the absolute index. + pub alpha_offset: u32, + /// Index into the clip attributes array. + pub attrs_idx: u32, +} + +/// Ranges of commands for a specific layer in a specific tile. +/// +/// This structure tracks two different ranges of commands: +/// - The full range includes all layer operations (push, draw, pop) +/// - The render range includes only the actual drawing commands +#[derive(Debug, Clone, Default)] +pub struct LayerCommandRanges { + /// Full range including `PushBuf`, all commands, and `PopBuf`. + pub full_range: Range, + /// Range containing only fill commands (`Fill`, `AlphaFill`). + /// This is the range to replace when sampling from a filtered layer. + pub render_range: Range, +} + +impl LayerCommandRanges { + /// Clear the full range and render range. + #[inline] + pub fn clear(&mut self) { + self.full_range = 0..0; + self.render_range = 0..0; + } +} + +#[cfg(test)] +mod tests { + use crate::coarse::{FillHint, LayerKind, MODE_CPU, NeedsBufLayerStack, Wide, WideTile}; + use crate::kurbo::Affine; + use crate::paint::{Paint, PremulColor}; + use crate::peniko; + use crate::peniko::{BlendMode, Compose, Mix}; + use crate::render_graph::RenderGraph; + use crate::strip::Strip; + use alloc::{boxed::Box, vec}; + + #[test] + fn optimize_empty_layers() { + let mut wide = WideTile::::new(0, 0); + wide.push_buf(LayerKind::Regular(0)); + wide.pop_buf(); + + assert!(wide.cmds.is_empty()); + } + + #[test] + fn basic_layer() { + let mut wide = WideTile::::new(0, 0); + let mut layers = NeedsBufLayerStack::default(); + layers.push(LayerKind::Regular(0)); + wide.push_buf(LayerKind::Regular(0)); + wide.fill(0, 0, &mut layers, 0, 10, 0, 0, FillHint::None); + wide.fill(0, 0, &mut layers, 10, 10, 0, 0, FillHint::None); + wide.pop_buf(); + + assert_eq!(wide.cmds.len(), 4); + } + + #[test] + fn dont_inline_blend_with_two_fills() { + let blend_mode = BlendMode::new(Mix::Lighten, Compose::SrcOver); + + let mut wide = WideTile::::new(0, 0); + let mut layers = NeedsBufLayerStack::default(); + layers.push(LayerKind::Regular(0)); + wide.push_buf(LayerKind::Regular(0)); + wide.fill(0, 0, &mut layers, 0, 10, 0, 0, FillHint::None); + wide.fill(0, 0, &mut layers, 10, 10, 0, 0, FillHint::None); + wide.blend(blend_mode); + wide.pop_buf(); + + assert_eq!(wide.cmds.len(), 5); + } + + #[test] + fn dont_inline_destructive_blend() { + let blend_mode = BlendMode::new(Mix::Lighten, Compose::Clear); + + let mut wide = WideTile::::new(0, 0); + let mut layers = NeedsBufLayerStack::default(); + layers.push(LayerKind::Regular(0)); + wide.push_buf(LayerKind::Regular(0)); + wide.fill(0, 0, &mut layers, 0, 10, 0, 0, FillHint::None); + wide.blend(blend_mode); + wide.pop_buf(); + + assert_eq!(wide.cmds.len(), 4); + } + + #[test] + fn tile_coordinates() { + let wide = Wide::::new(1000, 258); + + let tile_1 = wide.get(1, 3); + assert_eq!(tile_1.x, 256); + assert_eq!(tile_1.y, 12); + + let tile_2 = wide.get(2, 15); + assert_eq!(tile_2.x, 512); + assert_eq!(tile_2.y, 60); + } + + #[test] + fn reset_clears_layer_and_clip_stacks() { + type ClipPath = Option>; + + let mut wide = Wide::::new(1000, 258); + let mut render_graph = RenderGraph::new(); + let no_clip_path: ClipPath = None; + wide.push_layer( + 1, + no_clip_path, + BlendMode::default(), + None, + 0.5, + None, + Affine::IDENTITY, + &mut render_graph, + 0, + ); + + assert_eq!(wide.layer_stack.len(), 1); + assert_eq!(wide.clip_stack.len(), 0); + + let strip = Strip::new(2, 2, 0, true); + let clip_path = Some(vec![strip].into_boxed_slice()); + wide.push_layer( + 2, + clip_path, + BlendMode::default(), + None, + 0.09, + None, + Affine::IDENTITY, + &mut render_graph, + 0, + ); + + assert_eq!(wide.layer_stack.len(), 2); + assert_eq!(wide.clip_stack.len(), 1); + assert_eq!(wide.tiles[0].n_bufs, 0); + + wide.reset(); + + assert_eq!(wide.layer_stack.len(), 0); + assert_eq!(wide.clip_stack.len(), 0); + assert_eq!(wide.tiles[0].n_bufs, 0); + } + + #[test] + fn tiles_dirty_flag() { + type ClipPath = Option>; + + let mut wide = Wide::::new(256, 4); + let mut render_graph = RenderGraph::new(); + + assert!(!wide.tiles_dirty); + + let strips = [Strip::new(0, 0, 0, false), Strip::new(10, 0, 4, true)]; + wide.generate( + &strips, + Paint::Solid(PremulColor::from_alpha_color( + peniko::color::palette::css::RED, + )), + BlendMode::default(), + 0, + None, + &[], + ); + assert!(wide.tiles_dirty); + + wide.reset(); + assert!(!wide.tiles_dirty); + + let no_clip: ClipPath = None; + wide.push_layer( + 1, + no_clip, + BlendMode::default(), + None, + 1.0, + None, + Affine::IDENTITY, + &mut render_graph, + 0, + ); + assert!(wide.tiles_dirty); + + wide.pop_layer(&mut render_graph); + assert!(wide.tiles_dirty); + + wide.reset(); + assert!(!wide.tiles_dirty); + + // Generating with empty strips does not need to mark it as dirty. + wide.generate( + &[], + Paint::Solid(PremulColor::from_alpha_color( + peniko::color::palette::css::RED, + )), + BlendMode::default(), + 0, + None, + &[], + ); + assert!(!wide.tiles_dirty); + } +} diff --git a/vendor/vello_common/src/encode.rs b/vendor/vello_common/src/encode.rs new file mode 100644 index 000000000..5161fe021 --- /dev/null +++ b/vendor/vello_common/src/encode.rs @@ -0,0 +1,1354 @@ +// Copyright 2025 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Paints for drawing shapes. + +use crate::TextureId; +use crate::blurred_rounded_rect::BlurredRoundedRectangle; +use crate::color::palette::css::BLACK; +use crate::color::{ColorSpaceTag, HueDirection, Srgb, gradient}; +use crate::geometry::RectU16; +use crate::kurbo::{Affine, Point, Vec2}; +use crate::math::{FloatExt, compute_erf7}; +use crate::paint::{Image, ImageSource, IndexedPaint, Paint, PremulColor, Tint}; +use crate::peniko::{ColorStop, ColorStops, Extend, Gradient, GradientKind, ImageQuality}; +use alloc::borrow::Cow; +use alloc::fmt::Debug; +use alloc::vec; +use alloc::vec::Vec; +#[cfg(not(feature = "multithreading"))] +use core::cell::OnceCell; +use core::hash::{Hash, Hasher}; +use fearless_simd::{Simd, SimdBase, SimdFloat, f32x4, f32x16, mask32x4, mask32x16}; +use peniko::color::cache_key::{BitEq, BitHash, CacheKey}; +use peniko::color::gradient_unpremultiplied; +use peniko::{ + ImageSampler, InterpolationAlphaSpace, LinearGradientPosition, RadialGradientPosition, + SweepGradientPosition, +}; +use smallvec::ToSmallVec; +// So we can just use `OnceCell` regardless of which feature is activated. +#[cfg(feature = "multithreading")] +use std::sync::OnceLock as OnceCell; + +use crate::simd::{Splat4thExt, element_wise_splat}; +#[cfg(not(feature = "std"))] +use peniko::kurbo::common::FloatFuncs as _; + +const DEGENERATE_THRESHOLD: f32 = 1.0e-6; +const NUDGE_VAL: f32 = 1.0e-7; +#[cfg(feature = "std")] +fn exp(val: f32) -> f32 { + val.exp() +} + +#[cfg(not(feature = "std"))] +fn exp(val: f32) -> f32 { + #[cfg(feature = "libm")] + return libm::expf(val); + #[cfg(not(feature = "libm"))] + compile_error!("vello_common requires either the `std` or `libm` feature"); +} + +/// A trait for encoding paints. +pub trait EncodeExt: private::Sealed { + /// Encode the paint and push it into a vector of encoded paints, returning + /// the corresponding paint in the process. This will also validate the paint. + fn encode_into( + &self, + paints: &mut Vec, + transform: Affine, + tint: Option, + ) -> Paint; +} + +impl EncodeExt for Gradient { + /// Encode the gradient into a paint. + fn encode_into( + &self, + paints: &mut Vec, + transform: Affine, + _tint: Option, + ) -> Paint { + // First make sure that the gradient is valid and not degenerate. + if let Err(paint) = validate(self) { + return paint; + } + + let mut may_have_transparency = self.stops.iter().any(|s| s.color.components[3] != 1.0); + + let mut base_transform; + + let mut stops = Cow::Borrowed(&self.stops.0); + + let first_stop = &stops[0]; + let last_stop = &stops[stops.len() - 1]; + + if first_stop.offset != 0.0 || last_stop.offset != 1.0 { + let mut vec = stops.to_smallvec(); + + if first_stop.offset != 0.0 { + let mut first_stop = *first_stop; + first_stop.offset = 0.0; + vec.insert(0, first_stop); + } + + if last_stop.offset != 1.0 { + let mut last_stop = *last_stop; + last_stop.offset = 1.0; + vec.push(last_stop); + } + + stops = Cow::Owned(vec); + } + + let kind = match self.kind { + GradientKind::Linear(LinearGradientPosition { start: p0, end: p1 }) => { + // We update the transform currently in-place, such that the gradient line always + // starts at the point (0, 0) and ends at the point (1, 0). This simplifies the + // calculation for the current position along the gradient line a lot. + base_transform = ts_from_line_to_line(p0, p1, Point::ZERO, Point::new(1.0, 0.0)); + + EncodedKind::Linear(LinearKind) + } + GradientKind::Radial(RadialGradientPosition { + start_center: c0, + start_radius: r0, + end_center: c1, + end_radius: r1, + }) => { + // The implementation of radial gradients is translated from Skia. + // See: + // - + // - + // - + let d_radius = r1 - r0; + + // + let radial_kind = if ((c1 - c0).length() as f32).is_nearly_zero() { + base_transform = Affine::translate((-c1.x, -c1.y)); + base_transform = base_transform.then_scale(1.0 / r0.max(r1) as f64); + + let scale = r1.max(r0) / d_radius; + let bias = -r0 / d_radius; + + RadialKind::Radial { bias, scale } + } else { + base_transform = + ts_from_line_to_line(c0, c1, Point::ZERO, Point::new(1.0, 0.0)); + + if (r1 - r0).is_nearly_zero() { + let scaled_r0 = r1 / (c1 - c0).length() as f32; + RadialKind::Strip { + scaled_r0_squared: scaled_r0 * scaled_r0, + } + } else { + let d_center = (c0 - c1).length() as f32; + + let focal_data = + FocalData::create(r0 / d_center, r1 / d_center, &mut base_transform); + + let fp0 = 1.0 / focal_data.fr1; + let fp1 = focal_data.f_focal_x; + + RadialKind::Focal { + focal_data, + fp0, + fp1, + } + } + }; + + // Even if the gradient has no stops with transparency, we might have to force + // alpha-compositing in case the radial gradient is undefined in certain positions, + // in which case the resulting color will be transparent and thus the gradient overall + // must be treated as non-opaque. + may_have_transparency |= radial_kind.has_undefined(); + + EncodedKind::Radial(radial_kind) + } + GradientKind::Sweep(SweepGradientPosition { + center, + start_angle, + end_angle, + }) => { + // Make sure the center of the gradient falls on the origin (0, 0), to make + // angle calculation easier. + let x_offset = -center.x as f32; + let y_offset = -center.y as f32; + base_transform = Affine::translate((x_offset as f64, y_offset as f64)); + + EncodedKind::Sweep(SweepKind { + start_angle, + // Save the inverse so that we can use a multiplication in the shader instead. + inv_angle_delta: 1.0 / (end_angle - start_angle), + }) + } + }; + + let ranges = encode_stops( + &stops, + self.interpolation_cs, + self.hue_direction, + self.interpolation_alpha_space, + ); + + // This represents the transform that needs to be applied to the starting point of a + // command before starting with the rendering. + // First we need to account for the base transform of the shader, then + // we need to apply the _inverse_ paint transform to the point so that we can account + // for the paint transform of the render context. + let transform = base_transform * transform.inverse(); + + // One possible approach of calculating the positions would be to apply the above + // transform to _each_ pixel that we render in the wide tile. However, a much better + // approach is to apply the transform once for the first pixel in each wide tile, + // and from then on only apply incremental updates to the current x/y position + // that we calculate based on the transform. + // + // Remember that we render wide tiles in column major order (i.e. we first calculate the + // values for a specific x for all Tile::HEIGHT y by incrementing y by 1, and then finally + // we increment the x position by 1 and start from the beginning). If we want to implement + // the above approach of incrementally updating the position, we need to calculate + // how the x/y unit vectors are affected by the transform, and then use this as the + // step delta for a step in the x/y direction. + let (x_advance, y_advance) = x_y_advances(&transform); + + let cache_key = CacheKey(GradientCacheKey { + stops: self.stops.clone(), + interpolation_cs: self.interpolation_cs, + hue_direction: self.hue_direction, + }); + + let has_undefined = kind.has_undefined(); + + let encoded = EncodedGradient { + cache_key, + kind, + has_undefined, + transform, + x_advance, + y_advance, + ranges, + extend: self.extend, + may_have_transparency, + u8_lut: OnceCell::new(), + f32_lut: OnceCell::new(), + }; + + let idx = paints.len(); + paints.push(encoded.into()); + + Paint::Indexed(IndexedPaint::new(idx)) + } +} + +/// Returns a fallback paint in case the gradient is invalid. +/// +/// The paint will be either black or contain the color of the first stop of the gradient. +fn validate(gradient: &Gradient) -> Result<(), Paint> { + let black = Err(BLACK.into()); + + // Gradients need at least two stops. + if gradient.stops.is_empty() { + return black; + } + + let first = Err(gradient.stops[0].color.to_alpha_color::().into()); + + if gradient.stops.len() == 1 { + return first; + } + + for stops in gradient.stops.windows(2) { + let f = stops[0]; + let n = stops[1]; + + // Offsets must be between 0 and 1, and not NaN. + if !(0.0..=1.0).contains(&f.offset) { + return first; + } + + // Stops must be sorted by ascending offset. + if f.offset > n.offset { + return first; + } + } + + // Check the last stop as well. + let last = gradient.stops.last().unwrap(); + if !(0.0..=1.0).contains(&last.offset) { + return first; + } + + let degenerate_point = |p1: &Point, p2: &Point| { + (p1.x - p2.x).abs() as f32 <= DEGENERATE_THRESHOLD + && (p1.y - p2.y).abs() as f32 <= DEGENERATE_THRESHOLD + }; + + let degenerate_val = |v1: f32, v2: f32| (v2 - v1).abs() <= DEGENERATE_THRESHOLD; + + match &gradient.kind { + GradientKind::Linear(LinearGradientPosition { start, end }) => { + // Start and end points must not be too close together. + if degenerate_point(start, end) { + return first; + } + } + GradientKind::Radial(RadialGradientPosition { + start_center, + start_radius, + end_center, + end_radius, + }) => { + // Radii must not be negative. + if *start_radius < 0.0 || *end_radius < 0.0 { + return first; + } + + // Radii and center points must not be close to the same. + if degenerate_point(start_center, end_center) + && degenerate_val(*start_radius, *end_radius) + { + return first; + } + } + GradientKind::Sweep(SweepGradientPosition { + start_angle, + end_angle, + .. + }) => { + // The end angle must be larger than the start angle. + if degenerate_val(*start_angle, *end_angle) { + return first; + } + + if end_angle <= start_angle { + return first; + } + } + } + + Ok(()) +} + +/// Encode all stops into a sequence of ranges. +fn encode_stops( + stops: &[ColorStop], + cs: ColorSpaceTag, + hue_dir: HueDirection, + interpolation_alpha_space: InterpolationAlphaSpace, +) -> Vec { + #[derive(Debug)] + struct EncodedColorStop { + offset: f32, + color: crate::color::AlphaColor, + } + + let create_range = |left_stop: &EncodedColorStop, right_stop: &EncodedColorStop| { + let clamp = |mut color: [f32; 4]| { + // The linear approximation of the gradient can produce values slightly outside of + // [0.0, 1.0], so clamp them. + for c in &mut color { + *c = c.clamp(0.0, 1.0); + } + + color + }; + + let x0 = left_stop.offset; + let x1 = right_stop.offset; + let c0 = if interpolation_alpha_space == InterpolationAlphaSpace::Unpremultiplied { + clamp(left_stop.color.components) + } else { + clamp(left_stop.color.premultiply().components) + }; + let c1 = if interpolation_alpha_space == InterpolationAlphaSpace::Unpremultiplied { + clamp(right_stop.color.components) + } else { + clamp(right_stop.color.premultiply().components) + }; + + // We calculate a bias and scale factor, such that we can simply calculate + // bias + x * scale to get the interpolated color, where x is between x0 and x1, + // to calculate the resulting color. + // Apply a nudge value because we sometimes call `create_range` with the same offset + // to create the padded stops. + let x1_minus_x0 = (x1 - x0).max(NUDGE_VAL); + let mut scale = [0.0; 4]; + let mut bias = c0; + + for i in 0..4 { + scale[i] = (c1[i] - c0[i]) / x1_minus_x0; + bias[i] = c0[i] - x0 * scale[i]; + } + + GradientRange { + x1, + bias, + scale, + interpolation_alpha_space, + } + }; + + // Create additional (SRGB-encoded) stops in-between to approximate the color space we want to + // interpolate in. + if cs != ColorSpaceTag::Srgb { + let interpolated_stops = if interpolation_alpha_space + == InterpolationAlphaSpace::Premultiplied + { + stops + .windows(2) + .flat_map(|s| { + let left_stop = &s[0]; + let right_stop = &s[1]; + + let interpolated = + gradient::(left_stop.color, right_stop.color, cs, hue_dir, 0.01); + + interpolated.map(|st| EncodedColorStop { + offset: left_stop.offset + (right_stop.offset - left_stop.offset) * st.0, + color: st.1.un_premultiply(), + }) + }) + .collect::>() + } else { + stops + .windows(2) + .flat_map(|s| { + let left_stop = &s[0]; + let right_stop = &s[1]; + + let interpolated = gradient_unpremultiplied::( + left_stop.color, + right_stop.color, + cs, + hue_dir, + 0.01, + ); + + interpolated.map(|st| EncodedColorStop { + offset: left_stop.offset + (right_stop.offset - left_stop.offset) * st.0, + color: st.1, + }) + }) + .collect::>() + }; + + interpolated_stops + .windows(2) + .map(|s| { + let left_stop = &s[0]; + let right_stop = &s[1]; + + create_range(left_stop, right_stop) + }) + .collect() + } else { + stops + .windows(2) + .map(|c| { + let c0 = EncodedColorStop { + offset: c[0].offset, + color: c[0].color.to_alpha_color::(), + }; + + let c1 = EncodedColorStop { + offset: c[1].offset, + color: c[1].color.to_alpha_color::(), + }; + + create_range(&c0, &c1) + }) + .collect() + } +} + +pub(crate) fn x_y_advances(transform: &Affine) -> (Vec2, Vec2) { + let scale_skew_transform = { + let c = transform.as_coeffs(); + Affine::new([c[0], c[1], c[2], c[3], 0.0, 0.0]) + }; + + let x_advance = scale_skew_transform * Point::new(1.0, 0.0); + let y_advance = scale_skew_transform * Point::new(0.0, 1.0); + + ( + Vec2::new(x_advance.x, x_advance.y), + Vec2::new(y_advance.x, y_advance.y), + ) +} + +impl private::Sealed for Image {} + +impl EncodeExt for Image { + fn encode_into( + &self, + paints: &mut Vec, + transform: Affine, + tint: Option, + ) -> Paint { + let idx = paints.len(); + + let mut sampler = self.sampler; + + if sampler.alpha != 1.0 { + // If the sampler alpha is not 1.0, we need to force alpha compositing. + unimplemented!("Applying opacity to image commands"); + } + + let c = transform.as_coeffs(); + + // Optimize image quality for integer-only translations. + if (c[0] as f32 - 1.0).is_nearly_zero() + && (c[1] as f32).is_nearly_zero() + && (c[2] as f32).is_nearly_zero() + && (c[3] as f32 - 1.0).is_nearly_zero() + && ((c[4] - c[4].floor()) as f32).is_nearly_zero() + && ((c[5] - c[5].floor()) as f32).is_nearly_zero() + && sampler.quality == ImageQuality::Medium + { + sampler.quality = ImageQuality::Low; + } + + let transform = transform.inverse(); + + let (x_advance, y_advance) = x_y_advances(&transform); + + // If the tint color has alpha < 1.0, the image will have opacities + // even if the source pixels are all opaque. + let tint_has_opacity = tint.as_ref().is_some_and(|t| t.color.components[3] < 1.0); + + let encoded = EncodedImage { + may_have_transparency: self.image.may_have_transparency() || tint_has_opacity, + source: self.image.clone(), + sampler, + transform, + x_advance, + y_advance, + tint, + }; + + paints.push(EncodedPaint::Image(encoded)); + + Paint::Indexed(IndexedPaint::new(idx)) + } +} + +/// An encoded paint. +#[derive(Debug)] +pub enum EncodedPaint { + /// An encoded gradient. + Gradient(EncodedGradient), + /// An encoded image. + Image(EncodedImage), + /// An encoded external texture. + ExternalTexture(EncodedExternalTexture), + /// A blurred, rounded rectangle. + BlurredRoundedRect(EncodedBlurredRoundedRectangle), +} + +impl From for EncodedPaint { + fn from(value: EncodedGradient) -> Self { + Self::Gradient(value) + } +} + +impl From for EncodedPaint { + fn from(value: EncodedBlurredRoundedRectangle) -> Self { + Self::BlurredRoundedRect(value) + } +} + +/// An encoded image. +#[derive(Debug)] +pub struct EncodedImage { + /// The underlying pixmap of the image. + pub source: ImageSource, + /// Sampler + pub sampler: ImageSampler, + /// Whether the image has opacities. + pub may_have_transparency: bool, + /// A transform to apply to the image. + pub transform: Affine, + /// The advance in image coordinates for one step in the x direction. + pub x_advance: Vec2, + /// The advance in image coordinates for one step in the y direction. + pub y_advance: Vec2, + /// Optional tint applied to the image. + pub tint: Option, +} + +/// An encoded external texture. +/// +/// The texture must be bound by the user at render-time in order for us to be able to sample from +/// it; it is not interned into the renderer. +#[derive(Debug)] +pub struct EncodedExternalTexture { + /// External texture handle. + pub texture_id: TextureId, + /// Source region of the texture in texel coordinates. + pub source_region: RectU16, + /// Sampler parameters. + pub sampler: ImageSampler, + /// Whether the sampled content may contain non-opaque pixels. + pub may_have_transparency: bool, + /// Inverse destination transform, mapping scene coordinates to local source-rect space. + pub transform: Affine, + /// Optional tint applied to the sampled color. + pub tint: Option, +} + +/// Computed properties of a linear gradient. +#[derive(Debug, Copy, Clone)] +pub struct LinearKind; + +/// Focal data for a radial gradient. +#[derive(Debug, PartialEq, Copy, Clone)] +pub struct FocalData { + /// The normalized radius of the outer circle in focal space. + pub fr1: f32, + /// The x-coordinate of the focal point in normalized space \[0,1\]. + pub f_focal_x: f32, + /// Whether the focal points have been swapped. + pub f_is_swapped: bool, +} + +impl FocalData { + /// Create a new `FocalData` with the given radii and update the matrix. + pub fn create(mut r0: f32, mut r1: f32, matrix: &mut Affine) -> Self { + let mut swapped = false; + let mut f_focal_x = r0 / (r0 - r1); + + if (f_focal_x - 1.0).is_nearly_zero() { + *matrix = matrix.then_translate(Vec2::new(-1.0, 0.0)); + *matrix = matrix.then_scale_non_uniform(-1.0, 1.0); + core::mem::swap(&mut r0, &mut r1); + f_focal_x = 0.0; + swapped = true; + } + + let focal_matrix = ts_from_line_to_line( + Point::new(f_focal_x as f64, 0.0), + Point::new(1.0, 0.0), + Point::new(0.0, 0.0), + Point::new(1.0, 0.0), + ); + *matrix = focal_matrix * *matrix; + + let fr1 = r1 / (1.0 - f_focal_x).abs(); + + let data = Self { + fr1, + f_focal_x, + f_is_swapped: swapped, + }; + + if data.is_focal_on_circle() { + *matrix = matrix.then_scale(0.5); + } else { + *matrix = matrix.then_scale_non_uniform( + (fr1 / (fr1 * fr1 - 1.0)) as f64, + 1.0 / (fr1 * fr1 - 1.0).abs().sqrt() as f64, + ); + } + + *matrix = matrix.then_scale((1.0 - f_focal_x).abs() as f64); + + data + } + + /// Whether the focal is on the circle. + pub fn is_focal_on_circle(&self) -> bool { + (1.0 - self.fr1).is_nearly_zero() + } + + /// Whether the focal points have been swapped. + pub fn is_swapped(&self) -> bool { + self.f_is_swapped + } + + /// Whether the gradient is well-behaved. + pub fn is_well_behaved(&self) -> bool { + !self.is_focal_on_circle() && self.fr1 > 1.0 + } + + /// Whether the gradient is natively focal. + pub fn is_natively_focal(&self) -> bool { + self.f_focal_x.is_nearly_zero() + } +} + +/// A radial gradient. +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum RadialKind { + /// A radial gradient, i.e. the start and end center points are the same. + Radial { + /// The `bias` value (from the Skia implementation). + /// + /// It is a correction factor that accounts for the fact that the focal center might not + /// lie on the inner circle (if r0 > 0). + bias: f32, + /// The `scale` value (from the Skia implementation). + /// + /// It is a scaling factor that maps from r0 to r1. + scale: f32, + }, + /// A strip gradient, i.e. the start and end radius are the same. + Strip { + /// The squared value of `scaled_r0` (from the Skia implementation). + scaled_r0_squared: f32, + }, + /// A general, two-point conical gradient. + Focal { + /// The focal data (from the Skia implementation). + focal_data: FocalData, + /// The `fp0` value (from the Skia implementation). + fp0: f32, + /// The `fp1` value (from the Skia implementation). + fp1: f32, + }, +} + +impl RadialKind { + /// Whether the gradient is undefined at any location. + pub fn has_undefined(&self) -> bool { + match self { + Self::Radial { .. } => false, + Self::Strip { .. } => true, + Self::Focal { focal_data, .. } => !focal_data.is_well_behaved(), + } + } +} + +/// Computed properties of a sweep gradient. +#[derive(Debug)] +pub struct SweepKind { + /// The start angle of the sweep gradient. + pub start_angle: f32, + /// The inverse delta between start and end angle. + pub inv_angle_delta: f32, +} + +/// A kind of encoded gradient. +#[derive(Debug)] +pub enum EncodedKind { + /// An encoded linear gradient. + Linear(LinearKind), + /// An encoded radial gradient. + Radial(RadialKind), + /// An encoded sweep gradient. + Sweep(SweepKind), +} + +impl EncodedKind { + /// Whether the gradient is undefined at any location. + fn has_undefined(&self) -> bool { + match self { + Self::Radial(radial_kind) => radial_kind.has_undefined(), + _ => false, + } + } +} + +/// An encoded gradient. +#[derive(Debug)] +pub struct EncodedGradient { + /// The cache key for the gradient. + pub cache_key: CacheKey, + /// The underlying kind of gradient. + pub kind: EncodedKind, + /// Whether the gradient can yield undefined `t` values at some locations. + pub has_undefined: bool, + /// A transform that needs to be applied to the position of the first processed pixel. + pub transform: Affine, + /// How much to advance into the x/y direction for one step in the x direction. + pub x_advance: Vec2, + /// How much to advance into the x/y direction for one step in the y direction. + pub y_advance: Vec2, + /// The color ranges of the gradient. + pub ranges: Vec, + /// The extend of the gradient. + pub extend: Extend, + /// Whether the gradient requires `source_over` compositing. + pub may_have_transparency: bool, + u8_lut: OnceCell>, + f32_lut: OnceCell>, +} + +impl EncodedGradient { + /// Get the lookup table for sampling u8-based gradient values. + pub fn u8_lut(&self, simd: S) -> &GradientLut { + self.u8_lut + .get_or_init(|| GradientLut::new(simd, &self.ranges)) + } + + /// Get the lookup table for sampling f32-based gradient values. + pub fn f32_lut(&self, simd: S) -> &GradientLut { + self.f32_lut + .get_or_init(|| GradientLut::new(simd, &self.ranges)) + } +} + +/// Cache key for gradient color ramps based on color-affecting properties. +#[derive(Debug, Clone)] +pub struct GradientCacheKey { + /// The color stops (offsets + colors). + pub stops: ColorStops, + /// Color space used for interpolation. + pub interpolation_cs: ColorSpaceTag, + /// Hue direction used for interpolation. + pub hue_direction: HueDirection, +} + +impl BitHash for GradientCacheKey { + fn bit_hash(&self, state: &mut H) { + self.stops.bit_hash(state); + core::mem::discriminant(&self.interpolation_cs).hash(state); + core::mem::discriminant(&self.hue_direction).hash(state); + } +} + +impl BitEq for GradientCacheKey { + fn bit_eq(&self, other: &Self) -> bool { + self.stops.bit_eq(&other.stops) + && self.interpolation_cs == other.interpolation_cs + && self.hue_direction == other.hue_direction + } +} + +/// An encoded range between two color stops. +#[derive(Debug, Clone)] +pub struct GradientRange { + /// The end value of the range. + pub x1: f32, + /// A bias to apply when interpolating the color (in this case just the values of the start + /// color of the gradient). + pub bias: [f32; 4], + /// The scale factors of the range. By calculating bias + x * factors (where x is + /// between 0.0 and 1.0), we can interpolate between start and end color of the gradient range. + pub scale: [f32; 4], + /// The alpha space in which the interpolation was performed. + pub interpolation_alpha_space: InterpolationAlphaSpace, +} + +/// An encoded blurred, rounded rectangle. +#[derive(Debug)] +pub struct EncodedBlurredRoundedRectangle { + /// An component for computing the blur effect. + pub exponent: f32, + /// An component for computing the blur effect. + pub recip_exponent: f32, + /// An component for computing the blur effect. + pub scale: f32, + /// An component for computing the blur effect. + pub std_dev_inv: f32, + /// An component for computing the blur effect. + pub min_edge: f32, + /// An component for computing the blur effect. + pub w: f32, + /// An component for computing the blur effect. + pub h: f32, + /// An component for computing the blur effect. + pub width: f32, + /// An component for computing the blur effect. + pub height: f32, + /// An component for computing the blur effect. + pub r1: f32, + /// The base color for the blurred rectangle. + pub color: PremulColor, + /// A transform that needs to be applied to the position of the first processed pixel. + pub transform: Affine, + /// How much to advance into the x/y direction for one step in the x direction. + pub x_advance: Vec2, + /// How much to advance into the x/y direction for one step in the y direction. + pub y_advance: Vec2, +} + +impl private::Sealed for BlurredRoundedRectangle {} + +impl EncodeExt for BlurredRoundedRectangle { + fn encode_into( + &self, + paints: &mut Vec, + transform: Affine, + _tint: Option, + ) -> Paint { + let rect = { + // Ensure rectangle has positive width/height. + let mut rect = self.rect; + + if self.rect.x0 > self.rect.x1 { + core::mem::swap(&mut rect.x0, &mut rect.x1); + } + + if self.rect.y0 > self.rect.y1 { + core::mem::swap(&mut rect.y0, &mut rect.y1); + } + + rect + }; + + let transform = Affine::translate((-rect.x0, -rect.y0)) * transform.inverse(); + + let (x_advance, y_advance) = x_y_advances(&transform); + + let width = rect.width() as f32; + let height = rect.height() as f32; + let radius = self.radius.min(0.5 * width.min(height)); + + // To avoid divide by 0; potentially should be a bigger number for antialiasing. + let std_dev = self.std_dev.max(1e-6); + + let min_edge = width.min(height); + let rmax = 0.5 * min_edge; + let r0 = radius.hypot(std_dev * 1.15).min(rmax); + let r1 = radius.hypot(std_dev * 2.0).min(rmax); + + let exponent = 2.0 * r1 / r0; + + let std_dev_inv = std_dev.recip(); + + // Pull in long end (make less eccentric). + let delta = 1.25 + * std_dev + * (exp(-(0.5 * std_dev_inv * width).powi(2)) + - exp(-(0.5 * std_dev_inv * height).powi(2))); + let w = width + delta.min(0.0); + let h = height - delta.max(0.0); + + let recip_exponent = exponent.recip(); + let scale = 0.5 * compute_erf7(std_dev_inv * 0.5 * (w.max(h) - 0.5 * radius)); + + let encoded = EncodedBlurredRoundedRectangle { + exponent, + recip_exponent, + width, + height, + scale, + r1, + std_dev_inv, + min_edge, + color: PremulColor::from_alpha_color(self.color), + w, + h, + transform, + x_advance, + y_advance, + }; + + let idx = paints.len(); + paints.push(encoded.into()); + + Paint::Indexed(IndexedPaint::new(idx)) + } +} + +/// Calculates the transform necessary to map the line spanned by points src1, src2 to +/// the line spanned by dst1, dst2. +/// +/// This creates a transformation that maps any line segment to any other line segment. +/// For gradients, we use this to transform the gradient line to a standard form (0,0) → (1,0). +/// +/// Copied from +fn ts_from_line_to_line(src1: Point, src2: Point, dst1: Point, dst2: Point) -> Affine { + let unit_to_line1 = unit_to_line(src1, src2); + // Calculate the transform necessary to map line1 to the unit vector. + let line1_to_unit = unit_to_line1.inverse(); + // Then map the unit vector to line2. + let unit_to_line2 = unit_to_line(dst1, dst2); + + unit_to_line2 * line1_to_unit +} + +/// Calculate the transform necessary to map the unit vector to the line spanned by the points +/// `p1` and `p2`. +fn unit_to_line(p0: Point, p1: Point) -> Affine { + Affine::new([ + p1.y - p0.y, + p0.x - p1.x, + p1.x - p0.x, + p1.y - p0.y, + p0.x, + p0.y, + ]) +} + +/// A helper trait for converting a premultiplied f32 color to `Self`. +pub trait FromF32Color: Sized + Debug + Copy + Clone { + /// The zero value. + const ZERO: Self; + /// Convert from a premultiplied f32 color to `Self`. + fn from_f32(color: f32x4) -> [Self; 4]; +} + +impl FromF32Color for f32 { + const ZERO: Self = 0.0; + + fn from_f32(color: f32x4) -> [Self; 4] { + color.into() + } +} + +impl FromF32Color for u8 { + const ZERO: Self = 0; + + fn from_f32(mut color: f32x4) -> [Self; 4] { + let simd = color.simd; + color = color.mul_add(f32x4::splat(simd, 255.0), f32x4::splat(simd, 0.5)); + + [ + color[0] as Self, + color[1] as Self, + color[2] as Self, + color[3] as Self, + ] + } +} + +/// A lookup table for sampled gradient values. +#[derive(Debug)] +pub struct GradientLut { + lut: Vec<[T; 4]>, + scale: f32, +} + +impl GradientLut { + /// Create a new lookup table. + fn new(simd: S, ranges: &[GradientRange]) -> Self { + let lut_size = determine_lut_size(ranges); + let mut lut = vec![[T::ZERO; 4]; lut_size]; + + // Calculate how many indices are covered by each range. + let ramps = { + let mut ramps = Vec::with_capacity(ranges.len()); + let mut prev_idx = 0; + + for range in ranges { + let max_idx = (range.x1 * lut_size as f32) as usize; + + ramps.push((prev_idx..max_idx, range)); + prev_idx = max_idx; + } + + ramps + }; + + let scale = lut_size as f32 - 1.0; + + let inv_lut_scale = f32x4::splat(simd, 1.0 / scale); + let add_factor = f32x4::from_slice(simd, &[0.0, 1.0, 2.0, 3.0]) * inv_lut_scale; + + for (ramp_range, range) in ramps { + let biases = f32x16::block_splat(f32x4::from_slice(simd, &range.bias)); + let scales = f32x16::block_splat(f32x4::from_slice(simd, &range.scale)); + + ramp_range.clone().step_by(4).for_each(|idx| { + let t_vals = f32x4::splat(simd, idx as f32).mul_add(inv_lut_scale, add_factor); + + let t_vals = element_wise_splat(simd, t_vals); + + let mut result = scales.mul_add(t_vals, biases); + let alphas = result.splat_4th(); + // Premultiply colors, since we did interpolation in unpremultiplied space. + if range.interpolation_alpha_space == InterpolationAlphaSpace::Unpremultiplied { + result = { + let mask = + mask32x16::block_splat(mask32x4::from_slice(simd, &[-1, -1, -1, 0])); + simd.select_f32x16(mask, result * alphas, alphas) + }; + } + + // Due to floating-point impreciseness, it can happen that + // values either become greater than 1 or the RGB channels + // become greater than the alpha channel. To prevent overflows + // in later parts of the pipeline, we need to take the minimum here. + result = result.min(1.0).min(alphas); + let (im1, im2) = simd.split_f32x16(result); + let (r1, r2) = simd.split_f32x8(im1); + let (r3, r4) = simd.split_f32x8(im2); + let rs = [r1, r2, r3, r4].map(T::from_f32); + + // We always compute 4 samples at a time, but a gradient ramp does not necessarily + // start at a multiple of 4, therefore we might have to truncate. + let lut = &mut lut[idx..(idx + 4).min(lut_size)]; + lut.copy_from_slice(&rs[..lut.len()]); + }); + } + + Self { lut, scale } + } + + /// Get the sample value at a specific index. + #[inline(always)] + pub fn get(&self, idx: usize) -> [T; 4] { + self.lut[idx] + } + + /// Return the raw array of gradient sample values. + #[inline(always)] + pub fn lut(&self) -> &[[T; 4]] { + &self.lut + } + + /// Return the number of entries in the lookup table. + #[inline(always)] + pub fn width(&self) -> usize { + self.lut.len() + } + + /// Get the scale factor by which to scale the parametric value to + /// compute the correct lookup index. + #[inline(always)] + pub fn scale_factor(&self) -> f32 { + self.scale + } +} + +/// The maximum size of the gradient LUT. +// Of course in theory we could still have a stop at 0.0001 in which case this resolution +// wouldn't be enough, but for all intents and purposes this should be more than sufficient +// for most real cases. +pub const MAX_GRADIENT_LUT_SIZE: usize = 4096; + +fn determine_lut_size(ranges: &[GradientRange]) -> usize { + // Inspired by Blend2D. + // By default: + // 256 for 2 stops. + // 512 for 3 stops. + // 1024 for 4 or more stops. + let stop_len = match ranges.len() { + 1 => 256, + 2 => 512, + _ => 1024, + }; + + // In case we have some tricky stops (for example 3 stops with 0.0, 0.001, 1.0), we might + // increase the resolution. + let mut last_x1 = 0.0; + let mut min_size = 0; + + for x1 in ranges.iter().map(|e| e.x1) { + // For example, if the first stop is at 0.001, then we need a resolution of at least 1000 + // so that we can still safely capture the first stop. + let res = ((1.0 / (x1 - last_x1)).ceil() as usize) + .min(MAX_GRADIENT_LUT_SIZE) + .next_power_of_two(); + min_size = min_size.max(res); + last_x1 = x1; + } + + // Take the maximum of both, but don't exceed `MAX_LEN`. + stop_len.max(min_size) +} + +mod private { + #[expect(unnameable_types, reason = "Sealed trait pattern.")] + pub trait Sealed {} + + impl Sealed for super::Gradient {} +} + +#[cfg(test)] +mod tests { + use super::{EncodeExt, Gradient}; + use crate::color::DynamicColor; + use crate::color::palette::css::{BLACK, BLUE, GREEN}; + use crate::kurbo::{Affine, Point}; + use crate::peniko::{ColorStop, ColorStops}; + use alloc::vec; + use peniko::{LinearGradientPosition, RadialGradientPosition}; + use smallvec::smallvec; + + #[test] + fn gradient_missing_stops() { + let mut buf = vec![]; + + let gradient = Gradient { + kind: LinearGradientPosition { + start: Point::new(0.0, 0.0), + end: Point::new(20.0, 0.0), + } + .into(), + ..Default::default() + }; + + assert_eq!( + gradient.encode_into(&mut buf, Affine::IDENTITY, None), + BLACK.into() + ); + } + + #[test] + fn gradient_one_stop() { + let mut buf = vec![]; + + let gradient = Gradient { + kind: LinearGradientPosition { + start: Point::new(0.0, 0.0), + end: Point::new(20.0, 0.0), + } + .into(), + stops: ColorStops(smallvec![ColorStop { + offset: 0.0, + color: DynamicColor::from_alpha_color(GREEN), + }]), + ..Default::default() + }; + + // Should return the color of the first stop. + assert_eq!( + gradient.encode_into(&mut buf, Affine::IDENTITY, None), + GREEN.into() + ); + } + + #[test] + fn gradient_not_sorted_stops() { + let mut buf = vec![]; + + let gradient = Gradient { + kind: LinearGradientPosition { + start: Point::new(0.0, 0.0), + end: Point::new(20.0, 0.0), + } + .into(), + stops: ColorStops(smallvec![ + ColorStop { + offset: 1.0, + color: DynamicColor::from_alpha_color(GREEN), + }, + ColorStop { + offset: 0.0, + color: DynamicColor::from_alpha_color(BLUE), + }, + ]), + ..Default::default() + }; + + assert_eq!( + gradient.encode_into(&mut buf, Affine::IDENTITY, None), + GREEN.into() + ); + } + + #[test] + fn gradient_linear_degenerate() { + let mut buf = vec![]; + + let gradient = Gradient { + kind: LinearGradientPosition { + start: Point::new(0.0, 0.0), + end: Point::new(0.0, 0.0), + } + .into(), + stops: ColorStops(smallvec![ + ColorStop { + offset: 0.0, + color: DynamicColor::from_alpha_color(GREEN), + }, + ColorStop { + offset: 1.0, + color: DynamicColor::from_alpha_color(BLUE), + }, + ]), + ..Default::default() + }; + + assert_eq!( + gradient.encode_into(&mut buf, Affine::IDENTITY, None), + GREEN.into() + ); + } + + #[test] + fn gradient_last_stop_with_infinity_offset() { + let mut buf = vec![]; + + let gradient = Gradient { + kind: LinearGradientPosition { + start: Point::new(0.0, 0.0), + end: Point::new(20.0, 0.0), + } + .into(), + stops: ColorStops(smallvec![ + ColorStop { + offset: 0.0, + color: DynamicColor::from_alpha_color(GREEN), + }, + ColorStop { + offset: f32::INFINITY, + color: DynamicColor::from_alpha_color(BLUE), + }, + ]), + ..Default::default() + }; + + // Invalid gradient, so fall back to first color. + assert_eq!( + gradient.encode_into(&mut buf, Affine::IDENTITY, None), + GREEN.into() + ); + } + + #[test] + fn gradient_stop_with_nan_offset() { + let mut buf = vec![]; + + let gradient = Gradient { + kind: LinearGradientPosition { + start: Point::new(0.0, 0.0), + end: Point::new(20.0, 0.0), + } + .into(), + stops: ColorStops(smallvec![ + ColorStop { + offset: 0.0, + color: DynamicColor::from_alpha_color(GREEN), + }, + ColorStop { + offset: f32::NAN, + color: DynamicColor::from_alpha_color(BLUE), + }, + ]), + ..Default::default() + }; + + // Invalid gradient, so fall back to first color. + assert_eq!( + gradient.encode_into(&mut buf, Affine::IDENTITY, None), + GREEN.into() + ); + } + + #[test] + fn gradient_radial_degenerate() { + let mut buf = vec![]; + + let gradient = Gradient { + kind: RadialGradientPosition { + start_center: Point::new(0.0, 0.0), + start_radius: 20.0, + end_center: Point::new(0.0, 0.0), + end_radius: 20.0, + } + .into(), + stops: ColorStops(smallvec![ + ColorStop { + offset: 0.0, + color: DynamicColor::from_alpha_color(GREEN), + }, + ColorStop { + offset: 1.0, + color: DynamicColor::from_alpha_color(BLUE), + }, + ]), + ..Default::default() + }; + + assert_eq!( + gradient.encode_into(&mut buf, Affine::IDENTITY, None), + GREEN.into() + ); + } +} diff --git a/vendor/vello_common/src/filter/drop_shadow.rs b/vendor/vello_common/src/filter/drop_shadow.rs new file mode 100644 index 000000000..5260225c9 --- /dev/null +++ b/vendor/vello_common/src/filter/drop_shadow.rs @@ -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, + /// 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, + ) -> 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) +} diff --git a/vendor/vello_common/src/filter/flood.rs b/vendor/vello_common/src/filter/flood.rs new file mode 100644 index 000000000..638525a66 --- /dev/null +++ b/vendor/vello_common/src/filter/flood.rs @@ -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, +} + +impl Flood { + /// Create a new flood filter with the specified color. + pub fn new(color: AlphaColor) -> Self { + Self { color } + } +} diff --git a/vendor/vello_common/src/filter/gaussian_blur.rs b/vendor/vello_common/src/filter/gaussian_blur.rs new file mode 100644 index 000000000..e31bfd509 --- /dev/null +++ b/vendor/vello_common/src/filter/gaussian_blur.rs @@ -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)); + } +} diff --git a/vendor/vello_common/src/filter/mod.rs b/vendor/vello_common/src/filter/mod.rs new file mode 100644 index 000000000..4da635d61 --- /dev/null +++ b/vendor/vello_common/src/filter/mod.rs @@ -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) +} diff --git a/vendor/vello_common/src/filter/offset.rs b/vendor/vello_common/src/filter/offset.rs new file mode 100644 index 000000000..3ac7d5a27 --- /dev/null +++ b/vendor/vello_common/src/filter/offset.rs @@ -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 } + } +} diff --git a/vendor/vello_common/src/filter_effects.rs b/vendor/vello_common/src/filter_effects.rs new file mode 100644 index 000000000..40d7f82b9 --- /dev/null +++ b/vendor/vello_common/src/filter_effects.rs @@ -0,0 +1,1089 @@ +// Copyright 2025 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Filter effects API based on the W3C Filter Effects specification. +//! +//! This module provides a comprehensive filter system supporting both high-level +//! CSS filter functions and low-level SVG filter primitives. The API is designed +//! to follow the W3C Filter Effects Module Level 1 specification. +//! +//! See: +//! +//! ## Implementation Status +//! +//! ### ✅ Implemented +//! +//! **Filter Functions:** +//! - `Blur` - Gaussian blur effect +//! +//! **Filter Primitives (Single Use Only):** +//! - `Flood` - Solid color fill +//! - `GaussianBlur` - Gaussian blur filter +//! - `DropShadow` - Drop shadow effect (compound primitive) +//! - `Offset` - Translation/shift (single primitive) +//! +//! **Note:** Currently only single primitive filters are supported. Filter graphs with +//! multiple connected primitives are not yet implemented. +//! +//! ### 🚧 Not Yet Implemented +//! +//! **Core Features:** +//! - `FilterGraph` execution - Chaining multiple filter primitives together +//! - `FilterInputs` - Connecting primitives to create complex effects +//! +//! **Filter Functions:** +//! - `Brightness`, `Contrast`, `Grayscale`, `HueRotate`, `Invert`, +//! `Opacity`, `Saturate`, `Sepia` +//! +//! **Filter Primitives:** +//! - `ColorMatrix` - Matrix-based color transformation +//! - `Composite` - Porter-Duff compositing operations +//! - `Blend` - Blend mode operations +//! - `Morphology` - Dilate/erode operations +//! - `ConvolveMatrix` - Custom convolution kernels +//! - `Turbulence` - Perlin noise generation +//! - `DisplacementMap` - Pixel displacement +//! - `ComponentTransfer` - Per-channel transfer functions +//! - `Image` - External image reference +//! - `Tile` - Tiling operation +//! - `DiffuseLighting`, `SpecularLighting` - Lighting effects + +use crate::color::{AlphaColor, Srgb}; +use crate::kurbo::{Affine, Rect}; +use alloc::sync::Arc; +use alloc::vec::Vec; +use smallvec::SmallVec; + +/// The main filter system. +/// +/// A filter combines a graph of filter primitives with optional spatial bounds. +/// If bounds are specified, the filter only applies within that region. +#[derive(Debug, Clone, PartialEq)] +pub struct Filter { + /// Filter graph defining the effect pipeline. + pub graph: Arc, + // TODO: Add bounds restricting where the filter applies. + // Optional bounds restricting where the filter applies. + // If `None`, the filter applies to the entire filtered element. + // pub bounds: Option, +} + +impl Filter { + /// Create a simple filter system from a filter function. + /// + /// Converts a high-level CSS-style filter function into a filter graph. + /// Use this for simple effects like blur, brightness, etc. + pub fn from_function(function: FilterFunction) -> Self { + // Convert function to primitive + let primitive = match function { + FilterFunction::Blur { radius } => FilterPrimitive::GaussianBlur { + std_deviation: radius, + edge_mode: EdgeMode::default(), + }, + _ => unimplemented!("Filter function {:?} not supported", function), + }; + + Self::from_primitive(primitive) + } + + /// Create a filter system from a filter primitive. + /// + /// Creates a simple filter graph with a single primitive. + /// Use this for direct access to low-level SVG filter operations. + pub fn from_primitive(primitive: FilterPrimitive) -> Self { + let mut graph = FilterGraph::new(); + let filter_id = graph.add(primitive, None); + graph.set_output(filter_id); + + Self { + graph: Arc::new(graph), + } + } + + /// Calculate the bounds expansion for this filter in pixel/device space. + /// + /// Returns a `Rect` representing how many extra pixels are needed around the + /// filtered region to correctly compute the filter effect. For example, a blur + /// filter needs to sample beyond the original bounds to avoid edge artifacts. + /// + /// The expansion accounts for the transform (rotation, scale, and shear) to compute + /// the correct axis-aligned bounding box expansion in device space. + /// + /// The returned rect is centered at origin: + /// - x0: negative left expansion (in pixels) + /// - y0: negative top expansion (in pixels) + /// - x1: positive right expansion (in pixels) + /// - y1: positive bottom expansion (in pixels) + /// + /// # Arguments + /// * `transform` - The transform applied to this filter layer + pub fn bounds_expansion(&self, transform: &Affine) -> Rect { + let [a, b, c, d, _e, _f] = transform.as_coeffs(); + let linear_only = Affine::new([a, b, c, d, 0.0, 0.0]); + + self.graph.bounds_expansion(&linear_only) + } +} + +/// A directed acyclic graph (DAG) of filter operations. +/// +/// The graph represents a pipeline of filter primitives where outputs of some +/// primitives can be used as inputs to others. Each primitive has a unique `FilterId`. +#[derive(Debug, Clone, PartialEq)] +pub struct FilterGraph { + /// All filter primitives in the graph, stored in insertion order. + pub primitives: SmallVec<[FilterPrimitive; 1]>, + /// The final output filter ID whose result is the output of this graph. + pub output: FilterId, + /// Next available filter ID (monotonically increasing counter). + next_id: u16, + /// Accumulated bounds expansion from all primitives in the graph, cached in user space. + /// This is the axis-aligned bounding box of the expansion region (centered at origin), + /// which can be transformed to device space when needed. + expansion_rect: Rect, +} + +impl Default for FilterGraph { + fn default() -> Self { + Self::new() + } +} + +impl FilterGraph { + /// Create a new empty filter graph. + pub fn new() -> Self { + Self { + primitives: SmallVec::new(), + output: FilterId(0), + next_id: 0, + expansion_rect: Rect::ZERO, + } + } + + /// Add a filter primitive with optional inputs. + /// + /// Returns a `FilterId` that can be referenced by other primitives. + /// Automatically updates the accumulated bounds expansion based on the primitive's requirements. + pub fn add(&mut self, primitive: FilterPrimitive, _inputs: Option) -> FilterId { + let id = FilterId(self.next_id); + self.next_id += 1; + + // Update accumulated expansion by taking the union of rects + let primitive_rect = primitive.expansion_rect(); + self.expansion_rect = self.expansion_rect.union(primitive_rect); + + self.primitives.push(primitive); + + id + } + + /// Set the output filter for the graph. + pub fn set_output(&mut self, output: FilterId) { + self.output = output; + } + + /// Get the accumulated bounds expansion for all primitives in this graph. + /// + /// This returns the expansion required by all primitives in the graph, + /// representing the padding needed to render all filter effects correctly. + /// + /// The expansion accounts for the transform (rotation, scale, and shear) to compute + /// the correct axis-aligned bounding box expansion in device space. + /// + /// # Arguments + /// * `transform` - The transform applied to this filter layer + pub fn bounds_expansion(&self, transform: &Affine) -> Rect { + // Transform the cached expansion rect to device space + // transform_rect_bbox computes the axis-aligned bounding box of the transformed rect + transform.transform_rect_bbox(self.expansion_rect) + } +} + +/// All possible filter effects. +/// +/// This enum allows choosing between high-level filter functions (simple CSS-style effects) +/// and low-level filter primitives (complex SVG-style effects with full control). +/// Use `FilterFunction` for common effects like blur, and `FilterPrimitive` for +/// advanced composition and custom filter graphs. +#[derive(Debug, Clone)] +pub enum FilterEffect { + /// Simple, high-level filter functions. + Function(FilterFunction), + /// Low-level filter primitives (granular control). + Primitive(FilterPrimitive), +} + +/// High-level filter functions for common effects (CSS filter functions). +/// +/// These match the CSS Filter Effects specification and provide simple, +/// commonly-used visual effects without needing to construct a filter graph. +/// +/// See: +#[derive(Debug, Clone)] +pub enum FilterFunction { + /// Gaussian blur effect. + /// + /// Applies a Gaussian blur to the input image. Larger radius values + /// produce more blur. The blur is applied equally in all directions. + /// + /// Note: Per the W3C Filter Effects specification, this `radius` parameter + /// represents the standard deviation (σ) of the Gaussian function, not the + /// effective blur range. The effective blur range is approximately 3× this value. + Blur { + /// Standard deviation of the Gaussian blur in pixels. Must be non-negative. + /// A value of 0 means no blur. + /// + /// Despite being called "radius" (to match CSS filter syntax), this is + /// actually the standard deviation. The visible blur effect extends + /// approximately 3 times this value in each direction. + radius: f32, + }, + // + // ============================================================ + // TODO: The following filter functions are not yet implemented + // ============================================================ + // + /// Brightness adjustment. + /// + /// Adjusts the brightness of the input image using a linear multiplier. + Brightness { + /// Brightness amount: 0.0 = completely black, 1.0 = no change, >1.0 = brighter. + /// Must be non-negative. + amount: f32, + }, + /// Contrast adjustment. + /// + /// Adjusts the contrast of the input image. + Contrast { + /// Contrast amount: 0.0 = uniform gray, 1.0 = no change, >1.0 = higher contrast. + /// Must be non-negative. + amount: f32, + }, + /// Grayscale conversion. + /// + /// Converts the input to grayscale. Amount controls the strength of the conversion. + Grayscale { + /// Grayscale amount: 0.0 = original colors, 1.0 = full grayscale. + /// Values should be in range [0.0, 1.0]. + amount: f32, + }, + /// Hue rotation. + /// + /// Rotates the hue of all colors in the input image by the specified angle. + HueRotate { + /// Rotation angle in degrees. Can be negative. + /// 0° = no change, 180° = opposite hue, 360° = back to original. + angle: f32, + }, + /// Color inversion. + /// + /// Inverts the colors of the input image. + Invert { + /// Inversion amount: 0.0 = original colors, 1.0 = fully inverted. + /// Values should be in range [0.0, 1.0]. + amount: f32, + }, + /// Opacity adjustment. + /// + /// Multiplies the alpha channel by the specified amount. + Opacity { + /// Opacity amount: 0.0 = fully transparent, 1.0 = no change. + /// Values should be in range [0.0, 1.0]. + amount: f32, + }, + /// Saturation adjustment. + /// + /// Adjusts the color saturation of the input image. + Saturate { + /// Saturation amount: 0.0 = completely desaturated (grayscale), + /// 1.0 = no change, >1.0 = oversaturated. + /// Must be non-negative. + amount: f32, + }, + /// Sepia tone effect. + /// + /// Applies a sepia tone effect (vintage/old photo appearance). + Sepia { + /// Sepia amount: 0.0 = original colors, 1.0 = full sepia tone. + /// Values should be in range [0.0, 1.0]. + amount: f32, + }, +} + +/// Edge mode for filter operations. +/// +/// Determines how to extend the input image when filter operations require sampling +/// beyond the original image boundaries. This is particularly important for blur and +/// convolution operations near edges. +/// +/// See: +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum EdgeMode { + /// Extend by duplicating edge pixels (clamp to edge). + /// + /// The input image is extended along each border by replicating the color values + /// at the given edge of the input image. This prevents dark halos around edges. + Duplicate, + /// Extend by wrapping to the opposite edge (repeat/tile). + /// + /// The input image is extended by taking color values from the opposite edge, + /// creating a tiling effect. + Wrap, + /// Extend by mirroring across the edge. + /// + /// The input image is extended by taking color values mirrored across the edge. + /// This creates seamless continuation at boundaries. + Mirror, + /// Extend with transparent black (zeros). + /// + /// The input image is extended with pixel values of zero for R, G, B and A. + /// This is the default and most common mode, creating natural fade-to-transparent edges. + #[default] + None, +} + +/// Low-level filter primitives for granular control (SVG filter primitives). +/// +/// These are the building blocks for complex filter effects, corresponding to SVG +/// filter primitives. They can be combined in a `FilterGraph` to create sophisticated +/// visual effects. +/// +/// See: +#[derive(Debug, Clone, PartialEq)] +pub enum FilterPrimitive { + /// Generate a solid color fill. + /// + /// Creates a rectangle filled with the specified color, typically used as + /// input to other filter operations (e.g., for colored shadows). + Flood { + /// Fill color with alpha channel. + color: AlphaColor, + }, + /// Gaussian blur filter. + /// + /// Applies a Gaussian blur using the specified standard deviation (σ). + /// The effective blur range (distance over which pixels are sampled) is + /// approximately 3 × `std_deviation`, as this captures ~99.7% of the + /// Gaussian distribution. + GaussianBlur { + /// Standard deviation for the blur kernel. Larger values create more blur. + /// Must be non-negative. A value of 0 means no blur. + /// + /// This directly corresponds to the σ (sigma) parameter in the Gaussian + /// function. The visible blur effect extends approximately 3σ in each direction. + /// + /// TODO: Per the W3C specification, this should support separate x and y values. + /// The spec allows `stdDeviation` to be either one number (applied to both axes) + /// or two numbers (first for x-axis, second for y-axis). Currently only uniform + /// blur is supported. Consider changing to `(f32, f32)` or a dedicated type. + std_deviation: f32, + /// Edge mode determining how pixels beyond the input bounds are handled. + edge_mode: EdgeMode, + }, + /// Drop shadow effect (compound primitive). + /// + /// Creates a drop shadow by blurring the input's alpha channel, offsetting it, + /// and compositing it with the original. This is a compound operation that + /// combines multiple primitive operations into one. + /// + /// See: + DropShadow { + /// Horizontal offset of the shadow in pixels. Positive values shift right. + dx: f32, + /// Vertical offset of the shadow in pixels. Positive values shift down. + dy: f32, + /// Blur standard deviation for the shadow. Larger values create softer shadows. + std_deviation: f32, + /// Shadow color with alpha channel. Alpha controls shadow opacity. + color: AlphaColor, + /// Edge mode for handling boundaries during blur operation. + /// Default is `EdgeMode::None` per SVG spec. + edge_mode: EdgeMode, + }, + // + // ============================================================ + // TODO: The following filter primitives are not yet implemented + // ============================================================ + // + /// Matrix-based color transformation. + /// + /// Applies a 4x5 matrix transformation to colors, allowing arbitrary + /// color space transformations, hue shifts, and color adjustments. + ColorMatrix { + /// 4x5 color transformation matrix: 4 rows (R,G,B,A) × 5 columns (R,G,B,A,offset). + /// Each output channel is computed as a linear combination of input channels plus offset. + matrix: [f32; 20], + }, + /// Geometric offset/translation. + /// + /// Shifts the input image by the specified offset. Useful for creating + /// shadow effects or positioning elements in a filter graph. + Offset { + /// Horizontal offset in pixels. Positive values shift right. + dx: f32, + /// Vertical offset in pixels. Positive values shift down. + dy: f32, + }, + + /// Composite two inputs using Porter-Duff compositing operations. + /// + /// Combines two input images using standard compositing operators + /// (over, in, out, atop, xor) or custom arithmetic combination. + Composite { + /// Porter-Duff compositing operator to apply. + operator: CompositeOperator, + }, + /// Blend two inputs using blend modes. + /// + /// Combines two input images using Photoshop-style blend modes + /// (multiply, screen, overlay, etc.). + Blend { + /// Blend mode determining how colors are combined. + mode: BlendMode, + }, + /// Morphological operations (dilate/erode). + /// + /// Expands (dilate) or contracts (erode) the shapes in the input image. + /// Useful for creating outline effects or cleaning up edges. + Morphology { + /// Morphological operator determining whether to erode or dilate. + operator: MorphologyOperator, + /// Operation radius in pixels. Larger values create stronger effects. + radius: f32, + }, + /// Custom convolution kernel for image processing. + /// + /// Applies a custom convolution matrix to the input image, enabling + /// effects like sharpening, edge detection, embossing, and custom filters. + ConvolveMatrix { + /// Convolution kernel specification including size, values, and normalization. + kernel: ConvolutionKernel, + }, + /// Generate Perlin noise/turbulence patterns. + /// + /// Creates procedural noise patterns useful for textures, clouds, + /// marble effects, and other organic-looking randomness. + Turbulence { + /// Base frequency for noise generation. Higher values create finer detail. + base_frequency: f32, + /// Number of octaves for fractal noise. More octaves add finer detail. + num_octaves: u32, + /// Random seed for reproducible noise generation. + seed: u32, + /// Type of noise: smooth fractal or more chaotic turbulence. + turbulence_type: TurbulenceType, + }, + /// Displace pixels using a displacement map. + /// + /// Uses the color values from a second input to spatially displace pixels + /// in the primary input, creating warping and distortion effects. + DisplacementMap { + /// Scale factor controlling the displacement intensity. + scale: f32, + /// Color channel from the displacement map used for X-axis displacement. + x_channel: ColorChannel, + /// Color channel from the displacement map used for Y-axis displacement. + y_channel: ColorChannel, + }, + /// Per-channel component transfer using lookup tables or functions. + /// + /// Applies independent transfer functions to each color channel, + /// enabling color corrections, gamma adjustments, and custom mappings. + ComponentTransfer { + /// Transfer function applied to the red channel (None = identity). + red_function: Option, + /// Transfer function applied to the green channel (None = identity). + green_function: Option, + /// Transfer function applied to the blue channel (None = identity). + blue_function: Option, + /// Transfer function applied to the alpha channel (None = identity). + alpha_function: Option, + }, + /// Reference an external image as filter input. + /// + /// Allows using pre-existing images (from an atlas or resource) as + /// input to filter operations, useful for texturing and overlays. + Image { + /// Identifier referencing an image in the resource atlas. + image_id: u32, + /// Optional 2D affine transformation matrix [a, b, c, d, e, f]. + /// Transforms the image before using it as filter input. + transform: Option<[f32; 6]>, + }, + /// Tile the input to fill the filter region. + /// + /// Repeats the input image to fill the entire filter primitive subregion, + /// creating a tiling/repeating pattern. + Tile, + /// Diffuse lighting simulation. + /// + /// Creates a lighting effect by treating the input's alpha channel as a height map + /// and calculating diffuse (matte) reflection from a light source. + DiffuseLighting { + /// Surface scale factor for converting alpha values to heights. + surface_scale: f32, + /// Diffuse reflection constant (kd). Controls lighting intensity. + diffuse_constant: f32, + /// Kernel unit length for gradient calculations in user space. + kernel_unit_length: f32, + /// Configuration of the light source (point, distant, or spot). + light_source: LightSource, + }, + /// Specular lighting simulation. + /// + /// Creates a lighting effect by treating the input's alpha channel as a height map + /// and calculating specular (shiny) reflection highlights from a light source. + SpecularLighting { + /// Surface scale factor for converting alpha values to heights. + surface_scale: f32, + /// Specular reflection constant (ks). Controls highlight intensity. + specular_constant: f32, + /// Specular reflection exponent. Controls highlight sharpness (higher = sharper). + specular_exponent: f32, + /// Kernel unit length for gradient calculations in user space. + kernel_unit_length: f32, + /// Configuration of the light source (point, distant, or spot). + light_source: LightSource, + }, +} + +impl FilterPrimitive { + /// Calculate the bounds expansion as a `Rect` in user space. + /// + /// Returns a rectangle centered at the origin representing how much the filter + /// expands the processing region in each direction. The rect coordinates are: + /// - x0: negative left expansion + /// - y0: negative top expansion + /// - x1: positive right expansion + /// - y1: positive bottom expansion + /// + /// A `Rect::ZERO` means no expansion. This representation allows the expansion + /// to be correctly transformed (including rotation) using standard rect transforms. + /// + /// For example, a blur filter needs additional pixels around the edges (3*sigma). + /// Most filters that don't sample neighboring pixels return `Rect::ZERO`. + pub fn expansion_rect(&self) -> Rect { + match self { + Self::GaussianBlur { std_deviation, .. } => { + // Gaussian blur expands uniformly by 3*sigma (covers 99.7% of distribution) + let radius = (*std_deviation * 3.0) as f64; + Rect::new(-radius, -radius, radius, radius) + } + Self::Offset { dx, dy } => { + // Offset shifts pixels; expand bounds asymmetrically so shifted content isn't cut. + let dx = *dx as f64; + let dy = *dy as f64; + Rect::new(dx.min(0.0), dy.min(0.0), dx.max(0.0), dy.max(0.0)) + } + Self::DropShadow { + std_deviation, + dx, + dy, + .. + } => { + // Drop shadow = blur + offset + composite with original + // The expansion rect encompasses both the blur and the offset + let blur_radius = (*std_deviation * 3.0) as f64; + let dx = *dx as f64; + let dy = *dy as f64; + + Rect::new( + -(blur_radius + (-dx).max(0.0)), + -(blur_radius + (-dy).max(0.0)), + blur_radius + dx.max(0.0), + blur_radius + dy.max(0.0), + ) + } + // Most other filters don't expand bounds + _ => Rect::ZERO, + } + } +} + +#[cfg(test)] +mod offset_expansion_tests { + use super::FilterPrimitive; + use crate::kurbo::Rect; + + #[test] + fn offset_expands_in_direction_of_shift() { + let p = FilterPrimitive::Offset { dx: 2.5, dy: -3.0 }; + assert_eq!( + p.expansion_rect(), + Rect::new(0.0, -3.0, 2.5, 0.0), + "Offset expansion should be asymmetric and include the shift vector" + ); + } +} + +/// Unique identifier for a filter primitive in the graph. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct FilterId(pub u16); + +/// Input connections for a filter primitive. +#[derive(Debug, Clone, PartialEq)] +pub struct FilterInputs { + /// Primary input ("in" attribute in SVG). + pub primary: FilterInput, + /// Secondary input ("in2" attribute in SVG, for composite/blend operations). + pub secondary: Option, +} + +impl FilterInputs { + /// Create filter inputs with a single input. + /// + /// Use this for primitives that operate on a single source (blur, color matrix, etc.). + pub fn single(input: FilterInput) -> Self { + Self { + primary: input, + secondary: None, + } + } + + /// Create filter inputs with two inputs (for composite, blend, etc.). + /// + /// Use this for primitives that combine two sources (composite, blend, displacement map, etc.). + pub fn dual(input1: FilterInput, input2: FilterInput) -> Self { + Self { + primary: input1, + secondary: Some(input2), + } + } +} + +/// A single filter input. +#[derive(Debug, Clone, PartialEq)] +pub enum FilterInput { + /// Input from a source (`SourceGraphic`, `SourceAlpha`, etc.). + Source(FilterSource), + /// Input from another filter's result. + Result(FilterId), +} + +/// Filter input sources. +/// +/// Defines the various built-in sources that can be used as filter inputs, +/// matching the SVG filter primitive input types. These represent implicit +/// inputs available to any filter primitive without requiring previous operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FilterSource { + /// The original graphic content being filtered. + /// + /// This is the default input - the rendered result of the element + /// the filter is applied to, including all its fill, stroke, and content. + SourceGraphic, + /// Alpha channel only of the original graphic. + /// + /// Useful for creating effects based on shape/transparency, such as + /// shadows that follow the element's outline. + SourceAlpha, + /// Background image content behind the filtered element. + /// + /// Allows filters to incorporate or blend with content behind the element. + /// Not always available depending on the rendering context. + BackgroundImage, + /// Alpha channel only of the background image. + /// + /// The transparency mask of the background content. + BackgroundAlpha, + /// The fill paint of the element as an image input. + /// + /// For elements with gradient or pattern fills, this provides access + /// to the fill as a filter input. + FillPaint, + /// The stroke paint of the element as an image input. + /// + /// For elements with gradient or pattern strokes, this provides access + /// to the stroke as a filter input. + StrokePaint, +} + +/// Pre-built compound effects for common use cases. +/// +/// These effects combine multiple filter primitives into commonly-used visual effects. +/// They provide a convenient high-level API for complex multi-step filter operations. +/// +/// **Note:** These are planned but not yet implemented. Use `FilterGraph` to manually +/// construct these effects from primitives. +#[derive(Debug, Clone)] +pub enum CompoundFilter { + /// Inner shadow effect (shadow inside the shape). + /// + /// Creates a shadow that appears inside the boundaries of the shape, + /// giving a recessed or inset appearance. This is the opposite of a drop shadow. + InnerShadow { + /// Horizontal offset of the shadow in pixels. Positive values shift right. + dx: f32, + /// Vertical offset of the shadow in pixels. Positive values shift down. + dy: f32, + /// Blur radius for the shadow in pixels. Larger values create softer shadows. + blur: f32, + /// Shadow color with alpha channel. + color: AlphaColor, + }, + /// Glow effect around the shape. + /// + /// Creates a soft glowing halo around the shape by blurring and + /// compositing a colored version with the original. + Glow { + /// Blur radius for the glow in pixels. Larger values create softer glows. + blur: f32, + /// Glow color with alpha channel. + color: AlphaColor, + }, + /// Bevel effect (3D raised/recessed appearance). + /// + /// Creates a 3D beveled edge effect using lighting simulation, + /// making the shape appear raised or recessed from the surface. + Bevel { + /// Light source angle in degrees (0° = right, 90° = up). + angle: f32, + /// Width of the bevel edge in pixels. + distance: f32, + /// Color for the highlight (lit) side of the bevel. + highlight: AlphaColor, + /// Color for the shadow (dark) side of the bevel. + shadow: AlphaColor, + }, + /// Emboss effect for a raised relief appearance. + /// + /// Creates an embossed/stamped appearance by simulating lighting + /// on a raised surface based on the shape's alpha channel. + Emboss { + /// Light angle in degrees determining emboss direction. + angle: f32, + /// Depth of the emboss effect. + depth: f32, + /// Overall strength/intensity of the effect (0.0 = none, 1.0 = full). + amount: f32, + }, +} + +/// Composite operators for combining filter inputs. +/// +/// These are the Porter-Duff compositing operators used to combine two images. +/// Each operator defines how the source (input 1) and destination (input 2) +/// are combined based on their color and alpha values. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum CompositeOperator { + /// Source over destination (standard alpha blending). + /// + /// The source is composited over the destination. This is the most common + /// blending mode where source alpha determines visibility. + Over, + /// Source in destination (intersection). + /// + /// The source is only visible where the destination is opaque. + /// Result alpha = `source_alpha` × `dest_alpha`. + In, + /// Source out destination (subtract). + /// + /// The source is only visible where the destination is transparent. + /// Useful for masking/cutting out regions. + Out, + /// Source atop destination. + /// + /// Source is composited over destination, but only where destination is opaque. + Atop, + /// Source XOR destination (exclusive or). + /// + /// Shows source where destination is transparent and vice versa, + /// but not where both are opaque. + Xor, + /// Arithmetic combination with custom coefficients. + /// + /// Custom linear combination: result = k1*src*dst + k2*src + k3*dst + k4. + /// Allows creating custom compositing operations beyond the standard Porter-Duff set. + Arithmetic { + /// Coefficient k1 for the (source * destination) term. + k1: f32, + /// Coefficient k2 for the source term. + k2: f32, + /// Coefficient k3 for the destination term. + k3: f32, + /// Constant offset k4 added to the result. + k4: f32, + }, +} + +/// Blend modes for combining colors. +/// +/// These are blend modes that define how to combine the colors +/// of two layers. Unlike compositing operators which deal with alpha, blend modes +/// focus on color mixing while preserving the compositing behavior. +/// +/// See: +pub type BlendMode = peniko::Mix; + +/// Morphological operators for dilate/erode operations. +/// +/// These operators modify the shape of objects by expanding or contracting them. +/// They work by examining neighborhoods of pixels and applying min/max operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MorphologyOperator { + /// Erode operation (shrink/thin shapes). + /// + /// Makes objects smaller by removing pixels at the edges. Takes the minimum + /// value in the neighborhood. Useful for removing noise or separating touching objects. + Erode, + /// Dilate operation (expand/thicken shapes). + /// + /// Makes objects larger by adding pixels at the edges. Takes the maximum + /// value in the neighborhood. Useful for filling holes or connecting nearby objects. + Dilate, +} + +/// Convolution kernel for custom filtering operations. +/// +/// Defines a square matrix of weights used for convolution-based image processing. +/// The kernel is applied to each pixel by multiplying surrounding pixels by the weights, +/// summing the results, dividing by the divisor, and adding the bias. +#[derive(Debug, Clone, PartialEq)] +pub struct ConvolutionKernel { + /// Kernel size (e.g., 3 for a 3×3 kernel, 5 for 5×5). + /// The kernel must be square, so this defines both width and height. + pub size: u32, + /// Kernel weight values in row-major order. + /// Length must equal size × size. Center of kernel is typically at (size/2, size/2). + pub values: Vec, + /// Normalization divisor applied to the convolution result. + /// Common practice is to use the sum of all weights for averaging, or 1.0 otherwise. + pub divisor: f32, + /// Bias value added to the result after normalization. + /// Useful for edge detection or emboss effects to shift the result range. + pub bias: f32, + /// Whether to preserve the alpha channel unchanged. + /// If true, convolution only applies to RGB; if false, it applies to RGBA. + pub preserve_alpha: bool, +} + +/// Types of turbulence noise generation. +/// +/// Determines the algorithm used for generating procedural noise patterns. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TurbulenceType { + /// Fractal noise (smooth, natural-looking Perlin noise). + /// + /// Creates smooth, continuous patterns suitable for natural textures + /// like clouds, marble, wood grain, or terrain. + FractalNoise, + /// Turbulence noise (more chaotic and energetic). + /// + /// Creates more chaotic patterns with sharper transitions, + /// suitable for fire, smoke, or turbulent effects. + Turbulence, +} + +/// Color channels for displacement mapping and channel selection. +/// +/// Specifies which color channel to use for operations that need to +/// extract or reference individual channels from an image. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ColorChannel { + /// Red color channel (R component). + Red, + /// Green color channel (G component). + Green, + /// Blue color channel (B component). + Blue, + /// Alpha channel (transparency/opacity). + Alpha, +} + +/// Transfer functions for component transfer operations. +/// +/// These functions map input color channel values to output values, +/// enabling gamma correction, color grading, and custom color curves. +/// Input and output values are typically in the range [0, 1]. +#[derive(Debug, Clone, PartialEq)] +pub enum TransferFunction { + /// Identity function (output = input, no change). + Identity, + /// Table lookup with linear interpolation. + /// + /// Maps input values using a lookup table with linear interpolation between entries. + /// Input 0.0 maps to values\[0\], 1.0 maps to values\[n-1\], intermediate values interpolate. + Table { + /// Lookup table values defining the transfer curve. + /// More values provide smoother curves. Minimum 2 values required. + values: Vec, + }, + /// Discrete step function (posterization). + /// + /// Maps input to discrete output values without interpolation, creating step/banding effects. + /// Each segment gets a constant output value from the table. + Discrete { + /// Step values for each discrete output level. + /// Input range is divided into len(values) segments, each mapping to one value. + values: Vec, + }, + /// Linear function: output = slope × input + intercept. + /// + /// Simple linear transformation of the input value. + Linear { + /// Slope coefficient (rate of change). + slope: f32, + /// Intercept offset (constant added to result). + intercept: f32, + }, + /// Gamma correction: output = amplitude × input^exponent + offset. + /// + /// Applies power-law transformation, commonly used for gamma correction and + /// adjusting midtone brightness without affecting blacks or whites. + Gamma { + /// Amplitude multiplier applied to the result. + amplitude: f32, + /// Gamma exponent (< 1 brightens, > 1 darkens midtones). + exponent: f32, + /// Offset added to the final result. + offset: f32, + }, +} + +/// Light source configurations for lighting effects. +/// +/// Defines different types of light sources used in diffuse and specular lighting +/// filter primitives. Each type has different characteristics and use cases. +#[derive(Debug, Clone, PartialEq)] +pub enum LightSource { + /// Distant light source (infinitely far away, like the sun). + /// + /// All rays are parallel, creating uniform lighting across the surface. + /// Direction is specified using spherical coordinates (azimuth and elevation). + Distant { + /// Azimuth angle in degrees (0° = pointing right, 90° = pointing up). + /// Defines the horizontal direction of the light. + azimuth: f32, + /// Elevation angle in degrees (0° = horizon, 90° = directly overhead). + /// Defines the vertical angle of the light source. + elevation: f32, + }, + /// Point light source at a specific 3D position. + /// + /// Light radiates uniformly in all directions from a single point. + /// Intensity decreases with distance. Like a light bulb. + Point { + /// Light source X coordinate in user space. + x: f32, + /// Light source Y coordinate in user space. + y: f32, + /// Light source Z coordinate (height above the surface). + /// Larger values create softer lighting across larger areas. + z: f32, + }, + /// Spot light with position, direction, and cone angle. + /// + /// Light emanates from a point in a specific direction with limited spread. + /// Like a flashlight or stage spotlight with adjustable focus. + Spot { + /// Light source X coordinate in user space. + x: f32, + /// Light source Y coordinate in user space. + y: f32, + /// Light source Z coordinate (height above the surface). + z: f32, + /// X coordinate the spotlight is aimed at. + points_at_x: f32, + /// Y coordinate the spotlight is aimed at. + points_at_y: f32, + /// Z coordinate the spotlight is aimed at. + points_at_z: f32, + /// Specular exponent controlling the focus/sharpness of the spotlight beam. + /// Higher values create tighter, more focused beams. + specular_exponent: f32, + /// Optional cone angle in degrees limiting the spotlight spread. + /// If None, the light spreads based only on the specular exponent. + limiting_cone_angle: Option, + }, +} + +/// Common color transformation matrices. +/// +/// These 4x5 matrices are used with the `ColorMatrix` filter primitive. +/// Each row transforms a color channel: [R, G, B, A, offset]. +pub mod matrices { + /// Identity matrix (no change). + pub const IDENTITY: [f32; 20] = [ + 1.0, 0.0, 0.0, 0.0, 0.0, // Red + 0.0, 1.0, 0.0, 0.0, 0.0, // Green + 0.0, 0.0, 1.0, 0.0, 0.0, // Blue + 0.0, 0.0, 0.0, 1.0, 0.0, // Alpha + ]; + + /// Extract alpha channel to RGB (for shadow effects). + pub const ALPHA_TO_BLACK: [f32; 20] = [ + 0.0, 0.0, 0.0, 1.0, 0.0, // Red = Alpha + 0.0, 0.0, 0.0, 1.0, 0.0, // Green = Alpha + 0.0, 0.0, 0.0, 1.0, 0.0, // Blue = Alpha + 0.0, 0.0, 0.0, 1.0, 0.0, // Alpha = Alpha + ]; + + /// Grayscale conversion matrix using luminosity weights. + pub const GRAYSCALE: [f32; 20] = [ + 0.2126, 0.7152, 0.0722, 0.0, 0.0, // Red + 0.2126, 0.7152, 0.0722, 0.0, 0.0, // Green + 0.2126, 0.7152, 0.0722, 0.0, 0.0, // Blue + 0.0, 0.0, 0.0, 1.0, 0.0, // Alpha + ]; + + /// Sepia tone matrix for vintage photo effect. + pub const SEPIA: [f32; 20] = [ + 0.393, 0.769, 0.189, 0.0, 0.0, // Red + 0.349, 0.686, 0.168, 0.0, 0.0, // Green + 0.272, 0.534, 0.131, 0.0, 0.0, // Blue + 0.0, 0.0, 0.0, 1.0, 0.0, // Alpha + ]; +} + +/// Common convolution kernels. +/// +/// These kernels are used with the `ConvolveMatrix` filter primitive +/// for various image processing effects. All provided kernels are 3x3. +pub mod kernels { + use super::ConvolutionKernel; + use alloc::vec; + + /// 3x3 Gaussian blur kernel for basic smoothing. + pub fn gaussian_3x3() -> ConvolutionKernel { + ConvolutionKernel { + size: 3, + values: vec![1.0, 2.0, 1.0, 2.0, 4.0, 2.0, 1.0, 2.0, 1.0], + divisor: 16.0, + bias: 0.0, + preserve_alpha: false, + } + } + + /// 3x3 Sharpen kernel to enhance edges and details. + pub fn sharpen_3x3() -> ConvolutionKernel { + ConvolutionKernel { + size: 3, + values: vec![0.0, -1.0, 0.0, -1.0, 5.0, -1.0, 0.0, -1.0, 0.0], + divisor: 1.0, + bias: 0.0, + preserve_alpha: true, + } + } + + /// 3x3 Edge detection kernel (Laplacian operator). + pub fn edge_detect_3x3() -> ConvolutionKernel { + ConvolutionKernel { + size: 3, + values: vec![-1.0, -1.0, -1.0, -1.0, 8.0, -1.0, -1.0, -1.0, -1.0], + divisor: 1.0, + bias: 0.0, + preserve_alpha: true, + } + } + + /// 3x3 Emboss kernel for creating a raised/beveled appearance. + pub fn emboss_3x3() -> ConvolutionKernel { + ConvolutionKernel { + size: 3, + values: vec![-2.0, -1.0, 0.0, -1.0, 1.0, 1.0, 0.0, 1.0, 2.0], + divisor: 1.0, + bias: 0.5, + preserve_alpha: true, + } + } +} diff --git a/vendor/vello_common/src/flatten.rs b/vendor/vello_common/src/flatten.rs new file mode 100644 index 000000000..be219c6c3 --- /dev/null +++ b/vendor/vello_common/src/flatten.rs @@ -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 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 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, + affine: Affine, + line_buf: &mut Vec, + 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( + simd: S, + path: impl IntoIterator, + affine: Affine, + line_buf: &mut Vec, + 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, + style: &Stroke, + affine: Affine, + line_buf: &mut Vec, + 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, + 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, + 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; + } + } + } +} diff --git a/vendor/vello_common/src/flatten_simd.rs b/vendor/vello_common/src/flatten_simd.rs new file mode 100644 index 000000000..e71df65ca --- /dev/null +++ b/vendor/vello_common/src/flatten_simd.rs @@ -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: +/// +/// +/// This version works using a similar approach but using f32x4/f32x8 SIMD instead. +#[inline(always)] +pub(crate) fn flatten( + simd: S, + path: impl IntoIterator, + // 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, +} + +#[inline(always)] +fn is_finite_simd(x: f32x4) -> mask32x4 { + 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>(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(x: f32x8) -> f32x8 { + 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(simd: S, pt: Point32) -> f32x8 { + let p_f64: f64 = bytemuck::cast(pt); + simd.reinterpret_f32_f64x4(f64x4::splat(simd, p_f64)) +} + +#[inline(always)] +fn eval_cubics_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| { + 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(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( + 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(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}"); + } + } + } +} diff --git a/vendor/vello_common/src/geometry.rs b/vendor/vello_common/src/geometry.rs new file mode 100644 index 000000000..4337550eb --- /dev/null +++ b/vendor/vello_common/src/geometry.rs @@ -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 } +} diff --git a/vendor/vello_common/src/image_cache.rs b/vendor/vello_common/src/image_cache.rs new file mode 100644 index 000000000..f19c42f7a --- /dev/null +++ b/vendor/vello_common/src/image_cache.rs @@ -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>, + /// Stack of free indices. + free_idxs: Vec, +} + +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 { + 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, + ) -> Result { + 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 { + 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()); + } +} diff --git a/vendor/vello_common/src/lib.rs b/vendor/vello_common/src/lib.rs new file mode 100644 index 000000000..6862ea431 --- /dev/null +++ b/vendor/vello_common/src/lib.rs @@ -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); diff --git a/vendor/vello_common/src/mask.rs b/vendor/vello_common/src/mask.rs new file mode 100644 index 000000000..8047be828 --- /dev/null +++ b/vendor/vello_common/src/mask.rs @@ -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, + 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); + +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, 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 + // + // and Filter Effects Module Level 1 § 9.6 + // . + // 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::>(); + + 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] + } +} diff --git a/vendor/vello_common/src/math.rs b/vendor/vello_common/src/math.rs new file mode 100644 index 000000000..36efbacae --- /dev/null +++ b/vendor/vello_common/src/math.rs @@ -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 +// 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 { + /// 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 + } +} diff --git a/vendor/vello_common/src/multi_atlas.rs b/vendor/vello_common/src/multi_atlas.rs new file mode 100644 index 000000000..4e42918e2 --- /dev/null +++ b/vendor/vello_common/src/multi_atlas.rs @@ -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 { + 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, + /// 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 { + 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 { + 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, + ) -> Result { + // 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, + ) -> Result { + 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, + ) -> Result { + 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, + ) -> Result { + 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, + ) -> Result { + 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); + } +} diff --git a/vendor/vello_common/src/paint.rs b/vendor/vello_common/src/paint.rs new file mode 100644 index 000000000..b032bc31b --- /dev/null +++ b/vendor/vello_common/src/paint.rs @@ -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> for Paint { + fn from(value: AlphaColor) -> 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 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 + // . + 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; + +/// 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>; +} + +/// 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> { + None + } +} + +/// A premultiplied color. +#[derive(Debug, Clone, PartialEq, Copy)] +pub struct PremulColor { + premul_u8: PremulRgba8, + premul_f32: peniko::color::PremulColor, +} + +impl PremulColor { + /// Create a new premultiplied color. + pub fn from_alpha_color(color: AlphaColor) -> Self { + Self::from_premul_color(color.premultiply()) + } + + /// Create a new premultiplied color from `peniko::PremulColor`. + pub fn from_premul_color(color: peniko::color::PremulColor) -> 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 { + 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; diff --git a/vendor/vello_common/src/pico_svg.rs b/vendor/vello_common/src/pico_svg.rs new file mode 100644 index 000000000..c9f3fdc31 --- /dev/null +++ b/vendor/vello_common/src/pico_svg.rs @@ -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, + /// 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, + /// 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, + /// 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, +} + +struct Parser { + scale: f64, +} + +impl PicoSvg { + /// Load an SVG document from a string + pub fn load(xml_string: &str, scale: f64) -> Result> { + 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 = 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>, +} + +impl Parser { + fn new(scale: f64) -> Self { + Self { scale } + } + + fn rec_parse( + &mut self, + node: Node<'_, '_>, + properties: &RecursiveProperties, + items: &mut Vec, + ) -> Result<(), Box> { + 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::, _>>() + .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::, _>>() + { + 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::, _>>() + { + 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 { + 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, + attr_name: &str, + node: Node<'_, '_>, +) -> AlphaColor { + 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, c2: AlphaColor) { + 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); + } +} diff --git a/vendor/vello_common/src/pixmap.rs b/vendor/vello_common/src/pixmap.rs new file mode 100644 index 000000000..83055dd51 --- /dev/null +++ b/vendor/vello_common/src/pixmap.rs @@ -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, + /// 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, 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, + 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 { + 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, 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 { + 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 { + 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() + } +} diff --git a/vendor/vello_common/src/probe.rs b/vendor/vello_common/src/probe.rs new file mode 100644 index 000000000..5e48f1243 --- /dev/null +++ b/vendor/vello_common/src/probe.rs @@ -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 { + /// 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, +} + +impl Probe { + /// 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, opacity: Option); + 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(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() + } +} diff --git a/vendor/vello_common/src/rect.rs b/vendor/vello_common/src/rect.rs new file mode 100644 index 000000000..55f98b716 --- /dev/null +++ b/vendor/vello_common/src/rect.rs @@ -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, alpha_buf: &mut Vec) { + 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: S, rect: Rect, strip_buf: &mut Vec, alpha_buf: &mut Vec) { + 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(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: S, cov: &[f32; Tile::WIDTH as usize]) -> u8x16 { + 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: S, + x_cov: &[f32; Tile::WIDTH as usize], + y_cov: &[f32; Tile::HEIGHT as usize], +) -> u8x16 { + 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) +} diff --git a/vendor/vello_common/src/render_graph.rs b/vendor/vello_common/src/render_graph.rs new file mode 100644 index 000000000..4ddb871ec --- /dev/null +++ b/vendor/vello_common/src/render_graph.rs @@ -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, + /// Dependencies between nodes, defining execution order. + pub edges: Vec, + /// 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, + /// The root layer node ID, tracked separately since it's never popped. + /// This is appended to the execution order when iterating. + root_node: Option, + /// 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 + '_ { + 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 { + // 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> = 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 = (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 + } +} diff --git a/vendor/vello_common/src/render_state.rs b/vendor/vello_common/src/render_state.rs new file mode 100644 index 000000000..11e9056db --- /dev/null +++ b/vendor/vello_common/src/render_state.rs @@ -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, +} + +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(); + } +} diff --git a/vendor/vello_common/src/simd.rs b/vendor/vello_common/src/simd.rs new file mode 100644 index 000000000..d267fc33e --- /dev/null +++ b/vendor/vello_common/src/simd.rs @@ -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 { + /// Splat every 4th element of the vector. + fn splat_4th(self) -> Self; +} + +impl Splat4thExt for f32x4 { + #[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 Splat4thExt for f32x8 { + #[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 Splat4thExt for f32x16 { + #[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 Splat4thExt for u8x16 { + #[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 Splat4thExt for u8x32 { + #[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(simd: S, input: f32x4) -> f32x16 { + 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])), + ) +} diff --git a/vendor/vello_common/src/strip.rs b/vendor/vello_common/src/strip.rs new file mode 100644 index 000000000..315f1972e --- /dev/null +++ b/vendor/vello_common/src/strip.rs @@ -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( + start: u16, + end: u16, + strips: &mut Vec, + alphas: &mut Vec, + 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, + alpha_buf: &mut Vec, + fill_rule: Fill, + aliasing_threshold: Option, + lines: &[Line], +) { + dispatch!(level, simd => render_impl(simd, + tiles, + strip_buf, + alpha_buf, + fill_rule, + aliasing_threshold, + lines)); +} + +#[inline(always)] +fn render_impl( + s: S, + tiles: &Tiles, + strip_buf: &mut Vec, + alpha_buf: &mut Vec, + fill_rule: Fill, + aliasing_threshold: Option, + 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, alphas: &mut Vec| { + 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 F32x4MaxExt for f32x4 { + #[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 F32x4MaxExt for f32x4 { + #[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, + ); + } +} diff --git a/vendor/vello_common/src/strip_generator.rs b/vendor/vello_common/src/strip_generator.rs new file mode 100644 index 000000000..9a8bfc237 --- /dev/null +++ b/vendor/vello_common/src/strip_generator.rs @@ -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, + /// The alphas in the storage. + pub alphas: Vec, + 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, + 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, + fill_rule: Fill, + transform: Affine, + aliasing_threshold: Option, + strip_storage: &mut StripStorage, + clip_path: Option>, + ) { + 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, + stroke: &Stroke, + transform: Affine, + aliasing_threshold: Option, + strip_storage: &mut StripStorage, + clip_path: Option>, + ) { + 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, + strip_storage: &mut StripStorage, + fill_rule: Fill, + clip_path: Option>, + ) { + 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>, + ) { + 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>, + render_fn: impl FnOnce(&mut Vec, &mut Vec), +) { + 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"); + } +} diff --git a/vendor/vello_common/src/tile.rs b/vendor/vello_common/src/tile.rs new file mode 100644 index 000000000..92e3da418 --- /dev/null +++ b/vendor/vello_common/src/tile.rs @@ -0,0 +1,2426 @@ +// Copyright 2025 the Vello Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Primitives for creating tiles. + +use crate::flatten::Line; +use alloc::vec; +use alloc::vec::Vec; +use fearless_simd::*; +#[cfg(not(feature = "std"))] +use peniko::kurbo::common::FloatFuncs as _; + +/// T-op bit +const T: u32 = 0b00001; +/// B-ottom bit +const B: u32 = 0b00010; +/// L-eft bit +const L: u32 = 0b00100; +/// R-ight bit +const R: u32 = 0b01000; +/// W-inding bit +const W: u32 = 0b10000; + +/// Shift amount corresponding to the bottom bit. +const BOT_SHIFT: u32 = B.trailing_zeros(); +/// Shift amount corresponding to the left bit. +const LEFT_SHIFT: u32 = L.trailing_zeros(); +/// Shift amount corresponding to the right bit. +const RIGHT_SHIFT: u32 = R.trailing_zeros(); +/// Shift amount corresponding to the winding bit. +const WINDING_SHIFT: u32 = W.trailing_zeros(); + +/// Mask for all intersection and winding bits (Bits 0-4). +const INTERSECTION_MASK: u32 = W | R | L | B | T; +/// Shift amount corresponding to the intersection bits. +const INT_MASK_SHIFT: u32 = INTERSECTION_MASK.count_ones(); + +/// The max number of lines per path. +/// +/// Trying to render a path with more lines than this may result in visual artifacts. +pub const MAX_LINES_PER_PATH: u32 = 1 << (32 - INT_MASK_SHIFT); + +/// A logical grouping of arrays used for culled tile processing, +#[derive(Debug, Clone, Default)] +pub struct CulledWindings { + /// Fractional winding coverage for each individual scanline in a row. + pub partial: Vec<[f32; Tile::HEIGHT as usize]>, + // Note that this will cause issues if we have windings greater/less than i8, + // but this should only occur in pathological cases. + /// Accumulated integer winding deltas for each tile row. + pub coarse: Vec, + /// Bitmask tracking which rows contain active geometry or winding data. + pub active: Vec, + /// Flag indicating if any geometry was early-culled outside the viewport. + pub culled: bool, +} + +impl CulledWindings { + /// Number of bits in a single active mask word. + const WORD_BITS: usize = 32; + /// Bit shift equivalent to dividing by `WORD_BITS` (2^5 = 32). + const WORD_SHIFT: usize = 5; + /// Bitmask equivalent to modulo `WORD_BITS` (32 - 1 = 31). + const WORD_MASK: usize = 31; + + /// Constructor chained to `Tiles`' constructor and matches its lifetime. Since `Tiles` itself + /// matches the lifetime of `StripGenerator`, we know that the viewport dimensions will never + /// change, and thus the backing vecs never need to be resized. (For now). + pub fn new(height: u16) -> Self { + let height_usize = height as usize; + let tile_height = Tile::HEIGHT as usize; + let num_rows = height_usize.div_ceil(tile_height); + let num_bits = num_rows.div_ceil(Self::WORD_BITS); + + Self { + partial: vec![[0.0; Tile::HEIGHT as usize]; num_rows], + coarse: vec![0; num_rows], + active: vec![0; num_bits], + culled: false, + } + } + + /// Clears but does not resize + pub fn reset(&mut self) { + // TODO: Maybe consider tracking touched regions and only resetting those + // instead of always the full array? + if self.culled { + self.partial.fill([0.0; Tile::HEIGHT as usize]); + self.coarse.fill(0); + self.active.fill(0); + self.culled = false; + } + } + + /// Marks if a row was culled early for faster traversal in strip generation. + #[inline(always)] + pub fn mark_row_active(&mut self, row_idx: usize) { + self.active[row_idx >> Self::WORD_SHIFT] |= 1 << (row_idx & Self::WORD_MASK); + } + + /// Bulk marks a range of rows as active [`start_row`, `end_row`). + #[inline(always)] + pub fn mark_row_range_active(&mut self, start_row: usize, end_row: usize) { + if start_row >= end_row { + return; + } + + let start_word = start_row >> Self::WORD_SHIFT; + let end_word = (end_row - 1) >> Self::WORD_SHIFT; + + if start_word == end_word { + // All bits fall within the same u32 word + let shift = start_row & Self::WORD_MASK; + let count = end_row - start_row; + let mask = if count == Self::WORD_BITS { + u32::MAX + } else { + ((1_u32 << count) - 1) << shift + }; + self.active[start_word] |= mask; + } else { + // Bits span multiple words: handle start, full middle words, and end + self.active[start_word] |= u32::MAX << (start_row & Self::WORD_MASK); + + self.active[(start_word + 1)..end_word].fill(u32::MAX); + + let end_shift = ((end_row - 1) & Self::WORD_MASK) + 1; + let mask = if end_shift == Self::WORD_BITS { + u32::MAX + } else { + (1_u32 << end_shift) - 1 + }; + self.active[end_word] |= mask; + } + } + + /// Calls `f` on active rows in the range [`start`, `end`). + #[inline(always)] + pub fn for_active_rows_in_range(&self, start: usize, end: usize, mut f: F) + where + F: FnMut(usize), + { + if start >= end { + return; + } + + let start_word = start >> Self::WORD_SHIFT; + let end_word = (end - 1) >> Self::WORD_SHIFT; + + let mut process_word = |mut word: u32, word_idx: usize| { + while word != 0 { + let bit = word.trailing_zeros(); + word &= !(1_u32 << bit); + f((word_idx << Self::WORD_SHIFT) + bit as usize); + } + }; + + let end_limit = ((end - 1) & Self::WORD_MASK) + 1; + let end_mask = if end_limit == Self::WORD_BITS { + u32::MAX + } else { + (1_u32 << end_limit) - 1 + }; + + // Start Word + { + let mut word = self.active[start_word]; + let start_bit = start & Self::WORD_MASK; + word &= !((1_u32 << start_bit) - 1); + + if start_word == end_word { + word &= end_mask; + } + + process_word(word, start_word); + } + + // Middle Words & End Word + if start_word < end_word { + for (word_idx, &word_val) in self + .active + .iter() + .enumerate() + .take(end_word) + .skip(start_word + 1) + { + process_word(word_val, word_idx); + } + + // End Word + process_word(self.active[end_word] & end_mask, end_word); + } + } +} + +/// A tile represents an aligned area on the pixmap, used to subdivide the viewport into sub-areas +/// (currently 4x4) and analyze line intersections inside each such area. +/// +/// Keep in mind that it is possible to have multiple tiles with the same index, +/// namely if we have multiple lines crossing the same 4x4 area! +/// +/// # Note +/// +/// This struct is `#[repr(C)]`, but the byte order of its fields is dependent on the endianness of +/// the compilation target. +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct Tile { + // The field ordering is important. + // + // The given ordering (variant over little and big endian compilation targets), ensures that + // `Tile::to_bits` doesn't do any actual work, as the ordering of the fields is such that the + // numeric value of a `Tile` in memory is identical as returned by that method. This allows + // for, e.g., comparison and sorting. + #[cfg(target_endian = "big")] + /// The index of the tile in the y direction. + pub y: u16, + + #[cfg(target_endian = "big")] + /// The index of the tile in the x direction. + pub x: u16, + + /// The index of the line this tile belongs to into the line buffer, intersection data, + /// and winding data packed together. + /// + /// The layout is: + /// - **Bits 0-4 (5 bits):** Intersection and Winding Mask (`W | R | L | B | T`). + /// - Bit 0 (mask `0b00001`): Intersects top edge (T) + /// - Bit 1 (mask `0b00010`): Intersects bottom edge (B) + /// - Bit 2 (mask `0b00100`): Intersects left edge (L) + /// - Bit 3 (mask `0b01000`): Intersects right edge (R) + /// - Bit 4 (mask `0b10000`): Winding (W) - 1 if crosses top edge. + /// - **Bits 5-31 (27 bits):** The line index (`line_idx`). + /// + /// **Sorting Note:** The `line_idx` occupies the higher bits to ensure that when sorting + /// tiles with the same (x, y) coordinates, they are sorted by their line index first, + /// and then by their intersection mask. + pub packed_winding_line_idx: u32, + + #[cfg(target_endian = "little")] + /// The index of the tile in the x direction. + pub x: u16, + + #[cfg(target_endian = "little")] + /// The index of the tile in the y direction. + pub y: u16, +} + +impl Tile { + /// The width of a tile in pixels. + pub const WIDTH: u16 = 4; + + /// The height of a tile in pixels. + pub const HEIGHT: u16 = 4; + + /// A special tile used to signal the end of a tile stream during rendering. + pub const SENTINEL: Self = Self::new(u16::MAX, u16::MAX, 0, 0); + + /// Create a new tile. + /// `x` and `y` will be clamped to the largest possible coordinate if they are too large. + /// + /// `line_idx` must be smaller than [`MAX_LINES_PER_PATH`]. + #[inline] + pub fn new_clamped(x: u16, y: u16, line_idx: u32, intersection_mask: u32) -> Self { + Self::new( + // Make sure that x and y stay in range when multiplying + // with the tile width and height during strips generation. + x.min(u16::MAX / Self::WIDTH), + y.min(u16::MAX / Self::HEIGHT), + line_idx, + intersection_mask, + ) + } + + /// The base tile constructor + /// + /// Unlike [`Self::new_clamped`], this constructor stores `x` and `y` exactly as provided. + /// Callers must ensure these coordinates do not exceed the limits required by downstream + /// processing (typically `u16::MAX / WIDTH` and `u16::MAX / HEIGHT`). + #[inline] + pub const fn new(x: u16, y: u16, line_idx: u32, intersection_mask: u32) -> Self { + #[cfg(debug_assertions)] + if line_idx >= MAX_LINES_PER_PATH { + panic!("Max. number of lines per path exceeded."); + } + // The intersection_mask is expected to contain bits 0-4 (T, B, L, R, W). + // We pack line_idx into the high bits (5-31) and intersection_mask into low bits (0-4). + Self { + x, + y, + packed_winding_line_idx: (line_idx << INT_MASK_SHIFT) | intersection_mask, + } + } + + /// Check whether two tiles are at the same location. + #[inline] + pub const fn same_loc(&self, other: &Self) -> bool { + self.same_row(other) && self.x == other.x + } + + /// Check whether `self` is adjacent to the left of `other`. + #[inline] + pub const fn prev_loc(&self, other: &Self) -> bool { + self.same_row(other) && self.x + 1 == other.x + } + + /// Check whether two tiles are on the same row. + #[inline] + pub const fn same_row(&self, other: &Self) -> bool { + self.y == other.y + } + + /// The index of the line this tile belongs to into the line buffer. + /// + /// Returns the high 27 bits. + #[inline] + pub const fn line_idx(&self) -> u32 { + self.packed_winding_line_idx >> INT_MASK_SHIFT + } + + /// Whether the line crosses the top edge of the tile. + /// + /// Lines making this crossing increment or decrement the coarse tile winding, depending on the + /// line direction. + /// + /// Checks Bit 4 (Winding). + #[inline] + pub const fn winding(&self) -> bool { + (self.packed_winding_line_idx & W) != 0 + } + + /// The 5 bits of intersection and winding data. + #[inline] + pub const fn intersection_mask(&self) -> u32 { + self.packed_winding_line_idx & INTERSECTION_MASK + } + + /// Whether the line intersects the top edge of the tile. + #[inline] + pub const fn intersects_top(&self) -> bool { + (self.intersection_mask() & T) != 0 + } + + /// Whether the line intersects the bottom edge of the tile. + #[inline] + pub const fn intersects_bottom(&self) -> bool { + (self.intersection_mask() & B) != 0 + } + + /// Whether the line intersects the left edge of the tile. + #[inline] + pub const fn intersects_left(&self) -> bool { + (self.intersection_mask() & L) != 0 + } + + /// Whether the line intersects the right edge of the tile. + #[inline] + pub const fn intersects_right(&self) -> bool { + (self.intersection_mask() & R) != 0 + } + + /// Return the `u64` representation of this tile. + /// + /// This is the u64 interpretation of `(y, x, packed_winding_line_idx)` where `y` is the + /// most-significant part of the number and `packed_winding_line_idx` the least significant. + #[inline(always)] + const fn to_bits(self) -> u64 { + // Note that for correct rendering, tiles only need to be sorted on `(y, x)`. Sorting on + // the line index in addition to the coordinate improves data locality in strip rendering. + // This is trading off increased sorting time for decreased strip rendering time. How the + // trade-off falls is scene-dependent. + // + // This operation compiles to a no-op: `Tile`'s field order is such that this is exactly + // the in-memory representation. + ((self.y as u64) << 48) | ((self.x as u64) << 32) | self.packed_winding_line_idx as u64 + } + + /// Whether a tile is a sentinel tile + // + // A tile produced organically by a make_tiles call can never have this coordinate because of + // the division by tile size on creation, so checking on x is sufficient to identify it. + #[inline(always)] + pub const fn is_sentinel(&self) -> bool { + self.x == u16::MAX + } +} + +impl PartialEq for Tile { + #[inline(always)] + fn eq(&self, other: &Self) -> bool { + self.to_bits() == other.to_bits() + } +} + +impl Ord for Tile { + #[inline(always)] + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.to_bits().cmp(&other.to_bits()) + } +} + +impl PartialOrd for Tile { + #[inline(always)] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Eq for Tile {} + +/// Handles the tiling of paths. +#[derive(Clone, Debug)] +pub struct Tiles { + tile_buf: Vec, + level: Level, + sorted: bool, + /// Auxiliary data tracking row windings and active rows for early culling. + pub windings: CulledWindings, +} + +impl Tiles { + /// Create a new tiles container. + pub fn new(level: Level, height: u16) -> Self { + Self { + tile_buf: vec![], + level, + sorted: false, + windings: CulledWindings::new(height), + } + } + + /// Get the number of tiles in the container. + pub fn len(&self) -> u32 { + self.tile_buf.len() as u32 + } + + /// Returns `true` if the container has no tiles. + pub fn is_empty(&self) -> bool { + self.tile_buf.is_empty() + } + + /// Returns `true` if any geometry was early-culled outside the viewport. + pub fn has_culled_tiles(&self) -> bool { + self.windings.culled + } + + /// Reset the tiles' container. + pub fn reset(&mut self) { + self.windings.reset(); + self.tile_buf.clear(); + self.sorted = false; + } + + /// Sort the tiles in the container. + pub fn sort_tiles(&mut self) { + self.sorted = true; + // To enable auto-vectorization. + self.level.dispatch(|_| self.tile_buf.sort_unstable()); + } + + /// Get the tile at a certain index. + /// + /// Panics if the container hasn't been sorted before. + #[inline] + pub fn get(&self, index: u32) -> &Tile { + assert!( + self.sorted, + "attempted to call `get` before sorting the tile container." + ); + + &self.tile_buf[index as usize] + } + + /// Iterate over the tiles in sorted order. + /// + /// Panics if the container hasn't been sorted before. + #[inline] + pub fn iter(&self) -> impl Iterator { + assert!( + self.sorted, + "attempted to call `iter` before sorting the tile container." + ); + + self.tile_buf.iter() + } + + /// Generates tile commands for Analytic Anti-Aliasing rasterization. Unlike the MSAA path, this + /// function performs "coarse binning" to simply identify every tile a line segment traverses. + /// It encodes the line index and winding direction, delegating the precise calculation of pixel + /// coverage to `strip::render`. + pub fn make_tiles_analytic_aa( + &mut self, + level: Level, + lines: &[Line], + width: u16, + height: u16, + ) -> bool { + dispatch!(level, simd => self.make_tiles_analytic_aa_impl::<_>( + simd, + lines, + width, + height, + )) + } + + fn make_tiles_analytic_aa_impl( + &mut self, + s: S, + lines: &[Line], + width: u16, + height: u16, + ) -> bool { + self.reset(); + + if width == 0 || height == 0 { + return self.windings.culled; + } + + debug_assert!( + lines.len() <= MAX_LINES_PER_PATH as usize, + "Max. number of lines per path exceeded. Max is {}, got {}.", + MAX_LINES_PER_PATH, + lines.len() + ); + + let tile_columns = width.div_ceil(Tile::WIDTH); + let tile_rows = height.div_ceil(Tile::HEIGHT); + + let px_top = f32x4::from_slice(s, &[0.0, 1.0, 2.0, 3.0]); + let px_bottom = px_top + f32x4::splat(s, 1.0); + let simd_zero = f32x4::splat(s, 0.0); + let tile_height_f32 = Tile::HEIGHT as f32; + + for (line_idx, line) in lines.iter().take(MAX_LINES_PER_PATH as usize).enumerate() { + let line_idx = line_idx as u32; + + let p0_x = line.p0.x / f32::from(Tile::WIDTH); + let p0_y = line.p0.y / f32::from(Tile::HEIGHT); + let p1_x = line.p1.x / f32::from(Tile::WIDTH); + let p1_y = line.p1.y / f32::from(Tile::HEIGHT); + + let (line_left_x, line_right_x) = if p0_x < p1_x { + (p0_x, p1_x) + } else { + (p1_x, p0_x) + }; + + // Lines whose left-most endpoint exceed the right edge of the viewport are culled + if line_left_x > tile_columns as f32 { + continue; + } + + 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) + }; + + // The `as u16` casts here intentionally clamp negative coordinates to 0. + let y_top_tiles = (line_top_y as u16).min(tile_rows); + let line_bottom_y_ceil = line_bottom_y.ceil(); + let y_bottom_tiles = (line_bottom_y_ceil as u16).min(tile_rows); + + // If y_top_tiles == y_bottom_tiles, then the line is either completely above or below + // the viewport OR it is perfectly horizontal and aligned to the tile grid, contributing + // no winding. In either case, it should be culled. + if y_top_tiles >= y_bottom_tiles { + // Technically, the `>` part of the `>=` is unnecessary due to clamping, but this + // gives stronger signal + continue; + } + + let dir = if p0_y >= p1_y { 1 } else { -1 }; + let f_dir = dir as f32; + let f_dir_v = f32x4::splat(s, f_dir); + + macro_rules! calc_fractional_coverage { + ($y_idx:expr, $segment_top_y:expr, $segment_bottom_y:expr) => {{ + let y_idx_f32 = f32::from($y_idx); + let local_y_start = ($segment_top_y - y_idx_f32) * tile_height_f32; + let local_y_end = ($segment_bottom_y - y_idx_f32) * tile_height_f32; + + let start_v = f32x4::splat(s, local_y_start); + let end_v = f32x4::splat(s, local_y_end); + + (px_bottom.min(end_v) - px_top.max(start_v)).max(simd_zero) + }}; + } + + // Lines fully to the left of the viewport are not visible but still produce winding + // which we record here and forward to the rendering stage. + if line_right_x < 0.0 { + let is_start_culled = line_top_y < 0.0; + + // This branch is for handling the "start" of the line. In case + // the line reaches above the viewport, we are already in the + // middle so we can skip that part. + if !is_start_culled { + self.windings.mark_row_active(y_top_tiles as usize); + + // Note: In theory, == should be enough, but just as + // additional safety against numerical precision errors we + // use <=. + let at_top_of_tile = line_top_y <= f32::from(y_top_tiles); + if at_top_of_tile { + self.windings.coarse[y_top_tiles as usize] += dir; + } + + let fractional_coverage = + calc_fractional_coverage!(y_top_tiles, line_top_y, line_bottom_y); + let target_row = &mut self.windings.partial[y_top_tiles as usize]; + let current = f32x4::from_slice(s, target_row); + + // See comment below on the double counting risk! + let double_count = if at_top_of_tile { + f_dir_v + } else { + f32x4::splat(s, 0.0) + }; + let next = fractional_coverage.mul_add(f_dir_v, current - double_count); + next.store_slice(target_row); + } + + let y_start_middle = if is_start_culled { + y_top_tiles + } else { + y_top_tiles + 1 + }; + let line_bottom_floor = line_bottom_y.floor(); + let y_end_middle = (line_bottom_floor as u16).min(tile_rows); + + for y_idx in y_start_middle..y_end_middle { + self.windings.coarse[y_idx as usize] += dir; + } + self.windings + .mark_row_range_active(y_start_middle as usize, y_end_middle as usize); + + if line_bottom_y != line_bottom_floor + && y_end_middle < tile_rows + // Prevent double-processing, unless the start was off-screen and hasn't been + // handled yet. + && (is_start_culled || y_end_middle != y_top_tiles) + { + self.windings.mark_row_active(y_end_middle as usize); + // Ends implicitly cross the top. + self.windings.coarse[y_end_middle as usize] += dir; + let fractional_coverage = + calc_fractional_coverage!(y_end_middle, line_top_y, line_bottom_y); + let target_row = &mut self.windings.partial[y_end_middle as usize]; + let current = f32x4::from_slice(s, target_row); + // Subtract the inverse direction to avoid double counting with the coarse winding. + let next = fractional_coverage.mul_add(f_dir_v, current - f_dir_v); + next.store_slice(target_row); + } + + self.windings.culled = true; + continue; + } + + // Get tile coordinates for start/end points, use i32 to preserve negative coordinates. + let p0_tile_x = line_top_x.floor() as i32; + let p0_tile_y = line_top_y.floor() as i32; + let p1_tile_x = line_bottom_x.floor() as i32; + let p1_tile_y = line_bottom_y.floor() as i32; + + // Special-case out lines which are fully contained within a tile. + let not_same_tile = p0_tile_y != p1_tile_y || p0_tile_x != p1_tile_x; + if not_same_tile { + // Case vertical lines: By definition, these cannot be horizontally crossing, and + // thus require no additional left-edge culling handling. + if line_left_x == line_right_x { + let x = (line_left_x as u16).min(tile_columns.saturating_sub(1)); + + // Row Start, not culled. + let is_start_culled = line_top_y < 0.0; + if !is_start_culled { + let winding = + ((f32::from(y_top_tiles) >= line_top_y) as u32) << WINDING_SHIFT; + let tile = Tile::new_clamped(x, y_top_tiles, line_idx, winding); + self.tile_buf.push(tile); + } + + // Middle + // If the start was culled, the first tile inside the viewport is a middle. + let y_start = if is_start_culled { + y_top_tiles + } else { + y_top_tiles + 1 + }; + + for y_idx in y_start..y_bottom_tiles { + let tile = Tile::new_clamped(x, y_idx, line_idx, W); + self.tile_buf.push(tile); + } + } else { + // General case, any line which crosses more than one tile and is not vertical. + let dx = p1_x - p0_x; + let dy = p1_y - p0_y; + let x_slope = dx / dy; + let dx_dir = (line_bottom_x >= line_top_x) as u32; + let not_dx_dir = dx_dir ^ 1; + + let w_start_base = dx_dir << WINDING_SHIFT; + let w_end_base = not_dx_dir << WINDING_SHIFT; + + let mut push_row = { + #[inline(always)] + |y_idx: u16, + row_top_y: f32, + row_bottom_y: f32, + w_start: u32, + w_end: u32, + w_single: u32| { + let row_top_x = p0_x + (row_top_y - p0_y) * x_slope; + let row_bottom_x = p0_x + (row_bottom_y - p0_y) * x_slope; + + let row_left_x = f32::min(row_top_x, row_bottom_x).max(line_left_x); + let row_right_x = f32::max(row_top_x, row_bottom_x).min(line_right_x); + + if row_left_x < 0.0 { + self.windings.culled = true; + + if row_right_x < 0.0 { + // Although the line may cross the left edge, the rightmost point in + // this row may still be fully left of the viewport. In this case, + // record the winding and emit no tiles. + self.windings.mark_row_active(y_idx as usize); + + let crosses_top = (w_single & W) != 0; + if crosses_top { + self.windings.coarse[y_idx as usize] += dir; + } + + let fractional_coverage = + calc_fractional_coverage!(y_idx, row_top_y, row_bottom_y); + let target_row = &mut self.windings.partial[y_idx as usize]; + let current = f32x4::from_slice(s, target_row); + + let double_count = if crosses_top { + f_dir_v + } else { + f32x4::splat(s, 0.0) + }; + let next = fractional_coverage + .mul_add(f_dir_v, current - double_count); + next.store_slice(target_row); + + return; + } else { + // The line crosses into the viewport in this row. Record only the + // fractional portion of the winding, as the coarse winding will + // naturally get included by the clamped tile logic! + let y_slope = dy / dx; + let y_intersect = row_top_y - (row_top_x * y_slope); + + let (off_screen_top_y, off_screen_bottom_y) = if row_top_x < 0.0 + { + (row_top_y, f32::min(row_bottom_y, y_intersect)) + } else { + (f32::max(row_top_y, y_intersect), row_bottom_y) + }; + + if off_screen_top_y < off_screen_bottom_y { + self.windings.mark_row_active(y_idx as usize); + let fractional_coverage = calc_fractional_coverage!( + y_idx, + off_screen_top_y, + off_screen_bottom_y + ); + let target_row = &mut self.windings.partial[y_idx as usize]; + let current = f32x4::from_slice(s, target_row); + let next = fractional_coverage.mul_add(f_dir_v, current); + next.store_slice(target_row); + } + } + } + + let x_start = row_left_x as u16; + let x_end = (row_right_x as u16).min(tile_columns.saturating_sub(1)); + + if x_start <= x_end { + let winding = if x_start == x_end { w_single } else { w_start }; + + self.tile_buf + .push(Tile::new(x_start, y_idx, line_idx, winding)); + + for x_idx in x_start.saturating_add(1)..x_end { + self.tile_buf.push(Tile::new(x_idx, y_idx, line_idx, 0)); + } + + if x_start < x_end { + self.tile_buf.push(Tile::new(x_end, y_idx, line_idx, w_end)); + } + } + } + }; + + let is_start_culled = line_top_y < 0.0; + if !is_start_culled { + let y = f32::from(y_top_tiles); + let row_bottom_y = (y + 1.0).min(line_bottom_y); + let mask = ((y >= line_top_y) as u32) << WINDING_SHIFT; + push_row( + y_top_tiles, + line_top_y, + row_bottom_y, + w_start_base & mask, + w_end_base & mask, + W & mask, + ); + } + + let y_start = if is_start_culled { + y_top_tiles + } else { + y_top_tiles + 1 + }; + + for y_idx in y_start..y_bottom_tiles { + let y = f32::from(y_idx); + let row_bottom_y = (y + 1.0).min(line_bottom_y); + push_row(y_idx, y, row_bottom_y, w_start_base, w_end_base, W); + } + } + } else { + // Case line is fully contained within a single tile: These also cannot cross edges! + let tile = Tile::new_clamped( + (line_left_x as u16).min(tile_columns + 1), + y_top_tiles, + line_idx, + ((f32::from(y_top_tiles) >= line_top_y) as u32) << WINDING_SHIFT, + ); + self.tile_buf.push(tile); + } + } + + self.windings.culled + } + + /// Generates tile commands for MSAA (Multisample Anti-Aliasing) rasterization. + /// + /// [ Architecture & Watertightness ] + /// The primary goal of this function is to establish a source of "ground truth" for line-tile + /// intersections. Because the downstream rasterization (MSAA) occurs in parallel, it is + /// critical that intersections are "watertight." If Thread A handles Tile (0,0) and Thread B + /// handles Tile (1,0), they must agree exactly on whether a line crosses the shared edge. + /// + /// While calculating exact intersection coordinates here is feasible, it is computationally + /// expensive. Instead, we defer the heavy math to the GPU/rasterizer and produce a lightweight + /// Intersection Bitmask. This mask unambiguously defines which edges of a tile a line segment + /// touches or crosses. + /// + /// [ The Intersection Bitmask (5 bits) ] + /// The bitmask encodes winding information and edge intersections. A line is said to + /// "intersect" an edge if it touches that edge AND continues into the neighboring tile. + /// + /// Bit representation: + /// Bit: 4 | 3 | 2 | 1 | 0 + /// Val: W | R | L | B | T + /// + /// - W (Winding): Tracks whether the line touched the top edge of the tile. + /// - R/L/B/T: Right, Left, Bottom, and Top edge intersections. + pub fn make_tiles_msaa(&mut self, lines: &[Line], width: u16, height: u16) { + self.reset(); + + if width == 0 || height == 0 { + return; + } + + debug_assert!( + lines.len() <= MAX_LINES_PER_PATH as usize, + "Max. number of lines per path exceeded. Max is {}, got {}.", + MAX_LINES_PER_PATH, + lines.len() + ); + + let tile_columns = width.div_ceil(Tile::WIDTH); + let tile_rows = height.div_ceil(Tile::HEIGHT); + + for (line_idx, line) in lines.iter().take(MAX_LINES_PER_PATH as usize).enumerate() { + let line_idx = line_idx as u32; + + let p0_x = line.p0.x / f32::from(Tile::WIDTH); + let p0_y = line.p0.y / f32::from(Tile::HEIGHT); + let p1_x = line.p1.x / f32::from(Tile::WIDTH); + let p1_y = line.p1.y / f32::from(Tile::HEIGHT); + + let (line_left_x, line_right_x) = if p0_x < p1_x { + (p0_x, p1_x) + } else { + (p1_x, p0_x) + }; + + // Lines whose left-most endpoint exceed the right edge of the viewport are culled + if line_left_x > tile_columns as f32 { + continue; + } + + 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) + }; + + // The `as u16` casts here intentionally clamp negative coordinates to 0. + let y_top_tiles = (line_top_y as u16).min(tile_rows); + let line_bottom_y_ceil = line_bottom_y.ceil(); + let y_bottom_tiles = (line_bottom_y_ceil as u16).min(tile_rows); + + // If y_top_tiles == y_bottom_tiles, then the line is either completely above or below + // the viewport OR it is perfectly horizontal and aligned to the tile grid, contributing + // no winding. In either case, it should be culled. + if y_top_tiles >= y_bottom_tiles { + continue; + } + + // Get tile coordinates for start/end points, use i32 to preserve negative coordinates + let p0_tile_x = line_top_x.floor() as i32; + let p0_tile_y = line_top_y.floor() as i32; + let p1_tile_x = line_bottom_x.floor() as i32; + + let p1_tile_y = if line_bottom_y == line_bottom_y_ceil { + line_bottom_y as i32 - 1 + } else { + line_bottom_y.floor() as i32 + }; + + // Special-case out lines which are fully contained within a tile. + let not_same_tile = p0_tile_y != p1_tile_y || p0_tile_x != p1_tile_x; + if not_same_tile { + // For ease of logic, special-case purely vertical tiles. + if line_left_x == line_right_x { + let x = (line_left_x as u16).min(tile_columns.saturating_sub(1)); + + // Row Start, not culled. + let is_start_culled = line_top_y < 0.0; + if !is_start_culled { + let winding = + ((f32::from(y_top_tiles) >= line_top_y) as u32) << WINDING_SHIFT; + let intersection_mask = B | winding; + let tile = Tile::new_clamped(x, y_top_tiles, line_idx, intersection_mask); + self.tile_buf.push(tile); + } + + // Middle + // If the start was culled, the first tile inside the viewport is a middle. + let y_start = if is_start_culled { + y_top_tiles + } else { + y_top_tiles + 1 + }; + let line_bottom_floor = line_bottom_y.floor(); + let y_end_idx = (line_bottom_floor as u16).min(tile_rows); + + if y_start < y_end_idx { + let y_last = y_end_idx - 1; + for y_idx in y_start..y_last { + let intersection_mask = W | B | T; + let tile = Tile::new_clamped(x, y_idx, line_idx, intersection_mask); + self.tile_buf.push(tile); + } + + // Perfect touching B case. + { + let is_end_tile = ((y_last as i32) == p1_tile_y) as u32; + let intersection_mask = W | T | ((1 ^ is_end_tile) << BOT_SHIFT); + let tile = Tile::new_clamped(x, y_last, line_idx, intersection_mask); + self.tile_buf.push(tile); + } + } + + // Row End, handle the final tile (y_end_idx), but *only* if the line does + // not perfectly end on the top edge of the tile. In the case that it does, + // it gets handled by the middle logic above. + if line_bottom_y != line_bottom_floor && y_end_idx < tile_rows { + let intersection_mask = W | T; + let tile = Tile::new_clamped(x, y_end_idx, line_idx, intersection_mask); + self.tile_buf.push(tile); + } + } else { + let dx = p1_x - p0_x; + let dy = p1_y - p0_y; + let x_slope = dx / dy; + let dx_dir = (line_bottom_x >= line_top_x) as u32; + let not_dx_dir = dx_dir ^ 1; + + let w_start_base = dx_dir << WINDING_SHIFT; + let w_end_base = not_dx_dir << WINDING_SHIFT; + + // Check if the line is fully within the horizontal viewport bounds. If it is, + // we can skip the min/max clamping per row. + let min_x = p0_x.min(p1_x); + let max_x = p0_x.max(p1_x); + // Note: We use >= on the right edge to ensure strictly safe integer truncation + let needs_clamping = min_x < line_left_x || max_x >= line_right_x; + + // Handles the bitmask logic for start/end tiles. Invariant to clamping. + macro_rules! push_edge { + ($x:expr, $y:expr, $row_top_x:expr, $row_bottom_x:expr, + $canonical_start:expr, $canonical_end:expr, $winding_input:expr, + $check_s:tt, $check_e:expr) => {{ + let x_idx = $x; + + let unc_row_start = (x_idx as i32 == $canonical_start) as u32; + let unc_row_end = (x_idx == $canonical_end) as u32; + + let canonical_row_start = + (dx_dir & unc_row_start) | (not_dx_dir & unc_row_end); + let canonical_row_end = + (not_dx_dir & unc_row_start) | (dx_dir & unc_row_end); + + let start_tile = if $check_s { + ((x_idx as i32 == p0_tile_x) && ($y as i32 == p0_tile_y)) as u32 + } else { + 0 + }; + + let end_tile = if $check_e { + ((x_idx as i32 == p1_tile_x) && ($y as i32 == p1_tile_y)) as u32 + } else { + 0 + }; + + let mut mask = $winding_input; + + // Entrant/Exit + mask |= canonical_row_start & (1 ^ start_tile); + mask |= (1 ^ canonical_row_start) << not_dx_dir << LEFT_SHIFT; + mask |= (canonical_row_end & (1 ^ end_tile)) << BOT_SHIFT; + mask |= (1 ^ canonical_row_end) << dx_dir << LEFT_SHIFT; + + // Corner + let x_left_f = x_idx as f32; + let x_right_f = (x_idx + 1) as f32; + let trc = (($row_top_x == x_right_f) as u32) & (1 ^ start_tile); + let tlc = (($row_top_x == x_left_f) as u32) & (1 ^ start_tile); + let brc = (($row_bottom_x == x_right_f) as u32) & (1 ^ end_tile); + let blc = (($row_bottom_x == x_left_f) as u32) & (1 ^ end_tile); + // Top left is handled specially + let tie_break = tlc & (canonical_row_start ^ 1); + + mask |= (tie_break | blc) << LEFT_SHIFT; + mask |= (trc | brc) << RIGHT_SHIFT; + mask &= !(tie_break | trc); + mask &= !((blc | brc) << BOT_SHIFT); + + self.tile_buf.push(Tile::new(x_idx, $y, line_idx, mask)); + }}; + } + + // Handles row geometry and clamping logic. + macro_rules! process_row { + ($y_idx:expr, $row_top_y:expr, $row_bottom_y:expr, $w_mask:expr, + $check_s:tt, $check_e:tt, $clamped:tt) => {{ + let row_top_x = p0_x + ($row_top_y - p0_y) * x_slope; + let row_bottom_x = p0_x + ($row_bottom_y - p0_y) * x_slope; + + let (row_left_x, row_right_x, x_end) = if $clamped { + let lx = f32::min(row_top_x, row_bottom_x).max(line_left_x); + let rx = f32::max(row_top_x, row_bottom_x).min(line_right_x); + let xe = (rx as u16).min(tile_columns.saturating_sub(1)); + (lx, rx, xe) + } else { + let lx = f32::min(row_top_x, row_bottom_x); + let rx = f32::max(row_top_x, row_bottom_x); + let xe = rx as u16; // Safe because we checked bounds earlier + (lx, rx, xe) + }; + + let canonical_x_start = row_left_x.floor() as i32; + let canonical_x_end = row_right_x as u16; + let x_start = row_left_x as u16; + + if x_start <= x_end { + let is_single = (x_start == x_end) as u32; + let w_left = (w_start_base | (is_single << 4)) & $w_mask; + + push_edge!( + x_start, + $y_idx, + row_top_x, + row_bottom_x, + canonical_x_start, + canonical_x_end, + w_left, + $check_s, + $check_e + ); + + for x_idx in x_start.saturating_add(1)..x_end { + self.tile_buf + .push(Tile::new(x_idx, $y_idx, line_idx, R | L)); + } + + if x_start < x_end { + let w_right = w_end_base & $w_mask; + push_edge!( + x_end, + $y_idx, + row_top_x, + row_bottom_x, + canonical_x_start, + canonical_x_end, + w_right, + $check_s, + $check_e + ); + } + } + }}; + } + + // Central macro + macro_rules! run_loops { + ($clamped:tt) => {{ + // Top Row + let is_start_culled = line_top_y < 0.0; + if !is_start_culled { + let y = f32::from(y_top_tiles); + let row_bottom_y = (y + 1.0).min(line_bottom_y); + let mask = ((y >= line_top_y) as u32) << WINDING_SHIFT; + process_row!( + y_top_tiles, + line_top_y, + row_bottom_y, + mask, + true, + true, + $clamped + ); + } + + let y_start_middle = if is_start_culled { + y_top_tiles + } else { + y_top_tiles + 1 + }; + let line_bottom_floor = line_bottom_y.floor(); + let y_end_middle = (line_bottom_floor as u16).min(tile_rows); + let has_separate_bottom_row = line_bottom_y != line_bottom_floor + && y_end_middle < tile_rows + && (is_start_culled || y_end_middle != y_top_tiles); + + if y_start_middle < y_end_middle { + for y_idx in y_start_middle..y_end_middle { + let y = f32::from(y_idx); + let row_bottom_y = (y + 1.0).min(line_bottom_y); + let is_last_middle = y_idx == y_end_middle - 1; + let check_end = is_last_middle && !has_separate_bottom_row; + + process_row!( + y_idx, + y, + row_bottom_y, + u32::MAX, + false, + check_end, + $clamped + ); + } + } + + // Bottom Row + if has_separate_bottom_row { + let y_idx = y_end_middle; + let y = f32::from(y_idx); + process_row!( + y_idx, + y, + line_bottom_y, + u32::MAX, + false, + true, + $clamped + ); + } + }}; + } + + if needs_clamping { + run_loops!(true); + } else { + run_loops!(false); + } + } + } else { + // Case: Line is fully contained within a single tile. + let tile = Tile::new_clamped( + (line_left_x as u16).min(tile_columns + 1), + y_top_tiles, + line_idx, + ((f32::from(y_top_tiles) >= line_top_y) as u32) << WINDING_SHIFT, + ); + self.tile_buf.push(tile); + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::flatten::{FlattenCtx, Line, Point, fill}; + use crate::geometry::RectU16; + use crate::kurbo::{Affine, BezPath}; + use crate::tile::CulledWindings; + use crate::tile::{B, L, R, T, Tile, Tiles, W}; + use fearless_simd::Level; + use std::vec::Vec; + + const VIEW_DIM: u16 = 100; + const F_V_DIM: f32 = VIEW_DIM as f32; + + impl Tiles { + fn assert_tiles_match( + &mut self, + lines: &[Line], + width: u16, + height: u16, + expected: &[Tile], + ) { + self.make_tiles_msaa(lines, width, height); + assert_eq!(self.tile_buf, expected, "MSAA: Tile buffer mismatch"); + + self.make_tiles_analytic_aa(Level::baseline(), lines, width, height); + check_analytic_aa_matches(&self.tile_buf, expected); + } + } + + fn check_analytic_aa_matches(actual: &[Tile], expected: &[Tile]) { + assert_eq!( + actual.len(), + expected.len(), + "Analytic AA: Tile count mismatch." + ); + + for (i, (got, want)) in actual.iter().zip(expected.iter()).enumerate() { + assert_eq!(got.x, want.x, "Analytic AA: Tile[{}] X mismatch", i); + assert_eq!(got.y, want.y, "Analytic AA: Tile[{}] Y mismatch", i); + assert_eq!( + got.line_idx(), + want.line_idx(), + "Analytic AA: Tile[{}] Line Index mismatch", + i + ); + + let got_winding = got.packed_winding_line_idx & W; + let want_winding = want.packed_winding_line_idx & W; + assert_eq!( + got_winding, want_winding, + "Analytic AA: Tile[{}] Winding mismatch", + i + ); + } + } + + //============================================================================================== + // Culled Lines + //============================================================================================== + #[test] + fn cull_sloped_outside_lines() { + let lines = [ + Line { + p0: Point { x: 1.0, y: -7.0 }, + p1: Point { x: 3.0, y: -1.0 }, + }, + Line { + p0: Point { x: 1.0, y: -11.0 }, + p1: Point { x: 3.0, y: -1.0 }, + }, + Line { + p0: Point { + x: F_V_DIM + 1.0, + y: 50.0, + }, + p1: Point { + x: F_V_DIM + 3.0, + y: 70.0, + }, + }, + Line { + p0: Point { + x: 1.0, + y: F_V_DIM + 1.0, + }, + p1: Point { + x: 3.0, + y: F_V_DIM + 7.0, + }, + }, + Line { + p0: Point { + x: 1.0, + y: F_V_DIM + 1.0, + }, + p1: Point { + x: 3.0, + y: F_V_DIM + 13.0, + }, + }, + ]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &[]); + } + + #[test] + fn sloped_line_crossing_top() { + let lines = [ + Line { + p0: Point { x: -2.0, y: -3.0 }, + p1: Point { x: 2.0, y: 1.0 }, + }, + Line { + p0: Point { x: 6.0, y: -1.0 }, + p1: Point { x: 5.0, y: 2.0 }, + }, + Line { + p0: Point { x: 9.0, y: -10.0 }, + p1: Point { x: 10.0, y: 3.0 }, + }, + Line { + p0: Point { x: 2.0, y: 1.0 }, + p1: Point { x: -2.0, y: -3.0 }, + }, + ]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(0, 0, 0, W | T), + Tile::new(1, 0, 1, W | T), + Tile::new(2, 0, 2, W | T), + Tile::new(0, 0, 3, W | T), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn sloped_line_crossing_bot() { + let lines = [ + Line { + p0: Point { + x: 5.0, + y: F_V_DIM + 3.0, + }, + p1: Point { + x: 6.0, + y: F_V_DIM - 2.0, + }, + }, + Line { + p0: Point { + x: 10.0, + y: F_V_DIM + 1.0, + }, + p1: Point { + x: 9.0, + y: F_V_DIM - 1.0, + }, + }, + Line { + p0: Point { + x: 2.0, + y: F_V_DIM - 2.0, + }, + p1: Point { + x: 3.0, + y: F_V_DIM + 3.0, + }, + }, + ]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(1, 24, 0, B), + Tile::new(2, 24, 1, B), + Tile::new(0, 24, 2, B), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn sloped_line_crossing_top_multi_tile() { + let lines = [ + Line { + p0: Point { x: 1.0, y: -5.0 }, + p1: Point { x: 6.0, y: 7.0 }, + }, + Line { + p0: Point { x: 2.5, y: -10.0 }, + p1: Point { x: 3.5, y: 6.0 }, + }, + ]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(0, 0, 0, W | T | R), + Tile::new(1, 0, 0, L | B), + Tile::new(1, 1, 0, W | T), + Tile::new(0, 0, 1, W | T | B), + Tile::new(0, 1, 1, W | T), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn sloped_line_crossing_bot_multi_tile() { + let lines = [ + Line { + p0: Point { + x: 12.0, + y: F_V_DIM + 10.0, + }, + p1: Point { x: 2.0, y: 94.0 }, + }, + Line { + p0: Point { + x: 1.5, + y: F_V_DIM + 5.0, + }, + p1: Point { x: 3.5, y: 94.0 }, + }, + ]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(0, 23, 0, B), + Tile::new(0, 24, 0, W | T | R), + Tile::new(1, 24, 0, B | L), + Tile::new(0, 23, 1, B), + Tile::new(0, 24, 1, W | T | B), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn sloped_line_crossing_right() { + let lines = [ + Line { + p0: Point { x: 97.0, y: 1.0 }, + p1: Point { + x: F_V_DIM + 1.0, + y: 2.0, + }, + }, + Line { + p0: Point { x: 93.0, y: 1.0 }, + p1: Point { + x: F_V_DIM + 5.0, + y: 2.0, + }, + }, + ]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(24, 0, 0, R), + Tile::new(23, 0, 1, R), + Tile::new(24, 0, 1, R | L), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn sloped_line_crossing_left() { + let lines = [ + Line { + p0: Point { x: -5.0, y: 1.0 }, + p1: Point { x: 1.0, y: 2.0 }, + }, + Line { + p0: Point { x: -5.0, y: 1.0 }, + p1: Point { x: 5.0, y: 2.0 }, + }, + Line { + p0: Point { x: -5.0, y: 1.0 }, + p1: Point { x: 13.0, y: 9.0 }, + }, + ]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(0, 0, 0, L), + Tile::new(0, 0, 1, L | R), + Tile::new(1, 0, 1, L), + Tile::new(0, 0, 2, L | B), + Tile::new(0, 1, 2, W | R | T), + Tile::new(1, 1, 2, R | L), + Tile::new(2, 1, 2, L | B), + Tile::new(2, 2, 2, W | R | T), + Tile::new(3, 2, 2, L), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn horizontal_line_above_viewport() { + let lines = [Line { + p0: Point { x: 10.0, y: -5.0 }, + p1: Point { x: 90.0, y: -5.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &[]); + } + + #[test] + fn horizontal_line_below_viewport() { + let lines = [Line { + p0: Point { + x: 10.0, + y: F_V_DIM + 5.0, + }, + p1: Point { + x: 90.0, + y: F_V_DIM + 5.0, + }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &[]); + } + + #[test] + fn horizontal_line_crossing_left_viewport() { + let lines = [Line { + p0: Point { x: -10.0, y: 10.0 }, + p1: Point { x: 10.0, y: 10.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(0, 2, 0, L | R), + Tile::new(1, 2, 0, L | R), + Tile::new(2, 2, 0, L), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn horizontal_line_crossing_right_viewport() { + let lines = [Line { + p0: Point { + x: F_V_DIM - 5.0, + y: 10.0, + }, + p1: Point { + x: F_V_DIM + 5.0, + y: 10.0, + }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [Tile::new(23, 2, 0, R), Tile::new(24, 2, 0, L | R)]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn vertical_lines_outside_viewport() { + let lines = [ + Line { + p0: Point { x: 1.0, y: -5.0 }, + p1: Point { x: 1.0, y: -1.0 }, + }, + Line { + p0: Point { + x: 1.0, + y: F_V_DIM + 1.0, + }, + p1: Point { + x: 1.0, + y: F_V_DIM + 5.0, + }, + }, + ]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &[]); + } + + #[test] + fn vertical_path_on_the_right_of_viewport() { + const VIEWPORT_WIDTH: u16 = 10; + const VIEWPORT_HEIGHT: u16 = 10; + + let path = BezPath::from_svg("M261,0 L78848,0 L78848,4 L261,4 Z").unwrap(); + let mut line_buf: Vec = Vec::new(); + fill( + Level::try_detect().unwrap_or(Level::baseline()), + &path, + Affine::IDENTITY, + &mut line_buf, + &mut FlattenCtx::default(), + RectU16::new(0, 0, VIEWPORT_WIDTH, VIEWPORT_HEIGHT), + ); + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + tiles.assert_tiles_match(&line_buf, VIEWPORT_WIDTH, VIEWPORT_HEIGHT, &[]); + } + + #[test] + fn vertical_line_crossing_top_viewport() { + let lines = [ + Line { + p0: Point { x: 1.0, y: -7.0 }, + p1: Point { x: 1.0, y: 3.0 }, + }, + Line { + p0: Point { x: 1.0, y: -7.0 }, + p1: Point { x: 1.0, y: 7.0 }, + }, + Line { + p0: Point { x: 1.0, y: -7.0 }, + p1: Point { x: 1.0, y: 8.0 }, + }, + ]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(0, 0, 0, W | T), + Tile::new(0, 0, 1, W | B | T), + Tile::new(0, 1, 1, W | T), + Tile::new(0, 0, 2, W | B | T), + Tile::new(0, 1, 2, W | T), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn vertical_line_crossing_bot_viewport() { + let lines = [ + Line { + p0: Point { + x: 1.0, + y: F_V_DIM - 1.0, + }, + p1: Point { + x: 1.0, + y: F_V_DIM + 5.0, + }, + }, + Line { + p0: Point { + x: 1.0, + y: F_V_DIM - 5.0, + }, + p1: Point { + x: 1.0, + y: F_V_DIM + 5.0, + }, + }, + ]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(0, 24, 0, B), + Tile::new(0, 23, 1, B), + Tile::new(0, 24, 1, W | T | B), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn clip_top_left_corner() { + let lines = [Line { + p0: Point { x: -1.0, y: 2.0 }, + p1: Point { x: 2.0, y: -1.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [Tile::new(0, 0, 0, W | L | T)]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn clip_bottom_right_corner() { + let lines = [Line { + p0: Point { + x: F_V_DIM + 1.0, + y: F_V_DIM - 2.0, + }, + p1: Point { + x: F_V_DIM - 2.0, + y: F_V_DIM + 1.0, + }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [Tile::new(24, 24, 0, R | B)]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + //============================================================================================== + // Axis-aligned lines + //============================================================================================== + #[test] + fn horizontal_line_left_to_right_three_tile() { + let lines = [Line { + p0: Point { x: 1.5, y: 1.0 }, + p1: Point { x: 8.5, y: 1.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(0, 0, 0, R), + Tile::new(1, 0, 0, R | L), + Tile::new(2, 0, 0, L), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn horizontal_line_right_to_left_three_tile() { + let lines = [Line { + p0: Point { x: 8.5, y: 1.0 }, + p1: Point { x: 1.5, y: 1.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(0, 0, 0, R), + Tile::new(1, 0, 0, R | L), + Tile::new(2, 0, 0, L), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn horizontal_line_multi_tile() { + let lines = [Line { + p0: Point { x: 1.5, y: 1.0 }, + p1: Point { x: 12.5, y: 1.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(0, 0, 0, R), + Tile::new(1, 0, 0, R | L), + Tile::new(2, 0, 0, R | L), + Tile::new(3, 0, 0, L), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn vertical_line_down_three_tile() { + let lines = [Line { + p0: Point { x: 1.0, y: 1.5 }, + p1: Point { x: 1.0, y: 8.5 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(0, 0, 0, B), + Tile::new(0, 1, 0, W | T | B), + Tile::new(0, 2, 0, W | T), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn vertical_line_down_multi_tile() { + let lines = [Line { + p0: Point { x: 1.0, y: 1.0 }, + p1: Point { x: 1.0, y: 13.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(0, 0, 0, B), + Tile::new(0, 1, 0, W | T | B), + Tile::new(0, 2, 0, W | T | B), + Tile::new(0, 3, 0, W | T), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn vertical_line_up_three_tile() { + let lines = [Line { + p0: Point { x: 1.0, y: 13.0 }, + p1: Point { x: 1.0, y: 1.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(0, 0, 0, B), + Tile::new(0, 1, 0, W | T | B), + Tile::new(0, 2, 0, W | T | B), + Tile::new(0, 3, 0, W | T), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn vertical_line_up_multi_tile() { + let lines = [Line { + p0: Point { x: 1.0, y: 8.5 }, + p1: Point { x: 1.0, y: 1.5 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(0, 0, 0, B), + Tile::new(0, 1, 0, W | T | B), + Tile::new(0, 2, 0, W | T), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + // Exclusive to the bottom edge, no P required. + #[test] + fn vertical_line_touching_bot() { + let lines = [Line { + p0: Point { x: 1.0, y: 1.0 }, + p1: Point { x: 1.0, y: 8.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [Tile::new(0, 0, 0, B), Tile::new(0, 1, 0, W | T)]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn vertical_line_touching_top() { + let lines = [Line { + p0: Point { x: 1.0, y: 0.0 }, + p1: Point { x: 1.0, y: 7.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [Tile::new(0, 0, 0, W | B), Tile::new(0, 1, 0, W | T)]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + //============================================================================================== + // Sloped Lines + //============================================================================================== + #[test] + fn top_left_to_bottom_right() { + let lines = [Line { + p0: Point { x: 1.0, y: 1.0 }, + p1: Point { x: 11.0, y: 9.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(0, 0, 0, R), + Tile::new(1, 0, 0, L | B), + Tile::new(1, 1, 0, W | R | T), + Tile::new(2, 1, 0, L | B), + Tile::new(2, 2, 0, W | T), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn bottom_right_to_top_left() { + let lines = [Line { + p0: Point { x: 11.0, y: 9.0 }, + p1: Point { x: 1.0, y: 1.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(0, 0, 0, R), + Tile::new(1, 0, 0, L | B), + Tile::new(1, 1, 0, W | R | T), + Tile::new(2, 1, 0, L | B), + Tile::new(2, 2, 0, W | T), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn bottom_left_to_top_right() { + let lines = [Line { + p0: Point { x: 2.0, y: 11.0 }, + p1: Point { x: 14.0, y: 6.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(2, 1, 0, R | B), + Tile::new(3, 1, 0, L), + Tile::new(0, 2, 0, R), + Tile::new(1, 2, 0, R | L), + Tile::new(2, 2, 0, W | L | T), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn top_right_to_bottom_left() { + let lines = [Line { + p0: Point { x: 14.0, y: 6.0 }, + p1: Point { x: 2.0, y: 11.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(2, 1, 0, R | B), + Tile::new(3, 1, 0, L), + Tile::new(0, 2, 0, R), + Tile::new(1, 2, 0, R | L), + Tile::new(2, 2, 0, W | L | T), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn two_lines_in_single_tile() { + let lines = [ + Line { + p0: Point { x: 1.0, y: 3.0 }, + p1: Point { x: 3.0, y: 3.0 }, + }, + Line { + p0: Point { x: 3.0, y: 3.0 }, + p1: Point { x: 0.0, y: 1.0 }, + }, + ]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [Tile::new(0, 0, 0, 0), Tile::new(0, 0, 1, 0)]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn diagonal_cross_corner() { + let lines = [Line { + p0: Point { x: 3.0, y: 5.0 }, + p1: Point { x: 5.0, y: 3.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(1, 0, 0, L), + Tile::new(0, 1, 0, R), + Tile::new(1, 1, 0, W | L | T), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn diagonal_cross_corner_two() { + let lines = [Line { + p0: Point { x: 7.9, y: 7.9 }, + p1: Point { x: 0.1, y: 0.1 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(0, 0, 0, R), + Tile::new(1, 0, 0, L), + Tile::new(1, 1, 0, W | T), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn diagonal_down_slope_tiles() { + let lines = [Line { + p0: Point { x: 5.0, y: 5.0 }, + p1: Point { x: 9.0, y: 9.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(1, 1, 0, R), + Tile::new(2, 1, 0, L), + Tile::new(2, 2, 0, W | T), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn diagonal_up_slope_tiles() { + let lines = [Line { + p0: Point { x: 5.0, y: 9.0 }, + p1: Point { x: 9.0, y: 5.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(1, 1, 0, R | B), + Tile::new(2, 1, 0, L), + Tile::new(1, 2, 0, W | T), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn diagonal_down_one_tile() { + let lines = [Line { + p0: Point { x: 0.0, y: 0.0 }, + p1: Point { x: 4.0, y: 4.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [Tile::new(0, 0, 0, W | R), Tile::new(1, 0, 0, L)]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn diagonal_up_one_tile() { + let lines = [Line { + p0: Point { x: 0.0, y: 4.0 }, + p1: Point { x: 4.0, y: 0.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [Tile::new(0, 0, 0, R), Tile::new(1, 0, 0, W | L)]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn diagonal_down_two_tile() { + let lines = [Line { + p0: Point { x: 0.0, y: 0.0 }, + p1: Point { x: 8.0, y: 8.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(0, 0, 0, W | R), + Tile::new(1, 0, 0, L), + Tile::new(1, 1, 0, W | R | T), + Tile::new(2, 1, 0, L), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn diagonal_up_two_tile() { + let lines = [Line { + p0: Point { x: 0.0, y: 8.0 }, + p1: Point { x: 8.0, y: 0.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(1, 0, 0, R | L), + Tile::new(2, 0, 0, W | L), + Tile::new(0, 1, 0, R), + Tile::new(1, 1, 0, W | L | T), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn sloped_ending_right() { + let lines = [Line { + p0: Point { x: 1.0, y: 1.0 }, + p1: Point { x: 8.0, y: 2.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(0, 0, 0, R), + Tile::new(1, 0, 0, R | L), + Tile::new(2, 0, 0, L), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn sloped_touching_top() { + let lines = [Line { + p0: Point { x: 0.0, y: 8.0 }, + p1: Point { x: 4.0, y: 0.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(0, 0, 0, R | B), + Tile::new(1, 0, 0, W | L), + Tile::new(0, 1, 0, W | T), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn sloped_touching_bot() { + let lines = [Line { + p0: Point { x: 0.0, y: 0.0 }, + p1: Point { x: 4.0, y: 8.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [ + Tile::new(0, 0, 0, W | B), + Tile::new(0, 1, 0, W | R | T), + Tile::new(1, 1, 0, L), + ]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + //============================================================================================== + // Same Tile Cases + //============================================================================================== + #[test] + fn same_tile() { + let lines = [Line { + p0: Point { x: 1.0, y: 1.0 }, + p1: Point { x: 3.0, y: 3.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [Tile::new(0, 0, 0, 0)]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn same_tile_left() { + let lines = [Line { + p0: Point { x: 0.0, y: 1.0 }, + p1: Point { x: 3.0, y: 1.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [Tile::new(0, 0, 0, 0)]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn same_tile_top() { + let lines = [Line { + p0: Point { x: 1.0, y: 0.0 }, + p1: Point { x: 1.0, y: 3.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [Tile::new(0, 0, 0, W)]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn same_tile_right() { + let lines = [Line { + p0: Point { x: 1.0, y: 1.0 }, + p1: Point { x: 4.0, y: 1.0 }, + }]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [Tile::new(0, 0, 0, R), Tile::new(1, 0, 0, L)]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn same_tile_bottom() { + let lines = [ + Line { + p0: Point { x: 1.0, y: 1.0 }, + p1: Point { x: 1.0, y: 4.0 }, + }, + Line { + p0: Point { x: 1.0, y: 1.0 }, + p1: Point { x: 2.0, y: 4.0 }, + }, + ]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [Tile::new(0, 0, 0, 0), Tile::new(0, 0, 1, 0)]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + #[test] + fn same_tile_top_left() { + let lines = [ + Line { + p0: Point { x: 0.0, y: 1.0 }, + p1: Point { x: 1.0, y: 0.0 }, + }, + Line { + p0: Point { x: 0.0, y: 0.0001 }, + p1: Point { x: 0.0001, y: 0.0 }, + }, + ]; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + let expected = [Tile::new(0, 0, 0, W), Tile::new(0, 0, 1, W)]; + + tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected); + } + + //============================================================================================== + // CulledWindings & Row Marking Logic + //============================================================================================== + #[test] + fn test_culled_windings_new_and_reset() { + let mut windings = CulledWindings::new(100); + assert_eq!(windings.partial.len(), 25); + assert_eq!(windings.coarse.len(), 25); + assert_eq!(windings.active.len(), 1); + + windings.coarse[0] = 1; + windings.active[0] = 0xFF; + windings.culled = true; + + windings.reset(); + assert_eq!(windings.coarse[0], 0); + assert_eq!(windings.active[0], 0); + + windings.coarse[0] = 1; + windings.active[0] = 0xFF; + windings.culled = false; + + windings.reset(); + assert_eq!(windings.coarse[0], 1); + assert_eq!(windings.active[0], 0xFF); + } + + #[test] + fn test_mark_row_active() { + let mut windings = CulledWindings::new(200); + windings.mark_row_active(0); + windings.mark_row_active(5); + windings.mark_row_active(31); + windings.mark_row_active(32); + assert_eq!(windings.active[0], (1 << 0) | (1 << 5) | (1 << 31)); + assert_eq!(windings.active[1], 1 << 0); + } + + #[test] + fn test_mark_row_range_single_word() { + let mut windings = CulledWindings::new(200); + windings.mark_row_range_active(5, 10); + let expected_mask = ((1_u32 << 5) - 1) << 5; + assert_eq!(windings.active[0], expected_mask); + assert_eq!(windings.active[1], 0); + } + + #[test] + fn test_mark_row_range_full_word() { + let mut windings = CulledWindings::new(200); + windings.mark_row_range_active(0, 32); + assert_eq!(windings.active[0], u32::MAX); + assert_eq!(windings.active[1], 0); + } + + #[test] + fn test_mark_row_range_spanning_two_words() { + let mut windings = CulledWindings::new(200); + windings.mark_row_range_active(30, 35); + assert_eq!(windings.active[0], (1 << 30) | (1 << 31)); + assert_eq!(windings.active[1], (1 << 0) | (1 << 1) | (1 << 2)); + } + + #[test] + fn test_mark_row_range_spanning_multiple_words() { + let mut windings = CulledWindings::new(500); + windings.mark_row_range_active(10, 80); + assert_eq!(windings.active[0], u32::MAX << 10); + assert_eq!(windings.active[1], u32::MAX); + assert_eq!(windings.active[2], (1 << 16) - 1); + assert_eq!(windings.active[3], 0); + } + + #[test] + fn test_mark_row_range_empty_or_invalid() { + let mut windings = CulledWindings::new(200); + windings.mark_row_range_active(10, 10); + windings.mark_row_range_active(15, 10); + assert_eq!(windings.active[0], 0); + assert_eq!(windings.active[1], 0); + } + + //============================================================================================== + // Miscellaneous Cases + //============================================================================================== + #[test] + // See https://github.com/LaurenzV/cpu-sparse-experiments/issues/46. + fn infinite_loop() { + let line = Line { + p0: Point { x: 22.0, y: 552.0 }, + p1: Point { x: 224.0, y: 388.0 }, + }; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + tiles.make_tiles_msaa(&[line], 600, 600); + tiles.make_tiles_analytic_aa(Level::baseline(), &[line], 600, 600); + } + + #[test] + // See https://github.com/linebender/vello/issues/1321 + fn overflow() { + let line = Line { + p0: Point { + x: 59.60001, + y: 40.78, + }, + p1: Point { + x: 520599.6, + y: 100.18, + }, + }; + + let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::baseline()), VIEW_DIM); + tiles.make_tiles_analytic_aa(Level::baseline(), &[line], 200, 100); + tiles.make_tiles_msaa(&[line], 200, 100); + } + + #[test] + fn sort_test() { + let mut lines: Vec = Vec::new(); + let mut tiles = Tiles::new(Level::baseline(), VIEW_DIM); + + let step = 4.0; + let mut y = F_V_DIM - 10.0; + while y > 10.0 { + lines.push(Line { + p0: Point { + x: F_V_DIM - 10.0, + y, + }, + p1: Point { x: 10.0, y }, + }); + + lines.push(Line { + p0: Point { + x: F_V_DIM - 12.0, + y, + }, + p1: Point { x: 12.0, y }, + }); + + y -= step; + } + + tiles.make_tiles_msaa(&lines, VIEW_DIM, VIEW_DIM); + assert!(tiles.tile_buf.first().unwrap().y > tiles.tile_buf.last().unwrap().y); + tiles.sort_tiles(); + check_sorted(&tiles.tile_buf); + + tiles.make_tiles_analytic_aa(Level::baseline(), &lines, VIEW_DIM, VIEW_DIM); + assert!(tiles.tile_buf.first().unwrap().y > tiles.tile_buf.last().unwrap().y); + tiles.sort_tiles(); + check_sorted(&tiles.tile_buf); + } + + fn check_sorted(buf: &[Tile]) { + for i in 0..buf.len() - 1 { + let current = buf[i]; + let next = buf[i + 1]; + + if current.y > next.y { + panic!( + "Sort Failure [Y]: Tile[{}] (y={}) > Tile[{}] (y={})", + i, + current.y, + i + 1, + next.y + ); + } + + if current.y == next.y { + if current.x > next.x { + panic!( + "Sort Failure [X]: at Row y={}, Tile[{}] (x={}) > Tile[{}] (x={})", + current.y, + i, + current.x, + i + 1, + next.x + ); + } + + if current.x == next.x + && current.packed_winding_line_idx > next.packed_winding_line_idx + { + panic!( + "Sort Failure [Payload]: at {}x{}, Tile[{}] (val={}) > Tile[{}] (val={})", + current.x, + current.y, + i, + current.packed_winding_line_idx, + i + 1, + next.packed_winding_line_idx + ); + } + } + } + } +} diff --git a/vendor/vello_common/src/util.rs b/vendor/vello_common/src/util.rs new file mode 100644 index 000000000..a2dce6ea5 --- /dev/null +++ b/vendor/vello_common/src/util.rs @@ -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(val: f32x16) -> u8x16 { + let simd = val.simd; + let converted = val.to_int::>().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 Div255Ext for u16x32 { + #[inline(always)] + fn div_255(self) -> Self { + let p1 = Self::splat(self.simd, 255); + let p2 = self + p1; + p2 >> 8 + } +} + +impl Div255Ext for u16x16 { + #[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(a: u8x32, b: u8x32) -> u16x32 { + (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(a: u8x16, b: u8x16) -> u16x16 { + (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)) +}