Skip to main content

molgfx_render/engine/
init.rs

1//! Engine construction: device open, pass creation, graph declaration.
2use super::graph_setup::{realtime_nodes, realtime_resources};
3use super::{
4    AdaptiveQuality, DerivedCache, EngineConfig, FocusTracker, GpuProfiler, PassRegistry, Picker,
5    RenderMode, RenderProfile, ResolvedRenderPlan, ShadowBoundCache, TemporalState,
6    chunk_residency::ChunkGpuResidency,
7};
8use crate::error::RenderError;
9use crate::graph::{self, PassNode, ResourceDesc, TransientPool};
10use crate::passes::{
11    AmbientOcclusionPass, AoDenoisePass, BloomPass, BondPass, CartoonPass, CullPass,
12    DepthOfFieldPass, FrameBindings, InteractionPass, LabelPass, LightingPass, MotionBlurPass,
13    OccupancyBoundsFormat, OccupancyPass, OitCompositePass, OitPass, OverlayPass,
14    ParticleMotionPass, PointPass, PrimitivePass, RelationResolvePass, ShadowPass, SpherePass,
15    SurfaceComponentPass, SurfaceFieldPass, SurfacePass, TemporalPass, TonemapPass, TrajectoryPass,
16};
17use crate::scene_gpu::GpuScene;
18
19use molgfx_gpu::FenceValue;
20use molgfx_gpu::{Device, DeviceDesc, Opened, TextureFormat, WindowTarget};
21use web_time::Instant;
22/// The rendering engine, generic over the device with no dynamic dispatch
23/// on the frame path.
24#[derive(Debug)]
25pub struct Engine<D: Device> {
26    pub(crate) device: D,
27    pub(crate) queue: D::Queue,
28    pub(crate) surface: Option<D::Surface>,
29    pub(crate) passes: PassRegistry<D>,
30    pub(crate) resources: Vec<ResourceDesc>,
31    pub(crate) pass_nodes: Vec<PassNode<D>>,
32    pub(crate) order: Vec<usize>,
33    pub(crate) pool: Option<TransientPool<D>>,
34    pub(crate) bindings: Option<FrameBindings<D>>,
35    pub(crate) scene_gpu: GpuScene<D>,
36    pub(crate) width: u32,
37    pub(crate) height: u32,
38    pub(crate) target_format: TextureFormat,
39    pub(crate) profiler: Option<GpuProfiler<D>>,
40    pub(crate) picker: Picker<D>,
41    pub(crate) temporal: TemporalState,
42    pub(crate) temporal_scene_identity: Option<u64>,
43    pub(crate) mode: RenderMode,
44    /// The closed adaptive-quality loop: frame time in, quality tier out.
45    pub(crate) adaptive: AdaptiveQuality,
46    pub(crate) profile: RenderProfile,
47    pub(crate) resolved_plan: ResolvedRenderPlan,
48    pub(crate) focus_tracker: FocusTracker,
49    pub(crate) shadow_bound: ShadowBoundCache,
50    pub(crate) derived_cache: DerivedCache,
51    pub(super) derived_frame: u64,
52    pub(crate) host_working_set: molgfx_core::HostWorkingSet,
53    pub(super) chunk_residency: ChunkGpuResidency<D>,
54    /// Monotonic origin used to make submission/completion timestamps
55    /// comparable without exposing platform-specific clock epochs.
56    pub(crate) clock_origin: Instant,
57    pub(crate) last_submission_id: u64,
58    pub(crate) last_submission_timestamp_ns: u64,
59    pub(crate) last_completion_timestamp_ns: Option<u64>,
60
61    pub(crate) last_frame_submission: FenceValue,
62
63    pub(crate) frame_submission_pending: bool,
64
65    /// Elapsed time at the most recent tracked submission, used only on
66    /// browser targets where the adaptive controller samples
67    /// submission-to-fence-completion instead of the render call.
68    #[cfg(target_arch = "wasm32")]
69    pub(crate) frame_submitted_at: Option<std::time::Duration>,
70}
71
72fn cull_pass<D: Device>(device: &D, scene: &GpuScene<D>) -> Result<CullPass<D>, RenderError> {
73    let layouts = crate::passes::CullLayouts {
74        atoms: &scene.atom_cull_layout,
75        bonds: &scene.bond_cull_layout,
76        visuals: &scene.visual_cull_layout,
77        frame: &scene.group0_layout,
78        paged: scene.paged_chunk_layout(),
79        paged_bonds: scene.paged_bond_layout(),
80        points: &scene.generic_point_cull_layout,
81        instances: &scene.generic_instance_cull_layout,
82        instance_timeline: &scene.instance_timeline_layout,
83        attribute_timeline: &scene.attribute_timeline_layout,
84        relations: &scene.relation_cull_layout,
85    };
86    CullPass::new(device, &layouts)
87}
88
89fn realtime_passes<D: Device>(
90    device: &D,
91    target_format: TextureFormat,
92    scene: &GpuScene<D>,
93    plan: &ResolvedRenderPlan,
94) -> Result<PassRegistry<D>, RenderError> {
95    Ok(PassRegistry {
96        sphere: SpherePass::new(
97            device,
98            target_format,
99            &scene.group0_layout,
100            &scene.group2_layout,
101            scene.paged_chunk_layout(),
102        )?,
103        point: PointPass::new(
104            device,
105            &scene.group0_layout,
106            &scene.group2_layout,
107            scene.paged_chunk_layout(),
108            &scene.generic_point_render_layout,
109        )?,
110        primitive: PrimitivePass::new(
111            device,
112            &scene.group0_layout,
113            &scene.primitive_layout,
114            &scene.ligand_pose_layout,
115            &scene.generic_instance_render_layout,
116        )?,
117        surface: SurfacePass::new(device, &scene.group0_layout, &scene.group2_layout)?,
118        surface_field: SurfaceFieldPass::new(
119            device,
120            &scene.surface_field_output_layout,
121            &scene.surface_field_erosion_layout,
122            &scene.surface_field_normal_layout,
123            &scene.surface_field_input_layout,
124        )?,
125        surface_components: SurfaceComponentPass::new(device, &scene.surface_component_layout)?,
126        bond: BondPass::new(
127            device,
128            target_format,
129            &scene.group0_layout,
130            &scene.group2_layout,
131            scene.paged_bond_layout(),
132        )?,
133        cartoon: CartoonPass::new(device, &scene.group0_layout, &scene.ribbon_layout)?,
134        cull: cull_pass(device, scene)?,
135        depth_of_field: plan
136            .depth_of_field()
137            .map(|_| DepthOfFieldPass::new(device, &scene.group0_layout))
138            .transpose()?,
139        ambient_occlusion: AmbientOcclusionPass::new(
140            device,
141            &scene.group0_layout,
142            &scene.quality_layout,
143        )?,
144        ao_denoise: AoDenoisePass::new(device, &scene.group0_layout)?,
145        lighting: LightingPass::new(device, &scene.group0_layout)?,
146        shadow: ShadowPass::new(
147            device,
148            &scene.group0_layout,
149            &scene.group2_layout,
150            &scene.ribbon_layout,
151            &scene.primitive_shadow_layout,
152            &scene.ligand_pose_layout,
153        )?,
154        oit: OitPass::new(
155            device,
156            &scene.group0_layout,
157            &scene.group2_layout,
158            &scene.ribbon_layout,
159            (&scene.primitive_layout, &scene.ligand_pose_layout),
160            (
161                &scene.generic_point_render_layout,
162                &scene.generic_instance_render_layout,
163            ),
164            (&scene.volume_layout, &scene.segmentation_layout),
165        )?,
166        interaction: InteractionPass::new(device, &scene.group0_layout, &scene.interaction_layout)?,
167        relation_resolve: RelationResolvePass::new(device, &scene.relation_resolve_layout)?,
168        label: LabelPass::new(
169            device,
170            &scene.group0_layout,
171            &scene.label_declutter_layout,
172            &scene.label_render_layout,
173        )?,
174        oit_composite: OitCompositePass::new(device)?,
175        temporal: TemporalPass::new(device, &scene.group0_layout)?,
176        bloom: plan
177            .bloom()
178            .map(|_| BloomPass::new(device, &scene.group0_layout))
179            .transpose()?,
180        motion_blur: plan
181            .motion_blur()
182            .map(|_| MotionBlurPass::new(device, &scene.group0_layout))
183            .transpose()?,
184        tonemap: TonemapPass::new(device, target_format, &scene.group0_layout)?,
185        overlay: OverlayPass::new(
186            device,
187            target_format,
188            &scene.group0_layout,
189            &scene.overlay_layout,
190        )?,
191        trajectory: TrajectoryPass::new(device, &scene.trajectory_layout)?,
192        occupancy: None,
193        particle_motion: ParticleMotionPass::new(device, &scene.primitive_motion_layout)?,
194    })
195}
196
197impl<D: Device> Engine<D> {
198    pub(super) fn ensure_occupancy(
199        &mut self,
200        scene: &molgfx_core::Scene,
201    ) -> Result<(), RenderError> {
202        if !scene.has_occupancy_stream() || self.passes.occupancy.is_some() {
203            return Ok(());
204        }
205        let format = OccupancyBoundsFormat::resolve(self.device.capabilities())?;
206        let layout = self
207            .scene_gpu
208            .ensure_occupancy_layout(&self.device, format.texture_format());
209        self.passes.occupancy = Some(OccupancyPass::new(&self.device, layout, format)?);
210        Ok(())
211    }
212    /// Asynchronously opens a device and builds the realtime graph. Pass a
213    /// window to render to screen; none for off-screen rendering.
214    ///
215    /// # Errors
216    ///
217    /// No compatible adapter, or pipeline construction failed.
218    pub async fn new_async(
219        config: &EngineConfig,
220        window: Option<WindowTarget>,
221    ) -> Result<Self, RenderError> {
222        let opened = D::open_async(
223            &DeviceDesc {
224                power: config.power,
225                resource_memory_limit_bytes: config.resource_memory_limit_bytes,
226            },
227            window,
228        )
229        .await?;
230        Self::from_opened(config, opened)
231    }
232
233    /// Opens a native device synchronously for callers without an async
234    /// executor. Browser callers use [`Self::new_async`].
235    ///
236    /// # Errors
237    ///
238    /// No compatible adapter, or pipeline construction failed.
239    #[cfg(not(target_arch = "wasm32"))]
240    pub fn new(config: &EngineConfig, window: Option<WindowTarget>) -> Result<Self, RenderError> {
241        let opened = D::open_blocking(
242            &DeviceDesc {
243                power: config.power,
244                resource_memory_limit_bytes: config.resource_memory_limit_bytes,
245            },
246            window,
247        )?;
248        Self::from_opened(config, opened)
249    }
250
251    fn from_opened(config: &EngineConfig, opened: Opened<D>) -> Result<Self, RenderError> {
252        let mut surface = opened.surface;
253        let device = opened.device;
254
255        let target_format = super::target::configure(&device, &mut surface, config);
256
257        let profile = config.profile.clone();
258        let resolved_plan = profile.resolve();
259        let scene_gpu = GpuScene::new(&device, config.residency, config.picking_page_capacity)?;
260        let passes = realtime_passes(&device, target_format, &scene_gpu, &resolved_plan)?;
261        let pass_nodes = realtime_nodes(
262            resolved_plan.depth_of_field().is_some(),
263            resolved_plan.bloom().is_some(),
264            resolved_plan.motion_blur().is_some(),
265        );
266        let resources = realtime_resources();
267        let order = graph::schedule(&pass_nodes)?;
268
269        let profiler = GpuProfiler::new(&device, target_format)?;
270        let picker = Picker::new(&device, config.picking_page_capacity)?;
271        let chunk_residency = ChunkGpuResidency::new(&device, config.residency)?;
272        Ok(Self {
273            device,
274            queue: opened.queue,
275            surface,
276            passes,
277            resources,
278            pass_nodes,
279            order,
280            pool: None,
281            bindings: None,
282            scene_gpu,
283            width: config.width,
284            height: config.height,
285            target_format,
286            profiler,
287            picker,
288            temporal: TemporalState::default(),
289            temporal_scene_identity: None,
290            mode: config.mode,
291            adaptive: AdaptiveQuality::new(config.adaptive, config.mode == RenderMode::Cinematic),
292            profile,
293            resolved_plan,
294            focus_tracker: FocusTracker::default(),
295            shadow_bound: ShadowBoundCache::default(),
296            derived_cache: DerivedCache::new(config.derived_cache),
297            derived_frame: 0,
298            host_working_set: molgfx_core::HostWorkingSet::new(config.source_budget),
299            chunk_residency,
300            clock_origin: Instant::now(),
301            last_submission_id: 0,
302            last_submission_timestamp_ns: 0,
303            last_completion_timestamp_ns: None,
304
305            last_frame_submission: FenceValue::default(),
306
307            frame_submission_pending: false,
308
309            #[cfg(target_arch = "wasm32")]
310            frame_submitted_at: None,
311        })
312    }
313}