1use crate::gradient::RgbColor;
32use crate::renderer::Size;
33use crate::{Renderer, renderer};
34use std::sync;
35
36#[derive(Debug, thiserror::Error)]
37pub enum HeadlessCreationError {
38 #[error("Failed to create adapter")]
39 AdapterCreation(#[from] wgpu::RequestAdapterError),
40 #[error("Unable to request device")]
41 DeviceCreation(#[from] wgpu::RequestDeviceError),
42 #[error("Texture width must be a multiple of {alignment}, got {actual}")]
43 InvalidRowLength { alignment: u32, actual: u32 },
44}
45
46#[derive(Debug)]
47struct Context {
48 device: wgpu::Device,
49 queue: wgpu::Queue,
50}
51
52#[derive(Clone, Debug)]
57pub struct Bitmap {
58 pub size: Size,
60 pub buffer: Vec<u8>,
62}
63
64impl Context {
65 async fn new() -> Result<Self, HeadlessCreationError> {
66 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
67 backends: wgpu::Backends::all(),
68 ..Default::default()
69 });
70
71 let adapter = instance
72 .request_adapter(&wgpu::RequestAdapterOptions {
73 power_preference: wgpu::PowerPreference::default(),
74 compatible_surface: None,
75 force_fallback_adapter: false,
76 })
77 .await?;
78
79 let (device, queue) = adapter
80 .request_device(&wgpu::DeviceDescriptor {
81 required_features: wgpu::Features::empty(),
82 required_limits: wgpu::Limits::default(),
83 label: None,
84 memory_hints: Default::default(),
85 trace: wgpu::Trace::default(),
86 })
87 .await
88 .map_err(HeadlessCreationError::from)?;
89
90 Ok(Context { device, queue })
91 }
92}
93
94#[derive(Debug, thiserror::Error)]
95pub enum RenderError {
96 #[error("Failed to render intermediate texture")]
97 Intermediate(#[source] renderer::Error),
98 #[error("Error during post-processing step")]
99 Postprocessing(#[source] renderer::Error),
100 #[error("Error during readback")]
101 Readback(#[source] wgpu::BufferAsyncError),
102}
103
104#[derive(Debug)]
137pub struct PhosphorHeadless {
138 device: wgpu::Device,
139 queue: wgpu::Queue,
140 renderer: Renderer,
141 output_texture: wgpu::Texture,
142 output_buffer: wgpu::Buffer,
143}
144
145impl PhosphorHeadless {
146 pub async fn new(size: Size<u32>) -> Result<Self, HeadlessCreationError> {
159 let Context { device, queue } = Context::new().await?;
160
161 let bytes_per_pixel = 4;
162 let bytes_per_row = bytes_per_pixel * size.width;
163 if bytes_per_row % wgpu::COPY_BYTES_PER_ROW_ALIGNMENT != 0 {
164 return Err(HeadlessCreationError::InvalidRowLength {
165 alignment: wgpu::COPY_BYTES_PER_ROW_ALIGNMENT / bytes_per_pixel,
166 actual: size.width,
167 });
168 }
169
170 let renderer = Renderer::new(&device, &wgpu::TextureFormat::Rgba8UnormSrgb, size);
171
172 let (output_texture, output_buffer) = Self::create_output_buffer_and_texture(&device, size);
173
174 Ok(PhosphorHeadless {
175 device,
176 queue,
177 renderer,
178 output_buffer,
179 output_texture,
180 })
181 }
182
183 fn create_output_buffer_and_texture(
184 device: &wgpu::Device,
185 size: Size,
186 ) -> (wgpu::Texture, wgpu::Buffer) {
187 let output_texture = device.create_texture(&wgpu::TextureDescriptor {
189 size: wgpu::Extent3d {
190 width: size.width,
191 height: size.height,
192 depth_or_array_layers: 1,
193 },
194 mip_level_count: 1,
195 sample_count: 1,
196 dimension: wgpu::TextureDimension::D2,
197 format: wgpu::TextureFormat::Rgba8UnormSrgb,
198 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
199 label: Some("output_texture"),
200 view_formats: &[],
201 });
202
203 let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
205 size: (size.width * size.height * 4) as u64, usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
207 label: Some("output_buffer"),
208 mapped_at_creation: false,
209 });
210
211 (output_texture, output_buffer)
212 }
213
214 pub fn resize(&mut self, size: Size) -> renderer::Result<()> {
216 (self.output_texture, self.output_buffer) =
217 Self::create_output_buffer_and_texture(&self.device, size);
218
219 self.renderer.resize(size)
220 }
221
222 pub fn set_xlim(&mut self, left: f32, right: f32) {
224 self.renderer.set_xlim(left, right)
225 }
226
227 pub fn set_ylim(&mut self, lower: f32, upper: f32) {
229 self.renderer.set_ylim(lower, upper)
230 }
231
232 pub fn set_intensity(&mut self, value: f32) {
234 self.renderer.set_intensity(value)
235 }
236
237 pub fn set_gamma(&mut self, value: f32) {
239 self.renderer.set_gamma(value)
240 }
241
242 pub fn set_beam_width(&mut self, width: f32) {
244 self.renderer.set_beam_width(width)
245 }
246
247 pub fn set_waveform_yt(&mut self, waveform: &[f32]) {
249 self.renderer.set_waveform_yt(waveform)
250 }
251
252 pub fn set_waveform_xy(&mut self, waveform: &[[f32; 2]]) {
254 self.renderer.set_waveform_xy(waveform)
255 }
256
257 pub fn set_lut(&mut self, lut: impl IntoIterator<Item = (f32, RgbColor<f32>)>) {
259 self.renderer.set_lut(lut)
260 }
261
262 pub fn set_decay(&mut self, decay: renderer::Decay) {
264 self.renderer.set_decay(decay)
265 }
266}
267
268#[cfg(feature = "auto-intensity")]
270impl PhosphorHeadless {
271 pub fn auto_intensity(&mut self, value: bool) {
273 self.renderer.enable_auto_intensity(value)
274 }
275
276 pub fn reset_auto_intensity(&self, queue: &wgpu::Queue) -> renderer::Result<()> {
278 self.renderer.reset_auto_intensity(queue)
279 }
280}
281impl PhosphorHeadless {
282 pub fn render_to_buffer(&mut self) -> Result<Bitmap, RenderError> {
292 self.submit_render()?;
293 let buffer_owned: Vec<u8> = self.readback_buffer()?.to_vec();
294 Ok(Bitmap {
295 buffer: buffer_owned,
296 size: self.renderer.size(),
297 })
298 }
299
300 fn submit_render(&mut self) -> Result<(), RenderError> {
301 let intermediate_cmd = self
302 .renderer
303 .render_intermediate(&self.queue)
304 .map_err(RenderError::Intermediate)?;
305
306 let mut encoder = self
307 .device
308 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
309 label: Some("render_encoder"),
310 });
311
312 {
314 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
315 label: Some("render_pass"),
316 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
317 view: &self
318 .output_texture
319 .create_view(&wgpu::TextureViewDescriptor::default()),
320 resolve_target: None,
321 ops: wgpu::Operations {
322 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
323 store: wgpu::StoreOp::Store,
324 },
325 })],
326 depth_stencil_attachment: None,
327 timestamp_writes: None,
328 occlusion_query_set: None,
329 });
330
331 self.renderer
332 .render(&mut render_pass)
333 .map_err(RenderError::Postprocessing)?
334 }
335
336 encoder.copy_texture_to_buffer(
338 wgpu::TexelCopyTextureInfo {
339 texture: &self.output_texture,
340 mip_level: 0,
341 origin: wgpu::Origin3d::ZERO,
342 aspect: wgpu::TextureAspect::All,
343 },
344 wgpu::TexelCopyBufferInfo {
345 buffer: &self.output_buffer,
346 layout: wgpu::TexelCopyBufferLayout {
347 offset: 0,
348 bytes_per_row: Some(self.renderer.size().width * 4),
349 rows_per_image: Some(self.renderer.size().height),
350 },
351 },
352 wgpu::Extent3d {
353 width: self.renderer.size().width,
354 height: self.renderer.size().height,
355 depth_or_array_layers: 1,
356 },
357 );
358
359 self.queue.submit([intermediate_cmd, encoder.finish()]);
361 Ok(())
362 }
363
364 fn readback_buffer(&self) -> Result<Vec<u8>, RenderError> {
365 let data: Vec<_>;
366 {
367 let buffer_slice: wgpu::BufferSlice = self.output_buffer.slice(..);
368 let (sender, receiver) = sync::mpsc::channel();
369 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
370 sender.send(result).unwrap()
371 });
372
373 self.device.poll(wgpu::PollType::Wait).unwrap();
375 receiver
376 .try_recv()
377 .unwrap()
378 .map_err(RenderError::Readback)?;
379
380 data = buffer_slice.get_mapped_range().to_vec();
381 }
382
383 self.output_buffer.unmap();
384 Ok(data)
385 }
386}