aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/fparkan-assets/src/lib.rs63
-rw-r--r--crates/fparkan-runtime/src/lib.rs50
-rw-r--r--docs/tomes/05-render.md9
3 files changed, 110 insertions, 12 deletions
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<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");
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<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);
}
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