From ad86f92d856e9c38dd987d27bc31e93e4786db5b Mon Sep 17 00:00:00 2001 From: Matt Jackson <1085847+MattJackson@users.noreply.github.com> Date: Mon, 20 Apr 2026 21:51:46 +0000 Subject: [PATCH] v0.11.11: binary search error recovery in fill_extents --- CHANGELOG.md | 5 +++ Cargo.toml | 2 +- src/mux/disc.rs | 83 ++++++++++++++++++++++++++++++++++++------------- 3 files changed, 67 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc59aaa..a15e478 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.toml b/Cargo.toml index eb683aa..6e9db1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/mux/disc.rs b/src/mux/disc.rs index d577341..6702628 100644 --- a/src/mux/disc.rs +++ b/src/mux/disc.rs @@ -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 { 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() - { - self.read_buf[offset..offset + 2048].fill(0); - self.errors += 1; - } - self.buf_valid += 2048; - } - } - Err(e) => return Err(e.into()), + if self + .reader + .read_sectors(lba, sectors, &mut self.read_buf[..bytes]) + .is_ok() + { + // 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 {