Skip to main content

nnrp_core/
data.rs

1use crate::{
2    CacheObjectKind, CommonHeader, ErrorMetadata, ErrorScope, MessageType, NnrpError,
3    TypedPayloadDescriptor, CACHE_ERROR_MISS, TYPED_PAYLOAD_DESCRIPTOR_LEN,
4};
5
6pub const FRAME_SUBMIT_METADATA_LEN: usize = 72;
7pub const RESULT_PUSH_METADATA_LEN: usize = 64;
8pub const BODY_REGION_PRELUDE_LEN: usize = 32;
9pub const OBJECT_REFERENCE_BLOCK_LEN: usize = 24;
10
11pub const BUDGET_POLICY_KNOWN_MASK: u8 = 0x0f;
12pub const RESULT_FLAGS_KNOWN_MASK: u16 = 0x0007;
13pub const PAYLOAD_KIND_KNOWN_MASK: u32 = 0x0000_007f;
14pub const SUBMIT_OBJECT_REF_MASK_KNOWN_BITS: u32 = 0x0000_000f;
15pub const STANDARD_PROFILE_UNSPECIFIED: u16 = 0x0000;
16pub const STANDARD_PROFILE_TENSOR: u16 = 0x0001;
17pub const STANDARD_PROFILE_TOKEN: u16 = 0x0002;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[repr(u8)]
21pub enum InputProfile {
22    Unspecified = 0,
23    ChangedTilesLuma = 1,
24    DenseLumaFrame = 2,
25}
26
27impl InputProfile {
28    pub fn try_from_u8(value: u8) -> Result<Self, NnrpError> {
29        match value {
30            0 => Ok(Self::Unspecified),
31            1 => Ok(Self::ChangedTilesLuma),
32            2 => Ok(Self::DenseLumaFrame),
33            _ => Err(NnrpError::UnknownEnumValue {
34                enum_name: "input_profile",
35                value: value as u64,
36            }),
37        }
38    }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42#[repr(u8)]
43pub enum TileIndexMode {
44    DenseRange = 0,
45    RawU16 = 1,
46    DeltaU16 = 2,
47    Bitset = 3,
48}
49
50impl TileIndexMode {
51    pub fn try_from_u8(value: u8) -> Result<Self, NnrpError> {
52        match value {
53            0 => Ok(Self::DenseRange),
54            1 => Ok(Self::RawU16),
55            2 => Ok(Self::DeltaU16),
56            3 => Ok(Self::Bitset),
57            _ => Err(NnrpError::UnknownEnumValue {
58                enum_name: "tile_index_mode",
59                value: value as u64,
60            }),
61        }
62    }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66#[repr(u8)]
67pub enum SubmitMode {
68    Inline = 0,
69    Reference = 1,
70    Mixed = 2,
71}
72
73impl SubmitMode {
74    pub fn try_from_u8(value: u8) -> Result<Self, NnrpError> {
75        match value {
76            0 => Ok(Self::Inline),
77            1 => Ok(Self::Reference),
78            2 => Ok(Self::Mixed),
79            _ => Err(NnrpError::UnknownEnumValue {
80                enum_name: "submit_mode",
81                value: value as u64,
82            }),
83        }
84    }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88#[repr(u8)]
89pub enum ResultClass {
90    Complete = 0,
91    Partial = 1,
92    StaleReuse = 2,
93    Degraded = 3,
94}
95
96impl ResultClass {
97    pub fn try_from_u8(value: u8) -> Result<Self, NnrpError> {
98        match value {
99            0 => Ok(Self::Complete),
100            1 => Ok(Self::Partial),
101            2 => Ok(Self::StaleReuse),
102            3 => Ok(Self::Degraded),
103            _ => Err(NnrpError::UnknownEnumValue {
104                enum_name: "result_class",
105                value: value as u64,
106            }),
107        }
108    }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub struct PayloadKindBitmap(pub u32);
113
114impl PayloadKindBitmap {
115    pub const TENSOR: u32 = 0x0000_0001;
116    pub const TOKEN_CHUNK: u32 = 0x0000_0002;
117    pub const AUDIO_CHUNK: u32 = 0x0000_0004;
118    pub const VIDEO_CHUNK: u32 = 0x0000_0008;
119    pub const STRUCTURED_EVENT: u32 = 0x0000_0010;
120    pub const TOOL_DELTA: u32 = 0x0000_0020;
121    pub const OPAQUE_BYTES: u32 = 0x0000_0040;
122
123    pub fn validate(self) -> Result<(), NnrpError> {
124        validate_mask_u32(self.0, PAYLOAD_KIND_KNOWN_MASK)
125    }
126
127    pub fn contains_tensor(self) -> bool {
128        self.0 & Self::TENSOR != 0
129    }
130
131    pub fn contains(self, family: PayloadFamily) -> bool {
132        self.0 & family.bit() != 0
133    }
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137#[repr(u32)]
138pub enum PayloadFamily {
139    Tensor = PayloadKindBitmap::TENSOR,
140    TokenChunk = PayloadKindBitmap::TOKEN_CHUNK,
141    AudioChunk = PayloadKindBitmap::AUDIO_CHUNK,
142    VideoChunk = PayloadKindBitmap::VIDEO_CHUNK,
143    StructuredEvent = PayloadKindBitmap::STRUCTURED_EVENT,
144    ToolDelta = PayloadKindBitmap::TOOL_DELTA,
145    OpaqueBytes = PayloadKindBitmap::OPAQUE_BYTES,
146}
147
148impl PayloadFamily {
149    pub fn try_from_bit(bit: u32) -> Result<Self, NnrpError> {
150        match bit {
151            PayloadKindBitmap::TENSOR => Ok(Self::Tensor),
152            PayloadKindBitmap::TOKEN_CHUNK => Ok(Self::TokenChunk),
153            PayloadKindBitmap::AUDIO_CHUNK => Ok(Self::AudioChunk),
154            PayloadKindBitmap::VIDEO_CHUNK => Ok(Self::VideoChunk),
155            PayloadKindBitmap::STRUCTURED_EVENT => Ok(Self::StructuredEvent),
156            PayloadKindBitmap::TOOL_DELTA => Ok(Self::ToolDelta),
157            PayloadKindBitmap::OPAQUE_BYTES => Ok(Self::OpaqueBytes),
158            _ => Err(NnrpError::UnknownEnumValue {
159                enum_name: "payload_family_bit",
160                value: bit as u64,
161            }),
162        }
163    }
164
165    pub fn bit(self) -> u32 {
166        self as u32
167    }
168
169    pub fn is_standard_profile(self) -> bool {
170        matches!(self, Self::Tensor | Self::TokenChunk)
171    }
172
173    pub fn is_registry_bound_family(self) -> bool {
174        matches!(
175            self,
176            Self::AudioChunk
177                | Self::VideoChunk
178                | Self::StructuredEvent
179                | Self::ToolDelta
180                | Self::OpaqueBytes
181        )
182    }
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186pub struct FrameSubmitMetadata {
187    pub src_width: u16,
188    pub src_height: u16,
189    pub tile_width: u16,
190    pub tile_height: u16,
191    pub tile_count: u16,
192    pub section_count: u16,
193    pub frame_class: u8,
194    pub input_profile: InputProfile,
195    pub tile_index_mode: TileIndexMode,
196    pub latency_budget_ms: u16,
197    pub target_fps_x100: u16,
198    pub retry_of_frame: u32,
199    pub tile_base_id: u32,
200    pub camera_bytes: u32,
201    pub tile_index_bytes: u32,
202    pub operation_id: u64,
203    pub submit_mode: SubmitMode,
204    pub budget_policy: u8,
205    pub loss_tolerance_policy: u8,
206    pub object_ref_mask: u32,
207    pub dependency_frame_id: u32,
208    pub payload_kind_bitmap: PayloadKindBitmap,
209    pub payload_frame_count: u16,
210}
211
212impl FrameSubmitMetadata {
213    pub fn parse(source: &[u8]) -> Result<Self, NnrpError> {
214        require_len(source, FRAME_SUBMIT_METADATA_LEN)?;
215        validate_zero_u8("frame_submit.reserved0", source[15])?;
216        validate_zero_u32("frame_submit.reserved1", read_u32(source, 36))?;
217        validate_zero_u32("frame_submit.reserved2", read_u32(source, 48))?;
218        validate_zero_u8("frame_submit.reserved3", source[55])?;
219        validate_zero_u16("frame_submit.reserved4", read_u16(source, 70))?;
220
221        let budget_policy = source[53];
222        validate_mask_u8(budget_policy, BUDGET_POLICY_KNOWN_MASK)?;
223        let payload_kind_bitmap = PayloadKindBitmap(read_u32(source, 64));
224        payload_kind_bitmap.validate()?;
225
226        let metadata = Self {
227            src_width: read_u16(source, 0),
228            src_height: read_u16(source, 2),
229            tile_width: read_u16(source, 4),
230            tile_height: read_u16(source, 6),
231            tile_count: read_u16(source, 8),
232            section_count: read_u16(source, 10),
233            frame_class: source[12],
234            input_profile: InputProfile::try_from_u8(source[13])?,
235            tile_index_mode: TileIndexMode::try_from_u8(source[14])?,
236            latency_budget_ms: read_u16(source, 16),
237            target_fps_x100: read_u16(source, 18),
238            retry_of_frame: read_u32(source, 20),
239            tile_base_id: read_u32(source, 24),
240            camera_bytes: read_u32(source, 28),
241            tile_index_bytes: read_u32(source, 32),
242            operation_id: read_u64(source, 40),
243            submit_mode: SubmitMode::try_from_u8(source[52])?,
244            budget_policy,
245            loss_tolerance_policy: source[54],
246            object_ref_mask: read_u32(source, 56),
247            dependency_frame_id: read_u32(source, 60),
248            payload_kind_bitmap,
249            payload_frame_count: read_u16(source, 68),
250        };
251        metadata.validate_payload_shape()?;
252        Ok(metadata)
253    }
254
255    pub fn write(&self, destination: &mut [u8]) -> Result<(), NnrpError> {
256        require_destination_len(destination, FRAME_SUBMIT_METADATA_LEN)?;
257        validate_mask_u8(self.budget_policy, BUDGET_POLICY_KNOWN_MASK)?;
258        self.payload_kind_bitmap.validate()?;
259        self.validate_payload_shape()?;
260
261        destination[..FRAME_SUBMIT_METADATA_LEN].fill(0);
262        write_u16(destination, 0, self.src_width);
263        write_u16(destination, 2, self.src_height);
264        write_u16(destination, 4, self.tile_width);
265        write_u16(destination, 6, self.tile_height);
266        write_u16(destination, 8, self.tile_count);
267        write_u16(destination, 10, self.section_count);
268        destination[12] = self.frame_class;
269        destination[13] = self.input_profile as u8;
270        destination[14] = self.tile_index_mode as u8;
271        write_u16(destination, 16, self.latency_budget_ms);
272        write_u16(destination, 18, self.target_fps_x100);
273        write_u32(destination, 20, self.retry_of_frame);
274        write_u32(destination, 24, self.tile_base_id);
275        write_u32(destination, 28, self.camera_bytes);
276        write_u32(destination, 32, self.tile_index_bytes);
277        write_u64(destination, 40, self.operation_id);
278        destination[52] = self.submit_mode as u8;
279        destination[53] = self.budget_policy;
280        destination[54] = self.loss_tolerance_policy;
281        write_u32(destination, 56, self.object_ref_mask);
282        write_u32(destination, 60, self.dependency_frame_id);
283        write_u32(destination, 64, self.payload_kind_bitmap.0);
284        write_u16(destination, 68, self.payload_frame_count);
285        Ok(())
286    }
287
288    pub fn to_bytes(&self) -> Result<[u8; FRAME_SUBMIT_METADATA_LEN], NnrpError> {
289        let mut bytes = [0u8; FRAME_SUBMIT_METADATA_LEN];
290        self.write(&mut bytes)?;
291        Ok(bytes)
292    }
293
294    pub fn validate_payload_shape(&self) -> Result<(), NnrpError> {
295        if self.operation_id == 0 {
296            return Err(NnrpError::InvalidProtocolCombination {
297                rule: "FRAME_SUBMIT operation_id must not be zero",
298            });
299        }
300        if self.payload_kind_bitmap.contains_tensor() {
301            return Ok(());
302        }
303
304        if self.src_width != 0
305            || self.src_height != 0
306            || self.tile_width != 0
307            || self.tile_height != 0
308            || self.tile_count != 0
309            || self.section_count != 0
310            || self.tile_base_id != 0
311            || self.camera_bytes != 0
312            || self.tile_index_bytes != 0
313            || self.input_profile != InputProfile::Unspecified
314        {
315            return Err(NnrpError::InvalidProtocolCombination {
316                rule: "non-tensor FRAME_SUBMIT must clear tensor tile fields",
317            });
318        }
319
320        Ok(())
321    }
322}
323
324#[derive(Debug, Clone, Copy, PartialEq, Eq)]
325pub struct ResultPushMetadata {
326    pub status_code: u16,
327    pub result_flags: u16,
328    pub section_count: u16,
329    pub tile_count: u16,
330    pub active_profile_id: u16,
331    pub inference_ms: u16,
332    pub queue_ms: u16,
333    pub server_total_ms: u16,
334    pub tile_base_id: u32,
335    pub tile_index_bytes: u32,
336    pub result_class: ResultClass,
337    pub applied_budget_policy: u8,
338    pub reused_frame_id: u32,
339    pub covered_tile_count: u16,
340    pub dropped_tile_count: u16,
341    pub payload_kind_bitmap: PayloadKindBitmap,
342    pub payload_frame_count: u16,
343}
344
345impl ResultPushMetadata {
346    pub fn parse(source: &[u8]) -> Result<Self, NnrpError> {
347        require_len(source, RESULT_PUSH_METADATA_LEN)?;
348        validate_zero_u16("result_push.reserved0", read_u16(source, 10))?;
349        validate_zero_u16("result_push.reserved1", read_u16(source, 18))?;
350        validate_zero_u64("result_push.reserved2", read_u64(source, 28))?;
351        validate_zero_u64("result_push.reserved3", read_u64(source, 36))?;
352        validate_zero_u16("result_push.reserved4", read_u16(source, 46))?;
353        validate_zero_u16("result_push.reserved5", read_u16(source, 62))?;
354
355        let result_flags = read_u16(source, 2);
356        validate_mask_u16(result_flags, RESULT_FLAGS_KNOWN_MASK)?;
357        let applied_budget_policy = source[45];
358        validate_mask_u8(applied_budget_policy, BUDGET_POLICY_KNOWN_MASK)?;
359        let payload_kind_bitmap = PayloadKindBitmap(read_u32(source, 56));
360        payload_kind_bitmap.validate()?;
361
362        let metadata = Self {
363            status_code: read_u16(source, 0),
364            result_flags,
365            section_count: read_u16(source, 4),
366            tile_count: read_u16(source, 6),
367            active_profile_id: read_u16(source, 8),
368            inference_ms: read_u16(source, 12),
369            queue_ms: read_u16(source, 14),
370            server_total_ms: read_u16(source, 16),
371            tile_base_id: read_u32(source, 20),
372            tile_index_bytes: read_u32(source, 24),
373            result_class: ResultClass::try_from_u8(source[44])?,
374            applied_budget_policy,
375            reused_frame_id: read_u32(source, 48),
376            covered_tile_count: read_u16(source, 52),
377            dropped_tile_count: read_u16(source, 54),
378            payload_kind_bitmap,
379            payload_frame_count: read_u16(source, 60),
380        };
381        metadata.validate_payload_shape()?;
382        Ok(metadata)
383    }
384
385    pub fn write(&self, destination: &mut [u8]) -> Result<(), NnrpError> {
386        require_destination_len(destination, RESULT_PUSH_METADATA_LEN)?;
387        validate_mask_u16(self.result_flags, RESULT_FLAGS_KNOWN_MASK)?;
388        validate_mask_u8(self.applied_budget_policy, BUDGET_POLICY_KNOWN_MASK)?;
389        self.payload_kind_bitmap.validate()?;
390        self.validate_payload_shape()?;
391
392        destination[..RESULT_PUSH_METADATA_LEN].fill(0);
393        write_u16(destination, 0, self.status_code);
394        write_u16(destination, 2, self.result_flags);
395        write_u16(destination, 4, self.section_count);
396        write_u16(destination, 6, self.tile_count);
397        write_u16(destination, 8, self.active_profile_id);
398        write_u16(destination, 12, self.inference_ms);
399        write_u16(destination, 14, self.queue_ms);
400        write_u16(destination, 16, self.server_total_ms);
401        write_u32(destination, 20, self.tile_base_id);
402        write_u32(destination, 24, self.tile_index_bytes);
403        destination[44] = self.result_class as u8;
404        destination[45] = self.applied_budget_policy;
405        write_u32(destination, 48, self.reused_frame_id);
406        write_u16(destination, 52, self.covered_tile_count);
407        write_u16(destination, 54, self.dropped_tile_count);
408        write_u32(destination, 56, self.payload_kind_bitmap.0);
409        write_u16(destination, 60, self.payload_frame_count);
410        Ok(())
411    }
412
413    pub fn to_bytes(&self) -> Result<[u8; RESULT_PUSH_METADATA_LEN], NnrpError> {
414        let mut bytes = [0u8; RESULT_PUSH_METADATA_LEN];
415        self.write(&mut bytes)?;
416        Ok(bytes)
417    }
418
419    pub fn validate_payload_shape(&self) -> Result<(), NnrpError> {
420        if self.payload_kind_bitmap.contains_tensor() {
421            return Ok(());
422        }
423
424        if self.section_count != 0
425            || self.tile_count != 0
426            || self.tile_base_id != 0
427            || self.tile_index_bytes != 0
428            || self.covered_tile_count != 0
429            || self.dropped_tile_count != 0
430        {
431            return Err(NnrpError::InvalidProtocolCombination {
432                rule: "non-tensor RESULT_PUSH must clear tensor coverage fields",
433            });
434        }
435
436        Ok(())
437    }
438}
439
440#[derive(Debug, Clone, Copy, PartialEq, Eq)]
441pub struct BodyRegionPrelude {
442    pub inline_object_bytes: u32,
443    pub object_reference_bytes: u32,
444    pub typed_payload_descriptor_bytes: u32,
445    pub typed_payload_frame_bytes: u32,
446    pub extension_descriptor_bytes: u32,
447    pub extension_payload_bytes: u32,
448}
449
450impl BodyRegionPrelude {
451    pub fn parse(source: &[u8]) -> Result<Self, NnrpError> {
452        require_len(source, BODY_REGION_PRELUDE_LEN)?;
453        validate_zero_u32("body_region_prelude.body_flags", read_u32(source, 24))?;
454        validate_zero_u32("body_region_prelude.reserved", read_u32(source, 28))?;
455
456        let prelude = Self {
457            inline_object_bytes: read_u32(source, 0),
458            object_reference_bytes: read_u32(source, 4),
459            typed_payload_descriptor_bytes: read_u32(source, 8),
460            typed_payload_frame_bytes: read_u32(source, 12),
461            extension_descriptor_bytes: read_u32(source, 16),
462            extension_payload_bytes: read_u32(source, 20),
463        };
464        prelude.validate_alignment()?;
465        Ok(prelude)
466    }
467
468    pub fn write(&self, destination: &mut [u8]) -> Result<(), NnrpError> {
469        require_destination_len(destination, BODY_REGION_PRELUDE_LEN)?;
470        self.validate_alignment()?;
471
472        destination[..BODY_REGION_PRELUDE_LEN].fill(0);
473        write_u32(destination, 0, self.inline_object_bytes);
474        write_u32(destination, 4, self.object_reference_bytes);
475        write_u32(destination, 8, self.typed_payload_descriptor_bytes);
476        write_u32(destination, 12, self.typed_payload_frame_bytes);
477        write_u32(destination, 16, self.extension_descriptor_bytes);
478        write_u32(destination, 20, self.extension_payload_bytes);
479        Ok(())
480    }
481
482    pub fn to_bytes(&self) -> Result<[u8; BODY_REGION_PRELUDE_LEN], NnrpError> {
483        let mut bytes = [0u8; BODY_REGION_PRELUDE_LEN];
484        self.write(&mut bytes)?;
485        Ok(bytes)
486    }
487
488    pub fn total_region_bytes(&self) -> Result<u32, NnrpError> {
489        [
490            self.inline_object_bytes,
491            self.object_reference_bytes,
492            self.typed_payload_descriptor_bytes,
493            self.typed_payload_frame_bytes,
494            self.extension_descriptor_bytes,
495            self.extension_payload_bytes,
496        ]
497        .into_iter()
498        .try_fold(0u32, |sum, value| {
499            sum.checked_add(value)
500                .ok_or(NnrpError::MessageLengthOverflow)
501        })
502    }
503
504    fn validate_alignment(&self) -> Result<(), NnrpError> {
505        if self.object_reference_bytes as usize % OBJECT_REFERENCE_BLOCK_LEN != 0 {
506            return Err(NnrpError::InvalidProtocolCombination {
507                rule: "object_reference_bytes must be a multiple of object reference block length",
508            });
509        }
510        Ok(())
511    }
512}
513
514#[derive(Debug, Clone, Copy, PartialEq, Eq)]
515pub struct ObjectReferenceBlock {
516    pub object_kind: CacheObjectKind,
517    pub ref_flags: u16,
518    pub cache_namespace: u32,
519    pub cache_key_hi: u64,
520    pub cache_key_lo: u64,
521}
522
523impl ObjectReferenceBlock {
524    pub fn parse(source: &[u8]) -> Result<Self, NnrpError> {
525        require_len(source, OBJECT_REFERENCE_BLOCK_LEN)?;
526        let ref_flags = read_u16(source, 2);
527        validate_zero_u16("object_reference.ref_flags", ref_flags)?;
528
529        Ok(Self {
530            object_kind: CacheObjectKind::try_from_u32(read_u16(source, 0) as u32)?,
531            ref_flags,
532            cache_namespace: read_u32(source, 4),
533            cache_key_hi: read_u64(source, 8),
534            cache_key_lo: read_u64(source, 16),
535        })
536    }
537
538    pub fn write(&self, destination: &mut [u8]) -> Result<(), NnrpError> {
539        require_destination_len(destination, OBJECT_REFERENCE_BLOCK_LEN)?;
540        validate_zero_u16("object_reference.ref_flags", self.ref_flags)?;
541
542        destination[..OBJECT_REFERENCE_BLOCK_LEN].fill(0);
543        write_u16(destination, 0, self.object_kind as u16);
544        write_u32(destination, 4, self.cache_namespace);
545        write_u64(destination, 8, self.cache_key_hi);
546        write_u64(destination, 16, self.cache_key_lo);
547        Ok(())
548    }
549
550    pub fn to_bytes(&self) -> Result<[u8; OBJECT_REFERENCE_BLOCK_LEN], NnrpError> {
551        let mut bytes = [0u8; OBJECT_REFERENCE_BLOCK_LEN];
552        self.write(&mut bytes)?;
553        Ok(bytes)
554    }
555
556    pub fn cache_miss_error_metadata(
557        &self,
558        related_session_id: u32,
559        related_frame_id: u32,
560        related_view_id: u32,
561        diagnostic_bytes: u32,
562    ) -> ErrorMetadata {
563        ErrorMetadata {
564            error_code: CACHE_ERROR_MISS,
565            error_scope: ErrorScope::Frame,
566            is_fatal: false,
567            retry_after_ms: 0,
568            related_session_id,
569            related_frame_id,
570            related_view_id,
571            diagnostic_bytes,
572        }
573    }
574}
575
576#[derive(Debug, Clone, PartialEq, Eq)]
577pub struct ObjectReferenceRegion {
578    blocks: Vec<ObjectReferenceBlock>,
579}
580
581impl ObjectReferenceRegion {
582    pub fn parse(source: &[u8]) -> Result<Self, NnrpError> {
583        if source.len() % OBJECT_REFERENCE_BLOCK_LEN != 0 {
584            return Err(NnrpError::InvalidProtocolCombination {
585                rule: "object reference region length must be a multiple of object reference block length",
586            });
587        }
588
589        let blocks = source
590            .chunks_exact(OBJECT_REFERENCE_BLOCK_LEN)
591            .map(ObjectReferenceBlock::parse)
592            .collect::<Result<Vec<_>, _>>()?;
593
594        Ok(Self { blocks })
595    }
596
597    pub fn from_blocks(blocks: Vec<ObjectReferenceBlock>) -> Self {
598        Self { blocks }
599    }
600
601    pub fn blocks(&self) -> &[ObjectReferenceBlock] {
602        &self.blocks
603    }
604
605    pub fn to_bytes(&self) -> Result<Vec<u8>, NnrpError> {
606        let mut bytes = vec![0u8; self.blocks.len() * OBJECT_REFERENCE_BLOCK_LEN];
607        for (index, block) in self.blocks.iter().enumerate() {
608            block.write(
609                &mut bytes
610                    [index * OBJECT_REFERENCE_BLOCK_LEN..(index + 1) * OBJECT_REFERENCE_BLOCK_LEN],
611            )?;
612        }
613        Ok(bytes)
614    }
615
616    pub fn validate_submit_mask(
617        &self,
618        submit_mode: SubmitMode,
619        object_ref_mask: u32,
620    ) -> Result<(), NnrpError> {
621        validate_submit_object_ref_mask(submit_mode, object_ref_mask)?;
622
623        let mut expected_slot = 0usize;
624        let mut seen_mask = 0u32;
625        for block in &self.blocks {
626            let Some(slot) = submit_object_slot_index(block.object_kind) else {
627                continue;
628            };
629            if slot < expected_slot {
630                return Err(NnrpError::InvalidProtocolCombination {
631                    rule: "object reference region standard slots must be sorted",
632                });
633            }
634            expected_slot = slot;
635            let bit = 1u32 << slot;
636            if seen_mask & bit != 0 {
637                return Err(NnrpError::InvalidProtocolCombination {
638                    rule: "object reference region must not duplicate standard slots",
639                });
640            }
641            if object_ref_mask & bit == 0 {
642                return Err(NnrpError::InvalidProtocolCombination {
643                    rule: "object reference block requires matching object_ref_mask bit",
644                });
645            }
646            seen_mask |= bit;
647        }
648
649        if seen_mask != object_ref_mask {
650            return Err(NnrpError::InvalidProtocolCombination {
651                rule: "object reference region must exactly match object_ref_mask",
652            });
653        }
654
655        Ok(())
656    }
657
658    pub fn validate_resolved<F>(&self, mut contains: F) -> Result<(), NnrpError>
659    where
660        F: FnMut(&ObjectReferenceBlock) -> bool,
661    {
662        for block in &self.blocks {
663            if !contains(block) {
664                return Err(NnrpError::InvalidProtocolCombination {
665                    rule: "object reference must resolve from cache",
666                });
667            }
668        }
669        Ok(())
670    }
671
672    pub fn first_unresolved<F>(&self, mut contains: F) -> Option<ObjectReferenceBlock>
673    where
674        F: FnMut(&ObjectReferenceBlock) -> bool,
675    {
676        self.blocks.iter().copied().find(|block| !contains(block))
677    }
678
679    pub fn validate_resolved_or_cache_miss<F>(
680        &self,
681        contains: F,
682        related_session_id: u32,
683        related_frame_id: u32,
684        related_view_id: u32,
685    ) -> Result<(), ErrorMetadata>
686    where
687        F: FnMut(&ObjectReferenceBlock) -> bool,
688    {
689        match self.first_unresolved(contains) {
690            Some(block) => Err(block.cache_miss_error_metadata(
691                related_session_id,
692                related_frame_id,
693                related_view_id,
694                0,
695            )),
696            None => Ok(()),
697        }
698    }
699}
700
701#[derive(Debug, Clone, Copy, PartialEq, Eq)]
702pub struct TypedPayloadFrameView<'a> {
703    pub descriptor: TypedPayloadDescriptor,
704    pub payload: &'a [u8],
705}
706
707#[derive(Debug, Clone, PartialEq, Eq)]
708pub struct TypedPayloadRegion<'a> {
709    descriptors: Vec<TypedPayloadDescriptor>,
710    payload_region: &'a [u8],
711}
712
713impl<'a> TypedPayloadRegion<'a> {
714    pub fn parse(
715        payload_kind_bitmap: PayloadKindBitmap,
716        payload_frame_count: u16,
717        descriptor_region: &[u8],
718        payload_region: &'a [u8],
719    ) -> Result<Self, NnrpError> {
720        payload_kind_bitmap.validate()?;
721        let expected_descriptor_bytes = usize::from(payload_frame_count)
722            .checked_mul(TYPED_PAYLOAD_DESCRIPTOR_LEN)
723            .ok_or(NnrpError::MessageLengthOverflow)?;
724        if descriptor_region.len() != expected_descriptor_bytes {
725            return Err(NnrpError::InvalidProtocolCombination {
726                rule: "typed payload descriptor region length must match payload_frame_count",
727            });
728        }
729
730        if payload_frame_count == 0 {
731            if !descriptor_region.is_empty() || !payload_region.is_empty() {
732                return Err(NnrpError::InvalidProtocolCombination {
733                    rule: "zero typed payload frames require empty descriptor and frame regions",
734                });
735            }
736            return Ok(Self {
737                descriptors: Vec::new(),
738                payload_region,
739            });
740        }
741
742        let descriptors = descriptor_region
743            .chunks_exact(TYPED_PAYLOAD_DESCRIPTOR_LEN)
744            .map(TypedPayloadDescriptor::parse)
745            .collect::<Result<Vec<_>, _>>()?;
746        let region = Self {
747            descriptors,
748            payload_region,
749        };
750        region.validate(payload_kind_bitmap, payload_frame_count)?;
751        Ok(region)
752    }
753
754    pub fn from_parts(
755        payload_kind_bitmap: PayloadKindBitmap,
756        descriptors: Vec<TypedPayloadDescriptor>,
757        payload_region: &'a [u8],
758    ) -> Result<Self, NnrpError> {
759        let payload_frame_count =
760            u16::try_from(descriptors.len()).map_err(|_| NnrpError::MessageLengthOverflow)?;
761        let region = Self {
762            descriptors,
763            payload_region,
764        };
765        region.validate(payload_kind_bitmap, payload_frame_count)?;
766        Ok(region)
767    }
768
769    pub fn descriptors(&self) -> &[TypedPayloadDescriptor] {
770        &self.descriptors
771    }
772
773    pub fn payload_region(&self) -> &'a [u8] {
774        self.payload_region
775    }
776
777    pub fn frame_views(&self) -> Result<Vec<TypedPayloadFrameView<'a>>, NnrpError> {
778        self.descriptors
779            .iter()
780            .map(|descriptor| {
781                let start = descriptor.offset as usize;
782                let end = checked_payload_end(descriptor)?;
783                Ok(TypedPayloadFrameView {
784                    descriptor: *descriptor,
785                    payload: &self.payload_region[start..end],
786                })
787            })
788            .collect()
789    }
790
791    pub fn descriptor_region_bytes(&self) -> Result<Vec<u8>, NnrpError> {
792        let mut bytes = vec![0u8; self.descriptors.len() * TYPED_PAYLOAD_DESCRIPTOR_LEN];
793        for (index, descriptor) in self.descriptors.iter().enumerate() {
794            descriptor.write(
795                &mut bytes[index * TYPED_PAYLOAD_DESCRIPTOR_LEN
796                    ..(index + 1) * TYPED_PAYLOAD_DESCRIPTOR_LEN],
797            )?;
798        }
799        Ok(bytes)
800    }
801
802    fn validate(
803        &self,
804        payload_kind_bitmap: PayloadKindBitmap,
805        payload_frame_count: u16,
806    ) -> Result<(), NnrpError> {
807        if self.descriptors.len() != usize::from(payload_frame_count) {
808            return Err(NnrpError::InvalidProtocolCombination {
809                rule: "typed payload descriptor count must match payload_frame_count",
810            });
811        }
812
813        let mut next_expected_offset = 0usize;
814        for descriptor in &self.descriptors {
815            validate_descriptor_profile(payload_kind_bitmap, descriptor)?;
816            if descriptor.offset as usize != next_expected_offset {
817                return Err(NnrpError::InvalidProtocolCombination {
818                    rule: "typed payload descriptors must be packed in strictly contiguous order",
819                });
820            }
821            next_expected_offset = checked_payload_end(descriptor)?;
822            if next_expected_offset > self.payload_region.len() {
823                return Err(NnrpError::InvalidProtocolCombination {
824                    rule: "typed payload descriptor range must fit the frame region",
825                });
826            }
827        }
828
829        if next_expected_offset != self.payload_region.len() {
830            return Err(NnrpError::InvalidProtocolCombination {
831                rule: "typed payload frame region must be exactly covered by descriptors",
832            });
833        }
834
835        Ok(())
836    }
837}
838
839fn validate_descriptor_profile(
840    payload_kind_bitmap: PayloadKindBitmap,
841    descriptor: &TypedPayloadDescriptor,
842) -> Result<(), NnrpError> {
843    let non_tensor_payloads = payload_kind_bitmap.0 & !PayloadKindBitmap::TENSOR;
844    if non_tensor_payloads != 0 && descriptor.profile_id == STANDARD_PROFILE_TENSOR {
845        return Err(NnrpError::InvalidProtocolCombination {
846            rule: "non-tensor typed payload frames must not use tensor profile",
847        });
848    }
849
850    if payload_kind_bitmap.0 == PayloadKindBitmap::TOKEN_CHUNK
851        && descriptor.profile_id != STANDARD_PROFILE_TOKEN
852    {
853        return Err(NnrpError::InvalidProtocolCombination {
854            rule: "token-only typed payload frames require token profile",
855        });
856    }
857
858    Ok(())
859}
860
861fn checked_payload_end(descriptor: &TypedPayloadDescriptor) -> Result<usize, NnrpError> {
862    let end = descriptor
863        .offset
864        .checked_add(descriptor.length)
865        .ok_or(NnrpError::MessageLengthOverflow)?;
866    usize::try_from(end).map_err(|_| NnrpError::MessageLengthOverflow)
867}
868
869pub fn validate_submit_object_ref_mask(
870    submit_mode: SubmitMode,
871    object_ref_mask: u32,
872) -> Result<(), NnrpError> {
873    validate_mask_u32(object_ref_mask, SUBMIT_OBJECT_REF_MASK_KNOWN_BITS)?;
874
875    match submit_mode {
876        SubmitMode::Inline => {
877            if object_ref_mask != 0 {
878                return Err(NnrpError::InvalidProtocolCombination {
879                    rule: "inline FRAME_SUBMIT must not declare object_ref_mask",
880                });
881            }
882        }
883        SubmitMode::Reference | SubmitMode::Mixed => {
884            if object_ref_mask == 0 {
885                return Err(NnrpError::InvalidProtocolCombination {
886                    rule: "reference or mixed FRAME_SUBMIT requires non-zero object_ref_mask",
887                });
888            }
889        }
890    }
891
892    Ok(())
893}
894
895fn submit_object_slot_index(object_kind: CacheObjectKind) -> Option<usize> {
896    match object_kind {
897        CacheObjectKind::CameraBlock => Some(0),
898        CacheObjectKind::TileIndexBlock => Some(1),
899        CacheObjectKind::TensorSectionTable => Some(2),
900        CacheObjectKind::PayloadLayoutTemplate => Some(3),
901        _ => None,
902    }
903}
904
905pub fn validate_result_drop_header(header: &CommonHeader) -> Result<(), NnrpError> {
906    if header.message_type != MessageType::ResultDrop
907        || header.meta_len != 0
908        || header.body_len != 0
909    {
910        return Err(NnrpError::InvalidProtocolCombination {
911            rule: "RESULT_DROP is header-only and requires meta_len=0 and body_len=0",
912        });
913    }
914    Ok(())
915}
916
917fn require_len(source: &[u8], expected: usize) -> Result<(), NnrpError> {
918    if source.len() < expected {
919        return Err(NnrpError::SourceTooShort {
920            expected,
921            actual: source.len(),
922        });
923    }
924    Ok(())
925}
926
927fn require_destination_len(destination: &[u8], expected: usize) -> Result<(), NnrpError> {
928    if destination.len() < expected {
929        return Err(NnrpError::DestinationTooShort {
930            expected,
931            actual: destination.len(),
932        });
933    }
934    Ok(())
935}
936
937fn validate_zero_u8(field: &'static str, value: u8) -> Result<(), NnrpError> {
938    if value != 0 {
939        return Err(NnrpError::NonZeroReservedField { field });
940    }
941    Ok(())
942}
943
944fn validate_zero_u16(field: &'static str, value: u16) -> Result<(), NnrpError> {
945    if value != 0 {
946        return Err(NnrpError::NonZeroReservedField { field });
947    }
948    Ok(())
949}
950
951fn validate_zero_u32(field: &'static str, value: u32) -> Result<(), NnrpError> {
952    if value != 0 {
953        return Err(NnrpError::NonZeroReservedField { field });
954    }
955    Ok(())
956}
957
958fn validate_zero_u64(field: &'static str, value: u64) -> Result<(), NnrpError> {
959    if value != 0 {
960        return Err(NnrpError::NonZeroReservedField { field });
961    }
962    Ok(())
963}
964
965fn validate_mask_u8(value: u8, allowed: u8) -> Result<(), NnrpError> {
966    if value & !allowed != 0 {
967        return Err(NnrpError::ReservedBitsSet {
968            value: value as u64,
969            allowed: allowed as u64,
970        });
971    }
972    Ok(())
973}
974
975fn validate_mask_u16(value: u16, allowed: u16) -> Result<(), NnrpError> {
976    if value & !allowed != 0 {
977        return Err(NnrpError::ReservedBitsSet {
978            value: value as u64,
979            allowed: allowed as u64,
980        });
981    }
982    Ok(())
983}
984
985fn validate_mask_u32(value: u32, allowed: u32) -> Result<(), NnrpError> {
986    if value & !allowed != 0 {
987        return Err(NnrpError::ReservedBitsSet {
988            value: value as u64,
989            allowed: allowed as u64,
990        });
991    }
992    Ok(())
993}
994
995fn read_u16(source: &[u8], offset: usize) -> u16 {
996    u16::from_le_bytes(source[offset..offset + 2].try_into().expect("slice length"))
997}
998
999fn read_u32(source: &[u8], offset: usize) -> u32 {
1000    u32::from_le_bytes(source[offset..offset + 4].try_into().expect("slice length"))
1001}
1002
1003fn read_u64(source: &[u8], offset: usize) -> u64 {
1004    u64::from_le_bytes(source[offset..offset + 8].try_into().expect("slice length"))
1005}
1006
1007fn write_u16(destination: &mut [u8], offset: usize, value: u16) {
1008    destination[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
1009}
1010
1011fn write_u32(destination: &mut [u8], offset: usize, value: u32) {
1012    destination[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
1013}
1014
1015fn write_u64(destination: &mut [u8], offset: usize, value: u64) {
1016    destination[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
1017}
1018
1019#[cfg(test)]
1020mod tests {
1021    use super::*;
1022
1023    #[test]
1024    fn frame_submit_metadata_round_trips_frozen_layout() {
1025        let metadata = FrameSubmitMetadata {
1026            src_width: 640,
1027            src_height: 360,
1028            tile_width: 32,
1029            tile_height: 32,
1030            tile_count: 84,
1031            section_count: 2,
1032            frame_class: 1,
1033            input_profile: InputProfile::DenseLumaFrame,
1034            tile_index_mode: TileIndexMode::DenseRange,
1035            latency_budget_ms: 100,
1036            target_fps_x100: 6000,
1037            retry_of_frame: 7,
1038            tile_base_id: 0,
1039            camera_bytes: 192,
1040            tile_index_bytes: 24,
1041            operation_id: 0x0102_0304_0506_0708,
1042            submit_mode: SubmitMode::Mixed,
1043            budget_policy: 0x05,
1044            loss_tolerance_policy: 0xff,
1045            object_ref_mask: 0x0000_0003,
1046            dependency_frame_id: 41,
1047            payload_kind_bitmap: PayloadKindBitmap(
1048                PayloadKindBitmap::TENSOR | PayloadKindBitmap::STRUCTURED_EVENT,
1049            ),
1050            payload_frame_count: 2,
1051        };
1052        let bytes = metadata.to_bytes().unwrap();
1053
1054        assert_eq!(bytes.len(), FRAME_SUBMIT_METADATA_LEN);
1055        assert_eq!(&bytes[32..36], &24u32.to_le_bytes());
1056        assert_eq!(&bytes[36..40], &[0; 4]);
1057        assert_eq!(&bytes[40..48], &0x0102_0304_0506_0708u64.to_le_bytes());
1058        assert_eq!(FrameSubmitMetadata::parse(&bytes).unwrap(), metadata);
1059    }
1060
1061    #[test]
1062    fn frame_submit_rejects_reserved_bytes_and_zero_operation_id() {
1063        let metadata = FrameSubmitMetadata {
1064            src_width: 0,
1065            src_height: 0,
1066            tile_width: 0,
1067            tile_height: 0,
1068            tile_count: 0,
1069            section_count: 0,
1070            frame_class: 0,
1071            input_profile: InputProfile::Unspecified,
1072            tile_index_mode: TileIndexMode::DenseRange,
1073            latency_budget_ms: 0,
1074            target_fps_x100: 0,
1075            retry_of_frame: 0,
1076            tile_base_id: 0,
1077            camera_bytes: 0,
1078            tile_index_bytes: 0,
1079            operation_id: 1,
1080            submit_mode: SubmitMode::Inline,
1081            budget_policy: 0,
1082            loss_tolerance_policy: 0,
1083            object_ref_mask: 0,
1084            dependency_frame_id: 0,
1085            payload_kind_bitmap: PayloadKindBitmap(PayloadKindBitmap::TOKEN_CHUNK),
1086            payload_frame_count: 1,
1087        };
1088        let mut bytes = metadata.to_bytes().unwrap();
1089        bytes[36] = 1;
1090        assert_eq!(
1091            FrameSubmitMetadata::parse(&bytes),
1092            Err(NnrpError::NonZeroReservedField {
1093                field: "frame_submit.reserved1",
1094            })
1095        );
1096
1097        assert_eq!(
1098            FrameSubmitMetadata {
1099                operation_id: 0,
1100                ..metadata
1101            }
1102            .to_bytes(),
1103            Err(NnrpError::InvalidProtocolCombination {
1104                rule: "FRAME_SUBMIT operation_id must not be zero"
1105            })
1106        );
1107    }
1108
1109    #[test]
1110    fn frame_submit_rejects_unknown_budget_and_non_tensor_shape() {
1111        let mut bytes = [0u8; FRAME_SUBMIT_METADATA_LEN];
1112        bytes[53] = 0x80;
1113        write_u32(&mut bytes, 64, PayloadKindBitmap::TENSOR);
1114        assert_eq!(
1115            FrameSubmitMetadata::parse(&bytes),
1116            Err(NnrpError::ReservedBitsSet {
1117                value: 0x80,
1118                allowed: BUDGET_POLICY_KNOWN_MASK as u64
1119            })
1120        );
1121
1122        let metadata = FrameSubmitMetadata {
1123            src_width: 0,
1124            src_height: 0,
1125            tile_width: 0,
1126            tile_height: 0,
1127            tile_count: 1,
1128            section_count: 0,
1129            frame_class: 0,
1130            input_profile: InputProfile::Unspecified,
1131            tile_index_mode: TileIndexMode::DenseRange,
1132            latency_budget_ms: 0,
1133            target_fps_x100: 0,
1134            retry_of_frame: 0,
1135            tile_base_id: 0,
1136            camera_bytes: 0,
1137            tile_index_bytes: 0,
1138            operation_id: 1,
1139            submit_mode: SubmitMode::Inline,
1140            budget_policy: 0,
1141            loss_tolerance_policy: 0xff,
1142            object_ref_mask: 0,
1143            dependency_frame_id: 0,
1144            payload_kind_bitmap: PayloadKindBitmap(PayloadKindBitmap::STRUCTURED_EVENT),
1145            payload_frame_count: 1,
1146        };
1147        assert_eq!(
1148            metadata.to_bytes(),
1149            Err(NnrpError::InvalidProtocolCombination {
1150                rule: "non-tensor FRAME_SUBMIT must clear tensor tile fields"
1151            })
1152        );
1153    }
1154
1155    #[test]
1156    fn result_push_metadata_round_trips_frozen_layout() {
1157        let metadata = ResultPushMetadata {
1158            status_code: 0,
1159            result_flags: 0x0004,
1160            section_count: 1,
1161            tile_count: 84,
1162            active_profile_id: 2,
1163            inference_ms: 843,
1164            queue_ms: 2,
1165            server_total_ms: 846,
1166            tile_base_id: 0,
1167            tile_index_bytes: 16,
1168            result_class: ResultClass::Partial,
1169            applied_budget_policy: 0x01,
1170            reused_frame_id: 41,
1171            covered_tile_count: 53,
1172            dropped_tile_count: 31,
1173            payload_kind_bitmap: PayloadKindBitmap(
1174                PayloadKindBitmap::TENSOR | PayloadKindBitmap::TOKEN_CHUNK,
1175            ),
1176            payload_frame_count: 3,
1177        };
1178        let bytes = metadata.to_bytes().unwrap();
1179
1180        assert_eq!(bytes.len(), RESULT_PUSH_METADATA_LEN);
1181        assert_eq!(ResultPushMetadata::parse(&bytes).unwrap(), metadata);
1182    }
1183
1184    #[test]
1185    fn result_push_rejects_unknown_payload_bits_and_non_tensor_coverage() {
1186        let mut bytes = [0u8; RESULT_PUSH_METADATA_LEN];
1187        write_u32(&mut bytes, 56, 0x8000_0000);
1188        assert_eq!(
1189            ResultPushMetadata::parse(&bytes),
1190            Err(NnrpError::ReservedBitsSet {
1191                value: 0x8000_0000,
1192                allowed: PAYLOAD_KIND_KNOWN_MASK as u64
1193            })
1194        );
1195
1196        let metadata = ResultPushMetadata {
1197            status_code: 0,
1198            result_flags: 0,
1199            section_count: 0,
1200            tile_count: 0,
1201            active_profile_id: 0,
1202            inference_ms: 0,
1203            queue_ms: 0,
1204            server_total_ms: 0,
1205            tile_base_id: 0,
1206            tile_index_bytes: 0,
1207            result_class: ResultClass::Complete,
1208            applied_budget_policy: 0,
1209            reused_frame_id: 0,
1210            covered_tile_count: 1,
1211            dropped_tile_count: 0,
1212            payload_kind_bitmap: PayloadKindBitmap(PayloadKindBitmap::TOKEN_CHUNK),
1213            payload_frame_count: 1,
1214        };
1215        assert_eq!(
1216            metadata.to_bytes(),
1217            Err(NnrpError::InvalidProtocolCombination {
1218                rule: "non-tensor RESULT_PUSH must clear tensor coverage fields"
1219            })
1220        );
1221    }
1222
1223    #[test]
1224    fn payload_family_boundary_keeps_tool_and_event_out_of_profile_space() {
1225        let bitmap = PayloadKindBitmap(
1226            PayloadKindBitmap::TOKEN_CHUNK
1227                | PayloadKindBitmap::STRUCTURED_EVENT
1228                | PayloadKindBitmap::TOOL_DELTA,
1229        );
1230
1231        assert!(bitmap.contains(PayloadFamily::TokenChunk));
1232        assert!(bitmap.contains(PayloadFamily::StructuredEvent));
1233        assert!(PayloadFamily::TokenChunk.is_standard_profile());
1234        assert!(!PayloadFamily::StructuredEvent.is_standard_profile());
1235        assert!(!PayloadFamily::ToolDelta.is_standard_profile());
1236        assert!(PayloadFamily::StructuredEvent.is_registry_bound_family());
1237        assert!(PayloadFamily::ToolDelta.is_registry_bound_family());
1238        assert_eq!(
1239            PayloadFamily::try_from_bit(PayloadKindBitmap::TOOL_DELTA),
1240            Ok(PayloadFamily::ToolDelta)
1241        );
1242        assert_eq!(
1243            PayloadFamily::try_from_bit(0x8000_0000),
1244            Err(NnrpError::UnknownEnumValue {
1245                enum_name: "payload_family_bit",
1246                value: 0x8000_0000
1247            })
1248        );
1249    }
1250
1251    #[test]
1252    fn body_region_prelude_and_object_reference_round_trip() {
1253        let prelude = BodyRegionPrelude {
1254            inline_object_bytes: 16,
1255            object_reference_bytes: OBJECT_REFERENCE_BLOCK_LEN as u32,
1256            typed_payload_descriptor_bytes: 24,
1257            typed_payload_frame_bytes: 64,
1258            extension_descriptor_bytes: 0,
1259            extension_payload_bytes: 0,
1260        };
1261        let prelude_bytes = prelude.to_bytes().unwrap();
1262
1263        assert_eq!(BodyRegionPrelude::parse(&prelude_bytes).unwrap(), prelude);
1264        assert_eq!(prelude.total_region_bytes().unwrap(), 128);
1265
1266        let object_ref = ObjectReferenceBlock {
1267            object_kind: CacheObjectKind::TileIndexBlock,
1268            ref_flags: 0,
1269            cache_namespace: 7,
1270            cache_key_hi: 0x1122_3344_5566_7788,
1271            cache_key_lo: 0x99aa_bbcc_ddee_ff00,
1272        };
1273        let object_ref_bytes = object_ref.to_bytes().unwrap();
1274
1275        assert_eq!(
1276            ObjectReferenceBlock::parse(&object_ref_bytes).unwrap(),
1277            object_ref
1278        );
1279
1280        let region = ObjectReferenceRegion::from_blocks(vec![object_ref]);
1281        let region_bytes = region.to_bytes().unwrap();
1282        let parsed_region = ObjectReferenceRegion::parse(&region_bytes).unwrap();
1283        assert_eq!(parsed_region.blocks(), &[object_ref]);
1284        parsed_region
1285            .validate_submit_mask(SubmitMode::Mixed, 1 << 1)
1286            .unwrap();
1287        parsed_region
1288            .validate_resolved(|block| block.cache_namespace == 7)
1289            .unwrap();
1290    }
1291
1292    #[test]
1293    fn body_region_prelude_rejects_reserved_and_misaligned_reference_region() {
1294        let mut bytes = [0u8; BODY_REGION_PRELUDE_LEN];
1295        write_u32(&mut bytes, 24, 1);
1296        assert_eq!(
1297            BodyRegionPrelude::parse(&bytes),
1298            Err(NnrpError::NonZeroReservedField {
1299                field: "body_region_prelude.body_flags"
1300            })
1301        );
1302
1303        let prelude = BodyRegionPrelude {
1304            inline_object_bytes: 0,
1305            object_reference_bytes: 1,
1306            typed_payload_descriptor_bytes: 0,
1307            typed_payload_frame_bytes: 0,
1308            extension_descriptor_bytes: 0,
1309            extension_payload_bytes: 0,
1310        };
1311        assert_eq!(
1312            prelude.to_bytes(),
1313            Err(NnrpError::InvalidProtocolCombination {
1314                rule: "object_reference_bytes must be a multiple of object reference block length"
1315            })
1316        );
1317    }
1318
1319    #[test]
1320    fn object_reference_region_rejects_mask_order_duplicate_and_unresolved_cases() {
1321        assert_eq!(
1322            ObjectReferenceRegion::parse(&[0u8; OBJECT_REFERENCE_BLOCK_LEN - 1]),
1323            Err(NnrpError::InvalidProtocolCombination {
1324                rule: "object reference region length must be a multiple of object reference block length"
1325            })
1326        );
1327
1328        let camera = ObjectReferenceBlock {
1329            object_kind: CacheObjectKind::CameraBlock,
1330            ref_flags: 0,
1331            cache_namespace: 1,
1332            cache_key_hi: 1,
1333            cache_key_lo: 1,
1334        };
1335        let tile = ObjectReferenceBlock {
1336            object_kind: CacheObjectKind::TileIndexBlock,
1337            ref_flags: 0,
1338            cache_namespace: 1,
1339            cache_key_hi: 2,
1340            cache_key_lo: 2,
1341        };
1342
1343        assert_eq!(
1344            validate_submit_object_ref_mask(SubmitMode::Inline, 1),
1345            Err(NnrpError::InvalidProtocolCombination {
1346                rule: "inline FRAME_SUBMIT must not declare object_ref_mask"
1347            })
1348        );
1349        assert_eq!(
1350            validate_submit_object_ref_mask(SubmitMode::Reference, 0),
1351            Err(NnrpError::InvalidProtocolCombination {
1352                rule: "reference or mixed FRAME_SUBMIT requires non-zero object_ref_mask"
1353            })
1354        );
1355
1356        let reversed = ObjectReferenceRegion::from_blocks(vec![tile, camera]);
1357        assert_eq!(
1358            reversed.validate_submit_mask(SubmitMode::Mixed, 0b0011),
1359            Err(NnrpError::InvalidProtocolCombination {
1360                rule: "object reference region standard slots must be sorted"
1361            })
1362        );
1363
1364        let duplicate = ObjectReferenceRegion::from_blocks(vec![camera, camera]);
1365        assert_eq!(
1366            duplicate.validate_submit_mask(SubmitMode::Mixed, 0b0001),
1367            Err(NnrpError::InvalidProtocolCombination {
1368                rule: "object reference region must not duplicate standard slots"
1369            })
1370        );
1371
1372        let missing_mask_bit = ObjectReferenceRegion::from_blocks(vec![camera]);
1373        assert_eq!(
1374            missing_mask_bit.validate_submit_mask(SubmitMode::Mixed, 0b0010),
1375            Err(NnrpError::InvalidProtocolCombination {
1376                rule: "object reference block requires matching object_ref_mask bit"
1377            })
1378        );
1379        assert_eq!(
1380            missing_mask_bit.validate_submit_mask(SubmitMode::Mixed, 0b0011),
1381            Err(NnrpError::InvalidProtocolCombination {
1382                rule: "object reference region must exactly match object_ref_mask"
1383            })
1384        );
1385        assert_eq!(
1386            missing_mask_bit.validate_resolved(|_| false),
1387            Err(NnrpError::InvalidProtocolCombination {
1388                rule: "object reference must resolve from cache"
1389            })
1390        );
1391
1392        assert_eq!(missing_mask_bit.first_unresolved(|_| false), Some(camera));
1393        assert_eq!(
1394            missing_mask_bit.validate_resolved_or_cache_miss(|_| false, 10, 20, 30),
1395            Err(ErrorMetadata {
1396                error_code: CACHE_ERROR_MISS,
1397                error_scope: ErrorScope::Frame,
1398                is_fatal: false,
1399                retry_after_ms: 0,
1400                related_session_id: 10,
1401                related_frame_id: 20,
1402                related_view_id: 30,
1403                diagnostic_bytes: 0,
1404            })
1405        );
1406        assert!(missing_mask_bit
1407            .validate_resolved_or_cache_miss(|_| true, 10, 20, 30)
1408            .is_ok());
1409    }
1410
1411    #[test]
1412    fn typed_payload_region_packs_descriptors_and_projects_frames() {
1413        let first = TypedPayloadDescriptor {
1414            profile_id: STANDARD_PROFILE_TOKEN,
1415            descriptor_flags: 0x0002,
1416            schema_id: 0x0000_1001,
1417            schema_version: 3,
1418            stream_semantics: 2,
1419            offset: 0,
1420            length: 2,
1421        };
1422        let second = TypedPayloadDescriptor {
1423            profile_id: STANDARD_PROFILE_TOKEN,
1424            descriptor_flags: 0x0001,
1425            schema_id: 0x0000_1001,
1426            schema_version: 3,
1427            stream_semantics: 2,
1428            offset: 2,
1429            length: 3,
1430        };
1431        let payload = b"hello";
1432        let region = TypedPayloadRegion::from_parts(
1433            PayloadKindBitmap(PayloadKindBitmap::TOKEN_CHUNK),
1434            vec![first, second],
1435            payload,
1436        )
1437        .unwrap();
1438
1439        let descriptor_bytes = region.descriptor_region_bytes().unwrap();
1440        let parsed = TypedPayloadRegion::parse(
1441            PayloadKindBitmap(PayloadKindBitmap::TOKEN_CHUNK),
1442            2,
1443            &descriptor_bytes,
1444            payload,
1445        )
1446        .unwrap();
1447        let frames = parsed.frame_views().unwrap();
1448
1449        assert_eq!(parsed.descriptors(), &[first, second]);
1450        assert_eq!(frames[0].payload, b"he");
1451        assert_eq!(frames[1].payload, b"llo");
1452    }
1453
1454    #[test]
1455    fn typed_payload_region_rejects_bad_lengths_offsets_and_profiles() {
1456        let token = TypedPayloadDescriptor {
1457            profile_id: STANDARD_PROFILE_TOKEN,
1458            descriptor_flags: 0,
1459            schema_id: 0x0000_1001,
1460            schema_version: 3,
1461            stream_semantics: 2,
1462            offset: 1,
1463            length: 2,
1464        };
1465
1466        assert_eq!(
1467            TypedPayloadRegion::parse(
1468                PayloadKindBitmap(PayloadKindBitmap::TOKEN_CHUNK),
1469                1,
1470                &[],
1471                b""
1472            ),
1473            Err(NnrpError::InvalidProtocolCombination {
1474                rule: "typed payload descriptor region length must match payload_frame_count"
1475            })
1476        );
1477        assert_eq!(
1478            TypedPayloadRegion::parse(
1479                PayloadKindBitmap(PayloadKindBitmap::TOKEN_CHUNK),
1480                0,
1481                &[],
1482                b"x"
1483            ),
1484            Err(NnrpError::InvalidProtocolCombination {
1485                rule: "zero typed payload frames require empty descriptor and frame regions"
1486            })
1487        );
1488        assert_eq!(
1489            TypedPayloadRegion::from_parts(
1490                PayloadKindBitmap(PayloadKindBitmap::TOKEN_CHUNK),
1491                vec![token],
1492                b"abc"
1493            ),
1494            Err(NnrpError::InvalidProtocolCombination {
1495                rule: "typed payload descriptors must be packed in strictly contiguous order"
1496            })
1497        );
1498
1499        let too_long = TypedPayloadDescriptor { offset: 0, ..token };
1500        assert_eq!(
1501            TypedPayloadRegion::from_parts(
1502                PayloadKindBitmap(PayloadKindBitmap::TOKEN_CHUNK),
1503                vec![too_long],
1504                b"a"
1505            ),
1506            Err(NnrpError::InvalidProtocolCombination {
1507                rule: "typed payload descriptor range must fit the frame region"
1508            })
1509        );
1510
1511        let short_cover = TypedPayloadDescriptor {
1512            length: 1,
1513            ..too_long
1514        };
1515        assert_eq!(
1516            TypedPayloadRegion::from_parts(
1517                PayloadKindBitmap(PayloadKindBitmap::TOKEN_CHUNK),
1518                vec![short_cover],
1519                b"ab"
1520            ),
1521            Err(NnrpError::InvalidProtocolCombination {
1522                rule: "typed payload frame region must be exactly covered by descriptors"
1523            })
1524        );
1525
1526        let tensor_profile = TypedPayloadDescriptor {
1527            profile_id: STANDARD_PROFILE_TENSOR,
1528            offset: 0,
1529            length: 1,
1530            ..token
1531        };
1532        assert_eq!(
1533            TypedPayloadRegion::from_parts(
1534                PayloadKindBitmap(PayloadKindBitmap::STRUCTURED_EVENT),
1535                vec![tensor_profile],
1536                b"x"
1537            ),
1538            Err(NnrpError::InvalidProtocolCombination {
1539                rule: "non-tensor typed payload frames must not use tensor profile"
1540            })
1541        );
1542        assert_eq!(
1543            TypedPayloadRegion::from_parts(
1544                PayloadKindBitmap(PayloadKindBitmap::TOKEN_CHUNK),
1545                vec![TypedPayloadDescriptor {
1546                    profile_id: STANDARD_PROFILE_UNSPECIFIED,
1547                    ..tensor_profile
1548                }],
1549                b"x"
1550            ),
1551            Err(NnrpError::InvalidProtocolCombination {
1552                rule: "token-only typed payload frames require token profile"
1553            })
1554        );
1555    }
1556
1557    #[test]
1558    fn result_drop_is_header_only() {
1559        let header = CommonHeader::new(MessageType::ResultDrop, 0, 0);
1560        validate_result_drop_header(&header).unwrap();
1561
1562        let bad = CommonHeader::new(MessageType::ResultDrop, 1, 0);
1563        assert_eq!(
1564            validate_result_drop_header(&bad),
1565            Err(NnrpError::InvalidProtocolCombination {
1566                rule: "RESULT_DROP is header-only and requires meta_len=0 and body_len=0"
1567            })
1568        );
1569    }
1570}