Memoise multi-CPS key-map sampling per extent, not per title
resolve_content_key_map calls resolve_mux_key_map once per title, and on a
multi-CPS disc that path issues 8 random single-unit reads per extent. A disc's
playlists overwhelmingly reference the same few clips — main feature, play-all,
per-chapter and seamless-branch variants — so the same physical extents were
re-sampled from the drive once per playlist. On a 60-playlist / 15-clip disc
that is ~2,400 non-sequential 6144-byte reads, roughly 8 minutes of pure seeking
at 200 ms per seek, before the mux starts. Now ~600 reads.
Keyed per EXTENT — (format, start_lba, sector_count) — rather than per title's
whole extent list, which is finer-grained than the forced-subtitle probe's cache
and strictly better here: a play-all playlist sharing 4 of 5 extents with the
main feature still hits on those 4.
Why a cached pool index is provably identical to a recomputed one, verified
rather than assumed:
* `pick` iterates the pool IN ORDER and returns the FIRST index whose key
decrypts a sample to clean.
* The pool is APPEND-ONLY. Checked across the whole crate: only `push`, with no
insert/remove/clear/retain/sort/dedup/truncate/drain/swap/reverse anywhere.
So appended keys can only land AFTER a matched index, and the first match for
the same samples cannot shift.
* The samples are a pure function of the three values in the key, read from
read-only optical media.
Two outcomes are deliberately NOT cached, which is what makes this safe rather
than merely faster:
* the inherited index (`None if samples.is_empty() => last_idx`) is per-TITLE
state, not a property of the extent — caching it would let one title's
carry-in index leak into another title's clear extent, i.e. a WRONG key;
* the fail-loud DecryptFailed verdict, so a retry after a key source banks the
missing key re-samples instead of inheriting a stale answer.
Halt is still polled before the cache lookup, so cancellation is unchanged.
resolve_mux_key_map keeps its exact signature and delegates with a fresh cache,
so there is no public API change. ContentFormat gains Eq + Hash (additive).
Four tests, and the two mutants that matter both verified red: disabling the
cache short-circuit fails the hit and recompute-equivalence tests, and wrongly
caching the inherited index fails multi_cps_inherited_index_is_not_cached.
This commit is contained in:
+10
-2
@@ -76,7 +76,7 @@ pub struct Disc {
|
||||
}
|
||||
|
||||
/// Content format — determines how sectors are interpreted downstream.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum ContentFormat {
|
||||
/// Blu-ray BD Transport Stream (192-byte packets)
|
||||
BdTs,
|
||||
@@ -2396,14 +2396,22 @@ impl Disc {
|
||||
halt: Option<&crate::halt::Halt>,
|
||||
) -> Result<crate::decrypt::AacsKeyMap> {
|
||||
let mut ranges: Vec<(u32, u32, usize, crate::decrypt::Phase)> = Vec::new();
|
||||
// One multi-CPS extent cache across every title. A disc's playlists
|
||||
// overwhelmingly reference the same handful of clips (main feature,
|
||||
// play-all, per-chapter and seamless-branch variants), so without this the
|
||||
// same extents are re-sampled off the drive once per playlist — 8 random
|
||||
// 6144-byte reads each, ~200 ms of seek apiece on a stock BD drive, all to
|
||||
// recompute the same index from byte-identical input.
|
||||
let mut cps_cache = crate::mux::resolve::CpsUnitCache::new();
|
||||
for title in &self.titles {
|
||||
let map = crate::mux::resolve_mux_key_map(
|
||||
let map = crate::mux::resolve::resolve_mux_key_map_cached(
|
||||
reader,
|
||||
title,
|
||||
keys,
|
||||
fetch,
|
||||
self.content_format,
|
||||
halt,
|
||||
&mut cps_cache,
|
||||
)?;
|
||||
ranges.extend_from_slice(map.ranges());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user