From 9791e60c6523c58a6254a833c6782c0031aec6f4 Mon Sep 17 00:00:00 2001 From: Matt Jackson <1085847+MattJackson@users.noreply.github.com> Date: Fri, 17 Apr 2026 19:42:35 +0000 Subject: [PATCH] =?UTF-8?q?v0.10.8:=20prefetch=20all=20metadata=20file=20s?= =?UTF-8?q?ectors=20=E2=80=94=20scan=202min=20to=2018s=20on=20USB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 2 +- src/disc/mod.rs | 91 +++++++++++++++++++++++++++++++++++++++++-------- src/lib.rs | 2 +- src/udf.rs | 40 ++++++++++++++++++++-- 4 files changed, 116 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4d3bfe9..6565f30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libfreemkv" -version = "0.10.7" +version = "0.10.9" edition = "2021" rust-version = "1.86" license = "AGPL-3.0-only" diff --git a/src/disc/mod.rs b/src/disc/mod.rs index c78ee1c..4cc6b4b 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -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, + /// 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 { + /// 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 { + 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 pub fn capacity_gb(&self) -> f64 { 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. /// /// 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 /// /// Scan a disc. One pipeline, one order: - /// 1. Read capacity - /// 2. Read UDF filesystem - /// 3. Resolve AACS keys (all via UDF, no SCSI commands) - /// 4. Parse playlists + streams - /// 5. Apply labels + /// 1. Read capacity + UDF filesystem + /// 2. AACS handshake + key resolution + /// 3. Parse playlists + streams + /// 4. Apply labels /// /// The session must be open and unlocked (Drive::open handles this). /// All disc reads use standard READ(10) via UDF -- no vendor SCSI commands. pub fn scan(session: &mut Drive, opts: &ScanOptions) -> Result { - // 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) 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) session.set_speed(0xFFFF); - // Buffer sector reads — USB drives have ~500ms per SCSI command, - // so prefetching reduces hundreds of commands to dozens. - 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()); + // Read UDF filesystem with buffered sector reader + let (capacity, mut buffered, udf_fs) = Self::read_udf(session)?; + + // Pre-read all small file sectors (AACS, MPLS, CLPI, META, *.bdmv). + // Without this, each read_file() triggers individual SCSI commands at 500ms each. + 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)?; diff --git a/src/lib.rs b/src/lib.rs index 5ec8445..d5d4b7c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -105,7 +105,7 @@ pub use profile::DriveProfile; pub use decrypt::{decrypt_sectors, DecryptKeys}; pub use 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, }; pub use mux::DiscStream; diff --git a/src/udf.rs b/src/udf.rs index b9b074e..a263070 100644 --- a/src/udf.rs +++ b/src/udf.rs @@ -846,6 +846,8 @@ pub(crate) struct BufferedSectorReader<'a> { cache: Vec, cache_sectors: u32, batch: u16, + /// Pre-fetched sector data from bulk reads (sector ranges for AACS, MPLS, CLPI, etc.) + prefetched: std::collections::HashMap>, } impl<'a> BufferedSectorReader<'a> { @@ -856,15 +858,15 @@ impl<'a> BufferedSectorReader<'a> { cache: Vec::new(), cache_sectors: 0, batch, + prefetched: std::collections::HashMap::new(), } } } 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. 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; self.cache.resize(total, 0); let mut offset = 0u32; @@ -887,6 +889,33 @@ impl BufferedSectorReader<'_> { self.cache_start = start_lba; 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<'_> { @@ -897,7 +926,12 @@ impl SectorReader for BufferedSectorReader<'_> { buf: &mut [u8], ) -> std::result::Result { 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 { let offset = (lba - self.cache_start) as usize * 2048; buf[..2048].copy_from_slice(&self.cache[offset..offset + 2048]);