1use super::image::ImagePurpose;
4use super::{Engine, ImageConfig, MotionBlur, QualityTier, TemporalOptions};
5use crate::ResidencyMetrics;
6use crate::error::RenderError;
7use molgfx_core::Scene;
8use molgfx_gpu::{
9 BufferDesc, BufferUsage, CommandEncoder as _, Device, Queue as _, TextureDesc, TextureUsage,
10 TextureViewDesc,
11};
12use molgfx_math::Camera;
13use web_time::Instant;
15
16const QUERY_BYTES: u64 = 2 * std::mem::size_of::<u64>() as u64;
17
18#[derive(Clone, Copy, PartialEq, Eq, Debug)]
21pub struct FrameTiming {
22 pub gpu_ns: u64,
24 pub gpu_timing_resolved: bool,
29 pub cpu_ns: u64,
31 pub frame_ns: u64,
35 pub residency: ResidencyMetrics,
37}
38
39impl FrameTiming {
40 #[must_use]
42 pub fn residency_counters(self) -> crate::ResidencyCounters {
43 self.residency.counters()
44 }
45}
46
47#[derive(Debug)]
48pub(crate) struct GpuProfiler<D: Device> {
49 queries: D::QuerySet,
50 resolve: D::Buffer,
51 readback: D::Buffer,
52 target: Option<D::Texture>,
53 target_view: Option<D::TextureView>,
54 target_size: (u32, u32),
55 target_format: molgfx_gpu::TextureFormat,
56 pending_start: Option<u64>,
57}
58
59impl<D: Device> GpuProfiler<D> {
60 pub(crate) fn new(
61 device: &D,
62 target_format: molgfx_gpu::TextureFormat,
63 ) -> Result<Option<Self>, RenderError> {
64 if !device.capabilities().timestamp_queries() {
65 return Ok(None);
66 }
67 Ok(Some(Self {
68 queries: device.create_timestamp_query_set(2)?,
69 resolve: device.create_buffer(&BufferDesc {
70 label: "frame timestamp resolve",
71 size: QUERY_BYTES,
72 usage: BufferUsage::QUERY_RESOLVE.union(BufferUsage::COPY_SRC),
73 })?,
74 readback: device.create_buffer(&BufferDesc {
75 label: "frame timestamp readback",
76 size: QUERY_BYTES,
77 usage: BufferUsage::COPY_DST.union(BufferUsage::MAP_READ),
78 })?,
79 target: None,
80 target_view: None,
81 target_size: (0, 0),
82 target_format,
83 pending_start: None,
84 }))
85 }
86
87 fn ensure_target(&mut self, device: &D, config: ImageConfig) -> Result<(), RenderError> {
88 if self.target_size == (config.width, config.height) {
89 return Ok(());
90 }
91 let target = device.create_texture(&TextureDesc {
92 label: "profiling target",
93 width: config.width,
94 height: config.height,
95 depth: 1,
96 dimension: molgfx_gpu::TextureDimension::D2,
97 format: self.target_format,
98 usage: TextureUsage::RENDER_ATTACHMENT,
99 })?;
100 self.target_view = Some(device.create_texture_view(&target, &TextureViewDesc::default()));
101 self.target = Some(target);
102 self.target_size = (config.width, config.height);
103 Ok(())
104 }
105
106 async fn read_timing_async(
107 &mut self,
108 device: &D,
109 queue: &D::Queue,
110 ) -> Result<DecodedTiming, RenderError> {
111 let data = queue
112 .read_buffer_async(device, &self.readback, 0, QUERY_BYTES)
113 .await?;
114 self.decode_timing(&data, queue.timestamp_period())
115 }
116
117 #[cfg(not(target_arch = "wasm32"))]
118 fn read_timing(&mut self, device: &D, queue: &D::Queue) -> Result<DecodedTiming, RenderError> {
119 let data = queue.read_buffer_blocking(device, &self.readback, 0, QUERY_BYTES)?;
120 self.decode_timing(&data, queue.timestamp_period())
121 }
122
123 fn decode_timing(
124 &mut self,
125 data: &[u8],
126 timestamp_period: f32,
127 ) -> Result<DecodedTiming, RenderError> {
128 let Some(start) = read_u64(data, 0) else {
129 return Err(molgfx_gpu::GpuError::DeviceLost.into());
130 };
131 let Some(end) = read_u64(data, std::mem::size_of::<u64>()) else {
132 return Err(molgfx_gpu::GpuError::DeviceLost.into());
133 };
134 let previous_start = self.pending_start.replace(start);
135 let Some(ticks) = timestamp_delta(start, end, previous_start) else {
136 return Ok(DecodedTiming::unresolved());
137 };
138 let ticks = crate::fallback(u32::try_from(ticks), u32::MAX);
139 let seconds = f64::from(ticks) * f64::from(timestamp_period) / 1_000_000_000.0;
140 let duration = std::time::Duration::try_from_secs_f64(seconds)
141 .map_err(|_| molgfx_gpu::GpuError::DeviceLost)?;
142 Ok(DecodedTiming {
143 nanoseconds: duration_ns(duration),
144 resolved: true,
145 })
146 }
147}
148
149impl<D: Device> Engine<D> {
150 #[cfg(not(target_arch = "wasm32"))]
160 pub fn profile_frame_batch(
161 &mut self,
162 scene: &Scene,
163 camera: &Camera,
164 config: ImageConfig,
165 frames: std::num::NonZeroU32,
166 ) -> Result<FrameTiming, RenderError> {
167 let frame_start = Instant::now();
168 let Some(mut profiler) = self.profiler.take() else {
169 return Err(molgfx_gpu::GpuError::Capability {
170 name: "timestamp queries",
171 }
172 .into());
173 };
174 let mut cpu_ns = 0_u64;
175 let result = (|| {
176 for _ in 0..frames.get() {
177 let (cpu_start, quality) = self.prepare_profile(scene, camera, config)?;
178 cpu_ns = cpu_ns.saturating_add(self.submit_profile(
179 &mut profiler,
180 cpu_start,
181 config,
182 quality,
183 )?);
184 }
185 let gpu = profiler.read_timing(&self.device, &self.queue)?;
186 let count = u64::from(frames.get());
187 Ok(FrameTiming {
188 gpu_ns: gpu.nanoseconds,
189 gpu_timing_resolved: gpu.resolved,
190 cpu_ns: cpu_ns / count,
191 frame_ns: duration_ns(frame_start.elapsed()) / count,
192 residency: self.scene_gpu.residency_metrics(),
193 })
194 })();
195 self.profiler = Some(profiler);
196 result
197 }
198
199 pub async fn profile_frame_async(
206 &mut self,
207 scene: &Scene,
208 camera: &Camera,
209 config: ImageConfig,
210 ) -> Result<FrameTiming, RenderError> {
211 let frame_start = Instant::now();
212 let (cpu_start, quality) = self.prepare_profile(scene, camera, config)?;
213 let Some(mut profiler) = self.profiler.take() else {
214 return Err(molgfx_gpu::GpuError::Capability {
215 name: "timestamp queries",
216 }
217 .into());
218 };
219 let result = match self.submit_profile(&mut profiler, cpu_start, config, quality) {
220 Ok(cpu_ns) => profiler
221 .read_timing_async(&self.device, &self.queue)
222 .await
223 .map(|gpu| FrameTiming {
224 gpu_ns: gpu.nanoseconds,
225 gpu_timing_resolved: gpu.resolved,
226 cpu_ns,
227 frame_ns: duration_ns(frame_start.elapsed()),
228 residency: self.scene_gpu.residency_metrics(),
229 }),
230 Err(error) => Err(error),
231 };
232 self.profiler = Some(profiler);
233 result
234 }
235
236 #[cfg(not(target_arch = "wasm32"))]
244 pub fn profile_frame(
245 &mut self,
246 scene: &Scene,
247 camera: &Camera,
248 config: ImageConfig,
249 ) -> Result<FrameTiming, RenderError> {
250 let frame_start = Instant::now();
251 let (cpu_start, quality) = self.prepare_profile(scene, camera, config)?;
252 let Some(mut profiler) = self.profiler.take() else {
253 return Err(molgfx_gpu::GpuError::Capability {
254 name: "timestamp queries",
255 }
256 .into());
257 };
258 let result = self.profile_with(&mut profiler, cpu_start, frame_start, config, quality);
259 self.profiler = Some(profiler);
260 result
261 }
262
263 fn prepare_profile(
264 &mut self,
265 scene: &Scene,
266 camera: &Camera,
267 config: ImageConfig,
268 ) -> Result<(Instant, bool), RenderError> {
269 config.validate(self.device.capabilities().max_texture_dim)?;
270 self.scene_gpu.begin_frame();
271 let cpu_start = Instant::now();
272 self.width = config.width;
273 self.height = config.height;
274 let preparation = self.prepare_image(scene, ImagePurpose::Publication)?;
275 let identity = scene.cache_identity();
276 let scene_reset = self.temporal_scene_identity.replace(identity) != Some(identity);
277 let camera_changed = self.temporal.camera_changed(camera);
278 let quality = self.tier() >= QualityTier::Standard;
279 let optics = self.resolve_optics(scene, camera)?;
280 let shadow = self.shadow_bound.fit(
281 scene,
282 camera,
283 self.resolved_plan.lighting(),
284 preparation.scene_changed || preparation.rebuild,
285 );
286 let uniforms = self.temporal.prepare(
287 camera,
288 &TemporalOptions {
289 extent: [self.width, self.height],
290 reset: scene_reset
291 || preparation.rebuild
292 || (self.tier() >= QualityTier::Standard && camera_changed),
293 quality,
294 publication: false,
295 illustration: self.resolved_plan.illustration(),
296 optics,
297 motion_blur: self
298 .resolved_plan
299 .motion_blur()
300 .map_or([0.0; 4], MotionBlur::packed),
301 atmosphere: self
302 .resolved_plan
303 .packed_presentation(self.scene_gpu.has_translucency()),
304 lighting: self.resolved_plan.packed_lighting(),
305 shadow_view: shadow.view,
306 shadow_projection: shadow.projection,
307 shadow_view_proj: shadow.view_projection,
308 },
309 );
310 self.scene_gpu
311 .write_frame_uniforms(&self.queue, &uniforms)?;
312 Ok((cpu_start, quality))
313 }
314
315 #[cfg(not(target_arch = "wasm32"))]
316 fn profile_with(
317 &mut self,
318 profiler: &mut GpuProfiler<D>,
319 cpu_start: Instant,
320 frame_start: Instant,
321 config: ImageConfig,
322 quality: bool,
323 ) -> Result<FrameTiming, RenderError> {
324 let cpu_ns = self.submit_profile(profiler, cpu_start, config, quality)?;
325 let gpu = profiler.read_timing(&self.device, &self.queue)?;
326 Ok(FrameTiming {
327 gpu_ns: gpu.nanoseconds,
328 gpu_timing_resolved: gpu.resolved,
329 cpu_ns,
330 frame_ns: duration_ns(frame_start.elapsed()),
331 residency: self.scene_gpu.residency_metrics(),
332 })
333 }
334
335 fn submit_profile(
336 &mut self,
337 profiler: &mut GpuProfiler<D>,
338 cpu_start: Instant,
339 config: ImageConfig,
340 quality: bool,
341 ) -> Result<u64, RenderError> {
342 profiler.ensure_target(&self.device, config)?;
343 let Some(target) = &profiler.target_view else {
344 return Err(molgfx_gpu::GpuError::DeviceLost.into());
345 };
346 let mut encoder = self.device.create_command_encoder();
347 self.passes
348 .cull
349 .record_attribute_timelines(&self.scene_gpu, &mut encoder);
350 self.passes
351 .cull
352 .record_instance_timelines(&self.scene_gpu, &mut encoder);
353 let point_coordinates_changed = self
354 .passes
355 .cull
356 .record_point_timelines(&self.scene_gpu, &mut encoder);
357 self.scene_gpu
358 .record_particle_motion(&mut encoder, &self.passes.particle_motion);
359 let timestamps_started = self.scene_gpu.record_trajectories(
360 &mut encoder,
361 &self.passes.trajectory,
362 Some(molgfx_gpu::TimestampWrites {
363 queries: &profiler.queries,
364 beginning: Some(0),
365 end: None,
366 }),
367 );
368 let paged_coordinates_changed = self
369 .passes
370 .cull
371 .record_paged_trajectories(&self.scene_gpu, &mut encoder);
372 self.scene_gpu.record_dynamic_relations(
373 &mut encoder,
374 &self.passes.relation_resolve,
375 timestamps_started || paged_coordinates_changed || point_coordinates_changed,
376 );
377 self.scene_gpu
378 .record_occupancies(&mut encoder, &self.passes.occupancy);
379 self.scene_gpu.record_surface_fields(
380 &mut encoder,
381 &self.passes.surface_field,
382 &self.passes.surface_components,
383 );
384 self.scene_gpu
385 .record_quality_hardware(&mut encoder, quality);
386 self.record_image(
387 &mut encoder,
388 target,
389 Some(&profiler.queries),
390 quality,
391 timestamps_started,
392 );
393 encoder.resolve_query_set(&profiler.queries, 0..2, &profiler.resolve, 0);
394 encoder.copy_buffer_to_buffer(&profiler.resolve, 0, &profiler.readback, 0, QUERY_BYTES);
395 self.queue.submit(encoder);
396 Ok(duration_ns(cpu_start.elapsed()))
397 }
398}
399
400fn read_u64(bytes: &[u8], offset: usize) -> Option<u64> {
401 let slice = bytes.get(offset..offset + std::mem::size_of::<u64>())?;
402 let mut array = [0; std::mem::size_of::<u64>()];
403 array.copy_from_slice(slice);
404 Some(u64::from_le_bytes(array))
405}
406
407#[derive(Clone, Copy, Debug)]
408struct DecodedTiming {
409 nanoseconds: u64,
410 resolved: bool,
411}
412
413impl DecodedTiming {
414 const fn unresolved() -> Self {
415 Self {
416 nanoseconds: 0,
417 resolved: false,
418 }
419 }
420}
421
422fn timestamp_delta(
423 current_start: u64,
424 visible_end: u64,
425 previous_start: Option<u64>,
426) -> Option<u64> {
427 if current_start == 0 && visible_end == 0 {
428 return None;
429 }
430 if visible_end >= current_start {
431 return Some(visible_end - current_start);
432 }
433 previous_start.and_then(|start| visible_end.checked_sub(start))
434}
435
436fn duration_ns(duration: std::time::Duration) -> u64 {
437 crate::fallback(u64::try_from(duration.as_nanos()), u64::MAX)
438}
439
440#[cfg(test)]
441#[path = "profiling_tests.rs"]
442mod tests;