diff options
Diffstat (limited to 'tools/capture-terrain-shade-cache.ps1')
| -rw-r--r-- | tools/capture-terrain-shade-cache.ps1 | 272 |
1 files changed, 0 insertions, 272 deletions
diff --git a/tools/capture-terrain-shade-cache.ps1 b/tools/capture-terrain-shade-cache.ps1 deleted file mode 100644 index 5c7b10d..0000000 --- a/tools/capture-terrain-shade-cache.ps1 +++ /dev/null @@ -1,272 +0,0 @@ -[CmdletBinding()] -param( - [Parameter(Mandatory = $true)] - [int]$ProcessId, - [ValidateRange(1, 60)] - [int]$CaptureAttempts = 6, - [ValidateRange(0, 1000)] - [int]$RetryIntervalMilliseconds = 100, - [ValidateRange(0x00010000, 0x7fff0000)] - [UInt32]$SearchStart = 0x01000000, - [ValidateRange(0x00010000, 0x7fff0000)] - [UInt32]$SearchEnd = 0x20000000 -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -# This observer deliberately opens only PROCESS_QUERY_INFORMATION | PROCESS_VM_READ. -# It neither sends input nor writes, suspends, injects into, or calls the original game. -Add-Type -TypeDefinition @' -using System; -using System.Runtime.InteropServices; - -public static class FparkanTerrainShadeProbe { - public const uint TH32CS_SNAPMODULE = 0x00000008; - public const uint TH32CS_SNAPMODULE32 = 0x00000010; - public const uint PROCESS_QUERY_INFORMATION = 0x00000400; - public const uint PROCESS_VM_READ = 0x00000010; - public const uint MEM_COMMIT = 0x1000; - public const uint PAGE_NOACCESS = 0x01; - public const uint PAGE_GUARD = 0x100; - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - public struct MODULEENTRY32 { - public uint dwSize; - public uint th32ModuleID; - public uint th32ProcessID; - public uint GlblcntUsage; - public uint ProccntUsage; - public IntPtr modBaseAddr; - public uint modBaseSize; - public IntPtr hModule; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string szModule; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string szExePath; - } - - [StructLayout(LayoutKind.Sequential)] - public struct MEMORY_BASIC_INFORMATION { - public IntPtr BaseAddress; - public IntPtr AllocationBase; - public uint AllocationProtect; - public IntPtr RegionSize; - public uint State; - public uint Protect; - public uint Type; - } - - [DllImport("kernel32.dll", SetLastError = true)] - public static extern IntPtr CreateToolhelp32Snapshot(uint flags, uint processId); - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern bool Module32First(IntPtr snapshot, ref MODULEENTRY32 entry); - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern bool Module32Next(IntPtr snapshot, ref MODULEENTRY32 entry); - [DllImport("kernel32.dll", SetLastError = true)] - public static extern IntPtr OpenProcess(uint access, bool inheritHandle, uint processId); - [DllImport("kernel32.dll", SetLastError = true)] - public static extern bool ReadProcessMemory( - IntPtr process, IntPtr address, [Out] byte[] buffer, IntPtr size, out IntPtr bytesRead); - [DllImport("kernel32.dll", SetLastError = true)] - public static extern IntPtr VirtualQueryEx( - IntPtr process, IntPtr address, out MEMORY_BASIC_INFORMATION buffer, IntPtr length); - [DllImport("kernel32.dll", SetLastError = true)] - public static extern bool CloseHandle(IntPtr handle); - - public static int FindU32(byte[] bytes, uint expected, int start) { - for (int index = start; index <= bytes.Length - 4; index += 4) { - if (BitConverter.ToUInt32(bytes, index) == expected) return index; - } - return -1; - } - - public static ulong Fnv1a64(byte[] bytes, int offset, int length) { - ulong hash = 14695981039346656037UL; - unchecked { - for (int index = offset; index < offset + length; index++) { - hash ^= bytes[index]; - hash *= 1099511628211UL; - } - } - return hash; - } -} -'@ - -function Get-LastWin32ErrorText { - $code = [Runtime.InteropServices.Marshal]::GetLastWin32Error() - "$code ($([ComponentModel.Win32Exception]::new($code).Message))" -} - -function Get-TerrainModuleBase { - param([int]$ProbeProcessId) - $snapshot = [FparkanTerrainShadeProbe]::CreateToolhelp32Snapshot( - [FparkanTerrainShadeProbe]::TH32CS_SNAPMODULE -bor [FparkanTerrainShadeProbe]::TH32CS_SNAPMODULE32, - [uint32]$ProbeProcessId - ) - if ($snapshot -eq [IntPtr]::Zero -or $snapshot.ToInt64() -eq -1) { - throw "CreateToolhelp32Snapshot failed: $(Get-LastWin32ErrorText)" - } - try { - $module = [FparkanTerrainShadeProbe+MODULEENTRY32]::new() - $module.dwSize = [Runtime.InteropServices.Marshal]::SizeOf([type][FparkanTerrainShadeProbe+MODULEENTRY32]) - if (-not [FparkanTerrainShadeProbe]::Module32First($snapshot, [ref]$module)) { - throw "Module32First failed: $(Get-LastWin32ErrorText)" - } - do { - if ($module.szModule -ieq 'Terrain.dll') { - return $module.modBaseAddr.ToInt64() - } - $module = [FparkanTerrainShadeProbe+MODULEENTRY32]::new() - $module.dwSize = [Runtime.InteropServices.Marshal]::SizeOf([type][FparkanTerrainShadeProbe+MODULEENTRY32]) - } while ([FparkanTerrainShadeProbe]::Module32Next($snapshot, [ref]$module)) - } finally { - [void][FparkanTerrainShadeProbe]::CloseHandle($snapshot) - } - throw "Terrain.dll is not loaded by process $ProbeProcessId" -} - -function Read-Bytes { - param([IntPtr]$Process, [Int64]$Address, [int]$Length) - $buffer = [byte[]]::new($Length) - $read = [IntPtr]::Zero - if (-not [FparkanTerrainShadeProbe]::ReadProcessMemory( - $Process, [IntPtr]$Address, $buffer, [IntPtr]$Length, [ref]$read)) { - return $null - } - if ($read.ToInt64() -ne $Length) { - return $null - } - return $buffer -} - -function Find-ShadeCache { - param( - [IntPtr]$Process, - [UInt32]$ExpectedVtable, - [UInt32]$Start, - [UInt32]$End, - [Int64]$ExcludedImageStart, - [Int64]$ExcludedImageEnd - ) - $mbiSize = [Runtime.InteropServices.Marshal]::SizeOf([type][FparkanTerrainShadeProbe+MEMORY_BASIC_INFORMATION]) - [Int64]$cursor = $Start - while ($cursor -lt $End) { - $mbi = [FparkanTerrainShadeProbe+MEMORY_BASIC_INFORMATION]::new() - $queried = [FparkanTerrainShadeProbe]::VirtualQueryEx( - $Process, [IntPtr]$cursor, [ref]$mbi, [IntPtr]$mbiSize - ) - if ($queried -eq [IntPtr]::Zero) { break } - $base = $mbi.BaseAddress.ToInt64() - $size = $mbi.RegionSize.ToInt64() - if ($size -le 0) { break } - $next = $base + $size - if ($mbi.State -eq [FparkanTerrainShadeProbe]::MEM_COMMIT -and - ($mbi.Protect -band [FparkanTerrainShadeProbe]::PAGE_NOACCESS) -eq 0 -and - ($mbi.Protect -band [FparkanTerrainShadeProbe]::PAGE_GUARD) -eq 0) { - $regionStart = [Math]::Max($base, [Int64]$Start) - $regionEnd = [Math]::Min($next, [Int64]$End) - for ([Int64]$offset = $regionStart; $offset -lt $regionEnd; $offset += 65536) { - $length = [int][Math]::Min(65536, $regionEnd - $offset) - $bytes = Read-Bytes $Process $offset $length - if ($null -eq $bytes) { continue } - $searchIndex = 0 - while ($searchIndex -le $bytes.Length - 4) { - $index = [FparkanTerrainShadeProbe]::FindU32($bytes, $ExpectedVtable, $searchIndex) - if ($index -lt 0) { break } - $candidate = $offset + $index - if ($candidate -lt $ExcludedImageStart -or $candidate -ge $ExcludedImageEnd) { - return $candidate - } - $searchIndex = $index + 4 - } - } - } - $cursor = [Math]::Max($next, $cursor + 4096) - } - return $null -} - -function Get-ShadeCacheSummary { - param([IntPtr]$Process, [Int64]$Cache, [byte[]]$Header) - $entryTable = [BitConverter]::ToUInt32($Header, 316) - $entryCount = [BitConverter]::ToUInt32($Header, 320) - $summary = [ordered]@{ - materialized_entries = $null - profile_banks = @() - } - # The count field is runtime data. Keep a hard observer bound so a corrupt - # or stale candidate cannot turn a passive sample into an excessive read. - if ($entryTable -lt 0x1000 -or $entryCount -gt 100000) { return $summary } - $entryBytes = Read-Bytes $Process $entryTable ([int]($entryCount * 8)) - if ($null -eq $entryBytes) { return $summary } - $materialized = 0 - $banks = [System.Collections.Generic.SortedSet[int]]::new() - for ($index = 0; $index -lt $entryCount; $index++) { - $offset = $index * 8 - if ([BitConverter]::ToUInt32($entryBytes, $offset) -ge 0x1000) { - $materialized++ - [void]$banks.Add([int]$entryBytes[$offset + 4]) - } - } - $summary.materialized_entries = $materialized - if ($banks.Count -eq 0) { return $summary } - $maxBank = $banks.Max - if ($maxBank -ge 100) { return $summary } - $bankBytes = Read-Bytes $Process ($Cache + 332) (($maxBank + 1) * 212) - if ($null -eq $bankBytes) { return $summary } - $profiles = [System.Collections.Generic.List[object]]::new() - foreach ($bank in $banks) { - $profiles.Add([ordered]@{ - bank = $bank - fnv1a64 = [FparkanTerrainShadeProbe]::Fnv1a64($bankBytes, $bank * 212, 212).ToString() - }) - } - $summary.profile_banks = @($profiles) - return $summary -} - -if ($SearchEnd -le $SearchStart) { throw 'SearchEnd must exceed SearchStart' } -$terrainBase = Get-TerrainModuleBase $ProcessId -$process = [FparkanTerrainShadeProbe]::OpenProcess( - [FparkanTerrainShadeProbe]::PROCESS_QUERY_INFORMATION -bor [FparkanTerrainShadeProbe]::PROCESS_VM_READ, - $false, - [uint32]$ProcessId -) -if ($process -eq [IntPtr]::Zero) { throw "OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ) failed: $(Get-LastWin32ErrorText)" } - -try { - $expectedVtable = [uint32]($terrainBase + 0x643d0) - $last = 'cache vtable was not present in readable scan range' - for ($attempt = 1; $attempt -le $CaptureAttempts; $attempt++) { - # The vtable value itself naturally appears in the Terrain image as - # relocation data; only a heap-resident object is a cache candidate. - $cache = Find-ShadeCache $process $expectedVtable $SearchStart $SearchEnd $terrainBase ($terrainBase + 0x100000) - if ($null -ne $cache) { - $header = Read-Bytes $process $cache 324 - if ($null -ne $header) { - $summary = Get-ShadeCacheSummary $process $cache $header - [ordered]@{ - schema = 'fparkan-terrain-shade-cache-v1' - process_id = $ProcessId - terrain_module_base = ('0x{0:X8}' -f $terrainBase) - cache_vtable_rva = '0x643d0' - cache_object = ('0x{0:X8}' -f $cache) - result_view = ('0x{0:X8}' -f [BitConverter]::ToUInt32($header, 24)) - entry_table = ('0x{0:X8}' -f [BitConverter]::ToUInt32($header, 316)) - entry_count = [BitConverter]::ToUInt32($header, 320) - materialized_entries = $summary.materialized_entries - profile_banks = $summary.profile_banks - scan_attempt = $attempt - } | ConvertTo-Json -Compress - exit 0 - } - $last = "cache candidate 0x$('{0:X8}' -f $cache) became unreadable" - } - if ($attempt -lt $CaptureAttempts -and $RetryIntervalMilliseconds -gt 0) { - Start-Sleep -Milliseconds $RetryIntervalMilliseconds - } - } - throw "No live GetShade cache after $CaptureAttempts scans ($last)" -} finally { - [void][FparkanTerrainShadeProbe]::CloseHandle($process) -} |
