aboutsummaryrefslogtreecommitdiff
path: root/tools/native-frame-capture/NativeFrameCapture.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.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.cs')
-rw-r--r--tools/native-frame-capture/NativeFrameCapture.cs2525
1 files changed, 2525 insertions, 0 deletions
diff --git a/tools/native-frame-capture/NativeFrameCapture.cs b/tools/native-frame-capture/NativeFrameCapture.cs
new file mode 100644
index 0000000..2a2d8c9
--- /dev/null
+++ b/tools/native-frame-capture/NativeFrameCapture.cs
@@ -0,0 +1,2525 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Security.Cryptography;
+using System.Text;
+
+// Small, x86-only, one-shot debugger for the isolated GOG-compatible scratch
+// process. It records verified camera/projection fields and a D3D7 frame. Pixel
+// readback calls the target's verified D3D7 COM methods on its stopped render
+// thread through a bounded temporary stub; it does not load game DLLs.
+internal static partial class NativeFrameCapture
+{
+ private const uint DbgContinue = 0x00010002;
+ private const uint DbgExceptionNotHandled = 0x80010001;
+ private const uint ExceptionDebugEvent = 1;
+ private const uint CreateProcessDebugEvent = 3;
+ private const uint CreateThreadDebugEvent = 2;
+ private const uint ExitThreadDebugEvent = 4;
+ private const uint ExitProcessDebugEvent = 5;
+ private const uint LoadDllDebugEvent = 6;
+ private const uint PageExecuteReadWrite = 0x40;
+ private const uint ThreadGetContext = 0x0008;
+ private const uint ThreadSetContext = 0x0010;
+ private const int ContextEipOffset = 184;
+ private const int ContextEbxOffset = 164;
+ private const int ContextEdxOffset = 168;
+ private const int ContextEcxOffset = 172;
+ private const int ContextEsiOffset = 160;
+ private const int ContextEbpOffset = 180;
+ private const int ContextEspOffset = 196;
+ private const int PresentCallArgumentsBytes = 24;
+ private const int X86ContextSize = 716;
+ private const uint MemCommit = 0x1000;
+ private const uint PageNoAccess = 0x01;
+ private const uint PageReadOnly = 0x02;
+ private const uint PageReadWrite = 0x04;
+ private const uint PageWriteCopy = 0x08;
+ private const uint PageExecuteRead = 0x20;
+ private const uint PageExecuteWriteCopy = 0x80;
+ private const string GameDirectory = @"target\shadow-probe\Parkan - Iron Strategy";
+ private const string GameExe = "iron_3d.exe";
+ private const string World3DName = "World3D.dll";
+ private const string TerrainName = "Terrain.dll";
+ private const string Ngi32Name = "Ngi32.dll";
+ private const string Iron3dName = "iron3d.dll";
+ private const uint RenderGameRva = 0x13BD0;
+ private const uint ProjectionSnapshotRva = 0x13CE4;
+ private const uint RenderReturnRva = 0x13D7B;
+ private const uint AtmospherePhaseRva = 0x421DC;
+ private const uint PresentBoundaryRva = 0x6E1B;
+ private const uint RelocatedGlobalRva = 0x79518C;
+ private const uint ExternalCameraVtableRva = 0x665B4;
+ private const uint MissionFactoryRva = 0xA1FE1;
+ private const uint MissionVtableCallRva = 0xA1FF0;
+ private const uint MissionImportThunkRva = 0xCD01C;
+ private const int MissionProbeAttemptLimit = 8;
+ private const int FrameCaptureTimeoutSeconds = 300;
+
+ private static readonly Dictionary<string, string> ExpectedSha256 =
+ new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
+ {
+ { "iron_3d.exe", "F476AF85C034A4B4F34F49D0806E4DFF397B5DA0EE26D382A7674231144979F7" },
+ { "World3D.dll", "17E4A3089B2583A8CF2356C9DB0390B1ABA138356A09130D79B4E7E4791DA61E" },
+ { "Ngi32.dll", "BAB9840D94F4E4E74FFC26677724FA896CF4823845504D09A9E025F80016EDF5" },
+ { "Terrain.dll", "AF87D1B2E728A0BE73C52BE3B44CC196AB46DA7799F25A15D40F8C9B0B425EAD" },
+ { "iron3d.dll", "D1DC4EA8535E2069B0A05C9E8BE2724DD438BA44CCA7A83007D8B2E50F9E35FA" }
+ };
+
+ private static string _logPath;
+
+ private sealed class BreakpointInfo
+ {
+ public string Name;
+ public uint Address;
+ public byte OriginalByte;
+ public bool Armed;
+ }
+
+ private sealed class FrameSnapshot
+ {
+ public uint CameraGeneration;
+ public uint CameraThreadId;
+ public CameraReadback Camera;
+ public uint ProjectionGeneration;
+ public uint ProjectionThreadId;
+ public ProjectionReadback Projection;
+ public uint CameraWorldGameTimeWord;
+ public bool CameraWorldGameTimeWordReadable;
+ public uint WorldGameTimeWord;
+ public bool WorldGameTimeWordReadable;
+ public AtmosphereReadback Atmosphere;
+ public readonly Dictionary<uint, AtmosphereReadback> AtmosphereByThread = new Dictionary<uint, AtmosphereReadback>();
+ public string MissionPath;
+ public string MissionEvidence;
+ public bool SelectedCameraRequested;
+ public SelectedCameraState SelectedCamera;
+ }
+
+ private sealed class AtmosphereReadback
+ {
+ public uint SampleThreadId;
+ public uint CameraGenerationAtSample;
+ public uint CameraThreadIdAtSample;
+ public uint AtmosphereObject;
+ public uint RawClock;
+ public uint PhaseMilliseconds;
+ public uint Origin;
+ public uint PeriodMilliseconds;
+ public uint RecomputedPhaseMilliseconds;
+ public uint WorldGameTimeWord;
+ public bool WorldGameTimeWordReadable;
+ public string TerrainModuleSha256;
+ public bool TerrainModuleHashVerified;
+ public bool ArithmeticVerified;
+ public bool WorldTimeMatchesRawClock;
+ public uint CandidateRenderGeneration;
+ public uint CandidateRenderThreadId;
+ public uint MatchedRenderGeneration;
+ public uint MatchedRenderThreadId;
+ public bool SameGenerationVerified;
+ }
+
+ private sealed class PendingStep
+ {
+ public BreakpointInfo Breakpoint;
+ public uint ThreadId;
+ public DateTime DeadlineUtc;
+ public bool RearmOnComplete;
+ }
+
+ private sealed class SelectedCameraState
+ {
+ public SelectedCameraInput Input;
+ public uint CandidateCameraPointer;
+ public uint CandidateThreadId;
+ public uint CandidateGeneration;
+ public bool CandidateProjectionVerified;
+ public bool Applied;
+ public bool ProjectionMatchesInput;
+ public bool Restored;
+ public bool RestoreVerified;
+ public bool MatrixIntactAtReturn;
+ public bool PixelAttributionInvalidated;
+ public string PixelAttributionFailure;
+ public bool ProjectionGateDiagnosticLogged;
+ public bool ReturnGateDiagnosticLogged;
+ public DateTime RestoreDeadlineUtc;
+ public uint CameraPointer;
+ public uint ThreadId;
+ public uint Generation;
+ public uint EntryEsp;
+ public uint FunctionStackEsp;
+ public uint ReturnAddress;
+ public byte[] OriginalMatrixBytes;
+ public string OriginalMatrixSha256;
+ }
+
+ private enum RemoteReadbackPhase { Acquire, Cleanup }
+
+ private sealed class RemoteReadbackSession
+ {
+ public RemoteCode Acquire;
+ public RemoteCode Cleanup;
+ public uint Region;
+ public uint ThreadId;
+ public uint SafeEsp;
+ public byte[] OriginalContext;
+ public byte[] PresentStackArguments;
+ public bool PresentStackArgumentsIntact;
+ public bool RemoteContextIntact;
+ public BreakpointInfo PresentBreakpoint;
+ public FrameSnapshot Frame;
+ public uint Generation;
+ public string OutputJson;
+ public string OutputPng;
+ public DateTime DeadlineUtc;
+ public RemoteReadbackPhase Phase;
+ public SurfaceReadback Surface;
+ public string PixelFailure;
+ public uint Status;
+ public uint ReleaseResult;
+ public uint GetHr;
+ public uint LockHr;
+ public uint UnlockHr;
+ }
+
+ [StructLayout(LayoutKind.Explicit, Size = X86ContextSize)]
+ private struct X86ContextLayout
+ {
+ [FieldOffset(ContextEipOffset)] public uint Eip;
+ [FieldOffset(ContextEsiOffset)] public uint Esi;
+ [FieldOffset(ContextEbpOffset)] public uint Ebp;
+ [FieldOffset(ContextEspOffset)] public uint Esp;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct MemoryBasicInformation
+ {
+ public IntPtr BaseAddress;
+ public IntPtr AllocationBase;
+ public uint AllocationProtect;
+ public UIntPtr RegionSize;
+ public uint State;
+ public uint Protect;
+ public uint Type;
+ }
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern bool DebugActiveProcess(uint processId);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern bool DebugActiveProcessStop(uint processId);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern bool DebugSetProcessKillOnExit(bool killOnExit);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern bool WaitForDebugEvent(IntPtr debugEvent, uint milliseconds);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern bool ContinueDebugEvent(uint processId, uint threadId, uint continueStatus);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern bool ReadProcessMemory(IntPtr process, IntPtr address,
+ [Out] byte[] buffer, UIntPtr size, out UIntPtr bytesRead);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern bool WriteProcessMemory(IntPtr process, IntPtr address,
+ byte[] buffer, UIntPtr size, out UIntPtr bytesWritten);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern bool VirtualProtectEx(IntPtr process, IntPtr address,
+ UIntPtr size, uint newProtection, out uint oldProtection);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern bool FlushInstructionCache(IntPtr process, IntPtr address, UIntPtr size);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern IntPtr OpenThread(uint desiredAccess, bool inheritHandle, uint threadId);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern bool GetThreadContext(IntPtr thread, IntPtr context);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern bool SetThreadContext(IntPtr thread, IntPtr context);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern IntPtr OpenProcess(uint desiredAccess, bool inheritHandle, uint processId);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern UIntPtr VirtualQueryEx(IntPtr process, IntPtr address,
+ out MemoryBasicInformation information, UIntPtr length);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern bool CloseHandle(IntPtr handle);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ private static extern bool GetExitCodeProcess(IntPtr process, out uint exitCode);
+
+ [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern uint GetFinalPathNameByHandleW(IntPtr file, StringBuilder path,
+ uint pathLength, uint flags);
+
+ [DllImport("psapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ private static extern uint GetModuleFileNameExW(IntPtr process, IntPtr module,
+ StringBuilder fileName, uint size);
+
+ private static IntPtr Ptr(uint value) { return new IntPtr(unchecked((int)value)); }
+
+ private static void Log(string text)
+ {
+ if (_logPath != null)
+ File.AppendAllText(_logPath, DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture)
+ + " " + text + Environment.NewLine, Encoding.UTF8);
+ }
+
+ private static string NormalizePath(string path)
+ {
+ if (path.StartsWith(@"\\?\UNC\", StringComparison.OrdinalIgnoreCase)) return @"\\" + path.Substring(8);
+ if (path.StartsWith(@"\\?\", StringComparison.OrdinalIgnoreCase)) return path.Substring(4);
+ return Path.GetFullPath(path);
+ }
+
+ private static string FindRepositoryRoot()
+ {
+ DirectoryInfo directory = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory);
+ while (directory != null)
+ {
+ if (File.Exists(Path.Combine(directory.FullName, "Cargo.toml"))
+ && Directory.Exists(Path.Combine(directory.FullName, ".git"))) return directory.FullName;
+ directory = directory.Parent;
+ }
+ directory = new DirectoryInfo(Environment.CurrentDirectory);
+ while (directory != null)
+ {
+ if (File.Exists(Path.Combine(directory.FullName, "Cargo.toml"))) return directory.FullName;
+ directory = directory.Parent;
+ }
+ throw new InvalidOperationException("Run from this repository or place the EXE beneath it.");
+ }
+
+ private static string HashFile(string path)
+ {
+ using (SHA256 sha = SHA256.Create())
+ using (FileStream stream = File.OpenRead(path))
+ {
+ byte[] hash = sha.ComputeHash(stream);
+ 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 string VerifyScratchFiles(string repositoryRoot)
+ {
+ string directory = Path.GetFullPath(Path.Combine(repositoryRoot, GameDirectory));
+ foreach (KeyValuePair<string, string> expected in ExpectedSha256)
+ {
+ string path = Path.Combine(directory, expected.Key);
+ if (!File.Exists(path)) throw new FileNotFoundException("Missing scratch file: " + path, path);
+ string observed = HashFile(path);
+ if (!String.Equals(observed, expected.Value, StringComparison.OrdinalIgnoreCase))
+ throw new InvalidOperationException("SHA-256 mismatch for " + path + ": " + observed);
+ }
+ return Path.Combine(directory, GameExe);
+ }
+
+ private static bool Read(IntPtr process, uint address, byte[] bytes)
+ {
+ UIntPtr count;
+ return address != 0 && ReadProcessMemory(process, Ptr(address), bytes,
+ new UIntPtr((uint)bytes.Length), out count) && count.ToUInt32() == (uint)bytes.Length;
+ }
+
+ private static uint ReadU32(IntPtr process, uint address)
+ {
+ byte[] bytes = new byte[4];
+ return Read(process, address, bytes) ? BitConverter.ToUInt32(bytes, 0) : 0;
+ }
+
+ private static bool IsFinite(float value)
+ {
+ return !Single.IsNaN(value) && !Single.IsInfinity(value);
+ }
+
+ private static bool WriteByte(IntPtr process, uint address, byte value)
+ {
+ uint oldProtection;
+ if (!VirtualProtectEx(process, Ptr(address), new UIntPtr(1), PageExecuteReadWrite, out oldProtection))
+ return false;
+ UIntPtr written;
+ bool result = WriteProcessMemory(process, Ptr(address), new byte[] { value }, new UIntPtr(1), out written)
+ && written.ToUInt32() == 1;
+ uint ignored;
+ VirtualProtectEx(process, Ptr(address), new UIntPtr(1), oldProtection, out ignored);
+ FlushInstructionCache(process, Ptr(address), new UIntPtr(1));
+ return result;
+ }
+
+ private static string PathForHandle(IntPtr file)
+ {
+ if (file == IntPtr.Zero) return "";
+ StringBuilder buffer = new StringBuilder(1024);
+ uint length = GetFinalPathNameByHandleW(file, buffer, (uint)buffer.Capacity, 0);
+ return length == 0 || length >= buffer.Capacity ? "" : NormalizePath(buffer.ToString());
+ }
+
+ private static string ModulePath(IntPtr process, uint moduleBase)
+ {
+ StringBuilder buffer = new StringBuilder(1024);
+ uint length = GetModuleFileNameExW(process, Ptr(moduleBase), buffer, (uint)buffer.Capacity);
+ return length == 0 || length >= buffer.Capacity ? "" : NormalizePath(buffer.ToString());
+ }
+
+ private static bool VerifyRenderEntry(IntPtr process, uint moduleBase, out byte firstByte, out string evidence)
+ {
+ firstByte = 0;
+ evidence = "";
+ byte[] code = new byte[15];
+ uint address = unchecked(moduleBase + RenderGameRva);
+ if (!Read(process, address, code))
+ {
+ evidence = "World3D+0x13BD0 unreadable";
+ return false;
+ }
+ uint relocatedOperand = BitConverter.ToUInt32(code, 5);
+ bool match = code[0] == 0x83 && code[1] == 0xEC && code[2] == 0x64
+ && code[3] == 0xC7 && code[4] == 0x05
+ && relocatedOperand == unchecked(moduleBase + RelocatedGlobalRva)
+ && code[9] == 0 && code[10] == 0 && code[11] == 0 && code[12] == 0
+ && code[13] == 0x53 && code[14] == 0x56;
+ evidence = "base=0x" + moduleBase.ToString("X8") + " RVA=0x13BD0 bytes="
+ + BitConverter.ToString(code) + " relocOperand=0x" + relocatedOperand.ToString("X8")
+ + " expectedOperand=0x" + unchecked(moduleBase + RelocatedGlobalRva).ToString("X8");
+ firstByte = code[0];
+ return match;
+ }
+
+ private static bool VerifyRenderReturnBoundary(IntPtr process, uint moduleBase,
+ out byte firstByte, out string evidence)
+ {
+ firstByte = 0;
+ byte[] expected = new byte[] { 0x5F, 0x5E, 0x5B, 0x83, 0xC4, 0x64, 0xC2, 0x04, 0x00 };
+ byte[] actual = new byte[expected.Length];
+ uint address = unchecked(moduleBase + RenderReturnRva);
+ if (!ReadExact(process, address, actual)) { evidence = "return-epilogue bytes unreadable"; return false; }
+ if (!ByteArraysEqual(actual, expected))
+ {
+ evidence = "return-epilogue mismatch at 0x" + address.ToString("X8")
+ + " expected=" + BitConverter.ToString(expected) + " actual=" + BitConverter.ToString(actual);
+ return false;
+ }
+ firstByte = actual[0];
+ evidence = "verified one-byte pop-edi at World3D+0x13D7B followed by pop esi/pop ebx/add esp,64h/ret 4";
+ return true;
+ }
+
+ private static bool VerifyD3d7GetRenderTarget(IntPtr process, uint ngiBase,
+ out uint device, out uint getRenderTarget, out string evidence)
+ {
+ device = ReadU32(process, unchecked(ngiBase + 0x3A488));
+ getRenderTarget = 0;
+ if (device == 0)
+ {
+ evidence = "Ngi32 D3D7 device global is null";
+ return false;
+ }
+ uint vtable = ReadU32(process, device);
+ if (vtable == 0 || !ReadExact(process, unchecked(vtable + 0x24), new byte[4]))
+ {
+ evidence = "D3D7 device vtable or GetRenderTarget slot is unreadable";
+ return false;
+ }
+ getRenderTarget = ReadU32(process, unchecked(vtable + 0x24));
+ if (getRenderTarget == 0)
+ {
+ evidence = "D3D7 GetRenderTarget slot is null";
+ return false;
+ }
+ MemoryBasicInformation memory;
+ UIntPtr queried = VirtualQueryEx(process, Ptr(getRenderTarget), out memory,
+ new UIntPtr((uint)Marshal.SizeOf(typeof(MemoryBasicInformation))));
+ uint protection = memory.Protect & 0xFF;
+ bool executable = memory.State == 0x1000
+ && (protection == 0x10 || protection == 0x20 || protection == 0x40 || protection == 0x80);
+ evidence = "device=0x" + device.ToString("X8") + " vtable=0x" + vtable.ToString("X8")
+ + " GetRenderTarget=0x" + getRenderTarget.ToString("X8") + " pageState=0x"
+ + memory.State.ToString("X8") + " protect=0x" + memory.Protect.ToString("X8");
+ return queried.ToUInt32() != 0 && executable;
+ }
+
+ private static string JsonString(string value)
+ {
+ StringBuilder result = new StringBuilder();
+ result.Append('"');
+ for (int i = 0; i < value.Length; i++)
+ {
+ char c = value[i];
+ if (c == '"') result.Append("\\\"");
+ else if (c == '\\') result.Append("\\\\");
+ else if (c == '\n') result.Append("\\n");
+ else if (c == '\r') result.Append("\\r");
+ else if (c == '\t') result.Append("\\t");
+ else if (c < 0x20) result.Append("\\u").Append(((int)c).ToString("X4"));
+ else result.Append(c);
+ }
+ result.Append('"');
+ return result.ToString();
+ }
+
+ private static bool VerifyProjectionBoundary(IntPtr process, uint world3dBase, out byte firstByte, out string evidence)
+ {
+ const uint projectionGlobalRva = 0x795170;
+ firstByte = 0;
+ byte[] code = new byte[11];
+ if (!Read(process, unchecked(world3dBase + ProjectionSnapshotRva), code))
+ {
+ evidence = "projection boundary unreadable";
+ return false;
+ }
+ uint relocatedGlobal = BitConverter.ToUInt32(code, 2);
+ bool match = code[0] == 0x8B && code[1] == 0x0D
+ && relocatedGlobal == unchecked(world3dBase + projectionGlobalRva)
+ && code[6] == 0x8B && code[7] == 0x11
+ && code[8] == 0xFF && code[9] == 0x52 && code[10] == 0x34;
+ firstByte = code[0];
+ evidence = "base=0x" + world3dBase.ToString("X8") + " RVA=0x13CE4 bytes="
+ + BitConverter.ToString(code) + " global=0x" + relocatedGlobal.ToString("X8");
+ return match;
+ }
+
+ private static bool VerifyAtmospherePhaseBoundary(IntPtr process, uint terrainBase,
+ out byte firstByte, out string evidence)
+ {
+ firstByte = 0;
+ byte[] code = new byte[15];
+ uint address = unchecked(terrainBase + AtmospherePhaseRva - 12);
+ if (!Read(process, address, code))
+ {
+ evidence = "Terrain+0x421DC phase boundary unreadable";
+ return false;
+ }
+ byte[] expected = new byte[] {
+ 0x8B, 0xC5, // mov eax, ebp (raw clock)
+ 0x2B, 0xC1, // sub eax, ecx (origin from [esi+0x144])
+ 0x33, 0xD2, // xor edx, edx
+ 0xF7, 0xB6, 0x50, 0x01, 0x00, 0x00, // div dword ptr [esi+0x150] (phase remainder in edx)
+ 0x57, // push edi (breakpoint before phase is consumed)
+ 0x8B, 0xFA // mov edi, edx
+ };
+ bool match = ByteArraysEqual(code, expected);
+ firstByte = code[12];
+ evidence = "base=0x" + terrainBase.ToString("X8") + " RVA=0x421DC bytes="
+ + BitConverter.ToString(code) + " rawClock=EBP origin=[ESI+0x144]"
+ + " period=[ESI+0x150] remainder=EDX";
+ return match && firstByte == 0x57;
+ }
+
+ private static bool VerifyMissionIdentityBoundary(IntPtr process, uint iron3dBase,
+ out byte firstByte, out string evidence)
+ {
+ firstByte = 0;
+ byte[] code = new byte[18];
+ if (!Read(process, unchecked(iron3dBase + MissionFactoryRva), code))
+ {
+ evidence = "iron3d mission factory/call boundary unreadable";
+ return false;
+ }
+ firstByte = code[15];
+ int displacement = BitConverter.ToInt32(code, 1);
+ uint factoryTarget = unchecked(iron3dBase + MissionFactoryRva + 5 + (uint)displacement);
+ bool match = code[0] == 0xE8
+ && factoryTarget == unchecked(iron3dBase + MissionImportThunkRva)
+ && code[5] == 0x8B && code[6] == 0x54 && code[7] == 0x24 && code[8] == 0x40
+ && code[9] == 0x8B && code[10] == 0xD8
+ && code[11] == 0x8B && code[12] == 0x0B
+ && code[13] == 0x52 && code[14] == 0x53
+ && code[15] == 0xFF && code[16] == 0x51 && code[17] == 0x08;
+ evidence = "base=0x" + iron3dBase.ToString("X8") + " factoryRva=0xA1FE1 bytes="
+ + BitConverter.ToString(code) + " factoryTarget=0x" + factoryTarget.ToString("X8")
+ + " vtableCallRva=0xA1FF0";
+ return match;
+ }
+
+ private static bool IsReadableProtection(uint protection)
+ {
+ uint basic = protection & 0xFF;
+ return basic != PageNoAccess && (protection & PageGuard) == 0
+ && (basic == PageReadOnly || basic == PageReadWrite || basic == PageWriteCopy
+ || basic == PageExecuteRead || basic == PageExecuteReadWrite || basic == PageExecuteWriteCopy);
+ }
+
+ private static bool TryReadReadableRegion(IntPtr process, uint address, int maxBytes,
+ out byte[] bytes, out string evidence)
+ {
+ bytes = null;
+ evidence = "unreadable";
+ if (address < 0x10000 || maxBytes <= 0) { evidence = "invalid address or length"; return false; }
+ MemoryBasicInformation memory;
+ UIntPtr queried = VirtualQueryEx(process, Ptr(address), out memory,
+ new UIntPtr((uint)Marshal.SizeOf(typeof(MemoryBasicInformation))));
+ uint baseAddress = unchecked((uint)memory.BaseAddress.ToInt32());
+ uint regionSize = memory.RegionSize.ToUInt32();
+ ulong regionEnd = (ulong)baseAddress + regionSize;
+ uint protection = memory.Protect;
+ if (queried.ToUInt32() == 0 || memory.State != MemCommit || !IsReadableProtection(protection)
+ || address < baseAddress || (ulong)address >= regionEnd)
+ {
+ evidence = "VirtualQueryEx rejected addr=0x" + address.ToString("X8")
+ + " state=0x" + memory.State.ToString("X8") + " protect=0x" + protection.ToString("X8");
+ return false;
+ }
+ int count = (int)Math.Min((ulong)maxBytes, regionEnd - address);
+ if (count <= 0) { evidence = "empty readable region"; return false; }
+ byte[] result = new byte[count];
+ if (!Read(process, address, result))
+ {
+ evidence = "ReadProcessMemory failed for addr=0x" + address.ToString("X8") + " bytes=" + count;
+ return false;
+ }
+ bytes = result;
+ evidence = "addr=0x" + address.ToString("X8") + " bytes=" + count
+ + " state=0x" + memory.State.ToString("X8") + " protect=0x" + protection.ToString("X8");
+ return true;
+ }
+
+ private static string DecodeAsciiMissionCandidate(byte[] bytes, int start, int maxLength)
+ {
+ if (bytes == null || start < 0 || start >= bytes.Length) return null;
+ int end = Math.Min(bytes.Length, start + maxLength);
+ StringBuilder text = new StringBuilder();
+ for (int i = start; i < end; i++)
+ {
+ byte value = bytes[i];
+ if (value == 0) break;
+ if (value < 0x20 || value > 0x7E) return null;
+ text.Append((char)value);
+ }
+ string candidate = text.ToString().Trim();
+ return IsMissionPathCandidate(candidate) ? candidate : null;
+ }
+
+ private static bool IsMissionPathCandidate(string candidate)
+ {
+ if (String.IsNullOrEmpty(candidate) || candidate.Length > 512) return false;
+ string lower = candidate.ToLowerInvariant().Replace('/', '\\');
+ return lower.Contains("missions\\") && (lower.Contains(".tma") || lower.Contains(".mis")
+ || lower.Contains("autodemo") || lower.Contains("\\data"));
+ }
+
+ private static string TryReadMissionPathArgument(IntPtr process, uint argument, out string evidence)
+ {
+ evidence = "argument pointer is not a verified mission path";
+ if (argument < 0x10000) return null;
+
+ byte[] objectBytes;
+ string objectEvidence;
+ if (!TryReadReadableRegion(process, argument, 64, out objectBytes, out objectEvidence))
+ {
+ evidence = "argument=" + objectEvidence;
+ return null;
+ }
+ for (int i = 0; i < objectBytes.Length; i++)
+ {
+ string inline = DecodeAsciiMissionCandidate(objectBytes, i, 512);
+ if (inline != null)
+ {
+ evidence = "mission path found inline in call argument (" + objectEvidence + ")";
+ return inline;
+ }
+ }
+
+ // The callsite passes a string object by pointer. Try the observed word
+ // fields as readable C-string pointers without assuming a string ABI.
+ int pointerWords = Math.Min(8, objectBytes.Length / 4);
+ for (int i = 0; i < pointerWords; i++)
+ {
+ uint pointer = BitConverter.ToUInt32(objectBytes, i * 4);
+ byte[] pointed;
+ string pointedEvidence;
+ if (!TryReadReadableRegion(process, pointer, 512, out pointed, out pointedEvidence)) continue;
+ string candidate = DecodeAsciiMissionCandidate(pointed, 0, 512);
+ if (candidate == null) continue;
+ evidence = "mission path found through argument word +0x" + (i * 4).ToString("X2")
+ + " (" + pointedEvidence + ")";
+ return candidate;
+ }
+
+ evidence = "argument object=" + objectEvidence + " raw64=" + BitConverter.ToString(objectBytes);
+ return null;
+ }
+
+ private static bool CaptureMissionIdentityAtCall(IntPtr process, uint iron3dBase,
+ byte[] context, uint threadId, out string path, out string evidence)
+ {
+ path = null;
+ evidence = "mission call arguments were not verified";
+ if (context == null || context.Length != X86ContextSize
+ || BitConverter.ToUInt32(context, ContextEipOffset) != unchecked(iron3dBase + MissionVtableCallRva + 1))
+ {
+ evidence = "iron3d mission vtable-call breakpoint context mismatch";
+ return false;
+ }
+ uint ebx = BitConverter.ToUInt32(context, ContextEbxOffset);
+ uint edx = BitConverter.ToUInt32(context, ContextEdxOffset);
+ uint ecx = BitConverter.ToUInt32(context, ContextEcxOffset);
+ uint esp = BitConverter.ToUInt32(context, ContextEspOffset);
+ byte[] args = new byte[8];
+ if (ebx == 0 || edx == 0 || ecx == 0 || !Read(process, esp, args))
+ {
+ evidence = "mission vtable-call registers or stack arguments unreadable";
+ return false;
+ }
+ uint stackThis = BitConverter.ToUInt32(args, 0);
+ uint stackString = BitConverter.ToUInt32(args, 4);
+ uint vtable = ReadU32(process, ebx);
+ uint method = ReadU32(process, unchecked(vtable + 8));
+ MemoryBasicInformation methodMemory;
+ UIntPtr methodQuery = VirtualQueryEx(process, Ptr(method), out methodMemory,
+ new UIntPtr((uint)Marshal.SizeOf(typeof(MemoryBasicInformation))));
+ uint methodProtection = methodMemory.Protect & 0xFF;
+ bool executableMethod = methodQuery.ToUInt32() != 0 && methodMemory.State == MemCommit
+ && (methodProtection == 0x10 || methodProtection == 0x20
+ || methodProtection == 0x40 || methodProtection == 0x80);
+ Log("MISSION_VTABLE_CALL tid=" + threadId + " ebx=0x" + ebx.ToString("X8")
+ + " edx=0x" + edx.ToString("X8") + " ecx=0x" + ecx.ToString("X8")
+ + " vtable=0x" + vtable.ToString("X8") + " slot8=0x" + method.ToString("X8")
+ + " executableSlot=" + executableMethod + " stackThis=0x" + stackThis.ToString("X8")
+ + " stackArg1=0x" + stackString.ToString("X8"));
+ if (ecx != vtable || stackThis != ebx || stackString != edx || !executableMethod)
+ {
+ evidence = "mission vtable-call registers, stack arguments, or vtable slot failed verification";
+ return false;
+ }
+ path = TryReadMissionPathArgument(process, edx, out evidence);
+ if (path == null) evidence = "mission vtable argument path unresolved: " + evidence;
+ return path != null;
+ }
+
+ private static bool ComputeAtmospherePhaseMilliseconds(uint rawClock, uint origin,
+ uint periodMilliseconds, out uint phaseMilliseconds)
+ {
+ phaseMilliseconds = 0;
+ if (periodMilliseconds == 0) return false;
+ uint delta = unchecked(rawClock - origin);
+ phaseMilliseconds = delta % periodMilliseconds;
+ return true;
+ }
+
+ private static AtmosphereReadback CaptureAtmospherePhase(IntPtr process, uint terrainBase,
+ uint world3dBase, byte[] context, uint threadId, string terrainModuleSha256,
+ FrameSnapshot frame)
+ {
+ if (context == null || context.Length != X86ContextSize
+ || BitConverter.ToUInt32(context, ContextEipOffset) != unchecked(terrainBase + AtmospherePhaseRva + 1))
+ throw new InvalidOperationException("Terrain atmosphere-phase breakpoint context mismatch.");
+
+ uint atmosphereObject = BitConverter.ToUInt32(context, ContextEsiOffset);
+ uint rawClock = BitConverter.ToUInt32(context, ContextEbpOffset);
+ uint phaseMilliseconds = BitConverter.ToUInt32(context, ContextEdxOffset);
+ if (atmosphereObject < 0x10000 || world3dBase == 0)
+ throw new InvalidOperationException("Terrain atmosphere object or World3D game-time source was unavailable.");
+
+ uint origin = ReadU32Exact(process, unchecked(atmosphereObject + 0x144));
+ uint periodMilliseconds = ReadU32Exact(process, unchecked(atmosphereObject + 0x150));
+ uint worldGameTimeWord = ReadU32Exact(process, unchecked(world3dBase + 0x32A38));
+ uint recomputed;
+ bool arithmeticVerified = ComputeAtmospherePhaseMilliseconds(rawClock, origin,
+ periodMilliseconds, out recomputed) && recomputed == phaseMilliseconds;
+ bool worldTimeMatchesRawClock = rawClock == worldGameTimeWord;
+ return new AtmosphereReadback
+ {
+ SampleThreadId = threadId,
+ CameraGenerationAtSample = frame == null ? 0 : frame.CameraGeneration,
+ CameraThreadIdAtSample = frame == null ? 0 : frame.CameraThreadId,
+ AtmosphereObject = atmosphereObject,
+ RawClock = rawClock,
+ PhaseMilliseconds = phaseMilliseconds,
+ Origin = origin,
+ PeriodMilliseconds = periodMilliseconds,
+ RecomputedPhaseMilliseconds = recomputed,
+ WorldGameTimeWord = worldGameTimeWord,
+ WorldGameTimeWordReadable = true,
+ TerrainModuleSha256 = terrainModuleSha256,
+ TerrainModuleHashVerified = String.Equals(terrainModuleSha256,
+ ExpectedSha256[TerrainName], StringComparison.OrdinalIgnoreCase),
+ ArithmeticVerified = arithmeticVerified,
+ WorldTimeMatchesRawClock = worldTimeMatchesRawClock
+ };
+ }
+
+ private static bool IsVerifiedAtmosphereForFrame(FrameSnapshot frame, uint projectionThreadId)
+ {
+ if (frame == null || frame.Atmosphere == null) return false;
+ AtmosphereReadback atmosphere = frame.Atmosphere;
+ return atmosphere.TerrainModuleHashVerified
+ && atmosphere.PeriodMilliseconds != 0
+ && atmosphere.ArithmeticVerified
+ && atmosphere.WorldGameTimeWordReadable
+ && atmosphere.WorldTimeMatchesRawClock
+ && atmosphere.RawClock == frame.WorldGameTimeWord
+ && atmosphere.WorldGameTimeWord == frame.WorldGameTimeWord
+ && frame.CameraWorldGameTimeWordReadable
+ && frame.WorldGameTimeWordReadable
+ && frame.CameraWorldGameTimeWord == frame.WorldGameTimeWord
+ && atmosphere.SampleThreadId == frame.CameraThreadId
+ && atmosphere.SampleThreadId == frame.ProjectionThreadId
+ && frame.ProjectionThreadId == projectionThreadId
+ && frame.CameraGeneration != 0
+ && frame.CameraGeneration == frame.ProjectionGeneration
+ && frame.CameraThreadId == projectionThreadId
+ && frame.ProjectionGeneration == atmosphere.MatchedRenderGeneration
+ && atmosphere.MatchedRenderThreadId == projectionThreadId
+ && frame.Camera != null && frame.Camera.CurrentAtProjectionVerified
+ && frame.Projection != null && frame.Projection.Verified;
+ }
+
+ private static void PairAtmosphereToProjection(FrameSnapshot frame, uint projectionThreadId)
+ {
+ frame.Atmosphere = null;
+ AtmosphereReadback candidate;
+ if (frame.AtmosphereByThread == null
+ || !frame.AtmosphereByThread.TryGetValue(projectionThreadId, out candidate))
+ {
+ Log("ATMOSPHERE_FRAME_PAIR missing_sample tid=" + projectionThreadId
+ + " generation=" + frame.CameraGeneration);
+ return;
+ }
+ candidate.CandidateRenderGeneration = frame.CameraGeneration;
+ candidate.CandidateRenderThreadId = projectionThreadId;
+ candidate.MatchedRenderGeneration = frame.CameraGeneration;
+ candidate.MatchedRenderThreadId = projectionThreadId;
+ frame.Atmosphere = candidate;
+ candidate.SameGenerationVerified = IsVerifiedAtmosphereForFrame(frame, projectionThreadId);
+ if (!candidate.SameGenerationVerified)
+ {
+ candidate.MatchedRenderGeneration = 0;
+ candidate.MatchedRenderThreadId = 0;
+ }
+ Log("ATMOSPHERE_FRAME_PAIR generation=" + frame.CameraGeneration + " tid=" + projectionThreadId
+ + " sampleTid=" + candidate.SampleThreadId + " esi=0x" + candidate.AtmosphereObject.ToString("X8")
+ + " rawClock=0x" + candidate.RawClock.ToString("X8") + " phaseMs=" + candidate.PhaseMilliseconds
+ + " origin=" + candidate.Origin + " periodMs=" + candidate.PeriodMilliseconds
+ + " recomputedPhaseMs=" + candidate.RecomputedPhaseMilliseconds
+ + " worldGameTime=0x" + candidate.WorldGameTimeWord.ToString("X8")
+ + " arithmeticVerified=" + candidate.ArithmeticVerified
+ + " rawMatchesWorldTime=" + candidate.WorldTimeMatchesRawClock
+ + " entryWorldTime=0x" + frame.CameraWorldGameTimeWord.ToString("X8")
+ + " projectionWorldTime=0x" + frame.WorldGameTimeWord.ToString("X8")
+ + " sameGenerationVerified=" + candidate.SameGenerationVerified);
+ }
+
+ private static bool VerifyPresentBoundary(IntPtr process, uint ngiBase, out byte firstByte, out string evidence)
+ {
+ firstByte = 0;
+ byte[] code = new byte[3];
+ if (!Read(process, unchecked(ngiBase + PresentBoundaryRva), code))
+ {
+ evidence = "Ngi32 present boundary unreadable";
+ return false;
+ }
+ firstByte = code[0];
+ bool match = code[0] == 0xFF && code[1] == 0x50 && code[2] == 0x14;
+ evidence = "base=0x" + ngiBase.ToString("X8") + " RVA=0x6E1B bytes=" + BitConverter.ToString(code);
+ return match;
+ }
+
+ private static byte[] ContextAtBreakpoint(byte[] original, uint address, bool singleStep)
+ {
+ if (original == null || original.Length != X86ContextSize)
+ throw new ArgumentException("Expected a full x86 thread context.", "original");
+ byte[] result = (byte[])original.Clone();
+ Buffer.BlockCopy(BitConverter.GetBytes(unchecked((int)address)), 0, result, ContextEipOffset, 4);
+ int flags = BitConverter.ToInt32(result, ContextEflagsOffset);
+ flags = singleStep ? (flags | 0x100) : (flags & ~0x100);
+ Buffer.BlockCopy(BitConverter.GetBytes(flags), 0, result, ContextEflagsOffset, 4);
+ return result;
+ }
+
+ private static void BeginSingleStep(IntPtr process, uint threadId, BreakpointInfo breakpoint,
+ byte[] stoppedContext, out PendingStep pending, bool rearmOnComplete)
+ {
+ pending = null;
+ if (!breakpoint.Armed) throw new InvalidOperationException(breakpoint.Name + " was not armed at its hit.");
+ if (!WriteByte(process, breakpoint.Address, breakpoint.OriginalByte))
+ throw new InvalidOperationException("Could not restore " + breakpoint.Name + " before single-step.");
+ breakpoint.Armed = false;
+ IntPtr thread = OpenThread(ThreadGetContext | ThreadSetContext, false, threadId);
+ if (thread == IntPtr.Zero) throw new InvalidOperationException("OpenThread for " + breakpoint.Name + " failed: " + Marshal.GetLastWin32Error());
+ try
+ {
+ SetFullX86Context(thread, ContextAtBreakpoint(stoppedContext, breakpoint.Address, true));
+ }
+ finally { CloseHandle(thread); }
+ pending = new PendingStep { Breakpoint = breakpoint, ThreadId = threadId,
+ DeadlineUtc = DateTime.UtcNow.AddSeconds(5), RearmOnComplete = rearmOnComplete };
+ }
+
+ private static void FinishSingleStep(IntPtr process, uint threadId, PendingStep pending)
+ {
+ if (pending == null || pending.ThreadId != threadId)
+ throw new InvalidOperationException("Unexpected single-step thread.");
+ BreakpointInfo breakpoint = pending.Breakpoint;
+ if (pending.RearmOnComplete && !WriteByte(process, breakpoint.Address, 0xCC))
+ throw new InvalidOperationException("Could not re-arm " + breakpoint.Name + " after single-step.");
+ IntPtr thread = OpenThread(ThreadGetContext | ThreadSetContext, false, threadId);
+ if (thread == IntPtr.Zero) throw new InvalidOperationException("OpenThread after single-step failed: " + Marshal.GetLastWin32Error());
+ try
+ {
+ byte[] context = GetFullX86Context(thread);
+ int flags = BitConverter.ToInt32(context, ContextEflagsOffset) & ~0x100;
+ Buffer.BlockCopy(BitConverter.GetBytes(flags), 0, context, ContextEflagsOffset, 4);
+ SetFullX86Context(thread, context);
+ }
+ finally { CloseHandle(thread); }
+ breakpoint.Armed = pending.RearmOnComplete;
+ }
+
+ private static void ResumeOriginalPresent(IntPtr process, RemoteReadbackSession session)
+ {
+ BreakpointInfo breakpoint = session.PresentBreakpoint;
+ if (breakpoint.Armed)
+ {
+ if (!WriteByte(process, breakpoint.Address, breakpoint.OriginalByte))
+ throw new InvalidOperationException("Could not restore Ngi32 present call after readback.");
+ breakpoint.Armed = false;
+ }
+ IntPtr thread = OpenThread(ThreadGetContext | ThreadSetContext, false, session.ThreadId);
+ if (thread == IntPtr.Zero) throw new InvalidOperationException("OpenThread to resume Ngi32 present failed: " + Marshal.GetLastWin32Error());
+ try
+ {
+ byte[] resumeContext = ContextAtBreakpoint(session.OriginalContext, breakpoint.Address, false);
+ SetFullX86Context(thread, resumeContext);
+ byte[] restoredContext = GetFullX86Context(thread);
+ uint expectedEsp = BitConverter.ToUInt32(session.OriginalContext, ContextEspOffset);
+ uint restoredEip = BitConverter.ToUInt32(restoredContext, ContextEipOffset);
+ uint restoredEsp = BitConverter.ToUInt32(restoredContext, ContextEspOffset);
+ Log("PRESENT_RESUME_CONTEXT expectedEip=0x" + breakpoint.Address.ToString("X8")
+ + " actualEip=0x" + restoredEip.ToString("X8")
+ + " expectedEsp=0x" + expectedEsp.ToString("X8")
+ + " actualEsp=0x" + restoredEsp.ToString("X8"));
+ if (restoredEip != breakpoint.Address || restoredEsp != expectedEsp)
+ throw new InvalidOperationException("Could not verify restored Ngi32 present EIP/ESP.");
+ }
+ finally { CloseHandle(thread); }
+ }
+
+ private static bool CanStopCapture(bool captured, bool stopAfterAttempt,
+ bool remoteInFlight, bool singleStepInFlight, bool cameraRestorePending)
+ {
+ return (captured || stopAfterAttempt) && !remoteInFlight && !singleStepInFlight
+ && !cameraRestorePending;
+ }
+
+ private static bool IsMatchingPerspectiveFrame(FrameSnapshot frame, uint threadId)
+ {
+ return CanArmPresentForFrame(frame, threadId)
+ && (!frame.SelectedCameraRequested || (frame.SelectedCamera != null
+ && frame.SelectedCamera.Applied && frame.SelectedCamera.ProjectionMatchesInput
+ && frame.SelectedCamera.CandidateProjectionVerified
+ && frame.SelectedCamera.Restored && frame.SelectedCamera.RestoreVerified
+ && frame.SelectedCamera.MatrixIntactAtReturn
+ && !frame.SelectedCamera.PixelAttributionInvalidated
+ && SelectedMatrixMatchesProjection(frame)));
+ }
+
+ private static bool SelectedMatrixMatchesProjection(FrameSnapshot frame)
+ {
+ if (frame == null || !frame.SelectedCameraRequested) return true;
+ return frame.SelectedCamera != null && frame.SelectedCamera.Input != null
+ && frame.Camera != null && frame.Camera.CurrentAtProjectionVerified
+ && !frame.Camera.WordsChangedAtProjection
+ && ByteArraysEqual(frame.Camera.ProjectionMatrixBytes, frame.SelectedCamera.Input.MatrixBytes);
+ }
+
+ private static bool CanArmPresentForFrame(FrameSnapshot frame, uint threadId)
+ {
+ return frame != null && frame.Camera != null && frame.Camera.LayoutVerified && frame.Camera.MatrixFinite
+ && frame.Camera.CurrentAtProjectionVerified
+ && frame.Projection != null && frame.Projection.Verified && frame.Projection.Mode != 0
+ && frame.CameraGeneration != 0 && frame.ProjectionGeneration == frame.CameraGeneration
+ && frame.CameraThreadId == threadId && frame.ProjectionThreadId == threadId
+ && (!frame.SelectedCameraRequested || (frame.SelectedCamera != null
+ && frame.SelectedCamera.Applied && frame.SelectedCamera.ProjectionMatchesInput
+ && frame.SelectedCamera.CandidateProjectionVerified
+ && !frame.SelectedCamera.PixelAttributionInvalidated
+ && SelectedMatrixMatchesProjection(frame)));
+ }
+
+ private static bool IsViewportInsideSurface(int[] viewport, uint width, uint height)
+ {
+ return viewport != null && viewport.Length == 4 && width > 0 && height > 0
+ && viewport[0] >= 0 && viewport[1] >= 0
+ && viewport[2] > viewport[0] && viewport[3] > viewport[1]
+ && (uint)viewport[2] <= width && (uint)viewport[3] <= height;
+ }
+
+ private static string UsableFrameJson(string gamePath, uint world3dBase, uint ngiBase,
+ FrameSnapshot frame, SurfaceReadback surface, string pngPath, RemoteReadbackSession remote)
+ {
+ if (frame == null || frame.Camera == null || frame.Projection == null || surface == null)
+ throw new InvalidOperationException("A usable camera, projection, and pixel surface are required for the legacy input JSON.");
+ if (!IsMatchingPerspectiveFrame(frame, frame.ProjectionThreadId))
+ throw new InvalidOperationException("Refusing to emit app-compatible JSON before camera restoration and pixel attribution are verified.");
+ CameraReadback camera = frame.Camera;
+ ProjectionReadback projection = frame.Projection;
+ if (!camera.LayoutVerified || !camera.MatrixFinite || !camera.CurrentAtProjectionVerified
+ || !projection.Verified || projection.Mode == 0)
+ throw new InvalidOperationException("Refusing to emit app-compatible JSON from an unverified camera or non-perspective projection.");
+ if (!IsViewportInsideSurface(projection.Viewport, surface.Width, surface.Height))
+ throw new InvalidOperationException("Refusing to emit app-compatible JSON because the captured viewport lies outside the pixel surface.");
+ StringBuilder json = new StringBuilder();
+ json.AppendLine("{");
+ json.AppendLine(" \"schema\": \"fparkan-legacy-camera-v1\",");
+ json.AppendLine(" \"capture_status\": \"native-frame-captured\",");
+ json.AppendLine(" \"render_input_usable\": true,");
+ json.AppendLine(" \"source\": \"GOG World3D stdRenderGame + Ngi32 D3D7 render target\",");
+ json.AppendLine(" \"scratch_executable\": " + JsonString(gamePath) + ",");
+ json.AppendLine(" \"world3d_module_base\": " + JsonString("0x" + world3dBase.ToString("X8")) + ",");
+ json.AppendLine(" \"ngi32_module_base\": " + JsonString("0x" + ngiBase.ToString("X8")) + ",");
+ json.AppendLine(" \"frame_generation\": " + frame.CameraGeneration.ToString(CultureInfo.InvariantCulture) + ",");
+ json.AppendLine(" \"camera_argument\": " + JsonString("0x" + camera.Camera.ToString("X8")) + ",");
+ json.AppendLine(" \"camera_vtable\": " + JsonString("0x" + camera.Vtable.ToString("X8")) + ",");
+ json.AppendLine(" \"selector_field_plus_0x10\": " + JsonString("0x" + camera.SelectorField.ToString("X8")) + ",");
+ json.AppendLine(" \"camera_words_sampled_at\": \"World3D+0x13CE4\",");
+ json.AppendLine(" \"camera_words_changed_since_render_entry\": " + (camera.WordsChangedAtProjection ? "true" : "false") + ",");
+ json.AppendLine(" \"camera_words_render_entry_sha256\": " + JsonString(camera.EntryMatrixSha256) + ",");
+ json.AppendLine(" \"camera_words_projection_boundary_sha256\": " + JsonString(camera.ProjectionMatrixSha256) + ",");
+ json.Append(" \"selector0_words\": [");
+ for (int i = 0; i < camera.Words.Length; i++)
+ {
+ if (i != 0) json.Append(", ");
+ json.Append(camera.Words[i].ToString(CultureInfo.InvariantCulture));
+ }
+ json.AppendLine("],");
+ json.AppendLine(" \"viewport\": [" + String.Join(", ", projection.Viewport) + "],");
+ json.AppendLine(" \"near_plane\": " + projection.Near.ToString("R", CultureInfo.InvariantCulture) + ",");
+ json.AppendLine(" \"far_plane\": " + projection.Far.ToString("R", CultureInfo.InvariantCulture) + ",");
+ json.AppendLine(" \"field_of_view_radians\": " + projection.Fov.ToString("R", CultureInfo.InvariantCulture) + ",");
+ json.AppendLine(" \"projection_mode_byte\": " + projection.Mode.ToString(CultureInfo.InvariantCulture) + ",");
+ if (frame.SelectedCamera == null) json.AppendLine(" \"selected_camera\": null,");
+ else
+ {
+ SelectedCameraState selected = frame.SelectedCamera;
+ json.AppendLine(" \"selected_camera\": {");
+ json.AppendLine(" \"input_json\": " + JsonString(selected.Input.Path) + ",");
+ json.AppendLine(" \"requested_matrix_sha256\": " + JsonString(selected.Input.MatrixSha256) + ",");
+ json.AppendLine(" \"preflight_camera_argument\": " + JsonString("0x" + selected.CandidateCameraPointer.ToString("X8")) + ",");
+ json.AppendLine(" \"preflight_thread_id\": " + selected.CandidateThreadId.ToString(CultureInfo.InvariantCulture) + ",");
+ json.AppendLine(" \"preflight_render_generation\": " + selected.CandidateGeneration.ToString(CultureInfo.InvariantCulture) + ",");
+ json.AppendLine(" \"preflight_projection_verified\": " + (selected.CandidateProjectionVerified ? "true" : "false") + ",");
+ json.AppendLine(" \"original_matrix_sha256\": " + JsonString(selected.OriginalMatrixSha256) + ",");
+ json.AppendLine(" \"camera_argument\": " + JsonString("0x" + selected.CameraPointer.ToString("X8")) + ",");
+ json.AppendLine(" \"thread_id\": " + selected.ThreadId.ToString(CultureInfo.InvariantCulture) + ",");
+ json.AppendLine(" \"frame_generation\": " + selected.Generation.ToString(CultureInfo.InvariantCulture) + ",");
+ json.AppendLine(" \"render_entry_esp\": " + JsonString("0x" + selected.EntryEsp.ToString("X8")) + ",");
+ json.AppendLine(" \"render_return_address\": " + JsonString("0x" + selected.ReturnAddress.ToString("X8")) + ",");
+ json.AppendLine(" \"render_return_boundary_rva\": \"World3D+0x13D7B\",");
+ json.AppendLine(" \"native_setter_applied\": " + (selected.Applied ? "true" : "false") + ",");
+ json.AppendLine(" \"projection_matches_input\": " + (selected.ProjectionMatchesInput ? "true" : "false") + ",");
+ json.AppendLine(" \"matrix_intact_at_return\": " + (selected.MatrixIntactAtReturn ? "true" : "false") + ",");
+ json.AppendLine(" \"native_setter_restore_completed\": " + (selected.Restored ? "true" : "false") + ",");
+ json.AppendLine(" \"restore_matrix_verified\": " + (selected.RestoreVerified ? "true" : "false") + ",");
+ json.AppendLine(" \"pixel_attribution_invalidated\": " + (selected.PixelAttributionInvalidated ? "true" : "false"));
+ json.AppendLine(" },");
+ }
+ json.AppendLine(" \"mission_path\": " + (String.IsNullOrEmpty(frame.MissionPath) ? "null" : JsonString(frame.MissionPath)) + ",");
+ json.AppendLine(" \"mission_identity\": " + (String.IsNullOrEmpty(frame.MissionPath)
+ ? "\"unknown\"" : JsonString("path-observed")) + ",");
+ json.AppendLine(" \"mission_identity_evidence\": "
+ + (String.IsNullOrEmpty(frame.MissionEvidence) ? "null" : JsonString(frame.MissionEvidence)) + ",");
+ json.AppendLine(" \"simulation_time_seconds\": null,");
+ bool atmosphereVerified = IsVerifiedAtmosphereForFrame(frame, frame.ProjectionThreadId);
+ string atmosphereSeconds = atmosphereVerified
+ ? (((double)frame.Atmosphere.PhaseMilliseconds) / 1000.0).ToString("R", CultureInfo.InvariantCulture)
+ : "null";
+ json.AppendLine(" \"atmosphere_seconds\": " + atmosphereSeconds + ",");
+ json.AppendLine(" \"atmosphere_phase\": {");
+ json.AppendLine(" \"sample_rva\": \"Terrain+0x421DC\",");
+ json.AppendLine(" \"terrain_module_sha256\": " + (frame.Atmosphere == null
+ || String.IsNullOrEmpty(frame.Atmosphere.TerrainModuleSha256) ? "null"
+ : JsonString(frame.Atmosphere.TerrainModuleSha256)) + ",");
+ json.AppendLine(" \"terrain_sha256_verified\": "
+ + (frame.Atmosphere != null && frame.Atmosphere.TerrainModuleHashVerified ? "true" : "false") + ",");
+ json.AppendLine(" \"sample_thread_id\": " + (frame.Atmosphere == null ? "null"
+ : frame.Atmosphere.SampleThreadId.ToString(CultureInfo.InvariantCulture)) + ",");
+ json.AppendLine(" \"atmosphere_object_esi\": " + (frame.Atmosphere == null ? "null"
+ : JsonString("0x" + frame.Atmosphere.AtmosphereObject.ToString("X8"))) + ",");
+ json.AppendLine(" \"raw_clock_ebp\": " + (frame.Atmosphere == null ? "null"
+ : JsonString("0x" + frame.Atmosphere.RawClock.ToString("X8"))) + ",");
+ json.AppendLine(" \"phase_ms_edx\": " + (frame.Atmosphere == null ? "null"
+ : frame.Atmosphere.PhaseMilliseconds.ToString(CultureInfo.InvariantCulture)) + ",");
+ json.AppendLine(" \"origin_u32_esi_plus_0x144\": " + (frame.Atmosphere == null ? "null"
+ : frame.Atmosphere.Origin.ToString(CultureInfo.InvariantCulture)) + ",");
+ json.AppendLine(" \"period_ms_esi_plus_0x150\": " + (frame.Atmosphere == null ? "null"
+ : frame.Atmosphere.PeriodMilliseconds.ToString(CultureInfo.InvariantCulture)) + ",");
+ json.AppendLine(" \"recomputed_phase_ms\": " + (frame.Atmosphere == null ? "null"
+ : frame.Atmosphere.RecomputedPhaseMilliseconds.ToString(CultureInfo.InvariantCulture)) + ",");
+ json.AppendLine(" \"world_game_time_word_at_sample\": " + (frame.Atmosphere == null ? "null"
+ : JsonString("0x" + frame.Atmosphere.WorldGameTimeWord.ToString("X8"))) + ",");
+ json.AppendLine(" \"camera_generation_at_sample\": " + (frame.Atmosphere == null ? "null"
+ : frame.Atmosphere.CameraGenerationAtSample.ToString(CultureInfo.InvariantCulture)) + ",");
+ json.AppendLine(" \"camera_thread_id_at_sample\": " + (frame.Atmosphere == null ? "null"
+ : frame.Atmosphere.CameraThreadIdAtSample.ToString(CultureInfo.InvariantCulture)) + ",");
+ json.AppendLine(" \"candidate_render_generation\": " + (frame.Atmosphere == null ? "null"
+ : frame.Atmosphere.CandidateRenderGeneration.ToString(CultureInfo.InvariantCulture)) + ",");
+ json.AppendLine(" \"matched_render_generation\": " + (atmosphereVerified
+ ? frame.Atmosphere.MatchedRenderGeneration.ToString(CultureInfo.InvariantCulture) : "null") + ",");
+ json.AppendLine(" \"matched_render_thread_id\": " + (atmosphereVerified
+ ? frame.Atmosphere.MatchedRenderThreadId.ToString(CultureInfo.InvariantCulture) : "null") + ",");
+ json.AppendLine(" \"arithmetic_verified\": "
+ + (frame.Atmosphere != null && frame.Atmosphere.ArithmeticVerified ? "true" : "false") + ",");
+ json.AppendLine(" \"raw_clock_matches_world_time\": "
+ + (frame.Atmosphere != null && frame.Atmosphere.WorldTimeMatchesRawClock ? "true" : "false") + ",");
+ json.AppendLine(" \"same_generation_verified\": " + (atmosphereVerified ? "true" : "false"));
+ json.AppendLine(" },");
+ json.AppendLine(" \"weather_state\": null,");
+ json.AppendLine(" \"rng_state\": null,");
+ json.AppendLine(" \"raw_world_game_time_word32A38\": " + JsonString("0x" + frame.WorldGameTimeWord.ToString("X8")) + ",");
+ json.AppendLine(" \"native_frame_png\": " + JsonString(Path.GetFileName(pngPath)) + ",");
+ json.AppendLine(" \"pixel_capture\": {");
+ json.AppendLine(" \"width\": " + surface.Width.ToString(CultureInfo.InvariantCulture) + ",");
+ json.AppendLine(" \"height\": " + surface.Height.ToString(CultureInfo.InvariantCulture) + ",");
+ json.AppendLine(" \"pitch_bytes\": " + surface.Pitch.ToString(CultureInfo.InvariantCulture) + ",");
+ json.AppendLine(" \"bit_count\": " + surface.BitCount.ToString(CultureInfo.InvariantCulture) + ",");
+ json.AppendLine(" \"red_mask\": " + JsonString("0x" + surface.RedMask.ToString("X8")) + ",");
+ json.AppendLine(" \"green_mask\": " + JsonString("0x" + surface.GreenMask.ToString("X8")) + ",");
+ json.AppendLine(" \"blue_mask\": " + JsonString("0x" + surface.BlueMask.ToString("X8")) + ",");
+ json.AppendLine(" \"alpha_mask\": " + JsonString("0x" + surface.AlphaMask.ToString("X8")) + ",");
+ json.AppendLine(" \"rows_sha256\": " + JsonString(surface.Sha256) + ",");
+ json.AppendLine(" \"get_render_target_hresult\": " + JsonString("0x" + remote.GetHr.ToString("X8")) + ",");
+ json.AppendLine(" \"lock_hresult\": " + JsonString("0x" + remote.LockHr.ToString("X8")) + ",");
+ json.AppendLine(" \"unlock_hresult\": " + JsonString("0x" + remote.UnlockHr.ToString("X8")) + ",");
+ json.AppendLine(" \"release_result\": " + JsonString("0x" + remote.ReleaseResult.ToString("X8")));
+ json.AppendLine(" }");
+ json.AppendLine("}");
+ return json.ToString();
+ }
+
+ private static int SelfCheck()
+ {
+ if (IntPtr.Size != 4) throw new InvalidOperationException("This helper must be compiled and run as x86.");
+ SelfCheckCameraInput();
+ SelfCheckRenderInvocationGates();
+ uint relocated = 0x10000000u + RelocatedGlobalRva;
+ if (relocated != 0x1079518Cu) throw new InvalidOperationException("relocation self-check failed");
+ bool offsetsMatch = Marshal.SizeOf(typeof(X86ContextLayout)) == X86ContextSize
+ && Marshal.OffsetOf(typeof(X86ContextLayout), "Eip").ToInt32() == ContextEipOffset
+ && Marshal.OffsetOf(typeof(X86ContextLayout), "Esi").ToInt32() == ContextEsiOffset
+ && Marshal.OffsetOf(typeof(X86ContextLayout), "Ebp").ToInt32() == ContextEbpOffset
+ && Marshal.OffsetOf(typeof(X86ContextLayout), "Esp").ToInt32() == ContextEspOffset;
+ FrameSnapshot matched = new FrameSnapshot { CameraGeneration = 7, CameraThreadId = 11,
+ Camera = new CameraReadback { LayoutVerified = true, MatrixFinite = true, CurrentAtProjectionVerified = true },
+ ProjectionGeneration = 7, ProjectionThreadId = 11,
+ Projection = new ProjectionReadback { Verified = true, Mode = 1 } };
+ byte[] selectedMatrixBytes = new byte[64];
+ selectedMatrixBytes[0] = 1;
+ byte[] changedSelectedMatrixBytes = (byte[])selectedMatrixBytes.Clone();
+ changedSelectedMatrixBytes[0] = 2;
+ SelectedCameraInput selectedInput = new SelectedCameraInput { MatrixBytes = selectedMatrixBytes };
+ FrameSnapshot selectedMatched = new FrameSnapshot
+ {
+ CameraGeneration = 8, CameraThreadId = 11,
+ Camera = new CameraReadback { LayoutVerified = true, MatrixFinite = true, CurrentAtProjectionVerified = true,
+ EntryMatrixBytes = (byte[])selectedMatrixBytes.Clone(), ProjectionMatrixBytes = (byte[])selectedMatrixBytes.Clone() },
+ ProjectionGeneration = 8, ProjectionThreadId = 11,
+ Projection = new ProjectionReadback { Verified = true, Mode = 1 },
+ SelectedCameraRequested = true,
+ SelectedCamera = new SelectedCameraState
+ {
+ Applied = true, ProjectionMatchesInput = true, CandidateProjectionVerified = true,
+ Restored = true, RestoreVerified = true, MatrixIntactAtReturn = true, Input = selectedInput
+ }
+ };
+ FrameSnapshot selectedUnrestored = new FrameSnapshot
+ {
+ CameraGeneration = 8, CameraThreadId = 11,
+ Camera = selectedMatched.Camera, ProjectionGeneration = 8, ProjectionThreadId = 11,
+ Projection = selectedMatched.Projection, SelectedCameraRequested = true,
+ SelectedCamera = new SelectedCameraState
+ {
+ Applied = true, ProjectionMatchesInput = true, CandidateProjectionVerified = true,
+ MatrixIntactAtReturn = true, Input = selectedInput
+ }
+ };
+ FrameSnapshot selectedIntervened = new FrameSnapshot
+ {
+ CameraGeneration = 8, CameraThreadId = 11,
+ Camera = selectedMatched.Camera, ProjectionGeneration = 8, ProjectionThreadId = 11,
+ Projection = selectedMatched.Projection, SelectedCameraRequested = true,
+ SelectedCamera = new SelectedCameraState
+ {
+ Applied = true, ProjectionMatchesInput = true, CandidateProjectionVerified = true,
+ Restored = true, RestoreVerified = true, MatrixIntactAtReturn = true,
+ PixelAttributionInvalidated = true, Input = selectedInput
+ }
+ };
+ FrameSnapshot selectedMatrixChanged = new FrameSnapshot
+ {
+ CameraGeneration = 8, CameraThreadId = 11,
+ Camera = new CameraReadback { LayoutVerified = true, MatrixFinite = true, CurrentAtProjectionVerified = true,
+ EntryMatrixBytes = (byte[])selectedMatrixBytes.Clone(), ProjectionMatrixBytes = changedSelectedMatrixBytes },
+ ProjectionGeneration = 8, ProjectionThreadId = 11,
+ Projection = new ProjectionReadback { Verified = true, Mode = 1 },
+ SelectedCameraRequested = true,
+ SelectedCamera = new SelectedCameraState
+ {
+ Applied = true, ProjectionMatchesInput = true, CandidateProjectionVerified = true,
+ Restored = true, RestoreVerified = true, MatrixIntactAtReturn = true, Input = selectedInput
+ }
+ };
+ byte[] pendingContext = new byte[X86ContextSize];
+ const uint pendingEsp = 0x00100100;
+ Buffer.BlockCopy(BitConverter.GetBytes(0x00100100u), 0, pendingContext, ContextEspOffset, 4);
+ Buffer.BlockCopy(BitConverter.GetBytes(0xFFFFFFFFu), 0, pendingContext, ContextEflagsOffset, 4);
+ byte[] resumedContext = ContextAtBreakpoint(pendingContext, 0x12345678u, false);
+ uint wrappedPhase;
+ bool atmosphereArithmetic = ComputeAtmospherePhaseMilliseconds(0x00000010u, 0xFFFFFFF0u,
+ 0x00000100u, out wrappedPhase) && wrappedPhase == 32u;
+ uint ignoredPhase;
+ bool zeroPeriodRejected = !ComputeAtmospherePhaseMilliseconds(10u, 0u, 0u, out ignoredPhase);
+ FrameSnapshot atmosphereMatched = new FrameSnapshot
+ {
+ CameraGeneration = 7,
+ CameraThreadId = 11,
+ Camera = new CameraReadback { LayoutVerified = true, MatrixFinite = true, CurrentAtProjectionVerified = true },
+ ProjectionGeneration = 7,
+ ProjectionThreadId = 11,
+ Projection = new ProjectionReadback { Verified = true, Mode = 1 },
+ CameraWorldGameTimeWord = 0x10,
+ CameraWorldGameTimeWordReadable = true,
+ WorldGameTimeWord = 0x10,
+ WorldGameTimeWordReadable = true,
+ Atmosphere = new AtmosphereReadback
+ {
+ SampleThreadId = 11,
+ CameraGenerationAtSample = 7,
+ CameraThreadIdAtSample = 11,
+ RawClock = 0x10,
+ PhaseMilliseconds = 32,
+ Origin = 0xFFFFFFF0,
+ PeriodMilliseconds = 0x100,
+ RecomputedPhaseMilliseconds = 32,
+ WorldGameTimeWord = 0x10,
+ WorldGameTimeWordReadable = true,
+ TerrainModuleSha256 = ExpectedSha256[TerrainName],
+ TerrainModuleHashVerified = true,
+ ArithmeticVerified = true,
+ WorldTimeMatchesRawClock = true,
+ MatchedRenderGeneration = 7,
+ MatchedRenderThreadId = 11
+ }
+ };
+ FrameSnapshot atmosphereClockMismatch = new FrameSnapshot
+ {
+ CameraGeneration = 7,
+ CameraThreadId = 11,
+ Camera = atmosphereMatched.Camera,
+ ProjectionGeneration = 7,
+ ProjectionThreadId = 11,
+ Projection = atmosphereMatched.Projection,
+ CameraWorldGameTimeWord = 0x11,
+ CameraWorldGameTimeWordReadable = true,
+ WorldGameTimeWord = 0x11,
+ WorldGameTimeWordReadable = true,
+ Atmosphere = atmosphereMatched.Atmosphere
+ };
+ bool atmosphereFrameValid = IsVerifiedAtmosphereForFrame(atmosphereMatched, 11);
+ bool atmosphereClockMismatchRejected = !IsVerifiedAtmosphereForFrame(atmosphereClockMismatch, 11);
+ bool cameraFrameValid = IsMatchingPerspectiveFrame(matched, 11);
+ bool wrongCameraThreadRejected = !IsMatchingPerspectiveFrame(matched, 12);
+ bool emptyCameraFrameRejected = !IsMatchingPerspectiveFrame(new FrameSnapshot(), 11);
+ bool selectedFrameValid = IsMatchingPerspectiveFrame(selectedMatched, 11);
+ bool selectedUnrestoredRejected = !IsMatchingPerspectiveFrame(selectedUnrestored, 11);
+ bool selectedUnrestoredCaptureEligible = CanArmPresentForFrame(selectedUnrestored, 11);
+ bool selectedIntervenedCaptureRejected = !CanArmPresentForFrame(selectedIntervened, 11);
+ bool selectedIntervenedRejected = !IsMatchingPerspectiveFrame(selectedIntervened, 11);
+ bool selectedChangedMatrixRejected = !CanArmPresentForFrame(selectedMatrixChanged, 11)
+ && !IsMatchingPerspectiveFrame(selectedMatrixChanged, 11);
+ bool stopAfterCapture = CanStopCapture(true, false, false, false, false);
+ bool stopAfterFailure = CanStopCapture(false, true, false, false, false);
+ bool waitsForRemote = !CanStopCapture(true, false, true, false, false);
+ bool waitsForStep = !CanStopCapture(true, false, false, true, false);
+ bool waitsForCameraRestore = !CanStopCapture(true, false, false, false, true);
+ bool resumedEipOk = BitConverter.ToUInt32(resumedContext, ContextEipOffset) == 0x12345678u;
+ bool resumedEspOk = BitConverter.ToUInt32(resumedContext, ContextEspOffset) == pendingEsp;
+ bool resumedTrapCleared = (BitConverter.ToUInt32(resumedContext, ContextEflagsOffset) & 0x100u) == 0;
+ if (!offsetsMatch
+ || ThreadQueryInformation != 0x00000040
+ || ContextEbxOffset != 164 || ContextEdxOffset != 168 || ContextEcxOffset != 172
+ || !atmosphereArithmetic || !zeroPeriodRejected
+ || !atmosphereFrameValid
+ || !atmosphereClockMismatchRejected
+ || !cameraFrameValid || !wrongCameraThreadRejected || !emptyCameraFrameRejected
+ || !selectedFrameValid || !selectedUnrestoredRejected || !selectedUnrestoredCaptureEligible
+ || !selectedIntervenedCaptureRejected || !selectedIntervenedRejected
+ || !selectedChangedMatrixRejected
+ || !stopAfterCapture || !stopAfterFailure || !waitsForRemote || !waitsForStep || !waitsForCameraRestore
+ || !resumedEipOk || !resumedEspOk || !resumedTrapCleared)
+ throw new InvalidOperationException("Self-check failed: offsets=" + offsetsMatch
+ + " atmosphereArithmetic=" + atmosphereArithmetic + " zeroPeriodRejected=" + zeroPeriodRejected
+ + " atmosphereFrameValid=" + atmosphereFrameValid
+ + " atmosphereClockMismatchRejected=" + atmosphereClockMismatchRejected
+ + " cameraFrameValid=" + cameraFrameValid + " wrongCameraThreadRejected=" + wrongCameraThreadRejected
+ + " emptyCameraFrameRejected=" + emptyCameraFrameRejected + " stopAfterCapture=" + stopAfterCapture
+ + " selectedFrameValid=" + selectedFrameValid + " selectedUnrestoredRejected=" + selectedUnrestoredRejected
+ + " selectedUnrestoredCaptureEligible=" + selectedUnrestoredCaptureEligible
+ + " selectedIntervenedCaptureRejected=" + selectedIntervenedCaptureRejected
+ + " selectedIntervenedRejected=" + selectedIntervenedRejected
+ + " selectedChangedMatrixRejected=" + selectedChangedMatrixRejected
+ + " stopAfterFailure=" + stopAfterFailure + " waitsForRemote=" + waitsForRemote
+ + " waitsForStep=" + waitsForStep + " waitsForCameraRestore=" + waitsForCameraRestore
+ + " resumedEipOk=" + resumedEipOk
+ + " resumedEspOk=" + resumedEspOk + " resumedTrapCleared=" + resumedTrapCleared
+ + " ThreadQueryInformation=" + ThreadQueryInformation + " Ebx=" + ContextEbxOffset
+ + " Edx=" + ContextEdxOffset + " Ecx=" + ContextEcxOffset);
+ Console.WriteLine("self-check: x86 CONTEXT layout, relocation RVA, validated camera JSON, post-prologue invocation identity/stack gates, camera/projection gates, wrapping atmosphere phase arithmetic/clock pairing, and recovery states OK");
+ return SelfCheckReadback();
+ }
+
+ private static int Main(string[] args)
+ {
+ try
+ {
+ if (args.Length == 1 && args[0] == "--self-check") return SelfCheck();
+ if (args.Length == 2 && args[0] == "--validate-camera-input")
+ {
+ SelectedCameraInput validated = LoadSelectedCameraInput(args[1]);
+ Console.WriteLine("camera input valid: matrixSha256=" + validated.MatrixSha256
+ + " viewport=" + String.Join(",", validated.Viewport)
+ + " near=" + validated.Near.ToString("R", CultureInfo.InvariantCulture)
+ + " far=" + validated.Far.ToString("R", CultureInfo.InvariantCulture)
+ + " fov=" + validated.FieldOfView.ToString("R", CultureInfo.InvariantCulture)
+ + " mode=" + validated.ProjectionMode);
+ return 0;
+ }
+ bool hasCameraInput = args.Length == 6 && args[4] == "--camera-input";
+ if ((args.Length != 4 && !hasCameraInput) || args[0] != "--capture")
+ {
+ Console.Error.WriteLine(" NativeFrameCapture.exe --capture <pid> <expected-start-utc-ticks> <output.json>");
+ Console.Error.WriteLine(" [--camera-input <native-frame.json>]");
+ Console.Error.WriteLine(" NativeFrameCapture.exe --validate-camera-input <native-frame.json>");
+ Console.Error.WriteLine(" NativeFrameCapture.exe --self-check");
+ return 2;
+ }
+ if (IntPtr.Size != 4) throw new InvalidOperationException("Run the x86 build only.");
+ uint processId = UInt32.Parse(args[1], CultureInfo.InvariantCulture);
+ long expectedStartTicks = Int64.Parse(args[2], CultureInfo.InvariantCulture);
+ SelectedCameraInput selectedCameraInput = hasCameraInput ? LoadSelectedCameraInput(args[5]) : null;
+ if (selectedCameraInput != null)
+ Log("validated selected camera input path=" + selectedCameraInput.Path
+ + " matrixSha256=" + selectedCameraInput.MatrixSha256
+ + " viewport=" + String.Join(",", selectedCameraInput.Viewport)
+ + " near=" + selectedCameraInput.Near.ToString("R", CultureInfo.InvariantCulture)
+ + " far=" + selectedCameraInput.Far.ToString("R", CultureInfo.InvariantCulture)
+ + " fov=" + selectedCameraInput.FieldOfView.ToString("R", CultureInfo.InvariantCulture)
+ + " mode=" + selectedCameraInput.ProjectionMode);
+ string repositoryRoot = FindRepositoryRoot();
+ string expectedPath = Path.GetFullPath(VerifyScratchFiles(repositoryRoot));
+ string outputPath = Path.GetFullPath(args[3]);
+ string targetRoot = Path.GetFullPath(Path.Combine(repositoryRoot, "target")) + Path.DirectorySeparatorChar;
+ if (!outputPath.StartsWith(targetRoot, StringComparison.OrdinalIgnoreCase))
+ throw new InvalidOperationException("Output must be beneath the repository target directory: " + targetRoot);
+ if (!String.Equals(Path.GetExtension(outputPath), ".json", StringComparison.OrdinalIgnoreCase))
+ throw new InvalidOperationException("Capture output must use a .json extension so PNG/log paths cannot overlap it.");
+ string outputPng = Path.ChangeExtension(outputPath, ".png");
+ string outputLog = Path.ChangeExtension(outputPath, ".log");
+ if (File.Exists(outputPath) || File.Exists(outputPng) || File.Exists(outputLog))
+ throw new InvalidOperationException("Output JSON, PNG, or log already exists; choose a fresh basename: " + outputPath);
+ Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+ _logPath = outputLog;
+
+ using (Process target = Process.GetProcessById((int)processId))
+ {
+ IntPtr identityHandle = target.Handle;
+ if (identityHandle == IntPtr.Zero) throw new InvalidOperationException("Could not retain the target process identity handle.");
+ string actualPath = Path.GetFullPath(target.MainModule.FileName);
+ long actualStartTicks = target.StartTime.ToUniversalTime().Ticks;
+ if (target.ProcessName != "iron_3d"
+ || !String.Equals(actualPath, expectedPath, StringComparison.OrdinalIgnoreCase))
+ throw new InvalidOperationException("Refusing PID: expected exact scratch image " + expectedPath + ", got " + actualPath);
+ if (actualStartTicks != expectedStartTicks)
+ throw new InvalidOperationException("Refusing stale PID; expected UTC start ticks "
+ + expectedStartTicks + ", got " + actualStartTicks);
+ Log("identity pid=" + processId + " path=" + actualPath + " startUtc="
+ + target.StartTime.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture)
+ + " startTicks=" + actualStartTicks + " bits=" + (IntPtr.Size * 8));
+ foreach (KeyValuePair<string, string> expected in ExpectedSha256)
+ Log("verified sha256 " + expected.Key + "=" + expected.Value);
+ return AttachForFrame(processId, expectedPath, expectedStartTicks, outputPath,
+ selectedCameraInput, target, FrameCaptureTimeoutSeconds);
+ }
+ }
+ catch (Exception exception)
+ {
+ Log("ERROR " + exception);
+ Console.Error.WriteLine(exception.Message);
+ return 1;
+ }
+ }
+
+ private static int AttachForFrame(uint processId, string expectedPath, long expectedStartTicks,
+ string outputPath, SelectedCameraInput selectedCameraInput, Process identityProcess, int timeoutSeconds)
+ {
+ const uint ProcessTerminate = 0x0001;
+ const uint ProcessVmRead = 0x0010;
+ const uint ProcessVmWrite = 0x0020;
+ const uint ProcessVmOperation = 0x0008;
+ const uint ProcessQueryInformation = 0x0400;
+ const uint Synchronize = 0x00100000;
+ IntPtr process = IntPtr.Zero;
+ IntPtr eventBuffer = IntPtr.Zero;
+ uint world3dBase = 0;
+ uint terrainBase = 0;
+ uint ngiBase = 0;
+ byte presentOriginalByte = 0;
+ bool presentBoundaryVerified = false;
+ bool attached = false;
+ bool detached = false;
+ bool breakpointsRestored = false;
+ bool fatal = false;
+ bool processExited = false;
+ bool processExitUnconfirmed = false;
+ bool captured = false;
+ SurfaceReadback completedSurface = null;
+ FrameSnapshot completedFrame = null;
+ RemoteReadbackSession completedReadback = null;
+ bool stopAfterAttempt = false;
+ string captureFailure = null;
+ DateTime deadline = DateTime.UtcNow.AddSeconds(timeoutSeconds);
+ string expectedDirectory = Path.GetDirectoryName(expectedPath);
+ string expectedWorld3D = Path.Combine(expectedDirectory, World3DName);
+ string expectedTerrain = Path.Combine(expectedDirectory, TerrainName);
+ string expectedNgi32 = Path.Combine(expectedDirectory, Ngi32Name);
+ string expectedIron3d = Path.Combine(expectedDirectory, Iron3dName);
+ uint iron3dBase = 0;
+ string terrainModuleSha256 = null;
+ BreakpointInfo cameraBreakpoint = null;
+ BreakpointInfo projectionBreakpoint = null;
+ BreakpointInfo returnBreakpoint = null;
+ BreakpointInfo atmosphereBreakpoint = null;
+ BreakpointInfo presentBreakpoint = null;
+ BreakpointInfo missionBreakpoint = null;
+ int missionProbeAttempts = 0;
+ bool missionProbeFinished = false;
+ PendingStep pendingStep = null;
+ RemoteReadbackSession remote = null;
+ RemoteCameraSetterSession cameraSetter = null;
+ FrameSnapshot frame = new FrameSnapshot();
+ frame.SelectedCameraRequested = selectedCameraInput != null;
+ SelectedCameraState selectedCamera = selectedCameraInput == null ? null
+ : new SelectedCameraState { Input = selectedCameraInput };
+ try
+ {
+ if (identityProcess == null || identityProcess.HasExited
+ || identityProcess.StartTime.ToUniversalTime().Ticks != expectedStartTicks
+ || !String.Equals(Path.GetFullPath(identityProcess.MainModule.FileName), expectedPath,
+ StringComparison.OrdinalIgnoreCase))
+ throw new InvalidOperationException("The retained process identity changed before debugger attach.");
+ if (!DebugActiveProcess(processId))
+ throw new InvalidOperationException("DebugActiveProcess failed: " + Marshal.GetLastWin32Error());
+ attached = true;
+ if (identityProcess.HasExited || identityProcess.StartTime.ToUniversalTime().Ticks != expectedStartTicks
+ || !String.Equals(Path.GetFullPath(identityProcess.MainModule.FileName), expectedPath,
+ StringComparison.OrdinalIgnoreCase))
+ throw new InvalidOperationException("The retained process identity changed during debugger attach.");
+ if (!DebugSetProcessKillOnExit(false))
+ throw new InvalidOperationException("DebugSetProcessKillOnExit(false) failed: " + Marshal.GetLastWin32Error());
+ process = OpenProcess(ProcessTerminate | ProcessVmOperation | ProcessVmRead | ProcessVmWrite
+ | ProcessQueryInformation | Synchronize, false, processId);
+ if (process == IntPtr.Zero) throw new InvalidOperationException("OpenProcess failed: " + Marshal.GetLastWin32Error());
+ eventBuffer = Marshal.AllocHGlobal(128);
+ Log("frame capture attached; waiting for verified camera/projection/present boundaries for "
+ + timeoutSeconds + " seconds");
+
+ while (!fatal && !CanStopCapture(captured, stopAfterAttempt, remote != null || cameraSetter != null,
+ pendingStep != null, selectedCamera != null && selectedCamera.Applied && !selectedCamera.RestoreVerified)
+ && (DateTime.UtcNow < deadline || remote != null || cameraSetter != null || pendingStep != null
+ || selectedCamera != null && selectedCamera.Applied && !selectedCamera.RestoreVerified))
+ {
+ if (remote != null && DateTime.UtcNow >= remote.DeadlineUtc)
+ {
+ Log("REMOTE_COM_STALLED phase=" + remote.Phase + " tid=" + remote.ThreadId
+ + "; terminating only the path/tick/hash-verified scratch process");
+ if (!TerminateProcess(process, 0xE001))
+ Log("REMOTE_COM_TERMINATE_FAILED error=" + Marshal.GetLastWin32Error());
+ fatal = true;
+ continue;
+ }
+ if (cameraSetter != null && DateTime.UtcNow >= cameraSetter.DeadlineUtc)
+ {
+ Log("CAMERA_SETTER_STALLED phase=" + cameraSetter.Phase + " tid=" + cameraSetter.ThreadId
+ + "; terminating only the path/tick/hash-verified scratch process");
+ if (!TerminateProcess(process, 0xE00A))
+ Log("CAMERA_SETTER_TERMINATE_FAILED error=" + Marshal.GetLastWin32Error());
+ fatal = true;
+ continue;
+ }
+ if (selectedCamera != null && selectedCamera.Applied && !selectedCamera.RestoreVerified
+ && cameraSetter == null && selectedCamera.RestoreDeadlineUtc != DateTime.MinValue
+ && DateTime.UtcNow >= selectedCamera.RestoreDeadlineUtc)
+ {
+ Log("SELECTED_CAMERA_RESTORE_BOUND_EXPIRED; terminating only the verified scratch process");
+ if (!TerminateProcess(process, 0xE00B))
+ Log("SELECTED_CAMERA_RESTORE_TERMINATE_FAILED error=" + Marshal.GetLastWin32Error());
+ fatal = true;
+ continue;
+ }
+ if (pendingStep != null && DateTime.UtcNow >= pendingStep.DeadlineUtc)
+ {
+ Log("SINGLE_STEP_STALLED boundary=" + pendingStep.Breakpoint.Name + " tid=" + pendingStep.ThreadId
+ + "; terminating only the path/tick/hash-verified scratch process");
+ if (!TerminateProcess(process, 0xE002))
+ Log("SINGLE_STEP_TERMINATE_FAILED error=" + Marshal.GetLastWin32Error());
+ fatal = true;
+ continue;
+ }
+ if (!WaitForDebugEvent(eventBuffer, 1000))
+ {
+ int waitError = Marshal.GetLastWin32Error();
+ if (waitError == 121 || waitError == 258) continue;
+ throw new InvalidOperationException("WaitForDebugEvent failed: " + waitError);
+ }
+
+ uint eventCode = unchecked((uint)Marshal.ReadInt32(eventBuffer, 0));
+ uint eventPid = unchecked((uint)Marshal.ReadInt32(eventBuffer, 4));
+ uint eventTid = unchecked((uint)Marshal.ReadInt32(eventBuffer, 8));
+ uint continueStatus = DbgContinue;
+ bool shouldStop = false;
+ BreakpointInfo hitBreakpoint = null;
+ byte[] stoppedContext = null;
+ try
+ {
+ if (eventCode == CreateProcessDebugEvent)
+ {
+ IntPtr imageFile = Marshal.ReadIntPtr(eventBuffer, 12);
+ IntPtr processHandle = Marshal.ReadIntPtr(eventBuffer, 16);
+ IntPtr threadHandle = Marshal.ReadIntPtr(eventBuffer, 20);
+ string imagePath = PathForHandle(imageFile);
+ if (imagePath.Length != 0 && !String.Equals(imagePath, NormalizePath(expectedPath), StringComparison.OrdinalIgnoreCase))
+ throw new InvalidOperationException("CREATE_PROCESS image path mismatch: " + imagePath);
+ if (processHandle != IntPtr.Zero) CloseHandle(processHandle);
+ if (threadHandle != IntPtr.Zero) CloseHandle(threadHandle);
+ if (imageFile != IntPtr.Zero) CloseHandle(imageFile);
+ Log("CREATE_PROCESS path=" + imagePath);
+ }
+ else if (eventCode == CreateThreadDebugEvent)
+ {
+ IntPtr threadHandle = Marshal.ReadIntPtr(eventBuffer, 12);
+ if (threadHandle != IntPtr.Zero) CloseHandle(threadHandle);
+ Log("CREATE_THREAD tid=" + eventTid + " handleClosed=" + (threadHandle != IntPtr.Zero));
+ }
+ else if (eventCode == ExitThreadDebugEvent)
+ {
+ uint threadExitCode = unchecked((uint)Marshal.ReadInt32(eventBuffer, 12));
+ Log("EXIT_THREAD tid=" + eventTid + " exitCode=0x" + threadExitCode.ToString("X8"));
+ }
+ else if (eventCode == LoadDllDebugEvent)
+ {
+ IntPtr imageFile = Marshal.ReadIntPtr(eventBuffer, 12);
+ try
+ {
+ uint imageBase = unchecked((uint)Marshal.ReadInt32(eventBuffer, 16));
+ string imagePath = PathForHandle(imageFile);
+ if (imagePath.Length != 0 && String.Equals(imagePath, NormalizePath(expectedTerrain), StringComparison.OrdinalIgnoreCase))
+ {
+ string hash = HashFile(imagePath);
+ if (!String.Equals(hash, ExpectedSha256[TerrainName], StringComparison.OrdinalIgnoreCase))
+ throw new InvalidOperationException("Loaded Terrain hash mismatch: " + hash);
+ terrainBase = imageBase;
+ terrainModuleSha256 = hash;
+ byte atmosphereByte;
+ string atmosphereEvidence;
+ if (!VerifyAtmospherePhaseBoundary(process, terrainBase, out atmosphereByte, out atmosphereEvidence))
+ throw new InvalidOperationException("Terrain atmosphere phase boundary signature failed: " + atmosphereEvidence);
+ atmosphereBreakpoint = ArmBreakpoint(process, "Terrain.atmosphere-phase",
+ unchecked(terrainBase + AtmospherePhaseRva), atmosphereByte);
+ Log("Terrain verified base=0x" + terrainBase.ToString("X8") + " sha256=" + hash
+ + " " + atmosphereEvidence);
+ }
+ if (imagePath.Length != 0 && String.Equals(imagePath, NormalizePath(expectedIron3d), StringComparison.OrdinalIgnoreCase))
+ {
+ string loadedPath = ModulePath(process, imageBase);
+ string hash = HashFile(loadedPath);
+ if (!String.Equals(hash, ExpectedSha256[Iron3dName], StringComparison.OrdinalIgnoreCase))
+ throw new InvalidOperationException("Loaded iron3d hash mismatch: " + hash);
+ byte missionByte;
+ string missionEvidence;
+ if (!VerifyMissionIdentityBoundary(process, imageBase, out missionByte, out missionEvidence))
+ throw new InvalidOperationException("iron3d mission path boundary signature failed: " + missionEvidence);
+ iron3dBase = imageBase;
+ missionBreakpoint = ArmBreakpoint(process, "iron3d.mission-path-vtable-call",
+ unchecked(imageBase + MissionVtableCallRva), missionByte);
+ Log("iron3d verified sha256=" + hash + " " + missionEvidence);
+ }
+ if (imagePath.Length != 0 && String.Equals(imagePath, NormalizePath(expectedWorld3D), StringComparison.OrdinalIgnoreCase))
+ {
+ string loadedPath = ModulePath(process, imageBase);
+ string hash = HashFile(loadedPath);
+ if (!String.Equals(hash, ExpectedSha256[World3DName], StringComparison.OrdinalIgnoreCase))
+ throw new InvalidOperationException("Loaded World3D hash mismatch: " + hash);
+ byte renderByte;
+ byte returnByte = 0;
+ byte projectionByte;
+ string renderEvidence;
+ string returnEvidence = "";
+ string projectionEvidence;
+ if (!VerifyRenderEntry(process, imageBase, out renderByte, out renderEvidence))
+ throw new InvalidOperationException("World3D render-entry signature failed: " + renderEvidence);
+ if (!VerifyProjectionBoundary(process, imageBase, out projectionByte, out projectionEvidence))
+ throw new InvalidOperationException("World3D projection signature failed: " + projectionEvidence);
+ if (selectedCameraInput != null
+ && !VerifyRenderReturnBoundary(process, imageBase, out returnByte, out returnEvidence))
+ throw new InvalidOperationException("World3D selected-camera restore boundary failed: " + returnEvidence);
+ world3dBase = imageBase;
+ cameraBreakpoint = ArmBreakpoint(process, "World3D.stdRenderGame", imageBase + RenderGameRva, renderByte);
+ projectionBreakpoint = ArmBreakpoint(process, "World3D.projection-snapshot", imageBase + ProjectionSnapshotRva, projectionByte);
+ if (selectedCameraInput != null)
+ returnBreakpoint = ArmBreakpoint(process, "World3D.stdRenderGame-restore-return",
+ imageBase + RenderReturnRva, returnByte);
+ Log("World3D verified sha256=" + hash + " " + renderEvidence + " " + projectionEvidence
+ + (selectedCameraInput == null ? "" : " " + returnEvidence));
+ }
+ if (imagePath.Length != 0 && String.Equals(imagePath, NormalizePath(expectedNgi32), StringComparison.OrdinalIgnoreCase))
+ {
+ string loadedPath = ModulePath(process, imageBase);
+ string hash = HashFile(loadedPath);
+ if (!String.Equals(hash, ExpectedSha256[Ngi32Name], StringComparison.OrdinalIgnoreCase))
+ throw new InvalidOperationException("Loaded Ngi32 hash mismatch: " + hash);
+ byte presentByte;
+ string presentEvidence;
+ if (!VerifyPresentBoundary(process, imageBase, out presentByte, out presentEvidence))
+ throw new InvalidOperationException("Ngi32 present signature failed: " + presentEvidence);
+ ngiBase = imageBase;
+ presentOriginalByte = presentByte;
+ presentBoundaryVerified = true;
+ Log("Ngi32 verified sha256=" + hash + " " + presentEvidence);
+ if (IsMatchingPerspectiveFrame(frame, frame.CameraThreadId))
+ EnsurePresentBreakpoint(process, ngiBase, frame, frame.CameraThreadId,
+ presentOriginalByte, ref presentBreakpoint);
+ }
+ }
+ finally { if (imageFile != IntPtr.Zero) CloseHandle(imageFile); }
+ }
+ else if (eventCode == ExceptionDebugEvent)
+ {
+ uint exceptionCode = unchecked((uint)Marshal.ReadInt32(eventBuffer, 12));
+ uint exceptionAddress = unchecked((uint)Marshal.ReadInt32(eventBuffer, 24));
+ if (cameraSetter != null)
+ {
+ bool expectedTrap = exceptionCode == 0x80000003
+ && exceptionAddress == cameraSetter.Code.TrapAddress
+ && eventTid == cameraSetter.ThreadId;
+ if (!expectedTrap)
+ {
+ Log("CAMERA_SETTER_UNEXPECTED_EXCEPTION phase=" + cameraSetter.Phase
+ + " code=0x" + exceptionCode.ToString("X8")
+ + " address=0x" + exceptionAddress.ToString("X8") + " tid=" + eventTid
+ + "; terminating the verified scratch process");
+ TerminateProcess(process, 0xE00B);
+ fatal = true;
+ }
+ else
+ {
+ string returnEvidence;
+ bool returnContextOk = VerifyCameraSetterReturnContext(process, cameraSetter, out returnEvidence);
+ byte[] actualMatrix = ReadCameraMatrix(process, cameraSetter.Camera);
+ bool matrixMatches = ByteArraysEqual(actualMatrix, cameraSetter.MatrixBytes);
+ string abiEvidence;
+ uint transformInterface;
+ bool abiStillValid = VerifyCameraSetterAbi(process, terrainBase,
+ cameraSetter.Camera, out transformInterface, out abiEvidence);
+ Log("CAMERA_SETTER_RETURN phase=" + cameraSetter.Phase
+ + " contextOk=" + returnContextOk + " matrixMatches=" + matrixMatches
+ + " abiStillValid=" + abiStillValid + " " + returnEvidence + " " + abiEvidence);
+ if (!returnContextOk || !matrixMatches || !abiStillValid)
+ {
+ captureFailure = "Native camera setter could not be verified; the isolated scratch process will be stopped.";
+ Log("CAMERA_SETTER_UNVERIFIED; terminating the verified scratch process before detach");
+ TerminateProcess(process, 0xE00C);
+ fatal = true;
+ }
+ else if (cameraSetter.Phase == CameraSetterPhase.Apply)
+ {
+ CameraReadback appliedCamera = frame.Camera;
+ if (selectedCamera == null || appliedCamera == null || !appliedCamera.LayoutVerified
+ || appliedCamera.Camera != selectedCamera.CameraPointer
+ || !ByteArraysEqual(actualMatrix, selectedCamera.Input.MatrixBytes))
+ {
+ captureFailure = "Native setter did not leave the requested selected-camera matrix in place.";
+ Log("CAMERA_SETTER_APPLY_READBACK_FAILED; terminating the verified scratch process");
+ TerminateProcess(process, 0xE00D);
+ fatal = true;
+ }
+ else
+ {
+ appliedCamera.EntryMatrixBytes = (byte[])selectedCamera.Input.MatrixBytes.Clone();
+ appliedCamera.EntryMatrixSha256 = selectedCamera.Input.MatrixSha256;
+ appliedCamera.ProjectionMatrixSha256 = null;
+ appliedCamera.CurrentAtProjectionVerified = false;
+ appliedCamera.WordsChangedAtProjection = false;
+ appliedCamera.Words = (uint[])selectedCamera.Input.MatrixWords.Clone();
+ appliedCamera.MatrixFinite = true;
+ selectedCamera.Applied = true;
+ selectedCamera.RestoreDeadlineUtc = DateTime.UtcNow.AddSeconds(5);
+ frame.SelectedCamera = selectedCamera;
+ frame.Camera = appliedCamera;
+ Log("CAMERA_SETTER_APPLY_VERIFIED generation=" + selectedCamera.Generation
+ + " camera=0x" + selectedCamera.CameraPointer.ToString("X8")
+ + " matrixSha256=" + selectedCamera.Input.MatrixSha256
+ + " originalSha256=" + selectedCamera.OriginalMatrixSha256);
+ BeginSingleStep(process, eventTid, cameraSetter.ResumeBreakpoint,
+ cameraSetter.OriginalContext, out pendingStep, cameraSetter.RearmOnStep);
+ Log("SINGLE_STEP_STARTED boundary=" + cameraSetter.ResumeBreakpoint.Name + " tid=" + eventTid);
+ if (VirtualFreeEx(process, Ptr(cameraSetter.Region), UIntPtr.Zero, 0x8000))
+ Log("CAMERA_SETTER_REGION_FREED phase=Apply");
+ else Log("CAMERA_SETTER_REGION_FREE_FAILED phase=Apply error=" + Marshal.GetLastWin32Error());
+ cameraSetter = null;
+ }
+ }
+ else
+ {
+ if (selectedCamera == null || !ByteArraysEqual(actualMatrix, selectedCamera.OriginalMatrixBytes))
+ {
+ captureFailure = "The original native camera matrix was not restored exactly.";
+ Log("CAMERA_SETTER_RESTORE_READBACK_FAILED; terminating the verified scratch process");
+ TerminateProcess(process, 0xE00E);
+ fatal = true;
+ }
+ else
+ {
+ selectedCamera.Restored = true;
+ selectedCamera.RestoreVerified = true;
+ selectedCamera.RestoreDeadlineUtc = DateTime.MinValue;
+ Log("CAMERA_SETTER_RESTORE_VERIFIED camera=0x" + selectedCamera.CameraPointer.ToString("X8")
+ + " matrixSha256=" + selectedCamera.OriginalMatrixSha256
+ + " returnBoundary=World3D+0x13D7B");
+ BeginSingleStep(process, eventTid, cameraSetter.ResumeBreakpoint,
+ cameraSetter.OriginalContext, out pendingStep, cameraSetter.RearmOnStep);
+ Log("SINGLE_STEP_STARTED boundary=" + cameraSetter.ResumeBreakpoint.Name + " tid=" + eventTid);
+ if (VirtualFreeEx(process, Ptr(cameraSetter.Region), UIntPtr.Zero, 0x8000))
+ Log("CAMERA_SETTER_REGION_FREED phase=Restore");
+ else Log("CAMERA_SETTER_REGION_FREE_FAILED phase=Restore error=" + Marshal.GetLastWin32Error());
+ cameraSetter = null;
+ }
+ }
+ }
+ }
+ else if (remote != null)
+ {
+ uint expectedTrap = remote.Phase == RemoteReadbackPhase.Acquire
+ ? remote.Acquire.TrapAddress : remote.Cleanup.TrapAddress;
+ if (exceptionCode != 0x80000003 || exceptionAddress != expectedTrap || eventTid != remote.ThreadId)
+ {
+ Log("REMOTE_STUB_UNEXPECTED_EXCEPTION code=0x" + exceptionCode.ToString("X8")
+ + " address=0x" + exceptionAddress.ToString("X8") + " tid=" + eventTid);
+ TerminateProcess(process, 0xE003);
+ fatal = true;
+ }
+ else if (remote.Phase == RemoteReadbackPhase.Acquire)
+ {
+ uint data = remote.Acquire.DataBase;
+ remote.Status = ReadU32Exact(process, data + RemoteDataStatusOffset);
+ remote.GetHr = ReadU32Exact(process, data + RemoteDataGetHrOffset);
+ remote.LockHr = ReadU32Exact(process, data + RemoteDataLockHrOffset);
+ remote.ReleaseResult = ReadU32Exact(process, data + RemoteDataReleaseCountOffset);
+ Log("REMOTE_ACQUIRE_DONE status=" + remote.Status + " getHR=0x" + remote.GetHr.ToString("X8")
+ + " lockHR=0x" + remote.LockHr.ToString("X8") + " release=0x" + remote.ReleaseResult.ToString("X8"));
+ remote.PresentStackArgumentsIntact = remote.PresentStackArgumentsIntact
+ && VerifyPresentStackArguments(process, remote, "after-get-render-target-and-lock");
+ remote.RemoteContextIntact = remote.RemoteContextIntact
+ && VerifyRemoteStubContext(process, remote, unchecked(remote.Acquire.TrapAddress + 1),
+ "after-get-render-target-and-lock");
+ if (remote.Status == 1)
+ {
+ try { remote.Surface = ReadSurfaceRows(process, data + RemoteDataDescOffset); }
+ catch (Exception pixelError) { remote.PixelFailure = pixelError.Message; }
+ BeginRemoteCleanup(process, remote);
+ }
+ else if (!remote.PresentStackArgumentsIntact || !remote.RemoteContextIntact)
+ {
+ captureFailure = "Present call stack arguments changed during D3D7 readback.";
+ Log("PRESENT_STACK_CORRUPTION_UNRECOVERED; terminating the verified scratch process before detach");
+ fatal = true;
+ }
+ else
+ {
+ captureFailure = "D3D7 render-target readback returned status " + remote.Status
+ + " (GetRenderTarget 0x" + remote.GetHr.ToString("X8")
+ + ", Lock 0x" + remote.LockHr.ToString("X8") + ").";
+ Log("REMOTE_CAPTURE_RETRY " + captureFailure);
+ ResumeOriginalPresent(process, remote);
+ VirtualFreeEx(process, Ptr(remote.Region), UIntPtr.Zero, 0x8000);
+ remote = null;
+ }
+ }
+ else
+ {
+ uint data = remote.Cleanup.DataBase;
+ remote.Status = ReadU32Exact(process, data + RemoteDataStatusOffset);
+ remote.UnlockHr = ReadU32Exact(process, data + RemoteDataUnlockHrOffset);
+ remote.ReleaseResult = ReadU32Exact(process, data + RemoteDataReleaseCountOffset);
+ Log("REMOTE_CLEANUP_DONE status=" + remote.Status + " unlockHR=0x" + remote.UnlockHr.ToString("X8")
+ + " release=0x" + remote.ReleaseResult.ToString("X8"));
+ bool unlocked = remote.Status == 6 && unchecked((int)remote.UnlockHr) >= 0;
+ remote.PresentStackArgumentsIntact = remote.PresentStackArgumentsIntact
+ && VerifyPresentStackArguments(process, remote, "after-unlock-and-release");
+ remote.RemoteContextIntact = remote.RemoteContextIntact
+ && VerifyRemoteStubContext(process, remote, unchecked(remote.Cleanup.TrapAddress + 1),
+ "after-unlock-and-release");
+ bool outputStaged = false;
+ if (!unlocked)
+ {
+ captureFailure = "D3D7 Unlock failed; the render target may remain locked (status "
+ + remote.Status + ", HRESULT 0x" + remote.UnlockHr.ToString("X8") + ").";
+ Log("FRAME_CAPTURE_NOT_USABLE " + (remote.PixelFailure ?? captureFailure));
+ Log("UNLOCK_FAILURE_UNRECOVERED; terminating the verified scratch process before detach");
+ fatal = true;
+ // Keep the session non-null. The final recovery path terminates this
+ // exact scratch process and waits for exit before it detaches.
+ }
+ else if (!remote.PresentStackArgumentsIntact || !remote.RemoteContextIntact)
+ {
+ captureFailure = "Present call stack arguments changed during D3D7 readback.";
+ Log("PRESENT_STACK_CORRUPTION_UNRECOVERED; terminating the verified scratch process before detach");
+ fatal = true;
+ }
+ else
+ {
+ if (remote.Surface != null)
+ {
+ if (completedSurface != null || completedFrame != null || completedReadback != null)
+ {
+ captureFailure = "A second frame readback completed before the staged frame could be safely finalized.";
+ Log("FRAME_OUTPUT_STAGE_REJECTED " + captureFailure);
+ stopAfterAttempt = true;
+ }
+ else
+ {
+ completedSurface = remote.Surface;
+ completedFrame = remote.Frame;
+ completedReadback = remote;
+ outputStaged = true;
+ Log("FRAME_OUTPUT_STAGED generation=" + remote.Generation
+ + " size=" + remote.Surface.Width + "x" + remote.Surface.Height
+ + " bitCount=" + remote.Surface.BitCount + " rowsSha256=" + remote.Surface.Sha256);
+ }
+ }
+ else
+ {
+ captureFailure = remote.PixelFailure ?? "D3D7 Lock succeeded, but pixel rows were not available.";
+ Log("FRAME_CAPTURE_NOT_USABLE " + captureFailure);
+ stopAfterAttempt = true;
+ }
+
+ ResumeOriginalPresent(process, remote);
+ if (VirtualFreeEx(process, Ptr(remote.Region), UIntPtr.Zero, 0x8000))
+ Log("REMOTE_READBACK_REGION_FREED");
+ else Log("REMOTE_READBACK_REGION_FREE_FAILED error=" + Marshal.GetLastWin32Error());
+ remote = null;
+ if (outputStaged)
+ {
+ captured = true;
+ Log("FRAME_CAPTURE_CONTEXT_RESTORED; awaiting camera restore and safe detach before writing outputs");
+ }
+ }
+ }
+ }
+ else if (exceptionCode == 0x80000004 && pendingStep != null)
+ {
+ FinishSingleStep(process, eventTid, pendingStep);
+ Log("SINGLE_STEP_COMPLETE boundary=" + pendingStep.Breakpoint.Name + " tid=" + eventTid);
+ pendingStep = null;
+ }
+ else if (exceptionCode == 0x80000003)
+ {
+ if (cameraBreakpoint != null && cameraBreakpoint.Armed && exceptionAddress == cameraBreakpoint.Address)
+ hitBreakpoint = cameraBreakpoint;
+ else if (projectionBreakpoint != null && projectionBreakpoint.Armed && exceptionAddress == projectionBreakpoint.Address)
+ hitBreakpoint = projectionBreakpoint;
+ else if (returnBreakpoint != null && returnBreakpoint.Armed && exceptionAddress == returnBreakpoint.Address)
+ hitBreakpoint = returnBreakpoint;
+ else if (atmosphereBreakpoint != null && atmosphereBreakpoint.Armed && exceptionAddress == atmosphereBreakpoint.Address)
+ hitBreakpoint = atmosphereBreakpoint;
+ else if (missionBreakpoint != null && missionBreakpoint.Armed && exceptionAddress == missionBreakpoint.Address)
+ hitBreakpoint = missionBreakpoint;
+ else if (presentBreakpoint != null && presentBreakpoint.Armed && exceptionAddress == presentBreakpoint.Address)
+ hitBreakpoint = presentBreakpoint;
+
+ if (hitBreakpoint != null)
+ {
+ IntPtr thread = OpenThread(ThreadGetContext | ThreadSetContext, false, eventTid);
+ if (thread == IntPtr.Zero) throw new InvalidOperationException("OpenThread at " + hitBreakpoint.Name + " failed: " + Marshal.GetLastWin32Error());
+ try { stoppedContext = GetFullX86Context(thread); }
+ finally { CloseHandle(thread); }
+ if (unchecked(BitConverter.ToUInt32(stoppedContext, ContextEipOffset)) != unchecked(hitBreakpoint.Address + 1))
+ throw new InvalidOperationException(hitBreakpoint.Name + " breakpoint EIP did not point past INT3.");
+
+ if (hitBreakpoint == cameraBreakpoint)
+ {
+ if (selectedCamera != null && selectedCamera.Applied)
+ {
+ if (!selectedCamera.PixelAttributionInvalidated)
+ {
+ selectedCamera.PixelAttributionInvalidated = true;
+ selectedCamera.PixelAttributionFailure = "An additional World3D.stdRenderGame invocation occurred before the next present boundary.";
+ captureFailure = selectedCamera.PixelAttributionFailure;
+ stopAfterAttempt = true;
+ Log("SELECTED_PIXEL_ATTRIBUTION_INVALIDATED tid=" + eventTid
+ + " esp=0x" + BitConverter.ToUInt32(stoppedContext, ContextEspOffset).ToString("X8")
+ + " selectedGeneration=" + selectedCamera.Generation
+ + " restoreVerified=" + selectedCamera.RestoreVerified);
+ }
+ Log("CAMERA_ENTRY_SKIPPED_SELECTED_FRAME_PENDING tid=" + eventTid);
+ }
+ else
+ {
+ frame.CameraGeneration++;
+ frame.CameraThreadId = eventTid;
+ frame.CameraWorldGameTimeWordReadable = false;
+ if (world3dBase != 0)
+ {
+ try
+ {
+ frame.CameraWorldGameTimeWord = ReadU32Exact(process, unchecked(world3dBase + 0x32A38));
+ frame.CameraWorldGameTimeWordReadable = true;
+ }
+ catch (Exception clockError)
+ {
+ Log("CAMERA_WORLD_GAME_TIME_UNREADABLE " + clockError.Message);
+ }
+ }
+ frame.Projection = null;
+ frame.ProjectionGeneration = 0;
+ try { frame.Camera = ReadCameraSnapshot(process, eventTid, terrainBase); }
+ catch (Exception cameraError) { frame.Camera = null; Log("CAMERA_SNAPSHOT_FAILED " + cameraError.Message); }
+ Log("CAMERA_GENERATION " + frame.CameraGeneration + " tid=" + eventTid
+ + " pointer=" + (frame.Camera == null ? "unreadable" : "0x" + frame.Camera.Camera.ToString("X8"))
+ + " layoutVerified=" + (frame.Camera != null && frame.Camera.LayoutVerified)
+ + " matrixSha256=" + (frame.Camera == null ? "unreadable" : frame.Camera.EntryMatrixSha256)
+ + " entryEsp=0x" + (frame.Camera == null ? 0 : frame.Camera.EntryEsp).ToString("X8")
+ + " return=0x" + (frame.Camera == null ? 0 : frame.Camera.ReturnAddress).ToString("X8"));
+
+ if (selectedCamera != null && !selectedCamera.Applied && frame.Camera != null
+ && frame.Camera.LayoutVerified)
+ {
+ if (selectedCamera.CandidateProjectionVerified
+ && frame.Camera.Camera == selectedCamera.CandidateCameraPointer
+ && eventTid == selectedCamera.CandidateThreadId
+ && frame.CameraGeneration != selectedCamera.CandidateGeneration)
+ {
+ if (frame.Camera.EntryEsp < RenderFunctionStackFrameBytes)
+ throw new InvalidOperationException("Selected camera render stack pointer underflowed the verified prologue size.");
+ selectedCamera.CameraPointer = frame.Camera.Camera;
+ selectedCamera.ThreadId = eventTid;
+ selectedCamera.Generation = frame.CameraGeneration;
+ selectedCamera.EntryEsp = frame.Camera.EntryEsp;
+ selectedCamera.FunctionStackEsp = frame.Camera.EntryEsp - RenderFunctionStackFrameBytes;
+ selectedCamera.ReturnAddress = frame.Camera.ReturnAddress;
+ selectedCamera.OriginalMatrixBytes = (byte[])frame.Camera.EntryMatrixBytes.Clone();
+ selectedCamera.OriginalMatrixSha256 = frame.Camera.EntryMatrixSha256;
+ frame.SelectedCamera = selectedCamera;
+ cameraSetter = StartCameraSetter(process, eventTid, terrainBase,
+ selectedCamera.CameraPointer, selectedCamera.Input.MatrixBytes,
+ stoppedContext, cameraBreakpoint, true, CameraSetterPhase.Apply,
+ selectedCamera.EntryEsp, selectedCamera.ReturnAddress);
+ Log("SELECTED_CAMERA_APPLY_STARTED generation=" + selectedCamera.Generation
+ + " candidateGeneration=" + selectedCamera.CandidateGeneration
+ + " camera=0x" + selectedCamera.CameraPointer.ToString("X8")
+ + " entryEsp=0x" + selectedCamera.EntryEsp.ToString("X8")
+ + " return=0x" + selectedCamera.ReturnAddress.ToString("X8")
+ + " originalSha256=" + selectedCamera.OriginalMatrixSha256
+ + " requestedSha256=" + selectedCamera.Input.MatrixSha256);
+ }
+ else if (!selectedCamera.CandidateProjectionVerified)
+ {
+ selectedCamera.CandidateCameraPointer = frame.Camera.Camera;
+ selectedCamera.CandidateThreadId = eventTid;
+ selectedCamera.CandidateGeneration = frame.CameraGeneration;
+ frame.SelectedCamera = selectedCamera;
+ Log("SELECTED_CAMERA_PREFLIGHT_CANDIDATE generation=" + selectedCamera.CandidateGeneration
+ + " camera=0x" + selectedCamera.CandidateCameraPointer.ToString("X8")
+ + " tid=" + selectedCamera.CandidateThreadId
+ + " matrixSha256=" + frame.Camera.EntryMatrixSha256);
+ }
+ else
+ {
+ Log("SELECTED_CAMERA_WAITING_FOR_MATCHING_CANDIDATE tid=" + eventTid
+ + " camera=0x" + frame.Camera.Camera.ToString("X8")
+ + " candidateTid=" + selectedCamera.CandidateThreadId
+ + " candidateCamera=0x" + selectedCamera.CandidateCameraPointer.ToString("X8"));
+ }
+ }
+ else if (selectedCamera != null && !selectedCamera.Applied && frame.Camera != null)
+ Log("SELECTED_CAMERA_CANDIDATE_REJECTED tid=" + eventTid
+ + " layoutVerified=" + frame.Camera.LayoutVerified
+ + " matrixSha256=" + frame.Camera.EntryMatrixSha256
+ + " requestedSha256=" + selectedCamera.Input.MatrixSha256);
+ }
+ }
+ else if (hitBreakpoint == projectionBreakpoint)
+ {
+ string candidateProjectionEvidence = "candidate preconditions not met";
+ string selectedProjectionEvidence = "selected invocation was not checked";
+ bool selectedCandidateScope = selectedCamera != null && !selectedCamera.Applied
+ && !selectedCamera.CandidateProjectionVerified
+ && frame.Camera != null
+ && frame.Camera.Camera == selectedCamera.CandidateCameraPointer
+ && frame.CameraGeneration == selectedCamera.CandidateGeneration
+ && eventTid == selectedCamera.CandidateThreadId
+ && frame.Camera.EntryEsp >= RenderFunctionStackFrameBytes
+ && IsRenderInvocation(process, stoppedContext, eventTid,
+ selectedCamera.CandidateThreadId,
+ frame.Camera.Camera, frame.Camera.EntryEsp,
+ frame.Camera.EntryEsp - RenderFunctionStackFrameBytes,
+ frame.Camera.ReturnAddress, unchecked(projectionBreakpoint.Address + 1),
+ true,
+ out candidateProjectionEvidence);
+ bool selectedProjectionScope = selectedCamera == null
+ ? !frame.SelectedCameraRequested
+ : (selectedCamera.Applied
+ ? IsSelectedRenderInvocation(process, stoppedContext, eventTid,
+ selectedCamera, unchecked(projectionBreakpoint.Address + 1),
+ true,
+ out selectedProjectionEvidence)
+ : selectedCandidateScope);
+ if (!selectedProjectionScope)
+ {
+ if (selectedCamera != null && !selectedCamera.ProjectionGateDiagnosticLogged)
+ {
+ selectedCamera.ProjectionGateDiagnosticLogged = true;
+ Log("PROJECTION_SKIPPED_OUTSIDE_SELECTED_RENDER_INVOCATION tid=" + eventTid
+ + " applied=" + selectedCamera.Applied
+ + " candidateGeneration=" + selectedCamera.CandidateGeneration
+ + " candidateTid=" + selectedCamera.CandidateThreadId
+ + " candidateCamera=0x" + selectedCamera.CandidateCameraPointer.ToString("X8")
+ + " selectedGeneration=" + selectedCamera.Generation
+ + " selectedTid=" + selectedCamera.ThreadId
+ + " selectedCamera=0x" + selectedCamera.CameraPointer.ToString("X8")
+ + " candidateEvidence=" + candidateProjectionEvidence
+ + " selectedEvidence=" + selectedProjectionEvidence);
+ }
+ }
+ else
+ {
+ frame.Projection = null;
+ frame.ProjectionGeneration = 0;
+ if (frame.Camera != null && frame.CameraThreadId == eventTid && ngiBase != 0)
+ {
+ bool cameraWordsCurrent = RefreshCameraWordsAtProjection(process, frame.Camera,
+ terrainBase, frame.CameraGeneration);
+ frame.Projection = ReadProjection(process, ngiBase);
+ frame.ProjectionThreadId = eventTid;
+ frame.ProjectionGeneration = frame.CameraGeneration;
+ frame.WorldGameTimeWordReadable = false;
+ try
+ {
+ frame.WorldGameTimeWord = ReadU32Exact(process, unchecked(world3dBase + 0x32A38));
+ frame.WorldGameTimeWordReadable = true;
+ }
+ catch (Exception clockError)
+ {
+ Log("PROJECTION_WORLD_GAME_TIME_UNREADABLE " + clockError.Message);
+ }
+ PairAtmosphereToProjection(frame, eventTid);
+ bool cameraMatrixMatchesInput = selectedCamera == null || !selectedCamera.Applied
+ || SelectedMatrixMatchesProjection(frame);
+ bool projectionMatchesInput = selectedCamera == null
+ || ProjectionMatchesCameraInput(frame.Projection, selectedCamera.Input)
+ && cameraMatrixMatchesInput;
+ if (selectedCamera != null)
+ {
+ if (selectedCamera.Applied)
+ selectedCamera.ProjectionMatchesInput = projectionMatchesInput;
+ else
+ {
+ selectedCamera.CandidateProjectionVerified = cameraWordsCurrent
+ && frame.Projection.Verified && frame.Projection.Mode != 0
+ && projectionMatchesInput;
+ Log("SELECTED_CAMERA_PREFLIGHT_PROJECTION verified="
+ + selectedCamera.CandidateProjectionVerified
+ + " generation=" + selectedCamera.CandidateGeneration
+ + " camera=0x" + selectedCamera.CandidateCameraPointer.ToString("X8")
+ + " tid=" + selectedCamera.CandidateThreadId);
+ }
+ Log("SELECTED_CAMERA_PROJECTION_MATCH " + projectionMatchesInput
+ + " cameraMatrixMatchesInput=" + cameraMatrixMatchesInput
+ + " inputViewport=" + String.Join(",", selectedCamera.Input.Viewport)
+ + " actualViewport=" + (frame.Projection.Viewport == null ? "unreadable" : String.Join(",", frame.Projection.Viewport))
+ + " inputNear=" + selectedCamera.Input.Near.ToString("R", CultureInfo.InvariantCulture)
+ + " actualNear=" + frame.Projection.Near.ToString("R", CultureInfo.InvariantCulture)
+ + " inputFar=" + selectedCamera.Input.Far.ToString("R", CultureInfo.InvariantCulture)
+ + " actualFar=" + frame.Projection.Far.ToString("R", CultureInfo.InvariantCulture)
+ + " inputFov=" + selectedCamera.Input.FieldOfView.ToString("R", CultureInfo.InvariantCulture)
+ + " actualFov=" + frame.Projection.Fov.ToString("R", CultureInfo.InvariantCulture));
+ }
+ Log("PROJECTION_GENERATION " + frame.ProjectionGeneration + " tid=" + eventTid
+ + " renderer=0x" + frame.Projection.Renderer.ToString("X8")
+ + " viewport=" + (frame.Projection.Viewport == null ? "unreadable" : String.Join(",", frame.Projection.Viewport))
+ + " mode=" + frame.Projection.Mode + " cameraWordsCurrent=" + cameraWordsCurrent
+ + " usable=" + (cameraWordsCurrent && frame.Projection.Verified && frame.Projection.Mode != 0
+ && projectionMatchesInput)
+ + " rawWorldTime=0x" + frame.WorldGameTimeWord.ToString("X8"));
+ if (cameraWordsCurrent && frame.Projection.Verified && frame.Projection.Mode != 0
+ && projectionMatchesInput && ngiBase != 0 && presentBoundaryVerified)
+ EnsurePresentBreakpoint(process, ngiBase, frame, eventTid,
+ presentOriginalByte, ref presentBreakpoint);
+ }
+ }
+ if (frame.Camera == null || frame.CameraThreadId != eventTid || ngiBase == 0)
+ Log("PROJECTION_SKIPPED without loaded renderer and same-thread camera snapshot tid=" + eventTid);
+ }
+ else if (hitBreakpoint == returnBreakpoint)
+ {
+ if (selectedCamera != null && selectedCamera.Applied && !selectedCamera.Restored)
+ {
+ string returnEvidence;
+ // World3D+0x13D47 repurposes ESI for registry traversal before this epilogue.
+ bool returnInvocationMatches = IsSelectedRenderInvocation(process,
+ stoppedContext, eventTid, selectedCamera,
+ unchecked(returnBreakpoint.Address + 1), false, out returnEvidence);
+ if (!returnInvocationMatches && !selectedCamera.ReturnGateDiagnosticLogged)
+ {
+ selectedCamera.ReturnGateDiagnosticLogged = true;
+ Log("SELECTED_CAMERA_RETURN_GATE_REJECTED " + returnEvidence
+ + " selectedGeneration=" + selectedCamera.Generation
+ + " selectedTid=" + selectedCamera.ThreadId
+ + " selectedCamera=0x" + selectedCamera.CameraPointer.ToString("X8"));
+ }
+ if (returnInvocationMatches)
+ {
+ try
+ {
+ byte[] selectedAtReturn = ReadCameraMatrix(process, selectedCamera.CameraPointer);
+ selectedCamera.MatrixIntactAtReturn = ByteArraysEqual(selectedAtReturn,
+ selectedCamera.Input.MatrixBytes);
+ }
+ catch (Exception matrixReadError)
+ {
+ selectedCamera.MatrixIntactAtReturn = false;
+ Log("SELECTED_CAMERA_RETURN_MATRIX_READ_FAILED " + matrixReadError.Message);
+ }
+ if (!selectedCamera.MatrixIntactAtReturn)
+ {
+ selectedCamera.PixelAttributionInvalidated = true;
+ selectedCamera.PixelAttributionFailure = "Selected camera matrix changed or became unreadable before its verified render return.";
+ captureFailure = selectedCamera.PixelAttributionFailure;
+ stopAfterAttempt = true;
+ Log("SELECTED_CAMERA_CHANGED_BEFORE_RETURN; restoring the original native matrix but rejecting pixels");
+ }
+ cameraSetter = StartCameraSetter(process, eventTid, terrainBase,
+ selectedCamera.CameraPointer, selectedCamera.OriginalMatrixBytes,
+ stoppedContext, returnBreakpoint, false, CameraSetterPhase.Restore,
+ selectedCamera.EntryEsp, selectedCamera.ReturnAddress);
+ Log("SELECTED_CAMERA_RESTORE_STARTED generation=" + selectedCamera.Generation
+ + " camera=0x" + selectedCamera.CameraPointer.ToString("X8")
+ + " returnEsp=0x" + BitConverter.ToUInt32(stoppedContext, ContextEspOffset).ToString("X8")
+ + " matrixIntactAtReturn=" + selectedCamera.MatrixIntactAtReturn);
+ }
+ }
+ }
+ else if (hitBreakpoint == atmosphereBreakpoint)
+ {
+ try
+ {
+ AtmosphereReadback sample = CaptureAtmospherePhase(process, terrainBase,
+ world3dBase, stoppedContext, eventTid, terrainModuleSha256, frame);
+ frame.AtmosphereByThread[eventTid] = sample;
+ Log("ATMOSPHERE_PHASE_SAMPLE tid=" + eventTid
+ + " cameraGenerationAtSample=" + sample.CameraGenerationAtSample
+ + " cameraThreadAtSample=" + sample.CameraThreadIdAtSample
+ + " esi=0x" + sample.AtmosphereObject.ToString("X8")
+ + " ebpRaw=0x" + sample.RawClock.ToString("X8")
+ + " edxPhaseMs=" + sample.PhaseMilliseconds
+ + " origin=[esi+0x144]=" + sample.Origin
+ + " periodMs=[esi+0x150]=" + sample.PeriodMilliseconds
+ + " recomputedPhaseMs=" + sample.RecomputedPhaseMilliseconds
+ + " worldGameTime=0x" + sample.WorldGameTimeWord.ToString("X8")
+ + " arithmeticVerified=" + sample.ArithmeticVerified
+ + " rawMatchesWorldTime=" + sample.WorldTimeMatchesRawClock
+ + " terrainHashVerified=" + sample.TerrainModuleHashVerified);
+ }
+ catch (Exception atmosphereError)
+ {
+ Log("ATMOSPHERE_PHASE_SAMPLE_REJECTED tid=" + eventTid + " " + atmosphereError.Message);
+ }
+ }
+ else if (hitBreakpoint == missionBreakpoint)
+ {
+ missionProbeAttempts++;
+ string missionPath;
+ string missionEvidence;
+ bool found = CaptureMissionIdentityAtCall(process, iron3dBase, stoppedContext,
+ eventTid, out missionPath, out missionEvidence);
+ if (found)
+ {
+ frame.MissionPath = missionPath;
+ frame.MissionEvidence = missionEvidence;
+ missionProbeFinished = true;
+ }
+ else if (missionProbeAttempts >= MissionProbeAttemptLimit)
+ {
+ missionProbeFinished = true;
+ frame.MissionEvidence = "verified callsite reached " + missionProbeAttempts
+ + " times, but no validated mission path was found; " + missionEvidence;
+ }
+ else frame.MissionEvidence = "verified callsite reached " + missionProbeAttempts
+ + " times; no validated mission path yet; " + missionEvidence;
+ Log("MISSION_IDENTITY_PROBE attempt=" + missionProbeAttempts + "/"
+ + MissionProbeAttemptLimit + " found=" + found
+ + " path=" + (missionPath ?? "unknown") + " evidence=" + missionEvidence);
+ }
+ else if (hitBreakpoint == presentBreakpoint)
+ {
+ if (CanArmPresentForFrame(frame, eventTid))
+ {
+ try { remote = StartRemoteReadback(process, eventTid, ngiBase, presentBreakpoint, frame, outputPath); }
+ catch (Exception startError)
+ {
+ captureFailure = startError.Message;
+ Log("REMOTE_CAPTURE_START_FAILED " + startError);
+ stopAfterAttempt = true;
+ }
+ }
+ else Log("PRESENT_SKIPPED without matching verified perspective camera/projection");
+ }
+
+ if (remote == null && cameraSetter == null)
+ {
+ bool rearmAfterStep = hitBreakpoint != presentBreakpoint
+ && !(hitBreakpoint == missionBreakpoint && missionProbeFinished);
+ BeginSingleStep(process, eventTid, hitBreakpoint, stoppedContext,
+ out pendingStep, rearmAfterStep);
+ Log("SINGLE_STEP_STARTED boundary=" + hitBreakpoint.Name + " tid=" + eventTid);
+ }
+ }
+ else
+ {
+ continueStatus = DbgContinue;
+ }
+ }
+ else
+ {
+ continueStatus = DbgExceptionNotHandled;
+ }
+ }
+ else if (eventCode == ExitProcessDebugEvent)
+ {
+ uint exitCode = unchecked((uint)Marshal.ReadInt32(eventBuffer, 12));
+ Log("PROCESS_EXIT while capturing exitCode=0x" + exitCode.ToString("X8"));
+ processExited = true;
+ remote = null;
+ shouldStop = true;
+ }
+ }
+ catch (Exception eventError)
+ {
+ Log("FRAME_EVENT_ERROR " + eventError);
+ if (remote != null || cameraSetter != null)
+ {
+ TerminateProcess(process, cameraSetter != null ? 0xE00Cu : 0xE004u);
+ fatal = true;
+ }
+ else if (hitBreakpoint != null && stoppedContext != null)
+ {
+ try
+ {
+ if (hitBreakpoint.Armed)
+ {
+ if (!WriteByte(process, hitBreakpoint.Address, hitBreakpoint.OriginalByte))
+ throw new InvalidOperationException("Could not restore failed boundary byte.");
+ hitBreakpoint.Armed = false;
+ }
+ IntPtr thread = OpenThread(ThreadGetContext | ThreadSetContext, false, eventTid);
+ if (thread == IntPtr.Zero) throw new InvalidOperationException("OpenThread for failed boundary recovery failed.");
+ try { SetFullX86Context(thread, ContextAtBreakpoint(stoppedContext, hitBreakpoint.Address, false)); }
+ finally { CloseHandle(thread); }
+ captureFailure = eventError.Message;
+ stopAfterAttempt = true;
+ Log("FAILED_BOUNDARY_RECOVERED boundary=" + hitBreakpoint.Name);
+ }
+ catch (Exception recoveryError)
+ {
+ Log("FATAL_BOUNDARY_RECOVERY_FAILED " + recoveryError);
+ TerminateProcess(process, 0xE005);
+ fatal = true;
+ }
+ }
+ else
+ {
+ continueStatus = DbgContinue;
+ captureFailure = eventError.Message;
+ stopAfterAttempt = true;
+ Log("FRAME_EVENT_ABORT_RECOVERED eventCode=" + eventCode + " error=" + eventError.Message);
+ }
+ }
+
+ if (!captured && !fatal && !processExited && DateTime.UtcNow >= deadline
+ && remote == null && cameraSetter == null && pendingStep == null)
+ {
+ captureFailure = captureFailure ?? "No matching native camera/projection/present frame completed before the bounded timeout.";
+ Log("NO_FRAME_CAPTURE bounded timeout at stopped debug event");
+ if (!RestoreArmedBreakpointsWhileStopped(process,
+ cameraBreakpoint, projectionBreakpoint, returnBreakpoint, atmosphereBreakpoint, presentBreakpoint, missionBreakpoint))
+ {
+ Log("FATAL_TIMEOUT_BREAKPOINT_RESTORE_FAILED; terminating own verified scratch process");
+ TerminateProcess(process, 0xE007);
+ fatal = true;
+ }
+ else
+ {
+ stopAfterAttempt = true;
+ }
+ }
+
+ if (CanStopCapture(captured, stopAfterAttempt, remote != null || cameraSetter != null,
+ pendingStep != null, selectedCamera != null && selectedCamera.Applied && !selectedCamera.RestoreVerified)
+ && !fatal && !processExited
+ && (cameraBreakpoint != null && cameraBreakpoint.Armed
+ || projectionBreakpoint != null && projectionBreakpoint.Armed
+ || returnBreakpoint != null && returnBreakpoint.Armed
+ || atmosphereBreakpoint != null && atmosphereBreakpoint.Armed
+ || presentBreakpoint != null && presentBreakpoint.Armed
+ || missionBreakpoint != null && missionBreakpoint.Armed))
+ {
+ if (!RestoreArmedBreakpointsWhileStopped(process,
+ cameraBreakpoint, projectionBreakpoint, returnBreakpoint, atmosphereBreakpoint, presentBreakpoint, missionBreakpoint))
+ {
+ Log("FATAL_STOP_BREAKPOINT_RESTORE_FAILED; terminating own verified scratch process");
+ TerminateProcess(process, 0xE008);
+ fatal = true;
+ }
+ }
+
+ if (!ContinueDebugEvent(eventPid, eventTid, continueStatus))
+ {
+ fatal = true;
+ Log("FATAL ContinueDebugEvent failed=" + Marshal.GetLastWin32Error());
+ if (process != IntPtr.Zero && !TerminateProcess(process, 0xE009))
+ Log("FATAL ContinueDebugEvent recovery terminate failed=" + Marshal.GetLastWin32Error());
+ break;
+ }
+ if (shouldStop || CanStopCapture(captured, stopAfterAttempt, remote != null || cameraSetter != null,
+ pendingStep != null, selectedCamera != null && selectedCamera.Applied && !selectedCamera.RestoreVerified)) break;
+ }
+ if (!captured && !fatal && !stopAfterAttempt)
+ {
+ captureFailure = captureFailure ?? "No matching native camera/projection/present frame completed before the bounded timeout.";
+ Log("NO_FRAME_CAPTURE bounded timeout");
+ }
+ }
+ finally
+ {
+ bool cameraMutationUnrestored = selectedCamera != null && selectedCamera.Applied && !selectedCamera.RestoreVerified;
+ if (!processExited && process != IntPtr.Zero && (remote != null || cameraSetter != null || pendingStep != null
+ || cameraMutationUnrestored
+ || cameraBreakpoint != null && cameraBreakpoint.Armed
+ || projectionBreakpoint != null && projectionBreakpoint.Armed
+ || returnBreakpoint != null && returnBreakpoint.Armed
+ || atmosphereBreakpoint != null && atmosphereBreakpoint.Armed
+ || presentBreakpoint != null && presentBreakpoint.Armed
+ || missionBreakpoint != null && missionBreakpoint.Armed))
+ {
+ string state = remote != null ? "REMOTE_STUB_IN_FLIGHT" : (cameraSetter != null ? "CAMERA_SETTER_IN_FLIGHT"
+ : (cameraMutationUnrestored ? "SELECTED_CAMERA_MUTATION_NOT_RESTORED"
+ : (pendingStep != null ? "SINGLE_STEP_IN_FLIGHT" : "ARMED_BREAKPOINTS_WITHOUT_STOPPED_EVENT")));
+ Log("FINAL_" + state + "; terminating only the path/tick/hash-verified scratch process before detach");
+ if (!TerminateProcess(process, 0xE006))
+ Log("FINAL_TERMINATE_FAILED error=" + Marshal.GetLastWin32Error());
+ if (WaitForSingleObject(process, 2000) == 0) processExited = true;
+ else
+ {
+ fatal = true;
+ processExitUnconfirmed = true;
+ Log("FATAL scratch termination was not confirmed before detach");
+ }
+ }
+ if (attached && !processExitUnconfirmed)
+ {
+ detached = DebugActiveProcessStop(processId);
+ if (detached) Log("DEBUG_DETACHED");
+ else
+ {
+ fatal = true;
+ int detachError = Marshal.GetLastWin32Error();
+ Log("FATAL DebugActiveProcessStop failed=" + detachError);
+ if (process != IntPtr.Zero)
+ {
+ uint waitBeforeRecovery = WaitForSingleObject(process, 0);
+ int waitBeforeError = waitBeforeRecovery == 0xFFFFFFFF ? Marshal.GetLastWin32Error() : 0;
+ uint exitBeforeRecovery;
+ bool exitRead = GetExitCodeProcess(process, out exitBeforeRecovery);
+ int exitBeforeError = exitRead ? 0 : Marshal.GetLastWin32Error();
+ Log("PROCESS_STATE_AFTER_DETACH_FAILURE waitResult=0x" + waitBeforeRecovery.ToString("X8")
+ + " exitCode=" + (exitRead ? "0x" + exitBeforeRecovery.ToString("X8") : "unreadable")
+ + " waitError=" + waitBeforeError + " exitError=" + exitBeforeError);
+ if (waitBeforeRecovery == 0x00000102 && exitRead && exitBeforeRecovery == 0x00000103)
+ {
+ Log("DETACH_FAILURE_TARGET_STILL_ACTIVE; terminating only exact verified scratch after state snapshot");
+ if (!TerminateProcess(process, 0xE00A))
+ {
+ int terminateError = Marshal.GetLastWin32Error();
+ Log("DETACH_FAILURE_TERMINATE_FAILED error=" + terminateError);
+ }
+ uint waitAfterRecovery = WaitForSingleObject(process, 2000);
+ int waitAfterError = waitAfterRecovery == 0xFFFFFFFF ? Marshal.GetLastWin32Error() : 0;
+ uint exitAfterRecovery;
+ bool exitAfterRead = GetExitCodeProcess(process, out exitAfterRecovery);
+ int exitAfterError = exitAfterRead ? 0 : Marshal.GetLastWin32Error();
+ Log("PROCESS_STATE_AFTER_DETACH_FAILURE_RECOVERY waitResult=0x" + waitAfterRecovery.ToString("X8")
+ + " exitCode=" + (exitAfterRead ? "0x" + exitAfterRecovery.ToString("X8") : "unreadable")
+ + " waitError=" + waitAfterError + " exitError=" + exitAfterError);
+ if (waitAfterRecovery == 0) processExited = true;
+ else processExitUnconfirmed = true;
+ }
+ }
+ }
+ }
+ else if (attached)
+ {
+ Log("FATAL_DEBUG_DETACH_SKIPPED_PROCESS_STILL_RUNNING");
+ }
+ if (detached && process != IntPtr.Zero)
+ {
+ uint waitResult = WaitForSingleObject(process, 2000);
+ uint exitCode;
+ if (GetExitCodeProcess(process, out exitCode))
+ Log((waitResult == 0 ? "PROCESS_EXIT_AFTER_DETACH" : "PROCESS_STATE_AFTER_DETACH")
+ + " waitResult=0x" + waitResult.ToString("X8") + " exitCode=0x" + exitCode.ToString("X8"));
+ else Log("PROCESS_STATE_AFTER_DETACH unreadable error=" + Marshal.GetLastWin32Error());
+ }
+ breakpointsRestored = processExited || AreBreakpointsRestored(cameraBreakpoint,
+ projectionBreakpoint, returnBreakpoint, atmosphereBreakpoint, presentBreakpoint, missionBreakpoint);
+ Log("FINAL_BREAKPOINT_STATE restored=" + breakpointsRestored + " processExited=" + processExited);
+ if (eventBuffer != IntPtr.Zero) Marshal.FreeHGlobal(eventBuffer);
+ if (process != IntPtr.Zero) CloseHandle(process);
+ }
+
+ if (!detached) throw new InvalidOperationException("Debug detach not confirmed; inspect the frame log before reuse.");
+ if (fatal) throw new InvalidOperationException("The native frame capture could not recover safely; inspect the frame log.");
+ if (captured)
+ {
+ bool pngCreated = false;
+ bool jsonCreated = false;
+ string outputPng = Path.ChangeExtension(outputPath, ".png");
+ try
+ {
+ if (!breakpointsRestored)
+ throw new InvalidOperationException("The staged pixels cannot be published because breakpoint restoration was not confirmed.");
+ if (completedSurface == null || completedFrame == null || completedReadback == null)
+ throw new InvalidOperationException("The capture was marked ready without a staged frame and readback record.");
+ if (!IsMatchingPerspectiveFrame(completedFrame, completedReadback.ThreadId))
+ throw new InvalidOperationException("The staged pixels lost their verified camera or projection attribution before finalization.");
+ string json = UsableFrameJson(expectedPath, world3dBase, ngiBase, completedFrame,
+ completedSurface, outputPng, completedReadback);
+ SaveSurfacePng(completedSurface, outputPng);
+ pngCreated = true;
+ WriteTextCreateNew(outputPath, json);
+ jsonCreated = true;
+ }
+ catch (Exception outputError)
+ {
+ if (jsonCreated) try { File.Delete(outputPath); } catch { }
+ if (pngCreated) try { File.Delete(outputPng); } catch { }
+ captureFailure = "Could not safely finalize the detached frame output: " + outputError.Message;
+ stopAfterAttempt = true;
+ captured = false;
+ Log("FRAME_OUTPUT_FAILED " + outputError);
+ }
+ if (captured)
+ {
+ Log("FRAME_OUTPUT_COMPLETE generation=" + completedReadback.Generation + " png=" + outputPng
+ + " json=" + outputPath + " size=" + completedSurface.Width + "x" + completedSurface.Height
+ + " bitCount=" + completedSurface.BitCount + " rowsSha256=" + completedSurface.Sha256
+ + " afterContextRestore=true breakpointsRestored=true detached=true");
+ Console.WriteLine("native frame captured: " + outputPath + " and " + outputPng);
+ return 0;
+ }
+ }
+ if (captureFailure != null) Console.Error.WriteLine(captureFailure);
+ return stopAfterAttempt ? 4 : 3;
+ }
+
+ private static BreakpointInfo ArmBreakpoint(IntPtr process, string name, uint address, byte expectedByte)
+ {
+ byte[] current = new byte[1];
+ if (!Read(process, address, current) || current[0] != expectedByte)
+ throw new InvalidOperationException(name + " signature byte changed before arming at 0x" + address.ToString("X8"));
+ BreakpointInfo result = new BreakpointInfo();
+ result.Name = name;
+ result.Address = address;
+ result.OriginalByte = current[0];
+ if (!WriteByte(process, address, 0xCC)) throw new InvalidOperationException("Could not arm " + name + " at 0x" + address.ToString("X8"));
+ result.Armed = true;
+ Log("BREAKPOINT_ARMED " + name + " address=0x" + address.ToString("X8") + " original=0x" + current[0].ToString("X2"));
+ return result;
+ }
+
+ private static void EnsurePresentBreakpoint(IntPtr process, uint ngiBase, FrameSnapshot frame,
+ uint threadId, byte expectedByte, ref BreakpointInfo presentBreakpoint)
+ {
+ if (!CanArmPresentForFrame(frame, threadId)) return;
+ uint address = unchecked(ngiBase + PresentBoundaryRva);
+ if (presentBreakpoint == null)
+ {
+ presentBreakpoint = ArmBreakpoint(process, "Ngi32.windowed-present", address, expectedByte);
+ return;
+ }
+ if (presentBreakpoint.Address != address || presentBreakpoint.OriginalByte != expectedByte)
+ throw new InvalidOperationException("The verified Ngi32 present boundary changed during this capture.");
+ if (presentBreakpoint.Armed) return;
+ byte[] current = new byte[1];
+ if (!Read(process, address, current) || current[0] != presentBreakpoint.OriginalByte)
+ throw new InvalidOperationException("Ngi32 present signature changed before re-arming.");
+ if (!WriteByte(process, address, 0xCC))
+ throw new InvalidOperationException("Could not re-arm Ngi32 present boundary.");
+ presentBreakpoint.Armed = true;
+ Log("BREAKPOINT_REARMED Ngi32.windowed-present address=0x" + address.ToString("X8"));
+ }
+
+ private static bool RestoreArmedBreakpointsWhileStopped(IntPtr process,
+ BreakpointInfo cameraBreakpoint, BreakpointInfo projectionBreakpoint, BreakpointInfo returnBreakpoint,
+ BreakpointInfo atmosphereBreakpoint, BreakpointInfo presentBreakpoint, BreakpointInfo missionBreakpoint)
+ {
+ return RestoreBreakpointWhileStopped(process, cameraBreakpoint)
+ && RestoreBreakpointWhileStopped(process, projectionBreakpoint)
+ && RestoreBreakpointWhileStopped(process, returnBreakpoint)
+ && RestoreBreakpointWhileStopped(process, atmosphereBreakpoint)
+ && RestoreBreakpointWhileStopped(process, presentBreakpoint)
+ && RestoreBreakpointWhileStopped(process, missionBreakpoint);
+ }
+
+ private static bool AreBreakpointsRestored(BreakpointInfo cameraBreakpoint,
+ BreakpointInfo projectionBreakpoint, BreakpointInfo returnBreakpoint,
+ BreakpointInfo atmosphereBreakpoint, BreakpointInfo presentBreakpoint,
+ BreakpointInfo missionBreakpoint)
+ {
+ return (cameraBreakpoint == null || !cameraBreakpoint.Armed)
+ && (projectionBreakpoint == null || !projectionBreakpoint.Armed)
+ && (returnBreakpoint == null || !returnBreakpoint.Armed)
+ && (atmosphereBreakpoint == null || !atmosphereBreakpoint.Armed)
+ && (presentBreakpoint == null || !presentBreakpoint.Armed)
+ && (missionBreakpoint == null || !missionBreakpoint.Armed);
+ }
+
+ private static bool RestoreBreakpointWhileStopped(IntPtr process, BreakpointInfo breakpoint)
+ {
+ if (breakpoint == null || !breakpoint.Armed) return true;
+ if (!WriteByte(process, breakpoint.Address, breakpoint.OriginalByte))
+ {
+ Log("FATAL breakpoint restore failed while target stopped: " + breakpoint.Name
+ + " error=" + Marshal.GetLastWin32Error());
+ return false;
+ }
+ breakpoint.Armed = false;
+ Log("BREAKPOINT_RESTORED_WHILE_STOPPED " + breakpoint.Name + " address=0x" + breakpoint.Address.ToString("X8"));
+ return true;
+ }
+}