feat(game): add static mission Vulkan mode
This commit is contained in:
Generated
+1
@@ -453,6 +453,7 @@ dependencies = [
|
|||||||
"fparkan-runtime",
|
"fparkan-runtime",
|
||||||
"fparkan-vfs",
|
"fparkan-vfs",
|
||||||
"fparkan-world",
|
"fparkan-world",
|
||||||
|
"winit",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -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) — render-planning composition root; сейчас выдает planning report, а не живой отрисованный кадр.
|
- [apps/fparkan-game](apps/fparkan-game) — mission composition root: по умолчанию выдаёт planning report; opt-in `--backend static-vulkan` открывает native Vulkan окно для первой подготовленной MSH-модели миссии.
|
||||||
|
|
||||||
## Текущий статус рендера
|
## Текущий статус рендера
|
||||||
|
|
||||||
- `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` и `fparkan-game` подтверждают только deterministic command planning/capture, а не draw пикселей.
|
- `VulkanPlanningBackend` и default-режим `fparkan-game` подтверждают только deterministic command planning/capture, а не draw пикселей. Новый `fparkan-game --backend static-vulkan` — узкий mission-to-native-Vulkan bridge без подтверждённого полного corpus run, текстур, 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).
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan", version
|
|||||||
fparkan-runtime = { path = "../../crates/fparkan-runtime", version = "0.1.0" }
|
fparkan-runtime = { path = "../../crates/fparkan-runtime", version = "0.1.0" }
|
||||||
fparkan-vfs = { path = "../../crates/fparkan-vfs", version = "0.1.0" }
|
fparkan-vfs = { path = "../../crates/fparkan-vfs", version = "0.1.0" }
|
||||||
fparkan-world = { path = "../../crates/fparkan-world", version = "0.1.0" }
|
fparkan-world = { path = "../../crates/fparkan-world", version = "0.1.0" }
|
||||||
|
winit = { version = "0.30", default-features = false, features = ["rwh_06"] }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -23,13 +23,16 @@
|
|||||||
|
|
||||||
use fparkan_assets::PreparedVisual;
|
use fparkan_assets::PreparedVisual;
|
||||||
use fparkan_platform::WindowPort;
|
use fparkan_platform::WindowPort;
|
||||||
use fparkan_platform_winit::WinitWindow;
|
use fparkan_platform_winit::{window_native_handles, WinitWindow, WinitWindowPlan};
|
||||||
use fparkan_render::{
|
use fparkan_render::{
|
||||||
build_commands, CameraSnapshot, DrawId, GpuMaterialId, GpuMeshId, IndexRange, RenderBackend,
|
build_commands, CameraSnapshot, DrawId, GpuMaterialId, GpuMeshId, IndexRange, RenderBackend,
|
||||||
RenderCommand, RenderCommandList, RenderPhase, RenderProfile, RenderSnapshot,
|
RenderCommand, RenderCommandList, RenderPhase, RenderProfile, RenderSnapshot,
|
||||||
RenderSnapshotDraw,
|
RenderSnapshotDraw,
|
||||||
};
|
};
|
||||||
use fparkan_render_vulkan::VulkanPlanningBackend;
|
use fparkan_render_vulkan::{
|
||||||
|
project_msh_to_static_mesh, VulkanPlanningBackend, VulkanSmokeFrameOutcome,
|
||||||
|
VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanStaticMesh,
|
||||||
|
};
|
||||||
use fparkan_runtime::{
|
use fparkan_runtime::{
|
||||||
create, frame, load_mission, loaded_mission_assets, EngineConfig, EngineMode, EngineServices,
|
create, frame, load_mission, loaded_mission_assets, EngineConfig, EngineMode, EngineServices,
|
||||||
MissionAssets, MissionObjectDraft, MissionRequest,
|
MissionAssets, MissionObjectDraft, MissionRequest,
|
||||||
@@ -40,6 +43,11 @@ use fparkan_world::OriginalObjectId;
|
|||||||
use fparkan_world::WorldSnapshot;
|
use fparkan_world::WorldSnapshot;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use winit::application::ApplicationHandler;
|
||||||
|
use winit::dpi::PhysicalSize as WinitPhysicalSize;
|
||||||
|
use winit::event::WindowEvent;
|
||||||
|
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||||
|
use winit::window::{Window, WindowId};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let raw_args = std::env::args().skip(1).collect::<Vec<_>>();
|
let raw_args = std::env::args().skip(1).collect::<Vec<_>>();
|
||||||
@@ -74,6 +82,17 @@ fn run(args: &[String]) -> Result<String, String> {
|
|||||||
)
|
)
|
||||||
.map_err(|err| err.to_string())?;
|
.map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
|
if args.backend == RenderBackendMode::StaticVulkan {
|
||||||
|
let mission_assets = loaded_mission_assets(&engine)
|
||||||
|
.ok_or_else(|| "mission assets are unavailable after loading".to_string())?;
|
||||||
|
let model = mission_assets.models.first().ok_or_else(|| {
|
||||||
|
"mission has no mesh-backed model for the static Vulkan bridge".to_string()
|
||||||
|
})?;
|
||||||
|
let mesh = project_msh_to_static_mesh(&model.validated)
|
||||||
|
.map_err(|err| format!("project mission MSH for Vulkan: {err}"))?;
|
||||||
|
return run_static_vulkan_mode(mesh, args.frames, &args.mission, loaded.object_count);
|
||||||
|
}
|
||||||
|
|
||||||
let mut backend = VulkanPlanningBackend::new();
|
let mut backend = VulkanPlanningBackend::new();
|
||||||
let _request = WinitWindow::default_render_request();
|
let _request = WinitWindow::default_render_request();
|
||||||
let window = WinitWindow::synthetic(1280, 720);
|
let window = WinitWindow::synthetic(1280, 720);
|
||||||
@@ -116,6 +135,212 @@ fn run(args: &[String]) -> Result<String, String> {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_static_vulkan_mode(
|
||||||
|
mesh: VulkanStaticMesh,
|
||||||
|
target_frames: u64,
|
||||||
|
mission: &str,
|
||||||
|
object_count: usize,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let event_loop = EventLoop::new().map_err(|err| format!("winit event loop: {err}"))?;
|
||||||
|
event_loop.set_control_flow(ControlFlow::Poll);
|
||||||
|
let mut app = StaticVulkanApp::new(mesh, target_frames, mission, object_count);
|
||||||
|
if let Err(err) = event_loop.run_app(&mut app) {
|
||||||
|
app.error = Some(format!("winit event loop: {err}"));
|
||||||
|
}
|
||||||
|
app.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StaticVulkanApp {
|
||||||
|
mesh: VulkanStaticMesh,
|
||||||
|
target_frames: u64,
|
||||||
|
mission: String,
|
||||||
|
object_count: usize,
|
||||||
|
window_id: Option<WindowId>,
|
||||||
|
window: Option<Window>,
|
||||||
|
renderer: Option<VulkanSmokeRenderer>,
|
||||||
|
frames_presented: u64,
|
||||||
|
output: Option<String>,
|
||||||
|
error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StaticVulkanApp {
|
||||||
|
fn new(mesh: VulkanStaticMesh, target_frames: u64, mission: &str, object_count: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
mesh,
|
||||||
|
target_frames,
|
||||||
|
mission: mission.to_string(),
|
||||||
|
object_count,
|
||||||
|
window_id: None,
|
||||||
|
window: None,
|
||||||
|
renderer: None,
|
||||||
|
frames_presented: 0,
|
||||||
|
output: None,
|
||||||
|
error: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schedule_next_redraw(&self) {
|
||||||
|
if let Some(window) = self.window.as_ref() {
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn complete(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
|
let Some(renderer) = self.renderer.take() else {
|
||||||
|
self.error = Some("native Vulkan renderer was not initialized".to_string());
|
||||||
|
event_loop.exit();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let report = match renderer.shutdown() {
|
||||||
|
Ok(report) => report,
|
||||||
|
Err(err) => {
|
||||||
|
self.error = Some(err.to_string());
|
||||||
|
event_loop.exit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.window.take();
|
||||||
|
if report.validation.warning_count != 0 || report.validation.error_count != 0 {
|
||||||
|
self.error = Some(format!(
|
||||||
|
"native Vulkan validation must stay clean (warnings={}, errors={})",
|
||||||
|
report.validation.warning_count, report.validation.error_count
|
||||||
|
));
|
||||||
|
event_loop.exit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.output = Some(format!(
|
||||||
|
"{{\"report_kind\":\"rendered-static-mission\",\"backend\":\"vulkan-static\",\"window\":\"native\",\"mission\":{},\"objects\":{},\"frames\":{},\"swapchain\":[{},{}],\"swapchain_images\":{},\"validation_warnings\":{},\"validation_errors\":{},\"readback_bytes\":{},\"readback_hash\":{}}}",
|
||||||
|
json_string(&self.mission),
|
||||||
|
self.object_count,
|
||||||
|
self.frames_presented,
|
||||||
|
report.renderer_report.swapchain_extent.0,
|
||||||
|
report.renderer_report.swapchain_extent.1,
|
||||||
|
report.renderer_report.swapchain_image_count,
|
||||||
|
report.validation.warning_count,
|
||||||
|
report.validation.error_count,
|
||||||
|
report.renderer_report.readback_byte_count,
|
||||||
|
report.renderer_report.readback_fnv1a64,
|
||||||
|
));
|
||||||
|
event_loop.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(self) -> Result<String, String> {
|
||||||
|
self.output.ok_or_else(|| {
|
||||||
|
self.error.unwrap_or_else(|| {
|
||||||
|
"native Vulkan mode exited before producing a report".to_string()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApplicationHandler for StaticVulkanApp {
|
||||||
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
|
if self.window.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let plan = match WinitWindowPlan::smoke().validate() {
|
||||||
|
Ok(plan) => plan,
|
||||||
|
Err(err) => {
|
||||||
|
self.error = Some(err.to_string());
|
||||||
|
event_loop.exit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let attributes = Window::default_attributes()
|
||||||
|
.with_title("FParkan static mission Vulkan")
|
||||||
|
.with_inner_size(WinitPhysicalSize::new(plan.width, plan.height));
|
||||||
|
let window = match event_loop.create_window(attributes) {
|
||||||
|
Ok(window) => window,
|
||||||
|
Err(err) => {
|
||||||
|
self.error = Some(format!("winit window: {err}"));
|
||||||
|
event_loop.exit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let Some(native_handles) = window_native_handles(&window) else {
|
||||||
|
self.error = Some("winit window does not expose native handles".to_string());
|
||||||
|
event_loop.exit();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let size = window.inner_size();
|
||||||
|
let renderer = match VulkanSmokeRenderer::new(&VulkanSmokeRendererCreateInfo {
|
||||||
|
application_name: "fparkan-game".to_string(),
|
||||||
|
native_handles,
|
||||||
|
drawable_extent: (size.width.max(1), size.height.max(1)),
|
||||||
|
render_request: WinitWindow::default_render_request(),
|
||||||
|
enable_validation: true,
|
||||||
|
mesh: self.mesh.clone(),
|
||||||
|
materials: Vec::new(),
|
||||||
|
bootstrap_progress: None,
|
||||||
|
}) {
|
||||||
|
Ok(renderer) => renderer,
|
||||||
|
Err(err) => {
|
||||||
|
self.error = Some(err.to_string());
|
||||||
|
event_loop.exit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.window_id = Some(window.id());
|
||||||
|
self.renderer = Some(renderer);
|
||||||
|
self.window = Some(window);
|
||||||
|
self.schedule_next_redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_event(
|
||||||
|
&mut self,
|
||||||
|
event_loop: &ActiveEventLoop,
|
||||||
|
window_id: WindowId,
|
||||||
|
event: WindowEvent,
|
||||||
|
) {
|
||||||
|
if Some(window_id) != self.window_id {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match event {
|
||||||
|
WindowEvent::CloseRequested => {
|
||||||
|
self.error = Some("native Vulkan window closed before completion".to_string());
|
||||||
|
event_loop.exit();
|
||||||
|
}
|
||||||
|
WindowEvent::Resized(size) => {
|
||||||
|
if let Some(renderer) = self.renderer.as_mut() {
|
||||||
|
renderer.request_resize((size.width, size.height));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WindowEvent::RedrawRequested => {
|
||||||
|
let Some(renderer) = self.renderer.as_mut() else {
|
||||||
|
self.error = Some("native Vulkan renderer was not initialized".to_string());
|
||||||
|
event_loop.exit();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match renderer.draw_frame() {
|
||||||
|
Ok(VulkanSmokeFrameOutcome::Presented) => {
|
||||||
|
self.frames_presented = self.frames_presented.saturating_add(1);
|
||||||
|
}
|
||||||
|
Ok(
|
||||||
|
VulkanSmokeFrameOutcome::Recreated | VulkanSmokeFrameOutcome::ZeroExtent,
|
||||||
|
) => {}
|
||||||
|
Err(err) => {
|
||||||
|
self.error = Some(err.to_string());
|
||||||
|
event_loop.exit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.frames_presented >= self.target_frames {
|
||||||
|
self.complete(event_loop);
|
||||||
|
} else {
|
||||||
|
self.schedule_next_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
|
||||||
|
if self.output.is_none() && self.error.is_none() {
|
||||||
|
self.schedule_next_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn render_snapshot_commands(snapshot: &WorldSnapshot) -> Result<RenderCommandList, String> {
|
fn render_snapshot_commands(snapshot: &WorldSnapshot) -> Result<RenderCommandList, String> {
|
||||||
render_snapshot_commands_with_assets(snapshot, None, None)
|
render_snapshot_commands_with_assets(snapshot, None, None)
|
||||||
@@ -229,6 +454,13 @@ struct Args {
|
|||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
mission: String,
|
mission: String,
|
||||||
frames: u64,
|
frames: u64,
|
||||||
|
backend: RenderBackendMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
enum RenderBackendMode {
|
||||||
|
Planning,
|
||||||
|
StaticVulkan,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Args {
|
impl Args {
|
||||||
@@ -236,6 +468,7 @@ impl Args {
|
|||||||
let mut root = None;
|
let mut root = None;
|
||||||
let mut mission = None;
|
let mut mission = None;
|
||||||
let mut frames = 1;
|
let mut frames = 1;
|
||||||
|
let mut backend = RenderBackendMode::Planning;
|
||||||
let mut iter = args.iter();
|
let mut iter = args.iter();
|
||||||
while let Some(arg) = iter.next() {
|
while let Some(arg) = iter.next() {
|
||||||
match arg.as_str() {
|
match arg.as_str() {
|
||||||
@@ -260,6 +493,17 @@ impl Args {
|
|||||||
.parse()
|
.parse()
|
||||||
.map_err(|_| "--frames must be an integer".to_string())?;
|
.map_err(|_| "--frames must be an integer".to_string())?;
|
||||||
}
|
}
|
||||||
|
"--backend" => {
|
||||||
|
backend = match iter
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| "--backend requires a value".to_string())?
|
||||||
|
.as_str()
|
||||||
|
{
|
||||||
|
"planning" => RenderBackendMode::Planning,
|
||||||
|
"static-vulkan" => RenderBackendMode::StaticVulkan,
|
||||||
|
_ => return Err("--backend must be planning or static-vulkan".to_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
_ => return Err(usage()),
|
_ => return Err(usage()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,6 +516,7 @@ impl Args {
|
|||||||
root,
|
root,
|
||||||
mission,
|
mission,
|
||||||
frames,
|
frames,
|
||||||
|
backend,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -308,7 +553,7 @@ fn json_hash(hash: &[u8; 32]) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn usage() -> String {
|
fn usage() -> String {
|
||||||
"usage: fparkan-game --root <path> --mission <path> [--frames <n>]".to_string()
|
"usage: fparkan-game --root <path> --mission <path> [--frames <n>] [--backend <planning|static-vulkan>]".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -337,6 +582,27 @@ mod tests {
|
|||||||
root: PathBuf::from("testdata/IS"),
|
root: PathBuf::from("testdata/IS"),
|
||||||
mission: "MISSIONS/Autodemo.00/data.tma".to_string(),
|
mission: "MISSIONS/Autodemo.00/data.tma".to_string(),
|
||||||
frames: 3,
|
frames: 3,
|
||||||
|
backend: RenderBackendMode::Planning,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_static_vulkan_backend() {
|
||||||
|
assert_eq!(
|
||||||
|
Args::parse(&strings(&[
|
||||||
|
"--root",
|
||||||
|
"testdata/IS",
|
||||||
|
"--mission",
|
||||||
|
"MISSIONS/Autodemo.00/data.tma",
|
||||||
|
"--backend",
|
||||||
|
"static-vulkan",
|
||||||
|
])),
|
||||||
|
Ok(Args {
|
||||||
|
root: PathBuf::from("testdata/IS"),
|
||||||
|
mission: "MISSIONS/Autodemo.00/data.tma".to_string(),
|
||||||
|
frames: 1,
|
||||||
|
backend: RenderBackendMode::StaticVulkan,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +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` | Implemented; native run not yet accepted on a full corpus mission | Implemented static MSH draw | First prepared MSH from loaded mission assets | Not yet `covered-gpu` | Opt-in mission-to-native-window bootstrap, static MSH projection and synchronized teardown telemetry | Full mission scene, texture/material binding, 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
|
||||||
@@ -31,8 +32,13 @@
|
|||||||
|
|
||||||
- Реальный Vulkan в репозитории имеет smoke triangle path и узкий static asset bridge. `VulkanStaticDrawRange` сохраняет исходный `Batch20.material_index`; когда smoke запускается с `--wear-root`, `--wear-archive`, `--wear-name` без override, он дедуплицирует selectors, проходит каждый через `WEAR → MAT0 → Textures.lib`, создаёт по одному image/descriptor set и бинит set непосредственно перед соответствующим indexed draw. Direct TEXM и `--material-index` — намеренно однотекстурные compatibility modes. Fresh GOG `fortif.rlb::FR_L_MTP.msh` подтверждает 237 batch draws, но его selectors все `0`, поэтому live report содержит один binding `MTP_01.0`; unit contract подтверждает точное сопоставление двух разных selectors с разными descriptor sets. Это не доказывает material phase animation, lightmaps, alpha/depth/cull state, terrain, camera/node transforms или pixel approval.
|
- Реальный Vulkan в репозитории имеет smoke triangle path и узкий static asset bridge. `VulkanStaticDrawRange` сохраняет исходный `Batch20.material_index`; когда smoke запускается с `--wear-root`, `--wear-archive`, `--wear-name` без override, он дедуплицирует selectors, проходит каждый через `WEAR → MAT0 → Textures.lib`, создаёт по одному image/descriptor set и бинит set непосредственно перед соответствующим indexed draw. Direct TEXM и `--material-index` — намеренно однотекстурные compatibility modes. Fresh GOG `fortif.rlb::FR_L_MTP.msh` подтверждает 237 batch draws, но его selectors все `0`, поэтому live report содержит один binding `MTP_01.0`; unit contract подтверждает точное сопоставление двух разных selectors с разными descriptor sets. Это не доказывает material phase animation, lightmaps, alpha/depth/cull state, terrain, camera/node transforms или pixel approval.
|
||||||
- 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`.
|
synthetic window descriptor и `VulkanPlanningBackend`. Opt-in `--backend static-vulkan`
|
||||||
|
уже создаёт native `winit` window и передаёт первую подготовленную MSH модели миссии в
|
||||||
|
`VulkanSmokeRenderer` с пустым списком materials (явный white-fallback). Это не является
|
||||||
|
rendered acceptance: fresh GOG `MISSIONS/Autodemo.00/data.tma` test не дошёл до окна за
|
||||||
|
локальный 120-second runner limit, поэтому нет live report, pixel artifact или validation
|
||||||
|
evidence именно для game path.
|
||||||
- `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`
|
||||||
|
|||||||
@@ -934,6 +934,23 @@ visual component emitted for that object. The bridge deliberately leaves the
|
|||||||
value absent when no mission draft exists; it does not infer an original ID
|
value absent when no mission draft exists; it does not infer an original ID
|
||||||
from a runtime slot or draw order.
|
from a runtime slot or draw order.
|
||||||
|
|
||||||
|
### Opt-in mission static-Vulkan bridge
|
||||||
|
|
||||||
|
`fparkan-game --backend static-vulkan` is an explicit native-window experiment,
|
||||||
|
not the default planning path. After mission loading it selects the first
|
||||||
|
prepared MSH model, sends it through the existing static XZ clip-space
|
||||||
|
projection and renders a requested number of frames through
|
||||||
|
`VulkanSmokeRenderer`; teardown rejects validation warnings/errors and reports
|
||||||
|
swapchain/readback telemetry. It deliberately supplies no material textures,
|
||||||
|
so the renderer's documented white fallback is used.
|
||||||
|
|
||||||
|
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, bind
|
||||||
|
WEAR/MAT0/TEXM material data, or establish a game camera. A GOG
|
||||||
|
`MISSIONS/Autodemo.00/data.tma` attempt exceeded the local 120-second runner
|
||||||
|
limit before the window/report, so this mode has implementation and unit/lint
|
||||||
|
evidence only, not a corpus GPU acceptance claim.
|
||||||
|
|
||||||
`Land.msh` использует отдельный geometry-only bridge: validated `TerrainFace28`
|
`Land.msh` использует отдельный geometry-only bridge: validated `TerrainFace28`
|
||||||
сохраняет source triangle order, а его positions и packed UV0 попадают в тот же
|
сохраняет source triangle order, а его positions и packed UV0 попадают в тот же
|
||||||
static vertex/index upload path. Для текущего диагностического viewer XZ bounds
|
static vertex/index upload path. Для текущего диагностического viewer XZ bounds
|
||||||
|
|||||||
Reference in New Issue
Block a user