Conversation
kkysen
left a comment
There was a problem hiding this comment.
Thanks for the much safer implementation! I'm still taking a closer look at the unsafes, as the few remaining are still quite critical, but I wanted to give some initial feedback in general first.
Pulled out the docs additions from #1439 into its own PR.
kkysen
left a comment
There was a problem hiding this comment.
@leo030303, could you rebase this on main? There are a lot of other commits in here now, so it's harder to review.
Co-authored-by: Khyber Sen <kkysen@gmail.com>
Co-authored-by: Khyber Sen <kkysen@gmail.com>
Co-authored-by: Khyber Sen <kkysen@gmail.com>
Co-authored-by: Khyber Sen <kkysen@gmail.com>
Co-authored-by: Khyber Sen <kkysen@gmail.com>
Co-authored-by: Khyber Sen <kkysen@gmail.com>
Co-authored-by: Khyber Sen <kkysen@gmail.com>
Co-authored-by: Khyber Sen <kkysen@gmail.com>
Co-authored-by: Khyber Sen <kkysen@gmail.com>
…rap_c` (#1470) This can be used by #1439 in `fn send_data` to safely create a `Rav1dData` without a second extra allocation. We'll still need one copy with the way the APIs are right now, but this should keep things obviously safe and without a second extra allocation. Next, we can work on redesigning the API to allow zero-copy usage.
…API (#1471) This does some API simplification for #1439 while also supporting other `impl AsRef` types. It separates `CRef` from `CBox`. `CBox` is the pure C abstraction over an owned C reference/"Box". `CRef` is an `enum` over `CBox` and other Rust-native `impl AsRef`s. I tried using `dyn AsRef` first, but we make some safety guarantees about reference stability that we can't make about any `impl AsRef`, and that would force an extra `Box`. Moving the `dyn AsRef` one level higher to `Arc<dyn AsRef<T>>` would be super nice, but in order to send the `Arc`s through FFI boundaries, they can't wrap an unsized type, as we can't send unsized pointers over FFI boundaries (the ptr metadata API is still very unstable). Adding support for non `'static` `&T`s would be extremely useful and would allow us to eliminate a data copy, but that is much more tricky, as we'd have to thread the lifetime through a lot of places and probably rearrange some of the `Rav1dContext`/`Rav1dState` API.
Co-authored-by: Sergey "Shnatsel" Davidoff <shnatsel@gmail.com>
Co-authored-by: Sergey "Shnatsel" Davidoff <shnatsel@gmail.com>
Co-authored-by: Khyber Sen <kkysen@gmail.com>
|
This all looks good so far to me, only note is I had to remove static_assertions::assert_impl_all!(Decoder: Send, Sync);Since CRef isn't Send or Sync, if its trivial to add that back in then we should but I'm not sure what safety assumptions you made on the C backend so I didn't want to go messing with that, otherwise I don't think remove the assertion is a very big deal since it just restricts the API a bit |
|
Think that's most of the outstanding issues wrapped up! |
|
I can't wait for this to be finished! |
|
@kkysen if fixing the API for that to never be null isn't straightforward then it could probably be left for a follow up pr since I think the core of this pr should be good to go |
|
I've opened a proof-of-concept PR for It's not quite a drop-in replacement but the conversion was fairly straightforward. I've wired everything up to |
| pub fn plane_data(&self, component: PlanarImageComponent) -> &[u8] { | ||
| let height = match component { | ||
| PlanarImageComponent::Y => self.height(), | ||
| _ => match self.pixel_layout() { | ||
| PixelLayout::I420 => self.height().div_ceil(2), | ||
| PixelLayout::I422 | PixelLayout::I444 => self.height(), | ||
| PixelLayout::I400 => return &[], // grayscale images don't have color components | ||
| }, | ||
| }; | ||
|
|
||
| let stride = self.stride(component); | ||
|
|
||
| // Get a raw pointer to the plane data of the `component` for the decoded frame. | ||
| let index: usize = component.into(); | ||
| let raw_plane_data_pointer = self.inner.data.as_ref().unwrap().data[index] | ||
| .as_byte_mut_ptr() | ||
| .cast_const(); | ||
|
|
||
| if stride == 0 || raw_plane_data_pointer.is_null() { | ||
| return &[]; | ||
| } | ||
| let data_length = (stride as usize) | ||
| .checked_mul(height as usize) | ||
| .expect("The product of stride and height exceeded usize::MAX"); | ||
| // SAFETY: The following invariants are upheld: | ||
| // 1. Pointer validity: Checked above - if null or stride is 0, we return &[]. | ||
| // 2. Pointer alignment: The allocator guarantees RAV1D_PICTURE_ALIGNMENT (64-byte) | ||
| // alignment (see Rav1dPictureDataComponentInner::new), which exceeds any | ||
| // primitive type's alignment requirements. | ||
| // 3. Allocated size: The allocator guarantees the buffer is at least stride * height | ||
| // bytes (the allocator callback contract in Dav1dPicAllocator). The checked_mul | ||
| // ensures this calculation doesn't overflow. | ||
| // 4. Initialization: The allocator is required to initialize the data per the | ||
| // alloc_picture_callback safety requirements. | ||
| // 5. Lifetime: The returned slice borrows &self, keeping the Arc<Rav1dPictureData> | ||
| // alive for the duration of the borrow. | ||
| // 6. No mutable aliases: The Picture is only returned after decoding is complete, | ||
| // so the decoder no longer writes to this buffer. The public API only exposes | ||
| // shared (&[u8]) access, and no &mut references to this data can exist once | ||
| // the Picture is handed to the user. | ||
| // | ||
| // Past dav1d-rs PRs relevant to this line: | ||
| // https://github.com/rust-av/dav1d-rs/pull/121 | ||
| // https://github.com/rust-av/dav1d-rs/pull/123 | ||
| unsafe { slice::from_raw_parts(raw_plane_data_pointer, data_length) } |
There was a problem hiding this comment.
| pub fn plane_data(&self, component: PlanarImageComponent) -> &[u8] { | |
| let height = match component { | |
| PlanarImageComponent::Y => self.height(), | |
| _ => match self.pixel_layout() { | |
| PixelLayout::I420 => self.height().div_ceil(2), | |
| PixelLayout::I422 | PixelLayout::I444 => self.height(), | |
| PixelLayout::I400 => return &[], // grayscale images don't have color components | |
| }, | |
| }; | |
| let stride = self.stride(component); | |
| // Get a raw pointer to the plane data of the `component` for the decoded frame. | |
| let index: usize = component.into(); | |
| let raw_plane_data_pointer = self.inner.data.as_ref().unwrap().data[index] | |
| .as_byte_mut_ptr() | |
| .cast_const(); | |
| if stride == 0 || raw_plane_data_pointer.is_null() { | |
| return &[]; | |
| } | |
| let data_length = (stride as usize) | |
| .checked_mul(height as usize) | |
| .expect("The product of stride and height exceeded usize::MAX"); | |
| // SAFETY: The following invariants are upheld: | |
| // 1. Pointer validity: Checked above - if null or stride is 0, we return &[]. | |
| // 2. Pointer alignment: The allocator guarantees RAV1D_PICTURE_ALIGNMENT (64-byte) | |
| // alignment (see Rav1dPictureDataComponentInner::new), which exceeds any | |
| // primitive type's alignment requirements. | |
| // 3. Allocated size: The allocator guarantees the buffer is at least stride * height | |
| // bytes (the allocator callback contract in Dav1dPicAllocator). The checked_mul | |
| // ensures this calculation doesn't overflow. | |
| // 4. Initialization: The allocator is required to initialize the data per the | |
| // alloc_picture_callback safety requirements. | |
| // 5. Lifetime: The returned slice borrows &self, keeping the Arc<Rav1dPictureData> | |
| // alive for the duration of the borrow. | |
| // 6. No mutable aliases: The Picture is only returned after decoding is complete, | |
| // so the decoder no longer writes to this buffer. The public API only exposes | |
| // shared (&[u8]) access, and no &mut references to this data can exist once | |
| // the Picture is handed to the user. | |
| // | |
| // Past dav1d-rs PRs relevant to this line: | |
| // https://github.com/rust-av/dav1d-rs/pull/121 | |
| // https://github.com/rust-av/dav1d-rs/pull/123 | |
| unsafe { slice::from_raw_parts(raw_plane_data_pointer, data_length) } | |
| pub fn plane_data( | |
| &self, | |
| component: PlanarImageComponent, | |
| ) -> DisjointImmutGuard<'_, Rav1dPictureDataComponentInner, [u8]> { | |
| let data = &self.inner.data.as_ref().unwrap().data; | |
| let component = &data[usize::from(component)]; | |
| // SAFETY: `.slice` is not marked `unsafe`, | |
| // but that's because it is not meant to be used outside of `rav1d`, | |
| // and inside `rav1d` we collectively uphold the `DisjointMut` safety requirements. | |
| // Here, though, we expose this publicly, so it must be sound regardless of the caller. | |
| // It's safe because a [`Picture`] is only created in [`Decoder::get_picture`], | |
| // which calls [`rav1d_get_picture`], which is safe for all callers. | |
| // Once this [`Picture`] has been created, there aren't anymore | |
| // potentially mutable references to it while it's being decoded, | |
| // as it's already fully decoded. | |
| component.slice::<BitDepth8, _>(..) | |
| } |
This would make things fully safe (although there's a hidden unsafety, but one that's much more obviously sound). Judging by image-rs/image#2849, it seems like returning a guard from this instead of the &[u8] directly should still work.
There was a problem hiding this comment.
Or we could do this:
| pub fn plane_data(&self, component: PlanarImageComponent) -> &[u8] { | |
| let height = match component { | |
| PlanarImageComponent::Y => self.height(), | |
| _ => match self.pixel_layout() { | |
| PixelLayout::I420 => self.height().div_ceil(2), | |
| PixelLayout::I422 | PixelLayout::I444 => self.height(), | |
| PixelLayout::I400 => return &[], // grayscale images don't have color components | |
| }, | |
| }; | |
| let stride = self.stride(component); | |
| // Get a raw pointer to the plane data of the `component` for the decoded frame. | |
| let index: usize = component.into(); | |
| let raw_plane_data_pointer = self.inner.data.as_ref().unwrap().data[index] | |
| .as_byte_mut_ptr() | |
| .cast_const(); | |
| if stride == 0 || raw_plane_data_pointer.is_null() { | |
| return &[]; | |
| } | |
| let data_length = (stride as usize) | |
| .checked_mul(height as usize) | |
| .expect("The product of stride and height exceeded usize::MAX"); | |
| // SAFETY: The following invariants are upheld: | |
| // 1. Pointer validity: Checked above - if null or stride is 0, we return &[]. | |
| // 2. Pointer alignment: The allocator guarantees RAV1D_PICTURE_ALIGNMENT (64-byte) | |
| // alignment (see Rav1dPictureDataComponentInner::new), which exceeds any | |
| // primitive type's alignment requirements. | |
| // 3. Allocated size: The allocator guarantees the buffer is at least stride * height | |
| // bytes (the allocator callback contract in Dav1dPicAllocator). The checked_mul | |
| // ensures this calculation doesn't overflow. | |
| // 4. Initialization: The allocator is required to initialize the data per the | |
| // alloc_picture_callback safety requirements. | |
| // 5. Lifetime: The returned slice borrows &self, keeping the Arc<Rav1dPictureData> | |
| // alive for the duration of the borrow. | |
| // 6. No mutable aliases: The Picture is only returned after decoding is complete, | |
| // so the decoder no longer writes to this buffer. The public API only exposes | |
| // shared (&[u8]) access, and no &mut references to this data can exist once | |
| // the Picture is handed to the user. | |
| // | |
| // Past dav1d-rs PRs relevant to this line: | |
| // https://github.com/rust-av/dav1d-rs/pull/121 | |
| // https://github.com/rust-av/dav1d-rs/pull/123 | |
| unsafe { slice::from_raw_parts(raw_plane_data_pointer, data_length) } | |
| pub fn plane_data<'a>(&'a self, component: PlanarImageComponent) -> &'a [u8] { | |
| let data = &self.inner.data.as_ref().unwrap().data; | |
| let component = &data[usize::from(component)]; | |
| let guard = component.slice::<BitDepth8, _>(..); | |
| // SAFETY: [`Picture`] is only created after decoding is complete | |
| // (in [`Decoder::get_picture`], which calls [`rav1d_get_picture`]). | |
| // Thus, the decoder no longer writes to this data, | |
| // and there are no other safe public ways to have a `&mut` to this data. | |
| unsafe { guard.unchecked_disjoint_mut() } |
If that seems better, than I can open a PR to add this fn unchecked_disjoint_mut:
impl<'a, T: ?Sized + AsMutPtr, V: ?Sized> DisjointImmutGuard<'a, T, V> {
// # Safety
//
// This is no longer checked at runtime to be legitimately disjoint mut.
pub unsafe fn unchecked_disjoint_mut(&self) -> &'a V {
self.slice
}
}There was a problem hiding this comment.
I think once this is done, the PR should be all good to go.
Also, you could revert back to the original Plane API so that it's more of a drop-in replacement. That should be easy enough for either of these APIs I suggested above.
I've copy pasted the PR from #1362 and updated it with some of the suggestions made on that pull request. The main changes are:
enums fromrav1dinstead of redefining new ones, and added in doc comments from the originaldav1d-rslibrary to a few items.unsafecode as I could and replaced it with the Rust methods fromrav1das much as possible.It currently works as a drop-in replacement for
dav1d-rs; adding inuse rav1d as dav1d;to my fork of image makes everything work fine.The only functional changes I made are I removed the
unsafe impls ofSendandSyncforInnerPicturesoPictureis no longerSyncorSend. I looked through the code and I don't believeDisjointMut<Rav1dPictureDataComponentInner>, which is a field of one of its children, is thread safe, though I'm open to correction there; I'm pretty unfamiliar withunsafeRust.I also don't have safety comments on the two
unsafeblocks inrust_api.rs; I'm unsure what these would look like, so open to suggestions there. These are mostly taken verbatim from the old pull request.