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);
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.device.check_errors()?;
336 if purpose == ImagePurpose::Publication {
337 self.adaptive.set_publication(true);
338 }
339 self.sync_quality_tier();
340 self.chunk_residency.begin_epoch();
341 let scene_changed = self.scene_gpu.sync(crate::scene_gpu::SceneSync {
342 device: &self.device,
343 queue: &self.queue,
344 scene,
345 quality: self.tier() >= QualityTier::Standard,
346 extent: [self.width, self.height],
347 ray_query_layout: self.passes.ambient_occlusion.ray_query_layout(),
348 derived_cache: &mut self.derived_cache,
349 derived_frame: self.derived_frame,
350 })?;
351 self.chunk_residency.sync_scene(
352 &mut self.scene_gpu,
353 &self.device,
354 &self.queue,
355 &mut self.derived_cache,
356 self.derived_frame,
357 )?;
358 self.derived_frame = self.derived_frame.wrapping_add(1);
359 let rebuild = self.rebuild_pool_if_needed()?;
360 self.scene_gpu
361 .settle_specializations(&self.device, scene, &self.passes);
362 self.device.check_errors()?;
363 Ok(ImagePreparation {
364 scene_changed,
365 rebuild,
366 })
367 }
368
369 pub(super) fn record_image(
370 &self,
371 encoder: &mut D::CommandEncoder,
372 target: &D::TextureView,
373 queries: Option<&D::QuerySet>,
374 quality: bool,
375 timestamps_started: bool,
376 ) {
377 self.record_image_until(encoder, target, queries, quality, timestamps_started, None);
378 }
379
380 pub(super) fn record_image_until(
381 &self,
382 encoder: &mut D::CommandEncoder,
383 target: &D::TextureView,
384 queries: Option<&D::QuerySet>,
385 quality: bool,
386 timestamps_started: bool,
387 stop_after: Option<crate::graph::ResourceId>,
388 ) {
389 let Some(pool) = &self.pool else {
390 return;
391 };
392 let table = ResourceTable {
393 pool,
394 swapchain: target,
395 };
396 for (position, &index) in self.order.iter().enumerate() {
397 let Some(node) = self.pass_nodes.get(index) else {
398 continue;
399 };
400 let boundary = position == 0 || position + 1 == self.order.len();
401 (node.record)(&mut PassContext {
402 encoder,
403 resources: &table,
404 passes: &self.passes,
405 bindings: self.bindings.as_ref(),
406 scene: &self.scene_gpu,
407 timestamps: queries
408 .filter(|_| {
409 boundary && (!timestamps_started || position + 1 == self.order.len())
410 })
411 .map(|queries| molgfx_gpu::TimestampWrites {
412 queries,
413 beginning: (position == 0 && !timestamps_started).then_some(0),
414 end: (position + 1 == self.order.len()).then_some(1),
415 }),
416 temporal_write: self.temporal.write_index(),
417 quality,
418 display_encoding: self.display_encoding(),
419 });
420 if stop_after.is_some_and(|resource| node.writes.contains(&resource)) {
421 break;
422 }
423 }
424 }
425}
426
427pub(super) struct PendingImage<D: Device> {
428 config: ImageConfig,
429 layout: ImageLayout,
430 buffer: D::Buffer,
431 _texture: D::Texture,
432 completion: FenceValue,
433}
434
435impl<D: Device> PendingImage<D> {
436 #[cfg(not(target_arch = "wasm32"))]
437 pub(super) const fn completion(&self) -> FenceValue {
438 self.completion
439 }
440
441 #[cfg(not(target_arch = "wasm32"))]
442 pub(super) fn readback(&self) -> (&D::Buffer, u64) {
443 (&self.buffer, self.layout.buffer_size)
444 }
445
446 pub(super) fn resolve(
447 self,
448 mapped: Vec<u8>,
449 format: TextureFormat,
450 ) -> Result<Image, RenderError> {
451 let _completion = self.completion;
452 let mut pixels = self.layout.unpack(mapped)?;
453 if matches!(
454 format,
455 TextureFormat::Bgra8Unorm | TextureFormat::Bgra8UnormSrgb
456 ) {
457 for pixel in pixels.as_chunks_mut::<4>().0 {
458 pixel.swap(0, 2);
459 }
460 }
461 Ok(Image {
462 width: self.config.width,
463 height: self.config.height,
464 pixels,
465 })
466 }
467}
468
469#[cfg(all(test, not(target_arch = "wasm32")))]
470#[path = "image_tests.rs"]
471mod tests;