aboutsummaryrefslogtreecommitdiff
path: root/tools/native-frame-capture/NativeFrameCapture.Readback.cs
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-10-11 16:37:48 +0300
committerValentin Popov <valentin@popov.link>2026-10-11 16:37:48 +0300
commit100f656b089898e4ffef48871f5ed5ea068d54b6 (patch)
tree46bede940909108a40fef0bcca6ab7a2795c3ed0 /tools/native-frame-capture/NativeFrameCapture.Readback.cs
parent7b7324a413449b21b1d29f77a36abfe282c045e6 (diff)
downloadfparkan-100f656b089898e4ffef48871f5ed5ea068d54b6.tar.xz
fparkan-100f656b089898e4ffef48871f5ed5ea068d54b6.zip
Fix terrain height and native frame capture
Use raw world-space terrain heights throughout mesh, camera-floor, shadow, and sun-ray paths while preserving the projection far-plane conversion. Complete the verified selected-camera readback flow with frozen phase sampling, capture tooling, and documentation.
Diffstat (limited to 'tools/native-frame-capture/NativeFrameCapture.Readback.cs')
-rw-r--r--tools/native-frame-capture/NativeFrameCapture.Readback.cs1863
1 files changed, 1863 insertions, 0 deletions
diff --git a/tools/native-frame-capture/NativeFrameCapture.Readback.cs b/tools/native-frame-capture/NativeFrameCapture.Readback.cs
new file mode 100644
index 0000000..872487b
--- /dev/null
+++ b/tools/native-frame-capture/NativeFrameCapture.Readback.cs
@@ -0,0 +1,1863 @@
+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<byte> _bytes = new List<byte>();
+ private readonly Dictionary<string, int> _labels = new Dictionary<string, int>(StringComparer.Ordinal);
+ private readonly List<KeyValuePair<int, string>> _relative32 = new List<KeyValuePair<int, string>>();
+ private readonly List<int> _callInstructionOffsets = new List<int>();
+ 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<int, string>(operand, label));
+ }
+
+ public void Jump(string label)
+ {
+ _bytes.Add(0xE9);
+ int operand = _bytes.Count;
+ EmitU32(0);
+ _relative32.Add(new KeyValuePair<int, string>(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<int, string> 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<int, string> 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<string, object> 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<string, object> result = value as Dictionary<string, object>;
+ if (result == null) throw new InvalidDataException("Camera JSON must be a top-level object.");
+ return result;
+ }
+
+ private static object RequiredJsonField(Dictionary<string, object> 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<string, object> 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<string, object> 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<string, object> 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;
+ }
+}