From 09f5bb7816c50375412c9bb8e408774d1fd61c62 Mon Sep 17 00:00:00 2001 From: MattJackson <1085847+MattJackson@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:33:02 -0700 Subject: [PATCH] Speed table: generic zone-based speed management - SpeedTable: maps disc positions to optimal speeds - Default: max speed everywhere (drive manages itself) - After read_speed_table(): calibrated per-zone speeds - One u32 comparison per read on hot path - Error recovery: reduce() / resume() override table temporarily - Replaces old tier-based speed management in ContentReader - MT1959 split into mod.rs + variant_a.rs + variant_b.rs - PlatformDriver: init() + read_speed_table() + is_ready() --- src/disc.rs | 75 +++---- src/drive.rs | 14 +- src/platform/mod.rs | 20 +- src/platform/{mt1959.rs => mt1959/mod.rs} | 249 ++++++---------------- src/platform/mt1959/variant_a.rs | 33 +++ src/platform/mt1959/variant_b.rs | 52 +++++ src/speed.rs | 223 +++++++++++-------- 7 files changed, 327 insertions(+), 339 deletions(-) rename src/platform/{mt1959.rs => mt1959/mod.rs} (53%) create mode 100644 src/platform/mt1959/variant_a.rs create mode 100644 src/platform/mt1959/variant_b.rs diff --git a/src/disc.rs b/src/disc.rs index 9f851fe..67b6f1f 100644 --- a/src/disc.rs +++ b/src/disc.rs @@ -774,9 +774,7 @@ pub struct ContentReader<'a> { /// Consecutive errors at current position error_streak: u32, /// Current speed tier index (0 = max, higher = slower) - speed_tier: usize, /// Last time maintain_speed was called - last_speed_maintain: std::time::Instant, /// Total read errors encountered pub errors: u32, } @@ -823,8 +821,6 @@ impl Disc { max_batch_sectors: max_batch, ok_streak: 0, error_streak: 0, - speed_tier: 0, - last_speed_maintain: std::time::Instant::now(), errors: 0, }) } @@ -879,15 +875,6 @@ const RAMP_BATCH_AFTER: u32 = 5; // successes before doubling batch size const RAMP_SPEED_AFTER: u32 = 50; // successes at max batch before restoring speed const SLOW_SPEED_AFTER: u32 = 3; // consecutive errors before reducing disc speed -/// Disc speed tiers (KB/s for SET CD SPEED). -/// Blu-ray: 1x=4500, 2x=9000, 4x=18000, 8x=36000, 12x=54000 -const SPEED_TIERS: &[u16] = &[ - 0xFFFF, // tier 0: max (drive decides, typically 8-12x) - 36000, // tier 1: 8x BD (~36 MB/s) - 18000, // tier 2: 4x BD (~18 MB/s) - 9000, // tier 3: 2x BD (~9 MB/s) - 4500, // tier 4: 1x BD (~4.5 MB/s) — last resort -]; impl<'a> ContentReader<'a> { /// Total bytes across all extents (for progress display). @@ -973,29 +960,10 @@ impl<'a> ContentReader<'a> { Ok(()) } - /// Set disc spin speed via SCSI SET CD SPEED. - fn set_speed(&mut self, tier: usize) { - let tier = tier.min(SPEED_TIERS.len() - 1); - if tier != self.speed_tier { - self.speed_tier = tier; - let speed_kbs = SPEED_TIERS[tier]; - let cdb = crate::scsi::build_set_cd_speed(speed_kbs); - let mut dummy = [0u8; 0]; - let _ = self.session.scsi_execute( - &cdb, crate::scsi::DataDirection::None, &mut dummy, 5_000, - ); - } - } - /// Read a batch of sectors into the internal buffer. /// - /// Adaptive strategy: - /// 1. Read at current batch size - /// 2. On success: ramp batch up (double after 5 successes), - /// then restore disc speed (after 50 at max batch) - /// 3. On error: halve batch, pause. After 3 consecutive errors, - /// also reduce disc spin speed (scratched/damaged region). - /// 4. At min batch + still failing: retry once, then skip + zero-fill. + /// Speed management: speed table checked before each read. + /// On error: reduce speed, halve batch. On recovery: resume from table. fn fill_buffer(&mut self) -> Result { loop { if self.current_extent >= self.extents.len() { @@ -1019,6 +987,15 @@ impl<'a> ContentReader<'a> { let byte_count = sectors_to_read as usize * 2048; self.read_buf.resize(byte_count, 0); + // Check speed table — send SET_CD_SPEED if zone changed + if let Some(speed_kbs) = self.session.speed_table.speed_for(lba) { + let cdb = crate::scsi::build_set_cd_speed(speed_kbs); + let mut dummy = [0u8; 0]; + let _ = self.session.scsi_execute( + &cdb, crate::scsi::DataDirection::None, &mut dummy, 5_000, + ); + } + match self.read_sectors(lba, sectors_to_read) { Ok(_) => { self.buf_len = sectors_to_read as usize / 3; @@ -1031,19 +1008,18 @@ impl<'a> ContentReader<'a> { self.current_offset = 0; } - // Ramp up: batch size first, then disc speed + // Ramp up batch size after consecutive successes self.ok_streak += 1; - if self.batch_sectors < self.max_batch_sectors { - if self.ok_streak >= RAMP_BATCH_AFTER { - 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 { - // At max batch for a while — try faster disc speed - self.set_speed(self.speed_tier - 1); + if self.batch_sectors < self.max_batch_sectors && self.ok_streak >= RAMP_BATCH_AFTER { + self.batch_sectors = (self.batch_sectors * 2).min(self.max_batch_sectors); self.ok_streak = 0; } + // Resume table-driven speed after sustained success + if self.error_streak == 0 && self.ok_streak >= RAMP_SPEED_AFTER { + self.session.speed_table.resume(lba); + } + return Ok(true); } Err(_) => { @@ -1051,11 +1027,14 @@ impl<'a> ContentReader<'a> { self.error_streak += 1; self.ok_streak = 0; - // Reduce disc speed after repeated errors (physical problem) - if self.error_streak >= SLOW_SPEED_AFTER - && self.speed_tier < SPEED_TIERS.len() - 1 - { - self.set_speed(self.speed_tier + 1); + // Reduce speed after repeated errors + if self.error_streak >= SLOW_SPEED_AFTER { + let speed = self.session.speed_table.reduce(); + let cdb = crate::scsi::build_set_cd_speed(speed); + let mut dummy = [0u8; 0]; + let _ = self.session.scsi_execute( + &cdb, crate::scsi::DataDirection::None, &mut dummy, 5_000, + ); self.error_streak = 0; // reset — give new speed a chance } diff --git a/src/drive.rs b/src/drive.rs index 83bc259..f6ef01d 100644 --- a/src/drive.rs +++ b/src/drive.rs @@ -12,10 +12,12 @@ use crate::identity::DriveId; use crate::profile::{self, DriveProfile, ProfileMatch}; use crate::platform::PlatformDriver; use crate::platform::mt1959::Mt1959; +use crate::speed::SpeedTable; pub struct DriveSession { scsi: Box, driver: Box, + pub speed_table: SpeedTable, pub profile: DriveProfile, pub platform: profile::Platform, pub drive_id: DriveId, @@ -40,6 +42,7 @@ impl DriveSession { Ok(DriveSession { scsi: transport, driver, + speed_table: SpeedTable::new(), platform: m.platform, profile: m.profile, drive_id, @@ -71,16 +74,19 @@ impl DriveSession { &self.device_path } + /// Initialize drive — unlock + firmware upload. pub fn init(&mut self) -> Result<()> { self.driver.init(self.scsi.as_mut()) } - pub fn is_ready(&self) -> bool { - self.driver.is_ready() + /// Read speed zones from disc into speed table. + /// Requires init() first. Optional — without this, drive manages speed itself. + pub fn read_speed_table(&mut self) -> Result<()> { + self.driver.read_speed_table(self.scsi.as_mut(), &mut self.speed_table) } - pub fn set_read_speed(&mut self, lba: u32) -> Result<()> { - self.driver.set_read_speed(self.scsi.as_mut(), lba) + pub fn is_ready(&self) -> bool { + self.driver.is_ready() } pub fn read_disc(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result { diff --git a/src/platform/mod.rs b/src/platform/mod.rs index b486d10..46cf833 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -1,22 +1,18 @@ -//! Platform-specific drive initialization and speed management. -//! -//! The Platform trait is minimal by design. Callers use init() once, -//! then set_read_speed() during reads. Internal operations cannot be -//! called directly — this prevents out-of-sequence operations. +//! Platform-specific drive initialization and calibration. pub mod mt1959; use crate::error::Result; use crate::scsi::ScsiTransport; +use crate::speed::SpeedTable; -/// Platform trait — locked-down interface. -/// -/// Only three operations exposed: -/// init() — one-time initialization -/// set_read_speed() — per-zone speed during reads -/// is_ready() — state check pub(crate) trait PlatformDriver { + /// Unlock drive + upload firmware if needed. fn init(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()>; - fn set_read_speed(&mut self, scsi: &mut dyn ScsiTransport, lba: u32) -> Result<()>; + + /// Read speed zones from disc surface, fill speed table. + fn read_speed_table(&mut self, scsi: &mut dyn ScsiTransport, speed_table: &mut SpeedTable) -> Result<()>; + + /// True after successful init(). fn is_ready(&self) -> bool; } diff --git a/src/platform/mt1959.rs b/src/platform/mt1959/mod.rs similarity index 53% rename from src/platform/mt1959.rs rename to src/platform/mt1959/mod.rs index 939431c..9bb5d34 100644 --- a/src/platform/mt1959.rs +++ b/src/platform/mt1959/mod.rs @@ -1,27 +1,27 @@ -//! MT1959 platform — unlock, firmware upload, calibration, speed management. +//! MT1959 platform — shared logic for both variants. + +mod variant_a; +mod variant_b; use crate::error::{Error, Result}; use crate::profile::DriveProfile; use crate::scsi::{self, DataDirection, ScsiTransport}; +use crate::speed::SpeedTable; use super::PlatformDriver; const UNLOCK_RESPONSE_SIZE: u8 = 64; - -// Variant constants const MODE_A: u8 = 0x01; const MODE_B: u8 = 0x02; const BUFFER_ID_A: u8 = 0x44; const BUFFER_ID_B: u8 = 0x77; const NOMINAL_SPEED_A: [u8; 12] = [0xBB, 0x00, 0x23, 0x28, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; const NOMINAL_SPEED_B: [u8; 12] = [0x00, 0x00, 0xBB, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00]; -const FIRMWARE_EXTRA_B: [u8; 16] = [0; 16]; -const VERIFY_COMMAND_B: [u8; 10] = [0xF1, 0x01, 0x02, 0x00, 0x0D, 0x30, 0x01, 0xF3, 0xAD, 0x23]; pub struct Mt1959 { - profile: DriveProfile, - mode: u8, - buffer_id: u8, - unlocked: bool, + pub(crate) profile: DriveProfile, + pub(crate) mode: u8, + pub(crate) buffer_id: u8, + pub(crate) unlocked: bool, speed_table: [u16; 64], disc_sectors: u32, calibrated: bool, @@ -36,9 +36,7 @@ impl Mt1959 { (MODE_A, BUFFER_ID_A) }; Mt1959 { - profile, - mode, - buffer_id, + profile, mode, buffer_id, unlocked: false, speed_table: [0u16; 64], disc_sectors: 0, @@ -47,7 +45,9 @@ impl Mt1959 { } } - fn read_buffer_sub(&self, sub_cmd: u8, address: u16, length: u8) -> [u8; 10] { + // ── SCSI helpers (shared by both variants) ───────────────────────── + + pub(crate) fn read_buffer_sub(&self, sub_cmd: u8, address: u16, length: u8) -> [u8; 10] { [ 0x3C, self.mode, self.buffer_id, sub_cmd, (address >> 8) as u8, address as u8, @@ -55,7 +55,7 @@ impl Mt1959 { ] } - fn read_buffer_probe( + pub(crate) fn read_buffer_probe( &self, scsi: &mut dyn ScsiTransport, sub_cmd: u8, address: u16, buf: &mut [u8], expected: usize, ) -> Result { @@ -67,21 +67,16 @@ impl Mt1959 { Ok(result.bytes_transferred) } - fn set_cd_speed_max(&self, scsi: &mut dyn ScsiTransport) -> Result<()> { + pub(crate) fn set_cd_speed_max(&self, scsi: &mut dyn ScsiTransport) -> Result<()> { let cdb = scsi::build_set_cd_speed(0xFFFF); let mut dummy = [0u8; 0]; scsi.execute(&cdb, DataDirection::None, &mut dummy, 5_000)?; Ok(()) } - fn set_cd_speed(&self, scsi: &mut dyn ScsiTransport, speed: u16) -> Result<()> { - let cdb = scsi::build_set_cd_speed(speed); - let mut dummy = [0u8; 0]; - scsi.execute(&cdb, DataDirection::None, &mut dummy, 5_000)?; - Ok(()) - } + // ── Unlock (shared) ──────────────────────────────────────────────── - fn do_unlock(&mut self, scsi: &mut dyn ScsiTransport) -> Result> { + pub(crate) fn do_unlock(&mut self, scsi: &mut dyn ScsiTransport) -> Result> { let cdb = [ 0x3C, self.mode, self.buffer_id, 0x00, 0x00, 0x00, @@ -124,49 +119,38 @@ impl Mt1959 { } Err(Error::ScsiError { opcode: 0x3C, status: 0xFF, sense_key: 0 }) } -} -impl PlatformDriver for Mt1959 { - fn init(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> { - if self.unlocked && self.calibrated { - return Ok(()); + // ── Init (unlock + firmware) ─────────────────────────────────────── + + fn run_init(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> { + let mut unlocked = false; + for _attempt in 0..6 { + match self.do_unlock(scsi) { + Ok(_) => { unlocked = true; break; } + Err(Error::SignatureMismatch { .. }) => { + return Err(Error::UnlockFailed { + detail: "signature mismatch — wrong profile for this drive".into(), + }); + } + Err(_) => { + let ok = if self.mode == MODE_A { + variant_a::load_firmware(self, scsi).is_ok() + } else { + variant_b::load_firmware(self, scsi).is_ok() + }; + if ok { unlocked = true; break; } + } + } } - self.run_init(scsi) - } - - fn set_read_speed(&mut self, scsi: &mut dyn ScsiTransport, lba: u32) -> Result<()> { - if !self.calibrated { - return Ok(()); + if !unlocked { + return Err(Error::UnlockFailed { detail: "failed after 6 attempts".into() }); } - self.run_set_read_speed(scsi, lba) - } - - fn is_ready(&self) -> bool { - self.unlocked && self.calibrated - } -} - -impl Mt1959 { - fn unlock(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> { - self.do_unlock(scsi)?; Ok(()) } - fn load_firmware(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> { - if self.profile.firmware.is_empty() { - return Err(Error::UnlockFailed { - detail: "no firmware in profile".into(), - }); - } + // ── Calibrate (disc surface probes) ──────────────────────────────── - if self.mode == MODE_A { - self.load_firmware_a(scsi) - } else { - self.load_firmware_b(scsi) - } - } - - fn calibrate(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> { + fn run_calibrate(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> { if !self.unlocked { self.do_unlock(scsi)?; } let cap_cdb = [0x25u8, 0, 0, 0, 0, 0, 0, 0, 0, 0]; @@ -175,7 +159,7 @@ impl Mt1959 { self.disc_sectors = u32::from_be_bytes([cap_buf[0], cap_buf[1], cap_buf[2], cap_buf[3]]) + 1; } - let init_addr: u16 = 0x0100; // TODO: detect disc type (0x0200 for UHD) + let init_addr: u16 = 0x0100; let mut init_resp = [0u8; 4]; let _ = self.read_buffer_probe(scsi, 0x12, init_addr, &mut init_resp, 4); @@ -198,9 +182,7 @@ impl Mt1959 { self.calibration_config = [0u8; 4]; return Err(Error::ScsiError { opcode: 0x3C, status: 0xFF, sense_key: 0 }); } - if resp[0] != prev_speed { - prev_speed = resp[0]; - } + if resp[0] != prev_speed { prev_speed = resp[0]; } addr = addr.wrapping_add(0x100); } @@ -232,135 +214,36 @@ impl Mt1959 { self.calibrated = true; Ok(()) } +} - fn run_set_read_speed(&mut self, scsi: &mut dyn ScsiTransport, lba: u32) -> Result<()> { - if !self.calibrated { - return Ok(()); - } +// ── PlatformDriver trait ─────────────────────────────────────────────── - let mut best_idx: usize = 0; - let mut best_diff: u32 = 0x10000000; - let mut found = false; +impl PlatformDriver for Mt1959 { + fn init(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> { + if self.unlocked { return Ok(()); } + self.run_init(scsi) + } + fn read_speed_table(&mut self, scsi: &mut dyn ScsiTransport, speed_table: &mut SpeedTable) -> Result<()> { + if !self.unlocked { self.run_init(scsi)?; } + if self.calibrated { return Ok(()); } + self.run_calibrate(scsi)?; + + let mut probes: Vec<(u16, u8)> = Vec::new(); for i in 0..64 { - let entry = self.speed_table[i] as u32; - if entry == 0 { continue; } - let diff = if lba > entry { lba - entry } else { entry - lba }; - if diff < best_diff { - best_diff = diff; - best_idx = i; - found = true; - } + let addr = self.speed_table[i]; + if addr == 0 { continue; } + let speed_idx = ((i + 1) << 1) as u8; + probes.push((addr, speed_idx)); } - - if !found { - return Ok(()); - } - - let speed_val = self.speed_table[best_idx]; - let probe_addr = 0x0100 | (speed_val.swap_bytes() as u16); - let mut probe_resp = [0u8; 4]; - let _ = self.read_buffer_probe(scsi, 0x14, probe_addr, &mut probe_resp, 4); - - let _ = self.set_cd_speed_max(scsi); - let _ = self.set_cd_speed(scsi, speed_val); + const PROBE_RANGE: u32 = 0x10000; + const BD_1X_KBS: u16 = 4500; + speed_table.load_calibration(self.disc_sectors, &probes, PROBE_RANGE, BD_1X_KBS); Ok(()) } - fn run_init(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> { - let mut unlocked = false; - for _attempt in 0..6 { - match self.unlock(scsi) { - Ok(_) => { unlocked = true; break; } - Err(Error::SignatureMismatch { .. }) => { - return Err(Error::UnlockFailed { - detail: "signature mismatch — wrong profile for this drive".into(), - }); - } - Err(_) => { - if self.load_firmware(scsi).is_ok() { - unlocked = true; - break; - } - } - } - } - if !unlocked { - return Err(Error::UnlockFailed { - detail: "failed after 6 attempts".into(), - }); - } - - let mut calibrated = false; - for _attempt in 0..6 { - if self.calibrate(scsi).is_ok() { - calibrated = true; - break; - } - } - if !calibrated { - return Err(Error::ScsiError { opcode: 0x3C, status: 0xFF, sense_key: 0 }); - } - - Ok(()) - } -} - -impl Mt1959 { - fn load_firmware_a(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> { - let firmware = &self.profile.firmware; - let len = firmware.len(); - - let cdb = [ - 0x3B, 0x06, 0x00, - 0x00, 0x00, 0x00, - (len >> 16) as u8, (len >> 8) as u8, len as u8, - 0x00, - ]; - let mut data = firmware.clone(); - scsi.execute(&cdb, DataDirection::ToDevice, &mut data, 30_000)?; - - let verify_cdb = [0x3C, 0x01, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00]; - let mut verify_resp = [0u8; 4]; - let _ = scsi.execute(&verify_cdb, DataDirection::FromDevice, &mut verify_resp, 5_000); - - self.do_unlock(scsi)?; - self.do_unlock(scsi)?; - Ok(()) - } - - fn load_firmware_b(&mut self, scsi: &mut dyn ScsiTransport) -> Result<()> { - let firmware = &self.profile.firmware; - - let write_len = 0x9C0usize.min(firmware.len()); - let mode_select_cdb = [ - 0x55, 0x10, 0x00, - 0x00, 0x00, 0x00, - (write_len >> 16) as u8, (write_len >> 8) as u8, write_len as u8, - 0x00, - ]; - let mut data = firmware[..write_len].to_vec(); - scsi.execute(&mode_select_cdb, DataDirection::ToDevice, &mut data, 30_000)?; - - let read_meta_cdb = [0x3C, 0x06, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x10, 0x00]; - let mut meta_resp = [0u8; 16]; - let _ = scsi.execute(&read_meta_cdb, DataDirection::FromDevice, &mut meta_resp, 5_000); - - let write2_cdb = [0x3B, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00]; - let mut data2 = FIRMWARE_EXTRA_B.to_vec(); - let _ = scsi.execute(&write2_cdb, DataDirection::ToDevice, &mut data2, 5_000); - - let mut dummy = [0u8; 0]; - let _ = scsi.execute(&VERIFY_COMMAND_B, DataDirection::None, &mut dummy, 5_000); - - for _attempt in 0..5 { - if self.do_unlock(scsi).is_ok() { - let _ = self.do_unlock(scsi); - return Ok(()); - } - } - self.do_unlock(scsi)?; - Ok(()) + fn is_ready(&self) -> bool { + self.unlocked } } diff --git a/src/platform/mt1959/variant_a.rs b/src/platform/mt1959/variant_a.rs new file mode 100644 index 0000000..8b82670 --- /dev/null +++ b/src/platform/mt1959/variant_a.rs @@ -0,0 +1,33 @@ +//! MT1959 variant A firmware upload. + +use crate::error::Result; +use crate::scsi::{DataDirection, ScsiTransport}; +use super::Mt1959; + +pub(super) fn load_firmware(mt: &mut Mt1959, scsi: &mut dyn ScsiTransport) -> Result<()> { + let firmware = &mt.profile.firmware; + if firmware.is_empty() { + return Err(crate::error::Error::UnlockFailed { + detail: "no firmware in profile".into(), + }); + } + + let len = firmware.len(); + let cdb = [ + 0x3B, 0x06, 0x00, + 0x00, 0x00, 0x00, + (len >> 16) as u8, (len >> 8) as u8, len as u8, + 0x00, + ]; + let mut data = firmware.clone(); + scsi.execute(&cdb, DataDirection::ToDevice, &mut data, 30_000)?; + + // Verify (may fail, non-fatal) + let verify_cdb = [0x3C, 0x01, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00]; + let mut verify_resp = [0u8; 4]; + let _ = scsi.execute(&verify_cdb, DataDirection::FromDevice, &mut verify_resp, 5_000); + + mt.do_unlock(scsi)?; + mt.do_unlock(scsi)?; + Ok(()) +} diff --git a/src/platform/mt1959/variant_b.rs b/src/platform/mt1959/variant_b.rs new file mode 100644 index 0000000..3adcb4f --- /dev/null +++ b/src/platform/mt1959/variant_b.rs @@ -0,0 +1,52 @@ +//! MT1959 variant B firmware upload. + +use crate::error::Result; +use crate::scsi::{DataDirection, ScsiTransport}; +use super::Mt1959; + +const FIRMWARE_EXTRA: [u8; 16] = [0; 16]; +const VERIFY_COMMAND: [u8; 10] = [0xF1, 0x01, 0x02, 0x00, 0x0D, 0x30, 0x01, 0xF3, 0xAD, 0x23]; + +pub(super) fn load_firmware(mt: &mut Mt1959, scsi: &mut dyn ScsiTransport) -> Result<()> { + let firmware = &mt.profile.firmware; + if firmware.is_empty() { + return Err(crate::error::Error::UnlockFailed { + detail: "no firmware in profile".into(), + }); + } + + // Step 1: MODE SELECT with firmware payload + let write_len = 0x9C0usize.min(firmware.len()); + let mode_select_cdb = [ + 0x55, 0x10, 0x00, + 0x00, 0x00, 0x00, + (write_len >> 16) as u8, (write_len >> 8) as u8, write_len as u8, + 0x00, + ]; + let mut data = firmware[..write_len].to_vec(); + scsi.execute(&mode_select_cdb, DataDirection::ToDevice, &mut data, 30_000)?; + + // Step 2: Read firmware metadata + let read_meta_cdb = [0x3C, 0x06, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x10, 0x00]; + let mut meta_resp = [0u8; 16]; + let _ = scsi.execute(&read_meta_cdb, DataDirection::FromDevice, &mut meta_resp, 5_000); + + // Step 3: Write extra firmware data + let write2_cdb = [0x3B, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00]; + let mut data2 = FIRMWARE_EXTRA.to_vec(); + let _ = scsi.execute(&write2_cdb, DataDirection::ToDevice, &mut data2, 5_000); + + // Step 4: Vendor verify + let mut dummy = [0u8; 0]; + let _ = scsi.execute(&VERIFY_COMMAND, DataDirection::None, &mut dummy, 5_000); + + // Step 5: Unlock retries + for _attempt in 0..5 { + if mt.do_unlock(scsi).is_ok() { + let _ = mt.do_unlock(scsi); + return Ok(()); + } + } + mt.do_unlock(scsi)?; + Ok(()) +} diff --git a/src/speed.rs b/src/speed.rs index 1e3048d..72f2ea3 100644 --- a/src/speed.rs +++ b/src/speed.rs @@ -1,41 +1,141 @@ -//! Drive speed control — query and set read speeds. +//! Drive speed management — zone-based speed table. //! -//! Uses MMC-6 SET CD SPEED (0xBB) command. -//! Reference: MMC-6 §6.30 +//! Every DriveSession has a SpeedTable. Default: max speed everywhere. +//! After init(): calibrated per-zone speeds from disc surface probes. +//! One u32 comparison per read on the hot path. -/// Disc read speed. +/// Speed table — maps disc positions to optimal read speeds. +#[derive(Debug, Clone)] +pub struct SpeedTable { + zones: Vec<(u32, u16)>, // (start_lba, speed_kbs), sorted by lba + current_speed: u16, + next_boundary: u32, +} + +impl SpeedTable { + /// Default: max speed, whole disc. Drive manages itself. + pub fn new() -> Self { + SpeedTable { + zones: vec![(0, 0xFFFF)], + current_speed: 0, // force first SET_CD_SPEED + next_boundary: 0, // force first lookup + } + } + + /// Hot path: has the speed zone changed for this LBA? + /// Returns Some(speed_kbs) only when a SET_CD_SPEED is needed. + #[inline] + pub fn speed_for(&mut self, lba: u32) -> Option { + if lba < self.next_boundary { + return None; + } + self.transition(lba) + } + + /// Zone transition — lookup + precompute next boundary. + fn transition(&mut self, lba: u32) -> Option { + let mut zone_idx = 0; + for (i, &(start, _)) in self.zones.iter().enumerate() { + if start <= lba { + zone_idx = i; + } else { + break; + } + } + + let speed = self.zones[zone_idx].1; + + self.next_boundary = if zone_idx + 1 < self.zones.len() { + self.zones[zone_idx + 1].0 + } else { + u32::MAX + }; + + if speed == self.current_speed { + return None; + } + + self.current_speed = speed; + Some(speed) + } + + /// Load calibrated zones. Converts from platform probe data to generic (lba, kbs). + /// `disc_sectors`: total disc capacity from READ CAPACITY. + /// `probes`: (probe_address, speed_index) pairs from calibration scan. + /// `probe_range`: max probe address space (0x10000 for MT1959). + /// `speed_multiplier`: KB/s per speed unit (4500 for BD 1x). + pub fn load_calibration( + &mut self, + disc_sectors: u32, + probes: &[(u16, u8)], + probe_range: u32, + speed_multiplier: u16, + ) { + if probes.is_empty() || disc_sectors == 0 { + return; + } + + let mut zones: Vec<(u32, u16)> = Vec::new(); + + for &(probe_addr, speed_idx) in probes { + let lba = (probe_addr as u64 * disc_sectors as u64 / probe_range as u64) as u32; + let kbs = speed_idx as u16 * speed_multiplier; + zones.push((lba, kbs)); + } + + zones.sort_by_key(|&(lba, _)| lba); + + // Deduplicate: keep only zone boundaries where speed changes + let mut deduped: Vec<(u32, u16)> = Vec::new(); + for &(lba, kbs) in &zones { + if deduped.last().map_or(true, |&(_, prev_kbs)| prev_kbs != kbs) { + deduped.push((lba, kbs)); + } + } + + if deduped.is_empty() { + return; + } + + self.zones = deduped; + self.current_speed = 0; + self.next_boundary = 0; + } + + /// Temporarily reduce speed for error recovery. + pub fn reduce(&mut self) -> u16 { + let speed = (self.current_speed / 2).max(4500); + self.current_speed = speed; + speed + } + + /// Resume table-driven speed at this LBA. + pub fn resume(&mut self, lba: u32) { + self.current_speed = 0; + self.next_boundary = 0; + self.transition(lba); + } + + /// Current speed in KB/s. + pub fn current(&self) -> u16 { + self.current_speed + } + + /// Number of zones. + pub fn zone_count(&self) -> usize { + self.zones.len() + } +} + +// Keep DriveSpeed enum for CLI display #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum DriveSpeed { - /// Blu-ray 1x = 4,500 KB/s - BD1x, - /// Blu-ray 2x = 9,000 KB/s - BD2x, - /// Blu-ray 4x = 18,000 KB/s - BD4x, - /// Blu-ray 6x = 27,000 KB/s - BD6x, - /// Blu-ray 8x = 36,000 KB/s - BD8x, - /// Blu-ray 10x = 45,000 KB/s - BD10x, - /// Blu-ray 12x = 54,000 KB/s - BD12x, - /// DVD 1x = 1,385 KB/s - DVD1x, - /// DVD 2x = 2,770 KB/s - DVD2x, - /// DVD 4x = 5,540 KB/s - DVD4x, - /// DVD 8x = 11,080 KB/s - DVD8x, - /// DVD 16x = 22,160 KB/s - DVD16x, - /// Maximum speed — drive decides + BD1x, BD2x, BD4x, BD6x, BD8x, BD10x, BD12x, + DVD1x, DVD2x, DVD4x, DVD8x, DVD16x, Max, } impl DriveSpeed { - /// Convert to KB/s for MMC-6 SET CD SPEED command. pub fn to_kbps(self) -> u16 { match self { DriveSpeed::BD1x => 4_500, @@ -53,71 +153,10 @@ impl DriveSpeed { DriveSpeed::Max => 0xFFFF, } } - - /// Create from KB/s value, rounding to nearest standard speed. - pub fn from_kbps(kbps: u16) -> Self { - match kbps { - 0..=2_000 => DriveSpeed::DVD1x, - 2_001..=4_000 => DriveSpeed::DVD2x, - 4_001..=6_000 => DriveSpeed::BD1x, - 6_001..=13_000 => DriveSpeed::BD2x, - 13_001..=22_000 => DriveSpeed::BD4x, - 22_001..=31_000 => DriveSpeed::BD6x, - 31_001..=40_000 => DriveSpeed::BD8x, - 40_001..=49_000 => DriveSpeed::BD10x, - 49_001..=u16::MAX => DriveSpeed::BD12x, - } - } - - /// Human-readable label. - pub fn label(&self) -> &'static str { - match self { - DriveSpeed::BD1x => "BD 1x", - DriveSpeed::BD2x => "BD 2x", - DriveSpeed::BD4x => "BD 4x", - DriveSpeed::BD6x => "BD 6x", - DriveSpeed::BD8x => "BD 8x", - DriveSpeed::BD10x => "BD 10x", - DriveSpeed::BD12x => "BD 12x", - DriveSpeed::DVD1x => "DVD 1x", - DriveSpeed::DVD2x => "DVD 2x", - DriveSpeed::DVD4x => "DVD 4x", - DriveSpeed::DVD8x => "DVD 8x", - DriveSpeed::DVD16x => "DVD 16x", - DriveSpeed::Max => "Max", - } - } - - /// All standard Blu-ray speeds. - pub fn all_bd() -> &'static [DriveSpeed] { - &[DriveSpeed::BD1x, DriveSpeed::BD2x, DriveSpeed::BD4x, - DriveSpeed::BD6x, DriveSpeed::BD8x, DriveSpeed::BD10x, DriveSpeed::BD12x] - } - - /// All standard DVD speeds. - pub fn all_dvd() -> &'static [DriveSpeed] { - &[DriveSpeed::DVD1x, DriveSpeed::DVD2x, DriveSpeed::DVD4x, - DriveSpeed::DVD8x, DriveSpeed::DVD16x] - } } impl std::fmt::Display for DriveSpeed { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{} ({} KB/s)", self.label(), self.to_kbps()) + write!(f, "{:?} ({} KB/s)", self, self.to_kbps()) } } - -/// Build SET CD SPEED CDB — MMC-6 §6.30 -pub fn set_cd_speed_cdb(read_speed: DriveSpeed) -> [u8; 12] { - let kbps = read_speed.to_kbps(); - [ - 0xBB, // SET CD SPEED opcode - 0x00, // reserved - (kbps >> 8) as u8, // read speed MSB - kbps as u8, // read speed LSB - 0xFF, // write speed MSB (0xFFFF = don't change) - 0xFF, // write speed LSB - 0x00, 0x00, 0x00, 0x00, // reserved - 0x00, 0x00, // reserved - ] -}