├── .cargo └── config.toml ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── assets ├── images │ ├── juice-updated.png │ ├── juice.png │ ├── logo.png │ ├── paris-30k-rendered.png │ ├── spaceship-damage.webp │ └── spaceship.webp └── svgs │ └── paris-30k.svg ├── demo ├── Cargo.toml └── src │ ├── demos │ ├── circles.rs │ ├── mod.rs │ ├── spaceship.rs │ ├── svg.rs │ └── texture.rs │ ├── main.rs │ └── runner.rs ├── docs ├── code-of-conduct.md └── contributing.md ├── e2e-tests ├── Cargo.toml ├── expected │ ├── tests__blend_modes__ColorBurn__cpu.png │ ├── tests__blend_modes__ColorDodge__cpu.png │ ├── tests__blend_modes__Color__cpu.png │ ├── tests__blend_modes__Darken__cpu.png │ ├── tests__blend_modes__Difference__cpu.png │ ├── tests__blend_modes__Exclusion__cpu.png │ ├── tests__blend_modes__HardLight__cpu.png │ ├── tests__blend_modes__Hue__cpu.png │ ├── tests__blend_modes__Lighten__cpu.png │ ├── tests__blend_modes__Luminosity__cpu.png │ ├── tests__blend_modes__Multiply__cpu.png │ ├── tests__blend_modes__Over__cpu.png │ ├── tests__blend_modes__Overlay__cpu.png │ ├── tests__blend_modes__Saturation__cpu.png │ ├── tests__blend_modes__Screen__cpu.png │ ├── tests__blend_modes__SoftLight__cpu.png │ ├── tests__clipping2__cpu.png │ ├── tests__clipping__cpu.png │ ├── tests__covers__cpu.png │ ├── tests__fill_rules__EvenOdd__cpu.png │ ├── tests__fill_rules__NonZero__cpu.png │ ├── tests__linear_gradient__cpu.png │ ├── tests__pixel__cpu.png │ ├── tests__radial_gradient__cpu.png │ ├── tests__radial_gradient__gpu.png │ ├── tests__solid_color__blue__cpu.png │ ├── tests__solid_color__dark_blue__cpu.png │ ├── tests__solid_color__dark_green__cpu.png │ ├── tests__solid_color__dark_red__cpu.png │ ├── tests__solid_color__green__cpu.png │ ├── tests__solid_color__red__cpu.png │ ├── tests__solid_color__transparent_black__cpu.png │ ├── tests__texture__cpu.png │ └── tests__texture__gpu.png └── tests │ ├── report.html │ ├── test_env.rs │ └── tests.rs └── forma ├── Cargo.toml └── src ├── composition ├── interner.rs ├── layer.rs ├── mod.rs └── state.rs ├── consts.rs ├── cpu ├── buffer │ ├── layout │ │ ├── mod.rs │ │ └── slice_cache.rs │ └── mod.rs ├── channel.rs ├── mod.rs ├── painter │ ├── layer_workbench │ │ ├── mod.rs │ │ └── passes │ │ │ ├── mod.rs │ │ │ ├── skip_fully_covered_layers.rs │ │ │ ├── skip_trivial_clips.rs │ │ │ └── tile_unchanged.rs │ ├── mod.rs │ └── styling.rs ├── pixel_segment.rs ├── rasterizer.rs └── renderer.rs ├── gpu ├── conveyor_sort │ ├── mod.rs │ └── sort.wgsl ├── mod.rs ├── painter │ ├── mod.rs │ └── paint.wgsl ├── rasterizer │ ├── mod.rs │ └── rasterizer.wgsl ├── renderer │ ├── draw_texture.wgsl │ └── mod.rs └── style_map.rs ├── lib.rs ├── math ├── mod.rs ├── point.rs └── transform.rs ├── path.rs ├── segment.rs ├── styling.rs └── utils ├── extend.rs ├── mod.rs ├── order.rs ├── prefix_scan.rs ├── simd ├── aarch64.rs ├── auto.rs ├── avx.rs ├── mod.rs └── wasm32.rs └── small_bit_set.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [build] 16 | rustflags = ["-C", "target-cpu=native"] 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Continuous integration 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | profile: minimal 14 | toolchain: stable 15 | override: true 16 | - uses: actions-rs/cargo@v1 17 | with: 18 | command: check 19 | args: --all 20 | 21 | test: 22 | name: Test Suite 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: install llvmpipe and lavapipe 27 | run: | 28 | sudo apt-get update -y -qq 29 | sudo add-apt-repository ppa:oibaf/graphics-drivers -y 30 | sudo apt-get update 31 | sudo apt install -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers 32 | - uses: actions-rs/toolchain@v1 33 | with: 34 | profile: minimal 35 | toolchain: stable 36 | override: true 37 | - uses: actions-rs/cargo@v1 38 | with: 39 | command: test 40 | 41 | fmt: 42 | name: Rustfmt 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v2 46 | - uses: actions-rs/toolchain@v1 47 | with: 48 | profile: minimal 49 | toolchain: stable 50 | override: true 51 | - run: rustup component add rustfmt 52 | - uses: actions-rs/cargo@v1 53 | with: 54 | command: fmt 55 | args: --all -- --check 56 | 57 | clippy: 58 | name: Clippy 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v2 62 | - uses: actions-rs/toolchain@v1 63 | with: 64 | profile: minimal 65 | toolchain: stable 66 | override: true 67 | - run: rustup component add clippy 68 | - uses: actions-rs/cargo@v1 69 | with: 70 | command: clippy 71 | args: -- -D warnings 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Cargo specific. 2 | /target 3 | Cargo.lock 4 | /web/pkg 5 | 6 | # Editors and IDEs. 7 | .idea 8 | .vscode 9 | 10 | # Misc. 11 | .DS_Store -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | 3 | * Added line-culling to `SegmentBuffer` in #29 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [workspace] 16 | default-members = [ 17 | "e2e-tests", 18 | "forma", 19 | ] 20 | members = [ 21 | "demo", 22 | "e2e-tests", 23 | "forma", 24 | ] 25 | resolver = "2" 26 | 27 | [profile.release-with-debug-info] 28 | inherits = "release" 29 | debug = true 30 | 31 | [workspace.dependencies] 32 | anyhow = "1.0.66" 33 | bytemuck = { version = "1.12.3", features = ["derive"] } 34 | pollster = "0.2.5" 35 | rand = { version = "0.8.5", features = ["small_rng"] } 36 | wgpu = "0.14.0" 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![forma logo](assets/images/logo.png?raw=true) 2 | 3 | [![crates.io badge](https://img.shields.io/crates/v/forma-render?style=for-the-badge)](https://crates.io/crates/forma-render) [![](https://dcbadge.vercel.app/api/server/CYtcmqgh)](https://discord.gg/CYtcmqgh) 4 | 5 | A (thoroughly) parallelized **experimental** Rust vector-graphics renderer with both a software (CPU) and hardware (GPU) 6 | back-end having the following goals, in this order: 7 | 8 | 1. **Portability**; supporting Fuchsia, Linux, macOS, Windows, Android & iOS. 9 | 2. **Performance**; making use of compute-focused pipeline that is highly parallelized both at the instruction-level and the thread-level. 10 | 3. **Simplicity**; implementing an easy-to-understand 4-stage pipeline. 11 | 4. **Size**; minimizing the number of dependencies and focusing on vector-graphics only. 12 | 13 | It relies on Rust's SIMD auto-vectorization/intrinsics and [Rayon] to have good performance on the CPU, while using [WebGPU] ([wgpu]) to take advantage of the GPU. 14 | 15 | [Rayon]: https://github.com/rayon-rs/rayon 16 | [WebGPU]: https://github.com/gpuweb/gpuweb 17 | [wgpu]: https://wgpu.rs/ 18 | 19 | ## Getting started 20 | 21 | Add the following to your `Cargo.toml` dependencies: 22 | 23 | ```toml 24 | forma = { version = "0.1.0", package = "forma-render" } 25 | ``` 26 | 27 | ## 4-stage Pipeline 28 | 29 | | 1. Curve flattening | 2. Line segment rasterization | 3. Sorting | 4. Painting | 30 | |:-------------------:|:-----------------------------:|:---------------------:|:--------------------------------:| 31 | | Bézier curves | line segments | pixel segments | sorted pixel segments, old tiles | 32 | | ⬇️⬇️⬇️ | ⬇️⬇️⬇️ | ⬇️⬇️⬇️ | ⬇️⬇️⬇️ | 33 | | line segments | pixel segments | sorted pixel segments | freshly painted tiles | 34 | 35 | ## Implementation Highlights ✨ 36 | 37 | Here are a few implementation highlights that make forma stand out from commonly used vector renderers. 38 | 39 |
40 | Curvature-aware flattening 41 | 42 | All higher cubic Béziers are approximated by quadratic ones, then, in parallel, flattened to line segments according to their curvature. This [technique] was developed by Raph Levien. 43 | 44 | [technique]: https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html 45 | 46 |
47 | 48 |
49 | Cheap translations and rotations 50 | 51 | Translations and rotations can be rendered without having to re-flatten the curves, all the while maintaining full quality. 52 | 53 |
54 | 55 |
56 | Parallel pixel grid intersection 57 | 58 | Line segments are transformed into pixel segments by intersecting them with the pixel grid. We developed a simple method that performs this computation in *O(1)* and which is run in parallel. 59 | 60 |
61 | 62 |
63 | Efficient sorting 64 | 65 | We ported [crumsort] to Rust and parallelized it with Rayon, delivering improved performance over its pdqsort implementation for 64-bit random data. Scattering pixel segments with a sort was inspired from Allan MacKinnon's work on [Spinel]. 66 | 67 | [crumsort]: https://github.com/google/crumsort-rs 68 | [Spinel]: https://cs.opensource.google/fuchsia/fuchsia/+/main:src/graphics/lib/compute/spinel/ 69 | 70 |
71 | 72 |
73 | Update only the tiles that change (currently CPU-only) 74 | 75 | We implemented a fail-fast per-tile optimizer that tries to skip the painting step entirely. A similar approach could also be tested on the GPU. 76 | 77 |
78 | 79 | | Animation as it appears on the screen | Updated tiles only | 80 | |:------------------------------------------:|:------------------------------------------------------------------------------:| 81 | | ![](assets/images/spaceship.webp?raw=true) | ![juice animation updated tiles](assets/images/spaceship-damage.webp?raw=true) | 82 | 83 | You can run the demo above with: 84 | 85 | ```sh 86 | cargo run --release -p demo -- spaceship 87 | ``` 88 | 89 | ## Similar Projects 90 | 91 | forma draws heavy inspiration from the following projects: 92 | 93 | * [Spinel], with a Vulkan 1.2 back-end 94 | * [vello], with a wgpu back-end 95 | 96 | [vello]: https://github.com/linebender/vello 97 | 98 | ## Example 99 | 100 | You can use the included `demo` example to render a few examples, one of which is a non-compliant & incomplete SVG renderer: 101 | 102 | ```sh 103 | cargo run --release -p demo -- svg assets/svgs/paris-30k.svg 104 | ``` 105 | 106 | It renders enormous SVGs at interactive framerates, even on CPU: ([compare to your web browser]) 107 | 108 | [compare to your web browser]: assets/svgs/paris-30k.svg?raw=true 109 | 110 | ![window rendering map of Germany](assets/images/paris-30k-rendered.png?raw=true) 111 | 112 | ## (Currently) Missing Pieces 🧩 113 | 114 | Since this project is work-in-progress, breakage in the API, while not drastic, is expected. The performance on the GPU back-end is also expected to improve especially on mobile where performance is known to be poor and where the CPU back-end is currently advised instead. 115 | 116 | Other than that: 117 | 118 | * Automated layer ordering 119 | * Strokes 120 | * More color spaces for blends & gradients 121 | * Faster GPU sorter 122 | * Use of `f16` for great mobile GPU performance 123 | 124 | ## Note 125 | 126 | This is not an officially supported Google product. 127 | -------------------------------------------------------------------------------- /assets/images/juice-updated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/assets/images/juice-updated.png -------------------------------------------------------------------------------- /assets/images/juice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/assets/images/juice.png -------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/assets/images/logo.png -------------------------------------------------------------------------------- /assets/images/paris-30k-rendered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/assets/images/paris-30k-rendered.png -------------------------------------------------------------------------------- /assets/images/spaceship-damage.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/assets/images/spaceship-damage.webp -------------------------------------------------------------------------------- /assets/images/spaceship.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/assets/images/spaceship.webp -------------------------------------------------------------------------------- /demo/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [package] 16 | name = "demo" 17 | version = "0.1.0" 18 | edition = "2021" 19 | 20 | [dependencies] 21 | bytemuck.workspace = true 22 | clap = { version = "3.1", features = ["derive"] } 23 | forma = { path = "../forma", features = ["gpu"], package = "forma-render"} 24 | image = "0.23" 25 | nalgebra = "0.31.4" 26 | pollster.workspace = true 27 | rand.workspace = true 28 | svg = "0.5" 29 | svgtypes = "0.4" 30 | winit = "0.27.1" 31 | wgpu.workspace = true 32 | -------------------------------------------------------------------------------- /demo/src/demos/circles.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::time::Duration; 16 | 17 | use forma::prelude::*; 18 | use rand::prelude::*; 19 | 20 | use crate::{App, Keyboard}; 21 | 22 | fn circle(x: f32, y: f32, radius: f32) -> Path { 23 | let weight = 2.0f32.sqrt() / 2.0; 24 | 25 | let mut builder = PathBuilder::new(); 26 | 27 | builder.move_to(Point::new(x + radius, y)); 28 | builder.rat_quad_to( 29 | Point::new(x + radius, y - radius), 30 | Point::new(x, y - radius), 31 | weight, 32 | ); 33 | builder.rat_quad_to( 34 | Point::new(x - radius, y - radius), 35 | Point::new(x - radius, y), 36 | weight, 37 | ); 38 | builder.rat_quad_to( 39 | Point::new(x - radius, y + radius), 40 | Point::new(x, y + radius), 41 | weight, 42 | ); 43 | builder.rat_quad_to( 44 | Point::new(x + radius, y + radius), 45 | Point::new(x + radius, y), 46 | weight, 47 | ); 48 | 49 | builder.build() 50 | } 51 | 52 | pub struct Circles { 53 | count: usize, 54 | width: usize, 55 | height: usize, 56 | needs_composition: bool, 57 | } 58 | 59 | impl Circles { 60 | pub fn new(count: usize) -> Self { 61 | Self { 62 | count, 63 | width: 1000, 64 | height: 1000, 65 | needs_composition: true, 66 | } 67 | } 68 | } 69 | 70 | impl App for Circles { 71 | fn width(&self) -> usize { 72 | self.width 73 | } 74 | 75 | fn height(&self) -> usize { 76 | self.height 77 | } 78 | 79 | fn set_width(&mut self, width: usize) { 80 | if self.width == width { 81 | return; 82 | } 83 | 84 | self.width = width; 85 | self.needs_composition = true; 86 | } 87 | 88 | fn set_height(&mut self, height: usize) { 89 | if self.height == height { 90 | return; 91 | } 92 | 93 | self.height = height; 94 | self.needs_composition = true; 95 | } 96 | 97 | fn compose(&mut self, composition: &mut Composition, _: Duration, _: &Keyboard) { 98 | if !self.needs_composition { 99 | return; 100 | } 101 | 102 | let radius_range = 10.0..50.0; 103 | 104 | let mut rng = StdRng::seed_from_u64(42); 105 | 106 | for order in 0..self.count { 107 | let color = Color { 108 | r: rng.gen(), 109 | g: rng.gen(), 110 | b: rng.gen(), 111 | a: 0.2, 112 | }; 113 | 114 | composition 115 | .get_mut_or_insert_default(Order::new(order as u32).unwrap()) 116 | .clear() 117 | .insert(&circle( 118 | rng.gen_range(0.0..App::width(self) as f32), 119 | rng.gen_range(0.0..App::height(self) as f32), 120 | rng.gen_range(radius_range.clone()), 121 | )) 122 | .set_props(Props { 123 | fill_rule: FillRule::NonZero, 124 | func: Func::Draw(Style { 125 | fill: Fill::Solid(color), 126 | ..Default::default() 127 | }), 128 | }); 129 | } 130 | 131 | self.needs_composition = false; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /demo/src/demos/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | mod circles; 16 | mod spaceship; 17 | mod svg; 18 | mod texture; 19 | 20 | pub use self::svg::Svg; 21 | pub use circles::Circles; 22 | pub use spaceship::Spaceship; 23 | pub use texture::Texture; 24 | -------------------------------------------------------------------------------- /demo/src/demos/texture.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{ 16 | path::{self, PathBuf}, 17 | time::Duration, 18 | }; 19 | 20 | use forma::{prelude::*, styling}; 21 | use image::GenericImageView; 22 | 23 | use crate::{App, Keyboard}; 24 | 25 | #[derive(Debug)] 26 | pub struct Texture { 27 | width: usize, 28 | height: usize, 29 | time: Duration, 30 | image: Image, 31 | } 32 | 33 | impl Texture { 34 | pub fn new() -> Self { 35 | Self { 36 | width: 1000, 37 | height: 1000, 38 | time: Duration::ZERO, 39 | image: load_image(&PathBuf::from("assets/images/logo.png")), 40 | } 41 | } 42 | 43 | fn transform(&self, t: f32) -> AffineTransform { 44 | let scale = 1.5 - t.cos(); 45 | let mut t = AffineTransform { 46 | ux: t.cos() * scale, 47 | uy: t.sin() * scale, 48 | vx: -t.sin() * scale, 49 | vy: t.cos() * scale, 50 | tx: 0.0, 51 | ty: 0.0, 52 | }; 53 | 54 | let (x, y) = (self.width as f32 * 0.5, self.height as f32 * 0.5); 55 | (t.tx, t.ty) = ( 56 | self.image.width() as f32 * 0.5 - t.ux * x - t.vx * y, 57 | self.image.height() as f32 * 0.5 - t.uy * x - t.vy * y, 58 | ); 59 | t 60 | } 61 | } 62 | 63 | impl Default for Texture { 64 | fn default() -> Self { 65 | Self::new() 66 | } 67 | } 68 | 69 | fn load_image(file_path: &path::Path) -> Image { 70 | let img = image::io::Reader::open(file_path) 71 | .expect("Unable to open file") 72 | .decode() 73 | .expect("Unable to decode file"); 74 | 75 | let data: Vec<_> = img 76 | .to_rgb8() 77 | .pixels() 78 | .map(|p| [p.0[0], p.0[1], p.0[2], 255]) 79 | .collect(); 80 | Image::from_srgba(&data[..], img.width() as usize, img.height() as usize).unwrap() 81 | } 82 | 83 | impl App for Texture { 84 | fn width(&self) -> usize { 85 | self.width 86 | } 87 | 88 | fn height(&self) -> usize { 89 | self.height 90 | } 91 | 92 | fn set_width(&mut self, width: usize) { 93 | self.width = width; 94 | } 95 | 96 | fn set_height(&mut self, height: usize) { 97 | self.height = height; 98 | } 99 | 100 | fn compose(&mut self, composition: &mut Composition, elapsed: Duration, _: &Keyboard) { 101 | const PAD: f32 = 32.0; 102 | 103 | let (w, h) = (self.width as f32, self.height as f32); 104 | let layer = composition 105 | .get_mut_or_insert_default(Order::new(0).unwrap()) 106 | .clear() 107 | .insert( 108 | &PathBuilder::new() 109 | .move_to(Point { x: PAD, y: PAD }) 110 | .line_to(Point { x: w - PAD, y: PAD }) 111 | .line_to(Point { 112 | x: w - PAD, 113 | y: h - PAD, 114 | }) 115 | .line_to(Point { x: PAD, y: h - PAD }) 116 | .build(), 117 | ); 118 | 119 | self.time += elapsed; 120 | 121 | layer.set_props(Props { 122 | fill_rule: FillRule::NonZero, 123 | func: Func::Draw(Style { 124 | fill: Fill::Texture(styling::Texture { 125 | transform: self.transform(self.time.as_secs_f32()), 126 | image: self.image.clone(), 127 | }), 128 | ..Default::default() 129 | }), 130 | }); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /demo/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{ 16 | collections::HashSet, 17 | fmt, 18 | path::PathBuf, 19 | str::FromStr, 20 | time::{Duration, Instant}, 21 | }; 22 | 23 | use clap::{Parser, Subcommand}; 24 | use forma::prelude::*; 25 | use runner::{CpuRunner, GpuRunner}; 26 | use winit::{ 27 | event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, 28 | event_loop::{ControlFlow, EventLoop}, 29 | }; 30 | 31 | pub mod demos; 32 | mod runner; 33 | 34 | enum Device { 35 | Cpu, 36 | GpuLowPower, 37 | GpuHighPerformance, 38 | } 39 | 40 | impl fmt::Display for Device { 41 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 42 | match self { 43 | Device::Cpu => write!(f, "cpu"), 44 | Device::GpuLowPower => write!(f, "gpu low power"), 45 | Device::GpuHighPerformance => write!(f, "gpu high performance"), 46 | } 47 | } 48 | } 49 | 50 | impl FromStr for Device { 51 | type Err = &'static str; 52 | 53 | fn from_str(s: &str) -> Result { 54 | match s.to_lowercase().as_str() { 55 | "c" | "cpu" => Ok(Device::Cpu), 56 | "l" | "gpu-low" => Ok(Device::GpuLowPower), 57 | "h" | "gpu-high" => Ok(Device::GpuHighPerformance), 58 | _ => Err("must be c|cpu or l|gpu-low or h|gpu-high"), 59 | } 60 | } 61 | } 62 | 63 | #[derive(Parser)] 64 | #[clap(about = "forma demo with multiple modes")] 65 | struct Demo { 66 | /// Device to run the demo on 67 | #[clap(default_value = "cpu")] 68 | device: Device, 69 | #[clap(subcommand)] 70 | mode: Mode, 71 | } 72 | 73 | #[derive(Subcommand)] 74 | enum Mode { 75 | /// Renders random circles 76 | Circles { 77 | /// Amount of circles to draw 78 | #[clap(default_value = "100")] 79 | count: usize, 80 | }, 81 | /// Renders an SVG 82 | Svg { 83 | /// .svg input file 84 | #[clap(parse(from_os_str))] 85 | file: PathBuf, 86 | /// Scale of the SVG 87 | #[clap(short, long, default_value = "1.0")] 88 | scale: f32, 89 | }, 90 | /// Renders a spaceship game 91 | Spaceship, 92 | /// Renders a rotating texture 93 | Texture, 94 | } 95 | 96 | trait App { 97 | fn width(&self) -> usize; 98 | fn height(&self) -> usize; 99 | fn set_width(&mut self, width: usize); 100 | fn set_height(&mut self, height: usize); 101 | fn compose(&mut self, composition: &mut Composition, elapsed: Duration, keyboard: &Keyboard); 102 | } 103 | 104 | trait Runner { 105 | fn resize(&mut self, width: u32, height: u32); 106 | fn render(&mut self, app: &mut dyn App, elapsed: Duration, keyboard: &Keyboard); 107 | } 108 | 109 | struct Keyboard { 110 | pressed: HashSet, 111 | } 112 | 113 | impl Keyboard { 114 | fn new() -> Self { 115 | Self { 116 | pressed: HashSet::new(), 117 | } 118 | } 119 | 120 | fn is_key_down(&self, key: VirtualKeyCode) -> bool { 121 | self.pressed.contains(&key) 122 | } 123 | 124 | fn on_keyboard_input(&mut self, input: winit::event::KeyboardInput) { 125 | if let Some(code) = input.virtual_keycode { 126 | match input.state { 127 | ElementState::Pressed => self.pressed.insert(code), 128 | ElementState::Released => self.pressed.remove(&code), 129 | }; 130 | } 131 | } 132 | } 133 | 134 | pub fn to_linear(rgb: [u8; 3]) -> Color { 135 | fn conv(l: u8) -> f32 { 136 | let l = f32::from(l) * 255.0f32.recip(); 137 | 138 | if l <= 0.04045 { 139 | l * 12.92f32.recip() 140 | } else { 141 | ((l + 0.055) * 1.055f32.recip()).powf(2.4) 142 | } 143 | } 144 | 145 | Color { 146 | r: conv(rgb[0]), 147 | g: conv(rgb[1]), 148 | b: conv(rgb[2]), 149 | a: 1.0, 150 | } 151 | } 152 | 153 | fn main() { 154 | let opts = Demo::parse(); 155 | 156 | let mut app: Box = match opts.mode { 157 | Mode::Circles { count } => Box::new(demos::Circles::new(count)), 158 | Mode::Svg { file, scale } => Box::new(demos::Svg::new(file, scale)), 159 | Mode::Spaceship => Box::new(demos::Spaceship::new()), 160 | Mode::Texture {} => Box::new(demos::Texture::new()), 161 | }; 162 | 163 | let width = app.width(); 164 | let height = app.height(); 165 | 166 | let event_loop = EventLoop::new(); 167 | let mut runner: Box = match opts.device { 168 | Device::Cpu => Box::new(CpuRunner::new(&event_loop, width as u32, height as u32)), 169 | Device::GpuLowPower => Box::new(GpuRunner::new( 170 | &event_loop, 171 | width as u32, 172 | height as u32, 173 | wgpu::PowerPreference::LowPower, 174 | )), 175 | Device::GpuHighPerformance => Box::new(GpuRunner::new( 176 | &event_loop, 177 | width as u32, 178 | height as u32, 179 | wgpu::PowerPreference::HighPerformance, 180 | )), 181 | }; 182 | 183 | let mut instant = Instant::now(); 184 | let mut keyboard = Keyboard::new(); 185 | event_loop.run(move |event, _, control_flow| { 186 | *control_flow = ControlFlow::Poll; 187 | 188 | match event { 189 | Event::WindowEvent { 190 | event: 191 | WindowEvent::CloseRequested 192 | | WindowEvent::KeyboardInput { 193 | input: 194 | KeyboardInput { 195 | virtual_keycode: Some(VirtualKeyCode::Escape), 196 | .. 197 | }, 198 | .. 199 | }, 200 | .. 201 | } => { 202 | *control_flow = ControlFlow::Exit; 203 | } 204 | Event::WindowEvent { 205 | event: WindowEvent::KeyboardInput { input, .. }, 206 | .. 207 | } => { 208 | keyboard.on_keyboard_input(input); 209 | } 210 | Event::WindowEvent { 211 | event: 212 | WindowEvent::Resized(size) 213 | | WindowEvent::ScaleFactorChanged { 214 | new_inner_size: &mut size, 215 | .. 216 | }, 217 | .. 218 | } => { 219 | runner.resize(size.width, size.height); 220 | 221 | app.set_width(size.width as usize); 222 | app.set_height(size.height as usize); 223 | } 224 | Event::MainEventsCleared => { 225 | let elapsed = instant.elapsed(); 226 | instant = Instant::now(); 227 | 228 | runner.render(&mut *app, elapsed, &keyboard); 229 | } 230 | _ => (), 231 | } 232 | }); 233 | } 234 | -------------------------------------------------------------------------------- /docs/code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, education, socio-economic status, nationality, personal appearance, 10 | race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | This Code of Conduct also applies outside the project spaces when the Project 56 | Steward has a reasonable belief that an individual's behavior may have a 57 | negative impact on the project or its community. 58 | 59 | ## Conflict Resolution 60 | 61 | We do not believe that all conflict is bad; healthy debate and disagreement 62 | often yield positive results. However, it is never okay to be disrespectful or 63 | to engage in behavior that violates the project’s code of conduct. 64 | 65 | If you see someone violating the code of conduct, you are encouraged to address 66 | the behavior directly with those involved. Many issues can be resolved quickly 67 | and easily, and this gives people more control over the outcome of their 68 | dispute. If you are unable to resolve the matter for any reason, or if the 69 | behavior is threatening or harassing, report it. We are dedicated to providing 70 | an environment where participants feel welcome and safe. 71 | 72 | Reports should be directed to Dragoș Tiselice , the 73 | Project Steward(s) for crumsort-rs. It is the Project Steward’s duty to 74 | receive and address reported violations of the code of conduct. They will then 75 | work with a committee consisting of representatives from the Open Source 76 | Programs Office and the Google Open Source Strategy team. If for any reason you 77 | are uncomfortable reaching out to the Project Steward, please email 78 | opensource@google.com. 79 | 80 | We will investigate every complaint, but you may not receive a direct response. 81 | We will use our discretion in determining when and how to follow up on reported 82 | incidents, which may range from not taking action to permanent expulsion from 83 | the project and project-sponsored spaces. We will notify the accused of the 84 | report and provide them an opportunity to discuss it before any action is taken. 85 | The identity of the reporter will be omitted from the details of the report 86 | supplied to the accused. In potentially harmful situations, such as ongoing 87 | harassment or threats to anyone's safety, we may take action without notice. 88 | 89 | ## Attribution 90 | 91 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, 92 | available at 93 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 94 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code Reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google/conduct/). 29 | -------------------------------------------------------------------------------- /e2e-tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [package] 16 | name = "e2e-tests" 17 | version = "0.1.0" 18 | edition = "2021" 19 | 20 | [dependencies] 21 | anyhow.workspace = true 22 | base64 = "0.13.0" 23 | forma = { path = "../forma", features = ["gpu"], package = "forma-render"} 24 | image = "0.23" 25 | pollster = "0.2" 26 | once_cell = "1.12.0" 27 | serde = { version = "1.0", features = ["derive"] } 28 | serde_json = "1.0.81" 29 | wgpu.workspace = true 30 | -------------------------------------------------------------------------------- /e2e-tests/expected/tests__blend_modes__ColorBurn__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__blend_modes__ColorBurn__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__blend_modes__ColorDodge__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__blend_modes__ColorDodge__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__blend_modes__Color__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__blend_modes__Color__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__blend_modes__Darken__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__blend_modes__Darken__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__blend_modes__Difference__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__blend_modes__Difference__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__blend_modes__Exclusion__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__blend_modes__Exclusion__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__blend_modes__HardLight__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__blend_modes__HardLight__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__blend_modes__Hue__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__blend_modes__Hue__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__blend_modes__Lighten__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__blend_modes__Lighten__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__blend_modes__Luminosity__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__blend_modes__Luminosity__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__blend_modes__Multiply__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__blend_modes__Multiply__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__blend_modes__Over__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__blend_modes__Over__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__blend_modes__Overlay__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__blend_modes__Overlay__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__blend_modes__Saturation__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__blend_modes__Saturation__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__blend_modes__Screen__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__blend_modes__Screen__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__blend_modes__SoftLight__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__blend_modes__SoftLight__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__clipping2__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__clipping2__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__clipping__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__clipping__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__covers__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__covers__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__fill_rules__EvenOdd__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__fill_rules__EvenOdd__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__fill_rules__NonZero__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__fill_rules__NonZero__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__linear_gradient__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__linear_gradient__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__pixel__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__pixel__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__radial_gradient__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__radial_gradient__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__radial_gradient__gpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__radial_gradient__gpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__solid_color__blue__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__solid_color__blue__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__solid_color__dark_blue__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__solid_color__dark_blue__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__solid_color__dark_green__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__solid_color__dark_green__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__solid_color__dark_red__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__solid_color__dark_red__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__solid_color__green__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__solid_color__green__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__solid_color__red__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__solid_color__red__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__solid_color__transparent_black__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__solid_color__transparent_black__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__texture__cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__texture__cpu.png -------------------------------------------------------------------------------- /e2e-tests/expected/tests__texture__gpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/forma/681e8bfd348caa61aab47437e7d857764c2ce522/e2e-tests/expected/tests__texture__gpu.png -------------------------------------------------------------------------------- /e2e-tests/tests/report.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Forma test report 6 | 114 | 115 | 116 | 117 | 118 |
119 |

Forma test report

120 |
121 |
b toggle background draw checker |
122 |
d toggle diff overlay.
123 |
124 |
125 |
126 | 127 |
128 |
129 |
130 |
131 |

{name}

132 |
{message}
133 |
134 |
135 |
136 | CPU Actual 137 |
138 | 139 | 140 | 141 |
142 | 143 |

144 |                         
145 |
146 | CPU Expected 147 |
148 | 149 | 150 | 151 |
152 | 153 |

154 |                         
155 |
156 |
157 |
158 | GPU Actual 159 |
160 | 161 | 162 | 163 |
164 | 165 |

166 |                         
167 |
168 | GPU Expected 169 |
170 | 171 | 172 | 173 |
174 | 175 |

176 |                         
177 |
178 | 179 |
180 |
181 |
182 |
183 | 184 | 294 | 295 | 296 | -------------------------------------------------------------------------------- /forma/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | [package] 16 | name = "forma-render" 17 | version = "0.1.3" 18 | edition = "2021" 19 | authors = [ 20 | "Dragoș Tiselice ", 21 | "Pierre Labatut ", 22 | "Rémi Doreau ", 23 | ] 24 | license = "Apache-2.0" 25 | description = "An efficient vector-graphics renderer" 26 | categories = ["rendering"] 27 | keywords = ["2d", "rendering", "parallel", "vector-graphics"] 28 | repository = "https://github.com/google/forma" 29 | readme = "../README.md" 30 | 31 | [dependencies] 32 | anyhow.workspace = true 33 | bytemuck = { workspace = true, optional = true } 34 | crossbeam-utils = "0.8.12" 35 | crumsort = "0.1.0" 36 | etagere = "0.2.7" 37 | ramhorns = { version = "0.12", optional = true } 38 | rayon = "1.5.3" 39 | rustc-hash = "1.1.0" 40 | wgpu = { workspace = true, optional = true } 41 | 42 | [dev-dependencies] 43 | half = "2.1.0" 44 | pollster.workspace = true 45 | rand.workspace = true 46 | 47 | [features] 48 | default = ["gpu"] 49 | gpu = ["bytemuck", "ramhorns", "wgpu"] 50 | -------------------------------------------------------------------------------- /forma/src/composition/interner.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{hash::Hash, ops::Deref, rc::Rc}; 16 | 17 | use rustc_hash::FxHashSet; 18 | 19 | #[derive(Debug)] 20 | pub struct Interned { 21 | data: Rc, 22 | } 23 | 24 | impl Deref for Interned { 25 | type Target = T; 26 | 27 | fn deref(&self) -> &Self::Target { 28 | &self.data 29 | } 30 | } 31 | 32 | // Safe as long as `Interned` cannot clone the `Rc`. 33 | unsafe impl Sync for Interned {} 34 | 35 | #[derive(Debug, Default)] 36 | pub struct Interner { 37 | // We need to use `Rc` instead of `Weak` since `Weak` can have it's hash 38 | // value changed on the fly which is unsupported by `HashSet`. 39 | pointers: FxHashSet>, 40 | } 41 | 42 | impl Interner { 43 | pub fn get(&mut self, val: T) -> Interned { 44 | if let Some(rc) = self.pointers.get(&val) { 45 | return Interned { 46 | data: Rc::clone(rc), 47 | }; 48 | } 49 | 50 | let data = Rc::new(val); 51 | 52 | self.pointers.insert(Rc::clone(&data)); 53 | 54 | Interned { data } 55 | } 56 | 57 | pub fn compact(&mut self) { 58 | self.pointers.retain(|rc| Rc::strong_count(rc) > 1); 59 | } 60 | } 61 | 62 | #[cfg(test)] 63 | mod tests { 64 | use super::*; 65 | 66 | #[test] 67 | fn interned_same_ptr() { 68 | let mut interner = Interner::default(); 69 | 70 | let foo0 = interner.get(String::from("foo")); 71 | let foo1 = interner.get(String::from("foo")); 72 | let bar = interner.get(String::from("bar")); 73 | 74 | assert!(Rc::ptr_eq(&foo0.data, &foo1.data)); 75 | assert!(!Rc::ptr_eq(&foo0.data, &bar.data)); 76 | } 77 | 78 | #[test] 79 | fn interned_drop_data() { 80 | let mut interner = Interner::default(); 81 | 82 | let foo0 = interner.get(String::from("foo")); 83 | 84 | { 85 | let _foo1 = interner.get(String::from("foo")); 86 | let _bar = interner.get(String::from("bar")); 87 | 88 | assert_eq!(interner.pointers.len(), 2); 89 | } 90 | 91 | assert_eq!(interner.pointers.len(), 2); 92 | 93 | interner.compact(); 94 | 95 | assert_eq!(interner.pointers.len(), 1); 96 | 97 | let foo1 = interner.get(String::from("foo")); 98 | assert!(Rc::ptr_eq(&foo0.data, &foo1.data)); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /forma/src/composition/layer.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use crate::{math::GeomPresTransform, styling::Props, utils::SmallBitSet, GeomId, Order, Path}; 16 | 17 | use super::{Interned, LayerSharedState}; 18 | 19 | /// Small, compact version of a [`Layer`] that implements [`Send`] and is used by the 20 | /// [`SegmentBuffer`]. 21 | /// 22 | /// This type should be as small as possible since it gets copied on use and thus 23 | /// should only contain information needed by the [`SegmentBuffer`]. 24 | /// 25 | /// [`SegmentBuffer`]: crate::segment::SegmentBuffer 26 | #[derive(Clone, Debug)] 27 | pub struct InnerLayer { 28 | pub is_enabled: bool, 29 | pub affine_transform: Option, 30 | pub order: Option, 31 | } 32 | 33 | impl Default for InnerLayer { 34 | fn default() -> Self { 35 | Self { 36 | is_enabled: true, 37 | affine_transform: None, 38 | order: None, 39 | } 40 | } 41 | } 42 | 43 | /// A layer is a *reusable* collection of geometry (i.e. [`Path`]s) with common properties and 44 | /// order in the paint stack. 45 | /// 46 | /// They are created by calling [`Composition::create_layer`] or 47 | /// [`Composition::get_mut_or_insert_default`]. 48 | /// 49 | /// [`Composition::create_layer`]: crate::Composition::create_layer 50 | /// [`Composition::get_mut_or_insert_default`]: crate::Composition::get_mut_or_insert_default 51 | /// 52 | /// # Examples 53 | /// 54 | /// ``` 55 | /// # use forma_render::prelude::*; 56 | /// let mut composition = Composition::new(); 57 | /// 58 | /// let _layer = composition.get_mut_or_insert_default(Order::new(0).unwrap()); 59 | /// ``` 60 | #[derive(Debug)] 61 | pub struct Layer { 62 | pub(crate) inner: InnerLayer, 63 | pub(crate) shared_state: LayerSharedState, 64 | pub(crate) geom_id: GeomId, 65 | pub(crate) props: Interned, 66 | pub(crate) is_unchanged: SmallBitSet, 67 | pub(crate) lines_count: usize, 68 | } 69 | 70 | impl Layer { 71 | /// Inserts `path` into the geometry of the layer. 72 | /// 73 | /// The inserted paths basically contribute to a single internal path containing the geometry 74 | /// of all the paths. 75 | /// 76 | /// # Examples 77 | /// 78 | /// ``` 79 | /// # use forma_render::prelude::*; 80 | /// let mut composition = Composition::new(); 81 | /// 82 | /// let line0 = PathBuilder::new().line_to(Point::new(10.0, 10.0)).build(); 83 | /// let line1 = PathBuilder::new().line_to(Point::new(10.0, 0.0)).build(); 84 | /// 85 | /// composition 86 | /// .get_mut_or_insert_default(Order::new(0).unwrap()) 87 | /// .insert(&line0) 88 | /// .insert(&line1); 89 | /// ``` 90 | pub fn insert(&mut self, path: &Path) -> &mut Self { 91 | { 92 | let mut state = self.shared_state.inner(); 93 | let builder = state 94 | .segment_buffer 95 | .as_mut() 96 | .expect("lines_builder should not be None"); 97 | 98 | let old_len = builder.len(); 99 | builder.push_path(self.geom_id, path); 100 | let len = builder.len() - old_len; 101 | 102 | state 103 | .geom_id_to_order 104 | .insert(self.geom_id, self.inner.order); 105 | 106 | self.lines_count += len; 107 | } 108 | 109 | self.is_unchanged.clear(); 110 | self 111 | } 112 | 113 | /// Clears the geometry stored in the layer and resets its [geometry ID](Self::geom_id). 114 | /// 115 | /// # Examples 116 | /// 117 | /// ``` 118 | /// # use forma_render::prelude::*; 119 | /// let mut composition = Composition::new(); 120 | /// 121 | /// let mut layer = composition.create_layer(); 122 | /// 123 | /// let initial_id = layer.geom_id(); 124 | /// 125 | /// layer.clear(); 126 | /// 127 | /// assert_ne!(layer.geom_id(), initial_id); 128 | /// ``` 129 | pub fn clear(&mut self) -> &mut Self { 130 | { 131 | let mut state = self.shared_state.inner(); 132 | 133 | state.geom_id_to_order.remove(&self.geom_id); 134 | 135 | self.geom_id = state.new_geom_id(); 136 | state 137 | .geom_id_to_order 138 | .insert(self.geom_id, self.inner.order); 139 | } 140 | 141 | self.lines_count = 0; 142 | 143 | self.is_unchanged.clear(); 144 | self 145 | } 146 | 147 | pub(crate) fn set_order(&mut self, order: Option) { 148 | if order.is_some() && self.inner.order != order { 149 | self.inner.order = order; 150 | self.is_unchanged.clear(); 151 | } 152 | 153 | self.shared_state 154 | .inner() 155 | .geom_id_to_order 156 | .insert(self.geom_id, order); 157 | } 158 | 159 | /// Returns the layer's geometry ID. 160 | /// 161 | /// Used to retrieve the layer's [`Order`] if stored in a [`Composition`](crate::Composition). 162 | /// 163 | /// # Examples 164 | /// 165 | /// ``` 166 | /// # use forma_render::prelude::*; 167 | /// let mut composition = Composition::new(); 168 | /// 169 | /// let order = Order::new(0).unwrap(); 170 | /// let layer = composition.get_mut_or_insert_default(order); 171 | /// let id = layer.geom_id(); 172 | /// 173 | /// assert_eq!(composition.get_order_if_stored(id), Some(order)); 174 | /// ``` 175 | pub fn geom_id(&self) -> GeomId { 176 | self.geom_id 177 | } 178 | 179 | pub(crate) fn is_unchanged(&self, cache_id: u8) -> bool { 180 | self.is_unchanged.contains(&cache_id) 181 | } 182 | 183 | pub(crate) fn set_is_unchanged(&mut self, cache_id: u8, is_unchanged: bool) -> bool { 184 | if is_unchanged { 185 | self.is_unchanged.insert(cache_id) 186 | } else { 187 | self.is_unchanged.remove(cache_id) 188 | } 189 | } 190 | 191 | /// Returns `true` if the layer is enabled. 192 | /// 193 | /// # Examples 194 | /// 195 | /// ``` 196 | /// # use forma_render::prelude::*; 197 | /// let mut composition = Composition::new(); 198 | /// 199 | /// let layer = composition.create_layer(); 200 | /// 201 | /// assert!(layer.is_enabled()); 202 | /// ``` 203 | #[inline] 204 | pub fn is_enabled(&self) -> bool { 205 | self.inner.is_enabled 206 | } 207 | 208 | /// Sets the layer's enabled state. 209 | /// 210 | /// # Examples 211 | /// 212 | /// ``` 213 | /// # use forma_render::prelude::*; 214 | /// let mut composition = Composition::new(); 215 | /// 216 | /// let mut layer = composition.create_layer(); 217 | /// 218 | /// layer.set_is_enabled(false); 219 | /// 220 | /// assert!(!layer.is_enabled()); 221 | /// ``` 222 | #[inline] 223 | pub fn set_is_enabled(&mut self, is_enabled: bool) -> &mut Self { 224 | self.inner.is_enabled = is_enabled; 225 | self 226 | } 227 | 228 | /// Disables the layer. 229 | /// 230 | /// # Examples 231 | /// 232 | /// ``` 233 | /// # use forma_render::prelude::*; 234 | /// let mut composition = Composition::new(); 235 | /// 236 | /// let mut layer = composition.create_layer(); 237 | /// 238 | /// layer.disable(); 239 | /// 240 | /// assert!(!layer.is_enabled()); 241 | /// ``` 242 | #[inline] 243 | pub fn disable(&mut self) -> &mut Self { 244 | self.set_is_enabled(false) 245 | } 246 | 247 | /// Enables the layer. 248 | /// 249 | /// # Examples 250 | /// 251 | /// ``` 252 | /// # use forma_render::prelude::*; 253 | /// let mut composition = Composition::new(); 254 | /// 255 | /// let mut layer = composition.create_layer(); 256 | /// 257 | /// layer.disable(); 258 | /// layer.enable(); 259 | /// 260 | /// assert!(layer.is_enabled()); 261 | /// ``` 262 | #[inline] 263 | pub fn enable(&mut self) -> &mut Self { 264 | self.set_is_enabled(true) 265 | } 266 | 267 | /// Returns the layer's transform. 268 | /// 269 | /// # Examples 270 | /// 271 | /// ``` 272 | /// # use forma_render::prelude::*; 273 | /// let mut composition = Composition::new(); 274 | /// 275 | /// let layer = composition.create_layer(); 276 | /// 277 | /// assert!(layer.transform().is_identity()); 278 | /// ``` 279 | #[inline] 280 | pub fn transform(&self) -> GeomPresTransform { 281 | self.inner.affine_transform.unwrap_or_default() 282 | } 283 | 284 | /// Sets the layer's transform. 285 | /// 286 | /// For less constrained transforms, use [`Path::transform`] instead. 287 | /// 288 | /// # Examples 289 | /// 290 | /// ``` 291 | /// # use forma_render::prelude::*; 292 | /// let mut composition = Composition::new(); 293 | /// 294 | /// let mut layer = composition.create_layer(); 295 | /// 296 | /// layer.set_transform(AffineTransform::from([1.0, 0.0, 0.0, 1.0, 1.0, 0.0]).try_into().unwrap()); 297 | /// ``` 298 | #[inline] 299 | pub fn set_transform(&mut self, transform: GeomPresTransform) -> &mut Self { 300 | // We want to perform a cheap check for the common case without hampering this function too 301 | // much. 302 | #[allow(clippy::float_cmp)] 303 | let affine_transform = (!transform.is_identity()).then_some(transform); 304 | 305 | if self.inner.affine_transform != affine_transform { 306 | self.is_unchanged.clear(); 307 | self.inner.affine_transform = affine_transform; 308 | } 309 | 310 | self 311 | } 312 | 313 | /// Returns the layer's properties. 314 | /// 315 | /// # Examples 316 | /// 317 | /// ``` 318 | /// # use forma_render::prelude::*; 319 | /// let mut composition = Composition::new(); 320 | /// 321 | /// let layer = composition.create_layer(); 322 | /// 323 | /// assert_eq!(layer.props().fill_rule, FillRule::NonZero); 324 | /// ``` 325 | #[inline] 326 | pub fn props(&self) -> &Props { 327 | &self.props 328 | } 329 | 330 | /// Sets the layer's properties. 331 | /// 332 | /// # Examples 333 | /// 334 | /// ``` 335 | /// # use forma_render::prelude::*; 336 | /// let mut composition = Composition::new(); 337 | /// 338 | /// let mut layer = composition.create_layer(); 339 | /// 340 | /// layer.set_props(Props { 341 | /// fill_rule: FillRule::EvenOdd, 342 | /// ..Default::default() 343 | /// }); 344 | /// ``` 345 | #[inline] 346 | pub fn set_props(&mut self, props: Props) -> &mut Self { 347 | if *self.props != props { 348 | self.is_unchanged.clear(); 349 | self.props = self.shared_state.inner().props_interner.get(props); 350 | } 351 | 352 | self 353 | } 354 | } 355 | 356 | impl Drop for Layer { 357 | fn drop(&mut self) { 358 | self.shared_state 359 | .inner() 360 | .geom_id_to_order 361 | .remove(&self.geom_id); 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /forma/src/composition/state.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{ 16 | cell::{RefCell, RefMut}, 17 | mem, 18 | rc::Rc, 19 | }; 20 | 21 | use rustc_hash::FxHashMap; 22 | 23 | use crate::{styling::Props, GeomId, Order, SegmentBuffer}; 24 | 25 | use super::Interner; 26 | 27 | #[derive(Debug)] 28 | pub struct LayerSharedStateInner { 29 | pub segment_buffer: Option, 30 | pub geom_id_to_order: FxHashMap>, 31 | pub props_interner: Interner, 32 | geom_id_generator: GeomId, 33 | } 34 | 35 | impl Default for LayerSharedStateInner { 36 | fn default() -> Self { 37 | Self { 38 | segment_buffer: Some(SegmentBuffer::default()), 39 | geom_id_to_order: FxHashMap::default(), 40 | props_interner: Interner::default(), 41 | geom_id_generator: GeomId::default(), 42 | } 43 | } 44 | } 45 | 46 | impl LayerSharedStateInner { 47 | pub fn new_geom_id(&mut self) -> GeomId { 48 | let prev = self.geom_id_generator; 49 | mem::replace(&mut self.geom_id_generator, prev.next()) 50 | } 51 | } 52 | 53 | #[derive(Debug, Default)] 54 | pub struct LayerSharedState { 55 | inner: Rc>, 56 | } 57 | 58 | impl LayerSharedState { 59 | pub fn new(inner: Rc>) -> Self { 60 | Self { inner } 61 | } 62 | 63 | pub fn inner(&mut self) -> RefMut<'_, LayerSharedStateInner> { 64 | self.inner.borrow_mut() 65 | } 66 | } 67 | 68 | impl PartialEq>> for LayerSharedState { 69 | fn eq(&self, other: &Rc>) -> bool { 70 | Rc::ptr_eq(&self.inner, other) 71 | } 72 | } 73 | 74 | // Safe as long as `inner` can only be accessed by `&mut self`. 75 | unsafe impl Sync for LayerSharedState {} 76 | -------------------------------------------------------------------------------- /forma/src/consts.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #![allow(clippy::assertions_on_constants)] 16 | 17 | use std::mem; 18 | 19 | use crate::cpu::PixelSegment; 20 | 21 | pub(crate) const PIXEL_WIDTH: usize = 16; 22 | pub(crate) const PIXEL_DOUBLE_WIDTH: usize = PIXEL_WIDTH * 2; 23 | pub(crate) const PIXEL_SHIFT: usize = PIXEL_WIDTH.trailing_zeros() as usize; 24 | 25 | pub const MAX_WIDTH: usize = 1 << 16; 26 | pub const MAX_HEIGHT: usize = 1 << 15; 27 | 28 | pub(crate) const MAX_WIDTH_SHIFT: usize = MAX_WIDTH.trailing_zeros() as usize; 29 | pub(crate) const MAX_HEIGHT_SHIFT: usize = MAX_HEIGHT.trailing_zeros() as usize; 30 | 31 | pub mod cpu { 32 | pub const TILE_WIDTH: usize = 16; 33 | const _: () = assert!(TILE_WIDTH % 16 == 0); 34 | const _: () = assert!(TILE_WIDTH <= 128); 35 | 36 | pub(crate) const TILE_WIDTH_SHIFT: usize = TILE_WIDTH.trailing_zeros() as usize; 37 | 38 | pub const TILE_HEIGHT: usize = 16; 39 | const _: () = assert!(TILE_HEIGHT % 16 == 0); 40 | const _: () = assert!(TILE_HEIGHT <= 128); 41 | 42 | pub(crate) const TILE_HEIGHT_SHIFT: usize = TILE_HEIGHT.trailing_zeros() as usize; 43 | } 44 | 45 | pub mod gpu { 46 | pub const TILE_WIDTH: usize = 16; 47 | pub const TILE_HEIGHT: usize = 4; 48 | } 49 | 50 | #[derive(Clone, Copy, Debug)] 51 | pub(crate) enum BitField { 52 | TileY, 53 | TileX, 54 | LayerId, 55 | LocalX, 56 | LocalY, 57 | DoubleAreaMultiplier, 58 | Cover, 59 | } 60 | 61 | #[derive(Debug)] 62 | pub(crate) struct BitFieldMap { 63 | lengths: [usize; 7], 64 | } 65 | 66 | impl BitFieldMap { 67 | #[inline] 68 | pub const fn new() -> Self { 69 | let tile_width_shift = TW.trailing_zeros() as usize; 70 | let tile_height_shift = TH.trailing_zeros() as usize; 71 | 72 | let mut lengths = [ 73 | MAX_HEIGHT_SHIFT - tile_height_shift, 74 | MAX_WIDTH_SHIFT - tile_width_shift, 75 | 0, 76 | tile_width_shift, 77 | tile_height_shift, 78 | ((PIXEL_WIDTH + 1) * 2).next_power_of_two().trailing_zeros() as usize, 79 | ((PIXEL_WIDTH + 1) * 2).next_power_of_two().trailing_zeros() as usize, 80 | ]; 81 | 82 | let layer_id_len = mem::size_of::>() * 8 83 | - lengths[BitField::TileY as usize] 84 | - lengths[BitField::TileX as usize] 85 | - lengths[BitField::LocalX as usize] 86 | - lengths[BitField::LocalY as usize] 87 | - lengths[BitField::DoubleAreaMultiplier as usize] 88 | - lengths[BitField::Cover as usize]; 89 | 90 | lengths[BitField::LayerId as usize] = layer_id_len; 91 | 92 | Self { lengths } 93 | } 94 | 95 | #[inline] 96 | pub const fn get(&self, bit_field: BitField) -> usize { 97 | self.lengths[bit_field as usize] 98 | } 99 | 100 | #[inline] 101 | pub const fn get_by_index(&self, i: usize) -> usize { 102 | self.lengths[i] 103 | } 104 | } 105 | 106 | pub const LAYER_LIMIT: usize = (1 107 | << BitFieldMap::new::<{ cpu::TILE_WIDTH }, { cpu::TILE_HEIGHT }>().get(BitField::LayerId)) 108 | - 1; 109 | const _: () = assert!( 110 | LAYER_LIMIT 111 | == (1 112 | << BitFieldMap::new::<{ gpu::TILE_WIDTH }, { gpu::TILE_HEIGHT }>() 113 | .get(BitField::LayerId)) 114 | - 1, 115 | "LAYER_LIMIT must be the same both on cpu and gpu", 116 | ); 117 | -------------------------------------------------------------------------------- /forma/src/cpu/buffer/layout/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Buffer-layout-specific traits for user-defined behavior. 16 | //! 17 | //! [`Layout`]'s job is to split a buffer into sub-slices that will then be distributed to tile to 18 | //! be rendered, and to write color data to these sub-slices. 19 | 20 | use std::fmt; 21 | 22 | use rayon::prelude::*; 23 | 24 | mod slice_cache; 25 | pub use slice_cache::{Chunks, Ref, Slice, SliceCache, Span}; 26 | 27 | use crate::consts; 28 | 29 | /// Listener that gets called after every write to the buffer. Its main use is to flush freshly 30 | /// written memory slices. 31 | pub trait Flusher: fmt::Debug + Send + Sync { 32 | /// Called after `slice` was written to. 33 | fn flush(&self, slice: &mut [u8]); 34 | } 35 | 36 | /// A fill that the [`Layout`] uses to write to tiles. 37 | pub enum TileFill<'c> { 38 | /// Fill tile with a solid color. 39 | Solid([u8; 4]), 40 | /// Fill tile with provided colors buffer. They are provided in [column-major] order. 41 | /// 42 | /// [column-major]: https://en.wikipedia.org/wiki/Row-_and_column-major_order 43 | Full(&'c [[u8; 4]]), 44 | } 45 | 46 | /// A buffer's layout description. 47 | /// 48 | /// Implementors are supposed to cache sub-slices between uses provided they are being used with 49 | /// exactly the same buffer. This is achieved by storing a [`SliceCache`] in every layout 50 | /// implementation. 51 | pub trait Layout { 52 | /// Width in pixels. 53 | /// 54 | /// # Examples 55 | /// 56 | /// ``` 57 | /// # use forma_render::cpu::buffer::layout::{Layout, LinearLayout}; 58 | /// let layout = LinearLayout::new(2, 3 * 4, 4); 59 | /// 60 | /// assert_eq!(layout.width(), 2); 61 | /// ``` 62 | fn width(&self) -> usize; 63 | 64 | /// Height in pixels. 65 | /// 66 | /// # Examples 67 | /// 68 | /// ``` 69 | /// # use forma_render::cpu::buffer::layout::{Layout, LinearLayout}; 70 | /// let layout = LinearLayout::new(2, 3 * 4, 4); 71 | /// 72 | /// assert_eq!(layout.height(), 4); 73 | /// ``` 74 | fn height(&self) -> usize; 75 | 76 | /// Number of buffer sub-slices that will be passes to [`Layout::write`]. 77 | /// 78 | /// # Examples 79 | /// 80 | /// ``` 81 | /// # use forma_render::{ 82 | /// # cpu::buffer::{layout::{Layout, LinearLayout}}, consts::cpu::TILE_HEIGHT, 83 | /// # }; 84 | /// let layout = LinearLayout::new(2, 3 * 4, 4); 85 | /// 86 | /// assert_eq!(layout.slices_per_tile(), TILE_HEIGHT); 87 | /// ``` 88 | fn slices_per_tile(&self) -> usize; 89 | 90 | /// Returns self-stored sub-slices of `buffer` which are stored in a [`SliceCache`]. 91 | /// 92 | /// # Examples 93 | /// 94 | /// ``` 95 | /// # use forma_render::cpu::buffer::layout::{Layout, LinearLayout}; 96 | /// let mut buffer = [ 97 | /// [1; 4], [2; 4], [3; 4], 98 | /// [4; 4], [5; 4], [6; 4], 99 | /// ].concat(); 100 | /// let mut layout = LinearLayout::new(2, 3 * 4, 2); 101 | /// let slices = layout.slices(&mut buffer); 102 | /// 103 | /// assert_eq!(&*slices[0], &[[1; 4], [2; 4]].concat()); 104 | /// assert_eq!(&*slices[1], &[[4; 4], [5; 4]].concat()); 105 | /// ``` 106 | fn slices<'l, 'b>(&'l mut self, buffer: &'b mut [u8]) -> Ref<'l, [Slice<'b, u8>]>; 107 | 108 | /// Writes `fill` to `slices`, optionally calling the `flusher`. 109 | /// 110 | /// # Examples 111 | /// 112 | /// ``` 113 | /// # use forma_render::cpu::buffer::layout::{Layout, LinearLayout, TileFill}; 114 | /// let mut buffer = [ 115 | /// [1; 4], [2; 4], [3; 4], 116 | /// [4; 4], [5; 4], [6; 4], 117 | /// ].concat(); 118 | /// let mut layout = LinearLayout::new(2, 3 * 4, 2); 119 | /// 120 | /// LinearLayout::write(&mut *layout.slices(&mut buffer), None, TileFill::Solid([0; 4])); 121 | /// 122 | /// assert_eq!(buffer, [ 123 | /// [0; 4], [0; 4], [3; 4], 124 | /// [0; 4], [0; 4], [6; 4], 125 | /// ].concat()); 126 | fn write(slices: &mut [Slice<'_, u8>], flusher: Option<&dyn Flusher>, fill: TileFill<'_>); 127 | 128 | /// Width in tiles. 129 | /// 130 | /// # Examples 131 | /// 132 | /// ``` 133 | /// # use forma_render::{ 134 | /// # cpu::buffer::{layout::{Layout, LinearLayout}}, 135 | /// # consts::cpu::{TILE_HEIGHT, TILE_WIDTH}, 136 | /// # }; 137 | /// let layout = LinearLayout::new(2 * TILE_WIDTH, 3 * TILE_WIDTH * 4, 4 * TILE_HEIGHT); 138 | /// 139 | /// assert_eq!(layout.width_in_tiles(), 2); 140 | /// ``` 141 | #[inline] 142 | fn width_in_tiles(&self) -> usize { 143 | (self.width() + consts::cpu::TILE_WIDTH - 1) >> consts::cpu::TILE_WIDTH_SHIFT 144 | } 145 | 146 | /// Height in tiles. 147 | /// 148 | /// # Examples 149 | /// 150 | /// ``` 151 | /// # use forma_render::{ 152 | /// # cpu::buffer::{layout::{Layout, LinearLayout}}, 153 | /// # consts::cpu::{TILE_HEIGHT, TILE_WIDTH}, 154 | /// # }; 155 | /// let layout = LinearLayout::new(2 * TILE_WIDTH, 3 * TILE_WIDTH * 4, 4 * TILE_HEIGHT); 156 | /// 157 | /// assert_eq!(layout.height_in_tiles(), 4); 158 | /// ``` 159 | #[inline] 160 | fn height_in_tiles(&self) -> usize { 161 | (self.height() + consts::cpu::TILE_HEIGHT - 1) >> consts::cpu::TILE_HEIGHT_SHIFT 162 | } 163 | } 164 | 165 | /// A linear buffer layout where each optionally strided pixel row of an image is saved 166 | /// sequentially into the buffer. 167 | #[derive(Debug)] 168 | pub struct LinearLayout { 169 | cache: SliceCache, 170 | width: usize, 171 | width_stride: usize, 172 | height: usize, 173 | } 174 | 175 | impl LinearLayout { 176 | /// Creates a new linear layout from `width`, `width_stride` (in bytes) and `height`. 177 | /// 178 | /// # Examples 179 | /// 180 | /// ``` 181 | /// # use forma_render::cpu::buffer::layout::{Layout, LinearLayout}; 182 | /// let layout = LinearLayout::new(2, 3 * 4, 4); 183 | /// 184 | /// assert_eq!(layout.width(), 2); 185 | /// ``` 186 | #[inline] 187 | pub fn new(width: usize, width_stride: usize, height: usize) -> Self { 188 | assert!( 189 | width * 4 <= width_stride, 190 | "width exceeds width stride: {} * 4 > {}", 191 | width, 192 | width_stride 193 | ); 194 | 195 | let cache = SliceCache::new(width_stride * height, move |buffer| { 196 | let mut layout: Vec<_> = buffer 197 | .chunks(width_stride) 198 | .enumerate() 199 | .flat_map(|(tile_y, row)| { 200 | row.slice(..width * 4) 201 | .unwrap() 202 | .chunks(consts::cpu::TILE_WIDTH * 4) 203 | .enumerate() 204 | .map(move |(tile_x, slice)| { 205 | let tile_y = tile_y >> consts::cpu::TILE_HEIGHT_SHIFT; 206 | (tile_x, tile_y, slice) 207 | }) 208 | }) 209 | .collect(); 210 | layout.par_sort_by_key(|&(tile_x, tile_y, _)| (tile_y, tile_x)); 211 | 212 | layout.into_iter().map(|(_, _, slice)| slice).collect() 213 | }); 214 | 215 | LinearLayout { 216 | cache, 217 | width, 218 | width_stride, 219 | height, 220 | } 221 | } 222 | } 223 | 224 | impl Layout for LinearLayout { 225 | #[inline] 226 | fn width(&self) -> usize { 227 | self.width 228 | } 229 | 230 | #[inline] 231 | fn height(&self) -> usize { 232 | self.height 233 | } 234 | 235 | #[inline] 236 | fn slices_per_tile(&self) -> usize { 237 | consts::cpu::TILE_HEIGHT 238 | } 239 | 240 | #[inline] 241 | fn slices<'l, 'b>(&'l mut self, buffer: &'b mut [u8]) -> Ref<'l, [Slice<'b, u8>]> { 242 | assert!( 243 | self.width <= buffer.len(), 244 | "width exceeds buffer length: {} > {}", 245 | self.width, 246 | buffer.len() 247 | ); 248 | assert!( 249 | self.width_stride <= buffer.len(), 250 | "width_stride exceeds buffer length: {} > {}", 251 | self.width_stride, 252 | buffer.len(), 253 | ); 254 | assert!( 255 | self.height * self.width_stride <= buffer.len(), 256 | "height * width_stride exceeds buffer length: {} > {}", 257 | self.height * self.width_stride, 258 | buffer.len(), 259 | ); 260 | 261 | self.cache.access(buffer).unwrap() 262 | } 263 | 264 | #[inline] 265 | fn write(slices: &mut [Slice<'_, u8>], flusher: Option<&dyn Flusher>, fill: TileFill<'_>) { 266 | let tiles_len = slices.len(); 267 | match fill { 268 | TileFill::Solid(solid) => { 269 | for row in slices.iter_mut().take(tiles_len) { 270 | for color in row.chunks_exact_mut(4) { 271 | color.copy_from_slice(&solid); 272 | } 273 | } 274 | } 275 | TileFill::Full(colors) => { 276 | for (y, row) in slices.iter_mut().enumerate().take(tiles_len) { 277 | for (x, color) in row.chunks_exact_mut(4).enumerate() { 278 | color.copy_from_slice(&colors[x * consts::cpu::TILE_HEIGHT + y]); 279 | } 280 | } 281 | } 282 | } 283 | 284 | if let Some(flusher) = flusher { 285 | for row in slices.iter_mut().take(tiles_len) { 286 | flusher.flush( 287 | if let Some(subslice) = row.get_mut(..consts::cpu::TILE_WIDTH * 4) { 288 | subslice 289 | } else { 290 | row 291 | }, 292 | ); 293 | } 294 | } 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /forma/src/cpu/buffer/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{ 16 | cell::RefCell, 17 | rc::{Rc, Weak}, 18 | }; 19 | 20 | use crate::{styling::Color, utils::SmallBitSet}; 21 | 22 | use super::painter::CachedTile; 23 | 24 | pub mod layout; 25 | 26 | use self::layout::{Flusher, Layout}; 27 | 28 | /// A short-lived description of the buffer being rendered into for the current frame. 29 | /// 30 | /// # Examples 31 | /// 32 | /// ``` 33 | /// # use forma_render::cpu::buffer::{BufferBuilder, layout::LinearLayout}; 34 | /// let width = 100; 35 | /// let height = 100; 36 | /// let mut buffer = vec![0; 100 * 4 * 100]; 37 | /// 38 | /// let _buffer = BufferBuilder::new( 39 | /// &mut buffer, 40 | /// &mut LinearLayout::new(width, width * 4, height), 41 | /// ).build(); 42 | /// ``` 43 | #[derive(Debug)] 44 | pub struct Buffer<'b, 'l, L: Layout> { 45 | pub(crate) buffer: &'b mut [u8], 46 | pub(crate) layout: &'l mut L, 47 | pub(crate) layer_cache: Option, 48 | pub(crate) flusher: Option>, 49 | } 50 | 51 | /// A builder for the [`Buffer`]. 52 | /// 53 | /// # Examples 54 | /// 55 | /// ``` 56 | /// # use forma_render::cpu::buffer::{BufferBuilder, layout::LinearLayout}; 57 | /// let width = 100; 58 | /// let height = 100; 59 | /// let mut buffer = vec![0; 100 * 4 * 100]; 60 | /// let mut layout = LinearLayout::new(width, width * 4, height); 61 | /// let _buffer = BufferBuilder::new(&mut buffer, &mut layout).build(); 62 | /// ``` 63 | #[derive(Debug)] 64 | pub struct BufferBuilder<'b, 'l, L: Layout> { 65 | buffer: Buffer<'b, 'l, L>, 66 | } 67 | 68 | impl<'b, 'l, L: Layout> BufferBuilder<'b, 'l, L> { 69 | #[inline] 70 | pub fn new(buffer: &'b mut [u8], layout: &'l mut L) -> Self { 71 | Self { 72 | buffer: Buffer { 73 | buffer, 74 | layout, 75 | layer_cache: None, 76 | flusher: None, 77 | }, 78 | } 79 | } 80 | 81 | #[inline] 82 | pub fn layer_cache(mut self, layer_cache: BufferLayerCache) -> Self { 83 | self.buffer.layer_cache = Some(layer_cache); 84 | self 85 | } 86 | 87 | #[inline] 88 | pub fn flusher(mut self, flusher: Box) -> Self { 89 | self.buffer.flusher = Some(flusher); 90 | self 91 | } 92 | 93 | #[inline] 94 | pub fn build(self) -> Buffer<'b, 'l, L> { 95 | self.buffer 96 | } 97 | } 98 | 99 | #[derive(Debug)] 100 | struct IdDropper { 101 | id: u8, 102 | buffers_with_caches: Weak>, 103 | } 104 | 105 | impl Drop for IdDropper { 106 | fn drop(&mut self) { 107 | if let Some(buffers_with_caches) = Weak::upgrade(&self.buffers_with_caches) { 108 | buffers_with_caches.borrow_mut().remove(self.id); 109 | } 110 | } 111 | } 112 | 113 | #[derive(Debug)] 114 | pub(crate) struct CacheInner { 115 | pub clear_color: Option, 116 | pub tiles: Vec, 117 | pub width: Option, 118 | pub height: Option, 119 | _id_dropper: IdDropper, 120 | } 121 | 122 | /// A per-[`Buffer`] cache that enables forma to skip rendering to buffer 123 | /// regions that have not changed. 124 | /// 125 | /// # Examples 126 | /// 127 | /// ``` 128 | /// # use forma_render::{ 129 | /// # cpu::{buffer::{BufferBuilder, layout::LinearLayout}, Renderer, RGBA}, 130 | /// # styling::Color, Composition, 131 | /// # }; 132 | /// let mut buffer = vec![0; 4]; 133 | /// 134 | /// let mut composition = Composition::new(); 135 | /// let mut renderer = Renderer::new(); 136 | /// let layer_cache = renderer.create_buffer_layer_cache().unwrap(); 137 | /// 138 | /// renderer.render( 139 | /// &mut composition, 140 | /// &mut BufferBuilder::new(&mut buffer, &mut LinearLayout::new(1, 1 * 4, 1)) 141 | /// .layer_cache(layer_cache.clone()) 142 | /// .build(), 143 | /// RGBA, 144 | /// Color { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, 145 | /// None, 146 | /// ); 147 | /// 148 | /// // Rendered white on first frame. 149 | /// assert_eq!(buffer, [255; 4]); 150 | /// 151 | /// // Reset buffer manually. 152 | /// buffer = vec![0; 4]; 153 | /// 154 | /// renderer.render( 155 | /// &mut composition, 156 | /// &mut BufferBuilder::new(&mut buffer, &mut LinearLayout::new(1, 1 * 4, 1)) 157 | /// .layer_cache(layer_cache.clone()) 158 | /// .build(), 159 | /// RGBA, 160 | /// Color { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, 161 | /// None, 162 | /// ); 163 | /// 164 | /// // Skipped rendering on second frame since nothing changed. 165 | /// assert_eq!(buffer, [0; 4]); 166 | /// ``` 167 | #[derive(Clone, Debug)] 168 | pub struct BufferLayerCache { 169 | pub(crate) id: u8, 170 | pub(crate) cache: Rc>, 171 | } 172 | 173 | impl BufferLayerCache { 174 | pub(crate) fn new(id: u8, buffers_with_caches: Weak>) -> Self { 175 | Self { 176 | id, 177 | cache: Rc::new(RefCell::new(CacheInner { 178 | clear_color: None, 179 | tiles: Vec::new(), 180 | width: None, 181 | height: None, 182 | _id_dropper: IdDropper { 183 | id, 184 | buffers_with_caches, 185 | }, 186 | })), 187 | } 188 | } 189 | 190 | #[inline] 191 | pub fn clear(&self) { 192 | let mut cache = self.cache.borrow_mut(); 193 | 194 | cache.clear_color = None; 195 | cache.tiles.fill(CachedTile::default()); 196 | } 197 | } 198 | 199 | #[cfg(test)] 200 | mod tests { 201 | use super::*; 202 | 203 | use std::mem; 204 | 205 | fn new_cache(bit_set: &Rc>) -> BufferLayerCache { 206 | bit_set 207 | .borrow_mut() 208 | .first_empty_slot() 209 | .map(|id| BufferLayerCache::new(id, Rc::downgrade(bit_set))) 210 | .unwrap() 211 | } 212 | 213 | #[test] 214 | fn clone_and_drop() { 215 | let bit_set = Rc::new(RefCell::new(SmallBitSet::default())); 216 | 217 | let cache0 = new_cache(&bit_set); 218 | let cache1 = new_cache(&bit_set); 219 | let cache2 = new_cache(&bit_set); 220 | 221 | assert!(bit_set.borrow().contains(&0)); 222 | assert!(bit_set.borrow().contains(&1)); 223 | assert!(bit_set.borrow().contains(&2)); 224 | 225 | mem::drop(cache0.clone()); 226 | mem::drop(cache1.clone()); 227 | mem::drop(cache2.clone()); 228 | 229 | assert!(bit_set.borrow().contains(&0)); 230 | assert!(bit_set.borrow().contains(&1)); 231 | assert!(bit_set.borrow().contains(&2)); 232 | 233 | mem::drop(cache1); 234 | 235 | assert!(bit_set.borrow().contains(&0)); 236 | assert!(!bit_set.borrow().contains(&1)); 237 | assert!(bit_set.borrow().contains(&2)); 238 | 239 | let cache1 = new_cache(&bit_set); 240 | 241 | assert!(bit_set.borrow().contains(&0)); 242 | assert!(bit_set.borrow().contains(&1)); 243 | assert!(bit_set.borrow().contains(&2)); 244 | 245 | mem::drop(cache0); 246 | mem::drop(cache1); 247 | mem::drop(cache2); 248 | 249 | assert!(!bit_set.borrow().contains(&0)); 250 | assert!(!bit_set.borrow().contains(&1)); 251 | assert!(!bit_set.borrow().contains(&2)); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /forma/src/cpu/channel.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use crate::utils::simd::f32x8; 16 | 17 | pub(crate) trait Mask { 18 | fn zero() -> Self; 19 | fn one() -> Self; 20 | } 21 | 22 | impl Mask for f32x8 { 23 | #[inline] 24 | fn zero() -> Self { 25 | f32x8::splat(0.0) 26 | } 27 | 28 | #[inline] 29 | fn one() -> Self { 30 | f32x8::splat(1.0) 31 | } 32 | } 33 | 34 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 35 | pub enum Channel { 36 | Red, 37 | Green, 38 | Blue, 39 | Alpha, 40 | Zero, 41 | One, 42 | } 43 | 44 | impl Channel { 45 | pub(crate) fn select(self, r: M, g: M, b: M, a: M) -> M { 46 | match self { 47 | Channel::Red => r, 48 | Channel::Green => g, 49 | Channel::Blue => b, 50 | Channel::Alpha => a, 51 | Channel::Zero => M::zero(), 52 | Channel::One => M::one(), 53 | } 54 | } 55 | } 56 | 57 | pub const RGBA: [Channel; 4] = [Channel::Red, Channel::Green, Channel::Blue, Channel::Alpha]; 58 | pub const BGRA: [Channel; 4] = [Channel::Blue, Channel::Green, Channel::Red, Channel::Alpha]; 59 | pub const RGB0: [Channel; 4] = [Channel::Red, Channel::Green, Channel::Blue, Channel::Zero]; 60 | pub const BGR0: [Channel; 4] = [Channel::Blue, Channel::Green, Channel::Red, Channel::Zero]; 61 | pub const RGB1: [Channel; 4] = [Channel::Red, Channel::Green, Channel::Blue, Channel::One]; 62 | pub const BGR1: [Channel; 4] = [Channel::Blue, Channel::Green, Channel::Red, Channel::One]; 63 | -------------------------------------------------------------------------------- /forma/src/cpu/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | pub mod buffer; 16 | mod channel; 17 | mod painter; 18 | mod pixel_segment; 19 | mod rasterizer; 20 | mod renderer; 21 | 22 | pub(crate) use self::{ 23 | pixel_segment::{search_last_by_key, PixelSegment}, 24 | rasterizer::Rasterizer, 25 | }; 26 | 27 | pub use self::{ 28 | channel::*, 29 | renderer::{Rect, Renderer}, 30 | }; 31 | -------------------------------------------------------------------------------- /forma/src/cpu/painter/layer_workbench/passes/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use rustc_hash::FxHashSet; 16 | 17 | mod skip_fully_covered_layers; 18 | mod skip_trivial_clips; 19 | mod tile_unchanged; 20 | 21 | pub use skip_fully_covered_layers::skip_fully_covered_layers_pass; 22 | pub use skip_trivial_clips::skip_trivial_clips_pass; 23 | pub use tile_unchanged::tile_unchanged_pass; 24 | 25 | #[derive(Clone, Debug)] 26 | pub struct PassesSharedState { 27 | pub skip_clipping: FxHashSet, 28 | pub layers_were_removed: bool, 29 | } 30 | 31 | impl Default for PassesSharedState { 32 | fn default() -> Self { 33 | Self { 34 | layers_were_removed: true, 35 | skip_clipping: FxHashSet::default(), 36 | } 37 | } 38 | } 39 | 40 | impl PassesSharedState { 41 | pub fn reset(&mut self) { 42 | self.skip_clipping.clear(); 43 | self.layers_were_removed = true; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /forma/src/cpu/painter/layer_workbench/passes/skip_fully_covered_layers.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::ops::ControlFlow; 16 | 17 | use crate::{ 18 | cpu::painter::{ 19 | layer_workbench::{Context, LayerWorkbenchState, OptimizerTileWriteOp}, 20 | LayerProps, 21 | }, 22 | styling::{BlendMode, Color, Fill, Func, Style}, 23 | }; 24 | 25 | use super::PassesSharedState; 26 | 27 | pub fn skip_fully_covered_layers_pass<'w, 'c, P: LayerProps>( 28 | workbench: &'w mut LayerWorkbenchState, 29 | state: &'w mut PassesSharedState, 30 | context: &'c Context<'_, P>, 31 | ) -> ControlFlow { 32 | #[derive(Debug)] 33 | enum InterestingCover { 34 | Opaque(Color), 35 | Incomplete, 36 | } 37 | 38 | let mut first_interesting_cover = None; 39 | // If layers were removed, we cannot assume anything because a visible layer 40 | // might have been removed since last frame. 41 | let mut visible_layers_are_unchanged = !state.layers_were_removed; 42 | for (i, &id) in workbench.ids.iter_masked().rev() { 43 | let props = context.props.get(id); 44 | 45 | if !context.props.is_unchanged(id) { 46 | visible_layers_are_unchanged = false; 47 | } 48 | 49 | let is_clipped = || { 50 | matches!( 51 | props.func, 52 | Func::Draw(Style { 53 | is_clipped: true, 54 | .. 55 | }) 56 | ) && !state.skip_clipping.contains(&id) 57 | }; 58 | 59 | if is_clipped() || !workbench.layer_is_full(context, id, props.fill_rule) { 60 | if first_interesting_cover.is_none() { 61 | first_interesting_cover = Some(InterestingCover::Incomplete); 62 | // The loop does not break here in order to try to cull some layers that are 63 | // completely covered. 64 | } 65 | } else if let Func::Draw(Style { 66 | fill: Fill::Solid(color), 67 | blend_mode: BlendMode::Over, 68 | .. 69 | }) = props.func 70 | { 71 | if color.a == 1.0 { 72 | if first_interesting_cover.is_none() { 73 | first_interesting_cover = Some(InterestingCover::Opaque(color)); 74 | } 75 | 76 | workbench.ids.skip_until(i); 77 | 78 | break; 79 | } 80 | } 81 | } 82 | 83 | let (i, bottom_color) = match first_interesting_cover { 84 | // First opaque layer is skipped when blending. 85 | Some(InterestingCover::Opaque(color)) => { 86 | // All visible layers are unchanged so we can skip drawing altogether. 87 | if visible_layers_are_unchanged { 88 | return ControlFlow::Break(OptimizerTileWriteOp::None); 89 | } 90 | 91 | (1, color) 92 | } 93 | // The clear color is used as a virtual first opqaue layer. 94 | None => (0, context.clear_color), 95 | // Visible incomplete cover makes full optimization impossible. 96 | Some(InterestingCover::Incomplete) => return ControlFlow::Continue(()), 97 | }; 98 | 99 | let color = workbench 100 | .ids 101 | .iter_masked() 102 | .skip(i) 103 | .try_fold(bottom_color, |dst, (_, &id)| { 104 | match context.props.get(id).func { 105 | Func::Draw(Style { 106 | fill: Fill::Solid(color), 107 | blend_mode, 108 | .. 109 | }) => Some(blend_mode.blend(dst, color)), 110 | // Fill is not solid. 111 | _ => None, 112 | } 113 | }); 114 | 115 | match color { 116 | Some(color) => ControlFlow::Break(OptimizerTileWriteOp::Solid(color)), 117 | None => ControlFlow::Continue(()), 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /forma/src/cpu/painter/layer_workbench/passes/skip_trivial_clips.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::ops::ControlFlow; 16 | 17 | use crate::{ 18 | cpu::painter::{ 19 | layer_workbench::{Context, Index, LayerWorkbenchState, OptimizerTileWriteOp}, 20 | LayerProps, 21 | }, 22 | styling::{Func, Style}, 23 | }; 24 | 25 | use super::PassesSharedState; 26 | 27 | pub fn skip_trivial_clips_pass<'w, 'c, P: LayerProps>( 28 | workbench: &'w mut LayerWorkbenchState, 29 | state: &'w mut PassesSharedState, 30 | context: &'c Context<'_, P>, 31 | ) -> ControlFlow { 32 | struct Clip { 33 | is_full: bool, 34 | last_layer_id: u32, 35 | i: Index, 36 | is_used: bool, 37 | } 38 | 39 | let mut clip = None; 40 | 41 | for (i, &id) in workbench.ids.iter_masked() { 42 | let props = context.props.get(id); 43 | 44 | if let Func::Clip(layers) = props.func { 45 | let is_full = workbench.layer_is_full(context, id, props.fill_rule); 46 | 47 | clip = Some(Clip { 48 | is_full, 49 | last_layer_id: id + layers as u32, 50 | i, 51 | is_used: false, 52 | }); 53 | 54 | if is_full { 55 | // Skip full clips. 56 | workbench.ids.set_mask(i, false); 57 | } 58 | } 59 | 60 | if let Func::Draw(Style { 61 | is_clipped: true, .. 62 | }) = props.func 63 | { 64 | match clip { 65 | Some(Clip { 66 | is_full, 67 | last_layer_id, 68 | ref mut is_used, 69 | .. 70 | }) if id <= last_layer_id => { 71 | if is_full { 72 | // Skip clipping when clip is full. 73 | state.skip_clipping.insert(id); 74 | } else { 75 | *is_used = true; 76 | } 77 | } 78 | _ => { 79 | // Skip layer outside of clip. 80 | workbench.ids.set_mask(i, false); 81 | } 82 | } 83 | } 84 | 85 | if let Some(Clip { 86 | last_layer_id, 87 | i, 88 | is_used, 89 | .. 90 | }) = clip 91 | { 92 | if id > last_layer_id { 93 | clip = None; 94 | 95 | if !is_used { 96 | // Remove unused clips. 97 | workbench.ids.set_mask(i, false); 98 | } 99 | } 100 | } 101 | } 102 | 103 | // Clip layer might be last layer. 104 | if let Some(Clip { i, is_used, .. }) = clip { 105 | if !is_used { 106 | // Remove unused clips. 107 | workbench.ids.set_mask(i, false); 108 | } 109 | } 110 | 111 | ControlFlow::Continue(()) 112 | } 113 | -------------------------------------------------------------------------------- /forma/src/cpu/painter/layer_workbench/passes/tile_unchanged.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::ops::ControlFlow; 16 | 17 | use crate::cpu::painter::{ 18 | layer_workbench::{Context, LayerWorkbenchState, OptimizerTileWriteOp}, 19 | LayerProps, 20 | }; 21 | 22 | use super::PassesSharedState; 23 | 24 | pub fn tile_unchanged_pass<'w, 'c, P: LayerProps>( 25 | workbench: &'w mut LayerWorkbenchState, 26 | state: &'w mut PassesSharedState, 27 | context: &'c Context<'_, P>, 28 | ) -> ControlFlow { 29 | let clear_color_is_unchanged = context 30 | .cached_clear_color 31 | .map(|previous_clear_color| previous_clear_color == context.clear_color) 32 | .unwrap_or_default(); 33 | 34 | let tile_paint = context.cached_tile.as_ref().and_then(|cached_tile| { 35 | let layers = workbench.ids.len() as u32; 36 | let previous_layers = cached_tile.update_layer_count(Some(layers)); 37 | 38 | let is_unchanged = previous_layers 39 | .map(|previous_layers| { 40 | state.layers_were_removed = layers < previous_layers; 41 | 42 | previous_layers == layers 43 | && workbench 44 | .ids 45 | .iter() 46 | .all(|&id| context.props.is_unchanged(id)) 47 | }) 48 | .unwrap_or_default(); 49 | 50 | (clear_color_is_unchanged && is_unchanged).then_some(OptimizerTileWriteOp::None) 51 | }); 52 | 53 | match tile_paint { 54 | Some(tile_paint) => ControlFlow::Break(tile_paint), 55 | None => ControlFlow::Continue(()), 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /forma/src/cpu/renderer.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{ 16 | borrow::Cow, 17 | cell::{RefCell, RefMut}, 18 | ops::Range, 19 | rc::Rc, 20 | }; 21 | 22 | use rustc_hash::FxHashMap; 23 | 24 | use crate::{ 25 | consts, 26 | cpu::painter::{self, CachedTile, LayerProps}, 27 | styling::{Color, Props}, 28 | utils::{Order, SmallBitSet}, 29 | Composition, Layer, 30 | }; 31 | 32 | use super::{ 33 | buffer::{layout::Layout, Buffer, BufferLayerCache}, 34 | Channel, Rasterizer, 35 | }; 36 | 37 | #[derive(Clone, Debug, Eq, Hash, PartialEq)] 38 | pub struct Rect { 39 | pub(crate) hor: Range, 40 | pub(crate) vert: Range, 41 | } 42 | 43 | impl Rect { 44 | /// The resulting rectangle is currently approximated to the tile grid. 45 | pub fn new(horizontal: Range, vertical: Range) -> Self { 46 | Self { 47 | hor: horizontal.start / consts::cpu::TILE_WIDTH 48 | ..(horizontal.end + consts::cpu::TILE_WIDTH - 1) / consts::cpu::TILE_WIDTH, 49 | vert: vertical.start / consts::cpu::TILE_HEIGHT 50 | ..(vertical.end + consts::cpu::TILE_HEIGHT - 1) / consts::cpu::TILE_HEIGHT, 51 | } 52 | } 53 | } 54 | 55 | #[derive(Debug, Default)] 56 | pub struct Renderer { 57 | rasterizer: Rasterizer<{ consts::cpu::TILE_WIDTH }, { consts::cpu::TILE_HEIGHT }>, 58 | buffers_with_caches: Rc>, 59 | } 60 | 61 | impl Renderer { 62 | #[inline] 63 | pub fn new() -> Self { 64 | Self::default() 65 | } 66 | 67 | #[inline] 68 | pub fn create_buffer_layer_cache(&mut self) -> Option { 69 | self.buffers_with_caches 70 | .borrow_mut() 71 | .first_empty_slot() 72 | .map(|id| BufferLayerCache::new(id, Rc::downgrade(&self.buffers_with_caches))) 73 | } 74 | 75 | pub fn render( 76 | &mut self, 77 | composition: &mut Composition, 78 | buffer: &mut Buffer<'_, '_, L>, 79 | mut channels: [Channel; 4], 80 | clear_color: Color, 81 | crop: Option, 82 | ) where 83 | L: Layout, 84 | { 85 | // If `clear_color` has alpha = 1 we can upgrade the alpha channel to `Channel::One` 86 | // in order to skip reading the alpha channel. 87 | if clear_color.a == 1.0 { 88 | channels = channels.map(|c| match c { 89 | Channel::Alpha => Channel::One, 90 | c => c, 91 | }); 92 | } 93 | 94 | if let Some(layer_cache) = buffer.layer_cache.as_ref() { 95 | let tiles_len = buffer.layout.width_in_tiles() * buffer.layout.height_in_tiles(); 96 | let cache = &layer_cache.cache; 97 | 98 | cache 99 | .borrow_mut() 100 | .tiles 101 | .resize(tiles_len, CachedTile::default()); 102 | 103 | if cache.borrow().width != Some(buffer.layout.width()) 104 | || cache.borrow().height != Some(buffer.layout.height()) 105 | { 106 | cache.borrow_mut().width = Some(buffer.layout.width()); 107 | cache.borrow_mut().height = Some(buffer.layout.height()); 108 | 109 | layer_cache.clear(); 110 | } 111 | } 112 | 113 | composition.compact_geom(); 114 | composition 115 | .shared_state 116 | .borrow_mut() 117 | .props_interner 118 | .compact(); 119 | 120 | let layers = &composition.layers; 121 | let shared_state = &mut *composition.shared_state.borrow_mut(); 122 | let segment_buffer = &mut shared_state.segment_buffer; 123 | let geom_id_to_order = &shared_state.geom_id_to_order; 124 | let rasterizer = &mut self.rasterizer; 125 | 126 | struct CompositionContext<'l> { 127 | layers: &'l FxHashMap, 128 | cache_id: Option, 129 | } 130 | 131 | impl LayerProps for CompositionContext<'_> { 132 | #[inline] 133 | fn get(&self, id: u32) -> Cow<'_, Props> { 134 | Cow::Borrowed( 135 | self.layers 136 | .get(&Order::new(id).expect("PixelSegment layer_id cannot overflow Order")) 137 | .map(|layer| &layer.props) 138 | .expect( 139 | "Layers outside of HashMap should not produce visible PixelSegments", 140 | ), 141 | ) 142 | } 143 | 144 | #[inline] 145 | fn is_unchanged(&self, id: u32) -> bool { 146 | match self.cache_id { 147 | None => false, 148 | Some(cache_id) => self 149 | .layers 150 | .get(&Order::new(id).expect("PixelSegment layer_id cannot overflow Order")) 151 | .map(|layer| layer.is_unchanged(cache_id)) 152 | .expect( 153 | "Layers outside of HashMap should not produce visible PixelSegments", 154 | ), 155 | } 156 | } 157 | } 158 | 159 | let context = CompositionContext { 160 | layers, 161 | cache_id: buffer.layer_cache.as_ref().map(|cache| cache.id), 162 | }; 163 | 164 | // `take()` sets the RefCell's content with `Default::default()` which is cheap for Option. 165 | let taken_buffer = segment_buffer 166 | .take() 167 | .expect("segment_buffer should not be None"); 168 | 169 | *segment_buffer = { 170 | let segment_buffer_view = { 171 | duration!("gfx", "SegmentBuffer::fill_cpu_view"); 172 | taken_buffer.fill_cpu_view( 173 | buffer.layout.width(), 174 | buffer.layout.height(), 175 | context.layers, 176 | geom_id_to_order, 177 | ) 178 | }; 179 | 180 | { 181 | duration!("gfx", "Rasterizer::rasterize"); 182 | rasterizer.rasterize(&segment_buffer_view); 183 | } 184 | { 185 | duration!("gfx", "Rasterizer::sort"); 186 | rasterizer.sort(); 187 | } 188 | 189 | let previous_clear_color = buffer 190 | .layer_cache 191 | .as_ref() 192 | .and_then(|layer_cache| layer_cache.cache.borrow().clear_color); 193 | 194 | let cached_tiles = buffer.layer_cache.as_ref().map(|layer_cache| { 195 | RefMut::map(layer_cache.cache.borrow_mut(), |cache| &mut cache.tiles) 196 | }); 197 | 198 | { 199 | duration!("gfx", "painter::for_each_row"); 200 | painter::for_each_row( 201 | buffer.layout, 202 | buffer.buffer, 203 | channels, 204 | buffer.flusher.as_deref(), 205 | previous_clear_color, 206 | cached_tiles, 207 | rasterizer.segments(), 208 | clear_color, 209 | &crop, 210 | &context, 211 | ); 212 | } 213 | 214 | Some(segment_buffer_view.recycle()) 215 | }; 216 | 217 | if let Some(buffer_layer_cache) = &buffer.layer_cache { 218 | buffer_layer_cache.cache.borrow_mut().clear_color = Some(clear_color); 219 | 220 | for layer in composition.layers.values_mut() { 221 | layer.set_is_unchanged(buffer_layer_cache.id, layer.inner.is_enabled); 222 | } 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /forma/src/gpu/conveyor_sort/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{borrow::Cow, convert::TryFrom, mem}; 16 | 17 | use bytemuck::{Pod, Zeroable}; 18 | use ramhorns::{Content, Template}; 19 | use wgpu::util::DeviceExt; 20 | 21 | use crate::{segment::SegmentBufferView, utils::DivCeil}; 22 | 23 | use super::{GpuContext, TimeStamp}; 24 | 25 | const MAX_WORKGROUPS: usize = 65_536; 26 | pub const BLOCK_SIZE: BlockSize = BlockSize::new(64, 9); 27 | 28 | #[derive(Debug)] 29 | pub struct Data<'s> { 30 | pub segment_buffer_view: &'s SegmentBufferView, 31 | pub pixel_segment_buffer: wgpu::Buffer, 32 | pub swap_buffer: wgpu::Buffer, 33 | pub offset_buffer: wgpu::Buffer, 34 | } 35 | 36 | #[derive(Content, Debug)] 37 | pub struct BlockSize { 38 | block_width: u32, 39 | block_height: u32, 40 | pub block_len: u32, 41 | } 42 | 43 | impl BlockSize { 44 | pub const fn new(block_width: u32, block_height: u32) -> Self { 45 | Self { 46 | block_width, 47 | block_height, 48 | block_len: block_width * block_height, 49 | } 50 | } 51 | } 52 | 53 | #[repr(C)] 54 | #[derive(Clone, Copy, Debug, Pod, Zeroable)] 55 | struct Config { 56 | len: u32, 57 | len_in_blocks: u32, 58 | n_way: u32, 59 | } 60 | 61 | impl Config { 62 | pub fn new(len: usize) -> Option { 63 | let len_in_blocks = u32::try_from(len.div_ceil_(BLOCK_SIZE.block_len as _)).ok()?; 64 | 65 | Some(Self { 66 | len: len as _, 67 | len_in_blocks, 68 | n_way: 0, 69 | }) 70 | } 71 | 72 | pub fn workgroup_size(&self) -> u32 { 73 | self.len_in_blocks.min(MAX_WORKGROUPS as u32) 74 | } 75 | } 76 | 77 | #[derive(Debug)] 78 | pub struct SortContext { 79 | block_sort_pipeline: wgpu::ComputePipeline, 80 | block_sort_bind_group_layout: wgpu::BindGroupLayout, 81 | find_merge_offsets_pipeline: wgpu::ComputePipeline, 82 | find_merge_offsets_bind_group_layout: wgpu::BindGroupLayout, 83 | merge_blocks_pipeline: wgpu::ComputePipeline, 84 | merge_blocks_bind_group_layout: wgpu::BindGroupLayout, 85 | } 86 | 87 | impl GpuContext for SortContext { 88 | type Data<'s> = Data<'s>; 89 | 90 | fn init(device: &wgpu::Device) -> Self { 91 | let template = Template::new(include_str!("sort.wgsl")).unwrap(); 92 | 93 | let module = device.create_shader_module(wgpu::ShaderModuleDescriptor { 94 | label: None, 95 | source: wgpu::ShaderSource::Wgsl(Cow::Owned(template.render(&BLOCK_SIZE))), 96 | }); 97 | 98 | let block_sort_pipeline = 99 | device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { 100 | label: None, 101 | layout: None, 102 | module: &module, 103 | entry_point: "blockSort", 104 | }); 105 | 106 | let block_sort_bind_group_layout = block_sort_pipeline.get_bind_group_layout(0); 107 | 108 | let find_merge_offsets_pipeline = 109 | device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { 110 | label: None, 111 | layout: None, 112 | module: &module, 113 | entry_point: "findMergeOffsets", 114 | }); 115 | 116 | let find_merge_offsets_bind_group_layout = 117 | find_merge_offsets_pipeline.get_bind_group_layout(0); 118 | 119 | let merge_blocks_pipeline = 120 | device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { 121 | label: None, 122 | layout: None, 123 | module: &module, 124 | entry_point: "mergeBlocks", 125 | }); 126 | 127 | let merge_blocks_bind_group_layout = merge_blocks_pipeline.get_bind_group_layout(0); 128 | 129 | Self { 130 | block_sort_pipeline, 131 | block_sort_bind_group_layout, 132 | find_merge_offsets_pipeline, 133 | find_merge_offsets_bind_group_layout, 134 | merge_blocks_pipeline, 135 | merge_blocks_bind_group_layout, 136 | } 137 | } 138 | 139 | fn encode( 140 | &self, 141 | device: &wgpu::Device, 142 | encoder: &mut wgpu::CommandEncoder, 143 | time_stamp: Option, 144 | data: &mut Data, 145 | ) { 146 | let mut config = 147 | Config::new(data.segment_buffer_view.len()).expect("numbers length too high"); 148 | 149 | let config_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 150 | label: None, 151 | contents: bytemuck::bytes_of(&config), 152 | usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, 153 | }); 154 | 155 | let block_sort_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { 156 | label: None, 157 | layout: &self.block_sort_bind_group_layout, 158 | entries: &[ 159 | wgpu::BindGroupEntry { 160 | binding: 0, 161 | resource: data.pixel_segment_buffer.as_entire_binding(), 162 | }, 163 | wgpu::BindGroupEntry { 164 | binding: 2, 165 | resource: config_buffer.as_entire_binding(), 166 | }, 167 | ], 168 | }); 169 | 170 | if let Some(TimeStamp { 171 | query_set, 172 | start_index, 173 | .. 174 | }) = time_stamp 175 | { 176 | encoder.write_timestamp(query_set, start_index); 177 | } 178 | 179 | { 180 | let mut cpass = 181 | encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { label: None }); 182 | cpass.set_pipeline(&self.block_sort_pipeline); 183 | cpass.set_bind_group(0, &block_sort_bind_group, &[]); 184 | 185 | cpass.dispatch_workgroups(config.workgroup_size(), 1, 1); 186 | } 187 | 188 | let rounds = config.len_in_blocks.next_power_of_two().trailing_zeros(); 189 | let max_rounds = device 190 | .limits() 191 | .max_storage_buffer_binding_size 192 | .div_ceil_(BLOCK_SIZE.block_len) 193 | .next_power_of_two() 194 | .trailing_zeros(); 195 | 196 | for round in 0..max_rounds { 197 | config.n_way = 1 << (round + 1); 198 | 199 | let config_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 200 | label: None, 201 | contents: bytemuck::bytes_of(&config), 202 | usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM, 203 | }); 204 | 205 | let find_merge_offsets_bind_group = 206 | device.create_bind_group(&wgpu::BindGroupDescriptor { 207 | label: None, 208 | layout: &self.find_merge_offsets_bind_group_layout, 209 | entries: &[ 210 | wgpu::BindGroupEntry { 211 | binding: 0, 212 | resource: if round % 2 == 0 { 213 | data.pixel_segment_buffer.as_entire_binding() 214 | } else { 215 | data.swap_buffer.as_entire_binding() 216 | }, 217 | }, 218 | wgpu::BindGroupEntry { 219 | binding: 2, 220 | resource: config_buffer.as_entire_binding(), 221 | }, 222 | wgpu::BindGroupEntry { 223 | binding: 3, 224 | resource: data.offset_buffer.as_entire_binding(), 225 | }, 226 | ], 227 | }); 228 | 229 | { 230 | let mut cpass = 231 | encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { label: None }); 232 | cpass.set_pipeline(&self.find_merge_offsets_pipeline); 233 | cpass.set_bind_group(0, &find_merge_offsets_bind_group, &[]); 234 | 235 | cpass.dispatch_workgroups( 236 | config.workgroup_size().div_ceil_(BLOCK_SIZE.block_width), 237 | 1, 238 | 1, 239 | ); 240 | } 241 | 242 | let merge_blocks_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { 243 | label: None, 244 | layout: &self.merge_blocks_bind_group_layout, 245 | entries: &[ 246 | wgpu::BindGroupEntry { 247 | binding: 0, 248 | resource: if round % 2 == 0 { 249 | data.pixel_segment_buffer.as_entire_binding() 250 | } else { 251 | data.swap_buffer.as_entire_binding() 252 | }, 253 | }, 254 | wgpu::BindGroupEntry { 255 | binding: 1, 256 | resource: if round % 2 == 0 { 257 | data.swap_buffer.as_entire_binding() 258 | } else { 259 | data.pixel_segment_buffer.as_entire_binding() 260 | }, 261 | }, 262 | wgpu::BindGroupEntry { 263 | binding: 2, 264 | resource: config_buffer.as_entire_binding(), 265 | }, 266 | wgpu::BindGroupEntry { 267 | binding: 3, 268 | resource: data.offset_buffer.as_entire_binding(), 269 | }, 270 | ], 271 | }); 272 | 273 | { 274 | let mut cpass = 275 | encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { label: None }); 276 | cpass.set_pipeline(&self.merge_blocks_pipeline); 277 | cpass.set_bind_group(0, &merge_blocks_bind_group, &[]); 278 | 279 | cpass.dispatch_workgroups(config.workgroup_size(), 1, 1); 280 | } 281 | 282 | if round == max_rounds - 1 { 283 | if let Some(TimeStamp { 284 | query_set, 285 | end_index, 286 | .. 287 | }) = time_stamp 288 | { 289 | encoder.write_timestamp(query_set, end_index); 290 | } 291 | } 292 | } 293 | 294 | if rounds % 2 != 0 { 295 | mem::swap(&mut data.pixel_segment_buffer, &mut data.swap_buffer); 296 | } 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /forma/src/gpu/conveyor_sort/sort.wgsl: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | let BLOCK_WIDTH: u32 = {{block_width}}u; 16 | let BLOCK_HEIGHT: u32 = {{block_height}}u; 17 | let BLOCK_LEN: u32 = {{block_len}}u; 18 | 19 | struct _u64 { 20 | lo: u32, 21 | hi: u32, 22 | } 23 | 24 | let MAX = _u64(0xffffffffu, 0xffffffffu); 25 | 26 | fn le(x: _u64, y: _u64) -> bool { 27 | return x.hi < y.hi || x.hi == y.hi && x.lo <= y.lo; 28 | } 29 | 30 | fn gt(x: _u64, y: _u64) -> bool { 31 | return x.hi > y.hi || x.hi == y.hi && x.lo > y.lo; 32 | } 33 | 34 | struct Config { 35 | len: u32, 36 | len_in_blocks: u32, 37 | n_way: u32, 38 | } 39 | 40 | @group(0) @binding(0) var buffer0: array<_u64>; 41 | @group(0) @binding(1) var buffer1: array<_u64>; 42 | @group(0) @binding(2) var config: Config; 43 | @group(0) @binding(3) var offsets: array; 44 | 45 | var block: array<_u64, BLOCK_LEN>; 46 | 47 | struct MergeBlock { 48 | start: u32, 49 | mid: u32, 50 | end: u32, 51 | index: u32, 52 | } 53 | 54 | fn getMergeBlock( 55 | n_way: u32, 56 | id: u32, 57 | len_per_id: u32, 58 | ) -> MergeBlock { 59 | let n_way_mask = n_way - 1u; 60 | let start = (~n_way_mask & id) * len_per_id; 61 | let index = (n_way_mask & id) * len_per_id; 62 | 63 | let mid = start + (n_way >> 1u) * len_per_id; 64 | let end = start + n_way * len_per_id; 65 | 66 | return MergeBlock(start, mid, end, index); 67 | } 68 | 69 | fn getBuffer0(i: u32) -> _u64 { 70 | if i < config.len { 71 | return buffer0[i]; 72 | } else { 73 | return MAX; 74 | } 75 | } 76 | 77 | fn setBuffer0(i: u32, val: _u64) { 78 | if i < config.len { 79 | buffer0[i] = val; 80 | } 81 | } 82 | 83 | fn setBuffer1(i: u32, val: _u64) { 84 | if i < config.len { 85 | buffer1[i] = val; 86 | } 87 | } 88 | 89 | fn buffer0ToLocal( 90 | col: ptr>, 91 | offset: u32, 92 | local_id: u32, 93 | ) { 94 | for (var i = 0u; i < BLOCK_HEIGHT; i++) { 95 | (*col)[i] = getBuffer0(i * BLOCK_WIDTH + offset + local_id); 96 | } 97 | } 98 | 99 | fn localToBuffer0( 100 | col: ptr>, 101 | offset: u32, 102 | local_id: u32, 103 | ) { 104 | 105 | for (var i = 0u; i < BLOCK_HEIGHT; i++) { 106 | setBuffer0(i * BLOCK_WIDTH + offset + local_id, (*col)[i]); 107 | } 108 | } 109 | 110 | fn localToSharedTransposed( 111 | col: ptr>, 112 | local_id: u32, 113 | ) { 114 | for (var i = 0u; i < BLOCK_HEIGHT; i++) { 115 | block[local_id * BLOCK_HEIGHT + i] = (*col)[i]; 116 | } 117 | 118 | workgroupBarrier(); 119 | } 120 | 121 | fn sharedToBuffer0(offset: u32, local_id: u32) { 122 | for (var i = 0u; i < BLOCK_HEIGHT; i++) { 123 | let index = i * BLOCK_WIDTH + local_id; 124 | setBuffer0(index + offset, block[index]); 125 | } 126 | 127 | workgroupBarrier(); 128 | } 129 | 130 | fn oddEvenSort(col: ptr>) { 131 | for (var i = 0u; i < BLOCK_HEIGHT; i++) { 132 | for (var j = 1u & i; j < BLOCK_HEIGHT - 1u; j += 2u) { 133 | if gt((*col)[j], (*col)[j + 1u]) { 134 | let swap = (*col)[j]; 135 | (*col)[j] = (*col)[j + 1u]; 136 | (*col)[j + 1u] = swap; 137 | } 138 | } 139 | } 140 | } 141 | 142 | fn findMergeOffsetShared( 143 | left_start: u32, 144 | left_len: u32, 145 | right_start: u32, 146 | right_len: u32, 147 | index: u32, 148 | ) -> u32 { 149 | var start = u32(max(0, i32(index) - i32(right_len))); 150 | var end = min(index, left_len); 151 | 152 | while start < end { 153 | let mid = (start + end) >> 1u; 154 | 155 | let left = block[left_start + mid]; 156 | let right = block[right_start + index - 1u - mid]; 157 | 158 | if le(left, right) { 159 | start = mid + 1u; 160 | } else { 161 | end = mid; 162 | } 163 | } 164 | 165 | return start; 166 | } 167 | 168 | fn mergeInLocal( 169 | left_start: u32, 170 | left_end: u32, 171 | right_start: u32, 172 | right_end: u32, 173 | col: ptr>, 174 | ) { 175 | var left_i = left_start; 176 | var right_i = right_start; 177 | 178 | var left = block[left_i]; 179 | var right = block[right_i]; 180 | 181 | for (var i = 0u; i < BLOCK_HEIGHT; i++) { 182 | let go_left = (right_i >= right_end) 183 | || ((left_i < left_end) 184 | && le(left, right)); 185 | 186 | if go_left { 187 | (*col)[i] = left; 188 | 189 | left_i++; 190 | left = block[left_i]; 191 | } else { 192 | (*col)[i] = right; 193 | 194 | right_i++; 195 | right = block[right_i]; 196 | } 197 | } 198 | 199 | workgroupBarrier(); 200 | } 201 | 202 | fn blockSortBlock(block_id: u32, local_id: u32) { 203 | let offset = block_id * BLOCK_WIDTH * BLOCK_HEIGHT; 204 | var col: array<_u64, BLOCK_HEIGHT>; 205 | 206 | buffer0ToLocal(&col, offset, local_id); 207 | 208 | oddEvenSort(&col); 209 | 210 | localToSharedTransposed(&col, local_id); 211 | 212 | for (var n_way = 2u; n_way <= BLOCK_WIDTH; n_way <<= 1u) { 213 | let merge_block = getMergeBlock(n_way, local_id, BLOCK_HEIGHT); 214 | 215 | let shared_offset = findMergeOffsetShared( 216 | merge_block.start, 217 | merge_block.mid - merge_block.start, 218 | merge_block.mid, 219 | merge_block.end - merge_block.mid, 220 | merge_block.index, 221 | ); 222 | 223 | mergeInLocal( 224 | merge_block.start + shared_offset, 225 | merge_block.mid, 226 | merge_block.mid + merge_block.index - shared_offset, 227 | merge_block.end, 228 | &col, 229 | ); 230 | 231 | localToSharedTransposed(&col, local_id); 232 | } 233 | 234 | sharedToBuffer0(offset, local_id); 235 | } 236 | 237 | fn findMergeOffsetBuffer0( 238 | left_start: u32, 239 | left_len: u32, 240 | right_start: u32, 241 | right_len: u32, 242 | index: u32, 243 | ) -> u32 { 244 | var start = u32(max(0, i32(index) - i32(right_len))); 245 | var end = min(index, left_len); 246 | 247 | while start < end { 248 | let mid = (start + end) >> 1u; 249 | 250 | let left = getBuffer0(left_start + mid); 251 | let right = getBuffer0(right_start + index - 1u - mid); 252 | 253 | if le(left, right) { 254 | start = mid + 1u; 255 | } else { 256 | end = mid; 257 | } 258 | } 259 | 260 | return start; 261 | } 262 | 263 | fn findMergeOffsetBlock(block_id: u32) { 264 | let merge_block = getMergeBlock(config.n_way, block_id, BLOCK_LEN); 265 | 266 | let mid = min(config.len_in_blocks * BLOCK_LEN, merge_block.mid); 267 | let end = min(config.len_in_blocks * BLOCK_LEN, merge_block.end); 268 | 269 | let offset = findMergeOffsetBuffer0( 270 | merge_block.start, 271 | mid - merge_block.start, 272 | mid, 273 | end - mid, 274 | merge_block.index, 275 | ); 276 | 277 | offsets[block_id] = offset; 278 | } 279 | 280 | fn buffer0ToLocal2( 281 | col: ptr>, 282 | offset0: u32, 283 | len0: u32, 284 | offset1: u32, 285 | len1: u32, 286 | local_id: u32, 287 | ) { 288 | let new_offset1 = offset1 - len0; 289 | 290 | for (var i = 0u; i < BLOCK_HEIGHT; i++) { 291 | let index = i * BLOCK_WIDTH + local_id; 292 | 293 | var block_i: u32; 294 | if index < len0 { 295 | block_i = i * BLOCK_WIDTH + offset0 + local_id; 296 | } else { 297 | block_i = i * BLOCK_WIDTH + new_offset1 + local_id; 298 | } 299 | 300 | (*col)[i] = getBuffer0(block_i); 301 | } 302 | } 303 | 304 | fn localToShared( 305 | col: ptr>, 306 | local_id: u32, 307 | ) { 308 | for (var i = 0u; i < BLOCK_HEIGHT; i++) { 309 | block[i * BLOCK_WIDTH + local_id] = (*col)[i]; 310 | } 311 | 312 | workgroupBarrier(); 313 | } 314 | 315 | fn sharedToBuffer1(offset: u32, local_id: u32) { 316 | for (var i = 0u; i < BLOCK_HEIGHT; i++) { 317 | let index = i * BLOCK_WIDTH + local_id; 318 | setBuffer1(index + offset, block[index]); 319 | } 320 | 321 | workgroupBarrier(); 322 | } 323 | 324 | fn mergeBlocksBlock(block_id: u32, local_id: u32) { 325 | var col: array<_u64, BLOCK_HEIGHT>; 326 | 327 | let merge_block = getMergeBlock(config.n_way, block_id, BLOCK_LEN); 328 | let n_way_mask = config.n_way - 1u; 329 | 330 | let offset0 = offsets[block_id]; 331 | var offset1: u32; 332 | 333 | if (n_way_mask & block_id) == n_way_mask { 334 | offset1 = (config.n_way >> 1u) * BLOCK_LEN; 335 | } else { 336 | offset1 = offsets[block_id + 1u]; 337 | } 338 | 339 | let delta_offset = offset1 - offset0; 340 | let left_len = delta_offset; 341 | let right_len = BLOCK_LEN - delta_offset; 342 | 343 | buffer0ToLocal2( 344 | &col, 345 | merge_block.start + offset0, 346 | left_len, 347 | merge_block.mid + merge_block.index - offset0, 348 | right_len, 349 | local_id, 350 | ); 351 | 352 | localToShared(&col, local_id); 353 | 354 | // Only perform merge if needed and copy otherwise. 355 | if left_len != 0u && right_len != 0u { 356 | let i = local_id * BLOCK_HEIGHT; 357 | 358 | let shared_offset = findMergeOffsetShared( 359 | 0u, 360 | left_len, 361 | left_len, 362 | right_len, 363 | i, 364 | ); 365 | 366 | mergeInLocal( 367 | shared_offset, 368 | left_len, 369 | left_len + i - shared_offset, 370 | BLOCK_LEN, 371 | &col, 372 | ); 373 | 374 | localToSharedTransposed(&col, local_id); 375 | } 376 | 377 | sharedToBuffer1(block_id * BLOCK_LEN, local_id); 378 | } 379 | 380 | @compute @workgroup_size({{block_width}}) 381 | fn blockSort( 382 | @builtin(local_invocation_id) local_id_vec: vec3, 383 | @builtin(workgroup_id) workgroup_id_vec: vec3, 384 | @builtin(num_workgroups) num_workgroups_vec: vec3, 385 | ) { 386 | let local_id = local_id_vec.x; 387 | let num_blocks = num_workgroups_vec.x; 388 | 389 | for ( 390 | var block_id = workgroup_id_vec.x; 391 | block_id < config.len_in_blocks; 392 | block_id += num_blocks 393 | ) { 394 | blockSortBlock(block_id, local_id); 395 | } 396 | } 397 | 398 | @compute @workgroup_size({{block_width}}) 399 | fn findMergeOffsets( 400 | @builtin(global_invocation_id) global_id_vec: vec3, 401 | ) { 402 | if config.len_in_blocks < (config.n_way >> 1u) { 403 | return; 404 | } 405 | 406 | let block_id = global_id_vec.x; 407 | 408 | if block_id < config.len_in_blocks { 409 | if block_id == 0u { 410 | offsets[0] = 0u; 411 | } 412 | 413 | findMergeOffsetBlock(block_id + 1u); 414 | } 415 | } 416 | 417 | @compute @workgroup_size({{block_width}}) 418 | fn mergeBlocks( 419 | @builtin(local_invocation_id) local_id_vec: vec3, 420 | @builtin(workgroup_id) workgroup_id_vec: vec3, 421 | @builtin(num_workgroups) num_workgroups_vec: vec3, 422 | ) { 423 | if config.len_in_blocks < (config.n_way >> 1u) { 424 | return; 425 | } 426 | 427 | let local_id = local_id_vec.x; 428 | let num_blocks = num_workgroups_vec.x; 429 | 430 | for ( 431 | var block_id = workgroup_id_vec.x; 432 | block_id < config.len_in_blocks; 433 | block_id += num_blocks 434 | ) { 435 | mergeBlocksBlock(block_id, local_id); 436 | } 437 | } 438 | -------------------------------------------------------------------------------- /forma/src/gpu/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | mod conveyor_sort; 16 | mod painter; 17 | mod rasterizer; 18 | mod renderer; 19 | mod style_map; 20 | 21 | pub(crate) use style_map::StyleMap; 22 | 23 | pub use self::renderer::{Renderer, Timings}; 24 | 25 | pub(crate) struct TimeStamp<'qs> { 26 | query_set: &'qs wgpu::QuerySet, 27 | start_index: u32, 28 | end_index: u32, 29 | } 30 | pub(crate) trait GpuContext { 31 | type Data<'s>; 32 | 33 | fn init(device: &wgpu::Device) -> Self; 34 | fn encode( 35 | &self, 36 | device: &wgpu::Device, 37 | encoder: &mut wgpu::CommandEncoder, 38 | time_stamp: Option, 39 | data: &mut Self::Data<'_>, 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /forma/src/gpu/painter/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{borrow::Cow, mem, slice}; 16 | 17 | use bytemuck::{Pod, Zeroable}; 18 | use wgpu::util::DeviceExt; 19 | 20 | use crate::{consts, segment::SegmentBufferView}; 21 | 22 | use super::{GpuContext, TimeStamp}; 23 | 24 | #[derive(Debug)] 25 | pub struct Data<'s> { 26 | pub segment_buffer_view: &'s SegmentBufferView, 27 | pub pixel_segment_buffer: wgpu::Buffer, 28 | pub style_buffer: wgpu::Buffer, 29 | pub style_offset_buffer: wgpu::Buffer, 30 | pub atlas_texture: wgpu::TextureView, 31 | pub output_texture: wgpu::TextureView, 32 | pub width: u32, 33 | pub height: u32, 34 | pub clear_color: Color, 35 | } 36 | 37 | #[derive(Clone, Copy, Debug, Pod, Zeroable)] 38 | #[repr(C)] 39 | pub struct Style { 40 | pub fill_rule: u32, 41 | pub color: Color, 42 | pub blend_mode: u32, 43 | } 44 | 45 | #[derive(Clone, Copy, Debug, Pod, Zeroable)] 46 | #[repr(C)] 47 | pub struct Color { 48 | pub r: f32, 49 | pub g: f32, 50 | pub b: f32, 51 | pub a: f32, 52 | } 53 | 54 | #[derive(Copy, Clone)] 55 | #[repr(C)] 56 | struct Config { 57 | pub segments_len: u32, 58 | pub width: u32, 59 | pub height: u32, 60 | pub _padding: u32, 61 | pub clear_color: Color, 62 | } 63 | 64 | impl Config { 65 | pub fn as_byte_slice(&self) -> &[u8] { 66 | unsafe { slice::from_raw_parts((self as *const _) as *const u8, mem::size_of::()) } 67 | } 68 | } 69 | 70 | #[derive(Debug)] 71 | pub struct PaintContext { 72 | pub pipeline: wgpu::ComputePipeline, 73 | pub bind_group_layout: wgpu::BindGroupLayout, 74 | } 75 | 76 | impl GpuContext for PaintContext { 77 | type Data<'s> = Data<'s>; 78 | 79 | fn init(device: &wgpu::Device) -> Self { 80 | let module = device.create_shader_module(wgpu::ShaderModuleDescriptor { 81 | label: Some("paint.wgsl"), 82 | source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("paint.wgsl"))), 83 | }); 84 | 85 | let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { 86 | label: None, 87 | layout: None, 88 | module: &module, 89 | entry_point: "paint", 90 | }); 91 | 92 | let bind_group_layout = pipeline.get_bind_group_layout(0); 93 | 94 | Self { 95 | pipeline, 96 | bind_group_layout, 97 | } 98 | } 99 | 100 | fn encode( 101 | &self, 102 | device: &wgpu::Device, 103 | encoder: &mut wgpu::CommandEncoder, 104 | time_stamp: Option, 105 | data: &mut Data, 106 | ) { 107 | let config = Config { 108 | segments_len: data.segment_buffer_view.len() as _, 109 | width: data.width, 110 | height: data.height, 111 | clear_color: data.clear_color, 112 | _padding: 0, 113 | }; 114 | 115 | let config_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 116 | label: None, 117 | contents: config.as_byte_slice(), 118 | usage: wgpu::BufferUsages::UNIFORM, 119 | }); 120 | 121 | let sampler = device.create_sampler(&wgpu::SamplerDescriptor { 122 | address_mode_u: wgpu::AddressMode::ClampToEdge, 123 | address_mode_v: wgpu::AddressMode::ClampToEdge, 124 | address_mode_w: wgpu::AddressMode::ClampToEdge, 125 | mag_filter: wgpu::FilterMode::Linear, 126 | min_filter: wgpu::FilterMode::Nearest, 127 | mipmap_filter: wgpu::FilterMode::Nearest, 128 | ..Default::default() 129 | }); 130 | 131 | let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { 132 | label: None, 133 | layout: &self.bind_group_layout, 134 | entries: &[ 135 | wgpu::BindGroupEntry { 136 | binding: 0, 137 | resource: config_buffer.as_entire_binding(), 138 | }, 139 | wgpu::BindGroupEntry { 140 | binding: 1, 141 | resource: data.pixel_segment_buffer.as_entire_binding(), 142 | }, 143 | wgpu::BindGroupEntry { 144 | binding: 2, 145 | resource: data.style_offset_buffer.as_entire_binding(), 146 | }, 147 | wgpu::BindGroupEntry { 148 | binding: 3, 149 | resource: data.style_buffer.as_entire_binding(), 150 | }, 151 | wgpu::BindGroupEntry { 152 | binding: 4, 153 | resource: wgpu::BindingResource::TextureView(&data.atlas_texture), 154 | }, 155 | wgpu::BindGroupEntry { 156 | binding: 5, 157 | resource: wgpu::BindingResource::Sampler(&sampler), 158 | }, 159 | wgpu::BindGroupEntry { 160 | binding: 6, 161 | resource: wgpu::BindingResource::TextureView(&data.output_texture), 162 | }, 163 | ], 164 | }); 165 | 166 | if let Some(TimeStamp { 167 | query_set, 168 | start_index, 169 | .. 170 | }) = time_stamp 171 | { 172 | encoder.write_timestamp(query_set, start_index); 173 | } 174 | 175 | { 176 | let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { label: None }); 177 | pass.set_pipeline(&self.pipeline); 178 | pass.set_bind_group(0, &bind_group, &[]); 179 | 180 | pass.dispatch_workgroups( 181 | ((data.height as usize + consts::gpu::TILE_HEIGHT - 1) / consts::gpu::TILE_HEIGHT) 182 | as _, 183 | 1, 184 | 1, 185 | ); 186 | } 187 | 188 | if let Some(TimeStamp { 189 | query_set, 190 | end_index, 191 | .. 192 | }) = time_stamp 193 | { 194 | encoder.write_timestamp(query_set, end_index); 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /forma/src/gpu/renderer/draw_texture.wgsl: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | struct VertexOutput { 16 | @builtin(position) pos: vec4, 17 | @location(0) uv: vec2, 18 | } 19 | 20 | @vertex 21 | fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { 22 | var vertex_output: VertexOutput; 23 | 24 | vertex_output.uv = vec2( 25 | f32((i32(vertex_index) << 1u) & 2), 26 | f32(i32(vertex_index) & 2), 27 | ); 28 | 29 | let pos = 2.0 * vertex_output.uv - vec2(1.0, 1.0); 30 | vertex_output.pos = vec4(pos.x, pos.y, 0.0, 1.0); 31 | 32 | vertex_output.uv.y = 1.0 - vertex_output.uv.y; 33 | 34 | return vertex_output; 35 | } 36 | 37 | @group(0) @binding(0) var image: texture_2d; 38 | @group(0) @binding(1) var smp: sampler; 39 | 40 | @fragment 41 | fn fs_main(@location(0) uv: vec2) -> @location(0) vec4 { 42 | return textureSample(image, smp, uv); 43 | } 44 | -------------------------------------------------------------------------------- /forma/src/gpu/style_map.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{ 16 | collections::{HashMap, HashSet}, 17 | fmt, 18 | }; 19 | 20 | use etagere::{AllocId, Allocation, AtlasAllocator}; 21 | use rustc_hash::FxHashMap; 22 | 23 | use crate::{ 24 | styling::{Fill, Func, GradientType, Image, ImageId, Props}, 25 | utils::Order, 26 | Layer, 27 | }; 28 | 29 | const ATLAS_SIZE: i32 = 4_096; 30 | 31 | #[derive(Debug, Default)] 32 | struct PropHeader { 33 | func: u32, 34 | is_clipped: u32, 35 | fill_rule: u32, 36 | fill_type: u32, 37 | blend_mode: u32, 38 | gradient_stop_count: u32, 39 | } 40 | 41 | impl PropHeader { 42 | fn to_bits(&self) -> u32 { 43 | fn prepend(current: u32, value: u32) -> u32 { 44 | #[cfg(test)] 45 | { 46 | assert!( 47 | (value >> B) == 0, 48 | "Unable to store {} with {} bits", 49 | value, 50 | B 51 | ); 52 | assert!( 53 | (current >> (u32::BITS - B)) == 0, 54 | "Prepending {} bit would drop the mos significan bits of {}", 55 | B, 56 | value 57 | ); 58 | } 59 | (current << B) + value 60 | } 61 | let header = 0; 62 | let header = prepend::<1>(header, self.func); 63 | let header = prepend::<1>(header, self.is_clipped); 64 | let header = prepend::<1>(header, self.fill_rule); 65 | let header = prepend::<2>(header, self.fill_type); 66 | let header = prepend::<4>(header, self.blend_mode); 67 | 68 | prepend::<16>(header, self.gradient_stop_count) 69 | } 70 | } 71 | 72 | struct ImageAllocator { 73 | allocator: AtlasAllocator, 74 | image_to_alloc: HashMap, 75 | unused_allocs: HashSet, 76 | new_allocs: Vec<(Image, [u32; 4])>, 77 | } 78 | 79 | impl ImageAllocator { 80 | fn new() -> ImageAllocator { 81 | ImageAllocator { 82 | allocator: AtlasAllocator::new(etagere::size2(ATLAS_SIZE, ATLAS_SIZE)), 83 | image_to_alloc: HashMap::new(), 84 | unused_allocs: HashSet::new(), 85 | new_allocs: Vec::new(), 86 | } 87 | } 88 | 89 | fn start_populate(&mut self) { 90 | self.new_allocs.clear(); 91 | self.unused_allocs = self.allocator.iter().map(|alloc| alloc.id).collect(); 92 | } 93 | 94 | fn end_populate(&mut self) { 95 | for alloc_id in &self.unused_allocs { 96 | self.allocator.deallocate(*alloc_id); 97 | } 98 | } 99 | 100 | fn get_or_create_alloc(&mut self, image: &Image) -> &Allocation { 101 | let new_allocs = &mut self.new_allocs; 102 | let allocator = &mut self.allocator; 103 | 104 | let allocation = self.image_to_alloc.entry(image.id()).or_insert_with(|| { 105 | let allocation = allocator 106 | .allocate(etagere::size2(image.width() as i32, image.height() as i32)) 107 | .expect("Texture does not fit in the atlas"); 108 | 109 | let rect = allocation.rectangle.to_u32(); 110 | new_allocs.push(( 111 | image.clone(), 112 | [ 113 | rect.min.x, 114 | rect.min.y, 115 | rect.min.x + image.width(), 116 | rect.min.y + image.height(), 117 | ], 118 | )); 119 | allocation 120 | }); 121 | 122 | self.unused_allocs.remove(&allocation.id); 123 | 124 | allocation 125 | } 126 | } 127 | 128 | impl fmt::Debug for ImageAllocator { 129 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 130 | f.debug_struct("ImageAllocator") 131 | .field("allocator", &"{}") 132 | .field("image_to_alloc", &self.image_to_alloc) 133 | .field("unused_allocs", &self.unused_allocs) 134 | .field("new_allocs", &self.new_allocs) 135 | .finish() 136 | } 137 | } 138 | 139 | #[derive(Debug)] 140 | pub struct StyleMap { 141 | // Position of the style in the u32 buffer, by order value. 142 | style_offsets: Vec, 143 | styles: Vec, 144 | image_allocator: ImageAllocator, 145 | } 146 | 147 | impl StyleMap { 148 | pub fn new() -> StyleMap { 149 | StyleMap { 150 | style_offsets: Vec::new(), 151 | styles: Vec::new(), 152 | image_allocator: ImageAllocator::new(), 153 | } 154 | } 155 | 156 | pub fn push(&mut self, props: &Props) { 157 | let style = match &props.func { 158 | Func::Clip(order) => { 159 | self.styles.push( 160 | PropHeader { 161 | func: 1, 162 | fill_rule: props.fill_rule as u32, 163 | ..Default::default() 164 | } 165 | .to_bits(), 166 | ); 167 | self.styles.push(*order as u32); 168 | return; 169 | } 170 | Func::Draw(style) => style, 171 | }; 172 | self.styles.push( 173 | PropHeader { 174 | func: 0, 175 | fill_rule: props.fill_rule as u32, 176 | is_clipped: u32::from(style.is_clipped), 177 | fill_type: match &style.fill { 178 | Fill::Solid(_) => 0, 179 | Fill::Gradient(gradient) => match gradient.r#type() { 180 | GradientType::Linear => 1, 181 | GradientType::Radial => 2, 182 | }, 183 | Fill::Texture(_) => 3, 184 | }, 185 | blend_mode: style.blend_mode as u32, 186 | gradient_stop_count: match &style.fill { 187 | Fill::Gradient(gradient) => gradient.colors_with_stops().len() as u32, 188 | _ => 0, 189 | }, 190 | } 191 | .to_bits(), 192 | ); 193 | 194 | match &style.fill { 195 | Fill::Solid(color) => self.styles.extend(color.to_array().map(f32::to_bits)), 196 | Fill::Gradient(gradient) => { 197 | self.styles 198 | .extend(gradient.start().to_array().map(f32::to_bits)); 199 | self.styles 200 | .extend(gradient.end().to_array().map(f32::to_bits)); 201 | gradient 202 | .colors_with_stops() 203 | .iter() 204 | .for_each(|(color, stop)| { 205 | self.styles.extend(color.to_array().map(f32::to_bits)); 206 | self.styles.push(stop.to_bits()); 207 | }); 208 | } 209 | Fill::Texture(texture) => { 210 | self.styles 211 | .extend(texture.transform.to_array().map(f32::to_bits)); 212 | 213 | let image = &texture.image; 214 | let alloc = self.image_allocator.get_or_create_alloc(image); 215 | 216 | let min = alloc.rectangle.min.cast::(); 217 | self.styles.extend( 218 | [ 219 | min.x, 220 | min.y, 221 | min.x + image.width() as f32, 222 | min.y + image.height() as f32, 223 | ] 224 | .map(f32::to_bits), 225 | ); 226 | } 227 | } 228 | } 229 | 230 | pub fn populate(&mut self, layers: &FxHashMap) { 231 | self.style_offsets.clear(); 232 | self.styles.clear(); 233 | self.image_allocator.start_populate(); 234 | 235 | let mut props_set = FxHashMap::default(); 236 | 237 | for (order, layer) in layers.iter() { 238 | let order = order.as_u32(); 239 | let props = layer.props(); 240 | 241 | if self.style_offsets.len() <= order as usize { 242 | self.style_offsets.resize(order as usize + 1, 0); 243 | } 244 | 245 | let offset = *props_set.entry(props).or_insert_with(|| { 246 | let offset = self.styles.len() as u32; 247 | self.push(props); 248 | offset 249 | }); 250 | 251 | self.style_offsets[order as usize] = offset; 252 | } 253 | 254 | self.image_allocator.end_populate(); 255 | } 256 | 257 | pub fn style_offsets(&self) -> &[u32] { 258 | &self.style_offsets 259 | } 260 | 261 | pub fn styles(&self) -> &[u32] { 262 | &self.styles 263 | } 264 | 265 | pub fn new_allocs(&self) -> &[(Image, [u32; 4])] { 266 | self.image_allocator.new_allocs.as_ref() 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /forma/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #![doc(test(attr(deny(warnings))))] 16 | 17 | //! forma is a high-performance vector-graphics renderer with a CPU & GPU back-end. 18 | //! 19 | //! ## Example 20 | //! 21 | //! The following example renders two overlapping rectangles and highlights the most common API 22 | //! usage: 23 | //! 24 | //! ``` 25 | //! # use forma_render as forma; 26 | //! use forma::{cpu::{buffer::{BufferBuilder, layout::LinearLayout}, Renderer, RGBA}, prelude::*}; 27 | //! 28 | //! fn rect(x: f32, y: f32, width: f32, height: f32) -> Path { 29 | //! PathBuilder::new() 30 | //! .move_to(Point::new(x, y)) 31 | //! .line_to(Point::new(x + width, y)) 32 | //! .line_to(Point::new(x + width, y + height)) 33 | //! .line_to(Point::new(x, y + height)) 34 | //! .build() 35 | //! } 36 | //! 37 | //! fn solid(r: f32, g: f32, b: f32) -> Props { 38 | //! Props { 39 | //! func: Func::Draw(Style { 40 | //! fill: Fill::Solid(Color { r, g, b, a: 1.0 }), 41 | //! ..Default::default() 42 | //! }), 43 | //! ..Default::default() 44 | //! } 45 | //! } 46 | //! 47 | //! // The composition is akin to a `HashMap`. Layers can be inserted and 48 | //! // removed from the composition by their orders. 49 | //! let mut composition = Composition::new(); 50 | //! let mut renderer = Renderer::new(); 51 | //! 52 | //! // The layer cache enables updating only tiles that changed from last frame. 53 | //! let layer_cache = renderer.create_buffer_layer_cache().unwrap(); 54 | //! 55 | //! composition 56 | //! .get_mut_or_insert_default(Order::new(0).unwrap()) 57 | //! .insert(&rect(50.0, 50.0, 100.0, 50.0)) 58 | //! .set_props(solid(1.0, 0.0, 0.0)); 59 | //! 60 | //! composition 61 | //! .get_mut_or_insert_default(Order::new(1).unwrap()) 62 | //! .insert(&rect(100.0, 50.0, 100.0, 50.0)) 63 | //! .set_props(solid(0.0, 0.0, 1.0)); 64 | //! 65 | //! let width = 250; 66 | //! let height = 150; 67 | //! let mut buffer = vec![0; width * height * 4]; // 4 bytes per pixel. 68 | //! 69 | //! renderer.render( 70 | //! &mut composition, 71 | //! // Stride is width * 4 bytes per pixel. 72 | //! &mut BufferBuilder::new(&mut buffer, &mut LinearLayout::new(width, width * 4, height)) 73 | //! .layer_cache(layer_cache.clone()) 74 | //! .build(), 75 | //! RGBA, 76 | //! Color { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }, 77 | //! None, 78 | //! ); 79 | //! 80 | //! // Background is white. 81 | //! assert_eq!(buffer.chunks(4).next().unwrap(), [255, 255, 255, 255]); 82 | //! 83 | //! // First rectangle is red. 84 | //! let index = 75 + 75 * width; 85 | //! assert_eq!(buffer.chunks(4).nth(index).unwrap(), [255, 0, 0, 255]); 86 | //! 87 | //! // Overlap is blue. 88 | //! let index = 125 + 75 * width; 89 | //! assert_eq!(buffer.chunks(4).nth(index).unwrap(), [0, 0, 255, 255]); 90 | //! 91 | //! // Second rectangle is blue. 92 | //! let index = 175 + 75 * width; 93 | //! assert_eq!(buffer.chunks(4).nth(index).unwrap(), [0, 0, 255, 255]); 94 | //! ``` 95 | //! 96 | //! ## Reusing the composition 97 | //! 98 | //! For best possible performance, reusing the composition is essential. This may mean that in some 99 | //! cases one might have to remove, then re-insert some layers around in order to achieve the 100 | //! desired ordering of layers. 101 | //! 102 | //! For simple cases, [Layer::set_is_enabled] can provide an alternative to removing and 103 | //! re-inserting layers into the composition. 104 | 105 | #[cfg(target_os = "fuchsia")] 106 | macro_rules! duration { 107 | ($category:expr, $name:expr $(, $key:expr => $val:expr)*) => { 108 | fuchsia_trace::duration!($category, $name $(, $key => $val)*) 109 | } 110 | } 111 | 112 | #[cfg(not(target_os = "fuchsia"))] 113 | macro_rules! duration { 114 | ($category:expr, $name:expr $(, $key:expr => $val:expr)*) => {}; 115 | } 116 | 117 | mod composition; 118 | pub mod consts; 119 | pub mod cpu; 120 | #[cfg(feature = "gpu")] 121 | pub mod gpu; 122 | pub mod math; 123 | mod path; 124 | mod segment; 125 | pub mod styling; 126 | mod utils; 127 | 128 | pub(crate) use self::segment::{SegmentBuffer, SegmentBufferView}; 129 | 130 | pub use self::{ 131 | composition::{Composition, Layer}, 132 | path::{Path, PathBuilder}, 133 | segment::GeomId, 134 | utils::{Order, OrderError}, 135 | }; 136 | 137 | pub mod prelude { 138 | pub use crate::cpu::{ 139 | buffer::{ 140 | layout::{Layout, LinearLayout}, 141 | BufferBuilder, BufferLayerCache, 142 | }, 143 | Channel, BGR0, BGR1, BGRA, RGB0, RGB1, RGBA, 144 | }; 145 | pub use crate::gpu::Timings; 146 | pub use crate::{ 147 | math::{AffineTransform, GeomPresTransform, Point}, 148 | styling::{ 149 | BlendMode, Color, Fill, FillRule, Func, Gradient, GradientBuilder, GradientType, Image, 150 | Props, Style, Texture, 151 | }, 152 | Composition, Layer, Order, Path, PathBuilder, 153 | }; 154 | } 155 | -------------------------------------------------------------------------------- /forma/src/math/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | mod point; 16 | mod transform; 17 | 18 | pub use self::{ 19 | point::Point, 20 | transform::{AffineTransform, GeomPresTransform, GeomPresTransformError}, 21 | }; 22 | -------------------------------------------------------------------------------- /forma/src/math/point.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{ 16 | f32, hash, 17 | ops::{Add, Div, Mul, Sub}, 18 | }; 19 | 20 | use crate::utils::CanonBits; 21 | 22 | #[derive(Clone, Copy, Debug)] 23 | pub struct Point { 24 | pub x: f32, 25 | pub y: f32, 26 | } 27 | 28 | impl Eq for Point {} 29 | 30 | impl PartialEq for Point { 31 | fn eq(&self, other: &Self) -> bool { 32 | self.x == other.x && self.y == other.y 33 | } 34 | } 35 | 36 | impl hash::Hash for Point { 37 | fn hash(&self, state: &mut H) { 38 | self.x.to_canon_bits().hash(state); 39 | self.y.to_canon_bits().hash(state); 40 | } 41 | } 42 | 43 | impl Point { 44 | pub fn new(x: f32, y: f32) -> Self { 45 | Self { x, y } 46 | } 47 | 48 | pub(crate) fn to_array(self) -> [f32; 2] { 49 | [self.x, self.y] 50 | } 51 | } 52 | 53 | #[allow(clippy::many_single_char_names)] 54 | fn approx_atan2(y: f32, x: f32) -> f32 { 55 | let x_abs = x.abs(); 56 | let y_abs = y.abs(); 57 | 58 | let a = x_abs.min(y_abs) / x_abs.max(y_abs); 59 | let s = a * a; 60 | let mut r = s 61 | .mul_add(-0.046_496_473, 0.159_314_22) 62 | .mul_add(s, -0.327_622_77) 63 | .mul_add(s * a, a); 64 | 65 | if y_abs > x_abs { 66 | r = f32::consts::FRAC_PI_2 - r; 67 | } 68 | 69 | if x < 0.0 { 70 | r = f32::consts::PI - r; 71 | } 72 | 73 | if y < 0.0 { 74 | r = -r; 75 | } 76 | 77 | r 78 | } 79 | 80 | // Restrict the Point functions visibility as we do not want to run into the business of delivering 81 | // a linear algebra package. 82 | impl Point { 83 | pub(crate) fn len(self) -> f32 { 84 | (self.x * self.x + self.y * self.y).sqrt() 85 | } 86 | 87 | pub(crate) fn angle(self) -> Option { 88 | (self.len() >= f32::EPSILON).then(|| approx_atan2(self.y, self.x)) 89 | } 90 | } 91 | 92 | impl Add for Point { 93 | type Output = Self; 94 | 95 | #[inline] 96 | fn add(self, other: Self) -> Self { 97 | Self { 98 | x: self.x + other.x, 99 | y: self.y + other.y, 100 | } 101 | } 102 | } 103 | 104 | impl Sub for Point { 105 | type Output = Self; 106 | 107 | #[inline] 108 | fn sub(self, other: Self) -> Self { 109 | Self { 110 | x: self.x - other.x, 111 | y: self.y - other.y, 112 | } 113 | } 114 | } 115 | 116 | impl Mul for Point { 117 | type Output = Self; 118 | 119 | #[inline] 120 | fn mul(self, other: f32) -> Self { 121 | Self { 122 | x: self.x * other, 123 | y: self.y * other, 124 | } 125 | } 126 | } 127 | 128 | impl Div for Point { 129 | type Output = Self; 130 | 131 | #[inline] 132 | fn div(self, other: f32) -> Self { 133 | Self { 134 | x: self.x / other, 135 | y: self.y / other, 136 | } 137 | } 138 | } 139 | 140 | #[cfg(test)] 141 | mod tests { 142 | use super::*; 143 | use std::f32::consts::{FRAC_PI_2, FRAC_PI_4, PI}; 144 | 145 | #[test] 146 | fn add() { 147 | assert_eq!( 148 | Point { x: 32.0, y: 126.0 }, 149 | Point { x: 13.0, y: 27.0 } + Point { x: 19.0, y: 99.0 } 150 | ); 151 | } 152 | 153 | #[test] 154 | fn sub() { 155 | assert_eq!( 156 | Point { x: -6.0, y: -72.0 }, 157 | Point { x: 13.0, y: 27.0 } - Point { x: 19.0, y: 99.0 } 158 | ); 159 | } 160 | 161 | #[test] 162 | fn mul() { 163 | assert_eq!(Point { x: 3.0, y: 21.0 }, Point { x: 1.0, y: 7.0 } * 3.0); 164 | } 165 | 166 | #[test] 167 | fn div() { 168 | assert_eq!(Point { x: 1.0, y: 7.0 }, Point { x: 3.0, y: 21.0 } / 3.0); 169 | } 170 | 171 | #[test] 172 | fn angle() { 173 | assert_eq!(Some(0.0), Point { x: 1.0, y: 0.0 }.angle()); 174 | assert_eq!(Some(0.0), Point { x: 1e10, y: 0.0 }.angle()); 175 | assert_eq!(Some(PI), Point { x: -1.0, y: 0.0 }.angle()); 176 | assert_eq!(Some(FRAC_PI_2), Point { x: 0.0, y: 1.0 }.angle()); 177 | assert_eq!(Some(-FRAC_PI_2), Point { x: 0.0, y: -1.0 }.angle()); 178 | assert!((FRAC_PI_4 - Point { x: 1.0, y: 1.0 }.angle().unwrap()).abs() < 1e-3); 179 | assert!((-FRAC_PI_4 - Point { x: 1.0, y: -1.0 }.angle().unwrap()).abs() < 1e-3); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /forma/src/math/transform.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{convert::TryFrom, error::Error, fmt, hash}; 16 | 17 | use crate::{consts, math::Point, path::MAX_ERROR, utils::CanonBits}; 18 | 19 | const MAX_SCALING_FACTOR_X: f32 = 1.0 + MAX_ERROR / consts::MAX_WIDTH as f32; 20 | const MAX_SCALING_FACTOR_Y: f32 = 1.0 + MAX_ERROR / consts::MAX_HEIGHT as f32; 21 | 22 | /// 2D transformation that preserves parallel lines. 23 | /// 24 | /// Such a transformation can combine translation, scale, flip, rotate and shears. 25 | /// It is represented by a 3 by 3 matrix where the last row is [0, 0, 1]. 26 | /// 27 | /// ```text 28 | /// [ x' ] [ u.x v.x t.x ] [ x ] 29 | /// [ y' ] = [ u.y v.y t.y ] [ y ] 30 | /// [ 1 ] [ 0 0 1 ] [ 1 ] 31 | /// ``` 32 | #[derive(Copy, Clone, Debug)] 33 | pub struct AffineTransform { 34 | pub ux: f32, 35 | pub uy: f32, 36 | pub vx: f32, 37 | pub vy: f32, 38 | pub tx: f32, 39 | pub ty: f32, 40 | } 41 | 42 | impl AffineTransform { 43 | pub(crate) fn transform(&self, point: Point) -> Point { 44 | Point { 45 | x: self.ux.mul_add(point.x, self.vx.mul_add(point.y, self.tx)), 46 | y: self.uy.mul_add(point.x, self.vy.mul_add(point.y, self.ty)), 47 | } 48 | } 49 | 50 | pub(crate) fn is_identity(&self) -> bool { 51 | *self == Self::default() 52 | } 53 | 54 | pub fn to_array(&self) -> [f32; 6] { 55 | [self.ux, self.uy, self.vx, self.vy, self.tx, self.ty] 56 | } 57 | } 58 | 59 | impl Eq for AffineTransform {} 60 | 61 | impl PartialEq for AffineTransform { 62 | fn eq(&self, other: &Self) -> bool { 63 | self.ux == other.ux 64 | && self.uy == other.uy 65 | && self.vx == other.vx 66 | && self.vy == other.vy 67 | && self.tx == other.tx 68 | && self.ty == other.ty 69 | } 70 | } 71 | 72 | impl hash::Hash for AffineTransform { 73 | fn hash(&self, state: &mut H) { 74 | self.ux.to_canon_bits().hash(state); 75 | self.uy.to_canon_bits().hash(state); 76 | self.vx.to_canon_bits().hash(state); 77 | self.vy.to_canon_bits().hash(state); 78 | self.tx.to_canon_bits().hash(state); 79 | self.ty.to_canon_bits().hash(state); 80 | } 81 | } 82 | 83 | impl Default for AffineTransform { 84 | fn default() -> Self { 85 | Self { 86 | ux: 1.0, 87 | vx: 0.0, 88 | tx: 0.0, 89 | uy: 0.0, 90 | vy: 1.0, 91 | ty: 0.0, 92 | } 93 | } 94 | } 95 | 96 | impl From<[f32; 6]> for AffineTransform { 97 | fn from(transform: [f32; 6]) -> Self { 98 | Self { 99 | ux: transform[0], 100 | uy: transform[2], 101 | vx: transform[1], 102 | vy: transform[3], 103 | tx: transform[4], 104 | ty: transform[5], 105 | } 106 | } 107 | } 108 | 109 | #[derive(Debug, Eq, PartialEq)] 110 | pub enum GeomPresTransformError { 111 | ExceededScalingFactor { x: bool, y: bool }, 112 | } 113 | 114 | impl fmt::Display for GeomPresTransformError { 115 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 116 | match self { 117 | GeomPresTransformError::ExceededScalingFactor { x: true, y: false } => { 118 | write!(f, "exceeded scaling factor on the X axis (-1.0 to 1.0)") 119 | } 120 | GeomPresTransformError::ExceededScalingFactor { x: false, y: true } => { 121 | write!(f, "exceeded scaling factor on the Y axis (-1.0 to 1.0)") 122 | } 123 | GeomPresTransformError::ExceededScalingFactor { x: true, y: true } => { 124 | write!(f, "exceeded scaling factor on both axis (-1.0 to 1.0)") 125 | } 126 | _ => panic!("cannot display invalid GeomPresTransformError"), 127 | } 128 | } 129 | } 130 | 131 | impl Error for GeomPresTransformError {} 132 | 133 | /// An affine transform that does not scale up. 134 | /// 135 | /// For less constrained transforms, use [`Path::transform`] instead. 136 | /// 137 | /// [`Path::transform`]: crate::Path::transform 138 | /// 139 | /// # Examples 140 | /// 141 | /// ``` 142 | /// # use forma_render::prelude::*; 143 | /// let translate_x_1 = [1.0, 0.0, 0.0, 1.0, 1.0, 0.0]; 144 | /// let scale_y_2 = [1.0, 0.0, 0.0, 2.0, 0.0, 0.0]; 145 | /// let flip_scale_x_2 = [-2.0, 0.0, 0.0, 1.0, 0.0, 0.0]; 146 | /// 147 | /// assert!(GeomPresTransform::try_from(AffineTransform::from(translate_x_1)).is_ok()); 148 | /// assert!(GeomPresTransform::try_from(AffineTransform::from(scale_y_2)).is_err()); 149 | /// assert!(GeomPresTransform::try_from(AffineTransform::from(flip_scale_x_2)).is_err()); 150 | /// ``` 151 | #[derive(Default, Clone, Copy, Debug, Eq, PartialEq)] 152 | pub struct GeomPresTransform(pub(crate) AffineTransform); 153 | 154 | impl GeomPresTransform { 155 | /// ```text 156 | /// [ x' ] [ t.0 t.1 t.4 ] [ x ] 157 | /// [ y' ] = [ t.2 t.3 t.5 ] [ y ] 158 | /// [ 1 ] [ 0 0 1 ] [ 1 ] 159 | /// ``` 160 | #[inline] 161 | pub fn new(mut transform: [f32; 9]) -> Option { 162 | (transform[6].abs() <= f32::EPSILON && transform[7].abs() <= f32::EPSILON) 163 | .then(|| { 164 | if (transform[8] - 1.0).abs() > f32::EPSILON { 165 | let recip = transform[8].recip(); 166 | for val in &mut transform[..6] { 167 | *val *= recip; 168 | } 169 | } 170 | 171 | Self::try_from(AffineTransform { 172 | ux: transform[0], 173 | vx: transform[1], 174 | uy: transform[3], 175 | vy: transform[4], 176 | tx: transform[2], 177 | ty: transform[5], 178 | }) 179 | .ok() 180 | }) 181 | .flatten() 182 | } 183 | 184 | #[inline] 185 | pub fn is_identity(&self) -> bool { 186 | self.0.is_identity() 187 | } 188 | 189 | pub(crate) fn transform(&self, point: Point) -> Point { 190 | self.0.transform(point) 191 | } 192 | 193 | #[inline] 194 | pub fn to_array(&self) -> [f32; 6] { 195 | [ 196 | self.0.ux, self.0.vx, self.0.uy, self.0.vy, self.0.tx, self.0.ty, 197 | ] 198 | } 199 | } 200 | 201 | impl TryFrom<[f32; 6]> for GeomPresTransform { 202 | type Error = GeomPresTransformError; 203 | fn try_from(transform: [f32; 6]) -> Result { 204 | GeomPresTransform::try_from(AffineTransform::from(transform)) 205 | } 206 | } 207 | 208 | impl TryFrom for GeomPresTransform { 209 | type Error = GeomPresTransformError; 210 | 211 | fn try_from(t: AffineTransform) -> Result { 212 | let scales_up_x = t.ux * t.ux + t.uy * t.uy > MAX_SCALING_FACTOR_X; 213 | let scales_up_y = t.vx * t.vx + t.vy * t.vy > MAX_SCALING_FACTOR_Y; 214 | 215 | (!scales_up_x && !scales_up_y).then_some(Self(t)).ok_or( 216 | GeomPresTransformError::ExceededScalingFactor { 217 | x: scales_up_x, 218 | y: scales_up_y, 219 | }, 220 | ) 221 | } 222 | } 223 | 224 | #[cfg(test)] 225 | mod tests { 226 | use super::*; 227 | 228 | #[test] 229 | fn default_identity() { 230 | let transform = GeomPresTransform::default(); 231 | 232 | assert_eq!( 233 | transform.transform(Point::new(2.0, 3.0)), 234 | Point::new(2.0, 3.0) 235 | ); 236 | } 237 | 238 | #[test] 239 | fn as_slice() { 240 | let slice = [0.1, 0.5, 0.4, 0.3, 0.7, 0.9]; 241 | 242 | assert_eq!( 243 | slice, 244 | GeomPresTransform::try_from(slice).unwrap().to_array() 245 | ); 246 | } 247 | 248 | #[test] 249 | fn scale_translate() { 250 | let transform = GeomPresTransform::try_from([0.1, 0.5, 0.4, 0.3, 0.5, 0.6]).unwrap(); 251 | 252 | assert_eq!( 253 | transform.transform(Point::new(2.0, 3.0)), 254 | Point::new(2.2, 2.3) 255 | ); 256 | } 257 | 258 | #[test] 259 | fn wrong_scaling_factor() { 260 | let transform = [ 261 | 0.1, 262 | MAX_SCALING_FACTOR_Y.sqrt(), 263 | MAX_SCALING_FACTOR_X.sqrt(), 264 | 0.1, 265 | 0.5, 266 | 0.0, 267 | ]; 268 | 269 | assert_eq!( 270 | GeomPresTransform::try_from(transform), 271 | Err(GeomPresTransformError::ExceededScalingFactor { x: true, y: true }) 272 | ); 273 | } 274 | 275 | #[test] 276 | fn wrong_scaling_factor_x() { 277 | let transform = [0.1, 0.0, MAX_SCALING_FACTOR_X.sqrt(), 0.0, 0.5, 0.0]; 278 | 279 | assert_eq!( 280 | GeomPresTransform::try_from(transform), 281 | Err(GeomPresTransformError::ExceededScalingFactor { x: true, y: false }) 282 | ); 283 | } 284 | 285 | #[test] 286 | fn wrong_scaling_factor_y() { 287 | let transform = [0.0, MAX_SCALING_FACTOR_Y.sqrt(), 0.0, 0.1, 0.5, 0.0]; 288 | 289 | assert_eq!( 290 | GeomPresTransform::try_from(transform), 291 | Err(GeomPresTransformError::ExceededScalingFactor { x: false, y: true }) 292 | ); 293 | } 294 | 295 | #[test] 296 | fn correct_scaling_factor() { 297 | let transform = [1.0, MAX_SCALING_FACTOR_Y.sqrt(), 0.0, 0.0, 0.5, 0.0]; 298 | 299 | assert_eq!( 300 | transform, 301 | GeomPresTransform::try_from(transform).unwrap().to_array() 302 | ); 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /forma/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | mod extend; 16 | mod order; 17 | mod prefix_scan; 18 | pub mod simd; 19 | mod small_bit_set; 20 | 21 | pub use self::{ 22 | extend::{ExtendTuple10, ExtendTuple3, ExtendVec}, 23 | order::{Order, OrderError}, 24 | prefix_scan::{PrefixScanIter, PrefixScanParIter}, 25 | small_bit_set::SmallBitSet, 26 | }; 27 | 28 | pub trait CanonBits { 29 | fn to_canon_bits(self) -> u32; 30 | } 31 | 32 | impl CanonBits for f32 { 33 | fn to_canon_bits(self) -> u32 { 34 | if self.is_nan() { 35 | return f32::NAN.to_bits(); 36 | } 37 | 38 | if self == 0.0 { 39 | return 0.0f32.to_bits(); 40 | } 41 | 42 | self.to_bits() 43 | } 44 | } 45 | 46 | pub trait DivCeil { 47 | fn div_ceil_(self, other: Self) -> Self; 48 | } 49 | 50 | impl DivCeil for u32 { 51 | fn div_ceil_(self, other: Self) -> Self { 52 | (self + other - 1) / other 53 | } 54 | } 55 | 56 | impl DivCeil for usize { 57 | fn div_ceil_(self, other: Self) -> Self { 58 | (self + other - 1) / other 59 | } 60 | } 61 | 62 | #[cfg(test)] 63 | mod tests { 64 | use super::*; 65 | 66 | #[test] 67 | fn f32_canon_bits_nan() { 68 | let nan0 = f32::NAN; 69 | let nan1 = nan0 + 1.0; 70 | 71 | assert_ne!(nan0, nan1); 72 | assert_eq!(nan0.to_canon_bits(), nan1.to_canon_bits()); 73 | } 74 | 75 | #[test] 76 | fn f32_canon_bits_zero() { 77 | let neg_zero = -0.0f32; 78 | let pos_zero = 0.0; 79 | 80 | assert_eq!(neg_zero, pos_zero); 81 | assert_ne!(neg_zero.to_bits(), pos_zero.to_bits()); 82 | assert_eq!(neg_zero.to_canon_bits(), pos_zero.to_canon_bits()); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /forma/src/utils/order.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::{convert::TryFrom, error::Error, fmt, num::NonZeroU32}; 16 | 17 | use crate::consts; 18 | 19 | #[derive(Debug, Eq, PartialEq)] 20 | pub enum OrderError { 21 | ExceededLayerLimit, 22 | } 23 | 24 | impl fmt::Display for OrderError { 25 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 26 | write!(f, "exceeded layer limit ({})", consts::LAYER_LIMIT) 27 | } 28 | } 29 | 30 | impl Error for OrderError {} 31 | 32 | #[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)] 33 | pub struct Order(NonZeroU32); 34 | 35 | impl Default for Order { 36 | fn default() -> Self { 37 | match Self::new(0) { 38 | Ok(order) => order, 39 | Err(_) => panic!("0 is smaller than Order::MAX"), 40 | } 41 | } 42 | } 43 | 44 | impl Order { 45 | pub const MAX: Self = Self( 46 | match NonZeroU32::new(consts::LAYER_LIMIT as u32 ^ u32::MAX) { 47 | Some(val) => val, 48 | None => panic!("LAYER_LIMIT is smaller than u32::MAX"), 49 | }, 50 | ); 51 | 52 | pub const fn as_u32(&self) -> u32 { 53 | self.0.get() ^ u32::MAX 54 | } 55 | 56 | pub const fn new(order: u32) -> Result { 57 | if order > Self::MAX.as_u32() { 58 | Err(OrderError::ExceededLayerLimit) 59 | } else { 60 | Ok(Self(match NonZeroU32::new(order ^ u32::MAX) { 61 | Some(val) => val, 62 | None => panic!("Order::MAX is smaller than u32::MAX"), 63 | })) 64 | } 65 | } 66 | } 67 | 68 | impl fmt::Debug for Order { 69 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 70 | f.debug_tuple("Order").field(&self.as_u32()).finish() 71 | } 72 | } 73 | 74 | impl From for u32 { 75 | fn from(order: Order) -> Self { 76 | order.as_u32() 77 | } 78 | } 79 | 80 | impl TryFrom for Order { 81 | type Error = OrderError; 82 | 83 | fn try_from(order: u32) -> Result { 84 | Self::new(order) 85 | } 86 | } 87 | 88 | impl TryFrom for Order { 89 | type Error = OrderError; 90 | 91 | fn try_from(order: usize) -> Result { 92 | u32::try_from(order) 93 | .map_err(|_| OrderError::ExceededLayerLimit) 94 | .and_then(Self::try_from) 95 | } 96 | } 97 | 98 | #[cfg(test)] 99 | mod tests { 100 | use super::*; 101 | 102 | #[test] 103 | fn wrong_u32_order_value() { 104 | let order = Order::MAX.as_u32() + 1; 105 | 106 | assert_eq!(Order::try_from(order), Err(OrderError::ExceededLayerLimit)); 107 | } 108 | 109 | #[test] 110 | fn wrong_usize_order_values() { 111 | let order = (Order::MAX.as_u32() + 1) as usize; 112 | 113 | assert_eq!(Order::try_from(order), Err(OrderError::ExceededLayerLimit)); 114 | 115 | let order = u64::MAX as usize; 116 | 117 | assert_eq!(Order::try_from(order), Err(OrderError::ExceededLayerLimit)); 118 | } 119 | 120 | #[test] 121 | fn correct_order_value() { 122 | let order_value = Order::MAX.as_u32(); 123 | let order = Order::try_from(order_value); 124 | 125 | assert_eq!( 126 | order, 127 | Ok(Order( 128 | NonZeroU32::new(order_value as u32 ^ u32::MAX).unwrap() 129 | )) 130 | ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /forma/src/utils/prefix_scan.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use rayon::{ 16 | iter::plumbing::{bridge, Consumer, Producer, ProducerCallback, UnindexedConsumer}, 17 | prelude::*, 18 | }; 19 | 20 | /// Parallel-enabled prefix-scan that iterates over group- and local-indices. 21 | #[derive(Clone, Debug, Default)] 22 | pub struct PrefixScanIter<'s> { 23 | sums: &'s [u32], 24 | group_start: u32, 25 | group_end: u32, 26 | start: u32, 27 | end: u32, 28 | } 29 | 30 | impl Iterator for PrefixScanIter<'_> { 31 | type Item = (u32, u32); 32 | 33 | #[inline] 34 | fn next(&mut self) -> Option { 35 | loop { 36 | if self.start >= self.end { 37 | return None; 38 | } 39 | 40 | let exclusive_sum = self 41 | .group_start 42 | .checked_sub(1) 43 | .map(|i| self.sums[i as usize]) 44 | .unwrap_or_default(); 45 | let inclusive_sum = self.sums[self.group_start as usize]; 46 | 47 | if exclusive_sum == inclusive_sum { 48 | self.group_start += 1; 49 | continue; 50 | } 51 | 52 | let result = (self.group_start, self.start - exclusive_sum); 53 | 54 | self.start += 1; 55 | 56 | if self.start == inclusive_sum { 57 | self.group_start += 1; 58 | } 59 | 60 | return Some(result); 61 | } 62 | } 63 | } 64 | 65 | impl DoubleEndedIterator for PrefixScanIter<'_> { 66 | #[inline] 67 | fn next_back(&mut self) -> Option { 68 | loop { 69 | if self.start >= self.end { 70 | return None; 71 | } 72 | 73 | let exclusive_sum = self 74 | .group_end 75 | .checked_sub(1) 76 | .map(|i| self.sums[i as usize]) 77 | .unwrap_or_default(); 78 | let inclusive_sum = self.sums[self.group_end as usize]; 79 | 80 | if exclusive_sum == inclusive_sum { 81 | self.group_end = self.group_end.saturating_sub(1); 82 | continue; 83 | } 84 | 85 | let result = (self.group_end, self.end - 1 - exclusive_sum); 86 | 87 | self.end -= 1; 88 | 89 | if self.end == exclusive_sum { 90 | self.group_end = self.group_end.saturating_sub(1); 91 | } 92 | 93 | return Some(result); 94 | } 95 | } 96 | } 97 | 98 | impl ExactSizeIterator for PrefixScanIter<'_> { 99 | fn len(&self) -> usize { 100 | (self.end - self.start) as usize 101 | } 102 | } 103 | 104 | struct PrefixScanIterProducer<'s> { 105 | inner: PrefixScanIter<'s>, 106 | } 107 | 108 | impl<'s> Producer for PrefixScanIterProducer<'s> { 109 | type Item = (u32, u32); 110 | 111 | type IntoIter = PrefixScanIter<'s>; 112 | 113 | #[inline] 114 | fn into_iter(self) -> Self::IntoIter { 115 | self.inner 116 | } 117 | 118 | #[inline] 119 | fn split_at(self, index: usize) -> (Self, Self) { 120 | let index = index as u32 + self.inner.start; 121 | 122 | let mid = match self.inner.sums.binary_search(&index) { 123 | Ok(mid) => mid + 1, 124 | Err(mid) => mid, 125 | } as u32; 126 | 127 | ( 128 | Self { 129 | inner: PrefixScanIter { 130 | sums: self.inner.sums, 131 | group_start: self.inner.group_start, 132 | group_end: mid, 133 | start: self.inner.start, 134 | end: index, 135 | }, 136 | }, 137 | Self { 138 | inner: PrefixScanIter { 139 | sums: self.inner.sums, 140 | group_start: mid, 141 | group_end: self.inner.group_end, 142 | start: index, 143 | end: self.inner.end, 144 | }, 145 | }, 146 | ) 147 | } 148 | } 149 | 150 | impl<'s> PrefixScanIter<'s> { 151 | #[inline] 152 | pub fn new(sums: &'s [u32]) -> Self { 153 | Self { 154 | sums, 155 | group_start: 0, 156 | group_end: sums.len().saturating_sub(1) as u32, 157 | start: 0, 158 | end: sums.last().copied().unwrap_or_default(), 159 | } 160 | } 161 | } 162 | 163 | impl<'s> IntoParallelIterator for PrefixScanIter<'s> { 164 | type Iter = PrefixScanParIter<'s>; 165 | 166 | type Item = (u32, u32); 167 | 168 | #[inline] 169 | fn into_par_iter(self) -> Self::Iter { 170 | PrefixScanParIter { iter: self } 171 | } 172 | } 173 | 174 | pub struct PrefixScanParIter<'s> { 175 | iter: PrefixScanIter<'s>, 176 | } 177 | 178 | impl ParallelIterator for PrefixScanParIter<'_> { 179 | type Item = (u32, u32); 180 | 181 | #[inline] 182 | fn drive_unindexed(self, consumer: C) -> C::Result 183 | where 184 | C: UnindexedConsumer, 185 | { 186 | bridge(self, consumer) 187 | } 188 | 189 | #[inline] 190 | fn opt_len(&self) -> Option { 191 | Some(self.iter.len()) 192 | } 193 | } 194 | 195 | impl IndexedParallelIterator for PrefixScanParIter<'_> { 196 | #[inline] 197 | fn len(&self) -> usize { 198 | self.iter.len() 199 | } 200 | 201 | #[inline] 202 | fn drive>(self, consumer: C) -> C::Result { 203 | bridge(self, consumer) 204 | } 205 | 206 | #[inline] 207 | fn with_producer>(self, callback: CB) -> CB::Output { 208 | callback.callback(PrefixScanIterProducer { inner: self.iter }) 209 | } 210 | } 211 | 212 | #[cfg(test)] 213 | mod tests { 214 | use super::*; 215 | 216 | #[test] 217 | fn empty_iter() { 218 | let sums = &[]; 219 | 220 | assert_eq!(PrefixScanIter::new(sums).collect::>(), []); 221 | } 222 | 223 | #[test] 224 | fn local_iter() { 225 | let sums = &[2, 5, 9, 15]; 226 | let iter = PrefixScanIter::new(sums); 227 | 228 | assert_eq!( 229 | iter.collect::>(), 230 | [ 231 | (0, 0), 232 | (0, 1), 233 | (1, 0), 234 | (1, 1), 235 | (1, 2), 236 | (2, 0), 237 | (2, 1), 238 | (2, 2), 239 | (2, 3), 240 | (3, 0), 241 | (3, 1), 242 | (3, 2), 243 | (3, 3), 244 | (3, 4), 245 | (3, 5), 246 | ] 247 | ); 248 | } 249 | 250 | #[test] 251 | fn local_iter_rev() { 252 | let sums = &[2, 5, 9, 15]; 253 | let iter = PrefixScanIter::new(sums); 254 | 255 | assert_eq!( 256 | iter.rev().collect::>(), 257 | [ 258 | (3, 5), 259 | (3, 4), 260 | (3, 3), 261 | (3, 2), 262 | (3, 1), 263 | (3, 0), 264 | (2, 3), 265 | (2, 2), 266 | (2, 1), 267 | (2, 0), 268 | (1, 2), 269 | (1, 1), 270 | (1, 0), 271 | (0, 1), 272 | (0, 0), 273 | ] 274 | ); 275 | } 276 | 277 | #[test] 278 | fn both_ends() { 279 | let sums = &[2, 5, 9, 15]; 280 | let mut iter = PrefixScanIter::new(sums); 281 | 282 | assert_eq!(iter.len(), 15); 283 | 284 | assert_eq!(iter.next(), Some((0, 0))); 285 | assert_eq!(iter.next_back(), Some((3, 5))); 286 | assert_eq!(iter.next(), Some((0, 1))); 287 | assert_eq!(iter.next_back(), Some((3, 4))); 288 | assert_eq!(iter.next(), Some((1, 0))); 289 | assert_eq!(iter.next_back(), Some((3, 3))); 290 | assert_eq!(iter.next(), Some((1, 1))); 291 | 292 | assert_eq!(iter.len(), 8); 293 | 294 | assert_eq!(iter.next_back(), Some((3, 2))); 295 | assert_eq!(iter.next(), Some((1, 2))); 296 | assert_eq!(iter.next_back(), Some((3, 1))); 297 | assert_eq!(iter.next(), Some((2, 0))); 298 | assert_eq!(iter.next_back(), Some((3, 0))); 299 | assert_eq!(iter.next(), Some((2, 1))); 300 | assert_eq!(iter.next_back(), Some((2, 3))); 301 | assert_eq!(iter.next(), Some((2, 2))); 302 | assert_eq!(iter.next(), None); 303 | assert_eq!(iter.next_back(), None); 304 | 305 | assert_eq!(iter.len(), 0); 306 | } 307 | 308 | #[test] 309 | fn empty_groups() { 310 | let sums = &[2, 2, 5, 5]; 311 | let iter = PrefixScanIter::new(sums); 312 | 313 | assert_eq!( 314 | iter.collect::>(), 315 | [(0, 0), (0, 1), (2, 0), (2, 1), (2, 2),] 316 | ); 317 | } 318 | 319 | #[test] 320 | fn empty_groups_rev() { 321 | let sums = &[2, 2, 5, 5]; 322 | let iter = PrefixScanIter::new(sums); 323 | 324 | assert_eq!( 325 | iter.rev().collect::>(), 326 | [(2, 2), (2, 1), (2, 0), (0, 1), (0, 0),] 327 | ); 328 | } 329 | 330 | #[test] 331 | fn par_iter() { 332 | let sums = &[2, 5, 9, 15]; 333 | let iter = PrefixScanIter::new(sums); 334 | 335 | assert_eq!( 336 | iter.into_par_iter().collect::>(), 337 | [ 338 | (0, 0), 339 | (0, 1), 340 | (1, 0), 341 | (1, 1), 342 | (1, 2), 343 | (2, 0), 344 | (2, 1), 345 | (2, 2), 346 | (2, 3), 347 | (3, 0), 348 | (3, 1), 349 | (3, 2), 350 | (3, 3), 351 | (3, 4), 352 | (3, 5), 353 | ] 354 | ); 355 | } 356 | 357 | #[test] 358 | fn par_iter2() { 359 | let sums = &[3, 6, 10, 11]; 360 | let iter = PrefixScanIter::new(sums); 361 | 362 | assert_eq!( 363 | iter.into_par_iter().collect::>(), 364 | [ 365 | (0, 0), 366 | (0, 1), 367 | (0, 2), 368 | (1, 0), 369 | (1, 1), 370 | (1, 2), 371 | (2, 0), 372 | (2, 1), 373 | (2, 2), 374 | (2, 3), 375 | (3, 0), 376 | ] 377 | ); 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /forma/src/utils/simd/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #![allow(non_camel_case_types)] 16 | 17 | #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] 18 | mod aarch64; 19 | #[cfg(not(any( 20 | all( 21 | target_arch = "x86_64", 22 | target_feature = "avx", 23 | target_feature = "avx2", 24 | target_feature = "fma", 25 | ), 26 | all(target_arch = "aarch64", target_feature = "neon"), 27 | all(target_arch = "wasm32", target_feature = "simd128"), 28 | )))] 29 | mod auto; 30 | #[cfg(all( 31 | target_arch = "x86_64", 32 | target_feature = "avx", 33 | target_feature = "avx2", 34 | target_feature = "fma" 35 | ))] 36 | mod avx; 37 | #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))] 38 | mod wasm32; 39 | 40 | #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] 41 | pub use aarch64::*; 42 | #[cfg(not(any( 43 | all( 44 | target_arch = "x86_64", 45 | target_feature = "avx", 46 | target_feature = "avx2", 47 | target_feature = "fma", 48 | ), 49 | all(target_arch = "aarch64", target_feature = "neon"), 50 | all(target_arch = "wasm32", target_feature = "simd128"), 51 | )))] 52 | pub use auto::*; 53 | #[cfg(all( 54 | target_arch = "x86_64", 55 | target_feature = "avx", 56 | target_feature = "avx2", 57 | target_feature = "fma", 58 | ))] 59 | pub use avx::*; 60 | #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))] 61 | pub use wasm32::*; 62 | 63 | pub trait Simd { 64 | const LANES: usize; 65 | } 66 | 67 | impl Simd for u8x32 { 68 | const LANES: usize = 32; 69 | } 70 | 71 | impl Simd for i8x16 { 72 | const LANES: usize = 16; 73 | } 74 | 75 | impl Simd for i16x16 { 76 | const LANES: usize = 16; 77 | } 78 | 79 | impl Simd for i32x8 { 80 | const LANES: usize = 8; 81 | } 82 | 83 | impl Simd for f32x8 { 84 | const LANES: usize = 8; 85 | } 86 | 87 | #[cfg(test)] 88 | mod tests { 89 | use std::f32::INFINITY; 90 | 91 | use super::*; 92 | 93 | #[test] 94 | fn f32x8_splat() { 95 | for v in [1.0, 0.0, f32::INFINITY, f32::NEG_INFINITY] { 96 | assert_eq!(f32x8::splat(v).to_array(), [v; 8]); 97 | } 98 | } 99 | 100 | #[test] 101 | fn f32x8_indexed() { 102 | let index: Vec = (0..8).map(|v| v as f32).collect(); 103 | assert_eq!(f32x8::indexed().to_array(), index[..]); 104 | } 105 | 106 | #[test] 107 | fn f32x8_from_array() { 108 | let value = [-5.0, -4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0]; 109 | assert_eq!(f32x8::from_array(value).to_array(), value); 110 | } 111 | 112 | #[test] 113 | fn u32x8_splat() { 114 | assert_eq!([0; 8], u32x8::splat(0).to_array()); 115 | assert_eq!([u32::MAX; 8], u32x8::splat(u32::MAX).to_array()); 116 | assert_eq!([u32::MIN; 8], u32x8::splat(u32::MIN).to_array()); 117 | } 118 | 119 | #[test] 120 | fn u32x8_mul_add() { 121 | let a = u32x8::from(f32x8::from_array([ 122 | 10.0, 20.0, 30.0, 50.0, 70.0, 110.0, 130.0, 170.0, 123 | ])); 124 | let b = u32x8::from(f32x8::from_array([ 125 | 19.0, 23.0, 29.0, 31.0, 37.0, 41.0, 43.0, 47.0, 126 | ])); 127 | let c = u32x8::from(f32x8::from_array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0])); 128 | let expected = [191, 462, 873, 1554, 2595, 4516, 5597, 7998]; 129 | assert_eq!(expected, u32x8::mul_add(a, b, c).to_array()); 130 | 131 | let a = u32x8::from(f32x8::splat(0x007f_ffff as f32)); 132 | let b = u32x8::from(f32x8::splat(0x200 as f32)); 133 | let c = u32x8::from(f32x8::splat(0x1ff as f32)); 134 | let expected = [u32::MAX; 8]; 135 | assert_eq!(expected, u32x8::mul_add(a, b, c).to_array()); 136 | } 137 | 138 | #[test] 139 | fn u32x8_from_f32x8() { 140 | let values = u32x8::from(f32x8::from_array([ 141 | -INFINITY, 142 | -f32::MAX, 143 | -2.5, 144 | -1.0, 145 | -0.5, 146 | -f32::MIN_POSITIVE, 147 | -0.0, 148 | 0.0, 149 | ])); 150 | assert_eq!(values.to_array(), [0u32; 8]); 151 | 152 | let f32_before = |f| f32::from_bits(f32::to_bits(f) - 1); 153 | let values = u32x8::from(f32x8::from_array([ 154 | 0.0, 155 | f32::MIN_POSITIVE, 156 | 0.4, 157 | 0.5, 158 | 0.6, 159 | 0.7, 160 | 0.8, 161 | f32_before(1.), 162 | ])); 163 | assert_eq!(values.to_array(), [0u32; 8]); 164 | 165 | let values = u32x8::from(f32x8::from_array([ 166 | 1.0, 167 | 1.4, 168 | 1.5, 169 | f32_before(2.0), 170 | 2.0, 171 | 2.4, 172 | 2.5, 173 | f32_before(3.0), 174 | ])); 175 | assert_eq!(values.to_array(), [1, 1, 1, 1, 2, 2, 2, 2]); 176 | 177 | const MAX_INT_F32: u32 = 1u32 << 24; 178 | for value in (MAX_INT_F32 - 255)..MAX_INT_F32 { 179 | assert_eq!( 180 | [value; 8], 181 | u32x8::from(f32x8::splat(value as f32)).to_array() 182 | ); 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /forma/src/utils/small_bit_set.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | use std::mem; 16 | 17 | type Container = u32; 18 | 19 | #[derive(Clone, Debug, Default)] 20 | pub struct SmallBitSet { 21 | bit_set: Container, 22 | } 23 | 24 | impl SmallBitSet { 25 | pub fn clear(&mut self) { 26 | self.bit_set = 0; 27 | } 28 | 29 | pub const fn contains(&self, val: &u8) -> bool { 30 | (self.bit_set >> *val as Container) & 0b1 != 0 31 | } 32 | 33 | pub fn insert(&mut self, val: u8) -> bool { 34 | if val as usize >= mem::size_of_val(&self.bit_set) * 8 { 35 | return false; 36 | } 37 | 38 | self.bit_set |= 0b1 << u32::from(val); 39 | 40 | true 41 | } 42 | 43 | pub fn remove(&mut self, val: u8) -> bool { 44 | if val as usize >= mem::size_of_val(&self.bit_set) * 8 { 45 | return false; 46 | } 47 | 48 | self.bit_set &= !(0b1 << u32::from(val)); 49 | 50 | true 51 | } 52 | 53 | pub fn first_empty_slot(&mut self) -> Option { 54 | let slot = self.bit_set.trailing_ones() as u8; 55 | 56 | self.insert(slot).then_some(slot) 57 | } 58 | } 59 | --------------------------------------------------------------------------------