Skip to main content

gpu_trace_perf/
rdc_file.rs

1use anyhow::{Context as _, Result, bail};
2use std::{
3    fmt,
4    io::{self, BufReader, Read, Seek, SeekFrom},
5    path::Path,
6};
7
8/// Rendering API used in a .rdc capture file, corresponding to RenderDoc's RDCDriver enum.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum RdcDriver {
11    Unknown,
12    D3D11,
13    OpenGL,
14    Mantle,
15    D3D12,
16    D3D10,
17    D3D9,
18    Image,
19    Vulkan,
20    OpenGLES,
21    D3D8,
22    Metal,
23    Other(u32),
24}
25
26impl RdcDriver {
27    pub fn is_directx(self) -> bool {
28        matches!(
29            self,
30            RdcDriver::D3D8
31                | RdcDriver::D3D9
32                | RdcDriver::D3D10
33                | RdcDriver::D3D11
34                | RdcDriver::D3D12
35        )
36    }
37}
38
39impl fmt::Display for RdcDriver {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            RdcDriver::Unknown => write!(f, "Unknown"),
43            RdcDriver::D3D8 => write!(f, "D3D8"),
44            RdcDriver::D3D9 => write!(f, "D3D9"),
45            RdcDriver::D3D10 => write!(f, "D3D10"),
46            RdcDriver::D3D11 => write!(f, "D3D11"),
47            RdcDriver::D3D12 => write!(f, "D3D12"),
48            RdcDriver::OpenGL => write!(f, "OpenGL"),
49            RdcDriver::OpenGLES => write!(f, "OpenGLES"),
50            RdcDriver::Vulkan => write!(f, "Vulkan"),
51            RdcDriver::Metal => write!(f, "Metal"),
52            RdcDriver::Mantle => write!(f, "Mantle"),
53            RdcDriver::Image => write!(f, "Image"),
54            RdcDriver::Other(id) => write!(f, "Other({id})"),
55        }
56    }
57}
58
59const RDC_MAGIC: u32 = u32::from_le_bytes([b'R', b'D', b'O', b'C']);
60
61fn read_u16_le(r: &mut impl Read) -> Result<u16> {
62    let mut buf = [0u8; 2];
63    r.read_exact(&mut buf)?;
64    Ok(u16::from_le_bytes(buf))
65}
66
67fn read_u32_le(r: &mut impl Read) -> Result<u32> {
68    let mut buf = [0u8; 4];
69    r.read_exact(&mut buf)?;
70    Ok(u32::from_le_bytes(buf))
71}
72
73fn read_u64_le(r: &mut impl Read) -> Result<u64> {
74    let mut buf = [0u8; 8];
75    r.read_exact(&mut buf)?;
76    Ok(u64::from_le_bytes(buf))
77}
78
79/// Parse the rendering API from a RenderDoc .rdc capture file header.
80pub fn parse_rdc_driver(path: &Path) -> Result<RdcDriver> {
81    let mut f = std::fs::File::open(path).context("opening .rdc file")?;
82
83    // FileHeader: magic (u64), version (u32), headerLength (u32), progVersion ([u8;16])
84    // headerLength covers the entire preamble (FileHeader + BinaryThumbnail + CaptureMetaData).
85    let magic_lo = read_u32_le(&mut f).context("reading magic")?;
86    if magic_lo != RDC_MAGIC {
87        bail!("not a valid .rdc file: bad magic");
88    }
89    let magic_hi = read_u32_le(&mut f).context("reading magic hi")?;
90    if magic_hi != 0 {
91        bail!("not a valid .rdc file: unexpected magic high bytes {magic_hi:#x}");
92    }
93    let _version = read_u32_le(&mut f).context("reading version")?;
94    let _header_length = read_u32_le(&mut f).context("reading headerLength")?;
95    // Skip progVersion[16].
96    f.seek(SeekFrom::Current(16))
97        .context("seeking past progVersion")?;
98
99    // BinaryThumbnail: width (u16), height (u16), length (u32), data[length]
100    let _thumb_width = read_u16_le(&mut f).context("reading thumbnail width")?;
101    let _thumb_height = read_u16_le(&mut f).context("reading thumbnail height")?;
102    let thumb_length = read_u32_le(&mut f).context("reading thumbnail length")?;
103    f.seek(SeekFrom::Current(thumb_length as i64))
104        .context("seeking past thumbnail data")?;
105
106    // CaptureMetaData: machineIdent (u64), driverID (u32), driverNameLength (u8), driverName[...]
107    let _machine_ident = read_u64_le(&mut f).context("reading machineIdent")?;
108    let driver_id = read_u32_le(&mut f).context("reading driverID")?;
109
110    Ok(match driver_id {
111        0 => RdcDriver::Unknown,
112        1 => RdcDriver::D3D11,
113        2 => RdcDriver::OpenGL,
114        3 => RdcDriver::Mantle,
115        4 => RdcDriver::D3D12,
116        5 => RdcDriver::D3D10,
117        6 => RdcDriver::D3D9,
118        7 => RdcDriver::Image,
119        8 => RdcDriver::Vulkan,
120        9 => RdcDriver::OpenGLES,
121        10 => RdcDriver::D3D8,
122        11 => RdcDriver::Metal,
123        other => RdcDriver::Other(other),
124    })
125}
126
127const SECTION_TYPE_FRAME_CAPTURE: u32 = 1;
128const SECTION_FLAG_LZ4: u32 = 0x2;
129const SECTION_FLAG_ZSTD: u32 = 0x4;
130
131// D3D11 chunk IDs: SystemChunk::FirstDriverChunk=1000, SetResourceName=1001, CreateSwapBuffer=1002
132const D3D11_CREATE_SWAP_BUFFER: u32 = 1002;
133
134const CHUNK_INDEX_MASK: u32 = 0x0000_ffff;
135const CHUNK_FLAG_CALLSTACK: u32 = 0x0001_0000;
136const CHUNK_FLAG_THREAD_ID: u32 = 0x0002_0000;
137const CHUNK_FLAG_DURATION: u32 = 0x0004_0000;
138const CHUNK_FLAG_TIMESTAMP: u32 = 0x0008_0000;
139const CHUNK_FLAG_64BIT_SIZE: u32 = 0x0010_0000;
140
141// RenderDoc LZ4 block size
142const RDC_LZ4_BLOCK_SIZE: usize = 1024 * 1024;
143
144/// Streaming reader over RenderDoc's custom LZ4 block format.
145///
146/// Each block is an i32 compressed size followed by that many bytes of raw LZ4
147/// block data.  Blocks are linked via `LZ4_decompress_safe_continue`: each can
148/// reference the last 64 KB of the previous block's output.  We alternate
149/// between two 1-MB page buffers (matching RenderDoc's compressor) so the
150/// streaming context can resolve cross-block back-references.
151struct RdcLz4BlockReader<R: Read> {
152    reader: R,
153    comp_remaining: usize,
154    stream: *mut lz4_sys::LZ4StreamDecode,
155    /// Two alternating output pages.  Both stay alive so the LZ4 streaming
156    /// context's internal pointer into the previous page remains valid.
157    pages: [Vec<u8>; 2],
158    current_page: usize,
159    block_len: usize,
160    block_pos: usize,
161}
162
163impl<R: Read> RdcLz4BlockReader<R> {
164    fn new(reader: R, comp_remaining: usize) -> io::Result<Self> {
165        let stream = unsafe { lz4_sys::LZ4_createStreamDecode() };
166        if stream.is_null() {
167            return Err(io::Error::other("LZ4_createStreamDecode failed"));
168        }
169        unsafe { lz4_sys::LZ4_setStreamDecode(stream, std::ptr::null(), 0) };
170        Ok(Self {
171            reader,
172            comp_remaining,
173            stream,
174            pages: [vec![0u8; RDC_LZ4_BLOCK_SIZE], vec![0u8; RDC_LZ4_BLOCK_SIZE]],
175            current_page: 0,
176            block_len: 0,
177            block_pos: 0,
178        })
179    }
180
181    fn next_block(&mut self) -> io::Result<bool> {
182        if self.comp_remaining < 4 {
183            return Ok(false);
184        }
185        let mut size_buf = [0u8; 4];
186        self.reader.read_exact(&mut size_buf)?;
187        self.comp_remaining -= 4;
188
189        let block_comp_size = i32::from_le_bytes(size_buf);
190        if block_comp_size <= 0 {
191            return Ok(false);
192        }
193        let block_comp_size = block_comp_size as usize;
194        if block_comp_size > self.comp_remaining {
195            return Ok(false);
196        }
197
198        let mut compressed = vec![0u8; block_comp_size];
199        self.reader.read_exact(&mut compressed)?;
200        self.comp_remaining -= block_comp_size;
201
202        self.current_page ^= 1;
203        let page = &mut self.pages[self.current_page];
204
205        let n = unsafe {
206            lz4_sys::LZ4_decompress_safe_continue(
207                self.stream,
208                compressed.as_ptr(),
209                page.as_mut_ptr(),
210                block_comp_size as i32,
211                RDC_LZ4_BLOCK_SIZE as i32,
212            )
213        };
214        if n < 0 {
215            return Err(io::Error::new(
216                io::ErrorKind::InvalidData,
217                format!("LZ4_decompress_safe_continue returned {n}"),
218            ));
219        }
220
221        self.block_len = n as usize;
222        self.block_pos = 0;
223        Ok(true)
224    }
225}
226
227impl<R: Read> Drop for RdcLz4BlockReader<R> {
228    fn drop(&mut self) {
229        unsafe { lz4_sys::LZ4_freeStreamDecode(self.stream) };
230    }
231}
232
233impl<R: Read> Read for RdcLz4BlockReader<R> {
234    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
235        if self.block_pos >= self.block_len && !self.next_block()? {
236            return Ok(0);
237        }
238        let available = &self.pages[self.current_page][self.block_pos..self.block_len];
239        let n = buf.len().min(available.len());
240        buf[..n].copy_from_slice(&available[..n]);
241        self.block_pos += n;
242        Ok(n)
243    }
244}
245
246/// Streaming iterator over frame-capture chunks from a .rdc file.
247///
248/// Internally reads from a decompressing stream (LZ4, Zstd, or raw) via a
249/// `BufReader`, parsing each chunk header and yielding `(chunk_id, data)`.
250/// Only one decompressed LZ4 block (~1 MB) plus a 64-KB back-reference
251/// dictionary are held in memory at a time.
252pub struct RdcFrameCaptureChunks {
253    reader: BufReader<Box<dyn Read>>,
254    /// Byte offset in the decompressed stream — used to compute 64-byte chunk alignment.
255    stream_pos: usize,
256    done: bool,
257}
258
259/// Seeks to the FrameCapture section in an .rdc reader and returns a chunk
260/// iterator.  The reader must be positioned at the start of the file.
261pub fn parse_rdc_frame_capture_chunks<R: Read + Seek + 'static>(
262    mut reader: R,
263) -> Result<RdcFrameCaptureChunks> {
264    let magic_lo = read_u32_le(&mut reader).context("reading magic")?;
265    if magic_lo != RDC_MAGIC {
266        bail!("not a valid .rdc file: bad magic");
267    }
268    let _ = read_u32_le(&mut reader)?; // magic_hi
269    let _ = read_u32_le(&mut reader)?; // version
270    let header_length = read_u32_le(&mut reader).context("reading headerLength")? as u64;
271
272    reader
273        .seek(SeekFrom::Start(header_length))
274        .context("seeking to sections")?;
275
276    loop {
277        let mut first = [0u8; 1];
278        match reader.read_exact(&mut first) {
279            Ok(_) => {}
280            Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
281                bail!("no FrameCapture section found")
282            }
283            Err(e) => return Err(e.into()),
284        }
285        if first[0] == b'A' {
286            bail!("ASCII .rdc sections not supported");
287        }
288
289        // BinarySectionHeader remainder: zero[3] + sectionType(4) + compLen(8) +
290        //   uncompLen(8) + version(8) + flags(4) + nameLen(4)
291        reader.seek(SeekFrom::Current(3))?;
292        let section_type = read_u32_le(&mut reader)?;
293        let comp_len = read_u64_le(&mut reader)? as usize;
294        let _ = read_u64_le(&mut reader)?; // uncompressed length
295        let _ = read_u64_le(&mut reader)?; // section version
296        let section_flags = read_u32_le(&mut reader)?;
297        let name_len = read_u32_le(&mut reader)?;
298        reader.seek(SeekFrom::Current(name_len as i64))?;
299
300        if section_type != SECTION_TYPE_FRAME_CAPTURE {
301            reader.seek(SeekFrom::Current(comp_len as i64))?;
302            continue;
303        }
304
305        let section_reader: Box<dyn Read> = if section_flags & SECTION_FLAG_LZ4 != 0 {
306            Box::new(RdcLz4BlockReader::new(reader, comp_len).context("creating LZ4 reader")?)
307        } else if section_flags & SECTION_FLAG_ZSTD != 0 {
308            let take = reader.take(comp_len as u64);
309            Box::new(zstd::Decoder::new(BufReader::new(take)).context("creating zstd decoder")?)
310        } else {
311            Box::new(reader.take(comp_len as u64))
312        };
313
314        return Ok(RdcFrameCaptureChunks {
315            reader: BufReader::new(section_reader),
316            stream_pos: 0,
317            done: false,
318        });
319    }
320}
321
322impl RdcFrameCaptureChunks {
323    fn read_u32(&mut self) -> io::Result<u32> {
324        let mut buf = [0u8; 4];
325        self.reader.read_exact(&mut buf)?;
326        self.stream_pos += 4;
327        Ok(u32::from_le_bytes(buf))
328    }
329
330    fn read_u64(&mut self) -> io::Result<u64> {
331        let mut buf = [0u8; 8];
332        self.reader.read_exact(&mut buf)?;
333        self.stream_pos += 8;
334        Ok(u64::from_le_bytes(buf))
335    }
336
337    fn skip(&mut self, n: usize) -> io::Result<()> {
338        let mut remaining = n;
339        let mut discard = [0u8; 64];
340        while remaining > 0 {
341            let to_read = remaining.min(discard.len());
342            self.reader.read_exact(&mut discard[..to_read])?;
343            remaining -= to_read;
344        }
345        self.stream_pos += n;
346        Ok(())
347    }
348}
349
350impl Iterator for RdcFrameCaptureChunks {
351    /// `(chunk_id, chunk_data)` where chunk_data is this chunk's serialized bytes.
352    type Item = (u32, Vec<u8>);
353
354    fn next(&mut self) -> Option<(u32, Vec<u8>)> {
355        if self.done {
356            return None;
357        }
358
359        let word = match self.read_u32() {
360            Ok(0) | Err(_) => {
361                self.done = true;
362                return None;
363            }
364            Ok(w) => w,
365        };
366
367        let chunk_id = word & CHUNK_INDEX_MASK;
368
369        if word & CHUNK_FLAG_CALLSTACK != 0 {
370            let n = self.read_u32().ok()? as usize;
371            self.skip(n * 8).ok()?;
372        }
373        let meta_skip = (if word & CHUNK_FLAG_THREAD_ID != 0 {
374            8
375        } else {
376            0
377        }) + (if word & CHUNK_FLAG_DURATION != 0 {
378            8
379        } else {
380            0
381        }) + (if word & CHUNK_FLAG_TIMESTAMP != 0 {
382            8
383        } else {
384            0
385        });
386        if meta_skip > 0 {
387            self.skip(meta_skip).ok()?;
388        }
389
390        let chunk_size = if word & CHUNK_FLAG_64BIT_SIZE != 0 {
391            self.read_u64().ok()? as usize
392        } else {
393            self.read_u32().ok()? as usize
394        };
395
396        let mut data = vec![0u8; chunk_size];
397        if self.reader.read_exact(&mut data).is_err() {
398            self.done = true;
399            return None;
400        }
401        self.stream_pos += chunk_size;
402
403        // EndChunk aligns the stream to ChunkAlignment (64) on both read and write paths.
404        let padding = ((self.stream_pos + 63) & !63) - self.stream_pos;
405        if padding > 0 {
406            let _ = self.skip(padding);
407        }
408
409        Some((chunk_id, data))
410    }
411}
412
413/// For a D3D11 `CreateSwapBuffer` chunk, returns `(width, height)` of the
414/// back buffer.  Returns `None` for any other chunk type or truncated data.
415pub fn d3d11_swapchain_size_from_chunk(chunk_id: u32, data: &[u8]) -> Option<(u32, u32)> {
416    if chunk_id != D3D11_CREATE_SWAP_BUFFER {
417        return None;
418    }
419    // Serialised layout: Buffer(u32=4) + SwapbufferID(u64=8) + Width(u32) + Height(u32)
420    if data.len() < 20 {
421        return None;
422    }
423    let width = u32::from_le_bytes(data[12..16].try_into().unwrap());
424    let height = u32::from_le_bytes(data[16..20].try_into().unwrap());
425    Some((width, height))
426}
427
428#[cfg(test)]
429mod tests {
430    use super::*;
431
432    #[test]
433    fn test_parse_rdc_driver_vulkan() {
434        assert_eq!(
435            parse_rdc_driver(Path::new("src/test_data/vkcube.rdc")).unwrap(),
436            RdcDriver::Vulkan
437        );
438    }
439
440    #[test]
441    fn test_parse_rdc_driver_d3d11() {
442        assert_eq!(
443            parse_rdc_driver(Path::new(
444                "src/test_data/d3d11-humus-modernlightmapping.rdc"
445            ))
446            .unwrap(),
447            RdcDriver::D3D11
448        );
449    }
450
451    #[test]
452    fn test_parse_rdc_d3d11_swapchain_size() {
453        use std::{fs::File, io::BufReader};
454        let f =
455            BufReader::new(File::open("src/test_data/d3d11-humus-modernlightmapping.rdc").unwrap());
456        assert_eq!(
457            parse_rdc_frame_capture_chunks(f)
458                .unwrap()
459                .filter_map(|(id, data)| d3d11_swapchain_size_from_chunk(id, &data))
460                .collect::<Vec<_>>(),
461            vec![(640, 480)]
462        );
463    }
464}