/** * @file src/platform/linux/vaapi.cpp * @brief Definitions for VA-API hardware accelerated capture. */ // standard includes #include #include #include #include extern "C" { #include #include #include #include #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; /** * @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 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 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 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 *) 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 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 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(); if (egl->init(width, height, std::move(card), offset_x, offset_y)) { return nullptr; } return egl; } else { auto egl = std::make_unique(); if (egl->init(width, height, std::move(card))) { return nullptr; } return egl; } } /** * @brief Create AVCodec encode device. */ std::unique_ptr 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 make_avcodec_encode_device(int width, int height, bool vram) { return make_avcodec_encode_device(width, height, 0, 0, vram); } } // namespace va