1use std::mem::size_of;
14
15use crate::error::{Par2Error, Result};
16use crate::gf;
17use crate::types::{
18 CancellationToken, MAX_TOTAL_INPUT_SLICES, ProgressCallback, ProgressPhase, ProgressStage,
19 ProgressUpdate, RecoveryExponent,
20};
21use reedsolomon_rs::gf_simd::{self, PreparedFactorSrc};
22
23use super::plan::default_memory_limit;
24use super::transform::{self, TransformPolicy};
25
26pub(crate) use super::transform::EncodeAttempt;
27
28const DEFAULT_INPUT_GROUPING: usize = 12;
32#[cfg_attr(not(target_arch = "aarch64"), allow(dead_code))]
41const CLMUL_INPUT_GROUPING: usize = 16;
42const MAX_INPUT_GROUPING: usize = 16;
46const _: () = assert!(DEFAULT_INPUT_GROUPING <= MAX_INPUT_GROUPING);
47const _: () = assert!(CLMUL_INPUT_GROUPING <= MAX_INPUT_GROUPING);
48const DEFAULT_STAGING_AREA_COUNT: usize = 4;
72const MAX_STAGING_AREA_COUNT: usize = 8;
75const _: () = assert!(DEFAULT_STAGING_AREA_COUNT >= 2);
76const _: () = assert!(DEFAULT_STAGING_AREA_COUNT <= MAX_STAGING_AREA_COUNT);
77
78fn configured_staging_areas() -> usize {
87 static CONFIGURED: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
88 *CONFIGURED.get_or_init(|| {
89 std::env::var("WEAVER_PAR2_CREATE_AREAS")
90 .ok()
91 .and_then(|value| value.trim().parse::<usize>().ok())
92 .filter(|&areas| (2..=MAX_STAGING_AREA_COUNT).contains(&areas))
93 .unwrap_or(DEFAULT_STAGING_AREA_COUNT)
94 })
95}
96
97fn transfer_group_lanes() -> usize {
109 static CONFIGURED: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
110 *CONFIGURED.get_or_init(|| crate::md5_simd::max_lanes().clamp(1, MAX_INPUT_GROUPING))
111}
112
113#[cfg(target_arch = "x86_64")]
118const MAX_FOLDED_GROUPS: usize = DEFAULT_INPUT_GROUPING / gf_simd::FOLDED_GROUP;
119
120pub(crate) fn configured_create_threads() -> usize {
139 static CONFIGURED: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
140 *CONFIGURED.get_or_init(|| {
141 if !reedsolomon_rs::threading::parallel_enabled() {
150 return 1;
151 }
152 std::env::var("WEAVER_PAR2_CREATE_THREADS")
153 .ok()
154 .and_then(|value| value.trim().parse::<usize>().ok())
155 .filter(|&threads| threads != 0)
156 .unwrap_or_else(|| {
157 std::thread::available_parallelism()
158 .map(std::num::NonZeroUsize::get)
159 .unwrap_or(1)
160 })
161 })
162}
163
164fn configured_input_grouping() -> usize {
172 static CONFIGURED: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
173 *CONFIGURED.get_or_init(|| {
174 #[cfg(target_arch = "aarch64")]
175 let family_default = CLMUL_INPUT_GROUPING;
176 #[cfg(not(target_arch = "aarch64"))]
177 let family_default = DEFAULT_INPUT_GROUPING;
178 std::env::var("WEAVER_PAR2_CREATE_GROUPING")
179 .ok()
180 .and_then(|value| value.trim().parse::<usize>().ok())
181 .filter(|&grouping| (1..=MAX_INPUT_GROUPING).contains(&grouping))
182 .unwrap_or(family_default)
183 })
184}
185
186fn configured_interleave_lanes() -> usize {
212 static CONFIGURED: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
213 *CONFIGURED.get_or_init(|| {
214 std::env::var("WEAVER_PAR2_CREATE_INTERLEAVE")
215 .ok()
216 .and_then(|value| value.trim().parse::<usize>().ok())
217 .filter(|&lanes| (1..=MAX_INPUT_GROUPING).contains(&lanes))
218 .unwrap_or(gf_simd::INPUT_BATCH_INTERLEAVE_LANES)
219 })
220}
221
222fn simd_stride() -> usize {
232 if cfg!(target_arch = "aarch64") || configured_interleave_lanes() > 1 {
233 gf_simd::INPUT_BATCH_BLOCK_BYTES
234 } else {
235 2
236 }
237}
238
239fn create_band_shape(output_count: usize) -> (usize, usize) {
243 let outputs = output_count.max(1);
244 let target = configured_create_threads().clamp(1, outputs);
245 let band_size = outputs.div_ceil(target);
246 (band_size, outputs.div_ceil(band_size))
247}
248
249#[derive(Clone, Copy, Debug, Eq, PartialEq)]
251pub(crate) struct ForwardMemoryEstimate {
252 pub(crate) factor_workspace_bytes: usize,
253 pub(crate) jit_workspace_bytes: usize,
254 pub(crate) stripe_buffer_bytes: usize,
255 pub(crate) processing_peak_bytes: usize,
256}
257
258#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
265pub enum ForwardKernel {
266 #[default]
268 Auto,
269 Portable,
271 Simd,
273 #[cfg(target_arch = "x86_64")]
275 Folded,
276 #[cfg(target_arch = "x86_64")]
278 XorJitAvx2,
279}
280
281pub struct ForwardEncoderOptions {
283 pub memory_limit: Option<usize>,
286 pub cancel: Option<CancellationToken>,
288 pub progress: Option<ProgressCallback>,
291 pub kernel: ForwardKernel,
293 pub(crate) transform: TransformPolicy,
297}
298
299impl Default for ForwardEncoderOptions {
300 fn default() -> Self {
301 Self {
302 memory_limit: None,
303 cancel: None,
304 progress: None,
305 kernel: ForwardKernel::Auto,
306 transform: TransformPolicy::default(),
307 }
308 }
309}
310
311#[cfg(test)]
313#[derive(Clone, Debug, Eq, PartialEq)]
314pub struct ForwardRecoveryBlock {
315 pub exponent: RecoveryExponent,
317 pub data: Vec<u8>,
319}
320
321pub trait ForwardRecoverySink {
327 fn write_recovery_chunk(
329 &mut self,
330 output_index: usize,
331 exponent: RecoveryExponent,
332 offset: u64,
333 data: &[u8],
334 ) -> Result<()>;
335}
336
337pub(crate) trait ForwardSourceProvider {
339 fn source_count(&self) -> usize;
341
342 fn source_slice_len(&self, source_index: usize) -> Result<usize>;
344
345 fn read_source_chunk(
347 &mut self,
348 source_index: usize,
349 offset: usize,
350 destination: &mut [u8],
351 ) -> Result<usize>;
352}
353
354pub(crate) trait ForwardSourceObserver: Send {
369 fn observe_slices(&mut self, first_source_index: usize, slices: &[&[u8]]) -> Result<()>;
372}
373
374#[cfg(test)]
375struct InMemorySourceProvider<'a> {
376 sources: &'a [&'a [u8]],
377}
378
379#[cfg(test)]
380impl ForwardSourceProvider for InMemorySourceProvider<'_> {
381 fn source_count(&self) -> usize {
382 self.sources.len()
383 }
384
385 fn source_slice_len(&self, source_index: usize) -> Result<usize> {
386 self.sources
387 .get(source_index)
388 .map(|source| source.len())
389 .ok_or_else(|| invalid_input("source slice index is out of range"))
390 }
391
392 fn read_source_chunk(
393 &mut self,
394 source_index: usize,
395 offset: usize,
396 destination: &mut [u8],
397 ) -> Result<usize> {
398 let source = self
399 .sources
400 .get(source_index)
401 .ok_or_else(|| invalid_input("source slice index is out of range"))?;
402 let start = offset.min(source.len());
403 let take = destination.len().min(source.len().saturating_sub(start));
404 destination[..take].copy_from_slice(&source[start..start + take]);
405 Ok(take)
406 }
407}
408
409#[derive(Clone, Debug)]
411pub struct ForwardEncoder {
412 slice_size: usize,
413 recovery_exponents: Vec<RecoveryExponent>,
414}
415
416impl ForwardEncoder {
417 pub fn new(slice_size: usize, recovery_exponents: Vec<RecoveryExponent>) -> Result<Self> {
419 if slice_size == 0 || !slice_size.is_multiple_of(4) {
420 return Err(invalid_input(format!(
421 "slice size must be a nonzero multiple of 4, got {slice_size}"
422 )));
423 }
424 if recovery_exponents.len() > u32::MAX as usize {
425 return Err(resource_limit("recovery output count exceeds u32"));
426 }
427 Ok(Self {
428 slice_size,
429 recovery_exponents,
430 })
431 }
432
433 #[cfg(test)]
435 pub fn slice_size(&self) -> usize {
436 self.slice_size
437 }
438
439 #[cfg(test)]
441 pub fn available_kernels() -> Vec<ForwardKernel> {
442 let kernels = vec![ForwardKernel::Portable, ForwardKernel::Simd];
443 #[cfg(target_arch = "x86_64")]
444 {
445 let mut kernels = kernels;
446 let capabilities = runtime_kernel_capabilities();
447 if capabilities.folded {
448 kernels.push(ForwardKernel::Folded);
449 }
450 if capabilities.avx2_jit {
451 kernels.push(ForwardKernel::XorJitAvx2);
452 }
453 kernels
454 }
455 #[cfg(not(target_arch = "x86_64"))]
456 kernels
457 }
458
459 #[cfg(test)]
461 pub fn selected_kernel(&self, requested: ForwardKernel) -> Result<ForwardKernel> {
462 resolve_kernel_with_capabilities(requested, runtime_kernel_capabilities())
463 .map(public_kernel)
464 }
465
466 #[cfg(test)]
468 pub fn encode(
469 &self,
470 sources: &[&[u8]],
471 options: &ForwardEncoderOptions,
472 ) -> Result<Vec<ForwardRecoveryBlock>> {
473 let mut sink = VecRecoverySink::new(&self.recovery_exponents, self.slice_size);
474 let mut provider = InMemorySourceProvider { sources };
475 self.encode_to(&mut provider, options, &mut sink)?;
476 Ok(sink.blocks)
477 }
478
479 #[cfg(test)]
481 pub fn encode_slices_to<S: ForwardRecoverySink>(
482 &self,
483 sources: &[&[u8]],
484 options: &ForwardEncoderOptions,
485 sink: &mut S,
486 ) -> Result<()> {
487 let mut provider = InMemorySourceProvider { sources };
488 self.encode_to(&mut provider, options, sink)
489 }
490
491 #[cfg(test)]
493 pub fn encode_to<P: ForwardSourceProvider + ?Sized, S: ForwardRecoverySink>(
494 &self,
495 provider: &mut P,
496 options: &ForwardEncoderOptions,
497 sink: &mut S,
498 ) -> Result<()> {
499 self.encode_to_observed(provider, options, sink, None)
500 }
501
502 #[cfg(test)]
506 pub(crate) fn encode_to_observed<P: ForwardSourceProvider + ?Sized, S: ForwardRecoverySink>(
507 &self,
508 provider: &mut P,
509 options: &ForwardEncoderOptions,
510 sink: &mut S,
511 observer: Option<&mut dyn ForwardSourceObserver>,
512 ) -> Result<()> {
513 match self.encode_attempt(provider, options, sink, observer)? {
514 EncodeAttempt::Complete => Ok(()),
515 EncodeAttempt::TransformProbeMismatch => Err(resource_limit(
516 "transform recovery arithmetic disagreed with the dense definition",
517 )),
518 }
519 }
520
521 pub(crate) fn encode_attempt<P: ForwardSourceProvider + ?Sized, S: ForwardRecoverySink>(
525 &self,
526 provider: &mut P,
527 options: &ForwardEncoderOptions,
528 sink: &mut S,
529 observer: Option<&mut dyn ForwardSourceObserver>,
530 ) -> Result<EncodeAttempt> {
531 let mut observer = observer;
532 let observer = &mut observer;
533 validate_provider(provider, self.slice_size)?;
534 check_cancel(options)?;
535
536 if self.recovery_exponents.is_empty() {
537 return Ok(EncodeAttempt::Complete);
538 }
539
540 let memory_limit = options.memory_limit.unwrap_or_else(default_memory_limit);
541 let (kernel, buffers) = select_kernel_for_memory(
542 self.slice_size,
543 self.recovery_exponents.len(),
544 provider.source_count(),
545 memory_limit,
546 options.kernel,
547 )?;
548
549 if let Some(arm) = transform::admit(
553 self.slice_size,
554 provider.source_count(),
555 &self.recovery_exponents,
556 memory_limit.min(buffers.memory_bytes),
557 options.transform,
558 ) && (observer.is_none() || arm.shape().passes == 1)
559 {
560 return transform::encode(
561 &arm,
562 self.slice_size,
563 &self.recovery_exponents,
564 provider,
565 options,
566 sink,
567 match observer.as_mut() {
568 Some(observer) => Some(&mut **observer),
569 None => None,
570 },
571 );
572 }
573
574 let contract = KernelContract::for_kernel(kernel);
575
576 let factors = FactorSource::new(provider.source_count());
577
578 let staging_areas = configured_staging_areas();
583 let mut staging: Vec<std::sync::Arc<AlignedBuffer>> = (0..staging_areas)
584 .map(|_| std::sync::Arc::new(AlignedBuffer::new(buffers.staging_bytes)))
585 .collect();
586 let mut transfers: Vec<std::sync::Arc<TransferSlot>> = (0..staging_areas)
591 .map(|_| std::sync::Arc::new(TransferSlot::new(buffers.transfer_bytes)))
592 .collect();
593 let mut output = AlignedBuffer::new(buffers.output_bytes);
594
595 let (band_size, band_count) = create_band_shape(self.recovery_exponents.len());
596 #[cfg(not(target_arch = "x86_64"))]
597 let _ = band_count;
598 #[cfg(target_arch = "x86_64")]
599 let mut jit_workspaces: Vec<reedsolomon_rs::xor_jit::packed::PackedJitWorkspace> =
600 (0..band_count).map(|_| Default::default()).collect();
601 #[cfg(target_arch = "x86_64")]
602 let jit_code_budget = buffers.jit_build_limit_bytes;
603
604 let stripe_count = self.slice_size.div_ceil(buffers.chunk_len);
605 let stripe_count_u32 = u32::try_from(stripe_count)
606 .map_err(|_| resource_limit("stripe count exceeds progress range"))?;
607 let total_bytes = (self.recovery_exponents.len() as u64)
608 .checked_mul(self.slice_size as u64)
609 .ok_or_else(|| resource_limit("progress byte count overflow"))?;
610
611 let batch_starts: Vec<usize> = (0..provider.source_count())
625 .step_by(contract.input_grouping)
626 .collect();
627 let banded = band_size < self.recovery_exponents.len();
628
629 let mut stripe_offset = 0usize;
630 let mut stripe_index = 0usize;
631 while stripe_offset < self.slice_size {
632 check_cancel(options)?;
633 let actual_len = (self.slice_size - stripe_offset).min(buffers.chunk_len);
634 let aligned_len = round_up(actual_len, contract.stride)?;
635 if banded {
636 encode_stripe_banded(
637 kernel,
638 provider,
639 options,
640 contract,
641 &factors,
642 &self.recovery_exponents,
643 &mut staging,
644 &mut transfers,
645 &mut output.as_bytes_mut()[..buffers.output_bytes],
646 &batch_starts,
647 StripeGeometry {
648 stripe_offset,
649 actual_len,
650 aligned_len,
651 output_stride: buffers.row_stride,
652 },
653 band_size,
654 #[cfg(target_arch = "x86_64")]
655 &mut jit_workspaces,
656 #[cfg(target_arch = "x86_64")]
657 jit_code_budget,
658 match observer.as_mut() {
659 Some(observer) => Some(&mut **observer),
660 None => None,
661 },
662 )?;
663 } else {
664 output.as_bytes_mut()[..buffers.output_bytes].fill(0);
665 let mut slice_lens = [0usize; MAX_INPUT_GROUPING];
666 let source_count = provider.source_count();
667 if let Some(&first_start) = batch_starts.first() {
668 let slot = std::sync::Arc::get_mut(&mut transfers[0])
669 .ok_or_else(|| resource_limit("transfer slot is still in use"))?;
670 fill_staging(
671 kernel,
672 std::sync::Arc::get_mut(&mut staging[0])
673 .ok_or_else(|| resource_limit("staging area is still in use"))?,
674 &mut slot.buffer,
675 provider,
676 first_start,
677 stripe_offset,
678 actual_len,
679 aligned_len,
680 contract,
681 &mut slice_lens,
682 )?;
683 if let Some(observer) = observer.as_mut() {
684 observe_batch(
685 &mut **observer,
686 transfers[0].buffer.as_bytes(),
687 first_start,
688 live_batch_inputs(source_count, first_start, contract),
689 transfer_slot_stride(aligned_len)?,
690 &slice_lens,
691 )?;
692 }
693 }
694 for (batch_index, &source_start) in batch_starts.iter().enumerate() {
695 check_cancel(options)?;
696 let live_inputs = live_batch_inputs(source_count, source_start, contract);
697 let next_start = batch_starts.get(batch_index + 1).copied();
698 let current_area = batch_index % staging_areas;
699 let next_area = (batch_index + 1) % staging_areas;
700 accumulate_batch(
701 kernel,
702 &mut output.as_bytes_mut()[..buffers.output_bytes],
703 &staging[current_area],
704 &factors,
705 &self.recovery_exponents,
706 source_start,
707 live_inputs,
708 aligned_len,
709 buffers.row_stride,
710 contract,
711 band_size,
712 #[cfg(target_arch = "x86_64")]
713 &mut jit_workspaces,
714 #[cfg(target_arch = "x86_64")]
715 jit_code_budget,
716 )?;
717 if let Some(next_start) = next_start {
718 let slot = std::sync::Arc::get_mut(&mut transfers[next_area])
719 .ok_or_else(|| resource_limit("transfer slot is still in use"))?;
720 fill_staging(
721 kernel,
722 std::sync::Arc::get_mut(&mut staging[next_area])
723 .ok_or_else(|| resource_limit("staging area is still in use"))?,
724 &mut slot.buffer,
725 provider,
726 next_start,
727 stripe_offset,
728 actual_len,
729 aligned_len,
730 contract,
731 &mut slice_lens,
732 )?;
733 if let Some(observer) = observer.as_mut() {
734 observe_batch(
735 &mut **observer,
736 transfers[next_area].buffer.as_bytes(),
737 next_start,
738 live_batch_inputs(source_count, next_start, contract),
739 transfer_slot_stride(aligned_len)?,
740 &slice_lens,
741 )?;
742 }
743 }
744 }
745
746 finish_output(
747 kernel,
748 &mut output.as_bytes_mut()[..buffers.output_bytes],
749 buffers.row_stride,
750 aligned_len,
751 self.recovery_exponents.len(),
752 )?;
753 }
754
755 for (output_index, &exponent) in self.recovery_exponents.iter().enumerate() {
756 let start = output_index
757 .checked_mul(buffers.row_stride)
758 .ok_or_else(|| resource_limit("output stripe offset overflow"))?;
759 let end = start
760 .checked_add(actual_len)
761 .ok_or_else(|| resource_limit("output stripe end overflow"))?;
762 sink.write_recovery_chunk(
763 output_index,
764 exponent,
765 stripe_offset as u64,
766 &output.as_bytes()[start..end],
767 )?;
768 }
769
770 stripe_index += 1;
771 let completed_stripe = u32::try_from(stripe_index - 1)
772 .map_err(|_| resource_limit("completed stripe exceeds progress range"))?;
773 report_progress(
774 options,
775 completed_stripe,
776 stripe_count_u32,
777 (stripe_index as u64)
778 .saturating_mul(self.recovery_exponents.len() as u64)
779 .saturating_mul(buffers.chunk_len as u64)
780 .min(total_bytes),
781 total_bytes,
782 );
783 stripe_offset = stripe_offset
784 .checked_add(actual_len)
785 .ok_or_else(|| resource_limit("stripe offset overflow"))?;
786 }
787
788 check_cancel(options)?;
789 Ok(EncodeAttempt::Complete)
790 }
791}
792
793#[derive(Clone, Copy)]
795struct StripeGeometry {
796 stripe_offset: usize,
797 actual_len: usize,
798 aligned_len: usize,
799 output_stride: usize,
800}
801
802struct TransferSlot {
809 buffer: AlignedBuffer,
810 slot_stride: usize,
812 slice_lens: [usize; MAX_INPUT_GROUPING],
814}
815
816impl TransferSlot {
817 fn new(bytes: usize) -> Self {
818 Self {
819 buffer: AlignedBuffer::new(bytes),
820 slot_stride: 0,
821 slice_lens: [0; MAX_INPUT_GROUPING],
822 }
823 }
824}
825
826#[derive(Clone)]
834struct BatchTicket {
835 staging: std::sync::Arc<AlignedBuffer>,
836 transfer: std::sync::Arc<TransferSlot>,
837 source_start: usize,
838 live_inputs: usize,
839}
840
841struct FeedState {
842 tickets: Vec<Option<BatchTicket>>,
843 published: usize,
846 completed: usize,
849 done: Vec<usize>,
854 hash_turn: usize,
859 failed: bool,
862}
863
864struct StripeFeed {
866 areas: usize,
867 state: std::sync::Mutex<FeedState>,
868 ready: std::sync::Condvar,
869 free: std::sync::Condvar,
870 hashed: std::sync::Condvar,
871 band_count: usize,
872}
873
874impl StripeFeed {
875 fn new(band_count: usize, areas: usize) -> Self {
876 Self {
877 areas,
878 state: std::sync::Mutex::new(FeedState {
879 tickets: vec![None; areas],
880 published: 0,
881 completed: 0,
882 done: vec![0; areas],
883 hash_turn: 0,
884 failed: false,
885 }),
886 ready: std::sync::Condvar::new(),
887 free: std::sync::Condvar::new(),
888 hashed: std::sync::Condvar::new(),
889 band_count,
890 }
891 }
892
893 fn lock(&self) -> std::sync::MutexGuard<'_, FeedState> {
894 self.state
895 .lock()
896 .unwrap_or_else(std::sync::PoisonError::into_inner)
897 }
898
899 fn wait_for_area(&self, batch_index: usize) -> bool {
903 let mut state = self.lock();
904 while !state.failed && state.completed + self.areas <= batch_index {
905 state = self
906 .free
907 .wait(state)
908 .unwrap_or_else(std::sync::PoisonError::into_inner);
909 }
910 if state.failed {
911 return false;
912 }
913 state.tickets[batch_index % self.areas] = None;
914 true
915 }
916
917 fn publish(&self, batch_index: usize, ticket: BatchTicket) {
919 let mut state = self.lock();
920 state.tickets[batch_index % self.areas] = Some(ticket);
921 state.published = batch_index + 1;
922 drop(state);
923 self.ready.notify_all();
924 }
925
926 fn acquire(&self, batch_index: usize) -> Option<BatchTicket> {
929 let mut state = self.lock();
930 while !state.failed && state.published <= batch_index {
931 state = self
932 .ready
933 .wait(state)
934 .unwrap_or_else(std::sync::PoisonError::into_inner);
935 }
936 if state.failed {
937 return None;
938 }
939 state.tickets[batch_index % self.areas].clone()
940 }
941
942 fn release(&self, batch_index: usize) {
945 let mut state = self.lock();
946 let area = batch_index % self.areas;
947 state.done[area] += 1;
948 if state.done[area] == self.band_count {
949 state.done[area] = 0;
950 state.completed = batch_index + 1;
951 drop(state);
952 self.free.notify_all();
953 }
954 }
955
956 fn wait_for_hash_turn(&self, batch_index: usize) -> bool {
959 let mut state = self.lock();
960 while !state.failed && state.hash_turn < batch_index {
961 state = self
962 .hashed
963 .wait(state)
964 .unwrap_or_else(std::sync::PoisonError::into_inner);
965 }
966 !state.failed
967 }
968
969 fn finish_hash_turn(&self, batch_index: usize) {
972 let mut state = self.lock();
973 state.hash_turn = batch_index + 1;
974 drop(state);
975 self.hashed.notify_all();
976 }
977
978 fn fail(&self) {
980 let mut state = self.lock();
981 state.failed = true;
982 drop(state);
985 self.ready.notify_all();
986 self.free.notify_all();
987 self.hashed.notify_all();
988 }
989}
990
991#[allow(clippy::too_many_arguments)]
1002fn encode_stripe_banded<P: ForwardSourceProvider + ?Sized>(
1003 kernel: ResolvedKernel,
1004 provider: &mut P,
1005 options: &ForwardEncoderOptions,
1006 contract: KernelContract,
1007 factors: &FactorSource,
1008 exponents: &[RecoveryExponent],
1009 staging: &mut [std::sync::Arc<AlignedBuffer>],
1010 transfers: &mut [std::sync::Arc<TransferSlot>],
1011 output: &mut [u8],
1012 batch_starts: &[usize],
1013 geometry: StripeGeometry,
1014 band_size: usize,
1015 #[cfg(target_arch = "x86_64")]
1016 jit_workspaces: &mut [reedsolomon_rs::xor_jit::packed::PackedJitWorkspace],
1017 #[cfg(target_arch = "x86_64")] jit_code_budget: usize,
1018 observer: Option<&mut dyn ForwardSourceObserver>,
1019) -> Result<()> {
1020 debug_assert_eq!(output.len(), exponents.len() * geometry.output_stride);
1021 let band_bytes = checked_mul(
1022 band_size,
1023 geometry.output_stride,
1024 "band byte range overflow",
1025 )?;
1026 let band_count = exponents.len().div_ceil(band_size);
1027 #[cfg(target_arch = "x86_64")]
1028 debug_assert_eq!(jit_workspaces.len(), band_count);
1029 let batch_count = batch_starts.len();
1030 let feed = StripeFeed::new(band_count, configured_staging_areas());
1031 let feed = &feed;
1032 let source_count = provider.source_count();
1033
1034 let observer = observer.map(std::sync::Mutex::new);
1055 let observer = observer.as_ref();
1056 let mut band_results: Vec<Result<()>> = Vec::with_capacity(band_count);
1057
1058 let produced = std::thread::scope(|scope| {
1059 let mut handles = Vec::with_capacity(band_count);
1060 let bands = output
1061 .chunks_mut(band_bytes)
1062 .zip(exponents.chunks(band_size));
1063 #[cfg(target_arch = "x86_64")]
1064 let bands = bands.zip(jit_workspaces.iter_mut());
1065 for (band_index, band) in bands.enumerate() {
1066 #[cfg(target_arch = "x86_64")]
1067 let ((band_output, band_exponents), jit_workspace) = band;
1068 #[cfg(not(target_arch = "x86_64"))]
1069 let (band_output, band_exponents) = band;
1070 handles.push(scope.spawn(move || {
1071 accumulate_band_stream(
1072 feed,
1073 kernel,
1074 band_output,
1075 band_exponents,
1076 factors,
1077 contract,
1078 geometry,
1079 batch_count,
1080 #[cfg(target_arch = "x86_64")]
1081 jit_workspace,
1082 #[cfg(target_arch = "x86_64")]
1083 jit_code_budget,
1084 BandHashDuty {
1085 band_index,
1086 band_count,
1087 observer,
1088 },
1089 )
1090 }));
1091 }
1092
1093 let produced = produce_stripe(
1094 kernel,
1095 provider,
1096 options,
1097 contract,
1098 staging,
1099 transfers,
1100 batch_starts,
1101 geometry,
1102 source_count,
1103 feed,
1104 );
1105 if produced.is_err() {
1106 feed.fail();
1107 }
1108 band_results.extend(handles.into_iter().map(|handle| {
1109 handle
1110 .join()
1111 .unwrap_or_else(|payload| std::panic::resume_unwind(payload))
1112 }));
1113 produced
1114 });
1115
1116 produced?;
1117 for result in band_results {
1118 result?;
1119 }
1120 Ok(())
1121}
1122
1123#[derive(Clone, Copy)]
1126struct BandHashDuty<'turn, 'observer> {
1127 band_index: usize,
1128 band_count: usize,
1129 observer: Option<&'turn std::sync::Mutex<&'observer mut dyn ForwardSourceObserver>>,
1130}
1131
1132#[allow(clippy::too_many_arguments)]
1136fn produce_stripe<P: ForwardSourceProvider + ?Sized>(
1137 kernel: ResolvedKernel,
1138 provider: &mut P,
1139 options: &ForwardEncoderOptions,
1140 contract: KernelContract,
1141 staging: &mut [std::sync::Arc<AlignedBuffer>],
1142 transfers: &mut [std::sync::Arc<TransferSlot>],
1143 batch_starts: &[usize],
1144 geometry: StripeGeometry,
1145 source_count: usize,
1146 feed: &StripeFeed,
1147) -> Result<()> {
1148 let slot_stride = transfer_slot_stride(geometry.aligned_len)?;
1149 for (batch_index, &source_start) in batch_starts.iter().enumerate() {
1150 check_cancel(options)?;
1151 if !feed.wait_for_area(batch_index) {
1152 return Ok(());
1154 }
1155 let area = batch_index % feed.areas;
1156 let staged = std::sync::Arc::get_mut(&mut staging[area])
1157 .ok_or_else(|| resource_limit("staging area is still in use"))?;
1158 let slot = std::sync::Arc::get_mut(&mut transfers[area])
1159 .ok_or_else(|| resource_limit("transfer slot is still in use"))?;
1160 slot.slot_stride = slot_stride;
1161 fill_staging(
1162 kernel,
1163 staged,
1164 &mut slot.buffer,
1165 provider,
1166 source_start,
1167 geometry.stripe_offset,
1168 geometry.actual_len,
1169 geometry.aligned_len,
1170 contract,
1171 &mut slot.slice_lens,
1172 )?;
1173 feed.publish(
1174 batch_index,
1175 BatchTicket {
1176 staging: std::sync::Arc::clone(&staging[area]),
1177 transfer: std::sync::Arc::clone(&transfers[area]),
1178 source_start,
1179 live_inputs: live_batch_inputs(source_count, source_start, contract),
1180 },
1181 );
1182 }
1183 Ok(())
1184}
1185
1186#[allow(clippy::too_many_arguments)]
1190fn accumulate_band_stream(
1191 feed: &StripeFeed,
1192 kernel: ResolvedKernel,
1193 band_output: &mut [u8],
1194 band_exponents: &[RecoveryExponent],
1195 factors: &FactorSource,
1196 contract: KernelContract,
1197 geometry: StripeGeometry,
1198 batch_count: usize,
1199 #[cfg(target_arch = "x86_64")]
1200 jit_workspace: &mut reedsolomon_rs::xor_jit::packed::PackedJitWorkspace,
1201 #[cfg(target_arch = "x86_64")] jit_code_budget: usize,
1202 hash_duty: BandHashDuty<'_, '_>,
1203) -> Result<()> {
1204 band_output.fill(0);
1208 for batch_index in 0..batch_count {
1209 let Some(ticket) = feed.acquire(batch_index) else {
1210 return Ok(());
1211 };
1212 let accumulated = accumulate_band(
1213 kernel,
1214 band_output,
1215 &ticket.staging,
1216 factors,
1217 band_exponents,
1218 ticket.source_start,
1219 ticket.live_inputs,
1220 geometry.aligned_len,
1221 geometry.output_stride,
1222 contract,
1223 #[cfg(target_arch = "x86_64")]
1224 jit_workspace,
1225 #[cfg(target_arch = "x86_64")]
1226 jit_code_budget,
1227 );
1228 if let Err(error) = accumulated {
1229 drop(ticket);
1230 feed.fail();
1231 return Err(error);
1232 }
1233 if let Some(hashed) = hash_batch_if_owned(feed, &ticket, batch_index, hash_duty) {
1234 if let Err(error) = hashed {
1235 drop(ticket);
1236 feed.fail();
1237 return Err(error);
1238 }
1239 feed.finish_hash_turn(batch_index);
1242 }
1243 drop(ticket);
1247 feed.release(batch_index);
1248 }
1249 finish_band_rows(
1250 kernel,
1251 band_output,
1252 geometry.output_stride,
1253 geometry.aligned_len,
1254 band_exponents.len(),
1255 )
1256 .inspect_err(|_| feed.fail())
1257}
1258
1259fn hash_batch_if_owned(
1267 feed: &StripeFeed,
1268 ticket: &BatchTicket,
1269 batch_index: usize,
1270 duty: BandHashDuty<'_, '_>,
1271) -> Option<Result<()>> {
1272 let observer = duty.observer?;
1273 if batch_index % duty.band_count != duty.band_index {
1274 return None;
1275 }
1276 if !feed.wait_for_hash_turn(batch_index) {
1277 return None;
1280 }
1281 let mut observer = observer
1284 .lock()
1285 .unwrap_or_else(std::sync::PoisonError::into_inner);
1286 Some(observe_batch(
1287 &mut **observer,
1288 ticket.transfer.buffer.as_bytes(),
1289 ticket.source_start,
1290 ticket.live_inputs,
1291 ticket.transfer.slot_stride,
1292 &ticket.transfer.slice_lens,
1293 ))
1294}
1295
1296#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1297enum ResolvedKernel {
1298 Portable,
1299 Simd,
1300 #[cfg(target_arch = "x86_64")]
1301 Folded,
1302 #[cfg(target_arch = "x86_64")]
1303 XorJitAvx2,
1304}
1305
1306#[cfg(test)]
1307fn public_kernel(kernel: ResolvedKernel) -> ForwardKernel {
1308 match kernel {
1309 ResolvedKernel::Portable => ForwardKernel::Portable,
1310 ResolvedKernel::Simd => ForwardKernel::Simd,
1311 #[cfg(target_arch = "x86_64")]
1312 ResolvedKernel::Folded => ForwardKernel::Folded,
1313 #[cfg(target_arch = "x86_64")]
1314 ResolvedKernel::XorJitAvx2 => ForwardKernel::XorJitAvx2,
1315 }
1316}
1317
1318fn resolve_kernel_with_capabilities(
1319 requested: ForwardKernel,
1320 capabilities: KernelCapabilities,
1321) -> Result<ResolvedKernel> {
1322 #[cfg(not(target_arch = "x86_64"))]
1323 let _ = capabilities;
1324
1325 match requested {
1326 ForwardKernel::Portable => Ok(ResolvedKernel::Portable),
1327 ForwardKernel::Simd => Ok(ResolvedKernel::Simd),
1328 #[cfg(target_arch = "x86_64")]
1329 ForwardKernel::Folded => {
1330 if capabilities.folded {
1331 return Ok(ResolvedKernel::Folded);
1332 }
1333 Err(unavailable_kernel("folded AVX2"))
1334 }
1335 #[cfg(target_arch = "x86_64")]
1336 ForwardKernel::XorJitAvx2 => {
1337 if capabilities.avx2_jit {
1338 return Ok(ResolvedKernel::XorJitAvx2);
1339 }
1340 Err(unavailable_kernel("packed AVX2 XOR-JIT"))
1341 }
1342 ForwardKernel::Auto => {
1343 #[cfg(target_arch = "x86_64")]
1362 {
1363 if capabilities.folded {
1364 return Ok(ResolvedKernel::Folded);
1365 }
1366 if capabilities.avx2_jit {
1367 return Ok(ResolvedKernel::XorJitAvx2);
1368 }
1369 }
1370 Ok(ResolvedKernel::Simd)
1371 }
1372 }
1373}
1374
1375#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1376struct KernelCapabilities {
1377 folded: bool,
1379 folded_wide: bool,
1381 avx2_jit: bool,
1384}
1385
1386fn runtime_kernel_capabilities() -> KernelCapabilities {
1387 #[cfg(target_arch = "x86_64")]
1388 {
1389 KernelCapabilities {
1390 folded: gf_simd::altmap_supported(),
1391 folded_wide: gf_simd::folded_wide_shuffle_available(),
1392 avx2_jit: reedsolomon_rs::xor_jit::JitWidth::detect().is_some(),
1393 }
1394 }
1395 #[cfg(not(target_arch = "x86_64"))]
1396 KernelCapabilities {
1397 folded: false,
1398 folded_wide: false,
1399 avx2_jit: false,
1400 }
1401}
1402
1403#[cfg_attr(not(target_arch = "x86_64"), allow(dead_code))]
1421const AFFINE_TILE_BYTES: usize = 4 * 1024;
1422const TABLE_TILE_BYTES: usize = 8 * 1024;
1423#[cfg_attr(not(target_arch = "x86_64"), allow(dead_code))]
1426const UNTILED: usize = usize::MAX;
1427
1428fn configured_tile_bytes() -> Option<usize> {
1439 static CONFIGURED: std::sync::OnceLock<Option<usize>> = std::sync::OnceLock::new();
1440 *CONFIGURED.get_or_init(|| {
1441 std::env::var("WEAVER_PAR2_CREATE_TILE")
1442 .ok()
1443 .and_then(|value| value.trim().parse::<usize>().ok())
1444 .map(|bytes| if bytes == 0 { UNTILED } else { bytes })
1445 })
1446}
1447
1448fn family_tile_bytes(default_bytes: usize, stride: usize) -> usize {
1451 let requested = configured_tile_bytes().unwrap_or(default_bytes);
1452 if requested == UNTILED || stride == 0 {
1453 return requested;
1454 }
1455 requested
1456 .max(stride)
1457 .div_ceil(stride)
1458 .saturating_mul(stride)
1459}
1460
1461const SKEW_PERIOD_BYTES: usize = 4096;
1491const SKEW_TARGET_RESIDUE_BYTES: usize = 1024;
1492
1493fn stripe_skew_bytes(aligned_len: usize) -> usize {
1499 let residue = aligned_len % SKEW_PERIOD_BYTES;
1500 let wanted = (SKEW_TARGET_RESIDUE_BYTES + SKEW_PERIOD_BYTES - residue) % SKEW_PERIOD_BYTES;
1501 let cap = aligned_len / 8;
1502 wanted.min(cap) / 64 * 64
1503}
1504
1505fn lane_stride(contract: KernelContract, aligned_len: usize) -> usize {
1510 if contract.skewed_lanes {
1511 aligned_len + stripe_skew_bytes(aligned_len)
1512 } else {
1513 aligned_len
1514 }
1515}
1516
1517#[derive(Clone, Copy)]
1540struct StagingLayout {
1541 interleave: usize,
1543 lanes: usize,
1545 aligned_len: usize,
1547 group_pitch: usize,
1549}
1550
1551impl StagingLayout {
1552 fn new(contract: KernelContract, aligned_len: usize, lane_stride: usize) -> Self {
1553 let lanes = contract.input_grouping.max(1);
1554 let interleave = contract.interleave_lanes.clamp(1, lanes);
1555 let group_pitch = if interleave == 1 {
1556 lane_stride
1557 } else {
1558 interleave * aligned_len + stripe_skew_bytes(aligned_len)
1562 };
1563 Self {
1564 interleave,
1565 lanes,
1566 aligned_len,
1567 group_pitch,
1568 }
1569 }
1570
1571 fn group_count(&self) -> usize {
1572 self.lanes.div_ceil(self.interleave).max(1)
1573 }
1574
1575 fn group_width(&self, group: usize) -> usize {
1580 self.lanes
1581 .saturating_sub(group * self.interleave)
1582 .min(self.interleave)
1583 }
1584
1585 fn group_base(&self, group: usize) -> usize {
1586 group * self.group_pitch
1587 }
1588
1589 fn total_bytes(&self) -> Option<usize> {
1591 let last = self.group_count() - 1;
1592 last.checked_mul(self.group_pitch)?
1593 .checked_add(self.group_width(last).checked_mul(self.aligned_len)?)
1594 }
1595
1596 fn group_tile(&self, group: usize, tile_start: usize, tile_len: usize) -> (usize, usize) {
1601 let width = self.group_width(group);
1602 let start = self.group_base(group) + tile_start * width;
1603 (start, start + tile_len * width)
1604 }
1605}
1606
1607const COEFF_ROWS: usize = 16;
1617
1618fn stripe_tiles(aligned_len: usize, tile_bytes: usize) -> impl Iterator<Item = (usize, usize)> {
1625 let tile = tile_bytes.min(aligned_len).max(1);
1626 (0..aligned_len)
1627 .step_by(tile)
1628 .map(move |start| (start, tile.min(aligned_len - start)))
1629}
1630
1631#[derive(Clone, Copy)]
1632struct KernelContract {
1633 stride: usize,
1634 input_grouping: usize,
1635 tile_bytes: usize,
1636 skewed_lanes: bool,
1639 interleave_lanes: usize,
1643}
1644
1645impl KernelContract {
1646 fn for_kernel(kernel: ResolvedKernel) -> Self {
1647 match kernel {
1648 ResolvedKernel::Portable => Self {
1652 stride: 2,
1653 input_grouping: configured_input_grouping(),
1654 tile_bytes: family_tile_bytes(TABLE_TILE_BYTES, 2),
1655 skewed_lanes: true,
1656 interleave_lanes: 1,
1657 },
1658 ResolvedKernel::Simd => Self {
1659 stride: simd_stride(),
1660 input_grouping: configured_input_grouping(),
1661 tile_bytes: family_tile_bytes(TABLE_TILE_BYTES, simd_stride()),
1662 skewed_lanes: true,
1665 interleave_lanes: configured_interleave_lanes(),
1666 },
1667 #[cfg(target_arch = "x86_64")]
1668 ResolvedKernel::Folded => Self {
1669 stride: gf_simd::SPLIT_BLOCK_BYTES,
1670 input_grouping: DEFAULT_INPUT_GROUPING,
1671 tile_bytes: family_tile_bytes(
1676 if gf_simd::folded_uses_gfni() {
1677 AFFINE_TILE_BYTES
1678 } else {
1679 TABLE_TILE_BYTES
1680 },
1681 gf_simd::SPLIT_BLOCK_BYTES,
1682 ),
1683 skewed_lanes: true,
1687 interleave_lanes: 1,
1691 },
1692 #[cfg(target_arch = "x86_64")]
1693 ResolvedKernel::XorJitAvx2 => Self {
1694 stride: reedsolomon_rs::xor_jit::JitWidth::Avx2.block_bytes(),
1695 input_grouping: DEFAULT_INPUT_GROUPING,
1696 tile_bytes: UNTILED,
1700 skewed_lanes: false,
1703 interleave_lanes: 1,
1706 },
1707 }
1708 }
1709}
1710
1711fn factor_workspace_bytes(kernel: ResolvedKernel, source_count: usize) -> Result<usize> {
1712 let constants = checked_mul(
1713 source_count,
1714 size_of::<u16>(),
1715 "factor constant allocation overflow",
1716 )?;
1717 let grouping = KernelContract::for_kernel(kernel).input_grouping;
1720 let row = checked_mul(
1721 MAX_INPUT_GROUPING,
1722 size_of::<u16>(),
1723 "factor row allocation overflow",
1724 )?;
1725 let active = match kernel {
1726 ResolvedKernel::Portable => row,
1727 ResolvedKernel::Simd => checked_add(
1728 row,
1729 checked_add(
1730 checked_mul(
1731 checked_mul(COEFF_ROWS, grouping, "prepared factor allocation overflow")?,
1736 size_of::<gf_simd::PreparedInputFactor>(),
1737 "prepared factor allocation overflow",
1738 )?,
1739 checked_mul(
1740 MAX_INPUT_GROUPING,
1741 size_of::<PreparedFactorSrc>(),
1742 "prepared source allocation overflow",
1743 )?,
1744 "prepared factor allocation overflow",
1745 )?,
1746 "prepared factor allocation overflow",
1747 )?,
1748 #[cfg(target_arch = "x86_64")]
1749 ResolvedKernel::Folded => {
1750 let groups = DEFAULT_INPUT_GROUPING / gf_simd::FOLDED_GROUP;
1751 let chunk_lanes = checked_mul(
1753 COEFF_ROWS,
1754 DEFAULT_INPUT_GROUPING,
1755 "folded table allocation overflow",
1756 )?;
1757 let affine_tables = checked_mul(
1758 chunk_lanes,
1759 size_of::<gf_simd::AffineMulMatrices>(),
1760 "folded affine table allocation overflow",
1761 )?;
1762 let shuffle_tables = checked_mul(
1763 chunk_lanes,
1764 size_of::<gf_simd::Shuffle2xTables>(),
1765 "folded shuffle table allocation overflow",
1766 )?;
1767 let staging_views = checked_mul(
1768 groups,
1769 size_of::<&[u8]>(),
1770 "folded staging view allocation overflow",
1771 )?;
1772 let affine_sets = checked_mul(
1773 groups,
1774 size_of::<[&gf_simd::AffineMulMatrices; gf_simd::FOLDED_GROUP]>(),
1775 "folded affine set allocation overflow",
1776 )?;
1777 let shuffle_sets = checked_mul(
1778 groups,
1779 size_of::<[&gf_simd::Shuffle2xTables; gf_simd::FOLDED_GROUP]>(),
1780 "folded shuffle set allocation overflow",
1781 )?;
1782 checked_add(
1783 row,
1784 [
1785 affine_tables,
1786 shuffle_tables,
1787 staging_views,
1788 affine_sets,
1789 shuffle_sets,
1790 ]
1791 .into_iter()
1792 .try_fold(0usize, |total, bytes| {
1793 checked_add(total, bytes, "folded factor allocation overflow")
1794 })?,
1795 "folded factor allocation overflow",
1796 )?
1797 }
1798 #[cfg(target_arch = "x86_64")]
1799 ResolvedKernel::XorJitAvx2 => row,
1800 };
1801 checked_add(constants, active, "factor workspace allocation overflow")
1807}
1808
1809fn jit_workspace_bytes(kernel: ResolvedKernel, output_count: usize) -> Result<(usize, usize)> {
1815 #[cfg(target_arch = "x86_64")]
1816 if matches!(kernel, ResolvedKernel::XorJitAvx2) {
1817 let (band_size, band_count) = create_band_shape(output_count.max(1));
1818 let estimate = reedsolomon_rs::xor_jit::packed::PackedJitBatch::memory_upper_bound(
1819 reedsolomon_rs::xor_jit::JitWidth::Avx2,
1820 band_size.max(1),
1821 DEFAULT_INPUT_GROUPING,
1822 )
1823 .ok_or_else(|| resource_limit("packed JIT workspace size overflows"))?;
1824 let reserved = estimate
1825 .peak_bytes
1826 .checked_mul(band_count)
1827 .ok_or_else(|| resource_limit("banded JIT workspace accounting overflows"))?;
1828 return Ok((reserved, estimate.executable_arena_bytes));
1829 }
1830 let _ = (kernel, output_count);
1831 Ok((0, 0))
1832}
1833
1834fn parse_kernel_override(value: &str) -> Result<ForwardKernel> {
1869 match value.trim().to_ascii_lowercase().as_str() {
1870 "auto" => Ok(ForwardKernel::Auto),
1871 "portable" => Ok(ForwardKernel::Portable),
1872 "simd" => Ok(ForwardKernel::Simd),
1873 #[cfg(target_arch = "x86_64")]
1874 "folded" => Ok(ForwardKernel::Folded),
1875 #[cfg(target_arch = "x86_64")]
1876 "xor-jit-avx2" => Ok(ForwardKernel::XorJitAvx2),
1877 other => Err(invalid_input(format!(
1878 "WEAVER_PAR2_CREATE_KERNEL={other:?} names no kernel on this \
1879 architecture; use auto, portable, simd, folded or xor-jit-avx2"
1880 ))),
1881 }
1882}
1883
1884fn configured_kernel_override() -> Result<Option<ForwardKernel>> {
1895 static CONFIGURED: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
1896 CONFIGURED
1897 .get_or_init(|| std::env::var("WEAVER_PAR2_CREATE_KERNEL").ok())
1898 .as_deref()
1899 .filter(|value| !value.trim().is_empty())
1900 .map(parse_kernel_override)
1901 .transpose()
1902}
1903
1904fn configured_stripe_cap_bytes() -> Option<usize> {
1905 static CONFIGURED: std::sync::OnceLock<Option<usize>> = std::sync::OnceLock::new();
1906 *CONFIGURED.get_or_init(|| {
1907 std::env::var("WEAVER_PAR2_CREATE_STRIPE_MIB")
1908 .ok()
1909 .and_then(|value| value.trim().parse::<usize>().ok())
1910 .filter(|&mib| mib != 0)
1911 .and_then(|mib| mib.checked_mul(1024 * 1024))
1912 })
1913}
1914
1915struct BufferPlan {
1916 chunk_len: usize,
1917 #[cfg_attr(not(test), allow(dead_code))]
1920 aligned_chunk_len: usize,
1921 row_stride: usize,
1925 staging_bytes: usize,
1926 output_bytes: usize,
1927 transfer_bytes: usize,
1929 data_bytes: usize,
1930 memory_bytes: usize,
1931 #[cfg_attr(not(target_arch = "x86_64"), allow(dead_code))]
1934 jit_build_limit_bytes: usize,
1935}
1936
1937impl BufferPlan {
1938 fn new_with_reserved(
1939 slice_size: usize,
1940 output_count: usize,
1941 contract: KernelContract,
1942 memory_limit: usize,
1943 factor_workspace_bytes: usize,
1944 jit_workspace_bytes: usize,
1945 jit_build_limit_bytes: usize,
1946 ) -> Result<Self> {
1947 if memory_limit == 0 {
1948 return Err(resource_limit("forward memory limit is zero"));
1949 }
1950 let reserved_bytes = checked_add(
1951 factor_workspace_bytes,
1952 jit_workspace_bytes,
1953 "forward persistent memory accounting overflow",
1954 )?;
1955 let stripe_memory_limit = memory_limit.checked_sub(reserved_bytes).ok_or_else(|| {
1956 resource_limit(format!(
1957 "forward persistent allocations need {reserved_bytes} bytes, limit is {memory_limit}"
1958 ))
1959 })?;
1960 let chosen_stripe_limit = match configured_stripe_cap_bytes() {
1966 Some(cap) => stripe_memory_limit.min(cap),
1967 None => stripe_memory_limit,
1968 };
1969 let mut chunk_len = if slice_size >= contract.stride {
1970 slice_size - slice_size % contract.stride
1971 } else {
1972 slice_size
1973 };
1974 chunk_len = chunk_len.max(2);
1975
1976 loop {
1977 let aligned_chunk_len = round_up(chunk_len.min(slice_size), contract.stride)?;
1978 let skew = stripe_skew_bytes(aligned_chunk_len);
1983 let lane_alloc = checked_add(aligned_chunk_len, skew, "staging lane overflow")?;
1984 let row_stride = lane_alloc;
1985 let staging_bytes = checked_mul(
1986 contract.input_grouping,
1987 lane_alloc,
1988 "staging allocation overflow",
1989 )?;
1990 let output_bytes = checked_mul(output_count, row_stride, "output allocation overflow")?;
1991 let aligned_allocation_bytes = checked_mul(
1992 aligned_chunk_len.div_ceil(64),
1993 64,
1994 "aligned buffer allocation overflow",
1995 )?;
1996 let skewed_allocation_bytes = checked_add(
1997 aligned_allocation_bytes,
1998 skew,
1999 "aligned buffer allocation overflow",
2000 )?;
2001 let transfer_bytes = checked_mul(
2005 contract.input_grouping,
2006 aligned_allocation_bytes,
2007 "transfer allocation overflow",
2008 )?;
2009 let data_bytes = checked_add(
2010 checked_mul(
2011 configured_staging_areas(),
2012 checked_mul(
2013 contract.input_grouping,
2014 skewed_allocation_bytes,
2015 "staging allocation overflow",
2016 )?,
2017 "staging allocation overflow",
2018 )?,
2019 checked_add(
2020 checked_mul(
2021 output_count,
2022 skewed_allocation_bytes,
2023 "output allocation overflow",
2024 )?,
2025 checked_mul(
2026 configured_staging_areas(),
2027 transfer_bytes,
2028 "transfer allocation overflow",
2029 )?,
2030 "forward buffer allocation overflow",
2031 )?,
2032 "forward buffer allocation overflow",
2033 )?;
2034 if data_bytes <= chosen_stripe_limit
2035 || (chunk_len <= 2 && data_bytes <= stripe_memory_limit)
2036 {
2037 return Ok(Self {
2038 chunk_len: chunk_len.min(slice_size),
2039 aligned_chunk_len,
2040 row_stride,
2041 staging_bytes,
2042 output_bytes,
2043 transfer_bytes,
2044 data_bytes,
2045 memory_bytes: reserved_bytes + data_bytes,
2046 jit_build_limit_bytes,
2047 });
2048 }
2049 if chunk_len <= 2 {
2050 return Err(resource_limit(format!(
2051 "forward persistent allocations and stripe buffers need {} bytes, limit is {memory_limit}",
2052 reserved_bytes + data_bytes
2053 )));
2054 }
2055 if slice_size < contract.stride {
2056 chunk_len = 2;
2057 continue;
2058 }
2059 let bytes_per_aligned_byte = data_bytes / aligned_chunk_len;
2060 let max_aligned_len =
2061 (chosen_stripe_limit / bytes_per_aligned_byte) / contract.stride * contract.stride;
2062 let smaller_chunk_len = chunk_len.saturating_sub(contract.stride).max(2);
2063 chunk_len = max_aligned_len.max(2).min(smaller_chunk_len);
2064 }
2065 }
2066}
2067
2068fn select_kernel_for_memory(
2069 slice_size: usize,
2070 output_count: usize,
2071 source_count: usize,
2072 memory_limit: usize,
2073 requested: ForwardKernel,
2074) -> Result<(ResolvedKernel, BufferPlan)> {
2075 select_kernel_for_memory_with_capabilities(
2076 slice_size,
2077 output_count,
2078 source_count,
2079 memory_limit,
2080 requested,
2081 runtime_kernel_capabilities(),
2082 )
2083}
2084
2085fn select_kernel_for_memory_with_capabilities(
2086 slice_size: usize,
2087 output_count: usize,
2088 source_count: usize,
2089 memory_limit: usize,
2090 requested: ForwardKernel,
2091 capabilities: KernelCapabilities,
2092) -> Result<(ResolvedKernel, BufferPlan)> {
2093 let requested = match configured_kernel_override()? {
2094 Some(forced) => forced,
2095 None => requested,
2096 };
2097 let candidates = match requested {
2098 ForwardKernel::Auto => auto_kernel_candidates(capabilities),
2099 requested => vec![resolve_kernel_with_capabilities(requested, capabilities)?],
2100 };
2101 let mut last_error = None;
2102 for kernel in candidates {
2103 let contract = KernelContract::for_kernel(kernel);
2104 let factor_bytes = factor_workspace_bytes(kernel, source_count)?;
2105 let (jit_bytes, jit_arena_bytes) = jit_workspace_bytes(kernel, output_count)?;
2109 match BufferPlan::new_with_reserved(
2110 slice_size,
2111 output_count,
2112 contract,
2113 memory_limit,
2114 factor_bytes,
2115 jit_bytes,
2116 jit_arena_bytes,
2117 ) {
2118 Ok(buffers) => return Ok((kernel, buffers)),
2119 Err(error) => last_error = Some(error),
2120 }
2121 }
2122 Err(last_error.unwrap_or_else(|| resource_limit("no forward arithmetic kernel is available")))
2123}
2124
2125fn auto_kernel_candidates(capabilities: KernelCapabilities) -> Vec<ResolvedKernel> {
2126 let mut kernels = Vec::with_capacity(4);
2127 let preferred = resolve_kernel_with_capabilities(ForwardKernel::Auto, capabilities)
2128 .expect("automatic forward kernel selection cannot fail");
2129 kernels.push(preferred);
2130 #[cfg(target_arch = "x86_64")]
2131 {
2132 if capabilities.avx2_jit && preferred != ResolvedKernel::XorJitAvx2 {
2133 kernels.push(ResolvedKernel::XorJitAvx2);
2134 }
2135 if capabilities.folded && preferred != ResolvedKernel::Folded {
2136 kernels.push(ResolvedKernel::Folded);
2137 }
2138 }
2139 let simd = resolve_kernel_with_capabilities(ForwardKernel::Simd, capabilities)
2140 .expect("direct grouped SIMD selection cannot fail");
2141 if preferred != simd {
2142 kernels.push(simd);
2143 }
2144 let portable = resolve_kernel_with_capabilities(ForwardKernel::Portable, capabilities)
2145 .expect("portable selection cannot fail");
2146 if preferred != portable {
2147 kernels.push(portable);
2148 }
2149 kernels
2150}
2151
2152struct FactorSource {
2153 constants: Vec<u16>,
2157}
2158
2159impl FactorSource {
2160 fn new(source_count: usize) -> Self {
2161 Self {
2162 constants: gf::input_slice_constants(source_count),
2163 }
2164 }
2165
2166 fn row_factors(&self, source_start: usize, live_inputs: usize) -> RowFactors {
2174 let mut logs = [0u16; MAX_INPUT_GROUPING];
2175 for (lane, log) in logs[..live_inputs].iter_mut().enumerate() {
2176 let constant = self.constants[source_start + lane];
2177 debug_assert_ne!(constant, 0, "input slice constants are never zero");
2178 *log = gf::log(constant);
2179 }
2180 RowFactors { logs, live_inputs }
2181 }
2182}
2183
2184struct RowFactors {
2186 logs: [u16; MAX_INPUT_GROUPING],
2187 live_inputs: usize,
2188}
2189
2190impl RowFactors {
2191 fn fill_row(&self, exponent: RecoveryExponent, row: &mut [u16; MAX_INPUT_GROUPING]) {
2192 row.fill(0);
2193 for (factor, &log) in row[..self.live_inputs]
2194 .iter_mut()
2195 .zip(self.logs[..self.live_inputs].iter())
2196 {
2197 *factor = gf::pow_from_log(log, exponent);
2198 }
2199 }
2200}
2201
2202pub(crate) fn forward_stripe_count(
2211 slice_size: u64,
2212 source_count: usize,
2213 exponents: &[RecoveryExponent],
2214 memory_limit: usize,
2215 requested_kernel: ForwardKernel,
2216 transform_policy: TransformPolicy,
2217) -> Result<usize> {
2218 let output_count = exponents.len();
2219 if output_count == 0 {
2220 return Ok(0);
2221 }
2222 let slice_size = usize::try_from(slice_size)
2223 .map_err(|_| resource_limit("slice size exceeds addressable memory"))?;
2224 let (_, buffers) = select_kernel_for_memory(
2225 slice_size,
2226 output_count,
2227 source_count,
2228 memory_limit,
2229 requested_kernel,
2230 )?;
2231 if let Some(arm) = transform::admit(
2235 slice_size,
2236 source_count,
2237 exponents,
2238 memory_limit.min(buffers.memory_bytes),
2239 transform_policy,
2240 ) {
2241 return Ok(arm.shape().passes);
2242 }
2243 Ok(slice_size.div_ceil(buffers.chunk_len))
2244}
2245
2246pub(crate) fn estimate_forward_memory(
2247 slice_size: u64,
2248 source_count: usize,
2249 output_count: usize,
2250 memory_limit: usize,
2251 requested_kernel: ForwardKernel,
2252) -> Result<ForwardMemoryEstimate> {
2253 if output_count == 0 {
2254 return Ok(ForwardMemoryEstimate {
2255 factor_workspace_bytes: 0,
2256 jit_workspace_bytes: 0,
2257 stripe_buffer_bytes: 0,
2258 processing_peak_bytes: 0,
2259 });
2260 }
2261 let slice_size = usize::try_from(slice_size)
2262 .map_err(|_| resource_limit("slice size exceeds addressable memory"))?;
2263 let (kernel, buffers) = select_kernel_for_memory(
2264 slice_size,
2265 output_count,
2266 source_count,
2267 memory_limit,
2268 requested_kernel,
2269 )?;
2270 let factor_workspace_bytes = factor_workspace_bytes(kernel, source_count)?;
2271 let (jit_workspace_bytes, _) = jit_workspace_bytes(kernel, output_count)?;
2272 Ok(ForwardMemoryEstimate {
2273 factor_workspace_bytes,
2274 jit_workspace_bytes,
2275 stripe_buffer_bytes: buffers.data_bytes,
2276 processing_peak_bytes: buffers.memory_bytes,
2277 })
2278}
2279
2280#[repr(align(64))]
2281#[derive(Clone, Copy)]
2282struct AlignedCell(pub [u8; 64]);
2283
2284impl AlignedCell {
2285 fn as_ptr(&self) -> *const u8 {
2286 self.0.as_ptr()
2287 }
2288
2289 fn as_mut_ptr(&mut self) -> *mut u8 {
2290 self.0.as_mut_ptr()
2291 }
2292}
2293
2294pub(super) struct AlignedBuffer {
2295 cells: Vec<AlignedCell>,
2296 len: usize,
2297}
2298
2299impl AlignedBuffer {
2300 pub(super) fn new(len: usize) -> Self {
2301 Self {
2302 cells: vec![AlignedCell([0; 64]); len.div_ceil(64)],
2303 len,
2304 }
2305 }
2306
2307 pub(super) fn as_bytes(&self) -> &[u8] {
2308 let ptr = self
2309 .cells
2310 .first()
2311 .map_or_else(|| self.cells.as_ptr().cast::<u8>(), AlignedCell::as_ptr);
2312 unsafe { std::slice::from_raw_parts(ptr, self.len) }
2313 }
2314
2315 pub(super) fn as_bytes_mut(&mut self) -> &mut [u8] {
2316 let ptr = if self.cells.is_empty() {
2317 self.cells.as_mut_ptr().cast::<u8>()
2318 } else {
2319 self.cells[0].as_mut_ptr()
2320 };
2321 unsafe { std::slice::from_raw_parts_mut(ptr, self.len) }
2322 }
2323}
2324
2325#[allow(clippy::too_many_arguments)]
2326fn fill_staging<P: ForwardSourceProvider + ?Sized>(
2327 kernel: ResolvedKernel,
2328 staging: &mut AlignedBuffer,
2329 transfer: &mut AlignedBuffer,
2330 provider: &mut P,
2331 source_start: usize,
2332 stripe_offset: usize,
2333 actual_len: usize,
2334 aligned_len: usize,
2335 contract: KernelContract,
2336 slice_lens: &mut [usize; MAX_INPUT_GROUPING],
2337) -> Result<()> {
2338 let staging_bytes = staging.as_bytes_mut();
2339 staging_bytes.fill(0);
2340 let slot_stride = transfer_slot_stride(aligned_len)?;
2347 let transfer_bytes = transfer.as_bytes_mut();
2348 if transfer_bytes.len() < contract.input_grouping.saturating_mul(slot_stride) {
2349 return Err(resource_limit(
2350 "transfer buffer is shorter than one input batch",
2351 ));
2352 }
2353 let lane_stride = lane_stride(contract, aligned_len);
2354 let layout = StagingLayout::new(contract, aligned_len, lane_stride);
2355 let layout_bytes = layout
2358 .total_bytes()
2359 .ok_or_else(|| resource_limit("staging lane offset overflow"))?;
2360 if staging_bytes.len() < layout_bytes {
2361 return Err(resource_limit(
2362 "staging buffer is shorter than the batch layout",
2363 ));
2364 }
2365 let source_count = provider.source_count();
2366 *slice_lens = [0; MAX_INPUT_GROUPING];
2367
2368 for (lane, slice_len) in slice_lens[..contract.input_grouping].iter_mut().enumerate() {
2369 let slot_start = lane * slot_stride;
2370 transfer_bytes[slot_start..slot_start + aligned_len].fill(0);
2371 let source_index = source_start + lane;
2372 if source_index < source_count {
2373 *slice_len = provider.read_source_chunk(
2374 source_index,
2375 stripe_offset,
2376 &mut transfer_bytes[slot_start..slot_start + actual_len],
2377 )?;
2378 }
2379
2380 match kernel {
2381 ResolvedKernel::Portable | ResolvedKernel::Simd => {
2382 if layout.interleave == 1 {
2383 let start = layout.group_base(lane);
2384 staging_bytes[start..start + aligned_len]
2385 .copy_from_slice(&transfer_bytes[slot_start..slot_start + aligned_len]);
2386 } else {
2387 const BLOCK: usize = gf_simd::INPUT_BATCH_BLOCK_BYTES;
2392 debug_assert_eq!(aligned_len % BLOCK, 0, "interleaved stripe must be blocked");
2393 let group = lane / layout.interleave;
2394 let step = layout.group_width(group) * BLOCK;
2395 let mut start = layout.group_base(group) + (lane % layout.interleave) * BLOCK;
2396 for block in
2397 transfer_bytes[slot_start..slot_start + aligned_len].chunks_exact(BLOCK)
2398 {
2399 staging_bytes[start..start + BLOCK].copy_from_slice(block);
2400 start += step;
2401 }
2402 }
2403 }
2404 #[cfg(target_arch = "x86_64")]
2405 ResolvedKernel::Folded => {
2406 let fold_group = lane / gf_simd::FOLDED_GROUP;
2407 let group_lane = lane % gf_simd::FOLDED_GROUP;
2408 let group_start = fold_group
2409 .checked_mul(gf_simd::FOLDED_GROUP)
2410 .and_then(|value| value.checked_mul(lane_stride))
2411 .ok_or_else(|| resource_limit("folded staging offset overflow"))?;
2412 gf_simd::split_encode_scatter(
2413 &transfer_bytes[slot_start..slot_start + aligned_len],
2414 &mut staging_bytes
2415 [group_start..group_start + aligned_len * gf_simd::FOLDED_GROUP],
2416 group_lane,
2417 );
2418 }
2419 #[cfg(target_arch = "x86_64")]
2420 ResolvedKernel::XorJitAvx2 => {
2421 let width = reedsolomon_rs::xor_jit::JitWidth::Avx2;
2422 let block = width.block_bytes();
2423 debug_assert_eq!(aligned_len % block, 0);
2424 debug_assert_eq!(lane_stride, aligned_len);
2426 let lane_start = lane
2427 .checked_mul(lane_stride)
2428 .ok_or_else(|| resource_limit("packed staging offset overflow"))?;
2429 for offset in (0..aligned_len).step_by(block) {
2430 unsafe {
2431 width.prepare_block(
2432 &transfer_bytes[slot_start + offset..slot_start + offset + block],
2433 &mut staging_bytes[lane_start + offset..lane_start + offset + block],
2434 );
2435 }
2436 }
2437 }
2438 }
2439 }
2440 Ok(())
2441}
2442
2443fn transfer_slot_stride(aligned_len: usize) -> Result<usize> {
2447 round_up(aligned_len, 64)
2448}
2449
2450fn observe_batch(
2453 observer: &mut dyn ForwardSourceObserver,
2454 bytes: &[u8],
2455 first_source_index: usize,
2456 live_inputs: usize,
2457 slot_stride: usize,
2458 slice_lens: &[usize; MAX_INPUT_GROUPING],
2459) -> Result<()> {
2460 let run_len = transfer_group_lanes().clamp(1, MAX_INPUT_GROUPING);
2461 let mut index = 0usize;
2462 while index < live_inputs {
2463 let run = run_len.min(live_inputs - index);
2464 let mut views: [&[u8]; MAX_INPUT_GROUPING] = [&[][..]; MAX_INPUT_GROUPING];
2465 for (slot, view) in views[..run].iter_mut().enumerate() {
2466 let start = (index + slot) * slot_stride;
2467 *view = &bytes[start..start + slice_lens[index + slot]];
2468 }
2469 observer.observe_slices(first_source_index + index, &views[..run])?;
2470 index += run;
2471 }
2472 Ok(())
2473}
2474
2475fn live_batch_inputs(source_count: usize, source_start: usize, contract: KernelContract) -> usize {
2477 source_count
2478 .saturating_sub(source_start)
2479 .min(contract.input_grouping)
2480}
2481
2482#[allow(clippy::too_many_arguments)]
2483fn accumulate_batch(
2484 kernel: ResolvedKernel,
2485 output: &mut [u8],
2486 staging: &AlignedBuffer,
2487 factors: &FactorSource,
2488 exponents: &[RecoveryExponent],
2489 source_start: usize,
2490 live_inputs: usize,
2491 aligned_len: usize,
2492 output_stride: usize,
2493 contract: KernelContract,
2494 band_size: usize,
2495 #[cfg(target_arch = "x86_64")]
2496 jit_workspaces: &mut [reedsolomon_rs::xor_jit::packed::PackedJitWorkspace],
2497 #[cfg(target_arch = "x86_64")] jit_code_budget: usize,
2498) -> Result<()> {
2499 let output_count = exponents.len();
2500 debug_assert_eq!(output.len(), output_count * output_stride);
2504 #[cfg(target_arch = "x86_64")]
2505 debug_assert_eq!(
2506 jit_workspaces.len(),
2507 output_count.max(1).div_ceil(band_size)
2508 );
2509 if band_size >= output_count || output_count <= 1 {
2510 return accumulate_band(
2511 kernel,
2512 output,
2513 staging,
2514 factors,
2515 exponents,
2516 source_start,
2517 live_inputs,
2518 aligned_len,
2519 output_stride,
2520 contract,
2521 #[cfg(target_arch = "x86_64")]
2522 &mut jit_workspaces[0],
2523 #[cfg(target_arch = "x86_64")]
2524 jit_code_budget,
2525 );
2526 }
2527
2528 let band_bytes = checked_mul(band_size, output_stride, "band byte range overflow")?;
2534 let bands = output
2535 .chunks_mut(band_bytes)
2536 .zip(exponents.chunks(band_size));
2537 #[cfg(target_arch = "x86_64")]
2538 let bands = bands.zip(jit_workspaces.iter_mut());
2539 for band in bands {
2540 #[cfg(target_arch = "x86_64")]
2541 let ((band_output, band_exponents), jit_workspace) = band;
2542 #[cfg(not(target_arch = "x86_64"))]
2543 let (band_output, band_exponents) = band;
2544 accumulate_band(
2545 kernel,
2546 band_output,
2547 staging,
2548 factors,
2549 band_exponents,
2550 source_start,
2551 live_inputs,
2552 aligned_len,
2553 output_stride,
2554 contract,
2555 #[cfg(target_arch = "x86_64")]
2556 jit_workspace,
2557 #[cfg(target_arch = "x86_64")]
2558 jit_code_budget,
2559 )?;
2560 }
2561 Ok(())
2562}
2563
2564#[allow(clippy::too_many_arguments)]
2565fn accumulate_band(
2566 kernel: ResolvedKernel,
2567 output: &mut [u8],
2568 staging: &AlignedBuffer,
2569 factors: &FactorSource,
2570 exponents: &[RecoveryExponent],
2571 source_start: usize,
2572 live_inputs: usize,
2573 aligned_len: usize,
2574 output_stride: usize,
2575 contract: KernelContract,
2576 #[cfg(target_arch = "x86_64")]
2577 jit_workspace: &mut reedsolomon_rs::xor_jit::packed::PackedJitWorkspace,
2578 #[cfg(target_arch = "x86_64")] jit_code_budget: usize,
2579) -> Result<()> {
2580 let staging_bytes = staging.as_bytes();
2581 if live_inputs == 0 {
2584 return Ok(());
2585 }
2586 let lane_stride = lane_stride(contract, aligned_len);
2587 let layout = StagingLayout::new(contract, aligned_len, lane_stride);
2588 let mut row = [0u16; MAX_INPUT_GROUPING];
2589 match kernel {
2590 ResolvedKernel::Portable => {
2591 let row_factors = factors.row_factors(source_start, live_inputs);
2592 let mut rows = [[0u16; MAX_INPUT_GROUPING]; COEFF_ROWS];
2593 for (chunk_index, chunk) in exponents.chunks(COEFF_ROWS).enumerate() {
2594 for (slot, &exponent) in rows.iter_mut().zip(chunk) {
2595 row_factors.fill_row(exponent, slot);
2596 }
2597 let first_output = chunk_index * COEFF_ROWS;
2598 for (tile_start, tile_len) in stripe_tiles(aligned_len, contract.tile_bytes) {
2599 for (offset, row) in rows[..chunk.len()].iter().enumerate() {
2600 let dst_start = (first_output + offset) * output_stride + tile_start;
2601 scalar_accumulate(
2602 &mut output[dst_start..dst_start + tile_len],
2603 &staging_bytes[tile_start..],
2604 lane_stride,
2605 row,
2606 live_inputs,
2607 tile_len,
2608 );
2609 }
2610 }
2611 }
2612 }
2613 ResolvedKernel::Simd => {
2614 let mut prepared: Vec<gf_simd::PreparedInputFactor> =
2617 Vec::with_capacity(COEFF_ROWS * live_inputs);
2618 let row_factors = factors.row_factors(source_start, live_inputs);
2619 for (chunk_index, chunk) in exponents.chunks(COEFF_ROWS).enumerate() {
2620 prepared.clear();
2621 for &exponent in chunk {
2622 row_factors.fill_row(exponent, &mut row);
2623 prepared.extend(
2624 row[..live_inputs]
2625 .iter()
2626 .map(|&factor| gf_simd::prepare_input_factor(factor)),
2627 );
2628 }
2629 let first_output = chunk_index * COEFF_ROWS;
2630 for (tile_start, tile_len) in stripe_tiles(aligned_len, contract.tile_bytes) {
2631 for offset in 0..chunk.len() {
2632 let dst_start = (first_output + offset) * output_stride + tile_start;
2633 let row_base = offset * live_inputs;
2634 if layout.interleave == 1 {
2635 let inputs: [PreparedFactorSrc<'_>; MAX_INPUT_GROUPING] =
2642 std::array::from_fn(|lane| {
2643 let clamped = lane.min(live_inputs - 1);
2644 let source_start_bytes =
2645 layout.group_base(clamped) + tile_start;
2646 PreparedFactorSrc {
2647 prepared: &prepared[row_base + clamped],
2648 src: &staging_bytes
2649 [source_start_bytes..source_start_bytes + tile_len],
2650 }
2651 });
2652 gf_simd::mul_acc_input_batch_prepared(
2653 &mut output[dst_start..dst_start + tile_len],
2654 &inputs[..live_inputs],
2655 );
2656 continue;
2657 }
2658 for group in 0..layout.group_count() {
2663 let first_lane = group * layout.interleave;
2664 if first_lane >= live_inputs {
2665 break;
2666 }
2667 let width = layout.group_width(group);
2668 let live_in_group = (live_inputs - first_lane).min(width);
2669 let (stream_start, stream_end) =
2670 layout.group_tile(group, tile_start, tile_len);
2671 gf_simd::mul_acc_input_batch_prepared_interleaved(
2672 &mut output[dst_start..dst_start + tile_len],
2673 &prepared
2674 [row_base + first_lane..row_base + first_lane + live_in_group],
2675 &staging_bytes[stream_start..stream_end],
2676 width,
2677 );
2678 }
2679 }
2680 }
2681 }
2682 }
2683 #[cfg(target_arch = "x86_64")]
2684 ResolvedKernel::Folded => {
2685 debug_assert_eq!(contract.input_grouping, DEFAULT_INPUT_GROUPING);
2686 let groups = contract.input_grouping / gf_simd::FOLDED_GROUP;
2687 if groups > MAX_FOLDED_GROUPS {
2688 return Err(invalid_input(
2689 "folded input grouping exceeds the reserved group count",
2690 ));
2691 }
2692 let mut affine = Vec::with_capacity(COEFF_ROWS * live_inputs);
2693 let mut shuffle2x = Vec::with_capacity(COEFF_ROWS * live_inputs);
2694 let uses_gfni = gf_simd::folded_uses_gfni();
2696 let row_factors = factors.row_factors(source_start, live_inputs);
2697 let mut staging_views: Vec<&[u8]> = Vec::with_capacity(groups);
2700 for (chunk_index, chunk) in exponents.chunks(COEFF_ROWS).enumerate() {
2701 affine.clear();
2702 shuffle2x.clear();
2703 for &exponent in chunk {
2704 row_factors.fill_row(exponent, &mut row);
2705 if uses_gfni {
2706 affine.extend(
2707 row[..live_inputs]
2708 .iter()
2709 .map(|&factor| gf_simd::precompute_affine_matrices(factor)),
2710 );
2711 } else {
2712 shuffle2x.extend(
2713 row[..live_inputs]
2714 .iter()
2715 .map(|&factor| gf_simd::precompute_shuffle2x_tables(factor)),
2716 );
2717 }
2718 }
2719 let first_output = chunk_index * COEFF_ROWS;
2720 for (tile_start, tile_len) in stripe_tiles(aligned_len, contract.tile_bytes) {
2721 staging_views.clear();
2722 staging_views.extend((0..groups).map(|group| {
2723 let start = group * gf_simd::FOLDED_GROUP * lane_stride
2728 + tile_start * gf_simd::FOLDED_GROUP;
2729 &staging_bytes[start..start + gf_simd::FOLDED_GROUP * tile_len]
2730 }));
2731 for offset in 0..chunk.len() {
2732 let dst_start = (first_output + offset) * output_stride + tile_start;
2733 let row_base = offset * live_inputs;
2734 if uses_gfni {
2735 let matrix_sets: [[&gf_simd::AffineMulMatrices; gf_simd::FOLDED_GROUP];
2740 MAX_FOLDED_GROUPS] = std::array::from_fn(|group| {
2741 std::array::from_fn(|lane| {
2742 let source_index = group * gf_simd::FOLDED_GROUP + lane;
2743 affine
2744 .get(row_base + source_index)
2745 .filter(|_| source_index < live_inputs)
2746 .unwrap_or(&gf_simd::ZERO_AFFINE)
2747 })
2748 });
2749 gf_simd::mul_acc_folded_batch(
2750 &mut output[dst_start..dst_start + tile_len],
2751 &staging_views,
2752 &matrix_sets[..groups],
2753 );
2754 } else {
2755 let table_sets: [[&gf_simd::Shuffle2xTables; gf_simd::FOLDED_GROUP];
2756 MAX_FOLDED_GROUPS] = std::array::from_fn(|group| {
2757 std::array::from_fn(|lane| {
2758 let source_index = group * gf_simd::FOLDED_GROUP + lane;
2759 shuffle2x
2760 .get(row_base + source_index)
2761 .filter(|_| source_index < live_inputs)
2762 .unwrap_or(&gf_simd::ZERO_SHUFFLE2X)
2763 })
2764 });
2765 gf_simd::mul_acc_shuffle2x_batch(
2766 &mut output[dst_start..dst_start + tile_len],
2767 &staging_views,
2768 &table_sets[..groups],
2769 );
2770 }
2771 }
2772 }
2773 }
2774 }
2775 #[cfg(target_arch = "x86_64")]
2776 ResolvedKernel::XorJitAvx2 => {
2777 debug_assert_eq!(contract.tile_bytes, UNTILED);
2792 debug_assert_eq!(lane_stride, aligned_len);
2793 debug_assert_eq!(contract.input_grouping, DEFAULT_INPUT_GROUPING);
2794 let width = reedsolomon_rs::xor_jit::JitWidth::Avx2;
2795 let row_factors = factors.row_factors(source_start, live_inputs);
2796 let rows: Vec<[u16; DEFAULT_INPUT_GROUPING]> = exponents
2797 .iter()
2798 .map(|&exponent| {
2799 let mut wide = [0u16; MAX_INPUT_GROUPING];
2804 row_factors.fill_row(exponent, &mut wide);
2805 let mut row = [0u16; DEFAULT_INPUT_GROUPING];
2806 row.copy_from_slice(&wide[..DEFAULT_INPUT_GROUPING]);
2807 row
2808 })
2809 .collect();
2810 let row_refs: Vec<&[u16]> = rows.iter().map(|row| &row[..]).collect();
2811 let batch = jit_workspace
2812 .build(width, &row_refs, jit_code_budget.max(1))
2813 .map_err(|error| jit_build_error(error.to_string()))?;
2814 for output_index in 0..exponents.len() {
2815 let dst_start = output_index * output_stride;
2816 let code = batch
2817 .row(output_index)
2818 .ok_or_else(|| invalid_input("packed XOR-JIT output row missing"))?;
2819 unsafe {
2820 width
2821 .try_run_packed(
2822 code,
2823 &mut reedsolomon_rs::xor_jit::packed::PackedScratch::default(),
2824 reedsolomon_rs::xor_jit::packed::PackedRun {
2825 packed_regions: contract.input_grouping,
2826 live_regions: live_inputs,
2827 dst: output[dst_start..dst_start + aligned_len].as_mut_ptr(),
2828 src: staging_bytes.as_ptr(),
2829 len: aligned_len,
2830 prefetch_in: Some(staging_bytes.as_ptr()),
2831 prefetch_out: None,
2832 },
2833 )
2834 .map_err(|error| jit_build_error(error.to_string()))?;
2835 }
2836 }
2837 jit_workspace
2838 .recycle(batch)
2839 .map_err(|error| jit_build_error(error.to_string()))?;
2840 }
2841 }
2842 Ok(())
2843}
2844
2845fn scalar_accumulate(
2851 dst: &mut [u8],
2852 staging: &[u8],
2853 staging_stride: usize,
2854 row: &[u16],
2855 live_inputs: usize,
2856 len: usize,
2857) {
2858 for word in 0..len / 2 {
2859 let mut value = u16::from_le_bytes([dst[word * 2], dst[word * 2 + 1]]);
2860 for (lane, &factor) in row.iter().take(live_inputs).enumerate() {
2861 let source_offset = lane * staging_stride + word * 2;
2862 let source = u16::from_le_bytes([staging[source_offset], staging[source_offset + 1]]);
2863 value ^= gf::mul(source, factor);
2864 }
2865 dst[word * 2..word * 2 + 2].copy_from_slice(&value.to_le_bytes());
2866 }
2867}
2868
2869fn finish_output(
2870 kernel: ResolvedKernel,
2871 output: &mut [u8],
2872 output_stride: usize,
2873 aligned_len: usize,
2874 output_count: usize,
2875) -> Result<()> {
2876 debug_assert_eq!(output.len(), output_count * output_stride);
2877 finish_band_rows(kernel, output, output_stride, aligned_len, output_count)
2878}
2879
2880fn finish_band_rows(
2886 kernel: ResolvedKernel,
2887 output: &mut [u8],
2888 output_stride: usize,
2889 aligned_len: usize,
2890 output_count: usize,
2891) -> Result<()> {
2892 #[cfg(not(target_arch = "x86_64"))]
2893 {
2894 let _ = (kernel, output, output_stride, aligned_len, output_count);
2895 }
2896
2897 #[cfg(target_arch = "x86_64")]
2898 {
2899 if matches!(kernel, ResolvedKernel::Portable | ResolvedKernel::Simd) {
2900 return Ok(());
2901 }
2902 return finish_band(kernel, output, output_stride, aligned_len, output_count);
2903 }
2904 #[allow(unreachable_code)]
2905 Ok(())
2906}
2907
2908#[cfg(target_arch = "x86_64")]
2909fn finish_band(
2910 kernel: ResolvedKernel,
2911 output: &mut [u8],
2912 output_stride: usize,
2913 aligned_len: usize,
2914 output_count: usize,
2915) -> Result<()> {
2916 for output_index in 0..output_count {
2917 let start = output_index
2918 .checked_mul(output_stride)
2919 .ok_or_else(|| resource_limit("output finish offset overflow"))?;
2920 let end = start
2921 .checked_add(aligned_len)
2922 .ok_or_else(|| resource_limit("output finish end overflow"))?;
2923 let dst = &mut output[start..end];
2924
2925 match kernel {
2926 ResolvedKernel::Portable | ResolvedKernel::Simd => {}
2927 ResolvedKernel::Folded => {
2928 gf_simd::altmap_decode(dst);
2929 }
2930 ResolvedKernel::XorJitAvx2 => {
2931 let width = reedsolomon_rs::xor_jit::JitWidth::Avx2;
2932 let block = width.block_bytes();
2933 for offset in (0..aligned_len).step_by(block) {
2934 unsafe { width.finish_block(&mut dst[offset..offset + block]) };
2935 }
2936 }
2937 }
2938 }
2939 Ok(())
2940}
2941
2942fn validate_provider<P: ForwardSourceProvider + ?Sized>(
2943 provider: &P,
2944 slice_size: usize,
2945) -> Result<()> {
2946 let source_count = provider.source_count();
2947 if source_count > MAX_TOTAL_INPUT_SLICES {
2948 return Err(resource_limit(format!(
2949 "input slice count {} exceeds {MAX_TOTAL_INPUT_SLICES}",
2950 source_count
2951 )));
2952 }
2953 for source_index in 0..source_count {
2954 if provider.source_slice_len(source_index)? > slice_size {
2955 return Err(invalid_input(
2956 "an input slice is longer than the configured slice size",
2957 ));
2958 }
2959 }
2960 Ok(())
2961}
2962
2963fn check_cancel(options: &ForwardEncoderOptions) -> Result<()> {
2964 if options
2965 .cancel
2966 .as_ref()
2967 .is_some_and(CancellationToken::is_cancelled)
2968 {
2969 Err(Par2Error::Cancelled)
2970 } else {
2971 Ok(())
2972 }
2973}
2974
2975fn report_progress(
2976 options: &ForwardEncoderOptions,
2977 current: u32,
2978 total: u32,
2979 bytes_processed: u64,
2980 total_bytes: u64,
2981) {
2982 if let Some(progress) = &options.progress {
2983 progress(ProgressUpdate {
2984 stage: ProgressStage::Creating,
2985 current,
2986 total,
2987 bytes_processed,
2988 total_bytes: Some(total_bytes),
2989 phase: ProgressPhase::RecoveryEncode,
2990 });
2991 }
2992}
2993
2994#[cfg(test)]
2995struct VecRecoverySink {
2996 blocks: Vec<ForwardRecoveryBlock>,
2997 slice_size: usize,
2998}
2999
3000#[cfg(test)]
3001impl VecRecoverySink {
3002 fn new(exponents: &[RecoveryExponent], slice_size: usize) -> Self {
3003 Self {
3004 blocks: exponents
3005 .iter()
3006 .map(|&exponent| ForwardRecoveryBlock {
3007 exponent,
3008 data: vec![0; slice_size],
3009 })
3010 .collect(),
3011 slice_size,
3012 }
3013 }
3014}
3015
3016#[cfg(test)]
3017impl ForwardRecoverySink for VecRecoverySink {
3018 fn write_recovery_chunk(
3019 &mut self,
3020 output_index: usize,
3021 exponent: RecoveryExponent,
3022 offset: u64,
3023 data: &[u8],
3024 ) -> Result<()> {
3025 let block = self
3026 .blocks
3027 .get_mut(output_index)
3028 .ok_or_else(|| invalid_input("recovery output index is out of order"))?;
3029 if block.exponent != exponent {
3030 return Err(invalid_input("recovery exponent changed during encoding"));
3031 }
3032 let start =
3033 usize::try_from(offset).map_err(|_| resource_limit("stripe offset overflow"))?;
3034 let end = start
3035 .checked_add(data.len())
3036 .ok_or_else(|| resource_limit("recovery chunk end overflow"))?;
3037 if end > self.slice_size {
3038 return Err(invalid_input(
3039 "recovery chunk exceeds the configured slice size",
3040 ));
3041 }
3042 block.data[start..end].copy_from_slice(data);
3043 Ok(())
3044 }
3045}
3046
3047fn round_up(value: usize, alignment: usize) -> Result<usize> {
3048 if alignment == 0 {
3049 return Err(invalid_input("zero alignment"));
3050 }
3051 value
3052 .checked_add(alignment - 1)
3053 .map(|value| value / alignment * alignment)
3054 .ok_or_else(|| resource_limit("aligned length overflow"))
3055}
3056
3057fn checked_mul(left: usize, right: usize, reason: &'static str) -> Result<usize> {
3058 left.checked_mul(right)
3059 .ok_or_else(|| resource_limit(reason))
3060}
3061
3062fn checked_add(left: usize, right: usize, reason: &'static str) -> Result<usize> {
3063 left.checked_add(right)
3064 .ok_or_else(|| resource_limit(reason))
3065}
3066
3067fn invalid_input(reason: impl Into<String>) -> Par2Error {
3068 Par2Error::ReedSolomonError {
3069 reason: reason.into(),
3070 }
3071}
3072
3073fn resource_limit(reason: impl Into<String>) -> Par2Error {
3074 Par2Error::ResourceLimitExceeded {
3075 reason: reason.into(),
3076 }
3077}
3078
3079#[cfg(target_arch = "x86_64")]
3080fn unavailable_kernel(name: &'static str) -> Par2Error {
3081 Par2Error::ReedSolomonError {
3082 reason: format!("forward arithmetic kernel unavailable: {name}"),
3083 }
3084}
3085
3086#[cfg(target_arch = "x86_64")]
3087fn jit_build_error(reason: String) -> Par2Error {
3088 Par2Error::ReedSolomonError {
3089 reason: format!("forward packed arithmetic dispatch failed: {reason}"),
3090 }
3091}
3092
3093#[cfg(test)]
3094mod tests {
3095 use super::*;
3096
3097 fn test_sources() -> Vec<Vec<u8>> {
3098 (0..19usize)
3099 .map(|source| {
3100 (0..(73 + source * 11).min(256))
3101 .map(|index| (index.wrapping_mul(17) ^ (source * 29)) as u8)
3102 .collect()
3103 })
3104 .collect()
3105 }
3106
3107 fn encode_with_kernel(
3108 sources: &[Vec<u8>],
3109 kernel: ForwardKernel,
3110 ) -> Result<Vec<ForwardRecoveryBlock>> {
3111 let refs = sources.iter().map(Vec::as_slice).collect::<Vec<_>>();
3112 let encoder = ForwardEncoder::new(256, vec![0, 1, 2, 7, 31])?;
3113 encoder.encode(
3114 &refs,
3115 &ForwardEncoderOptions {
3116 memory_limit: Some(4 * 1024 * 1024),
3117 kernel,
3118 ..ForwardEncoderOptions::default()
3119 },
3120 )
3121 }
3122
3123 #[test]
3124 fn portable_output_matches_every_available_cpu_path() {
3125 let sources = test_sources();
3126 let portable = encode_with_kernel(&sources, ForwardKernel::Portable).unwrap();
3127 for kernel in ForwardEncoder::available_kernels() {
3128 let actual = encode_with_kernel(&sources, kernel).unwrap();
3129 assert_eq!(actual, portable, "kernel {kernel:?} differs from portable");
3130 }
3131 }
3132
3133 #[test]
3134 fn automatic_selection_matches_its_explicit_kernel() {
3135 let sources = test_sources();
3136 let auto = encode_with_kernel(&sources, ForwardKernel::Auto).unwrap();
3137 let encoder = ForwardEncoder::new(256, vec![0, 1, 2, 7, 31]).unwrap();
3138 let selected = encoder.selected_kernel(ForwardKernel::Auto).unwrap();
3139 let explicit = encode_with_kernel(&sources, selected).unwrap();
3140 assert_eq!(auto, explicit, "automatic kernel {selected:?} differs");
3141 }
3142
3143 #[test]
3145 fn kernel_override_values_parse_and_reject() {
3146 assert!(matches!(
3147 parse_kernel_override("auto"),
3148 Ok(ForwardKernel::Auto)
3149 ));
3150 assert!(matches!(
3151 parse_kernel_override(" Portable "),
3152 Ok(ForwardKernel::Portable)
3153 ));
3154 assert!(matches!(
3155 parse_kernel_override("SIMD"),
3156 Ok(ForwardKernel::Simd)
3157 ));
3158 #[cfg(target_arch = "x86_64")]
3159 {
3160 assert!(matches!(
3161 parse_kernel_override("folded"),
3162 Ok(ForwardKernel::Folded)
3163 ));
3164 assert!(matches!(
3165 parse_kernel_override("xor-jit-avx2"),
3166 Ok(ForwardKernel::XorJitAvx2)
3167 ));
3168 assert!(parse_kernel_override("xor-jit-avx512").is_err());
3171 }
3172 assert!(parse_kernel_override("fast").is_err());
3173 assert!(parse_kernel_override("").is_err());
3174 }
3175
3176 #[test]
3186 fn the_stripe_feed_reclaims_an_area_only_after_every_band_is_done() {
3187 const BATCHES: usize = 37;
3188 for band_count in [1usize, 2, 5] {
3189 let depth = configured_staging_areas();
3190 let feed = StripeFeed::new(band_count, depth);
3191 let feed = &feed;
3192 let mut areas: Vec<std::sync::Arc<AlignedBuffer>> = (0..depth)
3193 .map(|_| std::sync::Arc::new(AlignedBuffer::new(64)))
3194 .collect();
3195 let mut slots: Vec<std::sync::Arc<TransferSlot>> = (0..depth)
3196 .map(|_| std::sync::Arc::new(TransferSlot::new(64)))
3197 .collect();
3198 let hashed = std::sync::Mutex::new(Vec::<usize>::with_capacity(BATCHES));
3201 let hashed = &hashed;
3202 let faults = std::sync::Mutex::new(Vec::<String>::new());
3208 let faults = &faults;
3209 std::thread::scope(|scope| {
3210 for band_index in 0..band_count {
3211 scope.spawn(move || {
3212 let note = |fault: String| faults.lock().expect("uncontended").push(fault);
3213 for batch in 0..BATCHES {
3214 let Some(ticket) = feed.acquire(batch) else {
3215 note(format!("batch {batch}: no failure is injected"));
3216 return;
3217 };
3218 let stamp = (batch % 251) as u8;
3219 if ticket.source_start != batch * 7 {
3220 note(format!("batch {batch}: batch order"));
3221 }
3222 if ticket.staging.as_bytes()[0] != stamp {
3223 note(format!(
3224 "batch {batch}: area was refilled while a band still held it"
3225 ));
3226 }
3227 if batch % band_count == band_index {
3228 if !feed.wait_for_hash_turn(batch) {
3229 note(format!("batch {batch}: the hashing turn never came"));
3230 return;
3231 }
3232 if ticket.transfer.buffer.as_bytes()[0] != stamp {
3233 note(format!(
3234 "batch {batch}: transfer slot was refilled while a band still held it"
3235 ));
3236 }
3237 hashed.lock().expect("uncontended").push(batch);
3238 feed.finish_hash_turn(batch);
3239 }
3240 drop(ticket);
3241 feed.release(batch);
3242 }
3243 });
3244 }
3245 for batch in 0..BATCHES {
3246 assert!(feed.wait_for_area(batch));
3247 let area = batch % depth;
3248 let buffer = std::sync::Arc::get_mut(&mut areas[area])
3249 .expect("every band released the area before it was reclaimed");
3250 buffer.as_bytes_mut()[0] = (batch % 251) as u8;
3251 let slot = std::sync::Arc::get_mut(&mut slots[area])
3252 .expect("every band released the transfer slot before it was reclaimed");
3253 slot.buffer.as_bytes_mut()[0] = (batch % 251) as u8;
3254 feed.publish(
3255 batch,
3256 BatchTicket {
3257 staging: std::sync::Arc::clone(&areas[area]),
3258 transfer: std::sync::Arc::clone(&slots[area]),
3259 source_start: batch * 7,
3260 live_inputs: 1,
3261 },
3262 );
3263 }
3264 });
3265 assert!(
3266 faults.lock().expect("no band panicked").is_empty(),
3267 "{:?}",
3268 faults.lock().expect("no band panicked")
3269 );
3270 assert_eq!(
3271 hashed.lock().expect("no band panicked").as_slice(),
3272 (0..BATCHES).collect::<Vec<_>>(),
3273 "the hashing turn must reach the observer once per batch, in index order"
3274 );
3275 }
3276 }
3277
3278 #[test]
3283 fn a_failed_pass_releases_both_sides_of_the_feed() {
3284 let feed = StripeFeed::new(2, configured_staging_areas());
3285 feed.fail();
3286 assert!(feed.acquire(0).is_none(), "a band must stop on failure");
3287 assert!(
3288 !feed.wait_for_hash_turn(7),
3289 "a band owing a hashing turn must stop on failure"
3290 );
3291 assert!(
3292 !feed.wait_for_area(configured_staging_areas()),
3293 "the producer must stop on failure"
3294 );
3295 }
3296
3297 #[test]
3301 fn banded_accumulation_matches_sequential() {
3302 let sources = test_sources();
3303 let refs = sources.iter().map(Vec::as_slice).collect::<Vec<_>>();
3304 let exponents: Vec<RecoveryExponent> = vec![0, 1, 2, 7, 31, 64, 100];
3305 for requested in ForwardEncoder::available_kernels() {
3306 let resolved =
3307 resolve_kernel_with_capabilities(requested, runtime_kernel_capabilities())
3308 .expect("advertised kernels resolve");
3309 let contract = KernelContract::for_kernel(resolved);
3310 let aligned_len = round_up(256, contract.stride).unwrap();
3311
3312 let mut passes = Vec::new();
3313 for band_size in [7usize, 3] {
3316 let mut provider = InMemorySourceProvider { sources: &refs };
3317 let mut staging = AlignedBuffer::new(
3318 contract.input_grouping * lane_stride(contract, aligned_len),
3319 );
3320 let mut transfer = AlignedBuffer::new(
3321 contract.input_grouping * transfer_slot_stride(aligned_len).unwrap(),
3322 );
3323 fill_staging(
3324 resolved,
3325 &mut staging,
3326 &mut transfer,
3327 &mut provider,
3328 0,
3329 0,
3330 256,
3331 aligned_len,
3332 contract,
3333 &mut [0usize; MAX_INPUT_GROUPING],
3334 )
3335 .unwrap();
3336 let factors = FactorSource::new(refs.len());
3337 let mut output = AlignedBuffer::new(exponents.len() * aligned_len);
3338 #[cfg(target_arch = "x86_64")]
3339 let mut jit_workspaces: Vec<
3340 reedsolomon_rs::xor_jit::packed::PackedJitWorkspace,
3341 > = (0..exponents.len().div_ceil(band_size))
3342 .map(|_| Default::default())
3343 .collect();
3344 accumulate_batch(
3345 resolved,
3346 output.as_bytes_mut(),
3347 &staging,
3348 &factors,
3349 &exponents,
3350 0,
3351 contract.input_grouping.min(refs.len()),
3352 aligned_len,
3353 aligned_len,
3354 contract,
3355 band_size,
3356 #[cfg(target_arch = "x86_64")]
3357 &mut jit_workspaces,
3358 #[cfg(target_arch = "x86_64")]
3359 usize::MAX,
3360 )
3361 .unwrap();
3362 finish_output(
3363 resolved,
3364 output.as_bytes_mut(),
3365 aligned_len,
3366 aligned_len,
3367 exponents.len(),
3368 )
3369 .unwrap();
3370 passes.push(output.as_bytes().to_vec());
3371 }
3372 assert_eq!(
3373 passes[0], passes[1],
3374 "kernel {requested:?} banded output differs from sequential"
3375 );
3376 }
3377 }
3378
3379 #[test]
3382 fn stripe_tiles_cover_the_stripe_exactly() {
3383 for (aligned_len, tile) in [
3384 (4096usize, 4096usize),
3385 (4096, 8192),
3386 (4096, UNTILED),
3387 (10 * 1024, 4096),
3388 (32, 4096),
3389 (0, 4096),
3390 ] {
3391 let ranges: Vec<(usize, usize)> = stripe_tiles(aligned_len, tile).collect();
3392 let mut next = 0usize;
3393 for (start, len) in &ranges {
3394 assert_eq!(*start, next, "tiles are contiguous");
3395 assert!(*len > 0 && *len <= tile.min(aligned_len).max(1));
3396 next += len;
3397 }
3398 assert_eq!(next, aligned_len, "tiles cover the stripe");
3399 if aligned_len > 0 {
3400 for (_, len) in &ranges[..ranges.len() - 1] {
3402 assert_eq!(*len, tile.min(aligned_len));
3403 }
3404 }
3405 }
3406 }
3407
3408 #[test]
3412 fn stripe_tiling_matches_untiled_accumulation() {
3413 const SLICE: usize = 40 * 1024;
3414 let sources: Vec<Vec<u8>> = (0..14usize)
3415 .map(|source| {
3416 (0..SLICE)
3417 .map(|index| (index.wrapping_mul(31) ^ (source * 131)) as u8)
3418 .collect()
3419 })
3420 .collect();
3421 let refs = sources.iter().map(Vec::as_slice).collect::<Vec<_>>();
3422 let exponents: Vec<RecoveryExponent> = vec![0, 1, 2, 7, 31];
3423 for requested in ForwardEncoder::available_kernels() {
3424 let resolved =
3425 resolve_kernel_with_capabilities(requested, runtime_kernel_capabilities())
3426 .expect("advertised kernels resolve");
3427 let base = KernelContract::for_kernel(resolved);
3428 if base.tile_bytes == UNTILED {
3429 continue;
3430 }
3431 let aligned_len = round_up(SLICE, base.stride).unwrap();
3432 let mut passes = Vec::new();
3433 for tile_bytes in [UNTILED, 8192, 4096, 96, base.stride] {
3434 let contract = KernelContract { tile_bytes, ..base };
3435 let mut provider = InMemorySourceProvider { sources: &refs };
3436 let mut staging = AlignedBuffer::new(
3437 contract.input_grouping * lane_stride(contract, aligned_len),
3438 );
3439 let mut transfer = AlignedBuffer::new(
3440 contract.input_grouping * transfer_slot_stride(aligned_len).unwrap(),
3441 );
3442 fill_staging(
3443 resolved,
3444 &mut staging,
3445 &mut transfer,
3446 &mut provider,
3447 0,
3448 0,
3449 SLICE,
3450 aligned_len,
3451 contract,
3452 &mut [0usize; MAX_INPUT_GROUPING],
3453 )
3454 .unwrap();
3455 let factors = FactorSource::new(refs.len());
3456 let mut output = AlignedBuffer::new(exponents.len() * aligned_len);
3457 #[cfg(target_arch = "x86_64")]
3458 let mut jit_workspaces: Vec<
3459 reedsolomon_rs::xor_jit::packed::PackedJitWorkspace,
3460 > = vec![Default::default()];
3461 accumulate_batch(
3462 resolved,
3463 output.as_bytes_mut(),
3464 &staging,
3465 &factors,
3466 &exponents,
3467 0,
3468 contract.input_grouping.min(refs.len()),
3469 aligned_len,
3470 aligned_len,
3471 contract,
3472 exponents.len(),
3473 #[cfg(target_arch = "x86_64")]
3474 &mut jit_workspaces,
3475 #[cfg(target_arch = "x86_64")]
3476 usize::MAX,
3477 )
3478 .unwrap();
3479 finish_output(
3480 resolved,
3481 output.as_bytes_mut(),
3482 aligned_len,
3483 aligned_len,
3484 exponents.len(),
3485 )
3486 .unwrap();
3487 passes.push(output.as_bytes().to_vec());
3488 }
3489 for (index, pass) in passes.iter().enumerate().skip(1) {
3490 assert_eq!(
3491 *pass, passes[0],
3492 "kernel {requested:?} tiling pass {index} differs from the untiled pass"
3493 );
3494 }
3495 }
3496 }
3497
3498 #[test]
3510 fn the_feed_is_stripe_major_once_a_slice_needs_more_than_one_stripe() {
3511 struct Recorder<'a> {
3512 sources: &'a [&'a [u8]],
3513 reads: Vec<(usize, usize)>,
3514 }
3515 impl ForwardSourceProvider for Recorder<'_> {
3516 fn source_count(&self) -> usize {
3517 self.sources.len()
3518 }
3519 fn source_slice_len(&self, source_index: usize) -> Result<usize> {
3520 Ok(self.sources[source_index].len())
3521 }
3522 fn read_source_chunk(
3523 &mut self,
3524 source_index: usize,
3525 offset: usize,
3526 destination: &mut [u8],
3527 ) -> Result<usize> {
3528 if source_index < self.sources.len() {
3529 self.reads.push((source_index, offset));
3530 }
3531 let source = self.sources[source_index];
3532 let start = offset.min(source.len());
3533 let take = destination.len().min(source.len() - start);
3534 destination[..take].copy_from_slice(&source[start..start + take]);
3535 Ok(take)
3536 }
3537 }
3538
3539 const SLICE: usize = 4096;
3540 let sources: Vec<Vec<u8>> = (0..3usize).map(|s| vec![s as u8 + 1; SLICE]).collect();
3541 let refs = sources.iter().map(Vec::as_slice).collect::<Vec<_>>();
3542 let encoder = ForwardEncoder::new(SLICE, vec![0, 1]).unwrap();
3543
3544 let mut single = Recorder {
3547 sources: &refs,
3548 reads: Vec::new(),
3549 };
3550 let mut sink = VecRecoverySink::new(&[0, 1], SLICE);
3551 encoder
3552 .encode_to(
3553 &mut single,
3554 &ForwardEncoderOptions {
3555 memory_limit: Some(4 * 1024 * 1024),
3556 ..ForwardEncoderOptions::default()
3557 },
3558 &mut sink,
3559 )
3560 .unwrap();
3561 assert_eq!(
3562 single.reads,
3563 vec![(0, 0), (1, 0), (2, 0)],
3564 "a single-stripe feed must deliver each source once, whole"
3565 );
3566
3567 let mut split = Recorder {
3571 sources: &refs,
3572 reads: Vec::new(),
3573 };
3574 let mut sink = VecRecoverySink::new(&[0, 1], SLICE);
3575 encoder
3576 .encode_to(
3577 &mut split,
3578 &ForwardEncoderOptions {
3579 memory_limit: Some(32 * 1024),
3580 ..ForwardEncoderOptions::default()
3581 },
3582 &mut sink,
3583 )
3584 .unwrap();
3585 let offsets: Vec<usize> = split.reads.iter().map(|&(_, offset)| offset).collect();
3586 assert!(
3587 offsets.iter().any(|&offset| offset > 0),
3588 "the tight budget must split the slice into stripes"
3589 );
3590 assert!(
3591 split
3592 .reads
3593 .windows(2)
3594 .any(|pair| pair[0].0 > pair[1].0 && pair[1].1 > pair[0].1),
3595 "a multi-stripe feed is stripe-major: {:?}",
3596 split.reads
3597 );
3598 }
3599
3600 #[cfg(target_arch = "x86_64")]
3601 #[test]
3602 fn advertised_kernels_use_the_production_capability_resolver() {
3603 let capabilities = runtime_kernel_capabilities();
3604 let advertised = ForwardEncoder::available_kernels();
3605 assert_eq!(
3606 advertised.contains(&ForwardKernel::Folded),
3607 capabilities.folded
3608 );
3609 let encoder = ForwardEncoder::new(256, vec![0]).unwrap();
3610 for kernel in advertised {
3611 assert!(
3612 encoder.selected_kernel(kernel).is_ok(),
3613 "advertised kernel {kernel:?} cannot be selected"
3614 );
3615 }
3616 }
3617
3618 #[cfg(target_arch = "x86_64")]
3619 #[test]
3620 fn automatic_admission_keeps_the_full_kernel_ladder_ordered() {
3621 let folded_only = KernelCapabilities {
3622 folded: true,
3623 folded_wide: false,
3624 avx2_jit: false,
3625 };
3626 assert_eq!(
3627 auto_kernel_candidates(folded_only),
3628 vec![
3629 ResolvedKernel::Folded,
3630 ResolvedKernel::Simd,
3631 ResolvedKernel::Portable,
3632 ]
3633 );
3634
3635 let direct_simd_only = KernelCapabilities {
3636 folded: false,
3637 folded_wide: false,
3638 avx2_jit: false,
3639 };
3640 assert_eq!(
3641 auto_kernel_candidates(direct_simd_only),
3642 vec![ResolvedKernel::Simd, ResolvedKernel::Portable]
3643 );
3644 }
3645
3646 #[cfg(target_arch = "x86_64")]
3650 #[test]
3651 fn create_auto_ladder_prefers_shuffle_over_jit_on_fast_jit_hosts() {
3652 let fast_jit_avx2 = KernelCapabilities {
3653 folded: true,
3654 folded_wide: false,
3655 avx2_jit: true,
3656 };
3657 assert_eq!(
3658 resolve_kernel_with_capabilities(ForwardKernel::Auto, fast_jit_avx2).unwrap(),
3659 ResolvedKernel::Folded
3660 );
3661 assert_eq!(
3662 resolve_kernel_with_capabilities(ForwardKernel::XorJitAvx2, fast_jit_avx2).unwrap(),
3663 ResolvedKernel::XorJitAvx2
3664 );
3665 assert_eq!(
3666 auto_kernel_candidates(fast_jit_avx2),
3667 vec![
3668 ResolvedKernel::Folded,
3669 ResolvedKernel::XorJitAvx2,
3670 ResolvedKernel::Simd,
3671 ResolvedKernel::Portable,
3672 ]
3673 );
3674 let jit_without_folded = KernelCapabilities {
3677 folded: false,
3678 folded_wide: false,
3679 avx2_jit: true,
3680 };
3681 assert_eq!(
3682 resolve_kernel_with_capabilities(ForwardKernel::Auto, jit_without_folded).unwrap(),
3683 ResolvedKernel::XorJitAvx2
3684 );
3685 }
3686
3687 #[cfg(target_arch = "x86_64")]
3688 #[test]
3689 fn production_admission_can_fall_back_from_folded_to_simd() {
3690 let capabilities = KernelCapabilities {
3691 folded: true,
3692 folded_wide: false,
3693 avx2_jit: false,
3694 };
3695 let raw = resolve_kernel_with_capabilities(ForwardKernel::Auto, capabilities).unwrap();
3696 assert_eq!(raw, ResolvedKernel::Folded);
3697
3698 let slice_size = 60;
3699 let source_count = 19;
3700 let first_exponent = 0_u32;
3701 let recovery_count = u32::from(u16::MAX);
3702 assert!(first_exponent + recovery_count < u32::from(u16::MAX) + 1);
3703 let output_count = recovery_count as usize;
3704 let minimum_memory_limit = |requested| {
3705 let (_, full_plan) = select_kernel_for_memory_with_capabilities(
3706 slice_size,
3707 output_count,
3708 source_count,
3709 usize::MAX,
3710 requested,
3711 capabilities,
3712 )
3713 .unwrap();
3714 let mut lower = 0;
3715 let mut upper = full_plan.memory_bytes;
3716 while lower < upper {
3717 let middle = lower + (upper - lower) / 2;
3718 if select_kernel_for_memory_with_capabilities(
3719 slice_size,
3720 output_count,
3721 source_count,
3722 middle,
3723 requested,
3724 capabilities,
3725 )
3726 .is_ok()
3727 {
3728 upper = middle;
3729 } else {
3730 lower = middle + 1;
3731 }
3732 }
3733 assert!(
3734 select_kernel_for_memory_with_capabilities(
3735 slice_size,
3736 output_count,
3737 source_count,
3738 lower,
3739 requested,
3740 capabilities,
3741 )
3742 .is_ok()
3743 );
3744 if lower > 0 {
3745 assert!(
3746 select_kernel_for_memory_with_capabilities(
3747 slice_size,
3748 output_count,
3749 source_count,
3750 lower - 1,
3751 requested,
3752 capabilities,
3753 )
3754 .is_err()
3755 );
3756 }
3757 lower
3758 };
3759 let folded_minimum = minimum_memory_limit(ForwardKernel::Folded);
3760 let simd_minimum = minimum_memory_limit(ForwardKernel::Simd);
3761 assert!(
3762 folded_minimum > simd_minimum,
3763 "folded minimum {folded_minimum} is not above simd minimum {simd_minimum}"
3764 );
3765 let memory_limit = simd_minimum;
3766 assert!(
3767 select_kernel_for_memory_with_capabilities(
3768 slice_size,
3769 output_count,
3770 source_count,
3771 memory_limit,
3772 ForwardKernel::Folded,
3773 capabilities,
3774 )
3775 .is_err()
3776 );
3777 let (admitted, _) = select_kernel_for_memory_with_capabilities(
3778 slice_size,
3779 output_count,
3780 source_count,
3781 memory_limit,
3782 ForwardKernel::Auto,
3783 capabilities,
3784 )
3785 .unwrap();
3786 assert_eq!(admitted, ResolvedKernel::Simd);
3787 }
3788
3789 #[test]
3790 fn final_stripe_is_not_padded_in_sink() {
3791 struct Sink {
3792 chunks: Vec<(usize, RecoveryExponent, u64, Vec<u8>)>,
3793 }
3794 impl ForwardRecoverySink for Sink {
3795 fn write_recovery_chunk(
3796 &mut self,
3797 output_index: usize,
3798 exponent: RecoveryExponent,
3799 offset: u64,
3800 data: &[u8],
3801 ) -> Result<()> {
3802 self.chunks
3803 .push((output_index, exponent, offset, data.to_vec()));
3804 Ok(())
3805 }
3806 }
3807
3808 let sources = test_sources();
3809 let refs = sources.iter().map(Vec::as_slice).collect::<Vec<_>>();
3810 let encoder =
3811 ForwardEncoder::new(260, vec![4, 9]).expect("slice size is a valid PAR2 size");
3812 let mut sink = Sink { chunks: Vec::new() };
3813 encoder
3814 .encode_slices_to(
3815 &refs,
3816 &ForwardEncoderOptions {
3817 memory_limit: Some(8_800),
3818 kernel: ForwardKernel::Portable,
3819 ..ForwardEncoderOptions::default()
3820 },
3821 &mut sink,
3822 )
3823 .unwrap();
3824 assert!(sink.chunks.iter().all(|(_, _, _, data)| data.len() <= 260));
3825 let stripe = sink.chunks[0].3.len();
3830 assert!(
3831 (2..260).contains(&stripe),
3832 "the memory limit must force a multi-stripe plan, got stripe {stripe}"
3833 );
3834 let stripes = 260usize.div_ceil(stripe);
3835 assert_eq!(sink.chunks.len(), 2 * stripes);
3836 let last = sink.chunks.last().unwrap();
3837 assert_eq!(last.2 as usize, (stripes - 1) * stripe);
3838 assert_eq!(last.3.len(), 260 - (stripes - 1) * stripe);
3839 }
3840
3841 #[test]
3842 fn tight_memory_preserves_recovery_bytes_for_every_available_kernel() {
3843 let slice_size = 1028usize;
3844 let source_count = 19;
3845 let output_count = 3;
3846 let sources = (0..source_count)
3847 .map(|source| {
3848 (0..slice_size)
3849 .map(|index| (index.wrapping_mul(17) ^ (source * 29)) as u8)
3850 .collect()
3851 })
3852 .collect::<Vec<Vec<u8>>>();
3853 let refs = sources.iter().map(Vec::as_slice).collect::<Vec<_>>();
3854 let exponents = vec![4, 9, 17];
3855 assert_eq!(refs.len(), source_count);
3856 assert_eq!(exponents.len(), output_count);
3857 let encoder = ForwardEncoder::new(slice_size, exponents).unwrap();
3858 let (_, reference_plan) = select_kernel_for_memory(
3859 slice_size,
3860 output_count,
3861 source_count,
3862 usize::MAX,
3863 ForwardKernel::Portable,
3864 )
3865 .unwrap();
3866 assert_eq!(reference_plan.chunk_len, slice_size);
3867 let reference = encoder
3868 .encode(
3869 &refs,
3870 &ForwardEncoderOptions {
3871 memory_limit: Some(reference_plan.memory_bytes),
3872 kernel: ForwardKernel::Portable,
3873 ..ForwardEncoderOptions::default()
3874 },
3875 )
3876 .unwrap();
3877
3878 for kernel in ForwardEncoder::available_kernels() {
3879 let (_, full_plan) = select_kernel_for_memory(
3880 slice_size,
3881 output_count,
3882 source_count,
3883 usize::MAX,
3884 kernel,
3885 )
3886 .unwrap();
3887 let (tight_limit, tight_plan) = if full_plan.chunk_len < slice_size {
3888 (full_plan.memory_bytes, full_plan)
3889 } else {
3890 let mut memory_limit = full_plan.memory_bytes;
3891 loop {
3892 memory_limit = memory_limit
3893 .checked_sub(1)
3894 .expect("a full-stripe plan has a smaller admitted plan");
3895 match select_kernel_for_memory(
3896 slice_size,
3897 output_count,
3898 source_count,
3899 memory_limit,
3900 kernel,
3901 ) {
3902 Ok((_, plan))
3903 if plan.chunk_len < slice_size
3904 && !slice_size.is_multiple_of(plan.chunk_len) =>
3905 {
3906 break (memory_limit, plan);
3907 }
3908 Ok(_) | Err(_) => {}
3909 }
3910 }
3911 };
3912 assert!(
3913 tight_plan.chunk_len < slice_size,
3914 "kernel {kernel:?} retained a full-size stripe"
3915 );
3916 let stripe_count = slice_size.div_ceil(tight_plan.chunk_len);
3917 assert!(stripe_count > 1, "kernel {kernel:?} used one stripe");
3918 let final_len = slice_size % tight_plan.chunk_len;
3919 assert!(
3920 final_len > 0 && final_len < tight_plan.chunk_len,
3921 "kernel {kernel:?} did not produce a short final stripe"
3922 );
3923
3924 let actual = encoder
3925 .encode(
3926 &refs,
3927 &ForwardEncoderOptions {
3928 memory_limit: Some(tight_limit),
3929 kernel,
3930 ..ForwardEncoderOptions::default()
3931 },
3932 )
3933 .unwrap();
3934 assert_eq!(actual, reference, "kernel {kernel:?} differs from portable");
3935 }
3936 }
3937
3938 #[test]
3939 fn every_available_kernel_streams_contiguous_unpadded_chunks() {
3940 struct Sink {
3941 chunks: Vec<(usize, RecoveryExponent, u64, Vec<u8>)>,
3942 }
3943 impl ForwardRecoverySink for Sink {
3944 fn write_recovery_chunk(
3945 &mut self,
3946 output_index: usize,
3947 exponent: RecoveryExponent,
3948 offset: u64,
3949 data: &[u8],
3950 ) -> Result<()> {
3951 self.chunks
3952 .push((output_index, exponent, offset, data.to_vec()));
3953 Ok(())
3954 }
3955 }
3956
3957 let sources = test_sources();
3958 let refs = sources.iter().map(Vec::as_slice).collect::<Vec<_>>();
3959 let exponents = vec![4, 9];
3960 let encoder = ForwardEncoder::new(260, exponents.clone()).unwrap();
3961 let options = |kernel| ForwardEncoderOptions {
3962 memory_limit: Some(1024 * 1024),
3963 kernel,
3964 ..ForwardEncoderOptions::default()
3965 };
3966 let reference = encoder
3967 .encode(&refs, &options(ForwardKernel::Portable))
3968 .unwrap();
3969
3970 for kernel in ForwardEncoder::available_kernels() {
3971 let actual = encoder.encode(&refs, &options(kernel)).unwrap();
3972 assert_eq!(actual, reference, "kernel {kernel:?} differs from portable");
3973
3974 let mut sink = Sink { chunks: Vec::new() };
3975 encoder
3976 .encode_slices_to(&refs, &options(kernel), &mut sink)
3977 .unwrap();
3978 let mut next_offset = vec![0u64; exponents.len()];
3979 for (position, (output_index, exponent, offset, data)) in sink.chunks.iter().enumerate()
3980 {
3981 assert_eq!(*output_index, position % exponents.len());
3982 assert_eq!(*exponent, exponents[*output_index]);
3983 assert_eq!(*offset, next_offset[*output_index]);
3984 assert!(*offset + data.len() as u64 <= encoder.slice_size() as u64);
3985 next_offset[*output_index] += data.len() as u64;
3986 }
3987 assert!(next_offset.iter().all(|&offset| offset == 260));
3988 }
3989 }
3990
3991 #[test]
3992 fn insufficient_memory_rejects_without_zero_length_stripes() {
3993 let result = BufferPlan::new_with_reserved(
3994 260,
3995 1,
3996 KernelContract {
3997 stride: 32,
3998 input_grouping: DEFAULT_INPUT_GROUPING,
3999 tile_bytes: TABLE_TILE_BYTES,
4000 skewed_lanes: true,
4001 interleave_lanes: 1,
4002 },
4003 1,
4004 0,
4005 0,
4006 0,
4007 );
4008 assert!(matches!(
4009 result,
4010 Err(Par2Error::ResourceLimitExceeded { .. })
4011 ));
4012 }
4013
4014 #[test]
4015 fn factor_workspace_does_not_scale_with_recovery_rows() {
4016 let one = estimate_forward_memory(
4017 4,
4018 MAX_TOTAL_INPUT_SLICES,
4019 1,
4020 3 * 1024 * 1024,
4021 ForwardKernel::Portable,
4022 )
4023 .unwrap();
4024 let many = estimate_forward_memory(
4025 4,
4026 MAX_TOTAL_INPUT_SLICES,
4027 MAX_TOTAL_INPUT_SLICES,
4028 3 * 1024 * 1024,
4029 ForwardKernel::Portable,
4030 )
4031 .unwrap();
4032 assert_eq!(one.factor_workspace_bytes, many.factor_workspace_bytes);
4033 assert!(one.factor_workspace_bytes < 128 * 1024);
4034 assert!(many.processing_peak_bytes <= 3 * 1024 * 1024);
4035 }
4036
4037 #[test]
4038 fn low_memory_rejects_before_large_output_allocation() {
4039 let result = estimate_forward_memory(
4040 4096,
4041 MAX_TOTAL_INPUT_SLICES,
4042 MAX_TOTAL_INPUT_SLICES,
4043 64 * 1024,
4044 ForwardKernel::Portable,
4045 );
4046 assert!(matches!(
4047 result,
4048 Err(Par2Error::ResourceLimitExceeded { .. })
4049 ));
4050 }
4051
4052 #[test]
4053 fn staging_zero_pads_an_odd_final_byte_as_a_low_byte_word() {
4054 let source = [0x11, 0x22, 0x33];
4055 let refs = [source.as_slice()];
4056 let mut provider = InMemorySourceProvider { sources: &refs };
4057 let mut staging = AlignedBuffer::new(DEFAULT_INPUT_GROUPING * 4);
4058 let mut transfer = AlignedBuffer::new(DEFAULT_INPUT_GROUPING * 64);
4059 fill_staging(
4060 ResolvedKernel::Portable,
4061 &mut staging,
4062 &mut transfer,
4063 &mut provider,
4064 0,
4065 0,
4066 3,
4067 4,
4068 KernelContract {
4069 stride: 2,
4070 input_grouping: DEFAULT_INPUT_GROUPING,
4071 tile_bytes: TABLE_TILE_BYTES,
4072 skewed_lanes: true,
4073 interleave_lanes: 1,
4074 },
4075 &mut [0usize; MAX_INPUT_GROUPING],
4076 )
4077 .unwrap();
4078 assert_eq!(&staging.as_bytes()[..4], &[0x11, 0x22, 0x33, 0]);
4079 }
4080
4081 #[test]
4082 fn cancellation_is_observed_before_allocation() {
4083 let sources = test_sources();
4084 let refs = sources.iter().map(Vec::as_slice).collect::<Vec<_>>();
4085 let token = CancellationToken::new();
4086 token.cancel();
4087 let encoder = ForwardEncoder::new(256, vec![0]).unwrap();
4088 let error = encoder
4089 .encode(
4090 &refs,
4091 &ForwardEncoderOptions {
4092 cancel: Some(token),
4093 ..ForwardEncoderOptions::default()
4094 },
4095 )
4096 .unwrap_err();
4097 assert!(matches!(error, Par2Error::Cancelled));
4098 }
4099
4100 #[test]
4101 fn payload_matches_vandermonde_definition() {
4102 let sources = test_sources();
4103 let refs = sources.iter().map(Vec::as_slice).collect::<Vec<_>>();
4104 let exponents = [0, 31];
4105 let encoder = ForwardEncoder::new(256, exponents.to_vec()).unwrap();
4106 let actual = encoder
4107 .encode(
4108 &refs,
4109 &ForwardEncoderOptions {
4110 kernel: ForwardKernel::Portable,
4111 ..ForwardEncoderOptions::default()
4112 },
4113 )
4114 .unwrap();
4115 let constants = gf::input_slice_constants(refs.len());
4116
4117 for (output, &exponent) in exponents.iter().enumerate() {
4118 let mut expected = vec![0u8; 256];
4119 for (source_index, source) in refs.iter().enumerate() {
4120 let factor = gf::pow(constants[source_index], exponent);
4121 for word in 0..128 {
4122 let offset = word * 2;
4123 let source_word = if offset < source.len() {
4124 u16::from_le_bytes([
4125 source[offset],
4126 source.get(offset + 1).map_or(0, |byte| *byte),
4127 ])
4128 } else {
4129 0
4130 };
4131 let output_word = u16::from_le_bytes([expected[offset], expected[offset + 1]])
4132 ^ gf::mul(source_word, factor);
4133 expected[offset..offset + 2].copy_from_slice(&output_word.to_le_bytes());
4134 }
4135 }
4136 assert_eq!(actual[output].data, expected);
4137 }
4138 }
4139
4140 #[test]
4144 fn stripe_skew_follows_the_stripe_length() {
4145 assert_eq!(stripe_skew_bytes(2), 0);
4146 assert_eq!(stripe_skew_bytes(256), 0);
4147 assert_eq!(stripe_skew_bytes(1023), 0);
4148 assert_eq!(stripe_skew_bytes(1024), 0, "already 1 KiB mod 4 KiB");
4149 assert_eq!(stripe_skew_bytes(2048), 256, "wants 3 KiB, capped at 1/8");
4150 assert_eq!(stripe_skew_bytes(4096), 512, "wants 1 KiB, capped at 1/8");
4151 assert_eq!(stripe_skew_bytes(40_960), 1024);
4152 assert_eq!(stripe_skew_bytes(65_536), 1024);
4153 assert_eq!(stripe_skew_bytes(66_560), 0, "already 1 KiB mod 4 KiB");
4154 assert_eq!(
4155 stripe_skew_bytes(67_584),
4156 3072,
4157 "2 KiB residue moves to 1 KiB"
4158 );
4159 assert_eq!(stripe_skew_bytes(1 << 20), 1024);
4160 for aligned_len in [8192usize, 40_960, 65_536, 67_584, 1 << 20] {
4162 let stride = aligned_len + stripe_skew_bytes(aligned_len);
4163 assert_eq!(stride % 4096, 1024, "stride residue for {aligned_len}");
4164 }
4165 let contract = KernelContract {
4168 stride: 2,
4169 input_grouping: DEFAULT_INPUT_GROUPING,
4170 tile_bytes: TABLE_TILE_BYTES,
4171 skewed_lanes: true,
4172 interleave_lanes: 1,
4173 };
4174 let plan =
4175 BufferPlan::new_with_reserved(65_536, 820, contract, usize::MAX, 0, 0, 0).unwrap();
4176 assert_eq!(plan.aligned_chunk_len, 65_536);
4177 assert_eq!(plan.row_stride, 65_536 + 1024);
4178 assert_eq!(plan.staging_bytes, DEFAULT_INPUT_GROUPING * (65_536 + 1024));
4179 assert_eq!(plan.output_bytes, 820 * (65_536 + 1024));
4180 assert_eq!(lane_stride(contract, 65_536), 65_536 + 1024);
4181 assert_eq!(
4182 lane_stride(
4183 KernelContract {
4184 skewed_lanes: false,
4185 ..contract
4186 },
4187 65_536
4188 ),
4189 65_536
4190 );
4191 }
4192
4193 #[test]
4197 fn input_grouping_follows_the_kernel_family() {
4198 let simd = KernelContract::for_kernel(ResolvedKernel::Simd);
4199 let portable = KernelContract::for_kernel(ResolvedKernel::Portable);
4200 assert_eq!(simd.input_grouping, portable.input_grouping);
4201 assert!((1..=MAX_INPUT_GROUPING).contains(&simd.input_grouping));
4202 if std::env::var_os("WEAVER_PAR2_CREATE_GROUPING").is_none() {
4203 #[cfg(target_arch = "aarch64")]
4204 assert_eq!(simd.input_grouping, CLMUL_INPUT_GROUPING);
4205 #[cfg(not(target_arch = "aarch64"))]
4206 assert_eq!(simd.input_grouping, DEFAULT_INPUT_GROUPING);
4207 }
4208 #[cfg(target_arch = "x86_64")]
4209 for kernel in ForwardEncoder::available_kernels() {
4210 let resolved =
4211 resolve_kernel_with_capabilities(kernel, runtime_kernel_capabilities()).unwrap();
4212 if matches!(
4213 resolved,
4214 ResolvedKernel::Folded | ResolvedKernel::XorJitAvx2
4215 ) {
4216 assert_eq!(
4217 KernelContract::for_kernel(resolved).input_grouping,
4218 DEFAULT_INPUT_GROUPING
4219 );
4220 }
4221 }
4222 }
4223
4224 #[test]
4230 fn skewed_stripe_layout_matches_vandermonde_definition_on_every_kernel() {
4231 const SLICE: usize = 4096;
4232 assert_eq!(stripe_skew_bytes(SLICE), 512, "the skew must be live here");
4233 let sources: Vec<Vec<u8>> = (0..27usize)
4234 .map(|source| {
4235 (0..(SLICE - source * 97))
4236 .map(|index| (index.wrapping_mul(31) ^ (source * 53) ^ (index >> 7)) as u8)
4237 .collect()
4238 })
4239 .collect();
4240 let refs = sources.iter().map(Vec::as_slice).collect::<Vec<_>>();
4241 let exponents: [RecoveryExponent; 4] = [0, 1, 31, 100];
4242 let constants = gf::input_slice_constants(refs.len());
4243 let mut expected = Vec::new();
4244 for &exponent in &exponents {
4245 let mut block = vec![0u8; SLICE];
4246 for (source_index, source) in refs.iter().enumerate() {
4247 let factor = gf::pow(constants[source_index], exponent);
4248 for word in 0..SLICE / 2 {
4249 let offset = word * 2;
4250 let source_word = if offset < source.len() {
4251 u16::from_le_bytes([
4252 source[offset],
4253 source.get(offset + 1).map_or(0, |byte| *byte),
4254 ])
4255 } else {
4256 0
4257 };
4258 let output_word = u16::from_le_bytes([block[offset], block[offset + 1]])
4259 ^ gf::mul(source_word, factor);
4260 block[offset..offset + 2].copy_from_slice(&output_word.to_le_bytes());
4261 }
4262 }
4263 expected.push(block);
4264 }
4265 for kernel in ForwardEncoder::available_kernels() {
4266 let encoder = ForwardEncoder::new(SLICE, exponents.to_vec()).unwrap();
4267 let actual = encoder
4268 .encode(
4269 &refs,
4270 &ForwardEncoderOptions {
4271 kernel,
4272 ..ForwardEncoderOptions::default()
4273 },
4274 )
4275 .unwrap();
4276 for (output, block) in expected.iter().enumerate() {
4277 assert_eq!(
4278 &actual[output].data, block,
4279 "kernel {kernel:?} output {output} diverged from the definition"
4280 );
4281 }
4282 }
4283 }
4284
4285 #[test]
4289 fn staging_layout_fits_the_planned_area_at_every_width() {
4290 const BLOCK: usize = gf_simd::INPUT_BATCH_BLOCK_BYTES;
4291 for aligned_len in [BLOCK, 4096usize, 8192, 65_536] {
4292 for grouping in [1usize, 4, 12, 16] {
4293 let base = KernelContract {
4294 stride: BLOCK,
4295 input_grouping: grouping,
4296 tile_bytes: TABLE_TILE_BYTES,
4297 skewed_lanes: true,
4298 interleave_lanes: 1,
4299 };
4300 let stride = lane_stride(base, aligned_len);
4301 let planned = grouping * stride;
4302 for interleave in [1usize, 2, 4, 8, 16] {
4303 let contract = KernelContract {
4304 interleave_lanes: interleave,
4305 ..base
4306 };
4307 let layout = StagingLayout::new(contract, aligned_len, stride);
4308 let total = layout.total_bytes().expect("layout fits usize");
4309 assert!(
4310 total <= planned,
4311 "layout {interleave}x{grouping} at {aligned_len} wants {total} of {planned}"
4312 );
4313 let widths: usize = (0..layout.group_count())
4316 .map(|group| layout.group_width(group))
4317 .sum();
4318 assert_eq!(widths, grouping, "every lane belongs to exactly one group");
4319 for group in 0..layout.group_count() {
4321 let (_, end) = layout.group_tile(group, aligned_len - BLOCK, BLOCK);
4322 assert!(end <= total, "group {group} tile runs past the layout");
4323 }
4324 }
4325 let lane_major = StagingLayout::new(base, aligned_len, stride);
4327 for lane in 0..grouping {
4328 assert_eq!(lane_major.group_base(lane), lane * stride);
4329 }
4330 }
4331 }
4332 }
4333
4334 #[test]
4338 fn short_staging_is_refused_before_the_fill() {
4339 const BLOCK: usize = gf_simd::INPUT_BATCH_BLOCK_BYTES;
4340 let source = vec![0xA5u8; 512];
4341 let refs = [source.as_slice()];
4342 for interleave in [1usize, 8] {
4343 let contract = KernelContract {
4344 stride: BLOCK,
4345 input_grouping: 8,
4346 tile_bytes: TABLE_TILE_BYTES,
4347 skewed_lanes: true,
4348 interleave_lanes: interleave,
4349 };
4350 let stride = lane_stride(contract, 512);
4351 let needed = StagingLayout::new(contract, 512, stride)
4352 .total_bytes()
4353 .unwrap();
4354 let mut provider = InMemorySourceProvider { sources: &refs };
4355 let mut staging = AlignedBuffer::new(needed - 1);
4356 let mut transfer =
4359 AlignedBuffer::new(contract.input_grouping * transfer_slot_stride(512).unwrap());
4360 let mut slice_lens = [0usize; MAX_INPUT_GROUPING];
4361 let result = fill_staging(
4362 ResolvedKernel::Simd,
4363 &mut staging,
4364 &mut transfer,
4365 &mut provider,
4366 0,
4367 0,
4368 512,
4369 512,
4370 contract,
4371 &mut slice_lens,
4372 );
4373 assert!(
4374 matches!(result, Err(Par2Error::ResourceLimitExceeded { .. })),
4375 "interleave {interleave} accepted a short staging area"
4376 );
4377 }
4378 }
4379
4380 #[test]
4391 fn interleaved_staging_matches_lane_major_and_the_word_wise_kernel() {
4392 const BLOCK: usize = gf_simd::INPUT_BATCH_BLOCK_BYTES;
4393 const SLICE: usize = 8 * 1024 + 96;
4396 let sources: Vec<Vec<u8>> = (0..MAX_INPUT_GROUPING)
4397 .map(|source| {
4398 (0..(SLICE - source * 37))
4399 .map(|index| (index.wrapping_mul(31) ^ (source * 53) ^ (index >> 5)) as u8)
4400 .collect()
4401 })
4402 .collect();
4403 let refs = sources.iter().map(Vec::as_slice).collect::<Vec<_>>();
4404 let exponents: Vec<RecoveryExponent> = vec![0, 1, 2, 31, 100];
4405 let simd = KernelContract::for_kernel(ResolvedKernel::Simd);
4406 let aligned_len = round_up(SLICE, BLOCK).unwrap();
4407
4408 let run = |kernel: ResolvedKernel, contract: KernelContract, live: usize| -> Vec<u8> {
4409 let mut provider = InMemorySourceProvider { sources: &refs };
4410 let mut staging =
4411 AlignedBuffer::new(contract.input_grouping * lane_stride(contract, aligned_len));
4412 let mut transfer = AlignedBuffer::new(
4413 contract.input_grouping * transfer_slot_stride(aligned_len).unwrap(),
4414 );
4415 let mut slice_lens = [0usize; MAX_INPUT_GROUPING];
4416 fill_staging(
4417 kernel,
4418 &mut staging,
4419 &mut transfer,
4420 &mut provider,
4421 0,
4422 0,
4423 SLICE,
4424 aligned_len,
4425 contract,
4426 &mut slice_lens,
4427 )
4428 .unwrap();
4429 let factors = FactorSource::new(refs.len());
4430 let mut output = AlignedBuffer::new(exponents.len() * aligned_len);
4431 #[cfg(target_arch = "x86_64")]
4432 let mut jit_workspaces: Vec<
4433 reedsolomon_rs::xor_jit::packed::PackedJitWorkspace,
4434 > = vec![Default::default()];
4435 accumulate_batch(
4436 kernel,
4437 output.as_bytes_mut(),
4438 &staging,
4439 &factors,
4440 &exponents,
4441 0,
4442 live,
4443 aligned_len,
4444 aligned_len,
4445 contract,
4446 exponents.len(),
4447 #[cfg(target_arch = "x86_64")]
4448 &mut jit_workspaces,
4449 #[cfg(target_arch = "x86_64")]
4450 usize::MAX,
4451 )
4452 .unwrap();
4453 output.as_bytes().to_vec()
4454 };
4455
4456 for grouping in [12usize, 16, MAX_INPUT_GROUPING] {
4461 let portable = KernelContract {
4462 input_grouping: grouping,
4463 ..KernelContract::for_kernel(ResolvedKernel::Portable)
4464 };
4465 for live in [1usize, 3, 8, 11, grouping] {
4466 let live = live.min(grouping).min(refs.len());
4467 let definition = run(ResolvedKernel::Portable, portable, live);
4468 for tile_bytes in [UNTILED, 8192usize, 2048] {
4469 let mut lane_major: Option<Vec<u8>> = None;
4470 for interleave in [1usize, 2, 4, 8, 16] {
4471 let contract = KernelContract {
4472 stride: BLOCK,
4473 tile_bytes,
4474 input_grouping: grouping,
4475 interleave_lanes: interleave,
4476 ..simd
4477 };
4478 let got = run(ResolvedKernel::Simd, contract, live);
4479 let case = format!(
4480 "grouping={grouping} interleave={interleave} \
4481 tile={tile_bytes} live={live}"
4482 );
4483 assert_eq!(
4484 got, definition,
4485 "simd {case} diverged from the word-wise kernel"
4486 );
4487 match &lane_major {
4488 None => lane_major = Some(got),
4489 Some(expected) => assert_eq!(
4490 &got, expected,
4491 "simd {case} diverged from the lane-major layout"
4492 ),
4493 }
4494 }
4495 }
4496 }
4497 }
4498 }
4499
4500 #[test]
4501 fn zero_input_produces_zero_recovery_blocks() {
4502 let encoder = ForwardEncoder::new(256, vec![0, 5]).unwrap();
4503 let blocks = encoder
4504 .encode(&[], &ForwardEncoderOptions::default())
4505 .unwrap();
4506 assert_eq!(blocks.len(), 2);
4507 assert!(
4508 blocks
4509 .iter()
4510 .all(|block| block.data.iter().all(|&byte| byte == 0))
4511 );
4512 }
4513}