using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; using System.Web.Script.Serialization; using System.Drawing; using System.Drawing.Imaging; internal static partial class NativeFrameCapture { private const uint NgiRendererGlobalRva = 0x3A460; private const uint NgiDeviceGlobalRva = 0x3A488; private const uint NgiRendererVtableRva = 0x315E0; private const uint NgiWindowedPresentRva = 0x6E1B; private const uint NgiFullscreenPresentRva = 0x6E38; private const uint DdLockReadOnly = 0x00000010; private const uint DdLockDoNotWait = 0x00004000; private const int D3dDeviceGetRenderTargetOffset = 0x24; private const int DdSurfaceLockOffset = 0x64; private const int DdSurfaceUnlockOffset = 0x80; private const int DdSurfaceReleaseOffset = 0x08; private const uint DdErrSurfaceBusy = 0x887601AE; private const uint DdErrWasStillDrawing = 0x8876021C; private const uint DdsdPitch = 0x00000008; private const uint DdsdHeight = 0x00000002; private const uint DdsdWidth = 0x00000004; private const uint DdsdPixelFormat = 0x00001000; private const uint DdsdLpSurface = 0x00000800; private const uint DdpfFourCc = 0x00000004; private const uint DdpfRgb = 0x00000040; private const int DdSurfaceDesc2Size = 124; private const int CameraInputJsonLimit = 1024 * 1024; private const uint ContextAllX86 = 0x0001003F; // THREAD_QUERY_INFORMATION from the Windows SDK; required by NtQueryInformationThread. private const uint ThreadQueryInformation = 0x00000040; private const int ContextEflagsOffset = 192; private const int PixelReadbackLimit = 64 * 1024 * 1024; private const uint RemoteCodePage = 0x1000; private const uint RemoteDataOffset = 0x1000; private const uint RemoteAllocationBytes = 0x2000; private const uint RemoteStubMaxCallStackBytes = 24; private const uint PageGuard = 0x00000100; private const uint RemoteDataStatusOffset = 0; private const uint RemoteDataGetHrOffset = 4; private const uint RemoteDataLockHrOffset = 8; private const uint RemoteDataUnlockHrOffset = 12; private const uint RemoteDataReleaseCountOffset = 16; private const uint RemoteDataSurfaceOffset = 20; private const uint RemoteDataDescOffset = 24; private const uint CameraSetterMatrixOffset = 64; private const uint CameraTransformInterfaceVtableRva = 0x66558; private const uint CameraTransformSetterRva = 0x54C70; private const uint CameraTransformInvalidatorRva = 0x55280; private const uint CameraTransformSetterSlotOffset = 0x1C; private const uint CameraTransformInvalidatorSlotOffset = 0x30; private const uint RenderFunctionStackFrameBytes = 0x70; [StructLayout(LayoutKind.Sequential)] private struct ClientIdX86 { public IntPtr UniqueProcess; public IntPtr UniqueThread; } [StructLayout(LayoutKind.Sequential)] private struct ThreadBasicInformationX86 { public int ExitStatus; public IntPtr TebBaseAddress; public ClientIdX86 ClientId; public UIntPtr AffinityMask; public int Priority; public int BasePriority; } [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr VirtualAllocEx(IntPtr process, IntPtr address, UIntPtr size, uint allocationType, uint protection); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool VirtualFreeEx(IntPtr process, IntPtr address, UIntPtr size, uint freeType); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool TerminateProcess(IntPtr process, uint exitCode); [DllImport("ntdll.dll")] private static extern int NtQueryInformationThread(IntPtr thread, int informationClass, out ThreadBasicInformationX86 information, int informationLength, IntPtr returnLength); private sealed class SurfaceReadback { public uint Width; public uint Height; public int Pitch; public uint BitCount; public uint RedMask; public uint GreenMask; public uint BlueMask; public uint AlphaMask; public uint SurfacePointer; public byte[] Rows; public string Sha256; public bool RgbMasksVerified; } private sealed class ProjectionReadback { public int[] Viewport; public float Near; public float Far; public float Fov; public byte Mode; public uint Renderer; public uint RendererVtable; public bool Verified; public string Failure; } private sealed class CameraReadback { public uint Camera; public uint Vtable; public uint TransformInterface; public uint TransformVtable; public byte CameraMode; public uint EntryEsp; public uint ReturnAddress; public uint SelectorField; public uint[] Words; public byte[] EntryMatrixBytes; public byte[] ProjectionMatrixBytes; public bool CurrentAtProjectionVerified; public bool WordsChangedAtProjection; public string EntryMatrixSha256; public string ProjectionMatrixSha256; public bool MatrixFinite; public bool LayoutVerified; public string Failure; } private sealed class SelectedCameraInput { public string Path; public byte[] MatrixBytes; public uint[] MatrixWords; public string MatrixSha256; public int[] Viewport; public float Near; public float Far; public float FieldOfView; public byte ProjectionMode; } private enum CameraSetterPhase { Apply, Restore } private sealed class RemoteCameraSetterSession { public RemoteCode Code; public uint Region; public uint Camera; public uint ThreadId; public uint OriginalEsp; public byte[] OriginalContext; public byte[] CallerStack; public uint FunctionEntryEsp; public uint FunctionReturnAddress; public uint EsiAtBoundary; public byte[] FunctionCallerStack; public byte[] MatrixBytes; public CameraSetterPhase Phase; public BreakpointInfo ResumeBreakpoint; public bool RearmOnStep; public DateTime DeadlineUtc; } private sealed class RemoteCode { public uint Base; public uint TrapAddress; public uint DataBase; public byte[] Bytes; public int[] CallInstructionOffsets; public int MaxCallStackBytes; } private static IntPtr AllocateAlignedX86Context(out IntPtr allocation) { allocation = Marshal.AllocHGlobal(X86ContextSize + 15); long raw = allocation.ToInt64(); return new IntPtr((raw + 15L) & ~15L); } private static byte[] GetFullX86Context(IntPtr thread) { IntPtr allocation; IntPtr context = AllocateAlignedX86Context(out allocation); try { Marshal.Copy(new byte[X86ContextSize], 0, context, X86ContextSize); Marshal.WriteInt32(context, 0, unchecked((int)ContextAllX86)); if (!GetThreadContext(thread, context)) throw new InvalidOperationException("GetThreadContext(CONTEXT_ALL_X86) failed: " + Marshal.GetLastWin32Error()); byte[] bytes = new byte[X86ContextSize]; Marshal.Copy(context, bytes, 0, bytes.Length); return bytes; } finally { Marshal.FreeHGlobal(allocation); } } private static void SetFullX86Context(IntPtr thread, byte[] bytes) { if (bytes == null || bytes.Length != X86ContextSize) throw new ArgumentException("Expected a complete x86 CONTEXT buffer.", "bytes"); IntPtr allocation; IntPtr context = AllocateAlignedX86Context(out allocation); try { Marshal.Copy(bytes, 0, context, bytes.Length); Marshal.WriteInt32(context, 0, unchecked((int)ContextAllX86)); if (!SetThreadContext(thread, context)) throw new InvalidOperationException("SetThreadContext(CONTEXT_ALL_X86) failed: " + Marshal.GetLastWin32Error()); } finally { Marshal.FreeHGlobal(allocation); } } private static byte[] ContextForRemoteCode(byte[] original, uint codeAddress, uint stackPointer) { if (original == null || original.Length != X86ContextSize) throw new ArgumentException("Expected a complete x86 CONTEXT buffer.", "original"); byte[] result = (byte[])original.Clone(); Buffer.BlockCopy(BitConverter.GetBytes(unchecked((int)ContextAllX86)), 0, result, 0, 4); Buffer.BlockCopy(BitConverter.GetBytes(unchecked((int)codeAddress)), 0, result, ContextEipOffset, 4); Buffer.BlockCopy(BitConverter.GetBytes(unchecked((int)stackPointer)), 0, result, ContextEspOffset, 4); int flags = BitConverter.ToInt32(result, ContextEflagsOffset) & ~0x100; Buffer.BlockCopy(BitConverter.GetBytes(flags), 0, result, ContextEflagsOffset, 4); return result; } private static uint ComputeSafeNativeStackPointer(IntPtr process, IntPtr thread, byte[] originalContext) { if (originalContext == null || originalContext.Length != X86ContextSize) throw new ArgumentException("Expected a complete x86 CONTEXT buffer.", "originalContext"); ThreadBasicInformationX86 information; int status = NtQueryInformationThread(thread, 0, out information, Marshal.SizeOf(typeof(ThreadBasicInformationX86)), IntPtr.Zero); if (status < 0 || information.TebBaseAddress == IntPtr.Zero) throw new InvalidOperationException("NtQueryInformationThread(ThreadBasicInformation) failed: 0x" + status.ToString("X8")); uint teb = unchecked((uint)information.TebBaseAddress.ToInt32()); uint stackBase = ReadU32Exact(process, unchecked(teb + 4)); uint stackLimit = ReadU32Exact(process, unchecked(teb + 8)); uint originalEsp = BitConverter.ToUInt32(originalContext, ContextEspOffset); if (!HasNativeStackBounds(stackLimit, stackBase, originalEsp, RemoteStubMaxCallStackBytes)) { Log("REMOTE_STACK_BOUNDS_INVALID teb=0x" + teb.ToString("X8") + " stackLimit=0x" + stackLimit.ToString("X8") + " originalEsp=0x" + originalEsp.ToString("X8") + " stackBase=0x" + stackBase.ToString("X8") + " callerBytes=" + RemoteStubMaxCallStackBytes); throw new InvalidOperationException("Native render-thread ESP does not leave the verified caller-push bytes within its committed stack bounds."); } uint lowestPushByte = originalEsp - RemoteStubMaxCallStackBytes; MemoryBasicInformation memory = new MemoryBasicInformation(); UIntPtr queried = VirtualQueryEx(process, Ptr(lowestPushByte), out memory, new UIntPtr((uint)Marshal.SizeOf(typeof(MemoryBasicInformation)))); uint regionBase = unchecked((uint)memory.BaseAddress.ToInt32()); uint regionSize = memory.RegionSize.ToUInt32(); string stackEvidence = "state=0x" + memory.State.ToString("X8") + " protect=0x" + memory.Protect.ToString("X8") + " region=0x" + regionBase.ToString("X8") + "+0x" + regionSize.ToString("X8"); bool stackRangeWritable = queried.ToUInt32() != 0 && IsSafeNativeStackRange(stackLimit, stackBase, originalEsp, RemoteStubMaxCallStackBytes, regionBase, regionSize, memory.State, memory.Protect); Log("REMOTE_STACK_BOUNDS teb=0x" + teb.ToString("X8") + " stackLimit=0x" + stackLimit.ToString("X8") + " originalEsp=0x" + originalEsp.ToString("X8") + " stackBase=0x" + stackBase.ToString("X8") + " lowestPushByte=0x" + lowestPushByte.ToString("X8") + " callerBytes=" + RemoteStubMaxCallStackBytes + " " + stackEvidence + " writable=" + stackRangeWritable); if (!stackRangeWritable) throw new InvalidOperationException("The full present-thread stub push range is not in one committed writable stack region or touches a guard page."); return originalEsp; } private static bool HasNativeStackBounds(uint stackLimit, uint stackBase, uint originalEsp, uint callerBytes) { return callerBytes != 0 && originalEsp > stackLimit && originalEsp < stackBase && originalEsp >= callerBytes && originalEsp - callerBytes >= stackLimit; } private static bool IsSafeNativeStackRange(uint stackLimit, uint stackBase, uint originalEsp, uint callerBytes, uint regionBase, uint regionSize, uint state, uint protect) { if (!HasNativeStackBounds(stackLimit, stackBase, originalEsp, callerBytes) || regionSize == 0) return false; uint lowestPushByte = originalEsp - callerBytes; ulong regionEnd = (ulong)regionBase + regionSize; uint protection = protect & 0xFF; bool writableProtection = protection == 0x04 || protection == 0x08 || protection == 0x40 || protection == 0x80; return state == MemCommit && (protect & PageGuard) == 0 && writableProtection && lowestPushByte >= regionBase && (ulong)originalEsp <= regionEnd; } private sealed class X86CodeBuilder { private readonly List _bytes = new List(); private readonly Dictionary _labels = new Dictionary(StringComparer.Ordinal); private readonly List> _relative32 = new List>(); private readonly List _callInstructionOffsets = new List(); private int _int3Count; private int _terminalOffset = -1; private int _currentPushBytes; private int _maxCallStackBytes; public int Position { get { return _bytes.Count; } } public void Emit(params byte[] bytes) { _bytes.AddRange(bytes); } public void EmitU32(uint value) { _bytes.AddRange(BitConverter.GetBytes(value)); } public void PushReg(byte opcode) { if (opcode < 0x50 || opcode > 0x57) throw new InvalidOperationException("Expected one x86 push-register opcode."); _bytes.Add(opcode); _currentPushBytes = checked(_currentPushBytes + 4); } public void PushImm8(byte value) { _bytes.Add(0x6A); _bytes.Add(value); _currentPushBytes = checked(_currentPushBytes + 4); } public void PushImm32(uint value) { _bytes.Add(0x68); EmitU32(value); _currentPushBytes = checked(_currentPushBytes + 4); } public void CallStdCall(int argumentBytes, params byte[] opcode) { if (argumentBytes < 0 || (argumentBytes & 3) != 0 || _currentPushBytes != argumentBytes || opcode == null || opcode.Length == 0) throw new InvalidOperationException("x86 stdcall must match the emitted dword arguments."); _maxCallStackBytes = Math.Max(_maxCallStackBytes, checked(argumentBytes + 4)); _callInstructionOffsets.Add(_bytes.Count); _bytes.AddRange(opcode); // IDirect3DDevice7/IDirectDrawSurface7 vtable methods use STDMETHODCALLTYPE // (stdcall), so the callee removes its arguments before returning. _currentPushBytes -= argumentBytes; } public void Mark(string label) { if (_labels.ContainsKey(label)) throw new InvalidOperationException("Duplicate x86 label: " + label); _labels.Add(label, _bytes.Count); } public void JumpIf(byte condition, string label) { _bytes.Add(0x0F); _bytes.Add((byte)(0x80 | condition)); int operand = _bytes.Count; EmitU32(0); _relative32.Add(new KeyValuePair(operand, label)); } public void Jump(string label) { _bytes.Add(0xE9); int operand = _bytes.Count; EmitU32(0); _relative32.Add(new KeyValuePair(operand, label)); } public void EmitTerminalInt3() { _terminalOffset = _bytes.Count; _bytes.Add(0xCC); _int3Count++; } public int Int3Count { get { return _int3Count; } } public int TerminalOffset { get { return _terminalOffset; } } public int BranchCount { get { return _relative32.Count; } } public int MaxCallStackBytes { get { return _maxCallStackBytes; } } public int[] CallInstructionOffsets { get { return _callInstructionOffsets.ToArray(); } } public void AssertTerminalControlFlow(byte[] finishedBytes, int expectedBranchCount) { if (finishedBytes == null || _int3Count != 1 || _terminalOffset != finishedBytes.Length - 1 || _terminalOffset < 0 || finishedBytes[_terminalOffset] != 0xCC || _relative32.Count != expectedBranchCount || _currentPushBytes != 0) throw new InvalidOperationException("Readback stub must have one terminal INT3 and the expected branch count."); // Validate only branches registered by Jump/JumpIf. A rel32 operand // can itself contain 0xCC bytes, which are not instructions. for (int i = 0; i < _relative32.Count; i++) { KeyValuePair branch = _relative32[i]; int labelOffset; if (!_labels.TryGetValue(branch.Value, out labelOffset) || branch.Key < 0 || branch.Key + 4 > finishedBytes.Length) throw new InvalidOperationException("Readback stub has an invalid branch record."); int resolvedOffset = checked(branch.Key + 4 + BitConverter.ToInt32(finishedBytes, branch.Key)); if (resolvedOffset != labelOffset || resolvedOffset < 0 || resolvedOffset > _terminalOffset) throw new InvalidOperationException("Readback stub branch does not resolve to a valid instruction label."); } } public byte[] Finish() { for (int i = 0; i < _relative32.Count; i++) { KeyValuePair fixup = _relative32[i]; int destination; if (!_labels.TryGetValue(fixup.Value, out destination)) throw new InvalidOperationException("Unresolved x86 label: " + fixup.Value); int relative = destination - (fixup.Key + 4); byte[] bytes = BitConverter.GetBytes(relative); for (int j = 0; j < 4; j++) _bytes[fixup.Key + j] = bytes[j]; } return _bytes.ToArray(); } } private static RemoteCode BuildReadbackStub(uint allocationBase, uint device, bool cleanup) { uint data = unchecked(allocationBase + RemoteDataOffset); uint statusAddress = unchecked(data + RemoteDataStatusOffset); uint getHrAddress = unchecked(data + RemoteDataGetHrOffset); uint lockHrAddress = unchecked(data + RemoteDataLockHrOffset); uint unlockHrAddress = unchecked(data + RemoteDataUnlockHrOffset); uint releaseAddress = unchecked(data + RemoteDataReleaseCountOffset); uint surfaceAddress = unchecked(data + RemoteDataSurfaceOffset); uint descAddress = unchecked(data + RemoteDataDescOffset); X86CodeBuilder code = new X86CodeBuilder(); if (!cleanup) { code.Emit(0xBE); code.EmitU32(device); // mov esi, device code.Emit(0x8B, 0x06); // mov eax, [esi] code.Emit(0x8D, 0x3D); code.EmitU32(surfaceAddress); // lea edi, [surface] code.PushReg(0x57); code.PushReg(0x56); // push edi; push esi code.CallStdCall(8, 0xFF, 0x50, (byte)D3dDeviceGetRenderTargetOffset); // call [eax+GetRenderTarget] code.Emit(0xA3); code.EmitU32(getHrAddress); // mov [getHr], eax code.Emit(0x85, 0xC0); // test eax,eax code.JumpIf(0x89, "get_success"); // jns get_success code.Emit(0x8B, 0x35); code.EmitU32(surfaceAddress); // mov esi, [surface] code.Emit(0x85, 0xF6); // test esi,esi code.JumpIf(0x84, "get_failed_no_surface"); code.Emit(0xC7, 0x05); code.EmitU32(statusAddress); code.EmitU32(2); code.Jump("release_and_stop"); code.Mark("get_success"); code.Emit(0x8B, 0x35); code.EmitU32(surfaceAddress); // mov esi, [surface] code.Emit(0x85, 0xF6); // test esi,esi code.JumpIf(0x84, "get_success_no_surface"); code.Emit(0x8B, 0x06); // mov eax, [esi] code.PushImm8(0x00); // push NULL event code.PushImm32(DdLockReadOnly | DdLockDoNotWait); code.Emit(0x8D, 0x3D); code.EmitU32(descAddress); // lea edi,[desc] code.PushReg(0x57); code.PushImm8(0x00); code.PushReg(0x56); // desc; NULL rect; this code.CallStdCall(20, 0xFF, 0x50, (byte)DdSurfaceLockOffset); // call [eax+Lock] code.Emit(0xA3); code.EmitU32(lockHrAddress); // mov [lockHr], eax code.Emit(0x85, 0xC0); // test eax,eax code.JumpIf(0x88, "lock_failed"); // js lock_failed code.Emit(0xC7, 0x05); code.EmitU32(statusAddress); code.EmitU32(1); code.Jump("stop"); // lock held; host copies pixels, then cleans up code.Mark("lock_failed"); code.Emit(0xC7, 0x05); code.EmitU32(statusAddress); code.EmitU32(4); code.Jump("release_and_stop"); code.Mark("get_success_no_surface"); code.Emit(0xC7, 0x05); code.EmitU32(statusAddress); code.EmitU32(3); code.Jump("stop"); code.Mark("get_failed_no_surface"); code.Emit(0xC7, 0x05); code.EmitU32(statusAddress); code.EmitU32(5); code.Jump("stop"); code.Mark("release_and_stop"); code.Emit(0x8B, 0x35); code.EmitU32(surfaceAddress); code.Emit(0x8B, 0x06); // mov eax, [esi] code.PushReg(0x56); // push this code.CallStdCall(4, 0xFF, 0x50, (byte)DdSurfaceReleaseOffset); // call [eax+Release] code.Emit(0xA3); code.EmitU32(releaseAddress); code.Jump("stop"); } else { code.Emit(0x8B, 0x35); code.EmitU32(surfaceAddress); // mov esi, [surface] code.Emit(0x85, 0xF6); code.JumpIf(0x84, "cleanup_no_surface"); code.Emit(0x8B, 0x06); // mov eax, [esi] code.PushImm8(0x00); // Unlock(NULL) code.PushReg(0x56); // push this code.CallStdCall(8, 0xFF, 0x90, 0x80, 0x00, 0x00, 0x00); // call dword ptr [eax+0x80] (disp32) code.Emit(0xA3); code.EmitU32(unlockHrAddress); code.Emit(0x8B, 0x06); // call Release even if Unlock failed code.PushReg(0x56); code.CallStdCall(4, 0xFF, 0x50, (byte)DdSurfaceReleaseOffset); code.Emit(0xA3); code.EmitU32(releaseAddress); code.Emit(0xC7, 0x05); code.EmitU32(statusAddress); code.EmitU32(6); code.Jump("stop"); code.Mark("cleanup_no_surface"); code.Emit(0xC7, 0x05); code.EmitU32(statusAddress); code.EmitU32(7); code.Jump("stop"); } code.Mark("stop"); code.EmitTerminalInt3(); byte[] bytes = code.Finish(); code.AssertTerminalControlFlow(bytes, cleanup ? 3 : 10); int expectedStack = cleanup ? 12 : 24; if (code.MaxCallStackBytes != expectedStack) throw new InvalidOperationException("Unexpected x86 caller stack footprint in the readback stub."); if (bytes.Length > RemoteCodePage) throw new InvalidOperationException("Remote DirectDraw stub exceeds one code page."); RemoteCode result = new RemoteCode(); result.Base = allocationBase; result.DataBase = data; result.Bytes = bytes; result.CallInstructionOffsets = code.CallInstructionOffsets; result.MaxCallStackBytes = code.MaxCallStackBytes; result.TrapAddress = unchecked(allocationBase + (uint)code.TerminalOffset); return result; } private static RemoteCode BuildCameraSetterStub(uint allocationBase, uint transformInterface) { uint dataBase = unchecked(allocationBase + RemoteDataOffset); uint matrixAddress = unchecked(dataBase + CameraSetterMatrixOffset); X86CodeBuilder code = new X86CodeBuilder(); code.Emit(0xBE); code.EmitU32(transformInterface); // mov esi, transform interface code.Emit(0x8B, 0x0E); // mov ecx, [esi] (verified vtable) code.PushImm32(matrixAddress); // argument 3: 64-byte transform code.PushImm8(1); // argument 2: mode 1 code.PushReg(0x56); // argument 1: this code.CallStdCall(12, 0xFF, 0x51, (byte)CameraTransformSetterSlotOffset); code.EmitTerminalInt3(); byte[] bytes = code.Finish(); code.AssertTerminalControlFlow(bytes, 0); if (code.MaxCallStackBytes != 16 || bytes.Length > RemoteCodePage) throw new InvalidOperationException("Camera setter stub has an unexpected stack footprint or size."); RemoteCode result = new RemoteCode(); result.Base = allocationBase; result.DataBase = dataBase; result.Bytes = bytes; result.CallInstructionOffsets = code.CallInstructionOffsets; result.MaxCallStackBytes = code.MaxCallStackBytes; result.TrapAddress = unchecked(allocationBase + (uint)code.TerminalOffset); return result; } private static bool VerifyCameraSetterAbi(IntPtr process, uint terrainBase, uint camera, out uint transformInterface, out string evidence) { transformInterface = unchecked(camera + 4); uint primaryVtable = ReadU32(process, camera); uint selector = ReadU32(process, unchecked(camera + 0x10)); byte[] modeBytes = new byte[1]; bool modeRead = ReadExact(process, unchecked(camera + 0x1A0), modeBytes); uint transformVtable = ReadU32(process, transformInterface); uint setter = ReadU32(process, unchecked(transformVtable + CameraTransformSetterSlotOffset)); uint invalidate = ReadU32(process, unchecked(transformVtable + CameraTransformInvalidatorSlotOffset)); bool valid = terrainBase != 0 && primaryVtable == unchecked(terrainBase + ExternalCameraVtableRva) && transformVtable == unchecked(terrainBase + CameraTransformInterfaceVtableRva) && setter == unchecked(terrainBase + CameraTransformSetterRva) && invalidate == unchecked(terrainBase + CameraTransformInvalidatorRva) && selector == 0xFFFFFFFF && modeRead && modeBytes[0] == 0; evidence = "camera=0x" + camera.ToString("X8") + " primaryVtable=0x" + primaryVtable.ToString("X8") + " transform=0x" + transformInterface.ToString("X8") + " transformVtable=0x" + transformVtable.ToString("X8") + " setter=0x" + setter.ToString("X8") + " invalidator=0x" + invalidate.ToString("X8") + " selector=0x" + selector.ToString("X8") + " mode=" + (modeRead ? modeBytes[0].ToString(CultureInfo.InvariantCulture) : "unreadable") + " valid=" + valid; return valid; } private static RemoteCameraSetterSession StartCameraSetter(IntPtr process, uint threadId, uint terrainBase, uint camera, byte[] matrixBytes, byte[] originalContext, BreakpointInfo resumeBreakpoint, bool rearmOnStep, CameraSetterPhase phase, uint functionEntryEsp, uint functionReturnAddress) { if (matrixBytes == null || matrixBytes.Length != 64 || originalContext == null || originalContext.Length != X86ContextSize || resumeBreakpoint == null || !resumeBreakpoint.Armed) throw new InvalidOperationException("Camera setter request is incomplete."); string abiEvidence; uint transformInterface; if (!VerifyCameraSetterAbi(process, terrainBase, camera, out transformInterface, out abiEvidence)) throw new InvalidOperationException("Camera setter ABI refused: " + abiEvidence); Log("verified native camera setter ABI " + abiEvidence + " slot=+0x1C this=0x" + transformInterface.ToString("X8") + " mode=1 matrixBytes=64 ret=0x0C"); uint expectedEip = unchecked(resumeBreakpoint.Address + 1); uint originalEip = BitConverter.ToUInt32(originalContext, ContextEipOffset); uint originalEsp = BitConverter.ToUInt32(originalContext, ContextEspOffset); if (originalEip != expectedEip || originalEsp > UInt32.MaxValue - 8 || functionEntryEsp > UInt32.MaxValue - 8) throw new InvalidOperationException("Camera setter breakpoint EIP/ESP did not match its verified boundary."); byte[] callerStack = new byte[8]; if (!ReadExact(process, originalEsp, callerStack)) throw new InvalidOperationException("Could not snapshot the actual native stack words at the setter boundary."); byte[] functionCallerStack = new byte[8]; if (!ReadExact(process, functionEntryEsp, functionCallerStack) || !CameraFunctionStackMatches(phase, functionCallerStack, functionReturnAddress, camera)) throw new InvalidOperationException("The native stdRenderGame return/argument stack did not match the selected setter phase."); uint esiAtBoundary = BitConverter.ToUInt32(originalContext, ContextEsiOffset); if (phase == CameraSetterPhase.Apply) { if (originalEsp != functionEntryEsp || BitConverter.ToUInt32(callerStack, 4) != camera) throw new InvalidOperationException("The camera-entry setter stack did not contain the original camera argument."); } else { if (functionEntryEsp < RenderFunctionStackFrameBytes || originalEsp != functionEntryEsp - RenderFunctionStackFrameBytes) throw new InvalidOperationException("The restore boundary did not preserve the selected function ESP."); } IntPtr thread = OpenThread(ThreadGetContext | ThreadSetContext | ThreadQueryInformation, false, threadId); if (thread == IntPtr.Zero) throw new InvalidOperationException("OpenThread for camera setter failed: " + Marshal.GetLastWin32Error()); uint region = 0; try { uint safeEsp = ComputeSafeNativeStackPointer(process, thread, originalContext); region = AllocateReadbackRegion(process); RemoteCode code = BuildCameraSetterStub(region, transformInterface); WriteRemoteStub(process, code, false); UIntPtr written; if (!WriteProcessMemory(process, Ptr(unchecked(code.DataBase + CameraSetterMatrixOffset)), matrixBytes, new UIntPtr(64), out written) || written.ToUInt32() != 64) throw new InvalidOperationException("Could not copy the validated 64-byte camera matrix to the bounded setter stub."); RemoteCameraSetterSession session = new RemoteCameraSetterSession(); session.Code = code; session.Region = region; session.Camera = camera; session.ThreadId = threadId; session.OriginalEsp = originalEsp; session.OriginalContext = (byte[])originalContext.Clone(); session.CallerStack = callerStack; session.FunctionEntryEsp = functionEntryEsp; session.FunctionReturnAddress = functionReturnAddress; session.EsiAtBoundary = esiAtBoundary; session.FunctionCallerStack = functionCallerStack; session.MatrixBytes = (byte[])matrixBytes.Clone(); session.Phase = phase; session.ResumeBreakpoint = resumeBreakpoint; session.RearmOnStep = rearmOnStep; session.DeadlineUtc = DateTime.UtcNow.AddSeconds(5); SetFullX86Context(thread, ContextForRemoteCode(originalContext, code.Base, safeEsp)); Log("CAMERA_SETTER_STARTED phase=" + phase + " tid=" + threadId + " camera=0x" + camera.ToString("X8") + " stub=0x" + code.Base.ToString("X8") + " terminal=0x" + code.TrapAddress.ToString("X8") + " originalEsp=0x" + originalEsp.ToString("X8") + " currentStack=" + BitConverter.ToString(callerStack) + " functionCallerStack=" + BitConverter.ToString(functionCallerStack) + " observedFunctionArgument=0x" + BitConverter.ToUInt32(functionCallerStack, 4).ToString("X8") + " esiAtBoundary=0x" + esiAtBoundary.ToString("X8") + " matrixSha256=" + HashBytes(matrixBytes)); return session; } catch { if (region != 0) VirtualFreeEx(process, Ptr(region), UIntPtr.Zero, 0x8000); throw; } finally { CloseHandle(thread); } } private static bool VerifyCameraSetterReturnContext(IntPtr process, RemoteCameraSetterSession session, out string evidence) { evidence = "unverified"; IntPtr thread = OpenThread(ThreadGetContext, false, session.ThreadId); if (thread == IntPtr.Zero) { evidence = "OpenThread failed=" + Marshal.GetLastWin32Error(); return false; } try { byte[] context = GetFullX86Context(thread); uint eip = BitConverter.ToUInt32(context, ContextEipOffset); uint esp = BitConverter.ToUInt32(context, ContextEspOffset); byte[] callerStack = new byte[session.CallerStack.Length]; byte[] functionCallerStack = new byte[session.FunctionCallerStack.Length]; byte[] matrix = new byte[session.MatrixBytes.Length]; bool stackReadable = ReadExact(process, session.OriginalEsp, callerStack); bool functionStackReadable = ReadExact(process, session.FunctionEntryEsp, functionCallerStack); bool matrixReadable = ReadExact(process, unchecked(session.Code.DataBase + CameraSetterMatrixOffset), matrix); bool stackMatches = stackReadable && ByteArraysEqual(callerStack, session.CallerStack); bool functionStackMatches = functionStackReadable && ByteArraysEqual(functionCallerStack, session.FunctionCallerStack) && CameraFunctionStackMatches(session.Phase, functionCallerStack, session.FunctionReturnAddress, session.Camera); bool matrixMatches = matrixReadable && ByteArraysEqual(matrix, session.MatrixBytes); bool valid = eip == unchecked(session.Code.TrapAddress + 1) && esp == session.OriginalEsp && stackMatches && functionStackMatches && matrixMatches; evidence = "expectedEip=0x" + unchecked(session.Code.TrapAddress + 1).ToString("X8") + " actualEip=0x" + eip.ToString("X8") + " expectedEsp=0x" + session.OriginalEsp.ToString("X8") + " actualEsp=0x" + esp.ToString("X8") + " callerStackIntact=" + stackMatches + " functionCallerStackIntact=" + functionStackMatches + " observedFunctionArgument=0x" + BitConverter.ToUInt32(session.FunctionCallerStack, 4).ToString("X8") + " esiAtBoundary=0x" + session.EsiAtBoundary.ToString("X8") + " matrixDataIntact=" + matrixMatches; return valid; } finally { CloseHandle(thread); } } private static byte[] ReadCameraMatrix(IntPtr process, uint camera) { byte[] matrix = new byte[64]; if (!ReadExact(process, unchecked(camera + 0x20), matrix)) throw new InvalidOperationException("Could not read back the native camera selector-0 matrix."); return matrix; } private static bool CameraFunctionStackMatches(CameraSetterPhase phase, byte[] stackWords, uint expectedReturnAddress, uint camera) { if (stackWords == null || stackWords.Length != 8 || BitConverter.ToUInt32(stackWords, 0) != expectedReturnAddress) return false; return phase == CameraSetterPhase.Restore || BitConverter.ToUInt32(stackWords, 4) == camera; } private static void SelfCheckRenderInvocationGates() { const uint threadId = 11; const uint eip = 0x10013CE5; const uint esp = 0x001AFC28; const uint camera = 0x1644A8D8; const uint returnAddress = 0x1005F243; uint observedArgument; bool overwrittenArgumentAccepted = RenderInvocationMatches(threadId, threadId, eip, eip, esp, esp, camera, camera, returnAddress, returnAddress, true, 0x0BADF00D, out observedArgument) && observedArgument == 0x0BADF00D; bool wrongThreadRejected = !RenderInvocationMatches(threadId + 1, threadId, eip, eip, esp, esp, camera, camera, returnAddress, returnAddress, true, 0x0BADF00D, out observedArgument); bool wrongEsiRejected = !RenderInvocationMatches(threadId, threadId, eip, eip, esp, esp, camera + 4, camera, returnAddress, returnAddress, true, 0x0BADF00D, out observedArgument); bool wrongReturnRejected = !RenderInvocationMatches(threadId, threadId, eip, eip, esp, esp, camera, camera, returnAddress + 4, returnAddress, false, 0x0BADF00D, out observedArgument); bool epilogueAllowsReassignedEsi = RenderInvocationMatches(threadId, threadId, eip, eip, esp, esp, camera + 0x100, camera, returnAddress, returnAddress, false, 0x0BADF00D, out observedArgument); byte[] entryStack = new byte[8]; Buffer.BlockCopy(BitConverter.GetBytes(returnAddress), 0, entryStack, 0, 4); Buffer.BlockCopy(BitConverter.GetBytes(camera), 0, entryStack, 4, 4); byte[] overwrittenStack = (byte[])entryStack.Clone(); Buffer.BlockCopy(BitConverter.GetBytes(0x0BADF00Du), 0, overwrittenStack, 4, 4); bool applyRequiresOriginalCameraArgument = CameraFunctionStackMatches( CameraSetterPhase.Apply, entryStack, returnAddress, camera) && !CameraFunctionStackMatches(CameraSetterPhase.Apply, overwrittenStack, returnAddress, camera); bool restorePreservesOverwrittenArgument = CameraFunctionStackMatches( CameraSetterPhase.Restore, overwrittenStack, returnAddress, camera) && !CameraFunctionStackMatches(CameraSetterPhase.Restore, overwrittenStack, returnAddress + 4, camera); if (!overwrittenArgumentAccepted || !wrongThreadRejected || !wrongEsiRejected || !epilogueAllowsReassignedEsi || !wrongReturnRejected || !applyRequiresOriginalCameraArgument || !restorePreservesOverwrittenArgument) throw new InvalidOperationException("Self-check failed: selected invocation identity or phase-specific camera stack gate."); } private static bool IsSelectedRenderInvocation(IntPtr process, byte[] context, uint threadId, SelectedCameraState selected, uint expectedBreakpointEip, bool requireEsiCamera, out string evidence) { if (selected == null) { evidence = "selected invocation state is missing"; return false; } if (threadId != selected.ThreadId) { evidence = "actualTid=" + threadId + " expectedTid=" + selected.ThreadId + " camera=0x" + selected.CameraPointer.ToString("X8"); return false; } return IsRenderInvocation(process, context, threadId, selected.ThreadId, selected.CameraPointer, selected.EntryEsp, selected.FunctionStackEsp, selected.ReturnAddress, expectedBreakpointEip, requireEsiCamera, out evidence); } private static bool IsRenderInvocation(IntPtr process, byte[] context, uint threadId, uint expectedThreadId, uint camera, uint entryEsp, uint functionStackEsp, uint returnAddress, uint expectedBreakpointEip, bool requireEsiCamera, out string evidence) { uint actualEip = 0; uint actualEsp = 0; uint actualEsi = 0; if (context != null && context.Length == X86ContextSize) { actualEip = BitConverter.ToUInt32(context, ContextEipOffset); actualEsp = BitConverter.ToUInt32(context, ContextEspOffset); actualEsi = BitConverter.ToUInt32(context, ContextEsiOffset); } byte[] callerStack = new byte[8]; bool stackReadable = entryEsp <= UInt32.MaxValue - 8 && ReadExact(process, entryEsp, callerStack); uint savedReturn = stackReadable ? BitConverter.ToUInt32(callerStack, 0) : 0; uint observedArgument = stackReadable ? BitConverter.ToUInt32(callerStack, 4) : 0; uint diagnosticArgument; bool valid = RenderInvocationMatches(threadId, expectedThreadId, actualEip, expectedBreakpointEip, actualEsp, functionStackEsp, actualEsi, camera, savedReturn, returnAddress, requireEsiCamera, observedArgument, out diagnosticArgument) && stackReadable; evidence = "actualTid=" + threadId + " expectedTid=" + expectedThreadId + " actualEip=0x" + actualEip.ToString("X8") + " expectedEip=0x" + expectedBreakpointEip.ToString("X8") + " actualEsp=0x" + actualEsp.ToString("X8") + " expectedFunctionEsp=0x" + functionStackEsp.ToString("X8") + " entryEsp=0x" + entryEsp.ToString("X8") + " savedReturn=0x" + savedReturn.ToString("X8") + " expectedReturn=0x" + returnAddress.ToString("X8") + " observedEntryArgument=0x" + diagnosticArgument.ToString("X8") + " entryArgumentMayBeOverwritten=true esi=0x" + actualEsi.ToString("X8") + " esiRequired=" + requireEsiCamera + " expectedCamera=0x" + camera.ToString("X8") + " stackReadable=" + stackReadable + " match=" + valid; return valid; } private static bool RenderInvocationMatches(uint threadId, uint expectedThreadId, uint eip, uint expectedEip, uint esp, uint expectedEsp, uint esi, uint camera, uint savedReturn, uint expectedReturn, bool requireEsiCamera, uint observedArgument, out uint diagnosticArgument) { // The camera argument's original stack slot becomes a COM output pointer // during World3D's selector-6 query. Keep it in diagnostics, not identity. diagnosticArgument = observedArgument; return threadId == expectedThreadId && eip == expectedEip && esp == expectedEsp && (!requireEsiCamera || esi == camera) && savedReturn == expectedReturn; } private static RemoteReadbackSession StartRemoteReadback(IntPtr process, uint threadId, uint ngiBase, BreakpointInfo present, FrameSnapshot frame, string outputJson) { uint device; uint getRenderTarget; string evidence; if (!VerifyD3d7GetRenderTarget(process, ngiBase, out device, out getRenderTarget, out evidence)) throw new InvalidOperationException("D3D7 GetRenderTarget call was refused: " + evidence); Log("verified D3D7 GetRenderTarget ABI " + evidence); if (frame == null || frame.Projection == null || ReadU32(process, unchecked(ngiBase + NgiRendererGlobalRva)) != frame.Projection.Renderer) throw new InvalidOperationException("Ngi32 renderer changed since the matching projection snapshot."); if (ReadU32(process, unchecked(frame.Projection.Renderer + 0x114)) == 0) throw new InvalidOperationException("Ngi32 is not in the verified windowed present path for RVA 0x6E1B."); IntPtr thread = OpenThread(ThreadGetContext | ThreadSetContext | ThreadQueryInformation, false, threadId); if (thread == IntPtr.Zero) throw new InvalidOperationException("OpenThread for D3D7 readback failed: " + Marshal.GetLastWin32Error()); uint region = 0; try { byte[] originalContext = GetFullX86Context(thread); uint stoppedEip = BitConverter.ToUInt32(originalContext, ContextEipOffset); if (stoppedEip != unchecked(present.Address + 1)) throw new InvalidOperationException("Ngi32 present EIP did not match the armed callsite."); uint safeEsp = ComputeSafeNativeStackPointer(process, thread, originalContext); uint originalEsp = BitConverter.ToUInt32(originalContext, ContextEspOffset); if (originalEsp > UInt32.MaxValue - PresentCallArgumentsBytes) throw new InvalidOperationException("Ngi32 present argument block address overflowed."); byte[] presentStackArguments = new byte[PresentCallArgumentsBytes]; if (!ReadExact(process, originalEsp, presentStackArguments)) throw new InvalidOperationException("Could not read the original Ngi32 present call's 24-byte argument block."); region = AllocateReadbackRegion(process); RemoteCode acquire = BuildReadbackStub(region, device, false); WriteRemoteStub(process, acquire, true); RemoteReadbackSession session = new RemoteReadbackSession(); session.Acquire = acquire; session.Region = region; session.ThreadId = threadId; session.SafeEsp = safeEsp; session.OriginalContext = originalContext; session.PresentStackArguments = presentStackArguments; session.PresentStackArgumentsIntact = true; session.RemoteContextIntact = true; session.PresentBreakpoint = present; session.Frame = frame; session.Generation = frame.CameraGeneration; session.OutputJson = outputJson; session.OutputPng = Path.ChangeExtension(outputJson, ".png"); session.DeadlineUtc = DateTime.UtcNow.AddSeconds(5); session.Phase = RemoteReadbackPhase.Acquire; Log("PRESENT_STACK_ARGS stage=before-com esp=0x" + originalEsp.ToString("X8") + " bytes=" + BitConverter.ToString(presentStackArguments)); Log("REMOTE_ACQUIRE_STARTED tid=" + threadId + " stub=0x" + acquire.Base.ToString("X8") + " terminal=0x" + acquire.TrapAddress.ToString("X8") + " safeEsp=0x" + safeEsp.ToString("X8") + " generation=" + session.Generation); SetFullX86Context(thread, ContextForRemoteCode(originalContext, acquire.Base, safeEsp)); return session; } catch { if (region != 0) VirtualFreeEx(process, Ptr(region), UIntPtr.Zero, 0x8000); throw; } finally { CloseHandle(thread); } } private static void BeginRemoteCleanup(IntPtr process, RemoteReadbackSession session) { session.Cleanup = BuildReadbackStub(session.Region, 0, true); WriteRemoteStub(process, session.Cleanup, false); IntPtr thread = OpenThread(ThreadGetContext | ThreadSetContext, false, session.ThreadId); if (thread == IntPtr.Zero) throw new InvalidOperationException("OpenThread for D3D7 Unlock/Release failed: " + Marshal.GetLastWin32Error()); try { SetFullX86Context(thread, ContextForRemoteCode(session.OriginalContext, session.Cleanup.Base, session.SafeEsp)); } finally { CloseHandle(thread); } session.Phase = RemoteReadbackPhase.Cleanup; session.DeadlineUtc = DateTime.UtcNow.AddSeconds(5); Log("REMOTE_CLEANUP_STARTED tid=" + session.ThreadId + " stub=0x" + session.Cleanup.Base.ToString("X8") + " terminal=0x" + session.Cleanup.TrapAddress.ToString("X8")); } private static uint AllocateReadbackRegion(IntPtr process) { const uint MemReserve = 0x2000; IntPtr memory = VirtualAllocEx(process, IntPtr.Zero, new UIntPtr(RemoteAllocationBytes), MemReserve | MemCommit, 0x04); if (memory == IntPtr.Zero) throw new InvalidOperationException("VirtualAllocEx for bounded readback stub failed: " + Marshal.GetLastWin32Error()); uint address = unchecked((uint)memory.ToInt32()); if (address == 0 || address + RemoteAllocationBytes < address) { VirtualFreeEx(process, memory, UIntPtr.Zero, 0x8000); throw new InvalidOperationException("VirtualAllocEx returned an invalid x86 address."); } // Keep the executable stub on its own read/execute page. The result // structure stays writable; COM uses the stopped render thread's // native stack after a TEB stack-bound check. uint oldProtection; if (!VirtualProtectEx(process, memory, new UIntPtr(RemoteCodePage), 0x20, out oldProtection)) { VirtualFreeEx(process, memory, UIntPtr.Zero, 0x8000); throw new InvalidOperationException("VirtualProtectEx for readback code failed: " + Marshal.GetLastWin32Error()); } return address; } private static void WriteRemoteStub(IntPtr process, RemoteCode code, bool initializeData) { uint oldProtection; if (!VirtualProtectEx(process, Ptr(code.Base), new UIntPtr(RemoteCodePage), PageExecuteReadWrite, out oldProtection)) throw new InvalidOperationException("Could not make temporary readback stub writable: " + Marshal.GetLastWin32Error()); try { UIntPtr written; if (!WriteProcessMemory(process, Ptr(code.Base), code.Bytes, new UIntPtr((uint)code.Bytes.Length), out written) || written.ToUInt32() != (uint)code.Bytes.Length) throw new InvalidOperationException("Writing temporary readback stub failed: " + Marshal.GetLastWin32Error()); if (initializeData) { byte[] descriptor = new byte[DdSurfaceDesc2Size]; Buffer.BlockCopy(BitConverter.GetBytes((uint)DdSurfaceDesc2Size), 0, descriptor, 0, 4); UIntPtr descWritten; if (!WriteProcessMemory(process, Ptr(unchecked(code.DataBase + RemoteDataDescOffset)), descriptor, new UIntPtr((uint)descriptor.Length), out descWritten) || descWritten.ToUInt32() != (uint)descriptor.Length) throw new InvalidOperationException("Initializing DDSURFACEDESC2 failed: " + Marshal.GetLastWin32Error()); byte[] zeros = new byte[RemoteDataDescOffset]; UIntPtr zerosWritten; if (!WriteProcessMemory(process, Ptr(code.DataBase), zeros, new UIntPtr((uint)zeros.Length), out zerosWritten) || zerosWritten.ToUInt32() != (uint)zeros.Length) throw new InvalidOperationException("Initializing readback result block failed: " + Marshal.GetLastWin32Error()); } } finally { uint ignored; if (!VirtualProtectEx(process, Ptr(code.Base), new UIntPtr(RemoteCodePage), oldProtection, out ignored)) throw new InvalidOperationException("Restoring temporary readback code protection failed: " + Marshal.GetLastWin32Error()); if (!FlushInstructionCache(process, Ptr(code.Base), new UIntPtr((uint)code.Bytes.Length))) throw new InvalidOperationException("FlushInstructionCache for readback stub failed: " + Marshal.GetLastWin32Error()); } } private static bool ReadExact(IntPtr process, uint address, byte[] bytes) { UIntPtr read; return address != 0 && ReadProcessMemory(process, Ptr(address), bytes, new UIntPtr((uint)bytes.Length), out read) && read.ToUInt32() == (uint)bytes.Length; } private static bool VerifyPresentStackArguments(IntPtr process, RemoteReadbackSession session, string stage) { uint esp = BitConverter.ToUInt32(session.OriginalContext, ContextEspOffset); byte[] current = new byte[PresentCallArgumentsBytes]; bool readable = esp <= UInt32.MaxValue - PresentCallArgumentsBytes && ReadExact(process, esp, current); bool unchanged = readable && ByteArraysEqual(session.PresentStackArguments, current); Log("PRESENT_STACK_ARGS stage=" + stage + " esp=0x" + esp.ToString("X8") + " readable=" + readable + " unchanged=" + unchanged + " before=" + (session.PresentStackArguments == null ? "null" : BitConverter.ToString(session.PresentStackArguments)) + " after=" + (readable ? BitConverter.ToString(current) : "unreadable")); return unchanged; } private static bool VerifyRemoteStubContext(IntPtr process, RemoteReadbackSession session, uint expectedEip, string stage) { IntPtr thread = OpenThread(ThreadGetContext, false, session.ThreadId); if (thread == IntPtr.Zero) { Log("REMOTE_STUB_CONTEXT stage=" + stage + " OpenThreadFailed=" + Marshal.GetLastWin32Error()); return false; } try { byte[] context = GetFullX86Context(thread); uint eip = BitConverter.ToUInt32(context, ContextEipOffset); uint esp = BitConverter.ToUInt32(context, ContextEspOffset); uint expectedEsp = BitConverter.ToUInt32(session.OriginalContext, ContextEspOffset); bool matches = eip == expectedEip && esp == expectedEsp; Log("REMOTE_STUB_CONTEXT stage=" + stage + " expectedEip=0x" + expectedEip.ToString("X8") + " actualEip=0x" + eip.ToString("X8") + " expectedEsp=0x" + expectedEsp.ToString("X8") + " actualEsp=0x" + esp.ToString("X8") + " matches=" + matches); return matches; } catch (Exception error) { Log("REMOTE_STUB_CONTEXT stage=" + stage + " readFailed=" + error.Message); return false; } finally { CloseHandle(thread); } } private static bool ByteArraysEqual(byte[] left, byte[] right) { if (left == null || right == null || left.Length != right.Length) return false; for (int i = 0; i < left.Length; i++) if (left[i] != right[i]) return false; return true; } private static uint ReadU32Exact(IntPtr process, uint address) { byte[] bytes = new byte[4]; if (!ReadExact(process, address, bytes)) throw new InvalidOperationException("Unreadable remote readback result at 0x" + address.ToString("X8")); return BitConverter.ToUInt32(bytes, 0); } private static ProjectionReadback ReadProjection(IntPtr process, uint ngiBase) { ProjectionReadback result = new ProjectionReadback(); uint rendererPointer = ReadU32(process, unchecked(ngiBase + NgiRendererGlobalRva)); result.Renderer = rendererPointer; if (rendererPointer == 0) { result.Failure = "Ngi32 renderer global is null"; return result; } uint vtable = ReadU32(process, rendererPointer); result.RendererVtable = vtable; if (vtable != unchecked(ngiBase + NgiRendererVtableRva)) { result.Failure = "Ngi32 renderer vtable mismatch: 0x" + vtable.ToString("X8"); return result; } byte[] viewportBytes = new byte[16]; byte[] nearBytes = new byte[4]; byte[] farBytes = new byte[4]; byte[] fovBytes = new byte[4]; byte[] modeBytes = new byte[1]; if (!ReadExact(process, unchecked(rendererPointer + 0x18), viewportBytes) || !ReadExact(process, unchecked(rendererPointer + 0x38), nearBytes) || !ReadExact(process, unchecked(rendererPointer + 0x3C), farBytes) || !ReadExact(process, unchecked(rendererPointer + 0x54), fovBytes) || !ReadExact(process, unchecked(rendererPointer + 0x118), modeBytes)) { result.Failure = "Ngi32 projection fields are unreadable"; return result; } int[] viewport = new int[4]; for (int i = 0; i < viewport.Length; i++) viewport[i] = BitConverter.ToInt32(viewportBytes, i * 4); float nearPlane = BitConverter.ToSingle(nearBytes, 0); float farPlane = BitConverter.ToSingle(farBytes, 0); float fov = BitConverter.ToSingle(fovBytes, 0); result.Viewport = viewport; result.Near = nearPlane; result.Far = farPlane; result.Fov = fov; result.Mode = modeBytes[0]; int width = viewport[2] - viewport[0]; int height = viewport[3] - viewport[1]; result.Verified = width > 0 && height > 0 && width <= 8192 && height <= 8192 && IsFinite(nearPlane) && nearPlane > 0.0f && IsFinite(farPlane) && farPlane > nearPlane && IsFinite(fov) && fov > 0.0f && fov < 3.141593f; if (!result.Verified) result.Failure = "Ngi32 viewport or projection values failed bounded finite/range checks"; return result; } private static SurfaceReadback ReadSurfaceRows(IntPtr process, uint descAddress) { byte[] desc = new byte[DdSurfaceDesc2Size]; if (!ReadExact(process, descAddress, desc)) throw new InvalidOperationException("Could not read D3D7 surface descriptor."); Log("D3D7_LOCK_DDSURFACEDESC2 bytes=" + desc.Length + " raw=" + BitConverter.ToString(desc)); uint size = BitConverter.ToUInt32(desc, 0); uint flags = BitConverter.ToUInt32(desc, 4); uint height = BitConverter.ToUInt32(desc, 8); uint width = BitConverter.ToUInt32(desc, 12); int pitch = BitConverter.ToInt32(desc, 16); uint surface = BitConverter.ToUInt32(desc, 36); uint pfSize = BitConverter.ToUInt32(desc, 72); uint pfFlags = BitConverter.ToUInt32(desc, 76); uint fourCc = BitConverter.ToUInt32(desc, 80); uint bitCount = BitConverter.ToUInt32(desc, 84); uint red = BitConverter.ToUInt32(desc, 88); uint green = BitConverter.ToUInt32(desc, 92); uint blue = BitConverter.ToUInt32(desc, 96); uint alpha = BitConverter.ToUInt32(desc, 100); if (size != DdSurfaceDesc2Size || pfSize != 32) throw new InvalidOperationException("DDSURFACEDESC2 or DDPIXELFORMAT size was unexpected."); if (!HasLockedSurfaceDescriptor(flags, surface)) throw new InvalidOperationException("D3D7 Lock descriptor flags were incomplete: size=" + size + " flags=0x" + flags.ToString("X8") + " width=" + width + " height=" + height + " pitch=" + pitch + " surface=0x" + surface.ToString("X8") + " pfSize=" + pfSize + " pfFlags=0x" + pfFlags.ToString("X8") + " bitCount=" + bitCount + " masks=0x" + red.ToString("X8") + "/0x" + green.ToString("X8") + "/0x" + blue.ToString("X8") + "/0x" + alpha.ToString("X8")); if ((flags & DdsdLpSurface) == 0) Log("D3D7_LOCK_LPSURFACE_FLAG_ABSENT accepted_after_S_OK_and_nonzero_pointer; exact bounded ReadProcessMemory remains required"); if ((pfFlags & DdpfRgb) == 0 || (pfFlags & DdpfFourCc) != 0) throw new InvalidOperationException("D3D7 render target is not an uncompressed RGB surface."); if (bitCount != 16 && bitCount != 24 && bitCount != 32) throw new InvalidOperationException("Unsupported D3D7 render-target bit depth: " + bitCount); if (width == 0 || height == 0 || width > 8192 || height > 8192 || surface == 0) throw new InvalidOperationException("D3D7 surface dimensions or pointer are outside the accepted bounds."); long rowBytes = ((long)width * bitCount + 7) / 8; long pitchAbs = Math.Abs((long)pitch); long totalBytes = pitchAbs * height; if (pitch == 0 || pitchAbs < rowBytes || totalBytes <= 0 || totalBytes > PixelReadbackLimit) throw new InvalidOperationException("D3D7 pitch/byte count is outside the bounded readback limits."); long lowest = pitch >= 0 ? surface : (long)surface + (long)(height - 1) * pitch; if (lowest <= 0 || lowest + totalBytes > UInt32.MaxValue) throw new InvalidOperationException("D3D7 surface address range overflowed x86 address space."); byte[] rows = new byte[(int)totalBytes]; if (!ReadExact(process, unchecked((uint)lowest), rows)) throw new InvalidOperationException("Could not copy the bounded locked D3D7 surface rows."); SurfaceReadback result = new SurfaceReadback(); result.Width = width; result.Height = height; result.Pitch = pitch; result.BitCount = bitCount; result.RedMask = red; result.GreenMask = green; result.BlueMask = blue; result.AlphaMask = alpha; result.SurfacePointer = surface; result.Rows = rows; result.RgbMasksVerified = IsValidChannelMask(red, bitCount, true) && IsValidChannelMask(green, bitCount, true) && IsValidChannelMask(blue, bitCount, true) && IsValidChannelMask(alpha, bitCount, false) && (red & green) == 0 && (red & blue) == 0 && (green & blue) == 0 && (alpha == 0 || ((alpha & red) == 0 && (alpha & green) == 0 && (alpha & blue) == 0)); if (!result.RgbMasksVerified) throw new InvalidOperationException("D3D7 RGB channel masks are missing or overlap."); using (SHA256 sha = SHA256.Create()) { byte[] hash = sha.ComputeHash(rows); StringBuilder hex = new StringBuilder(hash.Length * 2); for (int i = 0; i < hash.Length; i++) hex.Append(hash[i].ToString("X2")); result.Sha256 = hex.ToString(); } return result; } private static bool HasLockedSurfaceDescriptor(uint flags, uint surface) { const uint required = DdsdHeight | DdsdWidth | DdsdPitch | DdsdPixelFormat; // Successful IDirectDrawSurface7::Lock returns the lpSurface address; // tolerate drivers that omit only DDSD_LPSURFACE, then require exact bounded // ReadProcessMemory for every pixel row before accepting the capture. return surface != 0 && (flags & required) == required; } private static int MaskChannel(uint pixel, uint mask, int fallback) { if (mask == 0) return fallback; int shift = 0; while (((mask >> shift) & 1) == 0 && shift < 31) shift++; ulong maximum = mask >> shift; ulong value = (pixel & mask) >> shift; return (int)((value * 255UL + maximum / 2UL) / maximum); } private static bool IsValidChannelMask(uint mask, uint bitCount, bool required) { if (mask == 0) return !required; if (bitCount < 32 && (mask >> (int)bitCount) != 0) return false; int shift = 0; while (shift < 32 && ((mask >> shift) & 1) == 0) shift++; if (shift >= 32) return false; uint normalized = mask >> shift; return (normalized & unchecked(normalized + 1u)) == 0; } private static byte[] DecodeSurfaceToBgra(SurfaceReadback surface) { if (surface == null || surface.Rows == null || !surface.RgbMasksVerified) throw new InvalidOperationException("Surface was not verified before pixel conversion."); int width = checked((int)surface.Width); int height = checked((int)surface.Height); int bytesPerPixel = checked((int)(surface.BitCount / 8)); int sourcePitch = checked((int)Math.Abs((long)surface.Pitch)); int rowBytes = checked(width * bytesPerPixel); if (width <= 0 || height <= 0 || (surface.BitCount != 16 && surface.BitCount != 24 && surface.BitCount != 32) || sourcePitch < rowBytes || surface.Rows.Length < checked(sourcePitch * height)) throw new InvalidOperationException("Surface pixel buffer shape is inconsistent."); byte[] bgra = new byte[checked(width * height * 4)]; for (int y = 0; y < height; y++) { int sourceRow = surface.Pitch >= 0 ? y : (height - 1 - y); int sourceBase = checked(sourceRow * sourcePitch); int destinationBase = checked(y * width * 4); for (int x = 0; x < width; x++) { int offset = sourceBase + x * bytesPerPixel; uint packed; if (surface.BitCount == 16) packed = BitConverter.ToUInt16(surface.Rows, offset); else if (surface.BitCount == 24) packed = (uint)(surface.Rows[offset] | (surface.Rows[offset + 1] << 8) | (surface.Rows[offset + 2] << 16)); else packed = BitConverter.ToUInt32(surface.Rows, offset); int outOffset = destinationBase + x * 4; bgra[outOffset] = (byte)MaskChannel(packed, surface.BlueMask, 0); bgra[outOffset + 1] = (byte)MaskChannel(packed, surface.GreenMask, 0); bgra[outOffset + 2] = (byte)MaskChannel(packed, surface.RedMask, 0); // PNG is a screenshot of the composited framebuffer. The // render target's alpha bits are not window opacity metadata. bgra[outOffset + 3] = 255; } } return bgra; } private static void AssertPixel(byte[] actual, params byte[] expected) { if (actual == null || actual.Length != expected.Length) throw new InvalidOperationException("Pixel converter self-check returned the wrong byte count."); for (int i = 0; i < expected.Length; i++) if (actual[i] != expected[i]) throw new InvalidOperationException("Pixel converter self-check failed at byte " + i + "."); } private static void SaveSurfacePng(SurfaceReadback surface, string outputPath) { int width = checked((int)surface.Width); int height = checked((int)surface.Height); bool created = false; try { using (FileStream png = new FileStream(outputPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { created = true; using (Bitmap bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb)) { Rectangle rectangle = new Rectangle(0, 0, width, height); BitmapData bits = bitmap.LockBits(rectangle, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); try { if (bits.Stride < width * 4) throw new InvalidOperationException("PNG staging bitmap stride is too short."); byte[] sourceBgra = DecodeSurfaceToBgra(surface); byte[] rgba = new byte[checked(bits.Stride * height)]; int sourceStride = width * 4; for (int y = 0; y < height; y++) Buffer.BlockCopy(sourceBgra, y * sourceStride, rgba, y * bits.Stride, sourceStride); Marshal.Copy(rgba, 0, bits.Scan0, rgba.Length); } finally { bitmap.UnlockBits(bits); } bitmap.Save(png, ImageFormat.Png); } png.Flush(); } } catch { if (created) try { File.Delete(outputPath); } catch { } throw; } } private static void WriteTextCreateNew(string outputPath, string text) { bool created = false; try { using (FileStream output = new FileStream(outputPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { created = true; byte[] bytes = new UTF8Encoding(false).GetBytes(text); output.Write(bytes, 0, bytes.Length); output.Flush(); } } catch { if (created) try { File.Delete(outputPath); } catch { } throw; } } private static CameraReadback ReadCameraSnapshot(IntPtr process, uint threadId, uint terrainBase) { CameraReadback result = new CameraReadback(); IntPtr thread = OpenThread(ThreadGetContext | ThreadSetContext, false, threadId); IntPtr contextAllocation; IntPtr context = AllocateAlignedX86Context(out contextAllocation); try { Marshal.Copy(new byte[X86ContextSize], 0, context, X86ContextSize); Marshal.WriteInt32(context, 0, unchecked((int)ContextAllX86)); if (thread == IntPtr.Zero || !GetThreadContext(thread, context)) { result.Failure = "World3D breakpoint context was unreadable: " + Marshal.GetLastWin32Error(); return result; } uint esp = unchecked((uint)Marshal.ReadInt32(context, ContextEspOffset)); uint returnAddress = ReadU32(process, esp); uint camera = ReadU32(process, unchecked(esp + 4)); uint vtable = ReadU32(process, camera); uint transformInterface = unchecked(camera + 4); uint transformVtable = ReadU32(process, transformInterface); uint selectorField = ReadU32(process, unchecked(camera + 0x10)); byte[] modeBytes = new byte[1]; byte cameraMode = ReadExact(process, unchecked(camera + 0x1A0), modeBytes) ? modeBytes[0] : (byte)0xFF; byte[] matrixBytes = new byte[64]; if (camera == 0 || !ReadExact(process, unchecked(camera + 0x20), matrixBytes)) { result.Failure = "World3D camera argument or 64-byte selector matrix was unreadable"; return result; } uint[] words = new uint[16]; bool matrixFinite = true; for (int i = 0; i < words.Length; i++) { words[i] = BitConverter.ToUInt32(matrixBytes, i * 4); if (!IsFinite(BitConverter.ToSingle(matrixBytes, i * 4))) matrixFinite = false; } uint expectedVtable = unchecked(terrainBase + ExternalCameraVtableRva); uint expectedTransformVtable = unchecked(terrainBase + 0x66558); uint setterSlot = ReadU32(process, unchecked(transformVtable + 0x1C)); uint invalidateSlot = ReadU32(process, unchecked(transformVtable + 0x30)); bool setterAbiVerified = transformVtable == expectedTransformVtable && setterSlot == unchecked(terrainBase + 0x54C70) && invalidateSlot == unchecked(terrainBase + 0x55280); result.Camera = camera; result.Vtable = vtable; result.TransformInterface = transformInterface; result.TransformVtable = transformVtable; result.CameraMode = cameraMode; result.EntryEsp = esp; result.ReturnAddress = returnAddress; result.SelectorField = selectorField; result.Words = words; result.EntryMatrixBytes = (byte[])matrixBytes.Clone(); result.EntryMatrixSha256 = HashBytes(matrixBytes); result.MatrixFinite = matrixFinite; result.LayoutVerified = terrainBase != 0 && vtable == expectedVtable && setterAbiVerified && cameraMode == 0 && selectorField == 0xFFFFFFFF && matrixFinite; if (!result.LayoutVerified) result.Failure = "external camera primary/transform vtables, setter slots, normal mode, selector field, or matrix finite check failed"; return result; } finally { Marshal.FreeHGlobal(contextAllocation); if (thread != IntPtr.Zero) CloseHandle(thread); } } private static Dictionary ParseJsonObject(string json) { if (String.IsNullOrEmpty(json) || json.Length > CameraInputJsonLimit) throw new InvalidDataException("Camera JSON is empty or exceeds the 1 MiB input limit."); JavaScriptSerializer serializer = new JavaScriptSerializer(); serializer.MaxJsonLength = CameraInputJsonLimit; serializer.RecursionLimit = 32; object value; try { value = serializer.DeserializeObject(json); } catch (Exception error) { throw new InvalidDataException("Camera JSON is malformed.", error); } Dictionary result = value as Dictionary; if (result == null) throw new InvalidDataException("Camera JSON must be a top-level object."); return result; } private static object RequiredJsonField(Dictionary json, string key) { object value; if (json == null || !json.TryGetValue(key, out value) || value == null) throw new InvalidDataException("Camera JSON is missing '" + key + "'."); return value; } private static string JsonStringField(Dictionary json, string key) { string value = RequiredJsonField(json, key) as string; if (value == null) throw new InvalidDataException("Camera JSON field '" + key + "' must be a string."); return value; } private static object[] JsonArrayField(Dictionary json, string key) { object value = RequiredJsonField(json, key); IList list = value as IList; if (list == null) throw new InvalidDataException("Camera JSON field '" + key + "' must be an array."); object[] result = new object[list.Count]; list.CopyTo(result, 0); return result; } private static bool TryJsonDouble(object value, out double result) { if (value is double) result = (double)value; else if (value is decimal) result = (double)(decimal)value; else if (value is int) result = (int)value; else if (value is long) result = (long)value; else if (value is uint) result = (uint)value; else if (value is ulong) result = (ulong)value; else if (value is float) result = (float)value; else { result = 0; return false; } return !Double.IsNaN(result) && !Double.IsInfinity(result); } private static bool TryJsonUInt32(object value, out uint result) { double number; if (!TryJsonDouble(value, out number) || number < 0 || number > UInt32.MaxValue || Math.Truncate(number) != number) { result = 0; return false; } result = (uint)number; return true; } private static bool TryJsonInt32(object value, out int result) { double number; if (!TryJsonDouble(value, out number) || number < Int32.MinValue || number > Int32.MaxValue || Math.Truncate(number) != number) { result = 0; return false; } result = (int)number; return true; } private static bool TryJsonFloat(object value, out float result) { double number; if (!TryJsonDouble(value, out number) || number < -Single.MaxValue || number > Single.MaxValue) { result = 0; return false; } result = (float)number; return IsFinite(result); } private static bool IsOrthonormalAffineCamera(byte[] matrixBytes, out string failure) { failure = null; if (matrixBytes == null || matrixBytes.Length != 64) { failure = "camera matrix must contain exactly 64 bytes"; return false; } float[] m = new float[16]; for (int i = 0; i < m.Length; i++) { m[i] = BitConverter.ToSingle(matrixBytes, i * 4); if (!IsFinite(m[i])) { failure = "camera matrix contains a nonfinite value"; return false; } } const double affineTolerance = 0.0001; if (Math.Abs(m[12]) > affineTolerance || Math.Abs(m[13]) > affineTolerance || Math.Abs(m[14]) > affineTolerance || Math.Abs(m[15] - 1.0) > affineTolerance) { failure = "camera matrix last row is not affine [0,0,0,1]"; return false; } if (Math.Abs(m[3]) > 1000000 || Math.Abs(m[7]) > 1000000 || Math.Abs(m[11]) > 1000000) { failure = "camera translation is outside the supported finite range"; return false; } // Native selector-0 matrices are row-major affine transforms with a // rigid, orthonormal 3x3 basis. Reject shear/scale before the DLL call. double[] rowLengthSquared = new double[3]; for (int row = 0; row < 3; row++) { int offset = row * 4; for (int column = 0; column < 3; column++) rowLengthSquared[row] += (double)m[offset + column] * m[offset + column]; if (Math.Abs(rowLengthSquared[row] - 1.0) > 0.02) { failure = "camera basis row is not unit length"; return false; } } for (int firstRow = 0; firstRow < 3; firstRow++) for (int secondRow = firstRow + 1; secondRow < 3; secondRow++) { double dot = 0; for (int column = 0; column < 3; column++) dot += (double)m[firstRow * 4 + column] * m[secondRow * 4 + column]; if (Math.Abs(dot) > 0.01) { failure = "camera basis contains shear or nonorthogonal axes"; return false; } } double determinant = (double)m[0] * (m[5] * m[10] - m[6] * m[9]) - (double)m[1] * (m[4] * m[10] - m[6] * m[8]) + (double)m[2] * (m[4] * m[9] - m[5] * m[8]); if (Math.Abs(Math.Abs(determinant) - 1.0) > 0.04) { failure = "camera basis is singular or not approximately rigid"; return false; } return true; } private static SelectedCameraInput ParseSelectedCameraJson(string json, string sourcePath) { Dictionary root = ParseJsonObject(json); bool renderInputUsable = RequiredJsonField(root, "render_input_usable") is bool && (bool)RequiredJsonField(root, "render_input_usable"); if (!String.Equals(JsonStringField(root, "schema"), "fparkan-legacy-camera-v1", StringComparison.Ordinal) || !String.Equals(JsonStringField(root, "capture_status"), "native-frame-captured", StringComparison.Ordinal) || !renderInputUsable) throw new InvalidDataException("Camera input must be a usable fparkan-legacy-camera-v1 native capture."); SelectedCameraInput input = new SelectedCameraInput(); input.Path = Path.GetFullPath(sourcePath); object[] words = JsonArrayField(root, "selector0_words"); if (words.Length != 16) throw new InvalidDataException("selector0_words must contain exactly 16 uint32 values."); input.MatrixWords = new uint[16]; input.MatrixBytes = new byte[64]; for (int i = 0; i < words.Length; i++) { uint word; if (!TryJsonUInt32(words[i], out word)) throw new InvalidDataException("selector0_words contains a value outside uint32 range."); input.MatrixWords[i] = word; Buffer.BlockCopy(BitConverter.GetBytes(word), 0, input.MatrixBytes, i * 4, 4); } string matrixFailure; if (!IsOrthonormalAffineCamera(input.MatrixBytes, out matrixFailure)) throw new InvalidDataException("Camera input rejected: " + matrixFailure + "."); input.MatrixSha256 = HashBytes(input.MatrixBytes); object[] viewport = JsonArrayField(root, "viewport"); if (viewport.Length != 4) throw new InvalidDataException("viewport must contain exactly four integers."); input.Viewport = new int[4]; for (int i = 0; i < viewport.Length; i++) if (!TryJsonInt32(viewport[i], out input.Viewport[i])) throw new InvalidDataException("viewport contains an invalid integer."); if (input.Viewport[0] < 0 || input.Viewport[1] < 0 || input.Viewport[2] <= input.Viewport[0] || input.Viewport[3] <= input.Viewport[1]) throw new InvalidDataException("viewport bounds must have positive width and height."); if (!TryJsonFloat(RequiredJsonField(root, "near_plane"), out input.Near) || !TryJsonFloat(RequiredJsonField(root, "far_plane"), out input.Far) || !TryJsonFloat(RequiredJsonField(root, "field_of_view_radians"), out input.FieldOfView) || !IsFinite(input.Near) || !IsFinite(input.Far) || !IsFinite(input.FieldOfView) || input.Near <= 0 || input.Far <= input.Near || input.FieldOfView <= 0.05f || input.FieldOfView >= 3.1f) throw new InvalidDataException("Camera projection must have finite, ordered near/far planes and a finite perspective FOV."); uint projectionMode; if (!TryJsonUInt32(RequiredJsonField(root, "projection_mode_byte"), out projectionMode) || projectionMode == 0 || projectionMode > Byte.MaxValue) throw new InvalidDataException("Camera input requires a verified perspective projection mode."); input.ProjectionMode = (byte)projectionMode; return input; } private static SelectedCameraInput LoadSelectedCameraInput(string path) { string fullPath = Path.GetFullPath(path); if (!File.Exists(fullPath)) throw new FileNotFoundException("Camera input JSON does not exist.", fullPath); FileInfo file = new FileInfo(fullPath); if (file.Length > CameraInputJsonLimit) throw new InvalidDataException("Camera input exceeds the 1 MiB limit."); byte[] bytes = File.ReadAllBytes(fullPath); if (bytes.Length > CameraInputJsonLimit) throw new InvalidDataException("Camera input exceeded the 1 MiB limit while being read."); int offset = bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF ? 3 : 0; string text; try { text = new UTF8Encoding(false, true).GetString(bytes, offset, bytes.Length - offset); } catch (DecoderFallbackException error) { throw new InvalidDataException("Camera input is not valid UTF-8.", error); } return ParseSelectedCameraJson(text, fullPath); } private static bool ProjectionMatchesCameraInput(ProjectionReadback projection, SelectedCameraInput input) { if (projection == null || input == null || !projection.Verified || projection.Mode != input.ProjectionMode || projection.Viewport == null || projection.Viewport.Length != 4) return false; for (int i = 0; i < 4; i++) if (projection.Viewport[i] != input.Viewport[i]) return false; return NearlyEqual(projection.Near, input.Near, 0.0001f) && NearlyEqual(projection.Far, input.Far, 0.001f) && NearlyEqual(projection.Fov, input.FieldOfView, 0.0001f); } private static bool NearlyEqual(float left, float right, float tolerance) { return IsFinite(left) && IsFinite(right) && Math.Abs((double)left - right) <= tolerance * Math.Max(1.0, Math.Max(Math.Abs((double)left), Math.Abs((double)right))); } private static void SelfCheckCameraInput() { string valid = "{\"schema\":\"fparkan-legacy-camera-v1\",\"capture_status\":\"native-frame-captured\"," + "\"render_input_usable\":true,\"selector0_words\":[1065353216,0,0,1065353216," + "0,1065353216,0,1073741824,0,0,1065353216,1077936128,0,0,0,1065353216]," + "\"viewport\":[0,0,1280,1024],\"near_plane\":0.5,\"far_plane\":700," + "\"field_of_view_radians\":1.04,\"projection_mode_byte\":1}"; SelectedCameraInput parsed = ParseSelectedCameraJson(valid, "input.json"); if (parsed.MatrixBytes.Length != 64 || parsed.Viewport[2] != 1280 || parsed.MatrixSha256.Length != 64) throw new InvalidOperationException("Camera JSON parser self-check failed on a valid rigid transform."); string zeroMatrix = valid.Replace("1065353216,0,0,1065353216,0,1065353216,0,1073741824,0,0,1065353216,1077936128,0,0,0,1065353216", "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0"); bool rejected = false; try { ParseSelectedCameraJson(zeroMatrix, "bad.json"); } catch (InvalidDataException) { rejected = true; } if (!rejected) throw new InvalidOperationException("Camera JSON parser accepted a singular all-zero matrix."); string nonPerspective = valid.Replace("\"projection_mode_byte\":1", "\"projection_mode_byte\":0"); rejected = false; try { ParseSelectedCameraJson(nonPerspective, "bad.json"); } catch (InvalidDataException) { rejected = true; } if (!rejected) throw new InvalidOperationException("Camera JSON parser accepted a non-perspective projection."); string nonNumericWord = valid.Replace("1065353216,0,0,1065353216", "\"1065353216\",0,0,1065353216"); rejected = false; try { ParseSelectedCameraJson(nonNumericWord, "bad.json"); } catch (InvalidDataException) { rejected = true; } if (!rejected) throw new InvalidOperationException("Camera JSON parser accepted a quoted matrix word."); } private static bool RefreshCameraWordsAtProjection(IntPtr process, CameraReadback camera, uint terrainBase, uint generation) { if (camera == null || !camera.LayoutVerified || camera.Camera == 0 || camera.EntryMatrixBytes == null || camera.EntryMatrixBytes.Length != 64) { if (camera != null) { camera.CurrentAtProjectionVerified = false; camera.Failure = "camera entry layout was not verified before the projection boundary"; } return false; } uint vtable = ReadU32(process, camera.Camera); uint selectorField = ReadU32(process, unchecked(camera.Camera + 0x10)); uint expectedVtable = unchecked(terrainBase + ExternalCameraVtableRva); byte[] current = new byte[64]; if (vtable != expectedVtable || selectorField != 0xFFFFFFFF || !ReadExact(process, unchecked(camera.Camera + 0x20), current)) { camera.CurrentAtProjectionVerified = false; camera.Failure = "camera pointer, vtable, selector, or matrix changed before projection"; Log("CAMERA_MATRIX_AT_PROJECTION_REJECTED generation=" + generation + " vtable=0x" + vtable.ToString("X8") + " selector=0x" + selectorField.ToString("X8") + " expectedVtable=0x" + expectedVtable.ToString("X8")); return false; } uint[] words = new uint[16]; bool finite = true; for (int i = 0; i < words.Length; i++) { words[i] = BitConverter.ToUInt32(current, i * 4); if (!IsFinite(BitConverter.ToSingle(current, i * 4))) finite = false; } if (!finite) { camera.CurrentAtProjectionVerified = false; camera.Failure = "camera matrix contains a non-finite value at projection boundary"; Log("CAMERA_MATRIX_AT_PROJECTION_REJECTED generation=" + generation + " nonFinite=true"); return false; } bool changed = !ByteArraysEqual(camera.EntryMatrixBytes, current); camera.Vtable = vtable; camera.SelectorField = selectorField; camera.Words = words; camera.ProjectionMatrixBytes = (byte[])current.Clone(); camera.MatrixFinite = true; camera.CurrentAtProjectionVerified = true; camera.WordsChangedAtProjection = changed; camera.ProjectionMatrixSha256 = HashBytes(current); Log("CAMERA_MATRIX_AT_PROJECTION generation=" + generation + " verified=true changedSinceEntry=" + changed + " entrySha256=" + camera.EntryMatrixSha256 + " projectionSha256=" + camera.ProjectionMatrixSha256); return true; } private static string HashBytes(byte[] bytes) { using (SHA256 sha = SHA256.Create()) { byte[] hash = sha.ComputeHash(bytes); StringBuilder result = new StringBuilder(hash.Length * 2); for (int i = 0; i < hash.Length; i++) result.Append(hash[i].ToString("X2")); return result.ToString(); } } private static int SelfCheckReadback() { if (IntPtr.Size != 4) throw new InvalidOperationException("This helper must run as x86."); uint flags = DdLockReadOnly | DdLockDoNotWait; if (DdLockReadOnly != 0x10 || DdLockDoNotWait != 0x4000 || flags != 0x4010 || flags == 0x30) throw new InvalidOperationException("DDLOCK flag self-check failed."); RemoteCode acquire = BuildReadbackStub(0x10000000, 0x20000000, false); RemoteCode cleanup = BuildReadbackStub(0x10000000, 0, true); RemoteCode cameraSetter = BuildCameraSetterStub(0x30000000, 0x40000000); string[] d3d7DeviceVtable = new string[] { "QueryInterface", "AddRef", "Release", "GetCaps", "EnumTextureFormats", "BeginScene", "EndScene", "GetDirect3D", "SetRenderTarget", "GetRenderTarget", "Clear", "SetTransform", "GetTransform", "SetViewport", "MultiplyTransform", "GetViewport", "SetMaterial", "GetMaterial", "SetLight", "GetLight" }; string[] surface7Vtable = new string[] { "QueryInterface", "AddRef", "Release", "AddAttachedSurface", "AddOverlayDirtyRect", "Blt", "BltBatch", "BltFast", "DeleteAttachedSurface", "EnumAttachedSurfaces", "EnumOverlayZOrders", "Flip", "GetAttachedSurface", "GetBltStatus", "GetCaps", "GetClipper", "GetColorKey", "GetDC", "GetFlipStatus", "GetOverlayPosition", "GetPalette", "GetPixelFormat", "GetSurfaceDesc", "Initialize", "IsLost", "Lock", "ReleaseDC", "Restore", "SetClipper", "SetColorKey", "SetOverlayPosition", "SetPalette", "Unlock", "UpdateOverlay", "UpdateOverlayDisplay", "UpdateOverlayZOrder", "GetDDInterface", "PageLock", "PageUnlock", "SetSurfaceDesc", "SetPrivateData", "GetPrivateData", "FreePrivateData", "GetUniquenessValue", "ChangeUniquenessValue", "SetPriority", "GetPriority", "SetLOD", "GetLOD" }; byte[] originalContext = new byte[X86ContextSize]; const uint originalEsp = 0x00100100; Buffer.BlockCopy(BitConverter.GetBytes(unchecked((int)0x12345678)), 0, originalContext, ContextEipOffset, 4); Buffer.BlockCopy(BitConverter.GetBytes(unchecked((int)originalEsp)), 0, originalContext, ContextEspOffset, 4); Buffer.BlockCopy(BitConverter.GetBytes(0x00000202), 0, originalContext, ContextEflagsOffset, 4); byte[] remoteContext = ContextForRemoteCode(originalContext, 0x10001000, originalEsp); byte[] presentArgs = new byte[PresentCallArgumentsBytes]; for (int i = 0; i < presentArgs.Length; i++) presentArgs[i] = (byte)(i + 1); byte[] samePresentArgs = (byte[])presentArgs.Clone(); byte[] changedPresentArgs = (byte[])presentArgs.Clone(); changedPresentArgs[PresentCallArgumentsBytes - 1] ^= 1; bool validStackRange = IsSafeNativeStackRange(0x00100000, 0x00200000, originalEsp, 24, 0x00100000, 0x1000, MemCommit, 0x04); if (Marshal.SizeOf(typeof(X86ContextLayout)) != X86ContextSize || Marshal.OffsetOf(typeof(X86ContextLayout), "Eip").ToInt32() != ContextEipOffset || Marshal.OffsetOf(typeof(X86ContextLayout), "Esp").ToInt32() != ContextEspOffset || ContextAllX86 != 0x0001003F || acquire.Bytes.Length == 0 || cleanup.Bytes.Length == 0 || acquire.MaxCallStackBytes != RemoteStubMaxCallStackBytes || cleanup.MaxCallStackBytes != 12 || cameraSetter.Bytes.Length == 0 || cameraSetter.MaxCallStackBytes != 16 || cameraSetter.CallInstructionOffsets == null || cameraSetter.CallInstructionOffsets.Length != 1 || DecodeIndirectCallDisplacement(cameraSetter.Bytes, cameraSetter.CallInstructionOffsets[0]) != CameraTransformSetterSlotOffset || cameraSetter.TrapAddress != 0x30000000 + cameraSetter.Bytes.Length - 1 || VtableOffset(d3d7DeviceVtable, "GetRenderTarget") != D3dDeviceGetRenderTargetOffset || VtableOffset(surface7Vtable, "Lock") != DdSurfaceLockOffset || VtableOffset(surface7Vtable, "SetOverlayPosition") != 0x78 || VtableOffset(surface7Vtable, "Unlock") != DdSurfaceUnlockOffset || VtableOffset(surface7Vtable, "Release") != DdSurfaceReleaseOffset || cleanup.CallInstructionOffsets == null || cleanup.CallInstructionOffsets.Length != 2 || DecodeIndirectCallDisplacement(cleanup.Bytes, cleanup.CallInstructionOffsets[0]) != DdSurfaceUnlockOffset || DecodeIndirectCallDisplacement(cleanup.Bytes, cleanup.CallInstructionOffsets[1]) != DdSurfaceReleaseOffset || acquire.CallInstructionOffsets == null || acquire.CallInstructionOffsets.Length != 3 || DecodeIndirectCallDisplacement(acquire.Bytes, acquire.CallInstructionOffsets[0]) != D3dDeviceGetRenderTargetOffset || DecodeIndirectCallDisplacement(acquire.Bytes, acquire.CallInstructionOffsets[1]) != DdSurfaceLockOffset || DecodeIndirectCallDisplacement(acquire.Bytes, acquire.CallInstructionOffsets[2]) != DdSurfaceReleaseOffset || DecodeIndirectCallDisplacement(new byte[] { 0xFF, 0x50, 0x80 }, 0) != -128 || acquire.TrapAddress != 0x10000000 + acquire.Bytes.Length - 1 || cleanup.TrapAddress != 0x10000000 + cleanup.Bytes.Length - 1 || !validStackRange || !HasNativeStackBounds(0x00100000, 0x00200000, 0x00100018, 24) || HasNativeStackBounds(0x00100000, 0x00200000, 0x00100017, 24) || HasNativeStackBounds(0x00100000, 0x00200000, 0x00000010, 24) || HasNativeStackBounds(0x00100000, 0x00200000, 0x00100100, 0) || !IsSafeNativeStackRange(0x00100000, 0x00200000, originalEsp, 24, 0x001000E8, 0x1000, MemCommit, 0x04) || IsSafeNativeStackRange(0x00100000, 0x00200000, originalEsp, 24, 0x00100000, 0x1000, MemCommit, 0x104) || IsSafeNativeStackRange(0x00100000, 0x00200000, originalEsp, 24, 0x00100000, 0xF0, MemCommit, 0x04) || BitConverter.ToUInt32(remoteContext, ContextEspOffset) != originalEsp || BitConverter.ToUInt32(remoteContext, ContextEipOffset) != 0x10001000 || BitConverter.ToUInt32(remoteContext, ContextEflagsOffset) != 0x00000202 || !ByteArraysEqual(presentArgs, samePresentArgs) || ByteArraysEqual(presentArgs, changedPresentArgs) || !IsViewportInsideSurface(new int[] { 0, 0, 1280, 1024 }, 1280, 1024) || IsViewportInsideSurface(new int[] { -1, 0, 1280, 1024 }, 1280, 1024) || IsViewportInsideSurface(new int[] { 0, 0, 1281, 1024 }, 1280, 1024) || IsViewportInsideSurface(new int[] { 0, 0, 0, 1024 }, 1280, 1024)) throw new InvalidOperationException("Remote stub branch/INT3 self-check failed."); if (!HasLockedSurfaceDescriptor(0x100F, 0x19510000) || HasLockedSurfaceDescriptor(0x1007, 0x19510000) || HasLockedSurfaceDescriptor(0x100F, 0)) throw new InvalidOperationException("D3D7 Lock descriptor compatibility self-check failed."); AssertPixel(DecodeSurfaceToBgra(new SurfaceReadback { Width = 1, Height = 1, Pitch = 2, BitCount = 16, RedMask = 0xF800, GreenMask = 0x07E0, BlueMask = 0x001F, Rows = new byte[] { 0x1F, 0xF8 }, RgbMasksVerified = true }), 255, 0, 255, 255); AssertPixel(DecodeSurfaceToBgra(new SurfaceReadback { Width = 1, Height = 1, Pitch = 3, BitCount = 24, RedMask = 0x00FF0000, GreenMask = 0x0000FF00, BlueMask = 0x000000FF, Rows = new byte[] { 0x11, 0x22, 0x33 }, RgbMasksVerified = true }), 0x11, 0x22, 0x33, 255); AssertPixel(DecodeSurfaceToBgra(new SurfaceReadback { Width = 1, Height = 1, Pitch = 4, BitCount = 32, RedMask = 0x00FF0000, GreenMask = 0x0000FF00, BlueMask = 0x000000FF, AlphaMask = 0xFF000000, Rows = new byte[] { 0x44, 0x55, 0x66, 0x77 }, RgbMasksVerified = true }), 0x44, 0x55, 0x66, 255); AssertPixel(DecodeSurfaceToBgra(new SurfaceReadback { Width = 1, Height = 2, Pitch = -3, BitCount = 24, RedMask = 0x00FF0000, GreenMask = 0x0000FF00, BlueMask = 0x000000FF, Rows = new byte[] { 1, 2, 3, 4, 5, 6 }, RgbMasksVerified = true }), 4, 5, 6, 255, 1, 2, 3, 255); Console.WriteLine("self-check: D3D7 GetRenderTarget and Surface7 Lock/Unlock/Release SDK vtable ordinals OK"); Console.WriteLine("self-check: caller stack footprint (24/12 bytes), original ESP, committed writable range and guard rejection OK"); Console.WriteLine("self-check: selected-camera setter stub uses verified interface slot +0x1C and 16-byte maximum caller stack OK"); Console.WriteLine("self-check: 24-byte present call arguments compare unchanged and detect a changed byte"); Console.WriteLine("self-check: return context logging verifies remote EIP/ESP before restoring the original present call"); Console.WriteLine("self-check: viewport bounds fit the captured pixel surface OK"); Console.WriteLine("self-check: DDLOCK_READONLY|DONOTWAIT=0x4010; success/failure paths include Release; cleanup calls Release after Unlock regardless of result"); Console.WriteLine("self-check: RGB565, 24-bit, 32-bit opaque PNG conversion and negative-pitch row order OK"); return 0; } private static int VtableOffset(string[] methodOrder, string method) { for (int i = 0; i < methodOrder.Length; i++) if (String.Equals(methodOrder[i], method, StringComparison.Ordinal)) return i * 4; return -1; } private static int DecodeIndirectCallDisplacement(byte[] code, int offset) { if (code == null || offset < 0 || offset + 2 > code.Length || code[offset] != 0xFF || ((code[offset + 1] >> 3) & 7) != 2) throw new InvalidOperationException("Expected an x86 indirect CALL instruction."); int mod = code[offset + 1] >> 6; int rm = code[offset + 1] & 7; if (mod == 1) { if (offset + 3 > code.Length) throw new InvalidOperationException("Truncated disp8 indirect CALL."); return unchecked((sbyte)code[offset + 2]); } if (mod == 2 || (mod == 0 && rm == 5)) { if (offset + 6 > code.Length) throw new InvalidOperationException("Truncated disp32 indirect CALL."); return BitConverter.ToInt32(code, offset + 2); } return 0; } }