1use rayon::prelude::*;
11use std::collections::{HashMap, HashSet, VecDeque};
12use std::fs::File;
13use std::io::{self, Read, Seek, SeekFrom};
14use std::mem::MaybeUninit;
15use std::ops::ControlFlow;
16use std::path::{Path, PathBuf};
17use std::sync::Arc;
18#[cfg(test)]
19use std::sync::atomic::AtomicU32;
20use std::sync::atomic::{AtomicU64, Ordering};
21use std::time::{Duration, Instant};
22use tracing::{debug, info, warn};
23
24use crate::cpu_repair_controller::{
25 ControllerAddResult, ControllerAddStatus, ControllerExecutionEvent, ControllerExecutionTrace,
26 ControllerFailurePhase, ControllerLayout, ControllerLifecycle, CpuControllerPlan,
27 CpuMethodContract, CpuPrefetch, InputBatch,
28};
29use crate::error::{Par2Error, Result};
30use crate::gf;
31use crate::matrix;
32use crate::par2_set::Par2FileSet;
33use crate::types::{
34 CancellationToken, FileId, MAX_SLICES_PER_FILE, MAX_TOTAL_INPUT_SLICES, ProgressCallback,
35 ProgressPhase, ProgressStage, ProgressUpdate,
36};
37use crate::verify::{FileAccess, FileRangeReader, Repairability, VerificationResult};
38
39pub(crate) const DEFAULT_REPAIR_MEMORY_LIMIT: usize = 128 * 1024 * 1024;
47const MATRIX_WORKSPACE_BUDGET_FLOOR: usize = 1024 * 1024 * 1024;
53const XOR_OUT_PAR_CHUNK: usize = 16;
54const CPU_CONTROLLER_BUDGET_INPUTS: usize = 12;
55
56#[derive(Debug, Clone)]
58pub struct RepairPlan {
59 pub missing_slices: Vec<(FileId, u32)>,
61 pub missing_global_indices: Vec<usize>,
63 pub available_input_global_indices: Vec<usize>,
65 pub recovery_exponents: Vec<u32>,
67 pub decode_matrix: matrix::Matrix,
69 pub input_factors: matrix::Matrix,
72 pub slice_size: u64,
74 pub constants: Vec<u16>,
76 pub total_input_slices: usize,
78 pub global_to_file: Vec<(FileId, u32)>,
80}
81
82pub fn plan_repair(
87 par2_set: &Par2FileSet,
88 verification: &VerificationResult,
89) -> Result<RepairPlan> {
90 plan_repair_with_memory_limit(par2_set, verification, Some(DEFAULT_REPAIR_MEMORY_LIMIT))
91}
92
93pub fn plan_repair_with_memory_limit(
95 par2_set: &Par2FileSet,
96 verification: &VerificationResult,
97 memory_limit: Option<usize>,
98) -> Result<RepairPlan> {
99 match &verification.repairable {
101 Repairability::NotNeeded => {
102 return Err(Par2Error::ReedSolomonError {
103 reason: "no repair needed".to_string(),
104 });
105 }
106 Repairability::Insufficient {
107 blocks_needed,
108 blocks_available,
109 deficit,
110 } => {
111 return Err(Par2Error::InsufficientRecoveryData {
112 needed: *blocks_needed,
113 available: *blocks_available,
114 deficit: *deficit,
115 });
116 }
117 Repairability::ResourceLimited { reason } => {
118 return Err(Par2Error::ResourceLimitExceeded {
119 reason: format!("PAR2 verification is resource-limited: {reason}"),
120 });
121 }
122 Repairability::Repairable { .. } => {}
123 }
124
125 let mut global_to_file: Vec<(FileId, u32)> = Vec::new();
128 for file_id in &par2_set.recovery_file_ids {
129 if let Some(desc) = par2_set.file_description(file_id) {
130 let slice_count =
131 usize::try_from(par2_set.slice_count_for_file(desc.length)).map_err(|_| {
132 Par2Error::ResourceLimitExceeded {
133 reason: format!(
134 "file {} has more than {MAX_SLICES_PER_FILE} addressable PAR2 slices",
135 desc.filename
136 ),
137 }
138 })?;
139 if slice_count > MAX_SLICES_PER_FILE {
140 return Err(Par2Error::ResourceLimitExceeded {
141 reason: format!(
142 "file {} has {slice_count} PAR2 slices; max is {MAX_SLICES_PER_FILE}",
143 desc.filename
144 ),
145 });
146 }
147 for s in 0..slice_count {
148 global_to_file.push((*file_id, s as u32));
149 }
150 }
151 }
152 let total_input_slices = global_to_file.len();
153 if total_input_slices > MAX_TOTAL_INPUT_SLICES {
154 return Err(Par2Error::ResourceLimitExceeded {
155 reason: format!(
156 "recovery set has {total_input_slices} input slices; PAR2 supports at most {MAX_TOTAL_INPUT_SLICES}"
157 ),
158 });
159 }
160
161 let mut missing_slices: Vec<(FileId, u32)> = Vec::new();
163 let mut missing_global_indices: Vec<usize> = Vec::new();
164
165 let mut global_idx = 0usize;
166 for file_id in &par2_set.recovery_file_ids {
167 let desc = match par2_set.file_description(file_id) {
168 Some(d) => d,
169 None => continue,
170 };
171 let slice_count =
172 usize::try_from(par2_set.slice_count_for_file(desc.length)).map_err(|_| {
173 Par2Error::ResourceLimitExceeded {
174 reason: format!(
175 "file {} has more than {MAX_SLICES_PER_FILE} addressable PAR2 slices",
176 desc.filename
177 ),
178 }
179 })?;
180 if slice_count > MAX_SLICES_PER_FILE {
181 return Err(Par2Error::ResourceLimitExceeded {
182 reason: format!(
183 "file {} has {slice_count} PAR2 slices; max is {MAX_SLICES_PER_FILE}",
184 desc.filename
185 ),
186 });
187 }
188
189 let file_verif = verification.files.iter().find(|fv| fv.file_id == *file_id);
191
192 for s in 0..slice_count {
193 let is_valid = file_verif
194 .map(|fv| fv.valid_slices.get(s).copied().unwrap_or(false))
195 .unwrap_or(false);
196
197 if !is_valid {
198 missing_slices.push((*file_id, s as u32));
199 missing_global_indices.push(global_idx + s);
200 }
201 }
202 global_idx += slice_count;
203 }
204
205 let missing_count = missing_slices.len();
206 debug!("repair: {missing_count} missing slices identified");
207
208 let mut all_exponents: Vec<u32> = par2_set.recovery_slices.keys().copied().collect();
212 all_exponents.sort_unstable();
213
214 if all_exponents.len() < missing_count {
215 return Err(Par2Error::InsufficientRecoveryData {
216 needed: missing_count as u32,
217 available: all_exponents.len() as u32,
218 deficit: (missing_count - all_exponents.len()) as u32,
219 });
220 }
221 if let Some(reason) =
222 repair_matrix_limit_reason(total_input_slices, missing_count, memory_limit)
223 {
224 return Err(Par2Error::ResourceLimitExceeded { reason });
225 }
226
227 let constants = gf::input_slice_constants(total_input_slices);
229 let missing_set: HashSet<usize> = missing_global_indices.iter().copied().collect();
230 let available_input_global_indices: Vec<usize> = (0..total_input_slices)
231 .filter(|global_idx| !missing_set.contains(global_idx))
232 .collect();
233
234 let mut skip_set: HashSet<usize> = HashSet::new();
238 let mut validated_exponents: HashMap<u32, bool> = HashMap::new();
239 let (recovery_exponents, input_factors, decode) = loop {
240 let selected_indices: Vec<usize> = all_exponents
241 .iter()
242 .enumerate()
243 .filter(|(i, _)| !skip_set.contains(i))
244 .map(|(i, _)| i)
245 .take(missing_count)
246 .collect();
247 let selected: Vec<u32> = selected_indices
248 .iter()
249 .map(|&idx| all_exponents[idx])
250 .collect();
251
252 if selected.len() < missing_count {
253 return Err(Par2Error::InsufficientRecoveryData {
254 needed: missing_count as u32,
255 available: selected.len() as u32,
256 deficit: (missing_count - selected.len()) as u32,
257 });
258 }
259
260 let mut corrupt_selection = None;
261 for (position, &exponent) in selected.iter().enumerate() {
262 let valid = *validated_exponents.entry(exponent).or_insert_with(|| {
263 let slice = &par2_set.recovery_slices[&exponent];
264 match slice
265 .data
266 .validate_packet_hash(par2_set.recovery_set_id.as_bytes(), exponent)
267 {
268 Ok(valid) => {
269 if !valid {
270 warn!(
271 "recovery block exponent {exponent} failed packet hash validation, skipping"
272 );
273 }
274 valid
275 }
276 Err(error) => {
277 warn!("recovery block exponent {exponent} is unreadable ({error}), skipping");
278 false
279 }
280 }
281 });
282 if !valid {
283 corrupt_selection = Some(selected_indices[position]);
284 break;
285 }
286 }
287 if let Some(skip_idx) = corrupt_selection {
288 skip_set.insert(skip_idx);
289 continue;
290 }
291
292 match matrix::build_repair_matrix_with_bad_row(
293 &available_input_global_indices,
294 &missing_global_indices,
295 &selected,
296 &constants,
297 ) {
298 Ok((input_factors, decode)) => break (selected, input_factors, decode),
299 Err(matrix_error) => {
300 let mut skip_idx = matrix_error
301 .bad_row
302 .and_then(|row| selected_indices.get(row).copied());
303 if skip_idx.is_none() {
304 for candidate_idx in &selected_indices {
305 let trial: Vec<u32> = all_exponents
306 .iter()
307 .enumerate()
308 .filter(|(idx, _)| !skip_set.contains(idx) && idx != candidate_idx)
309 .map(|(_, &exponent)| exponent)
310 .take(missing_count)
311 .collect();
312 if trial.len() < missing_count {
313 continue;
314 }
315 if matrix::build_repair_matrix_with_bad_row(
316 &available_input_global_indices,
317 &missing_global_indices,
318 &trial,
319 &constants,
320 )
321 .is_ok()
322 {
323 skip_idx = Some(*candidate_idx);
324 break;
325 }
326 }
327 }
328 let skip_idx = skip_idx.unwrap_or_else(|| {
329 *selected_indices
330 .last()
331 .expect("singular repair selection must contain at least one row")
332 });
333 warn!(
334 "recovery exponent {} produced singular matrix, skipping",
335 all_exponents[skip_idx]
336 );
337 skip_set.insert(skip_idx);
338 }
339 }
340 };
341
342 info!(
343 "repair plan: {} missing slices, {} recovery blocks selected",
344 missing_count,
345 recovery_exponents.len()
346 );
347
348 Ok(RepairPlan {
349 missing_slices,
350 missing_global_indices,
351 available_input_global_indices,
352 recovery_exponents,
353 decode_matrix: decode,
354 input_factors,
355 slice_size: par2_set.slice_size,
356 constants,
357 total_input_slices,
358 global_to_file,
359 })
360}
361
362pub(crate) fn repair_matrix_resource_limit_reason(
363 par2_set: &Par2FileSet,
364 verification: &VerificationResult,
365 memory_limit: Option<usize>,
366) -> Result<Option<String>> {
367 if !matches!(verification.repairable, Repairability::Repairable { .. }) {
368 return Ok(None);
369 }
370
371 let total_input_slices = total_input_slices_for_set(par2_set)?;
372 let missing_count = verification.total_missing_blocks as usize;
373 Ok(repair_matrix_limit_reason(
374 total_input_slices,
375 missing_count,
376 memory_limit,
377 ))
378}
379
380pub struct RepairOptions {
382 pub cancel: Option<CancellationToken>,
384 pub progress: Option<ProgressCallback>,
386 pub memory_limit: Option<usize>,
394}
395
396impl Default for RepairOptions {
397 fn default() -> Self {
398 Self {
399 cancel: None,
400 progress: None,
401 memory_limit: Some(DEFAULT_REPAIR_MEMORY_LIMIT),
402 }
403 }
404}
405
406#[derive(Clone, Copy)]
407struct FactorIndex {
408 factor: u16,
409 input_idx: u16,
410}
411
412#[derive(Clone, Debug)]
413pub(crate) struct RepairWriteTarget {
414 pub(crate) file_id: FileId,
415 pub(crate) filename: String,
416 pub(crate) offset: u64,
417 pub(crate) file_end: u64,
418}
419
420pub(crate) fn check_cancel(options: &RepairOptions) -> Result<()> {
421 if let Some(ref cancel) = options.cancel
422 && cancel.is_cancelled()
423 {
424 return Err(Par2Error::Cancelled);
425 }
426 Ok(())
427}
428
429fn recv_with_cancel<T>(
430 receiver: &std::sync::mpsc::Receiver<T>,
431 cancel: Option<&CancellationToken>,
432 reason: &'static str,
433) -> Result<T> {
434 loop {
435 match receiver.recv_timeout(Duration::from_millis(20)) {
436 Ok(value) => return Ok(value),
437 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
438 if cancel.is_some_and(|token| token.is_cancelled()) {
439 return Err(Par2Error::Cancelled);
440 }
441 }
442 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
443 return Err(Par2Error::ReedSolomonError {
444 reason: reason.to_string(),
445 });
446 }
447 }
448 }
449}
450
451fn estimated_repair_matrix_bytes(total_inputs: usize, missing_rows: usize) -> usize {
452 let working_words = missing_rows
453 .saturating_mul(missing_rows)
454 .saturating_add(missing_rows.saturating_mul(total_inputs));
455 working_words.saturating_mul(std::mem::size_of::<u16>())
456}
457
458fn repair_memory_limit_bytes(memory_limit: Option<usize>) -> usize {
459 memory_limit.unwrap_or(DEFAULT_REPAIR_MEMORY_LIMIT)
460}
461
462fn repair_matrix_limit_reason(
463 total_input_slices: usize,
464 missing_count: usize,
465 memory_limit: Option<usize>,
466) -> Option<String> {
467 if total_input_slices > MAX_TOTAL_INPUT_SLICES {
468 return Some(format!(
469 "recovery set has {total_input_slices} input slices; PAR2 supports at most {MAX_TOTAL_INPUT_SLICES}"
470 ));
471 }
472 let estimated = estimated_repair_matrix_bytes(total_input_slices, missing_count);
473 let limit = repair_memory_limit_bytes(memory_limit).max(MATRIX_WORKSPACE_BUDGET_FLOOR);
474 (estimated > limit).then(|| {
475 format!(
476 "repair matrix for {missing_count} missing slices would require {estimated} bytes, exceeding the {limit} byte matrix workspace budget"
477 )
478 })
479}
480
481fn total_input_slices_for_set(par2_set: &Par2FileSet) -> Result<usize> {
482 let mut total = 0usize;
483 for file_id in &par2_set.recovery_file_ids {
484 let Some(desc) = par2_set.file_description(file_id) else {
485 continue;
486 };
487 let slice_count =
488 usize::try_from(par2_set.slice_count_for_file(desc.length)).map_err(|_| {
489 Par2Error::ResourceLimitExceeded {
490 reason: format!(
491 "file {} has more than {MAX_SLICES_PER_FILE} addressable PAR2 slices",
492 desc.filename
493 ),
494 }
495 })?;
496 if slice_count > MAX_SLICES_PER_FILE {
497 return Err(Par2Error::ResourceLimitExceeded {
498 reason: format!(
499 "file {} has {slice_count} PAR2 slices; max is {MAX_SLICES_PER_FILE}",
500 desc.filename
501 ),
502 });
503 }
504 total = total.saturating_add(slice_count);
505 }
506 Ok(total)
507}
508
509fn cpu_controller_plan(
510 current_slice_size: usize,
511 output_count: usize,
512 worker_count: usize,
513 #[cfg_attr(not(target_arch = "x86_64"), allow(dead_code))] method: CpuMethodContract,
514 allocated_staging_width: usize,
515) -> CpuControllerPlan {
516 CpuControllerPlan::new_with_input_grouping_and_staging_width(
517 current_slice_size,
518 output_count,
519 worker_count,
520 method,
521 method.input_grouping(),
522 allocated_staging_width,
523 )
524}
525
526fn largest_fitting_chunk_words(
536 word_count: usize,
537 output_count: usize,
538 workers: usize,
539 method: CpuMethodContract,
540 allocated_staging_width: usize,
541 controller_budget: usize,
542) -> Option<usize> {
543 let fits = |chunk_words: usize| {
544 cpu_controller_plan(
545 chunk_words.saturating_mul(2),
546 output_count,
547 workers,
548 method,
549 allocated_staging_width,
550 )
551 .buffer_accounting()
552 .total_bytes
553 <= controller_budget
554 };
555 let word_count = word_count.max(1);
556 if !fits(1) {
557 return None;
558 }
559 if fits(word_count) {
560 return Some(word_count);
561 }
562 let (mut low, mut high) = (1usize, word_count);
567 while high - low > 1 {
568 let mid = low + (high - low) / 2;
569 if fits(mid) {
570 low = mid;
571 } else {
572 high = mid;
573 }
574 }
575 Some(low)
576}
577
578fn controller_execution_parameters(
579 plan: &RepairPlan,
580 options: &RepairOptions,
581 method: CpuMethodContract,
582 allocated_staging_width: usize,
583 persistent_bytes: usize,
584 workers: usize,
585) -> Result<(usize, usize, CpuControllerPlan)> {
586 let word_count = (plan.slice_size as usize / 2).max(1);
587 let limit = options.memory_limit.unwrap_or(DEFAULT_REPAIR_MEMORY_LIMIT);
588 let output_count = plan.missing_slices.len();
589 let controller_budget = limit.checked_sub(persistent_bytes).ok_or_else(|| {
590 Par2Error::ResourceLimitExceeded {
591 reason: format!(
592 "persistent CPU repair state needs {persistent_bytes} bytes, exceeding the {limit} byte memory limit"
593 ),
594 }
595 })?;
596 let chunk_words = largest_fitting_chunk_words(
597 word_count,
598 output_count,
599 workers,
600 method,
601 allocated_staging_width,
602 controller_budget,
603 )
604 .ok_or_else(|| {
605 let minimum_bytes =
606 cpu_controller_plan(2, output_count, workers, method, allocated_staging_width)
607 .buffer_accounting()
608 .total_bytes;
609 Par2Error::ResourceLimitExceeded {
610 reason: format!(
611 "CPU repair controller needs at least {minimum_bytes} bytes, leaving {controller_budget} bytes after persistent state"
612 ),
613 }
614 })?;
615 let controller = cpu_controller_plan(
616 chunk_words.saturating_mul(2),
617 output_count,
618 workers,
619 method,
620 allocated_staging_width,
621 );
622 Ok((chunk_words, limit, controller))
623}
624
625pub(crate) fn build_write_targets(
626 plan: &RepairPlan,
627 par2_set: &Par2FileSet,
628) -> Result<Vec<RepairWriteTarget>> {
629 plan.missing_slices
630 .iter()
631 .map(|(file_id, local_slice)| {
632 let desc =
633 par2_set
634 .file_description(file_id)
635 .ok_or_else(|| Par2Error::ReedSolomonError {
636 reason: format!("file description not found for {file_id}"),
637 })?;
638 Ok(RepairWriteTarget {
639 file_id: *file_id,
640 filename: desc.filename.clone(),
641 offset: *local_slice as u64 * plan.slice_size,
642 file_end: desc.length,
643 })
644 })
645 .collect()
646}
647
648fn grouped_input_factors(coefficients: &matrix::Matrix) -> Vec<Vec<FactorIndex>> {
649 (0..coefficients.rows)
650 .map(|row_idx| {
651 coefficients
652 .row(row_idx)
653 .iter()
654 .enumerate()
655 .filter_map(|(input_idx, &factor)| {
656 if factor == 0 {
657 None
658 } else {
659 Some(FactorIndex {
660 factor,
661 input_idx: input_idx as u16,
662 })
663 }
664 })
665 .collect()
666 })
667 .collect()
668}
669
670const PLAIN_IDEAL_CHUNK_BYTES: usize = 32 * 1024;
672const FOLDED_IDEAL_CHUNK_BYTES: usize = 8 * 1024;
673
674#[cfg(target_arch = "aarch64")]
678const NEON_PACKED_BLOCK_BYTES: usize = 32;
679
680#[cfg(target_arch = "aarch64")]
685const NEON_PACKED_CHECKSUM_BYTES: usize = 16;
686
687#[cfg(target_arch = "aarch64")]
691fn neon_packed_enabled() -> bool {
692 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
693 *ENABLED.get_or_init(|| std::env::var_os("WEAVER_PAR2_NEON_PACKED").is_none_or(|v| v != "0"))
694}
695#[cfg(target_arch = "x86_64")]
696const XORJIT_AVX2_IDEAL_CHUNK_BYTES: usize = 128 * 1024;
697
698#[repr(C, align(64))]
701#[derive(Clone, Copy)]
702struct StagingCell([u8; 64]);
703
704fn staging_cells_for(bytes: usize) -> Vec<StagingCell> {
705 vec![StagingCell([0u8; 64]); bytes.div_ceil(64)]
706}
707
708fn staging_bytes(cells: &[StagingCell]) -> &[u8] {
709 unsafe { std::slice::from_raw_parts(cells.as_ptr() as *const u8, cells.len() * 64) }
712}
713
714fn staging_bytes_mut(cells: &mut [StagingCell]) -> &mut [u8] {
715 unsafe { std::slice::from_raw_parts_mut(cells.as_mut_ptr() as *mut u8, cells.len() * 64) }
716}
717
718struct AlignedOutputArea {
724 cells: Vec<StagingCell>,
725}
726
727impl AlignedOutputArea {
728 fn new(output_count: usize, byte_len: usize) -> Self {
729 Self {
730 cells: staging_cells_for(output_count.saturating_mul(byte_len)),
731 }
732 }
733
734 fn base(&mut self) -> usize {
735 self.cells.as_mut_ptr() as *mut u8 as usize
736 }
737}
738
739struct MemoEntry {
743 prepared: crate::gf_simd::PreparedInputFactor,
744 affine: Option<crate::gf_simd::AffineMulMatrices>,
747 shuffle2x: Option<crate::gf_simd::Shuffle2xTables>,
750}
751
752struct PreparedFactorMemo {
753 slots: Vec<Option<Box<MemoEntry>>>,
754}
755
756impl PreparedFactorMemo {
757 fn from_matrix(matrix: &matrix::Matrix, with_folded: bool) -> Self {
758 let mut slots: Vec<Option<Box<MemoEntry>>> = (0..1usize << 16).map(|_| None).collect();
759 let uses_gfni = with_folded && crate::gf_simd::folded_uses_gfni();
763 let uses_shuffle2x = with_folded && !uses_gfni;
764 let ensure = |factor: u16, slots: &mut Vec<Option<Box<MemoEntry>>>| {
766 let slot = &mut slots[factor as usize];
767 if slot.is_none() {
768 *slot = Some(Box::new(MemoEntry {
769 prepared: crate::gf_simd::prepare_input_factor(factor),
770 affine: uses_gfni.then(|| crate::gf_simd::precompute_affine_matrices(factor)),
771 shuffle2x: uses_shuffle2x
772 .then(|| crate::gf_simd::precompute_shuffle2x_tables(factor)),
773 }));
774 }
775 };
776 ensure(0, &mut slots);
777 for output_idx in 0..matrix.rows {
778 for source_idx in 0..matrix.cols {
779 ensure(matrix.get(output_idx, source_idx), &mut slots);
780 }
781 }
782 Self { slots }
783 }
784
785 #[inline]
786 fn get(&self, factor: u16) -> &crate::gf_simd::PreparedInputFactor {
787 &self.slots[factor as usize]
788 .as_deref()
789 .expect("factor prepared during memo construction")
790 .prepared
791 }
792
793 #[inline]
794 fn get_affine(&self, factor: u16) -> &crate::gf_simd::AffineMulMatrices {
795 self.slots[factor as usize]
796 .as_deref()
797 .expect("factor prepared during memo construction")
798 .affine
799 .as_ref()
800 .expect("affine matrices built for the folded path")
801 }
802
803 #[inline]
804 fn get_shuffle2x(&self, factor: u16) -> &crate::gf_simd::Shuffle2xTables {
805 self.slots[factor as usize]
806 .as_deref()
807 .expect("factor prepared during memo construction")
808 .shuffle2x
809 .as_ref()
810 .expect("shuffle2x tables built for the folded path")
811 }
812}
813
814#[cfg(target_arch = "x86_64")]
819enum JitDispatchStorage {
820 RepairCodebook(Arc<reedsolomon_rs::xor_jit::packed::Avx2Codebook>),
821 ActiveArenas { arena_limit: usize },
822}
823
824#[cfg(target_arch = "x86_64")]
825struct JitMemo {
826 width: reedsolomon_rs::xor_jit::JitWidth,
827 input_grouping: usize,
828 output_count: usize,
829 storage: JitDispatchStorage,
830 reserved_bytes: usize,
831}
832
833#[cfg(target_arch = "x86_64")]
834impl JitMemo {
835 fn new(
836 width: reedsolomon_rs::xor_jit::JitWidth,
837 method: CpuMethodContract,
838 output_count: usize,
839 repair_factors: &[u16],
840 codebook_limit: usize,
841 available_bytes: usize,
842 ) -> std::result::Result<Self, reedsolomon_rs::xor_jit::packed::PackedBuildError> {
843 if !method.strict_wx_available {
844 return Err(
845 reedsolomon_rs::xor_jit::packed::PackedBuildError::InvalidInput(
846 "XOR-JIT method contract lacks strict W^X capability",
847 ),
848 );
849 }
850 let input_grouping = method.input_grouping();
851 let codebook = matches!(width, reedsolomon_rs::xor_jit::JitWidth::Avx2)
852 .then(|| {
853 reedsolomon_rs::xor_jit::packed::Avx2Codebook::build(repair_factors, codebook_limit)
854 })
855 .transpose();
856 let (storage, reserved_bytes) = match codebook {
857 Ok(Some(codebook)) => {
858 let retained_bytes = codebook.retained_bytes();
859 (JitDispatchStorage::RepairCodebook(codebook), retained_bytes)
860 }
861 Ok(None) | Err(reedsolomon_rs::xor_jit::packed::PackedBuildError::Resource { .. }) => {
862 let arena_limit =
865 reedsolomon_rs::xor_jit::packed::PackedJitBatch::active_arena_upper_bound(
866 width,
867 output_count,
868 input_grouping,
869 )
870 .ok_or(
871 reedsolomon_rs::xor_jit::packed::PackedBuildError::Resource {
872 requested_bytes: usize::MAX,
873 limit_bytes: available_bytes,
874 },
875 )?;
876 let reserved_bytes = arena_limit.checked_mul(2).ok_or(
877 reedsolomon_rs::xor_jit::packed::PackedBuildError::Resource {
878 requested_bytes: usize::MAX,
879 limit_bytes: available_bytes,
880 },
881 )?;
882 if reserved_bytes > available_bytes {
883 return Err(
884 reedsolomon_rs::xor_jit::packed::PackedBuildError::Resource {
885 requested_bytes: reserved_bytes,
886 limit_bytes: available_bytes,
887 },
888 );
889 }
890 (
891 JitDispatchStorage::ActiveArenas { arena_limit },
892 reserved_bytes,
893 )
894 }
895 Err(error) => return Err(error),
896 };
897
898 Ok(Self {
899 width,
900 input_grouping,
901 output_count,
902 storage,
903 reserved_bytes,
904 })
905 }
906
907 #[inline]
908 fn get<'a>(
909 &self,
910 batch: &'a reedsolomon_rs::xor_jit::packed::PackedJitBatch,
911 output: usize,
912 ) -> &'a reedsolomon_rs::xor_jit::packed::PackedJitCode {
913 batch
914 .row(output)
915 .expect("packed JIT row exists for every controller output")
916 }
917
918 fn build_active_batch(
919 &self,
920 set: &StreamBatchSet,
921 workspace: &mut reedsolomon_rs::xor_jit::packed::PackedJitWorkspace,
922 ) -> std::result::Result<
923 reedsolomon_rs::xor_jit::packed::PackedJitBatch,
924 reedsolomon_rs::xor_jit::packed::PackedBuildError,
925 > {
926 if set.input_grouping != self.input_grouping
927 || set.coefficients.len() != self.output_count.saturating_mul(self.input_grouping)
928 {
929 return Err(
930 reedsolomon_rs::xor_jit::packed::PackedBuildError::InvalidInput(
931 "controller coefficient batch shape does not match the JIT memo",
932 ),
933 );
934 }
935 let rows = set
936 .coefficients
937 .chunks_exact(self.input_grouping)
938 .collect::<Vec<_>>();
939 match &self.storage {
940 JitDispatchStorage::RepairCodebook(codebook) => codebook.build_batch(&rows),
941 JitDispatchStorage::ActiveArenas { arena_limit } => {
942 workspace.build(self.width, &rows, *arena_limit)
943 }
944 }
945 }
946
947 #[inline]
948 fn reserved_bytes(&self) -> usize {
949 self.reserved_bytes
950 }
951
952 fn storage_kind(&self) -> &'static str {
957 match self.storage {
958 JitDispatchStorage::RepairCodebook(_) => "codebook",
959 JitDispatchStorage::ActiveArenas { .. } => "active-arenas",
960 }
961 }
962}
963
964#[cfg(target_arch = "x86_64")]
967#[derive(Clone, Copy, Debug, Eq, PartialEq)]
968struct XorJitSelection {
969 width: reedsolomon_rs::xor_jit::JitWidth,
970 jit_method: CpuMethodContract,
971 baseline_method: CpuMethodContract,
973 output_count: usize,
974 word_count: usize,
975 workers: usize,
976 budget: usize,
977}
978
979#[cfg(target_arch = "x86_64")]
985#[derive(Clone, Copy, Debug, Eq, PartialEq)]
986struct XorJitBudgetDecision {
987 baseline_chunk_words: Option<usize>,
988 jit_chunk_words: Option<usize>,
989}
990
991#[cfg(target_arch = "x86_64")]
992impl XorJitBudgetDecision {
993 fn accepted(self) -> bool {
996 match (self.jit_chunk_words, self.baseline_chunk_words) {
997 (Some(jit), Some(baseline)) => jit >= baseline,
998 (Some(_), None) => true,
999 (None, _) => false,
1000 }
1001 }
1002}
1003
1004#[cfg(target_arch = "x86_64")]
1005impl XorJitSelection {
1006 fn budget_decision(self, jit_reserved_bytes: usize) -> XorJitBudgetDecision {
1013 XorJitBudgetDecision {
1014 baseline_chunk_words: largest_fitting_chunk_words(
1015 self.word_count,
1016 self.output_count,
1017 self.workers,
1018 self.baseline_method,
1019 self.baseline_method.staging_width(),
1020 self.budget,
1021 ),
1022 jit_chunk_words: largest_fitting_chunk_words(
1023 self.word_count,
1024 self.output_count,
1025 self.workers,
1026 self.jit_method,
1027 self.jit_method.staging_width(),
1028 self.budget.saturating_sub(jit_reserved_bytes),
1029 ),
1030 }
1031 }
1032
1033 fn select_memo(self, repair_factors: &[u16]) -> Result<Option<JitMemo>> {
1045 let Self {
1046 width,
1047 jit_method,
1048 output_count,
1049 word_count,
1050 workers,
1051 budget,
1052 ..
1053 } = self;
1054 let jit_staging_width = jit_method.staging_width();
1055 let minimum_controller_bytes =
1056 cpu_controller_plan(2, output_count, workers, jit_method, jit_staging_width)
1057 .buffer_accounting()
1058 .total_bytes;
1059 let Some(available_jit_bytes) = budget.checked_sub(minimum_controller_bytes) else {
1060 info!(
1061 ?width,
1062 minimum_controller_bytes,
1063 budget,
1064 "XOR-JIT controller base does not fit the repair memory limit; selecting the non-JIT kernel"
1065 );
1066 return Ok(None);
1067 };
1068 let full_controller_bytes = cpu_controller_plan(
1069 word_count.saturating_mul(2),
1070 output_count,
1071 workers,
1072 jit_method,
1073 jit_staging_width,
1074 )
1075 .buffer_accounting()
1076 .total_bytes;
1077 let codebook_limit = budget.saturating_sub(full_controller_bytes);
1078 let memo = match JitMemo::new(
1079 width,
1080 jit_method,
1081 output_count,
1082 repair_factors,
1083 codebook_limit,
1084 available_jit_bytes,
1085 ) {
1086 Ok(memo) => memo,
1087 Err(reedsolomon_rs::xor_jit::packed::PackedBuildError::Resource {
1088 requested_bytes,
1089 limit_bytes,
1090 }) => {
1091 info!(
1092 ?width,
1093 requested_bytes,
1094 limit_bytes,
1095 "XOR-JIT reservation does not fit the repair memory limit; selecting the non-JIT kernel"
1096 );
1097 return Ok(None);
1098 }
1099 Err(error) => {
1100 return Err(Par2Error::ReedSolomonError {
1101 reason: format!("XOR-JIT controller capacity setup failed: {error}"),
1102 });
1103 }
1104 };
1105 let reserved_bytes = memo.reserved_bytes();
1106 let storage = memo.storage_kind();
1107 let decision = self.budget_decision(reserved_bytes);
1108 if !decision.accepted() {
1109 info!(
1110 ?width,
1111 storage,
1112 reserved_bytes,
1113 jit_chunk_words = ?decision.jit_chunk_words,
1114 baseline_chunk_words = ?decision.baseline_chunk_words,
1115 "XOR-JIT reservation would shrink the repair chunk; selecting the non-JIT kernel"
1116 );
1117 return Ok(None);
1118 }
1119 info!(
1120 ?width,
1121 storage,
1122 reserved_bytes,
1123 jit_chunk_words = ?decision.jit_chunk_words,
1124 baseline_chunk_words = ?decision.baseline_chunk_words,
1125 "XOR-JIT reservation costs no repair chunk; selecting the XOR-JIT kernel"
1126 );
1127 Ok(Some(memo))
1128 }
1129}
1130
1131#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1132enum CpuKernelKind {
1133 Plain,
1134 Folded,
1135 #[cfg(target_arch = "aarch64")]
1171 NeonPacked,
1172 #[cfg(target_arch = "x86_64")]
1173 XorJit(reedsolomon_rs::xor_jit::JitWidth),
1174}
1175
1176impl CpuKernelKind {
1177 fn method(self) -> CpuMethodContract {
1180 match self {
1181 Self::Plain => CpuMethodContract {
1182 stride: 2,
1183 alignment: 64,
1184 ideal_input_multiple: 1,
1185 staging_multiple: 1,
1186 ideal_chunk_size: PLAIN_IDEAL_CHUNK_BYTES,
1187 checksum_width: 2,
1188 prefetch: CpuPrefetch {
1189 inputs_per_invoke: 0,
1190 input_distance_shift: 0,
1191 output: false,
1192 },
1193 strict_wx_available: false,
1194 },
1195 Self::Folded => CpuMethodContract {
1196 stride: crate::gf_simd::SPLIT_BLOCK_BYTES,
1197 alignment: 64,
1198 ideal_input_multiple: crate::gf_simd::FOLDED_GROUP,
1199 staging_multiple: crate::gf_simd::FOLDED_GROUP,
1200 ideal_chunk_size: FOLDED_IDEAL_CHUNK_BYTES,
1201 checksum_width: crate::gf_simd::SPLIT_BLOCK_BYTES,
1202 prefetch: CpuPrefetch {
1203 inputs_per_invoke: 0,
1204 input_distance_shift: 0,
1205 output: false,
1206 },
1207 strict_wx_available: false,
1208 },
1209 #[cfg(target_arch = "aarch64")]
1217 Self::NeonPacked => CpuMethodContract {
1218 stride: NEON_PACKED_BLOCK_BYTES,
1219 alignment: 64,
1220 ideal_input_multiple: 1,
1221 staging_multiple: 1,
1222 ideal_chunk_size: PLAIN_IDEAL_CHUNK_BYTES,
1223 checksum_width: NEON_PACKED_CHECKSUM_BYTES,
1224 prefetch: CpuPrefetch {
1225 inputs_per_invoke: 0,
1226 input_distance_shift: 0,
1227 output: false,
1228 },
1229 strict_wx_available: false,
1230 },
1231 #[cfg(target_arch = "x86_64")]
1232 Self::XorJit(width) => CpuMethodContract {
1233 stride: width.block_bytes(),
1234 alignment: 32,
1235 ideal_input_multiple: 1,
1236 staging_multiple: 1,
1237 ideal_chunk_size: XORJIT_AVX2_IDEAL_CHUNK_BYTES,
1238 checksum_width: width.block_bytes() / 16,
1239 prefetch: CpuPrefetch {
1240 inputs_per_invoke: 1,
1241 input_distance_shift: 1,
1242 output: true,
1243 },
1244 strict_wx_available: reedsolomon_rs::xor_jit::strict_wx_available(),
1245 },
1246 }
1247 }
1248}
1249
1250struct StreamBatchSet {
1254 bufs: Vec<Vec<u8>>,
1256 #[cfg_attr(not(target_arch = "x86_64"), allow(dead_code))]
1259 packed: Vec<StagingCell>,
1260 #[cfg_attr(not(target_arch = "x86_64"), allow(dead_code))]
1261 packed_stride: usize,
1262 staging: Vec<Vec<StagingCell>>,
1266 coefficients: Vec<u16>,
1269 input_grouping: usize,
1270 start: usize,
1271 len: usize,
1272}
1273
1274impl StreamBatchSet {
1275 fn new(
1276 max_byte_len: usize,
1277 input_grouping: usize,
1278 allocated_staging_width: usize,
1279 output_count: usize,
1280 folded: bool,
1281 xorjit: bool,
1282 gpu_staging: bool,
1283 ) -> Self {
1284 let groups = allocated_staging_width / crate::gf_simd::FOLDED_GROUP;
1285 let bufs = if gpu_staging || (!xorjit && !folded) {
1286 vec![vec![0u8; max_byte_len]; allocated_staging_width]
1287 } else {
1288 Vec::new()
1289 };
1290 let packed = if xorjit {
1291 staging_cells_for(allocated_staging_width.saturating_mul(max_byte_len))
1292 } else {
1293 Vec::new()
1294 };
1295 let staging = if folded {
1296 (0..groups)
1297 .map(|_| staging_cells_for(max_byte_len * crate::gf_simd::FOLDED_GROUP))
1298 .collect()
1299 } else {
1300 Vec::new()
1301 };
1302 Self {
1303 bufs,
1304 packed,
1305 packed_stride: max_byte_len,
1306 staging,
1307 coefficients: vec![0; output_count.saturating_mul(input_grouping)],
1308 input_grouping,
1309 start: 0,
1310 len: 0,
1311 }
1312 }
1313
1314 #[inline]
1315 fn coefficient(&self, output: usize, lane: usize) -> u16 {
1316 self.coefficients[output * self.input_grouping + lane]
1317 }
1318}
1319
1320pub(crate) struct StreamSourceReader {
1323 file_id: FileId,
1324 reader: Box<dyn FileRangeReader>,
1325}
1326
1327#[allow(clippy::too_many_arguments)]
1328pub(crate) fn read_stream_source_chunk(
1329 plan: &RepairPlan,
1330 par2_set: &Par2FileSet,
1331 file_access: &mut dyn FileAccess,
1332 recovery_files: &mut HashMap<PathBuf, File>,
1333 source_reader: &mut Option<StreamSourceReader>,
1334 available_inputs: usize,
1335 source_idx: usize,
1336 byte_start: usize,
1337 dst: &mut [u8],
1338) -> Result<()> {
1339 if source_idx < available_inputs {
1340 let global_idx = plan.available_input_global_indices[source_idx];
1341 let (file_id, local_slice) = plan.global_to_file[global_idx];
1342 let offset = local_slice as u64 * plan.slice_size + byte_start as u64;
1343 let file_length = par2_set
1344 .file_description(&file_id)
1345 .ok_or_else(|| Par2Error::ReedSolomonError {
1346 reason: format!("file description not found for {file_id}"),
1347 })?
1348 .length;
1349 let expected_len = file_length.saturating_sub(offset).min(dst.len() as u64) as usize;
1350 if source_reader
1351 .as_ref()
1352 .is_none_or(|open| open.file_id != file_id)
1353 {
1354 *source_reader = file_access
1355 .open_range_reader(&file_id)
1356 .map_err(Par2Error::Io)?
1357 .map(|reader| StreamSourceReader { file_id, reader });
1358 }
1359 if let Some(open) = source_reader.as_mut() {
1360 open.reader
1361 .seek(SeekFrom::Start(offset))
1362 .and_then(|_| open.reader.read_exact(&mut dst[..expected_len]))
1363 .map_err(Par2Error::Io)?;
1364 } else {
1365 let mut read_len = 0usize;
1366 while read_len < expected_len {
1367 let read = file_access
1368 .read_file_range_into(
1369 &file_id,
1370 offset + read_len as u64,
1371 &mut dst[read_len..expected_len],
1372 )
1373 .map_err(Par2Error::Io)?;
1374 if read == 0 {
1375 return Err(Par2Error::Io(std::io::Error::new(
1376 std::io::ErrorKind::UnexpectedEof,
1377 format!("source slice {file_id}:{local_slice} ended during repair"),
1378 )));
1379 }
1380 read_len += read;
1381 }
1382 }
1383 dst[expected_len..].fill(0);
1384 } else {
1385 *source_reader = None;
1386 let exp = plan.recovery_exponents[source_idx - available_inputs];
1387 let rs = par2_set
1388 .recovery_slices
1389 .get(&exp)
1390 .ok_or_else(|| Par2Error::ReedSolomonError {
1391 reason: format!("recovery block with exponent {exp} not found"),
1392 })?;
1393 fill_recovery_chunk(&rs.data, byte_start, dst, recovery_files).map_err(Par2Error::Io)?;
1394 }
1395 Ok(())
1396}
1397
1398fn prepare_stream_source(
1399 set: &mut StreamBatchSet,
1400 lane: usize,
1401 source: &[u8],
1402 aligned_len: usize,
1403 #[cfg(target_arch = "x86_64")] chunk_len: usize,
1404 #[cfg(not(target_arch = "x86_64"))] _chunk_len: usize,
1405 kernel: CpuKernelKind,
1406) {
1407 match kernel {
1408 #[cfg(target_arch = "x86_64")]
1409 CpuKernelKind::XorJit(width) => {
1410 let block = width.block_bytes();
1411 debug_assert_eq!(aligned_len % block, 0);
1412 debug_assert_eq!(chunk_len % block, 0);
1413 let num_chunks = aligned_len.div_ceil(chunk_len);
1414 let packed = staging_bytes_mut(&mut set.packed);
1415 for chunk in 0..num_chunks {
1416 let source_start = chunk * chunk_len;
1417 let source_len = (aligned_len - source_start).min(chunk_len);
1418 let lane_start = chunk
1419 .saturating_mul(set.input_grouping)
1420 .saturating_mul(chunk_len)
1421 .saturating_add(lane.saturating_mul(source_len));
1422 packed[lane_start..lane_start + source_len].fill(0);
1423 for offset in (0..source_len).step_by(block) {
1424 unsafe {
1428 width.prepare_block(
1429 &source[source_start + offset..source_start + offset + block],
1430 &mut packed[lane_start + offset..lane_start + offset + block],
1431 );
1432 }
1433 }
1434 }
1435 }
1436 CpuKernelKind::Folded => {
1437 let group = lane / crate::gf_simd::FOLDED_GROUP;
1438 let group_lane = lane % crate::gf_simd::FOLDED_GROUP;
1439 crate::gf_simd::split_encode_scatter(
1440 &source[..aligned_len],
1441 staging_bytes_mut(&mut set.staging[group]),
1442 group_lane,
1443 );
1444 }
1445 #[cfg(target_arch = "aarch64")]
1449 CpuKernelKind::NeonPacked => {}
1450 CpuKernelKind::Plain => {}
1451 }
1452 if !set.bufs.is_empty() {
1453 set.bufs[lane][..aligned_len].copy_from_slice(&source[..aligned_len]);
1454 }
1455}
1456
1457#[inline]
1458fn gf16_mul2(value: u16) -> u16 {
1459 (value << 1) ^ if value & 0x8000 != 0 { 0x100b } else { 0 }
1460}
1461
1462#[inline]
1471fn gf16_mul2_x4(v: u64) -> u64 {
1472 const LANE_HI: u64 = 0x8000_8000_8000_8000;
1473 const LANE_LO: u64 = 0x7fff_7fff_7fff_7fff;
1474 let carry = (v & LANE_HI) >> 15;
1475 ((v & LANE_LO) << 1) ^ carry.wrapping_mul(0x100b)
1476}
1477
1478#[cfg(target_arch = "aarch64")]
1590mod parpar_neon_checksum {
1591 use std::arch::aarch64::*;
1592
1593 const POLY: i16 = 0x100b;
1596
1597 #[inline(always)]
1602 unsafe fn gf16_vec_mul2_neon(v: uint8x16_t) -> uint8x16_t {
1603 unsafe {
1604 let vs = vreinterpretq_s16_u8(v);
1607 vreinterpretq_u8_s16(veorq_s16(
1608 vaddq_s16(vs, vs),
1609 vandq_s16(vdupq_n_s16(POLY), vshrq_n_s16::<15>(vs)),
1610 ))
1611 }
1612 }
1613
1614 #[inline(always)]
1621 unsafe fn checksum_block<const PLANES: usize>(
1622 acc: &mut [uint8x16_t; PLANES],
1623 src: *const u8,
1624 block_len: usize,
1625 ) {
1626 unsafe {
1627 for lane in acc.iter_mut() {
1628 *lane = gf16_vec_mul2_neon(*lane);
1629 }
1630 let mut i = 0usize;
1631 while i < block_len {
1632 for (plane, lane) in acc.iter_mut().enumerate() {
1633 *lane = veorq_u8(*lane, vld1q_u8(src.add(i + plane * 16)));
1634 }
1635 i += PLANES * 16;
1636 }
1637 }
1638 }
1639
1640 #[inline(always)]
1647 unsafe fn fold_planes<const PLANES: usize>(data: &[u8], block_len: usize, out: &mut [u8]) {
1648 unsafe {
1649 let mut acc = [vdupq_n_u8(0); PLANES];
1650 let mut src = data.as_ptr();
1651 for _ in 0..(data.len() / block_len) {
1652 checksum_block::<PLANES>(&mut acc, src, block_len);
1653 src = src.add(block_len);
1654 }
1655 for (plane, lane) in acc.iter().enumerate() {
1656 vst1q_u8(out.as_mut_ptr().add(plane * 16), *lane);
1657 }
1658 }
1659 }
1660
1661 pub(super) fn fold(data: &[u8], block_len: usize, checksum_width: usize) -> Option<[u8; 64]> {
1664 if checksum_width == 0
1665 || !checksum_width.is_multiple_of(16)
1666 || checksum_width > 64
1667 || block_len == 0
1668 || !block_len.is_multiple_of(checksum_width)
1669 {
1670 return None;
1671 }
1672 let mut out = [0u8; 64];
1673 unsafe {
1678 match checksum_width / 16 {
1679 1 => fold_planes::<1>(data, block_len, &mut out),
1680 2 => fold_planes::<2>(data, block_len, &mut out),
1681 3 => fold_planes::<3>(data, block_len, &mut out),
1682 4 => fold_planes::<4>(data, block_len, &mut out),
1683 _ => return None,
1684 }
1685 }
1686 Some(out)
1687 }
1688
1689 #[cfg_attr(not(test), allow(dead_code))]
1695 pub(super) fn update_block(checksum: &mut [u8], block: &[u8]) -> bool {
1696 let width = checksum.len();
1697 if width == 0
1698 || !width.is_multiple_of(16)
1699 || width > 64
1700 || block.is_empty()
1701 || !block.len().is_multiple_of(width)
1702 {
1703 return false;
1704 }
1705 unsafe {
1708 match width / 16 {
1709 1 => update_planes::<1>(checksum, block),
1710 2 => update_planes::<2>(checksum, block),
1711 3 => update_planes::<3>(checksum, block),
1712 4 => update_planes::<4>(checksum, block),
1713 _ => return false,
1714 }
1715 }
1716 true
1717 }
1718
1719 #[inline(always)]
1723 unsafe fn update_planes<const PLANES: usize>(checksum: &mut [u8], block: &[u8]) {
1724 unsafe {
1725 let mut acc = [vdupq_n_u8(0); PLANES];
1726 for (plane, lane) in acc.iter_mut().enumerate() {
1727 *lane = vld1q_u8(checksum.as_ptr().add(plane * 16));
1728 }
1729 checksum_block::<PLANES>(&mut acc, block.as_ptr(), block.len());
1730 for (plane, lane) in acc.iter().enumerate() {
1731 vst1q_u8(checksum.as_mut_ptr().add(plane * 16), *lane);
1732 }
1733 }
1734 }
1735
1736 pub(super) fn enabled() -> bool {
1739 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1740 *ENABLED.get_or_init(|| std::env::var_os("WEAVER_PAR2_CKSUM_NEON").is_none_or(|v| v != "0"))
1741 }
1742}
1743
1744fn update_packed_checksum(checksum: &mut [u8], block: &[u8]) {
1758 debug_assert_eq!(checksum.len() % 2, 0);
1759 debug_assert_eq!(block.len() % checksum.len(), 0);
1760 let width = checksum.len();
1761
1762 if width == 2 {
1766 let mut acc = gf16_mul2(u16::from_le_bytes([checksum[0], checksum[1]]));
1767 for region in block.chunks_exact(2) {
1768 acc ^= u16::from_le_bytes([region[0], region[1]]);
1769 }
1770 checksum.copy_from_slice(&acc.to_le_bytes());
1771 return;
1772 }
1773 if width <= 8 {
1774 let mut buf = [0u8; 8];
1775 buf[..width].copy_from_slice(checksum);
1776 let mut acc = gf16_mul2_x4(u64::from_le_bytes(buf));
1777 for region in block.chunks_exact(width) {
1778 let mut r = [0u8; 8];
1779 r[..width].copy_from_slice(region);
1780 acc ^= u64::from_le_bytes(r);
1781 }
1782 checksum.copy_from_slice(&acc.to_le_bytes()[..width]);
1783 return;
1784 }
1785
1786 let mut lane = 0usize;
1788 while lane + 8 <= width {
1789 let v = u64::from_le_bytes(checksum[lane..lane + 8].try_into().unwrap());
1790 checksum[lane..lane + 8].copy_from_slice(&gf16_mul2_x4(v).to_le_bytes());
1791 lane += 8;
1792 }
1793 while lane < width {
1794 let v = u16::from_le_bytes([checksum[lane], checksum[lane + 1]]);
1795 checksum[lane..lane + 2].copy_from_slice(&gf16_mul2(v).to_le_bytes());
1796 lane += 2;
1797 }
1798
1799 for region in block.chunks_exact(width) {
1801 let mut i = 0usize;
1802 while i + 8 <= width {
1803 let a = u64::from_le_bytes(checksum[i..i + 8].try_into().unwrap());
1804 let b = u64::from_le_bytes(region[i..i + 8].try_into().unwrap());
1805 checksum[i..i + 8].copy_from_slice(&(a ^ b).to_le_bytes());
1806 i += 8;
1807 }
1808 while i < width {
1809 checksum[i] ^= region[i];
1810 i += 1;
1811 }
1812 }
1813}
1814
1815fn fold_packed_checksum(data: &[u8], block_len: usize, checksum_width: usize) -> [u8; 64] {
1823 #[cfg(target_arch = "aarch64")]
1827 if parpar_neon_checksum::enabled()
1828 && let Some(out) = parpar_neon_checksum::fold(data, block_len, checksum_width)
1829 {
1830 return out;
1831 }
1832 let mut out = [0u8; 64];
1833 if checksum_width == 2 {
1839 let mut acc = 0u16;
1840 for block in data.chunks_exact(block_len) {
1841 let mut folded = 0u16;
1842 for region in block.chunks_exact(2) {
1843 folded ^= u16::from_le_bytes([region[0], region[1]]);
1844 }
1845 acc = gf16_mul2(acc) ^ folded;
1846 }
1847 out[..2].copy_from_slice(&acc.to_le_bytes());
1848 return out;
1849 }
1850 if checksum_width == 4 {
1851 let mut acc = 0u64;
1852 for block in data.chunks_exact(block_len) {
1853 let mut folded = 0u64;
1854 for region in block.chunks_exact(4) {
1855 folded ^= u32::from_le_bytes(region.try_into().unwrap()) as u64;
1856 }
1857 acc = gf16_mul2_x4(acc) ^ folded;
1858 }
1859 out[..4].copy_from_slice(&acc.to_le_bytes()[..4]);
1860 return out;
1861 }
1862 if checksum_width == 8 {
1863 let mut acc = 0u64;
1864 for block in data.chunks_exact(block_len) {
1865 let mut folded = 0u64;
1866 for region in block.chunks_exact(8) {
1867 folded ^= u64::from_le_bytes(region.try_into().unwrap());
1868 }
1869 acc = gf16_mul2_x4(acc) ^ folded;
1870 }
1871 out[..8].copy_from_slice(&acc.to_le_bytes());
1872 return out;
1873 }
1874 for block in data.chunks_exact(block_len) {
1875 update_packed_checksum(&mut out[..checksum_width], block);
1876 }
1877 out
1878}
1879
1880fn write_packed_checksum(
1881 buffer: &mut [u8],
1882 data_len: usize,
1883 block_len: usize,
1884 checksum_width: usize,
1885) {
1886 debug_assert_eq!(data_len % block_len, 0);
1887 debug_assert!(checksum_width <= 64);
1888 debug_assert!(checksum_width <= block_len);
1889 debug_assert_eq!(block_len % checksum_width, 0);
1890 let (data, checksum_block) = buffer.split_at_mut(data_len);
1891 let checksum_block = &mut checksum_block[..block_len];
1892 checksum_block.fill(0);
1893 let folded = fold_packed_checksum(data, block_len, checksum_width);
1894 checksum_block[..checksum_width].copy_from_slice(&folded[..checksum_width]);
1895}
1896
1897fn packed_checksum_matches(
1898 buffer: &[u8],
1899 data_len: usize,
1900 block_len: usize,
1901 checksum_width: usize,
1902) -> bool {
1903 debug_assert_eq!(data_len % block_len, 0);
1904 debug_assert!(checksum_width <= 64);
1905 debug_assert!(checksum_width <= block_len);
1906 let (data, checksum_block) = buffer.split_at(data_len);
1907 let checksum_block = &checksum_block[..block_len];
1908 let expected = fold_packed_checksum(data, block_len, checksum_width);
1909 checksum_block[..checksum_width] == expected[..checksum_width]
1910 && checksum_block[checksum_width..]
1911 .iter()
1912 .all(|byte| *byte == 0)
1913}
1914
1915struct PrepareBatch {
1916 set: StreamBatchSet,
1917 aligned_len: usize,
1918 chunk_len: usize,
1919 layout: Option<Arc<ControllerLayout>>,
1920}
1921
1922struct PreparedControllerBatch {
1923 set: StreamBatchSet,
1924}
1925
1926struct SubmittedControllerBatch<'a> {
1927 batch: crate::cpu_repair_controller::InputBatch,
1928 ticket: CpuComputeTicket<'a>,
1929}
1930
1931#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1932enum OutputEncoding {
1933 Plain,
1934 CpuEncoded,
1935}
1936
1937#[derive(Clone, Copy)]
1938enum OutputTransferSource {
1939 PlainContiguous(usize),
1940 CpuEncodedChunkInterleaved {
1941 base: usize,
1942 output: usize,
1943 output_count: usize,
1944 chunk_len: usize,
1945 },
1946}
1947
1948impl OutputTransferSource {
1949 fn encoding(self) -> OutputEncoding {
1950 match self {
1951 Self::PlainContiguous(_) => OutputEncoding::Plain,
1952 Self::CpuEncodedChunkInterleaved { .. } => OutputEncoding::CpuEncoded,
1953 }
1954 }
1955}
1956
1957enum OutputTransferLayout<'a> {
1958 PlainContiguous(&'a [usize]),
1959 CpuEncodedChunkInterleaved {
1960 base: usize,
1961 output_count: usize,
1962 chunk_len: usize,
1963 },
1964}
1965
1966impl OutputTransferLayout<'_> {
1967 fn len(&self) -> usize {
1968 match self {
1969 Self::PlainContiguous(outputs) => outputs.len(),
1970 Self::CpuEncodedChunkInterleaved { output_count, .. } => *output_count,
1971 }
1972 }
1973
1974 fn source(&self, output: usize) -> OutputTransferSource {
1975 match self {
1976 Self::PlainContiguous(outputs) => {
1977 OutputTransferSource::PlainContiguous(outputs[output])
1978 }
1979 Self::CpuEncodedChunkInterleaved {
1980 base,
1981 output_count,
1982 chunk_len,
1983 } => OutputTransferSource::CpuEncodedChunkInterleaved {
1984 base: *base,
1985 output,
1986 output_count: *output_count,
1987 chunk_len: *chunk_len,
1988 },
1989 }
1990 }
1991}
1992
1993enum PreparationMessage {
1994 Begin(PrepareBatch),
1995 Input {
1996 lane: usize,
1997 coefficients: Vec<u16>,
1998 buffer: TransferBuffer,
1999 submitted: Option<crate::cpu_repair_controller::InputBatch>,
2000 },
2001 Flush {
2002 batch: crate::cpu_repair_controller::InputBatch,
2003 },
2004 #[cfg(target_arch = "x86_64")]
2005 RecycleJit {
2006 staging_area: usize,
2007 batch: reedsolomon_rs::xor_jit::packed::PackedJitBatch,
2008 },
2009 FinishOutput {
2010 index: usize,
2011 source: OutputTransferSource,
2012 aligned_len: usize,
2013 buffer: TransferBuffer,
2014 },
2015}
2016
2017struct TransferBuffer {
2021 slot: usize,
2022 bytes: Vec<u8>,
2023}
2024
2025struct FinishedOutput {
2026 index: usize,
2027 buffer: TransferBuffer,
2028 checksum_valid: bool,
2029 elapsed: Duration,
2030}
2031
2032struct CpuInputPreparer<'a> {
2033 command_tx: std::sync::mpsc::SyncSender<PreparationMessage>,
2034 complete_rx: std::sync::mpsc::Receiver<TransferBuffer>,
2035 prepared_rx: std::sync::mpsc::Receiver<PreparedControllerBatch>,
2036 submitted_rx:
2037 std::sync::mpsc::Receiver<std::result::Result<SubmittedControllerBatch<'a>, String>>,
2038 finished_rx: std::sync::mpsc::Receiver<FinishedOutput>,
2039 transfer_buffers: [Option<TransferBuffer>; 2],
2040 transfer_buffer_len: usize,
2041 #[cfg(target_family = "wasm")]
2045 inline: Option<std::cell::RefCell<InlineControllerWorkers<'a>>>,
2046}
2047
2048impl<'a> CpuInputPreparer<'a> {
2049 #[inline(always)]
2055 fn pump(&self) {
2056 #[cfg(target_family = "wasm")]
2057 if let Some(inline) = self.inline.as_ref() {
2058 inline.borrow_mut().run();
2059 }
2060 }
2061
2062 #[allow(clippy::result_large_err)]
2072 fn send_command(
2073 &self,
2074 message: PreparationMessage,
2075 ) -> std::result::Result<(), std::sync::mpsc::SendError<PreparationMessage>> {
2076 let result = self.command_tx.send(message);
2077 self.pump();
2078 result
2079 }
2080
2081 #[cfg(target_family = "wasm")]
2085 fn inline_preparation_panicked(&self) -> bool {
2086 self.inline
2087 .as_ref()
2088 .is_some_and(|inline| inline.borrow().preparation_panicked)
2089 }
2090
2091 fn take_transfer_buffer(
2092 &mut self,
2093 cancel: Option<&CancellationToken>,
2094 ) -> Result<TransferBuffer> {
2095 if let Some(buffer) = self.transfer_buffers.iter_mut().find_map(Option::take) {
2096 return Ok(buffer);
2097 }
2098
2099 self.pump();
2100 let buffer = recv_with_cancel(
2101 &self.complete_rx,
2102 cancel,
2103 "CPU repair preparation worker stopped unexpectedly",
2104 )?;
2105 self.validate_transfer_buffer(&buffer)?;
2106 Ok(buffer)
2107 }
2108
2109 fn return_transfer_buffer(&mut self, buffer: TransferBuffer) -> Result<()> {
2110 self.validate_transfer_buffer(&buffer)?;
2111 let slot = buffer.slot;
2112 let Some(destination) = self.transfer_buffers.get_mut(slot) else {
2113 return Err(Par2Error::ReedSolomonError {
2114 reason: format!("CPU repair transfer buffer returned unknown slot {slot}"),
2115 });
2116 };
2117 if destination.is_some() {
2118 return Err(Par2Error::ReedSolomonError {
2119 reason: format!("CPU repair transfer buffer slot {slot} was returned twice"),
2120 });
2121 }
2122 *destination = Some(buffer);
2123 Ok(())
2124 }
2125
2126 fn restore_transfer_buffers(&mut self, cancel: Option<&CancellationToken>) -> Result<()> {
2127 while self.transfer_buffers.iter().any(Option::is_none) {
2128 self.pump();
2129 let buffer = recv_with_cancel(
2130 &self.complete_rx,
2131 cancel,
2132 "CPU repair preparation worker stopped unexpectedly",
2133 )?;
2134 self.return_transfer_buffer(buffer)?;
2135 }
2136 Ok(())
2137 }
2138
2139 fn validate_transfer_buffer(&self, buffer: &TransferBuffer) -> Result<()> {
2140 if buffer.slot >= self.transfer_buffers.len() {
2141 return Err(Par2Error::ReedSolomonError {
2142 reason: format!(
2143 "CPU repair transfer buffer returned unknown slot {}",
2144 buffer.slot
2145 ),
2146 });
2147 }
2148 if buffer.bytes.len() != self.transfer_buffer_len {
2149 return Err(Par2Error::ReedSolomonError {
2150 reason: format!(
2151 "CPU repair transfer buffer slot {} has {} bytes; expected {}",
2152 buffer.slot,
2153 buffer.bytes.len(),
2154 self.transfer_buffer_len
2155 ),
2156 });
2157 }
2158 Ok(())
2159 }
2160}
2161
2162fn finalize_output_bytes(
2163 kernel: CpuKernelKind,
2164 method: CpuMethodContract,
2165 encoding: OutputEncoding,
2166 buffer: &mut [u8],
2167) -> bool {
2168 match encoding {
2169 OutputEncoding::Plain => return true,
2170 OutputEncoding::CpuEncoded => {}
2171 }
2172
2173 match kernel {
2174 #[cfg(target_arch = "x86_64")]
2175 CpuKernelKind::XorJit(width) => {
2176 let block = width.block_bytes();
2177 debug_assert!(buffer.len().is_multiple_of(block));
2178 for bytes in buffer.chunks_exact_mut(block) {
2179 unsafe { width.finish_block(bytes) };
2182 }
2183 }
2184 CpuKernelKind::Folded => crate::gf_simd::altmap_decode(buffer),
2185 #[cfg(target_arch = "aarch64")]
2187 CpuKernelKind::NeonPacked => {}
2188 CpuKernelKind::Plain => {}
2189 }
2190
2191 packed_checksum_matches(
2192 buffer,
2193 buffer.len() - method.stride,
2194 method.stride,
2195 method.checksum_width,
2196 )
2197}
2198
2199struct PreparationWorker<'a> {
2204 complete_tx: std::sync::mpsc::SyncSender<TransferBuffer>,
2205 prepared_tx: std::sync::mpsc::SyncSender<PreparedControllerBatch>,
2206 submitted_tx:
2207 std::sync::mpsc::SyncSender<std::result::Result<SubmittedControllerBatch<'a>, String>>,
2208 finished_tx: std::sync::mpsc::SyncSender<FinishedOutput>,
2209 kernel: CpuKernelKind,
2210 method: CpuMethodContract,
2211 output_base: usize,
2212 output_count: usize,
2213 memo: &'a PreparedFactorMemo,
2214 #[cfg(target_arch = "x86_64")]
2215 jit_memo: Option<&'a JitMemo>,
2216 timings: &'a CpuControllerTimings,
2217 compute_submitter: CpuComputeSubmitter<'a>,
2218 trace: ControllerExecutionTrace,
2219 active: Option<PrepareBatch>,
2220 #[cfg(target_arch = "x86_64")]
2221 jit_workspaces: [reedsolomon_rs::xor_jit::packed::PackedJitWorkspace; 2],
2222}
2223
2224impl<'a> PreparationWorker<'a> {
2225 #[allow(clippy::too_many_arguments)]
2226 fn new(
2227 complete_tx: std::sync::mpsc::SyncSender<TransferBuffer>,
2228 prepared_tx: std::sync::mpsc::SyncSender<PreparedControllerBatch>,
2229 submitted_tx: std::sync::mpsc::SyncSender<
2230 std::result::Result<SubmittedControllerBatch<'a>, String>,
2231 >,
2232 finished_tx: std::sync::mpsc::SyncSender<FinishedOutput>,
2233 kernel: CpuKernelKind,
2234 method: CpuMethodContract,
2235 output_base: usize,
2236 output_count: usize,
2237 memo: &'a PreparedFactorMemo,
2238 #[cfg(target_arch = "x86_64")] jit_memo: Option<&'a JitMemo>,
2239 timings: &'a CpuControllerTimings,
2240 compute_submitter: CpuComputeSubmitter<'a>,
2241 trace: ControllerExecutionTrace,
2242 ) -> Self {
2243 Self {
2244 complete_tx,
2245 prepared_tx,
2246 submitted_tx,
2247 finished_tx,
2248 kernel,
2249 method,
2250 output_base,
2251 output_count,
2252 memo,
2253 #[cfg(target_arch = "x86_64")]
2254 jit_memo,
2255 timings,
2256 compute_submitter,
2257 trace,
2258 active: None,
2259 #[cfg(target_arch = "x86_64")]
2260 jit_workspaces: [
2261 reedsolomon_rs::xor_jit::packed::PackedJitWorkspace::default(),
2262 reedsolomon_rs::xor_jit::packed::PackedJitWorkspace::default(),
2263 ],
2264 }
2265 }
2266
2267 fn step(&mut self, message: PreparationMessage) -> ControlFlow<()> {
2270 match message {
2271 PreparationMessage::Begin(batch) => {
2272 debug_assert!(self.active.is_none());
2273 self.active = Some(batch);
2274 }
2275 PreparationMessage::Input {
2276 lane,
2277 coefficients,
2278 mut buffer,
2279 submitted,
2280 } => {
2281 let Some(batch) = self.active.as_mut() else {
2282 self.trace.record(ControllerExecutionEvent::Failed {
2283 phase: ControllerFailurePhase::Prepare,
2284 });
2285 return ControlFlow::Break(());
2286 };
2287 if coefficients.len() != batch.set.coefficients.len() / batch.set.input_grouping {
2288 self.trace.record(ControllerExecutionEvent::Failed {
2289 phase: ControllerFailurePhase::Prepare,
2290 });
2291 return ControlFlow::Break(());
2292 }
2293 for (output, coefficient) in coefficients.into_iter().enumerate() {
2294 batch.set.coefficients[output * batch.set.input_grouping + lane] = coefficient;
2295 }
2296 let checksum_block_len = self.method.stride;
2297 write_packed_checksum(
2298 &mut buffer.bytes[..batch.aligned_len],
2299 batch.aligned_len - checksum_block_len,
2300 checksum_block_len,
2301 self.method.checksum_width,
2302 );
2303 prepare_stream_source(
2304 &mut batch.set,
2305 lane,
2306 &buffer.bytes,
2307 batch.aligned_len,
2308 batch.chunk_len,
2309 self.kernel,
2310 );
2311 let mut stop_after_buffer = false;
2312 if let Some(submitted) = submitted {
2313 let mut batch = self.active.take().expect("active preparation batch");
2314 if submitted.input_len != lane + 1
2315 || submitted.staging_area >= 2
2316 || submitted.input_start != batch.set.start
2317 {
2318 self.trace.record(ControllerExecutionEvent::Failed {
2319 phase: ControllerFailurePhase::Prepare,
2320 });
2321 return ControlFlow::Break(());
2322 }
2323 batch.set.len = submitted.input_len;
2324 let submitted_info = submitted;
2325 if batch.layout.is_some() {
2326 let submitted = submit_prepared_controller_batch(
2327 submitted,
2328 batch,
2329 self.output_base,
2330 self.output_count,
2331 self.memo,
2332 #[cfg(target_arch = "x86_64")]
2333 self.jit_memo,
2334 #[cfg(target_arch = "x86_64")]
2335 &mut self.jit_workspaces,
2336 self.method,
2337 self.timings,
2338 &self.trace,
2339 &mut self.compute_submitter,
2340 );
2341 stop_after_buffer = submitted.is_err();
2342 if self.submitted_tx.send(submitted).is_err() {
2343 self.trace.record(ControllerExecutionEvent::Failed {
2344 phase: ControllerFailurePhase::Prepare,
2345 });
2346 return ControlFlow::Break(());
2347 }
2348 } else if self
2349 .prepared_tx
2350 .send(PreparedControllerBatch { set: batch.set })
2351 .is_err()
2352 {
2353 self.trace.record(ControllerExecutionEvent::Failed {
2354 phase: ControllerFailurePhase::Prepare,
2355 });
2356 return ControlFlow::Break(());
2357 }
2358 if !stop_after_buffer {
2359 self.trace
2360 .record(ControllerExecutionEvent::PreparationCompleted {
2361 staging_area: submitted_info.staging_area,
2362 input_len: submitted_info.input_len,
2363 });
2364 }
2365 }
2366 if self.complete_tx.send(buffer).is_err() {
2369 self.trace.record(ControllerExecutionEvent::Failed {
2370 phase: ControllerFailurePhase::Prepare,
2371 });
2372 return ControlFlow::Break(());
2373 }
2374 if stop_after_buffer {
2375 return ControlFlow::Break(());
2376 }
2377 }
2378 PreparationMessage::Flush { batch: submitted } => {
2379 let Some(mut batch) = self.active.take() else {
2380 self.trace.record(ControllerExecutionEvent::Failed {
2381 phase: ControllerFailurePhase::Prepare,
2382 });
2383 return ControlFlow::Break(());
2384 };
2385 if submitted.input_len == 0
2386 || submitted.input_len > batch.set.input_grouping
2387 || submitted.staging_area >= 2
2388 || submitted.input_start != batch.set.start
2389 {
2390 self.trace.record(ControllerExecutionEvent::Failed {
2391 phase: ControllerFailurePhase::Prepare,
2392 });
2393 return ControlFlow::Break(());
2394 }
2395 batch.set.len = submitted.input_len;
2396 let submitted_info = submitted;
2397 let submitted = submit_prepared_controller_batch(
2398 submitted,
2399 batch,
2400 self.output_base,
2401 self.output_count,
2402 self.memo,
2403 #[cfg(target_arch = "x86_64")]
2404 self.jit_memo,
2405 #[cfg(target_arch = "x86_64")]
2406 &mut self.jit_workspaces,
2407 self.method,
2408 self.timings,
2409 &self.trace,
2410 &mut self.compute_submitter,
2411 );
2412 let submit_failed = submitted.is_err();
2413 if self.submitted_tx.send(submitted).is_err() {
2414 self.trace.record(ControllerExecutionEvent::Failed {
2415 phase: ControllerFailurePhase::Prepare,
2416 });
2417 return ControlFlow::Break(());
2418 }
2419 if submit_failed {
2420 return ControlFlow::Break(());
2421 }
2422 self.trace
2423 .record(ControllerExecutionEvent::PreparationCompleted {
2424 staging_area: submitted_info.staging_area,
2425 input_len: submitted_info.input_len,
2426 });
2427 }
2428 #[cfg(target_arch = "x86_64")]
2429 PreparationMessage::RecycleJit {
2430 staging_area,
2431 batch,
2432 } => {
2433 if staging_area >= self.jit_workspaces.len()
2434 || self.jit_workspaces[staging_area].recycle(batch).is_err()
2435 {
2436 self.trace.record(ControllerExecutionEvent::Failed {
2437 phase: ControllerFailurePhase::Compute,
2438 });
2439 return ControlFlow::Break(());
2440 }
2441 }
2442 PreparationMessage::FinishOutput {
2443 index,
2444 source,
2445 aligned_len,
2446 mut buffer,
2447 } => {
2448 let started = Instant::now();
2449 debug_assert!(self.active.is_none());
2450 let encoding = source.encoding();
2451 match source {
2454 OutputTransferSource::PlainContiguous(source) => {
2455 let source =
2456 unsafe { std::slice::from_raw_parts(source as *const u8, aligned_len) };
2457 buffer.bytes[..aligned_len].copy_from_slice(source);
2458 }
2459 OutputTransferSource::CpuEncodedChunkInterleaved {
2460 base,
2461 output,
2462 output_count,
2463 chunk_len,
2464 } => {
2465 let source = unsafe {
2466 std::slice::from_raw_parts(
2467 base as *const u8,
2468 aligned_len.saturating_mul(output_count),
2469 )
2470 };
2471 for chunk_start in (0..aligned_len).step_by(chunk_len) {
2472 let len = (aligned_len - chunk_start).min(chunk_len);
2473 let source_start = chunk_start * output_count + output * len;
2474 buffer.bytes[chunk_start..chunk_start + len]
2475 .copy_from_slice(&source[source_start..source_start + len]);
2476 }
2477 }
2478 }
2479 let checksum_valid = finalize_output_bytes(
2480 self.kernel,
2481 self.method,
2482 encoding,
2483 &mut buffer.bytes[..aligned_len],
2484 );
2485 if self
2486 .finished_tx
2487 .send(FinishedOutput {
2488 index,
2489 buffer,
2490 checksum_valid,
2491 elapsed: started.elapsed(),
2492 })
2493 .is_err()
2494 {
2495 self.trace.record(ControllerExecutionEvent::Failed {
2496 phase: ControllerFailurePhase::OutputTransfer,
2497 });
2498 return ControlFlow::Break(());
2499 }
2500 }
2501 }
2502 ControlFlow::Continue(())
2503 }
2504}
2505
2506#[allow(clippy::too_many_arguments)]
2507fn run_preparation_worker<'a>(
2508 command_rx: std::sync::mpsc::Receiver<PreparationMessage>,
2509 complete_tx: std::sync::mpsc::SyncSender<TransferBuffer>,
2510 prepared_tx: std::sync::mpsc::SyncSender<PreparedControllerBatch>,
2511 submitted_tx: std::sync::mpsc::SyncSender<
2512 std::result::Result<SubmittedControllerBatch<'a>, String>,
2513 >,
2514 finished_tx: std::sync::mpsc::SyncSender<FinishedOutput>,
2515 kernel: CpuKernelKind,
2516 method: CpuMethodContract,
2517 output_base: usize,
2518 output_count: usize,
2519 memo: &'a PreparedFactorMemo,
2520 #[cfg(target_arch = "x86_64")] jit_memo: Option<&'a JitMemo>,
2521 timings: &'a CpuControllerTimings,
2522 compute_submitter: CpuComputeSubmitter<'a>,
2523 trace: ControllerExecutionTrace,
2524) {
2525 let mut worker = PreparationWorker::new(
2526 complete_tx,
2527 prepared_tx,
2528 submitted_tx,
2529 finished_tx,
2530 kernel,
2531 method,
2532 output_base,
2533 output_count,
2534 memo,
2535 #[cfg(target_arch = "x86_64")]
2536 jit_memo,
2537 timings,
2538 compute_submitter,
2539 trace,
2540 );
2541 while let Ok(message) = command_rx.recv() {
2542 if worker.step(message).is_break() {
2543 break;
2544 }
2545 }
2546}
2547
2548#[allow(clippy::too_many_arguments)]
2552fn run_guarded_preparation_worker<'a>(
2553 command_rx: std::sync::mpsc::Receiver<PreparationMessage>,
2554 complete_tx: std::sync::mpsc::SyncSender<TransferBuffer>,
2555 prepared_tx: std::sync::mpsc::SyncSender<PreparedControllerBatch>,
2556 submitted_tx: std::sync::mpsc::SyncSender<
2557 std::result::Result<SubmittedControllerBatch<'a>, String>,
2558 >,
2559 finished_tx: std::sync::mpsc::SyncSender<FinishedOutput>,
2560 kernel: CpuKernelKind,
2561 method: CpuMethodContract,
2562 output_base: usize,
2563 output_count: usize,
2564 memo: &'a PreparedFactorMemo,
2565 #[cfg(target_arch = "x86_64")] jit_memo: Option<&'a JitMemo>,
2566 timings: &'a CpuControllerTimings,
2567 compute_submitter: CpuComputeSubmitter<'a>,
2568 trace: ControllerExecutionTrace,
2569) -> bool {
2570 let panic_trace = trace.clone();
2571 if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2572 run_preparation_worker(
2573 command_rx,
2574 complete_tx,
2575 prepared_tx,
2576 submitted_tx,
2577 finished_tx,
2578 kernel,
2579 method,
2580 output_base,
2581 output_count,
2582 memo,
2583 #[cfg(target_arch = "x86_64")]
2584 jit_memo,
2585 timings,
2586 compute_submitter,
2587 trace,
2588 );
2589 }))
2590 .is_err()
2591 {
2592 panic_trace.record(ControllerExecutionEvent::Failed {
2593 phase: ControllerFailurePhase::Prepare,
2594 });
2595 true
2596 } else {
2597 false
2598 }
2599}
2600
2601#[cfg(target_family = "wasm")]
2633struct InlineComputeWorker<'a> {
2634 receiver: std::sync::mpsc::Receiver<CpuComputeJob<'a>>,
2635 completion_tx: std::sync::mpsc::SyncSender<CpuComputeCompletion>,
2636 state: ComputeWorker,
2637}
2638
2639#[cfg(target_family = "wasm")]
2640impl<'a> InlineComputeWorker<'a> {
2641 fn new(
2642 worker: usize,
2643 receiver: std::sync::mpsc::Receiver<CpuComputeJob<'a>>,
2644 completion_tx: std::sync::mpsc::SyncSender<CpuComputeCompletion>,
2645 ) -> Self {
2646 Self {
2647 receiver,
2648 completion_tx,
2649 state: ComputeWorker::new(worker),
2650 }
2651 }
2652}
2653
2654#[cfg(target_family = "wasm")]
2655struct InlineControllerWorkers<'a> {
2656 command_rx: Option<std::sync::mpsc::Receiver<PreparationMessage>>,
2660 preparation: Option<PreparationWorker<'a>>,
2662 compute: Vec<Option<InlineComputeWorker<'a>>>,
2664 preparation_panicked: bool,
2666}
2667
2668#[cfg(target_family = "wasm")]
2669impl<'a> InlineControllerWorkers<'a> {
2670 fn new(
2671 command_rx: std::sync::mpsc::Receiver<PreparationMessage>,
2672 preparation: PreparationWorker<'a>,
2673 compute: Vec<InlineComputeWorker<'a>>,
2674 ) -> Self {
2675 Self {
2676 command_rx: Some(command_rx),
2677 preparation: Some(preparation),
2678 compute: compute.into_iter().map(Some).collect(),
2679 preparation_panicked: false,
2680 }
2681 }
2682
2683 fn run(&mut self) {
2685 loop {
2686 self.run_ready_compute();
2687 let message = match self.command_rx.as_ref() {
2688 Some(command_rx) => match command_rx.try_recv() {
2689 Ok(message) => message,
2690 Err(_) => break,
2691 },
2692 None => return,
2693 };
2694 let Some(preparation) = self.preparation.as_mut() else {
2695 return;
2696 };
2697 let panic_trace = preparation.trace.clone();
2698 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2699 preparation.step(message)
2700 })) {
2701 Ok(ControlFlow::Continue(())) => {}
2702 Ok(ControlFlow::Break(())) => {
2703 self.stop_preparation();
2704 break;
2705 }
2706 Err(_) => {
2707 panic_trace.record(ControllerExecutionEvent::Failed {
2710 phase: ControllerFailurePhase::Prepare,
2711 });
2712 self.preparation_panicked = true;
2713 self.stop_preparation();
2714 break;
2715 }
2716 }
2717 }
2718 self.run_ready_compute();
2719 }
2720
2721 fn run_ready_compute(&mut self) {
2724 for slot in &mut self.compute {
2725 let Some(worker) = slot.as_mut() else {
2726 continue;
2727 };
2728 while let Ok(job) = worker.receiver.try_recv() {
2729 if worker.state.step(job, &worker.completion_tx).is_break() {
2730 *slot = None;
2731 break;
2732 }
2733 }
2734 }
2735 }
2736
2737 fn stop_preparation(&mut self) {
2738 self.preparation = None;
2739 self.command_rx = None;
2740 }
2741}
2742
2743fn queue_output_finish(
2744 preparer: &mut CpuInputPreparer,
2745 index: usize,
2746 source: OutputTransferSource,
2747 aligned_len: usize,
2748 buffer: TransferBuffer,
2749 trace: &ControllerExecutionTrace,
2750) -> Result<()> {
2751 preparer
2752 .send_command(PreparationMessage::FinishOutput {
2753 index,
2754 source,
2755 aligned_len,
2756 buffer,
2757 })
2758 .map_err(|_| {
2759 trace.record(ControllerExecutionEvent::Failed {
2760 phase: ControllerFailurePhase::OutputTransfer,
2761 });
2762 Par2Error::ReedSolomonError {
2763 reason: "CPU repair transfer worker stopped unexpectedly".to_string(),
2764 }
2765 })
2766}
2767
2768#[allow(clippy::too_many_arguments)]
2769fn finish_and_write_stream_outputs(
2770 preparer: &mut CpuInputPreparer,
2771 outputs: OutputTransferLayout<'_>,
2772 aligned_len: usize,
2773 byte_start: usize,
2774 byte_len: usize,
2775 write_targets: &[RepairWriteTarget],
2776 file_access: &mut dyn FileAccess,
2777 options: &RepairOptions,
2778 #[cfg_attr(not(target_arch = "x86_64"), allow(unused_variables))]
2779 timings: &CpuControllerTimings,
2780 trace: &ControllerExecutionTrace,
2781) -> Result<()> {
2782 debug_assert_eq!(outputs.len(), write_targets.len());
2783 let initially_queued = outputs.len().min(2);
2784 for index in 0..initially_queued {
2785 let buffer = preparer.take_transfer_buffer(options.cancel.as_ref())?;
2786 queue_output_finish(
2787 preparer,
2788 index,
2789 outputs.source(index),
2790 aligned_len,
2791 buffer,
2792 trace,
2793 )?;
2794 trace.record(ControllerExecutionEvent::OutputTransferQueued { output: index });
2795 }
2796
2797 let mut next_to_queue = initially_queued;
2798 for expected in 0..outputs.len() {
2799 preparer.pump();
2800 let FinishedOutput {
2801 index,
2802 buffer,
2803 checksum_valid,
2804 elapsed,
2805 } = recv_with_cancel(
2806 &preparer.finished_rx,
2807 options.cancel.as_ref(),
2808 "CPU repair transfer worker stopped unexpectedly",
2809 )
2810 .inspect_err(|_| {
2811 trace.record(ControllerExecutionEvent::Failed {
2812 phase: ControllerFailurePhase::OutputTransfer,
2813 });
2814 })?;
2815 if index != expected {
2816 return Err(Par2Error::ReedSolomonError {
2817 reason: "CPU repair output transfer arrived out of order".to_string(),
2818 });
2819 }
2820 CpuControllerTimings::record(&timings.finish_ns, elapsed);
2821 check_cancel(options)?;
2822 if !checksum_valid {
2823 trace.record(ControllerExecutionEvent::Failed {
2824 phase: ControllerFailurePhase::OutputTransfer,
2825 });
2826 preparer.return_transfer_buffer(buffer)?;
2827 return Err(Par2Error::ReedSolomonError {
2828 reason: format!("CPU repair output {index} failed its packed checksum"),
2829 });
2830 }
2831
2832 let target = &write_targets[index];
2833 let write_offset = target.offset + byte_start as u64;
2834 let remaining = target.file_end.saturating_sub(write_offset);
2835 let write_len = remaining.min(byte_len as u64) as usize;
2836 let write_started = Instant::now();
2837 if write_len != 0
2838 && let Err(error) = file_access.write_file_range(
2839 &target.file_id,
2840 write_offset,
2841 &buffer.bytes[..write_len],
2842 )
2843 {
2844 trace.record(ControllerExecutionEvent::Failed {
2845 phase: ControllerFailurePhase::Write,
2846 });
2847 return Err(Par2Error::RepairWriteFailed {
2848 filename: target.filename.clone(),
2849 offset: write_offset,
2850 source: error,
2851 });
2852 }
2853 CpuControllerTimings::record(&timings.write_ns, write_started.elapsed());
2854 trace.record(ControllerExecutionEvent::OutputWritten { output: index });
2855
2856 if next_to_queue < outputs.len() {
2857 queue_output_finish(
2858 preparer,
2859 next_to_queue,
2860 outputs.source(next_to_queue),
2861 aligned_len,
2862 buffer,
2863 trace,
2864 )?;
2865 trace.record(ControllerExecutionEvent::OutputTransferQueued {
2866 output: next_to_queue,
2867 });
2868 next_to_queue += 1;
2869 } else {
2870 preparer.return_transfer_buffer(buffer)?;
2871 }
2872 }
2873 Ok(())
2874}
2875
2876#[allow(clippy::too_many_arguments)]
2879fn fill_gpu_stream_batch(
2880 preparer: &mut CpuInputPreparer,
2881 mut set: StreamBatchSet,
2882 plan: &RepairPlan,
2883 par2_set: &Par2FileSet,
2884 file_access: &mut dyn FileAccess,
2885 recovery_files: &mut HashMap<PathBuf, File>,
2886 source_reader: &mut Option<StreamSourceReader>,
2887 available_inputs: usize,
2888 batch_start: usize,
2889 batch_len: usize,
2890 byte_start: usize,
2891 byte_len: usize,
2892 aligned_len: usize,
2893 chunk_len: usize,
2894 chunk_count: usize,
2895 options: &RepairOptions,
2896 timings: &CpuControllerTimings,
2897) -> Result<StreamBatchSet> {
2898 set.start = batch_start;
2899 set.len = batch_len;
2900 set.packed_stride = chunk_len;
2901 debug_assert_eq!(chunk_count, aligned_len.div_ceil(chunk_len));
2902 set.coefficients.fill(0);
2903
2904 let started = Instant::now();
2905 preparer
2906 .send_command(PreparationMessage::Begin(PrepareBatch {
2907 set,
2908 aligned_len,
2909 chunk_len,
2910 layout: None,
2911 }))
2912 .map_err(|_| Par2Error::ReedSolomonError {
2913 reason: "CPU repair preparation worker stopped unexpectedly".to_string(),
2914 })?;
2915 for lane in 0..batch_len {
2916 check_cancel(options)?;
2917 let mut buffer = preparer.take_transfer_buffer(options.cancel.as_ref())?;
2918 read_stream_source_chunk(
2919 plan,
2920 par2_set,
2921 file_access,
2922 recovery_files,
2923 source_reader,
2924 available_inputs,
2925 batch_start + lane,
2926 byte_start,
2927 &mut buffer.bytes[..byte_len],
2928 )?;
2929 buffer.bytes[byte_len..aligned_len].fill(0);
2930 let coefficients = (0..plan.input_factors.rows)
2931 .map(|output| plan.input_factors.get(output, batch_start + lane))
2932 .collect();
2933 preparer
2934 .send_command(PreparationMessage::Input {
2935 lane,
2936 coefficients,
2937 buffer,
2938 submitted: (lane + 1 == batch_len).then_some(
2939 crate::cpu_repair_controller::InputBatch {
2940 staging_area: 0,
2941 input_start: batch_start,
2942 input_len: batch_len,
2943 add: false,
2944 reason: crate::cpu_repair_controller::BatchSubmitReason::GroupFull,
2945 },
2946 ),
2947 })
2948 .map_err(|_| Par2Error::ReedSolomonError {
2949 reason: "CPU repair preparation worker stopped unexpectedly".to_string(),
2950 })?;
2951 }
2952 preparer.restore_transfer_buffers(options.cancel.as_ref())?;
2953 preparer.pump();
2954 let result = recv_with_cancel(
2955 &preparer.prepared_rx,
2956 options.cancel.as_ref(),
2957 "CPU repair preparation worker stopped unexpectedly",
2958 );
2959 CpuControllerTimings::record(&timings.read_prepare_ns, started.elapsed());
2960 result.map(|prepared| prepared.set)
2961}
2962
2963fn begin_live_stream_batch(
2967 preparer: &CpuInputPreparer,
2968 mut set: StreamBatchSet,
2969 input_start: usize,
2970 aligned_len: usize,
2971 chunk_len: usize,
2972 layout: Arc<ControllerLayout>,
2973 trace: &ControllerExecutionTrace,
2974) -> Result<()> {
2975 set.start = input_start;
2976 set.len = 0;
2977 set.packed_stride = chunk_len;
2978 set.coefficients.fill(0);
2979 preparer
2980 .send_command(PreparationMessage::Begin(PrepareBatch {
2981 set,
2982 aligned_len,
2983 chunk_len,
2984 layout: Some(layout),
2985 }))
2986 .map_err(|_| {
2987 trace.record(ControllerExecutionEvent::Failed {
2988 phase: ControllerFailurePhase::Prepare,
2989 });
2990 Par2Error::ReedSolomonError {
2991 reason: "CPU repair preparation worker stopped unexpectedly".to_string(),
2992 }
2993 })
2994}
2995
2996#[allow(clippy::too_many_arguments)]
2999fn read_live_stream_input(
3000 preparer: &mut CpuInputPreparer,
3001 plan: &RepairPlan,
3002 par2_set: &Par2FileSet,
3003 file_access: &mut dyn FileAccess,
3004 recovery_files: &mut HashMap<PathBuf, File>,
3005 source_reader: &mut Option<StreamSourceReader>,
3006 available_inputs: usize,
3007 source_index: usize,
3008 mut buffer: TransferBuffer,
3009 staging_area: usize,
3010 byte_start: usize,
3011 byte_len: usize,
3012 aligned_len: usize,
3013 options: &RepairOptions,
3014 trace: &ControllerExecutionTrace,
3015) -> Result<TransferBuffer> {
3016 check_cancel(options)?;
3017 if let Err(error) = read_stream_source_chunk(
3018 plan,
3019 par2_set,
3020 file_access,
3021 recovery_files,
3022 source_reader,
3023 available_inputs,
3024 source_index,
3025 byte_start,
3026 &mut buffer.bytes[..byte_len],
3027 ) {
3028 trace.record(ControllerExecutionEvent::Failed {
3029 phase: ControllerFailurePhase::Read,
3030 });
3031 preparer.return_transfer_buffer(buffer)?;
3032 return Err(error);
3033 }
3034 buffer.bytes[byte_len..aligned_len].fill(0);
3035 trace.record(ControllerExecutionEvent::SourceRead {
3036 source_index,
3037 staging_area,
3038 });
3039 Ok(buffer)
3040}
3041
3042fn queue_live_stream_input(
3045 preparer: &mut CpuInputPreparer,
3046 lifecycle: &mut ControllerLifecycle,
3047 plan: &RepairPlan,
3048 source_index: usize,
3049 buffer: TransferBuffer,
3050 trace: &ControllerExecutionTrace,
3051) -> Result<(usize, Option<InputBatch>)> {
3052 let expected_area = lifecycle.current_staging_area;
3053 let ControllerAddResult::Accepted {
3054 staging_area,
3055 slot,
3056 submitted,
3057 } = lifecycle.add_input(false)
3058 else {
3059 preparer.return_transfer_buffer(buffer)?;
3060 return Err(Par2Error::ReedSolomonError {
3061 reason: "CPU repair controller accepted a source while its staging area was full"
3062 .to_string(),
3063 });
3064 };
3065 if staging_area != expected_area {
3066 preparer.return_transfer_buffer(buffer)?;
3067 return Err(Par2Error::ReedSolomonError {
3068 reason: format!(
3069 "CPU repair controller changed staging area while admitting source {source_index}"
3070 ),
3071 });
3072 }
3073 let coefficients = (0..plan.input_factors.rows)
3074 .map(|output| plan.input_factors.get(output, source_index))
3075 .collect();
3076 preparer
3077 .send_command(PreparationMessage::Input {
3078 lane: slot,
3079 coefficients,
3080 buffer,
3081 submitted,
3082 })
3083 .map_err(|_| {
3084 trace.record(ControllerExecutionEvent::Failed {
3085 phase: ControllerFailurePhase::Prepare,
3086 });
3087 Par2Error::ReedSolomonError {
3088 reason: "CPU repair preparation worker stopped unexpectedly".to_string(),
3089 }
3090 })?;
3091 trace.record(ControllerExecutionEvent::InputQueued {
3092 source_index,
3093 staging_area,
3094 slot,
3095 });
3096 Ok((staging_area, submitted))
3097}
3098
3099fn flush_live_stream_batch(
3100 preparer: &CpuInputPreparer,
3101 batch: InputBatch,
3102 trace: &ControllerExecutionTrace,
3103) -> Result<()> {
3104 preparer
3105 .send_command(PreparationMessage::Flush { batch })
3106 .map_err(|_| {
3107 trace.record(ControllerExecutionEvent::Failed {
3108 phase: ControllerFailurePhase::Prepare,
3109 });
3110 Par2Error::ReedSolomonError {
3111 reason: "CPU repair preparation worker stopped unexpectedly".to_string(),
3112 }
3113 })
3114}
3115
3116enum FoldedBatchCoefficients<'a> {
3117 None,
3118 Gfni(Vec<[&'a crate::gf_simd::AffineMulMatrices; crate::gf_simd::FOLDED_GROUP]>),
3119 Shuffle2x(Vec<[&'a crate::gf_simd::Shuffle2xTables; crate::gf_simd::FOLDED_GROUP]>),
3120}
3121
3122impl<'a> FoldedBatchCoefficients<'a> {
3123 fn prepare(set: &StreamBatchSet, memo: &'a PreparedFactorMemo, output_count: usize) -> Self {
3124 if set.staging.is_empty() {
3125 return Self::None;
3126 }
3127 let groups = set.len.div_ceil(crate::gf_simd::FOLDED_GROUP);
3128 if crate::gf_simd::folded_uses_gfni() {
3129 let mut matrices = Vec::with_capacity(output_count * groups);
3130 for output in 0..output_count {
3131 for group in 0..groups {
3132 matrices.push(std::array::from_fn(|lane| {
3133 let input = group * crate::gf_simd::FOLDED_GROUP + lane;
3134 memo.get_affine(if input < set.len {
3135 set.coefficient(output, input)
3136 } else {
3137 0
3138 })
3139 }));
3140 }
3141 }
3142 Self::Gfni(matrices)
3143 } else {
3144 let mut tables = Vec::with_capacity(output_count * groups);
3145 for output in 0..output_count {
3146 for group in 0..groups {
3147 tables.push(std::array::from_fn(|lane| {
3148 let input = group * crate::gf_simd::FOLDED_GROUP + lane;
3149 memo.get_shuffle2x(if input < set.len {
3150 set.coefficient(output, input)
3151 } else {
3152 0
3153 })
3154 }));
3155 }
3156 }
3157 Self::Shuffle2x(tables)
3158 }
3159 }
3160}
3161
3162struct CpuComputeContext<'a> {
3163 output_base: usize,
3164 output_count: usize,
3165 set: StreamBatchSet,
3166 memo: &'a PreparedFactorMemo,
3167 #[cfg(target_arch = "x86_64")]
3168 jit_memo: Option<&'a JitMemo>,
3169 #[cfg(target_arch = "x86_64")]
3170 jit_batch: Option<reedsolomon_rs::xor_jit::packed::PackedJitBatch>,
3171 layout: Arc<ControllerLayout>,
3172 #[cfg_attr(not(target_arch = "x86_64"), allow(dead_code))]
3173 method: CpuMethodContract,
3174 trace: ControllerExecutionTrace,
3175 folded_coefficients: FoldedBatchCoefficients<'a>,
3176 add: bool,
3177}
3178
3179#[inline]
3180unsafe fn interleaved_output_ptr(
3181 base: usize,
3182 output_count: usize,
3183 output: usize,
3184 chunk_start: usize,
3185 chunk_len: usize,
3186) -> *mut u8 {
3187 unsafe { (base as *mut u8).add(chunk_start * output_count + output * chunk_len) }
3188}
3189
3190#[derive(Clone, Copy)]
3191struct CpuPlainSource {
3192 factor: u16,
3193 src: *const u8,
3194 len: usize,
3195}
3196
3197#[derive(Clone, Copy)]
3198struct CpuFoldedStaging {
3199 src: *const u8,
3200 len: usize,
3201}
3202
3203#[derive(Default)]
3204struct CpuWorkerScratch {
3205 active_inputs: Vec<usize>,
3206 plain_sources: Vec<CpuPlainSource>,
3207 folded_stagings: Vec<CpuFoldedStaging>,
3208 #[cfg(target_arch = "x86_64")]
3209 packed_scratch: reedsolomon_rs::xor_jit::packed::PackedScratch,
3210}
3211
3212#[inline]
3213fn xor_into(dst: &mut [u8], source: &[u8]) {
3214 for (out, input) in dst.iter_mut().zip(source.iter().copied()) {
3215 *out ^= input;
3216 }
3217}
3218
3219#[inline]
3220fn xor_folded_group_into(dst: &mut [u8], staging: &[u8], active_lanes: usize) {
3221 debug_assert_eq!(staging.len(), dst.len() * crate::gf_simd::FOLDED_GROUP);
3222 debug_assert!(active_lanes <= crate::gf_simd::FOLDED_GROUP);
3223 for (block, dst_block) in dst
3224 .chunks_exact_mut(crate::gf_simd::SPLIT_BLOCK_BYTES)
3225 .enumerate()
3226 {
3227 let staging_block =
3228 block * crate::gf_simd::FOLDED_GROUP * crate::gf_simd::SPLIT_BLOCK_BYTES;
3229 for lane in 0..active_lanes {
3230 let start = staging_block + lane * crate::gf_simd::SPLIT_BLOCK_BYTES;
3231 xor_into(
3232 dst_block,
3233 &staging[start..start + crate::gf_simd::SPLIT_BLOCK_BYTES],
3234 );
3235 }
3236 }
3237}
3238
3239fn run_cpu_worker(
3240 worker: usize,
3241 context: &CpuComputeContext<'_>,
3242 scratch: &mut CpuWorkerScratch,
3243) -> std::result::Result<(), String> {
3244 scratch.plain_sources.clear();
3245 scratch.folded_stagings.clear();
3246 #[cfg(target_arch = "x86_64")]
3247 if let Some(jit_memo) = context.jit_memo {
3248 let width = jit_memo.width;
3249 let jit_batch = context
3250 .jit_batch
3251 .as_ref()
3252 .expect("XOR-JIT compute context owns an active coefficient batch");
3253 let packed = staging_bytes(&context.set.packed).as_ptr();
3254 debug_assert_eq!(context.layout.aligned_len % width.block_bytes(), 0);
3255 debug_assert_eq!(context.layout.chunk_len, context.set.packed_stride);
3256 let packed_regions = context.set.input_grouping;
3257 let packed_chunk_bytes = packed_regions * context.set.packed_stride;
3258 for work in context
3259 .layout
3260 .assignments
3261 .iter()
3262 .filter(|work| work.worker == worker)
3263 {
3264 let work_end = work.byte_start + work.byte_len;
3265 let mut byte_start = work.byte_start;
3266 while byte_start < work_end {
3267 let chunk_index = byte_start / context.layout.chunk_len;
3268 let chunk_len = (work_end - byte_start).min(context.layout.chunk_len);
3269 let packed_chunk = unsafe { packed.add(chunk_index * packed_chunk_bytes) };
3270 let local_output_count = work.output_len;
3271 let prefetch = context.method.prefetch;
3272 let ideal_input_multiple = context.method.ideal_input_multiple.max(1);
3273 let pf_factor = prefetch.input_distance_shift;
3274 let mut inputs_prefetched_per_invoke = (context.set.len / ideal_input_multiple)
3275 .saturating_mul(prefetch.inputs_per_invoke);
3276 let mut input_prefetch_out_offset = local_output_count.saturating_sub(1);
3277 if inputs_prefetched_per_invoke > 0
3278 && inputs_prefetched_per_invoke > (1usize << pf_factor)
3279 {
3280 inputs_prefetched_per_invoke -= 1usize << pf_factor;
3281 inputs_prefetched_per_invoke <<= 3 - pf_factor;
3282 let input_prefetch_passes =
3283 (context.set.len << 3).div_ceil(inputs_prefetched_per_invoke);
3284 input_prefetch_out_offset =
3285 local_output_count.saturating_sub(input_prefetch_passes);
3286 }
3287 let next_packed_chunk = (byte_start + chunk_len < work_end)
3290 .then(|| unsafe { packed.add((chunk_index + 1) * packed_chunk_bytes) });
3291 for (local_output, output) in
3292 (work.output_start..work.output_start + work.output_len).enumerate()
3293 {
3294 let dst = unsafe {
3295 interleaved_output_ptr(
3296 context.output_base,
3297 context.output_count,
3298 output,
3299 byte_start,
3300 chunk_len,
3301 )
3302 };
3303 if !context.add {
3304 unsafe { std::slice::from_raw_parts_mut(dst, chunk_len) }.fill(0);
3305 }
3306 let prefetch_in = if local_output >= input_prefetch_out_offset {
3307 next_packed_chunk.map(|next| unsafe {
3308 next.add(
3309 (inputs_prefetched_per_invoke
3310 .saturating_mul(local_output - input_prefetch_out_offset)
3311 .saturating_mul(chunk_len))
3312 >> 3,
3313 )
3314 })
3315 } else {
3316 None
3317 };
3318 let prefetch_out = (prefetch.output && local_output + 1 < work.output_len)
3319 .then(|| unsafe { dst.add(chunk_len) as *const u8 });
3320 unsafe {
3321 jit_memo
3322 .get(jit_batch, output)
3323 .try_run_with_scratch(
3324 &mut scratch.packed_scratch,
3325 reedsolomon_rs::xor_jit::packed::PackedRun {
3326 packed_regions,
3327 live_regions: context.set.len,
3328 dst,
3329 src: packed_chunk,
3330 len: chunk_len,
3331 prefetch_in,
3332 prefetch_out,
3333 },
3334 )
3335 .map_err(|error| {
3336 format!(
3337 "XOR-JIT packed dispatch failed in worker {worker}, output {output}: {error}"
3338 )
3339 })?;
3340 }
3341 }
3342 byte_start += chunk_len;
3343 }
3344 }
3345 return Ok(());
3346 }
3347
3348 if !matches!(&context.folded_coefficients, FoldedBatchCoefficients::None) {
3349 let groups = context.set.len.div_ceil(crate::gf_simd::FOLDED_GROUP);
3350 for work in context
3351 .layout
3352 .assignments
3353 .iter()
3354 .filter(|work| work.worker == worker)
3355 {
3356 let work_end = work.byte_start + work.byte_len;
3357 let mut byte_start = work.byte_start;
3358 while byte_start < work_end {
3359 let chunk_len = (work_end - byte_start).min(context.layout.chunk_len);
3360 let byte_end = byte_start + chunk_len;
3361 scratch.folded_stagings.clear();
3362 for group in 0..groups {
3363 let staging = &staging_bytes(&context.set.staging[group])[byte_start
3364 * crate::gf_simd::FOLDED_GROUP
3365 ..byte_end * crate::gf_simd::FOLDED_GROUP];
3366 scratch.folded_stagings.push(CpuFoldedStaging {
3367 src: staging.as_ptr(),
3368 len: staging.len(),
3369 });
3370 }
3371 debug_assert!(groups <= 2);
3372 let mut staging_views: [&[u8]; 2] = [&[]; 2];
3373 for (group, source) in scratch.folded_stagings.iter().enumerate() {
3374 staging_views[group] =
3375 unsafe { std::slice::from_raw_parts(source.src, source.len) };
3376 }
3377 for output in work.output_start..work.output_start + work.output_len {
3378 let dst = unsafe {
3379 std::slice::from_raw_parts_mut(
3380 interleaved_output_ptr(
3381 context.output_base,
3382 context.output_count,
3383 output,
3384 byte_start,
3385 chunk_len,
3386 ),
3387 chunk_len,
3388 )
3389 };
3390 if !context.add {
3391 dst.fill(0);
3392 }
3393 if (0..context.set.len).all(|input| context.set.coefficient(output, input) == 1)
3394 {
3395 for (group, source) in scratch.folded_stagings.iter().enumerate() {
3396 let staging =
3397 unsafe { std::slice::from_raw_parts(source.src, source.len) };
3398 let active_lanes = context
3399 .set
3400 .len
3401 .saturating_sub(group * crate::gf_simd::FOLDED_GROUP)
3402 .min(crate::gf_simd::FOLDED_GROUP);
3403 xor_folded_group_into(dst, staging, active_lanes);
3404 }
3405 } else {
3406 match &context.folded_coefficients {
3407 FoldedBatchCoefficients::Gfni(matrices) => {
3408 let matrix_start = output * groups;
3409 crate::gf_simd::mul_acc_folded_batch(
3410 dst,
3411 &staging_views[..groups],
3412 &matrices[matrix_start..matrix_start + groups],
3413 );
3414 }
3415 FoldedBatchCoefficients::Shuffle2x(tables) => {
3416 let table_start = output * groups;
3417 crate::gf_simd::mul_acc_shuffle2x_batch(
3418 dst,
3419 &staging_views[..groups],
3420 &tables[table_start..table_start + groups],
3421 );
3422 }
3423 FoldedBatchCoefficients::None => unreachable!(),
3424 }
3425 }
3426 }
3427 byte_start = byte_end;
3428 }
3429 }
3430 scratch.folded_stagings.clear();
3431 return Ok(());
3432 }
3433
3434 for work in context
3435 .layout
3436 .assignments
3437 .iter()
3438 .filter(|work| work.worker == worker)
3439 {
3440 let work_end = work.byte_start + work.byte_len;
3441 let mut byte_start = work.byte_start;
3442 while byte_start < work_end {
3443 let chunk_len = (work_end - byte_start).min(context.layout.chunk_len);
3444 let byte_end = byte_start + chunk_len;
3445 for output in work.output_start..work.output_start + work.output_len {
3446 scratch.plain_sources.clear();
3447 scratch.active_inputs.clear();
3448 scratch.active_inputs.extend(
3449 (0..context.set.len)
3450 .filter(|input| context.set.coefficient(output, *input) != 0),
3451 );
3452 for &input in &scratch.active_inputs {
3453 let factor = context.set.coefficient(output, input);
3454 let source = &context.set.bufs[input][byte_start..byte_end];
3455 scratch.plain_sources.push(CpuPlainSource {
3456 factor,
3457 src: source.as_ptr(),
3458 len: source.len(),
3459 });
3460 }
3461 let dst = unsafe {
3462 std::slice::from_raw_parts_mut(
3463 interleaved_output_ptr(
3464 context.output_base,
3465 context.output_count,
3466 output,
3467 byte_start,
3468 chunk_len,
3469 ),
3470 chunk_len,
3471 )
3472 };
3473 if !context.add {
3474 dst.fill(0);
3475 }
3476 if !scratch.plain_sources.is_empty() {
3477 let all_one = scratch
3478 .active_inputs
3479 .iter()
3480 .all(|&input| context.set.coefficient(output, input) == 1);
3481 if all_one {
3482 for source in &scratch.plain_sources {
3483 let source =
3484 unsafe { std::slice::from_raw_parts(source.src, source.len) };
3485 xor_into(dst, source);
3486 }
3487 } else {
3488 debug_assert!(scratch.plain_sources.len() <= CPU_CONTROLLER_BUDGET_INPUTS);
3489 let mut prepared: [MaybeUninit<crate::gf_simd::PreparedFactorSrc<'_>>;
3490 CPU_CONTROLLER_BUDGET_INPUTS] =
3491 std::array::from_fn(|_| MaybeUninit::uninit());
3492 for (index, source) in scratch.plain_sources.iter().enumerate() {
3493 let source_bytes =
3494 unsafe { std::slice::from_raw_parts(source.src, source.len) };
3495 prepared[index].write(crate::gf_simd::PreparedFactorSrc {
3496 prepared: context.memo.get(source.factor),
3497 src: source_bytes,
3498 });
3499 }
3500 let prepared = unsafe {
3503 std::slice::from_raw_parts(
3504 prepared
3505 .as_ptr()
3506 .cast::<crate::gf_simd::PreparedFactorSrc<'_>>(),
3507 scratch.plain_sources.len(),
3508 )
3509 };
3510 crate::gf_simd::mul_acc_input_batch_prepared(dst, prepared);
3511 }
3512 }
3513 }
3514 byte_start = byte_end;
3515 }
3516 }
3517 scratch.plain_sources.clear();
3518 Ok(())
3519}
3520
3521struct CpuComputeJob<'a> {
3522 id: u64,
3523 context: Arc<CpuComputeContext<'a>>,
3524}
3525
3526struct CpuComputeCompletion {
3527 id: u64,
3528 worker: usize,
3529 elapsed: Duration,
3530 failure: Option<String>,
3531}
3532
3533struct CpuComputeTicket<'a> {
3534 id: u64,
3535 expected: usize,
3536 submission_failure: Option<String>,
3537 context: Arc<CpuComputeContext<'a>>,
3538}
3539
3540struct CpuComputeSubmitter<'a> {
3541 senders: Vec<std::sync::mpsc::SyncSender<CpuComputeJob<'a>>>,
3542 next_id: u64,
3543}
3544
3545struct CpuComputePool<'a> {
3546 completion_rx: std::sync::mpsc::Receiver<CpuComputeCompletion>,
3547 deferred: HashMap<u64, Vec<CpuComputeCompletion>>,
3548 _lifetime: std::marker::PhantomData<&'a ()>,
3549}
3550
3551impl<'a> CpuComputeSubmitter<'a> {
3552 fn submit(&mut self, context: CpuComputeContext<'a>) -> CpuComputeTicket<'a> {
3553 let id = self.next_id;
3554 self.next_id = self.next_id.wrapping_add(1);
3555 let context = Arc::new(context);
3556 let active_workers = context
3557 .layout
3558 .assignments
3559 .iter()
3560 .map(|work| work.worker)
3561 .max()
3562 .map_or(0, |worker| worker + 1);
3563 let mut expected = 0usize;
3564 let mut submission_failure = None;
3565 for sender in self.senders.iter().take(active_workers) {
3566 if sender
3567 .send(CpuComputeJob {
3568 id,
3569 context: Arc::clone(&context),
3570 })
3571 .is_err()
3572 {
3573 submission_failure =
3574 Some("CPU repair compute worker stopped unexpectedly".to_string());
3575 break;
3576 }
3577 expected += 1;
3578 }
3579 CpuComputeTicket {
3580 id,
3581 expected,
3582 submission_failure,
3583 context,
3584 }
3585 }
3586}
3587
3588impl<'a> CpuComputePool<'a> {
3589 fn wait(
3590 &mut self,
3591 ticket: CpuComputeTicket<'a>,
3592 cancel: Option<&CancellationToken>,
3593 timings: &CpuControllerTimings,
3594 ) -> Result<CpuComputeContext<'a>> {
3595 let CpuComputeTicket {
3596 id,
3597 expected,
3598 submission_failure,
3599 context,
3600 } = ticket;
3601 let mut max_elapsed = Duration::ZERO;
3602 let mut failure = submission_failure;
3603 let mut cancelled = cancel.is_some_and(|token| token.is_cancelled());
3604 let mut completions = self.deferred.remove(&id).unwrap_or_default();
3605 while completions.len() < expected {
3606 let completion = loop {
3607 match self.completion_rx.recv_timeout(Duration::from_millis(20)) {
3608 Ok(completion) => break completion,
3609 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
3610 cancelled |= cancel.is_some_and(|token| token.is_cancelled());
3611 }
3612 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
3613 return Err(Par2Error::ReedSolomonError {
3614 reason: "CPU repair compute workers stopped unexpectedly".to_string(),
3615 });
3616 }
3617 }
3618 };
3619 if completion.id == id {
3620 completions.push(completion);
3621 } else {
3622 self.deferred
3623 .entry(completion.id)
3624 .or_default()
3625 .push(completion);
3626 }
3627 }
3628 for completion in completions {
3629 cancelled |= cancel.is_some_and(|token| token.is_cancelled());
3630 max_elapsed = max_elapsed.max(completion.elapsed);
3631 if let Some(reason) = completion.failure {
3632 failure.get_or_insert_with(|| {
3633 format!("CPU repair worker {} failed: {reason}", completion.worker)
3634 });
3635 }
3636 }
3637 CpuControllerTimings::record(&timings.compute_ns, max_elapsed);
3638 if cancelled {
3639 Err(Par2Error::Cancelled)
3640 } else if let Some(reason) = failure {
3641 Err(Par2Error::ReedSolomonError { reason })
3642 } else {
3643 Arc::try_unwrap(context).map_err(|_| Par2Error::ReedSolomonError {
3644 reason: "CPU repair batch remained active after worker completion".to_string(),
3645 })
3646 }
3647 }
3648}
3649
3650#[allow(clippy::too_many_arguments)]
3651fn submit_prepared_controller_batch<'a>(
3652 batch: InputBatch,
3653 prepared: PrepareBatch,
3654 output_base: usize,
3655 output_count: usize,
3656 memo: &'a PreparedFactorMemo,
3657 #[cfg(target_arch = "x86_64")] jit_memo: Option<&'a JitMemo>,
3658 #[cfg(target_arch = "x86_64")] jit_workspaces: &mut [reedsolomon_rs::xor_jit::packed::PackedJitWorkspace;
3659 2],
3660 method: CpuMethodContract,
3661 #[cfg_attr(not(target_arch = "x86_64"), allow(unused_variables))]
3662 timings: &CpuControllerTimings,
3663 trace: &ControllerExecutionTrace,
3664 compute_submitter: &mut CpuComputeSubmitter<'a>,
3665) -> std::result::Result<SubmittedControllerBatch<'a>, String> {
3666 let staging_area = batch.staging_area;
3667 let layout = prepared
3668 .layout
3669 .ok_or_else(|| "CPU repair batch is missing its controller layout".to_string())?;
3670 let folded_coefficients = FoldedBatchCoefficients::prepare(&prepared.set, memo, output_count);
3671 #[cfg(target_arch = "x86_64")]
3672 let jit_batch = if let Some(jit_memo) = jit_memo {
3673 let started = Instant::now();
3674 let built = jit_memo.build_active_batch(&prepared.set, &mut jit_workspaces[staging_area]);
3675 CpuControllerTimings::record(&timings.jit_prepare_ns, started.elapsed());
3676 Some(built.map_err(|error| {
3677 trace.record(ControllerExecutionEvent::Failed {
3678 phase: ControllerFailurePhase::Compute,
3679 });
3680 format!("XOR-JIT packed batch generation failed: {error}")
3681 })?)
3682 } else {
3683 None
3684 };
3685 let compute_context = CpuComputeContext {
3686 output_base,
3687 output_count,
3688 set: prepared.set,
3689 memo,
3690 #[cfg(target_arch = "x86_64")]
3691 jit_memo,
3692 #[cfg(target_arch = "x86_64")]
3693 jit_batch,
3694 layout,
3695 method,
3696 trace: trace.clone(),
3697 folded_coefficients,
3698 add: batch.add,
3699 };
3700 let ticket = compute_submitter.submit(compute_context);
3701 trace.record(ControllerExecutionEvent::ComputeSubmitted {
3702 staging_area,
3703 add: batch.add,
3704 });
3705 Ok(SubmittedControllerBatch { batch, ticket })
3706}
3707
3708#[derive(Clone, Copy)]
3709enum SubmittedReceiveMode {
3710 ReadyOnly,
3711 Wait,
3712}
3713
3714fn receive_submitted_controller_batch<'a>(
3715 mode: SubmittedReceiveMode,
3716 preparer: &CpuInputPreparer<'a>,
3717 options: &RepairOptions,
3718 pending_prepared: &mut [Option<InputBatch>; 2],
3719 preparing: &mut [bool; 2],
3720 trace: &ControllerExecutionTrace,
3721 active: &mut [Option<CpuComputeTicket<'a>>; 2],
3722) -> Result<bool> {
3723 preparer.pump();
3724 let submitted = match mode {
3725 SubmittedReceiveMode::ReadyOnly => match preparer.submitted_rx.try_recv() {
3726 Ok(submitted) => submitted,
3727 Err(std::sync::mpsc::TryRecvError::Empty) => return Ok(false),
3728 Err(std::sync::mpsc::TryRecvError::Disconnected) => {
3729 trace.record(ControllerExecutionEvent::Failed {
3730 phase: ControllerFailurePhase::Prepare,
3731 });
3732 return Err(Par2Error::ReedSolomonError {
3733 reason: "CPU repair preparation worker stopped unexpectedly".to_string(),
3734 });
3735 }
3736 },
3737 SubmittedReceiveMode::Wait => recv_with_cancel(
3738 &preparer.submitted_rx,
3739 options.cancel.as_ref(),
3740 "CPU repair preparation worker stopped unexpectedly",
3741 )
3742 .inspect_err(|_| {
3743 trace.record(ControllerExecutionEvent::Failed {
3744 phase: ControllerFailurePhase::Prepare,
3745 });
3746 })?,
3747 }
3748 .map_err(|reason| Par2Error::ReedSolomonError { reason })?;
3749
3750 let staging_area = submitted.batch.staging_area;
3751 let expected = pending_prepared
3752 .get_mut(staging_area)
3753 .and_then(Option::take)
3754 .ok_or_else(|| Par2Error::ReedSolomonError {
3755 reason: format!(
3756 "CPU repair preparation completed unsubmitted staging area {staging_area}"
3757 ),
3758 })?;
3759 if submitted.batch != expected || !preparing[staging_area] {
3760 return Err(Par2Error::ReedSolomonError {
3761 reason: format!(
3762 "CPU repair preparation completed an unexpected batch for staging area {staging_area}"
3763 ),
3764 });
3765 }
3766 if active[staging_area].is_some() {
3767 return Err(Par2Error::ReedSolomonError {
3768 reason: format!("CPU repair controller submitted occupied staging area {staging_area}"),
3769 });
3770 }
3771 preparing[staging_area] = false;
3772 active[staging_area] = Some(submitted.ticket);
3773 Ok(true)
3774}
3775
3776#[allow(clippy::too_many_arguments)]
3777fn complete_active_controller_batch<'a>(
3778 staging_area: usize,
3779 preparer: &CpuInputPreparer<'a>,
3780 compute_pool: &mut CpuComputePool<'a>,
3781 active: &mut [Option<CpuComputeTicket<'a>>; 2],
3782 batch_sets: &mut [Option<StreamBatchSet>; 2],
3783 lifecycle: &mut ControllerLifecycle,
3784 options: &RepairOptions,
3785 timings: &CpuControllerTimings,
3786 trace: &ControllerExecutionTrace,
3787) -> Result<()> {
3788 preparer.pump();
3792 #[allow(unused_mut)]
3793 let mut finished = compute_pool.wait(
3794 active[staging_area]
3795 .take()
3796 .expect("active controller staging area has a ticket"),
3797 options.cancel.as_ref(),
3798 timings,
3799 )?;
3800 #[cfg(target_arch = "x86_64")]
3801 if let Some(jit_batch) = finished.jit_batch.take()
3802 && jit_batch.requires_workspace_recycle()
3803 {
3804 preparer
3805 .send_command(PreparationMessage::RecycleJit {
3806 staging_area,
3807 batch: jit_batch,
3808 })
3809 .map_err(|_| Par2Error::ReedSolomonError {
3810 reason: "CPU repair preparation worker stopped before recycling XOR-JIT state"
3811 .to_string(),
3812 })?;
3813 }
3814 batch_sets[staging_area] = Some(finished.set);
3815 lifecycle.complete_batch(staging_area);
3816 trace.record(ControllerExecutionEvent::ComputeCompleted { staging_area });
3817 Ok(())
3818}
3819
3820struct ComputeWorker {
3824 worker: usize,
3825 scratch: CpuWorkerScratch,
3826}
3827
3828impl ComputeWorker {
3829 fn new(worker: usize) -> Self {
3830 Self {
3831 worker,
3832 scratch: CpuWorkerScratch::default(),
3833 }
3834 }
3835
3836 fn step(
3839 &mut self,
3840 job: CpuComputeJob<'_>,
3841 completion_tx: &std::sync::mpsc::SyncSender<CpuComputeCompletion>,
3842 ) -> ControlFlow<()> {
3843 let CpuComputeJob { id, context } = job;
3844 let started = Instant::now();
3845 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3846 run_cpu_worker(self.worker, &context, &mut self.scratch)
3847 }));
3848 let failure = match result {
3849 Ok(Ok(())) => None,
3850 Ok(Err(reason)) => Some(reason),
3851 Err(_) => Some("kernel panicked".to_string()),
3852 };
3853 if failure.is_some() {
3854 context.trace.record(ControllerExecutionEvent::Failed {
3855 phase: ControllerFailurePhase::Compute,
3856 });
3857 }
3858 self.scratch.plain_sources.clear();
3859 self.scratch.folded_stagings.clear();
3860 drop(context);
3863 if completion_tx
3864 .send(CpuComputeCompletion {
3865 id,
3866 worker: self.worker,
3867 elapsed: started.elapsed(),
3868 failure: failure.clone(),
3869 })
3870 .is_err()
3871 {
3872 return ControlFlow::Break(());
3873 }
3874 if failure.is_some() {
3875 return ControlFlow::Break(());
3876 }
3877 ControlFlow::Continue(())
3878 }
3879}
3880
3881fn run_compute_worker<'a>(
3882 worker: usize,
3883 receiver: std::sync::mpsc::Receiver<CpuComputeJob<'a>>,
3884 completion_tx: std::sync::mpsc::SyncSender<CpuComputeCompletion>,
3885) {
3886 let mut state = ComputeWorker::new(worker);
3887 while let Ok(job) = receiver.recv() {
3888 if state.step(job, &completion_tx).is_break() {
3889 break;
3890 }
3891 }
3892}
3893
3894fn xor_out_known_data(
3895 recovery_buffers: &mut [Vec<u8>],
3896 recovery_factors: &[u16],
3897 data: &[u8],
3898 chunk_words: usize,
3899) {
3900 assert_eq!(
3901 recovery_buffers.len(),
3902 recovery_factors.len(),
3903 "recovery factor count must match recovery buffer count"
3904 );
3905 assert!(
3906 data.len().is_multiple_of(2),
3907 "PAR2 slice_size must be a multiple of 2"
3908 );
3909
3910 let word_count = data.len() / 2;
3911 let chunk_words = chunk_words.max(1).min(word_count.max(1));
3912 let word_chunks: Vec<usize> = (0..word_count.max(1)).step_by(chunk_words).collect();
3913 let recovery_ptrs: Vec<usize> = recovery_buffers
3914 .iter_mut()
3915 .map(|recovery| recovery.as_mut_ptr() as usize)
3916 .collect();
3917
3918 word_chunks.par_iter().for_each(|&chunk_start| {
3919 let chunk_end = (chunk_start + chunk_words).min(word_count);
3920 let byte_start = chunk_start * 2;
3921 let byte_len = (chunk_end - chunk_start) * 2;
3922 let src = &data[byte_start..byte_start + byte_len];
3923
3924 for factor_start in (0..recovery_factors.len()).step_by(XOR_OUT_PAR_CHUNK) {
3925 let factor_end = (factor_start + XOR_OUT_PAR_CHUNK).min(recovery_factors.len());
3926 let mut pairs: Vec<crate::gf_simd::FactorDst<'_>> =
3927 Vec::with_capacity(factor_end - factor_start);
3928
3929 for idx in factor_start..factor_end {
3930 let factor = recovery_factors[idx];
3931 if factor == 0 {
3932 continue;
3933 }
3934
3935 let dst = unsafe {
3936 let ptr = recovery_ptrs[idx] as *mut u8;
3937 std::slice::from_raw_parts_mut(ptr.add(byte_start), byte_len)
3938 };
3939 pairs.push(crate::gf_simd::FactorDst { factor, dst });
3940 }
3941
3942 if !pairs.is_empty() {
3943 crate::gf_simd::mul_acc_multi_region(&mut pairs, src);
3944 }
3945 }
3946 });
3947}
3948
3949fn read_exact_at_cached(
3950 files: &mut HashMap<PathBuf, File>,
3951 path: &Path,
3952 offset: u64,
3953 dst: &mut [u8],
3954) -> io::Result<()> {
3955 let file = if let Some(file) = files.get_mut(path) {
3956 file
3957 } else {
3958 files.insert(path.to_path_buf(), File::open(path)?);
3959 files.get_mut(path).expect("cached file should exist")
3960 };
3961 file.seek(SeekFrom::Start(offset))?;
3962 file.read_exact(dst)
3963}
3964
3965fn fill_recovery_chunk(
3966 data: &crate::packet::RecoverySliceData,
3967 start: usize,
3968 dst: &mut [u8],
3969 file_cache: &mut HashMap<PathBuf, File>,
3970) -> io::Result<()> {
3971 dst.fill(0);
3972
3973 if let Some(bytes) = data.as_bytes() {
3974 if start >= bytes.len() {
3975 return Ok(());
3976 }
3977 let end = (start + dst.len()).min(bytes.len());
3978 let copy_len = end - start;
3979 dst[..copy_len].copy_from_slice(&bytes[start..end]);
3980 return Ok(());
3981 }
3982
3983 let Some((path, base_offset, len)) = data.file_span() else {
3984 return Ok(());
3985 };
3986 if start >= len {
3987 return Ok(());
3988 }
3989
3990 let read_len = dst.len().min(len - start);
3991 read_exact_at_cached(
3992 file_cache,
3993 path,
3994 base_offset + start as u64,
3995 &mut dst[..read_len],
3996 )
3997}
3998
3999pub fn execute_repair(
4007 plan: &RepairPlan,
4008 par2_set: &Par2FileSet,
4009 file_access: &mut dyn FileAccess,
4010) -> Result<()> {
4011 execute_repair_with_options(plan, par2_set, file_access, &RepairOptions::default())
4012}
4013
4014pub fn prepare_recovery_buffers(
4017 plan: &RepairPlan,
4018 par2_set: &Par2FileSet,
4019 options: &RepairOptions,
4020) -> Result<Vec<Vec<u8>>> {
4021 let n = plan.missing_slices.len();
4022 let slice_size = plan.slice_size as usize;
4023
4024 let mut recovery_data: Vec<Vec<u8>> = Vec::with_capacity(n);
4025 for (i, &exp) in plan.recovery_exponents.iter().enumerate() {
4026 if let Some(ref cancel) = options.cancel
4027 && cancel.is_cancelled()
4028 {
4029 return Err(Par2Error::Cancelled);
4030 }
4031 let rs = par2_set
4032 .recovery_slices
4033 .get(&exp)
4034 .ok_or_else(|| Par2Error::ReedSolomonError {
4035 reason: format!("recovery block with exponent {exp} not found"),
4036 })?;
4037 let mut data = rs.data.to_vec().map_err(Par2Error::Io)?;
4038 data.resize(slice_size, 0);
4039 recovery_data.push(data);
4040
4041 if let Some(ref progress) = options.progress {
4042 progress(ProgressUpdate {
4043 stage: ProgressStage::ReadingRecovery,
4044 current: i as u32 + 1,
4045 total: n as u32,
4046 bytes_processed: (i + 1) as u64 * slice_size as u64,
4047 total_bytes: None,
4048 phase: ProgressPhase::Whole,
4049 });
4050 }
4051 }
4052
4053 Ok(recovery_data)
4054}
4055
4056pub fn xor_out_slice(
4061 recovery_buffers: &mut [Vec<u8>],
4062 plan: &RepairPlan,
4063 global_idx: usize,
4064 input_data: &[u8],
4065) {
4066 let slice_size = plan.slice_size as usize;
4067 assert!(
4068 slice_size.is_multiple_of(2),
4069 "PAR2 slice_size must be a multiple of 2"
4070 );
4071
4072 let padded;
4074 let data = if input_data.len() < slice_size {
4075 padded = {
4076 let mut v = input_data.to_vec();
4077 v.resize(slice_size, 0);
4078 v
4079 };
4080 &padded[..]
4081 } else {
4082 &input_data[..slice_size]
4083 };
4084
4085 let recovery_factors: Vec<u16> = plan
4086 .recovery_exponents
4087 .iter()
4088 .map(|&exp| gf::pow(plan.constants[global_idx], exp))
4089 .collect();
4090
4091 xor_out_known_data(
4092 recovery_buffers,
4093 &recovery_factors,
4094 &data[..slice_size],
4095 slice_size / 2,
4096 );
4097}
4098
4099pub fn reconstruct_and_write(
4102 _plan: &RepairPlan,
4103 _par2_set: &Par2FileSet,
4104 _recovery_buffers: Vec<Vec<u8>>,
4105 _file_access: &mut dyn FileAccess,
4106 _chunk_words: usize,
4107 _options: &RepairOptions,
4108) -> Result<()> {
4109 Err(Par2Error::ReedSolomonError {
4110 reason:
4111 "legacy in-memory PAR2 reconstruction is quarantined; use execute_repair_with_options"
4112 .to_string(),
4113 })
4114}
4115
4116#[cfg(test)]
4119#[allow(dead_code)]
4120fn legacy_reconstruct_and_write(
4121 plan: &RepairPlan,
4122 par2_set: &Par2FileSet,
4123 recovery_buffers: Vec<Vec<u8>>,
4124 file_access: &mut dyn FileAccess,
4125 chunk_words: usize,
4126 options: &RepairOptions,
4127) -> Result<()> {
4128 let n = plan.missing_slices.len();
4129 if n == 0 {
4130 return Ok(());
4131 }
4132
4133 let slice_size = plan.slice_size as usize;
4134 assert!(
4135 slice_size.is_multiple_of(2),
4136 "PAR2 slice_size must be a multiple of 2"
4137 );
4138 let word_count = slice_size / 2;
4139
4140 info!("reconstructing {} missing slices", n);
4142
4143 #[cfg(target_arch = "x86_64")]
4144 if is_x86_feature_detected!("gfni") && is_x86_feature_detected!("avx2") {
4145 return reconstruct_and_write_grouped_inputs(
4146 plan,
4147 par2_set,
4148 recovery_buffers,
4149 file_access,
4150 chunk_words,
4151 options,
4152 );
4153 }
4154
4155 let mut repaired_slices: Vec<Vec<u8>> = vec![vec![0u8; slice_size]; n];
4156
4157 let total_chunks_usize = word_count.div_ceil(chunk_words);
4158 let total_chunks = total_chunks_usize.min(u32::MAX as usize) as u32;
4159 let repair_total_bytes = (word_count as u64).saturating_mul(2);
4160 let write_total_bytes = (n as u64).saturating_mul(slice_size as u64);
4161 let operation_total_bytes = repair_total_bytes.saturating_add(write_total_bytes);
4162
4163 check_cancel(options)?;
4164
4165 let completed_chunks = AtomicU32::new(0);
4166 let repaired_ptrs: Vec<usize> = repaired_slices
4167 .iter_mut()
4168 .map(|slice| slice.as_mut_ptr() as usize)
4169 .collect();
4170
4171 (0..total_chunks_usize)
4172 .into_par_iter()
4173 .try_for_each(|chunk_idx| -> Result<()> {
4174 check_cancel(options)?;
4175
4176 let chunk_start = chunk_idx * chunk_words;
4177 let chunk_end = (chunk_start + chunk_words).min(word_count);
4178 let chunk_len = chunk_end - chunk_start;
4179 let byte_start = chunk_start * 2;
4180 let byte_len = chunk_len * 2;
4181
4182 for (r, recovery) in recovery_buffers.iter().enumerate() {
4188 let src = &recovery[byte_start..byte_start + byte_len];
4189 let mut pairs: Vec<crate::gf_simd::FactorDst<'_>> = (0..n)
4190 .filter_map(|j| {
4191 let factor = plan.decode_matrix.get(j, r);
4192 if factor != 0 {
4193 let dst = unsafe {
4197 let ptr = repaired_ptrs[j] as *mut u8;
4198 std::slice::from_raw_parts_mut(ptr.add(byte_start), byte_len)
4199 };
4200 Some(crate::gf_simd::FactorDst { factor, dst })
4201 } else {
4202 None
4203 }
4204 })
4205 .collect();
4206 if !pairs.is_empty() {
4207 crate::gf_simd::mul_acc_multi_region(&mut pairs, src);
4208 }
4209 }
4210
4211 if let Some(ref progress) = options.progress {
4212 let current = completed_chunks.fetch_add(1, Ordering::Relaxed) + 1;
4213 progress(ProgressUpdate {
4214 stage: ProgressStage::Repairing,
4215 current,
4216 total: total_chunks,
4217 bytes_processed: (current as u64)
4218 .saturating_mul(chunk_words as u64)
4219 .saturating_mul(2)
4220 .min(repair_total_bytes),
4221 total_bytes: Some(operation_total_bytes),
4222 phase: ProgressPhase::Whole,
4223 });
4224 }
4225
4226 Ok(())
4227 })?;
4228
4229 check_cancel(options)?;
4230
4231 info!("writing repaired slices to files");
4233 let write_targets = build_write_targets(plan, par2_set)?;
4234
4235 for (j, target) in write_targets.iter().enumerate() {
4236 check_cancel(options)?;
4237
4238 let slice_end = target.offset + plan.slice_size;
4239 let write_len = if slice_end > target.file_end {
4240 (target.file_end - target.offset) as usize
4241 } else {
4242 slice_size
4243 };
4244
4245 file_access
4246 .write_file_range(
4247 &target.file_id,
4248 target.offset,
4249 &repaired_slices[j][..write_len],
4250 )
4251 .map_err(|e| Par2Error::RepairWriteFailed {
4252 filename: target.filename.clone(),
4253 offset: target.offset,
4254 source: e,
4255 })?;
4256
4257 debug!(
4258 "repaired slice {} of file {} ({write_len} bytes at offset {})",
4259 plan.missing_slices[j].1, target.filename, target.offset
4260 );
4261
4262 if let Some(ref progress) = options.progress {
4263 progress(ProgressUpdate {
4264 stage: ProgressStage::WritingRepaired,
4265 current: j as u32 + 1,
4266 total: n as u32,
4267 bytes_processed: repair_total_bytes
4268 .saturating_add((j + 1) as u64 * slice_size as u64),
4269 total_bytes: Some(operation_total_bytes),
4270 phase: ProgressPhase::Whole,
4271 });
4272 }
4273 }
4274
4275 info!("repair complete: {} slices restored", n);
4276 Ok(())
4277}
4278
4279#[cfg(all(test, target_arch = "x86_64"))]
4280#[allow(dead_code)]
4281fn reconstruct_and_write_grouped_inputs(
4282 plan: &RepairPlan,
4283 par2_set: &Par2FileSet,
4284 recovery_buffers: Vec<Vec<u8>>,
4285 file_access: &mut dyn FileAccess,
4286 chunk_words: usize,
4287 options: &RepairOptions,
4288) -> Result<()> {
4289 let n = plan.missing_slices.len();
4290 if n == 0 {
4291 return Ok(());
4292 }
4293
4294 let slice_size = plan.slice_size as usize;
4295 assert!(
4296 slice_size.is_multiple_of(2),
4297 "PAR2 slice_size must be a multiple of 2"
4298 );
4299 let word_count = slice_size / 2;
4300 let total_chunks_usize = word_count.div_ceil(chunk_words);
4301 let output_inputs = grouped_input_factors(&plan.decode_matrix);
4302 let mut factor_slots: HashMap<u16, usize> = HashMap::new();
4305 let mut prepared_factors: Vec<crate::gf_simd::PreparedInputFactor> = Vec::new();
4306 let prepared_output_inputs: Vec<Vec<(u16, usize)>> = output_inputs
4307 .iter()
4308 .map(|inputs| {
4309 inputs
4310 .iter()
4311 .map(|factor_input| {
4312 let slot = *factor_slots.entry(factor_input.factor).or_insert_with(|| {
4313 prepared_factors
4314 .push(crate::gf_simd::prepare_input_factor(factor_input.factor));
4315 prepared_factors.len() - 1
4316 });
4317 (factor_input.input_idx, slot)
4318 })
4319 .collect()
4320 })
4321 .collect();
4322
4323 let mut repaired_slices: Vec<Vec<u8>> = vec![vec![0u8; slice_size]; n];
4324 let completed_outputs = AtomicU32::new(0);
4325 let repair_total_bytes = (n as u64).saturating_mul(slice_size as u64);
4326 let write_total_bytes = (n as u64).saturating_mul(slice_size as u64);
4327 let operation_total_bytes = repair_total_bytes.saturating_add(write_total_bytes);
4328
4329 repaired_slices.par_iter_mut().enumerate().try_for_each(
4330 |(output_idx, repaired)| -> Result<()> {
4331 check_cancel(options)?;
4332
4333 let decode_inputs = &prepared_output_inputs[output_idx];
4334 let mut chunk_inputs = Vec::with_capacity(decode_inputs.len());
4335
4336 for chunk_idx in 0..total_chunks_usize {
4337 let chunk_start = chunk_idx * chunk_words;
4338 let chunk_end = (chunk_start + chunk_words).min(word_count);
4339 let byte_start = chunk_start * 2;
4340 let byte_len = (chunk_end - chunk_start) * 2;
4341
4342 chunk_inputs.clear();
4343 for (input_idx, factor_slot) in decode_inputs {
4344 chunk_inputs.push(crate::gf_simd::PreparedFactorSrc {
4345 prepared: &prepared_factors[*factor_slot],
4346 src: &recovery_buffers[*input_idx as usize]
4347 [byte_start..byte_start + byte_len],
4348 });
4349 }
4350
4351 crate::gf_simd::mul_acc_input_batch_prepared(
4352 &mut repaired[byte_start..byte_start + byte_len],
4353 &chunk_inputs,
4354 );
4355 }
4356
4357 if let Some(ref progress) = options.progress {
4358 let current = completed_outputs.fetch_add(1, Ordering::Relaxed) + 1;
4359 progress(ProgressUpdate {
4360 stage: ProgressStage::Repairing,
4361 current,
4362 total: n as u32,
4363 bytes_processed: current as u64 * slice_size as u64,
4364 total_bytes: Some(operation_total_bytes),
4365 phase: ProgressPhase::Whole,
4366 });
4367 }
4368
4369 Ok(())
4370 },
4371 )?;
4372
4373 check_cancel(options)?;
4374
4375 info!("writing repaired slices to files");
4376 let write_targets = build_write_targets(plan, par2_set)?;
4377
4378 for (j, target) in write_targets.iter().enumerate() {
4379 check_cancel(options)?;
4380
4381 let slice_end = target.offset + plan.slice_size;
4382 let write_len = if slice_end > target.file_end {
4383 (target.file_end - target.offset) as usize
4384 } else {
4385 slice_size
4386 };
4387
4388 file_access
4389 .write_file_range(
4390 &target.file_id,
4391 target.offset,
4392 &repaired_slices[j][..write_len],
4393 )
4394 .map_err(|e| Par2Error::RepairWriteFailed {
4395 filename: target.filename.clone(),
4396 offset: target.offset,
4397 source: e,
4398 })?;
4399
4400 if let Some(ref progress) = options.progress {
4401 progress(ProgressUpdate {
4402 stage: ProgressStage::WritingRepaired,
4403 current: j as u32 + 1,
4404 total: n as u32,
4405 bytes_processed: repair_total_bytes
4406 .saturating_add((j + 1) as u64 * slice_size as u64),
4407 total_bytes: Some(operation_total_bytes),
4408 phase: ProgressPhase::Whole,
4409 });
4410 }
4411 }
4412
4413 info!("repair complete: {} slices restored", n);
4414 Ok(())
4415}
4416
4417#[cfg(any(
4421 all(feature = "metal", target_os = "macos", target_arch = "aarch64"),
4422 feature = "wgpu"
4423))]
4424enum GpuSession {
4425 #[cfg(all(feature = "metal", target_os = "macos", target_arch = "aarch64"))]
4426 Metal(reedsolomon_rs::metal_gf16::MetalGf16Session),
4427 #[cfg(feature = "wgpu")]
4428 Wgpu(reedsolomon_rs::wgpu_gf16::WgpuGf16Session),
4429}
4430
4431#[cfg(any(
4432 all(feature = "metal", target_os = "macos", target_arch = "aarch64"),
4433 feature = "wgpu"
4434))]
4435impl GpuSession {
4436 fn try_new(outputs: usize, max_byte_len: usize, effective_bytes: u64) -> Option<Self> {
4437 #[cfg(all(feature = "metal", target_os = "macos", target_arch = "aarch64"))]
4438 if let Some(session) = reedsolomon_rs::metal_gf16::MetalGf16Session::try_new(
4439 outputs,
4440 max_byte_len,
4441 effective_bytes,
4442 ) {
4443 return Some(GpuSession::Metal(session));
4444 }
4445 #[cfg(feature = "wgpu")]
4446 if let Some(session) = reedsolomon_rs::wgpu_gf16::WgpuGf16Session::try_new(
4447 outputs,
4448 max_byte_len,
4449 effective_bytes,
4450 ) {
4451 return Some(GpuSession::Wgpu(session));
4452 }
4453 None
4454 }
4455
4456 fn begin_chunk(&mut self, byte_len: usize) -> std::result::Result<(), &'static str> {
4457 match self {
4458 #[cfg(all(feature = "metal", target_os = "macos", target_arch = "aarch64"))]
4459 GpuSession::Metal(s) => s.begin_chunk(byte_len),
4460 #[cfg(feature = "wgpu")]
4461 GpuSession::Wgpu(s) => s.begin_chunk(byte_len),
4462 }
4463 }
4464
4465 fn accumulate(
4466 &mut self,
4467 srcs: &[&[u8]],
4468 factor: impl Fn(usize, usize) -> u16,
4469 ) -> std::result::Result<(), &'static str> {
4470 match self {
4471 #[cfg(all(feature = "metal", target_os = "macos", target_arch = "aarch64"))]
4472 GpuSession::Metal(s) => s.accumulate(srcs, factor),
4473 #[cfg(feature = "wgpu")]
4474 GpuSession::Wgpu(s) => s.accumulate(srcs, factor),
4475 }
4476 }
4477
4478 fn finish_chunk(&mut self, rows: &mut [Vec<u8>]) -> std::result::Result<(), &'static str> {
4479 match self {
4480 #[cfg(all(feature = "metal", target_os = "macos", target_arch = "aarch64"))]
4481 GpuSession::Metal(s) => s.finish_chunk(rows),
4482 #[cfg(feature = "wgpu")]
4483 GpuSession::Wgpu(s) => s.finish_chunk(rows),
4484 }
4485 }
4486
4487 fn device_name(&self) -> String {
4488 match self {
4489 #[cfg(all(feature = "metal", target_os = "macos", target_arch = "aarch64"))]
4490 GpuSession::Metal(s) => s.device_name(),
4491 #[cfg(feature = "wgpu")]
4492 GpuSession::Wgpu(s) => s.device_name(),
4493 }
4494 }
4495
4496 fn backend_name(&self) -> &'static str {
4497 match self {
4498 #[cfg(all(feature = "metal", target_os = "macos", target_arch = "aarch64"))]
4499 GpuSession::Metal(_) => "metal",
4500 #[cfg(feature = "wgpu")]
4501 GpuSession::Wgpu(_) => "wgpu",
4502 }
4503 }
4504}
4505
4506struct GpuComputeArm {
4514 #[cfg(any(
4515 all(feature = "metal", target_os = "macos", target_arch = "aarch64"),
4516 feature = "wgpu"
4517 ))]
4518 session: Option<GpuSession>,
4519}
4520
4521impl GpuComputeArm {
4522 fn is_engaged(&self) -> bool {
4523 #[cfg(any(
4524 all(feature = "metal", target_os = "macos", target_arch = "aarch64"),
4525 feature = "wgpu"
4526 ))]
4527 return self.session.is_some();
4528
4529 #[cfg(not(any(
4530 all(feature = "metal", target_os = "macos", target_arch = "aarch64"),
4531 feature = "wgpu"
4532 )))]
4533 false
4534 }
4535
4536 fn disable(&mut self) {
4537 #[cfg(any(
4538 all(feature = "metal", target_os = "macos", target_arch = "aarch64"),
4539 feature = "wgpu"
4540 ))]
4541 {
4542 self.session = None;
4543 }
4544 }
4545
4546 #[cfg(any(
4547 all(feature = "metal", target_os = "macos", target_arch = "aarch64"),
4548 feature = "wgpu"
4549 ))]
4550 fn engage(enabled: bool, outputs: usize, max_byte_len: usize, effective_bytes: u64) -> Self {
4551 let session = enabled
4555 .then(|| GpuSession::try_new(outputs, max_byte_len, effective_bytes))
4556 .flatten();
4557 if let Some(session) = &session {
4558 info!(
4559 backend = session.backend_name(),
4560 device = %session.device_name(),
4561 outputs,
4562 "gpu gf16 tier engaged for streaming repair"
4563 );
4564 }
4565 #[cfg(feature = "wgpu")]
4569 if session.is_none() && reedsolomon_rs::wgpu_gf16::auto_refused_cpu_adapter() {
4570 debug!("wgpu adapter is a cpu rasterizer; keeping the cpu gf16 tier");
4571 }
4572 Self { session }
4573 }
4574
4575 #[cfg(not(any(
4576 all(feature = "metal", target_os = "macos", target_arch = "aarch64"),
4577 feature = "wgpu"
4578 )))]
4579 fn engage(
4580 _enabled: bool,
4581 _outputs: usize,
4582 _max_byte_len: usize,
4583 _effective_bytes: u64,
4584 ) -> Self {
4585 Self {}
4586 }
4587
4588 fn begin_chunk(&mut self, _byte_len: usize) -> bool {
4590 #[cfg(any(
4591 all(feature = "metal", target_os = "macos", target_arch = "aarch64"),
4592 feature = "wgpu"
4593 ))]
4594 {
4595 if let Some(session) = self.session.as_mut() {
4596 match session.begin_chunk(_byte_len) {
4597 Ok(()) => return true,
4598 Err(reason) => {
4599 warn!(reason, "gpu gf16 begin_chunk failed; using CPU path");
4600 self.session = None;
4601 }
4602 }
4603 }
4604 }
4605 false
4606 }
4607
4608 fn accumulate(
4611 &mut self,
4612 _set: &StreamBatchSet,
4613 _plan: &RepairPlan,
4614 _byte_len: usize,
4615 ) -> std::result::Result<(), ()> {
4616 #[cfg(any(
4617 all(feature = "metal", target_os = "macos", target_arch = "aarch64"),
4618 feature = "wgpu"
4619 ))]
4620 {
4621 let Some(session) = self.session.as_mut() else {
4622 return Err(());
4623 };
4624 let srcs: Vec<&[u8]> = _set.bufs[.._set.len]
4625 .iter()
4626 .map(|buf| &buf[.._byte_len])
4627 .collect();
4628 let matrix = &_plan.input_factors;
4629 let start = _set.start;
4630 if let Err(reason) = session.accumulate(&srcs, |j, s| matrix.get(j, start + s)) {
4631 warn!(reason, "gpu gf16 accumulate failed; redoing chunk on CPU");
4632 self.session = None;
4633 return Err(());
4634 }
4635 return Ok(());
4636 }
4637 #[allow(unreachable_code)]
4638 Err(())
4639 }
4640
4641 fn finish_chunk(
4643 &mut self,
4644 _rows: &mut [Vec<u8>],
4645 _byte_len: usize,
4646 ) -> std::result::Result<(), ()> {
4647 #[cfg(any(
4648 all(feature = "metal", target_os = "macos", target_arch = "aarch64"),
4649 feature = "wgpu"
4650 ))]
4651 {
4652 let Some(session) = self.session.as_mut() else {
4653 return Err(());
4654 };
4655 if let Err(reason) = session.finish_chunk(_rows) {
4656 warn!(reason, "gpu gf16 finish_chunk failed; redoing chunk on CPU");
4657 self.session = None;
4658 return Err(());
4659 }
4660 return Ok(());
4661 }
4662 #[allow(unreachable_code)]
4663 Err(())
4664 }
4665}
4666
4667#[derive(Default)]
4668struct CpuControllerTimings {
4669 read_prepare_ns: AtomicU64,
4670 jit_prepare_ns: AtomicU64,
4671 compute_ns: AtomicU64,
4672 finish_ns: AtomicU64,
4673 write_ns: AtomicU64,
4674}
4675
4676impl CpuControllerTimings {
4677 fn record(counter: &AtomicU64, elapsed: Duration) {
4678 let nanos = elapsed.as_nanos().min(u64::MAX as u128) as u64;
4679 counter.fetch_add(nanos, Ordering::Relaxed);
4680 }
4681
4682 fn micros(counter: &AtomicU64) -> u64 {
4683 counter.load(Ordering::Relaxed) / 1_000
4684 }
4685
4686 fn duration_micros(elapsed: Duration) -> u64 {
4687 elapsed.as_micros().min(u64::MAX as u128) as u64
4688 }
4689}
4690
4691fn execute_repair_streaming(
4692 plan: &RepairPlan,
4693 par2_set: &Par2FileSet,
4694 file_access: &mut dyn FileAccess,
4695 options: &RepairOptions,
4696 budget: usize,
4697) -> Result<()> {
4698 execute_repair_streaming_with_trace(
4699 plan,
4700 par2_set,
4701 file_access,
4702 options,
4703 budget,
4704 ControllerExecutionTrace::default(),
4705 )
4706}
4707
4708fn execute_repair_streaming_with_trace(
4709 plan: &RepairPlan,
4710 par2_set: &Par2FileSet,
4711 file_access: &mut dyn FileAccess,
4712 options: &RepairOptions,
4713 budget: usize,
4714 trace: ControllerExecutionTrace,
4715) -> Result<()> {
4716 check_cancel(options)?;
4717 let controller_started = Instant::now();
4718 let n = plan.missing_slices.len();
4719 if n == 0 {
4720 return Ok(());
4721 }
4722
4723 let slice_size = plan.slice_size as usize;
4724 assert!(
4725 slice_size.is_multiple_of(2),
4726 "PAR2 slice_size must be a multiple of 2"
4727 );
4728 let word_count = slice_size / 2;
4729 let operation_total_bytes = (word_count as u64).saturating_mul(2);
4730 let write_targets = build_write_targets(plan, par2_set)?;
4731 let mut recovery_files: HashMap<PathBuf, File> = HashMap::new();
4732 let available_inputs = plan.available_input_global_indices.len();
4733 let total_sources = available_inputs + plan.recovery_exponents.len();
4734 let effective_bytes = (n as u64)
4742 .saturating_mul(total_sources as u64)
4743 .saturating_mul(plan.slice_size);
4744 #[cfg(feature = "wgpu")]
4748 let gpu_forced = reedsolomon_rs::wgpu_gf16::force_requested();
4749 #[cfg(not(feature = "wgpu"))]
4750 let gpu_forced = false;
4751 #[cfg(feature = "wgpu")]
4765 let gpu_discrete_auto = !gpu_forced
4766 && crate::gf_simd::altmap_supported()
4767 && reedsolomon_rs::wgpu_gf16::discrete_auto_candidate(effective_bytes);
4768 #[cfg(not(feature = "wgpu"))]
4769 let gpu_discrete_auto = false;
4770 let gpu_preferred = gpu_forced || gpu_discrete_auto;
4771 #[cfg(target_arch = "x86_64")]
4777 let jit_width = reedsolomon_rs::xor_jit::JitWidth::detect()
4778 .filter(|_| std::env::var_os("RARPAR_PAR2_XORJIT").is_none_or(|v| v != "0"));
4779 let workers = rayon::current_num_threads().max(1);
4780 #[cfg(target_arch = "x86_64")]
4783 let baseline_method = if crate::gf_simd::altmap_supported() {
4784 CpuKernelKind::Folded.method()
4785 } else {
4786 CpuKernelKind::Plain.method()
4787 };
4788 #[cfg(target_arch = "x86_64")]
4792 let jit_setup_started = Instant::now();
4793 #[cfg(target_arch = "x86_64")]
4794 let jit_memo = match jit_width {
4795 Some(width) => XorJitSelection {
4796 width,
4797 jit_method: CpuKernelKind::XorJit(width).method(),
4798 baseline_method,
4799 output_count: n,
4800 word_count,
4801 workers,
4802 budget,
4803 }
4804 .select_memo(&plan.input_factors.data)?,
4805 None => None,
4806 };
4807 #[cfg(target_arch = "x86_64")]
4808 let jit_setup = jit_setup_started.elapsed();
4809 #[cfg(not(target_arch = "x86_64"))]
4810 let jit_setup = Duration::ZERO;
4811 #[cfg(target_arch = "x86_64")]
4812 let use_xorjit = jit_memo.is_some();
4813 #[cfg(not(target_arch = "x86_64"))]
4814 let use_xorjit = false;
4815 let use_folded = crate::gf_simd::altmap_supported() && !use_xorjit;
4816 let cpu_kernel = if use_folded {
4817 CpuKernelKind::Folded
4818 } else {
4819 CpuKernelKind::Plain
4820 };
4821 #[cfg(target_arch = "aarch64")]
4827 let cpu_kernel = if neon_packed_enabled() {
4828 CpuKernelKind::NeonPacked
4829 } else {
4830 cpu_kernel
4831 };
4832 #[cfg(target_arch = "x86_64")]
4833 let cpu_kernel = jit_memo
4834 .as_ref()
4835 .map_or(cpu_kernel, |memo| CpuKernelKind::XorJit(memo.width));
4836 let method = cpu_kernel.method();
4837 let input_grouping = method.input_grouping();
4838 #[cfg(target_arch = "x86_64")]
4839 let persistent_jit_bytes = jit_memo.as_ref().map_or(0, JitMemo::reserved_bytes);
4840 #[cfg(not(target_arch = "x86_64"))]
4841 let persistent_jit_bytes = 0;
4842 let staging_width = method.staging_width();
4843 let (mut chunk_words, selected_budget, mut max_controller) = controller_execution_parameters(
4844 plan,
4845 options,
4846 method,
4847 staging_width,
4848 persistent_jit_bytes,
4849 workers,
4850 )?;
4851 debug_assert_eq!(selected_budget, budget);
4852 let max_byte_len = chunk_words * 2;
4853 let mut gpu = GpuComputeArm::engage(
4858 gpu_preferred || (!use_folded && !use_xorjit),
4859 n,
4860 max_byte_len,
4861 effective_bytes,
4862 );
4863 let mut gpu_staging = gpu.is_engaged();
4864 if gpu_staging {
4865 let extra_gpu_output_bytes = n
4866 .checked_mul(max_controller.layout().aligned_len)
4867 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
4868 reason: "GPU output allocation overflowed".to_string(),
4869 })?;
4870 let extra_gpu_staging_bytes = if use_folded || use_xorjit {
4871 max_controller
4872 .buffer_accounting()
4873 .staging_area_bytes
4874 .checked_mul(2)
4875 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
4876 reason: "GPU fallback staging allocation overflowed".to_string(),
4877 })?
4878 } else {
4879 0
4880 };
4881 let gpu_state_bytes = extra_gpu_output_bytes
4882 .checked_add(extra_gpu_staging_bytes)
4883 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
4884 reason: "GPU state accounting overflowed".to_string(),
4885 })?;
4886 let persistent_with_gpu_state = persistent_jit_bytes
4887 .checked_add(gpu_state_bytes)
4888 .ok_or_else(|| Par2Error::ResourceLimitExceeded {
4889 reason: "GPU state accounting overflowed".to_string(),
4890 })?;
4891 match controller_execution_parameters(
4892 plan,
4893 options,
4894 method,
4895 staging_width,
4896 persistent_with_gpu_state,
4897 workers,
4898 ) {
4899 Ok((words, _, controller)) => {
4900 chunk_words = words;
4901 max_controller = controller;
4902 }
4903 Err(error) => {
4904 warn!(reason = %error, "GPU state exceeds the repair memory limit; keeping the CPU controller");
4905 gpu.disable();
4906 gpu_staging = false;
4907 }
4908 }
4909 }
4910 let total_chunks_usize = word_count.div_ceil(chunk_words);
4911 let total_chunks = total_chunks_usize.min(u32::MAX as usize) as u32;
4912 debug_assert_eq!(max_controller.input_grouping(), input_grouping);
4913 #[cfg(any(
4914 all(feature = "metal", target_os = "macos", target_arch = "aarch64"),
4915 feature = "wgpu"
4916 ))]
4917 let max_aligned_len = max_controller.layout().aligned_len;
4918 let physical_row_len = max_controller.buffer_accounting().physical_row_len;
4919 let factor_setup_started = Instant::now();
4920 let memo = PreparedFactorMemo::from_matrix(&plan.input_factors, use_folded);
4921 let factor_setup = factor_setup_started.elapsed();
4922 let buffer_setup_started = Instant::now();
4923 let mut batch_sets = [
4924 Some(StreamBatchSet::new(
4925 physical_row_len,
4926 input_grouping,
4927 staging_width,
4928 n,
4929 use_folded,
4930 use_xorjit,
4931 gpu_staging,
4932 )),
4933 Some(StreamBatchSet::new(
4934 physical_row_len,
4935 input_grouping,
4936 staging_width,
4937 n,
4938 use_folded,
4939 use_xorjit,
4940 gpu_staging,
4941 )),
4942 ];
4943 let mut cpu_output_area = AlignedOutputArea::new(n, physical_row_len);
4944 let output_base = cpu_output_area.base();
4945 #[cfg(any(
4946 all(feature = "metal", target_os = "macos", target_arch = "aarch64"),
4947 feature = "wgpu"
4948 ))]
4949 let mut gpu_chunk_output: Vec<Vec<u8>> = vec![vec![0u8; max_aligned_len]; n];
4950 #[cfg(not(any(
4951 all(feature = "metal", target_os = "macos", target_arch = "aarch64"),
4952 feature = "wgpu"
4953 )))]
4954 let mut gpu_chunk_output: Vec<Vec<u8>> = Vec::new();
4955 let gpu_output_ptrs: Vec<usize> = gpu_chunk_output
4956 .iter_mut()
4957 .map(|output| output.as_mut_ptr() as usize)
4958 .collect();
4959 let buffer_setup = buffer_setup_started.elapsed();
4960 let timings = CpuControllerTimings::default();
4961
4962 info!(
4963 missing_slices = n,
4964 chunk_bytes = chunk_words * 2,
4965 budget_bytes = budget,
4966 source_batch = input_grouping,
4967 workers,
4968 backend = ?cpu_kernel,
4969 "repairing with CPU-controller streamed path"
4970 );
4971
4972 let repair_result = std::thread::scope(|scope| -> Result<()> {
4973 let (command_tx, command_rx) = std::sync::mpsc::sync_channel(2);
4974 let (complete_tx, complete_rx) = std::sync::mpsc::sync_channel(2);
4975 let (prepared_tx, prepared_rx) = std::sync::mpsc::sync_channel(1);
4976 let (submitted_tx, submitted_rx) = std::sync::mpsc::sync_channel(1);
4977 let (finished_tx, finished_rx) = std::sync::mpsc::sync_channel(2);
4978 let (compute_completion_tx, compute_completion_rx) =
4979 std::sync::mpsc::sync_channel(workers.saturating_mul(2).max(1));
4980 let mut compute_senders = Vec::with_capacity(workers);
4981 let mut compute_workers = Vec::with_capacity(workers);
4982 #[cfg(target_family = "wasm")]
4988 let inline_execution = !reedsolomon_rs::threading::parallel_enabled();
4989 #[cfg(target_family = "wasm")]
4990 let mut inline_compute = Vec::with_capacity(workers);
4991 for worker_index in 0..workers {
4992 let (sender, receiver) = std::sync::mpsc::sync_channel(1);
4993 let completion_tx = compute_completion_tx.clone();
4994 compute_senders.push(sender);
4995 #[cfg(target_family = "wasm")]
4996 if inline_execution {
4997 inline_compute.push(InlineComputeWorker::new(
4998 worker_index,
4999 receiver,
5000 completion_tx,
5001 ));
5002 continue;
5003 }
5004 compute_workers.push(scope.spawn(move || {
5005 run_compute_worker(worker_index, receiver, completion_tx);
5006 }));
5007 }
5008 drop(compute_completion_tx);
5009 let compute_submitter = CpuComputeSubmitter {
5010 senders: compute_senders,
5011 next_id: 0,
5012 };
5013 let mut compute_pool = CpuComputePool {
5014 completion_rx: compute_completion_rx,
5015 deferred: HashMap::new(),
5016 _lifetime: std::marker::PhantomData,
5017 };
5018 let preparation_trace = trace.clone();
5019 let preparation_memo = &memo;
5020 let preparation_timings = &timings;
5021 #[cfg(target_arch = "x86_64")]
5022 let preparation_jit_memo = jit_memo.as_ref();
5023 #[cfg(not(target_family = "wasm"))]
5024 let preparation_worker = scope.spawn(move || {
5025 run_guarded_preparation_worker(
5026 command_rx,
5027 complete_tx,
5028 prepared_tx,
5029 submitted_tx,
5030 finished_tx,
5031 cpu_kernel,
5032 method,
5033 output_base,
5034 n,
5035 preparation_memo,
5036 #[cfg(target_arch = "x86_64")]
5037 preparation_jit_memo,
5038 preparation_timings,
5039 compute_submitter,
5040 preparation_trace,
5041 )
5042 });
5043 #[cfg(target_family = "wasm")]
5045 let (preparation_worker, inline_workers) = if inline_execution {
5046 (
5047 None,
5048 Some(std::cell::RefCell::new(InlineControllerWorkers::new(
5049 command_rx,
5050 PreparationWorker::new(
5051 complete_tx,
5052 prepared_tx,
5053 submitted_tx,
5054 finished_tx,
5055 cpu_kernel,
5056 method,
5057 output_base,
5058 n,
5059 preparation_memo,
5060 preparation_timings,
5061 compute_submitter,
5062 preparation_trace,
5063 ),
5064 inline_compute,
5065 ))),
5066 )
5067 } else {
5068 (
5069 Some(scope.spawn(move || {
5070 run_guarded_preparation_worker(
5071 command_rx,
5072 complete_tx,
5073 prepared_tx,
5074 submitted_tx,
5075 finished_tx,
5076 cpu_kernel,
5077 method,
5078 output_base,
5079 n,
5080 preparation_memo,
5081 preparation_timings,
5082 compute_submitter,
5083 preparation_trace,
5084 )
5085 })),
5086 None,
5087 )
5088 };
5089 let mut preparer = CpuInputPreparer {
5090 command_tx,
5091 complete_rx,
5092 prepared_rx,
5093 submitted_rx,
5094 finished_rx,
5095 transfer_buffers: std::array::from_fn(|slot| {
5096 Some(TransferBuffer {
5097 slot,
5098 bytes: vec![0u8; physical_row_len],
5099 })
5100 }),
5101 transfer_buffer_len: physical_row_len,
5102 #[cfg(target_family = "wasm")]
5103 inline: inline_workers,
5104 };
5105
5106 let repair_result = (|| -> Result<()> {
5107 let mut chunk_idx = 0usize;
5108 let mut source_reader = None;
5109 while chunk_idx < total_chunks_usize {
5110 check_cancel(options)?;
5111
5112 let chunk_start = chunk_idx * chunk_words;
5113 let chunk_end = (chunk_start + chunk_words).min(word_count);
5114 let chunk_len = chunk_end - chunk_start;
5115 let byte_start = chunk_start * 2;
5116 let byte_len = chunk_len * 2;
5117 let controller = cpu_controller_plan(byte_len, n, workers, method, staging_width);
5118 let controller_layout = Arc::new(controller.layout().clone());
5119 debug!(
5120 chunk = chunk_idx,
5121 aligned_bytes = controller.layout().aligned_len,
5122 compute_chunk_bytes = controller.layout().chunk_len,
5123 compute_chunks = controller.layout().num_chunks,
5124 assignments = controller.layout().assignments.len(),
5125 input_grouping = controller.input_grouping(),
5126 "CPU repair controller plan"
5127 );
5128 let gpu_chunk = gpu.begin_chunk(byte_len);
5129 let mut gpu_failed = false;
5130 if gpu_chunk {
5131 let mut current_area = 0usize;
5134 let first_len = total_sources.min(controller.input_grouping());
5135 let mut current = fill_gpu_stream_batch(
5136 &mut preparer,
5137 batch_sets[current_area]
5138 .take()
5139 .expect("controller staging area available"),
5140 plan,
5141 par2_set,
5142 file_access,
5143 &mut recovery_files,
5144 &mut source_reader,
5145 available_inputs,
5146 0,
5147 first_len,
5148 byte_start,
5149 byte_len,
5150 controller.layout().aligned_len,
5151 controller.layout().chunk_len,
5152 controller.layout().num_chunks,
5153 options,
5154 &timings,
5155 )?;
5156 let mut spare = batch_sets[1 - current_area].take();
5157 let mut batch_start = 0usize;
5158 while batch_start < total_sources {
5159 check_cancel(options)?;
5160 if gpu.accumulate(¤t, plan, byte_len).is_err() {
5161 gpu_failed = true;
5162 break;
5163 }
5164 batch_start += current.len;
5165 if batch_start < total_sources {
5166 let next_len =
5167 (total_sources - batch_start).min(controller.input_grouping());
5168 let next = fill_gpu_stream_batch(
5169 &mut preparer,
5170 spare.take().expect("controller staging area available"),
5171 plan,
5172 par2_set,
5173 file_access,
5174 &mut recovery_files,
5175 &mut source_reader,
5176 available_inputs,
5177 batch_start,
5178 next_len,
5179 byte_start,
5180 byte_len,
5181 controller.layout().aligned_len,
5182 controller.layout().chunk_len,
5183 controller.layout().num_chunks,
5184 options,
5185 &timings,
5186 )?;
5187 spare = Some(current);
5188 current = next;
5189 current_area = 1 - current_area;
5190 }
5191 }
5192 batch_sets[current_area] = Some(current);
5193 batch_sets[1 - current_area] = spare;
5194 } else {
5195 let mut lifecycle = ControllerLifecycle::new(controller.input_grouping())
5199 .with_execution_trace(trace.clone());
5200 let mut active: [Option<CpuComputeTicket<'_>>; 2] = [None, None];
5201 let mut batch_order = VecDeque::<usize>::with_capacity(2);
5202 let mut preparing = [false; 2];
5203 let mut pending_prepared: [Option<InputBatch>; 2] = [None, None];
5204
5205 let mut input_start = 0usize;
5206 while input_start < total_sources {
5207 check_cancel(options)?;
5208 while receive_submitted_controller_batch(
5209 SubmittedReceiveMode::ReadyOnly,
5210 &preparer,
5211 options,
5212 &mut pending_prepared,
5213 &mut preparing,
5214 &trace,
5215 &mut active,
5216 )? {}
5217
5218 let read_area = lifecycle.current_staging_area;
5219 let buffer = preparer.take_transfer_buffer(options.cancel.as_ref())?;
5220 while receive_submitted_controller_batch(
5224 SubmittedReceiveMode::ReadyOnly,
5225 &preparer,
5226 options,
5227 &mut pending_prepared,
5228 &mut preparing,
5229 &trace,
5230 &mut active,
5231 )? {}
5232 let buffer = read_live_stream_input(
5233 &mut preparer,
5234 plan,
5235 par2_set,
5236 file_access,
5237 &mut recovery_files,
5238 &mut source_reader,
5239 available_inputs,
5240 input_start,
5241 buffer,
5242 read_area,
5243 byte_start,
5244 byte_len,
5245 controller.layout().aligned_len,
5246 options,
5247 &trace,
5248 )?;
5249
5250 if lifecycle.can_add() == ControllerAddStatus::Full {
5251 lifecycle.observe_backpressure();
5252 lifecycle.wait_for_add();
5253 let expected_area = lifecycle.current_staging_area;
5254 let completed_area = batch_order.front().copied().ok_or_else(|| {
5255 Par2Error::ReedSolomonError {
5256 reason: "CPU repair controller reached backpressure without a submitted batch"
5257 .to_string(),
5258 }
5259 })?;
5260 if completed_area != expected_area {
5261 return Err(Par2Error::ReedSolomonError {
5262 reason: format!(
5263 "CPU repair staging order mismatch: expected area {expected_area}, completed area {completed_area}"
5264 ),
5265 });
5266 }
5267 while active[completed_area].is_none() {
5268 receive_submitted_controller_batch(
5269 SubmittedReceiveMode::Wait,
5270 &preparer,
5271 options,
5272 &mut pending_prepared,
5273 &mut preparing,
5274 &trace,
5275 &mut active,
5276 )?;
5277 }
5278 batch_order.pop_front();
5279 complete_active_controller_batch(
5280 completed_area,
5281 &preparer,
5282 &mut compute_pool,
5283 &mut active,
5284 &mut batch_sets,
5285 &mut lifecycle,
5286 options,
5287 &timings,
5288 &trace,
5289 )?;
5290 }
5291
5292 let staging_area = lifecycle.current_staging_area;
5293 debug_assert_eq!(staging_area, read_area);
5294 if !preparing[staging_area] {
5295 begin_live_stream_batch(
5296 &preparer,
5297 batch_sets[staging_area]
5298 .take()
5299 .expect("controller staging area available"),
5300 input_start,
5301 controller.layout().aligned_len,
5302 controller.layout().chunk_len,
5303 Arc::clone(&controller_layout),
5304 &trace,
5305 )?;
5306 preparing[staging_area] = true;
5307 }
5308 let (accepted_area, submitted) = queue_live_stream_input(
5309 &mut preparer,
5310 &mut lifecycle,
5311 plan,
5312 input_start,
5313 buffer,
5314 &trace,
5315 )?;
5316 debug_assert_eq!(accepted_area, staging_area);
5317 input_start += 1;
5318 if let Some(batch) = submitted {
5319 if batch.staging_area != staging_area
5320 || pending_prepared[staging_area].replace(batch).is_some()
5321 {
5322 return Err(Par2Error::ReedSolomonError {
5323 reason: format!(
5324 "CPU repair controller resubmitted staging area {staging_area} before completion"
5325 ),
5326 });
5327 }
5328 batch_order.push_back(staging_area);
5329 }
5330 }
5331
5332 if let Some(batch) = lifecycle.end_input() {
5333 let staging_area = batch.staging_area;
5334 flush_live_stream_batch(&preparer, batch, &trace)?;
5335 if pending_prepared[staging_area].replace(batch).is_some() {
5336 return Err(Par2Error::ReedSolomonError {
5337 reason: format!(
5338 "CPU repair controller flushed occupied staging area {staging_area}"
5339 ),
5340 });
5341 }
5342 batch_order.push_back(staging_area);
5343 }
5344
5345 while let Some(staging_area) = batch_order.pop_front() {
5346 while active[staging_area].is_none() {
5347 receive_submitted_controller_batch(
5348 SubmittedReceiveMode::Wait,
5349 &preparer,
5350 options,
5351 &mut pending_prepared,
5352 &mut preparing,
5353 &trace,
5354 &mut active,
5355 )?;
5356 }
5357 complete_active_controller_batch(
5358 staging_area,
5359 &preparer,
5360 &mut compute_pool,
5361 &mut active,
5362 &mut batch_sets,
5363 &mut lifecycle,
5364 options,
5365 &timings,
5366 &trace,
5367 )?;
5368 }
5369 debug_assert!(preparing.iter().all(|active| !active));
5370 debug_assert!(pending_prepared.iter().all(Option::is_none));
5371 lifecycle.processing_finished();
5372 }
5373 if gpu_chunk
5374 && !gpu_failed
5375 && gpu.finish_chunk(&mut gpu_chunk_output, byte_len).is_err()
5376 {
5377 gpu_failed = true;
5378 }
5379 if gpu_failed {
5380 continue;
5383 }
5384
5385 finish_and_write_stream_outputs(
5386 &mut preparer,
5387 if gpu_chunk {
5388 OutputTransferLayout::PlainContiguous(gpu_output_ptrs.as_slice())
5389 } else {
5390 OutputTransferLayout::CpuEncodedChunkInterleaved {
5391 base: output_base,
5392 output_count: n,
5393 chunk_len: controller.layout().chunk_len,
5394 }
5395 },
5396 controller.layout().aligned_len,
5397 byte_start,
5398 byte_len,
5399 &write_targets,
5400 file_access,
5401 options,
5402 &timings,
5403 &trace,
5404 )?;
5405
5406 if let Some(ref progress) = options.progress {
5407 progress(ProgressUpdate {
5408 stage: ProgressStage::Repairing,
5409 current: chunk_idx as u32 + 1,
5410 total: total_chunks,
5411 bytes_processed: ((chunk_idx + 1) as u64)
5412 .saturating_mul(chunk_words as u64)
5413 .saturating_mul(2)
5414 .min(operation_total_bytes),
5415 total_bytes: Some(operation_total_bytes),
5416 phase: ProgressPhase::Whole,
5417 });
5418 }
5419 chunk_idx += 1;
5420 }
5421 Ok(())
5422 })();
5423 #[cfg(target_family = "wasm")]
5425 let inline_preparation_panicked = preparer.inline_preparation_panicked();
5426 drop(compute_pool);
5427 drop(preparer);
5428 let mut compute_panicked = false;
5429 for worker in compute_workers {
5430 compute_panicked |= worker.join().is_err();
5431 }
5432 if compute_panicked && repair_result.is_ok() {
5433 return Err(Par2Error::ReedSolomonError {
5434 reason: "CPU repair compute worker panicked".to_string(),
5435 });
5436 }
5437 #[cfg(not(target_family = "wasm"))]
5438 let preparation_panicked = preparation_worker.join().unwrap_or(true);
5439 #[cfg(target_family = "wasm")]
5443 let preparation_panicked = preparation_worker
5444 .map_or(inline_preparation_panicked, |worker| {
5445 worker.join().unwrap_or(true)
5446 });
5447 if preparation_panicked && repair_result.is_ok() {
5448 return Err(Par2Error::ReedSolomonError {
5449 reason: "CPU repair preparation worker panicked".to_string(),
5450 });
5451 }
5452 repair_result
5453 });
5454 repair_result?;
5455
5456 info!(
5457 missing_slices = n,
5458 total_us = CpuControllerTimings::duration_micros(controller_started.elapsed()),
5459 jit_setup_us = CpuControllerTimings::duration_micros(jit_setup),
5460 jit_prepare_work_us = CpuControllerTimings::micros(&timings.jit_prepare_ns),
5461 factor_setup_us = CpuControllerTimings::duration_micros(factor_setup),
5462 buffer_setup_us = CpuControllerTimings::duration_micros(buffer_setup),
5463 read_prepare_work_us = CpuControllerTimings::micros(&timings.read_prepare_ns),
5464 compute_work_us = CpuControllerTimings::micros(&timings.compute_ns),
5465 finish_us = CpuControllerTimings::micros(&timings.finish_ns),
5466 write_us = CpuControllerTimings::micros(&timings.write_ns),
5467 "streaming repair complete"
5468 );
5469 Ok(())
5470}
5471
5472pub struct RepairProblem<'a> {
5496 pub total_inputs: usize,
5498 pub word_count: usize,
5500 pub missing_indices: &'a [usize],
5502 pub available_indices: &'a [usize],
5504 pub recovery_exponents: &'a [u32],
5506 pub constants: &'a [u16],
5508 pub sources: &'a [&'a [u8]],
5510 pub outputs: &'a mut [&'a mut [u8]],
5512}
5513
5514impl RepairProblem<'_> {
5515 #[inline]
5517 pub fn slice_bytes(&self) -> usize {
5518 self.word_count * 2
5519 }
5520}
5521
5522#[derive(Debug, Clone, PartialEq, Eq)]
5524pub enum SolverError {
5525 Singular {
5527 bad_row: Option<usize>,
5529 },
5530 Dimensions(String),
5532 Cancelled,
5534 Host(String),
5536}
5537
5538impl std::fmt::Display for SolverError {
5539 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5540 match self {
5541 SolverError::Singular { bad_row: Some(row) } => {
5542 write!(f, "repair matrix is singular (recovery row {row})")
5543 }
5544 SolverError::Singular { bad_row: None } => write!(f, "repair matrix is singular"),
5545 SolverError::Dimensions(reason) => write!(f, "repair problem dimensions: {reason}"),
5546 SolverError::Cancelled => write!(f, "repair reconstruct cancelled"),
5547 SolverError::Host(reason) => write!(f, "repair solver host failure: {reason}"),
5548 }
5549 }
5550}
5551
5552impl std::error::Error for SolverError {}
5553
5554impl From<SolverError> for Par2Error {
5555 fn from(error: SolverError) -> Self {
5556 match error {
5557 SolverError::Cancelled => Par2Error::Cancelled,
5558 other => Par2Error::ReedSolomonError {
5559 reason: other.to_string(),
5560 },
5561 }
5562 }
5563}
5564
5565pub trait RepairSolver {
5574 fn reconstruct(&self, problem: &mut RepairProblem<'_>) -> std::result::Result<(), SolverError>;
5576}
5577
5578pub struct NativeRepairSolver<'a> {
5582 input_factors: &'a matrix::Matrix,
5583 chunk_words: usize,
5584 cancel: Option<CancellationToken>,
5585}
5586
5587impl<'a> NativeRepairSolver<'a> {
5588 pub fn new(input_factors: &'a matrix::Matrix, chunk_words: usize) -> Self {
5591 Self {
5592 input_factors,
5593 chunk_words,
5594 cancel: None,
5595 }
5596 }
5597
5598 pub fn with_cancellation(mut self, cancel: Option<CancellationToken>) -> Self {
5600 self.cancel = cancel;
5601 self
5602 }
5603}
5604
5605impl RepairSolver for NativeRepairSolver<'_> {
5606 fn reconstruct(&self, problem: &mut RepairProblem<'_>) -> std::result::Result<(), SolverError> {
5607 let n = problem.outputs.len();
5608 if n == 0 {
5609 return Ok(());
5610 }
5611 if self.input_factors.rows != n {
5612 return Err(SolverError::Dimensions(format!(
5613 "input_factors has {} rows but {n} outputs",
5614 self.input_factors.rows
5615 )));
5616 }
5617 if self.input_factors.cols != problem.sources.len() {
5618 return Err(SolverError::Dimensions(format!(
5619 "input_factors has {} cols but {} sources",
5620 self.input_factors.cols,
5621 problem.sources.len()
5622 )));
5623 }
5624
5625 let word_count = problem.word_count;
5626 let chunk_words = self.chunk_words.max(1);
5627 let total_chunks_usize = word_count.div_ceil(chunk_words);
5628
5629 let output_inputs = grouped_input_factors(self.input_factors);
5633 let mut factor_slots: HashMap<u16, usize> = HashMap::new();
5634 let mut prepared_factors: Vec<crate::gf_simd::PreparedInputFactor> = Vec::new();
5635 let prepared_output_inputs: Vec<Vec<(u16, usize)>> = output_inputs
5636 .iter()
5637 .map(|inputs| {
5638 inputs
5639 .iter()
5640 .map(|factor_input| {
5641 let slot = *factor_slots.entry(factor_input.factor).or_insert_with(|| {
5642 prepared_factors
5643 .push(crate::gf_simd::prepare_input_factor(factor_input.factor));
5644 prepared_factors.len() - 1
5645 });
5646 (factor_input.input_idx, slot)
5647 })
5648 .collect()
5649 })
5650 .collect();
5651
5652 let sources = problem.sources;
5653 let prepared_factors = &prepared_factors;
5654 let prepared_output_inputs = &prepared_output_inputs;
5655 let cancel = self.cancel.as_ref();
5656 let outputs = &mut *problem.outputs;
5657
5658 outputs.par_iter_mut().enumerate().try_for_each(
5659 |(output_idx, out)| -> std::result::Result<(), SolverError> {
5660 if let Some(cancel) = cancel
5661 && cancel.is_cancelled()
5662 {
5663 return Err(SolverError::Cancelled);
5664 }
5665 let out: &mut [u8] = out;
5666 let decode_inputs = &prepared_output_inputs[output_idx];
5667 let mut chunk_inputs = Vec::with_capacity(decode_inputs.len());
5668
5669 for chunk_idx in 0..total_chunks_usize {
5670 let chunk_start = chunk_idx * chunk_words;
5671 let chunk_end = (chunk_start + chunk_words).min(word_count);
5672 let byte_start = chunk_start * 2;
5673 let byte_len = (chunk_end - chunk_start) * 2;
5674
5675 chunk_inputs.clear();
5676 for (input_idx, factor_slot) in decode_inputs {
5677 chunk_inputs.push(crate::gf_simd::PreparedFactorSrc {
5678 prepared: &prepared_factors[*factor_slot],
5679 src: &sources[*input_idx as usize][byte_start..byte_start + byte_len],
5680 });
5681 }
5682
5683 crate::gf_simd::mul_acc_input_batch_prepared(
5684 &mut out[byte_start..byte_start + byte_len],
5685 &chunk_inputs,
5686 );
5687 }
5688
5689 Ok(())
5690 },
5691 )
5692 }
5693}
5694
5695#[cfg(target_arch = "wasm32")]
5698fn run_in_memory_repair<S: RepairSolver + ?Sized>(
5699 plan: &RepairPlan,
5700 par2_set: &Par2FileSet,
5701 file_access: &mut dyn FileAccess,
5702 options: &RepairOptions,
5703 solver: &S,
5704) -> Result<()> {
5705 let n = plan.missing_slices.len();
5706 if n == 0 {
5707 return Ok(());
5708 }
5709
5710 let slice_size = plan.slice_size as usize;
5711 assert!(
5712 slice_size.is_multiple_of(2),
5713 "PAR2 slice_size must be a multiple of 2"
5714 );
5715 let word_count = slice_size / 2;
5716 let available_inputs = plan.available_input_global_indices.len();
5717 let total_inputs = available_inputs + plan.recovery_exponents.len();
5718 let total_inputs_u32 = total_inputs.min(u32::MAX as usize) as u32;
5719 let read_total_bytes = (total_inputs as u64).saturating_mul(slice_size as u64);
5720 let repair_total_bytes = (n as u64).saturating_mul(slice_size as u64);
5721 let write_total_bytes = (n as u64).saturating_mul(slice_size as u64);
5722 let operation_total_bytes = read_total_bytes
5723 .saturating_add(repair_total_bytes)
5724 .saturating_add(write_total_bytes);
5725 let mut input_buffers: Vec<Vec<u8>> = vec![vec![0u8; slice_size]; total_inputs];
5726 let mut repaired_slices: Vec<Vec<u8>> = vec![vec![0u8; slice_size]; n];
5727
5728 for (input_idx, &global_idx) in plan.available_input_global_indices.iter().enumerate() {
5729 if input_idx % 64 == 0 {
5730 check_cancel(options)?;
5731 }
5732
5733 let (file_id, local_slice) = plan.global_to_file[global_idx];
5734 let offset = local_slice as u64 * plan.slice_size;
5735 let read_len = file_access
5736 .read_file_range_into(&file_id, offset, &mut input_buffers[input_idx])
5737 .map_err(Par2Error::Io)?;
5738 input_buffers[input_idx][read_len..].fill(0);
5739
5740 if let Some(ref progress) = options.progress {
5741 progress(ProgressUpdate {
5742 stage: ProgressStage::Repairing,
5743 current: input_idx as u32 + 1,
5744 total: total_inputs_u32,
5745 bytes_processed: (input_idx + 1) as u64 * slice_size as u64,
5746 total_bytes: Some(operation_total_bytes),
5747 phase: ProgressPhase::Whole,
5748 });
5749 }
5750 }
5751
5752 for (recovery_idx, &exp) in plan.recovery_exponents.iter().enumerate() {
5753 check_cancel(options)?;
5754 let rs = par2_set
5755 .recovery_slices
5756 .get(&exp)
5757 .ok_or_else(|| Par2Error::ReedSolomonError {
5758 reason: format!("recovery block with exponent {exp} not found"),
5759 })?;
5760 let recovery_data = rs.data.to_vec().map_err(Par2Error::Io)?;
5761 let copy_len = recovery_data.len().min(slice_size);
5762 input_buffers[available_inputs + recovery_idx][..copy_len]
5763 .copy_from_slice(&recovery_data[..copy_len]);
5764 input_buffers[available_inputs + recovery_idx][copy_len..].fill(0);
5765
5766 if let Some(ref progress) = options.progress {
5767 let current = available_inputs + recovery_idx + 1;
5768 progress(ProgressUpdate {
5769 stage: ProgressStage::Repairing,
5770 current: current.min(u32::MAX as usize) as u32,
5771 total: total_inputs_u32,
5772 bytes_processed: current as u64 * slice_size as u64,
5773 total_bytes: Some(operation_total_bytes),
5774 phase: ProgressPhase::Whole,
5775 });
5776 }
5777 }
5778
5779 check_cancel(options)?;
5780
5781 {
5784 let source_refs: Vec<&[u8]> = input_buffers.iter().map(|b| b.as_slice()).collect();
5785 let mut output_refs: Vec<&mut [u8]> = repaired_slices
5786 .iter_mut()
5787 .map(|b| b.as_mut_slice())
5788 .collect();
5789 let mut problem = RepairProblem {
5790 total_inputs: plan.total_input_slices,
5791 word_count,
5792 missing_indices: &plan.missing_global_indices,
5793 available_indices: &plan.available_input_global_indices,
5794 recovery_exponents: &plan.recovery_exponents,
5795 constants: &plan.constants,
5796 sources: &source_refs,
5797 outputs: &mut output_refs,
5798 };
5799 solver.reconstruct(&mut problem)?;
5800 }
5801
5802 if let Some(ref progress) = options.progress {
5803 progress(ProgressUpdate {
5804 stage: ProgressStage::Repairing,
5805 current: n as u32,
5806 total: n as u32,
5807 bytes_processed: read_total_bytes.saturating_add(repair_total_bytes),
5808 total_bytes: Some(operation_total_bytes),
5809 phase: ProgressPhase::Whole,
5810 });
5811 }
5812
5813 check_cancel(options)?;
5814 info!("writing repaired slices to files");
5815 let write_targets = build_write_targets(plan, par2_set)?;
5816 for (j, target) in write_targets.iter().enumerate() {
5817 check_cancel(options)?;
5818
5819 let slice_end = target.offset + plan.slice_size;
5820 let write_len = if slice_end > target.file_end {
5821 (target.file_end - target.offset) as usize
5822 } else {
5823 slice_size
5824 };
5825
5826 file_access
5827 .write_file_range(
5828 &target.file_id,
5829 target.offset,
5830 &repaired_slices[j][..write_len],
5831 )
5832 .map_err(|e| Par2Error::RepairWriteFailed {
5833 filename: target.filename.clone(),
5834 offset: target.offset,
5835 source: e,
5836 })?;
5837
5838 if let Some(ref progress) = options.progress {
5839 progress(ProgressUpdate {
5840 stage: ProgressStage::WritingRepaired,
5841 current: j as u32 + 1,
5842 total: n as u32,
5843 bytes_processed: read_total_bytes
5844 .saturating_add(repair_total_bytes)
5845 .saturating_add((j + 1) as u64 * slice_size as u64),
5846 total_bytes: Some(operation_total_bytes),
5847 phase: ProgressPhase::Whole,
5848 });
5849 }
5850 }
5851
5852 info!("repair complete: {} slices restored", n);
5853 Ok(())
5854}
5855
5856pub fn execute_repair_with_solver<S: RepairSolver + ?Sized>(
5861 plan: &RepairPlan,
5862 par2_set: &Par2FileSet,
5863 file_access: &mut dyn FileAccess,
5864 options: &RepairOptions,
5865 solver: &S,
5866) -> Result<()> {
5867 let n = plan.missing_slices.len();
5868 if n == 0 {
5869 return Ok(());
5870 }
5871 #[cfg(target_arch = "wasm32")]
5872 return run_in_memory_repair(plan, par2_set, file_access, options, solver);
5873
5874 #[cfg(not(target_arch = "wasm32"))]
5875 {
5876 let _ = (par2_set, file_access, options, solver);
5877 Err(Par2Error::ReedSolomonError {
5878 reason: "caller-provided in-memory PAR2 solvers are only supported on wasm32; native repair uses the streamed controller"
5879 .to_string(),
5880 })
5881 }
5882}
5883
5884pub fn execute_repair_with_options(
5886 plan: &RepairPlan,
5887 par2_set: &Par2FileSet,
5888 file_access: &mut dyn FileAccess,
5889 options: &RepairOptions,
5890) -> Result<()> {
5891 let n = plan.missing_slices.len();
5892 if n == 0 {
5893 return Ok(());
5894 }
5895
5896 let _cache_retention = crate::file_cache::CacheEvictionDeferral::acquire();
5899
5900 let slice_size = plan.slice_size as usize;
5901 assert!(
5902 slice_size.is_multiple_of(2),
5903 "PAR2 slice_size must be a multiple of 2"
5904 );
5905
5906 let budget = options.memory_limit.unwrap_or(DEFAULT_REPAIR_MEMORY_LIMIT);
5907
5908 match crate::repair_transform::try_execute(plan, par2_set, file_access, options, budget)? {
5915 crate::repair_transform::TransformOutcome::Executed => return Ok(()),
5916 crate::repair_transform::TransformOutcome::Declined(reason) => {
5917 debug!(reason, "repair transform arm declined");
5918 }
5919 crate::repair_transform::TransformOutcome::Diverged => {
5920 warn!("repair transform arm diverged; rerunning the repair on the dense path");
5921 }
5922 }
5923
5924 execute_repair_streaming(plan, par2_set, file_access, options, budget)
5925}
5926
5927#[cfg(test)]
5928pub(crate) mod tests {
5929 use super::*;
5930 use crate::checksum::{self, SliceChecksumState};
5931 use crate::packet::header;
5932 use crate::par2_set::{Par2FileSet, RecoverySlice};
5933 use crate::types::SliceChecksum;
5934 use crate::verify::{self, FileStatus, FileVerification, MemoryFileAccess};
5935 use bytes::Bytes;
5936 use md5::{Digest, Md5};
5937 use tempfile::tempdir;
5938
5939 #[test]
5940 fn plain_output_bypasses_every_cpu_finalizer() {
5941 let assert_plain = |kernel| {
5942 let mut output: Vec<u8> = (0..4096).map(|i| (i % 251) as u8).collect();
5943 let expected = output.clone();
5944 assert!(finalize_output_bytes(
5945 kernel,
5946 kernel.method(),
5947 OutputTransferSource::PlainContiguous(0).encoding(),
5948 &mut output,
5949 ));
5950 assert_eq!(output, expected, "plain output changed under {kernel:?}");
5951 };
5952
5953 for kernel in [CpuKernelKind::Plain, CpuKernelKind::Folded] {
5954 assert_plain(kernel);
5955 }
5956 #[cfg(target_arch = "x86_64")]
5957 if let Some(width) = reedsolomon_rs::xor_jit::JitWidth::detect() {
5958 assert_plain(CpuKernelKind::XorJit(width));
5959 }
5960 }
5961
5962 struct FailingReadAccess {
5963 inner: MemoryFileAccess,
5964 fail_after: usize,
5965 reads: std::sync::atomic::AtomicUsize,
5966 }
5967
5968 impl FileAccess for FailingReadAccess {
5969 fn read_file_range(
5970 &self,
5971 file_id: &FileId,
5972 offset: u64,
5973 len: u64,
5974 ) -> std::io::Result<Vec<u8>> {
5975 self.inner.read_file_range(file_id, offset, len)
5976 }
5977
5978 fn read_file_range_into(
5979 &self,
5980 file_id: &FileId,
5981 offset: u64,
5982 dst: &mut [u8],
5983 ) -> std::io::Result<usize> {
5984 let read = self
5985 .reads
5986 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
5987 if read >= self.fail_after {
5988 return Err(std::io::Error::other("injected controller read failure"));
5989 }
5990 self.inner.read_file_range_into(file_id, offset, dst)
5991 }
5992
5993 fn file_exists(&self, file_id: &FileId) -> bool {
5994 self.inner.file_exists(file_id)
5995 }
5996
5997 fn file_length(&self, file_id: &FileId) -> Option<u64> {
5998 self.inner.file_length(file_id)
5999 }
6000
6001 fn read_file(&self, file_id: &FileId) -> std::io::Result<Vec<u8>> {
6002 self.inner.read_file(file_id)
6003 }
6004
6005 fn write_file_range(
6006 &mut self,
6007 file_id: &FileId,
6008 offset: u64,
6009 data: &[u8],
6010 ) -> std::io::Result<()> {
6011 self.inner.write_file_range(file_id, offset, data)
6012 }
6013 }
6014
6015 struct CountingRangeAccess {
6016 inner: MemoryFileAccess,
6017 range_opens: std::sync::atomic::AtomicUsize,
6018 fallback_reads: std::sync::atomic::AtomicUsize,
6019 }
6020
6021 impl FileAccess for CountingRangeAccess {
6022 fn read_file_range(
6023 &self,
6024 file_id: &FileId,
6025 offset: u64,
6026 len: u64,
6027 ) -> std::io::Result<Vec<u8>> {
6028 self.inner.read_file_range(file_id, offset, len)
6029 }
6030
6031 fn read_file_range_into(
6032 &self,
6033 file_id: &FileId,
6034 offset: u64,
6035 dst: &mut [u8],
6036 ) -> std::io::Result<usize> {
6037 self.fallback_reads
6038 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
6039 self.inner.read_file_range_into(file_id, offset, dst)
6040 }
6041
6042 fn open_range_reader(
6043 &self,
6044 file_id: &FileId,
6045 ) -> std::io::Result<Option<Box<dyn FileRangeReader>>> {
6046 self.range_opens
6047 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
6048 Ok(Some(Box::new(std::io::Cursor::new(
6049 self.inner.read_file(file_id)?,
6050 ))))
6051 }
6052
6053 fn file_exists(&self, file_id: &FileId) -> bool {
6054 self.inner.file_exists(file_id)
6055 }
6056
6057 fn file_length(&self, file_id: &FileId) -> Option<u64> {
6058 self.inner.file_length(file_id)
6059 }
6060
6061 fn read_file(&self, file_id: &FileId) -> std::io::Result<Vec<u8>> {
6062 self.inner.read_file(file_id)
6063 }
6064
6065 fn write_file_range(
6066 &mut self,
6067 file_id: &FileId,
6068 offset: u64,
6069 data: &[u8],
6070 ) -> std::io::Result<()> {
6071 self.inner.write_file_range(file_id, offset, data)
6072 }
6073 }
6074
6075 struct FailingWriteAccess {
6076 inner: MemoryFileAccess,
6077 }
6078
6079 impl FileAccess for FailingWriteAccess {
6080 fn read_file_range(
6081 &self,
6082 file_id: &FileId,
6083 offset: u64,
6084 len: u64,
6085 ) -> std::io::Result<Vec<u8>> {
6086 self.inner.read_file_range(file_id, offset, len)
6087 }
6088
6089 fn read_file_range_into(
6090 &self,
6091 file_id: &FileId,
6092 offset: u64,
6093 dst: &mut [u8],
6094 ) -> std::io::Result<usize> {
6095 self.inner.read_file_range_into(file_id, offset, dst)
6096 }
6097
6098 fn file_exists(&self, file_id: &FileId) -> bool {
6099 self.inner.file_exists(file_id)
6100 }
6101
6102 fn file_length(&self, file_id: &FileId) -> Option<u64> {
6103 self.inner.file_length(file_id)
6104 }
6105
6106 fn read_file(&self, file_id: &FileId) -> std::io::Result<Vec<u8>> {
6107 self.inner.read_file(file_id)
6108 }
6109
6110 fn write_file_range(
6111 &mut self,
6112 _file_id: &FileId,
6113 _offset: u64,
6114 _data: &[u8],
6115 ) -> std::io::Result<()> {
6116 Err(std::io::Error::other("injected controller write failure"))
6117 }
6118 }
6119
6120 fn make_full_packet(packet_type: &[u8; 16], body: &[u8], recovery_set_id: [u8; 16]) -> Vec<u8> {
6122 let length = (header::HEADER_SIZE + body.len()) as u64;
6123 let mut hash_input = Vec::new();
6124 hash_input.extend_from_slice(&recovery_set_id);
6125 hash_input.extend_from_slice(packet_type);
6126 hash_input.extend_from_slice(body);
6127 let packet_hash: [u8; 16] = Md5::digest(&hash_input).into();
6128
6129 let mut data = Vec::new();
6130 data.extend_from_slice(header::MAGIC);
6131 data.extend_from_slice(&length.to_le_bytes());
6132 data.extend_from_slice(&packet_hash);
6133 data.extend_from_slice(&recovery_set_id);
6134 data.extend_from_slice(packet_type);
6135 data.extend_from_slice(body);
6136 data
6137 }
6138
6139 pub(crate) fn setup_repairable_set(
6143 file_data: &[u8],
6144 slice_size: u64,
6145 num_recovery: usize,
6146 ) -> (Par2FileSet, FileId) {
6147 let file_length = file_data.len() as u64;
6148 let hash_full = checksum::md5(file_data);
6149 let hash_16k_data = &file_data[..file_data.len().min(16384)];
6150 let hash_16k = checksum::md5(hash_16k_data);
6151
6152 let filename = b"testfile.dat";
6153 let mut id_input = Vec::new();
6154 id_input.extend_from_slice(&hash_16k);
6155 id_input.extend_from_slice(&file_length.to_le_bytes());
6156 id_input.extend_from_slice(filename);
6157 let file_id_bytes: [u8; 16] = Md5::digest(&id_input).into();
6158 let file_id = FileId::from_bytes(file_id_bytes);
6159
6160 let num_slices = if file_length == 0 {
6161 0
6162 } else {
6163 file_length.div_ceil(slice_size) as usize
6164 };
6165
6166 let mut checksums = Vec::new();
6167 for i in 0..num_slices {
6168 let offset = i as u64 * slice_size;
6169 let end = ((offset + slice_size) as usize).min(file_data.len());
6170 let slice_data = &file_data[offset as usize..end];
6171 let mut state = SliceChecksumState::new();
6172 state.update(slice_data);
6173 let pad_to = if (slice_data.len() as u64) < slice_size {
6174 Some(slice_size)
6175 } else {
6176 None
6177 };
6178 let (crc, md5) = state.finalize(pad_to);
6179 checksums.push(SliceChecksum { crc32: crc, md5 });
6180 }
6181
6182 let mut main_body = Vec::new();
6184 main_body.extend_from_slice(&slice_size.to_le_bytes());
6185 main_body.extend_from_slice(&1u32.to_le_bytes());
6186 main_body.extend_from_slice(&file_id_bytes);
6187 let rsid: [u8; 16] = Md5::digest(&main_body).into();
6188
6189 let mut fd_body = Vec::new();
6190 fd_body.extend_from_slice(&file_id_bytes);
6191 fd_body.extend_from_slice(&hash_full);
6192 fd_body.extend_from_slice(&hash_16k);
6193 fd_body.extend_from_slice(&file_length.to_le_bytes());
6194 fd_body.extend_from_slice(filename);
6195 while fd_body.len() % 4 != 0 {
6196 fd_body.push(0);
6197 }
6198
6199 let mut ifsc_body = Vec::new();
6200 ifsc_body.extend_from_slice(&file_id_bytes);
6201 for cs in &checksums {
6202 ifsc_body.extend_from_slice(&cs.md5);
6203 ifsc_body.extend_from_slice(&cs.crc32.to_le_bytes());
6204 }
6205
6206 let mut stream = Vec::new();
6207 stream.extend_from_slice(&make_full_packet(header::TYPE_MAIN, &main_body, rsid));
6208 stream.extend_from_slice(&make_full_packet(header::TYPE_FILE_DESC, &fd_body, rsid));
6209 stream.extend_from_slice(&make_full_packet(header::TYPE_IFSC, &ifsc_body, rsid));
6210
6211 let mut set = Par2FileSet::from_files(&[&stream]).unwrap();
6212
6213 let constants = gf::input_slice_constants(num_slices);
6215 let ss = slice_size as usize;
6216 let word_count = ss / 2;
6217
6218 let mut padded = file_data.to_vec();
6220 padded.resize(num_slices * ss, 0);
6221
6222 for r in 0..num_recovery {
6223 let exp = r as u32;
6224 let mut recovery = vec![0u8; ss];
6225
6226 for (i, &constant) in constants.iter().enumerate() {
6227 let factor = gf::pow(constant, exp);
6228 for w in 0..word_count {
6229 let input_word =
6230 u16::from_le_bytes([padded[i * ss + w * 2], padded[i * ss + w * 2 + 1]]);
6231 let contribution = gf::mul(input_word, factor);
6232 let rec_word = u16::from_le_bytes([recovery[w * 2], recovery[w * 2 + 1]]);
6233 let new_val = gf::add(rec_word, contribution);
6234 let bytes = new_val.to_le_bytes();
6235 recovery[w * 2] = bytes[0];
6236 recovery[w * 2 + 1] = bytes[1];
6237 }
6238 }
6239
6240 set.recovery_slices.insert(
6241 exp,
6242 RecoverySlice {
6243 exponent: exp,
6244 data: Bytes::from(recovery).into(),
6245 },
6246 );
6247 }
6248
6249 (set, file_id)
6250 }
6251
6252 fn spill_recovery_slices_to_disk(set: &mut Par2FileSet) -> tempfile::TempDir {
6253 let dir = tempdir().unwrap();
6254 for (exp, slice) in &mut set.recovery_slices {
6255 let path = dir.path().join(format!("recovery_{exp}.bin"));
6256 let bytes = slice.data.to_vec().unwrap();
6257 std::fs::write(&path, &bytes).unwrap();
6258 slice.data = crate::packet::RecoverySliceData::file_backed(path, 0, bytes.len());
6259 }
6260 dir
6261 }
6262
6263 fn spill_recovery_slices_to_disk_with_hashes(set: &mut Par2FileSet) -> tempfile::TempDir {
6267 let dir = tempdir().unwrap();
6268 let rsid = *set.recovery_set_id.as_bytes();
6269 for (exp, slice) in &mut set.recovery_slices {
6270 let path = dir.path().join(format!("recovery_{exp}.bin"));
6271 let bytes = slice.data.to_vec().unwrap();
6272 std::fs::write(&path, &bytes).unwrap();
6273
6274 let mut hash_input = Vec::new();
6275 hash_input.extend_from_slice(&rsid);
6276 hash_input.extend_from_slice(header::TYPE_RECOVERY);
6277 hash_input.extend_from_slice(&exp.to_le_bytes());
6278 hash_input.extend_from_slice(&bytes);
6279 let packet_hash: [u8; 16] = Md5::digest(&hash_input).into();
6280
6281 slice.data = crate::packet::RecoverySliceData::file_backed_with_hash(
6282 path,
6283 0,
6284 bytes.len(),
6285 packet_hash,
6286 );
6287 }
6288 dir
6289 }
6290
6291 #[test]
6292 fn plan_repair_skips_recovery_blocks_with_corrupt_payloads() {
6293 let slice_size = 64u64;
6294 let file_data: Vec<u8> = (0..256u32).map(|i| ((i * 11 + 3) % 256) as u8).collect();
6295 let (mut par2_set, file_id) = setup_repairable_set(&file_data, slice_size, 4);
6296 let spill_dir = spill_recovery_slices_to_disk_with_hashes(&mut par2_set);
6297
6298 for exp in [0u32, 1] {
6302 let path = spill_dir.path().join(format!("recovery_{exp}.bin"));
6303 let mut bytes = std::fs::read(&path).unwrap();
6304 bytes[7] ^= 0xFF;
6305 std::fs::write(&path, &bytes).unwrap();
6306 }
6307
6308 let mut damaged = file_data.clone();
6309 damaged[..64].fill(0);
6310 damaged[128..192].fill(0);
6311
6312 let mut access = MemoryFileAccess::new();
6313 access.add_file(file_id, damaged);
6314
6315 let result = verify::verify_all(&par2_set, &access);
6316 assert_eq!(result.total_missing_blocks, 2);
6317
6318 let plan = plan_repair(&par2_set, &result).unwrap();
6319 assert!(!plan.recovery_exponents.contains(&0));
6320 assert!(!plan.recovery_exponents.contains(&1));
6321
6322 execute_repair(&plan, &par2_set, &mut access).unwrap();
6323 let repaired = access.read_file(&file_id).unwrap();
6324 assert_eq!(repaired, file_data);
6325 }
6326
6327 #[test]
6328 fn plan_repair_fails_when_all_recovery_payloads_are_corrupt() {
6329 let slice_size = 64u64;
6330 let file_data: Vec<u8> = (0..256u32).map(|i| ((i * 5 + 1) % 256) as u8).collect();
6331 let (mut par2_set, file_id) = setup_repairable_set(&file_data, slice_size, 2);
6332 let spill_dir = spill_recovery_slices_to_disk_with_hashes(&mut par2_set);
6333
6334 for exp in [0u32, 1] {
6335 let path = spill_dir.path().join(format!("recovery_{exp}.bin"));
6336 let mut bytes = std::fs::read(&path).unwrap();
6337 bytes[0] ^= 0x01;
6338 std::fs::write(&path, &bytes).unwrap();
6339 }
6340
6341 let mut damaged = file_data.clone();
6342 damaged[..64].fill(0);
6343
6344 let mut access = MemoryFileAccess::new();
6345 access.add_file(file_id, damaged);
6346
6347 let result = verify::verify_all(&par2_set, &access);
6348 let err = plan_repair(&par2_set, &result).unwrap_err();
6349 assert!(matches!(err, Par2Error::InsufficientRecoveryData { .. }));
6350 }
6351
6352 #[test]
6353 fn end_to_end_repair_single_damaged_slice() {
6354 let slice_size = 64u64;
6356 let file_data: Vec<u8> = (0..256u32).map(|i| (i % 256) as u8).collect();
6357 let (par2_set, file_id) = setup_repairable_set(&file_data, slice_size, 2);
6358
6359 let mut damaged = file_data.clone();
6361 for item in damaged.iter_mut().take(192).skip(128) {
6362 *item ^= 0xFF;
6363 }
6364
6365 let mut access = MemoryFileAccess::new();
6366 access.add_file(file_id, damaged);
6367
6368 let result = verify::verify_all(&par2_set, &access);
6370 assert_eq!(result.total_missing_blocks, 1);
6371 assert!(matches!(
6372 result.repairable,
6373 Repairability::Repairable { .. }
6374 ));
6375
6376 let plan = plan_repair(&par2_set, &result).unwrap();
6378 assert_eq!(plan.missing_slices.len(), 1);
6379 assert_eq!(plan.missing_slices[0], (file_id, 2));
6380
6381 execute_repair(&plan, &par2_set, &mut access).unwrap();
6383
6384 let repaired = access.read_file(&file_id).unwrap();
6386 assert_eq!(repaired, file_data, "repaired data should match original");
6387 }
6388
6389 #[test]
6390 fn end_to_end_repair_multiple_damaged_slices() {
6391 let slice_size = 32u64;
6392 let file_data: Vec<u8> = (0..128u32).map(|i| ((i * 7 + 13) % 256) as u8).collect();
6393 let (par2_set, file_id) = setup_repairable_set(&file_data, slice_size, 3);
6394
6395 let mut damaged = file_data.clone();
6397 for item in damaged.iter_mut().take(32) {
6398 *item = 0;
6399 }
6400 for item in damaged.iter_mut().take(128).skip(96) {
6401 *item = 0;
6402 }
6403
6404 let mut access = MemoryFileAccess::new();
6405 access.add_file(file_id, damaged);
6406
6407 let result = verify::verify_all(&par2_set, &access);
6408 assert_eq!(result.total_missing_blocks, 2);
6409
6410 let plan = plan_repair(&par2_set, &result).unwrap();
6411 assert_eq!(plan.missing_slices.len(), 2);
6412
6413 execute_repair(&plan, &par2_set, &mut access).unwrap();
6414
6415 let repaired = access.read_file(&file_id).unwrap();
6416 assert_eq!(repaired, file_data);
6417 }
6418
6419 #[test]
6420 fn end_to_end_repair_missing_file() {
6421 let slice_size = 64u64;
6423 let file_data: Vec<u8> = (0..128u32).map(|i| (i % 256) as u8).collect();
6424 let (par2_set, file_id) = setup_repairable_set(&file_data, slice_size, 4);
6425
6426 let mut access = MemoryFileAccess::new();
6428 access.add_file(file_id, vec![0u8; 128]);
6429
6430 let result = verify::verify_all(&par2_set, &access);
6431 assert_eq!(result.total_missing_blocks, 2); let plan = plan_repair(&par2_set, &result).unwrap();
6434 assert_eq!(plan.missing_slices.len(), 2);
6435
6436 execute_repair(&plan, &par2_set, &mut access).unwrap();
6437
6438 let repaired = access.read_file(&file_id).unwrap();
6439 assert_eq!(repaired, file_data);
6440 }
6441
6442 #[test]
6443 fn plan_repair_not_needed() {
6444 let slice_size = 64u64;
6445 let file_data = vec![0xABu8; 128];
6446 let (par2_set, file_id) = setup_repairable_set(&file_data, slice_size, 2);
6447
6448 let mut access = MemoryFileAccess::new();
6449 access.add_file(file_id, file_data);
6450
6451 let result = verify::verify_all(&par2_set, &access);
6452 let err = plan_repair(&par2_set, &result).unwrap_err();
6453 assert!(matches!(err, Par2Error::ReedSolomonError { .. }));
6454 }
6455
6456 #[test]
6457 fn plan_repair_insufficient() {
6458 let slice_size = 64u64;
6459 let file_data: Vec<u8> = (0..256u32).map(|i| (i % 256) as u8).collect();
6460 let (par2_set, file_id) = setup_repairable_set(&file_data, slice_size, 1);
6461
6462 let mut damaged = file_data.clone();
6464 for item in damaged.iter_mut().take(64) {
6465 *item = 0;
6466 }
6467 for item in damaged.iter_mut().take(128).skip(64) {
6468 *item = 0;
6469 }
6470
6471 let mut access = MemoryFileAccess::new();
6472 access.add_file(file_id, damaged);
6473
6474 let result = verify::verify_all(&par2_set, &access);
6475 let err = plan_repair(&par2_set, &result).unwrap_err();
6476 assert!(matches!(err, Par2Error::InsufficientRecoveryData { .. }));
6477 }
6478
6479 #[test]
6480 fn plan_repair_rejects_resource_limited_verification() {
6481 let slice_size = 64u64;
6482 let file_data = vec![0xABu8; 128];
6483 let (par2_set, file_id) = setup_repairable_set(&file_data, slice_size, 2);
6484 let result = VerificationResult {
6485 files: vec![FileVerification {
6486 file_id,
6487 filename: "testfile.dat".to_string(),
6488 status: FileStatus::Damaged(0),
6489 valid_slices: Vec::new(),
6490 missing_slice_count: 0,
6491 }],
6492 recovery_blocks_available: 2,
6493 total_missing_blocks: 0,
6494 repairable: Repairability::ResourceLimited {
6495 reason: "file testfile.dat exceeds verifier slice limits".to_string(),
6496 },
6497 };
6498
6499 let err = plan_repair(&par2_set, &result).unwrap_err();
6500 assert!(matches!(err, Par2Error::ResourceLimitExceeded { .. }));
6501 }
6502
6503 #[test]
6504 fn matrix_memory_budget_has_floor_but_still_caps() {
6505 assert!(repair_matrix_limit_reason(4, 2, Some(8)).is_none());
6508
6509 let missing = 20_000usize;
6511 let reason = repair_matrix_limit_reason(32_768, missing, Some(8)).unwrap();
6512 assert!(reason.contains("matrix workspace budget"));
6513
6514 assert!(repair_matrix_limit_reason(32_768, missing, Some(8 << 30)).is_none());
6516
6517 let reason = repair_matrix_limit_reason(40_000, 1, None).unwrap();
6519 assert!(reason.contains("at most"));
6520 }
6521
6522 #[test]
6523 fn plan_repair_succeeds_with_tiny_configured_memory_limit() {
6524 let slice_size = 64u64;
6525 let file_data: Vec<u8> = (0..256u32).map(|i| (i % 256) as u8).collect();
6526 let (par2_set, file_id) = setup_repairable_set(&file_data, slice_size, 3);
6527
6528 let mut damaged = file_data.clone();
6529 damaged[..64].fill(0);
6530 damaged[64..128].fill(0);
6531
6532 let mut access = MemoryFileAccess::new();
6533 access.add_file(file_id, damaged);
6534
6535 let result = verify::verify_all(&par2_set, &access);
6536 assert_eq!(result.total_missing_blocks, 2);
6537
6538 let plan = plan_repair_with_memory_limit(&par2_set, &result, Some(8)).unwrap();
6539 assert_eq!(plan.missing_slices.len(), 2);
6540 }
6541
6542 #[test]
6543 fn plan_repair_rejects_sets_over_total_slice_limit() {
6544 let slice_size = 4u64;
6545 let slices_per_file = 20_000u64;
6546 let mut files = HashMap::new();
6547 let mut recovery_file_ids = Vec::new();
6548 let mut verifications = Vec::new();
6549 for index in 0..2u8 {
6550 let file_id = FileId::from_bytes([index + 1; 16]);
6551 recovery_file_ids.push(file_id);
6552 files.insert(
6553 file_id,
6554 crate::par2_set::FileDescription {
6555 file_id,
6556 hash_full: [0; 16],
6557 hash_16k: [0; 16],
6558 length: slice_size * slices_per_file,
6559 par2_name: format!("big{index}.dat"),
6560 filename: format!("big{index}.dat"),
6561 },
6562 );
6563 let mut valid_slices = vec![true; slices_per_file as usize];
6564 if index == 0 {
6565 valid_slices[0] = false;
6566 }
6567 verifications.push(FileVerification {
6568 file_id,
6569 filename: format!("big{index}.dat"),
6570 status: if index == 0 {
6571 FileStatus::Damaged(1)
6572 } else {
6573 FileStatus::Complete
6574 },
6575 missing_slice_count: u32::from(index == 0),
6576 valid_slices,
6577 });
6578 }
6579
6580 let mut recovery_slices = std::collections::BTreeMap::new();
6581 recovery_slices.insert(
6582 0,
6583 RecoverySlice {
6584 exponent: 0,
6585 data: Bytes::from(vec![0u8; slice_size as usize]).into(),
6586 },
6587 );
6588 let par2_set = Par2FileSet {
6589 recovery_set_id: crate::types::RecoverySetId::from_bytes([9; 16]),
6590 slice_size,
6591 recovery_file_ids,
6592 non_recovery_file_ids: Vec::new(),
6593 files,
6594 slice_checksums: HashMap::new(),
6595 recovery_slices,
6596 creator: None,
6597 };
6598 let result = VerificationResult {
6599 files: verifications,
6600 recovery_blocks_available: 1,
6601 total_missing_blocks: 1,
6602 repairable: Repairability::Repairable {
6603 blocks_needed: 1,
6604 blocks_available: 1,
6605 },
6606 };
6607
6608 let err = plan_repair(&par2_set, &result).unwrap_err();
6609 assert!(matches!(err, Par2Error::ResourceLimitExceeded { .. }));
6610 }
6611
6612 #[test]
6613 fn repair_with_partial_last_slice() {
6614 let slice_size = 64u64;
6616 let file_data: Vec<u8> = (0..100u32).map(|i| ((i * 3 + 5) % 256) as u8).collect();
6618 let (par2_set, file_id) = setup_repairable_set(&file_data, slice_size, 2);
6619
6620 let mut damaged = file_data.clone();
6622 for item in damaged.iter_mut().take(100).skip(64) {
6623 *item = 0;
6624 }
6625
6626 let mut access = MemoryFileAccess::new();
6627 access.add_file(file_id, damaged);
6628
6629 let result = verify::verify_all(&par2_set, &access);
6630 assert_eq!(result.total_missing_blocks, 1);
6631
6632 let plan = plan_repair(&par2_set, &result).unwrap();
6633 execute_repair(&plan, &par2_set, &mut access).unwrap();
6634
6635 let repaired = access.read_file(&file_id).unwrap();
6636 assert_eq!(repaired, file_data);
6637 }
6638
6639 #[test]
6640 fn repair_with_tiny_memory_limit_still_succeeds() {
6641 let slice_size = 128u64;
6642 let file_data: Vec<u8> = (0..384u32).map(|i| ((i * 9 + 17) % 256) as u8).collect();
6643 let (par2_set, file_id) = setup_repairable_set(&file_data, slice_size, 3);
6644
6645 let mut damaged = file_data.clone();
6646 for item in damaged.iter_mut().take(128) {
6647 *item = 0;
6648 }
6649 for item in damaged.iter_mut().take(384).skip(256) {
6650 *item = 0;
6651 }
6652
6653 let mut access = MemoryFileAccess::new();
6654 access.add_file(file_id, damaged);
6655
6656 let result = verify::verify_all(&par2_set, &access);
6657 let plan = plan_repair(&par2_set, &result).unwrap();
6658
6659 execute_repair_with_options(
6660 &plan,
6661 &par2_set,
6662 &mut access,
6663 &RepairOptions {
6664 memory_limit: Some(constrained_memory_limit_for_test(&plan)),
6665 ..RepairOptions::default()
6666 },
6667 )
6668 .unwrap();
6669
6670 let repaired = access.read_file(&file_id).unwrap();
6671 assert_eq!(repaired, file_data);
6672 }
6673
6674 #[test]
6675 fn streaming_controller_rotates_two_full_groups_and_flushes_partial_group() {
6676 let slice_size = 64u64;
6677 let file_data: Vec<u8> = (0..25 * slice_size as u32)
6678 .map(|i| ((i * 29 + 7) % 251) as u8)
6679 .collect();
6680 let (par2_set, file_id) = setup_repairable_set(&file_data, slice_size, 4);
6681
6682 let mut damaged = file_data.clone();
6683 for slice in [0usize, 12, 24] {
6684 let start = slice * slice_size as usize;
6685 damaged[start..start + slice_size as usize].fill(0);
6686 }
6687 let mut access = MemoryFileAccess::new();
6688 access.add_file(file_id, damaged);
6689
6690 let verification = verify::verify_all(&par2_set, &access);
6691 let plan = plan_repair(&par2_set, &verification).unwrap();
6692 assert_eq!(plan.input_factors.cols, 25);
6693 let trace = ControllerExecutionTrace::capture();
6694 execute_repair_streaming_with_trace(
6695 &plan,
6696 &par2_set,
6697 &mut access,
6698 &RepairOptions {
6699 memory_limit: Some(constrained_memory_limit_for_test(&plan)),
6700 ..RepairOptions::default()
6701 },
6702 constrained_memory_limit_for_test(&plan),
6703 trace.clone(),
6704 )
6705 .unwrap();
6706
6707 assert_eq!(access.read_file(&file_id).unwrap(), file_data);
6708 let events = trace.events();
6709 let reads = events
6710 .iter()
6711 .filter(|event| matches!(event, ControllerExecutionEvent::SourceRead { .. }))
6712 .count();
6713 let queued = events
6714 .iter()
6715 .filter(|event| matches!(event, ControllerExecutionEvent::InputQueued { .. }))
6716 .count();
6717 let prepared = events
6718 .iter()
6719 .filter(|event| matches!(event, ControllerExecutionEvent::PreparationCompleted { .. }))
6720 .count();
6721 let submitted = events
6722 .iter()
6723 .filter(|event| matches!(event, ControllerExecutionEvent::ComputeSubmitted { .. }))
6724 .count();
6725 let completed = events
6726 .iter()
6727 .filter(|event| matches!(event, ControllerExecutionEvent::ComputeCompleted { .. }))
6728 .count();
6729 let lifecycle_submitted = events
6730 .iter()
6731 .filter(|event| matches!(event, ControllerExecutionEvent::BatchSubmitted { .. }))
6732 .count();
6733 let rotations = events
6734 .iter()
6735 .filter(|event| matches!(event, ControllerExecutionEvent::StagingRotated { .. }))
6736 .count();
6737 assert!(reads > 12);
6738 assert_eq!(reads, queued);
6739 assert!(prepared > 2);
6740 assert_eq!(prepared, submitted);
6741 assert_eq!(submitted, completed);
6742 assert_eq!(lifecycle_submitted, submitted);
6743 assert_eq!(rotations, lifecycle_submitted);
6744 assert!(
6745 events
6746 .iter()
6747 .all(|event| !matches!(event, ControllerExecutionEvent::Failed { .. }))
6748 );
6749 for staging_area in 0..2 {
6750 let prepared = events
6751 .iter()
6752 .position(|event| matches!(event, ControllerExecutionEvent::PreparationCompleted { staging_area: area, .. } if *area == staging_area));
6753 let submitted = events
6754 .iter()
6755 .position(|event| matches!(event, ControllerExecutionEvent::ComputeSubmitted { staging_area: area, .. } if *area == staging_area));
6756 if let (Some(prepared), Some(submitted)) = (prepared, submitted) {
6757 assert!(submitted < prepared);
6758 }
6759 }
6760 let first_wait = events
6761 .iter()
6762 .position(|event| matches!(event, ControllerExecutionEvent::WaitForAdd { .. }))
6763 .expect("two active staging areas trigger waitForAdd");
6764 assert!(
6765 events[..first_wait]
6766 .iter()
6767 .filter(|event| matches!(event, ControllerExecutionEvent::BatchSubmitted { .. }))
6768 .count()
6769 >= 2,
6770 "the live controller must not wait after submitting only one staging area"
6771 );
6772 assert!(events[..first_wait].iter().any(|event| matches!(
6773 event,
6774 ControllerExecutionEvent::SourceRead {
6775 source_index: 24,
6776 staging_area: 0,
6777 }
6778 )));
6779 let input_ended = events
6780 .iter()
6781 .position(|event| matches!(event, ControllerExecutionEvent::InputEnded { .. }))
6782 .expect("input lifecycle ended");
6783 let processing_finished = events
6784 .iter()
6785 .position(|event| matches!(event, ControllerExecutionEvent::ProcessingFinished))
6786 .expect("processing lifecycle finished");
6787 assert!(input_ended < processing_finished);
6788 }
6789
6790 #[test]
6791 fn streaming_controller_read_failure_does_not_accept_partial_batch() {
6792 let slice_size = 64u64;
6793 let file_data: Vec<u8> = (0..25 * slice_size as u32)
6794 .map(|i| ((i * 17 + 5) % 251) as u8)
6795 .collect();
6796 let (par2_set, file_id) = setup_repairable_set(&file_data, slice_size, 4);
6797 let mut damaged = file_data.clone();
6798 for slice in [0usize, 12, 24] {
6799 let start = slice * slice_size as usize;
6800 damaged[start..start + slice_size as usize].fill(0);
6801 }
6802
6803 let mut verification_access = MemoryFileAccess::new();
6804 verification_access.add_file(file_id, damaged.clone());
6805 let verification = verify::verify_all(&par2_set, &verification_access);
6806 let plan = plan_repair(&par2_set, &verification).unwrap();
6807
6808 let mut inner = MemoryFileAccess::new();
6809 inner.add_file(file_id, damaged.clone());
6810 let mut access = FailingReadAccess {
6811 inner,
6812 fail_after: 1,
6813 reads: std::sync::atomic::AtomicUsize::new(0),
6814 };
6815 let trace = ControllerExecutionTrace::capture();
6816 let error = execute_repair_streaming_with_trace(
6817 &plan,
6818 &par2_set,
6819 &mut access,
6820 &RepairOptions {
6821 memory_limit: Some(constrained_memory_limit_for_test(&plan)),
6822 ..RepairOptions::default()
6823 },
6824 constrained_memory_limit_for_test(&plan),
6825 trace.clone(),
6826 )
6827 .unwrap_err();
6828
6829 assert!(matches!(error, Par2Error::Io(_)));
6830 assert_eq!(access.read_file(&file_id).unwrap(), damaged);
6831 assert!(trace.events().iter().any(|event| matches!(
6832 event,
6833 ControllerExecutionEvent::Failed {
6834 phase: ControllerFailurePhase::Read
6835 }
6836 )));
6837 }
6838
6839 #[test]
6840 fn streaming_controller_honors_cancellation_before_mutation() {
6841 let slice_size = 64u64;
6842 let file_data: Vec<u8> = (0..13 * slice_size as u32)
6843 .map(|i| ((i * 13 + 11) % 251) as u8)
6844 .collect();
6845 let (par2_set, file_id) = setup_repairable_set(&file_data, slice_size, 2);
6846 let mut damaged = file_data.clone();
6847 damaged[..slice_size as usize].fill(0);
6848
6849 let mut access = MemoryFileAccess::new();
6850 access.add_file(file_id, damaged.clone());
6851 let verification = verify::verify_all(&par2_set, &access);
6852 let plan = plan_repair(&par2_set, &verification).unwrap();
6853 let cancel = CancellationToken::new();
6854 cancel.cancel();
6855 let error = execute_repair_with_options(
6856 &plan,
6857 &par2_set,
6858 &mut access,
6859 &RepairOptions {
6860 memory_limit: Some(256),
6861 cancel: Some(cancel),
6862 ..RepairOptions::default()
6863 },
6864 )
6865 .unwrap_err();
6866
6867 assert!(matches!(error, Par2Error::Cancelled));
6868 assert_eq!(access.read_file(&file_id).unwrap(), damaged);
6869 }
6870
6871 #[test]
6872 fn streaming_controller_reuses_seekable_source_reader() {
6873 let slice_size = 64u64;
6874 let file_data: Vec<u8> = (0..25 * slice_size as u32)
6875 .map(|i| ((i * 7 + 19) % 251) as u8)
6876 .collect();
6877 let (par2_set, file_id) = setup_repairable_set(&file_data, slice_size, 4);
6878 let mut damaged = file_data.clone();
6879 for slice in [0usize, 12, 24] {
6880 let start = slice * slice_size as usize;
6881 damaged[start..start + slice_size as usize].fill(0);
6882 }
6883
6884 let mut verification_access = MemoryFileAccess::new();
6885 verification_access.add_file(file_id, damaged.clone());
6886 let verification = verify::verify_all(&par2_set, &verification_access);
6887 let plan = plan_repair(&par2_set, &verification).unwrap();
6888
6889 let mut inner = MemoryFileAccess::new();
6890 inner.add_file(file_id, damaged);
6891 let mut access = CountingRangeAccess {
6892 inner,
6893 range_opens: std::sync::atomic::AtomicUsize::new(0),
6894 fallback_reads: std::sync::atomic::AtomicUsize::new(0),
6895 };
6896 execute_repair_with_options(
6897 &plan,
6898 &par2_set,
6899 &mut access,
6900 &RepairOptions {
6901 memory_limit: Some(constrained_memory_limit_for_test(&plan)),
6902 ..RepairOptions::default()
6903 },
6904 )
6905 .unwrap();
6906
6907 assert!(
6908 access
6909 .range_opens
6910 .load(std::sync::atomic::Ordering::Relaxed)
6911 > 0
6912 );
6913 assert_eq!(
6914 access
6915 .fallback_reads
6916 .load(std::sync::atomic::Ordering::Relaxed),
6917 0
6918 );
6919 assert_eq!(access.read_file(&file_id).unwrap(), file_data);
6920 }
6921
6922 #[test]
6923 fn streaming_controller_output_failure_is_not_accepted() {
6924 let slice_size = 64u64;
6925 let file_data: Vec<u8> = (0..13 * slice_size as u32)
6926 .map(|i| ((i * 31 + 3) % 251) as u8)
6927 .collect();
6928 let (par2_set, file_id) = setup_repairable_set(&file_data, slice_size, 2);
6929 let mut damaged = file_data.clone();
6930 damaged[..slice_size as usize].fill(0);
6931
6932 let mut verification_access = MemoryFileAccess::new();
6933 verification_access.add_file(file_id, damaged.clone());
6934 let verification = verify::verify_all(&par2_set, &verification_access);
6935 let plan = plan_repair(&par2_set, &verification).unwrap();
6936
6937 let mut inner = MemoryFileAccess::new();
6938 inner.add_file(file_id, damaged.clone());
6939 let mut access = FailingWriteAccess { inner };
6940 let trace = ControllerExecutionTrace::capture();
6941 let error = execute_repair_streaming_with_trace(
6942 &plan,
6943 &par2_set,
6944 &mut access,
6945 &RepairOptions {
6946 memory_limit: Some(constrained_memory_limit_for_test(&plan)),
6947 ..RepairOptions::default()
6948 },
6949 constrained_memory_limit_for_test(&plan),
6950 trace.clone(),
6951 )
6952 .unwrap_err();
6953
6954 assert!(matches!(error, Par2Error::RepairWriteFailed { .. }));
6955 assert_eq!(access.read_file(&file_id).unwrap(), damaged);
6956 assert!(trace.events().iter().any(|event| matches!(
6957 event,
6958 ControllerExecutionEvent::Failed {
6959 phase: ControllerFailurePhase::Write
6960 }
6961 )));
6962 }
6963
6964 #[test]
6965 fn compute_worker_failure_is_reported_before_output_transfer() {
6966 let factors = matrix::Matrix::identity(1);
6967 let memo = PreparedFactorMemo::from_matrix(&factors, false);
6968 let mut set = StreamBatchSet::new(2, 1, 1, 1, false, false, false);
6969 set.len = 1;
6970 set.coefficients[0] = 1;
6971 let layout = ControllerLayout {
6972 aligned_len: 4,
6973 chunk_len: 4,
6974 num_chunks: 1,
6975 assignments: vec![crate::cpu_repair_controller::WorkAssignment {
6976 worker: 0,
6977 byte_start: 0,
6978 byte_len: 4,
6979 output_start: 0,
6980 output_len: 1,
6981 }],
6982 worker_count: 1,
6983 stride: 2,
6984 };
6985 let mut output = vec![0x5au8; 4];
6986 let trace = ControllerExecutionTrace::capture();
6987 let context = Arc::new(CpuComputeContext {
6988 output_base: output.as_mut_ptr() as usize,
6989 output_count: 1,
6990 set,
6991 memo: &memo,
6992 #[cfg(target_arch = "x86_64")]
6993 jit_memo: None,
6994 #[cfg(target_arch = "x86_64")]
6995 jit_batch: None,
6996 layout: Arc::new(layout),
6997 method: CpuKernelKind::Plain.method(),
6998 trace: trace.clone(),
6999 folded_coefficients: FoldedBatchCoefficients::None,
7000 add: false,
7001 });
7002
7003 std::thread::scope(|scope| {
7004 let (job_tx, job_rx) = std::sync::mpsc::sync_channel(1);
7005 let (completion_tx, completion_rx) = std::sync::mpsc::sync_channel(1);
7006 let worker = scope.spawn(move || run_compute_worker(0, job_rx, completion_tx));
7007 job_tx
7008 .send(CpuComputeJob {
7009 id: 7,
7010 context: Arc::clone(&context),
7011 })
7012 .unwrap();
7013 drop(job_tx);
7014 let completion = completion_rx.recv().unwrap();
7015 assert_eq!(completion.id, 7);
7016 assert!(completion.failure.is_some());
7017 worker.join().unwrap();
7018 });
7019 assert_eq!(output, vec![0x5a; 4]);
7020 assert!(trace.events().iter().any(|event| matches!(
7021 event,
7022 ControllerExecutionEvent::Failed {
7023 phase: ControllerFailurePhase::Compute
7024 }
7025 )));
7026 }
7027
7028 #[test]
7029 fn folded_coefficient_one_add_preserves_split_blocks() {
7030 let mut destination = vec![0x11; crate::gf_simd::SPLIT_BLOCK_BYTES * 2];
7031 let mut staging = vec![0u8; destination.len() * crate::gf_simd::FOLDED_GROUP];
7032 for (index, byte) in staging.iter_mut().enumerate() {
7033 *byte = index as u8;
7034 }
7035 let expected = destination
7036 .iter()
7037 .enumerate()
7038 .map(|(index, byte)| {
7039 (0..crate::gf_simd::FOLDED_GROUP).fold(*byte, |value, lane| {
7040 value
7041 ^ staging[index / crate::gf_simd::SPLIT_BLOCK_BYTES
7042 * crate::gf_simd::FOLDED_GROUP
7043 * crate::gf_simd::SPLIT_BLOCK_BYTES
7044 + (index % crate::gf_simd::SPLIT_BLOCK_BYTES)
7045 + lane * crate::gf_simd::SPLIT_BLOCK_BYTES]
7046 })
7047 })
7048 .collect::<Vec<_>>();
7049 xor_folded_group_into(&mut destination, &staging, crate::gf_simd::FOLDED_GROUP);
7050 assert_eq!(destination, expected);
7051 }
7052
7053 #[test]
7054 fn compute_wait_drains_workers_before_returning_cancelled() {
7055 let (completion_tx, completion_rx) = std::sync::mpsc::sync_channel(2);
7056 for worker in 0..2 {
7057 completion_tx
7058 .send(CpuComputeCompletion {
7059 id: 17,
7060 worker,
7061 elapsed: Duration::ZERO,
7062 failure: None,
7063 })
7064 .unwrap();
7065 }
7066 let cancel = CancellationToken::new();
7067 cancel.cancel();
7068 let factors = matrix::Matrix::identity(1);
7069 let memo = PreparedFactorMemo::from_matrix(&factors, false);
7070 let context = Arc::new(CpuComputeContext {
7071 output_base: std::ptr::NonNull::<u8>::dangling().as_ptr() as usize,
7072 output_count: 0,
7073 set: StreamBatchSet::new(2, 1, 1, 1, false, false, false),
7074 memo: &memo,
7075 #[cfg(target_arch = "x86_64")]
7076 jit_memo: None,
7077 #[cfg(target_arch = "x86_64")]
7078 jit_batch: None,
7079 layout: Arc::new(ControllerLayout {
7080 aligned_len: 2,
7081 chunk_len: 2,
7082 num_chunks: 1,
7083 assignments: Vec::new(),
7084 worker_count: 1,
7085 stride: 2,
7086 }),
7087 method: CpuKernelKind::Plain.method(),
7088 trace: ControllerExecutionTrace::default(),
7089 folded_coefficients: FoldedBatchCoefficients::None,
7090 add: false,
7091 });
7092 let mut pool = CpuComputePool {
7093 completion_rx,
7094 deferred: HashMap::new(),
7095 _lifetime: std::marker::PhantomData,
7096 };
7097 let error = match pool.wait(
7098 CpuComputeTicket {
7099 id: 17,
7100 expected: 2,
7101 submission_failure: None,
7102 context,
7103 },
7104 Some(&cancel),
7105 &CpuControllerTimings::default(),
7106 ) {
7107 Ok(_) => panic!("cancelled compute wait unexpectedly succeeded"),
7108 Err(error) => error,
7109 };
7110 assert!(matches!(error, Par2Error::Cancelled));
7111 }
7112
7113 #[test]
7114 fn preparation_failure_closes_batch_without_output() {
7115 let set = StreamBatchSet::new(64, 12, 12, 1, false, false, false);
7116 let trace = ControllerExecutionTrace::capture();
7117 let worker_trace = trace.clone();
7118 let factors = matrix::Matrix::identity(1);
7119 let memo = PreparedFactorMemo::from_matrix(&factors, false);
7120 let timings = CpuControllerTimings::default();
7121 std::thread::scope(|scope| {
7122 let (command_tx, command_rx) = std::sync::mpsc::sync_channel(2);
7123 let (complete_tx, complete_rx) = std::sync::mpsc::sync_channel(2);
7124 let (prepared_tx, prepared_rx) = std::sync::mpsc::sync_channel(1);
7125 let (submitted_tx, submitted_rx) = std::sync::mpsc::sync_channel(1);
7126 let (finished_tx, _finished_rx) = std::sync::mpsc::sync_channel(1);
7127 let compute_submitter = CpuComputeSubmitter {
7128 senders: Vec::new(),
7129 next_id: 0,
7130 };
7131 let worker_memo = &memo;
7132 let worker_timings = &timings;
7133 let worker = scope.spawn(move || {
7134 run_preparation_worker(
7135 command_rx,
7136 complete_tx,
7137 prepared_tx,
7138 submitted_tx,
7139 finished_tx,
7140 CpuKernelKind::Plain,
7141 CpuKernelKind::Plain.method(),
7142 std::ptr::NonNull::<u8>::dangling().as_ptr() as usize,
7143 1,
7144 worker_memo,
7145 #[cfg(target_arch = "x86_64")]
7146 None,
7147 worker_timings,
7148 compute_submitter,
7149 worker_trace,
7150 );
7151 });
7152 command_tx
7153 .send(PreparationMessage::Begin(PrepareBatch {
7154 set,
7155 aligned_len: 64,
7156 chunk_len: 64,
7157 layout: None,
7158 }))
7159 .unwrap();
7160 command_tx
7161 .send(PreparationMessage::Input {
7162 lane: 0,
7163 coefficients: Vec::new(),
7164 buffer: TransferBuffer {
7165 slot: 0,
7166 bytes: vec![0u8; 64],
7167 },
7168 submitted: Some(InputBatch {
7169 staging_area: 0,
7170 input_start: 0,
7171 input_len: 1,
7172 add: false,
7173 reason: crate::cpu_repair_controller::BatchSubmitReason::GroupFull,
7174 }),
7175 })
7176 .unwrap();
7177 drop(command_tx);
7178 assert!(worker.join().is_ok());
7179 assert!(complete_rx.recv().is_err());
7180 assert!(prepared_rx.recv().is_err());
7181 assert!(submitted_rx.recv().is_err());
7182 });
7183 assert!(trace.events().iter().any(|event| matches!(
7184 event,
7185 ControllerExecutionEvent::Failed {
7186 phase: ControllerFailurePhase::Prepare
7187 }
7188 )));
7189 }
7190
7191 #[test]
7192 fn controller_transfer_buffers_are_a_fixed_two_slot_protocol() {
7193 let (command_tx, _command_rx) = std::sync::mpsc::sync_channel(1);
7194 let (complete_tx, complete_rx) = std::sync::mpsc::sync_channel(2);
7195 let (_prepared_tx, prepared_rx) = std::sync::mpsc::sync_channel(1);
7196 let (_submitted_tx, submitted_rx) = std::sync::mpsc::sync_channel(1);
7197 let (_finished_tx, finished_rx) = std::sync::mpsc::sync_channel(1);
7198 let mut preparer = CpuInputPreparer {
7199 command_tx,
7200 complete_rx,
7201 prepared_rx,
7202 submitted_rx,
7203 finished_rx,
7204 transfer_buffers: std::array::from_fn(|_| None),
7205 transfer_buffer_len: 8,
7206 #[cfg(target_family = "wasm")]
7210 inline: None,
7211 };
7212
7213 complete_tx
7214 .send(TransferBuffer {
7215 slot: 0,
7216 bytes: vec![0; 8],
7217 })
7218 .unwrap();
7219 complete_tx
7220 .send(TransferBuffer {
7221 slot: 1,
7222 bytes: vec![0; 8],
7223 })
7224 .unwrap();
7225 preparer.restore_transfer_buffers(None).unwrap();
7226
7227 let first = preparer.take_transfer_buffer(None).unwrap();
7228 let second = preparer.take_transfer_buffer(None).unwrap();
7229 assert_eq!((first.slot, second.slot), (0, 1));
7230 assert!(preparer.transfer_buffers.iter().all(Option::is_none));
7231
7232 preparer.return_transfer_buffer(first).unwrap();
7233 let duplicate = TransferBuffer {
7234 slot: 0,
7235 bytes: vec![0; 8],
7236 };
7237 assert!(preparer.return_transfer_buffer(duplicate).is_err());
7238 assert!(
7239 preparer
7240 .return_transfer_buffer(TransferBuffer {
7241 slot: 2,
7242 bytes: vec![0; 8],
7243 })
7244 .is_err()
7245 );
7246 assert!(
7247 preparer
7248 .return_transfer_buffer(TransferBuffer {
7249 slot: 1,
7250 bytes: vec![0; 7],
7251 })
7252 .is_err()
7253 );
7254
7255 preparer.return_transfer_buffer(second).unwrap();
7256 assert!(preparer.transfer_buffers.iter().all(Option::is_some));
7257 }
7258
7259 #[test]
7260 fn controller_output_area_is_aligned_and_stable() {
7261 let mut area = AlignedOutputArea::new(3, 65_537);
7262 let first = area.base();
7263 let second = area.base();
7264 assert_eq!(first, second);
7265 assert_eq!(first % 64, 0);
7266 assert!(area.cells.len() * std::mem::size_of::<StagingCell>() >= 3 * 65_537);
7267 }
7268
7269 #[test]
7270 fn packed_checksum_is_linear_and_rejects_mutation() {
7271 const DATA_LEN: usize = 96;
7272 const BLOCK_LEN: usize = 32;
7273 let mut left = vec![0u8; DATA_LEN + BLOCK_LEN];
7274 let mut right = vec![0u8; DATA_LEN + BLOCK_LEN];
7275 for (index, byte) in left[..DATA_LEN].iter_mut().enumerate() {
7276 *byte = (index as u8).wrapping_mul(17).wrapping_add(3);
7277 }
7278 for (index, byte) in right[..DATA_LEN].iter_mut().enumerate() {
7279 *byte = (index as u8).wrapping_mul(29).wrapping_add(11);
7280 }
7281 write_packed_checksum(&mut left, DATA_LEN, BLOCK_LEN, BLOCK_LEN);
7282 write_packed_checksum(&mut right, DATA_LEN, BLOCK_LEN, BLOCK_LEN);
7283
7284 let mut combined: Vec<u8> = left
7285 .iter()
7286 .zip(&right)
7287 .map(|(left, right)| left ^ right)
7288 .collect();
7289 assert!(packed_checksum_matches(
7290 &combined,
7291 BLOCK_LEN * 3,
7292 BLOCK_LEN,
7293 BLOCK_LEN
7294 ));
7295
7296 combined[41] ^= 0x80;
7297 assert!(!packed_checksum_matches(
7298 &combined,
7299 BLOCK_LEN * 3,
7300 BLOCK_LEN,
7301 BLOCK_LEN
7302 ));
7303 }
7304
7305 #[test]
7311 fn packed_checksum_matches_original_scalar_algorithm() {
7312 fn reference(checksum: &mut [u8], block: &[u8]) {
7314 let width = checksum.len();
7315 for lane in (0..width).step_by(2) {
7316 let mut folded = 0u16;
7317 for region in block.chunks_exact(width) {
7318 folded ^= u16::from_le_bytes([region[lane], region[lane + 1]]);
7319 }
7320 let previous = u16::from_le_bytes([checksum[lane], checksum[lane + 1]]);
7321 checksum[lane..lane + 2]
7322 .copy_from_slice(&(gf16_mul2(previous) ^ folded).to_le_bytes());
7323 }
7324 }
7325
7326 let shapes = [
7332 (2usize, 2usize),
7333 (2, 32),
7334 (4, 64),
7335 (6, 12),
7336 (16, 32),
7337 (16, 128),
7338 (32, 32),
7339 (32, 96),
7340 (64, 64),
7341 (64, 256),
7342 ];
7343 #[cfg(target_arch = "aarch64")]
7346 let mut neon_shapes_covered = 0usize;
7347
7348 for (width, block_len) in shapes {
7349 for blocks in [1usize, 2, 7] {
7350 let data_len = block_len * blocks;
7351 let data: Vec<u8> = (0..data_len)
7352 .map(|i| ((i * 37 + width * 5 + 1) % 256) as u8)
7353 .collect();
7354
7355 let mut got = vec![0u8; width];
7356 let mut want = vec![0u8; width];
7357 for block in data.chunks_exact(block_len) {
7358 update_packed_checksum(&mut got, block);
7359 reference(&mut want, block);
7360 }
7361 assert_eq!(
7362 got, want,
7363 "checksum differs width={width} block_len={block_len} blocks={blocks}"
7364 );
7365
7366 #[cfg(target_arch = "aarch64")]
7367 {
7368 let mut neon = vec![0u8; width];
7371 let mut neon_ran = true;
7372 for block in data.chunks_exact(block_len) {
7373 neon_ran &= parpar_neon_checksum::update_block(&mut neon, block);
7374 }
7375 let swept = parpar_neon_checksum::fold(&data, block_len, width);
7378
7379 if width % 16 == 0 {
7380 assert!(
7381 neon_ran && swept.is_some(),
7382 "NEON port declined a 16-byte-multiple shape \
7383 width={width} block_len={block_len}"
7384 );
7385 assert_eq!(
7386 neon, want,
7387 "NEON per-block differs width={width} \
7388 block_len={block_len} blocks={blocks}"
7389 );
7390 assert_eq!(
7391 &swept.expect("swept")[..width],
7392 &want[..],
7393 "NEON sweep differs width={width} \
7394 block_len={block_len} blocks={blocks}"
7395 );
7396 neon_shapes_covered += 1;
7397 } else {
7398 assert!(
7401 !neon_ran && swept.is_none(),
7402 "NEON port accepted a shape ParPar has no form for: \
7403 width={width} block_len={block_len}"
7404 );
7405 }
7406 }
7407 }
7408 }
7409
7410 #[cfg(target_arch = "aarch64")]
7411 assert_eq!(
7412 neon_shapes_covered, 18,
7413 "NEON coverage went vacuous: expected all six 16-byte-multiple \
7414 shapes across three block counts"
7415 );
7416 #[cfg(not(target_arch = "aarch64"))]
7417 eprintln!("SKIP: NEON checksum arm not covered — target is not aarch64");
7418 }
7419
7420 #[cfg(target_arch = "aarch64")]
7424 #[test]
7425 fn parpar_neon_vec_mul2_matches_scalar_exhaustively() {
7426 let zero_block = [0u8; 16];
7429 for base in (0..=0xFFFFu32).step_by(8) {
7430 let lanes: [u16; 8] = std::array::from_fn(|i| (base + i as u32) as u16);
7431 let mut checksum = [0u8; 16];
7432 for (i, lane) in lanes.iter().enumerate() {
7433 checksum[i * 2..i * 2 + 2].copy_from_slice(&lane.to_le_bytes());
7434 }
7435 assert!(parpar_neon_checksum::update_block(
7436 &mut checksum,
7437 &zero_block
7438 ));
7439 for (i, lane) in lanes.iter().enumerate() {
7440 assert_eq!(
7441 &checksum[i * 2..i * 2 + 2],
7442 &gf16_mul2(*lane).to_le_bytes(),
7443 "lane {lane:#06x} differs"
7444 );
7445 }
7446 }
7447 }
7448
7449 #[cfg(target_arch = "aarch64")]
7454 #[test]
7455 fn parpar_neon_fold_agrees_with_portable_arm() {
7456 for (width, block_len) in [
7460 (16usize, 16usize),
7461 (16, 32),
7462 (16, 128),
7463 (32, 32),
7464 (32, 96),
7465 (48, 96),
7466 (64, 256),
7467 ] {
7468 for blocks in [0usize, 1, 3, 9] {
7469 let data: Vec<u8> = (0..block_len * blocks)
7470 .map(|i| ((i * 131 + width * 7 + 5) % 256) as u8)
7471 .collect();
7472 let neon = parpar_neon_checksum::fold(&data, block_len, width)
7473 .expect("NEON arm handles 16-byte-multiple widths");
7474 let mut portable = [0u8; 64];
7475 for block in data.chunks_exact(block_len) {
7476 update_packed_checksum(&mut portable[..width], block);
7477 }
7478 assert_eq!(
7479 &neon[..width],
7480 &portable[..width],
7481 "width={width} block_len={block_len} blocks={blocks}"
7482 );
7483 }
7484 }
7485 }
7486
7487 #[cfg(target_arch = "aarch64")]
7492 #[test]
7493 fn neon_packed_is_the_oracle_contract_and_reaches_the_checksum_port() {
7494 let method = CpuKernelKind::NeonPacked.method();
7495 assert_eq!(method.stride, 32, "oracle blockLen is sizeof(uint8x16x2_t)");
7499 assert_eq!(
7500 method.checksum_width, 16,
7501 "oracle checksum width is sizeof(uint8x16_t)"
7502 );
7503 assert_eq!(
7504 method.checksum_width / 16,
7505 1,
7506 "PLANES == 1 is gf16_checksum_block_neon verbatim, not a widened form"
7507 );
7508
7509 let plain = CpuKernelKind::Plain.method();
7512 assert_eq!(method.ideal_input_multiple, plain.ideal_input_multiple);
7513 assert_eq!(method.staging_multiple, plain.staging_multiple);
7514 assert_eq!(method.alignment, plain.alignment);
7515 assert_eq!(method.ideal_chunk_size, plain.ideal_chunk_size);
7516
7517 assert!(
7520 parpar_neon_checksum::fold(&[0u8; 128], method.stride, method.checksum_width).is_some(),
7521 "NeonPacked must reach the NEON checksum port"
7522 );
7523 assert!(
7524 parpar_neon_checksum::fold(&[0u8; 128], plain.stride, plain.checksum_width).is_none(),
7525 "Plain's 2-byte contract has no oracle NEON form — that was the gap"
7526 );
7527
7528 if std::env::var_os("WEAVER_PAR2_NEON_PACKED").is_none() {
7530 assert!(
7531 neon_packed_enabled(),
7532 "NeonPacked must be default-on when the pin is unset"
7533 );
7534 }
7535 }
7536
7537 #[test]
7540 fn gf16_mul2_x4_matches_scalar_exhaustively() {
7541 for base in (0..=0xFFFFu32).step_by(4) {
7542 let lanes: [u16; 4] = [
7543 base as u16,
7544 (base + 1) as u16,
7545 (base + 2) as u16,
7546 (base + 3) as u16,
7547 ];
7548 let mut packed = [0u8; 8];
7549 for (i, lane) in lanes.iter().enumerate() {
7550 packed[i * 2..i * 2 + 2].copy_from_slice(&lane.to_le_bytes());
7551 }
7552 let got = gf16_mul2_x4(u64::from_le_bytes(packed)).to_le_bytes();
7553 for (i, lane) in lanes.iter().enumerate() {
7554 let want = gf16_mul2(*lane).to_le_bytes();
7555 assert_eq!(&got[i * 2..i * 2 + 2], &want, "lane {lane:#06x} differs");
7556 }
7557 }
7558 }
7559
7560 #[test]
7561 fn repair_with_file_backed_recovery_streaming_succeeds() {
7562 let slice_size = 128u64;
7563 let file_data: Vec<u8> = (0..384u32).map(|i| ((i * 9 + 17) % 256) as u8).collect();
7564 let (mut par2_set, file_id) = setup_repairable_set(&file_data, slice_size, 3);
7565 let _spill_dir = spill_recovery_slices_to_disk(&mut par2_set);
7566
7567 let mut damaged = file_data.clone();
7568 for item in damaged.iter_mut().take(128) {
7569 *item = 0;
7570 }
7571 for item in damaged.iter_mut().take(384).skip(256) {
7572 *item = 0;
7573 }
7574
7575 let mut access = MemoryFileAccess::new();
7576 access.add_file(file_id, damaged);
7577
7578 let result = verify::verify_all(&par2_set, &access);
7579 let plan = plan_repair(&par2_set, &result).unwrap();
7580
7581 execute_repair_with_options(
7582 &plan,
7583 &par2_set,
7584 &mut access,
7585 &RepairOptions {
7586 memory_limit: Some(constrained_memory_limit_for_test(&plan)),
7587 ..RepairOptions::default()
7588 },
7589 )
7590 .unwrap();
7591
7592 let repaired = access.read_file(&file_id).unwrap();
7593 assert_eq!(repaired, file_data);
7594 }
7595
7596 fn synthetic_plan(missing_slices: usize, slice_size: u64) -> RepairPlan {
7597 RepairPlan {
7598 missing_slices: (0..missing_slices)
7599 .map(|i| (FileId::from_bytes([i as u8; 16]), i as u32))
7600 .collect(),
7601 missing_global_indices: (0..missing_slices).collect(),
7602 available_input_global_indices: Vec::new(),
7603 recovery_exponents: (0..missing_slices as u32).collect(),
7604 decode_matrix: matrix::Matrix {
7605 rows: missing_slices,
7606 cols: missing_slices,
7607 data: vec![1; missing_slices.saturating_mul(missing_slices)],
7608 },
7609 input_factors: matrix::Matrix {
7610 rows: missing_slices,
7611 cols: missing_slices,
7612 data: vec![1; missing_slices.saturating_mul(missing_slices)],
7613 },
7614 slice_size,
7615 constants: vec![1; missing_slices],
7616 total_input_slices: missing_slices,
7617 global_to_file: (0..missing_slices)
7618 .map(|i| (FileId::from_bytes([i as u8; 16]), i as u32))
7619 .collect(),
7620 }
7621 }
7622
7623 fn minimum_controller_bytes_for_test(plan: &RepairPlan, kernel: CpuKernelKind) -> usize {
7624 let method = kernel.method();
7625 cpu_controller_plan(
7626 2,
7627 plan.missing_slices.len(),
7628 rayon::current_num_threads().max(1),
7629 method,
7630 method.staging_width(),
7631 )
7632 .buffer_accounting()
7633 .total_bytes
7634 }
7635
7636 fn constrained_memory_limit_for_test(plan: &RepairPlan) -> usize {
7637 let required = minimum_controller_bytes_for_test(plan, CpuKernelKind::Plain).max(
7638 minimum_controller_bytes_for_test(plan, CpuKernelKind::Folded),
7639 );
7640 #[cfg(target_arch = "x86_64")]
7641 let mut required = required;
7642 #[cfg(target_arch = "x86_64")]
7643 if let Some(width) = reedsolomon_rs::xor_jit::JitWidth::detect() {
7644 let kernel = CpuKernelKind::XorJit(width);
7645 let controller_bytes = minimum_controller_bytes_for_test(plan, kernel);
7646 let memo = JitMemo::new(
7647 width,
7648 kernel.method(),
7649 plan.missing_slices.len(),
7650 &plan.input_factors.data,
7651 0,
7652 usize::MAX,
7653 )
7654 .expect("detected XOR-JIT method has bounded controller accounting");
7655 required = required.max(controller_bytes.saturating_add(memo.reserved_bytes()));
7656 }
7657 required
7658 }
7659
7660 #[test]
7661 fn controller_budget_below_physical_minimum_is_rejected_without_mutation() {
7662 let plan = synthetic_plan(1, 64);
7663 let kernel = CpuKernelKind::Plain;
7664 let method = kernel.method();
7665 let minimum = minimum_controller_bytes_for_test(&plan, kernel);
7666 assert!(minimum > 0);
7667 let error = controller_execution_parameters(
7668 &plan,
7669 &RepairOptions {
7670 memory_limit: Some(minimum - 1),
7671 ..RepairOptions::default()
7672 },
7673 method,
7674 method.staging_width(),
7675 0,
7676 rayon::current_num_threads().max(1),
7677 )
7678 .unwrap_err();
7679 assert!(matches!(error, Par2Error::ResourceLimitExceeded { .. }));
7680 }
7681
7682 #[test]
7683 fn controller_parameters_shrink_chunks_to_a_tight_budget() {
7684 let plan = synthetic_plan(450, 1024 * 1024);
7685 let kernel = CpuKernelKind::Plain;
7686 let method = kernel.method();
7687 let (chunk_words, budget, _) = controller_execution_parameters(
7688 &plan,
7689 &RepairOptions {
7690 memory_limit: Some(50 * 1024 * 1024),
7691 ..RepairOptions::default()
7692 },
7693 method,
7694 method.staging_width(),
7695 0,
7696 4,
7697 )
7698 .unwrap();
7699 assert_eq!(budget, 50 * 1024 * 1024);
7700 assert!(chunk_words < plan.slice_size as usize / 2);
7701 }
7702
7703 #[test]
7704 fn cpu_controller_keeps_kernel_grouping_for_small_source_sets() {
7705 let method = CpuKernelKind::Plain.method();
7706 let expected_grouping = method.input_grouping();
7707 for sources in [1, 2, 3, 4, 5, 23, 24] {
7708 let controller = cpu_controller_plan(4096, 2, 4, method, expected_grouping);
7709 assert_eq!(controller.input_grouping(), expected_grouping);
7710 assert_eq!(
7711 crate::cpu_repair_controller::ControllerLifecycle::simulate(
7712 sources,
7713 controller.input_grouping(),
7714 )
7715 .batches
7716 .iter()
7717 .map(|batch| batch.input_len)
7718 .sum::<usize>(),
7719 sources
7720 );
7721 let set = StreamBatchSet::new(
7722 controller.layout().aligned_len,
7723 controller.input_grouping(),
7724 controller.input_grouping(),
7725 2,
7726 false,
7727 false,
7728 false,
7729 );
7730 assert_eq!(set.bufs.len(), expected_grouping);
7731 }
7732 }
7733
7734 #[test]
7735 fn gpu_staging_keeps_plain_sources_for_packed_cpu_fallback() {
7736 let set = StreamBatchSet::new(256, 6, 6, 2, false, true, true);
7737 assert_eq!(set.bufs.len(), 6);
7738 assert!(!set.packed.is_empty());
7739 }
7740
7741 fn controller_bytes_for_test(
7742 chunk_words: usize,
7743 output_count: usize,
7744 workers: usize,
7745 method: CpuMethodContract,
7746 ) -> usize {
7747 cpu_controller_plan(
7748 chunk_words.saturating_mul(2),
7749 output_count,
7750 workers,
7751 method,
7752 method.staging_width(),
7753 )
7754 .buffer_accounting()
7755 .total_bytes
7756 }
7757
7758 #[test]
7759 fn chunk_sizing_takes_the_largest_chunk_the_budget_holds() {
7760 let method = CpuKernelKind::Plain.method();
7761 let word_count = 64 * 1024 / 2;
7762 let workers = 12;
7763 let budget = 64 * 1024 * 1024;
7764 for output_count in [1usize, 512, 3000] {
7765 let chunk = largest_fitting_chunk_words(
7766 word_count,
7767 output_count,
7768 workers,
7769 method,
7770 method.staging_width(),
7771 budget,
7772 )
7773 .expect("a 64 MiB budget holds at least one word");
7774 assert!(controller_bytes_for_test(chunk, output_count, workers, method) <= budget);
7775 assert!(
7776 chunk == word_count
7777 || controller_bytes_for_test(chunk + 1, output_count, workers, method) > budget,
7778 "chunk {chunk} for {output_count} outputs is not the largest fit"
7779 );
7780 let mut halved = word_count;
7783 while controller_bytes_for_test(halved, output_count, workers, method) > budget {
7784 halved = halved.div_ceil(2);
7785 }
7786 assert!(
7787 halved <= chunk,
7788 "halving beat the exact fit at {output_count}"
7789 );
7790 }
7791 }
7792
7793 #[cfg(target_arch = "x86_64")]
7794 fn xorjit_selection_for_test(output_count: usize, budget: usize) -> XorJitSelection {
7795 let width = reedsolomon_rs::xor_jit::JitWidth::Avx2;
7796 XorJitSelection {
7797 width,
7798 jit_method: CpuKernelKind::XorJit(width).method(),
7799 baseline_method: CpuKernelKind::Folded.method(),
7800 output_count,
7801 word_count: 64 * 1024 / 2,
7802 workers: 12,
7803 budget,
7804 }
7805 }
7806
7807 #[cfg(target_arch = "x86_64")]
7808 fn active_arena_reservation_for_test(selection: XorJitSelection) -> usize {
7809 reedsolomon_rs::xor_jit::packed::PackedJitBatch::active_arena_upper_bound(
7810 selection.width,
7811 selection.output_count,
7812 selection.jit_method.input_grouping(),
7813 )
7814 .expect("the AVX2 arena bound is finite")
7815 * 2
7816 }
7817
7818 #[cfg(target_arch = "x86_64")]
7819 #[test]
7820 fn xorjit_declines_when_its_active_arenas_cost_a_repair_chunk() {
7821 for budget in [64 * 1024 * 1024, DEFAULT_REPAIR_MEMORY_LIMIT] {
7825 for output_count in [2048usize, 3000] {
7826 let selection = xorjit_selection_for_test(output_count, budget);
7827 let decision =
7828 selection.budget_decision(active_arena_reservation_for_test(selection));
7829 assert!(
7830 !decision.accepted(),
7831 "{output_count} missing slices at {budget} bytes: {decision:?}"
7832 );
7833 }
7834 }
7835 }
7836
7837 #[cfg(target_arch = "x86_64")]
7838 #[test]
7839 fn xorjit_is_selected_when_its_reservation_costs_no_chunk() {
7840 let selection = xorjit_selection_for_test(4, DEFAULT_REPAIR_MEMORY_LIMIT);
7841 let decision = selection.budget_decision(active_arena_reservation_for_test(selection));
7842 assert!(decision.accepted(), "{decision:?}");
7843 }
7844
7845 #[cfg(target_arch = "x86_64")]
7846 #[test]
7847 fn xorjit_capacity_shortfall_selects_the_non_jit_kernel() {
7848 let mut selection = xorjit_selection_for_test(8192, DEFAULT_REPAIR_MEMORY_LIMIT);
7851 assert!(active_arena_reservation_for_test(selection) > selection.budget);
7852 selection.jit_method.strict_wx_available = true;
7855 let selected = selection
7856 .select_memo(&[1u16, 2, 3])
7857 .expect("a capacity shortfall is a selection outcome, not an error");
7858 assert!(selected.is_none());
7859 }
7860
7861 #[cfg(target_arch = "x86_64")]
7865 #[test]
7866 fn xorjit_codebook_sized_reservation_keeps_the_whole_slice() {
7867 let selection = xorjit_selection_for_test(3000, 512 * 1024 * 1024);
7868 let full_controller_bytes = controller_bytes_for_test(
7869 selection.word_count,
7870 selection.output_count,
7871 selection.workers,
7872 selection.jit_method,
7873 );
7874 let decision =
7875 selection.budget_decision(selection.budget.saturating_sub(full_controller_bytes));
7876 assert_eq!(decision.jit_chunk_words, Some(selection.word_count));
7877 assert!(decision.accepted(), "{decision:?}");
7878 }
7879
7880 #[cfg(target_arch = "x86_64")]
7881 #[test]
7882 fn xorjit_selects_the_codebook_at_a_large_limit() {
7883 if !reedsolomon_rs::xor_jit::strict_wx_available() {
7884 return;
7887 }
7888 let selection = xorjit_selection_for_test(3000, 512 * 1024 * 1024);
7889 let factors = (1u16..=3000).collect::<Vec<_>>();
7890 let memo = selection
7891 .select_memo(&factors)
7892 .expect("a codebook that fits is not a failure")
7893 .expect("512 MiB holds the codebook beside a full-slice chunk");
7894 assert_eq!(memo.storage_kind(), "codebook");
7895 }
7896
7897 #[cfg(target_arch = "x86_64")]
7898 #[test]
7899 fn avx2_jit_memo_accounts_two_active_batch_arenas() {
7900 let method = CpuMethodContract {
7901 stride: 32,
7902 alignment: 64,
7903 ideal_input_multiple: 1,
7904 staging_multiple: 1,
7905 ideal_chunk_size: XORJIT_AVX2_IDEAL_CHUNK_BYTES,
7906 checksum_width: 2,
7907 prefetch: CpuPrefetch {
7908 inputs_per_invoke: 1,
7909 input_distance_shift: 1,
7910 output: true,
7911 },
7912 strict_wx_available: true,
7913 };
7914 let area = reedsolomon_rs::xor_jit::packed::PackedJitBatch::active_arena_upper_bound(
7915 reedsolomon_rs::xor_jit::JitWidth::Avx2,
7916 1,
7917 method.input_grouping(),
7918 )
7919 .unwrap();
7920 let required = area * 2;
7921 let memo = JitMemo::new(
7922 reedsolomon_rs::xor_jit::JitWidth::Avx2,
7923 method,
7924 1,
7925 &[1],
7926 0,
7927 required,
7928 )
7929 .unwrap();
7930 assert_eq!(memo.reserved_bytes(), required);
7931 assert!(
7932 JitMemo::new(
7933 reedsolomon_rs::xor_jit::JitWidth::Avx2,
7934 method,
7935 1,
7936 &[1],
7937 0,
7938 required - 1,
7939 )
7940 .is_err()
7941 );
7942 }
7943
7944 #[cfg(target_arch = "x86_64")]
7945 #[test]
7946 fn avx2_jit_memo_uses_codebook_only_with_full_chunk_headroom() {
7947 let method = CpuKernelKind::XorJit(reedsolomon_rs::xor_jit::JitWidth::Avx2).method();
7948 let factors = [1u16, 2, 3, 2, 1];
7949 let measured =
7950 reedsolomon_rs::xor_jit::packed::Avx2Codebook::build(&factors, usize::MAX).unwrap();
7951 let codebook_limit = measured.build_peak_bytes();
7952 let retained_bytes = measured.retained_bytes();
7953 drop(measured);
7954
7955 let memo = JitMemo::new(
7956 reedsolomon_rs::xor_jit::JitWidth::Avx2,
7957 method,
7958 1,
7959 &factors,
7960 codebook_limit,
7961 usize::MAX,
7962 )
7963 .unwrap();
7964 assert!(matches!(
7965 &memo.storage,
7966 JitDispatchStorage::RepairCodebook(_)
7967 ));
7968 assert_eq!(memo.reserved_bytes(), retained_bytes);
7969 }
7970
7971 #[test]
7972 fn controller_parameters_use_a_full_slice_when_budget_allows() {
7973 let plan = synthetic_plan(8, 64 * 1024);
7974 let kernel = CpuKernelKind::Plain;
7975 let method = kernel.method();
7976 let (chunk_words, budget, _) = controller_execution_parameters(
7977 &plan,
7978 &RepairOptions {
7979 memory_limit: Some(16 * 1024 * 1024),
7980 ..RepairOptions::default()
7981 },
7982 method,
7983 method.staging_width(),
7984 0,
7985 4,
7986 )
7987 .unwrap();
7988 assert_eq!(budget, 16 * 1024 * 1024);
7989 assert_eq!(chunk_words, plan.slice_size as usize / 2);
7990 }
7991
7992 fn seam_sources(
7998 plan: &RepairPlan,
7999 par2_set: &Par2FileSet,
8000 padded_original: &[u8],
8001 slice_size: usize,
8002 ) -> Vec<Vec<u8>> {
8003 let mut sources = Vec::new();
8004 for &global_idx in &plan.available_input_global_indices {
8005 let (_file_id, local) = plan.global_to_file[global_idx];
8006 let start = local as usize * slice_size;
8007 sources.push(padded_original[start..start + slice_size].to_vec());
8008 }
8009 for &exp in &plan.recovery_exponents {
8010 let mut data = par2_set.recovery_slices[&exp].data.to_vec().unwrap();
8011 data.resize(slice_size, 0);
8012 sources.push(data);
8013 }
8014 sources
8015 }
8016
8017 fn serial_reconstruct(
8020 input_factors: &matrix::Matrix,
8021 sources: &[Vec<u8>],
8022 word_count: usize,
8023 ) -> Vec<Vec<u8>> {
8024 (0..input_factors.rows)
8025 .map(|j| {
8026 let mut out = vec![0u8; word_count * 2];
8027 for (s, src) in sources.iter().enumerate() {
8028 let factor = input_factors.get(j, s);
8029 if factor == 0 {
8030 continue;
8031 }
8032 for w in 0..word_count {
8033 let sv = u16::from_le_bytes([src[w * 2], src[w * 2 + 1]]);
8034 let cur = u16::from_le_bytes([out[w * 2], out[w * 2 + 1]]);
8035 let nv = gf::add(cur, gf::mul(factor, sv));
8036 let b = nv.to_le_bytes();
8037 out[w * 2] = b[0];
8038 out[w * 2 + 1] = b[1];
8039 }
8040 }
8041 out
8042 })
8043 .collect()
8044 }
8045
8046 #[test]
8051 fn seam_native_solver_matches_serial_reference_and_original() {
8052 let slice_size = 128u64;
8053 let ss = slice_size as usize;
8054 let file_data: Vec<u8> = (0..512u32).map(|i| ((i * 7 + 3) % 256) as u8).collect();
8055 let (par2_set, file_id) = setup_repairable_set(&file_data, slice_size, 4);
8056
8057 let mut damaged = file_data.clone();
8059 damaged[128..256].fill(0);
8060 damaged[384..512].fill(0);
8061 let mut access = MemoryFileAccess::new();
8062 access.add_file(file_id, damaged);
8063
8064 let result = verify::verify_all(&par2_set, &access);
8065 let plan = plan_repair(&par2_set, &result).unwrap();
8066 let n = plan.missing_slices.len();
8067 assert_eq!(n, 2);
8068
8069 let word_count = ss / 2;
8070 let num_slices = file_data.len() / ss;
8071 let mut padded = file_data.clone();
8072 padded.resize(num_slices * ss, 0);
8073 let sources = seam_sources(&plan, &par2_set, &padded, ss);
8074 let expected = serial_reconstruct(&plan.input_factors, &sources, word_count);
8075
8076 let source_refs: Vec<&[u8]> = sources.iter().map(|s| s.as_slice()).collect();
8077 let mut outputs: Vec<Vec<u8>> = vec![vec![0u8; ss]; n];
8078 {
8079 let mut out_refs: Vec<&mut [u8]> =
8080 outputs.iter_mut().map(|o| o.as_mut_slice()).collect();
8081 let mut problem = RepairProblem {
8082 total_inputs: plan.total_input_slices,
8083 word_count,
8084 missing_indices: &plan.missing_global_indices,
8085 available_indices: &plan.available_input_global_indices,
8086 recovery_exponents: &plan.recovery_exponents,
8087 constants: &plan.constants,
8088 sources: &source_refs,
8089 outputs: &mut out_refs,
8090 };
8091 NativeRepairSolver::new(&plan.input_factors, word_count)
8092 .reconstruct(&mut problem)
8093 .unwrap();
8094 }
8095
8096 assert_eq!(
8097 outputs, expected,
8098 "seam reconstruct must match the serial GF reference byte-for-byte"
8099 );
8100 for (j, &(_, local)) in plan.missing_slices.iter().enumerate() {
8101 let start = local as usize * ss;
8102 assert_eq!(
8103 outputs[j],
8104 &padded[start..start + ss],
8105 "missing slice {local} not recovered"
8106 );
8107 }
8108 }
8109
8110 struct HostStyleSolver;
8115
8116 impl RepairSolver for HostStyleSolver {
8117 fn reconstruct(
8118 &self,
8119 problem: &mut RepairProblem<'_>,
8120 ) -> std::result::Result<(), SolverError> {
8121 let coeffs = reedsolomon_rs::matrix::build_repair_matrix(
8122 problem.available_indices,
8123 problem.missing_indices,
8124 problem.recovery_exponents,
8125 problem.constants,
8126 )
8127 .map_err(|e| SolverError::Singular { bad_row: e.bad_row })?;
8128 let sources = problem.sources;
8129 for (j, out) in problem.outputs.iter_mut().enumerate() {
8130 let out: &mut [u8] = out;
8131 out.fill(0);
8132 for (s, src) in sources.iter().enumerate() {
8133 reedsolomon_rs::gf_simd::mul_acc_region(coeffs.get(j, s), src, out);
8134 }
8135 }
8136 Ok(())
8137 }
8138 }
8139
8140 #[test]
8143 fn execute_repair_with_solver_is_quarantined_on_native_targets() {
8144 let slice_size = 128u64;
8145 let file_data: Vec<u8> = (0..640u32).map(|i| ((i * 11 + 5) % 256) as u8).collect();
8146 let (par2_set, file_id) = setup_repairable_set(&file_data, slice_size, 3);
8147
8148 let mut damaged = file_data.clone();
8150 damaged[..128].fill(0);
8151 damaged[256..384].fill(0);
8152 let mut access = MemoryFileAccess::new();
8153 access.add_file(file_id, damaged);
8154
8155 let result = verify::verify_all(&par2_set, &access);
8156 assert_eq!(result.total_missing_blocks, 2);
8157 let plan = plan_repair(&par2_set, &result).unwrap();
8158
8159 let error = execute_repair_with_solver(
8160 &plan,
8161 &par2_set,
8162 &mut access,
8163 &RepairOptions::default(),
8164 &HostStyleSolver,
8165 )
8166 .unwrap_err();
8167
8168 assert!(matches!(error, Par2Error::ReedSolomonError { .. }));
8169 assert_ne!(access.read_file(&file_id).unwrap(), file_data);
8170 }
8171
8172 #[test]
8176 fn reedsolomon_rs_repair_matrix_matches_par2_rs() {
8177 let total = 20usize;
8178 let constants = gf::input_slice_constants(total);
8179 let missing = vec![3usize, 7, 11, 15];
8180 let exps: Vec<u32> = vec![0, 1, 2, 3];
8181 let avail: Vec<usize> = (0..total).filter(|i| !missing.contains(i)).collect();
8182
8183 let (weaver_repair, _decode) =
8184 matrix::build_repair_matrix_with_bad_row(&avail, &missing, &exps, &constants).unwrap();
8185 let host = reedsolomon_rs::matrix::build_repair_matrix(&avail, &missing, &exps, &constants)
8186 .unwrap();
8187
8188 assert_eq!(weaver_repair.rows, host.rows);
8189 assert_eq!(weaver_repair.cols, host.cols);
8190 assert_eq!(
8191 weaver_repair.data, host.data,
8192 "host repair matrix must be byte-identical to par2-rs's"
8193 );
8194 }
8195}