use std::mem::size_of;
use crate::error::{Par2Error, Result};
use crate::gf;
use crate::types::{
CancellationToken, MAX_TOTAL_INPUT_SLICES, ProgressCallback, ProgressPhase, ProgressStage,
ProgressUpdate, RecoveryExponent,
};
use reedsolomon_rs::gf_simd::{self, PreparedFactorSrc};
use super::plan::default_memory_limit;
const DEFAULT_INPUT_GROUPING: usize = 12;
#[cfg_attr(not(target_arch = "aarch64"), allow(dead_code))]
const CLMUL_INPUT_GROUPING: usize = 16;
const MAX_INPUT_GROUPING: usize = 16;
const _: () = assert!(DEFAULT_INPUT_GROUPING <= MAX_INPUT_GROUPING);
const _: () = assert!(CLMUL_INPUT_GROUPING <= MAX_INPUT_GROUPING);
const DEFAULT_STAGING_AREA_COUNT: usize = 4;
const MAX_STAGING_AREA_COUNT: usize = 8;
const _: () = assert!(DEFAULT_STAGING_AREA_COUNT >= 2);
const _: () = assert!(DEFAULT_STAGING_AREA_COUNT <= MAX_STAGING_AREA_COUNT);
fn configured_staging_areas() -> usize {
static CONFIGURED: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
*CONFIGURED.get_or_init(|| {
std::env::var("WEAVER_PAR2_CREATE_AREAS")
.ok()
.and_then(|value| value.trim().parse::<usize>().ok())
.filter(|&areas| (2..=MAX_STAGING_AREA_COUNT).contains(&areas))
.unwrap_or(DEFAULT_STAGING_AREA_COUNT)
})
}
fn transfer_group_lanes() -> usize {
static CONFIGURED: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
*CONFIGURED.get_or_init(|| crate::md5_simd::max_lanes().clamp(1, MAX_INPUT_GROUPING))
}
#[cfg(target_arch = "x86_64")]
const MAX_FOLDED_GROUPS: usize = DEFAULT_INPUT_GROUPING / gf_simd::FOLDED_GROUP;
pub(crate) fn configured_create_threads() -> usize {
static CONFIGURED: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
*CONFIGURED.get_or_init(|| {
if !reedsolomon_rs::threading::parallel_enabled() {
return 1;
}
std::env::var("WEAVER_PAR2_CREATE_THREADS")
.ok()
.and_then(|value| value.trim().parse::<usize>().ok())
.filter(|&threads| threads != 0)
.unwrap_or_else(|| {
std::thread::available_parallelism()
.map(std::num::NonZeroUsize::get)
.unwrap_or(1)
})
})
}
fn configured_input_grouping() -> usize {
static CONFIGURED: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
*CONFIGURED.get_or_init(|| {
#[cfg(target_arch = "aarch64")]
let family_default = CLMUL_INPUT_GROUPING;
#[cfg(not(target_arch = "aarch64"))]
let family_default = DEFAULT_INPUT_GROUPING;
std::env::var("WEAVER_PAR2_CREATE_GROUPING")
.ok()
.and_then(|value| value.trim().parse::<usize>().ok())
.filter(|&grouping| (1..=MAX_INPUT_GROUPING).contains(&grouping))
.unwrap_or(family_default)
})
}
fn configured_interleave_lanes() -> usize {
static CONFIGURED: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
*CONFIGURED.get_or_init(|| {
std::env::var("WEAVER_PAR2_CREATE_INTERLEAVE")
.ok()
.and_then(|value| value.trim().parse::<usize>().ok())
.filter(|&lanes| (1..=MAX_INPUT_GROUPING).contains(&lanes))
.unwrap_or(gf_simd::INPUT_BATCH_INTERLEAVE_LANES)
})
}
fn simd_stride() -> usize {
if cfg!(target_arch = "aarch64") || configured_interleave_lanes() > 1 {
gf_simd::INPUT_BATCH_BLOCK_BYTES
} else {
2
}
}
fn create_band_shape(output_count: usize) -> (usize, usize) {
let outputs = output_count.max(1);
let target = configured_create_threads().clamp(1, outputs);
let band_size = outputs.div_ceil(target);
(band_size, outputs.div_ceil(band_size))
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct ForwardMemoryEstimate {
pub(crate) factor_workspace_bytes: usize,
pub(crate) jit_workspace_bytes: usize,
pub(crate) stripe_buffer_bytes: usize,
pub(crate) processing_peak_bytes: usize,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum ForwardKernel {
#[default]
Auto,
Portable,
Simd,
#[cfg(target_arch = "x86_64")]
Folded,
#[cfg(target_arch = "x86_64")]
XorJitAvx2,
}
pub struct ForwardEncoderOptions {
pub memory_limit: Option<usize>,
pub cancel: Option<CancellationToken>,
pub progress: Option<ProgressCallback>,
pub kernel: ForwardKernel,
}
impl Default for ForwardEncoderOptions {
fn default() -> Self {
Self {
memory_limit: None,
cancel: None,
progress: None,
kernel: ForwardKernel::Auto,
}
}
}
#[cfg(test)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ForwardRecoveryBlock {
pub exponent: RecoveryExponent,
pub data: Vec<u8>,
}
pub trait ForwardRecoverySink {
fn write_recovery_chunk(
&mut self,
output_index: usize,
exponent: RecoveryExponent,
offset: u64,
data: &[u8],
) -> Result<()>;
}
pub(crate) trait ForwardSourceProvider {
fn source_count(&self) -> usize;
fn source_slice_len(&self, source_index: usize) -> Result<usize>;
fn read_source_chunk(
&mut self,
source_index: usize,
offset: usize,
destination: &mut [u8],
) -> Result<usize>;
}
pub(crate) trait ForwardSourceObserver: Send {
fn observe_slices(&mut self, first_source_index: usize, slices: &[&[u8]]) -> Result<()>;
}
#[cfg(test)]
struct InMemorySourceProvider<'a> {
sources: &'a [&'a [u8]],
}
#[cfg(test)]
impl ForwardSourceProvider for InMemorySourceProvider<'_> {
fn source_count(&self) -> usize {
self.sources.len()
}
fn source_slice_len(&self, source_index: usize) -> Result<usize> {
self.sources
.get(source_index)
.map(|source| source.len())
.ok_or_else(|| invalid_input("source slice index is out of range"))
}
fn read_source_chunk(
&mut self,
source_index: usize,
offset: usize,
destination: &mut [u8],
) -> Result<usize> {
let source = self
.sources
.get(source_index)
.ok_or_else(|| invalid_input("source slice index is out of range"))?;
let start = offset.min(source.len());
let take = destination.len().min(source.len().saturating_sub(start));
destination[..take].copy_from_slice(&source[start..start + take]);
Ok(take)
}
}
#[derive(Clone, Debug)]
pub struct ForwardEncoder {
slice_size: usize,
recovery_exponents: Vec<RecoveryExponent>,
}
impl ForwardEncoder {
pub fn new(slice_size: usize, recovery_exponents: Vec<RecoveryExponent>) -> Result<Self> {
if slice_size == 0 || !slice_size.is_multiple_of(4) {
return Err(invalid_input(format!(
"slice size must be a nonzero multiple of 4, got {slice_size}"
)));
}
if recovery_exponents.len() > u32::MAX as usize {
return Err(resource_limit("recovery output count exceeds u32"));
}
Ok(Self {
slice_size,
recovery_exponents,
})
}
#[cfg(test)]
pub fn slice_size(&self) -> usize {
self.slice_size
}
#[cfg(test)]
pub fn available_kernels() -> Vec<ForwardKernel> {
let kernels = vec![ForwardKernel::Portable, ForwardKernel::Simd];
#[cfg(target_arch = "x86_64")]
{
let mut kernels = kernels;
let capabilities = runtime_kernel_capabilities();
if capabilities.folded {
kernels.push(ForwardKernel::Folded);
}
if capabilities.avx2_jit {
kernels.push(ForwardKernel::XorJitAvx2);
}
kernels
}
#[cfg(not(target_arch = "x86_64"))]
kernels
}
#[cfg(test)]
pub fn selected_kernel(&self, requested: ForwardKernel) -> Result<ForwardKernel> {
resolve_kernel_with_capabilities(requested, runtime_kernel_capabilities())
.map(public_kernel)
}
#[cfg(test)]
pub fn encode(
&self,
sources: &[&[u8]],
options: &ForwardEncoderOptions,
) -> Result<Vec<ForwardRecoveryBlock>> {
let mut sink = VecRecoverySink::new(&self.recovery_exponents, self.slice_size);
let mut provider = InMemorySourceProvider { sources };
self.encode_to(&mut provider, options, &mut sink)?;
Ok(sink.blocks)
}
#[cfg(test)]
pub fn encode_slices_to<S: ForwardRecoverySink>(
&self,
sources: &[&[u8]],
options: &ForwardEncoderOptions,
sink: &mut S,
) -> Result<()> {
let mut provider = InMemorySourceProvider { sources };
self.encode_to(&mut provider, options, sink)
}
pub fn encode_to<P: ForwardSourceProvider + ?Sized, S: ForwardRecoverySink>(
&self,
provider: &mut P,
options: &ForwardEncoderOptions,
sink: &mut S,
) -> Result<()> {
self.encode_to_observed(provider, options, sink, None)
}
pub(crate) fn encode_to_observed<P: ForwardSourceProvider + ?Sized, S: ForwardRecoverySink>(
&self,
provider: &mut P,
options: &ForwardEncoderOptions,
sink: &mut S,
observer: Option<&mut dyn ForwardSourceObserver>,
) -> Result<()> {
let mut observer = observer;
let observer = &mut observer;
validate_provider(provider, self.slice_size)?;
check_cancel(options)?;
if self.recovery_exponents.is_empty() {
return Ok(());
}
let memory_limit = options.memory_limit.unwrap_or_else(default_memory_limit);
let (kernel, buffers) = select_kernel_for_memory(
self.slice_size,
self.recovery_exponents.len(),
provider.source_count(),
memory_limit,
options.kernel,
)?;
let contract = KernelContract::for_kernel(kernel);
let factors = FactorSource::new(provider.source_count());
let staging_areas = configured_staging_areas();
let mut staging: Vec<std::sync::Arc<AlignedBuffer>> = (0..staging_areas)
.map(|_| std::sync::Arc::new(AlignedBuffer::new(buffers.staging_bytes)))
.collect();
let mut transfers: Vec<std::sync::Arc<TransferSlot>> = (0..staging_areas)
.map(|_| std::sync::Arc::new(TransferSlot::new(buffers.transfer_bytes)))
.collect();
let mut output = AlignedBuffer::new(buffers.output_bytes);
let (band_size, band_count) = create_band_shape(self.recovery_exponents.len());
#[cfg(not(target_arch = "x86_64"))]
let _ = band_count;
#[cfg(target_arch = "x86_64")]
let mut jit_workspaces: Vec<reedsolomon_rs::xor_jit::packed::PackedJitWorkspace> =
(0..band_count).map(|_| Default::default()).collect();
#[cfg(target_arch = "x86_64")]
let jit_code_budget = buffers.jit_build_limit_bytes;
let stripe_count = self.slice_size.div_ceil(buffers.chunk_len);
let stripe_count_u32 = u32::try_from(stripe_count)
.map_err(|_| resource_limit("stripe count exceeds progress range"))?;
let total_bytes = (self.recovery_exponents.len() as u64)
.checked_mul(self.slice_size as u64)
.ok_or_else(|| resource_limit("progress byte count overflow"))?;
let batch_starts: Vec<usize> = (0..provider.source_count())
.step_by(contract.input_grouping)
.collect();
let banded = band_size < self.recovery_exponents.len();
let mut stripe_offset = 0usize;
let mut stripe_index = 0usize;
while stripe_offset < self.slice_size {
check_cancel(options)?;
let actual_len = (self.slice_size - stripe_offset).min(buffers.chunk_len);
let aligned_len = round_up(actual_len, contract.stride)?;
if banded {
encode_stripe_banded(
kernel,
provider,
options,
contract,
&factors,
&self.recovery_exponents,
&mut staging,
&mut transfers,
&mut output.as_bytes_mut()[..buffers.output_bytes],
&batch_starts,
StripeGeometry {
stripe_offset,
actual_len,
aligned_len,
output_stride: buffers.row_stride,
},
band_size,
#[cfg(target_arch = "x86_64")]
&mut jit_workspaces,
#[cfg(target_arch = "x86_64")]
jit_code_budget,
match observer.as_mut() {
Some(observer) => Some(&mut **observer),
None => None,
},
)?;
} else {
output.as_bytes_mut()[..buffers.output_bytes].fill(0);
let mut slice_lens = [0usize; MAX_INPUT_GROUPING];
let source_count = provider.source_count();
if let Some(&first_start) = batch_starts.first() {
let slot = std::sync::Arc::get_mut(&mut transfers[0])
.ok_or_else(|| resource_limit("transfer slot is still in use"))?;
fill_staging(
kernel,
std::sync::Arc::get_mut(&mut staging[0])
.ok_or_else(|| resource_limit("staging area is still in use"))?,
&mut slot.buffer,
provider,
first_start,
stripe_offset,
actual_len,
aligned_len,
contract,
&mut slice_lens,
)?;
if let Some(observer) = observer.as_mut() {
observe_batch(
&mut **observer,
transfers[0].buffer.as_bytes(),
first_start,
live_batch_inputs(source_count, first_start, contract),
transfer_slot_stride(aligned_len)?,
&slice_lens,
)?;
}
}
for (batch_index, &source_start) in batch_starts.iter().enumerate() {
check_cancel(options)?;
let live_inputs = live_batch_inputs(source_count, source_start, contract);
let next_start = batch_starts.get(batch_index + 1).copied();
let current_area = batch_index % staging_areas;
let next_area = (batch_index + 1) % staging_areas;
accumulate_batch(
kernel,
&mut output.as_bytes_mut()[..buffers.output_bytes],
&staging[current_area],
&factors,
&self.recovery_exponents,
source_start,
live_inputs,
aligned_len,
buffers.row_stride,
contract,
band_size,
#[cfg(target_arch = "x86_64")]
&mut jit_workspaces,
#[cfg(target_arch = "x86_64")]
jit_code_budget,
)?;
if let Some(next_start) = next_start {
let slot = std::sync::Arc::get_mut(&mut transfers[next_area])
.ok_or_else(|| resource_limit("transfer slot is still in use"))?;
fill_staging(
kernel,
std::sync::Arc::get_mut(&mut staging[next_area])
.ok_or_else(|| resource_limit("staging area is still in use"))?,
&mut slot.buffer,
provider,
next_start,
stripe_offset,
actual_len,
aligned_len,
contract,
&mut slice_lens,
)?;
if let Some(observer) = observer.as_mut() {
observe_batch(
&mut **observer,
transfers[next_area].buffer.as_bytes(),
next_start,
live_batch_inputs(source_count, next_start, contract),
transfer_slot_stride(aligned_len)?,
&slice_lens,
)?;
}
}
}
finish_output(
kernel,
&mut output.as_bytes_mut()[..buffers.output_bytes],
buffers.row_stride,
aligned_len,
self.recovery_exponents.len(),
)?;
}
for (output_index, &exponent) in self.recovery_exponents.iter().enumerate() {
let start = output_index
.checked_mul(buffers.row_stride)
.ok_or_else(|| resource_limit("output stripe offset overflow"))?;
let end = start
.checked_add(actual_len)
.ok_or_else(|| resource_limit("output stripe end overflow"))?;
sink.write_recovery_chunk(
output_index,
exponent,
stripe_offset as u64,
&output.as_bytes()[start..end],
)?;
}
stripe_index += 1;
let completed_stripe = u32::try_from(stripe_index - 1)
.map_err(|_| resource_limit("completed stripe exceeds progress range"))?;
report_progress(
options,
completed_stripe,
stripe_count_u32,
(stripe_index as u64)
.saturating_mul(self.recovery_exponents.len() as u64)
.saturating_mul(buffers.chunk_len as u64)
.min(total_bytes),
total_bytes,
);
stripe_offset = stripe_offset
.checked_add(actual_len)
.ok_or_else(|| resource_limit("stripe offset overflow"))?;
}
check_cancel(options)
}
}
#[derive(Clone, Copy)]
struct StripeGeometry {
stripe_offset: usize,
actual_len: usize,
aligned_len: usize,
output_stride: usize,
}
struct TransferSlot {
buffer: AlignedBuffer,
slot_stride: usize,
slice_lens: [usize; MAX_INPUT_GROUPING],
}
impl TransferSlot {
fn new(bytes: usize) -> Self {
Self {
buffer: AlignedBuffer::new(bytes),
slot_stride: 0,
slice_lens: [0; MAX_INPUT_GROUPING],
}
}
}
#[derive(Clone)]
struct BatchTicket {
staging: std::sync::Arc<AlignedBuffer>,
transfer: std::sync::Arc<TransferSlot>,
source_start: usize,
live_inputs: usize,
}
struct FeedState {
tickets: Vec<Option<BatchTicket>>,
published: usize,
completed: usize,
done: Vec<usize>,
hash_turn: usize,
failed: bool,
}
struct StripeFeed {
areas: usize,
state: std::sync::Mutex<FeedState>,
ready: std::sync::Condvar,
free: std::sync::Condvar,
hashed: std::sync::Condvar,
band_count: usize,
}
impl StripeFeed {
fn new(band_count: usize, areas: usize) -> Self {
Self {
areas,
state: std::sync::Mutex::new(FeedState {
tickets: vec![None; areas],
published: 0,
completed: 0,
done: vec![0; areas],
hash_turn: 0,
failed: false,
}),
ready: std::sync::Condvar::new(),
free: std::sync::Condvar::new(),
hashed: std::sync::Condvar::new(),
band_count,
}
}
fn lock(&self) -> std::sync::MutexGuard<'_, FeedState> {
self.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn wait_for_area(&self, batch_index: usize) -> bool {
let mut state = self.lock();
while !state.failed && state.completed + self.areas <= batch_index {
state = self
.free
.wait(state)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
if state.failed {
return false;
}
state.tickets[batch_index % self.areas] = None;
true
}
fn publish(&self, batch_index: usize, ticket: BatchTicket) {
let mut state = self.lock();
state.tickets[batch_index % self.areas] = Some(ticket);
state.published = batch_index + 1;
drop(state);
self.ready.notify_all();
}
fn acquire(&self, batch_index: usize) -> Option<BatchTicket> {
let mut state = self.lock();
while !state.failed && state.published <= batch_index {
state = self
.ready
.wait(state)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
if state.failed {
return None;
}
state.tickets[batch_index % self.areas].clone()
}
fn release(&self, batch_index: usize) {
let mut state = self.lock();
let area = batch_index % self.areas;
state.done[area] += 1;
if state.done[area] == self.band_count {
state.done[area] = 0;
state.completed = batch_index + 1;
drop(state);
self.free.notify_all();
}
}
fn wait_for_hash_turn(&self, batch_index: usize) -> bool {
let mut state = self.lock();
while !state.failed && state.hash_turn < batch_index {
state = self
.hashed
.wait(state)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
!state.failed
}
fn finish_hash_turn(&self, batch_index: usize) {
let mut state = self.lock();
state.hash_turn = batch_index + 1;
drop(state);
self.hashed.notify_all();
}
fn fail(&self) {
let mut state = self.lock();
state.failed = true;
drop(state);
self.ready.notify_all();
self.free.notify_all();
self.hashed.notify_all();
}
}
#[allow(clippy::too_many_arguments)]
fn encode_stripe_banded<P: ForwardSourceProvider + ?Sized>(
kernel: ResolvedKernel,
provider: &mut P,
options: &ForwardEncoderOptions,
contract: KernelContract,
factors: &FactorSource,
exponents: &[RecoveryExponent],
staging: &mut [std::sync::Arc<AlignedBuffer>],
transfers: &mut [std::sync::Arc<TransferSlot>],
output: &mut [u8],
batch_starts: &[usize],
geometry: StripeGeometry,
band_size: usize,
#[cfg(target_arch = "x86_64")]
jit_workspaces: &mut [reedsolomon_rs::xor_jit::packed::PackedJitWorkspace],
#[cfg(target_arch = "x86_64")] jit_code_budget: usize,
observer: Option<&mut dyn ForwardSourceObserver>,
) -> Result<()> {
debug_assert_eq!(output.len(), exponents.len() * geometry.output_stride);
let band_bytes = checked_mul(
band_size,
geometry.output_stride,
"band byte range overflow",
)?;
let band_count = exponents.len().div_ceil(band_size);
#[cfg(target_arch = "x86_64")]
debug_assert_eq!(jit_workspaces.len(), band_count);
let batch_count = batch_starts.len();
let feed = StripeFeed::new(band_count, configured_staging_areas());
let feed = &feed;
let source_count = provider.source_count();
let observer = observer.map(std::sync::Mutex::new);
let observer = observer.as_ref();
let mut band_results: Vec<Result<()>> = Vec::with_capacity(band_count);
let produced = std::thread::scope(|scope| {
let mut handles = Vec::with_capacity(band_count);
let bands = output
.chunks_mut(band_bytes)
.zip(exponents.chunks(band_size));
#[cfg(target_arch = "x86_64")]
let bands = bands.zip(jit_workspaces.iter_mut());
for (band_index, band) in bands.enumerate() {
#[cfg(target_arch = "x86_64")]
let ((band_output, band_exponents), jit_workspace) = band;
#[cfg(not(target_arch = "x86_64"))]
let (band_output, band_exponents) = band;
handles.push(scope.spawn(move || {
accumulate_band_stream(
feed,
kernel,
band_output,
band_exponents,
factors,
contract,
geometry,
batch_count,
#[cfg(target_arch = "x86_64")]
jit_workspace,
#[cfg(target_arch = "x86_64")]
jit_code_budget,
BandHashDuty {
band_index,
band_count,
observer,
},
)
}));
}
let produced = produce_stripe(
kernel,
provider,
options,
contract,
staging,
transfers,
batch_starts,
geometry,
source_count,
feed,
);
if produced.is_err() {
feed.fail();
}
band_results.extend(handles.into_iter().map(|handle| {
handle
.join()
.unwrap_or_else(|payload| std::panic::resume_unwind(payload))
}));
produced
});
produced?;
for result in band_results {
result?;
}
Ok(())
}
#[derive(Clone, Copy)]
struct BandHashDuty<'turn, 'observer> {
band_index: usize,
band_count: usize,
observer: Option<&'turn std::sync::Mutex<&'observer mut dyn ForwardSourceObserver>>,
}
#[allow(clippy::too_many_arguments)]
fn produce_stripe<P: ForwardSourceProvider + ?Sized>(
kernel: ResolvedKernel,
provider: &mut P,
options: &ForwardEncoderOptions,
contract: KernelContract,
staging: &mut [std::sync::Arc<AlignedBuffer>],
transfers: &mut [std::sync::Arc<TransferSlot>],
batch_starts: &[usize],
geometry: StripeGeometry,
source_count: usize,
feed: &StripeFeed,
) -> Result<()> {
let slot_stride = transfer_slot_stride(geometry.aligned_len)?;
for (batch_index, &source_start) in batch_starts.iter().enumerate() {
check_cancel(options)?;
if !feed.wait_for_area(batch_index) {
return Ok(());
}
let area = batch_index % feed.areas;
let staged = std::sync::Arc::get_mut(&mut staging[area])
.ok_or_else(|| resource_limit("staging area is still in use"))?;
let slot = std::sync::Arc::get_mut(&mut transfers[area])
.ok_or_else(|| resource_limit("transfer slot is still in use"))?;
slot.slot_stride = slot_stride;
fill_staging(
kernel,
staged,
&mut slot.buffer,
provider,
source_start,
geometry.stripe_offset,
geometry.actual_len,
geometry.aligned_len,
contract,
&mut slot.slice_lens,
)?;
feed.publish(
batch_index,
BatchTicket {
staging: std::sync::Arc::clone(&staging[area]),
transfer: std::sync::Arc::clone(&transfers[area]),
source_start,
live_inputs: live_batch_inputs(source_count, source_start, contract),
},
);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn accumulate_band_stream(
feed: &StripeFeed,
kernel: ResolvedKernel,
band_output: &mut [u8],
band_exponents: &[RecoveryExponent],
factors: &FactorSource,
contract: KernelContract,
geometry: StripeGeometry,
batch_count: usize,
#[cfg(target_arch = "x86_64")]
jit_workspace: &mut reedsolomon_rs::xor_jit::packed::PackedJitWorkspace,
#[cfg(target_arch = "x86_64")] jit_code_budget: usize,
hash_duty: BandHashDuty<'_, '_>,
) -> Result<()> {
band_output.fill(0);
for batch_index in 0..batch_count {
let Some(ticket) = feed.acquire(batch_index) else {
return Ok(());
};
let accumulated = accumulate_band(
kernel,
band_output,
&ticket.staging,
factors,
band_exponents,
ticket.source_start,
ticket.live_inputs,
geometry.aligned_len,
geometry.output_stride,
contract,
#[cfg(target_arch = "x86_64")]
jit_workspace,
#[cfg(target_arch = "x86_64")]
jit_code_budget,
);
if let Err(error) = accumulated {
drop(ticket);
feed.fail();
return Err(error);
}
if let Some(hashed) = hash_batch_if_owned(feed, &ticket, batch_index, hash_duty) {
if let Err(error) = hashed {
drop(ticket);
feed.fail();
return Err(error);
}
feed.finish_hash_turn(batch_index);
}
drop(ticket);
feed.release(batch_index);
}
finish_band_rows(
kernel,
band_output,
geometry.output_stride,
geometry.aligned_len,
band_exponents.len(),
)
.inspect_err(|_| feed.fail())
}
fn hash_batch_if_owned(
feed: &StripeFeed,
ticket: &BatchTicket,
batch_index: usize,
duty: BandHashDuty<'_, '_>,
) -> Option<Result<()>> {
let observer = duty.observer?;
if batch_index % duty.band_count != duty.band_index {
return None;
}
if !feed.wait_for_hash_turn(batch_index) {
return None;
}
let mut observer = observer
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
Some(observe_batch(
&mut **observer,
ticket.transfer.buffer.as_bytes(),
ticket.source_start,
ticket.live_inputs,
ticket.transfer.slot_stride,
&ticket.transfer.slice_lens,
))
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ResolvedKernel {
Portable,
Simd,
#[cfg(target_arch = "x86_64")]
Folded,
#[cfg(target_arch = "x86_64")]
XorJitAvx2,
}
#[cfg(test)]
fn public_kernel(kernel: ResolvedKernel) -> ForwardKernel {
match kernel {
ResolvedKernel::Portable => ForwardKernel::Portable,
ResolvedKernel::Simd => ForwardKernel::Simd,
#[cfg(target_arch = "x86_64")]
ResolvedKernel::Folded => ForwardKernel::Folded,
#[cfg(target_arch = "x86_64")]
ResolvedKernel::XorJitAvx2 => ForwardKernel::XorJitAvx2,
}
}
fn resolve_kernel_with_capabilities(
requested: ForwardKernel,
capabilities: KernelCapabilities,
) -> Result<ResolvedKernel> {
#[cfg(not(target_arch = "x86_64"))]
let _ = capabilities;
match requested {
ForwardKernel::Portable => Ok(ResolvedKernel::Portable),
ForwardKernel::Simd => Ok(ResolvedKernel::Simd),
#[cfg(target_arch = "x86_64")]
ForwardKernel::Folded => {
if capabilities.folded {
return Ok(ResolvedKernel::Folded);
}
Err(unavailable_kernel("folded AVX2"))
}
#[cfg(target_arch = "x86_64")]
ForwardKernel::XorJitAvx2 => {
if capabilities.avx2_jit {
return Ok(ResolvedKernel::XorJitAvx2);
}
Err(unavailable_kernel("packed AVX2 XOR-JIT"))
}
ForwardKernel::Auto => {
#[cfg(target_arch = "x86_64")]
{
if capabilities.folded {
return Ok(ResolvedKernel::Folded);
}
if capabilities.avx2_jit {
return Ok(ResolvedKernel::XorJitAvx2);
}
}
Ok(ResolvedKernel::Simd)
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct KernelCapabilities {
folded: bool,
folded_wide: bool,
avx2_jit: bool,
}
fn runtime_kernel_capabilities() -> KernelCapabilities {
#[cfg(target_arch = "x86_64")]
{
KernelCapabilities {
folded: gf_simd::altmap_supported(),
folded_wide: gf_simd::folded_wide_shuffle_available(),
avx2_jit: reedsolomon_rs::xor_jit::JitWidth::detect().is_some(),
}
}
#[cfg(not(target_arch = "x86_64"))]
KernelCapabilities {
folded: false,
folded_wide: false,
avx2_jit: false,
}
}
#[cfg_attr(not(target_arch = "x86_64"), allow(dead_code))]
const AFFINE_TILE_BYTES: usize = 4 * 1024;
const TABLE_TILE_BYTES: usize = 8 * 1024;
#[cfg_attr(not(target_arch = "x86_64"), allow(dead_code))]
const UNTILED: usize = usize::MAX;
fn configured_tile_bytes() -> Option<usize> {
static CONFIGURED: std::sync::OnceLock<Option<usize>> = std::sync::OnceLock::new();
*CONFIGURED.get_or_init(|| {
std::env::var("WEAVER_PAR2_CREATE_TILE")
.ok()
.and_then(|value| value.trim().parse::<usize>().ok())
.map(|bytes| if bytes == 0 { UNTILED } else { bytes })
})
}
fn family_tile_bytes(default_bytes: usize, stride: usize) -> usize {
let requested = configured_tile_bytes().unwrap_or(default_bytes);
if requested == UNTILED || stride == 0 {
return requested;
}
requested
.max(stride)
.div_ceil(stride)
.saturating_mul(stride)
}
const SKEW_PERIOD_BYTES: usize = 4096;
const SKEW_TARGET_RESIDUE_BYTES: usize = 1024;
fn stripe_skew_bytes(aligned_len: usize) -> usize {
let residue = aligned_len % SKEW_PERIOD_BYTES;
let wanted = (SKEW_TARGET_RESIDUE_BYTES + SKEW_PERIOD_BYTES - residue) % SKEW_PERIOD_BYTES;
let cap = aligned_len / 8;
wanted.min(cap) / 64 * 64
}
fn lane_stride(contract: KernelContract, aligned_len: usize) -> usize {
if contract.skewed_lanes {
aligned_len + stripe_skew_bytes(aligned_len)
} else {
aligned_len
}
}
#[derive(Clone, Copy)]
struct StagingLayout {
interleave: usize,
lanes: usize,
aligned_len: usize,
group_pitch: usize,
}
impl StagingLayout {
fn new(contract: KernelContract, aligned_len: usize, lane_stride: usize) -> Self {
let lanes = contract.input_grouping.max(1);
let interleave = contract.interleave_lanes.clamp(1, lanes);
let group_pitch = if interleave == 1 {
lane_stride
} else {
interleave * aligned_len + stripe_skew_bytes(aligned_len)
};
Self {
interleave,
lanes,
aligned_len,
group_pitch,
}
}
fn group_count(&self) -> usize {
self.lanes.div_ceil(self.interleave).max(1)
}
fn group_width(&self, group: usize) -> usize {
self.lanes
.saturating_sub(group * self.interleave)
.min(self.interleave)
}
fn group_base(&self, group: usize) -> usize {
group * self.group_pitch
}
fn total_bytes(&self) -> Option<usize> {
let last = self.group_count() - 1;
last.checked_mul(self.group_pitch)?
.checked_add(self.group_width(last).checked_mul(self.aligned_len)?)
}
fn group_tile(&self, group: usize, tile_start: usize, tile_len: usize) -> (usize, usize) {
let width = self.group_width(group);
let start = self.group_base(group) + tile_start * width;
(start, start + tile_len * width)
}
}
const COEFF_ROWS: usize = 16;
fn stripe_tiles(aligned_len: usize, tile_bytes: usize) -> impl Iterator<Item = (usize, usize)> {
let tile = tile_bytes.min(aligned_len).max(1);
(0..aligned_len)
.step_by(tile)
.map(move |start| (start, tile.min(aligned_len - start)))
}
#[derive(Clone, Copy)]
struct KernelContract {
stride: usize,
input_grouping: usize,
tile_bytes: usize,
skewed_lanes: bool,
interleave_lanes: usize,
}
impl KernelContract {
fn for_kernel(kernel: ResolvedKernel) -> Self {
match kernel {
ResolvedKernel::Portable => Self {
stride: 2,
input_grouping: configured_input_grouping(),
tile_bytes: family_tile_bytes(TABLE_TILE_BYTES, 2),
skewed_lanes: true,
interleave_lanes: 1,
},
ResolvedKernel::Simd => Self {
stride: simd_stride(),
input_grouping: configured_input_grouping(),
tile_bytes: family_tile_bytes(TABLE_TILE_BYTES, simd_stride()),
skewed_lanes: true,
interleave_lanes: configured_interleave_lanes(),
},
#[cfg(target_arch = "x86_64")]
ResolvedKernel::Folded => Self {
stride: gf_simd::SPLIT_BLOCK_BYTES,
input_grouping: DEFAULT_INPUT_GROUPING,
tile_bytes: family_tile_bytes(
if gf_simd::folded_uses_gfni() {
AFFINE_TILE_BYTES
} else {
TABLE_TILE_BYTES
},
gf_simd::SPLIT_BLOCK_BYTES,
),
skewed_lanes: true,
interleave_lanes: 1,
},
#[cfg(target_arch = "x86_64")]
ResolvedKernel::XorJitAvx2 => Self {
stride: reedsolomon_rs::xor_jit::JitWidth::Avx2.block_bytes(),
input_grouping: DEFAULT_INPUT_GROUPING,
tile_bytes: UNTILED,
skewed_lanes: false,
interleave_lanes: 1,
},
}
}
}
fn factor_workspace_bytes(kernel: ResolvedKernel, source_count: usize) -> Result<usize> {
let constants = checked_mul(
source_count,
size_of::<u16>(),
"factor constant allocation overflow",
)?;
let grouping = KernelContract::for_kernel(kernel).input_grouping;
let row = checked_mul(
MAX_INPUT_GROUPING,
size_of::<u16>(),
"factor row allocation overflow",
)?;
let active = match kernel {
ResolvedKernel::Portable => row,
ResolvedKernel::Simd => checked_add(
row,
checked_add(
checked_mul(
checked_mul(COEFF_ROWS, grouping, "prepared factor allocation overflow")?,
size_of::<gf_simd::PreparedInputFactor>(),
"prepared factor allocation overflow",
)?,
checked_mul(
MAX_INPUT_GROUPING,
size_of::<PreparedFactorSrc>(),
"prepared source allocation overflow",
)?,
"prepared factor allocation overflow",
)?,
"prepared factor allocation overflow",
)?,
#[cfg(target_arch = "x86_64")]
ResolvedKernel::Folded => {
let groups = DEFAULT_INPUT_GROUPING / gf_simd::FOLDED_GROUP;
let chunk_lanes = checked_mul(
COEFF_ROWS,
DEFAULT_INPUT_GROUPING,
"folded table allocation overflow",
)?;
let affine_tables = checked_mul(
chunk_lanes,
size_of::<gf_simd::AffineMulMatrices>(),
"folded affine table allocation overflow",
)?;
let shuffle_tables = checked_mul(
chunk_lanes,
size_of::<gf_simd::Shuffle2xTables>(),
"folded shuffle table allocation overflow",
)?;
let staging_views = checked_mul(
groups,
size_of::<&[u8]>(),
"folded staging view allocation overflow",
)?;
let affine_sets = checked_mul(
groups,
size_of::<[&gf_simd::AffineMulMatrices; gf_simd::FOLDED_GROUP]>(),
"folded affine set allocation overflow",
)?;
let shuffle_sets = checked_mul(
groups,
size_of::<[&gf_simd::Shuffle2xTables; gf_simd::FOLDED_GROUP]>(),
"folded shuffle set allocation overflow",
)?;
checked_add(
row,
[
affine_tables,
shuffle_tables,
staging_views,
affine_sets,
shuffle_sets,
]
.into_iter()
.try_fold(0usize, |total, bytes| {
checked_add(total, bytes, "folded factor allocation overflow")
})?,
"folded factor allocation overflow",
)?
}
#[cfg(target_arch = "x86_64")]
ResolvedKernel::XorJitAvx2 => row,
};
checked_add(constants, active, "factor workspace allocation overflow")
}
fn jit_workspace_bytes(kernel: ResolvedKernel, output_count: usize) -> Result<(usize, usize)> {
#[cfg(target_arch = "x86_64")]
if matches!(kernel, ResolvedKernel::XorJitAvx2) {
let (band_size, band_count) = create_band_shape(output_count.max(1));
let estimate = reedsolomon_rs::xor_jit::packed::PackedJitBatch::memory_upper_bound(
reedsolomon_rs::xor_jit::JitWidth::Avx2,
band_size.max(1),
DEFAULT_INPUT_GROUPING,
)
.ok_or_else(|| resource_limit("packed JIT workspace size overflows"))?;
let reserved = estimate
.peak_bytes
.checked_mul(band_count)
.ok_or_else(|| resource_limit("banded JIT workspace accounting overflows"))?;
return Ok((reserved, estimate.executable_arena_bytes));
}
let _ = (kernel, output_count);
Ok((0, 0))
}
fn parse_kernel_override(value: &str) -> Result<ForwardKernel> {
match value.trim().to_ascii_lowercase().as_str() {
"auto" => Ok(ForwardKernel::Auto),
"portable" => Ok(ForwardKernel::Portable),
"simd" => Ok(ForwardKernel::Simd),
#[cfg(target_arch = "x86_64")]
"folded" => Ok(ForwardKernel::Folded),
#[cfg(target_arch = "x86_64")]
"xor-jit-avx2" => Ok(ForwardKernel::XorJitAvx2),
other => Err(invalid_input(format!(
"WEAVER_PAR2_CREATE_KERNEL={other:?} names no kernel on this \
architecture; use auto, portable, simd, folded or xor-jit-avx2"
))),
}
}
fn configured_kernel_override() -> Result<Option<ForwardKernel>> {
static CONFIGURED: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
CONFIGURED
.get_or_init(|| std::env::var("WEAVER_PAR2_CREATE_KERNEL").ok())
.as_deref()
.filter(|value| !value.trim().is_empty())
.map(parse_kernel_override)
.transpose()
}
fn configured_stripe_cap_bytes() -> Option<usize> {
static CONFIGURED: std::sync::OnceLock<Option<usize>> = std::sync::OnceLock::new();
*CONFIGURED.get_or_init(|| {
std::env::var("WEAVER_PAR2_CREATE_STRIPE_MIB")
.ok()
.and_then(|value| value.trim().parse::<usize>().ok())
.filter(|&mib| mib != 0)
.and_then(|mib| mib.checked_mul(1024 * 1024))
})
}
struct BufferPlan {
chunk_len: usize,
#[cfg_attr(not(test), allow(dead_code))]
aligned_chunk_len: usize,
row_stride: usize,
staging_bytes: usize,
output_bytes: usize,
transfer_bytes: usize,
data_bytes: usize,
memory_bytes: usize,
#[cfg_attr(not(target_arch = "x86_64"), allow(dead_code))]
jit_build_limit_bytes: usize,
}
impl BufferPlan {
fn new_with_reserved(
slice_size: usize,
output_count: usize,
contract: KernelContract,
memory_limit: usize,
factor_workspace_bytes: usize,
jit_workspace_bytes: usize,
jit_build_limit_bytes: usize,
) -> Result<Self> {
if memory_limit == 0 {
return Err(resource_limit("forward memory limit is zero"));
}
let reserved_bytes = checked_add(
factor_workspace_bytes,
jit_workspace_bytes,
"forward persistent memory accounting overflow",
)?;
let stripe_memory_limit = memory_limit.checked_sub(reserved_bytes).ok_or_else(|| {
resource_limit(format!(
"forward persistent allocations need {reserved_bytes} bytes, limit is {memory_limit}"
))
})?;
let chosen_stripe_limit = match configured_stripe_cap_bytes() {
Some(cap) => stripe_memory_limit.min(cap),
None => stripe_memory_limit,
};
let mut chunk_len = if slice_size >= contract.stride {
slice_size - slice_size % contract.stride
} else {
slice_size
};
chunk_len = chunk_len.max(2);
loop {
let aligned_chunk_len = round_up(chunk_len.min(slice_size), contract.stride)?;
let skew = stripe_skew_bytes(aligned_chunk_len);
let lane_alloc = checked_add(aligned_chunk_len, skew, "staging lane overflow")?;
let row_stride = lane_alloc;
let staging_bytes = checked_mul(
contract.input_grouping,
lane_alloc,
"staging allocation overflow",
)?;
let output_bytes = checked_mul(output_count, row_stride, "output allocation overflow")?;
let aligned_allocation_bytes = checked_mul(
aligned_chunk_len.div_ceil(64),
64,
"aligned buffer allocation overflow",
)?;
let skewed_allocation_bytes = checked_add(
aligned_allocation_bytes,
skew,
"aligned buffer allocation overflow",
)?;
let transfer_bytes = checked_mul(
contract.input_grouping,
aligned_allocation_bytes,
"transfer allocation overflow",
)?;
let data_bytes = checked_add(
checked_mul(
configured_staging_areas(),
checked_mul(
contract.input_grouping,
skewed_allocation_bytes,
"staging allocation overflow",
)?,
"staging allocation overflow",
)?,
checked_add(
checked_mul(
output_count,
skewed_allocation_bytes,
"output allocation overflow",
)?,
checked_mul(
configured_staging_areas(),
transfer_bytes,
"transfer allocation overflow",
)?,
"forward buffer allocation overflow",
)?,
"forward buffer allocation overflow",
)?;
if data_bytes <= chosen_stripe_limit
|| (chunk_len <= 2 && data_bytes <= stripe_memory_limit)
{
return Ok(Self {
chunk_len: chunk_len.min(slice_size),
aligned_chunk_len,
row_stride,
staging_bytes,
output_bytes,
transfer_bytes,
data_bytes,
memory_bytes: reserved_bytes + data_bytes,
jit_build_limit_bytes,
});
}
if chunk_len <= 2 {
return Err(resource_limit(format!(
"forward persistent allocations and stripe buffers need {} bytes, limit is {memory_limit}",
reserved_bytes + data_bytes
)));
}
if slice_size < contract.stride {
chunk_len = 2;
continue;
}
let bytes_per_aligned_byte = data_bytes / aligned_chunk_len;
let max_aligned_len =
(chosen_stripe_limit / bytes_per_aligned_byte) / contract.stride * contract.stride;
let smaller_chunk_len = chunk_len.saturating_sub(contract.stride).max(2);
chunk_len = max_aligned_len.max(2).min(smaller_chunk_len);
}
}
}
fn select_kernel_for_memory(
slice_size: usize,
output_count: usize,
source_count: usize,
memory_limit: usize,
requested: ForwardKernel,
) -> Result<(ResolvedKernel, BufferPlan)> {
select_kernel_for_memory_with_capabilities(
slice_size,
output_count,
source_count,
memory_limit,
requested,
runtime_kernel_capabilities(),
)
}
fn select_kernel_for_memory_with_capabilities(
slice_size: usize,
output_count: usize,
source_count: usize,
memory_limit: usize,
requested: ForwardKernel,
capabilities: KernelCapabilities,
) -> Result<(ResolvedKernel, BufferPlan)> {
let requested = match configured_kernel_override()? {
Some(forced) => forced,
None => requested,
};
let candidates = match requested {
ForwardKernel::Auto => auto_kernel_candidates(capabilities),
requested => vec![resolve_kernel_with_capabilities(requested, capabilities)?],
};
let mut last_error = None;
for kernel in candidates {
let contract = KernelContract::for_kernel(kernel);
let factor_bytes = factor_workspace_bytes(kernel, source_count)?;
let (jit_bytes, jit_arena_bytes) = jit_workspace_bytes(kernel, output_count)?;
match BufferPlan::new_with_reserved(
slice_size,
output_count,
contract,
memory_limit,
factor_bytes,
jit_bytes,
jit_arena_bytes,
) {
Ok(buffers) => return Ok((kernel, buffers)),
Err(error) => last_error = Some(error),
}
}
Err(last_error.unwrap_or_else(|| resource_limit("no forward arithmetic kernel is available")))
}
fn auto_kernel_candidates(capabilities: KernelCapabilities) -> Vec<ResolvedKernel> {
let mut kernels = Vec::with_capacity(4);
let preferred = resolve_kernel_with_capabilities(ForwardKernel::Auto, capabilities)
.expect("automatic forward kernel selection cannot fail");
kernels.push(preferred);
#[cfg(target_arch = "x86_64")]
{
if capabilities.avx2_jit && preferred != ResolvedKernel::XorJitAvx2 {
kernels.push(ResolvedKernel::XorJitAvx2);
}
if capabilities.folded && preferred != ResolvedKernel::Folded {
kernels.push(ResolvedKernel::Folded);
}
}
let simd = resolve_kernel_with_capabilities(ForwardKernel::Simd, capabilities)
.expect("direct grouped SIMD selection cannot fail");
if preferred != simd {
kernels.push(simd);
}
let portable = resolve_kernel_with_capabilities(ForwardKernel::Portable, capabilities)
.expect("portable selection cannot fail");
if preferred != portable {
kernels.push(portable);
}
kernels
}
struct FactorSource {
constants: Vec<u16>,
}
impl FactorSource {
fn new(source_count: usize) -> Self {
Self {
constants: gf::input_slice_constants(source_count),
}
}
fn row_factors(&self, source_start: usize, live_inputs: usize) -> RowFactors {
let mut logs = [0u16; MAX_INPUT_GROUPING];
for (lane, log) in logs[..live_inputs].iter_mut().enumerate() {
let constant = self.constants[source_start + lane];
debug_assert_ne!(constant, 0, "input slice constants are never zero");
*log = gf::log(constant);
}
RowFactors { logs, live_inputs }
}
}
struct RowFactors {
logs: [u16; MAX_INPUT_GROUPING],
live_inputs: usize,
}
impl RowFactors {
fn fill_row(&self, exponent: RecoveryExponent, row: &mut [u16; MAX_INPUT_GROUPING]) {
row.fill(0);
for (factor, &log) in row[..self.live_inputs]
.iter_mut()
.zip(self.logs[..self.live_inputs].iter())
{
*factor = gf::pow_from_log(log, exponent);
}
}
}
pub(crate) fn forward_stripe_count(
slice_size: u64,
source_count: usize,
output_count: usize,
memory_limit: usize,
requested_kernel: ForwardKernel,
) -> Result<usize> {
if output_count == 0 {
return Ok(0);
}
let slice_size = usize::try_from(slice_size)
.map_err(|_| resource_limit("slice size exceeds addressable memory"))?;
let (_, buffers) = select_kernel_for_memory(
slice_size,
output_count,
source_count,
memory_limit,
requested_kernel,
)?;
Ok(slice_size.div_ceil(buffers.chunk_len))
}
pub(crate) fn estimate_forward_memory(
slice_size: u64,
source_count: usize,
output_count: usize,
memory_limit: usize,
requested_kernel: ForwardKernel,
) -> Result<ForwardMemoryEstimate> {
if output_count == 0 {
return Ok(ForwardMemoryEstimate {
factor_workspace_bytes: 0,
jit_workspace_bytes: 0,
stripe_buffer_bytes: 0,
processing_peak_bytes: 0,
});
}
let slice_size = usize::try_from(slice_size)
.map_err(|_| resource_limit("slice size exceeds addressable memory"))?;
let (kernel, buffers) = select_kernel_for_memory(
slice_size,
output_count,
source_count,
memory_limit,
requested_kernel,
)?;
let factor_workspace_bytes = factor_workspace_bytes(kernel, source_count)?;
let (jit_workspace_bytes, _) = jit_workspace_bytes(kernel, output_count)?;
Ok(ForwardMemoryEstimate {
factor_workspace_bytes,
jit_workspace_bytes,
stripe_buffer_bytes: buffers.data_bytes,
processing_peak_bytes: buffers.memory_bytes,
})
}
#[repr(align(64))]
#[derive(Clone, Copy)]
struct AlignedCell(pub [u8; 64]);
impl AlignedCell {
fn as_ptr(&self) -> *const u8 {
self.0.as_ptr()
}
fn as_mut_ptr(&mut self) -> *mut u8 {
self.0.as_mut_ptr()
}
}
struct AlignedBuffer {
cells: Vec<AlignedCell>,
len: usize,
}
impl AlignedBuffer {
fn new(len: usize) -> Self {
Self {
cells: vec![AlignedCell([0; 64]); len.div_ceil(64)],
len,
}
}
fn as_bytes(&self) -> &[u8] {
let ptr = self
.cells
.first()
.map_or_else(|| self.cells.as_ptr().cast::<u8>(), AlignedCell::as_ptr);
unsafe { std::slice::from_raw_parts(ptr, self.len) }
}
fn as_bytes_mut(&mut self) -> &mut [u8] {
let ptr = if self.cells.is_empty() {
self.cells.as_mut_ptr().cast::<u8>()
} else {
self.cells[0].as_mut_ptr()
};
unsafe { std::slice::from_raw_parts_mut(ptr, self.len) }
}
}
#[allow(clippy::too_many_arguments)]
fn fill_staging<P: ForwardSourceProvider + ?Sized>(
kernel: ResolvedKernel,
staging: &mut AlignedBuffer,
transfer: &mut AlignedBuffer,
provider: &mut P,
source_start: usize,
stripe_offset: usize,
actual_len: usize,
aligned_len: usize,
contract: KernelContract,
slice_lens: &mut [usize; MAX_INPUT_GROUPING],
) -> Result<()> {
let staging_bytes = staging.as_bytes_mut();
staging_bytes.fill(0);
let slot_stride = transfer_slot_stride(aligned_len)?;
let transfer_bytes = transfer.as_bytes_mut();
if transfer_bytes.len() < contract.input_grouping.saturating_mul(slot_stride) {
return Err(resource_limit(
"transfer buffer is shorter than one input batch",
));
}
let lane_stride = lane_stride(contract, aligned_len);
let layout = StagingLayout::new(contract, aligned_len, lane_stride);
let layout_bytes = layout
.total_bytes()
.ok_or_else(|| resource_limit("staging lane offset overflow"))?;
if staging_bytes.len() < layout_bytes {
return Err(resource_limit(
"staging buffer is shorter than the batch layout",
));
}
let source_count = provider.source_count();
*slice_lens = [0; MAX_INPUT_GROUPING];
for (lane, slice_len) in slice_lens[..contract.input_grouping].iter_mut().enumerate() {
let slot_start = lane * slot_stride;
transfer_bytes[slot_start..slot_start + aligned_len].fill(0);
let source_index = source_start + lane;
if source_index < source_count {
*slice_len = provider.read_source_chunk(
source_index,
stripe_offset,
&mut transfer_bytes[slot_start..slot_start + actual_len],
)?;
}
match kernel {
ResolvedKernel::Portable | ResolvedKernel::Simd => {
if layout.interleave == 1 {
let start = layout.group_base(lane);
staging_bytes[start..start + aligned_len]
.copy_from_slice(&transfer_bytes[slot_start..slot_start + aligned_len]);
} else {
const BLOCK: usize = gf_simd::INPUT_BATCH_BLOCK_BYTES;
debug_assert_eq!(aligned_len % BLOCK, 0, "interleaved stripe must be blocked");
let group = lane / layout.interleave;
let step = layout.group_width(group) * BLOCK;
let mut start = layout.group_base(group) + (lane % layout.interleave) * BLOCK;
for block in
transfer_bytes[slot_start..slot_start + aligned_len].chunks_exact(BLOCK)
{
staging_bytes[start..start + BLOCK].copy_from_slice(block);
start += step;
}
}
}
#[cfg(target_arch = "x86_64")]
ResolvedKernel::Folded => {
let fold_group = lane / gf_simd::FOLDED_GROUP;
let group_lane = lane % gf_simd::FOLDED_GROUP;
let group_start = fold_group
.checked_mul(gf_simd::FOLDED_GROUP)
.and_then(|value| value.checked_mul(lane_stride))
.ok_or_else(|| resource_limit("folded staging offset overflow"))?;
gf_simd::split_encode_scatter(
&transfer_bytes[slot_start..slot_start + aligned_len],
&mut staging_bytes
[group_start..group_start + aligned_len * gf_simd::FOLDED_GROUP],
group_lane,
);
}
#[cfg(target_arch = "x86_64")]
ResolvedKernel::XorJitAvx2 => {
let width = reedsolomon_rs::xor_jit::JitWidth::Avx2;
let block = width.block_bytes();
debug_assert_eq!(aligned_len % block, 0);
debug_assert_eq!(lane_stride, aligned_len);
let lane_start = lane
.checked_mul(lane_stride)
.ok_or_else(|| resource_limit("packed staging offset overflow"))?;
for offset in (0..aligned_len).step_by(block) {
unsafe {
width.prepare_block(
&transfer_bytes[slot_start + offset..slot_start + offset + block],
&mut staging_bytes[lane_start + offset..lane_start + offset + block],
);
}
}
}
}
}
Ok(())
}
fn transfer_slot_stride(aligned_len: usize) -> Result<usize> {
round_up(aligned_len, 64)
}
fn observe_batch(
observer: &mut dyn ForwardSourceObserver,
bytes: &[u8],
first_source_index: usize,
live_inputs: usize,
slot_stride: usize,
slice_lens: &[usize; MAX_INPUT_GROUPING],
) -> Result<()> {
let run_len = transfer_group_lanes().clamp(1, MAX_INPUT_GROUPING);
let mut index = 0usize;
while index < live_inputs {
let run = run_len.min(live_inputs - index);
let mut views: [&[u8]; MAX_INPUT_GROUPING] = [&[][..]; MAX_INPUT_GROUPING];
for (slot, view) in views[..run].iter_mut().enumerate() {
let start = (index + slot) * slot_stride;
*view = &bytes[start..start + slice_lens[index + slot]];
}
observer.observe_slices(first_source_index + index, &views[..run])?;
index += run;
}
Ok(())
}
fn live_batch_inputs(source_count: usize, source_start: usize, contract: KernelContract) -> usize {
source_count
.saturating_sub(source_start)
.min(contract.input_grouping)
}
#[allow(clippy::too_many_arguments)]
fn accumulate_batch(
kernel: ResolvedKernel,
output: &mut [u8],
staging: &AlignedBuffer,
factors: &FactorSource,
exponents: &[RecoveryExponent],
source_start: usize,
live_inputs: usize,
aligned_len: usize,
output_stride: usize,
contract: KernelContract,
band_size: usize,
#[cfg(target_arch = "x86_64")]
jit_workspaces: &mut [reedsolomon_rs::xor_jit::packed::PackedJitWorkspace],
#[cfg(target_arch = "x86_64")] jit_code_budget: usize,
) -> Result<()> {
let output_count = exponents.len();
debug_assert_eq!(output.len(), output_count * output_stride);
#[cfg(target_arch = "x86_64")]
debug_assert_eq!(
jit_workspaces.len(),
output_count.max(1).div_ceil(band_size)
);
if band_size >= output_count || output_count <= 1 {
return accumulate_band(
kernel,
output,
staging,
factors,
exponents,
source_start,
live_inputs,
aligned_len,
output_stride,
contract,
#[cfg(target_arch = "x86_64")]
&mut jit_workspaces[0],
#[cfg(target_arch = "x86_64")]
jit_code_budget,
);
}
let band_bytes = checked_mul(band_size, output_stride, "band byte range overflow")?;
let bands = output
.chunks_mut(band_bytes)
.zip(exponents.chunks(band_size));
#[cfg(target_arch = "x86_64")]
let bands = bands.zip(jit_workspaces.iter_mut());
for band in bands {
#[cfg(target_arch = "x86_64")]
let ((band_output, band_exponents), jit_workspace) = band;
#[cfg(not(target_arch = "x86_64"))]
let (band_output, band_exponents) = band;
accumulate_band(
kernel,
band_output,
staging,
factors,
band_exponents,
source_start,
live_inputs,
aligned_len,
output_stride,
contract,
#[cfg(target_arch = "x86_64")]
jit_workspace,
#[cfg(target_arch = "x86_64")]
jit_code_budget,
)?;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn accumulate_band(
kernel: ResolvedKernel,
output: &mut [u8],
staging: &AlignedBuffer,
factors: &FactorSource,
exponents: &[RecoveryExponent],
source_start: usize,
live_inputs: usize,
aligned_len: usize,
output_stride: usize,
contract: KernelContract,
#[cfg(target_arch = "x86_64")]
jit_workspace: &mut reedsolomon_rs::xor_jit::packed::PackedJitWorkspace,
#[cfg(target_arch = "x86_64")] jit_code_budget: usize,
) -> Result<()> {
let staging_bytes = staging.as_bytes();
if live_inputs == 0 {
return Ok(());
}
let lane_stride = lane_stride(contract, aligned_len);
let layout = StagingLayout::new(contract, aligned_len, lane_stride);
let mut row = [0u16; MAX_INPUT_GROUPING];
match kernel {
ResolvedKernel::Portable => {
let row_factors = factors.row_factors(source_start, live_inputs);
let mut rows = [[0u16; MAX_INPUT_GROUPING]; COEFF_ROWS];
for (chunk_index, chunk) in exponents.chunks(COEFF_ROWS).enumerate() {
for (slot, &exponent) in rows.iter_mut().zip(chunk) {
row_factors.fill_row(exponent, slot);
}
let first_output = chunk_index * COEFF_ROWS;
for (tile_start, tile_len) in stripe_tiles(aligned_len, contract.tile_bytes) {
for (offset, row) in rows[..chunk.len()].iter().enumerate() {
let dst_start = (first_output + offset) * output_stride + tile_start;
scalar_accumulate(
&mut output[dst_start..dst_start + tile_len],
&staging_bytes[tile_start..],
lane_stride,
row,
live_inputs,
tile_len,
);
}
}
}
}
ResolvedKernel::Simd => {
let mut prepared: Vec<gf_simd::PreparedInputFactor> =
Vec::with_capacity(COEFF_ROWS * live_inputs);
let row_factors = factors.row_factors(source_start, live_inputs);
for (chunk_index, chunk) in exponents.chunks(COEFF_ROWS).enumerate() {
prepared.clear();
for &exponent in chunk {
row_factors.fill_row(exponent, &mut row);
prepared.extend(
row[..live_inputs]
.iter()
.map(|&factor| gf_simd::prepare_input_factor(factor)),
);
}
let first_output = chunk_index * COEFF_ROWS;
for (tile_start, tile_len) in stripe_tiles(aligned_len, contract.tile_bytes) {
for offset in 0..chunk.len() {
let dst_start = (first_output + offset) * output_stride + tile_start;
let row_base = offset * live_inputs;
if layout.interleave == 1 {
let inputs: [PreparedFactorSrc<'_>; MAX_INPUT_GROUPING] =
std::array::from_fn(|lane| {
let clamped = lane.min(live_inputs - 1);
let source_start_bytes =
layout.group_base(clamped) + tile_start;
PreparedFactorSrc {
prepared: &prepared[row_base + clamped],
src: &staging_bytes
[source_start_bytes..source_start_bytes + tile_len],
}
});
gf_simd::mul_acc_input_batch_prepared(
&mut output[dst_start..dst_start + tile_len],
&inputs[..live_inputs],
);
continue;
}
for group in 0..layout.group_count() {
let first_lane = group * layout.interleave;
if first_lane >= live_inputs {
break;
}
let width = layout.group_width(group);
let live_in_group = (live_inputs - first_lane).min(width);
let (stream_start, stream_end) =
layout.group_tile(group, tile_start, tile_len);
gf_simd::mul_acc_input_batch_prepared_interleaved(
&mut output[dst_start..dst_start + tile_len],
&prepared
[row_base + first_lane..row_base + first_lane + live_in_group],
&staging_bytes[stream_start..stream_end],
width,
);
}
}
}
}
}
#[cfg(target_arch = "x86_64")]
ResolvedKernel::Folded => {
debug_assert_eq!(contract.input_grouping, DEFAULT_INPUT_GROUPING);
let groups = contract.input_grouping / gf_simd::FOLDED_GROUP;
if groups > MAX_FOLDED_GROUPS {
return Err(invalid_input(
"folded input grouping exceeds the reserved group count",
));
}
let mut affine = Vec::with_capacity(COEFF_ROWS * live_inputs);
let mut shuffle2x = Vec::with_capacity(COEFF_ROWS * live_inputs);
let uses_gfni = gf_simd::folded_uses_gfni();
let row_factors = factors.row_factors(source_start, live_inputs);
let mut staging_views: Vec<&[u8]> = Vec::with_capacity(groups);
for (chunk_index, chunk) in exponents.chunks(COEFF_ROWS).enumerate() {
affine.clear();
shuffle2x.clear();
for &exponent in chunk {
row_factors.fill_row(exponent, &mut row);
if uses_gfni {
affine.extend(
row[..live_inputs]
.iter()
.map(|&factor| gf_simd::precompute_affine_matrices(factor)),
);
} else {
shuffle2x.extend(
row[..live_inputs]
.iter()
.map(|&factor| gf_simd::precompute_shuffle2x_tables(factor)),
);
}
}
let first_output = chunk_index * COEFF_ROWS;
for (tile_start, tile_len) in stripe_tiles(aligned_len, contract.tile_bytes) {
staging_views.clear();
staging_views.extend((0..groups).map(|group| {
let start = group * gf_simd::FOLDED_GROUP * lane_stride
+ tile_start * gf_simd::FOLDED_GROUP;
&staging_bytes[start..start + gf_simd::FOLDED_GROUP * tile_len]
}));
for offset in 0..chunk.len() {
let dst_start = (first_output + offset) * output_stride + tile_start;
let row_base = offset * live_inputs;
if uses_gfni {
let matrix_sets: [[&gf_simd::AffineMulMatrices; gf_simd::FOLDED_GROUP];
MAX_FOLDED_GROUPS] = std::array::from_fn(|group| {
std::array::from_fn(|lane| {
let source_index = group * gf_simd::FOLDED_GROUP + lane;
affine
.get(row_base + source_index)
.filter(|_| source_index < live_inputs)
.unwrap_or(&gf_simd::ZERO_AFFINE)
})
});
gf_simd::mul_acc_folded_batch(
&mut output[dst_start..dst_start + tile_len],
&staging_views,
&matrix_sets[..groups],
);
} else {
let table_sets: [[&gf_simd::Shuffle2xTables; gf_simd::FOLDED_GROUP];
MAX_FOLDED_GROUPS] = std::array::from_fn(|group| {
std::array::from_fn(|lane| {
let source_index = group * gf_simd::FOLDED_GROUP + lane;
shuffle2x
.get(row_base + source_index)
.filter(|_| source_index < live_inputs)
.unwrap_or(&gf_simd::ZERO_SHUFFLE2X)
})
});
gf_simd::mul_acc_shuffle2x_batch(
&mut output[dst_start..dst_start + tile_len],
&staging_views,
&table_sets[..groups],
);
}
}
}
}
}
#[cfg(target_arch = "x86_64")]
ResolvedKernel::XorJitAvx2 => {
debug_assert_eq!(contract.tile_bytes, UNTILED);
debug_assert_eq!(lane_stride, aligned_len);
debug_assert_eq!(contract.input_grouping, DEFAULT_INPUT_GROUPING);
let width = reedsolomon_rs::xor_jit::JitWidth::Avx2;
let row_factors = factors.row_factors(source_start, live_inputs);
let rows: Vec<[u16; DEFAULT_INPUT_GROUPING]> = exponents
.iter()
.map(|&exponent| {
let mut wide = [0u16; MAX_INPUT_GROUPING];
row_factors.fill_row(exponent, &mut wide);
let mut row = [0u16; DEFAULT_INPUT_GROUPING];
row.copy_from_slice(&wide[..DEFAULT_INPUT_GROUPING]);
row
})
.collect();
let row_refs: Vec<&[u16]> = rows.iter().map(|row| &row[..]).collect();
let batch = jit_workspace
.build(width, &row_refs, jit_code_budget.max(1))
.map_err(|error| jit_build_error(error.to_string()))?;
for output_index in 0..exponents.len() {
let dst_start = output_index * output_stride;
let code = batch
.row(output_index)
.ok_or_else(|| invalid_input("packed XOR-JIT output row missing"))?;
unsafe {
width
.try_run_packed(
code,
&mut reedsolomon_rs::xor_jit::packed::PackedScratch::default(),
reedsolomon_rs::xor_jit::packed::PackedRun {
packed_regions: contract.input_grouping,
live_regions: live_inputs,
dst: output[dst_start..dst_start + aligned_len].as_mut_ptr(),
src: staging_bytes.as_ptr(),
len: aligned_len,
prefetch_in: Some(staging_bytes.as_ptr()),
prefetch_out: None,
},
)
.map_err(|error| jit_build_error(error.to_string()))?;
}
}
jit_workspace
.recycle(batch)
.map_err(|error| jit_build_error(error.to_string()))?;
}
}
Ok(())
}
fn scalar_accumulate(
dst: &mut [u8],
staging: &[u8],
staging_stride: usize,
row: &[u16],
live_inputs: usize,
len: usize,
) {
for word in 0..len / 2 {
let mut value = u16::from_le_bytes([dst[word * 2], dst[word * 2 + 1]]);
for (lane, &factor) in row.iter().take(live_inputs).enumerate() {
let source_offset = lane * staging_stride + word * 2;
let source = u16::from_le_bytes([staging[source_offset], staging[source_offset + 1]]);
value ^= gf::mul(source, factor);
}
dst[word * 2..word * 2 + 2].copy_from_slice(&value.to_le_bytes());
}
}
fn finish_output(
kernel: ResolvedKernel,
output: &mut [u8],
output_stride: usize,
aligned_len: usize,
output_count: usize,
) -> Result<()> {
debug_assert_eq!(output.len(), output_count * output_stride);
finish_band_rows(kernel, output, output_stride, aligned_len, output_count)
}
fn finish_band_rows(
kernel: ResolvedKernel,
output: &mut [u8],
output_stride: usize,
aligned_len: usize,
output_count: usize,
) -> Result<()> {
#[cfg(not(target_arch = "x86_64"))]
{
let _ = (kernel, output, output_stride, aligned_len, output_count);
}
#[cfg(target_arch = "x86_64")]
{
if matches!(kernel, ResolvedKernel::Portable | ResolvedKernel::Simd) {
return Ok(());
}
return finish_band(kernel, output, output_stride, aligned_len, output_count);
}
#[allow(unreachable_code)]
Ok(())
}
#[cfg(target_arch = "x86_64")]
fn finish_band(
kernel: ResolvedKernel,
output: &mut [u8],
output_stride: usize,
aligned_len: usize,
output_count: usize,
) -> Result<()> {
for output_index in 0..output_count {
let start = output_index
.checked_mul(output_stride)
.ok_or_else(|| resource_limit("output finish offset overflow"))?;
let end = start
.checked_add(aligned_len)
.ok_or_else(|| resource_limit("output finish end overflow"))?;
let dst = &mut output[start..end];
match kernel {
ResolvedKernel::Portable | ResolvedKernel::Simd => {}
ResolvedKernel::Folded => {
gf_simd::altmap_decode(dst);
}
ResolvedKernel::XorJitAvx2 => {
let width = reedsolomon_rs::xor_jit::JitWidth::Avx2;
let block = width.block_bytes();
for offset in (0..aligned_len).step_by(block) {
unsafe { width.finish_block(&mut dst[offset..offset + block]) };
}
}
}
}
Ok(())
}
fn validate_provider<P: ForwardSourceProvider + ?Sized>(
provider: &P,
slice_size: usize,
) -> Result<()> {
let source_count = provider.source_count();
if source_count > MAX_TOTAL_INPUT_SLICES {
return Err(resource_limit(format!(
"input slice count {} exceeds {MAX_TOTAL_INPUT_SLICES}",
source_count
)));
}
for source_index in 0..source_count {
if provider.source_slice_len(source_index)? > slice_size {
return Err(invalid_input(
"an input slice is longer than the configured slice size",
));
}
}
Ok(())
}
fn check_cancel(options: &ForwardEncoderOptions) -> Result<()> {
if options
.cancel
.as_ref()
.is_some_and(CancellationToken::is_cancelled)
{
Err(Par2Error::Cancelled)
} else {
Ok(())
}
}
fn report_progress(
options: &ForwardEncoderOptions,
current: u32,
total: u32,
bytes_processed: u64,
total_bytes: u64,
) {
if let Some(progress) = &options.progress {
progress(ProgressUpdate {
stage: ProgressStage::Creating,
current,
total,
bytes_processed,
total_bytes: Some(total_bytes),
phase: ProgressPhase::RecoveryEncode,
});
}
}
#[cfg(test)]
struct VecRecoverySink {
blocks: Vec<ForwardRecoveryBlock>,
slice_size: usize,
}
#[cfg(test)]
impl VecRecoverySink {
fn new(exponents: &[RecoveryExponent], slice_size: usize) -> Self {
Self {
blocks: exponents
.iter()
.map(|&exponent| ForwardRecoveryBlock {
exponent,
data: vec![0; slice_size],
})
.collect(),
slice_size,
}
}
}
#[cfg(test)]
impl ForwardRecoverySink for VecRecoverySink {
fn write_recovery_chunk(
&mut self,
output_index: usize,
exponent: RecoveryExponent,
offset: u64,
data: &[u8],
) -> Result<()> {
let block = self
.blocks
.get_mut(output_index)
.ok_or_else(|| invalid_input("recovery output index is out of order"))?;
if block.exponent != exponent {
return Err(invalid_input("recovery exponent changed during encoding"));
}
let start =
usize::try_from(offset).map_err(|_| resource_limit("stripe offset overflow"))?;
let end = start
.checked_add(data.len())
.ok_or_else(|| resource_limit("recovery chunk end overflow"))?;
if end > self.slice_size {
return Err(invalid_input(
"recovery chunk exceeds the configured slice size",
));
}
block.data[start..end].copy_from_slice(data);
Ok(())
}
}
fn round_up(value: usize, alignment: usize) -> Result<usize> {
if alignment == 0 {
return Err(invalid_input("zero alignment"));
}
value
.checked_add(alignment - 1)
.map(|value| value / alignment * alignment)
.ok_or_else(|| resource_limit("aligned length overflow"))
}
fn checked_mul(left: usize, right: usize, reason: &'static str) -> Result<usize> {
left.checked_mul(right)
.ok_or_else(|| resource_limit(reason))
}
fn checked_add(left: usize, right: usize, reason: &'static str) -> Result<usize> {
left.checked_add(right)
.ok_or_else(|| resource_limit(reason))
}
fn invalid_input(reason: impl Into<String>) -> Par2Error {
Par2Error::ReedSolomonError {
reason: reason.into(),
}
}
fn resource_limit(reason: impl Into<String>) -> Par2Error {
Par2Error::ResourceLimitExceeded {
reason: reason.into(),
}
}
#[cfg(target_arch = "x86_64")]
fn unavailable_kernel(name: &'static str) -> Par2Error {
Par2Error::ReedSolomonError {
reason: format!("forward arithmetic kernel unavailable: {name}"),
}
}
#[cfg(target_arch = "x86_64")]
fn jit_build_error(reason: String) -> Par2Error {
Par2Error::ReedSolomonError {
reason: format!("forward packed arithmetic dispatch failed: {reason}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_sources() -> Vec<Vec<u8>> {
(0..19usize)
.map(|source| {
(0..(73 + source * 11).min(256))
.map(|index| (index.wrapping_mul(17) ^ (source * 29)) as u8)
.collect()
})
.collect()
}
fn encode_with_kernel(
sources: &[Vec<u8>],
kernel: ForwardKernel,
) -> Result<Vec<ForwardRecoveryBlock>> {
let refs = sources.iter().map(Vec::as_slice).collect::<Vec<_>>();
let encoder = ForwardEncoder::new(256, vec![0, 1, 2, 7, 31])?;
encoder.encode(
&refs,
&ForwardEncoderOptions {
memory_limit: Some(4 * 1024 * 1024),
kernel,
..ForwardEncoderOptions::default()
},
)
}
#[test]
fn portable_output_matches_every_available_cpu_path() {
let sources = test_sources();
let portable = encode_with_kernel(&sources, ForwardKernel::Portable).unwrap();
for kernel in ForwardEncoder::available_kernels() {
let actual = encode_with_kernel(&sources, kernel).unwrap();
assert_eq!(actual, portable, "kernel {kernel:?} differs from portable");
}
}
#[test]
fn automatic_selection_matches_its_explicit_kernel() {
let sources = test_sources();
let auto = encode_with_kernel(&sources, ForwardKernel::Auto).unwrap();
let encoder = ForwardEncoder::new(256, vec![0, 1, 2, 7, 31]).unwrap();
let selected = encoder.selected_kernel(ForwardKernel::Auto).unwrap();
let explicit = encode_with_kernel(&sources, selected).unwrap();
assert_eq!(auto, explicit, "automatic kernel {selected:?} differs");
}
#[test]
fn kernel_override_values_parse_and_reject() {
assert!(matches!(
parse_kernel_override("auto"),
Ok(ForwardKernel::Auto)
));
assert!(matches!(
parse_kernel_override(" Portable "),
Ok(ForwardKernel::Portable)
));
assert!(matches!(
parse_kernel_override("SIMD"),
Ok(ForwardKernel::Simd)
));
#[cfg(target_arch = "x86_64")]
{
assert!(matches!(
parse_kernel_override("folded"),
Ok(ForwardKernel::Folded)
));
assert!(matches!(
parse_kernel_override("xor-jit-avx2"),
Ok(ForwardKernel::XorJitAvx2)
));
assert!(parse_kernel_override("xor-jit-avx512").is_err());
}
assert!(parse_kernel_override("fast").is_err());
assert!(parse_kernel_override("").is_err());
}
#[test]
fn the_stripe_feed_reclaims_an_area_only_after_every_band_is_done() {
const BATCHES: usize = 37;
for band_count in [1usize, 2, 5] {
let depth = configured_staging_areas();
let feed = StripeFeed::new(band_count, depth);
let feed = &feed;
let mut areas: Vec<std::sync::Arc<AlignedBuffer>> = (0..depth)
.map(|_| std::sync::Arc::new(AlignedBuffer::new(64)))
.collect();
let mut slots: Vec<std::sync::Arc<TransferSlot>> = (0..depth)
.map(|_| std::sync::Arc::new(TransferSlot::new(64)))
.collect();
let hashed = std::sync::Mutex::new(Vec::<usize>::with_capacity(BATCHES));
let hashed = &hashed;
let faults = std::sync::Mutex::new(Vec::<String>::new());
let faults = &faults;
std::thread::scope(|scope| {
for band_index in 0..band_count {
scope.spawn(move || {
let note = |fault: String| faults.lock().expect("uncontended").push(fault);
for batch in 0..BATCHES {
let Some(ticket) = feed.acquire(batch) else {
note(format!("batch {batch}: no failure is injected"));
return;
};
let stamp = (batch % 251) as u8;
if ticket.source_start != batch * 7 {
note(format!("batch {batch}: batch order"));
}
if ticket.staging.as_bytes()[0] != stamp {
note(format!(
"batch {batch}: area was refilled while a band still held it"
));
}
if batch % band_count == band_index {
if !feed.wait_for_hash_turn(batch) {
note(format!("batch {batch}: the hashing turn never came"));
return;
}
if ticket.transfer.buffer.as_bytes()[0] != stamp {
note(format!(
"batch {batch}: transfer slot was refilled while a band still held it"
));
}
hashed.lock().expect("uncontended").push(batch);
feed.finish_hash_turn(batch);
}
drop(ticket);
feed.release(batch);
}
});
}
for batch in 0..BATCHES {
assert!(feed.wait_for_area(batch));
let area = batch % depth;
let buffer = std::sync::Arc::get_mut(&mut areas[area])
.expect("every band released the area before it was reclaimed");
buffer.as_bytes_mut()[0] = (batch % 251) as u8;
let slot = std::sync::Arc::get_mut(&mut slots[area])
.expect("every band released the transfer slot before it was reclaimed");
slot.buffer.as_bytes_mut()[0] = (batch % 251) as u8;
feed.publish(
batch,
BatchTicket {
staging: std::sync::Arc::clone(&areas[area]),
transfer: std::sync::Arc::clone(&slots[area]),
source_start: batch * 7,
live_inputs: 1,
},
);
}
});
assert!(
faults.lock().expect("no band panicked").is_empty(),
"{:?}",
faults.lock().expect("no band panicked")
);
assert_eq!(
hashed.lock().expect("no band panicked").as_slice(),
(0..BATCHES).collect::<Vec<_>>(),
"the hashing turn must reach the observer once per batch, in index order"
);
}
}
#[test]
fn a_failed_pass_releases_both_sides_of_the_feed() {
let feed = StripeFeed::new(2, configured_staging_areas());
feed.fail();
assert!(feed.acquire(0).is_none(), "a band must stop on failure");
assert!(
!feed.wait_for_hash_turn(7),
"a band owing a hashing turn must stop on failure"
);
assert!(
!feed.wait_for_area(configured_staging_areas()),
"the producer must stop on failure"
);
}
#[test]
fn banded_accumulation_matches_sequential() {
let sources = test_sources();
let refs = sources.iter().map(Vec::as_slice).collect::<Vec<_>>();
let exponents: Vec<RecoveryExponent> = vec![0, 1, 2, 7, 31, 64, 100];
for requested in ForwardEncoder::available_kernels() {
let resolved =
resolve_kernel_with_capabilities(requested, runtime_kernel_capabilities())
.expect("advertised kernels resolve");
let contract = KernelContract::for_kernel(resolved);
let aligned_len = round_up(256, contract.stride).unwrap();
let mut passes = Vec::new();
for band_size in [7usize, 3] {
let mut provider = InMemorySourceProvider { sources: &refs };
let mut staging = AlignedBuffer::new(
contract.input_grouping * lane_stride(contract, aligned_len),
);
let mut transfer = AlignedBuffer::new(
contract.input_grouping * transfer_slot_stride(aligned_len).unwrap(),
);
fill_staging(
resolved,
&mut staging,
&mut transfer,
&mut provider,
0,
0,
256,
aligned_len,
contract,
&mut [0usize; MAX_INPUT_GROUPING],
)
.unwrap();
let factors = FactorSource::new(refs.len());
let mut output = AlignedBuffer::new(exponents.len() * aligned_len);
#[cfg(target_arch = "x86_64")]
let mut jit_workspaces: Vec<
reedsolomon_rs::xor_jit::packed::PackedJitWorkspace,
> = (0..exponents.len().div_ceil(band_size))
.map(|_| Default::default())
.collect();
accumulate_batch(
resolved,
output.as_bytes_mut(),
&staging,
&factors,
&exponents,
0,
contract.input_grouping.min(refs.len()),
aligned_len,
aligned_len,
contract,
band_size,
#[cfg(target_arch = "x86_64")]
&mut jit_workspaces,
#[cfg(target_arch = "x86_64")]
usize::MAX,
)
.unwrap();
finish_output(
resolved,
output.as_bytes_mut(),
aligned_len,
aligned_len,
exponents.len(),
)
.unwrap();
passes.push(output.as_bytes().to_vec());
}
assert_eq!(
passes[0], passes[1],
"kernel {requested:?} banded output differs from sequential"
);
}
}
#[test]
fn stripe_tiles_cover_the_stripe_exactly() {
for (aligned_len, tile) in [
(4096usize, 4096usize),
(4096, 8192),
(4096, UNTILED),
(10 * 1024, 4096),
(32, 4096),
(0, 4096),
] {
let ranges: Vec<(usize, usize)> = stripe_tiles(aligned_len, tile).collect();
let mut next = 0usize;
for (start, len) in &ranges {
assert_eq!(*start, next, "tiles are contiguous");
assert!(*len > 0 && *len <= tile.min(aligned_len).max(1));
next += len;
}
assert_eq!(next, aligned_len, "tiles cover the stripe");
if aligned_len > 0 {
for (_, len) in &ranges[..ranges.len() - 1] {
assert_eq!(*len, tile.min(aligned_len));
}
}
}
}
#[test]
fn stripe_tiling_matches_untiled_accumulation() {
const SLICE: usize = 40 * 1024;
let sources: Vec<Vec<u8>> = (0..14usize)
.map(|source| {
(0..SLICE)
.map(|index| (index.wrapping_mul(31) ^ (source * 131)) as u8)
.collect()
})
.collect();
let refs = sources.iter().map(Vec::as_slice).collect::<Vec<_>>();
let exponents: Vec<RecoveryExponent> = vec![0, 1, 2, 7, 31];
for requested in ForwardEncoder::available_kernels() {
let resolved =
resolve_kernel_with_capabilities(requested, runtime_kernel_capabilities())
.expect("advertised kernels resolve");
let base = KernelContract::for_kernel(resolved);
if base.tile_bytes == UNTILED {
continue;
}
let aligned_len = round_up(SLICE, base.stride).unwrap();
let mut passes = Vec::new();
for tile_bytes in [UNTILED, 8192, 4096, 96, base.stride] {
let contract = KernelContract { tile_bytes, ..base };
let mut provider = InMemorySourceProvider { sources: &refs };
let mut staging = AlignedBuffer::new(
contract.input_grouping * lane_stride(contract, aligned_len),
);
let mut transfer = AlignedBuffer::new(
contract.input_grouping * transfer_slot_stride(aligned_len).unwrap(),
);
fill_staging(
resolved,
&mut staging,
&mut transfer,
&mut provider,
0,
0,
SLICE,
aligned_len,
contract,
&mut [0usize; MAX_INPUT_GROUPING],
)
.unwrap();
let factors = FactorSource::new(refs.len());
let mut output = AlignedBuffer::new(exponents.len() * aligned_len);
#[cfg(target_arch = "x86_64")]
let mut jit_workspaces: Vec<
reedsolomon_rs::xor_jit::packed::PackedJitWorkspace,
> = vec![Default::default()];
accumulate_batch(
resolved,
output.as_bytes_mut(),
&staging,
&factors,
&exponents,
0,
contract.input_grouping.min(refs.len()),
aligned_len,
aligned_len,
contract,
exponents.len(),
#[cfg(target_arch = "x86_64")]
&mut jit_workspaces,
#[cfg(target_arch = "x86_64")]
usize::MAX,
)
.unwrap();
finish_output(
resolved,
output.as_bytes_mut(),
aligned_len,
aligned_len,
exponents.len(),
)
.unwrap();
passes.push(output.as_bytes().to_vec());
}
for (index, pass) in passes.iter().enumerate().skip(1) {
assert_eq!(
*pass, passes[0],
"kernel {requested:?} tiling pass {index} differs from the untiled pass"
);
}
}
}
#[test]
fn the_feed_is_stripe_major_once_a_slice_needs_more_than_one_stripe() {
struct Recorder<'a> {
sources: &'a [&'a [u8]],
reads: Vec<(usize, usize)>,
}
impl ForwardSourceProvider for Recorder<'_> {
fn source_count(&self) -> usize {
self.sources.len()
}
fn source_slice_len(&self, source_index: usize) -> Result<usize> {
Ok(self.sources[source_index].len())
}
fn read_source_chunk(
&mut self,
source_index: usize,
offset: usize,
destination: &mut [u8],
) -> Result<usize> {
if source_index < self.sources.len() {
self.reads.push((source_index, offset));
}
let source = self.sources[source_index];
let start = offset.min(source.len());
let take = destination.len().min(source.len() - start);
destination[..take].copy_from_slice(&source[start..start + take]);
Ok(take)
}
}
const SLICE: usize = 4096;
let sources: Vec<Vec<u8>> = (0..3usize).map(|s| vec![s as u8 + 1; SLICE]).collect();
let refs = sources.iter().map(Vec::as_slice).collect::<Vec<_>>();
let encoder = ForwardEncoder::new(SLICE, vec![0, 1]).unwrap();
let mut single = Recorder {
sources: &refs,
reads: Vec::new(),
};
let mut sink = VecRecoverySink::new(&[0, 1], SLICE);
encoder
.encode_to(
&mut single,
&ForwardEncoderOptions {
memory_limit: Some(4 * 1024 * 1024),
..ForwardEncoderOptions::default()
},
&mut sink,
)
.unwrap();
assert_eq!(
single.reads,
vec![(0, 0), (1, 0), (2, 0)],
"a single-stripe feed must deliver each source once, whole"
);
let mut split = Recorder {
sources: &refs,
reads: Vec::new(),
};
let mut sink = VecRecoverySink::new(&[0, 1], SLICE);
encoder
.encode_to(
&mut split,
&ForwardEncoderOptions {
memory_limit: Some(32 * 1024),
..ForwardEncoderOptions::default()
},
&mut sink,
)
.unwrap();
let offsets: Vec<usize> = split.reads.iter().map(|&(_, offset)| offset).collect();
assert!(
offsets.iter().any(|&offset| offset > 0),
"the tight budget must split the slice into stripes"
);
assert!(
split
.reads
.windows(2)
.any(|pair| pair[0].0 > pair[1].0 && pair[1].1 > pair[0].1),
"a multi-stripe feed is stripe-major: {:?}",
split.reads
);
}
#[cfg(target_arch = "x86_64")]
#[test]
fn advertised_kernels_use_the_production_capability_resolver() {
let capabilities = runtime_kernel_capabilities();
let advertised = ForwardEncoder::available_kernels();
assert_eq!(
advertised.contains(&ForwardKernel::Folded),
capabilities.folded
);
let encoder = ForwardEncoder::new(256, vec![0]).unwrap();
for kernel in advertised {
assert!(
encoder.selected_kernel(kernel).is_ok(),
"advertised kernel {kernel:?} cannot be selected"
);
}
}
#[cfg(target_arch = "x86_64")]
#[test]
fn automatic_admission_keeps_the_full_kernel_ladder_ordered() {
let folded_only = KernelCapabilities {
folded: true,
folded_wide: false,
avx2_jit: false,
};
assert_eq!(
auto_kernel_candidates(folded_only),
vec![
ResolvedKernel::Folded,
ResolvedKernel::Simd,
ResolvedKernel::Portable,
]
);
let direct_simd_only = KernelCapabilities {
folded: false,
folded_wide: false,
avx2_jit: false,
};
assert_eq!(
auto_kernel_candidates(direct_simd_only),
vec![ResolvedKernel::Simd, ResolvedKernel::Portable]
);
}
#[cfg(target_arch = "x86_64")]
#[test]
fn create_auto_ladder_prefers_shuffle_over_jit_on_fast_jit_hosts() {
let fast_jit_avx2 = KernelCapabilities {
folded: true,
folded_wide: false,
avx2_jit: true,
};
assert_eq!(
resolve_kernel_with_capabilities(ForwardKernel::Auto, fast_jit_avx2).unwrap(),
ResolvedKernel::Folded
);
assert_eq!(
resolve_kernel_with_capabilities(ForwardKernel::XorJitAvx2, fast_jit_avx2).unwrap(),
ResolvedKernel::XorJitAvx2
);
assert_eq!(
auto_kernel_candidates(fast_jit_avx2),
vec![
ResolvedKernel::Folded,
ResolvedKernel::XorJitAvx2,
ResolvedKernel::Simd,
ResolvedKernel::Portable,
]
);
let jit_without_folded = KernelCapabilities {
folded: false,
folded_wide: false,
avx2_jit: true,
};
assert_eq!(
resolve_kernel_with_capabilities(ForwardKernel::Auto, jit_without_folded).unwrap(),
ResolvedKernel::XorJitAvx2
);
}
#[cfg(target_arch = "x86_64")]
#[test]
fn production_admission_can_fall_back_from_folded_to_simd() {
let capabilities = KernelCapabilities {
folded: true,
folded_wide: false,
avx2_jit: false,
};
let raw = resolve_kernel_with_capabilities(ForwardKernel::Auto, capabilities).unwrap();
assert_eq!(raw, ResolvedKernel::Folded);
let slice_size = 60;
let source_count = 19;
let first_exponent = 0_u32;
let recovery_count = u32::from(u16::MAX);
assert!(first_exponent + recovery_count < u32::from(u16::MAX) + 1);
let output_count = recovery_count as usize;
let minimum_memory_limit = |requested| {
let (_, full_plan) = select_kernel_for_memory_with_capabilities(
slice_size,
output_count,
source_count,
usize::MAX,
requested,
capabilities,
)
.unwrap();
let mut lower = 0;
let mut upper = full_plan.memory_bytes;
while lower < upper {
let middle = lower + (upper - lower) / 2;
if select_kernel_for_memory_with_capabilities(
slice_size,
output_count,
source_count,
middle,
requested,
capabilities,
)
.is_ok()
{
upper = middle;
} else {
lower = middle + 1;
}
}
assert!(
select_kernel_for_memory_with_capabilities(
slice_size,
output_count,
source_count,
lower,
requested,
capabilities,
)
.is_ok()
);
if lower > 0 {
assert!(
select_kernel_for_memory_with_capabilities(
slice_size,
output_count,
source_count,
lower - 1,
requested,
capabilities,
)
.is_err()
);
}
lower
};
let folded_minimum = minimum_memory_limit(ForwardKernel::Folded);
let simd_minimum = minimum_memory_limit(ForwardKernel::Simd);
assert!(
folded_minimum > simd_minimum,
"folded minimum {folded_minimum} is not above simd minimum {simd_minimum}"
);
let memory_limit = simd_minimum;
assert!(
select_kernel_for_memory_with_capabilities(
slice_size,
output_count,
source_count,
memory_limit,
ForwardKernel::Folded,
capabilities,
)
.is_err()
);
let (admitted, _) = select_kernel_for_memory_with_capabilities(
slice_size,
output_count,
source_count,
memory_limit,
ForwardKernel::Auto,
capabilities,
)
.unwrap();
assert_eq!(admitted, ResolvedKernel::Simd);
}
#[test]
fn final_stripe_is_not_padded_in_sink() {
struct Sink {
chunks: Vec<(usize, RecoveryExponent, u64, Vec<u8>)>,
}
impl ForwardRecoverySink for Sink {
fn write_recovery_chunk(
&mut self,
output_index: usize,
exponent: RecoveryExponent,
offset: u64,
data: &[u8],
) -> Result<()> {
self.chunks
.push((output_index, exponent, offset, data.to_vec()));
Ok(())
}
}
let sources = test_sources();
let refs = sources.iter().map(Vec::as_slice).collect::<Vec<_>>();
let encoder =
ForwardEncoder::new(260, vec![4, 9]).expect("slice size is a valid PAR2 size");
let mut sink = Sink { chunks: Vec::new() };
encoder
.encode_slices_to(
&refs,
&ForwardEncoderOptions {
memory_limit: Some(8_800),
kernel: ForwardKernel::Portable,
..ForwardEncoderOptions::default()
},
&mut sink,
)
.unwrap();
assert!(sink.chunks.iter().all(|(_, _, _, data)| data.len() <= 260));
let stripe = sink.chunks[0].3.len();
assert!(
(2..260).contains(&stripe),
"the memory limit must force a multi-stripe plan, got stripe {stripe}"
);
let stripes = 260usize.div_ceil(stripe);
assert_eq!(sink.chunks.len(), 2 * stripes);
let last = sink.chunks.last().unwrap();
assert_eq!(last.2 as usize, (stripes - 1) * stripe);
assert_eq!(last.3.len(), 260 - (stripes - 1) * stripe);
}
#[test]
fn tight_memory_preserves_recovery_bytes_for_every_available_kernel() {
let slice_size = 1028usize;
let source_count = 19;
let output_count = 3;
let sources = (0..source_count)
.map(|source| {
(0..slice_size)
.map(|index| (index.wrapping_mul(17) ^ (source * 29)) as u8)
.collect()
})
.collect::<Vec<Vec<u8>>>();
let refs = sources.iter().map(Vec::as_slice).collect::<Vec<_>>();
let exponents = vec![4, 9, 17];
assert_eq!(refs.len(), source_count);
assert_eq!(exponents.len(), output_count);
let encoder = ForwardEncoder::new(slice_size, exponents).unwrap();
let (_, reference_plan) = select_kernel_for_memory(
slice_size,
output_count,
source_count,
usize::MAX,
ForwardKernel::Portable,
)
.unwrap();
assert_eq!(reference_plan.chunk_len, slice_size);
let reference = encoder
.encode(
&refs,
&ForwardEncoderOptions {
memory_limit: Some(reference_plan.memory_bytes),
kernel: ForwardKernel::Portable,
..ForwardEncoderOptions::default()
},
)
.unwrap();
for kernel in ForwardEncoder::available_kernels() {
let (_, full_plan) = select_kernel_for_memory(
slice_size,
output_count,
source_count,
usize::MAX,
kernel,
)
.unwrap();
let (tight_limit, tight_plan) = if full_plan.chunk_len < slice_size {
(full_plan.memory_bytes, full_plan)
} else {
let mut memory_limit = full_plan.memory_bytes;
loop {
memory_limit = memory_limit
.checked_sub(1)
.expect("a full-stripe plan has a smaller admitted plan");
match select_kernel_for_memory(
slice_size,
output_count,
source_count,
memory_limit,
kernel,
) {
Ok((_, plan))
if plan.chunk_len < slice_size
&& !slice_size.is_multiple_of(plan.chunk_len) =>
{
break (memory_limit, plan);
}
Ok(_) | Err(_) => {}
}
}
};
assert!(
tight_plan.chunk_len < slice_size,
"kernel {kernel:?} retained a full-size stripe"
);
let stripe_count = slice_size.div_ceil(tight_plan.chunk_len);
assert!(stripe_count > 1, "kernel {kernel:?} used one stripe");
let final_len = slice_size % tight_plan.chunk_len;
assert!(
final_len > 0 && final_len < tight_plan.chunk_len,
"kernel {kernel:?} did not produce a short final stripe"
);
let actual = encoder
.encode(
&refs,
&ForwardEncoderOptions {
memory_limit: Some(tight_limit),
kernel,
..ForwardEncoderOptions::default()
},
)
.unwrap();
assert_eq!(actual, reference, "kernel {kernel:?} differs from portable");
}
}
#[test]
fn every_available_kernel_streams_contiguous_unpadded_chunks() {
struct Sink {
chunks: Vec<(usize, RecoveryExponent, u64, Vec<u8>)>,
}
impl ForwardRecoverySink for Sink {
fn write_recovery_chunk(
&mut self,
output_index: usize,
exponent: RecoveryExponent,
offset: u64,
data: &[u8],
) -> Result<()> {
self.chunks
.push((output_index, exponent, offset, data.to_vec()));
Ok(())
}
}
let sources = test_sources();
let refs = sources.iter().map(Vec::as_slice).collect::<Vec<_>>();
let exponents = vec![4, 9];
let encoder = ForwardEncoder::new(260, exponents.clone()).unwrap();
let options = |kernel| ForwardEncoderOptions {
memory_limit: Some(1024 * 1024),
kernel,
..ForwardEncoderOptions::default()
};
let reference = encoder
.encode(&refs, &options(ForwardKernel::Portable))
.unwrap();
for kernel in ForwardEncoder::available_kernels() {
let actual = encoder.encode(&refs, &options(kernel)).unwrap();
assert_eq!(actual, reference, "kernel {kernel:?} differs from portable");
let mut sink = Sink { chunks: Vec::new() };
encoder
.encode_slices_to(&refs, &options(kernel), &mut sink)
.unwrap();
let mut next_offset = vec![0u64; exponents.len()];
for (position, (output_index, exponent, offset, data)) in sink.chunks.iter().enumerate()
{
assert_eq!(*output_index, position % exponents.len());
assert_eq!(*exponent, exponents[*output_index]);
assert_eq!(*offset, next_offset[*output_index]);
assert!(*offset + data.len() as u64 <= encoder.slice_size() as u64);
next_offset[*output_index] += data.len() as u64;
}
assert!(next_offset.iter().all(|&offset| offset == 260));
}
}
#[test]
fn insufficient_memory_rejects_without_zero_length_stripes() {
let result = BufferPlan::new_with_reserved(
260,
1,
KernelContract {
stride: 32,
input_grouping: DEFAULT_INPUT_GROUPING,
tile_bytes: TABLE_TILE_BYTES,
skewed_lanes: true,
interleave_lanes: 1,
},
1,
0,
0,
0,
);
assert!(matches!(
result,
Err(Par2Error::ResourceLimitExceeded { .. })
));
}
#[test]
fn factor_workspace_does_not_scale_with_recovery_rows() {
let one = estimate_forward_memory(
4,
MAX_TOTAL_INPUT_SLICES,
1,
3 * 1024 * 1024,
ForwardKernel::Portable,
)
.unwrap();
let many = estimate_forward_memory(
4,
MAX_TOTAL_INPUT_SLICES,
MAX_TOTAL_INPUT_SLICES,
3 * 1024 * 1024,
ForwardKernel::Portable,
)
.unwrap();
assert_eq!(one.factor_workspace_bytes, many.factor_workspace_bytes);
assert!(one.factor_workspace_bytes < 128 * 1024);
assert!(many.processing_peak_bytes <= 3 * 1024 * 1024);
}
#[test]
fn low_memory_rejects_before_large_output_allocation() {
let result = estimate_forward_memory(
4096,
MAX_TOTAL_INPUT_SLICES,
MAX_TOTAL_INPUT_SLICES,
64 * 1024,
ForwardKernel::Portable,
);
assert!(matches!(
result,
Err(Par2Error::ResourceLimitExceeded { .. })
));
}
#[test]
fn staging_zero_pads_an_odd_final_byte_as_a_low_byte_word() {
let source = [0x11, 0x22, 0x33];
let refs = [source.as_slice()];
let mut provider = InMemorySourceProvider { sources: &refs };
let mut staging = AlignedBuffer::new(DEFAULT_INPUT_GROUPING * 4);
let mut transfer = AlignedBuffer::new(DEFAULT_INPUT_GROUPING * 64);
fill_staging(
ResolvedKernel::Portable,
&mut staging,
&mut transfer,
&mut provider,
0,
0,
3,
4,
KernelContract {
stride: 2,
input_grouping: DEFAULT_INPUT_GROUPING,
tile_bytes: TABLE_TILE_BYTES,
skewed_lanes: true,
interleave_lanes: 1,
},
&mut [0usize; MAX_INPUT_GROUPING],
)
.unwrap();
assert_eq!(&staging.as_bytes()[..4], &[0x11, 0x22, 0x33, 0]);
}
#[test]
fn cancellation_is_observed_before_allocation() {
let sources = test_sources();
let refs = sources.iter().map(Vec::as_slice).collect::<Vec<_>>();
let token = CancellationToken::new();
token.cancel();
let encoder = ForwardEncoder::new(256, vec![0]).unwrap();
let error = encoder
.encode(
&refs,
&ForwardEncoderOptions {
cancel: Some(token),
..ForwardEncoderOptions::default()
},
)
.unwrap_err();
assert!(matches!(error, Par2Error::Cancelled));
}
#[test]
fn payload_matches_vandermonde_definition() {
let sources = test_sources();
let refs = sources.iter().map(Vec::as_slice).collect::<Vec<_>>();
let exponents = [0, 31];
let encoder = ForwardEncoder::new(256, exponents.to_vec()).unwrap();
let actual = encoder
.encode(
&refs,
&ForwardEncoderOptions {
kernel: ForwardKernel::Portable,
..ForwardEncoderOptions::default()
},
)
.unwrap();
let constants = gf::input_slice_constants(refs.len());
for (output, &exponent) in exponents.iter().enumerate() {
let mut expected = vec![0u8; 256];
for (source_index, source) in refs.iter().enumerate() {
let factor = gf::pow(constants[source_index], exponent);
for word in 0..128 {
let offset = word * 2;
let source_word = if offset < source.len() {
u16::from_le_bytes([
source[offset],
source.get(offset + 1).map_or(0, |byte| *byte),
])
} else {
0
};
let output_word = u16::from_le_bytes([expected[offset], expected[offset + 1]])
^ gf::mul(source_word, factor);
expected[offset..offset + 2].copy_from_slice(&output_word.to_le_bytes());
}
}
assert_eq!(actual[output].data, expected);
}
}
#[test]
fn stripe_skew_follows_the_stripe_length() {
assert_eq!(stripe_skew_bytes(2), 0);
assert_eq!(stripe_skew_bytes(256), 0);
assert_eq!(stripe_skew_bytes(1023), 0);
assert_eq!(stripe_skew_bytes(1024), 0, "already 1 KiB mod 4 KiB");
assert_eq!(stripe_skew_bytes(2048), 256, "wants 3 KiB, capped at 1/8");
assert_eq!(stripe_skew_bytes(4096), 512, "wants 1 KiB, capped at 1/8");
assert_eq!(stripe_skew_bytes(40_960), 1024);
assert_eq!(stripe_skew_bytes(65_536), 1024);
assert_eq!(stripe_skew_bytes(66_560), 0, "already 1 KiB mod 4 KiB");
assert_eq!(
stripe_skew_bytes(67_584),
3072,
"2 KiB residue moves to 1 KiB"
);
assert_eq!(stripe_skew_bytes(1 << 20), 1024);
for aligned_len in [8192usize, 40_960, 65_536, 67_584, 1 << 20] {
let stride = aligned_len + stripe_skew_bytes(aligned_len);
assert_eq!(stride % 4096, 1024, "stride residue for {aligned_len}");
}
let contract = KernelContract {
stride: 2,
input_grouping: DEFAULT_INPUT_GROUPING,
tile_bytes: TABLE_TILE_BYTES,
skewed_lanes: true,
interleave_lanes: 1,
};
let plan =
BufferPlan::new_with_reserved(65_536, 820, contract, usize::MAX, 0, 0, 0).unwrap();
assert_eq!(plan.aligned_chunk_len, 65_536);
assert_eq!(plan.row_stride, 65_536 + 1024);
assert_eq!(plan.staging_bytes, DEFAULT_INPUT_GROUPING * (65_536 + 1024));
assert_eq!(plan.output_bytes, 820 * (65_536 + 1024));
assert_eq!(lane_stride(contract, 65_536), 65_536 + 1024);
assert_eq!(
lane_stride(
KernelContract {
skewed_lanes: false,
..contract
},
65_536
),
65_536
);
}
#[test]
fn input_grouping_follows_the_kernel_family() {
let simd = KernelContract::for_kernel(ResolvedKernel::Simd);
let portable = KernelContract::for_kernel(ResolvedKernel::Portable);
assert_eq!(simd.input_grouping, portable.input_grouping);
assert!((1..=MAX_INPUT_GROUPING).contains(&simd.input_grouping));
if std::env::var_os("WEAVER_PAR2_CREATE_GROUPING").is_none() {
#[cfg(target_arch = "aarch64")]
assert_eq!(simd.input_grouping, CLMUL_INPUT_GROUPING);
#[cfg(not(target_arch = "aarch64"))]
assert_eq!(simd.input_grouping, DEFAULT_INPUT_GROUPING);
}
#[cfg(target_arch = "x86_64")]
for kernel in ForwardEncoder::available_kernels() {
let resolved =
resolve_kernel_with_capabilities(kernel, runtime_kernel_capabilities()).unwrap();
if matches!(
resolved,
ResolvedKernel::Folded | ResolvedKernel::XorJitAvx2
) {
assert_eq!(
KernelContract::for_kernel(resolved).input_grouping,
DEFAULT_INPUT_GROUPING
);
}
}
}
#[test]
fn skewed_stripe_layout_matches_vandermonde_definition_on_every_kernel() {
const SLICE: usize = 4096;
assert_eq!(stripe_skew_bytes(SLICE), 512, "the skew must be live here");
let sources: Vec<Vec<u8>> = (0..27usize)
.map(|source| {
(0..(SLICE - source * 97))
.map(|index| (index.wrapping_mul(31) ^ (source * 53) ^ (index >> 7)) as u8)
.collect()
})
.collect();
let refs = sources.iter().map(Vec::as_slice).collect::<Vec<_>>();
let exponents: [RecoveryExponent; 4] = [0, 1, 31, 100];
let constants = gf::input_slice_constants(refs.len());
let mut expected = Vec::new();
for &exponent in &exponents {
let mut block = vec![0u8; SLICE];
for (source_index, source) in refs.iter().enumerate() {
let factor = gf::pow(constants[source_index], exponent);
for word in 0..SLICE / 2 {
let offset = word * 2;
let source_word = if offset < source.len() {
u16::from_le_bytes([
source[offset],
source.get(offset + 1).map_or(0, |byte| *byte),
])
} else {
0
};
let output_word = u16::from_le_bytes([block[offset], block[offset + 1]])
^ gf::mul(source_word, factor);
block[offset..offset + 2].copy_from_slice(&output_word.to_le_bytes());
}
}
expected.push(block);
}
for kernel in ForwardEncoder::available_kernels() {
let encoder = ForwardEncoder::new(SLICE, exponents.to_vec()).unwrap();
let actual = encoder
.encode(
&refs,
&ForwardEncoderOptions {
kernel,
..ForwardEncoderOptions::default()
},
)
.unwrap();
for (output, block) in expected.iter().enumerate() {
assert_eq!(
&actual[output].data, block,
"kernel {kernel:?} output {output} diverged from the definition"
);
}
}
}
#[test]
fn staging_layout_fits_the_planned_area_at_every_width() {
const BLOCK: usize = gf_simd::INPUT_BATCH_BLOCK_BYTES;
for aligned_len in [BLOCK, 4096usize, 8192, 65_536] {
for grouping in [1usize, 4, 12, 16] {
let base = KernelContract {
stride: BLOCK,
input_grouping: grouping,
tile_bytes: TABLE_TILE_BYTES,
skewed_lanes: true,
interleave_lanes: 1,
};
let stride = lane_stride(base, aligned_len);
let planned = grouping * stride;
for interleave in [1usize, 2, 4, 8, 16] {
let contract = KernelContract {
interleave_lanes: interleave,
..base
};
let layout = StagingLayout::new(contract, aligned_len, stride);
let total = layout.total_bytes().expect("layout fits usize");
assert!(
total <= planned,
"layout {interleave}x{grouping} at {aligned_len} wants {total} of {planned}"
);
let widths: usize = (0..layout.group_count())
.map(|group| layout.group_width(group))
.sum();
assert_eq!(widths, grouping, "every lane belongs to exactly one group");
for group in 0..layout.group_count() {
let (_, end) = layout.group_tile(group, aligned_len - BLOCK, BLOCK);
assert!(end <= total, "group {group} tile runs past the layout");
}
}
let lane_major = StagingLayout::new(base, aligned_len, stride);
for lane in 0..grouping {
assert_eq!(lane_major.group_base(lane), lane * stride);
}
}
}
}
#[test]
fn short_staging_is_refused_before_the_fill() {
const BLOCK: usize = gf_simd::INPUT_BATCH_BLOCK_BYTES;
let source = vec![0xA5u8; 512];
let refs = [source.as_slice()];
for interleave in [1usize, 8] {
let contract = KernelContract {
stride: BLOCK,
input_grouping: 8,
tile_bytes: TABLE_TILE_BYTES,
skewed_lanes: true,
interleave_lanes: interleave,
};
let stride = lane_stride(contract, 512);
let needed = StagingLayout::new(contract, 512, stride)
.total_bytes()
.unwrap();
let mut provider = InMemorySourceProvider { sources: &refs };
let mut staging = AlignedBuffer::new(needed - 1);
let mut transfer =
AlignedBuffer::new(contract.input_grouping * transfer_slot_stride(512).unwrap());
let mut slice_lens = [0usize; MAX_INPUT_GROUPING];
let result = fill_staging(
ResolvedKernel::Simd,
&mut staging,
&mut transfer,
&mut provider,
0,
0,
512,
512,
contract,
&mut slice_lens,
);
assert!(
matches!(result, Err(Par2Error::ResourceLimitExceeded { .. })),
"interleave {interleave} accepted a short staging area"
);
}
}
#[test]
fn interleaved_staging_matches_lane_major_and_the_word_wise_kernel() {
const BLOCK: usize = gf_simd::INPUT_BATCH_BLOCK_BYTES;
const SLICE: usize = 8 * 1024 + 96;
let sources: Vec<Vec<u8>> = (0..MAX_INPUT_GROUPING)
.map(|source| {
(0..(SLICE - source * 37))
.map(|index| (index.wrapping_mul(31) ^ (source * 53) ^ (index >> 5)) as u8)
.collect()
})
.collect();
let refs = sources.iter().map(Vec::as_slice).collect::<Vec<_>>();
let exponents: Vec<RecoveryExponent> = vec![0, 1, 2, 31, 100];
let simd = KernelContract::for_kernel(ResolvedKernel::Simd);
let aligned_len = round_up(SLICE, BLOCK).unwrap();
let run = |kernel: ResolvedKernel, contract: KernelContract, live: usize| -> Vec<u8> {
let mut provider = InMemorySourceProvider { sources: &refs };
let mut staging =
AlignedBuffer::new(contract.input_grouping * lane_stride(contract, aligned_len));
let mut transfer = AlignedBuffer::new(
contract.input_grouping * transfer_slot_stride(aligned_len).unwrap(),
);
let mut slice_lens = [0usize; MAX_INPUT_GROUPING];
fill_staging(
kernel,
&mut staging,
&mut transfer,
&mut provider,
0,
0,
SLICE,
aligned_len,
contract,
&mut slice_lens,
)
.unwrap();
let factors = FactorSource::new(refs.len());
let mut output = AlignedBuffer::new(exponents.len() * aligned_len);
#[cfg(target_arch = "x86_64")]
let mut jit_workspaces: Vec<
reedsolomon_rs::xor_jit::packed::PackedJitWorkspace,
> = vec![Default::default()];
accumulate_batch(
kernel,
output.as_bytes_mut(),
&staging,
&factors,
&exponents,
0,
live,
aligned_len,
aligned_len,
contract,
exponents.len(),
#[cfg(target_arch = "x86_64")]
&mut jit_workspaces,
#[cfg(target_arch = "x86_64")]
usize::MAX,
)
.unwrap();
output.as_bytes().to_vec()
};
for grouping in [12usize, 16, MAX_INPUT_GROUPING] {
let portable = KernelContract {
input_grouping: grouping,
..KernelContract::for_kernel(ResolvedKernel::Portable)
};
for live in [1usize, 3, 8, 11, grouping] {
let live = live.min(grouping).min(refs.len());
let definition = run(ResolvedKernel::Portable, portable, live);
for tile_bytes in [UNTILED, 8192usize, 2048] {
let mut lane_major: Option<Vec<u8>> = None;
for interleave in [1usize, 2, 4, 8, 16] {
let contract = KernelContract {
stride: BLOCK,
tile_bytes,
input_grouping: grouping,
interleave_lanes: interleave,
..simd
};
let got = run(ResolvedKernel::Simd, contract, live);
let case = format!(
"grouping={grouping} interleave={interleave} \
tile={tile_bytes} live={live}"
);
assert_eq!(
got, definition,
"simd {case} diverged from the word-wise kernel"
);
match &lane_major {
None => lane_major = Some(got),
Some(expected) => assert_eq!(
&got, expected,
"simd {case} diverged from the lane-major layout"
),
}
}
}
}
}
}
#[test]
fn zero_input_produces_zero_recovery_blocks() {
let encoder = ForwardEncoder::new(256, vec![0, 5]).unwrap();
let blocks = encoder
.encode(&[], &ForwardEncoderOptions::default())
.unwrap();
assert_eq!(blocks.len(), 2);
assert!(
blocks
.iter()
.all(|block| block.data.iter().all(|&byte| byte == 0))
);
}
}