From 8f2e9d930875adca980fb94755bd98a40c2e58c4 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sat, 18 Jul 2026 11:10:47 +0400 Subject: [PATCH] perf(assets): cache prepared model dependencies --- crates/fparkan-assets/src/lib.rs | 50 ++++++++++++++++++++++++++------ docs/tomes/05-render.md | 7 +++++ 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/crates/fparkan-assets/src/lib.rs b/crates/fparkan-assets/src/lib.rs index 2dac7d4..0bafcfa 100644 --- a/crates/fparkan-assets/src/lib.rs +++ b/crates/fparkan-assets/src/lib.rs @@ -1430,6 +1430,8 @@ struct PreparedVisualBundle { #[derive(Default)] struct PreparationCache { + models: Vec<(ResourceKey, ModelAsset)>, + wears: Vec<(ResourceKey, WearTable)>, diffuse: HashMap, PreparedTexture>, lightmaps: HashMap, PreparedTexture>, materials: HashMap, ResolvedMaterial>, @@ -1453,13 +1455,7 @@ fn prepare_visual_with_repository_internal( }); }; - let nres = decode_nres( - read_key(repository, mesh_key, Some("mesh"))?, - ReadProfile::Compatible, - ) - .map_err(AssetError::Nres)?; - let msh_document = decode_msh(&nres).map_err(AssetError::Msh)?; - let model = validate_msh(&msh_document).map_err(AssetError::Msh)?; + let model = prepare_model_cached(repository, mesh_key, preparation_cache)?; let model_id = AssetId::new((identity_policy.model)(proto)); let prepared_model = PreparedModel { id: model_id, @@ -1474,8 +1470,7 @@ fn prepare_visual_with_repository_internal( name: wear_name, type_id: Some(WEAR_KIND), }; - let wear = decode_wear(&read_key(repository, &wear_key, Some("wear"))?) - .map_err(AssetError::Material)?; + let wear = prepare_wear_cached(repository, &wear_key, preparation_cache)?; let wear_id = AssetId::new((identity_policy.wear)(proto)); let prepared_wear = PreparedWear { id: wear_id, @@ -1593,6 +1588,43 @@ fn read_key( Ok(Arc::from(bytes.into_owned())) } +fn prepare_model_cached( + repository: &R, + key: &ResourceKey, + cache: &mut PreparationCache, +) -> Result { + if let Some((_, model)) = cache + .models + .iter() + .find(|(cached_key, _)| cached_key == key) + { + return Ok(model.clone()); + } + let nres = decode_nres( + read_key(repository, key, Some("mesh"))?, + ReadProfile::Compatible, + ) + .map_err(AssetError::Nres)?; + let document = decode_msh(&nres).map_err(AssetError::Msh)?; + let model = validate_msh(&document).map_err(AssetError::Msh)?; + cache.models.push((key.clone(), model.clone())); + Ok(model) +} + +fn prepare_wear_cached( + repository: &R, + key: &ResourceKey, + cache: &mut PreparationCache, +) -> Result { + if let Some((_, wear)) = cache.wears.iter().find(|(cached_key, _)| cached_key == key) { + return Ok(wear.clone()); + } + let wear = + decode_wear(&read_key(repository, key, Some("wear"))?).map_err(AssetError::Material)?; + cache.wears.push((key.clone(), wear.clone())); + Ok(wear) +} + fn map_resource_error(label: &str, key: &ResourceKey, source: ResourceError) -> AssetError { AssetError::Resource { context: format!( diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md index 28a7b7c..aee1f29 100644 --- a/docs/tomes/05-render.md +++ b/docs/tomes/05-render.md @@ -1056,6 +1056,13 @@ last checkpoint advanced to `Assets`. The process was deliberately terminated before a frame, so this proves reduced visual-graph work rather than a Vulkan acceptance result. +Asset preparation additionally caches validated MSH models and decoded WEAR +tables by their complete resource key. This preserves separate prepared visual +provenance while avoiding repeat format work for repeated components. The same +60-second first-root GOG probe remained at `Assets`, so that corpus does not +demonstrate an additional checkpoint advance from this cache; it must not be +reported as a measured startup improvement. + 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