use crate::backend::convert;
use crate::backend::mesh;
use std::num::NonZeroUsize;
use crate::geometry::Shape as _;
use vello::{AaConfig, AaSupport, RenderParams, Renderer as VRenderer, RendererOptions, Scene};
use crate::backend::{BackendError, Renderer, WgpuRenderer};
use crate::blend::BlendMode;
use crate::brush::{Brush, Image, Sampling};
use crate::color::Color;
use crate::geometry::Affine;
use crate::mesh::Mesh;
use crate::path::{FillRule, Path};
use crate::pick::{self, PickId};
use crate::scene::{GlyphRun, SceneBuilder};
use crate::stroke::Stroke;
const MIN_PICK_STROKE_WIDTH: f64 = 2.0;
pub const MAX_DRAW_INFO_WORDS: u32 = 1 << 18;
pub struct VelloScene {
inner: Scene,
pick: Option<Scene>,
}
impl VelloScene {
pub fn new() -> Self {
Self {
inner: Scene::new(),
pick: None,
}
}
pub(crate) fn with_picking() -> Self {
Self {
inner: Scene::new(),
pick: Some(Scene::new()),
}
}
pub fn raw(&self) -> &Scene {
&self.inner
}
pub(crate) fn raw_pick(&self) -> Option<&Scene> {
self.pick.as_ref()
}
pub fn draw_info_words(&self) -> u32 {
draw_info_words(&self.inner)
}
pub fn fits_draw_budget(&self) -> bool {
check_draw_budget(&self.inner).is_ok()
&& self
.pick
.as_ref()
.is_none_or(|p| check_draw_budget(p).is_ok())
}
}
fn draw_info_words(scene: &Scene) -> u32 {
scene
.encoding()
.draw_tags
.iter()
.map(|tag| tag.info_size())
.sum()
}
fn check_draw_budget(scene: &Scene) -> Result<(), BackendError> {
let used = draw_info_words(scene);
if used > MAX_DRAW_INFO_WORDS {
return Err(BackendError::SceneTooLarge {
used,
max: MAX_DRAW_INFO_WORDS,
});
}
Ok(())
}
impl Default for VelloScene {
fn default() -> Self {
Self::new()
}
}
impl SceneBuilder for VelloScene {
fn clear(&mut self) {
self.inner.reset();
if let Some(p) = &mut self.pick {
p.reset();
}
}
fn fill(
&mut self,
rule: FillRule,
transform: Affine,
brush: &Brush,
brush_transform: Option<Affine>,
path: &Path,
pick_id: PickId,
) {
let fill_rule = convert::fill_rule(rule);
self.inner
.fill(fill_rule, transform, brush, brush_transform, path);
if let Some(pick) = &mut self.pick {
if let Some(id) = pick::raw_id(pick_id) {
let pick_brush = Brush::Solid(pick::id_to_color(id));
pick.fill(fill_rule, transform, &pick_brush, None, path);
}
}
}
fn stroke(
&mut self,
stroke: &Stroke,
transform: Affine,
brush: &Brush,
brush_transform: Option<Affine>,
path: &Path,
pick_id: PickId,
) {
self.inner
.stroke(stroke, transform, brush, brush_transform, path);
if let Some(pick) = &mut self.pick {
if let Some(id) = pick::raw_id(pick_id) {
let pick_brush = Brush::Solid(pick::id_to_color(id));
let mut pick_stroke = stroke.clone();
if pick_stroke.width < MIN_PICK_STROKE_WIDTH {
pick_stroke.width = MIN_PICK_STROKE_WIDTH;
}
pick.stroke(&pick_stroke, transform, &pick_brush, None, path);
}
}
}
fn draw_image(
&mut self,
image: &Image,
transform: Affine,
sampling: Sampling,
alpha: f32,
pick_id: PickId,
) {
let sampler = peniko::ImageSampler {
x_extend: peniko::Extend::Pad,
y_extend: peniko::Extend::Pad,
quality: convert::sampling_to_quality(sampling),
alpha,
};
let brush = peniko::ImageBrush {
image: image.clone(),
sampler,
};
self.inner.draw_image(&brush, transform);
if let Some(pick) = &mut self.pick {
if let Some(id) = pick::raw_id(pick_id) {
let pick_brush = Brush::Solid(pick::id_to_color(id));
let bounds =
crate::geometry::Rect::new(0.0, 0.0, image.width as f64, image.height as f64)
.to_path(0.1);
pick.fill(peniko::Fill::NonZero, transform, &pick_brush, None, &bounds);
}
}
}
fn draw_glyphs(&mut self, run: &GlyphRun<'_>, pick_id: PickId) {
let style: peniko::StyleRef<'_> = match run.style {
Some(stroke) => peniko::StyleRef::from(stroke),
None => peniko::StyleRef::from(peniko::Fill::NonZero),
};
let builder = self
.inner
.draw_glyphs(run.font.data())
.font_size(run.font_size)
.transform(run.transform)
.glyph_transform(run.glyph_transform)
.brush(run.brush)
.brush_alpha(run.brush_alpha)
.hint(run.hint);
builder.draw(
style,
run.glyphs.iter().map(|g| vello::Glyph {
id: g.id,
x: g.x,
y: g.y,
}),
);
if let Some(pick) = &mut self.pick {
if let Some(id) = pick::raw_id(pick_id) {
let pick_brush = Brush::Solid(pick::id_to_color(id));
let pick_style: peniko::StyleRef<'_> = match run.style {
Some(stroke) => peniko::StyleRef::from(stroke),
None => peniko::StyleRef::from(peniko::Fill::NonZero),
};
let pick_builder = pick
.draw_glyphs(run.font.data())
.font_size(run.font_size)
.transform(run.transform)
.glyph_transform(run.glyph_transform)
.brush(&pick_brush)
.brush_alpha(1.0)
.hint(run.hint);
pick_builder.draw(
pick_style,
run.glyphs.iter().map(|g| vello::Glyph {
id: g.id,
x: g.x,
y: g.y,
}),
);
}
}
}
fn draw_mesh(&mut self, mesh: &Mesh, transform: Affine, pick_id: PickId) {
mesh::decompose(mesh, transform, pick_id, self);
}
fn push_layer(&mut self, blend: BlendMode, alpha: f32, transform: Affine, clip: &Path) {
self.inner.push_layer(
peniko::Fill::NonZero,
convert::blend_mode(blend),
alpha,
transform,
clip,
);
if let Some(pick) = &mut self.pick {
pick.push_layer(
peniko::Fill::NonZero,
convert::blend_mode(BlendMode::NORMAL),
1.0,
transform,
clip,
);
}
}
fn pop_layer(&mut self) {
self.inner.pop_layer();
if let Some(pick) = &mut self.pick {
pick.pop_layer();
}
}
}
struct HeadlessTarget {
texture: wgpu::Texture,
view: wgpu::TextureView,
readback: wgpu::Buffer,
width: u32,
height: u32,
padded_bytes_per_row: u32,
}
impl HeadlessTarget {
fn new(device: &wgpu::Device, width: u32, height: u32) -> Self {
let bytes_per_row = width * 4;
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
let padded_bytes_per_row = bytes_per_row.div_ceil(align) * align;
let buffer_size = (padded_bytes_per_row as u64) * (height as u64);
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("hephaestus.vello.target"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("hephaestus.vello.readback"),
size: buffer_size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
Self {
texture,
view,
readback,
width,
height,
padded_bytes_per_row,
}
}
}
struct PendingPick {
slot: std::sync::Arc<std::sync::Mutex<Option<Result<(), wgpu::BufferAsyncError>>>>,
width: u32,
height: u32,
}
pub struct VelloRenderer {
device: wgpu::Device,
queue: wgpu::Queue,
renderer: VRenderer,
scene: VelloScene,
target: Option<HeadlessTarget>,
pick_target: Option<HeadlessTarget>,
hitmap: Option<Vec<u32>>,
hitmap_dims: Option<(u32, u32)>,
pick_pending: Option<PendingPick>,
refresh_pick: bool,
}
impl VelloRenderer {
pub fn new() -> Result<Self, BackendError> {
pollster::block_on(Self::new_async(false))
}
pub fn with_picking() -> Result<Self, BackendError> {
pollster::block_on(Self::new_async(true))
}
pub fn with_device(device: &wgpu::Device, queue: &wgpu::Queue) -> Result<Self, BackendError> {
Self::build(device.clone(), queue.clone(), false)
}
pub fn with_device_and_picking(
device: &wgpu::Device,
queue: &wgpu::Queue,
) -> Result<Self, BackendError> {
Self::build(device.clone(), queue.clone(), true)
}
async fn new_async(picking: bool) -> Result<Self, BackendError> {
let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
desc.backends =
wgpu::Backends::from_env().unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::GL);
let instance = wgpu::Instance::new(desc);
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: None,
force_fallback_adapter: false,
})
.await
.map_err(|_| BackendError::NoAdapter)?;
let limits = wgpu::Limits::default();
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("hephaestus.vello.device"),
required_features: wgpu::Features::empty(),
required_limits: limits,
memory_hints: wgpu::MemoryHints::default(),
trace: wgpu::Trace::Off,
experimental_features: wgpu::ExperimentalFeatures::default(),
})
.await
.map_err(|e| BackendError::DeviceRequest(e.to_string()))?;
Self::build(device, queue, picking)
}
fn build(
device: wgpu::Device,
queue: wgpu::Queue,
picking: bool,
) -> Result<Self, BackendError> {
let renderer = VRenderer::new(
&device,
RendererOptions {
use_cpu: false,
antialiasing_support: AaSupport::area_only(),
num_init_threads: NonZeroUsize::new(1),
pipeline_cache: None,
},
)
.map_err(|e| BackendError::Other(format!("vello renderer init: {e}")))?;
let scene = if picking {
VelloScene::with_picking()
} else {
VelloScene::new()
};
Ok(Self {
device,
queue,
renderer,
scene,
target: None,
pick_target: None,
hitmap: None,
hitmap_dims: None,
pick_pending: None,
refresh_pick: true,
})
}
fn ensure_display_target(&mut self, width: u32, height: u32) {
let need_new = match &self.target {
None => true,
Some(t) => t.width != width || t.height != height,
};
if need_new {
self.target = Some(HeadlessTarget::new(&self.device, width, height));
}
}
fn ensure_pick_target(&mut self, width: u32, height: u32) {
if self.scene.raw_pick().is_none() {
return;
}
let need_new = match &self.pick_target {
None => true,
Some(t) => t.width != width || t.height != height,
};
if need_new {
self.pick_target = Some(HeadlessTarget::new(&self.device, width, height));
}
}
fn check_scene_budget(&self) -> Result<(), BackendError> {
check_draw_budget(self.scene.raw())?;
if let Some(pick) = self.scene.raw_pick() {
check_draw_budget(pick)?;
}
Ok(())
}
fn render_pick_and_readback(&mut self, width: u32, height: u32) -> Result<(), BackendError> {
self.submit_pick(width, height)?;
let _ = self.device.poll(wgpu::PollType::wait_indefinitely());
if !self.try_finish_pick()? {
return Err(BackendError::Readback(
"pick readback did not complete after a blocking device poll".into(),
));
}
Ok(())
}
fn submit_pick(&mut self, width: u32, height: u32) -> Result<(), BackendError> {
let pick_scene = self.scene.raw_pick().expect("pick scene present");
let pick_target = self.pick_target.as_ref().expect("pick target ensured");
self.renderer
.render_to_texture(
&self.device,
&self.queue,
pick_scene,
&pick_target.view,
&RenderParams {
base_color: Color::new([0.0, 0.0, 0.0, 0.0]),
width,
height,
antialiasing_method: AaConfig::Area,
},
)
.map_err(|e| BackendError::Other(format!("vello pick render: {e}")))?;
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("hephaestus.vello.pick_readback"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &pick_target.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &pick_target.readback,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(pick_target.padded_bytes_per_row),
rows_per_image: Some(height),
},
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
self.queue.submit(std::iter::once(encoder.finish()));
let slot = std::sync::Arc::new(std::sync::Mutex::new(None));
let sink = std::sync::Arc::clone(&slot);
pick_target
.readback
.slice(..)
.map_async(wgpu::MapMode::Read, move |res| {
if let Ok(mut guard) = sink.lock() {
*guard = Some(res);
}
});
self.pick_pending = Some(PendingPick {
slot,
width,
height,
});
Ok(())
}
pub fn try_finish_pick(&mut self) -> Result<bool, BackendError> {
let Some(pending) = self.pick_pending.as_ref() else {
return Ok(false);
};
let landed = pending
.slot
.lock()
.map_err(|_| BackendError::Readback("pick readback slot poisoned".into()))?
.take();
let Some(result) = landed else {
return Ok(false);
};
let PendingPick { width, height, .. } =
self.pick_pending.take().expect("checked just above");
result.map_err(|e| BackendError::Readback(e.to_string()))?;
let pick_target = self.pick_target.as_ref().expect("pick target ensured");
let pick_slice = pick_target.readback.slice(..);
let row_bytes = (width as usize) * 4;
let row_px = width as usize;
let total_px = (width as usize) * (height as usize);
let hitmap = self.hitmap.get_or_insert_with(Vec::new);
if hitmap.len() != total_px {
hitmap.resize(total_px, 0);
}
{
let data = pick_slice.get_mapped_range();
let padded = pick_target.padded_bytes_per_row as usize;
for y in 0..height as usize {
let src = &data[y * padded..y * padded + row_bytes];
let dst: &mut [u8] =
bytemuck::cast_slice_mut(&mut hitmap[y * row_px..y * row_px + row_px]);
dst.copy_from_slice(src);
}
}
pick_target.readback.unmap();
self.hitmap_dims = Some((width, height));
Ok(true)
}
pub fn render_to_texture_deferring_pick(
&mut self,
view: &wgpu::TextureView,
width: u32,
height: u32,
background: Color,
) -> Result<(), BackendError> {
self.check_scene_budget()?;
self.renderer
.render_to_texture(
&self.device,
&self.queue,
self.scene.raw(),
view,
&RenderParams {
base_color: background,
width,
height,
antialiasing_method: AaConfig::Area,
},
)
.map_err(|e| BackendError::Other(format!("vello render: {e}")))?;
if self.refreshes_pick() {
self.try_finish_pick()?;
if self.pick_pending.is_none() {
self.ensure_pick_target(width, height);
self.submit_pick(width, height)?;
}
}
Ok(())
}
pub fn set_refresh_pick(&mut self, refresh: bool) {
self.refresh_pick = refresh;
}
pub fn refreshes_pick(&self) -> bool {
self.refresh_pick && self.scene.raw_pick().is_some()
}
pub fn pick_at(&self, x: u32, y: u32) -> Option<u32> {
let (w, h) = self.hitmap_dims?;
if x >= w || y >= h {
return None;
}
let map = self.hitmap.as_deref()?;
pick::decode(map[(y * w + x) as usize])
}
pub fn hitmap(&self) -> Option<&[u32]> {
self.hitmap.as_deref()
}
}
impl Renderer for VelloRenderer {
type Scene = VelloScene;
fn scene(&mut self) -> &mut Self::Scene {
&mut self.scene
}
fn render_to_buffer(
&mut self,
width: u32,
height: u32,
background: Color,
out: &mut [u8],
) -> Result<(), BackendError> {
let expected = (width as usize) * (height as usize) * 4;
if out.len() != expected {
return Err(BackendError::BufferSize {
expected,
actual: out.len(),
});
}
self.check_scene_budget()?;
self.ensure_display_target(width, height);
self.ensure_pick_target(width, height);
let target = self.target.as_ref().unwrap();
self.renderer
.render_to_texture(
&self.device,
&self.queue,
self.scene.raw(),
&target.view,
&RenderParams {
base_color: background,
width,
height,
antialiasing_method: AaConfig::Area,
},
)
.map_err(|e| BackendError::Other(format!("vello render: {e}")))?;
let picking = self.refreshes_pick();
if picking {
let pick_scene = self.scene.raw_pick().unwrap();
let pick_target = self.pick_target.as_ref().expect("pick target ensured");
self.renderer
.render_to_texture(
&self.device,
&self.queue,
pick_scene,
&pick_target.view,
&RenderParams {
base_color: Color::new([0.0, 0.0, 0.0, 0.0]),
width,
height,
antialiasing_method: AaConfig::Area,
},
)
.map_err(|e| BackendError::Other(format!("vello pick render: {e}")))?;
}
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("hephaestus.vello.readback"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &target.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &target.readback,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(target.padded_bytes_per_row),
rows_per_image: Some(height),
},
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
if picking {
let pick_target = self.pick_target.as_ref().unwrap();
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &pick_target.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &pick_target.readback,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(pick_target.padded_bytes_per_row),
rows_per_image: Some(height),
},
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
}
self.queue.submit(std::iter::once(encoder.finish()));
let display_slice = target.readback.slice(..);
let (display_tx, display_rx) = futures_intrusive::channel::shared::oneshot_channel();
display_slice.map_async(wgpu::MapMode::Read, move |res| {
let _ = display_tx.send(res);
});
let pick_rx = if picking {
let pick_target = self.pick_target.as_ref().unwrap();
let pick_slice = pick_target.readback.slice(..);
let (pick_tx, pick_rx) = futures_intrusive::channel::shared::oneshot_channel();
pick_slice.map_async(wgpu::MapMode::Read, move |res| {
let _ = pick_tx.send(res);
});
Some(pick_rx)
} else {
None
};
let _ = self.device.poll(wgpu::PollType::wait_indefinitely());
match pollster::block_on(display_rx.receive()) {
Some(Ok(())) => {}
Some(Err(e)) => return Err(BackendError::Readback(e.to_string())),
None => return Err(BackendError::Readback("map_async sender dropped".into())),
}
if let Some(rx) = pick_rx.as_ref() {
match pollster::block_on(rx.receive()) {
Some(Ok(())) => {}
Some(Err(e)) => return Err(BackendError::Readback(e.to_string())),
None => {
return Err(BackendError::Readback(
"map_async pick sender dropped".into(),
))
}
}
}
let row_bytes = (width as usize) * 4;
{
let data = display_slice.get_mapped_range();
let padded = target.padded_bytes_per_row as usize;
for y in 0..height as usize {
let src = &data[y * padded..y * padded + row_bytes];
let dst = &mut out[y * row_bytes..y * row_bytes + row_bytes];
dst.copy_from_slice(src);
}
}
target.readback.unmap();
if picking {
let pick_target = self.pick_target.as_ref().unwrap();
let row_px = width as usize;
let total_px = (width as usize) * (height as usize);
let hitmap = self.hitmap.get_or_insert_with(Vec::new);
if hitmap.len() != total_px {
hitmap.resize(total_px, 0);
}
let pick_slice = pick_target.readback.slice(..);
{
let data = pick_slice.get_mapped_range();
let padded = pick_target.padded_bytes_per_row as usize;
for y in 0..height as usize {
let src = &data[y * padded..y * padded + row_bytes];
let dst: &mut [u8] =
bytemuck::cast_slice_mut(&mut hitmap[y * row_px..y * row_px + row_px]);
dst.copy_from_slice(src);
}
}
pick_target.readback.unmap();
self.hitmap_dims = Some((width, height));
}
Ok(())
}
}
impl WgpuRenderer for VelloRenderer {
const REQUIRED_TARGET_USAGE: wgpu::TextureUsages = wgpu::TextureUsages::STORAGE_BINDING;
const TARGET_IS_PREMULTIPLIED: bool = false;
fn render_to_texture(
&mut self,
view: &wgpu::TextureView,
width: u32,
height: u32,
background: Color,
) -> Result<(), BackendError> {
self.check_scene_budget()?;
self.renderer
.render_to_texture(
&self.device,
&self.queue,
self.scene.raw(),
view,
&RenderParams {
base_color: background,
width,
height,
antialiasing_method: AaConfig::Area,
},
)
.map_err(|e| BackendError::Other(format!("vello render: {e}")))?;
if self.refreshes_pick() {
self.ensure_pick_target(width, height);
self.render_pick_and_readback(width, height)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::rgb8;
use crate::geometry::Point;
fn solid_fills(scene: &mut VelloScene, n: usize) {
let brush = Brush::Solid(rgb8(10, 20, 30));
for i in 0..n {
let x = (i % 256) as f64;
let mut path = Path::new();
path.move_to(Point::new(x, 0.0));
path.line_to(Point::new(x + 1.0, 0.0));
path.line_to(Point::new(x + 1.0, 1.0));
path.close_path();
scene.fill(
FillRule::NonZero,
Affine::IDENTITY,
&brush,
None,
&path,
PickId::Skip,
);
}
}
#[test]
fn an_empty_scene_spends_no_draw_budget() {
let scene = VelloScene::new();
assert_eq!(scene.draw_info_words(), 0);
assert!(scene.fits_draw_budget());
}
#[test]
fn each_solid_fill_costs_one_draw_info_word() {
let mut scene = VelloScene::new();
solid_fills(&mut scene, 3);
assert_eq!(scene.draw_info_words(), 3);
}
#[test]
fn clearing_a_scene_returns_its_draw_budget() {
let mut scene = VelloScene::new();
solid_fills(&mut scene, 5);
scene.clear();
assert_eq!(scene.draw_info_words(), 0);
}
#[test]
fn a_scene_at_the_cap_fits_but_one_draw_more_does_not() {
let mut scene = VelloScene::new();
solid_fills(&mut scene, MAX_DRAW_INFO_WORDS as usize);
assert!(scene.fits_draw_budget());
solid_fills(&mut scene, 1);
assert!(!scene.fits_draw_budget());
assert!(matches!(
check_draw_budget(scene.raw()),
Err(BackendError::SceneTooLarge { used, max })
if used == MAX_DRAW_INFO_WORDS + 1 && max == MAX_DRAW_INFO_WORDS
));
}
}