|
|
hai 1 semana | |
|---|---|---|
| .. | ||
| addons | hai 1 semana | |
| tools | hai 1 semana | |
| README.md | hai 1 semana | |
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 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.
CVProfRecorder::Record_WriteTimings_R() reads m_nCurFrameCalls and
m_CurFrameTime; the patches redirect those reads to m_nPrevFrameCalls
and m_PrevFrameTime.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.)
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).
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.
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.
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.
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).
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.index.html, vprof_to_json.py) needs no changes: it already
handles both zero and non-zero timing data.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:
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);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.