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);
}
}