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:
Matthew Jackson
2026-07-29 20:27:50 -07:00
parent b4bf0daa82
commit 13897e14f0
3 changed files with 1003 additions and 114 deletions
+14
View File
@@ -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
View File
@@ -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());
}
+967 -106
View File
File diff suppressed because it is too large Load Diff