diff --git a/Cargo.toml b/Cargo.toml index cbf5e85..c2195db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md index 32f6496..7cbce08 100644 --- a/README.md +++ b/README.md @@ -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::()` 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::()` 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 @@ -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 diff --git a/src/chroma.rs b/src/chroma.rs index e13fddc..473aeb1 100644 --- a/src/chroma.rs +++ b/src/chroma.rs @@ -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)) } diff --git a/src/plane.rs b/src/plane.rs index 944c589..8a9a933 100644 --- a/src/plane.rs +++ b/src/plane.rs @@ -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::()` 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::()` 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::())`, 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::())`, +//! 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 //! @@ -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::()` instead +/// - If `T` has stricter alignment requirements, the allocation uses `align_of::()` instead /// /// More precisely, non-empty plane data is allocated with /// `max(DATA_ALIGNMENT, std::mem::align_of::())`, where `DATA_ALIGNMENT` is @@ -135,15 +130,14 @@ impl Plane { /// /// # 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::())`, where `DATA_ALIGNMENT` + /// The allocation returned by [`data`][Plane::data] and [`data_mut`][Plane::data_mut] + /// is aligned to `max(DATA_ALIGNMENT, align_of::())`, 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 /// diff --git a/src/plane/aligned.rs b/src/plane/aligned.rs index 24628ed..de17c70 100644 --- a/src/plane/aligned.rs +++ b/src/plane/aligned.rs @@ -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::()`, whichever is larger. const DATA_ALIGNMENT: usize = { if cfg!(target_arch = "wasm32") && cfg!(not(target_os = "wasi")) { @@ -33,41 +32,41 @@ unsafe impl Send for AlignedData {} unsafe impl Sync for AlignedData {} impl AlignedData { - const fn layout(len: NonZeroUsize) -> Layout { + const fn layout(len: usize) -> Layout { const { assert!(DATA_ALIGNMENT.is_power_of_two()) }; + const { assert!(size_of::() > 0, "T must be Sized") }; let alignment = if align_of::() > DATA_ALIGNMENT { align_of::() } else { DATA_ALIGNMENT }; - let t_size = const { NonZeroUsize::new(size_of::()).expect("T is Sized") }; let size = len - .checked_mul(t_size) + .checked_mul(size_of::()) .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> { - 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 }; - 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, } } @@ -95,23 +94,22 @@ impl AlignedData> { impl AlignedData { /// 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, } } @@ -181,10 +179,11 @@ impl Clone for AlignedData { impl Drop for AlignedData { 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 @@ -244,6 +243,21 @@ mod tests { data[0].write(OverAligned([0])); } + #[test] + fn guarantee_alignment_for_empty() { + let data = AlignedData::::new_uninit(0); + assert_eq!(data.as_ptr().align_offset(DATA_ALIGNMENT), 0); + + let data = AlignedData::::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::::new_uninit(0); + assert_eq!(data.as_ptr().align_offset(DATA_ALIGNMENT), 0); + } + #[test] #[should_panic(expected = "invalid layout")] fn invalid_layout_panic() {