Skip to main content

molgfx_render/engine/
frame.rs

1//! The frame loop: sync, build, record, one submission, present.
2//!
3//! CPU cost per frame is proportional to the active passes, the placed
4//! structures, the resident semantic tables and the representation states —
5//! never to atom or bond count, because per-primitive work lives on the GPU.
6//! A structure or representation whose revisions are unchanged costs one
7//! comparison and no upload, so a still scene walks a short fixed list rather
8//! than touching its contents. Nothing here allocates on the steady-state path
9//! beyond the first frame's pool construction.
10
11use super::{
12    Engine, FrameCompleteness, FrameDegradation, FrameMetrics, FrameReport, FrameStatus,
13    MotionBlur, QualityTier, RenderMode, TemporalOptions,
14};
15use crate::error::RenderError;
16use crate::graph::{DisplayEncoding, PassContext, ResourceTable, TransientPool, plan_aliases};
17use crate::passes::FrameBindings;
18use molgfx_core::Scene;
19use molgfx_gpu::Queue as _;
20use molgfx_gpu::{Device, Surface as _, SurfaceError, SurfaceFrame as _};
21use molgfx_math::Camera;
22// Uses the host monotonic clock on both native and browser targets, matching
23// the profiling path so the two frame-time sources stay comparable.
24use web_time::Instant;
25
26impl<D: Device> Engine<D> {
27    /// Uploads everything the scene changed since the previous frame.
28    ///
29    /// Returns whether any of it changed, and restarts temporal accumulation
30    /// when it did or while uploads are still in flight.
31    fn sync_scene(&mut self, scene: &Scene) -> Result<bool, RenderError> {
32        self.ensure_occupancy(scene)?;
33        let scene_changed = self.scene_gpu.sync(crate::scene_gpu::SceneSync {
34            device: &self.device,
35            queue: &self.queue,
36            scene,
37            quality: self.tier() >= QualityTier::Standard,
38            extent: [self.width, self.height],
39            ray_query_layout: self.passes.ambient_occlusion.ray_query_layout(),
40            derived_cache: &mut self.derived_cache,
41            derived_frame: self.derived_frame,
42        })?;
43        self.chunk_residency.sync_scene(
44            &mut self.scene_gpu,
45            &self.device,
46            &self.queue,
47            &mut self.derived_cache,
48            self.derived_frame,
49        )?;
50        self.derived_frame = self.derived_frame.wrapping_add(1);
51        if scene_changed || self.chunk_residency.metrics().uploads.active_tickets != 0 {
52            self.temporal.invalidate_convergence();
53        }
54        Ok(scene_changed)
55    }
56
57    /// Renders one frame of the scene to the presentation surface.
58    ///
59    /// A lost or outdated surface reconfigures and returns
60    /// [`FrameStatus::Skipped`]; the next frame recovers. Nothing panics on
61    /// conditions a caller can hit.
62    ///
63    /// # Errors
64    ///
65    /// Device loss beyond surface recovery, or graph reconstruction
66    /// failures.
67    pub fn render(&mut self, scene: &Scene, camera: &Camera) -> Result<FrameReport, RenderError> {
68        let frame_start = Instant::now();
69        self.sync_quality_tier();
70        self.device.check_errors()?;
71        self.chunk_residency.begin_epoch();
72        self.scene_gpu.begin_frame();
73        // Sync: upload only what changed since the last frame.
74        let scene_changed = self.sync_scene(scene)?;
75
76        // Build: (re)allocate the transient pool when the size changed.
77        let rebuild = self.rebuild_pool_if_needed()?;
78        // Settle: every generated pipeline this frame will draw is resolved
79        // before any pass opens a render pass, so recording only reads what
80        // this phase compiled.
81        self.scene_gpu
82            .settle_specializations(&self.device, scene, &self.passes);
83        let camera_changed = self.temporal.camera_changed(camera);
84        let identity = scene.cache_identity();
85        let scene_reset = self.temporal_scene_identity.replace(identity) != Some(identity);
86        let cinematic = self.tier() >= QualityTier::Standard;
87        let optics = self.resolve_optics(scene, camera)?;
88        let shadow =
89            self.shadow_bound
90                .fit(scene, camera, self.resolved_plan.lighting(), scene_changed);
91        let uniforms = self.temporal.prepare(
92            camera,
93            &TemporalOptions {
94                extent: [self.width, self.height],
95                reset: scene_reset
96                    || rebuild
97                    || (self.mode == RenderMode::Cinematic && camera_changed),
98                quality: cinematic,
99                publication: false,
100                illustration: self.resolved_plan.illustration(),
101                optics,
102                motion_blur: self
103                    .resolved_plan
104                    .motion_blur()
105                    .map_or([0.0; 4], MotionBlur::packed),
106                atmosphere: self
107                    .resolved_plan
108                    .packed_presentation(self.scene_gpu.has_translucency()),
109                lighting: self.resolved_plan.packed_lighting(),
110                shadow_view: shadow.view,
111                shadow_projection: shadow.projection,
112                shadow_view_proj: shadow.view_projection,
113            },
114        );
115        self.scene_gpu
116            .write_frame_uniforms(&self.queue, &uniforms)?;
117        let frame = self.acquire_surface_frame()?;
118        let Some(frame) = frame else {
119            // Off-screen targets arrive with the image-render path.
120            return Ok(self.frame_report(FrameStatus::Skipped));
121        };
122        let temporal_write = self.temporal.write_index();
123
124        // Record every pass in schedule order into one encoder.
125        let mut encoder = self.device.create_command_encoder();
126        self.scene_gpu
127            .settle_specializations(&self.device, scene, &self.passes);
128        self.record_scene_compute(&mut encoder, cinematic);
129        let Some(pool) = &self.pool else {
130            return Ok(self.frame_report(FrameStatus::Skipped));
131        };
132        {
133            let table = ResourceTable {
134                pool,
135                swapchain: frame.view(),
136            };
137            for &index in &self.order {
138                let Some(node) = self.pass_nodes.get(index) else {
139                    continue;
140                };
141                let mut ctx = PassContext {
142                    encoder: &mut encoder,
143                    resources: &table,
144                    passes: &self.passes,
145                    bindings: self.bindings.as_ref(),
146                    scene: &self.scene_gpu,
147                    timestamps: None,
148                    temporal_write,
149                    quality: cinematic,
150                    display_encoding: self.display_encoding(),
151                };
152                (node.record)(&mut ctx);
153            }
154        }
155
156        // One submission, then present.
157        self.queue.submit(encoder);
158        self.device.check_errors()?;
159        frame.present();
160        // The report describes the frame that was just rendered, so it is
161        // captured before the loop advances.
162        let report = self.frame_report(FrameStatus::Presented);
163        // Close the loop: this frame's real duration decides the tier the next
164        // frame builds at. That frame republishes the tier at its top, which is
165        // also where a move restarts accumulation.
166        // Kept in u64 throughout: a frame that outruns u64 nanoseconds is
167        // already slower than any tier can act on, so the arithmetic saturates.
168        let frame_time = frame_start.elapsed();
169        let elapsed = frame_time
170            .as_secs()
171            .saturating_mul(1_000_000_000)
172            .saturating_add(u64::from(frame_time.subsec_nanos()));
173        self.adaptive.observe(elapsed);
174        Ok(report)
175    }
176
177    fn record_scene_compute(&mut self, encoder: &mut D::CommandEncoder, cinematic: bool) {
178        self.passes
179            .cull
180            .record_attribute_timelines(&self.scene_gpu, encoder);
181        self.passes
182            .cull
183            .record_instance_timelines(&self.scene_gpu, encoder);
184        let point_coordinates_changed = self
185            .passes
186            .cull
187            .record_point_timelines(&self.scene_gpu, encoder);
188        self.scene_gpu
189            .record_particle_motion(encoder, &self.passes.particle_motion);
190        let structure_coordinates_changed =
191            self.scene_gpu
192                .record_trajectories(encoder, &self.passes.trajectory, None);
193        let paged_coordinates_changed = self
194            .passes
195            .cull
196            .record_paged_trajectories(&self.scene_gpu, encoder);
197        self.scene_gpu.record_dynamic_relations(
198            encoder,
199            &self.passes.relation_resolve,
200            structure_coordinates_changed || paged_coordinates_changed || point_coordinates_changed,
201        );
202        self.scene_gpu
203            .record_occupancies(encoder, self.passes.occupancy.as_ref());
204        self.scene_gpu.record_surface_fields(
205            encoder,
206            &self.passes.surface_field,
207            &self.passes.surface_components,
208        );
209        self.scene_gpu.record_quality_hardware(encoder, cinematic);
210    }
211
212    fn frame_report(&self, status: FrameStatus) -> FrameReport {
213        let residency = self.chunk_residency.metrics();
214        let derived = self.derived_cache.usage();
215        let physical = self.device.resource_memory();
216        let pending = residency.uploads.active_tickets;
217        FrameReport {
218            status,
219            completeness: if pending == 0 {
220                FrameCompleteness::Complete
221            } else {
222                FrameCompleteness::Progressive {
223                    pending_chunks: pending,
224                }
225            },
226            degradation: FrameDegradation::streaming_proxy(
227                self.mode == RenderMode::Realtime && pending > 0,
228            ),
229            metrics: FrameMetrics {
230                tracked_chunks: residency.tracked_chunks,
231                upload_in_flight_bytes: residency.uploads.in_flight_bytes,
232                derived_cache_gpu_bytes: derived.gpu_bytes,
233                derived_cache_peak_gpu_bytes: derived.peak_gpu_bytes,
234                physical_buffer_bytes: physical.buffer_bytes,
235                physical_texture_bytes: physical.texture_bytes,
236                physical_total_bytes: physical.total_bytes(),
237                physical_peak_bytes: physical.peak_bytes,
238            },
239            needs_another_frame: status == FrameStatus::Skipped
240                || pending != 0
241                || self
242                    .temporal
243                    .needs_another_frame(self.tier().temporal_samples()),
244            quality_tier: self.tier(),
245        }
246    }
247
248    /// The display encoding this frame presents for.
249    ///
250    /// Selects a pre-built tonemap pipeline rather than a per-pixel branch, so
251    /// the encoding is fixed for the whole frame by construction.
252    pub(super) fn display_encoding(&self) -> DisplayEncoding {
253        let display = self.resolved_plan.display();
254        DisplayEncoding {
255            gamut: display.gamut,
256            transfer: display.transfer,
257        }
258    }
259
260    pub(super) fn rebuild_pool_if_needed(&mut self) -> Result<bool, RenderError> {
261        let rebuild = self
262            .pool
263            .as_ref()
264            .is_none_or(|pool| !pool.matches(self.width, self.height));
265        if !rebuild {
266            return Ok(false);
267        }
268        let plan = plan_aliases(&self.resources, &self.pass_nodes, &self.order);
269        // Old views keep their textures alive. Release bindings first so a
270        // resize only reserves the new pool, including a large-to-small resize.
271        self.bindings = None;
272        self.pool = None;
273        self.temporal.reset();
274        self.pool = Some(TransientPool::build(
275            &self.device,
276            &self.resources,
277            plan,
278            self.width,
279            self.height,
280        )?);
281        self.bindings = self
282            .pool
283            .as_ref()
284            .and_then(|pool| FrameBindings::new(&self.device, pool, &self.passes));
285        Ok(true)
286    }
287
288    fn acquire_surface_frame(
289        &mut self,
290    ) -> Result<Option<<D::Surface as molgfx_gpu::Surface<D>>::Frame>, RenderError> {
291        let Some(surface) = &mut self.surface else {
292            return Ok(None);
293        };
294        match surface.acquire() {
295            Ok(frame) => Ok(Some(frame)),
296            Err(SurfaceError::Lost | SurfaceError::Outdated) => {
297                surface.configure(
298                    &self.device,
299                    &molgfx_gpu::SurfaceConfig {
300                        width: self.width,
301                        height: self.height,
302                        format: self.target_format,
303                    },
304                );
305                Ok(None)
306            }
307            Err(SurfaceError::Timeout) => Ok(None),
308            Err(error) => Err(RenderError::Gpu(error.into())),
309        }
310    }
311}