use anyhow::{Result, anyhow};
use image::{ImageBuffer, ImageFormat, Rgba};
use vello::peniko::Color;
use vello::util::RenderContext;
use vello::wgpu::{
BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, MapMode, Origin3d,
PollType, TexelCopyBufferInfo, TexelCopyBufferLayout, TexelCopyTextureInfo, TextureAspect,
TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureViewDescriptor,
};
use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene};
pub fn rasterize_scene_to_png(
scene: &Scene,
width: u32,
height: u32,
base_color: Color,
path: &str,
) -> Result<()> {
let mut context = RenderContext::new();
let dev_id = pollster::block_on(context.device(None))
.ok_or_else(|| anyhow!("no wgpu device (set WGPU_BACKEND=vulkan on Asahi)"))?;
let device = &context.devices[dev_id].device;
let queue = &context.devices[dev_id].queue;
let target = device.create_texture(&TextureDescriptor {
label: Some("raster_target"),
size: Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
usage: TextureUsages::STORAGE_BINDING | TextureUsages::COPY_SRC,
view_formats: &[],
});
let target_view = target.create_view(&TextureViewDescriptor::default());
let mut renderer = Renderer::new(
device,
RendererOptions {
use_cpu: false,
antialiasing_support: AaSupport::area_only(),
num_init_threads: None,
pipeline_cache: None,
},
)
.map_err(|e| anyhow!("Renderer::new failed: {e:?}"))?;
renderer
.render_to_texture(
device,
queue,
scene,
&target_view,
&RenderParams {
base_color,
width,
height,
antialiasing_method: AaConfig::Area,
},
)
.map_err(|e| anyhow!("render_to_texture failed: {e:?}"))?;
let bytes_per_pixel = 4u32;
let unpadded = width * bytes_per_pixel;
let align = 256u32;
let padded = unpadded.div_ceil(align) * align;
let buffer = device.create_buffer(&BufferDescriptor {
label: Some("raster_readback"),
size: (padded * height) as u64,
usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor { label: None });
encoder.copy_texture_to_buffer(
TexelCopyTextureInfo {
texture: &target,
mip_level: 0,
origin: Origin3d::ZERO,
aspect: TextureAspect::All,
},
TexelCopyBufferInfo {
buffer: &buffer,
layout: TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded),
rows_per_image: Some(height),
},
},
Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
queue.submit([encoder.finish()]);
let slice = buffer.slice(..);
slice.map_async(MapMode::Read, |r| r.expect("map_async failed"));
let _ = device.poll(PollType::wait_indefinitely());
let data = slice.get_mapped_range();
let mut pixels = Vec::with_capacity((unpadded * height) as usize);
for y in 0..height {
let row = &data[(y * padded) as usize..(y * padded + unpadded) as usize];
pixels.extend_from_slice(row);
}
let img: ImageBuffer<Rgba<u8>, Vec<u8>> = ImageBuffer::from_raw(width, height, pixels)
.ok_or_else(|| anyhow!("pixel buffer size mismatch"))?;
img.save_with_format(path, ImageFormat::Png)?;
Ok(())
}