diff --git a/src/disc/bluray.rs b/src/disc/bluray.rs index a0edc02..b2eeebe 100644 --- a/src/disc/bluray.rs +++ b/src/disc/bluray.rs @@ -12,8 +12,19 @@ impl Disc { reader: &mut dyn SectorReader, udf_fs: &udf::UdfFs, ) -> Vec { + let t0 = std::time::Instant::now(); let mut titles = Vec::new(); if let Some(playlist_dir) = udf_fs.find_dir("/BDMV/PLAYLIST") { + let mpls_count = playlist_dir + .entries + .iter() + .filter(|e| !e.is_dir && e.name.to_lowercase().ends_with(".mpls")) + .count(); + eprintln!( + "[titles {:.1}s] {} MPLS files", + t0.elapsed().as_secs_f64(), + mpls_count + ); for entry in &playlist_dir.entries { if !entry.is_dir && entry.name.to_lowercase().ends_with(".mpls") { let path = format!("/BDMV/PLAYLIST/{}", entry.name); @@ -21,12 +32,23 @@ impl Disc { if let Some(title) = Self::parse_playlist(reader, udf_fs, &entry.name, &mpls_data) { + eprintln!( + "[titles {:.1}s] {} -> title ({:.0}s)", + t0.elapsed().as_secs_f64(), + entry.name, + title.duration_secs + ); titles.push(title); } } } } } + eprintln!( + "[titles {:.1}s] done: {} titles", + t0.elapsed().as_secs_f64(), + titles.len() + ); titles } diff --git a/src/disc/mod.rs b/src/disc/mod.rs index c29e1e2..01ebe42 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -940,17 +940,37 @@ impl Disc { /// 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 { + let t0 = std::time::Instant::now(); // READ CAPACITY may fail in LibreDrive mode — proceed with 0 and estimate later let capacity = Self::read_capacity(session).unwrap_or(0); + eprintln!( + "[scan {:.1}s] capacity={}", + t0.elapsed().as_secs_f64(), + capacity + ); // AACS handshake (Blu-ray/UHD) let handshake = Self::do_handshake(session, opts); + eprintln!("[scan {:.1}s] handshake done", t0.elapsed().as_secs_f64()); // Request max read speed — removes riplock on DVD // (BD/UHD speed is set by firmware init, but DVD needs explicit SET CD SPEED) session.set_speed(0xFFFF); + eprintln!("[scan {:.1}s] speed set", t0.elapsed().as_secs_f64()); - let mut disc = Self::scan_with(session, capacity, handshake, opts)?; + 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)?; + eprintln!( + "[scan {:.1}s] UDF parsed, prefetching metadata ({} sectors)...", + t0.elapsed().as_secs_f64(), + udf_fs.metadata_sectors() + ); + buffered.prefetch(udf_fs.metadata_start(), udf_fs.metadata_sectors()); + eprintln!("[scan {:.1}s] metadata cached", t0.elapsed().as_secs_f64()); + + let mut disc = Self::scan_with(&mut buffered, capacity, handshake, opts, udf_fs)?; + eprintln!("[scan {:.1}s] scan_with done", t0.elapsed().as_secs_f64()); // CSS key extraction for DVDs (bus auth → disc key → title key). // Must be a single auth session — can't call authenticate() separately. @@ -988,7 +1008,8 @@ impl Disc { capacity: u32, opts: &ScanOptions, ) -> Result { - Self::scan_with(reader, capacity, None, opts) + let udf_fs = udf::read_filesystem(reader)?; + Self::scan_with(reader, capacity, None, opts, udf_fs) } /// Core scan pipeline — works with any SectorReader. @@ -997,15 +1018,19 @@ impl Disc { capacity: u32, handshake: Option, opts: &ScanOptions, + udf_fs: udf::UdfFs, ) -> Result { - // 1. UDF filesystem - let udf_fs = udf::read_filesystem(reader)?; + let t0 = std::time::Instant::now(); // 2. Resolve encryption (AACS, CSS, or none) let encrypted = udf_fs.find_dir("/AACS").is_some() || udf_fs.find_dir("/BDMV/AACS").is_some(); let aacs = if encrypted { + eprintln!( + "[scan_with {:.1}s] resolving encryption...", + t0.elapsed().as_secs_f64() + ); if let Some(keydb_path) = opts.resolve_keydb() { Self::resolve_encryption(&udf_fs, reader, &keydb_path, handshake.as_ref()).ok() } else { @@ -1014,6 +1039,10 @@ impl Disc { } else { None }; + eprintln!( + "[scan_with {:.1}s] encryption done", + t0.elapsed().as_secs_f64() + ); // 3. Titles — BD (MPLS playlists) or DVD (IFO title sets) let (mut titles, content_format) = if udf_fs.find_dir("/BDMV").is_some() { @@ -1029,6 +1058,11 @@ impl Disc { } else { (Vec::new(), ContentFormat::BdTs) }; + eprintln!( + "[scan_with {:.1}s] titles done: {}", + t0.elapsed().as_secs_f64(), + titles.len() + ); titles.sort_by(|a, b| { b.duration_secs .partial_cmp(&a.duration_secs) @@ -1037,7 +1071,12 @@ impl Disc { // 4. Metadata + labels let meta_title = Self::read_meta_title(reader, &udf_fs); + eprintln!( + "[scan_with {:.1}s] meta_title done", + t0.elapsed().as_secs_f64() + ); crate::labels::apply(reader, &udf_fs, &mut titles); + eprintln!("[scan_with {:.1}s] labels done", t0.elapsed().as_secs_f64()); // 5. Derive format, layers, region let format = Self::detect_format(&titles); diff --git a/src/udf.rs b/src/udf.rs index d756813..b9b074e 100644 --- a/src/udf.rs +++ b/src/udf.rs @@ -61,6 +61,11 @@ impl UdfFs { self.metadata_start } + /// Metadata partition size in sectors. + pub(crate) fn metadata_sectors(&self) -> u32 { + self.metadata_sectors + } + /// Find a directory by path (e.g. "/BDMV/PLAYLIST"). /// Path matching is case-insensitive. pub fn find_dir(&self, path: &str) -> Option<&DirEntry> { @@ -832,6 +837,96 @@ fn parse_dstring(data: &[u8]) -> String { /// Read a single 2048-byte sector from the drive. /// Uses standard READ(10) — no unlock required. +/// Buffered sector reader — reduces SCSI round-trips by pre-fetching blocks. +/// Each SCSI command has ~500ms overhead on USB drives, so reading 32 sectors +/// at once (one command) is 32x faster than 32 individual reads. +pub(crate) struct BufferedSectorReader<'a> { + inner: &'a mut dyn SectorReader, + cache_start: u32, + cache: Vec, + cache_sectors: u32, + batch: u16, +} + +impl<'a> BufferedSectorReader<'a> { + pub(crate) fn new(inner: &'a mut dyn SectorReader, batch: u16) -> Self { + Self { + inner, + cache_start: u32::MAX, + cache: Vec::new(), + cache_sectors: 0, + batch, + } + } +} + +impl BufferedSectorReader<'_> { + /// Pre-read a range of sectors into the 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; + while offset < count { + let batch = (count - offset).min(self.batch as u32) as u16; + let buf_off = offset as usize * 2048; + if self + .inner + .read_sectors( + start_lba + offset, + batch, + &mut self.cache[buf_off..buf_off + batch as usize * 2048], + ) + .is_err() + { + break; + } + offset += batch as u32; + } + self.cache_start = start_lba; + self.cache_sectors = offset; + } +} + +impl SectorReader for BufferedSectorReader<'_> { + fn read_sectors( + &mut self, + lba: u32, + count: u16, + buf: &mut [u8], + ) -> std::result::Result { + if count == 1 { + // Single sector — use 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]); + return Ok(2048); + } + let block = self.batch; + self.cache.resize(block as usize * 2048, 0); + match self.inner.read_sectors(lba, block, &mut self.cache) { + Ok(_) => { + self.cache_start = lba; + self.cache_sectors = block as u32; + } + Err(_) => { + // Near end of disc or error — single sector fallback + self.cache.resize(2048, 0); + self.inner.read_sectors(lba, 1, &mut self.cache)?; + self.cache_start = lba; + self.cache_sectors = 1; + } + } + buf[..2048].copy_from_slice(&self.cache[..2048]); + Ok(2048) + } else { + // Multi-sector read — pass through + self.inner.read_sectors(lba, count, buf) + } + } +} + fn read_sector(reader: &mut dyn SectorReader, lba: u32, buf: &mut [u8]) -> Result<()> { reader.read_sectors(lba, 1, buf)?; Ok(())