Buffer UDF sector reads — eliminates scan hang on USB drives
This commit is contained in:
@@ -12,8 +12,19 @@ impl Disc {
|
|||||||
reader: &mut dyn SectorReader,
|
reader: &mut dyn SectorReader,
|
||||||
udf_fs: &udf::UdfFs,
|
udf_fs: &udf::UdfFs,
|
||||||
) -> Vec<DiscTitle> {
|
) -> Vec<DiscTitle> {
|
||||||
|
let t0 = std::time::Instant::now();
|
||||||
let mut titles = Vec::new();
|
let mut titles = Vec::new();
|
||||||
if let Some(playlist_dir) = udf_fs.find_dir("/BDMV/PLAYLIST") {
|
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 {
|
for entry in &playlist_dir.entries {
|
||||||
if !entry.is_dir && entry.name.to_lowercase().ends_with(".mpls") {
|
if !entry.is_dir && entry.name.to_lowercase().ends_with(".mpls") {
|
||||||
let path = format!("/BDMV/PLAYLIST/{}", entry.name);
|
let path = format!("/BDMV/PLAYLIST/{}", entry.name);
|
||||||
@@ -21,12 +32,23 @@ impl Disc {
|
|||||||
if let Some(title) =
|
if let Some(title) =
|
||||||
Self::parse_playlist(reader, udf_fs, &entry.name, &mpls_data)
|
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);
|
titles.push(title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
eprintln!(
|
||||||
|
"[titles {:.1}s] done: {} titles",
|
||||||
|
t0.elapsed().as_secs_f64(),
|
||||||
|
titles.len()
|
||||||
|
);
|
||||||
titles
|
titles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+43
-4
@@ -940,17 +940,37 @@ impl Disc {
|
|||||||
/// 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> {
|
||||||
|
let t0 = std::time::Instant::now();
|
||||||
// READ CAPACITY may fail in LibreDrive mode — proceed with 0 and estimate later
|
// READ CAPACITY may fail in LibreDrive mode — proceed with 0 and estimate later
|
||||||
let capacity = Self::read_capacity(session).unwrap_or(0);
|
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)
|
// AACS handshake (Blu-ray/UHD)
|
||||||
let handshake = Self::do_handshake(session, opts);
|
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
|
// Request max read speed — removes riplock on DVD
|
||||||
// (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);
|
||||||
|
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).
|
// CSS key extraction for DVDs (bus auth → disc key → title key).
|
||||||
// Must be a single auth session — can't call authenticate() separately.
|
// Must be a single auth session — can't call authenticate() separately.
|
||||||
@@ -988,7 +1008,8 @@ impl Disc {
|
|||||||
capacity: u32,
|
capacity: u32,
|
||||||
opts: &ScanOptions,
|
opts: &ScanOptions,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
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.
|
/// Core scan pipeline — works with any SectorReader.
|
||||||
@@ -997,15 +1018,19 @@ impl Disc {
|
|||||||
capacity: u32,
|
capacity: u32,
|
||||||
handshake: Option<HandshakeResult>,
|
handshake: Option<HandshakeResult>,
|
||||||
opts: &ScanOptions,
|
opts: &ScanOptions,
|
||||||
|
udf_fs: udf::UdfFs,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
// 1. UDF filesystem
|
let t0 = std::time::Instant::now();
|
||||||
let udf_fs = udf::read_filesystem(reader)?;
|
|
||||||
|
|
||||||
// 2. Resolve encryption (AACS, CSS, or none)
|
// 2. Resolve encryption (AACS, CSS, or none)
|
||||||
let encrypted =
|
let encrypted =
|
||||||
udf_fs.find_dir("/AACS").is_some() || udf_fs.find_dir("/BDMV/AACS").is_some();
|
udf_fs.find_dir("/AACS").is_some() || udf_fs.find_dir("/BDMV/AACS").is_some();
|
||||||
|
|
||||||
let aacs = if encrypted {
|
let aacs = if encrypted {
|
||||||
|
eprintln!(
|
||||||
|
"[scan_with {:.1}s] resolving encryption...",
|
||||||
|
t0.elapsed().as_secs_f64()
|
||||||
|
);
|
||||||
if let Some(keydb_path) = opts.resolve_keydb() {
|
if let Some(keydb_path) = opts.resolve_keydb() {
|
||||||
Self::resolve_encryption(&udf_fs, reader, &keydb_path, handshake.as_ref()).ok()
|
Self::resolve_encryption(&udf_fs, reader, &keydb_path, handshake.as_ref()).ok()
|
||||||
} else {
|
} else {
|
||||||
@@ -1014,6 +1039,10 @@ impl Disc {
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
eprintln!(
|
||||||
|
"[scan_with {:.1}s] encryption done",
|
||||||
|
t0.elapsed().as_secs_f64()
|
||||||
|
);
|
||||||
|
|
||||||
// 3. Titles — BD (MPLS playlists) or DVD (IFO title sets)
|
// 3. Titles — BD (MPLS playlists) or DVD (IFO title sets)
|
||||||
let (mut titles, content_format) = if udf_fs.find_dir("/BDMV").is_some() {
|
let (mut titles, content_format) = if udf_fs.find_dir("/BDMV").is_some() {
|
||||||
@@ -1029,6 +1058,11 @@ impl Disc {
|
|||||||
} else {
|
} else {
|
||||||
(Vec::new(), ContentFormat::BdTs)
|
(Vec::new(), ContentFormat::BdTs)
|
||||||
};
|
};
|
||||||
|
eprintln!(
|
||||||
|
"[scan_with {:.1}s] titles done: {}",
|
||||||
|
t0.elapsed().as_secs_f64(),
|
||||||
|
titles.len()
|
||||||
|
);
|
||||||
titles.sort_by(|a, b| {
|
titles.sort_by(|a, b| {
|
||||||
b.duration_secs
|
b.duration_secs
|
||||||
.partial_cmp(&a.duration_secs)
|
.partial_cmp(&a.duration_secs)
|
||||||
@@ -1037,7 +1071,12 @@ impl Disc {
|
|||||||
|
|
||||||
// 4. Metadata + labels
|
// 4. Metadata + labels
|
||||||
let meta_title = Self::read_meta_title(reader, &udf_fs);
|
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);
|
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
|
// 5. Derive format, layers, region
|
||||||
let format = Self::detect_format(&titles);
|
let format = Self::detect_format(&titles);
|
||||||
|
|||||||
+95
@@ -61,6 +61,11 @@ impl UdfFs {
|
|||||||
self.metadata_start
|
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").
|
/// Find a directory by path (e.g. "/BDMV/PLAYLIST").
|
||||||
/// Path matching is case-insensitive.
|
/// Path matching is case-insensitive.
|
||||||
pub fn find_dir(&self, path: &str) -> Option<&DirEntry> {
|
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.
|
/// Read a single 2048-byte sector from the drive.
|
||||||
/// Uses standard READ(10) — no unlock required.
|
/// 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<u8>,
|
||||||
|
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<usize, crate::error::Error> {
|
||||||
|
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<()> {
|
fn read_sector(reader: &mut dyn SectorReader, lba: u32, buf: &mut [u8]) -> Result<()> {
|
||||||
reader.read_sectors(lba, 1, buf)?;
|
reader.read_sectors(lba, 1, buf)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
Reference in New Issue
Block a user