aboutsummaryrefslogtreecommitdiff
path: root/tools/native-frame-capture/NativeFrameCapture.Readback.cs
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-10-18 13:29:24 +0300
committerValentin Popov <valentin@popov.link>2026-10-18 13:29:24 +0300
commit76e5737b1dd047aede7712a6143fef61654df669 (patch)
tree77a897f25316b6cf434f0aa5bc46dd609a60e7b4 /tools/native-frame-capture/NativeFrameCapture.Readback.cs
parent13f563bb79228fdb075698c4ed79deea3cc65301 (diff)
downloadfparkan-76e5737b1dd047aede7712a6143fef61654df669.tar.xz
fparkan-76e5737b1dd047aede7712a6143fef61654df669.zip
Support parented camera capture matrices
Diffstat (limited to 'tools/native-frame-capture/NativeFrameCapture.Readback.cs')
-rw-r--r--tools/native-frame-capture/NativeFrameCapture.Readback.cs563
1 files changed, 530 insertions, 33 deletions
diff --git a/tools/native-frame-capture/NativeFrameCapture.Readback.cs b/tools/native-frame-capture/NativeFrameCapture.Readback.cs
index 872487b..35b1844 100644
--- a/tools/native-frame-capture/NativeFrameCapture.Readback.cs
+++ b/tools/native-frame-capture/NativeFrameCapture.Readback.cs
@@ -14,6 +14,7 @@ using System.Drawing.Imaging;
internal static partial class NativeFrameCapture
{
private const uint NgiRendererGlobalRva = 0x3A460;
+ private const uint NgiLastErrorGlobalRva = 0x3A464;
private const uint NgiDeviceGlobalRva = 0x3A488;
private const uint NgiRendererVtableRva = 0x315E0;
private const uint NgiWindowedPresentRva = 0x6E1B;
@@ -55,9 +56,12 @@ internal static partial class NativeFrameCapture
private const uint CameraSetterMatrixOffset = 64;
private const uint CameraTransformInterfaceVtableRva = 0x66558;
private const uint CameraTransformSetterRva = 0x54C70;
+ private const uint CameraTransformGetterRva = 0x55110;
private const uint CameraTransformInvalidatorRva = 0x55280;
private const uint CameraTransformSetterSlotOffset = 0x1C;
+ private const uint CameraTransformGetterSlotOffset = 0x20;
private const uint CameraTransformInvalidatorSlotOffset = 0x30;
+ private const uint CameraSetterWorldMatrixPointerOffset = 128;
private const uint RenderFunctionStackFrameBytes = 0x70;
[StructLayout(LayoutKind.Sequential)]
@@ -125,6 +129,8 @@ internal static partial class NativeFrameCapture
private sealed class CameraReadback
{
public uint Camera;
+ public uint Parent;
+ public bool Parented;
public uint Vtable;
public uint TransformInterface;
public uint TransformVtable;
@@ -133,13 +139,21 @@ internal static partial class NativeFrameCapture
public uint ReturnAddress;
public uint SelectorField;
public uint[] Words;
+ public uint[] EffectiveWorldWords;
public byte[] EntryMatrixBytes;
public byte[] ProjectionMatrixBytes;
+ public byte[] EntryEffectiveWorldMatrixBytes;
+ public byte[] ProjectionEffectiveWorldMatrixBytes;
public bool CurrentAtProjectionVerified;
+ public bool EffectiveWorldAtProjectionVerified;
public bool WordsChangedAtProjection;
+ public bool EffectiveWorldChangedAtProjection;
public string EntryMatrixSha256;
public string ProjectionMatrixSha256;
+ public string EntryEffectiveWorldMatrixSha256;
+ public string ProjectionEffectiveWorldMatrixSha256;
public bool MatrixFinite;
+ public bool EffectiveWorldMatrixFinite;
public bool LayoutVerified;
public string Failure;
}
@@ -147,8 +161,13 @@ internal static partial class NativeFrameCapture
private sealed class SelectedCameraInput
{
public string Path;
+ public bool Synthetic;
+ public bool ParentedCamera;
+ public bool UsedEffectiveWorldMatrix;
public byte[] MatrixBytes;
public uint[] MatrixWords;
+ public byte[] Selector0MatrixBytes;
+ public uint[] Selector0MatrixWords;
public string MatrixSha256;
public int[] Viewport;
public float Near;
@@ -539,17 +558,92 @@ internal static partial class NativeFrameCapture
return result;
}
- private static RemoteCode BuildCameraSetterStub(uint allocationBase, uint transformInterface)
+ // Boundary tracing only asks the already-active D3D7 device for its current
+ // render target, records the returned interface pointer, and balances the
+ // COM AddRef with Release. It deliberately never locks or reads pixels.
+ private static RemoteCode BuildSurfaceProbeStub(uint allocationBase, uint device)
{
+ uint data = unchecked(allocationBase + RemoteDataOffset);
+ uint statusAddress = unchecked(data + RemoteDataStatusOffset);
+ uint getHrAddress = unchecked(data + RemoteDataGetHrOffset);
+ uint releaseAddress = unchecked(data + RemoteDataReleaseCountOffset);
+ uint surfaceAddress = unchecked(data + RemoteDataSurfaceOffset);
+ X86CodeBuilder code = new X86CodeBuilder();
+ code.Emit(0xBE); code.EmitU32(device); // mov esi, device
+ code.Emit(0x8B, 0x06); // mov eax, [esi]
+ code.Emit(0x8D, 0x3D); code.EmitU32(surfaceAddress); // lea edi, [surface]
+ code.PushReg(0x57); code.PushReg(0x56); // push output; push this
+ code.CallStdCall(8, 0xFF, 0x50, (byte)D3dDeviceGetRenderTargetOffset);
+ code.Emit(0xA3); code.EmitU32(getHrAddress); // mov [getHr], eax
+ code.Emit(0x85, 0xC0); // test eax,eax
+ code.JumpIf(0x89, "get_succeeded"); // jns
+ code.Emit(0xC7, 0x05); code.EmitU32(statusAddress); code.EmitU32(9);
+ code.Jump("release_surface");
+ code.Mark("get_succeeded");
+ code.Emit(0x8B, 0x35); code.EmitU32(surfaceAddress); // mov esi,[surface]
+ code.Emit(0x85, 0xF6); // test esi,esi
+ code.JumpIf(0x84, "no_surface");
+ code.Emit(0xC7, 0x05); code.EmitU32(statusAddress); code.EmitU32(8);
+ code.Jump("release_surface");
+ code.Mark("no_surface");
+ code.Emit(0xC7, 0x05); code.EmitU32(statusAddress); code.EmitU32(10);
+ code.Jump("stop");
+ code.Mark("release_surface");
+ code.Emit(0x8B, 0x35); code.EmitU32(surfaceAddress);
+ code.Emit(0x85, 0xF6);
+ code.JumpIf(0x84, "stop");
+ code.Emit(0x8B, 0x06);
+ code.PushReg(0x56);
+ code.CallStdCall(4, 0xFF, 0x50, (byte)DdSurfaceReleaseOffset);
+ code.Emit(0xA3); code.EmitU32(releaseAddress);
+ code.Mark("stop");
+ code.EmitTerminalInt3();
+ byte[] bytes = code.Finish();
+ code.AssertTerminalControlFlow(bytes, 6);
+ if (code.MaxCallStackBytes != 12 || bytes.Length > RemoteCodePage)
+ throw new InvalidOperationException("Surface probe stub has an unexpected stack footprint or size.");
+ RemoteCode result = new RemoteCode();
+ result.Base = allocationBase;
+ result.DataBase = data;
+ result.Bytes = bytes;
+ result.CallInstructionOffsets = code.CallInstructionOffsets;
+ result.MaxCallStackBytes = code.MaxCallStackBytes;
+ result.TrapAddress = unchecked(allocationBase + (uint)code.TerminalOffset);
+ return result;
+ }
+
+ private static byte CameraSetterMode(CameraSetterPhase phase)
+ {
+ // Native setter mode 2 treats the supplied matrix as world space and
+ // converts it relative to camera+0x0C. Restoring a captured +0x20
+ // selector-0 value must use local mode 1.
+ return phase == CameraSetterPhase.Apply ? (byte)2 : (byte)1;
+ }
+
+ private static RemoteCode BuildCameraSetterStub(uint allocationBase, uint transformInterface, byte mode)
+ {
+ if (mode != 1 && mode != 2)
+ throw new ArgumentOutOfRangeException("mode", "Only verified local and world camera setter modes are supported.");
uint dataBase = unchecked(allocationBase + RemoteDataOffset);
uint matrixAddress = unchecked(dataBase + CameraSetterMatrixOffset);
X86CodeBuilder code = new X86CodeBuilder();
code.Emit(0xBE); code.EmitU32(transformInterface); // mov esi, transform interface
code.Emit(0x8B, 0x0E); // mov ecx, [esi] (verified vtable)
code.PushImm32(matrixAddress); // argument 3: 64-byte transform
- code.PushImm8(1); // argument 2: mode 1
+ code.PushImm8(mode); // argument 2: native transform space
code.PushReg(0x56); // argument 1: this
code.CallStdCall(12, 0xFF, 0x51, (byte)CameraTransformSetterSlotOffset);
+ if (mode == 2)
+ {
+ // Mode 2 stores a parent-relative local matrix and invalidates the
+ // effective-world cache at transformInterface+0x11C (camera+0x120). The selector-2 getter
+ // recomputes camera+0x60 before the caller verifies the new pose.
+ code.Emit(0x8B, 0x0E); // mov ecx, [esi] (verified vtable)
+ code.PushImm32(unchecked(dataBase + CameraSetterWorldMatrixPointerOffset));
+ code.PushImm8(2); // selector 2: effective world matrix
+ code.PushReg(0x56); // argument 1: transform interface
+ code.CallStdCall(12, 0xFF, 0x51, (byte)CameraTransformGetterSlotOffset);
+ }
code.EmitTerminalInt3();
byte[] bytes = code.Finish();
code.AssertTerminalControlFlow(bytes, 0);
@@ -575,16 +669,19 @@ internal static partial class NativeFrameCapture
bool modeRead = ReadExact(process, unchecked(camera + 0x1A0), modeBytes);
uint transformVtable = ReadU32(process, transformInterface);
uint setter = ReadU32(process, unchecked(transformVtable + CameraTransformSetterSlotOffset));
+ uint getter = ReadU32(process, unchecked(transformVtable + CameraTransformGetterSlotOffset));
uint invalidate = ReadU32(process, unchecked(transformVtable + CameraTransformInvalidatorSlotOffset));
bool valid = terrainBase != 0
&& primaryVtable == unchecked(terrainBase + ExternalCameraVtableRva)
&& transformVtable == unchecked(terrainBase + CameraTransformInterfaceVtableRva)
&& setter == unchecked(terrainBase + CameraTransformSetterRva)
+ && getter == unchecked(terrainBase + CameraTransformGetterRva)
&& invalidate == unchecked(terrainBase + CameraTransformInvalidatorRva)
&& selector == 0xFFFFFFFF && modeRead && modeBytes[0] == 0;
evidence = "camera=0x" + camera.ToString("X8") + " primaryVtable=0x" + primaryVtable.ToString("X8")
+ " transform=0x" + transformInterface.ToString("X8") + " transformVtable=0x" + transformVtable.ToString("X8")
- + " setter=0x" + setter.ToString("X8") + " invalidator=0x" + invalidate.ToString("X8")
+ + " setter=0x" + setter.ToString("X8") + " getter=0x" + getter.ToString("X8")
+ + " invalidator=0x" + invalidate.ToString("X8")
+ " selector=0x" + selector.ToString("X8")
+ " mode=" + (modeRead ? modeBytes[0].ToString(CultureInfo.InvariantCulture) : "unreadable")
+ " valid=" + valid;
@@ -603,8 +700,11 @@ internal static partial class NativeFrameCapture
uint transformInterface;
if (!VerifyCameraSetterAbi(process, terrainBase, camera, out transformInterface, out abiEvidence))
throw new InvalidOperationException("Camera setter ABI refused: " + abiEvidence);
+ byte setterMode = CameraSetterMode(phase);
Log("verified native camera setter ABI " + abiEvidence + " slot=+0x1C this=0x"
- + transformInterface.ToString("X8") + " mode=1 matrixBytes=64 ret=0x0C");
+ + transformInterface.ToString("X8") + " mode=" + setterMode
+ + " matrixSpace=" + (phase == CameraSetterPhase.Apply ? "world" : "local")
+ + " matrixBytes=64 ret=0x0C");
uint expectedEip = unchecked(resumeBreakpoint.Address + 1);
uint originalEip = BitConverter.ToUInt32(originalContext, ContextEipOffset);
@@ -639,7 +739,7 @@ internal static partial class NativeFrameCapture
{
uint safeEsp = ComputeSafeNativeStackPointer(process, thread, originalContext);
region = AllocateReadbackRegion(process);
- RemoteCode code = BuildCameraSetterStub(region, transformInterface);
+ RemoteCode code = BuildCameraSetterStub(region, transformInterface, setterMode);
WriteRemoteStub(process, code, false);
UIntPtr written;
if (!WriteProcessMemory(process, Ptr(unchecked(code.DataBase + CameraSetterMatrixOffset)), matrixBytes,
@@ -732,6 +832,46 @@ internal static partial class NativeFrameCapture
return matrix;
}
+ private static uint ReadCameraParent(IntPtr process, uint camera)
+ {
+ if (camera == 0 || camera > UInt32.MaxValue - 0x0C)
+ throw new InvalidOperationException("Camera pointer is outside the verified parent-field range.");
+ return ReadU32(process, unchecked(camera + 0x0C));
+ }
+
+ private static byte[] ReadEffectiveWorldCameraMatrix(IntPtr process, uint camera)
+ {
+ byte[] matrix = new byte[64];
+ if (camera == 0 || camera > UInt32.MaxValue - 0x60
+ || !ReadExact(process, unchecked(camera + 0x60), matrix))
+ throw new InvalidOperationException("Could not read the native effective-world camera matrix at camera+0x60.");
+ return matrix;
+ }
+
+ private static uint[] MatrixWords(byte[] matrixBytes)
+ {
+ if (matrixBytes == null || matrixBytes.Length != 64)
+ throw new ArgumentException("A camera matrix must contain exactly 64 bytes.", "matrixBytes");
+ uint[] words = new uint[16];
+ for (int i = 0; i < words.Length; i++)
+ words[i] = BitConverter.ToUInt32(matrixBytes, i * 4);
+ return words;
+ }
+
+ private static bool CameraMatricesNearlyEqual(byte[] left, byte[] right)
+ {
+ if (left == null || right == null || left.Length != 64 || right.Length != 64) return false;
+ const double absoluteTolerance = 0.001;
+ for (int i = 0; i < 16; i++)
+ {
+ float a = BitConverter.ToSingle(left, i * 4);
+ float b = BitConverter.ToSingle(right, i * 4);
+ if (!IsFinite(a) || !IsFinite(b) || Math.Abs((double)a - b) > absoluteTolerance)
+ return false;
+ }
+ return true;
+ }
+
private static bool CameraFunctionStackMatches(CameraSetterPhase phase, byte[] stackWords,
uint expectedReturnAddress, uint camera)
{
@@ -891,6 +1031,7 @@ internal static partial class NativeFrameCapture
session.OriginalContext = originalContext;
session.PresentStackArguments = presentStackArguments;
session.PresentStackArgumentsIntact = true;
+ session.ValidatePresentStackArguments = true;
session.RemoteContextIntact = true;
session.PresentBreakpoint = present;
session.Frame = frame;
@@ -915,6 +1056,188 @@ internal static partial class NativeFrameCapture
finally { CloseHandle(thread); }
}
+ private static RemoteReadbackSession StartRemoteBoundaryReadback(IntPtr process, uint threadId,
+ uint ngiBase, BreakpointInfo boundary, FrameSnapshot frame, string outputJson,
+ BreakpointInfo[] allBreakpoints)
+ {
+ uint device;
+ uint getRenderTarget;
+ string evidence;
+ if (!VerifyD3d7GetRenderTarget(process, ngiBase, out device, out getRenderTarget, out evidence))
+ throw new InvalidOperationException("D3D7 EndScene render-target readback was refused: " + evidence);
+ uint renderer = ReadU32Exact(process, unchecked(ngiBase + NgiRendererGlobalRva));
+ if (frame == null || frame.Projection == null || !frame.Projection.Verified
+ || frame.Projection.Renderer != renderer
+ || ReadU32Exact(process, renderer) != unchecked(ngiBase + NgiRendererVtableRva))
+ throw new InvalidOperationException("EndScene readback renderer changed since the selected projection snapshot.");
+ if (ReadU32Exact(process, unchecked(renderer + 0x114)) == 0)
+ throw new InvalidOperationException("Ngi32 is not in the verified windowed render-target path.");
+
+ IntPtr thread = OpenThread(ThreadGetContext | ThreadSetContext | ThreadQueryInformation, false, threadId);
+ if (thread == IntPtr.Zero)
+ throw new InvalidOperationException("OpenThread for selected EndScene readback failed: " + Marshal.GetLastWin32Error());
+ uint region = 0;
+ try
+ {
+ byte[] originalContext = GetFullX86Context(thread);
+ if (boundary == null || BitConverter.ToUInt32(originalContext, ContextEipOffset)
+ != unchecked(boundary.Address + 1))
+ throw new InvalidOperationException("Selected EndScene EIP did not match its verified post-call boundary.");
+ uint safeEsp = ComputeSafeNativeStackPointer(process, thread, originalContext);
+ region = AllocateReadbackRegion(process);
+ RemoteCode acquire = BuildReadbackStub(region, device, false);
+ WriteRemoteStub(process, acquire, true);
+ BreakpointInfo[] suspended;
+ if (!SuspendBreakpointsWhileStopped(process, allBreakpoints, out suspended))
+ throw new InvalidOperationException("Could not suspend every armed breakpoint before EndScene readback.");
+ RemoteReadbackSession session = new RemoteReadbackSession();
+ session.Acquire = acquire;
+ session.Region = region;
+ session.ThreadId = threadId;
+ session.SafeEsp = safeEsp;
+ session.OriginalContext = originalContext;
+ session.PresentStackArgumentsIntact = true;
+ session.ValidatePresentStackArguments = false;
+ session.RemoteContextIntact = true;
+ session.PresentBreakpoint = boundary;
+ session.ResumeBreakpoint = boundary;
+ session.SuspendedBreakpoints = suspended;
+ session.Frame = frame;
+ session.Generation = frame.CameraGeneration;
+ session.OutputJson = outputJson;
+ session.OutputPng = Path.ChangeExtension(outputJson, ".png");
+ session.DeadlineUtc = DateTime.UtcNow.AddSeconds(5);
+ session.Phase = RemoteReadbackPhase.Acquire;
+ session.PixelBoundary = "Ngi32 EndScene after selected World3D projection, before Present";
+ Log("BOUNDARY_READBACK_STARTED kind=" + boundary.Name + " tid=" + threadId
+ + " device=0x" + device.ToString("X8") + " renderer=0x" + renderer.ToString("X8")
+ + " generation=" + session.Generation + " stub=0x" + acquire.Base.ToString("X8")
+ + " suspendedBreakpoints=" + suspended.Length + " presentStackArgs=not-applicable");
+ SetFullX86Context(thread, ContextForRemoteCode(originalContext, acquire.Base, safeEsp));
+ return session;
+ }
+ catch
+ {
+ if (region != 0) VirtualFreeEx(process, Ptr(region), UIntPtr.Zero, 0x8000);
+ throw;
+ }
+ finally { CloseHandle(thread); }
+ }
+
+ private static bool SuspendBreakpointsWhileStopped(IntPtr process, BreakpointInfo[] candidates,
+ out BreakpointInfo[] suspended)
+ {
+ List<BreakpointInfo> active = new List<BreakpointInfo>();
+ if (candidates != null)
+ for (int i = 0; i < candidates.Length; i++)
+ {
+ BreakpointInfo breakpoint = candidates[i];
+ if (breakpoint == null || !breakpoint.Armed || active.Contains(breakpoint)) continue;
+ active.Add(breakpoint);
+ if (!WriteByte(process, breakpoint.Address, breakpoint.OriginalByte))
+ {
+ bool rollbackSucceeded = true;
+ foreach (BreakpointInfo suspendedBreakpoint in active)
+ {
+ if (!WriteByte(process, suspendedBreakpoint.Address, 0xCC))
+ {
+ rollbackSucceeded = false;
+ Log("FATAL boundary probe could not roll back breakpoint suspension for "
+ + suspendedBreakpoint.Name + " error=" + Marshal.GetLastWin32Error());
+ }
+ // Leave this true even after a failed write so final cleanup retries
+ // restoring the original byte before any detach.
+ suspendedBreakpoint.Armed = true;
+ }
+ suspended = active.ToArray();
+ Log("BOUNDARY_PROBE_SUSPEND_FAILED name=" + breakpoint.Name
+ + " rollbackSucceeded=" + rollbackSucceeded);
+ return false;
+ }
+ breakpoint.Armed = false;
+ Log("BOUNDARY_PROBE_BREAKPOINT_SUSPENDED " + breakpoint.Name);
+ }
+ suspended = active.ToArray();
+ return true;
+ }
+
+ private static void RearmSuspendedBreakpoints(IntPtr process, RemoteReadbackSession session)
+ {
+ if (session == null || session.SuspendedBreakpoints == null) return;
+ foreach (BreakpointInfo breakpoint in session.SuspendedBreakpoints)
+ {
+ if (breakpoint == null || breakpoint == session.ResumeBreakpoint) continue;
+ if (!WriteByte(process, breakpoint.Address, 0xCC))
+ throw new InvalidOperationException("Could not re-arm suspended boundary probe breakpoint " + breakpoint.Name);
+ breakpoint.Armed = true;
+ Log("BOUNDARY_PROBE_BREAKPOINT_REARMED " + breakpoint.Name);
+ }
+ if (session.ResumeBreakpoint != null) session.ResumeBreakpoint.Armed = true;
+ }
+
+ private static RemoteReadbackSession StartRemoteSurfaceProbe(IntPtr process, uint threadId,
+ uint ngiBase, BreakpointInfo resumeBreakpoint, FrameSnapshot frame,
+ BreakpointInfo[] allBreakpoints)
+ {
+ uint device;
+ uint getRenderTarget;
+ string evidence;
+ if (!VerifyD3d7GetRenderTarget(process, ngiBase, out device, out getRenderTarget, out evidence))
+ throw new InvalidOperationException("D3D7 surface probe refused: " + evidence);
+ uint renderer = ReadU32Exact(process, unchecked(ngiBase + NgiRendererGlobalRva));
+ if (frame == null || frame.Projection == null || !frame.Projection.Verified
+ || frame.Projection.Renderer != renderer
+ || ReadU32Exact(process, renderer) != unchecked(ngiBase + NgiRendererVtableRva))
+ throw new InvalidOperationException("Surface probe renderer did not match the verified active projection snapshot.");
+ Log("BOUNDARY_PROBE_GET_RENDER_TARGET_ABI " + evidence + " renderer=0x" + renderer.ToString("X8")
+ + " beginScene=" + (ReadU32Exact(process, unchecked(renderer + 0x118)) >> 8 & 0xFF));
+
+ IntPtr thread = OpenThread(ThreadGetContext | ThreadSetContext | ThreadQueryInformation, false, threadId);
+ if (thread == IntPtr.Zero) throw new InvalidOperationException("OpenThread for D3D7 boundary probe failed: " + Marshal.GetLastWin32Error());
+ uint region = 0;
+ try
+ {
+ byte[] originalContext = GetFullX86Context(thread);
+ if (resumeBreakpoint == null || BitConverter.ToUInt32(originalContext, ContextEipOffset)
+ != unchecked(resumeBreakpoint.Address + 1))
+ throw new InvalidOperationException("Boundary probe EIP did not match the verified breakpoint.");
+ uint safeEsp = ComputeSafeNativeStackPointer(process, thread, originalContext);
+ region = AllocateReadbackRegion(process);
+ RemoteCode code = BuildSurfaceProbeStub(region, device);
+ WriteRemoteStub(process, code, true);
+ BreakpointInfo[] suspended;
+ if (!SuspendBreakpointsWhileStopped(process, allBreakpoints, out suspended))
+ throw new InvalidOperationException("Could not suspend every armed breakpoint before the read-only surface query.");
+ RemoteReadbackSession session = new RemoteReadbackSession();
+ session.Acquire = code;
+ session.Region = region;
+ session.ThreadId = threadId;
+ session.SafeEsp = safeEsp;
+ session.OriginalContext = originalContext;
+ session.RemoteContextIntact = true;
+ session.PresentBreakpoint = resumeBreakpoint;
+ session.ResumeBreakpoint = resumeBreakpoint;
+ session.SuspendedBreakpoints = suspended;
+ session.SurfaceProbeOnly = true;
+ session.Frame = frame;
+ session.Generation = frame.CameraGeneration;
+ session.DeadlineUtc = DateTime.UtcNow.AddSeconds(5);
+ session.Phase = RemoteReadbackPhase.Acquire;
+ SetFullX86Context(thread, ContextForRemoteCode(originalContext, code.Base, safeEsp));
+ Log("BOUNDARY_PROBE_STARTED kind=" + resumeBreakpoint.Name + " tid=" + threadId
+ + " device=0x" + device.ToString("X8") + " renderer=0x" + renderer.ToString("X8")
+ + " generation=" + session.Generation + " stub=0x" + code.Base.ToString("X8")
+ + " lockCalled=false");
+ return session;
+ }
+ catch
+ {
+ if (region != 0) VirtualFreeEx(process, Ptr(region), UIntPtr.Zero, 0x8000);
+ throw;
+ }
+ finally { CloseHandle(thread); }
+ }
+
private static void BeginRemoteCleanup(IntPtr process, RemoteReadbackSession session)
{
session.Cleanup = BuildReadbackStub(session.Region, 0, true);
@@ -1337,15 +1660,18 @@ internal static partial class NativeFrameCapture
uint returnAddress = ReadU32(process, esp);
uint camera = ReadU32(process, unchecked(esp + 4));
uint vtable = ReadU32(process, camera);
+ uint parent = ReadCameraParent(process, camera);
uint transformInterface = unchecked(camera + 4);
uint transformVtable = ReadU32(process, transformInterface);
uint selectorField = ReadU32(process, unchecked(camera + 0x10));
byte[] modeBytes = new byte[1];
byte cameraMode = ReadExact(process, unchecked(camera + 0x1A0), modeBytes) ? modeBytes[0] : (byte)0xFF;
byte[] matrixBytes = new byte[64];
- if (camera == 0 || !ReadExact(process, unchecked(camera + 0x20), matrixBytes))
+ byte[] effectiveWorldMatrixBytes = new byte[64];
+ if (camera == 0 || !ReadExact(process, unchecked(camera + 0x20), matrixBytes)
+ || !ReadExact(process, unchecked(camera + 0x60), effectiveWorldMatrixBytes))
{
- result.Failure = "World3D camera argument or 64-byte selector matrix was unreadable";
+ result.Failure = "World3D camera argument, selector-0 matrix, or effective-world matrix was unreadable";
return result;
}
uint[] words = new uint[16];
@@ -1355,6 +1681,15 @@ internal static partial class NativeFrameCapture
words[i] = BitConverter.ToUInt32(matrixBytes, i * 4);
if (!IsFinite(BitConverter.ToSingle(matrixBytes, i * 4))) matrixFinite = false;
}
+ uint[] effectiveWorldWords = MatrixWords(effectiveWorldMatrixBytes);
+ bool effectiveWorldMatrixFinite = true;
+ for (int i = 0; i < effectiveWorldWords.Length; i++)
+ if (!IsFinite(BitConverter.ToSingle(effectiveWorldMatrixBytes, i * 4)))
+ effectiveWorldMatrixFinite = false;
+ string worldMatrixFailure = null;
+ bool effectiveWorldMatrixRigid = effectiveWorldMatrixFinite
+ && IsOrthonormalAffineCamera(effectiveWorldMatrixBytes, out worldMatrixFailure);
+ if (!effectiveWorldMatrixFinite) worldMatrixFailure = "effective-world matrix contains a nonfinite value";
uint expectedVtable = unchecked(terrainBase + ExternalCameraVtableRva);
uint expectedTransformVtable = unchecked(terrainBase + 0x66558);
uint setterSlot = ReadU32(process, unchecked(transformVtable + 0x1C));
@@ -1363,6 +1698,8 @@ internal static partial class NativeFrameCapture
&& setterSlot == unchecked(terrainBase + 0x54C70)
&& invalidateSlot == unchecked(terrainBase + 0x55280);
result.Camera = camera;
+ result.Parent = parent;
+ result.Parented = parent != 0;
result.Vtable = vtable;
result.TransformInterface = transformInterface;
result.TransformVtable = transformVtable;
@@ -1371,14 +1708,19 @@ internal static partial class NativeFrameCapture
result.ReturnAddress = returnAddress;
result.SelectorField = selectorField;
result.Words = words;
+ result.EffectiveWorldWords = effectiveWorldWords;
result.EntryMatrixBytes = (byte[])matrixBytes.Clone();
+ result.EntryEffectiveWorldMatrixBytes = (byte[])effectiveWorldMatrixBytes.Clone();
result.EntryMatrixSha256 = HashBytes(matrixBytes);
+ result.EntryEffectiveWorldMatrixSha256 = HashBytes(effectiveWorldMatrixBytes);
result.MatrixFinite = matrixFinite;
+ result.EffectiveWorldMatrixFinite = effectiveWorldMatrixRigid;
result.LayoutVerified = terrainBase != 0 && vtable == expectedVtable
- && setterAbiVerified && cameraMode == 0
+ && setterAbiVerified && cameraMode == 0 && effectiveWorldMatrixRigid
&& selectorField == 0xFFFFFFFF && matrixFinite;
if (!result.LayoutVerified)
- result.Failure = "external camera primary/transform vtables, setter slots, normal mode, selector field, or matrix finite check failed";
+ result.Failure = "external camera primary/transform vtables, setter slots, normal mode, selector field, local matrix, or effective-world matrix check failed"
+ + (effectiveWorldMatrixRigid ? "" : ": " + worldMatrixFailure);
return result;
}
finally
@@ -1537,26 +1879,75 @@ internal static partial class NativeFrameCapture
private static SelectedCameraInput ParseSelectedCameraJson(string json, string sourcePath)
{
Dictionary<string, object> root = ParseJsonObject(json);
- bool renderInputUsable = RequiredJsonField(root, "render_input_usable") is bool
- && (bool)RequiredJsonField(root, "render_input_usable");
+ object renderInputUsableValue;
+ bool renderInputUsable = root.TryGetValue("render_input_usable", out renderInputUsableValue)
+ && renderInputUsableValue is bool && (bool)renderInputUsableValue;
+ object syntheticCameraValue;
+ bool syntheticCamera = root.TryGetValue("synthetic_camera", out syntheticCameraValue)
+ && syntheticCameraValue is bool && (bool)syntheticCameraValue;
+ bool nativeCapture = String.Equals(JsonStringField(root, "capture_status"),
+ "native-frame-captured", StringComparison.Ordinal) && renderInputUsable;
if (!String.Equals(JsonStringField(root, "schema"), "fparkan-legacy-camera-v1", StringComparison.Ordinal)
- || !String.Equals(JsonStringField(root, "capture_status"), "native-frame-captured", StringComparison.Ordinal)
- || !renderInputUsable)
- throw new InvalidDataException("Camera input must be a usable fparkan-legacy-camera-v1 native capture.");
+ || (!nativeCapture && !syntheticCamera))
+ throw new InvalidDataException("Camera input must be a usable native capture or an explicitly marked synthetic camera with a verified projection tuple.");
SelectedCameraInput input = new SelectedCameraInput();
input.Path = Path.GetFullPath(sourcePath);
- object[] words = JsonArrayField(root, "selector0_words");
- if (words.Length != 16) throw new InvalidDataException("selector0_words must contain exactly 16 uint32 values.");
- input.MatrixWords = new uint[16];
- input.MatrixBytes = new byte[64];
- for (int i = 0; i < words.Length; i++)
+ input.Synthetic = syntheticCamera;
+ object parentedValue;
+ if (root.TryGetValue("parented_camera", out parentedValue))
+ {
+ if (!(parentedValue is bool))
+ throw new InvalidDataException("parented_camera must be a boolean when present.");
+ input.ParentedCamera = (bool)parentedValue;
+ }
+
+ object[] localWords = JsonArrayField(root, "selector0_words");
+ if (localWords.Length != 16) throw new InvalidDataException("selector0_words must contain exactly 16 uint32 values.");
+ input.Selector0MatrixWords = new uint[16];
+ input.Selector0MatrixBytes = new byte[64];
+ for (int i = 0; i < localWords.Length; i++)
{
uint word;
- if (!TryJsonUInt32(words[i], out word))
+ if (!TryJsonUInt32(localWords[i], out word))
throw new InvalidDataException("selector0_words contains a value outside uint32 range.");
- input.MatrixWords[i] = word;
- Buffer.BlockCopy(BitConverter.GetBytes(word), 0, input.MatrixBytes, i * 4, 4);
+ input.Selector0MatrixWords[i] = word;
+ Buffer.BlockCopy(BitConverter.GetBytes(word), 0, input.Selector0MatrixBytes, i * 4, 4);
+ }
+
+ object worldSpaceValue;
+ object worldWordsValue;
+ bool hasWorldSpace = root.TryGetValue("effective_world_matrix_space", out worldSpaceValue);
+ bool hasWorldWords = root.TryGetValue("effective_world_matrix_words", out worldWordsValue);
+ if (hasWorldSpace != hasWorldWords)
+ throw new InvalidDataException("effective_world_matrix_space and effective_world_matrix_words must be supplied together.");
+ if (hasWorldSpace)
+ {
+ string worldSpace = worldSpaceValue as string;
+ if (!String.Equals(worldSpace, "world", StringComparison.Ordinal))
+ throw new InvalidDataException("effective_world_matrix_space must be 'world'.");
+ IList worldWordList = worldWordsValue as IList;
+ if (worldWordList == null || worldWordList.Count != 16)
+ throw new InvalidDataException("effective_world_matrix_words must contain exactly 16 uint32 values.");
+ input.MatrixWords = new uint[16];
+ input.MatrixBytes = new byte[64];
+ for (int i = 0; i < 16; i++)
+ {
+ uint word;
+ if (!TryJsonUInt32(worldWordList[i], out word))
+ throw new InvalidDataException("effective_world_matrix_words contains a value outside uint32 range.");
+ input.MatrixWords[i] = word;
+ Buffer.BlockCopy(BitConverter.GetBytes(word), 0, input.MatrixBytes, i * 4, 4);
+ }
+ input.UsedEffectiveWorldMatrix = true;
+ }
+ else
+ {
+ if (input.ParentedCamera)
+ throw new InvalidDataException("A parented camera requires its effective world matrix; selector0_words is local space.");
+ input.MatrixWords = (uint[])input.Selector0MatrixWords.Clone();
+ input.MatrixBytes = (byte[])input.Selector0MatrixBytes.Clone();
+ input.UsedEffectiveWorldMatrix = false;
}
string matrixFailure;
if (!IsOrthonormalAffineCamera(input.MatrixBytes, out matrixFailure))
@@ -1628,11 +2019,49 @@ internal static partial class NativeFrameCapture
+ "\"viewport\":[0,0,1280,1024],\"near_plane\":0.5,\"far_plane\":700,"
+ "\"field_of_view_radians\":1.04,\"projection_mode_byte\":1}";
SelectedCameraInput parsed = ParseSelectedCameraJson(valid, "input.json");
- if (parsed.MatrixBytes.Length != 64 || parsed.Viewport[2] != 1280 || parsed.MatrixSha256.Length != 64)
+ if (parsed.MatrixBytes.Length != 64 || parsed.Selector0MatrixBytes.Length != 64
+ || parsed.Viewport[2] != 1280 || parsed.MatrixSha256.Length != 64
+ || parsed.UsedEffectiveWorldMatrix || parsed.MatrixWords[3] != parsed.Selector0MatrixWords[3])
throw new InvalidOperationException("Camera JSON parser self-check failed on a valid rigid transform.");
+ string parentedWorld = valid.Replace("\"viewport\":",
+ "\"parented_camera\":true,\"effective_world_matrix_space\":\"world\","
+ + "\"effective_world_matrix_words\":[1065353216,0,0,1092616192,0,1065353216,0,1101004800,"
+ + "0,0,1065353216,1106247680,0,0,0,1065353216],\"viewport\":");
+ SelectedCameraInput parsedParentedWorld = ParseSelectedCameraJson(parentedWorld, "parented.json");
+ if (!parsedParentedWorld.ParentedCamera || !parsedParentedWorld.UsedEffectiveWorldMatrix
+ || parsedParentedWorld.MatrixWords[3] != 1092616192
+ || parsedParentedWorld.Selector0MatrixWords[3] != 1065353216)
+ throw new InvalidOperationException("Camera JSON parser did not prefer the effective world matrix while preserving selector-0 local words.");
+ if (CameraMatricesNearlyEqual(parsedParentedWorld.MatrixBytes,
+ parsedParentedWorld.Selector0MatrixBytes)
+ || parsedParentedWorld.Selector0MatrixWords[3] != 1065353216)
+ throw new InvalidOperationException("Camera setter self-check did not keep the original local matrix distinct for restore.");
+ bool rejected;
+ string parentedWithoutWorld = valid.Replace("\"viewport\":", "\"parented_camera\":true,\"viewport\":");
+ rejected = false;
+ try { ParseSelectedCameraJson(parentedWithoutWorld, "bad-parented.json"); }
+ catch (InvalidDataException) { rejected = true; }
+ if (!rejected) throw new InvalidOperationException("Camera JSON parser accepted a parented camera without an effective world matrix.");
+ string wrongWorldSpace = parentedWorld.Replace("\"effective_world_matrix_space\":\"world\"",
+ "\"effective_world_matrix_space\":\"local\"");
+ rejected = false;
+ try { ParseSelectedCameraJson(wrongWorldSpace, "bad-space.json"); }
+ catch (InvalidDataException) { rejected = true; }
+ if (!rejected) throw new InvalidOperationException("Camera JSON parser accepted a non-world effective matrix marker.");
+ string synthetic = valid.Replace("\"capture_status\":\"native-frame-captured\",",
+ "\"capture_status\":\"synthetic-camera-plan\",\"synthetic_camera\":true,")
+ .Replace("\"render_input_usable\":true,", "");
+ SelectedCameraInput parsedSynthetic = ParseSelectedCameraJson(synthetic, "synthetic-input.json");
+ if (!parsedSynthetic.Synthetic || parsedSynthetic.ProjectionMode != 1)
+ throw new InvalidOperationException("Camera JSON parser failed to preserve explicit synthetic-camera provenance.");
+ string unmarkedSynthetic = synthetic.Replace("\"synthetic_camera\":true", "\"synthetic_camera\":false");
+ rejected = false;
+ try { ParseSelectedCameraJson(unmarkedSynthetic, "bad.json"); }
+ catch (InvalidDataException) { rejected = true; }
+ if (!rejected) throw new InvalidOperationException("Camera JSON parser accepted an unmarked synthetic camera plan.");
string zeroMatrix = valid.Replace("1065353216,0,0,1065353216,0,1065353216,0,1073741824,0,0,1065353216,1077936128,0,0,0,1065353216",
"0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0");
- bool rejected = false;
+ rejected = false;
try { ParseSelectedCameraJson(zeroMatrix, "bad.json"); }
catch (InvalidDataException) { rejected = true; }
if (!rejected) throw new InvalidOperationException("Camera JSON parser accepted a singular all-zero matrix.");
@@ -1664,15 +2093,20 @@ internal static partial class NativeFrameCapture
uint vtable = ReadU32(process, camera.Camera);
uint selectorField = ReadU32(process, unchecked(camera.Camera + 0x10));
+ uint parent = ReadCameraParent(process, camera.Camera);
uint expectedVtable = unchecked(terrainBase + ExternalCameraVtableRva);
byte[] current = new byte[64];
- if (vtable != expectedVtable || selectorField != 0xFFFFFFFF
- || !ReadExact(process, unchecked(camera.Camera + 0x20), current))
+ byte[] effectiveWorldCurrent = new byte[64];
+ if (vtable != expectedVtable || selectorField != 0xFFFFFFFF || parent != camera.Parent
+ || !ReadExact(process, unchecked(camera.Camera + 0x20), current)
+ || !ReadExact(process, unchecked(camera.Camera + 0x60), effectiveWorldCurrent))
{
camera.CurrentAtProjectionVerified = false;
- camera.Failure = "camera pointer, vtable, selector, or matrix changed before projection";
+ camera.EffectiveWorldAtProjectionVerified = false;
+ camera.Failure = "camera pointer, parent, vtable, selector, local matrix, or world matrix changed before projection";
Log("CAMERA_MATRIX_AT_PROJECTION_REJECTED generation=" + generation
+ " vtable=0x" + vtable.ToString("X8") + " selector=0x" + selectorField.ToString("X8")
+ + " parent=0x" + parent.ToString("X8") + " expectedParent=0x" + camera.Parent.ToString("X8")
+ " expectedVtable=0x" + expectedVtable.ToString("X8"));
return false;
}
@@ -1687,22 +2121,54 @@ internal static partial class NativeFrameCapture
if (!finite)
{
camera.CurrentAtProjectionVerified = false;
+ camera.EffectiveWorldAtProjectionVerified = false;
camera.Failure = "camera matrix contains a non-finite value at projection boundary";
Log("CAMERA_MATRIX_AT_PROJECTION_REJECTED generation=" + generation + " nonFinite=true");
return false;
}
+ uint[] effectiveWorldWords = MatrixWords(effectiveWorldCurrent);
+ bool effectiveWorldFinite = true;
+ for (int i = 0; i < effectiveWorldWords.Length; i++)
+ if (!IsFinite(BitConverter.ToSingle(effectiveWorldCurrent, i * 4)))
+ effectiveWorldFinite = false;
+ string worldFailure = null;
+ bool effectiveWorldRigid = effectiveWorldFinite
+ && IsOrthonormalAffineCamera(effectiveWorldCurrent, out worldFailure);
+ if (!effectiveWorldFinite) worldFailure = "effective-world matrix contains a nonfinite value";
+ if (!effectiveWorldRigid)
+ {
+ camera.CurrentAtProjectionVerified = false;
+ camera.EffectiveWorldAtProjectionVerified = false;
+ camera.Failure = "effective-world matrix failed validation at projection: " + worldFailure;
+ Log("CAMERA_WORLD_MATRIX_AT_PROJECTION_REJECTED generation=" + generation + " reason=" + worldFailure);
+ return false;
+ }
+
bool changed = !ByteArraysEqual(camera.EntryMatrixBytes, current);
+ bool effectiveWorldChanged = !ByteArraysEqual(camera.EntryEffectiveWorldMatrixBytes, effectiveWorldCurrent);
camera.Vtable = vtable;
camera.SelectorField = selectorField;
+ camera.Parent = parent;
+ camera.Parented = parent != 0;
camera.Words = words;
+ camera.EffectiveWorldWords = effectiveWorldWords;
camera.ProjectionMatrixBytes = (byte[])current.Clone();
+ camera.ProjectionEffectiveWorldMatrixBytes = (byte[])effectiveWorldCurrent.Clone();
camera.MatrixFinite = true;
+ camera.EffectiveWorldMatrixFinite = true;
camera.CurrentAtProjectionVerified = true;
+ camera.EffectiveWorldAtProjectionVerified = true;
camera.WordsChangedAtProjection = changed;
+ camera.EffectiveWorldChangedAtProjection = effectiveWorldChanged;
camera.ProjectionMatrixSha256 = HashBytes(current);
+ camera.ProjectionEffectiveWorldMatrixSha256 = HashBytes(effectiveWorldCurrent);
Log("CAMERA_MATRIX_AT_PROJECTION generation=" + generation + " verified=true changedSinceEntry=" + changed
+ " entrySha256=" + camera.EntryMatrixSha256 + " projectionSha256=" + camera.ProjectionMatrixSha256);
+ Log("CAMERA_WORLD_MATRIX_AT_PROJECTION generation=" + generation + " verified=true"
+ + " parent=0x" + parent.ToString("X8") + " changedSinceEntry=" + effectiveWorldChanged
+ + " entrySha256=" + camera.EntryEffectiveWorldMatrixSha256
+ + " projectionSha256=" + camera.ProjectionEffectiveWorldMatrixSha256);
return true;
}
@@ -1725,7 +2191,20 @@ internal static partial class NativeFrameCapture
throw new InvalidOperationException("DDLOCK flag self-check failed.");
RemoteCode acquire = BuildReadbackStub(0x10000000, 0x20000000, false);
RemoteCode cleanup = BuildReadbackStub(0x10000000, 0, true);
- RemoteCode cameraSetter = BuildCameraSetterStub(0x30000000, 0x40000000);
+ RemoteCode surfaceProbe = BuildSurfaceProbeStub(0x18000000, 0x28000000);
+ RemoteCode cameraSetterApply = BuildCameraSetterStub(0x30000000, 0x40000000, 2);
+ RemoteCode cameraSetterRestore = BuildCameraSetterStub(0x31000000, 0x40000000, 1);
+ int[] cameraApplyCalls = cameraSetterApply.CallInstructionOffsets;
+ int[] cameraRestoreCalls = cameraSetterRestore.CallInstructionOffsets;
+ int effectiveWorldGetterCall = cameraApplyCalls != null && cameraApplyCalls.Length > 1
+ ? cameraApplyCalls[1] : -1;
+ bool effectiveWorldGetterArgumentsMatch = effectiveWorldGetterCall >= 8
+ && cameraSetterApply.Bytes[effectiveWorldGetterCall - 1] == 0x56
+ && cameraSetterApply.Bytes[effectiveWorldGetterCall - 2] == 2
+ && cameraSetterApply.Bytes[effectiveWorldGetterCall - 3] == 0x6A
+ && cameraSetterApply.Bytes[effectiveWorldGetterCall - 8] == 0x68
+ && BitConverter.ToUInt32(cameraSetterApply.Bytes, effectiveWorldGetterCall - 7)
+ == 0x30000000 + RemoteDataOffset + CameraSetterWorldMatrixPointerOffset;
string[] d3d7DeviceVtable = new string[] {
"QueryInterface", "AddRef", "Release", "GetCaps", "EnumTextureFormats", "BeginScene", "EndScene",
"GetDirect3D", "SetRenderTarget", "GetRenderTarget", "Clear", "SetTransform", "GetTransform",
@@ -1759,11 +2238,28 @@ internal static partial class NativeFrameCapture
|| Marshal.OffsetOf(typeof(X86ContextLayout), "Esp").ToInt32() != ContextEspOffset
|| ContextAllX86 != 0x0001003F || acquire.Bytes.Length == 0 || cleanup.Bytes.Length == 0
|| acquire.MaxCallStackBytes != RemoteStubMaxCallStackBytes || cleanup.MaxCallStackBytes != 12
- || cameraSetter.Bytes.Length == 0 || cameraSetter.MaxCallStackBytes != 16
- || cameraSetter.CallInstructionOffsets == null || cameraSetter.CallInstructionOffsets.Length != 1
- || DecodeIndirectCallDisplacement(cameraSetter.Bytes, cameraSetter.CallInstructionOffsets[0])
+ || cameraSetterApply.Bytes.Length == 0 || cameraSetterApply.MaxCallStackBytes != 16
+ || cameraSetterApply.Bytes[12] != 0x6A || cameraSetterApply.Bytes[13] != 2
+ || cameraSetterRestore.Bytes[12] != 0x6A || cameraSetterRestore.Bytes[13] != 1
+ || CameraSetterMode(CameraSetterPhase.Apply) != 2 || CameraSetterMode(CameraSetterPhase.Restore) != 1
+ || cameraApplyCalls == null || cameraApplyCalls.Length != 2
+ || cameraRestoreCalls == null || cameraRestoreCalls.Length != 1
+ || DecodeIndirectCallDisplacement(cameraSetterApply.Bytes, cameraApplyCalls[0])
+ != CameraTransformSetterSlotOffset
+ || DecodeIndirectCallDisplacement(cameraSetterApply.Bytes, effectiveWorldGetterCall)
+ != CameraTransformGetterSlotOffset
+ || DecodeIndirectCallDisplacement(cameraSetterRestore.Bytes, cameraRestoreCalls[0])
!= CameraTransformSetterSlotOffset
- || cameraSetter.TrapAddress != 0x30000000 + cameraSetter.Bytes.Length - 1
+ || !effectiveWorldGetterArgumentsMatch
+ || cameraSetterApply.TrapAddress - cameraSetterApply.Base != (uint)(effectiveWorldGetterCall + 3)
+ || cameraSetterApply.TrapAddress != 0x30000000 + cameraSetterApply.Bytes.Length - 1
+ || surfaceProbe.Bytes.Length == 0 || surfaceProbe.MaxCallStackBytes != 12
+ || surfaceProbe.CallInstructionOffsets == null || surfaceProbe.CallInstructionOffsets.Length != 2
+ || DecodeIndirectCallDisplacement(surfaceProbe.Bytes, surfaceProbe.CallInstructionOffsets[0])
+ != D3dDeviceGetRenderTargetOffset
+ || DecodeIndirectCallDisplacement(surfaceProbe.Bytes, surfaceProbe.CallInstructionOffsets[1])
+ != DdSurfaceReleaseOffset
+ || surfaceProbe.TrapAddress != 0x18000000 + surfaceProbe.Bytes.Length - 1
|| VtableOffset(d3d7DeviceVtable, "GetRenderTarget") != D3dDeviceGetRenderTargetOffset
|| VtableOffset(surface7Vtable, "Lock") != DdSurfaceLockOffset
|| VtableOffset(surface7Vtable, "SetOverlayPosition") != 0x78
@@ -1824,8 +2320,9 @@ internal static partial class NativeFrameCapture
Rows = new byte[] { 1, 2, 3, 4, 5, 6 }, RgbMasksVerified = true
}), 4, 5, 6, 255, 1, 2, 3, 255);
Console.WriteLine("self-check: D3D7 GetRenderTarget and Surface7 Lock/Unlock/Release SDK vtable ordinals OK");
+ Console.WriteLine("self-check: boundary surface probe calls GetRenderTarget/Release only; no Lock; 12-byte max stack OK");
Console.WriteLine("self-check: caller stack footprint (24/12 bytes), original ESP, committed writable range and guard rejection OK");
- Console.WriteLine("self-check: selected-camera setter stub uses verified interface slot +0x1C and 16-byte maximum caller stack OK");
+ Console.WriteLine("self-check: selected-camera mode-2 setter refreshes selector-2 world cache before trap; local restore uses verified setter slot +0x1C; 16-byte maximum caller stack OK");
Console.WriteLine("self-check: 24-byte present call arguments compare unchanged and detect a changed byte");
Console.WriteLine("self-check: return context logging verifies remote EIP/ESP before restoring the original present call");
Console.WriteLine("self-check: viewport bounds fit the captured pixel surface OK");