1
0
nosoop 53f2c6e2e4 Initial commit 1 nedēļu atpakaļ
..
addons 53f2c6e2e4 Initial commit 1 nedēļu atpakaļ
tools 53f2c6e2e4 Initial commit 1 nedēļu atpakaļ
README.md 53f2c6e2e4 Initial commit 1 nedēļu atpakaļ

README.md

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.

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:

// 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:

// 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 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:

    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:

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):

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.