server/src/video.cpp
Rama main · réplica de lectura del repositorio original.
/**
* @file src/video.cpp
* @brief Definitions for video.
*/
// standard includes
#include <algorithm>
#include <array>
#include <atomic>
#include <bitset>
#include <cctype>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <future>
#include <list>
#include <limits>
#include <map>
#include <mutex>
#include <optional>
#include <sstream>
#include <system_error>
#include <thread>
#include <utility>
#include <vector>
// lib includes
#include <boost/algorithm/string/predicate.hpp>
#include <boost/pointer_cast.hpp>
extern "C" {
#include <libavutil/imgutils.h>
#include <libavutil/mastering_display_metadata.h>
#include <libavutil/opt.h>
#include <libavutil/pixdesc.h>
}
// local includes
#include "cbs.h"
#include "config.h"
#include "display_device.h"
#include "amf/amf_encoder.h"
#include "amf/amf_lifecycle.h"
#include "globals.h"
#include "input.h"
#include "logging.h"
#include "nvenc/nvenc_base.h"
#include "platform/common.h"
#include "process.h"
#include "pyrowave_host.h"
#include "sync.h"
#include "video.h"
#include "video_encoder_probe_policy.h"
#include "video_policy.h"
#include "webrtc_stream.h"
#if defined(__linux__) && defined(SUNSHINE_BUILD_CUDA)
#include "platform/linux/cuda.h"
#endif
#ifdef __linux__
#include "platform/linux/rtx_hdr_runtime.h"
#endif
#ifdef _WIN32
#include "src/platform/windows/display.h"
#include "src/platform/windows/display_helper_integration.h"
#include "src/platform/windows/display_vram.h"
#include "src/platform/windows/misc.h"
#include "src/platform/windows/rtx_hdr_runtime.h"
#include "src/platform/windows/virtual_display.h"
#include "uuid.h"
#include <AMF/core/Context.h>
#include <AMF/core/Debug.h>
#include <AMF/core/Factory.h>
#include <AMF/core/Trace.h>
// These using-declarations must sit between the AMF headers above and
// hwcontext_amf.h below, and must stay outside the extern "C" block. hwcontext_amf.h
// refers to the AMF types unqualified, but the AMF headers that define them are C++
// (namespace amf), so they cannot be pulled in with C linkage. Do not reorder.
using amf::AMFContext;
using amf::AMFFactory;
using amf::AMF_MEMORY_TYPE;
using amf::AMF_SURFACE_FORMAT;
#include <libavutil/hwcontext_amf.h>
extern "C" {
#include <libavutil/hwcontext_d3d11va.h>
}
namespace proc {
extern std::atomic<VDISPLAY::DRIVER_STATUS> vDisplayDriverStatus;
void initVDisplayDriver();
} // namespace proc
#endif
using namespace std::literals;
namespace video {
/**
* @brief Check if we can allow probing for the encoders.
* @return True if there should be no issues with the probing, false if we should prevent it.
*/
bool allow_encoder_probing() {
return true;
}
namespace {
// Serializes destruction of encode sessions (and, on Windows, the async release of shared
// D3D capture surfaces). When two clients share one capture target, a capture reinit makes
// both video threads tear down their NVENC session + D3D11 device at the same instant. Both
// devices have the same shared capture textures open, and the NVIDIA UMD's cross-device
// shared-resource dependency cleanup (DestroyDriverInstance) is not safe against a concurrent
// teardown of the other device: it faults walking freed dependency entries. Funnel all
// encoder teardown through this mutex so only one device is ever mid-destruction.
std::mutex encode_session_teardown_mutex;
// A host restart is the recovery boundary after a vendor call times out.
// Detached watchdog workers can outlive ordinary static destruction during
// process shutdown. Deliberately give the runtime fence process lifetime.
auto &native_amf_lifecycle_gate = *new amf::lifecycle::native_runtime_gate_t();
#if defined(__linux__) && defined(SUNSHINE_BUILD_CUDA)
// A failed native NVENC teardown leaves a live driver session referencing
// the quarantined CUDA/GL device. Refuse to create more native sessions in
// this process so repeated failures cannot exhaust the GPU's finite NVENC
// session capacity. FFmpeg NVENC remains a separate explicit choice.
std::atomic_bool native_nvenc_runtime_quarantined {false};
#endif
void wait_before_display_retry(std::size_t &consecutive_failures) {
std::this_thread::sleep_for(policy::display_retry_delay(consecutive_failures));
if (consecutive_failures < std::numeric_limits<std::size_t>::max()) {
++consecutive_failures;
}
}
#ifdef _WIN32
void wait_for_recent_display_apply_stability() {
constexpr auto kFallbackSettleWindow = std::chrono::milliseconds(1500);
constexpr auto kVerificationPollInterval = std::chrono::milliseconds(25);
// The helper re-applies HDR at 750/2500/5500ms after APPLY when its synchronous
// verification does not stick. Cover the 2500ms retry: past that, streaming SDR
// beats holding a black screen, and part of the point of waiting is to avoid a
// mid-stream SDR->HDR flip that costs some clients a full display mode-set.
constexpr auto kHdrReadyWindow = std::chrono::milliseconds(3000);
auto elapsed = std::chrono::milliseconds(display_helper_integration::ms_since_last_apply());
// A bounded stream-start APPLY shares one budget with the client's
// first-video deadline, so this wait has to come out of what is left of it
// rather than starting a fresh window afterwards. Windows are measured
// from APPLY completion, so the remaining budget is rebased the same way.
// Unbounded applies (WebRTC, recovery, non-stream) keep the full windows.
const auto stream_start_budget = display_helper_integration::remaining_stream_start_budget();
const auto bounded_window = [&stream_start_budget](std::chrono::milliseconds window, std::chrono::milliseconds since_apply) {
return stream_start_budget ? std::min(window, since_apply + *stream_start_budget) : window;
};
if (display_helper_integration::last_apply_requested_hdr()) {
const auto output_name = display_device::map_output_name(config::get_active_output_name());
if (platf::dxgi::is_hdr_active_for_output(output_name)) {
return;
}
if (elapsed >= kHdrReadyWindow) {
return;
}
const auto hdr_ready_window = bounded_window(kHdrReadyWindow, elapsed);
if (elapsed >= hdr_ready_window) {
BOOST_LOG(info) << "Stream-start display budget is exhausted; starting capture immediately to stay inside the client deadline.";
return;
}
BOOST_LOG(info) << "Display apply requested HDR; waiting up to "
<< (hdr_ready_window - elapsed).count()
<< "ms for the output to report HDR before starting capture";
while (elapsed < hdr_ready_window) {
std::this_thread::sleep_for(std::min(kVerificationPollInterval, hdr_ready_window - elapsed));
if (platf::dxgi::is_hdr_active_for_output(output_name)) {
BOOST_LOG(debug) << "Display output reported HDR active after "
<< display_helper_integration::ms_since_last_apply()
<< "ms; starting capture.";
return;
}
elapsed = std::chrono::milliseconds(display_helper_integration::ms_since_last_apply());
}
BOOST_LOG(warning) << "Display apply requested HDR but the output did not report HDR within "
<< hdr_ready_window.count() << "ms; starting capture in SDR.";
return;
}
if (elapsed >= kFallbackSettleWindow || display_helper_integration::last_apply_is_capture_stable()) {
return;
}
const auto settle_window = bounded_window(kFallbackSettleWindow, elapsed);
if (elapsed >= settle_window) {
BOOST_LOG(info) << "Stream-start display budget is exhausted; starting capture immediately to stay inside the client deadline.";
return;
}
BOOST_LOG(info) << "Display topology recently changed; waiting up to "
<< (settle_window - elapsed).count()
<< "ms for helper verification or display-settle fallback";
while (elapsed < settle_window) {
if (display_helper_integration::last_apply_is_capture_stable()) {
BOOST_LOG(debug) << "Display topology verification completed; ending settle wait early.";
return;
}
std::this_thread::sleep_for(std::min(kVerificationPollInterval, settle_window - elapsed));
elapsed = std::chrono::milliseconds(display_helper_integration::ms_since_last_apply());
}
}
bool should_prefer_virtual_display() {
if (platf::is_lock_screen_active() && VDISPLAY::has_active_physical_display()) {
return false;
}
if (auto runtime_output_name = config::runtime_output_name_override()) {
return !runtime_output_name->empty() && VDISPLAY::is_virtual_display_output(*runtime_output_name);
}
if (!VDISPLAY::isVirtualDisplayDriverInstalled()) {
return false;
}
auto virtual_displays = VDISPLAY::enumerateVirtualDisplays();
if (virtual_displays.empty()) {
return false;
}
const auto active_output_name = config::get_active_output_name();
const bool runtime_targets_virtual =
!active_output_name.empty() &&
VDISPLAY::is_virtual_display_output(active_output_name);
if (runtime_targets_virtual) {
return true;
}
const bool explicit_virtual = (config::video.virtual_display_mode == config::video_t::virtual_display_mode_e::per_client || config::video.virtual_display_mode == config::video_t::virtual_display_mode_e::shared);
const bool auto_activate = config::video.dd.activate_virtual_display;
if (explicit_virtual || auto_activate) {
return true;
}
const bool any_active = std::any_of(
virtual_displays.begin(),
virtual_displays.end(),
[](const VDISPLAY::VirtualDisplayInfo &info) {
return info.is_active;
}
);
if (!any_active) {
return false;
}
if (!VDISPLAY::has_active_physical_display()) {
return true;
}
return false;
}
#ifdef _WIN32
bool is_d3d_capture_image(const std::shared_ptr<platf::img_t> &img) {
return dynamic_cast<platf::dxgi::img_d3d_t *>(img.get()) != nullptr;
}
#endif
#ifdef _WIN32
void release_d3d_capture_images_async(std::vector<std::shared_ptr<platf::img_t>> images) {
if (images.empty()) {
return;
}
try {
std::thread {[images = std::move(images)]() mutable {
platf::set_thread_name("video::d3dRelease");
std::lock_guard lg {encode_session_teardown_mutex};
images.clear();
}}.detach();
} catch (const std::system_error &err) {
BOOST_LOG(warning) << "Failed to start async D3D image release thread: " << err.what();
}
}
#endif
std::optional<std::string> active_virtual_display_dxgi_name() {
auto virtual_displays = VDISPLAY::enumerateVirtualDisplays();
auto is_dxgi_display_name = [](const std::string &name) {
static const std::string prefix = "\\\\.\\DISPLAY";
if (name.size() < prefix.size()) {
return false;
}
for (size_t i = 0; i < prefix.size(); ++i) {
if (std::tolower(static_cast<unsigned char>(name[i])) != std::tolower(static_cast<unsigned char>(prefix[i]))) {
return false;
}
}
return true;
};
auto map_to_dxgi_name = [](const std::wstring &name) -> std::optional<std::string> {
if (name.empty()) {
return std::nullopt;
}
const auto mapped = display_device::map_output_name(platf::to_utf8(name));
if (mapped.empty()) {
return std::nullopt;
}
return mapped;
};
std::vector<std::string> active_virtual_outputs;
std::vector<std::string> all_virtual_outputs;
for (const auto &info : virtual_displays) {
if (auto mapped = map_to_dxgi_name(info.device_name)) {
if (is_dxgi_display_name(*mapped)) {
all_virtual_outputs.push_back(*mapped);
if (info.is_active) {
active_virtual_outputs.push_back(*mapped);
}
}
}
}
std::string configured_virtual_output;
const auto configured_output = config::get_active_output_name();
if (!configured_output.empty() && VDISPLAY::is_virtual_display_output(configured_output)) {
const auto mapped = display_device::map_output_name(configured_output);
if (is_dxgi_display_name(mapped)) {
configured_virtual_output = mapped;
}
}
return policy::select_preferred_virtual_output(
configured_virtual_output,
active_virtual_outputs,
all_virtual_outputs
);
}
#endif
bool ensure_virtual_display_ready(
std::vector<std::string> &display_names,
int &display_index,
const bool allow_process_display_preference = true
) {
#ifdef _WIN32
static thread_local std::chrono::steady_clock::time_point wait_start {};
static thread_local std::string pending_virtual_name;
if (display_names.empty()) {
display_index = 0;
wait_start = {};
pending_virtual_name.clear();
return false;
}
display_index = std::clamp(display_index, 0, static_cast<int>(display_names.size()) - 1);
if (!allow_process_display_preference) {
wait_start = {};
pending_virtual_name.clear();
return true;
}
if (!should_prefer_virtual_display()) {
wait_start = {};
pending_virtual_name.clear();
return true;
}
if (auto desired_name = active_virtual_display_dxgi_name()) {
for (int i = 0; i < static_cast<int>(display_names.size()); ++i) {
if (boost::iequals(display_names[i], *desired_name)) {
display_index = i;
wait_start = {};
pending_virtual_name.clear();
return true;
}
}
pending_virtual_name = *desired_name;
} else {
pending_virtual_name.clear();
}
const auto now = std::chrono::steady_clock::now();
if (wait_start == std::chrono::steady_clock::time_point {}) {
wait_start = now;
std::ostringstream available;
for (size_t i = 0; i < display_names.size(); ++i) {
if (i) {
available << ", ";
}
available << display_names[i];
}
BOOST_LOG(debug) << "Capture waiting for virtual display to become ready. desired='"
<< (pending_virtual_name.empty() ? std::string("(unresolved)") : pending_virtual_name)
<< "' active_output='" << config::get_active_output_name()
<< "' available=[" << available.str() << "]";
}
constexpr auto max_wait = std::chrono::seconds(3);
if (now - wait_start >= max_wait) {
BOOST_LOG(debug) << "Capture virtual display wait timed out after "
<< std::chrono::duration_cast<std::chrono::milliseconds>(max_wait).count()
<< "ms. desired='" << (pending_virtual_name.empty() ? std::string("(unresolved)") : pending_virtual_name)
<< "' active_output='" << config::get_active_output_name()
<< "'. Retrying without changing the capture target.";
wait_start = {};
pending_virtual_name.clear();
// A readiness timeout must not authorize capture of index zero (often
// a physical display). Return to the caller's cancellable retry loop.
return false;
}
return false;
#else
if (display_names.empty()) {
display_index = 0;
return false;
}
display_index = std::clamp(display_index, 0, static_cast<int>(display_names.size()) - 1);
return true;
#endif
}
bool is_placeholder_capture_image(const platf::img_t &img) {
#ifdef _WIN32
if (auto d3d_img = dynamic_cast<const platf::dxgi::img_d3d_t *>(&img)) {
return d3d_img->dummy;
}
#endif
return false;
}
struct encode_bootstrap_state_t {
bool allow_placeholder_before_first_real = false;
bool placeholder_encoded = false;
bool real_frame_seen = false;
bool current_input_placeholder = true;
bool should_encode_placeholder() const {
return !real_frame_seen && current_input_placeholder &&
allow_placeholder_before_first_real && !placeholder_encoded;
}
};
struct probe_cache_key_t {
std::string encoder_configuration;
std::string adapter_identity;
std::string adapter_identity_source;
bool adapter_identity_resolved = false;
bool operator==(const probe_cache_key_t &other) const {
return encoder_configuration == other.encoder_configuration &&
adapter_identity == other.adapter_identity;
}
};
struct probe_adapter_identity_t {
std::string identity;
std::string source;
bool resolved = false;
};
struct probe_target_t {
std::string display_name;
std::optional<platf::adapter_id_t> required_adapter;
probe_adapter_identity_t adapter_identity;
};
struct EncoderProbeCacheState {
std::mutex mutex;
std::optional<probe_cache_key_t> cache_key;
std::optional<probe_cache_key_t> attempted_cache_key;
bool valid = false;
bool hdr_supported = false;
bool hevc_passed = false;
bool hevc_hdr_supported = false;
bool av1_passed = false;
bool av1_hdr_supported = false;
advertised_encoder_capabilities_t advertised_capabilities;
#ifdef _WIN32
std::optional<LUID> pending_virtual_display_adapter_hint;
std::uint64_t pending_virtual_display_adapter_hint_lease = 0;
std::uint64_t next_pending_virtual_display_adapter_hint_lease = 0;
bool pending_virtual_display_adapter_hint_ready_for_verification = false;
#endif
// Track failed probe attempts per cache key for diagnostics.
std::optional<probe_cache_key_t> failure_cache_key;
int failure_count = 0;
};
EncoderProbeCacheState &encoder_probe_cache_state() {
static EncoderProbeCacheState state;
return state;
}
#ifdef _WIN32
platf::adapter_id_t adapter_id_from_luid(const LUID &luid) {
return platf::adapter_id_t {
.high_part = luid.HighPart,
.low_part = luid.LowPart,
};
}
std::string adapter_cache_identity(const platf::adapter_id_t &adapter_id) {
std::ostringstream oss;
oss << "luid=" << adapter_id.high_part << ':' << adapter_id.low_part;
return oss.str();
}
std::string luid_cache_identity(const LUID &luid) {
return adapter_cache_identity(adapter_id_from_luid(luid));
}
#endif
probe_target_t resolve_probe_target() {
#ifdef _WIN32
const auto active_output = config::get_active_output_name();
const auto mapped_output = display_device::map_output_name(active_output);
const bool mapped_output_is_active =
!mapped_output.empty() && display_device::output_is_active(mapped_output);
const auto current_wgc_identity = [&]() -> std::optional<platf::dxgi::wgc_adapter_identity_t> {
const auto identity = platf::dxgi::get_last_wgc_adapter_identity();
if (!identity ||
!mapped_output_is_active ||
!boost::iequals(identity->output_name, mapped_output)) {
return std::nullopt;
}
return identity;
}();
const auto mapped_output_matches_adapter = [&](const LUID &adapter_luid) {
if (!mapped_output_is_active) {
return false;
}
const auto output_adapter = platf::resolve_output_adapter(mapped_output);
return output_adapter && platf::adapter_luid_equal(*output_adapter.luid, adapter_luid);
};
std::optional<LUID> pending_virtual_display_adapter_hint;
bool pending_virtual_display_adapter_hint_ready_for_verification = false;
{
auto &state = encoder_probe_cache_state();
std::lock_guard<std::mutex> lock(state.mutex);
pending_virtual_display_adapter_hint =
state.pending_virtual_display_adapter_hint;
pending_virtual_display_adapter_hint_ready_for_verification =
state.pending_virtual_display_adapter_hint_ready_for_verification;
}
if (pending_virtual_display_adapter_hint) {
const auto pending_adapter = adapter_id_from_luid(*pending_virtual_display_adapter_hint);
std::optional<LUID> observed_adapter;
std::string_view observed_source;
if (current_wgc_identity) {
observed_adapter = current_wgc_identity->luid;
observed_source = "wgc";
} else {
const auto output_adapter = platf::resolve_output_adapter(mapped_output);
if (output_adapter) {
observed_adapter = *output_adapter.luid;
observed_source = "dxgi";
}
}
// A pending virtual-display adapter is a lookup expectation. It may
// reuse only a positive entry that was previously produced by an
// actual probe on that adapter. The observed output remains diagnostic
// here because it may still be the physical display being replaced.
if (observed_adapter) {
const bool matches_pending = platf::adapter_luid_equal(
*pending_virtual_display_adapter_hint,
*observed_adapter
);
const bool reuse_observed_output =
matches_pending && mapped_output_matches_adapter(*pending_virtual_display_adapter_hint);
std::string probe_output;
if (reuse_observed_output) {
probe_output = mapped_output;
} else if (const auto scoped_output = platf::dxgi::resolve_automatic_capture_output(
platf::mem_type_e::dxgi,
pending_adapter
)) {
probe_output = scoped_output->output_name;
}
return probe_target_t {
.display_name = std::move(probe_output),
.required_adapter = pending_adapter,
.adapter_identity = probe_adapter_identity_t {
.identity = luid_cache_identity(*pending_virtual_display_adapter_hint),
.source =
"pending-virtual-display-lookup-" + std::string(observed_source) +
(reuse_observed_output ? "-active-match" : "-automatic-output"),
.resolved = true,
},
};
}
// This identity is an expectation for cache lookup only. Successful
// producers replace it with the adapter returned by the initialized
// probe display before update_probe_cache() is called.
std::string probe_output;
if (const auto scoped_output = platf::dxgi::resolve_automatic_capture_output(
platf::mem_type_e::dxgi,
pending_adapter
)) {
probe_output = scoped_output->output_name;
}
return probe_target_t {
.display_name = std::move(probe_output),
.required_adapter = pending_adapter,
.adapter_identity = probe_adapter_identity_t {
.identity = luid_cache_identity(*pending_virtual_display_adapter_hint),
.source = pending_virtual_display_adapter_hint_ready_for_verification ?
"pending-virtual-display-adapter-awaiting-observation" :
"pending-virtual-display-adapter-before-publication",
.resolved = true,
},
};
}
const bool adapter_is_configured =
!config::video.adapter_name.empty() ||
!config::video.adapter_pnp_id.empty();
if (adapter_is_configured) {
const auto configured_adapter = platf::resolve_adapter(
config::video.adapter_name,
config::video.adapter_pnp_id
);
if (configured_adapter) {
const auto configured_adapter_id = adapter_id_from_luid(*configured_adapter.luid);
const bool preferred_output_matches_adapter =
mapped_output_matches_adapter(*configured_adapter.luid);
const bool wgc_mismatches_configured_adapter =
current_wgc_identity &&
!platf::adapter_luid_equal(*configured_adapter.luid, current_wgc_identity->luid);
std::string probe_output = preferred_output_matches_adapter ? mapped_output : std::string {};
if (probe_output.empty()) {
if (const auto scoped_output = platf::dxgi::resolve_automatic_capture_output(
platf::mem_type_e::dxgi,
configured_adapter_id
)) {
probe_output = scoped_output->output_name;
}
}
return probe_target_t {
.display_name = std::move(probe_output),
.required_adapter = configured_adapter_id,
.adapter_identity = probe_adapter_identity_t {
.identity = luid_cache_identity(*configured_adapter.luid),
.source = preferred_output_matches_adapter ?
(wgc_mismatches_configured_adapter ?
"configured-adapter-active-output-wgc-mismatch" :
"configured-adapter-active-output") :
(wgc_mismatches_configured_adapter ?
"configured-adapter-automatic-output-wgc-mismatch" :
"configured-adapter-automatic-output"),
.resolved = true,
},
};
}
return probe_target_t {
.display_name = mapped_output,
.adapter_identity = probe_adapter_identity_t {
.identity = "unresolved-configured-adapter=" +
std::string(platf::adapter_resolution_status_name(configured_adapter.status)),
.source = "configured-adapter-unresolved",
.resolved = false,
},
};
}
if (current_wgc_identity) {
return probe_target_t {
.display_name = mapped_output,
.required_adapter = adapter_id_from_luid(current_wgc_identity->luid),
.adapter_identity = probe_adapter_identity_t {
.identity = luid_cache_identity(current_wgc_identity->luid),
.source = "current-output-wgc",
.resolved = true,
},
};
}
if (!mapped_output.empty()) {
const auto output_adapter = platf::resolve_output_adapter(mapped_output);
if (mapped_output_is_active && output_adapter) {
return probe_target_t {
.display_name = mapped_output,
.required_adapter = adapter_id_from_luid(*output_adapter.luid),
.adapter_identity = probe_adapter_identity_t {
.identity = luid_cache_identity(*output_adapter.luid),
.source = "current-output-dxgi",
.resolved = true,
},
};
}
}
// Materialize Automatic using the same ordered, compatibility-tested
// output enumeration used by normal capture. Passing both values into
// the probe prevents WGC and DDX from choosing different monitors.
const auto automatic_output = platf::dxgi::resolve_automatic_capture_output(platf::mem_type_e::dxgi);
if (automatic_output) {
return probe_target_t {
.display_name = automatic_output->output_name,
.required_adapter = automatic_output->adapter_id,
.adapter_identity = probe_adapter_identity_t {
.identity = adapter_cache_identity(automatic_output->adapter_id),
.source = "automatic-output-dxgi",
.resolved = true,
},
};
}
// A headless or pre-login machine may have no capturable output at all.
// Encoder validation uses synthetic surfaces, so resolve the render GPU
// directly instead of treating missing WGC/DXGI publication as a missing
// encoder target.
const auto preferred_adapter = platf::resolve_preferred_render_adapter(
config::video.adapter_name,
config::video.adapter_pnp_id
);
if (preferred_adapter) {
return probe_target_t {
.required_adapter = adapter_id_from_luid(*preferred_adapter.luid),
.adapter_identity = probe_adapter_identity_t {
.identity = luid_cache_identity(*preferred_adapter.luid),
.source = "preferred-render-adapter-without-capture-output",
.resolved = true,
},
};
}
return probe_target_t {
.adapter_identity = probe_adapter_identity_t {
.identity = "unresolved-automatic-adapter=not-found|output=",
.source = "automatic-adapter-unresolved",
.resolved = false,
},
};
#else
const auto active_output = display_device::map_output_name(config::get_active_output_name());
return probe_target_t {
.display_name =
!active_output.empty() && display_device::output_is_active(active_output) ?
active_output :
std::string {},
.adapter_identity = probe_adapter_identity_t {
.identity = "platform-default",
.source = "platform-default",
.resolved = true,
},
};
#endif
}
probe_cache_key_t build_probe_cache_key(const probe_target_t *probe_target = nullptr) {
std::ostringstream oss;
// Keep encoder configuration separate from the exact selected adapter.
// Output identifiers and the whole-machine GPU inventory are deliberately
// excluded: neither changes the selected GPU's codec capability.
auto append_optional = [&](std::string_view name, const std::optional<int> &value) {
oss << '|' << name << '=';
if (value) {
oss << *value;
} else {
oss << "auto";
}
};
oss << "encoder=" << config::video.encoder
<< "|hevc=" << config::video.hevc_mode
<< "|av1=" << config::video.av1_mode
<< "|pyrowave=" << (config::video.pyrowave ? 1 : 0)
<< "|amd_coder=" << config::video.amd.amd_coder
<< "|amd_ltr=" << config::video.amd.amd_ltr_frames
<< "|amd_queue=" << config::video.amd.amd_input_queue_size;
append_optional("amd_usage_h264", config::video.amd.amd_usage_h264);
append_optional("amd_usage_hevc", config::video.amd.amd_usage_hevc);
append_optional("amd_usage_av1", config::video.amd.amd_usage_av1);
append_optional("amd_rc_h264", config::video.amd.amd_rc_h264);
append_optional("amd_rc_hevc", config::video.amd.amd_rc_hevc);
append_optional("amd_rc_av1", config::video.amd.amd_rc_av1);
append_optional("amd_quality_h264", config::video.amd.amd_quality_h264);
append_optional("amd_quality_hevc", config::video.amd.amd_quality_hevc);
append_optional("amd_quality_av1", config::video.amd.amd_quality_av1);
append_optional("amd_qvbr_quality", config::video.amd.amd_qvbr_quality_level);
append_optional("amd_vbaq", config::video.amd.amd_vbaq);
append_optional("amd_preanalysis", config::video.amd.amd_preanalysis);
append_optional("amd_enforce_hrd", config::video.amd.amd_enforce_hrd);
append_optional("amd_lowlatency", config::video.amd.amd_lowlatency_mode);
append_optional("amd_motion_boost", config::video.amd.amd_high_motion_quality_boost);
append_optional("amd_sav", config::video.amd.amd_smart_access_video);
append_optional("amd_av1_screen", config::video.amd.amd_av1_screen_content);
append_optional("amd_av1_latency", config::video.amd.amd_av1_latency_mode);
// Quarantine changes encoder selection, so it belongs in the key. Once the
// gate latches, `amdvce_experimental` can no longer build a session; a cached success
// from before the quarantine would keep handing back `chosen_encoder =
// &amdvce_experimental` and every later stream would end before its first packet, even
// though the software encoder would have validated. Re-key so the next
// probe re-runs and selection can degrade to software as designed.
oss << "|amf_quarantined=" << (native_amf_lifecycle_gate.is_quarantined() ? 1 : 0);
const auto adapter_identity = probe_target ?
probe_target->adapter_identity :
resolve_probe_target().adapter_identity;
return probe_cache_key_t {
.encoder_configuration = oss.str(),
.adapter_identity = adapter_identity.identity,
.adapter_identity_source = adapter_identity.source,
.adapter_identity_resolved = adapter_identity.resolved,
};
}
bool probe_cache_matches(const probe_cache_key_t &key, bool want_hdr, bool want_hevc, bool want_hevc_hdr, bool want_av1, bool want_av1_hdr) {
auto &state = encoder_probe_cache_state();
std::lock_guard<std::mutex> lock(state.mutex);
const auto policy_key = encoder_probe_policy::cache_key_t {
.encoder_configuration = key.encoder_configuration,
.adapter_identity = key.adapter_identity,
.adapter_identity_resolved = key.adapter_identity_resolved,
};
const auto policy_cached_key = state.cache_key ?
std::optional<encoder_probe_policy::cache_key_t> {
encoder_probe_policy::cache_key_t {
.encoder_configuration = state.cache_key->encoder_configuration,
.adapter_identity = state.cache_key->adapter_identity,
.adapter_identity_resolved = state.cache_key->adapter_identity_resolved,
}
} :
std::nullopt;
// Check if we have a valid cached success
if (state.valid &&
encoder_probe_policy::cache_key_matches(policy_key, policy_cached_key) &&
(!want_hdr || state.hdr_supported)) {
const bool hevc_supported = state.hevc_passed && (!want_hevc_hdr || state.hevc_hdr_supported);
const bool av1_supported = state.av1_passed && (!want_av1_hdr || state.av1_hdr_supported);
if ((want_hevc && !hevc_supported) || (want_av1 && !av1_supported)) {
// Never trust a cached negative codec result; force a fresh probe.
return false;
}
return true;
}
return false;
}
void update_probe_cache(
const probe_cache_key_t &key,
const bool success,
const bool hdr_supported,
const bool hevc_passed,
const bool hevc_hdr_supported,
const bool av1_passed,
const bool av1_hdr_supported,
const advertised_encoder_capabilities_t &advertised_capabilities = {}
) {
auto &state = encoder_probe_cache_state();
std::lock_guard<std::mutex> lock(state.mutex);
if (success) {
if (!key.adapter_identity_resolved) {
state.valid = false;
state.cache_key.reset();
state.hdr_supported = false;
state.hevc_passed = false;
state.hevc_hdr_supported = false;
state.av1_passed = false;
state.av1_hdr_supported = false;
state.advertised_capabilities = {};
BOOST_LOG(warning)
<< "Encoder probe succeeded but its effective adapter identity is unresolved; "
"refusing to cache positive capabilities (identity='"
<< key.adapter_identity << "', source="
<< key.adapter_identity_source << ").";
return;
}
state.cache_key = key;
state.valid = true;
state.hdr_supported = hdr_supported;
state.hevc_passed = hevc_passed;
state.hevc_hdr_supported = hevc_hdr_supported;
state.av1_passed = av1_passed;
state.av1_hdr_supported = av1_hdr_supported;
state.advertised_capabilities = advertised_capabilities;
// Clear failure tracking on success
state.failure_cache_key.reset();
state.failure_count = 0;
} else {
state.valid = false;
state.cache_key.reset();
state.hdr_supported = false;
state.hevc_passed = false;
state.hevc_hdr_supported = false;
state.av1_passed = false;
state.av1_hdr_supported = false;
state.advertised_capabilities = {};
// Track failures, but never permanently lock out future probes.
if (state.failure_cache_key && *state.failure_cache_key == key) {
state.failure_count++;
} else {
state.failure_cache_key = key;
state.failure_count = 1;
}
BOOST_LOG(warning) << "Encoder probe failed (attempt " << state.failure_count
<< " for this configuration), will retry on next attempt";
}
}
void mark_probe_attempted(const probe_cache_key_t &key) {
auto &state = encoder_probe_cache_state();
std::lock_guard<std::mutex> lock(state.mutex);
state.attempted_cache_key = key;
}
void log_probe_cache_miss(const probe_cache_key_t ¤t_key) {
auto &state = encoder_probe_cache_state();
std::lock_guard<std::mutex> lock(state.mutex);
if (!state.valid || !state.cache_key) {
BOOST_LOG(debug)
<< "Encoder capability cache miss: no successful probe is cached; current_adapter='"
<< current_key.adapter_identity << "' source="
<< current_key.adapter_identity_source << '.';
return;
}
BOOST_LOG(debug)
<< "Encoder capability cache miss: configuration_changed="
<< (state.cache_key->encoder_configuration != current_key.encoder_configuration)
<< ", adapter_changed="
<< (state.cache_key->adapter_identity != current_key.adapter_identity)
<< ", cached_adapter='" << state.cache_key->adapter_identity
<< "', current_adapter='" << current_key.adapter_identity
<< "', current_source=" << current_key.adapter_identity_source << '.';
}
} // namespace
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);
}
namespace nv {
enum class profile_h264_e : int {
high = 2, ///< High profile
high_444p = 3, ///< High 4:4:4 Predictive profile
};
enum class profile_hevc_e : int {
main = 0, ///< Main profile
main_10 = 1, ///< Main 10 profile
rext = 2, ///< Rext profile
};
} // namespace nv
namespace qsv {
enum class profile_h264_e : int {
high = 100, ///< High profile
high_444p = 244, ///< High 4:4:4 Predictive profile
};
enum class profile_hevc_e : int {
main = 1, ///< Main profile
main_10 = 2, ///< Main 10 profile
rext = 4, ///< RExt profile
};
enum class profile_av1_e : int {
main = 1, ///< Main profile
high = 2, ///< High profile
};
} // namespace qsv
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 *);
#ifdef SUNSHINE_BUILD_VULKAN
util::Either<avcodec_buffer_t, int> vulkan_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *);
#endif
class avcodec_software_encode_device_t: public platf::avcodec_encode_device_t {
public:
int convert(platf::img_t &img) override {
// If we need to add aspect ratio padding, we need to scale into an intermediate output buffer
bool requires_padding = (sw_frame->width != sws_output_frame->width || sw_frame->height != sws_output_frame->height);
// Setup the input frame using the caller's img_t
sws_input_frame->data[0] = img.data;
sws_input_frame->linesize[0] = img.row_pitch;
// Perform color conversion and scaling to the final size
auto status = sws_scale_frame(sws.get(), requires_padding ? sws_output_frame.get() : sw_frame.get(), sws_input_frame.get());
if (status < 0) {
char string[AV_ERROR_MAX_STRING_SIZE];
BOOST_LOG(error) << "Couldn't scale frame: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status);
return -1;
}
// If we require aspect ratio padding, copy the output frame into the final padded frame
if (requires_padding) {
auto fmt_desc = av_pix_fmt_desc_get((AVPixelFormat) sws_output_frame->format);
auto planes = av_pix_fmt_count_planes((AVPixelFormat) sws_output_frame->format);
for (int plane = 0; plane < planes; plane++) {
auto shift_h = plane == 0 ? 0 : fmt_desc->log2_chroma_h;
auto shift_w = plane == 0 ? 0 : fmt_desc->log2_chroma_w;
auto offset = ((offsetW >> shift_w) * fmt_desc->comp[plane].step) + (offsetH >> shift_h) * sw_frame->linesize[plane];
// Copy line-by-line to preserve leading padding for each row
for (int line = 0; line < sws_output_frame->height >> shift_h; line++) {
memcpy(sw_frame->data[plane] + offset + (line * sw_frame->linesize[plane]), sws_output_frame->data[plane] + (line * sws_output_frame->linesize[plane]), (size_t) (sws_output_frame->width >> shift_w) * fmt_desc->comp[plane].step);
}
}
}
// 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
*/
void prefill() {
auto frame = sw_frame ? sw_frame.get() : this->frame;
av_frame_get_buffer(frame, 0);
av_frame_make_writable(frame);
ptrdiff_t linesize[4] = {frame->linesize[0], frame->linesize[1], frame->linesize[2], frame->linesize[3]};
av_image_fill_black(frame->data, linesize, (AVPixelFormat) frame->format, frame->color_range, frame->width, frame->height);
}
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;
}
// Fill aspect ratio padding in the destination frame
prefill();
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;
sws_input_frame.reset(av_frame_alloc());
sws_input_frame->width = in_width;
sws_input_frame->height = in_height;
sws_input_frame->format = AV_PIX_FMT_BGR0;
sws_output_frame.reset(av_frame_alloc());
sws_output_frame->width = out_width;
sws_output_frame->height = out_height;
sws_output_frame->format = format;
// Result is always positive
offsetW = (frame->width - out_width) / 2;
offsetH = (frame->height - out_height) / 2;
sws.reset(sws_alloc_context());
if (!sws) {
return -1;
}
AVDictionary *options {nullptr};
av_dict_set_int(&options, "srcw", sws_input_frame->width, 0);
av_dict_set_int(&options, "srch", sws_input_frame->height, 0);
av_dict_set_int(&options, "src_format", sws_input_frame->format, 0);
av_dict_set_int(&options, "dstw", sws_output_frame->width, 0);
av_dict_set_int(&options, "dsth", sws_output_frame->height, 0);
av_dict_set_int(&options, "dst_format", sws_output_frame->format, 0);
av_dict_set_int(&options, "sws_flags", SWS_LANCZOS | SWS_ACCURATE_RND, 0);
av_dict_set_int(&options, "threads", config::video.min_threads, 0);
auto status = av_opt_set_dict(sws.get(), &options);
av_dict_free(&options);
if (status < 0) {
char string[AV_ERROR_MAX_STRING_SIZE];
BOOST_LOG(error) << "Failed to set SWS options: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status);
return -1;
}
status = sws_init_context(sws.get(), nullptr, nullptr);
if (status < 0) {
char string[AV_ERROR_MAX_STRING_SIZE];
BOOST_LOG(error) << "Failed to initialize SWS: "sv << av_make_error_string(string, AV_ERROR_MAX_STRING_SIZE, status);
return -1;
}
return 0;
}
// Store ownership when frame is hw_frame
avcodec_frame_t hw_frame;
avcodec_frame_t sw_frame;
avcodec_frame_t sws_input_frame;
avcodec_frame_t sws_output_frame;
sws_t sws;
// Offset of input image to output frame in pixels
int offsetW;
int offsetH;
};
enum flag_e : uint32_t {
DEFAULT = 0, ///< Default flags
PARALLEL_ENCODING = 1 << 1, ///< Capture and encoding can run concurrently on separate threads
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. e.g. VAAPI
SINGLE_SLICE_ONLY = 1 << 4, ///< Never use multiple slices. Older intel iGPU's ruin it for everyone else
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
ALWAYS_REPROBE = 1 << 9, ///< This is an encoder of last resort and we want to aggressively probe for a better one
YUV444_SUPPORT = 1 << 10, ///< Encoder may support 4:4:4 chroma sampling depending on hardware
ASYNC_TEARDOWN = 1 << 11, ///< Encoder supports async teardown on a different thread
FIXED_GOP_SIZE = 1 << 12, ///< Use fixed small GOP size (encoder doesn't support on-demand IDR frames)
OWNER_THREAD_TEARDOWN = 1 << 13, ///< Conversion resources require their current graphics context during destruction
};
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() {
// Don't drain the encoder before freeing it. The drained packets are discarded anyway,
// and FFmpeg's AMF backend waits on the driver without a deadline while draining — a
// wedged AMF runtime turns that into a permanent hang on the session teardown path
// (vibeshine#187). avcodec_free_context() is documented to be safe without a drain.
// 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();
}
void set_hdr_metadata(const SS_HDR_METADATA &metadata) override {
if (!device || !device->frame) {
return;
}
auto *frame = device->frame;
auto *mdm_side_data = av_frame_get_side_data(frame, AV_FRAME_DATA_MASTERING_DISPLAY_METADATA);
auto *mdm = mdm_side_data ?
reinterpret_cast<AVMasteringDisplayMetadata *>(mdm_side_data->data) :
av_mastering_display_metadata_create_side_data(frame);
if (mdm) {
for (int primary = 0; primary < 3; ++primary) {
mdm->display_primaries[primary][0] = av_make_q(metadata.displayPrimaries[primary].x, 50000);
mdm->display_primaries[primary][1] = av_make_q(metadata.displayPrimaries[primary].y, 50000);
}
mdm->white_point[0] = av_make_q(metadata.whitePoint.x, 50000);
mdm->white_point[1] = av_make_q(metadata.whitePoint.y, 50000);
mdm->min_luminance = av_make_q(metadata.minDisplayLuminance, 10000);
mdm->max_luminance = av_make_q(metadata.maxDisplayLuminance, 1);
mdm->has_luminance = metadata.maxDisplayLuminance != 0 ? 1 : 0;
mdm->has_primaries = metadata.displayPrimaries[0].x != 0 ? 1 : 0;
}
auto *clm_side_data = av_frame_get_side_data(frame, AV_FRAME_DATA_CONTENT_LIGHT_LEVEL);
auto *clm = clm_side_data ?
reinterpret_cast<AVContentLightMetadata *>(clm_side_data->data) :
av_content_light_metadata_create_side_data(frame);
if (clm) {
clm->MaxCLL = metadata.maxContentLightLevel;
clm->MaxFALL = metadata.maxFrameAverageLightLevel;
}
}
void restore_display_lease_after_initialization(std::shared_ptr<platf::display_t> display) {
if (device) device->restore_display_lease_after_initialization(std::move(display));
}
std::shared_ptr<platf::display_t> release_display_lease_for_driver_work() {
return device ? device->release_display_lease_for_initialization() : nullptr;
}
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)) {
}
~nvenc_encode_session_t() override {
if (device && !device->prepare_to_destroy()) {
#if defined(__linux__) && defined(SUNSHINE_BUILD_CUDA)
native_nvenc_runtime_quarantined.store(true, std::memory_order_release);
#endif
(void) device.release();
BOOST_LOG(error) << "NvEnc: encoder teardown failed; quarantining native NVENC and the complete device until process exit"sv;
}
}
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;
}
}
bool set_bitrate(int bitrate_kbps) override {
if (!device || !device->nvenc) {
return false;
}
return device->nvenc->set_bitrate(bitrate_kbps);
}
void set_hdr_metadata(const SS_HDR_METADATA &metadata) override {
if (!device || !device->nvenc) {
return;
}
device->hdr_metadata = metadata;
device->hdr_metadata_valid = true;
device->nvenc->set_hdr_metadata(metadata);
}
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;
};
class amf_encode_session_t: public encode_session_t {
public:
amf_encode_session_t(std::unique_ptr<platf::amf_encode_device_t> encode_device):
device(std::move(encode_device)) {
}
int convert(platf::img_t &img) override {
if (!device) {
return -1;
}
const auto result = device->convert(img);
if (result == 0) {
fresh_conversion_pending = true;
}
return result;
}
void request_idr_frame() override {
force_idr = true;
}
void request_normal_frame() override {
// encode_frames() clears force_idr only after AMF accepts the input. The
// generic loops call this after every nonfatal attempt, including
// backpressure drops, so clearing it here would lose recovery IDRs.
}
void invalidate_ref_frames(int64_t first_frame, int64_t last_frame) override {
if (!device || !device->amf) {
return;
}
if (!device->amf->invalidate_ref_frames(first_frame, last_frame)) {
force_idr = true;
}
}
bool set_bitrate(int bitrate_kbps) override {
return device && device->amf && device->amf->set_bitrate(bitrate_kbps);
}
// Without this the live RTX-HDR refresh path is a no-op on native AMF: the
// control-channel SS_HDR_METADATA and the forced IDR still go out, but the
// bitstream keeps carrying the session-start mastering-display values, so the
// client tone-maps against a peak the stream never declares. The legacy
// FFmpeg AMD path honoured this, and INPUT_HDR_METADATA is a per-submit
// component property, so it is writable after Init.
void set_hdr_metadata(const SS_HDR_METADATA &metadata) override {
if (!device || !device->amf) {
return;
}
device->hdr_metadata = metadata;
device->hdr_metadata_valid = true;
amf::amf_hdr_metadata amf_metadata;
for (int i = 0; i < 3; i++) {
amf_metadata.displayPrimaries[i].x = metadata.displayPrimaries[i].x;
amf_metadata.displayPrimaries[i].y = metadata.displayPrimaries[i].y;
}
amf_metadata.whitePoint.x = metadata.whitePoint.x;
amf_metadata.whitePoint.y = metadata.whitePoint.y;
amf_metadata.maxDisplayLuminance = metadata.maxDisplayLuminance;
amf_metadata.minDisplayLuminance = metadata.minDisplayLuminance;
amf_metadata.maxContentLightLevel = metadata.maxContentLightLevel;
amf_metadata.maxFrameAverageLightLevel = metadata.maxFrameAverageLightLevel;
if (!device->amf->set_hdr_metadata(std::optional<amf::amf_hdr_metadata> {amf_metadata})) {
BOOST_LOG(warning) << "AMF: failed to update live HDR mastering metadata; the bitstream keeps the previous values"sv;
}
}
amf::amf_encode_result encode_frames(uint64_t frame_index) {
if (!device || !device->amf) {
return {};
}
const bool meaningful_new_input = fresh_conversion_pending || force_idr;
auto result = device->amf->encode_frame(frame_index, force_idr);
last_input_accepted = result.input_accepted;
if (result.input_accepted) {
last_input_accepted_at = result.input_accepted_at;
force_idr = false;
// A duplicate submitted solely to release PA's retained final frame must
// not arm another flush just because PA now retains that duplicate. A
// newly converted capture frame or requested IDR starts a new cycle.
suppress_tail_flush_until_fresh_conversion = !meaningful_new_input;
fresh_conversion_pending = false;
}
return result;
}
bool was_last_input_accepted() const {
return last_input_accepted;
}
std::optional<std::chrono::steady_clock::time_point> input_accepted_at() const {
return last_input_accepted_at;
}
amf::amf_encode_result drain_frames(std::chrono::milliseconds timeout) {
if (!device || !device->amf) {
return {};
}
return device->amf->drain_output(timeout);
}
bool has_output_due() {
return device && device->amf && device->amf->has_output_due();
}
bool has_completed_output() {
return device && device->amf && device->amf->has_completed_output();
}
bool has_retained_preanalysis_tail() {
const bool retained = device && device->amf && device->amf->has_retained_preanalysis_tail();
return amf::lifecycle::preanalysis_tail_flush_is_due(
retained,
suppress_tail_flush_until_fresh_conversion);
}
bool begin_drain() {
return device && device->amf && device->amf->begin_drain();
}
std::shared_ptr<platf::display_t> release_display_lease_for_driver_work() {
return device ? device->release_display_lease_for_initialization() : nullptr;
}
// The native AMF encoder is pipelined. A catch-up batch can contain output from
// earlier submissions, but emitted indices remain strictly increasing (in order,
// no duplicates), so only flag a genuine regression (out-of-order / duplicate).
// Forward gaps are also normal (a dropped frame is logged separately at submit time).
bool note_emitted_index(uint64_t emitted) {
const bool monotonic = last_emitted_index < 0 || (int64_t) emitted > last_emitted_index;
last_emitted_index = (int64_t) emitted;
return monotonic;
}
bool has_emitted_frame(uint64_t frame_index) const {
return last_emitted_index >= 0 && static_cast<uint64_t>(last_emitted_index) >= frame_index;
}
bool has_emitted_any_frame() const {
return last_emitted_index >= 0;
}
// Per-frame timestamps captured at submit time. Because the encoder emits an
// earlier frame than the one just submitted, each packet must be stamped with the
// timestamps of the frame it actually carries, not the newest submitted frame -
// otherwise runtime latency stats are skewed by the pipeline depth.
struct frame_timestamps_t {
std::optional<std::chrono::steady_clock::time_point> frame_timestamp;
std::optional<std::chrono::steady_clock::time_point> capture_timestamp;
std::optional<std::chrono::steady_clock::time_point> host_processing_timestamp;
};
void store_frame_timestamps(uint64_t frame_index, const frame_timestamps_t &ts) {
// Fixed ring indexed by frame number: no per-frame heap traffic on the
// submit path. The capacity sits well above the real pipeline depth, and a
// collision only ever overwrites an entry 256 frames stale — one the old
// bounded map would have evicted anyway.
auto &slot = pending_timestamps[frame_index & (pending_timestamps.size() - 1)];
slot.frame_index = frame_index;
slot.valid = true;
slot.ts = ts;
}
frame_timestamps_t take_frame_timestamps(uint64_t frame_index) {
auto &slot = pending_timestamps[frame_index & (pending_timestamps.size() - 1)];
if (!slot.valid || slot.frame_index != frame_index) {
return {};
}
slot.valid = false;
return slot.ts;
}
void discard_frame_timestamps(uint64_t frame_index) {
auto &slot = pending_timestamps[frame_index & (pending_timestamps.size() - 1)];
if (slot.valid && slot.frame_index == frame_index) {
slot.valid = false;
}
}
private:
std::unique_ptr<platf::amf_encode_device_t> device;
bool force_idr = false;
bool last_input_accepted = false;
std::optional<std::chrono::steady_clock::time_point> last_input_accepted_at;
bool fresh_conversion_pending = false;
bool suppress_tail_flush_until_fresh_conversion = false;
int64_t last_emitted_index = -1;
struct pending_timestamp_slot_t {
uint64_t frame_index = 0;
bool valid = false;
frame_timestamps_t ts;
};
// Power-of-two size; indexed with frame_index & (size - 1).
std::array<pending_timestamp_slot_t, 256> pending_timestamps {};
};
// Sticky per-session HDR state, persists across capture reinits so a transient SDR
// display reading cannot downgrade an HDR stream's colorspace or poison its metadata.
struct hdr_latch_t {
// Set once the session has actually established HDR.
bool latched = false;
// Metadata captured while the display genuinely read HDR, reused during reinits
// where the display transiently reads SDR.
bool metadata_valid = false;
SS_HDR_METADATA metadata {};
};
struct rtx_hdr_metadata_refresh_state_t {
std::uint32_t observed_generation {0};
int pending_peak_nits {0};
std::chrono::steady_clock::time_point stable_since {};
};
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;
safe::mail_raw_t::event_t<int> bitrate_events;
config_t config;
int frame_nr;
void *channel_data;
hdr_latch_t hdr_latch;
// Last HDR info raised to this session's client, used to suppress duplicates on reinit.
std::optional<hdr_info_raw_t> last_hdr_info;
rtx_hdr_metadata_refresh_state_t rtx_hdr_metadata_refresh;
};
struct sync_session_t {
sync_session_ctx_t *ctx;
std::unique_ptr<encode_session_t> session;
encode_bootstrap_state_t bootstrap;
};
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;
};
#ifdef _WIN32
SS_HDR_METADATA synthetic_probe_hdr_metadata() {
SS_HDR_METADATA metadata {};
metadata.displayPrimaries[0] = {35400, 14600}; // Rec.2020 red
metadata.displayPrimaries[1] = {8500, 39850}; // Rec.2020 green
metadata.displayPrimaries[2] = {6550, 2300}; // Rec.2020 blue
metadata.whitePoint = {15635, 16450}; // D65
metadata.maxDisplayLuminance = 1000;
metadata.minDisplayLuminance = 1;
metadata.maxContentLightLevel = 1000;
metadata.maxFrameAverageLightLevel = 250;
metadata.maxFullFrameLuminance = 400;
return metadata;
}
void initialize_synthetic_display_geometry(platf::display_t &display, const config_t &config) {
display.width = display.logical_width = display.env_width = display.env_logical_width = std::max(1, config.width);
display.height = display.logical_height = display.env_height = display.env_logical_height = std::max(1, config.height);
}
// Encoder validation needs an initialized GPU and shareable surfaces, not a
// desktop capture session. Bind D3D to the exact selected adapter so the
// cache is owned by the device that actually created the probe surfaces.
class synthetic_vram_display_t final: public platf::dxgi::display_vram_t {
public:
synthetic_vram_display_t(
const config_t &config,
const std::optional<platf::adapter_id_t> &required_adapter
):
hdr_ {config.dynamicRange > 0 && !config.force_sdr} {
initialize_synthetic_display_geometry(*this, config);
width_before_rotation = width;
height_before_rotation = height;
capture_format = hdr_ ? DXGI_FORMAT_R16G16B16A16_FLOAT : DXGI_FORMAT_B8G8R8A8_UNORM;
next_image_id.store(0, std::memory_order_relaxed);
if (FAILED(CreateDXGIFactory1(IID_IDXGIFactory1, reinterpret_cast<void **>(&factory)))) {
return;
}
for (UINT index = 0;; ++index) {
IDXGIAdapter1 *candidate_ptr = nullptr;
const auto status = factory->EnumAdapters1(index, &candidate_ptr);
if (status == DXGI_ERROR_NOT_FOUND) {
break;
}
if (FAILED(status) || !candidate_ptr) {
return;
}
platf::dxgi::adapter_t candidate {candidate_ptr};
DXGI_ADAPTER_DESC1 description {};
if (FAILED(candidate->GetDesc1(&description)) ||
(description.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) != 0) {
continue;
}
const platf::adapter_id_t candidate_id {
.high_part = description.AdapterLuid.HighPart,
.low_part = description.AdapterLuid.LowPart,
};
if (required_adapter && candidate_id != *required_adapter) {
continue;
}
adapter = std::move(candidate);
break;
}
if (!adapter) {
return;
}
const D3D_FEATURE_LEVEL feature_levels[] {D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0};
if (FAILED(D3D11CreateDevice(
adapter.get(), D3D_DRIVER_TYPE_UNKNOWN, nullptr,
platf::dxgi::D3D11_CREATE_DEVICE_FLAGS |
D3D11_CREATE_DEVICE_BGRA_SUPPORT |
D3D11_CREATE_DEVICE_VIDEO_SUPPORT,
feature_levels, sizeof(feature_levels) / sizeof(D3D_FEATURE_LEVEL), D3D11_SDK_VERSION,
&device, &feature_level, &device_ctx))) {
device.reset();
return;
}
// Query the initialized device rather than trusting the requested
// adapter pointer. This is the identity published into the probe cache.
platf::dxgi::dxgi_t dxgi;
IDXGIAdapter *base_adapter = nullptr;
IDXGIAdapter1 *actual_adapter = nullptr;
if (FAILED(device->QueryInterface(IID_IDXGIDevice, reinterpret_cast<void **>(&dxgi))) ||
FAILED(dxgi->GetAdapter(&base_adapter)) ||
FAILED(base_adapter->QueryInterface(IID_IDXGIAdapter1, reinterpret_cast<void **>(&actual_adapter)))) {
if (base_adapter) base_adapter->Release();
device.reset();
return;
}
base_adapter->Release();
adapter.reset(actual_adapter);
DXGI_ADAPTER_DESC1 actual_description {};
if (FAILED(adapter->GetDesc1(&actual_description))) {
device.reset();
return;
}
captured_adapter_luid = actual_description.AdapterLuid;
const platf::adapter_id_t actual_id {
.high_part = captured_adapter_luid.HighPart,
.low_part = captured_adapter_luid.LowPart,
};
if (required_adapter && actual_id != *required_adapter) {
BOOST_LOG(error) << "Synthetic encoder probe initialized on a different adapter than requested.";
device.reset();
}
}
bool valid() const { return static_cast<bool>(device); }
bool is_hdr() override { return hdr_; }
bool get_hdr_metadata(SS_HDR_METADATA &metadata) override {
if (!hdr_) {
std::memset(&metadata, 0, sizeof(metadata));
return false;
}
metadata = synthetic_probe_hdr_metadata();
return true;
}
int dummy_img(platf::img_t *img_base) override {
if (platf::dxgi::display_vram_t::dummy_img(img_base)) {
return -1;
}
auto *img = static_cast<platf::dxgi::img_d3d_t *>(img_base);
const float black[] = {0.0f, 0.0f, 0.0f, 0.0f};
device_ctx->ClearRenderTargetView(img->capture_rt.get(), black);
return 0;
}
platf::capture_e capture(
const push_captured_image_cb_t &push,
const pull_free_image_cb_t &pull,
bool * /*cursor*/
) override {
return capture_synthetic_black(push, pull, client_frame_rate);
}
protected:
platf::capture_e snapshot(
const pull_free_image_cb_t &,
std::shared_ptr<platf::img_t> &,
std::chrono::milliseconds,
bool
) override { return platf::capture_e::error; }
platf::capture_e release_snapshot() override { return platf::capture_e::ok; }
private:
bool hdr_;
};
// Software fallback still needs a fake source, but it must not reopen
// Desktop Duplication merely to allocate a CPU image.
class synthetic_ram_display_t final: public platf::dxgi::display_ram_t {
public:
synthetic_ram_display_t(
const config_t &config,
std::optional<platf::adapter_id_t> required_adapter
):
adapter_id_ {std::move(required_adapter)},
hdr_ {config.dynamicRange > 0 && !config.force_sdr} {
initialize_synthetic_display_geometry(*this, config);
capture_format = DXGI_FORMAT_B8G8R8A8_UNORM;
}
platf::capture_e capture(
const push_captured_image_cb_t &,
const pull_free_image_cb_t &,
bool *
) override { return platf::capture_e::error; }
std::optional<platf::adapter_id_t> capture_adapter_id() const override {
return adapter_id_;
}
bool is_hdr() override { return hdr_; }
bool get_hdr_metadata(SS_HDR_METADATA &metadata) override {
if (!hdr_) {
std::memset(&metadata, 0, sizeof(metadata));
return false;
}
metadata = synthetic_probe_hdr_metadata();
return true;
}
protected:
platf::capture_e snapshot(
const pull_free_image_cb_t &,
std::shared_ptr<platf::img_t> &,
std::chrono::milliseconds,
bool
) override { return platf::capture_e::error; }
platf::capture_e release_snapshot() override { return platf::capture_e::ok; }
private:
std::optional<platf::adapter_id_t> adapter_id_;
bool hdr_;
};
std::shared_ptr<platf::display_t> make_synthetic_probe_display(
const platf::mem_type_e type,
const config_t &config,
const std::optional<platf::adapter_id_t> &required_adapter
) {
if (type == platf::mem_type_e::system) {
return std::make_shared<synthetic_ram_display_t>(config, required_adapter);
}
auto display = std::make_shared<synthetic_vram_display_t>(config, required_adapter);
if (!display->valid()) {
return {};
}
return display;
}
// A protocol video stream is still required for Remote Input, but it must
// never open a desktop/WGC/DD capture target. Reuse the synthetic D3D source
// and feed black frames at the client cadence.
std::shared_ptr<platf::display_t> make_black_display(const config_t &config) {
auto display = std::make_shared<synthetic_vram_display_t>(config, std::nullopt);
if (!display->valid()) return {};
display->client_frame_rate = std::max(1, config.framerate);
return display;
}
#elif defined(__linux__) && defined(SUNSHINE_BUILD_CUDA)
std::shared_ptr<platf::display_t> make_black_display(const config_t &config) {
return ::cuda::make_black_display(config);
}
#else
std::shared_ptr<platf::display_t> make_black_display(const config_t &) {
return {};
}
#endif
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_sync = safe::make_shared<capture_thread_sync_ctx_t>(start_capture_sync, end_capture_sync);
#ifdef _WIN32
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,
platf::pix_fmt_e::ayuv,
platf::pix_fmt_e::yuv444p16
),
{
{}, // Common options
{}, // SDR-specific options
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{}, // Fallback options
"av1_nvenc"s,
},
{
{}, // Common options
{}, // SDR-specific options
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{}, // Fallback options
"hevc_nvenc"s,
},
{
{}, // Common options
{}, // SDR-specific options
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{}, // Fallback options
"h264_nvenc"s,
},
PARALLEL_ENCODING | REF_FRAMES_INVALIDATION | YUV444_SUPPORT | ASYNC_TEARDOWN // flags
};
#elif !defined(__APPLE__)
#if defined(__linux__)
encoder_t nvenc_legacy {
"nvenc_legacy"sv,
#else
encoder_t nvenc {
"nvenc"sv,
#endif
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,
AV_PIX_FMT_NONE,
AV_PIX_FMT_NONE,
#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},
{"surfaces"s, 1},
{"cbr_padding"s, false},
{"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},
{"aq"s, &config::video.nv_legacy.aq},
{"split_encode_mode"s, &config::video.nv_legacy.split_encode_mode},
},
{}, // SDR-specific options
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{}, // Fallback options
"av1_nvenc"s,
},
{
// Common options
{
{"delay"s, 0},
{"forced-idr"s, 1},
{"zerolatency"s, 1},
{"surfaces"s, 1},
{"cbr_padding"s, false},
{"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},
{"aq"s, &config::video.nv_legacy.aq},
{"split_encode_mode"s, &config::video.nv_legacy.split_encode_mode},
},
{
// SDR-specific options
{"profile"s, (int) nv::profile_hevc_e::main},
},
{
// HDR-specific options
{"profile"s, (int) nv::profile_hevc_e::main_10},
},
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{}, // Fallback options
"hevc_nvenc"s,
},
{
{
{"delay"s, 0},
{"forced-idr"s, 1},
{"zerolatency"s, 1},
{"surfaces"s, 1},
{"cbr_padding"s, false},
{"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},
{"aq"s, &config::video.nv_legacy.aq},
},
{
// SDR-specific options
{"profile"s, (int) nv::profile_h264_e::high},
},
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{}, // Fallback options
"h264_nvenc"s,
},
PARALLEL_ENCODING
#if defined(__linux__) && defined(SUNSHINE_BUILD_CUDA)
| OWNER_THREAD_TEARDOWN
#endif
};
#endif
#if defined(__linux__) && defined(SUNSHINE_BUILD_CUDA)
// Native Linux NVENC encoder. This is the preferred `nvenc` backend and
// consumes the same CUDA NV12/P010 conversion surface as the legacy path.
encoder_t nvenc {
"nvenc"sv,
std::make_unique<encoder_platform_formats_nvenc>(
platf::mem_type_e::cuda,
platf::pix_fmt_e::nv12,
platf::pix_fmt_e::p010,
platf::pix_fmt_e::unknown,
platf::pix_fmt_e::unknown
),
{
{}, {}, {}, {}, {}, {}, "av1_nvenc"s,
},
{
{}, {}, {}, {}, {}, {}, "hevc_nvenc"s,
},
{
{}, {}, {}, {}, {}, {}, "h264_nvenc"s,
},
PARALLEL_ENCODING | REF_FRAMES_INVALIDATION | OWNER_THREAD_TEARDOWN
};
#endif
#ifdef _WIN32
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,
AV_PIX_FMT_VUYX,
AV_PIX_FMT_XV30,
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
{"profile"s, (int) qsv::profile_av1_e::main},
},
{
// HDR-specific options
{"profile"s, (int) qsv::profile_av1_e::main},
},
{
// YUV444 SDR-specific options
{"profile"s, (int) qsv::profile_av1_e::high},
},
{
// YUV444 HDR-specific options
{"profile"s, (int) qsv::profile_av1_e::high},
},
{}, // Fallback options
"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},
},
{
// YUV444 SDR-specific options
{"profile"s, (int) qsv::profile_hevc_e::rext},
},
{
// YUV444 HDR-specific options
{"profile"s, (int) qsv::profile_hevc_e::rext},
},
{
// Fallback options
{"low_power"s, []() {
return config::video.qsv.qsv_slow_hevc ? 0 : 1;
}},
},
"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
{
// YUV444 SDR-specific options
{"profile"s, (int) qsv::profile_h264_e::high_444p},
},
{}, // YUV444 HDR-specific options
{
// Fallback options
{"low_power"s, 0}, // Some old/low-end Intel GPUs don't support low power encoding
},
"h264_qsv"s,
},
PARALLEL_ENCODING | CBR_WITH_VBR | RELAXED_COMPLIANCE | NO_RC_BUF_LIMIT | YUV444_SUPPORT
};
// Experimental native AMD AMF encoder (src/amf/amf_d3d11.cpp). Bypasses the
// FFmpeg AMF wrapper for direct AMF SDK access: D3D11 zero-copy input, reference-frame
// invalidation and HDR metadata. Selecting amdvce_experimental is a strict native-AMF
// contract: feature, initialization, or runtime failures are reported instead
// of silently changing encoder implementations. It is excluded from automatic
// probing because hardware and driver coverage is currently limited.
encoder_t amdvce_experimental {
"amdvce_experimental"sv,
std::make_unique<encoder_platform_formats_amf>(
platf::mem_type_e::dxgi,
platf::pix_fmt_e::nv12,
platf::pix_fmt_e::p010,
platf::pix_fmt_e::unknown,
platf::pix_fmt_e::unknown
),
{
{}, // Common options (configured directly via AMF, not FFmpeg AVOptions)
{}, // SDR-specific options
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{}, // Fallback options
"av1_amf"s,
},
{
{},
{},
{},
{},
{},
{},
"hevc_amf"s,
},
{
{},
{},
{},
{},
{},
{},
"h264_amf"s,
},
// ASYNC_TEARDOWN: healthy session ends and runtime bitrate rebuilds detach
// the bounded destruction worker instead of stalling the encode thread for
// 100-300 ms; the lifecycle gate serializes it against the next session's
// initialization. Shutdown, display reinit, and runtime fatals still force
// an ordered synchronous teardown via the fail-guard's sync conditions.
PARALLEL_ENCODING | REF_FRAMES_INVALIDATION | ASYNC_TEARDOWN // flags
};
// Supported FFmpeg-based AMF encoder. Its explicit name identifies the
// implementation, and it is the AMD encoder selected by automatic probing.
encoder_t amdvce_ffmpeg {
"amdvce_ffmpeg"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,
AV_PIX_FMT_NONE,
AV_PIX_FMT_NONE,
dxgi_init_avcodec_hardware_input_buffer
),
{
// Common options
{
{"filler_data"s, false},
{"forced_idr"s, 1},
{"latency"s, "lowest_latency"s},
{"async_depth"s, 1},
{"skip_frame"s, 0},
{"log_to_dbg"s, []() {
return config::sunshine.min_log_level < 2 ? 1 : 0;
}},
{"preanalysis"s, []() {
return amf::lifecycle::resolve_preanalysis(
config::video.amd.amd_rc_av1,
config::video.amd.amd_preanalysis)
.enabled ? 1 : 0;
}},
{"quality"s, &config::video.amd.amd_quality_av1},
{"rc"s, &config::video.amd.amd_rc_av1},
{"aq_mode"s, encoder_t::option_t::optional_int_function_t {[]() -> std::optional<int> {
if (!amf::lifecycle::rate_control_supports_adaptive_quantization(config::video.amd.amd_rc_av1)) {
return 0;
}
if (!config::video.amd.amd_vbaq) return std::nullopt;
return *config::video.amd.amd_vbaq ? 1 : 0; // AMF AV1 CAQ / none
}}},
{"qvbr_quality_level"s, &config::video.amd.amd_qvbr_quality_level},
{"usage"s, &config::video.amd.amd_usage_av1},
{"enforce_hrd"s, &config::video.amd.amd_enforce_hrd},
},
{}, // SDR-specific options
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{}, // Fallback options
"av1_amf"s,
},
{
// Common options
{
{"filler_data"s, false},
{"forced_idr"s, 1},
{"latency"s, 1},
{"async_depth"s, 1},
{"skip_frame"s, 0},
{"log_to_dbg"s, []() {
return config::sunshine.min_log_level < 2 ? 1 : 0;
}},
{"gops_per_idr"s, 1},
{"header_insertion_mode"s, "idr"s},
{"preanalysis"s, []() {
return amf::lifecycle::resolve_preanalysis(
config::video.amd.amd_rc_hevc,
config::video.amd.amd_preanalysis)
.enabled ? 1 : 0;
}},
{"quality"s, &config::video.amd.amd_quality_hevc},
{"rc"s, &config::video.amd.amd_rc_hevc},
{"qvbr_quality_level"s, &config::video.amd.amd_qvbr_quality_level},
{"usage"s, &config::video.amd.amd_usage_hevc},
{"vbaq"s, encoder_t::option_t::optional_int_function_t {[]() -> std::optional<int> {
if (!amf::lifecycle::rate_control_supports_adaptive_quantization(config::video.amd.amd_rc_hevc)) {
return 0;
}
return config::video.amd.amd_vbaq;
}}},
{"enforce_hrd"s, &config::video.amd.amd_enforce_hrd},
{"level"s, [](const config_t &cfg) {
auto size = cfg.width * cfg.height;
// For 4K and below, try to use level 5.1 or 5.2 if possible
if (size <= 8912896) {
if (size * cfg.framerate <= 534773760) {
return "5.1"s;
} else if (size * cfg.framerate <= 1069547520) {
return "5.2"s;
}
}
return "auto"s;
}},
},
{}, // SDR-specific options
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{}, // Fallback options
"hevc_amf"s,
},
{
// Common options
{
{"filler_data"s, false},
{"forced_idr"s, 1},
{"latency"s, 1},
{"async_depth"s, 1},
{"frame_skipping"s, 0},
{"log_to_dbg"s, []() {
return config::sunshine.min_log_level < 2 ? 1 : 0;
}},
{"preanalysis"s, []() {
return amf::lifecycle::resolve_preanalysis(
config::video.amd.amd_rc_h264,
config::video.amd.amd_preanalysis)
.enabled ? 1 : 0;
}},
{"quality"s, &config::video.amd.amd_quality_h264},
{"rc"s, &config::video.amd.amd_rc_h264},
{"qvbr_quality_level"s, &config::video.amd.amd_qvbr_quality_level},
{"usage"s, &config::video.amd.amd_usage_h264},
{"vbaq"s, encoder_t::option_t::optional_int_function_t {[]() -> std::optional<int> {
if (!amf::lifecycle::rate_control_supports_adaptive_quantization(config::video.amd.amd_rc_h264)) {
return 0;
}
return config::video.amd.amd_vbaq;
}}},
{"enforce_hrd"s, &config::video.amd.amd_enforce_hrd},
},
{}, // SDR-specific options
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{
// Fallback options
{"usage"s, 2 /* AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY */}, // Workaround for https://github.com/GPUOpen-LibrariesAndSDKs/AMF/issues/410
},
"h264_amf"s,
},
// ASYNC_TEARDOWN: like NVENC, a hung AMF session must not stall the encoder thread on
// mid-stream reinit; the bounded sync teardown in encode_run covers shutdown/reinit.
PARALLEL_ENCODING | ASYNC_TEARDOWN
};
encoder_t mediafoundation {
"mediafoundation"sv,
std::make_unique<encoder_platform_formats_avcodec>(
AV_HWDEVICE_TYPE_D3D11VA,
AV_HWDEVICE_TYPE_NONE,
AV_PIX_FMT_D3D11,
AV_PIX_FMT_NV12, // SDR 4:2:0 8-bit (only format Qualcomm supports)
AV_PIX_FMT_NONE, // No HDR - Qualcomm MF only supports 8-bit
AV_PIX_FMT_NONE, // No YUV444 SDR
AV_PIX_FMT_NONE, // No YUV444 HDR
dxgi_init_avcodec_hardware_input_buffer
),
{
// Common options for AV1 - Qualcomm MF encoder
{
{"hw_encoding"s, 1},
{"rate_control"s, "cbr"s},
{"scenario"s, "display_remoting"s},
},
{}, // SDR-specific options
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{}, // Fallback options
"av1_mf"s,
},
{
// Common options for HEVC - Qualcomm MF encoder
{
{"hw_encoding"s, 1},
{"rate_control"s, "cbr"s},
{"scenario"s, "display_remoting"s},
},
{}, // SDR-specific options
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{}, // Fallback options
"hevc_mf"s,
},
{
// Common options for H.264 - Qualcomm MF encoder
{
{"hw_encoding"s, 1},
{"rate_control"s, "cbr"s},
{"scenario"s, "display_remoting"s},
},
{}, // SDR-specific options
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{}, // Fallback options
"h264_mf"s,
},
PARALLEL_ENCODING | FIXED_GOP_SIZE // MF encoder doesn't support on-demand IDR frames
};
#endif
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,
AV_PIX_FMT_YUV444P,
AV_PIX_FMT_YUV444P10,
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
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{}, // Fallback options
#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
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{}, // Fallback options
"libx265"s,
},
{
// Common options
{
{"preset"s, &config::video.sw.sw_preset},
{"tune"s, &config::video.sw.sw_tune},
},
{}, // SDR-specific options
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{}, // Fallback options
"libx264"s,
},
H264_ONLY | PARALLEL_ENCODING | ALWAYS_REPROBE | YUV444_SUPPORT
};
#if defined(__linux__) || defined(linux) || defined(__linux) || defined(__FreeBSD__)
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_P010,
AV_PIX_FMT_NONE,
AV_PIX_FMT_NONE,
vaapi_init_avcodec_hardware_input_buffer
),
{
// Common options
{
{"low_power"s, 1},
{"async_depth"s, 1},
{"idr_interval"s, std::numeric_limits<int>::max()},
},
{}, // SDR-specific options
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{
// Fallback options
{"low_power"s, 0}, // Not all VAAPI drivers expose LP entrypoints
},
"av1_vaapi"s,
},
{
// Common options
{
{"low_power"s, 1},
{"async_depth"s, 1},
{"sei"s, 0},
{"idr_interval"s, std::numeric_limits<int>::max()},
},
{}, // SDR-specific options
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{
// Fallback options
{"low_power"s, 0}, // Not all VAAPI drivers expose LP entrypoints
},
"hevc_vaapi"s,
},
{
// Common options
{
{"low_power"s, 1},
{"async_depth"s, 1},
{"sei"s, 0},
{"idr_interval"s, std::numeric_limits<int>::max()},
},
{}, // SDR-specific options
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{
// Fallback options
{"low_power"s, 0}, // Not all VAAPI drivers expose LP entrypoints
},
"h264_vaapi"s,
},
// RC buffer size will be set in platform code if supported
LIMITED_GOP_SIZE | PARALLEL_ENCODING | NO_RC_BUF_LIMIT
#if defined(__linux__) && defined(SUNSHINE_BUILD_VAAPI)
| OWNER_THREAD_TEARDOWN
#endif
};
#endif
#ifdef __APPLE__
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,
AV_PIX_FMT_NONE,
AV_PIX_FMT_NONE,
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},
{"max_ref_frames"s, 1},
},
{}, // SDR-specific options
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{}, // Fallback options
"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},
{"max_ref_frames"s, 1},
},
{}, // SDR-specific options
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{}, // Fallback options
"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},
{"max_ref_frames"s, 1},
},
{}, // SDR-specific options
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{
// Fallback options
{"flags"s, "-low_delay"},
},
"h264_videotoolbox"s,
},
DEFAULT
};
#endif
#ifdef SUNSHINE_BUILD_VULKAN
encoder_t vulkan {
"vulkan"sv,
std::make_unique<encoder_platform_formats_avcodec>(
AV_HWDEVICE_TYPE_VULKAN,
AV_HWDEVICE_TYPE_NONE,
AV_PIX_FMT_VULKAN,
AV_PIX_FMT_NV12,
AV_PIX_FMT_P010,
AV_PIX_FMT_NONE,
AV_PIX_FMT_NONE,
vulkan_init_avcodec_hardware_input_buffer
),
{
// Common options
{
{"idr_interval"s, std::numeric_limits<int>::max()},
{"tune"s, &config::video.vk.tune},
{"rc_mode"s, &config::video.vk.rc_mode},
{"units"s, 0},
{"usage"s, "stream"s},
{"content"s, "rendered"s},
},
{}, // SDR-specific options
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{}, // Fallback options
"av1_vulkan"s,
},
{
// Common options
{
{"idr_interval"s, std::numeric_limits<int>::max()},
{"tune"s, &config::video.vk.tune},
{"rc_mode"s, &config::video.vk.rc_mode},
{"units"s, 0},
{"usage"s, "stream"s},
{"content"s, "rendered"s},
},
{}, // SDR-specific options
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{}, // Fallback options
"hevc_vulkan"s,
},
{
// Common options
{
{"idr_interval"s, std::numeric_limits<int>::max()},
{"tune"s, &config::video.vk.tune},
{"rc_mode"s, &config::video.vk.rc_mode},
{"units"s, 0},
{"usage"s, "stream"s},
{"content"s, "rendered"s},
},
{}, // SDR-specific options
{}, // HDR-specific options
{}, // YUV444 SDR-specific options
{}, // YUV444 HDR-specific options
{}, // Fallback options
"h264_vulkan"s,
},
LIMITED_GOP_SIZE | PARALLEL_ENCODING
};
#endif
static const std::vector<encoder_t *> encoders {
#ifdef _WIN32
&nvenc,
#endif
#if defined(__linux__) && defined(SUNSHINE_BUILD_CUDA)
&nvenc,
&nvenc_legacy,
#elif defined(__linux__)
&nvenc_legacy,
#elif !defined(_WIN32) && !defined(__APPLE__)
&nvenc,
#endif
#ifdef _WIN32
&quicksync,
&amdvce_ffmpeg,
&amdvce_experimental,
&mediafoundation,
#endif
#if defined(__linux__) || defined(linux) || defined(__linux) || defined(__FreeBSD__)
#ifdef SUNSHINE_BUILD_VULKAN
&vulkan,
#endif
&vaapi,
#endif
#ifdef __APPLE__
&videotoolbox,
#endif
&software
};
static encoder_t *chosen_encoder;
int active_hevc_mode;
int active_av1_mode;
std::atomic_int active_pyrowave_mode {0};
bool last_encoder_probe_supported_ref_frames_invalidation = false;
std::array<bool, 3> last_encoder_probe_supported_yuv444_for_codec = {};
std::atomic<std::int64_t> last_negative_hdr_advertisement_probe_ns {0};
std::mutex encoder_probe_mutex;
namespace {
struct encoder_probe_status_t {
bool attempted = false;
bool successful = false;
advertised_encoder_capabilities_t advertised_capabilities;
};
encoder_probe_status_t encoder_probe_status_for_key(const probe_cache_key_t &key) {
auto &state = encoder_probe_cache_state();
std::lock_guard<std::mutex> lock(state.mutex);
const bool successful = state.valid && state.cache_key && *state.cache_key == key;
return encoder_probe_status_t {
.attempted = state.attempted_cache_key && *state.attempted_cache_key == key,
.successful = successful,
.advertised_capabilities = successful ?
state.advertised_capabilities :
advertised_encoder_capabilities_t {},
};
}
} // namespace
bool has_attempted_encoder_probe() {
const auto current_key = build_probe_cache_key();
return encoder_probe_status_for_key(current_key).attempted;
}
bool has_successful_encoder_probe() {
const auto current_key = build_probe_cache_key();
return encoder_probe_status_for_key(current_key).successful;
}
bool last_encoder_probe_failed() {
auto &state = encoder_probe_cache_state();
std::lock_guard<std::mutex> lock(state.mutex);
return state.failure_cache_key.has_value() && state.failure_count > 0;
}
#ifdef _WIN32
encoder_probe_adapter_hint_lease_t set_pending_virtual_display_adapter_hint(const LUID &adapter_luid) {
auto &state = encoder_probe_cache_state();
std::lock_guard<std::mutex> lock(state.mutex);
const auto lease = ++state.next_pending_virtual_display_adapter_hint_lease;
state.pending_virtual_display_adapter_hint = adapter_luid;
state.pending_virtual_display_adapter_hint_lease = lease;
state.pending_virtual_display_adapter_hint_ready_for_verification = false;
BOOST_LOG(debug)
<< "Published pending virtual-display adapter identity "
<< luid_cache_identity(adapter_luid)
<< " for encoder capability matching (lease=" << lease << ").";
return lease;
}
bool mark_pending_virtual_display_adapter_hint_ready_for_verification(
const encoder_probe_adapter_hint_lease_t lease
) {
auto &state = encoder_probe_cache_state();
std::lock_guard<std::mutex> lock(state.mutex);
if (lease == 0 || state.pending_virtual_display_adapter_hint_lease != lease) {
return false;
}
state.pending_virtual_display_adapter_hint_ready_for_verification = true;
BOOST_LOG(debug)
<< "Pending virtual-display adapter identity is ready for WGC verification (lease="
<< lease << ").";
return true;
}
bool clear_pending_virtual_display_adapter_hint(const encoder_probe_adapter_hint_lease_t lease) {
auto &state = encoder_probe_cache_state();
std::lock_guard<std::mutex> lock(state.mutex);
if (lease == 0 || state.pending_virtual_display_adapter_hint_lease != lease) {
return false;
}
state.pending_virtual_display_adapter_hint.reset();
state.pending_virtual_display_adapter_hint_lease = 0;
state.pending_virtual_display_adapter_hint_ready_for_verification = false;
BOOST_LOG(debug)
<< "Cleared pending virtual-display adapter identity after display publication (lease="
<< lease << ").";
return true;
}
#endif
advertised_encoder_capabilities_t advertised_encoder_capabilities(
const bool probe_before_negative,
bool *probe_complete
) {
auto current_key = build_probe_cache_key();
auto probe_status = encoder_probe_status_for_key(current_key);
const auto refresh_probe_status = [&]() {
current_key = build_probe_cache_key();
probe_status = encoder_probe_status_for_key(current_key);
};
if (probe_before_negative && !probe_status.successful && !probe_status.attempted) {
BOOST_LOG(info) << "Encoder capabilities are unprobed for the current adapter identity; probing encoders now.";
if (probe_encoders()) {
BOOST_LOG(warning) << "Encoder probe failed before HTTP capability advertisement; reporting current encoder capabilities.";
}
refresh_probe_status();
} else if (probe_before_negative && !probe_status.successful && probe_status.attempted) {
const auto now = std::chrono::steady_clock::now();
const auto now_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(now.time_since_epoch()).count();
const auto last_ns = last_negative_hdr_advertisement_probe_ns.load(std::memory_order_acquire);
const auto retry_interval = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::seconds(10)).count();
if (last_ns <= 0 || now_ns - last_ns >= retry_interval) {
last_negative_hdr_advertisement_probe_ns.store(now_ns, std::memory_order_release);
BOOST_LOG(info) << "Encoder capabilities lack a successful probe for the current adapter identity; re-probing before advertisement.";
if (probe_encoders()) {
BOOST_LOG(warning) << "Encoder re-probe failed before HTTP capability advertisement; reporting current encoder capabilities.";
}
refresh_probe_status();
}
}
// Capability globals describe the last successful probe, which may belong
// to a different adapter key. Never publish those stale positive values
// when the current identity has not produced a successful probe.
if (probe_complete) {
*probe_complete = probe_status.successful;
}
if (!probe_status.successful) {
log_probe_cache_miss(current_key);
return {};
}
return probe_status.advertised_capabilities;
}
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,
const std::optional<platf::adapter_id_t> &required_adapter = std::nullopt
) {
// After a recent display-helper APPLY (topology change), the display subsystem
// may need time to settle. Use more retries with progressive delays.
int max_attempts = 2;
std::chrono::milliseconds base_delay = 200ms;
#ifdef _WIN32
// The extended ladder only exists for the window right after an APPLY, and a
// bounded stream start has to spend that window inside the same budget the
// client's first-video deadline uses. Outside the window nothing changes.
std::optional<std::chrono::steady_clock::time_point> settle_deadline;
const auto ms_since_apply = display_helper_integration::ms_since_last_apply();
if (ms_since_apply < 5000) {
const auto stream_start_budget = display_helper_integration::remaining_stream_start_budget();
if (!stream_start_budget || *stream_start_budget > 0ms) {
max_attempts = 5;
base_delay = 300ms;
}
if (stream_start_budget) {
settle_deadline = std::chrono::steady_clock::now() + *stream_start_budget;
}
}
#endif
for (int x = 0; x < max_attempts; ++x) {
disp.reset();
disp = platf::display(type, display_name, config, required_adapter);
if (disp) {
break;
}
// The capture code depends on us to sleep between failures.
// Use progressive delays for topology changes to give the display time to settle.
auto delay = base_delay + std::chrono::milliseconds(x * 100);
#ifdef _WIN32
if (settle_deadline) {
const auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(*settle_deadline - std::chrono::steady_clock::now());
delay = std::max(0ms, std::min(delay, remaining));
}
#endif
std::this_thread::sleep_for(delay);
}
}
/**
* @brief Update the list of display names before or during a stream.
* @details This will attempt to keep `current_display_index` pointing at the same display.
* @param dev_type The encoder device type used for display lookup.
* @param display_names The list of display names to repopulate.
* @param current_display_index The current display index or -1 if not yet known.
*/
void refresh_displays(
platf::mem_type_e dev_type,
std::vector<std::string> &display_names,
int ¤t_display_index,
std::string &preferred_display_name,
const std::optional<std::string> &required_output = std::nullopt,
const bool require_exact_output = false,
const bool log_failure = true
) {
if (require_exact_output) {
display_names = platf::display_names(dev_type);
if (!required_output || required_output->empty()) {
BOOST_LOG(error) << "Remote monitor capture has no assigned output; refusing global display selection.";
display_names.clear();
current_display_index = -1;
return;
}
const auto exact = std::find_if(display_names.begin(), display_names.end(), [&](const auto &candidate) {
return boost::iequals(candidate, *required_output);
});
if (exact == display_names.end()) {
// This is a topology-owned target. Do not reuse a previous list or
// index zero because that can silently capture a physical display.
display_names.clear();
current_display_index = -1;
return;
}
current_display_index = static_cast<int>(std::distance(display_names.begin(), exact));
return;
}
// It is possible that the output name may be empty even if it wasn't before (device disconnected) or vice-versa
const auto runtime_output_override = config::runtime_output_name_override();
const bool has_runtime_output_override = runtime_output_override.has_value();
const auto output_name = display_device::map_output_name(config::get_active_output_name());
std::string current_display_name;
auto names_match = [](const std::string &lhs, const std::string &rhs) {
return boost::iequals(lhs, rhs);
};
// If we have a current display index, let's start with that
if (current_display_name.empty() && current_display_index >= 0 && current_display_index < display_names.size()) {
current_display_name = display_names.at(current_display_index);
}
// Refresh the display names
auto old_display_names = std::move(display_names);
display_names = platf::display_names(dev_type);
// If we now have no displays, let's put the old display array back and fail
if (display_names.empty() && !old_display_names.empty()) {
#ifdef _WIN32
// During a topology change (e.g. display helper just applied ensure_only_display),
// DXGI may temporarily report no displays. Don't fall back to stale names that
// include now-disabled physical displays — this causes the reinit loop to waste
// time trying to init on unavailable outputs. Instead, use the configured output
// name so the reinit loop targets the correct display.
const auto ms_since_apply = display_helper_integration::ms_since_last_apply();
if (ms_since_apply < 5000 && !output_name.empty()) {
if (log_failure) {
BOOST_LOG(info) << "No displays found after reenumeration during topology change; "
<< "using configured output ["sv << output_name << "] instead of stale list"sv;
}
display_names.clear();
display_names.emplace_back(output_name);
} else {
if (log_failure) {
BOOST_LOG(error) << "No displays were found after reenumeration; retrying with backoff."sv;
}
display_names = std::move(old_display_names);
return;
}
#else
if (log_failure) {
BOOST_LOG(error) << "No displays were found after reenumeration; retrying with backoff."sv;
}
display_names = std::move(old_display_names);
return;
#endif
} else if (display_names.empty()) {
display_names.emplace_back(output_name);
}
// We now have a new display name list, so reset the index back to 0
current_display_index = 0;
if (has_runtime_output_override && !output_name.empty()) {
for (int x = 0; x < display_names.size(); ++x) {
if (names_match(display_names[x], output_name)) {
current_display_index = x;
return;
}
}
BOOST_LOG(warning) << "Runtime display override [" << *runtime_output_override
<< "] mapped to [" << output_name
<< "] but was not found in the capture display list";
}
if (current_display_name.empty()) {
current_display_name = display_device::map_output_name(config::video.output_name);
}
// If we had a name previously, let's try to find it in the new list
if (!current_display_name.empty()) {
for (int x = 0; x < display_names.size(); ++x) {
if (names_match(display_names[x], current_display_name)) {
current_display_index = x;
return;
}
}
// The old display was removed, so we'll start back at the first display again
BOOST_LOG(warning) << "Previous active display ["sv << current_display_name << "] is no longer present"sv;
// If the previous display disappeared, prefer moving back to configured output before
// defaulting to index 0 (often primary physical display during transient display churn).
if (!output_name.empty()) {
for (int x = 0; x < display_names.size(); ++x) {
if (names_match(display_names[x], output_name)) {
current_display_index = x;
return;
}
}
}
} else {
for (int x = 0; x < display_names.size(); ++x) {
if (names_match(display_names[x], output_name)) {
current_display_index = x;
return;
}
}
}
}
void refresh_displays(
platf::mem_type_e dev_type,
std::vector<std::string> &display_names,
int ¤t_display_index,
const std::optional<std::string> &required_output,
const bool require_exact_output = false,
const bool log_failure = true
) {
static std::string empty_preferred_display_name;
refresh_displays(
dev_type,
display_names,
current_display_index,
empty_preferred_display_name,
required_output,
require_exact_output,
log_failure
);
}
void refresh_displays(platf::mem_type_e dev_type, std::vector<std::string> &display_names, int ¤t_display_index) {
static std::string empty_str = "";
refresh_displays(dev_type, display_names, current_display_index, empty_str, std::nullopt);
}
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);
std::uint64_t switch_display_generation {switch_display_event->generation()};
std::optional<std::string> pending_switch_output;
// 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));
const auto manual_switch_selection =
capture_ctxs.front().config.capture_source == capture_source_e::active_output ?
video::policy::capture_selection_e::process_preferred :
video::policy::capture_selection_e::exact_output;
std::vector<std::string> display_names;
int display_p = -1;
std::shared_ptr<platf::display_t> disp;
std::size_t display_retry_failures = 0;
while (capture_ctx_queue->running()) {
const auto &capture_config = capture_ctxs.front().config;
if (capture_config.capture_source == capture_source_e::synthetic_black) {
disp = make_black_display(capture_config);
if (disp) break;
BOOST_LOG(error) << "Synthetic black capture is unavailable for the selected encoder backend.";
return;
}
const auto exact_output = capture_config.capture_source == capture_source_e::exact_output;
const auto required_output = exact_output ? capture_config.capture_output : std::nullopt;
const bool log_display_retry = policy::should_log_display_retry(display_retry_failures);
refresh_displays(encoder.platform_formats->dev_type, display_names, display_p, required_output, exact_output, log_display_retry);
const bool allow_process_display_preference = video::policy::may_apply_process_display_preference(
capture_config.capture_source == capture_source_e::active_output ?
video::policy::capture_selection_e::process_preferred :
video::policy::capture_selection_e::exact_output
);
if (!ensure_virtual_display_ready(display_names, display_p, allow_process_display_preference)) {
wait_before_display_retry(display_retry_failures);
continue;
}
if (auto requested = switch_display_event->view_if_newer(switch_display_generation)) {
pending_switch_output = video::policy::select_manual_display_output(
manual_switch_selection,
*requested,
display_names
);
}
if (pending_switch_output) {
if (auto requested_index = video::policy::resolve_display_output(*pending_switch_output, display_names)) {
display_p = *requested_index;
}
pending_switch_output.reset();
}
if (log_display_retry) {
BOOST_LOG(info) << "Capture worker selecting source=" << static_cast<int>(capture_config.capture_source)
<< " output='" << display_names[display_p] << "'.";
}
disp = platf::display(encoder.platform_formats->dev_type, display_names[display_p], capture_ctxs.front().config);
if (disp) {
break;
}
wait_before_display_retry(display_retry_failures);
}
if (!disp) {
return;
}
display_wp = disp;
constexpr auto capture_buffer_size = 12;
std::list<std::shared_ptr<platf::img_t>> imgs(capture_buffer_size);
uint64_t image_pool_wait_count = 0;
std::vector<std::optional<std::chrono::steady_clock::time_point>> imgs_used_timestamps;
auto release_image_pool = [&]() {
#ifdef _WIN32
std::vector<std::shared_ptr<platf::img_t>> d3d_images;
#endif
for (auto &img : imgs) {
if (!img) {
continue;
}
#ifdef _WIN32
if (is_d3d_capture_image(img)) {
d3d_images.emplace_back(std::move(img));
continue;
}
#endif
img.reset();
}
#ifdef _WIN32
release_d3d_capture_images_async(std::move(d3d_images));
#endif
imgs_used_timestamps.clear();
};
auto release_image_pool_guard = util::fail_guard([&]() {
release_image_pool();
});
const std::chrono::seconds trim_timeot = 3s;
auto trim_imgs = [&]() {
#ifdef _WIN32
if (std::any_of(std::begin(imgs), std::end(imgs), [](const auto &img) {
return img && is_d3d_capture_image(img);
})) {
return;
}
#endif
// 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();
std::optional<std::chrono::steady_clock::time_point> wait_start;
uint32_t wait_iterations = 0;
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) {
if (wait_start) {
const auto wait_ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - *wait_start).count();
++image_pool_wait_count;
if (image_pool_wait_count <= 5 || wait_ms > 1.5 || image_pool_wait_count % 120 == 0) {
BOOST_LOG(debug) << "Capture image pool waited " << wait_ms
<< "ms for a free image"
<< " iterations=" << wait_iterations
<< " count=" << image_pool_wait_count;
}
}
// trim allocated but unused portion of the pool based on timeouts
trim_imgs();
img_out->frame_timestamp.reset();
img_out->host_processing_timestamp.reset();
img_out->capture_pacing_timestamp.reset();
return true;
} else {
if (!wait_start) {
wait_start = std::chrono::steady_clock::now();
}
++wait_iterations;
// sleep and retry if image pool is full
std::this_thread::sleep_for(1ms);
}
}
return false;
};
// Capture takes place on this thread
platf::set_thread_name("video::capture");
platf::adjust_thread_priority(platf::thread_priority_e::critical);
while (capture_ctx_queue->running()) {
bool artificial_reinit = false;
const bool event_driven_capture = disp->is_event_driven_capture();
auto push_captured_image_callback = [&](std::shared_ptr<platf::img_t> &&img, bool frame_captured) -> bool {
if (event_driven_capture) {
bool new_context_needs_frame = false;
while (auto pending_context = capture_ctx_queue->pop(0ms)) {
auto new_context = std::move(*pending_context);
if (!frame_captured && new_context.images->running()) {
new_context_needs_frame = true;
}
capture_ctxs.emplace_back(std::move(new_context));
}
if (new_context_needs_frame) {
disp->request_refresh();
}
}
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;
}
if (!event_driven_capture) {
while (auto pending_context = capture_ctx_queue->pop(0ms)) {
capture_ctxs.emplace_back(std::move(*pending_context));
}
} else if (capture_ctxs.empty()) {
return false;
}
if (auto requested = switch_display_event->view_if_newer(switch_display_generation)) {
pending_switch_output = video::policy::select_manual_display_output(
manual_switch_selection,
*requested,
display_names
);
if (*requested < 0 || pending_switch_output) {
artificial_reinit = true;
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);
disp->prepare_for_reinit();
// Some classes of images contain references to the display --> display won't delete unless img is deleted.
//
// D3D capture images additionally own cross-device SHARED keyed-mutex surfaces
// that the per-client encoder devices still have open. Freeing them now (the old
// behavior, on a detached thread) races the encoders' teardown and the GPU's
// deferred eviction during the mode change, which can bugcheck the kernel video
// memory manager (VIDEO_MEMORY_MANAGEMENT_INTERNAL 0x10e) when a second client is
// connected. These surfaces hold no display reference, so deferring them does not
// block the use_count() wait below; hold them until every encoder has dropped its
// display reference, then free them once no device still has them open.
#ifdef _WIN32
std::vector<std::shared_ptr<platf::img_t>> deferred_d3d_images;
for (auto &img : imgs) {
if (img && is_d3d_capture_image(img)) {
deferred_d3d_images.emplace_back(std::move(img));
}
}
#endif
release_image_pool();
// 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) {
// If capture is being torn down (stream/session ending), stop waiting on
// encoder threads that may still hold a display reference while blocked in a
// slow or hung driver call. Spinning here until those threads are force-joined
// is what lets a wedged reinit escalate into the 10s teardown watchdog crash.
if (!capture_ctx_queue->running()) {
#ifdef _WIN32
// Don't block this bail-out on a synchronous D3D teardown; hand the deferred
// surfaces to the async releaser as the old code path did.
release_d3d_capture_images_async(std::move(deferred_d3d_images));
#endif
return;
}
// 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->pop(0ms)) {}
++capture_ctx;
});
std::this_thread::sleep_for(20ms);
}
#ifdef _WIN32
// Every encoder device has now released the shared capture surfaces (their
// display references reached zero above), so it is safe to free them. Done
// synchronously on this thread rather than on a detached thread, so no surface
// is freed while another device still references it or while it is still queued
// for GPU eviction during the mode change. Take the teardown mutex so this free
// cannot overlap an async d3dRelease thread still draining a previous generation.
{
std::lock_guard lg {encode_session_teardown_mutex};
deferred_d3d_images.clear();
}
#endif
while (auto pending_context = capture_ctx_queue->pop(0ms)) {
if (pending_context->images->running()) {
capture_ctxs.emplace_back(std::move(*pending_context));
}
}
if (capture_ctxs.empty()) {
return;
}
std::size_t display_retry_failures = 0;
while (capture_ctx_queue->running()) {
// Release the display before reenumerating displays, since some capture backends
// only support a single display session per device/application.
disp.reset();
#ifdef _WIN32
wait_for_recent_display_apply_stability();
#endif
// Refresh display names since a display removal might have caused the reinitialization
const auto exact_output = capture_ctxs.front().config.capture_source == capture_source_e::exact_output;
const auto required_output = exact_output ? capture_ctxs.front().config.capture_output : std::nullopt;
refresh_displays(encoder.platform_formats->dev_type, display_names, display_p, proc::proc.display_name, required_output, exact_output, policy::should_log_display_retry(display_retry_failures));
const bool allow_process_display_preference = video::policy::may_apply_process_display_preference(
capture_ctxs.front().config.capture_source == capture_source_e::active_output ?
video::policy::capture_selection_e::process_preferred :
video::policy::capture_selection_e::exact_output
);
if (!ensure_virtual_display_ready(display_names, display_p, allow_process_display_preference)) {
wait_before_display_retry(display_retry_failures);
continue;
}
// Process any pending display switch with the new list of displays.
// Negative values mean "reinit only; keep display selection logic intact".
if (auto requested = switch_display_event->view_if_newer(switch_display_generation)) {
pending_switch_output = video::policy::select_manual_display_output(
manual_switch_selection,
*requested,
display_names
);
}
if (pending_switch_output) {
if (auto requested_index = video::policy::resolve_display_output(*pending_switch_output, display_names)) {
display_p = *requested_index;
}
pending_switch_output.reset();
}
// reset_display() will sleep between retries
reset_display(disp, encoder.platform_formats->dev_type, display_names[display_p], capture_ctxs.front().config);
if (disp) {
if (!exact_output) {
proc::proc.display_name = display_names[display_p];
}
break;
}
wait_before_display_retry(display_retry_failures);
}
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,
std::optional<std::chrono::steady_clock::time_point> capture_timestamp,
std::optional<std::chrono::steady_clock::time_point> host_processing_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 (av_packet->flags & AV_PKT_FLAG_KEY) {
BOOST_LOG(debug) << "Frame "sv << frame_nr << ": IDR Keyframe (AV_FRAME_FLAG_KEY)"sv;
}
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->capture_timestamp = capture_timestamp ? capture_timestamp : frame_timestamp;
packet->host_processing_timestamp = host_processing_timestamp;
}
packet->replacements = &session.replacements;
packet->channel_data = channel_data;
if (webrtc_stream::has_active_sessions()) {
webrtc_stream::submit_video_packet(*packet);
}
packet->packet_enqueue_timestamp = std::chrono::steady_clock::now();
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,
std::optional<std::chrono::steady_clock::time_point> capture_timestamp,
std::optional<std::chrono::steady_clock::time_point> host_processing_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;
packet->capture_timestamp = capture_timestamp ? capture_timestamp : frame_timestamp;
packet->host_processing_timestamp = host_processing_timestamp;
if (webrtc_stream::has_active_sessions()) {
webrtc_stream::submit_video_packet(*packet);
}
packet->packet_enqueue_timestamp = std::chrono::steady_clock::now();
packets->raise(std::move(packet));
return 0;
}
void deliver_amf_frames(
int64_t submitted_frame_nr,
amf_encode_session_t &session,
std::vector<amf::amf_encoded_frame> &encoded_frames,
safe::mail_raw_t::queue_t<packet_t> &packets,
void *channel_data
) {
for (auto &encoded_frame : encoded_frames) {
if (encoded_frame.data.empty()) {
continue;
}
// frame_nr != frame_index is expected when this batch is catching up after a
// transient delay. Only a non-monotonic emitted index is a real desync.
if (!session.note_emitted_index(encoded_frame.frame_index)) {
BOOST_LOG(warning) << "AMF emitted frame index regression: " << encoded_frame.frame_index
<< " (submitted " << submitted_frame_nr << ")";
}
// Stamp every packet with the timestamps of the frame it actually carries,
// including earlier frames drained in the same catch-up batch.
const auto ts = session.take_frame_timestamps(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 = ts.frame_timestamp;
packet->capture_timestamp = ts.capture_timestamp ? ts.capture_timestamp : ts.frame_timestamp;
packet->host_processing_timestamp = ts.host_processing_timestamp;
if (webrtc_stream::has_active_sessions()) {
webrtc_stream::submit_video_packet(*packet);
}
packet->packet_enqueue_timestamp = std::chrono::steady_clock::now();
packets->raise(std::move(packet));
}
}
int encode_amf(
int64_t frame_nr,
amf_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,
std::optional<std::chrono::steady_clock::time_point> capture_timestamp,
std::optional<std::chrono::steady_clock::time_point> host_processing_timestamp
) {
// Stash this frame's timestamps before submitting; the encoder is pipelined and
// emits an earlier frame, so the packet is stamped from this map by emitted index.
session.store_frame_timestamps((uint64_t) frame_nr, {frame_timestamp, capture_timestamp, host_processing_timestamp});
auto encode_result = session.encode_frames(frame_nr);
auto &encoded_frames = encode_result.frames;
if (!encode_result.input_accepted) {
session.discard_frame_timestamps((uint64_t) frame_nr);
}
if (encode_result.fatal || std::any_of(encoded_frames.begin(), encoded_frames.end(), [](const auto &frame) { return frame.fatal; })) {
BOOST_LOG(error) << "AMF encoder entered an unrecoverable state, requesting reinit";
return -1;
}
if (encoded_frames.empty()) {
// No output this call (pipeline still filling or transient stall); not fatal.
// The stashed timestamps stay until this frame is actually emitted.
return 0;
}
deliver_amf_frames(frame_nr, session, encoded_frames, packets, channel_data);
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,
std::optional<std::chrono::steady_clock::time_point> capture_timestamp,
std::optional<std::chrono::steady_clock::time_point> host_processing_timestamp
) {
thread_local logging::min_max_avg_periodic_logger<double> encode_duration_logger(debug, "Video encode call duration", "ms");
const auto encode_start = std::chrono::steady_clock::now();
int result = -1;
if (auto avcodec_session = dynamic_cast<avcodec_encode_session_t *>(&session)) {
result = encode_avcodec(frame_nr, *avcodec_session, packets, channel_data, frame_timestamp, capture_timestamp, host_processing_timestamp);
} else if (auto nvenc_session = dynamic_cast<nvenc_encode_session_t *>(&session)) {
result = encode_nvenc(frame_nr, *nvenc_session, packets, channel_data, frame_timestamp, capture_timestamp, host_processing_timestamp);
} else if (auto amf_session = dynamic_cast<amf_encode_session_t *>(&session)) {
result = encode_amf(frame_nr, *amf_session, packets, channel_data, frame_timestamp, capture_timestamp, host_processing_timestamp);
}
encode_duration_logger.collect_and_log(std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - encode_start).count());
return result;
}
#ifdef SUNSHINE_ENABLE_NV_TRUEHDR
/**
* @brief Synthesize Rec.2020/D65 HDR10 metadata for an RTX HDR (SDR->HDR) stream.
*
* RTX HDR captures an SDR source and post-processes it to HDR, so the capture display's own
* luminance metadata is irrelevant -- the client (and the encoded SEI) must advertise the HDR
* volume the TrueHDR conversion targets, not whatever the panel reports. Shared by every
* HDR-metadata producer so the bitstream SEI and the control-channel metadata stay consistent.
*/
SS_HDR_METADATA synthesize_rtx_hdr_metadata(const int peak_nits) {
const auto safe_peak_nits = static_cast<uint16_t>(std::clamp(peak_nits, 400, 2000));
SS_HDR_METADATA m {};
m.displayPrimaries[0] = {35400, 14600}; // R (Rec.2020, normalized to 50000)
m.displayPrimaries[1] = {8500, 39850}; // G
m.displayPrimaries[2] = {6550, 2300}; // B
m.whitePoint = {15635, 16450}; // D65
m.maxDisplayLuminance = safe_peak_nits; // nits
m.minDisplayLuminance = 1; // 1/10000th nit (~0)
m.maxContentLightLevel = safe_peak_nits; // nits
m.maxFrameAverageLightLevel = safe_peak_nits / 4; // nits
return m;
}
#endif
// Raise hdr_info unless it matches what this session last raised. Some clients
// (moonlight-xbox) perform a full HDMI display mode-set for every HDR mode message,
// so a redundant one on reinit costs seconds of black screen mid-stream.
void raise_hdr_info_if_changed(safe::mail_raw_t::event_t<hdr_info_t> &event, std::optional<hdr_info_raw_t> &last_hdr_info, hdr_info_t hdr_info) {
if (last_hdr_info && last_hdr_info->enabled == hdr_info->enabled &&
std::memcmp(&last_hdr_info->metadata, &hdr_info->metadata, sizeof(hdr_info->metadata)) == 0) {
return;
}
last_hdr_info = *hdr_info;
event->raise(std::move(hdr_info));
}
#ifdef SUNSHINE_ENABLE_NV_TRUEHDR
bool refresh_rtx_hdr_metadata_if_needed(
config_t &config,
encode_session_t *encode_session,
safe::mail_raw_t::event_t<hdr_info_t> &hdr_event,
std::optional<hdr_info_raw_t> &last_hdr_info,
rtx_hdr_metadata_refresh_state_t &refresh_state
) {
using namespace std::chrono_literals;
if (!config.rtx_hdr_active) {
return false;
}
const auto live_state = platf::rtx_hdr::live_output_metadata_state();
if (live_state.generation == 0) {
return false;
}
const auto now = std::chrono::steady_clock::now();
if (live_state.generation != refresh_state.observed_generation) {
refresh_state.observed_generation = live_state.generation;
const int new_peak_nits = std::clamp(live_state.peak_brightness, 400, 2000);
if (refresh_state.pending_peak_nits != new_peak_nits) {
refresh_state.pending_peak_nits = new_peak_nits;
refresh_state.stable_since = now;
}
}
// Live sliders may publish several intermediate values while being dragged. Wait
// for the peak to settle so clients that mode-set on HDR messages see one update.
if (refresh_state.pending_peak_nits == 0 || now - refresh_state.stable_since < 250ms) {
return false;
}
const int peak_nits = refresh_state.pending_peak_nits;
refresh_state.pending_peak_nits = 0;
if (config.rtx_hdr_peak_nits == peak_nits) {
return false;
}
config.rtx_hdr_peak_nits = peak_nits;
const auto metadata = synthesize_rtx_hdr_metadata(peak_nits);
if (encode_session) encode_session->set_hdr_metadata(metadata);
raise_hdr_info_if_changed(
hdr_event,
last_hdr_info,
std::make_unique<hdr_info_raw_t>(true, metadata)
);
BOOST_LOG(info) << "RTX HDR: updated live HDR10 metadata to peak " << peak_nits << " nits; forcing IDR.";
return true;
}
#endif
#ifdef _WIN32
struct amf_main10_compatibility_override_t {
avcodec_buffer_t device_ref;
AVAMFDeviceContext *device_context;
std::int64_t runtime_version;
};
// AMF packs a version as major << 48 | minor << 32 | release << 16 | build, with each
// field 16 bits wide (see AMF/core/Version.h). Decode it so logs are human-readable.
static std::string describe_amf_version(std::int64_t version) {
std::ostringstream description;
description << AMF_GET_MAJOR_VERSION(version) << '.'
<< AMF_GET_MINOR_VERSION(version) << '.'
<< AMF_GET_SUBMINOR_VERSION(version) << '.'
<< AMF_GET_BUILD_VERSION(version);
return description.str();
}
// FFmpeg exposes the AMF runtime version only once a device context exists, and that
// context is derived for the validated adapter alone. Query the runtime directly so
// every AMD HEVC Main10 attempt records the version the decision was made against,
// whichever way the decision goes.
static void log_amf_runtime_version(const DXGI_ADAPTER_DESC &adapter_desc) {
HMODULE amfrt = LoadLibraryW(AMF_DLL_NAME);
if (!amfrt) {
BOOST_LOG(info) << "AMF Main10 override: " << AMF_DLL_NAMEA << " could not be loaded; AMF runtime version unknown.";
return;
}
auto unload_amfrt = util::fail_guard([amfrt]() {
FreeLibrary(amfrt);
});
auto query_version = (AMFQueryVersion_Fn) GetProcAddress(amfrt, AMF_QUERY_VERSION_FUNCTION_NAME);
amf_uint64 version = 0;
if (!query_version || query_version(&version) != AMF_OK) {
BOOST_LOG(info) << "AMF Main10 override: " << AMF_QUERY_VERSION_FUNCTION_NAME
<< "() unavailable or failed; AMF runtime version unknown.";
return;
}
BOOST_LOG(info) << "AMF Main10 override: AMF runtime version "
<< describe_amf_version(static_cast<std::int64_t>(version)) << " on adapter "
<< std::hex << adapter_desc.VendorId << ':' << adapter_desc.DeviceId << std::dec << '.';
}
static std::optional<amf_main10_compatibility_override_t> enable_amf_main10_compatibility_override(
AVCodecContext *codec_context,
AVBufferRef *d3d_device_ref
) {
constexpr UINT kAmdVendorId = 0x1002;
constexpr UINT kRadeonPro5500XtDeviceId = 0x7340;
// Only the low 16 bits of the packed version hold the build number, so masking them
// off compares major.minor.release alone. FFmpeg's amfenc P010 guard is a blanket
// ">= 1.4.32" check, so every 1.4.31.x build is refused identically and needs the
// same override; masking keeps 1.4.31.5 from silently falling through. Runtimes at
// 1.4.32.0 or newer mask to a different value and are correctly left alone.
constexpr std::int64_t kBuildFieldMask = 0xFFFF;
constexpr std::int64_t kCompatibleRuntime = AMF_MAKE_FULL_VERSION(1, 4, 31, 0);
constexpr std::int64_t kFfmpegMain10Minimum = AMF_MAKE_FULL_VERSION(1, 4, 32, 0);
if (!codec_context || !d3d_device_ref || !d3d_device_ref->data) {
return std::nullopt;
}
auto *device_context = reinterpret_cast<AVHWDeviceContext *>(d3d_device_ref->data);
if (device_context->type != AV_HWDEVICE_TYPE_D3D11VA) {
return std::nullopt;
}
auto *d3d_context = reinterpret_cast<AVD3D11VADeviceContext *>(device_context->hwctx);
if (!d3d_context || !d3d_context->device) {
return std::nullopt;
}
platf::dxgi::dxgi_t dxgi_device;
using dxgi_adapter_t = util::safe_ptr<IDXGIAdapter, platf::dxgi::Release<IDXGIAdapter>>;
dxgi_adapter_t dxgi_adapter;
if (FAILED(d3d_context->device->QueryInterface(IID_PPV_ARGS(&dxgi_device))) ||
FAILED(dxgi_device->GetAdapter(&dxgi_adapter))) {
return std::nullopt;
}
DXGI_ADAPTER_DESC adapter_desc {};
if (FAILED(dxgi_adapter->GetDesc(&adapter_desc))) {
return std::nullopt;
}
log_amf_runtime_version(adapter_desc);
// Fail closed on anything but the one adapter this workaround has been validated
// against, before an AMF device context is derived.
if (adapter_desc.VendorId != kAmdVendorId || adapter_desc.DeviceId != kRadeonPro5500XtDeviceId) {
BOOST_LOG(info) << "AMF Main10 override: adapter " << std::hex << adapter_desc.VendorId << ':'
<< adapter_desc.DeviceId << std::dec
<< " is not the validated Radeon Pro 5500 XT (1002:7340); leaving FFmpeg's AMF Main10 gate untouched.";
return std::nullopt;
}
avcodec_buffer_t amf_device_ref;
if (auto status = av_hwdevice_ctx_create_derived(&amf_device_ref, AV_HWDEVICE_TYPE_AMF, d3d_device_ref, 0)) {
char err_str[AV_ERROR_MAX_STRING_SIZE] {0};
BOOST_LOG(warning) << "AMF Main10 override: failed to derive AMF device context for Radeon Pro 5500 XT Main10 compatibility: "
<< av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, status);
return std::nullopt;
}
auto *amf_hw_device = reinterpret_cast<AVHWDeviceContext *>(amf_device_ref->data);
if (!amf_hw_device || amf_hw_device->type != AV_HWDEVICE_TYPE_AMF) {
return std::nullopt;
}
auto *amf_context = reinterpret_cast<AVAMFDeviceContext *>(amf_hw_device->hwctx);
if (!amf_context) {
return std::nullopt;
}
const auto runtime_version = amf_context->version;
if ((runtime_version & ~kBuildFieldMask) != kCompatibleRuntime) {
BOOST_LOG(info) << "AMF Main10 override: AMF runtime " << describe_amf_version(runtime_version)
<< " is outside the targeted 1.4.31.x range (FFmpeg requires "
<< describe_amf_version(kFfmpegMain10Minimum)
<< " for P010); leaving FFmpeg's AMF Main10 gate untouched.";
return std::nullopt;
}
codec_context->hw_device_ctx = av_buffer_ref(amf_device_ref.get());
if (!codec_context->hw_device_ctx) {
return std::nullopt;
}
// FFmpeg applies its 1.4.32 guard to every P010 encoder, but issue artifacts
// prove this exact adapter/runtime tuple can stream HEVC Main10. Present the
// guarded version only during codec validation, then restore the real value.
amf_context->version = kFfmpegMain10Minimum;
BOOST_LOG(info) << "AMF Main10 override: reporting AMF " << describe_amf_version(kFfmpegMain10Minimum)
<< " to FFmpeg during codec validation (real runtime " << describe_amf_version(runtime_version)
<< ") so HEVC Main10/HDR is not refused.";
return amf_main10_compatibility_override_t {
std::move(amf_device_ref),
amf_context,
runtime_version
};
}
#endif
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 = encoder.codec_from_config(config);
if (!video_format[encoder_t::PASSED] || (disp && !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;
}
if (config.chromaSamplingType == 1 && !video_format[encoder_t::YUV444]) {
BOOST_LOG(error) << video_format.name << ": YUV 4:4:4 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;
}
auto colorspace = encode_device->colorspace;
if (!encode_device->initialize_hardware_device()) {
BOOST_LOG(error) << encoder.name << ": failed to initialize the hardware encode device"sv;
return nullptr;
}
auto sw_fmt = (colorspace.bit_depth == 8 && config.chromaSamplingType == 0) ? platform_formats->avcodec_pix_fmt_8bit :
(colorspace.bit_depth == 8 && config.chromaSamplingType == 1) ? platform_formats->avcodec_pix_fmt_yuv444_8bit :
(colorspace.bit_depth == 10 && config.chromaSamplingType == 0) ? platform_formats->avcodec_pix_fmt_10bit :
(colorspace.bit_depth == 10 && config.chromaSamplingType == 1) ? platform_formats->avcodec_pix_fmt_yuv444_10bit :
AV_PIX_FMT_NONE;
// Allow up to 1 retry to apply the set of fallback options.
//
// Note: If we later end up needing multiple sets of
// fallback options, we may need to allow more retries
// to try applying each set.
avcodec_ctx_t ctx;
for (int retries = 0; retries < 2; retries++) {
#ifdef _WIN32
std::optional<amf_main10_compatibility_override_t> amf_main10_compatibility_override;
// The override mutates AMF runtime state that must be handed back before this
// iteration ends, and avcodec_open2() is several hundred lines away with option
// handling in between. Tie the restore to the iteration scope so the fallback
// retry, the early return and any exception all restore the real version.
auto restore_amf_main10_compatibility_override = util::fail_guard([&amf_main10_compatibility_override]() {
if (amf_main10_compatibility_override) {
amf_main10_compatibility_override->device_context->version =
amf_main10_compatibility_override->runtime_version;
amf_main10_compatibility_override.reset();
}
});
#endif
ctx.reset(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};
if (config.framerateX100 > 0) {
AVRational fps = video::framerateX100_to_rational(config.framerateX100);
ctx->framerate = fps;
ctx->time_base = AVRational {fps.den, fps.num};
}
switch (config.videoFormat) {
case 0:
// 10-bit h264 encoding is not supported by our streaming protocol
assert(!config.dynamicRange);
ctx->profile = (config.chromaSamplingType == 1) ? AV_PROFILE_H264_HIGH_444_PREDICTIVE : AV_PROFILE_H264_HIGH;
break;
case 1:
if (config.chromaSamplingType == 1) {
// HEVC uses the same RExt profile for both 8 and 10 bit YUV 4:4:4 encoding
ctx->profile = AV_PROFILE_HEVC_REXT;
} else {
ctx->profile = config.dynamicRange ? AV_PROFILE_HEVC_MAIN_10 : AV_PROFILE_HEVC_MAIN;
}
break;
case 2:
// AV1 supports both 8 and 10 bit encoding with the same Main profile
// but YUV 4:4:4 sampling requires High profile
ctx->profile = (config.chromaSamplingType == 1) ? AV_PROFILE_AV1_HIGH : AV_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
// Exception: encoders with FIXED_GOP_SIZE flag don't support on-demand IDR
if (encoder.flags & FIXED_GOP_SIZE) {
// Fixed GOP for encoders that don't support on-demand IDR (e.g. Media Foundation)
ctx->gop_size = 120; // ~2 seconds at 60 FPS - larger to reduce oversized IDR frame frequency
ctx->keyint_min = 120;
} else {
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;
}
}
// We forcefully reset the flags to avoid clash on reuse of AVCodecContext
ctx->flags = 0;
ctx->flags |= AV_CODEC_FLAG_CLOSED_GOP | AV_CODEC_FLAG_LOW_DELAY;
ctx->flags2 |= AV_CODEC_FLAG2_FAST;
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;
// 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());
}
#ifdef _WIN32
// Only the FFmpeg-based rollback encoder reaches this function; native
// "amdvce_experimental" builds on encoder_platform_formats_amf and never gets here.
if (encoder.name == "amdvce_ffmpeg"sv &&
config.videoFormat == 1 &&
config.dynamicRange &&
sw_fmt == AV_PIX_FMT_P010) {
amf_main10_compatibility_override =
enable_amf_main10_compatibility_override(ctx.get(), encoding_stream_context.get());
}
#endif
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, &config](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);
}
},
[&](const std::function<int()> &v) {
av_dict_set_int(&options, option.name.c_str(), v(), 0);
},
[&](const encoder_t::option_t::optional_int_function_t &v) {
if (const auto value = v.evaluate()) {
av_dict_set_int(&options, option.name.c_str(), *value, 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);
}
},
[&](const std::function<const std::string(const config_t &cfg)> &v) {
av_dict_set(&options, option.name.c_str(), v(config).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 (config.chromaSamplingType == 1) {
for (auto &option : (config.dynamicRange ? video_format.hdr444_options : video_format.sdr444_options)) {
handle_option(option);
}
}
if (retries > 0) {
for (auto &option : video_format.fallback_options) {
handle_option(option);
}
}
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;
#ifndef __APPLE__
if (encoder.name == "nvenc" && config::video.nv_legacy.vbv_percentage_increase > 0) {
ctx->rc_buffer_size += ctx->rc_buffer_size * config::video.nv_legacy.vbv_percentage_increase / 100;
}
#endif
}
}
// Allow the encoding device a final opportunity to set/unset or override any options
encode_device->init_codec_options(ctx.get(), &options);
auto status = avcodec_open2(ctx.get(), codec, &options);
if (status) {
char err_str[AV_ERROR_MAX_STRING_SIZE] {0};
if (!video_format.fallback_options.empty() && retries == 0) {
BOOST_LOG(info)
<< "Retrying with fallback configuration options for ["sv << video_format.name << "] after error: "sv
<< av_make_error_string(err_str, AV_ERROR_MAX_STRING_SIZE, status);
continue;
} else {
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;
}
}
// Successfully opened the codec
break;
}
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, using the metadata resolved at encode-device
// creation so the SEI always matches the control-channel metadata (and never reflects
// a display that transiently reads SDR during a reinit).
if (colorspace_is_hdr(colorspace)) {
if (encode_device->hdr_metadata_valid) {
const auto &hdr_metadata = encode_device->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(warning) << "No HDR metadata available to attach to the encoded stream";
}
}
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)) {
if (!encode_device->prepare_to_destroy()) {
#if defined(__linux__) && defined(SUNSHINE_BUILD_CUDA)
native_nvenc_runtime_quarantined.store(true, std::memory_order_release);
#endif
(void) encode_device.release();
BOOST_LOG(error) << "NvEnc: failed initialization could not be torn down; quarantining native NVENC and the complete device until process exit"sv;
}
return nullptr;
}
return std::make_unique<nvenc_encode_session_t>(std::move(encode_device));
}
using initialization_cancel_t = std::function<bool()>;
bool acquire_amf_initialization_fence_until(
std::chrono::steady_clock::time_point deadline,
const initialization_cancel_t &cancelled) {
while (!cancelled() && std::chrono::steady_clock::now() < deadline) {
const auto remaining = deadline - std::chrono::steady_clock::now();
const auto poll_interval = std::chrono::duration_cast<std::chrono::steady_clock::duration>(50ms);
if (native_amf_lifecycle_gate.begin_initialization_for(std::min(remaining, poll_interval))) return true;
if (native_amf_lifecycle_gate.is_quarantined()) return false;
}
return false;
}
template<typename Owned>
void defer_contended_amf_teardown(
Owned owned,
std::shared_ptr<platf::display_t> display_lease,
std::string_view reason);
std::unique_ptr<amf_encode_session_t> make_amf_encode_session(
const config_t &client_config,
std::unique_ptr<platf::amf_encode_device_t> encode_device,
std::chrono::steady_clock::time_point deadline,
const initialization_cancel_t &cancelled,
bool &operation_cancelled,
bool &gate_contended) {
operation_cancelled = false;
gate_contended = false;
// Native AMF can wedge inside Init/Terminate on a damaged driver. Run the
// entire initialization attempt on an owning worker so probes, failed init,
// and real-session fallback all regain control within one watchdog interval.
// The caller already keeps the display alive for this synchronous wait. Do
// not also leave it inside a worker that may be detached after the watchdog:
// that stale owner would permanently block the next display generation.
if (native_amf_lifecycle_gate.is_quarantined()) {
BOOST_LOG(error) << "AMF: native runtime is quarantined after a watchdog timeout; refusing to re-enter the AMD runtime"sv;
return nullptr;
}
const auto gate_deadline = std::min(deadline, std::chrono::steady_clock::now() + 1s);
if (!acquire_amf_initialization_fence_until(gate_deadline, cancelled)) {
operation_cancelled = cancelled();
gate_contended = !operation_cancelled && !native_amf_lifecycle_gate.is_quarantined();
BOOST_LOG(warning) << "AMF: native initialization could not acquire the AMD runtime fence before the session deadline"sv;
return nullptr;
}
if (cancelled()) {
operation_cancelled = true;
native_amf_lifecycle_gate.cancel_initialization();
return nullptr;
}
if (deadline - std::chrono::steady_clock::now() < 1s) {
gate_contended = true;
native_amf_lifecycle_gate.cancel_initialization();
BOOST_LOG(warning) << "AMF: native initialization skipped because gate contention left less than one second of vendor budget"sv;
return nullptr;
}
std::optional<amf::amf_hdr_metadata> resolved_hdr_metadata;
if (colorspace_is_hdr(encode_device->colorspace)) {
if (encode_device->hdr_metadata_valid) {
const auto &hdr_metadata = encode_device->hdr_metadata;
amf::amf_hdr_metadata amf_metadata;
for (int i = 0; i < 3; i++) {
amf_metadata.displayPrimaries[i].x = hdr_metadata.displayPrimaries[i].x;
amf_metadata.displayPrimaries[i].y = hdr_metadata.displayPrimaries[i].y;
}
amf_metadata.whitePoint.x = hdr_metadata.whitePoint.x;
amf_metadata.whitePoint.y = hdr_metadata.whitePoint.y;
amf_metadata.maxDisplayLuminance = hdr_metadata.maxDisplayLuminance;
amf_metadata.minDisplayLuminance = hdr_metadata.minDisplayLuminance;
amf_metadata.maxContentLightLevel = hdr_metadata.maxContentLightLevel;
amf_metadata.maxFrameAverageLightLevel = hdr_metadata.maxFrameAverageLightLevel;
resolved_hdr_metadata = amf_metadata;
} else {
BOOST_LOG(warning) << "AMF: no resolved HDR metadata is available for the native bitstream"sv;
}
}
auto display_lease = encode_device->release_display_lease_for_initialization();
auto handoff = std::make_shared<amf::lifecycle::worker_handoff_t<std::unique_ptr<platf::amf_encode_device_t>>>();
const auto colorspace = encode_device->colorspace;
std::thread initialization_thread;
try {
initialization_thread = std::thread {
[encode_device = std::move(encode_device), client_config, colorspace, resolved_hdr_metadata, handoff]() mutable {
bool handed_off_to_caller = false;
auto initialization_gate = util::fail_guard([&handed_off_to_caller]() {
if (!handed_off_to_caller) {
native_amf_lifecycle_gate.cancel_initialization();
}
});
auto owned_encode_device = std::move(encode_device);
try {
if (!owned_encode_device->initialize_hardware_device() ||
!owned_encode_device->init_encoder(client_config, colorspace) ||
!owned_encode_device->finish_encoder_initialization(client_config, colorspace)) {
// Failure destruction is part of this same watchdog interval. Only
// publish failure after the D3D/AMF resources are gone, so legacy
// fallback cannot race a still-unwinding native runtime.
owned_encode_device.reset();
handoff->publish(nullptr);
return;
}
if (resolved_hdr_metadata && owned_encode_device->amf &&
!owned_encode_device->amf->set_hdr_metadata(resolved_hdr_metadata)) {
BOOST_LOG(warning) << "AMF: HDR stream will continue without mastering metadata"sv;
}
handed_off_to_caller = handoff->publish(std::move(owned_encode_device));
} catch (...) {
owned_encode_device.reset();
handoff->publish(nullptr);
}
}
};
} catch (const std::system_error &err) {
native_amf_lifecycle_gate.cancel_initialization();
BOOST_LOG(error) << "AMF: could not start native initialization worker: " << err.what();
return nullptr;
}
bool handoff_cancelled = false;
auto accepted = handoff->accept_until(deadline, cancelled, &handoff_cancelled);
if (!accepted) {
operation_cancelled = handoff_cancelled;
if (!handoff_cancelled) {
native_amf_lifecycle_gate.quarantine_initialization();
BOOST_LOG(error) << "AMF: native encoder initialization exceeded the vendor deadline; worker will reap ownership"sv;
} else {
BOOST_LOG(info) << "AMF: native encoder initialization cancelled; worker will reap ownership without quarantining AMD"sv;
}
initialization_thread.detach();
return nullptr;
}
encode_device = std::move(*accepted);
if (!encode_device) {
initialization_thread.join();
return nullptr;
}
initialization_thread.join();
if (!native_amf_lifecycle_gate.finish_initialization()) {
BOOST_LOG(error) << "AMF: initialization completed after a teardown/quarantine fence; discarding the native device"sv;
const auto gate_deadline = std::chrono::steady_clock::now() + 5s;
switch (native_amf_lifecycle_gate.begin_teardown_until(gate_deadline)) {
case amf::lifecycle::teardown_admission_e::granted:
break;
case amf::lifecycle::teardown_admission_e::quarantined:
(void) encode_device.release();
return nullptr;
case amf::lifecycle::teardown_admission_e::contended:
defer_contended_amf_teardown(std::move(encode_device), std::move(display_lease), "discarded native device"sv);
return nullptr;
}
// Fresh destruction budget once the fence is held — gate contention is
// not driver teardown time.
const bool discarded = amf::lifecycle::run_with_timeout(
[encode_device = std::move(encode_device)]() mutable {
encode_device.reset();
},
5s);
native_amf_lifecycle_gate.finish_teardown(discarded);
return nullptr;
}
encode_device->restore_display_lease_after_initialization(std::move(display_lease));
return std::make_unique<amf_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,
std::chrono::steady_clock::time_point initialization_deadline = std::chrono::steady_clock::time_point::max(),
initialization_cancel_t cancelled = {},
bool *operation_cancelled_out = nullptr,
bool *gate_contended_out = nullptr) {
if (!cancelled) cancelled = []() { return false; };
if (operation_cancelled_out) *operation_cancelled_out = false;
if (gate_contended_out) *gate_contended_out = false;
bool operation_cancelled = false;
bool gate_contended = false;
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));
} else if (dynamic_cast<platf::amf_encode_device_t *>(encode_device.get())) {
auto amf_encode_device = boost::dynamic_pointer_cast<platf::amf_encode_device_t>(std::move(encode_device));
auto session = make_amf_encode_session(
config, std::move(amf_encode_device), initialization_deadline, cancelled,
operation_cancelled, gate_contended);
if (operation_cancelled_out) *operation_cancelled_out = operation_cancelled;
if (gate_contended_out) *gate_contended_out = gate_contended;
return session;
}
return nullptr;
}
std::unique_ptr<platf::encode_device_t> make_encode_device(
platf::display_t &disp,
const encoder_t &encoder,
const config_t &config,
hdr_latch_t *hdr_latch,
bool deferred_avcodec);
void abandon_quarantined_session(
std::unique_ptr<encode_session_t> &session,
std::shared_ptr<platf::display_t> display_lease = {}) {
// The AMD runtime is already known to have an abandoned vendor call. Running
// another destructor can overlap it and hang/crash. Intentionally retain the
// resources until process termination without registering a static destructor.
(void) session.release();
// The leaked encoder/device is detached from capture. Releasing this external
// lease is required so display reinitialization can reach use_count()==1.
display_lease.reset();
}
// Fence contention is not a wedged runtime, so the session waiting on it is
// still healthy and still destroyable. Abandoning it would strand an AMF
// hardware encode session (a finite VCN resource), its ID3D11Device, its
// input-surface ring and its parked output-pump thread for the life of the
// process -- and because no quarantine is set, the host keeps streaming and
// repeats the leak on every later contended teardown until the GPU runs out of
// concurrent encode sessions.
//
// Hand ownership to a detached supervisor instead. The display lease is
// dropped immediately, exactly as an abandon would, so capture reinitialization
// still converges on use_count()==1 at the same moment it does today; only the
// destruction is deferred. The supervisor takes the runtime fence before
// touching the vendor runtime, and additionally holds
// encode_session_teardown_mutex so the late destructor cannot race the capture
// side's shared-surface release.
template<typename Owned>
void defer_contended_amf_teardown(
Owned owned,
std::shared_ptr<platf::display_t> display_lease,
std::string_view reason) {
if (!owned) {
return;
}
display_lease.reset();
BOOST_LOG(warning) << "AMF: " << reason
<< " teardown could not acquire the AMD runtime fence before its deadline; "
"deferring destruction instead of abandoning a healthy session"sv;
// std::thread's constructor moves the callable into its shared state before
// it can throw, so the owned pointer has to survive independently of the
// lambda for the launch-failure path to be able to fall back.
struct deferred_teardown_t {
Owned owned;
std::string reason;
};
auto deferred = std::make_shared<deferred_teardown_t>(
deferred_teardown_t {std::move(owned), std::string {reason}}
);
try {
std::thread supervisor {[deferred]() mutable {
// Nothing watchdogs this thread, so it can outwait a peer initialization
// that the caller's five-second budget could not.
const auto gate_deadline = std::chrono::steady_clock::now() + 30s;
if (native_amf_lifecycle_gate.begin_teardown_until(gate_deadline) !=
amf::lifecycle::teardown_admission_e::granted) {
BOOST_LOG(error) << "AMF: deferred " << deferred->reason
<< " teardown never acquired the runtime fence; abandoning the session"sv;
(void) deferred->owned.release();
return;
}
const bool completed = amf::lifecycle::run_with_timeout(
[deferred]() mutable {
std::lock_guard lg {encode_session_teardown_mutex};
deferred->owned.reset();
},
5s);
native_amf_lifecycle_gate.finish_teardown(completed);
if (completed) {
BOOST_LOG(info) << "AMF: deferred " << deferred->reason << " teardown completed"sv;
} else {
BOOST_LOG(error) << "AMF: deferred " << deferred->reason
<< " teardown exceeded 5 seconds; quarantining AMD encoding until host restart"sv;
}
}};
supervisor.detach();
} catch (...) {
BOOST_LOG(error) << "AMF: could not start the deferred " << reason
<< " teardown supervisor; abandoning the session"sv;
(void) deferred->owned.release();
}
}
#ifdef _WIN32
struct legacy_amf_session_bundle_t {
std::unique_ptr<encode_session_t> session;
bool hdr_metadata_valid = false;
SS_HDR_METADATA hdr_metadata {};
hdr_latch_t hdr_latch {};
};
std::optional<legacy_amf_session_bundle_t> make_legacy_amf_session_bounded(
std::shared_ptr<platf::display_t> disp,
const config_t &config,
int width,
int height,
hdr_latch_t *hdr_latch,
std::chrono::steady_clock::time_point deadline,
const initialization_cancel_t &cancelled,
bool &operation_cancelled,
bool &gate_contended) {
operation_cancelled = false;
gate_contended = false;
if (!disp || native_amf_lifecycle_gate.is_quarantined()) {
return std::nullopt;
}
const auto gate_deadline = std::min(deadline, std::chrono::steady_clock::now() + 1s);
if (!acquire_amf_initialization_fence_until(gate_deadline, cancelled)) {
operation_cancelled = cancelled();
gate_contended = !operation_cancelled && !native_amf_lifecycle_gate.is_quarantined();
BOOST_LOG(error) << "AMF: FFmpeg initialization could not acquire the AMD runtime fence before the session deadline"sv;
return std::nullopt;
}
if (cancelled()) {
operation_cancelled = true;
native_amf_lifecycle_gate.cancel_initialization();
return std::nullopt;
}
if (deadline - std::chrono::steady_clock::now() < 1s) {
gate_contended = true;
native_amf_lifecycle_gate.cancel_initialization();
BOOST_LOG(warning) << "AMF: FFmpeg initialization skipped because gate contention left less than one second of vendor budget"sv;
return std::nullopt;
}
auto local_latch = hdr_latch ? *hdr_latch : hdr_latch_t {};
auto base_device = make_encode_device(*disp, amdvce_ffmpeg, config, &local_latch, true);
auto prepared_device = boost::dynamic_pointer_cast<platf::avcodec_encode_device_t>(std::move(base_device));
if (!prepared_device) {
native_amf_lifecycle_gate.cancel_initialization();
return std::nullopt;
}
legacy_amf_session_bundle_t prepared_bundle;
prepared_bundle.hdr_metadata_valid = prepared_device->hdr_metadata_valid;
prepared_bundle.hdr_metadata = prepared_device->hdr_metadata;
prepared_bundle.hdr_latch = local_latch;
auto display_lease = prepared_device->release_display_lease_for_initialization();
auto handoff = std::make_shared<amf::lifecycle::worker_handoff_t<legacy_amf_session_bundle_t>>();
std::thread initialization_thread;
try {
initialization_thread = std::thread {
[config, width, height, prepared_device = std::move(prepared_device), prepared_bundle = std::move(prepared_bundle), handoff]() mutable {
bool gate_finished = false;
auto gate_cleanup = util::fail_guard([&gate_finished]() {
if (!gate_finished) {
native_amf_lifecycle_gate.cancel_initialization();
}
});
try {
if (prepared_device->is_codec_supported(amdvce_ffmpeg.codec_from_config(config).name, config)) {
prepared_bundle.session = make_encode_session(
nullptr, amdvce_ffmpeg, config, width, height, std::move(prepared_device));
}
const bool initialized = static_cast<bool>(prepared_bundle.session);
const bool accepted = handoff->publish(std::move(prepared_bundle));
gate_finished = initialized && accepted;
} catch (...) {
handoff->publish(legacy_amf_session_bundle_t {});
}
}
};
} catch (const std::system_error &err) {
native_amf_lifecycle_gate.cancel_initialization();
BOOST_LOG(error) << "AMF: could not start FFmpeg initialization worker: " << err.what();
return std::nullopt;
}
bool handoff_cancelled = false;
auto accepted = handoff->accept_until(deadline, cancelled, &handoff_cancelled);
if (!accepted) {
operation_cancelled = handoff_cancelled;
if (!handoff_cancelled) {
native_amf_lifecycle_gate.quarantine_initialization();
BOOST_LOG(error) << "AMF: complete D3D/FFmpeg initialization exceeded the vendor deadline; worker will reap ownership"sv;
} else {
BOOST_LOG(info) << "AMF: FFmpeg initialization cancelled; worker will reap ownership without quarantining AMD"sv;
}
initialization_thread.detach();
return std::nullopt;
}
initialization_thread.join();
auto bundle = std::move(*accepted);
if (!bundle.session) return std::nullopt;
if (!native_amf_lifecycle_gate.finish_initialization()) {
// The caller owns the accepted value, but publication was fenced. Avoid
// entering a quarantined runtime from this thread.
bundle.session.release();
return std::nullopt;
}
if (auto *legacy_session = dynamic_cast<avcodec_encode_session_t *>(bundle.session.get())) {
legacy_session->restore_display_lease_after_initialization(std::move(display_lease));
}
if (hdr_latch) *hdr_latch = bundle.hdr_latch;
return bundle;
}
bool destroy_legacy_amf_session_bounded(std::unique_ptr<encode_session_t> &session, std::string_view reason) {
if (!session) return true;
auto display_lease = static_cast<avcodec_encode_session_t *>(session.get())->release_display_lease_for_driver_work();
const auto gate_deadline = std::chrono::steady_clock::now() + 5s;
switch (native_amf_lifecycle_gate.begin_teardown_until(gate_deadline)) {
case amf::lifecycle::teardown_admission_e::granted:
break;
case amf::lifecycle::teardown_admission_e::quarantined:
BOOST_LOG(error) << "AMF: abandoning the FFmpeg " << reason
<< " session because the AMD runtime is quarantined"sv;
abandon_quarantined_session(session, std::move(display_lease));
return false;
case amf::lifecycle::teardown_admission_e::contended:
defer_contended_amf_teardown(std::move(session), std::move(display_lease), reason);
return false;
}
// Fresh destruction budget once the fence is held — gate contention is
// not driver teardown time.
const bool completed = amf::lifecycle::run_with_timeout(
[session = std::move(session), display_lease = std::move(display_lease)]() mutable {
std::lock_guard lock {encode_session_teardown_mutex};
session.reset();
(void) display_lease;
},
5s);
native_amf_lifecycle_gate.finish_teardown(completed);
if (!completed) {
BOOST_LOG(error) << "AMF: FFmpeg " << reason << " teardown exceeded 5 seconds; quarantining AMD encoding"sv;
}
return completed;
}
#endif
bool destroy_encode_session_bounded(std::unique_ptr<encode_session_t> &session, std::string_view reason) {
if (!session) {
return true;
}
const bool native_amf_session = dynamic_cast<amf_encode_session_t *>(session.get()) != nullptr;
std::shared_ptr<platf::display_t> display_lease;
if (native_amf_session) {
// Moving the shared_ptr itself cannot enter the D3D driver. Keep the old
// display alive inside the watchdog worker for every potentially blocking
// ClearState/COM/AMF destruction step. If it wedges, both the device and
// display lease remain quarantined together; releasing the display first
// would violate the shared-resource lifetime ordering.
display_lease = static_cast<amf_encode_session_t *>(session.get())->release_display_lease_for_driver_work();
}
auto owned_session = std::move(session);
if (native_amf_session) {
// Gate contention is not driver teardown time. A healthy initialization is
// allowed its own watchdog interval, so acquire the fence before starting
// the independent five-second destruction deadline.
const auto gate_deadline = std::chrono::steady_clock::now() + 5s;
switch (native_amf_lifecycle_gate.begin_teardown_until(gate_deadline)) {
case amf::lifecycle::teardown_admission_e::granted:
break;
case amf::lifecycle::teardown_admission_e::quarantined:
BOOST_LOG(error) << "AMF: abandoning the native " << reason
<< " session because the AMD runtime is quarantined"sv;
abandon_quarantined_session(owned_session, std::move(display_lease));
return false;
case amf::lifecycle::teardown_admission_e::contended:
defer_contended_amf_teardown(std::move(owned_session), std::move(display_lease), reason);
return false;
}
// Fresh destruction budget once the fence is held — gate contention is
// not driver teardown time.
const bool completed = amf::lifecycle::run_with_timeout(
[owned_session = std::move(owned_session), display_lease = std::move(display_lease)]() mutable {
owned_session.reset();
(void) display_lease;
},
5s);
native_amf_lifecycle_gate.finish_teardown(completed);
if (!completed) {
BOOST_LOG(error) << "Encoder " << reason << " teardown exceeded 5 seconds; abandoning that session"sv;
}
return completed;
}
const bool completed = amf::lifecycle::run_with_timeout(
[owned_session = std::move(owned_session)]() mutable {
std::lock_guard lock {encode_session_teardown_mutex};
owned_session.reset();
},
5s);
if (!completed) {
BOOST_LOG(error) << "Encoder " << reason << " teardown exceeded 5 seconds; abandoning that session"sv;
}
return completed;
}
enum class encode_run_result_e {
completed,
native_amf_failed,
temporarily_busy,
initialization_failed,
};
encode_run_result_e 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,
std::unique_ptr<encode_session_t> prepared_session,
safe::signal_t &reinit_event,
const encoder_t &encoder,
hdr_latch_t *hdr_latch,
void *channel_data,
std::chrono::steady_clock::time_point initialization_deadline,
initialization_cancel_t initialization_cancelled,
std::optional<hdr_info_raw_t> &last_hdr_info,
rtx_hdr_metadata_refresh_state_t &rtx_hdr_metadata_refresh
) {
const encoder_t *session_encoder = &encoder;
bool initialization_was_cancelled = false;
bool initialization_gate_contended = false;
auto session = prepared_session ?
std::move(prepared_session) :
make_encode_session(
disp.get(), encoder, config, disp->width, disp->height,
std::move(encode_device), initialization_deadline, initialization_cancelled,
&initialization_was_cancelled, &initialization_gate_contended);
#if defined(__linux__)
BOOST_LOG(info) << "Linux encoder dimensions: encoder=" << encoder.name
<< " capture=" << disp->width << 'x' << disp->height
<< " encoder_context=" << config.width << 'x' << config.height
<< " stream_fps=" << config.framerate
<< " encoding_framerate_milli_fps=" << config.encodingFramerate
<< " result=" << (session ? "ready" : initialization_gate_contended ? "temporarily_busy" :
"failed")
<< '.';
#endif
#ifdef _WIN32
if (initialization_was_cancelled) return encode_run_result_e::completed;
if (!session && &encoder == &amdvce_experimental) {
BOOST_LOG(error) << "AMF: native session initialization failed; refusing silent amdvce_ffmpeg fallback"sv;
}
#endif
#if defined(__linux__) && defined(SUNSHINE_BUILD_CUDA)
if (!session && &encoder == &nvenc) {
BOOST_LOG(error) << "NvEnc: native session initialization failed; refusing silent FFmpeg NVENC fallback"sv;
}
#endif
if (!session) {
if (initialization_gate_contended) return encode_run_result_e::temporarily_busy;
return encode_run_result_e::initialization_failed;
}
// One RTTI lookup for the whole session — the pointer stays valid until the
// fail_guard teardown moves the session out after the encode loop exits.
auto *const native_session = dynamic_cast<amf_encode_session_t *>(session.get());
const bool native_amf_session = native_session != nullptr;
#ifdef _WIN32
const bool legacy_amf_session = session_encoder == &amdvce_ffmpeg;
#else
const bool legacy_amf_session = false;
#endif
bool native_amf_runtime_failed = false;
const auto session_encoder_flags = session_encoder->flags;
auto shutdown_event = mail->event<bool>(mail::shutdown);
bool force_sync_teardown = false;
// As a workaround for NVENC hangs and to generally speed up encoder reinit,
// we will complete the encoder teardown in a separate thread if supported.
// This will move expensive processing off the encoder thread to allow us
// to restart encoding as soon as possible. For cases where the NVENC driver
// hang occurs, this thread may probably never exit, but it will allow
// streaming to continue without requiring a full restart of Sunshine.
auto fail_guard = util::fail_guard([session_encoder_flags, legacy_amf_session, &session, &force_sync_teardown, &reinit_event, shutdown_event] {
if (session_encoder_flags & OWNER_THREAD_TEARDOWN) {
// Linux GL conversion devices keep EGL current on this encoding
// thread. A watchdog worker cannot unregister their CUDA resources or
// delete their GL objects, even if we wait for that worker to finish.
std::lock_guard lock {encode_session_teardown_mutex};
session.reset();
return;
}
const bool shutdown_teardown = shutdown_event && shutdown_event->peek();
// A display reinit (resolution/HDR/colorspace change, e.g. alt-tabbing a game on a
// virtual display) frees the shared capture surfaces this encoder's device has open.
// Tearing the encoder down on a detached thread during that window abandons a hung
// NVENC session that races the surface free and the GPU's deferred eviction, which can
// bugcheck the kernel video memory manager (VIDEO_MEMORY_MANAGEMENT_INTERNAL 0x10e),
// especially with a second client connected. Force an ordered synchronous teardown
// whenever a reinit is in progress so this encoder releases its surfaces before the
// capture side frees them.
const bool sync_teardown = force_sync_teardown || shutdown_teardown || reinit_event.peek();
if ((session_encoder_flags & ASYNC_TEARDOWN) && !sync_teardown) {
const bool native_amf_session = dynamic_cast<amf_encode_session_t *>(session.get()) != nullptr;
auto display_lease = native_amf_session ?
static_cast<amf_encode_session_t *>(session.get())->release_display_lease_for_driver_work() :
legacy_amf_session ?
static_cast<avcodec_encode_session_t *>(session.get())->release_display_lease_for_driver_work() :
nullptr;
std::thread encoder_teardown_thread {[session = std::move(session), native_amf_session, legacy_amf_session, display_lease = std::move(display_lease)]() mutable {
BOOST_LOG(info) << "Starting async encoder teardown";
if (native_amf_session) {
// Supervise the vendor destructor from this already-asynchronous path.
// On timeout, the inner worker retains ownership while future sessions
// refuse to re-enter either AMF backend instead of accumulating more
// calls into the same wedged AMD runtime.
const auto gate_deadline = std::chrono::steady_clock::now() + 5s;
switch (native_amf_lifecycle_gate.begin_teardown_until(gate_deadline)) {
case amf::lifecycle::teardown_admission_e::granted:
break;
case amf::lifecycle::teardown_admission_e::quarantined:
BOOST_LOG(error) << "AMF: abandoning the native session in async teardown because the AMD runtime is quarantined"sv;
abandon_quarantined_session(session, std::move(display_lease));
return;
case amf::lifecycle::teardown_admission_e::contended:
defer_contended_amf_teardown(std::move(session), std::move(display_lease), "async native AMF"sv);
return;
}
// Fresh destruction budget once the fence is held — gate contention
// is not driver teardown time.
const bool completed = amf::lifecycle::run_with_timeout(
[session = std::move(session), display_lease = std::move(display_lease)]() mutable {
session.reset();
(void) display_lease;
},
5s);
native_amf_lifecycle_gate.finish_teardown(completed);
if (!completed) {
BOOST_LOG(error) << "AMF: async teardown exceeded 5 seconds; quarantining native AMF until host restart"sv;
}
} else if (legacy_amf_session) {
const auto gate_deadline = std::chrono::steady_clock::now() + 5s;
switch (native_amf_lifecycle_gate.begin_teardown_until(gate_deadline)) {
case amf::lifecycle::teardown_admission_e::granted:
break;
case amf::lifecycle::teardown_admission_e::quarantined:
BOOST_LOG(error) << "AMF: abandoning the FFmpeg session in async teardown because the AMD runtime is quarantined"sv;
abandon_quarantined_session(session, std::move(display_lease));
return;
case amf::lifecycle::teardown_admission_e::contended:
defer_contended_amf_teardown(std::move(session), std::move(display_lease), "async FFmpeg AMF"sv);
return;
}
// Fresh destruction budget once the fence is held — gate contention
// is not driver teardown time.
const bool completed = amf::lifecycle::run_with_timeout(
[session = std::move(session), display_lease = std::move(display_lease)]() mutable {
std::lock_guard lg {encode_session_teardown_mutex};
session.reset();
(void) display_lease;
},
5s);
native_amf_lifecycle_gate.finish_teardown(completed);
if (!completed) {
BOOST_LOG(error) << "AMF: FFmpeg async teardown exceeded 5 seconds; quarantining AMD encoding until host restart"sv;
}
} else {
std::lock_guard lg {encode_session_teardown_mutex};
session.reset();
}
BOOST_LOG(info) << "Async encoder teardown complete";
}};
encoder_teardown_thread.detach();
} else {
if ((session_encoder_flags & ASYNC_TEARDOWN) && sync_teardown) {
BOOST_LOG(debug) << "Using synchronous encoder teardown during "
<< (shutdown_teardown ? "shutdown"sv : "capture reinit"sv);
}
// Destroy the session here, under the teardown mutex, rather than letting it die at
// scope exit. During a capture reinit both video threads reach this point at the same
// moment; unserialized concurrent NVENC/D3D11 device destruction crashes the NVIDIA UMD
// (access violation in nvwgf2umx during cross-device shared-resource dependency cleanup).
const bool native_amf_session = dynamic_cast<amf_encode_session_t *>(session.get()) != nullptr;
if (native_amf_session) {
destroy_encode_session_bounded(session, "native AMF"sv);
} else if (dynamic_cast<avcodec_encode_session_t *>(session.get())) {
// A wedged driver can block avcodec session destruction indefinitely.
// During shutdown that blocks videoThread.join();
// during reinit it prevents capture recovery. Run destruction on a helper
// thread and abandon it if it overruns. An abandoned
// session leaks, but the stream host survives.
// NVENC keeps the fully synchronous teardown: its driver waits are already bounded
// (nvenc_base) and its teardown-vs-surface-free ordering is load-bearing
// (VIDEO_MEMORY_MANAGEMENT_INTERNAL 0x10e bugcheck).
std::promise<void> done;
auto done_future = done.get_future();
auto legacy_display_lease = legacy_amf_session ?
static_cast<avcodec_encode_session_t *>(session.get())->release_display_lease_for_driver_work() :
nullptr;
if (legacy_amf_session) {
const auto gate_deadline = std::chrono::steady_clock::now() + 5s;
switch (native_amf_lifecycle_gate.begin_teardown_until(gate_deadline)) {
case amf::lifecycle::teardown_admission_e::granted:
break;
case amf::lifecycle::teardown_admission_e::quarantined:
BOOST_LOG(error) << "AMF: abandoning the FFmpeg session in sync teardown because the AMD runtime is quarantined"sv;
abandon_quarantined_session(session, std::move(legacy_display_lease));
return;
case amf::lifecycle::teardown_admission_e::contended:
defer_contended_amf_teardown(std::move(session), std::move(legacy_display_lease), "sync FFmpeg AMF"sv);
return;
}
std::thread teardown_thread {[session = std::move(session), done = std::move(done), display_lease = std::move(legacy_display_lease)]() mutable {
std::lock_guard lg {encode_session_teardown_mutex};
session.reset();
(void) display_lease;
done.set_value();
}};
// Fresh destruction budget once the fence is held — gate contention
// is not driver teardown time.
const bool completed = done_future.wait_for(5s) == std::future_status::ready;
native_amf_lifecycle_gate.finish_teardown(completed);
if (completed) teardown_thread.join();
else {
BOOST_LOG(error) << "Encoder teardown did not finish within its 5 second destruction budget; abandoning the session"sv;
teardown_thread.detach();
}
return;
}
std::thread teardown_thread {[session = std::move(session), done = std::move(done), display_lease = std::move(legacy_display_lease)]() mutable {
std::lock_guard lg {encode_session_teardown_mutex};
session.reset();
(void) display_lease;
done.set_value();
}};
const bool completed = done_future.wait_for(5s) == std::future_status::ready;
if (completed) {
teardown_thread.join();
} else {
BOOST_LOG(error) << "Encoder teardown did not finish within 5 seconds; abandoning the session to keep the stream host alive"sv;
teardown_thread.detach();
}
} else {
std::lock_guard lg {encode_session_teardown_mutex};
session.reset();
}
}
});
auto native_amf_failure = [&]() {
force_sync_teardown = native_amf_session;
return native_amf_session ? encode_run_result_e::native_amf_failed : encode_run_result_e::completed;
};
if (config.encodingFramerate <= 0) {
const int fallback_fps = config.framerate > 0 ? config.framerate * 1000 : 60000;
BOOST_LOG(warning) << "Encoding framerate missing; falling back to " << fallback_fps;
config.encodingFramerate = fallback_fps;
}
// set max frame time based on client-requested target framerate.
double minimum_fps_target = (config::video.minimum_fps_target > 0.0) ? config::video.minimum_fps_target * 1000 : std::max(config.encodingFramerate / 5, 10000);
auto max_frametime = std::chrono::nanoseconds(1000ms) * 1000 / minimum_fps_target;
auto encode_frame_threshold = std::chrono::nanoseconds(1000ms) * 1000 / config.encodingFramerate;
auto frame_variation_threshold = encode_frame_threshold / 4;
BOOST_LOG(info) << "Minimum FPS target set to ~"sv << (minimum_fps_target / 2000) << "fps ("sv << max_frametime * 2 << ")"sv;
BOOST_LOG(info) << "Encoding Frame threshold: "sv << encode_frame_threshold;
auto packets = mail::man->queue<packet_t>(mail::video_packets);
auto idr_events = mail->event<bool>(mail::idr);
auto hdr_event = mail->event<hdr_info_t>(mail::hdr);
auto invalidate_ref_frames_events = mail->event<std::pair<int64_t, int64_t>>(mail::invalidate_ref_frames);
auto bitrate_events = mail->event<int>(mail::dynamic_bitrate);
{
// 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 native_amf_failure();
}
}
if (config.input_only) {
BOOST_LOG(info) << "Input only session, video will not be captured."sv;
// Native AMF is asynchronous and may initially reject the dummy surface
// with backpressure. Resubmit the same logical frame until it is accepted,
// then keep draining until that accepted input actually emits.
const auto now = std::chrono::steady_clock::now();
const auto dummy_frame_index = static_cast<uint64_t>(frame_nr++);
if (encode(dummy_frame_index, *session, packets, channel_data, now, now, now)) {
BOOST_LOG(error) << "Could not encode dummy video packet"sv;
return native_amf_failure();
}
if (auto *amf_session = dynamic_cast<amf_encode_session_t *>(session.get())) {
const auto acceptance_deadline = std::chrono::steady_clock::now() + 2s;
bool dummy_accepted = amf_session->was_last_input_accepted();
while (!dummy_accepted && std::chrono::steady_clock::now() < acceptance_deadline) {
auto delayed = amf_session->drain_frames(10ms);
if (delayed.fatal || std::any_of(delayed.frames.begin(), delayed.frames.end(), [](const auto &frame) { return frame.fatal; })) {
BOOST_LOG(error) << "AMF failed while accepting the input-only dummy packet"sv;
return native_amf_failure();
}
deliver_amf_frames(dummy_frame_index, *amf_session, delayed.frames, packets, channel_data);
if (encode(dummy_frame_index, *session, packets, channel_data, now, now, now)) {
BOOST_LOG(error) << "Could not resubmit the AMF input-only dummy packet"sv;
return native_amf_failure();
}
dummy_accepted = amf_session->was_last_input_accepted();
}
if (!dummy_accepted) {
BOOST_LOG(error) << "AMF did not accept the input-only dummy packet within 2 seconds"sv;
return native_amf_failure();
}
// A PA encoder deliberately holds its final lookahead frame until AMF Drain
// marks end-of-input. Input-only sessions have no future live frame to do it.
if (!amf_session->begin_drain()) {
BOOST_LOG(error) << "Could not begin draining the AMF input-only session"sv;
return native_amf_failure();
}
const auto drain_deadline = std::chrono::steady_clock::now() + 2s;
while (!amf_session->has_emitted_frame(dummy_frame_index) && std::chrono::steady_clock::now() < drain_deadline) {
auto delayed = amf_session->drain_frames(50ms);
if (delayed.fatal || std::any_of(delayed.frames.begin(), delayed.frames.end(), [](const auto &frame) { return frame.fatal; })) {
BOOST_LOG(error) << "AMF failed while draining the input-only dummy packet"sv;
return native_amf_failure();
}
deliver_amf_frames(dummy_frame_index, *amf_session, delayed.frames, packets, channel_data);
}
if (!amf_session->has_emitted_frame(dummy_frame_index)) {
BOOST_LOG(error) << "AMF did not emit the input-only dummy packet within 2 seconds"sv;
return native_amf_failure();
}
}
while (true) {
if (shutdown_event->peek() || !images->running() || (reinit_event.peek())) {
return encode_run_result_e::completed;
} else {
std::this_thread::sleep_for(300ms);
}
}
}
std::optional<std::chrono::steady_clock::time_point> encode_frame_timestamp;
encode_bootstrap_state_t bootstrap_state {.allow_placeholder_before_first_real = frame_nr <= 1};
// Per-session encode-loop accounting. When several clients share one capture target, a
// single client can freeze while the others stream fine, and nothing else in the log
// distinguishes a session that is encoding live frames from one that is starved or gated.
// The channel_data pointer is a stable per-session tag for correlating these lines.
struct {
uint64_t popped_real = 0;
uint64_t popped_placeholder = 0;
uint64_t pop_timeouts = 0;
uint64_t gate_skipped = 0;
uint64_t converted = 0;
uint64_t encoded = 0;
uint64_t dropped_submissions = 0;
std::chrono::steady_clock::time_point last_log = std::chrono::steady_clock::now();
} loop_stats;
while (true) {
if (auto now = std::chrono::steady_clock::now(); now - loop_stats.last_log >= 10s) {
BOOST_LOG(debug) << "Encode loop [" << channel_data << "] " << config.width << 'x' << config.height
<< ": popped_real=" << loop_stats.popped_real
<< " popped_placeholder=" << loop_stats.popped_placeholder
<< " pop_timeouts=" << loop_stats.pop_timeouts
<< " gate_skipped=" << loop_stats.gate_skipped
<< " converted=" << loop_stats.converted
<< " encoded=" << loop_stats.encoded
<< " dropped_submissions=" << loop_stats.dropped_submissions
<< " frame_nr=" << frame_nr;
loop_stats = {};
loop_stats.last_log = now;
}
// Break out of the encoding loop if any of the following are true:
// a) The stream is ending
// b) Sunshine is quitting
// c) The capture side is waiting to reinit and we've encoded at least one frame
//
// If we have to reinit before we have received any captured frames, we will encode
// the blank dummy frame just to let Moonlight know that we're alive.
const bool reinit_pending = reinit_event.peek() && frame_nr > 1;
if (shutdown_event->peek() || !images->running() || reinit_pending) {
force_sync_teardown = reinit_pending;
break;
}
// Apply any runtime bitrate change, coalescing rapid ABR updates to the latest value so a
// burst of requests causes at most one reconfigure/rebuild. NVENC reconfigures the live
// encoder seamlessly; encoders that cannot (avcodec-based) report failure and we rebuild this
// session by breaking out, so capture_async re-enters with config (held by reference) anew.
std::optional<int> latest_bitrate;
while (bitrate_events->peek()) {
if (auto new_bitrate = bitrate_events->pop(0ms)) {
latest_bitrate = *new_bitrate;
}
}
if (latest_bitrate) {
config.bitrate = *latest_bitrate;
config.client_requested_bitrate = *latest_bitrate;
if (session->set_bitrate(*latest_bitrate)) {
BOOST_LOG(info) << "Applied runtime bitrate "sv << *latest_bitrate << " kbps (live)"sv;
} else if (frame_nr > 1) {
BOOST_LOG(info) << "Rebuilding encoder to apply runtime bitrate "sv << *latest_bitrate << " kbps"sv;
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;
std::optional<std::chrono::steady_clock::time_point> capture_timestamp;
std::optional<std::chrono::steady_clock::time_point> host_processing_timestamp;
bool placeholder_input = bootstrap_state.current_input_placeholder;
// Encode at a minimum FPS to avoid image quality issues with static content
if (!requested_idr_frame || images->peek()) {
auto image_wait_budget = max_frametime;
if (bootstrap_state.should_encode_placeholder()) {
// Prime a lookahead encoder immediately; waiting for minimum FPS between
// bootstrap placeholders needlessly delays the first packet.
image_wait_budget = decltype(max_frametime)::zero();
}
const auto image_wait_started = std::chrono::steady_clock::now();
if (auto *amf_session = native_session;
amf_session && image_wait_budget > decltype(max_frametime)::zero()) {
// Never hold an already-completed packet behind conversion/submission
// of a newer image. This zero-wait drain also runs during continuous
// motion, where images->peek() remains true and the sparse loop below
// intentionally yields immediately.
if (amf_session->has_completed_output()) {
auto ready = amf_session->drain_frames(0ms);
if (ready.fatal || std::any_of(ready.frames.begin(), ready.frames.end(), [](const auto &frame) { return frame.fatal; })) {
BOOST_LOG(error) << "AMF failed while delivering ready output"sv;
native_amf_runtime_failed = true;
break;
}
deliver_amf_frames(frame_nr, *amf_session, ready.frames, packets, channel_data);
}
// Wait for either the next capture or output that became due with the
// previous native submission. This preserves high-refresh pipelining
// when another image arrives, while delivering the final moving frame
// promptly when WGC transitions to a sparse/static cadence.
const auto wait_started = std::chrono::steady_clock::now();
const auto delivery_grace = std::min(
std::chrono::duration_cast<std::chrono::steady_clock::duration>(image_wait_budget),
std::chrono::duration_cast<std::chrono::steady_clock::duration>(32ms));
const auto delivery_deadline = wait_started + delivery_grace;
bool delayed_output_failed = false;
while (!images->peek() && amf_session->has_output_due() &&
std::chrono::steady_clock::now() < delivery_deadline) {
auto delayed = amf_session->drain_frames(1ms);
if (delayed.fatal || std::any_of(delayed.frames.begin(), delayed.frames.end(), [](const auto &frame) { return frame.fatal; })) {
BOOST_LOG(error) << "AMF failed while delivering sparse-capture output"sv;
delayed_output_failed = true;
break;
}
deliver_amf_frames(frame_nr, *amf_session, delayed.frames, packets, channel_data);
}
if (delayed_output_failed) {
native_amf_runtime_failed = true;
break;
}
const auto waited = std::chrono::steady_clock::now() - wait_started;
image_wait_budget = waited < image_wait_budget ?
image_wait_budget - waited :
decltype(max_frametime)::zero();
}
if (auto *amf_session = native_session;
amf_session && amf_session->has_retained_preanalysis_tail()) {
// Check after sparse draining as well as before the final image wait:
// QueryOutput may have completed during the drain and exposed PA's
// retained tail. The one-shot session gate prevents the duplicate used
// here from arming an endless negotiated-FPS duplicate train.
const auto tail_flush_budget = std::chrono::duration_cast<decltype(image_wait_budget)>(
encode_frame_threshold + 1ms);
const auto tail_started = amf_session->input_accepted_at().value_or(image_wait_started);
const auto elapsed = std::chrono::duration_cast<decltype(image_wait_budget)>(
std::chrono::steady_clock::now() - tail_started);
const auto remaining_tail_budget = elapsed < tail_flush_budget ?
tail_flush_budget - elapsed :
decltype(image_wait_budget)::zero();
image_wait_budget = std::min(image_wait_budget, remaining_tail_budget);
}
if (auto img = images->pop(image_wait_budget)) {
placeholder_input = is_placeholder_capture_image(*img);
if (placeholder_input) {
++loop_stats.popped_placeholder;
} else {
++loop_stats.popped_real;
}
if (!placeholder_input && bootstrap_state.current_input_placeholder) {
session->request_idr_frame();
}
if (!placeholder_input) {
capture_timestamp = img->frame_timestamp;
frame_timestamp = capture_timestamp;
host_processing_timestamp = img->host_processing_timestamp;
}
if (session->convert(*img)) {
BOOST_LOG(error) << "Could not convert image"sv;
native_amf_runtime_failed = native_amf_session;
break;
}
++loop_stats.converted;
#ifdef SUNSHINE_ENABLE_NV_TRUEHDR
if (refresh_rtx_hdr_metadata_if_needed(
config,
session.get(),
hdr_event,
last_hdr_info,
rtx_hdr_metadata_refresh
)) {
session->request_idr_frame();
}
#endif
bootstrap_state.current_input_placeholder = placeholder_input;
if (!placeholder_input) {
bootstrap_state.real_frame_seen = true;
if (!encode_frame_timestamp) {
encode_frame_timestamp = *frame_timestamp;
}
const auto time_diff = (*frame_timestamp > *encode_frame_timestamp)
? (*frame_timestamp - *encode_frame_timestamp)
: (*encode_frame_timestamp - *frame_timestamp);
if (time_diff < frame_variation_threshold) {
*frame_timestamp = *encode_frame_timestamp;
} else {
*encode_frame_timestamp = *frame_timestamp;
}
*encode_frame_timestamp += encode_frame_threshold;
} else {
frame_timestamp.reset();
host_processing_timestamp.reset();
}
} else if (!images->running()) {
break;
} else {
++loop_stats.pop_timeouts;
placeholder_input = bootstrap_state.current_input_placeholder;
}
}
if (placeholder_input && !bootstrap_state.should_encode_placeholder()) {
++loop_stats.gate_skipped;
continue;
}
if (encode(frame_nr++, *session, packets, channel_data, frame_timestamp, capture_timestamp, host_processing_timestamp)) {
BOOST_LOG(error) << "Could not encode video packet"sv;
native_amf_runtime_failed = native_amf_session;
break;
}
++loop_stats.encoded;
// A dropped submission leaves a hole in the wire frameIndex sequence, which
// the client reads as loss. Reusing the index instead is NOT safe: several
// recoverable conditions (a minimum-FPS duplicate finding no free surface,
// exhausted submit backpressure) report "not accepted" indefinitely, and
// withholding the increment turns those into a permanently frozen stream.
// Count the drops so the condition stays observable.
if (native_session && !native_session->was_last_input_accepted()) {
++loop_stats.dropped_submissions;
}
if (placeholder_input) {
// PA can accept the first placeholder while intentionally emitting
// nothing until its lookahead is primed. Keep submitting placeholders
// until a packet actually exists; accepted input is not packet delivery.
bootstrap_state.placeholder_encoded = !native_session || native_session->has_emitted_any_frame();
}
session->request_normal_frame();
// While streaming check to see if the mouse is present and enable Mouse Keys to force the cursor to appear
// This is useful for KVM switch scenarios where mouse may disappear during streaming
platf::enable_mouse_keys();
}
if (native_amf_runtime_failed) {
force_sync_teardown = true;
}
return native_amf_runtime_failed ?
encode_run_result_e::native_amf_failed :
encode_run_result_e::completed;
}
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);
// we initialize scalar_tpcoords and logical dimensions to default values in case they are not set (non-KMS)
float scalar_tpcoords = 1.0f;
int display_env_logical_width = 0;
int display_env_logical_height = 0;
if (display->logical_width > 0 && display->logical_height > 0 &&
display->env_logical_width > 0 && display->env_logical_height > 0) {
float lwd = display->logical_width;
float lhd = display->logical_height;
scalar_tpcoords = std::fminf(wd / lwd, hd / lhd);
display_env_logical_width = display->env_logical_width;
display_env_logical_height = display->env_logical_height;
}
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,
scalar_tpcoords,
display_env_logical_width,
display_env_logical_height
};
}
std::unique_ptr<platf::encode_device_t> make_encode_device(
platf::display_t &disp,
const encoder_t &encoder,
const config_t &config,
hdr_latch_t *hdr_latch = nullptr,
bool deferred_avcodec = false) {
std::unique_ptr<platf::encode_device_t> result;
#ifdef _WIN32
if (&encoder == &amdvce_ffmpeg && native_amf_lifecycle_gate.is_quarantined()) {
BOOST_LOG(error) << "AMF: refusing FFmpeg initialization while the AMD runtime is quarantined"sv;
return nullptr;
}
#endif
#if defined(__linux__) && defined(SUNSHINE_BUILD_CUDA)
if (&encoder == &nvenc && native_nvenc_runtime_quarantined.load(std::memory_order_acquire)) {
BOOST_LOG(error) << "NvEnc: native runtime is quarantined after an unsafe teardown; refusing to create another native session until host restart"sv;
return nullptr;
}
#endif
const bool display_is_hdr = disp.is_hdr();
bool hdr_display = display_is_hdr;
#ifdef SUNSHINE_ENABLE_NV_TRUEHDR
// When NVIDIA TrueHDR (RTX HDR) is enabled for an HDR stream, synthesize HDR from an
// SDR capture. The source display may intentionally remain SDR; the actual SDR->HDR
// conversion happens in the encode device's convert() step.
const bool rtx_hdr_stream = config.rtx_hdr_active;
hdr_display = hdr_display || rtx_hdr_stream;
#endif
// HDR colorspace latch. A virtual display created for an HDR session can briefly
// re-enumerate as SDR during a capture reinit (the double-refresh / HDR-profile mode
// change that happens ~1s into a session momentarily drops the output to G22 8-bit).
// Without this guard the encoder gets rebuilt for an 8-bit SDR colorspace mid-session
// and streams SDR frames to an HDR client, which faults the client decoder
// ("decoder reported error") and tears the session down. Once an HDR-requested session
// has actually established HDR, keep the HDR colorspace for the rest of the session so a
// momentary SDR reading during a reinit can no longer downgrade the wire colorspace.
// (Genuinely-SDR sources never latch, because hdr_display is never true for them.)
if (config.dynamicRange > 0 && !config.prefer_sdr_10bit && !config.force_sdr && hdr_latch) {
if (hdr_display) {
hdr_latch->latched = true;
} else if (hdr_latch->latched) {
BOOST_LOG(info) << "Display momentarily reported SDR during reinit; keeping HDR colorspace for this HDR session.";
hdr_display = true;
}
}
auto colorspace = colorspace_from_client_config(config, hdr_display);
platf::pix_fmt_e pix_fmt;
if (config.chromaSamplingType == 1) {
// YUV 4:4:4
if (!(encoder.flags & YUV444_SUPPORT)) {
// Encoder can't support YUV 4:4:4 regardless of hardware capabilities
return {};
}
pix_fmt = (colorspace.bit_depth == 10) ?
encoder.platform_formats->pix_fmt_yuv444_10bit :
encoder.platform_formats->pix_fmt_yuv444_8bit;
} else {
// YUV 4:2:0
pix_fmt = (colorspace.bit_depth == 10) ?
encoder.platform_formats->pix_fmt_10bit :
encoder.platform_formats->pix_fmt_8bit;
}
{
auto encoder_name = encoder.codec_from_config(config).name;
BOOST_LOG(info) << "Creating encoder " << logging::bracket(encoder_name);
auto color_coding = colorspace.colorspace == colorspace_e::bt2020 ? "HDR (Rec. 2020 + SMPTE 2084 PQ)" :
colorspace.colorspace == colorspace_e::rec601 ? "SDR (Rec. 601)" :
colorspace.colorspace == colorspace_e::rec709 ? "SDR (Rec. 709)" :
colorspace.colorspace == colorspace_e::bt2020sdr ? "SDR (Rec. 2020)" :
"unknown";
BOOST_LOG(info) << "Color coding: " << color_coding;
BOOST_LOG(info) << "Color depth: " << colorspace.bit_depth << "-bit";
BOOST_LOG(info) << "Color range: " << (colorspace.full_range ? "JPEG" : "MPEG");
}
if (dynamic_cast<const encoder_platform_formats_avcodec *>(encoder.platform_formats.get())) {
result = deferred_avcodec ?
disp.make_deferred_avcodec_encode_device(pix_fmt) :
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);
} else if (dynamic_cast<const encoder_platform_formats_amf *>(encoder.platform_formats.get())) {
result = disp.make_amf_encode_device(pix_fmt);
}
if (result) {
#ifdef __linux__
// The Linux capture path must stay SDR for TrueHDR to have something to convert: an
// already-HDR capture is passed through untouched, and an encoder without the CUDA/GL
// converter cannot do it at all. Refuse rather than label unconverted SDR as HDR.
if (config.rtx_hdr_active && (!result->supports_rtx_hdr() || display_is_hdr)) {
BOOST_LOG(error) << "RTX HDR requires an SDR capture source and a CUDA/GL NVIDIA encoder on Linux.";
return {};
}
#endif
result->colorspace = colorspace;
result->rtx_hdr_active = config.rtx_hdr_active;
// Resolve the stream's HDR metadata once, here, so every consumer (control-channel
// HDR mode message, mastering-display SEI) reports the same values. Never query the
// display while it reads SDR: on Windows get_hdr_metadata() succeeds unconditionally
// and would return SDR-mode luminance, which clients like moonlight-xbox push straight
// into the HDMI HDR InfoFrame.
if (colorspace_is_hdr(colorspace)) {
#ifdef SUNSHINE_ENABLE_NV_TRUEHDR
if (config.rtx_hdr_active) {
// SDR source + TrueHDR: report the conversion's target peak, not the capture panel's.
result->hdr_metadata = synthesize_rtx_hdr_metadata(config.rtx_hdr_peak_nits);
result->hdr_metadata_valid = true;
BOOST_LOG(info) << "RTX HDR: synthesized HDR10 metadata (peak " << result->hdr_metadata.maxDisplayLuminance << " nits) for SDR source";
} else
#endif
if (display_is_hdr && disp.get_hdr_metadata(result->hdr_metadata)) {
result->hdr_metadata_valid = true;
if (hdr_latch) {
hdr_latch->metadata = result->hdr_metadata;
hdr_latch->metadata_valid = true;
}
} else if (hdr_latch && hdr_latch->metadata_valid) {
// Latched HDR colorspace but the display transiently reads SDR; reuse the
// metadata captured when this session established HDR.
result->hdr_metadata = hdr_latch->metadata;
result->hdr_metadata_valid = true;
} else {
BOOST_LOG(error) << "Couldn't get display hdr metadata when colorspace selection indicates it should have one";
}
}
}
return result;
}
std::optional<sync_session_t> make_synced_session(std::shared_ptr<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;
const auto initialization_deadline = std::chrono::steady_clock::now() + 5s;
initialization_cancel_t initialization_cancelled = [&]() {
return ctx.shutdown_event && ctx.shutdown_event->peek();
};
std::unique_ptr<platf::encode_device_t> encode_device;
std::unique_ptr<encode_session_t> session;
bool session_hdr_metadata_valid = false;
SS_HDR_METADATA session_hdr_metadata {};
#ifdef _WIN32
if (&encoder == &amdvce_ffmpeg) {
bool legacy_cancelled = false;
bool legacy_gate_contended = false;
auto legacy = make_legacy_amf_session_bounded(
disp, ctx.config, img.width, img.height, &ctx.hdr_latch,
initialization_deadline, initialization_cancelled,
legacy_cancelled, legacy_gate_contended);
if (legacy_cancelled || legacy_gate_contended) return std::nullopt;
if (legacy) {
session = std::move(legacy->session);
session_hdr_metadata_valid = legacy->hdr_metadata_valid;
session_hdr_metadata = legacy->hdr_metadata;
}
} else
#endif
{
encode_device = make_encode_device(*disp, encoder, ctx.config, &ctx.hdr_latch);
if (encode_device) {
session_hdr_metadata_valid = encode_device->hdr_metadata_valid;
session_hdr_metadata = encode_device->hdr_metadata;
}
}
if (!encode_device && !session) return std::nullopt;
// absolute mouse coordinates require that the dimensions of the screen are known
ctx.touch_port_events->raise(make_port(disp.get(), ctx.config));
// Update client with our current HDR stream state
hdr_info_t hdr_info = std::make_unique<hdr_info_raw_t>(false);
if (session_hdr_metadata_valid) {
hdr_info = std::make_unique<hdr_info_raw_t>(true, session_hdr_metadata);
}
raise_hdr_info_if_changed(ctx.hdr_events, ctx.last_hdr_info, std::move(hdr_info));
if (!session) {
session = make_encode_session(
disp.get(), encoder, ctx.config, img.width, img.height,
std::move(encode_device), initialization_deadline, initialization_cancelled);
}
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.bootstrap.allow_placeholder_before_first_real = ctx.frame_nr <= 1;
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,
std::vector<std::string> &display_names,
int &display_p,
std::uint64_t &switch_display_generation,
std::optional<std::string> &pending_switch_output
) {
const auto *enc_ptr = chosen_encoder;
if (!enc_ptr) {
BOOST_LOG(error) << "No encoder available for sync encoding"sv;
return encode_e::error;
}
const auto &encoder = *enc_ptr;
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)));
}
const auto manual_switch_selection =
synced_session_ctxs.front()->config.capture_source == capture_source_e::active_output ?
video::policy::capture_selection_e::process_preferred :
video::policy::capture_selection_e::exact_output;
std::size_t display_retry_failures = 0;
while (encode_session_ctx_queue.running()) {
// Admit queued peers before checking shutdown. A peer can disconnect
// while the first session is still waiting for its capture display.
while (auto pending_ctx = encode_session_ctx_queue.pop(0ms)) {
synced_session_ctxs.emplace_back(std::make_unique<sync_session_ctx_t>(std::move(*pending_ctx)));
}
// Capture readiness may outlive the client. Release stopped sessions here
// rather than waiting for a display before acknowledging their shutdown.
std::erase_if(synced_session_ctxs, [](const auto &ctx) {
if (!ctx->shutdown_event->peek()) {
return false;
}
ctx->join_event->raise(true);
return true;
});
if (synced_session_ctxs.empty()) {
return encode_e::ok;
}
#ifdef _WIN32
wait_for_recent_display_apply_stability();
#endif
// Refresh display names since a display removal might have caused the reinitialization
const auto exact_output = synced_session_ctxs.front()->config.capture_source == capture_source_e::exact_output;
const auto required_output = exact_output ? synced_session_ctxs.front()->config.capture_output : std::nullopt;
refresh_displays(encoder.platform_formats->dev_type, display_names, display_p, required_output, exact_output, policy::should_log_display_retry(display_retry_failures));
const bool allow_process_display_preference = video::policy::may_apply_process_display_preference(
synced_session_ctxs.front()->config.capture_source == capture_source_e::active_output ?
video::policy::capture_selection_e::process_preferred :
video::policy::capture_selection_e::exact_output
);
if (!ensure_virtual_display_ready(display_names, display_p, allow_process_display_preference)) {
wait_before_display_retry(display_retry_failures);
continue;
}
// Process any pending display switch with the new list of displays.
// Negative values mean "reinit only; keep display selection logic intact".
if (auto requested = switch_display_event->view_if_newer(switch_display_generation)) {
pending_switch_output = video::policy::select_manual_display_output(
manual_switch_selection,
*requested,
display_names
);
}
if (pending_switch_output) {
if (auto requested_index = video::policy::resolve_display_output(*pending_switch_output, display_names)) {
display_p = *requested_index;
}
pending_switch_output.reset();
}
// 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;
}
wait_before_display_retry(display_retry_failures);
}
if (!disp) {
return encode_e::error;
}
const bool event_driven_capture = disp->is_event_driven_capture();
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, 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 {
if (frame_captured && !img) {
BOOST_LOG(error) << "Capture backend reported a frame without an image"sv;
ec = platf::capture_e::error;
return false;
}
if (event_driven_capture && !frame_captured && encode_session_ctx_queue.peek()) {
disp->request_refresh();
}
while (!event_driven_capture || frame_captured) {
auto encode_session_ctx = encode_session_ctx_queue.pop(0ms);
if (!encode_session_ctx) break;
synced_session_ctxs.emplace_back(std::make_unique<sync_session_ctx_t>(std::move(*encode_session_ctx)));
auto encode_session = make_synced_session(disp, 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()) {
if (!event_driven_capture) {
return false;
}
if (!encode_session_ctx_queue.wait_for_data(50ms)) {
return false;
}
disp->request_refresh();
}
continue;
}
if (ctx->idr_events->peek()) {
pos->session->request_idr_frame();
ctx->idr_events->pop();
}
if (ctx->bitrate_events->peek()) {
// Coalesce rapid ABR updates to the latest requested value.
std::optional<int> latest_bitrate;
while (ctx->bitrate_events->peek()) {
if (auto new_bitrate = ctx->bitrate_events->pop(0ms)) {
latest_bitrate = *new_bitrate;
}
}
if (latest_bitrate) {
ctx->config.bitrate = *latest_bitrate;
ctx->config.client_requested_bitrate = *latest_bitrate;
if (pos->session->set_bitrate(*latest_bitrate)) {
BOOST_LOG(info) << "Applied runtime bitrate "sv << *latest_bitrate << " kbps (live, sync)"sv;
} else {
// avcodec encoder: rebuild synced sessions from their (now-updated) ctx config.
BOOST_LOG(info) << "Rebuilding encoder to apply runtime bitrate "sv << *latest_bitrate << " kbps (sync)"sv;
ec = platf::capture_e::reinit;
return false;
}
}
}
std::optional<std::chrono::steady_clock::time_point> frame_timestamp;
std::optional<std::chrono::steady_clock::time_point> capture_timestamp;
std::optional<std::chrono::steady_clock::time_point> host_processing_timestamp;
bool placeholder_input = pos->bootstrap.current_input_placeholder;
if (frame_captured) {
placeholder_input = is_placeholder_capture_image(*img);
if (!placeholder_input && pos->bootstrap.current_input_placeholder) {
pos->session->request_idr_frame();
}
if (!placeholder_input) {
capture_timestamp = img->frame_timestamp;
host_processing_timestamp = img->host_processing_timestamp;
}
if (pos->session->convert(*img)) {
BOOST_LOG(error) << "Could not convert image"sv;
ctx->shutdown_event->raise(true);
continue;
}
#ifdef SUNSHINE_ENABLE_NV_TRUEHDR
if (refresh_rtx_hdr_metadata_if_needed(
ctx->config,
pos->session.get(),
ctx->hdr_events,
ctx->last_hdr_info,
ctx->rtx_hdr_metadata_refresh
)) {
pos->session->request_idr_frame();
}
#endif
pos->bootstrap.current_input_placeholder = placeholder_input;
if (!placeholder_input) {
pos->bootstrap.real_frame_seen = true;
frame_timestamp = capture_timestamp;
}
} else {
placeholder_input = pos->bootstrap.current_input_placeholder;
}
if (placeholder_input && !pos->bootstrap.should_encode_placeholder()) {
++pos;
continue;
}
if (encode(ctx->frame_nr++, *pos->session, ctx->packets, ctx->channel_data, frame_timestamp, capture_timestamp, host_processing_timestamp)) {
BOOST_LOG(error) << "Could not encode video packet"sv;
ctx->shutdown_event->raise(true);
continue;
}
if (placeholder_input) {
auto *amf_session = dynamic_cast<amf_encode_session_t *>(pos->session.get());
pos->bootstrap.placeholder_encoded = !amf_session || amf_session->has_emitted_any_frame();
}
pos->session->request_normal_frame();
++pos;
})
if (auto requested = switch_display_event->view_if_newer(switch_display_generation)) {
pending_switch_output = video::policy::select_manual_display_output(
manual_switch_selection,
*requested,
display_names
);
if (*requested < 0 || pending_switch_output) {
ec = platf::capture_e::reinit;
return false;
}
}
if (!encode_session_ctx_queue.running()) {
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();
img_out->host_processing_timestamp.reset();
img_out->capture_pacing_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 take place on this thread. Late frames here turn into
// late hand-offs to the broadcast thread, producing the burst-then-idle pattern
// the send pacer is meant to absorb. Runs at critical (THREAD_PRIORITY_HIGHEST /
// nice -15, not a realtime class) — the same level the async capture thread uses.
platf::set_thread_name("video::capture_sync");
platf::adjust_thread_priority(platf::thread_priority_e::critical);
std::vector<std::string> display_names;
int display_p = -1;
auto switch_display_event = mail::man->event<int>(mail::switch_display);
std::uint64_t switch_display_generation {switch_display_event->generation()};
std::optional<std::string> pending_switch_output;
while (encode_run_sync(
synced_session_ctxs,
ctx,
display_names,
display_p,
switch_display_generation,
pending_switch_output
) == encode_e::reinit) {}
}
/**
* @brief Stream colorspace and HDR info for a PyroWave session.
*
* Mirrors make_encode_device(), including the per-session HDR latch that keeps a
* transient SDR reading during a capture reinit from downgrading an HDR stream.
* RTX HDR is never active for PyroWave sessions (see rtsp.cpp).
*/
sunshine_colorspace_t pyrowave_session_colorspace(platf::display_t &disp, const config_t &config, hdr_latch_t &hdr_latch, hdr_info_t &hdr_info) {
const bool display_is_hdr = disp.is_hdr();
bool hdr_display = display_is_hdr;
if (config.dynamicRange > 0 && !config.prefer_sdr_10bit && !config.force_sdr) {
if (hdr_display) {
hdr_latch.latched = true;
} else if (hdr_latch.latched) {
BOOST_LOG(info) << "Display momentarily reported SDR during reinit; keeping HDR colorspace for this HDR session.";
hdr_display = true;
}
}
const auto colorspace = colorspace_from_client_config(config, hdr_display);
hdr_info = std::make_unique<hdr_info_raw_t>(false);
if (colorspace_is_hdr(colorspace)) {
SS_HDR_METADATA metadata {};
if (display_is_hdr && disp.get_hdr_metadata(metadata)) {
hdr_latch.metadata = metadata;
hdr_latch.metadata_valid = true;
hdr_info = std::make_unique<hdr_info_raw_t>(true, metadata);
} else if (hdr_latch.metadata_valid) {
hdr_info = std::make_unique<hdr_info_raw_t>(true, hdr_latch.metadata);
} else {
BOOST_LOG(error) << "Couldn't get display hdr metadata when colorspace selection indicates it should have one";
}
}
const auto color_coding = colorspace.colorspace == colorspace_e::bt2020 ? "HDR (Rec. 2020 + SMPTE 2084 PQ)" :
colorspace.colorspace == colorspace_e::rec601 ? "SDR (Rec. 601)" :
colorspace.colorspace == colorspace_e::rec709 ? "SDR (Rec. 709)" :
colorspace.colorspace == colorspace_e::bt2020sdr ? "SDR (Rec. 2020)" :
"unknown";
BOOST_LOG(info) << "Creating encoder [pyrowave]";
BOOST_LOG(info) << "Color coding: " << color_coding;
BOOST_LOG(info) << "Color depth: " << colorspace.bit_depth << "-bit";
BOOST_LOG(info) << "Color range: " << (colorspace.full_range ? "JPEG" : "MPEG");
return colorspace;
}
/**
* @brief Encode loop of a PyroWave session (docs/pyrowave-protocol.md).
*
* PyroWave bypasses encoder_t: every frame is intra-coded, so IDR requests and
* reference frame invalidation need no action, and a bitrate change only moves the
* per-frame byte budget. When no new capture arrives within the minimum-FPS
* interval (by default a fifth of the stream rate), the last image is encoded again.
*
* @return true when the capture side is reinitializing and the session should
* continue with the new display; false when the stream is over or the
* encoder failed.
*/
bool encode_run_pyrowave(
int &frame_nr,
safe::mail_t mail,
img_event_t images,
config_t &config,
std::shared_ptr<platf::display_t> disp,
const sunshine_colorspace_t &colorspace,
safe::signal_t &reinit_event,
void *channel_data
) {
pyrowave::host::session_params_t params;
params.width = config.width;
params.height = config.height;
params.yuv444 = config.chromaSamplingType == 1;
params.colorspace = colorspace;
params.framerate = config.framerate;
params.bitrate_kbps = config.bitrate;
params.framing = config.pyrowave_framing;
params.packetsize = config.packetsize;
params.critical_fec = config::stream.pyrowave_critical_fec_percentage > 0;
auto encoder = pyrowave::host::make_encoder(params, disp);
if (!encoder) {
BOOST_LOG(error) << "PyroWave: could not create the encoder for "sv << config.width << 'x' << config.height;
return false;
}
// Serialize GPU device destruction with the other encoders' teardown.
auto destroy_encoder = util::fail_guard([&encoder]() {
std::lock_guard lg {encode_session_teardown_mutex};
encoder.reset();
});
// Independent frames recover from packet loss on the next submission. Default
// to the negotiated cadence. With record FEC, image size stays bounded by
// that cadence's allowance and slower submissions leave room for parity.
const double minimum_fps_target = (config::video.minimum_fps_target > 0.0) ?
std::min(config::video.minimum_fps_target, double(config.framerate)) :
double(config.framerate);
const std::chrono::duration<double, std::milli> max_frametime {1000.0 / minimum_fps_target};
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);
auto bitrate_events = mail->event<int>(mail::dynamic_bitrate);
// Encoded until the first capture arrives, so the client sees the stream come up.
std::shared_ptr<platf::img_t> last_img = disp->alloc_img();
if (!last_img || disp->dummy_img(last_img.get())) {
BOOST_LOG(error) << "PyroWave: could not allocate the initial frame"sv;
return false;
}
std::vector<std::uint8_t> frame;
std::size_t critical_bytes = 0;
pyrowave::policy::detail_fec_controller_t detail_fec_controller(config.framerate);
while (true) {
const bool reinit_pending = reinit_event.peek() && frame_nr > 1;
if (shutdown_event->peek() || !images->running() || reinit_pending) {
return reinit_pending && !shutdown_event->peek() && images->running();
}
std::optional<int> latest_bitrate;
while (bitrate_events->peek()) {
if (auto new_bitrate = bitrate_events->pop(0ms)) {
latest_bitrate = *new_bitrate;
}
}
if (latest_bitrate) {
config.bitrate = *latest_bitrate;
config.client_requested_bitrate = *latest_bitrate;
encoder->set_bitrate(*latest_bitrate);
}
while (invalidate_ref_frames_events->peek()) {
(void) invalidate_ref_frames_events->pop(0ms);
}
if (idr_events->peek()) {
(void) idr_events->pop();
}
std::optional<std::chrono::steady_clock::time_point> frame_timestamp;
std::optional<std::chrono::steady_clock::time_point> host_processing_timestamp;
if (auto img = images->pop(max_frametime)) {
if (!is_placeholder_capture_image(*img)) {
frame_timestamp = img->frame_timestamp;
host_processing_timestamp = img->host_processing_timestamp;
}
last_img = std::move(img);
} else if (!images->running()) {
return false;
}
const auto submitted_at = std::chrono::steady_clock::now();
encoder->on_frame(submitted_at);
const int result = encoder->encode(*last_img, frame, critical_bytes);
if (result < 0) {
BOOST_LOG(error) << "PyroWave: encoding failed; ending the video stream"sv;
return false;
}
if (result > 0) {
continue;
}
const auto detail_fec = params.critical_fec && params.framing == pyrowave::policy::framing_e::records ?
detail_fec_controller.observe(frame, submitted_at, config.bitrate) :
pyrowave::policy::detail_fec_t {};
auto packet = std::make_unique<packet_raw_generic>(std::move(frame), frame_nr++, true);
frame = {};
packet->channel_data = channel_data;
packet->pyrowave_critical_bytes = critical_bytes;
packet->pyrowave_detail_fec_percentage = detail_fec.percentage;
packet->pyrowave_frame_wire_budget = detail_fec.frame_wire_budget;
packet->frame_timestamp = frame_timestamp;
packet->host_processing_timestamp = host_processing_timestamp;
packet->packet_enqueue_timestamp = std::chrono::steady_clock::now();
// Each frame is independent. Replace this session's pending frame when
// sending falls behind, without discarding frames belonging to other sessions.
packets->raise_latest(std::move(packet), [channel_data](const packet_t &pending) {
return pending->channel_data == channel_data;
});
// While streaming check to see if the mouse is present and enable Mouse Keys to force the cursor to appear
// This is useful for KVM switch scenarios where mouse may disappear during streaming
platf::enable_mouse_keys();
}
}
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>();
// A capture worker belongs to the session's source key. The former shared
// worker selected its first context's display for every later context,
// which made a second Remote Monitor silently capture the wrong output.
capture_thread_async_ctx_t source_ctx {};
if (start_capture_async(source_ctx) != 0) {
return;
}
auto stop_source = util::fail_guard([&]() {
end_capture_async(source_ctx);
});
auto lg = util::fail_guard([&]() {
images->stop();
shutdown_event->raise(true);
});
source_ctx.capture_ctx_queue->raise(capture_ctx_t {images, config});
if (!source_ctx.capture_ctx_queue->running()) {
return;
}
int frame_nr = 1;
// Per-session HDR latch and last-raised HDR info, persisting across capture reinits
// so a transient SDR display reading cannot downgrade or re-signal an HDR stream.
hdr_latch_t hdr_latch;
std::optional<hdr_info_raw_t> last_hdr_info;
rtx_hdr_metadata_refresh_state_t rtx_hdr_metadata_refresh;
auto touch_port_event = mail->event<input::touch_port_t>(mail::touch_port);
auto hdr_event = mail->event<hdr_info_t>(mail::hdr);
#if defined(_WIN32) || (defined(__linux__) && defined(SUNSHINE_BUILD_CUDA))
int consecutive_encoder_initialization_failures = 0;
#endif
#ifdef _WIN32
int consecutive_native_amf_runtime_failures = 0;
#endif
// Encoding takes place on this thread (async-capture mode; capture lives in
// capture_thread_async at critical already). Match it so neither half of the
// pipeline waits on the other for a scheduler quantum. Critical is
// THREAD_PRIORITY_HIGHEST / nice -15, not a realtime class.
platf::adjust_thread_priority(platf::thread_priority_e::critical);
while (!shutdown_event->peek() && images->running()) {
// Wait for the main capture event when the display is being reinitialized
if (source_ctx.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 = source_ctx.display_wp.lock();
if (source_ctx.display_wp->expired()) {
continue;
}
display = source_ctx.display_wp->lock();
}
if (config.videoFormat == 3) {
hdr_info_t hdr_info;
const auto colorspace = pyrowave_session_colorspace(*display, config, hdr_latch, hdr_info);
// absolute mouse coordinates require that the dimensions of the screen are known
touch_port_event->raise(make_port(display.get(), config));
raise_hdr_info_if_changed(hdr_event, last_hdr_info, std::move(hdr_info));
if (!encode_run_pyrowave(frame_nr, mail, images, config, display, colorspace, source_ctx.reinit_event, channel_data)) {
// Shutdown, or an encoder failure: end the session either way.
return;
}
continue;
}
auto *enc_ptr = chosen_encoder;
if (!enc_ptr) {
BOOST_LOG(error) << "No encoder available for async capture"sv;
return;
}
auto &encoder = *enc_ptr;
const auto initialization_deadline = std::chrono::steady_clock::now() + 5s;
initialization_cancel_t initialization_cancelled = [&]() {
return shutdown_event->peek() || source_ctx.reinit_event.peek() || !images->running();
};
std::unique_ptr<platf::encode_device_t> encode_device;
std::unique_ptr<encode_session_t> prepared_session;
bool session_hdr_metadata_valid = false;
SS_HDR_METADATA session_hdr_metadata {};
bool initialization_was_cancelled = false;
bool initialization_gate_contended = false;
#ifdef _WIN32
if (&encoder == &amdvce_ffmpeg) {
auto legacy = make_legacy_amf_session_bounded(
display, config, display->width, display->height, &hdr_latch,
initialization_deadline, initialization_cancelled,
initialization_was_cancelled, initialization_gate_contended);
if (legacy) {
prepared_session = std::move(legacy->session);
session_hdr_metadata_valid = legacy->hdr_metadata_valid;
session_hdr_metadata = legacy->hdr_metadata;
}
} else
#endif
{
encode_device = make_encode_device(*display, encoder, config, &hdr_latch);
if (encode_device) {
session_hdr_metadata_valid = encode_device->hdr_metadata_valid;
session_hdr_metadata = encode_device->hdr_metadata;
}
}
#ifdef _WIN32
if (initialization_was_cancelled) continue;
if (!encode_device && !prepared_session && &encoder == &amdvce_experimental) {
BOOST_LOG(error) << "AMF: native device creation failed; refusing silent amdvce_ffmpeg fallback"sv;
}
#endif
#if defined(__linux__) && defined(SUNSHINE_BUILD_CUDA)
if (!encode_device && !prepared_session && &encoder == &nvenc) {
++consecutive_encoder_initialization_failures;
if (consecutive_encoder_initialization_failures >= 3) {
BOOST_LOG(error) << "NvEnc: native device creation failed 3 times; ending the stream without changing encoder implementations"sv;
return;
}
const auto retry_delay = std::chrono::milliseconds(100 * (1 << (consecutive_encoder_initialization_failures - 1)));
BOOST_LOG(warning) << "NvEnc: native device creation failed; retrying in " << retry_delay.count()
<< "ms without falling back to FFmpeg NVENC"sv;
std::this_thread::sleep_for(retry_delay);
continue;
}
#endif
if (initialization_was_cancelled) continue;
if (initialization_gate_contended && !encode_device && !prepared_session) {
std::this_thread::sleep_for(100ms);
continue;
}
if (!encode_device && !prepared_session) {
return;
}
auto &session_encoder = *enc_ptr;
// 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 stream state
hdr_info_t hdr_info = std::make_unique<hdr_info_raw_t>(false);
if (session_hdr_metadata_valid) {
hdr_info = std::make_unique<hdr_info_raw_t>(true, session_hdr_metadata);
}
raise_hdr_info_if_changed(hdr_event, last_hdr_info, std::move(hdr_info));
const auto encode_result = encode_run(
frame_nr,
mail,
images,
config,
display,
std::move(encode_device),
std::move(prepared_session),
source_ctx.reinit_event,
session_encoder,
&hdr_latch,
channel_data,
initialization_deadline,
initialization_cancelled,
last_hdr_info,
rtx_hdr_metadata_refresh
);
#ifdef _WIN32
if (encode_result == encode_run_result_e::native_amf_failed && &session_encoder == &amdvce_experimental) {
// Runtime fatals (TDR, sustained backpressure, output stalls) are
// classified by the encoder layer as reinit requests. Rebuild the same
// native session like every other encoder does — never a silent
// amdvce_ffmpeg fallback — but stay bounded so a wedged driver cannot
// busy-loop the stream.
if (native_amf_lifecycle_gate.is_quarantined()) {
BOOST_LOG(error) << "AMF: native runtime failed while quarantined; ending the stream. Host restart is required before retrying AMD encoding"sv;
return;
}
++consecutive_native_amf_runtime_failures;
if (consecutive_native_amf_runtime_failures >= 3) {
BOOST_LOG(error) << "AMF: native runtime failed 3 times without a stable session; ending the stream without changing encoder implementations"sv;
return;
}
BOOST_LOG(warning) << "AMF: native runtime failed; rebuilding the native session (attempt "
<< consecutive_native_amf_runtime_failures << " of 3)"sv;
continue;
}
#endif
if (encode_result == encode_run_result_e::initialization_failed) {
#ifdef _WIN32
if (&session_encoder != &amdvce_experimental && &session_encoder != &amdvce_ffmpeg) {
continue;
}
if (native_amf_lifecycle_gate.is_quarantined()) {
BOOST_LOG(error) << "AMF: ending the stream after a watchdog timeout; host restart is required before retrying AMD encoding"sv;
return;
}
++consecutive_encoder_initialization_failures;
if (consecutive_encoder_initialization_failures >= 3) {
BOOST_LOG(error) << "Encoder initialization failed 3 times; ending the stream instead of busy-looping"sv;
return;
}
const auto retry_delay = std::chrono::milliseconds(100 * (1 << (consecutive_encoder_initialization_failures - 1)));
BOOST_LOG(warning) << "Encoder initialization failed; retrying in " << retry_delay.count() << "ms";
std::this_thread::sleep_for(retry_delay);
continue;
#else
#if defined(__linux__) && defined(SUNSHINE_BUILD_CUDA)
if (&session_encoder == &nvenc) {
++consecutive_encoder_initialization_failures;
if (consecutive_encoder_initialization_failures >= 3) {
BOOST_LOG(error) << "NvEnc: native session initialization failed 3 times; ending the stream without changing encoder implementations"sv;
return;
}
const auto retry_delay = std::chrono::milliseconds(100 * (1 << (consecutive_encoder_initialization_failures - 1)));
BOOST_LOG(warning) << "NvEnc: native session initialization failed; retrying in " << retry_delay.count()
<< "ms without falling back to FFmpeg NVENC"sv;
std::this_thread::sleep_for(retry_delay);
}
#endif
continue;
#endif
}
if (encode_result == encode_run_result_e::temporarily_busy) {
std::this_thread::sleep_for(100ms);
continue;
}
#if defined(_WIN32) || (defined(__linux__) && defined(SUNSHINE_BUILD_CUDA))
consecutive_encoder_initialization_failures = 0;
#endif
#ifdef _WIN32
consecutive_native_amf_runtime_failures = 0;
#endif
}
}
void capture(
safe::mail_t mail,
config_t config,
void *channel_data
) {
// Snapshot the encoder pointer to avoid races with concurrent probe_encoders() calls
auto *encoder = chosen_encoder;
if (!encoder) {
BOOST_LOG(error) << "No encoder available for capture"sv;
return;
}
auto idr_events = mail->event<bool>(mail::idr);
idr_events->raise(true);
// PyroWave sessions always run on the async path (see encode_run_pyrowave).
if ((encoder->flags & PARALLEL_ENCODING) || config.capture_source != capture_source_e::active_output ||
config.videoFormat == 3) {
capture_async(std::move(mail), config, channel_data);
} else {
safe::signal_t join_event;
auto ref = capture_thread_sync.ref();
auto shutdown_event = mail->event<bool>(mail::shutdown);
sync_session_ctx_t session_ctx {
&join_event,
shutdown_event,
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),
mail->event<int>(mail::dynamic_bitrate),
config,
1,
channel_data,
};
if (!video::policy::try_admit_capture_session(
ref->encode_session_ctx_queue,
std::move(session_ctx),
*shutdown_event,
join_event
)) {
BOOST_LOG(error) << "Shared capture session queue is full or stopped; rejecting the session."sv;
return;
}
// Wait for join signal
join_event.view();
}
}
enum validate_flag_e {
VUI_PARAMS = 0x01, ///< VUI parameters
};
int validate_config(std::shared_ptr<platf::display_t> disp, const encoder_t &encoder, const config_t &config) {
const int max_attempts = config.videoFormat >= 1 ? 3 : 1; // HEVC/AV1 can fail transiently during probing
// The tight submission/wall-clock bounds exist for AMF drivers that stall in
// INPUT_FULL; probing for every other encoder keeps the pre-existing limits
// so this AMD-only change cannot alter NVENC/QSV/software negotiation.
#ifdef _WIN32
const bool amf_probe = &encoder == &amdvce_experimental || &encoder == &amdvce_ffmpeg;
#else
const bool amf_probe = false;
#endif
const auto probe_timeout = amf_probe ? std::chrono::seconds {5} : std::chrono::seconds {60};
const int max_probe_submissions = amf_probe ? 64 : 256;
const auto probe_start = std::chrono::steady_clock::now();
const auto probe_deadline = probe_start + probe_timeout;
const auto codec_name = [&]() -> std::string_view {
switch (config.videoFormat) {
case 0:
return "H.264"sv;
case 1:
return "HEVC"sv;
case 2:
return "AV1"sv;
default:
return "codec"sv;
}
}();
for (int attempt = 1; attempt <= max_attempts; ++attempt) {
auto validate_once = [&]() -> util::optional_t<int> {
std::unique_ptr<encode_session_t> session;
#ifdef _WIN32
if (&encoder == &amdvce_ffmpeg) {
bool legacy_cancelled = false;
bool legacy_gate_contended = false;
auto legacy = make_legacy_amf_session_bounded(
disp, config, disp->width, disp->height, nullptr,
probe_deadline, []() { return false; },
legacy_cancelled, legacy_gate_contended);
if (legacy_cancelled || legacy_gate_contended) {
return util::false_v<util::optional_t<int>>;
}
if (legacy) session = std::move(legacy->session);
} else
#endif
{
auto encode_device = make_encode_device(*disp, encoder, config);
if (encode_device) {
session = make_encode_session(
disp.get(), encoder, config, disp->width, disp->height,
std::move(encode_device), probe_deadline, []() { return false; });
}
}
if (!session) {
return util::false_v<util::optional_t<int>>;
}
auto bounded_probe_teardown = util::fail_guard([&]() {
if (encoder.flags & OWNER_THREAD_TEARDOWN) {
// Apply the same context ownership rule during probes and live
// capture, including the FFmpeg CUDA and VAAPI conversion devices.
std::lock_guard lock {encode_session_teardown_mutex};
session.reset();
return;
}
#ifdef _WIN32
if (&encoder == &amdvce_ffmpeg) {
destroy_legacy_amf_session_bounded(session, "probe"sv);
return;
}
#endif
destroy_encode_session_bounded(session, "probe"sv);
});
// Keep the probe image alive while native AMF primes a lookahead pipeline.
// Every PA input is rendered into a newly reserved ring surface; repeatedly
// submitting the first surface cannot make progress if AMF still owns it.
auto probe_img = disp->alloc_img();
if (!probe_img || disp->dummy_img(probe_img.get()) || session->convert(*probe_img)) {
return util::false_v<util::optional_t<int>>;
}
session->request_idr_frame();
// Use a probe-local mail/queue to avoid stale packets from previous encoder sessions.
auto probe_mail = std::make_shared<safe::mail_raw_t>();
auto packets = probe_mail->queue<packet_t>(mail::video_packets);
// Bound the whole codec probe by both submissions and wall time. An AMF driver
// stalled in INPUT_FULL can make a single encode() call take hundreds of
// milliseconds, so the old 256-attempt limit could block startup for minutes
// and even outlive the service shutdown watchdog. The deadline is shared by
// HEVC/AV1 retries below, keeping the complete validation below one watchdog
// interval while still leaving ample time for a cold hardware encoder.
for (int probe_attempts = 0; !packets->peek(); ++probe_attempts) {
const auto now = std::chrono::steady_clock::now();
if (probe_attempts >= max_probe_submissions || now >= probe_deadline) {
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - probe_start);
BOOST_LOG(error) << "Encoder probe produced no packet after "sv << probe_attempts
<< " submissions in " << elapsed.count() << "ms; treating "sv
<< codec_name << " as unsupported."sv;
return util::false_v<util::optional_t<int>>;
}
// Every submitted surface needs a unique PTS. Reusing frame index 1 for
// hundreds of probe submissions violates the normal encoder contract and
// makes delayed/catch-up output indistinguishable from a duplicate frame.
if (probe_attempts > 0 && dynamic_cast<amf_encode_session_t *>(session.get()) &&
session->convert(*probe_img)) {
BOOST_LOG(error) << "Encoder probe could not prepare the next native AMF lookahead surface"sv;
return util::false_v<util::optional_t<int>>;
}
const auto probe_frame_index = static_cast<int64_t>(probe_attempts) + 1;
if (encode(probe_frame_index, *session, packets, nullptr, {}, {}, {})) {
return util::false_v<util::optional_t<int>>;
}
}
auto packet = packets->pop();
if (!packet->is_idr()) {
BOOST_LOG(error) << "First packet type is not an IDR frame"sv;
return util::false_v<util::optional_t<int>>;
}
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;
};
auto result = validate_once();
#if defined(__linux__) && defined(SUNSHINE_BUILD_CUDA)
if (&encoder == &nvenc && native_nvenc_runtime_quarantined.load(std::memory_order_acquire)) {
BOOST_LOG(error) << "NvEnc: native probe teardown was unsafe; rejecting the probe result and quarantining native NVENC until process restart"sv;
return -1;
}
#endif
if (result) {
return *result;
}
if (attempt < max_attempts && (!amf_probe || std::chrono::steady_clock::now() < probe_deadline)) {
BOOST_LOG(debug) << "Encoder probe: failed to validate "sv << codec_name << " config (attempt "sv
<< attempt << "/" << max_attempts << "), retrying."sv;
std::this_thread::sleep_for(std::chrono::milliseconds {50});
} else {
break;
}
}
return -1;
}
static thread_local std::shared_ptr<platf::display_t> cached_probe_display;
static thread_local platf::mem_type_e cached_display_type = platf::mem_type_e::system;
#if defined(__linux__) && defined(SUNSHINE_BUILD_CUDA)
static thread_local bool cached_display_is_native_nvenc_probe = false;
#endif
bool validate_encoder(
encoder_t &encoder,
const bool expect_failure,
const std::optional<platf::adapter_id_t> &required_adapter,
std::optional<platf::adapter_id_t> *actual_adapter,
const std::string &probe_display_name
) {
#ifdef _WIN32
(void) probe_display_name;
#endif
if (actual_adapter) {
actual_adapter->reset();
}
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();
auto clear_capabilities = [&]() {
encoder.h264.capabilities.reset();
encoder.hevc.capabilities.reset();
encoder.av1.capabilities.reset();
};
const auto reset_probe_display = [&](const config_t &probe_config) {
#ifdef _WIN32
// Encoder capability validation is intentionally independent of WGC,
// Desktop Duplication, GDI publication, and the interactive desktop.
// A short retry covers transient D3D device creation without waiting for
// display topology to converge.
disp.reset();
for (int attempt = 0; attempt < 2 && !disp; ++attempt) {
disp = make_synthetic_probe_display(
encoder.platform_formats->dev_type,
probe_config,
required_adapter
);
if (!disp && attempt == 0) {
std::this_thread::sleep_for(100ms);
}
}
#else
#if defined(SUNSHINE_BUILD_CUDA)
if (&encoder == &nvenc && !required_adapter) {
// Encoder capability probing does not need access to a scanout
// framebuffer. A blank GL/CUDA source isolates the native API probe
// from KMS privileges while the real stream still uses KMS capture.
BOOST_LOG(info) << "NvEnc: using a synthetic GL/CUDA source for encoder-only probing; KMS capture readiness is validated separately"sv;
disp = ::cuda::make_nvenc_probe_display(probe_config);
} else
#endif
{
reset_display(
disp,
encoder.platform_formats->dev_type,
probe_display_name,
probe_config,
required_adapter
);
}
#endif
};
// First, test encoder viability
config_t config_max_ref_frames {1920, 1080, 60, 6000, 1000, 1, 1, 1, 0, 0, 0};
config_t config_autoselect {1920, 1080, 60, 6000, 1000, 1, 0, 1, 0, 0, 0};
// If the encoder isn't supported at all (not even H.264), bail early
// Try to reuse cached display if same device type
const auto cached_adapter = cached_probe_display ? cached_probe_display->capture_adapter_id() : std::nullopt;
const bool cached_display_matches_required =
!required_adapter || (cached_adapter && *cached_adapter == *required_adapter);
#if defined(__linux__) && defined(SUNSHINE_BUILD_CUDA)
const bool wants_native_nvenc_probe = &encoder == &nvenc && !required_adapter;
const bool cached_probe_kind_matches =
cached_display_is_native_nvenc_probe == wants_native_nvenc_probe;
#else
const bool cached_probe_kind_matches = true;
#endif
if (cached_probe_display &&
cached_display_type == encoder.platform_formats->dev_type &&
cached_probe_kind_matches &&
cached_display_matches_required) {
disp = cached_probe_display;
} else {
reset_probe_display(config_autoselect);
cached_probe_display = disp;
cached_display_type = encoder.platform_formats->dev_type;
#if defined(__linux__) && defined(SUNSHINE_BUILD_CUDA)
cached_display_is_native_nvenc_probe = wants_native_nvenc_probe;
#endif
}
if (!disp) {
clear_capabilities();
return false;
}
const auto initial_probe_adapter = disp->capture_adapter_id();
if (required_adapter && (!initial_probe_adapter || *initial_probe_adapter != *required_adapter)) {
clear_capabilities();
BOOST_LOG(error)
<< "Encoder probe display did not initialize on its required adapter; refusing cross-adapter validation.";
return false;
}
if (!disp->is_codec_supported(encoder.h264.name, config_autoselect)) {
fg.disable();
clear_capabilities();
BOOST_LOG(info) << "Encoder ["sv << encoder.name << "] is not supported on this GPU"sv;
return false;
}
// 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) {
clear_capabilities();
BOOST_LOG(warning) << "Encoder ["sv << encoder.name
<< "] failed H.264 validation before HEVC/AV1 capability probing completed; higher codec support is unknown"sv;
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)) {
auto max_ref_frames_hevc = validate_config(disp, encoder, config_max_ref_frames);
// If H.264 succeeded with max ref frames specified, assume that we can count on
// HEVC to also succeed with max ref frames specified if HEVC is supported.
auto autoselect_hevc = (max_ref_frames_hevc >= 0 || max_ref_frames_h264 >= 0) ?
max_ref_frames_hevc :
validate_config(disp, encoder, config_autoselect);
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)) {
auto max_ref_frames_av1 = validate_config(disp, encoder, config_max_ref_frames);
// If H.264 succeeded with max ref frames specified, assume that we can count on
// AV1 to also succeed with max ref frames specified if AV1 is supported.
auto autoselect_av1 = (max_ref_frames_av1 >= 0 || max_ref_frames_h264 >= 0) ?
max_ref_frames_av1 :
validate_config(disp, encoder, config_autoselect);
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();
}
// Test HDR and YUV444 support
{
// H.264 is special because encoders may support YUV 4:4:4 without supporting 10-bit color depth
if (encoder.flags & YUV444_SUPPORT) {
config_t config_h264_yuv444 {1920, 1080, 60, 6000, 1000, 1, 0, 1, 0, 0, 1};
encoder.h264[encoder_t::YUV444] = disp->is_codec_supported(encoder.h264.name, config_h264_yuv444) &&
validate_config(disp, encoder, config_h264_yuv444) >= 0;
} else {
encoder.h264[encoder_t::YUV444] = false;
}
const config_t generic_hdr_config = {1920, 1080, 60, 6000, 1000, 1, 0, 3, 1, 1, 0};
// A capture backend can reject HDR entirely (stock Gamescope is SDR).
// Keep its successful SDR probes instead of failing the whole encoder
// when constructing an unsupported HDR capture surface.
if (disp->is_codec_supported(encoder.hevc.name, generic_hdr_config) ||
disp->is_codec_supported(encoder.av1.name, generic_hdr_config)) {
// Reset the synthetic surface since we're switching from SDR to HDR.
// The D3D adapter remains exact while the fake source format changes to
// FP16, exercising the real 10-bit encoder conversion path.
cached_probe_display.reset();
reset_probe_display(generic_hdr_config);
if (!disp) {
return false;
}
const auto hdr_probe_adapter = disp->capture_adapter_id();
if (required_adapter && (!hdr_probe_adapter || *hdr_probe_adapter != *required_adapter)) {
BOOST_LOG(error)
<< "HDR encoder probe display did not initialize on its required adapter; refusing cross-adapter validation.";
return false;
}
auto test_hdr_and_yuv444 = [&](auto &flag_map, auto video_format) {
auto config = generic_hdr_config;
config.videoFormat = video_format;
if (!flag_map[encoder_t::PASSED]) {
return;
}
auto encoder_codec_name = encoder.codec_from_config(config).name;
flag_map[encoder_t::YUV444] = false;
// Test the mandatory HDR 4:2:0 path first. Some encoders support AV1/HEVC
// Main10 but reject optional 4:4:4, and that must not mask HDR support.
// Keep DYNAMIC_RANGE tentatively enabled while probing because validate_config()
// gates dynamicRange configs on the current codec capability bit.
config.chromaSamplingType = 0;
if (disp->is_codec_supported(encoder_codec_name, config) &&
validate_config(disp, encoder, config) >= 0) {
flag_map[encoder_t::DYNAMIC_RANGE] = true;
} else {
flag_map[encoder_t::DYNAMIC_RANGE] = false;
return;
}
// Test optional HDR 4:4:4 after 4:2:0 has already established HDR support.
config.chromaSamplingType = 1;
if ((encoder.flags & YUV444_SUPPORT) &&
disp->is_codec_supported(encoder_codec_name, config) &&
validate_config(disp, encoder, config) >= 0) {
flag_map[encoder_t::YUV444] = true;
}
};
// HDR is not supported with H.264. Don't bother even trying it.
encoder.h264[encoder_t::DYNAMIC_RANGE] = false;
test_hdr_and_yuv444(encoder.hevc, 1);
test_hdr_and_yuv444(encoder.av1, 2);
} else {
encoder.h264[encoder_t::DYNAMIC_RANGE] = false;
encoder.hevc[encoder_t::DYNAMIC_RANGE] = false;
encoder.av1[encoder_t::DYNAMIC_RANGE] = false;
encoder.hevc[encoder_t::YUV444] = false;
encoder.av1[encoder_t::YUV444] = false;
}
}
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;
}
if (actual_adapter) {
*actual_adapter = disp->capture_adapter_id();
}
fg.disable();
return true;
}
/**
* @brief Check whether PyroWave can encode on the capture adapter.
* @return The active_pyrowave_mode for this probe: 2 when available, else 1.
*/
int probe_pyrowave(const std::optional<platf::adapter_id_t> &adapter) {
if (!config::video.pyrowave) {
BOOST_LOG(info) << "PyroWave is disabled by configuration"sv;
return 1;
}
#ifdef SUNSHINE_ENABLE_PYROWAVE
const auto started = std::chrono::steady_clock::now();
std::string detail;
const bool supported = pyrowave::host::probe(adapter, detail);
const auto elapsed = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - started);
if (supported) {
BOOST_LOG(info) << "Found PyroWave encoder [Vulkan] (probe took "sv << elapsed.count() << " ms)"sv;
return 2;
}
BOOST_LOG(info) << "PyroWave is unavailable: "sv << detail;
#else
(void) adapter;
#endif
return 1;
}
int probe_encoders() {
std::lock_guard<std::mutex> lock(encoder_probe_mutex);
const auto probe_target = resolve_probe_target();
const auto &required_adapter = probe_target.required_adapter;
const auto cache_key = build_probe_cache_key(&probe_target);
mark_probe_attempted(cache_key);
// Attempt ownership is frozen with the target selected at probe start.
// A topology/configuration change during validation must remain unattempted
// under its new key so the next request can probe it immediately.
auto final_attempt_key_guard = util::fail_guard([cache_key]() {
try {
mark_probe_attempted(cache_key);
} catch (...) {
BOOST_LOG(warning) << "Unable to record the final encoder probe adapter key.";
}
});
#ifdef _WIN32
{
const auto adapter_resolution = platf::resolve_adapter(
config::video.adapter_name,
config::video.adapter_pnp_id
);
const auto wgc_adapter_luid = platf::dxgi::get_last_wgc_adapter_luid();
BOOST_LOG(info)
<< "Encoder probe adapter identity: configured_name='" << config::video.adapter_name
<< "', configured_pnp_id='" << config::video.adapter_pnp_id
<< "', resolution=" << platf::adapter_resolution_status_name(adapter_resolution.status)
<< ", resolved_name='" << adapter_resolution.description
<< "', resolved_pnp_id='" << adapter_resolution.pnp_id
<< "', configured_output='" << config::get_active_output_name()
<< "', probe_output='" << probe_target.display_name
<< "', wgc_luid="
<< (wgc_adapter_luid ?
std::to_string(wgc_adapter_luid->HighPart) + ":" + std::to_string(wgc_adapter_luid->LowPart) :
std::string("<none>"))
<< ", effective_cache_adapter='" << cache_key.adapter_identity
<< "', effective_cache_source=" << cache_key.adapter_identity_source
<< '.';
}
#endif
const bool hevc_mode_auto = config::video.hevc_mode == 0;
const bool av1_mode_auto = config::video.av1_mode == 0;
const bool wants_hdr = (config::video.hevc_mode == 3) || (config::video.av1_mode == 3);
const bool wants_hevc = config::video.hevc_mode >= 2 || hevc_mode_auto;
const bool wants_hevc_hdr = config::video.hevc_mode == 3 || hevc_mode_auto;
const bool wants_av1 = config::video.av1_mode >= 2 || av1_mode_auto;
const bool wants_av1_hdr = config::video.av1_mode == 3 || av1_mode_auto;
if (probe_cache_matches(cache_key, wants_hdr, wants_hevc, wants_hevc_hdr, wants_av1, wants_av1_hdr)) {
BOOST_LOG(debug) << "Encoder probe skipped (cached success).";
return 0;
}
if (!allow_encoder_probing()) {
// Error already logged
update_probe_cache(cache_key, false, false, false, false, false, false);
return -1;
}
const auto previous_active_hevc_mode = active_hevc_mode;
const auto previous_active_av1_mode = active_av1_mode;
const auto previous_active_pyrowave_mode = active_pyrowave_mode.load();
const auto previous_last_ref_frames_invalidation = last_encoder_probe_supported_ref_frames_invalidation;
const auto previous_last_yuv444_for_codec = last_encoder_probe_supported_yuv444_for_codec;
auto previous_encoder = chosen_encoder;
auto restore_previous_probe_state = util::fail_guard([&]() {
active_hevc_mode = previous_active_hevc_mode;
active_av1_mode = previous_active_av1_mode;
active_pyrowave_mode = previous_active_pyrowave_mode;
last_encoder_probe_supported_ref_frames_invalidation = previous_last_ref_frames_invalidation;
last_encoder_probe_supported_yuv444_for_codec = previous_last_yuv444_for_codec;
});
auto encoder_list = encoders;
#if defined(__linux__) && defined(SUNSHINE_BUILD_CUDA)
const auto nvenc_selection_policy = nvenc::encoder_selection_policy(config::video.encoder);
if (!nvenc_selection_policy.include_native) {
encoder_list.erase(std::remove(encoder_list.begin(), encoder_list.end(), &nvenc), encoder_list.end());
}
#endif
#ifdef _WIN32
const auto amf_selection_policy = amf::lifecycle::encoder_selection_policy(config::video.encoder);
// The native encoder is experimental and must never be selected implicitly.
if (!amf_selection_policy.include_experimental) {
encoder_list.erase(std::remove(encoder_list.begin(), encoder_list.end(), &amdvce_experimental), encoder_list.end());
}
#endif
// Use a local variable for encoder selection during probing so that
// chosen_encoder is never null while concurrent capture threads may read it.
encoder_t *new_encoder = nullptr;
std::optional<platf::adapter_id_t> candidate_probe_adapter;
std::optional<platf::adapter_id_t> successful_probe_adapter;
const auto validate_probe_encoder = [&](encoder_t &encoder, const bool expect_failure) {
candidate_probe_adapter.reset();
return validate_encoder(
encoder,
expect_failure,
required_adapter,
&candidate_probe_adapter,
probe_target.display_name
);
};
active_hevc_mode = config::video.hevc_mode;
active_av1_mode = config::video.av1_mode;
last_encoder_probe_supported_ref_frames_invalidation = false;
last_encoder_probe_supported_yuv444_for_codec = {};
// Clear any cached display from previous probes to ensure fresh start
cached_probe_display.reset();
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_probe_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 requ