v0.11.11: binary search error recovery in fill_extents

This commit is contained in:
Matt Jackson
2026-04-20 21:51:46 +00:00
parent cce1be29b2
commit ad86f92d85
3 changed files with 67 additions and 23 deletions
+5
View File
@@ -1,5 +1,10 @@
# Changelog
## 0.11.11 (2026-04-20)
### Binary search error recovery
- **fill_extents binary search** — when a batch read fails, binary search to isolate the failing sector(s). Good sectors read in sub-batches at full speed. Only truly bad sectors get individual recovery. 60-sector batch with 1 bad sector: ~5 seconds instead of 10+ minutes.
## 0.11.10 (2026-04-20)
### Skip errors + clean verify API
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "libfreemkv"
version = "0.11.10"
version = "0.11.11"
edition = "2021"
rust-version = "1.86"
license = "AGPL-3.0-only"
+58 -19
View File
@@ -119,6 +119,55 @@ impl DiscStream {
self.disc.as_ref()
}
/// Binary search to isolate failing sectors within a batch.
/// Reads good regions in sub-batches, handles bad sectors individually.
fn read_with_binary_search(&mut self, lba: u32, count: u16) -> io::Result<()> {
if count <= 1 {
// Single sector — read with full recovery (Tier 2)
let offset = self.buf_valid;
match self.reader.read_sectors(lba, 1, &mut self.read_buf[offset..offset + 2048]) {
Ok(_) => {
self.buf_valid += 2048;
}
Err(e) => {
if self.skip_errors {
self.read_buf[offset..offset + 2048].fill(0);
self.buf_valid += 2048;
self.errors += 1;
} else {
return Err(e.into());
}
}
}
return Ok(());
}
// Try this sub-batch as a whole first
let bytes = count as usize * 2048;
let offset = self.buf_valid;
if self
.reader
.read_sectors_recover(lba, count, &mut self.read_buf[offset..offset + bytes], false)
.is_ok()
{
// Sub-batch succeeded with fast read — all good
self.buf_valid += bytes;
return Ok(());
}
// Sub-batch failed — split in half and recurse
let half = count / 2;
let half = half - (half % 3).min(half); // align to 3-sector BD-TS boundary
let half = half.max(1);
let remainder = count - half;
self.read_with_binary_search(lba, half)?;
if remainder > 0 {
self.read_with_binary_search(lba + half as u32, remainder)?;
}
Ok(())
}
fn fill_extents(&mut self) -> io::Result<bool> {
if self.current_extent >= self.extents.len() {
return Ok(false);
@@ -139,28 +188,18 @@ impl DiscStream {
let bytes = sectors as usize * 2048;
self.read_buf.resize(bytes, 0);
match self.reader.read_sectors(lba, sectors, &mut self.read_buf[..bytes]) {
Ok(_) => {
self.buf_valid = bytes;
}
Err(e) if self.skip_errors => {
// Skip mode: try each sector individually, zero-fill failures
self.buf_valid = 0;
for i in 0..sectors {
let offset = i as usize * 2048;
let sector_lba = lba + i as u32;
if self
.reader
.read_sectors(sector_lba, 1, &mut self.read_buf[offset..offset + 2048])
.is_err()
.read_sectors(lba, sectors, &mut self.read_buf[..bytes])
.is_ok()
{
self.read_buf[offset..offset + 2048].fill(0);
self.errors += 1;
}
self.buf_valid += 2048;
}
}
Err(e) => return Err(e.into()),
// Fast path: batch succeeded
self.buf_valid = bytes;
} else {
// Tier 1: Binary search to isolate the failing sector(s),
// then read good regions in batches and bad sectors individually.
self.buf_valid = 0;
self.read_with_binary_search(lba, sectors)?;
}
self.current_offset += sectors as u32;
if self.current_offset >= ext_sectors {