feat(runtime): trace asset preparation phases
This commit is contained in:
@@ -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<R: ResourceRepository> AssetManager<R> {
|
||||
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<usize>],
|
||||
prototypes: &[EffectivePrototype],
|
||||
on_phase: impl FnMut(AssetPreparationPhase),
|
||||
) -> Result<MissionAssets, AssetError> {
|
||||
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<R: ResourceRepository>(
|
||||
.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<R: ResourceRepository>(
|
||||
repository: &R,
|
||||
root_prototype_spans: &[std::ops::Range<usize>],
|
||||
prototypes: &[EffectivePrototype],
|
||||
mut on_phase: impl FnMut(AssetPreparationPhase),
|
||||
) -> Result<MissionAssets, AssetError> {
|
||||
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<R: ResourceRepository>(
|
||||
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<R: ResourceRepository>(
|
||||
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<R: ResourceRepository>(
|
||||
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<R: ResourceRepository>(
|
||||
proto,
|
||||
AssetIdentityPolicy::default(),
|
||||
&mut preparation_cache,
|
||||
&mut |_| {},
|
||||
)?
|
||||
.visual)
|
||||
}
|
||||
@@ -1444,6 +1501,7 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
|
||||
proto: &EffectivePrototype,
|
||||
identity_policy: AssetIdentityPolicy,
|
||||
preparation_cache: &mut PreparationCache,
|
||||
on_phase: &mut dyn FnMut(AssetPreparationPhase),
|
||||
) -> Result<PreparedVisualBundle, AssetError> {
|
||||
let PrototypeGeometry::Mesh(mesh_key) = &proto.geometry else {
|
||||
return Ok(PreparedVisualBundle {
|
||||
@@ -1455,6 +1513,7 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
|
||||
});
|
||||
};
|
||||
|
||||
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<R: ResourceRepository>(
|
||||
.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<R: ResourceRepository>(
|
||||
});
|
||||
|
||||
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<R: ResourceRepository>(
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
|
||||
@@ -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<R: ResourceRepository>(
|
||||
asset_manager: &AssetManager<R>,
|
||||
root_spans: &[std::ops::Range<usize>],
|
||||
prototypes: &[fparkan_prototype::EffectivePrototype],
|
||||
on_phase: &mut dyn FnMut(AssetPreparationPhase),
|
||||
) -> Result<MissionAssets, AssetPreparationError> {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user