├── .gitignore ├── .github └── FUNDING.yml ├── examples ├── test-image.png └── gl.rs ├── .cargo └── config.toml ├── assets └── test-image.png ├── run-wasm ├── Cargo.toml └── src │ └── main.rs ├── .woodpecker ├── msrv.yml ├── github.yml ├── tidy.yml ├── test.yml └── release.yml ├── MAINTAINERS.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── DCO.txt ├── README.md ├── Cargo.toml ├── src ├── image.rs ├── text.rs ├── stroke.rs ├── resources.rs ├── atlas.rs ├── brush.rs ├── mask.rs ├── gpu_backend.rs ├── rasterizer.rs └── lib.rs ├── .drone.yml ├── LICENSE-LGPL-3.0.md ├── deny.toml └── LICENSE-MPL-2.0.md /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | Cargo.lock -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: notgull 2 | -------------------------------------------------------------------------------- /examples/test-image.png: -------------------------------------------------------------------------------- 1 | ../assets/test-image.png -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | run-wasm = ["run", "--release", "--package", "run-wasm", "--"] 3 | -------------------------------------------------------------------------------- /assets/test-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notgull/piet-hardware/HEAD/assets/test-image.png -------------------------------------------------------------------------------- /run-wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "run-wasm" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | cargo-run-wasm = "0.3.0" 8 | -------------------------------------------------------------------------------- /run-wasm/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | cargo_run_wasm::run_wasm_with_css( 3 | "body { margin: 0px; } canvas { width: 100%; height: 100%; }", 4 | ); 5 | } 6 | -------------------------------------------------------------------------------- /.woodpecker/msrv.yml: -------------------------------------------------------------------------------- 1 | when: 2 | branch: main 3 | 4 | matrix: 5 | RUST_VERSION: 6 | - "1.66.0" 7 | 8 | steps: 9 | msrv: 10 | image: rust 11 | commands: 12 | - rustup default ${RUST_VERSION} 13 | - cargo build 14 | - cargo build --no-default-features 15 | environment: 16 | CARGO_INCREMENTAL: "0" 17 | CARGO_NET_GIT_FETCH_WITH_CLI: "true" 18 | CARGO_NET_RETRY: "10" 19 | CARGO_TERM_COLOR: always 20 | RUST_BACKTRACE: "1" 21 | RUSTFLAGS: "-D warnings" 22 | RUSTDOCFLAGS: "-D warnings" 23 | RUSTUP_MAX_RETRIES: "10" 24 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Project Maintainers 2 | 3 | The intellectual property rights for `piet-hardware` are owned by Project 4 | Leadership, which consists of the following structure: 5 | 6 | ## Benevolent Dictator for Life 7 | 8 | - [John Nunley](https://notgull.net) is the Benevolent Dictator for Life (BDFL) 9 | of this project. He has the final say in project direction and disputes. 10 | 11 | ## Other Information 12 | 13 | Members of Project Leadership may change in the future without license changes. 14 | 15 | Contract project leadership at the email consisting of: 16 | 17 | - The first three letters of the word "developer". 18 | - At sign. 19 | - Notgull dot net. 20 | -------------------------------------------------------------------------------- /.woodpecker/github.yml: -------------------------------------------------------------------------------- 1 | when: 2 | - event: push 3 | branch: main 4 | 5 | clone: 6 | git: 7 | image: woodpeckerci/plugin-git 8 | settings: 9 | partial: false 10 | 11 | steps: 12 | github_mirror: 13 | image: alpine:latest 14 | secrets: [ github_ssh_key ] 15 | commands: 16 | - apk --no-cache update -q 17 | - apk --no-cache add git openssh 18 | - mkdir -pv $HOME/.ssh 19 | - ssh-keyscan -H -t rsa github.com >> ~/.ssh/known_hosts 20 | - echo "$GITHUB_SSH_KEY" > "$HOME/.ssh/id_rsa" && chmod 0600 $HOME/.ssh/id_rsa 21 | - git remote add github_origin git@github.com:notgull/piet-hardware.git 22 | - git push github_origin main 23 | -------------------------------------------------------------------------------- /.woodpecker/tidy.yml: -------------------------------------------------------------------------------- 1 | when: 2 | branch: main 3 | 4 | steps: 5 | fmt: 6 | image: rust 7 | commands: 8 | - rustup default stable 9 | - rustup component add rustfmt 10 | - cargo fmt --all --check 11 | 12 | lint: 13 | environment: 14 | CARGO_INCREMENTAL: "0" 15 | CARGO_NET_GIT_FETCH_WITH_CLI: "true" 16 | CARGO_NET_RETRY: "10" 17 | CARGO_TERM_COLOR: always 18 | RUST_BACKTRACE: "1" 19 | RUSTFLAGS: "-D warnings" 20 | RUSTDOCFLAGS: "-D warnings" 21 | RUSTUP_MAX_RETRIES: "10" 22 | PACKAGES: libx11-dev libxcb1-dev libxkbcommon-dev libx11-xcb-dev libwayland-dev pkg-config 23 | image: rust 24 | commands: 25 | - apt-get -o Acquire::Retries=10 -qq update 26 | - apt-get -o Acquire::Retries=10 -o Dpkg::Use-Pty=0 install -y --no-install-recommends $PACKAGES 27 | - rustup default stable 28 | - rustup component add clippy 29 | - cargo clippy --all-features --all-targets 30 | -------------------------------------------------------------------------------- /.woodpecker/test.yml: -------------------------------------------------------------------------------- 1 | when: 2 | branch: main 3 | 4 | matrix: 5 | RUST_VERSION: 6 | - stable 7 | - beta 8 | - nightly 9 | 10 | steps: 11 | test: 12 | image: rust 13 | commands: 14 | - apt-get -o Acquire::Retries=10 -qq update 15 | - apt-get -o Acquire::Retries=10 -o Dpkg::Use-Pty=0 install -y --no-install-recommends $PACKAGES 16 | - rustup default ${RUST_VERSION} 17 | - cargo build --all --all-features --all-targets 18 | - if [ ${RUST_VERSION} = "nightly" ]; then cargo check -Z features=dev_dep; fi 19 | - cargo test 20 | environment: 21 | CARGO_INCREMENTAL: "0" 22 | CARGO_NET_GIT_FETCH_WITH_CLI: "true" 23 | CARGO_NET_RETRY: "10" 24 | CARGO_TERM_COLOR: always 25 | RUST_BACKTRACE: "1" 26 | RUSTFLAGS: "-D warnings" 27 | RUSTDOCFLAGS: "-D warnings" 28 | RUSTUP_MAX_RETRIES: "10" 29 | PACKAGES: libx11-dev libxcb1-dev libxkbcommon-dev libx11-xcb-dev libwayland-dev pkg-config 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This log describes changes in the `piet-hardware`, `piet-glow` and `piet-wgpu` 4 | crates. 5 | 6 | ## piet-hardware 0.5.1 7 | 8 | - Moved source code to `codeberg.org` 9 | 10 | ## piet-hardware 0.5.0 11 | 12 | - **Breaking:** Add support for using the hardware's scissor rect for optimizing 13 | clipping simple rectangles. 14 | 15 | ## piet-hardware 0.4.1 16 | 17 | - Moved source code to `src.notgull.net`. 18 | 19 | ## piet-hardware 0.4.0 20 | 21 | - **Breaking:** Add a new scale parameter to `capture_area`. 22 | 23 | ## piet-hardware 0.3.0 24 | 25 | - **Breaking:** Add the `capture_area` method for capturing an area of the screen. 26 | - **Breaking:** Change the `GpuContext` trait to use `&mut self` instead of `&self`. 27 | - Add support for dashed lines. 28 | - Fix some minor bugs. 29 | - Improved text handling. 30 | - Fix bugs in the clipping code. 31 | 32 | ## piet-hardware v0.2.1 33 | 34 | - Add support for line decorations. 35 | 36 | ## piet-hardware 0.2.0 37 | 38 | - **Breaking:** The `push_buffers` method is now safe. 39 | -------------------------------------------------------------------------------- /.woodpecker/release.yml: -------------------------------------------------------------------------------- 1 | when: 2 | - event: tag 3 | ref: refs/tags/v* 4 | 5 | steps: 6 | create_release: 7 | image: alpine:latest 8 | secrets: [ codeberg_config, codeberg_ssh_key ] 9 | commands: 10 | - apk --no-cache update -q 11 | - apk --no-cache add curl git tea 12 | - curl --proto '=https' --tlsv1.2 -fsSL --retry 10 "$PARSE_CHANGELOG" | tar -xvzf - -C /usr/bin/ 13 | - chmod +x /usr/bin/parse-changelog 14 | - mkdir -p /tmp/.ssh && echo "$CODEBERG_SSH_KEY" > /tmp/.ssh/id_rsa 15 | - mkdir -p $HOME/.config/tea && echo "$CODEBERG_CONFIG" > $HOME/.config/tea/config.yml 16 | - tea release create --tag "$(git describe --tags)" --title "$(git describe --tags)" -n "$(/usr/bin/parse-changelog CHANGELOG.md)" 17 | environment: 18 | PARSE_CHANGELOG: https://github.com/taiki-e/parse-changelog/releases/download/v0.6.4/parse-changelog-x86_64-unknown-linux-musl.tar.gz 19 | 20 | crates_io_release: 21 | image: rust:latest 22 | secrets: [ crates_io_api_token ] 23 | commands: 24 | - cargo login $CRATES_IO_API_TOKEN 25 | - cargo publish 26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution 2 | 3 | We welcome contributions to this project. Please do not open issues or pull 4 | requests on the GitHub mirror. Please open issues and pull requests on our 5 | [`Codeberg`](https://codeberg.org/notgull/piet-hardware). 6 | 7 | ## Coding Style 8 | 9 | [`rustfmt`] and [`clippy`] is used to enforce coding style. Before pushing a 10 | commit, run `cargo fmt --all` to format your code and make sure [`clippy`] 11 | warnings are fixed. 12 | 13 | [`rustfmt`]: https://github.com/rust-lang/rustfmt 14 | [`clippy`]: https://github.com/rust-lang/clippy 15 | 16 | ## Testing 17 | 18 | All changes submitted to this repository are run through our CI system. 19 | 20 | ## DCO 21 | 22 | As an alternative to a Contributor License Agreement, this project uses a 23 | [Developer Certificate of Origin (DCO)](./DCO.txt) to ensure that contributors 24 | own the copyright terms of their contributions. In order to assert that you 25 | agree to the terms of the DCO, you must add the following line to every commit: 26 | 27 | ```plaintext 28 | Signed-off-by: Your Name 29 | ``` 30 | 31 | This can be done automatically by appending the `-s` option to `git commit`. 32 | -------------------------------------------------------------------------------- /DCO.txt: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 6 | Everyone is permitted to copy and distribute verbatim copies of this 7 | license document, but changing it is not allowed. 8 | 9 | 10 | Developer's Certificate of Origin 1.1 11 | 12 | By making a contribution to this project, I certify that: 13 | 14 | (a) The contribution was created in whole or in part by me and I 15 | have the right to submit it under the open source license 16 | indicated in the file; or 17 | 18 | (b) The contribution is based upon previous work that, to the best 19 | of my knowledge, is covered under an appropriate open source 20 | license and I have the right under that license to submit that 21 | work with modifications, whether created in whole or in part 22 | by me, under the same open source license (unless I am 23 | permitted to submit under a different license), as indicated 24 | in the file; or 25 | 26 | (c) The contribution was provided directly to me by some other 27 | person who certified (a), (b) or (c) and I have not modified 28 | it. 29 | 30 | (d) I understand and agree that this project and the contribution 31 | are public and that a record of the contribution (including all 32 | personal information I submit with it, including my sign-off) is 33 | maintained indefinitely and may be redistributed consistent with 34 | this project or the open source license(s) involved. 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # piet-hardware 2 | 3 | `piet-hardware` is a strategy for implementing the [`piet`] drawing interface 4 | using GPU primitives. The goal is to break down the drawing operations to 5 | rendering textured triangles. The resulting buffers are than passed to the GPU 6 | backend for rendering. 7 | 8 | As `piet-hardware` simply implements the high-level strategy, it has no unsafe 9 | code. The actual GPU calls are forwarded to an object that implements 10 | `GpuContext`. This object is intended to be an interface to OpenGL, Vulkan, 11 | Metal, or other GPU APIs. 12 | 13 | [`piet`]: https://crates.io/crates/piet 14 | 15 | ## Source Code 16 | 17 | The canonical code for this repository is kept on [Codeberg]. For 18 | convenience, a mirror is kept on [GitHub]. 19 | 20 | [Codeberg]: https://codeberg.org/notgull/piet-hardware 21 | [GitHub]: https://github.com/notgull/piet-hardware 22 | 23 | ## License 24 | 25 | `piet-hardware` is free software: you can redistribute it and/or modify it under 26 | the terms of either: 27 | 28 | * GNU Lesser General Public License as published by the Free Software Foundation, 29 | either version 3 of the License, or (at your option) any later version. 30 | * Mozilla Public License as published by the Mozilla Foundation, version 2. 31 | 32 | `piet-hardware` is distributed in the hope that it will be useful, but WITHOUT 33 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 34 | FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License or the 35 | Mozilla Public License for more details. 36 | 37 | You should have received a copy of the GNU Lesser General Public License and the 38 | Mozilla Public License along with `piet-hardware`. If not, see 39 | or . 40 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "piet-hardware" 3 | version = "0.5.1" 4 | edition = "2021" 5 | license = "LGPL-3.0-or-later OR MPL-2.0" 6 | rust-version = "1.66.0" 7 | authors = ["John Nunley "] 8 | repository = "https://codeberg.org/notgull/piet-hardware" 9 | homepage = "https://codeberg.org/notgull/piet-hardware" 10 | keywords = ["gpu", "graphics", "2d"] 11 | categories = ["rendering::graphics-api"] 12 | description = "Toolkit for creating GPU accelerated 2D graphics applications" 13 | 14 | [dependencies] 15 | ahash = { version = "0.8.3", default-features = false, features = ["std"] } 16 | arrayvec = "0.7.4" 17 | bytemuck = { version = "1.13.1", default-features = false, features = ["derive"] } 18 | cosmic-text = { version = "0.9.0", default-features = false, features = ["swash"] } 19 | etagere = "0.2.8" 20 | hashbrown = { version = "0.14.0", default-features = false } 21 | kurbo = { version = "0.9.5", default-features = false } 22 | lyon_tessellation = "1.0.10" 23 | piet = { version = "0.6.2", default-features = false } 24 | piet-cosmic-text = { version = "0.3.0", default-features = false, features = ["std", "tracing"] } 25 | tiny-skia = { version = "0.11.1", default-features = false, features = ["std"] } 26 | tinyvec = { version = "1.6.0", default-features = false, features = ["alloc"] } 27 | tracing = { version = "0.1.37", default-features = false } 28 | zeno = { version = "0.2.2", default-features = false } 29 | 30 | [dev-dependencies] 31 | env_logger = { version = "0.10.0", default-features = false, features = ["auto-color"] } 32 | gl = "0.14.0" 33 | glutin = { version = "0.30.9", default-features = false, features = ["x11", "glx", "egl", "wayland", "wgl"] } 34 | glutin-winit = { version = "0.3.0", default-features = false, features = ["x11", "egl", "glx", "wgl"] } 35 | image = { version = "0.24.6", default-features = false, features = ["png"] } 36 | instant = "0.1.12" 37 | log = "0.4.19" 38 | raw-window-handle = { version = "0.5.2", default-features = false } 39 | web-time = "0.2.3" 40 | winit = { version = "0.28.6", default-features = false, features = ["wayland", "x11"] } 41 | 42 | [workspace] 43 | resolver = "2" 44 | members = ["run-wasm"] 45 | 46 | -------------------------------------------------------------------------------- /src/image.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later OR MPL-2.0 2 | // This file is a part of `piet-hardware`. 3 | // 4 | // `piet-hardware` is free software: you can redistribute it and/or modify it under the 5 | // terms of either: 6 | // 7 | // * GNU Lesser General Public License as published by the Free Software Foundation, either 8 | // version 3 of the License, or (at your option) any later version. 9 | // * Mozilla Public License as published by the Mozilla Foundation, version 2. 10 | // 11 | // `piet-hardware` is distributed in the hope that it will be useful, but WITHOUT ANY 12 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 13 | // PURPOSE. See the GNU Lesser General Public License or the Mozilla Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License and the Mozilla 17 | // Public License along with `piet-hardware`. If not, see . 18 | 19 | //! The image type for the GPU renderer. 20 | 21 | use super::gpu_backend::GpuContext; 22 | use super::resources::Texture; 23 | 24 | use piet::kurbo::Size; 25 | 26 | use std::rc::Rc; 27 | 28 | /// The image type used by the GPU renderer. 29 | #[derive(Debug)] 30 | pub struct Image { 31 | /// The texture. 32 | texture: Rc>, 33 | 34 | /// The size of the image. 35 | size: Size, 36 | } 37 | 38 | impl Image { 39 | /// Create a new image from a texture. 40 | pub(crate) fn new(texture: Texture, size: Size) -> Self { 41 | Self { 42 | texture: Rc::new(texture), 43 | size, 44 | } 45 | } 46 | 47 | /// Get the texture. 48 | pub(crate) fn texture(&self) -> &Texture { 49 | &self.texture 50 | } 51 | } 52 | 53 | impl Clone for Image { 54 | fn clone(&self) -> Self { 55 | Self { 56 | texture: self.texture.clone(), 57 | size: self.size, 58 | } 59 | } 60 | } 61 | 62 | impl piet::Image for Image { 63 | fn size(&self) -> Size { 64 | self.size 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | type: docker 3 | name: tidy 4 | 5 | steps: 6 | - name: tidy 7 | image: notgull/ci:latest 8 | pull: true 9 | commands: 10 | - tidy.sh 11 | --- 12 | kind: pipeline 13 | type: docker 14 | name: test-stable 15 | 16 | depends_on: 17 | - tidy 18 | 19 | steps: 20 | - name: test 21 | image: notgull/ci:latest 22 | pull: always 23 | commands: 24 | - ensure_tool.sh wayland x11 25 | - test_rust.sh stable 26 | --- 27 | kind: pipeline 28 | type: docker 29 | name: test-beta 30 | 31 | depends_on: 32 | - tidy 33 | 34 | steps: 35 | - name: test 36 | image: notgull/ci:latest 37 | pull: always 38 | commands: 39 | - ensure_tool.sh wayland x11 40 | - test_rust.sh beta 41 | --- 42 | kind: pipeline 43 | type: docker 44 | name: test-nightly 45 | 46 | depends_on: 47 | - tidy 48 | 49 | steps: 50 | - name: test 51 | image: notgull/ci:latest 52 | pull: always 53 | commands: 54 | - ensure_tool.sh wayland x11 55 | - test_rust.sh nightly 56 | --- 57 | kind: pipeline 58 | type: docker 59 | name: test-msrv 60 | 61 | depends_on: 62 | - tidy 63 | 64 | steps: 65 | - name: test 66 | image: notgull/ci:latest 67 | pull: always 68 | commands: 69 | - ensure_tool.sh wayland x11 70 | - test_rust.sh 1.65.0 71 | --- 72 | kind: pipeline 73 | type: docker 74 | name: release 75 | 76 | trigger: 77 | branch: 78 | - main 79 | event: 80 | - push 81 | 82 | depends_on: 83 | - test-stable 84 | - test-beta 85 | - test-nightly 86 | - test-msrv 87 | 88 | steps: 89 | - name: gitea_release 90 | image: notgull/ci:stable 91 | environment: 92 | SSH_KEY: 93 | from_secret: tea_ssh_key 94 | TEA_CONFIG: 95 | from_secret: tea_config_yml 96 | commands: 97 | - mkdir -pv ~/.ssh && mkdir -pv ~/.config/tea 98 | - echo "$SSH_KEY" > ~/.ssh/id_rsa && chmod 0600 ~/.ssh/id_rsa 99 | - echo "$TEA_CONFIG" > ~/.config/tea/config.yml 100 | - release.sh 101 | --- 102 | kind: pipeline 103 | type: docker 104 | name: github 105 | 106 | trigger: 107 | branch: 108 | - main 109 | event: 110 | - push 111 | 112 | steps: 113 | - name: mirror to GitHub 114 | image: alpine:edge 115 | environment: 116 | SSH_KEY: 117 | from_secret: gh_ssh_key 118 | commands: 119 | - apk add git openssh 120 | - mkdir -pv ~/.ssh 121 | - ssh-keyscan -H -t rsa github.com >> ~/.ssh/known_hosts 122 | - echo "$SSH_KEY" > ~/.ssh/id_rsa && chmod 0600 ~/.ssh/id_rsa 123 | - git remote add github_origin git@github.com:notgull/piet-hardware.git 124 | - git push github_origin main 125 | -------------------------------------------------------------------------------- /src/text.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later OR MPL-2.0 2 | // This file is a part of `piet-hardware`. 3 | // 4 | // `piet-hardware` is free software: you can redistribute it and/or modify it under the 5 | // terms of either: 6 | // 7 | // * GNU Lesser General Public License as published by the Free Software Foundation, either 8 | // version 3 of the License, or (at your option) any later version. 9 | // * Mozilla Public License as published by the Mozilla Foundation, version 2. 10 | // 11 | // `piet-hardware` is distributed in the hope that it will be useful, but WITHOUT ANY 12 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 13 | // PURPOSE. See the GNU Lesser General Public License or the Mozilla Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License and the Mozilla 17 | // Public License along with `piet-hardware`. If not, see . 18 | 19 | use piet::kurbo::{Point, Rect, Size}; 20 | use piet::Error as Pierror; 21 | 22 | use piet_cosmic_text::{ 23 | Text as CosText, TextLayout as CosTextLayout, TextLayoutBuilder as CosTextLayoutBuilder, 24 | }; 25 | 26 | /// The text layout engine for the GPU renderer. 27 | #[derive(Debug, Clone)] 28 | pub struct Text(CosText); 29 | 30 | impl Text { 31 | /// Create a new text layout engine. 32 | pub(crate) fn new() -> Self { 33 | Self(CosText::new()) 34 | } 35 | 36 | /// Run a function with the `FontSystem` associated with this type. 37 | pub(crate) fn with_font_system_mut( 38 | &self, 39 | f: impl FnOnce(&mut cosmic_text::FontSystem) -> R, 40 | ) -> Option { 41 | self.0.with_font_system_mut(f) 42 | } 43 | 44 | /// Set the DPI for rendering text. 45 | pub fn set_dpi(&mut self, dpi: f64) { 46 | self.0.set_dpi(dpi); 47 | } 48 | 49 | /// Get the current DPI for rendering text. 50 | pub fn dpi(&self) -> f64 { 51 | self.0.dpi() 52 | } 53 | } 54 | 55 | impl piet::Text for Text { 56 | type TextLayout = TextLayout; 57 | type TextLayoutBuilder = TextLayoutBuilder; 58 | 59 | fn font_family(&mut self, family_name: &str) -> Option { 60 | self.0.font_family(family_name) 61 | } 62 | 63 | fn load_font(&mut self, data: &[u8]) -> Result { 64 | self.0.load_font(data) 65 | } 66 | 67 | fn new_text_layout(&mut self, text: impl piet::TextStorage) -> Self::TextLayoutBuilder { 68 | TextLayoutBuilder(self.0.new_text_layout(text)) 69 | } 70 | } 71 | 72 | /// The text layout builder for the GPU renderer. 73 | #[derive(Debug)] 74 | pub struct TextLayoutBuilder(CosTextLayoutBuilder); 75 | 76 | impl piet::TextLayoutBuilder for TextLayoutBuilder { 77 | type Out = TextLayout; 78 | 79 | fn max_width(self, width: f64) -> Self { 80 | Self(self.0.max_width(width)) 81 | } 82 | 83 | fn alignment(self, alignment: piet::TextAlignment) -> Self { 84 | Self(self.0.alignment(alignment)) 85 | } 86 | 87 | fn default_attribute(self, attribute: impl Into) -> Self { 88 | Self(self.0.default_attribute(attribute)) 89 | } 90 | 91 | fn range_attribute( 92 | self, 93 | range: impl std::ops::RangeBounds, 94 | attribute: impl Into, 95 | ) -> Self { 96 | Self(self.0.range_attribute(range, attribute)) 97 | } 98 | 99 | fn build(self) -> Result { 100 | Ok(TextLayout(self.0.build()?)) 101 | } 102 | } 103 | 104 | /// The text layout for the GPU renderer. 105 | #[derive(Debug, Clone)] 106 | pub struct TextLayout(CosTextLayout); 107 | 108 | impl TextLayout { 109 | pub(crate) fn buffer(&self) -> &cosmic_text::Buffer { 110 | self.0.buffer() 111 | } 112 | } 113 | 114 | impl piet::TextLayout for TextLayout { 115 | fn size(&self) -> Size { 116 | self.0.size() 117 | } 118 | 119 | fn trailing_whitespace_width(&self) -> f64 { 120 | self.0.trailing_whitespace_width() 121 | } 122 | 123 | fn image_bounds(&self) -> Rect { 124 | self.0.image_bounds() 125 | } 126 | 127 | fn text(&self) -> &str { 128 | self.0.text() 129 | } 130 | 131 | fn line_text(&self, line_number: usize) -> Option<&str> { 132 | self.0.line_text(line_number) 133 | } 134 | 135 | fn line_metric(&self, line_number: usize) -> Option { 136 | self.0.line_metric(line_number) 137 | } 138 | 139 | fn line_count(&self) -> usize { 140 | self.0.line_count() 141 | } 142 | 143 | fn hit_test_point(&self, point: Point) -> piet::HitTestPoint { 144 | self.0.hit_test_point(point) 145 | } 146 | 147 | fn hit_test_text_position(&self, idx: usize) -> piet::HitTestPosition { 148 | self.0.hit_test_text_position(idx) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /LICENSE-LGPL-3.0.md: -------------------------------------------------------------------------------- 1 | ### GNU LESSER GENERAL PUBLIC LICENSE 2 | 3 | Version 3, 29 June 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | This version of the GNU Lesser General Public License incorporates the 12 | terms and conditions of version 3 of the GNU General Public License, 13 | supplemented by the additional permissions listed below. 14 | 15 | #### 0. Additional Definitions. 16 | 17 | As used herein, "this License" refers to version 3 of the GNU Lesser 18 | General Public License, and the "GNU GPL" refers to version 3 of the 19 | GNU General Public License. 20 | 21 | "The Library" refers to a covered work governed by this License, other 22 | than an Application or a Combined Work as defined below. 23 | 24 | An "Application" is any work that makes use of an interface provided 25 | by the Library, but which is not otherwise based on the Library. 26 | Defining a subclass of a class defined by the Library is deemed a mode 27 | of using an interface provided by the Library. 28 | 29 | A "Combined Work" is a work produced by combining or linking an 30 | Application with the Library. The particular version of the Library 31 | with which the Combined Work was made is also called the "Linked 32 | Version". 33 | 34 | The "Minimal Corresponding Source" for a Combined Work means the 35 | Corresponding Source for the Combined Work, excluding any source code 36 | for portions of the Combined Work that, considered in isolation, are 37 | based on the Application, and not on the Linked Version. 38 | 39 | The "Corresponding Application Code" for a Combined Work means the 40 | object code and/or source code for the Application, including any data 41 | and utility programs needed for reproducing the Combined Work from the 42 | Application, but excluding the System Libraries of the Combined Work. 43 | 44 | #### 1. Exception to Section 3 of the GNU GPL. 45 | 46 | You may convey a covered work under sections 3 and 4 of this License 47 | without being bound by section 3 of the GNU GPL. 48 | 49 | #### 2. Conveying Modified Versions. 50 | 51 | If you modify a copy of the Library, and, in your modifications, a 52 | facility refers to a function or data to be supplied by an Application 53 | that uses the facility (other than as an argument passed when the 54 | facility is invoked), then you may convey a copy of the modified 55 | version: 56 | 57 | - a) under this License, provided that you make a good faith effort 58 | to ensure that, in the event an Application does not supply the 59 | function or data, the facility still operates, and performs 60 | whatever part of its purpose remains meaningful, or 61 | - b) under the GNU GPL, with none of the additional permissions of 62 | this License applicable to that copy. 63 | 64 | #### 3. Object Code Incorporating Material from Library Header Files. 65 | 66 | The object code form of an Application may incorporate material from a 67 | header file that is part of the Library. You may convey such object 68 | code under terms of your choice, provided that, if the incorporated 69 | material is not limited to numerical parameters, data structure 70 | layouts and accessors, or small macros, inline functions and templates 71 | (ten or fewer lines in length), you do both of the following: 72 | 73 | - a) Give prominent notice with each copy of the object code that 74 | the Library is used in it and that the Library and its use are 75 | covered by this License. 76 | - b) Accompany the object code with a copy of the GNU GPL and this 77 | license document. 78 | 79 | #### 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, taken 82 | together, effectively do not restrict modification of the portions of 83 | the Library contained in the Combined Work and reverse engineering for 84 | debugging such modifications, if you also do each of the following: 85 | 86 | - a) Give prominent notice with each copy of the Combined Work that 87 | the Library is used in it and that the Library and its use are 88 | covered by this License. 89 | - b) Accompany the Combined Work with a copy of the GNU GPL and this 90 | license document. 91 | - c) For a Combined Work that displays copyright notices during 92 | execution, include the copyright notice for the Library among 93 | these notices, as well as a reference directing the user to the 94 | copies of the GNU GPL and this license document. 95 | - d) Do one of the following: 96 | - 0) Convey the Minimal Corresponding Source under the terms of 97 | this License, and the Corresponding Application Code in a form 98 | suitable for, and under terms that permit, the user to 99 | recombine or relink the Application with a modified version of 100 | the Linked Version to produce a modified Combined Work, in the 101 | manner specified by section 6 of the GNU GPL for conveying 102 | Corresponding Source. 103 | - 1) Use a suitable shared library mechanism for linking with 104 | the Library. A suitable mechanism is one that (a) uses at run 105 | time a copy of the Library already present on the user's 106 | computer system, and (b) will operate properly with a modified 107 | version of the Library that is interface-compatible with the 108 | Linked Version. 109 | - e) Provide Installation Information, but only if you would 110 | otherwise be required to provide such information under section 6 111 | of the GNU GPL, and only to the extent that such information is 112 | necessary to install and execute a modified version of the 113 | Combined Work produced by recombining or relinking the Application 114 | with a modified version of the Linked Version. (If you use option 115 | 4d0, the Installation Information must accompany the Minimal 116 | Corresponding Source and Corresponding Application Code. If you 117 | use option 4d1, you must provide the Installation Information in 118 | the manner specified by section 6 of the GNU GPL for conveying 119 | Corresponding Source.) 120 | 121 | #### 5. Combined Libraries. 122 | 123 | You may place library facilities that are a work based on the Library 124 | side by side in a single library together with other library 125 | facilities that are not Applications and are not covered by this 126 | License, and convey such a combined library under terms of your 127 | choice, if you do both of the following: 128 | 129 | - a) Accompany the combined library with a copy of the same work 130 | based on the Library, uncombined with any other library 131 | facilities, conveyed under the terms of this License. 132 | - b) Give prominent notice with the combined library that part of it 133 | is a work based on the Library, and explaining where to find the 134 | accompanying uncombined form of the same work. 135 | 136 | #### 6. Revised Versions of the GNU Lesser General Public License. 137 | 138 | The Free Software Foundation may publish revised and/or new versions 139 | of the GNU Lesser General Public License from time to time. Such new 140 | versions will be similar in spirit to the present version, but may 141 | differ in detail to address new problems or concerns. 142 | 143 | Each version is given a distinguishing version number. If the Library 144 | as you received it specifies that a certain numbered version of the 145 | GNU Lesser General Public License "or any later version" applies to 146 | it, you have the option of following the terms and conditions either 147 | of that published version or of any later version published by the 148 | Free Software Foundation. If the Library as you received it does not 149 | specify a version number of the GNU Lesser General Public License, you 150 | may choose any version of the GNU Lesser General Public License ever 151 | published by the Free Software Foundation. 152 | 153 | If the Library as you received it specifies that a proxy can decide 154 | whether future versions of the GNU Lesser General Public License shall 155 | apply, that proxy's public statement of acceptance of any version is 156 | permanent authorization for you to choose that version for the 157 | Library. 158 | -------------------------------------------------------------------------------- /src/stroke.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later OR MPL-2.0 2 | // This file is a part of `piet-hardware`. 3 | // 4 | // `piet-hardware` is free software: you can redistribute it and/or modify it under the 5 | // terms of either: 6 | // 7 | // * GNU Lesser General Public License as published by the Free Software Foundation, either 8 | // version 3 of the License, or (at your option) any later version. 9 | // * Mozilla Public License as published by the Mozilla Foundation, version 2. 10 | // 11 | // `piet-hardware` is distributed in the hope that it will be useful, but WITHOUT ANY 12 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 13 | // PURPOSE. See the GNU Lesser General Public License or the Mozilla Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License and the Mozilla 17 | // Public License along with `piet-hardware`. If not, see . 18 | 19 | //! Advanced stroke rasterization using `zeno`. 20 | 21 | use crate::FillRule; 22 | use kurbo::{BezPath, PathEl, Shape}; 23 | use piet::StrokeStyle; 24 | use zeno::{Command, PathData, Scratch}; 25 | 26 | /// The buffers for stroking a path. 27 | pub(crate) struct StrokeBuffer { 28 | /// The scratch buffer for rendering. 29 | scratch: Scratch, 30 | 31 | /// Buffer for rendering the `kurbo` path. 32 | input_buffer: BezPath, 33 | 34 | /// Buffer for rendering the stroked path. 35 | output_buffer: Vec, 36 | 37 | /// The second output buffer for rendering the stroked path. 38 | kurbo_output: BezPath, 39 | 40 | /// The fill rule to use when stroking. 41 | fill_rule: FillRule, 42 | 43 | /// Dash length buffer. 44 | dashes: Vec, 45 | } 46 | 47 | impl Default for StrokeBuffer { 48 | fn default() -> Self { 49 | Self::new() 50 | } 51 | } 52 | 53 | impl StrokeBuffer { 54 | /// Create a new `StrokeBuffer`. 55 | pub(crate) fn new() -> Self { 56 | Self { 57 | scratch: Scratch::new(), 58 | input_buffer: BezPath::new(), 59 | output_buffer: Vec::new(), 60 | kurbo_output: BezPath::new(), 61 | fill_rule: FillRule::NonZero, 62 | dashes: Vec::new(), 63 | } 64 | } 65 | 66 | /// Render the stroked path. 67 | pub(crate) fn render_into( 68 | &mut self, 69 | shape: impl Shape, 70 | width: f64, 71 | style: &StrokeStyle, 72 | tolerance: f64, 73 | ) { 74 | // Adapting `Shape` to `zeno::PathData` requires the inner iterator to be `Clone`. 75 | // Since this can't be guaranteed by arbitrary shapes, we need to collect it into a 76 | // BezPath. 77 | // 78 | // TODO: Once specialization is stable in Rust, we can specialize so that we can use 79 | // the `Shape` if its iterator is `Clone`, and fall back to collecting into a `BezPath` 80 | // otherwise. 81 | self.input_buffer.truncate(0); 82 | self.input_buffer.extend(shape.path_elements(tolerance)); 83 | 84 | // Set up the zeno path style. 85 | let stroke = { 86 | let mut stroke = zeno::Stroke::new(width as f32); 87 | stroke.cap(match style.line_cap { 88 | piet::LineCap::Butt => zeno::Cap::Butt, 89 | piet::LineCap::Round => zeno::Cap::Round, 90 | piet::LineCap::Square => zeno::Cap::Square, 91 | }); 92 | 93 | let join_style = match style.line_join { 94 | piet::LineJoin::Bevel => zeno::Join::Bevel, 95 | piet::LineJoin::Miter { limit } => { 96 | stroke.miter_limit(limit as f32); 97 | zeno::Join::Miter 98 | } 99 | piet::LineJoin::Round => zeno::Join::Round, 100 | }; 101 | stroke.join(join_style); 102 | 103 | // Set up the dash pattern. 104 | self.dashes.clear(); 105 | self.dashes 106 | .extend(style.dash_pattern.iter().map(|&d| d as f32)); 107 | stroke.dash(&self.dashes, style.dash_offset as f32); 108 | 109 | stroke 110 | }; 111 | 112 | // Render the stroked path. 113 | self.output_buffer.clear(); 114 | let fill_rule = self.scratch.apply( 115 | &KurboShapeAsZenoPathData { 116 | shape: &self.input_buffer, 117 | tolerance, 118 | }, 119 | stroke, 120 | None, 121 | &mut self.output_buffer, 122 | ); 123 | 124 | // Convert the zeno path to a kurbo path. 125 | // TODO: Figure out how to make this part unnecessary. 126 | self.kurbo_output.truncate(0); 127 | self.kurbo_output 128 | .extend(self.output_buffer.iter().map(|&cmd| cvt_command(cmd))); 129 | 130 | // Convert the fill rule. 131 | self.fill_rule = match fill_rule { 132 | zeno::Fill::EvenOdd => FillRule::EvenOdd, 133 | zeno::Fill::NonZero => FillRule::NonZero, 134 | }; 135 | } 136 | 137 | /// Get the latest output buffer. 138 | pub(crate) fn output_buffer(&self) -> &BezPath { 139 | &self.kurbo_output 140 | } 141 | 142 | /// Get the latest fill rule. 143 | pub(crate) fn fill_rule(&self) -> FillRule { 144 | self.fill_rule 145 | } 146 | } 147 | 148 | /// Represent a `kurbo::Shape` as a `zeno::PathData`. 149 | struct KurboShapeAsZenoPathData<'a, S> { 150 | /// The shape to render. 151 | shape: &'a S, 152 | 153 | /// The tolerance to use when rendering the shape. 154 | tolerance: f64, 155 | } 156 | 157 | impl<'a, S: Shape> PathData for KurboShapeAsZenoPathData<'a, S> 158 | where 159 | S::PathElementsIter<'a>: Clone, 160 | { 161 | type Commands = PathElToCommandIter>; 162 | 163 | fn commands(&self) -> Self::Commands { 164 | PathElToCommandIter(self.shape.path_elements(self.tolerance)) 165 | } 166 | } 167 | 168 | /// Iterator that converts `PathEl` to `Command`. 169 | #[derive(Clone)] 170 | struct PathElToCommandIter(I); 171 | 172 | impl> Iterator for PathElToCommandIter { 173 | type Item = Command; 174 | 175 | fn next(&mut self) -> Option { 176 | self.0.next().map(cvt_path_el) 177 | } 178 | 179 | fn size_hint(&self) -> (usize, Option) { 180 | self.0.size_hint() 181 | } 182 | 183 | fn last(self) -> Option { 184 | self.0.last().map(cvt_path_el) 185 | } 186 | 187 | fn fold(self, init: B, mut f: F) -> B 188 | where 189 | F: FnMut(B, Self::Item) -> B, 190 | { 191 | self.0.fold(init, move |acc, el| f(acc, cvt_path_el(el))) 192 | } 193 | 194 | fn all(&mut self, mut f: F) -> bool 195 | where 196 | F: FnMut(Self::Item) -> bool, 197 | { 198 | self.0.all(|el| f(cvt_path_el(el))) 199 | } 200 | 201 | fn any(&mut self, mut f: F) -> bool 202 | where 203 | F: FnMut(Self::Item) -> bool, 204 | { 205 | self.0.any(|el| f(cvt_path_el(el))) 206 | } 207 | 208 | fn collect>(self) -> B { 209 | self.0.map(cvt_path_el).collect() 210 | } 211 | } 212 | 213 | /// Convert a `kurbo::PathEl` into a `zeno::Command`. 214 | fn cvt_path_el(el: PathEl) -> Command { 215 | match el { 216 | PathEl::MoveTo(p) => Command::MoveTo(cvt_point(p)), 217 | PathEl::LineTo(p) => Command::LineTo(cvt_point(p)), 218 | PathEl::QuadTo(p1, p2) => Command::QuadTo(cvt_point(p1), cvt_point(p2)), 219 | PathEl::CurveTo(p1, p2, p3) => { 220 | Command::CurveTo(cvt_point(p1), cvt_point(p2), cvt_point(p3)) 221 | } 222 | PathEl::ClosePath => Command::Close, 223 | } 224 | } 225 | 226 | /// Convert a `zeno::Command` to a `kurbo::PathEl`. 227 | fn cvt_command(cmd: Command) -> PathEl { 228 | match cmd { 229 | Command::MoveTo(p) => PathEl::MoveTo(cvt_vector(p)), 230 | Command::LineTo(p) => PathEl::LineTo(cvt_vector(p)), 231 | Command::QuadTo(p1, p2) => PathEl::QuadTo(cvt_vector(p1), cvt_vector(p2)), 232 | Command::CurveTo(p1, p2, p3) => { 233 | PathEl::CurveTo(cvt_vector(p1), cvt_vector(p2), cvt_vector(p3)) 234 | } 235 | Command::Close => PathEl::ClosePath, 236 | } 237 | } 238 | 239 | /// Convert a `kurbo::Point` into a `zeno::Vector`. 240 | fn cvt_point(p: kurbo::Point) -> zeno::Vector { 241 | zeno::Vector::new(p.x as f32, p.y as f32) 242 | } 243 | 244 | /// Convert a `zeno::Vector` into a `kurbo::Point`. 245 | fn cvt_vector(v: zeno::Vector) -> kurbo::Point { 246 | kurbo::Point::new(v.x as f64, v.y as f64) 247 | } 248 | -------------------------------------------------------------------------------- /src/resources.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later OR MPL-2.0 2 | // This file is a part of `piet-hardware`. 3 | // 4 | // `piet-hardware` is free software: you can redistribute it and/or modify it under the 5 | // terms of either: 6 | // 7 | // * GNU Lesser General Public License as published by the Free Software Foundation, either 8 | // version 3 of the License, or (at your option) any later version. 9 | // * Mozilla Public License as published by the Mozilla Foundation, version 2. 10 | // 11 | // `piet-hardware` is distributed in the hope that it will be useful, but WITHOUT ANY 12 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 13 | // PURPOSE. See the GNU Lesser General Public License or the Mozilla Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License and the Mozilla 17 | // Public License along with `piet-hardware`. If not, see . 18 | 19 | //! Defines useful resource wrappers. 20 | 21 | use super::gpu_backend::{GpuContext, RepeatStrategy, Vertex}; 22 | 23 | use std::fmt; 24 | 25 | use piet::kurbo::{Size, Vec2}; 26 | use piet::{ 27 | Error as Pierror, FixedLinearGradient, FixedRadialGradient, GradientStop, InterpolationMode, 28 | }; 29 | use tiny_skia::{Paint, Pixmap, Shader}; 30 | 31 | macro_rules! define_resource_wrappers { 32 | ($($name:ident($res:ident)),* $(,)?) => { 33 | $( 34 | pub(crate) struct $name { 35 | resource: C::$res, 36 | } 37 | 38 | impl fmt::Debug for $name { 39 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 40 | f.debug_struct(stringify!($name)) 41 | .finish_non_exhaustive() 42 | } 43 | } 44 | 45 | impl $name { 46 | pub(crate) fn from_raw(resource: C::$res) -> Self { 47 | Self { resource } 48 | } 49 | 50 | pub(crate) fn resource(&self) -> &C::$res { 51 | &self.resource 52 | } 53 | } 54 | )* 55 | }; 56 | } 57 | 58 | define_resource_wrappers! { 59 | Texture(Texture), 60 | VertexBuffer(VertexBuffer), 61 | } 62 | 63 | impl Texture { 64 | pub(crate) fn new( 65 | context: &mut C, 66 | device: &C::Device, 67 | interpolation: InterpolationMode, 68 | repeat: RepeatStrategy, 69 | ) -> Result { 70 | let resource = context.create_texture(device, interpolation, repeat)?; 71 | 72 | Ok(Self::from_raw(resource)) 73 | } 74 | 75 | pub(crate) fn write_linear_gradient( 76 | &self, 77 | context: &mut C, 78 | device: &C::Device, 79 | queue: &C::Queue, 80 | gradient: &FixedLinearGradient, 81 | size: Size, 82 | offset: Vec2, 83 | ) -> Result<(), Pierror> { 84 | let shader = tiny_skia::LinearGradient::new( 85 | convert_to_ts_point(gradient.start), 86 | convert_to_ts_point(gradient.end), 87 | gradient 88 | .stops 89 | .iter() 90 | .map(convert_to_ts_gradient_stop) 91 | .collect(), 92 | tiny_skia::SpreadMode::Pad, 93 | tiny_skia::Transform::from_translate(offset.x as f32, offset.y as f32), 94 | ) 95 | .ok_or_else(|| Pierror::BackendError("Invalid error".into()))?; 96 | 97 | self.write_shader(context, device, queue, shader, size); 98 | 99 | Ok(()) 100 | } 101 | 102 | pub(crate) fn write_radial_gradient( 103 | &self, 104 | context: &mut C, 105 | device: &C::Device, 106 | queue: &C::Queue, 107 | gradient: &FixedRadialGradient, 108 | size: Size, 109 | offset: Vec2, 110 | ) -> Result<(), Pierror> { 111 | let shader = tiny_skia::RadialGradient::new( 112 | convert_to_ts_point(gradient.center + gradient.origin_offset), 113 | convert_to_ts_point(gradient.center), 114 | gradient.radius as f32, 115 | gradient 116 | .stops 117 | .iter() 118 | .map(convert_to_ts_gradient_stop) 119 | .collect(), 120 | tiny_skia::SpreadMode::Pad, 121 | tiny_skia::Transform::from_translate(offset.x as f32, offset.y as f32), 122 | ) 123 | .ok_or_else(|| Pierror::BackendError("Invalid error".into()))?; 124 | 125 | self.write_shader(context, device, queue, shader, size); 126 | 127 | Ok(()) 128 | } 129 | 130 | pub(crate) fn write_shader( 131 | &self, 132 | context: &mut C, 133 | device: &C::Device, 134 | queue: &C::Queue, 135 | shader: Shader<'_>, 136 | mut size: Size, 137 | ) { 138 | // Pad the size out to at least one. 139 | if (size.width as isize) < 1 { 140 | size.width = 1.0; 141 | } 142 | if (size.height as isize) < 1 { 143 | size.height = 1.0; 144 | } 145 | 146 | // Create a pixmap to render the shader into. 147 | let mut pixmap = 148 | Pixmap::new(size.width as _, size.height as _).expect("failed to create pixmap"); 149 | 150 | // Render the shader into the pixmap. 151 | let paint = Paint { 152 | shader, 153 | ..Default::default() 154 | }; 155 | pixmap.fill_rect( 156 | tiny_skia::Rect::from_xywh(0.0, 0.0, size.width as _, size.height as _).unwrap(), 157 | &paint, 158 | tiny_skia::Transform::identity(), 159 | None, 160 | ); 161 | 162 | // Write the pixmap into the texture. 163 | let data = pixmap.take(); 164 | self.write_texture( 165 | context, 166 | device, 167 | queue, 168 | (size.width as _, size.height as _), 169 | piet::ImageFormat::RgbaPremul, 170 | Some(&data), 171 | ); 172 | self.set_interpolation(context, device, InterpolationMode::Bilinear); 173 | } 174 | 175 | pub(crate) fn write_texture( 176 | &self, 177 | context: &mut C, 178 | device: &C::Device, 179 | queue: &C::Queue, 180 | size: (u32, u32), 181 | format: piet::ImageFormat, 182 | data: Option<&[u8]>, 183 | ) { 184 | context.write_texture(crate::gpu_backend::TextureWrite { 185 | device, 186 | queue, 187 | size, 188 | format, 189 | data, 190 | texture: &self.resource, 191 | }); 192 | } 193 | 194 | #[allow(clippy::too_many_arguments)] 195 | pub(crate) fn write_subtexture( 196 | &self, 197 | context: &mut C, 198 | device: &C::Device, 199 | queue: &C::Queue, 200 | offset: (u32, u32), 201 | size: (u32, u32), 202 | format: piet::ImageFormat, 203 | data: &[u8], 204 | ) { 205 | context.write_subtexture(crate::gpu_backend::SubtextureWrite { 206 | device, 207 | queue, 208 | offset, 209 | size, 210 | format, 211 | data, 212 | texture: &self.resource, 213 | }); 214 | } 215 | 216 | pub(crate) fn set_interpolation( 217 | &self, 218 | context: &mut C, 219 | device: &C::Device, 220 | interpolation: InterpolationMode, 221 | ) { 222 | context.set_texture_interpolation(device, self.resource(), interpolation); 223 | } 224 | } 225 | 226 | impl VertexBuffer { 227 | pub(crate) fn new(context: &mut C, device: &C::Device) -> Result { 228 | let resource = context.create_vertex_buffer(device)?; 229 | Ok(Self::from_raw(resource)) 230 | } 231 | 232 | pub(crate) fn upload( 233 | &self, 234 | context: &mut C, 235 | device: &C::Device, 236 | queue: &C::Queue, 237 | data: &[Vertex], 238 | indices: &[u32], 239 | ) { 240 | context.write_vertices(device, queue, self.resource(), data, indices) 241 | } 242 | } 243 | 244 | pub(crate) fn convert_to_ts_point(point: piet::kurbo::Point) -> tiny_skia::Point { 245 | tiny_skia::Point { 246 | x: point.x as f32, 247 | y: point.y as f32, 248 | } 249 | } 250 | 251 | pub(crate) fn convert_to_ts_color(color: piet::Color) -> tiny_skia::Color { 252 | let (r, g, b, a) = color.as_rgba(); 253 | 254 | tiny_skia::Color::from_rgba(r as f32, g as f32, b as f32, a as f32).expect("Invalid color") 255 | } 256 | 257 | pub(crate) fn convert_to_ts_gradient_stop(grad_stop: &GradientStop) -> tiny_skia::GradientStop { 258 | tiny_skia::GradientStop::new(grad_stop.pos, convert_to_ts_color(grad_stop.color)) 259 | } 260 | -------------------------------------------------------------------------------- /src/atlas.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later OR MPL-2.0 2 | // This file is a part of `piet-hardware`. 3 | // 4 | // `piet-hardware` is free software: you can redistribute it and/or modify it under the 5 | // terms of either: 6 | // 7 | // * GNU Lesser General Public License as published by the Free Software Foundation, either 8 | // version 3 of the License, or (at your option) any later version. 9 | // * Mozilla Public License as published by the Mozilla Foundation, version 2. 10 | // 11 | // `piet-hardware` is distributed in the hope that it will be useful, but WITHOUT ANY 12 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 13 | // PURPOSE. See the GNU Lesser General Public License or the Mozilla Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License and the Mozilla 17 | // Public License along with `piet-hardware`. If not, see . 18 | 19 | //! The text atlas, which is used to cache glyphs. 20 | 21 | use super::gpu_backend::{GpuContext, RepeatStrategy}; 22 | use super::resources::Texture; 23 | use super::ResultExt; 24 | 25 | use ahash::RandomState; 26 | use cosmic_text::{CacheKey, FontSystem, LayoutGlyph, Placement, SwashCache, SwashContent}; 27 | use etagere::{Allocation, AtlasAllocator}; 28 | use hashbrown::hash_map::{Entry, HashMap}; 29 | 30 | use piet::kurbo::{Point, Rect, Size}; 31 | use piet::{Error as Pierror, InterpolationMode}; 32 | 33 | use std::rc::Rc; 34 | 35 | /// The atlas, combining all of the glyphs into a single texture. 36 | pub(crate) struct Atlas { 37 | /// The texture atlas. 38 | texture: Rc>, 39 | 40 | /// The size of the texture atlas. 41 | size: (u32, u32), 42 | 43 | /// The allocator for the texture atlas. 44 | allocator: AtlasAllocator, 45 | 46 | /// The hash map between the glyphs used and the texture allocation. 47 | glyphs: HashMap, 48 | 49 | /// The cache for the swash layout. 50 | swash_cache: SwashCache, 51 | } 52 | 53 | /// The data needed for rendering a glyph. 54 | pub(crate) struct GlyphData { 55 | /// The UV rectangle for the glyph. 56 | pub(crate) uv_rect: Rect, 57 | 58 | /// The size of the glyph. 59 | pub(crate) size: Size, 60 | 61 | /// The offset at which to draw the glyph. 62 | pub(crate) offset: Point, 63 | } 64 | 65 | /// The positioning of a glyph in the atlas. 66 | struct Position { 67 | /// The allocation of the glyph in the atlas. 68 | allocation: Allocation, 69 | 70 | /// Placement of the glyph. 71 | placement: Placement, 72 | } 73 | 74 | impl Atlas { 75 | /// Create a new, empty texture atlas. 76 | pub(crate) fn new( 77 | context: &mut C, 78 | device: &C::Device, 79 | queue: &C::Queue, 80 | ) -> Result { 81 | let (max_width, max_height) = context.max_texture_size(device); 82 | let texture = Texture::new( 83 | context, 84 | device, 85 | InterpolationMode::Bilinear, 86 | RepeatStrategy::Color(piet::Color::TRANSPARENT), 87 | ) 88 | .piet_err()?; 89 | 90 | // Initialize the texture to be transparent. 91 | texture.write_texture( 92 | context, 93 | device, 94 | queue, 95 | (max_width, max_height), 96 | piet::ImageFormat::RgbaPremul, 97 | None, 98 | ); 99 | 100 | Ok(Atlas { 101 | texture: Rc::new(texture), 102 | size: (max_width, max_height), 103 | allocator: AtlasAllocator::new([max_width as i32, max_height as i32].into()), 104 | glyphs: HashMap::with_hasher(RandomState::new()), 105 | swash_cache: SwashCache::new(), 106 | }) 107 | } 108 | 109 | /// Get a reference to the inner texture. 110 | pub(crate) fn texture(&self) -> &Rc> { 111 | &self.texture 112 | } 113 | 114 | /// Get the UV rectangle for the given glyph. 115 | /// 116 | /// This function rasterizes the glyph if it isn't already cached. 117 | pub(crate) fn uv_rect( 118 | &mut self, 119 | context: &mut C, 120 | device: &C::Device, 121 | queue: &C::Queue, 122 | glyph: &LayoutGlyph, 123 | font_system: &mut FontSystem, 124 | ) -> Result { 125 | let alloc_to_rect = { 126 | let (width, height) = self.size; 127 | move |posn: &Position| { 128 | let alloc = &posn.allocation; 129 | 130 | let max_x = alloc.rectangle.min.x + posn.placement.width as i32; 131 | let max_y = alloc.rectangle.min.y + posn.placement.height as i32; 132 | 133 | let uv_rect = Rect::new( 134 | alloc.rectangle.min.x as f64 / width as f64, 135 | alloc.rectangle.min.y as f64 / height as f64, 136 | max_x as f64 / width as f64, 137 | max_y as f64 / height as f64, 138 | ); 139 | let offset = (posn.placement.left as f64, posn.placement.top as f64); 140 | let size = (posn.placement.width as f64, posn.placement.height as f64); 141 | 142 | GlyphData { 143 | uv_rect, 144 | size: size.into(), 145 | offset: offset.into(), 146 | } 147 | } 148 | }; 149 | 150 | // TODO: Scaling. 151 | let physical = glyph.physical((0.0, 0.0), 1.0); 152 | let key = physical.cache_key; 153 | 154 | match self.glyphs.entry(key) { 155 | Entry::Occupied(o) => { 156 | let alloc = o.get(); 157 | Ok(alloc_to_rect(alloc)) 158 | } 159 | 160 | Entry::Vacant(v) => { 161 | // Get the swash image. 162 | let sw_image = self 163 | .swash_cache 164 | .get_image_uncached(font_system, key) 165 | .ok_or_else(|| { 166 | Pierror::BackendError({ 167 | format!("Failed to outline glyph {}", glyph.glyph_id).into() 168 | }) 169 | })?; 170 | 171 | // Render it to a buffer. 172 | let mut buffer = vec![ 173 | 0u32; 174 | sw_image.placement.width as usize 175 | * sw_image.placement.height as usize 176 | ]; 177 | match sw_image.content { 178 | SwashContent::Color => { 179 | // Copy the color to the buffer. 180 | buffer 181 | .iter_mut() 182 | .zip(sw_image.data.chunks(4)) 183 | .for_each(|(buf, input)| { 184 | let color = 185 | u32::from_ne_bytes([input[0], input[1], input[2], input[3]]); 186 | *buf = color; 187 | }); 188 | } 189 | SwashContent::Mask => { 190 | // Copy the mask to the buffer. 191 | buffer 192 | .iter_mut() 193 | .zip(sw_image.data.iter()) 194 | .for_each(|(buf, input)| { 195 | let color = u32::from_ne_bytes([255, 255, 255, *input]); 196 | *buf = color; 197 | }); 198 | } 199 | content => { 200 | tracing::warn!("Unsupported swash content: {:?}", content); 201 | return Err(Pierror::NotSupported); 202 | } 203 | } 204 | 205 | let (width, height) = (sw_image.placement.width, sw_image.placement.height); 206 | 207 | // Find a place for it in the texture. 208 | let alloc = self 209 | .allocator 210 | .allocate([width as i32, height as i32].into()) 211 | .ok_or_else(|| { 212 | Pierror::BackendError("Failed to allocate glyph in texture atlas.".into()) 213 | })?; 214 | 215 | // Insert the glyph into the texture. 216 | self.texture.write_subtexture( 217 | context, 218 | device, 219 | queue, 220 | (alloc.rectangle.min.x as u32, alloc.rectangle.min.y as u32), 221 | (width, height), 222 | piet::ImageFormat::RgbaPremul, 223 | bytemuck::cast_slice::<_, u8>(&buffer), 224 | ); 225 | 226 | // Insert the allocation into the map. 227 | let alloc = v.insert(Position { 228 | allocation: alloc, 229 | placement: sw_image.placement, 230 | }); 231 | 232 | // Return the UV rectangle. 233 | Ok(alloc_to_rect(alloc)) 234 | } 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # This template contains all of the possible sections and their default values 2 | 3 | # Note that all fields that take a lint level have these possible values: 4 | # * deny - An error will be produced and the check will fail 5 | # * warn - A warning will be produced, but the check will not fail 6 | # * allow - No warning or error will be produced, though in some cases a note 7 | # will be 8 | 9 | # The values provided in this template are the default values that will be used 10 | # when any section or field is not specified in your own configuration 11 | 12 | # If 1 or more target triples (and optionally, target_features) are specified, 13 | # only the specified targets will be checked when running `cargo deny check`. 14 | # This means, if a particular package is only ever used as a target specific 15 | # dependency, such as, for example, the `nix` crate only being used via the 16 | # `target_family = "unix"` configuration, that only having windows targets in 17 | # this list would mean the nix crate, as well as any of its exclusive 18 | # dependencies not shared by any other crates, would be ignored, as the target 19 | # list here is effectively saying which targets you are building for. 20 | targets = [ 21 | # The triple can be any string, but only the target triples built in to 22 | # rustc (as of 1.40) can be checked against actual config expressions 23 | #{ triple = "x86_64-unknown-linux-musl" }, 24 | # You can also specify which target_features you promise are enabled for a 25 | # particular target. target_features are currently not validated against 26 | # the actual valid features supported by the target architecture. 27 | #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, 28 | ] 29 | 30 | # This section is considered when running `cargo deny check advisories` 31 | # More documentation for the advisories section can be found here: 32 | # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html 33 | [advisories] 34 | # The path where the advisory database is cloned/fetched into 35 | db-path = "~/.cargo/advisory-db" 36 | # The url(s) of the advisory databases to use 37 | db-urls = ["https://github.com/rustsec/advisory-db"] 38 | # The lint level for security vulnerabilities 39 | vulnerability = "deny" 40 | # The lint level for unmaintained crates 41 | unmaintained = "warn" 42 | # The lint level for crates that have been yanked from their source registry 43 | yanked = "warn" 44 | # The lint level for crates with security notices. Note that as of 45 | # 2019-12-17 there are no security notice advisories in 46 | # https://github.com/rustsec/advisory-db 47 | notice = "warn" 48 | # A list of advisory IDs to ignore. Note that ignored advisories will still 49 | # output a note when they are encountered. 50 | ignore = [ 51 | "RUSTSEC-2022-0048", 52 | ] 53 | # Threshold for security vulnerabilities, any vulnerability with a CVSS score 54 | # lower than the range specified will be ignored. Note that ignored advisories 55 | # will still output a note when they are encountered. 56 | # * None - CVSS Score 0.0 57 | # * Low - CVSS Score 0.1 - 3.9 58 | # * Medium - CVSS Score 4.0 - 6.9 59 | # * High - CVSS Score 7.0 - 8.9 60 | # * Critical - CVSS Score 9.0 - 10.0 61 | #severity-threshold = 62 | 63 | # If this is true, then cargo deny will use the git executable to fetch advisory database. 64 | # If this is false, then it uses a built-in git library. 65 | # Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support. 66 | # See Git Authentication for more information about setting up git authentication. 67 | #git-fetch-with-cli = true 68 | 69 | # This section is considered when running `cargo deny check licenses` 70 | # More documentation for the licenses section can be found here: 71 | # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html 72 | [licenses] 73 | # The lint level for crates which do not have a detectable license 74 | unlicensed = "deny" 75 | # List of explicitly allowed licenses 76 | # See https://spdx.org/licenses/ for list of possible licenses 77 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. 78 | allow = [ 79 | "MIT", 80 | "Apache-2.0", 81 | "LGPL-3.0", 82 | "MPL-2.0", 83 | "Unicode-DFS-2016", 84 | "ISC", 85 | "BSD-3-Clause", 86 | "BSD-2-Clause" 87 | #"Apache-2.0 WITH LLVM-exception", 88 | ] 89 | # List of explicitly disallowed licenses 90 | # See https://spdx.org/licenses/ for list of possible licenses 91 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. 92 | deny = [ 93 | #"Nokia", 94 | ] 95 | # Lint level for licenses considered copyleft 96 | copyleft = "allow" 97 | # Blanket approval or denial for OSI-approved or FSF Free/Libre licenses 98 | # * both - The license will be approved if it is both OSI-approved *AND* FSF 99 | # * either - The license will be approved if it is either OSI-approved *OR* FSF 100 | # * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF 101 | # * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved 102 | # * neither - This predicate is ignored and the default lint level is used 103 | allow-osi-fsf-free = "neither" 104 | # Lint level used when no other predicates are matched 105 | # 1. License isn't in the allow or deny lists 106 | # 2. License isn't copyleft 107 | # 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" 108 | default = "deny" 109 | # The confidence threshold for detecting a license from license text. 110 | # The higher the value, the more closely the license text must be to the 111 | # canonical license text of a valid SPDX license file. 112 | # [possible values: any between 0.0 and 1.0]. 113 | confidence-threshold = 0.8 114 | # Allow 1 or more licenses on a per-crate basis, so that particular licenses 115 | # aren't accepted for every possible crate as with the normal allow list 116 | exceptions = [ 117 | # Each entry is the crate and version constraint, and its specific allow 118 | # list 119 | #{ allow = ["Zlib"], name = "adler32", version = "*" }, 120 | ] 121 | 122 | # Some crates don't have (easily) machine readable licensing information, 123 | # adding a clarification entry for it allows you to manually specify the 124 | # licensing information 125 | #[[licenses.clarify]] 126 | # The name of the crate the clarification applies to 127 | #name = "ring" 128 | # The optional version constraint for the crate 129 | #version = "*" 130 | # The SPDX expression for the license requirements of the crate 131 | #expression = "MIT AND ISC AND OpenSSL" 132 | # One or more files in the crate's source used as the "source of truth" for 133 | # the license expression. If the contents match, the clarification will be used 134 | # when running the license check, otherwise the clarification will be ignored 135 | # and the crate will be checked normally, which may produce warnings or errors 136 | # depending on the rest of your configuration 137 | #license-files = [ 138 | # Each entry is a crate relative path, and the (opaque) hash of its contents 139 | #{ path = "LICENSE", hash = 0xbd0eed23 } 140 | #] 141 | 142 | [licenses.private] 143 | # If true, ignores workspace crates that aren't published, or are only 144 | # published to private registries. 145 | # To see how to mark a crate as unpublished (to the official registry), 146 | # visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. 147 | ignore = false 148 | # One or more private registries that you might publish crates to, if a crate 149 | # is only published to private registries, and ignore is true, the crate will 150 | # not have its license(s) checked 151 | registries = [ 152 | #"https://sekretz.com/registry 153 | ] 154 | 155 | # This section is considered when running `cargo deny check bans`. 156 | # More documentation about the 'bans' section can be found here: 157 | # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html 158 | [bans] 159 | # Lint level for when multiple versions of the same crate are detected 160 | multiple-versions = "warn" 161 | # Lint level for when a crate version requirement is `*` 162 | wildcards = "allow" 163 | # The graph highlighting used when creating dotgraphs for crates 164 | # with multiple versions 165 | # * lowest-version - The path to the lowest versioned duplicate is highlighted 166 | # * simplest-path - The path to the version with the fewest edges is highlighted 167 | # * all - Both lowest-version and simplest-path are used 168 | highlight = "all" 169 | # List of crates that are allowed. Use with care! 170 | allow = [ 171 | #{ name = "ansi_term", version = "=0.11.0" }, 172 | ] 173 | # List of crates to deny 174 | deny = [ 175 | # Each entry the name of a crate and a version range. If version is 176 | # not specified, all versions will be matched. 177 | #{ name = "ansi_term", version = "=0.11.0" }, 178 | # 179 | # Wrapper crates can optionally be specified to allow the crate when it 180 | # is a direct dependency of the otherwise banned crate 181 | #{ name = "ansi_term", version = "=0.11.0", wrappers = [] }, 182 | ] 183 | # Certain crates/versions that will be skipped when doing duplicate detection. 184 | skip = [ 185 | #{ name = "ansi_term", version = "=0.11.0" }, 186 | ] 187 | # Similarly to `skip` allows you to skip certain crates during duplicate 188 | # detection. Unlike skip, it also includes the entire tree of transitive 189 | # dependencies starting at the specified crate, up to a certain depth, which is 190 | # by default infinite 191 | skip-tree = [ 192 | #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, 193 | ] 194 | 195 | # This section is considered when running `cargo deny check sources`. 196 | # More documentation about the 'sources' section can be found here: 197 | # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html 198 | [sources] 199 | # Lint level for what to happen when a crate from a crate registry that is not 200 | # in the allow list is encountered 201 | unknown-registry = "warn" 202 | # Lint level for what to happen when a crate from a git repository that is not 203 | # in the allow list is encountered 204 | unknown-git = "warn" 205 | # List of URLs for allowed crate registries. Defaults to the crates.io index 206 | # if not specified. If it is specified but empty, no registries are allowed. 207 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 208 | # List of URLs for allowed Git repositories 209 | allow-git = [] 210 | -------------------------------------------------------------------------------- /src/brush.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later OR MPL-2.0 2 | // This file is a part of `piet-hardware`. 3 | // 4 | // `piet-hardware` is free software: you can redistribute it and/or modify it under the 5 | // terms of either: 6 | // 7 | // * GNU Lesser General Public License as published by the Free Software Foundation, either 8 | // version 3 of the License, or (at your option) any later version. 9 | // * Mozilla Public License as published by the Mozilla Foundation, version 2. 10 | // 11 | // `piet-hardware` is distributed in the hope that it will be useful, but WITHOUT ANY 12 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 13 | // PURPOSE. See the GNU Lesser General Public License or the Mozilla Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License and the Mozilla 17 | // Public License along with `piet-hardware`. If not, see . 18 | 19 | //! The brush types used by `piet-hardware`. 20 | 21 | use super::gpu_backend::{GpuContext, RepeatStrategy, Vertex}; 22 | use super::image::Image; 23 | use super::resources::{self, Texture}; 24 | use super::{RenderContext, ResultExt, UV_WHITE}; 25 | 26 | use piet::kurbo::{Affine, Circle, Point, Rect, Shape}; 27 | use piet::{Error as Pierror, FixedGradient, FixedLinearGradient, FixedRadialGradient}; 28 | 29 | use std::borrow::Cow; 30 | 31 | /// The brush type used by the GPU renderer. 32 | #[derive(Debug)] 33 | pub struct Brush(BrushInner); 34 | 35 | impl Clone for Brush { 36 | fn clone(&self) -> Self { 37 | Self(self.0.clone()) 38 | } 39 | } 40 | 41 | #[derive(Debug)] 42 | enum BrushInner { 43 | /// A solid color. 44 | Solid(piet::Color), 45 | 46 | /// A texture to apply. 47 | Texture { 48 | /// The image to apply. 49 | image: Image, 50 | 51 | /// The transformation to translate UV texture points by. 52 | transform: Affine, 53 | 54 | /// The fixed gradient to apply. 55 | gradient: FixedGradient, 56 | }, 57 | } 58 | 59 | impl piet::IntoBrush> for Brush { 60 | fn make_brush<'a>( 61 | &'a self, 62 | _piet: &mut RenderContext<'_, '_, '_, C>, 63 | _bbox: impl FnOnce() -> Rect, 64 | ) -> Cow<'a, as piet::RenderContext>::Brush> { 65 | Cow::Borrowed(self) 66 | } 67 | } 68 | 69 | impl Brush { 70 | /// Create a new solid brush. 71 | pub(crate) fn solid(color: piet::Color) -> Self { 72 | Self(BrushInner::Solid(color)) 73 | } 74 | 75 | /// Create a new brush from a linear gradient. 76 | pub(crate) fn linear_gradient( 77 | context: &mut C, 78 | device: &C::Device, 79 | queue: &C::Queue, 80 | gradient: FixedLinearGradient, 81 | ) -> Result { 82 | let texture = Texture::new( 83 | context, 84 | device, 85 | piet::InterpolationMode::Bilinear, 86 | RepeatStrategy::Clamp, 87 | ) 88 | .piet_err()?; 89 | 90 | let (gradient, transform) = straighten_gradient(gradient); 91 | let bounds = Rect::from_points(gradient.start, gradient.end); 92 | let offset = -bounds.origin().to_vec2(); 93 | texture.write_linear_gradient(context, device, queue, &gradient, bounds.size(), offset)?; 94 | Ok(Self::textured(texture, bounds.size(), transform, gradient)) 95 | } 96 | 97 | /// Create a new brush from a radial gradient. 98 | pub(crate) fn radial_gradient( 99 | context: &mut C, 100 | device: &C::Device, 101 | queue: &C::Queue, 102 | gradient: FixedRadialGradient, 103 | ) -> Result { 104 | let texture = Texture::new( 105 | context, 106 | device, 107 | piet::InterpolationMode::Bilinear, 108 | RepeatStrategy::Clamp, 109 | ) 110 | .piet_err()?; 111 | 112 | let bounds = Circle::new(gradient.center, gradient.radius).bounding_box(); 113 | let offset = -bounds.origin().to_vec2(); 114 | let transform = scale_and_offset(bounds.size(), bounds.origin()); 115 | 116 | texture.write_radial_gradient(context, device, queue, &gradient, bounds.size(), offset)?; 117 | Ok(Self::textured(texture, bounds.size(), transform, gradient)) 118 | } 119 | 120 | /// Create a new brush from a texture. 121 | fn textured( 122 | texture: Texture, 123 | size: kurbo::Size, 124 | transform: Affine, 125 | gradient: impl Into, 126 | ) -> Self { 127 | // Create a new image. 128 | let image = Image::new(texture, size); 129 | 130 | Self(BrushInner::Texture { 131 | image, 132 | transform, 133 | gradient: gradient.into(), 134 | }) 135 | } 136 | 137 | /// Get the texture associated with this brush. 138 | pub(crate) fn texture(&self, _size: (u32, u32)) -> Option<&Image> { 139 | match self.0 { 140 | BrushInner::Solid(_) => None, 141 | BrushInner::Texture { ref image, .. } => Some(image), 142 | } 143 | } 144 | 145 | /// Transform a two-dimensional point into a vertex using this brush. 146 | pub(crate) fn make_vertex(&self, point: [f32; 2]) -> Vertex { 147 | match self.0 { 148 | BrushInner::Solid(color) => Vertex { 149 | pos: point, 150 | uv: UV_WHITE, 151 | color: { 152 | let (r, g, b, a) = color.as_rgba8(); 153 | [r, g, b, a] 154 | }, 155 | }, 156 | 157 | BrushInner::Texture { transform, .. } => { 158 | let uv = transform * Point::new(point[0] as f64, point[1] as f64); 159 | Vertex { 160 | pos: point, 161 | uv: [uv.x as f32, uv.y as f32], 162 | color: [0xFF, 0xFF, 0xFF, 0xFF], 163 | } 164 | } 165 | } 166 | } 167 | 168 | pub(crate) fn to_shader(&self) -> Option> { 169 | match &self.0 { 170 | BrushInner::Solid(color) => Some(tiny_skia::Shader::SolidColor( 171 | resources::convert_to_ts_color(*color), 172 | )), 173 | BrushInner::Texture { 174 | gradient: FixedGradient::Linear(linear), 175 | .. 176 | } => tiny_skia::LinearGradient::new( 177 | resources::convert_to_ts_point(linear.start), 178 | resources::convert_to_ts_point(linear.end), 179 | linear 180 | .stops 181 | .iter() 182 | .map(resources::convert_to_ts_gradient_stop) 183 | .collect(), 184 | tiny_skia::SpreadMode::Pad, 185 | tiny_skia::Transform::identity(), 186 | ), 187 | BrushInner::Texture { 188 | gradient: FixedGradient::Radial(radial), 189 | .. 190 | } => tiny_skia::RadialGradient::new( 191 | resources::convert_to_ts_point(radial.center + radial.origin_offset), 192 | resources::convert_to_ts_point(radial.center), 193 | radial.radius as f32, 194 | radial 195 | .stops 196 | .iter() 197 | .map(resources::convert_to_ts_gradient_stop) 198 | .collect(), 199 | tiny_skia::SpreadMode::Pad, 200 | tiny_skia::Transform::identity(), 201 | ), 202 | } 203 | } 204 | } 205 | 206 | impl Clone for BrushInner { 207 | fn clone(&self) -> Self { 208 | match self { 209 | Self::Solid(color) => Self::Solid(*color), 210 | Self::Texture { 211 | image, 212 | transform, 213 | gradient, 214 | } => Self::Texture { 215 | image: image.clone(), 216 | transform: *transform, 217 | gradient: gradient.clone(), 218 | }, 219 | } 220 | } 221 | } 222 | 223 | /// Convert a gradient into either a horizontal or vertical gradient as well as 224 | /// a rotation that rotates the start/end points to their former positions. 225 | fn straighten_gradient(gradient: FixedLinearGradient) -> (FixedLinearGradient, Affine) { 226 | // If the gradient is already almost straight, then no need to do anything. 227 | if (gradient.start.x - gradient.end.x).abs() < 1.0 228 | || (gradient.start.y - gradient.end.y).abs() < 1.0 229 | { 230 | let mut bounds = Rect::from_points(gradient.start, gradient.end); 231 | if (bounds.width() as isize) < 0 { 232 | bounds.x1 += 1.0; 233 | } 234 | if (bounds.height() as isize) < 0 { 235 | bounds.y1 += 1.0; 236 | } 237 | 238 | return (gradient, scale_and_offset(bounds.size(), bounds.origin())); 239 | } 240 | 241 | // Get the angle and length between the start and end points. 242 | let (angle, length) = { 243 | let delta = gradient.end - gradient.start; 244 | (delta.angle(), delta.hypot()) 245 | }; 246 | 247 | // Create a horizontal line starting at the start point with the same length 248 | // as the original gradient. 249 | let horizontal_end = gradient.start + kurbo::Vec2::new(length, 0.0); 250 | 251 | // Use that to create a new gradient. 252 | let new_gradient = FixedLinearGradient { 253 | start: gradient.start, 254 | end: horizontal_end, 255 | stops: gradient.stops, 256 | }; 257 | 258 | // A transform that maps UV coordinates into this plane. 259 | let offset = gradient.start.to_vec2(); 260 | let new_bounds = Rect::from_points(new_gradient.start, new_gradient.end); 261 | let transform = scale_and_offset(new_bounds.size(), new_bounds.origin()) 262 | * Affine::translate(offset) 263 | * Affine::rotate(-angle) 264 | * Affine::translate(-offset); 265 | (new_gradient, transform) 266 | } 267 | 268 | fn scale_and_offset(size: kurbo::Size, offset: kurbo::Point) -> Affine { 269 | Affine::scale_non_uniform(1.0 / size.width, 1.0 / size.height) 270 | * Affine::translate(-offset.to_vec2()) 271 | } 272 | -------------------------------------------------------------------------------- /src/mask.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later OR MPL-2.0 2 | // This file is a part of `piet-hardware`. 3 | // 4 | // `piet-hardware` is free software: you can redistribute it and/or modify it under the 5 | // terms of either: 6 | // 7 | // * GNU Lesser General Public License as published by the Free Software Foundation, either 8 | // version 3 of the License, or (at your option) any later version. 9 | // * Mozilla Public License as published by the Mozilla Foundation, version 2. 10 | // 11 | // `piet-hardware` is distributed in the hope that it will be useful, but WITHOUT ANY 12 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 13 | // PURPOSE. See the GNU Lesser General Public License or the Mozilla Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License and the Mozilla 17 | // Public License along with `piet-hardware`. If not, see . 18 | 19 | //! The mask used for clipping. 20 | 21 | use super::gpu_backend::{GpuContext, RepeatStrategy}; 22 | use super::resources::Texture; 23 | use super::shape_to_skia_path; 24 | 25 | use piet::kurbo::Shape; 26 | use piet::InterpolationMode; 27 | 28 | use std::{fmt, mem}; 29 | 30 | use tiny_skia as ts; 31 | use ts::{FillRule, Mask as ClipMask, PathBuilder, PixmapMut}; 32 | 33 | // TODO: Adjust based on real-world data. 34 | const TOO_MANY_TEXTURES: usize = 1024; 35 | 36 | /// The context for creating and modifying masks. 37 | pub(crate) struct MaskContext { 38 | /// A scratch buffer for rendering masks into. 39 | mask_render_buffer: Vec, 40 | 41 | /// List of GPU textures to re-use. 42 | gpu_textures: Vec>, 43 | 44 | /// The GPU textures currently in use. 45 | used_textures: Vec>, 46 | 47 | /// Cached path builder for drawing into the mask. 48 | path_builder: PathBuilder, 49 | } 50 | 51 | struct SizedTexture { 52 | texture: Texture, 53 | size: (u32, u32), 54 | } 55 | 56 | /// A mask that can be clipped into. 57 | pub(crate) struct Mask { 58 | /// The underlying tiny-skia mask. 59 | mask: ClipMask, 60 | 61 | /// The current state of the mask. 62 | state: MaskState, 63 | } 64 | 65 | enum MaskState { 66 | /// The mask is empty. 67 | Empty, 68 | 69 | /// The mask has data and the texture has not been created yet. 70 | DirtyWithNoTexture, 71 | 72 | /// The mask has data but the texture is out of date. 73 | DirtyWithTexture(SizedTexture), 74 | 75 | /// The mask has data and the texture is up to date. 76 | Clean(SizedTexture), 77 | } 78 | 79 | impl MaskState { 80 | /// Move into the dirty state. 81 | fn dirty(&mut self) { 82 | let new = match mem::replace(self, Self::Empty) { 83 | Self::Empty => Self::DirtyWithNoTexture, 84 | Self::Clean(tex) => Self::DirtyWithTexture(tex), 85 | rem => rem, 86 | }; 87 | 88 | *self = new; 89 | } 90 | 91 | /// Is this mask dirty? 92 | fn is_dirty(&self) -> bool { 93 | matches!(self, Self::DirtyWithNoTexture | Self::DirtyWithTexture(_)) 94 | } 95 | 96 | /// Get a reference to the texture, if there is one. 97 | fn texture(&self) -> Option<&Texture> { 98 | match self { 99 | Self::Clean(tex) | Self::DirtyWithTexture(tex) => Some(&tex.texture), 100 | _ => None, 101 | } 102 | } 103 | 104 | /// Take the texture out. 105 | fn take_texture(&mut self) -> Option> { 106 | let (new, tex) = match mem::replace(self, Self::Empty) { 107 | Self::Clean(tex) => (Self::DirtyWithNoTexture, Some(tex)), 108 | Self::DirtyWithTexture(tex) => (Self::DirtyWithNoTexture, Some(tex)), 109 | rem => (rem, None), 110 | }; 111 | 112 | *self = new; 113 | tex 114 | } 115 | 116 | /// Tell whether this is `Empty`. 117 | fn is_empty(&self) -> bool { 118 | matches!(self, Self::Empty) 119 | } 120 | } 121 | 122 | impl MaskContext { 123 | /// Create a new, empty mask context. 124 | pub(crate) fn new() -> Self { 125 | Self { 126 | mask_render_buffer: Vec::new(), 127 | gpu_textures: Vec::new(), 128 | used_textures: Vec::new(), 129 | path_builder: PathBuilder::new(), 130 | } 131 | } 132 | 133 | /// Add a new path to a mask. 134 | pub(crate) fn add_path(&mut self, mask: &mut Mask, shape: impl Shape, tolerance: f64) { 135 | // Convert the shape to a tiny-skia path. 136 | let path = { 137 | let mut builder = mem::take(&mut self.path_builder); 138 | shape_to_skia_path(&mut builder, shape, tolerance); 139 | builder.finish().expect("path builder failed") 140 | }; 141 | 142 | if mask.state.is_empty() { 143 | // This is the first stroke, so fill the mask with the path. 144 | mask.mask 145 | .fill_path(&path, FillRule::EvenOdd, false, ts::Transform::identity()); 146 | } else { 147 | // This is an intersection, so intersect the path with the mask. 148 | mask.mask 149 | .intersect_path(&path, FillRule::EvenOdd, false, ts::Transform::identity()); 150 | } 151 | 152 | mask.state.dirty(); 153 | self.path_builder = path.clear(); 154 | } 155 | 156 | /// Get the texture for a mask. 157 | pub(crate) fn texture<'a>( 158 | &mut self, 159 | mask: &'a mut Mask, 160 | context: &mut C, 161 | device: &C::Device, 162 | queue: &C::Queue, 163 | ) -> &'a Texture { 164 | self.upload_mask(mask, context, device, queue); 165 | mask.state.texture().expect("mask texture") 166 | } 167 | 168 | /// Indicate that a texture has been used in this operation. 169 | /// 170 | /// This keeps us from re-using it by accident in the future. 171 | pub(crate) fn mark_used(&mut self, mask: &mut Mask) { 172 | self.used_textures.extend(mask.state.take_texture()) 173 | } 174 | 175 | /// Indicate that the GPU queue has been flushed. 176 | pub(crate) fn gpu_flushed(&mut self) { 177 | self.gpu_textures.append(&mut self.used_textures); 178 | 179 | // If we have *a lot* of masks, get rid of some of them. 180 | self.gpu_textures.truncate(TOO_MANY_TEXTURES); 181 | } 182 | 183 | /// Upload a mask into a texture. 184 | fn upload_mask( 185 | &mut self, 186 | mask: &mut Mask, 187 | context: &mut C, 188 | device: &C::Device, 189 | queue: &C::Queue, 190 | ) { 191 | if mask.state.is_empty() { 192 | unreachable!("uploading empty mask"); 193 | } 194 | 195 | if !mask.state.is_dirty() { 196 | // No need to change anything. 197 | return; 198 | } 199 | 200 | // Create a pixmap to render into, using our scratch space. 201 | // TODO: It would be nice to go right from the clip mask to the texture without using the 202 | // pixmap as an intermediary. 203 | let width = mask.mask.width(); 204 | let height = mask.mask.height(); 205 | self.mask_render_buffer 206 | .resize(width as usize * height as usize, 0); 207 | let mut pixmap = PixmapMut::from_bytes( 208 | bytemuck::cast_slice_mut(&mut self.mask_render_buffer), 209 | width, 210 | height, 211 | ) 212 | .expect("If the width/height is valid for the mask, it should work for the pixmap as well"); 213 | 214 | // Clear the pixmap with a black color. 215 | pixmap.fill(ts::Color::TRANSPARENT); 216 | 217 | // Render the mask into the pixmap. 218 | pixmap.fill_rect( 219 | ts::Rect::from_xywh(0., 0., width as f32, height as f32).expect("valid rect"), 220 | &ts::Paint { 221 | shader: ts::Shader::SolidColor(ts::Color::WHITE), 222 | ..Default::default() 223 | }, 224 | ts::Transform::identity(), 225 | Some(&mask.mask), 226 | ); 227 | 228 | // Either create a new GPU texture or re-use an older one. 229 | let texture = mask 230 | .state 231 | .take_texture() 232 | .or_else(|| { 233 | // Look for a texture with the same size. 234 | self.gpu_textures 235 | .iter() 236 | .rposition(|tex| tex.size == (width, height)) 237 | .map(|idx| self.gpu_textures.swap_remove(idx)) 238 | }) 239 | .unwrap_or_else(|| { 240 | let texture = Texture::new( 241 | context, 242 | device, 243 | InterpolationMode::Bilinear, 244 | RepeatStrategy::Color(piet::Color::TRANSPARENT), 245 | ) 246 | .expect("failed to create texture"); 247 | let size = (width, height); 248 | 249 | SizedTexture { texture, size } 250 | }); 251 | 252 | // Upload the pixmap to the texture. 253 | texture.texture.write_texture( 254 | context, 255 | device, 256 | queue, 257 | (width, height), 258 | piet::ImageFormat::RgbaSeparate, 259 | Some(pixmap.data_mut()), 260 | ); 261 | 262 | // Put the texture back. 263 | // 264 | // This also marks the texture as non-dirty. 265 | mask.state = MaskState::Clean(texture); 266 | } 267 | 268 | /// Reclaim the textures of a set of masks. 269 | pub(crate) fn reclaim(&mut self, masks: impl Iterator>) { 270 | // Take out all of the GPU textures. 271 | self.used_textures 272 | .extend(masks.flat_map(|mut mask| mask.state.take_texture())); 273 | } 274 | } 275 | 276 | impl Mask { 277 | /// Create a new mask with the given size. 278 | pub(crate) fn new(width: u32, height: u32) -> Self { 279 | Self { 280 | mask: ClipMask::new(width, height).expect("failed to create mask"), 281 | state: MaskState::Empty, 282 | } 283 | } 284 | } 285 | 286 | impl Clone for Mask { 287 | /// Makes a new copy without the cached texture. 288 | fn clone(&self) -> Self { 289 | Self { 290 | mask: self.mask.clone(), 291 | state: if self.state.is_empty() { 292 | MaskState::Empty 293 | } else { 294 | MaskState::DirtyWithNoTexture 295 | }, 296 | } 297 | } 298 | } 299 | 300 | impl fmt::Debug for Mask { 301 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 302 | f.debug_struct("Mask") 303 | .field("width", &self.mask.width()) 304 | .field("height", &self.mask.height()) 305 | .field("state", &self.state) 306 | .finish() 307 | } 308 | } 309 | 310 | impl fmt::Debug for MaskState { 311 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 312 | match self { 313 | Self::Empty => f.write_str("Empty"), 314 | Self::DirtyWithNoTexture => f.write_str("DirtyWithNoTexture"), 315 | Self::DirtyWithTexture(_) => f.write_str("DirtyWithTexture"), 316 | Self::Clean(_) => f.write_str("Clean"), 317 | } 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /src/gpu_backend.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later OR MPL-2.0 2 | // This file is a part of `piet-hardware`. 3 | // 4 | // `piet-hardware` is free software: you can redistribute it and/or modify it under the 5 | // terms of either: 6 | // 7 | // * GNU Lesser General Public License as published by the Free Software Foundation, either 8 | // version 3 of the License, or (at your option) any later version. 9 | // * Mozilla Public License as published by the Mozilla Foundation, version 2. 10 | // 11 | // `piet-hardware` is distributed in the hope that it will be useful, but WITHOUT ANY 12 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 13 | // PURPOSE. See the GNU Lesser General Public License or the Mozilla Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License and the Mozilla 17 | // Public License along with `piet-hardware`. If not, see . 18 | 19 | //! Defines the GPU backend for piet-hardware. 20 | 21 | use piet::kurbo::{Affine, Rect}; 22 | use piet::InterpolationMode; 23 | 24 | use std::error::Error; 25 | 26 | /// The backend for the GPU renderer. 27 | pub trait GpuContext { 28 | /// A "device" that can be used to render. 29 | /// 30 | /// This corresponds to [`Device`] in [`wgpu`] and nothing in particular in [`glow`]. 31 | /// 32 | /// [`Device`]: wgpu::Device 33 | /// [`wgpu`]: https://crates.io/crates/wgpu 34 | /// [`glow`]: https://crates.io/crates/glow 35 | type Device; 36 | 37 | /// A "queue" that can be used to render. 38 | /// 39 | /// This corresponds to [`Queue`] in [`wgpu`] and nothing in particular in [`glow`]. 40 | /// 41 | /// [`Queue`]: wgpu::Queue 42 | /// [`wgpu`]: https://crates.io/crates/wgpu 43 | /// [`glow`]: https://crates.io/crates/glow 44 | type Queue; 45 | 46 | /// The type associated with a GPU texture. 47 | type Texture; 48 | 49 | /// The type associated with a GPU vertex buffer. 50 | /// 51 | /// Contains vertices, indices and any layout data. 52 | type VertexBuffer; 53 | 54 | /// The error type associated with this GPU context. 55 | type Error: Error + 'static; 56 | 57 | /// Clear the screen with the given color. 58 | fn clear(&mut self, device: &Self::Device, queue: &Self::Queue, color: piet::Color); 59 | 60 | /// Flush the GPU commands. 61 | fn flush(&mut self) -> Result<(), Self::Error>; 62 | 63 | /// Create a new texture. 64 | fn create_texture( 65 | &mut self, 66 | device: &Self::Device, 67 | interpolation: InterpolationMode, 68 | repeat: RepeatStrategy, 69 | ) -> Result; 70 | 71 | /// Write an image to a texture. 72 | fn write_texture(&mut self, texture_write: TextureWrite<'_, Self>); 73 | 74 | /// Write a sub-image to a texture. 75 | fn write_subtexture(&mut self, subtexture_write: SubtextureWrite<'_, Self>); 76 | 77 | /// Set the interpolation mode for a texture. 78 | fn set_texture_interpolation( 79 | &mut self, 80 | device: &Self::Device, 81 | texture: &Self::Texture, 82 | interpolation: InterpolationMode, 83 | ); 84 | 85 | /// Get the maximum texture size. 86 | fn max_texture_size(&mut self, device: &Self::Device) -> (u32, u32); 87 | 88 | /// Create a new vertex buffer. 89 | fn create_vertex_buffer( 90 | &mut self, 91 | device: &Self::Device, 92 | ) -> Result; 93 | 94 | /// Write vertices to a vertex buffer. 95 | /// 96 | /// The indices must be valid for the vertices set; however, it is up to the GPU implementation 97 | /// to actually check this. 98 | fn write_vertices( 99 | &mut self, 100 | device: &Self::Device, 101 | queue: &Self::Queue, 102 | buffer: &Self::VertexBuffer, 103 | vertices: &[Vertex], 104 | indices: &[u32], 105 | ); 106 | 107 | /// Capture an area from the screen and put it into a texture. 108 | fn capture_area(&mut self, area_capture: AreaCapture<'_, Self>) -> Result<(), Self::Error>; 109 | 110 | /// Push buffer data to the GPU. 111 | /// 112 | /// The backend is expected to set up a renderer that renders data in `vertex_buffer`, 113 | /// using `current_texture` to fill the triangles and `mask_texture` to clip them. In addition, 114 | /// the parameters `transform`, `viewport_size` and `clip` are also expected to be used. 115 | fn push_buffers(&mut self, buffer_push: BufferPush<'_, Self>) -> Result<(), Self::Error>; 116 | } 117 | 118 | /// The data necessary to write an image into a texture. 119 | pub struct TextureWrite<'a, C: GpuContext + ?Sized> { 120 | /// The device to render onto. 121 | pub device: &'a C::Device, 122 | 123 | /// The queue to push the operation into. 124 | pub queue: &'a C::Queue, 125 | 126 | /// The texture to write into. 127 | pub texture: &'a C::Texture, 128 | 129 | /// The size of the image. 130 | pub size: (u32, u32), 131 | 132 | /// The format of the image. 133 | pub format: piet::ImageFormat, 134 | 135 | /// The data to write. 136 | /// 137 | /// This is `None` if you want to write only zeroes. 138 | pub data: Option<&'a [u8]>, 139 | } 140 | 141 | /// The data necessary to write an image into a portion of a texture. 142 | pub struct SubtextureWrite<'a, C: GpuContext + ?Sized> { 143 | /// The device to render onto. 144 | pub device: &'a C::Device, 145 | 146 | /// The queue to push the operation into. 147 | pub queue: &'a C::Queue, 148 | 149 | /// The texture to write into. 150 | pub texture: &'a C::Texture, 151 | 152 | /// The offset to start writing at. 153 | pub offset: (u32, u32), 154 | 155 | /// The size of the image. 156 | pub size: (u32, u32), 157 | 158 | /// The format of the image. 159 | pub format: piet::ImageFormat, 160 | 161 | /// The data to write. 162 | pub data: &'a [u8], 163 | } 164 | 165 | /// The data necessary to capture an area of the screen. 166 | pub struct AreaCapture<'a, C: GpuContext + ?Sized> { 167 | /// The device to render onto. 168 | pub device: &'a C::Device, 169 | 170 | /// The queue to push the operation into. 171 | pub queue: &'a C::Queue, 172 | 173 | /// The texture to write into. 174 | pub texture: &'a C::Texture, 175 | 176 | /// The offset to start at. 177 | pub offset: (u32, u32), 178 | 179 | /// The size of the rectangle to capture. 180 | pub size: (u32, u32), 181 | 182 | /// The current bitmap scale. 183 | pub bitmap_scale: f64, 184 | } 185 | 186 | /// The data necessary to push buffer data to the GPU. 187 | pub struct BufferPush<'a, C: GpuContext + ?Sized> { 188 | /// The device to render onto. 189 | pub device: &'a C::Device, 190 | 191 | /// The queue to push the operation into. 192 | pub queue: &'a C::Queue, 193 | 194 | /// The vertex buffer to use when pushing data. 195 | pub vertex_buffer: &'a C::VertexBuffer, 196 | 197 | /// The texture to use as the background. 198 | pub current_texture: &'a C::Texture, 199 | 200 | /// The texture to use as the mask. 201 | pub mask_texture: &'a C::Texture, 202 | 203 | /// The transformation to apply to the vertices. 204 | pub transform: &'a Affine, 205 | 206 | /// The size of the viewport. 207 | pub viewport_size: (u32, u32), 208 | 209 | /// The rectangle to clip vertices to. 210 | /// 211 | /// This is sometimes known as the "scissor rect". 212 | pub clip: Option, 213 | } 214 | 215 | impl GpuContext for &mut C { 216 | type Device = C::Device; 217 | type Queue = C::Queue; 218 | type Texture = C::Texture; 219 | type VertexBuffer = C::VertexBuffer; 220 | type Error = C::Error; 221 | 222 | fn capture_area(&mut self, area_capture: AreaCapture<'_, Self>) -> Result<(), Self::Error> { 223 | // Convert &C to C 224 | let AreaCapture { 225 | device, 226 | queue, 227 | texture, 228 | offset, 229 | size, 230 | bitmap_scale, 231 | } = area_capture; 232 | 233 | (**self).capture_area(AreaCapture { 234 | device, 235 | queue, 236 | texture, 237 | offset, 238 | size, 239 | bitmap_scale, 240 | }) 241 | } 242 | 243 | fn clear(&mut self, device: &Self::Device, queue: &Self::Queue, color: piet::Color) { 244 | (**self).clear(device, queue, color) 245 | } 246 | 247 | fn create_texture( 248 | &mut self, 249 | device: &Self::Device, 250 | interpolation: InterpolationMode, 251 | repeat: RepeatStrategy, 252 | ) -> Result { 253 | (**self).create_texture(device, interpolation, repeat) 254 | } 255 | 256 | fn create_vertex_buffer( 257 | &mut self, 258 | device: &Self::Device, 259 | ) -> Result { 260 | (**self).create_vertex_buffer(device) 261 | } 262 | 263 | fn flush(&mut self) -> Result<(), Self::Error> { 264 | (**self).flush() 265 | } 266 | 267 | fn max_texture_size(&mut self, device: &Self::Device) -> (u32, u32) { 268 | (**self).max_texture_size(device) 269 | } 270 | 271 | fn push_buffers(&mut self, buffer_push: BufferPush<'_, Self>) -> Result<(), Self::Error> { 272 | // C and &C are different types, so make sure to convert. 273 | let BufferPush { 274 | device, 275 | queue, 276 | vertex_buffer, 277 | current_texture, 278 | mask_texture, 279 | transform, 280 | viewport_size, 281 | clip, 282 | } = buffer_push; 283 | 284 | (**self).push_buffers(BufferPush { 285 | device, 286 | queue, 287 | vertex_buffer, 288 | current_texture, 289 | mask_texture, 290 | transform, 291 | viewport_size, 292 | clip, 293 | }) 294 | } 295 | 296 | fn set_texture_interpolation( 297 | &mut self, 298 | device: &Self::Device, 299 | texture: &Self::Texture, 300 | interpolation: InterpolationMode, 301 | ) { 302 | (**self).set_texture_interpolation(device, texture, interpolation) 303 | } 304 | 305 | fn write_subtexture(&mut self, subtexture_write: SubtextureWrite<'_, Self>) { 306 | // Convert type from &C to C 307 | let SubtextureWrite { 308 | device, 309 | queue, 310 | texture, 311 | offset, 312 | size, 313 | format, 314 | data, 315 | } = subtexture_write; 316 | 317 | (**self).write_subtexture(SubtextureWrite { 318 | device, 319 | queue, 320 | texture, 321 | offset, 322 | size, 323 | format, 324 | data, 325 | }) 326 | } 327 | 328 | fn write_texture(&mut self, texture_write: TextureWrite<'_, Self>) { 329 | // Convert type from &C to C 330 | let TextureWrite { 331 | device, 332 | queue, 333 | texture, 334 | size, 335 | format, 336 | data, 337 | } = texture_write; 338 | 339 | (**self).write_texture(TextureWrite { 340 | device, 341 | queue, 342 | texture, 343 | size, 344 | format, 345 | data, 346 | }) 347 | } 348 | 349 | fn write_vertices( 350 | &mut self, 351 | device: &Self::Device, 352 | queue: &Self::Queue, 353 | buffer: &Self::VertexBuffer, 354 | vertices: &[Vertex], 355 | indices: &[u32], 356 | ) { 357 | (**self).write_vertices(device, queue, buffer, vertices, indices) 358 | } 359 | } 360 | 361 | /// The strategy to use for repeating. 362 | #[derive(Debug, Copy, Clone, PartialEq)] 363 | #[non_exhaustive] 364 | pub enum RepeatStrategy { 365 | /// Repeat the image. 366 | Repeat, 367 | 368 | /// Clamp to the edge of the image. 369 | Clamp, 370 | 371 | /// Don't repeat and instead use this color. 372 | Color(piet::Color), 373 | } 374 | 375 | /// The vertex type used by the GPU renderer. 376 | #[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Default, bytemuck::Pod, bytemuck::Zeroable)] 377 | #[repr(C)] 378 | pub struct Vertex { 379 | /// The position of the vertex. 380 | pub pos: [f32; 2], 381 | 382 | /// The coordinate of the vertex in the texture. 383 | pub uv: [f32; 2], 384 | 385 | /// The color of the vertex, in four SRGB channels. 386 | pub color: [u8; 4], 387 | } 388 | 389 | /// The type of the buffer to use. 390 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 391 | pub enum BufferType { 392 | /// The buffer is used for vertices. 393 | Vertex, 394 | 395 | /// The buffer is used for indices. 396 | Index, 397 | } 398 | -------------------------------------------------------------------------------- /src/rasterizer.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later OR MPL-2.0 2 | // This file is a part of `piet-hardware`. 3 | // 4 | // `piet-hardware` is free software: you can redistribute it and/or modify it under the 5 | // terms of either: 6 | // 7 | // * GNU Lesser General Public License as published by the Free Software Foundation, either 8 | // version 3 of the License, or (at your option) any later version. 9 | // * Mozilla Public License as published by the Mozilla Foundation, version 2. 10 | // 11 | // `piet-hardware` is distributed in the hope that it will be useful, but WITHOUT ANY 12 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 13 | // PURPOSE. See the GNU Lesser General Public License or the Mozilla Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License and the Mozilla 17 | // Public License along with `piet-hardware`. If not, see . 18 | 19 | //! The rasterizer, powered by `lyon_tessellation`. 20 | 21 | use super::gpu_backend::Vertex; 22 | use super::stroke::StrokeBuffer; 23 | use super::ResultExt; 24 | 25 | use arrayvec::ArrayVec; 26 | 27 | use tiny_skia::PathSegment; 28 | 29 | use lyon_tessellation::path::{Event, PathEvent}; 30 | use lyon_tessellation::{ 31 | BuffersBuilder, FillOptions, FillRule, FillTessellator, FillVertex, StrokeOptions, 32 | StrokeTessellator, StrokeVertex, VertexBuffers, 33 | }; 34 | 35 | use piet::kurbo::{BezPath, PathEl, Point, Rect, Shape}; 36 | use piet::{Color, Error as Pierror, LineCap, LineJoin}; 37 | 38 | pub(crate) struct Rasterizer { 39 | /// Buffers for tessellating the path. 40 | buffers: VertexBuffers, 41 | 42 | /// The fill tessellator. 43 | fill_tessellator: FillTessellator, 44 | 45 | /// The stroke tessellator. 46 | stroke_tessellator: StrokeTessellator, 47 | 48 | /// The buffer for dashing. 49 | dash_buffer: Option, 50 | } 51 | 52 | impl Rasterizer { 53 | /// Create a new rasterizer. 54 | pub(crate) fn new() -> Self { 55 | Self { 56 | buffers: VertexBuffers::new(), 57 | fill_tessellator: FillTessellator::new(), 58 | stroke_tessellator: StrokeTessellator::new(), 59 | dash_buffer: Some(StrokeBuffer::new()), 60 | } 61 | } 62 | 63 | /// Get a reference to the vertex buffer. 64 | pub(crate) fn vertices(&self) -> &[Vertex] { 65 | &self.buffers.vertices 66 | } 67 | 68 | /// Get a reference to the index buffer. 69 | pub(crate) fn indices(&self) -> &[u32] { 70 | &self.buffers.indices 71 | } 72 | 73 | /// Clear the rasterizer's buffers. 74 | pub(crate) fn clear(&mut self) { 75 | self.buffers.vertices.clear(); 76 | self.buffers.indices.clear(); 77 | } 78 | 79 | /// Tessellate a series of rectangles. 80 | pub(crate) fn fill_rects(&mut self, rects: impl IntoIterator) { 81 | // Get the vertices associated with the rectangles. 82 | let mut rect_count = 0; 83 | let mut vertices = |pos_rect: Rect, uv_rect: Rect, color: piet::Color| { 84 | rect_count += 1; 85 | let cast = |x: f64| x as f32; 86 | let (r, g, b, a) = color.as_rgba8(); 87 | let color = [r, g, b, a]; 88 | 89 | [ 90 | Vertex { 91 | pos: [cast(pos_rect.x0), cast(pos_rect.y0)], 92 | uv: [cast(uv_rect.x0), cast(uv_rect.y0)], 93 | color, 94 | }, 95 | Vertex { 96 | pos: [cast(pos_rect.x1), cast(pos_rect.y0)], 97 | uv: [cast(uv_rect.x1), cast(uv_rect.y0)], 98 | color, 99 | }, 100 | Vertex { 101 | pos: [cast(pos_rect.x1), cast(pos_rect.y1)], 102 | uv: [cast(uv_rect.x1), cast(uv_rect.y1)], 103 | color, 104 | }, 105 | Vertex { 106 | pos: [cast(pos_rect.x0), cast(pos_rect.y1)], 107 | uv: [cast(uv_rect.x0), cast(uv_rect.y1)], 108 | color, 109 | }, 110 | ] 111 | }; 112 | 113 | // Add the vertices to the buffers. 114 | self.buffers 115 | .vertices 116 | .extend(rects.into_iter().flat_map(|tess| { 117 | let TessRect { pos, uv, color } = tess; 118 | vertices(pos, uv, color) 119 | })); 120 | self.buffers.indices.extend((0..rect_count).flat_map(|i| { 121 | let base = i * 4; 122 | [base, base + 1, base + 2, base, base + 2, base + 3] 123 | })); 124 | } 125 | 126 | /// Tessellate a filled shape. 127 | pub(crate) fn fill_shape( 128 | &mut self, 129 | shape: impl Shape, 130 | mode: FillRule, 131 | tolerance: f64, 132 | cvt_vertex: impl Fn(FillVertex<'_>) -> Vertex, 133 | ) -> Result<(), Pierror> { 134 | // Create a new buffers builder. 135 | let mut builder = BuffersBuilder::new(&mut self.buffers, move |vertex: FillVertex<'_>| { 136 | cvt_vertex(vertex) 137 | }); 138 | 139 | // Create fill options. 140 | let mut options = FillOptions::default(); 141 | options.fill_rule = mode; 142 | options.tolerance = tolerance as f32; 143 | 144 | // Fill the shape. 145 | self.fill_tessellator 146 | .tessellate( 147 | shape_to_lyon_path(&shape, tolerance), 148 | &options, 149 | &mut builder, 150 | ) 151 | .piet_err() 152 | } 153 | 154 | /// Tessellate the stroke of a shape. 155 | pub(crate) fn stroke_shape( 156 | &mut self, 157 | shape: impl Shape, 158 | tolerance: f64, 159 | width: f64, 160 | style: &piet::StrokeStyle, 161 | cvt_vertex: impl Fn(StrokeVertex<'_, '_>) -> Vertex, 162 | cvt_fill_vertex: impl Fn(FillVertex<'_>) -> Vertex, 163 | ) -> Result<(), Pierror> { 164 | // lyon_tesselation does not support dashing. If this is a dashed path, use kurbo 165 | // to calculate the dash path. 166 | if !style.dash_pattern.is_empty() { 167 | let mut dash_buffer = self.dash_buffer.take().unwrap_or_default(); 168 | 169 | // Render into the dash buffer. 170 | dash_buffer.render_into(shape, width, style, tolerance); 171 | 172 | // Tessellate the dash buffer. 173 | let result = self.fill_shape( 174 | dash_buffer.output_buffer(), 175 | dash_buffer.fill_rule(), 176 | tolerance, 177 | cvt_fill_vertex, 178 | ); 179 | 180 | // Put the dash buffer back to conserve memory. 181 | self.dash_buffer = Some(dash_buffer); 182 | 183 | return result; 184 | } 185 | 186 | // Create a new buffers builder. 187 | let mut builder = 188 | BuffersBuilder::new(&mut self.buffers, move |vertex: StrokeVertex<'_, '_>| { 189 | cvt_vertex(vertex) 190 | }); 191 | 192 | let cvt_line_cap = |cap: LineCap| match cap { 193 | LineCap::Butt => lyon_tessellation::LineCap::Butt, 194 | LineCap::Round => lyon_tessellation::LineCap::Round, 195 | LineCap::Square => lyon_tessellation::LineCap::Square, 196 | }; 197 | 198 | // Create stroke options. 199 | let mut options = StrokeOptions::default(); 200 | options.tolerance = tolerance as f32; 201 | options.line_width = width as f32; 202 | options.start_cap = cvt_line_cap(style.line_cap); 203 | options.end_cap = cvt_line_cap(style.line_cap); 204 | options.line_join = match style.line_join { 205 | LineJoin::Bevel => lyon_tessellation::LineJoin::Bevel, 206 | LineJoin::Round => lyon_tessellation::LineJoin::Round, 207 | LineJoin::Miter { limit } => { 208 | options.miter_limit = limit as f32; 209 | lyon_tessellation::LineJoin::Miter 210 | } 211 | }; 212 | 213 | // Fill the shape. 214 | self.stroke_tessellator 215 | .tessellate( 216 | shape_to_lyon_path(&shape, tolerance), 217 | &options, 218 | &mut builder, 219 | ) 220 | .piet_err() 221 | } 222 | } 223 | 224 | /// A rectangle to be tessellated. 225 | #[derive(Debug, Clone)] 226 | pub(crate) struct TessRect { 227 | /// The rectangle to be tessellated. 228 | pub(crate) pos: Rect, 229 | 230 | /// The UV coordinates of the rectangle. 231 | pub(crate) uv: Rect, 232 | 233 | /// The color of the rectangle. 234 | pub(crate) color: Color, 235 | } 236 | 237 | fn shape_to_lyon_path(shape: &impl Shape, tolerance: f64) -> impl Iterator + '_ { 238 | use std::iter::Fuse; 239 | 240 | fn convert_point(pt: Point) -> lyon_tessellation::path::geom::Point { 241 | let (x, y): (f64, f64) = pt.into(); 242 | [x as f32, y as f32].into() 243 | } 244 | 245 | struct PathConverter { 246 | /// The iterator over `kurbo` `PathEl`s. 247 | iter: Fuse, 248 | 249 | /// The last point that we processed. 250 | last: Option, 251 | 252 | /// The first point of the current subpath. 253 | first: Option, 254 | 255 | // Whether or not we need to close the path. 256 | needs_close: bool, 257 | } 258 | 259 | impl> Iterator for PathConverter { 260 | type Item = ArrayVec; 261 | 262 | fn next(&mut self) -> Option { 263 | let close = |this: &mut PathConverter, close| { 264 | if let (Some(first), Some(last)) = (this.first.take(), this.last.take()) { 265 | if (!approx_eq(first.x, last.x) || !approx_eq(first.y, last.y)) 266 | || (this.needs_close || close) 267 | { 268 | this.needs_close = false; 269 | return Some(Event::End { 270 | last: convert_point(last), 271 | first: convert_point(first), 272 | close, 273 | }); 274 | } 275 | } 276 | 277 | None 278 | }; 279 | 280 | let el = match self.iter.next() { 281 | Some(el) => el, 282 | None => { 283 | // If we're at the end of the iterator, we need to close the path. 284 | return close(self, false).map(one); 285 | } 286 | }; 287 | 288 | match el { 289 | PathEl::MoveTo(pt) => { 290 | // Close if we need to. 291 | let close = close(self, false); 292 | 293 | // Set the first point. 294 | self.first = Some(pt); 295 | self.last = Some(pt); 296 | 297 | let mut v = ArrayVec::new(); 298 | v.extend(close); 299 | v.push(Event::Begin { 300 | at: convert_point(pt), 301 | }); 302 | Some(v) 303 | } 304 | 305 | PathEl::LineTo(pt) => { 306 | self.needs_close = true; 307 | let from = self.last.replace(pt).expect("last point should be set"); 308 | 309 | Some(one(Event::Line { 310 | from: convert_point(from), 311 | to: convert_point(pt), 312 | })) 313 | } 314 | 315 | PathEl::QuadTo(ctrl1, pt) => { 316 | self.needs_close = true; 317 | let from = self.last.replace(pt).expect("last point should be set"); 318 | 319 | Some(one(Event::Quadratic { 320 | from: convert_point(from), 321 | ctrl: convert_point(ctrl1), 322 | to: convert_point(pt), 323 | })) 324 | } 325 | 326 | PathEl::CurveTo(ctrl1, ctrl2, pt) => { 327 | self.needs_close = true; 328 | let from = self.last.replace(pt).expect("last point should be set"); 329 | 330 | Some(one(Event::Cubic { 331 | from: convert_point(from), 332 | ctrl1: convert_point(ctrl1), 333 | ctrl2: convert_point(ctrl2), 334 | to: convert_point(pt), 335 | })) 336 | } 337 | 338 | PathEl::ClosePath => { 339 | let mut v = ArrayVec::new(); 340 | v.extend(close(self, true)); 341 | Some(v) 342 | } 343 | } 344 | } 345 | } 346 | 347 | PathConverter { 348 | iter: shape.path_elements(tolerance).fuse(), 349 | last: None, 350 | first: None, 351 | needs_close: false, 352 | } 353 | .flatten() 354 | } 355 | 356 | struct TinySkiaPathAsShape(tiny_skia::Path); 357 | 358 | impl TinySkiaPathAsShape { 359 | fn ugly_convert_to_bezpath(&self) -> BezPath { 360 | let mut path = BezPath::new(); 361 | 362 | for elem in self.0.segments() { 363 | path.push(ts_pathseg_to_path_el(elem)); 364 | } 365 | 366 | path 367 | } 368 | } 369 | 370 | impl Shape for TinySkiaPathAsShape { 371 | type PathElementsIter<'iter> = TinySkiaPathIter<'iter>; 372 | 373 | fn path_elements(&self, _tolerance: f64) -> Self::PathElementsIter<'_> { 374 | TinySkiaPathIter(self.0.segments()) 375 | } 376 | 377 | fn area(&self) -> f64 { 378 | self.ugly_convert_to_bezpath().area() 379 | } 380 | 381 | fn perimeter(&self, accuracy: f64) -> f64 { 382 | self.ugly_convert_to_bezpath().perimeter(accuracy) 383 | } 384 | 385 | fn winding(&self, pt: kurbo::Point) -> i32 { 386 | self.ugly_convert_to_bezpath().winding(pt) 387 | } 388 | 389 | fn bounding_box(&self) -> kurbo::Rect { 390 | self.ugly_convert_to_bezpath().bounding_box() 391 | } 392 | } 393 | 394 | struct TinySkiaPathIter<'a>(tiny_skia::PathSegmentsIter<'a>); 395 | 396 | impl Iterator for TinySkiaPathIter<'_> { 397 | type Item = PathEl; 398 | 399 | fn next(&mut self) -> Option { 400 | self.0.next().map(ts_pathseg_to_path_el) 401 | } 402 | 403 | fn size_hint(&self) -> (usize, Option) { 404 | self.0.size_hint() 405 | } 406 | 407 | fn count(self) -> usize { 408 | self.0.count() 409 | } 410 | 411 | fn nth(&mut self, n: usize) -> Option { 412 | self.0.nth(n).map(ts_pathseg_to_path_el) 413 | } 414 | 415 | fn last(self) -> Option { 416 | self.0.last().map(ts_pathseg_to_path_el) 417 | } 418 | 419 | fn collect>(self) -> B { 420 | self.0.map(ts_pathseg_to_path_el).collect() 421 | } 422 | 423 | fn fold(self, init: Acc, mut g: G) -> Acc 424 | where 425 | G: FnMut(Acc, Self::Item) -> Acc, 426 | { 427 | self.0 428 | .fold(init, |acc, seg| g(acc, ts_pathseg_to_path_el(seg))) 429 | } 430 | } 431 | 432 | fn ts_pathseg_to_path_el(pathseg: PathSegment) -> PathEl { 433 | match pathseg { 434 | PathSegment::MoveTo(p) => PathEl::MoveTo(cvt_ts_point(p)), 435 | PathSegment::LineTo(p) => PathEl::LineTo(cvt_ts_point(p)), 436 | PathSegment::QuadTo(p1, p2) => PathEl::QuadTo(cvt_ts_point(p1), cvt_ts_point(p2)), 437 | PathSegment::CubicTo(p1, p2, p3) => { 438 | PathEl::CurveTo(cvt_ts_point(p1), cvt_ts_point(p2), cvt_ts_point(p3)) 439 | } 440 | PathSegment::Close => PathEl::ClosePath, 441 | } 442 | } 443 | 444 | fn cvt_ts_point(p: tiny_skia::Point) -> kurbo::Point { 445 | kurbo::Point::new(p.x as f64, p.y as f64) 446 | } 447 | 448 | fn approx_eq(a: f64, b: f64) -> bool { 449 | (a - b).abs() < 0.01 450 | } 451 | 452 | fn one(p: PathEvent) -> ArrayVec { 453 | let mut v = ArrayVec::new(); 454 | v.push(p); 455 | v 456 | } 457 | -------------------------------------------------------------------------------- /LICENSE-MPL-2.0.md: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | ### 1. Definitions 5 | 6 | **1.1. “Contributor”** 7 | means each individual or legal entity that creates, contributes to 8 | the creation of, or owns Covered Software. 9 | 10 | **1.2. “Contributor Version”** 11 | means the combination of the Contributions of others (if any) used 12 | by a Contributor and that particular Contributor's Contribution. 13 | 14 | **1.3. “Contribution”** 15 | means Covered Software of a particular Contributor. 16 | 17 | **1.4. “Covered Software”** 18 | means Source Code Form to which the initial Contributor has attached 19 | the notice in Exhibit A, the Executable Form of such Source Code 20 | Form, and Modifications of such Source Code Form, in each case 21 | including portions thereof. 22 | 23 | **1.5. “Incompatible With Secondary Licenses”** 24 | means 25 | 26 | * **(a)** that the initial Contributor has attached the notice described 27 | in Exhibit B to the Covered Software; or 28 | * **(b)** that the Covered Software was made available under the terms of 29 | version 1.1 or earlier of the License, but not also under the 30 | terms of a Secondary License. 31 | 32 | **1.6. “Executable Form”** 33 | means any form of the work other than Source Code Form. 34 | 35 | **1.7. “Larger Work”** 36 | means a work that combines Covered Software with other material, in 37 | a separate file or files, that is not Covered Software. 38 | 39 | **1.8. “License”** 40 | means this document. 41 | 42 | **1.9. “Licensable”** 43 | means having the right to grant, to the maximum extent possible, 44 | whether at the time of the initial grant or subsequently, any and 45 | all of the rights conveyed by this License. 46 | 47 | **1.10. “Modifications”** 48 | means any of the following: 49 | 50 | * **(a)** any file in Source Code Form that results from an addition to, 51 | deletion from, or modification of the contents of Covered 52 | Software; or 53 | * **(b)** any new file in Source Code Form that contains any Covered 54 | Software. 55 | 56 | **1.11. “Patent Claims” of a Contributor** 57 | means any patent claim(s), including without limitation, method, 58 | process, and apparatus claims, in any patent Licensable by such 59 | Contributor that would be infringed, but for the grant of the 60 | License, by the making, using, selling, offering for sale, having 61 | made, import, or transfer of either its Contributions or its 62 | Contributor Version. 63 | 64 | **1.12. “Secondary License”** 65 | means either the GNU General Public License, Version 2.0, the GNU 66 | Lesser General Public License, Version 2.1, the GNU Affero General 67 | Public License, Version 3.0, or any later versions of those 68 | licenses. 69 | 70 | **1.13. “Source Code Form”** 71 | means the form of the work preferred for making modifications. 72 | 73 | **1.14. “You” (or “Your”)** 74 | means an individual or a legal entity exercising rights under this 75 | License. For legal entities, “You” includes any entity that 76 | controls, is controlled by, or is under common control with You. For 77 | purposes of this definition, “control” means **(a)** the power, direct 78 | or indirect, to cause the direction or management of such entity, 79 | whether by contract or otherwise, or **(b)** ownership of more than 80 | fifty percent (50%) of the outstanding shares or beneficial 81 | ownership of such entity. 82 | 83 | 84 | ### 2. License Grants and Conditions 85 | 86 | #### 2.1. Grants 87 | 88 | Each Contributor hereby grants You a world-wide, royalty-free, 89 | non-exclusive license: 90 | 91 | * **(a)** under intellectual property rights (other than patent or trademark) 92 | Licensable by such Contributor to use, reproduce, make available, 93 | modify, display, perform, distribute, and otherwise exploit its 94 | Contributions, either on an unmodified basis, with Modifications, or 95 | as part of a Larger Work; and 96 | * **(b)** under Patent Claims of such Contributor to make, use, sell, offer 97 | for sale, have made, import, and otherwise transfer either its 98 | Contributions or its Contributor Version. 99 | 100 | #### 2.2. Effective Date 101 | 102 | The licenses granted in Section 2.1 with respect to any Contribution 103 | become effective for each Contribution on the date the Contributor first 104 | distributes such Contribution. 105 | 106 | #### 2.3. Limitations on Grant Scope 107 | 108 | The licenses granted in this Section 2 are the only rights granted under 109 | this License. No additional rights or licenses will be implied from the 110 | distribution or licensing of Covered Software under this License. 111 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 112 | Contributor: 113 | 114 | * **(a)** for any code that a Contributor has removed from Covered Software; 115 | or 116 | * **(b)** for infringements caused by: **(i)** Your and any other third party's 117 | modifications of Covered Software, or **(ii)** the combination of its 118 | Contributions with other software (except as part of its Contributor 119 | Version); or 120 | * **(c)** under Patent Claims infringed by Covered Software in the absence of 121 | its Contributions. 122 | 123 | This License does not grant any rights in the trademarks, service marks, 124 | or logos of any Contributor (except as may be necessary to comply with 125 | the notice requirements in Section 3.4). 126 | 127 | #### 2.4. Subsequent Licenses 128 | 129 | No Contributor makes additional grants as a result of Your choice to 130 | distribute the Covered Software under a subsequent version of this 131 | License (see Section 10.2) or under the terms of a Secondary License (if 132 | permitted under the terms of Section 3.3). 133 | 134 | #### 2.5. Representation 135 | 136 | Each Contributor represents that the Contributor believes its 137 | Contributions are its original creation(s) or it has sufficient rights 138 | to grant the rights to its Contributions conveyed by this License. 139 | 140 | #### 2.6. Fair Use 141 | 142 | This License is not intended to limit any rights You have under 143 | applicable copyright doctrines of fair use, fair dealing, or other 144 | equivalents. 145 | 146 | #### 2.7. Conditions 147 | 148 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 149 | in Section 2.1. 150 | 151 | 152 | ### 3. Responsibilities 153 | 154 | #### 3.1. Distribution of Source Form 155 | 156 | All distribution of Covered Software in Source Code Form, including any 157 | Modifications that You create or to which You contribute, must be under 158 | the terms of this License. You must inform recipients that the Source 159 | Code Form of the Covered Software is governed by the terms of this 160 | License, and how they can obtain a copy of this License. You may not 161 | attempt to alter or restrict the recipients' rights in the Source Code 162 | Form. 163 | 164 | #### 3.2. Distribution of Executable Form 165 | 166 | If You distribute Covered Software in Executable Form then: 167 | 168 | * **(a)** such Covered Software must also be made available in Source Code 169 | Form, as described in Section 3.1, and You must inform recipients of 170 | the Executable Form how they can obtain a copy of such Source Code 171 | Form by reasonable means in a timely manner, at a charge no more 172 | than the cost of distribution to the recipient; and 173 | 174 | * **(b)** You may distribute such Executable Form under the terms of this 175 | License, or sublicense it under different terms, provided that the 176 | license for the Executable Form does not attempt to limit or alter 177 | the recipients' rights in the Source Code Form under this License. 178 | 179 | #### 3.3. Distribution of a Larger Work 180 | 181 | You may create and distribute a Larger Work under terms of Your choice, 182 | provided that You also comply with the requirements of this License for 183 | the Covered Software. If the Larger Work is a combination of Covered 184 | Software with a work governed by one or more Secondary Licenses, and the 185 | Covered Software is not Incompatible With Secondary Licenses, this 186 | License permits You to additionally distribute such Covered Software 187 | under the terms of such Secondary License(s), so that the recipient of 188 | the Larger Work may, at their option, further distribute the Covered 189 | Software under the terms of either this License or such Secondary 190 | License(s). 191 | 192 | #### 3.4. Notices 193 | 194 | You may not remove or alter the substance of any license notices 195 | (including copyright notices, patent notices, disclaimers of warranty, 196 | or limitations of liability) contained within the Source Code Form of 197 | the Covered Software, except that You may alter any license notices to 198 | the extent required to remedy known factual inaccuracies. 199 | 200 | #### 3.5. Application of Additional Terms 201 | 202 | You may choose to offer, and to charge a fee for, warranty, support, 203 | indemnity or liability obligations to one or more recipients of Covered 204 | Software. However, You may do so only on Your own behalf, and not on 205 | behalf of any Contributor. You must make it absolutely clear that any 206 | such warranty, support, indemnity, or liability obligation is offered by 207 | You alone, and You hereby agree to indemnify every Contributor for any 208 | liability incurred by such Contributor as a result of warranty, support, 209 | indemnity or liability terms You offer. You may include additional 210 | disclaimers of warranty and limitations of liability specific to any 211 | jurisdiction. 212 | 213 | 214 | ### 4. Inability to Comply Due to Statute or Regulation 215 | 216 | If it is impossible for You to comply with any of the terms of this 217 | License with respect to some or all of the Covered Software due to 218 | statute, judicial order, or regulation then You must: **(a)** comply with 219 | the terms of this License to the maximum extent possible; and **(b)** 220 | describe the limitations and the code they affect. Such description must 221 | be placed in a text file included with all distributions of the Covered 222 | Software under this License. Except to the extent prohibited by statute 223 | or regulation, such description must be sufficiently detailed for a 224 | recipient of ordinary skill to be able to understand it. 225 | 226 | 227 | ### 5. Termination 228 | 229 | **5.1.** The rights granted under this License will terminate automatically 230 | if You fail to comply with any of its terms. However, if You become 231 | compliant, then the rights granted under this License from a particular 232 | Contributor are reinstated **(a)** provisionally, unless and until such 233 | Contributor explicitly and finally terminates Your grants, and **(b)** on an 234 | ongoing basis, if such Contributor fails to notify You of the 235 | non-compliance by some reasonable means prior to 60 days after You have 236 | come back into compliance. Moreover, Your grants from a particular 237 | Contributor are reinstated on an ongoing basis if such Contributor 238 | notifies You of the non-compliance by some reasonable means, this is the 239 | first time You have received notice of non-compliance with this License 240 | from such Contributor, and You become compliant prior to 30 days after 241 | Your receipt of the notice. 242 | 243 | **5.2.** If You initiate litigation against any entity by asserting a patent 244 | infringement claim (excluding declaratory judgment actions, 245 | counter-claims, and cross-claims) alleging that a Contributor Version 246 | directly or indirectly infringes any patent, then the rights granted to 247 | You by any and all Contributors for the Covered Software under Section 248 | 2.1 of this License shall terminate. 249 | 250 | **5.3.** In the event of termination under Sections 5.1 or 5.2 above, all 251 | end user license agreements (excluding distributors and resellers) which 252 | have been validly granted by You or Your distributors under this License 253 | prior to termination shall survive termination. 254 | 255 | 256 | ### 6. Disclaimer of Warranty 257 | 258 | > Covered Software is provided under this License on an “as is” 259 | > basis, without warranty of any kind, either expressed, implied, or 260 | > statutory, including, without limitation, warranties that the 261 | > Covered Software is free of defects, merchantable, fit for a 262 | > particular purpose or non-infringing. The entire risk as to the 263 | > quality and performance of the Covered Software is with You. 264 | > Should any Covered Software prove defective in any respect, You 265 | > (not any Contributor) assume the cost of any necessary servicing, 266 | > repair, or correction. This disclaimer of warranty constitutes an 267 | > essential part of this License. No use of any Covered Software is 268 | > authorized under this License except under this disclaimer. 269 | 270 | ### 7. Limitation of Liability 271 | 272 | > Under no circumstances and under no legal theory, whether tort 273 | > (including negligence), contract, or otherwise, shall any 274 | > Contributor, or anyone who distributes Covered Software as 275 | > permitted above, be liable to You for any direct, indirect, 276 | > special, incidental, or consequential damages of any character 277 | > including, without limitation, damages for lost profits, loss of 278 | > goodwill, work stoppage, computer failure or malfunction, or any 279 | > and all other commercial damages or losses, even if such party 280 | > shall have been informed of the possibility of such damages. This 281 | > limitation of liability shall not apply to liability for death or 282 | > personal injury resulting from such party's negligence to the 283 | > extent applicable law prohibits such limitation. Some 284 | > jurisdictions do not allow the exclusion or limitation of 285 | > incidental or consequential damages, so this exclusion and 286 | > limitation may not apply to You. 287 | 288 | 289 | ### 8. Litigation 290 | 291 | Any litigation relating to this License may be brought only in the 292 | courts of a jurisdiction where the defendant maintains its principal 293 | place of business and such litigation shall be governed by laws of that 294 | jurisdiction, without reference to its conflict-of-law provisions. 295 | Nothing in this Section shall prevent a party's ability to bring 296 | cross-claims or counter-claims. 297 | 298 | 299 | ### 9. Miscellaneous 300 | 301 | This License represents the complete agreement concerning the subject 302 | matter hereof. If any provision of this License is held to be 303 | unenforceable, such provision shall be reformed only to the extent 304 | necessary to make it enforceable. Any law or regulation which provides 305 | that the language of a contract shall be construed against the drafter 306 | shall not be used to construe this License against a Contributor. 307 | 308 | 309 | ### 10. Versions of the License 310 | 311 | #### 10.1. New Versions 312 | 313 | Mozilla Foundation is the license steward. Except as provided in Section 314 | 10.3, no one other than the license steward has the right to modify or 315 | publish new versions of this License. Each version will be given a 316 | distinguishing version number. 317 | 318 | #### 10.2. Effect of New Versions 319 | 320 | You may distribute the Covered Software under the terms of the version 321 | of the License under which You originally received the Covered Software, 322 | or under the terms of any subsequent version published by the license 323 | steward. 324 | 325 | #### 10.3. Modified Versions 326 | 327 | If you create software not governed by this License, and you want to 328 | create a new license for such software, you may create and use a 329 | modified version of this License if you rename the license and remove 330 | any references to the name of the license steward (except to note that 331 | such modified license differs from this License). 332 | 333 | #### 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses 334 | 335 | If You choose to distribute Source Code Form that is Incompatible With 336 | Secondary Licenses under the terms of this version of the License, the 337 | notice described in Exhibit B of this License must be attached. 338 | 339 | ## Exhibit A - Source Code Form License Notice 340 | 341 | This Source Code Form is subject to the terms of the Mozilla Public 342 | License, v. 2.0. If a copy of the MPL was not distributed with this 343 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 344 | 345 | If it is not possible or desirable to put the notice in a particular 346 | file, then You may include the notice in a location (such as a LICENSE 347 | file in a relevant directory) where a recipient would be likely to look 348 | for such a notice. 349 | 350 | You may add additional accurate notices of copyright ownership. 351 | 352 | ## Exhibit B - “Incompatible With Secondary Licenses” Notice 353 | 354 | This Source Code Form is "Incompatible With Secondary Licenses", as 355 | defined by the Mozilla Public License, v. 2.0. 356 | 357 | 358 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later OR MPL-2.0 2 | // This file is a part of `piet-hardware`. 3 | // 4 | // `piet-hardware` is free software: you can redistribute it and/or modify it under the 5 | // terms of either: 6 | // 7 | // * GNU Lesser General Public License as published by the Free Software Foundation, either 8 | // version 3 of the License, or (at your option) any later version. 9 | // * Mozilla Public License as published by the Mozilla Foundation, version 2. 10 | // 11 | // `piet-hardware` is distributed in the hope that it will be useful, but WITHOUT ANY 12 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 13 | // PURPOSE. See the GNU Lesser General Public License or the Mozilla Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License and the Mozilla 17 | // Public License along with `piet-hardware`. If not, see . 18 | 19 | //! An adaptor for [`piet`] that allows it to take advantage of GPU acceleration. 20 | //! 21 | //! This crate provides common types, traits and functionality that should be useful for 22 | //! implementations of the [`piet`] drawing framework for hardware-accelerated backends 23 | //! like OpenGL, Vulkan and WGPU. It handles things like rasterization, atlas packing and 24 | //! memory management, while leaving the actual implementation of the GPU commands to the 25 | //! backend. 26 | //! 27 | //! To use, first implement the [`GpuContext`] trait on a type of your choice that represents 28 | //! an active GPU context. Wrap this type in the [`Source`] type, and then use that to 29 | //! create a [`RenderContext`]. From here, you can pass that type to your rendering code. It 30 | //! conforms to the [`piet`] API, so you can use it as a drop-in replacement for any [`piet`] 31 | //! backend, including [`piet-common`]. 32 | //! 33 | //! Note that this crate generally uses thread-unsafe primitives. This is because UI management is 34 | //! usually pinned to one thread anyways, and it's a bad idea to do drawing outside of that thread. 35 | //! 36 | //! ## Implementation 37 | //! 38 | //! This crate works first and foremost by converting drawing operations to a series of 39 | //! triangles. 40 | 41 | #![forbid(unsafe_code, rust_2018_idioms)] 42 | 43 | pub use piet; 44 | 45 | use lyon_tessellation::FillRule; 46 | 47 | use piet::kurbo::{Affine, PathEl, Point, Rect, Shape, Size}; 48 | use piet::{Error as Pierror, FixedGradient, Image as _, InterpolationMode}; 49 | 50 | use piet_cosmic_text::LineProcessor; 51 | use tinyvec::TinyVec; 52 | 53 | use std::error::Error as StdError; 54 | use std::fmt; 55 | use std::mem; 56 | 57 | mod atlas; 58 | mod brush; 59 | mod gpu_backend; 60 | mod image; 61 | mod mask; 62 | mod rasterizer; 63 | mod resources; 64 | mod stroke; 65 | mod text; 66 | 67 | pub use self::brush::Brush; 68 | pub use self::gpu_backend::{BufferType, GpuContext, RepeatStrategy, Vertex}; 69 | pub use self::image::Image; 70 | pub use self::text::{Text, TextLayout, TextLayoutBuilder}; 71 | 72 | pub(crate) use atlas::{Atlas, GlyphData}; 73 | pub(crate) use mask::{Mask, MaskContext}; 74 | pub(crate) use rasterizer::{Rasterizer, TessRect}; 75 | pub(crate) use resources::{Texture, VertexBuffer}; 76 | 77 | const UV_WHITE: [f32; 2] = [0.5, 0.5]; 78 | 79 | /// Structures that are useful for implementing the `GpuContext` type. 80 | pub mod gpu_types { 81 | pub use crate::gpu_backend::{AreaCapture, BufferPush, SubtextureWrite, TextureWrite}; 82 | } 83 | 84 | /// The source of the GPU renderer. 85 | pub struct Source { 86 | /// A texture that consists of an endless repeating pattern of a single white pixel. 87 | /// 88 | /// This is used for solid-color fills. It is also used as the mask for when a 89 | /// clipping mask is not defined. 90 | white_pixel: Texture, 91 | 92 | /// The buffers used by the GPU renderer. 93 | buffers: Buffers, 94 | 95 | /// The text API. 96 | text: Text, 97 | 98 | /// The font atlas. 99 | atlas: Option>, 100 | 101 | /// The mask rendering context. 102 | mask_context: MaskContext, 103 | 104 | /// The cached list of render states. 105 | /// 106 | /// This is always empty, but it keeps the memory around. 107 | render_states: Option; 1]>>, 108 | 109 | /// The context to use for the GPU renderer. 110 | context: C, 111 | } 112 | 113 | impl fmt::Debug for Source { 114 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 115 | f.debug_struct("Source") 116 | .field("context", &&self.context) 117 | .finish_non_exhaustive() 118 | } 119 | } 120 | 121 | struct Buffers { 122 | /// The rasterizer for the GPU renderer. 123 | rasterizer: Rasterizer, 124 | 125 | /// The VBO for vertices. 126 | vbo: VertexBuffer, 127 | } 128 | 129 | impl Source { 130 | /// Create a new source from a context. 131 | pub fn new(mut context: C, device: &C::Device, queue: &C::Queue) -> Result 132 | where 133 | C: Sized, 134 | { 135 | const WHITE: [u8; 4] = [0xFF, 0xFF, 0xFF, 0xFF]; 136 | 137 | // Setup a white pixel texture. 138 | let texture = Texture::new( 139 | &mut context, 140 | device, 141 | InterpolationMode::NearestNeighbor, 142 | RepeatStrategy::Repeat, 143 | ) 144 | .piet_err()?; 145 | 146 | texture.write_texture( 147 | &mut context, 148 | device, 149 | queue, 150 | (1, 1), 151 | piet::ImageFormat::RgbaSeparate, 152 | Some(&WHITE), 153 | ); 154 | 155 | Ok(Self { 156 | white_pixel: texture, 157 | buffers: { 158 | let vbo = VertexBuffer::new(&mut context, device).piet_err()?; 159 | 160 | Buffers { 161 | rasterizer: Rasterizer::new(), 162 | vbo, 163 | } 164 | }, 165 | atlas: Some(Atlas::new(&mut context, device, queue)?), 166 | mask_context: MaskContext::new(), 167 | render_states: None, 168 | context, 169 | text: Text::new(), 170 | }) 171 | } 172 | 173 | /// Get a reference to the context. 174 | pub fn context(&self) -> &C { 175 | &self.context 176 | } 177 | 178 | /// Get a mutable reference to the context. 179 | pub fn context_mut(&mut self) -> &mut C { 180 | &mut self.context 181 | } 182 | 183 | /// Create a new rendering context. 184 | pub fn render_context<'this, 'dev, 'que>( 185 | &'this mut self, 186 | device: &'dev C::Device, 187 | queue: &'que C::Queue, 188 | width: u32, 189 | height: u32, 190 | ) -> RenderContext<'this, 'dev, 'que, C> { 191 | RenderContext { 192 | state: { 193 | let mut list = self.render_states.take().unwrap_or_default(); 194 | list.clear(); 195 | list.push(RenderState::default()); 196 | list 197 | }, 198 | source: self, 199 | device, 200 | queue, 201 | size: (width, height), 202 | status: Ok(()), 203 | tolerance: 0.1, 204 | ignore_state: false, 205 | bitmap_scale: 1.0, 206 | } 207 | } 208 | 209 | /// Get a reference to the text backend. 210 | pub fn text(&self) -> &Text { 211 | &self.text 212 | } 213 | 214 | /// Get a mutable reference to the text backend. 215 | pub fn text_mut(&mut self) -> &mut Text { 216 | &mut self.text 217 | } 218 | 219 | /// Indicate that we've flushed the queue and all of the GPU resources can be overwritten. 220 | pub fn gpu_flushed(&mut self) { 221 | self.mask_context.gpu_flushed(); 222 | } 223 | } 224 | 225 | /// The whole point of this crate. 226 | #[derive(Debug)] 227 | pub struct RenderContext<'src, 'dev, 'que, C: GpuContext + ?Sized> { 228 | /// The source of the GPU renderer. 229 | source: &'src mut Source, 230 | 231 | /// The device that we are rendering to. 232 | device: &'dev C::Device, 233 | 234 | /// The queue that we are rendering to. 235 | queue: &'que C::Queue, 236 | 237 | /// The width and height of the target. 238 | size: (u32, u32), 239 | 240 | /// The current state of the renderer. 241 | state: TinyVec<[RenderState; 1]>, 242 | 243 | /// The result to use for `status`. 244 | status: Result<(), Pierror>, 245 | 246 | /// Tolerance for tesselation. 247 | tolerance: f64, 248 | 249 | /// Scale to apply for bitmaps. 250 | bitmap_scale: f64, 251 | 252 | /// Flag to ignore the current state. 253 | ignore_state: bool, 254 | } 255 | 256 | #[derive(Debug)] 257 | struct RenderState { 258 | /// The current transform in pixel space. 259 | transform: Affine, 260 | 261 | /// The current clip. 262 | clip: ClipState, 263 | } 264 | 265 | impl Default for RenderState { 266 | fn default() -> Self { 267 | Self { 268 | transform: Affine::IDENTITY, 269 | clip: ClipState::NoClip, 270 | } 271 | } 272 | } 273 | 274 | /// Current state for clipping. 275 | #[derive(Debug)] 276 | enum ClipState { 277 | /// There is no clip at all. 278 | NoClip, 279 | 280 | /// There is a simple rectangle to use for clips. 281 | SimpleRect(Rect), 282 | 283 | /// There is a more complicated texture mask. 284 | Mask(Mask), 285 | } 286 | 287 | impl ClipState { 288 | /// Convert into an `Option` 289 | #[inline] 290 | fn into_mask(self) -> Option> { 291 | match self { 292 | Self::Mask(mask) => Some(mask), 293 | _ => None, 294 | } 295 | } 296 | 297 | /// Convert into an `Option<&mut Mask>`. 298 | #[inline] 299 | fn as_mut(&mut self) -> Option<&mut Mask> { 300 | match self { 301 | Self::Mask(mask) => Some(mask), 302 | _ => None, 303 | } 304 | } 305 | } 306 | 307 | impl Clone for ClipState { 308 | #[inline] 309 | fn clone(&self) -> Self { 310 | match self { 311 | Self::NoClip => Self::NoClip, 312 | Self::SimpleRect(rect) => Self::SimpleRect(*rect), 313 | Self::Mask(mask) => Self::Mask(mask.clone()), 314 | } 315 | } 316 | } 317 | 318 | impl Drop for RenderContext<'_, '_, '_, C> { 319 | fn drop(&mut self) { 320 | match &mut self.state { 321 | TinyVec::Heap(h) => self 322 | .source 323 | .mask_context 324 | .reclaim(h.drain(..).filter_map(|s| s.clip.into_mask())), 325 | TinyVec::Inline(i) => self 326 | .source 327 | .mask_context 328 | .reclaim(i.drain(..).filter_map(|s| s.clip.into_mask())), 329 | } 330 | 331 | let mut state = mem::take(&mut self.state); 332 | state.clear(); 333 | self.source.render_states = Some(state); 334 | } 335 | } 336 | 337 | impl<'a, 'b, 'c, C: GpuContext + ?Sized> RenderContext<'a, 'b, 'c, C> { 338 | /// Temporarily ignore the transform and the clip. 339 | fn temporarily_ignore_state<'this>( 340 | &'this mut self, 341 | ) -> TemporarilyIgnoreState<'this, 'a, 'b, 'c, C> { 342 | self.ignore_state = true; 343 | TemporarilyIgnoreState(self) 344 | } 345 | 346 | /// Fill in a rectangle. 347 | fn fill_rects( 348 | &mut self, 349 | rects: impl IntoIterator, 350 | texture: Option<&Texture>, 351 | ) -> Result<(), Pierror> { 352 | self.source.buffers.rasterizer.fill_rects(rects); 353 | 354 | // Push the buffers to the GPU. 355 | self.push_buffers(texture) 356 | } 357 | 358 | /// Fill in the provided shape. 359 | fn fill_impl( 360 | &mut self, 361 | shape: impl Shape, 362 | brush: &Brush, 363 | mode: FillRule, 364 | ) -> Result<(), Pierror> { 365 | self.source 366 | .buffers 367 | .rasterizer 368 | .fill_shape(shape, mode, self.tolerance, |vert| { 369 | let pos = vert.position(); 370 | brush.make_vertex(pos.into()) 371 | })?; 372 | 373 | // Push the incoming buffers. 374 | self.push_buffers(brush.texture(self.size).as_ref().map(|t| t.texture())) 375 | } 376 | 377 | fn stroke_impl( 378 | &mut self, 379 | shape: impl Shape, 380 | brush: &Brush, 381 | width: f64, 382 | style: &piet::StrokeStyle, 383 | ) -> Result<(), Pierror> { 384 | self.source.buffers.rasterizer.stroke_shape( 385 | shape, 386 | self.tolerance, 387 | width, 388 | style, 389 | |vert| { 390 | let pos = vert.position(); 391 | brush.make_vertex(pos.into()) 392 | }, 393 | |vert| { 394 | let pos = vert.position(); 395 | brush.make_vertex(pos.into()) 396 | }, 397 | )?; 398 | 399 | // Push the incoming buffers. 400 | self.push_buffers(brush.texture(self.size).as_ref().map(|t| t.texture())) 401 | } 402 | 403 | /// Push the values currently in the renderer to the GPU. 404 | fn push_buffers(&mut self, texture: Option<&Texture>) -> Result<(), Pierror> { 405 | // Upload the vertex and index buffers. 406 | self.source.buffers.vbo.upload( 407 | &mut self.source.context, 408 | self.device, 409 | self.queue, 410 | self.source.buffers.rasterizer.vertices(), 411 | self.source.buffers.rasterizer.indices(), 412 | ); 413 | 414 | // Decide which mask and transform to use. 415 | let (transform, mask_texture, clip_rect, used_mask) = if self.ignore_state { 416 | ( 417 | Affine::scale(self.bitmap_scale), 418 | &self.source.white_pixel, 419 | None, 420 | false, 421 | ) 422 | } else { 423 | let state = self.state.last_mut().unwrap(); 424 | 425 | let (has_mask, clip_rect, mask) = match &mut state.clip { 426 | ClipState::NoClip => (false, None, &self.source.white_pixel), 427 | ClipState::SimpleRect(rect) => (false, Some(*rect), &self.source.white_pixel), 428 | ClipState::Mask(mask) => ( 429 | true, 430 | None, 431 | self.source.mask_context.texture( 432 | mask, 433 | &mut self.source.context, 434 | self.device, 435 | self.queue, 436 | ), 437 | ), 438 | }; 439 | 440 | ( 441 | Affine::scale(self.bitmap_scale) * state.transform, 442 | mask, 443 | clip_rect, 444 | has_mask, 445 | ) 446 | }; 447 | 448 | // Decide the texture to use. 449 | let texture = texture.unwrap_or(&self.source.white_pixel); 450 | 451 | // Draw! 452 | self.source 453 | .context 454 | .push_buffers(gpu_types::BufferPush { 455 | device: self.device, 456 | queue: self.queue, 457 | vertex_buffer: self.source.buffers.vbo.resource(), 458 | current_texture: texture.resource(), 459 | mask_texture: mask_texture.resource(), 460 | transform: &transform, 461 | viewport_size: self.size, 462 | clip: clip_rect, 463 | }) 464 | .piet_err()?; 465 | 466 | // Clear the original buffers. 467 | self.source.buffers.rasterizer.clear(); 468 | 469 | // Mark the mask as used so we don't overwrite it. 470 | if used_mask { 471 | if let Some(mask) = &mut self.state.last_mut().unwrap().clip.as_mut() { 472 | self.source.mask_context.mark_used(mask); 473 | } 474 | } 475 | 476 | Ok(()) 477 | } 478 | 479 | fn clip_impl(&mut self, shape: impl Shape) { 480 | let state = self.state.last_mut().unwrap(); 481 | 482 | // If this shape is just a rectangle, use a simple scissor rect instead. 483 | if let Some(rect) = shape.as_rect() { 484 | if let ClipState::NoClip = &state.clip { 485 | state.clip = ClipState::SimpleRect(rect); 486 | return; 487 | } 488 | } 489 | 490 | let mask = match &mut state.clip { 491 | ClipState::Mask(mask) => mask, 492 | ClipState::SimpleRect(rect) => { 493 | // Create a clip mask with the existing rectangle 494 | let mut mask = Mask::new(self.size.0, self.size.1); 495 | self.source 496 | .mask_context 497 | .add_path(&mut mask, *rect, self.tolerance); 498 | state.clip = ClipState::Mask(mask); 499 | state.clip.as_mut().unwrap() 500 | } 501 | clip @ ClipState::NoClip => { 502 | *clip = ClipState::Mask(Mask::new(self.size.0, self.size.1)); 503 | clip.as_mut().unwrap() 504 | } 505 | }; 506 | 507 | self.source 508 | .mask_context 509 | .add_path(mask, shape, self.tolerance); 510 | } 511 | 512 | /// Get the source of this render context. 513 | pub fn source(&self) -> &Source { 514 | self.source 515 | } 516 | 517 | /// Get a mutable reference to the source of this render context. 518 | pub fn source_mut(&mut self) -> &mut Source { 519 | self.source 520 | } 521 | 522 | /// Get the current tolerance for tesselation. 523 | /// 524 | /// This is used to convert curves into line segments. 525 | pub fn tolerance(&self) -> f64 { 526 | self.tolerance 527 | } 528 | 529 | /// Set the current tolerance for tesselation. 530 | /// 531 | /// This is used to convert curves into line segments. 532 | pub fn set_tolerance(&mut self, tolerance: f64) { 533 | self.tolerance = tolerance; 534 | } 535 | 536 | /// Get the bitmap scale. 537 | pub fn bitmap_scale(&self) -> f64 { 538 | self.bitmap_scale 539 | } 540 | 541 | /// Set the bitmap scale. 542 | pub fn set_bitmap_scale(&mut self, scale: f64) { 543 | self.bitmap_scale = scale; 544 | } 545 | } 546 | 547 | macro_rules! leap { 548 | ($self:expr, $e:expr) => {{ 549 | match $e { 550 | Ok(v) => v, 551 | Err(e) => { 552 | $self.status = Err(Pierror::BackendError(e.into())); 553 | return; 554 | } 555 | } 556 | }}; 557 | ($self:expr, $e:expr, $err:expr) => {{ 558 | match $e { 559 | Ok(v) => v, 560 | Err(e) => { 561 | let err = $err; 562 | $self.status = Err(err.into()); 563 | return; 564 | } 565 | } 566 | }}; 567 | } 568 | 569 | impl piet::RenderContext for RenderContext<'_, '_, '_, C> { 570 | type Brush = Brush; 571 | type Text = Text; 572 | type TextLayout = TextLayout; 573 | type Image = Image; 574 | 575 | fn status(&mut self) -> Result<(), Pierror> { 576 | mem::replace(&mut self.status, Ok(())) 577 | } 578 | 579 | fn solid_brush(&mut self, color: piet::Color) -> Self::Brush { 580 | Brush::solid(color) 581 | } 582 | 583 | fn gradient(&mut self, gradient: impl Into) -> Result { 584 | match gradient.into() { 585 | FixedGradient::Linear(linear) => { 586 | Brush::linear_gradient(&mut self.source.context, self.device, self.queue, linear) 587 | } 588 | FixedGradient::Radial(radial) => { 589 | Brush::radial_gradient(&mut self.source.context, self.device, self.queue, radial) 590 | } 591 | } 592 | } 593 | 594 | fn clear(&mut self, region: impl Into>, mut color: piet::Color) { 595 | let region = region.into(); 596 | 597 | // Premultiply the color. 598 | let clamp = |x: f64| { 599 | if x < 0.0 { 600 | 0.0 601 | } else if x > 1.0 { 602 | 1.0 603 | } else { 604 | x 605 | } 606 | }; 607 | let (r, g, b, a) = color.as_rgba(); 608 | let r = clamp(r * a); 609 | let g = clamp(g * a); 610 | let b = clamp(b * a); 611 | color = piet::Color::rgba(r, g, b, 1.0); 612 | 613 | // Use optimized clear if possible. 614 | if region.is_none() { 615 | self.source.context.clear(self.device, self.queue, color); 616 | return; 617 | } 618 | 619 | // Ignore clipping mask and transform. 620 | let ignore_state = self.temporarily_ignore_state(); 621 | 622 | // Otherwise, fall back to filling in the screen rectangle. 623 | let result = ignore_state.0.fill_rects( 624 | { 625 | let uv_white = Point::new(UV_WHITE[0] as f64, UV_WHITE[1] as f64); 626 | [TessRect { 627 | pos: region.unwrap_or_else(|| { 628 | Rect::from_origin_size( 629 | (0.0, 0.0), 630 | (ignore_state.0.size.0 as f64, ignore_state.0.size.1 as f64), 631 | ) 632 | }), 633 | uv: Rect::from_points(uv_white, uv_white), 634 | color, 635 | }] 636 | }, 637 | None, 638 | ); 639 | 640 | leap!(ignore_state.0, result); 641 | } 642 | 643 | fn stroke(&mut self, shape: impl Shape, brush: &impl piet::IntoBrush, width: f64) { 644 | let brush = brush.make_brush(self, || shape.bounding_box()); 645 | if let Err(e) = 646 | self.stroke_impl(shape, brush.as_ref(), width, &piet::StrokeStyle::default()) 647 | { 648 | self.status = Err(e); 649 | } 650 | } 651 | 652 | fn stroke_styled( 653 | &mut self, 654 | shape: impl Shape, 655 | brush: &impl piet::IntoBrush, 656 | width: f64, 657 | style: &piet::StrokeStyle, 658 | ) { 659 | let brush = brush.make_brush(self, || shape.bounding_box()); 660 | if let Err(e) = self.stroke_impl(shape, brush.as_ref(), width, style) { 661 | self.status = Err(e); 662 | } 663 | } 664 | 665 | fn fill(&mut self, shape: impl Shape, brush: &impl piet::IntoBrush) { 666 | let brush = brush.make_brush(self, || shape.bounding_box()); 667 | if let Err(e) = self.fill_impl(shape, brush.as_ref(), FillRule::NonZero) { 668 | self.status = Err(e); 669 | } 670 | } 671 | 672 | fn fill_even_odd(&mut self, shape: impl Shape, brush: &impl piet::IntoBrush) { 673 | let brush = brush.make_brush(self, || shape.bounding_box()); 674 | if let Err(e) = self.fill_impl(shape, brush.as_ref(), FillRule::EvenOdd) { 675 | self.status = Err(e); 676 | } 677 | } 678 | 679 | fn clip(&mut self, shape: impl Shape) { 680 | // If we have a bitmap scale, scale the clip shape up. 681 | if (self.bitmap_scale - 1.0).abs() > 0.001 { 682 | let mut path = shape.into_path(self.tolerance); 683 | path.apply_affine(Affine::scale(self.bitmap_scale)); 684 | self.clip_impl(path); 685 | } else { 686 | self.clip_impl(shape); 687 | } 688 | } 689 | 690 | fn text(&mut self) -> &mut Self::Text { 691 | &mut self.source.text 692 | } 693 | 694 | fn draw_text(&mut self, layout: &Self::TextLayout, pos: impl Into) { 695 | struct RestoreAtlas<'a, 'b, 'c, 'd, G: GpuContext + ?Sized> { 696 | context: &'a mut RenderContext<'b, 'c, 'd, G>, 697 | atlas: Option>, 698 | } 699 | 700 | impl Drop for RestoreAtlas<'_, '_, '_, '_, G> { 701 | fn drop(&mut self) { 702 | self.context.source.atlas = Some(self.atlas.take().unwrap()); 703 | } 704 | } 705 | 706 | let pos = pos.into(); 707 | let mut restore = RestoreAtlas { 708 | atlas: self.source.atlas.take(), 709 | context: self, 710 | }; 711 | 712 | // Iterate over the glyphs and use them to write. 713 | let texture = restore.atlas.as_ref().unwrap().texture().clone(); 714 | 715 | let text = restore.context.text().clone(); 716 | let device = restore.context.device; 717 | let queue = restore.context.queue; 718 | let mut line_state = LineProcessor::new(); 719 | let rects = layout 720 | .buffer() 721 | .layout_runs() 722 | .flat_map(|run| { 723 | // Combine the run's glyphs and the layout's y position. 724 | run.glyphs 725 | .iter() 726 | .map(move |glyph| (glyph, run.line_y as f64)) 727 | }) 728 | .filter_map({ 729 | let atlas = restore.atlas.as_mut().unwrap(); 730 | |(glyph, line_y)| { 731 | // Get the rectangle in texture space representing the glyph. 732 | let GlyphData { 733 | uv_rect, 734 | offset, 735 | size, 736 | } = match text.with_font_system_mut(|fs| { 737 | atlas.uv_rect( 738 | &mut restore.context.source.context, 739 | device, 740 | queue, 741 | glyph, 742 | fs, 743 | ) 744 | }) { 745 | Some(Ok(rect)) => rect, 746 | Some(Err(e)) => { 747 | tracing::trace!("failed to get uv rect: {}", e); 748 | return None; 749 | } 750 | None => { 751 | // Still waiting to load. 752 | tracing::trace!("font system not loaded yet"); 753 | return None; 754 | } 755 | }; 756 | 757 | let physical = glyph.physical((0.0, 0.0), 1.0); 758 | 759 | // Get the rectangle in screen space representing the glyph. 760 | let pos_rect = Rect::from_origin_size( 761 | ( 762 | physical.x as f64 + pos.x + offset.x, 763 | physical.y as f64 + line_y + pos.y - offset.y, 764 | ), 765 | size, 766 | ); 767 | 768 | let color = glyph.color_opt.unwrap_or({ 769 | let piet_color = piet::util::DEFAULT_TEXT_COLOR; 770 | let (r, g, b, a) = piet_color.as_rgba8(); 771 | cosmic_text::Color::rgba(r, g, b, a) 772 | }); 773 | let piet_color = 774 | glyph 775 | .color_opt 776 | .map_or(piet::util::DEFAULT_TEXT_COLOR, |color| { 777 | let [r, g, b, a] = [color.r(), color.g(), color.b(), color.a()]; 778 | piet::Color::rgba8(r, g, b, a) 779 | }); 780 | 781 | // Register the glyph in the atlas. 782 | line_state.handle_glyph(glyph, line_y as f32, color); 783 | 784 | Some(TessRect { 785 | pos: pos_rect, 786 | uv: uv_rect, 787 | color: piet_color, 788 | }) 789 | } 790 | }) 791 | .collect::>(); 792 | let result = restore.context.fill_rects(rects, Some(&texture)); 793 | 794 | drop(restore); 795 | 796 | let lines_result = { 797 | let lines = line_state.lines(); 798 | if lines.is_empty() { 799 | Ok(()) 800 | } else { 801 | self.fill_rects( 802 | lines.into_iter().map(|line| { 803 | let mut rect = line.into_rect(); 804 | rect.x0 += pos.x; 805 | rect.y0 += pos.y; 806 | rect.x1 += pos.x; 807 | rect.y1 += pos.y; 808 | TessRect { 809 | pos: rect, 810 | uv: Rect::new(0.5, 0.5, 0.5, 0.5), 811 | color: line.color, 812 | } 813 | }), 814 | None, 815 | ) 816 | } 817 | }; 818 | 819 | leap!(self, result); 820 | leap!(self, lines_result); 821 | } 822 | 823 | fn save(&mut self) -> Result<(), Pierror> { 824 | let last = self.state.last().unwrap(); 825 | self.state.push(RenderState { 826 | transform: last.transform, 827 | clip: last.clip.clone(), 828 | }); 829 | Ok(()) 830 | } 831 | 832 | fn restore(&mut self) -> Result<(), Pierror> { 833 | if self.state.len() <= 1 { 834 | return Err(Pierror::StackUnbalance); 835 | } 836 | 837 | let mut state = self.state.pop().unwrap(); 838 | self.source.mask_context.reclaim( 839 | mem::replace(&mut state.clip, ClipState::NoClip) 840 | .into_mask() 841 | .into_iter(), 842 | ); 843 | 844 | Ok(()) 845 | } 846 | 847 | fn finish(&mut self) -> Result<(), Pierror> { 848 | self.source 849 | .context 850 | .flush() 851 | .map_err(|x| Pierror::BackendError(x.into())) 852 | } 853 | 854 | fn transform(&mut self, transform: Affine) { 855 | let slot = &mut self.state.last_mut().unwrap().transform; 856 | *slot *= transform; 857 | } 858 | 859 | fn make_image( 860 | &mut self, 861 | width: usize, 862 | height: usize, 863 | buf: &[u8], 864 | format: piet::ImageFormat, 865 | ) -> Result { 866 | let tex = Texture::new( 867 | &mut self.source.context, 868 | self.device, 869 | InterpolationMode::Bilinear, 870 | RepeatStrategy::Color(piet::Color::TRANSPARENT), 871 | ) 872 | .piet_err()?; 873 | 874 | tex.write_texture( 875 | &mut self.source.context, 876 | self.device, 877 | self.queue, 878 | (width as u32, height as u32), 879 | format, 880 | Some(buf), 881 | ); 882 | 883 | Ok(Image::new(tex, Size::new(width as f64, height as f64))) 884 | } 885 | 886 | fn draw_image( 887 | &mut self, 888 | image: &Self::Image, 889 | dst_rect: impl Into, 890 | interp: piet::InterpolationMode, 891 | ) { 892 | self.draw_image_area(image, Rect::ZERO.with_size(image.size()), dst_rect, interp) 893 | } 894 | 895 | fn draw_image_area( 896 | &mut self, 897 | image: &Self::Image, 898 | src_rect: impl Into, 899 | dst_rect: impl Into, 900 | interp: piet::InterpolationMode, 901 | ) { 902 | // Create a rectangle for the destination and a rectangle for UV. 903 | let pos_rect = dst_rect.into(); 904 | let uv_rect = { 905 | let scale_x = 1.0 / image.size().width; 906 | let scale_y = 1.0 / image.size().height; 907 | 908 | let src_rect = src_rect.into(); 909 | Rect::new( 910 | src_rect.x0 * scale_x, 911 | src_rect.y0 * scale_y, 912 | src_rect.x1 * scale_x, 913 | src_rect.y1 * scale_y, 914 | ) 915 | }; 916 | 917 | // Set the interpolation mode. 918 | image 919 | .texture() 920 | .set_interpolation(&mut self.source.context, self.device, interp); 921 | 922 | // Use this to draw the image. 923 | if let Err(e) = self.fill_rects( 924 | [TessRect { 925 | pos: pos_rect, 926 | uv: uv_rect, 927 | color: piet::Color::WHITE, 928 | }], 929 | Some(image.texture()), 930 | ) { 931 | self.status = Err(e); 932 | } 933 | } 934 | 935 | fn capture_image_area(&mut self, src_rect: impl Into) -> Result { 936 | let src_rect = src_rect.into(); 937 | let src_size = src_rect.size(); 938 | let src_bitmap_size = Size::new( 939 | src_size.width * self.bitmap_scale, 940 | src_size.height * self.bitmap_scale, 941 | ); 942 | 943 | // Create a new texture to copy the image to. 944 | let image = { 945 | let texture = Texture::new( 946 | &mut self.source.context, 947 | self.device, 948 | InterpolationMode::Bilinear, 949 | RepeatStrategy::Repeat, 950 | ) 951 | .piet_err()?; 952 | 953 | Image::new(texture, src_bitmap_size) 954 | }; 955 | 956 | // Capture the area in the texture. 957 | let offset = (src_rect.x0 as u32, src_rect.y0 as u32); 958 | let size = (src_size.width as u32, src_size.height as u32); 959 | self.source 960 | .context 961 | .capture_area(gpu_backend::AreaCapture { 962 | device: self.device, 963 | queue: self.queue, 964 | texture: image.texture().resource(), 965 | offset, 966 | size, 967 | bitmap_scale: self.bitmap_scale, 968 | }) 969 | .piet_err()?; 970 | 971 | Ok(image) 972 | } 973 | 974 | fn blurred_rect( 975 | &mut self, 976 | input_rect: Rect, 977 | blur_radius: f64, 978 | brush: &impl piet::IntoBrush, 979 | ) { 980 | let size = piet::util::size_for_blurred_rect(input_rect, blur_radius); 981 | let width = size.width as u32; 982 | let height = size.height as u32; 983 | if width == 0 || height == 0 { 984 | return; 985 | } 986 | 987 | // Compute the blurred rectangle image. 988 | let (mask, rect_exp) = { 989 | let mut mask = tiny_skia::Mask::new(width, height).unwrap(); 990 | 991 | let rect_exp = piet::util::compute_blurred_rect( 992 | input_rect, 993 | blur_radius, 994 | width.try_into().unwrap(), 995 | mask.data_mut(), 996 | ); 997 | 998 | (mask, rect_exp) 999 | }; 1000 | 1001 | // Create an image using this mask. 1002 | let mut image = tiny_skia::Pixmap::new(width, height) 1003 | .expect("Pixmap width/height should be valid clipmask width/height"); 1004 | let shader = match brush.make_brush(self, || input_rect).to_shader() { 1005 | Some(shader) => shader, 1006 | None => { 1007 | self.status = Err(Pierror::BackendError("Failed to create shader".into())); 1008 | return; 1009 | } 1010 | }; 1011 | image.fill(tiny_skia::Color::TRANSPARENT); 1012 | image.fill_rect( 1013 | tiny_skia::Rect::from_xywh(0., 0., width as f32, height as f32).unwrap(), 1014 | &tiny_skia::Paint { 1015 | shader, 1016 | ..Default::default() 1017 | }, 1018 | tiny_skia::Transform::identity(), 1019 | Some(&mask), 1020 | ); 1021 | 1022 | // Draw this image. 1023 | let image = leap!( 1024 | self, 1025 | self.make_image( 1026 | width as usize, 1027 | height as usize, 1028 | image.data(), 1029 | piet::ImageFormat::RgbaSeparate 1030 | ) 1031 | ); 1032 | self.draw_image(&image, rect_exp, piet::InterpolationMode::Bilinear); 1033 | } 1034 | 1035 | fn current_transform(&self) -> Affine { 1036 | self.state.last().unwrap().transform 1037 | } 1038 | } 1039 | 1040 | struct TemporarilyIgnoreState<'this, 'a, 'b, 'c, C: GpuContext + ?Sized>( 1041 | &'this mut RenderContext<'a, 'b, 'c, C>, 1042 | ); 1043 | 1044 | impl Drop for TemporarilyIgnoreState<'_, '_, '_, '_, C> { 1045 | fn drop(&mut self) { 1046 | self.0.ignore_state = false; 1047 | } 1048 | } 1049 | 1050 | trait ResultExt { 1051 | fn piet_err(self) -> Result; 1052 | } 1053 | 1054 | impl ResultExt for Result { 1055 | fn piet_err(self) -> Result { 1056 | self.map_err(|e| Pierror::BackendError(Box::new(LibraryError(e)))) 1057 | } 1058 | } 1059 | 1060 | struct LibraryError(E); 1061 | 1062 | impl fmt::Debug for LibraryError { 1063 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1064 | fmt::Debug::fmt(&self.0, f) 1065 | } 1066 | } 1067 | 1068 | impl fmt::Display for LibraryError { 1069 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 1070 | fmt::Display::fmt(&self.0, f) 1071 | } 1072 | } 1073 | 1074 | impl StdError for LibraryError {} 1075 | 1076 | /// Convert a `piet::Shape` to a `tiny_skia` path. 1077 | fn shape_to_skia_path(builder: &mut tiny_skia::PathBuilder, shape: impl Shape, tolerance: f64) { 1078 | shape.path_elements(tolerance).for_each(|el| match el { 1079 | PathEl::MoveTo(pt) => builder.move_to(pt.x as f32, pt.y as f32), 1080 | PathEl::LineTo(pt) => builder.line_to(pt.x as f32, pt.y as f32), 1081 | PathEl::QuadTo(p1, p2) => { 1082 | builder.quad_to(p1.x as f32, p1.y as f32, p2.x as f32, p2.y as f32) 1083 | } 1084 | PathEl::CurveTo(p1, p2, p3) => builder.cubic_to( 1085 | p1.x as f32, 1086 | p1.y as f32, 1087 | p2.x as f32, 1088 | p2.y as f32, 1089 | p3.x as f32, 1090 | p3.y as f32, 1091 | ), 1092 | PathEl::ClosePath => builder.close(), 1093 | }) 1094 | } 1095 | -------------------------------------------------------------------------------- /examples/gl.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later OR MPL-2.0 2 | // This file is a part of `piet-hardware`. 3 | // 4 | // `piet-hardware` is free software: you can redistribute it and/or modify it under the 5 | // terms of either: 6 | // 7 | // * GNU Lesser General Public License as published by the Free Software Foundation, either 8 | // version 3 of the License, or (at your option) any later version. 9 | // * Mozilla Public License as published by the Mozilla Foundation, version 2. 10 | // 11 | // `piet-hardware` is distributed in the hope that it will be useful, but WITHOUT ANY 12 | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR 13 | // PURPOSE. See the GNU Lesser General Public License or the Mozilla Public License for more 14 | // details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License and the Mozilla 17 | // Public License along with `piet-hardware`. If not, see . 18 | 19 | //! An example that uses the `gl` crate to render to a `winit` window. 20 | //! 21 | //! This uses `glutin` crate to set up a GL context, `winit` to create a window, and the `gl` 22 | //! crate to make GL calls. 23 | //! 24 | //! This example exists mostly to give an example of how a `GpuContext` can be implemented. 25 | //! If you actually want to use `piet` with OpenGL, consider the `piet-glow` crate. 26 | 27 | use glutin::config::ConfigTemplateBuilder; 28 | use glutin::context::{ContextApi, ContextAttributesBuilder, Version}; 29 | use glutin::display::GetGlDisplay; 30 | use glutin::prelude::*; 31 | 32 | use glutin_winit::{DisplayBuilder, GlWindow}; 33 | 34 | use piet::kurbo::{Affine, BezPath, Point, Rect}; 35 | use piet::RenderContext as _; 36 | 37 | use piet_hardware::gpu_types::{AreaCapture, BufferPush, SubtextureWrite, TextureWrite}; 38 | 39 | use raw_window_handle::HasRawWindowHandle; 40 | 41 | use winit::dpi::PhysicalSize; 42 | use winit::event::{Event, WindowEvent}; 43 | use winit::event_loop::EventLoop; 44 | use winit::window::WindowBuilder; 45 | 46 | use std::cell::Cell; 47 | use std::ffi::CString; 48 | use std::fmt; 49 | use std::mem; 50 | use std::num::NonZeroU32; 51 | 52 | use web_time::{Duration, Instant}; 53 | 54 | const TEST_IMAGE: &[u8] = include_bytes!("test-image.png"); 55 | 56 | fn main() -> Result<(), Box> { 57 | env_logger::init(); 58 | 59 | // Create the winit event loop. 60 | let event_loop = EventLoop::new(); 61 | 62 | let mut size = PhysicalSize::new(800, 600); 63 | let make_window_builder = move || { 64 | WindowBuilder::new() 65 | .with_title("piet-hardware example") 66 | .with_transparent(true) 67 | .with_inner_size(size) 68 | }; 69 | 70 | // If we're on Windows, start with the window. 71 | let window = if cfg!(windows) { 72 | Some(make_window_builder()) 73 | } else { 74 | None 75 | }; 76 | 77 | // Start building an OpenGL display. 78 | let display = DisplayBuilder::new().with_window_builder(window); 79 | 80 | // Look for a config that supports transparency and has a good sample count. 81 | let (mut window, gl_config) = display.build( 82 | &event_loop, 83 | ConfigTemplateBuilder::new().with_alpha_size(8), 84 | |configs| { 85 | configs 86 | .reduce(|accum, config| { 87 | let transparency_check = config.supports_transparency().unwrap_or(false) 88 | & !accum.supports_transparency().unwrap_or(false); 89 | 90 | if transparency_check || config.num_samples() > accum.num_samples() { 91 | config 92 | } else { 93 | accum 94 | } 95 | }) 96 | .unwrap() 97 | }, 98 | )?; 99 | 100 | // Try to build a several different contexts. 101 | let window_handle = window.as_ref().map(|w| w.raw_window_handle()); 102 | let contexts = [ 103 | ContextAttributesBuilder::new().build(window_handle), 104 | ContextAttributesBuilder::new() 105 | .with_context_api(ContextApi::Gles(None)) 106 | .build(window_handle), 107 | ContextAttributesBuilder::new() 108 | .with_context_api(ContextApi::Gles(Some(Version::new(2, 0)))) 109 | .build(window_handle), 110 | ]; 111 | 112 | let display = gl_config.display(); 113 | let gl_handler = (|| { 114 | // Try to build a context for each config. 115 | for context in &contexts { 116 | if let Ok(gl_context) = unsafe { display.create_context(&gl_config, context) } { 117 | return Ok(gl_context); 118 | } 119 | } 120 | 121 | // If we couldn't build a context, return an error. 122 | Err(Box::::from( 123 | "Could not create a context", 124 | )) 125 | })()?; 126 | 127 | // Set up data for the window. 128 | let framerate = Duration::from_millis({ 129 | let framerate = 1.0 / 60.0; 130 | (framerate * 1000.0) as u64 131 | }); 132 | let mut next_frame = Instant::now() + framerate; 133 | let mut state = None; 134 | let mut renderer = None; 135 | let mut not_current_gl_context = Some(gl_handler); 136 | 137 | // Load the image. 138 | let (image_width, image_height, image_data) = { 139 | let image = image::load_from_memory(TEST_IMAGE).unwrap(); 140 | let image = image.to_rgba8(); 141 | 142 | let (width, height) = image.dimensions(); 143 | let data = image.into_raw(); 144 | (width, height, data) 145 | }; 146 | 147 | // Drawing data. 148 | let star = generate_five_pointed_star((0.0, 0.0).into(), 75.0, 150.0); 149 | let mut solid_red = None; 150 | let mut outline = None; 151 | let mut image = None; 152 | let mut tick = 0; 153 | let mut num_frames = 0; 154 | let mut last_second = Instant::now(); 155 | 156 | // Draw the window. 157 | let mut draw = 158 | move |ctx: &mut piet_hardware::RenderContext<'_, 'static, 'static, GlContext>| { 159 | ctx.clear(None, piet::Color::AQUA); 160 | 161 | let outline = outline.get_or_insert_with(|| ctx.solid_brush(piet::Color::BLACK)); 162 | 163 | // Draw a rotating star. 164 | ctx.with_save(|ctx| { 165 | ctx.transform({ 166 | let rotation = Affine::rotate((tick as f64) * 0.02); 167 | let translation = Affine::translate((200.0, 200.0)); 168 | 169 | translation * rotation 170 | }); 171 | 172 | let solid_red = solid_red 173 | .get_or_insert_with(|| ctx.solid_brush(piet::Color::rgb8(0x39, 0xe5, 0x8a))); 174 | 175 | ctx.fill(&star, solid_red); 176 | ctx.stroke(&star, outline, 5.0); 177 | 178 | Ok(()) 179 | }) 180 | .unwrap(); 181 | 182 | // Draw a moving image. 183 | { 184 | let cos_curve = |x: f64, amp: f64, freq: f64| { 185 | let x = x * std::f64::consts::PI * freq; 186 | x.cos() * amp 187 | }; 188 | let sin_curve = |x: f64, amp: f64, freq: f64| { 189 | let x = x * std::f64::consts::PI * freq; 190 | x.sin() * amp 191 | }; 192 | 193 | let posn_shift_x = cos_curve(tick as f64, 50.0, 0.01); 194 | let posn_shift_y = sin_curve(tick as f64, 50.0, 0.01); 195 | let posn_x = 450.0 + posn_shift_x; 196 | let posn_y = 150.0 + posn_shift_y; 197 | 198 | let size_shift_x = cos_curve(tick as f64, 25.0, 0.02); 199 | let size_shift_y = sin_curve(tick as f64, 25.0, 0.02); 200 | let size_x = 100.0 + size_shift_x; 201 | let size_y = 100.0 + size_shift_y; 202 | 203 | let target_rect = Rect::new(posn_x, posn_y, posn_x + size_x, posn_y + size_y); 204 | 205 | let image_handle = image.get_or_insert_with(|| { 206 | ctx.make_image( 207 | image_width as usize, 208 | image_height as usize, 209 | &image_data, 210 | piet::ImageFormat::RgbaSeparate, 211 | ) 212 | .unwrap() 213 | }); 214 | 215 | ctx.draw_image(image_handle, target_rect, piet::InterpolationMode::Bilinear); 216 | 217 | // Also draw a subset of the image. 218 | let source_rect = Rect::new( 219 | 25.0 + posn_shift_x, 220 | 25.0 + posn_shift_y, 221 | 100.0 + posn_shift_x, 222 | 100.0 + posn_shift_y, 223 | ); 224 | 225 | let target_rect = Rect::from_origin_size((625.0, 50.0), (100.0, 100.0)); 226 | 227 | ctx.draw_image_area( 228 | image_handle, 229 | source_rect, 230 | target_rect, 231 | piet::InterpolationMode::Bilinear, 232 | ); 233 | ctx.stroke(target_rect, outline, 5.0); 234 | } 235 | 236 | tick += 1; 237 | 238 | num_frames += 1; 239 | if Instant::now().duration_since(last_second) >= Duration::from_secs(1) { 240 | last_second = Instant::now(); 241 | println!("fps: {num_frames}"); 242 | num_frames = 0; 243 | } 244 | 245 | ctx.finish().unwrap(); 246 | ctx.status() 247 | }; 248 | 249 | event_loop.run(move |event, target, control_flow| { 250 | control_flow.set_wait_until(next_frame); 251 | 252 | match event { 253 | Event::Resumed => { 254 | // We can now create windows. 255 | let window = window.take().unwrap_or_else(|| { 256 | let window_builder = make_window_builder(); 257 | glutin_winit::finalize_window(target, window_builder, &gl_config).unwrap() 258 | }); 259 | 260 | let attrs = window.build_surface_attributes(Default::default()); 261 | let surface = unsafe { 262 | gl_config 263 | .display() 264 | .create_window_surface(&gl_config, &attrs) 265 | .unwrap() 266 | }; 267 | 268 | // Make the context current. 269 | let gl_context = not_current_gl_context 270 | .take() 271 | .unwrap() 272 | .make_current(&surface) 273 | .unwrap(); 274 | 275 | unsafe { 276 | renderer 277 | .get_or_insert_with(|| { 278 | // Register the GL pointers if we can. 279 | { 280 | gl::load_with(|symbol| { 281 | let symbol_cstr = CString::new(symbol).unwrap(); 282 | gl_config.display().get_proc_address(symbol_cstr.as_c_str()) 283 | }); 284 | 285 | piet_hardware::Source::new(GlContext::new(), &(), &()).unwrap() 286 | } 287 | }) 288 | .context() 289 | .set_context(); 290 | } 291 | 292 | state = Some((surface, window, gl_context)); 293 | } 294 | 295 | Event::Suspended => { 296 | // Destroy the window. 297 | if let Some((.., context)) = state.take() { 298 | not_current_gl_context = Some(context.make_not_current().unwrap()); 299 | } 300 | 301 | if let Some(renderer) = &renderer { 302 | renderer.context().unset_context(); 303 | } 304 | } 305 | 306 | Event::WindowEvent { event, .. } => match event { 307 | WindowEvent::CloseRequested => control_flow.set_exit(), 308 | WindowEvent::Resized(new_size) => { 309 | size = new_size; 310 | 311 | if let Some((surface, _, context)) = &state { 312 | surface.resize( 313 | context, 314 | NonZeroU32::new(size.width).unwrap(), 315 | NonZeroU32::new(size.height).unwrap(), 316 | ); 317 | } 318 | } 319 | _ => {} 320 | }, 321 | 322 | Event::RedrawEventsCleared => { 323 | if let (Some((surface, _, context)), Some(renderer)) = (&state, &mut renderer) { 324 | // Create the render context. 325 | let mut render_context = 326 | renderer.render_context(&(), &(), size.width, size.height); 327 | 328 | // Perform drawing. 329 | draw(&mut render_context).unwrap(); 330 | 331 | // Swap buffers. 332 | surface.swap_buffers(context).unwrap(); 333 | } 334 | 335 | // Schedule the next frame. 336 | next_frame += framerate; 337 | } 338 | 339 | _ => {} 340 | } 341 | }) 342 | } 343 | 344 | fn generate_five_pointed_star(center: Point, inner_radius: f64, outer_radius: f64) -> BezPath { 345 | let point_from_polar = |radius: f64, angle: f64| { 346 | let x = center.x + radius * angle.cos(); 347 | let y = center.y + radius * angle.sin(); 348 | Point::new(x, y) 349 | }; 350 | 351 | let one_fifth_circle = std::f64::consts::PI * 2.0 / 5.0; 352 | 353 | let outer_points = (0..5).map(|i| point_from_polar(outer_radius, one_fifth_circle * i as f64)); 354 | let inner_points = (0..5).map(|i| { 355 | point_from_polar( 356 | inner_radius, 357 | one_fifth_circle * i as f64 + one_fifth_circle / 2.0, 358 | ) 359 | }); 360 | let mut points = outer_points.zip(inner_points).flat_map(|(a, b)| [a, b]); 361 | 362 | // Set up the path. 363 | let mut path = BezPath::new(); 364 | path.move_to(points.next().unwrap()); 365 | 366 | // Add the points to the path. 367 | for point in points { 368 | path.line_to(point); 369 | } 370 | 371 | // Close the path. 372 | path.close_path(); 373 | path 374 | } 375 | 376 | /// The global OpenGL context. 377 | struct GlContext { 378 | /// Whether we have a context installed. 379 | has_context: Cell, 380 | 381 | /// A program for rendering. 382 | render_program: gl::types::GLuint, 383 | 384 | // Uniform locations. 385 | u_transform: gl::types::GLint, 386 | viewport_size: gl::types::GLint, 387 | tex: gl::types::GLint, 388 | mask: gl::types::GLint, 389 | } 390 | 391 | #[derive(Clone)] 392 | struct GlVertexBuffer { 393 | vbo: gl::types::GLuint, 394 | ebo: gl::types::GLuint, 395 | vao: gl::types::GLuint, 396 | num_indices: Cell, 397 | } 398 | 399 | impl GlContext { 400 | fn assert_context(&self) { 401 | if !self.has_context.get() { 402 | panic!("No GL context installed"); 403 | } 404 | } 405 | 406 | // SAFETY: Context must be current. 407 | unsafe fn new() -> Self { 408 | // Create the program. 409 | let program = unsafe { 410 | let vertex_shader = Self::compile_shader(gl::VERTEX_SHADER, VERTEX_SHADER).unwrap(); 411 | 412 | let fragment_shader = 413 | Self::compile_shader(gl::FRAGMENT_SHADER, FRAGMENT_SHADER).unwrap(); 414 | 415 | let program = gl::CreateProgram(); 416 | gl::AttachShader(program, vertex_shader); 417 | gl::AttachShader(program, fragment_shader); 418 | gl::LinkProgram(program); 419 | 420 | let mut success = gl::FALSE as gl::types::GLint; 421 | gl::GetProgramiv(program, gl::LINK_STATUS, &mut success); 422 | 423 | if success == gl::FALSE as gl::types::GLint { 424 | let mut len = 0; 425 | gl::GetProgramiv(program, gl::INFO_LOG_LENGTH, &mut len); 426 | 427 | let mut buf = Vec::with_capacity(len as usize); 428 | gl::GetProgramInfoLog(program, len, std::ptr::null_mut(), buf.as_mut_ptr() as _); 429 | buf.set_len((len as usize) - 1); 430 | panic!( 431 | "Could not link program: {}", 432 | std::str::from_utf8(&buf).unwrap() 433 | ); 434 | } 435 | 436 | gl::DetachShader(program, vertex_shader); 437 | gl::DetachShader(program, fragment_shader); 438 | gl::DeleteShader(vertex_shader); 439 | gl::DeleteShader(fragment_shader); 440 | 441 | program 442 | }; 443 | 444 | // Enable wireframe mode. 445 | //unsafe { 446 | // gl::PolygonMode(gl::FRONT_AND_BACK, gl::LINE); 447 | //} 448 | 449 | unsafe { 450 | extern "system" fn debug_callback( 451 | source: u32, 452 | ty: u32, 453 | id: u32, 454 | severity: u32, 455 | msg_len: i32, 456 | msg: *const i8, 457 | _user_param: *mut std::ffi::c_void, 458 | ) { 459 | let source = match source { 460 | gl::DEBUG_SOURCE_API => "API", 461 | gl::DEBUG_SOURCE_WINDOW_SYSTEM => "Window System", 462 | gl::DEBUG_SOURCE_SHADER_COMPILER => "Shader Compiler", 463 | gl::DEBUG_SOURCE_THIRD_PARTY => "Third Party", 464 | gl::DEBUG_SOURCE_APPLICATION => "Application", 465 | gl::DEBUG_SOURCE_OTHER => "Other", 466 | _ => "Unknown", 467 | }; 468 | 469 | let ty = match ty { 470 | gl::DEBUG_TYPE_ERROR => "Error", 471 | gl::DEBUG_TYPE_DEPRECATED_BEHAVIOR => "Deprecated Behavior", 472 | gl::DEBUG_TYPE_UNDEFINED_BEHAVIOR => "Undefined Behavior", 473 | gl::DEBUG_TYPE_PORTABILITY => "Portability", 474 | gl::DEBUG_TYPE_PERFORMANCE => "Performance", 475 | gl::DEBUG_TYPE_MARKER => "Marker", 476 | gl::DEBUG_TYPE_OTHER => "Other", 477 | _ => "Unknown", 478 | }; 479 | 480 | let message = { 481 | let slice = 482 | unsafe { std::slice::from_raw_parts(msg as *const u8, msg_len as usize) }; 483 | std::str::from_utf8(slice).unwrap() 484 | }; 485 | 486 | match severity { 487 | gl::DEBUG_SEVERITY_HIGH => { 488 | log::error!("{ty}-{id} ({source}): {message}"); 489 | } 490 | gl::DEBUG_SEVERITY_MEDIUM => { 491 | log::warn!("{ty}-{id} ({source}): {message}"); 492 | } 493 | gl::DEBUG_SEVERITY_LOW => { 494 | log::info!("{ty}-{id} ({source}): {message}"); 495 | } 496 | gl::DEBUG_SEVERITY_NOTIFICATION => { 497 | log::debug!("{ty}-{id} ({source}): {message}"); 498 | } 499 | _ => (), 500 | }; 501 | } 502 | 503 | // Set up a debug callback. 504 | gl::Enable(gl::DEBUG_OUTPUT); 505 | 506 | gl::DebugMessageCallback(Some(debug_callback), std::ptr::null()); 507 | } 508 | 509 | // Get the uniform locations. 510 | let u_transform = unsafe { 511 | let name = CString::new("transform").unwrap(); 512 | gl::GetUniformLocation(program, name.as_ptr()) 513 | }; 514 | 515 | let viewport_size = unsafe { 516 | let name = CString::new("viewportSize").unwrap(); 517 | gl::GetUniformLocation(program, name.as_ptr()) 518 | }; 519 | 520 | let tex = unsafe { 521 | let name = CString::new("tex").unwrap(); 522 | gl::GetUniformLocation(program, name.as_ptr()) 523 | }; 524 | 525 | let mask = unsafe { 526 | let name = CString::new("mask").unwrap(); 527 | gl::GetUniformLocation(program, name.as_ptr()) 528 | }; 529 | 530 | gl_error(); 531 | 532 | Self { 533 | has_context: Cell::new(true), 534 | render_program: program, 535 | u_transform, 536 | viewport_size, 537 | tex, 538 | mask, 539 | } 540 | } 541 | 542 | fn unset_context(&self) { 543 | self.has_context.set(false); 544 | } 545 | 546 | unsafe fn set_context(&self) { 547 | self.has_context.set(true); 548 | } 549 | 550 | unsafe fn compile_shader( 551 | shader_type: gl::types::GLenum, 552 | source: &str, 553 | ) -> Result { 554 | let shader = gl::CreateShader(shader_type); 555 | let source = CString::new(source).unwrap(); 556 | gl::ShaderSource(shader, 1, &source.as_ptr(), std::ptr::null()); 557 | gl::CompileShader(shader); 558 | 559 | let mut success = gl::FALSE as gl::types::GLint; 560 | gl::GetShaderiv(shader, gl::COMPILE_STATUS, &mut success); 561 | 562 | if success == gl::FALSE as gl::types::GLint { 563 | let mut len = 0; 564 | gl::GetShaderiv(shader, gl::INFO_LOG_LENGTH, &mut len); 565 | 566 | let mut buf = Vec::with_capacity(len as usize); 567 | gl::GetShaderInfoLog( 568 | shader, 569 | len, 570 | std::ptr::null_mut(), 571 | buf.as_mut_ptr() as *mut gl::types::GLchar, 572 | ); 573 | buf.set_len((len as usize) - 1); 574 | 575 | return Err(GlError(format!( 576 | "Shader compilation failed: {}", 577 | std::str::from_utf8(&buf).unwrap() 578 | ))); 579 | } 580 | 581 | Ok(shader) 582 | } 583 | } 584 | 585 | #[derive(Debug)] 586 | struct GlError(String); 587 | 588 | impl fmt::Display for GlError { 589 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 590 | write!(f, "GL error: {}", self.0) 591 | } 592 | } 593 | 594 | impl std::error::Error for GlError {} 595 | 596 | impl piet_hardware::GpuContext for GlContext { 597 | type Device = (); 598 | type Queue = (); 599 | type Error = GlError; 600 | type Texture = gl::types::GLuint; 601 | type VertexBuffer = GlVertexBuffer; 602 | 603 | fn clear(&mut self, _device: &(), _queue: &(), color: piet::Color) { 604 | self.assert_context(); 605 | let (r, g, b, a) = color.as_rgba(); 606 | 607 | unsafe { 608 | gl::Disable(gl::SCISSOR_TEST); 609 | gl::ClearColor(r as f32, g as f32, b as f32, a as f32); 610 | gl::Clear(gl::COLOR_BUFFER_BIT); 611 | gl_error(); 612 | } 613 | } 614 | 615 | fn flush(&mut self) -> Result<(), Self::Error> { 616 | self.assert_context(); 617 | 618 | unsafe { 619 | gl::Flush(); 620 | gl_error(); 621 | Ok(()) 622 | } 623 | } 624 | 625 | fn create_texture( 626 | &mut self, 627 | _device: &(), 628 | interpolation: piet::InterpolationMode, 629 | repeat: piet_hardware::RepeatStrategy, 630 | ) -> Result { 631 | self.assert_context(); 632 | 633 | unsafe { 634 | let mut texture = 0; 635 | gl::GenTextures(1, &mut texture); 636 | gl::BindTexture(gl::TEXTURE_2D, texture); 637 | 638 | let (min_filter, mag_filter) = match interpolation { 639 | piet::InterpolationMode::NearestNeighbor => (gl::NEAREST, gl::NEAREST), 640 | piet::InterpolationMode::Bilinear => (gl::LINEAR, gl::LINEAR), 641 | }; 642 | 643 | gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, min_filter as _); 644 | gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, mag_filter as _); 645 | 646 | let (wrap_s, wrap_t) = match repeat { 647 | piet_hardware::RepeatStrategy::Color(clr) => { 648 | let (r, g, b, a) = clr.as_rgba(); 649 | gl::TexParameterfv( 650 | gl::TEXTURE_2D, 651 | gl::TEXTURE_BORDER_COLOR, 652 | [r as f32, g as f32, b as f32, a as f32].as_ptr(), 653 | ); 654 | 655 | (gl::CLAMP_TO_EDGE, gl::CLAMP_TO_EDGE) 656 | } 657 | piet_hardware::RepeatStrategy::Repeat => (gl::REPEAT, gl::REPEAT), 658 | _ => panic!("unsupported repeat strategy"), 659 | }; 660 | 661 | gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, wrap_s as _); 662 | gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, wrap_t as _); 663 | 664 | Ok(texture as _) 665 | } 666 | } 667 | 668 | fn write_texture( 669 | &mut self, 670 | TextureWrite { 671 | device: (), 672 | queue: (), 673 | texture, 674 | size, 675 | format, 676 | data, 677 | }: TextureWrite<'_, Self>, 678 | ) { 679 | self.assert_context(); 680 | 681 | unsafe { 682 | gl::BindTexture(gl::TEXTURE_2D, *texture); 683 | 684 | let (internal_format, format, ty) = match format { 685 | piet::ImageFormat::RgbaSeparate => (gl::RGBA8, gl::RGBA, gl::UNSIGNED_BYTE), 686 | piet::ImageFormat::RgbaPremul => (gl::RGBA8, gl::RGBA, gl::UNSIGNED_BYTE), 687 | _ => panic!("unsupported image format"), 688 | }; 689 | 690 | let (width, height) = size; 691 | let data_ptr = data 692 | .map(|data| data.as_ptr() as *const _) 693 | .unwrap_or(std::ptr::null()); 694 | 695 | gl::TexImage2D( 696 | gl::TEXTURE_2D, 697 | 0, 698 | internal_format as _, 699 | width as _, 700 | height as _, 701 | 0, 702 | format, 703 | ty, 704 | data_ptr, 705 | ); 706 | } 707 | } 708 | 709 | fn write_subtexture( 710 | &mut self, 711 | SubtextureWrite { 712 | device: (), 713 | queue: (), 714 | texture, 715 | offset, 716 | size, 717 | format, 718 | data, 719 | }: SubtextureWrite<'_, Self>, 720 | ) { 721 | self.assert_context(); 722 | 723 | unsafe { 724 | gl::BindTexture(gl::TEXTURE_2D, *texture); 725 | 726 | let (format, ty) = match format { 727 | piet::ImageFormat::RgbaSeparate => (gl::RGBA, gl::UNSIGNED_BYTE), 728 | _ => panic!("unsupported image format"), 729 | }; 730 | 731 | let (width, height) = size; 732 | let (x, y) = offset; 733 | 734 | gl::TexSubImage2D( 735 | gl::TEXTURE_2D, 736 | 0, 737 | x as _, 738 | y as _, 739 | width as _, 740 | height as _, 741 | format, 742 | ty, 743 | data.as_ptr() as *const _, 744 | ); 745 | } 746 | } 747 | 748 | fn set_texture_interpolation( 749 | &mut self, 750 | _device: &(), 751 | texture: &Self::Texture, 752 | interpolation: piet::InterpolationMode, 753 | ) { 754 | self.assert_context(); 755 | 756 | let mode = match interpolation { 757 | piet::InterpolationMode::Bilinear => gl::LINEAR, 758 | piet::InterpolationMode::NearestNeighbor => gl::NEAREST, 759 | }; 760 | 761 | unsafe { 762 | gl::BindTexture(gl::TEXTURE_2D, *texture); 763 | gl::TexParameteri( 764 | gl::TEXTURE_2D, 765 | gl::TEXTURE_MAG_FILTER, 766 | mode as gl::types::GLint, 767 | ); 768 | gl::TexParameteri( 769 | gl::TEXTURE_2D, 770 | gl::TEXTURE_MIN_FILTER, 771 | mode as gl::types::GLint, 772 | ); 773 | //gl::BindTexture(gl::TEXTURE_2D, 0); 774 | } 775 | } 776 | 777 | fn capture_area( 778 | &mut self, 779 | AreaCapture { 780 | device: (), 781 | queue: (), 782 | texture, 783 | offset, 784 | size, 785 | bitmap_scale: scale, 786 | }: AreaCapture<'_, Self>, 787 | ) -> Result<(), Self::Error> { 788 | // Use glReadPixels to read into the texture. 789 | self.assert_context(); 790 | 791 | unsafe { 792 | let (x, y) = offset; 793 | let (width, height) = size; 794 | let (x, y, width, height) = ( 795 | (x as f64 * scale) as i32, 796 | (y as f64 * scale) as i32, 797 | (width as f64 * scale) as i32, 798 | (height as f64 * scale) as i32, 799 | ); 800 | let mut buffer = vec![0u8; (width * height * 4) as usize]; 801 | 802 | gl::ReadPixels( 803 | x as _, 804 | y as _, 805 | width as _, 806 | height as _, 807 | gl::RGBA, 808 | gl::UNSIGNED_BYTE, 809 | buffer.as_mut_ptr() as *mut _, 810 | ); 811 | gl_error(); 812 | 813 | // Flip the image. 814 | let stride = width as usize * 4; 815 | let mut row = vec![0u8; stride]; 816 | for i in 0..(height / 2) { 817 | let top = i as usize; 818 | let bottom = (height - i - 1) as usize; 819 | 820 | let top_start = top * stride; 821 | let bottom_start = bottom * stride; 822 | 823 | row.copy_from_slice(&buffer[top_start..(top_start + stride)]); 824 | buffer.copy_within(bottom_start..(bottom_start + stride), top_start); 825 | buffer[bottom_start..(bottom_start + stride)].copy_from_slice(&row); 826 | } 827 | 828 | // Write the image to the texture. 829 | self.write_subtexture(SubtextureWrite { 830 | device: &(), 831 | queue: &(), 832 | texture, 833 | offset, 834 | size, 835 | format: piet::ImageFormat::RgbaSeparate, 836 | data: &buffer, 837 | }); 838 | } 839 | 840 | Ok(()) 841 | } 842 | 843 | fn max_texture_size(&mut self, _device: &()) -> (u32, u32) { 844 | self.assert_context(); 845 | 846 | unsafe { 847 | let mut side = 0; 848 | gl::GetIntegerv(gl::MAX_TEXTURE_SIZE, &mut side); 849 | (side as u32, side as u32) 850 | } 851 | } 852 | 853 | fn create_vertex_buffer(&mut self, _device: &()) -> Result { 854 | self.assert_context(); 855 | 856 | unsafe { 857 | let mut buffers = [0; 2]; 858 | gl::GenBuffers(2, buffers.as_mut_ptr()); 859 | let [vbo, ebo] = buffers; 860 | 861 | // Set up the vertex array object. 862 | let mut vao = 0; 863 | gl::GenVertexArrays(1, &mut vao); 864 | gl::BindVertexArray(vao); 865 | gl::BindBuffer(gl::ARRAY_BUFFER, vbo); 866 | gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, ebo); 867 | gl::UseProgram(self.render_program); 868 | 869 | let stride = std::mem::size_of::() as _; 870 | 871 | // Set up the layout: 872 | // - vec2 of floats for aPos 873 | // - vec2 of floats for aTexCoord 874 | // - vec4 of unsigned bytes for aColor 875 | let apos_name = CString::new("aPos").unwrap(); 876 | let apos_coord = gl::GetAttribLocation(self.render_program, apos_name.as_ptr()); 877 | gl::EnableVertexAttribArray(apos_coord as _); 878 | gl::VertexAttribPointer( 879 | apos_coord as _, 880 | 2, 881 | gl::FLOAT, 882 | gl::FALSE, 883 | stride, 884 | bytemuck::offset_of!(piet_hardware::Vertex, pos) as *const _, 885 | ); 886 | 887 | let atex_name = CString::new("aTexCoord").unwrap(); 888 | let atex_coord = gl::GetAttribLocation(self.render_program, atex_name.as_ptr() as _); 889 | gl::EnableVertexAttribArray(atex_coord as _); 890 | gl::VertexAttribPointer( 891 | atex_coord as _, 892 | 2, 893 | gl::FLOAT, 894 | gl::FALSE, 895 | stride, 896 | bytemuck::offset_of!(piet_hardware::Vertex, uv) as *const _, 897 | ); 898 | 899 | let acolor_name = CString::new("aColor").unwrap(); 900 | let acolor_coord = gl::GetAttribLocation(self.render_program, acolor_name.as_ptr()); 901 | gl::EnableVertexAttribArray(acolor_coord as _); 902 | gl::VertexAttribPointer( 903 | acolor_coord as _, 904 | 4, 905 | gl::UNSIGNED_BYTE, 906 | gl::FALSE, 907 | stride, 908 | bytemuck::offset_of!(piet_hardware::Vertex, color) as *const _, 909 | ); 910 | 911 | // Unbind the vertex array object. 912 | //gl::BindVertexArray(0); 913 | //gl::BindBuffer(gl::ARRAY_BUFFER, 0); 914 | 915 | Ok(GlVertexBuffer { 916 | vao, 917 | vbo, 918 | ebo, 919 | num_indices: Cell::new(0), 920 | }) 921 | } 922 | } 923 | 924 | fn write_vertices( 925 | &mut self, 926 | _device: &(), 927 | _queue: &(), 928 | buffer: &Self::VertexBuffer, 929 | vertices: &[piet_hardware::Vertex], 930 | indices: &[u32], 931 | ) { 932 | self.assert_context(); 933 | 934 | unsafe { 935 | //gl::BindBuffer(gl::ARRAY_BUFFER, buffer.vbo); 936 | //gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, buffer.ebo); 937 | gl::BufferData( 938 | gl::ARRAY_BUFFER, 939 | mem::size_of_val(vertices) as _, 940 | vertices.as_ptr() as *const _, 941 | gl::DYNAMIC_DRAW, 942 | ); 943 | gl::BufferData( 944 | gl::ELEMENT_ARRAY_BUFFER, 945 | mem::size_of_val(indices) as _, 946 | indices.as_ptr() as *const _, 947 | gl::DYNAMIC_DRAW, 948 | ); 949 | gl_error(); 950 | buffer.num_indices.set(indices.len() as _); 951 | } 952 | } 953 | 954 | fn push_buffers( 955 | &mut self, 956 | BufferPush { 957 | device: (), 958 | queue: (), 959 | vertex_buffer, 960 | current_texture, 961 | mask_texture, 962 | transform, 963 | viewport_size, 964 | clip, 965 | }: BufferPush<'_, Self>, 966 | ) -> Result<(), Self::Error> { 967 | unsafe { 968 | // Use our program. 969 | gl::UseProgram(self.render_program); 970 | 971 | // Set the viewport size. 972 | let (width, height) = viewport_size; 973 | gl::Viewport(0, 0, width as i32, height as i32); 974 | gl::Uniform2f(self.viewport_size, width as f32, height as f32); 975 | 976 | // Set the scissor rect. 977 | let (sx, sy, s_width, s_height) = match clip { 978 | Some(Rect { x0, y0, x1, y1 }) => (x0, y0, x1 - x0, y1 - y0), 979 | None => (0.0, 0.0, width as f64, height as f64), 980 | }; 981 | gl::Enable(gl::SCISSOR_TEST); 982 | gl::Scissor(sx as i32, sy as i32, s_width as i32, s_height as i32); 983 | 984 | // Set the transform. 985 | let [a, b, c, d, e, f] = transform.as_coeffs(); 986 | let transform = [ 987 | a as f32, b as f32, 0.0, c as f32, d as f32, 0.0, e as f32, f as f32, 1.0, 988 | ]; 989 | gl::UniformMatrix3fv(self.u_transform, 1, gl::FALSE, transform.as_ptr()); 990 | 991 | // Set the texture. 992 | gl::ActiveTexture(gl::TEXTURE1); 993 | gl::BindTexture(gl::TEXTURE_2D, *current_texture); 994 | gl::Uniform1i(self.tex, 1); 995 | 996 | // Set the mask texture. 997 | gl::ActiveTexture(gl::TEXTURE0); 998 | gl::BindTexture(gl::TEXTURE_2D, *mask_texture); 999 | gl::Uniform1i(self.mask, 0); 1000 | 1001 | // Set the blend mode. 1002 | gl::Enable(gl::BLEND); 1003 | gl::BlendFunc(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA); 1004 | 1005 | // Set vertex attributes. 1006 | gl::BindVertexArray(vertex_buffer.vao); 1007 | 1008 | // Set buffers. 1009 | gl::BindBuffer(gl::ARRAY_BUFFER, vertex_buffer.vbo); 1010 | gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, vertex_buffer.ebo); 1011 | 1012 | // Draw. 1013 | gl::DrawElements( 1014 | gl::TRIANGLES, 1015 | vertex_buffer.num_indices.get() as i32, 1016 | gl::UNSIGNED_INT, 1017 | std::ptr::null(), 1018 | ); 1019 | 1020 | // Unbind everything. 1021 | //gl::BindVertexArray(0); 1022 | //gl::BindBuffer(gl::ARRAY_BUFFER, 0); 1023 | //gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, 0); 1024 | //gl::BindTexture(gl::TEXTURE_2D, 0); 1025 | //gl::UseProgram(0); 1026 | } 1027 | 1028 | Ok(()) 1029 | } 1030 | } 1031 | 1032 | fn gl_error() { 1033 | let err = unsafe { gl::GetError() }; 1034 | 1035 | if err != gl::NO_ERROR { 1036 | let error_str = match err { 1037 | gl::INVALID_ENUM => "GL_INVALID_ENUM", 1038 | gl::INVALID_VALUE => "GL_INVALID_VALUE", 1039 | gl::INVALID_OPERATION => "GL_INVALID_OPERATION", 1040 | gl::STACK_OVERFLOW => "GL_STACK_OVERFLOW", 1041 | gl::STACK_UNDERFLOW => "GL_STACK_UNDERFLOW", 1042 | gl::OUT_OF_MEMORY => "GL_OUT_OF_MEMORY", 1043 | gl::INVALID_FRAMEBUFFER_OPERATION => "GL_INVALID_FRAMEBUFFER_OPERATION", 1044 | gl::CONTEXT_LOST => "GL_CONTEXT_LOST", 1045 | _ => "Unknown GL error", 1046 | }; 1047 | 1048 | log::error!("GL error: {}", error_str) 1049 | } 1050 | } 1051 | 1052 | const VERTEX_SHADER: &str = " 1053 | #version 330 core 1054 | 1055 | in vec2 aPos; 1056 | in vec2 aTexCoord; 1057 | in vec4 aColor; 1058 | 1059 | out vec4 rgbaColor; 1060 | out vec2 fTexCoord; 1061 | out vec2 fMaskCoord; 1062 | 1063 | uniform mat3 transform; 1064 | uniform vec2 viewportSize; 1065 | 1066 | void main() { 1067 | // Transform the vertex position. 1068 | vec3 pos = transform * vec3(aPos, 1.0); 1069 | pos /= pos.z; 1070 | 1071 | // Transform to screen-space coordinates. 1072 | gl_Position = vec4( 1073 | (2.0 * pos.x / viewportSize.x) - 1.0, 1074 | 1.0 - (2.0 * pos.y / viewportSize.y), 1075 | 0.0, 1076 | 1.0 1077 | ); 1078 | 1079 | // Transform to mask-space coordinates. 1080 | fMaskCoord = vec2( 1081 | pos.x / viewportSize.x, 1082 | 1.0 - (pos.y / viewportSize.y) 1083 | ); 1084 | 1085 | rgbaColor = aColor / 255.0; 1086 | fTexCoord = aTexCoord; 1087 | } 1088 | "; 1089 | 1090 | const FRAGMENT_SHADER: &str = " 1091 | #version 330 core 1092 | 1093 | in vec4 rgbaColor; 1094 | in vec2 fTexCoord; 1095 | in vec2 fMaskCoord; 1096 | 1097 | uniform sampler2D tex; 1098 | uniform sampler2D mask; 1099 | 1100 | void main() { 1101 | vec4 textureColor = texture2D(tex, fTexCoord); 1102 | vec4 mainColor = rgbaColor * textureColor; 1103 | 1104 | vec4 maskColor = texture2D(mask, fMaskCoord); 1105 | vec4 finalColor = mainColor * maskColor; 1106 | 1107 | gl_FragColor = finalColor; 1108 | } 1109 | "; 1110 | --------------------------------------------------------------------------------