fenestra_shell/
headless.rs1use std::num::NonZeroUsize;
5
6use image::RgbaImage;
7use vello::peniko::Color;
8use vello::util::RenderContext;
9use vello::wgpu::{
10 self, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, TexelCopyBufferInfo,
11 TexelCopyBufferLayout, TextureDescriptor, TextureFormat, TextureUsages,
12};
13use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene};
14
15use crate::ShellError;
16
17pub struct Headless {
20 context: RenderContext,
21 dev_id: usize,
22 renderer: Renderer,
23 max_dim: u32,
24}
25
26impl Headless {
27 pub fn new() -> Result<Self, ShellError> {
30 let mut context = RenderContext::new();
31 let dev_id = pollster::block_on(context.device(None)).ok_or(ShellError::NoDevice)?;
32 let device = &context.devices[dev_id].device;
33 let max_dim = device.limits().max_texture_dimension_2d;
34 let renderer = Renderer::new(
35 device,
36 RendererOptions {
37 use_cpu: false,
38 antialiasing_support: AaSupport::area_only(),
39 num_init_threads: NonZeroUsize::new(1),
40 pipeline_cache: None,
41 },
42 )
43 .map_err(ShellError::Vello)?;
44 Ok(Self {
45 context,
46 dev_id,
47 renderer,
48 max_dim,
49 })
50 }
51
52 pub fn max_dimension(&self) -> u32 {
55 self.max_dim
56 }
57
58 pub fn clamp_size(&self, width: u32, height: u32) -> (u32, u32) {
61 (width.clamp(1, self.max_dim), height.clamp(1, self.max_dim))
62 }
63
64 pub fn render(
69 &mut self,
70 scene: &Scene,
71 width: u32,
72 height: u32,
73 base_color: Color,
74 ) -> Result<RgbaImage, ShellError> {
75 let (width, height) = self.clamp_size(width, height);
76 let handle = &self.context.devices[self.dev_id];
77 let (device, queue) = (&handle.device, &handle.queue);
78
79 let size = Extent3d {
80 width,
81 height,
82 depth_or_array_layers: 1,
83 };
84 let target = device.create_texture(&TextureDescriptor {
85 label: Some("fenestra headless target"),
86 size,
87 mip_level_count: 1,
88 sample_count: 1,
89 dimension: wgpu::TextureDimension::D2,
90 format: TextureFormat::Rgba8Unorm,
91 usage: TextureUsages::STORAGE_BINDING | TextureUsages::COPY_SRC,
92 view_formats: &[],
93 });
94 let view = target.create_view(&wgpu::TextureViewDescriptor::default());
95 self.renderer
96 .render_to_texture(
97 device,
98 queue,
99 scene,
100 &view,
101 &RenderParams {
102 base_color,
103 width,
104 height,
105 antialiasing_method: AaConfig::Area,
106 },
107 )
108 .map_err(ShellError::Vello)?;
109
110 let padded_byte_width = (width * 4).next_multiple_of(256);
112 let buffer = device.create_buffer(&BufferDescriptor {
113 label: Some("fenestra headless readback"),
114 size: u64::from(padded_byte_width) * u64::from(height),
115 usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
116 mapped_at_creation: false,
117 });
118 let mut encoder = device.create_command_encoder(&CommandEncoderDescriptor {
119 label: Some("fenestra headless copy"),
120 });
121 encoder.copy_texture_to_buffer(
122 target.as_image_copy(),
123 TexelCopyBufferInfo {
124 buffer: &buffer,
125 layout: TexelCopyBufferLayout {
126 offset: 0,
127 bytes_per_row: Some(padded_byte_width),
128 rows_per_image: None,
129 },
130 },
131 size,
132 );
133 queue.submit([encoder.finish()]);
134
135 let slice = buffer.slice(..);
136 let (tx, rx) = std::sync::mpsc::channel();
137 slice.map_async(wgpu::MapMode::Read, move |result| {
138 let _ = tx.send(result);
139 });
140 device
141 .poll(wgpu::PollType::wait_indefinitely())
142 .map_err(|_| ShellError::Readback)?;
143 rx.recv()
144 .map_err(|_| ShellError::Readback)?
145 .map_err(|_| ShellError::Readback)?;
146
147 let data = slice.get_mapped_range();
148 let mut pixels = Vec::with_capacity((width * height * 4) as usize);
149 for row in 0..height {
150 let start = (row * padded_byte_width) as usize;
151 pixels.extend_from_slice(&data[start..start + (width * 4) as usize]);
152 }
153 drop(data);
154 buffer.unmap();
155
156 Ok(RgbaImage::from_raw(width, height, pixels)
157 .expect("readback buffer matches image dimensions"))
158 }
159}