feat(game): merge preview root components
Docs Deploy / Build and Deploy MkDocs (push) Successful in 33s
Test / Lint (push) Failing after 2m4s
Test / Test (push) Skipped
Test / Render parity (push) Skipped

This commit is contained in:
2026-07-18 10:06:34 +04:00
parent 3633c91a9b
commit 1b62c2c315
4 changed files with 208 additions and 64 deletions
+2 -2
View File
@@ -27,12 +27,12 @@ Open source проект с реализацией компонентов игр
- [apps/fparkan-cli](apps/fparkan-cli) — CLI для архивов, графов и acceptance-отчетов. - [apps/fparkan-cli](apps/fparkan-cli) — CLI для архивов, графов и acceptance-отчетов.
- [apps/fparkan-viewer](apps/fparkan-viewer) — inspection-only CLI для archive/model/texture/map без live Vulkan draw path. - [apps/fparkan-viewer](apps/fparkan-viewer) — inspection-only CLI для archive/model/texture/map без live Vulkan draw path.
- [apps/fparkan-headless](apps/fparkan-headless) — headless runtime composition root. - [apps/fparkan-headless](apps/fparkan-headless) — headless runtime composition root.
- [apps/fparkan-game](apps/fparkan-game) — mission composition root: по умолчанию выдаёт planning report; opt-in `--backend static-vulkan` использует first-root preview loader и открывает native Vulkan окно для первой MSH-модели выбранного root. - [apps/fparkan-game](apps/fparkan-game) — mission composition root: по умолчанию выдаёт planning report; opt-in `--backend static-vulkan` использует first-root preview loader и открывает native Vulkan окно для всех подготовленных MSH-компонентов выбранного root.
## Текущий статус рендера ## Текущий статус рендера
- `fparkan-vulkan-smoke` доказывает живой Stage 0 Vulkan triangle path с native window, swapchain и validation telemetry. - `fparkan-vulkan-smoke` доказывает живой Stage 0 Vulkan triangle path с native window, swapchain и validation telemetry.
- `VulkanPlanningBackend` и default-режим `fparkan-game` подтверждают только deterministic command planning/capture, а не draw пикселей. `fparkan-game --backend static-vulkan` — узкий mission-to-native-Vulkan bridge: для первого root он загружает первую MAT0 diffuse texture на каждый используемый MSH selector; полный corpus, material phases, transforms и pixel parity ещё не подтверждены. - `VulkanPlanningBackend` и default-режим `fparkan-game` подтверждают только deterministic command planning/capture, а не draw пикселей. `fparkan-game --backend static-vulkan` — узкий mission-to-native-Vulkan bridge: для первого root он объединяет все подготовленные MSH-компоненты и загружает первую MAT0 diffuse texture на каждый используемый selector с preview-local remap; полный corpus, material phases, transforms и pixel parity ещё не подтверждены.
- `fparkan-viewer` пока является инспектором ассетов. `fparkan-vulkan-smoke` имеет live Stage 3 bridge для original `MSH`/`Texm`/`WEAR`/`MAT0` и geometry-only `Land.msh`; полноценный viewer, исходные terrain-material states, camera и pixel parity ещё не закрыты. - `fparkan-viewer` пока является инспектором ассетов. `fparkan-vulkan-smoke` имеет live Stage 3 bridge для original `MSH`/`Texm`/`WEAR`/`MAT0` и geometry-only `Land.msh`; полноценный viewer, исходные terrain-material states, camera и pixel parity ещё не закрыты.
- Truth table и evidence-артефакты вынесены в [`docs/rendering/renderer_truth_table.md`](docs/rendering/renderer_truth_table.md) и [`docs/evidence/`](docs/evidence). - Truth table и evidence-артефакты вынесены в [`docs/rendering/renderer_truth_table.md`](docs/rendering/renderer_truth_table.md) и [`docs/evidence/`](docs/evidence).
+178 -41
View File
@@ -82,10 +82,11 @@ fn run(args: &[String]) -> Result<String, String> {
if args.backend == RenderBackendMode::StaticVulkan { if args.backend == RenderBackendMode::StaticVulkan {
let mission_assets = loaded_mission_assets(&engine) let mission_assets = loaded_mission_assets(&engine)
.ok_or_else(|| "mission assets are unavailable after loading".to_string())?; .ok_or_else(|| "mission assets are unavailable after loading".to_string())?;
let (mesh, materials) = static_preview_mesh_and_materials(mission_assets)?; let preview = static_preview_mesh_and_materials(mission_assets)?;
return run_static_vulkan_mode( return run_static_vulkan_mode(
mesh, preview.mesh,
materials, preview.materials,
preview.mesh_components,
args.frames, args.frames,
&args.mission, &args.mission,
loaded.object_count, loaded.object_count,
@@ -177,48 +178,87 @@ fn load_requested_mission(
.map_err(|err| err.to_string()) .map_err(|err| err.to_string())
} }
/// Projects the explicitly bounded static-preview root into geometry and its /// Static geometry and descriptors belonging to the first preview root.
/// selector-keyed diffuse material descriptors. struct StaticPreviewScene {
mesh: VulkanStaticMesh,
materials: Vec<VulkanStaticMaterial>,
mesh_components: usize,
}
/// Projects every MSH component of the explicitly bounded static-preview root
/// into one mesh and selector-keyed diffuse material descriptors.
/// ///
/// This intentionally takes only the first MAT0 texture request for each MSH /// This intentionally takes only the first MAT0 texture request for each MSH
/// batch selector. Later material phases, animation, lightmaps, transforms, /// batch selector. Later material phases, animation, lightmaps, transforms,
/// and gameplay visibility remain outside this preview bridge. /// and gameplay visibility remain outside this preview bridge.
fn static_preview_mesh_and_materials( fn static_preview_mesh_and_materials(assets: &MissionAssets) -> Result<StaticPreviewScene, String> {
let visual_ids = assets.visuals_for_object(0);
if visual_ids.is_empty() {
return Err("first static preview root has no prepared visuals".to_string());
}
let mut mesh = VulkanStaticMesh {
vertices: Vec::new(),
indices: Vec::new(),
draw_ranges: Vec::new(),
};
let mut materials = Vec::new();
let mut mesh_components = 0;
for visual_id in visual_ids {
let visual = assets.visual_by_id(*visual_id).ok_or_else(|| {
format!("first static preview root references unknown visual {visual_id:?}")
})?;
let Some(model_id) = visual.model_id else {
continue;
};
let model = assets.model_by_id(model_id).ok_or_else(|| {
format!("static preview visual {visual_id:?} references unknown model {model_id:?}")
})?;
let component = project_msh_to_static_mesh(&model.validated)
.map_err(|err| format!("project mission MSH for Vulkan: {err}"))?;
let selector_remap =
static_preview_component_materials(assets, visual, &component, &mut materials)?;
append_static_preview_component(&mut mesh, component, &selector_remap)?;
mesh_components += 1;
}
if mesh_components == 0 {
return Err("first static preview root has no mesh-backed visual".to_string());
}
Ok(StaticPreviewScene {
mesh,
materials,
mesh_components,
})
}
fn static_preview_component_materials(
assets: &MissionAssets, assets: &MissionAssets,
) -> Result<(VulkanStaticMesh, Vec<VulkanStaticMaterial>), String> { visual: &PreparedVisual,
let model = assets.models.first().ok_or_else(|| { mesh: &VulkanStaticMesh,
"mission has no mesh-backed model for the static Vulkan bridge".to_string() materials: &mut Vec<VulkanStaticMaterial>,
})?; ) -> Result<Vec<(u16, u16)>, String> {
let mesh = project_msh_to_static_mesh(&model.validated) let mut source_selectors = mesh
.map_err(|err| format!("project mission MSH for Vulkan: {err}"))?;
let visual = assets
.visuals
.iter()
.find(|visual| visual.model_id == Some(model.id))
.ok_or_else(|| "static preview model has no prepared visual".to_string())?;
let mut selectors = mesh
.draw_ranges .draw_ranges
.iter() .iter()
.map(|range| range.material_index) .map(|range| range.material_index)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
selectors.sort_unstable(); source_selectors.sort_unstable();
selectors.dedup(); source_selectors.dedup();
source_selectors
let materials = selectors
.into_iter() .into_iter()
.map(|material_index| { .map(|source_selector| {
let material_id = visual.material_ids.get(usize::from(material_index)).ok_or_else(|| { let material_id = visual
format!( .material_ids
"static preview MSH batch selector {material_index} has no prepared WEAR material" .get(usize::from(source_selector))
) .ok_or_else(|| {
})?; format!(
"static preview MSH batch selector {source_selector} has no prepared WEAR material"
)
})?;
let material = assets.material_by_id(*material_id).ok_or_else(|| { let material = assets.material_by_id(*material_id).ok_or_else(|| {
format!( format!("static preview prepared material {source_selector} is unavailable")
"static preview prepared material {material_index} is unavailable"
)
})?; })?;
let texture_name = material.texture_requests.first().ok_or_else(|| { let texture_name = material.texture_requests.first().ok_or_else(|| {
format!("static preview material {material_index} has no MAT0 diffuse texture") format!("static preview material {source_selector} has no MAT0 diffuse texture")
})?; })?;
let texture = assets let texture = assets
.textures .textures
@@ -229,25 +269,81 @@ fn static_preview_mesh_and_materials(
}) })
.ok_or_else(|| { .ok_or_else(|| {
format!( format!(
"static preview diffuse texture {texture_name:?} for material {material_index} is unavailable" "static preview diffuse texture {texture_name:?} for material {source_selector} is unavailable"
) )
})?; })?;
let image = texture.decode_mip_rgba8(0).map_err(|err| { let image = texture.decode_mip_rgba8(0).map_err(|err| {
format!( format!(
"decode static preview diffuse texture {texture_name:?} for material {material_index}: {err}" "decode static preview diffuse texture {texture_name:?} for material {source_selector}: {err}"
) )
})?; })?;
Ok(VulkanStaticMaterial { let preview_selector = u16::try_from(materials.len()).map_err(|_| {
material_index, "static preview exceeds the available 16-bit material selector space".to_string()
})?;
materials.push(VulkanStaticMaterial {
material_index: preview_selector,
texture: VulkanStaticTexture { texture: VulkanStaticTexture {
width: image.width, width: image.width,
height: image.height, height: image.height,
rgba8: image.rgba8, rgba8: image.rgba8,
}, },
}) });
Ok((source_selector, preview_selector))
}) })
.collect::<Result<Vec<_>, String>>()?; .collect()
Ok((mesh, materials)) }
fn append_static_preview_component(
target: &mut VulkanStaticMesh,
component: VulkanStaticMesh,
selector_remap: &[(u16, u16)],
) -> Result<(), String> {
let vertex_base = u16::try_from(target.vertices.len()).map_err(|_| {
"static preview exceeds the available 16-bit vertex index space".to_string()
})?;
let first_index_base = u32::try_from(target.indices.len())
.map_err(|_| "static preview index count exceeds u32".to_string())?;
target
.vertices
.len()
.checked_add(component.vertices.len())
.filter(|count| *count <= usize::from(u16::MAX) + 1)
.ok_or_else(|| {
"static preview exceeds the available 16-bit vertex index space".to_string()
})?;
target.vertices.extend(component.vertices);
target.indices.extend(
component
.indices
.into_iter()
.map(|index| {
index.checked_add(vertex_base).ok_or_else(|| {
"static preview exceeds the available 16-bit vertex index space".to_string()
})
})
.collect::<Result<Vec<_>, _>>()?,
);
for range in component.draw_ranges {
let material_index = selector_remap
.iter()
.find_map(|(source, preview)| (*source == range.material_index).then_some(*preview))
.ok_or_else(|| {
format!(
"static preview component has no material remap for selector {}",
range.material_index
)
})?;
target
.draw_ranges
.push(fparkan_render_vulkan::VulkanStaticDrawRange {
first_index: first_index_base
.checked_add(range.first_index)
.ok_or_else(|| "static preview index range exceeds u32".to_string())?,
material_index,
..range
});
}
Ok(())
} }
fn prepare_load_progress_path(path: &std::path::Path) -> Result<(), String> { fn prepare_load_progress_path(path: &std::path::Path) -> Result<(), String> {
@@ -264,13 +360,21 @@ fn write_load_progress(path: &std::path::Path, phase: MissionLoadPhase) -> Resul
fn run_static_vulkan_mode( fn run_static_vulkan_mode(
mesh: VulkanStaticMesh, mesh: VulkanStaticMesh,
materials: Vec<VulkanStaticMaterial>, materials: Vec<VulkanStaticMaterial>,
mesh_components: usize,
target_frames: u64, target_frames: u64,
mission: &str, mission: &str,
object_count: usize, object_count: usize,
) -> Result<String, String> { ) -> Result<String, String> {
let event_loop = EventLoop::new().map_err(|err| format!("winit event loop: {err}"))?; let event_loop = EventLoop::new().map_err(|err| format!("winit event loop: {err}"))?;
event_loop.set_control_flow(ControlFlow::Poll); event_loop.set_control_flow(ControlFlow::Poll);
let mut app = StaticVulkanApp::new(mesh, materials, target_frames, mission, object_count); let mut app = StaticVulkanApp::new(
mesh,
materials,
mesh_components,
target_frames,
mission,
object_count,
);
if let Err(err) = event_loop.run_app(&mut app) { if let Err(err) = event_loop.run_app(&mut app) {
app.error = Some(format!("winit event loop: {err}")); app.error = Some(format!("winit event loop: {err}"));
} }
@@ -280,6 +384,7 @@ fn run_static_vulkan_mode(
struct StaticVulkanApp { struct StaticVulkanApp {
mesh: VulkanStaticMesh, mesh: VulkanStaticMesh,
materials: Vec<VulkanStaticMaterial>, materials: Vec<VulkanStaticMaterial>,
mesh_components: usize,
target_frames: u64, target_frames: u64,
mission: String, mission: String,
object_count: usize, object_count: usize,
@@ -295,6 +400,7 @@ impl StaticVulkanApp {
fn new( fn new(
mesh: VulkanStaticMesh, mesh: VulkanStaticMesh,
materials: Vec<VulkanStaticMaterial>, materials: Vec<VulkanStaticMaterial>,
mesh_components: usize,
target_frames: u64, target_frames: u64,
mission: &str, mission: &str,
object_count: usize, object_count: usize,
@@ -302,6 +408,7 @@ impl StaticVulkanApp {
Self { Self {
mesh, mesh,
materials, materials,
mesh_components,
target_frames, target_frames,
mission: mission.to_string(), mission: mission.to_string(),
object_count, object_count,
@@ -344,10 +451,11 @@ impl StaticVulkanApp {
return; return;
} }
self.output = Some(format!( self.output = Some(format!(
"{{\"report_kind\":\"rendered-static-mission\",\"backend\":\"vulkan-static\",\"window\":\"native\",\"mission\":{},\"objects\":{},\"frames\":{},\"material_descriptors\":{},\"swapchain\":[{},{}],\"swapchain_images\":{},\"validation_warnings\":{},\"validation_errors\":{},\"readback_bytes\":{},\"readback_hash\":{}}}", "{{\"report_kind\":\"rendered-static-mission\",\"backend\":\"vulkan-static\",\"window\":\"native\",\"mission\":{},\"objects\":{},\"frames\":{},\"mesh_components\":{},\"material_descriptors\":{},\"swapchain\":[{},{}],\"swapchain_images\":{},\"validation_warnings\":{},\"validation_errors\":{},\"readback_bytes\":{},\"readback_hash\":{}}}",
json_string(&self.mission), json_string(&self.mission),
self.object_count, self.object_count,
self.frames_presented, self.frames_presented,
self.mesh_components,
self.materials.len(), self.materials.len(),
report.renderer_report.swapchain_extent.0, report.renderer_report.swapchain_extent.0,
report.renderer_report.swapchain_extent.1, report.renderer_report.swapchain_extent.1,
@@ -776,6 +884,35 @@ mod tests {
); );
} }
#[test]
fn static_preview_component_merge_offsets_indices_and_remaps_local_selectors(
) -> Result<(), String> {
let mut merged = VulkanStaticMesh {
vertices: Vec::new(),
indices: Vec::new(),
draw_ranges: Vec::new(),
};
append_static_preview_component(
&mut merged,
VulkanStaticMesh::smoke_triangle(),
&[(0, 4)],
)?;
append_static_preview_component(
&mut merged,
VulkanStaticMesh::smoke_triangle(),
&[(0, 9)],
)?;
assert_eq!(merged.vertices.len(), 6);
assert_eq!(merged.indices, vec![0, 1, 2, 3, 4, 5]);
assert_eq!(merged.draw_ranges.len(), 2);
assert_eq!(merged.draw_ranges[0].first_index, 0);
assert_eq!(merged.draw_ranges[0].material_index, 4);
assert_eq!(merged.draw_ranges[1].first_index, 3);
assert_eq!(merged.draw_ranges[1].material_index, 9);
Ok(())
}
#[test] #[test]
fn render_commands_follow_snapshot_order() -> Result<(), String> { fn render_commands_follow_snapshot_order() -> Result<(), String> {
let snapshot = WorldSnapshot { let snapshot = WorldSnapshot {
+10 -8
View File
@@ -13,7 +13,7 @@
| `RecordingBackend` | No | No | Optional CPU-side IDs only | `covered-planning` | Stable command capture for backend-neutral tests | Native window, Vulkan, GPU resource lifetime, pixels | | `RecordingBackend` | No | No | Optional CPU-side IDs only | `covered-planning` | Stable command capture for backend-neutral tests | Native window, Vulkan, GPU resource lifetime, pixels |
| `NullBackend` | No | No | Optional CPU-side IDs only | Usually `covered` for validation-only rows | Command stream framing and bounds validation | Capture stability, GPU execution, pixels | | `NullBackend` | No | No | Optional CPU-side IDs only | Usually `covered` for validation-only rows | Command stream framing and bounds validation | Capture stability, GPU execution, pixels |
| `VulkanAssetRenderer` | Yes | Yes | Yes | `covered-gpu` | Static original asset rendering: MSH/Texm/WEAR/MAT0/terrain through Vulkan | Animation/FX parity unless explicitly wired | | `VulkanAssetRenderer` | Yes | Yes | Yes | `covered-gpu` | Static original asset rendering: MSH/Texm/WEAR/MAT0/terrain through Vulkan | Animation/FX parity unless explicitly wired |
| `fparkan-game --backend static-vulkan` | Yes, GOG `Autodemo.00` | Yes, static MSH draw | First MSH from first mission root; first MAT0 diffuse TEXM per used selector | `covered-gpu` only for the narrow static-preview bridge | Opt-in mission-to-native-window bootstrap, static MSH projection, selector-keyed diffuse descriptor upload and synchronized teardown telemetry | Full mission scene, later MAT0 phases/animation, lightmaps, placed transforms/orientation, camera, gameplay, original-runtime parity | | `fparkan-game --backend static-vulkan` | Yes, GOG/Part 1/Part 2 `Autodemo.00` | Yes, merged static MSH component draws | All prepared MSH components from first mission root; first MAT0 diffuse TEXM per source selector | `covered-gpu` only for the narrow static-preview bridge | Opt-in mission-to-native-window bootstrap, component mesh merge, preview-local selector remap, diffuse descriptor upload and synchronized teardown telemetry | Full mission scene, later MAT0 phases/animation, lightmaps, placed transforms/orientation, camera, gameplay, original-runtime parity |
| Future rendered `fparkan-game` mode | Yes | Yes | Yes | `covered-gpu` plus original-evidence IDs | Mission-driven render snapshot execution and pixel capture | Original-runtime parity for animation/FX/x87 without dedicated captures | | Future rendered `fparkan-game` mode | Yes | Yes | Yes | `covered-gpu` plus original-evidence IDs | Mission-driven render snapshot execution and pixel capture | Original-runtime parity for animation/FX/x87 without dedicated captures |
## Rules ## Rules
@@ -34,17 +34,19 @@
- Lightmap остаётся отдельным, не реализованным contract: оригинальный `World3D.dll` экспортирует самостоятельный `SetLightMapLib` наряду с `SetTexturesLib` и `SetMaterialLib`; WEAR содержит независимый блок `LIGHTMAPS`. Текущая документация не подтверждает связь этих slots с `Batch20.material_index` или их UV/channel semantics, поэтому viewer не подменяет lightmap diffuse texture и не добавляет недоказанное binding. - Lightmap остаётся отдельным, не реализованным contract: оригинальный `World3D.dll` экспортирует самостоятельный `SetLightMapLib` наряду с `SetTexturesLib` и `SetMaterialLib`; WEAR содержит независимый блок `LIGHTMAPS`. Текущая документация не подтверждает связь этих slots с `Batch20.material_index` или их UV/channel semantics, поэтому viewer не подменяет lightmap diffuse texture и не добавляет недоказанное binding.
- `apps/fparkan-game` по умолчанию выдает `render-planning` JSON report поверх - `apps/fparkan-game` по умолчанию выдает `render-planning` JSON report поверх
synthetic window descriptor и `VulkanPlanningBackend`. Opt-in `--backend static-vulkan` synthetic window descriptor и `VulkanPlanningBackend`. Opt-in `--backend static-vulkan`
уже создаёт native `winit` window и передаёт первую подготовленную MSH модели миссии в уже создаёт native `winit` window и передаёт все подготовленные MSH-компоненты первого root в
`VulkanSmokeRenderer` вместе с первым diffuse TEXM из MAT0 для каждого реально используемого `VulkanSmokeRenderer`. Каждый исходный `Batch20.material_index` сначала разрешается внутри
`Batch20.material_index`. Режим использует собственного WEAR/MAT0 visual, затем получает уникальный preview-local selector и первый
diffuse TEXM. Режим использует
отдельный first-root preview loader: normal `load_mission` по-прежнему готовит все reachable отдельный first-root preview loader: normal `load_mission` по-прежнему готовит все reachable
assets и весь graph, тогда как preview строит graph и готовит assets только для первого assets и весь graph, тогда как preview строит graph и готовит assets только для первого
mission root. `--load-progress <file>` writes the last entered loader phase synchronously for mission root. `--load-progress <file>` writes the last entered loader phase synchronously for
timeout diagnosis. Fresh GOG `MISSIONS/Autodemo.00/data.tma` run passed in 38.7 seconds with timeout diagnosis. Fresh GOG `MISSIONS/Autodemo.00/data.tma` run passed in 38.7 seconds with
one presented frame, native 1280×720 swapchain (2 images), one original diffuse material one presented frame, native 1280×720 swapchain (2 images), 14 mesh components and 14 original
descriptor, 7,372,800-byte readback hash `12332214764918703197`, and validation diffuse material descriptors, 7,372,800-byte readback hash `16595193636416981301`, and
warnings/errors `0/0`. This is `covered-gpu` evidence for that narrow static-preview bridge validation warnings/errors `0/0`. Part 1 matches that artifact; Part 2 passes validation with
only, not full-scene or original-renderer parity. 14/14 but has a distinct hash `18268338333658342130`. This is `covered-gpu` evidence for that
narrow static-preview bridge only, not full-scene or original-renderer parity.
- `apps/fparkan-viewer` сейчас inspection-only CLI и не открывает live Vulkan - `apps/fparkan-viewer` сейчас inspection-only CLI и не открывает live Vulkan
asset viewer. asset viewer.
- Следующий реальный milestone для rendered acceptance: `VulkanAssetRenderer` - Следующий реальный milestone для rendered acceptance: `VulkanAssetRenderer`
+18 -13
View File
@@ -938,33 +938,38 @@ from a runtime slot or draw order.
`fparkan-game --backend static-vulkan` is an explicit native-window experiment, `fparkan-game --backend static-vulkan` is an explicit native-window experiment,
not the default planning path. It visits only the first mission root, selects not the default planning path. It visits only the first mission root, selects
its prepared MSH model, sends it through the existing static XZ clip-space every prepared MSH component of that root, merges their static XZ clip-space
projection and renders a requested number of frames through geometry, and renders a requested number of frames through
`VulkanSmokeRenderer`; teardown rejects validation warnings/errors and reports `VulkanSmokeRenderer`; teardown rejects validation warnings/errors and reports
swapchain/readback telemetry. For each used `Batch20.material_index`, it resolves swapchain/readback telemetry. For each used `Batch20.material_index`, it resolves
the positional prepared WEAR material and uploads mip 0 of that material's first the positional prepared WEAR material and uploads mip 0 of that material's first
MAT0 diffuse texture request as a selector-keyed descriptor. MAT0 diffuse texture request. Because `material_index` is local to an MSH/WEAR
component, the merged preview assigns a unique preview-local selector only after
this source resolution; it does not falsely treat equal local indexes as equal
materials.
This is a narrow bootstrap from mission assets to a live Vulkan renderer. It This is a narrow bootstrap from mission assets to a live Vulkan renderer. It
does not render every placed object, apply TMA transforms or orientation, select does not render every placed object, apply TMA transforms or orientation, select
later MAT0 phases or animation, bind lightmaps, or establish a game camera. Fresh GOG later MAT0 phases or animation, bind lightmaps, or establish a game camera. Fresh GOG
`MISSIONS/Autodemo.00/data.tma` evidence now proves the narrow GPU bridge: one `MISSIONS/Autodemo.00/data.tma` evidence now proves the narrow GPU bridge: one
presented frame completed in 38.7 seconds with a native 1280×720 two-image presented frame completed in 39.6 seconds with a native 1280×720 two-image
swapchain, one selector-keyed original diffuse descriptor, 7,372,800-byte swapchain, 14 merged mesh components and 14 selector-keyed original diffuse
synchronized readback (FNV-1a `12332214764918703197`) and validation descriptors, 7,372,800-byte synchronized readback (FNV-1a
`16595193636416981301`) and validation
warnings/errors `0/0`. This is not a warnings/errors `0/0`. This is not a
full-scene or original-renderer pixel-parity claim. full-scene or original-renderer pixel-parity claim.
The same bounded command was then run against the licensed installed Part 1 The same bounded command was then run against the licensed installed Part 1
(`C:\\Program Files (x86)\\Nikita\\IS`) and Part 2 (`C:\\Program Files (x86)\\Nikita\\IS`) and Part 2
(`C:\\Program Files (x86)\\Nikita\\IS2`) corpora supplied for testing. Both (`C:\\Program Files (x86)\\Nikita\\IS2`) corpora supplied for testing. Both
`MISSIONS/Autodemo.00/data.tma` runs completed with the same one-material, `MISSIONS/Autodemo.00/data.tma` runs completed with 14 mesh components and 14
1280×720 two-image report, the same `12332214764918703197` readback hash and original diffuse descriptors. Part 1 took 28.2 seconds and matched the GOG
validation warnings/errors `0/0`; Part 1 took 28.5 seconds and Part 2 took readback hash `16595193636416981301`; Part 2 took 93.6 seconds and remained
94.5 seconds. Part 2's synchronous checkpoint was `Graph` while it was still validation-clean but produced distinct hash `18268338333658342130`. Part 2's
loading, so the longer startup is not attributed to Vulkan. This is only a synchronous checkpoint was `Graph` while it was still loading, so the longer
cross-corpus confirmation of the first-root static-preview bridge, not a claim startup is not attributed to Vulkan. This is only a cross-corpus confirmation
that the two games' full mission renderers are compatible. of the first-root static-preview bridge, not a claim that the two games' full
mission renderers are compatible.
The preview now asks `load_mission_static_preview` for both graph and assets. The preview now asks `load_mission_static_preview` for both graph and assets.
Normal mission loading remains full and transactional; preview graph traversal Normal mission loading remains full and transactional; preview graph traversal