Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "v_frame"
version = "0.6.0"
rust-version = "1.85.0"
rust-version = "1.95.0"
description = "Video Frame data structures, originally part of rav1e"
categories = ["multimedia::video"]
license = "BSD-2-Clause"
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ A Rust library providing efficient data structures and utilities for handling YU
- **Flexible plane structure**: Efficient memory layout with configurable padding for SIMD operations
- **Multiple chroma formats**: Support for YUV 4:2:0, 4:2:2, 4:4:4, and monochrome
- **Builder pattern API**: Safe and ergonomic frame construction with compile-time guarantees
- **SIMD-friendly alignment**: non-empty planes are aligned to at least 64 bytes
on most targets, 8 bytes on non-WASI `wasm32`, or `align_of::<T>()` if larger
- **SIMD-friendly alignment**: Plane data is aligned to at least 64 bytes on most targets, 8 bytes on non-WASI `wasm32`, or `align_of::<T>()` if larger
- **WebAssembly support**: Works in both browser (`wasm32-unknown-unknown`) and WASI environments
- **Zero-copy iterators**: Efficient row-based and pixel-based iteration without allocations

Expand Down Expand Up @@ -170,7 +169,7 @@ larger.

## Requirements

- Rust 1.85.0 or later
- Rust 1.95.0 or later
- For WebAssembly: `wasm-bindgen` is automatically included for `wasm32-unknown-unknown` target

## Documentation
Expand Down
2 changes: 1 addition & 1 deletion src/chroma.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ impl ChromaSubsampling {
let ss_y = subsample.1.get() as usize;

// Check if the division is exact (no remainder)
(luma_width % ss_x == 0 && luma_height % ss_y == 0)
(luma_width.is_multiple_of(ss_x) && luma_height.is_multiple_of(ss_y))
.then(|| (luma_width / ss_x, luma_height / ss_y))
}

Expand Down
28 changes: 11 additions & 17 deletions src/plane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,14 @@
//! # Memory Layout
//!
//! Planes store data in a contiguous, aligned buffer with support for padding on all sides:
//! - Non-empty allocations are aligned to at least 64 bytes on most targets
//! (SIMD-friendly)
//! - Non-empty allocations are aligned to at least 8 bytes on `wasm32` targets
//! that are not WASI
//! - If `T` has stricter alignment requirements, the allocation uses
//! `std::mem::align_of::<T>()` instead
//! - Data is aligned to at least 64 bytes on most targets (SIMD-friendly)
//! - Data is aligned to at least 8 bytes on `wasm32` targets that are not WASI
//! - If `T` has stricter alignment requirements, the allocation uses `align_of::<T>()` instead
//! - Padding pixels surround the visible area for codec algorithms that need border access
//!
//! More precisely, non-empty plane data is allocated with
//! `max(DATA_ALIGNMENT, std::mem::align_of::<T>())`, where `DATA_ALIGNMENT` is
//! 64 except on non-WASI `wasm32` targets, where it is 8. Empty planes do not
//! allocate and must not be assumed to have this extra SIMD alignment.
//! More precisely, plane data is aligned to `max(DATA_ALIGNMENT, align_of::<T>())`,
//! where `DATA_ALIGNMENT` is 64 except on non-WASI `wasm32` targets, where it is 8.
//! Empty planes do not allocate but the dangling pointers still follow these alignment rules.
//!
//! # API Design
//!
Expand Down Expand Up @@ -71,8 +67,7 @@ use crate::pixel::Pixel;
/// (optimized for SIMD operations)
/// - Non-empty allocations are aligned to at least 8 bytes on `wasm32` targets
/// that are not WASI
/// - If `T` has stricter alignment requirements, the allocation uses
/// `std::mem::align_of::<T>()` instead
/// - If `T` has stricter alignment requirements, the allocation uses `align_of::<T>()` instead
///
/// More precisely, non-empty plane data is allocated with
/// `max(DATA_ALIGNMENT, std::mem::align_of::<T>())`, where `DATA_ALIGNMENT` is
Expand Down Expand Up @@ -135,15 +130,14 @@ impl<T> Plane<T> {
///
/// # Allocation Alignment
///
/// For non-empty planes, the allocation returned by [`data`][Plane::data]
/// and [`data_mut`][Plane::data_mut] is aligned to
/// `max(DATA_ALIGNMENT, std::mem::align_of::<T>())`, where `DATA_ALIGNMENT`
/// The allocation returned by [`data`][Plane::data] and [`data_mut`][Plane::data_mut]
/// is aligned to `max(DATA_ALIGNMENT, align_of::<T>())`, where `DATA_ALIGNMENT`
/// is 64 except on non-WASI `wasm32` targets, where it is 8. This matters
/// if you use unsafe code to access the backing pointer directly, especially
/// with over-aligned `T`.
///
/// Empty planes do not allocate and must not be assumed to have the extra
/// SIMD alignment beyond what Rust requires for an empty `[T]` slice.
/// Empty planes do not allocate but the dangling pointers can still be assumed to be
/// aligned as described above.
///
/// # Example
///
Expand Down
82 changes: 48 additions & 34 deletions src/plane/aligned.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ use std::alloc::{Layout, alloc, alloc_zeroed, dealloc, handle_alloc_error};
use std::fmt::Debug;
use std::marker::PhantomData;
use std::mem::{ManuallyDrop, MaybeUninit, align_of};
use std::num::NonZeroUsize;
use std::ops::{Deref, DerefMut};
use std::ptr::NonNull;

use crate::pixel::Pixel;

// Minimum data alignment to help with SIMD. Non-empty allocations use this or
// Minimum data alignment to help with SIMD. Allocations use this or
// `align_of::<T>()`, whichever is larger.
const DATA_ALIGNMENT: usize = {
if cfg!(target_arch = "wasm32") && cfg!(not(target_os = "wasi")) {
Expand All @@ -33,41 +32,41 @@ unsafe impl<T: Send> Send for AlignedData<T> {}
unsafe impl<T: Sync> Sync for AlignedData<T> {}

impl<T> AlignedData<T> {
const fn layout(len: NonZeroUsize) -> Layout {
const fn layout(len: usize) -> Layout {
const { assert!(DATA_ALIGNMENT.is_power_of_two()) };
const { assert!(size_of::<T>() > 0, "T must be Sized") };
let alignment = if align_of::<T>() > DATA_ALIGNMENT {
align_of::<T>()
} else {
DATA_ALIGNMENT
};
let t_size = const { NonZeroUsize::new(size_of::<T>()).expect("T is Sized") };

let size = len
.checked_mul(t_size)
.checked_mul(size_of::<T>())
.expect("allocation size does not overflow usize");

match Layout::from_size_align(size.get(), alignment) {
match Layout::from_size_align(size, alignment) {
Ok(l) => l,
_ => panic!("invalid layout"),
}
}

pub fn new_uninit(len: usize) -> AlignedData<MaybeUninit<T>> {
let ptr = if let Some(len) = NonZeroUsize::new(len) {
let layout = Self::layout(len);
// SAFETY: `Self::layout` guarantees that the layout is valid and has nonzero size.
let ptr = unsafe { alloc(layout) as *mut MaybeUninit<T> };
let Some(ptr) = NonNull::new(ptr) else {
handle_alloc_error(layout);
};

NonNull::slice_from_raw_parts(ptr, len.get())
let layout = Self::layout(len);
let ptr = if layout.size() != 0 {
// SAFETY: `Self::layout` guarantees that the layout is valid and
// layout.size() is checked above.
let ptr = unsafe { alloc(layout).cast() };
match NonNull::new(ptr) {
Some(p) => p,
None => handle_alloc_error(layout),
}
} else {
NonNull::slice_from_raw_parts(NonNull::dangling(), 0)
layout.dangling_ptr().cast()
};

AlignedData {
ptr,
ptr: NonNull::slice_from_raw_parts(ptr, len),
_marker: PhantomData,
}
}
Expand Down Expand Up @@ -95,23 +94,22 @@ impl<T> AlignedData<MaybeUninit<T>> {
impl<T: Pixel> AlignedData<T> {
/// Zeroed.
pub fn new(len: usize) -> Self {
let ptr = if let Some(len) = NonZeroUsize::new(len) {
let layout = Self::layout(len);
// SAFETY:
// - `Self::layout` guarantees that the layout is valid and has nonzero size
// - The Pixel trait guarantees that zeroed memory is a valid T
let ptr = unsafe { alloc_zeroed(layout) as *mut T };
let Some(ptr) = NonNull::new(ptr) else {
handle_alloc_error(layout);
};

NonNull::slice_from_raw_parts(ptr, len.get())
let layout = Self::layout(len);
let ptr = if layout.size() != 0 {
// SAFETY: `Self::layout` guarantees that the layout is valid and
// layout.size() is checked above.
// The Pixel trait guarantees that zeroed memory is a valid T
let ptr = unsafe { alloc_zeroed(layout).cast() };
match NonNull::new(ptr) {
Some(p) => p,
None => handle_alloc_error(layout),
}
} else {
NonNull::slice_from_raw_parts(NonNull::dangling(), 0)
layout.dangling_ptr().cast()
};

Self {
ptr,
ptr: NonNull::slice_from_raw_parts(ptr, len),
_marker: PhantomData,
}
}
Expand Down Expand Up @@ -181,10 +179,11 @@ impl<T: Clone> Clone for AlignedData<T> {

impl<T> Drop for AlignedData<T> {
fn drop(&mut self) {
let layout = match NonZeroUsize::new(self.len()) {
Some(len) => Self::layout(len),
None => return, // nothing allocated, nothing to deallocate
};
let layout = Self::layout(self.len());
if layout.size() == 0 {
// nothing allocated, nothing to deallocate
return;
}

// drop the contained T (i.e. dropping [T]), then dealloc

Expand Down Expand Up @@ -244,6 +243,21 @@ mod tests {
data[0].write(OverAligned([0]));
}

#[test]
fn guarantee_alignment_for_empty() {
let data = AlignedData::<u8>::new_uninit(0);
assert_eq!(data.as_ptr().align_offset(DATA_ALIGNMENT), 0);

let data = AlignedData::<u16>::new_uninit(0);
assert_eq!(data.as_ptr().align_offset(DATA_ALIGNMENT), 0);

#[expect(dead_code)]
#[repr(align(1048576))]
struct OverAligned([u8; 1]);
let data = AlignedData::<OverAligned>::new_uninit(0);
assert_eq!(data.as_ptr().align_offset(DATA_ALIGNMENT), 0);
}

#[test]
#[should_panic(expected = "invalid layout")]
fn invalid_layout_panic() {
Expand Down