Part 4: Rusty VAInfo
6 minutes read •
In part 3, we finished wrapping a minimal subset of functions required to get a VADisplay up and running, however we won’t be able to verify our work without implementing some core functionality. It would also be nice if we had some sort of simple executable that we could use for testing the functions we have implemented so far, and in the suite of VAAPI tools, the simplest one I can think of is vainfo.
)
)
It’s a basic utility for listing what version of VAAPI is installed on your system, and what profiles (codecs) and entrypoints (operations) your combination of hardware and driver supports. Such feature enumeration is going to be vital for all applications, so it would be a good first target to implement.
How to VAInfo?
If we look back to the example we’ve read through in part 1, we can see that there are query functions for listing entrypoints for a profile. And if we skim the function list in the API documentation, we can see a similar one for querying supported profiles too. We’ll also need to implement vaInitialize and vaTerminate, as well as a few ancillary queries to show library version and driver information. So let’s get started!
VADisplay and a VAAPI instance
To do anything with VAAPI, we’re going to need to initialise it on the VADisplay returned by the platform-specific module. It would be nice if the library’s end users didn’t have to worry about this detail, so the Display type Vaudeville will expose should take care of the whole display handle -> initialised VAAPI instance flow.
This display handle will also be required all over the user’s application, so I’ll create a separate DisplayOwner as well as the user-facing Display that will contain a reference counted handle to the DisplayOwner.
Something like the following:
Also note that I’ve opted to store a handle to the loaded dynamic library functions inside the DisplayOwner. This is hopefully going to make fetching the handles a little more efficient.
For getting a suitable display handle, let’s utilise the raw_window_handle interoperability crate. Then we can have a platform-agnostic Display::new() that calls out to the correct backend.
platforms::display_from_raw_handle() matches on the RawDisplayHandle type:
pub
And the platform-specific modules do the actual work:
pub
It would be nice to enable users of the library to insert an uninitialised VADisplay they acquired somewhere else into Vaudeville. To allow this without much duplication, the initialisation is put into the separate function Display::new_from_va_display(), which then calls the DisplayOwner to initialise and take ownership of the provided handle.
After initialisation, it’ll also hook the VAAPI logging callbacks into the corresponding Rust logging functions.
And to complete the Display’s lifecycle, let’s not forget about cleaning up after ourselves. To do this, let’s implement a custom destructor for DisplayOwner. To do this in Rust, we just need to implement Drop on the type.
Starting vainfo
If we start a new binary crate within the workspace called vaudeville-vainfo, we can start writing some actual end user code.
First off, we want to use Winit to get a raw_window_handle::DisplayHandle we can feed into Display::new:
use HasDisplayHandle;
Of course the real vainfo automatically tries multiple display options in order until it finds one it can work with, but let’s keep things simple at first.
With a display handle acquired, it’s time to query the data we need to log:
- VAAPI version and library implementation
- Driver name and version
- Supported profiles and entrypoints
VAAPI version is easy, we already receive it from the vaInitialize invocation. Let’s store that with the DisplayOwner so it can be requested by the user.
pub
This lets us print the VA-API version, however the API doesn’t seem to expose the library version. I wonder where vainfo gets it from…
)
The line within vainfo’s source code that prints this string can be found here. And if we run a search for what defines LIBVA_VERSION_S we find that it’s injected into the source code directly at build time. I guess we could put the Vaudeville version there instead.
To get the version of a crate to print in Rust, we can make use of the environment variables Cargo sets before calling rustc, CARGO_PKG_VERSION. Might as well expose it at the library level.
And now we can print the first line of our vainfo output:
println!;
The next line is going to be much simpler, it just gets the vendor string using vaQueryVendorString and prints the result. Just have to implement querying the vendor string for Display:
…then use that implementation to print:
println!;
Wait, we can use default?
Apparently we can!If we run the following line of code in a debugger, it will correctly point to a null terminated empty string:
let value: &CStr = Defaultdefault;
This behaviour is thanks to an impl in the Rust standard library:
Querying profiles and entrypoints
The final step of the basic vainfo output is the list of profiles and entrypoints.
For this we just need to call vaQueryConfigProfiles to get the supported profiles. Then once we have the profiles, we can call vaQueryConfigEntrypoints with each profile.
As a safety mechanism, I’d like to wrap the profiles and entrypoints into structs so we can use the Rust type system to enforce that we only operate using profile and entrypoint combinations that are valid.
So let’s create a Profile struct that stores the Display and the associated VAProfile:
Then we can add a method to Display that enumerates the supported profiles and wraps them:
Since we’re once again receiving enum values from C, we run into the same potential for UB that was mentioned in the previous part. And just like in the previous part, we are going to ignore it for the moment.
self.max_num_profiles() just calls vaMaxNumProfiles to get us the size of array to allocate for the profile list, then we just take the actual length of the array returned by the vaQueryConfigProfiles invocation and iterate over the response, validating each value as we encounter them and wrapping them into a Profile.
Getting entrypoints for a profile is practically the same code, just defined as a member function within Profile instead of Display, and also wrapping the associated VAProfile within the returned Entrypoint values, since not all profiles implement the same set of entrypoints.
Then we can just iterate over the options within our vainfo implementation to list everything:
println!;
for profile in display.profiles.unwrap
And finally we can cargo run our very own vainfo!
)
)
Woohoo! We finally have something to show for all our effort.
In the next part we will finally tackle some of the enum UB.