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

This commit is contained in:
2026-04-29 22:08:52 -07:00
parent 5d7946350c
commit 966bc13dd2
3 changed files with 116 additions and 50 deletions
+7 -7
View File
@@ -44,17 +44,17 @@ Three failure modes on this USB bridge:
2. **Transport failure** (status=0xFF) — bridge crash, auto-recovers ~15s. Aborts copy.
3. **INCOMPATIBLE FORMAT** (ASC=0x30) wedge — ALL sectors fail, requires power cycle.
### Adaptive probe algorithm (Pass 1 sweep)
### Damage-jump algorithm (Pass 1 sweep)
When `skip_on_error=true` (multipass mode):
- Read each ECC block sequentially. On success, reset consecutive error counter.
- On error: zero-fill, mark NonTrimmed, increment consecutive error counter.
- After 4 consecutive errors: **probe** 1 sector at 256×batch (8 MB) ahead.
- Probe succeeds: zero-fill the gap, mark it NonTrimmed, jump to probed position.
- Probe fails: stay put, accumulate 4 more errors, probe again.
- Read each ECC block sequentially. Track a sliding window of the last 50 ECC block results.
- On error: zero-fill, mark NonTrimmed, push `false` to window.
- On success: write data, mark Finished, push `true` to window. Track consecutive good count.
- When ≥25% of the 50-block window are failures → **jump** ahead by `256×batch×multiplier` sectors (8 MB base). Zero-fill the gap as NonTrimmed. Double the multiplier (8→16→32→64 MB...).
- When 50 consecutive good reads → reset multiplier to 1.
- Only transport failures (bridge crash) abort the pass.
Design rationale: consecutive errors (not total) so isolated scattered bad blocks don't trigger probes. The 8 MB probe distance clears typical ~30 MB bad zones in 2-3 probes. Gaps are zero-filled and marked NonTrimmed for patch passes to recover later.
Two tuning knobs: `DAMAGE_WINDOW` (50 ECC blocks) and `DAMAGE_THRESHOLD_PCT` (25%). If the jump is too aggressive or too timid, adjust these.
## Public repo rules
+70 -21
View File
@@ -1288,8 +1288,15 @@ impl Disc {
let mut last_log_iter: u64 = 0;
let mut not_ready_retries: u32 = 0;
const NOT_READY_MAX_RETRIES: u32 = 3;
let mut error_count_in_zone: u64 = 0;
let mut probe_buf = vec![0u8; 2048];
let mut bridge_degradation_count: u32 = 0;
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!(
target: "freemkv::disc",
phase = "copy_start",
@@ -1353,7 +1360,15 @@ impl Disc {
if read_result.is_ok() {
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 {
crate::decrypt::decrypt_sectors(
@@ -1383,6 +1398,7 @@ impl Disc {
} else {
let err = read_result.err().unwrap();
read_err_count += 1;
consecutive_good = 0;
if err.is_scsi_transport_failure() {
tracing::warn!(
@@ -1395,6 +1411,28 @@ impl Disc {
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_key = sense.map(|s| s.sense_key).unwrap_or(0);
let asc = sense.map(|s| s.asc).unwrap_or(0);
@@ -1437,28 +1475,34 @@ impl Disc {
.map_err(|e| Error::IoError { source: e })?;
bytes_done = bytes_done.saturating_add(block_bytes);
error_count_in_zone += 1;
if error_count_in_zone >= 4 {
let probe_offset_sectors = 256 * batch as u64;
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 {
match reader.read_sectors(probe_lba, 1, &mut probe_buf, false) {
Ok(_) => {
let jump_pos = probe_lba as u64 * 2048;
damage_window.push(false);
if damage_window.len() > DAMAGE_WINDOW {
damage_window.remove(0);
}
let bad_count = damage_window.iter().filter(|&&b| !b).count();
if damage_window.len() >= DAMAGE_WINDOW
&& bad_count * 100 / damage_window.len() >= DAMAGE_THRESHOLD_PCT
{
let jump_sectors = JUMP_SECTORS_FACTOR * batch as u64 * jump_multiplier;
let jump_lba = ((pos / 2048) + jump_sectors) as u32;
let region_end_lba = (region_end / 2048) as u32;
if jump_lba < region_end_lba {
let jump_pos = jump_lba as u64 * 2048;
let gap_start = pos + block_bytes;
let gap_bytes = jump_pos.saturating_sub(gap_start);
let jump_mb = gap_bytes / 1_048_576;
if gap_bytes > 0 {
tracing::warn!(
target: "freemkv::disc",
phase = "probe_jump",
phase = "damage_jump",
from_lba = block_lba,
to_lba = probe_lba,
to_lba = jump_lba,
jump_mb,
errors = error_count_in_zone,
"probe found good media; jumping ahead"
bad_pct = bad_count * 100 / damage_window.len(),
multiplier = jump_multiplier,
"25%+ failures in last 50 blocks; jumping ahead"
);
if gap_bytes > 0 {
let zero = vec![0u8; 65536];
let mut filled: u64 = 0;
while filled < gap_bytes {
@@ -1474,11 +1518,8 @@ impl Disc {
bytes_done = bytes_done.saturating_add(gap_bytes);
}
pos = jump_pos;
jump_multiplier *= 2;
did_skip_ahead = true;
error_count_in_zone = 0;
}
Err(_) => {}
}
}
}
}
@@ -1861,6 +1902,14 @@ const MAX_BATCH_SECTORS: u16 = 510;
const DEFAULT_BATCH_SECTORS: u16 = 60;
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
/// observable signals (bad sector count + lost wallclock playback time)
/// 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
/// drive returned an error category in which smaller-granularity
/// retries can sometimes recover the data: