# VProf timing fix — Source Scramble memory patches Runtime fix for the Source Engine `.vprof` recorder bug that produces captures with a correct call tree but **all-zero timings**. No engine recompile is required; this is a small set of byte patches applied by [Source Scramble](https://github.com/nosoop/SMExt-SourceScramble). This lives alongside the viewer in this repository, but it is installed only on the game server. ## The bug The recorder writes the *current*-frame counters: ```cpp // engine/vprof_record.cpp unsigned short curCalls = min( pIn->m_nCurFrameCalls, 0xFFFF ); unsigned long nMicroseconds = pIn->m_CurFrameTime.GetMicroseconds() / 4; ``` but `PreUpdateProfile()` calls `VProfRecord_Snapshot()` **after** `CVProfile::MarkFrame()` has rotated those counters into the previous-frame fields and cleared the current ones: ```cpp // public/tier0/vprof.h CVProfile::MarkFrame() m_Root.ExitScope(); m_Root.MarkFrame(); // m_nPrevFrameCalls = m_nCurFrameCalls; m_nCurFrameCalls = 0; (same for time) m_Root.EnterScope(); // root current calls becomes 1 ``` So the writer sees `root.calls == 1`, every other node `calls == 0`, and every time `0`. This affects 32-bit and 64-bit Linux (and Windows) server builds. The export path runs first and calls `CVProfNode::ClearPrevTime()`, so the previous-frame data would be wiped before the recorder even if the recorder read it. ## What the patches do 1. **Read the previous-frame fields in the recorder.** `CVProfRecorder::Record_WriteTimings_R()` reads `m_nCurFrameCalls` and `m_CurFrameTime`; the patches redirect those reads to `m_nPrevFrameCalls` and `m_PrevFrameTime`. 2. **Keep the previous-frame data alive until the recorder runs.** `VProfRecord_IsPlayingBack()` is forced to return `true`, which makes `CVProfExport::CalculateBudgetGroupTimes_Recursive()` skip `ClearPrevTime()`. The export path still computes its budget-group history; it just no longer clears the data first. After the patches, each recorded frame contains the just-completed frame's timings. (The tick label remains the snapshot frame's tick, so it is one frame ahead of the data — the timing values themselves are correct.) ## Requirements * Team Fortress 2 dedicated server, **build 11068238**, Linux x86 or x86_64. The offsets are build-specific; see *Updating* below. * Metamod:Source and SourceMod (1.11/1.12) installed. * The [Source Scramble](https://github.com/nosoop/SMExt-SourceScramble) extension (`sourcescramble.ext`) and its `sourcescramble_manager` plugin. The signatures use SourceMod's `@mangled` symbol form. TF2 reports its symbols as hidden, so SourceMod resolves them from the unstripped engine's ELF `.symtab` and adds the load base at runtime, which makes the patches immune to ASLR / relocation (unlike raw byte signatures containing absolute addresses). ## Installation Copy the `addons/` tree over the server's `tf/` directory: ``` addons/sourcemod/gamedata/vprof_timings.txt addons/sourcemod/configs/sourcescramble/vprof_timings.cfg ``` Then restart the server (or `sm plugins reload sourcescramble_manager`). On load the server console should print one line per patch: ``` [sourcescramble] Enabled patch "CVProfRecorder::Record_WriteTimings_R()::UsePrevCalls" from "vprof_timings" [sourcescramble] Enabled patch "CVProfRecorder::Record_WriteTimings_R()::UsePrevTimeHigh" from "vprof_timings" [sourcescramble] Enabled patch "CVProfRecorder::Record_WriteTimings_R()::UsePrevTimeLow" from "vprof_timings" [sourcescramble] Enabled patch "VProfRecord_IsPlayingBack()::SkipClearPrev" from "vprof_timings" ``` If any line instead says `Failed to verify patch`, the server binary does not match the supported build. **Do not force it** — regenerate the offsets for the new build (below) or remove the patch. ## Verifying the fix 1. Enable profiling and record a short capture: ``` vprof_on vprof_record_start mycapture ... let the server run ... vprof_record_stop ``` The file is written under the server's working directory. 2. Parse it with the tools in this repository: ```bash python vprof_to_json.py mycapture.vprof --summary-only --indent 2 ``` or open it in `index.html`. The viewer's summary should now report `"anyTime": true`, and nodes should have non-zero `total_microseconds`. Previously every time value was `0`. ## Patches applied `linux` = `engine_srv.so`, `linux64` = `bin/linux64/engine_srv.so`. | Patch | Module | Offset | Original | Patched | Purpose | |---|---|---|---:|---|---|---| | `...Record_WriteTimings_R()::UsePrevCalls` | engine | `1Dh` / `1Eh` | `38` / `4C` | `44` / `58` | read `m_nPrevFrameCalls` instead of `m_nCurFrameCalls` | | `...::UsePrevTimeHigh` | engine | `53h` / `66h` | `40` / `50` | `4Ch` / `60h` | read `m_PrevFrameTime` instead of `m_CurFrameTime` | | `...::UsePrevTimeLow` | engine | `5Fh` (32-bit only) | `3Ch` | `48h` | low dword of the 32-bit `int64` time read | | `VProfRecord_IsPlayingBack()::SkipClearPrev` | engine | `7h` | `0F 94 C0` | `B0 01 90` | make the predicate return `1`, skipping `ClearPrevTime()` | The 64-bit build has a single 64-bit time load, so `UsePrevTimeLow` has no `linux64` block. On a 64-bit server that entry resolves but is a no-op (the manager will still log it as enabled). ## Caveats * These are position/byte-specific patches. A TF2 update that touches `engine_srv.so` will make `verify` fail; re-derive the offsets rather than disabling verification. * `VProfRecord_IsPlayingBack()` is also consulted by the VProf VGUI panels. Dedicated servers have no such UI, so there is no practical effect there. On a listen server the VProf UI may treat the session as if a recording were being played back. * Existing all-zero captures cannot be repaired; re-record after installing. * The viewer (`index.html`, `vprof_to_json.py`) needs no changes: it already handles both zero and non-zero timing data. ## Updating for a new TF2 build Symbols are present in the unstripped `engine_srv.so`, so the offsets can be re-derived with `nm` + `objdump`: ```bash nm engine_srv.so | grep -E \ '_ZN14CVProfRecorder21Record_WriteTimings_REPK10CVProfNode|_Z25VProfRecord_IsPlayingBackv' ``` Disassemble just the relevant ranges (do not `objdump -d` the whole module on a small VM) and locate: * the `mov`/`imul`/`mul` displacements reading `m_nCurFrameCalls` and `m_CurFrameTime` (cross-check against `CVProfNode::MarkFrame()` in `libtier0_srv.so`, which shows the current and previous field offsets); * the `setX al` in `VProfRecord_IsPlayingBack()`. Then update only the `offset`/`verify`/`patch` values in `gamedata/vprof_timings.txt`. The signatures themselves are `@mangled` symbols, so they do not depend on the load base (ASLR) and normally only change if Valve renames or removes the functions. Validate offline with the bundled tool (pass the two `engine_srv.so` binaries): ```bash python3 server-patch/tools/verify_gamedata.py \ server-patch/addons/sourcemod/gamedata/vprof_timings.txt \ engine_srv.so engine_srv.x64.so ``` `verify_gamedata.py` parses the ELF `.symtab` to resolve the `@symbol` signatures the same way SourceMod's `MemoryUtils::ResolveSymbol` does, then applies Source Scramble's offset/verify/patch semantics, so a passing run means the gamedata will resolve and apply on the server.