blob: 81bb357534a4e40664b06dfa14de45df5351edad [file] [edit]
// Copyright 2022 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Vello is a 2d graphics rendering engine written in Rust, using [`wgpu`].
//! It efficiently draws large 2d scenes with interactive or near-interactive performance.
//!
//! ![image](https://github.com/linebender/vello/assets/8573618/cc2b742e-2135-4b70-8051-c49aeddb5d19)
//!
//!
//! ## Motivation
//!
//! Vello is meant to fill the same place in the graphics stack as other vector graphics renderers like [Skia](https://skia.org/), [Cairo](https://www.cairographics.org/), and its predecessor project [Piet](https://www.cairographics.org/).
//! On a basic level, that means it provides tools to render shapes, images, gradients, texts, etc, using a PostScript-inspired API, the same that powers SVG files and [the browser `<canvas>` element](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D).
//!
//! Vello's selling point is that it gets better performance than other renderers by better leveraging the GPU.
//! In traditional PostScript-style renderers, some steps of the render process like sorting and clipping either need to be handled in the CPU or done through the use of intermediary textures.
//! Vello avoids this by using prefix-scan algorithms to parallelize work that usually needs to happen in sequence, so that work can be offloaded to the GPU with minimal use of temporary buffers.
//!
//! This means that Vello needs a GPU with support for compute shaders to run.
//!
//!
//! ## Getting started
//!
//! Vello is meant to be integrated deep in UI render stacks.
//! While drawing in a Vello [`Scene`] is easy, actually rendering that scene to a surface setting up a wgpu context, which is a non-trivial task.
//!
//! To use Vello as the renderer for your PDF reader / GUI toolkit / etc, your code will have to look roughly like this:
//!
//! ```ignore
//! // Initialize wgpu and get handles
//! let (width, height) = ...;
//! let device: wgpu::Device = ...;
//! let queue: wgpu::Queue = ...;
//! let mut renderer = Renderer::new(
//! &device,
//! RendererOptions {
//! use_cpu: false,
//! antialiasing_support: vello::AaSupport::all(),
//! num_init_threads: NonZeroUsize::new(1),
//! },
//! ).expect("Failed to create renderer");
//!
//! // Create scene and draw stuff in it
//! let mut scene = vello::Scene::new();
//! scene.fill(
//! vello::peniko::Fill::NonZero,
//! vello::Affine::IDENTITY,
//! vello::Color::from_rgb8(242, 140, 168),
//! None,
//! &vello::Circle::new((420.0, 200.0), 120.0),
//! );
//!
//! // Draw more stuff
//! scene.push_layer(...);
//! scene.fill(...);
//! scene.stroke(...);
//! scene.pop_layer(...);
//!
//! let texture = device.create_texture(&...);
//! // Render to a wgpu Texture
//! renderer
//! .render_to_texture(
//! &device,
//! &queue,
//! &scene,
//! &texture,
//! &vello::RenderParams {
//! base_color: palette::css::BLACK, // Background color
//! width,
//! height,
//! antialiasing_method: AaConfig::Msaa16,
//! },
//! )
//! .expect("Failed to render to a texture");
//! // Do things with surface texture, such as blitting it to the Surface using
//! // wgpu::util::TextureBlitter.
//! ```
//!
//! See the [`examples/`](https://github.com/linebender/vello/tree/main/examples) folder to see how that code integrates with frameworks like winit.
// LINEBENDER LINT SET - lib.rs - v2
// See https://linebender.org/wiki/canonical-lints/
// These lints aren't included in Cargo.toml because they
// shouldn't apply to examples and tests
#![warn(unused_crate_dependencies)]
#![warn(clippy::print_stdout, clippy::print_stderr)]
// Targeting e.g. 32-bit means structs containing usize can give false positives for 64-bit.
#![cfg_attr(target_pointer_width = "64", warn(clippy::trivially_copy_pass_by_ref))]
// END LINEBENDER LINT SET
#![cfg_attr(docsrs, feature(doc_cfg))]
// The following lints are part of the Linebender standard set,
// but resolving them has been deferred for now.
// Feel free to send a PR that solves one or more of these.
// Need to allow instead of expect until Rust 1.83 https://github.com/rust-lang/rust/pull/130025
#![allow(missing_docs, reason = "We have many as-yet undocumented items.")]
#![expect(
missing_debug_implementations,
clippy::cast_possible_truncation,
clippy::missing_assert_message,
reason = "Deferred"
)]
#![allow(
clippy::todo,
unreachable_pub,
unnameable_types,
reason = "Deferred, only apply in some feature sets so not expect"
)]
mod debug;
mod recording;
mod render;
mod scene;
mod shaders;
#[cfg(feature = "wgpu")]
pub mod util;
#[cfg(feature = "wgpu")]
mod wgpu_engine;
pub mod low_level {
//! Utilities which can be used to create an alternative Vello renderer to [`Renderer`][crate::Renderer].
//!
//! These APIs have not been carefully designed, and might not be powerful enough for this use case.
pub use crate::debug::DebugLayers;
pub use crate::recording::{
BindType, BufferProxy, Command, ImageFormat, ImageProxy, Recording, ResourceId,
ResourceProxy, ShaderId,
};
pub use crate::render::Render;
pub use crate::shaders::FullShaders;
/// Temporary export, used in `with_winit` for stats
pub use vello_encoding::BumpAllocators;
}
/// Styling and composition primitives.
pub use peniko;
/// 2D geometry, with a focus on curves.
pub use peniko::kurbo;
#[cfg(feature = "wgpu")]
use peniko::ImageData;
#[cfg(feature = "wgpu")]
pub use wgpu;
pub use scene::{DrawGlyphs, Scene};
pub use vello_encoding::{Glyph, NormalizedCoord};
use low_level::ShaderId;
#[cfg(feature = "wgpu")]
use low_level::{BumpAllocators, FullShaders, Recording, Render};
use thiserror::Error;
#[cfg(feature = "wgpu")]
use debug::DebugLayers;
#[cfg(feature = "wgpu")]
use vello_encoding::Resolver;
#[cfg(feature = "wgpu")]
use wgpu_engine::{ExternalResource, WgpuEngine};
#[cfg(feature = "wgpu")]
use std::{num::NonZeroUsize, sync::atomic::AtomicBool};
#[cfg(feature = "wgpu")]
use wgpu::{Device, Queue, TextureView};
#[cfg(all(feature = "wgpu", feature = "wgpu-profiler"))]
use wgpu_profiler::{GpuProfiler, GpuProfilerSettings};
/// Represents the anti-aliasing method to use during a render pass.
///
/// Can be configured for a render operation by setting [`RenderParams::antialiasing_method`].
/// Each value of this can only be used if the corresponding field on [`AaSupport`] was used.
///
/// This can be converted into an `AaSupport` using [`Iterator::collect`],
/// as `AaSupport` implements `FromIterator`.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum AaConfig {
/// Area anti-aliasing, where the alpha value for a pixel is computed from integrating
/// the winding number over its square area.
///
/// This technique produces very accurate values when the shape has winding number of 0 or 1
/// everywhere, but can result in conflation artifacts otherwise.
/// It generally has better performance than the multi-sampling methods.
///
/// Can only be used if [enabled][AaSupport::area] for the `Renderer`.
Area,
/// 8x Multisampling
///
/// Can only be used if [enabled][AaSupport::msaa8] for the `Renderer`.
Msaa8,
/// 16x Multisampling
///
/// Can only be used if [enabled][AaSupport::msaa16] for the `Renderer`.
Msaa16,
}
/// Represents the set of anti-aliasing configurations to enable during pipeline creation.
///
/// This is configured at `Renderer` creation time ([`Renderer::new`]) by setting
/// [`RendererOptions::antialiasing_support`].
///
/// This can be created from a set of `AaConfig` using [`Iterator::collect`],
/// as `AaSupport` implements `FromIterator`.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct AaSupport {
/// Support [`AaConfig::Area`].
pub area: bool,
/// Support [`AaConfig::Msaa8`].
pub msaa8: bool,
/// Support [`AaConfig::Msaa16`].
pub msaa16: bool,
}
impl AaSupport {
/// Support every anti-aliasing method.
///
/// This might increase startup time, as more shader variations must be compiled.
pub fn all() -> Self {
Self {
area: true,
msaa8: true,
msaa16: true,
}
}
/// Support only [`AaConfig::Area`].
///
/// This should be the default choice for most users.
pub fn area_only() -> Self {
Self {
area: true,
msaa8: false,
msaa16: false,
}
}
}
impl FromIterator<AaConfig> for AaSupport {
fn from_iter<T: IntoIterator<Item = AaConfig>>(iter: T) -> Self {
let mut result = Self {
area: false,
msaa8: false,
msaa16: false,
};
for config in iter {
match config {
AaConfig::Area => result.area = true,
AaConfig::Msaa8 => result.msaa8 = true,
AaConfig::Msaa16 => result.msaa16 = true,
}
}
result
}
}
/// Errors that can occur in Vello.
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum Error {
/// There is no available device with the features required by Vello.
#[cfg(feature = "wgpu")]
#[error("Couldn't find suitable device")]
NoCompatibleDevice,
/// Failed to create surface.
/// See [`wgpu::CreateSurfaceError`] for more information.
#[cfg(feature = "wgpu")]
#[error("Couldn't create wgpu surface")]
WgpuCreateSurfaceError(#[from] wgpu::CreateSurfaceError),
/// Surface doesn't support the required texture formats.
/// Make sure that you have a surface which provides one of
/// [`TextureFormat::Rgba8Unorm`][wgpu::TextureFormat::Rgba8Unorm]
/// or [`TextureFormat::Bgra8Unorm`][wgpu::TextureFormat::Bgra8Unorm] as texture formats.
// TODO: Why does this restriction exist?
#[cfg(feature = "wgpu")]
#[error("Couldn't find `Rgba8Unorm` or `Bgra8Unorm` texture formats for surface")]
UnsupportedSurfaceFormat,
/// Used a buffer inside a recording while it was not available.
/// Check if you have created it and not freed before its last usage.
#[cfg(feature = "wgpu")]
#[error("Buffer '{0}' is not available but used for {1}")]
UnavailableBufferUsed(&'static str, &'static str),
/// Failed to async map a buffer.
/// See [`wgpu::BufferAsyncError`] for more information.
#[cfg(feature = "wgpu")]
#[error("Failed to async map a buffer")]
BufferAsyncError(#[from] wgpu::BufferAsyncError),
/// Failed to download an internal buffer for debug visualization.
#[cfg(feature = "wgpu")]
#[cfg(feature = "debug_layers")]
#[error("Failed to download internal buffer '{0}' for visualization")]
DownloadError(&'static str),
#[cfg(feature = "wgpu")]
#[error("wgpu Error from scope")]
WgpuErrorFromScope(#[from] wgpu::Error),
/// Failed to create [`GpuProfiler`].
/// See [`wgpu_profiler::CreationError`] for more information.
#[cfg(feature = "wgpu-profiler")]
#[error("Couldn't create wgpu profiler")]
#[doc(hidden)] // End-users of Vello should not have `wgpu-profiler` enabled.
ProfilerCreationError(#[from] wgpu_profiler::CreationError),
/// Failed to compile the shaders.
#[cfg(feature = "hot_reload")]
#[error("Failed to compile shaders:\n{0}")]
#[doc(hidden)] // End-users of Vello should not have `hot_reload` enabled.
ShaderCompilation(#[from] vello_shaders::compile::ErrorVec),
}
#[cfg_attr(
not(feature = "wgpu"),
expect(dead_code, reason = "this can be unused when wgpu feature is not used")
)]
pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;
/// Renders a scene into a texture or surface.
///
/// Currently, each renderer only supports a single surface format, if it
/// supports drawing to surfaces at all.
/// This is an assumption which is known to be limiting, and is planned to change.
#[cfg(feature = "wgpu")]
pub struct Renderer {
#[cfg_attr(
not(feature = "hot_reload"),
expect(
dead_code,
reason = "Options are only used to reinitialise on a hot reload"
)
)]
options: RendererOptions,
engine: WgpuEngine,
resolver: Resolver,
image_atlas: Option<recording::ImageProxy>,
shaders: FullShaders,
#[cfg(feature = "debug_layers")]
debug: debug::DebugRenderer,
#[cfg(feature = "wgpu-profiler")]
#[doc(hidden)] // End-users of Vello should not have `wgpu-profiler` enabled.
/// The profiler used with events for this renderer. This is *not* treated as public API.
pub profiler: GpuProfiler,
#[cfg(feature = "wgpu-profiler")]
#[doc(hidden)] // End-users of Vello should not have `wgpu-profiler` enabled.
/// The results from profiling. This is *not* treated as public API.
pub profile_result: Option<Vec<wgpu_profiler::GpuTimerQueryResult>>,
}
// This is not `Send` (or `Sync`) on WebAssembly as the
// underlying wgpu types are not. This can be enabled with the
// `fragile-send-sync-non-atomic-wasm` feature in wgpu.
// See https://github.com/gfx-rs/wgpu/discussions/4127 for
// further discussion of this topic.
#[cfg(all(feature = "wgpu", not(target_arch = "wasm32")))]
static_assertions::assert_impl_all!(Renderer: Send);
/// Parameters used in a single render that are configurable by the client.
///
/// These are used in [`Renderer::render_to_texture`].
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 = "wgpu")]
/// Options which are set at renderer creation time, used in [`Renderer::new`].
pub struct RendererOptions {
/// If true, run all stages up to fine rasterization on the CPU.
///
/// This is not a recommended configuration as it is expected to have poor performance,
/// but it can be useful for debugging.
// 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.
///
/// By default this will be all modes, to support the widest range of.
/// It is recommended that most users configure this.
pub antialiasing_support: AaSupport,
/// How many threads to use for initialisation of shaders.
///
/// Use `Some(1)` to use a single thread. This is recommended when on macOS
/// (see <https://github.com/bevyengine/bevy/pull/10812#discussion_r1496138004>)
///
/// Set to `None` to use a heuristic which will use many but not all threads
///
/// Has no effect on WebAssembly
///
/// Will default to `None` on most platforms, `Some(1)` on macOS.
pub num_init_threads: Option<NonZeroUsize>,
/// The pipeline cache to use when creating the shaders.
///
/// For much more discussion of expected usage patterns, see the documentation on that type.
pub pipeline_cache: Option<wgpu::PipelineCache>,
}
#[cfg(feature = "wgpu")]
impl Default for RendererOptions {
fn default() -> Self {
Self {
use_cpu: false,
antialiasing_support: AaSupport::all(),
#[cfg(target_os = "macos")]
num_init_threads: NonZeroUsize::new(1),
#[cfg(not(target_os = "macos"))]
num_init_threads: None,
pipeline_cache: None,
}
}
}
#[cfg(feature = "wgpu")]
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, options.pipeline_cache.clone());
// If we are running in parallel (i.e. the number of threads is not 1)
if options.num_init_threads != NonZeroUsize::new(1) {
#[cfg(not(target_arch = "wasm32"))]
engine.use_parallel_initialisation();
}
let shaders = shaders::full_shaders(device, &mut engine, &options)?;
#[cfg(not(target_arch = "wasm32"))]
engine.build_shaders_if_needed(device, options.num_init_threads);
#[cfg(feature = "debug_layers")]
let debug = debug::DebugRenderer::new(device, wgpu::TextureFormat::Rgba8Unorm, &mut engine);
Ok(Self {
options,
engine,
resolver: Resolver::new(),
image_atlas: None,
shaders,
#[cfg(feature = "debug_layers")]
debug,
#[cfg(feature = "wgpu-profiler")]
profiler: GpuProfiler::new(device, GpuProfilerSettings::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.
///
/// If you want to render Vello content to a surface (such as in a UI toolkit), you have two options:
/// 1) Render to an intermediate texture, which is the same size as the surface.
/// You would then use [`TextureBlitter`][wgpu::util::TextureBlitter] to blit the rendered result from
/// that texture to the surface.
/// This pattern is supported by the [`util`] module.
/// 2) Call `render_to_texture` directly on the [`SurfaceTexture`][wgpu::SurfaceTexture]'s texture, if
/// it has the right usages. This should generally be avoided, as some GPUs assume that you will not
/// be rendering to the surface using a compute pipeline, and optimise accordingly.
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,
&mut self.resolver,
&self.shaders,
&mut self.image_atlas,
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,
)?;
// N.B. This is horrible; this integration of wgpu-profiler really needs some work...
#[cfg(feature = "wgpu-profiler")]
{
self.profiler.end_frame().unwrap();
if let Some(result) = self
.profiler
.process_finished_frame(queue.get_timestamp_period())
{
self.profile_result = Some(result);
}
}
Ok(())
}
/// Overwrite `image` with `texture`.
///
/// Most users should prefer [`register_texture`](Self::register_texture), which
/// ergonomically encapulates these requirements.
///
/// `texture` must have the [`wgpu::TextureFormat::Rgba8Unorm`] format and
/// the [`wgpu::TextureUsages::COPY_SRC`] flag set. The `Rgba8UnormSrgb` format
/// might also be supported, but this is not tested.
///
/// The given `Texture`'s data will be copied into the slot in Vello's image
/// atlas where the image would be placed each frame.
/// This has the effect that wherever you request the image to be drawn (e.g.
/// through [`Scene::draw_image`]), the data from the texture will be used instead.
///
/// Correct behaviour is not guaranteed if the texture does not have the same
/// dimensions as the image, nor if an image which uses the same [blob] but different
/// dimensions would be rendered.
///
/// [blob]: peniko::ImageData::data
pub fn override_image(
&mut self,
image: &ImageData,
texture: Option<wgpu::TexelCopyTextureInfoBase<wgpu::Texture>>,
) -> Option<wgpu::TexelCopyTextureInfoBase<wgpu::Texture>> {
match texture {
Some(texture) => self.engine.image_overrides.insert(image.data.id(), texture),
None => self.engine.image_overrides.remove(&image.data.id()),
}
}
/// Marks `image` as dirty in the atlas cache, so its override texture will be re-copied on
/// the next render.
///
/// Call this each frame for any `image` whose override texture has changed since the last
/// render. Otherwise, it's possible stale image data from the atlas will be used.
pub fn mark_override_image_dirty(&mut self, image: &ImageData) {
self.resolver.mark_image_dirty(image);
}
/// Register a [`wgpu::Texture`] with Vello, to allow drawing GPU-resident data.
///
/// The returned `Image` can be used in [`Scene`]s (only those rendered with this `Renderer`)
/// Rendering Scenes which use this `Image` with other `Renderer`s will panic.
///
/// `texture` must have the [`wgpu::TextureFormat::Rgba8Unorm`] format and
/// the [`wgpu::TextureUsages::COPY_SRC`] flag set. This is because the data will
/// be copied into Vello's image atlas at the start of each frame.
/// The `Rgba8UnormSrgb` format might also be supported, but this is not tested.
/// The texture is assumed to have unpremultiplied alpha.
///
/// This is a utility wrapper around [`override_image`](Self::override_image).
/// For greater control, use that method.
///
/// If the texture is no longer active then it should be unregistered using [`unregister_texture`](Self::unregister_texture)
pub fn register_texture(&mut self, texture: wgpu::Texture) -> ImageData {
// Create a fake, empty blob which will be used to back the returned image
// This image data will never be read by Vello, due to being added to
// image_overrides, below.
let fake_blob = peniko::Blob::new(std::sync::Arc::new(&[]));
let image = ImageData {
data: fake_blob,
format: peniko::ImageFormat::Rgba8,
alpha_type: peniko::ImageAlphaType::Alpha,
width: texture.width(),
height: texture.height(),
};
// For this utility API, we take the full texture and use the base layer and mip level
let texture_base = wgpu::TexelCopyTextureInfoBase {
texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
};
// We overwrite any attempt to use the fake blob, instead reading from the texture
self.engine
.image_overrides
.insert(image.data.id(), texture_base);
image
}
/// Unregister a [`wgpu::Texture`] that was registered with [`register_texture`](Self::register_texture).
pub fn unregister_texture(&mut self, handle: ImageData) {
self.engine.image_overrides.remove(&handle.data.id());
}
/// Reload the shaders. This should only be used during `vello` development
#[cfg(feature = "hot_reload")]
#[doc(hidden)] // End-users of Vello should not have `hot_reload` enabled.
pub async fn reload_shaders(&mut self, device: &Device) -> Result<(), Error> {
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let mut engine = WgpuEngine::new(self.options.use_cpu, self.options.pipeline_cache.clone());
// We choose not to initialise these shaders in parallel, to ensure the error scope works correctly
let shaders = shaders::full_shaders(device, &mut engine, &self.options)?;
#[cfg(feature = "debug_layers")]
let debug = debug::DebugRenderer::new(device, wgpu::TextureFormat::Rgba8Unorm, &mut engine);
let error = scope.pop().await;
if let Some(error) = error {
return Err(error.into());
}
self.engine = engine;
self.image_atlas = None;
self.shaders = shaders;
#[cfg(feature = "debug_layers")]
{
self.debug = debug;
}
Ok(())
}
/// Renders a scene to the target texture using an async pipeline.
///
/// Almost all consumers should prefer [`Self::render_to_texture`].
///
/// 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
#[cfg_attr(docsrs, doc(hidden))]
#[deprecated(
note = "render_to_texture should be preferred, as the _async version has no stability guarantees"
)]
pub async fn render_to_texture_async(
&mut self,
device: &Device,
queue: &Queue,
scene: &Scene,
texture: &TextureView,
params: &RenderParams,
debug_layers: DebugLayers,
) -> Result<Option<BumpAllocators>> {
if cfg!(not(feature = "debug_layers")) && !debug_layers.is_empty() {
static HAS_WARNED: AtomicBool = AtomicBool::new(false);
if !HAS_WARNED.swap(true, std::sync::atomic::Ordering::Release) {
log::warn!(
"Requested debug layers {debug_layers:?} but `debug_layers` feature is not enabled"
);
}
}
let result = self
.render_to_texture_async_internal(device, queue, scene, texture, params)
.await?;
#[cfg(feature = "debug_layers")]
{
let mut recording = Recording::default();
let target_proxy = recording::ImageProxy::new(
params.width,
params.height,
recording::ImageFormat::Rgba8,
);
if let Some(captured) = result.captured {
let bump = result.bump.as_ref().unwrap();
// TODO: We could avoid this download if `DebugLayers::VALIDATION` is unset.
let downloads = DebugDownloads::map(&self.engine, &captured, bump).await?;
self.debug.render(
&mut recording,
target_proxy,
&captured,
bump,
params,
&downloads,
debug_layers,
);
// 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 external_resources = [ExternalResource::Image(target_proxy, texture)];
self.engine.run_recording(
device,
queue,
&recording,
&external_resources,
"render_to_texture_async debug layers",
#[cfg(feature = "wgpu-profiler")]
&mut self.profiler,
)?;
}
#[cfg(feature = "wgpu-profiler")]
{
self.profiler.end_frame().unwrap();
if let Some(result) = self
.profiler
.process_finished_frame(queue.get_timestamp_period())
{
self.profile_result = Some(result);
}
}
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,
&mut self.resolver,
&self.shaders,
&mut self.image_atlas,
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());
receiver.receive().await.expect("channel was closed")?;
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,
})
}
}
#[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(Error::DownloadError("linesoup"));
};
let lines = lines_buf.slice(..bump.lines as u64 * 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());
receiver.receive().await.expect("channel was closed")?;
Ok(Self { lines })
}
}