.vprof) File FormatThis 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.
+------------------------------+
| Header |
+------------------------------+
| Frame 0 |
| Frame 1 |
| ... |
| Frame N-1 |
+------------------------------+
| EOF (end of file) |
+------------------------------+
| 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.
| 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. |
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:
Token_AddBudgetGroupuint8 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).
Token_AddNodeuint8 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.
Token_Timingsuint8 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).0xFFFF units
(≈262 ms) fit in the 16-bit token; 0xFFFF escapes to a 32-bit count of
the same quarter-microsecond units.calls and time are per-frame deltas ("cur frame" values), not
cumulative totals.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.
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 |
CVProfRecorder supports random access by re-reading the stream from the
header:
AddBudgetGroup / AddNode records are
idempotent (a node already present is reused by name and re-assigned its
ID).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.
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
};