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
+10
View File
@@ -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
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/crates/capi-types/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/crates/cdda-interface/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/crates/cdda-paranoia/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/crates/cli/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/crates/cli/tests" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/crates/core/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/crates/core/tests" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/crates/core/examples" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/cdrustanoia.iml" filepath="$PROJECT_DIR$/.idea/cdrustanoia.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
Generated
+5 -5
View File
@@ -10,7 +10,7 @@ checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
[[package]] [[package]]
name = "cdda_interface" name = "cdda_interface"
version = "0.1.0" version = "26.0.1"
dependencies = [ dependencies = [
"cdrustanoia_capi_types", "cdrustanoia_capi_types",
"cdrustanoia_core", "cdrustanoia_core",
@@ -19,7 +19,7 @@ dependencies = [
[[package]] [[package]]
name = "cdda_paranoia" name = "cdda_paranoia"
version = "0.1.0" version = "26.0.1"
dependencies = [ dependencies = [
"cdda_interface", "cdda_interface",
"cdrustanoia_capi_types", "cdrustanoia_capi_types",
@@ -29,14 +29,14 @@ dependencies = [
[[package]] [[package]]
name = "cdrustanoia_capi_types" name = "cdrustanoia_capi_types"
version = "0.1.0" version = "26.0.1"
dependencies = [ dependencies = [
"libc", "libc",
] ]
[[package]] [[package]]
name = "cdrustanoia_cli" name = "cdrustanoia_cli"
version = "0.1.0" version = "26.0.1"
dependencies = [ dependencies = [
"cdrustanoia_core", "cdrustanoia_core",
"libc", "libc",
@@ -45,7 +45,7 @@ dependencies = [
[[package]] [[package]]
name = "cdrustanoia_core" name = "cdrustanoia_core"
version = "0.1.0" version = "26.0.1"
dependencies = [ dependencies = [
"libc", "libc",
"tempfile", "tempfile",
+2 -1
View File
@@ -9,7 +9,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "0.1.0" version = "26.0.1"
edition = "2021" edition = "2021"
license = "LGPL-2.1-or-later" license = "LGPL-2.1-or-later"
@@ -20,4 +20,5 @@ cdrustanoia_capi_types = { path = "crates/capi-types" }
[profile.release] [profile.release]
lto = true lto = true
opt-level = 3
codegen-units = 1 codegen-units = 1
+14
View File
@@ -58,6 +58,20 @@ cdrustanoia -r -- "-3" out.raw
Use `cdrustanoia --help` for the full option and span documentation. The man Use `cdrustanoia --help` for the full option and span documentation. The man
page is the original cdparanoia man page. 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 ## C API
The two C libraries export the traditional symbols and use the original The two C libraries export the traditional symbols and use the original
+149
View File
@@ -17,6 +17,13 @@ pub struct BackendHolder {
pub drive: Box<dyn Drive>, 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 { unsafe fn cstr(s: &str) -> *mut c_char {
let clean: String = s.chars().filter(|c| *c != '\0').collect(); let clean: String = s.chars().filter(|c| *c != '\0').collect();
match CString::new(clean) { match CString::new(clean) {
@@ -212,6 +219,16 @@ pub unsafe extern "C" fn cdda_identify(
device: *const c_char, device: *const c_char,
messagedest: c_int, messagedest: c_int,
messages: *mut *mut c_char, 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 { ) -> *mut CdromDrive {
if !messages.is_null() { if !messages.is_null() {
*messages = ptr::null_mut(); *messages = ptr::null_mut();
@@ -234,6 +251,16 @@ pub unsafe extern "C" fn cdda_identify_cooked(
device: *const c_char, device: *const c_char,
messagedest: c_int, messagedest: c_int,
messages: *mut *mut c_char, 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 { ) -> *mut CdromDrive {
if !messages.is_null() { if !messages.is_null() {
*messages = ptr::null_mut(); *messages = ptr::null_mut();
@@ -257,6 +284,17 @@ pub unsafe extern "C" fn cdda_identify_scsi(
ioctl_device: *const c_char, ioctl_device: *const c_char,
messagedest: c_int, messagedest: c_int,
messages: *mut *mut c_char, 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 { ) -> *mut CdromDrive {
if !messages.is_null() { if !messages.is_null() {
*messages = ptr::null_mut(); *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( pub unsafe extern "C" fn cdda_find_a_cdrom(
messagedest: c_int, messagedest: c_int,
messages: *mut *mut c_char, 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 { ) -> *mut CdromDrive {
if !messages.is_null() { if !messages.is_null() {
*messages = ptr::null_mut(); *messages = ptr::null_mut();
@@ -301,6 +348,10 @@ pub unsafe extern "C" fn cdda_find_a_cdrom(
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn cdda_open(d: *mut CdromDrive) -> c_int { 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() { if d.is_null() {
return -100; return -100;
} }
@@ -343,6 +394,10 @@ pub unsafe extern "C" fn cdda_open(d: *mut CdromDrive) -> c_int {
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn cdda_close(d: *mut CdromDrive) -> c_int { 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() { if d.is_null() {
return 0; return 0;
} }
@@ -357,6 +412,18 @@ pub unsafe extern "C" fn cdda_read_timed(
beginsector: c_long, beginsector: c_long,
sectors: c_long, sectors: c_long,
ms: *mut c_int, 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 { ) -> c_long {
if !ms.is_null() { if !ms.is_null() {
*ms = -1; *ms = -1;
@@ -422,6 +489,10 @@ pub unsafe extern "C" fn cdda_read(
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn cdda_speed_set(d: *mut CdromDrive, speed: c_int) -> c_int { 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) { match holder(d) {
Some(h) => { Some(h) => {
let ret = h.drive.speed_set(speed); 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. // Keep the c_long import used on all targets.
#[allow(dead_code)] #[allow(dead_code)]
fn _typecheck(_: c_long) {} 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)>; 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 { struct CAbiDrive {
d: *mut CdromDrive, d: *mut CdromDrive,
toc: DiscToc, toc: DiscToc,
@@ -190,6 +197,10 @@ pub unsafe extern "C" fn paranoia_version() -> *const c_char {
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn paranoia_init(d: *mut CdromDrive) -> *mut c_void { 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() { if d.is_null() {
return ptr::null_mut(); return ptr::null_mut();
} }
@@ -201,6 +212,10 @@ pub unsafe extern "C" fn paranoia_init(d: *mut CdromDrive) -> *mut c_void {
#[no_mangle] #[no_mangle]
pub unsafe extern "C" fn paranoia_free(p: *mut c_void) { 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() { if !p.is_null() {
drop(Box::from_raw(p as *mut Paranoia)); drop(Box::from_raw(p as *mut Paranoia));
} }
@@ -246,6 +261,16 @@ pub unsafe extern "C" fn paranoia_read_limited(
p: *mut c_void, p: *mut c_void,
callback: ParanoiaCallback, callback: ParanoiaCallback,
max_retries: c_int, 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 { ) -> *mut i16 {
let p = match p_ref(p) { let p = match p_ref(p) {
Some(p) => 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::os::unix::io::FromRawFd;
use std::path::Path; 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: USAGE:
cdparanoia [options] <span> [outfile] cdrustanoia [options] <span> [outfile]
OPTIONS: OPTIONS:
-A --analyze-drive : run and log a complete analysis of drive -A --analyze-drive : run and log a complete analysis of drive
caching, timing and reading behavior; caching, timing and reading behavior;
verifies that cdparanoia is correctly verifies that cdrustanoia is correctly
modelling a sprcific drive's cache and modelling a sprcific drive's cache and
read behavior. Implies -vQL read behavior. Implies -vQL
-v --verbose : extra verbose operation -v --verbose : extra verbose operation
@@ -42,10 +43,10 @@ OPTIONS:
-e --stderr-progress : force output of progress information to -e --stderr-progress : force output of progress information to
stderr (for wrapper scripts) stderr (for wrapper scripts)
-l --log-summary [<file>] : save result summary to file, default -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 -L --log-debug [<file>] : save detailed device autosense and
debugging output to file, default debugging output to file, default
filename cdparanoia.log filename cdrustanoia.log
-V --version : print version info and quit -V --version : print version info and quit
-Q --query : autosense drive, query disc and quit -Q --query : autosense drive, query disc and quit
-B --batch : 'batch' mode (saves each track to a -B --batch : 'batch' mode (saves each track to a
@@ -77,7 +78,7 @@ OPTIONS:
force use of the old SG kernel force use of the old SG kernel
interface. -g cannot be used with -k. interface. -g cannot be used with -k.
-S --force-read-speed <n> : read from device at specified speed; by -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. speed.
-t --toc-offset <n> : Add <n> sectors to the values reported -t --toc-offset <n> : Add <n> sectors to the values reported
when addressing tracks. May be negative when addressing tracks. May be negative
@@ -154,16 +155,14 @@ the shell...
A few examples, protected from the shell: A few examples, protected from the shell:
A) query only with exhaustive search for a drive and full reporting A) query only with exhaustive search for a drive and full reporting
of autosense: of autosense:
cdparanoia -vsQ cdrustanoia -vsQ
B) extract up to and including track 3, putting each track in a seperate B) extract up to and including track 3, putting each track in a seperate
file: file:
cdparanoia -B -- "-3" cdrustanoia -B -- "-3"
C) extract from track 1, time 0:30.12 to 1:10.00: C) extract from track 1, time 0:30.12 to 1:10.00:
cdparanoia "1[:30.12]-1[1:10]" cdrustanoia "1[:30.12]-1[1:10]"
Submit bug reports to paranoia@xiph.org
"#; "#;
@@ -283,17 +282,6 @@ fn display_toc(d: &dyn Drive, log: &mut Log) {
log.report(format_args!(" ")); 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] { fn samples_as_bytes(samples: &[i16]) -> &[u8] {
unsafe { unsafe {
std::slice::from_raw_parts( std::slice::from_raw_parts(
@@ -708,7 +696,11 @@ of the machine.\n\nMore information about /dev/cdrom:"
progress.skipped_flag = false; progress.skipped_flag = false;
while cursor <= batch_last { 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, Some(s) => s,
None => { None => {
progress.skipped_flag = true; progress.skipped_flag = true;
@@ -725,23 +717,32 @@ of the machine.\n\nMore information about /dev/cdrom:"
progress.skipped_flag = false; progress.skipped_flag = false;
cursor += 1; cursor += 1;
let mut data = sector;
if target_big != host_big { if target_big != host_big {
for s in &mut data { for s in data.iter_mut() {
*s = swap16(*s); *s = swap16(*s);
} }
} }
progress.callback(cursor * 1176 - 1, -2, &mut log); 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())..]) { if let Err(e) = out.write_all(&data_bytes[offset_skip.min(data_bytes.len())..]) {
log.report(format_args!("Error writing output: {}", e)); log.report(format_args!("Error writing output: {}", e));
return 1; return 1;
} }
offset_skip = 0; offset_skip = 0;
if target_big != host_big {
for s in data.iter_mut() {
*s = swap16(*s);
}
}
if sample_offset != 0 && cursor > batch_last { 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, Some(s) => s,
None => { None => {
progress.skipped_flag = true; progress.skipped_flag = true;
@@ -756,14 +757,18 @@ of the machine.\n\nMore information about /dev/cdrom:"
break; break;
} }
progress.skipped_flag = false; progress.skipped_flag = false;
let mut extra = extra;
if target_big != host_big { if target_big != host_big {
for s in &mut extra { for s in extra.iter_mut() {
*s = swap16(*s); *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)]); 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); offset_buffer_used = (sample_offset as usize * 4).min(2352);
progress.callback(cursor * 1176, -2, &mut log); progress.callback(cursor * 1176, -2, &mut log);
if let Err(e) = out.write_all(&offset_buffer[..offset_buffer_used]) { 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 version.workspace = true
edition.workspace = true edition.workspace = true
license = "LGPL-2.1-or-later" license = "LGPL-2.1-or-later"
publish = false
[dependencies] [dependencies]
libc.workspace = true 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> { fn read_words(&mut self, begin_sector: i64, sectors: i64) -> Vec<i16> {
let words = sectors.max(0) * CD_FRAMEWORDS; 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; let mut jitter = 0i64;
if self.faults.jitter_words > 0 { if self.faults.jitter_words > 0 {
self.read_count = self.read_count.wrapping_add(1); self.read_count = self.read_count.wrapping_add(1);
@@ -211,10 +217,9 @@ impl ImageDrive {
} }
} }
let start = begin_sector * CD_FRAMEWORDS + jitter; let start = begin_sector * CD_FRAMEWORDS + jitter;
for w in 0..words { for (w, slot) in out.iter_mut().enumerate().take(words as usize) {
out.push(self.mapped_sample(start + w).unwrap_or(0)); *slot = self.mapped_sample(start + w as i64).unwrap_or(0);
} }
out
} }
} }
@@ -281,6 +286,32 @@ impl Drive for ImageDrive {
self.last_begin = begin; self.last_begin = begin;
Ok(sectors as usize) 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 { fn read_timed(&mut self, buffer: Option<&mut [u8]>, begin: i64, sectors: i64) -> TimedRead {
let start = Instant::now(); let start = Instant::now();
let previous_begin = self.last_begin; let previous_begin = self.last_begin;
+9 -3
View File
@@ -178,6 +178,7 @@ pub struct LinuxDrive {
fua: bool, fua: bool,
read_cmd: ReadCommand, read_cmd: ReadCommand,
enable_mode: EnableMode, enable_mode: EnableMode,
last_milliseconds: i32,
} }
impl LinuxDrive { impl LinuxDrive {
@@ -199,6 +200,7 @@ impl LinuxDrive {
fua: false, fua: false,
read_cmd: ReadCommand::D8, read_cmd: ReadCommand::D8,
enable_mode: EnableMode::Dummy, enable_mode: EnableMode::Dummy,
last_milliseconds: 0,
}; };
d.inquiry()?; d.inquiry()?;
d.check_atapi(); d.check_atapi();
@@ -238,6 +240,7 @@ impl LinuxDrive {
fua: false, fua: false,
read_cmd: ReadCommand::Read28, read_cmd: ReadCommand::Read28,
enable_mode: EnableMode::Dummy, enable_mode: EnableMode::Dummy,
last_milliseconds: 0,
}; };
Ok(d) Ok(d)
} }
@@ -561,7 +564,9 @@ impl LinuxDrive {
duration: 0, duration: 0,
info: 0, info: 0,
}; };
let started = Instant::now();
let rc = unsafe { libc::ioctl(self.fd, SG_IO, &mut hdr as *mut SgIoHdr) }; 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 { if rc < 0 {
return Err(ScsiError::io(io::Error::last_os_error())); return Err(ScsiError::io(io::Error::last_os_error()));
} }
@@ -819,7 +824,9 @@ impl LinuxDrive {
nframes: requested as c_int, nframes: requested as c_int,
buf: buf.as_mut_ptr(), buf: buf.as_mut_ptr(),
}; };
let started = Instant::now();
let rc = unsafe { libc::ioctl(self.fd, CDROMREADAUDIO, &mut arg as *mut _) }; let rc = unsafe { libc::ioctl(self.fd, CDROMREADAUDIO, &mut arg as *mut _) };
self.last_milliseconds = started.elapsed().as_millis() as i32;
if rc == 0 { if rc == 0 {
return Ok(requested as usize); return Ok(requested as usize);
} }
@@ -1211,12 +1218,11 @@ impl Drive for LinuxDrive {
self.read_raw(buffer, begin, sectors) self.read_raw(buffer, begin, sectors)
} }
fn read_timed(&mut self, buffer: Option<&mut [u8]>, begin: i64, sectors: i64) -> TimedRead { 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 result = self.read_audio(buffer, begin, sectors);
let elapsed = started.elapsed().as_millis() as i32;
TimedRead { TimedRead {
result, result,
milliseconds: elapsed, milliseconds: self.last_milliseconds,
} }
} }
fn speed_set(&mut self, speed: i32) -> i32 { fn speed_set(&mut self, speed: i32) -> i32 {
+25
View File
@@ -275,6 +275,31 @@ pub trait Drive {
begin: i64, begin: i64,
sectors: i64, sectors: i64,
) -> Result<usize, ReadError>; ) -> 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 /// Low-level read without host-endian conversion, matching the original
/// `cdrom_drive::read_audio` function pointer. /// `cdrom_drive::read_audio` function pointer.
fn read_audio_raw( fn read_audio_raw(
+2 -2
View File
@@ -15,8 +15,8 @@ pub mod callback;
pub mod device; pub mod device;
pub mod paranoia; pub mod paranoia;
pub const VERSIONNUM: &str = "10.2"; pub const VERSIONNUM: &str = "26.0.1";
pub const VERSION: &str = "cdparanoia III release 10.2 (September 11, 2008)"; pub const VERSION: &str = "cdrustanoia 26.0.1 (2026-11-03)";
pub fn paranoia_version() -> &'static str { pub fn paranoia_version() -> &'static str {
VERSIONNUM VERSIONNUM
+18 -9
View File
@@ -1,12 +1,14 @@
//! c_block/v_fragment/root data structures and vector operations. //! c_block/v_fragment/root data structures and vector operations.
use std::sync::Arc;
pub const FLAGS_EDGE: u8 = 0x1; pub const FLAGS_EDGE: u8 = 0x1;
pub const FLAGS_UNREAD: u8 = 0x2; pub const FLAGS_UNREAD: u8 = 0x2;
pub const FLAGS_VERIFIED: u8 = 0x4; pub const FLAGS_VERIFIED: u8 = 0x4;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct CBlock { pub struct CBlock {
pub vector: Vec<i16>, pub vector: Arc<Vec<i16>>,
pub flags: Option<Vec<u8>>, pub flags: Option<Vec<u8>>,
pub begin: i64, pub begin: i64,
pub size: i64, pub size: i64,
@@ -22,7 +24,7 @@ impl Default for CBlock {
impl CBlock { impl CBlock {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
vector: Vec::new(), vector: Arc::new(Vec::new()),
flags: None, flags: None,
begin: 0, begin: 0,
size: 0, size: 0,
@@ -33,7 +35,7 @@ impl CBlock {
pub fn from_parts(vector: Vec<i16>, begin: i64, lastsector: bool) -> Self { pub fn from_parts(vector: Vec<i16>, begin: i64, lastsector: bool) -> Self {
let size = vector.len() as i64; let size = vector.len() as i64;
Self { Self {
vector, vector: Arc::new(vector),
flags: None, flags: None,
begin, begin,
size, size,
@@ -62,7 +64,7 @@ impl CBlock {
return; return;
} }
let pos = pos as usize; 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; self.size += data.len() as i64;
if let Some(flags) = self.flags.as_mut() { if let Some(flags) = self.flags.as_mut() {
let n = data.len(); let n = data.len();
@@ -87,7 +89,7 @@ impl CBlock {
} }
let pos = cutpos as usize; let pos = cutpos as usize;
let n = cutsize 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; self.size -= cutsize;
if let Some(flags) = self.flags.as_mut() { if let Some(flags) = self.flags.as_mut() {
let pos = pos.min(flags.len()); let pos = pos.min(flags.len());
@@ -109,11 +111,11 @@ impl CBlock {
} }
let p = pos as usize; let p = pos as usize;
let n = size 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]) { 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; self.size += data.len() as i64;
if let Some(flags) = self.flags.as_mut() { if let Some(flags) = self.flags.as_mut() {
flags.extend(std::iter::repeat_n(0, data.len())); flags.extend(std::iter::repeat_n(0, data.len()));
@@ -125,7 +127,7 @@ impl CBlock {
return; return;
} }
let n = (cut as usize).min(self.vector.len()); 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.size -= n as i64;
self.begin += n as i64; self.begin += n as i64;
if let Some(flags) = self.flags.as_mut() { if let Some(flags) = self.flags.as_mut() {
@@ -148,7 +150,10 @@ impl CBlock {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct VFragment { 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 begin: i64,
pub size: i64, pub size: i64,
pub lastsector: bool, pub lastsector: bool,
@@ -158,6 +163,10 @@ impl VFragment {
pub fn end(&self) -> i64 { pub fn end(&self) -> i64 {
self.begin + self.size self.begin + self.size
} }
pub fn vector(&self) -> &[i16] {
&self.data[self.offset..self.offset + self.size as usize]
}
} }
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
+174 -111
View File
@@ -8,6 +8,7 @@ use crate::device::{Drive, CD_FRAMESIZE_RAW, CD_FRAMEWORDS};
use block::*; use block::*;
use sort::SortIndex; use sort::SortIndex;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::sync::Arc;
pub const MIN_WORDS_OVERLAP: i64 = 64; pub const MIN_WORDS_OVERLAP: i64 = 64;
pub const MIN_WORDS_SEARCH: i64 = 64; pub const MIN_WORDS_SEARCH: i64 = 64;
@@ -61,6 +62,8 @@ pub struct Paranoia {
pub dynoverlap: i64, pub dynoverlap: i64,
pub dyndrift: i64, pub dyndrift: i64,
stage2_adjust_pending: bool, 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. // Search for a matching sample within dynoverlap.
let search_post = post - a_begin; let search_post = post - a_begin;
if sort 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() .is_some()
{ {
loop { loop {
@@ -356,7 +359,7 @@ fn try_sort_sync(
*end = bend; *end = bend;
return true; return true;
} }
if !sort.advance() { if !sort.advance(a_vector) {
break; break;
} }
} }
@@ -732,7 +735,7 @@ fn iterate_stage2(
callback(v.begin, PARANOIA_CB_VERIFY); callback(v.begin, PARANOIA_CB_VERIFY);
let mut fbv = v.begin.max(root.begin - *dynoverlap); 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; fbv += 1;
} }
if fbv == v.end() { if fbv == v.end() {
@@ -744,7 +747,7 @@ fn iterate_stage2(
.min(v.end()); .min(v.end());
let searchend = (fev + *dynoverlap).min(root.end()); let searchend = (fev + *dynoverlap).min(root.end());
let searchbegin = (fbv - *dynoverlap).max(root.begin); 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; let mut j = searchbegin;
while j < searchend { while j < searchend {
@@ -759,7 +762,7 @@ fn iterate_stage2(
let mut offset = 0; let mut offset = 0;
if try_sort_sync( if try_sort_sync(
sort, sort,
&v.vector, v.vector(),
v.begin, v.begin,
v.size, v.size,
None, None,
@@ -827,7 +830,7 @@ fn silence_match(
return false; return false;
} }
let mut j = 0; let mut j = 0;
while j < end && v.vector[j as usize] == 0 { while j < end && v.vector()[j as usize] == 0 {
j += 1; j += 1;
} }
if j < MIN_SILENCE_BOUNDARY { if j < MIN_SILENCE_BOUNDARY {
@@ -849,7 +852,7 @@ fn silence_match(
if v.end() > root.end() { if v.end() > root.end() {
let voff = begin - v.begin; let voff = begin - v.begin;
root.remove(begin - root.begin, root.size - (begin - root.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); add_stage2_value(stage2, 0, stage2_pending);
} else if j < begin { } else if j < begin {
@@ -859,7 +862,7 @@ fn silence_match(
*silence_begin - root.begin, *silence_begin - root.begin,
root.size - (*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); add_stage2_value(stage2, end - begin, stage2_pending);
} else { } else {
@@ -907,7 +910,7 @@ fn stage2_each(
callback: &mut dyn FnMut(i64, i32), callback: &mut dyn FnMut(i64, i32),
) -> MergeOutcome { ) -> MergeOutcome {
let dyno = *dynoverlap / 2 * 2; let dyno = *dynoverlap / 2 * 2;
if v.size <= 0 || v.vector.is_empty() { if v.size <= 0 || v.vector().is_empty() {
return MergeOutcome::Discarded; return MergeOutcome::Discarded;
} }
@@ -931,7 +934,11 @@ fn stage2_each(
while begin + offset > 0 && begin > 0 { while begin + offset > 0 && begin > 0 {
let begin_l = begin + offset; let begin_l = begin + offset;
if scratch.is_none() { 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); 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 begin_l = begin + offset;
let end_l = end + offset; let end_l = end + offset;
if scratch.is_none() { 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); 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() { let data: Vec<i16> = match scratch.as_ref() {
Some(l) => l.vector[(end + offset) as usize..size_b as usize].to_vec(), 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() { if !data.is_empty() {
root.append(&data); root.append(&data);
@@ -1183,6 +1194,8 @@ impl Paranoia {
dynoverlap: MAX_SECTOR_OVERLAP * CD_FRAMEWORDS, dynoverlap: MAX_SECTOR_OVERLAP * CD_FRAMEWORDS,
dyndrift: 0, dyndrift: 0,
stage2_adjust_pending: false, stage2_adjust_pending: false,
sample_pool: Vec::new(),
flag_pool: Vec::new(),
}; };
p.firstlast(); p.firstlast();
p p
@@ -1212,6 +1225,41 @@ impl Paranoia {
ret 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) { fn firstlast(&mut self) {
let tracks = self.d.tracks(); let tracks = self.d.tracks();
let curtrack = self.d.sector_gettrack(self.cursor); let curtrack = self.d.sector_gettrack(self.cursor);
@@ -1249,7 +1297,7 @@ impl Paranoia {
if self.d.sector_gettrack(sector) < 0 { if self.d.sector_gettrack(sector) < 0 {
return -1; return -1;
} }
self.root.vector = None; self.recycle_root();
self.root.lastsector = false; self.root.lastsector = false;
self.root.returned_limit = 0; self.root.returned_limit = 0;
self.root.silence_flag = false; self.root.silence_flag = false;
@@ -1264,19 +1312,23 @@ impl Paranoia {
self.root.returned_limit = 0; self.root.returned_limit = 0;
self.dyndrift = 0; self.dyndrift = 0;
self.root.lastsector = false; self.root.lastsector = false;
self.root.vector = None; self.recycle_root();
self.root.silence_flag = false; self.root.silence_flag = false;
self.resetcache(); self.resetcache();
} }
fn resetcache(&mut self) { fn resetcache(&mut self) {
self.cache.clear();
self.fragments.clear(); self.fragments.clear();
while let Some(block) = self.cache.pop_front() {
self.recycle_block(block);
}
} }
fn recover_cache(&mut self) { fn recover_cache(&mut self) {
while self.cache.len() > self.cache_limit { 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, beginword: i64,
_endword: i64, _endword: i64,
callback: &mut dyn FnMut(i64, i32), callback: &mut dyn FnMut(i64, i32),
) -> Option<CBlock> { ) -> bool {
let totaltoread = self.cdcache_size; let totaltoread = self.cdcache_size;
let sectatonce = self.d.nsectors().max(1); let sectatonce = self.d.nsectors().max(1);
let driftcomp = (self.dyndrift as f64 / CD_FRAMEWORDS as f64 + 0.5) as i64; 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(); let mut new = CBlock::new();
new.flags = if self.enable & (PARANOIA_MODE_OVERLAP | PARANOIA_MODE_VERIFY) != 0 { 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 { } else {
None None
}; };
self.cache.push_front(new); self.cache.push_front(new);
self.recover_cache(); 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 sofar = 0i64;
let mut firstread = -1i64; let mut firstread = -1i64;
let mut anyflag = false; let mut anyflag = false;
@@ -1402,12 +1455,12 @@ impl Paranoia {
if firstread < 0 { if firstread < 0 {
firstread = adjread; firstread = adjread;
} }
let start = (sofar * CD_FRAMESIZE_RAW as i64) as usize; let start = (sofar * CD_FRAMEWORDS) as usize;
let end = start + (secread * CD_FRAMESIZE_RAW as i64) as usize; let end = start + (secread * CD_FRAMEWORDS) as usize;
let end = end.min(raw.len()); let end = end.min(samples.len());
let result = self let result = self
.d .d
.read_audio(Some(&mut raw[start..end]), adjread, secread); .read_audio_samples(&mut samples[start..end], adjread, secread);
let thisread = match result { let thisread = match result {
Ok(n) => { Ok(n) => {
let mut n = n as i64; let mut n = n as i64;
@@ -1418,8 +1471,10 @@ impl Paranoia {
} }
Err(e) => { Err(e) => {
if e.errno == libc::ENOMEDIUM { if e.errno == libc::ENOMEDIUM {
self.cache.pop_front(); if let Some(block) = self.cache.pop_front() {
return None; self.recycle_block(block);
}
return false;
} }
0 0
} }
@@ -1428,15 +1483,13 @@ impl Paranoia {
if thisread >= 0 { if thisread >= 0 {
callback((adjread + thisread) * CD_FRAMEWORDS, PARANOIA_CB_READERR); callback((adjread + thisread) * CD_FRAMEWORDS, PARANOIA_CB_READERR);
} }
let start = (sofar + thisread) as usize * CD_FRAMESIZE_RAW; let start = ((sofar + thisread) * CD_FRAMEWORDS) as usize;
let end = (sofar + secread) as usize * CD_FRAMESIZE_RAW; let end = ((sofar + secread) * CD_FRAMEWORDS) as usize;
if start < raw.len() { if start < samples.len() {
let end = end.min(raw.len()); let end = end.min(samples.len());
raw[start..end].fill(0); samples[start..end].fill(0);
} }
if let Some(flags) = self.cache.front_mut().and_then(|c| c.flags.as_mut()) { 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() { if start < flags.len() {
let end = end.min(flags.len()); let end = end.min(flags.len());
flags[start..end].fill(FLAGS_UNREAD); flags[start..end].fill(FLAGS_UNREAD);
@@ -1476,23 +1529,20 @@ impl Paranoia {
if anyflag { if anyflag {
let words = (sofar * CD_FRAMEWORDS) as usize; let words = (sofar * CD_FRAMEWORDS) as usize;
let bytes = (sofar * CD_FRAMESIZE_RAW as i64) as usize; samples.truncate(words);
raw.truncate(bytes); let block = self.cache.front_mut().unwrap();
let samples = crate::device::bytes_to_samples(&raw); if let Some(flags) = block.flags.as_mut() {
let mut flags = self.cache.front().and_then(|c| c.flags.clone()); flags.truncate(words);
if let Some(f) = flags.as_mut() {
f.truncate(words);
} }
let mut new = self.cache.pop_front().unwrap(); block.vector = Arc::new(samples);
new.vector = samples; block.begin = firstread * CD_FRAMEWORDS - self.dyndrift;
new.flags = flags; block.size = words as i64;
new.begin = firstread * CD_FRAMEWORDS - self.dyndrift; true
new.size = words as i64;
self.cache.push_front(new);
Some(self.cache.front().unwrap().clone())
} else { } else {
self.cache.pop_front(); if let Some(block) = self.cache.pop_front() {
None self.recycle_block(block);
}
false
} }
} }
@@ -1535,9 +1585,9 @@ impl Paranoia {
let frag_end = new.begin + (end + OVERLAP_ADJ).min(new.size); let frag_end = new.begin + (end + OVERLAP_ADJ).min(new.size);
let lastsector = end + OVERLAP_ADJ >= new.size && new.lastsector; let lastsector = end + OVERLAP_ADJ >= new.size && new.lastsector;
let start_idx = (frag_begin - new.begin) as usize; let start_idx = (frag_begin - new.begin) as usize;
let end_idx = (frag_end - new.begin) as usize;
self.fragments.push(Some(VFragment { 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, begin: frag_begin,
size: frag_end - frag_begin, size: frag_end - frag_begin,
lastsector, lastsector,
@@ -1586,7 +1636,7 @@ impl Paranoia {
} }
if let Some(id) = initialized { if let Some(id) = initialized {
let v = self.fragments[id].take().unwrap(); 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.lastsector = v.lastsector;
self.root.returned_limit = beginword; self.root.returned_limit = beginword;
self.root.silence_flag = false; self.root.silence_flag = false;
@@ -1726,7 +1776,8 @@ impl Paranoia {
if v.begin < av { if v.begin < av {
// Drop fragments that would move before absolute zero. // Drop fragments that would move before absolute zero.
*v = VFragment { *v = VFragment {
vector: Vec::new(), data: Arc::new(Vec::new()),
offset: 0,
begin: 0, begin: 0,
size: -1, size: -1,
lastsector: false, lastsector: false,
@@ -1754,29 +1805,38 @@ impl Paranoia {
} }
fn trim(&mut self, beginword: i64) { fn trim(&mut self, beginword: i64) {
let mut free_root = false;
if let Some(root) = self.root.vector.as_mut() { if let Some(root) = self.root.vector.as_mut() {
let target = beginword - MAX_SECTOR_OVERLAP * CD_FRAMEWORDS; let target = beginword - MAX_SECTOR_OVERLAP * CD_FRAMEWORDS;
let rbegin = root.begin; let rbegin = root.begin;
let rend = root.end(); let rend = root.end();
if rbegin > beginword { if rbegin > beginword {
self.root.vector = None; free_root = true;
self.root.returned_limit = -1; } else if rbegin + MAX_SECTOR_OVERLAP * CD_FRAMEWORDS < beginword {
self.root.lastsector = false;
return;
}
if rbegin + MAX_SECTOR_OVERLAP * CD_FRAMEWORDS < beginword {
if target + MIN_WORDS_OVERLAP > rend { if target + MIN_WORDS_OVERLAP > rend {
self.root.vector = None; free_root = true;
self.root.returned_limit = -1; } else {
self.root.lastsector = false; let offset = target - rbegin;
return; 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; 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) { fn end_case(&mut self, endword: i64) {
@@ -1856,7 +1916,8 @@ impl Paranoia {
gend = (gend + OVERLAP_ADJ).min(cend); gend = (gend + OVERLAP_ADJ).min(cend);
if self.root.is_empty() { if self.root.is_empty() {
let c = &self.cache[idx]; 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 { } else {
let c = &self.cache[idx]; let c = &self.cache[idx];
let data: Vec<i16> = let data: Vec<i16> =
@@ -1904,7 +1965,8 @@ impl Paranoia {
if let Some(root) = self.root.vector.as_mut() { if let Some(root) = self.root.vector.as_mut() {
if root.begin > beginword { if root.begin > beginword {
let add = (root.begin - beginword) as usize; 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.size += add as i64;
root.begin = beginword; root.begin = beginword;
} }
@@ -1938,63 +2000,63 @@ impl Paranoia {
let satisfied = !(self.root.begin() == -1 let satisfied = !(self.root.begin() == -1
|| self.root.begin() > beginword || self.root.begin() > beginword
|| self.root.end() < endword + MAX_SECTOR_OVERLAP * CD_FRAMEWORDS); || 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 { if satisfied {
break; break;
} }
let new = self.read_c_block(beginword, endword, callback); if 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.enable & (PARANOIA_MODE_OVERLAP | PARANOIA_MODE_VERIFY) != 0 { if self.enable & (PARANOIA_MODE_OVERLAP | PARANOIA_MODE_VERIFY) != 0 {
if self.enable & 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); self.stage1(callback);
} else { } else {
let mut begin = 0i64; // Build fragments from the block already at the
while begin < new.size { // cache head. Collect first so the cache borrow is
while begin < new.size // released before touching self.fragments.
&& new.flags.as_ref().unwrap()[begin as usize] & FLAGS_EDGE != 0 let mut new_frags = Vec::new();
{ {
begin += 1; 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 { } else {
let new = self.cache.pop_front().unwrap();
self.root.vector = Some(CBlock { self.root.vector = Some(CBlock {
vector: std::mem::take(&mut new.vector), vector: new.vector,
flags: None, flags: None,
begin: new.begin, begin: new.begin,
size: new.size, size: new.size,
@@ -2038,6 +2100,7 @@ impl Paranoia {
if end > root.vector.len() { if end > root.vector.len() {
return None; 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 { pub struct SortIndex {
buckets: HashMap<i16, Vec<u32>>, mode: Mode,
pub begin: i64, pub begin: i64,
pub size: usize, 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 { impl SortIndex {
pub fn new(_maxsize: usize) -> Self { pub fn new(_maxsize: usize) -> Self {
Self { Self {
buckets: HashMap::new(), mode: Mode::Empty,
begin: 0, begin: 0,
size: 0, size: 0,
index_lo: 0,
index_hi: 0,
positions: Vec::new(),
starts: Vec::new(),
write: Vec::new(),
cursor: None, cursor: None,
} }
} }
pub fn setup(&mut self, samples: &[i16], begin_abs: i64, sortlo: i64, sorthi: i64) { pub fn setup(&mut self, samples: &[i16], begin_abs: i64, sortlo: i64, sorthi: i64) {
self.buckets.clear();
self.begin = begin_abs; self.begin = begin_abs;
self.size = samples.len(); self.size = samples.len();
self.cursor = None; self.cursor = None;
let lo = ((sortlo - begin_abs).max(0) as usize).min(samples.len()); let lo = ((sortlo - begin_abs).max(0) as usize).min(samples.len());
let hi = ((sorthi - 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 { if lo >= hi {
self.mode = Mode::Empty;
return; 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 /// Equivalent of sort_getmatch(). Returns the first matching relative
/// position, and leaves a cursor for sort_nextmatch(). /// 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 post = post.clamp(0, self.size as i64);
let lo = (post - overlap).max(0) as usize; let lo = (post - overlap).max(0) as usize;
let hi = ((post + overlap).min(self.size as i64)).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)?; match self.mode {
let idx = positions.partition_point(|p| (*p as usize) < lo); Mode::Empty => None,
if idx >= positions.len() { Mode::Linear => {
return None; 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> { pub fn nextmatch(&mut self, samples: &[i16]) -> Option<usize> {
let (value, idx, hi) = self.cursor?; match self.cursor.take()? {
let positions = self.buckets.get(&value)?; Cursor::Linear {
if idx >= positions.len() { value, next, hi, ..
self.cursor = None; } => {
return None; 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(). /// Position most recently returned by getmatch()/advance().
pub fn cursor_position(&self) -> Option<i64> { pub fn cursor_position(&self) -> Option<i64> {
let (value, idx, _hi) = self.cursor?; match self.cursor? {
let positions = self.buckets.get(&value)?; Cursor::Linear { current, .. } | Cursor::Csr { current, .. } => Some(current as i64),
if idx == 0 || idx > positions.len() {
return None;
} }
Some(positions[idx - 1] as i64)
} }
/// Advance to the next match. Returns false when exhausted. /// Advance to the next match. Returns false when exhausted.
pub fn advance(&mut self) -> bool { pub fn advance(&mut self, samples: &[i16]) -> bool {
self.nextmatch().is_some() 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);
} }
} }