Skip to main content

argui_inspect/
trace.rs

1use std::{collections::VecDeque, error::Error, fmt, time::Duration};
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    AdapterRecord, FrameRecord, GpuFrameRecord, GpuPassRecord, InspectNodeId, Invalidation,
7};
8
9pub const TRACE_VERSION: &str = "argui-gpu-trace-v3";
10
11#[derive(Clone, Debug, Eq, PartialEq)]
12pub enum TraceError {
13    InvalidJson(String),
14    UnsupportedVersion(String),
15}
16
17impl fmt::Display for TraceError {
18    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
19        match self {
20            Self::InvalidJson(message) => {
21                write!(formatter, "invalid Argui GPU trace JSON: {message}")
22            }
23            Self::UnsupportedVersion(version) => {
24                write!(formatter, "unsupported Argui GPU trace version '{version}'")
25            }
26        }
27    }
28}
29
30impl Error for TraceError {}
31
32#[derive(Serialize, Deserialize)]
33#[serde(deny_unknown_fields)]
34pub(crate) struct TraceDocument {
35    version: String,
36    tree_revision: u64,
37    tree_nodes: usize,
38    selected: Option<u64>,
39    adapter: TraceAdapter,
40    frames: Vec<TraceFrame>,
41}
42
43#[derive(Default, Serialize, Deserialize)]
44#[serde(deny_unknown_fields)]
45struct TraceAdapter {
46    name: String,
47    vendor: u32,
48    device: u32,
49    device_type: String,
50    driver: String,
51    driver_info: String,
52    backend: String,
53    features: String,
54    timestamp_queries: bool,
55    max_texture_dimension_2d: u32,
56    max_buffer_size: u64,
57    max_storage_buffer_binding_size: u64,
58    max_bind_groups: u32,
59}
60
61#[derive(Serialize, Deserialize)]
62#[serde(deny_unknown_fields)]
63struct TraceFrame {
64    interval_ns: u64,
65    model_ns: u64,
66    surface_ns: u64,
67    tree_ns: u64,
68    layout_ns: u64,
69    paint_ns: u64,
70    render_cpu_ns: u64,
71    resize_events: u32,
72    invalidation: TraceInvalidation,
73    layers: usize,
74    passes: usize,
75    offscreen_pixels: u64,
76    cached_layers: usize,
77    damaged_pixels: u64,
78    textures: usize,
79    reused_textures: usize,
80    texture_bytes: u64,
81    vector_atlas_bytes: u64,
82    vector_atlas_entries: usize,
83    vector_atlas_hits: usize,
84    vector_rasterizations: usize,
85    gpu: Option<TraceGpuFrame>,
86}
87
88#[derive(Serialize, Deserialize)]
89#[serde(deny_unknown_fields)]
90struct TraceGpuFrame {
91    sequence: u64,
92    total_ns: u64,
93    passes: Vec<TraceGpuPass>,
94}
95
96#[derive(Serialize, Deserialize)]
97#[serde(deny_unknown_fields)]
98struct TraceGpuPass {
99    label: String,
100    start_ns: u64,
101    duration_ns: u64,
102    pixels: u64,
103    object_domain: Option<String>,
104    object_id: Option<u64>,
105}
106
107#[derive(Clone, Copy, Serialize, Deserialize)]
108#[serde(rename_all = "lowercase")]
109enum TraceInvalidation {
110    None,
111    Paint,
112    Layout,
113}
114
115impl TraceDocument {
116    pub(crate) fn validate_version(&self) -> Result<(), TraceError> {
117        if self.version == TRACE_VERSION {
118            Ok(())
119        } else {
120            Err(TraceError::UnsupportedVersion(self.version.clone()))
121        }
122    }
123
124    pub(crate) fn capture(
125        tree_revision: u64,
126        tree_nodes: usize,
127        selected: Option<InspectNodeId>,
128        frames: &VecDeque<FrameRecord>,
129    ) -> Self {
130        let adapter = frames
131            .back()
132            .map(|frame| TraceAdapter::from(&frame.adapter))
133            .unwrap_or_default();
134        Self {
135            version: TRACE_VERSION.into(),
136            tree_revision,
137            tree_nodes,
138            selected: selected.map(|id| id.0),
139            adapter,
140            frames: frames.iter().map(TraceFrame::from).collect(),
141        }
142    }
143
144    pub(crate) fn into_records(self) -> (u64, Option<InspectNodeId>, Vec<FrameRecord>) {
145        let adapter = AdapterRecord::from(self.adapter);
146        let frames = self
147            .frames
148            .into_iter()
149            .map(|frame| frame.into_record(adapter.clone()))
150            .collect();
151        (self.tree_revision, self.selected.map(InspectNodeId), frames)
152    }
153}
154
155impl From<&AdapterRecord> for TraceAdapter {
156    fn from(value: &AdapterRecord) -> Self {
157        Self {
158            name: value.name.clone(),
159            vendor: value.vendor,
160            device: value.device,
161            device_type: value.device_type.clone(),
162            driver: value.driver.clone(),
163            driver_info: value.driver_info.clone(),
164            backend: value.backend.clone(),
165            features: value.features.clone(),
166            timestamp_queries: value.timestamp_queries,
167            max_texture_dimension_2d: value.max_texture_dimension_2d,
168            max_buffer_size: value.max_buffer_size,
169            max_storage_buffer_binding_size: value.max_storage_buffer_binding_size,
170            max_bind_groups: value.max_bind_groups,
171        }
172    }
173}
174
175impl From<TraceAdapter> for AdapterRecord {
176    fn from(value: TraceAdapter) -> Self {
177        Self {
178            name: value.name,
179            vendor: value.vendor,
180            device: value.device,
181            device_type: value.device_type,
182            driver: value.driver,
183            driver_info: value.driver_info,
184            backend: value.backend,
185            features: value.features,
186            timestamp_queries: value.timestamp_queries,
187            max_texture_dimension_2d: value.max_texture_dimension_2d,
188            max_buffer_size: value.max_buffer_size,
189            max_storage_buffer_binding_size: value.max_storage_buffer_binding_size,
190            max_bind_groups: value.max_bind_groups,
191        }
192    }
193}
194
195impl From<&FrameRecord> for TraceFrame {
196    fn from(value: &FrameRecord) -> Self {
197        Self {
198            interval_ns: nanos(value.interval),
199            model_ns: nanos(value.model),
200            surface_ns: nanos(value.surface),
201            tree_ns: nanos(value.tree),
202            layout_ns: nanos(value.layout),
203            paint_ns: nanos(value.paint),
204            render_cpu_ns: nanos(value.render_cpu),
205            resize_events: value.resize_events,
206            invalidation: TraceInvalidation::from(value.update),
207            layers: value.layers,
208            passes: value.passes,
209            offscreen_pixels: value.offscreen_pixels,
210            cached_layers: value.cached_layers,
211            damaged_pixels: value.damaged_pixels,
212            textures: value.textures,
213            reused_textures: value.reused_textures,
214            texture_bytes: value.texture_bytes,
215            vector_atlas_bytes: value.vector_atlas_bytes,
216            vector_atlas_entries: value.vector_atlas_entries,
217            vector_atlas_hits: value.vector_atlas_hits,
218            vector_rasterizations: value.vector_rasterizations,
219            gpu: value.gpu.as_ref().map(TraceGpuFrame::from),
220        }
221    }
222}
223
224impl TraceFrame {
225    fn into_record(self, adapter: AdapterRecord) -> FrameRecord {
226        FrameRecord {
227            interval: Duration::from_nanos(self.interval_ns),
228            model: Duration::from_nanos(self.model_ns),
229            surface: Duration::from_nanos(self.surface_ns),
230            tree: Duration::from_nanos(self.tree_ns),
231            layout: Duration::from_nanos(self.layout_ns),
232            paint: Duration::from_nanos(self.paint_ns),
233            render_cpu: Duration::from_nanos(self.render_cpu_ns),
234            resize_events: self.resize_events,
235            update: self.invalidation.into(),
236            layers: self.layers,
237            passes: self.passes,
238            offscreen_pixels: self.offscreen_pixels,
239            cached_layers: self.cached_layers,
240            damaged_pixels: self.damaged_pixels,
241            textures: self.textures,
242            reused_textures: self.reused_textures,
243            texture_bytes: self.texture_bytes,
244            vector_atlas_bytes: self.vector_atlas_bytes,
245            vector_atlas_entries: self.vector_atlas_entries,
246            vector_atlas_hits: self.vector_atlas_hits,
247            vector_rasterizations: self.vector_rasterizations,
248            adapter,
249            gpu: self.gpu.map(TraceGpuFrame::into_record),
250        }
251    }
252}
253
254impl From<&GpuFrameRecord> for TraceGpuFrame {
255    fn from(value: &GpuFrameRecord) -> Self {
256        Self {
257            sequence: value.sequence,
258            total_ns: nanos(value.total),
259            passes: value.passes.iter().map(TraceGpuPass::from).collect(),
260        }
261    }
262}
263
264impl TraceGpuFrame {
265    fn into_record(self) -> GpuFrameRecord {
266        GpuFrameRecord {
267            sequence: self.sequence,
268            total: Duration::from_nanos(self.total_ns),
269            passes: self
270                .passes
271                .into_iter()
272                .map(TraceGpuPass::into_record)
273                .collect(),
274        }
275    }
276}
277
278impl From<&GpuPassRecord> for TraceGpuPass {
279    fn from(value: &GpuPassRecord) -> Self {
280        Self {
281            label: value.label.clone(),
282            start_ns: nanos(value.start),
283            duration_ns: nanos(value.duration),
284            pixels: value.pixels,
285            object_domain: value.object_domain.clone(),
286            object_id: value.object_id,
287        }
288    }
289}
290
291impl TraceGpuPass {
292    fn into_record(self) -> GpuPassRecord {
293        GpuPassRecord {
294            label: self.label,
295            start: Duration::from_nanos(self.start_ns),
296            duration: Duration::from_nanos(self.duration_ns),
297            pixels: self.pixels,
298            object_domain: self.object_domain,
299            object_id: self.object_id,
300        }
301    }
302}
303
304fn nanos(duration: Duration) -> u64 {
305    duration.as_nanos().min(u128::from(u64::MAX)) as u64
306}
307
308impl From<Invalidation> for TraceInvalidation {
309    fn from(value: Invalidation) -> Self {
310        match value {
311            Invalidation::None => Self::None,
312            Invalidation::Paint => Self::Paint,
313            Invalidation::Layout => Self::Layout,
314        }
315    }
316}
317
318impl From<TraceInvalidation> for Invalidation {
319    fn from(value: TraceInvalidation) -> Self {
320        match value {
321            TraceInvalidation::None => Self::None,
322            TraceInvalidation::Paint => Self::Paint,
323            TraceInvalidation::Layout => Self::Layout,
324        }
325    }
326}