This commit is contained in:
tux
2026-11-03 22:13:16 +01:00
parent 158e115242
commit 8012e912ec
19 changed files with 975 additions and 208 deletions
+149
View File
@@ -17,6 +17,13 @@ pub struct BackendHolder {
pub drive: Box<dyn Drive>,
}
fn ffi_guard<T>(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<usize, ReadError> {
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);
}
}
}
+25
View File
@@ -19,6 +19,13 @@ use std::time::Instant;
pub type ParanoiaCallback = Option<unsafe extern "C" fn(c_long, c_int)>;
fn ffi_guard<T>(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,
+38 -33
View File
@@ -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 <monty@xiph.org> and Xiph.Org
(C) 2008 Monty <monty@xiph.org> and Xiph.Org
(C) 2026 Martin Wohlert <martin.wohlert@b-root-force.de>
Report bugs to martin.wohlert@b-root-force.de
Report bugs to paranoia@xiph.org
http://www.xiph.org/paranoia/
USAGE:
cdparanoia [options] <span> [outfile]
cdrustanoia [options] <span> [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 [<file>] : save result summary to file, default
filename cdparanoia.log
filename cdrustanoia.log
-L --log-debug [<file>] : 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 <n> : read from device at specified speed; by
default, cdparanoia sets drive to full
default, cdrustanoia sets drive to full
speed.
-t --toc-offset <n> : Add <n> 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<Vec<i16>> {
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]) {
+1
View File
@@ -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
+168
View File
@@ -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<i16> {
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<i16, Vec<u32>>,
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<usize> {
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<usize> {
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);
}
+35 -4
View File
@@ -192,7 +192,13 @@ impl ImageDrive {
fn read_words(&mut self, begin_sector: i64, sectors: i64) -> Vec<i16> {
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<usize, ReadError> {
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;
+9 -3
View File
@@ -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 {
+25
View File
@@ -275,6 +275,31 @@ pub trait Drive {
begin: i64,
sectors: i64,
) -> Result<usize, ReadError>;
/// 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<u8>` 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<usize, ReadError> {
// 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::<u8>(),
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(
+2 -2
View File
@@ -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
+18 -9
View File
@@ -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<i16>,
pub vector: Arc<Vec<i16>>,
pub flags: Option<Vec<u8>>,
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<i16>, 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<i16>,
/// Shared backing data. Cache blocks and all fragments carved from them
/// reference the same allocation; no per-fragment copy is made.
pub data: Arc<Vec<i16>>,
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)]
+174 -111
View File
@@ -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<Vec<i16>>,
flag_pool: Vec<Vec<u8>>,
}
// ---------------------------------------------------------------------------
@@ -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<i16> = 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<i16> {
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<u8> {
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<CBlock> {
) -> 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<i16> =
@@ -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])
}
}
+268 -40
View File
@@ -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<i16, Vec<u32>>,
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<u32>,
starts: Vec<u32>,
write: Vec<u32>,
cursor: Option<Cursor>,
}
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<usize> {
pub fn getmatch(
&mut self,
samples: &[i16],
post: i64,
overlap: i64,
value: i16,
) -> Option<usize> {
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<usize> {
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<usize> {
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<i64> {
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<usize> {
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<usize> {
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::<Vec<_>>();
assert_eq!(got, expected);
}
}