v0.10.8: prefetch all metadata file sectors — scan 2min to 18s on USB

This commit is contained in:
Matt Jackson
2026-04-17 19:51:32 +00:00
parent af13347cd9
commit a3de3d2ed9
4 changed files with 116 additions and 19 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "libfreemkv" name = "libfreemkv"
version = "0.10.7" version = "0.10.9"
edition = "2021" edition = "2021"
rust-version = "1.86" rust-version = "1.86"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
+77 -14
View File
@@ -917,12 +917,77 @@ impl ScanOptions {
} }
} }
/// Quick disc identification — name, format, capacity. No title/stream parsing.
#[derive(Debug)]
pub struct DiscId {
/// UDF Volume Identifier (always present, e.g. "V_FOR_VENDETTA")
pub volume_id: String,
/// Disc title from META/DL/bdmt_eng.xml (e.g. "V for Vendetta")
pub meta_title: Option<String>,
/// Disc format (BD, UHD, DVD) — UHD vs BD requires full scan to confirm
pub format: DiscFormat,
/// Disc capacity in sectors
pub capacity_sectors: u32,
/// Whether AACS directory exists (disc is likely encrypted)
pub encrypted: bool,
/// Number of layers
pub layers: u8,
}
impl DiscId {
/// Best available name: meta_title, then formatted volume_id.
pub fn name(&self) -> &str {
self.meta_title
.as_deref()
.unwrap_or(&self.volume_id)
}
}
impl Disc { impl Disc {
/// Fast disc identification — reads only UDF metadata for name and format.
/// No AACS handshake, no playlist parsing, no CLPI, no labels.
/// Typically completes in 2-3 seconds on USB drives.
pub fn identify(session: &mut Drive) -> Result<DiscId> {
let (capacity, mut buffered, udf_fs) = Self::read_udf(session)?;
let meta_title = Self::read_meta_title(&mut buffered, &udf_fs);
let format = if udf_fs.find_dir("/BDMV").is_some() {
DiscFormat::BluRay // full scan distinguishes UHD vs BD
} else if udf_fs.find_dir("/VIDEO_TS").is_some() {
DiscFormat::Dvd
} else {
DiscFormat::Unknown
};
let encrypted =
udf_fs.find_dir("/AACS").is_some() || udf_fs.find_dir("/BDMV/AACS").is_some();
let layers = if capacity > 24_000_000 { 2 } else { 1 };
Ok(DiscId {
volume_id: udf_fs.volume_id,
meta_title,
format,
capacity_sectors: capacity,
encrypted,
layers,
})
}
/// Disc capacity in GB /// Disc capacity in GB
pub fn capacity_gb(&self) -> f64 { pub fn capacity_gb(&self) -> f64 {
self.capacity_sectors as f64 * 2048.0 / (1024.0 * 1024.0 * 1024.0) self.capacity_sectors as f64 * 2048.0 / (1024.0 * 1024.0 * 1024.0)
} }
/// Read UDF filesystem and set up buffered reader with metadata prefetched.
/// Shared setup for both identify() and scan().
fn read_udf(session: &mut Drive) -> Result<(u32, udf::BufferedSectorReader<'_>, udf::UdfFs)> {
let capacity = Self::read_capacity(session).unwrap_or(0);
let batch = detect_max_batch_sectors(session.device_path());
let mut buffered = udf::BufferedSectorReader::new(session, batch);
let udf_fs = udf::read_filesystem(&mut buffered)?;
buffered.prefetch(udf_fs.metadata_start(), udf_fs.metadata_sectors());
Ok((capacity, buffered, udf_fs))
}
/// Scan a disc -- parse filesystem, playlists, streams, and set up AACS decryption. /// Scan a disc -- parse filesystem, playlists, streams, and set up AACS decryption.
/// ///
/// This is the main entry point. After scan(), the Disc is ready: /// This is the main entry point. After scan(), the Disc is ready:
@@ -931,18 +996,14 @@ impl Disc {
/// - content can be read and decrypted transparently /// - content can be read and decrypted transparently
/// ///
/// Scan a disc. One pipeline, one order: /// Scan a disc. One pipeline, one order:
/// 1. Read capacity /// 1. Read capacity + UDF filesystem
/// 2. Read UDF filesystem /// 2. AACS handshake + key resolution
/// 3. Resolve AACS keys (all via UDF, no SCSI commands) /// 3. Parse playlists + streams
/// 4. Parse playlists + streams /// 4. Apply labels
/// 5. Apply labels
/// ///
/// The session must be open and unlocked (Drive::open handles this). /// The session must be open and unlocked (Drive::open handles this).
/// All disc reads use standard READ(10) via UDF -- no vendor SCSI commands. /// All disc reads use standard READ(10) via UDF -- no vendor SCSI commands.
pub fn scan(session: &mut Drive, opts: &ScanOptions) -> Result<Self> { pub fn scan(session: &mut Drive, opts: &ScanOptions) -> Result<Self> {
// READ CAPACITY may fail in LibreDrive mode — proceed with 0 and estimate later
let capacity = Self::read_capacity(session).unwrap_or(0);
// AACS handshake (Blu-ray/UHD) // AACS handshake (Blu-ray/UHD)
let handshake = Self::do_handshake(session, opts); let handshake = Self::do_handshake(session, opts);
@@ -950,12 +1011,14 @@ impl Disc {
// (BD/UHD speed is set by firmware init, but DVD needs explicit SET CD SPEED) // (BD/UHD speed is set by firmware init, but DVD needs explicit SET CD SPEED)
session.set_speed(0xFFFF); session.set_speed(0xFFFF);
// Buffer sector reads — USB drives have ~500ms per SCSI command, // Read UDF filesystem with buffered sector reader
// so prefetching reduces hundreds of commands to dozens. let (capacity, mut buffered, udf_fs) = Self::read_udf(session)?;
let batch = detect_max_batch_sectors(session.device_path());
let mut buffered = udf::BufferedSectorReader::new(session, batch); // Pre-read all small file sectors (AACS, MPLS, CLPI, META, *.bdmv).
let udf_fs = udf::read_filesystem(&mut buffered)?; // Without this, each read_file() triggers individual SCSI commands at 500ms each.
buffered.prefetch(udf_fs.metadata_start(), udf_fs.metadata_sectors()); if let Ok(ranges) = udf_fs.metadata_sector_ranges(&mut buffered) {
buffered.prefetch_ranges(&ranges);
}
let mut disc = Self::scan_with(&mut buffered, capacity, handshake, opts, udf_fs)?; let mut disc = Self::scan_with(&mut buffered, capacity, handshake, opts, udf_fs)?;
+1 -1
View File
@@ -105,7 +105,7 @@ pub use profile::DriveProfile;
pub use decrypt::{decrypt_sectors, DecryptKeys}; pub use decrypt::{decrypt_sectors, DecryptKeys};
pub use disc::{ pub use disc::{
AacsState, AudioChannels, AudioStream, Clip, Codec, ColorSpace, ContentFormat, Disc, AacsState, AudioChannels, AudioStream, Clip, Codec, ColorSpace, ContentFormat, Disc,
DiscFormat, DiscTitle, Extent, FrameRate, HdrFormat, KeySource, Resolution, SampleRate, DiscFormat, DiscId, DiscTitle, Extent, FrameRate, HdrFormat, KeySource, Resolution, SampleRate,
ScanOptions, Stream, SubtitleStream, VideoStream, ScanOptions, Stream, SubtitleStream, VideoStream,
}; };
pub use mux::DiscStream; pub use mux::DiscStream;
+37 -3
View File
@@ -846,6 +846,8 @@ pub(crate) struct BufferedSectorReader<'a> {
cache: Vec<u8>, cache: Vec<u8>,
cache_sectors: u32, cache_sectors: u32,
batch: u16, batch: u16,
/// Pre-fetched sector data from bulk reads (sector ranges for AACS, MPLS, CLPI, etc.)
prefetched: std::collections::HashMap<u32, Vec<u8>>,
} }
impl<'a> BufferedSectorReader<'a> { impl<'a> BufferedSectorReader<'a> {
@@ -856,15 +858,15 @@ impl<'a> BufferedSectorReader<'a> {
cache: Vec::new(), cache: Vec::new(),
cache_sectors: 0, cache_sectors: 0,
batch, batch,
prefetched: std::collections::HashMap::new(),
} }
} }
} }
impl BufferedSectorReader<'_> { impl BufferedSectorReader<'_> {
/// Pre-read a range of sectors into the cache. /// Pre-read a contiguous range of sectors into the sliding cache.
/// Used to bulk-load the UDF metadata partition so subsequent reads are instant. /// Used to bulk-load the UDF metadata partition so subsequent reads are instant.
pub(crate) fn prefetch(&mut self, start_lba: u32, count: u32) { pub(crate) fn prefetch(&mut self, start_lba: u32, count: u32) {
// Read in chunks of 30 sectors (within USB SCSI limits)
let total = count as usize * 2048; let total = count as usize * 2048;
self.cache.resize(total, 0); self.cache.resize(total, 0);
let mut offset = 0u32; let mut offset = 0u32;
@@ -887,6 +889,33 @@ impl BufferedSectorReader<'_> {
self.cache_start = start_lba; self.cache_start = start_lba;
self.cache_sectors = offset; self.cache_sectors = offset;
} }
/// Pre-read multiple sector ranges into the permanent cache.
/// Each range is read in batch-sized chunks and stored per-sector in a HashMap.
/// Used to bulk-load all small files (AACS, MPLS, CLPI, META) before scanning.
pub(crate) fn prefetch_ranges(&mut self, ranges: &[(u32, u32)]) {
let mut tmp = vec![0u8; self.batch as usize * 2048];
for &(start, count) in ranges {
let mut offset = 0u32;
while offset < count {
let batch = (count - offset).min(self.batch as u32) as u16;
let bytes = batch as usize * 2048;
if self
.inner
.read_sectors(start + offset, batch, &mut tmp[..bytes])
.is_err()
{
break;
}
for i in 0..batch as u32 {
let s = i as usize * 2048;
self.prefetched
.insert(start + offset + i, tmp[s..s + 2048].to_vec());
}
offset += batch as u32;
}
}
}
} }
impl SectorReader for BufferedSectorReader<'_> { impl SectorReader for BufferedSectorReader<'_> {
@@ -897,7 +926,12 @@ impl SectorReader for BufferedSectorReader<'_> {
buf: &mut [u8], buf: &mut [u8],
) -> std::result::Result<usize, crate::error::Error> { ) -> std::result::Result<usize, crate::error::Error> {
if count == 1 { if count == 1 {
// Single sector — use cache // Check permanent prefetch cache first (HashMap)
if let Some(data) = self.prefetched.get(&lba) {
buf[..2048].copy_from_slice(data);
return Ok(2048);
}
// Check sliding cache
if lba >= self.cache_start && lba < self.cache_start + self.cache_sectors { if lba >= self.cache_start && lba < self.cache_start + self.cache_sectors {
let offset = (lba - self.cache_start) as usize * 2048; let offset = (lba - self.cache_start) as usize * 2048;
buf[..2048].copy_from_slice(&self.cache[offset..offset + 2048]); buf[..2048].copy_from_slice(&self.cache[offset..offset + 2048]);