1use std::alloc::{Layout, alloc_zeroed, dealloc, handle_alloc_error};
4use std::marker::PhantomData;
5use std::ptr::NonNull;
6use std::sync::Arc;
7
8use crate::{
9 ErrorCategory, ErrorCode, FormatDescriptor, Metadata, MetadataSchema, PixelFlowError, Result,
10 SampleType,
11};
12
13pub trait Sample: sealed::Sealed + Copy + 'static {
15 const SAMPLE_TYPE: SampleType;
17}
18
19mod sealed {
20 pub trait Sealed {}
21
22 impl Sealed for u8 {}
23 impl Sealed for u16 {}
24 impl Sealed for f32 {}
25}
26
27impl Sample for u8 {
28 const SAMPLE_TYPE: SampleType = SampleType::U8;
29}
30
31impl Sample for u16 {
32 const SAMPLE_TYPE: SampleType = SampleType::U16;
33}
34
35impl Sample for f32 {
36 const SAMPLE_TYPE: SampleType = SampleType::F32;
37}
38
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub struct AllocatorConfig {
42 alignment: usize,
43}
44
45impl AllocatorConfig {
46 #[must_use]
48 pub const fn new(alignment: usize) -> Self {
49 Self { alignment }
50 }
51
52 #[must_use]
54 pub const fn actual_alignment(self) -> usize {
55 let requested = if self.alignment < 64 {
56 64
57 } else {
58 self.alignment
59 };
60
61 if requested.is_power_of_two() {
62 requested
63 } else {
64 requested.next_power_of_two()
65 }
66 }
67}
68
69impl Default for AllocatorConfig {
70 fn default() -> Self {
71 Self { alignment: 64 }
72 }
73}
74
75#[derive(Debug)]
76struct AlignedBuffer {
77 ptr: NonNull<u8>,
78 len: usize,
79 alignment: usize,
80}
81
82unsafe impl Send for AlignedBuffer {}
86unsafe impl Sync for AlignedBuffer {}
88
89impl AlignedBuffer {
90 fn new_zeroed(len: usize, config: AllocatorConfig) -> Self {
91 let alignment = config.actual_alignment();
92 let layout = Layout::from_size_align(len.max(1), alignment)
93 .expect("alignment must be non-zero power of two");
94 let ptr = unsafe { alloc_zeroed(layout) };
96 let ptr = NonNull::new(ptr).unwrap_or_else(|| handle_alloc_error(layout));
97
98 Self {
99 ptr,
100 len,
101 alignment,
102 }
103 }
104
105 const fn as_ptr(&self) -> *const u8 {
106 self.ptr.as_ptr()
107 }
108
109 const fn as_mut_ptr(&mut self) -> *mut u8 {
110 unsafe { self.ptr.as_mut() }
113 }
114}
115
116impl Drop for AlignedBuffer {
117 fn drop(&mut self) {
118 let layout = Layout::from_size_align(self.len.max(1), self.alignment)
119 .expect("stored allocation layout must remain valid");
120 unsafe {
122 dealloc(self.ptr.as_ptr(), layout);
123 }
124 }
125}
126
127#[derive(Clone, Debug)]
128struct PlaneStorage {
129 buffer: Arc<AlignedBuffer>,
130 sample_type: SampleType,
131}
132
133#[derive(Clone, Debug)]
134struct PlaneView {
135 storage: PlaneStorage,
136 offset_bytes: usize,
137 stride_bytes: usize,
138 width: usize,
139 height: usize,
140}
141
142#[repr(C)]
144#[derive(Clone, Copy, Debug, Eq, PartialEq)]
145pub struct RawPlane {
146 pub ptr: *const u8,
148 pub stride_bytes: usize,
150 pub width: usize,
152 pub height: usize,
154 pub sample_type: SampleType,
156}
157
158#[repr(C)]
160#[derive(Clone, Copy, Debug, Eq, PartialEq)]
161pub struct RawPlaneMut {
162 pub ptr: *mut u8,
164 pub stride_bytes: usize,
166 pub width: usize,
168 pub height: usize,
170 pub sample_type: SampleType,
172}
173
174#[derive(Clone)]
176pub struct Frame {
177 format: FormatDescriptor,
178 width: usize,
179 height: usize,
180 planes: Vec<PlaneView>,
181 metadata: Metadata,
182 alignment: usize,
183}
184
185pub struct FrameBuilder {
187 format: FormatDescriptor,
188 width: usize,
189 height: usize,
190 planes: Vec<PlaneView>,
191 metadata: Metadata,
192 alignment: usize,
193}
194
195#[derive(Debug)]
197pub struct Plane<T: Sample> {
198 view: PlaneView,
199 _sample: PhantomData<T>,
200}
201
202#[derive(Debug)]
204pub struct PlaneMut<'a, T: Sample> {
205 view: &'a mut PlaneView,
206 _sample: PhantomData<&'a mut T>,
207}
208
209pub struct PlaneRows<'a, T: Sample> {
211 plane: &'a Plane<T>,
212 next_row: usize,
213}
214
215impl FrameBuilder {
216 pub fn new(
218 format: FormatDescriptor,
219 width: usize,
220 height: usize,
221 schema: &MetadataSchema,
222 allocator: AllocatorConfig,
223 ) -> Result<Self> {
224 if width == 0 || height == 0 {
225 return Err(PixelFlowError::new(
226 ErrorCategory::Core,
227 ErrorCode::new("frame.invalid_dimensions"),
228 "frame dimensions must be non-zero",
229 ));
230 }
231
232 let alignment = allocator.actual_alignment();
233 let mut planes = Vec::with_capacity(format.planes().len());
234
235 for descriptor in format.planes() {
236 let plane_width = div_ceil(width, descriptor.width_divisor)?;
237 let plane_height = div_ceil(height, descriptor.height_divisor)?;
238 let row_bytes = plane_width
239 .checked_mul(descriptor.sample_type.bytes_per_sample())
240 .ok_or_else(|| {
241 PixelFlowError::new(
242 ErrorCategory::Core,
243 ErrorCode::new("frame.allocation_overflow"),
244 "row byte size overflowed",
245 )
246 })?;
247 let stride_bytes = align_up(row_bytes, alignment)?;
248 let buffer_len = stride_bytes.checked_mul(plane_height).ok_or_else(|| {
249 PixelFlowError::new(
250 ErrorCategory::Core,
251 ErrorCode::new("frame.allocation_overflow"),
252 "plane allocation size overflowed",
253 )
254 })?;
255
256 let storage = PlaneStorage {
257 buffer: Arc::new(AlignedBuffer::new_zeroed(buffer_len, allocator)),
258 sample_type: descriptor.sample_type,
259 };
260
261 planes.push(PlaneView {
262 storage,
263 offset_bytes: 0,
264 stride_bytes,
265 width: plane_width,
266 height: plane_height,
267 });
268 }
269
270 Ok(Self {
271 format,
272 width,
273 height,
274 planes,
275 metadata: Metadata::new(schema),
276 alignment,
277 })
278 }
279
280 #[must_use]
282 pub const fn actual_alignment(&self) -> usize {
283 self.alignment
284 }
285
286 pub fn plane_mut<T: Sample>(&mut self, index: usize) -> Result<PlaneMut<'_, T>> {
288 let view = self
289 .planes
290 .get_mut(index)
291 .ok_or_else(|| plane_index_error(index))?;
292
293 if view.storage.sample_type != T::SAMPLE_TYPE {
294 return Err(sample_mismatch_error(
295 view.storage.sample_type,
296 T::SAMPLE_TYPE,
297 ));
298 }
299
300 Ok(PlaneMut {
301 view,
302 _sample: PhantomData,
303 })
304 }
305
306 #[must_use]
308 pub fn finish(self) -> Frame {
309 Frame {
310 format: self.format,
311 width: self.width,
312 height: self.height,
313 planes: self.planes,
314 metadata: self.metadata,
315 alignment: self.alignment,
316 }
317 }
318}
319
320impl Frame {
321 #[must_use]
323 pub const fn format(&self) -> &FormatDescriptor {
324 &self.format
325 }
326
327 #[must_use]
329 pub const fn width(&self) -> usize {
330 self.width
331 }
332
333 #[must_use]
335 pub const fn height(&self) -> usize {
336 self.height
337 }
338
339 #[must_use]
341 pub const fn metadata(&self) -> &Metadata {
342 &self.metadata
343 }
344
345 #[must_use]
347 pub const fn actual_alignment(&self) -> usize {
348 self.alignment
349 }
350
351 pub fn plane<T: Sample>(&self, index: usize) -> Result<Plane<T>> {
353 let view = self
354 .planes
355 .get(index)
356 .ok_or_else(|| plane_index_error(index))?;
357
358 if view.storage.sample_type != T::SAMPLE_TYPE {
359 return Err(sample_mismatch_error(
360 view.storage.sample_type,
361 T::SAMPLE_TYPE,
362 ));
363 }
364
365 Ok(Plane {
366 view: view.clone(),
367 _sample: PhantomData,
368 })
369 }
370
371 #[must_use]
373 pub fn with_metadata(&self, metadata: Metadata) -> Self {
374 Self {
375 format: self.format.clone(),
376 width: self.width,
377 height: self.height,
378 planes: self.planes.clone(),
379 metadata,
380 alignment: self.alignment,
381 }
382 }
383
384 #[must_use]
386 pub fn shares_plane_storage(&self, other: &Self, plane_index: usize) -> bool {
387 self.shares_plane_storage_at(plane_index, other, plane_index)
388 }
389
390 #[must_use]
392 pub fn shares_plane_storage_at(
393 &self,
394 plane_index: usize,
395 other: &Self,
396 other_plane_index: usize,
397 ) -> bool {
398 match (
399 self.planes.get(plane_index),
400 other.planes.get(other_plane_index),
401 ) {
402 (Some(left), Some(right)) => Arc::ptr_eq(&left.storage.buffer, &right.storage.buffer),
403 _ => false,
404 }
405 }
406
407 pub fn view(&self, left: usize, top: usize, width: usize, height: usize) -> Result<Self> {
409 if width == 0 || height == 0 {
410 return Err(PixelFlowError::new(
411 ErrorCategory::Core,
412 ErrorCode::new("frame.invalid_view"),
413 "view dimensions must be non-zero",
414 ));
415 }
416 if left.checked_add(width).is_none_or(|r| r > self.width)
417 || top.checked_add(height).is_none_or(|b| b > self.height)
418 {
419 return Err(PixelFlowError::new(
420 ErrorCategory::Core,
421 ErrorCode::new("frame.invalid_view"),
422 "view rectangle is outside frame bounds",
423 ));
424 }
425
426 let mut planes = Vec::with_capacity(self.planes.len());
427 for (descriptor, source) in self.format.planes().iter().zip(&self.planes) {
428 let plane_left = left / descriptor.width_divisor;
429 let plane_top = top / descriptor.height_divisor;
430 let plane_width = div_ceil(width, descriptor.width_divisor)?;
431 let plane_height = div_ceil(height, descriptor.height_divisor)?;
432 let sample_offset = plane_left
433 .checked_mul(descriptor.sample_type.bytes_per_sample())
434 .ok_or_else(|| {
435 PixelFlowError::new(
436 ErrorCategory::Core,
437 ErrorCode::new("frame.offset_overflow"),
438 "plane sample offset overflowed",
439 )
440 })?;
441 let row_offset = plane_top.checked_mul(source.stride_bytes).ok_or_else(|| {
442 PixelFlowError::new(
443 ErrorCategory::Core,
444 ErrorCode::new("frame.offset_overflow"),
445 "plane row offset overflowed",
446 )
447 })?;
448 let offset_bytes = source
449 .offset_bytes
450 .checked_add(row_offset)
451 .and_then(|value| value.checked_add(sample_offset))
452 .ok_or_else(|| {
453 PixelFlowError::new(
454 ErrorCategory::Core,
455 ErrorCode::new("frame.offset_overflow"),
456 "plane view offset overflowed",
457 )
458 })?;
459
460 planes.push(PlaneView {
461 storage: source.storage.clone(),
462 offset_bytes,
463 stride_bytes: source.stride_bytes,
464 width: plane_width,
465 height: plane_height,
466 });
467 }
468
469 Ok(Self {
470 format: self.format.clone(),
471 width,
472 height,
473 planes,
474 metadata: self.metadata.clone(),
475 alignment: self.alignment,
476 })
477 }
478
479 pub fn single_plane_view(&self, plane_index: usize, format: FormatDescriptor) -> Result<Self> {
481 let view = self.plane_view(plane_index)?;
482
483 Self::from_plane_sources(
484 format,
485 view.width,
486 view.height,
487 &[(self, plane_index)],
488 self.metadata.clone(),
489 )
490 }
491
492 pub fn from_plane_sources(
494 format: FormatDescriptor,
495 width: usize,
496 height: usize,
497 sources: &[(&Self, usize)],
498 metadata: Metadata,
499 ) -> Result<Self> {
500 if width == 0 || height == 0 {
501 return Err(PixelFlowError::new(
502 ErrorCategory::Core,
503 ErrorCode::new("frame.invalid_dimensions"),
504 "frame dimensions must be non-zero",
505 ));
506 }
507 if sources.len() != format.planes().len() {
508 return Err(PixelFlowError::new(
509 ErrorCategory::Core,
510 ErrorCode::new("frame.plane_count_mismatch"),
511 format!(
512 "format '{}' requires {} planes, got {}",
513 format.name(),
514 format.planes().len(),
515 sources.len()
516 ),
517 ));
518 }
519
520 let mut planes = Vec::with_capacity(sources.len());
521 let mut alignment = usize::MAX;
522 for (descriptor, (source, plane_index)) in format.planes().iter().zip(sources.iter()) {
523 let source_view = source.plane_view(*plane_index)?.clone();
524 let expected_width = div_ceil(width, descriptor.width_divisor)?;
525 let expected_height = div_ceil(height, descriptor.height_divisor)?;
526 if source_view.width != expected_width || source_view.height != expected_height {
527 return Err(PixelFlowError::new(
528 ErrorCategory::Core,
529 ErrorCode::new("frame.plane_shape_mismatch"),
530 format!(
531 "plane role {:?} requires {}x{}, got {}x{}",
532 descriptor.role,
533 expected_width,
534 expected_height,
535 source_view.width,
536 source_view.height
537 ),
538 ));
539 }
540 if source_view.storage.sample_type != descriptor.sample_type {
541 return Err(sample_mismatch_error(
542 source_view.storage.sample_type,
543 descriptor.sample_type,
544 ));
545 }
546
547 alignment = alignment.min(source.actual_alignment());
548 planes.push(source_view);
549 }
550
551 Ok(Self {
552 format,
553 width,
554 height,
555 planes,
556 metadata,
557 alignment,
558 })
559 }
560
561 fn plane_view(&self, index: usize) -> Result<&PlaneView> {
562 self.planes
563 .get(index)
564 .ok_or_else(|| plane_index_error(index))
565 }
566
567 pub unsafe fn raw_plane(&self, index: usize) -> Result<RawPlane> {
574 let view = self
575 .planes
576 .get(index)
577 .ok_or_else(|| plane_index_error(index))?;
578
579 Ok(RawPlane {
580 ptr: view
581 .storage
582 .buffer
583 .as_ptr()
584 .wrapping_add(view.offset_bytes)
585 .cast::<u8>(),
586 stride_bytes: view.stride_bytes,
587 width: view.width,
588 height: view.height,
589 sample_type: view.storage.sample_type,
590 })
591 }
592}
593
594impl<T: Sample> Plane<T> {
595 #[must_use]
597 pub const fn width(&self) -> usize {
598 self.view.width
599 }
600
601 #[must_use]
603 pub const fn height(&self) -> usize {
604 self.view.height
605 }
606
607 #[must_use]
609 pub fn row(&self, row: usize) -> Option<&[T]> {
610 if row >= self.view.height {
611 return None;
612 }
613
614 Some(typed_row(&self.view, row))
615 }
616
617 #[must_use]
619 pub const fn rows(&self) -> PlaneRows<'_, T> {
620 PlaneRows {
621 plane: self,
622 next_row: 0,
623 }
624 }
625}
626
627impl<'a, T: Sample> PlaneMut<'a, T> {
628 #[must_use]
630 pub const fn width(&self) -> usize {
631 self.view.width
632 }
633
634 #[must_use]
636 pub const fn height(&self) -> usize {
637 self.view.height
638 }
639
640 #[must_use]
642 pub fn row_mut(&mut self, row: usize) -> Option<&mut [T]> {
643 if row >= self.view.height {
644 return None;
645 }
646
647 Some(typed_row_mut(self.view, row))
648 }
649
650 #[must_use]
652 pub fn raw_parts(&mut self) -> RawPlaneMut {
653 RawPlaneMut {
654 ptr: self.view.storage_mut_ptr(),
655 stride_bytes: self.view.stride_bytes,
656 width: self.view.width,
657 height: self.view.height,
658 sample_type: self.view.storage.sample_type,
659 }
660 }
661}
662
663impl<'a, T: Sample> Iterator for PlaneRows<'a, T> {
664 type Item = &'a [T];
665
666 fn next(&mut self) -> Option<Self::Item> {
667 let row = self.plane.row(self.next_row)?;
668 self.next_row += 1;
669 Some(row)
670 }
671}
672
673fn div_ceil(value: usize, divisor: usize) -> Result<usize> {
674 if divisor == 0 {
675 return Err(PixelFlowError::new(
676 ErrorCategory::Core,
677 ErrorCode::new("frame.invalid_divisor"),
678 "plane divisor must be non-zero",
679 ));
680 }
681
682 Ok(value.div_ceil(divisor))
683}
684
685fn align_up(value: usize, alignment: usize) -> Result<usize> {
686 let mask = alignment.checked_sub(1).ok_or_else(|| {
687 PixelFlowError::new(
688 ErrorCategory::Core,
689 ErrorCode::new("frame.invalid_alignment"),
690 "alignment underflowed",
691 )
692 })?;
693
694 value
695 .checked_add(mask)
696 .map(|sum| sum & !mask)
697 .ok_or_else(|| {
698 PixelFlowError::new(
699 ErrorCategory::Core,
700 ErrorCode::new("frame.allocation_overflow"),
701 "aligned row size overflowed",
702 )
703 })
704}
705
706fn plane_index_error(index: usize) -> PixelFlowError {
707 PixelFlowError::new(
708 ErrorCategory::Core,
709 ErrorCode::new("frame.plane_index"),
710 format!("plane index {index} is out of range"),
711 )
712}
713
714fn sample_mismatch_error(actual: SampleType, requested: SampleType) -> PixelFlowError {
715 PixelFlowError::new(
716 ErrorCategory::Format,
717 ErrorCode::new("format.sample_type_mismatch"),
718 format!(
719 "plane sample type is {:?}, requested {:?}",
720 actual, requested
721 ),
722 )
723}
724
725fn typed_row<T: Sample>(view: &PlaneView, row: usize) -> &[T] {
726 let byte_offset = view.offset_bytes + row * view.stride_bytes;
727 unsafe {
730 std::slice::from_raw_parts(
731 view.storage.buffer.as_ptr().add(byte_offset).cast::<T>(),
732 view.width,
733 )
734 }
735}
736
737fn typed_row_mut<T: Sample>(view: &mut PlaneView, row: usize) -> &mut [T] {
738 let byte_offset = view.offset_bytes + row * view.stride_bytes;
739 let buffer = Arc::get_mut(&mut view.storage.buffer)
740 .expect("builder must uniquely own plane storage before finish");
741 unsafe {
743 std::slice::from_raw_parts_mut(buffer.as_mut_ptr().add(byte_offset).cast::<T>(), view.width)
744 }
745}
746
747impl PlaneView {
748 fn storage_mut_ptr(&mut self) -> *mut u8 {
749 let buffer = Arc::get_mut(&mut self.storage.buffer)
750 .expect("builder must uniquely own plane storage before finish");
751 buffer.as_mut_ptr().wrapping_add(self.offset_bytes)
752 }
753}
754
755#[cfg(test)]
756mod tests {
757 use crate::{
758 ErrorCategory, ErrorCode, MetadataKind, MetadataSchema, MetadataValue, SampleType,
759 resolve_format_alias,
760 };
761
762 use super::{AllocatorConfig, Frame, FrameBuilder};
763
764 #[test]
765 fn frame_builder_allocates_aligned_plane_buffers() {
766 let format = resolve_format_alias("gray8").expect("format should resolve");
767 let schema = MetadataSchema::core();
768 let mut builder = FrameBuilder::new(format, 4, 3, &schema, AllocatorConfig::default())
769 .expect("builder should allocate");
770
771 assert!(builder.actual_alignment() >= 64);
772 let mut plane = builder.plane_mut::<u8>(0).expect("u8 plane should match");
773 let raw = plane.raw_parts();
774
775 assert_eq!(raw.width, 4);
776 assert_eq!(raw.height, 3);
777 assert_eq!(raw.sample_type, SampleType::U8);
778 assert_eq!((raw.ptr as usize) % builder.actual_alignment(), 0);
779 }
780
781 #[test]
782 fn builder_writes_rows_and_finish_returns_immutable_frame() {
783 let format = resolve_format_alias("gray8").expect("format should resolve");
784 let schema = MetadataSchema::core();
785 let mut builder = FrameBuilder::new(format, 3, 2, &schema, AllocatorConfig::default())
786 .expect("builder should allocate");
787
788 {
789 let mut plane = builder.plane_mut::<u8>(0).expect("u8 plane should match");
790 plane
791 .row_mut(0)
792 .expect("row 0 should exist")
793 .copy_from_slice(&[1, 2, 3]);
794 plane
795 .row_mut(1)
796 .expect("row 1 should exist")
797 .copy_from_slice(&[4, 5, 6]);
798 }
799
800 let frame = builder.finish();
801 let plane = frame.plane::<u8>(0).expect("u8 plane should match");
802
803 assert_eq!(plane.row(0).expect("row 0 should exist"), &[1, 2, 3]);
804 assert_eq!(plane.row(1).expect("row 1 should exist"), &[4, 5, 6]);
805 assert_eq!(
806 frame.metadata().get("core:matrix"),
807 Some(&MetadataValue::None)
808 );
809 }
810
811 #[test]
812 fn prop_only_clone_shares_plane_storage_but_replaces_metadata() {
813 let format = resolve_format_alias("gray8").expect("format should resolve");
814 let mut schema = MetadataSchema::core();
815 schema
816 .register_plugin_key("acme/filter:enabled", MetadataKind::Bool)
817 .expect("plugin key should register");
818
819 let mut builder = FrameBuilder::new(format, 2, 2, &schema, AllocatorConfig::default())
820 .expect("builder should allocate");
821 builder
822 .plane_mut::<u8>(0)
823 .expect("u8 plane should match")
824 .row_mut(0)
825 .expect("row should exist")
826 .copy_from_slice(&[7, 8]);
827 let frame = builder.finish();
828
829 let mut metadata = frame.metadata().clone();
830 metadata
831 .set(&schema, "acme/filter:enabled", MetadataValue::Bool(true))
832 .expect("metadata write should pass");
833
834 let cloned = frame.with_metadata(metadata);
835
836 assert!(frame.shares_plane_storage(&cloned, 0));
837 assert_eq!(
838 cloned.metadata().get("acme/filter:enabled"),
839 Some(&MetadataValue::Bool(true))
840 );
841 assert_eq!(
842 cloned.plane::<u8>(0).expect("u8 plane").row(0),
843 Some(&[7, 8][..])
844 );
845 }
846
847 #[test]
848 fn crop_like_view_shares_storage_and_adjusts_visible_rows() {
849 let format = resolve_format_alias("gray8").expect("format should resolve");
850 let schema = MetadataSchema::core();
851 let mut builder = FrameBuilder::new(format, 4, 4, &schema, AllocatorConfig::default())
852 .expect("builder should allocate");
853
854 {
855 let mut plane = builder.plane_mut::<u8>(0).expect("u8 plane should match");
856 plane
857 .row_mut(0)
858 .expect("row")
859 .copy_from_slice(&[1, 2, 3, 4]);
860 plane
861 .row_mut(1)
862 .expect("row")
863 .copy_from_slice(&[5, 6, 7, 8]);
864 plane
865 .row_mut(2)
866 .expect("row")
867 .copy_from_slice(&[9, 10, 11, 12]);
868 plane
869 .row_mut(3)
870 .expect("row")
871 .copy_from_slice(&[13, 14, 15, 16]);
872 }
873
874 let frame = builder.finish();
875 let cropped = frame
876 .view(1, 1, 2, 2)
877 .expect("crop-like view should succeed");
878
879 assert!(frame.shares_plane_storage(&cropped, 0));
880 assert_eq!(cropped.width(), 2);
881 assert_eq!(cropped.height(), 2);
882
883 let plane = cropped.plane::<u8>(0).expect("u8 plane should match");
884 assert_eq!(plane.row(0), Some(&[6, 7][..]));
885 assert_eq!(plane.row(1), Some(&[10, 11][..]));
886 }
887
888 #[test]
889 fn frame_single_plane_view_shares_storage_and_preserves_metadata() {
890 let source_format = resolve_format_alias("yuv420p8").expect("format should resolve");
891 let gray_format = resolve_format_alias("gray8").expect("format should resolve");
892 let schema = MetadataSchema::core();
893 let mut builder =
894 FrameBuilder::new(source_format, 4, 4, &schema, AllocatorConfig::default())
895 .expect("builder should allocate");
896 {
897 let mut u = builder.plane_mut::<u8>(1).expect("u plane should match");
898 let row = u.row_mut(0).expect("row should exist");
899 *row.first_mut().expect("plane row should be non-empty") = 77;
900 }
901 let source = builder.finish();
902 let mut metadata = source.metadata().clone();
903 metadata
904 .set(&schema, "core:frame_number", MetadataValue::Int(9))
905 .expect("metadata should set");
906 let source = source.with_metadata(metadata);
907
908 let output = source
909 .single_plane_view(1, gray_format)
910 .expect("single plane view should build");
911
912 assert_eq!(output.width(), 2);
913 assert_eq!(output.height(), 2);
914 assert_eq!(
915 output.metadata().get("core:frame_number"),
916 Some(&MetadataValue::Int(9))
917 );
918 assert!(source.shares_plane_storage_at(1, &output, 0));
919 assert_eq!(
920 output.plane::<u8>(0).expect("u8 plane").row(0),
921 Some(&[77, 0][..])
922 );
923 }
924
925 #[test]
926 fn frame_from_plane_sources_combines_compatible_planes_without_copying() {
927 let schema = MetadataSchema::core();
928 let gray_format = resolve_format_alias("gray8").expect("format should resolve");
929 let target_format = resolve_format_alias("yuv420p8").expect("format should resolve");
930 let y = FrameBuilder::new(
931 gray_format.clone(),
932 4,
933 4,
934 &schema,
935 AllocatorConfig::default(),
936 )
937 .expect("y builder should allocate")
938 .finish();
939 let u = FrameBuilder::new(
940 gray_format.clone(),
941 2,
942 2,
943 &schema,
944 AllocatorConfig::default(),
945 )
946 .expect("u builder should allocate")
947 .finish();
948 let v = FrameBuilder::new(gray_format, 2, 2, &schema, AllocatorConfig::default())
949 .expect("v builder should allocate")
950 .finish();
951
952 let output = Frame::from_plane_sources(
953 target_format,
954 4,
955 4,
956 &[(&y, 0), (&u, 0), (&v, 0)],
957 y.metadata().clone(),
958 )
959 .expect("compatible planes should compose");
960
961 assert_eq!(output.width(), 4);
962 assert_eq!(output.height(), 4);
963 assert!(y.shares_plane_storage_at(0, &output, 0));
964 assert!(u.shares_plane_storage_at(0, &output, 1));
965 assert!(v.shares_plane_storage_at(0, &output, 2));
966 }
967
968 #[test]
969 fn frame_from_plane_sources_rejects_incompatible_shape_and_sample_type() {
970 let schema = MetadataSchema::core();
971 let gray8 = resolve_format_alias("gray8").expect("format should resolve");
972 let gray10 = resolve_format_alias("gray10").expect("format should resolve");
973 let target = resolve_format_alias("yuv420p8").expect("format should resolve");
974 let y = FrameBuilder::new(gray8.clone(), 4, 4, &schema, AllocatorConfig::default())
975 .expect("builder should allocate")
976 .finish();
977 let wrong_shape = FrameBuilder::new(gray8, 3, 2, &schema, AllocatorConfig::default())
978 .expect("builder should allocate")
979 .finish();
980 let wrong_sample = FrameBuilder::new(gray10, 2, 2, &schema, AllocatorConfig::default())
981 .expect("builder should allocate")
982 .finish();
983
984 let Err(shape_error) = Frame::from_plane_sources(
985 target.clone(),
986 4,
987 4,
988 &[(&y, 0), (&wrong_shape, 0), (&wrong_shape, 0)],
989 y.metadata().clone(),
990 ) else {
991 panic!("wrong chroma shape should fail");
992 };
993 assert_eq!(shape_error.category(), ErrorCategory::Core);
994 assert_eq!(
995 shape_error.code(),
996 ErrorCode::new("frame.plane_shape_mismatch")
997 );
998
999 let Err(sample_error) = Frame::from_plane_sources(
1000 target,
1001 4,
1002 4,
1003 &[(&y, 0), (&wrong_sample, 0), (&wrong_sample, 0)],
1004 y.metadata().clone(),
1005 ) else {
1006 panic!("wrong sample type should fail");
1007 };
1008 assert_eq!(sample_error.category(), ErrorCategory::Format);
1009 assert_eq!(
1010 sample_error.code(),
1011 ErrorCode::new("format.sample_type_mismatch")
1012 );
1013 }
1014
1015 #[test]
1016 fn typed_plane_mismatch_returns_error_not_panic() {
1017 let format = resolve_format_alias("gray10").expect("format should resolve");
1018 let schema = MetadataSchema::core();
1019 let frame = FrameBuilder::new(format, 2, 2, &schema, AllocatorConfig::default())
1020 .expect("builder should allocate")
1021 .finish();
1022
1023 let error = frame
1024 .plane::<u8>(0)
1025 .expect_err("wrong typed access should fail");
1026
1027 assert_eq!(error.category(), ErrorCategory::Format);
1028 assert_eq!(error.code(), ErrorCode::new("format.sample_type_mismatch"));
1029 }
1030
1031 #[test]
1032 fn unsafe_raw_plane_access_exposes_pointer_stride_and_contract_shape() {
1033 let format = resolve_format_alias("gray8").expect("format should resolve");
1034 let schema = MetadataSchema::core();
1035 let frame = FrameBuilder::new(format, 5, 2, &schema, AllocatorConfig::default())
1036 .expect("builder should allocate")
1037 .finish();
1038
1039 let raw = unsafe { frame.raw_plane(0) }.expect("raw plane should exist");
1041
1042 assert!(!raw.ptr.is_null());
1043 assert!(raw.stride_bytes >= 5);
1044 assert_eq!(raw.width, 5);
1045 assert_eq!(raw.height, 2);
1046 assert_eq!(raw.sample_type, SampleType::U8);
1047 }
1048
1049 #[test]
1050 fn typed_plane_rows_iterate_visible_slices() {
1051 let format = resolve_format_alias("gray8").expect("format should resolve");
1052 let schema = MetadataSchema::core();
1053 let mut builder = FrameBuilder::new(format, 2, 2, &schema, AllocatorConfig::default())
1054 .expect("builder should allocate");
1055
1056 {
1057 let mut plane = builder.plane_mut::<u8>(0).expect("u8 plane should match");
1058 plane.row_mut(0).expect("row").copy_from_slice(&[1, 2]);
1059 plane.row_mut(1).expect("row").copy_from_slice(&[3, 4]);
1060 }
1061
1062 let frame = builder.finish();
1063 let plane = frame.plane::<u8>(0).expect("u8 plane should match");
1064 let rows: Vec<&[u8]> = plane.rows().collect();
1065
1066 assert_eq!(rows, vec![&[1, 2][..], &[3, 4][..]]);
1067 }
1068}