sunshine-sdk/src/video.cpp
Cameron Gutman fe1832cda1 Revert "Use new 'remotegaming' scenario setting for QSV"
This causes Kaby Lake iGPUs to generate H.264 bitstreams with multiple
PPS NALUs which lead to some issues in current versions of Moonlight on
iOS and Android platforms.

This option also seems to override our max_dec_frame_buffering option
so it may increase latency on some Android devices too.

This reverts commit f838069a0e.
2023-10-14 01:49:47 -05:00

2759 lines
89 KiB
C++

/**
* @file src/video.cpp
* @brief todo
*/
#include <atomic>
#include <bitset>
#include <list>
#include <thread>
#include <boost/pointer_cast.hpp>
extern "C" {
#include <libavutil/mastering_display_metadata.h>
#include <libswscale/swscale.h>
}
#include "cbs.h"
#include "config.h"
#include "input.h"
#include "main.h"
#include "nvenc/nvenc_base.h"
#include "platform/common.h"
#include "sync.h"
#include "video.h"
#ifdef _WIN32
extern "C" {
#include <libavutil/hwcontext_d3d11va.h>
}
#endif
using namespace std::literals;
namespace video {
void
free_ctx(AVCodecContext *ctx) {
avcodec_free_context(&ctx);
}
void
free_frame(AVFrame *frame) {
av_frame_free(&frame);
}
void
free_buffer(AVBufferRef *ref) {
av_buffer_unref(&ref);
}
using avcodec_ctx_t = util::safe_ptr<AVCodecContext, free_ctx>;
using avcodec_frame_t = util::safe_ptr<AVFrame, free_frame>;
using avcodec_buffer_t = util::safe_ptr<AVBufferRef, free_buffer>;
using sws_t = util::safe_ptr<SwsContext, sws_freeContext>;
using img_event_t = std::shared_ptr<safe::event_t<std::shared_ptr<platf::img_t>>>;
namespace nv {
enum class profile_h264_e : int {
baseline,
main,
high,
high_444p,
};
enum class profile_hevc_e : int {
main,
main_10,
rext,
};
} // namespace nv
namespace qsv {
enum class profile_h264_e : int {
baseline = 66,
main = 77,
high = 100,
};
enum class profile_hevc_e : int {
main = 1,
main_10 = 2,
};
} // namespace qsv
platf::mem_type_e
map_base_dev_type(AVHWDeviceType type);
platf::pix_fmt_e
map_pix_fmt(AVPixelFormat fmt);
util::Either<avcodec_buffer_t, int>
dxgi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *);
util::Either<avcodec_buffer_t, int>
vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *);
util::Either<avcodec_buffer_t, int>
cuda_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *);
util::Either<avcodec_buffer_t, int>
vt_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *);
class avcodec_software_encode_device_t: public platf::avcodec_encode_device_t {
public:
int
convert(platf::img_t &img) override {
av_frame_make_writable(sw_frame.get());
const int linesizes[2] {
img.row_pitch, 0
};
std::uint8_t *data[4];
data[0] = sw_frame->data[0] + offsetY;
if (sw_frame->format == AV_PIX_FMT_NV12) {
data[1] = sw_frame->data[1] + offsetUV * 2;
data[2] = nullptr;
}
else {
data[1] = sw_frame->data[1] + offsetUV;
data[2] = sw_frame->data[2] + offsetUV;
data[3] = nullptr;
}
int ret = sws_scale(sws.get(), (std::uint8_t *const *) &img.data, linesizes, 0, img.height, data, sw_frame->linesize);
if (ret <= 0) {
BOOST_LOG(error) << "Couldn't convert image to required format and/or size"sv;
return -1;
}
// If frame is not a software frame, it means we still need to transfer from main memory
// to vram memory
if (frame->hw_frames_ctx) {
auto status = av_hwframe_transfer_data(frame, sw_frame.get(), 0);
if (status < 0) {
char string[AV_ERROR_MAX_STRING_SIZE];
BOOST_LOG(error) << "Failed to transfer image data to hardware frame: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status);
return -1;
}
}
return 0;
}
int
set_frame(AVFrame *frame, AVBufferRef *hw_frames_ctx) override {
this->frame = frame;
// If it's a hwframe, allocate buffers for hardware
if (hw_frames_ctx) {
hw_frame.reset(frame);
if (av_hwframe_get_buffer(hw_frames_ctx, frame, 0)) return -1;
}
else {
sw_frame.reset(frame);
}
return 0;
}
void
apply_colorspace() override {
auto avcodec_colorspace = avcodec_colorspace_from_sunshine_colorspace(colorspace);
sws_setColorspaceDetails(sws.get(),
sws_getCoefficients(SWS_CS_DEFAULT), 0,
sws_getCoefficients(avcodec_colorspace.software_format), avcodec_colorspace.range - 1,
0, 1 << 16, 1 << 16);
}
/**
* When preserving aspect ratio, ensure that padding is black
*/
int
prefill() {
auto frame = sw_frame ? sw_frame.get() : this->frame;
auto width = frame->width;
auto height = frame->height;
av_frame_get_buffer(frame, 0);
sws_t sws {
sws_getContext(
width, height, AV_PIX_FMT_BGR0,
width, height, (AVPixelFormat) frame->format,
SWS_LANCZOS | SWS_ACCURATE_RND,
nullptr, nullptr, nullptr)
};
if (!sws) {
return -1;
}
util::buffer_t<std::uint32_t> img { (std::size_t)(width * height) };
std::fill(std::begin(img), std::end(img), 0);
const int linesizes[2] {
width, 0
};
av_frame_make_writable(frame);
auto data = img.begin();
int ret = sws_scale(sws.get(), (std::uint8_t *const *) &data, linesizes, 0, height, frame->data, frame->linesize);
if (ret <= 0) {
BOOST_LOG(error) << "Couldn't convert image to required format and/or size"sv;
return -1;
}
return 0;
}
int
init(int in_width, int in_height, AVFrame *frame, AVPixelFormat format, bool hardware) {
// If the device used is hardware, yet the image resides on main memory
if (hardware) {
sw_frame.reset(av_frame_alloc());
sw_frame->width = frame->width;
sw_frame->height = frame->height;
sw_frame->format = format;
}
else {
this->frame = frame;
}
if (prefill()) {
return -1;
}
auto out_width = frame->width;
auto out_height = frame->height;
// Ensure aspect ratio is maintained
auto scalar = std::fminf((float) out_width / in_width, (float) out_height / in_height);
out_width = in_width * scalar;
out_height = in_height * scalar;
// result is always positive
auto offsetW = (frame->width - out_width) / 2;
auto offsetH = (frame->height - out_height) / 2;
offsetUV = (offsetW + offsetH * frame->width / 2) / 2;
offsetY = offsetW + offsetH * frame->width;
sws.reset(sws_getContext(
in_width, in_height, AV_PIX_FMT_BGR0,
out_width, out_height, format,
SWS_LANCZOS | SWS_ACCURATE_RND,
nullptr, nullptr, nullptr));
return sws ? 0 : -1;
}
// Store ownership when frame is hw_frame
avcodec_frame_t hw_frame;
avcodec_frame_t sw_frame;
sws_t sws;
// offset of input image to output frame in pixels
int offsetUV;
int offsetY;
};
enum flag_e : uint32_t {
DEFAULT = 0,
PARALLEL_ENCODING = 1 << 1,
H264_ONLY = 1 << 2, // When HEVC is too heavy
LIMITED_GOP_SIZE = 1 << 3, // Some encoders don't like it when you have an infinite GOP_SIZE. *cough* VAAPI *cough*
SINGLE_SLICE_ONLY = 1 << 4, // Never use multiple slices <-- Older intel iGPU's ruin it for everyone else :P
CBR_WITH_VBR = 1 << 5, // Use a VBR rate control mode to simulate CBR
RELAXED_COMPLIANCE = 1 << 6, // Use FF_COMPLIANCE_UNOFFICIAL compliance mode
NO_RC_BUF_LIMIT = 1 << 7, // Don't set rc_buffer_size
REF_FRAMES_INVALIDATION = 1 << 8, // Support reference frames invalidation
};
struct encoder_platform_formats_t {
virtual ~encoder_platform_formats_t() = default;
platf::mem_type_e dev_type;
platf::pix_fmt_e pix_fmt_8bit, pix_fmt_10bit;
};
struct encoder_platform_formats_avcodec: encoder_platform_formats_t {
using init_buffer_function_t = std::function<util::Either<avcodec_buffer_t, int>(platf::avcodec_encode_device_t *)>;
encoder_platform_formats_avcodec(
const AVHWDeviceType &avcodec_base_dev_type,
const AVHWDeviceType &avcodec_derived_dev_type,
const AVPixelFormat &avcodec_dev_pix_fmt,
const AVPixelFormat &avcodec_pix_fmt_8bit,
const AVPixelFormat &avcodec_pix_fmt_10bit,
const init_buffer_function_t &init_avcodec_hardware_input_buffer_function):
avcodec_base_dev_type { avcodec_base_dev_type },
avcodec_derived_dev_type { avcodec_derived_dev_type },
avcodec_dev_pix_fmt { avcodec_dev_pix_fmt },
avcodec_pix_fmt_8bit { avcodec_pix_fmt_8bit },
avcodec_pix_fmt_10bit { avcodec_pix_fmt_10bit },
init_avcodec_hardware_input_buffer { init_avcodec_hardware_input_buffer_function } {
dev_type = map_base_dev_type(avcodec_base_dev_type);
pix_fmt_8bit = map_pix_fmt(avcodec_pix_fmt_8bit);
pix_fmt_10bit = map_pix_fmt(avcodec_pix_fmt_10bit);
}
AVHWDeviceType avcodec_base_dev_type, avcodec_derived_dev_type;
AVPixelFormat avcodec_dev_pix_fmt;
AVPixelFormat avcodec_pix_fmt_8bit, avcodec_pix_fmt_10bit;
init_buffer_function_t init_avcodec_hardware_input_buffer;
};
struct encoder_platform_formats_nvenc: encoder_platform_formats_t {
encoder_platform_formats_nvenc(
const platf::mem_type_e &dev_type,
const platf::pix_fmt_e &pix_fmt_8bit,
const platf::pix_fmt_e &pix_fmt_10bit) {
encoder_platform_formats_t::dev_type = dev_type;
encoder_platform_formats_t::pix_fmt_8bit = pix_fmt_8bit;
encoder_platform_formats_t::pix_fmt_10bit = pix_fmt_10bit;
}
};
struct encoder_t {
std::string_view name;
enum flag_e {
PASSED, // Is supported
REF_FRAMES_RESTRICT, // Set maximum reference frames
CBR, // Some encoders don't support CBR, if not supported --> attempt constant quantatication parameter instead
DYNAMIC_RANGE, // hdr
VUI_PARAMETERS, // AMD encoder with VAAPI doesn't add VUI parameters to SPS
MAX_FLAGS
};
static std::string_view
from_flag(flag_e flag) {
#define _CONVERT(x) \
case flag_e::x: \
return #x##sv
switch (flag) {
_CONVERT(PASSED);
_CONVERT(REF_FRAMES_RESTRICT);
_CONVERT(CBR);
_CONVERT(DYNAMIC_RANGE);
_CONVERT(VUI_PARAMETERS);
_CONVERT(MAX_FLAGS);
}
#undef _CONVERT
return "unknown"sv;
}
struct option_t {
KITTY_DEFAULT_CONSTR_MOVE(option_t)
option_t(const option_t &) = default;
std::string name;
std::variant<int, int *, std::optional<int> *, std::function<int()>, std::string, std::string *> value;
option_t(std::string &&name, decltype(value) &&value):
name { std::move(name) }, value { std::move(value) } {}
};
const std::unique_ptr<const encoder_platform_formats_t> platform_formats;
struct {
std::vector<option_t> common_options;
std::vector<option_t> sdr_options;
std::vector<option_t> hdr_options;
std::optional<option_t> qp;
std::string name;
std::bitset<MAX_FLAGS> capabilities;
bool
operator[](flag_e flag) const {
return capabilities[(std::size_t) flag];
}
std::bitset<MAX_FLAGS>::reference
operator[](flag_e flag) {
return capabilities[(std::size_t) flag];
}
} av1, hevc, h264;
uint32_t flags;
};
struct encode_session_t {
virtual ~encode_session_t() = default;
virtual int
convert(platf::img_t &img) = 0;
virtual void
request_idr_frame() = 0;
virtual void
request_normal_frame() = 0;
virtual void
invalidate_ref_frames(int64_t first_frame, int64_t last_frame) = 0;
};
class avcodec_encode_session_t: public encode_session_t {
public:
avcodec_encode_session_t() = default;
avcodec_encode_session_t(avcodec_ctx_t &&avcodec_ctx, std::unique_ptr<platf::avcodec_encode_device_t> encode_device, int inject):
avcodec_ctx { std::move(avcodec_ctx) }, device { std::move(encode_device) }, inject { inject } {}
avcodec_encode_session_t(avcodec_encode_session_t &&other) noexcept = default;
~avcodec_encode_session_t() {
// Order matters here because the context relies on the hwdevice still being valid
avcodec_ctx.reset();
device.reset();
}
// Ensure objects are destroyed in the correct order
avcodec_encode_session_t &
operator=(avcodec_encode_session_t &&other) {
device = std::move(other.device);
avcodec_ctx = std::move(other.avcodec_ctx);
replacements = std::move(other.replacements);
sps = std::move(other.sps);
vps = std::move(other.vps);
inject = other.inject;
return *this;
}
int
convert(platf::img_t &img) override {
if (!device) return -1;
return device->convert(img);
}
void
request_idr_frame() override {
if (device && device->frame) {
auto &frame = device->frame;
frame->pict_type = AV_PICTURE_TYPE_I;
frame->flags |= AV_FRAME_FLAG_KEY;
}
}
void
request_normal_frame() override {
if (device && device->frame) {
auto &frame = device->frame;
frame->pict_type = AV_PICTURE_TYPE_NONE;
frame->flags &= ~AV_FRAME_FLAG_KEY;
}
}
void
invalidate_ref_frames(int64_t first_frame, int64_t last_frame) override {
BOOST_LOG(error) << "Encoder doesn't support reference frame invalidation";
request_idr_frame();
}
avcodec_ctx_t avcodec_ctx;
std::unique_ptr<platf::avcodec_encode_device_t> device;
std::vector<packet_raw_t::replace_t> replacements;
cbs::nal_t sps;
cbs::nal_t vps;
// inject sps/vps data into idr pictures
int inject;
};
class nvenc_encode_session_t: public encode_session_t {
public:
nvenc_encode_session_t(std::unique_ptr<platf::nvenc_encode_device_t> encode_device):
device(std::move(encode_device)) {
}
int
convert(platf::img_t &img) override {
if (!device) return -1;
return device->convert(img);
}
void
request_idr_frame() override {
force_idr = true;
}
void
request_normal_frame() override {
force_idr = false;
}
void
invalidate_ref_frames(int64_t first_frame, int64_t last_frame) override {
if (!device || !device->nvenc) return;
if (!device->nvenc->invalidate_ref_frames(first_frame, last_frame)) {
force_idr = true;
}
}
nvenc::nvenc_encoded_frame
encode_frame(uint64_t frame_index) {
if (!device || !device->nvenc) return {};
auto result = device->nvenc->encode_frame(frame_index, force_idr);
force_idr = false;
return result;
}
private:
std::unique_ptr<platf::nvenc_encode_device_t> device;
bool force_idr = false;
};
struct sync_session_ctx_t {
safe::signal_t *join_event;
safe::mail_raw_t::event_t<bool> shutdown_event;
safe::mail_raw_t::queue_t<packet_t> packets;
safe::mail_raw_t::event_t<bool> idr_events;
safe::mail_raw_t::event_t<hdr_info_t> hdr_events;
safe::mail_raw_t::event_t<input::touch_port_t> touch_port_events;
config_t config;
int frame_nr;
void *channel_data;
};
struct sync_session_t {
sync_session_ctx_t *ctx;
std::unique_ptr<encode_session_t> session;
};
using encode_session_ctx_queue_t = safe::queue_t<sync_session_ctx_t>;
using encode_e = platf::capture_e;
struct capture_ctx_t {
img_event_t images;
config_t config;
};
struct capture_thread_async_ctx_t {
std::shared_ptr<safe::queue_t<capture_ctx_t>> capture_ctx_queue;
std::thread capture_thread;
safe::signal_t reinit_event;
const encoder_t *encoder_p;
sync_util::sync_t<std::weak_ptr<platf::display_t>> display_wp;
};
struct capture_thread_sync_ctx_t {
encode_session_ctx_queue_t encode_session_ctx_queue { 30 };
};
int
start_capture_sync(capture_thread_sync_ctx_t &ctx);
void
end_capture_sync(capture_thread_sync_ctx_t &ctx);
int
start_capture_async(capture_thread_async_ctx_t &ctx);
void
end_capture_async(capture_thread_async_ctx_t &ctx);
// Keep a reference counter to ensure the capture thread only runs when other threads have a reference to the capture thread
auto capture_thread_async = safe::make_shared<capture_thread_async_ctx_t>(start_capture_async, end_capture_async);
auto capture_thread_sync = safe::make_shared<capture_thread_sync_ctx_t>(start_capture_sync, end_capture_sync);
#ifdef _WIN32
static encoder_t nvenc {
"nvenc"sv,
std::make_unique<encoder_platform_formats_nvenc>(
platf::mem_type_e::dxgi,
platf::pix_fmt_e::nv12, platf::pix_fmt_e::p010),
{
// Common options
{},
// SDR-specific options
{},
// HDR-specific options
{},
std::nullopt, // QP
"av1_nvenc"s,
},
{
// Common options
{},
// SDR-specific options
{},
// HDR-specific options
{},
std::nullopt, // QP
"hevc_nvenc"s,
},
{
// Common options
{},
// SDR-specific options
{},
// HDR-specific options
{},
std::nullopt, // QP
"h264_nvenc"s,
},
PARALLEL_ENCODING | REF_FRAMES_INVALIDATION // flags
};
#elif !defined(__APPLE__)
static encoder_t nvenc {
"nvenc"sv,
std::make_unique<encoder_platform_formats_avcodec>(
#ifdef _WIN32
AV_HWDEVICE_TYPE_D3D11VA, AV_HWDEVICE_TYPE_NONE,
AV_PIX_FMT_D3D11,
#else
AV_HWDEVICE_TYPE_CUDA, AV_HWDEVICE_TYPE_NONE,
AV_PIX_FMT_CUDA,
#endif
AV_PIX_FMT_NV12, AV_PIX_FMT_P010,
#ifdef _WIN32
dxgi_init_avcodec_hardware_input_buffer
#else
cuda_init_avcodec_hardware_input_buffer
#endif
),
{
// Common options
{
{ "delay"s, 0 },
{ "forced-idr"s, 1 },
{ "zerolatency"s, 1 },
{ "preset"s, &config::video.nv_legacy.preset },
{ "tune"s, NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY },
{ "rc"s, NV_ENC_PARAMS_RC_CBR },
{ "multipass"s, &config::video.nv_legacy.multipass },
},
// SDR-specific options
{},
// HDR-specific options
{},
std::nullopt,
"av1_nvenc"s,
},
{
// Common options
{
{ "delay"s, 0 },
{ "forced-idr"s, 1 },
{ "zerolatency"s, 1 },
{ "preset"s, &config::video.nv_legacy.preset },
{ "tune"s, NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY },
{ "rc"s, NV_ENC_PARAMS_RC_CBR },
{ "multipass"s, &config::video.nv_legacy.multipass },
},
// SDR-specific options
{
{ "profile"s, (int) nv::profile_hevc_e::main },
},
// HDR-specific options
{
{ "profile"s, (int) nv::profile_hevc_e::main_10 },
},
std::nullopt,
"hevc_nvenc"s,
},
{
{
{ "delay"s, 0 },
{ "forced-idr"s, 1 },
{ "zerolatency"s, 1 },
{ "preset"s, &config::video.nv_legacy.preset },
{ "tune"s, NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY },
{ "rc"s, NV_ENC_PARAMS_RC_CBR },
{ "coder"s, &config::video.nv_legacy.h264_coder },
{ "multipass"s, &config::video.nv_legacy.multipass },
},
// SDR-specific options
{
{ "profile"s, (int) nv::profile_h264_e::high },
},
{}, // HDR-specific options
std::make_optional<encoder_t::option_t>({ "qp"s, &config::video.qp }),
"h264_nvenc"s,
},
PARALLEL_ENCODING
};
#endif
#ifdef _WIN32
static encoder_t quicksync {
"quicksync"sv,
std::make_unique<encoder_platform_formats_avcodec>(
AV_HWDEVICE_TYPE_D3D11VA, AV_HWDEVICE_TYPE_QSV,
AV_PIX_FMT_QSV,
AV_PIX_FMT_NV12, AV_PIX_FMT_P010,
dxgi_init_avcodec_hardware_input_buffer),
{
// Common options
{
{ "preset"s, &config::video.qsv.qsv_preset },
{ "forced_idr"s, 1 },
{ "async_depth"s, 1 },
{ "low_delay_brc"s, 1 },
{ "low_power"s, 1 },
},
// SDR-specific options
{},
// HDR-specific options
{},
std::make_optional<encoder_t::option_t>({ "qp"s, &config::video.qp }),
"av1_qsv"s,
},
{
// Common options
{
{ "preset"s, &config::video.qsv.qsv_preset },
{ "forced_idr"s, 1 },
{ "async_depth"s, 1 },
{ "low_delay_brc"s, 1 },
{ "low_power"s, 1 },
{ "recovery_point_sei"s, 0 },
{ "pic_timing_sei"s, 0 },
},
// SDR-specific options
{
{ "profile"s, (int) qsv::profile_hevc_e::main },
},
// HDR-specific options
{
{ "profile"s, (int) qsv::profile_hevc_e::main_10 },
},
std::make_optional<encoder_t::option_t>({ "qp"s, &config::video.qp }),
"hevc_qsv"s,
},
{
// Common options
{
{ "preset"s, &config::video.qsv.qsv_preset },
{ "cavlc"s, &config::video.qsv.qsv_cavlc },
{ "forced_idr"s, 1 },
{ "async_depth"s, 1 },
{ "low_delay_brc"s, 1 },
{ "low_power"s, 1 },
{ "recovery_point_sei"s, 0 },
{ "vcm"s, 1 },
{ "pic_timing_sei"s, 0 },
{ "max_dec_frame_buffering"s, 1 },
},
// SDR-specific options
{
{ "profile"s, (int) qsv::profile_h264_e::high },
},
{}, // HDR-specific options
std::make_optional<encoder_t::option_t>({ "qp"s, &config::video.qp }),
"h264_qsv"s,
},
PARALLEL_ENCODING | CBR_WITH_VBR | RELAXED_COMPLIANCE | NO_RC_BUF_LIMIT
};
static encoder_t amdvce {
"amdvce"sv,
std::make_unique<encoder_platform_formats_avcodec>(
AV_HWDEVICE_TYPE_D3D11VA, AV_HWDEVICE_TYPE_NONE,
AV_PIX_FMT_D3D11,
AV_PIX_FMT_NV12, AV_PIX_FMT_P010,
dxgi_init_avcodec_hardware_input_buffer),
{
// Common options
{
{ "filler_data"s, false },
{ "log_to_dbg"s, []() { return config::sunshine.min_log_level < 2 ? 1 : 0; } },
{ "preencode"s, &config::video.amd.amd_preanalysis },
{ "quality"s, &config::video.amd.amd_quality_av1 },
{ "rc"s, &config::video.amd.amd_rc_av1 },
{ "usage"s, &config::video.amd.amd_usage_av1 },
},
{}, // SDR-specific options
{}, // HDR-specific options
std::make_optional<encoder_t::option_t>({ "qp_p"s, &config::video.qp }),
"av1_amf"s,
},
{
// Common options
{
{ "filler_data"s, false },
{ "log_to_dbg"s, []() { return config::sunshine.min_log_level < 2 ? 1 : 0; } },
{ "gops_per_idr"s, 1 },
{ "header_insertion_mode"s, "idr"s },
{ "preencode"s, &config::video.amd.amd_preanalysis },
{ "qmax"s, 51 },
{ "qmin"s, 0 },
{ "quality"s, &config::video.amd.amd_quality_hevc },
{ "rc"s, &config::video.amd.amd_rc_hevc },
{ "usage"s, &config::video.amd.amd_usage_hevc },
{ "vbaq"s, &config::video.amd.amd_vbaq },
},
{}, // SDR-specific options
{}, // HDR-specific options
std::make_optional<encoder_t::option_t>({ "qp_p"s, &config::video.qp }),
"hevc_amf"s,
},
{
// Common options
{
{ "filler_data"s, false },
{ "log_to_dbg"s, []() { return config::sunshine.min_log_level < 2 ? 1 : 0; } },
{ "preencode"s, &config::video.amd.amd_preanalysis },
{ "qmax"s, 51 },
{ "qmin"s, 0 },
{ "quality"s, &config::video.amd.amd_quality_h264 },
{ "rc"s, &config::video.amd.amd_rc_h264 },
{ "usage"s, &config::video.amd.amd_usage_h264 },
{ "vbaq"s, &config::video.amd.amd_vbaq },
},
{}, // SDR-specific options
{}, // HDR-specific options
std::make_optional<encoder_t::option_t>({ "qp_p"s, &config::video.qp }),
"h264_amf"s,
},
PARALLEL_ENCODING
};
#endif
static encoder_t software {
"software"sv,
std::make_unique<encoder_platform_formats_avcodec>(
AV_HWDEVICE_TYPE_NONE, AV_HWDEVICE_TYPE_NONE,
AV_PIX_FMT_NONE,
AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P10,
nullptr),
{
// libsvtav1 takes different presets than libx264/libx265.
// We set an infinite GOP length, use a low delay prediction structure,
// force I frames to be key frames, and set max bitrate to default to work
// around a FFmpeg bug with CBR mode.
{
{ "svtav1-params"s, "keyint=-1:pred-struct=1:force-key-frames=1:mbr=0"s },
{ "preset"s, &config::video.sw.svtav1_preset },
},
{}, // SDR-specific options
{}, // HDR-specific options
std::make_optional<encoder_t::option_t>("qp"s, &config::video.qp),
#ifdef ENABLE_BROKEN_AV1_ENCODER
// Due to bugs preventing on-demand IDR frames from working and very poor
// real-time encoding performance, we do not enable libsvtav1 by default.
// It is only suitable for testing AV1 until the IDR frame issue is fixed.
"libsvtav1"s,
#else
{},
#endif
},
{
// x265's Info SEI is so long that it causes the IDR picture data to be
// kicked to the 2nd packet in the frame, breaking Moonlight's parsing logic.
// It also looks like gop_size isn't passed on to x265, so we have to set
// 'keyint=-1' in the parameters ourselves.
{
{ "forced-idr"s, 1 },
{ "x265-params"s, "info=0:keyint=-1"s },
{ "preset"s, &config::video.sw.sw_preset },
{ "tune"s, &config::video.sw.sw_tune },
},
{}, // SDR-specific options
{}, // HDR-specific options
std::make_optional<encoder_t::option_t>("qp"s, &config::video.qp),
"libx265"s,
},
{
// Common options
{
{ "preset"s, &config::video.sw.sw_preset },
{ "tune"s, &config::video.sw.sw_tune },
},
{}, // SDR-specific options
{}, // HDR-specific options
std::make_optional<encoder_t::option_t>("qp"s, &config::video.qp),
"libx264"s,
},
H264_ONLY | PARALLEL_ENCODING
};
#ifdef __linux__
static encoder_t vaapi {
"vaapi"sv,
std::make_unique<encoder_platform_formats_avcodec>(
AV_HWDEVICE_TYPE_VAAPI, AV_HWDEVICE_TYPE_NONE,
AV_PIX_FMT_VAAPI,
AV_PIX_FMT_NV12, AV_PIX_FMT_YUV420P10,
vaapi_init_avcodec_hardware_input_buffer),
{
// Common options
{
{ "async_depth"s, 1 },
{ "idr_interval"s, std::numeric_limits<int>::max() },
},
{}, // SDR-specific options
{}, // HDR-specific options
std::make_optional<encoder_t::option_t>("qp"s, &config::video.qp),
"av1_vaapi"s,
},
{
// Common options
{
{ "async_depth"s, 1 },
{ "sei"s, 0 },
{ "idr_interval"s, std::numeric_limits<int>::max() },
},
{}, // SDR-specific options
{}, // HDR-specific options
std::make_optional<encoder_t::option_t>("qp"s, &config::video.qp),
"hevc_vaapi"s,
},
{
// Common options
{
{ "async_depth"s, 1 },
{ "sei"s, 0 },
{ "idr_interval"s, std::numeric_limits<int>::max() },
},
{}, // SDR-specific options
{}, // HDR-specific options
std::make_optional<encoder_t::option_t>("qp"s, &config::video.qp),
"h264_vaapi"s,
},
LIMITED_GOP_SIZE | PARALLEL_ENCODING | SINGLE_SLICE_ONLY | NO_RC_BUF_LIMIT
};
#endif
#ifdef __APPLE__
static encoder_t videotoolbox {
"videotoolbox"sv,
std::make_unique<encoder_platform_formats_avcodec>(
AV_HWDEVICE_TYPE_VIDEOTOOLBOX, AV_HWDEVICE_TYPE_NONE,
AV_PIX_FMT_VIDEOTOOLBOX,
AV_PIX_FMT_NV12, AV_PIX_FMT_P010,
vt_init_avcodec_hardware_input_buffer),
{
// Common options
{
{ "allow_sw"s, &config::video.vt.vt_allow_sw },
{ "require_sw"s, &config::video.vt.vt_require_sw },
{ "realtime"s, &config::video.vt.vt_realtime },
{ "prio_speed"s, 1 },
},
{}, // SDR-specific options
{}, // HDR-specific options
std::nullopt,
"av1_videotoolbox"s,
},
{
// Common options
{
{ "allow_sw"s, &config::video.vt.vt_allow_sw },
{ "require_sw"s, &config::video.vt.vt_require_sw },
{ "realtime"s, &config::video.vt.vt_realtime },
{ "prio_speed"s, 1 },
},
{}, // SDR-specific options
{}, // HDR-specific options
std::nullopt,
"hevc_videotoolbox"s,
},
{
// Common options
{
{ "allow_sw"s, &config::video.vt.vt_allow_sw },
{ "require_sw"s, &config::video.vt.vt_require_sw },
{ "realtime"s, &config::video.vt.vt_realtime },
{ "prio_speed"s, 1 },
},
{}, // SDR-specific options
{}, // HDR-specific options
std::nullopt,
"h264_videotoolbox"s,
},
DEFAULT
};
#endif
static const std::vector<encoder_t *> encoders {
#ifndef __APPLE__
&nvenc,
#endif
#ifdef _WIN32
&quicksync,
&amdvce,
#endif
#ifdef __linux__
&vaapi,
#endif
#ifdef __APPLE__
&videotoolbox,
#endif
&software
};
static encoder_t *chosen_encoder;
int active_hevc_mode;
int active_av1_mode;
bool last_encoder_probe_supported_ref_frames_invalidation = false;
void
reset_display(std::shared_ptr<platf::display_t> &disp, const platf::mem_type_e &type, const std::string &display_name, const config_t &config) {
// We try this twice, in case we still get an error on reinitialization
for (int x = 0; x < 2; ++x) {
disp.reset();
disp = platf::display(type, display_name, config);
if (disp) {
break;
}
// The capture code depends on us to sleep between failures
std::this_thread::sleep_for(200ms);
}
}
void
captureThread(
std::shared_ptr<safe::queue_t<capture_ctx_t>> capture_ctx_queue,
sync_util::sync_t<std::weak_ptr<platf::display_t>> &display_wp,
safe::signal_t &reinit_event,
const encoder_t &encoder) {
std::vector<capture_ctx_t> capture_ctxs;
auto fg = util::fail_guard([&]() {
capture_ctx_queue->stop();
// Stop all sessions listening to this thread
for (auto &capture_ctx : capture_ctxs) {
capture_ctx.images->stop();
}
for (auto &capture_ctx : capture_ctx_queue->unsafe()) {
capture_ctx.images->stop();
}
});
auto switch_display_event = mail::man->event<int>(mail::switch_display);
// Get all the monitor names now, rather than at boot, to
// get the most up-to-date list available monitors
auto display_names = platf::display_names(encoder.platform_formats->dev_type);
int display_p = 0;
if (display_names.empty()) {
display_names.emplace_back(config::video.output_name);
}
for (int x = 0; x < display_names.size(); ++x) {
if (display_names[x] == config::video.output_name) {
display_p = x;
break;
}
}
// Wait for the initial capture context or a request to stop the queue
auto initial_capture_ctx = capture_ctx_queue->pop();
if (!initial_capture_ctx) {
return;
}
capture_ctxs.emplace_back(std::move(*initial_capture_ctx));
auto disp = platf::display(encoder.platform_formats->dev_type, display_names[display_p], capture_ctxs.front().config);
if (!disp) {
return;
}
display_wp = disp;
constexpr auto capture_buffer_size = 12;
std::list<std::shared_ptr<platf::img_t>> imgs(capture_buffer_size);
std::vector<std::optional<std::chrono::steady_clock::time_point>> imgs_used_timestamps;
const std::chrono::seconds trim_timeot = 3s;
auto trim_imgs = [&]() {
// count allocated and used within current pool
size_t allocated_count = 0;
size_t used_count = 0;
for (const auto &img : imgs) {
if (img) {
allocated_count += 1;
if (img.use_count() > 1) {
used_count += 1;
}
}
}
// remember the timestamp of currently used count
const auto now = std::chrono::steady_clock::now();
if (imgs_used_timestamps.size() <= used_count) {
imgs_used_timestamps.resize(used_count + 1);
}
imgs_used_timestamps[used_count] = now;
// decide whether to trim allocated unused above the currently used count
// based on last used timestamp and universal timeout
size_t trim_target = used_count;
for (size_t i = used_count; i < imgs_used_timestamps.size(); i++) {
if (imgs_used_timestamps[i] && now - *imgs_used_timestamps[i] < trim_timeot) {
trim_target = i;
}
}
// trim allocated unused above the newly decided trim target
if (allocated_count > trim_target) {
size_t to_trim = allocated_count - trim_target;
// prioritize trimming least recently used
for (auto it = imgs.rbegin(); it != imgs.rend(); it++) {
auto &img = *it;
if (img && img.use_count() == 1) {
img.reset();
to_trim -= 1;
if (to_trim == 0) break;
}
}
// forget timestamps that no longer relevant
imgs_used_timestamps.resize(trim_target + 1);
}
};
auto pull_free_image_callback = [&](std::shared_ptr<platf::img_t> &img_out) -> bool {
img_out.reset();
while (capture_ctx_queue->running()) {
// pick first allocated but unused
for (auto it = imgs.begin(); it != imgs.end(); it++) {
if (*it && it->use_count() == 1) {
img_out = *it;
if (it != imgs.begin()) {
// move image to the front of the list to prioritize its reusal
imgs.erase(it);
imgs.push_front(img_out);
}
break;
}
}
// otherwise pick first unallocated
if (!img_out) {
for (auto it = imgs.begin(); it != imgs.end(); it++) {
if (!*it) {
// allocate image
*it = disp->alloc_img();
img_out = *it;
if (it != imgs.begin()) {
// move image to the front of the list to prioritize its reusal
imgs.erase(it);
imgs.push_front(img_out);
}
break;
}
}
}
if (img_out) {
// trim allocated but unused portion of the pool based on timeouts
trim_imgs();
img_out->frame_timestamp.reset();
return true;
}
else {
// sleep and retry if image pool is full
std::this_thread::sleep_for(1ms);
}
}
return false;
};
// Capture takes place on this thread
platf::adjust_thread_priority(platf::thread_priority_e::critical);
while (capture_ctx_queue->running()) {
bool artificial_reinit = false;
auto push_captured_image_callback = [&](std::shared_ptr<platf::img_t> &&img, bool frame_captured) -> bool {
KITTY_WHILE_LOOP(auto capture_ctx = std::begin(capture_ctxs), capture_ctx != std::end(capture_ctxs), {
if (!capture_ctx->images->running()) {
capture_ctx = capture_ctxs.erase(capture_ctx);
continue;
}
if (frame_captured) {
capture_ctx->images->raise(img);
}
++capture_ctx;
})
if (!capture_ctx_queue->running()) {
return false;
}
while (capture_ctx_queue->peek()) {
capture_ctxs.emplace_back(std::move(*capture_ctx_queue->pop()));
}
if (switch_display_event->peek()) {
artificial_reinit = true;
display_p = std::clamp(*switch_display_event->pop(), 0, (int) display_names.size() - 1);
return false;
}
return true;
};
auto status = disp->capture(push_captured_image_callback, pull_free_image_callback, &display_cursor);
if (artificial_reinit && status != platf::capture_e::error) {
status = platf::capture_e::reinit;
artificial_reinit = false;
}
switch (status) {
case platf::capture_e::reinit: {
reinit_event.raise(true);
// Some classes of images contain references to the display --> display won't delete unless img is deleted
for (auto &img : imgs) {
img.reset();
}
// display_wp is modified in this thread only
// Wait for the other shared_ptr's of display to be destroyed.
// New displays will only be created in this thread.
while (display_wp->use_count() != 1) {
// Free images that weren't consumed by the encoders. These can reference the display and prevent
// the ref count from reaching 1. We do this here rather than on the encoder thread to avoid race
// conditions where the encoding loop might free a good frame after reinitializing if we capture
// a new frame here before the encoder has finished reinitializing.
KITTY_WHILE_LOOP(auto capture_ctx = std::begin(capture_ctxs), capture_ctx != std::end(capture_ctxs), {
if (!capture_ctx->images->running()) {
capture_ctx = capture_ctxs.erase(capture_ctx);
continue;
}
while (capture_ctx->images->peek()) {
capture_ctx->images->pop();
}
++capture_ctx;
});
std::this_thread::sleep_for(20ms);
}
while (capture_ctx_queue->running()) {
// reset_display() will sleep between retries
reset_display(disp, encoder.platform_formats->dev_type, display_names[display_p], capture_ctxs.front().config);
if (disp) {
break;
}
}
if (!disp) {
return;
}
display_wp = disp;
reinit_event.reset();
continue;
}
case platf::capture_e::error:
case platf::capture_e::ok:
case platf::capture_e::timeout:
case platf::capture_e::interrupted:
return;
default:
BOOST_LOG(error) << "Unrecognized capture status ["sv << (int) status << ']';
return;
}
}
}
int
encode_avcodec(int64_t frame_nr, avcodec_encode_session_t &session, safe::mail_raw_t::queue_t<packet_t> &packets, void *channel_data, std::optional<std::chrono::steady_clock::time_point> frame_timestamp) {
auto &frame = session.device->frame;
frame->pts = frame_nr;
auto &ctx = session.avcodec_ctx;
auto &sps = session.sps;
auto &vps = session.vps;
// send the frame to the encoder
auto ret = avcodec_send_frame(ctx.get(), frame);
if (ret < 0) {
char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };
BOOST_LOG(error) << "Could not send a frame for encoding: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, ret);
return -1;
}
while (ret >= 0) {
auto packet = std::make_unique<packet_raw_avcodec>();
auto av_packet = packet.get()->av_packet;
ret = avcodec_receive_packet(ctx.get(), av_packet);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
return 0;
}
else if (ret < 0) {
return ret;
}
if ((frame->flags & AV_FRAME_FLAG_KEY) && !(av_packet->flags & AV_PKT_FLAG_KEY)) {
BOOST_LOG(error) << "Encoder did not produce IDR frame when requested!"sv;
}
if (session.inject) {
if (session.inject == 1) {
auto h264 = cbs::make_sps_h264(ctx.get(), av_packet);
sps = std::move(h264.sps);
}
else {
auto hevc = cbs::make_sps_hevc(ctx.get(), av_packet);
sps = std::move(hevc.sps);
vps = std::move(hevc.vps);
session.replacements.emplace_back(
std::string_view((char *) std::begin(vps.old), vps.old.size()),
std::string_view((char *) std::begin(vps._new), vps._new.size()));
}
session.inject = 0;
session.replacements.emplace_back(
std::string_view((char *) std::begin(sps.old), sps.old.size()),
std::string_view((char *) std::begin(sps._new), sps._new.size()));
}
if (av_packet && av_packet->pts == frame_nr) {
packet->frame_timestamp = frame_timestamp;
}
packet->replacements = &session.replacements;
packet->channel_data = channel_data;
packets->raise(std::move(packet));
}
return 0;
}
int
encode_nvenc(int64_t frame_nr, nvenc_encode_session_t &session, safe::mail_raw_t::queue_t<packet_t> &packets, void *channel_data, std::optional<std::chrono::steady_clock::time_point> frame_timestamp) {
auto encoded_frame = session.encode_frame(frame_nr);
if (encoded_frame.data.empty()) {
BOOST_LOG(error) << "NvENC returned empty packet";
return -1;
}
if (frame_nr != encoded_frame.frame_index) {
BOOST_LOG(error) << "NvENC frame index mismatch " << frame_nr << " " << encoded_frame.frame_index;
}
auto packet = std::make_unique<packet_raw_generic>(std::move(encoded_frame.data), encoded_frame.frame_index, encoded_frame.idr);
packet->channel_data = channel_data;
packet->after_ref_frame_invalidation = encoded_frame.after_ref_frame_invalidation;
packet->frame_timestamp = frame_timestamp;
packets->raise(std::move(packet));
return 0;
}
int
encode(int64_t frame_nr, encode_session_t &session, safe::mail_raw_t::queue_t<packet_t> &packets, void *channel_data, std::optional<std::chrono::steady_clock::time_point> frame_timestamp) {
if (auto avcodec_session = dynamic_cast<avcodec_encode_session_t *>(&session)) {
return encode_avcodec(frame_nr, *avcodec_session, packets, channel_data, frame_timestamp);
}
else if (auto nvenc_session = dynamic_cast<nvenc_encode_session_t *>(&session)) {
return encode_nvenc(frame_nr, *nvenc_session, packets, channel_data, frame_timestamp);
}
return -1;
}
std::unique_ptr<avcodec_encode_session_t>
make_avcodec_encode_session(platf::display_t *disp, const encoder_t &encoder, const config_t &config, int width, int height, std::unique_ptr<platf::avcodec_encode_device_t> encode_device) {
auto platform_formats = dynamic_cast<const encoder_platform_formats_avcodec *>(encoder.platform_formats.get());
if (!platform_formats) {
return nullptr;
}
bool hardware = platform_formats->avcodec_base_dev_type != AV_HWDEVICE_TYPE_NONE;
auto &video_format = config.videoFormat == 0 ? encoder.h264 :
config.videoFormat == 1 ? encoder.hevc :
encoder.av1;
if (!video_format[encoder_t::PASSED] || !disp->is_codec_supported(video_format.name, config)) {
BOOST_LOG(error) << encoder.name << ": "sv << video_format.name << " mode not supported"sv;
return nullptr;
}
if (config.dynamicRange && !video_format[encoder_t::DYNAMIC_RANGE]) {
BOOST_LOG(error) << video_format.name << ": dynamic range not supported"sv;
return nullptr;
}
auto codec = avcodec_find_encoder_by_name(video_format.name.c_str());
if (!codec) {
BOOST_LOG(error) << "Couldn't open ["sv << video_format.name << ']';
return nullptr;
}
avcodec_ctx_t ctx { avcodec_alloc_context3(codec) };
ctx->width = config.width;
ctx->height = config.height;
ctx->time_base = AVRational { 1, config.framerate };
ctx->framerate = AVRational { config.framerate, 1 };
switch (config.videoFormat) {
case 0:
ctx->profile = FF_PROFILE_H264_HIGH;
break;
case 1:
ctx->profile = config.dynamicRange ? FF_PROFILE_HEVC_MAIN_10 : FF_PROFILE_HEVC_MAIN;
break;
case 2:
// AV1 supports both 8 and 10 bit encoding with the same Main profile
ctx->profile = FF_PROFILE_AV1_MAIN;
break;
}
// B-frames delay decoder output, so never use them
ctx->max_b_frames = 0;
// Use an infinite GOP length since I-frames are generated on demand
ctx->gop_size = encoder.flags & LIMITED_GOP_SIZE ?
std::numeric_limits<std::int16_t>::max() :
std::numeric_limits<int>::max();
ctx->keyint_min = std::numeric_limits<int>::max();
// Some client decoders have limits on the number of reference frames
if (config.numRefFrames) {
if (video_format[encoder_t::REF_FRAMES_RESTRICT]) {
ctx->refs = config.numRefFrames;
}
else {
BOOST_LOG(warning) << "Client requested reference frame limit, but encoder doesn't support it!"sv;
}
}
ctx->flags |= (AV_CODEC_FLAG_CLOSED_GOP | AV_CODEC_FLAG_LOW_DELAY);
ctx->flags2 |= AV_CODEC_FLAG2_FAST;
auto colorspace = encode_device->colorspace;
auto avcodec_colorspace = avcodec_colorspace_from_sunshine_colorspace(colorspace);
ctx->color_range = avcodec_colorspace.range;
ctx->color_primaries = avcodec_colorspace.primaries;
ctx->color_trc = avcodec_colorspace.transfer_function;
ctx->colorspace = avcodec_colorspace.matrix;
auto sw_fmt = (colorspace.bit_depth == 10) ? platform_formats->avcodec_pix_fmt_10bit : platform_formats->avcodec_pix_fmt_8bit;
// Used by cbs::make_sps_hevc
ctx->sw_pix_fmt = sw_fmt;
if (hardware) {
avcodec_buffer_t encoding_stream_context;
ctx->pix_fmt = platform_formats->avcodec_dev_pix_fmt;
// Create the base hwdevice context
auto buf_or_error = platform_formats->init_avcodec_hardware_input_buffer(encode_device.get());
if (buf_or_error.has_right()) {
return nullptr;
}
encoding_stream_context = std::move(buf_or_error.left());
// If this encoder requires derivation from the base, derive the desired type
if (platform_formats->avcodec_derived_dev_type != AV_HWDEVICE_TYPE_NONE) {
avcodec_buffer_t derived_context;
// Allow the hwdevice to prepare for this type of context to be derived
if (encode_device->prepare_to_derive_context(platform_formats->avcodec_derived_dev_type)) {
return nullptr;
}
auto err = av_hwdevice_ctx_create_derived(&derived_context, platform_formats->avcodec_derived_dev_type, encoding_stream_context.get(), 0);
if (err) {
char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };
BOOST_LOG(error) << "Failed to derive device context: "sv << av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, err);
return nullptr;
}
encoding_stream_context = std::move(derived_context);
}
// Initialize avcodec hardware frames
{
avcodec_buffer_t frame_ref { av_hwframe_ctx_alloc(encoding_stream_context.get()) };
auto frame_ctx = (AVHWFramesContext *) frame_ref->data;
frame_ctx->format = ctx->pix_fmt;
frame_ctx->sw_format = sw_fmt;
frame_ctx->height = ctx->height;
frame_ctx->width = ctx->width;
frame_ctx->initial_pool_size = 0;
// Allow the hwdevice to modify hwframe context parameters
encode_device->init_hwframes(frame_ctx);
if (auto err = av_hwframe_ctx_init(frame_ref.get()); err < 0) {
return nullptr;
}
ctx->hw_frames_ctx = av_buffer_ref(frame_ref.get());
}
ctx->slices = config.slicesPerFrame;
}
else /* software */ {
ctx->pix_fmt = sw_fmt;
// Clients will request for the fewest slices per frame to get the
// most efficient encode, but we may want to provide more slices than
// requested to ensure we have enough parallelism for good performance.
ctx->slices = std::max(config.slicesPerFrame, config::video.min_threads);
}
if (encoder.flags & SINGLE_SLICE_ONLY) {
ctx->slices = 1;
}
ctx->thread_type = FF_THREAD_SLICE;
ctx->thread_count = ctx->slices;
AVDictionary *options { nullptr };
auto handle_option = [&options](const encoder_t::option_t &option) {
std::visit(
util::overloaded {
[&](int v) { av_dict_set_int(&options, option.name.c_str(), v, 0); },
[&](int *v) { av_dict_set_int(&options, option.name.c_str(), *v, 0); },
[&](std::optional<int> *v) { if(*v) av_dict_set_int(&options, option.name.c_str(), **v, 0); },
[&](std::function<int()> v) { av_dict_set_int(&options, option.name.c_str(), v(), 0); },
[&](const std::string &v) { av_dict_set(&options, option.name.c_str(), v.c_str(), 0); },
[&](std::string *v) { if(!v->empty()) av_dict_set(&options, option.name.c_str(), v->c_str(), 0); } },
option.value);
};
// Apply common options, then format-specific overrides
for (auto &option : video_format.common_options) {
handle_option(option);
}
for (auto &option : (config.dynamicRange ? video_format.hdr_options : video_format.sdr_options)) {
handle_option(option);
}
if (video_format[encoder_t::CBR]) {
auto bitrate = config.bitrate * 1000;
ctx->rc_max_rate = bitrate;
ctx->bit_rate = bitrate;
if (encoder.flags & CBR_WITH_VBR) {
// Ensure rc_max_bitrate != bit_rate to force VBR mode
ctx->bit_rate--;
}
else {
ctx->rc_min_rate = bitrate;
}
if (encoder.flags & RELAXED_COMPLIANCE) {
ctx->strict_std_compliance = FF_COMPLIANCE_UNOFFICIAL;
}
if (!(encoder.flags & NO_RC_BUF_LIMIT)) {
if (!hardware && (ctx->slices > 1 || config.videoFormat == 1)) {
// Use a larger rc_buffer_size for software encoding when slices are enabled,
// because libx264 can severely degrade quality if the buffer is too small.
// libx265 encounters this issue more frequently, so always scale the
// buffer by 1.5x for software HEVC encoding.
ctx->rc_buffer_size = bitrate / ((config.framerate * 10) / 15);
}
else {
ctx->rc_buffer_size = bitrate / config.framerate;
}
}
}
else if (video_format.qp) {
handle_option(*video_format.qp);
}
else {
BOOST_LOG(error) << "Couldn't set video quality: encoder "sv << encoder.name << " doesn't support qp"sv;
return nullptr;
}
if (auto status = avcodec_open2(ctx.get(), codec, &options)) {
char err_str[AV_ERROR_MAX_STRING_SIZE] { 0 };
BOOST_LOG(error)
<< "Could not open codec ["sv
<< video_format.name << "]: "sv
<< av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, status);
return nullptr;
}
avcodec_frame_t frame { av_frame_alloc() };
frame->format = ctx->pix_fmt;
frame->width = ctx->width;
frame->height = ctx->height;
frame->color_range = ctx->color_range;
frame->color_primaries = ctx->color_primaries;
frame->color_trc = ctx->color_trc;
frame->colorspace = ctx->colorspace;
frame->chroma_location = ctx->chroma_sample_location;
// Attach HDR metadata to the AVFrame
if (colorspace_is_hdr(colorspace)) {
SS_HDR_METADATA hdr_metadata;
if (disp->get_hdr_metadata(hdr_metadata)) {
auto mdm = av_mastering_display_metadata_create_side_data(frame.get());
mdm->display_primaries[0][0] = av_make_q(hdr_metadata.displayPrimaries[0].x, 50000);
mdm->display_primaries[0][1] = av_make_q(hdr_metadata.displayPrimaries[0].y, 50000);
mdm->display_primaries[1][0] = av_make_q(hdr_metadata.displayPrimaries[1].x, 50000);
mdm->display_primaries[1][1] = av_make_q(hdr_metadata.displayPrimaries[1].y, 50000);
mdm->display_primaries[2][0] = av_make_q(hdr_metadata.displayPrimaries[2].x, 50000);
mdm->display_primaries[2][1] = av_make_q(hdr_metadata.displayPrimaries[2].y, 50000);
mdm->white_point[0] = av_make_q(hdr_metadata.whitePoint.x, 50000);
mdm->white_point[1] = av_make_q(hdr_metadata.whitePoint.y, 50000);
mdm->min_luminance = av_make_q(hdr_metadata.minDisplayLuminance, 10000);
mdm->max_luminance = av_make_q(hdr_metadata.maxDisplayLuminance, 1);
mdm->has_luminance = hdr_metadata.maxDisplayLuminance != 0 ? 1 : 0;
mdm->has_primaries = hdr_metadata.displayPrimaries[0].x != 0 ? 1 : 0;
if (hdr_metadata.maxContentLightLevel != 0 || hdr_metadata.maxFrameAverageLightLevel != 0) {
auto clm = av_content_light_metadata_create_side_data(frame.get());
clm->MaxCLL = hdr_metadata.maxContentLightLevel;
clm->MaxFALL = hdr_metadata.maxFrameAverageLightLevel;
}
}
else {
BOOST_LOG(error) << "Couldn't get display hdr metadata when colorspace selection indicates it should have one";
}
}
std::unique_ptr<platf::avcodec_encode_device_t> encode_device_final;
if (!encode_device->data) {
auto software_encode_device = std::make_unique<avcodec_software_encode_device_t>();
if (software_encode_device->init(width, height, frame.get(), sw_fmt, hardware)) {
return nullptr;
}
software_encode_device->colorspace = colorspace;
encode_device_final = std::move(software_encode_device);
}
else {
encode_device_final = std::move(encode_device);
}
if (encode_device_final->set_frame(frame.release(), ctx->hw_frames_ctx)) {
return nullptr;
}
encode_device_final->apply_colorspace();
auto session = std::make_unique<avcodec_encode_session_t>(
std::move(ctx),
std::move(encode_device_final),
// 0 ==> don't inject, 1 ==> inject for h264, 2 ==> inject for hevc
config.videoFormat <= 1 ? (1 - (int) video_format[encoder_t::VUI_PARAMETERS]) * (1 + config.videoFormat) : 0);
return session;
}
std::unique_ptr<nvenc_encode_session_t>
make_nvenc_encode_session(const config_t &client_config, std::unique_ptr<platf::nvenc_encode_device_t> encode_device) {
if (!encode_device->init_encoder(client_config, encode_device->colorspace)) {
return nullptr;
}
return std::make_unique<nvenc_encode_session_t>(std::move(encode_device));
}
std::unique_ptr<encode_session_t>
make_encode_session(platf::display_t *disp, const encoder_t &encoder, const config_t &config, int width, int height, std::unique_ptr<platf::encode_device_t> encode_device) {
if (encode_device) {
switch (encode_device->colorspace.colorspace) {
case colorspace_e::bt2020:
BOOST_LOG(info) << "HDR color coding [Rec. 2020 + SMPTE 2084 PQ]"sv;
break;
case colorspace_e::rec601:
BOOST_LOG(info) << "SDR color coding [Rec. 601]"sv;
break;
case colorspace_e::rec709:
BOOST_LOG(info) << "SDR color coding [Rec. 709]"sv;
break;
case colorspace_e::bt2020sdr:
BOOST_LOG(info) << "SDR color coding [Rec. 2020]"sv;
break;
}
BOOST_LOG(info) << "Color depth: " << encode_device->colorspace.bit_depth << "-bit";
BOOST_LOG(info) << "Color range: ["sv << (encode_device->colorspace.full_range ? "JPEG"sv : "MPEG"sv) << ']';
}
if (dynamic_cast<platf::avcodec_encode_device_t *>(encode_device.get())) {
auto avcodec_encode_device = boost::dynamic_pointer_cast<platf::avcodec_encode_device_t>(std::move(encode_device));
return make_avcodec_encode_session(disp, encoder, config, width, height, std::move(avcodec_encode_device));
}
else if (dynamic_cast<platf::nvenc_encode_device_t *>(encode_device.get())) {
auto nvenc_encode_device = boost::dynamic_pointer_cast<platf::nvenc_encode_device_t>(std::move(encode_device));
return make_nvenc_encode_session(config, std::move(nvenc_encode_device));
}
return nullptr;
}
void
encode_run(
int &frame_nr, // Store progress of the frame number
safe::mail_t mail,
img_event_t images,
config_t config,
std::shared_ptr<platf::display_t> disp,
std::unique_ptr<platf::encode_device_t> encode_device,
safe::signal_t &reinit_event,
const encoder_t &encoder,
void *channel_data) {
auto session = make_encode_session(disp.get(), encoder, config, disp->width, disp->height, std::move(encode_device));
if (!session) {
return;
}
auto shutdown_event = mail->event<bool>(mail::shutdown);
auto packets = mail::man->queue<packet_t>(mail::video_packets);
auto idr_events = mail->event<bool>(mail::idr);
auto invalidate_ref_frames_events = mail->event<std::pair<int64_t, int64_t>>(mail::invalidate_ref_frames);
{
// Load a dummy image into the AVFrame to ensure we have something to encode
// even if we timeout waiting on the first frame. This is a relatively large
// allocation which can be freed immediately after convert(), so we do this
// in a separate scope.
auto dummy_img = disp->alloc_img();
if (!dummy_img || disp->dummy_img(dummy_img.get()) || session->convert(*dummy_img)) {
return;
}
}
while (true) {
if (shutdown_event->peek() || reinit_event.peek() || !images->running()) {
break;
}
bool requested_idr_frame = false;
while (invalidate_ref_frames_events->peek()) {
if (auto frames = invalidate_ref_frames_events->pop(0ms)) {
session->invalidate_ref_frames(frames->first, frames->second);
}
}
if (idr_events->peek()) {
requested_idr_frame = true;
idr_events->pop();
}
if (requested_idr_frame) {
session->request_idr_frame();
}
std::optional<std::chrono::steady_clock::time_point> frame_timestamp;
// Encode at a minimum of 10 FPS to avoid image quality issues with static content
if (!requested_idr_frame || images->peek()) {
if (auto img = images->pop(100ms)) {
frame_timestamp = img->frame_timestamp;
if (session->convert(*img)) {
BOOST_LOG(error) << "Could not convert image"sv;
return;
}
}
else if (!images->running()) {
break;
}
}
if (encode(frame_nr++, *session, packets, channel_data, frame_timestamp)) {
BOOST_LOG(error) << "Could not encode video packet"sv;
return;
}
session->request_normal_frame();
}
}
input::touch_port_t
make_port(platf::display_t *display, const config_t &config) {
float wd = display->width;
float hd = display->height;
float wt = config.width;
float ht = config.height;
auto scalar = std::fminf(wt / wd, ht / hd);
auto w2 = scalar * wd;
auto h2 = scalar * hd;
auto offsetX = (config.width - w2) * 0.5f;
auto offsetY = (config.height - h2) * 0.5f;
return input::touch_port_t {
{
display->offset_x,
display->offset_y,
config.width,
config.height,
},
display->env_width,
display->env_height,
offsetX,
offsetY,
1.0f / scalar,
};
}
std::unique_ptr<platf::encode_device_t>
make_encode_device(platf::display_t &disp, const encoder_t &encoder, const config_t &config) {
std::unique_ptr<platf::encode_device_t> result;
auto colorspace = colorspace_from_client_config(config, disp.is_hdr());
auto pix_fmt = (colorspace.bit_depth == 10) ? encoder.platform_formats->pix_fmt_10bit : encoder.platform_formats->pix_fmt_8bit;
if (dynamic_cast<const encoder_platform_formats_avcodec *>(encoder.platform_formats.get())) {
result = disp.make_avcodec_encode_device(pix_fmt);
}
else if (dynamic_cast<const encoder_platform_formats_nvenc *>(encoder.platform_formats.get())) {
result = disp.make_nvenc_encode_device(pix_fmt);
}
if (result) {
result->colorspace = colorspace;
}
return result;
}
std::optional<sync_session_t>
make_synced_session(platf::display_t *disp, const encoder_t &encoder, platf::img_t &img, sync_session_ctx_t &ctx) {
sync_session_t encode_session;
encode_session.ctx = &ctx;
auto encode_device = make_encode_device(*disp, encoder, ctx.config);
if (!encode_device) {
return std::nullopt;
}
// absolute mouse coordinates require that the dimensions of the screen are known
ctx.touch_port_events->raise(make_port(disp, ctx.config));
// Update client with our current HDR display state
hdr_info_t hdr_info = std::make_unique<hdr_info_raw_t>(false);
if (colorspace_is_hdr(encode_device->colorspace)) {
if (disp->get_hdr_metadata(hdr_info->metadata)) {
hdr_info->enabled = true;
}
else {
BOOST_LOG(error) << "Couldn't get display hdr metadata when colorspace selection indicates it should have one";
}
}
ctx.hdr_events->raise(std::move(hdr_info));
auto session = make_encode_session(disp, encoder, ctx.config, img.width, img.height, std::move(encode_device));
if (!session) {
return std::nullopt;
}
// Load the initial image to prepare for encoding
if (session->convert(img)) {
BOOST_LOG(error) << "Could not convert initial image"sv;
return std::nullopt;
}
encode_session.session = std::move(session);
return encode_session;
}
encode_e
encode_run_sync(
std::vector<std::unique_ptr<sync_session_ctx_t>> &synced_session_ctxs,
encode_session_ctx_queue_t &encode_session_ctx_queue) {
const auto &encoder = *chosen_encoder;
auto display_names = platf::display_names(encoder.platform_formats->dev_type);
int display_p = 0;
if (display_names.empty()) {
display_names.emplace_back(config::video.output_name);
}
for (int x = 0; x < display_names.size(); ++x) {
if (display_names[x] == config::video.output_name) {
display_p = x;
break;
}
}
std::shared_ptr<platf::display_t> disp;
auto switch_display_event = mail::man->event<int>(mail::switch_display);
if (synced_session_ctxs.empty()) {
auto ctx = encode_session_ctx_queue.pop();
if (!ctx) {
return encode_e::ok;
}
synced_session_ctxs.emplace_back(std::make_unique<sync_session_ctx_t>(std::move(*ctx)));
}
while (encode_session_ctx_queue.running()) {
// reset_display() will sleep between retries
reset_display(disp, encoder.platform_formats->dev_type, display_names[display_p], synced_session_ctxs.front()->config);
if (disp) {
break;
}
}
if (!disp) {
return encode_e::error;
}
auto img = disp->alloc_img();
if (!img || disp->dummy_img(img.get())) {
return encode_e::error;
}
std::vector<sync_session_t> synced_sessions;
for (auto &ctx : synced_session_ctxs) {
auto synced_session = make_synced_session(disp.get(), encoder, *img, *ctx);
if (!synced_session) {
return encode_e::error;
}
synced_sessions.emplace_back(std::move(*synced_session));
}
auto ec = platf::capture_e::ok;
while (encode_session_ctx_queue.running()) {
auto push_captured_image_callback = [&](std::shared_ptr<platf::img_t> &&img, bool frame_captured) -> bool {
while (encode_session_ctx_queue.peek()) {
auto encode_session_ctx = encode_session_ctx_queue.pop();
if (!encode_session_ctx) {
return false;
}
synced_session_ctxs.emplace_back(std::make_unique<sync_session_ctx_t>(std::move(*encode_session_ctx)));
auto encode_session = make_synced_session(disp.get(), encoder, *img, *synced_session_ctxs.back());
if (!encode_session) {
ec = platf::capture_e::error;
return false;
}
synced_sessions.emplace_back(std::move(*encode_session));
}
KITTY_WHILE_LOOP(auto pos = std::begin(synced_sessions), pos != std::end(synced_sessions), {
auto ctx = pos->ctx;
if (ctx->shutdown_event->peek()) {
// Let waiting thread know it can delete shutdown_event
ctx->join_event->raise(true);
pos = synced_sessions.erase(pos);
synced_session_ctxs.erase(std::find_if(std::begin(synced_session_ctxs), std::end(synced_session_ctxs), [&ctx_p = ctx](auto &ctx) {
return ctx.get() == ctx_p;
}));
if (synced_sessions.empty()) {
return false;
}
continue;
}
if (ctx->idr_events->peek()) {
pos->session->request_idr_frame();
ctx->idr_events->pop();
}
if (frame_captured && pos->session->convert(*img)) {
BOOST_LOG(error) << "Could not convert image"sv;
ctx->shutdown_event->raise(true);
continue;
}
std::optional<std::chrono::steady_clock::time_point> frame_timestamp;
if (img) {
frame_timestamp = img->frame_timestamp;
}
if (encode(ctx->frame_nr++, *pos->session, ctx->packets, ctx->channel_data, frame_timestamp)) {
BOOST_LOG(error) << "Could not encode video packet"sv;
ctx->shutdown_event->raise(true);
continue;
}
pos->session->request_normal_frame();
++pos;
})
if (switch_display_event->peek()) {
ec = platf::capture_e::reinit;
display_p = std::clamp(*switch_display_event->pop(), 0, (int) display_names.size() - 1);
return false;
}
return true;
};
auto pull_free_image_callback = [&img](std::shared_ptr<platf::img_t> &img_out) -> bool {
img_out = img;
img_out->frame_timestamp.reset();
return true;
};
auto status = disp->capture(push_captured_image_callback, pull_free_image_callback, &display_cursor);
switch (status) {
case platf::capture_e::reinit:
case platf::capture_e::error:
case platf::capture_e::ok:
case platf::capture_e::timeout:
case platf::capture_e::interrupted:
return ec != platf::capture_e::ok ? ec : status;
}
}
return encode_e::ok;
}
void
captureThreadSync() {
auto ref = capture_thread_sync.ref();
std::vector<std::unique_ptr<sync_session_ctx_t>> synced_session_ctxs;
auto &ctx = ref->encode_session_ctx_queue;
auto lg = util::fail_guard([&]() {
ctx.stop();
for (auto &ctx : synced_session_ctxs) {
ctx->shutdown_event->raise(true);
ctx->join_event->raise(true);
}
for (auto &ctx : ctx.unsafe()) {
ctx.shutdown_event->raise(true);
ctx.join_event->raise(true);
}
});
// Encoding and capture takes place on this thread
platf::adjust_thread_priority(platf::thread_priority_e::high);
while (encode_run_sync(synced_session_ctxs, ctx) == encode_e::reinit) {}
}
void
capture_async(
safe::mail_t mail,
config_t &config,
void *channel_data) {
auto shutdown_event = mail->event<bool>(mail::shutdown);
auto images = std::make_shared<img_event_t::element_type>();
auto lg = util::fail_guard([&]() {
images->stop();
shutdown_event->raise(true);
});
auto ref = capture_thread_async.ref();
if (!ref) {
return;
}
ref->capture_ctx_queue->raise(capture_ctx_t { images, config });
if (!ref->capture_ctx_queue->running()) {
return;
}
int frame_nr = 1;
auto touch_port_event = mail->event<input::touch_port_t>(mail::touch_port);
auto hdr_event = mail->event<hdr_info_t>(mail::hdr);
// Encoding takes place on this thread
platf::adjust_thread_priority(platf::thread_priority_e::high);
while (!shutdown_event->peek() && images->running()) {
// Wait for the main capture event when the display is being reinitialized
if (ref->reinit_event.peek()) {
std::this_thread::sleep_for(20ms);
continue;
}
// Wait for the display to be ready
std::shared_ptr<platf::display_t> display;
{
auto lg = ref->display_wp.lock();
if (ref->display_wp->expired()) {
continue;
}
display = ref->display_wp->lock();
}
auto &encoder = *chosen_encoder;
auto encode_device = make_encode_device(*display, encoder, config);
if (!encode_device) {
return;
}
// absolute mouse coordinates require that the dimensions of the screen are known
touch_port_event->raise(make_port(display.get(), config));
// Update client with our current HDR display state
hdr_info_t hdr_info = std::make_unique<hdr_info_raw_t>(false);
if (colorspace_is_hdr(encode_device->colorspace)) {
if (display->get_hdr_metadata(hdr_info->metadata)) {
hdr_info->enabled = true;
}
else {
BOOST_LOG(error) << "Couldn't get display hdr metadata when colorspace selection indicates it should have one";
}
}
hdr_event->raise(std::move(hdr_info));
encode_run(
frame_nr,
mail, images,
config, display,
std::move(encode_device),
ref->reinit_event, *ref->encoder_p,
channel_data);
}
}
void
capture(
safe::mail_t mail,
config_t config,
void *channel_data) {
auto idr_events = mail->event<bool>(mail::idr);
idr_events->raise(true);
if (chosen_encoder->flags & PARALLEL_ENCODING) {
capture_async(std::move(mail), config, channel_data);
}
else {
safe::signal_t join_event;
auto ref = capture_thread_sync.ref();
ref->encode_session_ctx_queue.raise(sync_session_ctx_t {
&join_event,
mail->event<bool>(mail::shutdown),
mail::man->queue<packet_t>(mail::video_packets),
std::move(idr_events),
mail->event<hdr_info_t>(mail::hdr),
mail->event<input::touch_port_t>(mail::touch_port),
config,
1,
channel_data,
});
// Wait for join signal
join_event.view();
}
}
enum validate_flag_e {
VUI_PARAMS = 0x01,
};
int
validate_config(std::shared_ptr<platf::display_t> &disp, const encoder_t &encoder, const config_t &config) {
reset_display(disp, encoder.platform_formats->dev_type, config::video.output_name, config);
if (!disp) {
return -1;
}
auto encode_device = make_encode_device(*disp, encoder, config);
if (!encode_device) {
return -1;
}
auto session = make_encode_session(disp.get(), encoder, config, disp->width, disp->height, std::move(encode_device));
if (!session) {
return -1;
}
{
// Image buffers are large, so we use a separate scope to free it immediately after convert()
auto img = disp->alloc_img();
if (!img || disp->dummy_img(img.get()) || session->convert(*img)) {
return -1;
}
}
session->request_idr_frame();
auto packets = mail::man->queue<packet_t>(mail::video_packets);
while (!packets->peek()) {
if (encode(1, *session, packets, nullptr, {})) {
return -1;
}
}
auto packet = packets->pop();
if (!packet->is_idr()) {
BOOST_LOG(error) << "First packet type is not an IDR frame"sv;
return -1;
}
int flag = 0;
// This check only applies for H.264 and HEVC
if (config.videoFormat <= 1) {
if (auto packet_avcodec = dynamic_cast<packet_raw_avcodec *>(packet.get())) {
if (cbs::validate_sps(packet_avcodec->av_packet, config.videoFormat ? AV_CODEC_ID_H265 : AV_CODEC_ID_H264)) {
flag |= VUI_PARAMS;
}
}
else {
// Don't check it for non-avcodec encoders.
flag |= VUI_PARAMS;
}
}
return flag;
}
bool
validate_encoder(encoder_t &encoder, bool expect_failure) {
std::shared_ptr<platf::display_t> disp;
BOOST_LOG(info) << "Trying encoder ["sv << encoder.name << ']';
auto fg = util::fail_guard([&]() {
BOOST_LOG(info) << "Encoder ["sv << encoder.name << "] failed"sv;
});
auto test_hevc = active_hevc_mode >= 2 || (active_hevc_mode == 0 && !(encoder.flags & H264_ONLY));
auto test_av1 = active_av1_mode >= 2 || (active_av1_mode == 0 && !(encoder.flags & H264_ONLY));
encoder.h264.capabilities.set();
encoder.hevc.capabilities.set();
encoder.av1.capabilities.set();
// First, test encoder viability
config_t config_max_ref_frames { 1920, 1080, 60, 1000, 1, 1, 1, 0, 0 };
config_t config_autoselect { 1920, 1080, 60, 1000, 1, 0, 1, 0, 0 };
// If the encoder isn't supported at all (not even H.264), bail early
reset_display(disp, encoder.platform_formats->dev_type, config::video.output_name, config_autoselect);
if (!disp) {
return false;
}
if (!disp->is_codec_supported(encoder.h264.name, config_autoselect)) {
fg.disable();
BOOST_LOG(info) << "Encoder ["sv << encoder.name << "] is not supported on this GPU"sv;
return false;
}
retry:
// If we're expecting failure, use the autoselect ref config first since that will always succeed
// if the encoder is available.
auto max_ref_frames_h264 = expect_failure ? -1 : validate_config(disp, encoder, config_max_ref_frames);
auto autoselect_h264 = max_ref_frames_h264 >= 0 ? max_ref_frames_h264 : validate_config(disp, encoder, config_autoselect);
if (autoselect_h264 < 0) {
if (encoder.h264.qp && encoder.h264[encoder_t::CBR]) {
// It's possible the encoder isn't accepting Constant Bit Rate. Turn off CBR and make another attempt
encoder.h264.capabilities.set();
encoder.h264[encoder_t::CBR] = false;
goto retry;
}
return false;
}
else if (expect_failure) {
// We expected failure, but actually succeeded. Do the max_ref_frames probe we skipped.
max_ref_frames_h264 = validate_config(disp, encoder, config_max_ref_frames);
}
std::vector<std::pair<validate_flag_e, encoder_t::flag_e>> packet_deficiencies {
{ VUI_PARAMS, encoder_t::VUI_PARAMETERS },
};
for (auto [validate_flag, encoder_flag] : packet_deficiencies) {
encoder.h264[encoder_flag] = (max_ref_frames_h264 & validate_flag && autoselect_h264 & validate_flag);
}
encoder.h264[encoder_t::REF_FRAMES_RESTRICT] = max_ref_frames_h264 >= 0;
encoder.h264[encoder_t::PASSED] = true;
if (test_hevc) {
config_max_ref_frames.videoFormat = 1;
config_autoselect.videoFormat = 1;
if (disp->is_codec_supported(encoder.hevc.name, config_autoselect)) {
retry_hevc:
auto max_ref_frames_hevc = validate_config(disp, encoder, config_max_ref_frames);
auto autoselect_hevc = max_ref_frames_hevc >= 0 ? max_ref_frames_hevc : validate_config(disp, encoder, config_autoselect);
if (autoselect_hevc < 0 && encoder.hevc.qp && encoder.hevc[encoder_t::CBR]) {
// It's possible the encoder isn't accepting Constant Bit Rate. Turn off CBR and make another attempt
encoder.hevc.capabilities.set();
encoder.hevc[encoder_t::CBR] = false;
goto retry_hevc;
}
for (auto [validate_flag, encoder_flag] : packet_deficiencies) {
encoder.hevc[encoder_flag] = (max_ref_frames_hevc & validate_flag && autoselect_hevc & validate_flag);
}
encoder.hevc[encoder_t::REF_FRAMES_RESTRICT] = max_ref_frames_hevc >= 0;
encoder.hevc[encoder_t::PASSED] = max_ref_frames_hevc >= 0 || autoselect_hevc >= 0;
}
else {
BOOST_LOG(info) << "Encoder ["sv << encoder.hevc.name << "] is not supported on this GPU"sv;
encoder.hevc.capabilities.reset();
}
}
else {
// Clear all cap bits for HEVC if we didn't probe it
encoder.hevc.capabilities.reset();
}
if (test_av1) {
config_max_ref_frames.videoFormat = 2;
config_autoselect.videoFormat = 2;
if (disp->is_codec_supported(encoder.av1.name, config_autoselect)) {
retry_av1:
auto max_ref_frames_av1 = validate_config(disp, encoder, config_max_ref_frames);
auto autoselect_av1 = max_ref_frames_av1 >= 0 ? max_ref_frames_av1 : validate_config(disp, encoder, config_autoselect);
if (autoselect_av1 < 0 && encoder.av1.qp && encoder.av1[encoder_t::CBR]) {
// It's possible the encoder isn't accepting Constant Bit Rate. Turn off CBR and make another attempt
encoder.av1.capabilities.set();
encoder.av1[encoder_t::CBR] = false;
goto retry_av1;
}
for (auto [validate_flag, encoder_flag] : packet_deficiencies) {
encoder.av1[encoder_flag] = (max_ref_frames_av1 & validate_flag && autoselect_av1 & validate_flag);
}
encoder.av1[encoder_t::REF_FRAMES_RESTRICT] = max_ref_frames_av1 >= 0;
encoder.av1[encoder_t::PASSED] = max_ref_frames_av1 >= 0 || autoselect_av1 >= 0;
}
else {
BOOST_LOG(info) << "Encoder ["sv << encoder.av1.name << "] is not supported on this GPU"sv;
encoder.av1.capabilities.reset();
}
}
else {
// Clear all cap bits for AV1 if we didn't probe it
encoder.av1.capabilities.reset();
}
std::vector<std::pair<encoder_t::flag_e, config_t>> configs {
{ encoder_t::DYNAMIC_RANGE, { 1920, 1080, 60, 1000, 1, 0, 3, 1, 1 } },
};
for (auto &[flag, config] : configs) {
auto h264 = config;
auto hevc = config;
auto av1 = config;
h264.videoFormat = 0;
hevc.videoFormat = 1;
av1.videoFormat = 2;
// HDR is not supported with H.264. Don't bother even trying it.
encoder.h264[flag] = flag != encoder_t::DYNAMIC_RANGE && validate_config(disp, encoder, h264) >= 0;
if (encoder.hevc[encoder_t::PASSED]) {
encoder.hevc[flag] = validate_config(disp, encoder, hevc) >= 0;
}
if (encoder.av1[encoder_t::PASSED]) {
encoder.av1[flag] = validate_config(disp, encoder, av1) >= 0;
}
}
encoder.h264[encoder_t::VUI_PARAMETERS] = encoder.h264[encoder_t::VUI_PARAMETERS] && !config::sunshine.flags[config::flag::FORCE_VIDEO_HEADER_REPLACE];
encoder.hevc[encoder_t::VUI_PARAMETERS] = encoder.hevc[encoder_t::VUI_PARAMETERS] && !config::sunshine.flags[config::flag::FORCE_VIDEO_HEADER_REPLACE];
if (!encoder.h264[encoder_t::VUI_PARAMETERS]) {
BOOST_LOG(warning) << encoder.name << ": h264 missing sps->vui parameters"sv;
}
if (encoder.hevc[encoder_t::PASSED] && !encoder.hevc[encoder_t::VUI_PARAMETERS]) {
BOOST_LOG(warning) << encoder.name << ": hevc missing sps->vui parameters"sv;
}
fg.disable();
return true;
}
/**
* This is called once at startup and each time a stream is launched to
* ensure the best encoder is selected. Encoder availability can change
* at runtime due to all sorts of things from driver updates to eGPUs.
*
* This is only safe to call when there is no client actively streaming.
*/
int
probe_encoders() {
auto encoder_list = encoders;
// Restart encoder selection
auto previous_encoder = chosen_encoder;
chosen_encoder = nullptr;
active_hevc_mode = config::video.hevc_mode;
active_av1_mode = config::video.av1_mode;
last_encoder_probe_supported_ref_frames_invalidation = false;
auto adjust_encoder_constraints = [&](encoder_t *encoder) {
// If we can't satisfy both the encoder and codec requirement, prefer the encoder over codec support
if (active_hevc_mode == 3 && !encoder->hevc[encoder_t::DYNAMIC_RANGE]) {
BOOST_LOG(warning) << "Encoder ["sv << encoder->name << "] does not support HEVC Main10 on this system"sv;
active_hevc_mode = 0;
}
else if (active_hevc_mode == 2 && !encoder->hevc[encoder_t::PASSED]) {
BOOST_LOG(warning) << "Encoder ["sv << encoder->name << "] does not support HEVC on this system"sv;
active_hevc_mode = 0;
}
if (active_av1_mode == 3 && !encoder->av1[encoder_t::DYNAMIC_RANGE]) {
BOOST_LOG(warning) << "Encoder ["sv << encoder->name << "] does not support AV1 Main10 on this system"sv;
active_av1_mode = 0;
}
else if (active_av1_mode == 2 && !encoder->av1[encoder_t::PASSED]) {
BOOST_LOG(warning) << "Encoder ["sv << encoder->name << "] does not support AV1 on this system"sv;
active_av1_mode = 0;
}
};
if (!config::video.encoder.empty()) {
// If there is a specific encoder specified, use it if it passes validation
KITTY_WHILE_LOOP(auto pos = std::begin(encoder_list), pos != std::end(encoder_list), {
auto encoder = *pos;
if (encoder->name == config::video.encoder) {
// Remove the encoder from the list entirely if it fails validation
if (!validate_encoder(*encoder, previous_encoder && previous_encoder != encoder)) {
pos = encoder_list.erase(pos);
break;
}
// We will return an encoder here even if it fails one of the codec requirements specified by the user
adjust_encoder_constraints(encoder);
chosen_encoder = encoder;
break;
}
pos++;
});
if (chosen_encoder == nullptr) {
BOOST_LOG(error) << "Couldn't find any working encoder matching ["sv << config::video.encoder << ']';
}
}
BOOST_LOG(info) << "// Testing for available encoders, this may generate errors. You can safely ignore those errors. //"sv;
// If we haven't found an encoder yet, but we want one with specific codec support, search for that now.
if (chosen_encoder == nullptr && (active_hevc_mode >= 2 || active_av1_mode >= 2)) {
KITTY_WHILE_LOOP(auto pos = std::begin(encoder_list), pos != std::end(encoder_list), {
auto encoder = *pos;
// Remove the encoder from the list entirely if it fails validation
if (!validate_encoder(*encoder, previous_encoder && previous_encoder != encoder)) {
pos = encoder_list.erase(pos);
continue;
}
// Skip it if it doesn't support the specified codec at all
if ((active_hevc_mode >= 2 && !encoder->hevc[encoder_t::PASSED]) ||
(active_av1_mode >= 2 && !encoder->av1[encoder_t::PASSED])) {
pos++;
continue;
}
// Skip it if it doesn't support HDR on the specified codec
if ((active_hevc_mode == 3 && !encoder->hevc[encoder_t::DYNAMIC_RANGE]) ||
(active_av1_mode == 3 && !encoder->av1[encoder_t::DYNAMIC_RANGE])) {
pos++;
continue;
}
chosen_encoder = encoder;
break;
});
if (chosen_encoder == nullptr) {
BOOST_LOG(error) << "Couldn't find any working encoder that meets HEVC/AV1 requirements"sv;
}
}
// If no encoder was specified or the specified encoder was unusable, keep trying
// the remaining encoders until we find one that passes validation.
if (chosen_encoder == nullptr) {
KITTY_WHILE_LOOP(auto pos = std::begin(encoder_list), pos != std::end(encoder_list), {
auto encoder = *pos;
// If we've used a previous encoder and it's not this one, we expect this encoder to
// fail to validate. It will use a slightly different order of checks to more quickly
// eliminate failing encoders.
if (!validate_encoder(*encoder, previous_encoder && previous_encoder != encoder)) {
pos = encoder_list.erase(pos);
continue;
}
// We will return an encoder here even if it fails one of the codec requirements specified by the user
adjust_encoder_constraints(encoder);
chosen_encoder = encoder;
break;
});
}
if (chosen_encoder == nullptr) {
BOOST_LOG(fatal) << "Couldn't find any working encoder"sv;
return -1;
}
BOOST_LOG(info);
BOOST_LOG(info) << "// Ignore any errors mentioned above, they are not relevant. //"sv;
BOOST_LOG(info);
auto &encoder = *chosen_encoder;
last_encoder_probe_supported_ref_frames_invalidation = (encoder.flags & REF_FRAMES_INVALIDATION);
BOOST_LOG(debug) << "------ h264 ------"sv;
for (int x = 0; x < encoder_t::MAX_FLAGS; ++x) {
auto flag = (encoder_t::flag_e) x;
BOOST_LOG(debug) << encoder_t::from_flag(flag) << (encoder.h264[flag] ? ": supported"sv : ": unsupported"sv);
}
BOOST_LOG(debug) << "-------------------"sv;
BOOST_LOG(info) << "Found H.264 encoder: "sv << encoder.h264.name << " ["sv << encoder.name << ']';
if (encoder.hevc[encoder_t::PASSED]) {
BOOST_LOG(debug) << "------ hevc ------"sv;
for (int x = 0; x < encoder_t::MAX_FLAGS; ++x) {
auto flag = (encoder_t::flag_e) x;
BOOST_LOG(debug) << encoder_t::from_flag(flag) << (encoder.hevc[flag] ? ": supported"sv : ": unsupported"sv);
}
BOOST_LOG(debug) << "-------------------"sv;
BOOST_LOG(info) << "Found HEVC encoder: "sv << encoder.hevc.name << " ["sv << encoder.name << ']';
}
if (encoder.av1[encoder_t::PASSED]) {
BOOST_LOG(debug) << "------ av1 ------"sv;
for (int x = 0; x < encoder_t::MAX_FLAGS; ++x) {
auto flag = (encoder_t::flag_e) x;
BOOST_LOG(debug) << encoder_t::from_flag(flag) << (encoder.av1[flag] ? ": supported"sv : ": unsupported"sv);
}
BOOST_LOG(debug) << "-------------------"sv;
BOOST_LOG(info) << "Found AV1 encoder: "sv << encoder.av1.name << " ["sv << encoder.name << ']';
}
if (active_hevc_mode == 0) {
active_hevc_mode = encoder.hevc[encoder_t::PASSED] ? (encoder.hevc[encoder_t::DYNAMIC_RANGE] ? 3 : 2) : 1;
}
if (active_av1_mode == 0) {
active_av1_mode = encoder.av1[encoder_t::PASSED] ? (encoder.av1[encoder_t::DYNAMIC_RANGE] ? 3 : 2) : 1;
}
return 0;
}
// Linux only declaration
typedef int (*vaapi_init_avcodec_hardware_input_buffer_fn)(platf::avcodec_encode_device_t *encode_device, AVBufferRef **hw_device_buf);
util::Either<avcodec_buffer_t, int>
vaapi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device) {
avcodec_buffer_t hw_device_buf;
// If an egl hwdevice
if (encode_device->data) {
if (((vaapi_init_avcodec_hardware_input_buffer_fn) encode_device->data)(encode_device, &hw_device_buf)) {
return -1;
}
return hw_device_buf;
}
auto render_device = config::video.adapter_name.empty() ? nullptr : config::video.adapter_name.c_str();
auto status = av_hwdevice_ctx_create(&hw_device_buf, AV_HWDEVICE_TYPE_VAAPI, render_device, nullptr, 0);
if (status < 0) {
char string[AV_ERROR_MAX_STRING_SIZE];
BOOST_LOG(error) << "Failed to create a VAAPI device: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status);
return -1;
}
return hw_device_buf;
}
util::Either<avcodec_buffer_t, int>
cuda_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device) {
avcodec_buffer_t hw_device_buf;
auto status = av_hwdevice_ctx_create(&hw_device_buf, AV_HWDEVICE_TYPE_CUDA, nullptr, nullptr, 1 /* AV_CUDA_USE_PRIMARY_CONTEXT */);
if (status < 0) {
char string[AV_ERROR_MAX_STRING_SIZE];
BOOST_LOG(error) << "Failed to create a CUDA device: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status);
return -1;
}
return hw_device_buf;
}
util::Either<avcodec_buffer_t, int>
vt_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device) {
avcodec_buffer_t hw_device_buf;
auto status = av_hwdevice_ctx_create(&hw_device_buf, AV_HWDEVICE_TYPE_VIDEOTOOLBOX, nullptr, nullptr, 0);
if (status < 0) {
char string[AV_ERROR_MAX_STRING_SIZE];
BOOST_LOG(error) << "Failed to create a VideoToolbox device: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status);
return -1;
}
return hw_device_buf;
}
#ifdef _WIN32
}
void
do_nothing(void *) {}
namespace video {
util::Either<avcodec_buffer_t, int>
dxgi_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *encode_device) {
avcodec_buffer_t ctx_buf { av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_D3D11VA) };
auto ctx = (AVD3D11VADeviceContext *) ((AVHWDeviceContext *) ctx_buf->data)->hwctx;
std::fill_n((std::uint8_t *) ctx, sizeof(AVD3D11VADeviceContext), 0);
auto device = (ID3D11Device *) encode_device->data;
device->AddRef();
ctx->device = device;
ctx->lock_ctx = (void *) 1;
ctx->lock = do_nothing;
ctx->unlock = do_nothing;
auto err = av_hwdevice_ctx_init(ctx_buf.get());
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 ctx_buf;
}
#endif
int
start_capture_async(capture_thread_async_ctx_t &capture_thread_ctx) {
capture_thread_ctx.encoder_p = chosen_encoder;
capture_thread_ctx.reinit_event.reset();
capture_thread_ctx.capture_ctx_queue = std::make_shared<safe::queue_t<capture_ctx_t>>(30);
capture_thread_ctx.capture_thread = std::thread {
captureThread,
capture_thread_ctx.capture_ctx_queue,
std::ref(capture_thread_ctx.display_wp),
std::ref(capture_thread_ctx.reinit_event),
std::ref(*capture_thread_ctx.encoder_p)
};
return 0;
}
void
end_capture_async(capture_thread_async_ctx_t &capture_thread_ctx) {
capture_thread_ctx.capture_ctx_queue->stop();
capture_thread_ctx.capture_thread.join();
}
int
start_capture_sync(capture_thread_sync_ctx_t &ctx) {
std::thread { &captureThreadSync }.detach();
return 0;
}
void
end_capture_sync(capture_thread_sync_ctx_t &ctx) {}
platf::mem_type_e
map_base_dev_type(AVHWDeviceType type) {
switch (type) {
case AV_HWDEVICE_TYPE_D3D11VA:
return platf::mem_type_e::dxgi;
case AV_HWDEVICE_TYPE_VAAPI:
return platf::mem_type_e::vaapi;
case AV_HWDEVICE_TYPE_CUDA:
return platf::mem_type_e::cuda;
case AV_HWDEVICE_TYPE_NONE:
return platf::mem_type_e::system;
case AV_HWDEVICE_TYPE_VIDEOTOOLBOX:
return platf::mem_type_e::videotoolbox;
default:
return platf::mem_type_e::unknown;
}
return platf::mem_type_e::unknown;
}
platf::pix_fmt_e
map_pix_fmt(AVPixelFormat fmt) {
switch (fmt) {
case AV_PIX_FMT_YUV420P10:
return platf::pix_fmt_e::yuv420p10;
case AV_PIX_FMT_YUV420P:
return platf::pix_fmt_e::yuv420p;
case AV_PIX_FMT_NV12:
return platf::pix_fmt_e::nv12;
case AV_PIX_FMT_P010:
return platf::pix_fmt_e::p010;
default:
return platf::pix_fmt_e::unknown;
}
return platf::pix_fmt_e::unknown;
}
} // namespace video