fix(render): animate sky layers on the native UV clock
Docs Deploy / Build and Deploy MkDocs (push) Successful in 37s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 37s
This commit is contained in:
8 files changed
+285
-13
No files matched your search
@@ -15,9 +15,16 @@ pub(super) struct LegacyCameraCapture {
|
|||||||
pub(super) far_plane: f32,
|
pub(super) far_plane: f32,
|
||||||
pub(super) field_of_view_radians: f32,
|
pub(super) field_of_view_radians: f32,
|
||||||
pub(super) atmosphere_seconds: Option<f32>,
|
pub(super) atmosphere_seconds: Option<f32>,
|
||||||
|
pub(super) sky_uv_clock: Option<SkyUvClock>,
|
||||||
pub(super) geometry_projection: Option<GeometryProjection>,
|
pub(super) geometry_projection: Option<GeometryProjection>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||||
|
pub(super) struct SkyUvClock {
|
||||||
|
pub(super) verified: bool,
|
||||||
|
pub(super) elapsed_milliseconds: u32,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||||
pub(super) struct GeometryProjection {
|
pub(super) struct GeometryProjection {
|
||||||
verified: bool,
|
verified: bool,
|
||||||
@@ -118,6 +125,9 @@ pub(super) fn load_legacy_camera_capture(
|
|||||||
if capture.geometry_projection.is_none() {
|
if capture.geometry_projection.is_none() {
|
||||||
eprintln!("render_coverage captured_projection_unverified=1 (capture has no geometry_projection; using the recorded camera boundary, which can clip nearby surfaces)");
|
eprintln!("render_coverage captured_projection_unverified=1 (capture has no geometry_projection; using the recorded camera boundary, which can clip nearby surfaces)");
|
||||||
}
|
}
|
||||||
|
if capture.sky_uv_clock.is_none() {
|
||||||
|
eprintln!("render_coverage captured_sky_phase_unverified=1 (capture has no sky UV clock; using replay elapsed time)");
|
||||||
|
}
|
||||||
Ok(capture)
|
Ok(capture)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +153,13 @@ pub(super) fn parse_legacy_camera_capture(bytes: &[u8]) -> Result<LegacyCameraCa
|
|||||||
if capture.render_input_usable == Some(false) {
|
if capture.render_input_usable == Some(false) {
|
||||||
return Err("legacy camera capture is marked render_input_usable=false".to_string());
|
return Err("legacy camera capture is marked render_input_usable=false".to_string());
|
||||||
}
|
}
|
||||||
|
if capture
|
||||||
|
.sky_uv_clock
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|clock| !clock.verified)
|
||||||
|
{
|
||||||
|
return Err("legacy camera sky UV clock is unverified".to_string());
|
||||||
|
}
|
||||||
if capture
|
if capture
|
||||||
.atmosphere_seconds
|
.atmosphere_seconds
|
||||||
.is_some_and(|seconds| !seconds.is_finite() || seconds < 0.0)
|
.is_some_and(|seconds| !seconds.is_finite() || seconds < 0.0)
|
||||||
|
|||||||
@@ -141,6 +141,10 @@ fn run(args: &[String]) -> Result<String, String> {
|
|||||||
.map(load_legacy_camera_capture)
|
.map(load_legacy_camera_capture)
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
let atmosphere_seconds = selected_atmosphere_seconds(args.atmosphere_seconds, camera.as_ref());
|
let atmosphere_seconds = selected_atmosphere_seconds(args.atmosphere_seconds, camera.as_ref());
|
||||||
|
let captured_sky_uv_ms = camera
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|c| c.sky_uv_clock.as_ref())
|
||||||
|
.map(|c| c.elapsed_milliseconds);
|
||||||
let hold_schedule_phase = camera.is_some() && atmosphere_seconds.is_some();
|
let hold_schedule_phase = camera.is_some() && atmosphere_seconds.is_some();
|
||||||
let terrain_materials_duration_started = Instant::now();
|
let terrain_materials_duration_started = Instant::now();
|
||||||
let preview = static_preview_mesh_and_materials(
|
let preview = static_preview_mesh_and_materials(
|
||||||
@@ -159,12 +163,15 @@ fn run(args: &[String]) -> Result<String, String> {
|
|||||||
// sky to the selected map (there are many `sky.ske` files in the campaign
|
// sky to the selected map (there are many `sky.ske` files in the campaign
|
||||||
// tree).
|
// tree).
|
||||||
let sky_started = Instant::now();
|
let sky_started = Instant::now();
|
||||||
let environment = load_environment(
|
let mut environment = load_environment(
|
||||||
&args.root,
|
&args.root,
|
||||||
&args.mission,
|
&args.mission,
|
||||||
atmosphere_seconds,
|
atmosphere_seconds,
|
||||||
hold_schedule_phase,
|
hold_schedule_phase,
|
||||||
)?;
|
)?;
|
||||||
|
if let Some(environment) = &mut environment {
|
||||||
|
environment.fixed_sky_uv_ms = captured_sky_uv_ms;
|
||||||
|
}
|
||||||
let sky_duration = sky_started.elapsed();
|
let sky_duration = sky_started.elapsed();
|
||||||
let audio = match audio::GameAudio::new(Arc::new(DirectoryVfs::new(&args.root)), &args.mission)
|
let audio = match audio::GameAudio::new(Arc::new(DirectoryVfs::new(&args.root)), &args.mission)
|
||||||
{
|
{
|
||||||
@@ -4188,6 +4195,7 @@ struct DynamicEnvironment {
|
|||||||
time_seconds: f32,
|
time_seconds: f32,
|
||||||
fixed_schedule_seconds: Option<f32>,
|
fixed_schedule_seconds: Option<f32>,
|
||||||
fixed_phase_reported: bool,
|
fixed_phase_reported: bool,
|
||||||
|
fixed_sky_uv_ms: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DynamicEnvironmentFrame {
|
struct DynamicEnvironmentFrame {
|
||||||
@@ -4232,6 +4240,7 @@ impl DynamicEnvironment {
|
|||||||
fn update_cpu(
|
fn update_cpu(
|
||||||
&mut self,
|
&mut self,
|
||||||
dt_seconds: f32,
|
dt_seconds: f32,
|
||||||
|
clock_ms: u32,
|
||||||
camera: &PreviewCameraFrame,
|
camera: &PreviewCameraFrame,
|
||||||
aspect: f32,
|
aspect: f32,
|
||||||
viewport: [f32; 2],
|
viewport: [f32; 2],
|
||||||
@@ -4259,6 +4268,7 @@ impl DynamicEnvironment {
|
|||||||
}
|
}
|
||||||
let atmosphere = self.schedule.sample(absolute_time_seconds);
|
let atmosphere = self.schedule.sample(absolute_time_seconds);
|
||||||
let mut sky_mesh = self.sky.mesh().clone();
|
let mut sky_mesh = self.sky.mesh().clone();
|
||||||
|
sky_mesh.update_uvs(self.fixed_sky_uv_ms.unwrap_or(clock_ms));
|
||||||
let sky_frame =
|
let sky_frame =
|
||||||
SkyFrame::from_atmosphere(&atmosphere, &self.materials, absolute_time_seconds);
|
SkyFrame::from_atmosphere(&atmosphere, &self.materials, absolute_time_seconds);
|
||||||
sky_mesh.update_colors(&sky_frame.sky);
|
sky_mesh.update_colors(&sky_frame.sky);
|
||||||
@@ -4465,6 +4475,7 @@ fn load_environment(
|
|||||||
time_seconds,
|
time_seconds,
|
||||||
fixed_schedule_seconds,
|
fixed_schedule_seconds,
|
||||||
fixed_phase_reported: false,
|
fixed_phase_reported: false,
|
||||||
|
fixed_sky_uv_ms: None,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10187,6 +10198,7 @@ impl StaticVulkanApp {
|
|||||||
let environment = if let Some(environment) = self.environment.as_mut() {
|
let environment = if let Some(environment) = self.environment.as_mut() {
|
||||||
Some(environment.update_cpu(
|
Some(environment.update_cpu(
|
||||||
clock.delta_seconds,
|
clock.delta_seconds,
|
||||||
|
clock.time_ms,
|
||||||
&camera,
|
&camera,
|
||||||
aspect,
|
aspect,
|
||||||
viewport,
|
viewport,
|
||||||
@@ -13562,6 +13574,7 @@ mod tests {
|
|||||||
time_seconds: 0.0,
|
time_seconds: 0.0,
|
||||||
fixed_schedule_seconds: None,
|
fixed_schedule_seconds: None,
|
||||||
fixed_phase_reported: false,
|
fixed_phase_reported: false,
|
||||||
|
fixed_sky_uv_ms: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut land = fparkan_terrain_format::LandMeshDocument {
|
let mut land = fparkan_terrain_format::LandMeshDocument {
|
||||||
@@ -13811,6 +13824,34 @@ mod tests {
|
|||||||
app.pending_scene_update.as_ref().unwrap().clock.time_ms,
|
app.pending_scene_update.as_ref().unwrap().clock.time_ms,
|
||||||
500
|
500
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
app.pending_scene_update
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.environment
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.sky_mesh
|
||||||
|
.vertices[0]
|
||||||
|
.uv[2],
|
||||||
|
[0.01, 0.0]
|
||||||
|
);
|
||||||
|
app.environment.as_mut().unwrap().fixed_sky_uv_ms = Some(25_000);
|
||||||
|
next.id = SceneSampleId::Live(2);
|
||||||
|
next.time_ms = 10_000;
|
||||||
|
app.prepare_scene_update(next, [1280.0, 720.0]).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
app.pending_scene_update
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.environment
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.sky_mesh
|
||||||
|
.vertices[0]
|
||||||
|
.uv[2],
|
||||||
|
[0.5, 0.0]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -15633,6 +15674,22 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn captured_sky_uv_phase_requires_verified_clock() {
|
||||||
|
let mut value: serde_json::Value =
|
||||||
|
serde_json::from_slice(include_bytes!("../testdata/legacy-camera-parented.json"))
|
||||||
|
.unwrap();
|
||||||
|
value["sky_uv_clock"] =
|
||||||
|
serde_json::json!({"verified": true, "elapsed_milliseconds": 4051271258_u32});
|
||||||
|
let capture = parse_legacy_camera_capture(&serde_json::to_vec(&value).unwrap()).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
capture.sky_uv_clock.unwrap().elapsed_milliseconds,
|
||||||
|
4051271258
|
||||||
|
);
|
||||||
|
value["sky_uv_clock"]["verified"] = serde_json::json!(false);
|
||||||
|
assert!(parse_legacy_camera_capture(&serde_json::to_vec(&value).unwrap()).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn captured_atmosphere_is_default_and_cli_time_takes_precedence() {
|
fn captured_atmosphere_is_default_and_cli_time_takes_precedence() {
|
||||||
let capture = LegacyCameraCapture {
|
let capture = LegacyCameraCapture {
|
||||||
@@ -15648,6 +15705,7 @@ mod tests {
|
|||||||
field_of_view_radians: 1.0,
|
field_of_view_radians: 1.0,
|
||||||
atmosphere_seconds: Some(56.227),
|
atmosphere_seconds: Some(56.227),
|
||||||
geometry_projection: None,
|
geometry_projection: None,
|
||||||
|
sky_uv_clock: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -328,6 +328,24 @@ pub struct SkyMesh {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SkyMesh {
|
impl SkyMesh {
|
||||||
|
/// Updates native nebula, star and cloud scroll from the sky object's elapsed milliseconds.
|
||||||
|
pub fn update_uvs(&mut self, elapsed_ms: u32) {
|
||||||
|
let offsets = [
|
||||||
|
[(elapsed_ms % 300_000) as f32 * 3.333_333_3e-6; 2],
|
||||||
|
[(elapsed_ms % 100_000) as f32 * 1.0e-5; 2],
|
||||||
|
[(elapsed_ms % 50_000) as f32 * 2.0e-5, 0.0],
|
||||||
|
];
|
||||||
|
let inverse_radius = self.radius.recip();
|
||||||
|
for vertex in &mut self.vertices {
|
||||||
|
for stage in 0..3 {
|
||||||
|
let scale = inverse_radius * SKY_UV_SCALES[stage];
|
||||||
|
vertex.uv[stage] = std::array::from_fn(|axis| {
|
||||||
|
scale * vertex.position[axis] + offsets[stage][axis]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Number of triangles in the mesh.
|
/// Number of triangles in the mesh.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn triangle_count(&self) -> usize {
|
pub fn triangle_count(&self) -> usize {
|
||||||
@@ -1786,6 +1804,26 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scrolling_uvs_match_native_periods_and_wrapped_elapsed_clock() {
|
||||||
|
let mut mesh = SkyGeometryConfig::default().try_mesh(&sample()).unwrap();
|
||||||
|
let topology = mesh.indices.clone();
|
||||||
|
for elapsed in [0, 25_000, 49_999, 50_000, 100_000, 300_000, 4_051_271_258] {
|
||||||
|
mesh.update_uvs(elapsed);
|
||||||
|
let zenith = mesh.vertices[0].uv;
|
||||||
|
assert_eq!(zenith[0], [(elapsed % 300_000) as f32 * 3.333_333_3e-6; 2]);
|
||||||
|
assert_eq!(zenith[1], [(elapsed % 100_000) as f32 * 1e-5; 2]);
|
||||||
|
assert_eq!(zenith[2], [(elapsed % 50_000) as f32 * 2e-5, 0.0]);
|
||||||
|
}
|
||||||
|
// Live GOG sky render: now 265584022, object origin 509280060.
|
||||||
|
mesh.update_uvs(265_584_022_u32.wrapping_sub(509_280_060));
|
||||||
|
assert!((mesh.vertices[0].uv[2][0] - 0.42516).abs() < 1e-7);
|
||||||
|
let once = mesh.vertices.clone();
|
||||||
|
mesh.update_uvs(4_051_271_258);
|
||||||
|
assert_eq!(mesh.vertices, once, "updates must not accumulate drift");
|
||||||
|
assert_eq!(mesh.indices, topology);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn geometry_matches_original_counts_positions_uv_and_winding() {
|
fn geometry_matches_original_counts_positions_uv_and_winding() {
|
||||||
let config = SkyGeometryConfig::new(10.0, std::f32::consts::FRAC_PI_2, 2.0, 4, 3);
|
let config = SkyGeometryConfig::new(10.0, std::f32::consts::FRAC_PI_2, 2.0, 4, 3);
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ MSH-запрос со свободной preview-камерой; это не п
|
|||||||
обхода мира, которое может отсекать близкие поверхности. Это отмечается
|
обхода мира, которое может отсекать близкие поверхности. Это отмечается
|
||||||
`captured_projection_unverified`; для приёмки таких поверхностей нужен новый захват.
|
`captured_projection_unverified`; для приёмки таких поверхностей нужен новый захват.
|
||||||
|
|
||||||
|
Захваты без `sky_uv_clock` не подтверждают начальное положение облаков,
|
||||||
|
звёзд и фона. Replay использует elapsed clock приложения и считает
|
||||||
|
это как `captured_sky_phase_unverified`; для приёмки неба нужен новый захват.
|
||||||
|
|
||||||
Закрывающий эксперимент: запустить оригинал в совместимой Windows/DirectX
|
Закрывающий эксперимент: запустить оригинал в совместимой Windows/DirectX
|
||||||
среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state
|
среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state
|
||||||
log на минимальных сценах с одним типом материала.
|
log на минимальных сценах с одним типом материала.
|
||||||
|
|||||||
@@ -149,6 +149,31 @@ gradient и clouds row 2. Для nebula/stars/clouds используются UV
|
|||||||
directional RGB из `sky.packed[0]`. Это четыре фиксированных pass, а не три
|
directional RGB из `sky.packed[0]`. Это четыре фиксированных pass, а не три
|
||||||
независимых цветных слоя.
|
независимых цветных слоя.
|
||||||
|
|
||||||
|
Все три текстурных слоя движутся. Обновление GOG `CSky`
|
||||||
|
(`Terrain.dll`, RVA `0x48B90`, запись UV `0x4942A–0x495B7`;
|
||||||
|
[сборка и SHA-256](original-binaries.md)) берёт разность текущего `u32` clock
|
||||||
|
и сохранённого поля объекта `+8` с переполнением. Это отдельные миллисекунды
|
||||||
|
жизни неба, а не абсолютная фаза суток из SKE.
|
||||||
|
|
||||||
|
| Слой | Период | Добавка к исходным UV |
|
||||||
|
|---|---:|---|
|
||||||
|
| Фон | 300 000 мс | одинаковый сдвиг U и V |
|
||||||
|
| Звёзды | 100 000 мс | одинаковый сдвиг U и V |
|
||||||
|
| Облака | 50 000 мс | сдвиг только U |
|
||||||
|
|
||||||
|
Добавка равна остатку от деления elapsed clock на период, умноженному на
|
||||||
|
нативный коэффициент `f32` (`3.3333333e-6`, `1e-5`, `2e-5`). Вершинные UV
|
||||||
|
каждый раз вычисляются от позиции купола заново; накопление приращений
|
||||||
|
создавало бы дрейф. `SkyMesh::update_uvs` получает текущие миллисекунды кадра
|
||||||
|
и сохраняет геометрию купола. В работающем оригинале проверены UV всех трёх
|
||||||
|
слоёв, включая разность clock с переполнением.
|
||||||
|
|
||||||
|
Захват сохраняет `sky_uv_clock` на входе в рендер неба, проверяя его по
|
||||||
|
фактическим UV вершины зенита и clock того же кадра. Replay удерживает эту
|
||||||
|
фазу вместе с фазой суток. Без такого поля текстуры движутся по elapsed clock
|
||||||
|
приложения; начальная фаза оригинала неизвестна и явно считается как
|
||||||
|
`captured_sky_phase_unverified`.
|
||||||
|
|
||||||
Цвет первого screen-gradient и последнего пояса вычисляет
|
Цвет первого screen-gradient и последнего пояса вычисляет
|
||||||
`SkyFrame::screen_gradient(yaw_radians, glare_rgb_delta)`. Native `CSky` сначала
|
`SkyFrame::screen_gradient(yaw_radians, glare_rgb_delta)`. Native `CSky` сначала
|
||||||
округляет `(yaw + pi) * 180 / pi` режимом nearest-even, выбирает сектор
|
округляет `(yaw + pi) * 180 / pi` режимом nearest-even, выбирает сектор
|
||||||
|
|||||||
@@ -1187,10 +1187,15 @@ drawable размер `(right - left) × (bottom - top)` и отклоняет
|
|||||||
`FreeFlightCamera` в этом режиме не создаётся, но redraw продолжает обновлять
|
`FreeFlightCamera` в этом режиме не создаётся, но redraw продолжает обновлять
|
||||||
environment и projected shadows от captured camera. Перед environment update и
|
environment и projected shadows от captured camera. Перед environment update и
|
||||||
его audio events listener получает position, forward и up captured camera;
|
его audio events listener получает position, forward и up captured camera;
|
||||||
начальная сортировка world draw использует её же position. Phase capture может
|
начальная сортировка world draw использует её же position. Облака в обычном
|
||||||
|
запуске движутся с отдельным 50-секундным циклом UV; фон и звёзды имеют свои
|
||||||
|
периоды. Изменение цвета неба по расписанию не заменяет это движение.
|
||||||
|
Phase capture может
|
||||||
содержать наблюдаемый mission path и проверенный `atmosphere_seconds`; viewer
|
содержать наблюдаемый mission path и проверенный `atmosphere_seconds`; viewer
|
||||||
использует эту абсолютную фазу по умолчанию, если не указан явный CLI override,
|
использует эту абсолютную фазу по умолчанию, если не указан явный CLI override,
|
||||||
и удерживает sample атмосферы на ней через readback frames. Фазы FX следуют
|
и удерживает sample атмосферы на ней через readback frames. Дополнительный
|
||||||
|
`sky_uv_clock` фиксирует положение движущихся текстур на том же кадре;
|
||||||
|
без него их начальная фаза не подтверждена. Фазы FX следуют
|
||||||
выбранному control clock: без `--control-clock-ms` он обновляется по elapsed
|
выбранному control clock: без `--control-clock-ms` он обновляется по elapsed
|
||||||
time, а с явным значением readback frames переигрывают заданный clock. Это не
|
time, а с явным значением readback frames переигрывают заданный clock. Это не
|
||||||
полное simulation time: `weather`, RNG и
|
полное simulation time: `weather`, RNG и
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ internal static partial class NativeFrameCapture
|
|||||||
private const uint RenderGameRva = 0x13BD0;
|
private const uint RenderGameRva = 0x13BD0;
|
||||||
private const uint ProjectionSnapshotRva = 0x13CE4;
|
private const uint ProjectionSnapshotRva = 0x13CE4;
|
||||||
private const uint GeometryProjectionRva = 0x1D110;
|
private const uint GeometryProjectionRva = 0x1D110;
|
||||||
|
private const uint SkyRenderRva = 0x48110;
|
||||||
private const uint RenderReturnRva = 0x13D7B;
|
private const uint RenderReturnRva = 0x13D7B;
|
||||||
private const uint AtmospherePhaseRva = 0x421DC;
|
private const uint AtmospherePhaseRva = 0x421DC;
|
||||||
private const uint FogConsumerRva = 0x2F230;
|
private const uint FogConsumerRva = 0x2F230;
|
||||||
@@ -105,6 +106,7 @@ internal static partial class NativeFrameCapture
|
|||||||
public ProjectionReadback GeometryProjection;
|
public ProjectionReadback GeometryProjection;
|
||||||
public uint GeometryProjectionGeneration;
|
public uint GeometryProjectionGeneration;
|
||||||
public uint GeometryProjectionThreadId;
|
public uint GeometryProjectionThreadId;
|
||||||
|
public SkyUvReadback SkyUv;
|
||||||
public uint CameraWorldGameTimeWord;
|
public uint CameraWorldGameTimeWord;
|
||||||
public bool CameraWorldGameTimeWordReadable;
|
public bool CameraWorldGameTimeWordReadable;
|
||||||
public uint WorldGameTimeWord;
|
public uint WorldGameTimeWord;
|
||||||
@@ -133,6 +135,11 @@ internal static partial class NativeFrameCapture
|
|||||||
public string NativeFogStatus;
|
public string NativeFogStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class SkyUvReadback
|
||||||
|
{
|
||||||
|
public uint Object, Origin, RawClock, ElapsedMilliseconds, Generation, ThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class AtmosphereReadback
|
private sealed class AtmosphereReadback
|
||||||
{
|
{
|
||||||
public uint SampleThreadId;
|
public uint SampleThreadId;
|
||||||
@@ -780,6 +787,66 @@ internal static partial class NativeFrameCapture
|
|||||||
json.AppendLine(" },");
|
json.AppendLine(" },");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static SkyUvReadback CaptureSkyUv(IntPtr process, uint terrainBase, uint worldBase,
|
||||||
|
byte[] context, FrameSnapshot frame, uint threadId)
|
||||||
|
{
|
||||||
|
uint obj = BitConverter.ToUInt32(context, ContextEcxOffset);
|
||||||
|
uint stack = BitConverter.ToUInt32(context, ContextEspOffset);
|
||||||
|
if (frame.Camera == null || frame.CameraThreadId != threadId || frame.CameraGeneration == 0
|
||||||
|
|| ReadU32Exact(process, stack + 4) != frame.Camera.Camera
|
||||||
|
|| ReadU32Exact(process, obj) != unchecked(terrainBase + 0x65F98)) return null;
|
||||||
|
uint origin = ReadU32Exact(process, obj + 8);
|
||||||
|
uint rawClock = ReadU32Exact(process, unchecked(worldBase + 0x32A38));
|
||||||
|
uint elapsed = unchecked(rawClock - origin);
|
||||||
|
float[] expected = new float[] {
|
||||||
|
(elapsed % 300000) * 3.3333333e-6f, (elapsed % 300000) * 3.3333333e-6f,
|
||||||
|
(elapsed % 100000) * 1e-5f, (elapsed % 100000) * 1e-5f,
|
||||||
|
(elapsed % 50000) * 2e-5f, 0 };
|
||||||
|
uint[] pointers = new uint[] { ReadU32Exact(process, obj + 0x48),
|
||||||
|
ReadU32Exact(process, obj + 0x40), ReadU32Exact(process, obj + 0x44) };
|
||||||
|
for (int i = 0; i < expected.Length; i++)
|
||||||
|
{
|
||||||
|
float actual = BitConverter.ToSingle(BitConverter.GetBytes(
|
||||||
|
ReadU32Exact(process, pointers[i / 2] + (uint)(i % 2) * 4)), 0);
|
||||||
|
if (Single.IsNaN(actual) || Single.IsInfinity(actual)
|
||||||
|
|| Math.Abs(actual - expected[i]) > 1e-6f)
|
||||||
|
{
|
||||||
|
Log("SKY_UV_CLOCK_REJECTED zenith coordinate does not match the current world clock.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log("SKY_UV_CLOCK generation=" + frame.CameraGeneration + " tid=" + threadId
|
||||||
|
+ " raw=" + rawClock + " origin=" + origin + " elapsed_ms=" + elapsed);
|
||||||
|
return new SkyUvReadback { Object = obj, Origin = origin, RawClock = rawClock,
|
||||||
|
ElapsedMilliseconds = elapsed, Generation = frame.CameraGeneration, ThreadId = threadId };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool SkyUvMatchesFrame(FrameSnapshot frame)
|
||||||
|
{
|
||||||
|
SkyUvReadback sky = frame.SkyUv;
|
||||||
|
return sky != null && sky.Generation == frame.CameraGeneration
|
||||||
|
&& sky.Generation == frame.ProjectionGeneration && sky.ThreadId == frame.CameraThreadId
|
||||||
|
&& sky.ThreadId == frame.ProjectionThreadId && frame.WorldGameTimeWordReadable
|
||||||
|
&& sky.RawClock == frame.WorldGameTimeWord && frame.CameraWorldGameTimeWordReadable
|
||||||
|
&& sky.RawClock == frame.CameraWorldGameTimeWord;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendSkyUvJson(StringBuilder json, FrameSnapshot frame)
|
||||||
|
{
|
||||||
|
if (!SkyUvMatchesFrame(frame)) { json.AppendLine(" \"sky_uv_clock\": null,"); return; }
|
||||||
|
SkyUvReadback sky = frame.SkyUv;
|
||||||
|
json.AppendLine(" \"sky_uv_clock\": {");
|
||||||
|
json.AppendLine(" \"verified\": true,");
|
||||||
|
json.AppendLine(" \"sample_boundary\": \"Terrain+0x48110, sky render\",");
|
||||||
|
json.AppendLine(" \"terrain_module_sha256\": " + JsonString(ExpectedSha256[TerrainName]) + ",");
|
||||||
|
json.AppendLine(" \"raw_clock\": " + sky.RawClock + ",");
|
||||||
|
json.AppendLine(" \"origin_raw_clock\": " + sky.Origin + ",");
|
||||||
|
json.AppendLine(" \"frame_generation\": " + sky.Generation + ",");
|
||||||
|
json.AppendLine(" \"thread_id\": " + sky.ThreadId + ",");
|
||||||
|
json.AppendLine(" \"elapsed_milliseconds\": " + sky.ElapsedMilliseconds);
|
||||||
|
json.AppendLine(" },");
|
||||||
|
}
|
||||||
|
|
||||||
private static bool VerifyAtmospherePhaseBoundary(IntPtr process, uint terrainBase,
|
private static bool VerifyAtmospherePhaseBoundary(IntPtr process, uint terrainBase,
|
||||||
out byte firstByte, out string evidence)
|
out byte firstByte, out string evidence)
|
||||||
{
|
{
|
||||||
@@ -1931,6 +1998,7 @@ internal static partial class NativeFrameCapture
|
|||||||
json.AppendLine(" \"field_of_view_radians\": " + projection.Fov.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) + ",");
|
json.AppendLine(" \"projection_mode_byte\": " + projection.Mode.ToString(CultureInfo.InvariantCulture) + ",");
|
||||||
AppendGeometryProjectionJson(json, frame);
|
AppendGeometryProjectionJson(json, frame);
|
||||||
|
AppendSkyUvJson(json, frame);
|
||||||
if (frame.SelectedCamera == null) json.AppendLine(" \"selected_camera\": null,");
|
if (frame.SelectedCamera == null) json.AppendLine(" \"selected_camera\": null,");
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -2040,6 +2108,7 @@ internal static partial class NativeFrameCapture
|
|||||||
SelfCheckCameraInput();
|
SelfCheckCameraInput();
|
||||||
SelfCheckSelectedCameraWarmup();
|
SelfCheckSelectedCameraWarmup();
|
||||||
SelfCheckGeometryProjection();
|
SelfCheckGeometryProjection();
|
||||||
|
SelfCheckSkyUv();
|
||||||
SelfCheckNativeFogPairing();
|
SelfCheckNativeFogPairing();
|
||||||
SelfCheckRenderInvocationGates();
|
SelfCheckRenderInvocationGates();
|
||||||
SelfCheckDebugEventHandleOwnership();
|
SelfCheckDebugEventHandleOwnership();
|
||||||
@@ -2350,6 +2419,32 @@ internal static partial class NativeFrameCapture
|
|||||||
return SelfCheckReadback();
|
return SelfCheckReadback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void SelfCheckSkyUv()
|
||||||
|
{
|
||||||
|
FrameSnapshot frame = new FrameSnapshot { CameraGeneration = 7, ProjectionGeneration = 7,
|
||||||
|
CameraThreadId = 11, ProjectionThreadId = 11, WorldGameTimeWordReadable = true,
|
||||||
|
CameraWorldGameTimeWordReadable = true, CameraWorldGameTimeWord = 265584022,
|
||||||
|
WorldGameTimeWord = 265584022,
|
||||||
|
SkyUv = new SkyUvReadback { Generation = 7, ThreadId = 11, RawClock = 265584022,
|
||||||
|
Origin = 509280060, ElapsedMilliseconds = unchecked(265584022U - 509280060U) } };
|
||||||
|
StringBuilder json = new StringBuilder();
|
||||||
|
AppendSkyUvJson(json, frame);
|
||||||
|
if (!SkyUvMatchesFrame(frame) || json.ToString().IndexOf("4051271258", StringComparison.Ordinal) < 0)
|
||||||
|
throw new InvalidOperationException("Verified sky UV wrapping clock was rejected.");
|
||||||
|
frame.SkyUv.ThreadId++;
|
||||||
|
if (SkyUvMatchesFrame(frame)) throw new InvalidOperationException("Sky UV thread mismatch accepted.");
|
||||||
|
frame.SkyUv.ThreadId--;
|
||||||
|
frame.SkyUv.Generation++;
|
||||||
|
if (SkyUvMatchesFrame(frame)) throw new InvalidOperationException("Sky UV generation mismatch accepted.");
|
||||||
|
frame.SkyUv.Generation--;
|
||||||
|
frame.SkyUv.RawClock++;
|
||||||
|
json.Length = 0;
|
||||||
|
AppendSkyUvJson(json, frame);
|
||||||
|
if (SkyUvMatchesFrame(frame) || json.ToString().IndexOf("null", StringComparison.Ordinal) < 0)
|
||||||
|
throw new InvalidOperationException("Sky UV clock mismatch accepted.");
|
||||||
|
Console.WriteLine("self-check: sky UV clock requires matching thread/generation/world clock and preserves u32 wrap OK");
|
||||||
|
}
|
||||||
|
|
||||||
private static void SelfCheckGeometryProjection()
|
private static void SelfCheckGeometryProjection()
|
||||||
{
|
{
|
||||||
FrameSnapshot frame = new FrameSnapshot {
|
FrameSnapshot frame = new FrameSnapshot {
|
||||||
@@ -3018,6 +3113,7 @@ internal static partial class NativeFrameCapture
|
|||||||
BreakpointInfo cameraBreakpoint = null;
|
BreakpointInfo cameraBreakpoint = null;
|
||||||
BreakpointInfo projectionBreakpoint = null;
|
BreakpointInfo projectionBreakpoint = null;
|
||||||
BreakpointInfo geometryProjectionBreakpoint = null;
|
BreakpointInfo geometryProjectionBreakpoint = null;
|
||||||
|
BreakpointInfo skyUvBreakpoint = null;
|
||||||
BreakpointInfo returnBreakpoint = null;
|
BreakpointInfo returnBreakpoint = null;
|
||||||
BreakpointInfo atmosphereBreakpoint = null;
|
BreakpointInfo atmosphereBreakpoint = null;
|
||||||
BreakpointInfo beginSceneBreakpoint = null;
|
BreakpointInfo beginSceneBreakpoint = null;
|
||||||
@@ -3208,6 +3304,14 @@ internal static partial class NativeFrameCapture
|
|||||||
throw new InvalidOperationException("Terrain geometry projection boundary signature failed.");
|
throw new InvalidOperationException("Terrain geometry projection boundary signature failed.");
|
||||||
geometryProjectionBreakpoint = ArmBreakpoint(process, "Terrain.geometry-projection",
|
geometryProjectionBreakpoint = ArmBreakpoint(process, "Terrain.geometry-projection",
|
||||||
unchecked(terrainBase + GeometryProjectionRva), geometryPrefix[0]);
|
unchecked(terrainBase + GeometryProjectionRva), geometryPrefix[0]);
|
||||||
|
byte[] skyPrefix = new byte[] { 0x6A, 0xFF, 0x68, 0, 0, 0, 0, 0x64, 0xA1, 0, 0, 0, 0 };
|
||||||
|
Array.Copy(BitConverter.GetBytes(unchecked(terrainBase + 0x637E6)), 0, skyPrefix, 3, 4);
|
||||||
|
byte[] skyCode = new byte[skyPrefix.Length];
|
||||||
|
if (!ReadExact(process, unchecked(terrainBase + SkyRenderRva), skyCode)
|
||||||
|
|| !ByteArraysEqual(skyCode, skyPrefix))
|
||||||
|
throw new InvalidOperationException("Terrain sky render boundary signature failed.");
|
||||||
|
skyUvBreakpoint = ArmBreakpoint(process, "Terrain.sky-uv-clock",
|
||||||
|
unchecked(terrainBase + SkyRenderRva), skyPrefix[0]);
|
||||||
byte atmosphereByte;
|
byte atmosphereByte;
|
||||||
string atmosphereEvidence;
|
string atmosphereEvidence;
|
||||||
if (!VerifyAtmospherePhaseBoundary(process, terrainBase, out atmosphereByte, out atmosphereEvidence))
|
if (!VerifyAtmospherePhaseBoundary(process, terrainBase, out atmosphereByte, out atmosphereEvidence))
|
||||||
@@ -3676,6 +3780,9 @@ internal static partial class NativeFrameCapture
|
|||||||
else if (geometryProjectionBreakpoint != null && geometryProjectionBreakpoint.Armed
|
else if (geometryProjectionBreakpoint != null && geometryProjectionBreakpoint.Armed
|
||||||
&& exceptionAddress == geometryProjectionBreakpoint.Address)
|
&& exceptionAddress == geometryProjectionBreakpoint.Address)
|
||||||
hitBreakpoint = geometryProjectionBreakpoint;
|
hitBreakpoint = geometryProjectionBreakpoint;
|
||||||
|
else if (skyUvBreakpoint != null && skyUvBreakpoint.Armed
|
||||||
|
&& exceptionAddress == skyUvBreakpoint.Address)
|
||||||
|
hitBreakpoint = skyUvBreakpoint;
|
||||||
else if (returnBreakpoint != null && returnBreakpoint.Armed && exceptionAddress == returnBreakpoint.Address)
|
else if (returnBreakpoint != null && returnBreakpoint.Armed && exceptionAddress == returnBreakpoint.Address)
|
||||||
hitBreakpoint = returnBreakpoint;
|
hitBreakpoint = returnBreakpoint;
|
||||||
else if (atmosphereBreakpoint != null && atmosphereBreakpoint.Armed && exceptionAddress == atmosphereBreakpoint.Address)
|
else if (atmosphereBreakpoint != null && atmosphereBreakpoint.Armed && exceptionAddress == atmosphereBreakpoint.Address)
|
||||||
@@ -3750,6 +3857,10 @@ internal static partial class NativeFrameCapture
|
|||||||
throw new InvalidOperationException("Could not disarm the previous generation's native fog boundary.");
|
throw new InvalidOperationException("Could not disarm the previous generation's native fog boundary.");
|
||||||
frame.CameraGeneration++;
|
frame.CameraGeneration++;
|
||||||
frame.GeometryProjection = null;
|
frame.GeometryProjection = null;
|
||||||
|
frame.SkyUv = null;
|
||||||
|
if (skyUvBreakpoint != null && !skyUvBreakpoint.Armed)
|
||||||
|
skyUvBreakpoint = ArmBreakpoint(process, "Terrain.sky-uv-clock",
|
||||||
|
skyUvBreakpoint.Address, skyUvBreakpoint.OriginalByte);
|
||||||
frame.GeometryProjectionGeneration = 0;
|
frame.GeometryProjectionGeneration = 0;
|
||||||
frame.GeometryProjectionThreadId = 0;
|
frame.GeometryProjectionThreadId = 0;
|
||||||
if (geometryProjectionBreakpoint != null && !geometryProjectionBreakpoint.Armed)
|
if (geometryProjectionBreakpoint != null && !geometryProjectionBreakpoint.Armed)
|
||||||
@@ -3843,6 +3954,12 @@ internal static partial class NativeFrameCapture
|
|||||||
frame, eventTid, ref fogConsumerBreakpoint);
|
frame, eventTid, ref fogConsumerBreakpoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (hitBreakpoint == skyUvBreakpoint)
|
||||||
|
{
|
||||||
|
if (frame.SkyUv == null && world3dBase != 0)
|
||||||
|
frame.SkyUv = CaptureSkyUv(process, terrainBase, world3dBase,
|
||||||
|
stoppedContext, frame, eventTid);
|
||||||
|
}
|
||||||
else if (hitBreakpoint == geometryProjectionBreakpoint)
|
else if (hitBreakpoint == geometryProjectionBreakpoint)
|
||||||
{
|
{
|
||||||
uint esp = BitConverter.ToUInt32(stoppedContext, ContextEspOffset);
|
uint esp = BitConverter.ToUInt32(stoppedContext, ContextEspOffset);
|
||||||
@@ -4086,7 +4203,7 @@ internal static partial class NativeFrameCapture
|
|||||||
{
|
{
|
||||||
remote = StartRemoteSurfaceProbe(process, eventTid, ngiBase,
|
remote = StartRemoteSurfaceProbe(process, eventTid, ngiBase,
|
||||||
returnBreakpoint, frame,
|
returnBreakpoint, frame,
|
||||||
new BreakpointInfo[] { cameraBreakpoint, projectionBreakpoint, geometryProjectionBreakpoint,
|
new BreakpointInfo[] { cameraBreakpoint, projectionBreakpoint, geometryProjectionBreakpoint, skyUvBreakpoint,
|
||||||
returnBreakpoint, atmosphereBreakpoint, beginSceneBreakpoint,
|
returnBreakpoint, atmosphereBreakpoint, beginSceneBreakpoint,
|
||||||
fogConsumerBreakpoint,
|
fogConsumerBreakpoint,
|
||||||
endSceneBreakpoint, presentBreakpoint,
|
endSceneBreakpoint, presentBreakpoint,
|
||||||
@@ -4234,7 +4351,7 @@ internal static partial class NativeFrameCapture
|
|||||||
{
|
{
|
||||||
remote = StartRemoteSurfaceProbe(process, eventTid, ngiBase,
|
remote = StartRemoteSurfaceProbe(process, eventTid, ngiBase,
|
||||||
endSceneBreakpoint, frame,
|
endSceneBreakpoint, frame,
|
||||||
new BreakpointInfo[] { cameraBreakpoint, projectionBreakpoint, geometryProjectionBreakpoint,
|
new BreakpointInfo[] { cameraBreakpoint, projectionBreakpoint, geometryProjectionBreakpoint, skyUvBreakpoint,
|
||||||
returnBreakpoint, atmosphereBreakpoint, beginSceneBreakpoint,
|
returnBreakpoint, atmosphereBreakpoint, beginSceneBreakpoint,
|
||||||
endSceneBreakpoint,
|
endSceneBreakpoint,
|
||||||
fogConsumerBreakpoint, presentBreakpoint,
|
fogConsumerBreakpoint, presentBreakpoint,
|
||||||
@@ -4282,7 +4399,7 @@ internal static partial class NativeFrameCapture
|
|||||||
{
|
{
|
||||||
remote = StartRemoteBoundaryReadback(process, eventTid, ngiBase,
|
remote = StartRemoteBoundaryReadback(process, eventTid, ngiBase,
|
||||||
endSceneBreakpoint, frame, outputPath,
|
endSceneBreakpoint, frame, outputPath,
|
||||||
new BreakpointInfo[] { cameraBreakpoint, projectionBreakpoint, geometryProjectionBreakpoint,
|
new BreakpointInfo[] { cameraBreakpoint, projectionBreakpoint, geometryProjectionBreakpoint, skyUvBreakpoint,
|
||||||
returnBreakpoint, atmosphereBreakpoint, beginSceneBreakpoint,
|
returnBreakpoint, atmosphereBreakpoint, beginSceneBreakpoint,
|
||||||
endSceneBreakpoint,
|
endSceneBreakpoint,
|
||||||
fogConsumerBreakpoint, presentBreakpoint,
|
fogConsumerBreakpoint, presentBreakpoint,
|
||||||
@@ -4502,6 +4619,8 @@ internal static partial class NativeFrameCapture
|
|||||||
&& !frame.NativeFogSampleLimitExceeded;
|
&& !frame.NativeFogSampleLimitExceeded;
|
||||||
if (hitBreakpoint == geometryProjectionBreakpoint)
|
if (hitBreakpoint == geometryProjectionBreakpoint)
|
||||||
rearmAfterStep = frame.GeometryProjection == null;
|
rearmAfterStep = frame.GeometryProjection == null;
|
||||||
|
if (hitBreakpoint == skyUvBreakpoint)
|
||||||
|
rearmAfterStep = frame.SkyUv == null;
|
||||||
BeginSingleStep(process, eventTid, hitBreakpoint, stoppedContext,
|
BeginSingleStep(process, eventTid, hitBreakpoint, stoppedContext,
|
||||||
out pendingStep, rearmAfterStep);
|
out pendingStep, rearmAfterStep);
|
||||||
Log("SINGLE_STEP_STARTED boundary=" + hitBreakpoint.Name + " tid=" + eventTid);
|
Log("SINGLE_STEP_STARTED boundary=" + hitBreakpoint.Name + " tid=" + eventTid);
|
||||||
@@ -4574,7 +4693,7 @@ internal static partial class NativeFrameCapture
|
|||||||
captureFailure = captureFailure ?? "No matching native camera/projection/present frame completed before the bounded timeout.";
|
captureFailure = captureFailure ?? "No matching native camera/projection/present frame completed before the bounded timeout.";
|
||||||
Log("NO_FRAME_CAPTURE bounded timeout at stopped debug event");
|
Log("NO_FRAME_CAPTURE bounded timeout at stopped debug event");
|
||||||
if (!RestoreArmedBreakpointsWhileStopped(process,
|
if (!RestoreArmedBreakpointsWhileStopped(process,
|
||||||
cameraBreakpoint, projectionBreakpoint, geometryProjectionBreakpoint, returnBreakpoint, atmosphereBreakpoint,
|
cameraBreakpoint, projectionBreakpoint, geometryProjectionBreakpoint, skyUvBreakpoint, returnBreakpoint, atmosphereBreakpoint,
|
||||||
beginSceneBreakpoint,
|
beginSceneBreakpoint,
|
||||||
endSceneBreakpoint, fogConsumerBreakpoint, presentBreakpoint,
|
endSceneBreakpoint, fogConsumerBreakpoint, presentBreakpoint,
|
||||||
presentCompletionBreakpoint, missionBreakpoint))
|
presentCompletionBreakpoint, missionBreakpoint))
|
||||||
@@ -4595,6 +4714,7 @@ internal static partial class NativeFrameCapture
|
|||||||
&& (cameraBreakpoint != null && cameraBreakpoint.Armed
|
&& (cameraBreakpoint != null && cameraBreakpoint.Armed
|
||||||
|| projectionBreakpoint != null && projectionBreakpoint.Armed
|
|| projectionBreakpoint != null && projectionBreakpoint.Armed
|
||||||
|| geometryProjectionBreakpoint != null && geometryProjectionBreakpoint.Armed
|
|| geometryProjectionBreakpoint != null && geometryProjectionBreakpoint.Armed
|
||||||
|
|| skyUvBreakpoint != null && skyUvBreakpoint.Armed
|
||||||
|| returnBreakpoint != null && returnBreakpoint.Armed
|
|| returnBreakpoint != null && returnBreakpoint.Armed
|
||||||
|| atmosphereBreakpoint != null && atmosphereBreakpoint.Armed
|
|| atmosphereBreakpoint != null && atmosphereBreakpoint.Armed
|
||||||
|| beginSceneBreakpoint != null && beginSceneBreakpoint.Armed
|
|| beginSceneBreakpoint != null && beginSceneBreakpoint.Armed
|
||||||
@@ -4605,7 +4725,7 @@ internal static partial class NativeFrameCapture
|
|||||||
|| missionBreakpoint != null && missionBreakpoint.Armed))
|
|| missionBreakpoint != null && missionBreakpoint.Armed))
|
||||||
{
|
{
|
||||||
if (!RestoreArmedBreakpointsWhileStopped(process,
|
if (!RestoreArmedBreakpointsWhileStopped(process,
|
||||||
cameraBreakpoint, projectionBreakpoint, geometryProjectionBreakpoint, returnBreakpoint, atmosphereBreakpoint,
|
cameraBreakpoint, projectionBreakpoint, geometryProjectionBreakpoint, skyUvBreakpoint, returnBreakpoint, atmosphereBreakpoint,
|
||||||
beginSceneBreakpoint,
|
beginSceneBreakpoint,
|
||||||
endSceneBreakpoint, fogConsumerBreakpoint, presentBreakpoint,
|
endSceneBreakpoint, fogConsumerBreakpoint, presentBreakpoint,
|
||||||
presentCompletionBreakpoint, missionBreakpoint))
|
presentCompletionBreakpoint, missionBreakpoint))
|
||||||
@@ -4655,6 +4775,7 @@ internal static partial class NativeFrameCapture
|
|||||||
|| cameraBreakpoint != null && cameraBreakpoint.Armed
|
|| cameraBreakpoint != null && cameraBreakpoint.Armed
|
||||||
|| projectionBreakpoint != null && projectionBreakpoint.Armed
|
|| projectionBreakpoint != null && projectionBreakpoint.Armed
|
||||||
|| geometryProjectionBreakpoint != null && geometryProjectionBreakpoint.Armed
|
|| geometryProjectionBreakpoint != null && geometryProjectionBreakpoint.Armed
|
||||||
|
|| skyUvBreakpoint != null && skyUvBreakpoint.Armed
|
||||||
|| returnBreakpoint != null && returnBreakpoint.Armed
|
|| returnBreakpoint != null && returnBreakpoint.Armed
|
||||||
|| atmosphereBreakpoint != null && atmosphereBreakpoint.Armed
|
|| atmosphereBreakpoint != null && atmosphereBreakpoint.Armed
|
||||||
|| beginSceneBreakpoint != null && beginSceneBreakpoint.Armed
|
|| beginSceneBreakpoint != null && beginSceneBreakpoint.Armed
|
||||||
@@ -4684,7 +4805,7 @@ internal static partial class NativeFrameCapture
|
|||||||
if (DrainFrameTerminationEvents(process, eventBuffer, processId,
|
if (DrainFrameTerminationEvents(process, eventBuffer, processId,
|
||||||
ref eventOutstanding, ref outstandingPid, ref outstandingTid, ref outstandingCode,
|
ref eventOutstanding, ref outstandingPid, ref outstandingTid, ref outstandingCode,
|
||||||
ref outstandingContinueStatus, ref outstandingFileHandleClosed,
|
ref outstandingContinueStatus, ref outstandingFileHandleClosed,
|
||||||
new BreakpointInfo[] { cameraBreakpoint, projectionBreakpoint, geometryProjectionBreakpoint, returnBreakpoint,
|
new BreakpointInfo[] { cameraBreakpoint, projectionBreakpoint, geometryProjectionBreakpoint, skyUvBreakpoint, returnBreakpoint,
|
||||||
atmosphereBreakpoint, beginSceneBreakpoint, endSceneBreakpoint, fogConsumerBreakpoint,
|
atmosphereBreakpoint, beginSceneBreakpoint, endSceneBreakpoint, fogConsumerBreakpoint,
|
||||||
presentBreakpoint, presentCompletionBreakpoint, missionBreakpoint },
|
presentBreakpoint, presentCompletionBreakpoint, missionBreakpoint },
|
||||||
out drainEvidence))
|
out drainEvidence))
|
||||||
@@ -4753,7 +4874,7 @@ internal static partial class NativeFrameCapture
|
|||||||
if (DrainFrameTerminationEvents(process, eventBuffer, processId,
|
if (DrainFrameTerminationEvents(process, eventBuffer, processId,
|
||||||
ref eventOutstanding, ref outstandingPid, ref outstandingTid, ref outstandingCode,
|
ref eventOutstanding, ref outstandingPid, ref outstandingTid, ref outstandingCode,
|
||||||
ref outstandingContinueStatus, ref outstandingFileHandleClosed,
|
ref outstandingContinueStatus, ref outstandingFileHandleClosed,
|
||||||
new BreakpointInfo[] { cameraBreakpoint, projectionBreakpoint, geometryProjectionBreakpoint, returnBreakpoint,
|
new BreakpointInfo[] { cameraBreakpoint, projectionBreakpoint, geometryProjectionBreakpoint, skyUvBreakpoint, returnBreakpoint,
|
||||||
atmosphereBreakpoint, beginSceneBreakpoint, endSceneBreakpoint, fogConsumerBreakpoint,
|
atmosphereBreakpoint, beginSceneBreakpoint, endSceneBreakpoint, fogConsumerBreakpoint,
|
||||||
presentBreakpoint, presentCompletionBreakpoint, missionBreakpoint },
|
presentBreakpoint, presentCompletionBreakpoint, missionBreakpoint },
|
||||||
out drainEvidence))
|
out drainEvidence))
|
||||||
@@ -4791,7 +4912,7 @@ internal static partial class NativeFrameCapture
|
|||||||
else Log("PROCESS_STATE_AFTER_DETACH unreadable error=" + Marshal.GetLastWin32Error());
|
else Log("PROCESS_STATE_AFTER_DETACH unreadable error=" + Marshal.GetLastWin32Error());
|
||||||
}
|
}
|
||||||
breakpointsRestored = processExited || AreBreakpointsRestored(cameraBreakpoint,
|
breakpointsRestored = processExited || AreBreakpointsRestored(cameraBreakpoint,
|
||||||
projectionBreakpoint, geometryProjectionBreakpoint, returnBreakpoint, atmosphereBreakpoint, beginSceneBreakpoint,
|
projectionBreakpoint, geometryProjectionBreakpoint, skyUvBreakpoint, returnBreakpoint, atmosphereBreakpoint, beginSceneBreakpoint,
|
||||||
endSceneBreakpoint, fogConsumerBreakpoint, presentBreakpoint,
|
endSceneBreakpoint, fogConsumerBreakpoint, presentBreakpoint,
|
||||||
presentCompletionBreakpoint, missionBreakpoint);
|
presentCompletionBreakpoint, missionBreakpoint);
|
||||||
Log("FINAL_BREAKPOINT_STATE restored=" + breakpointsRestored + " processExited=" + processExited);
|
Log("FINAL_BREAKPOINT_STATE restored=" + breakpointsRestored + " processExited=" + processExited);
|
||||||
@@ -4923,7 +5044,7 @@ internal static partial class NativeFrameCapture
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static bool RestoreArmedBreakpointsWhileStopped(IntPtr process,
|
private static bool RestoreArmedBreakpointsWhileStopped(IntPtr process,
|
||||||
BreakpointInfo cameraBreakpoint, BreakpointInfo projectionBreakpoint, BreakpointInfo geometryProjectionBreakpoint, BreakpointInfo returnBreakpoint,
|
BreakpointInfo cameraBreakpoint, BreakpointInfo projectionBreakpoint, BreakpointInfo geometryProjectionBreakpoint, BreakpointInfo skyUvBreakpoint, BreakpointInfo returnBreakpoint,
|
||||||
BreakpointInfo atmosphereBreakpoint, BreakpointInfo beginSceneBreakpoint, BreakpointInfo endSceneBreakpoint,
|
BreakpointInfo atmosphereBreakpoint, BreakpointInfo beginSceneBreakpoint, BreakpointInfo endSceneBreakpoint,
|
||||||
BreakpointInfo fogConsumerBreakpoint,
|
BreakpointInfo fogConsumerBreakpoint,
|
||||||
BreakpointInfo presentBreakpoint, BreakpointInfo presentCompletionBreakpoint,
|
BreakpointInfo presentBreakpoint, BreakpointInfo presentCompletionBreakpoint,
|
||||||
@@ -4932,6 +5053,7 @@ internal static partial class NativeFrameCapture
|
|||||||
return RestoreBreakpointWhileStopped(process, cameraBreakpoint)
|
return RestoreBreakpointWhileStopped(process, cameraBreakpoint)
|
||||||
&& RestoreBreakpointWhileStopped(process, projectionBreakpoint)
|
&& RestoreBreakpointWhileStopped(process, projectionBreakpoint)
|
||||||
&& RestoreBreakpointWhileStopped(process, geometryProjectionBreakpoint)
|
&& RestoreBreakpointWhileStopped(process, geometryProjectionBreakpoint)
|
||||||
|
&& RestoreBreakpointWhileStopped(process, skyUvBreakpoint)
|
||||||
&& RestoreBreakpointWhileStopped(process, returnBreakpoint)
|
&& RestoreBreakpointWhileStopped(process, returnBreakpoint)
|
||||||
&& RestoreBreakpointWhileStopped(process, atmosphereBreakpoint)
|
&& RestoreBreakpointWhileStopped(process, atmosphereBreakpoint)
|
||||||
&& RestoreBreakpointWhileStopped(process, beginSceneBreakpoint)
|
&& RestoreBreakpointWhileStopped(process, beginSceneBreakpoint)
|
||||||
@@ -4943,7 +5065,7 @@ internal static partial class NativeFrameCapture
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static bool AreBreakpointsRestored(BreakpointInfo cameraBreakpoint,
|
private static bool AreBreakpointsRestored(BreakpointInfo cameraBreakpoint,
|
||||||
BreakpointInfo projectionBreakpoint, BreakpointInfo geometryProjectionBreakpoint, BreakpointInfo returnBreakpoint,
|
BreakpointInfo projectionBreakpoint, BreakpointInfo geometryProjectionBreakpoint, BreakpointInfo skyUvBreakpoint, BreakpointInfo returnBreakpoint,
|
||||||
BreakpointInfo atmosphereBreakpoint, BreakpointInfo beginSceneBreakpoint, BreakpointInfo endSceneBreakpoint,
|
BreakpointInfo atmosphereBreakpoint, BreakpointInfo beginSceneBreakpoint, BreakpointInfo endSceneBreakpoint,
|
||||||
BreakpointInfo fogConsumerBreakpoint,
|
BreakpointInfo fogConsumerBreakpoint,
|
||||||
BreakpointInfo presentBreakpoint, BreakpointInfo presentCompletionBreakpoint,
|
BreakpointInfo presentBreakpoint, BreakpointInfo presentCompletionBreakpoint,
|
||||||
@@ -4952,6 +5074,7 @@ internal static partial class NativeFrameCapture
|
|||||||
return (cameraBreakpoint == null || !cameraBreakpoint.Armed)
|
return (cameraBreakpoint == null || !cameraBreakpoint.Armed)
|
||||||
&& (projectionBreakpoint == null || !projectionBreakpoint.Armed)
|
&& (projectionBreakpoint == null || !projectionBreakpoint.Armed)
|
||||||
&& (geometryProjectionBreakpoint == null || !geometryProjectionBreakpoint.Armed)
|
&& (geometryProjectionBreakpoint == null || !geometryProjectionBreakpoint.Armed)
|
||||||
|
&& (skyUvBreakpoint == null || !skyUvBreakpoint.Armed)
|
||||||
&& (returnBreakpoint == null || !returnBreakpoint.Armed)
|
&& (returnBreakpoint == null || !returnBreakpoint.Armed)
|
||||||
&& (atmosphereBreakpoint == null || !atmosphereBreakpoint.Armed)
|
&& (atmosphereBreakpoint == null || !atmosphereBreakpoint.Armed)
|
||||||
&& (beginSceneBreakpoint == null || !beginSceneBreakpoint.Armed)
|
&& (beginSceneBreakpoint == null || !beginSceneBreakpoint.Armed)
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ Capture modes attach only to the isolated GOG-compatible scratch copy of `Parkan
|
|||||||
|
|
||||||
`geometry_projection` records the renderer's perspective values at the first indexed geometry draw (`Terrain+0x1D110`) using that world's camera matrix and renderer. It is emitted only when thread, generation, renderer, viewport, far plane and FOV match the later World3D boundary. The near plane may differ: the observed world draw uses `0.5`, while the restored camera boundary contains `5`. FParkan prefers this verified object when replaying the capture. The top-level projection fields remain the camera-boundary tuple used for selected-camera preflight. Older captures without this object still load, with `render_coverage captured_projection_unverified=1`; nearby surfaces in those replays can be clipped incorrectly. Capture a fresh frame to verify them.
|
`geometry_projection` records the renderer's perspective values at the first indexed geometry draw (`Terrain+0x1D110`) using that world's camera matrix and renderer. It is emitted only when thread, generation, renderer, viewport, far plane and FOV match the later World3D boundary. The near plane may differ: the observed world draw uses `0.5`, while the restored camera boundary contains `5`. FParkan prefers this verified object when replaying the capture. The top-level projection fields remain the camera-boundary tuple used for selected-camera preflight. Older captures without this object still load, with `render_coverage captured_projection_unverified=1`; nearby surfaces in those replays can be clipped incorrectly. Capture a fresh frame to verify them.
|
||||||
|
|
||||||
|
`sky_uv_clock` records the independent scrolling-texture clock at sky render entry (`Terrain+0x48110`). It requires a verified module/prologue, the selected camera, matching render thread/generation/world clock, and zenith UV values matching all three native scroll formulas. It preserves wrapping `u32` subtraction from the sky object's origin clock. FParkan holds this phase during exact capture replay; ordinary application frames animate the cloud, star and nebula UVs with periods of 50, 100 and 300 seconds. Older captures without this object still load and report `render_coverage captured_sky_phase_unverified=1`; their sky texture phase is not a native reference.
|
||||||
|
|
||||||
The JSON keeps the `fparkan-legacy-camera-v1` schema and adds `native_fog_status`, diagnostic counts, and `native_fog`. The latter is an optional passive CPU-side snapshot of the effective sink flags/RGB/range plus the paired calls to the `Terrain+0x2F230` consumer. The flags byte is raw and not a decoded fog mode. Each draw record preserves the distance argument, raw bypass dword and effective low byte, source color, output pointers, and return address. Geometry at `EBP` is included only for the verified `Terrain+0x18A6A` caller with the expected geometry vtable and slot+0x14 target; other callers have `distance_geometry: null`. The fog object is `null` unless the samples match the same render thread, camera, generation, and projection matrix and agree on sink settings. `native_fog_status` is `paired`, `conflict`, `unpaired`, or `incomplete`; `native_fog_status_reason` explains an unpaired, conflicting, or incomplete result. The counts include processed, overflow, paired, rejected, other-thread, and matrix-mismatch calls. At most 512 calls are processed per camera generation; reaching the limit stops sampling and reports an incomplete result. These fields are additive: camera-input validation continues to accept older JSON without them. This probe observes CPU consumer state; it does not set fog or change GPU state.
|
The JSON keeps the `fparkan-legacy-camera-v1` schema and adds `native_fog_status`, diagnostic counts, and `native_fog`. The latter is an optional passive CPU-side snapshot of the effective sink flags/RGB/range plus the paired calls to the `Terrain+0x2F230` consumer. The flags byte is raw and not a decoded fog mode. Each draw record preserves the distance argument, raw bypass dword and effective low byte, source color, output pointers, and return address. Geometry at `EBP` is included only for the verified `Terrain+0x18A6A` caller with the expected geometry vtable and slot+0x14 target; other callers have `distance_geometry: null`. The fog object is `null` unless the samples match the same render thread, camera, generation, and projection matrix and agree on sink settings. `native_fog_status` is `paired`, `conflict`, `unpaired`, or `incomplete`; `native_fog_status_reason` explains an unpaired, conflicting, or incomplete result. The counts include processed, overflow, paired, rejected, other-thread, and matrix-mismatch calls. At most 512 calls are processed per camera generation; reaching the limit stops sampling and reports an incomplete result. These fields are additive: camera-input validation continues to accept older JSON without them. This probe observes CPU consumer state; it does not set fog or change GPU state.
|
||||||
|
|
||||||
The capture helper expects an isolated copy at `target\shadow-probe\Parkan - Iron Strategy`; the original GOG installation stays untouched. For the default local install path, create that scratch copy from the repository root with:
|
The capture helper expects an isolated copy at `target\shadow-probe\Parkan - Iron Strategy`; the original GOG installation stays untouched. For the default local install path, create that scratch copy from the repository root with:
|
||||||
|
|||||||
Reference in new issue
Block a user