feat(runtime): trace asset preparation phases
Docs Deploy / Build and Deploy MkDocs (push) Successful in 35s
Test / Lint (push) Failing after 2m3s
Test / Test (push) Skipped
Test / Render parity (push) Skipped

This commit is contained in:
2026-07-18 11:15:43 +04:00
parent 8f2e9d9308
commit c55d835d35
3 changed files with 110 additions and 12 deletions
+63
View File
@@ -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");
+33 -7
View File
@@ -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,21 +704,37 @@ 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(
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 {
@@ -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);
}
+9
View File
@@ -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