aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-07-18 14:15:29 +0300
committerValentin Popov <valentin@popov.link>2026-07-18 14:15:29 +0300
commit733bfcf6fa2e17dbc94487fb1b9eb4f896e53845 (patch)
treeced4252a65d0ad4c1ddc8adca70981187f746b41
parent4007f6fcec12eab91c1d2cb25dc3582afb121f76 (diff)
downloadfparkan-733bfcf6fa2e17dbc94487fb1b9eb4f896e53845.tar.xz
fparkan-733bfcf6fa2e17dbc94487fb1b9eb4f896e53845.zip
perf(assets): reuse archives during mission loads
-rw-r--r--crates/fparkan-assets/src/lib.rs6
-rw-r--r--crates/fparkan-resource/src/lib.rs45
-rw-r--r--docs/tomes/05-render.md22
3 files changed, 65 insertions, 8 deletions
diff --git a/crates/fparkan-assets/src/lib.rs b/crates/fparkan-assets/src/lib.rs
index 58fa5ed..5bb3edf 100644
--- a/crates/fparkan-assets/src/lib.rs
+++ b/crates/fparkan-assets/src/lib.rs
@@ -1863,7 +1863,7 @@ fn read_key<R: ResourceRepository>(
) -> Result<Arc<[u8]>, AssetError> {
let label = label.unwrap_or("asset");
let archive = repository
- .open_archive(&key.archive)
+ .open_archive_unchanged(&key.archive)
.map_err(|err| map_resource_error(label, key, err))?;
let handle = repository
.find(archive, &key.name)
@@ -1928,7 +1928,7 @@ fn resolve_wear_table<R: ResourceRepository>(
mesh: &ResourceKey,
) -> Result<fparkan_material::WearTable, AssetError> {
let archive = repository
- .open_archive(&mesh.archive)
+ .open_archive_unchanged(&mesh.archive)
.map_err(|err| map_resource_error("wear", mesh, err))?;
let wear_name = sibling_name(mesh, "wea")?;
let handle = repository
@@ -2271,7 +2271,7 @@ fn read_optional_key<R: ResourceRepository>(
key: &ResourceKey,
label: Option<&str>,
) -> Result<Option<Arc<[u8]>>, AssetError> {
- let archive = match repository.open_archive(&key.archive) {
+ let archive = match repository.open_archive_unchanged(&key.archive) {
Ok(archive) => archive,
Err(ResourceError::MissingArchive { .. } | ResourceError::MissingEntry) => return Ok(None),
Err(err) => {
diff --git a/crates/fparkan-resource/src/lib.rs b/crates/fparkan-resource/src/lib.rs
index 8fbd62e..f996a71 100644
--- a/crates/fparkan-resource/src/lib.rs
+++ b/crates/fparkan-resource/src/lib.rs
@@ -258,6 +258,21 @@ pub trait ResourceRepository {
/// Returns [`ResourceError`] when the archive is missing, unsupported, or
/// malformed.
fn open_archive(&self, path: &NormalizedPath) -> Result<ArchiveId, ResourceError>;
+ /// Opens an archive known to remain unchanged for the caller's bounded
+ /// loading transaction.
+ ///
+ /// Implementations may reuse an already decoded archive without a second
+ /// content-fingerprint pass. Callers must use this only while their asset
+ /// source is immutable; the default preserves [`Self::open_archive`]'s
+ /// strict invalidation behavior.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`ResourceError`] when the archive is missing, unsupported, or
+ /// malformed.
+ fn open_archive_unchanged(&self, path: &NormalizedPath) -> Result<ArchiveId, ResourceError> {
+ self.open_archive(path)
+ }
/// Finds entry.
///
/// # Errors
@@ -524,6 +539,19 @@ impl ResourceRepository for CachedResourceRepository {
}
}
+ fn open_archive_unchanged(&self, path: &NormalizedPath) -> Result<ArchiveId, ResourceError> {
+ let key = path.identity_bytes().to_vec();
+ let mut state = self.state.lock().map_err(|_| ResourceError::Poisoned)?;
+ if let Some(id) = state.paths.get(&key).copied() {
+ if state.archive(id)?.document.is_some() {
+ state.touch_archive(id)?;
+ return Ok(id);
+ }
+ }
+ drop(state);
+ self.open_archive(path)
+ }
+
fn find(
&self,
archive: ArchiveId,
@@ -1102,6 +1130,23 @@ mod tests {
}
#[test]
+ fn unchanged_transaction_reuses_decoded_archive_without_rereading() {
+ let path = archive_path(b"archives/test.lib").expect("path");
+ let bytes = Arc::from(build_nres(&[("one", b"payload")]).into_boxed_slice());
+ let vfs = Arc::new(CountingVfs::new(bytes));
+ let repo = CachedResourceRepository::new(Arc::clone(&vfs) as Arc<dyn Vfs>);
+
+ let first = repo.open_archive_unchanged(&path).expect("first open");
+ let second = repo
+ .open_archive_unchanged(&path)
+ .expect("transactional cached open");
+
+ assert_eq!(first, second);
+ assert_eq!(vfs.reads.load(Ordering::Relaxed), 1);
+ assert_eq!(vfs.metadata_reads.load(Ordering::Relaxed), 1);
+ }
+
+ #[test]
fn concurrent_same_archive_open_reuses_archive_id() {
let path = archive_path(b"archives/test.lib").expect("path");
let bytes = Arc::from(build_nres(&[("Alpha.TXT", b"alpha".as_slice())]).into_boxed_slice());
diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md
index 4a082f8..543af27 100644
--- a/docs/tomes/05-render.md
+++ b/docs/tomes/05-render.md
@@ -1457,11 +1457,23 @@ is the base of the `0x1A4` camera object (vtables at `+0` and `+4`), while
with its own vtable. This capture format deliberately preserves the former
base-object interpretation and does not invent camera ownership.
-A bounded first launch of the capture-driven static preview against the
-canonical GOG `MISSIONS/Autodemo.00/data.tma` did not reach a native frame
-within 60 seconds. It is therefore load-path evidence only, not a rendering or
-pixel-parity result; the existing full dependency preparation remains the next
-performance/runtime obstacle before this exact capture can be visually compared.
+The first bounded launch showed that repeated fingerprinting, rather than
+Vulkan initialization, was the load-path bottleneck: each new MAT0/TEXM request
+re-opened an already decoded archive and re-hashed its entire source file.
+`ResourceRepository::open_archive_unchanged` now makes the *explicit* bounded
+loading-transaction contract available. The normal `open_archive` remains the
+strict public path and still invalidates stale entry handles after an external
+archive replacement; only mission asset preparation uses the transactional
+variant because it consumes an immutable snapshot of the selected game root.
+
+With that scoped cache, canonical GOG
+`MISSIONS/Autodemo.00/data.tma --backend static-vulkan --preview-roots 1`
+completed preparation in 6.308 seconds (the visual graph completed in 6.012
+seconds) and produced a 1280×720 native Vulkan frame with the captured D3D7
+camera, 15 material descriptors, and zero validation warnings or errors. This
+is a real original-asset rendering milestone, but not pixel parity or gameplay
+camera parity: it intentionally renders the bounded first preview root and
+uses a one-time offline camera capture.
A fresh no-input launch of the canonical `iron_3d.exe` did create a responsive
window titled `Parkan. Железная Стратегия`. A read-only probe then requested