1
0

VPROF_FORMAT.md 9.8 KB

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)
<recursive timings>
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
<timing for root>

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)

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
};