perf(assets): reuse archives during mission loads
This commit is contained in:
@@ -1863,7 +1863,7 @@ fn read_key<R: ResourceRepository>(
|
|||||||
) -> Result<Arc<[u8]>, AssetError> {
|
) -> Result<Arc<[u8]>, AssetError> {
|
||||||
let label = label.unwrap_or("asset");
|
let label = label.unwrap_or("asset");
|
||||||
let archive = repository
|
let archive = repository
|
||||||
.open_archive(&key.archive)
|
.open_archive_unchanged(&key.archive)
|
||||||
.map_err(|err| map_resource_error(label, key, err))?;
|
.map_err(|err| map_resource_error(label, key, err))?;
|
||||||
let handle = repository
|
let handle = repository
|
||||||
.find(archive, &key.name)
|
.find(archive, &key.name)
|
||||||
@@ -1928,7 +1928,7 @@ fn resolve_wear_table<R: ResourceRepository>(
|
|||||||
mesh: &ResourceKey,
|
mesh: &ResourceKey,
|
||||||
) -> Result<fparkan_material::WearTable, AssetError> {
|
) -> Result<fparkan_material::WearTable, AssetError> {
|
||||||
let archive = repository
|
let archive = repository
|
||||||
.open_archive(&mesh.archive)
|
.open_archive_unchanged(&mesh.archive)
|
||||||
.map_err(|err| map_resource_error("wear", mesh, err))?;
|
.map_err(|err| map_resource_error("wear", mesh, err))?;
|
||||||
let wear_name = sibling_name(mesh, "wea")?;
|
let wear_name = sibling_name(mesh, "wea")?;
|
||||||
let handle = repository
|
let handle = repository
|
||||||
@@ -2271,7 +2271,7 @@ fn read_optional_key<R: ResourceRepository>(
|
|||||||
key: &ResourceKey,
|
key: &ResourceKey,
|
||||||
label: Option<&str>,
|
label: Option<&str>,
|
||||||
) -> Result<Option<Arc<[u8]>>, AssetError> {
|
) -> 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,
|
Ok(archive) => archive,
|
||||||
Err(ResourceError::MissingArchive { .. } | ResourceError::MissingEntry) => return Ok(None),
|
Err(ResourceError::MissingArchive { .. } | ResourceError::MissingEntry) => return Ok(None),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|||||||
@@ -258,6 +258,21 @@ pub trait ResourceRepository {
|
|||||||
/// Returns [`ResourceError`] when the archive is missing, unsupported, or
|
/// Returns [`ResourceError`] when the archive is missing, unsupported, or
|
||||||
/// malformed.
|
/// malformed.
|
||||||
fn open_archive(&self, path: &NormalizedPath) -> Result<ArchiveId, ResourceError>;
|
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.
|
/// Finds entry.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # 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(
|
fn find(
|
||||||
&self,
|
&self,
|
||||||
archive: ArchiveId,
|
archive: ArchiveId,
|
||||||
@@ -1101,6 +1129,23 @@ mod tests {
|
|||||||
assert_eq!(vfs.metadata_reads.load(Ordering::Relaxed), 1);
|
assert_eq!(vfs.metadata_reads.load(Ordering::Relaxed), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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]
|
#[test]
|
||||||
fn concurrent_same_archive_open_reuses_archive_id() {
|
fn concurrent_same_archive_open_reuses_archive_id() {
|
||||||
let path = archive_path(b"archives/test.lib").expect("path");
|
let path = archive_path(b"archives/test.lib").expect("path");
|
||||||
|
|||||||
+17
-5
@@ -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
|
with its own vtable. This capture format deliberately preserves the former
|
||||||
base-object interpretation and does not invent camera ownership.
|
base-object interpretation and does not invent camera ownership.
|
||||||
|
|
||||||
A bounded first launch of the capture-driven static preview against the
|
The first bounded launch showed that repeated fingerprinting, rather than
|
||||||
canonical GOG `MISSIONS/Autodemo.00/data.tma` did not reach a native frame
|
Vulkan initialization, was the load-path bottleneck: each new MAT0/TEXM request
|
||||||
within 60 seconds. It is therefore load-path evidence only, not a rendering or
|
re-opened an already decoded archive and re-hashed its entire source file.
|
||||||
pixel-parity result; the existing full dependency preparation remains the next
|
`ResourceRepository::open_archive_unchanged` now makes the *explicit* bounded
|
||||||
performance/runtime obstacle before this exact capture can be visually compared.
|
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
|
A fresh no-input launch of the canonical `iron_3d.exe` did create a responsive
|
||||||
window titled `Parkan. Железная Стратегия`. A read-only probe then requested
|
window titled `Parkan. Железная Стратегия`. A read-only probe then requested
|
||||||
|
|||||||
Reference in New Issue
Block a user