From 3f14e1ad7d8c01bd99eb75580b54ac558e6c6465 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sat, 18 Jul 2026 06:29:29 +0400 Subject: [PATCH] perf(assets): reuse textures during mission preparation --- crates/fparkan-assets/src/lib.rs | 90 +++++++++++++++++++++++++++++--- 1 file changed, 82 insertions(+), 8 deletions(-) diff --git a/crates/fparkan-assets/src/lib.rs b/crates/fparkan-assets/src/lib.rs index 9fb90ed..a5dbfd1 100644 --- a/crates/fparkan-assets/src/lib.rs +++ b/crates/fparkan-assets/src/lib.rs @@ -693,6 +693,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(); for proto in prototypes { let visual_id = AssetId::new((identity_policy.visual)(proto)); @@ -706,8 +707,12 @@ fn prepare_mission_assets_with_repository_internal( Some(_) => {} None => { visual_index_by_id.insert(visual_id, signature); - let bundle = - prepare_visual_with_repository_internal(repository, proto, identity_policy)?; + let bundle = prepare_visual_with_repository_internal( + repository, + proto, + identity_policy, + &mut texture_cache, + )?; if bundle.visual.id != visual_id { // Defensive check. stable IDs are deterministic for the same inputs. return Err(AssetError::InvalidPrototype( @@ -1380,10 +1385,14 @@ pub fn prepare_visual_with_repository( repository: &R, proto: &EffectivePrototype, ) -> Result { - Ok( - prepare_visual_with_repository_internal(repository, proto, AssetIdentityPolicy::default())? - .visual, - ) + let mut texture_cache = TexturePreparationCache::default(); + Ok(prepare_visual_with_repository_internal( + repository, + proto, + AssetIdentityPolicy::default(), + &mut texture_cache, + )? + .visual) } struct PreparedVisualBundle { @@ -1394,12 +1403,19 @@ struct PreparedVisualBundle { textures: Vec, } +#[derive(Default)] +struct TexturePreparationCache { + diffuse: HashMap, PreparedTexture>, + lightmaps: HashMap, PreparedTexture>, +} + // Keeps the complete model -> WEAR -> material -> texture transaction in one fallible boundary. #[allow(clippy::too_many_lines)] fn prepare_visual_with_repository_internal( repository: &R, proto: &EffectivePrototype, identity_policy: AssetIdentityPolicy, + texture_cache: &mut TexturePreparationCache, ) -> Result { let PrototypeGeometry::Mesh(mesh_key) = &proto.geometry else { return Ok(PreparedVisualBundle { @@ -1483,11 +1499,12 @@ fn prepare_visual_with_repository_internal( }); for texture in texture_requests { - let prepared_texture = prepare_texture( + let prepared_texture = prepare_texture_cached( repository, &texture, PreparedTextureUsage::Diffuse, identity_policy, + texture_cache, )?; texture_ids.push(prepared_texture.id); prepared_textures.push(prepared_texture); @@ -1496,11 +1513,12 @@ fn prepare_visual_with_repository_internal( } for lightmap in &wear.lightmaps { - let prepared_lightmap = prepare_texture( + let prepared_lightmap = prepare_texture_cached( repository, &lightmap.lightmap, PreparedTextureUsage::Lightmap, identity_policy, + texture_cache, )?; lightmap_ids.push(prepared_lightmap.id); prepared_textures.push(prepared_lightmap); @@ -1785,6 +1803,25 @@ fn prepare_texture( }) } +fn prepare_texture_cached( + repository: &R, + name: &ResourceName, + usage: PreparedTextureUsage, + identity_policy: AssetIdentityPolicy, + cache: &mut TexturePreparationCache, +) -> Result { + let entries = match usage { + PreparedTextureUsage::Diffuse => &mut cache.diffuse, + PreparedTextureUsage::Lightmap => &mut cache.lightmaps, + }; + if let Some(texture) = entries.get(&name.0) { + return Ok(texture.clone()); + } + let texture = prepare_texture(repository, name, usage, identity_policy)?; + entries.insert(name.0.clone(), texture.clone()); + Ok(texture) +} + fn resolve_texm( repository: &R, name: &ResourceName, @@ -2425,6 +2462,43 @@ mod tests { assert_eq!(visual.lightmap_ids.len(), 1); } + #[test] + fn texture_preparation_cache_reuses_exact_request_within_one_mission() { + let texm = texm_payload(); + let repo = repository_with_archives_meta(&[( + TEXTURES_ARCHIVE, + &[TestNresEntry { + name: b"TEX_A", + payload: &texm, + type_id: 0, + attr2: 0, + }], + )]); + let name = resource_name(b"TEX_A"); + let mut cache = TexturePreparationCache::default(); + + let first = prepare_texture_cached( + &repo, + &name, + PreparedTextureUsage::Diffuse, + AssetIdentityPolicy::default(), + &mut cache, + ) + .expect("first texture"); + let second = prepare_texture_cached( + &repo, + &name, + PreparedTextureUsage::Diffuse, + AssetIdentityPolicy::default(), + &mut cache, + ) + .expect("cached texture"); + + assert_eq!(first.id, second.id); + assert_eq!(cache.diffuse.len(), 1); + assert!(cache.lightmaps.is_empty()); + } + #[test] fn forced_model_id_collision_is_rejected() { assert_forced_collision(