From 65a79703826c4be0dab71f435eaaa0454623aafd Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Tue, 7 Feb 2023 12:52:22 +0000 Subject: [PATCH 1/6] Setup infra for headless examples --- .gitignore | 3 +++ Cargo.toml | 1 + examples/headless/Cargo.toml | 10 ++++++++++ examples/headless/outputs/.tracked | 0 examples/headless/src/main.rs | 3 +++ src/lib.rs | 2 +- 6 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 examples/headless/Cargo.toml create mode 100644 examples/headless/outputs/.tracked create mode 100644 examples/headless/src/main.rs diff --git a/.gitignore b/.gitignore index ad0306f..2a2be18 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ /target Cargo.lock + examples/assets/downloads/* !examples/assets/downloads/.tracked +examples/headless/outputs/* +!examples/headless/outputs/.tracked diff --git a/Cargo.toml b/Cargo.toml index d0ded0e..bf92017 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ resolver = "2" members = [ "integrations/vello_svg", + "examples/headless", "examples/with_winit", "examples/with_bevy", "examples/run_wasm", diff --git a/examples/headless/Cargo.toml b/examples/headless/Cargo.toml new file mode 100644 index 0000000..2b310eb --- /dev/null +++ b/examples/headless/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "headless" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +vello = { path = "../../" } +clap = { workspace = true } diff --git a/examples/headless/outputs/.tracked b/examples/headless/outputs/.tracked new file mode 100644 index 0000000..e69de29 diff --git a/examples/headless/src/main.rs b/examples/headless/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/examples/headless/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/src/lib.rs b/src/lib.rs index 30f3bc8..4116f49 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -196,7 +196,7 @@ impl Renderer { return Err("channel was closed".into()); } let mapped = buf_slice.get_mapped_range(); - println!("{:?}", bytemuck::cast_slice::<_, u32>(&mapped)); + // println!("{:?}", bytemuck::cast_slice::<_, u32>(&mapped)); } // TODO: apply logic to determine whether we need to rerun coarse, and also // allocate the blend stack as needed. From 6e1481db40c124a06a009e2ecb915ca5a7fd1b4a Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Tue, 7 Feb 2023 16:26:56 +0000 Subject: [PATCH 2/6] Finish the headless example --- examples/headless/Cargo.toml | 9 +- examples/headless/src/main.rs | 221 +++++++++++++++++++++++++++++++++- examples/scenes/src/lib.rs | 2 +- 3 files changed, 228 insertions(+), 4 deletions(-) diff --git a/examples/headless/Cargo.toml b/examples/headless/Cargo.toml index 2b310eb..91ac4dc 100644 --- a/examples/headless/Cargo.toml +++ b/examples/headless/Cargo.toml @@ -6,5 +6,12 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = { workspace = true } +clap = { workspace = true, features = ["derive"] } vello = { path = "../../" } -clap = { workspace = true } +scenes = { path = "../scenes" } + +wgpu = { workspace = true } +pollster = "0.2.5" +env_logger = "0.10.0" +png = "0.17.7" diff --git a/examples/headless/src/main.rs b/examples/headless/src/main.rs index e7a11a9..d4dd7f1 100644 --- a/examples/headless/src/main.rs +++ b/examples/headless/src/main.rs @@ -1,3 +1,220 @@ -fn main() { - println!("Hello, world!"); +use std::num::NonZeroU32; + +use anyhow::{anyhow, bail, Context, Result}; +use clap::{CommandFactory, Parser}; +use scenes::{SceneParams, SceneSet, SimpleText}; +use vello::{ + kurbo::{Affine, Vec2}, + Scene, SceneBuilder, SceneFragment, +}; +use wgpu::{ + util::initialize_adapter_from_env_or_default, Backends, BufferDescriptor, BufferUsages, + CommandEncoderDescriptor, DeviceDescriptor, Extent3d, ImageCopyBuffer, Instance, Limits, + TextureDescriptor, TextureFormat, TextureUsages, +}; + +fn main() -> Result<()> { + #[cfg(not(target_arch = "wasm32"))] + env_logger::init(); + let args = Args::parse(); + let scenes = args.args.select_scene_set(|| Args::command())?; + if let Some(scenes) = scenes { + let mut scene_idx = None; + for (idx, scene) in scenes.scenes.iter().enumerate() { + if scene.config.name.eq_ignore_ascii_case(&args.scene) { + if let Some(scene_idx) = scene_idx { + eprintln!("Scene names conflict, skipping scene {idx} (instead rendering {scene_idx})"); + } else { + scene_idx = Some(idx); + } + } + } + let scene_idx = match scene_idx { + Some(idx) => idx, + None => { + let parsed = args.scene.parse::().context(format!( + "'{}' didn't match any scene, trying to parse as index", + args.scene + ))?; + + if !(parsed < scenes.scenes.len()) { + if scenes.scenes.len() == 0 { + bail!("Cannot select a scene, as there are no scenes") + } + bail!( + "{parsed} doesn't fit in scenes (len {})", + scenes.scenes.len() + ); + } + parsed + } + }; + if args.print_scenes { + println!("Available scenes:"); + + for (idx, scene) in scenes.scenes.iter().enumerate() { + println!( + "{idx}: {}{}{}", + scene.config.name, + if scene.config.animated { + " (animated)" + } else { + "" + }, + if scene_idx == idx { " (selected)" } else { "" } + ); + } + return Ok(()); + } + pollster::block_on(render(scenes, scene_idx, &args))?; + } + Ok(()) +} + +async fn render(mut scenes: SceneSet, index: usize, args: &Args) -> Result<()> { + let instance = Instance::new(wgpu::InstanceDescriptor { + backends: wgpu::Backends::PRIMARY, + dx12_shader_compiler: wgpu::Dx12Compiler::Fxc, + }); + let adapter = initialize_adapter_from_env_or_default(&instance, Backends::PRIMARY, None) + .await + .ok_or_else(|| anyhow!("Failed to intiialise adapter"))?; + let (device, queue) = adapter + .request_device( + &DeviceDescriptor { + label: Some("Vello Headless"), + features: wgpu::Features::TIMESTAMP_QUERY | wgpu::Features::CLEAR_TEXTURE, + limits: Limits::default(), + }, + None, + ) + .await?; + let mut renderer = vello::Renderer::new(&device) + .or_else(|_| bail!("Got non-Send/Sync error from creating renderer"))?; + let mut fragment = SceneFragment::new(); + let mut builder = SceneBuilder::for_fragment(&mut fragment); + let example_scene = &mut scenes.scenes[index]; + let mut text = SimpleText::new(); + let mut params = SceneParams { + time: args.time.unwrap_or(0.), + text: &mut text, + resolution: None, + }; + (example_scene.function)(&mut builder, &mut params); + builder.finish(); + let mut transform = Affine::IDENTITY; + let (width, height) = if let Some(resolution) = params.resolution { + let ratio = resolution.x / resolution.y; + let (new_width, new_height) = match (args.x_resolution, args.y_resolution) { + (None, None) => (resolution.x.ceil() as u32, resolution.y.ceil() as u32), + (None, Some(y)) => ((ratio * (y as f64)).ceil() as u32, y), + (Some(x), None) => (x, ((x as f64) / ratio).ceil() as u32), + (Some(x), Some(y)) => (x, y), + }; + let factor = Vec2::new(new_width as f64, new_height as f64); + let scale_factor = (factor.x / resolution.x).min(factor.y / resolution.y); + transform = transform * Affine::scale(scale_factor); + (new_width, new_height) + } else { + match (args.x_resolution, args.y_resolution) { + (None, None) => (1000, 1000), + (None, Some(y)) => { + let y = y.try_into()?; + (y, y) + } + (Some(x), None) => { + let x = x.try_into()?; + (x, x) + } + (Some(x), Some(y)) => (x.try_into()?, y.try_into()?), + } + }; + let mut scene = Scene::new(); + let mut builder = SceneBuilder::for_scene(&mut scene); + builder.append(&fragment, Some(transform)); + builder.finish(); + let size = Extent3d { + width, + height, + depth_or_array_layers: 1, + }; + let target = device.create_texture(&TextureDescriptor { + label: Some("Target texture"), + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: TextureFormat::Rgba8Unorm, + usage: TextureUsages::STORAGE_BINDING | TextureUsages::COPY_SRC, + view_formats: &[], + }); + let view = target.create_view(&wgpu::TextureViewDescriptor::default()); + renderer + .render_to_texture(&device, &queue, &scene, &view, width, height) + .or_else(|_| bail!("Got non-Send/Sync error from rendering"))?; + // (width * 4).next_multiple_of(256) + let padded_width = { + let w = width as u32 * 4; + match w % 256 { + 0 => w, + r => w + (256 - r), + } + }; + let buffer_size = padded_width as u64 * height as u64; + let buffer = device.create_buffer(&BufferDescriptor { + label: Some("val"), + size: buffer_size, + usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("Copy out buffer"), + }); + encoder.copy_texture_to_buffer( + target.as_image_copy(), + ImageCopyBuffer { + buffer: &buffer, + layout: wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: NonZeroU32::new(padded_width), + rows_per_image: None, + }, + }, + size, + ); + queue.submit([encoder.finish()]); + let buf_slice = buffer.slice(..); + buf_slice.map_async(wgpu::MapMode::Read, |done| done.unwrap()); + device.poll(wgpu::MaintainBase::Wait); + let data = buf_slice.get_mapped_range(); + let mut result = Vec::::new(); + let mut encoder = png::Encoder::new(&mut result, padded_width / 4, height); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + let mut writer = encoder.write_header()?; + writer.write_image_data(&data)?; + writer.finish()?; + std::fs::write("./test.png", result)?; + Ok(()) +} + +#[derive(Parser, Debug)] +#[command(about, long_about = None, bin_name="cargo run -p with_winit --")] +struct Args { + #[arg(long, short, global(false))] + x_resolution: Option, + #[arg(long, short, global(false))] + y_resolution: Option, + /// Which scene (name) to render + /// If no scenes have that name, an index can be specified instead + #[arg(long, short, default_value = "0", global(false))] + scene: String, + #[arg(long, short, global(false))] + /// The time in seconds since the frame start, for animated scenes + time: Option, + #[arg(long, short, global(false))] + /// Display a list of all scene names + print_scenes: bool, + #[command(flatten)] + args: scenes::Arguments, } diff --git a/examples/scenes/src/lib.rs b/examples/scenes/src/lib.rs index fefe691..b23e54a 100644 --- a/examples/scenes/src/lib.rs +++ b/examples/scenes/src/lib.rs @@ -40,7 +40,7 @@ pub struct SceneSet { /// Shared config for scene selection pub struct Arguments { #[arg(help_heading = "Scene Selection")] - #[arg(short = 't', long, global(false))] + #[arg(long, global(false))] /// Whether to use the test scenes created by code test_scenes: bool, #[arg(help_heading = "Scene Selection", global(false))] From d366151970bf5ced612d3d72a3fafebc14e91282 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Tue, 7 Feb 2023 16:37:29 +0000 Subject: [PATCH 3/6] Support changing the output directory --- examples/headless/src/main.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/examples/headless/src/main.rs b/examples/headless/src/main.rs index d4dd7f1..2130930 100644 --- a/examples/headless/src/main.rs +++ b/examples/headless/src/main.rs @@ -1,4 +1,7 @@ -use std::num::NonZeroU32; +use std::{ + num::NonZeroU32, + path::{Path, PathBuf}, +}; use anyhow::{anyhow, bail, Context, Result}; use clap::{CommandFactory, Parser}; @@ -194,7 +197,12 @@ async fn render(mut scenes: SceneSet, index: usize, args: &Args) -> Result<()> { let mut writer = encoder.write_header()?; writer.write_image_data(&data)?; writer.finish()?; - std::fs::write("./test.png", result)?; + let out_path = args + .out_directory + .join(&example_scene.config.name) + .with_extension("png"); + std::fs::write(&out_path, result)?; + println!("Wrote result to {out_path:?}"); Ok(()) } @@ -212,9 +220,16 @@ struct Args { #[arg(long, short, global(false))] /// The time in seconds since the frame start, for animated scenes time: Option, + /// Directory to store the result into + #[arg(long, default_value_os_t = default_directory())] + pub out_directory: PathBuf, #[arg(long, short, global(false))] /// Display a list of all scene names print_scenes: bool, #[command(flatten)] args: scenes::Arguments, } + +fn default_directory() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("outputs") +} From 3a63f00e7ef3e9cd189fd58126f123a1c0aca644 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Wed, 8 Feb 2023 21:43:24 +0000 Subject: [PATCH 4/6] Fix handling of wgpu to hopefully be valid --- examples/headless/Cargo.toml | 1 + examples/headless/src/main.rs | 41 +++++++++++++++++++++++------------ 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/examples/headless/Cargo.toml b/examples/headless/Cargo.toml index 91ac4dc..2982c06 100644 --- a/examples/headless/Cargo.toml +++ b/examples/headless/Cargo.toml @@ -15,3 +15,4 @@ wgpu = { workspace = true } pollster = "0.2.5" env_logger = "0.10.0" png = "0.17.7" +futures-intrusive = "0.5.0" diff --git a/examples/headless/src/main.rs b/examples/headless/src/main.rs index 2130930..a0970fd 100644 --- a/examples/headless/src/main.rs +++ b/examples/headless/src/main.rs @@ -1,4 +1,5 @@ use std::{ + fs::File, num::NonZeroU32, path::{Path, PathBuf}, }; @@ -7,6 +8,7 @@ use anyhow::{anyhow, bail, Context, Result}; use clap::{CommandFactory, Parser}; use scenes::{SceneParams, SceneSet, SimpleText}; use vello::{ + block_on_wgpu, kurbo::{Affine, Vec2}, Scene, SceneBuilder, SceneFragment, }; @@ -156,14 +158,14 @@ async fn render(mut scenes: SceneSet, index: usize, args: &Args) -> Result<()> { .render_to_texture(&device, &queue, &scene, &view, width, height) .or_else(|_| bail!("Got non-Send/Sync error from rendering"))?; // (width * 4).next_multiple_of(256) - let padded_width = { + let padded_byte_width = { let w = width as u32 * 4; match w % 256 { 0 => w, r => w + (256 - r), } }; - let buffer_size = padded_width as u64 * height as u64; + let buffer_size = padded_byte_width as u64 * height as u64; let buffer = device.create_buffer(&BufferDescriptor { label: Some("val"), size: buffer_size, @@ -179,7 +181,7 @@ async fn render(mut scenes: SceneSet, index: usize, args: &Args) -> Result<()> { buffer: &buffer, layout: wgpu::ImageDataLayout { offset: 0, - bytes_per_row: NonZeroU32::new(padded_width), + bytes_per_row: NonZeroU32::new(padded_byte_width), rows_per_image: None, }, }, @@ -187,22 +189,33 @@ async fn render(mut scenes: SceneSet, index: usize, args: &Args) -> Result<()> { ); queue.submit([encoder.finish()]); let buf_slice = buffer.slice(..); - buf_slice.map_async(wgpu::MapMode::Read, |done| done.unwrap()); - device.poll(wgpu::MaintainBase::Wait); + + 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) = block_on_wgpu(&device, receiver.receive()) { + recv_result?; + } else { + bail!("channel was closed"); + } + let data = buf_slice.get_mapped_range(); - let mut result = Vec::::new(); - let mut encoder = png::Encoder::new(&mut result, padded_width / 4, height); - encoder.set_color(png::ColorType::Rgba); - encoder.set_depth(png::BitDepth::Eight); - let mut writer = encoder.write_header()?; - writer.write_image_data(&data)?; - writer.finish()?; + let mut result_unpadded = Vec::::with_capacity((width * height * 4).try_into()?); + for row in 0..height { + let start = (row * padded_byte_width).try_into()?; + result_unpadded.extend(&data[start..start + (width * 4) as usize]); + } let out_path = args .out_directory .join(&example_scene.config.name) .with_extension("png"); - std::fs::write(&out_path, result)?; - println!("Wrote result to {out_path:?}"); + let mut file = File::create(&out_path)?; + let mut encoder = png::Encoder::new(&mut file, width, height); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + let mut writer = encoder.write_header()?; + writer.write_image_data(&result_unpadded)?; + writer.finish()?; + println!("Wrote result ({width}x{height}) to {out_path:?}"); Ok(()) } From 19552ad819dda120a76f403ef1ab7339c9914d44 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Wed, 8 Feb 2023 22:48:34 +0000 Subject: [PATCH 5/6] Update to better match other examples --- examples/headless/Cargo.toml | 9 +++++++-- examples/headless/src/main.rs | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/examples/headless/Cargo.toml b/examples/headless/Cargo.toml index 2982c06..d14570b 100644 --- a/examples/headless/Cargo.toml +++ b/examples/headless/Cargo.toml @@ -1,7 +1,12 @@ [package] name = "headless" -version = "0.1.0" -edition = "2021" +description = "An example showing how to use `vello` to create raster images" +publish = false + +version.workspace = true +license.workspace = true +edition.workspace = true +repository.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/examples/headless/src/main.rs b/examples/headless/src/main.rs index a0970fd..b5c258e 100644 --- a/examples/headless/src/main.rs +++ b/examples/headless/src/main.rs @@ -220,7 +220,7 @@ async fn render(mut scenes: SceneSet, index: usize, args: &Args) -> Result<()> { } #[derive(Parser, Debug)] -#[command(about, long_about = None, bin_name="cargo run -p with_winit --")] +#[command(about, long_about = None, bin_name="cargo run -p headless --")] struct Args { #[arg(long, short, global(false))] x_resolution: Option, From 56939df615af8726906718e2888aea1841a51d54 Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Thu, 9 Feb 2023 09:34:53 +0000 Subject: [PATCH 6/6] Re-use render context in headless --- examples/headless/src/main.rs | 28 ++++++++++------------------ examples/scenes/src/svg.rs | 1 + src/util.rs | 2 +- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/examples/headless/src/main.rs b/examples/headless/src/main.rs index b5c258e..c82318e 100644 --- a/examples/headless/src/main.rs +++ b/examples/headless/src/main.rs @@ -10,11 +10,11 @@ use scenes::{SceneParams, SceneSet, SimpleText}; use vello::{ block_on_wgpu, kurbo::{Affine, Vec2}, + util::RenderContext, Scene, SceneBuilder, SceneFragment, }; use wgpu::{ - util::initialize_adapter_from_env_or_default, Backends, BufferDescriptor, BufferUsages, - CommandEncoderDescriptor, DeviceDescriptor, Extent3d, ImageCopyBuffer, Instance, Limits, + BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, ImageCopyBuffer, TextureDescriptor, TextureFormat, TextureUsages, }; @@ -77,23 +77,15 @@ fn main() -> Result<()> { } async fn render(mut scenes: SceneSet, index: usize, args: &Args) -> Result<()> { - let instance = Instance::new(wgpu::InstanceDescriptor { - backends: wgpu::Backends::PRIMARY, - dx12_shader_compiler: wgpu::Dx12Compiler::Fxc, - }); - let adapter = initialize_adapter_from_env_or_default(&instance, Backends::PRIMARY, None) + let mut context = RenderContext::new() + .or_else(|_| bail!("Got non-Send/Sync error from creating render context"))?; + let device_id = context + .device(None) .await - .ok_or_else(|| anyhow!("Failed to intiialise adapter"))?; - let (device, queue) = adapter - .request_device( - &DeviceDescriptor { - label: Some("Vello Headless"), - features: wgpu::Features::TIMESTAMP_QUERY | wgpu::Features::CLEAR_TEXTURE, - limits: Limits::default(), - }, - None, - ) - .await?; + .ok_or_else(|| anyhow!("No compatible device found"))?; + let device_handle = &mut context.devices[device_id]; + let device = &device_handle.device; + let queue = &device_handle.queue; let mut renderer = vello::Renderer::new(&device) .or_else(|_| bail!("Got non-Send/Sync error from creating renderer"))?; let mut fragment = SceneFragment::new(); diff --git a/examples/scenes/src/svg.rs b/examples/scenes/src/svg.rs index 56d7f1c..9f4f5c0 100644 --- a/examples/scenes/src/svg.rs +++ b/examples/scenes/src/svg.rs @@ -90,6 +90,7 @@ fn example_scene_of(file: PathBuf) -> ExampleScene { let mut builder = SceneBuilder::for_fragment(&mut new_scene); vello_svg::render_tree(&mut builder, &svg); let resolution = Vec2::new(svg.size.width(), svg.size.height()); + // TODO: Handle svg.view_box (new_scene, resolution) }); builder.append(&scene_frag, None); diff --git a/src/util.rs b/src/util.rs index 4d5b09c..ac1070e 100644 --- a/src/util.rs +++ b/src/util.rs @@ -84,7 +84,7 @@ impl RenderContext { } /// Finds or creates a compatible device handle id. - async fn device(&mut self, compatible_surface: Option<&Surface>) -> Option { + pub async fn device(&mut self, compatible_surface: Option<&Surface>) -> Option { let compatible = match compatible_surface { Some(s) => self .devices