aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-07-18 10:10:47 +0300
committerValentin Popov <valentin@popov.link>2026-07-18 10:10:47 +0300
commit8f2e9d930875adca980fb94755bd98a40c2e58c4 (patch)
tree7e675865e5b98cb24ac57ae801c2bcd452ee5d95
parent2cef0c62b982c6a5e1157cbe471c5653b851bd0c (diff)
downloadfparkan-8f2e9d930875adca980fb94755bd98a40c2e58c4.tar.xz
fparkan-8f2e9d930875adca980fb94755bd98a40c2e58c4.zip
perf(assets): cache prepared model dependencies
-rw-r--r--crates/fparkan-assets/src/lib.rs50
-rw-r--r--docs/tomes/05-render.md7
2 files changed, 48 insertions, 9 deletions
diff --git a/crates/fparkan-assets/src/lib.rs b/crates/fparkan-assets/src/lib.rs
index 2dac7d4..0bafcfa 100644
--- a/crates/fparkan-assets/src/lib.rs
+++ b/crates/fparkan-assets/src/lib.rs
@@ -1430,6 +1430,8 @@ struct PreparedVisualBundle {
#[derive(Default)]
struct PreparationCache {
+ models: Vec<(ResourceKey, ModelAsset)>,
+ wears: Vec<(ResourceKey, WearTable)>,
diffuse: HashMap<Vec<u8>, PreparedTexture>,
lightmaps: HashMap<Vec<u8>, PreparedTexture>,
materials: HashMap<Vec<u8>, ResolvedMaterial>,
@@ -1453,13 +1455,7 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
});
};
- let nres = decode_nres(
- read_key(repository, mesh_key, Some("mesh"))?,
- ReadProfile::Compatible,
- )
- .map_err(AssetError::Nres)?;
- let msh_document = decode_msh(&nres).map_err(AssetError::Msh)?;
- let model = validate_msh(&msh_document).map_err(AssetError::Msh)?;
+ let model = prepare_model_cached(repository, mesh_key, preparation_cache)?;
let model_id = AssetId::new((identity_policy.model)(proto));
let prepared_model = PreparedModel {
id: model_id,
@@ -1474,8 +1470,7 @@ fn prepare_visual_with_repository_internal<R: ResourceRepository>(
name: wear_name,
type_id: Some(WEAR_KIND),
};
- let wear = decode_wear(&read_key(repository, &wear_key, Some("wear"))?)
- .map_err(AssetError::Material)?;
+ let wear = prepare_wear_cached(repository, &wear_key, preparation_cache)?;
let wear_id = AssetId::new((identity_policy.wear)(proto));
let prepared_wear = PreparedWear {
id: wear_id,
@@ -1593,6 +1588,43 @@ fn read_key<R: ResourceRepository>(
Ok(Arc::from(bytes.into_owned()))
}
+fn prepare_model_cached<R: ResourceRepository>(
+ repository: &R,
+ key: &ResourceKey,
+ cache: &mut PreparationCache,
+) -> Result<ModelAsset, AssetError> {
+ if let Some((_, model)) = cache
+ .models
+ .iter()
+ .find(|(cached_key, _)| cached_key == key)
+ {
+ return Ok(model.clone());
+ }
+ let nres = decode_nres(
+ read_key(repository, key, Some("mesh"))?,
+ ReadProfile::Compatible,
+ )
+ .map_err(AssetError::Nres)?;
+ let document = decode_msh(&nres).map_err(AssetError::Msh)?;
+ let model = validate_msh(&document).map_err(AssetError::Msh)?;
+ cache.models.push((key.clone(), model.clone()));
+ Ok(model)
+}
+
+fn prepare_wear_cached<R: ResourceRepository>(
+ repository: &R,
+ key: &ResourceKey,
+ cache: &mut PreparationCache,
+) -> Result<WearTable, AssetError> {
+ if let Some((_, wear)) = cache.wears.iter().find(|(cached_key, _)| cached_key == key) {
+ return Ok(wear.clone());
+ }
+ let wear =
+ decode_wear(&read_key(repository, key, Some("wear"))?).map_err(AssetError::Material)?;
+ cache.wears.push((key.clone(), wear.clone()));
+ Ok(wear)
+}
+
fn map_resource_error(label: &str, key: &ResourceKey, source: ResourceError) -> AssetError {
AssetError::Resource {
context: format!(
diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md
index 28a7b7c..aee1f29 100644
--- a/docs/tomes/05-render.md
+++ b/docs/tomes/05-render.md
@@ -1056,6 +1056,13 @@ last checkpoint advanced to `Assets`. The process was deliberately terminated
before a frame, so this proves reduced visual-graph work rather than a Vulkan
acceptance result.
+Asset preparation additionally caches validated MSH models and decoded WEAR
+tables by their complete resource key. This preserves separate prepared visual
+provenance while avoiding repeat format work for repeated components. The same
+60-second first-root GOG probe remained at `Assets`, so that corpus does not
+demonstrate an additional checkpoint advance from this cache; it must not be
+reported as a measured startup improvement.
+
The same source-axis proof now applies to `TerrainWorld`: its `Land.msh` surface
height query and `Land.map` areal/grid lookup use XY ground coordinates, return
source Z height, and leave raycasts as full 3D intersections. The Part 1 and