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