Skip to main content

pixelflow_core/
frame.rs

1//! Immutable frames, aligned plane storage, and typed plane views.
2
3use 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
13/// Sample marker trait for supported plane sample types.
14pub trait Sample: sealed::Sealed + Copy + 'static {
15    /// Matching storage sample type.
16    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/// Runtime allocator configuration for frame plane buffers.
40#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub struct AllocatorConfig {
42    alignment: usize,
43}
44
45impl AllocatorConfig {
46    /// Creates allocator config with requested alignment.
47    #[must_use]
48    pub const fn new(alignment: usize) -> Self {
49        Self { alignment }
50    }
51
52    /// Returns actual alignment used for allocations.
53    #[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
82// SAFETY: AlignedBuffer owns a heap allocation and exposes raw pointers only through APIs that
83// preserve aliasing rules. Shared access is read-only unless unique ownership is proven with
84// Arc::get_mut, so sending or sharing between threads is safe.
85unsafe impl Send for AlignedBuffer {}
86// SAFETY: Same reasoning as Send; no interior mutability and deallocation happens once in Drop.
87unsafe 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        // SAFETY: `layout` has valid non-zero size and power-of-two alignment.
95        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        // SAFETY: `NonNull<u8>` always points to live allocation owned by `self`, and method
111        // requires `&mut self`, so returning mutable pointer does not create aliased mutable refs.
112        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        // SAFETY: buffer was allocated with exact same layout in `new_zeroed`.
121        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/// Raw immutable plane view used by expert Rust and C ABI adapters.
143#[repr(C)]
144#[derive(Clone, Copy, Debug, Eq, PartialEq)]
145pub struct RawPlane {
146    /// Plane start pointer.
147    pub ptr: *const u8,
148    /// Stride in bytes.
149    pub stride_bytes: usize,
150    /// Visible width in samples.
151    pub width: usize,
152    /// Visible height in rows.
153    pub height: usize,
154    /// Plane storage sample type.
155    pub sample_type: SampleType,
156}
157
158/// Raw mutable plane view used by expert builders and C ABI adapters.
159#[repr(C)]
160#[derive(Clone, Copy, Debug, Eq, PartialEq)]
161pub struct RawPlaneMut {
162    /// Plane start pointer.
163    pub ptr: *mut u8,
164    /// Stride in bytes.
165    pub stride_bytes: usize,
166    /// Visible width in samples.
167    pub width: usize,
168    /// Visible height in rows.
169    pub height: usize,
170    /// Plane storage sample type.
171    pub sample_type: SampleType,
172}
173
174/// Immutable ref-counted frame handle.
175#[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
185/// Builder used for explicit output allocation and writes.
186pub struct FrameBuilder {
187    format: FormatDescriptor,
188    width: usize,
189    height: usize,
190    planes: Vec<PlaneView>,
191    metadata: Metadata,
192    alignment: usize,
193}
194
195/// Typed immutable plane view.
196#[derive(Debug)]
197pub struct Plane<T: Sample> {
198    view: PlaneView,
199    _sample: PhantomData<T>,
200}
201
202/// Typed mutable plane view available while building outputs.
203#[derive(Debug)]
204pub struct PlaneMut<'a, T: Sample> {
205    view: &'a mut PlaneView,
206    _sample: PhantomData<&'a mut T>,
207}
208
209/// Iterator over immutable typed plane rows.
210pub struct PlaneRows<'a, T: Sample> {
211    plane: &'a Plane<T>,
212    next_row: usize,
213}
214
215impl FrameBuilder {
216    /// Allocates explicit output buffers for a frame.
217    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    /// Returns actual allocation alignment.
281    #[must_use]
282    pub const fn actual_alignment(&self) -> usize {
283        self.alignment
284    }
285
286    /// Returns typed mutable plane view.
287    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    /// Finishes builder and returns immutable frame handle.
307    #[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    /// Returns frame format descriptor.
322    #[must_use]
323    pub const fn format(&self) -> &FormatDescriptor {
324        &self.format
325    }
326
327    /// Returns frame width.
328    #[must_use]
329    pub const fn width(&self) -> usize {
330        self.width
331    }
332
333    /// Returns frame height.
334    #[must_use]
335    pub const fn height(&self) -> usize {
336        self.height
337    }
338
339    /// Returns frame metadata.
340    #[must_use]
341    pub const fn metadata(&self) -> &Metadata {
342        &self.metadata
343    }
344
345    /// Returns actual alignment used for this frame allocation.
346    #[must_use]
347    pub const fn actual_alignment(&self) -> usize {
348        self.alignment
349    }
350
351    /// Returns typed immutable plane view.
352    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    /// Creates frame clone that shares all plane buffers and replaces metadata.
372    #[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    /// Returns true when both frames share same backing storage for a plane index.
385    #[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    /// Returns true when selected planes share the same backing storage.
391    #[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    /// Creates zero-copy crop-like view over parent plane storage.
408    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    /// Creates a zero-copy frame containing one source plane as plane 0 of `format`.
480    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    /// Creates a zero-copy frame from existing plane storage.
493    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    /// Returns raw pointer and stride view for expert Rust and ABI adapters.
568    ///
569    /// # Safety
570    /// Caller must honor plane bounds from `height` and `stride_bytes`, and must interpret sample
571    /// values using returned `sample_type`. Pointer remains valid only while frame storage remains
572    /// alive through this frame or another shared owner.
573    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    /// Returns visible plane width in samples.
596    #[must_use]
597    pub const fn width(&self) -> usize {
598        self.view.width
599    }
600
601    /// Returns visible plane height in rows.
602    #[must_use]
603    pub const fn height(&self) -> usize {
604        self.view.height
605    }
606
607    /// Returns immutable row slice by row index.
608    #[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    /// Iterates over visible row slices.
618    #[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    /// Returns visible plane width in samples.
629    #[must_use]
630    pub const fn width(&self) -> usize {
631        self.view.width
632    }
633
634    /// Returns visible plane height in rows.
635    #[must_use]
636    pub const fn height(&self) -> usize {
637        self.view.height
638    }
639
640    /// Returns mutable row slice by row index.
641    #[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    /// Returns raw mutable pointer and plane shape for expert code.
651    #[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    // SAFETY: offsets and strides are calculated from buffer bounds at build/view time; pointer
728    // alignment is at least allocator alignment and storage sample type matches T in caller checks.
729    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    // SAFETY: mutable row access only exists on FrameBuilder where storage is uniquely owned.
742    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        // SAFETY: test inspects metadata only and does not dereference raw pointer.
1040        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}