Files
fparkan/tools/native-frame-capture/NativeFrameCapture.cs
T

2526 lines
157 KiB
C#
Raw Normal View History

2026-10-11 17:37:48 +04:00
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;
}
}