1use anyhow::{Context as _, Result, bail};
2use std::{
3 fmt,
4 io::{self, BufReader, Read, Seek, SeekFrom},
5 path::Path,
6};
7
8#[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
79pub fn parse_rdc_driver(path: &Path) -> Result<RdcDriver> {
81 let mut f = std::fs::File::open(path).context("opening .rdc file")?;
82
83 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 f.seek(SeekFrom::Current(16))
97 .context("seeking past progVersion")?;
98
99 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 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
131const 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
141const RDC_LZ4_BLOCK_SIZE: usize = 1024 * 1024;
143
144struct RdcLz4BlockReader<R: Read> {
152 reader: R,
153 comp_remaining: usize,
154 stream: *mut lz4_sys::LZ4StreamDecode,
155 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
246pub struct RdcFrameCaptureChunks {
253 reader: BufReader<Box<dyn Read>>,
254 stream_pos: usize,
256 done: bool,
257}
258
259pub 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)?; let _ = read_u32_le(&mut reader)?; 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 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)?; let _ = read_u64_le(&mut reader)?; 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 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 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
413pub 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 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}