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 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<R: ResourceRepository>(
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<R: ResourceRepository>(
repository: &R,
proto: &EffectivePrototype,
) -> Result<PreparedVisual, AssetError> {
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<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.
#[allow(clippy::too_many_lines)]
fn prepare_visual_with_repository_internal<R: ResourceRepository>(
repository: &R,
proto: &EffectivePrototype,
identity_policy: AssetIdentityPolicy,
texture_cache: &mut TexturePreparationCache,
) -> Result<PreparedVisualBundle, AssetError> {
let PrototypeGeometry::Mesh(mesh_key) = &proto.geometry else {
return Ok(PreparedVisualBundle {
@@ -1483,11 +1499,12 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
});
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<R: ResourceRepository>(
}
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<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>(
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(