fix(tools): reject non-world camera captures
This commit is contained in:
+26
-20
@@ -1421,14 +1421,15 @@ proves ownership and the D3D7 boundary, while keeping the earlier
|
|||||||
`CBufferingCamera` FOV=`1.04` as a distinct upstream interface value.
|
`CBufferingCamera` FOV=`1.04` as a distinct upstream interface value.
|
||||||
|
|
||||||
The live GOG layout is now independently repeatable without invoking any game
|
The live GOG layout is now independently repeatable without invoking any game
|
||||||
method: `Terrain.dll + 0x7355c` contains the current camera interface pointer,
|
method: `Terrain.dll + 0x7355c` contains the current outer camera-object
|
||||||
whose vtable relocates to `Terrain.dll` RVA `0x665b4`. Its selector-0 block
|
pointer, whose vtable relocates to `Terrain.dll` RVA `0x665b4`. Selector 0
|
||||||
starts with one 32-bit internal tag at interface `+28`; the actual sixteen
|
returns the affine block beginning at outer `+0x20`; its sixteen row-major
|
||||||
row-major matrix words begin at `+32`. Captures for `RawCameraTransform` must
|
words have translations at indices 3, 7 and 11. The public `LoadCamera`
|
||||||
therefore contain those sixteen payload words, not the tag. A passive AutoDemo
|
interface at outer `+0x134` is a different view and must not be substituted
|
||||||
sample produced a finite affine camera matrix and passed through the offline
|
for that block. Captures for `RawCameraTransform` therefore contain the sixteen
|
||||||
Vulkan adapter as source-world geometry, rather than relying on a guessed
|
selector-0 words from outer `+0x20`. A passive AutoDemo sample produced a
|
||||||
identity view.
|
finite affine camera matrix and passed through the offline Vulkan adapter as
|
||||||
|
source-world geometry, rather than relying on a guessed identity view.
|
||||||
|
|
||||||
The Vulkan static path now accepts this recovered camera as
|
The Vulkan static path now accepts this recovered camera as
|
||||||
`VulkanStaticCamera`. It composes the row-major D3D7 result as
|
`VulkanStaticCamera`. It composes the row-major D3D7 result as
|
||||||
@@ -1501,8 +1502,13 @@ comparison.
|
|||||||
`tools/capture-original-camera.ps1` makes that handoff repeatable for a live
|
`tools/capture-original-camera.ps1` makes that handoff repeatable for a live
|
||||||
GOG process. It finds the actual `Terrain.dll` base with Toolhelp module
|
GOG process. It finds the actual `Terrain.dll` base with Toolhelp module
|
||||||
enumeration, opens the chosen PID with only
|
enumeration, opens the chosen PID with only
|
||||||
`PROCESS_QUERY_INFORMATION | PROCESS_VM_READ`, reads the camera global at its
|
`PROCESS_QUERY_INFORMATION | PROCESS_VM_READ`, verifies the expected outer
|
||||||
RVA and then exactly 64 matrix bytes at interface `+32`. It emits the
|
vtable, and reads exactly 64 selector-0 bytes at outer `+0x20`. AutoDemo can
|
||||||
|
briefly select a normalized/reflection-like camera object whose finite
|
||||||
|
translation is near the origin. The tool therefore re-reads the global on
|
||||||
|
each attempt and accepts a sample only when the proven translation length
|
||||||
|
exceeds its explicit `-MinimumWorldTranslation` threshold (default `100`);
|
||||||
|
the JSON retains the accepted outer pointer and translation for audit. It emits the
|
||||||
`fparkan-legacy-camera-v1` JSON accepted by `fparkan-game`; it does not write
|
`fparkan-legacy-camera-v1` JSON accepted by `fparkan-game`; it does not write
|
||||||
to the process, send window messages, inject code, suspend threads, or invoke
|
to the process, send window messages, inject code, suspend threads, or invoke
|
||||||
an original method. PowerShell execution policy may require the explicit
|
an original method. PowerShell execution policy may require the explicit
|
||||||
@@ -1518,16 +1524,16 @@ cargo run -q -p fparkan-game -- --root 'C:\GOG Games\Parkan - Iron Strategy' `
|
|||||||
--readback-out target\fparkan\autodemo-live-camera.raw
|
--readback-out target\fparkan\autodemo-live-camera.raw
|
||||||
```
|
```
|
||||||
|
|
||||||
On the unattended live AutoDemo the tool produced a new finite matrix and the
|
On the unattended live AutoDemo the accepted world-space capture completed the
|
||||||
same all-root static bridge completed with 46 clip-visible vertices, 71
|
same all-root static bridge with 10,152 clip-visible vertices, 71 descriptors,
|
||||||
descriptors, a 7,372,800-byte format-50 readback, FNV-1a
|
a 7,372,800-byte format-50 readback, and zero Vulkan validation
|
||||||
`11081931966175262997`, and zero Vulkan validation warnings/errors. A passive
|
warnings/errors. A passive desktop-surface capture of that original window also
|
||||||
desktop-surface capture of that original window also proved that the selected
|
proved that the selected instant contains the textured terrain, atmospheric
|
||||||
instant contains the textured terrain, atmospheric sky, HUD and weapon FX;
|
sky, HUD and weapon FX; `PrintWindow` itself returns a black Direct3D buffer,
|
||||||
`PrintWindow` itself returns a black Direct3D buffer, so it must not be treated
|
so it must not be treated as an original-frame oracle. The paired images expose
|
||||||
as an original-frame oracle. Neither screenshot establishes pixel parity: HUD,
|
the next real renderer gap: sharing the raw pose and Ngi32 projection inputs is
|
||||||
FX, lighting, terrain layer composition and the remaining scene objects are
|
not yet pixel parity, because the static bridge still lacks the full runtime
|
||||||
not yet represented by the static bridge.
|
camera-selection timing, terrain composition, culling, lighting, UI and FX.
|
||||||
|
|
||||||
For visual regression work, `fparkan-game --backend static-vulkan` also accepts
|
For visual regression work, `fparkan-game --backend static-vulkan` also accepts
|
||||||
`--readback-out <path>`. It writes the final synchronized Vulkan readback bytes
|
`--readback-out <path>`. It writes the final synchronized Vulkan readback bytes
|
||||||
|
|||||||
@@ -11,7 +11,13 @@ param(
|
|||||||
[ValidateRange(0.01, 100000.0)]
|
[ValidateRange(0.01, 100000.0)]
|
||||||
[double]$FarPlane = 700.0,
|
[double]$FarPlane = 700.0,
|
||||||
[ValidateRange(0.01, 3.13)]
|
[ValidateRange(0.01, 3.13)]
|
||||||
[double]$FieldOfViewRadians = 1.3
|
[double]$FieldOfViewRadians = 1.3,
|
||||||
|
[ValidateRange(1, 600)]
|
||||||
|
[int]$CaptureAttempts = 60,
|
||||||
|
[ValidateRange(0, 1000000.0)]
|
||||||
|
[double]$MinimumWorldTranslation = 100.0,
|
||||||
|
[ValidateRange(0, 1000)]
|
||||||
|
[int]$RetryIntervalMilliseconds = 100
|
||||||
)
|
)
|
||||||
|
|
||||||
Set-StrictMode -Version Latest
|
Set-StrictMode -Version Latest
|
||||||
@@ -131,15 +137,60 @@ if ($process -eq [IntPtr]::Zero) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
# Terrain.dll image RVA 0x7355c -> active camera interface pointer.
|
# Terrain.dll image RVA 0x7355c -> active 0x1a4-byte outer camera object.
|
||||||
# Selector 0's matrix is prefixed by a four-byte internal tag, so the
|
# Its selector-0 affine block begins at outer + 0x20. Words 3/7/11 are
|
||||||
# 16 IEEE-754 row-major words begin at interface + 32, not + 28.
|
# the proven world-space translation components. AutoDemo also briefly
|
||||||
$cameraPointerBytes = Read-OriginalBytes $process ($terrainBase + 0x7355c) 4
|
# selects normalized/reflection-like camera objects; a finite matrix alone
|
||||||
$cameraInterface = [BitConverter]::ToUInt32($cameraPointerBytes, 0)
|
# is not sufficient evidence that it can project the mission world.
|
||||||
if ($cameraInterface -lt 0x10000) {
|
$expectedOuterVtable = [uint32]($terrainBase + 0x665b4)
|
||||||
throw "Terrain camera pointer is unavailable or invalid: 0x$('{0:X8}' -f $cameraInterface)"
|
$matrixBytes = $null
|
||||||
|
$translation = $null
|
||||||
|
$cameraOuter = $null
|
||||||
|
$lastObservation = 'camera pointer unavailable'
|
||||||
|
for ($attempt = 1; $attempt -le $CaptureAttempts; $attempt++) {
|
||||||
|
$candidateOuter = [BitConverter]::ToUInt32(
|
||||||
|
(Read-OriginalBytes $process ($terrainBase + 0x7355c) 4),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
if ($candidateOuter -lt 0x10000) {
|
||||||
|
$lastObservation = "camera pointer 0x$('{0:X8}' -f $candidateOuter)"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$outerVtable = [BitConverter]::ToUInt32(
|
||||||
|
(Read-OriginalBytes $process ([int64]$candidateOuter) 4),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
if ($outerVtable -ne $expectedOuterVtable) {
|
||||||
|
$lastObservation = "outer vtable 0x$('{0:X8}' -f $outerVtable)"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$matrixBytes = Read-OriginalBytes $process ([int64]$candidateOuter + 0x20) 64
|
||||||
|
$candidateTranslation = @(
|
||||||
|
[BitConverter]::ToSingle($matrixBytes, 12),
|
||||||
|
[BitConverter]::ToSingle($matrixBytes, 28),
|
||||||
|
[BitConverter]::ToSingle($matrixBytes, 44)
|
||||||
|
)
|
||||||
|
$nonFinite = @($candidateTranslation | Where-Object {
|
||||||
|
[Single]::IsNaN($_) -or [Single]::IsInfinity($_)
|
||||||
|
})
|
||||||
|
$length = [Math]::Sqrt(
|
||||||
|
[double]$candidateTranslation[0] * $candidateTranslation[0] +
|
||||||
|
[double]$candidateTranslation[1] * $candidateTranslation[1] +
|
||||||
|
[double]$candidateTranslation[2] * $candidateTranslation[2]
|
||||||
|
)
|
||||||
|
if ($nonFinite.Count -eq 0 -and $length -ge $MinimumWorldTranslation) {
|
||||||
|
$cameraOuter = $candidateOuter
|
||||||
|
$translation = $candidateTranslation
|
||||||
|
break
|
||||||
|
}
|
||||||
|
$lastObservation = "translation length $length at 0x$('{0:X8}' -f $candidateOuter)"
|
||||||
|
if ($attempt -lt $CaptureAttempts -and $RetryIntervalMilliseconds -gt 0) {
|
||||||
|
Start-Sleep -Milliseconds $RetryIntervalMilliseconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($null -eq $translation) {
|
||||||
|
throw "No world-space selector-0 transform after $CaptureAttempts samples (minimum translation length $MinimumWorldTranslation; last observation: $lastObservation)"
|
||||||
}
|
}
|
||||||
$matrixBytes = Read-OriginalBytes $process ([int64]$cameraInterface + 32) 64
|
|
||||||
$words = for ($index = 0; $index -lt 16; $index++) {
|
$words = for ($index = 0; $index -lt 16; $index++) {
|
||||||
[BitConverter]::ToUInt32($matrixBytes, $index * 4)
|
[BitConverter]::ToUInt32($matrixBytes, $index * 4)
|
||||||
}
|
}
|
||||||
@@ -148,7 +199,9 @@ try {
|
|||||||
process_id = $ProcessId
|
process_id = $ProcessId
|
||||||
terrain_module_base = ('0x{0:X8}' -f $terrainBase)
|
terrain_module_base = ('0x{0:X8}' -f $terrainBase)
|
||||||
terrain_camera_global_rva = '0x7355c'
|
terrain_camera_global_rva = '0x7355c'
|
||||||
|
terrain_camera_outer = ('0x{0:X8}' -f $cameraOuter)
|
||||||
selector0_words = @($words)
|
selector0_words = @($words)
|
||||||
|
selector0_translation = @($translation)
|
||||||
# LegacyD3d7Projection carries a D3D7 RECT: left, top, right, bottom.
|
# LegacyD3d7Projection carries a D3D7 RECT: left, top, right, bottom.
|
||||||
viewport = @(0, 0, $ViewportWidth, $ViewportHeight)
|
viewport = @(0, 0, $ViewportWidth, $ViewportHeight)
|
||||||
near_plane = $NearPlane
|
near_plane = $NearPlane
|
||||||
|
|||||||
Reference in New Issue
Block a user