| // Copyright 2022 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| // |
| // Also licensed under MIT license, at your choice. |
| |
| #![warn(clippy::doc_markdown, clippy::semicolon_if_nothing_returned)] |
| |
| mod cpu_dispatch; |
| mod cpu_shader; |
| #[cfg(all(feature = "debug_layers", feature = "wgpu"))] |
| mod debug; |
| mod engine; |
| mod render; |
| mod scene; |
| mod shaders; |
| #[cfg(feature = "wgpu")] |
| mod wgpu_engine; |
| |
| /// Styling and composition primitives. |
| pub use peniko; |
| /// 2D geometry, with a focus on curves. |
| pub use peniko::kurbo; |
| |
| #[doc(hidden)] |
| pub use skrifa; |
| |
| pub mod glyph; |
| |
| #[cfg(feature = "wgpu")] |
| pub mod util; |
| |
| pub use render::Render; |
| pub use scene::{DrawGlyphs, Scene}; |
| #[cfg(feature = "wgpu")] |
| pub use util::block_on_wgpu; |
| |
| pub use engine::{ |
| BindType, BufProxy, Command, Id, ImageFormat, ImageProxy, Recording, ResourceProxy, ShaderId, |
| }; |
| pub use shaders::FullShaders; |
| #[cfg(feature = "wgpu")] |
| use wgpu_engine::{ExternalResource, WgpuEngine}; |
| |
| #[cfg(all(feature = "debug_layers", feature = "wgpu"))] |
| pub use debug::DebugLayers; |
| /// Temporary export, used in `with_winit` for stats |
| pub use vello_encoding::BumpAllocators; |
| #[cfg(feature = "wgpu")] |
| use wgpu::{Device, Queue, SurfaceTexture, TextureFormat, TextureView}; |
| #[cfg(feature = "wgpu-profiler")] |
| use wgpu_profiler::{GpuProfiler, GpuProfilerSettings}; |
| |
| /// Catch-all error type. |
| pub type Error = Box<dyn std::error::Error>; |
| |
| /// Specialization of `Result` for our catch-all error type. |
| pub type Result<T> = std::result::Result<T, Error>; |
| |
| /// Represents the antialiasing method to use during a render pass. |
| #[derive(Copy, Clone, PartialEq, Eq)] |
| pub enum AaConfig { |
| Area, |
| Msaa8, |
| Msaa16, |
| } |
| |
| /// Represents the set of antialiasing configurations to enable during pipeline creation. |
| pub struct AaSupport { |
| pub area: bool, |
| pub msaa8: bool, |
| pub msaa16: bool, |
| } |
| |
| impl AaSupport { |
| pub fn all() -> Self { |
| Self { |
| area: true, |
| msaa8: true, |
| msaa16: true, |
| } |
| } |
| |
| pub fn area_only() -> Self { |
| Self { |
| area: true, |
| msaa8: false, |
| msaa16: false, |
| } |
| } |
| } |
| |
| /// Renders a scene into a texture or surface. |
| #[cfg(feature = "wgpu")] |
| pub struct Renderer { |
| options: RendererOptions, |
| engine: WgpuEngine, |
| shaders: FullShaders, |
| blit: Option<BlitPipeline>, |
| #[cfg(feature = "debug_layers")] |
| debug: Option<debug::DebugRenderer>, |
| target: Option<TargetTexture>, |
| #[cfg(feature = "wgpu-profiler")] |
| profiler: GpuProfiler, |
| #[cfg(feature = "wgpu-profiler")] |
| pub profile_result: Option<Vec<wgpu_profiler::GpuTimerScopeResult>>, |
| } |
| |
| /// Parameters used in a single render that are configurable by the client. |
| pub struct RenderParams { |
| /// The background color applied to the target. This value is only applicable to the full |
| /// pipeline. |
| pub base_color: peniko::Color, |
| |
| /// Dimensions of the rasterization target |
| pub width: u32, |
| pub height: u32, |
| |
| /// The anti-aliasing algorithm. The selected algorithm must have been initialized while |
| /// constructing the `Renderer`. |
| pub antialiasing_method: AaConfig, |
| |
| #[cfg(feature = "debug_layers")] |
| /// Options for debug layer rendering. |
| pub debug: DebugLayers, |
| } |
| |
| #[cfg(feature = "wgpu")] |
| pub struct RendererOptions { |
| /// The format of the texture used for surfaces with this renderer/device |
| /// If None, the renderer cannot be used with surfaces |
| pub surface_format: Option<TextureFormat>, |
| |
| /// If true, run all stages up to fine rasterization on the CPU. |
| // TODO: Consider evolving this so that the CPU stages can be configured dynamically via |
| // `RenderParams`. |
| pub use_cpu: bool, |
| |
| /// Represents the enabled set of AA configurations. This will be used to determine which |
| /// pipeline permutations should be compiled at startup. |
| pub antialiasing_support: AaSupport, |
| } |
| |
| struct RenderResult { |
| bump: Option<BumpAllocators>, |
| #[cfg(feature = "debug_layers")] |
| captured: Option<render::CapturedBuffers>, |
| } |
| |
| #[cfg(feature = "wgpu")] |
| impl Renderer { |
| /// Creates a new renderer for the specified device. |
| pub fn new(device: &Device, options: RendererOptions) -> Result<Self> { |
| let mut engine = WgpuEngine::new(options.use_cpu); |
| let shaders = shaders::full_shaders(device, &mut engine, &options)?; |
| let blit = options |
| .surface_format |
| .map(|surface_format| BlitPipeline::new(device, surface_format, &mut engine)); |
| #[cfg(feature = "debug_layers")] |
| let debug = options |
| .surface_format |
| .map(|surface_format| debug::DebugRenderer::new(device, surface_format, &mut engine)); |
| |
| Ok(Self { |
| options, |
| engine, |
| shaders, |
| blit, |
| #[cfg(feature = "debug_layers")] |
| debug, |
| target: None, |
| // Use 3 pending frames |
| #[cfg(feature = "wgpu-profiler")] |
| profiler: GpuProfiler::new(GpuProfilerSettings { |
| ..Default::default() |
| })?, |
| #[cfg(feature = "wgpu-profiler")] |
| profile_result: None, |
| }) |
| } |
| |
| /// Renders a scene to the target texture. |
| /// |
| /// The texture is assumed to be of the specified dimensions and have been created with |
| /// the [`wgpu::TextureFormat::Rgba8Unorm`] format and the [`wgpu::TextureUsages::STORAGE_BINDING`] |
| /// flag set. |
| pub fn render_to_texture( |
| &mut self, |
| device: &Device, |
| queue: &Queue, |
| scene: &Scene, |
| texture: &TextureView, |
| params: &RenderParams, |
| ) -> Result<()> { |
| let (recording, target) = render::render_full(scene, &self.shaders, params); |
| let external_resources = [ExternalResource::Image( |
| *target.as_image().unwrap(), |
| texture, |
| )]; |
| self.engine.run_recording( |
| device, |
| queue, |
| &recording, |
| &external_resources, |
| "render_to_texture", |
| #[cfg(feature = "wgpu-profiler")] |
| &mut self.profiler, |
| )?; |
| Ok(()) |
| } |
| |
| /// Renders a scene to the target surface. |
| /// |
| /// This renders to an intermediate texture and then runs a render pass to blit to the |
| /// specified surface texture. |
| /// |
| /// The surface is assumed to be of the specified dimensions and have been configured with |
| /// the same format passed in the constructing [`RendererOptions`]' `surface_format`. |
| /// Panics if `surface_format` was `None` |
| pub fn render_to_surface( |
| &mut self, |
| device: &Device, |
| queue: &Queue, |
| scene: &Scene, |
| surface: &SurfaceTexture, |
| params: &RenderParams, |
| ) -> Result<()> { |
| let width = params.width; |
| let height = params.height; |
| let mut target = self |
| .target |
| .take() |
| .unwrap_or_else(|| TargetTexture::new(device, width, height)); |
| // TODO: implement clever resizing semantics here to avoid thrashing the memory allocator |
| // during resize, specifically on metal. |
| if target.width != width || target.height != height { |
| target = TargetTexture::new(device, width, height); |
| } |
| self.render_to_texture(device, queue, scene, &target.view, params)?; |
| let blit = self |
| .blit |
| .as_ref() |
| .expect("renderer should have configured surface_format to use on a surface"); |
| let mut recording = Recording::default(); |
| let target_proxy = ImageProxy::new(width, height, ImageFormat::from_wgpu(target.format)); |
| let surface_proxy = ImageProxy::new( |
| width, |
| height, |
| ImageFormat::from_wgpu(surface.texture.format()), |
| ); |
| recording.draw( |
| blit.0, |
| 1, |
| 6, |
| None, |
| [ResourceProxy::Image(target_proxy)], |
| surface_proxy, |
| Some([0., 0., 0., 0.]), |
| ); |
| |
| let surface_view = surface |
| .texture |
| .create_view(&wgpu::TextureViewDescriptor::default()); |
| let external_resources = [ |
| ExternalResource::Image(target_proxy, &target.view), |
| ExternalResource::Image(surface_proxy, &surface_view), |
| ]; |
| self.engine.run_recording( |
| device, |
| queue, |
| &recording, |
| &external_resources, |
| "blit (render_to_surface_async)", |
| #[cfg(feature = "wgpu-profiler")] |
| &mut self.profiler, |
| )?; |
| |
| #[cfg(feature = "wgpu-profiler")] |
| { |
| let mut encoder = |
| device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); |
| self.profiler.resolve_queries(&mut encoder); |
| queue.submit(Some(encoder.finish())); |
| self.profiler.end_frame().unwrap(); |
| if let Some(result) = self |
| .profiler |
| .process_finished_frame(queue.get_timestamp_period()) |
| { |
| self.profile_result = Some(result); |
| } |
| } |
| |
| self.target = Some(target); |
| Ok(()) |
| } |
| |
| /// Reload the shaders. This should only be used during `vello` development |
| #[cfg(feature = "hot_reload")] |
| pub async fn reload_shaders(&mut self, device: &Device) -> Result<()> { |
| device.push_error_scope(wgpu::ErrorFilter::Validation); |
| let mut engine = WgpuEngine::new(self.options.use_cpu); |
| let shaders = shaders::full_shaders(device, &mut engine, &self.options)?; |
| let blit = self |
| .options |
| .surface_format |
| .map(|surface_format| BlitPipeline::new(device, surface_format, &mut engine)); |
| #[cfg(feature = "debug_layers")] |
| let debug = self |
| .options |
| .surface_format |
| .map(|format| debug::DebugRenderer::new(device, format, &mut engine)); |
| let error = device.pop_error_scope().await; |
| if let Some(error) = error { |
| return Err(error.into()); |
| } |
| self.engine = engine; |
| self.shaders = shaders; |
| self.blit = blit; |
| #[cfg(feature = "debug_layers")] |
| { |
| self.debug = debug; |
| } |
| Ok(()) |
| } |
| |
| /// Renders a scene to the target texture. |
| /// |
| /// The texture is assumed to be of the specified dimensions and have been created with |
| /// the [`wgpu::TextureFormat::Rgba8Unorm`] format and the [`wgpu::TextureUsages::STORAGE_BINDING`] |
| /// flag set. |
| /// |
| /// The return value is the value of the `BumpAllocators` in this rendering, which is currently used |
| /// for debug output. |
| /// |
| /// This return type is not stable, and will likely be changed when a more principled way to access |
| /// relevant statistics is implemented |
| pub async fn render_to_texture_async( |
| &mut self, |
| device: &Device, |
| queue: &Queue, |
| scene: &Scene, |
| texture: &TextureView, |
| params: &RenderParams, |
| ) -> Result<Option<BumpAllocators>> { |
| let result = self |
| .render_to_texture_async_internal(device, queue, scene, texture, params) |
| .await?; |
| #[cfg(feature = "debug_layers")] |
| { |
| // TODO: it would be better to improve buffer ownership tracking so that it's not |
| // necessary to submit a whole new Recording to free the captured buffers. |
| if let Some(captured) = result.captured { |
| let mut recording = Recording::default(); |
| // TODO: this sucks. better to release everything in a helper |
| self.engine.free_download(captured.lines); |
| captured.release_buffers(&mut recording); |
| self.engine.run_recording( |
| device, |
| queue, |
| &recording, |
| &[], |
| "free memory", |
| #[cfg(feature = "wgpu-profiler")] |
| &mut self.profiler, |
| )?; |
| } |
| } |
| Ok(result.bump) |
| } |
| |
| async fn render_to_texture_async_internal( |
| &mut self, |
| device: &Device, |
| queue: &Queue, |
| scene: &Scene, |
| texture: &TextureView, |
| params: &RenderParams, |
| ) -> Result<RenderResult> { |
| let mut render = Render::new(); |
| let encoding = scene.encoding(); |
| // TODO: turn this on; the download feature interacts with CPU dispatch. |
| // Currently this is always enabled when the `debug_layers` setting is enabled as the bump |
| // counts are used for debug visualiation. |
| let robust = cfg!(feature = "debug_layers"); |
| let recording = render.render_encoding_coarse(encoding, &self.shaders, params, robust); |
| let target = render.out_image(); |
| let bump_buf = render.bump_buf(); |
| #[cfg(feature = "debug_layers")] |
| let captured = render.take_captured_buffers(); |
| self.engine.run_recording( |
| device, |
| queue, |
| &recording, |
| &[], |
| "t_async_coarse", |
| #[cfg(feature = "wgpu-profiler")] |
| &mut self.profiler, |
| )?; |
| |
| let mut bump: Option<BumpAllocators> = None; |
| if let Some(bump_buf) = self.engine.get_download(bump_buf) { |
| let buf_slice = bump_buf.slice(..); |
| let (sender, receiver) = futures_intrusive::channel::shared::oneshot_channel(); |
| buf_slice.map_async(wgpu::MapMode::Read, move |v| sender.send(v).unwrap()); |
| if let Some(recv_result) = receiver.receive().await { |
| recv_result?; |
| } else { |
| return Err("channel was closed".into()); |
| } |
| let mapped = buf_slice.get_mapped_range(); |
| bump = Some(bytemuck::pod_read_unaligned(&mapped)); |
| } |
| // TODO: apply logic to determine whether we need to rerun coarse, and also |
| // allocate the blend stack as needed. |
| self.engine.free_download(bump_buf); |
| // Maybe clear to reuse allocation? |
| let mut recording = Recording::default(); |
| render.record_fine(&self.shaders, &mut recording); |
| let external_resources = [ExternalResource::Image(target, texture)]; |
| self.engine.run_recording( |
| device, |
| queue, |
| &recording, |
| &external_resources, |
| "t_async_fine", |
| #[cfg(feature = "wgpu-profiler")] |
| &mut self.profiler, |
| )?; |
| Ok(RenderResult { |
| bump, |
| #[cfg(feature = "debug_layers")] |
| captured, |
| }) |
| } |
| |
| /// See [`Self::render_to_surface`] |
| pub async fn render_to_surface_async( |
| &mut self, |
| device: &Device, |
| queue: &Queue, |
| scene: &Scene, |
| surface: &SurfaceTexture, |
| params: &RenderParams, |
| ) -> Result<Option<BumpAllocators>> { |
| let width = params.width; |
| let height = params.height; |
| let mut target = self |
| .target |
| .take() |
| .unwrap_or_else(|| TargetTexture::new(device, width, height)); |
| // TODO: implement clever resizing semantics here to avoid thrashing the memory allocator |
| // during resize, specifically on metal. |
| if target.width != width || target.height != height { |
| target = TargetTexture::new(device, width, height); |
| } |
| let result = self |
| .render_to_texture_async_internal(device, queue, scene, &target.view, params) |
| .await?; |
| let blit = self |
| .blit |
| .as_ref() |
| .expect("renderer should have configured surface_format to use on a surface"); |
| let mut recording = Recording::default(); |
| let target_proxy = ImageProxy::new(width, height, ImageFormat::from_wgpu(target.format)); |
| let surface_proxy = ImageProxy::new( |
| width, |
| height, |
| ImageFormat::from_wgpu(surface.texture.format()), |
| ); |
| recording.draw( |
| blit.0, |
| 1, |
| 6, |
| None, |
| [ResourceProxy::Image(target_proxy)], |
| surface_proxy, |
| Some([0., 0., 0., 0.]), |
| ); |
| |
| #[cfg(feature = "debug_layers")] |
| { |
| if let Some(captured) = result.captured { |
| let debug = self |
| .debug |
| .as_ref() |
| .expect("renderer should have configured surface_format to use on a surface"); |
| let bump = result.bump.as_ref().unwrap(); |
| // TODO: We could avoid this download if `DebugLayers::VALIDATION` is unset. |
| if let Ok(downloads) = DebugDownloads::map(&mut self.engine, &captured, bump).await |
| { |
| debug.render( |
| &mut recording, |
| surface_proxy, |
| &captured, |
| bump, |
| ¶ms, |
| &downloads, |
| ); |
| } |
| |
| // TODO: this sucks. better to release everything in a helper |
| // TODO: it would be much better to have a way to safely destroy a buffer. |
| self.engine.free_download(captured.lines); |
| captured.release_buffers(&mut recording); |
| } |
| } |
| |
| let surface_view = surface |
| .texture |
| .create_view(&wgpu::TextureViewDescriptor::default()); |
| let external_resources = [ |
| ExternalResource::Image(target_proxy, &target.view), |
| ExternalResource::Image(surface_proxy, &surface_view), |
| ]; |
| self.engine.run_recording( |
| device, |
| queue, |
| &recording, |
| &external_resources, |
| "blit (render_to_surface_async)", |
| #[cfg(feature = "wgpu-profiler")] |
| &mut self.profiler, |
| )?; |
| |
| #[cfg(feature = "wgpu-profiler")] |
| { |
| let mut encoder = |
| device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); |
| self.profiler.resolve_queries(&mut encoder); |
| queue.submit(Some(encoder.finish())); |
| self.profiler.end_frame().unwrap(); |
| if let Some(result) = self |
| .profiler |
| .process_finished_frame(queue.get_timestamp_period()) |
| { |
| self.profile_result = Some(result); |
| } |
| } |
| |
| self.target = Some(target); |
| Ok(result.bump) |
| } |
| } |
| |
| #[cfg(feature = "wgpu")] |
| struct TargetTexture { |
| view: TextureView, |
| width: u32, |
| height: u32, |
| format: wgpu::TextureFormat, |
| } |
| |
| #[cfg(feature = "wgpu")] |
| impl TargetTexture { |
| pub fn new(device: &Device, width: u32, height: u32) -> Self { |
| let format = wgpu::TextureFormat::Rgba8Unorm; |
| let texture = device.create_texture(&wgpu::TextureDescriptor { |
| label: None, |
| size: wgpu::Extent3d { |
| width, |
| height, |
| depth_or_array_layers: 1, |
| }, |
| mip_level_count: 1, |
| sample_count: 1, |
| dimension: wgpu::TextureDimension::D2, |
| usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::TEXTURE_BINDING, |
| format, |
| view_formats: &[], |
| }); |
| let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); |
| Self { |
| view, |
| width, |
| height, |
| format, |
| } |
| } |
| } |
| |
| #[cfg(feature = "wgpu")] |
| struct BlitPipeline(ShaderId); |
| |
| #[cfg(feature = "wgpu")] |
| impl BlitPipeline { |
| fn new(device: &Device, format: TextureFormat, engine: &mut WgpuEngine) -> Self { |
| const SHADERS: &str = r#" |
| @vertex |
| fn vs_main(@builtin(vertex_index) ix: u32) -> @builtin(position) vec4<f32> { |
| // Generate a full screen quad in NDCs |
| var vertex = vec2(-1.0, 1.0); |
| switch ix { |
| case 1u: { |
| vertex = vec2(-1.0, -1.0); |
| } |
| case 2u, 4u: { |
| vertex = vec2(1.0, -1.0); |
| } |
| case 5u: { |
| vertex = vec2(1.0, 1.0); |
| } |
| default: {} |
| } |
| return vec4(vertex, 0.0, 1.0); |
| } |
| |
| @group(0) @binding(0) |
| var fine_output: texture_2d<f32>; |
| |
| @fragment |
| fn fs_main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> { |
| let rgba_sep = textureLoad(fine_output, vec2<i32>(pos.xy), 0); |
| return vec4(rgba_sep.rgb * rgba_sep.a, rgba_sep.a); |
| } |
| "#; |
| let module = device.create_shader_module(wgpu::ShaderModuleDescriptor { |
| label: Some("blit shaders"), |
| source: wgpu::ShaderSource::Wgsl(SHADERS.into()), |
| }); |
| let shader_id = engine.add_render_shader( |
| device, |
| "blit", |
| &module, |
| "vs_main", |
| "fs_main", |
| wgpu::PrimitiveTopology::TriangleList, |
| wgpu::ColorTargetState { |
| format, |
| blend: None, |
| write_mask: wgpu::ColorWrites::ALL, |
| }, |
| None, |
| &[( |
| BindType::ImageRead(ImageFormat::from_wgpu(format)), |
| wgpu::ShaderStages::FRAGMENT, |
| )], |
| ); |
| Self(shader_id) |
| } |
| } |
| |
| #[cfg(all(feature = "debug_layers", feature = "wgpu"))] |
| pub(crate) struct DebugDownloads<'a> { |
| pub lines: wgpu::BufferSlice<'a>, |
| } |
| |
| #[cfg(all(feature = "debug_layers", feature = "wgpu"))] |
| impl<'a> DebugDownloads<'a> { |
| pub async fn map( |
| engine: &'a WgpuEngine, |
| captured: &render::CapturedBuffers, |
| bump: &BumpAllocators, |
| ) -> Result<DebugDownloads<'a>> { |
| use vello_encoding::LineSoup; |
| |
| let Some(lines_buf) = engine.get_download(captured.lines) else { |
| return Err("could not download LineSoup buffer".into()); |
| }; |
| |
| if bump.lines == 0 { |
| return Err("bump indicates 0 LineSoup".into()); |
| } |
| |
| let lines = lines_buf.slice(..bump.lines as u64 * std::mem::size_of::<LineSoup>() as u64); |
| let (sender, receiver) = futures_intrusive::channel::shared::oneshot_channel(); |
| lines.map_async(wgpu::MapMode::Read, move |v| sender.send(v).unwrap()); |
| |
| if let Some(recv_result) = receiver.receive().await { |
| recv_result?; |
| } else { |
| return Err("channel was closed".into()); |
| } |
| Ok(Self { lines }) |
| } |
| } |