832 lines
27 KiB
C++
832 lines
27 KiB
C++
/**
|
|
* @file src/platform/linux/vaapi.cpp
|
|
* @brief Definitions for VA-API hardware accelerated capture.
|
|
*/
|
|
// standard includes
|
|
#include <fcntl.h>
|
|
#include <format>
|
|
#include <sstream>
|
|
#include <string>
|
|
|
|
extern "C" {
|
|
#include <libavcodec/avcodec.h>
|
|
#include <libavutil/pixdesc.h>
|
|
#include <va/va.h>
|
|
#include <va/va_drm.h>
|
|
#if !VA_CHECK_VERSION(1, 9, 0)
|
|
/**
|
|
* @brief Stub vaSyncBuffer when building against libva before 2.9.0.
|
|
*
|
|
* @param dpy VA display.
|
|
* @param buf_id VA buffer ID.
|
|
* @param timeout_ns Sync timeout in nanoseconds.
|
|
* @return VA status code.
|
|
*/
|
|
VAStatus
|
|
vaSyncBuffer(
|
|
VADisplay dpy,
|
|
VABufferID buf_id,
|
|
uint64_t timeout_ns
|
|
) {
|
|
return VA_STATUS_ERROR_UNIMPLEMENTED;
|
|
}
|
|
#endif
|
|
#if !VA_CHECK_VERSION(1, 21, 0)
|
|
/**
|
|
* @brief Stub vaMapBuffer2 when building against libva before 2.21.0.
|
|
*
|
|
* @param dpy VA display.
|
|
* @param buf_id VA buffer ID.
|
|
* @param pbuf Output mapped buffer pointer.
|
|
* @param flags Mapping flags.
|
|
* @return VA status code.
|
|
*/
|
|
VAStatus
|
|
vaMapBuffer2(
|
|
VADisplay dpy,
|
|
VABufferID buf_id,
|
|
void **pbuf,
|
|
uint32_t flags
|
|
) {
|
|
return vaMapBuffer(dpy, buf_id, pbuf);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// local includes
|
|
#include "graphics.h"
|
|
#include "misc.h"
|
|
#include "src/config.h"
|
|
#include "src/logging.h"
|
|
#include "src/platform/common.h"
|
|
#include "src/utility.h"
|
|
#include "src/video.h"
|
|
|
|
using namespace std::literals;
|
|
|
|
extern "C" struct AVBufferRef;
|
|
|
|
namespace va {
|
|
constexpr auto SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2 = 0x40000000; ///< Protocol or platform constant for surface attrib mem type drm prime 2.
|
|
constexpr auto EXPORT_SURFACE_WRITE_ONLY = 0x0002; ///< GameStream port offset for export surface write only.
|
|
constexpr auto EXPORT_SURFACE_SEPARATE_LAYERS = 0x0004; ///< GameStream port offset for export surface separate layers.
|
|
|
|
/**
|
|
* @brief Native VA display handle.
|
|
*/
|
|
using VADisplay = void *;
|
|
/**
|
|
* @brief Status code returned by VAAPI functions.
|
|
*/
|
|
using VAStatus = int;
|
|
/**
|
|
* @brief Generic numeric VAAPI object identifier.
|
|
*/
|
|
using VAGenericID = unsigned int;
|
|
/**
|
|
* @brief VAAPI surface identifier.
|
|
*/
|
|
using VASurfaceID = VAGenericID;
|
|
|
|
/**
|
|
* @brief DRM PRIME descriptor imported from a VAAPI surface.
|
|
*/
|
|
struct DRMPRIMESurfaceDescriptor {
|
|
// VA Pixel format fourcc of the whole surface (VA_FOURCC_*).
|
|
uint32_t fourcc; ///< VA fourcc pixel format for the imported surface.
|
|
|
|
uint32_t width; ///< Surface width in pixels.
|
|
uint32_t height; ///< Surface height in pixels.
|
|
|
|
// Number of distinct DRM objects making up the surface.
|
|
uint32_t num_objects; ///< Num objects.
|
|
|
|
struct {
|
|
// DRM PRIME file descriptor for this object.
|
|
// Needs to be closed manually
|
|
int fd;
|
|
|
|
// Total size of this object (may include regions which are not part of the surface)
|
|
uint32_t size;
|
|
// Format modifier applied to this object, not sure what that means
|
|
uint64_t drm_format_modifier;
|
|
} objects[4]; ///< DRM PRIME backing objects referenced by the descriptor..
|
|
|
|
// Number of layers making up the surface.
|
|
uint32_t num_layers; ///< Num layers.
|
|
|
|
struct {
|
|
// DRM format fourcc of this layer (DRM_FOURCC_*).
|
|
uint32_t drm_format;
|
|
|
|
// Number of planes in this layer.
|
|
uint32_t num_planes;
|
|
|
|
// references objects --> DRMPRIMESurfaceDescriptor.objects[object_index[0]]
|
|
uint32_t object_index[4];
|
|
|
|
// Offset within the object of each plane.
|
|
uint32_t offset[4];
|
|
|
|
// Pitch of each plane.
|
|
uint32_t pitch[4];
|
|
} layers[4]; ///< DRM PRIME layer descriptions for the frame..
|
|
};
|
|
|
|
/**
|
|
* @brief VA display handle released with `vaTerminate`.
|
|
*/
|
|
using display_t = util::safe_ptr_v2<void, VAStatus, vaTerminate>;
|
|
|
|
/**
|
|
* @brief Create an FFmpeg VA-API hardware device context from a Sunshine encode device.
|
|
*
|
|
* @param encode_device Encode device.
|
|
* @param hw_device_buf Output FFmpeg hardware device buffer.
|
|
* @return 0 when the buffer is initialized; negative FFmpeg error code on failure.
|
|
*/
|
|
int vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device, AVBufferRef **hw_device_buf);
|
|
|
|
/**
|
|
* @brief VAAPI encode device that imports captured frames into VA surfaces.
|
|
*/
|
|
class va_t: public platf::avcodec_encode_device_t {
|
|
public:
|
|
/**
|
|
* @brief Initialize VAAPI display, EGL, and conversion resources.
|
|
*
|
|
* @param in_width In width.
|
|
* @param in_height In height.
|
|
* @param render_device Render device.
|
|
* @return 0 on success; nonzero or negative platform status on failure.
|
|
*/
|
|
int init(int in_width, int in_height, file_t &&render_device) {
|
|
file = std::move(render_device);
|
|
|
|
if (!gbm::create_device) {
|
|
BOOST_LOG(warning) << "libgbm not initialized"sv;
|
|
return -1;
|
|
}
|
|
|
|
this->data = (void *) vaapi_init_avcodec_hardware_input_buffer;
|
|
|
|
gbm.reset(gbm::create_device(file.el));
|
|
if (!gbm) {
|
|
char string[1024];
|
|
BOOST_LOG(error) << "Couldn't create GBM device: ["sv << strerror_r(errno, string, sizeof(string)) << ']';
|
|
return -1;
|
|
}
|
|
|
|
display = egl::make_display(gbm.get());
|
|
if (!display) {
|
|
return -1;
|
|
}
|
|
|
|
auto ctx_opt = egl::make_ctx(display.get());
|
|
if (!ctx_opt) {
|
|
return -1;
|
|
}
|
|
|
|
ctx = std::move(*ctx_opt);
|
|
|
|
width = in_width;
|
|
height = in_height;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @brief Finds a supported VA entrypoint for the given VA profile.
|
|
* @param profile The profile to match.
|
|
* @return A valid encoding entrypoint or 0 on failure.
|
|
*/
|
|
VAEntrypoint select_va_entrypoint(VAProfile profile) {
|
|
std::vector<VAEntrypoint> entrypoints(vaMaxNumEntrypoints(va_display));
|
|
int num_eps;
|
|
auto status = vaQueryConfigEntrypoints(va_display, profile, entrypoints.data(), &num_eps);
|
|
if (status != VA_STATUS_SUCCESS) {
|
|
BOOST_LOG(error) << "Failed to query VA entrypoints: "sv << vaErrorStr(status);
|
|
return (VAEntrypoint) 0;
|
|
}
|
|
entrypoints.resize(num_eps);
|
|
|
|
// Sorted in order of descending preference
|
|
VAEntrypoint ep_preferences[] = {
|
|
VAEntrypointEncSliceLP,
|
|
VAEntrypointEncSlice,
|
|
VAEntrypointEncPicture
|
|
};
|
|
for (auto ep_pref : ep_preferences) {
|
|
if (std::find(entrypoints.begin(), entrypoints.end(), ep_pref) != entrypoints.end()) {
|
|
return ep_pref;
|
|
}
|
|
}
|
|
|
|
return (VAEntrypoint) 0;
|
|
}
|
|
|
|
/**
|
|
* @brief Determines if a given VA profile is supported.
|
|
* @param profile The profile to match.
|
|
* @return Boolean value indicating if the profile is supported.
|
|
*/
|
|
bool is_va_profile_supported(VAProfile profile) {
|
|
std::vector<VAProfile> profiles(vaMaxNumProfiles(va_display));
|
|
int num_profs;
|
|
auto status = vaQueryConfigProfiles(va_display, profiles.data(), &num_profs);
|
|
if (status != VA_STATUS_SUCCESS) {
|
|
BOOST_LOG(error) << "Failed to query VA profiles: "sv << vaErrorStr(status);
|
|
return false;
|
|
}
|
|
profiles.resize(num_profs);
|
|
|
|
return std::find(profiles.begin(), profiles.end(), profile) != profiles.end();
|
|
}
|
|
|
|
/**
|
|
* @brief Determines the matching VA profile for the codec configuration.
|
|
* @param ctx The FFmpeg codec context.
|
|
* @return The matching VA profile or `VAProfileNone` on failure.
|
|
*/
|
|
VAProfile get_va_profile(AVCodecContext *ctx) {
|
|
if (ctx->codec_id == AV_CODEC_ID_H264) {
|
|
// There's no VAAPI profile for H.264 4:4:4
|
|
return VAProfileH264High;
|
|
} else if (ctx->codec_id == AV_CODEC_ID_HEVC) {
|
|
switch (ctx->profile) {
|
|
case AV_PROFILE_HEVC_REXT:
|
|
switch (av_pix_fmt_desc_get(ctx->sw_pix_fmt)->comp[0].depth) {
|
|
case 10:
|
|
return VAProfileHEVCMain444_10;
|
|
case 8:
|
|
return VAProfileHEVCMain444;
|
|
}
|
|
break;
|
|
case AV_PROFILE_HEVC_MAIN_10:
|
|
return VAProfileHEVCMain10;
|
|
case AV_PROFILE_HEVC_MAIN:
|
|
return VAProfileHEVCMain;
|
|
}
|
|
} else if (ctx->codec_id == AV_CODEC_ID_AV1) {
|
|
switch (ctx->profile) {
|
|
case AV_PROFILE_AV1_HIGH:
|
|
return VAProfileAV1Profile1;
|
|
case AV_PROFILE_AV1_MAIN:
|
|
return VAProfileAV1Profile0;
|
|
}
|
|
}
|
|
|
|
BOOST_LOG(error) << "Unknown encoder profile: "sv << ctx->profile;
|
|
return VAProfileNone;
|
|
}
|
|
|
|
/**
|
|
* @brief Initialize codec options.
|
|
*
|
|
* @param ctx Native context object used by the operation or callback.
|
|
* @param options Request options or socket options to apply.
|
|
*/
|
|
void init_codec_options(AVCodecContext *ctx, AVDictionary **options) override {
|
|
auto va_profile = get_va_profile(ctx);
|
|
if (va_profile == VAProfileNone || !is_va_profile_supported(va_profile)) {
|
|
// Don't bother doing anything if the profile isn't supported
|
|
return;
|
|
}
|
|
|
|
auto va_entrypoint = select_va_entrypoint(va_profile);
|
|
if (va_entrypoint == 0) {
|
|
// It's possible that only decoding is supported for this profile
|
|
return;
|
|
}
|
|
|
|
auto vendor = vaQueryVendorString(va_display);
|
|
|
|
if (va_entrypoint == VAEntrypointEncSliceLP) {
|
|
BOOST_LOG(info) << "Using LP encoding mode"sv;
|
|
av_dict_set_int(options, "low_power", 1, 0);
|
|
} else {
|
|
BOOST_LOG(info) << "Using normal encoding mode"sv;
|
|
}
|
|
|
|
// When the compression_level AVOption is set, vaapi_encode.c assigns the value to VAEncMiscParameterBufferQualityLevel
|
|
VAConfigAttrib quality_attr = {VAConfigAttribEncQualityRange};
|
|
auto status = vaGetConfigAttributes(va_display, va_profile, va_entrypoint, &quality_attr, 1);
|
|
if (status != VA_STATUS_SUCCESS || quality_attr.value == VA_ATTRIB_NOT_SUPPORTED) {
|
|
quality_attr.value = 0;
|
|
}
|
|
auto vaapi_quality = config::video.vaapi.vaapi_quality.value_or(0);
|
|
auto target_quality = 0;
|
|
switch (vaapi_quality) {
|
|
default:
|
|
case 0: // auto or unset
|
|
break;
|
|
case 1: // low quality (highest value in range)
|
|
case 2: // med quality (middle value in range)
|
|
target_quality = quality_attr.value / vaapi_quality;
|
|
break;
|
|
case 3: // high quality (1)
|
|
target_quality = 1;
|
|
break;
|
|
}
|
|
if (quality_attr.value > 0) {
|
|
ctx->compression_level = target_quality;
|
|
BOOST_LOG(info) << "[VAAPI] Quality level set to "sv << ctx->compression_level << " (fastest level: "sv << quality_attr.value << ")"sv;
|
|
}
|
|
|
|
VAConfigAttrib rc_attr = {VAConfigAttribRateControl};
|
|
status = vaGetConfigAttributes(va_display, va_profile, va_entrypoint, &rc_attr, 1);
|
|
if (status != VA_STATUS_SUCCESS) {
|
|
// Stick to the default rate control (CQP)
|
|
rc_attr.value = 0;
|
|
}
|
|
|
|
VAConfigAttrib slice_attr = {VAConfigAttribEncMaxSlices};
|
|
status = vaGetConfigAttributes(va_display, va_profile, va_entrypoint, &slice_attr, 1);
|
|
if (status != VA_STATUS_SUCCESS) {
|
|
// Assume only a single slice is supported
|
|
slice_attr.value = 1;
|
|
}
|
|
if (ctx->slices > slice_attr.value) {
|
|
BOOST_LOG(info) << "Limiting slice count to encoder maximum: "sv << slice_attr.value;
|
|
ctx->slices = slice_attr.value;
|
|
}
|
|
|
|
// Use VBR with a single frame VBV when the user forces it and for known good cases:
|
|
// - Intel GPUs
|
|
// - AV1
|
|
//
|
|
// VBR ensures the bitstream isn't full of filler data for bitrate undershoots and
|
|
// single frame VBV ensures that we don't have large bitrate overshoots (at least
|
|
// as much as they can be avoided without pre-analysis).
|
|
//
|
|
// When we have to resort to the default 1 second VBV for encoding quality reasons,
|
|
// we stick to CBR in order to avoid encoding huge frames after bitrate undershoots
|
|
// leave headroom available in the RC window.
|
|
//
|
|
// If a user-supplied rate control is detected, override the whitelist logic to allow
|
|
// full user control of both the rate control and strict VBV settings.
|
|
auto auto_whitelist = false;
|
|
auto rc_mode = config::video.vaapi.vaapi_rc_str;
|
|
auto rc_vbv = "with standard VBV size";
|
|
auto rc_whitelist = "";
|
|
auto rc_val = config::video.vaapi.vaapi_rc.value_or(0);
|
|
|
|
// Detect whitelisted configurations
|
|
if ((vendor && std::string_view(vendor).contains("Intel") == true) || ctx->codec_id == AV_CODEC_ID_AV1) {
|
|
auto_whitelist = true;
|
|
rc_whitelist = " (whitelist override)";
|
|
}
|
|
|
|
// First try user config, else fall back to auto-detection that respects whitelist
|
|
if (rc_val > 0 && rc_attr.value & rc_val) {
|
|
// override whitelist if the user-specified RC is supported
|
|
auto_whitelist = false;
|
|
rc_whitelist = "";
|
|
} else if (rc_attr.value & VA_RC_VBR && auto_whitelist) {
|
|
rc_mode = "vbr";
|
|
rc_val = VA_RC_VBR;
|
|
} else if (rc_attr.value & VA_RC_CBR) {
|
|
rc_mode = "cbr";
|
|
rc_val = VA_RC_CBR;
|
|
} else {
|
|
rc_mode = "cqp";
|
|
rc_val = VA_RC_CQP;
|
|
}
|
|
|
|
if (config::video.vaapi.strict_rc_buffer || auto_whitelist) {
|
|
ctx->rc_buffer_size = ctx->bit_rate * ctx->framerate.den / ctx->framerate.num;
|
|
rc_vbv = "with single frame VBV size";
|
|
}
|
|
|
|
// ffmpeg's rc_mode values don't align with VAAPI's rc_val values, so transform string to uppercase
|
|
std::transform(rc_mode.begin(), rc_mode.end(), rc_mode.begin(), [](unsigned char c) {
|
|
return std::toupper(c);
|
|
});
|
|
av_dict_set(options, "rc_mode", rc_mode.c_str(), 0);
|
|
if (rc_val == VA_RC_CQP || rc_val == VA_RC_ICQ || rc_val == VA_RC_QVBR) {
|
|
BOOST_LOG(warning) << "[VAAPI] Applying QP for compatible rate control method (QP value: "sv << config::video.qp << ")"sv;
|
|
av_dict_set_int(options, "qp", config::video.qp, 0);
|
|
}
|
|
BOOST_LOG(info) << "[VAAPI] Using "sv << rc_mode << " rate control "sv << rc_vbv << rc_whitelist;
|
|
}
|
|
|
|
/**
|
|
* @brief Attach frame resources used by the next conversion or encode operation.
|
|
*
|
|
* @param frame Video or graphics frame being processed.
|
|
* @param hw_frames_ctx_buf Hardware frames context buffer.
|
|
* @return Status from updating frame.
|
|
*/
|
|
int set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx_buf) override {
|
|
this->hwframe.reset(frame);
|
|
this->frame = frame;
|
|
|
|
if (!frame->buf[0]) {
|
|
if (av_hwframe_get_buffer(hw_frames_ctx_buf, frame, 0)) {
|
|
BOOST_LOG(error) << "Couldn't get hwframe for VAAPI"sv;
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
va::DRMPRIMESurfaceDescriptor prime;
|
|
va::VASurfaceID surface = (std::uintptr_t) frame->data[3];
|
|
auto hw_frames_ctx = (AVHWFramesContext *) hw_frames_ctx_buf->data;
|
|
|
|
auto status = vaExportSurfaceHandle(
|
|
this->va_display,
|
|
surface,
|
|
va::SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
|
|
va::EXPORT_SURFACE_WRITE_ONLY | va::EXPORT_SURFACE_SEPARATE_LAYERS,
|
|
&prime
|
|
);
|
|
if (status) {
|
|
BOOST_LOG(error) << "Couldn't export va surface handle: ["sv << (int) surface << "]: "sv << vaErrorStr(status);
|
|
|
|
return -1;
|
|
}
|
|
|
|
// Keep track of file descriptors
|
|
std::array<file_t, egl::nv12_img_t::num_fds> fds;
|
|
for (int x = 0; x < prime.num_objects; ++x) {
|
|
fds[x] = prime.objects[x].fd;
|
|
}
|
|
|
|
if (prime.num_layers != 2) {
|
|
BOOST_LOG(error) << "Invalid layer count for VA surface: expected 2, got "sv << prime.num_layers;
|
|
return -1;
|
|
}
|
|
|
|
egl::surface_descriptor_t sds[2] = {};
|
|
for (int plane = 0; plane < 2; ++plane) {
|
|
auto &sd = sds[plane];
|
|
auto &layer = prime.layers[plane];
|
|
|
|
sd.fourcc = layer.drm_format;
|
|
|
|
// UV plane is subsampled
|
|
sd.width = prime.width / (plane == 0 ? 1 : 2);
|
|
sd.height = prime.height / (plane == 0 ? 1 : 2);
|
|
|
|
// The modifier must be the same for all planes
|
|
sd.modifier = prime.objects[layer.object_index[0]].drm_format_modifier;
|
|
|
|
std::fill_n(sd.fds, 4, -1);
|
|
for (int x = 0; x < layer.num_planes; ++x) {
|
|
sd.fds[x] = prime.objects[layer.object_index[x]].fd;
|
|
sd.pitches[x] = layer.pitch[x];
|
|
sd.offsets[x] = layer.offset[x];
|
|
}
|
|
}
|
|
|
|
auto nv12_opt = egl::import_target(display.get(), std::move(fds), sds[0], sds[1]);
|
|
if (!nv12_opt) {
|
|
return -1;
|
|
}
|
|
|
|
auto sws_opt = egl::sws_t::make(width, height, frame->width, frame->height, hw_frames_ctx->sw_format, false);
|
|
if (!sws_opt) {
|
|
return -1;
|
|
}
|
|
|
|
this->sws = std::move(*sws_opt);
|
|
this->nv12 = std::move(*nv12_opt);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @brief Apply the configured colorspace metadata to the active frame.
|
|
*/
|
|
void apply_colorspace() override {
|
|
sws.apply_colorspace(colorspace, false);
|
|
}
|
|
|
|
va::display_t::pointer va_display; ///< VA display used to allocate and destroy surfaces.
|
|
file_t file; ///< DRM render-node file descriptor used by VAAPI and EGL.
|
|
|
|
gbm::gbm_t gbm; ///< GBM device used for buffer allocation.
|
|
egl::display_t display; ///< EGL display created from the DRM render node.
|
|
egl::ctx_t ctx; ///< EGL context used for VA-API frame conversion.
|
|
|
|
// This must be destroyed before display_t to ensure the GPU
|
|
// driver is still loaded when vaDestroySurfaces() is called.
|
|
frame_t hwframe; ///< FFmpeg hardware frame backed by a VAAPI surface.
|
|
|
|
egl::sws_t sws; ///< EGL/OpenGL conversion pipeline for VA-API frames.
|
|
egl::nv12_t nv12; ///< EGL/OpenGL resources used for NV12 output frames.
|
|
|
|
int width; ///< Frame or display width in pixels.
|
|
int height; ///< Frame or display height in pixels.
|
|
};
|
|
|
|
/**
|
|
* @brief VAAPI encode path that copies converted frames through system memory.
|
|
*/
|
|
class va_ram_t: public va_t {
|
|
public:
|
|
/**
|
|
* @brief Convert a captured VAAPI frame into system-memory encoder input.
|
|
*
|
|
* @param img Image or frame object to read from or populate.
|
|
* @return Conversion status.
|
|
*/
|
|
int convert(platf::img_t &img) override {
|
|
sws.load_ram(img);
|
|
|
|
sws.convert_nv12(nv12->buf);
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @brief VAAPI encode path that keeps converted frames in GPU memory.
|
|
*/
|
|
class va_vram_t: public va_t {
|
|
public:
|
|
/**
|
|
* @brief Convert a captured VAAPI frame into GPU encoder input.
|
|
*
|
|
* @param img Image or frame object to read from or populate.
|
|
* @return Conversion status.
|
|
*/
|
|
int convert(platf::img_t &img) override {
|
|
auto &descriptor = (egl::img_descriptor_t &) img;
|
|
|
|
if (descriptor.sequence == 0) {
|
|
// For dummy images, use a blank RGB texture instead of importing a DMA-BUF
|
|
rgb = egl::create_blank(img);
|
|
} else if (descriptor.sequence > sequence) {
|
|
sequence = descriptor.sequence;
|
|
|
|
auto rgb_opt = egl::import_source(display.get(), descriptor.sd);
|
|
|
|
if (!rgb_opt) {
|
|
// The plane's current format/modifier can't be imported (e.g. a game switched to a
|
|
// 10bpc swapchain in exclusive fullscreen that this driver won't bind to a GL texture).
|
|
// Encode a blank frame instead of dropping the client, and only log once per failure
|
|
// streak to avoid flooding the log every frame.
|
|
if (!import_failed_last_frame) {
|
|
BOOST_LOG(warning) << "Skipping frame(s): plane format (fourcc: "sv << util::hex(descriptor.sd.fourcc).to_string_view()
|
|
<< ") failed to import; will resume automatically if the format changes back"sv;
|
|
import_failed_last_frame = true;
|
|
}
|
|
|
|
rgb = egl::create_blank(img);
|
|
} else {
|
|
if (import_failed_last_frame) {
|
|
BOOST_LOG(info) << "Resumed capture after plane format import failure"sv;
|
|
import_failed_last_frame = false;
|
|
}
|
|
|
|
rgb = std::move(*rgb_opt);
|
|
}
|
|
}
|
|
|
|
sws.load_vram(descriptor, offset_x, offset_y, rgb->tex[0], false);
|
|
|
|
sws.convert_nv12(nv12->buf);
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @brief Initialize VAAPI GPU-frame conversion for the selected display.
|
|
*
|
|
* @param in_width In width.
|
|
* @param in_height In height.
|
|
* @param render_device Render device.
|
|
* @param offset_x Offset x.
|
|
* @param offset_y Offset y.
|
|
* @return 0 on success; nonzero or negative platform status on failure.
|
|
*/
|
|
int init(int in_width, int in_height, file_t &&render_device, int offset_x, int offset_y) {
|
|
if (va_t::init(in_width, in_height, std::move(render_device))) {
|
|
return -1;
|
|
}
|
|
|
|
sequence = 0;
|
|
|
|
this->offset_x = offset_x;
|
|
this->offset_y = offset_y;
|
|
|
|
return 0;
|
|
}
|
|
|
|
std::uint64_t sequence; ///< Monotonic sequence used to recreate imported EGL resources.
|
|
egl::rgb_t rgb; ///< Imported RGB image used before VAAPI conversion.
|
|
|
|
int offset_x; ///< Horizontal offset in physical pixels.
|
|
int offset_y; ///< Vertical offset in physical pixels.
|
|
|
|
bool import_failed_last_frame = false; ///< Whether the previous frame's plane import failed, to avoid log spam.
|
|
};
|
|
|
|
/**
|
|
* This is a private structure of FFmpeg, I need this to manually create
|
|
* a VAAPI hardware context
|
|
*
|
|
* xdisplay will not be used internally by FFmpeg
|
|
*/
|
|
typedef struct VAAPIDevicePriv {
|
|
union {
|
|
void *xdisplay;
|
|
int fd;
|
|
} drm; ///< Native display or DRM fd passed to FFmpeg's VA-API context.
|
|
|
|
int drm_fd; ///< DRM fd.
|
|
} VAAPIDevicePriv;
|
|
|
|
/**
|
|
* VAAPI connection details.
|
|
*
|
|
* Allocated as AVHWDeviceContext.hwctx
|
|
*/
|
|
typedef struct AVVAAPIDeviceContext {
|
|
/**
|
|
* The VADisplay handle, to be filled by the user.
|
|
*/
|
|
va::VADisplay display;
|
|
/**
|
|
* Driver quirks to apply - this is filled by av_hwdevice_ctx_init(),
|
|
* with reference to a table of known drivers, unless the
|
|
* AV_VAAPI_DRIVER_QUIRK_USER_SET bit is already present. The user
|
|
* may need to refer to this field when performing any later
|
|
* operations using VAAPI with the same VADisplay.
|
|
*/
|
|
unsigned int driver_quirks;
|
|
} AVVAAPIDeviceContext;
|
|
|
|
static void __log(void *level, const char *msg) {
|
|
BOOST_LOG(*(boost::log::sources::severity_logger<int> *) level) << msg;
|
|
}
|
|
|
|
static void vaapi_hwdevice_ctx_free(AVHWDeviceContext *ctx) {
|
|
auto hwctx = (AVVAAPIDeviceContext *) ctx->hwctx;
|
|
auto priv = (VAAPIDevicePriv *) ctx->user_opaque;
|
|
|
|
vaTerminate(hwctx->display);
|
|
close(priv->drm_fd);
|
|
av_freep(&priv);
|
|
}
|
|
|
|
/**
|
|
* @brief Initialize FFmpeg's VA-API hardware device buffer.
|
|
*/
|
|
int vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *base, AVBufferRef **hw_device_buf) {
|
|
auto va = (va::va_t *) base;
|
|
auto fd = dup(va->file.el);
|
|
|
|
auto *priv = (VAAPIDevicePriv *) av_mallocz(sizeof(VAAPIDevicePriv));
|
|
priv->drm_fd = fd;
|
|
|
|
auto fg = util::fail_guard([fd, priv]() {
|
|
close(fd);
|
|
av_free(priv);
|
|
});
|
|
|
|
va::display_t display {vaGetDisplayDRM(fd)};
|
|
if (!display) {
|
|
BOOST_LOG(error) << "Couldn't open a va display from DRM with device: "sv << platf::resolve_render_device();
|
|
return -1;
|
|
}
|
|
|
|
va->va_display = display.get();
|
|
|
|
vaSetErrorCallback(display.get(), __log, &error);
|
|
vaSetErrorCallback(display.get(), __log, &info);
|
|
|
|
int major;
|
|
int minor;
|
|
auto status = vaInitialize(display.get(), &major, &minor);
|
|
if (status) {
|
|
BOOST_LOG(error) << "Couldn't initialize va display: "sv << vaErrorStr(status);
|
|
return -1;
|
|
}
|
|
|
|
BOOST_LOG(info) << "vaapi vendor: "sv << vaQueryVendorString(display.get());
|
|
|
|
*hw_device_buf = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VAAPI);
|
|
auto ctx = (AVHWDeviceContext *) (*hw_device_buf)->data;
|
|
auto hwctx = (AVVAAPIDeviceContext *) ctx->hwctx;
|
|
|
|
// Ownership of the VADisplay and DRM fd is now ours to manage via the free() function
|
|
hwctx->display = display.release();
|
|
ctx->user_opaque = priv;
|
|
ctx->free = vaapi_hwdevice_ctx_free;
|
|
fg.disable();
|
|
|
|
auto err = av_hwdevice_ctx_init(*hw_device_buf);
|
|
if (err) {
|
|
char err_str[AV_ERROR_MAX_STRING_SIZE] {0};
|
|
BOOST_LOG(error) << "Failed to create FFMpeg hardware device context: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);
|
|
|
|
return err;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static bool query(display_t::pointer display, VAProfile profile) {
|
|
std::vector<VAEntrypoint> entrypoints;
|
|
entrypoints.resize(vaMaxNumEntrypoints(display));
|
|
|
|
int count;
|
|
auto status = vaQueryConfigEntrypoints(display, profile, entrypoints.data(), &count);
|
|
if (status) {
|
|
BOOST_LOG(error) << "Couldn't query entrypoints: "sv << vaErrorStr(status);
|
|
return false;
|
|
}
|
|
entrypoints.resize(count);
|
|
|
|
for (auto entrypoint : entrypoints) {
|
|
if (entrypoint == VAEntrypointEncSlice || entrypoint == VAEntrypointEncSliceLP) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @brief Validate that the configured VAAPI device can be used.
|
|
*/
|
|
bool validate(int fd) {
|
|
va::display_t display {vaGetDisplayDRM(fd)};
|
|
if (!display) {
|
|
char string[1024];
|
|
|
|
auto bytes = readlink(std::format("/proc/self/fd/{}", fd).c_str(), string, sizeof(string));
|
|
|
|
std::string_view render_device {string, (std::size_t) bytes};
|
|
|
|
BOOST_LOG(error) << "Couldn't open a va display from DRM with device: "sv << render_device;
|
|
return false;
|
|
}
|
|
|
|
int major;
|
|
int minor;
|
|
auto status = vaInitialize(display.get(), &major, &minor);
|
|
if (status) {
|
|
BOOST_LOG(error) << "Couldn't initialize va display: "sv << vaErrorStr(status);
|
|
return false;
|
|
}
|
|
|
|
if (!query(display.get(), VAProfileH264Main)) {
|
|
return false;
|
|
}
|
|
|
|
if (video::active_hevc_mode > 1 && !query(display.get(), VAProfileHEVCMain)) {
|
|
return false;
|
|
}
|
|
|
|
if (video::active_hevc_mode > 2 && !query(display.get(), VAProfileHEVCMain10)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Create AVCodec encode device.
|
|
*/
|
|
std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_encode_device(int width, int height, file_t &&card, int offset_x, int offset_y, bool vram) {
|
|
if (vram) {
|
|
auto egl = std::make_unique<va::va_vram_t>();
|
|
if (egl->init(width, height, std::move(card), offset_x, offset_y)) {
|
|
return nullptr;
|
|
}
|
|
|
|
return egl;
|
|
}
|
|
|
|
else {
|
|
auto egl = std::make_unique<va::va_ram_t>();
|
|
if (egl->init(width, height, std::move(card))) {
|
|
return nullptr;
|
|
}
|
|
|
|
return egl;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Create AVCodec encode device.
|
|
*/
|
|
std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_encode_device(int width, int height, int offset_x, int offset_y, bool vram) {
|
|
auto render_device = platf::resolve_render_device();
|
|
|
|
file_t file = ::open(render_device.c_str(), O_RDWR); // NOSONAR(cpp:S1874): `_sopen_s` not available
|
|
if (file.el < 0) {
|
|
char string[1024];
|
|
BOOST_LOG(error) << "Couldn't open "sv << render_device << ": " << strerror_r(errno, string, sizeof(string));
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
return make_avcodec_encode_device(width, height, std::move(file), offset_x, offset_y, vram);
|
|
}
|
|
|
|
std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_encode_device(int width, int height, bool vram) {
|
|
return make_avcodec_encode_device(width, height, 0, 0, vram);
|
|
}
|
|
} // namespace va
|