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