1use super::{Engine, MotionBlur, QualityTier, TemporalOptions};
3use crate::error::RenderError;
4use crate::graph::{PassContext, ResourceTable};
5use molgfx_core::Scene;
6use molgfx_gpu::{
7 BufferDesc, BufferUsage, CommandEncoder as _, Device, FenceValue, Queue as _, TextureDesc,
8 TextureFormat, TextureUsage, TextureViewDesc,
9};
10use molgfx_math::Camera;
11#[path = "image/layout.rs"]
12mod layout;
13pub(super) use layout::ImageLayout;
14pub(crate) const PUBLICATION_IMAGE_SAMPLES: u32 = 64;
16#[derive(Clone, Copy, PartialEq, Eq)]
17pub(crate) enum ImagePurpose {
18 Publication,
19 #[cfg(not(target_arch = "wasm32"))]
20 SequenceFrame,
21}
22#[derive(Clone, Copy)]
23pub(super) struct ImagePreparation {
24 pub(super) scene_changed: bool,
25 pub(super) rebuild: bool,
26}
27#[derive(Clone, Copy, PartialEq, Eq, Debug)]
29pub struct ImageConfig {
30 pub width: u32,
32 pub height: u32,
34}
35impl ImageConfig {
36 #[must_use]
38 pub const fn publication_4k() -> Self {
39 Self {
40 width: 3840,
41 height: 2160,
42 }
43 }
44
45 pub(super) fn validate(self, max_dimension: u32) -> Result<(), RenderError> {
46 if self.width == 0
47 || self.height == 0
48 || self.width > max_dimension
49 || self.height > max_dimension
50 {
51 return Err(RenderError::InvalidImageSize);
52 }
53 Ok(())
54 }
55}
56
57#[derive(Clone, PartialEq, Eq, Debug)]
59pub struct Image {
60 pub width: u32,
62 pub height: u32,
64 pub pixels: Vec<u8>,
66}
67
68impl Image {
69 pub fn png_bytes(&self) -> Result<Vec<u8>, RenderError> {
81 let mut bytes = Vec::new();
82 self.write_png(&mut bytes)?;
83 Ok(bytes)
84 }
85
86 pub fn write_png(&self, output: impl std::io::Write) -> Result<(), RenderError> {
93 self.validate_pixels()?;
94 let mut encoder = png::Encoder::new(output, self.width, self.height);
95 encoder.set_color(png::ColorType::Rgba);
96 encoder.set_depth(png::BitDepth::Eight);
97 let mut writer = encoder
98 .write_header()
99 .map_err(|error| RenderError::ImageEncoding {
100 summary: error.to_string(),
101 })?;
102 writer
103 .write_image_data(&self.pixels)
104 .map_err(|error| RenderError::ImageEncoding {
105 summary: error.to_string(),
106 })?;
107 writer.finish().map_err(|error| RenderError::ImageEncoding {
108 summary: error.to_string(),
109 })
110 }
111
112 fn validate_pixels(&self) -> Result<(), RenderError> {
113 let expected = usize::try_from(self.width)
114 .ok()
115 .and_then(|width| width.checked_mul(usize::try_from(self.height).ok()?))
116 .and_then(|pixels| pixels.checked_mul(4))
117 .ok_or(RenderError::InvalidImageSize)?;
118 if self.pixels.len() != expected {
119 return Err(RenderError::ImageEncoding {
120 summary: "RGBA8 pixel buffer length does not match image dimensions".to_owned(),
121 });
122 }
123 Ok(())
124 }
125}
126
127impl<D: Device> Engine<D> {
128 pub async fn render_image_async(
137 &mut self,
138 scene: &Scene,
139 camera: &Camera,
140 config: ImageConfig,
141 ) -> Result<Image, RenderError> {
142 let pending =
143 self.render_image_to_buffer(scene, camera, config, ImagePurpose::Publication)?;
144 let mapped = self
145 .queue
146 .read_buffer_async(&self.device, &pending.buffer, 0, pending.layout.buffer_size)
147 .await?;
148 pending.resolve(mapped, self.target_format)
149 }
150
151 #[cfg(not(target_arch = "wasm32"))]
158 pub fn render_image(
159 &mut self,
160 scene: &Scene,
161 camera: &Camera,
162 config: ImageConfig,
163 ) -> Result<Image, RenderError> {
164 let pending =
165 self.render_image_to_buffer(scene, camera, config, ImagePurpose::Publication)?;
166 let mapped = self.queue.read_buffer_blocking(
167 &self.device,
168 &pending.buffer,
169 0,
170 pending.layout.buffer_size,
171 )?;
172 pending.resolve(mapped, self.target_format)
173 }
174
175 pub(super) fn render_image_to_buffer(
176 &mut self,
177 scene: &Scene,
178 camera: &Camera,
179 config: ImageConfig,
180 purpose: ImagePurpose,
181 ) -> Result<PendingImage<D>, RenderError> {
182 config.validate(self.device.capabilities().max_texture_dim)?;
183 let layout = ImageLayout::new(config, 4)?;
184 self.width = config.width;
185 self.height = config.height;
186 let preparation = self.prepare_image(scene, purpose)?;
187 let identity = scene.cache_identity();
188 let scene_reset = self.temporal_scene_identity.replace(identity) != Some(identity);
189 if purpose == ImagePurpose::Publication {
190 self.temporal.reset();
191 }
192 let optics = self.resolve_optics(scene, camera)?;
193 let texture = self.device.create_texture(&TextureDesc {
194 label: "off-screen image",
195 width: config.width,
196 height: config.height,
197 depth: 1,
198 dimension: molgfx_gpu::TextureDimension::D2,
199 format: self.target_format,
200 usage: TextureUsage::RENDER_ATTACHMENT.union(TextureUsage::COPY_SRC),
201 })?;
202 let view = self
203 .device
204 .create_texture_view(&texture, &TextureViewDesc::default());
205 let readback = self.device.create_buffer(&BufferDesc {
206 label: "off-screen readback",
207 size: layout.buffer_size,
208 usage: BufferUsage::COPY_DST.union(BufferUsage::MAP_READ),
209 })?;
210 let samples = match purpose {
211 #[cfg(not(target_arch = "wasm32"))]
213 ImagePurpose::SequenceFrame => 1,
214 ImagePurpose::Publication => self.tier().image_samples(),
215 };
216 let shadow = self.shadow_bound.fit(
217 scene,
218 camera,
219 self.resolved_plan.lighting(),
220 preparation.scene_changed || preparation.rebuild,
221 );
222 let mut completion = FenceValue::default();
223 for sample in 0..samples {
224 self.scene_gpu.begin_frame();
225 let cinematic = self.tier() >= QualityTier::Standard;
226 let uniforms = self.temporal.prepare(
227 camera,
228 &TemporalOptions {
229 extent: [self.width, self.height],
230 reset: (purpose == ImagePurpose::Publication
231 || scene_reset
232 || preparation.rebuild)
233 && sample == 0,
234 quality: cinematic,
235 publication: purpose == ImagePurpose::Publication,
236 illustration: self.resolved_plan.illustration(),
237 optics,
238 motion_blur: self
239 .resolved_plan
240 .motion_blur()
241 .map_or([0.0; 4], MotionBlur::packed),
242 atmosphere: self
243 .resolved_plan
244 .packed_presentation(self.scene_gpu.has_translucency()),
245 lighting: self.resolved_plan.packed_lighting(),
246 shadow_view: shadow.view,
247 shadow_projection: shadow.projection,
248 shadow_view_proj: shadow.view_projection,
249 },
250 );
251 self.scene_gpu
252 .write_frame_uniforms(&self.queue, &uniforms)?;
253 let mut encoder = self.device.create_command_encoder();
254 self.record_image_scene_updates(&mut encoder, cinematic);
255 self.record_image(&mut encoder, &view, None, cinematic, false);
256 if sample + 1 == samples {
257 encoder.copy_texture_to_buffer(
258 &texture,
259 (0, 0),
260 (config.width, config.height),
261 layout.padded_row,
262 0,
263 &readback,
264 );
265 }
266 completion = self.submit_image_sample(encoder, sample + 1 == samples);
267 }
268 if purpose == ImagePurpose::Publication {
269 self.temporal_scene_identity = None;
270 }
271 Ok(PendingImage {
272 config,
273 layout,
274 buffer: readback,
275 _texture: texture,
276 completion,
277 })
278 }
279
280 fn submit_image_sample(&self, encoder: D::CommandEncoder, final_sample: bool) -> FenceValue {
281 if final_sample {
282 self.queue.submit_tracked(encoder)
283 } else {
284 self.queue.submit(encoder);
285 FenceValue::default()
286 }
287 }
288
289 fn record_image_scene_updates(&mut self, encoder: &mut D::CommandEncoder, quality: bool) {
290 self.passes
291 .cull
292 .record_attribute_timelines(&self.scene_gpu, encoder);
293 self.passes
294 .cull
295 .record_instance_timelines(&self.scene_gpu, encoder);
296 let point_coordinates_changed = self
297 .passes
298 .cull
299 .record_point_timelines(&self.scene_gpu, encoder);
300 self.scene_gpu
301 .record_particle_motion(encoder, &self.passes.particle_motion);
302 let structure_coordinates_changed =
303 self.scene_gpu
304 .record_trajectories(encoder, &self.passes.trajectory, None);
305 let paged_coordinates_changed = self
306 .passes
307 .cull
308 .record_paged_trajectories(&self.scene_gpu, encoder);
309 self.scene_gpu.record_dynamic_relations(
310 encoder,
311 &self.passes.relation_resolve,
312 structure_coordinates_changed || paged_coordinates_changed || point_coordinates_changed,
313 );
314 self.scene_gpu
315 .record_occupancies(encoder, self.passes.occupancy.as_ref());
316 self.scene_gpu.record_surface_fields(
317 encoder,
318 &self.passes.surface_field,
319 &self.passes.surface_components,
320 );
321 self.scene_gpu.record_quality_hardware(encoder, quality);
322 }
323
324 pub(super) fn prepare_image(
331 &mut self,
332 scene: &Scene,
333 purpose: ImagePurpose,
334 ) -> Result<ImagePreparation, RenderError> {
335 self.ensure_occupancy(scene)?;
336 self.device.check_errors()?;
337 if purpose == ImagePurpose::Publication {
338 self.adaptive.set_publication(true);
339 }
340 self.sync_quality_tier();
341 self.chunk_residency.begin_epoch();
342 let scene_changed = self.scene_gpu.sync(crate::scene_gpu::SceneSync {
343 device: &self.device,
344 queue: &self.queue,
345 scene,
346 quality: self.tier() >= QualityTier::Standard,
347 extent: [self.width, self.height],
348 ray_query_layout: self.passes.ambient_occlusion.ray_query_layout(),
349 derived_cache: &mut self.derived_cache,
350 derived_frame: self.derived_frame,
351 })?;
352 self.chunk_residency.sync_scene(
353 &mut self.scene_gpu,
354 &self.device,
355 &self.queue,
356 &mut self.derived_cache,
357 self.derived_frame,
358 )?;
359 self.derived_frame = self.derived_frame.wrapping_add(1);
360 let rebuild = self.rebuild_pool_if_needed()?;
361 self.scene_gpu
362 .settle_specializations(&self.device, scene, &self.passes);
363 self.device.check_errors()?;
364 Ok(ImagePreparation {
365 scene_changed,
366 rebuild,
367 })
368 }
369
370 pub(super) fn record_image(
371 &self,
372 encoder: &mut D::CommandEncoder,
373 target: &D::TextureView,
374 queries: Option<&D::QuerySet>,
375 quality: bool,
376 timestamps_started: bool,
377 ) {
378 self.record_image_until(encoder, target, queries, quality, timestamps_started, None);
379 }
380
381 pub(super) fn record_image_until(
382 &self,
383 encoder: &mut D::CommandEncoder,
384 target: &D::TextureView,
385 queries: Option<&D::QuerySet>,
386 quality: bool,
387 timestamps_started: bool,
388 stop_after: Option<crate::graph::ResourceId>,
389 ) {
390 let Some(pool) = &self.pool else {
391 return;
392 };
393 let table = ResourceTable {
394 pool,
395 swapchain: target,
396 };
397 for (position, &index) in self.order.iter().enumerate() {
398 let Some(node) = self.pass_nodes.get(index) else {
399 continue;
400 };
401 let boundary = position == 0 || position + 1 == self.order.len();
402 (node.record)(&mut PassContext {
403 encoder,
404 resources: &table,
405 passes: &self.passes,
406 bindings: self.bindings.as_ref(),
407 scene: &self.scene_gpu,
408 timestamps: queries
409 .filter(|_| {
410 boundary && (!timestamps_started || position + 1 == self.order.len())
411 })
412 .map(|queries| molgfx_gpu::TimestampWrites {
413 queries,
414 beginning: (position == 0 && !timestamps_started).then_some(0),
415 end: (position + 1 == self.order.len()).then_some(1),
416 }),
417 temporal_write: self.temporal.write_index(),
418 quality,
419 display_encoding: self.display_encoding(),
420 });
421 if stop_after.is_some_and(|resource| node.writes.contains(&resource)) {
422 break;
423 }
424 }
425 }
426}
427
428pub(super) struct PendingImage<D: Device> {
429 config: ImageConfig,
430 layout: ImageLayout,
431 buffer: D::Buffer,
432 _texture: D::Texture,
433 completion: FenceValue,
434}
435
436impl<D: Device> PendingImage<D> {
437 #[cfg(not(target_arch = "wasm32"))]
438 pub(super) const fn completion(&self) -> FenceValue {
439 self.completion
440 }
441
442 #[cfg(not(target_arch = "wasm32"))]
443 pub(super) fn readback(&self) -> (&D::Buffer, u64) {
444 (&self.buffer, self.layout.buffer_size)
445 }
446
447 pub(super) fn resolve(
448 self,
449 mapped: Vec<u8>,
450 format: TextureFormat,
451 ) -> Result<Image, RenderError> {
452 let _completion = self.completion;
453 let mut pixels = self.layout.unpack(mapped)?;
454 if matches!(
455 format,
456 TextureFormat::Bgra8Unorm | TextureFormat::Bgra8UnormSrgb
457 ) {
458 for pixel in pixels.as_chunks_mut::<4>().0 {
459 pixel.swap(0, 2);
460 }
461 }
462 Ok(Image {
463 width: self.config.width,
464 height: self.config.height,
465 pixels,
466 })
467 }
468}
469
470#[cfg(all(test, not(target_arch = "wasm32")))]
471#[path = "image_tests.rs"]
472mod tests;