Skip to main content

vyre_primitives/decode/
rle_segment_lengths.rs

1//! `rle_segment_lengths`  -  run-length-encoded segment-length scan with
2//! per-segment output start positions.
3//!
4//! Op id: `vyre-primitives::decode::rle_segment_lengths`. Soundness: `Exact`
5//! over the RLE segment header layout where each segment is a (length, value)
6//! pair packed into one u32. The CPU reference at the bottom of this file is
7//! the contract.
8//!
9//! ## Why it matters
10//!
11//! Block-oriented compression formats (LZ4 literal/match runs, zstd FSE
12//! literal counts, PNG IDAT zlib chunks, snappy raw runs) decode via a
13//! sequence of "emit N copies of value V" segments. The bottleneck on GPU
14//! is figuring out *where each segment writes*  -  segment K's output offset
15//! depends on the cumulative segment-length sum of segments 0..K. Naive
16//! sequential scan serializes the whole decode.
17//!
18//! This primitive ships the prefix-sum pre-pass: read the segment headers,
19//! emit a per-segment "starts here" offset array. Once each thread knows
20//! its absolute output range it can launch a separate, fully parallel,
21//! load-balanced expand pass. This is the LZ4-style "decode in two passes"
22//! trick lifted to the GPU.
23//!
24//! ## Wire layout
25//!
26//! Inputs:
27//!   - `segments_in`  -  u32 stream where each u32 packs `(length << 8) | value`
28//!     (24-bit length max ≈ 16 MB per segment, 8-bit value).
29//!
30//! Outputs:
31//!   - `segment_lengths_out`  -  u32 per segment: just the length field.
32//!   - `segment_values_out`  -  u32 per segment: just the value field
33//!     (zero-extended into u32 for downstream byte materialization).
34//!
35//! The prefix-sum that converts `segment_lengths_out` into per-segment
36//! start offsets is the existing `prefix_scan` primitive (math/#5).
37//! This module emits the unpacked length + value arrays it consumes.
38//!
39//! ## Why split, not fuse
40//!
41//! Splitting unpack from prefix-sum is the right separation: the unpack is
42//! one-load-per-segment with no inter-thread dependency, while prefix-sum
43//! is a tree-reduction with logarithmic-depth communication. Different
44//! launch-grid shapes, different optimization trade-offs. Fusing them
45//! would force the prefix-sum to wait on the unpack inside the same
46//! warp's lifetime  -  strictly worse occupancy.
47
48use std::sync::Arc;
49
50use vyre_foundation::ir::model::expr::Ident;
51use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
52
53/// Canonical op id for region-chain audits and bench attribution.
54pub const OP_ID: &str = "vyre-primitives::decode::rle_segment_lengths";
55
56/// Canonical binding indices.
57pub const BINDING_SEGMENTS_IN: u32 = 0;
58/// Per-segment length output binding.
59pub const BINDING_SEGMENT_LENGTHS_OUT: u32 = 1;
60/// Per-segment value output binding.
61pub const BINDING_SEGMENT_VALUES_OUT: u32 = 2;
62
63/// Maximum segment length representable in the 24-bit length field
64/// (= 16777215 ≈ 16 MB per segment).
65pub const MAX_SEGMENT_LENGTH: u32 = (1 << 24) - 1;
66
67/// Maximum segment value representable in the 8-bit value field.
68pub const MAX_SEGMENT_VALUE: u32 = 0xFF;
69
70/// One lane per packed RLE segment.
71pub const RLE_SEGMENT_LENGTHS_WORKGROUP_SIZE: [u32; 3] = [256, 1, 1];
72
73/// Dispatch grid that covers every packed RLE segment lane.
74#[must_use]
75pub const fn rle_segment_lengths_dispatch_grid(segment_count: u32) -> [u32; 3] {
76    let lanes_per_block = RLE_SEGMENT_LENGTHS_WORKGROUP_SIZE[0];
77    let full_blocks = segment_count / lanes_per_block;
78    let tail_block = if segment_count % lanes_per_block == 0 {
79        0
80    } else {
81        1
82    };
83    let blocks = full_blocks + tail_block;
84    [if blocks == 0 { 1 } else { blocks }, 1, 1]
85}
86
87/// Pack errors raised by the host-side packer.
88#[derive(Debug, Clone, PartialEq, Eq)]
89#[non_exhaustive]
90pub enum PackError {
91    /// Segment length exceeded the 24-bit field budget.
92    LengthTooLarge {
93        /// The segment index whose length overflowed.
94        segment: usize,
95        /// The length that exceeded `MAX_SEGMENT_LENGTH`.
96        length: u32,
97    },
98    /// Segment value exceeded the 8-bit field budget.
99    ValueTooLarge {
100        /// The segment index whose value overflowed.
101        segment: usize,
102        /// The value that exceeded `MAX_SEGMENT_VALUE`.
103        value: u32,
104    },
105    /// Caller-owned output storage could not be reserved.
106    AllocationFailed {
107        /// Allocator or capacity diagnostic.
108        message: String,
109    },
110}
111
112/// Build the IR `Program` that unpacks `(length, value)` segments from the
113/// packed RLE header stream.
114///
115/// One thread per segment. Each thread:
116///   1. Loads `segments_in[gid]`.
117///   2. Extracts `length = segment >> 8` and `value = segment & 0xFF`.
118///   3. Stores both into the per-segment output buffers.
119///
120/// `segment_count` must be > 0; workgroup size is fixed at 256 lanes.
121#[must_use]
122pub fn rle_segment_lengths(segment_count: u32) -> Program {
123    if segment_count == 0 {
124        return crate::invalid_output_program(
125            OP_ID,
126            "segment_lengths_out",
127            DataType::U32,
128            "Fix: rle_segment_lengths requires segment_count > 0, got 0.".to_string(),
129        );
130    }
131
132    let body = vec![
133        Node::let_bind("seg_idx", Expr::InvocationId { axis: 0 }),
134        Node::if_then(
135            Expr::lt(Expr::var("seg_idx"), Expr::u32(segment_count)),
136            vec![
137                Node::let_bind("packed", Expr::load("segments_in", Expr::var("seg_idx"))),
138                Node::let_bind("length", Expr::shr(Expr::var("packed"), Expr::u32(8))),
139                Node::let_bind("value", Expr::bitand(Expr::var("packed"), Expr::u32(0xFF))),
140                Node::store(
141                    "segment_lengths_out",
142                    Expr::var("seg_idx"),
143                    Expr::var("length"),
144                ),
145                Node::store(
146                    "segment_values_out",
147                    Expr::var("seg_idx"),
148                    Expr::var("value"),
149                ),
150            ],
151        ),
152    ];
153
154    let buffers = vec![
155        BufferDecl::storage(
156            "segments_in",
157            BINDING_SEGMENTS_IN,
158            BufferAccess::ReadOnly,
159            DataType::U32,
160        )
161        .with_count(segment_count),
162        BufferDecl::storage(
163            "segment_lengths_out",
164            BINDING_SEGMENT_LENGTHS_OUT,
165            BufferAccess::ReadWrite,
166            DataType::U32,
167        )
168        .with_count(segment_count),
169        BufferDecl::storage(
170            "segment_values_out",
171            BINDING_SEGMENT_VALUES_OUT,
172            BufferAccess::ReadWrite,
173            DataType::U32,
174        )
175        .with_count(segment_count),
176    ];
177
178    let entry = vec![Node::Region {
179        generator: Ident::from(OP_ID),
180        source_region: None,
181        body: Arc::new(body),
182    }];
183    Program::wrapped(buffers, RLE_SEGMENT_LENGTHS_WORKGROUP_SIZE, entry)
184}
185
186/// Pack `(length, value)` pairs into the canonical u32 wire format.
187///
188/// # Errors
189///
190/// Returns the first encoding overflow encountered. Caller fixes by
191/// splitting overlong segments (length > 24 bits) or refusing to
192/// register a value > 255.
193pub fn pack_rle_segments(segments: &[(u32, u8)]) -> Result<Vec<u32>, PackError> {
194    let mut packed = Vec::with_capacity(segments.len());
195    try_pack_rle_segments_into(segments, &mut packed)?;
196    Ok(packed)
197}
198
199/// Pack `(length, value)` pairs into caller-owned storage.
200///
201/// Clears `out`, then reuses its capacity.
202///
203/// # Errors
204///
205/// Returns the first encoding overflow encountered. On error, `out` is
206/// cleared and contains only segments packed before the failing one.
207pub fn pack_rle_segments_into(segments: &[(u32, u8)], out: &mut Vec<u32>) -> Result<(), PackError> {
208    try_pack_rle_segments_into(segments, out)
209}
210
211/// Fallible pack into caller-owned storage.
212///
213/// Clears `out`, then reuses its capacity. On encoding error, `out`
214/// contains only segments packed before the failing one.
215pub fn try_pack_rle_segments_into(
216    segments: &[(u32, u8)],
217    out: &mut Vec<u32>,
218) -> Result<(), PackError> {
219    if segments.len() > out.capacity() {
220        reserve_items(out, segments.len(), "RLE segment packer", "packed segments")
221            .map_err(|message| PackError::AllocationFailed { message })?;
222    }
223    out.clear();
224    for (idx, (length, value)) in segments.iter().enumerate() {
225        if *length > MAX_SEGMENT_LENGTH {
226            return Err(PackError::LengthTooLarge {
227                segment: idx,
228                length: *length,
229            });
230        }
231        let value_u32 = u32::from(*value);
232        if value_u32 > MAX_SEGMENT_VALUE {
233            // Unreachable for u8 input but kept defensively for parallel
234            // u16/u32 entry points added later.
235            return Err(PackError::ValueTooLarge {
236                segment: idx,
237                value: value_u32,
238            });
239        }
240        out.push((length << 8) | value_u32);
241    }
242    Ok(())
243}
244
245/// CPU reference. Returns `(lengths, values)` matching the GPU `Program`
246/// lane-for-lane.
247#[must_use]
248#[cfg(any(test, feature = "cpu-parity"))]
249pub fn rle_segment_lengths_cpu(segments_in: &[u32]) -> (Vec<u32>, Vec<u32>) {
250    let mut lengths = Vec::new();
251    let mut values = Vec::new();
252    if let Err(error) = try_rle_segment_lengths_cpu_into(segments_in, &mut lengths, &mut values) {
253        // A decode oracle that clears to empty on failure silently masks a
254        // parity divergence (Law 10 / Law 6). Fail loud; use the try_ variant.
255        panic!("vyre-primitives RLE segment-lengths reference failed: {error}");
256    }
257    (lengths, values)
258}
259
260/// CPU reference into caller-owned output buffers.
261///
262/// Clears `lengths` and `values`, then reuses their allocations.
263#[cfg(any(test, feature = "cpu-parity"))]
264pub fn rle_segment_lengths_cpu_into(
265    segments_in: &[u32],
266    lengths: &mut Vec<u32>,
267    values: &mut Vec<u32>,
268) {
269    if let Err(error) = try_rle_segment_lengths_cpu_into(segments_in, lengths, values) {
270        panic!("vyre-primitives RLE segment-lengths reference failed: {error}");
271    }
272}
273
274/// Fallible CPU reference into caller-owned output buffers.
275#[cfg(any(test, feature = "cpu-parity"))]
276pub fn try_rle_segment_lengths_cpu_into(
277    segments_in: &[u32],
278    lengths: &mut Vec<u32>,
279    values: &mut Vec<u32>,
280) -> Result<(), String> {
281    reserve_u32_output(
282        lengths,
283        segments_in.len(),
284        "RLE segment lengths CPU oracle",
285        "lengths",
286    )?;
287    reserve_u32_output(
288        values,
289        segments_in.len(),
290        "RLE segment lengths CPU oracle",
291        "values",
292    )?;
293    lengths.clear();
294    values.clear();
295    for packed in segments_in {
296        lengths.push(packed >> 8);
297        values.push(packed & 0xFF);
298    }
299    Ok(())
300}
301
302/// Compute per-segment output start offsets via exclusive prefix sum
303/// over `segment_lengths`. CPU reference for the canonical
304/// "RLE → expand-pass start offsets" pipeline. The GPU version of this
305/// step is `math::prefix_scan` (#5).
306///
307/// Returns `(start_offsets, total_output_length)`.
308#[must_use]
309#[cfg(any(test, feature = "cpu-parity"))]
310pub fn rle_segment_start_offsets_cpu(segment_lengths: &[u32]) -> (Vec<u32>, u32) {
311    let mut offsets = Vec::new();
312    let total = match try_rle_segment_start_offsets_cpu_into(segment_lengths, &mut offsets) {
313        Ok(total) => total,
314        // Clearing and returning 0 on failure silently masks a parity
315        // divergence (Law 10 / Law 6). Fail loud; use the try_ variant.
316        Err(error) => panic!("vyre-primitives RLE start-offsets reference failed: {error}"),
317    };
318    (offsets, total)
319}
320
321/// Compute exclusive start offsets into caller-owned storage.
322///
323/// Clears `offsets`, then reuses its capacity. Returns the saturated total
324/// output length.
325#[cfg(any(test, feature = "cpu-parity"))]
326pub fn rle_segment_start_offsets_cpu_into(segment_lengths: &[u32], offsets: &mut Vec<u32>) -> u32 {
327    match try_rle_segment_start_offsets_cpu_into(segment_lengths, offsets) {
328        Ok(total) => total,
329        Err(error) => panic!("vyre-primitives RLE start-offsets reference failed: {error}"),
330    }
331}
332
333/// Fallible exclusive start-offset computation into caller-owned storage.
334#[cfg(any(test, feature = "cpu-parity"))]
335pub fn try_rle_segment_start_offsets_cpu_into(
336    segment_lengths: &[u32],
337    offsets: &mut Vec<u32>,
338) -> Result<u32, String> {
339    reserve_u32_output(
340        offsets,
341        segment_lengths.len(),
342        "RLE segment offset CPU oracle",
343        "start offsets",
344    )?;
345    offsets.clear();
346    let mut acc: u32 = 0;
347    for length in segment_lengths {
348        offsets.push(acc);
349        acc = acc.saturating_add(*length);
350    }
351    Ok(acc)
352}
353
354/// Decode a packed RLE stream to its expanded byte sequence. Composes
355/// the unpack + start-offset + emit-bytes passes. CPU reference for
356/// end-to-end RLE decode used by integration tests.
357#[must_use]
358#[cfg(any(test, feature = "cpu-parity"))]
359pub fn rle_decode_cpu(segments_in: &[u32]) -> Vec<u8> {
360    let mut output = Vec::new();
361    if let Err(error) = try_rle_decode_cpu_into(segments_in, &mut output) {
362        // Clearing to empty on failure silently masks a parity divergence
363        // (Law 10 / Law 6). Fail loud; use the try_ variant.
364        panic!("vyre-primitives RLE decode reference failed: {error}");
365    }
366    output
367}
368
369/// Decode packed RLE into caller-owned output storage.
370///
371/// Clears `output`, pre-reserves the saturated decoded byte length, and emits
372/// each run directly from the packed header stream without building temporary
373/// length/value vectors.
374#[cfg(any(test, feature = "cpu-parity"))]
375pub fn rle_decode_cpu_into(segments_in: &[u32], output: &mut Vec<u8>) {
376    if let Err(error) = try_rle_decode_cpu_into(segments_in, output) {
377        panic!("vyre-primitives RLE decode reference failed: {error}");
378    }
379}
380
381/// Fallible packed RLE decode into caller-owned output storage.
382#[cfg(any(test, feature = "cpu-parity"))]
383pub fn try_rle_decode_cpu_into(segments_in: &[u32], output: &mut Vec<u8>) -> Result<(), String> {
384    let total = decoded_len(segments_in);
385    if total > output.capacity() {
386        reserve_items(output, total, "RLE decode CPU oracle", "decoded output")?;
387    }
388    output.clear();
389    for packed in segments_in {
390        let length = (packed >> 8) as usize;
391        let value = (packed & 0xFF) as u8;
392        let new_len = output.len().saturating_add(length);
393        output.resize(new_len, value);
394    }
395    Ok(())
396}
397
398#[cfg(any(test, feature = "cpu-parity"))]
399fn decoded_len(segments_in: &[u32]) -> usize {
400    segments_in
401        .iter()
402        .map(|packed| packed >> 8)
403        .fold(0_u32, u32::saturating_add) as usize
404}
405
406#[cfg(any(test, feature = "cpu-parity"))]
407fn reserve_u32_output(
408    out: &mut Vec<u32>,
409    len: usize,
410    context: &str,
411    name: &str,
412) -> Result<(), String> {
413    if len > out.capacity() {
414        reserve_items(out, len, context, name)?;
415    }
416    Ok(())
417}
418
419fn reserve_items<T>(out: &mut Vec<T>, len: usize, context: &str, name: &str) -> Result<(), String> {
420    if len > out.capacity() {
421        out.try_reserve(len - out.len()).map_err(|err| {
422            format!(
423                "{context}: failed to reserve {len} items for {name}: {err}. Fix: shard the RLE stream before CPU parity evaluation."
424            )
425        })?;
426    }
427    Ok(())
428}
429
430#[cfg(feature = "inventory-registry")]
431fn fixture_u32(words: &[u32]) -> Vec<u8> {
432    crate::wire::pack_u32_slice(words)
433}
434
435#[cfg(feature = "inventory-registry")]
436inventory::submit! {
437    vyre_foundation::operation::OperationRegistration::primitive(
438        OP_ID,
439        || rle_segment_lengths(3),
440        Some(|| {
441            let packed = pack_rle_segments(&[(2, b'A'), (0, b'X'), (3, b'B')])
442                .unwrap_or_else(|_| unreachable!("fixture RLE segments fit the 24-bit length field"));
443            vec![vec![
444                fixture_u32(&packed),
445                fixture_u32(&[0, 0, 0]),
446                fixture_u32(&[0, 0, 0]),
447            ]]
448        }),
449        Some(|| vec![vec![
450            fixture_u32(&[2, 0, 3]),
451            fixture_u32(&[u32::from(b'A'), u32::from(b'X'), u32::from(b'B')]),
452        ]]),
453    )
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459
460    #[test]
461    fn pack_then_unpack_round_trips_simple_segments() {
462        let segments = [(1u32, 0xABu8), (5u32, 0xCDu8)];
463        let packed = pack_rle_segments(&segments).expect("Fix: pack must succeed");
464        let (lengths, values) = rle_segment_lengths_cpu(&packed);
465        assert_eq!(lengths, vec![1, 5]);
466        assert_eq!(values, vec![0xAB, 0xCD]);
467    }
468
469    #[test]
470    fn pack_rejects_length_at_field_boundary() {
471        let segments = [(1u32 << 24, 0u8)]; // exactly the limit + 1
472        match pack_rle_segments(&segments) {
473            Err(PackError::LengthTooLarge { segment: 0, length }) => {
474                assert_eq!(length, 1u32 << 24);
475            }
476            other => panic!("expected LengthTooLarge at the 24-bit boundary; got {other:?}"),
477        }
478    }
479
480    #[test]
481    fn pack_handles_max_representable_length() {
482        let segments = [(MAX_SEGMENT_LENGTH, 0xFFu8)];
483        let packed = pack_rle_segments(&segments).expect("Fix: max-length must pack");
484        let (lengths, values) = rle_segment_lengths_cpu(&packed);
485        assert_eq!(lengths, vec![MAX_SEGMENT_LENGTH]);
486        assert_eq!(values, vec![0xFF]);
487    }
488
489    #[test]
490    fn pack_handles_zero_length_segment_as_no_op() {
491        // Zero-length segments are valid (some encoders emit them as
492        // padding); they expand to nothing.
493        let segments = [(0u32, 0xABu8)];
494        let packed = pack_rle_segments(&segments).expect("Fix: zero-length must pack");
495        let (lengths, _values) = rle_segment_lengths_cpu(&packed);
496        assert_eq!(lengths, vec![0]);
497    }
498
499    #[test]
500    fn pack_preserves_per_segment_index_in_error() {
501        // Segment 3 is the bad one; error must report segment = 3.
502        let mut segments: Vec<(u32, u8)> = (0..10).map(|i| (i as u32, 0u8)).collect();
503        segments[3].0 = 1u32 << 25; // overflow
504        match pack_rle_segments(&segments) {
505            Err(PackError::LengthTooLarge { segment: 3, .. }) => {}
506            other => panic!("expected error at segment 3; got {other:?}"),
507        }
508    }
509
510    #[test]
511    fn start_offsets_are_exclusive_prefix_sum() {
512        let lengths = [3u32, 5, 2, 7];
513        let (offsets, total) = rle_segment_start_offsets_cpu(&lengths);
514        assert_eq!(offsets, vec![0, 3, 8, 10]);
515        assert_eq!(total, 17, "sum of lengths");
516    }
517
518    #[test]
519    fn start_offsets_handle_zero_length_runs_correctly() {
520        let lengths = [3u32, 0, 5, 0, 2];
521        let (offsets, total) = rle_segment_start_offsets_cpu(&lengths);
522        assert_eq!(offsets, vec![0, 3, 3, 8, 8]);
523        assert_eq!(total, 10);
524    }
525
526    #[test]
527    fn start_offsets_handle_empty_input() {
528        let (offsets, total) = rle_segment_start_offsets_cpu(&[]);
529        assert!(offsets.is_empty());
530        assert_eq!(total, 0);
531    }
532
533    #[test]
534    fn end_to_end_decode_expands_runs_in_order() {
535        // [(3, 'A'), (2, 'B'), (1, 'C')] → "AAABBC"
536        let segments = [(3u32, b'A'), (2u32, b'B'), (1u32, b'C')];
537        let packed = pack_rle_segments(&segments).expect("Fix: pack must succeed");
538        let decoded = rle_decode_cpu(&packed);
539        assert_eq!(decoded, b"AAABBC".to_vec());
540    }
541
542    #[test]
543    fn end_to_end_decode_handles_long_run() {
544        // 1000 copies of 0x42 in a single segment.
545        let segments = [(1000u32, 0x42u8)];
546        let packed = pack_rle_segments(&segments).expect("Fix: pack must succeed");
547        let decoded = rle_decode_cpu(&packed);
548        assert_eq!(decoded.len(), 1000);
549        assert!(decoded.iter().all(|&b| b == 0x42));
550    }
551
552    #[test]
553    fn end_to_end_decode_handles_alternating_short_runs() {
554        // 256 alternating (1, 0xAA), (1, 0xBB) segments → 256 bytes.
555        let mut segments = Vec::with_capacity(256);
556        for i in 0..256 {
557            segments.push((1u32, if i % 2 == 0 { 0xAAu8 } else { 0xBBu8 }));
558        }
559        let packed = pack_rle_segments(&segments).expect("Fix: pack must succeed");
560        let decoded = rle_decode_cpu(&packed);
561        assert_eq!(decoded.len(), 256);
562        for (i, byte) in decoded.iter().enumerate() {
563            let expected = if i % 2 == 0 { 0xAA } else { 0xBB };
564            assert_eq!(*byte, expected);
565        }
566    }
567
568    #[test]
569    fn end_to_end_decode_handles_empty_input() {
570        let decoded = rle_decode_cpu(&[]);
571        assert!(decoded.is_empty());
572    }
573
574    #[test]
575    fn end_to_end_decode_handles_zero_length_segments_as_skips() {
576        let segments = [(2u32, b'A'), (0u32, b'X'), (3u32, b'B')];
577        let packed = pack_rle_segments(&segments).expect("Fix: pack must succeed");
578        let decoded = rle_decode_cpu(&packed);
579        assert_eq!(decoded, b"AABBB".to_vec());
580    }
581
582    #[test]
583    fn pack_into_reuses_existing_capacity() {
584        let segments = [(2u32, b'A'), (4u32, b'B')];
585        let mut out = Vec::with_capacity(64);
586        let before = out.capacity();
587        pack_rle_segments_into(&segments, &mut out).expect("Fix: pack_into must succeed");
588        assert_eq!(out.len(), 2);
589        assert_eq!(
590            out.capacity(),
591            before,
592            "pack_into must reuse caller-owned capacity"
593        );
594    }
595
596    #[test]
597    fn pack_into_truncates_stale_tail_without_reallocating() {
598        let segments = [(2u32, b'A'), (4u32, b'B')];
599        let mut out = Vec::with_capacity(64);
600        out.extend([0xFFFF_FFFF; 16]);
601        let ptr = out.as_ptr();
602
603        try_pack_rle_segments_into(&segments, &mut out).expect("Fix: pack_into must succeed");
604
605        assert_eq!(
606            out,
607            vec![(2u32 << 8) | u32::from(b'A'), (4u32 << 8) | u32::from(b'B')]
608        );
609        assert_eq!(out.as_ptr(), ptr);
610    }
611
612    #[test]
613    fn cpu_unpack_into_reuses_existing_capacity() {
614        let segments = [(2u32, b'A'), (4u32, b'B')];
615        let packed = pack_rle_segments(&segments).expect("Fix: pack must succeed");
616        let mut lengths = Vec::with_capacity(64);
617        let mut values = Vec::with_capacity(64);
618        let lengths_capacity = lengths.capacity();
619        let values_capacity = values.capacity();
620
621        rle_segment_lengths_cpu_into(&packed, &mut lengths, &mut values);
622
623        assert_eq!(lengths, vec![2, 4]);
624        assert_eq!(values, vec![u32::from(b'A'), u32::from(b'B')]);
625        assert_eq!(lengths.capacity(), lengths_capacity);
626        assert_eq!(values.capacity(), values_capacity);
627    }
628
629    #[test]
630    fn cpu_unpack_into_truncates_stale_tail_without_reallocating() {
631        let segments = [(2u32, b'A'), (4u32, b'B')];
632        let packed = pack_rle_segments(&segments).expect("Fix: pack must succeed");
633        let mut lengths = Vec::with_capacity(64);
634        let mut values = Vec::with_capacity(64);
635        lengths.extend([99u32; 16]);
636        values.extend([99u32; 16]);
637        let lengths_ptr = lengths.as_ptr();
638        let values_ptr = values.as_ptr();
639
640        try_rle_segment_lengths_cpu_into(&packed, &mut lengths, &mut values).unwrap();
641
642        assert_eq!(lengths, vec![2, 4]);
643        assert_eq!(values, vec![u32::from(b'A'), u32::from(b'B')]);
644        assert_eq!(lengths.as_ptr(), lengths_ptr);
645        assert_eq!(values.as_ptr(), values_ptr);
646    }
647
648    #[test]
649    fn start_offsets_into_reuses_existing_capacity() {
650        let mut offsets = Vec::with_capacity(64);
651        let capacity = offsets.capacity();
652        let total = rle_segment_start_offsets_cpu_into(&[2, 0, 4], &mut offsets);
653
654        assert_eq!(offsets, vec![0, 2, 2]);
655        assert_eq!(total, 6);
656        assert_eq!(offsets.capacity(), capacity);
657    }
658
659    #[test]
660    fn start_offsets_into_truncates_stale_tail_without_reallocating() {
661        let mut offsets = Vec::with_capacity(64);
662        offsets.extend([99u32; 16]);
663        let ptr = offsets.as_ptr();
664
665        let total = try_rle_segment_start_offsets_cpu_into(&[2, 0, 4], &mut offsets).unwrap();
666
667        assert_eq!(offsets, vec![0, 2, 2]);
668        assert_eq!(total, 6);
669        assert_eq!(offsets.as_ptr(), ptr);
670    }
671
672    #[test]
673    fn decode_into_reuses_existing_capacity_without_intermediate_vectors() {
674        let segments = [(2u32, b'A'), (0u32, b'X'), (3u32, b'B')];
675        let packed = pack_rle_segments(&segments).expect("Fix: pack must succeed");
676        let mut decoded = Vec::with_capacity(64);
677        let capacity = decoded.capacity();
678
679        rle_decode_cpu_into(&packed, &mut decoded);
680
681        assert_eq!(decoded, b"AABBB".to_vec());
682        assert_eq!(decoded.capacity(), capacity);
683    }
684
685    #[test]
686    fn decode_into_truncates_stale_tail_without_reallocating() {
687        let segments = [(2u32, b'A'), (0u32, b'X'), (3u32, b'B')];
688        let packed = pack_rle_segments(&segments).expect("Fix: pack must succeed");
689        let mut decoded = Vec::with_capacity(64);
690        decoded.extend([0xFFu8; 16]);
691        let ptr = decoded.as_ptr();
692
693        try_rle_decode_cpu_into(&packed, &mut decoded).unwrap();
694
695        assert_eq!(decoded, b"AABBB".to_vec());
696        assert_eq!(decoded.as_ptr(), ptr);
697    }
698
699    #[test]
700    fn generated_pack_unpack_offsets_and_decode_match_independent_reference() {
701        let mut state = 0xA17E_D15C_u32;
702        for case in 0..4096u32 {
703            state = state.wrapping_mul(1_103_515_245).wrapping_add(12_345);
704            let count = match case {
705                0 => 0,
706                1 => 1,
707                2 => 256,
708                3 => 257,
709                4 => 1025,
710                _ => state % 4097,
711            } as usize;
712            let mut segments = Vec::with_capacity(count);
713            for idx in 0..count {
714                state = state.rotate_left(5) ^ (idx as u32).wrapping_mul(0x9E37_79B9);
715                segments.push((state % 9, state.rotate_right(8) as u8));
716            }
717            let packed = pack_rle_segments(&segments).unwrap();
718            let mut lengths = Vec::with_capacity(count + 3);
719            let mut values = Vec::with_capacity(count + 3);
720            let mut offsets = Vec::with_capacity(count + 3);
721            let mut decoded = Vec::with_capacity(
722                segments.iter().map(|(len, _)| *len as usize).sum::<usize>() + 3,
723            );
724
725            try_rle_segment_lengths_cpu_into(&packed, &mut lengths, &mut values).unwrap();
726            let total = try_rle_segment_start_offsets_cpu_into(&lengths, &mut offsets).unwrap();
727            try_rle_decode_cpu_into(&packed, &mut decoded).unwrap();
728
729            let expected_lengths: Vec<u32> = segments.iter().map(|(len, _)| *len).collect();
730            let expected_values: Vec<u32> = segments
731                .iter()
732                .map(|(_, value)| u32::from(*value))
733                .collect();
734            let mut expected_offsets = Vec::with_capacity(count);
735            let mut expected_total = 0u32;
736            let mut expected_decoded = Vec::new();
737            for &(len, value) in &segments {
738                expected_offsets.push(expected_total);
739                expected_total = expected_total.saturating_add(len);
740                expected_decoded.extend(std::iter::repeat_n(value, len as usize));
741            }
742
743            assert_eq!(lengths, expected_lengths, "case {case}");
744            assert_eq!(values, expected_values, "case {case}");
745            assert_eq!(offsets, expected_offsets, "case {case}");
746            assert_eq!(total, expected_total, "case {case}");
747            assert_eq!(decoded, expected_decoded, "case {case}");
748        }
749    }
750
751    #[test]
752    fn build_program_returns_well_formed_program() {
753        let program = rle_segment_lengths(8);
754        assert_eq!(
755            program.buffers().len(),
756            3,
757            "segments_in + lengths_out + values_out"
758        );
759        assert_eq!(program.workgroup_size(), RLE_SEGMENT_LENGTHS_WORKGROUP_SIZE);
760    }
761
762    #[test]
763    fn dispatch_grid_packs_segment_lanes_into_workgroups() {
764        assert_eq!(rle_segment_lengths_dispatch_grid(0), [1, 1, 1]);
765        assert_eq!(rle_segment_lengths_dispatch_grid(1), [1, 1, 1]);
766        assert_eq!(rle_segment_lengths_dispatch_grid(256), [1, 1, 1]);
767        assert_eq!(rle_segment_lengths_dispatch_grid(257), [2, 1, 1]);
768        assert_eq!(rle_segment_lengths_dispatch_grid(1025), [5, 1, 1]);
769    }
770
771    #[test]
772    fn zero_segment_count_traps() {
773        let program = rle_segment_lengths(0);
774        assert!(program.stats().trap());
775    }
776
777    #[test]
778    fn build_program_is_deterministic_across_calls() {
779        let p1 = rle_segment_lengths(32);
780        let p2 = rle_segment_lengths(32);
781        assert_eq!(p1.buffers().len(), p2.buffers().len());
782        assert_eq!(p1.workgroup_size(), p2.workgroup_size());
783    }
784
785    #[test]
786    fn op_id_is_canonical_and_stable() {
787        assert_eq!(OP_ID, "vyre-primitives::decode::rle_segment_lengths");
788    }
789
790    #[test]
791    fn binding_indices_are_canonical_and_stable() {
792        assert_eq!(BINDING_SEGMENTS_IN, 0);
793        assert_eq!(BINDING_SEGMENT_LENGTHS_OUT, 1);
794        assert_eq!(BINDING_SEGMENT_VALUES_OUT, 2);
795    }
796
797    #[test]
798    fn max_segment_length_is_canonical_24_bit_field_max() {
799        assert_eq!(MAX_SEGMENT_LENGTH, (1u32 << 24) - 1);
800        assert_eq!(MAX_SEGMENT_VALUE, 0xFF);
801    }
802}
803
804#[cfg(test)]
805mod non_panicking_wrapper_tests {
806    use super::*;
807
808    #[test]
809    fn compatibility_wrappers_match_fallible_references() {
810        let packed = pack_rle_segments(&[(3, b'a'), (0, b'b'), (2, b'c')])
811            .expect("Fix: unit-test oracle precondition - valid RLE headers must pack");
812
813        let mut lengths = Vec::new();
814        let mut values = Vec::new();
815        try_rle_segment_lengths_cpu_into(&packed, &mut lengths, &mut values)
816            .expect("Fix: unit-test oracle precondition - fallible length/value oracle must accept valid packed input");
817        assert_eq!(
818            rle_segment_lengths_cpu(&packed),
819            (lengths.clone(), values.clone())
820        );
821
822        lengths.fill(u32::MAX);
823        values.fill(u32::MAX);
824        rle_segment_lengths_cpu_into(&packed, &mut lengths, &mut values);
825        assert_eq!(lengths, vec![3, 0, 2]);
826        assert_eq!(
827            values,
828            vec![u32::from(b'a'), u32::from(b'b'), u32::from(b'c')]
829        );
830
831        let mut offsets = Vec::new();
832        let total = try_rle_segment_start_offsets_cpu_into(&lengths, &mut offsets).expect(
833            "Fix: unit-test oracle precondition - fallible offset oracle must accept valid lengths",
834        );
835        assert_eq!(
836            rle_segment_start_offsets_cpu(&lengths),
837            (offsets.clone(), total)
838        );
839
840        offsets.fill(u32::MAX);
841        let total = rle_segment_start_offsets_cpu_into(&lengths, &mut offsets);
842        assert_eq!(offsets, vec![0, 3, 3]);
843        assert_eq!(total, 5);
844
845        let mut decoded = Vec::new();
846        try_rle_decode_cpu_into(&packed, &mut decoded)
847            .expect("Fix: unit-test oracle precondition - fallible decode oracle must accept valid packed input");
848        assert_eq!(rle_decode_cpu(&packed), decoded);
849
850        decoded.fill(0);
851        rle_decode_cpu_into(&packed, &mut decoded);
852        assert_eq!(decoded, b"aaacc");
853    }
854}