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}