refactor(host/W6.2): extract the video encode backends into the pf-encode crate
encode.rs + encode/* (NVENC, VAAPI, native AMF, AMF/QSV ffmpeg, direct-SDK NVENC/CUDA, raw Vulkan-Video, PyroWave, openh264) move into crates/pf-encode behind one Encoder trait + open_video selector (plan §W6). The crate speaks the shared frame vocabulary (pf-frame: CapturedFrame/PixelFormat + the DXGI identity D3d11Frame/make_device) and pf-zerocopy (CUDA context/buffers), and NEVER pf-capture — the capture→encode edge is one-way (ZeroCopyPolicy, prior commit). Dep moves: the heavy encoder deps (ffmpeg-next, the NVENC SDK, openh264, pyrowave-sys) move from the host to pf-encode; the host's nvenc/amf-qsv/vulkan-encode/pyrowave features now FORWARD to pf-encode/*. The host keeps a mod-encode shim (pub use pf_encode) so every crate::encode::* path (negotiator + GameStream/native/mgmt planes) is unchanged. resolve_render_adapter_luid moves from the host's windows/win_adapter.rs into pf-gpu (both pf-encode and pf-capture need it as a peer of GPU selection); its 5 call sites (encode amf/nvenc, capture idd_push/synthetic_nv12, vdisplay manager) rewire to pf_gpu::resolve_render_adapter_luid and win_adapter.rs is deleted. pf-frame's make_device gains a # Safety section (public-unsafe-fn lint, latent since the pf-frame carve — a full-workspace -D warnings clippy catches it). Verified: Linux clippy -D warnings (pf-encode + host nvenc,vulkan-encode,pyrowave --all-targets) + 13/13 pf-encode + 299/299 host tests; Windows clippy -D warnings (pf-encode nvenc,amf-qsv --all-targets + host nvenc,amf-qsv --all-targets) Finished exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -65,8 +65,8 @@ use windows::Win32::UI::WindowsAndMessaging::{GetCursorPos, SetCursorPos};
|
||||
// `DRV_STATUS_*` codes and the channel-delivery struct — lives in `pf_driver_proto`; both sides
|
||||
// `use` it, so a layout/code drift is a compile error (the proto has `const` size asserts).
|
||||
use frame::{
|
||||
SharedHeader, DRV_STATUS_BIND_FAIL, DRV_STATUS_NO_DEVICE1, DRV_STATUS_OPENED,
|
||||
DRV_STATUS_TEX_FAIL, MAGIC, RING_LEN, VERSION,
|
||||
unpack_opened_detail, SharedHeader, DRV_STATUS_BIND_FAIL, DRV_STATUS_NONE,
|
||||
DRV_STATUS_NO_DEVICE1, DRV_STATUS_OPENED, DRV_STATUS_TEX_FAIL, MAGIC, RING_LEN, VERSION,
|
||||
};
|
||||
|
||||
/// `DXGI_SHARED_RESOURCE_READ | _WRITE` for `CreateSharedHandle`/`OpenSharedResourceByName`. Local (not
|
||||
@@ -561,7 +561,7 @@ impl IddPushCapturer {
|
||||
// the driver HAVE drifted — identical twin GPUs whose max-VRAM tie moved between ADD and
|
||||
// this open, or a stale kept monitor across an adapter re-init — the driver reports
|
||||
// TEX_FAIL plus the adapter it actually renders on, and the rebind below reopens on that.
|
||||
let luid = crate::win_adapter::resolve_render_adapter_luid().unwrap_or(LUID {
|
||||
let luid = pf_gpu::resolve_render_adapter_luid().unwrap_or(LUID {
|
||||
LowPart: (target.adapter_luid & 0xffff_ffff) as u32,
|
||||
HighPart: (target.adapter_luid >> 32) as i32,
|
||||
});
|
||||
@@ -935,15 +935,63 @@ impl IddPushCapturer {
|
||||
}
|
||||
if Instant::now() > deadline {
|
||||
bail!(
|
||||
"IDD-push: driver_status={st} but no frame published within 4s (despite compose \
|
||||
kicks) — the virtual display is likely in a format/size the ring can't match \
|
||||
(fullscreen game?); falling back"
|
||||
"IDD-push: no frame published within 4s (despite compose kicks) — {}; \
|
||||
falling back",
|
||||
self.no_first_frame_diagnosis(st)
|
||||
);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
}
|
||||
|
||||
/// Name a first-frame timeout from the driver's own evidence — `driver_status` plus the live
|
||||
/// OPENED detail word (proto `pack_opened_detail`) — instead of guessing. The three no-frames
|
||||
/// states look identical from the host side but have disjoint causes and fixes; the lid-closed
|
||||
/// field report burned days for lack of exactly this line. Appends a console-session hint when
|
||||
/// the host itself is in the wrong session (display writes + input kicks can't work from there).
|
||||
fn no_first_frame_diagnosis(&self, st: u32) -> String {
|
||||
let what = match st {
|
||||
// The delivery was never consumed: no swap-chain worker ran for this monitor at all.
|
||||
DRV_STATUS_NONE => "the driver never attached — the channel delivery was never \
|
||||
consumed, so the OS ran no swap-chain worker for this monitor (display not \
|
||||
composed at all: console display-off / modern standby, or the mode commit \
|
||||
never reached the adapter)"
|
||||
.to_string(),
|
||||
DRV_STATUS_OPENED => {
|
||||
// SAFETY: in-bounds, aligned u32 read of the live, owned shared-header mapping
|
||||
// (same best-effort diagnostic access as the `driver_status` read in the caller);
|
||||
// no reference into the shared region is formed.
|
||||
let detail = unsafe { (*self.header).driver_status_detail };
|
||||
match unpack_opened_detail(detail) {
|
||||
Some((0, _)) => "driver attached with a live swap-chain, but DWM composed \
|
||||
ZERO frames — an undamaged or powered-off desktop, and the compose \
|
||||
kicks didn't bite (synthetic input is blocked on the secure desktop)"
|
||||
.to_string(),
|
||||
Some((offered, mismatched)) => format!(
|
||||
"driver attached and DWM composed {offered} frame(s), but none matched \
|
||||
the ring — {mismatched} dropped for a size/format mismatch (the \
|
||||
display's actual mode differs from what the host sized the ring to: \
|
||||
a mid-open mode-set, a fullscreen game, or a stale GDI view)"
|
||||
),
|
||||
// A pre-detail driver never stamps the live bit — say so rather than guess.
|
||||
None => "driver attached but published nothing; this pf-vdisplay build \
|
||||
predates attach diagnostics, so the cause can't be named — update the \
|
||||
driver for a precise line here"
|
||||
.to_string(),
|
||||
}
|
||||
}
|
||||
other => format!("driver_status={other} (unexpected at this point)"),
|
||||
};
|
||||
match crate::interactive::console_session_mismatch() {
|
||||
Some((own, console)) => format!(
|
||||
"{what} [host is in session {own} but the console is session {console} — display \
|
||||
writes and input kicks cannot work from a non-console session; reconnect the \
|
||||
console or run via the installed service]"
|
||||
),
|
||||
None => what,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn latest(&self) -> u64 {
|
||||
// SAFETY: `self.header` is the live, owned shared-header mapping (page-aligned, sized for a
|
||||
|
||||
@@ -140,7 +140,7 @@ impl Capturer for SyntheticNv12Capturer {
|
||||
/// Calls DXGI factory/adapter enumeration; returns owned COM objects or an error.
|
||||
unsafe fn resolve_render_adapter() -> Result<IDXGIAdapter1> {
|
||||
let factory: IDXGIFactory4 = CreateDXGIFactory1().context("CreateDXGIFactory1")?;
|
||||
if let Some(luid) = crate::win_adapter::resolve_render_adapter_luid() {
|
||||
if let Some(luid) = pf_gpu::resolve_render_adapter_luid() {
|
||||
if let Ok(a) = factory.EnumAdapterByLuid::<IDXGIAdapter1>(luid) {
|
||||
return Ok(a);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,424 +0,0 @@
|
||||
//! The encoder contract (plan §7, Tier 1): the [`Encoder`] trait plus the plain-data value types its
|
||||
//! signatures use — [`EncodedFrame`], [`Codec`], [`ChromaFormat`], [`EncoderCaps`] — and the
|
||||
//! dimension/VBV helpers [`validate_dimensions`] and [`vbv_frames_env`]. Backend selection, the
|
||||
//! capability probes that mirror it, and `Codec::host_wire_caps` stay in the parent [`crate::encode`]
|
||||
//! facade, which re-exports this module (`pub(crate) use codec::*;`) so every `crate::encode::*` path
|
||||
//! is unchanged.
|
||||
use crate::capture::CapturedFrame;
|
||||
use anyhow::Result;
|
||||
|
||||
/// An encoded access unit (one NAL/AU) to hand to `punktfunk_core` for FEC + packetization.
|
||||
/// `data` is in-band Annex-B (the encoder is opened without a global header), so each
|
||||
/// keyframe carries its own VPS/SPS/PPS — the bytes are both a playable elementary
|
||||
/// stream and a self-contained AU for the wire.
|
||||
pub struct EncodedFrame {
|
||||
pub data: Vec<u8>,
|
||||
pub pts_ns: u64,
|
||||
/// True for IDR/keyframes (sets the SOF/keyframe wire flags).
|
||||
pub keyframe: bool,
|
||||
/// True when this AU is a **reference-frame-invalidation recovery frame** — a clean P-frame the
|
||||
/// encoder coded against a known-good reference in response to
|
||||
/// [`invalidate_ref_frames`](Encoder::invalidate_ref_frames). The pump tags it
|
||||
/// [`punktfunk_core::packet::USER_FLAG_RECOVERY_ANCHOR`] so the client lifts its post-loss
|
||||
/// freeze on it without an IDR. Set by BOTH RFI backends: native AMF (the LTR force-reference
|
||||
/// frame) and Windows direct-NVENC (the first frame encoded after `nvEncInvalidateRefFrames` —
|
||||
/// the invalidation applies at the next `encode_picture`, so that AU is by construction the
|
||||
/// clean re-anchor). Without it the client's freeze can only lift on an IDR — which the host
|
||||
/// suppresses after a successful RFI (the cooldown), a ~1 s frozen stall per loss event.
|
||||
pub recovery_anchor: bool,
|
||||
/// The AU is shard-aligned self-delimiting chunks (see [`Encoder::set_wire_chunking`]);
|
||||
/// the session stamps [`punktfunk_core::packet::USER_FLAG_CHUNK_ALIGNED`] so the client
|
||||
/// windows its parse and may opt into partial delivery. Only the PyroWave backend sets it.
|
||||
pub chunk_aligned: bool,
|
||||
}
|
||||
|
||||
/// Codec selection negotiated with the client.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Codec {
|
||||
H264,
|
||||
H265,
|
||||
Av1,
|
||||
/// PyroWave — the opt-in wired-LAN intra-only wavelet codec (design/pyrowave-codec-plan.md).
|
||||
/// Only ever negotiated via the client's explicit `preferred_codec` (never the precedence
|
||||
/// ladder) and only emitted by the `pyrowave`-feature backend; every AU is a keyframe.
|
||||
PyroWave,
|
||||
}
|
||||
|
||||
/// Chroma subsampling the encoder emits, negotiated with the client (the `PUNKTFUNK_444` gate + the
|
||||
/// client's `VIDEO_CAP_444` + a GPU probe). `Yuv420` is the universal default; `Yuv444` is HEVC-only,
|
||||
/// native-protocol-only (GameStream stays 4:2:0), and the host only ever passes it after
|
||||
/// [`can_encode_444`] confirmed the active backend supports it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
pub enum ChromaFormat {
|
||||
#[default]
|
||||
Yuv420,
|
||||
Yuv444,
|
||||
}
|
||||
|
||||
impl ChromaFormat {
|
||||
/// The HEVC `chroma_format_idc` this maps to: `1` (4:2:0) or `3` (4:4:4). Also the wire value
|
||||
/// echoed in [`punktfunk_core::quic::Welcome::chroma_format`].
|
||||
pub fn idc(self) -> u8 {
|
||||
match self {
|
||||
ChromaFormat::Yuv420 => punktfunk_core::quic::CHROMA_IDC_420,
|
||||
ChromaFormat::Yuv444 => punktfunk_core::quic::CHROMA_IDC_444,
|
||||
}
|
||||
}
|
||||
|
||||
/// True for full-chroma 4:4:4.
|
||||
pub fn is_444(self) -> bool {
|
||||
matches!(self, ChromaFormat::Yuv444)
|
||||
}
|
||||
}
|
||||
|
||||
impl Codec {
|
||||
/// Map a negotiated `quic` codec bit ([`punktfunk_core::quic::CODEC_H264`] etc.) to the encoder
|
||||
/// [`Codec`]. Unknown / `0` → HEVC (the pre-negotiation default). Inverse of [`Codec::to_wire`].
|
||||
pub fn from_wire(bit: u8) -> Codec {
|
||||
match bit {
|
||||
punktfunk_core::quic::CODEC_H264 => Codec::H264,
|
||||
punktfunk_core::quic::CODEC_AV1 => Codec::Av1,
|
||||
punktfunk_core::quic::CODEC_PYROWAVE => Codec::PyroWave,
|
||||
_ => Codec::H265,
|
||||
}
|
||||
}
|
||||
|
||||
/// The single `quic` codec bit for this codec (echoed in [`punktfunk_core::quic::Welcome::codec`]).
|
||||
pub fn to_wire(self) -> u8 {
|
||||
match self {
|
||||
Codec::H264 => punktfunk_core::quic::CODEC_H264,
|
||||
Codec::H265 => punktfunk_core::quic::CODEC_HEVC,
|
||||
Codec::Av1 => punktfunk_core::quic::CODEC_AV1,
|
||||
Codec::PyroWave => punktfunk_core::quic::CODEC_PYROWAVE,
|
||||
}
|
||||
}
|
||||
|
||||
/// Lowercase stats/console label (`"h264"` / `"hevc"` / `"av1"`) — the codec string seeded into
|
||||
/// the web console's session meta ([`crate::stats_recorder::StatsRecorder::register_session`]).
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Codec::H264 => "h264",
|
||||
Codec::H265 => "hevc",
|
||||
Codec::Av1 => "av1",
|
||||
Codec::PyroWave => "pyrowave",
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this codec has a negotiable **10-bit** encode path (HEVC Main10 / AV1 10-bit).
|
||||
/// H.264 is always 8-bit (High10 is neither an NVENC nor a VCN encode mode — negotiation
|
||||
/// never asks), and PyroWave's wavelet path ingests 8-bit. `true` here is only the
|
||||
/// *codec-level* gate: the active GPU/backend must still pass
|
||||
/// [`can_encode_10bit`](crate::encode::can_encode_10bit) before the host negotiates 10-bit.
|
||||
pub fn supports_10bit(self) -> bool {
|
||||
matches!(self, Codec::H265 | Codec::Av1)
|
||||
}
|
||||
|
||||
/// The FFmpeg NVENC encoder name (selected by name, not codec id — the latter would
|
||||
/// pick the software encoder).
|
||||
pub fn nvenc_name(self) -> &'static str {
|
||||
match self {
|
||||
Codec::H264 => "h264_nvenc",
|
||||
Codec::H265 => "hevc_nvenc",
|
||||
Codec::Av1 => "av1_nvenc",
|
||||
// Guarded by the open_video dispatch: a PyroWave session never reaches a
|
||||
// libavcodec backend.
|
||||
Codec::PyroWave => unreachable!("PyroWave has no FFmpeg encoder"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The FFmpeg VAAPI encoder name (AMD via Mesa `radeonsi`, Intel via `iHD`/`i965`). One
|
||||
/// libavcodec encoder per codec covers both vendors — the kernel driver differs, the libva
|
||||
/// userspace API is identical. Selected by name (the codec id would pick the SW encoder).
|
||||
/// AV1 VAAPI encode is narrow (Intel Arc/Xe2+, AMD RDNA3+/RDNA4) — gate it on a capability
|
||||
/// probe, never assume it (see [`open_video`]).
|
||||
pub fn vaapi_name(self) -> &'static str {
|
||||
match self {
|
||||
Codec::H264 => "h264_vaapi",
|
||||
Codec::H265 => "hevc_vaapi",
|
||||
Codec::Av1 => "av1_vaapi",
|
||||
// Guarded by the open_video dispatch: a PyroWave session never reaches a
|
||||
// libavcodec backend.
|
||||
Codec::PyroWave => unreachable!("PyroWave has no FFmpeg encoder"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The FFmpeg AMD **AMF** encoder name (the Windows AMD backend). Selected by name (the codec id
|
||||
/// would pick the software encoder). AV1 (`av1_amf`) is RDNA3+/RX 7000+ — probe, never assume.
|
||||
pub fn amf_name(self) -> &'static str {
|
||||
match self {
|
||||
Codec::H264 => "h264_amf",
|
||||
Codec::H265 => "hevc_amf",
|
||||
Codec::Av1 => "av1_amf",
|
||||
// Guarded by the open_video dispatch: a PyroWave session never reaches a
|
||||
// libavcodec backend.
|
||||
Codec::PyroWave => unreachable!("PyroWave has no FFmpeg encoder"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The FFmpeg Intel **QSV** encoder name (the Windows Intel backend). Selected by name. AV1
|
||||
/// (`av1_qsv`) is Arc/Xe2+; HEVC Main10 is Gen9.5+ — probe, never assume.
|
||||
pub fn qsv_name(self) -> &'static str {
|
||||
match self {
|
||||
Codec::H264 => "h264_qsv",
|
||||
Codec::H265 => "hevc_qsv",
|
||||
Codec::Av1 => "av1_qsv",
|
||||
// Guarded by the open_video dispatch: a PyroWave session never reaches a
|
||||
// libavcodec backend.
|
||||
Codec::PyroWave => unreachable!("PyroWave has no FFmpeg encoder"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Static capabilities an [`Encoder`] declares so the session glue routes loss-recovery and HDR
|
||||
/// plumbing by *query* rather than relying on a method's no-op/`false` default. Cheap `Copy`; fixed
|
||||
/// for the session (an HDR toggle re-initialises the encoder — re-query if that matters).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct EncoderCaps {
|
||||
/// The encoder can perform real reference-frame invalidation — i.e.
|
||||
/// [`invalidate_ref_frames`](Encoder::invalidate_ref_frames) can return `true`. When `false`
|
||||
/// the caller skips that always-`false` call and forces a keyframe directly on loss recovery.
|
||||
/// Two backends implement RFI: Windows direct-NVENC (`nvEncInvalidateRefFrames`) and native
|
||||
/// AMF (user-LTR force-reference, when the driver accepted the LTR slots at open). The
|
||||
/// libavcodec paths (Linux NVENC, VAAPI, QSV) can't express it and always keyframe.
|
||||
pub supports_rfi: bool,
|
||||
/// The encoder emits in-band HDR mastering/CLL SEI from [`set_hdr_meta`](Encoder::set_hdr_meta).
|
||||
/// When `false`, `set_hdr_meta` is a no-op and no in-band grade reaches the client. Only the
|
||||
/// Windows direct-NVENC path attaches it today.
|
||||
pub supports_hdr_metadata: bool,
|
||||
/// The opened encoder is actually producing a full-chroma 4:4:4 (`chroma_format_idc = 3`) stream.
|
||||
/// `false` on every 4:2:0 session (the default) and on a backend that declined 4:4:4. Set by the
|
||||
/// NVENC backends (Linux + Windows). The chroma is committed to the wire (`Welcome::chroma_format`)
|
||||
/// from the pre-open probe, so this is a *post-open cross-check*: the session glue logs loudly if
|
||||
/// the encoder's real chroma disagrees with what was negotiated (the in-band SPS is authoritative
|
||||
/// for the decoder either way).
|
||||
pub chroma_444: bool,
|
||||
/// The encoder runs a periodic **intra-refresh wave** — a moving band of intra blocks that
|
||||
/// re-codes the whole picture over ~0.5 s, no periodic IDR. FEC-unrecoverable loss self-heals as
|
||||
/// the band sweeps, so the session glue rate-limits client keyframe requests instead of answering
|
||||
/// each with a full IDR (the 20-40× frame-size spike that cascades under loss). Linux NVENC / AMF
|
||||
/// set it when `PUNKTFUNK_INTRA_REFRESH` opened the encoder in that mode; VAAPI/QSV/software never
|
||||
/// do. NOTE — the wave carries NO decoder-visible clean-point: FFmpeg never sets `AV_FRAME_FLAG_KEY`
|
||||
/// at a recovery point (H.264 flags key only when `recovery_frame_cnt == 0`; HEVC only on IRAP),
|
||||
/// and AMF emits no recovery-point SEI at all. So this cap ALONE does not let the client lift its
|
||||
/// post-loss freeze without an IDR — that needs [`intra_refresh_recovery`](Self::intra_refresh_recovery).
|
||||
pub intra_refresh: bool,
|
||||
/// The intra-refresh wave is a *validated constrained GDR* — verified on real hardware to fully
|
||||
/// heal a lost picture within one wave period with no residual artifacts. Only then does the host
|
||||
/// tag each wave-boundary AU with [`USER_FLAG_RECOVERY_POINT`](punktfunk_core::packet::USER_FLAG_RECOVERY_POINT),
|
||||
/// so the client can lift its freeze on the second mark (a proven clean re-anchor) instead of
|
||||
/// waiting out its backstop and forcing a full IDR. Default `false` on every backend until on-glass
|
||||
/// validation flips it — an un-validated encoder keeps the IDR recovery path, so this is inert and
|
||||
/// cannot regress. Meaningless unless [`intra_refresh`](Self::intra_refresh) is also set.
|
||||
pub intra_refresh_recovery: bool,
|
||||
/// Length of the intra-refresh wave in frames — the boundary period the host marks on (it sets
|
||||
/// `USER_FLAG_RECOVERY_POINT` on every Nth emitted AU, re-phased at each IDR). 0 when intra-refresh
|
||||
/// is off. Only consulted when [`intra_refresh_recovery`](Self::intra_refresh_recovery) is set.
|
||||
pub intra_refresh_period: u32,
|
||||
}
|
||||
|
||||
/// A hardware encoder. One per session; runs on the encode thread.
|
||||
pub trait Encoder: Send {
|
||||
fn submit(&mut self, frame: &CapturedFrame) -> Result<()>;
|
||||
/// [`submit`](Self::submit) with the **wire frame index** this frame's AU will carry — the
|
||||
/// number the packetizer stamps on it and the client's loss reports/RFI requests name. The
|
||||
/// session glue predicts it exactly as `AUs sent so far + frames in flight` (AUs are emitted
|
||||
/// FIFO, one per submission; anything that would break the prediction — an in-place reset, a
|
||||
/// device-change teardown, an encoder rebuild — forfeits the in-flight frames on BOTH sides
|
||||
/// and clears the encoder's reference state, so stale predictions die with it). The RFI
|
||||
/// backends pin their frame numbering (LTR marks, DPB timestamps) to this so
|
||||
/// [`invalidate_ref_frames`](Self::invalidate_ref_frames) compares client frame numbers
|
||||
/// against the same domain — an encoder-internal counter desyncs from the wire on the first
|
||||
/// mid-stream rebuild (adaptive bitrate steps do this under congestion, exactly when losses
|
||||
/// happen). Default: ignore the index and delegate to `submit` (backends without per-frame
|
||||
/// reference bookkeeping don't care).
|
||||
fn submit_indexed(&mut self, frame: &CapturedFrame, wire_index: u32) -> Result<()> {
|
||||
let _ = wire_index;
|
||||
self.submit(frame)
|
||||
}
|
||||
/// This encoder's static [capabilities](EncoderCaps) (RFI, HDR SEI), so the session glue can
|
||||
/// route by query rather than rely on the no-op/`false` defaults of
|
||||
/// [`invalidate_ref_frames`](Self::invalidate_ref_frames) / [`set_hdr_meta`](Self::set_hdr_meta).
|
||||
/// Default: no optional capabilities (the SDR / libavcodec backends) — only the direct-NVENC
|
||||
/// path overrides it.
|
||||
fn caps(&self) -> EncoderCaps {
|
||||
EncoderCaps::default()
|
||||
}
|
||||
/// Force the next submitted frame to be an IDR keyframe (e.g. after a client
|
||||
/// reference-frame-invalidation request). Default: no-op.
|
||||
fn request_keyframe(&mut self) {}
|
||||
/// Set the source's static HDR mastering metadata (from the capturer). An HDR encoder emits it
|
||||
/// as in-band SEI (`mastering_display_colour_volume` + `content_light_level_info`) on each
|
||||
/// keyframe so any decoder — including stock Moonlight — tone-maps from the source's real grade.
|
||||
/// Default: no-op (SDR encoders / libavcodec paths that don't attach it yet). Cheap to call
|
||||
/// every frame; only the direct-NVENC path consumes it.
|
||||
fn set_hdr_meta(&mut self, _meta: Option<punktfunk_core::quic::HdrMeta>) {}
|
||||
/// Invalidate a contiguous range of previously-encoded reference frames (client frame numbers
|
||||
/// — WIRE frame indexes, the domain [`submit_indexed`](Self::submit_indexed) pins the encoder's
|
||||
/// bookkeeping to) so the encoder re-references an older still-valid frame instead of emitting
|
||||
/// a full IDR. Returns `true` if a real reference invalidation was performed; `false` means the
|
||||
/// encoder couldn't (range older than the DPB/LTR history, or the backend has no RFI) and the
|
||||
/// caller should fall back to [`request_keyframe`](Self::request_keyframe). Default: `false` —
|
||||
/// the Windows direct-NVENC path (`nvEncInvalidateRefFrames`) and native AMF (LTR
|
||||
/// force-reference) implement true RFI; the libavcodec paths can't express it, so they keyframe.
|
||||
fn invalidate_ref_frames(&mut self, _first_frame: i64, _last_frame: i64) -> bool {
|
||||
false
|
||||
}
|
||||
/// Pull the next encoded AU if one is ready.
|
||||
fn poll(&mut self) -> Result<Option<EncodedFrame>>;
|
||||
/// Tear the underlying hardware encoder down and rebuild it in place, keeping the session's
|
||||
/// negotiated parameters — the encode-stall watchdog's recovery lever (a wedged AMF/QSV
|
||||
/// driver stops emitting AUs or accepting frames without ever returning an error). Returns
|
||||
/// `true` when the encoder was rebuilt: every submitted-but-unpolled frame is forfeited and
|
||||
/// the next submitted frame starts a fresh stream (IDR). Default `false`: the backend has no
|
||||
/// in-place rebuild and the caller must treat the stall as fatal instead.
|
||||
fn reset(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
/// Retarget the encoder's rate control to `bps` (average == max, CBR) **in place** — same
|
||||
/// codec/resolution/fps, only the bitrate and its derived VBV move. Returns `true` when the
|
||||
/// live encoder accepted the change: the reference chain, the in-flight frames and the
|
||||
/// caller's wire-index prediction all survive, so an adaptive-bitrate step costs *nothing* on
|
||||
/// the wire (no IDR, no in-flight forfeit — the whole point vs. a rebuild). `false` = the
|
||||
/// backend can't (or the driver rejected the new rate, e.g. above the codec-level ceiling) —
|
||||
/// the caller falls back to its full rebuild path, which also owns the bitrate clamping.
|
||||
/// Default: no in-place retarget (the libavcodec/software paths).
|
||||
fn reconfigure_bitrate(&mut self, _bps: u64) -> bool {
|
||||
false
|
||||
}
|
||||
/// Wire-chunk the encoder's AUs at the session's shard payload size (the PyroWave
|
||||
/// datagram-aligned mode, plan §4.4): every `shard_payload` window of the emitted AU
|
||||
/// starts a fresh self-delimiting codec packet, zero-padded to the window — so a lost
|
||||
/// datagram costs a few coefficient blocks, not the frame. AUs produced this way are
|
||||
/// flagged [`EncodedFrame::chunk_aligned`] and the session marks them on the wire.
|
||||
/// Default: no-op (the H.26x backends' bitstreams cannot be cut losslessly).
|
||||
fn set_wire_chunking(&mut self, _shard_payload: usize) {}
|
||||
/// Signal end-of-stream. After this, drain the remaining AUs with [`poll`](Self::poll)
|
||||
/// until it returns `None` — NVENC buffers frames internally even at `delay=0`.
|
||||
fn flush(&mut self) -> Result<()>;
|
||||
}
|
||||
|
||||
impl Codec {
|
||||
/// Maximum encodable dimension (px) per side for this codec on NVENC. H.264 tops out at
|
||||
/// 4096 (level constraint); HEVC and AV1 allow 8192. Used to reject out-of-range client
|
||||
/// modes up front (see [`validate_dimensions`]).
|
||||
pub fn max_dimension(self) -> u32 {
|
||||
match self {
|
||||
Codec::H264 => 4096,
|
||||
// PyroWave has no codec-level dimension cap (arbitrary even sizes); 8192 matches the
|
||||
// buffer-math guard the other codecs get.
|
||||
Codec::H265 | Codec::Av1 | Codec::PyroWave => 8192,
|
||||
}
|
||||
}
|
||||
|
||||
/// The codec's *spec* top level/tier bitrate (bits/s) — the usual boundary at which NVENC
|
||||
/// starts rejecting `avcodec_open2` with EINVAL. NOT a hard cap: [`open_video`](crate::encode::
|
||||
/// open_video) probes the actual GPU ceiling by stepping DOWN from the requested bitrate only on
|
||||
/// EINVAL, and uses this purely as the first step-down candidate (so a card that accepts more —
|
||||
/// an RTX 5070 Ti does >1 Gbps HEVC where a 4090 caps at ~800 Mbps — is never clamped to it).
|
||||
/// HEVC Level 6.2 High tier = 800 Mbps; H.264 High level 6.2 ≈ 480 Mbps; AV1's levels allow more.
|
||||
pub fn max_bitrate_bps(self) -> u64 {
|
||||
match self {
|
||||
Codec::H264 => 480_000_000,
|
||||
Codec::H265 => 800_000_000,
|
||||
Codec::Av1 => 1_200_000_000,
|
||||
// No spec level/tier: the rate is a plain per-frame byte budget. Use the protocol's
|
||||
// own bitrate clamp so the step-down probe logic never binds below it.
|
||||
Codec::PyroWave => 8_000_000_000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `PUNKTFUNK_VBV_FRAMES` — HRD/VBV size in frame intervals (default 1.0, the strict low-latency
|
||||
/// shape every backend ships: each frame must fit its rate share, keeping frame sizes uniform for
|
||||
/// the pacer). The AMF/VAAPI/QSV paths parse the same variable locally; this helper brings the
|
||||
/// direct-NVENC paths (which used to hardwire 1 frame) to parity. Larger values let complex
|
||||
/// frames borrow bits — better rate utilization at the cost of per-frame size variance.
|
||||
pub(crate) fn vbv_frames_env() -> f64 {
|
||||
std::env::var("PUNKTFUNK_VBV_FRAMES")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<f64>().ok())
|
||||
.filter(|v| v.is_finite() && *v > 0.0)
|
||||
.unwrap_or(1.0)
|
||||
}
|
||||
|
||||
/// Validate a requested encode resolution before we allocate buffers or open NVENC. Rejects
|
||||
/// zero/odd-sized and out-of-range modes with a clear error instead of letting buffer math
|
||||
/// overflow or the encoder open fail with an opaque NVENC code. A client can request any
|
||||
/// `mode=WxHxFPS`, so this is the gate on attacker/typo-controlled dimensions.
|
||||
pub fn validate_dimensions(codec: Codec, width: u32, height: u32) -> Result<()> {
|
||||
if width == 0 || height == 0 {
|
||||
anyhow::bail!("invalid encode resolution {width}x{height}: dimensions must be non-zero");
|
||||
}
|
||||
// NVENC requires even dimensions for the chroma subsampling it does internally.
|
||||
if width % 2 != 0 || height % 2 != 0 {
|
||||
anyhow::bail!("invalid encode resolution {width}x{height}: dimensions must be even");
|
||||
}
|
||||
// PyroWave's 5-level wavelet decomposition needs ≥ 4·2⁵ px per axis (upstream
|
||||
// `MinimumImageSize` — the band mirroring breaks below it); reject a tiny mode here
|
||||
// (e.g. a match-window resize dragged to a sliver) instead of failing the encoder
|
||||
// rebuild after the switch was acked.
|
||||
if codec == Codec::PyroWave && (width < 128 || height < 128) {
|
||||
anyhow::bail!(
|
||||
"invalid PyroWave resolution {width}x{height}: the wavelet needs at least 128px per axis"
|
||||
);
|
||||
}
|
||||
let max = codec.max_dimension();
|
||||
if width > max || height > max {
|
||||
anyhow::bail!(
|
||||
"{codec:?} max dimension is {max}px; requested {width}x{height} \
|
||||
(use HEVC/AV1 above 4096, or lower the client resolution)"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rejects_zero_and_odd_dimensions() {
|
||||
assert!(validate_dimensions(Codec::H265, 0, 1080).is_err());
|
||||
assert!(validate_dimensions(Codec::H265, 1920, 0).is_err());
|
||||
assert!(validate_dimensions(Codec::H265, 1921, 1080).is_err()); // odd width
|
||||
assert!(validate_dimensions(Codec::H265, 1920, 1081).is_err()); // odd height
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn h264_capped_at_4096() {
|
||||
assert!(validate_dimensions(Codec::H264, 3840, 2160).is_ok()); // 4K fits (width < 4096)
|
||||
assert!(validate_dimensions(Codec::H264, 4096, 4096).is_ok()); // exactly at the limit
|
||||
assert!(validate_dimensions(Codec::H264, 4098, 2160).is_err());
|
||||
assert!(validate_dimensions(Codec::H264, 3840, 4098).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hevc_and_av1_allow_up_to_8192() {
|
||||
for c in [Codec::H265, Codec::Av1] {
|
||||
assert!(validate_dimensions(c, 3840, 2160).is_ok());
|
||||
assert!(validate_dimensions(c, 7680, 4320).is_ok()); // 8K fits
|
||||
assert!(validate_dimensions(c, 8192, 8192).is_ok());
|
||||
assert!(validate_dimensions(c, 8194, 4320).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn common_modes_accepted() {
|
||||
for c in [Codec::H264, Codec::H265, Codec::Av1] {
|
||||
for (w, h) in [(1280, 720), (1920, 1080), (2560, 1440)] {
|
||||
assert!(validate_dimensions(c, w, h).is_ok(), "{c:?} {w}x{h}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wire round-trip and the stats label stay in lockstep with the `quic::CODEC_*` bits.
|
||||
#[test]
|
||||
fn codec_wire_roundtrip_and_label() {
|
||||
for c in [Codec::H264, Codec::H265, Codec::Av1] {
|
||||
assert_eq!(Codec::from_wire(c.to_wire()), c);
|
||||
}
|
||||
assert_eq!(Codec::H264.label(), "h264");
|
||||
assert_eq!(Codec::H265.label(), "hevc");
|
||||
assert_eq!(Codec::Av1.label(), "av1");
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
//! Shared libavcodec (FFmpeg) glue for the three libav encode backends — Linux NVENC
|
||||
//! (`encode/linux/mod.rs`), VAAPI (`encode/linux/vaapi.rs`), and Windows AMF/QSV
|
||||
//! (`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, Rational};
|
||||
use std::os::raw::c_int;
|
||||
|
||||
/// swscale: nearest-neighbour scaler flag (`SWS_POINT`). We never rescale (src dims == dst dims), so
|
||||
/// the resampler choice only governs the colour-conversion path; POINT is the cheapest.
|
||||
pub(crate) const SWS_POINT: c_int = 0x10;
|
||||
/// swscale colorspace id for ITU-R BT.709 (`SWS_CS_ITU709`) — the CSC coefficients for our RGB→YUV.
|
||||
pub(crate) const SWS_CS_ITU709: c_int = 1;
|
||||
/// swscale colorspace id for ITU-R BT.2020 non-constant-luminance (`SWS_CS_BT2020`) — the CSC
|
||||
/// coefficients for the HDR X2BGR10→P010 path (Windows only today).
|
||||
pub(crate) const SWS_CS_BT2020: c_int = 9;
|
||||
|
||||
/// `Pixel` → `AVPixelFormat`. `Pixel` is `#[repr(i32)]`-compatible with `AVPixelFormat` (the bindgen
|
||||
/// enum) via this documented conversion in ffmpeg-next.
|
||||
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,
|
||||
}
|
||||
|
||||
/// Apply the shared low-latency rate-control contract to a **not-yet-opened** encoder context: a
|
||||
/// fixed frame rate, CBR (target == max bitrate), B-frames off, and a tight ~1-frame VBV/HRD buffer.
|
||||
///
|
||||
/// The VBV size bounds any single frame. Under CBR with no buffer set, libav's encoders use a loose
|
||||
/// default VBV, so a high-motion P-frame can balloon to many times the average; those extra packets
|
||||
/// overflow the bounded send queue + kernel socket buffer and get dropped, which the client sees as
|
||||
/// framedrops/jitter (and, on the infinite-GOP path, as old/stale frames flashing until the next
|
||||
/// RFI). A tight ~1-frame buffer makes the encoder hold frame size roughly constant and absorb motion
|
||||
/// as a momentary QP (quality) dip instead — the trade we want. Default = 1 frame of bits
|
||||
/// (bitrate/fps); `PUNKTFUNK_VBV_FRAMES` tunes it (larger = better motion quality, bigger bursts).
|
||||
///
|
||||
/// The caller still owns `set_format` (pixel format) and `gop_size` (GOP policy differs: NVENC's
|
||||
/// infinite/intra-refresh wave vs the VAAPI/AMF `i32::MAX`), since those are backend-specific.
|
||||
pub(crate) fn apply_low_latency_rc(video: &mut encoder::video::Video, fps: u32, bitrate_bps: u64) {
|
||||
video.set_time_base(Rational(1, fps as i32));
|
||||
video.set_frame_rate(Some(Rational(fps as i32, 1)));
|
||||
video.set_bit_rate(bitrate_bps as usize);
|
||||
video.set_max_bit_rate(bitrate_bps as usize);
|
||||
video.set_max_b_frames(0);
|
||||
let vbv_bits = ((bitrate_bps as f64 / fps.max(1) as f64) * crate::encode::vbv_frames_env())
|
||||
.clamp(1.0, i32::MAX as f64);
|
||||
// SAFETY: `video` wraps a freshly-allocated `AVCodecContext` we hold by value and have not opened
|
||||
// yet; `as_mut_ptr()` returns that non-null, aligned, exclusively-owned context. Writing the plain
|
||||
// `rc_buffer_size` int before `open_with` is the supported way to set a field ffmpeg-next exposes
|
||||
// no setter for. Sole owner → no aliasing; synchronous in-bounds scalar write.
|
||||
unsafe {
|
||||
(*video.as_mut_ptr()).rc_buffer_size = vbv_bits as i32;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<PollOutcome> {
|
||||
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"),
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
// Cursor-overlay blend kernels for the CUDA/NVENC path (cursor-as-metadata). The cursor bitmap is
|
||||
// straight-alpha RGBA, row-packed (stride = curW*4). Blended into the encoder-OWNED NVENC input
|
||||
// surface — never the compositor's dmabuf. One thread per cursor pixel (ARGB / YUV444) or per 2x2
|
||||
// chroma block (NV12). Coefficients are BT.709 limited, matching rgb2yuv.comp so the cursor colour
|
||||
// matches the rest of the frame regardless of which zero-copy backend encodes it.
|
||||
//
|
||||
// Build (regenerate cursor_blend.ptx after editing):
|
||||
// nvcc -ptx -arch=compute_75 cursor_blend.cu -o cursor_blend.ptx
|
||||
// PTX is JIT'd by the driver forward to the actual GPU, so a compute_75 (Turing) baseline runs on
|
||||
// every Turing-or-newer NVENC GPU. (CUDA 13's nvcc no longer targets pre-Turing archs.)
|
||||
|
||||
typedef unsigned char u8;
|
||||
|
||||
__device__ __forceinline__ u8 blend8(int dst, int src, int a) {
|
||||
return (u8)((src * a + dst * (255 - a)) / 255);
|
||||
}
|
||||
|
||||
// Packed 4-byte surface. NVENC's ARGB format stores bytes B,G,R,A in memory; the cursor is R,G,B,A.
|
||||
extern "C" __global__ void blend_argb(
|
||||
u8* surf, int pitch, int surfW, int surfH,
|
||||
const u8* cur, int curW, int curH, int ox, int oy)
|
||||
{
|
||||
int cx = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
int cy = blockIdx.y * blockDim.y + threadIdx.y;
|
||||
if (cx >= curW || cy >= curH) return;
|
||||
int px = ox + cx, py = oy + cy;
|
||||
if (px < 0 || py < 0 || px >= surfW || py >= surfH) return;
|
||||
const u8* s = cur + (size_t)(cy * curW + cx) * 4;
|
||||
int a = s[3];
|
||||
if (a == 0) return;
|
||||
u8* d = surf + (size_t)py * pitch + (size_t)px * 4;
|
||||
d[0] = blend8(d[0], s[2], a); // B <- cursor B
|
||||
d[1] = blend8(d[1], s[1], a); // G <- cursor G
|
||||
d[2] = blend8(d[2], s[0], a); // R <- cursor R
|
||||
}
|
||||
|
||||
// Planar YUV444: three full-res planes stacked at base, base+plane, base+2*plane (plane=pitch*surfH).
|
||||
extern "C" __global__ void blend_yuv444(
|
||||
u8* base, int pitch, int surfW, int surfH,
|
||||
const u8* cur, int curW, int curH, int ox, int oy)
|
||||
{
|
||||
int cx = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
int cy = blockIdx.y * blockDim.y + threadIdx.y;
|
||||
if (cx >= curW || cy >= curH) return;
|
||||
int px = ox + cx, py = oy + cy;
|
||||
if (px < 0 || py < 0 || px >= surfW || py >= surfH) return;
|
||||
const u8* s = cur + (size_t)(cy * curW + cx) * 4;
|
||||
int a = s[3];
|
||||
if (a == 0) return;
|
||||
float R = s[0], G = s[1], B = s[2];
|
||||
int Y = (int)(16.0f + 0.1826f * R + 0.6142f * G + 0.0620f * B + 0.5f);
|
||||
int U = (int)(128.0f - 0.1006f * R - 0.3386f * G + 0.4392f * B + 0.5f);
|
||||
int V = (int)(128.0f + 0.4392f * R - 0.3989f * G - 0.0403f * B + 0.5f);
|
||||
size_t plane = (size_t)pitch * surfH;
|
||||
u8* yp = base + (size_t)py * pitch + px;
|
||||
u8* up = base + plane + (size_t)py * pitch + px;
|
||||
u8* vp = base + 2 * plane + (size_t)py * pitch + px;
|
||||
*yp = blend8(*yp, Y, a);
|
||||
*up = blend8(*up, U, a);
|
||||
*vp = blend8(*vp, V, a);
|
||||
}
|
||||
|
||||
// NV12: full-res Y plane + interleaved half-res UV plane. One thread per 2x2 luma block; each blends
|
||||
// up to four Y samples and one (alpha-weighted) UV sample.
|
||||
extern "C" __global__ void blend_nv12(
|
||||
u8* yb, int yPitch, u8* uvb, int uvPitch, int surfW, int surfH,
|
||||
const u8* cur, int curW, int curH, int ox, int oy)
|
||||
{
|
||||
int bx = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
int by = blockIdx.y * blockDim.y + threadIdx.y;
|
||||
int base_cx = bx * 2, base_cy = by * 2;
|
||||
if (base_cx >= curW || base_cy >= curH) return;
|
||||
float ua = 0.0f, va = 0.0f, wa = 0.0f;
|
||||
int cnt = 0;
|
||||
for (int j = 0; j < 2; j++) {
|
||||
for (int i = 0; i < 2; i++) {
|
||||
int cx = base_cx + i, cy = base_cy + j;
|
||||
if (cx >= curW || cy >= curH) continue;
|
||||
int px = ox + cx, py = oy + cy;
|
||||
if (px < 0 || py < 0 || px >= surfW || py >= surfH) continue;
|
||||
const u8* s = cur + (size_t)(cy * curW + cx) * 4;
|
||||
int a = s[3];
|
||||
if (a == 0) continue;
|
||||
float R = s[0], G = s[1], B = s[2];
|
||||
int Y = (int)(16.0f + 0.1826f * R + 0.6142f * G + 0.0620f * B + 0.5f);
|
||||
u8* yp = yb + (size_t)py * yPitch + px;
|
||||
*yp = blend8(*yp, Y, a);
|
||||
ua += (128.0f - 0.1006f * R - 0.3386f * G + 0.4392f * B) * a;
|
||||
va += (128.0f + 0.4392f * R - 0.3989f * G - 0.0403f * B) * a;
|
||||
wa += a;
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
if (wa <= 0.0f || cnt == 0) return;
|
||||
// The chroma sample covering this block's top-left surface pixel.
|
||||
int uvx = (ox + base_cx) / 2;
|
||||
int uvy = (oy + base_cy) / 2;
|
||||
if (uvx < 0 || uvy < 0 || uvx * 2 >= surfW || uvy * 2 >= surfH) return;
|
||||
int U = (int)(ua / wa + 0.5f);
|
||||
int V = (int)(va / wa + 0.5f);
|
||||
int amean = (int)(wa / cnt + 0.5f);
|
||||
u8* uv = uvb + (size_t)uvy * uvPitch + (size_t)uvx * 2;
|
||||
uv[0] = blend8(uv[0], U, amean);
|
||||
uv[1] = blend8(uv[1], V, amean);
|
||||
}
|
||||
@@ -1,576 +0,0 @@
|
||||
//
|
||||
// Generated by NVIDIA NVVM Compiler
|
||||
//
|
||||
// Compiler Build ID: CL-38244171
|
||||
// Cuda compilation tools, release 13.3, V13.3.73
|
||||
// Based on NVVM 7.0.1
|
||||
//
|
||||
|
||||
.version 9.3
|
||||
.target sm_75
|
||||
.address_size 64
|
||||
|
||||
// .globl blend_argb
|
||||
|
||||
.visible .entry blend_argb(
|
||||
.param .u64 blend_argb_param_0,
|
||||
.param .u32 blend_argb_param_1,
|
||||
.param .u32 blend_argb_param_2,
|
||||
.param .u32 blend_argb_param_3,
|
||||
.param .u64 blend_argb_param_4,
|
||||
.param .u32 blend_argb_param_5,
|
||||
.param .u32 blend_argb_param_6,
|
||||
.param .u32 blend_argb_param_7,
|
||||
.param .u32 blend_argb_param_8
|
||||
)
|
||||
{
|
||||
.reg .pred %p<10>;
|
||||
.reg .b16 %rs<2>;
|
||||
.reg .b32 %r<34>;
|
||||
.reg .b64 %rd<17>;
|
||||
|
||||
|
||||
ld.param.u64 %rd2, [blend_argb_param_0];
|
||||
ld.param.u32 %r5, [blend_argb_param_1];
|
||||
ld.param.u32 %r6, [blend_argb_param_2];
|
||||
ld.param.u32 %r7, [blend_argb_param_3];
|
||||
ld.param.u64 %rd3, [blend_argb_param_4];
|
||||
ld.param.u32 %r8, [blend_argb_param_5];
|
||||
ld.param.u32 %r11, [blend_argb_param_6];
|
||||
ld.param.u32 %r9, [blend_argb_param_7];
|
||||
ld.param.u32 %r10, [blend_argb_param_8];
|
||||
mov.u32 %r12, %ntid.x;
|
||||
mov.u32 %r13, %ctaid.x;
|
||||
mov.u32 %r14, %tid.x;
|
||||
mad.lo.s32 %r1, %r13, %r12, %r14;
|
||||
mov.u32 %r15, %ntid.y;
|
||||
mov.u32 %r16, %ctaid.y;
|
||||
mov.u32 %r17, %tid.y;
|
||||
mad.lo.s32 %r2, %r16, %r15, %r17;
|
||||
setp.ge.s32 %p1, %r1, %r8;
|
||||
setp.ge.s32 %p2, %r2, %r11;
|
||||
or.pred %p3, %p1, %p2;
|
||||
@%p3 bra $L__BB0_4;
|
||||
|
||||
add.s32 %r3, %r1, %r9;
|
||||
add.s32 %r4, %r2, %r10;
|
||||
or.b32 %r18, %r4, %r3;
|
||||
setp.lt.s32 %p4, %r18, 0;
|
||||
setp.ge.s32 %p5, %r3, %r6;
|
||||
or.pred %p6, %p5, %p4;
|
||||
setp.ge.s32 %p7, %r4, %r7;
|
||||
or.pred %p8, %p7, %p6;
|
||||
@%p8 bra $L__BB0_4;
|
||||
|
||||
mad.lo.s32 %r19, %r2, %r8, %r1;
|
||||
mul.wide.s32 %rd4, %r19, 4;
|
||||
cvta.to.global.u64 %rd5, %rd3;
|
||||
add.s64 %rd1, %rd5, %rd4;
|
||||
ld.global.u8 %rs1, [%rd1+3];
|
||||
setp.eq.s16 %p9, %rs1, 0;
|
||||
@%p9 bra $L__BB0_4;
|
||||
|
||||
cvt.u32.u16 %r20, %rs1;
|
||||
mul.wide.s32 %rd6, %r4, %r5;
|
||||
mul.wide.s32 %rd7, %r3, 4;
|
||||
add.s64 %rd8, %rd6, %rd7;
|
||||
cvta.to.global.u64 %rd9, %rd2;
|
||||
add.s64 %rd10, %rd9, %rd8;
|
||||
ld.global.u8 %r21, [%rd10];
|
||||
ld.global.u8 %r22, [%rd1+2];
|
||||
xor.b32 %r23, %r20, 255;
|
||||
mul.lo.s32 %r24, %r23, %r21;
|
||||
mad.lo.s32 %r25, %r22, %r20, %r24;
|
||||
mul.wide.u32 %rd11, %r25, -2139062143;
|
||||
shr.u64 %rd12, %rd11, 39;
|
||||
st.global.u8 [%rd10], %rd12;
|
||||
ld.global.u8 %r26, [%rd10+1];
|
||||
ld.global.u8 %r27, [%rd1+1];
|
||||
mul.lo.s32 %r28, %r23, %r26;
|
||||
mad.lo.s32 %r29, %r27, %r20, %r28;
|
||||
mul.wide.u32 %rd13, %r29, -2139062143;
|
||||
shr.u64 %rd14, %rd13, 39;
|
||||
st.global.u8 [%rd10+1], %rd14;
|
||||
ld.global.u8 %r30, [%rd10+2];
|
||||
ld.global.u8 %r31, [%rd1];
|
||||
mul.lo.s32 %r32, %r23, %r30;
|
||||
mad.lo.s32 %r33, %r31, %r20, %r32;
|
||||
mul.wide.u32 %rd15, %r33, -2139062143;
|
||||
shr.u64 %rd16, %rd15, 39;
|
||||
st.global.u8 [%rd10+2], %rd16;
|
||||
|
||||
$L__BB0_4:
|
||||
ret;
|
||||
|
||||
}
|
||||
// .globl blend_yuv444
|
||||
.visible .entry blend_yuv444(
|
||||
.param .u64 blend_yuv444_param_0,
|
||||
.param .u32 blend_yuv444_param_1,
|
||||
.param .u32 blend_yuv444_param_2,
|
||||
.param .u32 blend_yuv444_param_3,
|
||||
.param .u64 blend_yuv444_param_4,
|
||||
.param .u32 blend_yuv444_param_5,
|
||||
.param .u32 blend_yuv444_param_6,
|
||||
.param .u32 blend_yuv444_param_7,
|
||||
.param .u32 blend_yuv444_param_8
|
||||
)
|
||||
{
|
||||
.reg .pred %p<10>;
|
||||
.reg .b16 %rs<5>;
|
||||
.reg .f32 %f<16>;
|
||||
.reg .b32 %r<49>;
|
||||
.reg .b64 %rd<14>;
|
||||
|
||||
|
||||
ld.param.u64 %rd2, [blend_yuv444_param_0];
|
||||
ld.param.u32 %r5, [blend_yuv444_param_1];
|
||||
ld.param.u32 %r6, [blend_yuv444_param_2];
|
||||
ld.param.u32 %r7, [blend_yuv444_param_3];
|
||||
ld.param.u64 %rd3, [blend_yuv444_param_4];
|
||||
ld.param.u32 %r8, [blend_yuv444_param_5];
|
||||
ld.param.u32 %r11, [blend_yuv444_param_6];
|
||||
ld.param.u32 %r9, [blend_yuv444_param_7];
|
||||
ld.param.u32 %r10, [blend_yuv444_param_8];
|
||||
mov.u32 %r12, %ntid.x;
|
||||
mov.u32 %r13, %ctaid.x;
|
||||
mov.u32 %r14, %tid.x;
|
||||
mad.lo.s32 %r1, %r13, %r12, %r14;
|
||||
mov.u32 %r15, %ntid.y;
|
||||
mov.u32 %r16, %ctaid.y;
|
||||
mov.u32 %r17, %tid.y;
|
||||
mad.lo.s32 %r2, %r16, %r15, %r17;
|
||||
setp.ge.s32 %p1, %r1, %r8;
|
||||
setp.ge.s32 %p2, %r2, %r11;
|
||||
or.pred %p3, %p1, %p2;
|
||||
@%p3 bra $L__BB1_4;
|
||||
|
||||
add.s32 %r3, %r1, %r9;
|
||||
add.s32 %r4, %r2, %r10;
|
||||
or.b32 %r18, %r4, %r3;
|
||||
setp.lt.s32 %p4, %r18, 0;
|
||||
setp.ge.s32 %p5, %r3, %r6;
|
||||
or.pred %p6, %p5, %p4;
|
||||
setp.ge.s32 %p7, %r4, %r7;
|
||||
or.pred %p8, %p7, %p6;
|
||||
@%p8 bra $L__BB1_4;
|
||||
|
||||
mad.lo.s32 %r19, %r2, %r8, %r1;
|
||||
mul.wide.s32 %rd4, %r19, 4;
|
||||
cvta.to.global.u64 %rd5, %rd3;
|
||||
add.s64 %rd1, %rd5, %rd4;
|
||||
ld.global.u8 %rs1, [%rd1+3];
|
||||
setp.eq.s16 %p9, %rs1, 0;
|
||||
@%p9 bra $L__BB1_4;
|
||||
|
||||
cvt.u32.u16 %r20, %rs1;
|
||||
ld.global.u8 %rs2, [%rd1];
|
||||
cvt.rn.f32.u16 %f1, %rs2;
|
||||
ld.global.u8 %rs3, [%rd1+1];
|
||||
cvt.rn.f32.u16 %f2, %rs3;
|
||||
ld.global.u8 %rs4, [%rd1+2];
|
||||
cvt.rn.f32.u16 %f3, %rs4;
|
||||
fma.rn.f32 %f4, %f1, 0f3E3AFB7F, 0f41800000;
|
||||
fma.rn.f32 %f5, %f2, 0f3F1D3C36, %f4;
|
||||
fma.rn.f32 %f6, %f3, 0f3D7DF3B6, %f5;
|
||||
add.f32 %f7, %f6, 0f3F000000;
|
||||
cvt.rzi.s32.f32 %r21, %f7;
|
||||
fma.rn.f32 %f8, %f1, 0fBDCE075F, 0f43000000;
|
||||
fma.rn.f32 %f9, %f2, 0fBEAD5CFB, %f8;
|
||||
fma.rn.f32 %f10, %f3, 0f3EE0DED3, %f9;
|
||||
add.f32 %f11, %f10, 0f3F000000;
|
||||
cvt.rzi.s32.f32 %r22, %f11;
|
||||
fma.rn.f32 %f12, %f1, 0f3EE0DED3, 0f43000000;
|
||||
fma.rn.f32 %f13, %f2, 0fBECC3C9F, %f12;
|
||||
fma.rn.f32 %f14, %f3, 0fBD25119D, %f13;
|
||||
add.f32 %f15, %f14, 0f3F000000;
|
||||
cvt.rzi.s32.f32 %r23, %f15;
|
||||
mul.wide.s32 %rd6, %r4, %r5;
|
||||
cvt.s64.s32 %rd7, %r3;
|
||||
add.s64 %rd8, %rd6, %rd7;
|
||||
cvta.to.global.u64 %rd9, %rd2;
|
||||
add.s64 %rd10, %rd9, %rd8;
|
||||
ld.global.u8 %r24, [%rd10];
|
||||
mul.lo.s32 %r25, %r21, %r20;
|
||||
xor.b32 %r26, %r20, 255;
|
||||
mad.lo.s32 %r27, %r26, %r24, %r25;
|
||||
mul.hi.s32 %r28, %r27, -2139062143;
|
||||
add.s32 %r29, %r28, %r27;
|
||||
shr.u32 %r30, %r29, 31;
|
||||
shr.u32 %r31, %r29, 7;
|
||||
add.s32 %r32, %r31, %r30;
|
||||
st.global.u8 [%rd10], %r32;
|
||||
mul.wide.s32 %rd11, %r7, %r5;
|
||||
add.s64 %rd12, %rd10, %rd11;
|
||||
ld.global.u8 %r33, [%rd12];
|
||||
mul.lo.s32 %r34, %r22, %r20;
|
||||
mad.lo.s32 %r35, %r26, %r33, %r34;
|
||||
mul.hi.s32 %r36, %r35, -2139062143;
|
||||
add.s32 %r37, %r36, %r35;
|
||||
shr.u32 %r38, %r37, 31;
|
||||
shr.u32 %r39, %r37, 7;
|
||||
add.s32 %r40, %r39, %r38;
|
||||
st.global.u8 [%rd12], %r40;
|
||||
add.s64 %rd13, %rd12, %rd11;
|
||||
ld.global.u8 %r41, [%rd13];
|
||||
mul.lo.s32 %r42, %r23, %r20;
|
||||
mad.lo.s32 %r43, %r26, %r41, %r42;
|
||||
mul.hi.s32 %r44, %r43, -2139062143;
|
||||
add.s32 %r45, %r44, %r43;
|
||||
shr.u32 %r46, %r45, 31;
|
||||
shr.u32 %r47, %r45, 7;
|
||||
add.s32 %r48, %r47, %r46;
|
||||
st.global.u8 [%rd13], %r48;
|
||||
|
||||
$L__BB1_4:
|
||||
ret;
|
||||
|
||||
}
|
||||
// .globl blend_nv12
|
||||
.visible .entry blend_nv12(
|
||||
.param .u64 blend_nv12_param_0,
|
||||
.param .u32 blend_nv12_param_1,
|
||||
.param .u64 blend_nv12_param_2,
|
||||
.param .u32 blend_nv12_param_3,
|
||||
.param .u32 blend_nv12_param_4,
|
||||
.param .u32 blend_nv12_param_5,
|
||||
.param .u64 blend_nv12_param_6,
|
||||
.param .u32 blend_nv12_param_7,
|
||||
.param .u32 blend_nv12_param_8,
|
||||
.param .u32 blend_nv12_param_9,
|
||||
.param .u32 blend_nv12_param_10
|
||||
)
|
||||
{
|
||||
.reg .pred %p<43>;
|
||||
.reg .b16 %rs<17>;
|
||||
.reg .f32 %f<108>;
|
||||
.reg .b32 %r<123>;
|
||||
.reg .b64 %rd<35>;
|
||||
|
||||
|
||||
ld.param.u64 %rd11, [blend_nv12_param_0];
|
||||
ld.param.u32 %r21, [blend_nv12_param_1];
|
||||
ld.param.u64 %rd10, [blend_nv12_param_2];
|
||||
ld.param.u32 %r22, [blend_nv12_param_3];
|
||||
ld.param.u32 %r23, [blend_nv12_param_4];
|
||||
ld.param.u32 %r24, [blend_nv12_param_5];
|
||||
ld.param.u64 %rd12, [blend_nv12_param_6];
|
||||
ld.param.u32 %r25, [blend_nv12_param_7];
|
||||
ld.param.u32 %r26, [blend_nv12_param_8];
|
||||
ld.param.u32 %r27, [blend_nv12_param_9];
|
||||
ld.param.u32 %r28, [blend_nv12_param_10];
|
||||
cvta.to.global.u64 %rd1, %rd11;
|
||||
cvta.to.global.u64 %rd2, %rd12;
|
||||
mov.u32 %r29, %ntid.x;
|
||||
mov.u32 %r30, %ctaid.x;
|
||||
mov.u32 %r31, %tid.x;
|
||||
mad.lo.s32 %r32, %r30, %r29, %r31;
|
||||
mov.u32 %r33, %ntid.y;
|
||||
mov.u32 %r34, %ctaid.y;
|
||||
mov.u32 %r35, %tid.y;
|
||||
mad.lo.s32 %r36, %r34, %r33, %r35;
|
||||
shl.b32 %r1, %r32, 1;
|
||||
shl.b32 %r2, %r36, 1;
|
||||
setp.ge.s32 %p1, %r1, %r25;
|
||||
setp.ge.s32 %p2, %r2, %r26;
|
||||
or.pred %p3, %p1, %p2;
|
||||
mov.f32 %f102, 0f00000000;
|
||||
mov.f32 %f103, 0f00000000;
|
||||
mov.f32 %f104, 0f00000000;
|
||||
@%p3 bra $L__BB2_19;
|
||||
|
||||
cvt.s64.s32 %rd3, %r21;
|
||||
add.s32 %r3, %r2, %r28;
|
||||
setp.ge.s32 %p4, %r3, %r24;
|
||||
mul.lo.s32 %r4, %r2, %r25;
|
||||
mul.wide.s32 %rd4, %r3, %r21;
|
||||
add.s32 %r5, %r1, %r27;
|
||||
or.b32 %r38, %r5, %r3;
|
||||
setp.lt.s32 %p5, %r38, 0;
|
||||
mov.u32 %r121, 0;
|
||||
setp.ge.s32 %p6, %r5, %r23;
|
||||
or.pred %p7, %p6, %p5;
|
||||
or.pred %p8, %p4, %p7;
|
||||
@%p8 bra $L__BB2_4;
|
||||
|
||||
add.s32 %r40, %r1, %r4;
|
||||
mul.wide.s32 %rd13, %r40, 4;
|
||||
add.s64 %rd5, %rd2, %rd13;
|
||||
ld.global.u8 %rs1, [%rd5+3];
|
||||
setp.eq.s16 %p9, %rs1, 0;
|
||||
@%p9 bra $L__BB2_4;
|
||||
|
||||
cvt.u32.u16 %r42, %rs1;
|
||||
ld.global.u8 %rs5, [%rd5];
|
||||
cvt.rn.f32.u16 %f31, %rs5;
|
||||
ld.global.u8 %rs6, [%rd5+1];
|
||||
cvt.rn.f32.u16 %f32, %rs6;
|
||||
ld.global.u8 %rs7, [%rd5+2];
|
||||
cvt.rn.f32.u16 %f33, %rs7;
|
||||
fma.rn.f32 %f34, %f31, 0f3E3AFB7F, 0f41800000;
|
||||
fma.rn.f32 %f35, %f32, 0f3F1D3C36, %f34;
|
||||
fma.rn.f32 %f36, %f33, 0f3D7DF3B6, %f35;
|
||||
add.f32 %f37, %f36, 0f3F000000;
|
||||
cvt.rzi.s32.f32 %r43, %f37;
|
||||
cvt.s64.s32 %rd14, %r5;
|
||||
add.s64 %rd15, %rd4, %rd14;
|
||||
add.s64 %rd16, %rd1, %rd15;
|
||||
ld.global.u8 %r44, [%rd16];
|
||||
mul.lo.s32 %r45, %r43, %r42;
|
||||
xor.b32 %r46, %r42, 255;
|
||||
mad.lo.s32 %r47, %r46, %r44, %r45;
|
||||
mul.hi.s32 %r48, %r47, -2139062143;
|
||||
add.s32 %r49, %r48, %r47;
|
||||
shr.u32 %r50, %r49, 31;
|
||||
shr.u32 %r51, %r49, 7;
|
||||
add.s32 %r52, %r51, %r50;
|
||||
st.global.u8 [%rd16], %r52;
|
||||
fma.rn.f32 %f38, %f31, 0fBDCE075F, 0f43000000;
|
||||
fma.rn.f32 %f39, %f32, 0fBEAD5CFB, %f38;
|
||||
fma.rn.f32 %f40, %f33, 0f3EE0DED3, %f39;
|
||||
cvt.rn.f32.u16 %f104, %rs1;
|
||||
fma.rn.f32 %f102, %f40, %f104, 0f00000000;
|
||||
fma.rn.f32 %f41, %f31, 0f3EE0DED3, 0f43000000;
|
||||
fma.rn.f32 %f42, %f32, 0fBECC3C9F, %f41;
|
||||
fma.rn.f32 %f43, %f33, 0fBD25119D, %f42;
|
||||
fma.rn.f32 %f103, %f43, %f104, 0f00000000;
|
||||
mov.u32 %r121, 1;
|
||||
|
||||
$L__BB2_4:
|
||||
add.s32 %r7, %r1, 1;
|
||||
setp.ge.s32 %p10, %r7, %r25;
|
||||
@%p10 bra $L__BB2_8;
|
||||
|
||||
add.s32 %r8, %r7, %r27;
|
||||
or.b32 %r53, %r8, %r3;
|
||||
setp.lt.s32 %p12, %r53, 0;
|
||||
setp.ge.s32 %p13, %r8, %r23;
|
||||
or.pred %p14, %p13, %p12;
|
||||
or.pred %p15, %p4, %p14;
|
||||
@%p15 bra $L__BB2_8;
|
||||
|
||||
add.s32 %r54, %r7, %r4;
|
||||
mul.wide.s32 %rd17, %r54, 4;
|
||||
add.s64 %rd6, %rd2, %rd17;
|
||||
ld.global.u8 %rs2, [%rd6+3];
|
||||
setp.eq.s16 %p16, %rs2, 0;
|
||||
@%p16 bra $L__BB2_8;
|
||||
|
||||
cvt.u32.u16 %r55, %rs2;
|
||||
ld.global.u8 %rs8, [%rd6];
|
||||
cvt.rn.f32.u16 %f44, %rs8;
|
||||
ld.global.u8 %rs9, [%rd6+1];
|
||||
cvt.rn.f32.u16 %f45, %rs9;
|
||||
ld.global.u8 %rs10, [%rd6+2];
|
||||
cvt.rn.f32.u16 %f46, %rs10;
|
||||
fma.rn.f32 %f47, %f44, 0f3E3AFB7F, 0f41800000;
|
||||
fma.rn.f32 %f48, %f45, 0f3F1D3C36, %f47;
|
||||
fma.rn.f32 %f49, %f46, 0f3D7DF3B6, %f48;
|
||||
add.f32 %f50, %f49, 0f3F000000;
|
||||
cvt.rzi.s32.f32 %r56, %f50;
|
||||
cvt.s64.s32 %rd18, %r8;
|
||||
add.s64 %rd19, %rd4, %rd18;
|
||||
add.s64 %rd20, %rd1, %rd19;
|
||||
ld.global.u8 %r57, [%rd20];
|
||||
mul.lo.s32 %r58, %r56, %r55;
|
||||
xor.b32 %r59, %r55, 255;
|
||||
mad.lo.s32 %r60, %r59, %r57, %r58;
|
||||
mul.hi.s32 %r61, %r60, -2139062143;
|
||||
add.s32 %r62, %r61, %r60;
|
||||
shr.u32 %r63, %r62, 31;
|
||||
shr.u32 %r64, %r62, 7;
|
||||
add.s32 %r65, %r64, %r63;
|
||||
st.global.u8 [%rd20], %r65;
|
||||
fma.rn.f32 %f51, %f44, 0fBDCE075F, 0f43000000;
|
||||
fma.rn.f32 %f52, %f45, 0fBEAD5CFB, %f51;
|
||||
fma.rn.f32 %f53, %f46, 0f3EE0DED3, %f52;
|
||||
cvt.rn.f32.u16 %f54, %rs2;
|
||||
fma.rn.f32 %f102, %f53, %f54, %f102;
|
||||
fma.rn.f32 %f55, %f44, 0f3EE0DED3, 0f43000000;
|
||||
fma.rn.f32 %f56, %f45, 0fBECC3C9F, %f55;
|
||||
fma.rn.f32 %f57, %f46, 0fBD25119D, %f56;
|
||||
fma.rn.f32 %f103, %f57, %f54, %f103;
|
||||
add.f32 %f104, %f104, %f54;
|
||||
add.s32 %r121, %r121, 1;
|
||||
|
||||
$L__BB2_8:
|
||||
add.s32 %r11, %r2, 1;
|
||||
setp.ge.s32 %p17, %r11, %r26;
|
||||
add.s32 %r12, %r11, %r28;
|
||||
add.s32 %r13, %r4, %r25;
|
||||
cvt.s64.s32 %rd21, %r12;
|
||||
mul.lo.s64 %rd7, %rd21, %rd3;
|
||||
@%p17 bra $L__BB2_12;
|
||||
|
||||
setp.ge.s32 %p18, %r12, %r24;
|
||||
or.b32 %r66, %r5, %r12;
|
||||
setp.lt.s32 %p19, %r66, 0;
|
||||
or.pred %p21, %p6, %p19;
|
||||
or.pred %p22, %p18, %p21;
|
||||
@%p22 bra $L__BB2_12;
|
||||
|
||||
add.s32 %r67, %r1, %r13;
|
||||
mul.wide.s32 %rd22, %r67, 4;
|
||||
add.s64 %rd8, %rd2, %rd22;
|
||||
ld.global.u8 %rs3, [%rd8+3];
|
||||
setp.eq.s16 %p23, %rs3, 0;
|
||||
@%p23 bra $L__BB2_12;
|
||||
|
||||
cvt.u32.u16 %r68, %rs3;
|
||||
ld.global.u8 %rs11, [%rd8];
|
||||
cvt.rn.f32.u16 %f58, %rs11;
|
||||
ld.global.u8 %rs12, [%rd8+1];
|
||||
cvt.rn.f32.u16 %f59, %rs12;
|
||||
ld.global.u8 %rs13, [%rd8+2];
|
||||
cvt.rn.f32.u16 %f60, %rs13;
|
||||
fma.rn.f32 %f61, %f58, 0f3E3AFB7F, 0f41800000;
|
||||
fma.rn.f32 %f62, %f59, 0f3F1D3C36, %f61;
|
||||
fma.rn.f32 %f63, %f60, 0f3D7DF3B6, %f62;
|
||||
add.f32 %f64, %f63, 0f3F000000;
|
||||
cvt.rzi.s32.f32 %r69, %f64;
|
||||
cvt.s64.s32 %rd23, %r5;
|
||||
add.s64 %rd24, %rd7, %rd23;
|
||||
add.s64 %rd25, %rd1, %rd24;
|
||||
ld.global.u8 %r70, [%rd25];
|
||||
mul.lo.s32 %r71, %r69, %r68;
|
||||
xor.b32 %r72, %r68, 255;
|
||||
mad.lo.s32 %r73, %r72, %r70, %r71;
|
||||
mul.hi.s32 %r74, %r73, -2139062143;
|
||||
add.s32 %r75, %r74, %r73;
|
||||
shr.u32 %r76, %r75, 31;
|
||||
shr.u32 %r77, %r75, 7;
|
||||
add.s32 %r78, %r77, %r76;
|
||||
st.global.u8 [%rd25], %r78;
|
||||
fma.rn.f32 %f65, %f58, 0fBDCE075F, 0f43000000;
|
||||
fma.rn.f32 %f66, %f59, 0fBEAD5CFB, %f65;
|
||||
fma.rn.f32 %f67, %f60, 0f3EE0DED3, %f66;
|
||||
cvt.rn.f32.u16 %f68, %rs3;
|
||||
fma.rn.f32 %f102, %f67, %f68, %f102;
|
||||
fma.rn.f32 %f69, %f58, 0f3EE0DED3, 0f43000000;
|
||||
fma.rn.f32 %f70, %f59, 0fBECC3C9F, %f69;
|
||||
fma.rn.f32 %f71, %f60, 0fBD25119D, %f70;
|
||||
fma.rn.f32 %f103, %f71, %f68, %f103;
|
||||
add.f32 %f104, %f104, %f68;
|
||||
add.s32 %r121, %r121, 1;
|
||||
|
||||
$L__BB2_12:
|
||||
or.pred %p26, %p17, %p10;
|
||||
@%p26 bra $L__BB2_16;
|
||||
|
||||
setp.ge.s32 %p27, %r12, %r24;
|
||||
add.s32 %r16, %r7, %r27;
|
||||
or.b32 %r79, %r16, %r12;
|
||||
setp.lt.s32 %p28, %r79, 0;
|
||||
setp.ge.s32 %p29, %r16, %r23;
|
||||
or.pred %p30, %p29, %p28;
|
||||
or.pred %p31, %p27, %p30;
|
||||
@%p31 bra $L__BB2_16;
|
||||
|
||||
add.s32 %r80, %r7, %r13;
|
||||
mul.wide.s32 %rd26, %r80, 4;
|
||||
add.s64 %rd9, %rd2, %rd26;
|
||||
ld.global.u8 %rs4, [%rd9+3];
|
||||
setp.eq.s16 %p32, %rs4, 0;
|
||||
@%p32 bra $L__BB2_16;
|
||||
|
||||
cvt.u32.u16 %r81, %rs4;
|
||||
ld.global.u8 %rs14, [%rd9];
|
||||
cvt.rn.f32.u16 %f72, %rs14;
|
||||
ld.global.u8 %rs15, [%rd9+1];
|
||||
cvt.rn.f32.u16 %f73, %rs15;
|
||||
ld.global.u8 %rs16, [%rd9+2];
|
||||
cvt.rn.f32.u16 %f74, %rs16;
|
||||
fma.rn.f32 %f75, %f72, 0f3E3AFB7F, 0f41800000;
|
||||
fma.rn.f32 %f76, %f73, 0f3F1D3C36, %f75;
|
||||
fma.rn.f32 %f77, %f74, 0f3D7DF3B6, %f76;
|
||||
add.f32 %f78, %f77, 0f3F000000;
|
||||
cvt.rzi.s32.f32 %r82, %f78;
|
||||
cvt.s64.s32 %rd27, %r16;
|
||||
add.s64 %rd28, %rd7, %rd27;
|
||||
add.s64 %rd29, %rd1, %rd28;
|
||||
ld.global.u8 %r83, [%rd29];
|
||||
mul.lo.s32 %r84, %r82, %r81;
|
||||
xor.b32 %r85, %r81, 255;
|
||||
mad.lo.s32 %r86, %r85, %r83, %r84;
|
||||
mul.hi.s32 %r87, %r86, -2139062143;
|
||||
add.s32 %r88, %r87, %r86;
|
||||
shr.u32 %r89, %r88, 31;
|
||||
shr.u32 %r90, %r88, 7;
|
||||
add.s32 %r91, %r90, %r89;
|
||||
st.global.u8 [%rd29], %r91;
|
||||
fma.rn.f32 %f79, %f72, 0fBDCE075F, 0f43000000;
|
||||
fma.rn.f32 %f80, %f73, 0fBEAD5CFB, %f79;
|
||||
fma.rn.f32 %f81, %f74, 0f3EE0DED3, %f80;
|
||||
cvt.rn.f32.u16 %f82, %rs4;
|
||||
fma.rn.f32 %f102, %f81, %f82, %f102;
|
||||
fma.rn.f32 %f83, %f72, 0f3EE0DED3, 0f43000000;
|
||||
fma.rn.f32 %f84, %f73, 0fBECC3C9F, %f83;
|
||||
fma.rn.f32 %f85, %f74, 0fBD25119D, %f84;
|
||||
fma.rn.f32 %f103, %f85, %f82, %f103;
|
||||
add.f32 %f104, %f104, %f82;
|
||||
add.s32 %r121, %r121, 1;
|
||||
|
||||
$L__BB2_16:
|
||||
setp.eq.s32 %p33, %r121, 0;
|
||||
setp.le.f32 %p34, %f104, 0f00000000;
|
||||
or.pred %p35, %p34, %p33;
|
||||
@%p35 bra $L__BB2_19;
|
||||
|
||||
shr.u32 %r92, %r5, 31;
|
||||
add.s32 %r93, %r5, %r92;
|
||||
shr.s32 %r19, %r93, 1;
|
||||
setp.lt.s32 %p36, %r3, -1;
|
||||
setp.lt.s32 %p37, %r5, -1;
|
||||
or.pred %p38, %p37, %p36;
|
||||
and.b32 %r94, %r93, -2;
|
||||
setp.ge.s32 %p39, %r94, %r23;
|
||||
or.pred %p40, %p38, %p39;
|
||||
shr.u32 %r95, %r3, 31;
|
||||
add.s32 %r96, %r3, %r95;
|
||||
shr.s32 %r20, %r96, 1;
|
||||
and.b32 %r97, %r96, -2;
|
||||
setp.ge.s32 %p41, %r97, %r24;
|
||||
or.pred %p42, %p40, %p41;
|
||||
@%p42 bra $L__BB2_19;
|
||||
|
||||
div.rn.f32 %f86, %f102, %f104;
|
||||
add.f32 %f87, %f86, 0f3F000000;
|
||||
cvt.rzi.s32.f32 %r98, %f87;
|
||||
div.rn.f32 %f88, %f103, %f104;
|
||||
add.f32 %f89, %f88, 0f3F000000;
|
||||
cvt.rzi.s32.f32 %r99, %f89;
|
||||
cvt.rn.f32.s32 %f90, %r121;
|
||||
div.rn.f32 %f91, %f104, %f90;
|
||||
add.f32 %f92, %f91, 0f3F000000;
|
||||
cvt.rzi.s32.f32 %r100, %f92;
|
||||
mul.wide.s32 %rd30, %r20, %r22;
|
||||
mul.wide.s32 %rd31, %r19, 2;
|
||||
add.s64 %rd32, %rd30, %rd31;
|
||||
cvta.to.global.u64 %rd33, %rd10;
|
||||
add.s64 %rd34, %rd33, %rd32;
|
||||
ld.global.u8 %r101, [%rd34];
|
||||
mul.lo.s32 %r102, %r100, %r98;
|
||||
mov.u32 %r103, 255;
|
||||
sub.s32 %r104, %r103, %r100;
|
||||
mad.lo.s32 %r105, %r104, %r101, %r102;
|
||||
mul.hi.s32 %r106, %r105, -2139062143;
|
||||
add.s32 %r107, %r106, %r105;
|
||||
shr.u32 %r108, %r107, 31;
|
||||
shr.u32 %r109, %r107, 7;
|
||||
add.s32 %r110, %r109, %r108;
|
||||
st.global.u8 [%rd34], %r110;
|
||||
ld.global.u8 %r111, [%rd34+1];
|
||||
mul.lo.s32 %r112, %r100, %r99;
|
||||
mad.lo.s32 %r113, %r104, %r111, %r112;
|
||||
mul.hi.s32 %r114, %r113, -2139062143;
|
||||
add.s32 %r115, %r114, %r113;
|
||||
shr.u32 %r116, %r115, 31;
|
||||
shr.u32 %r117, %r115, 7;
|
||||
add.s32 %r118, %r117, %r116;
|
||||
st.global.u8 [%rd34+1], %r118;
|
||||
|
||||
$L__BB2_19:
|
||||
ret;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,862 +0,0 @@
|
||||
//! NVENC encoder via `ffmpeg-next` (binds the system FFmpeg — `ffmpeg-sys-next` auto-detects the
|
||||
//! installed version, so this builds against FFmpeg 7.x/libavcodec 61 *or* 8.x/libavcodec 62;
|
||||
//! validated live on Ubuntu 26.04 (FFmpeg 8) and Bazzite F43 (FFmpeg 7.1)).
|
||||
//!
|
||||
//! Input is a packed RGB/BGR CPU frame; `*_nvenc` accepts `rgb0`/`bgr0`/`rgba`/`bgra`
|
||||
//! directly and does the RGB→YUV conversion on the GPU, so the host stays off the
|
||||
//! colour-conversion path. The portal commonly negotiates packed 24-bit `RGB`, which NVENC
|
||||
//! does *not* accept — we expand it to `rgb0` (one padding byte/pixel, no colour math).
|
||||
//! The encoder is opened *without* a global header so VPS/SPS/PPS are emitted in-band on
|
||||
//! every IDR — the output is both a playable raw Annex-B stream and self-contained AUs.
|
||||
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use super::{ChromaFormat, Codec, EncodedFrame, Encoder};
|
||||
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};
|
||||
use ffmpeg_next as ffmpeg;
|
||||
use std::os::raw::c_int;
|
||||
use std::ptr;
|
||||
|
||||
use super::libav::{
|
||||
apply_low_latency_rc, 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
|
||||
/// the NVENC-padded `*0` form). Used by the 4:4:4 RGB→YUV444P conversion path. Mirrors the VAAPI
|
||||
/// CPU-input mapping; YUV/10-bit inputs can't feed this path (the 4:4:4 session forces packed RGB).
|
||||
fn sws_src_pixel(format: PixelFormat) -> Result<Pixel> {
|
||||
Ok(match format {
|
||||
PixelFormat::Bgrx => Pixel::BGRZ, // bgr0
|
||||
PixelFormat::Rgbx => Pixel::RGBZ, // rgb0
|
||||
PixelFormat::Bgra => Pixel::BGRA,
|
||||
PixelFormat::Rgba => Pixel::RGBA,
|
||||
PixelFormat::Rgb => Pixel::RGB24,
|
||||
PixelFormat::Bgr => Pixel::BGR24,
|
||||
PixelFormat::Nv12 | PixelFormat::P010 | PixelFormat::Rgb10a2 | PixelFormat::Yuv444 => {
|
||||
bail!("NVENC 4:4:4 CPU-input path supports packed RGB/BGR only; got {format:?}")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `AVCUDADeviceContext` (libavutil/hwcontext_cuda.h) — not in the ffmpeg-sys bindings (the
|
||||
/// crate doesn't allowlist that header), so mirror its stable 3-pointer layout. We set the
|
||||
/// first field to *our* `CUcontext` so NVENC shares the context the EGL importer maps into.
|
||||
#[repr(C)]
|
||||
struct AVCUDADeviceContext {
|
||||
cuda_ctx: *mut std::ffi::c_void, // CUcontext
|
||||
stream: *mut std::ffi::c_void, // CUstream (null = default)
|
||||
internal: *mut std::ffi::c_void, // filled by ctx_init
|
||||
}
|
||||
|
||||
/// CUDA hardware-frame contexts that wrap our shared `CUcontext`, so `hevc_nvenc` reads the
|
||||
/// imported device buffer directly. Owns two `AVBufferRef`s, unref'd on drop.
|
||||
struct CudaHw {
|
||||
device_ref: *mut ffi::AVBufferRef,
|
||||
frames_ref: *mut ffi::AVBufferRef,
|
||||
}
|
||||
|
||||
impl CudaHw {
|
||||
/// Build a CUDA hwdevice wrapping `cu_ctx` and a frames pool (`sw_format` = `pixel`).
|
||||
unsafe fn new(cu_ctx: *mut std::ffi::c_void, sw_format: Pixel, w: u32, h: u32) -> Result<Self> {
|
||||
let mut device_ref = ffi::av_hwdevice_ctx_alloc(ffi::AVHWDeviceType::AV_HWDEVICE_TYPE_CUDA);
|
||||
if device_ref.is_null() {
|
||||
bail!("av_hwdevice_ctx_alloc(CUDA) failed");
|
||||
}
|
||||
let dev_ctx = (*device_ref).data as *mut ffi::AVHWDeviceContext;
|
||||
let cu = (*dev_ctx).hwctx as *mut AVCUDADeviceContext;
|
||||
(*cu).cuda_ctx = cu_ctx; // share the importer's context
|
||||
let r = ffi::av_hwdevice_ctx_init(device_ref);
|
||||
if r < 0 {
|
||||
ffi::av_buffer_unref(&mut device_ref);
|
||||
bail!("av_hwdevice_ctx_init failed ({r})");
|
||||
}
|
||||
|
||||
let mut frames_ref = ffi::av_hwframe_ctx_alloc(device_ref);
|
||||
if frames_ref.is_null() {
|
||||
ffi::av_buffer_unref(&mut device_ref);
|
||||
bail!("av_hwframe_ctx_alloc failed");
|
||||
}
|
||||
let fc = (*frames_ref).data as *mut ffi::AVHWFramesContext;
|
||||
(*fc).format = ffi::AVPixelFormat::AV_PIX_FMT_CUDA;
|
||||
(*fc).sw_format = pixel_to_av(sw_format);
|
||||
(*fc).width = w as c_int;
|
||||
(*fc).height = h as c_int;
|
||||
(*fc).initial_pool_size = 0; // we supply the device pointers
|
||||
let r = ffi::av_hwframe_ctx_init(frames_ref);
|
||||
if r < 0 {
|
||||
ffi::av_buffer_unref(&mut frames_ref);
|
||||
ffi::av_buffer_unref(&mut device_ref);
|
||||
bail!("av_hwframe_ctx_init failed ({r})");
|
||||
}
|
||||
Ok(CudaHw {
|
||||
device_ref,
|
||||
frames_ref,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CudaHw {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: `frames_ref`/`device_ref` are the two non-null `AVBufferRef`s `CudaHw::new` created
|
||||
// (it bails before returning `Self` if either alloc fails, so a live `CudaHw` always holds
|
||||
// both). `av_buffer_unref` drops one reference and nulls the pointer through the `&mut`. This
|
||||
// `Drop` runs exactly once and `CudaHw` owns these refs exclusively → no double-free /
|
||||
// use-after-free. Frames are unref'd before the device (the frames ctx internally refs the
|
||||
// device; refcounted, so the order is sound regardless).
|
||||
unsafe {
|
||||
ffi::av_buffer_unref(&mut self.frames_ref);
|
||||
ffi::av_buffer_unref(&mut self.device_ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a captured layout to the NVENC input pixel format, and whether a 3→4 byte expand is
|
||||
/// needed (packed RGB/BGR have no padding byte; the NVENC `*0` formats do).
|
||||
fn nvenc_input(format: PixelFormat) -> (Pixel, bool) {
|
||||
match format {
|
||||
PixelFormat::Bgrx => (Pixel::BGRZ, false), // bgr0
|
||||
PixelFormat::Rgbx => (Pixel::RGBZ, false), // rgb0
|
||||
PixelFormat::Bgra => (Pixel::BGRA, false),
|
||||
PixelFormat::Rgba => (Pixel::RGBA, false),
|
||||
PixelFormat::Rgb => (Pixel::RGBZ, true), // RGB -> rgb0
|
||||
PixelFormat::Bgr => (Pixel::BGRZ, true), // BGR -> bgr0
|
||||
// NV12 is native YUV: NVENC encodes it with NO internal RGB→YUV CSC (the Tier 2A win). On
|
||||
// Linux it's produced by the GPU convert on the zero-copy tiled path (`PUNKTFUNK_NV12`); on
|
||||
// Windows by the D3D11 video processor.
|
||||
PixelFormat::Nv12 => (Pixel::NV12, false),
|
||||
// Planar YUV444 from the zero-copy worker's GPU convert (a 4:4:4 session) — native
|
||||
// full-chroma YUV in, `hevc_nvenc` emits Range-Extensions 4:4:4.
|
||||
PixelFormat::Yuv444 => (Pixel::YUV444P, false),
|
||||
// Rgb10a2 (HDR) and P010 (the Windows 10-bit video-processor output) are produced only by
|
||||
// the Windows paths; the Linux capturer never emits them. Map to BGRA so the match is
|
||||
// exhaustive — unreachable here.
|
||||
PixelFormat::Rgb10a2 | PixelFormat::P010 => (Pixel::BGRA, false),
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`NvencEncoder::open`] arguments, kept on the encoder so [`Encoder::reset`] can rebuild it
|
||||
/// in place with the session's negotiated parameters — the encode-stall watchdog's recovery lever
|
||||
/// (drop the wedged libavcodec encoder, reopen fresh, forfeit the owed AUs, restart at an IDR).
|
||||
#[derive(Clone, Copy)]
|
||||
struct OpenArgs {
|
||||
codec: Codec,
|
||||
format: PixelFormat,
|
||||
width: u32,
|
||||
height: u32,
|
||||
fps: u32,
|
||||
bitrate_bps: u64,
|
||||
cuda: bool,
|
||||
bit_depth: u8,
|
||||
chroma: ChromaFormat,
|
||||
}
|
||||
|
||||
pub struct NvencEncoder {
|
||||
enc: encoder::video::Encoder,
|
||||
/// Reusable 4-bpp CPU input frame (CPU path only; `None` for the zero-copy/CUDA path).
|
||||
/// Mutating it in place across frames is sound only because the encoder is opened with
|
||||
/// `delay=0`/`bf=0`/`max_b_frames=0` and the caller drains `poll()` after each `submit`,
|
||||
/// so libavcodec holds no reference to the previous frame's buffer when we overwrite it.
|
||||
frame: Option<VideoFrame>,
|
||||
/// Zero-copy path: CUDA hwdevice/hwframes contexts (the encoder takes `AV_PIX_FMT_CUDA`).
|
||||
cuda: Option<CudaHw>,
|
||||
/// 4:4:4 CPU path only: swscale context converting the captured packed RGB/BGR → planar
|
||||
/// YUV444P into [`Self::frame`], because `hevc_nvenc` only emits 4:4:4 from a YUV444 *input*
|
||||
/// (RGB-in is always 4:2:0). `None` on the 4:2:0 paths AND on the zero-copy 4:4:4 path (the
|
||||
/// worker's GPU convert delivers YUV444 CUDA frames). Freed in `Drop`.
|
||||
sws_444: Option<*mut ffi::SwsContext>,
|
||||
/// This session opened as full-chroma 4:4:4 (FREXT) — via either input path.
|
||||
want_444: bool,
|
||||
src_format: PixelFormat,
|
||||
expand: bool,
|
||||
width: u32,
|
||||
height: u32,
|
||||
fps: u32,
|
||||
/// Monotonic presentation index, in `1/fps` time-base units.
|
||||
frame_idx: i64,
|
||||
/// Force the next submitted frame to be an IDR (set by [`request_keyframe`]).
|
||||
force_kf: bool,
|
||||
/// Opened in intra-refresh mode (surfaced via [`caps`](Encoder::caps) so the session glue
|
||||
/// rate-limits forced IDRs — the wave heals loss without them).
|
||||
intra_refresh: bool,
|
||||
/// Resolved wave length in frames when [`intra_refresh`](Self::intra_refresh), else 0. Cached at
|
||||
/// open so the pump's per-AU `caps()` doesn't re-read `PUNKTFUNK_IR_PERIOD_FRAMES`; the pump marks
|
||||
/// every Nth AU with `USER_FLAG_RECOVERY_POINT` for the client's clean re-anchor.
|
||||
intra_refresh_period: u32,
|
||||
/// The open arguments, for the in-place [`reset`](Encoder::reset) rebuild.
|
||||
args: OpenArgs,
|
||||
}
|
||||
|
||||
// `CudaHw` holds raw `AVBufferRef`s and `sws_444` a raw `SwsContext`; the encoder lives on a single
|
||||
// thread. The CPU encoder is already `Send` via ffmpeg-next; assert it for the raw fields too.
|
||||
// SAFETY: `NvencEncoder` owns an ffmpeg-next `Encoder`/`VideoFrame` (already `Send`) plus a `CudaHw`
|
||||
// holding raw `AVBufferRef`s and an optional raw `SwsContext`, none of which are `Send` by default.
|
||||
// The `SwsContext` is a self-contained swscale state object with no thread affinity, touched only
|
||||
// through `&mut self` on the one encode thread. The encoder is owned and driven by
|
||||
// exactly ONE thread — the per-session encode thread it is moved to — and is only touched through
|
||||
// `&mut self` methods, so it is never aliased or accessed concurrently. The wrapped libav contexts
|
||||
// (and the shared `CUcontext` the `CudaHw` references) have no thread affinity, so transferring
|
||||
// ownership across threads is sound. This asserts `Send` (transfer) only, extending ffmpeg-next's
|
||||
// existing `Send` to the raw CUDA fields; `Sync` (shared `&`) is deliberately NOT implemented.
|
||||
unsafe impl Send for NvencEncoder {}
|
||||
|
||||
/// Latched true once an intra-refresh open failed with the device-capability error (ENOSYS from
|
||||
/// `NV_ENC_CAPS_SUPPORT_INTRA_REFRESH`), so later sessions skip the doomed attempt. Never set by
|
||||
/// other open failures (a bitrate EINVAL must not permanently disable the feature).
|
||||
static IR_UNSUPPORTED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
/// Whether this open should run the NVENC **intra-refresh** loss-recovery mode
|
||||
/// (`PUNKTFUNK_INTRA_REFRESH` truthy, opt-in until on-glass validated): a moving intra band +
|
||||
/// recovery-point SEI refreshes the whole picture every [`intra_refresh_period`] frames, so
|
||||
/// FEC-unrecoverable loss heals without the 20-40× full-IDR spike (which under loss causes more
|
||||
/// loss — the cascade). The session glue then rate-limits client keyframe requests
|
||||
/// ([`EncoderCaps::intra_refresh`](super::EncoderCaps)).
|
||||
fn intra_refresh_requested() -> bool {
|
||||
std::env::var("PUNKTFUNK_INTRA_REFRESH")
|
||||
.map(|v| matches!(v.trim(), "1" | "true" | "yes" | "on"))
|
||||
.unwrap_or(false)
|
||||
&& !IR_UNSUPPORTED.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// The intra-refresh wave length in frames — ffmpeg derives `intraRefreshPeriod`/`Cnt` from
|
||||
/// `gop_size` before forcing the real GOP infinite, so this is what `gop_size` is set to in IR
|
||||
/// mode. Default = half a second of frames (heals fast, spreads the intra cost to ~2-3% per
|
||||
/// frame); `PUNKTFUNK_IR_PERIOD_FRAMES` overrides.
|
||||
fn intra_refresh_period(fps: u32) -> i32 {
|
||||
std::env::var("PUNKTFUNK_IR_PERIOD_FRAMES")
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<i32>().ok())
|
||||
.filter(|v| *v >= 2)
|
||||
.unwrap_or_else(|| (fps.max(16) / 2) as i32)
|
||||
}
|
||||
|
||||
impl NvencEncoder {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn open(
|
||||
codec: Codec,
|
||||
format: PixelFormat,
|
||||
width: u32,
|
||||
height: u32,
|
||||
fps: u32,
|
||||
bitrate_bps: u64,
|
||||
cuda: bool,
|
||||
bit_depth: u8,
|
||||
chroma: ChromaFormat,
|
||||
) -> Result<Self> {
|
||||
// TODO(hdr): Linux 10-bit parity. Unlike the Windows raw-SDK path (which upconverts 8-bit
|
||||
// ARGB → Main10 via pixelBitDepthMinus8), libavcodec hevc_nvenc needs a 10-bit input pixel
|
||||
// format (p010) for Main10, so it's a bigger change; deferred until a Linux GPU box is
|
||||
// available to validate. The Linux host stays 8-bit for now.
|
||||
if bit_depth != 8 {
|
||||
tracing::warn!(
|
||||
bit_depth,
|
||||
"Linux NVENC 10-bit not yet wired — encoding 8-bit"
|
||||
);
|
||||
}
|
||||
// Full-chroma 4:4:4 (HEVC Range Extensions). `hevc_nvenc` only emits 4:4:4 from a YUV444
|
||||
// *input* frame — feeding RGB always subsamples to 4:2:0 regardless of profile (verified on
|
||||
// the RTX 5070 Ti). Two ways to produce that input: the zero-copy worker's GPU convert
|
||||
// (planar-YUV444 CUDA frames — `cuda` true), or the CPU path's swscale RGB→YUV444P. Both
|
||||
// feed `profile=rext`; the range follows `PUNKTFUNK_444_FULLRANGE` in both.
|
||||
let want_444 = chroma.is_444() && codec == Codec::H265;
|
||||
ffmpeg::init().context("ffmpeg init")?;
|
||||
if std::env::var_os("PUNKTFUNK_FFMPEG_DEBUG").is_some() {
|
||||
// SAFETY: `av_log_set_level` sets libav's global integer log level; `48` (= AV_LOG_DEBUG)
|
||||
// is a valid level with no pointer args, and libav was just initialized by `ffmpeg::init()`
|
||||
// above — always sound.
|
||||
unsafe { ffi::av_log_set_level(48) }; // AV_LOG_DEBUG — surface NVENC hw-frame rejects
|
||||
}
|
||||
let name = codec.nvenc_name();
|
||||
let av_codec = encoder::find_by_name(name)
|
||||
.ok_or_else(|| anyhow!("{name} not built into libavcodec"))?;
|
||||
let (rgb_pixel, rgb_expand) = nvenc_input(format);
|
||||
// 4:4:4 feeds NVENC a planar YUV444P frame we produce by swscale; the ordinary path feeds the
|
||||
// captured RGB straight in and lets NVENC's internal CSC subsample to 4:2:0.
|
||||
let (nvenc_pixel, expand) = if want_444 {
|
||||
(Pixel::YUV444P, false)
|
||||
} else {
|
||||
(rgb_pixel, rgb_expand)
|
||||
};
|
||||
|
||||
let mut video = codec::context::Context::new_with_codec(av_codec)
|
||||
.encoder()
|
||||
.video()
|
||||
.context("alloc video encoder")?;
|
||||
video.set_width(width);
|
||||
video.set_height(height);
|
||||
video.set_format(nvenc_pixel); // NVENC converts RGB→YUV internally
|
||||
// Fixed rate, CBR, no B-frames, ~1-frame VBV — the shared low-latency RC contract.
|
||||
apply_low_latency_rc(&mut video, fps, bitrate_bps);
|
||||
// Infinite GOP — NO periodic IDR. A keyframe at 5120x1440 is ~20-40x a P-frame, so a
|
||||
// periodic IDR is a recurring multi-millisecond encode+packetize+send spike — the ~2s
|
||||
// "freeze". NVENC emits one IDR at stream start, then P-frames only; `forced-idr` (below)
|
||||
// turns a client recovery request (RFI, via `request_keyframe`) into an IDR on demand.
|
||||
// This is the Moonlight/Sunshine low-latency model.
|
||||
// In intra-refresh mode the GOP is still infinite — ffmpeg reads `gop_size` as the refresh
|
||||
// WAVE length (`intraRefreshPeriod`/`Cnt`) and then forces `gopLength` infinite itself, so
|
||||
// a positive `gop_size` here does NOT reintroduce periodic IDRs.
|
||||
let intra_refresh = intra_refresh_requested();
|
||||
// SAFETY: same `video` builder as above — a non-null, properly-aligned, sole-owned, not-yet-
|
||||
// opened `AVCodecContext`. We write the plain `gop_size` int field (-1 = infinite GOP, or the
|
||||
// intra-refresh wave length) before `open_with`, which ffmpeg-next has no setter for. No
|
||||
// aliasing; synchronous scalar write.
|
||||
unsafe {
|
||||
(*video.as_mut_ptr()).gop_size = if intra_refresh {
|
||||
intra_refresh_period(fps)
|
||||
} else {
|
||||
-1
|
||||
};
|
||||
}
|
||||
|
||||
// NV12 / 4:4:4 paths: we do the RGB→YUV conversion ourselves as BT.709 (swscale), so
|
||||
// signal that in the bitstream VUI (colorspace/range/primaries/transfer) — otherwise the
|
||||
// client decoder assumes a default and the picture comes out washed-out / wrong-contrast.
|
||||
// The RGB-input 4:2:0 path leaves these unset (NVENC's internal CSC writes its own VUI).
|
||||
// Matches the Windows NV12 path's BT.709 limited-range signalling.
|
||||
//
|
||||
// PUNKTFUNK_444_FULLRANGE=1 (experimental, 4:4:4-only): convert AND signal FULL range —
|
||||
// recovers the ~12% of code space limited-range quantization gives up, for the exact
|
||||
// text/UI chroma 4:4:4 exists for. Every punktfunk client honors the signaled range
|
||||
// (csc_rows / the Apple rows port); ship as default only if the on-glass A/B shows a
|
||||
// visible win. Linux-only: the Windows path's NVENC-internal CSC range is unmeasured.
|
||||
let full_range_444 =
|
||||
want_444 && std::env::var("PUNKTFUNK_444_FULLRANGE").is_ok_and(|v| v.trim() == "1");
|
||||
if matches!(format, PixelFormat::Nv12) || want_444 {
|
||||
// SAFETY: same `video` builder — `raw = video.as_mut_ptr()` is the non-null, properly-
|
||||
// aligned, sole-owned, not-yet-opened `AVCodecContext`. We set its four VUI colour enum
|
||||
// fields to valid `AVColorSpace`/`AVColorRange`/`AVColorPrimaries`/`AVColorTransfer-
|
||||
// Characteristic` variants before `open_with`. Sole owner → no aliasing; synchronous writes.
|
||||
unsafe {
|
||||
let raw = video.as_mut_ptr();
|
||||
(*raw).colorspace = ffi::AVColorSpace::AVCOL_SPC_BT709;
|
||||
(*raw).color_range = if full_range_444 {
|
||||
ffi::AVColorRange::AVCOL_RANGE_JPEG // full
|
||||
} else {
|
||||
ffi::AVColorRange::AVCOL_RANGE_MPEG // limited/studio
|
||||
};
|
||||
(*raw).color_primaries = ffi::AVColorPrimaries::AVCOL_PRI_BT709;
|
||||
(*raw).color_trc = ffi::AVColorTransferCharacteristic::AVCOL_TRC_BT709;
|
||||
}
|
||||
}
|
||||
|
||||
// For the zero-copy path, take CUDA surfaces: wrap the shared CUcontext in CUDA
|
||||
// hwdevice/hwframes contexts and set `pix_fmt = CUDA` on the raw encoder context
|
||||
// *before* open (NVENC derives the device from `hw_frames_ctx`).
|
||||
let cuda_hw = if cuda {
|
||||
let cu_ctx = crate::zerocopy::cuda::context().context("shared CUDA context")?;
|
||||
// SAFETY: `CudaHw::new` (an `unsafe fn`) requires libav initialized (the `ffmpeg::init()`
|
||||
// above ran) and a valid `CUcontext`; `cu_ctx` is the shared importer context from
|
||||
// `zerocopy::cuda::context()?`, non-null on the `Ok` path. `nvenc_pixel` is a valid `Pixel`
|
||||
// and `width`/`height` are the validated positive dims. It returns a RAII `CudaHw` wrapping
|
||||
// (not owning) `cu_ctx` and owning two `AVBufferRef`s freed on drop.
|
||||
let hw = unsafe { CudaHw::new(cu_ctx, nvenc_pixel, width, height)? };
|
||||
// SAFETY: `raw = video.as_mut_ptr()` is the non-null, sole-owned, not-yet-opened
|
||||
// `AVCodecContext`. We set `pix_fmt = CUDA` and attach NEW refs (`av_buffer_ref`) of
|
||||
// `hw.device_ref`/`hw.frames_ref` — both non-null (`CudaHw::new` guarantees) and from the
|
||||
// live `hw`, which is moved into `NvencEncoder.cuda` next to `enc` and so outlives the
|
||||
// encoder. The context owns its own refs (freed when the context closes). No aliasing.
|
||||
unsafe {
|
||||
let raw = video.as_mut_ptr();
|
||||
(*raw).pix_fmt = ffi::AVPixelFormat::AV_PIX_FMT_CUDA;
|
||||
(*raw).hw_device_ctx = ffi::av_buffer_ref(hw.device_ref);
|
||||
(*raw).hw_frames_ctx = ffi::av_buffer_ref(hw.frames_ref);
|
||||
}
|
||||
Some(hw)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// 4:4:4 CPU path: build the RGB→YUV444P swscale (BT.709, range per the flag; no rescale).
|
||||
// Mirrors the VAAPI CPU path's RGB→NV12 scaler, but the dst is full-chroma planar 4:4:4.
|
||||
// Skipped on the zero-copy path (`cuda`): the worker's GPU convert already delivers
|
||||
// planar YUV444 CUDA frames — no CPU pixels exist to scale.
|
||||
let sws_444 = if want_444 && !cuda {
|
||||
let src_av = pixel_to_av(sws_src_pixel(format)?);
|
||||
// SAFETY: `sws_getContext` allocates a swscale context for the given src/dst dims + pixel
|
||||
// formats. Both dims are the encoder's positive `width`/`height` as `c_int`; `src_av` is a
|
||||
// valid `AVPixelFormat` (from the `sws_src_pixel`-validated, packed-RGB-only source), the
|
||||
// dst is YUV444P. The trailing filter/param pointers are null = "use defaults" (documented
|
||||
// as accepted). No Rust memory is borrowed; the returned pointer is null-checked below.
|
||||
let sws = unsafe {
|
||||
ffi::sws_getContext(
|
||||
width as c_int,
|
||||
height as c_int,
|
||||
src_av,
|
||||
width as c_int,
|
||||
height as c_int,
|
||||
ffi::AVPixelFormat::AV_PIX_FMT_YUV444P,
|
||||
SWS_POINT,
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
ptr::null(),
|
||||
)
|
||||
};
|
||||
if sws.is_null() {
|
||||
bail!("sws_getContext(RGB→YUV444P) failed");
|
||||
}
|
||||
// SAFETY: `sws` is the non-null context from the call above (null-checked). The ITU-709
|
||||
// coefficient table from `sws_getCoefficients` is a process-lifetime libswscale static,
|
||||
// reused for src+dst matrices; `sws_setColorspaceDetails` only reads it and writes scalar
|
||||
// CSC settings into `sws` (dstRange matches the VUI: 0 = limited, 1 = the
|
||||
// PUNKTFUNK_444_FULLRANGE experiment). No Rust memory is passed.
|
||||
unsafe {
|
||||
let cs709 = ffi::sws_getCoefficients(SWS_CS_ITU709);
|
||||
let dst_range = i32::from(full_range_444);
|
||||
ffi::sws_setColorspaceDetails(sws, cs709, 1, cs709, dst_range, 0, 1 << 16, 1 << 16);
|
||||
}
|
||||
Some(sws)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Low-latency NVENC tuning (plan §7 / linux-setup doc).
|
||||
let mut opts = Dictionary::new();
|
||||
opts.set("preset", "p1"); // fastest
|
||||
opts.set("tune", "ull"); // ultra-low-latency
|
||||
opts.set("rc", "cbr");
|
||||
opts.set("bf", "0");
|
||||
opts.set("delay", "0");
|
||||
opts.set("forced-idr", "1"); // RFI/request_keyframe → real IDR under the infinite GOP
|
||||
if intra_refresh {
|
||||
// Moving intra band + recovery-point SEI (period set via gop_size above). Loss now
|
||||
// self-heals within the wave; forced IDRs remain available (rate-limited by the glue).
|
||||
opts.set("intra-refresh", "1");
|
||||
}
|
||||
if want_444 {
|
||||
// HEVC Range Extensions — the profile that carries chroma_format_idc=3. With a YUV444P
|
||||
// input `hevc_nvenc` auto-selects it, but pin it explicitly so the chroma is never silently
|
||||
// dropped on a future libavcodec.
|
||||
opts.set("profile", "rext");
|
||||
}
|
||||
|
||||
// Split-frame encode across both NVENC engines (GB203 has 2) when the pixel rate exceeds
|
||||
// a single engine's HEVC capacity (~1 Gpix/s); e.g. 5120x1440@240 = 1.77 Gpix/s needs it,
|
||||
// @120 = 0.88 Gpix/s does not. HEVC/AV1 only (not H.264). AUTO won't engage below ~2112px
|
||||
// height, so we force `2`; below the threshold we leave it AUTO (split costs ~2% BD-rate).
|
||||
// Output is standard HEVC — transparent to the client. Override with PUNKTFUNK_SPLIT_ENCODE.
|
||||
let pix_rate = width as u64 * height as u64 * fps as u64;
|
||||
let split = std::env::var("PUNKTFUNK_SPLIT_ENCODE").ok();
|
||||
match split.as_deref() {
|
||||
Some(mode) => opts.set("split_encode_mode", mode),
|
||||
None if matches!(codec, Codec::H265 | Codec::Av1) && pix_rate > 1_000_000_000 => {
|
||||
opts.set("split_encode_mode", "2");
|
||||
tracing::info!(
|
||||
pix_rate,
|
||||
"NVENC: forcing 2-way split encode (high pixel rate)"
|
||||
);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
let enc = match video.open_with(opts) {
|
||||
Ok(enc) => enc,
|
||||
// The GPU lacks NV_ENC_CAPS_SUPPORT_INTRA_REFRESH — ffmpeg fails the open with
|
||||
// ENOSYS ("Function not implemented"). Latch it (skip the doomed attempt on later
|
||||
// sessions) and reopen this session without intra-refresh; any other failure — and
|
||||
// any failure when IR wasn't requested — propagates untouched (the bitrate probe
|
||||
// keys on EINVAL, which must not trip the latch).
|
||||
Err(e) if intra_refresh && format!("{e:#}").contains("Function not implemented") => {
|
||||
tracing::warn!(
|
||||
encoder = name,
|
||||
"NVENC intra-refresh not supported by this GPU — falling back to IDR-only \
|
||||
recovery"
|
||||
);
|
||||
IR_UNSUPPORTED.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
return Self::open(
|
||||
codec,
|
||||
format,
|
||||
width,
|
||||
height,
|
||||
fps,
|
||||
bitrate_bps,
|
||||
cuda,
|
||||
bit_depth,
|
||||
chroma,
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e).with_context(|| {
|
||||
format!("open {name} ({width}x{height}@{fps}, {bitrate_bps} bps)")
|
||||
})
|
||||
}
|
||||
};
|
||||
if intra_refresh {
|
||||
tracing::info!(
|
||||
encoder = name,
|
||||
period_frames = intra_refresh_period(fps),
|
||||
"NVENC intra-refresh recovery active (no periodic IDR; wave heals loss)"
|
||||
);
|
||||
}
|
||||
|
||||
let frame = if cuda {
|
||||
None
|
||||
} else {
|
||||
Some(VideoFrame::new(nvenc_pixel, width, height))
|
||||
};
|
||||
Ok(NvencEncoder {
|
||||
enc,
|
||||
frame,
|
||||
cuda: cuda_hw,
|
||||
sws_444,
|
||||
want_444,
|
||||
src_format: format,
|
||||
expand,
|
||||
width,
|
||||
height,
|
||||
fps,
|
||||
frame_idx: 0,
|
||||
force_kf: false,
|
||||
intra_refresh,
|
||||
intra_refresh_period: if intra_refresh {
|
||||
intra_refresh_period(fps).max(1) as u32
|
||||
} else {
|
||||
0
|
||||
},
|
||||
args: OpenArgs {
|
||||
codec,
|
||||
format,
|
||||
width,
|
||||
height,
|
||||
fps,
|
||||
bitrate_bps,
|
||||
cuda,
|
||||
bit_depth,
|
||||
chroma,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder for NvencEncoder {
|
||||
fn caps(&self) -> super::EncoderCaps {
|
||||
super::EncoderCaps {
|
||||
// 4:4:4 iff this session opened FREXT — the CPU swscale path or the zero-copy GPU
|
||||
// convert. RFI/HDR-SEI stay unsupported on libavcodec NVENC (the trait defaults).
|
||||
chroma_444: self.want_444,
|
||||
intra_refresh: self.intra_refresh,
|
||||
// NVENC intra-refresh is purpose-built GDR loss recovery (moving band + recovery-point
|
||||
// SEI): the wave heals a lost picture within one period, so mark the boundary AUs and let
|
||||
// the client re-anchor on them instead of forcing a full IDR. Tied to `intra_refresh`
|
||||
// (already the `PUNKTFUNK_INTRA_REFRESH` opt-in), unlike AMF/QSV which stay unvalidated.
|
||||
intra_refresh_recovery: self.intra_refresh,
|
||||
intra_refresh_period: self.intra_refresh_period,
|
||||
..super::EncoderCaps::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn submit(&mut self, captured: &CapturedFrame) -> Result<()> {
|
||||
anyhow::ensure!(
|
||||
captured.width == self.width && captured.height == self.height,
|
||||
"captured frame {}x{} != encoder {}x{}",
|
||||
captured.width,
|
||||
captured.height,
|
||||
self.width,
|
||||
self.height
|
||||
);
|
||||
let pts = self.frame_idx;
|
||||
self.frame_idx += 1;
|
||||
// Force an IDR when requested (client RFI); otherwise let NVENC pick (GOP/P-frame).
|
||||
let idr = self.force_kf;
|
||||
self.force_kf = false;
|
||||
match &captured.payload {
|
||||
FramePayload::Cuda(buf) => self.submit_cuda(buf, pts, idr),
|
||||
FramePayload::Cpu(bytes) => self.submit_cpu(bytes, captured.format, pts, idr),
|
||||
FramePayload::Dmabuf(_) => {
|
||||
bail!("NVENC got a VAAPI dmabuf frame — capture/encoder backend mismatch")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn request_keyframe(&mut self) {
|
||||
self.force_kf = true;
|
||||
}
|
||||
|
||||
/// Encode-stall recovery: drop the wedged libavcodec encoder and reopen it fresh with the
|
||||
/// session's negotiated parameters (the stored [`OpenArgs`]) — the drop-and-reopen lever the
|
||||
/// QSV/VAAPI paths use, so the encode-stall watchdog can heal a wedged NVENC/driver instead of
|
||||
/// ending the session. Owed AUs are forfeited; the fresh encoder opens on an IDR.
|
||||
fn reset(&mut self) -> bool {
|
||||
let a = self.args;
|
||||
match Self::open(
|
||||
a.codec,
|
||||
a.format,
|
||||
a.width,
|
||||
a.height,
|
||||
a.fps,
|
||||
a.bitrate_bps,
|
||||
a.cuda,
|
||||
a.bit_depth,
|
||||
a.chroma,
|
||||
) {
|
||||
Ok(mut fresh) => {
|
||||
fresh.force_kf = true;
|
||||
*self = fresh; // drops the wedged encoder (frees its contexts) in the same step
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(error = %format!("{e:#}"), "NVENC in-place reopen failed");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll(&mut self) -> Result<Option<EncodedFrame>> {
|
||||
// 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),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<()> {
|
||||
self.enc.send_eof().context("send_eof")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl NvencEncoder {
|
||||
/// CPU path: expand/copy the packed RGB/BGR bytes into the reusable 4-bpp frame, then send.
|
||||
fn submit_cpu(&mut self, bytes: &[u8], format: PixelFormat, pts: i64, idr: bool) -> Result<()> {
|
||||
anyhow::ensure!(
|
||||
format == self.src_format,
|
||||
"captured format {:?} != encoder source {:?}",
|
||||
format,
|
||||
self.src_format
|
||||
);
|
||||
let w = self.width as usize;
|
||||
let h = self.height as usize;
|
||||
let src_bpp = self.src_format.bytes_per_pixel();
|
||||
let src_row = w * src_bpp;
|
||||
anyhow::ensure!(
|
||||
bytes.len() >= src_row * h,
|
||||
"captured buffer {} bytes < required {}",
|
||||
bytes.len(),
|
||||
src_row * h
|
||||
);
|
||||
// 4:4:4: swscale the packed RGB straight into the planar YUV444P input frame (BT.709 limited),
|
||||
// then send it — no byte-expand. The 4:2:0 RGB path (below) feeds NVENC packed RGB directly.
|
||||
if let Some(sws) = self.sws_444 {
|
||||
let frame = self
|
||||
.frame
|
||||
.as_mut()
|
||||
.context("CPU frame missing (encoder opened in CUDA mode)")?;
|
||||
// SAFETY: `format == self.src_format` and `bytes.len() >= src_row * h` (the `ensure!`s
|
||||
// above), so `sws_scale` reads `h` rows of `src_row` bytes from `src_data[0] = bytes`
|
||||
// (packed RGB is single-plane; the other src planes are null/0) — all in bounds. `sws` is
|
||||
// the non-null context built in `open`. The dst is `frame`'s underlying `AVFrame`: its
|
||||
// `data`/`linesize` in-struct arrays were sized for YUV444P by `VideoFrame::new`, and the
|
||||
// 3 planes are each `width`×`height`. All pointers are live locals for this synchronous
|
||||
// call; the encoder runs only on this thread (`unsafe impl Send`), so no aliasing/race.
|
||||
unsafe {
|
||||
let dst_av = frame.as_mut_ptr();
|
||||
let src_data: [*const u8; 4] =
|
||||
[bytes.as_ptr(), ptr::null(), ptr::null(), ptr::null()];
|
||||
let src_stride: [c_int; 4] = [src_row as c_int, 0, 0, 0];
|
||||
let r = ffi::sws_scale(
|
||||
sws,
|
||||
src_data.as_ptr(),
|
||||
src_stride.as_ptr(),
|
||||
0,
|
||||
h as c_int,
|
||||
(*dst_av).data.as_ptr(),
|
||||
(*dst_av).linesize.as_ptr(),
|
||||
);
|
||||
if r < 0 {
|
||||
bail!("sws_scale(RGB→YUV444P) failed ({r})");
|
||||
}
|
||||
}
|
||||
frame.set_pts(Some(pts));
|
||||
frame.set_kind(if idr {
|
||||
ffmpeg::picture::Type::I
|
||||
} else {
|
||||
ffmpeg::picture::Type::None
|
||||
});
|
||||
self.enc.send_frame(frame).context("send_frame(444)")?;
|
||||
return Ok(());
|
||||
}
|
||||
let frame = self
|
||||
.frame
|
||||
.as_mut()
|
||||
.context("CPU frame missing (encoder opened in CUDA mode)")?;
|
||||
let stride = frame.stride(0); // dst is 4-bpp, aligned
|
||||
let dst = frame.data_mut(0);
|
||||
if self.expand {
|
||||
// packed 3-bpp RGB/BGR → 4-bpp *0 (copy 3 bytes, zero the pad byte)
|
||||
for y in 0..h {
|
||||
let s = &bytes[y * src_row..y * src_row + src_row];
|
||||
let drow = &mut dst[y * stride..y * stride + w * 4];
|
||||
for x in 0..w {
|
||||
drow[x * 4..x * 4 + 3].copy_from_slice(&s[x * 3..x * 3 + 3]);
|
||||
drow[x * 4 + 3] = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 4-bpp → 4-bpp, honoring the (possibly larger) dst stride
|
||||
for y in 0..h {
|
||||
dst[y * stride..y * stride + src_row]
|
||||
.copy_from_slice(&bytes[y * src_row..y * src_row + src_row]);
|
||||
}
|
||||
}
|
||||
frame.set_pts(Some(pts));
|
||||
frame.set_kind(if idr {
|
||||
ffmpeg::picture::Type::I
|
||||
} else {
|
||||
ffmpeg::picture::Type::None
|
||||
});
|
||||
self.enc.send_frame(frame).context("send_frame")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Zero-copy path: hand the imported CUDA device buffer to NVENC with no CPU touch.
|
||||
///
|
||||
/// We take a *pooled* surface from the CUDA hwframes context (`av_hwframe_get_buffer`) and
|
||||
/// device→device-copy our imported buffer into it, rather than wrapping our own pointer in a
|
||||
/// bare frame. Two reasons: (1) NVENC's `nvenc_send_frame` ignores frames whose `buf[0]` is
|
||||
/// null and the generic encode path's `av_frame_ref` needs a refcounted buffer — a bare
|
||||
/// frame is rejected with `EINVAL`; (2) NVENC caches CUDA-resource *registrations* keyed by
|
||||
/// device pointer with a bounded table, so a fresh pointer every frame would thrash/overflow
|
||||
/// it — the pool recycles a small set of pointers. The extra copy is device-local (~8 MB at
|
||||
/// 1080p, sub-millisecond on the GPU) and keeps the host fully off the pixel path.
|
||||
fn submit_cuda(
|
||||
&mut self,
|
||||
buf: &crate::zerocopy::DeviceBuffer,
|
||||
pts: i64,
|
||||
idr: bool,
|
||||
) -> Result<()> {
|
||||
let frames_ref = self
|
||||
.cuda
|
||||
.as_ref()
|
||||
.context("CUDA hw context missing (encoder opened in CPU mode)")?
|
||||
.frames_ref;
|
||||
// The device→device copy below uses our shared context directly; make it current on the
|
||||
// encode thread (ffmpeg pushes its own around the pool alloc, so order is fine).
|
||||
crate::zerocopy::cuda::make_current().context("CUDA context current (encode thread)")?;
|
||||
// SAFETY: `frames_ref` is the non-null CUDA frames ctx from `self.cuda` (unwrapped via
|
||||
// `.context(..)?` above), and the shared CUDA context was just made current on THIS thread
|
||||
// (`make_current()?`), the precondition for the device-pointer copies below.
|
||||
// * `av_frame_alloc` → `f` (null-checked). `av_hwframe_get_buffer(frames_ref, f, 0)` fills `f`
|
||||
// with a pooled CUDA surface (sets `data[]`/`linesize[]`/`buf[0]`/`hw_frames_ctx`); on
|
||||
// failure we free `f` and bail.
|
||||
// * For NV12 we read `(*f).data[0..2]` / `linesize[0..2]` (Y + interleaved UV), else
|
||||
// `data[0]`/`linesize[0]` — in-struct fields of the non-null `f`, valid for the surface dims
|
||||
// ffmpeg allocated — and pass them to the cuda copy helpers, which device→device copy `buf`
|
||||
// (the imported `DeviceBuffer`, owned by the caller and live for this call) into the surface.
|
||||
// * On copy error we free `f` and return. Otherwise we write `pts`/`pict_type` through `f` and
|
||||
// `avcodec_send_frame` it into the live owned `self.enc` context (which takes its own ref of
|
||||
// the pooled surface), then free our `f` ref exactly once. Single-threaded encoder → no race.
|
||||
unsafe {
|
||||
let mut f = ffi::av_frame_alloc();
|
||||
if f.is_null() {
|
||||
bail!("av_frame_alloc failed");
|
||||
}
|
||||
// Pooled CUDA surface: sets format, width/height, data[0]/linesize[0], buf[0] and
|
||||
// hw_frames_ctx. Reused across frames (the pool recycles), keeping NVENC's
|
||||
// registration cache warm.
|
||||
let r = ffi::av_hwframe_get_buffer(frames_ref, f, 0);
|
||||
if r < 0 {
|
||||
ffi::av_frame_free(&mut f);
|
||||
bail!("av_hwframe_get_buffer(CUDA) failed ({r})");
|
||||
}
|
||||
// NV12 surfaces are two-plane (Y in data[0], interleaved UV in data[1]); YUV444
|
||||
// surfaces are three-plane (`yuv444p` frames ctx — data[0..3]); the RGB surfaces are
|
||||
// single-plane. Copy the matching layout into NVENC's pooled surface. A 4:4:4 session
|
||||
// whose buffer ISN'T YUV444 (a LINEAR/gamescope capture the worker can't convert)
|
||||
// fails loudly here rather than letting `hevc_nvenc` silently subsample RGB to 4:2:0.
|
||||
let copy_res = if buf.yuv444 {
|
||||
let dsts = core::array::from_fn(|i| {
|
||||
(
|
||||
(*f).data[i] as crate::zerocopy::cuda::CUdeviceptr,
|
||||
(*f).linesize[i] as usize,
|
||||
)
|
||||
});
|
||||
crate::zerocopy::cuda::copy_yuv444_to_device(buf, dsts)
|
||||
} else if self.want_444 {
|
||||
ffi::av_frame_free(&mut f);
|
||||
bail!(
|
||||
"4:4:4 session but the zero-copy frame is not YUV444 (LINEAR/gamescope \
|
||||
capture has no GPU 4:4:4 convert) — unset PUNKTFUNK_ZEROCOPY to use the \
|
||||
CPU 4:4:4 path on this compositor"
|
||||
);
|
||||
} else if buf.is_nv12() {
|
||||
let y_ptr = (*f).data[0] as crate::zerocopy::cuda::CUdeviceptr;
|
||||
let y_pitch = (*f).linesize[0] as usize;
|
||||
let uv_ptr = (*f).data[1] as crate::zerocopy::cuda::CUdeviceptr;
|
||||
let uv_pitch = (*f).linesize[1] as usize;
|
||||
crate::zerocopy::cuda::copy_nv12_to_device(buf, y_ptr, y_pitch, uv_ptr, uv_pitch)
|
||||
} else {
|
||||
let dst_ptr = (*f).data[0] as crate::zerocopy::cuda::CUdeviceptr;
|
||||
let dst_pitch = (*f).linesize[0] as usize;
|
||||
crate::zerocopy::cuda::copy_device_to_device(buf, dst_ptr, dst_pitch)
|
||||
};
|
||||
if let Err(e) = copy_res {
|
||||
ffi::av_frame_free(&mut f);
|
||||
return Err(e).context("copy imported buffer into NVENC surface");
|
||||
}
|
||||
(*f).pts = pts;
|
||||
(*f).pict_type = if idr {
|
||||
ffi::AVPictureType::AV_PICTURE_TYPE_I
|
||||
} else {
|
||||
ffi::AVPictureType::AV_PICTURE_TYPE_NONE
|
||||
};
|
||||
let r = ffi::avcodec_send_frame(self.enc.as_mut_ptr(), f);
|
||||
ffi::av_frame_free(&mut f);
|
||||
if r < 0 {
|
||||
bail!("avcodec_send_frame(CUDA) failed ({r})");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for NvencEncoder {
|
||||
fn drop(&mut self) {
|
||||
if let Some(sws) = self.sws_444.take() {
|
||||
// SAFETY: `sws` is the non-null `SwsContext` allocated by `sws_getContext` in `open` and
|
||||
// owned exclusively by this encoder (taken out of the field so it can't be freed twice).
|
||||
// `sws_freeContext` frees it; nothing else references it after this single-threaded drop.
|
||||
unsafe { ffi::sws_freeContext(sws) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Probe whether this NVIDIA GPU + driver + libavcodec can actually encode HEVC **4:4:4** (Range
|
||||
/// Extensions). Opens a tiny real `hevc_nvenc` 4:4:4 session — the exact path [`NvencEncoder::open`]
|
||||
/// takes for a live 4:4:4 stream — and reports whether it succeeded. HEVC-only; the result is cached
|
||||
/// by the caller ([`crate::encode::can_encode_444`]). A GPU/driver/ffmpeg without RExt 4:4:4 fails
|
||||
/// the open here, so the host resolves the session to 4:2:0 before the Welcome (honest downgrade).
|
||||
pub fn probe_can_encode_444(codec: Codec) -> bool {
|
||||
if codec != Codec::H265 {
|
||||
return false;
|
||||
}
|
||||
if ffmpeg::init().is_err() {
|
||||
return false;
|
||||
}
|
||||
// Quiet ffmpeg's open error on a GPU that lacks 4:4:4 — the probe failing is an expected outcome.
|
||||
// SAFETY: libav initialized above; `av_log_{get,set}_level` only read/write the global int level
|
||||
// (no pointer args) and are always sound post-init.
|
||||
let prev = unsafe {
|
||||
let p = ffi::av_log_get_level();
|
||||
ffi::av_log_set_level(ffi::AV_LOG_FATAL);
|
||||
p
|
||||
};
|
||||
let ok = NvencEncoder::open(
|
||||
codec,
|
||||
PixelFormat::Bgra,
|
||||
640,
|
||||
480,
|
||||
30,
|
||||
2_000_000,
|
||||
false, // CPU input (the 4:4:4 path never uses CUDA)
|
||||
8,
|
||||
ChromaFormat::Yuv444,
|
||||
)
|
||||
.is_ok();
|
||||
// SAFETY: restore the saved global log level (scalar arg, no pointers).
|
||||
unsafe { ffi::av_log_set_level(prev) };
|
||||
ok
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,51 +0,0 @@
|
||||
#version 450
|
||||
// RGB(A) -> NV12 (BT.709 limited range). One invocation per chroma sample = 2x2 luma block.
|
||||
// Optionally blends a straight-alpha RGBA cursor bitmap over the RGB *before* the YUV conversion
|
||||
// (so the chroma stays correct) — cursor-as-metadata for the GPU zero-copy paths. The blend is
|
||||
// gated by the push constant and touches only the cursor's small rectangle, so a disabled or
|
||||
// off-screen cursor costs one compare per invocation.
|
||||
layout(local_size_x = 8, local_size_y = 8) in;
|
||||
layout(binding = 0) uniform sampler2D rgb; // packed RGB input (sampled; BGRA import ok)
|
||||
layout(binding = 1, r8) uniform writeonly image2D yImg; // full-res Y
|
||||
layout(binding = 2, rg8) uniform writeonly image2D uvImg; // half-res UV (interleaved)
|
||||
layout(binding = 3) uniform sampler2D cursorTex; // straight-alpha RGBA cursor (top-left)
|
||||
|
||||
layout(push_constant) uniform Push {
|
||||
ivec2 curOrigin; // top-left of the cursor in frame pixels (position - hotspot)
|
||||
ivec2 curSize; // cursor w,h in pixels; x <= 0 => disabled
|
||||
} pc;
|
||||
|
||||
float lumaY(vec3 c) { return 16.0/255.0 + 0.1826*c.r + 0.6142*c.g + 0.0620*c.b; }
|
||||
|
||||
// Blend the cursor over `col` at frame pixel `p`, when `p` falls inside the cursor rectangle.
|
||||
vec3 withCursor(ivec2 p, vec3 col) {
|
||||
if (pc.curSize.x <= 0) return col;
|
||||
ivec2 cp = p - pc.curOrigin;
|
||||
if (cp.x < 0 || cp.y < 0 || cp.x >= pc.curSize.x || cp.y >= pc.curSize.y) return col;
|
||||
vec4 c = texelFetch(cursorTex, cp, 0);
|
||||
return mix(col, c.rgb, c.a);
|
||||
}
|
||||
|
||||
// Source may be SMALLER than the coded (16-aligned) Y plane — e.g. 1080 source vs 1088 coded. Clamp
|
||||
// every fetch to the source edge so the alignment-padding rows duplicate the last real row instead
|
||||
// of reading out of bounds (undefined → green garbage that shows if a client ignores the SPS
|
||||
// conformance-window crop). `textureSize` gives the bound source's real extent.
|
||||
void main() {
|
||||
ivec2 sz = imageSize(yImg);
|
||||
ivec2 rmax = textureSize(rgb, 0) - 1;
|
||||
ivec2 uvc = ivec2(gl_GlobalInvocationID.xy);
|
||||
ivec2 p = uvc * 2;
|
||||
if (p.x >= sz.x || p.y >= sz.y) return;
|
||||
vec3 c00 = withCursor(p, texelFetch(rgb, min(p, rmax), 0).rgb);
|
||||
vec3 c10 = withCursor(p + ivec2(1, 0), texelFetch(rgb, min(p + ivec2(1, 0), rmax), 0).rgb);
|
||||
vec3 c01 = withCursor(p + ivec2(0, 1), texelFetch(rgb, min(p + ivec2(0, 1), rmax), 0).rgb);
|
||||
vec3 c11 = withCursor(p + ivec2(1, 1), texelFetch(rgb, min(p + ivec2(1, 1), rmax), 0).rgb);
|
||||
imageStore(yImg, p, vec4(lumaY(c00), 0, 0, 1));
|
||||
imageStore(yImg, p + ivec2(1, 0), vec4(lumaY(c10), 0, 0, 1));
|
||||
imageStore(yImg, p + ivec2(0, 1), vec4(lumaY(c01), 0, 0, 1));
|
||||
imageStore(yImg, p + ivec2(1, 1), vec4(lumaY(c11), 0, 0, 1));
|
||||
vec3 a = (c00 + c10 + c01 + c11) * 0.25;
|
||||
float U = 128.0/255.0 - 0.1006*a.r - 0.3386*a.g + 0.4392*a.b;
|
||||
float V = 128.0/255.0 + 0.4392*a.r - 0.3989*a.g - 0.0403*a.b;
|
||||
imageStore(uvImg, uvc, vec4(U, V, 0, 1));
|
||||
}
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,500 +0,0 @@
|
||||
//! Vendored `VK_KHR_video_encode_av1` bindings — the AV1-encode structs, `StdVideoEncodeAV1*`
|
||||
//! types and struct-type constants that our pinned `ash 0.38.0+1.3.281` does not ship (the
|
||||
//! extension was finalized in Vulkan 1.3.290). Bumping `ash` to git-master (`+1.4.352`, which has
|
||||
//! them) breaks the *client*: it drops the lifetime on `vk::AllocationCallbacks`, and `sdl3-sys`'s
|
||||
//! `ash` feature still generates `AllocationCallbacks<'static>`, so the presenter's SDL/Vulkan
|
||||
//! surface path won't compile. Rather than churn the client for a host-only need, we vendor just
|
||||
//! the encode-side definitions here, **copied verbatim from ash-master's generated code** (so the
|
||||
//! layouts are correct-by-construction) and chain them into ash's generic video-encode-queue calls
|
||||
//! via raw `p_next`, exactly as the HEVC path already chains its rate-control struct.
|
||||
//!
|
||||
//! Everything *common* to AV1 (sequence header, tile/quant/loop-filter/CDEF/… sub-structs, the
|
||||
//! `StdVideoAV1*` enums) is already present in 1.3.281's `ash::vk::native` — AV1 **decode** brought
|
||||
//! it in — so we reuse those and vendor only the encode-specific pieces. Delete this module and
|
||||
//! switch to `ash::vk::*` once `ash` publishes a 1.4.x release and `sdl3-sys` regenerates.
|
||||
#![allow(non_snake_case, non_camel_case_types, dead_code)]
|
||||
|
||||
use ash::vk;
|
||||
use ash::vk::native::{
|
||||
StdVideoAV1FrameType, StdVideoAV1InterpolationFilter, StdVideoAV1Level, StdVideoAV1Profile,
|
||||
StdVideoAV1SequenceHeader, StdVideoAV1TxMode,
|
||||
};
|
||||
use std::ffi::{c_void, CStr};
|
||||
|
||||
/// `VK_KHR_video_encode_av1` extension name — ash 0.38's `ash::khr::video_encode_av1` doesn't exist,
|
||||
/// so we pass this raw to `enabled_extension_names`.
|
||||
pub const EXTENSION_NAME: &CStr = c"VK_KHR_video_encode_av1";
|
||||
|
||||
// ---------- struct-type (VkStructureType) values — construct via `vk::StructureType::from_raw` ----------
|
||||
pub const ST_CAPABILITIES: i32 = 1_000_513_000;
|
||||
pub const ST_SESSION_PARAMETERS_CREATE_INFO: i32 = 1_000_513_001;
|
||||
pub const ST_PICTURE_INFO: i32 = 1_000_513_002;
|
||||
pub const ST_DPB_SLOT_INFO: i32 = 1_000_513_003;
|
||||
pub const ST_PHYSICAL_DEVICE_FEATURES: i32 = 1_000_513_004;
|
||||
pub const ST_PROFILE_INFO: i32 = 1_000_513_005;
|
||||
pub const ST_RATE_CONTROL_INFO: i32 = 1_000_513_006;
|
||||
pub const ST_RATE_CONTROL_LAYER_INFO: i32 = 1_000_513_007;
|
||||
pub const ST_SESSION_CREATE_INFO: i32 = 1_000_513_009;
|
||||
pub const ST_GOP_REMAINING_FRAME_INFO: i32 = 1_000_513_010;
|
||||
|
||||
/// `VK_VIDEO_CODEC_OPERATION_ENCODE_AV1_BIT_KHR` (bit 18).
|
||||
pub const VIDEO_CODEC_OPERATION_ENCODE_AV1: u32 = 0x0004_0000;
|
||||
/// `VK_MAX_VIDEO_AV1_REFERENCES_PER_FRAME_KHR` — LAST..ALTREF (the 7 inter reference names).
|
||||
pub const MAX_VIDEO_AV1_REFERENCES_PER_FRAME: usize = 7;
|
||||
/// `STD_VIDEO_AV1_PRIMARY_REF_NONE` — a frame that inherits no CDF/context from any reference
|
||||
/// (the recovery-anchor lever: a clean P-frame independent of prior probability state).
|
||||
pub const PRIMARY_REF_NONE: u8 = 7;
|
||||
/// `VK_VIDEO_ENCODE_AV1_SUPERBLOCK_SIZE_128_BIT_KHR` (bit 1 of the superblock-size flags).
|
||||
pub const SUPERBLOCK_SIZE_128: u32 = 0x2;
|
||||
|
||||
// `VkVideoEncodeAV1PredictionModeKHR`
|
||||
pub const PREDICTION_MODE_INTRA_ONLY: i32 = 0;
|
||||
pub const PREDICTION_MODE_SINGLE_REFERENCE: i32 = 1;
|
||||
// `VkVideoEncodeAV1RateControlGroupKHR`
|
||||
pub const RC_GROUP_INTRA: i32 = 0;
|
||||
pub const RC_GROUP_PREDICTIVE: i32 = 1;
|
||||
pub const RC_GROUP_BIPREDICTIVE: i32 = 2;
|
||||
|
||||
// AV1 reference names (index into `reference_name_slot_indices`, which is 0-based over LAST..ALTREF).
|
||||
pub const REFERENCE_NAME_LAST_FRAME_IDX: usize = 0; // STD_VIDEO_AV1_REFERENCE_NAME_LAST_FRAME - 1
|
||||
|
||||
// ---------- bindgen bitfield helper (copied verbatim from ash-master native.rs) ----------
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Default, Copy, Clone)]
|
||||
pub struct __BindgenBitfieldUnit<Storage> {
|
||||
storage: Storage,
|
||||
}
|
||||
impl<Storage> __BindgenBitfieldUnit<Storage> {
|
||||
#[inline]
|
||||
pub const fn new(storage: Storage) -> Self {
|
||||
Self { storage }
|
||||
}
|
||||
}
|
||||
impl<Storage> __BindgenBitfieldUnit<Storage>
|
||||
where
|
||||
Storage: AsRef<[u8]> + AsMut<[u8]>,
|
||||
{
|
||||
#[inline]
|
||||
pub fn get_bit(&self, index: usize) -> bool {
|
||||
let byte_index = index / 8;
|
||||
let byte = self.storage.as_ref()[byte_index];
|
||||
let bit_index = if cfg!(target_endian = "big") {
|
||||
7 - (index % 8)
|
||||
} else {
|
||||
index % 8
|
||||
};
|
||||
byte & (1 << bit_index) == (1 << bit_index)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_bit(&mut self, index: usize, val: bool) {
|
||||
let byte_index = index / 8;
|
||||
let byte = &mut self.storage.as_mut()[byte_index];
|
||||
let bit_index = if cfg!(target_endian = "big") {
|
||||
7 - (index % 8)
|
||||
} else {
|
||||
index % 8
|
||||
};
|
||||
let mask = 1 << bit_index;
|
||||
if val {
|
||||
*byte |= mask;
|
||||
} else {
|
||||
*byte &= !mask;
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
pub fn get(&self, bit_offset: usize, bit_width: u8) -> u64 {
|
||||
let mut val = 0;
|
||||
for i in 0..(bit_width as usize) {
|
||||
if self.get_bit(i + bit_offset) {
|
||||
let index = if cfg!(target_endian = "big") {
|
||||
bit_width as usize - 1 - i
|
||||
} else {
|
||||
i
|
||||
};
|
||||
val |= 1 << index;
|
||||
}
|
||||
}
|
||||
val
|
||||
}
|
||||
#[inline]
|
||||
pub fn set(&mut self, bit_offset: usize, bit_width: u8, val: u64) {
|
||||
for i in 0..(bit_width as usize) {
|
||||
let mask = 1 << i;
|
||||
let val_bit_is_set = val & mask == mask;
|
||||
let index = if cfg!(target_endian = "big") {
|
||||
bit_width as usize - 1 - i
|
||||
} else {
|
||||
i
|
||||
};
|
||||
self.set_bit(index + bit_offset, val_bit_is_set);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Std encode structs (copied from ash-master native.rs; common Std types reused from ash) ----------
|
||||
#[repr(C, align(4))]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct StdVideoEncodeAV1PictureInfoFlags {
|
||||
pub _bitfield_align_1: [u8; 0],
|
||||
pub _bitfield_1: __BindgenBitfieldUnit<[u8; 4usize]>,
|
||||
}
|
||||
impl StdVideoEncodeAV1PictureInfoFlags {
|
||||
#[inline]
|
||||
pub fn set_error_resilient_mode(&mut self, val: u32) {
|
||||
self._bitfield_1.set(0, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_disable_cdf_update(&mut self, val: u32) {
|
||||
self._bitfield_1.set(1, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_use_superres(&mut self, val: u32) {
|
||||
self._bitfield_1.set(2, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_render_and_frame_size_different(&mut self, val: u32) {
|
||||
self._bitfield_1.set(3, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_allow_screen_content_tools(&mut self, val: u32) {
|
||||
self._bitfield_1.set(4, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_is_filter_switchable(&mut self, val: u32) {
|
||||
self._bitfield_1.set(5, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_force_integer_mv(&mut self, val: u32) {
|
||||
self._bitfield_1.set(6, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_frame_size_override_flag(&mut self, val: u32) {
|
||||
self._bitfield_1.set(7, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_buffer_removal_time_present_flag(&mut self, val: u32) {
|
||||
self._bitfield_1.set(8, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_allow_intrabc(&mut self, val: u32) {
|
||||
self._bitfield_1.set(9, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_frame_refs_short_signaling(&mut self, val: u32) {
|
||||
self._bitfield_1.set(10, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_allow_high_precision_mv(&mut self, val: u32) {
|
||||
self._bitfield_1.set(11, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_is_motion_mode_switchable(&mut self, val: u32) {
|
||||
self._bitfield_1.set(12, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_use_ref_frame_mvs(&mut self, val: u32) {
|
||||
self._bitfield_1.set(13, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_disable_frame_end_update_cdf(&mut self, val: u32) {
|
||||
self._bitfield_1.set(14, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_allow_warped_motion(&mut self, val: u32) {
|
||||
self._bitfield_1.set(15, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_reduced_tx_set(&mut self, val: u32) {
|
||||
self._bitfield_1.set(16, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_skip_mode_present(&mut self, val: u32) {
|
||||
self._bitfield_1.set(17, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_delta_q_present(&mut self, val: u32) {
|
||||
self._bitfield_1.set(18, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_delta_lf_present(&mut self, val: u32) {
|
||||
self._bitfield_1.set(19, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_delta_lf_multi(&mut self, val: u32) {
|
||||
self._bitfield_1.set(20, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_segmentation_enabled(&mut self, val: u32) {
|
||||
self._bitfield_1.set(21, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_segmentation_update_map(&mut self, val: u32) {
|
||||
self._bitfield_1.set(22, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_segmentation_temporal_update(&mut self, val: u32) {
|
||||
self._bitfield_1.set(23, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_segmentation_update_data(&mut self, val: u32) {
|
||||
self._bitfield_1.set(24, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_UsesLr(&mut self, val: u32) {
|
||||
self._bitfield_1.set(25, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_usesChromaLr(&mut self, val: u32) {
|
||||
self._bitfield_1.set(26, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_show_frame(&mut self, val: u32) {
|
||||
self._bitfield_1.set(27, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_showable_frame(&mut self, val: u32) {
|
||||
self._bitfield_1.set(28, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_reserved(&mut self, val: u32) {
|
||||
self._bitfield_1.set(29, 3, val as u64)
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct StdVideoEncodeAV1PictureInfo {
|
||||
pub flags: StdVideoEncodeAV1PictureInfoFlags,
|
||||
pub frame_type: StdVideoAV1FrameType,
|
||||
pub frame_presentation_time: u32,
|
||||
pub current_frame_id: u32,
|
||||
pub order_hint: u8,
|
||||
pub primary_ref_frame: u8,
|
||||
pub refresh_frame_flags: u8,
|
||||
pub coded_denom: u8,
|
||||
pub render_width_minus_1: u16,
|
||||
pub render_height_minus_1: u16,
|
||||
pub interpolation_filter: StdVideoAV1InterpolationFilter,
|
||||
pub TxMode: StdVideoAV1TxMode,
|
||||
pub delta_q_res: u8,
|
||||
pub delta_lf_res: u8,
|
||||
pub ref_order_hint: [u8; 8usize],
|
||||
pub ref_frame_idx: [i8; 7usize],
|
||||
pub reserved1: [u8; 3usize],
|
||||
pub delta_frame_id_minus_1: [u32; 7usize],
|
||||
pub pTileInfo: *const ash::vk::native::StdVideoAV1TileInfo,
|
||||
pub pQuantization: *const ash::vk::native::StdVideoAV1Quantization,
|
||||
pub pSegmentation: *const ash::vk::native::StdVideoAV1Segmentation,
|
||||
pub pLoopFilter: *const ash::vk::native::StdVideoAV1LoopFilter,
|
||||
pub pCDEF: *const ash::vk::native::StdVideoAV1CDEF,
|
||||
pub pLoopRestoration: *const ash::vk::native::StdVideoAV1LoopRestoration,
|
||||
pub pGlobalMotion: *const ash::vk::native::StdVideoAV1GlobalMotion,
|
||||
pub pExtensionHeader: *const StdVideoEncodeAV1ExtensionHeader,
|
||||
pub pBufferRemovalTimes: *const u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct StdVideoEncodeAV1ReferenceInfoFlags {
|
||||
pub _bitfield_align_1: [u32; 0],
|
||||
pub _bitfield_1: __BindgenBitfieldUnit<[u8; 4usize]>,
|
||||
}
|
||||
impl StdVideoEncodeAV1ReferenceInfoFlags {
|
||||
#[inline]
|
||||
pub fn set_disable_frame_end_update_cdf(&mut self, val: u32) {
|
||||
self._bitfield_1.set(0, 1, val as u64)
|
||||
}
|
||||
#[inline]
|
||||
pub fn set_segmentation_enabled(&mut self, val: u32) {
|
||||
self._bitfield_1.set(1, 1, val as u64)
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct StdVideoEncodeAV1ReferenceInfo {
|
||||
pub flags: StdVideoEncodeAV1ReferenceInfoFlags,
|
||||
pub RefFrameId: u32,
|
||||
pub frame_type: StdVideoAV1FrameType,
|
||||
pub OrderHint: u8,
|
||||
pub reserved1: [u8; 3usize],
|
||||
pub pExtensionHeader: *const StdVideoEncodeAV1ExtensionHeader,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct StdVideoEncodeAV1ExtensionHeader {
|
||||
pub temporal_id: u8,
|
||||
pub spatial_id: u8,
|
||||
}
|
||||
|
||||
// ---------- KHR extension structs (repr(C); lifetimes/PhantomData dropped — layout-identical,
|
||||
// chained by raw p_next). Flag/enum newtypes flattened to their u32/i32 repr. ----------
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct VideoEncodeAV1ProfileInfoKHR {
|
||||
pub s_type: vk::StructureType,
|
||||
pub p_next: *const c_void,
|
||||
pub std_profile: StdVideoAV1Profile,
|
||||
}
|
||||
|
||||
/// `VkPhysicalDeviceVideoEncodeAV1FeaturesKHR` — the `videoEncodeAV1` feature MUST be enabled at
|
||||
/// device creation for any `VK_VIDEO_CODEC_OPERATION_ENCODE_AV1` use (a spec requirement RADV may
|
||||
/// tolerate omitting but validation layers and stricter drivers do not).
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct PhysicalDeviceVideoEncodeAV1FeaturesKHR {
|
||||
pub s_type: vk::StructureType,
|
||||
pub p_next: *mut c_void,
|
||||
pub video_encode_av1: vk::Bool32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct VideoEncodeAV1CapabilitiesKHR {
|
||||
pub s_type: vk::StructureType,
|
||||
pub p_next: *mut c_void,
|
||||
pub flags: u32,
|
||||
pub max_level: StdVideoAV1Level,
|
||||
pub coded_picture_alignment: vk::Extent2D,
|
||||
pub max_tiles: vk::Extent2D,
|
||||
pub min_tile_size: vk::Extent2D,
|
||||
pub max_tile_size: vk::Extent2D,
|
||||
pub superblock_sizes: u32,
|
||||
pub max_single_reference_count: u32,
|
||||
pub single_reference_name_mask: u32,
|
||||
pub max_unidirectional_compound_reference_count: u32,
|
||||
pub max_unidirectional_compound_group1_reference_count: u32,
|
||||
pub unidirectional_compound_reference_name_mask: u32,
|
||||
pub max_bidirectional_compound_reference_count: u32,
|
||||
pub max_bidirectional_compound_group1_reference_count: u32,
|
||||
pub max_bidirectional_compound_group2_reference_count: u32,
|
||||
pub bidirectional_compound_reference_name_mask: u32,
|
||||
pub max_temporal_layer_count: u32,
|
||||
pub max_spatial_layer_count: u32,
|
||||
pub max_operating_points: u32,
|
||||
pub min_q_index: u32,
|
||||
pub max_q_index: u32,
|
||||
pub prefers_gop_remaining_frames: vk::Bool32,
|
||||
pub requires_gop_remaining_frames: vk::Bool32,
|
||||
pub std_syntax_flags: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct VideoEncodeAV1SessionParametersCreateInfoKHR {
|
||||
pub s_type: vk::StructureType,
|
||||
pub p_next: *const c_void,
|
||||
pub p_std_sequence_header: *const StdVideoAV1SequenceHeader,
|
||||
pub p_std_decoder_model_info: *const c_void,
|
||||
pub std_operating_point_count: u32,
|
||||
pub p_std_operating_points: *const c_void,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct VideoEncodeAV1PictureInfoKHR {
|
||||
pub s_type: vk::StructureType,
|
||||
pub p_next: *const c_void,
|
||||
pub prediction_mode: i32,
|
||||
pub rate_control_group: i32,
|
||||
pub constant_q_index: u32,
|
||||
pub p_std_picture_info: *const StdVideoEncodeAV1PictureInfo,
|
||||
pub reference_name_slot_indices: [i32; MAX_VIDEO_AV1_REFERENCES_PER_FRAME],
|
||||
pub primary_reference_cdf_only: vk::Bool32,
|
||||
pub generate_obu_extension_header: vk::Bool32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct VideoEncodeAV1DpbSlotInfoKHR {
|
||||
pub s_type: vk::StructureType,
|
||||
pub p_next: *const c_void,
|
||||
pub p_std_reference_info: *const StdVideoEncodeAV1ReferenceInfo,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct VideoEncodeAV1RateControlInfoKHR {
|
||||
pub s_type: vk::StructureType,
|
||||
pub p_next: *const c_void,
|
||||
pub flags: u32,
|
||||
pub gop_frame_count: u32,
|
||||
pub key_frame_period: u32,
|
||||
pub consecutive_bipredictive_frame_count: u32,
|
||||
pub temporal_layer_count: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct VideoEncodeAV1QIndexKHR {
|
||||
pub intra_q_index: u32,
|
||||
pub predictive_q_index: u32,
|
||||
pub bipredictive_q_index: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct VideoEncodeAV1FrameSizeKHR {
|
||||
pub intra_frame_size: u32,
|
||||
pub predictive_frame_size: u32,
|
||||
pub bipredictive_frame_size: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct VideoEncodeAV1RateControlLayerInfoKHR {
|
||||
pub s_type: vk::StructureType,
|
||||
pub p_next: *const c_void,
|
||||
pub use_min_q_index: vk::Bool32,
|
||||
pub min_q_index: VideoEncodeAV1QIndexKHR,
|
||||
pub use_max_q_index: vk::Bool32,
|
||||
pub max_q_index: VideoEncodeAV1QIndexKHR,
|
||||
pub use_max_frame_size: vk::Bool32,
|
||||
pub max_frame_size: VideoEncodeAV1FrameSizeKHR,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct VideoEncodeAV1GopRemainingFrameInfoKHR {
|
||||
pub s_type: vk::StructureType,
|
||||
pub p_next: *const c_void,
|
||||
pub use_gop_remaining_frames: vk::Bool32,
|
||||
pub gop_remaining_intra: u32,
|
||||
pub gop_remaining_predictive: u32,
|
||||
pub gop_remaining_bipredictive: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct VideoEncodeAV1SessionCreateInfoKHR {
|
||||
pub s_type: vk::StructureType,
|
||||
pub p_next: *const c_void,
|
||||
pub use_max_level: vk::Bool32,
|
||||
pub max_level: StdVideoAV1Level,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct StdVideoEncodeAV1OperatingPointInfoFlags {
|
||||
pub _bitfield_align_1: [u32; 0],
|
||||
pub _bitfield_1: __BindgenBitfieldUnit<[u8; 4usize]>,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct StdVideoEncodeAV1OperatingPointInfo {
|
||||
pub flags: StdVideoEncodeAV1OperatingPointInfoFlags,
|
||||
pub operating_point_idc: u16,
|
||||
pub seq_level_idx: u8,
|
||||
pub seq_tier: u8,
|
||||
pub decoder_buffer_delay: u32,
|
||||
pub encoder_buffer_delay: u32,
|
||||
pub initial_display_delay_minus_1: u8,
|
||||
}
|
||||
|
||||
/// `vk::StructureType` for a raw `ST_*` constant above.
|
||||
#[inline]
|
||||
pub fn stype(raw: i32) -> vk::StructureType {
|
||||
vk::StructureType::from_raw(raw)
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
//! Small ash/Vulkan leaf helpers shared by the Linux Vulkan encode backends
|
||||
//! (`vulkan_video.rs`, `pyrowave.rs`) — extracted verbatim from `vulkan_video.rs`
|
||||
//! when the PyroWave backend arrived so the two don't fork copies.
|
||||
// Every unsafe block carries a `// SAFETY:` proof (parent module enforces it).
|
||||
|
||||
use crate::capture::PixelFormat;
|
||||
use anyhow::Result;
|
||||
use ash::vk;
|
||||
|
||||
pub(crate) fn color_range(layer: u32) -> vk::ImageSubresourceRange {
|
||||
vk::ImageSubresourceRange {
|
||||
aspect_mask: vk::ImageAspectFlags::COLOR,
|
||||
base_mip_level: 0,
|
||||
level_count: 1,
|
||||
base_array_layer: layer,
|
||||
layer_count: 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn find_mem(
|
||||
mp: &vk::PhysicalDeviceMemoryProperties,
|
||||
bits: u32,
|
||||
want: vk::MemoryPropertyFlags,
|
||||
) -> u32 {
|
||||
for i in 0..mp.memory_type_count {
|
||||
if (bits & (1 << i)) != 0 && mp.memory_types[i as usize].property_flags.contains(want) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
/// DRM fourcc -> the VkFormat whose *color* components match (Vulkan handles the byte swizzle).
|
||||
pub(crate) fn fourcc_to_vk(fourcc: u32) -> Option<vk::Format> {
|
||||
// fourcc_code(a,b,c,d) = a | b<<8 | c<<16 | d<<24
|
||||
const XR24: u32 = 0x3432_5258; // XRGB8888
|
||||
const AR24: u32 = 0x3432_5241; // ARGB8888
|
||||
const XB24: u32 = 0x3432_4258; // XBGR8888
|
||||
const AB24: u32 = 0x3432_4241; // ABGR8888
|
||||
match fourcc {
|
||||
XR24 | AR24 => Some(vk::Format::B8G8R8A8_UNORM),
|
||||
XB24 | AB24 => Some(vk::Format::R8G8B8A8_UNORM),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn pixel_to_vk(fmt: PixelFormat) -> Option<vk::Format> {
|
||||
match fmt {
|
||||
PixelFormat::Bgrx | PixelFormat::Bgra => Some(vk::Format::B8G8R8A8_UNORM),
|
||||
PixelFormat::Rgbx | PixelFormat::Rgba => Some(vk::Format::R8G8B8A8_UNORM),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn make_view(
|
||||
device: &ash::Device,
|
||||
image: vk::Image,
|
||||
fmt: vk::Format,
|
||||
layer: u32,
|
||||
) -> Result<vk::ImageView> {
|
||||
Ok(device.create_image_view(
|
||||
&vk::ImageViewCreateInfo::default()
|
||||
.image(image)
|
||||
.view_type(vk::ImageViewType::TYPE_2D)
|
||||
.format(fmt)
|
||||
.subresource_range(color_range(layer)),
|
||||
None,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Import a packed-RGB dmabuf as a SAMPLED VkImage (explicit DRM modifier). Caller destroys all
|
||||
/// three returned handles. Extracted verbatim from `vulkan_video.rs`'s import path.
|
||||
pub(crate) unsafe fn import_rgb_dmabuf(
|
||||
device: &ash::Device,
|
||||
ext_fd: &ash::khr::external_memory_fd::Device,
|
||||
mem_props: &vk::PhysicalDeviceMemoryProperties,
|
||||
d: &crate::capture::DmabufFrame,
|
||||
cw: u32,
|
||||
ch: u32,
|
||||
) -> Result<(vk::Image, vk::DeviceMemory, vk::ImageView)> {
|
||||
use anyhow::Context;
|
||||
use std::os::fd::IntoRawFd;
|
||||
let fmt = fourcc_to_vk(d.fourcc)
|
||||
.with_context(|| format!("unsupported dmabuf fourcc {:#x}", d.fourcc))?;
|
||||
let plane = [vk::SubresourceLayout::default()
|
||||
.offset(d.offset as u64)
|
||||
.row_pitch(d.stride as u64)];
|
||||
let mut drm = vk::ImageDrmFormatModifierExplicitCreateInfoEXT::default()
|
||||
.drm_format_modifier(d.modifier)
|
||||
.plane_layouts(&plane);
|
||||
let mut ext = vk::ExternalMemoryImageCreateInfo::default()
|
||||
.handle_types(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT);
|
||||
let img = device.create_image(
|
||||
&vk::ImageCreateInfo::default()
|
||||
.image_type(vk::ImageType::TYPE_2D)
|
||||
.format(fmt)
|
||||
.extent(vk::Extent3D {
|
||||
width: cw,
|
||||
height: ch,
|
||||
depth: 1,
|
||||
})
|
||||
.mip_levels(1)
|
||||
.array_layers(1)
|
||||
.samples(vk::SampleCountFlags::TYPE_1)
|
||||
.tiling(vk::ImageTiling::DRM_FORMAT_MODIFIER_EXT)
|
||||
.usage(vk::ImageUsageFlags::SAMPLED)
|
||||
.sharing_mode(vk::SharingMode::EXCLUSIVE)
|
||||
.initial_layout(vk::ImageLayout::UNDEFINED)
|
||||
.push_next(&mut ext)
|
||||
.push_next(&mut drm),
|
||||
None,
|
||||
)?;
|
||||
// dup the fd; Vulkan takes ownership of the dup on a successful import.
|
||||
let dup = d.fd.try_clone().context("dup dmabuf fd")?.into_raw_fd();
|
||||
let fd_props = {
|
||||
let mut p = vk::MemoryFdPropertiesKHR::default();
|
||||
let _ = (ext_fd.fp().get_memory_fd_properties_khr)(
|
||||
device.handle(),
|
||||
vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT,
|
||||
dup,
|
||||
&mut p,
|
||||
);
|
||||
p.memory_type_bits
|
||||
};
|
||||
let req = device.get_image_memory_requirements(img);
|
||||
let bits = req.memory_type_bits & fd_props;
|
||||
let ti = find_mem(
|
||||
mem_props,
|
||||
if bits != 0 {
|
||||
bits
|
||||
} else {
|
||||
req.memory_type_bits
|
||||
},
|
||||
vk::MemoryPropertyFlags::empty(),
|
||||
);
|
||||
let mut ded = vk::MemoryDedicatedAllocateInfo::default().image(img);
|
||||
let mut import = vk::ImportMemoryFdInfoKHR::default()
|
||||
.handle_type(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT)
|
||||
.fd(dup);
|
||||
let mem = device.allocate_memory(
|
||||
&vk::MemoryAllocateInfo::default()
|
||||
.allocation_size(req.size)
|
||||
.memory_type_index(ti)
|
||||
.push_next(&mut ded)
|
||||
.push_next(&mut import),
|
||||
None,
|
||||
)?;
|
||||
device.bind_image_memory(img, mem, 0)?;
|
||||
let view = device.create_image_view(
|
||||
&vk::ImageViewCreateInfo::default()
|
||||
.image(img)
|
||||
.view_type(vk::ImageViewType::TYPE_2D)
|
||||
.format(fmt)
|
||||
.subresource_range(color_range(0)),
|
||||
None,
|
||||
)?;
|
||||
Ok((img, mem, view))
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn make_plain_image(
|
||||
device: &ash::Device,
|
||||
mp: &vk::PhysicalDeviceMemoryProperties,
|
||||
fmt: vk::Format,
|
||||
w: u32,
|
||||
h: u32,
|
||||
usage: vk::ImageUsageFlags,
|
||||
) -> Result<(vk::Image, vk::DeviceMemory, vk::ImageView)> {
|
||||
let img = device.create_image(
|
||||
&vk::ImageCreateInfo::default()
|
||||
.image_type(vk::ImageType::TYPE_2D)
|
||||
.format(fmt)
|
||||
.extent(vk::Extent3D {
|
||||
width: w,
|
||||
height: h,
|
||||
depth: 1,
|
||||
})
|
||||
.mip_levels(1)
|
||||
.array_layers(1)
|
||||
.samples(vk::SampleCountFlags::TYPE_1)
|
||||
.tiling(vk::ImageTiling::OPTIMAL)
|
||||
.usage(usage)
|
||||
.initial_layout(vk::ImageLayout::UNDEFINED),
|
||||
None,
|
||||
)?;
|
||||
let req = device.get_image_memory_requirements(img);
|
||||
let mem = device.allocate_memory(
|
||||
&vk::MemoryAllocateInfo::default()
|
||||
.allocation_size(req.size)
|
||||
.memory_type_index(find_mem(
|
||||
mp,
|
||||
req.memory_type_bits,
|
||||
vk::MemoryPropertyFlags::DEVICE_LOCAL,
|
||||
)),
|
||||
None,
|
||||
)?;
|
||||
device.bind_image_memory(img, mem, 0)?;
|
||||
let view = make_view(device, img, fmt, 0)?;
|
||||
Ok((img, mem, view))
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,245 +0,0 @@
|
||||
//! Shared direct-SDK NVENC core — the platform-agnostic pieces of the two `nvEncodeAPI` backends,
|
||||
//! Windows D3D11 (`encode/windows/nvenc.rs`) and Linux CUDA (`encode/linux/nvenc_cuda.rs`), so the
|
||||
//! byte-identical glue lives once (plan §2.2, the direct-NVENC Tier-2). The per-platform parts —
|
||||
//! the entry-table load (`nvEncodeAPI64.dll` via `LoadLibrary` vs `libnvidia-encode.so` via
|
||||
//! `libloading`), the device binding (D3D11 vs CUDA), input-surface registration, and the
|
||||
//! Windows-only async retrieve — stay in their backends. Sibling of [`super::nvenc_status`].
|
||||
|
||||
use super::Codec;
|
||||
use nvidia_video_codec_sdk::sys::nvEncodeAPI as nv;
|
||||
|
||||
/// Local `NVENCSTATUS` → `Result` (replaces the sdk's `result_without_string`, which lives in the
|
||||
/// crate's `safe` module — code these backends must not pull in). The raw status's Debug repr
|
||||
/// (`NV_ENC_ERR_INVALID_PARAM`, …) is the error payload; callers fold it through
|
||||
/// [`super::nvenc_status`] for an operator-actionable cause.
|
||||
pub(super) trait NvStatusExt {
|
||||
fn nv_ok(self) -> std::result::Result<(), nv::NVENCSTATUS>;
|
||||
}
|
||||
impl NvStatusExt for nv::NVENCSTATUS {
|
||||
fn nv_ok(self) -> std::result::Result<(), nv::NVENCSTATUS> {
|
||||
match self {
|
||||
nv::NVENCSTATUS::NV_ENC_SUCCESS => Ok(()),
|
||||
err => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The NVENC codec GUID for a session [`Codec`]. PyroWave never opens the direct-NVENC backend
|
||||
/// (guarded by the `open_video` dispatch), so it is unreachable here.
|
||||
pub(super) fn codec_guid(codec: Codec) -> nv::GUID {
|
||||
match codec {
|
||||
Codec::H264 => nv::NV_ENC_CODEC_H264_GUID,
|
||||
Codec::H265 => nv::NV_ENC_CODEC_HEVC_GUID,
|
||||
Codec::Av1 => nv::NV_ENC_CODEC_AV1_GUID,
|
||||
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Reference-frame DPB depth when RFI is supported (Apollo uses 5). A deeper DPB lets an invalidated
|
||||
/// reference fall back to an older still-valid frame instead of a full IDR; `numRefL0 = 1` keeps each
|
||||
/// P-frame single-reference for low latency. Also the window the backends' `invalidate_ref_frames`
|
||||
/// paths check against (`frame_idx - RFI_DPB` = the oldest frame still in the DPB).
|
||||
pub(super) const RFI_DPB: u32 = 5;
|
||||
|
||||
/// The per-session knobs both direct-NVENC backends feed [`apply_low_latency_config`]. `Copy` so the
|
||||
/// backend fills it from `self` at the call. The two input-format fields bridge the only real
|
||||
/// divergence between the CUDA and D3D11 paths (which surface formats can carry full chroma / 10-bit
|
||||
/// input); everything else in the config is identical across platforms.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) struct LowLatencyConfig {
|
||||
pub codec: Codec,
|
||||
/// Target bitrate (bps); CBR average == max.
|
||||
pub bitrate: u64,
|
||||
pub fps: u32,
|
||||
/// This GPU advertises custom VBV — else leave the preset default (per the caps probe).
|
||||
pub custom_vbv: bool,
|
||||
/// A 4:4:4 session was negotiated (HEVC Range Extensions).
|
||||
pub chroma_444: bool,
|
||||
/// The input surface can carry full chroma — Linux feeds a YUV444 surface, Windows a packed-RGB
|
||||
/// surface NVENC CSCs internally. 4:4:4 engages only when this AND [`chroma_444`](Self::chroma_444).
|
||||
pub full_chroma_input: bool,
|
||||
/// Output bit depth (8 or 10).
|
||||
pub bit_depth: u8,
|
||||
/// AV1 `inputPixelBitDepthMinus8` — the encoder's view of the INPUT depth (Linux is 8-bit in
|
||||
/// today, so 0; Windows derives it from the surface format). `u32` to match the SDK setter.
|
||||
pub av1_input_depth_minus8: u32,
|
||||
pub hdr: bool,
|
||||
/// This GPU supports reference-frame invalidation (a deeper DPB for graceful loss recovery).
|
||||
pub rfi_supported: bool,
|
||||
}
|
||||
|
||||
/// Author the shared `NV_ENC_INITIALIZE_PARAMS` (P1/ULL preset, PTD, the session dimensions/rate)
|
||||
/// pointing at `cfg`. `enable_async` drives the Windows two-thread async retrieve — Linux is
|
||||
/// sync-only and passes `false`, leaving `enableEncodeAsync` at 0 as before. The returned struct
|
||||
/// borrows `cfg` as a raw pointer; the caller must keep `cfg` alive across the NVENC call it feeds
|
||||
/// this into. Used at open and at in-place reconfigure, which must present the SAME init params.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn build_init_params(
|
||||
codec_guid: nv::GUID,
|
||||
width: u32,
|
||||
height: u32,
|
||||
fps: u32,
|
||||
cfg: &mut nv::NV_ENC_CONFIG,
|
||||
split_mode: u32,
|
||||
enable_async: bool,
|
||||
) -> nv::NV_ENC_INITIALIZE_PARAMS {
|
||||
let mut init = nv::NV_ENC_INITIALIZE_PARAMS {
|
||||
version: nv::NV_ENC_INITIALIZE_PARAMS_VER,
|
||||
encodeGUID: codec_guid,
|
||||
presetGUID: nv::NV_ENC_PRESET_P1_GUID,
|
||||
tuningInfo: nv::NV_ENC_TUNING_INFO::NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY,
|
||||
encodeWidth: width,
|
||||
encodeHeight: height,
|
||||
darWidth: width,
|
||||
darHeight: height,
|
||||
frameRateNum: fps,
|
||||
frameRateDen: 1,
|
||||
enablePTD: 1,
|
||||
enableEncodeAsync: enable_async as u32,
|
||||
encodeConfig: cfg,
|
||||
..Default::default()
|
||||
};
|
||||
// splitEncodeMode is a C bitfield — set via the generated accessor, not a struct field.
|
||||
init.set_splitEncodeMode(split_mode);
|
||||
init
|
||||
}
|
||||
|
||||
/// Author the shared low-latency NVENC config onto a **preset-seeded** `cfg`: CBR + infinite GOP +
|
||||
/// P-only + ~1-frame VBV, per-codec tier/level, chroma + bit depth, unconditional colour signaling,
|
||||
/// and the RFI DPB. The caller seeds `cfg` from the P1/ULL preset first (that call needs the
|
||||
/// per-platform entry table) and passes the input-format specifics via [`LowLatencyConfig`]; the
|
||||
/// per-platform surface registration, device binding, and async retrieve stay in the backends.
|
||||
///
|
||||
/// # Safety
|
||||
/// Writes NVENC codec-config union fields on `cfg`, which must be a valid, preset-seeded
|
||||
/// `NV_ENC_CONFIG` whose active union arm matches [`LowLatencyConfig::codec`].
|
||||
pub(super) unsafe fn apply_low_latency_config(cfg: &mut nv::NV_ENC_CONFIG, c: LowLatencyConfig) {
|
||||
// CBR, infinite GOP, P-only, ~1-frame VBV — mirrors the AMF/VAAPI/QSV libav RC config.
|
||||
cfg.gopLength = nv::NVENC_INFINITE_GOPLENGTH;
|
||||
cfg.frameIntervalP = 1;
|
||||
cfg.rcParams.rateControlMode = nv::NV_ENC_PARAMS_RC_MODE::NV_ENC_PARAMS_RC_CBR;
|
||||
let bps = c.bitrate.min(u32::MAX as u64) as u32;
|
||||
cfg.rcParams.averageBitRate = bps;
|
||||
cfg.rcParams.maxBitRate = bps;
|
||||
// Shrink the VBV with the bitrate (NVENC validates it against the same level ceiling), but only
|
||||
// when the GPU advertises custom-VBV support — else keep the preset default.
|
||||
if c.custom_vbv {
|
||||
// ~1-frame VBV by default; PUNKTFUNK_VBV_FRAMES scales it (parity with AMF/VAAPI/QSV).
|
||||
let vbv = ((c.bitrate as f64 / c.fps.max(1) as f64) * crate::encode::vbv_frames_env())
|
||||
.clamp(1.0, u32::MAX as f64) as u32;
|
||||
cfg.rcParams.vbvBufferSize = vbv;
|
||||
cfg.rcParams.vbvInitialDelay = vbv;
|
||||
}
|
||||
|
||||
// Tier + autoselect level, PER CODEC (the union writes must match the negotiated codec). HEVC
|
||||
// keeps HIGH tier for its higher per-level bitrate ceiling (Main at 5K ≈ 240 Mbps, HIGH ≈ 800
|
||||
// Mbps); AV1 supports the Main tier ONLY — tier=1 fails the session open with INVALID_PARAM, and
|
||||
// its level enum's 0 is LEVEL 2.0 (NOT autoselect), so AV1 takes NO writes (the preset defaults
|
||||
// are the only accepted config). H.264 has no tier. Level 0 = autoselect for HEVC.
|
||||
match c.codec {
|
||||
Codec::H265 => {
|
||||
cfg.encodeCodecConfig.hevcConfig.tier = 1;
|
||||
cfg.encodeCodecConfig.hevcConfig.level = 0;
|
||||
}
|
||||
Codec::Av1 => {}
|
||||
Codec::H264 => {}
|
||||
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
|
||||
}
|
||||
|
||||
// Chroma + bit depth. Full-chroma 4:4:4 (HEVC Range Extensions, chromaFormatIDC=3 under the FREXT
|
||||
// profile) takes precedence and composes with 10-bit (Main 4:4:4 10); it needs a full-chroma-
|
||||
// capable input. Otherwise 10-bit selects Main10 (HEVC) or the AV1 output depth — stamping the
|
||||
// HEVC Main10 GUID onto an AV1 session is an INVALID_PARAM, so bit depth is set PER CODEC.
|
||||
if c.chroma_444 && c.full_chroma_input {
|
||||
cfg.profileGUID = nv::NV_ENC_HEVC_PROFILE_FREXT_GUID;
|
||||
cfg.encodeCodecConfig.hevcConfig.set_chromaFormatIDC(3);
|
||||
if c.bit_depth == 10 {
|
||||
cfg.encodeCodecConfig.hevcConfig.set_pixelBitDepthMinus8(2); // Main 4:4:4 10
|
||||
}
|
||||
} else if c.bit_depth == 10 {
|
||||
match c.codec {
|
||||
Codec::H265 => {
|
||||
cfg.profileGUID = nv::NV_ENC_HEVC_PROFILE_MAIN10_GUID;
|
||||
cfg.encodeCodecConfig.hevcConfig.set_pixelBitDepthMinus8(2);
|
||||
}
|
||||
Codec::Av1 => {
|
||||
cfg.encodeCodecConfig.av1Config.set_pixelBitDepthMinus8(2);
|
||||
cfg.encodeCodecConfig
|
||||
.av1Config
|
||||
.set_inputPixelBitDepthMinus8(c.av1_input_depth_minus8);
|
||||
}
|
||||
Codec::H264 => {} // no 10-bit H.264 encode on NVENC — negotiation never asks
|
||||
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
|
||||
}
|
||||
}
|
||||
|
||||
// Colour signaling, written UNCONDITIONALLY: the input is already CSC'd to a specific matrix
|
||||
// (BT.709 limited SDR, or BT.2020 PQ for HDR), so the stream must say so — a decoder whose
|
||||
// "unspecified" default is 601 (Moonlight/third-party/Android-vendor at sub-HD) otherwise
|
||||
// mis-renders. HEVC/H.264 carry it in the VUI; AV1 has no VUI, so the same CICP code points go in
|
||||
// the sequence-header colour config.
|
||||
{
|
||||
let (prim, trc, mat) = if c.hdr {
|
||||
(
|
||||
nv::NV_ENC_VUI_COLOR_PRIMARIES::NV_ENC_VUI_COLOR_PRIMARIES_BT2020,
|
||||
nv::NV_ENC_VUI_TRANSFER_CHARACTERISTIC::NV_ENC_VUI_TRANSFER_CHARACTERISTIC_SMPTE2084,
|
||||
nv::NV_ENC_VUI_MATRIX_COEFFS::NV_ENC_VUI_MATRIX_COEFFS_BT2020_NCL,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
nv::NV_ENC_VUI_COLOR_PRIMARIES::NV_ENC_VUI_COLOR_PRIMARIES_BT709,
|
||||
nv::NV_ENC_VUI_TRANSFER_CHARACTERISTIC::NV_ENC_VUI_TRANSFER_CHARACTERISTIC_BT709,
|
||||
nv::NV_ENC_VUI_MATRIX_COEFFS::NV_ENC_VUI_MATRIX_COEFFS_BT709,
|
||||
)
|
||||
};
|
||||
match c.codec {
|
||||
Codec::H265 => {
|
||||
let vui = &mut cfg.encodeCodecConfig.hevcConfig.hevcVUIParameters;
|
||||
vui.videoSignalTypePresentFlag = 1;
|
||||
vui.videoFullRangeFlag = 0;
|
||||
vui.colourDescriptionPresentFlag = 1;
|
||||
vui.colourPrimaries = prim;
|
||||
vui.transferCharacteristics = trc;
|
||||
vui.colourMatrix = mat;
|
||||
}
|
||||
Codec::H264 => {
|
||||
let vui = &mut cfg.encodeCodecConfig.h264Config.h264VUIParameters;
|
||||
vui.videoSignalTypePresentFlag = 1;
|
||||
vui.videoFullRangeFlag = 0;
|
||||
vui.colourDescriptionPresentFlag = 1;
|
||||
vui.colourPrimaries = prim;
|
||||
vui.transferCharacteristics = trc;
|
||||
vui.colourMatrix = mat;
|
||||
}
|
||||
Codec::Av1 => {
|
||||
let av1 = &mut cfg.encodeCodecConfig.av1Config;
|
||||
av1.colorPrimaries = prim;
|
||||
av1.transferCharacteristics = trc;
|
||||
av1.matrixCoefficients = mat;
|
||||
av1.colorRange = 0; // studio/limited swing
|
||||
}
|
||||
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
|
||||
}
|
||||
}
|
||||
|
||||
// Reference-frame invalidation: a deeper DPB so an invalidated reference can fall back to an
|
||||
// older still-valid frame instead of a full IDR; `numRefL0 = 1` keeps each P-frame
|
||||
// single-reference for low latency. Only when this GPU supports RFI.
|
||||
if c.rfi_supported {
|
||||
let one = nv::NV_ENC_NUM_REF_FRAMES::NV_ENC_NUM_REF_FRAMES_1;
|
||||
match c.codec {
|
||||
Codec::H264 => {
|
||||
cfg.encodeCodecConfig.h264Config.maxNumRefFrames = RFI_DPB;
|
||||
cfg.encodeCodecConfig.h264Config.numRefL0 = one;
|
||||
}
|
||||
Codec::H265 => {
|
||||
cfg.encodeCodecConfig.hevcConfig.maxNumRefFramesInDPB = RFI_DPB;
|
||||
cfg.encodeCodecConfig.hevcConfig.numRefL0 = one;
|
||||
}
|
||||
Codec::Av1 => {
|
||||
cfg.encodeCodecConfig.av1Config.maxNumRefFramesInDPB = RFI_DPB;
|
||||
}
|
||||
Codec::PyroWave => unreachable!("PyroWave never opens the direct-NVENC backend"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
//! Actionable explanations for `NVENCSTATUS` failures — shared by the direct-SDK NVENC backends on
|
||||
//! Windows (`encode/windows/nvenc.rs`) and Linux (`encode/linux/nvenc_cuda.rs`).
|
||||
//!
|
||||
//! Every NVENC entry-point failure used to be annotated `(no NVIDIA GPU?)`, which actively misled
|
||||
//! triage: the direct-NVENC path only loads on a machine that HAS an NVIDIA GPU, and the failure a
|
||||
//! user actually hit — `NV_ENC_ERR_INVALID_VERSION` from a userspace/kernel driver version skew,
|
||||
//! fixed by a reboot — has nothing to do with a missing GPU. This maps each status to what it really
|
||||
//! means and what the operator should do, and folds that cause into the `anyhow::Error` at
|
||||
//! construction, so every downstream `{e:#}` log (the encode-recovery loop, session teardown) says
|
||||
//! the useful thing without extra plumbing.
|
||||
|
||||
use nvidia_video_codec_sdk::sys::nvEncodeAPI as nv;
|
||||
|
||||
/// A one-line, operator-actionable cause for an NVENC status. Does not repeat the raw code —
|
||||
/// callers print that alongside (see [`call_err`]). Public for the few sites that build a
|
||||
/// `String`/`format!` error instead of an `anyhow::Error`.
|
||||
pub(super) fn explain(status: nv::NVENCSTATUS) -> String {
|
||||
match status {
|
||||
// The one this whole module exists for: a version word the driver rejects. Either the
|
||||
// driver is genuinely older than our headers, or (the sneaky case) the userspace
|
||||
// `libnvidia-encode` reports a new-enough version to the pre-flight probe but the running
|
||||
// kernel module is older and rejects the session — the classic "updated the driver, didn't
|
||||
// reboot" skew. Both heal the same way.
|
||||
nv::NVENCSTATUS::NV_ENC_ERR_INVALID_VERSION => format!(
|
||||
"the NVIDIA driver is older than this build's NVENC headers (needs NVENC API {}.{} or \
|
||||
newer), or the userspace and kernel-module driver versions are mismatched — common \
|
||||
right after a driver update without a reboot. Update the NVIDIA driver, or reboot if \
|
||||
you just updated it (a host restart is the usual fix).",
|
||||
nv::NVENCAPI_MAJOR_VERSION,
|
||||
nv::NVENCAPI_MINOR_VERSION,
|
||||
),
|
||||
nv::NVENCSTATUS::NV_ENC_ERR_NO_ENCODE_DEVICE => {
|
||||
"this GPU exposes no usable NVENC engine — it has no hardware video encoder, or NVENC is \
|
||||
disabled on this card"
|
||||
.to_string()
|
||||
}
|
||||
nv::NVENCSTATUS::NV_ENC_ERR_UNSUPPORTED_DEVICE => {
|
||||
"this GPU model is not supported by NVENC".to_string()
|
||||
}
|
||||
nv::NVENCSTATUS::NV_ENC_ERR_INVALID_ENCODERDEVICE
|
||||
| nv::NVENCSTATUS::NV_ENC_ERR_INVALID_DEVICE => {
|
||||
"the device/context handed to NVENC is invalid — a GPU reset or driver reload can cause \
|
||||
this"
|
||||
.to_string()
|
||||
}
|
||||
nv::NVENCSTATUS::NV_ENC_ERR_DEVICE_NOT_EXIST => {
|
||||
"the NVENC device no longer exists — the driver reset, or the GPU fell off the bus"
|
||||
.to_string()
|
||||
}
|
||||
nv::NVENCSTATUS::NV_ENC_ERR_OUT_OF_MEMORY => "the GPU is out of memory".to_string(),
|
||||
nv::NVENCSTATUS::NV_ENC_ERR_INCOMPATIBLE_CLIENT_KEY => {
|
||||
"NVENC rejected the client key — the GeForce concurrent-NVENC-session limit was reached, \
|
||||
or the driver is unlicensed for this many encoders"
|
||||
.to_string()
|
||||
}
|
||||
nv::NVENCSTATUS::NV_ENC_ERR_UNIMPLEMENTED
|
||||
| nv::NVENCSTATUS::NV_ENC_ERR_UNSUPPORTED_PARAM => {
|
||||
"this driver/GPU does not implement the requested NVENC encode mode".to_string()
|
||||
}
|
||||
nv::NVENCSTATUS::NV_ENC_ERR_INVALID_PARAM => {
|
||||
"NVENC rejected a parameter — an encode mode this GPU does not support".to_string()
|
||||
}
|
||||
nv::NVENCSTATUS::NV_ENC_ERR_ENCODER_BUSY => {
|
||||
"the NVENC engine is busy — retry, or reduce the number of concurrent encode sessions"
|
||||
.to_string()
|
||||
}
|
||||
nv::NVENCSTATUS::NV_ENC_ERR_GENERIC => {
|
||||
"the NVIDIA driver returned a generic NVENC failure — check dmesg and the driver install"
|
||||
.to_string()
|
||||
}
|
||||
other => format!("unexpected NVENC status ({other:?})"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build an actionable `anyhow::Error` for a failed NVENC entry-point call. `call` names the API
|
||||
/// (e.g. `"open_encode_session_ex"`); the message carries both the raw status and its real-world
|
||||
/// cause, so triage never again reads a version mismatch as "(no NVIDIA GPU?)".
|
||||
pub(super) fn call_err(call: &str, status: nv::NVENCSTATUS) -> anyhow::Error {
|
||||
anyhow::anyhow!("NVENC {call} failed: {status:?} — {}", explain(status))
|
||||
}
|
||||
@@ -1,336 +0,0 @@
|
||||
//! Software H.264 encoder (openh264) — the GPU-less encode path for the Windows host (and a
|
||||
//! fallback when NVENC is unavailable). Low-latency screen-content config: single-reference,
|
||||
//! no B-frames (Baseline), bitrate rate-control, in-band SPS/PPS each IDR.
|
||||
//! Synchronous: `submit` encodes immediately and stashes the AU for `poll` (no internal queue).
|
||||
//!
|
||||
//! The RGB→YUV conversion is OURS, BT.709 limited range: openh264 writes no colour description
|
||||
//! into the VUI (unspecified), so decoders fall back to their default — BT.709 limited on every
|
||||
//! punktfunk client — and the pixels must match that default. The crate's own `YUVBuffer`
|
||||
//! converter is BT.601 (0.2578/0.5039/0.0977 + 16), which decoded-as-709 is a constant hue
|
||||
//! error; that's why it is NOT used here.
|
||||
// Every `unsafe` block in this file carries a `// SAFETY:` proof; enforce it (unsafe-proof program).
|
||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||
|
||||
use super::{EncodedFrame, Encoder};
|
||||
use crate::capture::{CapturedFrame, FramePayload, PixelFormat};
|
||||
use anyhow::{bail, ensure, Context, Result};
|
||||
use openh264::encoder::{
|
||||
BitRate, Complexity, Encoder as Oh264, EncoderConfig, FrameRate, FrameType, IntraFramePeriod,
|
||||
Profile, RateControlMode, SpsPpsStrategy, UsageType,
|
||||
};
|
||||
use openh264::formats::YUVSlices;
|
||||
use openh264::OpenH264API;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
pub struct OpenH264Encoder {
|
||||
enc: Oh264,
|
||||
width: u32,
|
||||
height: u32,
|
||||
fps: u32,
|
||||
src_format: PixelFormat,
|
||||
/// The converted I420 planes (our BT.709-limited CSC — see the module doc), reused across
|
||||
/// frames: full-res luma + quarter-res Cb/Cr, tightly packed (stride = width, width/2).
|
||||
y_plane: Vec<u8>,
|
||||
u_plane: Vec<u8>,
|
||||
v_plane: Vec<u8>,
|
||||
frame_idx: i64,
|
||||
force_kf: bool,
|
||||
/// One AU per submit (no lookahead), handed back FIFO by `poll`. A queue, not an `Option`:
|
||||
/// the session loop pipelines up to `capturer.pipeline_depth()` submits before polling, and a
|
||||
/// single-slot pending would silently overwrite (lose) the older AUs — including the opening
|
||||
/// IDR — and permanently skew the loop's FIFO pts pairing.
|
||||
pending: VecDeque<EncodedFrame>,
|
||||
}
|
||||
|
||||
// openh264's Encoder holds a raw C handle (not auto-Send); it lives on the single encode thread.
|
||||
// SAFETY: `OpenH264Encoder` wraps `Oh264` (openh264's `Encoder`), which holds a raw C handle to the
|
||||
// openh264 `ISVCEncoder` and is not auto-`Send`; the other fields (the plane `Vec`s, scalars,
|
||||
// `Option<EncodedFrame>`) are plain owned data. The session creates the encoder, calls
|
||||
// `submit`/`poll`/`flush`, and drops it all on one dedicated encode thread, never sharing it by
|
||||
// reference across threads, so the C handle is only ever touched from a single thread. Moving the
|
||||
// whole value to that thread is therefore sound — there is no concurrent access to the handle.
|
||||
unsafe impl Send for OpenH264Encoder {}
|
||||
|
||||
impl OpenH264Encoder {
|
||||
pub fn open(
|
||||
format: PixelFormat,
|
||||
width: u32,
|
||||
height: u32,
|
||||
fps: u32,
|
||||
bitrate_bps: u64,
|
||||
) -> Result<Self> {
|
||||
// validate_dimensions() ran in open_video: even, non-zero, <= 4096.
|
||||
let bps: u32 = bitrate_bps.try_into().unwrap_or(u32::MAX);
|
||||
let cfg = EncoderConfig::new()
|
||||
.usage_type(UsageType::ScreenContentRealTime)
|
||||
.max_frame_rate(FrameRate::from_hz(fps.max(1) as f32))
|
||||
.rate_control_mode(RateControlMode::Bitrate)
|
||||
.bitrate(BitRate::from_bps(bps))
|
||||
.skip_frames(false)
|
||||
.intra_frame_period(IntraFramePeriod::from_num_frames(intra_period_frames(fps)))
|
||||
.sps_pps_strategy(SpsPpsStrategy::ConstantId) // SPS/PPS in-band on every IDR
|
||||
.num_threads(num_threads())
|
||||
.scene_change_detect(false) // no surprise IDRs (bitrate spikes / freeze)
|
||||
.adaptive_quantization(true)
|
||||
.complexity(Complexity::Low) // latency over BD-rate
|
||||
.profile(Profile::Baseline); // no B-frames; the VUI carries no colour description
|
||||
let api = OpenH264API::from_source(); // statically-bundled build (default `source` feature)
|
||||
let enc = Oh264::with_api_config(api, cfg).context("openh264 Encoder::with_api_config")?;
|
||||
let (w, h) = (width as usize, height as usize);
|
||||
tracing::info!(
|
||||
"openh264 software encoder: {width}x{height}@{fps} {} Mbps (Baseline, screen-content)",
|
||||
bps / 1_000_000
|
||||
);
|
||||
Ok(Self {
|
||||
enc,
|
||||
width,
|
||||
height,
|
||||
fps,
|
||||
src_format: format,
|
||||
y_plane: vec![0; w * h],
|
||||
u_plane: vec![0; (w / 2) * (h / 2)],
|
||||
v_plane: vec![0; (w / 2) * (h / 2)],
|
||||
frame_idx: 0,
|
||||
force_kf: false,
|
||||
pending: VecDeque::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert one packed full-range RGB frame into the I420 planes, BT.709 limited range.
|
||||
/// `bpp` is the source pixel stride; `ri`/`gi`/`bi` the channel byte offsets within a pixel.
|
||||
/// Luma per pixel; Cb/Cr from the 2×2 block's averaged RGB (the same box filter the crate's
|
||||
/// converter used, so only the matrix changed).
|
||||
fn convert_bt709(&mut self, src: &[u8], bpp: usize, ri: usize, gi: usize, bi: usize) {
|
||||
let w = self.width as usize;
|
||||
let h = self.height as usize;
|
||||
let cw = w / 2;
|
||||
for by in 0..h / 2 {
|
||||
for bx in 0..cw {
|
||||
let mut sum = (0f32, 0f32, 0f32);
|
||||
for (dy, dx) in [(0, 0), (0, 1), (1, 0), (1, 1)] {
|
||||
let (px, py) = (bx * 2 + dx, by * 2 + dy);
|
||||
let s = &src[(py * w + px) * bpp..];
|
||||
let (r, g, b) = (f32::from(s[ri]), f32::from(s[gi]), f32::from(s[bi]));
|
||||
self.y_plane[py * w + px] = luma709(r, g, b);
|
||||
sum = (sum.0 + r, sum.1 + g, sum.2 + b);
|
||||
}
|
||||
let (cb, cr) = chroma709(sum.0 / 4.0, sum.1 / 4.0, sum.2 / 4.0);
|
||||
self.u_plane[by * cw + bx] = cb;
|
||||
self.v_plane[by * cw + bx] = cr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// BT.709 luma coefficients (Kg = 1 − Kr − Kb).
|
||||
const KR: f32 = 0.2126;
|
||||
const KB: f32 = 0.0722;
|
||||
const KG: f32 = 1.0 - KR - KB;
|
||||
|
||||
/// One full-range RGB pixel (0..=255 channels) → the BT.709 limited-range 8-bit luma code
|
||||
/// (16..=235). Kept in lockstep with the client-side inverse (`pf-client-core::video::csc_rows`).
|
||||
fn luma709(r: f32, g: f32, b: f32) -> u8 {
|
||||
let y = KR * r + KG * g + KB * b; // full-scale luma, 0..=255
|
||||
(16.0 + y * (219.0 / 255.0) + 0.5) as u8 // `as` saturates — no manual clamp needed
|
||||
}
|
||||
|
||||
/// (Averaged) full-range RGB → the BT.709 limited-range Cb/Cr codes (16..=240, neutral 128).
|
||||
fn chroma709(r: f32, g: f32, b: f32) -> (u8, u8) {
|
||||
let y = KR * r + KG * g + KB * b;
|
||||
let cb = 128.0 + (b - y) * (224.0 / 255.0) / (2.0 * (1.0 - KB));
|
||||
let cr = 128.0 + (r - y) * (224.0 / 255.0) / (2.0 * (1.0 - KR));
|
||||
((cb + 0.5) as u8, (cr + 0.5) as u8)
|
||||
}
|
||||
|
||||
impl Encoder for OpenH264Encoder {
|
||||
fn submit(&mut self, captured: &CapturedFrame) -> Result<()> {
|
||||
ensure!(
|
||||
captured.width == self.width && captured.height == self.height,
|
||||
"captured {}x{} != encoder {}x{}",
|
||||
captured.width,
|
||||
captured.height,
|
||||
self.width,
|
||||
self.height
|
||||
);
|
||||
ensure!(
|
||||
captured.format == self.src_format,
|
||||
"captured format {:?} != encoder source {:?}",
|
||||
captured.format,
|
||||
self.src_format
|
||||
);
|
||||
// Refutable once the capture backend adds `FramePayload::D3d11`; today `Cpu` is the only
|
||||
// non-Linux variant, so the pattern is (temporarily) irrefutable.
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
let FramePayload::Cpu(bytes) = &captured.payload
|
||||
else {
|
||||
bail!("openh264 backend requires a CPU frame payload");
|
||||
};
|
||||
let w = self.width as usize;
|
||||
let h = self.height as usize;
|
||||
ensure!(
|
||||
bytes.len() >= w * h * self.src_format.bytes_per_pixel(),
|
||||
"captured buffer {} bytes too small for {w}x{h} {:?}",
|
||||
bytes.len(),
|
||||
self.src_format
|
||||
);
|
||||
|
||||
// Source pixel stride + R/G/B byte offsets within a pixel — one converter for every
|
||||
// packed-RGB layout the capturers emit (no BGRA normalization pass needed).
|
||||
let (bpp, ri, gi, bi) = match self.src_format {
|
||||
PixelFormat::Rgb => (3, 0, 1, 2),
|
||||
PixelFormat::Bgr => (3, 2, 1, 0),
|
||||
PixelFormat::Rgba | PixelFormat::Rgbx => (4, 0, 1, 2),
|
||||
PixelFormat::Bgra | PixelFormat::Bgrx => (4, 2, 1, 0),
|
||||
// 10-bit HDR comes only from the GPU NVENC path; the software 8-bit H.264 encoder
|
||||
// can't represent it (and never receives it — the capturer pairs Rgb10a2 with NVENC).
|
||||
PixelFormat::Rgb10a2 => {
|
||||
anyhow::bail!("software H.264 encoder cannot encode 10-bit HDR (Rgb10a2)")
|
||||
}
|
||||
// NV12/P010 are GPU-resident video-processor outputs for the NVENC path; the software
|
||||
// encoder never receives them (it only gets CPU RGB frames).
|
||||
PixelFormat::Nv12 | PixelFormat::P010 | PixelFormat::Yuv444 => {
|
||||
anyhow::bail!(
|
||||
"software encoder cannot encode YUV GPU frames (NV12/P010/YUV444 → NVENC only)"
|
||||
)
|
||||
}
|
||||
};
|
||||
self.convert_bt709(bytes, bpp, ri, gi, bi);
|
||||
|
||||
if self.force_kf {
|
||||
self.enc.force_intra_frame();
|
||||
self.force_kf = false;
|
||||
}
|
||||
let slices = YUVSlices::new(
|
||||
(&self.y_plane, &self.u_plane, &self.v_plane),
|
||||
(w, h),
|
||||
(w, w / 2, w / 2),
|
||||
);
|
||||
let bs = self.enc.encode(&slices).context("openh264 encode")?;
|
||||
let mut data = Vec::new();
|
||||
bs.write_vec(&mut data); // AnnexB start codes; SPS/PPS prepended on IDR
|
||||
if !data.is_empty() {
|
||||
let keyframe = matches!(bs.frame_type(), FrameType::IDR | FrameType::I);
|
||||
let pts_ns = self.frame_idx as u64 * 1_000_000_000 / self.fps.max(1) as u64;
|
||||
self.pending.push_back(EncodedFrame {
|
||||
data,
|
||||
pts_ns,
|
||||
keyframe,
|
||||
recovery_anchor: false,
|
||||
chunk_aligned: false,
|
||||
});
|
||||
}
|
||||
self.frame_idx += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn request_keyframe(&mut self) {
|
||||
self.force_kf = true;
|
||||
}
|
||||
|
||||
fn poll(&mut self) -> Result<Option<EncodedFrame>> {
|
||||
Ok(self.pending.pop_front())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<()> {
|
||||
Ok(()) // synchronous: nothing buffered
|
||||
}
|
||||
}
|
||||
|
||||
/// Approximate infinite-GOP: insert IDRs rarely (recovery is via `request_keyframe`/RFI). Env
|
||||
/// `PUNKTFUNK_OH264_GOP` overrides (0 = encoder-auto).
|
||||
fn intra_period_frames(fps: u32) -> u32 {
|
||||
if let Ok(v) = std::env::var("PUNKTFUNK_OH264_GOP") {
|
||||
if let Ok(n) = v.trim().parse::<u32>() {
|
||||
return n;
|
||||
}
|
||||
}
|
||||
fps.max(1).saturating_mul(600) // ~10 min between automatic IDRs
|
||||
}
|
||||
|
||||
/// Encode threads. Env `PUNKTFUNK_OH264_THREADS` overrides; default 2 (latency over throughput).
|
||||
fn num_threads() -> u16 {
|
||||
std::env::var("PUNKTFUNK_OH264_THREADS")
|
||||
.ok()
|
||||
.and_then(|v| v.trim().parse::<u16>().ok())
|
||||
.unwrap_or(2)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::capture::{CapturedFrame, FramePayload, PixelFormat};
|
||||
|
||||
/// The BT.709 limited-range anchor points: reference white → (235,128,128), black →
|
||||
/// (16,128,128), pure red's Cr must hit the positive extreme 240 (it does exactly:
|
||||
/// 255(1−Kr)·(224/255)/(2(1−Kr)) = 112). ±1 code for float rounding.
|
||||
#[test]
|
||||
fn bt709_conversion_anchor_points() {
|
||||
assert_eq!(luma709(255.0, 255.0, 255.0), 235);
|
||||
assert_eq!(luma709(0.0, 0.0, 0.0), 16);
|
||||
assert_eq!(chroma709(255.0, 255.0, 255.0), (128, 128));
|
||||
assert_eq!(chroma709(0.0, 0.0, 0.0), (128, 128));
|
||||
let (cb, cr) = chroma709(255.0, 0.0, 0.0);
|
||||
assert_eq!(cr, 240, "pure red must reach the Cr extreme");
|
||||
assert!((101..=103).contains(&cb), "red Cb ~102, got {cb}");
|
||||
let (cb, _) = chroma709(0.0, 0.0, 255.0);
|
||||
assert_eq!(cb, 240, "pure blue must reach the Cb extreme");
|
||||
}
|
||||
|
||||
/// The 601-vs-709 luma split on pure green (Kg 0.587 vs 0.7152) — guards against anyone
|
||||
/// "simplifying" the coefficients back to the crate's BT.601 converter (the hue-shift bug
|
||||
/// this module's own conversion exists to prevent).
|
||||
#[test]
|
||||
fn bt709_is_not_bt601() {
|
||||
// BT.601 green luma: 16 + 219·0.587 = 144.5; BT.709: 16 + 219·0.7152 = 172.6.
|
||||
let y = luma709(0.0, 255.0, 0.0);
|
||||
assert!((172..=174).contains(&y), "709 green luma ~173, got {y}");
|
||||
}
|
||||
|
||||
/// A flat gray frame converts to neutral chroma and mid luma across every plane byte
|
||||
/// (exercises the block loop + plane sizing, not just the per-pixel math).
|
||||
#[test]
|
||||
fn converts_flat_gray_to_neutral_planes() {
|
||||
let (w, h) = (16u32, 8u32);
|
||||
let mut enc =
|
||||
OpenH264Encoder::open(PixelFormat::Bgrx, w, h, 60, 1_000_000).expect("open openh264");
|
||||
let bytes = vec![0x80u8; (w * h * 4) as usize];
|
||||
enc.convert_bt709(&bytes, 4, 2, 1, 0);
|
||||
// 16 + 128·(219/255) = 125.9 → 126.
|
||||
assert!(
|
||||
enc.y_plane.iter().all(|&y| y == 126),
|
||||
"{:?}",
|
||||
&enc.y_plane[..4]
|
||||
);
|
||||
assert!(enc.u_plane.iter().all(|&u| u == 128));
|
||||
assert!(enc.v_plane.iter().all(|&v| v == 128));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encodes_synthetic_frame_to_annexb_idr() {
|
||||
let (w, h, fps) = (1280u32, 720u32, 60u32);
|
||||
let mut enc =
|
||||
OpenH264Encoder::open(PixelFormat::Bgrx, w, h, fps, 8_000_000).expect("open openh264");
|
||||
// A flat gray BGRx frame.
|
||||
let frame = CapturedFrame {
|
||||
width: w,
|
||||
height: h,
|
||||
pts_ns: 0,
|
||||
format: PixelFormat::Bgrx,
|
||||
payload: FramePayload::Cpu(vec![0x80u8; (w * h * 4) as usize]),
|
||||
cursor: None,
|
||||
};
|
||||
enc.submit(&frame).expect("submit");
|
||||
let au = enc.poll().expect("poll").expect("an AU");
|
||||
assert!(au.keyframe, "first frame must be an IDR");
|
||||
// AnnexB start code + an SPS NAL (type 7) somewhere in the first frame.
|
||||
assert!(
|
||||
au.data.starts_with(&[0, 0, 0, 1]) || au.data.starts_with(&[0, 0, 1]),
|
||||
"expected AnnexB start code"
|
||||
);
|
||||
let has_sps = au
|
||||
.data
|
||||
.windows(5)
|
||||
.any(|w| w[0] == 0 && w[1] == 0 && w[2] == 0 && w[3] == 1 && (w[4] & 0x1f) == 7);
|
||||
assert!(has_sps, "IDR must carry an SPS NAL (type 7)");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,13 @@ mod ddc;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "linux/drm_sync.rs"]
|
||||
mod drm_sync;
|
||||
mod encode;
|
||||
// The video encode backends live in the `pf-encode` leaf crate (plan §W6); this shim keeps every
|
||||
// existing `crate::encode::*` path valid (the host is the sole consumer, via the negotiator + the
|
||||
// GameStream/native/mgmt planes). Feature flags (nvenc/amf-qsv/vulkan-encode/pyrowave) forward to
|
||||
// pf-encode from this crate's `[features]`.
|
||||
mod encode {
|
||||
pub(crate) use pf_encode::*;
|
||||
}
|
||||
mod events;
|
||||
mod gamestream;
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -67,9 +73,6 @@ mod spike;
|
||||
mod stats_recorder;
|
||||
mod stream_marker;
|
||||
mod vdisplay;
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "windows/win_adapter.rs"]
|
||||
mod win_adapter;
|
||||
// The Windows display-topology cluster (CCD/GDI mode-set, PnP monitor devnodes, the display-change
|
||||
// watch) lives in the `pf-win-display` leaf crate (plan §W6); import the modules at the crate root
|
||||
// so every existing `crate::{win_display,monitor_devnode,display_events}::*` path stays valid.
|
||||
|
||||
@@ -1445,12 +1445,12 @@ pub(crate) fn slot_id_for(client_fp: Option<[u8; 32]>, mode: (u32, u32)) -> u32
|
||||
/// The render-GPU pin (backend-neutral): IDD-push — the sole Windows capture path — runs NVENC on the
|
||||
/// render adapter, so it must always be pinned to the selected encoder GPU (a hybrid box would
|
||||
/// otherwise render on the wrong one). The selection itself (web-console preference >
|
||||
/// `PUNKTFUNK_RENDER_ADAPTER` > max VRAM) lives in [`crate::win_adapter::resolve_render_adapter_luid`].
|
||||
/// `PUNKTFUNK_RENDER_ADAPTER` > max VRAM) lives in [`pf_gpu::resolve_render_adapter_luid`].
|
||||
/// (This was gated on the removed `PUNKTFUNK_IDD_PUSH` knob — a dispatch disagreement, since capture
|
||||
/// stopped consulting it when DDA/WGC were removed.)
|
||||
fn resolve_render_pin() -> Option<LUID> {
|
||||
tracing::info!("IDD push: pinning the render GPU (SET_RENDER_ADAPTER)");
|
||||
crate::win_adapter::resolve_render_adapter_luid()
|
||||
pf_gpu::resolve_render_adapter_luid()
|
||||
}
|
||||
|
||||
/// A reused monitor keeps the render GPU the driver was pinned to at its ADD — the pin is never
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
//! Backend-neutral DXGI adapter selection.
|
||||
//!
|
||||
//! The discrete render-GPU LUID picker used to live in the SudoVDA backend (`vdisplay::sudovda`) — a
|
||||
//! historical accident, since it is display-utility, not SudoVDA-specific. It lives here so the capturers
|
||||
//! (IDD-push) and the pf-vdisplay backend depend on it as a *peer* instead of reaching into the SudoVDA
|
||||
//! module — breaking that circular reach-in, which let the SudoVDA backend be dropped without losing this
|
||||
//! helper (audit §9 / Goal 2 — done). This is the plan's `windows/adapter.rs`.
|
||||
//!
|
||||
//! The selection logic itself now lives in [`crate::gpu`] (shared with the mgmt API's GPU
|
||||
//! endpoints): **operator preference (web console) > `PUNKTFUNK_RENDER_ADAPTER` substring > max
|
||||
//! `DedicatedVideoMemory`**, WARP/Basic-Render and indirect-display ghost twins always excluded.
|
||||
//! This wrapper is the LUID-shaped view of it, plus the per-decision logging (call sites are
|
||||
//! per-session, never per-frame).
|
||||
|
||||
use windows::Win32::Foundation::LUID;
|
||||
|
||||
/// Pick the render GPU LUID the pipeline is created on: the IDD-push capturer's shared-texture
|
||||
/// ring, the IddCx SET_RENDER_ADAPTER pin, and (via the captured frame's device) NVENC/AMF/QSV all
|
||||
/// follow this one decision — see [`pf_gpu::selected_gpu`] for the precedence. A configured
|
||||
/// preference that doesn't match a present GPU falls back to auto selection (with a warning)
|
||||
/// rather than returning `None`, so a stale preference never stops the host from streaming.
|
||||
pub(crate) fn resolve_render_adapter_luid() -> Option<LUID> {
|
||||
match pf_gpu::selected_gpu() {
|
||||
Some(sel) => {
|
||||
tracing::info!(
|
||||
adapter = sel.info.name,
|
||||
vram_mb = sel.info.vram_bytes / (1024 * 1024),
|
||||
source = sel.source.tag(),
|
||||
"render adapter selected"
|
||||
);
|
||||
if sel.source == pf_gpu::PickSource::PreferenceMissing {
|
||||
tracing::warn!(
|
||||
"the preferred GPU is not present — auto-selected the adapter above \
|
||||
(fix or clear the preference in the web console)"
|
||||
);
|
||||
}
|
||||
Some(sel.info.luid())
|
||||
}
|
||||
None => {
|
||||
tracing::warn!("no suitable render adapter found for SET_RENDER_ADAPTER");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user