From f8e447ffee746cfe6580cc0e78a8a225aa39b546 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Tue, 23 Jun 2026 22:05:16 +0400 Subject: feat: close stage 0-2 audit groundwork Remove legacy SDL/OpenGL adapters from the workspace and introduce winit/Vulkan adapter boundaries for the rendered composition root. Add reproducible toolchain and xtask CI coverage for formatting, tests, clippy, docs, policy, deny, acceptance auditing, and hosted OS matrix evidence. Strengthen Stage 1 data contracts with byte-first paths, VFS hardening, structured diagnostics, RsLi writer/edit scaffolding, corpus reporting, and resource error classification. Advance Stage 2 asset preparation by moving mission loading through assets/runtime boundaries, materializing prototype graph data, preserving provenance, and adding inspection/viewer integration. Record the Stage 0-2 audit input, acceptance roadmap, coverage updates, and documentation notes for follow-up evidence. --- crates/fparkan-runtime/Cargo.toml | 5 +- crates/fparkan-runtime/src/lib.rs | 160 +++++++++++++++++++------------------- 2 files changed, 82 insertions(+), 83 deletions(-) (limited to 'crates/fparkan-runtime') diff --git a/crates/fparkan-runtime/Cargo.toml b/crates/fparkan-runtime/Cargo.toml index 17d95c1..347c713 100644 --- a/crates/fparkan-runtime/Cargo.toml +++ b/crates/fparkan-runtime/Cargo.toml @@ -6,15 +6,12 @@ license.workspace = true repository.workspace = true [dependencies] -fparkan-mission-format = { path = "../fparkan-mission-format" } -fparkan-nres = { path = "../fparkan-nres" } +fparkan-assets = { path = "../fparkan-assets" } fparkan-path = { path = "../fparkan-path" } fparkan-platform = { path = "../fparkan-platform" } fparkan-prototype = { path = "../fparkan-prototype" } fparkan-render = { path = "../fparkan-render" } fparkan-resource = { path = "../fparkan-resource" } -fparkan-terrain = { path = "../fparkan-terrain" } -fparkan-terrain-format = { path = "../fparkan-terrain-format" } fparkan-vfs = { path = "../fparkan-vfs" } fparkan-world = { path = "../fparkan-world" } diff --git a/crates/fparkan-runtime/src/lib.rs b/crates/fparkan-runtime/src/lib.rs index 1fc0137..053d7bd 100644 --- a/crates/fparkan-runtime/src/lib.rs +++ b/crates/fparkan-runtime/src/lib.rs @@ -1,19 +1,20 @@ #![forbid(unsafe_code)] //! Runtime orchestration for headless and rendered modes. -use fparkan_mission_format::{ - decode_tma, decode_tma_land_path, LpString, MissionDocument, MissionError, TmaProfile, +use fparkan_assets::{ + AssetError as AssetPreparationError, AssetManager, MissionAssetPlan, + decode_mission_land_path, decode_nres_payload, decode_mission_payload, prepare_terrain_world, + derive_mission_land_paths, BuildCategory, MissionDocument, MissionError, MissionTerrainPaths, + TerrainFormatError, TerrainPreparationError, TmaProfile, TerrainWorld, + NresError, + extend_graph_report_with_visual_dependencies, }; use fparkan_path::{normalize_relative, NormalizedPath, PathError, PathPolicy}; use fparkan_prototype::{ - build_prototype_graph_report, extend_graph_report_with_visual_dependencies, EffectivePrototype, + build_prototype_graph_report, PrototypeGraph, PrototypeGraphFailure, PrototypeGraphReport, }; use fparkan_resource::{resource_name, CachedResourceRepository}; -use fparkan_terrain::TerrainWorld; -use fparkan_terrain_format::{ - decode_build_dat, decode_land_map, decode_land_msh, BuildCategory, TerrainFormatError, -}; use fparkan_vfs::{Vfs, VfsError}; use fparkan_world::{ construct_object, new as new_world, register_object, step, InputSnapshot, ObjectDraft, @@ -21,6 +22,8 @@ use fparkan_world::{ }; use std::sync::Arc; +pub use fparkan_assets::MissionAssets; + /// Engine mode. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum EngineMode { @@ -167,6 +170,8 @@ pub struct LoadedMission { pub graph_unit_component_count: usize, /// Mission prototype graph root count. pub graph_root_count: usize, + /// Mission asset plan visual count after dependency preparation. + pub asset_visual_count: usize, /// Expanded prototype requests resolved to effective prototypes. pub graph_resolved_count: usize, /// Reached mesh dependency count. @@ -189,6 +194,14 @@ pub struct LoadedMission { pub graph_lightmap_request_count: usize, /// Lightmap Texm entries decoded. pub graph_lightmap_resolved_count: usize, + /// Mission asset plan mesh-backed count after dependency preparation. + pub asset_model_count: usize, + /// Mission asset plan material count after dependency preparation. + pub asset_material_count: usize, + /// Mission asset plan texture count after dependency preparation. + pub asset_texture_count: usize, + /// Mission asset plan lightmap count after dependency preparation. + pub asset_lightmap_count: usize, } /// Frame result. @@ -222,7 +235,8 @@ struct LoadedMissionState { build_categories: Vec, prototype_graph: PrototypeGraph, prototype_report: PrototypeGraphReport, - resolved_prototypes: Vec, + mission_assets: MissionAssets, + asset_plan: MissionAssetPlan, } /// Engine error. @@ -251,7 +265,7 @@ pub enum EngineError { /// Resource path. path: String, /// Source error. - source: fparkan_nres::NresError, + source: NresError, }, /// Mission decode error. Mission { @@ -268,12 +282,19 @@ pub enum EngineError { source: TerrainFormatError, }, /// Terrain runtime build error. - Terrain(fparkan_terrain::TerrainError), + Terrain(fparkan_assets::TerrainError), /// Prototype graph errors. PrototypeGraph { /// Root failures. failures: Vec, }, + /// Asset preparation errors. + AssetPreparation { + /// Mission key. + mission: String, + /// Source error. + source: AssetPreparationError, + }, /// World error. World(fparkan_world::WorldError), /// Scheduler phase order was violated. @@ -319,6 +340,9 @@ impl std::fmt::Display for EngineError { Self::PrototypeGraph { failures } => { write!(f, "mission prototype graph has {} failures", failures.len()) } + Self::AssetPreparation { mission, source } => { + write!(f, "{mission}: asset preparation failed: {source}") + } Self::World(source) => write!(f, "{source}"), Self::SchedulerPhaseOrder { previous, current } => write!( f, @@ -346,6 +370,7 @@ impl std::error::Error for EngineError { Self::TerrainFormat { source, .. } => Some(source), Self::Terrain(source) => Some(source), Self::World(source) => Some(source), + Self::AssetPreparation { source, .. } => Some(source), Self::MissingVfs | Self::PrototypeGraph { .. } | Self::SchedulerPhaseOrder { .. } @@ -410,44 +435,44 @@ fn load_mission_with_options( let mission_bytes = read_vfs(&vfs, &mission_path)?; trace.phases.push(MissionLoadPhase::Map); - let land_path = decode_tma_land_path(&mission_bytes, TmaProfile::Strict).map_err(|source| { + let land_path = decode_mission_land_path(&mission_bytes, TmaProfile::Strict).map_err(|source| { EngineError::Mission { path: mission_path.as_str().to_string(), source, } })?; - let (land_msh_path, land_map_path) = terrain_paths_from_land_path(&land_path)?; - let land_msh_nres = decode_nres(&vfs, &land_msh_path)?; - let land_map_nres = decode_nres(&vfs, &land_map_path)?; - let land_msh = - decode_land_msh(&land_msh_nres).map_err(|source| EngineError::TerrainFormat { + let MissionTerrainPaths { land_msh: land_msh_path, land_map: land_map_path } = + derive_mission_land_paths(&land_path).map_err(|source| EngineError::Path { + role: "mission land", + value: mission_path.as_str().to_string(), + source, + })?; + let land_msh_nres = decode_nres_payload(read_vfs(&vfs, &land_msh_path)?) + .map_err(|source| EngineError::Nres { path: land_msh_path.as_str().to_string(), source, })?; - let land_map = - decode_land_map(&land_map_nres).map_err(|source| EngineError::TerrainFormat { + let land_map_nres = decode_nres_payload(read_vfs(&vfs, &land_map_path)?) + .map_err(|source| EngineError::Nres { path: land_map_path.as_str().to_string(), source, })?; - let terrain = - TerrainWorld::from_land_assets(&land_msh, &land_map).map_err(EngineError::Terrain)?; - let build_dat_path = normalize_engine_path("BuildDat", "BuildDat.lst")?; let build_dat = read_vfs(&vfs, &build_dat_path)?; - let build_categories = - decode_build_dat(&build_dat).map_err(|source| EngineError::TerrainFormat { - path: build_dat_path.as_str().to_string(), - source, + let (terrain, build_categories) = prepare_terrain_world(&land_msh_nres, &land_map_nres, &build_dat) + .map_err(|source| match source { + TerrainPreparationError::Decode(source) => EngineError::TerrainFormat { + path: build_dat_path.as_str().to_string(), + source, + }, + TerrainPreparationError::Runtime(source) => EngineError::Terrain(source), })?; trace.phases.push(MissionLoadPhase::Tma); let mission = - decode_tma(mission_bytes, TmaProfile::Strict).map_err(|source| EngineError::Mission { + decode_mission_payload(mission_bytes, TmaProfile::Strict).map_err(|source| EngineError::Mission { path: mission_path.as_str().to_string(), source, })?; - let verified_terrain_paths = terrain_paths(&mission)?; - debug_assert_eq!(verified_terrain_paths.0.as_str(), land_msh_path.as_str()); - debug_assert_eq!(verified_terrain_paths.1.as_str(), land_map_path.as_str()); trace.transforms = mission .objects .iter() @@ -471,6 +496,7 @@ fn load_mission_with_options( extend_graph_report_with_visual_dependencies( &repository, &mut prototype_report, + &prototype_graph, &resolved_prototypes, ); if !prototype_report.is_success() { @@ -478,6 +504,16 @@ fn load_mission_with_options( failures: prototype_report.failures.clone(), }); } + let mission_assets = AssetManager::new(repository) + .prepare_mission_assets( + &prototype_graph.root_prototype_request_spans, + &resolved_prototypes, + ) + .map_err(|source| EngineError::AssetPreparation { + mission: request.key.clone(), + source, + })?; + let mission_asset_plan = mission_assets.to_plan(); trace.phases.push(MissionLoadPhase::Assets); let mut new_runtime_world = new_world(WorldConfig); @@ -519,6 +555,7 @@ fn load_mission_with_options( graph_direct_reference_count: prototype_report.direct_reference_count, graph_unit_component_count: prototype_report.unit_component_count, graph_root_count: prototype_report.root_count, + asset_visual_count: mission_asset_plan.visual_count, graph_resolved_count: prototype_report.resolved_count, graph_mesh_dependency_count: prototype_report.mesh_dependency_count, graph_failure_count: prototype_report.failures.len(), @@ -530,6 +567,10 @@ fn load_mission_with_options( graph_texture_resolved_count: prototype_report.texture_resolved_count, graph_lightmap_request_count: prototype_report.lightmap_request_count, graph_lightmap_resolved_count: prototype_report.lightmap_resolved_count, + asset_model_count: mission_asset_plan.model_count, + asset_material_count: mission_asset_plan.material_count, + asset_texture_count: mission_asset_plan.texture_count, + asset_lightmap_count: mission_asset_plan.lightmap_count, }; engine.world = new_runtime_world; @@ -540,7 +581,8 @@ fn load_mission_with_options( build_categories, prototype_graph, prototype_report, - resolved_prototypes, + mission_assets, + asset_plan: mission_asset_plan, }); Ok((summary, trace)) } @@ -618,13 +660,16 @@ pub fn loaded_prototype_graph_report(engine: &Engine) -> Option<&PrototypeGraphR engine.loaded.as_ref().map(|state| &state.prototype_report) } -/// Returns resolved effective prototypes for the loaded mission. +/// Returns the prepared mission asset plan for the loaded mission. #[must_use] -pub fn loaded_resolved_prototypes(engine: &Engine) -> Option<&[EffectivePrototype]> { - engine - .loaded - .as_ref() - .map(|state| state.resolved_prototypes.as_slice()) +pub fn loaded_mission_asset_plan(engine: &Engine) -> Option<&MissionAssetPlan> { + engine.loaded.as_ref().map(|state| &state.asset_plan) +} + +/// Returns prepared mission assets for the loaded mission. +#[must_use] +pub fn loaded_mission_assets(engine: &Engine) -> Option<&MissionAssets> { + engine.loaded.as_ref().map(|state| &state.mission_assets) } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -716,49 +761,6 @@ fn read_vfs(vfs: &Arc, path: &NormalizedPath) -> Result, Engi }) } -fn decode_nres( - vfs: &Arc, - path: &NormalizedPath, -) -> Result { - let bytes = read_vfs(vfs, path)?; - fparkan_nres::decode(bytes, fparkan_nres::ReadProfile::Compatible).map_err(|source| { - EngineError::Nres { - path: path.as_str().to_string(), - source, - } - }) -} - -fn terrain_paths( - mission: &MissionDocument, -) -> Result<(NormalizedPath, NormalizedPath), EngineError> { - terrain_paths_from_land_path(&mission.land_path) -} - -fn terrain_paths_from_land_path( - land_path: &LpString, -) -> Result<(NormalizedPath, NormalizedPath), EngineError> { - let land_path_raw = String::from_utf8_lossy(&land_path.raw).to_string(); - let normalized = - normalize_relative(&land_path.raw, PathPolicy::StrictLegacy).map_err(|source| { - EngineError::Path { - role: "mission land", - value: land_path_raw.clone(), - source, - } - })?; - let Some((parent, _stem)) = normalized.as_str().rsplit_once('/') else { - return Err(EngineError::Path { - role: "mission land", - value: normalized.as_str().to_string(), - source: PathError::Empty, - }); - }; - let mesh = normalize_engine_path("Land.msh", &format!("{parent}/Land.msh"))?; - let map = normalize_engine_path("Land.map", &format!("{parent}/Land.map"))?; - Ok((mesh, map)) -} - #[cfg(test)] mod tests { use super::*; -- cgit v1.2.3