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(®ion_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}