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