feat(render): recover Iron3D Euler transform

This commit is contained in:
2026-07-18 15:28:20 +04:00
parent 438aaebcbc
commit 468ecc4e32
2 changed files with 98 additions and 0 deletions
+87
View File
@@ -246,6 +246,66 @@ impl LegacyD3d7Projection {
} }
} }
/// Affine placement inputs consumed by `Iron3D`'s recovered Euler-matrix builder.
///
/// This records the exact matrix construction at `iron3d.dll` RVA `0x36610`.
/// It is intentionally distinct from a mission placement contract: the static
/// trace has not yet proved that TMA's three raw orientation floats reach this
/// builder unchanged for every object category.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LegacyIron3dEulerTransform {
/// World-space translation passed to the builder.
pub translation: [f32; 3],
/// Three angles in the builder's `(x, y, z)` input order, in radians.
pub orientation_radians: [f32; 3],
}
impl LegacyIron3dEulerTransform {
/// Reconstructs the builder's finite row-major affine matrix.
///
/// The recovered x87 code evaluates `Rz(z) * Ry(y) * Rx(x)` and writes
/// translation into the last column. This uses portable `f32` trigonometry;
/// any future x87-compatibility path must be separately capture-validated.
#[must_use]
pub fn try_row_major(self) -> Option<[f32; 16]> {
if !self
.translation
.iter()
.chain(self.orientation_radians.iter())
.all(|value| value.is_finite())
{
return None;
}
let [x, y, z] = self.orientation_radians;
let (sin_x, cos_x) = x.sin_cos();
let (sin_y, cos_y) = y.sin_cos();
let (sin_z, cos_z) = z.sin_cos();
let [translation_x, translation_y, translation_z] = self.translation;
let matrix = [
cos_z * cos_y,
cos_z * sin_y * sin_x - sin_z * cos_x,
cos_z * sin_y * cos_x + sin_z * sin_x,
translation_x,
sin_z * cos_y,
sin_z * sin_y * sin_x + cos_z * cos_x,
sin_z * sin_y * cos_x - cos_z * sin_x,
translation_y,
-sin_y,
cos_y * sin_x,
cos_y * cos_x,
translation_z,
0.0,
0.0,
0.0,
1.0,
];
matrix
.iter()
.all(|value| value.is_finite())
.then_some(matrix)
}
}
/// Raw camera state observed through the original Terrain camera interface. /// Raw camera state observed through the original Terrain camera interface.
/// ///
/// Selector 0 supplies the currently active transform and selector 2 supplies /// Selector 0 supplies the currently active transform and selector 2 supplies
@@ -1133,6 +1193,33 @@ mod tests {
); );
} }
#[test]
fn iron3d_euler_builder_uses_rz_ry_rx_and_last_column_translation() {
let matrix = LegacyIron3dEulerTransform {
translation: [418.103_18, 717.433, 3.040_938_9],
orientation_radians: [0.0, 0.0, std::f32::consts::FRAC_PI_2],
}
.try_row_major()
.expect("finite recovered builder inputs");
assert!((matrix[0]).abs() < 0.000_001);
assert!((matrix[1] + 1.0).abs() < 0.000_001);
assert!((matrix[4] - 1.0).abs() < 0.000_001);
assert!((matrix[5]).abs() < 0.000_001);
assert_eq!(matrix[3], 418.103_18);
assert_eq!(matrix[7], 717.433);
assert_eq!(matrix[11], 3.040_938_9);
assert_eq!(matrix[15], 1.0);
assert_eq!(
LegacyIron3dEulerTransform {
translation: [0.0, 0.0, 0.0],
orientation_radians: [f32::NAN, 0.0, 0.0],
}
.try_row_major(),
None
);
}
fn snapshot_draw( fn snapshot_draw(
id: u64, id: u64,
phase: RenderPhase, phase: RenderPhase,
+11
View File
@@ -1437,6 +1437,17 @@ on its XY diagnostic projection until a runtime supplies a real legacy camera;
selecting source-world coordinates with identity camera would be a misleading selecting source-world coordinates with identity camera would be a misleading
empty/off-screen viewer rather than a compatibility result. empty/off-screen viewer rather than a compatibility result.
Static analysis of GOG `iron3d.dll` now recovers an exact adjacent placement
primitive: RVA `0x36610` evaluates a row-major affine
`Rz(z) * Ry(y) * Rx(x)` and writes translation in the final column. The
backend-neutral `LegacyIron3dEulerTransform` preserves that finite portable
formula with a golden yaw-vector test. This is deliberately not wired to TMA:
AutoDemo records strongly suggest the candidate `(0, 0, z)` radians (all eight
objects have zero first two values and `z` in approximately `[-pi, pi]`), but
the current static trace has not proved that those raw record fields reach this
builder unchanged for every mission object category. The remaining link needs
a direct loader-to-builder dataflow or a dynamic placement capture.
`VulkanSmokeRenderer::set_camera` now accepts a new finite camera between frame `VulkanSmokeRenderer::set_camera` now accepts a new finite camera between frame
submissions and uses it for the next command buffer without rebuilding the submissions and uses it for the next command buffer without rebuilding the
swapchain, buffers, descriptors or pipelines. `VulkanStaticCamera` also accepts swapchain, buffers, descriptors or pipelines. `VulkanStaticCamera` also accepts