# Source Engine VProf (`.vprof`) File Format This document describes the binary file format produced by the Source Engine's **VProf** profiler when recording a capture with `vprof_record_start`. The reference implementation lives in `engine/vprof_record.cpp` (class `CVProfRecorder`) with supporting definitions in `public/tier0/vprof.h` and `tier0/vprof.cpp`. A `.vprof` file is a *frame-by-frame recording* of the VProf tree: the tree structure (nodes, their parent, budget group) is written incrementally as it changes, and after each structural update a set of timing samples is written for the whole tree. Everything is **little-endian**. Integers are fixed-width; the code compiles with `long` == 4 bytes (the writer explicitly uses `unsigned long` only for the extended microsecond field, handled as a `uint32`). Strings are NUL-terminated, typically ASCII/Windows-1252; UTF-8 is accepted on read. --- ## 1. File layout ``` +------------------------------+ | Header | +------------------------------+ | Frame 0 | | Frame 1 | | ... | | Frame N-1 | +------------------------------+ | EOF (end of file) | +------------------------------+ ``` ### 1.1 Header | Offset | Type | Field | Notes | |-------:|-----------|-----------------|--------------------------------------| | 0 | `int32` | `version` | Format version. Currently always `1` (`VPROF_FILE_VERSION`). | | 4 | `int32` | `root_node_id` | Unique ID of the tree's root node (usually `0`). | After the header the file is a stream of byte **tokens**. The first token of each frame must be `Token_StartFrame` (`0`). A token value of `5` (`TOKEN_FILE_FINISHED`) is reported by the reader when a read runs off the end of the file; it is not physically present in a well-formed file. ### 1.2 Token values | Value | Name | Meaning | |------:|---------------------|---------------------------------------------| | `0` | `Token_StartFrame` | Begins a frame; followed by a `int32` tick. | | `1` | `Token_AddNode` | Declares a new tree node. | | `2` | `Token_AddBudgetGroup` | Declares a new budget group. | | `3` | `Token_Timings` | Followed by recursive timings for the whole tree. | | `4` | `Token_EndOfFrame` | Ends the current frame. | | `5` | `TOKEN_FILE_FINISHED` | Pseudo-token; never written, returned at EOF. | --- ## 2. Frame structure Each frame is written by `CVProfRecorder::Record_Snapshot()` as: ``` Token_StartFrame (uint8, value 0) tickcount (int32) [Token_AddBudgetGroup ...] (zero or more) [Token_AddNode ...] (zero or more) Token_Timings (uint8, value 3) Token_EndOfFrame (uint8, value 4) ``` The order of the structural sections matters: 1. The tick is written first. 2. **Budget groups** are written before any node that might reference them. Only groups that did not already exist in the recorder's own profile are written, so indices are stable across frames. 3. **New nodes** are written (depth-first), then: 4. **Timings** for every node currently in the tree, in pre-order (node, then its children in sibling order). ### 2.1 `Token_AddBudgetGroup` ``` uint8 token = 2 char[] name NUL-terminated int32 flags combination of BUDGETFLAG_* (see section 3) ``` A budget group's numeric ID is its insertion index into the recorder's budget group table. The base `CVProfile` constructor pre-registers 31 default groups (see section 4), so a file recorded by that build only emits groups *after* those 31 positions. Other builds emit all groups from index 0. Consumers should therefore treat the default groups as always present, and append unknown names after them (matching by name for known defaults). ### 2.2 `Token_AddNode` ``` uint8 token = 1 int32 parent_node_id char[] node_name NUL-terminated int32 budget_group_id int32 node_id unique node ID (cvprof node ID) ``` `parent_node_id` always refers to a node that already exists (the root was declared in the header, or the parent was added earlier in the stream). Nodes are never removed during a recording. **Child ordering.** `CVProfNode::GetSubNode()` inserts a new child at the *head* of the parent's child list, and the recorder writes new nodes in an order that compensates for this. To reconstruct the tree exactly as the engine sees it, a new `AddNode` must be **prepended** to the parent's children list. ### 2.3 `Token_Timings` ``` uint8 token = 3 ``` Timings are written recursively by `Record_WriteTimings_R` / read by `Playback_ReadTimings_R`. For each node, in pre-order: ``` uint8 calls_token if calls_token == 255: uint16 cur_frame_calls else: cur_frame_calls = calls_token uint16 time_token if time_token == 0xFFFF: uint32 microseconds_div4 cur_frame_time_us = microseconds_div4 * 4 else: cur_frame_time_us = time_token * 4 ``` Then the same records are written for each child, in sibling order. Notes: * `calls` is 0–254 in the single byte; `255` is an escape for a 16-bit count (up to 65535). * Time is stored in **units of 4 microseconds**. Values below `0xFFFF` units (≈262 ms) fit in the 16-bit token; `0xFFFF` escapes to a 32-bit count of the same quarter-microsecond units. * Both `calls` and time are per-frame deltas ("cur frame" values), not cumulative totals. --- ## 3. Budget group flags `BUDGETFLAG_*` bit values (from `public/tier0/vprof.h`): | Bit | Name | Value | |-----------|------------------|-------:| | `1 << 0` | `BUDGETFLAG_CLIENT` | `1` | | `1 << 1` | `BUDGETFLAG_SERVER` | `2` | | `1 << 2` | `BUDGETFLAG_OTHER` | `4` | | `1 << 15` | `BUDGETFLAG_HIDDEN` | `32768` | | | `BUDGETFLAG_ALL` | `0xFFFF` | Examples: `Unaccounted = SERVER | OTHER = 6`, `World Rendering = CLIENT = 1`. --- ## 4. Default budget groups `CVProfile::CVProfile()` registers these 31 groups in this exact order, so IDs 0–30 are stable. A parser that sees only higher-numbered `Token_AddBudgetGroup` records should still map IDs 0–30 to these names. | ID | Name | Flags | Decoded | |---:|-------------------------------|------:|--------------------| | 0 | `Unaccounted` | 6 | SERVER, OTHER | | 1 | `World Rendering` | 1 | CLIENT | | 2 | `Displacement_Rendering` | 1 | CLIENT | | 3 | `Game` | 6 | SERVER, OTHER | | 4 | `Player` | 6 | SERVER, OTHER | | 5 | `NPCs` | 6 | SERVER, OTHER | | 6 | `Server Animation` | 6 | SERVER, OTHER | | 7 | `Client_Animation` | 1 | CLIENT | | 8 | `Physics` | 6 | SERVER, OTHER | | 9 | `Static_Prop_Rendering` | 1 | CLIENT | | 10 | `Other_Model_Rendering` | 1 | CLIENT | | 11 | `Fast Path Model Rendering` | 1 | CLIENT | | 12 | `Light_Cache` | 1 | CLIENT | | 13 | `Brush_Model_Rendering` | 1 | CLIENT | | 14 | `Shadow_Rendering` | 1 | CLIENT | | 15 | `Detail_Prop_Rendering` | 1 | CLIENT | | 16 | `Particle/Effect_Rendering` | 1 | CLIENT | | 17 | `Ropes` | 1 | CLIENT | | 18 | `Dynamic_Light_Rendering` | 1 | CLIENT | | 19 | `Networking` | 6 | SERVER, OTHER | | 20 | `Sound` | 6 | SERVER, OTHER | | 21 | `VGUI` | 6 | SERVER, OTHER | | 22 | `FileSystem` | 6 | SERVER, OTHER | | 23 | `Prediction` | 1 | CLIENT | | 24 | `Interpolation` | 1 | CLIENT | | 25 | `Swap_Buffers` | 1 | CLIENT | | 26 | `Occlusion` | 1 | CLIENT | | 27 | `Overlays` | 1 | CLIENT | | 28 | `Tools` | 5 | CLIENT, OTHER | | 29 | `Texture_Cache` | 1 | CLIENT | | 30 | `Replay` | 2 | SERVER | --- ## 5. Playback semantics (why the format works) `CVProfRecorder` supports random access by re-reading the stream from the header: * Seeking **forward** reads frames until the wanted tick is reached. * Seeking **backward** clears the tree and replays from the first frame, relying on the fact that `AddBudgetGroup` / `AddNode` records are idempotent (a node already present is reused by name and re-assigned its ID). * Because structure changes are only ever additions, the union of all `AddNode` records yields the final tree. A parser can simply keep a `node_id → node` map while streaming. `Playback_ReadTick` also relies on the exact frame envelope: it reads `Token_StartFrame`, the tick, then loops over inner tokens until `Token_EndOfFrame`. --- ## 6. Quick reference (pseudo-struct) ```c struct VProfHeader { int32 version; // 1 int32 root_node_id; }; // Stream of frames: struct VProfFrame { uint8 start_frame; // 0 int32 tick; // repeated until timings: uint8 token; // 1 = AddNode, 2 = AddBudgetGroup union { struct { int32 parent; cstring name; int32 budget_group; int32 node; } node; struct { cstring name; int32 flags; } group; }; uint8 timings; // 3 // recursive pre-order: // uint8 calls (255 => uint16) // uint16 time_div4 (0xFFFF => uint32), microseconds = value * 4 uint8 end_of_frame; // 4 }; ```