Skip to content

Capture File Format

baldurk edited this page Apr 21, 2014 · 1 revision

Capture File Format

Header

The capture file format starts with a simple header (see Serialiser::Serialiser, Serialiser::DebuggerHeader).

struct Header
{
  uint64_t magic;         // this must be RDOC in the least significant 32 bits.
  uint64_t version;       // version number of the file. As of this writing, 0x30
  uint64_t fileSize;      // total file size including this header
  uint64_t resolveDBSize; // size of callstack resolving information, immediately following this header (or 0)
};

After the header, if resolveDBSize is set to 0 come the chunks that form the file proper. If there is callstack resolving information it follows the header in this format, listing all modules open at the time of capture (see Callstack::MakeResolver):

struct Module
{
  uint64_t baseAddr;      // the base address of the module's code segment in address space
  uint32_t size;          // the size of the module's code segment
  uint32_t age;           // age of the module (from MS pdb identifier)
  GUID     guid;          // 16 byte guid identifying the module's debug information
  size_t   nameLength;    // TODO: FIX THIS. size_t indicating how many wchar_ts follow with the name of the module
  wchar_t  name[];        // TODO: FIX THIS. null-terminated wide string of length nameLength
};

(note that nameLength/name aren't portable and need to be fixed).

Chunks

Following the header there are a series of chunks, a couple of which are defined but most are up to the driver implementation that produced this chunk. The chunk format is as follows:

uint16_t chunkIdx = ReadBytes(2); // the bottom 14 bits are the type of the chunk. Mostly implementation-defined
                              // 0x8000 = this chunk has a callstack
                              // 0x4000 = this chunk is 'small'. Used for chunks that need minimal overhead. See below

if(chunkIdx == 0)
  // special case - see below 

if(chunkIdx & 0x8000)
{
  uint8_t numLevels = ReadBytes(1);           // If this chunk has a callstack associated with it, a byte indicates the number of levels.
  uint64_t stack[] = ReadBytes(8*numLevels); // a 64bit address array (regardless of the bitness of the application). Length above.
}

if(chunkIdx & 0x4000)
  uint16_t chunkLength = ReadBytes(2);   // length of the chunk data (not this header)
else
  uint32_t chunkLength = ReadBytes(4);   // length of the chunk data (not this header)

// chunk data follows
// there is no chunk footer

Note that all chunks have at least 4 bytes overhead to encode the type & length of the chunk.

Chunk type 0 is invalid as an actual chunk, so when it is 0 this indicates some special case control bytes to the reader. Usually these chunks are inserted by the serialiser itself and not by user code.

There can be more than one such series of control bytes, so pseudocode to parse them goes like so:

while(chunkIdx == 0)
{
  // get type of control byte sequence
  uint8_t controlByte = ReadBytes(1);

  // .. act on controlByte, reading more data potentially ..

  // read real chunk type, or next control byte indicator (0)
  chunkIdx = ReadBytes(2);
}

Currently the only control byte is 0. This indicates some padding bytes as the chunk data might require some alignment greater than the base alignment for the overall stream of data. In this case, the control byte is followed by one more byte indicating how many padding bytes to read afterwards. This padding length might be 0, as it could be that the chunkIdx, control byte, then padding length (so 3 bytes total) may have satisfied the alignment requirements.

uint16_t chunkIdx;       // 0 to indicate control bytes
uint8_t controlByteType; // 0 - padding sequence
uint8_t paddingLength;   // [0..255] padding bytes follow
uint8_t padding[];       // padding bytes, read and ignore (could be 0 length array)

Specially defined chunks

Most chunks are defined by the implementation of whichever driver wrote this log, but some are guaranteed to provide some introspection of captures with no knowledge/understanding of the driver used (see RenderDoc::OpenWriteSerialiser, SystemChunks).

The first chunk is thumbnail data, type 2. Its format is:

bool HasThumbnail;    // true or false if this chunk contains a thumbnail.

if(HasThumbnail)
{
  uint32_t Width;
  uint32_t Height;
  Buffer Pixels;      // See Serialiser::SerialiseBuffer for how buffers are serialised.
}

By convention the Pixels buffer contains jpeg-encoded data and it is heavily suggested that driver implementations follow this - Rich Geldreich's excellent jpeg compressor is provided and easy to use. This isn't required though, so users shouldn't assume the pixel data will be jpeg format.

Following this chunk is one containing create parameters, with the type of the API used in the log and any API-specific data used to initialise the driver (so that the driver can read just this data to determine e.g. if the hardware capabilities to replay the log are present, etc). Most of its contents are driver-defined but there are two fields at the start used for locating the right driver and handling when a driver isn't available for a log.

RDCDriver Driver;     // an enum with a globally registered list of APIs. Values less than 100000 for this
                      // enum are illegal to use unless they match the enum in the original official source repository.
                      // this allows every implementation/fork/etc to stay in sync and ensure that when a log says it uses
                      // driver 5, everyone agrees what API that refers to.
                      // values above 100000 are undefined and can be used for private API drivers that don't get
                      // distributed, drivers that haven't been registered yet, etc. There's no guarantee that if you see
                      // a log with driver 100004 that your implementation's 100004 is the same. It's up to the user to
                      // ensure things match at this point.
wstring DriverName;   // Purely for informational purposes, this is the name the driver called itself. It's generally
                      // the same no matter what for an implementation (ie. D3D11 not D3D11.0, D3D11.1, D3D11.2), but
                      // its only purpose is for displaying a friendly name to the user when encountering an API that
                      // the local implementation doesn't understand.

Everything after this in the chunk is to be consumed by the API driver itself.

Serialising guidelines

When serialising bear in mind that 64bit logs must load in a 32bit app and vice-versa, and that logs must load on a different platform than they were saved from, at least until the driver implementation reads them and you can choose to reject a log from a different platform at that point if you wish.

For this reason make sure you only ever serialise explicitly sized types like uint32_t, int64_t, etc. Don't serialise size_t as it's different between 32bit and 64bit. Don't serialise long as it might be defined to be 32bit or 64bit depending on the compiler. Strings must be encoded as UTF-8 - no exceptions. This can include strings that are always just plain ASCII if that makes sense.

Clone this wiki locally