From fd8a062e2c219d500399103ec34948c60f0387f5 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 16 Jul 2026 13:16:45 +0200 Subject: [PATCH] refactor(host): hoist the libav poll_encoder drain + PollOutcome into encode/libav.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three libavcodec backends each carried a byte-identical single-packet receive_packet drain. Move it once into the shared Tier-2 glue as poll_encoder -> PollOutcome (the richest form: Packet / Again / Eof), and have the callers narrow it: - Linux NVENC (encode/linux/mod.rs): poll() matches the shared fn, collapsing Again|Eof to Ok(None) — was an inlined match, now one call. - VAAPI (encode/linux/vaapi.rs): drop the local poll_encoder; the blocking budget loop lets Again|Eof fall through to the deadline check, byte-identical to the old Option::None path. - Windows AMF/QSV (encode/windows/ffmpeg_win.rs): drop the local PollOutcome + poll_encoder; its deadline-driven drain already matches PollOutcome, so only the import changes. No behavior change on any backend. Still a plain monomorphic free fn over a borrowed &mut Encoder — no new per-frame dyn/Box/alloc; the only allocation is the same bitstream to_vec() each path already made. Drops the now-unused ffmpeg::Packet import from all three. Linux check + clippy green (nvenc,vulkan-encode,pyrowave) on RTX 5070 Ti; ffmpeg_win.rs is Windows-cfg, pending .173 compile. Co-Authored-By: Claude Fable 5 --- crates/punktfunk-host/src/encode/libav.rs | 43 +++++++++++++++++++ crates/punktfunk-host/src/encode/linux/mod.rs | 33 +++----------- .../punktfunk-host/src/encode/linux/vaapi.rs | 41 +++++------------- .../src/encode/windows/ffmpeg_win.rs | 40 ++--------------- 4 files changed, 64 insertions(+), 93 deletions(-) diff --git a/crates/punktfunk-host/src/encode/libav.rs b/crates/punktfunk-host/src/encode/libav.rs index a078cb3b..12c5c7b0 100644 --- a/crates/punktfunk-host/src/encode/libav.rs +++ b/crates/punktfunk-host/src/encode/libav.rs @@ -3,8 +3,12 @@ //! (`encode/windows/ffmpeg_win.rs`) — so the byte-identical pieces live once (plan §2.2, the Tier-2 //! gap). Free functions and consts over borrowed handles; nothing here is per-frame `dyn`, //! allocating, or on the zero-copy ingest path. +use crate::encode::EncodedFrame; +use anyhow::{Context, Result}; +use ffmpeg_next as ffmpeg; use ffmpeg_next::ffi; // = ffmpeg_sys_next use ffmpeg_next::format::Pixel; +use ffmpeg_next::{encoder, Packet}; use std::os::raw::c_int; /// swscale: nearest-neighbour scaler flag (`SWS_POINT`). We never rescale (src dims == dst dims), so @@ -21,3 +25,42 @@ pub(crate) const SWS_CS_BT2020: c_int = 9; pub(crate) fn pixel_to_av(p: Pixel) -> ffi::AVPixelFormat { ffi::AVPixelFormat::from(p) } + +/// One `receive_packet` attempt, with the not-ready states kept distinct so a blocking drain can +/// tell "still encoding" (retry) from "stream over" (stop). The Linux NVENC/VAAPI polls collapse +/// `Again`/`Eof` to `None`; the Windows AMF/QSV path keeps them apart for its deadline-driven loop. +pub(crate) enum PollOutcome { + Packet(EncodedFrame), + Again, + Eof, +} + +/// Drain the encoder for one packet (shared across the NVENC/VAAPI/AMF/QSV libav backends). The +/// `EncodedFrame`'s only allocation is the `to_vec()` of the bitstream — the same copy each backend +/// already made — so this stays off any per-frame `dyn`/`Box`/channel path. +pub(crate) fn poll_encoder(enc: &mut encoder::video::Encoder, fps: u32) -> Result { + let mut pkt = Packet::empty(); + match enc.receive_packet(&mut pkt) { + Ok(()) => { + let data = pkt.data().map(|d| d.to_vec()).unwrap_or_default(); + let pts = pkt.pts().unwrap_or(0).max(0) as u64; + Ok(PollOutcome::Packet(EncodedFrame { + data, + pts_ns: pts * 1_000_000_000 / fps as u64, + keyframe: pkt.is_key(), + recovery_anchor: false, + chunk_aligned: false, + })) + } + // No packet ready yet (need another input frame). + Err(ffmpeg::Error::Other { errno }) + if errno == ffmpeg::util::error::EAGAIN + || errno == ffmpeg::util::error::EWOULDBLOCK => + { + Ok(PollOutcome::Again) + } + // Fully drained after flush(). + Err(ffmpeg::Error::Eof) => Ok(PollOutcome::Eof), + Err(e) => Err(e).context("receive_packet"), + } +} diff --git a/crates/punktfunk-host/src/encode/linux/mod.rs b/crates/punktfunk-host/src/encode/linux/mod.rs index b4620788..792ab1f2 100644 --- a/crates/punktfunk-host/src/encode/linux/mod.rs +++ b/crates/punktfunk-host/src/encode/linux/mod.rs @@ -16,12 +16,12 @@ use crate::capture::{CapturedFrame, FramePayload, PixelFormat}; use anyhow::{anyhow, bail, Context, Result}; use ffmpeg::format::Pixel; use ffmpeg::util::frame::Video as VideoFrame; -use ffmpeg::{codec, encoder, Dictionary, Packet, Rational}; +use ffmpeg::{codec, encoder, Dictionary, Rational}; use ffmpeg_next as ffmpeg; use std::os::raw::c_int; use std::ptr; -use super::libav::{pixel_to_av, SWS_CS_ITU709, SWS_POINT}; +use super::libav::{pixel_to_av, poll_encoder, PollOutcome, SWS_CS_ITU709, SWS_POINT}; use ffmpeg::ffi; // = ffmpeg_sys_next /// The swscale *source* pixel format for a captured packed RGB/BGR layout (the real byte order, not @@ -624,30 +624,11 @@ impl Encoder for NvencEncoder { } fn poll(&mut self) -> Result> { - let mut pkt = Packet::empty(); - match self.enc.receive_packet(&mut pkt) { - Ok(()) => { - let data = pkt.data().map(|d| d.to_vec()).unwrap_or_default(); - let pts = pkt.pts().unwrap_or(0).max(0) as u64; - let pts_ns = pts * 1_000_000_000 / self.fps as u64; - Ok(Some(EncodedFrame { - data, - pts_ns, - keyframe: pkt.is_key(), - recovery_anchor: false, - chunk_aligned: false, - })) - } - // No packet ready yet (need another input frame). - Err(ffmpeg::Error::Other { errno }) - if errno == ffmpeg::util::error::EAGAIN - || errno == ffmpeg::util::error::EWOULDBLOCK => - { - Ok(None) - } - // Fully drained after flush(). - Err(ffmpeg::Error::Eof) => Ok(None), - Err(e) => Err(e).context("receive_packet"), + // Non-blocking single drain: a packet ships, EAGAIN (need another input frame) and EOF + // (drained after flush) both mean "nothing this tick". + match poll_encoder(&mut self.enc, self.fps)? { + PollOutcome::Packet(au) => Ok(Some(au)), + PollOutcome::Again | PollOutcome::Eof => Ok(None), } } diff --git a/crates/punktfunk-host/src/encode/linux/vaapi.rs b/crates/punktfunk-host/src/encode/linux/vaapi.rs index 94444539..8775c830 100644 --- a/crates/punktfunk-host/src/encode/linux/vaapi.rs +++ b/crates/punktfunk-host/src/encode/linux/vaapi.rs @@ -26,7 +26,7 @@ use super::{Codec, EncodedFrame, Encoder}; use crate::capture::{CapturedFrame, DmabufFrame, FramePayload, PixelFormat}; use anyhow::{anyhow, bail, Context, Result}; use ffmpeg::format::Pixel; -use ffmpeg::{codec, encoder, Dictionary, Packet, Rational}; +use ffmpeg::{codec, encoder, Dictionary, Rational}; use ffmpeg_next as ffmpeg; use std::ffi::{CStr, CString}; use std::os::fd::AsRawFd; @@ -34,7 +34,7 @@ use std::os::raw::c_int; use std::ptr; use std::sync::atomic::{AtomicU8, Ordering}; -use super::libav::{pixel_to_av, SWS_CS_ITU709, SWS_POINT}; +use super::libav::{pixel_to_av, poll_encoder, PollOutcome, SWS_CS_ITU709, SWS_POINT}; use ffmpeg::ffi; // = ffmpeg_sys_next /// `fourcc(a,b,c,d)` — DRM FourCC packing (`a | b<<8 | c<<16 | d<<24`). @@ -271,32 +271,6 @@ pub fn probe_can_encode_444(_codec: Codec) -> bool { false } -/// Drain the encoder for one packet (shared poll logic). -fn poll_encoder(enc: &mut encoder::video::Encoder, fps: u32) -> Result> { - let mut pkt = Packet::empty(); - match enc.receive_packet(&mut pkt) { - Ok(()) => { - let data = pkt.data().map(|d| d.to_vec()).unwrap_or_default(); - let pts = pkt.pts().unwrap_or(0).max(0) as u64; - Ok(Some(EncodedFrame { - data, - pts_ns: pts * 1_000_000_000 / fps as u64, - keyframe: pkt.is_key(), - recovery_anchor: false, - chunk_aligned: false, - })) - } - Err(ffmpeg::Error::Other { errno }) - if errno == ffmpeg::util::error::EAGAIN - || errno == ffmpeg::util::error::EWOULDBLOCK => - { - Ok(None) - } - Err(ffmpeg::Error::Eof) => Ok(None), - Err(e) => Err(e).context("receive_packet"), - } -} - // --------------------------------------------------------------------------------------------- // CPU upload path (Phase 1): swscale RGB→NV12 → upload into a pooled VA surface → encode. // --------------------------------------------------------------------------------------------- @@ -1143,9 +1117,14 @@ impl Encoder for VaapiEncoder { .min(std::time::Duration::from_millis(12)); let deadline = std::time::Instant::now() + budget; loop { - if let Some(au) = poll_encoder(enc, self.fps)? { - self.in_flight = self.in_flight.saturating_sub(1); - return Ok(Some(au)); + match poll_encoder(enc, self.fps)? { + PollOutcome::Packet(au) => { + self.in_flight = self.in_flight.saturating_sub(1); + return Ok(Some(au)); + } + // No AU yet (EAGAIN) or drained (EOF) both fall through to the budget check, + // exactly as the previous `Option::None` did. + PollOutcome::Again | PollOutcome::Eof => {} } // Nothing ready: only wait when a frame is actually in flight (a drained/EOF'd // encoder must not spin the budget), and give the ASIC ~250 µs between checks. diff --git a/crates/punktfunk-host/src/encode/windows/ffmpeg_win.rs b/crates/punktfunk-host/src/encode/windows/ffmpeg_win.rs index 60c02e57..1c028ea7 100644 --- a/crates/punktfunk-host/src/encode/windows/ffmpeg_win.rs +++ b/crates/punktfunk-host/src/encode/windows/ffmpeg_win.rs @@ -44,7 +44,7 @@ use super::{ChromaFormat, Codec, EncodedFrame, Encoder}; use crate::capture::{dxgi::D3d11Frame, CapturedFrame, FramePayload, PixelFormat}; use anyhow::{anyhow, bail, Context, Result}; use ffmpeg::format::Pixel; -use ffmpeg::{codec, encoder, Dictionary, Packet, Rational}; +use ffmpeg::{codec, encoder, Dictionary, Rational}; use ffmpeg_next as ffmpeg; use std::os::raw::{c_int, c_uint, c_void}; use std::ptr; @@ -60,7 +60,9 @@ use windows::Win32::Graphics::Dxgi::Common::{ DXGI_FORMAT_R10G10B10A2_UNORM, DXGI_SAMPLE_DESC, }; -use super::libav::{pixel_to_av, SWS_CS_BT2020, SWS_CS_ITU709, SWS_POINT}; +use super::libav::{ + pixel_to_av, poll_encoder, PollOutcome, SWS_CS_BT2020, SWS_CS_ITU709, SWS_POINT, +}; use ffmpeg::ffi; // = ffmpeg_sys_next /// `AVD3D11VADeviceContext` (libavutil/hwcontext_d3d11va.h) — mirrored (the ffmpeg-sys bindings @@ -305,40 +307,6 @@ pub fn probe_can_encode(vendor: WinVendor, codec: Codec) -> bool { } } -/// One `receive_packet` attempt, with the not-ready states kept distinct so the blocking poll -/// below can tell "still encoding" (retry) from "stream over" (stop). -enum PollOutcome { - Packet(EncodedFrame), - Again, - Eof, -} - -/// Drain the encoder for one packet (shared poll logic, identical to the VAAPI/NVENC paths). -fn poll_encoder(enc: &mut encoder::video::Encoder, fps: u32) -> Result { - let mut pkt = Packet::empty(); - match enc.receive_packet(&mut pkt) { - Ok(()) => { - let data = pkt.data().map(|d| d.to_vec()).unwrap_or_default(); - let pts = pkt.pts().unwrap_or(0).max(0) as u64; - Ok(PollOutcome::Packet(EncodedFrame { - data, - pts_ns: pts * 1_000_000_000 / fps as u64, - keyframe: pkt.is_key(), - recovery_anchor: false, - chunk_aligned: false, - })) - } - Err(ffmpeg::Error::Other { errno }) - if errno == ffmpeg::util::error::EAGAIN - || errno == ffmpeg::util::error::EWOULDBLOCK => - { - Ok(PollOutcome::Again) - } - Err(ffmpeg::Error::Eof) => Ok(PollOutcome::Eof), - Err(e) => Err(e).context("receive_packet"), - } -} - /// The immediate context of an `ID3D11Device` (for `CopyResource`/`CopySubresourceRegion`). unsafe fn immediate_context(device: &ID3D11Device) -> ID3D11DeviceContext { // windows-rs 0.62: the inherent method takes no args and returns the context (the OutRef form is