#![warn(unused_crate_dependencies)]
#![warn(clippy::print_stdout, clippy::print_stderr)]
#![cfg_attr(target_pointer_width = "64", warn(clippy::trivially_copy_pass_by_ref))]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![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 {
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;
pub use vello_encoding::BumpAllocators;
}
pub use peniko;
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};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum AaConfig {
Area,
Msaa8,
Msaa16,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
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,
}
}
}
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
}
}
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum Error {
#[cfg(feature = "wgpu")]
#[error("Couldn't find suitable device")]
NoCompatibleDevice,
#[cfg(feature = "wgpu")]
#[error("Couldn't create wgpu surface")]
WgpuCreateSurfaceError(#[from] wgpu::CreateSurfaceError),
#[cfg(feature = "wgpu")]
#[error("Couldn't find `Rgba8Unorm` or `Bgra8Unorm` texture formats for surface")]
UnsupportedSurfaceFormat,
#[cfg(feature = "wgpu")]
#[error("Buffer '{0}' is not available but used for {1}")]
UnavailableBufferUsed(&'static str, &'static str),
#[cfg(feature = "wgpu")]
#[error("Failed to async map a buffer")]
BufferAsyncError(#[from] wgpu::BufferAsyncError),
#[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),
#[cfg(feature = "wgpu-profiler")]
#[error("Couldn't create wgpu profiler")]
#[doc(hidden)] ProfilerCreationError(#[from] wgpu_profiler::CreationError),
#[cfg(feature = "hot_reload")]
#[error("Failed to compile shaders:\n{0}")]
#[doc(hidden)] 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>;
#[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,
shaders: FullShaders,
#[cfg(feature = "debug_layers")]
debug: debug::DebugRenderer,
#[cfg(feature = "wgpu-profiler")]
#[doc(hidden)] pub profiler: GpuProfiler,
#[cfg(feature = "wgpu-profiler")]
#[doc(hidden)] pub profile_result: Option<Vec<wgpu_profiler::GpuTimerQueryResult>>,
}
#[cfg(all(feature = "wgpu", not(target_arch = "wasm32")))]
static_assertions::assert_impl_all!(Renderer: Send);
pub struct RenderParams {
pub base_color: peniko::Color,
pub width: u32,
pub height: u32,
pub antialiasing_method: AaConfig,
}
#[cfg(feature = "wgpu")]
pub struct RendererOptions {
pub use_cpu: bool,
pub antialiasing_support: AaSupport,
pub num_init_threads: Option<NonZeroUsize>,
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 {
pub fn new(device: &Device, options: RendererOptions) -> Result<Self> {
let mut engine = WgpuEngine::new(options.use_cpu, options.pipeline_cache.clone());
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(),
shaders,
#[cfg(feature = "debug_layers")]
debug,
#[cfg(feature = "wgpu-profiler")]
profiler: GpuProfiler::new(device, GpuProfilerSettings::default())?,
#[cfg(feature = "wgpu-profiler")]
profile_result: None,
})
}
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, 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,
)?;
#[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(())
}
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()),
}
}
pub fn register_texture(&mut self, texture: wgpu::Texture) -> ImageData {
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(),
};
let texture_base = wgpu::TexelCopyTextureInfoBase {
texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
};
self.engine
.image_overrides
.insert(image.data.id(), texture_base);
image
}
pub fn unregister_texture(&mut self, handle: ImageData) {
self.engine.image_overrides.remove(&handle.data.id());
}
#[cfg(feature = "hot_reload")]
#[doc(hidden)] 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());
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.shaders = shaders;
#[cfg(feature = "debug_layers")]
{
self.debug = debug;
}
Ok(())
}
#[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();
let downloads = DebugDownloads::map(&self.engine, &captured, bump).await?;
self.debug.render(
&mut recording,
target_proxy,
&captured,
bump,
params,
&downloads,
debug_layers,
);
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();
let robust = cfg!(feature = "debug_layers");
let recording = render.render_encoding_coarse(
encoding,
&mut self.resolver,
&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());
receiver.receive().await.expect("channel was closed")?;
let mapped = buf_slice.get_mapped_range();
bump = Some(bytemuck::pod_read_unaligned(&mapped));
}
self.engine.free_download(bump_buf);
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 })
}
}