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 depth_cue: self.resolved_plan.packed_depth_cue(),
238 optics,
239 motion_blur: self
240 .resolved_plan
241 .motion_blur()
242 .map_or([0.0; 4], MotionBlur::packed),
243 atmosphere: self
244 .resolved_plan
245 .packed_presentation(self.scene_gpu.has_translucency()),
246 lighting: self.resolved_plan.packed_lighting(),
247 shadow_view: shadow.view,
248 shadow_projection: shadow.projection,
249 shadow_view_proj: shadow.view_projection,
250 },
251 );
252 self.scene_gpu
253 .write_frame_uniforms(&self.queue, &uniforms)?;
254 let mut encoder = self.device.create_command_encoder();
255 self.record_image_scene_updates(&mut encoder, cinematic);
256 self.record_image(&mut encoder, &view, None, cinematic, false);
257 if sample + 1 == samples {
258 encoder.copy_texture_to_buffer(
259 &texture,
260 (0, 0),
261 (config.width, config.height),
262 layout.padded_row,
263 0,
264 &readback,
265 );
266 }
267 completion = self.submit_image_sample(encoder, sample + 1 == samples);
268 }
269 if purpose == ImagePurpose::Publication {
270 self.temporal_scene_identity = None;
271 }
272 Ok(PendingImage {
273 config,
274 layout,
275 buffer: readback,
276 _texture: texture,
277 completion,
278 })
279 }
280
281 fn submit_image_sample(&self, encoder: D::CommandEncoder, final_sample: bool) -> FenceValue {
282 if final_sample {
283 self.queue.submit_tracked(encoder)
284 } else {
285 self.queue.submit(encoder);
286 FenceValue::default()
287 }
288 }
289
290 fn record_image_scene_updates(&mut self, encoder: &mut D::CommandEncoder, quality: bool) {
291 self.passes
292 .cull
293 .record_attribute_timelines(&self.scene_gpu, encoder);
294 self.passes
295 .cull
296 .record_instance_timelines(&self.scene_gpu, encoder);
297 let point_coordinates_changed = self
298 .passes
299 .cull
300 .record_point_timelines(&self.scene_gpu, encoder);
301 self.scene_gpu
302 .record_particle_motion(encoder, &self.passes.particle_motion);
303 let structure_coordinates_changed =
304 self.scene_gpu
305 .record_trajectories(encoder, &self.passes.trajectory, None);
306 let paged_coordinates_changed = self
307 .passes
308 .cull
309 .record_paged_trajectories(&self.scene_gpu, encoder);
310 self.scene_gpu.record_dynamic_relations(
311 encoder,
312 &self.passes.relation_resolve,
313 structure_coordinates_changed || paged_coordinates_changed || point_coordinates_changed,
314 );
315 self.scene_gpu
316 .record_occupancies(encoder, self.passes.occupancy.as_ref());
317 self.scene_gpu.record_surface_fields(
318 encoder,
319 &self.passes.surface_field,
320 &self.passes.surface_components,
321 );
322 self.scene_gpu.record_quality_hardware(encoder, quality);
323 }
324
325 pub(super) fn prepare_image(
332 &mut self,
333 scene: &Scene,
334 purpose: ImagePurpose,
335 ) -> Result<ImagePreparation, RenderError> {
336 self.ensure_occupancy(scene)?;
337 self.device.check_errors()?;
338 if purpose == ImagePurpose::Publication {
339 self.adaptive.set_publication(true);
340 }
341 self.adaptive.set_atom_count(scene.atom_count());
342 self.sync_quality_tier();
343 self.chunk_residency.begin_epoch();
344 let scene_changed = self.scene_gpu.sync(crate::scene_gpu::SceneSync {
345 device: &self.device,
346 queue: &self.queue,
347 scene,
348 quality: self.tier() >= QualityTier::Standard,
349 detail: self.tier().detail(),
350 extent: [self.width, self.height],
351 ray_query_layout: self.passes.ambient_occlusion.ray_query_layout(),
352 derived_cache: &mut self.derived_cache,
353 derived_frame: self.derived_frame,
354 })?;
355 self.chunk_residency.sync_scene(
356 &mut self.scene_gpu,
357 &self.device,
358 &self.queue,
359 &mut self.derived_cache,
360 self.derived_frame,
361 )?;
362 self.derived_frame = self.derived_frame.wrapping_add(1);
363 let rebuild = self.rebuild_pool_if_needed()?;
364 self.scene_gpu
365 .settle_specializations(&self.device, scene, &self.passes);
366 self.device.check_errors()?;
367 Ok(ImagePreparation {
368 scene_changed,
369 rebuild,
370 })
371 }
372
373 pub(super) fn record_image(
374 &self,
375 encoder: &mut D::CommandEncoder,
376 target: &D::TextureView,
377 queries: Option<&D::QuerySet>,
378 quality: bool,
379 timestamps_started: bool,
380 ) {
381 self.record_image_until(encoder, target, queries, quality, timestamps_started, None);
382 }
383
384 pub(super) fn record_image_until(
385 &self,
386 encoder: &mut D::CommandEncoder,
387 target: &D::TextureView,
388 queries: Option<&D::QuerySet>,
389 quality: bool,
390 timestamps_started: bool,
391 stop_after: Option<crate::graph::ResourceId>,
392 ) {
393 let Some(pool) = &self.pool else {
394 return;
395 };
396 let table = ResourceTable {
397 pool,
398 swapchain: target,
399 };
400 for (position, &index) in self.order.iter().enumerate() {
401 let Some(node) = self.pass_nodes.get(index) else {
402 continue;
403 };
404 let boundary = position == 0 || position + 1 == self.order.len();
405 (node.record)(&mut PassContext {
406 encoder,
407 resources: &table,
408 passes: &self.passes,
409 bindings: self.bindings.as_ref(),
410 scene: &self.scene_gpu,
411 timestamps: queries
412 .filter(|_| {
413 boundary && (!timestamps_started || position + 1 == self.order.len())
414 })
415 .map(|queries| molgfx_gpu::TimestampWrites {
416 queries,
417 beginning: (position == 0 && !timestamps_started).then_some(0),
418 end: (position + 1 == self.order.len()).then_some(1),
419 }),
420 temporal_write: self.temporal.write_index(),
421 quality,
422 edge_smoothing: self.edge_smoothing(),
423 display_encoding: self.display_encoding(),
424 });
425 if stop_after.is_some_and(|resource| node.writes.contains(&resource)) {
426 break;
427 }
428 }
429 }
430}
431
432pub(super) struct PendingImage<D: Device> {
433 config: ImageConfig,
434 layout: ImageLayout,
435 buffer: D::Buffer,
436 _texture: D::Texture,
437 completion: FenceValue,
438}
439
440impl<D: Device> PendingImage<D> {
441 #[cfg(not(target_arch = "wasm32"))]
442 pub(super) const fn completion(&self) -> FenceValue {
443 self.completion
444 }
445
446 #[cfg(not(target_arch = "wasm32"))]
447 pub(super) fn readback(&self) -> (&D::Buffer, u64) {
448 (&self.buffer, self.layout.buffer_size)
449 }
450
451 pub(super) fn resolve(
452 self,
453 mapped: Vec<u8>,
454 format: TextureFormat,
455 ) -> Result<Image, RenderError> {
456 let _completion = self.completion;
457 let mut pixels = self.layout.unpack(mapped)?;
458 if matches!(
459 format,
460 TextureFormat::Bgra8Unorm | TextureFormat::Bgra8UnormSrgb
461 ) {
462 for pixel in pixels.as_chunks_mut::<4>().0 {
463 pixel.swap(0, 2);
464 }
465 }
466 Ok(Image {
467 width: self.config.width,
468 height: self.config.height,
469 pixels,
470 })
471 }
472}
473
474#[cfg(all(test, not(target_arch = "wasm32")))]
475#[path = "image_tests.rs"]
476mod tests;