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