aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md2
-rw-r--r--apps/fparkan-game/src/main.rs110
-rw-r--r--crates/fparkan-assets/src/lib.rs14
-rw-r--r--docs/rendering/renderer_truth_table.md12
-rw-r--r--docs/tomes/05-render.md14
5 files changed, 127 insertions, 25 deletions
diff --git a/README.md b/README.md
index 4455504..1fc5784 100644
--- a/README.md
+++ b/README.md
@@ -32,7 +32,7 @@ Open source проект с реализацией компонентов игр
## Текущий статус рендера
- `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 без подтверждённого полного corpus run, текстур, transforms или pixel parity.
+- `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 ещё не подтверждены.
- `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).
diff --git a/apps/fparkan-game/src/main.rs b/apps/fparkan-game/src/main.rs
index 90be507..2d4f4a0 100644
--- a/apps/fparkan-game/src/main.rs
+++ b/apps/fparkan-game/src/main.rs
@@ -21,7 +21,7 @@
#![allow(clippy::print_stderr, clippy::print_stdout)]
//! `FParkan` render-planning composition root.
-use fparkan_assets::PreparedVisual;
+use fparkan_assets::{PreparedTextureUsage, PreparedVisual};
use fparkan_platform::WindowPort;
use fparkan_platform_winit::{window_native_handles, WinitWindow, WinitWindowPlan};
use fparkan_render::{
@@ -31,7 +31,8 @@ use fparkan_render::{
};
use fparkan_render_vulkan::{
project_msh_to_static_mesh, VulkanPlanningBackend, VulkanSmokeFrameOutcome,
- VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanStaticMesh,
+ VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanStaticMaterial, VulkanStaticMesh,
+ VulkanStaticTexture,
};
use fparkan_runtime::{
create, frame, load_mission, load_mission_static_preview,
@@ -81,12 +82,14 @@ fn run(args: &[String]) -> Result<String, 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 (mesh, materials) = static_preview_mesh_and_materials(mission_assets)?;
+ return run_static_vulkan_mode(
+ mesh,
+ materials,
+ args.frames,
+ &args.mission,
+ loaded.object_count,
+ );
}
let mut backend = VulkanPlanningBackend::new();
@@ -174,6 +177,79 @@ fn load_requested_mission(
.map_err(|err| err.to_string())
}
+/// Projects the explicitly bounded static-preview root into geometry and its
+/// selector-keyed diffuse material descriptors.
+///
+/// This intentionally takes only the first MAT0 texture request for each MSH
+/// batch selector. Later material phases, animation, lightmaps, transforms,
+/// and gameplay visibility remain outside this preview bridge.
+fn static_preview_mesh_and_materials(
+ assets: &MissionAssets,
+) -> Result<(VulkanStaticMesh, Vec<VulkanStaticMaterial>), String> {
+ let model = 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}"))?;
+ 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
+ .iter()
+ .map(|range| range.material_index)
+ .collect::<Vec<_>>();
+ selectors.sort_unstable();
+ selectors.dedup();
+
+ let materials = selectors
+ .into_iter()
+ .map(|material_index| {
+ let material_id = visual.material_ids.get(usize::from(material_index)).ok_or_else(|| {
+ format!(
+ "static preview MSH batch selector {material_index} has no prepared WEAR material"
+ )
+ })?;
+ let material = assets.material_by_id(*material_id).ok_or_else(|| {
+ format!(
+ "static preview prepared material {material_index} is unavailable"
+ )
+ })?;
+ let texture_name = material.texture_requests.first().ok_or_else(|| {
+ format!("static preview material {material_index} has no MAT0 diffuse texture")
+ })?;
+ let texture = assets
+ .textures
+ .iter()
+ .find(|texture| {
+ texture.usage == PreparedTextureUsage::Diffuse
+ && texture.source.name == *texture_name
+ })
+ .ok_or_else(|| {
+ format!(
+ "static preview diffuse texture {texture_name:?} for material {material_index} is unavailable"
+ )
+ })?;
+ let image = texture.decode_mip_rgba8(0).map_err(|err| {
+ format!(
+ "decode static preview diffuse texture {texture_name:?} for material {material_index}: {err}"
+ )
+ })?;
+ Ok(VulkanStaticMaterial {
+ material_index,
+ texture: VulkanStaticTexture {
+ width: image.width,
+ height: image.height,
+ rgba8: image.rgba8,
+ },
+ })
+ })
+ .collect::<Result<Vec<_>, String>>()?;
+ Ok((mesh, materials))
+}
+
fn prepare_load_progress_path(path: &std::path::Path) -> Result<(), String> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|err| format!("{}: {err}", parent.display()))?;
@@ -187,13 +263,14 @@ fn write_load_progress(path: &std::path::Path, phase: MissionLoadPhase) -> Resul
fn run_static_vulkan_mode(
mesh: VulkanStaticMesh,
+ materials: Vec<VulkanStaticMaterial>,
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);
+ let mut app = StaticVulkanApp::new(mesh, materials, target_frames, mission, object_count);
if let Err(err) = event_loop.run_app(&mut app) {
app.error = Some(format!("winit event loop: {err}"));
}
@@ -202,6 +279,7 @@ fn run_static_vulkan_mode(
struct StaticVulkanApp {
mesh: VulkanStaticMesh,
+ materials: Vec<VulkanStaticMaterial>,
target_frames: u64,
mission: String,
object_count: usize,
@@ -214,9 +292,16 @@ struct StaticVulkanApp {
}
impl StaticVulkanApp {
- fn new(mesh: VulkanStaticMesh, target_frames: u64, mission: &str, object_count: usize) -> Self {
+ fn new(
+ mesh: VulkanStaticMesh,
+ materials: Vec<VulkanStaticMaterial>,
+ target_frames: u64,
+ mission: &str,
+ object_count: usize,
+ ) -> Self {
Self {
mesh,
+ materials,
target_frames,
mission: mission.to_string(),
object_count,
@@ -259,10 +344,11 @@ impl StaticVulkanApp {
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\":{}}}",
+ "{{\"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\":{}}}",
json_string(&self.mission),
self.object_count,
self.frames_presented,
+ self.materials.len(),
report.renderer_report.swapchain_extent.0,
report.renderer_report.swapchain_extent.1,
report.renderer_report.swapchain_image_count,
@@ -320,7 +406,7 @@ impl ApplicationHandler for StaticVulkanApp {
render_request: WinitWindow::default_render_request(),
enable_validation: true,
mesh: self.mesh.clone(),
- materials: Vec::new(),
+ materials: self.materials.clone(),
bootstrap_progress: None,
}) {
Ok(renderer) => renderer,
diff --git a/crates/fparkan-assets/src/lib.rs b/crates/fparkan-assets/src/lib.rs
index f2b820c..e4a9b40 100644
--- a/crates/fparkan-assets/src/lib.rs
+++ b/crates/fparkan-assets/src/lib.rs
@@ -39,7 +39,7 @@ use fparkan_resource::{ResourceError, ResourceKey, ResourceRepository};
pub use fparkan_terrain::{TerrainError, TerrainWorld};
use fparkan_terrain_format::{decode_build_dat, decode_land_map, decode_land_msh};
pub use fparkan_terrain_format::{BuildCategory, TerrainFormatError};
-use fparkan_texm::{decode_texm, TexmDocument, TexmError};
+use fparkan_texm::{decode_mip_rgba8, decode_texm, RgbaImage, TexmDocument, TexmError};
use std::collections::{hash_map::Entry, HashMap, HashSet};
use std::fmt;
use std::hash::{Hash, Hasher};
@@ -307,6 +307,18 @@ pub struct PreparedTexture {
pub usage: PreparedTextureUsage,
}
+impl PreparedTexture {
+ /// Decodes one prepared texture mip into the renderer upload representation.
+ ///
+ /// # Errors
+ ///
+ /// Returns the original TEXM decode error when the requested mip is invalid
+ /// or malformed.
+ pub fn decode_mip_rgba8(&self, level: u32) -> Result<RgbaImage, TexmError> {
+ decode_mip_rgba8(&self.texm, level)
+ }
+}
+
impl PreparedVisual {
/// Returns the primary material id, if any.
#[must_use]
diff --git a/docs/rendering/renderer_truth_table.md b/docs/rendering/renderer_truth_table.md
index 00f756d..0700dbb 100644
--- a/docs/rendering/renderer_truth_table.md
+++ b/docs/rendering/renderer_truth_table.md
@@ -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 |
| `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 |
-| `fparkan-game --backend static-vulkan` | Yes, GOG `Autodemo.00` | Yes, static MSH draw | First MSH from first mission root | `covered-gpu` only for the narrow static-preview bridge | 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 |
+| `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 |
| 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
@@ -35,14 +35,16 @@
- `apps/fparkan-game` по умолчанию выдает `render-planning` JSON report поверх
synthetic window descriptor и `VulkanPlanningBackend`. Opt-in `--backend static-vulkan`
уже создаёт native `winit` window и передаёт первую подготовленную MSH модели миссии в
- `VulkanSmokeRenderer` с пустым списком materials (явный white-fallback). Режим использует
+ `VulkanSmokeRenderer` вместе с первым diffuse TEXM из MAT0 для каждого реально используемого
+ `Batch20.material_index`. Режим использует
отдельный first-root preview loader: normal `load_mission` по-прежнему готовит все reachable
assets и весь graph, тогда как preview строит graph и готовит assets только для первого
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
- one presented frame, native 1280×720 swapchain (2 images), 7,372,800-byte readback hash
- `10681850560830502773`, and validation warnings/errors `0/0`. This is `covered-gpu` evidence
- for that narrow static-preview bridge only, not full-scene or original-renderer parity.
+ one presented frame, native 1280×720 swapchain (2 images), one original diffuse material
+ descriptor, 7,372,800-byte readback hash `12332214764918703197`, and validation
+ warnings/errors `0/0`. 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
asset viewer.
- Следующий реальный milestone для rendered acceptance: `VulkanAssetRenderer`
diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md
index b689c08..cc65092 100644
--- a/docs/tomes/05-render.md
+++ b/docs/tomes/05-render.md
@@ -941,16 +941,18 @@ 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
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.
+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
+MAT0 diffuse texture request as a selector-keyed descriptor.
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. Fresh GOG
+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
`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
-swapchain, 7,372,800-byte synchronized readback (FNV-1a
-`10681850560830502773`) and validation warnings/errors `0/0`. This is not a
+swapchain, one selector-keyed original diffuse descriptor, 7,372,800-byte
+synchronized readback (FNV-1a `12332214764918703197`) and validation
+warnings/errors `0/0`. This is not a
full-scene or original-renderer pixel-parity claim.
The preview now asks `load_mission_static_preview` for both graph and assets.