// 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"
				}
			}
		}
	}
}