perf(assets): reuse textures during mission preparation
Docs Deploy / Build and Deploy MkDocs (push) Successful in 37s
Test / Lint (push) Failing after 2m6s
Test / Test (push) Skipped
Test / Render parity (push) Skipped

This commit is contained in:
2026-07-18 06:29:29 +04:00
parent 597ffef19c
commit 3f14e1ad7d
+82 -8
View File
@@ -693,6 +693,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();
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));
@@ -706,8 +707,12 @@ fn prepare_mission_assets_with_repository_internal<R: ResourceRepository>(
Some(_) => {} Some(_) => {}
None => { None => {
visual_index_by_id.insert(visual_id, signature); visual_index_by_id.insert(visual_id, signature);
let bundle = let bundle = prepare_visual_with_repository_internal(
prepare_visual_with_repository_internal(repository, proto, identity_policy)?; repository,
proto,
identity_policy,
&mut texture_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.
return Err(AssetError::InvalidPrototype( return Err(AssetError::InvalidPrototype(
@@ -1380,10 +1385,14 @@ pub fn prepare_visual_with_repository<R: ResourceRepository>(
repository: &R, repository: &R,
proto: &EffectivePrototype, proto: &EffectivePrototype,
) -> Result<PreparedVisual, AssetError> { ) -> Result<PreparedVisual, AssetError> {
Ok( let mut texture_cache = TexturePreparationCache::default();
prepare_visual_with_repository_internal(repository, proto, AssetIdentityPolicy::default())? Ok(prepare_visual_with_repository_internal(
.visual, repository,
) proto,
AssetIdentityPolicy::default(),
&mut texture_cache,
)?
.visual)
} }
struct PreparedVisualBundle { struct PreparedVisualBundle {
@@ -1394,12 +1403,19 @@ struct PreparedVisualBundle {
textures: Vec<PreparedTexture>, textures: Vec<PreparedTexture>,
} }
#[derive(Default)]
struct TexturePreparationCache {
diffuse: HashMap<Vec<u8>, PreparedTexture>,
lightmaps: HashMap<Vec<u8>, PreparedTexture>,
}
// Keeps the complete model -> WEAR -> material -> texture transaction in one fallible boundary. // Keeps the complete model -> WEAR -> material -> texture transaction in one fallible boundary.
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
fn prepare_visual_with_repository_internal<R: ResourceRepository>( 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,
) -> 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 {
@@ -1483,11 +1499,12 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
}); });
for texture in texture_requests { for texture in texture_requests {
let prepared_texture = prepare_texture( let prepared_texture = prepare_texture_cached(
repository, repository,
&texture, &texture,
PreparedTextureUsage::Diffuse, PreparedTextureUsage::Diffuse,
identity_policy, identity_policy,
texture_cache,
)?; )?;
texture_ids.push(prepared_texture.id); texture_ids.push(prepared_texture.id);
prepared_textures.push(prepared_texture); prepared_textures.push(prepared_texture);
@@ -1496,11 +1513,12 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
} }
for lightmap in &wear.lightmaps { for lightmap in &wear.lightmaps {
let prepared_lightmap = prepare_texture( let prepared_lightmap = prepare_texture_cached(
repository, repository,
&lightmap.lightmap, &lightmap.lightmap,
PreparedTextureUsage::Lightmap, PreparedTextureUsage::Lightmap,
identity_policy, identity_policy,
texture_cache,
)?; )?;
lightmap_ids.push(prepared_lightmap.id); lightmap_ids.push(prepared_lightmap.id);
prepared_textures.push(prepared_lightmap); prepared_textures.push(prepared_lightmap);
@@ -1785,6 +1803,25 @@ fn prepare_texture<R: ResourceRepository>(
}) })
} }
fn prepare_texture_cached<R: ResourceRepository>(
repository: &R,
name: &ResourceName,
usage: PreparedTextureUsage,
identity_policy: AssetIdentityPolicy,
cache: &mut TexturePreparationCache,
) -> Result<PreparedTexture, AssetError> {
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<R: ResourceRepository>( fn resolve_texm<R: ResourceRepository>(
repository: &R, repository: &R,
name: &ResourceName, name: &ResourceName,
@@ -2425,6 +2462,43 @@ mod tests {
assert_eq!(visual.lightmap_ids.len(), 1); 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] #[test]
fn forced_model_id_collision_is_rejected() { fn forced_model_id_collision_is_rejected() {
assert_forced_collision( assert_forced_collision(