Fix read speed: detect kernel max transfer size, fix phantom streams

Root cause: READ_10 requested 510 sectors (1MB) but kernel sg driver
limits to max_hw_sectors_kb=120 (60 sectors). All bulk reads errored,
error handler fell to 3-sector reads → 4.8 MB/s instead of 12+ MB/s.

- detect_max_batch_sectors() now resolves sg→block device via sysfs
 and uses 80% of kernel limit for safety margin
- Default fallback reduced from 510 to 48 sectors
- ContentReader uses per-device detected max, not hardcoded constant
- Filter out coding_type 0x00 (empty/padding) stream entries
- Add MPLS stream_type 5/6/7 attribute parsing (secondary audio/video)

Tested on BU40N: 4.8 → 12.5 MB/s sustained, 23.7 MB/s peak.
This commit is contained in:
MattJackson
2026-04-08 22:30:08 -07:00
parent 6456e24bb5
commit d9c5f7a0d2
2 changed files with 60 additions and 13 deletions
+39 -13
View File
@@ -672,6 +672,8 @@ impl Disc {
// Build streams from STN table
let streams: Vec<Stream> = parsed.streams.iter().filter_map(|s| {
// Skip empty/padding entries (coding_type 0x00)
if s.coding_type == 0 { return None; }
let codec = Codec::from_coding_type(s.coding_type);
match s.stream_type {
1 | 6 | 7 => Some(Stream::Video(VideoStream {
@@ -765,6 +767,8 @@ pub struct ContentReader<'a> {
buf_len: usize,
/// Current batch size in sectors (adapts on errors)
batch_sectors: u16,
/// Maximum batch size detected from kernel limits
max_batch_sectors: u16,
/// Consecutive successful batch reads
ok_streak: u32,
/// Consecutive errors at current position
@@ -804,6 +808,9 @@ impl Disc {
let mut dummy = [0u8; 0];
let _ = session.scsi_execute(&speed_cdb, crate::scsi::DataDirection::None, &mut dummy, 5_000);
// Detect kernel max transfer size for this device
let max_batch = detect_max_batch_sectors(session.device_path());
Ok(ContentReader {
session,
aacs: self.aacs.as_ref(),
@@ -811,10 +818,11 @@ impl Disc {
current_extent: 0,
current_offset: 0,
unit_key_idx: 0,
read_buf: Vec::with_capacity(MAX_BATCH_SECTORS as usize * 2048),
read_buf: Vec::with_capacity(max_batch as usize * 2048),
buf_pos: 0,
buf_len: 0,
batch_sectors: MAX_BATCH_SECTORS,
batch_sectors: max_batch,
max_batch_sectors: max_batch,
ok_streak: 0,
error_streak: 0,
speed_tier: 0,
@@ -826,30 +834,48 @@ impl Disc {
/// Detect the maximum transfer size in sectors for a device.
/// Reads /sys/block/<dev>/queue/max_hw_sectors_kb on Linux.
/// For sg devices, resolves the corresponding block device via sysfs.
/// Returns a value aligned to 3 sectors (one aligned unit).
fn detect_max_batch_sectors(device_path: &str) -> u16 {
// Extract block device name: /dev/sr0 → sr0
let dev_name = device_path.rsplit('/').next().unwrap_or("");
if !dev_name.is_empty() {
let sysfs_path = format!("/sys/block/{}/queue/max_hw_sectors_kb", dev_name);
if dev_name.is_empty() {
return DEFAULT_BATCH_SECTORS;
}
// For sg devices, find the corresponding block device name
let block_name = if dev_name.starts_with("sg") {
let block_dir = format!("/sys/class/scsi_generic/{}/device/block", dev_name);
std::fs::read_dir(&block_dir).ok()
.and_then(|mut entries| entries.next())
.and_then(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().to_string())
} else {
Some(dev_name.to_string())
};
if let Some(bname) = block_name {
let sysfs_path = format!("/sys/block/{}/queue/max_hw_sectors_kb", bname);
if let Ok(content) = std::fs::read_to_string(&sysfs_path) {
if let Ok(kb) = content.trim().parse::<u32>() {
// Convert KB to sectors (1 sector = 2 KB on disc = 2048 bytes)
// Convert KB to sectors (1 sector = 2 KB = 2048 bytes)
let sectors = (kb / 2) as u16;
// Align down to 3 (one aligned unit) and cap at a reasonable max
let aligned = (sectors / 3) * 3;
// Stay well under kernel limit (80% of max) to avoid edge cases
let safe = (sectors * 4 / 5).max(MIN_BATCH_SECTORS);
// Align down to 3 (one aligned unit)
let aligned = (safe / 3) * 3;
if aligned >= MIN_BATCH_SECTORS {
return aligned.min(MAX_BATCH_SECTORS);
}
}
}
}
// Fallback: conservative default
MAX_BATCH_SECTORS
// Fallback: safe default well under typical kernel limits
DEFAULT_BATCH_SECTORS
}
/// Read strategy constants
const MAX_BATCH_SECTORS: u16 = 510; // 170 aligned units ≈ 1MB (kernel caps to hw limit)
const MAX_BATCH_SECTORS: u16 = 510; // absolute max (170 aligned units ≈ 1MB)
const DEFAULT_BATCH_SECTORS: u16 = 48; // safe fallback (96KB, under typical 120KB kernel limit)
const MIN_BATCH_SECTORS: u16 = 3; // 1 aligned unit = 6KB (error recovery)
const RAMP_BATCH_AFTER: u32 = 5; // successes before doubling batch size
const RAMP_SPEED_AFTER: u32 = 50; // successes at max batch before restoring speed
@@ -1009,9 +1035,9 @@ impl<'a> ContentReader<'a> {
// Ramp up: batch size first, then disc speed
self.ok_streak += 1;
if self.batch_sectors < MAX_BATCH_SECTORS {
if self.batch_sectors < self.max_batch_sectors {
if self.ok_streak >= RAMP_BATCH_AFTER {
self.batch_sectors = (self.batch_sectors * 2).min(MAX_BATCH_SECTORS);
self.batch_sectors = (self.batch_sectors * 2).min(self.max_batch_sectors);
self.ok_streak = 0;
}
} else if self.speed_tier > 0 && self.ok_streak >= RAMP_SPEED_AFTER {
+21
View File
@@ -263,6 +263,27 @@ fn parse_stream_entry(item: &[u8], pos: usize, stream_type: u8) -> Option<(Strea
language = String::from_utf8_lossy(&sa[1..4]).to_string();
}
}
5 => {
// Secondary audio: same as primary audio
if sa.len() >= 2 {
audio_format = (sa[1] >> 4) & 0x0F;
audio_rate = sa[1] & 0x0F;
}
if sa.len() >= 5 {
language = String::from_utf8_lossy(&sa[2..5]).to_string();
}
}
6 | 7 => {
// Secondary video: same as primary video
if sa.len() >= 2 {
video_format = (sa[1] >> 4) & 0x0F;
video_rate = sa[1] & 0x0F;
}
if coding_type == 0x24 && sa.len() > 2 {
dynamic_range = (sa[2] >> 4) & 0x0F;
color_space_val = sa[2] & 0x0F;
}
}
_ => {}
}