feat(render): recover legacy d3d camera matrices

This commit is contained in:
2026-07-18 14:42:36 +04:00
parent f1a3315798
commit 94de743581
2 changed files with 216 additions and 0 deletions
+182
View File
@@ -127,6 +127,123 @@ impl RawCameraTransform {
1.0,
])
}
/// Reproduces Ngi32's `Direct3D7` view-matrix conversion for this transform.
///
/// The legacy renderer copies selector-0 into its camera state, then maps
/// its axes and translation in this exact order before calling
/// `IDirect3DDevice7::SetTransform(D3DTRANSFORMSTATE_VIEW, ...)`. This is
/// deliberately distinct from [`Self::try_inverse_affine_row_major`]: it
/// includes the original renderer's coordinate-system conversion.
///
/// The returned matrix is row-major D3D7 data, not yet a Vulkan view
/// matrix. A later adapter must explicitly account for clip-space and
/// shader-vector conventions.
#[must_use]
pub fn try_direct3d7_view_row_major(self) -> Option<[f32; 16]> {
let matrix = self.words.map(f32::from_bits);
if !matrix.iter().all(|value| value.is_finite())
|| matrix[12].abs() > f32::EPSILON
|| matrix[13].abs() > f32::EPSILON
|| matrix[14].abs() > f32::EPSILON
|| (matrix[15] - 1.0).abs() > f32::EPSILON
{
return None;
}
let [m00, m01, m02, m03, m10, m11, m12, m13, m20, m21, m22, m23, _, _, _, _] = matrix;
Some([
-m01,
m02,
m00,
0.0,
-m11,
m12,
m10,
0.0,
-m21,
m22,
m20,
0.0,
m23.mul_add(m21, m13.mul_add(m11, m03 * m01)),
-(m23.mul_add(m12, m13.mul_add(m02, m03 * m10))),
-(m00.mul_add(m03, m23.mul_add(m20, m13 * m10))),
1.0,
])
}
}
/// Parameters consumed by Ngi32's `Direct3D7` projection-matrix builder.
///
/// These are a separate legacy-renderer boundary from
/// [`RawCameraProjection`]. The latter preserves Terrain's source ABI, while
/// this type records the already-resolved Ngi32 values submitted to `Direct3D7`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LegacyD3d7Projection {
/// Viewport rectangle as `(left, top, right, bottom)`.
pub viewport: [i32; 4],
/// Positive camera near-plane distance.
pub near_plane: f32,
/// Camera far-plane distance, greater than [`Self::near_plane`].
pub far_plane: f32,
/// Full field-of-view angle in radians.
pub field_of_view_radians: f32,
}
impl LegacyD3d7Projection {
/// Reconstructs the exact row-major matrix passed to D3D7 projection state.
///
/// Ngi32 uses the viewport's `height / width`, writes `cos(fov / 2)` to
/// the diagonal and `sin(fov / 2)` to the homogeneous-W term. The ratio
/// after D3D's perspective divide is therefore the expected cotangent
/// scale. This remains legacy D3D7 data rather than a Vulkan projection.
#[must_use]
pub fn try_direct3d7_projection_row_major(self) -> Option<[f32; 16]> {
let width = self.viewport[2].checked_sub(self.viewport[0])?;
let height = self.viewport[3].checked_sub(self.viewport[1])?;
if width <= 0
|| height <= 0
|| !self.near_plane.is_finite()
|| !self.far_plane.is_finite()
|| !self.field_of_view_radians.is_finite()
|| self.near_plane <= 0.0
|| self.far_plane <= self.near_plane
|| self.field_of_view_radians <= 0.0
|| self.field_of_view_radians >= std::f32::consts::PI
{
return None;
}
// Ngi32 converts these signed viewport dimensions into single-precision
// arithmetic before building the legacy matrix.
#[allow(clippy::cast_precision_loss)]
let aspect = (height as f32) / (width as f32);
let half_fov = self.field_of_view_radians * 0.5;
let cosine = half_fov.cos();
let sine = half_fov.sin();
let depth_scale = 1.0 / (1.0 - self.near_plane / self.far_plane);
[aspect, cosine, sine, depth_scale]
.iter()
.all(|value| value.is_finite())
.then_some([
cosine,
0.0,
0.0,
0.0,
0.0,
aspect * cosine,
0.0,
0.0,
0.0,
0.0,
depth_scale,
sine,
0.0,
0.0,
-(depth_scale * self.near_plane),
0.0,
])
}
}
/// Raw camera state observed through the original Terrain camera interface.
@@ -951,6 +1068,71 @@ mod tests {
assert_eq!(singular.try_inverse_affine_row_major(), None);
}
#[test]
fn raw_camera_transform_reproduces_direct3d7_view_axis_conversion() {
let transform = RawCameraTransform {
words: [
0.0_f32.to_bits(),
(-1.0_f32).to_bits(),
0.0_f32.to_bits(),
10.0_f32.to_bits(),
1.0_f32.to_bits(),
0.0_f32.to_bits(),
0.0_f32.to_bits(),
20.0_f32.to_bits(),
0.0_f32.to_bits(),
0.0_f32.to_bits(),
1.0_f32.to_bits(),
30.0_f32.to_bits(),
0.0_f32.to_bits(),
0.0_f32.to_bits(),
0.0_f32.to_bits(),
1.0_f32.to_bits(),
],
};
assert_eq!(
transform.try_direct3d7_view_row_major(),
Some([
1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, -10.0, -10.0, -20.0,
1.0,
])
);
assert_eq!(
RawCameraTransform { words: [0_u32; 16] }.try_direct3d7_view_row_major(),
None
);
}
#[test]
fn legacy_d3d7_projection_matches_recovered_camera_formula() {
let projection = LegacyD3d7Projection {
viewport: [0, 0, 1024, 768],
near_plane: 0.5,
far_plane: 700.0,
field_of_view_radians: 1.3,
};
let matrix = projection
.try_direct3d7_projection_row_major()
.expect("live Ngi32 projection parameters are valid");
let half_fov = 0.65_f32;
let depth_scale = 700.0_f32 / 699.5;
assert_eq!(matrix[0], half_fov.cos());
assert_eq!(matrix[5], 0.75 * half_fov.cos());
assert_eq!(matrix[10], depth_scale);
assert_eq!(matrix[11], half_fov.sin());
assert_eq!(matrix[14], -(depth_scale * 0.5));
assert_eq!(
LegacyD3d7Projection {
viewport: [0, 0, 0, 768],
..projection
}
.try_direct3d7_projection_row_major(),
None
);
}
fn snapshot_draw(
id: u64,
phase: RenderPhase,
+34
View File
@@ -1383,6 +1383,40 @@ next recovery target is therefore the Direct3D transform consumer in
`700`, `0.1`, and `0.99` values remain unlabelled until that consumer proves
their roles.
### Ngi32 Direct3D7 camera matrices recovered
The downstream renderer is now concrete. `iron3d.dll` obtains an opaque
render interface through `Ngi32!niGet3DRender`; Ngi32 RVA `0x5640` returns the
global renderer object at RVA `0x3A460`. Its constructor at RVA `0x5E10` gives
the `0x8CC`-byte object vtable `0x100315E0`. During device setup, Ngi32 RVA
`0x8E70` calls the Direct3D7 device vtable at `+0x2C` (`SetTransform`) with
state `3` for the matrix made by RVA `0x7030` and state `2` for the matrix made
by RVA `0x9450`: projection then view.
RVA `0x7030` exactly builds the row-major D3D7 projection from renderer FOV
`f`, near `n`, far `z` and viewport width/height `w`/`h`:
```text
[ cos(f/2), 0, 0, 0 ]
[ 0, h/w * cos(f/2), 0, 0 ]
[ 0, 0, z/(z-n), sin(f/2) ]
[ 0, 0, -n*z/(z-n), 0 ]
```
The sine in the homogeneous-W term is intentional: after the D3D perspective
divide the diagonal has the expected cotangent scale. RVA `0x9450` applies a
specific axis permutation/sign change and translated dot products to selector
0 before the view `SetTransform`; it is not merely the generic affine inverse.
An elevated read-only AutoDemo probe sampled a 1024×768 renderer with
near=`0.5`, far=`700`, FOV=`1.3` radians and showed its view-source pointer
byte-identical to the active Terrain outer camera's selector-0 block. This
proves ownership and the D3D7 boundary, while keeping the earlier
`CBufferingCamera` FOV=`1.04` as a distinct upstream interface value. The
backend-neutral render crate now exposes the two recovered D3D7 matrices, but
does not yet use them as Vulkan matrices: vector and clip-space conversion is
still an explicit next task.
A fresh no-input launch of the canonical `iron_3d.exe` did create a responsive
window titled `Parkan. Железная Стратегия`. A read-only probe then requested
`PROCESS_QUERY_INFORMATION | PROCESS_VM_READ` and attempted to read the known