diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml
index 36c5e2c43..2c24dcd08 100644
--- a/.github/workflows/ci-windows.yml
+++ b/.github/workflows/ci-windows.yml
@@ -460,6 +460,20 @@ jobs:
cmake --build build --target all -j "$(nproc)"
echo "::remove-matcher owner=gcc::"
+ - name: Test native AMF lifecycle behavior
+ shell: msys2 {0}
+ run: |
+ set -euo pipefail
+ c++ \
+ -std=gnu++23 \
+ -O2 \
+ -pthread \
+ -DSUNSHINE_AMF_LIFECYCLE_STANDALONE \
+ -I. \
+ tests/unit/platform/windows/test_native_amf_review.cpp \
+ -o build/native-amf-lifecycle-test.exe
+ ./build/native-amf-lifecycle-test.exe
+
- name: Install cv2pdb
if: startsWith(github.ref, 'refs/tags/') || inputs.publish_symbols || inputs.upload_symbols_artifact
shell: pwsh
diff --git a/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake
index 145e5aa83..1946c8a60 100644
--- a/cmake/compile_definitions/windows.cmake
+++ b/cmake/compile_definitions/windows.cmake
@@ -242,6 +242,14 @@ set(PLATFORM_TARGET_FILES
"${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Common.h"
"${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Util.h"
"${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/km/BusShared.h"
+ # Native AMD AMF encoder (D3D11). AMF SDK headers are provided by the
+ # bundled FFmpeg build-deps (include/AMF), so no extra dependency is
+ # required - the runtime (amfrt64.dll) is loaded dynamically.
+ "${CMAKE_SOURCE_DIR}/src/amf/amf_config.h"
+ "${CMAKE_SOURCE_DIR}/src/amf/amf_encoded_frame.h"
+ "${CMAKE_SOURCE_DIR}/src/amf/amf_encoder.h"
+ "${CMAKE_SOURCE_DIR}/src/amf/amf_d3d11.h"
+ "${CMAKE_SOURCE_DIR}/src/amf/amf_d3d11.cpp"
${NVPREFS_FILES})
set(OPENSSL_LIBRARIES
diff --git a/docs/configuration.md b/docs/configuration.md
index c2facba66..4fa0ffaf1 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -2647,7 +2647,7 @@ editing the `conf` file in a text editor. Use the examples as reference.
@endcode
- | Choices |
+ Choices |
nvenc |
For NVIDIA graphics cards |
@@ -2657,7 +2657,13 @@ editing the `conf` file in a text editor. Use the examples as reference.
| amdvce |
- For AMD graphics cards |
+ For AMD graphics cards (native AMF encoder) |
+
+
+ | amdvce_legacy |
+ Explicit rollback to the FFmpeg-based AMD AMF encoder. Never selected automatically —
+ automatic probing and `amdvce` fail closed instead of silently falling back.
+ @note{Applies to Windows only.} |
| vaapi |
@@ -3420,7 +3426,7 @@ version is written to the log on every AMD HDR HEVC attempt (search for @code{AM
@endcode
- | Choices |
+ Choices |
cqp |
constant qp mode |
@@ -3436,6 +3442,48 @@ version is written to the log on every AMD HDR HEVC attempt (search for @code{AM
vbr_peak |
variable bitrate, peak constrained |
+
+ | qvbr |
+ quality-defined variable bitrate (see amd_qvbr_quality_level) |
+
+
+ | hqvbr |
+ high quality variable bitrate |
+
+
+ | hqcbr |
+ high quality constant bitrate |
+
+
+
+### amd_qvbr_quality_level
+
+
+
+ | Description |
+
+ The target quality level used by the `qvbr` rate control method, where 1 is the lowest quality and 51
+ is the highest. Higher values spend more bits to preserve quality.
+ @note{This option only applies to AMD [encoders](#encoder) with `amd_rc` set to `qvbr`. Native `amdvce` automatically enables PreAnalysis with a one-frame low-latency lookahead for `qvbr`, `hqvbr`, and `hqcbr`.}
+ @note{Leave this at `0` to keep the encoder default.}
+ |
+
+
+ | Default |
+ @code{}
+ 0
+ @endcode |
+
+
+ | Range |
+ 1-51 (0 to use the encoder default) |
+
+
+ | Example |
+ @code{}
+ amd_qvbr_quality_level = 18
+ @endcode |
+
### amd_enforce_hrd
@@ -3470,6 +3518,7 @@ version is written to the log on every AMD HDR HEVC attempt (search for @code{AM
Description |
The quality profile controls the tradeoff between speed and quality of encoding.
+ `auto` leaves the quality property unset so the selected AMF usage preset can choose it.
@note{This option only applies when using amdvce [encoder](#encoder).}
|
@@ -3486,7 +3535,11 @@ version is written to the log on every AMD HDR HEVC attempt (search for @code{AM
@endcode
- | Choices |
+ Choices |
+ auto |
+ follow the selected AMF usage preset |
+
+
| speed |
prefer speed |
@@ -3506,8 +3559,9 @@ version is written to the log on every AMD HDR HEVC attempt (search for @code{AM
| Description |
- Preanalysis can increase encoding quality at the cost of latency.
- @note{This option only applies when using amdvce [encoder](#encoder).}
+ Preanalysis can increase encoding quality at the cost of latency. Native `amdvce` uses a one-frame
+ low-latency lookahead; it is enabled automatically by `qvbr`, `hqvbr`, and `hqcbr`. The setting is
+ also forwarded to `amdvce_legacy`.
|
@@ -3532,6 +3586,8 @@ version is written to the log on every AMD HDR HEVC attempt (search for @code{AM
|
Variance Based Adaptive Quantization (VBAQ) can increase subjective visual quality by prioritizing
allocation of more bits to smooth areas compared to more textured areas.
+ `auto` leaves the property unset so the selected AMF usage preset can choose it. VBAQ is enabled
+ by default.
@note{This option only applies when using amdvce [encoder](#encoder).}
|
@@ -3547,6 +3603,19 @@ version is written to the log on every AMD HDR HEVC attempt (search for @code{AM
amd_vbaq = enabled
@endcode
+
+ | Choices |
+ auto |
+ follow the selected AMF usage preset |
+
+
+ | enabled |
+ enable VBAQ |
+
+
+ | disabled |
+ disable VBAQ |
+
### amd_coder
@@ -3575,7 +3644,7 @@ version is written to the log on every AMD HDR HEVC attempt (search for @code{AM
| Choices |
auto |
- let ffmpeg decide |
+ leave the encoder default |
| cabac |
@@ -3587,6 +3656,91 @@ version is written to the log on every AMD HDR HEVC attempt (search for @code{AM
+### amd_av1_screen_content
+
+
+
+ | Description |
+
+ Enable AV1 screen-content coding tools, which can improve efficiency and text/UI clarity for desktop and
+ screen-heavy content.
+ @note{AV1 only. This option only applies to the native amdvce [encoder](#encoder) (not amdvce_legacy).}
+ @note{Leave at `auto` to use the driver default.}
+ |
+
+
+ | Default |
+ @code{}
+ auto
+ @endcode |
+
+
+ | Example |
+ @code{}
+ amd_av1_screen_content = enabled
+ @endcode |
+
+
+ | Choices |
+ auto |
+ leave the driver default |
+
+
+ | enabled |
+ force screen-content tools on |
+
+
+ | disabled |
+ force screen-content tools off |
+
+
+
+### amd_av1_latency_mode
+
+
+
+ | Description |
+
+ AV1 encoding-latency tier. Lower tiers finish each frame faster at the cost of higher power draw.
+ @note{AV1 only. This option only applies to the native amdvce [encoder](#encoder) (not amdvce_legacy).}
+ @note{Leave at `auto` to use the driver default.}
+ |
+
+
+ | Default |
+ @code{}
+ auto
+ @endcode |
+
+
+ | Example |
+ @code{}
+ amd_av1_latency_mode = lowest
+ @endcode |
+
+
+ | Choices |
+ auto |
+ leave the driver default |
+
+
+ | none |
+ balance latency and power |
+
+
+ | power_saving |
+ real-time with lower power |
+
+
+ | realtime |
+ real-time |
+
+
+ | lowest |
+ lowest latency (highest power) |
+
+
+
## VideoToolbox Encoder
### vt_coder
diff --git a/src/amf/amf_config.h b/src/amf/amf_config.h
new file mode 100644
index 000000000..43b36fb5d
--- /dev/null
+++ b/src/amf/amf_config.h
@@ -0,0 +1,139 @@
+/**
+ * @file src/amf/amf_config.h
+ * @brief Declarations for AMF encoder configuration.
+ */
+#pragma once
+
+#include
+#include
+
+namespace amf {
+
+ /**
+ * @brief HDR metadata for AMF encoder.
+ */
+ struct amf_hdr_metadata {
+ struct {
+ uint16_t x; // Normalized to 50,000
+ uint16_t y; // Normalized to 50,000
+ } displayPrimaries[3]; // RGB order
+
+ struct {
+ uint16_t x; // Normalized to 50,000
+ uint16_t y; // Normalized to 50,000
+ } whitePoint;
+
+ uint16_t maxDisplayLuminance; // Nits
+ uint16_t minDisplayLuminance; // 1/10000th of a nit
+ uint16_t maxContentLightLevel; // Nits
+ uint16_t maxFrameAverageLightLevel; // Nits
+ };
+
+ /**
+ * @brief AMF encoder configuration.
+ * Integer values correspond directly to AMF SDK enum values.
+ */
+ struct amf_config {
+ // Usage preset (AMF_VIDEO_ENCODER_USAGE_ENUM values)
+ std::optional usage;
+
+ // Quality preset (AMF_VIDEO_ENCODER_QUALITY_PRESET_ENUM values)
+ std::optional quality_preset;
+
+ // Rate control mode (AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_ENUM values)
+ std::optional rc_mode;
+
+ // Pre-analysis enable
+ std::optional preanalysis;
+
+ // VBAQ enable
+ std::optional vbaq;
+
+ // H.264 entropy coding (0=CAVLC, 1=CABAC, nullopt=driver default)
+ std::optional h264_cabac;
+
+ // Enforce HRD
+ std::optional enforce_hrd;
+
+ // Number of LTR frames for RFI (0 = disabled, matches FFmpeg amfenc behavior).
+ // When enabled, static screen regions may retain encoder artifacts from the
+ // baseline LTR frame until motion forces a refresh; only opt in when the
+ // network actually needs reference-frame invalidation recovery.
+ int max_ltr_frames = 0;
+
+ // --- Pre-Analysis sub-system ---
+ // PAQ mode (AMF_PA_PAQ_MODE_ENUM): 0=none, 1=CAQ
+ std::optional pa_paq_mode;
+ // TAQ mode (AMF_PA_TAQ_MODE_ENUM): 0=none, 1=mode1, 2=mode2
+ std::optional pa_taq_mode;
+ // CAQ strength (AMF_PA_CAQ_STRENGTH_ENUM): 0=low, 1=medium, 2=high
+ std::optional pa_caq_strength;
+ // Lookahead buffer depth (0=disabled)
+ std::optional pa_lookahead_depth;
+ // Scene change detection sensitivity (AMF_PA_SCENE_CHANGE_DETECTION_SENSITIVITY_ENUM): 0=low, 1=medium, 2=high
+ std::optional pa_scene_change_sensitivity;
+ // High motion quality boost mode (AMF_PA_HIGH_MOTION_QUALITY_BOOST_MODE_ENUM): 0=none, 1=auto
+ std::optional pa_high_motion_quality_boost;
+ // Initial QP after scene change (0-51, 0=auto)
+ std::optional pa_initial_qp_after_scene_change;
+ // Activity type (AMF_PA_ACTIVITY_TYPE_ENUM): 0=Y, 1=YUV
+ std::optional pa_activity_type;
+
+ // --- QVBR quality level ---
+ // For QVBR rate control mode: quality level 1-51 (higher=better)
+ std::optional qvbr_quality_level;
+
+ // --- Smart Access Video ---
+ // Default nullopt = do not set the property, let the driver decide.
+ // This is distinct from HEVC/AV1 split-frame multi-HW instance encoding;
+ // AMF already enables that independently where appropriate.
+ std::optional smart_access_video;
+
+ // --- AV1 Encoding Latency Mode ---
+ // AMF_VIDEO_ENCODER_AV1_ENCODING_LATENCY_MODE_ENUM: 0=none, 1=power saving RT, 2=RT, 3=lowest latency
+ std::optional av1_encoding_latency_mode;
+
+ // --- AV1 Screen Content Tools ---
+ std::optional av1_screen_content_tools;
+ std::optional av1_palette_mode;
+ std::optional av1_force_integer_mv;
+
+ // --- Intra Refresh ---
+ // H.264: number of MBs per slot; HEVC: number of CTBs per slot; AV1: mode enum
+ std::optional intra_refresh_mbs;
+ // H.264 AMF exposes intra refresh only with more than one reference frame.
+ int intra_refresh_minimum_reference_frames = 0;
+ // AV1-specific: intra refresh mode (AMF_VIDEO_ENCODER_AV1_INTRA_REFRESH_MODE_ENUM)
+ std::optional av1_intra_refresh_mode;
+ // AV1-specific: number of frames/stripes in one intra-refresh cycle
+ std::optional av1_intra_refresh_stripes;
+
+ // --- Statistics feedback ---
+ bool enable_statistics_feedback = false;
+ bool enable_psnr_feedback = false;
+ bool enable_ssim_feedback = false;
+
+ // --- High Motion Quality Boost (encoder-level, separate from PA) ---
+ // Default nullopt = do not set the property, let the AMD driver decide
+ // (FFmpeg amfenc.c never sets this property either). Some AMD driver
+ // releases (e.g. Adrenalin 26.5.x on RDNA4) appear to expose latent VCN
+ // bugs when this is enabled, leading to encoder freezes after ~minutes.
+ std::optional high_motion_quality_boost_enable;
+
+ // --- Low Latency Mode (encoder-level) ---
+ // Default nullopt = do not set the property, let the driver default decide.
+ // Matches FFmpeg amfenc behavior (only set when the user opts in).
+ // Streaming workloads usually want this true, but exposing it lets users
+ // disable it as a workaround for driver bugs.
+ std::optional lowlatency_mode;
+
+ // --- Input Queue Size (async_depth) ---
+ // Default nullopt = do not set the property; AMD driver default ~16,
+ // matches FFmpeg amfenc default. Sunshine historically forced 1 for
+ // minimum latency, but that is the most fragile code path inside the
+ // driver. Users can opt-in to 1 for absolute lowest latency or larger
+ // values (4/8/16) as a workaround for driver freezes.
+ std::optional input_queue_size;
+ };
+
+} // namespace amf
diff --git a/src/amf/amf_d3d11.cpp b/src/amf/amf_d3d11.cpp
new file mode 100644
index 000000000..061e261f5
--- /dev/null
+++ b/src/amf/amf_d3d11.cpp
@@ -0,0 +1,2426 @@
+/**
+ * @file src/amf/amf_d3d11.cpp
+ * @brief Implementation of standalone AMF encoder with D3D11 texture input.
+ */
+
+#include "amf_d3d11.h"
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "src/config.h"
+#include "src/logging.h"
+#include "src/utility.h"
+
+namespace amf {
+
+ // AMF DLL function types
+ typedef AMF_RESULT(AMF_CDECL_CALL *AMFInit_Fn)(amf_uint64 version, ::amf::AMFFactory **ppFactory);
+ typedef AMF_RESULT(AMF_CDECL_CALL *AMFQueryVersion_Fn)(amf_uint64 *pVersion);
+
+ amf_d3d11::amf_d3d11(ID3D11Device *d3d_device):
+ device(d3d_device) {
+ for (std::size_t i = 0; i < input_surface_release_observers.size(); ++i) {
+ input_surface_release_observers[i].owner = this;
+ input_surface_release_observers[i].slot_index = i;
+ }
+ }
+
+ amf_d3d11::~amf_d3d11() {
+ destroy_encoder();
+ }
+
+ // Stamped onto every AMF wrapper at creation; checked on release so a
+ // driver-delayed callback from an old wrapper cannot free the slot's next
+ // occupant while the VCN may still be reading it.
+ static const wchar_t *const kSlotGenerationProperty = L"VibepolloSlotGeneration";
+
+ void AMF_STD_CALL
+ amf_d3d11::input_surface_release_observer_t::OnSurfaceDataRelease(::amf::AMFSurface *surface) {
+ amf_int64 generation = 0;
+ if (surface) {
+ surface->GetProperty(kSlotGenerationProperty, &generation);
+ }
+ if (owner) {
+ owner->on_input_surface_released(slot_index, static_cast(generation));
+ }
+ }
+
+ void
+ amf_d3d11::on_input_surface_released(std::size_t slot_index, uint64_t generation) noexcept {
+ std::lock_guard lock(state_mutex);
+ if (slot_index >= input_surface_ring.size()) {
+ return;
+ }
+
+ auto &slot = input_surface_ring[slot_index];
+ if (generation != slot.generation) {
+ // Stale release from a wrapper of a previous reservation.
+ return;
+ }
+ if (lifecycle::on_surface_released(slot)) {
+ if (input_surfaces_in_flight > 0) {
+ --input_surfaces_in_flight;
+ }
+ state_cv.notify_all();
+ }
+ }
+
+ Microsoft::WRL::ComPtr
+ amf_d3d11::create_input_surface_texture() {
+ static const GUID AMFTextureArrayIndexGUID = { 0x28115527, 0xe7c3, 0x4b66, { 0x99, 0xd3, 0x4f, 0x2a, 0xe6, 0xb4, 0x7f, 0xaf } };
+ int array_index = 0;
+
+ Microsoft::WRL::ComPtr texture;
+ const auto hr = device->CreateTexture2D(
+ &input_surface_desc,
+ nullptr,
+ texture.ReleaseAndGetAddressOf());
+ if (FAILED(hr)) {
+ BOOST_LOG(error) << "AMF: failed to create direct-render input texture"
+ << ", HRESULT: 0x" << std::hex << hr;
+ return nullptr;
+ }
+ texture->SetPrivateData(AMFTextureArrayIndexGUID, sizeof(array_index), &array_index);
+ return texture;
+ }
+
+ bool
+ amf_d3d11::ensure_input_surface_count(std::size_t count) {
+ count = std::min(count, input_surface_ring.size());
+ for (std::size_t index = 0; index < count; ++index) {
+ auto &slot = input_surface_ring[index];
+ if (slot.texture) {
+ continue;
+ }
+ slot.texture = create_input_surface_texture();
+ if (!slot.texture) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ bool
+ amf_d3d11::init_amf_library() {
+ if (factory) return true;
+
+ amf_dll = LoadLibraryA(AMF_DLL_NAMEA);
+ if (!amf_dll) {
+ BOOST_LOG(error) << "AMF: failed to load " << AMF_DLL_NAMEA;
+ return false;
+ }
+
+ auto amf_query_version = reinterpret_cast(GetProcAddress(amf_dll, AMF_QUERY_VERSION_FUNCTION_NAME));
+ auto amf_init = reinterpret_cast(GetProcAddress(amf_dll, AMF_INIT_FUNCTION_NAME));
+
+ if (!amf_query_version || !amf_init) {
+ BOOST_LOG(error) << "AMF: missing entry points in " << AMF_DLL_NAMEA;
+ FreeLibrary(amf_dll);
+ amf_dll = nullptr;
+ return false;
+ }
+
+ amf_uint64 version = 0;
+ if (amf_query_version(&version) != AMF_OK) {
+ BOOST_LOG(error) << "AMF: failed to query runtime version";
+ FreeLibrary(amf_dll);
+ amf_dll = nullptr;
+ return false;
+ }
+
+ BOOST_LOG(info) << "AMF runtime version: "
+ << AMF_GET_MAJOR_VERSION(version) << "."
+ << AMF_GET_MINOR_VERSION(version) << "."
+ << AMF_GET_SUBMINOR_VERSION(version) << "."
+ << AMF_GET_BUILD_VERSION(version);
+
+ if (amf_init(AMF_FULL_VERSION, &factory) != AMF_OK || !factory) {
+ BOOST_LOG(error) << "AMF: AMFInit failed";
+ FreeLibrary(amf_dll);
+ amf_dll = nullptr;
+ return false;
+ }
+
+ return true;
+ }
+
+ AMF_SURFACE_FORMAT
+ amf_d3d11::get_amf_format(platf::pix_fmt_e buffer_format, int bit_depth) {
+ switch (buffer_format) {
+ case platf::pix_fmt_e::nv12:
+ return AMF_SURFACE_NV12;
+ case platf::pix_fmt_e::p010:
+ return AMF_SURFACE_P010;
+ default:
+ return (bit_depth == 10) ? AMF_SURFACE_P010 : AMF_SURFACE_NV12;
+ }
+ }
+
+ const wchar_t *
+ amf_d3d11::get_codec_id() {
+ switch (video_format) {
+ case 0:
+ return AMFVideoEncoderVCE_AVC;
+ case 1:
+ return AMFVideoEncoder_HEVC;
+ case 2:
+ return AMFVideoEncoder_AV1;
+ default:
+ return AMFVideoEncoderVCE_AVC;
+ }
+ }
+
+ bool
+ amf_d3d11::configure_encoder(const amf_config &config,
+ const video::config_t &client_config,
+ const video::sunshine_colorspace_t &colorspace) {
+ auto bitrate = static_cast(client_config.bitrate) * 1000;
+ AVRational fps {client_config.framerate > 0 ? client_config.framerate : 60, 1};
+ if (client_config.framerateX100 > 0) {
+ fps = video::framerateX100_to_rational(client_config.framerateX100);
+ }
+ auto framerate = AMFConstructRate(fps.num, fps.den);
+ // Cap the VBV/HRD buffer at ~1 frame of bits, matching FFmpeg's amfenc
+ // (rc_buffer_size = bitrate / framerate). Left at the AMF default (~1 second of
+ // bitrate) a single IDR or scene-change frame can balloon enormous - big enough
+ // to overflow the stream FEC block limit (stream.cpp MAX_FEC_BLOCKS = 4). Such
+ // frames are then sent WITHOUT FEC, and losing one makes the client request an
+ // IDR that is also oversized, cascading into multi-second freezes (observed on
+ // RDNA4 / RX 9070 XT at 200 Mbps + 165 fps: "Skipping FEC for abnormally large
+ // encoded frame"). A ~1-frame buffer keeps every frame comfortably FEC-sized.
+ const int64_t vbv_buffer_size = fps.num > 0 ? (bitrate * fps.den / fps.num) : bitrate;
+ // Match FFmpeg's default AMF path: set only target bitrate unless the user
+ // explicitly selects a rate-control mode. In that opt-in path, keep the
+ // legacy Sunshine peak/VBV constraints paired with the selected RC mode.
+ user_configured_rate_control = config.rc_mode.has_value();
+ enforce_hrd_enabled = config.enforce_hrd && *config.enforce_hrd;
+
+ ::amf::AMFCapsPtr encoder_caps;
+ const auto caps_result = encoder->GetCaps(&encoder_caps);
+ if (caps_result != AMF_OK || !encoder_caps) {
+ BOOST_LOG(warning) << "AMF: failed to query encoder capabilities, error: " << caps_result;
+ }
+
+ max_ltr_frames = 0;
+ rfi_enabled = false;
+ preanalysis_enabled = false;
+ preanalysis_lookahead_depth = 0;
+
+ const auto preanalysis_plan = lifecycle::resolve_preanalysis(config.rc_mode, config.preanalysis);
+ // PreAnalysis needs shader-readable inputs and lookahead surface headroom,
+ // not a deeper submission queue, so the user's low-latency queue choice is
+ // used as-is; the surface pool expands on its own if a runtime retains more.
+ const auto effective_input_queue_size = config.input_queue_size;
+ const bool adaptive_quantization_supported =
+ lifecycle::rate_control_supports_adaptive_quantization(config.rc_mode);
+ if (!adaptive_quantization_supported && config.vbaq && *config.vbaq) {
+ BOOST_LOG(info) << "AMF: disabling adaptive quantization because CQP rate control is selected";
+ }
+ const wchar_t *rate_control_property = video_format == 0 ? AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD :
+ video_format == 1 ? AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD :
+ AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD;
+ const wchar_t *preanalysis_property = video_format == 0 ? AMF_VIDEO_ENCODER_PRE_ANALYSIS_ENABLE :
+ video_format == 1 ? AMF_VIDEO_ENCODER_HEVC_PRE_ANALYSIS_ENABLE :
+ AMF_VIDEO_ENCODER_AV1_PRE_ANALYSIS_ENABLE;
+ const wchar_t *preanalysis_capability = video_format == 0 ? AMF_VIDEO_ENCODER_CAP_PRE_ANALYSIS :
+ video_format == 1 ? AMF_VIDEO_ENCODER_HEVC_CAP_PRE_ANALYSIS :
+ AMF_VIDEO_ENCODER_AV1_CAP_PRE_ANALYSIS;
+
+ if (preanalysis_plan.enabled && encoder_caps) {
+ amf_bool supported = false;
+ const auto capability_result = encoder_caps->GetProperty(preanalysis_capability, &supported);
+ if (capability_result == AMF_OK && !supported) {
+ BOOST_LOG(warning) << "AMF: PreAnalysis is required by the requested configuration but unsupported by this encoder";
+ return false;
+ }
+ }
+
+ auto set_verified_int64 = [&](const wchar_t *property, amf_int64 requested, const char *label) {
+ const auto set_result = encoder->SetProperty(property, requested);
+ amf_int64 applied = 0;
+ const auto get_result = encoder->GetProperty(property, &applied);
+ if (set_result != AMF_OK || get_result != AMF_OK || applied != requested) {
+ BOOST_LOG(warning) << "AMF: failed to apply " << label << " (requested=" << requested
+ << ", applied=" << applied << ", set=" << set_result
+ << ", get=" << get_result << ')';
+ return false;
+ }
+ return true;
+ };
+ auto set_verified_bool = [&](const wchar_t *property, bool requested, const char *label) {
+ const auto set_result = encoder->SetProperty(property, requested);
+ amf_bool applied = false;
+ const auto get_result = encoder->GetProperty(property, &applied);
+ if (set_result != AMF_OK || get_result != AMF_OK || static_cast(applied) != requested) {
+ BOOST_LOG(warning) << "AMF: failed to apply " << label << " (requested=" << requested
+ << ", applied=" << static_cast(applied) << ", set=" << set_result
+ << ", get=" << get_result << ')';
+ return false;
+ }
+ return true;
+ };
+
+ auto configure_reference_frames = [&](const wchar_t *property) {
+ const auto requested_limit = lifecycle::effective_reference_frame_limit(
+ client_config.numRefFrames,
+ config.intra_refresh_minimum_reference_frames);
+ if (!requested_limit) {
+ return true;
+ }
+
+ const auto requested = static_cast(*requested_limit);
+ if (client_config.numRefFrames <= 0) {
+ BOOST_LOG(info) << "AMF: applying the intra-refresh minimum of " << requested
+ << " reference frames (client sent no reference-frame limit)";
+ } else if (requested != client_config.numRefFrames) {
+ BOOST_LOG(info) << "AMF: raised the client reference-frame limit from "
+ << client_config.numRefFrames << " to " << requested
+ << " because H.264 intra refresh requires more than one reference frame";
+ }
+ const auto set_result = encoder->SetProperty(property, requested);
+ amf_int64 applied = 0;
+ const auto get_result = encoder->GetProperty(property, &applied);
+ if (set_result != AMF_OK || get_result != AMF_OK || applied != requested) {
+ BOOST_LOG(warning) << "AMF: failed to apply the client reference-frame limit (requested="
+ << requested << ", applied=" << applied << ", set=" << set_result
+ << ", get=" << get_result << ')';
+ return false;
+ }
+ return true;
+ };
+
+ auto configure_ltr = [&](const wchar_t *max_frames_property,
+ const wchar_t *mode_property,
+ amf_int64 reset_unused_mode,
+ const wchar_t *max_frames_capability) {
+ if (config.max_ltr_frames <= 0) {
+ return;
+ }
+
+ int supported_frames = std::min(config.max_ltr_frames, MAX_LTR_SLOTS);
+
+ // Property metadata is available for AVC/HEVC even though those codecs do
+ // not expose a named AMFCaps maximum. AV1 additionally exposes an explicit
+ // capability; clamp to whichever driver limit is available.
+ const ::amf::AMFPropertyInfo *property_info = nullptr;
+ if (encoder->GetPropertyInfo(max_frames_property, &property_info) == AMF_OK &&
+ property_info && property_info->maxValue.type == AMF_VARIANT_INT64 &&
+ property_info->maxValue.int64Value > 0) {
+ supported_frames = std::min(supported_frames, static_cast(property_info->maxValue.int64Value));
+ }
+ if (max_frames_capability && encoder_caps) {
+ amf_int64 capability_limit = 0;
+ if (encoder_caps->GetProperty(max_frames_capability, &capability_limit) == AMF_OK && capability_limit >= 0) {
+ supported_frames = std::min(supported_frames, static_cast(capability_limit));
+ }
+ }
+
+ if (supported_frames <= 0) {
+ BOOST_LOG(warning) << "AMF: LTR requested, but the driver reports no available LTR slots; disabling RFI";
+ return;
+ }
+
+ const auto max_result = encoder->SetProperty(max_frames_property, static_cast(supported_frames));
+ const auto mode_result = encoder->SetProperty(mode_property, reset_unused_mode);
+ amf_int64 applied_frames = 0;
+ const auto readback_result = encoder->GetProperty(max_frames_property, &applied_frames);
+ if (max_result != AMF_OK || mode_result != AMF_OK || readback_result != AMF_OK || applied_frames <= 0) {
+ BOOST_LOG(warning) << "AMF: failed to configure LTR reliably (max=" << max_result
+ << ", mode=" << mode_result << ", readback=" << readback_result
+ << "); disabling RFI";
+ // Best effort: avoid reserving driver LTR resources when Sunshine has
+ // disabled the feature locally because setup was not trustworthy.
+ encoder->SetProperty(max_frames_property, static_cast(0));
+ return;
+ }
+
+ max_ltr_frames = std::min({supported_frames, static_cast(applied_frames), MAX_LTR_SLOTS});
+ rfi_enabled = max_ltr_frames > 0;
+ if (max_ltr_frames != config.max_ltr_frames) {
+ BOOST_LOG(info) << "AMF: clamped requested LTR slots from " << config.max_ltr_frames
+ << " to driver-supported value " << max_ltr_frames;
+ }
+ };
+
+ auto configure_smart_access_video = [&](const wchar_t *sav_property,
+ const wchar_t *sav_support_cap) {
+ if (!config.smart_access_video) return true;
+
+ const bool enabled = *config.smart_access_video;
+ amf_bool sav_supported = false;
+ const bool sav_cap_known = sav_support_cap && encoder_caps && encoder_caps->GetProperty(sav_support_cap, &sav_supported) == AMF_OK;
+ const bool sav_supported_by_codec = sav_property && sav_cap_known && sav_supported;
+
+ if (enabled) {
+ if (!sav_supported_by_codec) {
+ BOOST_LOG(warning) << "AMF: Smart Access Video was requested, but AMFCaps does not report support"
+ << " (SAV=" << (sav_cap_known ? (sav_supported ? "yes" : "no") : "unknown") << ')';
+ return false;
+ }
+ return set_verified_bool(sav_property, true, "Smart Access Video");
+ }
+
+ // A feature absent from AMFCaps is already effectively disabled. When the
+ // property is supported, verify the user's explicit opt-out.
+ if (sav_supported_by_codec && !set_verified_bool(sav_property, false, "Smart Access Video")) {
+ return false;
+ }
+ return true;
+ };
+
+ // Split-frame encoding spreads one frame across multiple VCN engines. AMF
+ // exposes no direct capability bit for it, only the instance count, and H.264
+ // has no such control at all. The driver default is already enabled on parts
+ // that support it, so read the current value first and only touch the
+ // property when this GPU actually reports more than one engine — that way a
+ // single-VCN part is never asked for something meaningless.
+ if (encoder_caps && video_format != 0) {
+ const wchar_t *instances_cap = video_format == 1 ? AMF_VIDEO_ENCODER_HEVC_CAP_NUM_OF_HW_INSTANCES :
+ AMF_VIDEO_ENCODER_AV1_CAP_NUM_OF_HW_INSTANCES;
+ const wchar_t *multi_instance_property = video_format == 1 ? AMF_VIDEO_ENCODER_HEVC_MULTI_HW_INSTANCE_ENCODE :
+ AMF_VIDEO_ENCODER_AV1_MULTI_HW_INSTANCE_ENCODE;
+ amf_int64 hw_instances = 0;
+ if (encoder_caps->GetProperty(instances_cap, &hw_instances) == AMF_OK && hw_instances > 1) {
+ amf_bool current_multi_instance = false;
+ const bool have_current = encoder->GetProperty(multi_instance_property, ¤t_multi_instance) == AMF_OK;
+ if (have_current && current_multi_instance) {
+ BOOST_LOG(info) << "AMF: split-frame encoding already enabled by the driver across "
+ << hw_instances << " VCN instances";
+ } else if (set_verified_bool(multi_instance_property, true, "split-frame encoding")) {
+ BOOST_LOG(info) << "AMF: enabled split-frame encoding across " << hw_instances << " VCN instances";
+ } else {
+ // Not fatal: the encoder is perfectly usable on a single instance.
+ BOOST_LOG(info) << "AMF: split-frame encoding unavailable on this driver; continuing on one VCN instance";
+ }
+ } else {
+ BOOST_LOG(debug) << "AMF: split-frame encoding not applicable (hw_instances=" << hw_instances << ')';
+ }
+ }
+
+ if (video_format == 0) {
+ // H.264
+ if (config.usage && !set_verified_int64(AMF_VIDEO_ENCODER_USAGE, *config.usage, "H.264 usage preset")) return false;
+ if (config.quality_preset && !set_verified_int64(AMF_VIDEO_ENCODER_QUALITY_PRESET, *config.quality_preset, "H.264 quality preset")) return false;
+ // Sunshine's FFmpeg path requests High profile for H.264. Apply it after
+ // USAGE (which resets the preset parameter set) so native and legacy have
+ // the same compression tools and client-visible stream contract.
+ if (!set_verified_int64(AMF_VIDEO_ENCODER_PROFILE, AMF_VIDEO_ENCODER_PROFILE_HIGH, "H.264 profile")) return false;
+ if (!configure_reference_frames(AMF_VIDEO_ENCODER_MAX_NUM_REFRAMES)) return false;
+ encoder->SetProperty(AMF_VIDEO_ENCODER_TARGET_BITRATE, bitrate);
+ if (user_configured_rate_control) {
+ encoder->SetProperty(AMF_VIDEO_ENCODER_PEAK_BITRATE, bitrate);
+ encoder->SetProperty(AMF_VIDEO_ENCODER_VBV_BUFFER_SIZE, vbv_buffer_size);
+ }
+ encoder->SetProperty(AMF_VIDEO_ENCODER_FRAMERATE, framerate);
+ if (config.enforce_hrd) {
+ if (!set_verified_bool(AMF_VIDEO_ENCODER_ENFORCE_HRD, !!(*config.enforce_hrd), "H.264 HRD enforcement")) return false;
+ // Belt-and-braces with HRD: hard-cap the peak access-unit size so no single frame
+ // (IDR / scene change) can overrun the stream FEC budget at high bitrate. ~4x the
+ // per-frame VBV budget leaves normal IDRs intact while stopping the runaway frames
+ // that froze RDNA4 at 200+ Mbps. Only applied when HRD enforcement is opted in.
+ if (*config.enforce_hrd &&
+ !set_verified_int64(AMF_VIDEO_ENCODER_MAX_AU_SIZE, vbv_buffer_size * 4, "H.264 maximum access-unit size")) return false;
+ }
+ encoder->SetProperty(AMF_VIDEO_ENCODER_IDR_PERIOD, (amf_int64) 0);
+ encoder->SetProperty(AMF_VIDEO_ENCODER_DE_BLOCKING_FILTER, true);
+ if (config.h264_cabac) {
+ if (!set_verified_int64(
+ AMF_VIDEO_ENCODER_CABAC_ENABLE,
+ static_cast(*config.h264_cabac ? AMF_VIDEO_ENCODER_CABAC : AMF_VIDEO_ENCODER_CALV),
+ "H.264 entropy coder")) {
+ return false;
+ }
+ }
+ if ((config.vbaq || !adaptive_quantization_supported) &&
+ !set_verified_bool(
+ AMF_VIDEO_ENCODER_ENABLE_VBAQ,
+ adaptive_quantization_supported && config.vbaq && !!(*config.vbaq),
+ "H.264 VBAQ")) return false;
+ encoder->SetProperty(AMF_VIDEO_ENCODER_B_PIC_PATTERN, (amf_int64) 0);
+ // LOWLATENCY_MODE and INPUT_QUEUE_SIZE: only set when user opts in.
+ // Matches FFmpeg amfenc behavior (FFmpeg never forces these properties).
+ // Forcing them to true/1 has been observed to expose latent AMD driver
+ // bugs (see AlkaidLab/foundation-sunshine#666 freeze on RDNA4 26.5.x).
+ if (config.lowlatency_mode && !set_verified_bool(AMF_VIDEO_ENCODER_LOWLATENCY_MODE, *config.lowlatency_mode, "H.264 low-latency mode")) return false;
+ if (effective_input_queue_size && !set_verified_int64(AMF_VIDEO_ENCODER_INPUT_QUEUE_SIZE, *effective_input_queue_size, "H.264 input queue size")) return false;
+ if (!configure_smart_access_video(
+ AMF_VIDEO_ENCODER_ENABLE_SMART_ACCESS_VIDEO,
+ AMF_VIDEO_ENCODER_CAP_SUPPORT_SMART_ACCESS_VIDEO)) {
+ return false;
+ }
+ encoder->SetProperty(AMF_VIDEO_ENCODER_QUERY_TIMEOUT, (amf_int64) 1);
+
+ // LTR for RFI (Reference Frame Invalidation, weak-network recovery).
+ //
+ // Disabled by default (max_ltr_frames == 0) to match FFmpeg amfenc behavior:
+ // FFmpeg's libavcodec/amfenc.c never sets MAX_LTR_FRAMES / LTR_MODE, so static
+ // screen regions are not pinned to a baseline LTR frame and never accumulate
+ // color-block artifacts.
+ //
+ // Trade-off when the user opts in (amd_ltr_frames >= 1):
+ // + On lossy links, client-side reference invalidation can recover by
+ // sending a P-frame referencing a known-good LTR slot instead of a full
+ // IDR. IDRs are 10-20x larger than P-frames and themselves more likely
+ // to be lost on weak networks, which can cascade into an "IDR storm".
+ // - Static regions may inherit the IDR-time quantization noise of the
+ // baseline LTR slot until motion forces a fresh intra block.
+ //
+ // The slot rotation / IDR-baseline preservation logic below (PR #630) only
+ // takes effect when LTR is opted in.
+ configure_ltr(
+ AMF_VIDEO_ENCODER_MAX_LTR_FRAMES,
+ AMF_VIDEO_ENCODER_LTR_MODE,
+ AMF_VIDEO_ENCODER_LTR_MODE_RESET_UNUSED,
+ nullptr);
+
+ // High motion quality boost
+ if (config.high_motion_quality_boost_enable &&
+ !set_verified_bool(
+ AMF_VIDEO_ENCODER_HIGH_MOTION_QUALITY_BOOST_ENABLE,
+ *config.high_motion_quality_boost_enable,
+ "H.264 high-motion quality boost")) return false;
+
+ // Intra refresh
+ if (config.intra_refresh_mbs &&
+ !set_verified_int64(
+ AMF_VIDEO_ENCODER_INTRA_REFRESH_NUM_MBS_PER_SLOT,
+ *config.intra_refresh_mbs,
+ "H.264 intra-refresh macroblocks")) return false;
+
+ // Slices per frame
+ if (client_config.slicesPerFrame > 1 &&
+ !set_verified_int64(AMF_VIDEO_ENCODER_SLICES_PER_FRAME, client_config.slicesPerFrame, "H.264 slices per frame")) return false;
+
+ // Statistics feedback is a per-submission surface property. It is applied to
+ // sampled input surfaces in encode_frame(), never to the encoder component.
+ }
+ else if (video_format == 1) {
+ // HEVC
+ if (config.usage && !set_verified_int64(AMF_VIDEO_ENCODER_HEVC_USAGE, *config.usage, "HEVC usage preset")) return false;
+ if (config.quality_preset && !set_verified_int64(AMF_VIDEO_ENCODER_HEVC_QUALITY_PRESET, *config.quality_preset, "HEVC quality preset")) return false;
+ if (!configure_reference_frames(AMF_VIDEO_ENCODER_HEVC_MAX_NUM_REFRAMES)) return false;
+ encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_TARGET_BITRATE, bitrate);
+ if (user_configured_rate_control) {
+ encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_PEAK_BITRATE, bitrate);
+ encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_VBV_BUFFER_SIZE, vbv_buffer_size);
+ }
+ encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_FRAMERATE, framerate);
+ if (config.enforce_hrd) {
+ if (!set_verified_bool(AMF_VIDEO_ENCODER_HEVC_ENFORCE_HRD, !!(*config.enforce_hrd), "HEVC HRD enforcement")) return false;
+ // See H.264 above: cap the peak AU size (~4x per-frame VBV) so no frame overruns FEC.
+ if (*config.enforce_hrd &&
+ !set_verified_int64(AMF_VIDEO_ENCODER_HEVC_MAX_AU_SIZE, vbv_buffer_size * 4, "HEVC maximum access-unit size")) return false;
+ }
+ // HEADER_INSERTION_MODE is deliberately left at the driver default (NONE),
+ // matching FFmpeg's hevc_amf (stable on the same cards). Forcing IDR_ALIGNED
+ // was the last unconditional HEVC-only divergence from the paths that work on
+ // RDNA4, where native HEVC kept freezing. Headers still reach the client:
+ // every force_idr frame sets HEVC_INSERT_HEADER below, so VPS/SPS/PPS ride on
+ // each IDR exactly like the FFmpeg path.
+ // Infinite GOP (no periodic IDR), matching both FFmpeg's hevc_amf path (which
+ // uses an infinite GOP - see video.cpp "infinite GOP length") and the native AV1
+ // path. Keyframes are client-driven via force_idr (initial frame + packet-loss
+ // recovery). A periodic IDR (GOP 60) emitted a large keyframe every 60 frames;
+ // at high bitrate those overflow the stream FEC block limit (MAX_FEC_BLOCKS = 4,
+ // stream.cpp) and ship unprotected, so a single loss cascades into a freeze. A
+ // tester's host log proved it directly: in one session, GOP-60 HEVC produced 319
+ // over-FEC frames while GOP-0 AV1 produced 0 on the same RX 9070 XT. (An earlier
+ // note that GOP 0 misbehaves on RDNA4 is contradicted by AV1 and FFmpeg both
+ // running infinite-GOP fine on that card.)
+ //
+ // GOP_SIZE=0 alone gives the infinite GOP. We deliberately do NOT set
+ // NUM_GOPS_PER_IDR: the native AV1 path (rock-solid on RDNA4 up to 500 Mbps) sets
+ // only GOP_SIZE, and NUM_GOPS_PER_IDR=0 is a degenerate value ("zero GOPs per
+ // IDR") that RDNA4's HEVC VCN mishandles - multiple RX 9070 XT testers froze on
+ // native HEVC (freeze on the first keyframe need) while native AV1 and FFmpeg
+ // hevc_amf, neither of which sets it, ran clean on the same cards. force_idr
+ // keyframes are driven per-surface via HEVC_FORCE_PICTURE_TYPE, independent of this.
+ encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_GOP_SIZE, (amf_int64) 0);
+ if ((config.vbaq || !adaptive_quantization_supported) &&
+ !set_verified_bool(
+ AMF_VIDEO_ENCODER_HEVC_ENABLE_VBAQ,
+ adaptive_quantization_supported && config.vbaq && !!(*config.vbaq),
+ "HEVC VBAQ")) return false;
+ // LOWLATENCY_MODE and INPUT_QUEUE_SIZE: only set when user opts in.
+ // See H.264 block above for rationale (FFmpeg-aligned default behavior).
+ if (config.lowlatency_mode && !set_verified_bool(AMF_VIDEO_ENCODER_HEVC_LOWLATENCY_MODE, *config.lowlatency_mode, "HEVC low-latency mode")) return false;
+ if (effective_input_queue_size && !set_verified_int64(AMF_VIDEO_ENCODER_HEVC_INPUT_QUEUE_SIZE, *effective_input_queue_size, "HEVC input queue size")) return false;
+ if (!configure_smart_access_video(
+ AMF_VIDEO_ENCODER_HEVC_ENABLE_SMART_ACCESS_VIDEO,
+ AMF_VIDEO_ENCODER_HEVC_CAP_SUPPORT_SMART_ACCESS_VIDEO)) {
+ return false;
+ }
+ encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_QUERY_TIMEOUT, (amf_int64) 1);
+
+ if (colorspace.bit_depth == 10) {
+ encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_PROFILE, (amf_int64) AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN_10);
+ }
+ else {
+ encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_PROFILE, (amf_int64) AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN);
+ }
+
+ // LTR for RFI - see H.264 block above for detailed trade-off rationale.
+ // Disabled by default; opt-in via amd_ltr_frames config.
+ configure_ltr(
+ AMF_VIDEO_ENCODER_HEVC_MAX_LTR_FRAMES,
+ AMF_VIDEO_ENCODER_HEVC_LTR_MODE,
+ AMF_VIDEO_ENCODER_HEVC_LTR_MODE_RESET_UNUSED,
+ nullptr);
+
+ // High motion quality boost
+ if (config.high_motion_quality_boost_enable &&
+ !set_verified_bool(
+ AMF_VIDEO_ENCODER_HEVC_HIGH_MOTION_QUALITY_BOOST_ENABLE,
+ *config.high_motion_quality_boost_enable,
+ "HEVC high-motion quality boost")) return false;
+
+ // Intra refresh
+ if (config.intra_refresh_mbs &&
+ !set_verified_int64(
+ AMF_VIDEO_ENCODER_HEVC_INTRA_REFRESH_NUM_CTBS_PER_SLOT,
+ *config.intra_refresh_mbs,
+ "HEVC intra-refresh CTBs")) return false;
+
+ // Slices per frame
+ if (client_config.slicesPerFrame > 1 &&
+ !set_verified_int64(AMF_VIDEO_ENCODER_HEVC_SLICES_PER_FRAME, client_config.slicesPerFrame, "HEVC slices per frame")) return false;
+
+ // Statistics feedback is applied per input surface in encode_frame().
+ }
+ else {
+ // AV1
+ if (config.usage && !set_verified_int64(AMF_VIDEO_ENCODER_AV1_USAGE, *config.usage, "AV1 usage preset")) return false;
+ if (config.quality_preset && !set_verified_int64(AMF_VIDEO_ENCODER_AV1_QUALITY_PRESET, *config.quality_preset, "AV1 quality preset")) return false;
+ if (!configure_reference_frames(AMF_VIDEO_ENCODER_AV1_MAX_NUM_REFRAMES)) return false;
+ encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_TARGET_BITRATE, bitrate);
+ if (user_configured_rate_control) {
+ encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_PEAK_BITRATE, bitrate);
+ encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_VBV_BUFFER_SIZE, vbv_buffer_size);
+ }
+ encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_FRAMERATE, framerate);
+ if (config.enforce_hrd) {
+ if (!set_verified_bool(AMF_VIDEO_ENCODER_AV1_ENFORCE_HRD, !!(*config.enforce_hrd), "AV1 HRD enforcement")) return false;
+ // See H.264 above: cap the peak compressed frame size (~4x per-frame VBV) to fit FEC.
+ if (*config.enforce_hrd &&
+ !set_verified_int64(
+ AMF_VIDEO_ENCODER_AV1_MAX_COMPRESSED_FRAME_SIZE,
+ vbv_buffer_size * 4,
+ "AV1 maximum compressed-frame size")) return false;
+ }
+ encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_ALIGNMENT_MODE, (amf_int64) AMF_VIDEO_ENCODER_AV1_ALIGNMENT_MODE_NO_RESTRICTIONS);
+ encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_GOP_SIZE, (amf_int64) 0);
+ // INPUT_QUEUE_SIZE / ENCODING_LATENCY_MODE: only set when user opts in.
+ // Matches FFmpeg amfenc behavior (never auto-forces LOWEST_LATENCY).
+ // See AlkaidLab/foundation-sunshine#666 for the RDNA4 freeze that
+ // motivated stopping aggressive defaults.
+ if (effective_input_queue_size && !set_verified_int64(AMF_VIDEO_ENCODER_AV1_INPUT_QUEUE_SIZE, *effective_input_queue_size, "AV1 input queue size")) return false;
+ if (!configure_smart_access_video(
+ AMF_VIDEO_ENCODER_AV1_ENABLE_SMART_ACCESS_VIDEO,
+ AMF_VIDEO_ENCODER_AV1_CAP_SUPPORT_SMART_ACCESS_VIDEO)) {
+ return false;
+ }
+ encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_QUERY_TIMEOUT, (amf_int64) 1);
+ if (config.av1_encoding_latency_mode) {
+ if (!set_verified_int64(
+ AMF_VIDEO_ENCODER_AV1_ENCODING_LATENCY_MODE,
+ *config.av1_encoding_latency_mode,
+ "AV1 encoding latency mode")) return false;
+ }
+
+ // AV1 Screen Content Tools
+ if (config.av1_screen_content_tools &&
+ !set_verified_bool(
+ AMF_VIDEO_ENCODER_AV1_SCREEN_CONTENT_TOOLS,
+ *config.av1_screen_content_tools,
+ "AV1 screen-content tools")) return false;
+ if (config.av1_palette_mode &&
+ !set_verified_bool(AMF_VIDEO_ENCODER_AV1_PALETTE_MODE, *config.av1_palette_mode, "AV1 palette mode")) return false;
+ if (config.av1_force_integer_mv &&
+ !set_verified_bool(AMF_VIDEO_ENCODER_AV1_FORCE_INTEGER_MV, *config.av1_force_integer_mv, "AV1 integer motion vectors")) return false;
+
+ // AV1 high motion quality boost
+ if (config.high_motion_quality_boost_enable &&
+ !set_verified_bool(
+ AMF_VIDEO_ENCODER_AV1_HIGH_MOTION_QUALITY_BOOST,
+ *config.high_motion_quality_boost_enable,
+ "AV1 high-motion quality boost")) return false;
+
+ // The codec-unqualified amd_vbaq setting maps to AV1 content-adaptive
+ // quantization. PAQ remains a fallback for callers that configure the
+ // lower-level AMF API directly.
+ if (!adaptive_quantization_supported) {
+ if (!set_verified_int64(
+ AMF_VIDEO_ENCODER_AV1_AQ_MODE,
+ AMF_VIDEO_ENCODER_AV1_AQ_MODE_NONE,
+ "AV1 adaptive quantization")) return false;
+ } else if (config.vbaq) {
+ if (!set_verified_int64(
+ AMF_VIDEO_ENCODER_AV1_AQ_MODE,
+ static_cast(*config.vbaq ? AMF_VIDEO_ENCODER_AV1_AQ_MODE_CAQ : AMF_VIDEO_ENCODER_AV1_AQ_MODE_NONE),
+ "AV1 adaptive quantization")) return false;
+ } else if (config.pa_paq_mode) {
+ if (!set_verified_int64(AMF_VIDEO_ENCODER_AV1_AQ_MODE, *config.pa_paq_mode, "AV1 PAQ mode")) return false;
+ }
+
+ // LTR for RFI - see H.264 block above for detailed trade-off rationale.
+ // Disabled by default; opt-in via amd_ltr_frames config.
+ configure_ltr(
+ AMF_VIDEO_ENCODER_AV1_MAX_LTR_FRAMES,
+ AMF_VIDEO_ENCODER_AV1_LTR_MODE,
+ AMF_VIDEO_ENCODER_AV1_LTR_MODE_RESET_UNUSED,
+ AMF_VIDEO_ENCODER_AV1_CAP_MAX_NUM_LTR_FRAMES);
+
+ // Intra refresh
+ if (config.av1_intra_refresh_mode) {
+ if (!set_verified_int64(
+ AMF_VIDEO_ENCODER_AV1_INTRA_REFRESH_MODE,
+ *config.av1_intra_refresh_mode,
+ "AV1 intra-refresh mode")) return false;
+ if (config.av1_intra_refresh_stripes &&
+ !set_verified_int64(
+ AMF_VIDEO_ENCODER_AV1_INTRAREFRESH_STRIPES,
+ *config.av1_intra_refresh_stripes,
+ "AV1 intra-refresh stripes")) return false;
+ }
+
+ // Tiles per frame
+ if (client_config.slicesPerFrame > 1 &&
+ !set_verified_int64(AMF_VIDEO_ENCODER_AV1_TILES_PER_FRAME, client_config.slicesPerFrame, "AV1 tiles per frame")) return false;
+
+ // Statistics feedback is applied per input surface in encode_frame().
+ }
+
+ // AMD's TranscodePipeline sample sets rate control before enabling PA because
+ // PA property application can otherwise fail. Keep that dependency ordering
+ // and verify the complete pair. A driver that rejects either property fails
+ // this native session so the upper layer can choose legacy AMF without
+ // silently changing modes.
+ const int requested_depth = preanalysis_plan.enabled ?
+ std::max(1, config.pa_lookahead_depth.value_or(preanalysis_plan.lookahead_depth)) :
+ 0;
+ if (!lifecycle::apply_rate_control_and_preanalysis(
+ config.rc_mode,
+ config.preanalysis.has_value() || preanalysis_plan.enabled,
+ preanalysis_plan,
+ requested_depth,
+ [&](int value) {
+ return set_verified_int64(rate_control_property, value, "rate-control mode");
+ },
+ [&](bool value) {
+ return set_verified_bool(preanalysis_property, value, "PreAnalysis");
+ },
+ [&](int value) {
+ return set_verified_int64(AMF_PA_LOOKAHEAD_BUFFER_DEPTH, value, "PreAnalysis lookahead depth");
+ })) {
+ return false;
+ }
+ if (preanalysis_plan.enabled) {
+ preanalysis_enabled = true;
+ preanalysis_lookahead_depth = requested_depth;
+ }
+
+ // Sunshine's legacy AMF path explicitly disables rate-control frame skipping.
+ // Keep native packet/PTS semantics identical and do not let ULL usage presets
+ // silently discard a frame when the bitrate controller is under pressure.
+ const wchar_t *skip_frame_property = video_format == 0 ? AMF_VIDEO_ENCODER_RATE_CONTROL_SKIP_FRAME_ENABLE :
+ video_format == 1 ? AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_SKIP_FRAME_ENABLE :
+ AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_SKIP_FRAME;
+ if (!set_verified_bool(skip_frame_property, false, "rate-control frame skipping")) {
+ return false;
+ }
+
+ if (config.qvbr_quality_level && config.rc_mode && *config.rc_mode == 4) {
+ const wchar_t *qvbr_quality_property = video_format == 0 ? AMF_VIDEO_ENCODER_QVBR_QUALITY_LEVEL :
+ video_format == 1 ? AMF_VIDEO_ENCODER_HEVC_QVBR_QUALITY_LEVEL :
+ AMF_VIDEO_ENCODER_AV1_QVBR_QUALITY_LEVEL;
+ if (!set_verified_int64(
+ qvbr_quality_property,
+ static_cast(*config.qvbr_quality_level),
+ "QVBR quality level")) {
+ return false;
+ }
+ }
+
+ // Color space properties
+ if (video_format == 0) {
+ encoder->SetProperty(AMF_VIDEO_ENCODER_FULL_RANGE_COLOR, colorspace.full_range);
+ }
+ else if (video_format == 1) {
+ encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_NOMINAL_RANGE, (amf_int64)(colorspace.full_range ? AMF_VIDEO_ENCODER_HEVC_NOMINAL_RANGE_FULL : AMF_VIDEO_ENCODER_HEVC_NOMINAL_RANGE_STUDIO));
+ }
+ else {
+ // AV1: amf_bool type
+ encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_OUTPUT_FULL_RANGE_COLOR, colorspace.full_range);
+ }
+
+ // Color properties for bitstream metadata.
+ // Only set OUTPUT properties, matching FFmpeg's approach.
+ // Do NOT set INPUT_COLOR_xxx — setting them may trigger AMF's internal color converter.
+ amf_int64 amf_primaries;
+ amf_int64 amf_transfer;
+ amf_int64 amf_color_profile;
+
+ switch (colorspace.colorspace) {
+ case video::colorspace_e::rec601:
+ amf_primaries = AMF_COLOR_PRIMARIES_SMPTE170M;
+ amf_transfer = AMF_COLOR_TRANSFER_CHARACTERISTIC_SMPTE170M;
+ amf_color_profile = colorspace.full_range ? AMF_VIDEO_CONVERTER_COLOR_PROFILE_FULL_601 : AMF_VIDEO_CONVERTER_COLOR_PROFILE_601;
+ break;
+ case video::colorspace_e::rec709:
+ amf_primaries = AMF_COLOR_PRIMARIES_BT709;
+ amf_transfer = AMF_COLOR_TRANSFER_CHARACTERISTIC_BT709;
+ amf_color_profile = colorspace.full_range ? AMF_VIDEO_CONVERTER_COLOR_PROFILE_FULL_709 : AMF_VIDEO_CONVERTER_COLOR_PROFILE_709;
+ break;
+ case video::colorspace_e::bt2020sdr:
+ amf_primaries = AMF_COLOR_PRIMARIES_BT2020;
+ amf_transfer = AMF_COLOR_TRANSFER_CHARACTERISTIC_BT2020_10;
+ amf_color_profile = colorspace.full_range ? AMF_VIDEO_CONVERTER_COLOR_PROFILE_FULL_2020 : AMF_VIDEO_CONVERTER_COLOR_PROFILE_2020;
+ break;
+ case video::colorspace_e::bt2020:
+ amf_primaries = AMF_COLOR_PRIMARIES_BT2020;
+ amf_transfer = AMF_COLOR_TRANSFER_CHARACTERISTIC_SMPTE2084;
+ amf_color_profile = colorspace.full_range ? AMF_VIDEO_CONVERTER_COLOR_PROFILE_FULL_2020 : AMF_VIDEO_CONVERTER_COLOR_PROFILE_2020;
+ break;
+ default:
+ amf_primaries = AMF_COLOR_PRIMARIES_BT709;
+ amf_transfer = AMF_COLOR_TRANSFER_CHARACTERISTIC_BT709;
+ amf_color_profile = colorspace.full_range ? AMF_VIDEO_CONVERTER_COLOR_PROFILE_FULL_709 : AMF_VIDEO_CONVERTER_COLOR_PROFILE_709;
+ break;
+ }
+
+ auto amf_bit_depth = (amf_int64)((colorspace.bit_depth == 10) ? AMF_COLOR_BIT_DEPTH_10 : AMF_COLOR_BIT_DEPTH_8);
+
+ if (video_format == 0) {
+ encoder->SetProperty(AMF_VIDEO_ENCODER_COLOR_BIT_DEPTH, amf_bit_depth);
+ encoder->SetProperty(AMF_VIDEO_ENCODER_OUTPUT_COLOR_PROFILE, amf_color_profile);
+ encoder->SetProperty(AMF_VIDEO_ENCODER_OUTPUT_TRANSFER_CHARACTERISTIC, amf_transfer);
+ encoder->SetProperty(AMF_VIDEO_ENCODER_OUTPUT_COLOR_PRIMARIES, amf_primaries);
+ }
+ else if (video_format == 1) {
+ encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_COLOR_BIT_DEPTH, amf_bit_depth);
+ encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_OUTPUT_COLOR_PROFILE, amf_color_profile);
+ encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_OUTPUT_TRANSFER_CHARACTERISTIC, amf_transfer);
+ encoder->SetProperty(AMF_VIDEO_ENCODER_HEVC_OUTPUT_COLOR_PRIMARIES, amf_primaries);
+ }
+ else {
+ encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_COLOR_BIT_DEPTH, amf_bit_depth);
+ encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_OUTPUT_COLOR_PROFILE, amf_color_profile);
+ encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_OUTPUT_TRANSFER_CHARACTERISTIC, amf_transfer);
+ encoder->SetProperty(AMF_VIDEO_ENCODER_AV1_OUTPUT_COLOR_PRIMARIES, amf_primaries);
+ }
+
+ // Save statistics feedback state for encode_frame()
+ statistics_enabled = config.enable_statistics_feedback;
+ psnr_enabled = config.enable_psnr_feedback;
+ ssim_enabled = config.enable_ssim_feedback;
+
+ // Pre-Analysis sub-system properties (set on encoder when PA is enabled)
+ if (preanalysis_plan.enabled) {
+ if (config.pa_paq_mode && !set_verified_int64(AMF_PA_PAQ_MODE, *config.pa_paq_mode, "PAQ mode")) return false;
+ if (config.pa_taq_mode && !set_verified_int64(AMF_PA_TAQ_MODE, *config.pa_taq_mode, "TAQ mode")) return false;
+ if (config.pa_caq_strength && !set_verified_int64(AMF_PA_CAQ_STRENGTH, *config.pa_caq_strength, "CAQ strength")) return false;
+ if (config.pa_scene_change_sensitivity &&
+ !set_verified_int64(
+ AMF_PA_SCENE_CHANGE_DETECTION_SENSITIVITY,
+ *config.pa_scene_change_sensitivity,
+ "PA scene-change sensitivity")) return false;
+ if (config.pa_high_motion_quality_boost &&
+ !set_verified_int64(
+ AMF_PA_HIGH_MOTION_QUALITY_BOOST_MODE,
+ *config.pa_high_motion_quality_boost,
+ "PA high-motion quality boost")) return false;
+ if (config.pa_initial_qp_after_scene_change &&
+ !set_verified_int64(
+ AMF_PA_INITIAL_QP_AFTER_SCENE_CHANGE,
+ *config.pa_initial_qp_after_scene_change,
+ "PA initial scene-change QP")) return false;
+ if (config.pa_activity_type && !set_verified_int64(AMF_PA_ACTIVITY_TYPE, *config.pa_activity_type, "PA activity type")) return false;
+ }
+
+ // NOTE: LOWLATENCY_MODE is intentionally NOT forced here.
+ //
+ // Previously this block hard-coded AMF_VIDEO_ENCODER_(HEVC_)LOWLATENCY_MODE = true
+ // for both H264 and HEVC (AV1 was never forced). This:
+ // 1) Silently overrode the per-codec `config.lowlatency_mode` opt-in
+ // we set earlier in configure_*_encoder().
+ // 2) Diverged from FFmpeg amfenc behavior, which only writes this
+ // property when the user passes `-latency 1` (default -1 = leave
+ // property unset, driver default = false).
+ // 3) Triggered a firmware freeze on AMD RDNA4 (RX 9070/9070 XT) with
+ // Adrenalin 26.5.x on HEVC: video stalls while audio keeps flowing,
+ // toggling HDR (which forces encoder reinit) temporarily recovers.
+ // AV1 was unaffected precisely because no AV1 branch existed here.
+ //
+ // LOWLATENCY_MODE is now controlled solely by `config.lowlatency_mode`
+ // (WebUI: amd_lowlatency_mode). Combined with USAGE = ULTRA_LOW_LATENCY,
+ // the encoder pipeline still achieves low latency without the firmware
+ // bug path. Users who want the aggressive mode can opt in explicitly.
+
+ return true;
+ }
+
+ bool
+ amf_d3d11::create_encoder(const amf_config &config,
+ const video::config_t &client_config,
+ const video::sunshine_colorspace_t &colorspace,
+ platf::pix_fmt_e buffer_format) {
+ // Determine video format from client config
+ video_format = client_config.videoFormat;
+ current_config = client_config;
+
+ // Initialize AMF library
+ if (!init_amf_library()) return false;
+
+ // Create AMF context
+ auto res = factory->CreateContext(&context);
+ if (res != AMF_OK || !context) {
+ BOOST_LOG(error) << "AMF: CreateContext failed, error: " << res;
+ return false;
+ }
+
+ // Set surface cache size to match FFmpeg's hwcontext_amf initialization
+ context->SetProperty(L"DeviceSurfaceCacheSize", (amf_int64) 50);
+
+ // Initialize D3D11 in AMF context with DX11_1 (matching FFmpeg)
+ res = context->InitDX11(device, AMF_DX11_1);
+ if (res != AMF_OK) {
+ BOOST_LOG(error) << "AMF: InitDX11 failed, error: " << res;
+ return false;
+ }
+
+ // Create encoder component
+ res = factory->CreateComponent(context, get_codec_id(), &encoder);
+ if (res != AMF_OK || !encoder) {
+ BOOST_LOG(error) << "AMF: CreateComponent failed for codec " << video_format << ", error: " << res;
+ return false;
+ }
+
+ // Configure encoder properties (before Init)
+ if (!configure_encoder(config, client_config, colorspace)) {
+ return false;
+ }
+
+ // Initialize encoder
+ auto amf_format = get_amf_format(buffer_format, colorspace.bit_depth);
+ surface_format = amf_format;
+ encode_width = client_config.width;
+ encode_height = client_config.height;
+ res = encoder->Init(amf_format, client_config.width, client_config.height);
+
+ if (res != AMF_OK) {
+ BOOST_LOG(error) << "AMF: encoder Init failed with the requested encode settings, error: " << res;
+ return false;
+ }
+
+ // Some runtimes accept a property before Init but substitute a different
+ // value while constructing the hardware pipeline. Verify the semantic pair
+ // again after Init so probing cannot advertise a mode the driver did not keep.
+ const auto applied_preanalysis_plan = lifecycle::resolve_preanalysis(config.rc_mode, config.preanalysis);
+ if (config.preanalysis || applied_preanalysis_plan.enabled) {
+ const wchar_t *preanalysis_property = video_format == 0 ? AMF_VIDEO_ENCODER_PRE_ANALYSIS_ENABLE :
+ video_format == 1 ? AMF_VIDEO_ENCODER_HEVC_PRE_ANALYSIS_ENABLE :
+ AMF_VIDEO_ENCODER_AV1_PRE_ANALYSIS_ENABLE;
+ amf_bool applied_preanalysis = false;
+ const auto preanalysis_result = encoder->GetProperty(preanalysis_property, &applied_preanalysis);
+ if (preanalysis_result != AMF_OK || static_cast(applied_preanalysis) != applied_preanalysis_plan.enabled) {
+ BOOST_LOG(error) << "AMF: driver changed the requested PreAnalysis state after Init"
+ << " (requested=" << applied_preanalysis_plan.enabled
+ << ", applied=" << static_cast(applied_preanalysis)
+ << ", result=" << preanalysis_result << ')';
+ return false;
+ }
+ if (applied_preanalysis_plan.enabled) {
+ // PAEngineType is optional and defaults to automatic selection. Older
+ // AMF runtimes do not expose it through the integrated encoder and
+ // return AMF_INVALID_ARG when it is set or queried. Do not turn an
+ // implementation-specific optimization hint into a codec failure.
+ amf_int64 applied_engine_type = AMF_MEMORY_UNKNOWN;
+ const auto engine_result = encoder->GetProperty(AMF_PA_ENGINE_TYPE, &applied_engine_type);
+ if (engine_result == AMF_OK) {
+ BOOST_LOG(debug) << "AMF: driver-selected PreAnalysis engine=" << applied_engine_type;
+ } else {
+ BOOST_LOG(debug) << "AMF: runtime does not expose the optional PreAnalysis engine"
+ << " (result=" << engine_result << ')';
+ }
+ amf_int64 applied_depth = 0;
+ const auto depth_result = encoder->GetProperty(AMF_PA_LOOKAHEAD_BUFFER_DEPTH, &applied_depth);
+ if (depth_result != AMF_OK || applied_depth != preanalysis_lookahead_depth) {
+ BOOST_LOG(error) << "AMF: driver changed the requested PreAnalysis depth after Init"
+ << " (requested=" << preanalysis_lookahead_depth << ", applied=" << applied_depth
+ << ", result=" << depth_result << ')';
+ return false;
+ }
+ }
+ }
+
+ if (config.rc_mode) {
+ const wchar_t *rate_control_property = video_format == 0 ? AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD :
+ video_format == 1 ? AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD :
+ AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD;
+ amf_int64 applied_rate_control = -1;
+ const auto rate_control_result = encoder->GetProperty(rate_control_property, &applied_rate_control);
+ if (rate_control_result != AMF_OK || applied_rate_control != *config.rc_mode) {
+ BOOST_LOG(error) << "AMF: driver changed the requested rate-control mode after Init"
+ << " (requested=" << *config.rc_mode << ", applied=" << applied_rate_control
+ << ", result=" << rate_control_result << ')';
+ return false;
+ }
+ }
+
+ // Verify against the same effective limit the configure path applied — the
+ // intra-refresh raise is ours, not a driver change (see effective_reference_frame_limit).
+ if (const auto effective_reference_frames = lifecycle::effective_reference_frame_limit(
+ client_config.numRefFrames,
+ config.intra_refresh_minimum_reference_frames)) {
+ const wchar_t *reference_frames_property = video_format == 0 ? AMF_VIDEO_ENCODER_MAX_NUM_REFRAMES :
+ video_format == 1 ? AMF_VIDEO_ENCODER_HEVC_MAX_NUM_REFRAMES :
+ AMF_VIDEO_ENCODER_AV1_MAX_NUM_REFRAMES;
+ const auto requested_reference_frames = static_cast(*effective_reference_frames);
+ amf_int64 applied_reference_frames = 0;
+ const auto reference_frames_result = encoder->GetProperty(reference_frames_property, &applied_reference_frames);
+ if (reference_frames_result != AMF_OK || applied_reference_frames != requested_reference_frames) {
+ BOOST_LOG(error) << "AMF: driver changed the effective reference-frame limit after Init"
+ << " (requested=" << requested_reference_frames
+ << ", applied=" << applied_reference_frames
+ << ", result=" << reference_frames_result << ')';
+ return false;
+ }
+ }
+
+ const bool adaptive_quantization_supported_after_init =
+ lifecycle::rate_control_supports_adaptive_quantization(config.rc_mode);
+ if (!adaptive_quantization_supported_after_init) {
+ if (video_format == 2) {
+ amf_int64 applied_aq_mode = AMF_VIDEO_ENCODER_AV1_AQ_MODE_CAQ;
+ const auto aq_result = encoder->GetProperty(AMF_VIDEO_ENCODER_AV1_AQ_MODE, &applied_aq_mode);
+ if (aq_result != AMF_OK || applied_aq_mode != AMF_VIDEO_ENCODER_AV1_AQ_MODE_NONE) {
+ BOOST_LOG(error) << "AMF: driver enabled AV1 adaptive quantization with CQP after Init"
+ << " (applied=" << applied_aq_mode << ", result=" << aq_result << ')';
+ return false;
+ }
+ } else {
+ const wchar_t *vbaq_property = video_format == 0 ? AMF_VIDEO_ENCODER_ENABLE_VBAQ :
+ AMF_VIDEO_ENCODER_HEVC_ENABLE_VBAQ;
+ amf_bool applied_vbaq = true;
+ const auto vbaq_result = encoder->GetProperty(vbaq_property, &applied_vbaq);
+ if (vbaq_result != AMF_OK || static_cast(applied_vbaq)) {
+ BOOST_LOG(error) << "AMF: driver enabled VBAQ with CQP after Init"
+ << " (applied=" << static_cast(applied_vbaq)
+ << ", result=" << vbaq_result << ')';
+ return false;
+ }
+ }
+ }
+
+ {
+ const wchar_t *skip_frame_property = video_format == 0 ? AMF_VIDEO_ENCODER_RATE_CONTROL_SKIP_FRAME_ENABLE :
+ video_format == 1 ? AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_SKIP_FRAME_ENABLE :
+ AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_SKIP_FRAME;
+ amf_bool applied_skip_frame = true;
+ const auto skip_frame_result = encoder->GetProperty(skip_frame_property, &applied_skip_frame);
+ if (skip_frame_result != AMF_OK || static_cast(applied_skip_frame)) {
+ BOOST_LOG(error) << "AMF: driver changed the requested rate-control frame-skipping state after Init"
+ << " (requested=false, applied=" << static_cast(applied_skip_frame)
+ << ", result=" << skip_frame_result << ')';
+ return false;
+ }
+ }
+
+ if (config.qvbr_quality_level && config.rc_mode && *config.rc_mode == 4) {
+ const wchar_t *qvbr_quality_property = video_format == 0 ? AMF_VIDEO_ENCODER_QVBR_QUALITY_LEVEL :
+ video_format == 1 ? AMF_VIDEO_ENCODER_HEVC_QVBR_QUALITY_LEVEL :
+ AMF_VIDEO_ENCODER_AV1_QVBR_QUALITY_LEVEL;
+ amf_int64 applied_qvbr_quality = 0;
+ const auto qvbr_result = encoder->GetProperty(qvbr_quality_property, &applied_qvbr_quality);
+ if (qvbr_result != AMF_OK || applied_qvbr_quality != *config.qvbr_quality_level) {
+ BOOST_LOG(error) << "AMF: driver changed the requested QVBR quality after Init"
+ << " (requested=" << *config.qvbr_quality_level << ", applied=" << applied_qvbr_quality
+ << ", result=" << qvbr_result << ')';
+ return false;
+ }
+ }
+
+ // INPUT_QUEUE_SIZE is a static encoder property. Read back the value after
+ // Init because it determines how many external textures AMF may own at once.
+ // An explicit setting must survive Init; for the driver default, use the
+ // applied value to size the lazy direct-render pool accurately.
+ {
+ const wchar_t *input_queue_property = video_format == 0 ? AMF_VIDEO_ENCODER_INPUT_QUEUE_SIZE :
+ video_format == 1 ? AMF_VIDEO_ENCODER_HEVC_INPUT_QUEUE_SIZE :
+ AMF_VIDEO_ENCODER_AV1_INPUT_QUEUE_SIZE;
+ amf_int64 applied_input_queue_size = 0;
+ const auto queue_result = encoder->GetProperty(input_queue_property, &applied_input_queue_size);
+ const bool valid_queue_size = queue_result == AMF_OK &&
+ applied_input_queue_size >= 1 &&
+ applied_input_queue_size <= static_cast(lifecycle::maximum_amf_input_queue_size);
+ const auto expected_input_queue_size = config.input_queue_size;
+ if (expected_input_queue_size &&
+ (!valid_queue_size || applied_input_queue_size != *expected_input_queue_size)) {
+ BOOST_LOG(error) << "AMF: driver changed the requested input queue size after Init"
+ << " (requested=" << *expected_input_queue_size
+ << ", applied=" << applied_input_queue_size
+ << ", result=" << queue_result << ')';
+ return false;
+ }
+ if (valid_queue_size) {
+ encoder_input_queue_size = static_cast(applied_input_queue_size);
+ } else {
+ encoder_input_queue_size = lifecycle::default_amf_input_queue_size;
+ BOOST_LOG(warning) << "AMF: could not read the applied input queue size; reserving for the documented default of "
+ << encoder_input_queue_size;
+ }
+ }
+
+ // Derive runtime watchdog threshold from framerate so the fatal-error
+ // signal fires after roughly 1s of wall-clock time regardless of fps.
+ // Floor at 30 so brief driver scheduling stalls do not cause false reinit.
+ {
+ int fps = client_config.framerate > 0 ? client_config.framerate : 60;
+ max_consecutive_failures = std::max(30, fps);
+ }
+
+ // Check if driver supports QUERY_TIMEOUT by reading back the property (FFmpeg pattern)
+ {
+ const wchar_t *qt_prop = (video_format == 0) ? AMF_VIDEO_ENCODER_QUERY_TIMEOUT :
+ (video_format == 1) ? AMF_VIDEO_ENCODER_HEVC_QUERY_TIMEOUT :
+ AMF_VIDEO_ENCODER_AV1_QUERY_TIMEOUT;
+ amf_int64 qt_val = 0;
+ auto qt_res = encoder->GetProperty(qt_prop, &qt_val);
+ query_timeout_supported = qt_res == AMF_OK && qt_val > 0;
+ BOOST_LOG(info) << "AMF: QUERY_TIMEOUT " << (query_timeout_supported ? "supported" : "not supported") << " (value=" << qt_val << ")";
+ }
+
+ if (video_format == 2) {
+ amf_int64 applied_latency_mode = -1;
+ const auto latency_result = encoder->GetProperty(
+ AMF_VIDEO_ENCODER_AV1_ENCODING_LATENCY_MODE,
+ &applied_latency_mode);
+ BOOST_LOG(debug) << "AMF: applied AV1 encoding latency mode=" << applied_latency_mode
+ << " (result=" << latency_result << ')';
+ }
+
+ // Create the rotating textures that are both render targets and native AMF
+ // inputs. Rendering directly into a reserved slot removes a full-frame GPU copy.
+ DXGI_FORMAT dxgi_fmt;
+ switch (buffer_format) {
+ case platf::pix_fmt_e::nv12:
+ dxgi_fmt = DXGI_FORMAT_NV12;
+ break;
+ case platf::pix_fmt_e::p010:
+ dxgi_fmt = DXGI_FORMAT_P010;
+ break;
+ default:
+ dxgi_fmt = (colorspace.bit_depth == 10) ? DXGI_FORMAT_P010 : DXGI_FORMAT_NV12;
+ break;
+ }
+
+ input_surface_desc = {};
+ input_surface_desc.Width = client_config.width;
+ input_surface_desc.Height = client_config.height;
+ input_surface_desc.MipLevels = 1;
+ input_surface_desc.ArraySize = 1;
+ input_surface_desc.Format = dxgi_fmt;
+ input_surface_desc.SampleDesc.Count = 1;
+ input_surface_desc.Usage = D3D11_USAGE_DEFAULT;
+ input_surface_desc.BindFlags = D3D11_BIND_RENDER_TARGET |
+ (preanalysis_enabled ? D3D11_BIND_SHADER_RESOURCE : 0);
+
+ for (auto &slot : input_surface_ring) {
+ slot.texture.Reset();
+ slot.frame_index = 0;
+ slot.state = input_surface_state_e::free;
+ slot.release_notified = false;
+ }
+ next_input_surface_slot = 0;
+ active_input_surface_count = lifecycle::initial_input_surface_count(preanalysis_lookahead_depth);
+ if (!ensure_input_surface_count(active_input_surface_count)) {
+ return false;
+ }
+ prepared_input_surface_slot.reset();
+ last_rendered_input_surface_slot.reset();
+
+
+
+ // Clamp effective LTR slots to what the encoder actually reserves.
+ // When max_ltr_frames == 0 (default), the entire LTR/RFI subsystem becomes
+ // a no-op: the IDR baseline marking, slot rotation, and invalidate handling
+ // below all gate on `effective_ltr_slots > 0`. The fallback for client-side
+ // invalidate_ref_frames in this case is force_idr=true (see video.cpp).
+ effective_ltr_slots = (max_ltr_frames > 0) ? std::min(max_ltr_frames, MAX_LTR_SLOTS) : 0;
+
+ // Reset LTR state
+ for (auto &valid : ltr_slots_valid) valid = false;
+ for (auto &fi : ltr_slot_frame_index) fi = 0;
+ current_ltr_slot = 0;
+ rfi_pending = false;
+ input_surfaces_in_flight = 0;
+ accepted_input_count = 0;
+ completed_output_count = 0;
+ accepted_frame_indices.clear();
+ consecutive_submit_failures = 0;
+ consecutive_surface_failures = 0;
+ consecutive_query_failures = 0;
+ consecutive_output_failures = 0;
+ consecutive_catchup_misses = 0;
+ last_output_progress = std::chrono::steady_clock::now();
+ submit_backpressure_started = {};
+ completed_outputs.clear();
+ frame_rfi_flags.clear();
+ last_completed_frame_index = 0;
+ last_submitted_frame_index = 0;
+ output_fatal = false;
+ drain_requested = false;
+ drain_complete = false;
+ output_poll_requested = false;
+ active_output_poll_waiters = 0;
+ catchup_batch_count = 0;
+
+ output_thread = std::jthread([this](std::stop_token stop_token) {
+ output_pump(stop_token);
+ });
+
+ auto codec_name = (video_format == 0) ? "H.264" :
+ (video_format == 1) ? "HEVC" :
+ (video_format == 2) ? "AV1" : "Unknown";
+ BOOST_LOG(info) << "AMF: standalone " << codec_name << " encoder created ("
+ << client_config.width << "x" << client_config.height << " @ "
+ << client_config.framerate << "fps, LTR=" << max_ltr_frames
+ << ", PA=" << (preanalysis_enabled ? "on" : "off")
+ << ", lookahead=" << preanalysis_lookahead_depth
+ << ", input_queue=" << encoder_input_queue_size
+ << ", input_surfaces=" << active_input_surface_count
+ << ", slices=" << client_config.slicesPerFrame << ")";
+ return true;
+ }
+
+ void
+ amf_d3d11::destroy_encoder() {
+ if (output_thread.joinable()) {
+ // Synchronize the stop request with the pump's predicate check. Requesting
+ // stop and notifying without this mutex permits the pump to check false,
+ // miss the notification, and sleep forever because std::stop_token does not
+ // wake an ordinary condition_variable on its own.
+ {
+ std::lock_guard lock(state_mutex);
+ output_thread.request_stop();
+ }
+ state_cv.notify_all();
+ output_thread.join();
+ }
+ if (encoder) {
+ encoder->Terminate();
+ encoder = nullptr;
+ }
+ if (context) {
+ context->Terminate();
+ context = nullptr;
+ }
+ for (auto &slot : input_surface_ring) {
+ slot.texture.Reset();
+ slot.frame_index = 0;
+ slot.state = input_surface_state_e::free;
+ slot.release_notified = false;
+ }
+ {
+ std::lock_guard lock(state_mutex);
+ prepared_input_surface_slot.reset();
+ last_rendered_input_surface_slot.reset();
+ completed_outputs.clear();
+ frame_rfi_flags.clear();
+ input_surfaces_in_flight = 0;
+ accepted_input_count = 0;
+ completed_output_count = 0;
+ accepted_frame_indices.clear();
+ last_completed_frame_index = 0;
+ last_submitted_frame_index = 0;
+ output_fatal = false;
+ drain_requested = false;
+ drain_complete = false;
+ output_poll_requested = false;
+ active_output_poll_waiters = 0;
+ catchup_batch_count = 0;
+ consecutive_output_failures = 0;
+ }
+ last_output_progress = {};
+ submit_backpressure_started = {};
+ preanalysis_enabled = false;
+ preanalysis_lookahead_depth = 0;
+ query_timeout_supported = false;
+ encoder_input_queue_size = lifecycle::default_amf_input_queue_size;
+ input_surface_desc = {};
+
+ if (amf_dll) {
+ FreeLibrary(amf_dll);
+ amf_dll = nullptr;
+ }
+ factory = nullptr;
+ }
+
+ amf_encoded_frame
+ amf_d3d11::extract_encoded_frame(const ::amf::AMFDataPtr &output_data) {
+ amf_encoded_frame result;
+ if (!output_data) {
+ return result;
+ }
+
+ const auto output_pts = output_data->GetPts();
+ if (output_pts >= 0) {
+ result.frame_index = static_cast(output_pts);
+ }
+ ::amf::AMFBufferPtr buffer(output_data);
+ if (!buffer) {
+ BOOST_LOG(error) << "AMF: output is not a buffer";
+ return result;
+ }
+
+ auto data_ptr = static_cast(buffer->GetNative());
+ auto data_size = buffer->GetSize();
+ if (!data_ptr || data_size == 0) {
+ BOOST_LOG(error) << "AMF: encoder returned an empty output buffer for frame " << result.frame_index;
+ return result;
+ }
+ result.data.assign(data_ptr, data_ptr + data_size);
+
+ // after_ref_frame_invalidation is resolved by the pump under state_mutex.
+ // This function deliberately touches no lock-protected members so the
+ // bitstream copy and driver property reads can run outside the lock.
+
+ amf_int64 output_type = 0;
+ if (video_format == 0) {
+ if (output_data->GetProperty(AMF_VIDEO_ENCODER_OUTPUT_DATA_TYPE, &output_type) == AMF_OK) {
+ result.idr = (output_type == AMF_VIDEO_ENCODER_OUTPUT_DATA_TYPE_IDR);
+ }
+ }
+ else if (video_format == 1) {
+ if (output_data->GetProperty(AMF_VIDEO_ENCODER_HEVC_OUTPUT_DATA_TYPE, &output_type) == AMF_OK) {
+ result.idr = (output_type == AMF_VIDEO_ENCODER_HEVC_OUTPUT_DATA_TYPE_IDR);
+ }
+ }
+ else {
+ if (output_data->GetProperty(AMF_VIDEO_ENCODER_AV1_OUTPUT_FRAME_TYPE, &output_type) == AMF_OK) {
+ result.idr = (output_type == AMF_VIDEO_ENCODER_AV1_OUTPUT_FRAME_TYPE_KEY);
+ }
+ }
+
+ if (statistics_enabled) {
+ amf_int64 avg_qp = 0;
+ const wchar_t *avg_qp_prop = (video_format == 0) ? AMF_VIDEO_ENCODER_STATISTIC_AVERAGE_QP :
+ (video_format == 1) ? AMF_VIDEO_ENCODER_HEVC_STATISTIC_AVERAGE_QP :
+ AMF_VIDEO_ENCODER_AV1_STATISTIC_AVERAGE_Q_INDEX;
+ if (output_data->GetProperty(avg_qp_prop, &avg_qp) == AMF_OK) {
+ BOOST_LOG(debug) << "AMF: frame " << result.frame_index << " avg_qp=" << avg_qp << " size=" << data_size;
+ }
+ }
+ if (psnr_enabled) {
+ double psnr_y = 0;
+ const wchar_t *psnr_prop = (video_format == 0) ? AMF_VIDEO_ENCODER_STATISTIC_PSNR_Y :
+ (video_format == 1) ? AMF_VIDEO_ENCODER_HEVC_STATISTIC_PSNR_Y :
+ AMF_VIDEO_ENCODER_AV1_STATISTIC_PSNR_Y;
+ if (output_data->GetProperty(psnr_prop, &psnr_y) == AMF_OK) {
+ BOOST_LOG(debug) << "AMF: frame " << result.frame_index << " PSNR_Y=" << psnr_y;
+ }
+ }
+ if (ssim_enabled) {
+ double ssim_y = 0;
+ const wchar_t *ssim_prop = (video_format == 0) ? AMF_VIDEO_ENCODER_STATISTIC_SSIM_Y :
+ (video_format == 1) ? AMF_VIDEO_ENCODER_HEVC_STATISTIC_SSIM_Y :
+ AMF_VIDEO_ENCODER_AV1_STATISTIC_SSIM_Y;
+ if (output_data->GetProperty(ssim_prop, &ssim_y) == AMF_OK) {
+ BOOST_LOG(debug) << "AMF: frame " << result.frame_index << " SSIM_Y=" << ssim_y;
+ }
+ }
+
+ return result;
+ }
+
+ void
+ amf_d3d11::output_pump(std::stop_token stop_token) noexcept {
+ SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL);
+
+ try {
+ while (!stop_token.stop_requested()) {
+ {
+ std::unique_lock lock(state_mutex);
+ state_cv.wait(lock, [&]() {
+ return stop_token.stop_requested() || output_poll_requested || drain_requested || output_fatal;
+ });
+ if (stop_token.stop_requested() || output_fatal) {
+ break;
+ }
+ }
+
+ uint64_t queried_through_input = 0;
+ {
+ std::lock_guard lock(state_mutex);
+ queried_through_input = accepted_input_count;
+ }
+
+ ::amf::AMFDataPtr output_data;
+ const auto query_result = encoder ? encoder->QueryOutput(&output_data) : AMF_FAIL;
+ if (stop_token.stop_requested()) {
+ break;
+ }
+
+ if (output_data) {
+ // Extract outside state_mutex: the bitstream assign() is multi-MB on
+ // high-bitrate IDR frames and encode_frame takes this same mutex as
+ // its first action — copying under the lock adds submit-path jitter.
+ auto encoded_frame = extract_encoded_frame(output_data);
+ {
+ std::lock_guard lock(state_mutex);
+ if (encoded_frame.data.empty()) {
+ if (++consecutive_output_failures >= max_consecutive_failures) {
+ BOOST_LOG(error) << "AMF: encoder repeatedly returned invalid output; signaling reinit";
+ output_fatal = true;
+ }
+ } else {
+ auto rfi_flag = frame_rfi_flags.find(encoded_frame.frame_index);
+ if (rfi_flag != frame_rfi_flags.end()) {
+ encoded_frame.after_ref_frame_invalidation = rfi_flag->second;
+ frame_rfi_flags.erase(rfi_flag);
+ }
+ while (frame_rfi_flags.size() > 256) {
+ frame_rfi_flags.erase(frame_rfi_flags.begin());
+ }
+ consecutive_output_failures = 0;
+ ++completed_output_count;
+ last_completed_frame_index = std::max(last_completed_frame_index, encoded_frame.frame_index);
+ last_output_progress = std::chrono::steady_clock::now();
+ consecutive_query_failures = 0;
+ completed_outputs.emplace_back(std::move(encoded_frame));
+ }
+ }
+ state_cv.notify_all();
+ continue;
+ }
+
+ if (query_result == AMF_EOF) {
+ bool expected_eof = false;
+ {
+ std::lock_guard lock(state_mutex);
+ expected_eof = drain_requested;
+ if (!expected_eof) {
+ BOOST_LOG(error) << "AMF: output pump reached EOF without a drain request";
+ output_fatal = true;
+ }
+ drain_complete = expected_eof && !output_fatal;
+ }
+ state_cv.notify_all();
+ break;
+ }
+
+ const bool no_output_available = query_result == AMF_OK ||
+ query_result == AMF_REPEAT ||
+ query_result == AMF_NEED_MORE_INPUT;
+ if (no_output_available) {
+ bool poll_disarmed = false;
+ {
+ std::lock_guard lock(state_mutex);
+ consecutive_query_failures = 0;
+ if (lifecycle::should_disarm_output_poll(
+ queried_through_input,
+ accepted_input_count,
+ drain_requested,
+ active_output_poll_waiters)) {
+ // No bounded waiter remains for this accepted generation. Sleep
+ // until a new input explicitly re-arms polling; an output that
+ // legitimately never arrives must not leave a permanent poll loop.
+ // Do not clear a re-arm from SubmitInput that raced this query.
+ output_poll_requested = false;
+ poll_disarmed = true;
+ }
+ }
+ if (!poll_disarmed) {
+ // Drain, an active waiter, or a concurrent submission keeps polling
+ // armed. QUERY_TIMEOUT-backed OK/REPEAT calls already blocked for up
+ // to 1 ms. Some runtimes return NEED_MORE_INPUT immediately even from
+ // QueryOutput, so pace that defensive compatibility case rather
+ // than hot-spinning an above-normal-priority thread — but stay
+ // interruptible so stop/drain/new-input wake the pump immediately
+ // instead of eating the remainder of a fixed sleep quantum.
+ if (query_result == AMF_NEED_MORE_INPUT || !query_timeout_supported) {
+ std::unique_lock lock(state_mutex);
+ state_cv.wait_for(lock, std::chrono::milliseconds(1), [&]() {
+ return stop_token.stop_requested() || output_fatal || drain_requested;
+ });
+ }
+ }
+ continue;
+ }
+
+ {
+ bool fatal = false;
+ {
+ std::lock_guard lock(state_mutex);
+ const auto removed_reason = device ? device->GetDeviceRemovedReason() : S_OK;
+ if (removed_reason != S_OK) {
+ BOOST_LOG(error) << "AMF: output pump detected D3D11 device loss, reason: 0x"
+ << util::hex(removed_reason).to_string_view();
+ output_fatal = true;
+ fatal = true;
+ } else if (++consecutive_query_failures >= max_consecutive_failures && !output_fatal) {
+ BOOST_LOG(error) << "AMF: output pump observed repeated QueryOutput failures (error="
+ << query_result << "); signaling reinit";
+ output_fatal = true;
+ fatal = true;
+ }
+ }
+ if (fatal) {
+ state_cv.notify_all();
+ break;
+ }
+ // Interruptible failure backoff — stop requests must not wait out
+ // the sleep quantum.
+ std::unique_lock lock(state_mutex);
+ state_cv.wait_for(lock, std::chrono::milliseconds(1), [&]() {
+ return stop_token.stop_requested() || output_fatal;
+ });
+ }
+ }
+ } catch (const std::exception &ex) {
+ BOOST_LOG(error) << "AMF: output pump failed: " << ex.what();
+ std::lock_guard lock(state_mutex);
+ output_fatal = true;
+ state_cv.notify_all();
+ } catch (...) {
+ BOOST_LOG(error) << "AMF: output pump failed with an unknown exception";
+ std::lock_guard lock(state_mutex);
+ output_fatal = true;
+ state_cv.notify_all();
+ }
+ }
+
+ amf_encode_result
+ amf_d3d11::encode_frame(uint64_t frame_index, bool force_idr) {
+ amf_encode_result result;
+ auto &results = result.frames;
+ auto drain_completed_outputs_locked = [&]() {
+ while (!completed_outputs.empty()) {
+ results.emplace_back(std::move(completed_outputs.front()));
+ completed_outputs.pop_front();
+ }
+ };
+
+ if (!encoder) {
+ result.fatal = true;
+ return result;
+ }
+
+ std::size_t slot_index = 0;
+ std::optional duplicate_source_slot;
+ uint64_t completed_before_submission = 0;
+ {
+ std::unique_lock lock(state_mutex);
+ drain_completed_outputs_locked();
+ completed_before_submission = completed_output_count;
+ if (output_fatal) {
+ result.fatal = true;
+ return result;
+ }
+
+ if (prepared_input_surface_slot) {
+ slot_index = *prepared_input_surface_slot;
+ prepared_input_surface_slot.reset();
+ last_rendered_input_surface_slot = slot_index;
+ } else if (last_rendered_input_surface_slot) {
+ // Sunshine intentionally encodes the most recent converted image again when
+ // capture is static/times out (minimum-FPS refresh, IDR requests, and encoder
+ // probing). Prefer the same direct-render surface once AMF releases it. A
+ // PA lookahead may still own that slot, so rotate to another free texture
+ // and copy the last converted image there rather than starving lookahead.
+ const auto source_slot = *last_rendered_input_surface_slot;
+ auto find_repeat_slot = [&]() -> std::optional {
+ return lifecycle::select_repeat_surface(
+ input_surface_ring,
+ source_slot,
+ next_input_surface_slot,
+ active_input_surface_count);
+ };
+ auto repeat_slot = find_repeat_slot();
+ if (!repeat_slot && !output_fatal && active_input_surface_count < input_surface_ring.size()) {
+ // Allocate outside the lock: a multi-MB CreateTexture2D under
+ // state_mutex stalls the pump and AMF's release observers. Expansion
+ // only ever runs on this thread, so re-checking after relock only has
+ // to account for slots the observers freed meanwhile.
+ lock.unlock();
+ auto expansion_texture = create_input_surface_texture();
+ lock.lock();
+ repeat_slot = find_repeat_slot();
+ if (!repeat_slot && expansion_texture && !output_fatal &&
+ active_input_surface_count < input_surface_ring.size()) {
+ auto &expansion_slot = input_surface_ring[active_input_surface_count];
+ if (!expansion_slot.texture) {
+ expansion_slot.texture = std::move(expansion_texture);
+ }
+ repeat_slot = active_input_surface_count;
+ ++active_input_surface_count;
+ BOOST_LOG(debug) << "AMF: expanded direct-render input pool to " << active_input_surface_count
+ << " surfaces for repeated input";
+ }
+ }
+ if (!repeat_slot) {
+ state_cv.wait_for(lock, lifecycle::driver_wait_budget(current_config.framerate), [&]() {
+ return output_fatal || find_repeat_slot().has_value();
+ });
+ repeat_slot = find_repeat_slot();
+ }
+ drain_completed_outputs_locked();
+ if (output_fatal) {
+ result.fatal = true;
+ return result;
+ }
+ if (!repeat_slot) {
+ // The previous submission is still owned by AMF. Dropping this minimum-FPS
+ // duplicate is safer than reusing an in-flight surface; the pump can still
+ // return completed output in this result.
+ return result;
+ }
+ slot_index = *repeat_slot;
+ if (slot_index != source_slot) {
+ duplicate_source_slot = source_slot;
+ }
+ input_surface_ring[slot_index].state = input_surface_state_e::reserved;
+ input_surface_ring[slot_index].release_notified = false;
+ ++input_surface_ring[slot_index].generation;
+ last_rendered_input_surface_slot = slot_index;
+ next_input_surface_slot = (slot_index + 1) % active_input_surface_count;
+ } else {
+ BOOST_LOG(error) << "AMF: encode called before any input surface was rendered";
+ result.fatal = true;
+ return result;
+ }
+ }
+
+ auto &input_slot = input_surface_ring[slot_index];
+ auto release_reserved_slot = util::fail_guard([&]() {
+ std::lock_guard lock(state_mutex);
+ if (input_slot.state == input_surface_state_e::reserved) {
+ input_slot.state = input_surface_state_e::free;
+ input_slot.frame_index = 0;
+ state_cv.notify_all();
+ }
+ });
+ if (duplicate_source_slot) {
+ Microsoft::WRL::ComPtr immediate_context;
+ device->GetImmediateContext(immediate_context.GetAddressOf());
+ if (!immediate_context) {
+ BOOST_LOG(error) << "AMF: could not acquire D3D11 context for repeated lookahead input";
+ result.fatal = true;
+ return result;
+ }
+ immediate_context->CopyResource(
+ input_slot.texture.Get(),
+ input_surface_ring[*duplicate_source_slot].texture.Get());
+ }
+ if (const auto removed_reason = device->GetDeviceRemovedReason(); removed_reason != S_OK) {
+ BOOST_LOG(error) << "AMF: D3D11 device lost before native input submission, reason: 0x"
+ << util::hex(removed_reason).to_string_view();
+ result.fatal = true;
+ return result;
+ }
+
+ // Each wrapper references a distinct ring texture. AMF may retain an input after
+ // producing output (or drop it without output), so recycle the texture only from
+ // AMFSurfaceObserver::OnSurfaceDataRelease -- never by guessing from output PTS.
+ ::amf::AMFSurfacePtr surface;
+ auto res = context->CreateSurfaceFromDX11Native(
+ input_slot.texture.Get(),
+ &surface,
+ &input_surface_release_observers[slot_index]);
+ if (res != AMF_OK || !surface) {
+ BOOST_LOG(error) << "AMF: CreateSurfaceFromDX11Native failed, error: " << res;
+ const auto removed_reason = device->GetDeviceRemovedReason();
+ if (removed_reason != S_OK) {
+ BOOST_LOG(error) << "AMF: D3D11 device lost, reason: 0x" << util::hex(removed_reason).to_string_view();
+ result.fatal = true;
+ } else if (++consecutive_surface_failures >= max_consecutive_failures) {
+ BOOST_LOG(error) << "AMF: repeated native surface creation failures; signaling reinit";
+ result.fatal = true;
+ }
+ return result;
+ }
+ consecutive_surface_failures = 0;
+ // Set crop to actual frame dimensions (hw surfaces can be vertically aligned by 16)
+ surface->SetCrop(0, 0, encode_width, encode_height);
+ surface->SetPts(static_cast(frame_index));
+ // Reading the slot generation without the lock is safe: it only changes at
+ // re-reservation, which cannot happen while this thread holds the reservation.
+ surface->SetProperty(kSlotGenerationProperty, static_cast(input_slot.generation));
+
+ // Snapshot the recovery plan under the state lock, then release it before any
+ // AMF call. SubmitInput may synchronously invoke OnSurfaceDataRelease(), which
+ // must be able to acquire state_mutex without re-entering a non-recursive lock.
+ bool frame_after_ref_frame_invalidation = false;
+ int ltr_slot_to_commit = -1;
+ int next_ltr_slot_to_commit = 0;
+ bool consume_rfi_on_accept = false;
+ bool reset_ltr_cache_on_accept = false;
+ int ltr_slot_to_preserve_on_accept = -1;
+ int ltr_reference_slot = -1;
+ int effective_ltr_slots_snapshot = 0;
+ {
+ std::lock_guard lock(state_mutex);
+ effective_ltr_slots_snapshot = rfi_enabled ? effective_ltr_slots : 0;
+ next_ltr_slot_to_commit = current_ltr_slot;
+ if (force_idr) {
+ reset_ltr_cache_on_accept = true;
+ consume_rfi_on_accept = true;
+ if (effective_ltr_slots_snapshot > 0) {
+ ltr_slot_to_commit = 0;
+ next_ltr_slot_to_commit = (effective_ltr_slots_snapshot > 1) ? 1 : 0;
+ }
+ } else if (rfi_pending && effective_ltr_slots_snapshot > 0) {
+ ltr_reference_slot = static_cast(last_rfi_ltr_index);
+ consume_rfi_on_accept = true;
+ reset_ltr_cache_on_accept = true;
+ ltr_slot_to_preserve_on_accept = ltr_reference_slot;
+ frame_after_ref_frame_invalidation = true;
+ } else if (effective_ltr_slots_snapshot > 0 && (frame_index % LTR_MARK_INTERVAL) == 0) {
+ ltr_slot_to_commit = current_ltr_slot;
+ if (effective_ltr_slots_snapshot > 1) {
+ next_ltr_slot_to_commit = current_ltr_slot + 1;
+ if (next_ltr_slot_to_commit >= effective_ltr_slots_snapshot) {
+ next_ltr_slot_to_commit = 1;
+ }
+ }
+ }
+ }
+
+ auto disable_rfi_after_property_failure = [&](const char *property_label, AMF_RESULT property_result) {
+ BOOST_LOG(warning) << "AMF: failed to apply " << property_label << " (error=" << property_result
+ << "); disabling RFI and falling back to IDR recovery";
+ std::lock_guard lock(state_mutex);
+ rfi_enabled = false;
+ max_ltr_frames = 0;
+ effective_ltr_slots = 0;
+ rfi_pending = false;
+ for (auto &valid : ltr_slots_valid) valid = false;
+ for (auto &marked_frame : ltr_slot_frame_index) marked_frame = 0;
+ };
+
+ auto set_ltr_surface_property = [&](const wchar_t *property, amf_int64 value, const char *label) {
+ const auto property_result = surface->SetProperty(property, value);
+ if (property_result == AMF_OK) {
+ return true;
+ }
+ disable_rfi_after_property_failure(label, property_result);
+ return false;
+ };
+
+ auto set_forced_idr_properties = [&]() {
+ auto check = [&](AMF_RESULT property_result, const char *label) {
+ if (property_result == AMF_OK) return true;
+ BOOST_LOG(error) << "AMF: failed to set " << label << " on recovery frame, error=" << property_result;
+ return false;
+ };
+ if (video_format == 0) {
+ return check(surface->SetProperty(AMF_VIDEO_ENCODER_FORCE_PICTURE_TYPE, AMF_VIDEO_ENCODER_PICTURE_TYPE_IDR), "H.264 IDR type") &&
+ check(surface->SetProperty(AMF_VIDEO_ENCODER_INSERT_SPS, true), "H.264 SPS insertion") &&
+ check(surface->SetProperty(AMF_VIDEO_ENCODER_INSERT_PPS, true), "H.264 PPS insertion");
+ }
+ if (video_format == 1) {
+ return check(surface->SetProperty(AMF_VIDEO_ENCODER_HEVC_FORCE_PICTURE_TYPE, AMF_VIDEO_ENCODER_HEVC_PICTURE_TYPE_IDR), "HEVC IDR type") &&
+ check(surface->SetProperty(AMF_VIDEO_ENCODER_HEVC_INSERT_HEADER, true), "HEVC header insertion");
+ }
+ return check(surface->SetProperty(AMF_VIDEO_ENCODER_AV1_FORCE_FRAME_TYPE, AMF_VIDEO_ENCODER_AV1_FORCE_FRAME_TYPE_KEY), "AV1 keyframe type") &&
+ check(surface->SetProperty(AMF_VIDEO_ENCODER_AV1_FORCE_INSERT_SEQUENCE_HEADER, true), "AV1 sequence header insertion");
+ };
+
+ if (force_idr) {
+ if (!set_forced_idr_properties()) {
+ result.fatal = true;
+ return result;
+ }
+
+ // After IDR, mark LTR slot 0 for RFI baseline
+ if (ltr_slot_to_commit >= 0) {
+ const wchar_t *mark_property = video_format == 0 ? AMF_VIDEO_ENCODER_MARK_CURRENT_WITH_LTR_INDEX :
+ video_format == 1 ? AMF_VIDEO_ENCODER_HEVC_MARK_CURRENT_WITH_LTR_INDEX :
+ AMF_VIDEO_ENCODER_AV1_MARK_CURRENT_WITH_LTR_INDEX;
+ if (!set_ltr_surface_property(mark_property, 0, "IDR LTR baseline mark")) {
+ ltr_slot_to_commit = -1;
+ }
+ }
+ }
+ else if (ltr_reference_slot >= 0) {
+ // After RFI: force reference to the saved LTR frame
+ const auto ltr_bitfield = static_cast(1LL << ltr_reference_slot);
+ const wchar_t *reference_property = video_format == 0 ? AMF_VIDEO_ENCODER_FORCE_LTR_REFERENCE_BITFIELD :
+ video_format == 1 ? AMF_VIDEO_ENCODER_HEVC_FORCE_LTR_REFERENCE_BITFIELD :
+ AMF_VIDEO_ENCODER_AV1_FORCE_LTR_REFERENCE_BITFIELD;
+ if (!set_ltr_surface_property(reference_property, ltr_bitfield, "forced LTR reference")) {
+ // Do not claim RFI recovery if the driver rejected the reference. Make
+ // this same submission an IDR so the caller's recovery is not suppressed.
+ frame_after_ref_frame_invalidation = false;
+ consume_rfi_on_accept = false;
+ ltr_slot_to_preserve_on_accept = -1;
+ reset_ltr_cache_on_accept = true;
+ if (!set_forced_idr_properties()) {
+ result.fatal = true;
+ return result;
+ }
+ }
+ }
+ else if (ltr_slot_to_commit >= 0) {
+ // Periodically mark current frame as LTR for future RFI use.
+ // Rotate through slots 1..N-1 so the IDR baseline in slot 0 stays valid
+ // even if every recent periodic anchor lands inside a loss burst. With a
+ // single slot configured, fall back to overwriting slot 0.
+ const wchar_t *mark_property = video_format == 0 ? AMF_VIDEO_ENCODER_MARK_CURRENT_WITH_LTR_INDEX :
+ video_format == 1 ? AMF_VIDEO_ENCODER_HEVC_MARK_CURRENT_WITH_LTR_INDEX :
+ AMF_VIDEO_ENCODER_AV1_MARK_CURRENT_WITH_LTR_INDEX;
+ if (!set_ltr_surface_property(mark_property, static_cast(ltr_slot_to_commit), "periodic LTR mark")) {
+ ltr_slot_to_commit = -1;
+ }
+ }
+
+ if (statistics_enabled) {
+ surface->SetProperty(video_format == 0 ? AMF_VIDEO_ENCODER_STATISTICS_FEEDBACK :
+ video_format == 1 ? AMF_VIDEO_ENCODER_HEVC_STATISTICS_FEEDBACK :
+ AMF_VIDEO_ENCODER_AV1_STATISTICS_FEEDBACK, true);
+ }
+ if (psnr_enabled) {
+ surface->SetProperty(video_format == 0 ? AMF_VIDEO_ENCODER_PSNR_FEEDBACK :
+ video_format == 1 ? AMF_VIDEO_ENCODER_HEVC_PSNR_FEEDBACK :
+ AMF_VIDEO_ENCODER_AV1_PSNR_FEEDBACK, true);
+ }
+ if (ssim_enabled) {
+ surface->SetProperty(video_format == 0 ? AMF_VIDEO_ENCODER_SSIM_FEEDBACK :
+ video_format == 1 ? AMF_VIDEO_ENCODER_HEVC_SSIM_FEEDBACK :
+ AMF_VIDEO_ENCODER_AV1_SSIM_FEEDBACK, true);
+ }
+
+ // SubmitInput can block inside the AMD runtime once a PA pipeline has retained
+ // more inputs than its configured queue. Check known ownership before entering
+ // the driver so probe/runtime deadlines remain enforceable even on a stalled
+ // encoder. The reserved wrapper is released by release_reserved_slot on return.
+ const auto submission_deadline = std::chrono::steady_clock::now() +
+ lifecycle::driver_wait_budget(current_config.framerate);
+ {
+ std::unique_lock lock(state_mutex);
+ auto capacity_available = [&]() {
+ return lifecycle::driver_submit_capacity_available(
+ input_surfaces_in_flight,
+ encoder_input_queue_size);
+ };
+ if (!capacity_available()) {
+ drain_completed_outputs_locked();
+ ++active_output_poll_waiters;
+ output_poll_requested = true;
+ state_cv.notify_all();
+ state_cv.wait_until(lock, submission_deadline, [&]() {
+ // QueryOutput completion and AMFSurfaceObserver release are separate
+ // events. Keep the remaining budget after an output arrives; only an
+ // actual ownership release makes SubmitInput safe again.
+ return lifecycle::saturation_wait_should_finish(output_fatal, capacity_available());
+ });
+ --active_output_poll_waiters;
+ state_cv.notify_all();
+ drain_completed_outputs_locked();
+ if (output_fatal) {
+ result.fatal = true;
+ return result;
+ }
+ if (!capacity_available()) {
+ const auto in_flight = static_cast(input_surfaces_in_flight);
+ lock.unlock();
+ const auto now = std::chrono::steady_clock::now();
+ const auto exhausted_submissions = ++consecutive_submit_failures;
+ if (submit_backpressure_started.time_since_epoch().count() == 0) {
+ submit_backpressure_started = now;
+ }
+ if (exhausted_submissions == 1 || exhausted_submissions % 10 == 0) {
+ BOOST_LOG(warning) << "AMF: not entering SubmitInput while the reported queue is saturated"
+ << " (consecutive=" << exhausted_submissions
+ << ", in_flight=" << in_flight
+ << ", queue=" << encoder_input_queue_size << ')';
+ }
+ if (lifecycle::submit_backpressure_requires_reinit(
+ exhausted_submissions,
+ max_consecutive_failures,
+ true,
+ now - submit_backpressure_started)) {
+ BOOST_LOG(error) << "AMF: saturated input queue made no bounded progress; signaling reinit";
+ result.fatal = true;
+ }
+ return result;
+ }
+ }
+ }
+
+ // Submit input — retry with output draining if input queue is still full (like FFmpeg).
+ //
+ // AMF SubmitInput return values we explicitly handle:
+ // AMF_OK — submitted, count it.
+ // AMF_INPUT_FULL — NOT consumed; encoder queue full, drain output + retry.
+ // AMF_DECODER_NO_FREE_SURFACES — NOT consumed; surface pool exhausted, semantically
+ // equivalent to INPUT_FULL on the input side.
+ // AMF_NEED_MORE_INPUT — CONSUMED; output is merely deferred (PreAnalysis
+ // lookahead priming). Never resubmit the same surface:
+ // FFmpeg's amfenc.c retries AMF_INPUT_FULL only and
+ // releases the surface for every other result.
+ // anything else — real error.
+ auto retryable_submit = [](AMF_RESULT value) {
+ return value == AMF_INPUT_FULL || value == AMF_DECODER_NO_FREE_SURFACES;
+ };
+ res = lifecycle::submit_with_bounded_retry(
+ [&]() {
+ return encoder->SubmitInput(surface);
+ },
+ [&]() {
+ std::unique_lock lock(state_mutex);
+ if (std::chrono::steady_clock::now() >= submission_deadline) return true;
+ const auto completion_generation = completed_output_count;
+ const auto owned_surface_generation = input_surfaces_in_flight;
+ // QueryOutput may have disarmed itself after AMF_NEED_MORE_INPUT. A full
+ // input queue is the opposite condition: queued work must be polled to
+ // make room before this exact surface can be retried.
+ ++active_output_poll_waiters;
+ output_poll_requested = true;
+ state_cv.notify_all();
+ state_cv.wait_until(lock, std::min(
+ submission_deadline,
+ std::chrono::steady_clock::now() + std::chrono::milliseconds(1)), [&]() {
+ return output_fatal ||
+ completed_output_count > completion_generation ||
+ input_surfaces_in_flight < owned_surface_generation;
+ });
+ --active_output_poll_waiters;
+ state_cv.notify_all();
+ return output_fatal || std::chrono::steady_clock::now() >= submission_deadline;
+ },
+ retryable_submit,
+ 20);
+ if (retryable_submit(res)) {
+ int in_flight = 0;
+ {
+ std::lock_guard lock(state_mutex);
+ drain_completed_outputs_locked();
+ if (output_fatal) {
+ result.fatal = true;
+ return result;
+ }
+ in_flight = static_cast(input_surfaces_in_flight);
+ }
+ const char *reason = res == AMF_INPUT_FULL ? "AMF_INPUT_FULL" : "AMF_DECODER_NO_FREE_SURFACES";
+ BOOST_LOG(warning) << "AMF: SubmitInput still " << reason
+ << " after retries, dropping frame " << frame_index
+ << " (in_flight=" << in_flight << ")";
+ const auto now = std::chrono::steady_clock::now();
+ const auto exhausted_submissions = ++consecutive_submit_failures;
+ if (submit_backpressure_started.time_since_epoch().count() == 0) {
+ submit_backpressure_started = now;
+ }
+ const auto backpressure_start_known = submit_backpressure_started.time_since_epoch().count() != 0;
+ const auto backpressure_duration = backpressure_start_known ?
+ now - submit_backpressure_started :
+ std::chrono::steady_clock::duration::zero();
+ if (lifecycle::submit_backpressure_requires_reinit(
+ exhausted_submissions,
+ max_consecutive_failures,
+ backpressure_start_known,
+ backpressure_duration)) {
+ BOOST_LOG(error) << "AMF: submit backpressure made no bounded progress; signaling reinit"
+ << " (consecutive=" << exhausted_submissions
+ << ", in_flight=" << in_flight << ')';
+ result.fatal = true;
+ }
+ return result;
+ }
+ if (res == AMF_NEED_MORE_INPUT) {
+ // The encoder consumed the surface and only defers output (PreAnalysis
+ // lookahead priming). Account for it exactly like AMF_OK — the pump
+ // already tolerates output arriving later than its input.
+ res = AMF_OK;
+ }
+ if (res != AMF_OK) {
+ BOOST_LOG(error) << "AMF: SubmitInput failed, error: " << res;
+ // Check if the D3D11 device is lost (TDR, driver crash, etc.)
+ if (device) {
+ auto removed_reason = device->GetDeviceRemovedReason();
+ if (removed_reason != S_OK) {
+ BOOST_LOG(error) << "AMF: D3D11 device lost after SubmitInput, reason: 0x" << util::hex(removed_reason).to_string_view();
+ result.fatal = true;
+ return result;
+ }
+ }
+ if (++consecutive_submit_failures >= max_consecutive_failures) {
+ BOOST_LOG(error) << "AMF: " << consecutive_submit_failures << " consecutive SubmitInput failures, signaling reinit";
+ result.fatal = true;
+ }
+ return result;
+ }
+ consecutive_submit_failures = 0;
+ submit_backpressure_started = {};
+ result.input_accepted = true;
+
+ // Drop our wrapper reference immediately after acceptance. AMF retains its
+ // own reference for as long as it owns the native texture; keeping ours until
+ // the end of the bounded output wait would delay the observer callback and
+ // make an otherwise free texture look busy for another 20-50 ms.
+ surface = nullptr;
+
+ std::unique_lock state_lock(state_mutex);
+ const auto input_accepted_at = std::chrono::steady_clock::now();
+ result.input_accepted_at = input_accepted_at;
+ const bool synchronously_released = lifecycle::on_input_accepted(input_slot, frame_index);
+ last_submitted_frame_index = std::max(last_submitted_frame_index, frame_index);
+ const int effective_lookahead_depth = preanalysis_enabled ? preanalysis_lookahead_depth : 0;
+ ++accepted_input_count;
+ const auto required_output_frame_index = lifecycle::record_accepted_frame(
+ accepted_frame_indices,
+ frame_index,
+ effective_lookahead_depth);
+ if (!synchronously_released) {
+ ++input_surfaces_in_flight;
+ }
+ output_poll_requested = true;
+ if (synchronously_released) {
+ state_cv.notify_all();
+ }
+ release_reserved_slot.disable();
+ const bool output_was_expected = lifecycle::delayed_output_is_expected(
+ accepted_input_count - 1,
+ effective_lookahead_depth);
+ const bool output_is_expected = lifecycle::delayed_output_is_expected(
+ accepted_input_count,
+ effective_lookahead_depth);
+ if (!output_was_expected && output_is_expected) {
+ // Start the watchdog only after the PA lookahead is primed. At low minimum
+ // FPS, measuring from the first buffered frame would falsely treat the
+ // intentional lookahead interval as an encoder stall.
+ last_output_progress = std::chrono::steady_clock::now();
+ }
+ state_cv.notify_all();
+
+ lifecycle::commit_recovery_state(
+ result.input_accepted,
+ effective_ltr_slots,
+ reset_ltr_cache_on_accept,
+ ltr_slot_to_preserve_on_accept,
+ ltr_slot_to_commit,
+ next_ltr_slot_to_commit,
+ consume_rfi_on_accept,
+ frame_after_ref_frame_invalidation,
+ frame_index,
+ ltr_slots_valid,
+ ltr_slot_frame_index,
+ current_ltr_slot,
+ rfi_pending,
+ [&](uint64_t recovered_frame_index) {
+ frame_rfi_flags.emplace(recovered_frame_index, true);
+ });
+
+ // AMD's sample and FFmpeg both submit while a separate thread polls output.
+ // Keep that poller alive for this short coalescing window: an early
+ // AMF_NEED_MORE_INPUT is only a point-in-time result and must not disarm the
+ // pump underneath the waiter. With no PA lookahead, wait through any older
+ // queued output until this submission completes; otherwise one initial miss
+ // becomes a permanent one-frame backlog.
+ const auto output_wait_budget = lifecycle::output_coalesce_budget(current_config.framerate);
+ drain_completed_outputs_locked();
+ bool coalesce_target_reached = required_output_frame_index && lifecycle::output_coalesce_target_reached(
+ *required_output_frame_index,
+ completed_before_submission,
+ completed_output_count,
+ last_completed_frame_index);
+ // Capacity handling and SubmitInput retries can drain an older packet after
+ // the initial snapshot. Re-check the live result here so a send-ready packet
+ // is never held while coalescing output for this newer generation.
+ if (output_is_expected && !coalesce_target_reached && results.empty()) {
+ ++active_output_poll_waiters;
+ output_poll_requested = true;
+ state_cv.notify_all();
+ state_cv.wait_for(state_lock, output_wait_budget, [&]() {
+ return output_fatal || lifecycle::output_coalesce_target_reached(
+ *required_output_frame_index,
+ completed_before_submission,
+ completed_output_count,
+ last_completed_frame_index);
+ });
+ --active_output_poll_waiters;
+ state_cv.notify_all();
+ coalesce_target_reached = lifecycle::output_coalesce_target_reached(
+ *required_output_frame_index,
+ completed_before_submission,
+ completed_output_count,
+ last_completed_frame_index);
+ }
+ while (!completed_outputs.empty()) {
+ results.emplace_back(std::move(completed_outputs.front()));
+ completed_outputs.pop_front();
+ }
+ if (output_fatal) {
+ result.fatal = true;
+ }
+
+ if (!output_is_expected || coalesce_target_reached) {
+ consecutive_catchup_misses = 0;
+ } else if (++consecutive_catchup_misses % max_consecutive_failures == 0) {
+ BOOST_LOG(warning) << "AMF: encoder output has not caught up for " << consecutive_catchup_misses
+ << " submitted frames (owned_surfaces=" << input_surfaces_in_flight
+ << ", accepted=" << accepted_input_count
+ << ", outputs=" << completed_output_count << ')';
+ }
+
+ const auto now = std::chrono::steady_clock::now();
+ const bool output_still_expected = lifecycle::delayed_output_is_expected(
+ accepted_input_count,
+ effective_lookahead_depth);
+ if (output_still_expected && last_output_progress.time_since_epoch().count() != 0 &&
+ now - last_output_progress >= std::chrono::seconds(2)) {
+ BOOST_LOG(error) << "AMF: accepted input but produced no output for 2 seconds; signaling reinit";
+ result.fatal = true;
+ }
+
+ if (results.size() > 1 && ++catchup_batch_count % static_cast(max_consecutive_failures) == 0) {
+ BOOST_LOG(debug) << "AMF: observed " << catchup_batch_count
+ << " multi-frame catch-up batches; latest drained " << results.size()
+ << " frames through frame " << results.back().frame_index;
+ }
+
+ return result;
+ }
+
+ amf_encode_result
+ amf_d3d11::drain_output(std::chrono::milliseconds timeout) {
+ amf_encode_result result;
+ std::unique_lock lock(state_mutex);
+ ++active_output_poll_waiters;
+ output_poll_requested = true;
+ state_cv.notify_all();
+ state_cv.wait_for(lock, timeout, [&]() {
+ return output_fatal || drain_complete || !completed_outputs.empty();
+ });
+ --active_output_poll_waiters;
+ state_cv.notify_all();
+ while (!completed_outputs.empty()) {
+ result.frames.emplace_back(std::move(completed_outputs.front()));
+ completed_outputs.pop_front();
+ }
+ result.fatal = output_fatal;
+ return result;
+ }
+
+ bool
+ amf_d3d11::has_output_due() {
+ std::lock_guard lock(state_mutex);
+ return lifecycle::output_delivery_is_due(
+ accepted_input_count,
+ completed_output_count,
+ preanalysis_lookahead_depth,
+ !completed_outputs.empty());
+ }
+
+ bool
+ amf_d3d11::has_completed_output() {
+ std::lock_guard lock(state_mutex);
+ return !completed_outputs.empty();
+ }
+
+ bool
+ amf_d3d11::has_retained_preanalysis_tail() {
+ std::lock_guard lock(state_mutex);
+ return lifecycle::retained_preanalysis_tail_exists(
+ accepted_input_count,
+ completed_output_count,
+ preanalysis_lookahead_depth);
+ }
+
+ bool
+ amf_d3d11::begin_drain() {
+ if (!encoder) {
+ return false;
+ }
+
+ {
+ std::lock_guard lock(state_mutex);
+ if (drain_requested) {
+ return true;
+ }
+ drain_requested = true;
+ drain_complete = false;
+ output_poll_requested = true;
+ }
+ state_cv.notify_all();
+
+ auto retryable_drain = [](AMF_RESULT value) {
+ return value == AMF_INPUT_FULL || value == AMF_REPEAT;
+ };
+ const auto drain_result = lifecycle::submit_with_bounded_retry(
+ [&]() {
+ return encoder->Drain();
+ },
+ [&]() {
+ std::unique_lock lock(state_mutex);
+ state_cv.wait_for(lock, std::chrono::milliseconds(1));
+ return output_fatal;
+ },
+ retryable_drain,
+ 100);
+
+ if (drain_result == AMF_OK || drain_result == AMF_EOF) {
+ return true;
+ }
+
+ {
+ std::lock_guard lock(state_mutex);
+ drain_requested = false;
+ output_poll_requested = false;
+ }
+ state_cv.notify_all();
+ BOOST_LOG(error) << "AMF: failed to begin delayed-output drain, error: " << drain_result;
+ return false;
+ }
+
+ bool
+ amf_d3d11::invalidate_ref_frames(uint64_t first_frame, uint64_t last_frame) {
+ std::lock_guard lock(state_mutex);
+ if (!encoder || !rfi_enabled || effective_ltr_slots <= 0) return false;
+
+ // Find a valid LTR slot whose frame was marked BEFORE the invalidation range.
+ // This ensures we reference a frame that predates the corrupted frames.
+ int best_ltr = -1;
+ uint64_t best_frame = 0;
+ for (int i = 0; i < effective_ltr_slots; i++) {
+ if (ltr_slots_valid[i] && ltr_slot_frame_index[i] < first_frame) {
+ if (best_ltr < 0 || ltr_slot_frame_index[i] > best_frame) {
+ best_ltr = i;
+ best_frame = ltr_slot_frame_index[i];
+ }
+ }
+ }
+
+ if (best_ltr < 0) {
+ BOOST_LOG(warning) << "AMF: RFI failed, no valid LTR frame before frame " << first_frame;
+ return false;
+ }
+
+ // Loss feedback arrives after newer frames may already have been submitted.
+ // Those frames can depend transitively on the loss, so invalidate anchors
+ // through the latest submitted frame, matching the NVENC recovery model.
+ const auto effective_last_frame = std::max(last_frame, last_submitted_frame_index);
+ for (int i = 0; i < effective_ltr_slots; i++) {
+ if (ltr_slots_valid[i] && ltr_slot_frame_index[i] >= first_frame && ltr_slot_frame_index[i] <= effective_last_frame) {
+ ltr_slots_valid[i] = false;
+ }
+ }
+
+ last_rfi_ltr_index = best_ltr;
+ rfi_pending = true;
+
+ BOOST_LOG(info) << "AMF: RFI pending, using LTR index " << best_ltr
+ << " (frame " << best_frame << ") for invalidated frames " << first_frame << "-" << effective_last_frame;
+ return true;
+ }
+
+ bool
+ amf_d3d11::set_bitrate(int bitrate_kbps) {
+ if (!encoder || bitrate_kbps <= 0) return false;
+
+ auto bitrate = static_cast(bitrate_kbps) * 1000;
+ // Keep the VBV at ~1 frame of bits on dynamic bitrate changes too, so a raised
+ // bitrate can't start producing FEC-overflowing frames (see configure_encoder).
+ AVRational fps {current_config.framerate > 0 ? current_config.framerate : 60, 1};
+ if (current_config.framerateX100 > 0) {
+ fps = video::framerateX100_to_rational(current_config.framerateX100);
+ }
+ const int64_t vbv_buffer_size = fps.num > 0 ? (bitrate * fps.den / fps.num) : bitrate;
+ bool success = true;
+ auto set_runtime_property = [&](const wchar_t *name, amf_int64 value, const char *label) {
+ const auto property_result = encoder->SetProperty(name, value);
+ if (property_result != AMF_OK) {
+ BOOST_LOG(warning) << "AMF: runtime " << label << " update failed, error: " << property_result;
+ success = false;
+ }
+ };
+
+ if (video_format == 0) {
+ set_runtime_property(AMF_VIDEO_ENCODER_TARGET_BITRATE, bitrate, "target bitrate");
+ if (user_configured_rate_control) {
+ set_runtime_property(AMF_VIDEO_ENCODER_PEAK_BITRATE, bitrate, "peak bitrate");
+ set_runtime_property(AMF_VIDEO_ENCODER_VBV_BUFFER_SIZE, vbv_buffer_size, "VBV buffer");
+ }
+ if (enforce_hrd_enabled) set_runtime_property(AMF_VIDEO_ENCODER_MAX_AU_SIZE, vbv_buffer_size * 4, "maximum access-unit size");
+ }
+ else if (video_format == 1) {
+ set_runtime_property(AMF_VIDEO_ENCODER_HEVC_TARGET_BITRATE, bitrate, "target bitrate");
+ if (user_configured_rate_control) {
+ set_runtime_property(AMF_VIDEO_ENCODER_HEVC_PEAK_BITRATE, bitrate, "peak bitrate");
+ set_runtime_property(AMF_VIDEO_ENCODER_HEVC_VBV_BUFFER_SIZE, vbv_buffer_size, "VBV buffer");
+ }
+ if (enforce_hrd_enabled) set_runtime_property(AMF_VIDEO_ENCODER_HEVC_MAX_AU_SIZE, vbv_buffer_size * 4, "maximum access-unit size");
+ }
+ else {
+ set_runtime_property(AMF_VIDEO_ENCODER_AV1_TARGET_BITRATE, bitrate, "target bitrate");
+ if (user_configured_rate_control) {
+ set_runtime_property(AMF_VIDEO_ENCODER_AV1_PEAK_BITRATE, bitrate, "peak bitrate");
+ set_runtime_property(AMF_VIDEO_ENCODER_AV1_VBV_BUFFER_SIZE, vbv_buffer_size, "VBV buffer");
+ }
+ if (enforce_hrd_enabled) set_runtime_property(AMF_VIDEO_ENCODER_AV1_MAX_COMPRESSED_FRAME_SIZE, vbv_buffer_size * 4, "maximum compressed-frame size");
+ }
+
+ if (success) {
+ current_config.bitrate = bitrate_kbps;
+ BOOST_LOG(info) << "AMF: bitrate dynamically changed to " << bitrate_kbps << " Kbps";
+ }
+ else {
+ BOOST_LOG(warning) << "AMF: live bitrate update was incomplete; encoder rebuild required";
+ }
+ return success;
+ }
+
+ bool
+ amf_d3d11::set_hdr_metadata(const std::optional &metadata) {
+ if (!encoder || !context || video_format < 0 || video_format > 2) return false;
+
+ const wchar_t *property = video_format == 0 ? AMF_VIDEO_ENCODER_INPUT_HDR_METADATA :
+ video_format == 1 ? AMF_VIDEO_ENCODER_HEVC_INPUT_HDR_METADATA :
+ AMF_VIDEO_ENCODER_AV1_INPUT_HDR_METADATA;
+ if (!metadata) {
+ const ::amf::AMFBufferPtr empty_metadata;
+ const auto property_result = encoder->SetProperty(property, empty_metadata);
+ if (property_result != AMF_OK) {
+ BOOST_LOG(warning) << "AMF: failed to clear HDR metadata, error: " << property_result;
+ return false;
+ }
+ return true;
+ }
+
+ {
+ // Create AMFBuffer containing AMFHDRMetadata
+ ::amf::AMFBufferPtr hdr_buffer;
+ auto res = context->AllocBuffer(::amf::AMF_MEMORY_HOST, sizeof(AMFHDRMetadata), &hdr_buffer);
+ if (res != AMF_OK || !hdr_buffer) {
+ BOOST_LOG(warning) << "AMF: failed to allocate HDR metadata buffer";
+ return false;
+ }
+
+ auto *amf_hdr = static_cast(hdr_buffer->GetNative());
+ // Display primaries: both normalized to 50,000
+ amf_hdr->redPrimary[0] = metadata->displayPrimaries[0].x;
+ amf_hdr->redPrimary[1] = metadata->displayPrimaries[0].y;
+ amf_hdr->greenPrimary[0] = metadata->displayPrimaries[1].x;
+ amf_hdr->greenPrimary[1] = metadata->displayPrimaries[1].y;
+ amf_hdr->bluePrimary[0] = metadata->displayPrimaries[2].x;
+ amf_hdr->bluePrimary[1] = metadata->displayPrimaries[2].y;
+ amf_hdr->whitePoint[0] = metadata->whitePoint.x;
+ amf_hdr->whitePoint[1] = metadata->whitePoint.y;
+ // maxMasteringLuminance: AMF expects nits * 10000, SS_HDR_METADATA provides nits
+ amf_hdr->maxMasteringLuminance = static_cast(metadata->maxDisplayLuminance) * 10000;
+ // minMasteringLuminance: both in 1/10000th of a nit
+ amf_hdr->minMasteringLuminance = metadata->minDisplayLuminance;
+ amf_hdr->maxContentLightLevel = metadata->maxContentLightLevel;
+ amf_hdr->maxFrameAverageLightLevel = metadata->maxFrameAverageLightLevel;
+
+ // Set HDR metadata on encoder
+ const auto property_result = encoder->SetProperty(property, hdr_buffer);
+ if (property_result != AMF_OK) {
+ BOOST_LOG(warning) << "AMF: failed to apply HDR metadata, error: " << property_result;
+ return false;
+ }
+
+ BOOST_LOG(info) << "AMF: HDR metadata set (max luminance: " << metadata->maxDisplayLuminance << " nits)";
+ return true;
+ }
+ }
+
+ void *
+ amf_d3d11::get_input_texture() {
+ return input_surface_ring[0].texture.Get();
+ }
+
+ ID3D11Texture2D *
+ amf_d3d11::acquire_input_texture_for_render() {
+ std::unique_lock lock(state_mutex);
+ if (prepared_input_surface_slot) {
+ return input_surface_ring[*prepared_input_surface_slot].texture.Get();
+ }
+
+ auto find_free_slot = [&]() -> std::optional {
+ for (std::size_t offset = 0; offset < active_input_surface_count; ++offset) {
+ const auto slot = (next_input_surface_slot + offset) % active_input_surface_count;
+ if (input_surface_ring[slot].state == input_surface_state_e::free) {
+ return slot;
+ }
+ }
+ return std::nullopt;
+ };
+
+ auto slot = find_free_slot();
+ if (!slot && !output_fatal && active_input_surface_count < input_surface_ring.size()) {
+ // Allocate outside the lock: a multi-MB CreateTexture2D under state_mutex
+ // stalls the pump and AMF's release observers. Expansion only ever runs
+ // on this thread, so re-checking after relock only has to account for
+ // slots the observers freed meanwhile.
+ lock.unlock();
+ auto expansion_texture = create_input_surface_texture();
+ lock.lock();
+ slot = find_free_slot();
+ if (!slot && expansion_texture && !output_fatal &&
+ active_input_surface_count < input_surface_ring.size()) {
+ auto &expansion_slot = input_surface_ring[active_input_surface_count];
+ if (!expansion_slot.texture) {
+ expansion_slot.texture = std::move(expansion_texture);
+ }
+ slot = active_input_surface_count;
+ ++active_input_surface_count;
+ BOOST_LOG(debug) << "AMF: expanded direct-render input pool to " << active_input_surface_count
+ << " surfaces during driver backlog";
+ }
+ }
+ if (!slot) {
+ state_cv.wait_for(lock, lifecycle::driver_wait_budget(current_config.framerate), [&]() {
+ return output_fatal || find_free_slot().has_value();
+ });
+ slot = find_free_slot();
+ }
+ if (!slot || output_fatal) {
+ BOOST_LOG(error) << "AMF: no free direct-render input surface after bounded wait";
+ return nullptr;
+ }
+
+ input_surface_ring[*slot].state = input_surface_state_e::reserved;
+ input_surface_ring[*slot].frame_index = 0;
+ input_surface_ring[*slot].release_notified = false;
+ ++input_surface_ring[*slot].generation;
+ prepared_input_surface_slot = *slot;
+ next_input_surface_slot = (*slot + 1) % active_input_surface_count;
+ return input_surface_ring[*slot].texture.Get();
+ }
+
+ void
+ amf_d3d11::cancel_input_texture_for_render() {
+ std::lock_guard lock(state_mutex);
+ if (!prepared_input_surface_slot) {
+ return;
+ }
+ auto &slot = input_surface_ring[*prepared_input_surface_slot];
+ if (slot.state == input_surface_state_e::reserved) {
+ slot.state = input_surface_state_e::free;
+ slot.frame_index = 0;
+ }
+ prepared_input_surface_slot.reset();
+ state_cv.notify_all();
+ }
+
+ std::unique_ptr
+ create_amf_d3d11(ID3D11Device *d3d_device) {
+ if (!d3d_device) return nullptr;
+
+ auto enc = std::make_unique(d3d_device);
+ return enc;
+ }
+
+} // namespace amf
diff --git a/src/amf/amf_d3d11.h b/src/amf/amf_d3d11.h
new file mode 100644
index 000000000..6bd9a0b2a
--- /dev/null
+++ b/src/amf/amf_d3d11.h
@@ -0,0 +1,236 @@
+/**
+ * @file src/amf/amf_d3d11.h
+ * @brief Declarations for AMF D3D11 encoder.
+ */
+#pragma once
+
+#include "amf_encoder.h"
+#include "amf_lifecycle.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+namespace amf {
+
+ /**
+ * @brief AMF encoder using D3D11 for texture input.
+ */
+ class amf_d3d11: public amf_encoder {
+ public:
+ explicit amf_d3d11(ID3D11Device *d3d_device);
+ ~amf_d3d11();
+
+ bool
+ create_encoder(const amf_config &config,
+ const video::config_t &client_config,
+ const video::sunshine_colorspace_t &colorspace,
+ platf::pix_fmt_e buffer_format) override;
+
+ void
+ destroy_encoder() override;
+
+ amf_encode_result
+ encode_frame(uint64_t frame_index, bool force_idr) override;
+
+ amf_encode_result
+ drain_output(std::chrono::milliseconds timeout) override;
+
+ bool
+ has_output_due() override;
+
+ bool
+ has_completed_output() override;
+
+ bool
+ has_retained_preanalysis_tail() override;
+
+ bool
+ begin_drain() override;
+
+ bool
+ invalidate_ref_frames(uint64_t first_frame, uint64_t last_frame) override;
+
+ bool
+ set_bitrate(int bitrate_kbps) override;
+
+ bool
+ set_hdr_metadata(const std::optional &metadata) override;
+
+ void *
+ get_input_texture() override;
+
+ ID3D11Texture2D *
+ acquire_input_texture_for_render();
+
+ void
+ cancel_input_texture_for_render();
+
+ private:
+ bool
+ init_amf_library();
+
+ bool
+ configure_encoder(const amf_config &config,
+ const video::config_t &client_config,
+ const video::sunshine_colorspace_t &colorspace);
+
+ AMF_SURFACE_FORMAT
+ get_amf_format(platf::pix_fmt_e buffer_format, int bit_depth);
+
+ const wchar_t *
+ get_codec_id();
+
+ amf_encoded_frame
+ extract_encoded_frame(const ::amf::AMFDataPtr &output_data);
+
+ void
+ output_pump(std::stop_token stop_token) noexcept;
+
+ void
+ on_input_surface_released(std::size_t slot_index, uint64_t generation) noexcept;
+
+ bool
+ ensure_input_surface_count(std::size_t count);
+
+ // Safe to call with state_mutex released: device and input_surface_desc are
+ // immutable while the encode thread runs. Pool expansion allocates through
+ // this so a multi-MB CreateTexture2D never stalls the pump or AMF's
+ // release observers behind the lock.
+ Microsoft::WRL::ComPtr
+ create_input_surface_texture();
+
+ ID3D11Device *device = nullptr;
+ ::amf::AMFFactory *factory = nullptr;
+ ::amf::AMFContextPtr context;
+ ::amf::AMFComponentPtr encoder;
+ HMODULE amf_dll = nullptr;
+
+ // The converter renders directly into a reserved pool surface. AMF may retain an
+ // accepted native surface while encoding it, so a slot is recycled exclusively by
+ // AMFSurfaceObserver. The pool starts at the queue/lookahead-aware working depth
+ // and allocates additional slots lazily during a transient driver backlog.
+ static constexpr std::size_t INPUT_SURFACE_RING_SIZE = lifecycle::maximum_input_surface_count;
+ using input_surface_state_e = lifecycle::input_surface_state_e;
+
+ struct input_surface_release_observer_t final: ::amf::AMFSurfaceObserver {
+ amf_d3d11 *owner = nullptr;
+ std::size_t slot_index = 0;
+
+ void AMF_STD_CALL OnSurfaceDataRelease(::amf::AMFSurface *surface) override;
+ };
+ struct input_surface_slot_t: lifecycle::input_surface_state_t {
+ Microsoft::WRL::ComPtr texture;
+ // Bumped on every reservation; each AMF wrapper is stamped with the value
+ // it was created under so a stale OnSurfaceDataRelease from a previous
+ // wrapper can never recycle the slot's next occupant.
+ uint64_t generation = 0;
+ };
+ std::array input_surface_ring;
+ std::array input_surface_release_observers;
+ std::size_t active_input_surface_count = lifecycle::minimum_input_surface_count;
+ std::size_t encoder_input_queue_size = lifecycle::default_amf_input_queue_size;
+ D3D11_TEXTURE2D_DESC input_surface_desc {};
+ std::size_t next_input_surface_slot = 0;
+ std::optional prepared_input_surface_slot;
+ std::optional last_rendered_input_surface_slot;
+
+ // Encoder state
+ video::config_t current_config {};
+ int video_format = 0; // 0=H264, 1=HEVC, 2=AV1
+ AMF_SURFACE_FORMAT surface_format = AMF_SURFACE_NV12;
+ int encode_width = 0;
+ int encode_height = 0;
+ bool rfi_pending = false;
+ uint64_t last_rfi_ltr_index = 0;
+ int max_ltr_frames = 0;
+ bool rfi_enabled = false;
+
+ // Current LTR state for RFI.
+ // Slot 0 is reserved as the IDR baseline (set on every IDR, never overwritten by
+ // periodic marks) so RFI always has a known-good fallback even when every recent
+ // periodic-marked frame was inside a packet-loss window. Slots 1..N-1 form a
+ // sliding window of more recent anchors.
+ static constexpr int MAX_LTR_SLOTS = 4;
+ static constexpr uint64_t LTR_MARK_INTERVAL = 4; // Mark LTR every N frames
+ int effective_ltr_slots = 0; // Clamped to min(max_ltr_frames, MAX_LTR_SLOTS)
+ int current_ltr_slot = 0; // Which LTR slot to mark next
+ std::array ltr_slots_valid {};
+ std::array ltr_slot_frame_index {}; // Frame index when each LTR slot was marked
+
+ // QueryOutput is owned by a dedicated pump. This keeps AMF driver completion work
+ // off Sunshine's latency-critical encode thread and gives input-slot ownership one
+ // synchronization boundary.
+ std::mutex state_mutex;
+ std::condition_variable state_cv;
+ std::jthread output_thread;
+ std::deque completed_outputs;
+ std::unordered_map frame_rfi_flags;
+ uint64_t last_completed_frame_index = 0;
+ uint64_t last_submitted_frame_index = 0;
+ bool output_fatal = false;
+ bool drain_requested = false;
+ bool drain_complete = false;
+ bool output_poll_requested = false;
+ std::size_t active_output_poll_waiters = 0;
+
+ // Input ownership and output production are deliberately tracked separately.
+ // AMF explicitly does not guarantee a one-to-one input/output relationship;
+ // AMFSurfaceObserver owns the first count while QueryOutput owns the totals.
+ std::size_t input_surfaces_in_flight = 0;
+ uint64_t accepted_input_count = 0;
+ uint64_t completed_output_count = 0;
+ std::deque accepted_frame_indices;
+ bool preanalysis_enabled = false;
+ int preanalysis_lookahead_depth = 0;
+ bool query_timeout_supported = false;
+ bool user_configured_rate_control = false;
+ bool enforce_hrd_enabled = false;
+
+ // Statistics feedback state
+ bool statistics_enabled = false;
+ bool psnr_enabled = false;
+ bool ssim_enabled = false;
+
+ // Runtime fault watchdog: count consecutive failures so we can signal
+ // a fatal error to the upper layer (triggering a real reinit) instead
+ // of silently producing no output forever. Threshold is derived from
+ // client framerate in create_encoder() so the watchdog fires after
+ // roughly the same wall-clock time regardless of fps.
+ int consecutive_submit_failures = 0;
+ int consecutive_surface_failures = 0;
+ int consecutive_query_failures = 0;
+ int consecutive_output_failures = 0;
+ // Consecutive submissions whose bounded coalescing target was not reached.
+ int consecutive_catchup_misses = 0;
+ uint64_t catchup_batch_count = 0;
+ int max_consecutive_failures = 60; // Set to ~1s of frames in create_encoder()
+ std::chrono::steady_clock::time_point last_output_progress {};
+ std::chrono::steady_clock::time_point submit_backpressure_started {};
+
+ std::string last_error_string;
+ };
+
+ /**
+ * @brief Create an AMF D3D11 encoder instance.
+ * @param d3d_device The D3D11 device to use.
+ * @return AMF encoder or nullptr on failure.
+ */
+ std::unique_ptr
+ create_amf_d3d11(ID3D11Device *d3d_device);
+
+} // namespace amf
diff --git a/src/amf/amf_encoded_frame.h b/src/amf/amf_encoded_frame.h
new file mode 100644
index 000000000..c2e26e26f
--- /dev/null
+++ b/src/amf/amf_encoded_frame.h
@@ -0,0 +1,39 @@
+/**
+ * @file src/amf/amf_encoded_frame.h
+ * @brief Declarations for AMF encoded frame.
+ */
+#pragma once
+
+#include
+#include
+#include
+#include
+
+namespace amf {
+
+ /**
+ * @brief Encoded frame from AMF encoder.
+ */
+ struct amf_encoded_frame {
+ std::vector data;
+ uint64_t frame_index = 0;
+ bool idr = false;
+ bool after_ref_frame_invalidation = false;
+ bool fatal = false; // Set when encoder is in unrecoverable state (device lost, repeated failures)
+ };
+
+ /**
+ * @brief Result of one native AMF submission attempt.
+ *
+ * Output can contain frames from earlier submissions because AMF is asynchronous.
+ * input_accepted is kept separate so recovery state (IDR/RFI/LTR) is committed only
+ * after the current surface was actually accepted by the encoder.
+ */
+ struct amf_encode_result {
+ std::vector frames;
+ std::optional input_accepted_at;
+ bool input_accepted = false;
+ bool fatal = false;
+ };
+
+} // namespace amf
diff --git a/src/amf/amf_encoder.h b/src/amf/amf_encoder.h
new file mode 100644
index 000000000..f025c8b89
--- /dev/null
+++ b/src/amf/amf_encoder.h
@@ -0,0 +1,118 @@
+/**
+ * @file src/amf/amf_encoder.h
+ * @brief Declarations for standalone AMF encoder interface.
+ */
+#pragma once
+
+#include "amf_config.h"
+#include "amf_encoded_frame.h"
+
+#include "src/platform/common.h"
+#include "src/video.h"
+#include "src/video_colorspace.h"
+
+#include
+
+namespace amf {
+
+ /**
+ * @brief Standalone AMF encoder interface.
+ */
+ class amf_encoder {
+ public:
+ virtual ~amf_encoder() = default;
+
+ /**
+ * @brief Create the encoder.
+ * @param config AMF encoder configuration.
+ * @param client_config Stream configuration requested by the client.
+ * @param colorspace YUV colorspace.
+ * @param buffer_format Platform-agnostic input surface format.
+ * @return `true` on success, `false` on error
+ */
+ virtual bool
+ create_encoder(const amf_config &config,
+ const video::config_t &client_config,
+ const video::sunshine_colorspace_t &colorspace,
+ platf::pix_fmt_e buffer_format) = 0;
+
+ /**
+ * @brief Destroy the encoder.
+ */
+ virtual void
+ destroy_encoder() = 0;
+
+ /**
+ * @brief Encode the next frame using platform-specific input surface.
+ * @param frame_index Unique frame identifier.
+ * @param force_idr Whether to encode frame as forced IDR.
+ * More than one frame may be returned when a transient encoder delay left
+ * completed output queued from an earlier call. Returning the whole ready batch
+ * lets the caller catch up without permanently adding frame-interval latency.
+ * @return Encoded frames in presentation order.
+ */
+ virtual amf_encode_result
+ encode_frame(uint64_t frame_index, bool force_idr) = 0;
+
+ /**
+ * @brief Wait for and return output from already accepted inputs without submitting another frame.
+ * @param timeout Maximum time to wait for output-pump progress.
+ */
+ virtual amf_encode_result
+ drain_output(std::chrono::milliseconds timeout) = 0;
+
+ /** @brief Whether accepted input has output due for delivery or already queued. */
+ virtual bool
+ has_output_due() = 0;
+
+ /** @brief Whether an encoded packet is already queued for immediate delivery. */
+ virtual bool
+ has_completed_output() = 0;
+
+ /** @brief Whether PA retains a final input that needs one future submission. */
+ virtual bool
+ has_retained_preanalysis_tail() = 0;
+
+ /**
+ * @brief Tell AMF that no more input will be submitted and flush delayed output.
+ * @return `true` when the drain request was accepted.
+ *
+ * This is used by input-only sessions, where a PreAnalysis pipeline cannot be
+ * primed with a future live frame.
+ */
+ virtual bool
+ begin_drain() = 0;
+
+ /**
+ * @brief Perform reference frame invalidation (RFI).
+ * @param first_frame First frame index of the invalidation range.
+ * @param last_frame Last frame index of the invalidation range.
+ * @return `true` on success, `false` on error (caller should force IDR).
+ */
+ virtual bool
+ invalidate_ref_frames(uint64_t first_frame, uint64_t last_frame) = 0;
+
+ /**
+ * @brief Set the bitrate for the encoder dynamically.
+ * @param bitrate_kbps Bitrate in kilobits per second.
+ */
+ virtual bool
+ set_bitrate(int bitrate_kbps) = 0;
+
+ /**
+ * @brief Set HDR metadata for the encoder.
+ * @param metadata HDR metadata, or nullopt to disable.
+ * @return `true` when the requested metadata state was applied.
+ */
+ virtual bool
+ set_hdr_metadata(const std::optional &metadata) = 0;
+
+ /**
+ * @brief Get the D3D11 input texture the encoder reads from.
+ * @return Pointer to ID3D11Texture2D, or nullptr.
+ */
+ virtual void *
+ get_input_texture() = 0;
+ };
+
+} // namespace amf
diff --git a/src/amf/amf_lifecycle.h b/src/amf/amf_lifecycle.h
new file mode 100644
index 000000000..347acbbd5
--- /dev/null
+++ b/src/amf/amf_lifecycle.h
@@ -0,0 +1,698 @@
+/**
+ * @file src/amf/amf_lifecycle.h
+ * @brief Small, driver-independent AMF lifecycle primitives.
+ *
+ * These helpers intentionally contain no AMF or D3D types. Production uses them
+ * for input ownership, bounded retry, and teardown; tests can therefore exercise
+ * the same state transitions with a fake AMF implementation.
+ */
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace amf::lifecycle {
+
+ class native_runtime_gate_t {
+ public:
+ template
+ bool begin_initialization_for(const std::chrono::duration &timeout) {
+ std::unique_lock lock(mutex);
+ if (!state_changed.wait_for(lock, timeout, [&]() {
+ return quarantined || (!initialization_in_progress &&
+ pending_teardowns == 0 &&
+ teardowns_in_progress == 0);
+ }) || quarantined) {
+ return false;
+ }
+ initialization_in_progress = true;
+ return true;
+ }
+
+ bool try_begin_initialization() {
+ return begin_initialization_for(std::chrono::steady_clock::duration::zero());
+ }
+
+ bool finish_initialization() {
+ std::lock_guard lock(mutex);
+ const bool publish = initialization_in_progress && !quarantined && teardowns_in_progress == 0;
+ initialization_in_progress = false;
+ state_changed.notify_all();
+ return publish;
+ }
+
+ void cancel_initialization() {
+ std::lock_guard lock(mutex);
+ initialization_in_progress = false;
+ state_changed.notify_all();
+ }
+
+ void quarantine_initialization() {
+ std::lock_guard lock(mutex);
+ quarantined = true;
+ state_changed.notify_all();
+ }
+
+ template
+ bool begin_teardown_until(const std::chrono::time_point &deadline) {
+ std::unique_lock lock(mutex);
+ ++pending_teardowns;
+ const bool available = state_changed.wait_until(lock, deadline, [&]() {
+ return quarantined || (!initialization_in_progress && teardowns_in_progress == 0);
+ });
+ --pending_teardowns;
+ if (!available || quarantined) {
+ state_changed.notify_all();
+ return false;
+ }
+ ++teardowns_in_progress;
+ return true;
+ }
+
+ bool begin_teardown() {
+ return begin_teardown_until(std::chrono::steady_clock::time_point::max());
+ }
+
+ void finish_teardown(bool completed) {
+ std::lock_guard lock(mutex);
+ if (completed && teardowns_in_progress != 0) {
+ --teardowns_in_progress;
+ }
+ if (!completed) {
+ // The owning worker may still be executing vendor code after its
+ // watchdog expires. Keep the teardown fence occupied until process
+ // restart as well as quarantining new initialization; decrementing here
+ // would permit another teardown to overlap the abandoned call.
+ quarantined = true;
+ }
+ state_changed.notify_all();
+ }
+
+ bool is_quarantined() const {
+ std::lock_guard lock(mutex);
+ return quarantined;
+ }
+
+ bool runtime_is_idle() const {
+ std::lock_guard lock(mutex);
+ return !quarantined && !initialization_in_progress &&
+ pending_teardowns == 0 && teardowns_in_progress == 0;
+ }
+
+ bool operation_in_progress() const {
+ std::lock_guard lock(mutex);
+ return initialization_in_progress || pending_teardowns != 0 || teardowns_in_progress != 0;
+ }
+
+ private:
+ mutable std::mutex mutex;
+ std::condition_variable state_changed;
+ bool quarantined = false;
+ bool initialization_in_progress = false;
+ std::size_t pending_teardowns = 0;
+ std::size_t teardowns_in_progress = 0;
+ };
+
+ // A timeout-safe, two-party ownership transfer. The producing worker cannot
+ // exit after publishing until the caller explicitly accepts or abandons the
+ // value. Consequently a timeout always leaves candidate destruction on the
+ // worker, never on the caller through a last-owner future race.
+ template
+ class worker_handoff_t {
+ public:
+ bool publish(T value) {
+ std::unique_lock lock(mutex);
+ if (abandoned) return false;
+ candidate.emplace(std::move(value));
+ ready = true;
+ changed.notify_all();
+ changed.wait(lock, [&]() { return accepted || abandoned; });
+ if (abandoned) {
+ // Explicitly destroy on the producer before it can drop its shared state
+ // reference; otherwise the caller may become the last handoff owner at
+ // the timeout boundary and run the candidate destructor itself.
+ candidate.reset();
+ return false;
+ }
+ return accepted;
+ }
+
+ template
+ std::optional accept_until(
+ const std::chrono::time_point &deadline,
+ Cancelled &&cancelled,
+ bool *was_cancelled = nullptr) {
+ if (was_cancelled) *was_cancelled = false;
+ std::unique_lock lock(mutex);
+ while (!ready && !abandoned) {
+ if (cancelled()) {
+ if (was_cancelled) *was_cancelled = true;
+ abandoned = true;
+ changed.notify_all();
+ return std::nullopt;
+ }
+ if (Clock::now() >= deadline) {
+ abandoned = true;
+ changed.notify_all();
+ return std::nullopt;
+ }
+ const auto poll_interval = std::chrono::duration_cast(
+ std::chrono::milliseconds(50));
+ changed.wait_until(lock, std::min(deadline, Clock::now() + poll_interval));
+ }
+ if (!ready || abandoned) return std::nullopt;
+ auto result = std::move(candidate);
+ candidate.reset();
+ accepted = true;
+ changed.notify_all();
+ return result;
+ }
+
+ private:
+ std::mutex mutex;
+ std::condition_variable changed;
+ std::optional candidate;
+ bool ready = false;
+ bool accepted = false;
+ bool abandoned = false;
+ };
+
+ enum class input_surface_state_e {
+ free,
+ reserved,
+ in_flight
+ };
+
+ struct input_surface_state_t {
+ uint64_t frame_index = 0;
+ input_surface_state_e state = input_surface_state_e::free;
+ bool release_notified = false;
+ };
+
+ struct encoder_selection_policy_t {
+ bool include_legacy = false;
+ bool fail_closed = false;
+ };
+
+ inline constexpr encoder_selection_policy_t encoder_selection_policy(
+ std::string_view requested_encoder) noexcept {
+ // Native AMF and the FFmpeg AMF implementation are separate, explicit
+ // contracts. Automatic probing must never make a native feature/property
+ // failure look successful by selecting the legacy encoder behind the user.
+ return {
+ requested_encoder == "amdvce_legacy",
+ requested_encoder == "amdvce",
+ };
+ }
+
+ inline constexpr int intra_refresh_period_frames = 300;
+ inline constexpr int intra_refresh_duration_frames = intra_refresh_period_frames - 1;
+ inline constexpr int av1_continuous_intra_refresh_mode = 2;
+
+ struct intra_refresh_plan_t {
+ bool enabled = false;
+ std::optional blocks_per_slot;
+ std::optional av1_mode;
+ std::optional av1_cycle_frames;
+ int minimum_reference_frames = 0;
+ bool disable_ltr = false;
+ };
+
+ inline constexpr intra_refresh_plan_t resolve_intra_refresh(
+ bool requested,
+ int video_format,
+ int width,
+ int height) noexcept {
+ if (!requested || width <= 0 || height <= 0) {
+ return {};
+ }
+
+ if (video_format == 2) {
+ // AMF's AV1 stripe count is the number of frames in a complete refresh
+ // cycle. Continuous mode therefore maps directly to NVENC's 300-frame
+ // Xbox-compatible period.
+ return {
+ true,
+ std::nullopt,
+ av1_continuous_intra_refresh_mode,
+ intra_refresh_period_frames,
+ 0,
+ false,
+ };
+ }
+
+ if (video_format != 0 && video_format != 1) {
+ return {};
+ }
+
+ const int block_edge = video_format == 0 ? 16 : 64;
+ const int64_t blocks_wide = (static_cast(width) + block_edge - 1) / block_edge;
+ const int64_t blocks_high = (static_cast(height) + block_edge - 1) / block_edge;
+ const int64_t block_count = blocks_wide * blocks_high;
+ const int64_t refresh_frames = std::min(block_count, intra_refresh_duration_frames);
+ const int blocks_per_slot = static_cast((block_count + refresh_frames - 1) / refresh_frames);
+
+ return {
+ true,
+ blocks_per_slot,
+ std::nullopt,
+ std::nullopt,
+ video_format == 0 ? 2 : 0,
+ video_format == 0,
+ };
+ }
+
+ // The client's reference-frame limit and the intra-refresh minimum must be
+ // combined identically at configure time and at post-Init verification, or
+ // the readback contract rejects its own raise. nullopt == leave the driver
+ // default untouched (no limit requested and no minimum required).
+ inline constexpr std::optional effective_reference_frame_limit(
+ int client_num_ref_frames,
+ int intra_refresh_minimum_reference_frames) noexcept {
+ if (client_num_ref_frames <= 0) {
+ if (intra_refresh_minimum_reference_frames <= 0) {
+ return std::nullopt;
+ }
+ // "No limit" from the client still needs the intra-refresh floor applied,
+ // otherwise H.264 intra refresh rides on a driver-default single reference.
+ return intra_refresh_minimum_reference_frames;
+ }
+ return std::max(client_num_ref_frames, intra_refresh_minimum_reference_frames);
+ }
+
+ inline bool rate_control_requires_preanalysis(int mode) noexcept {
+ // AMF uses these values consistently for AVC, HEVC, and AV1.
+ return mode >= 4 && mode <= 6; // QVBR, HQVBR, HQCBR
+ }
+
+ inline bool rate_control_supports_adaptive_quantization(const std::optional &rate_control) noexcept {
+ // All three AMF encoders use zero for CONSTANT_QP. AMD documents VBAQ/CAQ
+ // as incompatible with CQP, so an enabled-by-default AQ setting must not turn
+ // an otherwise valid CQP request into a driver-dependent configuration.
+ return !rate_control || *rate_control != 0;
+ }
+
+ // AMD documents a lookahead depth of one for the ultra-low-latency usage
+ // preset. Keep native streaming on that bounded pipeline instead of inheriting
+ // the 11-frame default used by transcoding/high-quality presets.
+ inline constexpr int low_latency_preanalysis_lookahead_depth = 1;
+
+ // Native encoders can retain several inputs before the first output even
+ // without B-frames or PreAnalysis. Three surfaces proved too small on current
+ // Radeon drivers: once every wrapper was retained, the host could no longer
+ // submit the input needed to advance the VCN queue. Keep two transit surfaces
+ // beyond AMF's configured queue and additional room for PA lookahead. Production
+ // can grow lazily to AMF's documented maximum queue plus that transit headroom.
+ inline constexpr std::size_t minimum_input_surface_count = 4;
+ inline constexpr std::size_t input_surface_transit_count = 2;
+ inline constexpr std::size_t default_amf_input_queue_size = 16;
+ inline constexpr std::size_t maximum_amf_input_queue_size = 32;
+ inline constexpr std::size_t maximum_input_surface_count =
+ maximum_amf_input_queue_size + input_surface_transit_count;
+
+ inline constexpr std::size_t input_surface_count_for_lookahead(int lookahead_depth) noexcept {
+ const auto requested = minimum_input_surface_count +
+ static_cast(std::max(0, lookahead_depth)) * 2;
+ return std::clamp(requested, minimum_input_surface_count, maximum_input_surface_count);
+ }
+
+ inline constexpr std::size_t initial_input_surface_count(int lookahead_depth) noexcept {
+ // Start with only the lookahead/transit working set. The native encoder grows
+ // this pool one texture at a time if a driver actually retains more inputs;
+ // eagerly mirroring a 16/32-frame AMF queue wastes hundreds of MiB at 4K.
+ return input_surface_count_for_lookahead(lookahead_depth);
+ }
+
+ inline constexpr bool driver_submit_capacity_available(
+ std::size_t input_surfaces_in_flight,
+ std::size_t encoder_input_queue_size) noexcept {
+ // Current AMF runtimes can retain one wrapper beyond the configured queue.
+ // Do not enter SubmitInput again once ownership has already crossed that
+ // boundary: on a stalled PA pipeline the call itself can block for tens of
+ // seconds and cannot be bounded by the caller's probe deadline.
+ return input_surfaces_in_flight <= encoder_input_queue_size;
+ }
+
+ inline constexpr bool saturation_wait_should_finish(bool output_fatal,
+ bool capacity_available) noexcept {
+ return output_fatal || capacity_available;
+ }
+
+ struct preanalysis_plan_t {
+ bool enabled = false;
+ int lookahead_depth = 0;
+ bool enabled_for_rate_control = false;
+ };
+
+ inline preanalysis_plan_t resolve_preanalysis(const std::optional &rate_control,
+ const std::optional &explicit_preanalysis) noexcept {
+ const bool required_by_rate_control = rate_control && rate_control_requires_preanalysis(*rate_control);
+ const bool explicitly_enabled = explicit_preanalysis && *explicit_preanalysis != 0;
+ const bool enabled = required_by_rate_control || explicitly_enabled;
+ return {
+ enabled,
+ enabled ? low_latency_preanalysis_lookahead_depth : 0,
+ required_by_rate_control,
+ };
+ }
+
+ // AMD's encoder sample requires this exact dependency order: rate control
+ // first, then PA enable, then PA's dynamic lookahead properties.
+ template
+ bool apply_rate_control_and_preanalysis(
+ const std::optional &rate_control,
+ bool configure_preanalysis,
+ const preanalysis_plan_t &plan,
+ int lookahead_depth,
+ ApplyRateControl &&apply_rate_control,
+ ApplyPreanalysis &&apply_preanalysis,
+ ApplyLookahead &&apply_lookahead) {
+ if (rate_control && !apply_rate_control(*rate_control)) {
+ return false;
+ }
+ if (configure_preanalysis && !apply_preanalysis(plan.enabled)) {
+ return false;
+ }
+ if (plan.enabled && !apply_lookahead(std::max(1, lookahead_depth))) {
+ return false;
+ }
+ return true;
+ }
+
+ // A lookahead encoder can only be expected to emit once it has accepted more
+ // inputs than it keeps for future-frame analysis. This gates waits/watchdogs;
+ // it does not prevent QueryOutput from returning earlier if a driver can do so.
+ inline bool delayed_output_is_expected(uint64_t accepted_input_count, int lookahead_depth) noexcept {
+ return accepted_input_count > static_cast(std::max(0, lookahead_depth));
+ }
+
+ inline constexpr bool should_disarm_output_poll(uint64_t queried_through_input,
+ uint64_t accepted_input_count,
+ bool drain_requested,
+ std::size_t active_poll_waiters) noexcept {
+ // A no-data QueryOutput result only describes that call. It does not guarantee
+ // that an already-submitted hardware job cannot complete a moment later. A
+ // bounded encode-side waiter therefore owns a polling lease; disarming
+ // underneath that waiter strands the completion until the next input.
+ return !drain_requested && active_poll_waiters == 0 &&
+ queried_through_input == accepted_input_count;
+ }
+
+ // Keep submission coalescing within one negotiated frame period. Sparse/static
+ // delivery gets its longer, image-interruptible grace in the outer capture loop;
+ // carrying that grace into encode_frame lets a fresh capture inherit a 32 ms wait.
+ inline constexpr std::chrono::milliseconds output_coalesce_budget(int framerate) noexcept {
+ const auto frame_period = framerate > 0 ?
+ std::chrono::milliseconds((1000 + framerate - 1) / framerate) :
+ std::chrono::milliseconds(17);
+ return std::clamp(
+ frame_period > std::chrono::milliseconds(1) ?
+ frame_period - std::chrono::milliseconds(1) :
+ std::chrono::milliseconds(1),
+ std::chrono::milliseconds(1),
+ std::chrono::milliseconds(32));
+ }
+
+ inline constexpr std::chrono::milliseconds driver_wait_budget(int framerate) noexcept {
+ return std::min(std::chrono::milliseconds(20), output_coalesce_budget(framerate));
+ }
+
+ inline constexpr bool output_delivery_is_due(uint64_t accepted_input_count,
+ uint64_t completed_output_count,
+ int lookahead_depth,
+ bool completed_output_queued) noexcept {
+ const auto delay = static_cast(std::max(0, lookahead_depth));
+ const auto expected_output_count = accepted_input_count > delay ?
+ accepted_input_count - delay :
+ 0;
+ return completed_output_queued || completed_output_count < expected_output_count;
+ }
+
+ inline constexpr bool retained_preanalysis_tail_exists(uint64_t accepted_input_count,
+ uint64_t completed_output_count,
+ int lookahead_depth) noexcept {
+ const auto depth = static_cast(std::max(0, lookahead_depth));
+ if (depth == 0 || accepted_input_count <= completed_output_count) {
+ return false;
+ }
+ return accepted_input_count - completed_output_count <= depth;
+ }
+
+ inline constexpr bool preanalysis_tail_flush_is_due(bool retained_tail_exists,
+ bool suppressed_until_fresh_conversion) noexcept {
+ return retained_tail_exists && !suppressed_until_fresh_conversion;
+ }
+
+ inline bool submit_backpressure_requires_reinit(
+ int consecutive_exhaustions,
+ int failure_threshold,
+ bool sequence_start_known,
+ std::chrono::steady_clock::duration time_since_sequence_start) noexcept {
+ return consecutive_exhaustions >= std::max(1, failure_threshold) ||
+ (sequence_start_known && time_since_sequence_start >= std::chrono::seconds(2));
+ }
+
+ inline constexpr bool output_coalesce_target_reached(uint64_t required_frame_index,
+ uint64_t completed_before_submission,
+ uint64_t completed_after_submission,
+ uint64_t last_completed_frame_index) noexcept {
+ // encode_frame drains the completed queue before taking its completion-count
+ // snapshot. The packet required by this submission may therefore already be
+ // in the caller's result even though the count cannot increase again. Frame
+ // generation, rather than count movement, is the authoritative condition.
+ if (last_completed_frame_index >= required_frame_index) return true;
+
+ if (completed_after_submission <= completed_before_submission) return false;
+
+ // Require progress from the generation that became due with this input. A PA
+ // completion observed during surface preparation may be newer than the count
+ // snapshot yet still belong to an older generation; treating it as catch-up
+ // silently adds another frame of latency.
+ return false;
+ }
+
+ inline std::optional record_accepted_frame(
+ std::deque &accepted_frame_indices,
+ uint64_t frame_index,
+ int lookahead_depth) {
+ const auto retained_depth = static_cast(std::max(0, lookahead_depth));
+ accepted_frame_indices.push_back(frame_index);
+ while (accepted_frame_indices.size() > retained_depth + 1) {
+ accepted_frame_indices.pop_front();
+ }
+ if (accepted_frame_indices.size() <= retained_depth) {
+ return std::nullopt;
+ }
+ return accepted_frame_indices.front();
+ }
+
+ inline std::optional resolve_h264_cabac(int coder_mode) noexcept {
+ if (coder_mode == 1) return 1; // CABAC
+ if (coder_mode == 2) return 0; // CAVLC
+ return std::nullopt; // auto: preserve the driver default
+ }
+
+ template
+ std::optional select_repeat_surface(
+ const std::array &slots,
+ std::size_t source_slot,
+ std::size_t next_slot,
+ std::size_t active_slot_count = SlotCount) noexcept {
+ static_assert(SlotCount > 0);
+ const auto bounded_slot_count = std::clamp(active_slot_count, 1, SlotCount);
+ if (source_slot >= bounded_slot_count) {
+ return std::nullopt;
+ }
+ if (slots[source_slot].state == input_surface_state_e::free) {
+ return source_slot;
+ }
+ for (std::size_t offset = 0; offset < bounded_slot_count; ++offset) {
+ const auto candidate = (next_slot + offset) % bounded_slot_count;
+ if (slots[candidate].state == input_surface_state_e::free) {
+ return candidate;
+ }
+ }
+ return std::nullopt;
+ }
+
+ // Returns true when the slot became reusable.
+ inline bool on_surface_released(input_surface_state_t &slot) noexcept {
+ slot.release_notified = true;
+ if (slot.state != input_surface_state_e::in_flight) {
+ return false;
+ }
+
+ slot.state = input_surface_state_e::free;
+ slot.frame_index = 0;
+ return true;
+ }
+
+ // Commit ownership only after SubmitInput accepted the surface. If AMF
+ // synchronously released it from inside SubmitInput, the observer has already
+ // set release_notified and the slot is immediately reusable.
+ inline bool on_input_accepted(input_surface_state_t &slot, uint64_t frame_index) noexcept {
+ slot.state = input_surface_state_e::in_flight;
+ slot.frame_index = frame_index;
+ if (!slot.release_notified) {
+ return false;
+ }
+
+ slot.state = input_surface_state_e::free;
+ slot.frame_index = 0;
+ return true;
+ }
+
+ template
+ void commit_recovery_state(bool input_accepted,
+ int effective_slots,
+ bool reset_cache,
+ int slot_to_preserve,
+ int slot_to_mark,
+ int next_mark_slot,
+ bool consume_pending_rfi,
+ bool frame_after_rfi,
+ uint64_t frame_index,
+ std::array &slots_valid,
+ std::array &slot_frame_indices,
+ int ¤t_mark_slot,
+ bool &rfi_pending,
+ FlagRecoveredFrame &&flag_recovered_frame) {
+ if (!input_accepted) {
+ return;
+ }
+
+ const int bounded_slots = std::min(effective_slots, static_cast(SlotCount));
+ if (reset_cache) {
+ for (int slot = 0; slot < bounded_slots; ++slot) {
+ if (slot != slot_to_preserve) {
+ slots_valid[slot] = false;
+ slot_frame_indices[slot] = 0;
+ }
+ }
+ }
+ if (slot_to_mark >= 0 && slot_to_mark < bounded_slots) {
+ slots_valid[slot_to_mark] = true;
+ slot_frame_indices[slot_to_mark] = frame_index;
+ current_mark_slot = next_mark_slot;
+ }
+ if (consume_pending_rfi) {
+ rfi_pending = false;
+ }
+ if (frame_after_rfi) {
+ flag_recovered_frame(frame_index);
+ }
+ }
+
+ template
+ auto submit_with_bounded_retry(Submit &&submit,
+ WaitForProgress &&wait_for_progress,
+ Retryable &&retryable,
+ int max_retries) {
+ auto result = submit();
+ for (int retry = 0; retry < max_retries && retryable(result); ++retry) {
+ // A true result asks us to abort (fatal state/shutdown). The caller owns
+ // policy for the still-retryable result.
+ if (wait_for_progress()) {
+ break;
+ }
+ result = submit();
+ }
+ return result;
+ }
+
+ struct smart_access_video_plan_t {
+ std::optional enabled;
+ bool disabled_for_low_latency = false;
+ };
+
+ // Smart Access Video distributes media work for throughput; the encoder-level
+ // LOWLATENCY_MODE override selects a different, aggressive driver path. A
+ // confirmed HEVC 4K120 HDR run with both enabled wedged AMF, triggered two
+ // LiveKernelEvent 141 GPU resets, and faulted amdxx64.dll. Keep either explicit
+ // setting independently, but resolve their unsafe conjunction in favor of the
+ // streaming-oriented low-latency setting.
+ inline smart_access_video_plan_t resolve_smart_access_video(
+ std::optional smart_access_video,
+ std::optional low_latency_mode) {
+ if (smart_access_video && *smart_access_video &&
+ low_latency_mode && *low_latency_mode) {
+ return {false, true};
+ }
+ return {smart_access_video, false};
+ }
+
+ struct concurrent_session_feature_plan_t {
+ std::optional low_latency_mode;
+ std::optional high_motion_quality_boost;
+ bool overrides_suppressed = false;
+ };
+
+ // Current Radeon drivers can wedge VCN when a second high-rate session enables
+ // either of these optional driver paths. The AMF ultra-low-latency usage preset
+ // remains active without them, so preserve a usable native session and suppress
+ // only explicitly enabled overrides on concurrent sessions. Explicit false and
+ // automatic settings are preserved exactly.
+ inline concurrent_session_feature_plan_t resolve_concurrent_session_features(
+ int active_encoder_count,
+ std::optional low_latency_mode,
+ std::optional high_motion_quality_boost) noexcept {
+ const bool suppress_low_latency = active_encoder_count > 1 &&
+ low_latency_mode && *low_latency_mode;
+ const bool suppress_high_motion = active_encoder_count > 1 &&
+ high_motion_quality_boost && *high_motion_quality_boost;
+ if (suppress_low_latency) {
+ low_latency_mode = std::nullopt;
+ }
+ if (suppress_high_motion) {
+ high_motion_quality_boost = std::nullopt;
+ }
+ return {
+ low_latency_mode,
+ high_motion_quality_boost,
+ suppress_low_latency || suppress_high_motion,
+ };
+ }
+
+ // Executes potentially driver-blocking cleanup away from the caller. A timed
+ // out worker deliberately retains ownership of its resources until it exits;
+ // abandoning that session is safer than wedging the host process.
+ template
+ bool run_with_timeout(Work &&work, std::chrono::duration timeout) {
+ std::promise done;
+ auto done_future = done.get_future();
+ std::thread worker {
+ [work = std::forward(work), done = std::move(done)]() mutable {
+ try {
+ work();
+ } catch (...) {
+ // Cleanup is best-effort; completion still releases the waiter.
+ }
+ try {
+ done.set_value();
+ } catch (...) {
+ // The waiter may already have abandoned the shared state.
+ }
+ }
+ };
+
+ if (done_future.wait_for(timeout) == std::future_status::ready) {
+ worker.join();
+ return true;
+ }
+
+ worker.detach();
+ return false;
+ }
+
+} // namespace amf::lifecycle
diff --git a/src/config.cpp b/src/config.cpp
index 4876d05d0..8858e1fd6 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -171,6 +171,15 @@ namespace config {
#define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CBR 1
#define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR 2
#define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR 3
+ #define AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_QUALITY_VBR 4
+ #define AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_HIGH_QUALITY_VBR 5
+ #define AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_HIGH_QUALITY_CBR 6
+ #define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_QUALITY_VBR 4
+ #define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_HIGH_QUALITY_VBR 5
+ #define AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_HIGH_QUALITY_CBR 6
+ #define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_QUALITY_VBR 4
+ #define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_HIGH_QUALITY_VBR 5
+ #define AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_HIGH_QUALITY_CBR 6
#define AMF_VIDEO_ENCODER_AV1_USAGE_TRANSCODING 0
#define AMF_VIDEO_ENCODER_AV1_USAGE_LOW_LATENCY 1
#define AMF_VIDEO_ENCODER_AV1_USAGE_ULTRA_LOW_LATENCY 2
@@ -213,21 +222,30 @@ namespace config {
cbr = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_CBR, ///< CBR
cqp = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_CONSTANT_QP, ///< CQP
vbr_latency = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR, ///< VBR with latency constraints
- vbr_peak = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR ///< VBR with peak constraints
+ vbr_peak = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR, ///< VBR with peak constraints
+ qvbr = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_QUALITY_VBR, ///< Quality-defined VBR
+ hqvbr = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_HIGH_QUALITY_VBR, ///< High quality VBR
+ hqcbr = AMF_VIDEO_ENCODER_AV1_RATE_CONTROL_METHOD_HIGH_QUALITY_CBR ///< High quality CBR
};
enum class rc_hevc_e : int {
cbr = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CBR, ///< CBR
cqp = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_CONSTANT_QP, ///< CQP
vbr_latency = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR, ///< VBR with latency constraints
- vbr_peak = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR ///< VBR with peak constraints
+ vbr_peak = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR, ///< VBR with peak constraints
+ qvbr = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_QUALITY_VBR, ///< Quality-defined VBR
+ hqvbr = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_HIGH_QUALITY_VBR, ///< High quality VBR
+ hqcbr = AMF_VIDEO_ENCODER_HEVC_RATE_CONTROL_METHOD_HIGH_QUALITY_CBR ///< High quality CBR
};
enum class rc_h264_e : int {
cbr = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CBR, ///< CBR
cqp = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_CONSTANT_QP, ///< CQP
vbr_latency = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_LATENCY_CONSTRAINED_VBR, ///< VBR with latency constraints
- vbr_peak = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR ///< VBR with peak constraints
+ vbr_peak = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_PEAK_CONSTRAINED_VBR, ///< VBR with peak constraints
+ qvbr = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_QUALITY_VBR, ///< Quality-defined VBR
+ hqvbr = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_HIGH_QUALITY_VBR, ///< High quality VBR
+ hqcbr = AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_HIGH_QUALITY_CBR ///< High quality CBR
};
enum class usage_av1_e : int {
@@ -262,6 +280,9 @@ namespace config {
template
::std::optional quality_from_view(const ::std::string_view &quality_type, const ::std::optional(&original)) {
+ if (quality_type == "auto"sv) {
+ return ::std::nullopt;
+ }
#define _CONVERT_(x) \
if (quality_type == #x##sv) \
return (int) T::x
@@ -281,6 +302,9 @@ namespace config {
_CONVERT_(cqp);
_CONVERT_(vbr_latency);
_CONVERT_(vbr_peak);
+ _CONVERT_(qvbr);
+ _CONVERT_(hqvbr);
+ _CONVERT_(hqcbr);
#undef _CONVERT_
return original;
}
@@ -299,6 +323,46 @@ namespace config {
return original;
}
+ // Tri-state knobs: explicit boolean spellings force the AMF property on/off;
+ // anything else (including "auto") returns nullopt so the usage preset's
+ // driver default is left untouched. Accept the legacy bool spellings because
+ // older configs may contain amd_vbaq=true/false rather than enabled/disabled.
+ ::std::optional tristate_from_view(const ::std::string_view &value) {
+ if (value == "true"sv || value == "yes"sv || value == "enable"sv ||
+ value == "enabled"sv || value == "on"sv || value == "1"sv) {
+ return 1;
+ }
+ if (value == "false"sv || value == "no"sv || value == "disable"sv ||
+ value == "disabled"sv || value == "off"sv || value == "0"sv) {
+ return 0;
+ }
+ if (!value.empty() && value != "auto"sv) {
+ BOOST_LOG(warning) << "Unknown tri-state value ["sv << value << "], falling back to auto"sv;
+ }
+ return ::std::nullopt;
+ }
+
+ // AV1 encoding-latency mode. "auto" leaves the driver default; the rest map to
+ // AMF_VIDEO_ENCODER_AV1_ENCODING_LATENCY_MODE_ENUM values.
+ ::std::optional av1_latency_from_view(const ::std::string_view &value) {
+ if (value == "none"sv) {
+ return 0;
+ }
+ if (value == "power_saving"sv) {
+ return 1;
+ }
+ if (value == "realtime"sv) {
+ return 2;
+ }
+ if (value == "lowest"sv) {
+ return 3;
+ }
+ if (!value.empty() && value != "auto"sv) {
+ BOOST_LOG(warning) << "Unknown amd_av1_latency_mode value ["sv << value << "], falling back to auto"sv;
+ }
+ return ::std::nullopt;
+ }
+
int coder_from_view(const ::std::string_view &coder) {
if (coder == "auto"sv) {
return _auto;
@@ -790,6 +854,7 @@ namespace config {
(int) amd::rc_h264_e::vbr_latency, // rate control (h264)
(int) amd::rc_hevc_e::vbr_latency, // rate control (hevc)
(int) amd::rc_av1_e::vbr_latency, // rate control (av1)
+ std::nullopt, // qvbr_quality_level (0/unset = encoder default)
0, // enforce_hrd
(int) amd::quality_h264_e::balanced, // quality (h264)
(int) amd::quality_hevc_e::balanced, // quality (hevc)
@@ -797,6 +862,13 @@ namespace config {
0, // preanalysis
1, // vbaq
(int) amd::coder_e::_auto, // coder
+ 0, // ltr_frames (native AMF; 0 = off)
+ 0, // input_queue_size (native AMF; 0 = leave driver queue unset/auto)
+ std::nullopt, // smart_access_video (auto)
+ std::nullopt, // lowlatency_mode (auto)
+ std::nullopt, // high_motion_quality_boost (auto)
+ std::nullopt, // av1_screen_content (auto)
+ std::nullopt, // av1_latency_mode (auto)
}, // amd
{
@@ -1638,6 +1710,20 @@ namespace config {
video.amd.amd_rc_av1 = amd::rc_from_view(rc, video.amd.amd_rc_av1);
}
+ // Only forwarded to the encoder when the 'qvbr' rate control method is selected.
+ // 0 (or an unset value) leaves the level at the encoder default.
+ int qvbr_quality_level = 0;
+ int_f(vars, "amd_qvbr_quality_level", qvbr_quality_level);
+ if (qvbr_quality_level > 0) {
+ if (qvbr_quality_level > 51) {
+ BOOST_LOG(warning) << "config: amd_qvbr_quality_level must be between 1 and 51, ignoring value: "sv << qvbr_quality_level;
+ } else {
+ video.amd.amd_qvbr_quality_level = qvbr_quality_level;
+ }
+ } else if (qvbr_quality_level < 0) {
+ BOOST_LOG(warning) << "config: amd_qvbr_quality_level must be between 1 and 51, ignoring value: "sv << qvbr_quality_level;
+ }
+
std::string usage;
string_f(vars, "amd_usage", usage);
if (!usage.empty()) {
@@ -1647,9 +1733,29 @@ namespace config {
}
bool_f(vars, "amd_preanalysis", (bool &) video.amd.amd_preanalysis);
- bool_f(vars, "amd_vbaq", (bool &) video.amd.amd_vbaq);
+ int_f(vars, "amd_vbaq", video.amd.amd_vbaq, amd::tristate_from_view);
bool_f(vars, "amd_enforce_hrd", (bool &) video.amd.amd_enforce_hrd);
+ // Native AMF encoder (amdvce) tuning knobs.
+ int_f(vars, "amd_ltr_frames", video.amd.amd_ltr_frames);
+ if (video.amd.amd_ltr_frames < 0 || video.amd.amd_ltr_frames > 2) {
+ BOOST_LOG(warning) << "config: amd_ltr_frames must be between 0 and 2, clamping: "sv << video.amd.amd_ltr_frames;
+ video.amd.amd_ltr_frames = std::clamp(video.amd.amd_ltr_frames, 0, 2);
+ }
+ int_f(vars, "amd_input_queue_size", video.amd.amd_input_queue_size);
+ if (video.amd.amd_input_queue_size < 0 || video.amd.amd_input_queue_size > 32) {
+ BOOST_LOG(warning) << "config: amd_input_queue_size must be between 0 and 32, clamping: "sv << video.amd.amd_input_queue_size;
+ video.amd.amd_input_queue_size = std::clamp(video.amd.amd_input_queue_size, 0, 32);
+ }
+
+ // Curated opt-in native-AMF feature knobs. Default "auto" leaves the AMF
+ // driver default untouched, so none of these change behavior unless enabled.
+ int_f(vars, "amd_smart_access_video", video.amd.amd_smart_access_video, amd::tristate_from_view);
+ int_f(vars, "amd_lowlatency_mode", video.amd.amd_lowlatency_mode, amd::tristate_from_view);
+ int_f(vars, "amd_high_motion_quality_boost", video.amd.amd_high_motion_quality_boost, amd::tristate_from_view);
+ int_f(vars, "amd_av1_screen_content", video.amd.amd_av1_screen_content, amd::tristate_from_view);
+ int_f(vars, "amd_av1_latency_mode", video.amd.amd_av1_latency_mode, amd::av1_latency_from_view);
+
int_f(vars, "vt_coder", video.vt.vt_coder, vt::coder_from_view);
int_f(vars, "vt_software", video.vt.vt_allow_sw, vt::allow_software_from_view);
int_f(vars, "vt_software", video.vt.vt_require_sw, vt::force_software_from_view);
@@ -2410,11 +2516,19 @@ namespace config {
"qsv_slow_hevc",
"amd_usage",
"amd_rc",
+ "amd_qvbr_quality_level",
"amd_enforce_hrd",
"amd_quality",
"amd_preanalysis",
"amd_vbaq",
"amd_coder",
+ "amd_ltr_frames",
+ "amd_input_queue_size",
+ "amd_smart_access_video",
+ "amd_lowlatency_mode",
+ "amd_high_motion_quality_boost",
+ "amd_av1_screen_content",
+ "amd_av1_latency_mode",
"vt_coder",
"vt_software",
"vt_realtime",
diff --git a/src/config.h b/src/config.h
index 734ca68d7..f1ffcd42b 100644
--- a/src/config.h
+++ b/src/config.h
@@ -97,13 +97,25 @@ namespace config {
std::optional amd_rc_h264;
std::optional amd_rc_hevc;
std::optional amd_rc_av1;
+ std::optional amd_qvbr_quality_level;
std::optional amd_enforce_hrd;
- std::optional amd_quality_h264;
- std::optional amd_quality_hevc;
- std::optional amd_quality_av1;
+ std::optional amd_quality_h264; // nullopt = follow usage-preset default
+ std::optional amd_quality_hevc; // nullopt = follow usage-preset default
+ std::optional amd_quality_av1; // nullopt = follow usage-preset default
std::optional amd_preanalysis;
- std::optional amd_vbaq;
+ std::optional amd_vbaq; // nullopt = follow usage-preset default
int amd_coder;
+ // Native AMF encoder (amdvce) tuning knobs.
+ int amd_ltr_frames; // Long-term reference frames for RFI (0 = off)
+ int amd_input_queue_size; // AMF input queue depth (0 = driver default)
+ // Curated tri-state native-AMF feature knobs. nullopt (auto) leaves the
+ // AMF driver default untouched; 1 forces the property on and 0 forces it
+ // off — both explicit values are applied and read back like any other.
+ std::optional amd_smart_access_video; // Multi-VCN encode (Smart Access Video): 1=on, 0=off
+ std::optional amd_lowlatency_mode; // AMF LOWLATENCY_MODE (H.264/HEVC): 1=on, 0=off
+ std::optional amd_high_motion_quality_boost; // High-motion quality boost: 1=on, 0=off
+ std::optional amd_av1_screen_content; // AV1 screen-content tools: 1=on, 0=off
+ std::optional amd_av1_latency_mode; // AV1 encoding-latency mode (0-3)
} amd;
struct {
diff --git a/src/platform/common.h b/src/platform/common.h
index acd6ca7d5..3ac9219b9 100644
--- a/src/platform/common.h
+++ b/src/platform/common.h
@@ -48,7 +48,13 @@ namespace nvenc {
class nvenc_base;
}
+namespace amf {
+ class amf_encoder;
+}
+
namespace platf {
+ class display_t;
+
// Limited by bits in activeGamepadMask
constexpr auto MAX_GAMEPADS = 16;
@@ -399,6 +405,24 @@ namespace platf {
void *data {};
AVFrame *frame {};
+ virtual bool initialize_hardware_device() {
+ return true;
+ }
+
+ virtual std::shared_ptr release_display_lease_for_initialization() {
+ return {};
+ }
+
+ virtual void restore_display_lease_after_initialization(std::shared_ptr display) {
+ (void) display;
+ }
+
+ virtual bool is_codec_supported(std::string_view name, const video::config_t &config) {
+ (void) name;
+ (void) config;
+ return true;
+ }
+
int convert(platf::img_t &img) override {
return -1;
}
@@ -442,6 +466,32 @@ namespace platf {
nvenc::nvenc_base *nvenc = nullptr;
};
+ struct amf_encode_device_t: encode_device_t {
+ // Native backends prepare only non-driver state on the capture thread.
+ // D3D/AMF construction is invoked by the bounded initialization worker.
+ virtual bool initialize_hardware_device() {
+ return true;
+ }
+ virtual bool init_encoder(const video::config_t &client_config, const video::sunshine_colorspace_t &colorspace) = 0;
+ virtual bool finish_encoder_initialization(const video::config_t &client_config, const video::sunshine_colorspace_t &colorspace) {
+ (void) client_config;
+ (void) colorspace;
+ return true;
+ }
+
+ // Native AMF initialization is watchdog-bounded. A timed-out worker must not
+ // retain the capture display generation forever; Windows detaches this lease
+ // while Init runs and restores it only after successful ownership transfer.
+ virtual std::shared_ptr release_display_lease_for_initialization() {
+ return {};
+ }
+ virtual void restore_display_lease_after_initialization(std::shared_ptr display) {
+ (void) display;
+ }
+
+ amf::amf_encoder *amf = nullptr;
+ };
+
enum class capture_e : int {
ok, ///< Success
reinit, ///< Need to reinitialize
@@ -505,10 +555,20 @@ namespace platf {
return nullptr;
}
+ // Windows legacy AMF alone needs D3D creation deferred into its watchdog.
+ // Other AVCodec backends retain their established synchronous behavior.
+ virtual std::unique_ptr make_deferred_avcodec_encode_device(pix_fmt_e pix_fmt) {
+ return nullptr;
+ }
+
virtual std::unique_ptr make_nvenc_encode_device(pix_fmt_e pix_fmt) {
return nullptr;
}
+ virtual std::unique_ptr make_amf_encode_device(pix_fmt_e pix_fmt) {
+ return nullptr;
+ }
+
virtual bool is_hdr() {
return false;
}
diff --git a/src/platform/windows/display.h b/src/platform/windows/display.h
index b41fa2116..6c65ebb36 100644
--- a/src/platform/windows/display.h
+++ b/src/platform/windows/display.h
@@ -306,8 +306,12 @@ namespace platf::dxgi {
std::unique_ptr make_avcodec_encode_device(pix_fmt_e pix_fmt) override;
+ std::unique_ptr make_deferred_avcodec_encode_device(pix_fmt_e pix_fmt) override;
+
std::unique_ptr make_nvenc_encode_device(pix_fmt_e pix_fmt) override;
+ std::unique_ptr make_amf_encode_device(pix_fmt_e pix_fmt) override;
+
std::atomic next_image_id;
};
diff --git a/src/platform/windows/display_vram.cpp b/src/platform/windows/display_vram.cpp
index e195c2a55..f910fa6bf 100644
--- a/src/platform/windows/display_vram.cpp
+++ b/src/platform/windows/display_vram.cpp
@@ -4,6 +4,7 @@
*/
// standard includes
#include
+#include
#include
#include
#include
@@ -29,6 +30,7 @@ extern "C" {
#include "nv_truehdr.h"
#include "rtx_hdr_runtime.h"
#endif
+#include "src/amf/amf_d3d11.h"
#include "src/config.h"
#include "src/logging.h"
#include "src/nvenc/nvenc_config.h"
@@ -56,6 +58,10 @@ using frame_t = util::safe_ptr;
namespace platf::dxgi {
+ namespace {
+ std::atomic g_active_native_amf_encoders {0};
+ }
+
template
buf_t make_buffer(device_t::pointer device, const T &t) {
static_assert(sizeof(T) % 16 == 0, "Buffer needs to be aligned on a 16-byte alignment");
@@ -466,6 +472,35 @@ namespace platf::dxgi {
class d3d_base_encode_device final {
public:
+ void enable_dynamic_output_textures() {
+ dynamic_output_textures = true;
+ }
+
+ bool prepare_display_lease(std::shared_ptr prepared_display) {
+ auto display_base = std::dynamic_pointer_cast(std::move(prepared_display));
+ if (!display_base) {
+ return false;
+ }
+ detached_display_width = display_base->width;
+ detached_display_height = display_base->height;
+ detached_display_rotation = display_base->display_rotation;
+ display = std::move(display_base);
+ return true;
+ }
+
+ std::shared_ptr release_display_lease_for_initialization() {
+ if (display) {
+ detached_display_width = display->width;
+ detached_display_height = display->height;
+ detached_display_rotation = display->display_rotation;
+ }
+ return std::move(display);
+ }
+
+ void restore_display_lease_after_initialization(std::shared_ptr restored_display) {
+ display = std::move(restored_display);
+ }
+
int convert(platf::img_t &img_base) {
// Garbage collect mapped capture images whose weak references have expired
for (auto it = img_ctx_map.begin(); it != img_ctx_map.end();) {
@@ -536,6 +571,10 @@ namespace platf::dxgi {
device_ctx->PSSetShaderResources(0, 1, &emptyShaderResourceView);
};
+ auto unbind_render_targets = [&]() {
+ device_ctx->OMSetRenderTargets(0, nullptr, nullptr);
+ };
+
auto clear_output_to_black = [&]() -> bool {
if (!ensure_black_texture_for_rtv_clear()) {
return false;
@@ -543,12 +582,22 @@ namespace platf::dxgi {
draw(black_texture_for_clear_srv, out_Y_or_YUV_viewports_for_clear, out_UV_viewport_for_clear, DXGI_FORMAT_B8G8R8A8_UNORM, false, false);
rtvs_cleared = true;
+ if (dynamic_output_textures && output_texture) {
+ auto target = output_targets.find(output_texture);
+ if (target != output_targets.end()) {
+ target->second.cleared = true;
+ }
+ }
unbind_shader_resource();
return true;
};
if (img.blank) {
- return clear_output_to_black() ? 0 : -1;
+ const bool cleared = clear_output_to_black();
+ if (dynamic_output_textures) {
+ unbind_render_targets();
+ }
+ return cleared ? 0 : -1;
}
auto &img_ctx = img_ctx_map[img.id];
@@ -796,6 +845,9 @@ namespace platf::dxgi {
maybe_log_truehdr_live_readback();
}
#endif
+ if (dynamic_output_textures) {
+ unbind_render_targets();
+ }
// Release encoder mutex to allow capture code to reuse this image
if (release_encoder_mutex_now()) {
@@ -876,7 +928,8 @@ namespace platf::dxgi {
using namespace std::chrono_literals;
auto &request = truehdr_live_readback_request;
- if (!request || request->logged || std::chrono::steady_clock::now() - request->stable_since < 500ms) {
+ if (!request || request->logged || !output_texture ||
+ std::chrono::steady_clock::now() - request->stable_since < 500ms) {
return;
}
request->logged = true;
@@ -917,7 +970,7 @@ namespace platf::dxgi {
// The luma/UV render targets must be unbound before copying the encoder input.
device_ctx->OMSetRenderTargets(0, nullptr, nullptr);
- device_ctx->CopyResource(truehdr_live_readback_texture.get(), output_texture.get());
+ device_ctx->CopyResource(truehdr_live_readback_texture.get(), output_texture);
D3D11_MAPPED_SUBRESOURCE mapped {};
const HRESULT map_status = device_ctx->Map(truehdr_live_readback_texture.get(), 0, D3D11_MAP_READ, 0, &mapped);
@@ -1028,10 +1081,71 @@ namespace platf::dxgi {
this->color_matrix = std::move(color_matrix);
}
+ int set_output_texture(ID3D11Texture2D *frame_texture) {
+ if (!frame_texture) {
+ return -1;
+ }
+ if (!dynamic_output_textures) {
+ BOOST_LOG(error) << "Attempted to rotate a fixed D3D11 encoder output texture";
+ return -1;
+ }
+ if (output_texture == frame_texture) {
+ return 0;
+ }
+
+ if (output_texture) {
+ auto current = output_targets.find(output_texture);
+ if (current != output_targets.end()) {
+ current->second.cleared = rtvs_cleared;
+ }
+ }
+
+ auto [target_it, inserted] = output_targets.try_emplace(frame_texture);
+ auto &target = target_it->second;
+ if (inserted) {
+ frame_texture->AddRef();
+ target.texture.reset(frame_texture);
+
+ auto create_rtv = [&](render_target_t &view, DXGI_FORMAT view_format) -> bool {
+ if (view_format == DXGI_FORMAT_UNKNOWN) {
+ return true;
+ }
+ D3D11_RENDER_TARGET_VIEW_DESC rtv_desc = {};
+ rtv_desc.Format = view_format;
+ rtv_desc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
+ const auto status = device->CreateRenderTargetView(frame_texture, &rtv_desc, &view);
+ if (FAILED(status)) {
+ BOOST_LOG(error) << "Failed to create render target view: " << util::log_hex(status);
+ return false;
+ }
+ return true;
+ };
+
+ if (!create_rtv(target.y_or_yuv, output_y_or_yuv_rtv_format) ||
+ !create_rtv(target.uv, output_uv_rtv_format)) {
+ output_targets.erase(target_it);
+ return -1;
+ }
+
+ if (output_rtv_simple_clear) {
+ const float y_black[] = {0.0f, 0.0f, 0.0f, 0.0f};
+ device_ctx->ClearRenderTargetView(target.y_or_yuv.get(), y_black);
+ if (target.uv) {
+ const float uv_black[] = {0.5f, 0.5f, 0.5f, 0.5f};
+ device_ctx->ClearRenderTargetView(target.uv.get(), uv_black);
+ }
+ target.cleared = true;
+ }
+ }
+
+ output_texture = frame_texture;
+ out_Y_or_YUV_rtv = target.y_or_yuv.get();
+ out_UV_rtv = target.uv.get();
+ rtvs_cleared = target.cleared;
+ return 0;
+ }
+
int init_output(ID3D11Texture2D *frame_texture, int width, int height, const ::video::sunshine_colorspace_t &colorspace) {
- // The underlying frame pool owns the texture, so we must reference it for ourselves
- frame_texture->AddRef();
- output_texture.reset(frame_texture);
HRESULT status = S_OK;
@@ -1054,7 +1168,14 @@ namespace platf::dxgi {
return -1; \
}
- const bool downscaling = display->width > width || display->height > height;
+ const auto display_width = display ? display->width : detached_display_width;
+ const auto display_height = display ? display->height : detached_display_height;
+ const auto display_rotation = display ? display->display_rotation : detached_display_rotation;
+ if (display_width <= 0 || display_height <= 0) {
+ BOOST_LOG(error) << "Missing source display geometry for D3D11 encoder initialization";
+ return -1;
+ }
+ const bool downscaling = display_width > width || display_height > height;
const bool target_hdr = ::video::colorspace_is_hdr(colorspace);
switch (format) {
@@ -1171,8 +1292,8 @@ namespace platf::dxgi {
auto out_width = width;
auto out_height = height;
- float in_width = display->width;
- float in_height = display->height;
+ float in_width = display_width;
+ float in_height = display_height;
// Ensure aspect ratio is maintained
auto scalar = std::fminf(out_width / in_width, out_height / in_height);
@@ -1208,7 +1329,7 @@ namespace platf::dxgi {
device_ctx->VSSetConstantBuffers(0, 1, &subsample_offset);
{
- int32_t rotation_modifier = display->display_rotation == DXGI_MODE_ROTATION_UNSPECIFIED ? 0 : display->display_rotation - 1;
+ int32_t rotation_modifier = display_rotation == DXGI_MODE_ROTATION_UNSPECIFIED ? 0 : display_rotation - 1;
int32_t rotation_data[16 / sizeof(int32_t)] {-rotation_modifier}; // aligned to 16-byte
auto rotation = make_buffer(device.get(), rotation_data);
if (!rotation) {
@@ -1218,33 +1339,33 @@ namespace platf::dxgi {
device_ctx->VSSetConstantBuffers(1, 1, &rotation);
}
- DXGI_FORMAT rtv_Y_or_YUV_format = DXGI_FORMAT_UNKNOWN;
- DXGI_FORMAT rtv_UV_format = DXGI_FORMAT_UNKNOWN;
- bool rtv_simple_clear = false;
+ output_y_or_yuv_rtv_format = DXGI_FORMAT_UNKNOWN;
+ output_uv_rtv_format = DXGI_FORMAT_UNKNOWN;
+ output_rtv_simple_clear = false;
switch (format) {
case DXGI_FORMAT_NV12:
- rtv_Y_or_YUV_format = DXGI_FORMAT_R8_UNORM;
- rtv_UV_format = DXGI_FORMAT_R8G8_UNORM;
- rtv_simple_clear = true;
+ output_y_or_yuv_rtv_format = DXGI_FORMAT_R8_UNORM;
+ output_uv_rtv_format = DXGI_FORMAT_R8G8_UNORM;
+ output_rtv_simple_clear = true;
break;
case DXGI_FORMAT_P010:
- rtv_Y_or_YUV_format = DXGI_FORMAT_R16_UNORM;
- rtv_UV_format = DXGI_FORMAT_R16G16_UNORM;
- rtv_simple_clear = true;
+ output_y_or_yuv_rtv_format = DXGI_FORMAT_R16_UNORM;
+ output_uv_rtv_format = DXGI_FORMAT_R16G16_UNORM;
+ output_rtv_simple_clear = true;
break;
case DXGI_FORMAT_AYUV:
- rtv_Y_or_YUV_format = DXGI_FORMAT_R8G8B8A8_UINT;
+ output_y_or_yuv_rtv_format = DXGI_FORMAT_R8G8B8A8_UINT;
break;
case DXGI_FORMAT_R16_UINT:
- rtv_Y_or_YUV_format = DXGI_FORMAT_R16_UINT;
+ output_y_or_yuv_rtv_format = DXGI_FORMAT_R16_UINT;
break;
case DXGI_FORMAT_Y410:
- rtv_Y_or_YUV_format = DXGI_FORMAT_R10G10B10A2_UINT;
+ output_y_or_yuv_rtv_format = DXGI_FORMAT_R10G10B10A2_UINT;
break;
default:
@@ -1252,44 +1373,51 @@ namespace platf::dxgi {
return -1;
}
- auto create_rtv = [&](auto &rt, DXGI_FORMAT rt_format) -> bool {
+ if (dynamic_output_textures) {
+ return set_output_texture(frame_texture);
+ }
+
+ // Preserve the original fixed-output path for NVENC and legacy/shared D3D
+ // users. Native AMF alone opts into the rotating texture map above.
+ frame_texture->AddRef();
+ fixed_output_texture.reset(frame_texture);
+ // Keep the raw output pointer in sync for consumers shared with the
+ // dynamic path (TrueHDR live readback dereferences it unconditionally).
+ output_texture = frame_texture;
+
+ auto create_fixed_rtv = [&](auto &target, DXGI_FORMAT view_format) -> bool {
+ if (view_format == DXGI_FORMAT_UNKNOWN) {
+ return true;
+ }
D3D11_RENDER_TARGET_VIEW_DESC rtv_desc = {};
- rtv_desc.Format = rt_format;
+ rtv_desc.Format = view_format;
rtv_desc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
-
- auto status = device->CreateRenderTargetView(output_texture.get(), &rtv_desc, &rt);
- if (FAILED(status)) {
- BOOST_LOG(error) << "Failed to create render target view: " << util::log_hex(status);
+ const auto create_status = device->CreateRenderTargetView(fixed_output_texture.get(), &rtv_desc, &target);
+ if (FAILED(create_status)) {
+ BOOST_LOG(error) << "Failed to create render target view: " << util::log_hex(create_status);
return false;
}
-
return true;
};
- // Create Y/YUV render target view
- if (!create_rtv(out_Y_or_YUV_rtv, rtv_Y_or_YUV_format)) {
+ if (!create_fixed_rtv(fixed_out_Y_or_YUV_rtv, output_y_or_yuv_rtv_format) ||
+ !create_fixed_rtv(fixed_out_UV_rtv, output_uv_rtv_format)) {
return -1;
}
+ out_Y_or_YUV_rtv = fixed_out_Y_or_YUV_rtv.get();
+ out_UV_rtv = fixed_out_UV_rtv.get();
- // Create UV render target view if needed
- if (rtv_UV_format != DXGI_FORMAT_UNKNOWN && !create_rtv(out_UV_rtv, rtv_UV_format)) {
- return -1;
- }
-
- if (rtv_simple_clear) {
- // Clear the RTVs to ensure the aspect ratio padding is black
+ if (output_rtv_simple_clear) {
const float y_black[] = {0.0f, 0.0f, 0.0f, 0.0f};
- device_ctx->ClearRenderTargetView(out_Y_or_YUV_rtv.get(), y_black);
+ device_ctx->ClearRenderTargetView(out_Y_or_YUV_rtv, y_black);
if (out_UV_rtv) {
const float uv_black[] = {0.5f, 0.5f, 0.5f, 0.5f};
- device_ctx->ClearRenderTargetView(out_UV_rtv.get(), uv_black);
+ device_ctx->ClearRenderTargetView(out_UV_rtv, uv_black);
}
rtvs_cleared = true;
} else {
- // Can't use ClearRenderTargetView(), will clear on first convert()
rtvs_cleared = false;
}
-
return 0;
}
@@ -1320,6 +1448,15 @@ namespace platf::dxgi {
return -1;
}
+ if (display) {
+ if (!prepare_display_lease(std::move(display))) {
+ return -1;
+ }
+ } else if (!this->display && (detached_display_width <= 0 || detached_display_height <= 0)) {
+ BOOST_LOG(error) << "Missing prepared display state for deferred D3D11 encoder initialization";
+ return -1;
+ }
+
D3D_FEATURE_LEVEL featureLevels[] {
D3D_FEATURE_LEVEL_11_1,
D3D_FEATURE_LEVEL_11_0,
@@ -1385,12 +1522,6 @@ namespace platf::dxgi {
device_ctx->VSSetConstantBuffers(3, 1, &color_matrix);
device_ctx->PSSetConstantBuffers(0, 1, &color_matrix);
- this->display = std::dynamic_pointer_cast(display);
- if (!this->display) {
- return -1;
- }
- display = nullptr;
-
blend_disable = make_blend(device.get(), false, false);
if (!blend_disable) {
return -1;
@@ -1716,6 +1847,9 @@ namespace platf::dxgi {
// device concurrently with this device's DestroyDriverInstance, faulting the NVIDIA UMD on a freed
// cross-device shared-resource dependency.
std::shared_ptr display;
+ int detached_display_width = 0;
+ int detached_display_height = 0;
+ DXGI_MODE_ROTATION detached_display_rotation = DXGI_MODE_ROTATION_UNSPECIFIED;
// Keep the underlying D3D device/context alive until after all dependent resources have been released.
device_t device;
@@ -1750,7 +1884,21 @@ namespace platf::dxgi {
bool truehdr_live_readback_failure_logged = false;
#endif
- texture2d_t output_texture;
+ struct output_target_t {
+ texture2d_t texture;
+ render_target_t y_or_yuv;
+ render_target_t uv;
+ bool cleared = false;
+ };
+
+ ID3D11Texture2D *output_texture = nullptr;
+ std::map output_targets;
+ texture2d_t fixed_output_texture;
+ render_target_t fixed_out_Y_or_YUV_rtv;
+ render_target_t fixed_out_UV_rtv;
+ DXGI_FORMAT output_y_or_yuv_rtv_format = DXGI_FORMAT_UNKNOWN;
+ DXGI_FORMAT output_uv_rtv_format = DXGI_FORMAT_UNKNOWN;
+ bool output_rtv_simple_clear = false;
texture2d_t black_texture_for_clear;
shader_res_t black_texture_for_clear_srv;
@@ -1762,9 +1910,10 @@ namespace platf::dxgi {
blend_t blend_disable;
sampler_state_t sampler_linear;
- render_target_t out_Y_or_YUV_rtv;
- render_target_t out_UV_rtv;
+ ID3D11RenderTargetView *out_Y_or_YUV_rtv = nullptr;
+ ID3D11RenderTargetView *out_UV_rtv = nullptr;
bool rtvs_cleared = false;
+ bool dynamic_output_textures = false;
// d3d_img_t::id -> encoder_img_ctx_t
// These store the encoder textures for each img_t that passes through
@@ -1799,12 +1948,48 @@ namespace platf::dxgi {
DXGI_FORMAT format;
};
+ bool is_codec_supported_for_adapter(
+ const DXGI_ADAPTER_DESC &adapter_desc,
+ std::string_view name,
+ const ::video::config_t &config);
+
class d3d_avcodec_encode_device_t: public avcodec_encode_device_t {
public:
int init(std::shared_ptr display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) {
- int result = base.init(display, adapter_p, pix_fmt);
+ if (!prepare_device(std::move(display), adapter_p, pix_fmt)) return -1;
+ return initialize_hardware_device() ? 0 : -1;
+ }
+
+ bool prepare_device(std::shared_ptr display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) {
+ if (!adapter_p || !base.prepare_display_lease(std::move(display))) return false;
+ adapter_p->GetDesc(&adapter_desc);
+ adapter_p->AddRef();
+ initialization_adapter.reset(adapter_p);
+ buffer_format = pix_fmt;
+ return true;
+ }
+
+ bool initialize_hardware_device() override {
+ if (data) return true;
+ if (!initialization_adapter || base.init(nullptr, initialization_adapter.get(), buffer_format) != 0) {
+ return false;
+ }
data = base.device.get();
- return result;
+ initialization_adapter.reset();
+ return true;
+ }
+
+ std::shared_ptr release_display_lease_for_initialization() override {
+ return base.release_display_lease_for_initialization();
+ }
+
+ void restore_display_lease_after_initialization(std::shared_ptr display) override {
+ base.restore_display_lease_after_initialization(
+ std::dynamic_pointer_cast(std::move(display)));
+ }
+
+ bool is_codec_supported(std::string_view name, const ::video::config_t &config) override {
+ return is_codec_supported_for_adapter(adapter_desc, name, config);
}
int convert(platf::img_t &img_base) override {
@@ -1887,6 +2072,9 @@ namespace platf::dxgi {
private:
d3d_base_encode_device base;
frame_t hwframe;
+ adapter_t initialization_adapter;
+ DXGI_ADAPTER_DESC adapter_desc {};
+ pix_fmt_e buffer_format = pix_fmt_e::unknown;
};
class d3d_nvenc_encode_device_t: public nvenc_encode_device_t {
@@ -1952,6 +2140,236 @@ namespace platf::dxgi {
NV_ENC_BUFFER_FORMAT buffer_format = NV_ENC_BUFFER_FORMAT_UNDEFINED;
};
+ // Native AMD AMF encode device. Like the NVENC device, it shares the
+ // d3d_base_encode_device colour-conversion pipeline and writes the converted
+ // NV12/P010 frame straight into the D3D11 texture the AMF encoder reads from
+ // (zero-copy). The AMF runtime is loaded dynamically inside amf_d3d11.
+ class d3d_amf_encode_device_t: public amf_encode_device_t {
+ public:
+ ~d3d_amf_encode_device_t() override {
+ if (registered_active_encoder) {
+ amf = nullptr;
+ amf_d3d.reset();
+ const auto remaining = g_active_native_amf_encoders.fetch_sub(1, std::memory_order_acq_rel) - 1;
+ BOOST_LOG(info) << "AMF: native encoder session closed (active=" << remaining << ')';
+ }
+ }
+
+ bool prepare_device(std::shared_ptr display, adapter_t::pointer adapter_p, pix_fmt_e pix_fmt) {
+ if (!adapter_p || !base.prepare_display_lease(std::move(display))) {
+ return false;
+ }
+ adapter_p->AddRef();
+ initialization_adapter.reset(adapter_p);
+ buffer_format = pix_fmt;
+ return true;
+ }
+
+ bool initialize_hardware_device() override {
+ if (amf_d3d) {
+ return true;
+ }
+ if (!initialization_adapter || base.init(nullptr, initialization_adapter.get(), buffer_format)) {
+ return false;
+ }
+
+ // Async encoder teardown may destroy D3D resources on a different thread.
+ multithread_t mt;
+ auto status = base.device->QueryInterface(IID_ID3D11Multithread, (void **) &mt);
+ if (SUCCEEDED(status)) {
+ mt->SetMultithreadProtected(TRUE);
+ } else {
+ BOOST_LOG(warning) << "Failed to query ID3D11Multithread interface from device [0x"sv << util::hex(status).to_string_view() << ']';
+ }
+
+ amf_d3d = ::amf::create_amf_d3d11(base.device.get());
+ if (!amf_d3d) {
+ return false;
+ }
+
+ base.enable_dynamic_output_textures();
+ amf = amf_d3d.get();
+ initialization_adapter.reset();
+ return true;
+ }
+
+ std::shared_ptr release_display_lease_for_initialization() override {
+ return base.release_display_lease_for_initialization();
+ }
+
+ void restore_display_lease_after_initialization(std::shared_ptr display) override {
+ base.restore_display_lease_after_initialization(
+ std::dynamic_pointer_cast(std::move(display)));
+ }
+
+ bool init_encoder(const ::video::config_t &client_config, const ::video::sunshine_colorspace_t &colorspace) override {
+ if (!amf_d3d) {
+ return false;
+ }
+
+ ::amf::amf_config amf_cfg;
+ // Initialization is single-flight at the video layer. Count this session
+ // as active only after AMF Init succeeds so a timed-out vendor call cannot
+ // permanently poison concurrency policy for every later session.
+ const auto active_encoder_count = g_active_native_amf_encoders.load(std::memory_order_acquire) + 1;
+ BOOST_LOG(info) << "AMF: creating native encoder session " << client_config.width << 'x' << client_config.height
+ << '@' << client_config.framerate << " codec=" << client_config.videoFormat
+ << " bitrate=" << client_config.bitrate << "kbps (active=" << active_encoder_count << ')';
+
+ // AMF SDK integer values are passed straight through from the existing
+ // amd_* config (shared with the FFmpeg amdvce_legacy path).
+ if (client_config.videoFormat == 0) {
+ amf_cfg.usage = config::video.amd.amd_usage_h264;
+ amf_cfg.quality_preset = config::video.amd.amd_quality_h264;
+ amf_cfg.rc_mode = config::video.amd.amd_rc_h264;
+ } else if (client_config.videoFormat == 1) {
+ amf_cfg.usage = config::video.amd.amd_usage_hevc;
+ amf_cfg.quality_preset = config::video.amd.amd_quality_hevc;
+ amf_cfg.rc_mode = config::video.amd.amd_rc_hevc;
+ } else {
+ amf_cfg.usage = config::video.amd.amd_usage_av1;
+ amf_cfg.quality_preset = config::video.amd.amd_quality_av1;
+ amf_cfg.rc_mode = config::video.amd.amd_rc_av1;
+ }
+
+ amf_cfg.vbaq = config::video.amd.amd_vbaq;
+ amf_cfg.enforce_hrd = config::video.amd.amd_enforce_hrd;
+ amf_cfg.qvbr_quality_level = config::video.amd.amd_qvbr_quality_level;
+ amf_cfg.h264_cabac = ::amf::lifecycle::resolve_h264_cabac(config::video.amd.amd_coder);
+
+ // QVBR/HQVBR/HQCBR require PreAnalysis. Native AMF uses AMD's one-frame
+ // ultra-low-latency PA depth so those modes stay native without inheriting
+ // the much deeper transcoding/high-quality lookahead defaults.
+ const auto preanalysis_plan = ::amf::lifecycle::resolve_preanalysis(
+ amf_cfg.rc_mode,
+ config::video.amd.amd_preanalysis);
+ amf_cfg.preanalysis = preanalysis_plan.enabled ? 1 : 0;
+ if (preanalysis_plan.enabled) {
+ amf_cfg.pa_lookahead_depth = preanalysis_plan.lookahead_depth;
+ if (preanalysis_plan.enabled_for_rate_control &&
+ (!config::video.amd.amd_preanalysis || !*config::video.amd.amd_preanalysis)) {
+ BOOST_LOG(info) << "AMF: enabling native PreAnalysis required by the selected rate-control mode";
+ }
+ }
+
+ // AMF statistics feedback changes the driver submission path and is not a
+ // harmless consequence of debug logging. Keep it disabled unless a dedicated
+ // diagnostic option is added; normal debug logs must exercise the production
+ // encoder path.
+ amf_cfg.enable_statistics_feedback = false;
+
+ // Native-AMF tuning knobs.
+ amf_cfg.max_ltr_frames = config::video.amd.amd_ltr_frames; // 0 = RFI off
+ if (config::video.amd.amd_input_queue_size > 0) {
+ amf_cfg.input_queue_size = config::video.amd.amd_input_queue_size;
+ }
+
+ // Xbox clients request rolling intra refresh because they cannot always
+ // recover by asking for a keyframe. Match NVENC's 300-frame period using
+ // the codec-specific AMF controls. H.264 AMF requires LTR to be disabled
+ // and at least two reference frames while intra refresh is active.
+ const auto intra_refresh_plan = ::amf::lifecycle::resolve_intra_refresh(
+ client_config.enableIntraRefresh == 1,
+ client_config.videoFormat,
+ client_config.width,
+ client_config.height);
+ amf_cfg.intra_refresh_mbs = intra_refresh_plan.blocks_per_slot;
+ amf_cfg.intra_refresh_minimum_reference_frames = intra_refresh_plan.minimum_reference_frames;
+ amf_cfg.av1_intra_refresh_mode = intra_refresh_plan.av1_mode;
+ amf_cfg.av1_intra_refresh_stripes = intra_refresh_plan.av1_cycle_frames;
+ if (intra_refresh_plan.disable_ltr && amf_cfg.max_ltr_frames > 0) {
+ BOOST_LOG(warning) << "AMF: disabling LTR because the client requested H.264 intra refresh";
+ amf_cfg.max_ltr_frames = 0;
+ }
+ if (intra_refresh_plan.enabled) {
+ BOOST_LOG(info) << "AMF: enabling client-requested intra refresh (period="
+ << ::amf::lifecycle::intra_refresh_period_frames
+ << ", blocks_per_slot=" << intra_refresh_plan.blocks_per_slot.value_or(0)
+ << ", AV1_mode=" << intra_refresh_plan.av1_mode.value_or(0)
+ << ", AV1_cycle=" << intra_refresh_plan.av1_cycle_frames.value_or(0) << ')';
+ }
+
+ // Curated opt-in AMF feature knobs. Each defaults to "auto" (nullopt), which
+ // leaves the AMF driver default untouched, so behavior is unchanged unless the
+ // user opts in. Tri-state knobs map nullopt -> unset, 1 -> true, 0 -> false.
+ auto amf_tristate = [](const std::optional &v) -> std::optional {
+ if (!v) {
+ return std::nullopt;
+ }
+ return *v != 0;
+ };
+ amf_cfg.lowlatency_mode = amf_tristate(config::video.amd.amd_lowlatency_mode);
+ amf_cfg.high_motion_quality_boost_enable = amf_tristate(config::video.amd.amd_high_motion_quality_boost);
+ amf_cfg.av1_screen_content_tools = amf_tristate(config::video.amd.amd_av1_screen_content);
+ amf_cfg.av1_encoding_latency_mode = config::video.amd.amd_av1_latency_mode;
+
+ const auto concurrent_feature_plan = ::amf::lifecycle::resolve_concurrent_session_features(
+ active_encoder_count,
+ amf_cfg.lowlatency_mode,
+ amf_cfg.high_motion_quality_boost_enable);
+ amf_cfg.lowlatency_mode = concurrent_feature_plan.low_latency_mode;
+ amf_cfg.high_motion_quality_boost_enable = concurrent_feature_plan.high_motion_quality_boost;
+ if (concurrent_feature_plan.overrides_suppressed) {
+ BOOST_LOG(warning) << "AMF: preserving concurrent native session by leaving the optional"
+ " Low Latency Mode/high-motion overrides unset; the AMF ultra-low-latency"
+ " usage preset remains active";
+ }
+
+ // Resolve Smart Access Video against the post-suppression Low Latency
+ // Mode: a second concurrent session that suppressed LLM must not also
+ // lose an explicit SAV opt-in based on the stale pre-suppression value.
+ const auto smart_access_video_plan = ::amf::lifecycle::resolve_smart_access_video(
+ amf_tristate(config::video.amd.amd_smart_access_video),
+ amf_cfg.lowlatency_mode);
+ amf_cfg.smart_access_video = smart_access_video_plan.enabled;
+ if (smart_access_video_plan.disabled_for_low_latency) {
+ BOOST_LOG(warning) << "AMF: disabling Smart Access Video because AMF Low Latency Mode is enabled;"
+ " this combination has caused AMD GPU watchdog resets";
+ }
+
+ if (!amf_d3d->create_encoder(amf_cfg, client_config, colorspace, buffer_format)) {
+ return false;
+ }
+ const auto registered_count = g_active_native_amf_encoders.fetch_add(1, std::memory_order_acq_rel) + 1;
+ registered_active_encoder = true;
+ if (registered_count != active_encoder_count) {
+ BOOST_LOG(debug) << "AMF: active session count changed during initialization (planned="
+ << active_encoder_count << ", registered=" << registered_count << ')';
+ }
+ return true;
+ }
+
+ bool finish_encoder_initialization(const ::video::config_t &client_config, const ::video::sunshine_colorspace_t &colorspace) override {
+ base.apply_colorspace(colorspace, client_config.rtx_hdr_active);
+ return base.init_output(
+ static_cast(amf_d3d->get_input_texture()),
+ client_config.width,
+ client_config.height,
+ colorspace) == 0;
+ }
+
+ int convert(platf::img_t &img_base) override {
+ auto *render_target = amf_d3d->acquire_input_texture_for_render();
+ if (!render_target || base.set_output_texture(render_target) != 0) {
+ amf_d3d->cancel_input_texture_for_render();
+ return -1;
+ }
+
+ const auto result = base.convert(img_base);
+ if (result != 0) {
+ amf_d3d->cancel_input_texture_for_render();
+ }
+ return result;
+ }
+
+ private:
+ d3d_base_encode_device base;
+ std::unique_ptr<::amf::amf_d3d11> amf_d3d;
+ adapter_t initialization_adapter;
+ platf::pix_fmt_e buffer_format = platf::pix_fmt_e::unknown;
+ bool registered_active_encoder = false;
+ };
+
bool set_cursor_texture(device_t::pointer device, gpu_cursor_t &cursor, util::buffer_t