v0.10.8: prefetch all metadata file sectors — scan 2min to 18s on USB
This commit is contained in:
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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]);
|
||||||
|
|||||||
Reference in New Issue
Block a user