feat(runtime): trace asset preparation phases
This commit is contained in:
@@ -455,6 +455,17 @@ pub struct AssetPreparationReport {
|
|||||||
pub texture_pixels: u64,
|
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.
|
/// Errors raised while preparing CPU-side assets.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum AssetError {
|
pub enum AssetError {
|
||||||
@@ -571,6 +582,25 @@ impl<R: ResourceRepository> AssetManager<R> {
|
|||||||
prepare_mission_assets_with_repository(&self.repository, root_prototype_spans, prototypes)
|
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.
|
/// Builds mission assets together with a bounded preparation report.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
@@ -656,6 +686,28 @@ pub fn prepare_mission_assets_with_repository<R: ResourceRepository>(
|
|||||||
.0)
|
.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.
|
/// Builds mission assets while enforcing explicit preparation limits.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
@@ -668,12 +720,14 @@ pub fn prepare_mission_assets_profiled_with_repository<R: ResourceRepository>(
|
|||||||
prototypes: &[EffectivePrototype],
|
prototypes: &[EffectivePrototype],
|
||||||
limits: AssetPreparationLimits,
|
limits: AssetPreparationLimits,
|
||||||
) -> Result<(MissionAssets, AssetPreparationReport), AssetError> {
|
) -> Result<(MissionAssets, AssetPreparationReport), AssetError> {
|
||||||
|
let mut ignore_phase = |_| {};
|
||||||
prepare_mission_assets_with_repository_internal(
|
prepare_mission_assets_with_repository_internal(
|
||||||
repository,
|
repository,
|
||||||
root_prototype_spans,
|
root_prototype_spans,
|
||||||
prototypes,
|
prototypes,
|
||||||
AssetIdentityPolicy::default(),
|
AssetIdentityPolicy::default(),
|
||||||
limits,
|
limits,
|
||||||
|
&mut ignore_phase,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -686,6 +740,7 @@ fn prepare_mission_assets_with_repository_internal<R: ResourceRepository>(
|
|||||||
prototypes: &[EffectivePrototype],
|
prototypes: &[EffectivePrototype],
|
||||||
identity_policy: AssetIdentityPolicy,
|
identity_policy: AssetIdentityPolicy,
|
||||||
limits: AssetPreparationLimits,
|
limits: AssetPreparationLimits,
|
||||||
|
on_phase: &mut dyn FnMut(AssetPreparationPhase),
|
||||||
) -> Result<(MissionAssets, AssetPreparationReport), AssetError> {
|
) -> Result<(MissionAssets, AssetPreparationReport), AssetError> {
|
||||||
if prototypes.is_empty() {
|
if prototypes.is_empty() {
|
||||||
return Ok((MissionAssets::default(), AssetPreparationReport::default()));
|
return Ok((MissionAssets::default(), AssetPreparationReport::default()));
|
||||||
@@ -725,6 +780,7 @@ fn prepare_mission_assets_with_repository_internal<R: ResourceRepository>(
|
|||||||
proto,
|
proto,
|
||||||
identity_policy,
|
identity_policy,
|
||||||
&mut preparation_cache,
|
&mut preparation_cache,
|
||||||
|
on_phase,
|
||||||
)?;
|
)?;
|
||||||
if bundle.visual.id != visual_id {
|
if bundle.visual.id != visual_id {
|
||||||
// Defensive check. stable IDs are deterministic for the same inputs.
|
// Defensive check. stable IDs are deterministic for the same inputs.
|
||||||
@@ -1416,6 +1472,7 @@ pub fn prepare_visual_with_repository<R: ResourceRepository>(
|
|||||||
proto,
|
proto,
|
||||||
AssetIdentityPolicy::default(),
|
AssetIdentityPolicy::default(),
|
||||||
&mut preparation_cache,
|
&mut preparation_cache,
|
||||||
|
&mut |_| {},
|
||||||
)?
|
)?
|
||||||
.visual)
|
.visual)
|
||||||
}
|
}
|
||||||
@@ -1444,6 +1501,7 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
|
|||||||
proto: &EffectivePrototype,
|
proto: &EffectivePrototype,
|
||||||
identity_policy: AssetIdentityPolicy,
|
identity_policy: AssetIdentityPolicy,
|
||||||
preparation_cache: &mut PreparationCache,
|
preparation_cache: &mut PreparationCache,
|
||||||
|
on_phase: &mut dyn FnMut(AssetPreparationPhase),
|
||||||
) -> Result<PreparedVisualBundle, AssetError> {
|
) -> Result<PreparedVisualBundle, AssetError> {
|
||||||
let PrototypeGeometry::Mesh(mesh_key) = &proto.geometry else {
|
let PrototypeGeometry::Mesh(mesh_key) = &proto.geometry else {
|
||||||
return Ok(PreparedVisualBundle {
|
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 = prepare_model_cached(repository, mesh_key, preparation_cache)?;
|
||||||
let model_id = AssetId::new((identity_policy.model)(proto));
|
let model_id = AssetId::new((identity_policy.model)(proto));
|
||||||
let prepared_model = PreparedModel {
|
let prepared_model = PreparedModel {
|
||||||
@@ -1491,6 +1550,7 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|lightmap| lightmap.lightmap.clone())
|
.map(|lightmap| lightmap.lightmap.clone())
|
||||||
.collect();
|
.collect();
|
||||||
|
on_phase(AssetPreparationPhase::Materials);
|
||||||
for material_index in 0..wear.entries.len() {
|
for material_index in 0..wear.entries.len() {
|
||||||
let material_index = u16::try_from(material_index).map_err(|_| {
|
let material_index = u16::try_from(material_index).map_err(|_| {
|
||||||
AssetError::InvalidPrototype("material index does not fit archive format".to_string())
|
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 {
|
for texture in texture_requests {
|
||||||
|
on_phase(AssetPreparationPhase::Textures);
|
||||||
let prepared_texture = prepare_texture_cached(
|
let prepared_texture = prepare_texture_cached(
|
||||||
repository,
|
repository,
|
||||||
&texture,
|
&texture,
|
||||||
@@ -1534,6 +1595,7 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for lightmap in &wear.lightmaps {
|
for lightmap in &wear.lightmaps {
|
||||||
|
on_phase(AssetPreparationPhase::Textures);
|
||||||
let prepared_lightmap = prepare_texture_cached(
|
let prepared_lightmap = prepare_texture_cached(
|
||||||
repository,
|
repository,
|
||||||
&lightmap.lightmap,
|
&lightmap.lightmap,
|
||||||
@@ -2830,6 +2892,7 @@ mod tests {
|
|||||||
&prototypes,
|
&prototypes,
|
||||||
policy,
|
policy,
|
||||||
AssetPreparationLimits::default(),
|
AssetPreparationLimits::default(),
|
||||||
|
&mut |_| {},
|
||||||
)
|
)
|
||||||
.expect_err("collision should fail");
|
.expect_err("collision should fail");
|
||||||
|
|
||||||
|
|||||||
@@ -23,9 +23,9 @@
|
|||||||
use fparkan_assets::{
|
use fparkan_assets::{
|
||||||
decode_mission_land_path, decode_mission_payload, decode_nres_payload,
|
decode_mission_land_path, decode_mission_payload, decode_nres_payload,
|
||||||
derive_mission_land_paths, extend_graph_report_with_visual_dependencies, prepare_terrain_world,
|
derive_mission_land_paths, extend_graph_report_with_visual_dependencies, prepare_terrain_world,
|
||||||
AssetError as AssetPreparationError, AssetId, AssetManager, BuildCategory, MissionAssetPlan,
|
AssetError as AssetPreparationError, AssetId, AssetManager, AssetPreparationPhase,
|
||||||
MissionDocument, MissionError, MissionTerrainPaths, NresError, PreparedVisual,
|
BuildCategory, MissionAssetPlan, MissionDocument, MissionError, MissionTerrainPaths, NresError,
|
||||||
TerrainFormatError, TerrainPreparationError, TerrainWorld, TmaProfile,
|
PreparedVisual, TerrainFormatError, TerrainPreparationError, TerrainWorld, TmaProfile,
|
||||||
};
|
};
|
||||||
use fparkan_path::{normalize_relative, NormalizedPath, PathError, PathPolicy};
|
use fparkan_path::{normalize_relative, NormalizedPath, PathError, PathPolicy};
|
||||||
use fparkan_prototype::{
|
use fparkan_prototype::{
|
||||||
@@ -120,6 +120,12 @@ pub enum MissionLoadPhase {
|
|||||||
GraphVisuals,
|
GraphVisuals,
|
||||||
/// Prepare all reachable visual/resource dependencies.
|
/// Prepare all reachable visual/resource dependencies.
|
||||||
Assets,
|
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 all object drafts before registration.
|
||||||
Construct,
|
Construct,
|
||||||
/// Register constructed objects.
|
/// Register constructed objects.
|
||||||
@@ -698,22 +704,38 @@ fn load_mission_with_options_and_progress(
|
|||||||
}
|
}
|
||||||
record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Assets);
|
record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Assets);
|
||||||
let asset_manager = AssetManager::new(repository);
|
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 {
|
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,
|
&prototype_graph.root_prototype_request_spans,
|
||||||
&resolved_prototypes,
|
&resolved_prototypes,
|
||||||
|
&mut observe_asset_phase,
|
||||||
),
|
),
|
||||||
MissionAssetScope::FirstMeshPreview => prepare_first_preview_assets(
|
MissionAssetScope::FirstMeshPreview => prepare_first_preview_assets(
|
||||||
&asset_manager,
|
&asset_manager,
|
||||||
&prototype_graph.root_prototype_request_spans,
|
&prototype_graph.root_prototype_request_spans,
|
||||||
&resolved_prototypes,
|
&resolved_prototypes,
|
||||||
|
&mut observe_asset_phase,
|
||||||
),
|
),
|
||||||
MissionAssetScope::PreviewRoots(root_count) => asset_manager.prepare_mission_assets(
|
MissionAssetScope::PreviewRoots(root_count) => asset_manager
|
||||||
&prototype_graph.root_prototype_request_spans[..root_count
|
.prepare_mission_assets_with_progress(
|
||||||
.get()
|
&prototype_graph.root_prototype_request_spans[..root_count
|
||||||
.min(prototype_graph.root_prototype_request_spans.len())],
|
.get()
|
||||||
&resolved_prototypes,
|
.min(prototype_graph.root_prototype_request_spans.len())],
|
||||||
),
|
&resolved_prototypes,
|
||||||
|
&mut observe_asset_phase,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
.map_err(|source| EngineError::AssetPreparation {
|
.map_err(|source| EngineError::AssetPreparation {
|
||||||
mission: request.key.clone(),
|
mission: request.key.clone(),
|
||||||
@@ -831,10 +853,14 @@ fn prepare_first_preview_assets<R: ResourceRepository>(
|
|||||||
asset_manager: &AssetManager<R>,
|
asset_manager: &AssetManager<R>,
|
||||||
root_spans: &[std::ops::Range<usize>],
|
root_spans: &[std::ops::Range<usize>],
|
||||||
prototypes: &[fparkan_prototype::EffectivePrototype],
|
prototypes: &[fparkan_prototype::EffectivePrototype],
|
||||||
|
on_phase: &mut dyn FnMut(AssetPreparationPhase),
|
||||||
) -> Result<MissionAssets, AssetPreparationError> {
|
) -> Result<MissionAssets, AssetPreparationError> {
|
||||||
for span in root_spans {
|
for span in root_spans {
|
||||||
let assets =
|
let assets = asset_manager.prepare_mission_assets_with_progress(
|
||||||
asset_manager.prepare_mission_assets(std::slice::from_ref(span), prototypes)?;
|
std::slice::from_ref(span),
|
||||||
|
prototypes,
|
||||||
|
&mut *on_phase,
|
||||||
|
)?;
|
||||||
if !assets.models.is_empty() {
|
if !assets.models.is_empty() {
|
||||||
return Ok(assets);
|
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
|
demonstrate an additional checkpoint advance from this cache; it must not be
|
||||||
reported as a measured startup improvement.
|
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
|
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
|
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
|
source Z height, and leave raycasts as full 3D intersections. The Part 1 and
|
||||||
|
|||||||
Reference in New Issue
Block a user