├── examples ├── bunnymark │ ├── src │ │ ├── bunny.png │ │ ├── bunny.rs │ │ └── main.rs │ └── Cargo.toml └── winit │ ├── Cargo.toml │ └── src │ └── main.rs ├── justfile ├── .gitignore ├── crates ├── anyrender_vello_hybrid │ ├── src │ │ ├── lib.rs │ │ ├── scene.rs │ │ └── window_renderer.rs │ └── Cargo.toml ├── anyrender_svg │ ├── src │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── render.rs │ │ └── util.rs │ └── Cargo.toml ├── anyrender_skia │ ├── src │ │ ├── lib.rs │ │ ├── image_renderer.rs │ │ ├── window_renderer.rs │ │ ├── metal.rs │ │ ├── cache.rs │ │ └── opengl.rs │ └── Cargo.toml ├── wgpu_context │ ├── Cargo.toml │ └── src │ │ ├── util.rs │ │ ├── error.rs │ │ ├── buffer_renderer.rs │ │ ├── lib.rs │ │ └── surface_renderer.rs ├── anyrender │ ├── Cargo.toml │ └── src │ │ ├── wasm_send_sync.rs │ │ ├── types.rs │ │ ├── null_backend.rs │ │ └── lib.rs ├── anyrender_vello_cpu │ ├── src │ │ ├── lib.rs │ │ ├── window_renderer.rs │ │ ├── image_renderer.rs │ │ └── scene.rs │ └── Cargo.toml ├── pixels_window_renderer │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── softbuffer_window_renderer │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── anyrender_vello │ ├── src │ ├── lib.rs │ ├── custom_paint_source.rs │ ├── image_renderer.rs │ ├── scene.rs │ └── window_renderer.rs │ └── Cargo.toml ├── LICENSE-MIT ├── Cargo.toml ├── README.md ├── .github └── workflows │ └── ci.yml └── LICENSE-APACHE /examples/bunnymark/src/bunny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DioxusLabs/anyrender/main/examples/bunnymark/src/bunny.png -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | check: 2 | cargo check --workspace 3 | 4 | clippy: 5 | cargo clippy --workspace 6 | 7 | fmt: 8 | cargo fmt --all -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .DS_Store 3 | /scratch 4 | /.vscode 5 | /examples/output/**/*.png 6 | /examples/output/**/*.jpg 7 | /examples/output/**/*.jpeg -------------------------------------------------------------------------------- /crates/anyrender_vello_hybrid/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A [`vello_hybrid`] backend for the [`anyrender`] 2D drawing abstraction 2 | #![cfg_attr(docsrs, feature(doc_cfg))] 3 | 4 | mod scene; 5 | mod window_renderer; 6 | 7 | pub use scene::VelloHybridScenePainter; 8 | pub use window_renderer::*; 9 | -------------------------------------------------------------------------------- /crates/anyrender_svg/src/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 the Vello Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use thiserror::Error; 5 | 6 | /// Triggered when there is an issue parsing user input. 7 | #[derive(Error, Debug)] 8 | #[non_exhaustive] 9 | pub enum Error { 10 | #[error("Error parsing svg: {0}")] 11 | Svg(#[from] usvg::Error), 12 | } 13 | -------------------------------------------------------------------------------- /crates/anyrender_skia/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod image_renderer; 2 | mod scene; 3 | mod window_renderer; 4 | 5 | // Backends 6 | mod cache; 7 | #[cfg(target_os = "macos")] 8 | mod metal; 9 | #[cfg(not(target_os = "macos"))] 10 | mod opengl; 11 | #[cfg(feature = "vulkan")] 12 | mod vulkan; 13 | 14 | pub use image_renderer::SkiaImageRenderer; 15 | pub use scene::SkiaScenePainter; 16 | pub use window_renderer::*; 17 | -------------------------------------------------------------------------------- /crates/wgpu_context/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wgpu_context" 3 | description = "Context for managing WGPU surfaces" 4 | version = "0.1.1" 5 | documentation = "https://docs.rs/wgpu_context" 6 | homepage.workspace = true 7 | repository.workspace = true 8 | license.workspace = true 9 | edition.workspace = true 10 | 11 | [dependencies] 12 | wgpu = { workspace = true } 13 | futures-intrusive = { workspace = true } 14 | -------------------------------------------------------------------------------- /crates/anyrender/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "anyrender" 3 | description = "2D Canvas abstraction" 4 | version = "0.6.2" 5 | documentation = "https://docs.rs/anyrender" 6 | homepage.workspace = true 7 | repository.workspace = true 8 | license.workspace = true 9 | edition.workspace = true 10 | 11 | [dependencies] 12 | kurbo = { workspace = true } 13 | peniko = { workspace = true } 14 | raw-window-handle = { workspace = true } 15 | -------------------------------------------------------------------------------- /crates/anyrender_vello_cpu/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A [`vello_cpu`] backend for the [`anyrender`] 2D drawing abstraction 2 | #![cfg_attr(docsrs, feature(doc_cfg))] 3 | 4 | mod image_renderer; 5 | mod scene; 6 | mod window_renderer; 7 | 8 | pub use image_renderer::VelloCpuImageRenderer; 9 | pub use scene::VelloCpuScenePainter; 10 | 11 | #[cfg(any( 12 | feature = "pixels_window_renderer", 13 | feature = "softbuffer_window_renderer" 14 | ))] 15 | pub use window_renderer::*; 16 | -------------------------------------------------------------------------------- /crates/pixels_window_renderer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pixels_window_renderer" 3 | description = "AnyRender WindowRenderer backed by the pixels crate" 4 | version = "0.1.0" 5 | documentation = "https://docs.rs/pixels_window_renderer" 6 | homepage.workspace = true 7 | repository.workspace = true 8 | license.workspace = true 9 | edition.workspace = true 10 | 11 | [features] 12 | log_frame_times = ["debug_timer/enable"] 13 | 14 | [dependencies] 15 | anyrender = { workspace = true } 16 | debug_timer = { workspace = true } 17 | pixels = { workspace = true } -------------------------------------------------------------------------------- /crates/softbuffer_window_renderer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "softbuffer_window_renderer" 3 | description = "AnyRender WindowRenderer backed by the softbuffer crate" 4 | version = "0.1.0" 5 | documentation = "https://docs.rs/softbuffer_window_renderer" 6 | homepage.workspace = true 7 | repository.workspace = true 8 | license.workspace = true 9 | edition.workspace = true 10 | 11 | [features] 12 | log_frame_times = ["debug_timer/enable"] 13 | 14 | [dependencies] 15 | anyrender = { workspace = true } 16 | debug_timer = { workspace = true } 17 | softbuffer = { workspace = true } -------------------------------------------------------------------------------- /examples/winit/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "winit-example" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | license.workspace = true 6 | publish = false 7 | 8 | [dependencies] 9 | kurbo = { workspace = true } 10 | winit = { workspace = true } 11 | peniko = { workspace = true } 12 | anyrender = { workspace = true } 13 | anyrender_vello = { workspace = true } 14 | anyrender_skia = { workspace = true } 15 | anyrender_vello_hybrid = { workspace = true } 16 | anyrender_vello_cpu = { workspace = true, features = [ 17 | "pixels_window_renderer", 18 | "softbuffer_window_renderer", 19 | ] } -------------------------------------------------------------------------------- /crates/anyrender_vello_cpu/src/window_renderer.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "softbuffer_window_renderer")] 2 | pub use softbuffer_window_renderer::SoftbufferWindowRenderer; 3 | 4 | #[cfg(feature = "pixels_window_renderer")] 5 | pub use pixels_window_renderer::PixelsWindowRenderer; 6 | 7 | #[cfg(feature = "pixels_window_renderer")] 8 | pub type VelloCpuWindowRenderer = PixelsWindowRenderer; 9 | #[cfg(all( 10 | feature = "softbuffer_window_renderer", 11 | not(feature = "pixels_window_renderer") 12 | ))] 13 | pub type VelloCpuWindowRenderer = SoftbufferWindowRenderer; 14 | -------------------------------------------------------------------------------- /crates/anyrender_vello/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A [`vello`] backend for the [`anyrender`] 2D drawing abstraction 2 | mod image_renderer; 3 | mod scene; 4 | mod window_renderer; 5 | 6 | pub mod custom_paint_source; 7 | 8 | pub use custom_paint_source::*; 9 | pub use image_renderer::VelloImageRenderer; 10 | pub use scene::VelloScenePainter; 11 | pub use window_renderer::{VelloRendererOptions, VelloWindowRenderer}; 12 | 13 | pub use wgpu; 14 | 15 | use std::num::NonZeroUsize; 16 | 17 | #[cfg(target_os = "macos")] 18 | const DEFAULT_THREADS: Option = NonZeroUsize::new(1); 19 | #[cfg(not(target_os = "macos"))] 20 | const DEFAULT_THREADS: Option = None; 21 | -------------------------------------------------------------------------------- /crates/anyrender_vello/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "anyrender_vello" 3 | description = "Vello backend for anyrender" 4 | version = "0.6.1" 5 | documentation = "https://docs.rs/anyrender_vello" 6 | homepage.workspace = true 7 | repository.workspace = true 8 | license.workspace = true 9 | edition.workspace = true 10 | 11 | [features] 12 | log_frame_times = ["debug_timer/enable"] 13 | 14 | [dependencies] 15 | anyrender = { workspace = true } 16 | wgpu_context = { workspace = true } 17 | 18 | debug_timer = { workspace = true } 19 | kurbo = { workspace = true } 20 | peniko = { workspace = true } 21 | vello = { workspace = true } 22 | wgpu = { workspace = true } 23 | pollster = { workspace = true } 24 | rustc-hash = { workspace = true } 25 | -------------------------------------------------------------------------------- /examples/bunnymark/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bunnymark" 3 | version = "0.1.0" 4 | edition = "2024" 5 | license.workspace = true 6 | publish = false 7 | 8 | [dependencies] 9 | kurbo = { workspace = true } 10 | winit = { workspace = true } 11 | peniko = { workspace = true } 12 | image = { workspace = true, features = ["png"] } 13 | anyrender = { workspace = true } 14 | anyrender_vello = { workspace = true, features = ["log_frame_times"] } 15 | anyrender_vello_hybrid = { workspace = true, features = ["log_frame_times"] } 16 | anyrender_vello_cpu = { workspace = true, features = ["pixels_window_renderer", "multithreading", "log_frame_times"] } 17 | anyrender_skia = { workspace = true, features = ["log_frame_times"] } 18 | fastrand = "2.3" 19 | -------------------------------------------------------------------------------- /crates/anyrender_vello_hybrid/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "anyrender_vello_hybrid" 3 | description = "vello_hybrid backend for anyrender" 4 | version = "0.1.1" 5 | documentation = "https://docs.rs/anyrender_vello_hybrid" 6 | homepage.workspace = true 7 | repository.workspace = true 8 | license.workspace = true 9 | edition.workspace = true 10 | 11 | [features] 12 | log_frame_times = ["debug_timer/enable"] 13 | 14 | [dependencies] 15 | anyrender = { workspace = true } 16 | debug_timer = { workspace = true } 17 | kurbo = { workspace = true } 18 | peniko = { workspace = true } 19 | vello_hybrid = { workspace = true } 20 | vello_common = { workspace = true } 21 | wgpu = { workspace = true } 22 | pollster = { workspace = true } 23 | rustc-hash = { workspace = true } 24 | wgpu_context = { workspace = true } -------------------------------------------------------------------------------- /crates/anyrender_svg/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "anyrender_svg" 3 | description = "Render SVGs with anyrender" 4 | version = "0.6.3" 5 | documentation = "https://docs.rs/anyrender-svg" 6 | homepage.workspace = true 7 | repository.workspace = true 8 | license.workspace = true 9 | edition.workspace = true 10 | 11 | [features] 12 | default = ["text", "image_format_png", "image_format_gif", "image_format_jpeg", "image_format_webp"] 13 | 14 | text = ["usvg/text", "usvg/memmap-fonts", "usvg/system-fonts"] 15 | 16 | image = ["dep:image"] 17 | image_format_png = ["image", "image/png"] 18 | image_format_jpeg = ["image", "image/jpeg"] 19 | image_format_gif = ["image", "image/gif"] 20 | image_format_webp = ["image", "image/webp"] 21 | 22 | [dependencies] 23 | anyrender = { workspace = true } 24 | peniko = { workspace = true } 25 | kurbo = { workspace = true } 26 | usvg = { workspace = true, default-features = false } 27 | image = { workspace = true, default-features = false, optional = true } 28 | thiserror = "2" -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /crates/anyrender_vello/src/custom_paint_source.rs: -------------------------------------------------------------------------------- 1 | use peniko::ImageData; 2 | use vello::Renderer as VelloRenderer; 3 | use wgpu::Texture; 4 | pub use wgpu_context::DeviceHandle; 5 | 6 | pub trait CustomPaintSource: 'static { 7 | fn resume(&mut self, device_handle: &DeviceHandle); 8 | fn suspend(&mut self); 9 | fn render( 10 | &mut self, 11 | ctx: CustomPaintCtx<'_>, 12 | width: u32, 13 | height: u32, 14 | scale: f64, 15 | ) -> Option; 16 | } 17 | 18 | pub struct CustomPaintCtx<'r> { 19 | pub(crate) renderer: &'r mut VelloRenderer, 20 | } 21 | 22 | #[derive(Clone, PartialEq)] 23 | pub struct TextureHandle(pub ImageData); 24 | 25 | impl CustomPaintCtx<'_> { 26 | pub(crate) fn new<'a>(renderer: &'a mut VelloRenderer) -> CustomPaintCtx<'a> { 27 | CustomPaintCtx { renderer } 28 | } 29 | 30 | pub fn register_texture(&mut self, texture: Texture) -> TextureHandle { 31 | TextureHandle(self.renderer.register_texture(texture)) 32 | } 33 | 34 | pub fn unregister_texture(&mut self, handle: TextureHandle) { 35 | self.renderer.unregister_texture(handle.0); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /crates/anyrender_vello_cpu/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "anyrender_vello_cpu" 3 | description = "vello_cpu backend for anyrender" 4 | version = "0.8.1" 5 | documentation = "https://docs.rs/anyrender_vello_cpu" 6 | homepage.workspace = true 7 | repository.workspace = true 8 | license.workspace = true 9 | edition.workspace = true 10 | 11 | [features] 12 | pixels_window_renderer = ["dep:pixels_window_renderer"] 13 | softbuffer_window_renderer = ["dep:softbuffer_window_renderer"] 14 | multithreading = ["vello_cpu/multithreading"] 15 | log_frame_times = [ 16 | "debug_timer/enable", 17 | "softbuffer_window_renderer?/log_frame_times", 18 | "pixels_window_renderer?/log_frame_times", 19 | ] 20 | 21 | [dependencies] 22 | anyrender = { workspace = true } 23 | debug_timer = { workspace = true } 24 | kurbo = { workspace = true } 25 | peniko = { workspace = true } 26 | 27 | # WindowRenderer backends 28 | softbuffer_window_renderer = { workspace = true, optional = true } 29 | pixels_window_renderer = { workspace = true, optional = true } 30 | 31 | # External vello_cpu 32 | vello_cpu = { workspace = true } 33 | 34 | [package.metadata.docs.rs] 35 | features = ["pixels_window_renderer", "softbuffer_window_renderer"] -------------------------------------------------------------------------------- /crates/anyrender/src/wasm_send_sync.rs: -------------------------------------------------------------------------------- 1 | //! Traits that imply `Send`/`Sync` only on non-wasm platforms. For interop with wgpu. 2 | 3 | use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; 4 | 5 | /// A raw window handle that is `WasmNotSendSync`. For interop with wgpu. 6 | pub trait WindowHandle: HasWindowHandle + HasDisplayHandle + WasmNotSendSync {} 7 | impl WindowHandle for T {} 8 | 9 | /// Trait that implies `Send` and `Sync` on non-wasm platforms 10 | pub trait WasmNotSendSync: WasmNotSend + WasmNotSync {} 11 | impl WasmNotSendSync for T {} 12 | 13 | /// Trait that implies `Send` on non-wasm platforms 14 | #[cfg(not(target_arch = "wasm32"))] 15 | pub trait WasmNotSend: Send {} 16 | #[cfg(not(target_arch = "wasm32"))] 17 | impl WasmNotSend for T {} 18 | /// Trait that implies `Send` on non-wasm platforms 19 | #[cfg(target_arch = "wasm32")] 20 | pub trait WasmNotSend {} 21 | #[cfg(target_arch = "wasm32")] 22 | impl WasmNotSend for T {} 23 | 24 | /// Trait that implies `Sync` on non-wasm platforms 25 | #[cfg(not(target_arch = "wasm32"))] 26 | pub trait WasmNotSync: Sync {} 27 | #[cfg(not(target_arch = "wasm32"))] 28 | impl WasmNotSync for T {} 29 | /// Trait that implies `Sync` on non-wasm platforms 30 | #[cfg(target_arch = "wasm32")] 31 | pub trait WasmNotSync {} 32 | #[cfg(target_arch = "wasm32")] 33 | impl WasmNotSync for T {} 34 | -------------------------------------------------------------------------------- /crates/anyrender_vello_cpu/src/image_renderer.rs: -------------------------------------------------------------------------------- 1 | use crate::VelloCpuScenePainter; 2 | use anyrender::ImageRenderer; 3 | use debug_timer::debug_timer; 4 | use vello_cpu::{RenderContext, RenderMode}; 5 | 6 | pub struct VelloCpuImageRenderer { 7 | scene: VelloCpuScenePainter, 8 | } 9 | 10 | impl ImageRenderer for VelloCpuImageRenderer { 11 | type ScenePainter<'a> = VelloCpuScenePainter; 12 | 13 | fn new(width: u32, height: u32) -> Self { 14 | Self { 15 | scene: VelloCpuScenePainter(RenderContext::new(width as u16, height as u16)), 16 | } 17 | } 18 | 19 | fn resize(&mut self, width: u32, height: u32) { 20 | self.scene.0 = RenderContext::new(width as u16, height as u16); 21 | } 22 | 23 | fn reset(&mut self) { 24 | self.scene.0.reset(); 25 | } 26 | 27 | fn render)>(&mut self, draw_fn: F, buffer: &mut [u8]) { 28 | debug_timer!(timer, feature = "log_frame_times"); 29 | 30 | draw_fn(&mut self.scene); 31 | timer.record_time("cmds"); 32 | 33 | self.scene.0.flush(); 34 | timer.record_time("flush"); 35 | 36 | self.scene.0.render_to_buffer( 37 | buffer, 38 | self.scene.0.width(), 39 | self.scene.0.height(), 40 | RenderMode::OptimizeSpeed, 41 | ); 42 | timer.record_time("render"); 43 | 44 | timer.print_times("vello_cpu: "); 45 | } 46 | 47 | fn render_to_vec)>( 48 | &mut self, 49 | draw_fn: F, 50 | buffer: &mut Vec, 51 | ) { 52 | let width = self.scene.0.width(); 53 | let height = self.scene.0.height(); 54 | buffer.resize(width as usize * height as usize * 4, 0); 55 | self.render(draw_fn, &mut *buffer); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /crates/anyrender_skia/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "anyrender_skia" 3 | description = "Skia backend for anyrender" 4 | version = "0.2.0" 5 | documentation = "https://docs.rs/anyrender_skia" 6 | homepage.workspace = true 7 | repository.workspace = true 8 | license.workspace = true 9 | edition.workspace = true 10 | 11 | [features] 12 | pixels_window_renderer = ["dep:pixels_window_renderer"] 13 | softbuffer_window_renderer = ["dep:softbuffer_window_renderer"] 14 | log_frame_times = ["debug_timer/enable", "pixels_window_renderer?/log_frame_times", "softbuffer_window_renderer?/log_frame_times"] 15 | vulkan = ["skia-safe/vulkan"] 16 | 17 | [dependencies] 18 | anyrender = { workspace = true } 19 | debug_timer = { workspace = true } 20 | kurbo = { workspace = true } 21 | peniko = { workspace = true } 22 | raw-window-handle = { workspace = true } 23 | softbuffer_window_renderer = { workspace = true, optional = true } 24 | pixels_window_renderer = { workspace = true, optional = true } 25 | skia-safe = { version = "0.90.0", features = ["gl", "pdf", "textlayout"]} 26 | oaty = "0.1" 27 | gl = "0.14.0" 28 | ash = "^0.38.0" 29 | ash-window = "0.13.0" 30 | glutin = { version = "0.32.3", features = ["default", "wgl", "egl"] } 31 | hashbrown = "0.16.0" 32 | 33 | [target.'cfg(target_os = "macos")'.dependencies] 34 | objc2 = { version = "0.6.3", default-features = false } 35 | objc2-core-foundation = { version = "0.3.2", default-features = false } 36 | objc2-metal = { version = "0.3.2", default-features = false, features = ["MTLCommandBuffer", "MTLCommandQueue", "MTLDrawable", "MTLPixelFormat"] } 37 | objc2-app-kit = { version = "0.3.2", default-features = false, features = ["objc2-quartz-core", "NSView"] } 38 | objc2-quartz-core = { version = "0.3.2", default-features = false, features = ["objc2-core-foundation", "objc2-metal", "CAMetalLayer"] } 39 | skia-safe = { version = "0.90.0", features = ["metal"] } 40 | -------------------------------------------------------------------------------- /crates/wgpu_context/src/util.rs: -------------------------------------------------------------------------------- 1 | use crate::WgpuContextError; 2 | use wgpu::{Device, TextureFormat, TextureUsages, TextureView}; 3 | 4 | /// Block on a future, polling the device as needed. 5 | /// 6 | /// This will deadlock if the future is awaiting anything other than GPU progress. 7 | #[cfg_attr(docsrs, doc(hidden))] 8 | pub fn block_on_wgpu(device: &Device, fut: F) -> Result { 9 | if cfg!(target_arch = "wasm32") { 10 | panic!("WGPU is inherently async on WASM, so blocking doesn't work."); 11 | } 12 | 13 | // Dummy waker 14 | struct NullWake; 15 | impl std::task::Wake for NullWake { 16 | fn wake(self: std::sync::Arc) {} 17 | } 18 | 19 | // Create context to poll future with 20 | let waker = std::task::Waker::from(std::sync::Arc::new(NullWake)); 21 | let mut context = std::task::Context::from_waker(&waker); 22 | 23 | // Same logic as `pin_mut!` macro from `pin_utils`. 24 | let mut fut = std::pin::pin!(fut); 25 | loop { 26 | match fut.as_mut().poll(&mut context) { 27 | std::task::Poll::Pending => { 28 | device.poll(wgpu::PollType::Wait)?; 29 | } 30 | std::task::Poll::Ready(item) => break Ok(item), 31 | } 32 | } 33 | } 34 | 35 | /// Create a WGPU Texture, returning a default TextureView 36 | pub(crate) fn create_texture( 37 | width: u32, 38 | height: u32, 39 | format: TextureFormat, 40 | usage: TextureUsages, 41 | device: &Device, 42 | ) -> TextureView { 43 | let texture = device.create_texture(&wgpu::TextureDescriptor { 44 | label: None, 45 | size: wgpu::Extent3d { 46 | width, 47 | height, 48 | depth_or_array_layers: 1, 49 | }, 50 | mip_level_count: 1, 51 | sample_count: 1, 52 | dimension: wgpu::TextureDimension::D2, 53 | usage, 54 | format, 55 | view_formats: &[], 56 | }); 57 | 58 | texture.create_view(&wgpu::TextureViewDescriptor::default()) 59 | } 60 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "crates/anyrender", 4 | "crates/anyrender_skia", 5 | "crates/anyrender_vello", 6 | "crates/anyrender_vello_cpu", 7 | "crates/anyrender_vello_hybrid", 8 | "crates/anyrender_svg", 9 | "crates/wgpu_context", 10 | "crates/pixels_window_renderer", 11 | "crates/softbuffer_window_renderer", 12 | "examples/winit", 13 | "examples/bunnymark", 14 | ] 15 | resolver = "2" 16 | 17 | [workspace.package] 18 | version = "0.6.0" 19 | license = "MIT OR Apache-2.0" 20 | homepage = "https://github.com/dioxuslabs/anyrender" 21 | repository = "https://github.com/dioxuslabs/anyrender" 22 | categories = ["gui"] 23 | edition = "2024" 24 | rust-version = "1.86.0" 25 | 26 | [workspace.dependencies] 27 | # AnyRender dependencies (in-repo) 28 | anyrender = { version = "0.6.1", path = "./crates/anyrender" } 29 | anyrender_skia = { version = "0.2.0", path = "./crates/anyrender_skia" } 30 | anyrender_vello = { version = "0.6.1", path = "./crates/anyrender_vello" } 31 | anyrender_vello_cpu = { version = "0.8.1", path = "./crates/anyrender_vello_cpu" } 32 | anyrender_vello_hybrid = { version = "0.1.1", path = "./crates/anyrender_vello_hybrid" } 33 | anyrender_svg = { version = "0.6.0", path = "./crates/anyrender_svg" } 34 | wgpu_context = { version = "0.1.1", path = "./crates/wgpu_context" } 35 | pixels_window_renderer = { version = "0.1.0", path = "./crates/pixels_window_renderer" } 36 | softbuffer_window_renderer = { version = "0.1.0", path = "./crates/softbuffer_window_renderer" } 37 | 38 | # Linebender 39 | color = "0.3" 40 | linebender_resource_handle = "0.1" 41 | peniko = "0.5.0" 42 | kurbo = "0.12" 43 | vello = { version = "0.6", features = [ "wgpu" ] } 44 | vello_cpu = { version = "0.0.4", default-features = false, features = ["std", "text"] } 45 | vello_hybrid = { version = "0.0.4" } 46 | vello_common = { version = "0.0.4" } 47 | 48 | # Rendering 49 | wgpu = "26" 50 | raw-window-handle = "0.6.0" 51 | softbuffer = "0.4" 52 | pixels = "0.15" 53 | 54 | # SVG 55 | usvg = { version = "0.45.1", default-features = false } 56 | image = { version = "0.25", default-features = false } 57 | 58 | # Other dependencies 59 | debug_timer = "0.1.1" 60 | rustc-hash = "2" 61 | futures-util = "0.3.31" 62 | futures-intrusive = "0.5.0" 63 | pollster = "0.4" 64 | 65 | # Dev-dependencies 66 | winit = { version = "0.30.2", features = ["rwh_06"] } 67 | -------------------------------------------------------------------------------- /crates/wgpu_context/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error type for WGPU Context 2 | 3 | use std::error::Error; 4 | use std::fmt::Display; 5 | use wgpu::{PollError, RequestAdapterError, RequestDeviceError}; 6 | 7 | /// Errors that can occur in WgpuContext. 8 | #[derive(Debug)] 9 | pub enum WgpuContextError { 10 | /// There is no available device with the features required by Vello. 11 | NoCompatibleDevice, 12 | /// Failed to create surface. 13 | /// See [`wgpu::CreateSurfaceError`] for more information. 14 | WgpuCreateSurfaceError(wgpu::CreateSurfaceError), 15 | /// Surface doesn't support the required texture formats. 16 | /// Make sure that you have a surface which provides one of 17 | /// `TextureFormat::Rgba8Unorm` 18 | /// or `TextureFormat::Bgra8Unorm` as texture formats. 19 | // TODO: Why does this restriction exist? 20 | UnsupportedSurfaceFormat, 21 | /// Wgpu failed to request an adapter 22 | RequestAdapterError(RequestAdapterError), 23 | /// Wgpu failed to request a device 24 | RequestDeviceError(RequestDeviceError), 25 | /// Wgpu failed to poll a device 26 | PollError(PollError), 27 | } 28 | 29 | impl Display for WgpuContextError { 30 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 31 | match self { 32 | Self::NoCompatibleDevice => writeln!(f, "Couldn't find suitable device"), 33 | Self::WgpuCreateSurfaceError(inner) => { 34 | writeln!(f, "Couldn't create wgpu surface")?; 35 | inner.fmt(f) 36 | } 37 | Self::UnsupportedSurfaceFormat => { 38 | writeln!( 39 | f, 40 | "Couldn't find `Rgba8Unorm` or `Bgra8Unorm` texture formats for surface" 41 | ) 42 | } 43 | Self::RequestAdapterError(inner) => { 44 | writeln!(f, "Couldn't request an adapter: {:#}", inner) 45 | } 46 | Self::RequestDeviceError(inner) => { 47 | writeln!(f, "Couldn't request a device: {:#}", inner) 48 | } 49 | Self::PollError(inner) => { 50 | writeln!(f, "Couldn't poll a device: {:#}", inner) 51 | } 52 | } 53 | } 54 | } 55 | 56 | impl Error for WgpuContextError {} 57 | impl From for WgpuContextError { 58 | fn from(value: wgpu::CreateSurfaceError) -> Self { 59 | Self::WgpuCreateSurfaceError(value) 60 | } 61 | } 62 | 63 | impl From for WgpuContextError { 64 | fn from(value: RequestAdapterError) -> Self { 65 | Self::RequestAdapterError(value) 66 | } 67 | } 68 | 69 | impl From for WgpuContextError { 70 | fn from(value: RequestDeviceError) -> Self { 71 | Self::RequestDeviceError(value) 72 | } 73 | } 74 | 75 | impl From for WgpuContextError { 76 | fn from(value: PollError) -> Self { 77 | Self::PollError(value) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /crates/anyrender_skia/src/image_renderer.rs: -------------------------------------------------------------------------------- 1 | use anyrender::ImageRenderer; 2 | use debug_timer::debug_timer; 3 | use skia_safe::{AlphaType, Color, ColorType, ImageInfo, SurfaceProps, graphics, surfaces}; 4 | 5 | use crate::{SkiaScenePainter, scene::SkiaSceneCache}; 6 | 7 | pub struct SkiaImageRenderer { 8 | image_info: ImageInfo, 9 | surface_props: SurfaceProps, 10 | scene_cache: SkiaSceneCache, 11 | } 12 | 13 | impl ImageRenderer for SkiaImageRenderer { 14 | type ScenePainter<'a> 15 | = SkiaScenePainter<'a> 16 | where 17 | Self: 'a; 18 | 19 | fn new(width: u32, height: u32) -> Self { 20 | graphics::set_font_cache_count_limit(100); 21 | graphics::set_typeface_cache_count_limit(100); 22 | graphics::set_resource_cache_total_bytes_limit(10485760); 23 | 24 | Self { 25 | image_info: ImageInfo::new( 26 | (width as i32, height as i32), 27 | ColorType::RGBA8888, 28 | AlphaType::Opaque, 29 | None, 30 | ), 31 | surface_props: SurfaceProps::default(), 32 | scene_cache: SkiaSceneCache::default(), 33 | } 34 | } 35 | 36 | fn resize(&mut self, width: u32, height: u32) { 37 | self.image_info = ImageInfo::new( 38 | (width as i32, height as i32), 39 | ColorType::RGBA8888, 40 | AlphaType::Opaque, 41 | None, 42 | ); 43 | } 44 | 45 | fn reset(&mut self) {} 46 | 47 | fn render_to_vec)>( 48 | &mut self, 49 | draw_fn: F, 50 | buffer: &mut Vec, 51 | ) { 52 | debug_timer!(timer, feature = "log_frame_times"); 53 | 54 | let mut surface = surfaces::wrap_pixels( 55 | &self.image_info, 56 | &mut buffer[..], 57 | None, 58 | Some(&self.surface_props), 59 | ) 60 | .unwrap(); 61 | 62 | surface.canvas().clear(Color::WHITE); 63 | 64 | draw_fn(&mut SkiaScenePainter { 65 | inner: surface.canvas(), 66 | cache: &mut self.scene_cache, 67 | }); 68 | timer.record_time("render"); 69 | 70 | self.scene_cache.next_gen(); 71 | timer.record_time("cache next gen"); 72 | 73 | timer.print_times("skia_raster: "); 74 | } 75 | 76 | fn render)>(&mut self, draw_fn: F, buffer: &mut [u8]) { 77 | debug_timer!(timer, feature = "log_frame_times"); 78 | 79 | let mut surface = surfaces::wrap_pixels( 80 | &self.image_info, 81 | &mut buffer[..], 82 | None, 83 | Some(&self.surface_props), 84 | ) 85 | .unwrap(); 86 | 87 | surface.canvas().clear(Color::WHITE); 88 | 89 | draw_fn(&mut SkiaScenePainter { 90 | inner: surface.canvas(), 91 | cache: &mut self.scene_cache, 92 | }); 93 | timer.record_time("render"); 94 | 95 | self.scene_cache.next_gen(); 96 | timer.record_time("cache next gen"); 97 | 98 | timer.print_times("skia_raster: "); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /crates/anyrender_svg/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 the Vello Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | //! Render an SVG into any impl of [`anyrender::PaintScene`]. 5 | //! 6 | //! This currently lacks support for some important SVG features. Known missing features include: masking, filter effects, group backgrounds 7 | //! path shape-rendering, and patterns. 8 | 9 | // LINEBENDER LINT SET - lib.rs - v1 10 | // See https://linebender.org/wiki/canonical-lints/ 11 | // These lints aren't included in Cargo.toml because they 12 | // shouldn't apply to examples and tests 13 | #![warn(unused_crate_dependencies)] 14 | #![warn(clippy::print_stdout, clippy::print_stderr)] 15 | // END LINEBENDER LINT SET 16 | #![cfg_attr(docsrs, feature(doc_cfg))] 17 | // The following lints are part of the Linebender standard set, 18 | // but resolving them has been deferred for now. 19 | // Feel free to send a PR that solves one or more of these. 20 | #![allow(missing_docs, clippy::shadow_unrelated, clippy::missing_errors_doc)] 21 | #![cfg_attr(test, allow(unused_crate_dependencies))] // Some dev dependencies are only used in tests 22 | 23 | mod error; 24 | mod render; 25 | mod util; 26 | 27 | pub use error::Error; 28 | pub use usvg; 29 | 30 | use anyrender::PaintScene; 31 | use kurbo::Affine; 32 | 33 | /// Append an SVG to an [`anyrender::PaintScene`]. 34 | /// 35 | /// This will draw a red box over (some) unsupported elements. 36 | pub fn render_svg_str( 37 | scene: &mut S, 38 | svg: &str, 39 | transform: Affine, 40 | ) -> Result<(), Error> { 41 | let opt = usvg::Options::default(); 42 | let tree = usvg::Tree::from_str(svg, &opt)?; 43 | render_svg_tree(scene, &tree, transform); 44 | Ok(()) 45 | } 46 | 47 | /// Append an SVG to an [`anyrender::PaintScene`] (with custom error handling). 48 | /// 49 | /// See the [module level documentation](crate#unsupported-features) for a list of some unsupported svg features 50 | pub fn render_svg_str_with( 51 | scene: &mut S, 52 | svg: &str, 53 | transform: Affine, 54 | error_handler: &mut F, 55 | ) -> Result<(), Error> { 56 | let opt = usvg::Options::default(); 57 | let tree = usvg::Tree::from_str(svg, &opt)?; 58 | render_svg_tree_with(scene, &tree, transform, error_handler); 59 | Ok(()) 60 | } 61 | 62 | /// Append a [`usvg::Tree`] to an [`anyrender::PaintScene`]. 63 | /// 64 | /// This will draw a red box over (some) unsupported elements. 65 | pub fn render_svg_tree(scene: &mut S, svg: &usvg::Tree, transform: Affine) { 66 | render_svg_tree_with(scene, svg, transform, &mut util::default_error_handler); 67 | } 68 | 69 | /// Append a [`usvg::Tree`] to an [`anyrender::PaintScene`] (with custom error handling). 70 | /// 71 | /// See the [module level documentation](crate#unsupported-features) for a list of some unsupported svg features 72 | pub fn render_svg_tree_with( 73 | scene: &mut S, 74 | svg: &usvg::Tree, 75 | transform: Affine, 76 | error_handler: &mut F, 77 | ) { 78 | render::render_group( 79 | scene, 80 | svg.root(), 81 | Affine::IDENTITY, 82 | transform, 83 | error_handler, 84 | ); 85 | } 86 | 87 | #[cfg(test)] 88 | mod tests { 89 | // CI will fail unless cargo nextest can execute at least one test per workspace. 90 | // Delete this dummy test once we have an actual real test. 91 | #[test] 92 | fn dummy_test_until_we_have_a_real_test() {} 93 | } 94 | -------------------------------------------------------------------------------- /crates/anyrender/src/types.rs: -------------------------------------------------------------------------------- 1 | //! Types that are used within the Anyrender traits 2 | 3 | use peniko::{Brush, BrushRef, Color, Gradient, ImageBrush, ImageBrushRef}; 4 | use std::{any::Any, sync::Arc}; 5 | 6 | pub type NormalizedCoord = i16; 7 | 8 | /// A positioned glyph. 9 | #[derive(Copy, Clone, Debug)] 10 | pub struct Glyph { 11 | pub id: u32, 12 | pub x: f32, 13 | pub y: f32, 14 | } 15 | 16 | #[derive(Copy, Clone, Debug)] 17 | pub struct CustomPaint { 18 | pub source_id: u64, 19 | pub width: u32, 20 | pub height: u32, 21 | pub scale: f64, 22 | } 23 | 24 | #[derive(Clone, Debug)] 25 | pub enum Paint> { 26 | /// Solid color brush. 27 | Solid(Color), 28 | /// Gradient brush. 29 | Gradient(G), 30 | /// Image brush. 31 | Image(I), 32 | /// Custom paint (type erased as each backend will have their own) 33 | Custom(C), 34 | } 35 | 36 | pub type PaintRef<'a> = Paint, &'a Gradient, &'a (dyn Any + Send + Sync)>; 37 | 38 | impl Paint { 39 | pub fn as_ref(&self) -> PaintRef<'_> { 40 | match self { 41 | Paint::Solid(color) => Paint::Solid(*color), 42 | Paint::Gradient(gradient) => Paint::Gradient(gradient), 43 | Paint::Image(image) => Paint::Image(image.as_ref()), 44 | 45 | // Custom paints are translated into "invisible" where they are not supported 46 | Paint::Custom(custom) => Paint::Custom(custom.as_ref()), 47 | } 48 | } 49 | } 50 | 51 | impl<'a> From<&'a Paint> for PaintRef<'a> { 52 | fn from(paint: &'a Paint) -> Self { 53 | paint.as_ref() 54 | } 55 | } 56 | 57 | impl<'a> From> for BrushRef<'a> { 58 | fn from(value: PaintRef<'a>) -> Self { 59 | match value { 60 | Paint::Solid(color) => Brush::Solid(color), 61 | Paint::Gradient(gradient) => Brush::Gradient(gradient), 62 | Paint::Image(image) => Brush::Image(image), 63 | 64 | // Custom paints are translated into "invisible" where they are not supported 65 | Paint::Custom(_) => Brush::Solid(Color::TRANSPARENT), 66 | } 67 | } 68 | } 69 | 70 | // #[derive(Clone, Debug)] 71 | // pub enum PaintRef<'a> { 72 | // /// Solid color brush. 73 | // Solid(Color), 74 | // /// Gradient brush. 75 | // Gradient(&'a Gradient), 76 | // /// Image brush. 77 | // Image(ImageBrushRef<'a>), 78 | // /// Custom paint (type erased as each backend will have their own) 79 | // Custom(Arc), 80 | // } 81 | 82 | impl From for Paint { 83 | fn from(value: Color) -> Self { 84 | Paint::Solid(value) 85 | } 86 | } 87 | impl<'a, I, C> From<&'a Gradient> for Paint { 88 | fn from(value: &'a Gradient) -> Self { 89 | Paint::Gradient(value) 90 | } 91 | } 92 | impl<'a, G, C> From> for Paint, G, C> { 93 | fn from(value: ImageBrushRef<'a>) -> Self { 94 | Paint::Image(value) 95 | } 96 | } 97 | impl From> for Paint> { 98 | fn from(value: Arc) -> Self { 99 | Paint::Custom(value) 100 | } 101 | } 102 | impl<'a> From> for PaintRef<'a> { 103 | fn from(value: BrushRef<'a>) -> Self { 104 | match value { 105 | BrushRef::Solid(color) => PaintRef::Solid(color), 106 | BrushRef::Gradient(gradient) => PaintRef::Gradient(gradient), 107 | BrushRef::Image(image) => PaintRef::Image(image), 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /crates/anyrender_vello/src/image_renderer.rs: -------------------------------------------------------------------------------- 1 | use anyrender::ImageRenderer; 2 | use rustc_hash::FxHashMap; 3 | use vello::{Renderer as VelloRenderer, RendererOptions, Scene as VelloScene}; 4 | use wgpu::TextureUsages; 5 | use wgpu_context::{BufferRenderer, BufferRendererConfig, WGPUContext}; 6 | 7 | use crate::{DEFAULT_THREADS, VelloScenePainter}; 8 | 9 | pub struct VelloImageRenderer { 10 | buffer_renderer: BufferRenderer, 11 | vello_renderer: VelloRenderer, 12 | scene: VelloScene, 13 | } 14 | 15 | impl ImageRenderer for VelloImageRenderer { 16 | type ScenePainter<'a> 17 | = VelloScenePainter<'a, 'a> 18 | where 19 | Self: 'a; 20 | 21 | fn new(width: u32, height: u32) -> Self { 22 | // Create WGPUContext 23 | let mut context = WGPUContext::new(); 24 | 25 | // Create wgpu_context::BufferRenderer 26 | let buffer_renderer = 27 | pollster::block_on(context.create_buffer_renderer(BufferRendererConfig { 28 | width, 29 | height, 30 | usage: TextureUsages::STORAGE_BINDING, 31 | })) 32 | .expect("No compatible device found"); 33 | 34 | // Create vello::Renderer 35 | let vello_renderer = VelloRenderer::new( 36 | buffer_renderer.device(), 37 | RendererOptions { 38 | use_cpu: false, 39 | num_init_threads: DEFAULT_THREADS, 40 | antialiasing_support: vello::AaSupport::area_only(), 41 | pipeline_cache: None, 42 | }, 43 | ) 44 | .expect("Got non-Send/Sync error from creating renderer"); 45 | 46 | Self { 47 | buffer_renderer, 48 | vello_renderer, 49 | scene: VelloScene::new(), 50 | } 51 | } 52 | 53 | fn resize(&mut self, width: u32, height: u32) { 54 | self.buffer_renderer.resize(width, height); 55 | } 56 | 57 | fn reset(&mut self) { 58 | self.scene.reset(); 59 | } 60 | 61 | fn render_to_vec)>( 62 | &mut self, 63 | draw_fn: F, 64 | cpu_buffer: &mut Vec, 65 | ) { 66 | let size = self.buffer_renderer.size(); 67 | cpu_buffer.clear(); 68 | cpu_buffer.reserve((size.width * size.height * 4) as usize); 69 | self.render(draw_fn, cpu_buffer); 70 | } 71 | 72 | fn render)>( 73 | &mut self, 74 | draw_fn: F, 75 | cpu_buffer: &mut [u8], 76 | ) { 77 | draw_fn(&mut VelloScenePainter { 78 | inner: &mut self.scene, 79 | renderer: Some(&mut self.vello_renderer), 80 | custom_paint_sources: Some(&mut FxHashMap::default()), 81 | }); 82 | 83 | let size = self.buffer_renderer.size(); 84 | self.vello_renderer 85 | .render_to_texture( 86 | self.buffer_renderer.device(), 87 | self.buffer_renderer.queue(), 88 | &self.scene, 89 | &self.buffer_renderer.target_texture_view(), 90 | &vello::RenderParams { 91 | base_color: vello::peniko::Color::TRANSPARENT, 92 | width: size.width, 93 | height: size.height, 94 | antialiasing_method: vello::AaConfig::Area, 95 | }, 96 | ) 97 | .expect("Got non-Send/Sync error from rendering"); 98 | 99 | self.buffer_renderer.copy_texture_to_buffer(cpu_buffer); 100 | 101 | // Empty the Vello scene (memory optimisation) 102 | self.scene.reset(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /crates/pixels_window_renderer/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! An AnyRender WindowRenderer for rendering pixel buffers using the pixels crate 2 | 3 | #![cfg_attr(docsrs, feature(doc_cfg))] 4 | 5 | use anyrender::{ImageRenderer, WindowHandle, WindowRenderer}; 6 | use debug_timer::debug_timer; 7 | use pixels::{Pixels, SurfaceTexture, wgpu::Color}; 8 | use std::sync::Arc; 9 | 10 | // Simple struct to hold the state of the renderer 11 | pub struct ActiveRenderState { 12 | // surface: SurfaceTexture>, 13 | pixels: Pixels<'static>, 14 | } 15 | 16 | #[allow(clippy::large_enum_variant)] 17 | pub enum RenderState { 18 | Active(ActiveRenderState), 19 | Suspended, 20 | } 21 | 22 | pub struct PixelsWindowRenderer { 23 | // The fields MUST be in this order, so that the surface is dropped before the window 24 | // Window is cached even when suspended so that it can be reused when the app is resumed after being suspended 25 | render_state: RenderState, 26 | window_handle: Option>, 27 | renderer: Renderer, 28 | } 29 | 30 | impl PixelsWindowRenderer { 31 | #[allow(clippy::new_without_default)] 32 | pub fn new() -> Self { 33 | Self::with_renderer(Renderer::new(0, 0)) 34 | } 35 | 36 | pub fn with_renderer(renderer: R) -> PixelsWindowRenderer { 37 | PixelsWindowRenderer { 38 | render_state: RenderState::Suspended, 39 | window_handle: None, 40 | renderer, 41 | } 42 | } 43 | } 44 | 45 | impl WindowRenderer for PixelsWindowRenderer { 46 | type ScenePainter<'a> 47 | = ::ScenePainter<'a> 48 | where 49 | Renderer: 'a; 50 | 51 | fn is_active(&self) -> bool { 52 | matches!(self.render_state, RenderState::Active(_)) 53 | } 54 | 55 | fn resume(&mut self, window_handle: Arc, width: u32, height: u32) { 56 | let surface = SurfaceTexture::new(width, height, window_handle.clone()); 57 | let mut pixels = Pixels::new(width, height, surface).unwrap(); 58 | pixels.enable_vsync(true); 59 | pixels.clear_color(Color { 60 | r: 1.0, 61 | g: 1.0, 62 | b: 1.0, 63 | a: 1.0, 64 | }); 65 | self.render_state = RenderState::Active(ActiveRenderState { pixels }); 66 | self.window_handle = Some(window_handle); 67 | 68 | self.set_size(width, height); 69 | } 70 | 71 | fn suspend(&mut self) { 72 | self.render_state = RenderState::Suspended; 73 | } 74 | 75 | fn set_size(&mut self, physical_width: u32, physical_height: u32) { 76 | if let RenderState::Active(state) = &mut self.render_state { 77 | state 78 | .pixels 79 | .resize_buffer(physical_width, physical_height) 80 | .unwrap(); 81 | state 82 | .pixels 83 | .resize_surface(physical_width, physical_height) 84 | .unwrap(); 85 | self.renderer.resize(physical_width, physical_height); 86 | }; 87 | } 88 | 89 | fn render)>(&mut self, draw_fn: F) { 90 | let RenderState::Active(state) = &mut self.render_state else { 91 | return; 92 | }; 93 | 94 | debug_timer!(timer, feature = "log_frame_times"); 95 | 96 | // Paint 97 | self.renderer.render(draw_fn, state.pixels.frame_mut()); 98 | timer.record_time("render"); 99 | 100 | state.pixels.render().unwrap(); 101 | timer.record_time("present"); 102 | timer.print_times("pixels: "); 103 | 104 | // Reset the renderer ready for the next render 105 | self.renderer.reset(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /crates/anyrender/src/null_backend.rs: -------------------------------------------------------------------------------- 1 | //! A dummy implementation of the AnyRender traits while simply ignores all commands 2 | 3 | use crate::{ImageRenderer, PaintScene, WindowHandle, WindowRenderer}; 4 | use std::sync::Arc; 5 | 6 | #[derive(Copy, Clone, Default)] 7 | pub struct NullWindowRenderer { 8 | is_active: bool, 9 | } 10 | 11 | impl NullWindowRenderer { 12 | pub fn new() -> Self { 13 | Self::default() 14 | } 15 | } 16 | 17 | impl WindowRenderer for NullWindowRenderer { 18 | type ScenePainter<'a> 19 | = NullScenePainter 20 | where 21 | Self: 'a; 22 | 23 | fn resume(&mut self, _window: Arc, _width: u32, _height: u32) { 24 | self.is_active = true; 25 | } 26 | 27 | fn suspend(&mut self) { 28 | self.is_active = false 29 | } 30 | 31 | fn is_active(&self) -> bool { 32 | self.is_active 33 | } 34 | 35 | fn set_size(&mut self, _width: u32, _height: u32) {} 36 | 37 | fn render)>(&mut self, _draw_fn: F) {} 38 | } 39 | 40 | #[derive(Copy, Clone, Default)] 41 | pub struct NullImageRenderer; 42 | 43 | impl NullImageRenderer { 44 | pub fn new() -> Self { 45 | Self 46 | } 47 | } 48 | 49 | impl ImageRenderer for NullImageRenderer { 50 | type ScenePainter<'a> 51 | = NullScenePainter 52 | where 53 | Self: 'a; 54 | 55 | fn new(_width: u32, _height: u32) -> Self { 56 | Self 57 | } 58 | 59 | fn resize(&mut self, _width: u32, _height: u32) {} 60 | 61 | fn reset(&mut self) {} 62 | 63 | fn render_to_vec)>( 64 | &mut self, 65 | _draw_fn: F, 66 | _vec: &mut Vec, 67 | ) { 68 | } 69 | 70 | fn render)>(&mut self, _draw_fn: F, _buffer: &mut [u8]) {} 71 | } 72 | 73 | #[derive(Copy, Clone, Default)] 74 | pub struct NullScenePainter; 75 | 76 | impl NullScenePainter { 77 | pub fn new() -> Self { 78 | Self 79 | } 80 | } 81 | 82 | impl PaintScene for NullScenePainter { 83 | fn reset(&mut self) {} 84 | 85 | fn push_layer( 86 | &mut self, 87 | _blend: impl Into, 88 | _alpha: f32, 89 | _transform: kurbo::Affine, 90 | _clip: &impl kurbo::Shape, 91 | ) { 92 | } 93 | 94 | fn push_clip_layer(&mut self, _transform: kurbo::Affine, _clip: &impl kurbo::Shape) {} 95 | 96 | fn pop_layer(&mut self) {} 97 | 98 | fn stroke<'a>( 99 | &mut self, 100 | _style: &kurbo::Stroke, 101 | _transform: kurbo::Affine, 102 | _brush: impl Into>, 103 | _brush_transform: Option, 104 | _shape: &impl kurbo::Shape, 105 | ) { 106 | } 107 | 108 | fn fill<'a>( 109 | &mut self, 110 | _style: peniko::Fill, 111 | _transform: kurbo::Affine, 112 | _brush: impl Into>, 113 | _brush_transform: Option, 114 | _shape: &impl kurbo::Shape, 115 | ) { 116 | } 117 | 118 | fn draw_glyphs<'a, 's: 'a>( 119 | &'s mut self, 120 | _font: &'a peniko::FontData, 121 | _font_size: f32, 122 | _hint: bool, 123 | _normalized_coords: &'a [crate::NormalizedCoord], 124 | _style: impl Into>, 125 | _brush: impl Into>, 126 | _brush_alpha: f32, 127 | _transform: kurbo::Affine, 128 | _glyph_transform: Option, 129 | _glyphs: impl Iterator, 130 | ) { 131 | } 132 | 133 | fn draw_box_shadow( 134 | &mut self, 135 | _transform: kurbo::Affine, 136 | _rect: kurbo::Rect, 137 | _brush: peniko::Color, 138 | _radius: f64, 139 | _std_dev: f64, 140 | ) { 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /crates/anyrender_skia/src/window_renderer.rs: -------------------------------------------------------------------------------- 1 | use anyrender::WindowRenderer; 2 | use debug_timer::debug_timer; 3 | use skia_safe::{Color, Surface, graphics}; 4 | use std::sync::Arc; 5 | 6 | use crate::{SkiaScenePainter, scene::SkiaSceneCache}; 7 | 8 | pub(crate) trait SkiaBackend { 9 | fn set_size(&mut self, width: u32, height: u32); 10 | 11 | fn prepare(&mut self) -> Option; 12 | 13 | fn flush(&mut self, surface: Surface); 14 | } 15 | 16 | enum RenderState { 17 | Active(Box), 18 | Suspended, 19 | } 20 | 21 | struct ActiveRenderState { 22 | backend: Box, 23 | scene_cache: SkiaSceneCache, 24 | } 25 | 26 | pub struct SkiaWindowRenderer { 27 | render_state: RenderState, 28 | } 29 | 30 | impl Default for SkiaWindowRenderer { 31 | fn default() -> Self { 32 | Self::new() 33 | } 34 | } 35 | 36 | impl SkiaWindowRenderer { 37 | pub fn new() -> Self { 38 | Self { 39 | render_state: RenderState::Suspended, 40 | } 41 | } 42 | } 43 | 44 | impl SkiaWindowRenderer {} 45 | 46 | impl WindowRenderer for SkiaWindowRenderer { 47 | type ScenePainter<'a> 48 | = SkiaScenePainter<'a> 49 | where 50 | Self: 'a; 51 | 52 | fn resume(&mut self, window: Arc, width: u32, height: u32) { 53 | graphics::set_font_cache_count_limit(100); 54 | graphics::set_typeface_cache_count_limit(100); 55 | graphics::set_resource_cache_total_bytes_limit(10485760); 56 | 57 | #[cfg(target_os = "macos")] 58 | let backend = crate::metal::MetalBackend::new(window, width, height); 59 | #[cfg(not(target_os = "macos"))] 60 | let backend = crate::opengl::OpenGLBackend::new(window, width, height); 61 | 62 | self.render_state = RenderState::Active(Box::new(ActiveRenderState { 63 | backend: Box::new(backend), 64 | scene_cache: SkiaSceneCache::default(), 65 | })) 66 | } 67 | 68 | fn suspend(&mut self) { 69 | self.render_state = RenderState::Suspended; 70 | } 71 | 72 | fn is_active(&self) -> bool { 73 | matches!(self.render_state, RenderState::Active(..)) 74 | } 75 | 76 | fn set_size(&mut self, width: u32, height: u32) { 77 | if let RenderState::Active(state) = &mut self.render_state { 78 | state.backend.set_size(width, height); 79 | } 80 | } 81 | 82 | fn render)>(&mut self, draw_fn: F) { 83 | let RenderState::Active(state) = &mut self.render_state else { 84 | return; 85 | }; 86 | 87 | debug_timer!(timer, feature = "log_frame_times"); 88 | 89 | let mut surface = match state.backend.prepare() { 90 | Some(it) => it, 91 | None => return, 92 | }; 93 | 94 | surface.canvas().restore_to_count(1); 95 | surface.canvas().clear(Color::WHITE); 96 | 97 | draw_fn(&mut SkiaScenePainter { 98 | inner: surface.canvas(), 99 | cache: &mut state.scene_cache, 100 | }); 101 | timer.record_time("cmd"); 102 | 103 | state.backend.flush(surface); 104 | timer.record_time("render"); 105 | 106 | state.scene_cache.next_gen(); 107 | timer.record_time("cache next gen"); 108 | 109 | timer.print_times("skia: "); 110 | } 111 | } 112 | 113 | #[cfg(any( 114 | feature = "pixels_window_renderer", 115 | feature = "softbuffer_window_renderer" 116 | ))] 117 | pub mod raster { 118 | #[cfg(feature = "pixels_window_renderer")] 119 | pub use pixels_window_renderer::PixelsWindowRenderer; 120 | #[cfg(feature = "softbuffer_window_renderer")] 121 | pub use softbuffer_window_renderer::SoftbufferWindowRenderer; 122 | 123 | #[cfg(feature = "pixels_window_renderer")] 124 | pub type SkiaRasterWindowRenderer = 125 | PixelsWindowRenderer; 126 | #[cfg(all( 127 | feature = "softbuffer_window_renderer", 128 | not(feature = "pixels_window_renderer") 129 | ))] 130 | pub type SkiaRasterWindowRenderer = 131 | SoftbufferWindowRenderer; 132 | } 133 | -------------------------------------------------------------------------------- /examples/bunnymark/src/bunny.rs: -------------------------------------------------------------------------------- 1 | use std::{io::Cursor, sync::Arc}; 2 | 3 | use anyrender::PaintScene; 4 | use image::ImageReader; 5 | use kurbo::{Affine, Size, Vec2}; 6 | use peniko::{Blob, ImageBrush, ImageData, ImageSampler}; 7 | 8 | const GRAVITY: f64 = 0.5; 9 | 10 | #[derive(Debug)] 11 | pub struct Bunny { 12 | x: f64, 13 | y: f64, 14 | speed_x: f64, 15 | speed_y: f64, 16 | } 17 | 18 | impl Bunny { 19 | pub fn new(canvas: Size) -> Self { 20 | Self { 21 | x: fastrand::f64() * canvas.width, 22 | y: fastrand::f64() * canvas.height, 23 | speed_x: fastrand::f64() * 10.0, 24 | speed_y: fastrand::f64() * 10.0, 25 | } 26 | } 27 | 28 | pub fn position(&self) -> Vec2 { 29 | Vec2 { 30 | x: self.x, 31 | y: self.y, 32 | } 33 | } 34 | 35 | pub fn update(&mut self, canvas: Size) { 36 | // Apply speed to position 37 | self.x += self.speed_x; 38 | self.y += self.speed_y; 39 | 40 | // Apply gravity to y speed 41 | self.speed_y += GRAVITY; 42 | 43 | // Bounce off left wall 44 | if self.x < 0.0 { 45 | self.x = 0.0; 46 | self.speed_x *= -1.0; 47 | } 48 | 49 | // Bounce off right wall 50 | if self.x > canvas.width { 51 | self.x = canvas.width; 52 | self.speed_x *= -1.0; 53 | } 54 | 55 | if self.y > canvas.height { 56 | self.y = canvas.height; 57 | self.speed_y *= -0.85; 58 | if fastrand::f64() > 0.5 { 59 | self.speed_y -= fastrand::f64() * 6.0; 60 | } 61 | } 62 | 63 | // Floor y at 0 64 | if self.y < 0.0 { 65 | self.y = 0.0; 66 | self.speed_y = 0.0; 67 | } 68 | } 69 | } 70 | 71 | pub struct BunnyManager { 72 | canvas_size: Size, 73 | bunny_image: ImageBrush, 74 | bunnies: Vec, 75 | } 76 | 77 | impl BunnyManager { 78 | pub fn new(canvas_width: f64, canvas_height: f64) -> Self { 79 | Self { 80 | canvas_size: Size { 81 | width: canvas_width, 82 | height: canvas_height, 83 | }, 84 | bunny_image: create_bunny_image(), 85 | bunnies: Vec::new(), 86 | } 87 | } 88 | 89 | pub fn add_bunnies(&mut self, count: usize) { 90 | self.bunnies 91 | .resize_with(self.bunnies.len() + count, || Bunny::new(self.canvas_size)); 92 | } 93 | 94 | pub fn clear_bunnies(&mut self) { 95 | self.bunnies.clear(); 96 | } 97 | 98 | pub fn count(&self) -> usize { 99 | self.bunnies.len() 100 | } 101 | 102 | pub fn update(&mut self, canvas_width: f64, canvas_height: f64) { 103 | self.canvas_size.width = canvas_width; 104 | self.canvas_size.height = canvas_height; 105 | for bunny in &mut self.bunnies { 106 | bunny.update(self.canvas_size); 107 | } 108 | } 109 | 110 | pub fn draw(&self, scene: &mut S, scale_factor: f64) { 111 | for bunny in &self.bunnies { 112 | let pos = bunny.position(); 113 | scene.draw_image( 114 | self.bunny_image.as_ref(), 115 | Affine::translate(pos).then_scale(scale_factor), 116 | ); 117 | } 118 | } 119 | } 120 | 121 | fn create_bunny_image() -> ImageBrush { 122 | static BUNNY_IMAGE_DATA: &[u8] = include_bytes!("./bunny.png"); 123 | let raw_bunny_image = 124 | ImageReader::with_format(Cursor::new(BUNNY_IMAGE_DATA), image::ImageFormat::Png) 125 | .decode() 126 | .unwrap() 127 | .into_rgba8(); 128 | let width = raw_bunny_image.width(); 129 | let height = raw_bunny_image.height(); 130 | ImageBrush { 131 | image: ImageData { 132 | data: Blob::new(Arc::new(raw_bunny_image.into_vec())), 133 | format: peniko::ImageFormat::Rgba8, 134 | alpha_type: peniko::ImageAlphaType::Alpha, 135 | width, 136 | height, 137 | }, 138 | sampler: ImageSampler { 139 | x_extend: peniko::Extend::Pad, 140 | y_extend: peniko::Extend::Pad, 141 | quality: peniko::ImageQuality::Medium, 142 | alpha: 1.0, 143 | }, 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AnyRender 2 | 3 | **A Rust 2D drawing abstraction.** 4 | 5 | [![Linebender Zulip, #kurbo channel](https://img.shields.io/badge/Linebender-grey?logo=Zulip)](https://xi.zulipchat.com) 6 | [![dependency status](https://deps.rs/repo/github/dioxuslabs/anyrender/status.svg)](https://deps.rs/repo/github/dioxuslabs/anyrender) 7 | [![Apache 2.0 or MIT license.](https://img.shields.io/badge/license-Apache--2.0_OR_MIT-blue.svg)](#license) 8 | [![Crates.io](https://img.shields.io/crates/v/anyrender.svg)](https://crates.io/crates/anyrender) 9 | [![Docs](https://docs.rs/anyrender/badge.svg)](https://docs.rs/anyrender) 10 | 11 | AnyRender is a 2D drawing abstaction that allows applications/frameworks to support many rendering backends through a unified API. 12 | 13 | Discussion of AnyRender development happens in the Linebender Zulip at . 14 | 15 | ## Crates 16 | 17 | ### `anyrender` 18 | 19 | The core [anyrender](https://docs.rs/anyrender) crate is a lightweight type/trait-only crate that defines three abstractions: 20 | 21 | - **The [PaintScene](https://docs.rs/anyrender/latest/anyrender/trait.PaintScene.html) trait accepts drawing commands.** 22 | Applications and libraries draw by pushing commands into a `PaintScene`. Backends generally execute those commands to 23 | produce an output (although they may do other things like store them for later use). 24 | - **The [WindowRenderer](https://docs.rs/anyrender/latest/anyrender/trait.WindowRenderer.html) trait abstracts over types that can render to a window** 25 | - **The [ImageRenderer](https://docs.rs/anyrender/latest/anyrender/trait.ImageRenderer.html) trait abstracts over types that can render to a `Vec` image buffer** 26 | 27 | ### Backends 28 | 29 | Currently existing backends are: 30 | 31 | - [anyrender_vello](https://docs.rs/anyrender_vello) which draws using [vello](https://docs.rs/vello) 32 | - [anyrender_vello_cpu](https://docs.rs/anyrender_vello_cpu) which draws using [vello_cpu](https://docs.rs/vello_cpu) 33 | - [anyrender_vello_hybrid](https://docs.rs/anyrender_vello_hybrid) ALPHA which draws using [vello_hybrid](https://docs.rs/vello_hybrid) 34 | - [anyrender_skia](https://crates.io/crates/anyrender_skia) which draws using Skia (via the [skia-safe](https://github.com/rust-skia/rust-skia) crate) 35 | 36 | Contributions for other backends (tiny-skia, femtovg, etc) would be very welcome. 37 | 38 | ### Content renderers 39 | 40 | These crates sit on top of the the AnyRender abstraction, and allow you render content through it: 41 | 42 | - [anyrender_svg](https://docs.rs/anyrender_svg) allows you to render SVGs with AnyRender. [usvg](https://docs.rs/usvg) is used to parse the SVGs. 43 | - [blitz-paint](https://docs.rs/blitz-paint) can be used to HTML/CSS (and markdown) that has been parsed, styled, and layouted by [blitz-dom](https://docs.rs/blitz-dom) using AnyRender. 44 | - [polymorpher](https://github.com/Aiving/polymorpher) implements Material Design 3 shape morphing, and can be used with AnyRender by enabling the `kurbo` feature. 45 | 46 | ### Utility crates 47 | 48 | - [wgpu_context](https://docs.rs/wgpu_context) is a utility for managing `Device`s and other WGPU types 49 | - [pixels_window_renderer](https://docs.rs/pixels_window_renderer) implements an AnyRender `WindowRenderer` for any AnyRenderer `ImageRenderer` using the [pixels](https://docs.rs/pixels) crate. 50 | - [softbuffer_window_renderer](https://docs.rs/softbuffer_window_renderer) implements an AnyRender `WindowRenderer` for any AnyRenderer `ImageRenderer` using the [softbuffer](https://docs.rs/softbuffer) crate. 51 | 52 | 53 | ## Minimum supported Rust Version (MSRV) 54 | 55 | This version of AnyRender has been verified to compile with **Rust 1.86** and later. 56 | 57 | Future versions of AnyRender might increase the Rust version requirement. 58 | It will not be treated as a breaking change and as such can even happen with small patch releases. 59 | 60 | ## License 61 | 62 | Licensed under either of 63 | 64 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) 65 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or ) 66 | 67 | at your option. 68 | 69 | ## Contribution 70 | 71 | Contributions are welcome by pull request. The [Rust code of conduct] applies. 72 | 73 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be licensed as above, without any additional terms or conditions. 74 | 75 | [kurbo]: https://crates.io/crates/kurbo 76 | [Rust Code of Conduct]: https://www.rust-lang.org/policies/code-of-conduct 77 | 78 | -------------------------------------------------------------------------------- /crates/softbuffer_window_renderer/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! An AnyRender WindowRenderer for rendering pixel buffers using the softbuffer crate 2 | 3 | #![cfg_attr(docsrs, feature(doc_cfg))] 4 | 5 | use anyrender::{ImageRenderer, WindowHandle, WindowRenderer}; 6 | use debug_timer::debug_timer; 7 | use softbuffer::{Context, Surface}; 8 | use std::{num::NonZero, sync::Arc}; 9 | 10 | // Simple struct to hold the state of the renderer 11 | pub struct ActiveRenderState { 12 | _context: Context>, 13 | surface: Surface, Arc>, 14 | } 15 | 16 | #[allow(clippy::large_enum_variant)] 17 | pub enum RenderState { 18 | Active(ActiveRenderState), 19 | Suspended, 20 | } 21 | 22 | pub struct SoftbufferWindowRenderer { 23 | // The fields MUST be in this order, so that the surface is dropped before the window 24 | // Window is cached even when suspended so that it can be reused when the app is resumed after being suspended 25 | render_state: RenderState, 26 | window_handle: Option>, 27 | renderer: Renderer, 28 | } 29 | 30 | impl SoftbufferWindowRenderer { 31 | #[allow(clippy::new_without_default)] 32 | pub fn new() -> Self { 33 | Self::with_renderer(Renderer::new(0, 0)) 34 | } 35 | 36 | pub fn with_renderer(renderer: R) -> SoftbufferWindowRenderer { 37 | SoftbufferWindowRenderer { 38 | render_state: RenderState::Suspended, 39 | window_handle: None, 40 | renderer, 41 | } 42 | } 43 | } 44 | 45 | impl WindowRenderer for SoftbufferWindowRenderer { 46 | type ScenePainter<'a> 47 | = Renderer::ScenePainter<'a> 48 | where 49 | Self: 'a; 50 | 51 | fn is_active(&self) -> bool { 52 | matches!(self.render_state, RenderState::Active(_)) 53 | } 54 | 55 | fn resume(&mut self, window_handle: Arc, width: u32, height: u32) { 56 | let context = Context::new(window_handle.clone()).unwrap(); 57 | let surface = Surface::new(&context, window_handle.clone()).unwrap(); 58 | self.render_state = RenderState::Active(ActiveRenderState { 59 | _context: context, 60 | surface, 61 | }); 62 | self.window_handle = Some(window_handle); 63 | 64 | self.set_size(width, height); 65 | } 66 | 67 | fn suspend(&mut self) { 68 | self.render_state = RenderState::Suspended; 69 | } 70 | 71 | fn set_size(&mut self, physical_width: u32, physical_height: u32) { 72 | if let RenderState::Active(state) = &mut self.render_state { 73 | state 74 | .surface 75 | .resize( 76 | NonZero::new(physical_width.max(1)).unwrap(), 77 | NonZero::new(physical_height.max(1)).unwrap(), 78 | ) 79 | .unwrap(); 80 | self.renderer.resize(physical_width, physical_height); 81 | }; 82 | } 83 | 84 | fn render)>(&mut self, draw_fn: F) { 85 | let RenderState::Active(state) = &mut self.render_state else { 86 | return; 87 | }; 88 | 89 | debug_timer!(timer, feature = "log_frame_times"); 90 | 91 | let Ok(mut surface_buffer) = state.surface.buffer_mut() else { 92 | return; 93 | }; 94 | timer.record_time("buffer_mut"); 95 | 96 | // Paint 97 | let mut vec = Vec::new(); 98 | self.renderer.render_to_vec(draw_fn, &mut vec); 99 | timer.record_time("render"); 100 | 101 | let out = surface_buffer.as_mut(); 102 | 103 | // TODO: replace chunk_exacts with as_chunks once MSRV hits 1.88 104 | let chunks = vec.chunks_exact(4); 105 | assert_eq!(chunks.size_hint().0, out.len()); 106 | assert_eq!(chunks.remainder().len(), 0); 107 | 108 | for (src, dest) in chunks.zip(out.iter_mut()) { 109 | let [r, g, b, a]: [u8; 4] = src.try_into().unwrap(); 110 | if a == 0 { 111 | *dest = u32::MAX; 112 | } else { 113 | *dest = (r as u32) << 16 | (g as u32) << 8 | b as u32; 114 | } 115 | } 116 | timer.record_time("swizel"); 117 | 118 | surface_buffer.present().unwrap(); 119 | timer.record_time("present"); 120 | timer.print_times("softbuffer: "); 121 | 122 | // Reset the renderer ready for the next render 123 | self.renderer.reset(); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /crates/anyrender_skia/src/metal.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use objc2::{rc::Retained, runtime::ProtocolObject}; 4 | use objc2_app_kit::NSView; 5 | use objc2_core_foundation::CGSize; 6 | use objc2_metal::{MTLCommandBuffer, MTLCommandQueue, MTLCreateSystemDefaultDevice, MTLDevice}; 7 | use objc2_quartz_core::{CAMetalDrawable, CAMetalLayer}; 8 | use skia_safe::{ 9 | ColorType, Surface, 10 | gpu::{self, DirectContext, SurfaceOrigin, backend_render_targets, mtl}, 11 | scalar, 12 | }; 13 | 14 | use crate::window_renderer::SkiaBackend; 15 | 16 | pub struct MetalBackend { 17 | pub metal_layer: Retained, 18 | pub command_queue: Retained>, 19 | pub skia: DirectContext, 20 | prepared_drawable: Option>>, 21 | } 22 | 23 | impl MetalBackend { 24 | pub fn new(window: Arc, width: u32, height: u32) -> Self { 25 | let device = MTLCreateSystemDefaultDevice().expect("no device found"); 26 | 27 | let metal_layer = { 28 | let layer = CAMetalLayer::new(); 29 | layer.setDevice(Some(&device)); 30 | layer.setPixelFormat(objc2_metal::MTLPixelFormat::BGRA8Unorm); 31 | layer.setPresentsWithTransaction(false); 32 | // Disabling this option allows Skia's Blend Mode to work. 33 | // More about: https://developer.apple.com/documentation/quartzcore/cametallayer/1478168-framebufferonly 34 | layer.setFramebufferOnly(false); 35 | 36 | let view_ptr = match window.window_handle().unwrap().as_raw() { 37 | raw_window_handle::RawWindowHandle::AppKit(appkit) => { 38 | appkit.ns_view.as_ptr() as *mut NSView 39 | } 40 | _ => panic!("Wrong window handle type"), 41 | }; 42 | let view = unsafe { view_ptr.as_ref().unwrap() }; 43 | 44 | view.setWantsLayer(true); 45 | view.setLayer(Some(&layer.clone().into_super())); 46 | layer.setDrawableSize(CGSize::new(width as f64, height as f64)); 47 | layer 48 | }; 49 | 50 | let command_queue = device 51 | .newCommandQueue() 52 | .expect("unable to get command queue"); 53 | 54 | let backend = unsafe { 55 | mtl::BackendContext::new( 56 | Retained::as_ptr(&device) as mtl::Handle, 57 | Retained::as_ptr(&command_queue) as mtl::Handle, 58 | ) 59 | }; 60 | 61 | let skia_context = gpu::direct_contexts::make_metal(&backend, None).unwrap(); 62 | 63 | Self { 64 | metal_layer, 65 | command_queue, 66 | skia: skia_context, 67 | prepared_drawable: None, 68 | } 69 | } 70 | } 71 | 72 | impl SkiaBackend for MetalBackend { 73 | fn set_size(&mut self, width: u32, height: u32) { 74 | self.metal_layer 75 | .setDrawableSize(CGSize::new(width as f64, height as f64)); 76 | } 77 | 78 | fn prepare(&mut self) -> Option { 79 | let drawable = self.metal_layer.nextDrawable()?; 80 | 81 | let (drawable_width, drawable_height) = { 82 | let size = self.metal_layer.drawableSize(); 83 | (size.width as scalar, size.height as scalar) 84 | }; 85 | 86 | let surface = { 87 | let texture_info = unsafe { 88 | mtl::TextureInfo::new(Retained::as_ptr(&drawable.texture()) as mtl::Handle) 89 | }; 90 | 91 | let backend_render_target = backend_render_targets::make_mtl( 92 | (drawable_width as i32, drawable_height as i32), 93 | &texture_info, 94 | ); 95 | 96 | gpu::surfaces::wrap_backend_render_target( 97 | &mut self.skia, 98 | &backend_render_target, 99 | SurfaceOrigin::TopLeft, 100 | ColorType::BGRA8888, 101 | None, 102 | None, 103 | ) 104 | .unwrap() 105 | }; 106 | 107 | self.prepared_drawable = Some((&drawable).into()); 108 | 109 | Some(surface) 110 | } 111 | 112 | fn flush(&mut self, surface: Surface) { 113 | self.skia.flush_and_submit(); 114 | drop(surface); 115 | let command_buffer = self 116 | .command_queue 117 | .commandBuffer() 118 | .expect("unsable to get command buffer"); 119 | 120 | // TODO: save drawable 121 | let drawable = self.prepared_drawable.take().unwrap(); 122 | command_buffer.presentDrawable(&drawable); 123 | command_buffer.commit(); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 11 | cancel-in-progress: true 12 | 13 | env: 14 | RUSTDOCFLAGS: "-D warnings" 15 | CARGO_REGISTRIES_CRATES_IO_PROTOCOL: "sparse" 16 | 17 | jobs: 18 | 19 | # MSRV check. 20 | # AnyRender only guarantees "latest stable". However we have this check here to ensure that we advertise 21 | # our MSRV. We also make an effort not to increase MSRV in patch versions of AnyRender. 22 | # 23 | # We only run `cargo build` (not `cargo test`) so as to avoid requiring dev-dependencies to build with the MSRV 24 | # version. Building is likely sufficient as runtime errors varying between rust versions is very unlikely. 25 | build-msrv: 26 | name: "MSRV Build [Rust 1.86]" 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: dtolnay/rust-toolchain@master 31 | with: 32 | toolchain: 1.86 33 | - name: Install development libraries on ubuntu 34 | run: sudo apt-get update && sudo apt-get install -y libfontconfig1-dev libfreetype6-dev 35 | - run: cargo build --workspace 36 | 37 | build-features-default: 38 | name: "Build [default features]" 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v4 42 | - uses: dtolnay/rust-toolchain@stable 43 | - name: Install development libraries on ubuntu 44 | run: sudo apt-get update && sudo apt-get install -y libfontconfig1-dev libfreetype6-dev 45 | - run: cargo build --workspace 46 | 47 | fmt: 48 | name: Rustfmt 49 | runs-on: ubuntu-latest 50 | steps: 51 | - uses: actions/checkout@v4 52 | - uses: dtolnay/rust-toolchain@master 53 | with: 54 | toolchain: stable 55 | components: rustfmt 56 | - run: cargo fmt --all --check 57 | 58 | clippy: 59 | name: Clippy 60 | runs-on: ubuntu-latest 61 | steps: 62 | - uses: actions/checkout@v4 63 | - uses: dtolnay/rust-toolchain@master 64 | with: 65 | toolchain: stable 66 | components: clippy 67 | - run: cargo clippy --workspace -- -D warnings 68 | 69 | doc: 70 | name: Documentation 71 | runs-on: ubuntu-latest 72 | steps: 73 | - uses: actions/checkout@v4 74 | - uses: dtolnay/rust-toolchain@stable 75 | - run: cargo doc 76 | 77 | # just cargo check for now 78 | matrix_test: 79 | runs-on: ${{ matrix.platform.os }} 80 | env: 81 | RUST_CARGO_COMMAND: ${{ matrix.platform.cross == true && 'cross' || 'cargo' }} 82 | strategy: 83 | matrix: 84 | platform: 85 | - { 86 | name: windows, 87 | target: x86_64-pc-windows-msvc, 88 | os: windows-latest, 89 | cross: false, 90 | command: "test", 91 | args: "--all --tests", 92 | setup: perl -pi.bak -e 's/opt-level = 2/opt-level = 0/g' Cargo.toml 93 | } 94 | - { 95 | name: macos, 96 | target: aarch64-apple-darwin, 97 | os: macos-latest, 98 | cross: false, 99 | command: "test", 100 | args: "--all --tests", 101 | setup: perl -pi.bak -e 's/opt-level = 2/opt-level = 0/g' Cargo.toml 102 | } 103 | - { 104 | name: linux, 105 | target: x86_64-unknown-linux-gnu, 106 | os: ubuntu-latest, 107 | cross: false, 108 | command: "test", 109 | args: "--all --tests", 110 | } 111 | 112 | name: Test (${{ matrix.platform.name }}) 113 | 114 | steps: 115 | - uses: actions/checkout@v4 116 | - name: install stable 117 | uses: dtolnay/rust-toolchain@master 118 | with: 119 | toolchain: stable 120 | targets: ${{ matrix.platform.target }} 121 | components: rustfmt 122 | 123 | - name: Install cross 124 | if: ${{ matrix.platform.cross == true }} 125 | uses: taiki-e/install-action@cross 126 | 127 | - uses: Swatinem/rust-cache@v2 128 | with: 129 | key: "${{ matrix.platform.target }}" 130 | cache-all-crates: "true" 131 | save-if: ${{ github.ref == 'refs/heads/main' }} 132 | 133 | - name: Install development libraries on ubuntu 134 | if: ${{ matrix.platform.os == 'ubuntu-latest' }} 135 | run: sudo apt-get update && sudo apt-get install -y libfontconfig1-dev libfreetype6-dev 136 | 137 | - name: Setup 138 | run: ${{ matrix.platform.setup }} 139 | shell: bash 140 | 141 | - name: test 142 | run: | 143 | ${{ env.RUST_CARGO_COMMAND }} ${{ matrix.platform.command }} ${{ matrix.platform.args }} --target ${{ matrix.platform.target }} 144 | -------------------------------------------------------------------------------- /crates/anyrender_skia/src/cache.rs: -------------------------------------------------------------------------------- 1 | use std::hash::{Hash, Hasher}; 2 | 3 | use hashbrown::{Equivalent, HashMap}; 4 | 5 | struct CacheEntry { 6 | resource: T, 7 | generation: usize, 8 | } 9 | 10 | pub(crate) struct GenerationalCache 11 | where 12 | K: Eq + Hash, 13 | { 14 | resources: HashMap>, 15 | current_generation: usize, 16 | max_age: usize, 17 | } 18 | 19 | impl Default for GenerationalCache 20 | where 21 | K: Eq + Hash, 22 | { 23 | fn default() -> Self { 24 | Self { 25 | resources: Default::default(), 26 | current_generation: 0, 27 | max_age: 2, 28 | } 29 | } 30 | } 31 | 32 | impl GenerationalCache 33 | where 34 | K: Eq + Hash, 35 | { 36 | pub(crate) fn new(max_age: usize) -> Self { 37 | GenerationalCache { 38 | resources: HashMap::default(), 39 | current_generation: 0, 40 | max_age, 41 | } 42 | } 43 | 44 | pub(crate) fn next_gen(&mut self) { 45 | self.resources.retain(|_, entry| { 46 | self.current_generation.wrapping_sub(entry.generation) < self.max_age 47 | }); 48 | 49 | self.current_generation = self.current_generation.wrapping_add(1); 50 | } 51 | 52 | #[allow(unused)] 53 | pub(crate) fn contains_key(&mut self, key: &Q) -> bool 54 | where 55 | Q: Hash + Equivalent + ?Sized, 56 | { 57 | self.resources.contains_key(key) 58 | } 59 | 60 | pub(crate) fn hit(&mut self, key: &Q) -> Option<&T> 61 | where 62 | Q: Hash + Equivalent + ?Sized, 63 | { 64 | if let Some(entry) = self.resources.get_mut(key) { 65 | entry.generation = self.current_generation; 66 | Some(&entry.resource) 67 | } else { 68 | None 69 | } 70 | } 71 | 72 | pub(crate) fn insert(&mut self, key: K, resource: T) { 73 | let entry = CacheEntry { 74 | resource, 75 | generation: self.current_generation, 76 | }; 77 | self.resources.insert(key, entry); 78 | } 79 | } 80 | 81 | #[derive(PartialEq, Eq)] 82 | pub(crate) struct NormalizedTypefaceCacheKey { 83 | pub(crate) typeface_id: u64, 84 | pub(crate) typeface_index: u32, 85 | pub(crate) normalized_coords: Vec, 86 | } 87 | 88 | impl Hash for NormalizedTypefaceCacheKey { 89 | fn hash(&self, state: &mut H) { 90 | self.typeface_id.hash(state); 91 | self.typeface_index.hash(state); 92 | for &coord in &self.normalized_coords { 93 | coord.hash(state); 94 | } 95 | } 96 | } 97 | 98 | pub(crate) struct NormalizedTypefaceCacheKeyBorrowed<'a> { 99 | pub(crate) typeface_id: u64, 100 | pub(crate) typeface_index: u32, 101 | pub(crate) normalized_coords: &'a [i16], 102 | } 103 | 104 | impl<'a> Hash for NormalizedTypefaceCacheKeyBorrowed<'a> { 105 | fn hash(&self, state: &mut H) { 106 | self.typeface_id.hash(state); 107 | self.typeface_index.hash(state); 108 | for &coord in self.normalized_coords { 109 | coord.hash(state); 110 | } 111 | } 112 | } 113 | 114 | impl Equivalent for NormalizedTypefaceCacheKeyBorrowed<'_> { 115 | fn equivalent(&self, key: &NormalizedTypefaceCacheKey) -> bool { 116 | self.typeface_id == key.typeface_id 117 | && self.typeface_index == key.typeface_index 118 | && self.normalized_coords == key.normalized_coords 119 | } 120 | } 121 | 122 | pub(crate) struct FontCacheKeyBorrowed<'a> { 123 | pub(crate) typeface_id: u64, 124 | pub(crate) typeface_index: u32, 125 | pub(crate) normalized_coords: &'a [i16], 126 | pub(crate) font_size: u32, 127 | pub(crate) hint: bool, 128 | } 129 | 130 | impl<'a> Hash for FontCacheKeyBorrowed<'a> { 131 | fn hash(&self, state: &mut H) { 132 | self.typeface_id.hash(state); 133 | self.typeface_index.hash(state); 134 | for &coord in self.normalized_coords { 135 | coord.hash(state); 136 | } 137 | self.font_size.hash(state); 138 | self.hint.hash(state); 139 | } 140 | } 141 | 142 | #[derive(PartialEq, Eq)] 143 | pub(crate) struct FontCacheKey { 144 | pub(crate) typeface_id: u64, 145 | pub(crate) typeface_index: u32, 146 | pub(crate) normalized_coords: Vec, 147 | pub(crate) font_size: u32, 148 | pub(crate) hint: bool, 149 | } 150 | 151 | impl Hash for FontCacheKey { 152 | fn hash(&self, state: &mut H) { 153 | self.typeface_id.hash(state); 154 | self.typeface_index.hash(state); 155 | for &coord in &self.normalized_coords { 156 | coord.hash(state); 157 | } 158 | self.font_size.hash(state); 159 | self.hint.hash(state); 160 | } 161 | } 162 | 163 | impl Equivalent for FontCacheKeyBorrowed<'_> { 164 | fn equivalent(&self, key: &FontCacheKey) -> bool { 165 | self.typeface_id == key.typeface_id 166 | && self.typeface_index == key.typeface_index 167 | && self.font_size == key.font_size 168 | && self.hint == key.hint 169 | && self.normalized_coords == key.normalized_coords 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /crates/anyrender_vello/src/scene.rs: -------------------------------------------------------------------------------- 1 | use anyrender::{CustomPaint, NormalizedCoord, Paint, PaintRef, PaintScene}; 2 | use kurbo::{Affine, Rect, Shape, Stroke}; 3 | use peniko::{BlendMode, BrushRef, Color, Fill, FontData, ImageBrush, StyleRef}; 4 | use rustc_hash::FxHashMap; 5 | use vello::Renderer as VelloRenderer; 6 | 7 | use crate::{CustomPaintSource, custom_paint_source::CustomPaintCtx}; 8 | 9 | pub struct VelloScenePainter<'r, 's> { 10 | pub(crate) renderer: Option<&'r mut VelloRenderer>, 11 | pub(crate) custom_paint_sources: Option<&'r mut FxHashMap>>, 12 | pub(crate) inner: &'s mut vello::Scene, 13 | } 14 | 15 | impl VelloScenePainter<'_, '_> { 16 | pub fn new<'s>(scene: &'s mut vello::Scene) -> VelloScenePainter<'static, 's> { 17 | VelloScenePainter { 18 | renderer: None, 19 | custom_paint_sources: None, 20 | inner: scene, 21 | } 22 | } 23 | 24 | fn render_custom_source(&mut self, custom_paint: CustomPaint) -> Option { 25 | let (Some(renderer), Some(custom_paint_sources)) = 26 | (&mut self.renderer, &mut self.custom_paint_sources) 27 | else { 28 | return None; 29 | }; 30 | 31 | let CustomPaint { 32 | source_id, 33 | width, 34 | height, 35 | scale, 36 | } = custom_paint; 37 | 38 | // Render custom paint source 39 | let source = custom_paint_sources.get_mut(&source_id)?; 40 | let ctx = CustomPaintCtx::new(renderer); 41 | let texture_handle = source.render(ctx, width, height, scale)?; 42 | 43 | // Return dummy image 44 | Some(ImageBrush::new(texture_handle.0)) 45 | } 46 | } 47 | 48 | impl PaintScene for VelloScenePainter<'_, '_> { 49 | fn reset(&mut self) { 50 | self.inner.reset(); 51 | } 52 | 53 | fn push_layer( 54 | &mut self, 55 | blend: impl Into, 56 | alpha: f32, 57 | transform: Affine, 58 | clip: &impl Shape, 59 | ) { 60 | self.inner.push_layer(blend, alpha, transform, clip); 61 | } 62 | 63 | fn push_clip_layer(&mut self, transform: Affine, clip: &impl Shape) { 64 | self.inner.push_clip_layer(transform, clip); 65 | } 66 | 67 | fn pop_layer(&mut self) { 68 | self.inner.pop_layer(); 69 | } 70 | 71 | fn stroke<'a>( 72 | &mut self, 73 | style: &Stroke, 74 | transform: Affine, 75 | paint_ref: impl Into>, 76 | brush_transform: Option, 77 | shape: &impl Shape, 78 | ) { 79 | let paint_ref: PaintRef<'_> = paint_ref.into(); 80 | let brush_ref: BrushRef<'_> = paint_ref.into(); 81 | self.inner 82 | .stroke(style, transform, brush_ref, brush_transform, shape); 83 | } 84 | 85 | fn fill<'a>( 86 | &mut self, 87 | style: Fill, 88 | transform: Affine, 89 | paint: impl Into>, 90 | brush_transform: Option, 91 | shape: &impl Shape, 92 | ) { 93 | let paint: PaintRef<'_> = paint.into(); 94 | 95 | let dummy_image: peniko::ImageBrush; 96 | let brush_ref: BrushRef<'_> = match paint { 97 | Paint::Solid(color) => BrushRef::Solid(color), 98 | Paint::Gradient(gradient) => BrushRef::Gradient(gradient), 99 | Paint::Image(image) => BrushRef::Image(image), 100 | Paint::Custom(custom_paint) => { 101 | let Some(custom_paint) = custom_paint.downcast_ref::() else { 102 | return; 103 | }; 104 | let Some(image) = self.render_custom_source(*custom_paint) else { 105 | return; 106 | }; 107 | dummy_image = image; 108 | BrushRef::Image(dummy_image.as_ref()) 109 | } 110 | }; 111 | 112 | self.inner 113 | .fill(style, transform, brush_ref, brush_transform, shape); 114 | } 115 | 116 | fn draw_glyphs<'a, 's: 'a>( 117 | &'a mut self, 118 | font: &'a FontData, 119 | font_size: f32, 120 | hint: bool, 121 | normalized_coords: &'a [NormalizedCoord], 122 | style: impl Into>, 123 | paint: impl Into>, 124 | brush_alpha: f32, 125 | transform: Affine, 126 | glyph_transform: Option, 127 | glyphs: impl Iterator, 128 | ) { 129 | self.inner 130 | .draw_glyphs(font) 131 | .font_size(font_size) 132 | .hint(hint) 133 | .normalized_coords(normalized_coords) 134 | .brush(paint.into()) 135 | .brush_alpha(brush_alpha) 136 | .transform(transform) 137 | .glyph_transform(glyph_transform) 138 | .draw( 139 | style, 140 | glyphs.map(|g: anyrender::Glyph| vello::Glyph { 141 | id: g.id, 142 | x: g.x, 143 | y: g.y, 144 | }), 145 | ); 146 | } 147 | 148 | fn draw_box_shadow( 149 | &mut self, 150 | transform: Affine, 151 | rect: Rect, 152 | brush: Color, 153 | radius: f64, 154 | std_dev: f64, 155 | ) { 156 | self.inner 157 | .draw_blurred_rounded_rect(transform, rect, brush, radius, std_dev); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /crates/anyrender_vello_cpu/src/scene.rs: -------------------------------------------------------------------------------- 1 | use anyrender::{NormalizedCoord, Paint, PaintRef, PaintScene}; 2 | use kurbo::{Affine, Rect, Shape, Stroke}; 3 | use peniko::{BlendMode, Color, Fill, FontData, ImageBrush, ImageData, StyleRef}; 4 | use vello_cpu::{ImageSource, PaintType, Pixmap}; 5 | 6 | const DEFAULT_TOLERANCE: f64 = 0.1; 7 | 8 | fn anyrender_paint_to_vello_cpu_paint<'a>(paint: PaintRef<'a>) -> PaintType { 9 | match paint { 10 | Paint::Solid(alpha_color) => PaintType::Solid(alpha_color), 11 | Paint::Gradient(gradient) => PaintType::Gradient(gradient.clone()), 12 | Paint::Image(image) => PaintType::Image(ImageBrush { 13 | image: ImageSource::from_peniko_image_data(image.image), 14 | sampler: image.sampler, 15 | }), 16 | // TODO: custom paint 17 | Paint::Custom(_) => PaintType::Solid(peniko::color::palette::css::TRANSPARENT), 18 | } 19 | } 20 | 21 | #[allow(unused)] 22 | fn convert_image_cached(image: &ImageData) -> ImageSource { 23 | use std::collections::HashMap; 24 | use std::sync::{LazyLock, Mutex}; 25 | static CACHE: LazyLock>> = 26 | LazyLock::new(|| Mutex::new(HashMap::new())); 27 | 28 | let mut map = CACHE.lock().unwrap(); 29 | let id = image.data.id(); 30 | map.entry(id) 31 | .or_insert_with(|| ImageSource::from_peniko_image_data(image)) 32 | .clone() 33 | } 34 | 35 | pub struct VelloCpuScenePainter(pub vello_cpu::RenderContext); 36 | 37 | impl VelloCpuScenePainter { 38 | pub fn finish(self) -> Pixmap { 39 | let mut pixmap = Pixmap::new(self.0.width(), self.0.height()); 40 | self.0.render_to_pixmap(&mut pixmap); 41 | pixmap 42 | } 43 | } 44 | 45 | impl PaintScene for VelloCpuScenePainter { 46 | fn reset(&mut self) { 47 | self.0.reset(); 48 | } 49 | 50 | fn push_layer( 51 | &mut self, 52 | blend: impl Into, 53 | alpha: f32, 54 | transform: Affine, 55 | clip: &impl Shape, 56 | ) { 57 | self.0.set_transform(transform); 58 | self.0.push_layer( 59 | Some(&clip.into_path(DEFAULT_TOLERANCE)), 60 | Some(blend.into()), 61 | Some(alpha), 62 | None, 63 | ); 64 | } 65 | 66 | fn push_clip_layer(&mut self, transform: Affine, clip: &impl Shape) { 67 | self.0.set_transform(transform); 68 | self.0.push_clip_layer(&clip.into_path(DEFAULT_TOLERANCE)); 69 | } 70 | 71 | fn pop_layer(&mut self) { 72 | self.0.pop_layer(); 73 | } 74 | 75 | fn stroke<'a>( 76 | &mut self, 77 | style: &Stroke, 78 | transform: Affine, 79 | paint: impl Into>, 80 | brush_transform: Option, 81 | shape: &impl Shape, 82 | ) { 83 | self.0.set_transform(transform); 84 | self.0.set_stroke(style.clone()); 85 | self.0 86 | .set_paint(anyrender_paint_to_vello_cpu_paint(paint.into())); 87 | self.0 88 | .set_paint_transform(brush_transform.unwrap_or(Affine::IDENTITY)); 89 | self.0.stroke_path(&shape.into_path(DEFAULT_TOLERANCE)); 90 | } 91 | 92 | fn fill<'a>( 93 | &mut self, 94 | style: Fill, 95 | transform: Affine, 96 | paint: impl Into>, 97 | brush_transform: Option, 98 | shape: &impl Shape, 99 | ) { 100 | self.0.set_transform(transform); 101 | self.0.set_fill_rule(style); 102 | self.0 103 | .set_paint(anyrender_paint_to_vello_cpu_paint(paint.into())); 104 | self.0 105 | .set_paint_transform(brush_transform.unwrap_or(Affine::IDENTITY)); 106 | self.0.fill_path(&shape.into_path(DEFAULT_TOLERANCE)); 107 | } 108 | 109 | fn draw_glyphs<'a, 's: 'a>( 110 | &'a mut self, 111 | font: &'a FontData, 112 | font_size: f32, 113 | hint: bool, 114 | normalized_coords: &'a [NormalizedCoord], 115 | style: impl Into>, 116 | paint: impl Into>, 117 | _brush_alpha: f32, 118 | transform: Affine, 119 | glyph_transform: Option, 120 | glyphs: impl Iterator, 121 | ) { 122 | self.0.set_transform(transform); 123 | self.0 124 | .set_paint(anyrender_paint_to_vello_cpu_paint(paint.into())); 125 | 126 | fn into_vello_cpu_glyph(g: anyrender::Glyph) -> vello_cpu::Glyph { 127 | vello_cpu::Glyph { 128 | id: g.id, 129 | x: g.x, 130 | y: g.y, 131 | } 132 | } 133 | 134 | let style: StyleRef<'a> = style.into(); 135 | match style { 136 | StyleRef::Fill(fill) => { 137 | self.0.set_fill_rule(fill); 138 | self.0 139 | .glyph_run(font) 140 | .font_size(font_size) 141 | .hint(hint) 142 | .normalized_coords(normalized_coords) 143 | .glyph_transform(glyph_transform.unwrap_or_default()) 144 | .fill_glyphs(glyphs.map(into_vello_cpu_glyph)); 145 | } 146 | StyleRef::Stroke(stroke) => { 147 | self.0.set_stroke(stroke.clone()); 148 | self.0 149 | .glyph_run(font) 150 | .font_size(font_size) 151 | .hint(hint) 152 | .normalized_coords(normalized_coords) 153 | .glyph_transform(glyph_transform.unwrap_or_default()) 154 | .stroke_glyphs(glyphs.map(into_vello_cpu_glyph)); 155 | } 156 | } 157 | } 158 | fn draw_box_shadow( 159 | &mut self, 160 | transform: Affine, 161 | rect: Rect, 162 | color: Color, 163 | radius: f64, 164 | std_dev: f64, 165 | ) { 166 | self.0.set_transform(transform); 167 | self.0.set_paint(PaintType::Solid(color)); 168 | self.0 169 | .fill_blurred_rounded_rect(&rect, radius as f32, std_dev as f32); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /crates/anyrender/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! 2D drawing abstraction that allows applications/frameworks to support many rendering backends through 2 | //! a unified API. 3 | //! 4 | //! ### Painting a scene 5 | //! 6 | //! The core abstraction in AnyRender is the [`PaintScene`] trait. 7 | //! 8 | //! [`PaintScene`] is a "sink" which accepts drawing commands: 9 | //! 10 | //! - Applications and libraries draw by pushing commands into a [`PaintScene`] 11 | //! - Backends execute those commands to produce an output 12 | //! 13 | //! ### Rendering to surface or buffer 14 | //! 15 | //! In addition to PaintScene, there is: 16 | //! 17 | //! - The [`ImageRenderer`] trait which provides an abstraction for rendering to a `Vec` RGBA8 buffer. 18 | //! - The [`WindowRenderer`] trait which provides an abstraction for rendering to a surface/window 19 | //! 20 | //! ### SVG 21 | //! 22 | //! The [anyrender_svg](https://docs.rs/anyrender_svg) crate allows SVGs to be rendered using AnyRender 23 | //! 24 | //! ### Backends 25 | //! 26 | //! Currently existing backends are: 27 | //! - [anyrender_vello](https://docs.rs/anyrender_vello) 28 | //! - [anyrender_vello_cpu](https://docs.rs/anyrender_vello_cpu) 29 | 30 | use kurbo::{Affine, Rect, Shape, Stroke}; 31 | use peniko::{BlendMode, Color, Fill, FontData, ImageBrushRef, Mix, StyleRef}; 32 | use std::sync::Arc; 33 | 34 | pub mod wasm_send_sync; 35 | pub use wasm_send_sync::*; 36 | pub mod types; 37 | pub use types::*; 38 | mod null_backend; 39 | pub use null_backend::*; 40 | 41 | /// Abstraction for rendering a scene to a window 42 | pub trait WindowRenderer { 43 | type ScenePainter<'a>: PaintScene 44 | where 45 | Self: 'a; 46 | fn resume(&mut self, window: Arc, width: u32, height: u32); 47 | fn suspend(&mut self); 48 | fn is_active(&self) -> bool; 49 | fn set_size(&mut self, width: u32, height: u32); 50 | fn render)>(&mut self, draw_fn: F); 51 | } 52 | 53 | /// Abstraction for rendering a scene to an image buffer 54 | pub trait ImageRenderer { 55 | type ScenePainter<'a>: PaintScene 56 | where 57 | Self: 'a; 58 | fn new(width: u32, height: u32) -> Self; 59 | fn resize(&mut self, width: u32, height: u32); 60 | fn reset(&mut self); 61 | fn render_to_vec)>( 62 | &mut self, 63 | draw_fn: F, 64 | vec: &mut Vec, 65 | ); 66 | fn render)>(&mut self, draw_fn: F, buffer: &mut [u8]); 67 | } 68 | 69 | /// Draw a scene to a buffer using an `ImageRenderer` 70 | pub fn render_to_buffer)>( 71 | draw_fn: F, 72 | width: u32, 73 | height: u32, 74 | ) -> Vec { 75 | let mut buf = Vec::with_capacity((width * height * 4) as usize); 76 | let mut renderer = R::new(width, height); 77 | renderer.render_to_vec(draw_fn, &mut buf); 78 | 79 | buf 80 | } 81 | 82 | /// Abstraction for drawing a 2D scene 83 | pub trait PaintScene { 84 | /// Removes all content from the scene 85 | fn reset(&mut self); 86 | 87 | /// Pushes a new layer clipped by the specified shape and composed with previous layers using the specified blend mode. 88 | /// Every drawing command after this call will be clipped by the shape until the layer is popped. 89 | /// However, the transforms are not saved or modified by the layer stack. 90 | fn push_layer( 91 | &mut self, 92 | blend: impl Into, 93 | alpha: f32, 94 | transform: Affine, 95 | clip: &impl Shape, 96 | ); 97 | 98 | /// Pushes a new clip layer clipped by the specified shape. 99 | /// Every drawing command after this call will be clipped by the shape until the layer is popped. 100 | /// However, the transforms are not saved or modified by the layer stack. 101 | fn push_clip_layer(&mut self, transform: Affine, clip: &impl Shape) { 102 | #[allow(deprecated, reason = "backwards compatibility until the next release")] 103 | self.push_layer(BlendMode::from(Mix::Clip), 1.0, transform, clip); 104 | } 105 | 106 | /// Pops the current layer. 107 | fn pop_layer(&mut self); 108 | 109 | /// Strokes a shape using the specified style and brush. 110 | fn stroke<'a>( 111 | &mut self, 112 | style: &Stroke, 113 | transform: Affine, 114 | brush: impl Into>, 115 | brush_transform: Option, 116 | shape: &impl Shape, 117 | ); 118 | 119 | /// Fills a shape using the specified style and brush. 120 | fn fill<'a>( 121 | &mut self, 122 | style: Fill, 123 | transform: Affine, 124 | brush: impl Into>, 125 | brush_transform: Option, 126 | shape: &impl Shape, 127 | ); 128 | 129 | /// Returns a builder for encoding a glyph run. 130 | #[allow(clippy::too_many_arguments)] 131 | fn draw_glyphs<'a, 's: 'a>( 132 | &'s mut self, 133 | font: &'a FontData, 134 | font_size: f32, 135 | hint: bool, 136 | normalized_coords: &'a [NormalizedCoord], 137 | style: impl Into>, 138 | brush: impl Into>, 139 | brush_alpha: f32, 140 | transform: Affine, 141 | glyph_transform: Option, 142 | glyphs: impl Iterator, 143 | ); 144 | 145 | /// Draw a rounded rectangle blurred with a gaussian filter. 146 | fn draw_box_shadow( 147 | &mut self, 148 | transform: Affine, 149 | rect: Rect, 150 | brush: Color, 151 | radius: f64, 152 | std_dev: f64, 153 | ); 154 | 155 | // --- Provided methods 156 | 157 | /// Utility method to draw an image at it's natural size. For more advanced image drawing use the `fill` method 158 | fn draw_image(&mut self, image: ImageBrushRef, transform: Affine) { 159 | self.fill( 160 | Fill::NonZero, 161 | transform, 162 | image, 163 | None, 164 | &Rect::new( 165 | 0.0, 166 | 0.0, 167 | image.image.width as f64, 168 | image.image.height as f64, 169 | ), 170 | ); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /crates/wgpu_context/src/buffer_renderer.rs: -------------------------------------------------------------------------------- 1 | use crate::{DeviceHandle, block_on_wgpu, util::create_texture}; 2 | use wgpu::{ 3 | BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Device, Extent3d, Queue, 4 | TexelCopyBufferInfo, TexelCopyBufferLayout, TextureFormat, TextureUsages, TextureView, 5 | }; 6 | 7 | #[derive(Clone, Debug)] 8 | pub struct BufferRendererConfig { 9 | pub width: u32, 10 | pub height: u32, 11 | pub usage: TextureUsages, 12 | } 13 | 14 | /// Utility struct for rendering to `Vec` 15 | pub struct BufferRenderer { 16 | // The device and queue for rendering to the surface 17 | pub dev_id: usize, 18 | pub device_handle: DeviceHandle, 19 | 20 | config: BufferRendererConfig, 21 | texture_view: wgpu::TextureView, 22 | gpu_buffer: wgpu::Buffer, 23 | } 24 | 25 | impl std::fmt::Debug for BufferRenderer { 26 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 27 | f.debug_struct("SurfaceRenderer") 28 | .field("dev_id", &self.dev_id) 29 | .field("config", &self.config) 30 | .finish() 31 | } 32 | } 33 | 34 | impl BufferRenderer { 35 | /// Creates a new render surface for the specified window and dimensions. 36 | pub fn new(config: BufferRendererConfig, device_handle: DeviceHandle, dev_id: usize) -> Self { 37 | let texture_view = create_texture( 38 | config.width, 39 | config.height, 40 | TextureFormat::Rgba8Unorm, 41 | config.usage | TextureUsages::COPY_SRC, 42 | &device_handle.device, 43 | ); 44 | 45 | let padded_byte_width = (config.width * 4).next_multiple_of(256); 46 | let buffer_size = padded_byte_width as u64 * config.height as u64; 47 | let gpu_buffer = device_handle.device.create_buffer(&BufferDescriptor { 48 | label: None, 49 | size: buffer_size, 50 | usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST, 51 | mapped_at_creation: false, 52 | }); 53 | 54 | Self { 55 | dev_id, 56 | device_handle, 57 | config, 58 | texture_view, 59 | gpu_buffer, 60 | } 61 | } 62 | 63 | pub fn device(&self) -> &Device { 64 | &self.device_handle.device 65 | } 66 | 67 | pub fn queue(&self) -> &Queue { 68 | &self.device_handle.queue 69 | } 70 | 71 | pub fn size(&self) -> Extent3d { 72 | Extent3d { 73 | width: self.config.width, 74 | height: self.config.height, 75 | depth_or_array_layers: 1, 76 | } 77 | } 78 | 79 | pub fn resize(&mut self, width: u32, height: u32) { 80 | self.config.width = width; 81 | self.config.height = height; 82 | self.texture_view = create_texture( 83 | self.config.width, 84 | self.config.height, 85 | TextureFormat::Rgba8Unorm, 86 | self.config.usage | TextureUsages::COPY_SRC, 87 | &self.device_handle.device, 88 | ); 89 | 90 | let padded_byte_width = (width * 4).next_multiple_of(256); 91 | let buffer_size = padded_byte_width as u64 * height as u64; 92 | self.gpu_buffer = self.device_handle.device.create_buffer(&BufferDescriptor { 93 | label: None, 94 | size: buffer_size, 95 | usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST, 96 | mapped_at_creation: false, 97 | }); 98 | } 99 | 100 | // /// Resizes the surface to the new dimensions. 101 | // pub fn resize(&mut self, width: u32, height: u32) { 102 | // // TODO: Use clever resize semantics to avoid thrashing the memory allocator during a resize 103 | // // especially important on metal. 104 | // if let Some(intermediate_texture_stuff) = &mut self.intermediate_texture { 105 | // intermediate_texture_stuff.texture_view = create_intermediate_texture( 106 | // width, 107 | // height, 108 | // intermediate_texture_stuff.config.usage, 109 | // &self.device_handle.device, 110 | // ); 111 | // } 112 | // self.config.width = width; 113 | // self.config.height = height; 114 | // self.configure(); 115 | // } 116 | 117 | pub fn target_texture_view(&self) -> TextureView { 118 | self.texture_view.clone() 119 | } 120 | 121 | pub fn copy_texture_to_vec(&self, cpu_buffer: &mut Vec) { 122 | cpu_buffer.clear(); 123 | cpu_buffer.reserve((self.config.width * self.config.height * 4) as usize); 124 | self.copy_texture_to_buffer(&mut *cpu_buffer); 125 | } 126 | 127 | pub fn copy_texture_to_buffer(&self, cpu_buffer: &mut [u8]) { 128 | let mut encoder = self 129 | .device() 130 | .create_command_encoder(&CommandEncoderDescriptor { 131 | label: Some("Copy out buffer"), 132 | }); 133 | let row_byte_width = self.config.width as usize * 4; 134 | let padded_row_byte_width = row_byte_width.next_multiple_of(256); 135 | 136 | let texture = self.texture_view.texture(); 137 | encoder.copy_texture_to_buffer( 138 | texture.as_image_copy(), 139 | TexelCopyBufferInfo { 140 | buffer: &self.gpu_buffer, 141 | layout: TexelCopyBufferLayout { 142 | offset: 0, 143 | bytes_per_row: Some(padded_row_byte_width as u32), 144 | rows_per_image: None, 145 | }, 146 | }, 147 | texture.size(), 148 | ); 149 | 150 | self.queue().submit([encoder.finish()]); 151 | let buf_slice = self.gpu_buffer.slice(..); 152 | 153 | let (sender, receiver) = futures_intrusive::channel::shared::oneshot_channel(); 154 | buf_slice.map_async(wgpu::MapMode::Read, move |v| sender.send(v).unwrap()); 155 | 156 | if let Ok(recv_result) = 157 | block_on_wgpu(self.device(), receiver.receive()).inspect_err(|err| { 158 | panic!("channel inaccessible: {:#}", err); 159 | }) 160 | { 161 | let _ = recv_result.unwrap(); 162 | } 163 | 164 | let data = buf_slice.get_mapped_range(); 165 | 166 | // Pad result 167 | for row in 0..(self.config.height as usize) { 168 | let src_start = row * padded_row_byte_width; 169 | let src = &data[src_start..(src_start + row_byte_width)]; 170 | 171 | let dest_start = row * row_byte_width; 172 | cpu_buffer[dest_start..(dest_start + row_byte_width)].clone_from_slice(src); 173 | } 174 | 175 | // Unmap buffer 176 | drop(data); 177 | self.gpu_buffer.unmap(); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /crates/wgpu_context/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 the Vello Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | //! Simple helpers for managing wgpu state and surfaces. 5 | 6 | use wgpu::{ 7 | Adapter, Device, Features, Instance, Limits, MemoryHints, Queue, Surface, SurfaceTarget, 8 | }; 9 | 10 | mod buffer_renderer; 11 | mod error; 12 | mod surface_renderer; 13 | mod util; 14 | 15 | pub use buffer_renderer::{BufferRenderer, BufferRendererConfig}; 16 | pub use error::WgpuContextError; 17 | pub use surface_renderer::{SurfaceRenderer, SurfaceRendererConfiguration, TextureConfiguration}; 18 | pub use util::block_on_wgpu; 19 | 20 | /// A wgpu `Device`, it's associated `Queue`, and the `Adapter` and `Instance` used to create them 21 | #[derive(Clone, Debug)] 22 | pub struct DeviceHandle { 23 | pub instance: Instance, 24 | pub adapter: Adapter, 25 | pub device: Device, 26 | pub queue: Queue, 27 | } 28 | 29 | /// Simple render context that maintains wgpu state for rendering the pipeline. 30 | pub struct WGPUContext { 31 | /// A WGPU `Instance`. This only needs to be created once per application. 32 | pub instance: Instance, 33 | /// A pool of already-created devices so that we can avoid recreating devices 34 | /// when we already have a suitable one available 35 | pub device_pool: Vec, 36 | 37 | // Config 38 | extra_features: Option, 39 | override_limits: Option, 40 | } 41 | 42 | impl Default for WGPUContext { 43 | fn default() -> Self { 44 | Self::new() 45 | } 46 | } 47 | 48 | impl WGPUContext { 49 | pub fn new() -> Self { 50 | Self::with_features_and_limits(None, None) 51 | } 52 | 53 | pub fn with_features_and_limits( 54 | extra_features: Option, 55 | override_limits: Option, 56 | ) -> Self { 57 | Self { 58 | instance: Instance::new(&wgpu::InstanceDescriptor { 59 | backends: wgpu::Backends::from_env().unwrap_or_default(), 60 | flags: wgpu::InstanceFlags::from_build_config().with_env(), 61 | backend_options: wgpu::BackendOptions::from_env_or_default(), 62 | memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(), 63 | }), 64 | device_pool: Vec::new(), 65 | extra_features, 66 | override_limits, 67 | } 68 | } 69 | 70 | /// Creates a new surface for the specified window and dimensions. 71 | pub async fn create_surface<'w>( 72 | &mut self, 73 | window: impl Into>, 74 | surface_config: SurfaceRendererConfiguration, 75 | intermediate_texture_config: Option, 76 | ) -> Result, WgpuContextError> { 77 | // Create a surface from the window handle 78 | let surface = self.instance.create_surface(window.into())?; 79 | 80 | // Find or create a suitable device for rendering to the surface 81 | let dev_id = self 82 | .find_or_create_device(Some(&surface)) 83 | .await 84 | .or(Err(WgpuContextError::NoCompatibleDevice))?; 85 | let device_handle = self.device_pool[dev_id].clone(); 86 | 87 | SurfaceRenderer::new( 88 | surface, 89 | surface_config, 90 | intermediate_texture_config, 91 | device_handle, 92 | dev_id, 93 | ) 94 | .await 95 | } 96 | 97 | /// Creates a new `BufferRenderer` for the specified dimensions. 98 | pub async fn create_buffer_renderer( 99 | &mut self, 100 | config: BufferRendererConfig, 101 | ) -> Result { 102 | // Find or create a suitable device for rendering to the surface 103 | let dev_id = self 104 | .find_or_create_device(None) 105 | .await 106 | .or(Err(WgpuContextError::NoCompatibleDevice))?; 107 | let device_handle = self.device_pool[dev_id].clone(); 108 | 109 | Ok(BufferRenderer::new(config, device_handle, dev_id)) 110 | } 111 | 112 | /// Finds or creates a compatible device handle id. 113 | pub async fn find_or_create_device( 114 | &mut self, 115 | compatible_surface: Option<&Surface<'_>>, 116 | ) -> Result { 117 | match self.find_existing_device(compatible_surface) { 118 | Some(device_id) => Ok(device_id), 119 | None => self.create_device(compatible_surface).await, 120 | } 121 | } 122 | 123 | /// Finds or creates a compatible device handle id. 124 | fn find_existing_device(&self, compatible_surface: Option<&Surface<'_>>) -> Option { 125 | match compatible_surface { 126 | Some(s) => self 127 | .device_pool 128 | .iter() 129 | .enumerate() 130 | .find(|(_, d)| d.adapter.is_surface_supported(s)) 131 | .map(|(i, _)| i), 132 | None => (!self.device_pool.is_empty()).then_some(0), 133 | } 134 | } 135 | 136 | /// Creates a compatible device handle id. 137 | async fn create_device( 138 | &mut self, 139 | compatible_surface: Option<&Surface<'_>>, 140 | ) -> Result { 141 | let instance = self.instance.clone(); 142 | let adapter = 143 | wgpu::util::initialize_adapter_from_env_or_default(&instance, compatible_surface) 144 | .await?; 145 | 146 | // Determine features to request 147 | // The user may request additional features 148 | let requested_features = self.extra_features.unwrap_or(Features::empty()); 149 | let available_features = adapter.features(); 150 | let required_features = requested_features & available_features; 151 | 152 | // Determine limits to request 153 | // The user may override the limits 154 | let required_limits = self.override_limits.clone().unwrap_or_default(); 155 | 156 | // Create the device and the queue 157 | let descripter = wgpu::DeviceDescriptor { 158 | label: None, 159 | required_features, 160 | required_limits, 161 | memory_hints: MemoryHints::MemoryUsage, 162 | trace: wgpu::Trace::default(), 163 | }; 164 | let (device, queue) = adapter.request_device(&descripter).await?; 165 | 166 | // Create the device handle and store in the pool 167 | let device_handle = DeviceHandle { 168 | instance, 169 | adapter, 170 | device, 171 | queue, 172 | }; 173 | self.device_pool.push(device_handle); 174 | 175 | // Return the ID 176 | Ok(self.device_pool.len() - 1) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /crates/anyrender_skia/src/opengl.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::CString, num::NonZeroU32, sync::Arc}; 2 | 3 | use glutin::display::DisplayApiPreference; 4 | use glutin::{ 5 | config::{ConfigTemplateBuilder, GetGlConfig, GlConfig}, 6 | context::{ContextAttributesBuilder, PossiblyCurrentContext}, 7 | display::{Display, GetGlDisplay}, 8 | prelude::{GlDisplay, NotCurrentGlContext, PossiblyCurrentGlContext}, 9 | surface::{GlSurface, SurfaceAttributesBuilder, WindowSurface}, 10 | }; 11 | use skia_safe::{ 12 | Surface, 13 | gpu::{ 14 | DirectContext, direct_contexts, 15 | gl::{FramebufferInfo, Interface}, 16 | }, 17 | }; 18 | 19 | use crate::window_renderer::SkiaBackend; 20 | 21 | pub(crate) struct OpenGLBackend { 22 | surface: Option, 23 | gr_context: DirectContext, 24 | gl_surface: glutin::surface::Surface, 25 | gl_context: PossiblyCurrentContext, 26 | fb_info: FramebufferInfo, 27 | } 28 | 29 | impl OpenGLBackend { 30 | pub(crate) fn new( 31 | window: Arc, 32 | width: u32, 33 | height: u32, 34 | ) -> OpenGLBackend { 35 | let raw_display_handle = window.display_handle().unwrap().as_raw(); 36 | let raw_window_handle = window.window_handle().unwrap().as_raw(); 37 | 38 | let gl_display = unsafe { 39 | Display::new( 40 | raw_display_handle, 41 | #[cfg(target_os = "macos")] 42 | DisplayApiPreference::Cgl, 43 | #[cfg(target_os = "windows")] 44 | DisplayApiPreference::Wgl(Some(raw_window_handle.clone())), 45 | #[cfg(not(any(target_os = "windows", target_os = "macos")))] 46 | DisplayApiPreference::Egl, 47 | ) 48 | .unwrap() 49 | }; 50 | 51 | let gl_config_template = ConfigTemplateBuilder::new().with_transparency(true).build(); 52 | let gl_config = unsafe { 53 | gl_display 54 | .find_configs(gl_config_template) 55 | .unwrap() 56 | .reduce(|accum, config| { 57 | let transparency_check = config.supports_transparency().unwrap_or(false) 58 | & !accum.supports_transparency().unwrap_or(false); 59 | 60 | if transparency_check || config.num_samples() < accum.num_samples() { 61 | config 62 | } else { 63 | accum 64 | } 65 | }) 66 | .unwrap() 67 | }; 68 | 69 | let gl_context_attrs = ContextAttributesBuilder::new().build(Some(raw_window_handle)); 70 | let gl_surface_attrs = SurfaceAttributesBuilder::::new().build( 71 | raw_window_handle, 72 | NonZeroU32::new(width).expect("width should be a positive value"), 73 | NonZeroU32::new(height).expect("height should be a positive value"), 74 | ); 75 | 76 | let gl_not_current_context = unsafe { 77 | gl_display 78 | .create_context(&gl_config, &gl_context_attrs) 79 | .unwrap() 80 | }; 81 | 82 | let gl_surface = unsafe { 83 | gl_config 84 | .display() 85 | .create_window_surface(&gl_config, &gl_surface_attrs) 86 | .unwrap() 87 | }; 88 | 89 | let gl_context = gl_not_current_context.make_current(&gl_surface).unwrap(); 90 | 91 | gl::load_with(|s| { 92 | gl_config 93 | .display() 94 | .get_proc_address(CString::new(s).unwrap().as_c_str()) 95 | }); 96 | 97 | let interface = Interface::new_load_with(|name| { 98 | if name == "eglGetCurrentDisplay" { 99 | return std::ptr::null(); 100 | } 101 | gl_config 102 | .display() 103 | .get_proc_address(CString::new(name).unwrap().as_c_str()) 104 | }) 105 | .unwrap(); 106 | 107 | let mut gr_context = direct_contexts::make_gl(interface, None).unwrap(); 108 | 109 | let mut fb_info = { 110 | let mut fboid: gl::types::GLint = 0; 111 | unsafe { 112 | gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fboid); 113 | } 114 | 115 | skia_safe::gpu::gl::FramebufferInfo { 116 | fboid: fboid.try_into().unwrap(), 117 | format: skia_safe::gpu::gl::Format::RGBA8.into(), 118 | ..Default::default() 119 | } 120 | }; 121 | 122 | OpenGLBackend { 123 | surface: Some(Self::create_surface( 124 | width, 125 | height, 126 | &mut gr_context, 127 | &gl_surface, 128 | &gl_context, 129 | &mut fb_info, 130 | )), 131 | gr_context, 132 | gl_surface, 133 | gl_context, 134 | fb_info, 135 | } 136 | } 137 | 138 | fn create_surface( 139 | width: u32, 140 | height: u32, 141 | gr_context: &mut DirectContext, 142 | gl_surface: &glutin::surface::Surface, 143 | gl_context: &PossiblyCurrentContext, 144 | fb_info: &mut FramebufferInfo, 145 | ) -> Surface { 146 | gl_surface.resize( 147 | gl_context, 148 | NonZeroU32::new(width).unwrap(), 149 | NonZeroU32::new(height).unwrap(), 150 | ); 151 | 152 | let backend_render_target = skia_safe::gpu::backend_render_targets::make_gl( 153 | (width as i32, height as i32), 154 | gl_context.config().num_samples() as usize, 155 | gl_context.config().stencil_size() as usize, 156 | *fb_info, 157 | ); 158 | 159 | skia_safe::gpu::surfaces::wrap_backend_render_target( 160 | gr_context, 161 | &backend_render_target, 162 | skia_safe::gpu::SurfaceOrigin::BottomLeft, 163 | skia_safe::ColorType::RGBA8888, 164 | None, 165 | None, 166 | ) 167 | .unwrap() 168 | } 169 | } 170 | 171 | impl SkiaBackend for OpenGLBackend { 172 | fn set_size(&mut self, width: u32, height: u32) { 173 | self.surface = Some(Self::create_surface( 174 | width, 175 | height, 176 | &mut self.gr_context, 177 | &self.gl_surface, 178 | &self.gl_context, 179 | &mut self.fb_info, 180 | )); 181 | } 182 | 183 | fn prepare(&mut self) -> Option { 184 | self.gl_context.make_current(&self.gl_surface).unwrap(); 185 | self.surface.take() 186 | } 187 | 188 | fn flush(&mut self, mut surface: Surface) { 189 | self.gr_context.flush_and_submit(); 190 | self.gl_surface.swap_buffers(&self.gl_context).unwrap(); 191 | surface.canvas().discard(); 192 | 193 | self.surface = Some(surface); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /crates/anyrender_svg/src/render.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 the Vello Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use crate::util; 5 | use anyrender::PaintScene; 6 | use kurbo::{Affine, BezPath}; 7 | use peniko::{BlendMode, Fill}; 8 | use usvg::{Node, Path}; 9 | 10 | pub(crate) fn render_group( 11 | scene: &mut S, 12 | group: &usvg::Group, 13 | transform: Affine, 14 | global_transform: Affine, 15 | error_handler: &mut F, 16 | ) { 17 | for node in group.children() { 18 | let transform = transform * util::to_affine(&node.abs_transform()); 19 | match node { 20 | usvg::Node::Group(g) => { 21 | let alpha = g.opacity().get(); 22 | let is_fully_opaque = alpha >= 1.0; 23 | let mix = util::to_mix(g.blend_mode(), is_fully_opaque); 24 | 25 | // Support clip-path with a single path 26 | let clip_path = g 27 | .clip_path() 28 | .and_then(|path| path.root().children().first()); 29 | 30 | let did_push_layer = match clip_path { 31 | // If there is a clip path, then push a layer that clips using it 32 | Some(usvg::Node::Path(clip_path)) => { 33 | let local_path = util::to_bez_path(clip_path); 34 | scene.push_layer( 35 | BlendMode { 36 | mix, 37 | compose: peniko::Compose::SrcOver, 38 | }, 39 | alpha, 40 | global_transform * transform, 41 | &local_path, 42 | ); 43 | 44 | true 45 | } 46 | // Else if there is blending to be done then push a layer with a rectangular clip 47 | #[allow(deprecated)] 48 | _ if mix != peniko::Mix::Clip => { 49 | // Use bounding box as the clip path. 50 | let bounding_box = g.layer_bounding_box(); 51 | let rect = kurbo::Rect::from_origin_size( 52 | (bounding_box.x(), bounding_box.y()), 53 | (bounding_box.width() as f64, bounding_box.height() as f64), 54 | ); 55 | scene.push_layer( 56 | BlendMode { 57 | mix, 58 | compose: peniko::Compose::SrcOver, 59 | }, 60 | alpha, 61 | global_transform * transform, 62 | &rect, 63 | ); 64 | 65 | true 66 | } 67 | // Else if there is no clip or blending then don't push a layer 68 | _ => false, 69 | }; 70 | 71 | render_group(scene, g, Affine::IDENTITY, global_transform, error_handler); 72 | 73 | if did_push_layer { 74 | scene.pop_layer(); 75 | } 76 | } 77 | usvg::Node::Path(path) => { 78 | if !path.is_visible() { 79 | continue; 80 | } 81 | let local_path = util::to_bez_path(path); 82 | 83 | let transform = global_transform * transform; 84 | match path.paint_order() { 85 | usvg::PaintOrder::FillAndStroke => { 86 | fill(scene, error_handler, path, transform, &local_path, node); 87 | stroke(scene, error_handler, path, transform, &local_path, node); 88 | } 89 | usvg::PaintOrder::StrokeAndFill => { 90 | stroke(scene, error_handler, path, transform, &local_path, node); 91 | fill(scene, error_handler, path, transform, &local_path, node); 92 | } 93 | } 94 | } 95 | usvg::Node::Image(img) => { 96 | if !img.is_visible() { 97 | continue; 98 | } 99 | match img.kind() { 100 | usvg::ImageKind::JPEG(_) 101 | | usvg::ImageKind::PNG(_) 102 | | usvg::ImageKind::GIF(_) 103 | | usvg::ImageKind::WEBP(_) => { 104 | #[cfg(feature = "image")] 105 | { 106 | let Ok(decoded_image) = util::decode_raw_raster_image(img.kind()) 107 | else { 108 | error_handler(scene, node); 109 | continue; 110 | }; 111 | let image = util::into_image(decoded_image); 112 | let image_ts = global_transform * util::to_affine(&img.abs_transform()); 113 | scene.draw_image(image.as_ref(), image_ts); 114 | } 115 | 116 | #[cfg(not(feature = "image"))] 117 | { 118 | error_handler(scene, node); 119 | continue; 120 | } 121 | } 122 | usvg::ImageKind::SVG(svg) => { 123 | render_group( 124 | scene, 125 | svg.root(), 126 | transform, 127 | global_transform, 128 | error_handler, 129 | ); 130 | } 131 | } 132 | } 133 | usvg::Node::Text(text) => { 134 | render_group( 135 | scene, 136 | text.flattened(), 137 | transform, 138 | global_transform, 139 | error_handler, 140 | ); 141 | } 142 | } 143 | } 144 | } 145 | 146 | fn fill( 147 | scene: &mut S, 148 | error_handler: &mut F, 149 | path: &Path, 150 | transform: Affine, 151 | local_path: &BezPath, 152 | node: &Node, 153 | ) { 154 | if let Some(fill) = &path.fill() { 155 | if let Some((paint, brush_transform)) = util::to_brush(fill.paint(), fill.opacity()) { 156 | scene.fill( 157 | match fill.rule() { 158 | usvg::FillRule::NonZero => Fill::NonZero, 159 | usvg::FillRule::EvenOdd => Fill::EvenOdd, 160 | }, 161 | transform, 162 | paint.as_ref(), 163 | Some(brush_transform), 164 | local_path, 165 | ); 166 | } else { 167 | error_handler(scene, node); 168 | } 169 | } 170 | } 171 | 172 | fn stroke( 173 | scene: &mut S, 174 | error_handler: &mut F, 175 | path: &Path, 176 | transform: Affine, 177 | local_path: &BezPath, 178 | node: &Node, 179 | ) { 180 | if let Some(stroke) = &path.stroke() { 181 | if let Some((paint, brush_transform)) = util::to_brush(stroke.paint(), stroke.opacity()) { 182 | let conv_stroke = util::to_stroke(stroke); 183 | scene.stroke( 184 | &conv_stroke, 185 | transform, 186 | paint.as_ref(), 187 | Some(brush_transform), 188 | local_path, 189 | ); 190 | } else { 191 | error_handler(scene, node); 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /crates/anyrender_vello_hybrid/src/scene.rs: -------------------------------------------------------------------------------- 1 | use anyrender::{NormalizedCoord, Paint, PaintRef, PaintScene}; 2 | use kurbo::{Affine, Rect, Shape, Stroke}; 3 | use peniko::{BlendMode, Color, Fill, FontData, ImageBrush, ImageData, StyleRef}; 4 | use rustc_hash::FxHashMap; 5 | use vello_common::paint::{ImageId, ImageSource, PaintType}; 6 | use vello_hybrid::Renderer; 7 | use wgpu::{CommandEncoder, Device, Queue}; 8 | 9 | const DEFAULT_TOLERANCE: f64 = 0.1; 10 | 11 | fn anyrender_paint_to_vello_hybrid_paint<'a>( 12 | paint: PaintRef<'a>, 13 | mut image_manager: &mut Option<&mut ImageManager<'_>>, 14 | ) -> PaintType { 15 | match paint { 16 | Paint::Solid(alpha_color) => PaintType::Solid(alpha_color), 17 | Paint::Gradient(gradient) => PaintType::Gradient(gradient.clone()), 18 | 19 | Paint::Image(image_brush) => { 20 | if let Some(image_manager) = &mut image_manager { 21 | let image_id = image_manager.upload_image(image_brush.image); 22 | PaintType::Image(ImageBrush { 23 | image: ImageSource::OpaqueId(image_id), 24 | sampler: image_brush.sampler, 25 | }) 26 | } else { 27 | PaintType::Solid(peniko::color::palette::css::TRANSPARENT) 28 | } 29 | } 30 | 31 | // TODO: custom paint 32 | Paint::Custom(_) => PaintType::Solid(peniko::color::palette::css::TRANSPARENT), 33 | } 34 | } 35 | 36 | pub(crate) struct ImageManager<'a> { 37 | pub(crate) renderer: &'a mut Renderer, 38 | pub(crate) device: &'a Device, 39 | pub(crate) queue: &'a Queue, 40 | pub(crate) encoder: &'a mut CommandEncoder, 41 | pub(crate) cache: &'a mut FxHashMap, 42 | } 43 | 44 | impl ImageManager<'_> { 45 | pub(crate) fn upload_image(&mut self, image: &ImageData) -> ImageId { 46 | let peniko_id = image.data.id(); 47 | 48 | // Try to get ImageId from cache first 49 | if let Some(atlas_id) = self.cache.get(&peniko_id) { 50 | return *atlas_id; 51 | }; 52 | 53 | // Convert ImageData to Pixmap 54 | let ImageSource::Pixmap(pixmap) = ImageSource::from_peniko_image_data(image) else { 55 | unreachable!(); // ImageSource::from_peniko_image_data always return a Pixmap 56 | }; 57 | 58 | // Upload Pixamp 59 | let atlas_id = self 60 | .renderer 61 | .upload_image(self.device, self.queue, self.encoder, &pixmap); 62 | 63 | // Store ImageId in cache 64 | self.cache.insert(peniko_id, atlas_id); 65 | 66 | // Return ImageId 67 | atlas_id 68 | } 69 | } 70 | 71 | pub struct VelloHybridScenePainter<'s> { 72 | pub(crate) scene: &'s mut vello_hybrid::Scene, 73 | pub(crate) image_manager: Option>, 74 | } 75 | 76 | impl VelloHybridScenePainter<'_> { 77 | pub fn new<'s>(scene: &'s mut vello_hybrid::Scene) -> VelloHybridScenePainter<'s> { 78 | VelloHybridScenePainter { 79 | scene, 80 | image_manager: None, 81 | } 82 | } 83 | } 84 | 85 | impl PaintScene for VelloHybridScenePainter<'_> { 86 | fn reset(&mut self) { 87 | self.scene.reset(); 88 | } 89 | 90 | fn push_layer( 91 | &mut self, 92 | blend: impl Into, 93 | alpha: f32, 94 | transform: Affine, 95 | clip: &impl Shape, 96 | ) { 97 | self.scene.set_transform(transform); 98 | self.scene.push_layer( 99 | Some(&clip.into_path(DEFAULT_TOLERANCE)), 100 | Some(blend.into()), 101 | Some(alpha), 102 | None, 103 | ); 104 | } 105 | 106 | fn push_clip_layer(&mut self, transform: Affine, clip: &impl Shape) { 107 | self.scene.set_transform(transform); 108 | self.scene 109 | .push_clip_layer(&clip.into_path(DEFAULT_TOLERANCE)); 110 | } 111 | 112 | fn pop_layer(&mut self) { 113 | self.scene.pop_layer(); 114 | } 115 | 116 | fn stroke<'a>( 117 | &mut self, 118 | style: &Stroke, 119 | transform: Affine, 120 | paint: impl Into>, 121 | brush_transform: Option, 122 | shape: &impl Shape, 123 | ) { 124 | self.scene.set_transform(transform); 125 | self.scene.set_stroke(style.clone()); 126 | let paint = 127 | anyrender_paint_to_vello_hybrid_paint(paint.into(), &mut self.image_manager.as_mut()); 128 | self.scene.set_paint(paint); 129 | self.scene 130 | .set_paint_transform(brush_transform.unwrap_or(Affine::IDENTITY)); 131 | self.scene.stroke_path(&shape.into_path(DEFAULT_TOLERANCE)); 132 | } 133 | 134 | fn fill<'a>( 135 | &mut self, 136 | style: Fill, 137 | transform: Affine, 138 | paint: impl Into>, 139 | brush_transform: Option, 140 | shape: &impl Shape, 141 | ) { 142 | self.scene.set_transform(transform); 143 | self.scene.set_fill_rule(style); 144 | let paint = 145 | anyrender_paint_to_vello_hybrid_paint(paint.into(), &mut self.image_manager.as_mut()); 146 | self.scene.set_paint(paint); 147 | self.scene 148 | .set_paint_transform(brush_transform.unwrap_or(Affine::IDENTITY)); 149 | self.scene.fill_path(&shape.into_path(DEFAULT_TOLERANCE)); 150 | } 151 | 152 | fn draw_glyphs<'a, 's: 'a>( 153 | &'a mut self, 154 | font: &'a FontData, 155 | font_size: f32, 156 | hint: bool, 157 | normalized_coords: &'a [NormalizedCoord], 158 | style: impl Into>, 159 | paint: impl Into>, 160 | _brush_alpha: f32, 161 | transform: Affine, 162 | glyph_transform: Option, 163 | glyphs: impl Iterator, 164 | ) { 165 | let paint = 166 | anyrender_paint_to_vello_hybrid_paint(paint.into(), &mut self.image_manager.as_mut()); 167 | self.scene.set_paint(paint); 168 | self.scene.set_transform(transform); 169 | 170 | fn into_vello_hybrid_glyph(g: anyrender::Glyph) -> vello_common::glyph::Glyph { 171 | vello_common::glyph::Glyph { 172 | id: g.id, 173 | x: g.x, 174 | y: g.y, 175 | } 176 | } 177 | 178 | let style: StyleRef<'a> = style.into(); 179 | match style { 180 | StyleRef::Fill(fill) => { 181 | self.scene.set_fill_rule(fill); 182 | self.scene 183 | .glyph_run(font) 184 | .font_size(font_size) 185 | .hint(hint) 186 | .normalized_coords(normalized_coords) 187 | .glyph_transform(glyph_transform.unwrap_or_default()) 188 | .fill_glyphs(glyphs.map(into_vello_hybrid_glyph)); 189 | } 190 | StyleRef::Stroke(stroke) => { 191 | self.scene.set_stroke(stroke.clone()); 192 | self.scene 193 | .glyph_run(font) 194 | .font_size(font_size) 195 | .hint(hint) 196 | .normalized_coords(normalized_coords) 197 | .glyph_transform(glyph_transform.unwrap_or_default()) 198 | .stroke_glyphs(glyphs.map(into_vello_hybrid_glyph)); 199 | } 200 | } 201 | } 202 | fn draw_box_shadow( 203 | &mut self, 204 | _transform: Affine, 205 | _rect: Rect, 206 | _color: Color, 207 | _radius: f64, 208 | _std_dev: f64, 209 | ) { 210 | // FIXME: implement once supported in vello_hybrid 211 | // 212 | // self.scene.set_transform(transform); 213 | // self.scene.set_paint(PaintType::Solid(color)); 214 | // self.scene 215 | // .fill_blurred_rounded_rect(&rect, radius as f32, std_dev as f32); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /crates/anyrender_vello/src/window_renderer.rs: -------------------------------------------------------------------------------- 1 | use anyrender::{WindowHandle, WindowRenderer}; 2 | use debug_timer::debug_timer; 3 | use peniko::Color; 4 | use rustc_hash::FxHashMap; 5 | use std::sync::{ 6 | Arc, 7 | atomic::{self, AtomicU64}, 8 | }; 9 | use vello::{ 10 | AaConfig, AaSupport, RenderParams, Renderer as VelloRenderer, RendererOptions, 11 | Scene as VelloScene, 12 | }; 13 | use wgpu::{Features, Limits, PresentMode, TextureFormat, TextureUsages}; 14 | use wgpu_context::{ 15 | DeviceHandle, SurfaceRenderer, SurfaceRendererConfiguration, TextureConfiguration, WGPUContext, 16 | }; 17 | 18 | use crate::{CustomPaintSource, DEFAULT_THREADS, VelloScenePainter}; 19 | 20 | static PAINT_SOURCE_ID: AtomicU64 = AtomicU64::new(0); 21 | 22 | // Simple struct to hold the state of the renderer 23 | struct ActiveRenderState { 24 | renderer: VelloRenderer, 25 | render_surface: SurfaceRenderer<'static>, 26 | } 27 | 28 | #[allow(clippy::large_enum_variant)] 29 | enum RenderState { 30 | Active(ActiveRenderState), 31 | Suspended, 32 | } 33 | 34 | impl RenderState { 35 | fn current_device_handle(&self) -> Option<&DeviceHandle> { 36 | let RenderState::Active(state) = self else { 37 | return None; 38 | }; 39 | Some(&state.render_surface.device_handle) 40 | } 41 | } 42 | 43 | #[derive(Clone)] 44 | pub struct VelloRendererOptions { 45 | pub features: Option, 46 | pub limits: Option, 47 | pub base_color: Color, 48 | pub antialiasing_method: AaConfig, 49 | } 50 | 51 | impl Default for VelloRendererOptions { 52 | fn default() -> Self { 53 | Self { 54 | features: None, 55 | limits: None, 56 | base_color: Color::WHITE, 57 | antialiasing_method: AaConfig::Msaa16, 58 | } 59 | } 60 | } 61 | 62 | pub struct VelloWindowRenderer { 63 | // The fields MUST be in this order, so that the surface is dropped before the window 64 | // Window is cached even when suspended so that it can be reused when the app is resumed after being suspended 65 | render_state: RenderState, 66 | window_handle: Option>, 67 | 68 | // Vello 69 | wgpu_context: WGPUContext, 70 | scene: VelloScene, 71 | config: VelloRendererOptions, 72 | 73 | custom_paint_sources: FxHashMap>, 74 | } 75 | impl VelloWindowRenderer { 76 | #[allow(clippy::new_without_default)] 77 | pub fn new() -> Self { 78 | Self::with_options(VelloRendererOptions::default()) 79 | } 80 | 81 | pub fn with_options(config: VelloRendererOptions) -> Self { 82 | let features = config.features.unwrap_or_default() 83 | | Features::CLEAR_TEXTURE 84 | | Features::PIPELINE_CACHE; 85 | Self { 86 | wgpu_context: WGPUContext::with_features_and_limits( 87 | Some(features), 88 | config.limits.clone(), 89 | ), 90 | config, 91 | render_state: RenderState::Suspended, 92 | window_handle: None, 93 | scene: VelloScene::new(), 94 | custom_paint_sources: FxHashMap::default(), 95 | } 96 | } 97 | 98 | pub fn current_device_handle(&self) -> Option<&DeviceHandle> { 99 | self.render_state.current_device_handle() 100 | } 101 | 102 | pub fn register_custom_paint_source(&mut self, mut source: Box) -> u64 { 103 | if let Some(device_handle) = self.render_state.current_device_handle() { 104 | source.resume(device_handle); 105 | } 106 | let id = PAINT_SOURCE_ID.fetch_add(1, atomic::Ordering::SeqCst); 107 | self.custom_paint_sources.insert(id, source); 108 | 109 | id 110 | } 111 | 112 | pub fn unregister_custom_paint_source(&mut self, id: u64) { 113 | if let Some(mut source) = self.custom_paint_sources.remove(&id) { 114 | source.suspend(); 115 | drop(source); 116 | } 117 | } 118 | } 119 | 120 | impl WindowRenderer for VelloWindowRenderer { 121 | type ScenePainter<'a> 122 | = VelloScenePainter<'a, 'a> 123 | where 124 | Self: 'a; 125 | 126 | fn is_active(&self) -> bool { 127 | matches!(self.render_state, RenderState::Active(_)) 128 | } 129 | 130 | fn resume(&mut self, window_handle: Arc, width: u32, height: u32) { 131 | // Create wgpu_context::SurfaceRenderer 132 | let render_surface = pollster::block_on(self.wgpu_context.create_surface( 133 | window_handle.clone(), 134 | SurfaceRendererConfiguration { 135 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT, 136 | formats: vec![TextureFormat::Rgba8Unorm, TextureFormat::Bgra8Unorm], 137 | width, 138 | height, 139 | present_mode: PresentMode::AutoVsync, 140 | desired_maximum_frame_latency: 2, 141 | alpha_mode: wgpu::CompositeAlphaMode::Auto, 142 | view_formats: vec![], 143 | }, 144 | Some(TextureConfiguration { 145 | usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, 146 | }), 147 | )) 148 | .expect("Error creating surface"); 149 | 150 | // Create vello::Renderer 151 | let renderer = VelloRenderer::new( 152 | render_surface.device(), 153 | RendererOptions { 154 | antialiasing_support: AaSupport::all(), 155 | use_cpu: false, 156 | num_init_threads: DEFAULT_THREADS, 157 | // TODO: add pipeline cache 158 | pipeline_cache: None, 159 | }, 160 | ) 161 | .unwrap(); 162 | 163 | // Resume custom paint sources 164 | let device_handle = &render_surface.device_handle; 165 | for source in self.custom_paint_sources.values_mut() { 166 | source.resume(device_handle) 167 | } 168 | 169 | // Set state to Active 170 | self.window_handle = Some(window_handle); 171 | self.render_state = RenderState::Active(ActiveRenderState { 172 | renderer, 173 | render_surface, 174 | }); 175 | } 176 | 177 | fn suspend(&mut self) { 178 | // Suspend custom paint sources 179 | for source in self.custom_paint_sources.values_mut() { 180 | source.suspend() 181 | } 182 | 183 | // Set state to Suspended 184 | self.render_state = RenderState::Suspended; 185 | } 186 | 187 | fn set_size(&mut self, width: u32, height: u32) { 188 | if let RenderState::Active(state) = &mut self.render_state { 189 | state.render_surface.resize(width, height); 190 | }; 191 | } 192 | 193 | fn render)>(&mut self, draw_fn: F) { 194 | let RenderState::Active(state) = &mut self.render_state else { 195 | return; 196 | }; 197 | 198 | let render_surface = &state.render_surface; 199 | 200 | debug_timer!(timer, feature = "log_frame_times"); 201 | 202 | // Regenerate the vello scene 203 | draw_fn(&mut VelloScenePainter { 204 | inner: &mut self.scene, 205 | renderer: Some(&mut state.renderer), 206 | custom_paint_sources: Some(&mut self.custom_paint_sources), 207 | }); 208 | timer.record_time("cmd"); 209 | 210 | state 211 | .renderer 212 | .render_to_texture( 213 | render_surface.device(), 214 | render_surface.queue(), 215 | &self.scene, 216 | &render_surface.target_texture_view(), 217 | &RenderParams { 218 | base_color: self.config.base_color, 219 | width: render_surface.config.width, 220 | height: render_surface.config.height, 221 | antialiasing_method: self.config.antialiasing_method, 222 | }, 223 | ) 224 | .expect("failed to render to texture"); 225 | timer.record_time("render"); 226 | 227 | render_surface.maybe_blit_and_present(); 228 | timer.record_time("present"); 229 | 230 | render_surface.device().poll(wgpu::PollType::Wait).unwrap(); 231 | 232 | timer.record_time("wait"); 233 | timer.print_times("vello: "); 234 | 235 | // static COUNTER: AtomicU64 = AtomicU64::new(0); 236 | // println!("FRAME {}", COUNTER.fetch_add(1, atomic::Ordering::Relaxed)); 237 | 238 | // Empty the Vello scene (memory optimisation) 239 | self.scene.reset(); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /examples/winit/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyrender::{NullWindowRenderer, PaintScene, WindowRenderer}; 2 | use anyrender_skia::SkiaWindowRenderer; 3 | use anyrender_vello::VelloWindowRenderer; 4 | use anyrender_vello_cpu::{PixelsWindowRenderer, SoftbufferWindowRenderer, VelloCpuImageRenderer}; 5 | use anyrender_vello_hybrid::VelloHybridWindowRenderer; 6 | use kurbo::{Affine, Circle, Point, Rect, Stroke}; 7 | use peniko::{Color, Fill}; 8 | use std::sync::Arc; 9 | use winit::{ 10 | application::ApplicationHandler, 11 | event::{ElementState, KeyEvent, WindowEvent}, 12 | event_loop::{ActiveEventLoop, EventLoop}, 13 | keyboard::{Key, NamedKey}, 14 | window::{Window, WindowId}, 15 | }; 16 | 17 | struct App { 18 | render_state: RenderState, 19 | width: u32, 20 | height: u32, 21 | } 22 | 23 | type VelloCpuSBWindowRenderer = SoftbufferWindowRenderer; 24 | type VelloCpuWindowRenderer = PixelsWindowRenderer; 25 | 26 | type InitialBackend = SkiaWindowRenderer; 27 | // type InitialBackend = VelloWindowRenderer; 28 | // type InitialBackend = VelloHybridWindowRenderer; 29 | // type InitialBackend = VelloCpuWindowRenderer; 30 | // type InitialBackend = VelloCpuSBWindowRenderer; 31 | // type InitialBackend = NullWindowRenderer; 32 | 33 | enum Renderer { 34 | Gpu(Box), 35 | Hybrid(Box), 36 | Cpu(Box), 37 | CpuSoftbuffer(Box), 38 | Skia(Box), 39 | Null(NullWindowRenderer), 40 | } 41 | impl From for Renderer { 42 | fn from(renderer: VelloWindowRenderer) -> Self { 43 | Self::Gpu(Box::new(renderer)) 44 | } 45 | } 46 | impl From for Renderer { 47 | fn from(renderer: VelloHybridWindowRenderer) -> Self { 48 | Self::Hybrid(Box::new(renderer)) 49 | } 50 | } 51 | impl From for Renderer { 52 | fn from(renderer: VelloCpuWindowRenderer) -> Self { 53 | Self::Cpu(Box::new(renderer)) 54 | } 55 | } 56 | impl From for Renderer { 57 | fn from(renderer: VelloCpuSBWindowRenderer) -> Self { 58 | Self::CpuSoftbuffer(Box::new(renderer)) 59 | } 60 | } 61 | impl From for Renderer { 62 | fn from(renderer: SkiaWindowRenderer) -> Self { 63 | Self::Skia(Box::new(renderer)) 64 | } 65 | } 66 | impl From for Renderer { 67 | fn from(renderer: NullWindowRenderer) -> Self { 68 | Self::Null(renderer) 69 | } 70 | } 71 | 72 | impl Renderer { 73 | fn is_active(&self) -> bool { 74 | match self { 75 | Renderer::Gpu(r) => r.is_active(), 76 | Renderer::Hybrid(r) => r.is_active(), 77 | Renderer::Cpu(r) => r.is_active(), 78 | Renderer::CpuSoftbuffer(r) => r.is_active(), 79 | Renderer::Null(r) => r.is_active(), 80 | Renderer::Skia(r) => r.is_active(), 81 | } 82 | } 83 | 84 | fn set_size(&mut self, w: u32, h: u32) { 85 | match self { 86 | Renderer::Gpu(r) => r.set_size(w, h), 87 | Renderer::Hybrid(r) => r.set_size(w, h), 88 | Renderer::Cpu(r) => r.set_size(w, h), 89 | Renderer::CpuSoftbuffer(r) => r.set_size(w, h), 90 | Renderer::Null(r) => r.set_size(w, h), 91 | Renderer::Skia(r) => r.set_size(w, h), 92 | } 93 | } 94 | } 95 | 96 | enum RenderState { 97 | Active { 98 | window: Arc, 99 | renderer: Renderer, 100 | }, 101 | Suspended(Option>), 102 | } 103 | 104 | impl App { 105 | fn request_redraw(&mut self) { 106 | let window = match &self.render_state { 107 | RenderState::Active { window, renderer } => { 108 | if renderer.is_active() { 109 | Some(window) 110 | } else { 111 | None 112 | } 113 | } 114 | RenderState::Suspended(_) => None, 115 | }; 116 | 117 | if let Some(window) = window { 118 | window.request_redraw(); 119 | } 120 | } 121 | 122 | fn draw_scene(scene: &mut T, color: Color) { 123 | scene.fill( 124 | Fill::NonZero, 125 | Affine::IDENTITY, 126 | Color::WHITE, 127 | None, 128 | &Rect::new(0.0, 0.0, 50.0, 50.0), 129 | ); 130 | scene.stroke( 131 | &Stroke::new(2.0), 132 | Affine::IDENTITY, 133 | Color::BLACK, 134 | None, 135 | &Rect::new(5.0, 5.0, 35.0, 35.0), 136 | ); 137 | scene.fill( 138 | Fill::NonZero, 139 | Affine::IDENTITY, 140 | color, 141 | None, 142 | &Circle::new(Point::new(20.0, 20.0), 10.0), 143 | ); 144 | } 145 | 146 | fn set_backend>( 147 | &mut self, 148 | mut renderer: R, 149 | event_loop: &ActiveEventLoop, 150 | ) { 151 | let mut window = match &self.render_state { 152 | RenderState::Active { window, .. } => Some(window.clone()), 153 | RenderState::Suspended(cached_window) => cached_window.clone(), 154 | }; 155 | let window = window.take().unwrap_or_else(|| { 156 | let attr = Window::default_attributes() 157 | .with_inner_size(winit::dpi::LogicalSize::new(self.width, self.height)) 158 | .with_resizable(true) 159 | .with_title("anyrender + winit demo") 160 | .with_visible(true) 161 | .with_active(true); 162 | Arc::new(event_loop.create_window(attr).unwrap()) 163 | }); 164 | 165 | renderer.resume(window.clone(), self.width, self.height); 166 | let renderer = renderer.into(); 167 | self.render_state = RenderState::Active { window, renderer }; 168 | self.request_redraw(); 169 | } 170 | } 171 | 172 | impl ApplicationHandler for App { 173 | fn suspended(&mut self, _event_loop: &ActiveEventLoop) { 174 | if let RenderState::Active { window, .. } = &self.render_state { 175 | self.render_state = RenderState::Suspended(Some(window.clone())); 176 | } 177 | } 178 | 179 | fn resumed(&mut self, event_loop: &ActiveEventLoop) { 180 | self.set_backend(InitialBackend::new(), event_loop); 181 | } 182 | 183 | fn window_event( 184 | &mut self, 185 | event_loop: &ActiveEventLoop, 186 | window_id: WindowId, 187 | event: WindowEvent, 188 | ) { 189 | let RenderState::Active { window, renderer } = &mut self.render_state else { 190 | return; 191 | }; 192 | 193 | if window.id() != window_id { 194 | return; 195 | } 196 | 197 | match event { 198 | WindowEvent::CloseRequested => event_loop.exit(), 199 | WindowEvent::Resized(physical_size) => { 200 | self.width = physical_size.width; 201 | self.height = physical_size.height; 202 | renderer.set_size(self.width, self.height); 203 | self.request_redraw(); 204 | } 205 | WindowEvent::RedrawRequested => match renderer { 206 | Renderer::Skia(r) => { 207 | r.render(|p| App::draw_scene(p, Color::from_rgb8(128, 128, 128))) 208 | } 209 | Renderer::Gpu(r) => r.render(|p| App::draw_scene(p, Color::from_rgb8(255, 0, 0))), 210 | Renderer::Hybrid(r) => r.render(|p| App::draw_scene(p, Color::from_rgb8(0, 0, 0))), 211 | Renderer::Cpu(r) => r.render(|p| App::draw_scene(p, Color::from_rgb8(0, 255, 0))), 212 | Renderer::CpuSoftbuffer(r) => { 213 | r.render(|p| App::draw_scene(p, Color::from_rgb8(0, 0, 255))) 214 | } 215 | Renderer::Null(r) => r.render(|p| App::draw_scene(p, Color::from_rgb8(0, 0, 0))), 216 | }, 217 | WindowEvent::KeyboardInput { 218 | event: 219 | KeyEvent { 220 | logical_key: Key::Named(NamedKey::Space), 221 | state: ElementState::Pressed, 222 | .. 223 | }, 224 | .. 225 | } => match renderer { 226 | Renderer::Cpu(_) | Renderer::CpuSoftbuffer(_) => { 227 | self.set_backend(VelloHybridWindowRenderer::new(), event_loop); 228 | } 229 | Renderer::Hybrid(_) => { 230 | self.set_backend(VelloWindowRenderer::new(), event_loop); 231 | } 232 | Renderer::Gpu(_) => { 233 | self.set_backend(SkiaWindowRenderer::new(), event_loop); 234 | } 235 | Renderer::Skia(_) => { 236 | self.set_backend(NullWindowRenderer::new(), event_loop); 237 | } 238 | Renderer::Null(_) => { 239 | self.set_backend(VelloCpuWindowRenderer::new(), event_loop); 240 | } 241 | }, 242 | _ => {} 243 | } 244 | } 245 | } 246 | 247 | fn main() { 248 | let mut app = App { 249 | render_state: RenderState::Suspended(None), 250 | width: 800, 251 | height: 600, 252 | }; 253 | 254 | let event_loop = EventLoop::new().unwrap(); 255 | event_loop 256 | .run_app(&mut app) 257 | .expect("Couldn't run event loop"); 258 | } 259 | -------------------------------------------------------------------------------- /crates/anyrender_vello_hybrid/src/window_renderer.rs: -------------------------------------------------------------------------------- 1 | use anyrender::{WindowHandle, WindowRenderer}; 2 | use debug_timer::debug_timer; 3 | use rustc_hash::FxHashMap; 4 | use std::sync::{ 5 | Arc, 6 | // atomic::{AtomicU64}, 7 | }; 8 | use vello_common::paint::ImageId; 9 | use vello_hybrid::{ 10 | RenderSettings, RenderSize, RenderTargetConfig, Renderer as VelloHybridRenderer, 11 | Scene as VelloHybridScene, 12 | }; 13 | use wgpu::{ 14 | CommandEncoderDescriptor, Features, Limits, PresentMode, TextureFormat, TextureViewDescriptor, 15 | }; 16 | use wgpu_context::{DeviceHandle, SurfaceRenderer, SurfaceRendererConfiguration, WGPUContext}; 17 | 18 | use crate::{VelloHybridScenePainter, scene::ImageManager}; 19 | // use crate::CustomPaintSource; 20 | 21 | // static PAINT_SOURCE_ID: AtomicU64 = AtomicU64::new(0); 22 | 23 | // Simple struct to hold the state of the renderer 24 | struct ActiveRenderState { 25 | renderer: VelloHybridRenderer, 26 | render_surface: SurfaceRenderer<'static>, 27 | } 28 | 29 | #[allow(clippy::large_enum_variant)] 30 | enum RenderState { 31 | Active(ActiveRenderState), 32 | Suspended, 33 | } 34 | 35 | impl RenderState { 36 | fn current_device_handle(&self) -> Option<&DeviceHandle> { 37 | let RenderState::Active(state) = self else { 38 | return None; 39 | }; 40 | Some(&state.render_surface.device_handle) 41 | } 42 | } 43 | 44 | #[derive(Clone, Default)] 45 | pub struct VelloHybridRendererOptions { 46 | pub features: Option, 47 | pub limits: Option, 48 | pub render_settings: RenderSettings, 49 | } 50 | 51 | pub struct VelloHybridWindowRenderer { 52 | // The fields MUST be in this order, so that the surface is dropped before the window 53 | // Window is cached even when suspended so that it can be reused when the app is resumed after being suspended 54 | render_state: RenderState, 55 | window_handle: Option>, 56 | 57 | // Vello 58 | wgpu_context: WGPUContext, 59 | scene: VelloHybridScene, 60 | config: VelloHybridRendererOptions, 61 | // custom_paint_sources: FxHashMap>, 62 | cached_images: FxHashMap, 63 | } 64 | impl VelloHybridWindowRenderer { 65 | #[allow(clippy::new_without_default)] 66 | pub fn new() -> Self { 67 | Self::with_options(VelloHybridRendererOptions::default()) 68 | } 69 | 70 | pub fn with_options(config: VelloHybridRendererOptions) -> Self { 71 | let features = config.features.unwrap_or_default() 72 | | Features::CLEAR_TEXTURE 73 | | Features::PIPELINE_CACHE; 74 | let render_settings = config.render_settings; 75 | Self { 76 | wgpu_context: WGPUContext::with_features_and_limits( 77 | Some(features), 78 | config.limits.clone(), 79 | ), 80 | config, 81 | render_state: RenderState::Suspended, 82 | window_handle: None, 83 | scene: VelloHybridScene::new_with(0, 0, render_settings), 84 | // custom_paint_sources: FxHashMap::default(), 85 | cached_images: FxHashMap::default(), 86 | } 87 | } 88 | 89 | pub fn current_device_handle(&self) -> Option<&DeviceHandle> { 90 | self.render_state.current_device_handle() 91 | } 92 | 93 | // pub fn register_custom_paint_source(&mut self, mut source: Box) -> u64 { 94 | // if let Some(device_handle) = self.render_state.current_device_handle() { 95 | // source.resume(device_handle); 96 | // } 97 | // let id = PAINT_SOURCE_ID.fetch_add(1, atomic::Ordering::SeqCst); 98 | // self.custom_paint_sources.insert(id, source); 99 | 100 | // id 101 | // } 102 | 103 | // pub fn unregister_custom_paint_source(&mut self, id: u64) { 104 | // if let Some(mut source) = self.custom_paint_sources.remove(&id) { 105 | // source.suspend(); 106 | // drop(source); 107 | // } 108 | // } 109 | } 110 | 111 | impl WindowRenderer for VelloHybridWindowRenderer { 112 | type ScenePainter<'a> 113 | = VelloHybridScenePainter<'a> 114 | where 115 | Self: 'a; 116 | 117 | fn is_active(&self) -> bool { 118 | matches!(self.render_state, RenderState::Active(_)) 119 | } 120 | 121 | fn resume(&mut self, window_handle: Arc, width: u32, height: u32) { 122 | // Create wgpu_context::SurfaceRenderer 123 | let render_surface = pollster::block_on(self.wgpu_context.create_surface( 124 | window_handle.clone(), 125 | SurfaceRendererConfiguration { 126 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT, 127 | formats: vec![ 128 | /*TextureFormat::Rgba8Unorm, */ TextureFormat::Bgra8Unorm, 129 | ], 130 | width, 131 | height, 132 | present_mode: PresentMode::AutoVsync, 133 | desired_maximum_frame_latency: 2, 134 | alpha_mode: wgpu::CompositeAlphaMode::Auto, 135 | view_formats: vec![], 136 | }, 137 | None, 138 | // Some(TextureConfiguration { 139 | // usage: TextureUsages::STORAGE_BINDING | TextureUsages::TEXTURE_BINDING, 140 | // }), 141 | )) 142 | .expect("Error creating surface"); 143 | 144 | // Create vello::Renderer 145 | let renderer = VelloHybridRenderer::new( 146 | render_surface.device(), 147 | &RenderTargetConfig { 148 | // TODO: Make configurable? 149 | format: TextureFormat::Bgra8Unorm, 150 | width, 151 | height, 152 | }, 153 | ); 154 | 155 | // Resume custom paint sources 156 | // let device_handle = &render_surface.device_handle; 157 | // for source in self.custom_paint_sources.values_mut() { 158 | // source.resume(device_handle) 159 | // } 160 | 161 | // Set state to Active 162 | self.window_handle = Some(window_handle); 163 | self.render_state = RenderState::Active(ActiveRenderState { 164 | renderer, 165 | render_surface, 166 | }); 167 | } 168 | 169 | fn suspend(&mut self) { 170 | // Suspend custom paint sources 171 | // for source in self.custom_paint_sources.values_mut() { 172 | // source.suspend() 173 | // } 174 | 175 | // Set state to Suspended 176 | self.render_state = RenderState::Suspended; 177 | } 178 | 179 | fn set_size(&mut self, width: u32, height: u32) { 180 | if width as u16 != self.scene.width() || height as u16 != self.scene.height() { 181 | self.scene = VelloHybridScene::new_with( 182 | width as u16, 183 | height as u16, 184 | self.config.render_settings, 185 | ); 186 | if let RenderState::Active(state) = &mut self.render_state { 187 | state.render_surface.resize(width, height); 188 | }; 189 | } 190 | } 191 | 192 | fn render)>(&mut self, draw_fn: F) { 193 | let RenderState::Active(state) = &mut self.render_state else { 194 | return; 195 | }; 196 | 197 | let render_surface = &state.render_surface; 198 | 199 | debug_timer!(timer, feature = "log_frame_times"); 200 | 201 | let mut encoder = 202 | render_surface 203 | .device() 204 | .create_command_encoder(&CommandEncoderDescriptor { 205 | label: Some("Render scene"), 206 | }); 207 | 208 | let image_manager = ImageManager { 209 | renderer: &mut state.renderer, 210 | device: render_surface.device(), 211 | queue: render_surface.queue(), 212 | encoder: &mut encoder, 213 | cache: &mut self.cached_images, 214 | }; 215 | 216 | // Regenerate the vello scene 217 | draw_fn(&mut VelloHybridScenePainter { 218 | scene: &mut self.scene, 219 | image_manager: Some(image_manager), 220 | }); 221 | timer.record_time("cmd"); 222 | 223 | let surface_texture = render_surface.current_surface_texture(); 224 | let texture_view = surface_texture 225 | .texture 226 | .create_view(&TextureViewDescriptor::default()); 227 | 228 | state 229 | .renderer 230 | .render( 231 | &self.scene, 232 | render_surface.device(), 233 | render_surface.queue(), 234 | &mut encoder, 235 | &RenderSize { 236 | width: render_surface.config.width, 237 | height: render_surface.config.height, 238 | }, 239 | &texture_view, 240 | ) 241 | .expect("failed to render to texture"); 242 | render_surface.queue().submit([encoder.finish()]); 243 | timer.record_time("render"); 244 | 245 | drop(texture_view); 246 | drop(surface_texture); 247 | 248 | render_surface.maybe_blit_and_present(); 249 | timer.record_time("present"); 250 | 251 | render_surface.device().poll(wgpu::PollType::Wait).unwrap(); 252 | 253 | timer.record_time("wait"); 254 | timer.print_times("vello_hybrid: "); 255 | 256 | // static COUNTER: AtomicU64 = AtomicU64::new(0); 257 | // println!("FRAME {}", COUNTER.fetch_add(1, atomic::Ordering::Relaxed)); 258 | 259 | // Empty the Vello scene (memory optimisation) 260 | self.scene.reset(); 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /crates/anyrender_svg/src/util.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 the Vello Authors 2 | // SPDX-License-Identifier: Apache-2.0 OR MIT 3 | 4 | use anyrender::{Paint, PaintScene}; 5 | use kurbo::{Affine, BezPath, Point, Rect, Stroke}; 6 | use peniko::color::{self, DynamicColor}; 7 | use peniko::{Color, Fill, Mix}; 8 | 9 | #[cfg(feature = "image")] 10 | use peniko::{Blob, ImageBrush}; 11 | 12 | pub(crate) fn to_affine(ts: &usvg::Transform) -> Affine { 13 | let usvg::Transform { 14 | sx, 15 | kx, 16 | ky, 17 | sy, 18 | tx, 19 | ty, 20 | } = ts; 21 | Affine::new([sx, ky, kx, sy, tx, ty].map(|&x| f64::from(x))) 22 | } 23 | 24 | pub(crate) fn to_stroke(stroke: &usvg::Stroke) -> Stroke { 25 | let mut conv_stroke = Stroke::new(stroke.width().get() as f64) 26 | .with_caps(match stroke.linecap() { 27 | usvg::LineCap::Butt => kurbo::Cap::Butt, 28 | usvg::LineCap::Round => kurbo::Cap::Round, 29 | usvg::LineCap::Square => kurbo::Cap::Square, 30 | }) 31 | .with_join(match stroke.linejoin() { 32 | usvg::LineJoin::Miter | usvg::LineJoin::MiterClip => kurbo::Join::Miter, 33 | usvg::LineJoin::Round => kurbo::Join::Round, 34 | usvg::LineJoin::Bevel => kurbo::Join::Bevel, 35 | }) 36 | .with_miter_limit(stroke.miterlimit().get() as f64); 37 | if let Some(dash_array) = stroke.dasharray().as_ref() { 38 | conv_stroke = conv_stroke.with_dashes( 39 | stroke.dashoffset() as f64, 40 | dash_array.iter().map(|x| *x as f64), 41 | ); 42 | } 43 | conv_stroke 44 | } 45 | 46 | pub(crate) fn to_mix(blend_mode: usvg::BlendMode, is_fully_opaque: bool) -> Mix { 47 | match blend_mode { 48 | usvg::BlendMode::Normal => { 49 | if is_fully_opaque { 50 | #[allow(deprecated)] 51 | Mix::Clip 52 | } else { 53 | Mix::Normal 54 | } 55 | } 56 | usvg::BlendMode::Multiply => Mix::Multiply, 57 | usvg::BlendMode::Screen => Mix::Screen, 58 | usvg::BlendMode::Overlay => Mix::Overlay, 59 | usvg::BlendMode::Darken => Mix::Darken, 60 | usvg::BlendMode::Lighten => Mix::Lighten, 61 | usvg::BlendMode::ColorDodge => Mix::ColorDodge, 62 | usvg::BlendMode::ColorBurn => Mix::ColorBurn, 63 | usvg::BlendMode::HardLight => Mix::HardLight, 64 | usvg::BlendMode::SoftLight => Mix::SoftLight, 65 | usvg::BlendMode::Difference => Mix::Difference, 66 | usvg::BlendMode::Exclusion => Mix::Exclusion, 67 | usvg::BlendMode::Hue => Mix::Hue, 68 | usvg::BlendMode::Saturation => Mix::Saturation, 69 | usvg::BlendMode::Color => Mix::Color, 70 | usvg::BlendMode::Luminosity => Mix::Luminosity, 71 | } 72 | } 73 | 74 | pub(crate) fn to_bez_path(path: &usvg::Path) -> BezPath { 75 | let mut local_path = BezPath::new(); 76 | // The semantics of SVG paths don't line up with `BezPath`; we 77 | // must manually track initial points 78 | let mut just_closed = false; 79 | let mut most_recent_initial = (0., 0.); 80 | for elt in path.data().segments() { 81 | match elt { 82 | usvg::tiny_skia_path::PathSegment::MoveTo(p) => { 83 | if std::mem::take(&mut just_closed) { 84 | local_path.move_to(most_recent_initial); 85 | } 86 | most_recent_initial = (p.x.into(), p.y.into()); 87 | local_path.move_to(most_recent_initial); 88 | } 89 | usvg::tiny_skia_path::PathSegment::LineTo(p) => { 90 | if std::mem::take(&mut just_closed) { 91 | local_path.move_to(most_recent_initial); 92 | } 93 | local_path.line_to(Point::new(p.x as f64, p.y as f64)); 94 | } 95 | usvg::tiny_skia_path::PathSegment::QuadTo(p1, p2) => { 96 | if std::mem::take(&mut just_closed) { 97 | local_path.move_to(most_recent_initial); 98 | } 99 | local_path.quad_to( 100 | Point::new(p1.x as f64, p1.y as f64), 101 | Point::new(p2.x as f64, p2.y as f64), 102 | ); 103 | } 104 | usvg::tiny_skia_path::PathSegment::CubicTo(p1, p2, p3) => { 105 | if std::mem::take(&mut just_closed) { 106 | local_path.move_to(most_recent_initial); 107 | } 108 | local_path.curve_to( 109 | Point::new(p1.x as f64, p1.y as f64), 110 | Point::new(p2.x as f64, p2.y as f64), 111 | Point::new(p3.x as f64, p3.y as f64), 112 | ); 113 | } 114 | usvg::tiny_skia_path::PathSegment::Close => { 115 | just_closed = true; 116 | local_path.close_path(); 117 | } 118 | } 119 | } 120 | 121 | local_path 122 | } 123 | 124 | #[cfg(feature = "image")] 125 | pub(crate) fn into_image(image: image::ImageBuffer, Vec>) -> ImageBrush { 126 | use peniko::ImageData; 127 | 128 | let (width, height) = (image.width(), image.height()); 129 | let image_data: Vec = image.into_vec(); 130 | ImageBrush::new(ImageData { 131 | data: Blob::new(std::sync::Arc::new(image_data)), 132 | format: peniko::ImageFormat::Rgba8, 133 | alpha_type: peniko::ImageAlphaType::Alpha, 134 | width, 135 | height, 136 | }) 137 | } 138 | 139 | pub(crate) fn to_brush(paint: &usvg::Paint, opacity: usvg::Opacity) -> Option<(Paint, Affine)> { 140 | match paint { 141 | usvg::Paint::Color(color) => Some(( 142 | Paint::Solid(Color::from_rgba8( 143 | color.red, 144 | color.green, 145 | color.blue, 146 | opacity.to_u8(), 147 | )), 148 | Affine::IDENTITY, 149 | )), 150 | usvg::Paint::LinearGradient(gr) => { 151 | let stops: Vec = gr 152 | .stops() 153 | .iter() 154 | .map(|stop| peniko::ColorStop { 155 | offset: stop.offset().get(), 156 | color: DynamicColor::from_alpha_color(Color::from_rgba8( 157 | stop.color().red, 158 | stop.color().green, 159 | stop.color().blue, 160 | (stop.opacity() * opacity).to_u8(), 161 | )), 162 | }) 163 | .collect(); 164 | let start = Point::new(gr.x1() as f64, gr.y1() as f64); 165 | let end = Point::new(gr.x2() as f64, gr.y2() as f64); 166 | let arr = [ 167 | gr.transform().sx, 168 | gr.transform().ky, 169 | gr.transform().kx, 170 | gr.transform().sy, 171 | gr.transform().tx, 172 | gr.transform().ty, 173 | ] 174 | .map(f64::from); 175 | let transform = Affine::new(arr); 176 | let gradient = peniko::Gradient::new_linear(start, end).with_stops(stops.as_slice()); 177 | Some((Paint::Gradient(gradient), transform)) 178 | } 179 | usvg::Paint::RadialGradient(gr) => { 180 | let stops: Vec = gr 181 | .stops() 182 | .iter() 183 | .map(|stop| peniko::ColorStop { 184 | offset: stop.offset().get(), 185 | color: DynamicColor::from_alpha_color(Color::from_rgba8( 186 | stop.color().red, 187 | stop.color().green, 188 | stop.color().blue, 189 | (stop.opacity() * opacity).to_u8(), 190 | )), 191 | }) 192 | .collect(); 193 | 194 | let start_center = Point::new(gr.cx() as f64, gr.cy() as f64); 195 | let end_center = Point::new(gr.fx() as f64, gr.fy() as f64); 196 | let start_radius = 0_f32; 197 | let end_radius = gr.r().get(); 198 | let arr = [ 199 | gr.transform().sx, 200 | gr.transform().ky, 201 | gr.transform().kx, 202 | gr.transform().sy, 203 | gr.transform().tx, 204 | gr.transform().ty, 205 | ] 206 | .map(f64::from); 207 | let transform = Affine::new(arr); 208 | let gradient = peniko::Gradient::new_two_point_radial( 209 | start_center, 210 | start_radius, 211 | end_center, 212 | end_radius, 213 | ) 214 | .with_stops(stops.as_slice()); 215 | Some((Paint::Gradient(gradient), transform)) 216 | } 217 | usvg::Paint::Pattern(_) => None, 218 | } 219 | } 220 | 221 | /// Error handler function for [`super::append_tree_with`] which draws a transparent red box 222 | /// instead of unsupported SVG features 223 | pub(crate) fn default_error_handler(scene: &mut S, node: &usvg::Node) { 224 | let bb = node.bounding_box(); 225 | let rect = Rect { 226 | x0: bb.left() as f64, 227 | y0: bb.top() as f64, 228 | x1: bb.right() as f64, 229 | y1: bb.bottom() as f64, 230 | }; 231 | scene.fill( 232 | Fill::NonZero, 233 | Affine::IDENTITY, 234 | color::palette::css::RED.multiply_alpha(0.5), 235 | None, 236 | &rect, 237 | ); 238 | } 239 | 240 | #[cfg(feature = "image")] 241 | pub(crate) fn decode_raw_raster_image( 242 | img: &usvg::ImageKind, 243 | ) -> Result { 244 | // All `image::ImageFormat` variants exist even if the feature in the image crate is disabled, 245 | // but `image::load_from_memory_with_format` will fail with an Unsupported error if the 246 | // image crate feature flag is disabled. So we don't need any of our own feature handling here. 247 | let (data, format) = match img { 248 | usvg::ImageKind::JPEG(data) => (data, image::ImageFormat::Jpeg), 249 | usvg::ImageKind::PNG(data) => (data, image::ImageFormat::Png), 250 | usvg::ImageKind::GIF(data) => (data, image::ImageFormat::Gif), 251 | usvg::ImageKind::WEBP(data) => (data, image::ImageFormat::WebP), 252 | usvg::ImageKind::SVG(_) => unreachable!(), 253 | }; 254 | 255 | let dyn_image = image::load_from_memory_with_format(data, format)?; 256 | Ok(dyn_image.into_rgba8()) 257 | } 258 | -------------------------------------------------------------------------------- /examples/bunnymark/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyrender::{PaintScene, WindowRenderer}; 2 | use anyrender_skia::SkiaWindowRenderer; 3 | use anyrender_vello::VelloWindowRenderer; 4 | use anyrender_vello_cpu::VelloCpuWindowRenderer; 5 | use anyrender_vello_hybrid::VelloHybridWindowRenderer; 6 | use bunny::BunnyManager; 7 | use kurbo::{Affine, Circle, Point, Rect, Stroke}; 8 | use peniko::{Color, Fill}; 9 | use std::sync::Arc; 10 | use winit::{ 11 | application::ApplicationHandler, 12 | event::{ElementState, KeyEvent, WindowEvent}, 13 | event_loop::{ActiveEventLoop, EventLoop}, 14 | keyboard::{Key, NamedKey, SmolStr}, 15 | window::{Window, WindowId}, 16 | }; 17 | 18 | mod bunny; 19 | 20 | const SKY_BLUE: Color = Color::from_rgb8(135, 206, 235); 21 | 22 | struct App { 23 | render_state: RenderState, 24 | bunny_manager: BunnyManager, 25 | logical_width: u32, 26 | logical_height: u32, 27 | scale_factor: f64, 28 | } 29 | 30 | enum Renderer { 31 | Skia(Box), 32 | Gpu(Box), 33 | Hybrid(Box), 34 | Cpu(Box), 35 | } 36 | 37 | impl Renderer { 38 | fn is_active(&self) -> bool { 39 | match self { 40 | Renderer::Skia(r) => r.is_active(), 41 | Renderer::Gpu(r) => r.is_active(), 42 | Renderer::Hybrid(r) => r.is_active(), 43 | Renderer::Cpu(r) => r.is_active(), 44 | } 45 | } 46 | 47 | fn set_size(&mut self, w: u32, h: u32) { 48 | match self { 49 | Renderer::Skia(r) => r.set_size(w, h), 50 | Renderer::Gpu(r) => r.set_size(w, h), 51 | Renderer::Hybrid(r) => r.set_size(w, h), 52 | Renderer::Cpu(r) => r.set_size(w, h), 53 | } 54 | } 55 | } 56 | 57 | enum RenderState { 58 | Active { 59 | window: Arc, 60 | renderer: Renderer, 61 | }, 62 | Suspended(Option>), 63 | } 64 | 65 | impl App { 66 | fn request_redraw(&mut self) { 67 | let window = match &self.render_state { 68 | RenderState::Active { window, renderer } => { 69 | if renderer.is_active() { 70 | Some(window) 71 | } else { 72 | None 73 | } 74 | } 75 | RenderState::Suspended(_) => None, 76 | }; 77 | 78 | if let Some(window) = window { 79 | window.request_redraw(); 80 | } 81 | } 82 | 83 | fn draw_scene( 84 | scene: &mut T, 85 | width: u32, 86 | height: u32, 87 | scale_factor: f64, 88 | bunny_manager: &BunnyManager, 89 | color: Color, 90 | ) { 91 | // Draw background 92 | scene.fill( 93 | Fill::NonZero, 94 | Affine::scale(scale_factor), 95 | SKY_BLUE, 96 | None, 97 | &Rect::new(0.0, 0.0, width as f64, height as f64), 98 | ); 99 | 100 | // Draw small circle indicating renderer in use 101 | scene.stroke( 102 | &Stroke::new(2.0), 103 | Affine::scale(scale_factor), 104 | Color::BLACK, 105 | None, 106 | &Rect::new(5.0, 5.0, 35.0, 35.0), 107 | ); 108 | scene.fill( 109 | Fill::NonZero, 110 | Affine::scale(scale_factor), 111 | color, 112 | None, 113 | &Circle::new(Point::new(20.0, 20.0), 10.0), 114 | ); 115 | 116 | // Draw bunnies 117 | bunny_manager.draw(scene, scale_factor); 118 | } 119 | 120 | fn set_backend( 121 | &mut self, 122 | mut renderer: R, 123 | event_loop: &ActiveEventLoop, 124 | f: impl FnOnce(R) -> Renderer, 125 | ) { 126 | let mut window = match &self.render_state { 127 | RenderState::Active { window, .. } => Some(window.clone()), 128 | RenderState::Suspended(cached_window) => cached_window.clone(), 129 | }; 130 | let window = window.take().unwrap_or_else(|| { 131 | let attr = Window::default_attributes() 132 | .with_inner_size(winit::dpi::LogicalSize::new( 133 | self.logical_width, 134 | self.logical_height, 135 | )) 136 | .with_resizable(true) 137 | .with_title("anyrender + winit demo") 138 | .with_visible(true) 139 | .with_active(true); 140 | Arc::new(event_loop.create_window(attr).unwrap()) 141 | }); 142 | self.scale_factor = window.scale_factor(); 143 | 144 | let physical_size = window.inner_size(); 145 | renderer.resume(window.clone(), physical_size.width, physical_size.height); 146 | self.render_state = RenderState::Active { 147 | window, 148 | renderer: f(renderer), 149 | }; 150 | self.request_redraw(); 151 | } 152 | } 153 | 154 | impl ApplicationHandler for App { 155 | fn suspended(&mut self, _event_loop: &ActiveEventLoop) { 156 | if let RenderState::Active { window, .. } = &self.render_state { 157 | self.render_state = RenderState::Suspended(Some(window.clone())); 158 | } 159 | } 160 | 161 | fn resumed(&mut self, event_loop: &ActiveEventLoop) { 162 | self.set_backend(SkiaWindowRenderer::new(), event_loop, |r| { 163 | Renderer::Skia(Box::new(r)) 164 | }); 165 | } 166 | 167 | fn window_event( 168 | &mut self, 169 | event_loop: &ActiveEventLoop, 170 | window_id: WindowId, 171 | event: WindowEvent, 172 | ) { 173 | let RenderState::Active { window, renderer } = &mut self.render_state else { 174 | return; 175 | }; 176 | 177 | if window.id() != window_id { 178 | return; 179 | } 180 | 181 | match event { 182 | WindowEvent::CloseRequested => event_loop.exit(), 183 | WindowEvent::Resized(physical_size) => { 184 | let logical_size = physical_size.to_logical(self.scale_factor); 185 | self.logical_width = logical_size.width; 186 | self.logical_height = logical_size.height; 187 | renderer.set_size(physical_size.width, physical_size.height); 188 | self.request_redraw(); 189 | } 190 | WindowEvent::ScaleFactorChanged { scale_factor, .. } => { 191 | self.scale_factor = scale_factor; 192 | if let RenderState::Active { window, renderer } = &mut self.render_state { 193 | let physical_size = window.inner_size(); 194 | let logical_size = physical_size.to_logical(scale_factor); 195 | self.logical_width = logical_size.width; 196 | self.logical_height = logical_size.height; 197 | renderer.set_size(physical_size.width, physical_size.height); 198 | }; 199 | } 200 | WindowEvent::RedrawRequested => { 201 | self.bunny_manager 202 | .update(self.logical_width as f64, self.logical_height as f64); 203 | let renderer_name = match renderer { 204 | Renderer::Skia(_) => "skia", 205 | Renderer::Gpu(_) => "vello", 206 | Renderer::Hybrid(_) => "vello_hybrid", 207 | Renderer::Cpu(_) => "vello_cpu", 208 | }; 209 | print!( 210 | "[{}] [{} bunnies] ", 211 | renderer_name, 212 | self.bunny_manager.count(), 213 | ); 214 | match renderer { 215 | Renderer::Skia(r) => r.render(|scene_painter| { 216 | App::draw_scene( 217 | scene_painter, 218 | self.logical_width, 219 | self.logical_height, 220 | self.scale_factor, 221 | &self.bunny_manager, 222 | Color::from_rgb8(255, 0, 0), 223 | ); 224 | }), 225 | Renderer::Gpu(r) => r.render(|scene_painter| { 226 | App::draw_scene( 227 | scene_painter, 228 | self.logical_width, 229 | self.logical_height, 230 | self.scale_factor, 231 | &self.bunny_manager, 232 | Color::from_rgb8(255, 0, 0), 233 | ); 234 | }), 235 | Renderer::Hybrid(r) => r.render(|scene_painter| { 236 | App::draw_scene( 237 | scene_painter, 238 | self.logical_width, 239 | self.logical_height, 240 | self.scale_factor, 241 | &self.bunny_manager, 242 | Color::from_rgb8(255, 0, 0), 243 | ); 244 | }), 245 | Renderer::Cpu(r) => r.render(|scene_painter| { 246 | App::draw_scene( 247 | scene_painter, 248 | self.logical_width, 249 | self.logical_height, 250 | self.scale_factor, 251 | &self.bunny_manager, 252 | Color::from_rgb8(0, 255, 0), 253 | ); 254 | }), 255 | } 256 | window.request_redraw(); 257 | } 258 | WindowEvent::MouseInput { state, .. } => { 259 | if state.is_pressed() { 260 | self.bunny_manager.add_bunnies(100); 261 | } 262 | } 263 | WindowEvent::KeyboardInput { 264 | event: 265 | KeyEvent { 266 | logical_key, 267 | state: ElementState::Pressed, 268 | .. 269 | }, 270 | .. 271 | } => { 272 | if logical_key == Key::Named(NamedKey::Space) { 273 | match renderer { 274 | Renderer::Cpu(_) => { 275 | self.set_backend(VelloHybridWindowRenderer::new(), event_loop, |r| { 276 | Renderer::Hybrid(Box::new(r)) 277 | }); 278 | } 279 | Renderer::Hybrid(_) => { 280 | self.set_backend(VelloWindowRenderer::new(), event_loop, |r| { 281 | Renderer::Gpu(Box::new(r)) 282 | }); 283 | } 284 | Renderer::Gpu(_) => { 285 | self.set_backend(SkiaWindowRenderer::new(), event_loop, |r| { 286 | Renderer::Skia(Box::new(r)) 287 | }); 288 | } 289 | Renderer::Skia(_) => { 290 | self.set_backend(VelloCpuWindowRenderer::new(), event_loop, |r| { 291 | Renderer::Cpu(Box::new(r)) 292 | }); 293 | } 294 | } 295 | } else if logical_key == Key::Character(SmolStr::new("r")) { 296 | self.bunny_manager.clear_bunnies(); 297 | } 298 | } 299 | _ => {} 300 | } 301 | } 302 | } 303 | 304 | fn main() { 305 | let mut app = App { 306 | render_state: RenderState::Suspended(None), 307 | bunny_manager: BunnyManager::new(1024.0, 1024.0), 308 | logical_width: 1024, 309 | logical_height: 1024, 310 | scale_factor: 1.0, 311 | }; 312 | 313 | let event_loop = EventLoop::new().unwrap(); 314 | event_loop 315 | .run_app(&mut app) 316 | .expect("Couldn't run event loop"); 317 | } 318 | -------------------------------------------------------------------------------- /crates/wgpu_context/src/surface_renderer.rs: -------------------------------------------------------------------------------- 1 | use crate::{DeviceHandle, WgpuContextError, util::create_texture}; 2 | use wgpu::{ 3 | CommandEncoderDescriptor, CompositeAlphaMode, Device, PresentMode, Queue, Surface, 4 | SurfaceConfiguration, SurfaceTexture, TextureFormat, TextureUsages, TextureView, 5 | TextureViewDescriptor, util::TextureBlitter, 6 | }; 7 | 8 | #[derive(Clone)] 9 | pub struct TextureConfiguration { 10 | pub usage: TextureUsages, 11 | } 12 | 13 | #[derive(Clone)] 14 | pub struct SurfaceRendererConfiguration { 15 | /// The usage of the swap chain. The only usage guaranteed to be supported is [`TextureUsages::RENDER_ATTACHMENT`]. 16 | pub usage: TextureUsages, 17 | /// The texture format of the swap chain. The only formats that are guaranteed are 18 | /// [`TextureFormat::Bgra8Unorm`] and [`TextureFormat::Bgra8UnormSrgb`]. 19 | pub formats: Vec, 20 | /// Width of the swap chain. Must be the same size as the surface, and nonzero. 21 | /// 22 | /// If this is not the same size as the underlying surface (e.g. if it is 23 | /// set once, and the window is later resized), the behaviour is defined 24 | /// but platform-specific, and may change in the future (currently macOS 25 | /// scales the surface, other platforms may do something else). 26 | pub width: u32, 27 | /// Height of the swap chain. Must be the same size as the surface, and nonzero. 28 | /// 29 | /// If this is not the same size as the underlying surface (e.g. if it is 30 | /// set once, and the window is later resized), the behaviour is defined 31 | /// but platform-specific, and may change in the future (currently macOS 32 | /// scales the surface, other platforms may do something else). 33 | pub height: u32, 34 | /// Presentation mode of the swap chain. Fifo is the only mode guaranteed to be supported. 35 | /// `FifoRelaxed`, `Immediate`, and `Mailbox` will crash if unsupported, while `AutoVsync` and 36 | /// `AutoNoVsync` will gracefully do a designed sets of fallbacks if their primary modes are 37 | /// unsupported. 38 | pub present_mode: PresentMode, 39 | /// Desired maximum number of frames that the presentation engine should queue in advance. 40 | /// 41 | /// This is a hint to the backend implementation and will always be clamped to the supported range. 42 | /// As a consequence, either the maximum frame latency is set directly on the swap chain, 43 | /// or waits on present are scheduled to avoid exceeding the maximum frame latency if supported, 44 | /// or the swap chain size is set to (max-latency + 1). 45 | /// 46 | /// Defaults to 2 when created via `Surface::get_default_config`. 47 | /// 48 | /// Typical values range from 3 to 1, but higher values are possible: 49 | /// * Choose 2 or higher for potentially smoother frame display, as it allows to be at least one frame 50 | /// to be queued up. This typically avoids starving the GPU's work queue. 51 | /// Higher values are useful for achieving a constant flow of frames to the display under varying load. 52 | /// * Choose 1 for low latency from frame recording to frame display. 53 | /// ⚠️ If the backend does not support waiting on present, this will cause the CPU to wait for the GPU 54 | /// to finish all work related to the previous frame when calling `Surface::get_current_texture`, 55 | /// causing CPU-GPU serialization (i.e. when `Surface::get_current_texture` returns, the GPU might be idle). 56 | /// It is currently not possible to query this. See . 57 | /// * A value of 0 is generally not supported and always clamped to a higher value. 58 | pub desired_maximum_frame_latency: u32, 59 | /// Specifies how the alpha channel of the textures should be handled during compositing. 60 | pub alpha_mode: CompositeAlphaMode, 61 | /// Specifies what view formats will be allowed when calling `Texture::create_view` on the texture returned by `Surface::get_current_texture`. 62 | /// 63 | /// View formats of the same format as the texture are always allowed. 64 | /// 65 | /// Note: currently, only the srgb-ness is allowed to change. (ex: `Rgba8Unorm` texture + `Rgba8UnormSrgb` view) 66 | pub view_formats: Vec, 67 | } 68 | 69 | struct IntermediateTextureStuff { 70 | pub config: TextureConfiguration, 71 | // TextureView for the intermediate Texture which we sometimes render to because compute shaders 72 | // cannot always render directly to surfaces. Since WGPU 26, the underlying Texture can be accessed 73 | // from the TextureView so we don't need to store both. 74 | pub texture_view: TextureView, 75 | // Blitter for blitting from the intermediate texture to the surface. 76 | pub blitter: TextureBlitter, 77 | } 78 | 79 | /// Combination of surface and its configuration. 80 | pub struct SurfaceRenderer<'s> { 81 | // The device and queue for rendering to the surface 82 | pub dev_id: usize, 83 | pub device_handle: DeviceHandle, 84 | 85 | // The surface and it's configuration 86 | pub surface: Surface<'s>, 87 | pub config: SurfaceConfiguration, 88 | 89 | intermediate_texture: Option>, 90 | } 91 | 92 | impl std::fmt::Debug for SurfaceRenderer<'_> { 93 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 94 | f.debug_struct("SurfaceRenderer") 95 | .field("dev_id", &self.dev_id) 96 | .field("surface_config", &self.config) 97 | .field("has_intermediate_texture", &true) 98 | .finish() 99 | } 100 | } 101 | 102 | impl<'s> SurfaceRenderer<'s> { 103 | /// Creates a new render surface for the specified window and dimensions. 104 | pub async fn new<'w>( 105 | surface: Surface<'w>, 106 | surface_renderer_config: SurfaceRendererConfiguration, 107 | intermediate_texture_config: Option, 108 | device_handle: DeviceHandle, 109 | dev_id: usize, 110 | ) -> Result, WgpuContextError> { 111 | // Convert SurfaceRendererConfiguration to SurfaceConfiguration. 112 | // The difference is that `format` is a Vec in SurfaceRendererConfiguration and a single value in SurfaceConfiguration 113 | let surface_config = SurfaceConfiguration { 114 | usage: surface_renderer_config.usage, 115 | format: surface 116 | .get_capabilities(&device_handle.adapter) 117 | .formats 118 | .into_iter() 119 | .find(|it| surface_renderer_config.formats.contains(it)) 120 | .ok_or(WgpuContextError::UnsupportedSurfaceFormat)?, 121 | width: surface_renderer_config.width, 122 | height: surface_renderer_config.height, 123 | present_mode: surface_renderer_config.present_mode, 124 | desired_maximum_frame_latency: surface_renderer_config.desired_maximum_frame_latency, 125 | alpha_mode: surface_renderer_config.alpha_mode, 126 | view_formats: surface_renderer_config.view_formats, 127 | }; 128 | 129 | let intermediate_texture = intermediate_texture_config.map(|texture_config| { 130 | Box::new(IntermediateTextureStuff { 131 | config: texture_config.clone(), 132 | texture_view: create_texture( 133 | surface_renderer_config.width, 134 | surface_renderer_config.height, 135 | TextureFormat::Rgba8Unorm, 136 | texture_config.usage, 137 | &device_handle.device, 138 | ), 139 | blitter: TextureBlitter::new(&device_handle.device, surface_config.format), 140 | }) 141 | }); 142 | 143 | let surface = SurfaceRenderer { 144 | dev_id, 145 | device_handle, 146 | surface, 147 | config: surface_config, 148 | intermediate_texture, 149 | }; 150 | surface.configure(); 151 | Ok(surface) 152 | } 153 | 154 | pub fn device(&self) -> &Device { 155 | &self.device_handle.device 156 | } 157 | 158 | pub fn queue(&self) -> &Queue { 159 | &self.device_handle.queue 160 | } 161 | 162 | /// Resizes the surface to the new dimensions. 163 | pub fn resize(&mut self, width: u32, height: u32) { 164 | // TODO: Use clever resize semantics to avoid thrashing the memory allocator during a resize 165 | // especially important on metal. 166 | if let Some(intermediate_texture_stuff) = &mut self.intermediate_texture { 167 | intermediate_texture_stuff.texture_view = create_texture( 168 | width, 169 | height, 170 | TextureFormat::Rgba8Unorm, 171 | intermediate_texture_stuff.config.usage, 172 | &self.device_handle.device, 173 | ); 174 | } 175 | self.config.width = width; 176 | self.config.height = height; 177 | self.configure(); 178 | } 179 | 180 | pub fn set_present_mode(&mut self, present_mode: wgpu::PresentMode) { 181 | self.config.present_mode = present_mode; 182 | self.configure(); 183 | } 184 | 185 | fn configure(&self) { 186 | self.surface 187 | .configure(&self.device_handle.device, &self.config); 188 | } 189 | 190 | pub fn current_surface_texture(&self) -> SurfaceTexture { 191 | self.surface 192 | .get_current_texture() 193 | .expect("failed to get surface texture") 194 | } 195 | 196 | pub fn target_texture_view(&self) -> TextureView { 197 | match &self.intermediate_texture { 198 | Some(intermediate_texture) => intermediate_texture.texture_view.clone(), 199 | None => { 200 | let surface_texture = self 201 | .surface 202 | .get_current_texture() 203 | .expect("failed to get surface texture"); 204 | surface_texture 205 | .texture 206 | .create_view(&TextureViewDescriptor::default()) 207 | } 208 | } 209 | } 210 | 211 | pub fn maybe_blit_and_present(&self) { 212 | let surface_texture = self 213 | .surface 214 | .get_current_texture() 215 | .expect("failed to get surface texture"); 216 | 217 | if let Some(its) = &self.intermediate_texture { 218 | self.blit_from_intermediate_texture_to_surface(&surface_texture, its); 219 | } 220 | 221 | surface_texture.present(); 222 | } 223 | 224 | /// Blit from the intermediate texture to the surface texture 225 | fn blit_from_intermediate_texture_to_surface( 226 | &self, 227 | surface_texture: &SurfaceTexture, 228 | intermediate_texture_stuff: &IntermediateTextureStuff, 229 | ) { 230 | // TODO: verify that handling of SurfaceError::Outdated is no longer required 231 | // 232 | // let surface_texture = match state.surface.surface.get_current_texture() { 233 | // Ok(surface) => surface, 234 | // // When resizing too aggresively, the surface can get outdated (another resize) before being rendered into 235 | // Err(SurfaceError::Outdated) => return, 236 | // Err(_) => panic!("failed to get surface texture"), 237 | // }; 238 | 239 | // Perform the copy 240 | // (TODO: Does it improve throughput to acquire the surface after the previous texture render has happened?) 241 | let mut encoder = 242 | self.device_handle 243 | .device 244 | .create_command_encoder(&CommandEncoderDescriptor { 245 | label: Some("Surface Blit"), 246 | }); 247 | 248 | intermediate_texture_stuff.blitter.copy( 249 | &self.device_handle.device, 250 | &mut encoder, 251 | &intermediate_texture_stuff.texture_view, 252 | &surface_texture 253 | .texture 254 | .create_view(&TextureViewDescriptor::default()), 255 | ); 256 | self.device_handle.queue.submit([encoder.finish()]); 257 | } 258 | } 259 | --------------------------------------------------------------------------------