Repository navigation
v0.0.2
Other
- dlopen_succeeds + va_get_display_drm_returns_non_null skip on no-libva CI
- apply rustfmt layout + fix always-zero op + identical-if-block lints
- skip frameworks_load + vtable_resolves on hosts without the driver
- honour CodecParameters::device_index — pick matching DRI render node
- cache SPS/PPS across packets — works under bench's slice-only feed
- query vaGetConfigAttributes per device for max width/height
- implement engine_info() — enumerate DRI render nodes + per-codec caps
Added — Round 7 (CodecParameters::device_index plumbing)
H264VaCodecDecoder::make (the registered factory) now honours
[oxideav_core::CodecParameters::device_index]. Indexing matches
[engine::engine_info]'s walk order so info h264 device-block
indices and --device selectors line up — passing
with_device_index(1) opens the second device that
engine_info() reports, not just "the second renderD* node on
disk" (which may not be the same thing on hosts where libva
refuses to bind to one of the nodes).
- New
pub fn engine::device_path_for_index(index: u32) -> Result<PathBuf, VaError>
(also re-exported from the crate root). Walks
/dev/dri/renderD128..renderD191, attempts
[Display::open_drm] on each existing path, counts only those
that initialise cleanly, and returns the path at position
indexin that filtered list. Errors with a descriptive
[VaError::Init] whenindexis out of range. - New
H264VaCodecDecoder::new_with_device_index(codec_id, index)
constructor; the existing
[H264VaCodecDecoder::new(codec_id)] now delegates to it with
index = 0for source-compatibility. - Decoder factory
decoder::h264_decoder_factoryreads
params.device_index.unwrap_or(0)and threads the index
through. Out-of-range values return
[oxideav_core::Error::Unsupported] rather than silently
falling back to device 0 — silent fallback would mask
configuration mistakes. - New
tests/round7_device_index.rs(3 tests):device_index_none_opens_first_working_device— default
CodecParametershasdevice_index = None; the factory
accepts it anddevice_path_for_index(0)matches the
dri_nodeextra entry onengine_info()[0].device_index_one_opens_second_device— skips when
engine_info()reports fewer than 2 working devices; on
multi-device hosts (the dev box now: NVIDIA renderD128 +
Intel iHD renderD129) confirms the second-position
device_index resolves to the second device's path and the
factory builds successfully.device_index_out_of_range_errors— runs everywhere;
device_index = 99errors at both the helper and the
factory layer. Always-on so CI hosts without a working
VA-API stack still cover the negative path.
Added — Round 6 (engine probe — DRI render-node enumeration + per-codec caps)
Wires up Phase 1's [oxideav_core::engine::EngineProbeFn] contract so
oxideav list and any other consumer can ask the VA-API bridge "which
engines do you see, and what can each do?" without spinning up a
decoder.
- New
enginemodule gated behind the default-onregistryfeature
(the [HwDeviceInfo] / [HwCodecCaps] return types live in
oxideav-core):pub fn engine_info() -> Vec<HwDeviceInfo>walks the standard
DRM render-node range/dev/dri/renderD128..renderD191,
skips nodes that don't exist, opens each existing one with
[Display::open_drm], and silently skips nodes whose libva
driver refuses to load (the standard "node exists but the GPU
driver doesn't bind" case — on this dev box renderD129 is the
second NVIDIA GPU's render node where libva falls through to
iHD/i965 and bails).- Per-device entry:
name: vendor string + render-node basename, so multi-GPU
hosts producing the same vendor string still get unique
device names (e.g."VA-API NVDEC driver [direct backend] (renderD128)").api_version:"VA-API <major>.<minor>"from
[Display::api_version].extra: includes("dri_node", "/dev/dri/renderD128").codecs: one [HwCodecCaps] per codec family (h264 / hevc /
av1 / vp8 / vp9 / mpeg2 / vc1) with at least one advertised
profile. Decode/encode flags are any-of-family; max
width/height come fromvaGetConfigAttributeson the highest
advertised profile (fallbackNonewhen the driver returns
VA_ATTRIB_NOT_SUPPORTEDor a sentinel0);profiles
lists the [VaProfile::name] string for every advertised
family profile.
register()now chains.with_engine_id("vaapi")and
.with_engine_probe(engine_info)onto the H.264 [CodecInfo] so
the CLI can group all VA-API codec entries by engine and call the
probe at most once per pass.- New
tests/round6_engine_info.rs:engine_info_finds_render_node_or_skips— dumps the first
device, asserts non-empty vendor + advertised api_version + an
h264 entry withdecode = true. Skip-friendly when no render
node is present.engine_info_does_not_panic_when_called_twice— confirms the
probe is callable repeatedly (consumers may dedupe by engine id
and call once per pass, but the contract is "idempotent + safe
to call multiple times").
Findings — Round 6
On the dev box (RTX 5080 + nvidia-vaapi-driver 0.0.16 + libva 1.22):
- 2 render nodes exist (
/dev/dri/renderD128,renderD129);
engine_info()reports 1 working device because the nvidia
driver shim binds to renderD128 only — opening renderD129
through libva falls through to the iHD/i965 fallbacks and
returnsVaError::Init. - The single device exposes decode-only caps (
encode = false)
for every codec family, matching the Round 4 finding that
nvidia-vaapi-driver is NVDEC-only. MaxPictureWidth/MaxPictureHeightcome back as0from this
driver — the engine module treats0asNone(the alternative
Some(0)would be a misleading "max dimension is zero" report
to consumers like the CLI).
Added — Round 5 (H.264 decode wall RESOLVED + first registered factory)
The Round 3 H.264 decode silent-fail wall is gone. Retrying the
same end-to-end IDR submission flow with the shared, unit-tested
[oxideav-bitstream] parser populating the parameter buffers gave a
pixel-perfect match against ffmpeg's reference render
(mean abs diff = 0/255 on the bundled 320×240 fixture, all three
planes). The Round 3 wall was a parser bug, not a driver issue — the
NVDEC backend was always willing to decode; the silent-fail signature
came from at least one mis-packed bitfield in the in-tree parser's
parameter-buffer construction.
- New
oxideav-bitstream = "0.0"dependency (matching the workspace
shape used byoxideav-vdpau). - New
decodermodule:H264VaDecoder<'d>— single-IDR helper that owns aConfig+
Context(bound by lifetime to aDisplay) and drives the
submission flowvaCreateBuffer × 4 → vaBeginPicture → vaRenderPicture → vaEndPicture → vaSyncSurface → vaCreateImage(NV12) → vaGetImage. Returns the decoded frame in I420 layout.compute_slice_data_bit_offset— full IDR-I-slice header
bit-counter that extends theoxideav-bitstream's minimal slice
header parse with ref_pic_list_modification, dec_ref_pic_marking
(IDR-flavoured: no_output_of_prior_pics + long_term_reference),
slice_qp_delta and the deblocking-filter triplet — far enough to
locate the start ofslice_data()accurately in the bitstream.
Required because libva'sslice_data_bit_offsetis the post-EBSP
bit count from the NAL header byte through the end of
slice_header() and theoxideav-bitstreamminimal parser stops at
pic_order_cnt_lsb.H264VaCodecDecoder—oxideav_core::Decoderimplementation,
Sendviaunsafe impl(libva's "not thread-safe" rule is about
concurrent access; serialized owner moves are sound). Opens its
own DRM render node, lazily constructs the innerH264VaDecoder
on the first packet, decodes the IDR access unit and emits an
oxideav_core::VideoFramecropped to display dimensions.
- New
VAIQMatrixBufferH264struct insys.rsplus aflat()
constructor that returns the all-16 default scaling lists. Verified
240 bytes via a one-shotsizeof()C check against
/usr/include/va/va.h. register()now wires the H.264 decode factory into the codec
registry at priority 10 withhardware_accelerated = true. On hosts
where libva loads but no driver.sois available for the GPU, the
pre-flighthost_supports_h264_decode()check returns false and
registration is skipped — the SW codec stays the only candidate.- New
tests/round5_decode.rs(3 tests):h264_high_decoder_constructs—H264VaDecoder::newsucceeds
and reports the SPS-derived 320×240 dimensions.h264_high_idr_decode_succeeds_or_documents_silent_fail—
cross-validates against an ffmpeg reference rendered via
ffmpeg -f h264 -i pipe:0 -f rawvideo -pix_fmt yuv420p pipe:1.
On the dev box this lands themad_total = 0.000success
branch; the test code retains the silent-fail-asserting branch
(constant 0x80 luma + 0x80/0x80 chroma) so the same source
expresses both possible truths and passes against either.registry_decoder_roundtrips_idr_packet— feeds the fixture as a
singleoxideav_core::Packetto the registry-shaped
H264VaCodecDecoder, pulls aFrame::Videoback, asserts the
three-plane shape and per-plane sizes (320×240 Y, 160×120 U/V).
- New
tests/fixtures/h264_high_320x240_1frame.h264— same
ffmpeg-generated single-IDR Annex-B as theoxideav-vdpauRound 3
fixture (~6.6 KB).
Findings — Round 5
The Round 3 wall held only against the parser used during that
round. Three concrete shape differences between the in-tree parser
and oxideav-bitstream's parser most likely caused the silent-fail —
without keeping the in-tree parser around for diff we can only point
at categories — but the seq_fields / pic_fields packing is a
known minefield, the slice_data_bit_offset is sensitive to whether
emulation-prevention bytes are stripped before counting bits, and the
MinLumaBiPredSize8x8 : 1 slot in seq_fields is silently load-
bearing on NVDEC even for I-slices. The new code documents the bit
positions explicitly against va.h.
The lesson for the wider OxideAV project is the architectural one:
bitstream parsing belongs in oxideav-bitstream (a tested, shared
crate) and not in each hardware-bridge crate. The Round 3 attempt
predated that crate, and the in-tree parser was never unit-tested
against a reference. With the same parser now driving both oxideav- vdpau and oxideav-vaapi end-to-end, both backends now decode the
same fixture bit-exactly.
Added — Round 4 (capability probing API + driver-reality findings)
- New
Displaycapability methods built onvaQueryConfigEntrypoints:Display::entrypoints(VaProfile) -> Result<Vec<i32>, VaError>—
list every entrypoint the driver advertises for a given profile.
MapsVA_STATUS_ERROR_UNSUPPORTED_PROFILEtoOk(Vec::new())so
capability audits don't have to special-caseErr.Display::is_supported(VaProfile, entrypoint) -> bool—
convenience yes/no check; swallows errors asfalseso callers
don't drown inResult<bool>for a question that's structurally
boolean.Display::profiles_with_entrypoint(entrypoint) -> Result<Vec<VaProfile>, VaError>
— filter the full profile list to those that advertise a given
entrypoint. Useful for "which codecs can I decode?" /
"which codecs can I encode?" capability dumps.
- Extended
VaProfile::name()to identify the wider set of profiles
the dev box surfaces (MPEG-2 Simple/Main, VC1 Simple/Main/Advanced,
H.264 ConstrainedBaseline, JPEG Baseline, VP8, VP9 Profile 0/2,
HEVC Main/Main10/Main12/Main444/Main444_10/Main444_12, AV1
Profile 0/1). - Added the matching
VAProfile*constants insys::profile. - New integration tests in
tests/round4_capabilities.rs(5 passing,
1#[ignore]'d for NVDEC-specific driver-truth):entrypoints_for_h264_high_includes_vldis_supported_recognises_h264_decodeis_supported_for_unsupported_entrypoint_is_falsedecode_profile_set_is_non_emptyentrypoints_for_unsupported_profile_handled_gracefullyencode_unavailable_on_nvdec_backend(#[ignore]'d)
- New
tests/capability_dump.rs—--ignoreddiagnostic test that
prints the full(profile, entrypoint, RTFormat)matrix and a
decode/encode summary. Canonical command-line probe for hosts.
Findings — Round 4 (Path A: VA-API encode is structurally impossible
on nvidia-vaapi-driver)
The nvidia-vaapi-driver 0.0.16 shim wraps NVDEC, NVIDIA's hardware
decode engine. vaQueryConfigEntrypoints for every advertised
profile (H.264 Main/High/ConstrainedBaseline, HEVC Main/Main10/Main12/
Main444 family, AV1 Profile 0, VP8, VP9 Profile 0/2, VC1 Simple/Main/
Advanced, MPEG-2 Simple/Main) returns the single entrypoint
VAEntrypointVLD = 1. No EncSlice, no EncSliceLP, no
EncPicture. vaCreateConfig for any (profile, EncSlice) pair
fails with VA_STATUS_ERROR_UNSUPPORTED_ENTRYPOINT (13): "the
requested VAEntryPoint is not supported".
The Round 4 strategy of "if decode silently fails, try encode"
therefore has no path on this hardware via VA-API. NVENC (NVIDIA's
hardware encode engine) is exposed through NVENC-direct (used by the
oxideav-nvidia sibling crate) and through CUDA/Vulkan Video; the
VA-API frontend does not bridge to NVENC at all.
The capability_dump test confirms this on demand. The
encode_unavailable_on_nvdec_backend test asserts it programmatically
(skipped on non-NVDEC hosts).
Findings — Round 4 (Path B: decode wall remains; not landed this round)
The Round 3 H.264 decode silent-fail (parameter buffers accepted,
surface returns constant 0x80) is unresolved. With encode ruled out
structurally and per the workspace clean-room policy (no consulting
nvidia-vaapi-driver source / ffmpeg vaapi.c / gstreamer-vaapi /
third-party Rust VA-API bindings), debugging the silent-fail requires
either:
- A second VA-API driver on the box (Intel iGPU, AMD GPU, or
mesa-va-gallium) to cross-validate parameter-buffer setups
against, or - Tens of thousands of lines of bitstream parsing per codec for
HEVC / AV1 / VP9 / VP8, each of which is independently at risk of
the same silent-fail wall (different NVDEC code paths but the same
driver dispatch surface).
Neither is achievable in one round without bloating the crate beyond
its bridge mandate. Proper bitstream parsing belongs in
oxideav-h264 / oxideav-hevc / oxideav-av1 / oxideav-vp9 /
oxideav-vp8, where it's reusable across all hardware backends
(VA-API, NVENC, VDPAU, Vulkan Video, VideoToolbox). When those crates
land their parsers, this crate provides the pipeline glue
(Config / Context / planned Surface / Buffer / Picture
helpers) — and the capability-probing API added in this round is what
they will use to skip-detect what the driver can actually accelerate.
Status — Round 4 deliverable
This round ships the capability-probing API, the codified driver-truth
findings, and the diagnostic capability_dump infrastructure. No
codec factories register: there is nothing on this hardware that
the bridge can usefully wire up via VA-API today, and the framework
already falls back to the pure-Rust path for every codec id. The
crate's value in Round 4 is that capability audits (oxideav list,
future codec crates' priority resolution) can now ask "does this
host's VA-API stack accelerate codec X for operation Y?" and get a
correct answer in O(1) calls.
Added — Round 3 (Tier A: decode pipeline scaffolding)
- New
configmodule — safe wrapper aroundVAConfigID.Config::new(&Display, profile, entrypoint, attribs)calls
vaCreateConfig. Emptyattribsslice maps to(NULL, 0)so
the driver picks defaults (e.g.VA_RT_FORMAT_YUV420on H.264
VLD).Config::supported_attributes()walks a portable shortlist
(RTFormat,DecSliceMode,MaxPictureWidth,
MaxPictureHeight,RateControl) and returns each value the
driver advertises (filtered againstVA_ATTRIB_NOT_SUPPORTED).Config::get_attribute(type)reads a single attribute.- Standalone
config::supported_attributes/
config::get_attributemirror the methods so capability
probing doesn't require constructing aConfigfirst. DropcallsvaDestroyConfig.
- New
contextmodule — safe wrapper aroundVAContextIDplus
the render-target surfaces it owns.Context::new(&Display, &Config, w, h, num_surfaces)allocates
VA_RT_FORMAT_YUV420surfaces viavaCreateSurfacesand binds
them to a context viavaCreateContext. Failures during
vaCreateContextclean up the just-allocated surfaces.Droptears down in reverse order (vaDestroyContext→
vaDestroySurfaces).
sys.rsextensions:- Resolved into the vtable:
vaQueryConfigEntrypoints,
vaGetConfigAttributes,vaDestroyConfig,vaDestroyContext,
vaSyncSurface,vaDestroyBuffer,vaMapBuffer,
vaUnmapBuffer,vaDeriveImage,vaDestroyImage,vaGetImage,
vaPutImage,vaCreateImage,vaQueryImageFormats,
vaMaxNumImageFormats. - New constants:
VAConfigAttribTypevalues (RTFormat,
DecSliceMode,MaxPictureWidth,MaxPictureHeight,
RateControl, etc.),VA_ATTRIB_NOT_SUPPORTED,
VA_RT_FORMAT_YUV420/422/444/420_10,VA_FOURCC_NV12 / I420 / YV12 / IYUV,VABufferTypedecode entries
(VAPictureParameterBufferType,VASliceParameterBufferType,
VASliceDataBufferType,VAImageBufferType),
VA_SLICE_DATA_FLAG_*,VA_PICTURE_H264_*,
VA_PADDING_LOW/MEDIUM/HIGH,VA_INVALID_ID,
VA_INVALID_SURFACE. - New struct types:
VAConfigAttrib,VAImageFormat,VAImage,
VAPictureH264(with::invalid()helper), and the
decoder-target buffersVAPictureParameterBufferH264and
VASliceParameterBufferH264. All#[repr(C)]; sizes verified
by hand against a Csizeof()driver to match
/usr/include/va/va.hexactly (36 / 672 / 3128 / 120 / 48 / 8
bytes respectively on x86_64).
- Resolved into the vtable:
- New integration tests in
tests/round3_pipeline.rs(skip-friendly
on no-driver hosts):h264_high_decode_config_creates—Config::new(&dpy, VAProfileH264High, VAEntrypointVLD, &[])returns Ok with a
non-zero config id, profile, and entrypoint round-tripping
through accessors.h264_high_decode_context_creates— config + a 1920×1088
single-surface context create succeeds end-to-end.h264_high_supports_yuv420_render_target— query
VAConfigAttribRTFormatfor(H264High, VLD)and assert the
returned bitmask includesVA_RT_FORMAT_YUV420 = 0x01.
Status — Round 3 (Tier B: actual decode)
- A first attempt at end-to-end H.264 IDR decode (parse SPS/PPS,
buildVAPictureParameterBufferH264+VASliceParameterBufferH264,
submitvaCreateBuffer × 3→vaBeginPicture→vaRenderPicture
→vaEndPicture→vaSyncSurface→vaGetImage) was implemented
locally and the submission half works: every libva entry point
returnsVA_STATUS_SUCCESSand the surface read-back hits the
vaCreateImage+vaGetImagefallback (because
nvidia-vaapi-driverdoesn't implementvaDeriveImage). - However on the dev box (RTX 5080 + nvidia-vaapi-driver 0.0.16 +
libva 1.22) the surface comes back as constant 0x80 luma /
0x80/0x80 chroma — the driver accepts the parameter buffers but
the GPU never writes to the surface. The submission flow is
correct against the libva spec; the issue is most likely a field
in the H.264-specific parameter buffers that
nvidia-vaapi-driver's shim handles differently from the way
va.hdocuments (or a missing field-pic / DPB bookkeeping detail
for the IDR-only case). - Per workspace clean-room policy we cannot consult
nvidia-vaapi-driver, ffmpeg'svaapi.c, gstreamer-vaapi, or
third-party Rust bindings to diff our parameter setup against a
known-good submission. Without a second VA-API driver on the box
for cross-checking (Intel/AMD), debugging this becomes an
open-ended bisection exercise across ~60 fields and the
driver-specific quirks behind the closed-source NVDEC firmware. - The Tier B scaffolding (NAL splitter, RBSP de-emulation,
Exp-GolombBitReader, SPS/PPS/slice-header parsers,
C-bitfield-faithfulseq_fields/pic_fieldspacking — all
unit-tested) was removed from the crate before commit so the
shipped surface only contains code that actually does what its
docs claim. It will land as part of the H.264 decoder crate
(oxideav-h264) where the parsers belong — at which point this
crate just needs the pipeline glue to call them. - Tier C (encode entrypoint) was therefore deliberately not started.
Added — Round 2
- New
displaymodule — safe wrapper around the DRM render-node
libva backend.Display::open_drm(path)opens/dev/dri/renderD*via
libc::open, callsvaGetDisplayDRM, thenvaInitialize.
Returns a fully populatedDisplayon success or a precise
error variant on each step's failure.Display::api_version(),Display::vendor_string(),
Display::profiles(),Display::raw(),Display::fd().DropcallsvaTerminate(only if init succeeded) then
closes the fd. Init-failure path leaks nothing.
- New
VaErrorenum:OpenDrm(io::Error),GetDisplayNull,
Sys(String),Init { status, message },Va { status, message }.
TheInitvariant carries the driver-supplied string from
vaErrorStr, so the no-driver-installed case bubbles up a useful
reason instead of an opaque code. - New
VaProfile(i32)newtype +VaProfile::name()for the
H.264 / HEVC / AV1 profiles oxideav cares about. sys.rsextensions:vaQueryVendorString,vaMaxNumProfilesresolved into the
vtable.VAProfile*andVAEntrypoint*constants in
sys::profile::*andsys::entrypoint::*.- Additional
VA_STATUS_ERROR_*named constants matching the
cases we route to specific error variants. sys::error_str(vt, status)helper that callsvaErrorStrand
returns an ownedString.
libc = "0.2"added undertarget.'cfg(target_os = "linux")'
foropen/closeof the render node.- Integration test
tests/round2_init.rs:dlopen_succeeds,drm_fd_opens,
va_get_display_drm_returns_non_null,
va_initialize_propagates_error_when_no_driver_installed.- The headline test accepts both the success path (real driver
present, assertsapi_version > (0,0), non-empty vendor,
non-empty profile list) AND the graceful-failure path
(Err(VaError::Init { status, message })with non-zero status
and non-empty driver message). Same source on both kinds of
box.
Added — Round 1 (previously released as 0.0.1)
- Initial scaffolding:
#![cfg(target_os = "linux")]crate that
dlopenslibva.so.2+libva-drm.so.2vialibloadingon first
use. sys.rsexposes opaque type aliases (VADisplay,VAConfigID,
VAContextID,VASurfaceID,VABufferID,VAStatus) and a
resolvedVtablecovering the bootstrap symbol set:
vaInitialize,vaTerminate,vaErrorStr,vaQueryConfigProfiles,
vaCreateConfig,vaCreateContext,vaCreateSurfaces2,
vaBeginPicture,vaRenderPicture,vaEndPicture,
vaCreateBuffer, plusvaGetDisplayDRMfrom the DRM backend.- Process-wide
OnceLock<Result<Vtable, String>>cache so the
dlopen + dlsym round-trip happens at most once per process. - Unified
register(&mut RuntimeContext)entry point. Round 1: the
function confirms the libraries load and returns; no codec
factories are wired up yet. If load fails (libva not installed,
sandbox without/dev/dri, etc.) the function logs and returns —
the pure-Rust codec path remains the only resolution candidate. - Standalone-friendly
registryfeature (default-on) gates the
oxideav-core+linkmedeps. - README coverage roadmap and priority explanation.
- Smoke tests:
frameworks_loadandvtable_resolvesconfirm
symbol resolution on Linux machines that have the VA-API stack
installed.