Resolve the forensic key map once per disc, not once per playlist
resolve_content_key_map loops every title into resolve_mux_key_map, which called resolve_fmts_key_map FIRST — before the CpsUnitCache — and on an FMTS disc returned immediately. So every playlist re-derived facts that belong to the DISC: a full UDF walk plus /AACS/IndividualSegment.tbl, and on an FMTS disc the anchor probe, the 32-index phase probe, and a fetch.fmts_indexes round trip. On a 60-playlist disc, measured on a synthetic fixture: 840 -> 14 metadata reads, 2,400 -> 40 probe reads, and 60 -> 1 key-service calls. Worst case before was up to 256 probe reads and 32 key-service calls per title. The 60 redundant key-service round trips are a strong candidate for the keyserver storm seen in the field. Two memos behind a pub(crate) DiscKeyCache. The table memo (UDF walk + tbl parse) is disc-invariant outright — nothing in that path mentions the title — and runs on EVERY disc, so a plain BD benefits too. Only the deterministic negatives are memoised as "not FMTS"; a DiscRead fault propagates uncached so a later title retries. A blind once-per-disc hoist of the PROBES was rejected as unsafe, and this is the load-bearing reasoning: the title enters through clip_byte_to_lba, which decides which segments are addressable and which LBA every probed clip byte reads from, so two titles with different extent lists probe different physical bytes. A hoist would serve title B an answer derived from title A's media and could silently turn a per-title FmtsKeyMissing into a success. The extent list is the ONLY per-title input, so keying on it is exactly sufficient — matching the ForcedProbeCache precedent. Result-identity was proved, not assumed: NEITHER probe reads the key pool. Verified here independently — probe_fmts_index_keys takes no keys parameter at all; index keys come from `fetch`, and the anchor's reply feeds the phase probe. So the pool's growth across titles, the one thing that does change between calls, cannot move a memoised value, and the result is order-independent. A test resolves three titles through a shared memo and through fresh memos and asserts both the per-title ranges and the final key pool (keys, slots, order) are identical. Not memoised, deliberately: fail-loud FmtsKeyMissing, and any run where an index hit a read fault — that is a property of a transient drive fault, not of the extents, and caching it would spread one bad read across 59 playlists. A fully-memoised title now does zero I/O, which made the old in-loop halt polls unreachable for it, so a check_halt on entry was added with a test that cancels after warming the memos. Also corrects my own overstatement from last round: the CpsUnitCache doc now says plainly that on an FMTS disc it removes NO reads, because this function returns before the extent loop ever runs. Five mutations, all verified red. Pre-existing bug flagged but not fixed: filter_addressable_segments only checks that a segment's START byte maps to some LBA in the title, so a play-all playlist can pass the filter while mapping segment bytes into the wrong clip, whose anchor then returns empty and aborts the sweep.
This commit is contained in:
@@ -41,6 +41,20 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The FMTS (AACS 2.1) forensic key resolution now runs once per disc, not once
|
||||
per title.** `Disc::resolve_content_key_map` resolves every title, and the FMTS
|
||||
branch ran ahead of everything else — so each playlist re-walked the UDF
|
||||
filesystem to re-read `/AACS/IndividualSegment.tbl`, re-ran the forensic anchor
|
||||
probe and the per-index phase probe, and **re-asked the key service for the
|
||||
disc's index-key set**. On a 60-playlist AACS 2.1 disc that was 60 identical
|
||||
key-service round trips (a key-server storm) and tens of thousands of random
|
||||
6144-byte reads for one disc-wide answer. The UDF walk is now memoised for the
|
||||
whole disc and the index keys + phases per distinct extent list — the only
|
||||
per-title input to the probes. A read-faulted phase probe is deliberately never
|
||||
memoised, so one bad read is not spread across the remaining playlists. The
|
||||
per-title UDF walk was paid on **every** disc, FMTS or not, so a plain BD sweep
|
||||
loses ~59 full-stroke seeks too. The multi-CPS extent memo added in 1.6.0 is
|
||||
also reachable on an FMTS disc for the first time.
|
||||
- **Mux correctness pass** (the `v1.4.0..HEAD` 10-phase audit): DTS core-header
|
||||
false-drops that dropped good DTS frames; the TrueHD channel-correction probe
|
||||
now runs correctly on AACS discs (7.1/Atmos no longer understated as 5.1);
|
||||
|
||||
+22
-8
@@ -2396,13 +2396,27 @@ 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();
|
||||
// ONE per-disc memo across every title. Without it every playlist re-derives
|
||||
// the same disc-wide facts off the drive:
|
||||
//
|
||||
// * the multi-CPS "which held key opens this extent" decision — 8 random
|
||||
// 6144-byte reads per extent, ~200 ms of seek apiece on a stock BD drive.
|
||||
// A disc's playlists overwhelmingly reference the same handful of clips
|
||||
// (main feature, play-all, per-chapter and seamless-branch variants), so
|
||||
// this recomputed the same index from byte-identical input;
|
||||
// * the UDF walk + `/AACS/IndividualSegment.tbl` read that decides whether
|
||||
// the disc is FMTS at all — ~35 single-sector reads at low LBAs, reached
|
||||
// from a head the previous title's content sampling left deep in the
|
||||
// content area, so a full-stroke seek out and back per playlist. This
|
||||
// runs on EVERY disc, FMTS or not;
|
||||
// * on an FMTS (AACS 2.1) disc, the forensic anchor probe, the per-index
|
||||
// phase probe AND the key-service round trip that returns the disc's
|
||||
// index-key set. That last one is the key-server storm: one round trip
|
||||
// per playlist for one disc-wide answer.
|
||||
//
|
||||
// The FMTS memos are what makes the multi-CPS memo reachable at all on an
|
||||
// FMTS disc — that path returns its finished map before the extent loop.
|
||||
let mut cache = crate::mux::resolve::DiscKeyCache::new();
|
||||
for title in &self.titles {
|
||||
let map = crate::mux::resolve::resolve_mux_key_map_cached(
|
||||
reader,
|
||||
@@ -2411,7 +2425,7 @@ impl Disc {
|
||||
fetch,
|
||||
self.content_format,
|
||||
halt,
|
||||
&mut cps_cache,
|
||||
&mut cache,
|
||||
)?;
|
||||
ranges.extend_from_slice(map.ranges());
|
||||
}
|
||||
|
||||
+962
-101
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user