Skip to main content

dino_seq/
pack.rs

1//! Base and quality packing utilities.
2//!
3//! Bases are packed four per byte using A=0, C=1, G=2, and T=3. Ambiguous or
4//! non-canonical bases are represented by zero bits in the packed byte stream
5//! and a set bit in the separate ambiguity mask. Quality helpers interpret
6//! input as Phred+33 FASTQ qualities.
7
8#[cfg(all(feature = "simd", target_arch = "x86_64"))]
9use std::arch::x86_64::{
10    __m128i, __m256i, _mm_cvtsi128_si32, _mm_max_epu8, _mm_min_epu8, _mm_srli_si128,
11    _mm256_add_epi64, _mm256_castsi256_si128, _mm256_cmpgt_epi8, _mm256_extracti128_si256,
12    _mm256_loadu_si256, _mm256_max_epu8, _mm256_min_epu8, _mm256_movemask_epi8, _mm256_or_si256,
13    _mm256_sad_epu8, _mm256_set1_epi8, _mm256_setzero_si256, _mm256_storeu_si256, _mm256_sub_epi8,
14};
15use std::fmt;
16use std::io::Read;
17
18use crate::fastq_frame::{self, Line, RecordLines, RecordValidation};
19use crate::{FastqConfig, FastqError, Result as FastqResult};
20
21/// Summary of a packed DNA sequence.
22#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
23pub struct BaseSummary {
24    /// Number of bases observed.
25    pub len: usize,
26    /// Number of canonical A bases.
27    pub a: usize,
28    /// Number of canonical C bases.
29    pub c: usize,
30    /// Number of canonical G bases.
31    pub g: usize,
32    /// Number of canonical T bases.
33    pub t: usize,
34    /// Number of ambiguous or non-canonical bases.
35    pub n: usize,
36}
37
38impl BaseSummary {
39    /// Return the number of C or G bases.
40    pub fn gc_bases(self) -> usize {
41        self.c + self.g
42    }
43
44    /// Return the number of A/C/G/T bases.
45    pub fn canonical_bases(self) -> usize {
46        self.a + self.c + self.g + self.t
47    }
48
49    /// Return true when no bases were observed.
50    pub fn is_empty(self) -> bool {
51        self.len == 0
52    }
53}
54
55/// Packed sequence bytes plus one bit per ambiguous/non-ACGT base.
56///
57/// Bases are packed four per byte with base 0 in the least significant two
58/// bits. Canonical bases use A=0, C=1, G=2, T=3. Masked bases store 0 in the
59/// packed stream and set the corresponding bit in `n_mask`.
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct PackedSequence {
62    /// Two-bit packed base bytes, four bases per byte.
63    pub bases: Vec<u8>,
64    /// Ambiguity bit mask, one bit per input base.
65    pub n_mask: Vec<u8>,
66    /// Base counts for the original sequence.
67    pub summary: BaseSummary,
68}
69
70impl PackedSequence {
71    /// Return the number of original bases.
72    pub fn len(&self) -> usize {
73        self.summary.len
74    }
75
76    /// Return true when the original sequence was empty.
77    pub fn is_empty(&self) -> bool {
78        self.summary.is_empty()
79    }
80}
81
82/// Decoded base value from a packed sequence.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum PackedBase {
85    /// Canonical A.
86    A,
87    /// Canonical C.
88    C,
89    /// Canonical G.
90    G,
91    /// Canonical T.
92    T,
93    /// Ambiguous or non-canonical base.
94    N,
95}
96
97/// Output buffer involved in a packing error.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum PackBuffer {
100    /// Two-bit base buffer.
101    Bases,
102    /// Ambiguity mask buffer.
103    NMask,
104    /// Quality-bin output buffer.
105    QualityBins,
106}
107
108/// Error returned by base or quality packing helpers.
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum PackError {
111    /// A caller-supplied output buffer was too small.
112    OutputTooSmall {
113        /// Buffer that was too small.
114        buffer: PackBuffer,
115        /// Required number of bytes.
116        needed: usize,
117        /// Provided number of bytes.
118        provided: usize,
119    },
120    /// A quality byte was outside the printable Phred+33 range.
121    InvalidQuality {
122        /// Offset within the supplied quality slice.
123        offset: usize,
124        /// Invalid byte value.
125        byte: u8,
126    },
127    /// Quality thresholds were not sorted in ascending order.
128    UnsortedQualityThresholds {
129        /// Threshold index that violates sorted order.
130        index: usize,
131    },
132    /// More quality thresholds were provided than fit in a `u8` bin.
133    TooManyQualityThresholds {
134        /// Number of thresholds supplied by the caller.
135        count: usize,
136    },
137}
138
139/// Phred+33 quality summary.
140#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
141pub struct QualitySummary {
142    /// Number of quality values observed.
143    pub len: usize,
144    /// Minimum Phred value, or `None` when empty.
145    pub min_phred: Option<u8>,
146    /// Maximum Phred value, or `None` when empty.
147    pub max_phred: Option<u8>,
148    /// Sum of Phred values.
149    pub sum_phred: u64,
150    /// Number of bases with Phred score at least 20.
151    pub q20_bases: usize,
152    /// Number of bases with Phred score at least 30.
153    pub q30_bases: usize,
154}
155
156impl QualitySummary {
157    /// Return the arithmetic mean Phred score, or `None` when empty.
158    pub fn mean_phred(self) -> Option<f64> {
159        if self.len == 0 {
160            None
161        } else {
162            Some(self.sum_phred as f64 / self.len as f64)
163        }
164    }
165
166    /// Return true when no qualities were observed.
167    pub fn is_empty(self) -> bool {
168        self.len == 0
169    }
170
171    fn observe(&mut self, phred: u8) {
172        self.len += 1;
173        self.min_phred = Some(self.min_phred.map_or(phred, |min| min.min(phred)));
174        self.max_phred = Some(self.max_phred.map_or(phred, |max| max.max(phred)));
175        self.sum_phred += u64::from(phred);
176        self.q20_bases += usize::from(phred >= 20);
177        self.q30_bases += usize::from(phred >= 30);
178    }
179}
180
181/// Combined base and quality summary for one FASTQ record.
182#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
183pub struct PackedRecordSummary {
184    /// Packed base summary.
185    pub bases: BaseSummary,
186    /// Phred+33 quality summary.
187    pub qualities: QualitySummary,
188}
189
190/// Borrowed view of one trusted packed FASTQ record.
191///
192/// The packed buffers are reused by streaming pack paths. Callers must consume
193/// or copy them before the next callback invocation.
194#[derive(Debug, Clone, Copy)]
195pub struct TrustedPackedRecord<'a> {
196    /// Header line, including the leading `@` and excluding the newline.
197    pub name: &'a [u8],
198    /// Original sequence bytes.
199    pub seq: &'a [u8],
200    /// Original quality bytes.
201    pub qual: &'a [u8],
202    /// Two-bit packed base bytes.
203    pub bases: &'a [u8],
204    /// Ambiguity bit mask.
205    pub n_mask: &'a [u8],
206    /// Base and quality summary for the record.
207    pub summary: PackedRecordSummary,
208}
209
210/// Borrowed view of one trusted packed R1/R2 pair.
211#[derive(Debug, Clone, Copy)]
212pub struct TrustedPackedPair<'a> {
213    /// First mate.
214    pub first: TrustedPackedRecord<'a>,
215    /// Second mate.
216    pub second: TrustedPackedRecord<'a>,
217}
218
219/// Selected implementation family for base packing.
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub enum PackKernel {
222    /// Portable scalar implementation.
223    Scalar,
224    /// Legacy portable-SIMD implementation marker.
225    ///
226    /// Current `simd` builds use stable `std::arch` paths where available and
227    /// otherwise fall back to [`Scalar`](Self::Scalar).
228    PortableSimd,
229    /// x86-64 AVX2 implementation.
230    Avx2,
231}
232
233/// Return the base-packing kernel selected for this build and host.
234pub fn selected_pack_kernel() -> PackKernel {
235    select_pack_kernel()
236}
237
238#[cfg(all(feature = "simd", target_arch = "x86_64"))]
239fn select_pack_kernel() -> PackKernel {
240    if std::is_x86_feature_detected!("avx2") {
241        return PackKernel::Avx2;
242    }
243    PackKernel::Scalar
244}
245
246#[cfg(not(all(feature = "simd", target_arch = "x86_64")))]
247fn select_pack_kernel() -> PackKernel {
248    PackKernel::Scalar
249}
250
251/// Progress notification for one trusted pack slab.
252#[derive(Debug, Clone, Copy)]
253pub struct TrustedPackSlab {
254    /// Number of complete records emitted from the slab.
255    pub records: u64,
256}
257
258/// Callback interface for trusted streaming pack paths.
259pub trait TrustedPackSink {
260    /// Observe one packed record.
261    fn record(&mut self, record: TrustedPackedRecord<'_>) -> FastqResult<()>;
262
263    /// Observe slab-level progress after records have been emitted.
264    fn slab(&mut self, _slab: TrustedPackSlab) -> FastqResult<()> {
265        Ok(())
266    }
267}
268
269impl<F> TrustedPackSink for F
270where
271    F: FnMut(TrustedPackedRecord<'_>) -> FastqResult<()>,
272{
273    fn record(&mut self, record: TrustedPackedRecord<'_>) -> FastqResult<()> {
274        self(record)
275    }
276}
277
278impl fmt::Display for PackError {
279    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280        match self {
281            Self::OutputTooSmall {
282                buffer,
283                needed,
284                provided,
285            } => write!(
286                f,
287                "{buffer:?} output too small: need {needed} bytes, got {provided}"
288            ),
289            Self::InvalidQuality { offset, byte } => {
290                write!(f, "invalid Phred+33 quality byte {byte} at offset {offset}")
291            }
292            Self::UnsortedQualityThresholds { index } => {
293                write!(f, "quality threshold at index {index} is not sorted")
294            }
295            Self::TooManyQualityThresholds { count } => {
296                write!(f, "too many quality thresholds: {count}")
297            }
298        }
299    }
300}
301
302impl std::error::Error for PackError {}
303
304/// Pack all complete records from an in-memory trusted four-line FASTQ buffer.
305///
306/// This path validates record shape and quality length, but it assumes ordinary
307/// four-line FASTQ and does not support multiline sequence or quality fields.
308pub fn pack_trusted_fastq(
309    input: &[u8],
310    on_record: impl FnMut(TrustedPackedRecord<'_>) -> FastqResult<()>,
311) -> FastqResult<()> {
312    pack_trusted_fastq_sink(input, on_record)
313}
314
315/// Pack all complete records from an in-memory trusted FASTQ buffer into a sink.
316pub fn pack_trusted_fastq_sink(input: &[u8], sink: impl TrustedPackSink) -> FastqResult<()> {
317    pack_trusted_fastq_direct_sink(input, sink)
318}
319
320/// Stream trusted four-line FASTQ from a reader and invoke a callback per record.
321pub fn pack_trusted_fastq_read<R: Read>(
322    mut reader: R,
323    config: FastqConfig,
324    on_record: impl FnMut(TrustedPackedRecord<'_>) -> FastqResult<()>,
325) -> FastqResult<()> {
326    pack_trusted_fastq_read_sink(&mut reader, config, on_record)
327}
328
329/// Stream trusted four-line FASTQ from a reader into a sink.
330pub fn pack_trusted_fastq_read_sink<R: Read>(
331    mut reader: R,
332    config: FastqConfig,
333    mut sink: impl TrustedPackSink,
334) -> FastqResult<()> {
335    pack_trusted_fastq_read_direct_sink_impl(&mut reader, config, &mut sink)
336}
337
338/// Pack an in-memory trusted FASTQ buffer with the direct scanner.
339pub fn pack_trusted_fastq_direct(
340    input: &[u8],
341    on_record: impl FnMut(TrustedPackedRecord<'_>) -> FastqResult<()>,
342) -> FastqResult<()> {
343    pack_trusted_fastq_direct_sink(input, on_record)
344}
345
346/// Pack an in-memory trusted FASTQ buffer with the direct scanner into a sink.
347pub fn pack_trusted_fastq_direct_sink(
348    input: &[u8],
349    mut sink: impl TrustedPackSink,
350) -> FastqResult<()> {
351    let mut bases = Vec::new();
352    let mut n_mask = Vec::new();
353    let slab = pack_trusted_fastq_direct_slab(
354        input,
355        SlabContext {
356            base_offset: 0,
357            first_record_index: 0,
358            eof: true,
359        },
360        &mut bases,
361        &mut n_mask,
362        &mut sink,
363    )?;
364    debug_assert_eq!(slab.next_start, input.len());
365    Ok(())
366}
367
368/// Stream trusted FASTQ from a reader using the direct scanner.
369pub fn pack_trusted_fastq_read_direct<R: Read>(
370    mut reader: R,
371    config: FastqConfig,
372    on_record: impl FnMut(TrustedPackedRecord<'_>) -> FastqResult<()>,
373) -> FastqResult<()> {
374    pack_trusted_fastq_read_direct_sink(&mut reader, config, on_record)
375}
376
377/// Stream trusted FASTQ from a reader using the direct scanner into a sink.
378pub fn pack_trusted_fastq_read_direct_sink<R: Read>(
379    reader: R,
380    config: FastqConfig,
381    mut sink: impl TrustedPackSink,
382) -> FastqResult<()> {
383    pack_trusted_fastq_read_direct_sink_impl(reader, config, &mut sink)
384}
385
386fn pack_trusted_fastq_read_direct_sink_impl<R: Read>(
387    reader: R,
388    config: FastqConfig,
389    sink: &mut impl TrustedPackSink,
390) -> FastqResult<()> {
391    let mut reader = TrustedFastqPackReader::new(reader, config);
392    while reader.next_slab(sink)? {}
393    Ok(())
394}
395
396struct TrustedFastqPackReader<R> {
397    reader: R,
398    slab_size: usize,
399    buf: Vec<u8>,
400    start: usize,
401    len: usize,
402    eof: bool,
403    base_offset: u64,
404    record_index: u64,
405    bases: Vec<u8>,
406    n_mask: Vec<u8>,
407}
408
409impl<R: Read> TrustedFastqPackReader<R> {
410    fn new(reader: R, config: FastqConfig) -> Self {
411        let slab_size = config.slab_size.max(1024);
412        Self {
413            reader,
414            slab_size,
415            buf: vec![0_u8; slab_size],
416            start: 0,
417            len: 0,
418            eof: false,
419            base_offset: 0,
420            record_index: 0,
421            bases: Vec::new(),
422            n_mask: Vec::new(),
423        }
424    }
425
426    fn next_slab(&mut self, sink: &mut impl TrustedPackSink) -> FastqResult<bool> {
427        if self.eof && self.start == self.len {
428            return Ok(false);
429        }
430
431        self.compact_incomplete_tail()?;
432        while !self.eof && self.len < self.slab_size {
433            let n = self.reader.read(&mut self.buf[self.len..self.slab_size])?;
434            if n == 0 {
435                self.eof = true;
436                break;
437            }
438            self.len += n;
439        }
440
441        let context = SlabContext {
442            base_offset: self.base_offset,
443            first_record_index: self.record_index,
444            eof: self.eof,
445        };
446        let slab = pack_trusted_fastq_direct_slab(
447            &self.buf[self.start..self.len],
448            context,
449            &mut self.bases,
450            &mut self.n_mask,
451            sink,
452        )?;
453        if slab.records != 0 {
454            sink.slab(TrustedPackSlab {
455                records: slab.records,
456            })?;
457        }
458        self.record_index += slab.records;
459
460        let next_start = self.start + slab.next_start;
461        if next_start == self.len {
462            self.base_offset += self.len.saturating_sub(self.start) as u64;
463            self.start = 0;
464            self.len = 0;
465        } else {
466            let carry = self.len - next_start;
467            if next_start == self.start && carry == self.slab_size && !self.eof {
468                return Err(FastqError::RecordTooLarge {
469                    slab_size: self.slab_size,
470                });
471            }
472            self.base_offset += slab.next_start as u64;
473            self.start = next_start;
474        }
475
476        if self.eof {
477            if self.start == self.len {
478                return Ok(slab.records != 0);
479            }
480            return Err(FastqError::RecordTooLarge {
481                slab_size: self.slab_size,
482            });
483        }
484
485        Ok(true)
486    }
487
488    fn compact_incomplete_tail(&mut self) -> FastqResult<()> {
489        if self.start == 0 {
490            if self.len == self.slab_size && !self.eof {
491                return Err(FastqError::RecordTooLarge {
492                    slab_size: self.slab_size,
493                });
494            }
495            return Ok(());
496        }
497        if self.start == self.len {
498            self.start = 0;
499            self.len = 0;
500            return Ok(());
501        }
502
503        let carry = self.len - self.start;
504        self.buf.copy_within(self.start..self.len, 0);
505        self.start = 0;
506        self.len = carry;
507        if self.len == self.slab_size && !self.eof {
508            return Err(FastqError::RecordTooLarge {
509                slab_size: self.slab_size,
510            });
511        }
512        Ok(())
513    }
514}
515
516/// Stream ordered R1/R2 FASTQ inputs and emit trusted packed pairs.
517///
518/// Pair validation uses the supplied [`crate::PairValidation`] mode. This path
519/// does not synchronize reordered mates; it expects the two streams to be in
520/// lockstep order.
521pub fn pack_trusted_paired_fastq_read<R1: Read, R2: Read>(
522    first: R1,
523    second: R2,
524    config: FastqConfig,
525    pair_validation: crate::PairValidation,
526    mut on_pair: impl FnMut(TrustedPackedPair<'_>) -> FastqResult<()>,
527) -> FastqResult<()> {
528    let mut first_reader = TrustedFastqLineReader::new(first, config.clone());
529    let mut second_reader = TrustedFastqLineReader::new(second, config);
530    let mut first_bases = Vec::new();
531    let mut first_n_mask = Vec::new();
532    let mut second_bases = Vec::new();
533    let mut second_n_mask = Vec::new();
534    let mut pair_index = 0_u64;
535
536    loop {
537        let first_count = first_reader.available_records()?;
538        let second_count = second_reader.available_records()?;
539
540        if first_count == 0 || second_count == 0 {
541            if first_count == 0
542                && second_count == 0
543                && first_reader.is_done()
544                && second_reader.is_done()
545            {
546                return Ok(());
547            }
548            if (first_count == 0 && first_reader.is_done())
549                || (second_count == 0 && second_reader.is_done())
550            {
551                return Err(FastqError::Format(
552                    "paired FASTQ inputs have different record counts".into(),
553                ));
554            }
555            continue;
556        }
557
558        let pairs = first_count.min(second_count);
559        for index in 0..pairs {
560            let first = first_reader.record_lines(index);
561            let second = second_reader.record_lines(index);
562            let first_record_index = first_reader.record_index + index as u64;
563            let second_record_index = second_reader.record_index + index as u64;
564
565            fastq_frame::validate_record(
566                first,
567                first_reader.base_offset,
568                first_record_index,
569                RecordValidation::TRUSTED_PACK,
570            )?;
571            fastq_frame::validate_record(
572                second,
573                second_reader.base_offset,
574                second_record_index,
575                RecordValidation::TRUSTED_PACK,
576            )?;
577
578            if pair_validation != crate::PairValidation::None
579                && !trusted_pair_ids_match(first.name.bytes, second.name.bytes, pair_validation)
580            {
581                return Err(fastq_frame::format_at(
582                    "paired FASTQ record identifiers do not match",
583                    0,
584                    0,
585                    pair_index,
586                    0,
587                ));
588            }
589
590            let first_summary = pack_bases_and_summarize_qualities_into(
591                first.seq.bytes,
592                first.qual.bytes,
593                &mut first_bases,
594                &mut first_n_mask,
595            )
596            .map_err(|err| {
597                fastq_frame::format_at(
598                    err.to_string(),
599                    first_reader.base_offset,
600                    first.qual.start,
601                    first_record_index,
602                    3,
603                )
604            })?;
605            let second_summary = pack_bases_and_summarize_qualities_into(
606                second.seq.bytes,
607                second.qual.bytes,
608                &mut second_bases,
609                &mut second_n_mask,
610            )
611            .map_err(|err| {
612                fastq_frame::format_at(
613                    err.to_string(),
614                    second_reader.base_offset,
615                    second.qual.start,
616                    second_record_index,
617                    3,
618                )
619            })?;
620
621            on_pair(TrustedPackedPair {
622                first: TrustedPackedRecord {
623                    name: first.name.bytes,
624                    seq: first.seq.bytes,
625                    qual: first.qual.bytes,
626                    bases: &first_bases,
627                    n_mask: &first_n_mask,
628                    summary: first_summary,
629                },
630                second: TrustedPackedRecord {
631                    name: second.name.bytes,
632                    seq: second.seq.bytes,
633                    qual: second.qual.bytes,
634                    bases: &second_bases,
635                    n_mask: &second_n_mask,
636                    summary: second_summary,
637                },
638            })?;
639            pair_index += 1;
640        }
641
642        first_reader.consume_records(pairs)?;
643        second_reader.consume_records(pairs)?;
644    }
645}
646
647struct TrustedFastqLineReader<R> {
648    reader: R,
649    slab_size: usize,
650    buf: Vec<u8>,
651    len: usize,
652    eof: bool,
653    base_offset: u64,
654    record_index: u64,
655    scan_cursor: usize,
656    records: Vec<RecordSpan>,
657    consumed_records: usize,
658}
659
660impl<R: Read> TrustedFastqLineReader<R> {
661    fn new(reader: R, config: FastqConfig) -> Self {
662        let slab_size = config.slab_size.max(1024);
663        Self {
664            reader,
665            slab_size,
666            buf: vec![0_u8; slab_size],
667            len: 0,
668            eof: false,
669            base_offset: 0,
670            record_index: 0,
671            scan_cursor: 0,
672            records: Vec::with_capacity(slab_size / 96),
673            consumed_records: 0,
674        }
675    }
676
677    fn available_records(&mut self) -> FastqResult<usize> {
678        if self.consumed_records < self.records.len() {
679            return Ok(self.records.len() - self.consumed_records);
680        }
681        if self.eof && self.scan_cursor == self.len {
682            self.reset_consumed();
683            return Ok(0);
684        }
685
686        self.compact_consumed_or_tail()?;
687        while !self.eof && self.len < self.slab_size {
688            let n = self.reader.read(&mut self.buf[self.len..self.slab_size])?;
689            if n == 0 {
690                self.eof = true;
691                break;
692            }
693            self.len += n;
694        }
695
696        self.scan_records()?;
697        let available_records = self.records.len() - self.consumed_records;
698        if available_records == 0 && self.len == self.slab_size && !self.eof {
699            return Err(FastqError::RecordTooLarge {
700                slab_size: self.slab_size,
701            });
702        }
703        Ok(available_records)
704    }
705
706    fn is_done(&self) -> bool {
707        self.eof && self.consumed_records == self.records.len() && self.scan_cursor == self.len
708    }
709
710    fn record_lines(&self, index: usize) -> RecordLines<'_> {
711        self.records[self.consumed_records + index].to_lines(&self.buf[..self.len])
712    }
713
714    fn consume_records(&mut self, records: usize) -> FastqResult<()> {
715        if records == 0 {
716            return Ok(());
717        }
718        self.consumed_records += records;
719        if self.consumed_records > self.records.len() {
720            return Err(FastqError::Format(
721                "internal trusted FASTQ record cursor advanced past available records".into(),
722            ));
723        }
724        self.record_index += records as u64;
725        Ok(())
726    }
727
728    fn scan_records(&mut self) -> FastqResult<()> {
729        let mut cursor = self.scan_cursor;
730        let scan_base = cursor;
731        let mut newlines = memchr::memchr_iter(b'\n', &self.buf[scan_base..self.len])
732            .map(|offset| scan_base + offset);
733        while cursor < self.len {
734            let record_start = cursor;
735            let Some(name) =
736                trusted_direct_line(&self.buf[..self.len], &mut cursor, &mut newlines, self.eof)
737            else {
738                self.scan_cursor = record_start;
739                return Ok(());
740            };
741            let Some(seq) =
742                trusted_direct_line(&self.buf[..self.len], &mut cursor, &mut newlines, self.eof)
743            else {
744                return self.incomplete_or_truncated(record_start, 1);
745            };
746            let Some(plus) =
747                trusted_direct_line(&self.buf[..self.len], &mut cursor, &mut newlines, self.eof)
748            else {
749                return self.incomplete_or_truncated(record_start, 2);
750            };
751            let Some(qual) =
752                trusted_direct_line(&self.buf[..self.len], &mut cursor, &mut newlines, self.eof)
753            else {
754                return self.incomplete_or_truncated(record_start, 3);
755            };
756
757            self.records.push(RecordSpan {
758                name: LineSpan::from_line(name),
759                seq: LineSpan::from_line(seq),
760                plus: LineSpan::from_line(plus),
761                qual: LineSpan::from_line(qual),
762            });
763            self.scan_cursor = cursor;
764        }
765        Ok(())
766    }
767
768    fn incomplete_or_truncated(&mut self, record_start: usize, line_index: u8) -> FastqResult<()> {
769        if self.eof {
770            let record_index =
771                self.record_index + (self.records.len() - self.consumed_records) as u64;
772            Err(fastq_frame::format_at(
773                "truncated FASTQ record",
774                self.base_offset,
775                record_start,
776                record_index,
777                line_index,
778            ))
779        } else {
780            self.scan_cursor = record_start;
781            Ok(())
782        }
783    }
784
785    fn compact_consumed_or_tail(&mut self) -> FastqResult<()> {
786        if self.consumed_records < self.records.len() {
787            return Ok(());
788        }
789
790        let retain_start = self.scan_cursor;
791        if retain_start == self.len {
792            self.base_offset += self.len as u64;
793            self.len = 0;
794            self.scan_cursor = 0;
795            self.reset_consumed();
796            return Ok(());
797        }
798        if retain_start > 0 {
799            let carry = self.len - retain_start;
800            self.buf.copy_within(retain_start..self.len, 0);
801            self.base_offset += retain_start as u64;
802            self.len = carry;
803            self.scan_cursor = 0;
804            self.reset_consumed();
805        }
806        if self.len == self.slab_size && !self.eof {
807            return Err(FastqError::RecordTooLarge {
808                slab_size: self.slab_size,
809            });
810        }
811        Ok(())
812    }
813
814    fn reset_consumed(&mut self) {
815        self.records.clear();
816        self.consumed_records = 0;
817    }
818}
819
820#[derive(Clone, Copy)]
821struct LineSpan {
822    start: usize,
823    end: usize,
824}
825
826impl LineSpan {
827    fn from_line(line: Line<'_>) -> Self {
828        Self {
829            start: line.start,
830            end: line.start + line.bytes.len(),
831        }
832    }
833
834    fn to_line<'a>(self, bytes: &'a [u8]) -> Line<'a> {
835        Line {
836            bytes: &bytes[self.start..self.end],
837            start: self.start,
838        }
839    }
840}
841
842#[derive(Clone, Copy)]
843struct RecordSpan {
844    name: LineSpan,
845    seq: LineSpan,
846    plus: LineSpan,
847    qual: LineSpan,
848}
849
850impl RecordSpan {
851    fn to_lines<'a>(self, bytes: &'a [u8]) -> RecordLines<'a> {
852        RecordLines {
853            name: self.name.to_line(bytes),
854            seq: self.seq.to_line(bytes),
855            plus: self.plus.to_line(bytes),
856            qual: self.qual.to_line(bytes),
857        }
858    }
859}
860
861/// Return the number of bytes needed to store `base_count` two-bit bases.
862pub const fn packed_base_len(base_count: usize) -> usize {
863    base_count / 4 + if base_count.is_multiple_of(4) { 0 } else { 1 }
864}
865
866/// Return the number of bytes needed for a one-bit-per-item mask.
867pub const fn bit_mask_len(bit_count: usize) -> usize {
868    bit_count / 8 + if bit_count.is_multiple_of(8) { 0 } else { 1 }
869}
870
871const BASE_N: u8 = 4;
872const BASE_LUT: [u8; 256] = base_lut();
873const BASE_QUAD_STATES: usize = 5 * 5 * 5 * 5;
874const BASE_QUAD_LUT: [u32; BASE_QUAD_STATES] = base_quad_lut();
875
876const fn base_lut() -> [u8; 256] {
877    let mut table = [BASE_N; 256];
878    table[b'A' as usize] = 0;
879    table[b'a' as usize] = 0;
880    table[b'C' as usize] = 1;
881    table[b'c' as usize] = 1;
882    table[b'G' as usize] = 2;
883    table[b'g' as usize] = 2;
884    table[b'T' as usize] = 3;
885    table[b't' as usize] = 3;
886    table
887}
888
889const fn base_quad_lut() -> [u32; BASE_QUAD_STATES] {
890    let mut table = [0_u32; BASE_QUAD_STATES];
891    let mut c0 = 0_u8;
892    while c0 <= BASE_N {
893        let mut c1 = 0_u8;
894        while c1 <= BASE_N {
895            let mut c2 = 0_u8;
896            while c2 <= BASE_N {
897                let mut c3 = 0_u8;
898                while c3 <= BASE_N {
899                    let key = quad_key(c0, c1, c2, c3);
900                    table[key] = quad_entry(c0, c1, c2, c3);
901                    c3 += 1;
902                }
903                c2 += 1;
904            }
905            c1 += 1;
906        }
907        c0 += 1;
908    }
909    table
910}
911
912const fn quad_key(c0: u8, c1: u8, c2: u8, c3: u8) -> usize {
913    c0 as usize + (c1 as usize * 5) + (c2 as usize * 25) + (c3 as usize * 125)
914}
915
916const fn quad_entry(c0: u8, c1: u8, c2: u8, c3: u8) -> u32 {
917    let codes = [c0, c1, c2, c3];
918    let mut packed = 0_u32;
919    let mut mask = 0_u32;
920    let mut counts = [0_u32; 5];
921    let mut i = 0;
922    while i < 4 {
923        let code = codes[i];
924        if code < BASE_N {
925            packed |= (code as u32) << (i * 2);
926            counts[code as usize] += 1;
927        } else {
928            mask |= 1 << i;
929            counts[BASE_N as usize] += 1;
930        }
931        i += 1;
932    }
933
934    packed
935        | (mask << 8)
936        | (counts[0] << 12)
937        | (counts[1] << 15)
938        | (counts[2] << 18)
939        | (counts[3] << 21)
940        | (counts[4] << 24)
941}
942
943/// Pack a sequence into newly allocated packed-base and ambiguity buffers.
944pub fn pack_bases(seq: &[u8]) -> PackedSequence {
945    let mut bases = vec![0; packed_base_len(seq.len())];
946    let mut n_mask = vec![0; bit_mask_len(seq.len())];
947    let summary = pack_bases_exact_zeroed(seq, &mut bases, &mut n_mask);
948    PackedSequence {
949        bases,
950        n_mask,
951        summary,
952    }
953}
954
955/// Pack a sequence into reusable `Vec` buffers and return base counts.
956pub fn pack_bases_into(seq: &[u8], bases: &mut Vec<u8>, n_mask: &mut Vec<u8>) -> BaseSummary {
957    bases.clear();
958    n_mask.clear();
959    bases.resize(packed_base_len(seq.len()), 0);
960    n_mask.resize(bit_mask_len(seq.len()), 0);
961    pack_bases_exact_zeroed(seq, bases, n_mask)
962}
963
964/// Pack bases and summarize Phred+33 qualities into reusable buffers.
965///
966/// When sequence and quality lengths match, the implementation may fuse base
967/// packing and quality summary work in one pass.
968#[inline]
969pub fn pack_bases_and_summarize_qualities_into(
970    seq: &[u8],
971    qualities: &[u8],
972    bases: &mut Vec<u8>,
973    n_mask: &mut Vec<u8>,
974) -> Result<PackedRecordSummary, PackError> {
975    bases.clear();
976    n_mask.clear();
977    bases.resize(packed_base_len(seq.len()), 0);
978    n_mask.resize(bit_mask_len(seq.len()), 0);
979
980    if seq.len() == qualities.len() {
981        pack_bases_and_qualities_exact_zeroed(seq, qualities, bases, n_mask)
982    } else {
983        Ok(PackedRecordSummary {
984            bases: pack_bases_exact_zeroed(seq, bases, n_mask),
985            qualities: summarize_qualities(qualities)?,
986        })
987    }
988}
989
990/// Pack a sequence into caller-provided fixed-size slices.
991///
992/// The slices may be larger than needed; only the required prefix is written.
993pub fn pack_bases_into_slices(
994    seq: &[u8],
995    bases: &mut [u8],
996    n_mask: &mut [u8],
997) -> Result<BaseSummary, PackError> {
998    let bases_needed = packed_base_len(seq.len());
999    let mask_needed = bit_mask_len(seq.len());
1000    if bases.len() < bases_needed {
1001        return Err(PackError::OutputTooSmall {
1002            buffer: PackBuffer::Bases,
1003            needed: bases_needed,
1004            provided: bases.len(),
1005        });
1006    }
1007    if n_mask.len() < mask_needed {
1008        return Err(PackError::OutputTooSmall {
1009            buffer: PackBuffer::NMask,
1010            needed: mask_needed,
1011            provided: n_mask.len(),
1012        });
1013    }
1014
1015    Ok(pack_bases_exact(
1016        seq,
1017        &mut bases[..bases_needed],
1018        &mut n_mask[..mask_needed],
1019    ))
1020}
1021
1022/// Decode one base at `index` from packed-base and ambiguity buffers.
1023pub fn packed_base_at(bases: &[u8], n_mask: &[u8], index: usize) -> Option<PackedBase> {
1024    let base_byte = *bases.get(index / 4)?;
1025    if is_masked(n_mask, index)? {
1026        return Some(PackedBase::N);
1027    }
1028
1029    match (base_byte >> ((index % 4) * 2)) & 0b11 {
1030        0 => Some(PackedBase::A),
1031        1 => Some(PackedBase::C),
1032        2 => Some(PackedBase::G),
1033        3 => Some(PackedBase::T),
1034        _ => None,
1035    }
1036}
1037
1038/// Return whether `index` is marked ambiguous in an ambiguity mask.
1039pub fn is_masked(n_mask: &[u8], index: usize) -> Option<bool> {
1040    let mask_byte = *n_mask.get(index / 8)?;
1041    Some(((mask_byte >> (index % 8)) & 1) != 0)
1042}
1043
1044/// Summarize a slice of Phred+33 quality bytes.
1045pub fn summarize_qualities(qualities: &[u8]) -> Result<QualitySummary, PackError> {
1046    #[cfg(all(feature = "simd", target_arch = "x86_64"))]
1047    if qualities.len() >= 32 && std::is_x86_feature_detected!("avx2") {
1048        return unsafe { summarize_qualities_avx2(qualities) };
1049    }
1050    let mut summary = QualityAccumulator::default();
1051    for (offset, &byte) in qualities.iter().enumerate() {
1052        summary.observe(byte, offset)?;
1053    }
1054    Ok(summary.finish())
1055}
1056
1057#[cfg(all(feature = "simd", target_arch = "x86_64"))]
1058#[target_feature(enable = "avx2")]
1059unsafe fn summarize_qualities_avx2(qualities: &[u8]) -> Result<QualitySummary, PackError> {
1060    let low = _mm256_set1_epi8(33);
1061    let high = _mm256_set1_epi8(126);
1062    let offset = _mm256_set1_epi8(33);
1063    let q20 = _mm256_set1_epi8(52);
1064    let q30 = _mm256_set1_epi8(62);
1065    let zero = _mm256_setzero_si256();
1066    let mut min_phred = _mm256_set1_epi8(93);
1067    let mut max_phred = _mm256_setzero_si256();
1068    let mut sum_phred = _mm256_setzero_si256();
1069
1070    let mut summary = QualityAccumulator::default();
1071    let mut i = 0;
1072    while i + 32 <= qualities.len() {
1073        let bytes = unsafe { _mm256_loadu_si256(qualities.as_ptr().add(i).cast::<__m256i>()) };
1074        let too_low = _mm256_cmpgt_epi8(low, bytes);
1075        let too_high = _mm256_cmpgt_epi8(bytes, high);
1076        let invalid = _mm256_movemask_epi8(_mm256_or_si256(too_low, too_high));
1077        if invalid != 0 {
1078            let offset = invalid.trailing_zeros() as usize;
1079            return Err(PackError::InvalidQuality {
1080                offset: i + offset,
1081                byte: qualities[i + offset],
1082            });
1083        }
1084
1085        summary.q20_bases +=
1086            _mm256_movemask_epi8(_mm256_cmpgt_epi8(bytes, q20)).count_ones() as usize;
1087        summary.q30_bases +=
1088            _mm256_movemask_epi8(_mm256_cmpgt_epi8(bytes, q30)).count_ones() as usize;
1089        let phreds = _mm256_sub_epi8(bytes, offset);
1090        min_phred = _mm256_min_epu8(min_phred, phreds);
1091        max_phred = _mm256_max_epu8(max_phred, phreds);
1092        sum_phred = _mm256_add_epi64(sum_phred, _mm256_sad_epu8(phreds, zero));
1093        summary.len += 32;
1094        i += 32;
1095    }
1096
1097    unsafe { finish_avx2_quality_vectors(&mut summary, min_phred, max_phred, sum_phred) };
1098
1099    while i < qualities.len() {
1100        summary.observe(qualities[i], i)?;
1101        i += 1;
1102    }
1103
1104    Ok(summary.finish())
1105}
1106
1107#[cfg(all(feature = "simd", target_arch = "x86_64"))]
1108#[target_feature(enable = "avx2")]
1109unsafe fn finish_avx2_quality_vectors(
1110    summary: &mut QualityAccumulator,
1111    min_phred: __m256i,
1112    max_phred: __m256i,
1113    sum_phred: __m256i,
1114) {
1115    if summary.len == 0 {
1116        return;
1117    }
1118
1119    let mut sum_lanes = [0_u64; 4];
1120    unsafe {
1121        _mm256_storeu_si256(sum_lanes.as_mut_ptr().cast::<__m256i>(), sum_phred);
1122    }
1123    summary.min_phred = summary
1124        .min_phred
1125        .min(unsafe { horizontal_min_u8_256(min_phred) });
1126    summary.max_phred = summary
1127        .max_phred
1128        .max(unsafe { horizontal_max_u8_256(max_phred) });
1129    summary.sum_phred = sum_lanes.iter().copied().sum();
1130}
1131
1132#[cfg(all(feature = "simd", target_arch = "x86_64"))]
1133#[target_feature(enable = "avx2")]
1134unsafe fn horizontal_min_u8_256(value: __m256i) -> u8 {
1135    let low = _mm256_castsi256_si128(value);
1136    let high = _mm256_extracti128_si256(value, 1);
1137    unsafe { horizontal_min_u8_128(_mm_min_epu8(low, high)) }
1138}
1139
1140#[cfg(all(feature = "simd", target_arch = "x86_64"))]
1141#[target_feature(enable = "avx2")]
1142unsafe fn horizontal_max_u8_256(value: __m256i) -> u8 {
1143    let low = _mm256_castsi256_si128(value);
1144    let high = _mm256_extracti128_si256(value, 1);
1145    unsafe { horizontal_max_u8_128(_mm_max_epu8(low, high)) }
1146}
1147
1148#[cfg(all(feature = "simd", target_arch = "x86_64"))]
1149#[target_feature(enable = "avx2")]
1150unsafe fn horizontal_min_u8_128(mut value: __m128i) -> u8 {
1151    value = _mm_min_epu8(value, _mm_srli_si128(value, 8));
1152    value = _mm_min_epu8(value, _mm_srli_si128(value, 4));
1153    value = _mm_min_epu8(value, _mm_srli_si128(value, 2));
1154    value = _mm_min_epu8(value, _mm_srli_si128(value, 1));
1155    _mm_cvtsi128_si32(value) as u8
1156}
1157
1158#[cfg(all(feature = "simd", target_arch = "x86_64"))]
1159#[target_feature(enable = "avx2")]
1160unsafe fn horizontal_max_u8_128(mut value: __m128i) -> u8 {
1161    value = _mm_max_epu8(value, _mm_srli_si128(value, 8));
1162    value = _mm_max_epu8(value, _mm_srli_si128(value, 4));
1163    value = _mm_max_epu8(value, _mm_srli_si128(value, 2));
1164    value = _mm_max_epu8(value, _mm_srli_si128(value, 1));
1165    _mm_cvtsi128_si32(value) as u8
1166}
1167
1168/// Bin Phred+33 qualities into threshold indexes.
1169///
1170/// Thresholds are lower bounds for the next bin. For thresholds `[10, 20, 30]`,
1171/// Phred 0-9 maps to 0, 10-19 maps to 1, 20-29 maps to 2, and 30+ maps to 3.
1172pub fn bin_qualities_into(
1173    qualities: &[u8],
1174    thresholds: &[u8],
1175    out: &mut Vec<u8>,
1176) -> Result<QualitySummary, PackError> {
1177    validate_thresholds(thresholds)?;
1178    out.clear();
1179    out.reserve(qualities.len());
1180
1181    let mut summary = QualitySummary::default();
1182    for (offset, &byte) in qualities.iter().enumerate() {
1183        let phred = phred33(byte, offset)?;
1184        summary.observe(phred);
1185        out.push(quality_bin(phred, thresholds));
1186    }
1187    Ok(summary)
1188}
1189
1190/// Bin Phred+33 qualities into a caller-provided output slice.
1191///
1192/// See [`bin_qualities_into`] for threshold semantics.
1193pub fn bin_qualities_into_slice(
1194    qualities: &[u8],
1195    thresholds: &[u8],
1196    out: &mut [u8],
1197) -> Result<QualitySummary, PackError> {
1198    validate_thresholds(thresholds)?;
1199    if out.len() < qualities.len() {
1200        return Err(PackError::OutputTooSmall {
1201            buffer: PackBuffer::QualityBins,
1202            needed: qualities.len(),
1203            provided: out.len(),
1204        });
1205    }
1206
1207    let mut summary = QualitySummary::default();
1208    for (offset, &byte) in qualities.iter().enumerate() {
1209        let phred = phred33(byte, offset)?;
1210        summary.observe(phred);
1211        out[offset] = quality_bin(phred, thresholds);
1212    }
1213    Ok(summary)
1214}
1215
1216fn pack_trusted_fastq_direct_slab(
1217    input: &[u8],
1218    context: SlabContext,
1219    bases: &mut Vec<u8>,
1220    n_mask: &mut Vec<u8>,
1221    sink: &mut impl TrustedPackSink,
1222) -> FastqResult<SlabResult> {
1223    let mut cursor = 0;
1224    let mut records = 0_u64;
1225    let mut newlines = memchr::memchr_iter(b'\n', input);
1226
1227    while cursor < input.len() {
1228        let record_start = cursor;
1229        let Some(name) = trusted_direct_line(input, &mut cursor, &mut newlines, context.eof) else {
1230            return Ok(SlabResult {
1231                next_start: record_start,
1232                records,
1233            });
1234        };
1235        let Some(seq) = trusted_direct_line(input, &mut cursor, &mut newlines, context.eof) else {
1236            return incomplete_or_truncated_direct(input, context, record_start, records, 1);
1237        };
1238        let Some(plus) = trusted_direct_line(input, &mut cursor, &mut newlines, context.eof) else {
1239            return incomplete_or_truncated_direct(input, context, record_start, records, 2);
1240        };
1241        let Some(qual) = trusted_direct_line(input, &mut cursor, &mut newlines, context.eof) else {
1242            return incomplete_or_truncated_direct(input, context, record_start, records, 3);
1243        };
1244        let record = RecordLines {
1245            name,
1246            seq,
1247            plus,
1248            qual,
1249        };
1250
1251        observe_trusted_packed_record(
1252            record,
1253            context.base_offset,
1254            context.first_record_index + records,
1255            bases,
1256            n_mask,
1257            sink,
1258        )?;
1259        records += 1;
1260    }
1261
1262    Ok(SlabResult {
1263        next_start: input.len(),
1264        records,
1265    })
1266}
1267
1268fn trusted_direct_line<'a>(
1269    input: &'a [u8],
1270    cursor: &mut usize,
1271    newlines: &mut impl Iterator<Item = usize>,
1272    eof: bool,
1273) -> Option<Line<'a>> {
1274    let start = *cursor;
1275    if start >= input.len() {
1276        return None;
1277    }
1278
1279    let end = match newlines.next() {
1280        Some(end) => {
1281            *cursor = end + 1;
1282            end
1283        }
1284        None if eof => {
1285            *cursor = input.len();
1286            input.len()
1287        }
1288        None => return None,
1289    };
1290    let end = fastq_frame::trim_cr_end(input, start, end);
1291    Some(Line {
1292        bytes: &input[start..end],
1293        start,
1294    })
1295}
1296
1297fn incomplete_or_truncated_direct(
1298    _input: &[u8],
1299    context: SlabContext,
1300    record_start: usize,
1301    records: u64,
1302    line_index: u8,
1303) -> FastqResult<SlabResult> {
1304    if context.eof {
1305        Err(fastq_frame::format_at(
1306            "truncated FASTQ record",
1307            context.base_offset,
1308            record_start,
1309            context.first_record_index + records,
1310            line_index,
1311        ))
1312    } else {
1313        Ok(SlabResult {
1314            next_start: record_start,
1315            records,
1316        })
1317    }
1318}
1319
1320#[derive(Clone, Copy)]
1321struct SlabContext {
1322    base_offset: u64,
1323    first_record_index: u64,
1324    eof: bool,
1325}
1326
1327#[derive(Clone, Copy)]
1328struct SlabResult {
1329    next_start: usize,
1330    records: u64,
1331}
1332
1333fn observe_trusted_packed_record(
1334    record: RecordLines<'_>,
1335    base_offset: u64,
1336    record_index: u64,
1337    bases: &mut Vec<u8>,
1338    n_mask: &mut Vec<u8>,
1339    sink: &mut impl TrustedPackSink,
1340) -> FastqResult<()> {
1341    fastq_frame::validate_record(
1342        record,
1343        base_offset,
1344        record_index,
1345        RecordValidation::TRUSTED_PACK,
1346    )?;
1347
1348    let summary =
1349        pack_bases_and_summarize_qualities_into(record.seq.bytes, record.qual.bytes, bases, n_mask)
1350            .map_err(|err| {
1351                fastq_frame::format_at(
1352                    err.to_string(),
1353                    base_offset,
1354                    record.qual.start,
1355                    record_index,
1356                    3,
1357                )
1358            })?;
1359    sink.record(TrustedPackedRecord {
1360        name: record.name.bytes,
1361        seq: record.seq.bytes,
1362        qual: record.qual.bytes,
1363        bases: &bases[..],
1364        n_mask: &n_mask[..],
1365        summary,
1366    })
1367}
1368
1369fn trusted_pair_ids_match(
1370    first_name: &[u8],
1371    second_name: &[u8],
1372    mode: crate::PairValidation,
1373) -> bool {
1374    match mode {
1375        crate::PairValidation::None => true,
1376        crate::PairValidation::FastSlash => {
1377            fastq_frame::fast_slash_pair_ids_match(first_name, second_name).unwrap_or_else(|| {
1378                fastq_frame::normalized_pair_id(first_name)
1379                    == fastq_frame::normalized_pair_id(second_name)
1380            })
1381        }
1382        crate::PairValidation::Full => {
1383            fastq_frame::normalized_pair_id(first_name)
1384                == fastq_frame::normalized_pair_id(second_name)
1385        }
1386    }
1387}
1388
1389fn pack_bases_exact(seq: &[u8], bases: &mut [u8], n_mask: &mut [u8]) -> BaseSummary {
1390    let bases_needed = packed_base_len(seq.len());
1391    if bases_needed == 0 {
1392        return BaseSummary::default();
1393    }
1394
1395    bases[..bases_needed].fill(0);
1396    let mask_needed = bit_mask_len(seq.len());
1397    if mask_needed != 0 {
1398        n_mask[..mask_needed].fill(0);
1399    }
1400
1401    pack_bases_exact_zeroed(seq, bases, n_mask)
1402}
1403
1404fn pack_bases_exact_zeroed(seq: &[u8], bases: &mut [u8], n_mask: &mut [u8]) -> BaseSummary {
1405    let bases_needed = packed_base_len(seq.len());
1406    if bases_needed == 0 {
1407        return BaseSummary::default();
1408    }
1409
1410    let mut summary = BaseSummary {
1411        len: seq.len(),
1412        ..BaseSummary::default()
1413    };
1414
1415    let full_chunks = seq.len() / 4;
1416    let mut chunk_index = 0;
1417    let mut base_index = 0;
1418    #[cfg(feature = "simd")]
1419    while chunk_index + 4 <= full_chunks {
1420        pack_seq_16(
1421            &seq[base_index..base_index + 16],
1422            base_index,
1423            &mut bases[chunk_index..chunk_index + 4],
1424            &mut summary,
1425            n_mask,
1426        );
1427        chunk_index += 4;
1428        base_index += 16;
1429    }
1430    while chunk_index < full_chunks {
1431        let c0 = BASE_LUT[usize::from(seq[base_index])];
1432        let c1 = BASE_LUT[usize::from(seq[base_index + 1])];
1433        let c2 = BASE_LUT[usize::from(seq[base_index + 2])];
1434        let c3 = BASE_LUT[usize::from(seq[base_index + 3])];
1435        pack_quad_from_codes(
1436            c0,
1437            c1,
1438            c2,
1439            c3,
1440            base_index,
1441            &mut bases[chunk_index],
1442            &mut summary,
1443            n_mask,
1444        );
1445        chunk_index += 1;
1446        base_index += 4;
1447    }
1448
1449    let tail_start = full_chunks * 4;
1450    let mut index = tail_start;
1451    while index < seq.len() {
1452        let offset = index - tail_start;
1453        let code = BASE_LUT[usize::from(seq[index])];
1454        if code < BASE_N {
1455            add_base_count(&mut summary, code);
1456            bases[full_chunks] |= code << (offset * 2);
1457        } else {
1458            summary.n += 1;
1459            n_mask[index / 8] |= 1 << (index % 8);
1460        }
1461        index += 1;
1462    }
1463
1464    summary
1465}
1466
1467fn pack_bases_and_qualities_exact_zeroed(
1468    seq: &[u8],
1469    qualities: &[u8],
1470    bases: &mut [u8],
1471    n_mask: &mut [u8],
1472) -> Result<PackedRecordSummary, PackError> {
1473    let bases_needed = packed_base_len(seq.len());
1474    if bases_needed == 0 {
1475        return Ok(PackedRecordSummary::default());
1476    }
1477
1478    #[cfg(all(feature = "simd", target_arch = "x86_64"))]
1479    if seq.len() >= 32 && std::is_x86_feature_detected!("avx2") {
1480        return unsafe { pack_bases_and_qualities_exact_avx2(seq, qualities, bases, n_mask) };
1481    }
1482
1483    let mut bases_summary = BaseSummary {
1484        len: seq.len(),
1485        ..BaseSummary::default()
1486    };
1487    let mut quality_summary = QualityAccumulator::default();
1488
1489    let full_chunks = seq.len() / 4;
1490    let mut chunk_index = 0;
1491    let mut base_index = 0;
1492    #[cfg(feature = "simd")]
1493    while chunk_index + 4 <= full_chunks {
1494        pack_seq_16(
1495            &seq[base_index..base_index + 16],
1496            base_index,
1497            &mut bases[chunk_index..chunk_index + 4],
1498            &mut bases_summary,
1499            n_mask,
1500        );
1501        let end = base_index + 16;
1502        while base_index < end {
1503            quality_summary.observe(qualities[base_index], base_index)?;
1504            base_index += 1;
1505        }
1506        chunk_index += 4;
1507    }
1508    while chunk_index < full_chunks {
1509        let c0 = BASE_LUT[usize::from(seq[base_index])];
1510        let c1 = BASE_LUT[usize::from(seq[base_index + 1])];
1511        let c2 = BASE_LUT[usize::from(seq[base_index + 2])];
1512        let c3 = BASE_LUT[usize::from(seq[base_index + 3])];
1513        pack_quad_from_codes(
1514            c0,
1515            c1,
1516            c2,
1517            c3,
1518            base_index,
1519            &mut bases[chunk_index],
1520            &mut bases_summary,
1521            n_mask,
1522        );
1523        quality_summary.observe(qualities[base_index], base_index)?;
1524        quality_summary.observe(qualities[base_index + 1], base_index + 1)?;
1525        quality_summary.observe(qualities[base_index + 2], base_index + 2)?;
1526        quality_summary.observe(qualities[base_index + 3], base_index + 3)?;
1527        chunk_index += 1;
1528        base_index += 4;
1529    }
1530
1531    let tail_start = full_chunks * 4;
1532    let mut index = tail_start;
1533    while index < seq.len() {
1534        let offset = index - tail_start;
1535        let code = BASE_LUT[usize::from(seq[index])];
1536        if code < BASE_N {
1537            add_base_count(&mut bases_summary, code);
1538            bases[full_chunks] |= code << (offset * 2);
1539        } else {
1540            bases_summary.n += 1;
1541            n_mask[index / 8] |= 1 << (index % 8);
1542        }
1543        quality_summary.observe(qualities[index], index)?;
1544        index += 1;
1545    }
1546
1547    Ok(PackedRecordSummary {
1548        bases: bases_summary,
1549        qualities: quality_summary.finish(),
1550    })
1551}
1552
1553#[cfg(all(feature = "simd", target_arch = "x86_64"))]
1554#[target_feature(enable = "avx2")]
1555unsafe fn pack_bases_and_qualities_exact_avx2(
1556    seq: &[u8],
1557    qualities: &[u8],
1558    bases: &mut [u8],
1559    n_mask: &mut [u8],
1560) -> Result<PackedRecordSummary, PackError> {
1561    let low = _mm256_set1_epi8(33);
1562    let high = _mm256_set1_epi8(126);
1563    let offset = _mm256_set1_epi8(33);
1564    let q20 = _mm256_set1_epi8(52);
1565    let q30 = _mm256_set1_epi8(62);
1566    let zero = _mm256_setzero_si256();
1567    let mut min_phred = _mm256_set1_epi8(93);
1568    let mut max_phred = _mm256_setzero_si256();
1569    let mut sum_phred = _mm256_setzero_si256();
1570
1571    let mut bases_summary = BaseSummary {
1572        len: seq.len(),
1573        ..BaseSummary::default()
1574    };
1575    let mut quality_summary = QualityAccumulator::default();
1576    let mut base_index = 0;
1577    let mut chunk_index = 0;
1578
1579    while base_index + 32 <= seq.len() {
1580        let block_start = base_index;
1581        pack_seq_16(
1582            &seq[base_index..base_index + 16],
1583            base_index,
1584            &mut bases[chunk_index..chunk_index + 4],
1585            &mut bases_summary,
1586            n_mask,
1587        );
1588        base_index += 16;
1589        chunk_index += 4;
1590
1591        pack_seq_16(
1592            &seq[base_index..base_index + 16],
1593            base_index,
1594            &mut bases[chunk_index..chunk_index + 4],
1595            &mut bases_summary,
1596            n_mask,
1597        );
1598        base_index += 16;
1599        chunk_index += 4;
1600
1601        let bytes =
1602            unsafe { _mm256_loadu_si256(qualities.as_ptr().add(block_start).cast::<__m256i>()) };
1603        let too_low = _mm256_cmpgt_epi8(low, bytes);
1604        let too_high = _mm256_cmpgt_epi8(bytes, high);
1605        let invalid = _mm256_movemask_epi8(_mm256_or_si256(too_low, too_high));
1606        if invalid != 0 {
1607            let offset = invalid.trailing_zeros() as usize;
1608            return Err(PackError::InvalidQuality {
1609                offset: block_start + offset,
1610                byte: qualities[block_start + offset],
1611            });
1612        }
1613
1614        quality_summary.q20_bases +=
1615            _mm256_movemask_epi8(_mm256_cmpgt_epi8(bytes, q20)).count_ones() as usize;
1616        quality_summary.q30_bases +=
1617            _mm256_movemask_epi8(_mm256_cmpgt_epi8(bytes, q30)).count_ones() as usize;
1618        let phreds = _mm256_sub_epi8(bytes, offset);
1619        min_phred = _mm256_min_epu8(min_phred, phreds);
1620        max_phred = _mm256_max_epu8(max_phred, phreds);
1621        sum_phred = _mm256_add_epi64(sum_phred, _mm256_sad_epu8(phreds, zero));
1622        quality_summary.len += 32;
1623    }
1624
1625    unsafe { finish_avx2_quality_vectors(&mut quality_summary, min_phred, max_phred, sum_phred) };
1626
1627    let full_chunks = seq.len() / 4;
1628    while chunk_index < full_chunks {
1629        let c0 = BASE_LUT[usize::from(seq[base_index])];
1630        let c1 = BASE_LUT[usize::from(seq[base_index + 1])];
1631        let c2 = BASE_LUT[usize::from(seq[base_index + 2])];
1632        let c3 = BASE_LUT[usize::from(seq[base_index + 3])];
1633        pack_quad_from_codes(
1634            c0,
1635            c1,
1636            c2,
1637            c3,
1638            base_index,
1639            &mut bases[chunk_index],
1640            &mut bases_summary,
1641            n_mask,
1642        );
1643        quality_summary.observe(qualities[base_index], base_index)?;
1644        quality_summary.observe(qualities[base_index + 1], base_index + 1)?;
1645        quality_summary.observe(qualities[base_index + 2], base_index + 2)?;
1646        quality_summary.observe(qualities[base_index + 3], base_index + 3)?;
1647        chunk_index += 1;
1648        base_index += 4;
1649    }
1650
1651    while base_index < seq.len() {
1652        let offset = base_index - (full_chunks * 4);
1653        let code = BASE_LUT[usize::from(seq[base_index])];
1654        if code < BASE_N {
1655            add_base_count(&mut bases_summary, code);
1656            bases[full_chunks] |= code << (offset * 2);
1657        } else {
1658            bases_summary.n += 1;
1659            n_mask[base_index / 8] |= 1 << (base_index % 8);
1660        }
1661        quality_summary.observe(qualities[base_index], base_index)?;
1662        base_index += 1;
1663    }
1664
1665    Ok(PackedRecordSummary {
1666        bases: bases_summary,
1667        qualities: quality_summary.finish(),
1668    })
1669}
1670
1671#[cfg(feature = "simd")]
1672#[inline(always)]
1673fn pack_seq_16(
1674    seq: &[u8],
1675    base_index: usize,
1676    bases: &mut [u8],
1677    summary: &mut BaseSummary,
1678    n_mask: &mut [u8],
1679) {
1680    debug_assert!(seq.len() >= 16);
1681    debug_assert!(bases.len() >= 4);
1682    debug_assert_eq!(base_index % 8, 0);
1683
1684    let e0 = quad_entry_from_bases(seq[0], seq[1], seq[2], seq[3]);
1685    let e1 = quad_entry_from_bases(seq[4], seq[5], seq[6], seq[7]);
1686    let e2 = quad_entry_from_bases(seq[8], seq[9], seq[10], seq[11]);
1687    let e3 = quad_entry_from_bases(seq[12], seq[13], seq[14], seq[15]);
1688
1689    bases[0] = e0 as u8;
1690    bases[1] = e1 as u8;
1691    bases[2] = e2 as u8;
1692    bases[3] = e3 as u8;
1693
1694    let mask01 = (((e0 >> 8) & 0x0f) | (((e1 >> 8) & 0x0f) << 4)) as u8;
1695    if mask01 != 0 {
1696        n_mask[base_index / 8] |= mask01;
1697    }
1698    let mask23 = (((e2 >> 8) & 0x0f) | (((e3 >> 8) & 0x0f) << 4)) as u8;
1699    if mask23 != 0 {
1700        n_mask[base_index / 8 + 1] |= mask23;
1701    }
1702
1703    summary.a += entry_count_4(e0, e1, e2, e3, 12);
1704    summary.c += entry_count_4(e0, e1, e2, e3, 15);
1705    summary.g += entry_count_4(e0, e1, e2, e3, 18);
1706    summary.t += entry_count_4(e0, e1, e2, e3, 21);
1707    summary.n += entry_count_4(e0, e1, e2, e3, 24);
1708}
1709
1710#[cfg(feature = "simd")]
1711#[inline(always)]
1712fn quad_entry_from_bases(b0: u8, b1: u8, b2: u8, b3: u8) -> u32 {
1713    quad_entry_from_codes(
1714        BASE_LUT[usize::from(b0)],
1715        BASE_LUT[usize::from(b1)],
1716        BASE_LUT[usize::from(b2)],
1717        BASE_LUT[usize::from(b3)],
1718    )
1719}
1720
1721#[cfg(feature = "simd")]
1722#[inline(always)]
1723fn entry_count_4(e0: u32, e1: u32, e2: u32, e3: u32, shift: u32) -> usize {
1724    (((e0 >> shift) & 0x07)
1725        + ((e1 >> shift) & 0x07)
1726        + ((e2 >> shift) & 0x07)
1727        + ((e3 >> shift) & 0x07)) as usize
1728}
1729
1730#[inline(always)]
1731#[allow(clippy::too_many_arguments)]
1732fn pack_quad_from_codes(
1733    c0: u8,
1734    c1: u8,
1735    c2: u8,
1736    c3: u8,
1737    base_index: usize,
1738    base_out: &mut u8,
1739    summary: &mut BaseSummary,
1740    n_mask: &mut [u8],
1741) {
1742    apply_quad_entry(
1743        quad_entry_from_codes(c0, c1, c2, c3),
1744        base_index,
1745        base_out,
1746        summary,
1747        n_mask,
1748    );
1749}
1750
1751#[inline(always)]
1752fn quad_entry_from_codes(c0: u8, c1: u8, c2: u8, c3: u8) -> u32 {
1753    debug_assert!(c0 <= BASE_N);
1754    debug_assert!(c1 <= BASE_N);
1755    debug_assert!(c2 <= BASE_N);
1756    debug_assert!(c3 <= BASE_N);
1757    let key = quad_key(c0, c1, c2, c3);
1758    debug_assert!(key < BASE_QUAD_LUT.len());
1759    // Codes are produced by BASE_LUT, whose only values are 0..=BASE_N, so
1760    // the base-5 quad key is always inside BASE_QUAD_LUT.
1761    unsafe { *BASE_QUAD_LUT.get_unchecked(key) }
1762}
1763
1764#[inline(always)]
1765fn apply_quad_entry(
1766    entry: u32,
1767    base_index: usize,
1768    base_out: &mut u8,
1769    summary: &mut BaseSummary,
1770    n_mask: &mut [u8],
1771) {
1772    *base_out = entry as u8;
1773    let mask = ((entry >> 8) & 0x0f) as u8;
1774    if mask != 0 {
1775        n_mask[base_index / 8] |= mask << (base_index % 8);
1776    }
1777    summary.a += ((entry >> 12) & 0x07) as usize;
1778    summary.c += ((entry >> 15) & 0x07) as usize;
1779    summary.g += ((entry >> 18) & 0x07) as usize;
1780    summary.t += ((entry >> 21) & 0x07) as usize;
1781    summary.n += ((entry >> 24) & 0x07) as usize;
1782}
1783
1784struct QualityAccumulator {
1785    len: usize,
1786    min_phred: u8,
1787    max_phred: u8,
1788    sum_phred: u64,
1789    q20_bases: usize,
1790    q30_bases: usize,
1791}
1792
1793impl Default for QualityAccumulator {
1794    fn default() -> Self {
1795        Self {
1796            len: 0,
1797            min_phred: u8::MAX,
1798            max_phred: 0,
1799            sum_phred: 0,
1800            q20_bases: 0,
1801            q30_bases: 0,
1802        }
1803    }
1804}
1805
1806impl QualityAccumulator {
1807    #[inline(always)]
1808    fn observe(&mut self, byte: u8, offset: usize) -> Result<(), PackError> {
1809        let phred = phred33(byte, offset)?;
1810        self.min_phred = self.min_phred.min(phred);
1811        self.max_phred = self.max_phred.max(phred);
1812        self.len += 1;
1813        self.sum_phred += u64::from(phred);
1814        self.q20_bases += usize::from(phred >= 20);
1815        self.q30_bases += usize::from(phred >= 30);
1816        Ok(())
1817    }
1818
1819    #[inline(always)]
1820    fn finish(self) -> QualitySummary {
1821        if self.len == 0 {
1822            return QualitySummary::default();
1823        }
1824        QualitySummary {
1825            len: self.len,
1826            min_phred: Some(self.min_phred),
1827            max_phred: Some(self.max_phred),
1828            sum_phred: self.sum_phred,
1829            q20_bases: self.q20_bases,
1830            q30_bases: self.q30_bases,
1831        }
1832    }
1833}
1834
1835#[inline(always)]
1836fn add_base_count(summary: &mut BaseSummary, code: u8) {
1837    match code {
1838        0 => summary.a += 1,
1839        1 => summary.c += 1,
1840        2 => summary.g += 1,
1841        3 => summary.t += 1,
1842        _ => unreachable!(),
1843    }
1844}
1845
1846#[inline(always)]
1847fn phred33(byte: u8, offset: usize) -> Result<u8, PackError> {
1848    if (33..=126).contains(&byte) {
1849        Ok(byte - 33)
1850    } else {
1851        Err(PackError::InvalidQuality { offset, byte })
1852    }
1853}
1854
1855fn validate_thresholds(thresholds: &[u8]) -> Result<(), PackError> {
1856    if thresholds.len() > usize::from(u8::MAX) {
1857        return Err(PackError::TooManyQualityThresholds {
1858            count: thresholds.len(),
1859        });
1860    }
1861
1862    for (index, pair) in thresholds.windows(2).enumerate() {
1863        if pair[0] > pair[1] {
1864            return Err(PackError::UnsortedQualityThresholds { index: index + 1 });
1865        }
1866    }
1867
1868    Ok(())
1869}
1870
1871fn quality_bin(phred: u8, thresholds: &[u8]) -> u8 {
1872    let mut bin = 0;
1873    for &threshold in thresholds {
1874        if phred < threshold {
1875            break;
1876        }
1877        bin += 1;
1878    }
1879    bin
1880}
1881
1882#[cfg(test)]
1883mod tests;