diff --git a/crates/fparkan-assets/src/lib.rs b/crates/fparkan-assets/src/lib.rs index 0bafcfa..423e6ec 100644 --- a/crates/fparkan-assets/src/lib.rs +++ b/crates/fparkan-assets/src/lib.rs @@ -455,6 +455,17 @@ pub struct AssetPreparationReport { pub texture_pixels: u64, } +/// Asset preparation checkpoint emitted while resolving visual dependencies. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum AssetPreparationPhase { + /// Decode a model and its corresponding WEAR table. + ModelAndWear, + /// Resolve MAT0 material documents referenced by a WEAR table. + Materials, + /// Decode diffuse textures and baked lightmaps. + Textures, +} + /// Errors raised while preparing CPU-side assets. #[derive(Debug)] pub enum AssetError { @@ -571,6 +582,25 @@ impl AssetManager { prepare_mission_assets_with_repository(&self.repository, root_prototype_spans, prototypes) } + /// Builds mission assets and reports the active dependency class. + /// + /// # Errors + /// + /// Returns [`AssetError`] if any visual dependency is missing or malformed. + pub fn prepare_mission_assets_with_progress( + &self, + root_prototype_spans: &[std::ops::Range], + prototypes: &[EffectivePrototype], + on_phase: impl FnMut(AssetPreparationPhase), + ) -> Result { + prepare_mission_assets_with_repository_with_progress( + &self.repository, + root_prototype_spans, + prototypes, + on_phase, + ) + } + /// Builds mission assets together with a bounded preparation report. /// /// # Errors @@ -656,6 +686,28 @@ pub fn prepare_mission_assets_with_repository( .0) } +/// Builds mission assets and reports the active dependency class. +/// +/// # Errors +/// +/// Returns [`AssetError`] if any visual dependency is missing or malformed. +pub fn prepare_mission_assets_with_repository_with_progress( + repository: &R, + root_prototype_spans: &[std::ops::Range], + prototypes: &[EffectivePrototype], + mut on_phase: impl FnMut(AssetPreparationPhase), +) -> Result { + Ok(prepare_mission_assets_with_repository_internal( + repository, + root_prototype_spans, + prototypes, + AssetIdentityPolicy::default(), + AssetPreparationLimits::default(), + &mut on_phase, + )? + .0) +} + /// Builds mission assets while enforcing explicit preparation limits. /// /// # Errors @@ -668,12 +720,14 @@ pub fn prepare_mission_assets_profiled_with_repository( prototypes: &[EffectivePrototype], limits: AssetPreparationLimits, ) -> Result<(MissionAssets, AssetPreparationReport), AssetError> { + let mut ignore_phase = |_| {}; prepare_mission_assets_with_repository_internal( repository, root_prototype_spans, prototypes, AssetIdentityPolicy::default(), limits, + &mut ignore_phase, ) } @@ -686,6 +740,7 @@ fn prepare_mission_assets_with_repository_internal( prototypes: &[EffectivePrototype], identity_policy: AssetIdentityPolicy, limits: AssetPreparationLimits, + on_phase: &mut dyn FnMut(AssetPreparationPhase), ) -> Result<(MissionAssets, AssetPreparationReport), AssetError> { if prototypes.is_empty() { return Ok((MissionAssets::default(), AssetPreparationReport::default())); @@ -725,6 +780,7 @@ fn prepare_mission_assets_with_repository_internal( proto, identity_policy, &mut preparation_cache, + on_phase, )?; if bundle.visual.id != visual_id { // Defensive check. stable IDs are deterministic for the same inputs. @@ -1416,6 +1472,7 @@ pub fn prepare_visual_with_repository( proto, AssetIdentityPolicy::default(), &mut preparation_cache, + &mut |_| {}, )? .visual) } @@ -1444,6 +1501,7 @@ fn prepare_visual_with_repository_internal( proto: &EffectivePrototype, identity_policy: AssetIdentityPolicy, preparation_cache: &mut PreparationCache, + on_phase: &mut dyn FnMut(AssetPreparationPhase), ) -> Result { let PrototypeGeometry::Mesh(mesh_key) = &proto.geometry else { return Ok(PreparedVisualBundle { @@ -1455,6 +1513,7 @@ fn prepare_visual_with_repository_internal( }); }; + on_phase(AssetPreparationPhase::ModelAndWear); let model = prepare_model_cached(repository, mesh_key, preparation_cache)?; let model_id = AssetId::new((identity_policy.model)(proto)); let prepared_model = PreparedModel { @@ -1491,6 +1550,7 @@ fn prepare_visual_with_repository_internal( .iter() .map(|lightmap| lightmap.lightmap.clone()) .collect(); + on_phase(AssetPreparationPhase::Materials); for material_index in 0..wear.entries.len() { let material_index = u16::try_from(material_index).map_err(|_| { AssetError::InvalidPrototype("material index does not fit archive format".to_string()) @@ -1520,6 +1580,7 @@ fn prepare_visual_with_repository_internal( }); for texture in texture_requests { + on_phase(AssetPreparationPhase::Textures); let prepared_texture = prepare_texture_cached( repository, &texture, @@ -1534,6 +1595,7 @@ fn prepare_visual_with_repository_internal( } for lightmap in &wear.lightmaps { + on_phase(AssetPreparationPhase::Textures); let prepared_lightmap = prepare_texture_cached( repository, &lightmap.lightmap, @@ -2830,6 +2892,7 @@ mod tests { &prototypes, policy, AssetPreparationLimits::default(), + &mut |_| {}, ) .expect_err("collision should fail"); diff --git a/crates/fparkan-runtime/src/lib.rs b/crates/fparkan-runtime/src/lib.rs index 76c681e..88b4bd1 100644 --- a/crates/fparkan-runtime/src/lib.rs +++ b/crates/fparkan-runtime/src/lib.rs @@ -23,9 +23,9 @@ use fparkan_assets::{ decode_mission_land_path, decode_mission_payload, decode_nres_payload, derive_mission_land_paths, extend_graph_report_with_visual_dependencies, prepare_terrain_world, - AssetError as AssetPreparationError, AssetId, AssetManager, BuildCategory, MissionAssetPlan, - MissionDocument, MissionError, MissionTerrainPaths, NresError, PreparedVisual, - TerrainFormatError, TerrainPreparationError, TerrainWorld, TmaProfile, + AssetError as AssetPreparationError, AssetId, AssetManager, AssetPreparationPhase, + BuildCategory, MissionAssetPlan, MissionDocument, MissionError, MissionTerrainPaths, NresError, + PreparedVisual, TerrainFormatError, TerrainPreparationError, TerrainWorld, TmaProfile, }; use fparkan_path::{normalize_relative, NormalizedPath, PathError, PathPolicy}; use fparkan_prototype::{ @@ -120,6 +120,12 @@ pub enum MissionLoadPhase { GraphVisuals, /// Prepare all reachable visual/resource dependencies. Assets, + /// Decode model meshes and their WEAR tables. + AssetModels, + /// Resolve MAT0 material documents. + AssetMaterials, + /// Decode diffuse textures and baked lightmaps. + AssetTextures, /// Construct all object drafts before registration. Construct, /// Register constructed objects. @@ -698,22 +704,38 @@ fn load_mission_with_options_and_progress( } record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Assets); let asset_manager = AssetManager::new(repository); + let mut last_asset_phase = None; + let mut observe_asset_phase = |phase| { + let runtime_phase = match phase { + AssetPreparationPhase::ModelAndWear => MissionLoadPhase::AssetModels, + AssetPreparationPhase::Materials => MissionLoadPhase::AssetMaterials, + AssetPreparationPhase::Textures => MissionLoadPhase::AssetTextures, + }; + if last_asset_phase != Some(runtime_phase) { + record_load_phase(&mut trace, &mut on_phase, runtime_phase); + last_asset_phase = Some(runtime_phase); + } + }; let mission_assets = match options.asset_scope { - MissionAssetScope::Full => asset_manager.prepare_mission_assets( + MissionAssetScope::Full => asset_manager.prepare_mission_assets_with_progress( &prototype_graph.root_prototype_request_spans, &resolved_prototypes, + &mut observe_asset_phase, ), MissionAssetScope::FirstMeshPreview => prepare_first_preview_assets( &asset_manager, &prototype_graph.root_prototype_request_spans, &resolved_prototypes, + &mut observe_asset_phase, ), - MissionAssetScope::PreviewRoots(root_count) => asset_manager.prepare_mission_assets( - &prototype_graph.root_prototype_request_spans[..root_count - .get() - .min(prototype_graph.root_prototype_request_spans.len())], - &resolved_prototypes, - ), + MissionAssetScope::PreviewRoots(root_count) => asset_manager + .prepare_mission_assets_with_progress( + &prototype_graph.root_prototype_request_spans[..root_count + .get() + .min(prototype_graph.root_prototype_request_spans.len())], + &resolved_prototypes, + &mut observe_asset_phase, + ), } .map_err(|source| EngineError::AssetPreparation { mission: request.key.clone(), @@ -831,10 +853,14 @@ fn prepare_first_preview_assets( asset_manager: &AssetManager, root_spans: &[std::ops::Range], prototypes: &[fparkan_prototype::EffectivePrototype], + on_phase: &mut dyn FnMut(AssetPreparationPhase), ) -> Result { for span in root_spans { - let assets = - asset_manager.prepare_mission_assets(std::slice::from_ref(span), prototypes)?; + let assets = asset_manager.prepare_mission_assets_with_progress( + std::slice::from_ref(span), + prototypes, + &mut *on_phase, + )?; if !assets.models.is_empty() { return Ok(assets); } diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md index aee1f29..e39f753 100644 --- a/docs/tomes/05-render.md +++ b/docs/tomes/05-render.md @@ -1063,6 +1063,15 @@ provenance while avoiding repeat format work for repeated components. The same demonstrate an additional checkpoint advance from this cache; it must not be reported as a measured startup improvement. +`Assets` now has three ordered diagnostic sub-checkpoints: `AssetModels` +(MSH and WEAR), `AssetMaterials` (MAT0) and `AssetTextures` (diffuse TEXM and +lightmaps). The callback is observational: it neither changes the preparation +order nor deduplicates requests. A fresh controlled 60-second canonical GOG +`Autodemo.00` first-root probe reached `AssetModels` and did not reach +`AssetMaterials`; its created process was terminated and no native frame was +reported. This narrows the next investigation to model/WEAR preparation, but +does not by itself attribute the delay to either decoder or I/O. + The same source-axis proof now applies to `TerrainWorld`: its `Land.msh` surface height query and `Land.map` areal/grid lookup use XY ground coordinates, return source Z height, and leave raycasts as full 3D intersections. The Part 1 and