From 57d8b654e988c47a0fc738a763e494784679b314 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sat, 18 Jul 2026 06:35:20 +0400 Subject: [PATCH] perf(assets): reuse materials during mission preparation --- crates/fparkan-assets/src/lib.rs | 73 ++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/crates/fparkan-assets/src/lib.rs b/crates/fparkan-assets/src/lib.rs index a5dbfd1..f2b820c 100644 --- a/crates/fparkan-assets/src/lib.rs +++ b/crates/fparkan-assets/src/lib.rs @@ -21,7 +21,8 @@ //! Asset manager ports and transactional preparation models. use fparkan_material::{ - decode_wear, resolve_material, Mat0Document, MaterialError, WearTable, MAT0_KIND, WEAR_KIND, + decode_wear, resolve_material, Mat0Document, MaterialError, ResolvedMaterial, WearTable, + MAT0_KIND, WEAR_KIND, }; use fparkan_mission_format::{decode_tma, decode_tma_land_path}; pub use fparkan_mission_format::{LpString, MissionDocument, MissionError, TmaProfile}; @@ -693,7 +694,7 @@ fn prepare_mission_assets_with_repository_internal( let mut textures = Vec::new(); let mut visuals = Vec::new(); let mut prototype_visual_ids = Vec::with_capacity(prototypes.len()); - let mut texture_cache = TexturePreparationCache::default(); + let mut preparation_cache = PreparationCache::default(); for proto in prototypes { let visual_id = AssetId::new((identity_policy.visual)(proto)); @@ -711,7 +712,7 @@ fn prepare_mission_assets_with_repository_internal( repository, proto, identity_policy, - &mut texture_cache, + &mut preparation_cache, )?; if bundle.visual.id != visual_id { // Defensive check. stable IDs are deterministic for the same inputs. @@ -1385,12 +1386,12 @@ pub fn prepare_visual_with_repository( repository: &R, proto: &EffectivePrototype, ) -> Result { - let mut texture_cache = TexturePreparationCache::default(); + let mut preparation_cache = PreparationCache::default(); Ok(prepare_visual_with_repository_internal( repository, proto, AssetIdentityPolicy::default(), - &mut texture_cache, + &mut preparation_cache, )? .visual) } @@ -1404,9 +1405,10 @@ struct PreparedVisualBundle { } #[derive(Default)] -struct TexturePreparationCache { +struct PreparationCache { diffuse: HashMap, PreparedTexture>, lightmaps: HashMap, PreparedTexture>, + materials: HashMap, ResolvedMaterial>, } // Keeps the complete model -> WEAR -> material -> texture transaction in one fallible boundary. @@ -1415,7 +1417,7 @@ fn prepare_visual_with_repository_internal( repository: &R, proto: &EffectivePrototype, identity_policy: AssetIdentityPolicy, - texture_cache: &mut TexturePreparationCache, + preparation_cache: &mut PreparationCache, ) -> Result { let PrototypeGeometry::Mesh(mesh_key) = &proto.geometry else { return Ok(PreparedVisualBundle { @@ -1475,7 +1477,7 @@ fn prepare_visual_with_repository_internal( AssetError::InvalidPrototype("material index does not fit archive format".to_string()) })?; let material = - resolve_material(repository, &wear, material_index).map_err(AssetError::Material)?; + resolve_material_cached(repository, &wear, material_index, preparation_cache)?; material_count += 1; let material_id = AssetId::new((identity_policy.material)( proto, @@ -1504,7 +1506,7 @@ fn prepare_visual_with_repository_internal( &texture, PreparedTextureUsage::Diffuse, identity_policy, - texture_cache, + preparation_cache, )?; texture_ids.push(prepared_texture.id); prepared_textures.push(prepared_texture); @@ -1518,7 +1520,7 @@ fn prepare_visual_with_repository_internal( &lightmap.lightmap, PreparedTextureUsage::Lightmap, identity_policy, - texture_cache, + preparation_cache, )?; lightmap_ids.push(prepared_lightmap.id); prepared_textures.push(prepared_lightmap); @@ -1808,7 +1810,7 @@ fn prepare_texture_cached( name: &ResourceName, usage: PreparedTextureUsage, identity_policy: AssetIdentityPolicy, - cache: &mut TexturePreparationCache, + cache: &mut PreparationCache, ) -> Result { let entries = match usage { PreparedTextureUsage::Diffuse => &mut cache.diffuse, @@ -1822,6 +1824,31 @@ fn prepare_texture_cached( Ok(texture) } +fn resolve_material_cached( + repository: &R, + table: &WearTable, + index: u16, + cache: &mut PreparationCache, +) -> Result { + let request = table + .entries + .get(usize::from(index)) + .ok_or(MaterialError::WearIndexOutOfBounds { + index, + count: table.entries.len(), + }) + .map_err(AssetError::Material)? + .material + .0 + .clone(); + if let Some(material) = cache.materials.get(&request) { + return Ok(material.clone()); + } + let material = resolve_material(repository, table, index).map_err(AssetError::Material)?; + cache.materials.insert(request, material.clone()); + Ok(material) +} + fn resolve_texm( repository: &R, name: &ResourceName, @@ -2475,7 +2502,7 @@ mod tests { }], )]); let name = resource_name(b"TEX_A"); - let mut cache = TexturePreparationCache::default(); + let mut cache = PreparationCache::default(); let first = prepare_texture_cached( &repo, @@ -2499,6 +2526,28 @@ mod tests { assert!(cache.lightmaps.is_empty()); } + #[test] + fn material_preparation_cache_preserves_exact_wear_request() { + let mat0 = mat0_with_texture(b"TEX_A"); + let repo = repository_with_archives_meta(&[( + "material.lib", + &[TestNresEntry { + name: b"MAT_A", + payload: &mat0, + type_id: MAT0_KIND, + attr2: 0, + }], + )]); + let wear = decode_wear(b"1\n0 MAT_A\n").expect("wear"); + let mut cache = PreparationCache::default(); + + let first = resolve_material_cached(&repo, &wear, 0, &mut cache).expect("first material"); + let second = resolve_material_cached(&repo, &wear, 0, &mut cache).expect("cached material"); + + assert_eq!(first, second); + assert_eq!(cache.materials.len(), 1); + } + #[test] fn forced_model_id_collision_is_rejected() { assert_forced_collision(