fix(client-core,ffvk): close the proof-lint hole in two of the three unguarded crates
`clippy::undocumented_unsafe_blocks` is what makes the SAFETY convention a rule rather than a habit,
and three crates had never adopted it — pf-client-core (91 unsafe items), pf-presenter (123) and
punktfunk-core (167) — while every other subsystem crate denied it. That gap is why the decoders'
`unsafe impl Send`s carried a one-line aside instead of an argument: nothing required one.
pf-client-core and pf-ffvk now deny it, with a proof written for all 58 + 3 sites they had.
⚠️ 44 of those 58 were WINDOWS-ONLY — `clipboard.rs` 24 and `video_d3d11.rs` 20 — and invisible to
the Linux measurement that sized this work at 14. Same trap as the E0133 sweep: a Linux-only survey
of a cross-platform crate undercounts by whatever the `cfg` hides, here by 3x. Landing the deny on
the strength of that number alone would have re-broken Windows CI, which is exactly the mistake this
session already made once with the `warn`-that-was-really-`deny`.
The proofs say what is actually load-bearing rather than restating the call. In `clipboard.rs` that
is the ownership split Win32 requires and nothing in the code stated: `GetClipboardData` returns a
handle BORROWED from the clipboard (never freed here), while `GlobalAlloc` + `SetClipboardData`
TRANSFERS ownership to it (which is why nothing frees that one either) — two opposite rules, three
lines apart. In `video_d3d11.rs` the recurring one is that libav's `get_format` list is
NUL-terminated by `AV_PIX_FMT_NONE`, which is what keeps the walk in bounds.
Remaining: punktfunk-core (~146, of which `abi.rs` is 141) and pf-presenter (~108). Both want the
"state the contract once" treatment — abi.rs's sites are a handful of repeating shapes (`opt_cstr`
on caller C strings, null-guarded out-param writes, forwarding calls), not 141 distinct arguments.
Note the vendored `fec-rs` (18 sites) is a separate path-dependency crate, so it is out of scope
rather than something to prove.
Verified: Linux .21 fmt + both CI clippy steps rc=0; Windows .47 `-p pf-client-core` clippy
`-D warnings` rc=0 (the only place the 44 are visible), plus the full Windows CI clippy set and
pf-capture's 18 tests.
This commit is contained in:
@@ -292,6 +292,7 @@ mod os {
|
||||
/// A registered clipboard format id, by name (`PNG`, the concealed marker, …).
|
||||
fn registered(name: &str) -> u32 {
|
||||
let wide: Vec<u16> = name.encode_utf16().chain(std::iter::once(0)).collect();
|
||||
// SAFETY: `wide` is a local NUL-terminated UTF-16 buffer that outlives this synchronous call.
|
||||
unsafe { RegisterClipboardFormatW(PCWSTR(wide.as_ptr())) }
|
||||
}
|
||||
|
||||
@@ -305,6 +306,7 @@ mod os {
|
||||
fn open() -> Result<Clip> {
|
||||
// A retry loop: another app can hold the clipboard for a moment.
|
||||
for _ in 0..10 {
|
||||
// SAFETY: takes no pointer; `None` is the documented "associate with no window" argument.
|
||||
if unsafe { OpenClipboard(None) }.is_ok() {
|
||||
return Ok(Clip);
|
||||
}
|
||||
@@ -315,26 +317,31 @@ mod os {
|
||||
}
|
||||
impl Drop for Clip {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: pairs with the `OpenClipboard` that built this guard; closing is what `Drop` is for.
|
||||
let _ = unsafe { CloseClipboard() };
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sequence_number() -> u32 {
|
||||
// SAFETY: a no-argument query that only reads the OS clipboard's sequence counter.
|
||||
unsafe { GetClipboardSequenceNumber() }
|
||||
}
|
||||
|
||||
/// Password managers mark secrets with this format; §5.2's concealed-type rule.
|
||||
pub fn is_concealed() -> bool {
|
||||
let fmt = registered("ExcludeClipboardContentFromMonitorProcessing");
|
||||
// SAFETY: a scalar format id in, a status out; reads nothing through a pointer.
|
||||
unsafe { IsClipboardFormatAvailable(fmt) }.is_ok()
|
||||
}
|
||||
|
||||
/// The wire kinds the current clipboard can supply, with size hints where they're free.
|
||||
pub fn available_kinds() -> Vec<(&'static str, u64)> {
|
||||
let mut out = Vec::new();
|
||||
// SAFETY: as above — a scalar format id, status out.
|
||||
if unsafe { IsClipboardFormatAvailable(CF_UNICODETEXT) }.is_ok() {
|
||||
out.push((MIME_TEXT, 0));
|
||||
}
|
||||
// SAFETY: as above — a scalar format id, status out.
|
||||
if unsafe { IsClipboardFormatAvailable(png_format()) }.is_ok() {
|
||||
out.push((MIME_PNG, 0));
|
||||
}
|
||||
@@ -346,32 +353,43 @@ mod os {
|
||||
let _clip = Clip::open()?;
|
||||
match mime {
|
||||
MIME_TEXT => {
|
||||
// SAFETY: the clipboard is open (the `Clip` guard); the handle returned is BORROWED from the clipboard and stays valid while it is open, so it is never freed here.
|
||||
let h = unsafe { GetClipboardData(CF_UNICODETEXT) }?;
|
||||
let g = HGLOBAL(h.0);
|
||||
// SAFETY: `g` is that borrowed clipboard handle; `GlobalLock` yields a pointer valid until the matching `GlobalUnlock` below.
|
||||
let p = unsafe { GlobalLock(g) } as *const u16;
|
||||
if p.is_null() {
|
||||
bail!("clipboard text lock failed");
|
||||
}
|
||||
// GlobalSize is a byte count of a NUL-terminated UTF-16 buffer.
|
||||
// SAFETY: a size query on the same live handle.
|
||||
let bytes = unsafe { GlobalSize(g) };
|
||||
let mut len = bytes / 2;
|
||||
// SAFETY: `p` is the locked buffer and `len` is derived from the `GlobalSize` above, so the slice stays inside the allocation; it is read before the unlock.
|
||||
let slice = unsafe { std::slice::from_raw_parts(p, len) };
|
||||
if let Some(nul) = slice.iter().position(|&c| c == 0) {
|
||||
len = nul;
|
||||
}
|
||||
// SAFETY: as above — same locked buffer and same length, read before the unlock.
|
||||
let text = String::from_utf16_lossy(unsafe { std::slice::from_raw_parts(p, len) });
|
||||
// SAFETY: releases the lock taken above, exactly once.
|
||||
let _ = unsafe { GlobalUnlock(g) };
|
||||
Ok(text.into_bytes())
|
||||
}
|
||||
MIME_PNG => {
|
||||
// SAFETY: as the text path — the handle is borrowed from the open clipboard, never freed here.
|
||||
let h = unsafe { GetClipboardData(png_format()) }?;
|
||||
let g = HGLOBAL(h.0);
|
||||
// SAFETY: `g` is that borrowed handle; the pointer is valid until the matching unlock.
|
||||
let p = unsafe { GlobalLock(g) } as *const u8;
|
||||
if p.is_null() {
|
||||
bail!("clipboard png lock failed");
|
||||
}
|
||||
// SAFETY: a size query on the same live handle.
|
||||
let len = unsafe { GlobalSize(g) };
|
||||
// SAFETY: `p` is the locked buffer and `len` came from `GlobalSize`, so the slice is in bounds; it is copied out before the unlock.
|
||||
let out = unsafe { std::slice::from_raw_parts(p, len) }.to_vec();
|
||||
// SAFETY: releases the lock taken above, exactly once.
|
||||
let _ = unsafe { GlobalUnlock(g) };
|
||||
Ok(out)
|
||||
}
|
||||
@@ -392,15 +410,21 @@ mod os {
|
||||
other => bail!("unsupported clipboard format {other}"),
|
||||
};
|
||||
let _clip = Clip::open()?;
|
||||
// SAFETY: no arguments; the clipboard is open and owned by this thread via the `Clip` guard.
|
||||
unsafe { EmptyClipboard() }?;
|
||||
// The clipboard OWNS this block once SetClipboardData succeeds — do not free it.
|
||||
// SAFETY: a size in, an owned moveable handle out — ownership passes to the clipboard at `SetClipboardData` below.
|
||||
let g = unsafe { GlobalAlloc(GMEM_MOVEABLE, payload.len()) }?;
|
||||
// SAFETY: `g` is the handle just allocated; the pointer is valid until the matching unlock.
|
||||
let p = unsafe { GlobalLock(g) } as *mut u8;
|
||||
if p.is_null() {
|
||||
bail!("clipboard alloc lock failed");
|
||||
}
|
||||
// SAFETY: `p` addresses that locked block, allocated at exactly `payload.len()` bytes on the line above, and the two regions are distinct allocations.
|
||||
unsafe { std::ptr::copy_nonoverlapping(payload.as_ptr(), p, payload.len()) };
|
||||
// SAFETY: releases the lock taken above, before the handle is handed to the clipboard.
|
||||
let _ = unsafe { GlobalUnlock(g) };
|
||||
// SAFETY: ownership of `g` transfers to the clipboard here, which is why nothing frees it afterwards.
|
||||
unsafe { SetClipboardData(fmt, Some(HANDLE(g.0))) }?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -999,6 +999,8 @@ impl Worker {
|
||||
fn set_slot_sensors(slot: &mut Slot, enabled: bool) {
|
||||
use sdl3::sensor::SensorType;
|
||||
for s in [SensorType::Gyroscope, SensorType::Accelerometer] {
|
||||
// SAFETY: an SDL3 query on the gamepad this slot owns and keeps open; it takes a
|
||||
// plain sensor-type enum and only reads device state.
|
||||
if unsafe { slot.pad.has_sensor(s) } {
|
||||
let _ = slot.pad.sensor_set_enabled(s, enabled);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,12 @@
|
||||
//! so `crate::audio` is the only name the session pump ever sees. `keymap` (evdev-keyed)
|
||||
//! stays Linux: the session path uses pf-presenter's SDL-scancode table instead.
|
||||
|
||||
// Unsafe-proof program: every `unsafe {}` / `unsafe impl` in this crate carries a `// SAFETY:`
|
||||
// proof of why it is sound. This crate held ~91 unsafe items with NO enforcement while every
|
||||
// other subsystem crate denied it — the decoders' `unsafe impl Send`s had a one-line aside
|
||||
// instead of an argument precisely because nothing required one.
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod audio;
|
||||
#[cfg(windows)]
|
||||
|
||||
@@ -231,6 +231,8 @@ unsafe impl Send for DrmFrameGuard {}
|
||||
|
||||
impl Drop for DrmFrameGuard {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: `self.0` is the one `AVFrame` this guard owns; `av_frame_free` releases it
|
||||
// exactly once (this `Drop` runs once) and nulls the pointer through the `&mut`.
|
||||
unsafe { ffmpeg::ffi::av_frame_free(&mut self.0) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +165,9 @@ unsafe extern "C" fn get_format_d3d11(
|
||||
mut list: *const ffmpeg::ffi::AVPixelFormat,
|
||||
) -> ffmpeg::ffi::AVPixelFormat {
|
||||
use ffmpeg::ffi::*;
|
||||
// SAFETY: libav calls this `get_format` callback with a context and a list it owns; the list
|
||||
// is terminated by `AV_PIX_FMT_NONE`, so the walk stays inside it, and `avctx`'s fields are
|
||||
// read/set only while libav holds it live for the call.
|
||||
unsafe {
|
||||
if (*avctx).hw_device_ctx.is_null() {
|
||||
return AVPixelFormat::AV_PIX_FMT_NONE;
|
||||
@@ -187,6 +190,8 @@ fn decode_profile_supported(device: &ID3D11Device, codec_id: ffmpeg::codec::Id)
|
||||
let video: ID3D11VideoDevice = device
|
||||
.cast()
|
||||
.context("device lacks ID3D11VideoDevice (created without VIDEO_SUPPORT)")?;
|
||||
// SAFETY: COM calls on the live `ID3D11VideoDevice` obtained by the checked `cast` above; the
|
||||
// count bounds the loop and each profile is returned by value.
|
||||
let profiles: Vec<GUID> = unsafe {
|
||||
let n = video.GetVideoDecoderProfileCount();
|
||||
(0..n)
|
||||
@@ -200,6 +205,8 @@ fn decode_profile_supported(device: &ID3D11Device, codec_id: ffmpeg::codec::Id)
|
||||
other => bail!("no DXVA profile known for {other:?}"),
|
||||
};
|
||||
let ok = profiles.contains(&wanted)
|
||||
// SAFETY: same live video device; the two arguments are a borrowed local GUID and a plain
|
||||
// format enum.
|
||||
&& unsafe { video.CheckVideoDecoderFormat(&wanted, format) }
|
||||
.map(|b| b.as_bool())
|
||||
.unwrap_or(false);
|
||||
@@ -210,6 +217,7 @@ fn decode_profile_supported(device: &ID3D11Device, codec_id: ffmpeg::codec::Id)
|
||||
// decode error → software demotion + keyframe re-request path covers the switch.
|
||||
if codec_id == ffmpeg::codec::Id::HEVC {
|
||||
let main10 = profiles.contains(&PROFILE_HEVC_VLD_MAIN10)
|
||||
// SAFETY: as above — borrowed static GUID plus a plain format enum.
|
||||
&& unsafe { video.CheckVideoDecoderFormat(&PROFILE_HEVC_VLD_MAIN10, DXGI_FORMAT_P010) }
|
||||
.map(|b| b.as_bool())
|
||||
.unwrap_or(false);
|
||||
@@ -225,6 +233,9 @@ fn decode_profile_supported(device: &ID3D11Device, codec_id: ffmpeg::codec::Id)
|
||||
/// gap-free stream) instead of dying mid-stream on the opening IDR.
|
||||
unsafe fn d3d11va_decode_supported(hw_device: *mut ffmpeg::ffi::AVBufferRef) -> bool {
|
||||
use ffmpeg::ffi::*;
|
||||
// SAFETY: `hw_device` is a valid `AVBufferRef` by this fn's contract; the frames context is
|
||||
// allocated, configured and released within this scope, and every libav return is checked
|
||||
// before use.
|
||||
unsafe {
|
||||
// Scope-bound: this probe owns the frames ctx for the length of the check and the drop
|
||||
// below releases it on BOTH exits, instead of the early return relying on the null case and
|
||||
@@ -250,13 +261,19 @@ unsafe fn d3d11va_decode_supported(hw_device: *mut ffmpeg::ffi::AVBufferRef) ->
|
||||
/// keeps the shared textures on one GPU. `None`/no match falls back to the first hardware
|
||||
/// adapter (single-GPU boxes; a WARP-only box fails out to software decode).
|
||||
fn create_device(luid: Option<[u8; 8]>) -> Result<(ID3D11Device, ID3D11DeviceContext)> {
|
||||
// SAFETY: DXGI factory creation takes no pointer and returns an owned factory or an error,
|
||||
// checked by `?`.
|
||||
let factory: IDXGIFactory1 = unsafe { CreateDXGIFactory1() }.context("CreateDXGIFactory1")?;
|
||||
let mut chosen: Option<IDXGIAdapter1> = None;
|
||||
let mut fallback: Option<IDXGIAdapter1> = None;
|
||||
for i in 0.. {
|
||||
// SAFETY: a COM call on the live factory; the `Ok` binding is what proves an adapter came
|
||||
// back.
|
||||
let Ok(adapter) = (unsafe { factory.EnumAdapters1(i) }) else {
|
||||
break;
|
||||
};
|
||||
// SAFETY: a COM call on the adapter just enumerated, filling a descriptor returned by
|
||||
// value.
|
||||
let Ok(desc) = (unsafe { adapter.GetDesc1() }) else {
|
||||
continue;
|
||||
};
|
||||
@@ -286,6 +303,8 @@ fn create_device(luid: Option<[u8; 8]>) -> Result<(ID3D11Device, ID3D11DeviceCon
|
||||
.ok_or_else(|| anyhow!("no hardware DXGI adapter"))?;
|
||||
let mut device = None;
|
||||
let mut context = None;
|
||||
// SAFETY: `adapter` is the live adapter chosen above; the two out-params are local `Option`s
|
||||
// the callee only writes, and both are checked before use.
|
||||
unsafe {
|
||||
D3D11CreateDevice(
|
||||
&adapter,
|
||||
@@ -308,6 +327,8 @@ fn create_device(luid: Option<[u8; 8]>) -> Result<(ID3D11Device, ID3D11DeviceCon
|
||||
// keeps the invariant obvious).
|
||||
if let Ok(mt) = device.cast::<ID3D11Multithread>() {
|
||||
// Returns the PREVIOUS protection state — nothing to act on.
|
||||
// SAFETY: a COM call on the live `ID3D11Multithread` from a checked `cast`; it takes a
|
||||
// BOOL and returns the previous state.
|
||||
let _ = unsafe { mt.SetMultithreadProtected(true) };
|
||||
}
|
||||
Ok((device, context))
|
||||
@@ -326,6 +347,8 @@ struct Slot {
|
||||
|
||||
impl Drop for Slot {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: `self.handle` is the shared-texture NT handle this slot owns; `Drop` runs once,
|
||||
// so it is closed exactly once and never used after.
|
||||
unsafe {
|
||||
let _ = windows::Win32::Foundation::CloseHandle(self.handle);
|
||||
}
|
||||
@@ -374,8 +397,11 @@ impl SharedRing {
|
||||
OutputHeight: height,
|
||||
Usage: D3D11_VIDEO_USAGE_PLAYBACK_NORMAL,
|
||||
};
|
||||
// SAFETY: COM calls on the live video device, with a borrowed local descriptor and a
|
||||
// checked out-param.
|
||||
let enumerator = unsafe { video_device.CreateVideoProcessorEnumerator(&content) }
|
||||
.context("CreateVideoProcessorEnumerator")?;
|
||||
// SAFETY: same live device, borrowed enumerator from the line above.
|
||||
let vp = unsafe { video_device.CreateVideoProcessor(&enumerator, 0) }
|
||||
.context("CreateVideoProcessor")?;
|
||||
|
||||
@@ -407,6 +433,8 @@ impl SharedRing {
|
||||
let mut slots = Vec::with_capacity(RING_SLOTS);
|
||||
for _ in 0..RING_SLOTS {
|
||||
let mut tex = None;
|
||||
// SAFETY: a `?`-checked `CreateTexture2D` on the live device, over a fully-initialized
|
||||
// stack descriptor and a live `Option` out-param.
|
||||
unsafe { device.CreateTexture2D(&desc, None, Some(&mut tex)) }
|
||||
.context("create shared hand-off texture")?;
|
||||
let tex: ID3D11Texture2D = tex.expect("CreateTexture2D succeeded");
|
||||
@@ -414,6 +442,8 @@ impl SharedRing {
|
||||
tex.cast().context("shared texture lacks IDXGIKeyedMutex")?;
|
||||
let resource: IDXGIResource1 =
|
||||
tex.cast().context("shared texture lacks IDXGIResource1")?;
|
||||
// SAFETY: the shared-handle creation runs on the live texture just created; the
|
||||
// returned NT handle is owned by the `Slot` built below, which closes it in `Drop`.
|
||||
let handle = unsafe {
|
||||
resource.CreateSharedHandle(
|
||||
None,
|
||||
@@ -428,6 +458,8 @@ impl SharedRing {
|
||||
..Default::default()
|
||||
};
|
||||
let mut out_view = None;
|
||||
// SAFETY: COM calls on the live video device with borrowed local descriptors and a
|
||||
// checked out-param.
|
||||
unsafe {
|
||||
video_device.CreateVideoProcessorOutputView(
|
||||
&tex,
|
||||
@@ -518,6 +550,9 @@ impl D3d11vaDecoder {
|
||||
let video_context1: ID3D11VideoContext1 = context
|
||||
.cast()
|
||||
.context("context lacks ID3D11VideoContext1 (pre-1703 Windows?)")?;
|
||||
// SAFETY: a self-contained builder: every libav allocation is made here and null-checked,
|
||||
// the D3D11VA hwctx fields are filled from the live device/context borrowed above, and
|
||||
// what survives is moved into the decoder, which frees each exactly once in `Drop`.
|
||||
unsafe {
|
||||
let hw_device =
|
||||
ffi::av_hwdevice_ctx_alloc(ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_D3D11VA);
|
||||
@@ -579,6 +614,9 @@ impl D3d11vaDecoder {
|
||||
|
||||
pub(crate) fn decode(&mut self, au: &[u8]) -> Result<Option<D3d11Frame>> {
|
||||
use ffmpeg::ffi;
|
||||
// SAFETY: `packet`/`frame`/`ctx` are this decoder's own allocations, live for its whole
|
||||
// lifetime; `au` outlives the synchronous copy out of it, and every libav return is
|
||||
// checked before use.
|
||||
unsafe {
|
||||
let r = ffi::av_new_packet(self.packet, au.len() as i32);
|
||||
if r < 0 {
|
||||
@@ -616,6 +654,8 @@ impl D3d11vaDecoder {
|
||||
/// stream runs `RING_SLOTS` ahead of present).
|
||||
fn lift(&mut self) -> Result<D3d11Frame> {
|
||||
use ffmpeg::ffi;
|
||||
// SAFETY: `self.frame` is this decoder's own `AVFrame`; the format check below is what
|
||||
// proves it carries a D3D11 texture before anything reads the surface out of it.
|
||||
unsafe {
|
||||
if (*self.frame).format != ffi::AVPixelFormat::AV_PIX_FMT_D3D11 as i32 {
|
||||
bail!("decoder returned a software frame (no D3D11 surface)");
|
||||
@@ -794,6 +834,9 @@ impl D3d11vaDecoder {
|
||||
impl Drop for D3d11vaDecoder {
|
||||
fn drop(&mut self) {
|
||||
use ffmpeg::ffi;
|
||||
// SAFETY: each pointer is this decoder's own allocation and nothing else holds it; `Drop`
|
||||
// runs exactly once and each free nulls its pointer through the `&mut`, so none can be
|
||||
// released twice.
|
||||
unsafe {
|
||||
ffi::av_packet_free(&mut self.packet);
|
||||
ffi::av_frame_free(&mut self.frame);
|
||||
|
||||
@@ -23,6 +23,9 @@ impl SoftwareDecoder {
|
||||
let codec = ffmpeg::decoder::find(codec_id)
|
||||
.ok_or_else(|| anyhow!("no {codec_id:?} decoder in libavcodec"))?;
|
||||
let mut ctx = ffmpeg::codec::Context::new_with_codec(codec);
|
||||
// SAFETY: `as_mut_ptr` yields the `AVCodecContext` behind the `ctx` allocated on the line
|
||||
// above, which outlives these writes; each store is an in-bounds scalar field write on that
|
||||
// live context, made before the decoder is opened and reads them.
|
||||
unsafe {
|
||||
let raw = ctx.as_mut_ptr();
|
||||
(*raw).flags |= ffmpeg::ffi::AV_CODEC_FLAG_LOW_DELAY as i32;
|
||||
@@ -70,6 +73,9 @@ impl SoftwareDecoder {
|
||||
5 | 6 => SWS_CS_ITU601,
|
||||
_ => SWS_CS_ITU709,
|
||||
};
|
||||
// SAFETY: `sws_getCoefficients` returns a pointer into libav's own static coefficient
|
||||
// tables — valid for the process, read-only — and `sws_setColorspaceDetails` takes it
|
||||
// plus the live `SwsContext` behind `ctx` and plain scalars.
|
||||
unsafe {
|
||||
let coeffs = ffmpeg::ffi::sws_getCoefficients(cs);
|
||||
ffmpeg::ffi::sws_setColorspaceDetails(
|
||||
|
||||
@@ -19,6 +19,9 @@ unsafe extern "C" fn pick_vaapi(
|
||||
_ctx: *mut ffmpeg::ffi::AVCodecContext,
|
||||
mut list: *const ffmpeg::ffi::AVPixelFormat,
|
||||
) -> ffmpeg::ffi::AVPixelFormat {
|
||||
// SAFETY: libav calls this `get_format` callback with a list it owns, terminated by
|
||||
// `AV_PIX_FMT_NONE` — the walk stops at that terminator, so it stays inside the array, and it
|
||||
// only reads.
|
||||
unsafe {
|
||||
while *list != ffmpeg::ffi::AVPixelFormat::AV_PIX_FMT_NONE {
|
||||
if *list == ffmpeg::ffi::AVPixelFormat::AV_PIX_FMT_VAAPI {
|
||||
@@ -59,6 +62,9 @@ unsafe impl Send for VaapiDecoder {}
|
||||
impl VaapiDecoder {
|
||||
pub(crate) fn new(codec_id: ffmpeg::codec::Id) -> Result<VaapiDecoder> {
|
||||
use ffmpeg::ffi;
|
||||
// SAFETY: a self-contained builder — every allocation below is made here, each result is
|
||||
// checked before the next call uses it, and the ones that survive are moved into the
|
||||
// returned `VaapiDecoder`, which frees each exactly once in `Drop`.
|
||||
unsafe {
|
||||
let mut hw_device: *mut ffi::AVBufferRef = ptr::null_mut();
|
||||
let r = ffi::av_hwdevice_ctx_create(
|
||||
@@ -109,6 +115,9 @@ impl VaapiDecoder {
|
||||
|
||||
pub(crate) fn decode(&mut self, au: &[u8]) -> Result<Option<DmabufFrame>> {
|
||||
use ffmpeg::ffi;
|
||||
// SAFETY: `packet`/`frame`/`ctx` are this decoder's own allocations, live for its whole
|
||||
// lifetime; `au` outlives the synchronous `send_packet` that copies out of it, and every
|
||||
// libav return is checked before the result is used.
|
||||
unsafe {
|
||||
let r = ffi::av_new_packet(self.packet, au.len() as i32);
|
||||
if r < 0 {
|
||||
@@ -147,6 +156,8 @@ impl VaapiDecoder {
|
||||
/// `DRM_FORMAT_NV12`) and flatten every plane across every layer in order (Y then UV).
|
||||
fn map_dmabuf(&mut self) -> Result<DmabufFrame> {
|
||||
use ffmpeg::ffi;
|
||||
// SAFETY: `self.frame` is this decoder's own `AVFrame`, holding a decoded VAAPI surface —
|
||||
// the format check below is what proves that before anything reads the hardware layout.
|
||||
unsafe {
|
||||
if (*self.frame).format != ffi::AVPixelFormat::AV_PIX_FMT_VAAPI as i32 {
|
||||
bail!("decoder returned a software frame (no VAAPI surface)");
|
||||
@@ -247,6 +258,9 @@ fn log_descriptor_once(
|
||||
impl Drop for VaapiDecoder {
|
||||
fn drop(&mut self) {
|
||||
use ffmpeg::ffi;
|
||||
// SAFETY: each pointer is this decoder's own allocation and nothing else holds it; `Drop`
|
||||
// runs exactly once, and each free nulls the pointer through its `&mut`, so none can be
|
||||
// released twice. Freed packet-then-frame-then-context, the order libav documents.
|
||||
unsafe {
|
||||
ffi::av_packet_free(&mut self.packet);
|
||||
ffi::av_frame_free(&mut self.frame);
|
||||
|
||||
@@ -113,6 +113,10 @@ impl VulkanDecoder {
|
||||
vk: &VulkanDecodeDevice,
|
||||
) -> Result<VulkanDecoder> {
|
||||
use ffmpeg::ffi;
|
||||
// SAFETY: a self-contained builder — every allocation is made here and null-checked before
|
||||
// use, the `AVVulkanDeviceContext` fields are filled from `vk`'s live handles and from
|
||||
// `_ctx_storage`, which the decoder keeps alive alongside the context, and what survives is
|
||||
// moved into the returned `VulkanDecoder`, which frees each exactly once in `Drop`.
|
||||
unsafe {
|
||||
let mut hw_device =
|
||||
ffi::av_hwdevice_ctx_alloc(ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_VULKAN);
|
||||
@@ -273,6 +277,9 @@ impl VulkanDecoder {
|
||||
|
||||
pub(crate) fn decode(&mut self, au: &[u8]) -> Result<Option<VkVideoFrame>> {
|
||||
use ffmpeg::ffi;
|
||||
// SAFETY: `packet`/`frame`/`ctx` are this decoder's own allocations, live for its whole
|
||||
// lifetime; `au` outlives the synchronous `send_packet` that copies out of it, and every
|
||||
// libav return is checked before the result is used.
|
||||
unsafe {
|
||||
let r = ffi::av_new_packet(self.packet, au.len() as i32);
|
||||
if r < 0 {
|
||||
@@ -327,6 +334,9 @@ impl VulkanDecoder {
|
||||
/// at its own submit time.
|
||||
fn extract(&mut self) -> Result<VkVideoFrame> {
|
||||
use ffmpeg::ffi;
|
||||
// SAFETY: `self.frame` is this decoder's own `AVFrame`; the format check below is what
|
||||
// proves it carries an `AVVkFrame` before anything reads the Vulkan image out of it, and
|
||||
// the clone handed onward keeps the image + frames context alive through present.
|
||||
unsafe {
|
||||
if (*self.frame).format != ffi::AVPixelFormat::AV_PIX_FMT_VULKAN as i32 {
|
||||
bail!("decoder returned a non-Vulkan frame");
|
||||
@@ -387,6 +397,9 @@ impl VulkanDecoder {
|
||||
impl Drop for VulkanDecoder {
|
||||
fn drop(&mut self) {
|
||||
use ffmpeg::ffi;
|
||||
// SAFETY: each pointer is this decoder's own allocation and nothing else holds it; `Drop`
|
||||
// runs exactly once, and each free nulls the pointer through its `&mut`, so none can be
|
||||
// released twice. Freed packet-then-frame-then-context, the order libav documents.
|
||||
unsafe {
|
||||
ffi::av_packet_free(&mut self.packet);
|
||||
ffi::av_frame_free(&mut self.frame);
|
||||
@@ -406,6 +419,9 @@ unsafe extern "C" fn pick_vulkan(
|
||||
mut list: *const ffmpeg::ffi::AVPixelFormat,
|
||||
) -> ffmpeg::ffi::AVPixelFormat {
|
||||
use ffmpeg::ffi;
|
||||
// SAFETY: libav calls this `get_format` callback with a list it owns, terminated by
|
||||
// `AV_PIX_FMT_NONE` — the walk stops at that terminator, so it stays inside the array, and it
|
||||
// only reads.
|
||||
unsafe {
|
||||
let mut offered = false;
|
||||
while *list != ffi::AVPixelFormat::AV_PIX_FMT_NONE {
|
||||
|
||||
Reference in New Issue
Block a user