perf(assets): reuse materials during mission preparation
Docs Deploy / Build and Deploy MkDocs (push) Successful in 38s
Test / Lint (push) Failing after 1m58s
Test / Test (push) Skipped
Test / Render parity (push) Skipped

This commit is contained in:
2026-07-18 06:35:20 +04:00
parent 3f14e1ad7d
commit 57d8b654e9
+61 -12
View File
@@ -21,7 +21,8 @@
//! Asset manager ports and transactional preparation models. //! Asset manager ports and transactional preparation models.
use fparkan_material::{ 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}; use fparkan_mission_format::{decode_tma, decode_tma_land_path};
pub use fparkan_mission_format::{LpString, MissionDocument, MissionError, TmaProfile}; pub use fparkan_mission_format::{LpString, MissionDocument, MissionError, TmaProfile};
@@ -693,7 +694,7 @@ fn prepare_mission_assets_with_repository_internal<R: ResourceRepository>(
let mut textures = Vec::new(); let mut textures = Vec::new();
let mut visuals = Vec::new(); let mut visuals = Vec::new();
let mut prototype_visual_ids = Vec::with_capacity(prototypes.len()); 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 { for proto in prototypes {
let visual_id = AssetId::new((identity_policy.visual)(proto)); let visual_id = AssetId::new((identity_policy.visual)(proto));
@@ -711,7 +712,7 @@ fn prepare_mission_assets_with_repository_internal<R: ResourceRepository>(
repository, repository,
proto, proto,
identity_policy, identity_policy,
&mut texture_cache, &mut preparation_cache,
)?; )?;
if bundle.visual.id != visual_id { if bundle.visual.id != visual_id {
// Defensive check. stable IDs are deterministic for the same inputs. // Defensive check. stable IDs are deterministic for the same inputs.
@@ -1385,12 +1386,12 @@ pub fn prepare_visual_with_repository<R: ResourceRepository>(
repository: &R, repository: &R,
proto: &EffectivePrototype, proto: &EffectivePrototype,
) -> Result<PreparedVisual, AssetError> { ) -> Result<PreparedVisual, AssetError> {
let mut texture_cache = TexturePreparationCache::default(); let mut preparation_cache = PreparationCache::default();
Ok(prepare_visual_with_repository_internal( Ok(prepare_visual_with_repository_internal(
repository, repository,
proto, proto,
AssetIdentityPolicy::default(), AssetIdentityPolicy::default(),
&mut texture_cache, &mut preparation_cache,
)? )?
.visual) .visual)
} }
@@ -1404,9 +1405,10 @@ struct PreparedVisualBundle {
} }
#[derive(Default)] #[derive(Default)]
struct TexturePreparationCache { struct PreparationCache {
diffuse: HashMap<Vec<u8>, PreparedTexture>, diffuse: HashMap<Vec<u8>, PreparedTexture>,
lightmaps: HashMap<Vec<u8>, PreparedTexture>, lightmaps: HashMap<Vec<u8>, PreparedTexture>,
materials: HashMap<Vec<u8>, ResolvedMaterial>,
} }
// Keeps the complete model -> WEAR -> material -> texture transaction in one fallible boundary. // Keeps the complete model -> WEAR -> material -> texture transaction in one fallible boundary.
@@ -1415,7 +1417,7 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
repository: &R, repository: &R,
proto: &EffectivePrototype, proto: &EffectivePrototype,
identity_policy: AssetIdentityPolicy, identity_policy: AssetIdentityPolicy,
texture_cache: &mut TexturePreparationCache, preparation_cache: &mut PreparationCache,
) -> Result<PreparedVisualBundle, AssetError> { ) -> Result<PreparedVisualBundle, AssetError> {
let PrototypeGeometry::Mesh(mesh_key) = &proto.geometry else { let PrototypeGeometry::Mesh(mesh_key) = &proto.geometry else {
return Ok(PreparedVisualBundle { return Ok(PreparedVisualBundle {
@@ -1475,7 +1477,7 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
AssetError::InvalidPrototype("material index does not fit archive format".to_string()) AssetError::InvalidPrototype("material index does not fit archive format".to_string())
})?; })?;
let material = let material =
resolve_material(repository, &wear, material_index).map_err(AssetError::Material)?; resolve_material_cached(repository, &wear, material_index, preparation_cache)?;
material_count += 1; material_count += 1;
let material_id = AssetId::new((identity_policy.material)( let material_id = AssetId::new((identity_policy.material)(
proto, proto,
@@ -1504,7 +1506,7 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
&texture, &texture,
PreparedTextureUsage::Diffuse, PreparedTextureUsage::Diffuse,
identity_policy, identity_policy,
texture_cache, preparation_cache,
)?; )?;
texture_ids.push(prepared_texture.id); texture_ids.push(prepared_texture.id);
prepared_textures.push(prepared_texture); prepared_textures.push(prepared_texture);
@@ -1518,7 +1520,7 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
&lightmap.lightmap, &lightmap.lightmap,
PreparedTextureUsage::Lightmap, PreparedTextureUsage::Lightmap,
identity_policy, identity_policy,
texture_cache, preparation_cache,
)?; )?;
lightmap_ids.push(prepared_lightmap.id); lightmap_ids.push(prepared_lightmap.id);
prepared_textures.push(prepared_lightmap); prepared_textures.push(prepared_lightmap);
@@ -1808,7 +1810,7 @@ fn prepare_texture_cached<R: ResourceRepository>(
name: &ResourceName, name: &ResourceName,
usage: PreparedTextureUsage, usage: PreparedTextureUsage,
identity_policy: AssetIdentityPolicy, identity_policy: AssetIdentityPolicy,
cache: &mut TexturePreparationCache, cache: &mut PreparationCache,
) -> Result<PreparedTexture, AssetError> { ) -> Result<PreparedTexture, AssetError> {
let entries = match usage { let entries = match usage {
PreparedTextureUsage::Diffuse => &mut cache.diffuse, PreparedTextureUsage::Diffuse => &mut cache.diffuse,
@@ -1822,6 +1824,31 @@ fn prepare_texture_cached<R: ResourceRepository>(
Ok(texture) Ok(texture)
} }
fn resolve_material_cached<R: ResourceRepository>(
repository: &R,
table: &WearTable,
index: u16,
cache: &mut PreparationCache,
) -> Result<ResolvedMaterial, AssetError> {
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<R: ResourceRepository>( fn resolve_texm<R: ResourceRepository>(
repository: &R, repository: &R,
name: &ResourceName, name: &ResourceName,
@@ -2475,7 +2502,7 @@ mod tests {
}], }],
)]); )]);
let name = resource_name(b"TEX_A"); let name = resource_name(b"TEX_A");
let mut cache = TexturePreparationCache::default(); let mut cache = PreparationCache::default();
let first = prepare_texture_cached( let first = prepare_texture_cached(
&repo, &repo,
@@ -2499,6 +2526,28 @@ mod tests {
assert!(cache.lightmaps.is_empty()); 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] #[test]
fn forced_model_id_collision_is_rejected() { fn forced_model_id_collision_is_rejected() {
assert_forced_collision( assert_forced_collision(