// vprof_timings.txt — Source Scramble memory patches that make Source Engine // VProf captures record real per-frame timing data. // // Target: Team Fortress 2 build 11068238 (Linux x86 and x86_64), // addons/sourcemod/extensions/sourcescramble.ext. // // Background // ---------- // The VProf recorder writes the *current*-frame counters: // CVProfRecorder::Record_WriteTimings_R() reads pIn->m_nCurFrameCalls // and pIn->m_CurFrameTime // but engine/vprof_engine.cpp calls VProfRecord_Snapshot() after // CVProfile::MarkFrame() has already rotated those counters into the // *previous*-frame fields (m_nPrevFrameCalls / m_PrevFrameTime) and cleared // the current ones. The result is that every .vprof capture contains a // correct call tree with all-zero times and call counts. // // CVProfExport::CalculateBudgetGroupTimes_Recursive() additionally calls // CVProfNode::ClearPrevTime() before the recorder runs, which would wipe the // previous-frame data even if the recorder read it. // // This patch set: // 1. redirects the three current-frame field reads in // Record_WriteTimings_R() to the matching previous-frame fields, and // 2. forces VProfRecord_IsPlayingBack() to return true so that // CalculateBudgetGroupTimes_Recursive() skips ClearPrevTime(). // // The recorder still runs after the export, but the previous-frame data now // survives until it does. The recorded frame carries the tick of the frame // it was snapshotted in (a harmless one-frame label offset; the timing values // themselves are correct). // // The signatures are resolved from the unstripped engine binary's ELF // .symtab via SourceMod's `@symbol` support (TF2 reports hidden symbols, so // SourceMod uses MemoryUtils::ResolveSymbol rather than dlsym). This keeps // them independent of ASLR / load-base relocation. The byte offsets are // relative to the function entry and are build-specific; regenerate them // whenever the engine is updated. "Games" { "tf" { "Signatures" { // CVProfRecorder::Record_WriteTimings_R(CVProfNode const*) "CVProfRecorder::Record_WriteTimings_R()" { "library" "engine" "linux" "@_ZN14CVProfRecorder21Record_WriteTimings_REPK10CVProfNode" "linux64" "@_ZN14CVProfRecorder21Record_WriteTimings_REPK10CVProfNode" } // bool VProfRecord_IsPlayingBack() "VProfRecord_IsPlayingBack()" { "library" "engine" "linux" "@_Z25VProfRecord_IsPlayingBackv" "linux64" "@_Z25VProfRecord_IsPlayingBackv" } } "MemPatches" { // pIn->m_nCurFrameCalls (0x38) -> pIn->m_nPrevFrameCalls (0x44). // 32-bit: mov eax,[esi+0x38] at +0x1B, displacement at +0x1D. // 64-bit: mov eax,[rsi+0x4C] at +0x1C, displacement at +0x1E. "CVProfRecorder::Record_WriteTimings_R()::UsePrevCalls" { "signature" "CVProfRecorder::Record_WriteTimings_R()" "linux" { "offset" "1Dh" "verify" "\x38" "patch" "\x44" } "linux64" { "offset" "1Eh" "verify" "\x4C" "patch" "\x58" } } // pIn->m_CurFrameTime -> pIn->m_PrevFrameTime. // // 32-bit CCycleCount is an unaligned int64 and the compiler reads // its high dword for the 64x32 multiplication: // imul ecx,[esi+0x40],1000000 (high, 0x40 -> 0x4C, +0x53) // mul [esi+0x3C] (low, 0x3C -> 0x48, +0x5F) // 64-bit is a single 64-bit load: // imul rax,[rbx+0x50],1000000 (0x50 -> 0x60, +0x66) "CVProfRecorder::Record_WriteTimings_R()::UsePrevTimeHigh" { "signature" "CVProfRecorder::Record_WriteTimings_R()" "linux" { "offset" "53h" "verify" "\x40" "patch" "\x4C" } "linux64" { "offset" "66h" "verify" "\x50" "patch" "\x60" } } // 32-bit only: low dword of m_CurFrameTime. "CVProfRecorder::Record_WriteTimings_R()::UsePrevTimeLow" { "signature" "CVProfRecorder::Record_WriteTimings_R()" "linux" { "offset" "5Fh" "verify" "\x3C" "patch" "\x48" } } // Force VProfRecord_IsPlayingBack() to return true so the export // path does not clear the previous-frame data before the recorder // reads it: // cmp dword [...], 2 ; sete al ; ret // becomes // cmp dword [...], 2 ; mov al,1 ; nop ; ret "VProfRecord_IsPlayingBack()::SkipClearPrev" { "signature" "VProfRecord_IsPlayingBack()" "linux" { "offset" "7h" "verify" "\x0F\x94\xC0" "patch" "\xB0\x01\x90" } "linux64" { "offset" "7h" "verify" "\x0F\x94\xC0" "patch" "\xB0\x01\x90" } } } } }