Skip to main content

oximedia_graph/
frame.rs

1//! Frame types for passing through the filter graph.
2//!
3//! This module provides wrapper types for video and audio frames that can be
4//! passed between nodes in the filter graph.
5
6use std::sync::Arc;
7
8use oximedia_audio::{AudioFrame, ChannelLayout};
9use oximedia_codec::VideoFrame;
10use oximedia_core::{PixelFormat, SampleFormat, Timestamp};
11
12/// A frame that can be passed through the filter graph.
13#[derive(Clone, Debug)]
14pub enum FilterFrame {
15    /// Video frame.
16    Video(VideoFrame),
17    /// Audio frame.
18    Audio(AudioFrame),
19}
20
21impl FilterFrame {
22    /// Get the timestamp of the frame.
23    #[must_use]
24    pub fn timestamp(&self) -> &Timestamp {
25        match self {
26            Self::Video(f) => &f.timestamp,
27            Self::Audio(f) => &f.timestamp,
28        }
29    }
30
31    /// Check if this is a video frame.
32    #[must_use]
33    pub fn is_video(&self) -> bool {
34        matches!(self, Self::Video(_))
35    }
36
37    /// Check if this is an audio frame.
38    #[must_use]
39    pub fn is_audio(&self) -> bool {
40        matches!(self, Self::Audio(_))
41    }
42
43    /// Get as video frame if applicable.
44    #[must_use]
45    pub fn as_video(&self) -> Option<&VideoFrame> {
46        match self {
47            Self::Video(f) => Some(f),
48            Self::Audio(_) => None,
49        }
50    }
51
52    /// Get as audio frame if applicable.
53    #[must_use]
54    pub fn as_audio(&self) -> Option<&AudioFrame> {
55        match self {
56            Self::Video(_) => None,
57            Self::Audio(f) => Some(f),
58        }
59    }
60
61    /// Get mutable video frame if applicable.
62    pub fn as_video_mut(&mut self) -> Option<&mut VideoFrame> {
63        match self {
64            Self::Video(f) => Some(f),
65            Self::Audio(_) => None,
66        }
67    }
68
69    /// Get mutable audio frame if applicable.
70    pub fn as_audio_mut(&mut self) -> Option<&mut AudioFrame> {
71        match self {
72            Self::Video(_) => None,
73            Self::Audio(f) => Some(f),
74        }
75    }
76}
77
78impl From<VideoFrame> for FilterFrame {
79    fn from(frame: VideoFrame) -> Self {
80        Self::Video(frame)
81    }
82}
83
84impl From<AudioFrame> for FilterFrame {
85    fn from(frame: AudioFrame) -> Self {
86        Self::Audio(frame)
87    }
88}
89
90/// Reference-counted frame for zero-copy passing.
91///
92/// When a frame needs to be shared between multiple consumers without copying,
93/// use `FrameRef` to wrap it in an `Arc`.
94#[derive(Clone, Debug)]
95pub struct FrameRef {
96    inner: Arc<FilterFrame>,
97}
98
99impl FrameRef {
100    /// Create a new frame reference.
101    pub fn new(frame: FilterFrame) -> Self {
102        Self {
103            inner: Arc::new(frame),
104        }
105    }
106
107    /// Get a reference to the inner frame.
108    #[must_use]
109    pub fn frame(&self) -> &FilterFrame {
110        &self.inner
111    }
112
113    /// Try to get exclusive access to the frame.
114    ///
115    /// Returns `Some` if this is the only reference, `None` otherwise.
116    pub fn try_unwrap(self) -> Option<FilterFrame> {
117        Arc::try_unwrap(self.inner).ok()
118    }
119
120    /// Get the reference count.
121    #[must_use]
122    pub fn ref_count(&self) -> usize {
123        Arc::strong_count(&self.inner)
124    }
125
126    /// Make a copy of the frame if needed for mutation.
127    ///
128    /// If this is the only reference, returns the frame directly.
129    /// Otherwise, clones the frame.
130    #[must_use]
131    pub fn make_mut(self) -> FilterFrame {
132        match Arc::try_unwrap(self.inner) {
133            Ok(frame) => frame,
134            Err(arc) => (*arc).clone(),
135        }
136    }
137}
138
139impl From<FilterFrame> for FrameRef {
140    fn from(frame: FilterFrame) -> Self {
141        Self::new(frame)
142    }
143}
144
145impl From<VideoFrame> for FrameRef {
146    fn from(frame: VideoFrame) -> Self {
147        Self::new(FilterFrame::Video(frame))
148    }
149}
150
151impl From<AudioFrame> for FrameRef {
152    fn from(frame: AudioFrame) -> Self {
153        Self::new(FilterFrame::Audio(frame))
154    }
155}
156
157// ─────────────────────────────────────────────────────────────────────────────
158// SIMD-accelerated frame copy
159// ─────────────────────────────────────────────────────────────────────────────
160
161/// Copy `src` bytes into `dst` as fast as possible.
162///
163/// On `x86_64` hosts where AVX2 is detected at runtime, the copy is dispatched
164/// through a 32-byte-chunk path that the compiler can auto-vectorize into
165/// `vmovdqu` loads/stores.  On all other targets (and for any trailing bytes)
166/// a plain `copy_from_slice` is used, which LLVM lowers to a `memcpy` and
167/// vectorizes independently.
168///
169/// # Panics
170/// Panics if `src.len() != dst.len()`.
171pub fn simd_copy_frame(src: &[u8], dst: &mut [u8]) {
172    assert_eq!(src.len(), dst.len(), "simd_copy_frame: length mismatch");
173
174    #[cfg(target_arch = "x86_64")]
175    {
176        if is_x86_feature_detected!("avx2") {
177            avx2_copy_safe(src, dst);
178            return;
179        }
180    }
181
182    dst.copy_from_slice(src);
183}
184
185/// 32-byte-chunk copy path that LLVM will lower to AVX2 `vmovdqu` on x86_64
186/// hosts where AVX2 is available.
187///
188/// The function is `#[inline(never)]` so the compiler emits a dedicated
189/// version without inlining overhead at every call site; the runtime AVX2
190/// check in [`simd_copy_frame`] guards the dispatch.
191#[cfg(target_arch = "x86_64")]
192#[inline(never)]
193fn avx2_copy_safe(src: &[u8], dst: &mut [u8]) {
194    const CHUNK: usize = 32;
195    let chunks = src.len() / CHUNK;
196    // Process 32-byte chunks.
197    for i in 0..chunks {
198        let offset = i * CHUNK;
199        dst[offset..offset + CHUNK].copy_from_slice(&src[offset..offset + CHUNK]);
200    }
201    // Handle tail bytes (0–31).
202    let done = chunks * CHUNK;
203    if done < src.len() {
204        dst[done..].copy_from_slice(&src[done..]);
205    }
206}
207
208// ─────────────────────────────────────────────────────────────────────────────
209
210/// Configuration for pre-allocating raw byte buffers in a [`FramePool`].
211///
212/// Use [`FramePool::with_config`] to construct a pool that eagerly allocates
213/// `pre_allocate` buffers of `frame_bytes` bytes each before the first call to
214/// [`FramePool::acquire_raw`].
215#[derive(Debug, Clone)]
216pub struct FramePoolConfig {
217    /// Number of raw byte buffers to allocate at construction time.
218    pub pre_allocate: usize,
219    /// Hard cap on the total number of pooled raw buffers.
220    pub max_size: usize,
221    /// Size in bytes of each raw buffer (`width * height * channels`).
222    pub frame_bytes: usize,
223}
224
225impl Default for FramePoolConfig {
226    fn default() -> Self {
227        Self {
228            pre_allocate: 0,
229            max_size: 32,
230            frame_bytes: 0,
231        }
232    }
233}
234
235/// Frame pool for reusing frame allocations.
236///
237/// Supports both typed [`VideoFrame`]/[`AudioFrame`] recycling and a raw
238/// byte-buffer free-list that can be pre-allocated via [`FramePoolConfig`].
239pub struct FramePool {
240    /// Maximum number of frames to keep in the pool.
241    capacity: usize,
242    /// Pooled video frames.
243    video_frames: Vec<VideoFrame>,
244    /// Pooled audio frames.
245    audio_frames: Vec<AudioFrame>,
246    /// Pre-allocated / recycled raw byte buffers.
247    free_list: Vec<Vec<u8>>,
248    /// Hard cap on the `free_list`.
249    raw_max: usize,
250    /// Expected byte length for raw buffers (0 = unchecked).
251    raw_frame_bytes: usize,
252}
253
254impl FramePool {
255    /// Create a new frame pool with the given capacity for typed frames.
256    #[must_use]
257    pub fn new(capacity: usize) -> Self {
258        Self {
259            capacity,
260            video_frames: Vec::with_capacity(capacity),
261            audio_frames: Vec::with_capacity(capacity),
262            free_list: Vec::new(),
263            raw_max: capacity,
264            raw_frame_bytes: 0,
265        }
266    }
267
268    /// Create a pool pre-populated according to `config`.
269    ///
270    /// `config.pre_allocate` zeroed buffers of `config.frame_bytes` bytes are
271    /// placed in the internal free-list immediately, so the first
272    /// `pre_allocate` calls to [`Self::acquire_raw`] return without any heap
273    /// allocation.
274    #[must_use]
275    pub fn with_config(config: FramePoolConfig) -> Self {
276        let pre = config.pre_allocate.min(config.max_size);
277        let mut free_list = Vec::with_capacity(pre.max(config.max_size));
278        for _ in 0..pre {
279            free_list.push(vec![0u8; config.frame_bytes]);
280        }
281        Self {
282            capacity: config.max_size,
283            video_frames: Vec::new(),
284            audio_frames: Vec::new(),
285            free_list,
286            raw_max: config.max_size,
287            raw_frame_bytes: config.frame_bytes,
288        }
289    }
290
291    /// Acquire a raw byte buffer from the free-list, or allocate a new one.
292    ///
293    /// If `frame_bytes` was set in the config, the returned buffer is exactly
294    /// that size.  Otherwise a zero-length buffer is returned for newly
295    /// allocated entries — callers should resize as needed.
296    pub fn acquire_raw(&mut self) -> Vec<u8> {
297        self.free_list.pop().unwrap_or_else(|| {
298            if self.raw_frame_bytes > 0 {
299                vec![0u8; self.raw_frame_bytes]
300            } else {
301                Vec::new()
302            }
303        })
304    }
305
306    /// Return a raw byte buffer to the free-list for reuse.
307    ///
308    /// Silently drops the buffer when the free-list is at capacity.
309    pub fn release_raw(&mut self, buf: Vec<u8>) {
310        if self.free_list.len() < self.raw_max {
311            self.free_list.push(buf);
312        }
313    }
314
315    /// Number of raw buffers currently available in the free-list.
316    #[must_use]
317    pub fn pre_allocated_count(&self) -> usize {
318        self.free_list.len()
319    }
320
321    /// Get a video frame from the pool or create a new one.
322    #[must_use]
323    pub fn get_video_frame(&mut self, format: PixelFormat, width: u32, height: u32) -> VideoFrame {
324        // Try to find a matching frame in the pool
325        if let Some(pos) = self
326            .video_frames
327            .iter()
328            .position(|f| f.format == format && f.width == width && f.height == height)
329        {
330            return self.video_frames.swap_remove(pos);
331        }
332
333        // Create a new frame
334        let mut frame = VideoFrame::new(format, width, height);
335        frame.allocate();
336        frame
337    }
338
339    /// Return a video frame to the pool.
340    pub fn return_video_frame(&mut self, frame: VideoFrame) {
341        if self.video_frames.len() < self.capacity {
342            self.video_frames.push(frame);
343        }
344    }
345
346    /// Get an audio frame from the pool or create a new one.
347    #[must_use]
348    pub fn get_audio_frame(
349        &mut self,
350        format: SampleFormat,
351        sample_rate: u32,
352        channels: ChannelLayout,
353    ) -> AudioFrame {
354        // Try to find a matching frame in the pool
355        if let Some(pos) = self.audio_frames.iter().position(|f| {
356            f.format == format && f.sample_rate == sample_rate && f.channels == channels
357        }) {
358            return self.audio_frames.swap_remove(pos);
359        }
360
361        // Create a new frame
362        AudioFrame::new(format, sample_rate, channels)
363    }
364
365    /// Return an audio frame to the pool.
366    pub fn return_audio_frame(&mut self, frame: AudioFrame) {
367        if self.audio_frames.len() < self.capacity {
368            self.audio_frames.push(frame);
369        }
370    }
371
372    /// Clear all pooled frames.
373    pub fn clear(&mut self) {
374        self.video_frames.clear();
375        self.audio_frames.clear();
376    }
377
378    /// Get the number of video frames in the pool.
379    #[must_use]
380    pub fn video_frame_count(&self) -> usize {
381        self.video_frames.len()
382    }
383
384    /// Get the number of audio frames in the pool.
385    #[must_use]
386    pub fn audio_frame_count(&self) -> usize {
387        self.audio_frames.len()
388    }
389}
390
391impl Default for FramePool {
392    fn default() -> Self {
393        Self::new(16)
394    }
395}
396
397// ─────────────────────────────────────────────────────────────────────────────
398// Zero-copy frame passing
399// ─────────────────────────────────────────────────────────────────────────────
400
401/// A raw-bytes frame that may be shared (zero-copy) or exclusively owned.
402///
403/// Adjacent nodes that produce and consume the same format can pass a
404/// [`SharedFrame::Shared`] variant through the graph; the receiving node reads
405/// the bytes via [`SharedFrame::as_bytes`] without any memcpy.  When a node
406/// *must* mutate the payload it calls [`SharedFrame::into_owned`], which clones
407/// the bytes only when there is more than one live `Arc` reference.
408#[derive(Clone, Debug)]
409pub enum SharedFrame {
410    /// Exclusively-owned byte buffer.
411    Owned(Vec<u8>),
412    /// Reference-counted (shared) byte buffer — zero-copy on the read path.
413    Shared(Arc<Vec<u8>>),
414}
415
416impl SharedFrame {
417    /// Borrow the underlying bytes regardless of ownership variant.
418    #[must_use]
419    pub fn as_bytes(&self) -> &[u8] {
420        match self {
421            Self::Owned(v) => v.as_slice(),
422            Self::Shared(arc) => arc.as_slice(),
423        }
424    }
425
426    /// Consume into an exclusively-owned `Vec<u8>`.
427    ///
428    /// * `Owned` — free, no allocation.
429    /// * `Shared` with a unique reference — moves the inner `Vec` out of the
430    ///   `Arc` without copying.
431    /// * `Shared` with multiple references — clones the bytes once.
432    #[must_use]
433    pub fn into_owned(self) -> Vec<u8> {
434        match self {
435            Self::Owned(v) => v,
436            Self::Shared(arc) => match Arc::try_unwrap(arc) {
437                Ok(v) => v,
438                Err(arc) => (*arc).clone(),
439            },
440        }
441    }
442
443    /// Return an `Arc<Vec<u8>>` that points at the same allocation as `self`.
444    ///
445    /// * `Shared` — clones the `Arc` (O(1), no heap allocation).
446    /// * `Owned` — wraps the buffer in a new `Arc` (one allocation, O(1)).
447    #[must_use]
448    pub fn try_share(&self) -> Arc<Vec<u8>> {
449        match self {
450            Self::Owned(v) => Arc::new(v.clone()),
451            Self::Shared(arc) => Arc::clone(arc),
452        }
453    }
454
455    /// Promote an `Owned` frame to `Shared`, consuming `self`.
456    ///
457    /// If already `Shared`, returns `self` unchanged.
458    #[must_use]
459    pub fn promote(self) -> Self {
460        match self {
461            Self::Owned(v) => Self::Shared(Arc::new(v)),
462            already_shared => already_shared,
463        }
464    }
465
466    /// Returns `true` if this is the `Shared` variant.
467    #[must_use]
468    pub fn is_shared(&self) -> bool {
469        matches!(self, Self::Shared(_))
470    }
471
472    /// Returns `true` if this is the `Owned` variant.
473    #[must_use]
474    pub fn is_owned(&self) -> bool {
475        matches!(self, Self::Owned(_))
476    }
477}
478
479impl From<Vec<u8>> for SharedFrame {
480    fn from(v: Vec<u8>) -> Self {
481        Self::Owned(v)
482    }
483}
484
485impl From<Arc<Vec<u8>>> for SharedFrame {
486    fn from(arc: Arc<Vec<u8>>) -> Self {
487        Self::Shared(arc)
488    }
489}
490
491/// Trait for nodes that can participate in the zero-copy frame-passing protocol.
492///
493/// Nodes advertise compatibility via [`ZeroCopyPort::accepts_zero_copy`].  When
494/// two adjacent nodes are both compatible, the graph executor may pass a
495/// [`SharedFrame::Shared`] variant directly instead of copying bytes.
496pub trait ZeroCopyPort {
497    /// Return `true` when this port can receive a [`SharedFrame::Shared`]
498    /// without requiring an exclusive copy.
499    fn accepts_zero_copy(&self) -> bool;
500
501    /// Pass a frame through this port.
502    ///
503    /// A compatible implementation should return the frame as-is (or promote
504    /// it to `Shared`) when [`Self::accepts_zero_copy`] is `true`.  Otherwise
505    /// it may convert to an `Owned` copy.
506    fn pass_frame(&self, frame: SharedFrame) -> SharedFrame;
507}
508
509// ─────────────────────────────────────────────────────────────────────────────
510
511#[cfg(test)]
512mod tests {
513    use super::*;
514
515    #[test]
516    fn test_filter_frame_video() {
517        let video = VideoFrame::new(PixelFormat::Yuv420p, 1920, 1080);
518        let frame = FilterFrame::Video(video);
519
520        assert!(frame.is_video());
521        assert!(!frame.is_audio());
522        assert!(frame.as_video().is_some());
523        assert!(frame.as_audio().is_none());
524    }
525
526    #[test]
527    fn test_filter_frame_audio() {
528        let audio = AudioFrame::new(SampleFormat::F32, 48000, ChannelLayout::Stereo);
529        let frame = FilterFrame::Audio(audio);
530
531        assert!(!frame.is_video());
532        assert!(frame.is_audio());
533        assert!(frame.as_video().is_none());
534        assert!(frame.as_audio().is_some());
535    }
536
537    #[test]
538    fn test_frame_ref() {
539        let video = VideoFrame::new(PixelFormat::Yuv420p, 1920, 1080);
540        let frame = FilterFrame::Video(video);
541        let frame_ref = FrameRef::new(frame);
542
543        assert_eq!(frame_ref.ref_count(), 1);
544
545        let frame_ref2 = frame_ref.clone();
546        assert_eq!(frame_ref.ref_count(), 2);
547        assert_eq!(frame_ref2.ref_count(), 2);
548    }
549
550    #[test]
551    fn test_frame_ref_try_unwrap() {
552        let video = VideoFrame::new(PixelFormat::Yuv420p, 1920, 1080);
553        let frame = FilterFrame::Video(video);
554        let frame_ref = FrameRef::new(frame);
555
556        // Should succeed with single reference
557        let unwrapped = frame_ref.try_unwrap();
558        assert!(unwrapped.is_some());
559    }
560
561    #[test]
562    fn test_frame_ref_make_mut() {
563        let video = VideoFrame::new(PixelFormat::Yuv420p, 1920, 1080);
564        let frame = FilterFrame::Video(video);
565        let frame_ref = FrameRef::new(frame);
566        let frame_ref2 = frame_ref.clone();
567
568        // Should clone since there are multiple references
569        let owned = frame_ref.make_mut();
570        assert!(owned.is_video());
571
572        // frame_ref2 should still be valid
573        assert!(frame_ref2.frame().is_video());
574    }
575
576    #[test]
577    fn test_frame_pool() {
578        let mut pool = FramePool::new(4);
579
580        // Get a new frame
581        let frame = pool.get_video_frame(PixelFormat::Yuv420p, 1920, 1080);
582        assert_eq!(frame.width, 1920);
583        assert_eq!(frame.height, 1080);
584
585        // Return it to the pool
586        pool.return_video_frame(frame);
587        assert_eq!(pool.video_frame_count(), 1);
588
589        // Get it back (should be the same allocation)
590        let frame2 = pool.get_video_frame(PixelFormat::Yuv420p, 1920, 1080);
591        assert_eq!(frame2.width, 1920);
592        assert_eq!(pool.video_frame_count(), 0);
593    }
594
595    #[test]
596    fn test_filter_frame_from() {
597        let video = VideoFrame::new(PixelFormat::Yuv420p, 1920, 1080);
598        let frame: FilterFrame = video.into();
599        assert!(frame.is_video());
600
601        let audio = AudioFrame::new(SampleFormat::F32, 48000, ChannelLayout::Stereo);
602        let frame: FilterFrame = audio.into();
603        assert!(frame.is_audio());
604    }
605
606    #[test]
607    fn test_frame_timestamp() {
608        use oximedia_core::Rational;
609
610        let mut video = VideoFrame::new(PixelFormat::Yuv420p, 1920, 1080);
611        video.timestamp = Timestamp::new(1000, Rational::new(1, 1000));
612        let frame = FilterFrame::Video(video);
613
614        assert_eq!(frame.timestamp().pts, 1000);
615    }
616
617    // ── simd_copy_frame ───────────────────────────────────────────────────────
618
619    #[test]
620    fn test_simd_copy_correctness() {
621        // 4096-byte buffer — covers 128 × 32-byte AVX2 chunks.
622        let src: Vec<u8> = (0u32..4096).map(|i| (i % 251) as u8).collect();
623        let mut dst = vec![0u8; 4096];
624        super::simd_copy_frame(&src, &mut dst);
625        assert_eq!(src, dst, "4096-byte copy must be byte-perfect");
626    }
627
628    #[test]
629    fn test_simd_copy_non_aligned() {
630        // 4097 bytes — 128 full AVX2 chunks plus 1 tail byte.
631        let src: Vec<u8> = (0u32..4097).map(|i| (i % 197) as u8).collect();
632        let mut dst = vec![0u8; 4097];
633        super::simd_copy_frame(&src, &mut dst);
634        assert_eq!(src, dst, "4097-byte copy (tail) must be byte-perfect");
635    }
636
637    // ── FramePoolConfig / pre-allocation ─────────────────────────────────────
638
639    #[test]
640    fn test_pool_pre_allocation_count() {
641        let config = FramePoolConfig {
642            pre_allocate: 5,
643            max_size: 10,
644            frame_bytes: 64,
645        };
646        let pool = FramePool::with_config(config);
647        assert_eq!(
648            pool.pre_allocated_count(),
649            5,
650            "pool must expose 5 pre-allocated buffers before any acquire"
651        );
652    }
653
654    #[test]
655    fn test_pool_pre_allocation_acquire() {
656        let config = FramePoolConfig {
657            pre_allocate: 5,
658            max_size: 10,
659            frame_bytes: 64,
660        };
661        let mut pool = FramePool::with_config(config);
662
663        // Drain all 5 pre-allocated buffers — each must be 64 bytes.
664        for i in 0..5 {
665            let buf = pool.acquire_raw();
666            assert_eq!(buf.len(), 64, "pre-allocated buffer {i} must be 64 bytes");
667        }
668        assert_eq!(
669            pool.pre_allocated_count(),
670            0,
671            "free-list should be empty now"
672        );
673
674        // 6th acquire must still succeed (dynamic allocation, same size).
675        let buf6 = pool.acquire_raw();
676        assert_eq!(buf6.len(), 64, "6th (dynamic) buffer must also be 64 bytes");
677    }
678
679    // ── SharedFrame / ZeroCopyPort ───────────────────────────────────────────
680
681    #[test]
682    fn test_shared_frame_zero_copy_count() {
683        let data: Vec<u8> = vec![1, 2, 3, 4];
684        let arc = Arc::new(data);
685        let frame = SharedFrame::Shared(Arc::clone(&arc));
686
687        // The shared clone created by try_share should bring the count to 3
688        // (arc + frame's inner arc + the one returned by try_share).
689        let shared = frame.try_share();
690        // arc, frame's inner arc, and `shared` all point to the same allocation.
691        assert_eq!(
692            Arc::strong_count(&shared),
693            3,
694            "strong_count must be 3 after arc + frame + try_share clone"
695        );
696    }
697
698    #[test]
699    fn test_shared_frame_into_owned_clone() {
700        let data: Vec<u8> = vec![10, 20, 30];
701        let arc = Arc::new(data.clone());
702        // Create a second reference so try_unwrap will fail and clone.
703        let _second_ref = Arc::clone(&arc);
704        let frame = SharedFrame::Shared(arc);
705
706        let owned = frame.into_owned();
707        assert_eq!(owned, data, "cloned bytes must match the original");
708    }
709
710    #[test]
711    fn test_owned_frame_to_shared() {
712        let data: Vec<u8> = vec![7, 8, 9];
713        let frame = SharedFrame::Owned(data.clone());
714
715        assert!(frame.is_owned());
716
717        let promoted = frame.promote();
718        assert!(
719            promoted.is_shared(),
720            "Owned frame must become Shared after promote()"
721        );
722
723        // Bytes are preserved.
724        assert_eq!(promoted.as_bytes(), data.as_slice());
725    }
726}