Skip to main content

molgfx_render/engine/
settings.rs

1//! Runtime engine settings and render-profile topology updates.
2
3use super::{Engine, QualityTier, RenderMode, RenderProfile, ResolvedRenderPlan};
4use crate::engine::graph_setup::realtime_nodes;
5use crate::error::RenderError;
6use crate::graph;
7use molgfx_gpu::{Device, Surface as _, SurfaceConfig};
8
9impl<D: Device> Engine<D> {
10    /// Updates the frame size after a window resize; the surface and pool
11    /// reconfigure lazily before the next frame.
12    pub fn resize(&mut self, width: u32, height: u32) {
13        self.width = width.max(1);
14        self.height = height.max(1);
15        self.temporal.reset();
16        if let Some(surface) = &mut self.surface {
17            surface.configure(
18                &self.device,
19                &SurfaceConfig {
20                    width: self.width,
21                    height: self.height,
22                    format: self.target_format,
23                },
24            );
25        }
26    }
27
28    /// The opened device's capability report.
29    #[must_use]
30    pub fn capabilities(&self) -> &molgfx_gpu::Capabilities {
31        self.device.capabilities()
32    }
33
34    /// Changes rendering strategy without rebuilding the scene or device.
35    pub fn set_render_mode(&mut self, mode: RenderMode) {
36        if self.mode != mode {
37            self.mode = mode;
38            // The cinematic path is the deterministic publication path, so it
39            // holds one tier.
40            self.adaptive.set_publication(mode == RenderMode::Cinematic);
41            self.temporal.reset();
42        }
43    }
44
45    /// The adaptive quality tier the next frame presents at.
46    #[must_use]
47    pub const fn quality_tier(&self) -> QualityTier {
48        self.adaptive.tier()
49    }
50
51    /// The tier every stage of the current frame reads.
52    pub(crate) const fn tier(&self) -> QualityTier {
53        self.adaptive.tier()
54    }
55
56    /// Publishes this frame's tier into the state the frame loop reads.
57    ///
58    /// Called at the top of every frame path, before any pass is built, so one
59    /// frame never mixes two tiers. A tier move restarts accumulation, because
60    /// history gathered under one sample budget is not a valid prefix of
61    /// another.
62    pub(crate) fn sync_quality_tier(&mut self) {
63        if self.temporal.tier() != self.adaptive.tier() {
64            self.temporal.set_tier(self.adaptive.tier());
65            self.temporal.reset();
66        }
67    }
68
69    /// Deterministic description of the adaptive quality loop: the tier it
70    /// currently holds, the smoothed frame time that drives it, and the
71    /// resolution that tier selects.
72    #[must_use]
73    pub fn explain(&self) -> String {
74        format!(
75            "quality tier: {:?}\nsmoothed frame time ns: {}\ntarget fps: {}\n{}",
76            self.adaptive.tier(),
77            self.adaptive.smoothed_ns(),
78            self.adaptive.target_fps(),
79            self.scene_gpu.specialization_report()
80        )
81    }
82
83    /// Current rendering strategy.
84    #[must_use]
85    pub const fn render_mode(&self) -> RenderMode {
86        self.mode
87    }
88
89    /// Resolves and applies a reusable presentation recipe. Parameter-only
90    /// changes preserve scene and pipeline allocations; temporal history is
91    /// reset so the previous recipe never bleeds into the new presentation.
92    ///
93    /// # Errors
94    ///
95    /// Returns a graph error if a topology-changing module cannot be
96    /// scheduled.
97    pub fn set_render_profile(&mut self, profile: RenderProfile) -> Result<(), RenderError> {
98        if self.profile == profile {
99            return Ok(());
100        }
101        let resolved_plan = profile.resolve();
102        let old_topology = (
103            self.resolved_plan.depth_of_field().is_some(),
104            self.resolved_plan.bloom().is_some(),
105            self.resolved_plan.motion_blur().is_some(),
106        );
107        let new_topology = (
108            resolved_plan.depth_of_field().is_some(),
109            resolved_plan.bloom().is_some(),
110            resolved_plan.motion_blur().is_some(),
111        );
112        if old_topology != new_topology {
113            let pass_nodes = realtime_nodes(new_topology.0, new_topology.1, new_topology.2);
114            let order = graph::schedule(&pass_nodes)?;
115            // Prepare new pipelines before replacing any live state so a
116            // failed profile transition leaves the previous frame usable.
117            let depth_of_field = if new_topology.0 && self.passes.depth_of_field.is_none() {
118                Some(crate::passes::DepthOfFieldPass::new(
119                    &self.device,
120                    &self.scene_gpu.group0_layout,
121                )?)
122            } else {
123                None
124            };
125            let bloom = if new_topology.1 && self.passes.bloom.is_none() {
126                Some(crate::passes::BloomPass::new(
127                    &self.device,
128                    &self.scene_gpu.group0_layout,
129                )?)
130            } else {
131                None
132            };
133            let motion_blur = if new_topology.2 && self.passes.motion_blur.is_none() {
134                Some(crate::passes::MotionBlurPass::new(
135                    &self.device,
136                    &self.scene_gpu.group0_layout,
137                )?)
138            } else {
139                None
140            };
141            self.passes.depth_of_field = if new_topology.0 {
142                self.passes.depth_of_field.take().or(depth_of_field)
143            } else {
144                None
145            };
146            self.passes.bloom = if new_topology.1 {
147                self.passes.bloom.take().or(bloom)
148            } else {
149                None
150            };
151            self.passes.motion_blur = if new_topology.2 {
152                self.passes.motion_blur.take().or(motion_blur)
153            } else {
154                None
155            };
156            self.pass_nodes = pass_nodes;
157            self.order = order;
158            self.bindings = None;
159            self.pool = None;
160        }
161        self.resolved_plan = resolved_plan;
162        self.profile = profile;
163        self.temporal.reset();
164        Ok(())
165    }
166
167    /// Replaces the derived-resource budget.
168    ///
169    /// The new limit takes effect on the next frame, which releases whatever
170    /// no longer fits; a released resource rebuilds on next use.
171    pub fn set_derived_cache_budget(&mut self, budget: crate::DerivedCacheBudget) {
172        self.derived_cache.set_budget(budget);
173    }
174
175    /// The caller-authored presentation recipe.
176    #[must_use]
177    pub const fn render_profile(&self) -> &RenderProfile {
178        &self.profile
179    }
180
181    /// The sanitized plan consumed by the frame loop.
182    #[must_use]
183    pub const fn resolved_render_plan(&self) -> &ResolvedRenderPlan {
184        &self.resolved_plan
185    }
186}
187
188#[cfg(all(test, not(target_arch = "wasm32")))]
189#[path = "settings_tests.rs"]
190mod tests;