v0.13.46: damage-jump algorithm replaces probe, bridge degradation detection, ecc_sectors()

This commit is contained in:
MattJackson
2026-04-29 22:08:52 -07:00
parent a6f1bd19bc
commit e2cb7a78bb
2 changed files with 109 additions and 43 deletions
+92 -43
View File
@@ -1288,8 +1288,15 @@ impl Disc {
let mut last_log_iter: u64 = 0; let mut last_log_iter: u64 = 0;
let mut not_ready_retries: u32 = 0; let mut not_ready_retries: u32 = 0;
const NOT_READY_MAX_RETRIES: u32 = 3; const NOT_READY_MAX_RETRIES: u32 = 3;
let mut error_count_in_zone: u64 = 0; let mut bridge_degradation_count: u32 = 0;
let mut probe_buf = vec![0u8; 2048]; const BRIDGE_DEGRADATION_MAX: u32 = 5;
const BRIDGE_DEGRADATION_COOLDOWN_SECS: u64 = 10;
const DAMAGE_WINDOW: usize = 50;
const DAMAGE_THRESHOLD_PCT: usize = 25;
const JUMP_SECTORS_FACTOR: u64 = 256;
let mut damage_window: Vec<bool> = Vec::with_capacity(DAMAGE_WINDOW);
let mut jump_multiplier: u64 = 1;
let mut consecutive_good: u64 = 0;
tracing::trace!( tracing::trace!(
target: "freemkv::disc", target: "freemkv::disc",
phase = "copy_start", phase = "copy_start",
@@ -1353,7 +1360,15 @@ impl Disc {
if read_result.is_ok() { if read_result.is_ok() {
read_ok_count += 1; read_ok_count += 1;
error_count_in_zone = 0; damage_window.push(true);
if damage_window.len() > DAMAGE_WINDOW {
damage_window.remove(0);
}
consecutive_good += 1;
if consecutive_good >= DAMAGE_WINDOW as u64 {
jump_multiplier = 1;
}
bridge_degradation_count = 0;
if opts.decrypt { if opts.decrypt {
crate::decrypt::decrypt_sectors( crate::decrypt::decrypt_sectors(
@@ -1383,6 +1398,7 @@ impl Disc {
} else { } else {
let err = read_result.err().unwrap(); let err = read_result.err().unwrap();
read_err_count += 1; read_err_count += 1;
consecutive_good = 0;
if err.is_scsi_transport_failure() { if err.is_scsi_transport_failure() {
tracing::warn!( tracing::warn!(
@@ -1395,6 +1411,28 @@ impl Disc {
return Err(err); return Err(err);
} }
if err.is_bridge_degradation() {
if bridge_degradation_count < BRIDGE_DEGRADATION_MAX {
bridge_degradation_count += 1;
tracing::warn!(
target: "freemkv::disc",
phase = "bridge_degradation",
lba = block_lba,
degradation_count = bridge_degradation_count,
error = %err,
"bridge degradation; cooling down 10s"
);
std::thread::sleep(std::time::Duration::from_secs(BRIDGE_DEGRADATION_COOLDOWN_SECS));
continue;
}
tracing::warn!(
target: "freemkv::disc",
phase = "bridge_degradation_exhausted",
lba = block_lba,
"bridge degradation retries exhausted; treating as bad sector"
);
}
let sense = err.scsi_sense(); let sense = err.scsi_sense();
let sense_key = sense.map(|s| s.sense_key).unwrap_or(0); let sense_key = sense.map(|s| s.sense_key).unwrap_or(0);
let asc = sense.map(|s| s.asc).unwrap_or(0); let asc = sense.map(|s| s.asc).unwrap_or(0);
@@ -1437,48 +1475,51 @@ impl Disc {
.map_err(|e| Error::IoError { source: e })?; .map_err(|e| Error::IoError { source: e })?;
bytes_done = bytes_done.saturating_add(block_bytes); bytes_done = bytes_done.saturating_add(block_bytes);
error_count_in_zone += 1; damage_window.push(false);
if error_count_in_zone >= 4 { if damage_window.len() > DAMAGE_WINDOW {
let probe_offset_sectors = 256 * batch as u64; damage_window.remove(0);
let probe_lba = ((pos / 2048) + probe_offset_sectors) as u32; }
let probe_end_lba = (region_end / 2048) as u32;
if probe_lba < probe_end_lba { let bad_count = damage_window.iter().filter(|&&b| !b).count();
match reader.read_sectors(probe_lba, 1, &mut probe_buf, false) { if damage_window.len() >= DAMAGE_WINDOW
Ok(_) => { && bad_count * 100 / damage_window.len() >= DAMAGE_THRESHOLD_PCT
let jump_pos = probe_lba as u64 * 2048; {
let gap_start = pos + block_bytes; let jump_sectors = JUMP_SECTORS_FACTOR * batch as u64 * jump_multiplier;
let gap_bytes = jump_pos.saturating_sub(gap_start); let jump_lba = ((pos / 2048) + jump_sectors) as u32;
let jump_mb = gap_bytes / 1_048_576; let region_end_lba = (region_end / 2048) as u32;
if gap_bytes > 0 { if jump_lba < region_end_lba {
tracing::warn!( let jump_pos = jump_lba as u64 * 2048;
target: "freemkv::disc", let gap_start = pos + block_bytes;
phase = "probe_jump", let gap_bytes = jump_pos.saturating_sub(gap_start);
from_lba = block_lba, let jump_mb = gap_bytes / 1_048_576;
to_lba = probe_lba, tracing::warn!(
jump_mb, target: "freemkv::disc",
errors = error_count_in_zone, phase = "damage_jump",
"probe found good media; jumping ahead" from_lba = block_lba,
); to_lba = jump_lba,
let zero = vec![0u8; 65536]; jump_mb,
let mut filled: u64 = 0; bad_pct = bad_count * 100 / damage_window.len(),
while filled < gap_bytes { multiplier = jump_multiplier,
let chunk = (gap_bytes - filled).min(zero.len() as u64); "25%+ failures in last 50 blocks; jumping ahead"
file.seek(SeekFrom::Start(gap_start + filled)) );
.map_err(|e| Error::IoError { source: e })?; if gap_bytes > 0 {
file.write_all(&zero[..chunk as usize]) let zero = vec![0u8; 65536];
.map_err(|e| Error::IoError { source: e })?; let mut filled: u64 = 0;
filled += chunk; while filled < gap_bytes {
} let chunk = (gap_bytes - filled).min(zero.len() as u64);
map.record(gap_start, gap_bytes, mapfile::SectorStatus::NonTrimmed) file.seek(SeekFrom::Start(gap_start + filled))
.map_err(|e| Error::IoError { source: e })?; .map_err(|e| Error::IoError { source: e })?;
bytes_done = bytes_done.saturating_add(gap_bytes); file.write_all(&zero[..chunk as usize])
} .map_err(|e| Error::IoError { source: e })?;
pos = jump_pos; filled += chunk;
did_skip_ahead = true;
error_count_in_zone = 0;
} }
Err(_) => {} map.record(gap_start, gap_bytes, mapfile::SectorStatus::NonTrimmed)
.map_err(|e| Error::IoError { source: e })?;
bytes_done = bytes_done.saturating_add(gap_bytes);
} }
pos = jump_pos;
jump_multiplier *= 2;
did_skip_ahead = true;
} }
} }
} }
@@ -1861,6 +1902,14 @@ const MAX_BATCH_SECTORS: u16 = 510;
const DEFAULT_BATCH_SECTORS: u16 = 60; const DEFAULT_BATCH_SECTORS: u16 = 60;
const MIN_BATCH_SECTORS: u16 = 3; const MIN_BATCH_SECTORS: u16 = 3;
pub fn ecc_sectors(format: DiscFormat) -> u16 {
match format {
DiscFormat::Uhd | DiscFormat::BluRay => 32,
DiscFormat::Dvd => 16,
DiscFormat::Unknown => 32,
}
}
/// Coarse damage tier for a finished or in-progress rip. Maps the /// Coarse damage tier for a finished or in-progress rip. Maps the
/// observable signals (bad sector count + lost wallclock playback time) /// observable signals (bad sector count + lost wallclock playback time)
/// onto a small discrete classification so UIs can render a colored badge /// onto a small discrete classification so UIs can render a colored badge
+17
View File
@@ -516,6 +516,23 @@ impl Error {
) )
} }
/// True if this error indicates bridge degradation — the SCSI status
/// is neither GOOD (0x00), CHECK CONDITION (0x02), nor transport failure
/// (0xFF). Observed on the Initio INIC-1618L USB bridge preceding a full
/// crash: the bridge firmware returns non-standard status bytes (e.g.
/// 0x04, 0x05) with empty sense data. The caller should cool down
/// (10 s pause) and retry rather than hammering the bridge.
pub fn is_bridge_degradation(&self) -> bool {
let status = match self {
Error::ScsiError { status, .. } => *status,
Error::DiscRead { status, .. } => status.unwrap_or(0),
_ => return false,
};
status != crate::scsi::SCSI_STATUS_GOOD
&& status != crate::scsi::SCSI_STATUS_CHECK_CONDITION
&& status != crate::scsi::SCSI_STATUS_TRANSPORT_FAILURE
}
/// True if the underlying SCSI failure is a *marginal read* — the /// True if the underlying SCSI failure is a *marginal read* — the
/// drive returned an error category in which smaller-granularity /// drive returned an error category in which smaller-granularity
/// retries can sometimes recover the data: /// retries can sometimes recover the data: