diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..3ff5bed --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Markdown personal settings +/markdown.xml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/cdrustanoia.iml b/.idea/cdrustanoia.iml new file mode 100644 index 0000000..659704d --- /dev/null +++ b/.idea/cdrustanoia.iml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..d2a23af --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e5860a1..7edfa4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,7 +10,7 @@ checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "cdda_interface" -version = "0.1.0" +version = "26.0.1" dependencies = [ "cdrustanoia_capi_types", "cdrustanoia_core", @@ -19,7 +19,7 @@ dependencies = [ [[package]] name = "cdda_paranoia" -version = "0.1.0" +version = "26.0.1" dependencies = [ "cdda_interface", "cdrustanoia_capi_types", @@ -29,14 +29,14 @@ dependencies = [ [[package]] name = "cdrustanoia_capi_types" -version = "0.1.0" +version = "26.0.1" dependencies = [ "libc", ] [[package]] name = "cdrustanoia_cli" -version = "0.1.0" +version = "26.0.1" dependencies = [ "cdrustanoia_core", "libc", @@ -45,7 +45,7 @@ dependencies = [ [[package]] name = "cdrustanoia_core" -version = "0.1.0" +version = "26.0.1" dependencies = [ "libc", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index 714a601..0226ed9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "0.1.0" +version = "26.0.1" edition = "2021" license = "LGPL-2.1-or-later" @@ -20,4 +20,5 @@ cdrustanoia_capi_types = { path = "crates/capi-types" } [profile.release] lto = true +opt-level = 3 codegen-units = 1 diff --git a/README.md b/README.md index 329f064..8408329 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,20 @@ cdrustanoia -r -- "-3" out.raw Use `cdrustanoia --help` for the full option and span documentation. The man page is the original cdparanoia man page. +## Benchmarks + +The core crate contains a small synthetic benchmark for the paranoia read path +and the sample index: + +```sh +cargo run --release -p cdrustanoia_core --example bench_paranoia -- 5000 +``` + +It reports index setup/query times, a comparison with the old `HashMap` +index, and overall synthetic read throughput. The image backend is +memory-backed, so the absolute numbers are useful for regression tracking +rather than as a prediction of physical drive speed. + ## C API The two C libraries export the traditional symbols and use the original diff --git a/crates/cdda-interface/src/lib.rs b/crates/cdda-interface/src/lib.rs index a0a11c4..316f24a 100644 --- a/crates/cdda-interface/src/lib.rs +++ b/crates/cdda-interface/src/lib.rs @@ -17,6 +17,13 @@ pub struct BackendHolder { pub drive: Box, } +fn ffi_guard(default: T, f: impl FnOnce() -> T) -> T { + match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) { + Ok(value) => value, + Err(_) => default, + } +} + unsafe fn cstr(s: &str) -> *mut c_char { let clean: String = s.chars().filter(|c| *c != '\0').collect(); match CString::new(clean) { @@ -212,6 +219,16 @@ pub unsafe extern "C" fn cdda_identify( device: *const c_char, messagedest: c_int, messages: *mut *mut c_char, +) -> *mut CdromDrive { + ffi_guard(ptr::null_mut(), || { + cdda_identify_impl(device, messagedest, messages) + }) +} + +unsafe fn cdda_identify_impl( + device: *const c_char, + messagedest: c_int, + messages: *mut *mut c_char, ) -> *mut CdromDrive { if !messages.is_null() { *messages = ptr::null_mut(); @@ -234,6 +251,16 @@ pub unsafe extern "C" fn cdda_identify_cooked( device: *const c_char, messagedest: c_int, messages: *mut *mut c_char, +) -> *mut CdromDrive { + ffi_guard(ptr::null_mut(), || { + cdda_identify_cooked_impl(device, messagedest, messages) + }) +} + +unsafe fn cdda_identify_cooked_impl( + device: *const c_char, + messagedest: c_int, + messages: *mut *mut c_char, ) -> *mut CdromDrive { if !messages.is_null() { *messages = ptr::null_mut(); @@ -257,6 +284,17 @@ pub unsafe extern "C" fn cdda_identify_scsi( ioctl_device: *const c_char, messagedest: c_int, messages: *mut *mut c_char, +) -> *mut CdromDrive { + ffi_guard(ptr::null_mut(), || { + cdda_identify_scsi_impl(generic_device, ioctl_device, messagedest, messages) + }) +} + +unsafe fn cdda_identify_scsi_impl( + generic_device: *const c_char, + ioctl_device: *const c_char, + messagedest: c_int, + messages: *mut *mut c_char, ) -> *mut CdromDrive { if !messages.is_null() { *messages = ptr::null_mut(); @@ -289,6 +327,15 @@ pub unsafe extern "C" fn cdda_identify_scsi( pub unsafe extern "C" fn cdda_find_a_cdrom( messagedest: c_int, messages: *mut *mut c_char, +) -> *mut CdromDrive { + ffi_guard(ptr::null_mut(), || { + cdda_find_a_cdrom_impl(messagedest, messages) + }) +} + +unsafe fn cdda_find_a_cdrom_impl( + messagedest: c_int, + messages: *mut *mut c_char, ) -> *mut CdromDrive { if !messages.is_null() { *messages = ptr::null_mut(); @@ -301,6 +348,10 @@ pub unsafe extern "C" fn cdda_find_a_cdrom( #[no_mangle] pub unsafe extern "C" fn cdda_open(d: *mut CdromDrive) -> c_int { + ffi_guard(-100, || cdda_open_impl(d)) +} + +unsafe fn cdda_open_impl(d: *mut CdromDrive) -> c_int { if d.is_null() { return -100; } @@ -343,6 +394,10 @@ pub unsafe extern "C" fn cdda_open(d: *mut CdromDrive) -> c_int { #[no_mangle] pub unsafe extern "C" fn cdda_close(d: *mut CdromDrive) -> c_int { + ffi_guard(0, || cdda_close_impl(d)) +} + +unsafe fn cdda_close_impl(d: *mut CdromDrive) -> c_int { if d.is_null() { return 0; } @@ -357,6 +412,18 @@ pub unsafe extern "C" fn cdda_read_timed( beginsector: c_long, sectors: c_long, ms: *mut c_int, +) -> c_long { + ffi_guard(-1, || { + cdda_read_timed_impl(d, buffer, beginsector, sectors, ms) + }) +} + +unsafe fn cdda_read_timed_impl( + d: *mut CdromDrive, + buffer: *mut c_void, + beginsector: c_long, + sectors: c_long, + ms: *mut c_int, ) -> c_long { if !ms.is_null() { *ms = -1; @@ -422,6 +489,10 @@ pub unsafe extern "C" fn cdda_read( #[no_mangle] pub unsafe extern "C" fn cdda_speed_set(d: *mut CdromDrive, speed: c_int) -> c_int { + ffi_guard(-405, || cdda_speed_set_impl(d, speed)) +} + +unsafe fn cdda_speed_set_impl(d: *mut CdromDrive, speed: c_int) -> c_int { match holder(d) { Some(h) => { let ret = h.drive.speed_set(speed); @@ -569,3 +640,81 @@ pub unsafe extern "C" fn cdda_track_bitmap( // Keep the c_long import used on all targets. #[allow(dead_code)] fn _typecheck(_: c_long) {} + +#[cfg(test)] +mod tests { + use super::*; + use cdrustanoia_core::device::{DiscToc, InterfaceKind, ReadError, TimedRead}; + + struct PanicDrive { + toc: DiscToc, + } + + impl Drive for PanicDrive { + fn toc(&self) -> &DiscToc { + &self.toc + } + fn toc_mut(&mut self) -> &mut DiscToc { + &mut self.toc + } + fn opened(&self) -> bool { + false + } + fn open_disc(&mut self) -> Result<(), String> { + panic!("intentional test panic"); + } + fn read_audio( + &mut self, + _buffer: Option<&mut [u8]>, + _begin: i64, + _sectors: i64, + ) -> Result { + Err(ReadError::closed()) + } + fn read_timed( + &mut self, + _buffer: Option<&mut [u8]>, + _begin: i64, + _sectors: i64, + ) -> TimedRead { + TimedRead { + result: Err(ReadError::closed()), + milliseconds: -1, + } + } + fn speed_set(&mut self, _speed: i32) -> i32 { + -405 + } + fn close(&mut self) {} + fn drive_model(&self) -> &str { + "panic drive" + } + fn bigendianp(&self) -> i32 { + 0 + } + fn set_bigendianp(&mut self, _value: i32) {} + fn nsectors(&self) -> i64 { + 1 + } + fn set_nsectors(&mut self, _value: i64) {} + fn interface(&self) -> InterfaceKind { + InterfaceKind::SgIo + } + } + + #[test] + fn c_api_catches_panics() { + unsafe { + let d = make_drive( + "panic", + None, + Box::new(PanicDrive { + toc: DiscToc::default(), + }), + ); + assert!(!d.is_null()); + assert_eq!(cdda_open(d), -100); + assert_eq!(cdda_close(d), 0); + } + } +} diff --git a/crates/cdda-paranoia/src/lib.rs b/crates/cdda-paranoia/src/lib.rs index e2e9a46..720e8a8 100644 --- a/crates/cdda-paranoia/src/lib.rs +++ b/crates/cdda-paranoia/src/lib.rs @@ -19,6 +19,13 @@ use std::time::Instant; pub type ParanoiaCallback = Option; +fn ffi_guard(default: T, f: impl FnOnce() -> T) -> T { + match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) { + Ok(value) => value, + Err(_) => default, + } +} + struct CAbiDrive { d: *mut CdromDrive, toc: DiscToc, @@ -190,6 +197,10 @@ pub unsafe extern "C" fn paranoia_version() -> *const c_char { #[no_mangle] pub unsafe extern "C" fn paranoia_init(d: *mut CdromDrive) -> *mut c_void { + ffi_guard(ptr::null_mut(), || paranoia_init_impl(d)) +} + +unsafe fn paranoia_init_impl(d: *mut CdromDrive) -> *mut c_void { if d.is_null() { return ptr::null_mut(); } @@ -201,6 +212,10 @@ pub unsafe extern "C" fn paranoia_init(d: *mut CdromDrive) -> *mut c_void { #[no_mangle] pub unsafe extern "C" fn paranoia_free(p: *mut c_void) { + ffi_guard((), || paranoia_free_impl(p)) +} + +unsafe fn paranoia_free_impl(p: *mut c_void) { if !p.is_null() { drop(Box::from_raw(p as *mut Paranoia)); } @@ -246,6 +261,16 @@ pub unsafe extern "C" fn paranoia_read_limited( p: *mut c_void, callback: ParanoiaCallback, max_retries: c_int, +) -> *mut i16 { + ffi_guard(ptr::null_mut(), || { + paranoia_read_limited_impl(p, callback, max_retries) + }) +} + +unsafe fn paranoia_read_limited_impl( + p: *mut c_void, + callback: ParanoiaCallback, + max_retries: c_int, ) -> *mut i16 { let p = match p_ref(p) { Some(p) => p, diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 885c2f0..67ba5a9 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -22,19 +22,20 @@ use std::os::unix::fs::OpenOptionsExt; use std::os::unix::io::FromRawFd; use std::path::Path; -const MANUAL: &str = r#"cdparanoia III release 10.2 (September 11, 2008) +const MANUAL: &str = r#"cdrustanoia 26.0.1 (2026-11-03) +based on the great cdparanoia by Monty and Xiph.Org -(C) 2008 Monty and Xiph.Org +(C) 2026 Martin Wohlert + +Report bugs to martin.wohlert@b-root-force.de -Report bugs to paranoia@xiph.org -http://www.xiph.org/paranoia/ USAGE: - cdparanoia [options] [outfile] + cdrustanoia [options] [outfile] OPTIONS: -A --analyze-drive : run and log a complete analysis of drive caching, timing and reading behavior; - verifies that cdparanoia is correctly + verifies that cdrustanoia is correctly modelling a sprcific drive's cache and read behavior. Implies -vQL -v --verbose : extra verbose operation @@ -42,10 +43,10 @@ OPTIONS: -e --stderr-progress : force output of progress information to stderr (for wrapper scripts) -l --log-summary [] : save result summary to file, default - filename cdparanoia.log + filename cdrustanoia.log -L --log-debug [] : save detailed device autosense and debugging output to file, default - filename cdparanoia.log + filename cdrustanoia.log -V --version : print version info and quit -Q --query : autosense drive, query disc and quit -B --batch : 'batch' mode (saves each track to a @@ -77,7 +78,7 @@ OPTIONS: force use of the old SG kernel interface. -g cannot be used with -k. -S --force-read-speed : read from device at specified speed; by - default, cdparanoia sets drive to full + default, cdrustanoia sets drive to full speed. -t --toc-offset : Add sectors to the values reported when addressing tracks. May be negative @@ -154,16 +155,14 @@ the shell... A few examples, protected from the shell: A) query only with exhaustive search for a drive and full reporting of autosense: - cdparanoia -vsQ + cdrustanoia -vsQ B) extract up to and including track 3, putting each track in a seperate file: - cdparanoia -B -- "-3" + cdrustanoia -B -- "-3" C) extract from track 1, time 0:30.12 to 1:10.00: - cdparanoia "1[:30.12]-1[1:10]" - -Submit bug reports to paranoia@xiph.org + cdrustanoia "1[:30.12]-1[1:10]" "#; @@ -283,17 +282,6 @@ fn display_toc(d: &dyn Drive, log: &mut Log) { log.report(format_args!(" ")); } -fn read_sector( - p: &mut Paranoia, - progress: &mut Progress, - log: &mut Log, - max_retries: i32, -) -> Option> { - let mut callback = |pos: i64, func: i32| progress.callback(pos, func, log); - p.read_limited(&mut callback, max_retries) - .map(|s| s.to_vec()) -} - fn samples_as_bytes(samples: &[i16]) -> &[u8] { unsafe { std::slice::from_raw_parts( @@ -708,7 +696,11 @@ of the machine.\n\nMore information about /dev/cdrom:" progress.skipped_flag = false; while cursor <= batch_last { - let sector = match read_sector(&mut p, &mut progress, &mut log, args.max_retries) { + let sector = { + let mut callback = |pos: i64, func: i32| progress.callback(pos, func, &mut log); + p.read_limited(&mut callback, args.max_retries) + }; + let data = match sector { Some(s) => s, None => { progress.skipped_flag = true; @@ -725,23 +717,32 @@ of the machine.\n\nMore information about /dev/cdrom:" progress.skipped_flag = false; cursor += 1; - let mut data = sector; if target_big != host_big { - for s in &mut data { + for s in data.iter_mut() { *s = swap16(*s); } } progress.callback(cursor * 1176 - 1, -2, &mut log); - let data_bytes = samples_as_bytes(&data); + let data_bytes = samples_as_bytes(data); if let Err(e) = out.write_all(&data_bytes[offset_skip.min(data_bytes.len())..]) { log.report(format_args!("Error writing output: {}", e)); return 1; } offset_skip = 0; + if target_big != host_big { + for s in data.iter_mut() { + *s = swap16(*s); + } + } + if sample_offset != 0 && cursor > batch_last { - let extra = match read_sector(&mut p, &mut progress, &mut log, args.max_retries) { + let extra = { + let mut callback = |pos: i64, func: i32| progress.callback(pos, func, &mut log); + p.read_limited(&mut callback, args.max_retries) + }; + let extra = match extra { Some(s) => s, None => { progress.skipped_flag = true; @@ -756,14 +757,18 @@ of the machine.\n\nMore information about /dev/cdrom:" break; } progress.skipped_flag = false; - let mut extra = extra; if target_big != host_big { - for s in &mut extra { + for s in extra.iter_mut() { *s = swap16(*s); } } - let eb = samples_as_bytes(&extra); + let eb = samples_as_bytes(extra); offset_buffer[..eb.len().min(2352)].copy_from_slice(&eb[..eb.len().min(2352)]); + if target_big != host_big { + for s in extra.iter_mut() { + *s = swap16(*s); + } + } offset_buffer_used = (sample_offset as usize * 4).min(2352); progress.callback(cursor * 1176, -2, &mut log); if let Err(e) = out.write_all(&offset_buffer[..offset_buffer_used]) { diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index ec17ba5..e08190f 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -3,6 +3,7 @@ name = "cdrustanoia_core" version.workspace = true edition.workspace = true license = "LGPL-2.1-or-later" +publish = false [dependencies] libc.workspace = true diff --git a/crates/core/examples/bench_paranoia.rs b/crates/core/examples/bench_paranoia.rs new file mode 100644 index 0000000..c835696 --- /dev/null +++ b/crates/core/examples/bench_paranoia.rs @@ -0,0 +1,168 @@ +//! Small benchmark harness for the paranoia read path and sample index. +//! +//! Run with: +//! cargo run --release --example bench_paranoia +//! cargo run --release --example bench_paranoia -- 5000 + +use cdrustanoia_core::device::image::ImageDrive; +use cdrustanoia_core::device::samples_to_bytes; +use cdrustanoia_core::paranoia::sort::SortIndex; +use cdrustanoia_core::paranoia::{Paranoia, PARANOIA_MODE_FULL}; +use std::collections::HashMap; +use std::time::Instant; + +fn make_samples(sectors: usize) -> Vec { + let mut out = Vec::with_capacity(sectors * 1176); + let mut x = 0x1234_5678_9abc_def0u64; + for i in 0..sectors * 1176 { + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + let v = if i % 5000 < 700 { + 0 + } else { + (x as i16).wrapping_add((i as i16).wrapping_mul(11)) + }; + out.push(v); + } + out +} + +fn bench_index(samples: &[i16]) { + let mut index = SortIndex::new(samples.len()); + let started = Instant::now(); + index.setup(samples, 0, 0, samples.len() as i64); + let setup = started.elapsed(); + + let mut found = 0usize; + let started = Instant::now(); + for i in 0..10_000i64 { + let post = (i * 7919) % samples.len() as i64; + let value = samples[post as usize]; + if index.getmatch(samples, post, 512, value).is_some() { + found += 1; + while index.nextmatch(samples).is_some() { + found += 1; + } + } + } + let queries = started.elapsed(); + println!( + "SortIndex: setup={:?} 10k queries={:?} matches={} ({} samples)", + setup, + queries, + found, + samples.len() + ); +} + +struct OldIndex { + buckets: HashMap>, + size: usize, + cursor: Option<(i16, usize, usize)>, +} + +impl OldIndex { + fn new() -> Self { + Self { + buckets: HashMap::new(), + size: 0, + cursor: None, + } + } + fn setup(&mut self, samples: &[i16]) { + self.buckets.clear(); + self.size = samples.len(); + for (i, &sample) in samples.iter().enumerate() { + self.buckets.entry(sample).or_default().push(i as u32); + } + } + fn getmatch(&mut self, post: i64, overlap: i64, value: i16) -> Option { + let post = post.clamp(0, self.size as i64); + let lo = (post - overlap).max(0) as usize; + let hi = ((post + overlap).min(self.size as i64)).max(0) as usize; + self.cursor = None; + let positions = self.buckets.get(&value)?; + let idx = positions.partition_point(|p| (*p as usize) < lo); + if idx >= positions.len() { + return None; + } + let pos = positions[idx] as usize; + if pos >= hi { + return None; + } + self.cursor = Some((value, idx + 1, hi)); + Some(pos) + } + fn nextmatch(&mut self) -> Option { + let (value, idx, hi) = self.cursor?; + let positions = self.buckets.get(&value)?; + if idx >= positions.len() { + self.cursor = None; + return None; + } + let pos = positions[idx] as usize; + if pos >= hi { + self.cursor = None; + return None; + } + self.cursor = Some((value, idx + 1, hi)); + Some(pos) + } +} + +fn bench_old_index(samples: &[i16]) { + let mut index = OldIndex::new(); + let started = Instant::now(); + index.setup(samples); + let setup = started.elapsed(); + let mut found = 0usize; + let started = Instant::now(); + for i in 0..10_000i64 { + let post = (i * 7919) % samples.len() as i64; + let value = samples[post as usize]; + if index.getmatch(post, 512, value).is_some() { + found += 1; + while index.nextmatch().is_some() { + found += 1; + } + } + } + let queries = started.elapsed(); + println!( + "Old HashMap index: setup={:?} 10k queries={:?} matches={}", + setup, queries, found + ); +} + +fn main() { + let sectors: usize = std::env::args() + .nth(1) + .and_then(|s| s.parse().ok()) + .unwrap_or(2000); + let samples = make_samples(sectors); + bench_index(&samples); + bench_old_index(&samples); + + let bytes = samples_to_bytes(&samples); + let drive = ImageDrive::from_bytes(bytes); + let mut p = Paranoia::new(Box::new(drive)); + p.modeset(PARANOIA_MODE_FULL); + let mut cb = |_: i64, _: i32| {}; + let started = Instant::now(); + let mut got = 0usize; + for _ in 0..sectors { + let sector = p.read(&mut cb).expect("read sector"); + got += sector.len(); + } + let elapsed = started.elapsed(); + let bytes_read = (got * 2) as f64; + println!( + "Read: {} sectors, {} samples, {:?}, {:.1} MB/s", + sectors, + got, + elapsed, + bytes_read / elapsed.as_secs_f64() / 1_000_000.0 + ); + assert_eq!(got, sectors * 1176); +} diff --git a/crates/core/src/device/image.rs b/crates/core/src/device/image.rs index c97b606..8426770 100644 --- a/crates/core/src/device/image.rs +++ b/crates/core/src/device/image.rs @@ -192,7 +192,13 @@ impl ImageDrive { fn read_words(&mut self, begin_sector: i64, sectors: i64) -> Vec { let words = sectors.max(0) * CD_FRAMEWORDS; - let mut out = Vec::with_capacity(words as usize); + let mut out = vec![0i16; words as usize]; + self.read_words_into(begin_sector, sectors, &mut out); + out + } + + fn read_words_into(&mut self, begin_sector: i64, sectors: i64, out: &mut [i16]) { + let words = sectors.max(0) * CD_FRAMEWORDS; let mut jitter = 0i64; if self.faults.jitter_words > 0 { self.read_count = self.read_count.wrapping_add(1); @@ -211,10 +217,9 @@ impl ImageDrive { } } let start = begin_sector * CD_FRAMEWORDS + jitter; - for w in 0..words { - out.push(self.mapped_sample(start + w).unwrap_or(0)); + for (w, slot) in out.iter_mut().enumerate().take(words as usize) { + *slot = self.mapped_sample(start + w as i64).unwrap_or(0); } - out } } @@ -281,6 +286,32 @@ impl Drive for ImageDrive { self.last_begin = begin; Ok(sectors as usize) } + fn read_audio_samples( + &mut self, + buffer: &mut [i16], + begin: i64, + sectors: i64, + ) -> Result { + if !self.opened { + return Err(ReadError::closed()); + } + if sectors <= 0 { + return Ok(0); + } + let mut sectors = sectors; + if self.faults.max_read_sectors > 0 { + sectors = sectors.min(self.faults.max_read_sectors); + } + let out_len = (sectors * CD_FRAMEWORDS) as usize; + self.read_words_into(begin, sectors, buffer); + if super::host_big_endian() { + for sample in buffer.iter_mut().take(out_len) { + *sample = super::swap16(*sample); + } + } + self.last_begin = begin; + Ok(sectors as usize) + } fn read_timed(&mut self, buffer: Option<&mut [u8]>, begin: i64, sectors: i64) -> TimedRead { let start = Instant::now(); let previous_begin = self.last_begin; diff --git a/crates/core/src/device/linux.rs b/crates/core/src/device/linux.rs index 2a0d326..0bc6db7 100644 --- a/crates/core/src/device/linux.rs +++ b/crates/core/src/device/linux.rs @@ -178,6 +178,7 @@ pub struct LinuxDrive { fua: bool, read_cmd: ReadCommand, enable_mode: EnableMode, + last_milliseconds: i32, } impl LinuxDrive { @@ -199,6 +200,7 @@ impl LinuxDrive { fua: false, read_cmd: ReadCommand::D8, enable_mode: EnableMode::Dummy, + last_milliseconds: 0, }; d.inquiry()?; d.check_atapi(); @@ -238,6 +240,7 @@ impl LinuxDrive { fua: false, read_cmd: ReadCommand::Read28, enable_mode: EnableMode::Dummy, + last_milliseconds: 0, }; Ok(d) } @@ -561,7 +564,9 @@ impl LinuxDrive { duration: 0, info: 0, }; + let started = Instant::now(); let rc = unsafe { libc::ioctl(self.fd, SG_IO, &mut hdr as *mut SgIoHdr) }; + self.last_milliseconds = started.elapsed().as_millis() as i32; if rc < 0 { return Err(ScsiError::io(io::Error::last_os_error())); } @@ -819,7 +824,9 @@ impl LinuxDrive { nframes: requested as c_int, buf: buf.as_mut_ptr(), }; + let started = Instant::now(); let rc = unsafe { libc::ioctl(self.fd, CDROMREADAUDIO, &mut arg as *mut _) }; + self.last_milliseconds = started.elapsed().as_millis() as i32; if rc == 0 { return Ok(requested as usize); } @@ -1211,12 +1218,11 @@ impl Drive for LinuxDrive { self.read_raw(buffer, begin, sectors) } fn read_timed(&mut self, buffer: Option<&mut [u8]>, begin: i64, sectors: i64) -> TimedRead { - let started = Instant::now(); + self.last_milliseconds = -1; let result = self.read_audio(buffer, begin, sectors); - let elapsed = started.elapsed().as_millis() as i32; TimedRead { result, - milliseconds: elapsed, + milliseconds: self.last_milliseconds, } } fn speed_set(&mut self, speed: i32) -> i32 { diff --git a/crates/core/src/device/mod.rs b/crates/core/src/device/mod.rs index 249ecfd..d6ae905 100644 --- a/crates/core/src/device/mod.rs +++ b/crates/core/src/device/mod.rs @@ -275,6 +275,31 @@ pub trait Drive { begin: i64, sectors: i64, ) -> Result; + + /// Read audio directly into a host-endian `i16` buffer. + /// + /// This is a byte-view over the same memory; it lets the paranoia engine + /// read straight into the `CBlock` vector without an intermediate + /// `Vec` plus `bytes_to_samples()` copy. Implementations of + /// `read_audio()` already promise host endian PCM data. + fn read_audio_samples( + &mut self, + buffer: &mut [i16], + begin: i64, + sectors: i64, + ) -> Result { + // SAFETY: `i16` has no padding and the byte view covers exactly the + // initialized elements of `buffer`. The view lives only for this + // call, and all bytes are written by the backend before it returns. + let bytes = unsafe { + std::slice::from_raw_parts_mut( + buffer.as_mut_ptr().cast::(), + std::mem::size_of_val(buffer), + ) + }; + self.read_audio(Some(bytes), begin, sectors) + } + /// Low-level read without host-endian conversion, matching the original /// `cdrom_drive::read_audio` function pointer. fn read_audio_raw( diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 45f86b3..615ab7d 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -15,8 +15,8 @@ pub mod callback; pub mod device; pub mod paranoia; -pub const VERSIONNUM: &str = "10.2"; -pub const VERSION: &str = "cdparanoia III release 10.2 (September 11, 2008)"; +pub const VERSIONNUM: &str = "26.0.1"; +pub const VERSION: &str = "cdrustanoia 26.0.1 (2026-11-03)"; pub fn paranoia_version() -> &'static str { VERSIONNUM diff --git a/crates/core/src/paranoia/block.rs b/crates/core/src/paranoia/block.rs index 9633463..02a707c 100644 --- a/crates/core/src/paranoia/block.rs +++ b/crates/core/src/paranoia/block.rs @@ -1,12 +1,14 @@ //! c_block/v_fragment/root data structures and vector operations. +use std::sync::Arc; + pub const FLAGS_EDGE: u8 = 0x1; pub const FLAGS_UNREAD: u8 = 0x2; pub const FLAGS_VERIFIED: u8 = 0x4; #[derive(Clone, Debug)] pub struct CBlock { - pub vector: Vec, + pub vector: Arc>, pub flags: Option>, pub begin: i64, pub size: i64, @@ -22,7 +24,7 @@ impl Default for CBlock { impl CBlock { pub fn new() -> Self { Self { - vector: Vec::new(), + vector: Arc::new(Vec::new()), flags: None, begin: 0, size: 0, @@ -33,7 +35,7 @@ impl CBlock { pub fn from_parts(vector: Vec, begin: i64, lastsector: bool) -> Self { let size = vector.len() as i64; Self { - vector, + vector: Arc::new(vector), flags: None, begin, size, @@ -62,7 +64,7 @@ impl CBlock { return; } let pos = pos as usize; - self.vector.splice(pos..pos, data.iter().copied()); + Arc::make_mut(&mut self.vector).splice(pos..pos, data.iter().copied()); self.size += data.len() as i64; if let Some(flags) = self.flags.as_mut() { let n = data.len(); @@ -87,7 +89,7 @@ impl CBlock { } let pos = cutpos as usize; let n = cutsize as usize; - self.vector.drain(pos..pos + n); + Arc::make_mut(&mut self.vector).drain(pos..pos + n); self.size -= cutsize; if let Some(flags) = self.flags.as_mut() { let pos = pos.min(flags.len()); @@ -109,11 +111,11 @@ impl CBlock { } let p = pos as usize; let n = size as usize; - self.vector[p..p + n].copy_from_slice(&data[..n]); + Arc::make_mut(&mut self.vector)[p..p + n].copy_from_slice(&data[..n]); } pub fn append(&mut self, data: &[i16]) { - self.vector.extend_from_slice(data); + Arc::make_mut(&mut self.vector).extend_from_slice(data); self.size += data.len() as i64; if let Some(flags) = self.flags.as_mut() { flags.extend(std::iter::repeat_n(0, data.len())); @@ -125,7 +127,7 @@ impl CBlock { return; } let n = (cut as usize).min(self.vector.len()); - self.vector.drain(..n); + Arc::make_mut(&mut self.vector).drain(..n); self.size -= n as i64; self.begin += n as i64; if let Some(flags) = self.flags.as_mut() { @@ -148,7 +150,10 @@ impl CBlock { #[derive(Clone, Debug)] pub struct VFragment { - pub vector: Vec, + /// Shared backing data. Cache blocks and all fragments carved from them + /// reference the same allocation; no per-fragment copy is made. + pub data: Arc>, + pub offset: usize, pub begin: i64, pub size: i64, pub lastsector: bool, @@ -158,6 +163,10 @@ impl VFragment { pub fn end(&self) -> i64 { self.begin + self.size } + + pub fn vector(&self) -> &[i16] { + &self.data[self.offset..self.offset + self.size as usize] + } } #[derive(Clone, Debug, Default)] diff --git a/crates/core/src/paranoia/mod.rs b/crates/core/src/paranoia/mod.rs index 2df4b55..e7d5f72 100644 --- a/crates/core/src/paranoia/mod.rs +++ b/crates/core/src/paranoia/mod.rs @@ -8,6 +8,7 @@ use crate::device::{Drive, CD_FRAMESIZE_RAW, CD_FRAMEWORDS}; use block::*; use sort::SortIndex; use std::collections::VecDeque; +use std::sync::Arc; pub const MIN_WORDS_OVERLAP: i64 = 64; pub const MIN_WORDS_SEARCH: i64 = 64; @@ -61,6 +62,8 @@ pub struct Paranoia { pub dynoverlap: i64, pub dyndrift: i64, stage2_adjust_pending: bool, + sample_pool: Vec>, + flag_pool: Vec>, } // --------------------------------------------------------------------------- @@ -329,7 +332,7 @@ fn try_sort_sync( // Search for a matching sample within dynoverlap. let search_post = post - a_begin; if sort - .getmatch(search_post, *dynoverlap, b.vector[b_idx as usize]) + .getmatch(a_vector, search_post, *dynoverlap, b.vector[b_idx as usize]) .is_some() { loop { @@ -356,7 +359,7 @@ fn try_sort_sync( *end = bend; return true; } - if !sort.advance() { + if !sort.advance(a_vector) { break; } } @@ -732,7 +735,7 @@ fn iterate_stage2( callback(v.begin, PARANOIA_CB_VERIFY); let mut fbv = v.begin.max(root.begin - *dynoverlap); - while fbv < v.end() && v.vector[(fbv - v.begin) as usize] == 0 { + while fbv < v.end() && v.vector()[(fbv - v.begin) as usize] == 0 { fbv += 1; } if fbv == v.end() { @@ -744,7 +747,7 @@ fn iterate_stage2( .min(v.end()); let searchend = (fev + *dynoverlap).min(root.end()); let searchbegin = (fbv - *dynoverlap).max(root.begin); - sort.setup(&v.vector, v.begin, fbv, fev); + sort.setup(v.vector(), v.begin, fbv, fev); let mut j = searchbegin; while j < searchend { @@ -759,7 +762,7 @@ fn iterate_stage2( let mut offset = 0; if try_sort_sync( sort, - &v.vector, + v.vector(), v.begin, v.size, None, @@ -827,7 +830,7 @@ fn silence_match( return false; } let mut j = 0; - while j < end && v.vector[j as usize] == 0 { + while j < end && v.vector()[j as usize] == 0 { j += 1; } if j < MIN_SILENCE_BOUNDARY { @@ -849,7 +852,7 @@ fn silence_match( if v.end() > root.end() { let voff = begin - v.begin; root.remove(begin - root.begin, root.size - (begin - root.begin)); - root.append(&v.vector[voff as usize..]); + root.append(&v.vector()[voff as usize..]); } add_stage2_value(stage2, 0, stage2_pending); } else if j < begin { @@ -859,7 +862,7 @@ fn silence_match( *silence_begin - root.begin, root.size - (*silence_begin - root.begin), ); - root.append(&v.vector[voff as usize..]); + root.append(&v.vector()[voff as usize..]); } add_stage2_value(stage2, end - begin, stage2_pending); } else { @@ -907,7 +910,7 @@ fn stage2_each( callback: &mut dyn FnMut(i64, i32), ) -> MergeOutcome { let dyno = *dynoverlap / 2 * 2; - if v.size <= 0 || v.vector.is_empty() { + if v.size <= 0 || v.vector().is_empty() { return MergeOutcome::Discarded; } @@ -931,7 +934,11 @@ fn stage2_each( while begin + offset > 0 && begin > 0 { let begin_l = begin + offset; if scratch.is_none() { - scratch = Some(CBlock::from_parts(v.vector.clone(), v.begin, v.lastsector)); + scratch = Some(CBlock::from_parts( + v.vector().to_vec(), + v.begin, + v.lastsector, + )); } let (mut match_a, mut match_b, mut match_c) = (0, 0, 0); { @@ -1021,7 +1028,11 @@ fn stage2_each( let begin_l = begin + offset; let end_l = end + offset; if scratch.is_none() { - scratch = Some(CBlock::from_parts(v.vector.clone(), v.begin, v.lastsector)); + scratch = Some(CBlock::from_parts( + v.vector().to_vec(), + v.begin, + v.lastsector, + )); } let (mut match_a, mut match_b, mut match_c) = (0, 0, 0); { @@ -1130,7 +1141,7 @@ fn stage2_each( } let data: Vec = match scratch.as_ref() { Some(l) => l.vector[(end + offset) as usize..size_b as usize].to_vec(), - None => v.vector[(end + offset) as usize..size_b as usize].to_vec(), + None => v.vector()[(end + offset) as usize..size_b as usize].to_vec(), }; if !data.is_empty() { root.append(&data); @@ -1183,6 +1194,8 @@ impl Paranoia { dynoverlap: MAX_SECTOR_OVERLAP * CD_FRAMEWORDS, dyndrift: 0, stage2_adjust_pending: false, + sample_pool: Vec::new(), + flag_pool: Vec::new(), }; p.firstlast(); p @@ -1212,6 +1225,41 @@ impl Paranoia { ret } + fn take_sample_buf(&mut self, len: usize) -> Vec { + let mut buf = self.sample_pool.pop().unwrap_or_default(); + buf.clear(); + buf.resize(len, 0); + buf + } + + fn take_flag_buf(&mut self, len: usize) -> Vec { + let mut buf = self.flag_pool.pop().unwrap_or_default(); + buf.clear(); + buf.resize(len, 0); + buf + } + + fn recycle_block(&mut self, mut block: CBlock) { + if let Some(mut flags) = block.flags.take() { + if self.flag_pool.len() < 2 { + flags.clear(); + self.flag_pool.push(flags); + } + } + if let Ok(mut vector) = Arc::try_unwrap(block.vector) { + if self.sample_pool.len() < 2 { + vector.clear(); + self.sample_pool.push(vector); + } + } + } + + fn recycle_root(&mut self) { + if let Some(block) = self.root.vector.take() { + self.recycle_block(block); + } + } + fn firstlast(&mut self) { let tracks = self.d.tracks(); let curtrack = self.d.sector_gettrack(self.cursor); @@ -1249,7 +1297,7 @@ impl Paranoia { if self.d.sector_gettrack(sector) < 0 { return -1; } - self.root.vector = None; + self.recycle_root(); self.root.lastsector = false; self.root.returned_limit = 0; self.root.silence_flag = false; @@ -1264,19 +1312,23 @@ impl Paranoia { self.root.returned_limit = 0; self.dyndrift = 0; self.root.lastsector = false; - self.root.vector = None; + self.recycle_root(); self.root.silence_flag = false; self.resetcache(); } fn resetcache(&mut self) { - self.cache.clear(); self.fragments.clear(); + while let Some(block) = self.cache.pop_front() { + self.recycle_block(block); + } } fn recover_cache(&mut self) { while self.cache.len() > self.cache_limit { - self.cache.pop_back(); + if let Some(block) = self.cache.pop_back() { + self.recycle_block(block); + } } } @@ -1338,7 +1390,7 @@ impl Paranoia { beginword: i64, _endword: i64, callback: &mut dyn FnMut(i64, i32), - ) -> Option { + ) -> bool { let totaltoread = self.cdcache_size; let sectatonce = self.d.nsectors().max(1); let driftcomp = (self.dyndrift as f64 / CD_FRAMEWORDS as f64 + 0.5) as i64; @@ -1370,14 +1422,15 @@ impl Paranoia { let mut new = CBlock::new(); new.flags = if self.enable & (PARANOIA_MODE_OVERLAP | PARANOIA_MODE_VERIFY) != 0 { - Some(vec![0u8; (totaltoread * CD_FRAMEWORDS) as usize]) + Some(self.take_flag_buf((totaltoread * CD_FRAMEWORDS) as usize)) } else { None }; self.cache.push_front(new); self.recover_cache(); - let mut raw = vec![0u8; (totaltoread * CD_FRAMESIZE_RAW as i64) as usize]; + let word_count = (totaltoread * CD_FRAMEWORDS) as usize; + let mut samples = self.take_sample_buf(word_count); let mut sofar = 0i64; let mut firstread = -1i64; let mut anyflag = false; @@ -1402,12 +1455,12 @@ impl Paranoia { if firstread < 0 { firstread = adjread; } - let start = (sofar * CD_FRAMESIZE_RAW as i64) as usize; - let end = start + (secread * CD_FRAMESIZE_RAW as i64) as usize; - let end = end.min(raw.len()); + let start = (sofar * CD_FRAMEWORDS) as usize; + let end = start + (secread * CD_FRAMEWORDS) as usize; + let end = end.min(samples.len()); let result = self .d - .read_audio(Some(&mut raw[start..end]), adjread, secread); + .read_audio_samples(&mut samples[start..end], adjread, secread); let thisread = match result { Ok(n) => { let mut n = n as i64; @@ -1418,8 +1471,10 @@ impl Paranoia { } Err(e) => { if e.errno == libc::ENOMEDIUM { - self.cache.pop_front(); - return None; + if let Some(block) = self.cache.pop_front() { + self.recycle_block(block); + } + return false; } 0 } @@ -1428,15 +1483,13 @@ impl Paranoia { if thisread >= 0 { callback((adjread + thisread) * CD_FRAMEWORDS, PARANOIA_CB_READERR); } - let start = (sofar + thisread) as usize * CD_FRAMESIZE_RAW; - let end = (sofar + secread) as usize * CD_FRAMESIZE_RAW; - if start < raw.len() { - let end = end.min(raw.len()); - raw[start..end].fill(0); + let start = ((sofar + thisread) * CD_FRAMEWORDS) as usize; + let end = ((sofar + secread) * CD_FRAMEWORDS) as usize; + if start < samples.len() { + let end = end.min(samples.len()); + samples[start..end].fill(0); } if let Some(flags) = self.cache.front_mut().and_then(|c| c.flags.as_mut()) { - let start = ((sofar + thisread) * CD_FRAMEWORDS) as usize; - let end = ((sofar + secread) * CD_FRAMEWORDS) as usize; if start < flags.len() { let end = end.min(flags.len()); flags[start..end].fill(FLAGS_UNREAD); @@ -1476,23 +1529,20 @@ impl Paranoia { if anyflag { let words = (sofar * CD_FRAMEWORDS) as usize; - let bytes = (sofar * CD_FRAMESIZE_RAW as i64) as usize; - raw.truncate(bytes); - let samples = crate::device::bytes_to_samples(&raw); - let mut flags = self.cache.front().and_then(|c| c.flags.clone()); - if let Some(f) = flags.as_mut() { - f.truncate(words); + samples.truncate(words); + let block = self.cache.front_mut().unwrap(); + if let Some(flags) = block.flags.as_mut() { + flags.truncate(words); } - let mut new = self.cache.pop_front().unwrap(); - new.vector = samples; - new.flags = flags; - new.begin = firstread * CD_FRAMEWORDS - self.dyndrift; - new.size = words as i64; - self.cache.push_front(new); - Some(self.cache.front().unwrap().clone()) + block.vector = Arc::new(samples); + block.begin = firstread * CD_FRAMEWORDS - self.dyndrift; + block.size = words as i64; + true } else { - self.cache.pop_front(); - None + if let Some(block) = self.cache.pop_front() { + self.recycle_block(block); + } + false } } @@ -1535,9 +1585,9 @@ impl Paranoia { let frag_end = new.begin + (end + OVERLAP_ADJ).min(new.size); let lastsector = end + OVERLAP_ADJ >= new.size && new.lastsector; let start_idx = (frag_begin - new.begin) as usize; - let end_idx = (frag_end - new.begin) as usize; self.fragments.push(Some(VFragment { - vector: new.vector[start_idx..end_idx].to_vec(), + data: Arc::clone(&new.vector), + offset: start_idx, begin: frag_begin, size: frag_end - frag_begin, lastsector, @@ -1586,7 +1636,7 @@ impl Paranoia { } if let Some(id) = initialized { let v = self.fragments[id].take().unwrap(); - let c = CBlock::from_parts(v.vector, v.begin, v.lastsector); + let c = CBlock::from_parts(v.vector().to_vec(), v.begin, v.lastsector); self.root.lastsector = v.lastsector; self.root.returned_limit = beginword; self.root.silence_flag = false; @@ -1726,7 +1776,8 @@ impl Paranoia { if v.begin < av { // Drop fragments that would move before absolute zero. *v = VFragment { - vector: Vec::new(), + data: Arc::new(Vec::new()), + offset: 0, begin: 0, size: -1, lastsector: false, @@ -1754,29 +1805,38 @@ impl Paranoia { } fn trim(&mut self, beginword: i64) { + let mut free_root = false; if let Some(root) = self.root.vector.as_mut() { let target = beginword - MAX_SECTOR_OVERLAP * CD_FRAMEWORDS; let rbegin = root.begin; let rend = root.end(); if rbegin > beginword { - self.root.vector = None; - self.root.returned_limit = -1; - self.root.lastsector = false; - return; - } - if rbegin + MAX_SECTOR_OVERLAP * CD_FRAMEWORDS < beginword { + free_root = true; + } else if rbegin + MAX_SECTOR_OVERLAP * CD_FRAMEWORDS < beginword { if target + MIN_WORDS_OVERLAP > rend { - self.root.vector = None; - self.root.returned_limit = -1; - self.root.lastsector = false; - return; + free_root = true; + } else { + let offset = target - rbegin; + root.remove_front(offset); } - let offset = target - rbegin; - root.remove_front(offset); } } + if free_root { + self.recycle_root(); + self.root.returned_limit = -1; + self.root.lastsector = false; + return; + } let cutoff = beginword - MAX_SECTOR_OVERLAP * CD_FRAMEWORDS; - self.cache.retain(|c| c.end() >= cutoff); + let mut kept = VecDeque::with_capacity(self.cache.len()); + while let Some(block) = self.cache.pop_front() { + if block.end() >= cutoff { + kept.push_back(block); + } else { + self.recycle_block(block); + } + } + self.cache = kept; } fn end_case(&mut self, endword: i64) { @@ -1856,7 +1916,8 @@ impl Paranoia { gend = (gend + OVERLAP_ADJ).min(cend); if self.root.is_empty() { let c = &self.cache[idx]; - self.root.vector = Some(CBlock::from_parts(c.vector.clone(), cbegin, false)); + self.root.vector = + Some(CBlock::from_parts(c.vector.as_ref().clone(), cbegin, false)); } else { let c = &self.cache[idx]; let data: Vec = @@ -1904,7 +1965,8 @@ impl Paranoia { if let Some(root) = self.root.vector.as_mut() { if root.begin > beginword { let add = (root.begin - beginword) as usize; - root.vector.splice(0..0, std::iter::repeat_n(0i16, add)); + Arc::make_mut(&mut root.vector) + .splice(0..0, std::iter::repeat_n(0i16, add)); root.size += add as i64; root.begin = beginword; } @@ -1938,63 +2000,63 @@ impl Paranoia { let satisfied = !(self.root.begin() == -1 || self.root.begin() > beginword || self.root.end() < endword + MAX_SECTOR_OVERLAP * CD_FRAMEWORDS); - if std::env::var_os("CDR_NEG").is_some() { - eprintln!( - "neg loop cursor={} root={:?}..{:?} need={} satisfied={} retry={}", - self.cursor, - self.root.begin(), - self.root.end(), - need_more, - satisfied, - retry_count - ); - } if satisfied { break; } - let new = self.read_c_block(beginword, endword, callback); - if std::env::var_os("CDR_NEG").is_some() { - eprintln!( - " new={:?} frags={}", - new.as_ref().map(|b| (b.begin, b.size)), - self.fragments.iter().filter(|f| f.is_some()).count() - ); - } - if let Some(mut new) = new { + if self.read_c_block(beginword, endword, callback) { if self.enable & (PARANOIA_MODE_OVERLAP | PARANOIA_MODE_VERIFY) != 0 { if self.enable & PARANOIA_MODE_VERIFY != 0 { - // stage1() expects the new block at the head. + // stage1() expects the new block at the head and + // leaves it there when it returns. self.stage1(callback); } else { - let mut begin = 0i64; - while begin < new.size { - while begin < new.size - && new.flags.as_ref().unwrap()[begin as usize] & FLAGS_EDGE != 0 - { - begin += 1; + // Build fragments from the block already at the + // cache head. Collect first so the cache borrow is + // released before touching self.fragments. + let mut new_frags = Vec::new(); + { + let new = self.cache.front().unwrap(); + if let Some(flags) = new.flags.as_ref() { + let mut begin = 0i64; + while begin < new.size { + while begin < new.size + && flags[begin as usize] & FLAGS_EDGE != 0 + { + begin += 1; + } + let mut end = begin + 1; + while end < new.size && flags[end as usize] & FLAGS_EDGE == 0 { + end += 1; + } + let frag_begin = new.begin + begin; + let frag_end = new.begin + end; + let lastsector = new.lastsector && new.begin + end == new.end(); + new_frags.push(VFragment { + data: Arc::clone(&new.vector), + offset: begin as usize, + begin: frag_begin, + size: frag_end - frag_begin, + lastsector, + }); + begin = end; + } + } else { + new_frags.push(VFragment { + data: Arc::clone(&new.vector), + offset: 0, + begin: new.begin, + size: new.size, + lastsector: new.lastsector, + }); } - let mut end = begin + 1; - while end < new.size - && new.flags.as_ref().unwrap()[end as usize] & FLAGS_EDGE == 0 - { - end += 1; - } - let frag_begin = new.begin + begin; - let frag_end = new.begin + end; - let lastsector = new.lastsector && new.begin + end == new.end(); - self.fragments.push(Some(VFragment { - vector: new.vector[begin as usize..end as usize].to_vec(), - begin: frag_begin, - size: frag_end - frag_begin, - lastsector, - })); - begin = end; } + self.fragments.extend(new_frags.into_iter().map(Some)); } } else { + let new = self.cache.pop_front().unwrap(); self.root.vector = Some(CBlock { - vector: std::mem::take(&mut new.vector), + vector: new.vector, flags: None, begin: new.begin, size: new.size, @@ -2038,6 +2100,7 @@ impl Paranoia { if end > root.vector.len() { return None; } - Some(&mut root.vector[start..end]) + let vector = Arc::make_mut(&mut root.vector); + Some(&mut vector[start..end]) } } diff --git a/crates/core/src/paranoia/sort.rs b/crates/core/src/paranoia/sort.rs index 16f806b..c409596 100644 --- a/crates/core/src/paranoia/sort.rs +++ b/crates/core/src/paranoia/sort.rs @@ -1,87 +1,315 @@ -//! Sample-value index, a safe Rust equivalent of isort.c. +//! Sample-value index for paranoia. +//! +//! This is a Rust equivalent of the original `isort.c`, with two strategies: +//! +//! * small search ranges (stage 2) use a direct linear scan, avoiding the +//! cost of building an index for a few hundred samples; +//! * large ranges (stage 1) use a compact CSR-style index with one bucket +//! per 16-bit sample value. +//! +//! The CSR index has no per-value `Vec` allocations and no hashing, which is +//! important because stage 1 indexes up to 1200 CD sectors at a time. -use std::collections::HashMap; +const BUCKETS: usize = 65536; +const LINEAR_THRESHOLD: usize = 4096; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Mode { + Empty, + Linear, + Csr, +} + +#[derive(Clone, Copy, Debug)] +enum Cursor { + Linear { + value: i16, + next: usize, + hi: usize, + current: usize, + }, + Csr { + key: usize, + next: usize, + hi: usize, + current: usize, + }, +} pub struct SortIndex { - buckets: HashMap>, + mode: Mode, pub begin: i64, pub size: usize, - cursor: Option<(i16, usize, usize)>, // value, next bucket index, hi + index_lo: usize, + index_hi: usize, + + // CSR payload: positions grouped by sample value. + positions: Vec, + starts: Vec, + write: Vec, + + cursor: Option, } impl SortIndex { pub fn new(_maxsize: usize) -> Self { Self { - buckets: HashMap::new(), + mode: Mode::Empty, begin: 0, size: 0, + index_lo: 0, + index_hi: 0, + positions: Vec::new(), + starts: Vec::new(), + write: Vec::new(), cursor: None, } } pub fn setup(&mut self, samples: &[i16], begin_abs: i64, sortlo: i64, sorthi: i64) { - self.buckets.clear(); self.begin = begin_abs; self.size = samples.len(); self.cursor = None; let lo = ((sortlo - begin_abs).max(0) as usize).min(samples.len()); let hi = ((sorthi - begin_abs).max(0) as usize).min(samples.len()); + self.index_lo = lo; + self.index_hi = hi; + if lo >= hi { + self.mode = Mode::Empty; return; } - for (i, sample) in samples.iter().enumerate().take(hi).skip(lo) { - self.buckets.entry(*sample).or_default().push(i as u32); + + if hi - lo <= LINEAR_THRESHOLD { + self.mode = Mode::Linear; + return; + } + + self.mode = Mode::Csr; + if self.starts.is_empty() { + self.starts = vec![0u32; BUCKETS + 1]; + self.write = vec![0u32; BUCKETS]; + } else { + self.starts.fill(0); + } + let range_len = hi - lo; + self.positions.resize(range_len, 0); + + for &sample in &samples[lo..hi] { + self.starts[(sample as u16) as usize + 1] += 1; + } + for i in 1..=BUCKETS { + self.starts[i] += self.starts[i - 1]; + } + self.write.copy_from_slice(&self.starts[..BUCKETS]); + + for (offset, &sample) in samples[lo..hi].iter().enumerate() { + let key = (sample as u16) as usize; + let slot = self.write[key] as usize; + self.positions[slot] = (lo + offset) as u32; + self.write[key] += 1; } } /// Equivalent of sort_getmatch(). Returns the first matching relative /// position, and leaves a cursor for sort_nextmatch(). - pub fn getmatch(&mut self, post: i64, overlap: i64, value: i16) -> Option { + pub fn getmatch( + &mut self, + samples: &[i16], + post: i64, + overlap: i64, + value: i16, + ) -> Option { + self.cursor = None; let post = post.clamp(0, self.size as i64); let lo = (post - overlap).max(0) as usize; let hi = ((post + overlap).min(self.size as i64)).max(0) as usize; - self.cursor = None; - let positions = self.buckets.get(&value)?; - let idx = positions.partition_point(|p| (*p as usize) < lo); - if idx >= positions.len() { - return None; + + match self.mode { + Mode::Empty => None, + Mode::Linear => { + let start = lo.max(self.index_lo); + let end = hi.min(self.index_hi); + if start >= end { + return None; + } + if let Some(offset) = samples[start..end] + .iter() + .position(|&sample| sample == value) + { + let pos = start + offset; + self.cursor = Some(Cursor::Linear { + value, + next: pos + 1, + hi: end, + current: pos, + }); + return Some(pos); + } + None + } + Mode::Csr => { + let key = (value as u16) as usize; + let start = self.starts[key] as usize; + let end = self.starts[key + 1] as usize; + let idx = + start + self.positions[start..end].partition_point(|p| (*p as usize) < lo); + if idx >= end { + return None; + } + let pos = self.positions[idx] as usize; + if pos >= hi { + return None; + } + self.cursor = Some(Cursor::Csr { + key, + next: idx + 1, + hi, + current: pos, + }); + Some(pos) + } } - let pos = positions[idx] as usize; - if pos >= hi { - return None; - } - self.cursor = Some((value, idx + 1, hi)); - Some(pos) } - pub fn nextmatch(&mut self) -> Option { - let (value, idx, hi) = self.cursor?; - let positions = self.buckets.get(&value)?; - if idx >= positions.len() { - self.cursor = None; - return None; + pub fn nextmatch(&mut self, samples: &[i16]) -> Option { + match self.cursor.take()? { + Cursor::Linear { + value, next, hi, .. + } => { + if next >= hi { + return None; + } + if let Some(offset) = samples[next..hi].iter().position(|&sample| sample == value) { + let pos = next + offset; + self.cursor = Some(Cursor::Linear { + value, + next: pos + 1, + hi, + current: pos, + }); + return Some(pos); + } + None + } + Cursor::Csr { key, next, hi, .. } => { + let end = self.starts[key + 1] as usize; + if next >= end { + return None; + } + let pos = self.positions[next] as usize; + if pos >= hi { + return None; + } + self.cursor = Some(Cursor::Csr { + key, + next: next + 1, + hi, + current: pos, + }); + Some(pos) + } } - let pos = positions[idx] as usize; - if pos >= hi { - self.cursor = None; - return None; - } - self.cursor = Some((value, idx + 1, hi)); - Some(pos) } /// Position most recently returned by getmatch()/advance(). pub fn cursor_position(&self) -> Option { - let (value, idx, _hi) = self.cursor?; - let positions = self.buckets.get(&value)?; - if idx == 0 || idx > positions.len() { - return None; + match self.cursor? { + Cursor::Linear { current, .. } | Cursor::Csr { current, .. } => Some(current as i64), } - Some(positions[idx - 1] as i64) } /// Advance to the next match. Returns false when exhausted. - pub fn advance(&mut self) -> bool { - self.nextmatch().is_some() + pub fn advance(&mut self, samples: &[i16]) -> bool { + self.nextmatch(samples).is_some() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn naive_matches( + samples: &[i16], + index_lo: usize, + index_hi: usize, + post: i64, + overlap: i64, + value: i16, + ) -> Vec { + let post = post.clamp(0, samples.len() as i64); + let lo = (post - overlap).max(0) as usize; + let hi = ((post + overlap).min(samples.len() as i64)).max(0) as usize; + (lo.max(index_lo)..hi.min(index_hi)) + .filter(|&i| samples[i] == value) + .collect() + } + + fn collect_matches( + index: &mut SortIndex, + samples: &[i16], + post: i64, + overlap: i64, + value: i16, + ) -> Vec { + let mut out = Vec::new(); + if let Some(first) = index.getmatch(samples, post, overlap, value) { + out.push(first); + while let Some(next) = index.nextmatch(samples) { + out.push(next); + } + } + out + } + + #[test] + fn adaptive_index_matches_naive_search() { + let mut x = 0x1234_5678u64; + let mut samples = Vec::with_capacity(12000); + for i in 0..12000 { + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + let value = if i % 7 == 0 { + 0 + } else { + ((x as i32) % 9 - 4) as i16 + }; + samples.push(value); + } + + let mut index = SortIndex::new(samples.len()); + for (lo, hi) in [(0usize, samples.len()), (300, 900), (1000, 1400)] { + index.setup(&samples, 0, lo as i64, hi as i64); + let expected_mode = if hi - lo <= LINEAR_THRESHOLD { + Mode::Linear + } else { + Mode::Csr + }; + assert_eq!(index.mode, expected_mode); + for post in [0i64, 1, 500, 1500, 9000, 11999] { + for overlap in [0i64, 1, 64, 512] { + for value in -4i16..=4 { + let expected = naive_matches(&samples, lo, hi, post, overlap, value); + let got = collect_matches(&mut index, &samples, post, overlap, value); + assert_eq!( + got, expected, + "lo={lo} hi={hi} post={post} overlap={overlap} value={value}" + ); + } + } + } + } + } + + #[test] + fn duplicate_bucket_is_ordered() { + let samples = vec![0i16; 20000]; + let mut index = SortIndex::new(samples.len()); + index.setup(&samples, 0, 0, samples.len() as i64); + assert_eq!(index.mode, Mode::Csr); + let got = collect_matches(&mut index, &samples, 10000, 200, 0); + let expected = (9800..10200).collect::>(); + assert_eq!(got, expected); } }