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
Range1-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
Choicesautoleave the driver default
enabledforce screen-content tools on
disabledforce 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
Choicesautoleave the driver default
nonebalance latency and power
power_savingreal-time with lower power
realtimereal-time
lowestlowest 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 &&cursor_img, DXGI_OUTDUPL_POINTER_SHAPE_INFO &shape_info) { // This cursor image may not be used if (cursor_img.size() == 0) { @@ -2636,10 +3054,10 @@ namespace platf::dxgi { * @param config The codec configuration. * @return `true` if supported, `false` otherwise. */ - bool display_vram_t::is_codec_supported(std::string_view name, const ::video::config_t &config) { - DXGI_ADAPTER_DESC adapter_desc; - adapter->GetDesc(&adapter_desc); - + bool is_codec_supported_for_adapter( + const DXGI_ADAPTER_DESC &adapter_desc, + std::string_view name, + const ::video::config_t &config) { if (adapter_desc.VendorId == 0x1002) { // AMD // If it's not an AMF encoder, it's not compatible with an AMD GPU if (!boost::algorithm::ends_with(name, "_amf")) { @@ -2720,6 +3138,12 @@ namespace platf::dxgi { return true; } + bool display_vram_t::is_codec_supported(std::string_view name, const ::video::config_t &config) { + DXGI_ADAPTER_DESC adapter_desc {}; + adapter->GetDesc(&adapter_desc); + return is_codec_supported_for_adapter(adapter_desc, name, config); + } + std::unique_ptr display_vram_t::make_avcodec_encode_device(pix_fmt_e pix_fmt) { auto device = std::make_unique(); if (device->init(shared_from_this(), adapter.get(), pix_fmt) != 0) { @@ -2728,6 +3152,14 @@ namespace platf::dxgi { return device; } + std::unique_ptr display_vram_t::make_deferred_avcodec_encode_device(pix_fmt_e pix_fmt) { + auto device = std::make_unique(); + if (!device->prepare_device(shared_from_this(), adapter.get(), pix_fmt)) { + return nullptr; + } + return device; + } + std::unique_ptr display_vram_t::make_nvenc_encode_device(pix_fmt_e pix_fmt) { auto device = std::make_unique(); if (!device->init_device(shared_from_this(), adapter.get(), pix_fmt)) { @@ -2736,6 +3168,16 @@ namespace platf::dxgi { return device; } + std::unique_ptr display_vram_t::make_amf_encode_device(pix_fmt_e pix_fmt) { + auto device = std::make_unique(); + // This path must remain free of driver calls. The video-layer watchdog owns + // D3D11CreateDevice and every subsequent AMF initialization/destruction call. + if (!device->prepare_device(shared_from_this(), adapter.get(), pix_fmt)) { + return nullptr; + } + return device; + } + int init() { BOOST_LOG(info) << "Compiling shaders..."sv; diff --git a/src/video.cpp b/src/video.cpp index e490c8542..58f3c8f3e 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,8 @@ extern "C" { #include "cbs.h" #include "config.h" #include "display_device.h" +#include "amf/amf_encoder.h" +#include "amf/amf_lifecycle.h" #include "globals.h" #include "input.h" #include "logging.h" @@ -157,6 +160,10 @@ namespace video { // teardown of the other device: it faults walking freed dependency entries. Funnel all // encoder teardown through this mutex so only one device is ever mid-destruction. std::mutex encode_session_teardown_mutex; + // A host restart is the recovery boundary after a vendor call times out. + // Detached watchdog workers can outlive ordinary static destruction during + // process shutdown. Deliberately give the runtime fence process lifetime. + auto &native_amf_lifecycle_gate = *new amf::lifecycle::native_runtime_gate_t(); #ifdef _WIN32 void release_d3d_capture_images_async(std::vector> images) { @@ -342,7 +349,8 @@ namespace video { std::string build_probe_cache_key() { std::ostringstream oss; - // Cache probe results strictly by detected GPU identity. + // Cache probe results by GPU identity and every setting that can change + // encoder selection or native-AMF capability/initialization behavior. oss << "gpu|"; #ifdef _WIN32 auto gpus = platf::enumerate_gpus(); @@ -370,6 +378,40 @@ namespace video { #else oss << "nogpu"; #endif + auto append_optional = [&](std::string_view name, const std::optional &value) { + oss << '|' << name << '='; + if (value) { + oss << *value; + } else { + oss << "auto"; + } + }; + oss << "|encoder=" << config::video.encoder + << "|adapter=" << config::video.adapter_name + << "|capture=" << config::video.capture + << "|hevc=" << config::video.hevc_mode + << "|av1=" << config::video.av1_mode + << "|amd_coder=" << config::video.amd.amd_coder + << "|amd_ltr=" << config::video.amd.amd_ltr_frames + << "|amd_queue=" << config::video.amd.amd_input_queue_size; + append_optional("amd_usage_h264", config::video.amd.amd_usage_h264); + append_optional("amd_usage_hevc", config::video.amd.amd_usage_hevc); + append_optional("amd_usage_av1", config::video.amd.amd_usage_av1); + append_optional("amd_rc_h264", config::video.amd.amd_rc_h264); + append_optional("amd_rc_hevc", config::video.amd.amd_rc_hevc); + append_optional("amd_rc_av1", config::video.amd.amd_rc_av1); + append_optional("amd_quality_h264", config::video.amd.amd_quality_h264); + append_optional("amd_quality_hevc", config::video.amd.amd_quality_hevc); + append_optional("amd_quality_av1", config::video.amd.amd_quality_av1); + append_optional("amd_qvbr_quality", config::video.amd.amd_qvbr_quality_level); + append_optional("amd_vbaq", config::video.amd.amd_vbaq); + append_optional("amd_preanalysis", config::video.amd.amd_preanalysis); + append_optional("amd_enforce_hrd", config::video.amd.amd_enforce_hrd); + append_optional("amd_lowlatency", config::video.amd.amd_lowlatency_mode); + append_optional("amd_motion_boost", config::video.amd.amd_high_motion_quality_boost); + append_optional("amd_sav", config::video.amd.amd_smart_access_video); + append_optional("amd_av1_screen", config::video.amd.amd_av1_screen_content); + append_optional("amd_av1_latency", config::video.amd.amd_av1_latency_mode); return oss.str(); } @@ -767,6 +809,14 @@ namespace video { } } + void restore_display_lease_after_initialization(std::shared_ptr display) { + if (device) device->restore_display_lease_after_initialization(std::move(display)); + } + + std::shared_ptr release_display_lease_for_driver_work() { + return device ? device->release_display_lease_for_initialization() : nullptr; + } + avcodec_ctx_t avcodec_ctx; std::unique_ptr device; @@ -841,6 +891,179 @@ namespace video { bool force_idr = false; }; + class amf_encode_session_t: public encode_session_t { + public: + amf_encode_session_t(std::unique_ptr encode_device): + device(std::move(encode_device)) { + } + + int convert(platf::img_t &img) override { + if (!device) { + return -1; + } + const auto result = device->convert(img); + if (result == 0) { + fresh_conversion_pending = true; + } + return result; + } + + void request_idr_frame() override { + force_idr = true; + } + + void request_normal_frame() override { + // encode_frames() clears force_idr only after AMF accepts the input. The + // generic loops call this after every nonfatal attempt, including + // backpressure drops, so clearing it here would lose recovery IDRs. + } + + void invalidate_ref_frames(int64_t first_frame, int64_t last_frame) override { + if (!device || !device->amf) { + return; + } + + if (!device->amf->invalidate_ref_frames(first_frame, last_frame)) { + force_idr = true; + } + } + + bool set_bitrate(int bitrate_kbps) override { + return device && device->amf && device->amf->set_bitrate(bitrate_kbps); + } + + amf::amf_encode_result encode_frames(uint64_t frame_index) { + if (!device || !device->amf) { + return {}; + } + + const bool meaningful_new_input = fresh_conversion_pending || force_idr; + auto result = device->amf->encode_frame(frame_index, force_idr); + last_input_accepted = result.input_accepted; + if (result.input_accepted) { + last_input_accepted_at = result.input_accepted_at; + force_idr = false; + // A duplicate submitted solely to release PA's retained final frame must + // not arm another flush just because PA now retains that duplicate. A + // newly converted capture frame or requested IDR starts a new cycle. + suppress_tail_flush_until_fresh_conversion = !meaningful_new_input; + fresh_conversion_pending = false; + } + return result; + } + + bool was_last_input_accepted() const { + return last_input_accepted; + } + + std::optional input_accepted_at() const { + return last_input_accepted_at; + } + + amf::amf_encode_result drain_frames(std::chrono::milliseconds timeout) { + if (!device || !device->amf) { + return {}; + } + return device->amf->drain_output(timeout); + } + + bool has_output_due() { + return device && device->amf && device->amf->has_output_due(); + } + + bool has_completed_output() { + return device && device->amf && device->amf->has_completed_output(); + } + + bool has_retained_preanalysis_tail() { + const bool retained = device && device->amf && device->amf->has_retained_preanalysis_tail(); + return amf::lifecycle::preanalysis_tail_flush_is_due( + retained, + suppress_tail_flush_until_fresh_conversion); + } + + bool begin_drain() { + return device && device->amf && device->amf->begin_drain(); + } + + std::shared_ptr release_display_lease_for_driver_work() { + return device ? device->release_display_lease_for_initialization() : nullptr; + } + + // The native AMF encoder is pipelined. A catch-up batch can contain output from + // earlier submissions, but emitted indices remain strictly increasing (in order, + // no duplicates), so only flag a genuine regression (out-of-order / duplicate). + // Forward gaps are also normal (a dropped frame is logged separately at submit time). + bool note_emitted_index(uint64_t emitted) { + const bool monotonic = last_emitted_index < 0 || (int64_t) emitted > last_emitted_index; + last_emitted_index = (int64_t) emitted; + return monotonic; + } + + bool has_emitted_frame(uint64_t frame_index) const { + return last_emitted_index >= 0 && static_cast(last_emitted_index) >= frame_index; + } + + bool has_emitted_any_frame() const { + return last_emitted_index >= 0; + } + + // Per-frame timestamps captured at submit time. Because the encoder emits an + // earlier frame than the one just submitted, each packet must be stamped with the + // timestamps of the frame it actually carries, not the newest submitted frame - + // otherwise runtime latency stats are skewed by the pipeline depth. + struct frame_timestamps_t { + std::optional frame_timestamp; + std::optional capture_timestamp; + std::optional host_processing_timestamp; + }; + + void store_frame_timestamps(uint64_t frame_index, const frame_timestamps_t &ts) { + // Fixed ring indexed by frame number: no per-frame heap traffic on the + // submit path. The capacity sits well above the real pipeline depth, and a + // collision only ever overwrites an entry 256 frames stale — one the old + // bounded map would have evicted anyway. + auto &slot = pending_timestamps[frame_index & (pending_timestamps.size() - 1)]; + slot.frame_index = frame_index; + slot.valid = true; + slot.ts = ts; + } + + frame_timestamps_t take_frame_timestamps(uint64_t frame_index) { + auto &slot = pending_timestamps[frame_index & (pending_timestamps.size() - 1)]; + if (!slot.valid || slot.frame_index != frame_index) { + return {}; + } + slot.valid = false; + return slot.ts; + } + + void discard_frame_timestamps(uint64_t frame_index) { + auto &slot = pending_timestamps[frame_index & (pending_timestamps.size() - 1)]; + if (slot.valid && slot.frame_index == frame_index) { + slot.valid = false; + } + } + + private: + std::unique_ptr device; + bool force_idr = false; + bool last_input_accepted = false; + std::optional last_input_accepted_at; + bool fresh_conversion_pending = false; + bool suppress_tail_flush_until_fresh_conversion = false; + int64_t last_emitted_index = -1; + + struct pending_timestamp_slot_t { + uint64_t frame_index = 0; + bool valid = false; + frame_timestamps_t ts; + }; + + // Power-of-two size; indexed with frame_index & (size - 1). + std::array pending_timestamps {}; + }; + // Sticky per-session HDR state, persists across capture reinits so a transient SDR // display reading cannot downgrade an HDR stream's colorspace or poison its metadata. struct hdr_latch_t { @@ -1159,8 +1382,60 @@ namespace video { PARALLEL_ENCODING | CBR_WITH_VBR | RELAXED_COMPLIANCE | NO_RC_BUF_LIMIT | YUV444_SUPPORT }; + // Native AMD AMF encoder (src/amf/amf_d3d11.cpp). Bypasses the FFmpeg AMF + // wrapper for direct AMF SDK access: D3D11 zero-copy input, reference-frame + // invalidation and HDR metadata. Selecting amdvce is a strict native-AMF + // contract: feature, initialization, or runtime failures are reported instead + // of silently changing encoder implementations. amdvce_legacy remains an + // explicit user-selected rollback below. encoder_t amdvce { "amdvce"sv, + std::make_unique( + platf::mem_type_e::dxgi, + platf::pix_fmt_e::nv12, + platf::pix_fmt_e::p010, + platf::pix_fmt_e::unknown, + platf::pix_fmt_e::unknown + ), + { + {}, // Common options (configured directly via AMF, not FFmpeg AVOptions) + {}, // SDR-specific options + {}, // HDR-specific options + {}, // YUV444 SDR-specific options + {}, // YUV444 HDR-specific options + {}, // Fallback options + "av1_amf"s, + }, + { + {}, + {}, + {}, + {}, + {}, + {}, + "hevc_amf"s, + }, + { + {}, + {}, + {}, + {}, + {}, + {}, + "h264_amf"s, + }, + // ASYNC_TEARDOWN: healthy session ends and runtime bitrate rebuilds detach + // the bounded destruction worker instead of stalling the encode thread for + // 100-300 ms; the lifecycle gate serializes it against the next session's + // initialization. Shutdown, display reinit, and runtime fatals still force + // an ordered synchronous teardown via the fail-guard's sync conditions. + PARALLEL_ENCODING | REF_FRAMES_INVALIDATION | ASYNC_TEARDOWN // flags + }; + + // Legacy FFmpeg-based AMF encoder. This is an explicit rollback target only; + // native feature, initialization, and runtime failures never select it. + encoder_t amdvce_legacy { + "amdvce_legacy"sv, std::make_unique( AV_HWDEVICE_TYPE_D3D11VA, AV_HWDEVICE_TYPE_NONE, @@ -1182,9 +1457,22 @@ namespace video { {"log_to_dbg"s, []() { return config::sunshine.min_log_level < 2 ? 1 : 0; }}, - {"preencode"s, &config::video.amd.amd_preanalysis}, + {"preanalysis"s, []() { + return amf::lifecycle::resolve_preanalysis( + config::video.amd.amd_rc_av1, + config::video.amd.amd_preanalysis) + .enabled ? 1 : 0; + }}, {"quality"s, &config::video.amd.amd_quality_av1}, {"rc"s, &config::video.amd.amd_rc_av1}, + {"aq_mode"s, encoder_t::option_t::optional_int_function_t {[]() -> std::optional { + if (!amf::lifecycle::rate_control_supports_adaptive_quantization(config::video.amd.amd_rc_av1)) { + return 0; + } + if (!config::video.amd.amd_vbaq) return std::nullopt; + return *config::video.amd.amd_vbaq ? 1 : 0; // AMF AV1 CAQ / none + }}}, + {"qvbr_quality_level"s, &config::video.amd.amd_qvbr_quality_level}, {"usage"s, &config::video.amd.amd_usage_av1}, {"enforce_hrd"s, &config::video.amd.amd_enforce_hrd}, }, @@ -1208,11 +1496,22 @@ namespace video { }}, {"gops_per_idr"s, 1}, {"header_insertion_mode"s, "idr"s}, - {"preencode"s, &config::video.amd.amd_preanalysis}, + {"preanalysis"s, []() { + return amf::lifecycle::resolve_preanalysis( + config::video.amd.amd_rc_hevc, + config::video.amd.amd_preanalysis) + .enabled ? 1 : 0; + }}, {"quality"s, &config::video.amd.amd_quality_hevc}, {"rc"s, &config::video.amd.amd_rc_hevc}, + {"qvbr_quality_level"s, &config::video.amd.amd_qvbr_quality_level}, {"usage"s, &config::video.amd.amd_usage_hevc}, - {"vbaq"s, &config::video.amd.amd_vbaq}, + {"vbaq"s, encoder_t::option_t::optional_int_function_t {[]() -> std::optional { + if (!amf::lifecycle::rate_control_supports_adaptive_quantization(config::video.amd.amd_rc_hevc)) { + return 0; + } + return config::video.amd.amd_vbaq; + }}}, {"enforce_hrd"s, &config::video.amd.amd_enforce_hrd}, {"level"s, [](const config_t &cfg) { auto size = cfg.width * cfg.height; @@ -1245,11 +1544,22 @@ namespace video { {"log_to_dbg"s, []() { return config::sunshine.min_log_level < 2 ? 1 : 0; }}, - {"preencode"s, &config::video.amd.amd_preanalysis}, + {"preanalysis"s, []() { + return amf::lifecycle::resolve_preanalysis( + config::video.amd.amd_rc_h264, + config::video.amd.amd_preanalysis) + .enabled ? 1 : 0; + }}, {"quality"s, &config::video.amd.amd_quality_h264}, {"rc"s, &config::video.amd.amd_rc_h264}, + {"qvbr_quality_level"s, &config::video.amd.amd_qvbr_quality_level}, {"usage"s, &config::video.amd.amd_usage_h264}, - {"vbaq"s, &config::video.amd.amd_vbaq}, + {"vbaq"s, encoder_t::option_t::optional_int_function_t {[]() -> std::optional { + if (!amf::lifecycle::rate_control_supports_adaptive_quantization(config::video.amd.amd_rc_h264)) { + return 0; + } + return config::video.amd.amd_vbaq; + }}}, {"enforce_hrd"s, &config::video.amd.amd_enforce_hrd}, }, {}, // SDR-specific options @@ -1597,6 +1907,7 @@ namespace video { #ifdef _WIN32 &quicksync, &amdvce, + &amdvce_legacy, &mediafoundation, #endif #if defined(__linux__) || defined(linux) || defined(__linux) || defined(__FreeBSD__) @@ -2321,6 +2632,76 @@ namespace video { return 0; } + void deliver_amf_frames( + int64_t submitted_frame_nr, + amf_encode_session_t &session, + std::vector &encoded_frames, + safe::mail_raw_t::queue_t &packets, + void *channel_data + ) { + for (auto &encoded_frame : encoded_frames) { + if (encoded_frame.data.empty()) { + continue; + } + + // frame_nr != frame_index is expected when this batch is catching up after a + // transient delay. Only a non-monotonic emitted index is a real desync. + if (!session.note_emitted_index(encoded_frame.frame_index)) { + BOOST_LOG(warning) << "AMF emitted frame index regression: " << encoded_frame.frame_index + << " (submitted " << submitted_frame_nr << ")"; + } + + // Stamp every packet with the timestamps of the frame it actually carries, + // including earlier frames drained in the same catch-up batch. + const auto ts = session.take_frame_timestamps(encoded_frame.frame_index); + + auto packet = std::make_unique(std::move(encoded_frame.data), encoded_frame.frame_index, encoded_frame.idr); + packet->channel_data = channel_data; + packet->after_ref_frame_invalidation = encoded_frame.after_ref_frame_invalidation; + packet->frame_timestamp = ts.frame_timestamp; + packet->capture_timestamp = ts.capture_timestamp ? ts.capture_timestamp : ts.frame_timestamp; + packet->host_processing_timestamp = ts.host_processing_timestamp; + if (webrtc_stream::has_active_sessions()) { + webrtc_stream::submit_video_packet(*packet); + } + packet->packet_enqueue_timestamp = std::chrono::steady_clock::now(); + packets->raise(std::move(packet)); + } + } + + int encode_amf( + int64_t frame_nr, + amf_encode_session_t &session, + safe::mail_raw_t::queue_t &packets, + void *channel_data, + std::optional frame_timestamp, + std::optional capture_timestamp, + std::optional host_processing_timestamp + ) { + // Stash this frame's timestamps before submitting; the encoder is pipelined and + // emits an earlier frame, so the packet is stamped from this map by emitted index. + session.store_frame_timestamps((uint64_t) frame_nr, {frame_timestamp, capture_timestamp, host_processing_timestamp}); + + auto encode_result = session.encode_frames(frame_nr); + auto &encoded_frames = encode_result.frames; + if (!encode_result.input_accepted) { + session.discard_frame_timestamps((uint64_t) frame_nr); + } + if (encode_result.fatal || std::any_of(encoded_frames.begin(), encoded_frames.end(), [](const auto &frame) { return frame.fatal; })) { + BOOST_LOG(error) << "AMF encoder entered an unrecoverable state, requesting reinit"; + return -1; + } + if (encoded_frames.empty()) { + // No output this call (pipeline still filling or transient stall); not fatal. + // The stashed timestamps stay until this frame is actually emitted. + return 0; + } + + deliver_amf_frames(frame_nr, session, encoded_frames, packets, channel_data); + + return 0; + } + int encode( int64_t frame_nr, encode_session_t &session, @@ -2337,6 +2718,8 @@ namespace video { result = encode_avcodec(frame_nr, *avcodec_session, packets, channel_data, frame_timestamp, capture_timestamp, host_processing_timestamp); } else if (auto nvenc_session = dynamic_cast(&session)) { result = encode_nvenc(frame_nr, *nvenc_session, packets, channel_data, frame_timestamp, capture_timestamp, host_processing_timestamp); + } else if (auto amf_session = dynamic_cast(&session)) { + result = encode_amf(frame_nr, *amf_session, packets, channel_data, frame_timestamp, capture_timestamp, host_processing_timestamp); } encode_duration_logger.collect_and_log(std::chrono::duration(std::chrono::steady_clock::now() - encode_start).count()); @@ -2594,7 +2977,7 @@ namespace video { bool hardware = platform_formats->avcodec_base_dev_type != AV_HWDEVICE_TYPE_NONE; auto &video_format = encoder.codec_from_config(config); - if (!video_format[encoder_t::PASSED] || !disp->is_codec_supported(video_format.name, config)) { + if (!video_format[encoder_t::PASSED] || (disp && !disp->is_codec_supported(video_format.name, config))) { BOOST_LOG(error) << encoder.name << ": "sv << video_format.name << " mode not supported"sv; return nullptr; } @@ -2617,6 +3000,10 @@ namespace video { } auto colorspace = encode_device->colorspace; + if (!encode_device->initialize_hardware_device()) { + BOOST_LOG(error) << encoder.name << ": failed to initialize the hardware encode device"sv; + return nullptr; + } auto sw_fmt = (colorspace.bit_depth == 8 && config.chromaSamplingType == 0) ? platform_formats->avcodec_pix_fmt_8bit : (colorspace.bit_depth == 8 && config.chromaSamplingType == 1) ? platform_formats->avcodec_pix_fmt_yuv444_8bit : (colorspace.bit_depth == 10 && config.chromaSamplingType == 0) ? platform_formats->avcodec_pix_fmt_10bit : @@ -2817,6 +3204,11 @@ namespace video { [&](const std::function &v) { av_dict_set_int(&options, option.name.c_str(), v(), 0); }, + [&](const encoder_t::option_t::optional_int_function_t &v) { + if (const auto value = v.evaluate()) { + av_dict_set_int(&options, option.name.c_str(), *value, 0); + } + }, [&](const std::string &v) { av_dict_set(&options, option.name.c_str(), v.c_str(), 0); }, @@ -2996,35 +3388,465 @@ namespace video { return std::make_unique(std::move(encode_device)); } - std::unique_ptr make_encode_session(platf::display_t *disp, const encoder_t &encoder, const config_t &config, int width, int height, std::unique_ptr encode_device) { + using initialization_cancel_t = std::function; + + bool acquire_amf_initialization_fence_until( + std::chrono::steady_clock::time_point deadline, + const initialization_cancel_t &cancelled) { + while (!cancelled() && std::chrono::steady_clock::now() < deadline) { + const auto remaining = deadline - std::chrono::steady_clock::now(); + const auto poll_interval = std::chrono::duration_cast(50ms); + if (native_amf_lifecycle_gate.begin_initialization_for(std::min(remaining, poll_interval))) return true; + if (native_amf_lifecycle_gate.is_quarantined()) return false; + } + return false; + } + + std::unique_ptr make_amf_encode_session( + const config_t &client_config, + std::unique_ptr encode_device, + std::chrono::steady_clock::time_point deadline, + const initialization_cancel_t &cancelled, + bool &operation_cancelled, + bool &gate_contended) { + operation_cancelled = false; + gate_contended = false; + // Native AMF can wedge inside Init/Terminate on a damaged driver. Run the + // entire initialization attempt on an owning worker so probes, failed init, + // and real-session fallback all regain control within one watchdog interval. + // The caller already keeps the display alive for this synchronous wait. Do + // not also leave it inside a worker that may be detached after the watchdog: + // that stale owner would permanently block the next display generation. + if (native_amf_lifecycle_gate.is_quarantined()) { + BOOST_LOG(error) << "AMF: native runtime is quarantined after a watchdog timeout; refusing to re-enter the AMD runtime"sv; + return nullptr; + } + + const auto gate_deadline = std::min(deadline, std::chrono::steady_clock::now() + 1s); + if (!acquire_amf_initialization_fence_until(gate_deadline, cancelled)) { + operation_cancelled = cancelled(); + gate_contended = !operation_cancelled && !native_amf_lifecycle_gate.is_quarantined(); + BOOST_LOG(warning) << "AMF: native initialization could not acquire the AMD runtime fence before the session deadline"sv; + return nullptr; + } + if (cancelled()) { + operation_cancelled = true; + native_amf_lifecycle_gate.cancel_initialization(); + return nullptr; + } + if (deadline - std::chrono::steady_clock::now() < 1s) { + gate_contended = true; + native_amf_lifecycle_gate.cancel_initialization(); + BOOST_LOG(warning) << "AMF: native initialization skipped because gate contention left less than one second of vendor budget"sv; + return nullptr; + } + + std::optional resolved_hdr_metadata; + if (colorspace_is_hdr(encode_device->colorspace)) { + if (encode_device->hdr_metadata_valid) { + const auto &hdr_metadata = encode_device->hdr_metadata; + amf::amf_hdr_metadata amf_metadata; + for (int i = 0; i < 3; i++) { + amf_metadata.displayPrimaries[i].x = hdr_metadata.displayPrimaries[i].x; + amf_metadata.displayPrimaries[i].y = hdr_metadata.displayPrimaries[i].y; + } + amf_metadata.whitePoint.x = hdr_metadata.whitePoint.x; + amf_metadata.whitePoint.y = hdr_metadata.whitePoint.y; + amf_metadata.maxDisplayLuminance = hdr_metadata.maxDisplayLuminance; + amf_metadata.minDisplayLuminance = hdr_metadata.minDisplayLuminance; + amf_metadata.maxContentLightLevel = hdr_metadata.maxContentLightLevel; + amf_metadata.maxFrameAverageLightLevel = hdr_metadata.maxFrameAverageLightLevel; + resolved_hdr_metadata = amf_metadata; + } else { + BOOST_LOG(warning) << "AMF: no resolved HDR metadata is available for the native bitstream"sv; + } + } + + auto display_lease = encode_device->release_display_lease_for_initialization(); + auto handoff = std::make_shared>>(); + const auto colorspace = encode_device->colorspace; + std::thread initialization_thread; + try { + initialization_thread = std::thread { + [encode_device = std::move(encode_device), client_config, colorspace, resolved_hdr_metadata, handoff]() mutable { + bool handed_off_to_caller = false; + auto initialization_gate = util::fail_guard([&handed_off_to_caller]() { + if (!handed_off_to_caller) { + native_amf_lifecycle_gate.cancel_initialization(); + } + }); + auto owned_encode_device = std::move(encode_device); + try { + if (!owned_encode_device->initialize_hardware_device() || + !owned_encode_device->init_encoder(client_config, colorspace) || + !owned_encode_device->finish_encoder_initialization(client_config, colorspace)) { + // Failure destruction is part of this same watchdog interval. Only + // publish failure after the D3D/AMF resources are gone, so legacy + // fallback cannot race a still-unwinding native runtime. + owned_encode_device.reset(); + handoff->publish(nullptr); + return; + } + if (resolved_hdr_metadata && owned_encode_device->amf && + !owned_encode_device->amf->set_hdr_metadata(resolved_hdr_metadata)) { + BOOST_LOG(warning) << "AMF: HDR stream will continue without mastering metadata"sv; + } + handed_off_to_caller = handoff->publish(std::move(owned_encode_device)); + } catch (...) { + owned_encode_device.reset(); + handoff->publish(nullptr); + } + } + }; + } catch (const std::system_error &err) { + native_amf_lifecycle_gate.cancel_initialization(); + BOOST_LOG(error) << "AMF: could not start native initialization worker: " << err.what(); + return nullptr; + } + + bool handoff_cancelled = false; + auto accepted = handoff->accept_until(deadline, cancelled, &handoff_cancelled); + if (!accepted) { + operation_cancelled = handoff_cancelled; + if (!handoff_cancelled) { + native_amf_lifecycle_gate.quarantine_initialization(); + BOOST_LOG(error) << "AMF: native encoder initialization exceeded the vendor deadline; worker will reap ownership"sv; + } else { + BOOST_LOG(info) << "AMF: native encoder initialization cancelled; worker will reap ownership without quarantining AMD"sv; + } + initialization_thread.detach(); + return nullptr; + } + + encode_device = std::move(*accepted); + if (!encode_device) { + initialization_thread.join(); + return nullptr; + } + initialization_thread.join(); + + if (!native_amf_lifecycle_gate.finish_initialization()) { + BOOST_LOG(error) << "AMF: initialization completed after a teardown/quarantine fence; discarding the native device"sv; + const auto gate_deadline = std::chrono::steady_clock::now() + 5s; + if (!native_amf_lifecycle_gate.begin_teardown_until(gate_deadline)) { + (void) encode_device.release(); + return nullptr; + } + // Fresh destruction budget once the fence is held — gate contention is + // not driver teardown time. + const bool discarded = amf::lifecycle::run_with_timeout( + [encode_device = std::move(encode_device)]() mutable { + encode_device.reset(); + }, + 5s); + native_amf_lifecycle_gate.finish_teardown(discarded); + return nullptr; + } + encode_device->restore_display_lease_after_initialization(std::move(display_lease)); + + return std::make_unique(std::move(encode_device)); + } + + std::unique_ptr make_encode_session( + platf::display_t *disp, + const encoder_t &encoder, + const config_t &config, + int width, + int height, + std::unique_ptr encode_device, + std::chrono::steady_clock::time_point initialization_deadline = std::chrono::steady_clock::time_point::max(), + initialization_cancel_t cancelled = {}, + bool *operation_cancelled_out = nullptr, + bool *gate_contended_out = nullptr) { + if (!cancelled) cancelled = []() { return false; }; + if (operation_cancelled_out) *operation_cancelled_out = false; + if (gate_contended_out) *gate_contended_out = false; + bool operation_cancelled = false; + bool gate_contended = false; if (dynamic_cast(encode_device.get())) { auto avcodec_encode_device = boost::dynamic_pointer_cast(std::move(encode_device)); return make_avcodec_encode_session(disp, encoder, config, width, height, std::move(avcodec_encode_device)); } else if (dynamic_cast(encode_device.get())) { auto nvenc_encode_device = boost::dynamic_pointer_cast(std::move(encode_device)); return make_nvenc_encode_session(config, std::move(nvenc_encode_device)); + } else if (dynamic_cast(encode_device.get())) { + auto amf_encode_device = boost::dynamic_pointer_cast(std::move(encode_device)); + auto session = make_amf_encode_session( + config, std::move(amf_encode_device), initialization_deadline, cancelled, + operation_cancelled, gate_contended); + if (operation_cancelled_out) *operation_cancelled_out = operation_cancelled; + if (gate_contended_out) *gate_contended_out = gate_contended; + return session; } return nullptr; } - void encode_run( + std::unique_ptr make_encode_device( + platf::display_t &disp, + const encoder_t &encoder, + const config_t &config, + hdr_latch_t *hdr_latch, + bool deferred_avcodec); + + void abandon_quarantined_session( + std::unique_ptr &session, + std::shared_ptr display_lease = {}) { + // The AMD runtime is already known to have an abandoned vendor call. Running + // another destructor can overlap it and hang/crash. Intentionally retain the + // resources until process termination without registering a static destructor. + (void) session.release(); + // The leaked encoder/device is detached from capture. Releasing this external + // lease is required so display reinitialization can reach use_count()==1. + display_lease.reset(); + } + +#ifdef _WIN32 + struct legacy_amf_session_bundle_t { + std::unique_ptr session; + bool hdr_metadata_valid = false; + SS_HDR_METADATA hdr_metadata {}; + hdr_latch_t hdr_latch {}; + }; + + std::optional make_legacy_amf_session_bounded( + std::shared_ptr disp, + const config_t &config, + int width, + int height, + hdr_latch_t *hdr_latch, + std::chrono::steady_clock::time_point deadline, + const initialization_cancel_t &cancelled, + bool &operation_cancelled, + bool &gate_contended) { + operation_cancelled = false; + gate_contended = false; + if (!disp || native_amf_lifecycle_gate.is_quarantined()) { + return std::nullopt; + } + + const auto gate_deadline = std::min(deadline, std::chrono::steady_clock::now() + 1s); + if (!acquire_amf_initialization_fence_until(gate_deadline, cancelled)) { + operation_cancelled = cancelled(); + gate_contended = !operation_cancelled && !native_amf_lifecycle_gate.is_quarantined(); + BOOST_LOG(error) << "AMF: legacy initialization could not acquire the AMD runtime fence before the session deadline"sv; + return std::nullopt; + } + if (cancelled()) { + operation_cancelled = true; + native_amf_lifecycle_gate.cancel_initialization(); + return std::nullopt; + } + if (deadline - std::chrono::steady_clock::now() < 1s) { + gate_contended = true; + native_amf_lifecycle_gate.cancel_initialization(); + BOOST_LOG(warning) << "AMF: legacy initialization skipped because gate contention left less than one second of vendor budget"sv; + return std::nullopt; + } + + auto local_latch = hdr_latch ? *hdr_latch : hdr_latch_t {}; + auto base_device = make_encode_device(*disp, amdvce_legacy, config, &local_latch, true); + auto prepared_device = boost::dynamic_pointer_cast(std::move(base_device)); + if (!prepared_device) { + native_amf_lifecycle_gate.cancel_initialization(); + return std::nullopt; + } + + legacy_amf_session_bundle_t prepared_bundle; + prepared_bundle.hdr_metadata_valid = prepared_device->hdr_metadata_valid; + prepared_bundle.hdr_metadata = prepared_device->hdr_metadata; + prepared_bundle.hdr_latch = local_latch; + auto display_lease = prepared_device->release_display_lease_for_initialization(); + auto handoff = std::make_shared>(); + std::thread initialization_thread; + try { + initialization_thread = std::thread { + [config, width, height, prepared_device = std::move(prepared_device), prepared_bundle = std::move(prepared_bundle), handoff]() mutable { + bool gate_finished = false; + auto gate_cleanup = util::fail_guard([&gate_finished]() { + if (!gate_finished) { + native_amf_lifecycle_gate.cancel_initialization(); + } + }); + try { + if (prepared_device->is_codec_supported(amdvce_legacy.codec_from_config(config).name, config)) { + prepared_bundle.session = make_encode_session( + nullptr, amdvce_legacy, config, width, height, std::move(prepared_device)); + } + const bool initialized = static_cast(prepared_bundle.session); + const bool accepted = handoff->publish(std::move(prepared_bundle)); + gate_finished = initialized && accepted; + } catch (...) { + handoff->publish(legacy_amf_session_bundle_t {}); + } + } + }; + } catch (const std::system_error &err) { + native_amf_lifecycle_gate.cancel_initialization(); + BOOST_LOG(error) << "AMF: could not start legacy initialization worker: " << err.what(); + return std::nullopt; + } + + bool handoff_cancelled = false; + auto accepted = handoff->accept_until(deadline, cancelled, &handoff_cancelled); + if (!accepted) { + operation_cancelled = handoff_cancelled; + if (!handoff_cancelled) { + native_amf_lifecycle_gate.quarantine_initialization(); + BOOST_LOG(error) << "AMF: complete legacy D3D/FFmpeg initialization exceeded the vendor deadline; worker will reap ownership"sv; + } else { + BOOST_LOG(info) << "AMF: legacy initialization cancelled; worker will reap ownership without quarantining AMD"sv; + } + initialization_thread.detach(); + return std::nullopt; + } + + initialization_thread.join(); + auto bundle = std::move(*accepted); + if (!bundle.session) return std::nullopt; + if (!native_amf_lifecycle_gate.finish_initialization()) { + // The caller owns the accepted value, but publication was fenced. Avoid + // entering a quarantined runtime from this thread. + bundle.session.release(); + return std::nullopt; + } + if (auto *legacy_session = dynamic_cast(bundle.session.get())) { + legacy_session->restore_display_lease_after_initialization(std::move(display_lease)); + } + if (hdr_latch) *hdr_latch = bundle.hdr_latch; + return bundle; + } + + bool destroy_legacy_amf_session_bounded(std::unique_ptr &session, std::string_view reason) { + if (!session) return true; + auto display_lease = static_cast(session.get())->release_display_lease_for_driver_work(); + const auto gate_deadline = std::chrono::steady_clock::now() + 5s; + if (!native_amf_lifecycle_gate.begin_teardown_until(gate_deadline)) { + abandon_quarantined_session(session, std::move(display_lease)); + return false; + } + // Fresh destruction budget once the fence is held — gate contention is + // not driver teardown time. + const bool completed = amf::lifecycle::run_with_timeout( + [session = std::move(session), display_lease = std::move(display_lease)]() mutable { + std::lock_guard lock {encode_session_teardown_mutex}; + session.reset(); + (void) display_lease; + }, + 5s); + native_amf_lifecycle_gate.finish_teardown(completed); + if (!completed) { + BOOST_LOG(error) << "AMF: legacy " << reason << " teardown exceeded 5 seconds; quarantining AMD encoding"sv; + } + return completed; + } +#endif + + bool destroy_encode_session_bounded(std::unique_ptr &session, std::string_view reason) { + if (!session) { + return true; + } + + const bool native_amf_session = dynamic_cast(session.get()) != nullptr; + std::shared_ptr display_lease; + if (native_amf_session) { + // Moving the shared_ptr itself cannot enter the D3D driver. Keep the old + // display alive inside the watchdog worker for every potentially blocking + // ClearState/COM/AMF destruction step. If it wedges, both the device and + // display lease remain quarantined together; releasing the display first + // would violate the shared-resource lifetime ordering. + display_lease = static_cast(session.get())->release_display_lease_for_driver_work(); + } + auto owned_session = std::move(session); + if (native_amf_session) { + // Gate contention is not driver teardown time. A healthy initialization is + // allowed its own watchdog interval, so acquire the fence before starting + // the independent five-second destruction deadline. + const auto gate_deadline = std::chrono::steady_clock::now() + 5s; + if (!native_amf_lifecycle_gate.begin_teardown_until(gate_deadline)) { + abandon_quarantined_session(owned_session, std::move(display_lease)); + return false; + } + // Fresh destruction budget once the fence is held — gate contention is + // not driver teardown time. + const bool completed = amf::lifecycle::run_with_timeout( + [owned_session = std::move(owned_session), display_lease = std::move(display_lease)]() mutable { + owned_session.reset(); + (void) display_lease; + }, + 5s); + native_amf_lifecycle_gate.finish_teardown(completed); + if (!completed) { + BOOST_LOG(error) << "Encoder " << reason << " teardown exceeded 5 seconds; abandoning that session"sv; + } + return completed; + } + const bool completed = amf::lifecycle::run_with_timeout( + [owned_session = std::move(owned_session)]() mutable { + std::lock_guard lock {encode_session_teardown_mutex}; + owned_session.reset(); + }, + 5s); + if (!completed) { + BOOST_LOG(error) << "Encoder " << reason << " teardown exceeded 5 seconds; abandoning that session"sv; + } + return completed; + } + + enum class encode_run_result_e { + completed, + native_amf_failed, + temporarily_busy, + initialization_failed, + }; + + encode_run_result_e encode_run( int &frame_nr, // Store progress of the frame number safe::mail_t mail, img_event_t images, config_t &config, std::shared_ptr disp, std::unique_ptr encode_device, + std::unique_ptr prepared_session, safe::signal_t &reinit_event, const encoder_t &encoder, + hdr_latch_t *hdr_latch, void *channel_data, + std::chrono::steady_clock::time_point initialization_deadline, + initialization_cancel_t initialization_cancelled, std::optional &last_hdr_info, rtx_hdr_metadata_refresh_state_t &rtx_hdr_metadata_refresh ) { - auto session = make_encode_session(disp.get(), encoder, config, disp->width, disp->height, std::move(encode_device)); + const encoder_t *session_encoder = &encoder; + bool initialization_was_cancelled = false; + bool initialization_gate_contended = false; + auto session = prepared_session ? + std::move(prepared_session) : + make_encode_session( + disp.get(), encoder, config, disp->width, disp->height, + std::move(encode_device), initialization_deadline, initialization_cancelled, + &initialization_was_cancelled, &initialization_gate_contended); +#ifdef _WIN32 + if (initialization_was_cancelled) return encode_run_result_e::completed; + if (!session && &encoder == &amdvce) { + BOOST_LOG(error) << "AMF: native session initialization failed; refusing silent amdvce_legacy fallback"sv; + } +#endif if (!session) { - return; + if (initialization_gate_contended) return encode_run_result_e::temporarily_busy; + return encode_run_result_e::initialization_failed; } + // One RTTI lookup for the whole session — the pointer stays valid until the + // fail_guard teardown moves the session out after the encode loop exits. + auto *const native_session = dynamic_cast(session.get()); + const bool native_amf_session = native_session != nullptr; +#ifdef _WIN32 + const bool legacy_amf_session = session_encoder == &amdvce_legacy; +#else + const bool legacy_amf_session = false; +#endif + bool native_amf_runtime_failed = false; + const auto session_encoder_flags = session_encoder->flags; auto shutdown_event = mail->event(mail::shutdown); @@ -3036,7 +3858,7 @@ namespace video { // to restart encoding as soon as possible. For cases where the NVENC driver // hang occurs, this thread may probably never exit, but it will allow // streaming to continue without requiring a full restart of Sunshine. - auto fail_guard = util::fail_guard([&encoder, &session, &force_sync_teardown, &reinit_event, shutdown_event] { + auto fail_guard = util::fail_guard([session_encoder_flags, legacy_amf_session, &session, &force_sync_teardown, &reinit_event, shutdown_event] { const bool shutdown_teardown = shutdown_event && shutdown_event->peek(); // A display reinit (resolution/HDR/colorspace change, e.g. alt-tabbing a game on a // virtual display) frees the shared capture surfaces this encoder's device has open. @@ -3047,16 +3869,65 @@ namespace video { // whenever a reinit is in progress so this encoder releases its surfaces before the // capture side frees them. const bool sync_teardown = force_sync_teardown || shutdown_teardown || reinit_event.peek(); - if ((encoder.flags & ASYNC_TEARDOWN) && !sync_teardown) { - std::thread encoder_teardown_thread {[session = std::move(session)]() mutable { + if ((session_encoder_flags & ASYNC_TEARDOWN) && !sync_teardown) { + const bool native_amf_session = dynamic_cast(session.get()) != nullptr; + auto display_lease = native_amf_session ? + static_cast(session.get())->release_display_lease_for_driver_work() : + legacy_amf_session ? + static_cast(session.get())->release_display_lease_for_driver_work() : + nullptr; + std::thread encoder_teardown_thread {[session = std::move(session), native_amf_session, legacy_amf_session, display_lease = std::move(display_lease)]() mutable { BOOST_LOG(info) << "Starting async encoder teardown"; - std::lock_guard lg {encode_session_teardown_mutex}; - session.reset(); + if (native_amf_session) { + // Supervise the vendor destructor from this already-asynchronous path. + // On timeout, the inner worker retains ownership while future sessions + // refuse to re-enter either AMF backend instead of accumulating more + // calls into the same wedged AMD runtime. + const auto gate_deadline = std::chrono::steady_clock::now() + 5s; + if (!native_amf_lifecycle_gate.begin_teardown_until(gate_deadline)) { + abandon_quarantined_session(session, std::move(display_lease)); + return; + } + // Fresh destruction budget once the fence is held — gate contention + // is not driver teardown time. + const bool completed = amf::lifecycle::run_with_timeout( + [session = std::move(session), display_lease = std::move(display_lease)]() mutable { + session.reset(); + (void) display_lease; + }, + 5s); + native_amf_lifecycle_gate.finish_teardown(completed); + if (!completed) { + BOOST_LOG(error) << "AMF: async teardown exceeded 5 seconds; quarantining native AMF until host restart"sv; + } + } else if (legacy_amf_session) { + const auto gate_deadline = std::chrono::steady_clock::now() + 5s; + if (!native_amf_lifecycle_gate.begin_teardown_until(gate_deadline)) { + abandon_quarantined_session(session, std::move(display_lease)); + return; + } + // Fresh destruction budget once the fence is held — gate contention + // is not driver teardown time. + const bool completed = amf::lifecycle::run_with_timeout( + [session = std::move(session), display_lease = std::move(display_lease)]() mutable { + std::lock_guard lg {encode_session_teardown_mutex}; + session.reset(); + (void) display_lease; + }, + 5s); + native_amf_lifecycle_gate.finish_teardown(completed); + if (!completed) { + BOOST_LOG(error) << "AMF: legacy async teardown exceeded 5 seconds; quarantining AMD encoding until host restart"sv; + } + } else { + std::lock_guard lg {encode_session_teardown_mutex}; + session.reset(); + } BOOST_LOG(info) << "Async encoder teardown complete"; }}; encoder_teardown_thread.detach(); } else { - if ((encoder.flags & ASYNC_TEARDOWN) && sync_teardown) { + if ((session_encoder_flags & ASYNC_TEARDOWN) && sync_teardown) { BOOST_LOG(debug) << "Using synchronous encoder teardown during " << (shutdown_teardown ? "shutdown"sv : "capture reinit"sv); } @@ -3065,23 +3936,54 @@ namespace video { // scope exit. During a capture reinit both video threads reach this point at the same // moment; unserialized concurrent NVENC/D3D11 device destruction crashes the NVIDIA UMD // (access violation in nvwgf2umx during cross-device shared-resource dependency cleanup). - if (dynamic_cast(session.get())) { - // A wedged driver can block avcodec session destruction indefinitely (vibeshine#187: - // AMF teardown stuck inside amfrtdrv64). On the shutdown path that blocks - // videoThread.join() until the 10-second session watchdog kills the whole host, so - // run the destruction on a helper thread and abandon it if it overruns. An abandoned - // session leaks (and keeps the teardown mutex held), but the stream host survives. + const bool native_amf_session = dynamic_cast(session.get()) != nullptr; + if (native_amf_session) { + destroy_encode_session_bounded(session, "native AMF"sv); + } else if (dynamic_cast(session.get())) { + // A wedged driver can block avcodec session destruction indefinitely. + // During shutdown that blocks videoThread.join(); + // during reinit it prevents capture recovery. Run destruction on a helper + // thread and abandon it if it overruns. An abandoned + // session leaks, but the stream host survives. // NVENC keeps the fully synchronous teardown: its driver waits are already bounded // (nvenc_base) and its teardown-vs-surface-free ordering is load-bearing // (VIDEO_MEMORY_MANAGEMENT_INTERNAL 0x10e bugcheck). std::promise done; auto done_future = done.get_future(); - std::thread teardown_thread {[session = std::move(session), done = std::move(done)]() mutable { + auto legacy_display_lease = legacy_amf_session ? + static_cast(session.get())->release_display_lease_for_driver_work() : + nullptr; + if (legacy_amf_session) { + const auto gate_deadline = std::chrono::steady_clock::now() + 5s; + if (!native_amf_lifecycle_gate.begin_teardown_until(gate_deadline)) { + abandon_quarantined_session(session, std::move(legacy_display_lease)); + return; + } + std::thread teardown_thread {[session = std::move(session), done = std::move(done), display_lease = std::move(legacy_display_lease)]() mutable { + std::lock_guard lg {encode_session_teardown_mutex}; + session.reset(); + (void) display_lease; + done.set_value(); + }}; + // Fresh destruction budget once the fence is held — gate contention + // is not driver teardown time. + const bool completed = done_future.wait_for(5s) == std::future_status::ready; + native_amf_lifecycle_gate.finish_teardown(completed); + if (completed) teardown_thread.join(); + else { + BOOST_LOG(error) << "Encoder teardown did not finish within its 5 second destruction budget; abandoning the session"sv; + teardown_thread.detach(); + } + return; + } + std::thread teardown_thread {[session = std::move(session), done = std::move(done), display_lease = std::move(legacy_display_lease)]() mutable { std::lock_guard lg {encode_session_teardown_mutex}; session.reset(); + (void) display_lease; done.set_value(); }}; - if (done_future.wait_for(5s) == std::future_status::ready) { + const bool completed = done_future.wait_for(5s) == std::future_status::ready; + if (completed) { teardown_thread.join(); } else { BOOST_LOG(error) << "Encoder teardown did not finish within 5 seconds; abandoning the session to keep the stream host alive"sv; @@ -3094,6 +3996,11 @@ namespace video { } }); + auto native_amf_failure = [&]() { + force_sync_teardown = native_amf_session; + return native_amf_session ? encode_run_result_e::native_amf_failed : encode_run_result_e::completed; + }; + if (config.encodingFramerate <= 0) { const int fallback_fps = config.framerate > 0 ? config.framerate * 1000 : 60000; BOOST_LOG(warning) << "Encoding framerate missing; falling back to " << fallback_fps; @@ -3121,23 +4028,68 @@ namespace video { // in a separate scope. auto dummy_img = disp->alloc_img(); if (!dummy_img || disp->dummy_img(dummy_img.get()) || session->convert(*dummy_img)) { - return; + return native_amf_failure(); } } if (config.input_only) { BOOST_LOG(info) << "Input only session, video will not be captured."sv; - // Encode the dummy img only once + // Native AMF is asynchronous and may initially reject the dummy surface + // with backpressure. Resubmit the same logical frame until it is accepted, + // then keep draining until that accepted input actually emits. const auto now = std::chrono::steady_clock::now(); - if (encode(frame_nr++, *session, packets, channel_data, now, now, now)) { + const auto dummy_frame_index = static_cast(frame_nr++); + if (encode(dummy_frame_index, *session, packets, channel_data, now, now, now)) { BOOST_LOG(error) << "Could not encode dummy video packet"sv; - return; + return native_amf_failure(); + } + + if (auto *amf_session = dynamic_cast(session.get())) { + const auto acceptance_deadline = std::chrono::steady_clock::now() + 2s; + bool dummy_accepted = amf_session->was_last_input_accepted(); + while (!dummy_accepted && std::chrono::steady_clock::now() < acceptance_deadline) { + auto delayed = amf_session->drain_frames(10ms); + if (delayed.fatal || std::any_of(delayed.frames.begin(), delayed.frames.end(), [](const auto &frame) { return frame.fatal; })) { + BOOST_LOG(error) << "AMF failed while accepting the input-only dummy packet"sv; + return native_amf_failure(); + } + deliver_amf_frames(dummy_frame_index, *amf_session, delayed.frames, packets, channel_data); + if (encode(dummy_frame_index, *session, packets, channel_data, now, now, now)) { + BOOST_LOG(error) << "Could not resubmit the AMF input-only dummy packet"sv; + return native_amf_failure(); + } + dummy_accepted = amf_session->was_last_input_accepted(); + } + if (!dummy_accepted) { + BOOST_LOG(error) << "AMF did not accept the input-only dummy packet within 2 seconds"sv; + return native_amf_failure(); + } + + // A PA encoder deliberately holds its final lookahead frame until AMF Drain + // marks end-of-input. Input-only sessions have no future live frame to do it. + if (!amf_session->begin_drain()) { + BOOST_LOG(error) << "Could not begin draining the AMF input-only session"sv; + return native_amf_failure(); + } + const auto drain_deadline = std::chrono::steady_clock::now() + 2s; + while (!amf_session->has_emitted_frame(dummy_frame_index) && std::chrono::steady_clock::now() < drain_deadline) { + auto delayed = amf_session->drain_frames(50ms); + if (delayed.fatal || std::any_of(delayed.frames.begin(), delayed.frames.end(), [](const auto &frame) { return frame.fatal; })) { + BOOST_LOG(error) << "AMF failed while draining the input-only dummy packet"sv; + return native_amf_failure(); + } + deliver_amf_frames(dummy_frame_index, *amf_session, delayed.frames, packets, channel_data); + } + if (!amf_session->has_emitted_frame(dummy_frame_index)) { + BOOST_LOG(error) << "AMF did not emit the input-only dummy packet within 2 seconds"sv; + return native_amf_failure(); + } } while (true) { if (shutdown_event->peek() || !images->running() || (reinit_event.peek())) { - return; + return encode_run_result_e::completed; } else { std::this_thread::sleep_for(300ms); } @@ -3157,6 +4109,7 @@ namespace video { uint64_t pop_timeouts = 0; uint64_t gate_skipped = 0; uint64_t encoded = 0; + uint64_t dropped_submissions = 0; std::chrono::steady_clock::time_point last_log = std::chrono::steady_clock::now(); } loop_stats; @@ -3168,6 +4121,7 @@ namespace video { << " pop_timeouts=" << loop_stats.pop_timeouts << " gate_skipped=" << loop_stats.gate_skipped << " encoded=" << loop_stats.encoded + << " dropped_submissions=" << loop_stats.dropped_submissions << " frame_nr=" << frame_nr; loop_stats = {}; loop_stats.last_log = now; @@ -3230,7 +4184,79 @@ namespace video { // Encode at a minimum FPS to avoid image quality issues with static content if (!requested_idr_frame || images->peek()) { - if (auto img = images->pop(max_frametime)) { + auto image_wait_budget = max_frametime; + if (bootstrap_state.should_encode_placeholder()) { + // Prime a lookahead encoder immediately; waiting for minimum FPS between + // bootstrap placeholders needlessly delays the first packet. + image_wait_budget = decltype(max_frametime)::zero(); + } + + const auto image_wait_started = std::chrono::steady_clock::now(); + if (auto *amf_session = native_session; + amf_session && image_wait_budget > decltype(max_frametime)::zero()) { + // Never hold an already-completed packet behind conversion/submission + // of a newer image. This zero-wait drain also runs during continuous + // motion, where images->peek() remains true and the sparse loop below + // intentionally yields immediately. + if (amf_session->has_completed_output()) { + auto ready = amf_session->drain_frames(0ms); + if (ready.fatal || std::any_of(ready.frames.begin(), ready.frames.end(), [](const auto &frame) { return frame.fatal; })) { + BOOST_LOG(error) << "AMF failed while delivering ready output"sv; + native_amf_runtime_failed = true; + break; + } + deliver_amf_frames(frame_nr, *amf_session, ready.frames, packets, channel_data); + } + + // Wait for either the next capture or output that became due with the + // previous native submission. This preserves high-refresh pipelining + // when another image arrives, while delivering the final moving frame + // promptly when WGC transitions to a sparse/static cadence. + const auto wait_started = std::chrono::steady_clock::now(); + const auto delivery_grace = std::min( + std::chrono::duration_cast(image_wait_budget), + std::chrono::duration_cast(32ms)); + const auto delivery_deadline = wait_started + delivery_grace; + bool delayed_output_failed = false; + while (!images->peek() && amf_session->has_output_due() && + std::chrono::steady_clock::now() < delivery_deadline) { + auto delayed = amf_session->drain_frames(1ms); + if (delayed.fatal || std::any_of(delayed.frames.begin(), delayed.frames.end(), [](const auto &frame) { return frame.fatal; })) { + BOOST_LOG(error) << "AMF failed while delivering sparse-capture output"sv; + delayed_output_failed = true; + break; + } + deliver_amf_frames(frame_nr, *amf_session, delayed.frames, packets, channel_data); + } + if (delayed_output_failed) { + native_amf_runtime_failed = true; + break; + } + + const auto waited = std::chrono::steady_clock::now() - wait_started; + image_wait_budget = waited < image_wait_budget ? + image_wait_budget - waited : + decltype(max_frametime)::zero(); + } + + if (auto *amf_session = native_session; + amf_session && amf_session->has_retained_preanalysis_tail()) { + // Check after sparse draining as well as before the final image wait: + // QueryOutput may have completed during the drain and exposed PA's + // retained tail. The one-shot session gate prevents the duplicate used + // here from arming an endless negotiated-FPS duplicate train. + const auto tail_flush_budget = std::chrono::duration_cast( + encode_frame_threshold + 1ms); + const auto tail_started = amf_session->input_accepted_at().value_or(image_wait_started); + const auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - tail_started); + const auto remaining_tail_budget = elapsed < tail_flush_budget ? + tail_flush_budget - elapsed : + decltype(image_wait_budget)::zero(); + image_wait_budget = std::min(image_wait_budget, remaining_tail_budget); + } + + if (auto img = images->pop(image_wait_budget)) { placeholder_input = is_placeholder_capture_image(*img); if (placeholder_input) { ++loop_stats.popped_placeholder; @@ -3247,6 +4273,7 @@ namespace video { } if (session->convert(*img)) { BOOST_LOG(error) << "Could not convert image"sv; + native_amf_runtime_failed = native_amf_session; break; } @@ -3300,12 +4327,26 @@ namespace video { if (encode(frame_nr++, *session, packets, channel_data, frame_timestamp, capture_timestamp, host_processing_timestamp)) { BOOST_LOG(error) << "Could not encode video packet"sv; + native_amf_runtime_failed = native_amf_session; break; } ++loop_stats.encoded; + // A dropped submission leaves a hole in the wire frameIndex sequence, which + // the client reads as loss. Reusing the index instead is NOT safe: several + // recoverable conditions (a minimum-FPS duplicate finding no free surface, + // exhausted submit backpressure) report "not accepted" indefinitely, and + // withholding the increment turns those into a permanently frozen stream. + // Count the drops so the condition stays observable. + if (native_session && !native_session->was_last_input_accepted()) { + ++loop_stats.dropped_submissions; + } + if (placeholder_input) { - bootstrap_state.placeholder_encoded = true; + // PA can accept the first placeholder while intentionally emitting + // nothing until its lookahead is primed. Keep submitting placeholders + // until a packet actually exists; accepted input is not packet delivery. + bootstrap_state.placeholder_encoded = !native_session || native_session->has_emitted_any_frame(); } session->request_normal_frame(); @@ -3314,6 +4355,12 @@ namespace video { // This is useful for KVM switch scenarios where mouse may disappear during streaming platf::enable_mouse_keys(); } + if (native_amf_runtime_failed) { + force_sync_teardown = true; + } + return native_amf_runtime_failed ? + encode_run_result_e::native_amf_failed : + encode_run_result_e::completed; } input::touch_port_t make_port(platf::display_t *display, const config_t &config) { @@ -3362,9 +4409,21 @@ namespace video { }; } - std::unique_ptr make_encode_device(platf::display_t &disp, const encoder_t &encoder, const config_t &config, hdr_latch_t *hdr_latch = nullptr) { + std::unique_ptr make_encode_device( + platf::display_t &disp, + const encoder_t &encoder, + const config_t &config, + hdr_latch_t *hdr_latch = nullptr, + bool deferred_avcodec = false) { std::unique_ptr result; +#ifdef _WIN32 + if (&encoder == &amdvce_legacy && native_amf_lifecycle_gate.is_quarantined()) { + BOOST_LOG(error) << "AMF: refusing legacy initialization while the AMD runtime is quarantined"sv; + return nullptr; + } +#endif + const bool display_is_hdr = disp.is_hdr(); bool hdr_display = display_is_hdr; #ifdef SUNSHINE_ENABLE_NV_TRUEHDR @@ -3429,9 +4488,13 @@ namespace video { } if (dynamic_cast(encoder.platform_formats.get())) { - result = disp.make_avcodec_encode_device(pix_fmt); + result = deferred_avcodec ? + disp.make_deferred_avcodec_encode_device(pix_fmt) : + disp.make_avcodec_encode_device(pix_fmt); } else if (dynamic_cast(encoder.platform_formats.get())) { result = disp.make_nvenc_encode_device(pix_fmt); + } else if (dynamic_cast(encoder.platform_formats.get())) { + result = disp.make_amf_encode_device(pix_fmt); } if (result) { @@ -3472,27 +4535,59 @@ namespace video { return result; } - std::optional make_synced_session(platf::display_t *disp, const encoder_t &encoder, platf::img_t &img, sync_session_ctx_t &ctx) { + std::optional make_synced_session(std::shared_ptr disp, const encoder_t &encoder, platf::img_t &img, sync_session_ctx_t &ctx) { sync_session_t encode_session; encode_session.ctx = &ctx; + const auto initialization_deadline = std::chrono::steady_clock::now() + 5s; + initialization_cancel_t initialization_cancelled = [&]() { + return ctx.shutdown_event && ctx.shutdown_event->peek(); + }; - auto encode_device = make_encode_device(*disp, encoder, ctx.config, &ctx.hdr_latch); - if (!encode_device) { - return std::nullopt; + std::unique_ptr encode_device; + std::unique_ptr session; + bool session_hdr_metadata_valid = false; + SS_HDR_METADATA session_hdr_metadata {}; +#ifdef _WIN32 + if (&encoder == &amdvce_legacy) { + bool legacy_cancelled = false; + bool legacy_gate_contended = false; + auto legacy = make_legacy_amf_session_bounded( + disp, ctx.config, img.width, img.height, &ctx.hdr_latch, + initialization_deadline, initialization_cancelled, + legacy_cancelled, legacy_gate_contended); + if (legacy_cancelled || legacy_gate_contended) return std::nullopt; + if (legacy) { + session = std::move(legacy->session); + session_hdr_metadata_valid = legacy->hdr_metadata_valid; + session_hdr_metadata = legacy->hdr_metadata; + } + } else +#endif + { + encode_device = make_encode_device(*disp, encoder, ctx.config, &ctx.hdr_latch); + if (encode_device) { + session_hdr_metadata_valid = encode_device->hdr_metadata_valid; + session_hdr_metadata = encode_device->hdr_metadata; + } } + if (!encode_device && !session) return std::nullopt; // absolute mouse coordinates require that the dimensions of the screen are known - ctx.touch_port_events->raise(make_port(disp, ctx.config)); + ctx.touch_port_events->raise(make_port(disp.get(), ctx.config)); // Update client with our current HDR stream state hdr_info_t hdr_info = std::make_unique(false); - if (encode_device->hdr_metadata_valid) { - hdr_info = std::make_unique(true, encode_device->hdr_metadata); + if (session_hdr_metadata_valid) { + hdr_info = std::make_unique(true, session_hdr_metadata); } raise_hdr_info_if_changed(ctx.hdr_events, ctx.last_hdr_info, std::move(hdr_info)); - auto session = make_encode_session(disp, encoder, ctx.config, img.width, img.height, std::move(encode_device)); + if (!session) { + session = make_encode_session( + disp.get(), encoder, ctx.config, img.width, img.height, + std::move(encode_device), initialization_deadline, initialization_cancelled); + } if (!session) { return std::nullopt; } @@ -3582,7 +4677,7 @@ namespace video { std::vector synced_sessions; for (auto &ctx : synced_session_ctxs) { - auto synced_session = make_synced_session(disp.get(), encoder, *img, *ctx); + auto synced_session = make_synced_session(disp, encoder, *img, *ctx); if (!synced_session) { return encode_e::error; } @@ -3601,7 +4696,7 @@ namespace video { synced_session_ctxs.emplace_back(std::make_unique(std::move(*encode_session_ctx))); - auto encode_session = make_synced_session(disp.get(), encoder, *img, *synced_session_ctxs.back()); + auto encode_session = make_synced_session(disp, encoder, *img, *synced_session_ctxs.back()); if (!encode_session) { ec = platf::capture_e::error; return false; @@ -3712,7 +4807,8 @@ namespace video { } if (placeholder_input) { - pos->bootstrap.placeholder_encoded = true; + auto *amf_session = dynamic_cast(pos->session.get()); + pos->bootstrap.placeholder_encoded = !amf_session || amf_session->has_emitted_any_frame(); } pos->session->request_normal_frame(); @@ -3814,6 +4910,10 @@ namespace video { auto touch_port_event = mail->event(mail::touch_port); auto hdr_event = mail->event(mail::hdr); + int consecutive_encoder_initialization_failures = 0; +#ifdef _WIN32 + int consecutive_native_amf_runtime_failures = 0; +#endif // Encoding takes place on this thread (async-capture mode; capture lives in // capture_thread_async at critical already). Match it so neither half of the @@ -3844,35 +4944,131 @@ namespace video { return; } auto &encoder = *enc_ptr; + const auto initialization_deadline = std::chrono::steady_clock::now() + 5s; + initialization_cancel_t initialization_cancelled = [&]() { + return shutdown_event->peek() || ref->reinit_event.peek() || !images->running(); + }; - auto encode_device = make_encode_device(*display, encoder, config, &hdr_latch); - if (!encode_device) { + std::unique_ptr encode_device; + std::unique_ptr prepared_session; + bool session_hdr_metadata_valid = false; + SS_HDR_METADATA session_hdr_metadata {}; + bool initialization_was_cancelled = false; + bool initialization_gate_contended = false; +#ifdef _WIN32 + if (&encoder == &amdvce_legacy) { + auto legacy = make_legacy_amf_session_bounded( + display, config, display->width, display->height, &hdr_latch, + initialization_deadline, initialization_cancelled, + initialization_was_cancelled, initialization_gate_contended); + if (legacy) { + prepared_session = std::move(legacy->session); + session_hdr_metadata_valid = legacy->hdr_metadata_valid; + session_hdr_metadata = legacy->hdr_metadata; + } + } else +#endif + { + encode_device = make_encode_device(*display, encoder, config, &hdr_latch); + if (encode_device) { + session_hdr_metadata_valid = encode_device->hdr_metadata_valid; + session_hdr_metadata = encode_device->hdr_metadata; + } + } +#ifdef _WIN32 + if (initialization_was_cancelled) continue; + if (!encode_device && !prepared_session && &encoder == &amdvce) { + BOOST_LOG(error) << "AMF: native device creation failed; refusing silent amdvce_legacy fallback"sv; + } +#endif + if (initialization_was_cancelled) continue; + if (initialization_gate_contended && !encode_device && !prepared_session) { + std::this_thread::sleep_for(100ms); + continue; + } + if (!encode_device && !prepared_session) { return; } + auto &session_encoder = *enc_ptr; // absolute mouse coordinates require that the dimensions of the screen are known touch_port_event->raise(make_port(display.get(), config)); // Update client with our current HDR stream state hdr_info_t hdr_info = std::make_unique(false); - if (encode_device->hdr_metadata_valid) { - hdr_info = std::make_unique(true, encode_device->hdr_metadata); + if (session_hdr_metadata_valid) { + hdr_info = std::make_unique(true, session_hdr_metadata); } raise_hdr_info_if_changed(hdr_event, last_hdr_info, std::move(hdr_info)); - encode_run( + const auto encode_result = encode_run( frame_nr, mail, images, config, display, std::move(encode_device), + std::move(prepared_session), ref->reinit_event, - *ref->encoder_p, + session_encoder, + &hdr_latch, channel_data, + initialization_deadline, + initialization_cancelled, last_hdr_info, rtx_hdr_metadata_refresh ); +#ifdef _WIN32 + if (encode_result == encode_run_result_e::native_amf_failed && &session_encoder == &amdvce) { + // Runtime fatals (TDR, sustained backpressure, output stalls) are + // classified by the encoder layer as reinit requests. Rebuild the same + // native session like every other encoder does — never a silent + // amdvce_legacy fallback — but stay bounded so a wedged driver cannot + // busy-loop the stream. + if (native_amf_lifecycle_gate.is_quarantined()) { + BOOST_LOG(error) << "AMF: native runtime failed while quarantined; ending the stream. Host restart is required before retrying AMD encoding"sv; + return; + } + ++consecutive_native_amf_runtime_failures; + if (consecutive_native_amf_runtime_failures >= 3) { + BOOST_LOG(error) << "AMF: native runtime failed 3 times without a stable session; ending the stream without changing encoder implementations"sv; + return; + } + BOOST_LOG(warning) << "AMF: native runtime failed; rebuilding the native session (attempt " + << consecutive_native_amf_runtime_failures << " of 3)"sv; + continue; + } +#endif + if (encode_result == encode_run_result_e::initialization_failed) { +#ifdef _WIN32 + if (&session_encoder != &amdvce && &session_encoder != &amdvce_legacy) { + continue; + } + if (native_amf_lifecycle_gate.is_quarantined()) { + BOOST_LOG(error) << "AMF: ending the stream after a watchdog timeout; host restart is required before retrying AMD encoding"sv; + return; + } + ++consecutive_encoder_initialization_failures; + if (consecutive_encoder_initialization_failures >= 3) { + BOOST_LOG(error) << "Encoder initialization failed 3 times; ending the stream instead of busy-looping"sv; + return; + } + const auto retry_delay = std::chrono::milliseconds(100 * (1 << (consecutive_encoder_initialization_failures - 1))); + BOOST_LOG(warning) << "Encoder initialization failed; retrying in " << retry_delay.count() << "ms"; + std::this_thread::sleep_for(retry_delay); + continue; +#else + continue; +#endif + } + if (encode_result == encode_run_result_e::temporarily_busy) { + std::this_thread::sleep_for(100ms); + continue; + } + consecutive_encoder_initialization_failures = 0; +#ifdef _WIN32 + consecutive_native_amf_runtime_failures = 0; +#endif } } @@ -3920,6 +5116,18 @@ namespace video { int validate_config(std::shared_ptr disp, const encoder_t &encoder, const config_t &config) { const int max_attempts = config.videoFormat >= 1 ? 3 : 1; // HEVC/AV1 can fail transiently during probing + // The tight submission/wall-clock bounds exist for AMF drivers that stall in + // INPUT_FULL; probing for every other encoder keeps the pre-existing limits + // so this AMD-only change cannot alter NVENC/QSV/software negotiation. +#ifdef _WIN32 + const bool amf_probe = &encoder == &amdvce || &encoder == &amdvce_legacy; +#else + const bool amf_probe = false; +#endif + const auto probe_timeout = amf_probe ? std::chrono::seconds {5} : std::chrono::seconds {60}; + const int max_probe_submissions = amf_probe ? 64 : 256; + const auto probe_start = std::chrono::steady_clock::now(); + const auto probe_deadline = probe_start + probe_timeout; const auto codec_name = [&]() -> std::string_view { switch (config.videoFormat) { case 0: @@ -3935,22 +5143,48 @@ namespace video { for (int attempt = 1; attempt <= max_attempts; ++attempt) { auto validate_once = [&]() -> util::optional_t { - auto encode_device = make_encode_device(*disp, encoder, config); - if (!encode_device) { - return util::false_v>; + std::unique_ptr session; +#ifdef _WIN32 + if (&encoder == &amdvce_legacy) { + bool legacy_cancelled = false; + bool legacy_gate_contended = false; + auto legacy = make_legacy_amf_session_bounded( + disp, config, disp->width, disp->height, nullptr, + probe_deadline, []() { return false; }, + legacy_cancelled, legacy_gate_contended); + if (legacy_cancelled || legacy_gate_contended) { + return util::false_v>; + } + if (legacy) session = std::move(legacy->session); + } else +#endif + { + auto encode_device = make_encode_device(*disp, encoder, config); + if (encode_device) { + session = make_encode_session( + disp.get(), encoder, config, disp->width, disp->height, + std::move(encode_device), probe_deadline, []() { return false; }); + } } - - auto session = make_encode_session(disp.get(), encoder, config, disp->width, disp->height, std::move(encode_device)); if (!session) { return util::false_v>; } - - { - // Image buffers are large, so we use a separate scope to free it immediately after convert() - auto img = disp->alloc_img(); - if (!img || disp->dummy_img(img.get()) || session->convert(*img)) { - return util::false_v>; + auto bounded_probe_teardown = util::fail_guard([&]() { +#ifdef _WIN32 + if (&encoder == &amdvce_legacy) { + destroy_legacy_amf_session_bounded(session, "probe"sv); + return; } +#endif + destroy_encode_session_bounded(session, "probe"sv); + }); + + // Keep the probe image alive while native AMF primes a lookahead pipeline. + // Every PA input is rendered into a newly reserved ring surface; repeatedly + // submitting the first surface cannot make progress if AMF still owns it. + auto probe_img = disp->alloc_img(); + if (!probe_img || disp->dummy_img(probe_img.get()) || session->convert(*probe_img)) { + return util::false_v>; } session->request_idr_frame(); @@ -3959,8 +5193,32 @@ namespace video { auto probe_mail = std::make_shared(); auto packets = probe_mail->queue(mail::video_packets); - while (!packets->peek()) { - if (encode(1, *session, packets, nullptr, {}, {}, {})) { + // Bound the whole codec probe by both submissions and wall time. An AMF driver + // stalled in INPUT_FULL can make a single encode() call take hundreds of + // milliseconds, so the old 256-attempt limit could block startup for minutes + // and even outlive the service shutdown watchdog. The deadline is shared by + // HEVC/AV1 retries below, keeping the complete validation below one watchdog + // interval while still leaving ample time for a cold hardware encoder. + for (int probe_attempts = 0; !packets->peek(); ++probe_attempts) { + const auto now = std::chrono::steady_clock::now(); + if (probe_attempts >= max_probe_submissions || now >= probe_deadline) { + const auto elapsed = std::chrono::duration_cast(now - probe_start); + BOOST_LOG(error) << "Encoder probe produced no packet after "sv << probe_attempts + << " submissions in " << elapsed.count() << "ms; treating "sv + << codec_name << " as unsupported."sv; + return util::false_v>; + } + + // Every submitted surface needs a unique PTS. Reusing frame index 1 for + // hundreds of probe submissions violates the normal encoder contract and + // makes delayed/catch-up output indistinguishable from a duplicate frame. + if (probe_attempts > 0 && dynamic_cast(session.get()) && + session->convert(*probe_img)) { + BOOST_LOG(error) << "Encoder probe could not prepare the next native AMF lookahead surface"sv; + return util::false_v>; + } + const auto probe_frame_index = static_cast(probe_attempts) + 1; + if (encode(probe_frame_index, *session, packets, nullptr, {}, {}, {})) { return util::false_v>; } } @@ -3993,10 +5251,12 @@ namespace video { return *result; } - if (attempt < max_attempts) { + if (attempt < max_attempts && (!amf_probe || std::chrono::steady_clock::now() < probe_deadline)) { BOOST_LOG(debug) << "Encoder probe: failed to validate "sv << codec_name << " config (attempt "sv << attempt << "/" << max_attempts << "), retrying."sv; std::this_thread::sleep_for(std::chrono::milliseconds {50}); + } else { + break; } } @@ -4253,6 +5513,14 @@ namespace video { }); auto encoder_list = encoders; +#ifdef _WIN32 + const auto amf_selection_policy = amf::lifecycle::encoder_selection_policy(config::video.encoder); + // amdvce_legacy is rollback-only. It participates in probing solely when + // explicitly selected; native feature or capability failures must remain visible. + if (!amf_selection_policy.include_legacy) { + encoder_list.erase(std::remove(encoder_list.begin(), encoder_list.end(), &amdvce_legacy), encoder_list.end()); + } +#endif // Use a local variable for encoder selection during probing so that // chosen_encoder is never null while concurrent capture threads may read it. @@ -4308,6 +5576,12 @@ namespace video { if (new_encoder == nullptr) { BOOST_LOG(error) << "Couldn't find any working encoder matching ["sv << config::video.encoder << ']'; +#ifdef _WIN32 + if (amf_selection_policy.fail_closed) { + BOOST_LOG(error) << "Native AMF was explicitly selected; refusing automatic fallback to amdvce_legacy or another encoder"sv; + return -1; + } +#endif } } @@ -4387,6 +5661,16 @@ namespace video { auto &encoder = *new_encoder; +#ifdef _WIN32 + if (encoder.name == "software"sv) { + // Software is probed last, so reaching it means every hardware encoder — + // including native AMF — failed validation. Make the degradation loud: + // an AMD user should never discover software encoding from stutter alone. + BOOST_LOG(error) << "No hardware encoder passed validation; the SOFTWARE encoder was selected."sv; + BOOST_LOG(error) << "If this system has an AMD GPU, hardware encoding is NOT active. Check the AMD driver and AMF runtime, or set encoder = amdvce_legacy to try the FFmpeg AMF fallback."sv; + } +#endif + last_encoder_probe_supported_ref_frames_invalidation = (encoder.flags & REF_FRAMES_INVALIDATION); last_encoder_probe_supported_yuv444_for_codec[0] = encoder.h264[encoder_t::PASSED] && encoder.h264[encoder_t::YUV444]; diff --git a/src/video.h b/src/video.h index 521d73b20..98db2aa41 100644 --- a/src/video.h +++ b/src/video.h @@ -147,6 +147,22 @@ namespace video { } }; + struct encoder_platform_formats_amf: encoder_platform_formats_t { + encoder_platform_formats_amf( + const platf::mem_type_e &dev_type, + const platf::pix_fmt_e &pix_fmt_8bit, + const platf::pix_fmt_e &pix_fmt_10bit, + const platf::pix_fmt_e &pix_fmt_yuv444_8bit, + const platf::pix_fmt_e &pix_fmt_yuv444_10bit + ) { + encoder_platform_formats_t::dev_type = dev_type; + encoder_platform_formats_t::pix_fmt_8bit = pix_fmt_8bit; + encoder_platform_formats_t::pix_fmt_10bit = pix_fmt_10bit; + encoder_platform_formats_t::pix_fmt_yuv444_8bit = pix_fmt_yuv444_8bit; + encoder_platform_formats_t::pix_fmt_yuv444_10bit = pix_fmt_yuv444_10bit; + } + }; + struct encoder_t { std::string_view name; @@ -181,7 +197,10 @@ namespace video { option_t(const option_t &) = default; std::string name; - std::variant *, std::function, std::string, std::string *, std::function> value; + struct optional_int_function_t { + std::function()> evaluate; + }; + std::variant *, std::function, optional_int_function_t, std::string, std::string *, std::function> value; option_t(std::string &&name, decltype(value) &&value): name {std::move(name)}, @@ -270,6 +289,7 @@ namespace video { #ifdef _WIN32 extern encoder_t amdvce; + extern encoder_t amdvce_legacy; extern encoder_t quicksync; extern encoder_t mediafoundation; #endif diff --git a/src_assets/common/assets/web/components/app-edit/AppEditConfigOverridesSection.vue b/src_assets/common/assets/web/components/app-edit/AppEditConfigOverridesSection.vue index 47605a298..9043fb0c5 100644 --- a/src_assets/common/assets/web/components/app-edit/AppEditConfigOverridesSection.vue +++ b/src_assets/common/assets/web/components/app-edit/AppEditConfigOverridesSection.vue @@ -984,11 +984,14 @@ const ALLOWED_OVERRIDE_KEYS = new Set([ 'qsv_slow_hevc', 'amd_usage', 'amd_rc', + 'amd_qvbr_quality_level', 'amd_enforce_hrd', 'amd_quality', 'amd_preanalysis', 'amd_vbaq', 'amd_coder', + 'amd_av1_screen_content', + 'amd_av1_latency_mode', 'vt_coder', 'vt_software', 'vt_realtime', diff --git a/src_assets/common/assets/web/config.html b/src_assets/common/assets/web/config.html index 90c926b03..18c7f2200 100644 --- a/src_assets/common/assets/web/config.html +++ b/src_assets/common/assets/web/config.html @@ -278,11 +278,14 @@

{{ $t('config.configuration') }}

options: { amd_usage: 'ultralowlatency', amd_rc: 'vbr_latency', + amd_qvbr_quality_level: 0, amd_enforce_hrd: 'disabled', amd_quality: 'balanced', amd_preanalysis: 'disabled', amd_vbaq: 'enabled', amd_coder: 'auto', + amd_av1_screen_content: 'auto', + amd_av1_latency_mode: 'auto', }, }, { diff --git a/src_assets/common/assets/web/configs/configFieldSchema.ts b/src_assets/common/assets/web/configs/configFieldSchema.ts index 9e84b38b4..8a2acb8da 100644 --- a/src_assets/common/assets/web/configs/configFieldSchema.ts +++ b/src_assets/common/assets/web/configs/configFieldSchema.ts @@ -77,6 +77,7 @@ const NUMBER_FIELD_OVERRIDES: Record> = { rtx_hdr_peak_brightness: { min: 400, max: 2000, step: 1, placeholder: '1000' }, dd_virtual_display_permanent_count: { min: 0, max: 4, step: 1, precision: 0, placeholder: '0' }, nvenc_vbv_increase: { min: 0, max: 400, placeholder: '0' }, + amd_qvbr_quality_level: { min: 0, max: 51, step: 1, placeholder: '0' }, frame_limiter_fps_limit: { min: 0, max: 1000, step: 1, precision: 0, placeholder: '0' }, }; diff --git a/src_assets/common/assets/web/configs/configSelectOptions.ts b/src_assets/common/assets/web/configs/configSelectOptions.ts index 1a60197b3..d4f2b710f 100644 --- a/src_assets/common/assets/web/configs/configSelectOptions.ts +++ b/src_assets/common/assets/web/configs/configSelectOptions.ts @@ -7,6 +7,11 @@ export type ConfigSelectOptionsContext = { currentValue?: unknown; }; +function translateOr(t: (key: string) => string, key: string, fallback: string): string { + const translated = t(key); + return translated && translated !== key ? translated : fallback; +} + function isSelectValue(value: unknown): value is string | number { return typeof value === 'string' || (typeof value === 'number' && Number.isFinite(value)); } @@ -207,6 +212,9 @@ export function getConfigSelectOptions( if (hasNvidia) options.push({ label: 'NVIDIA NVENC', value: 'nvenc' }); if (hasIntel) options.push({ label: 'Intel QuickSync', value: 'quicksync' }); if (hasAmd) options.push({ label: 'AMD AMF/VCE', value: 'amdvce' }); + // Explicit rollback to the FFmpeg AMF path — never probed automatically, + // so it must stay selectable here. + if (hasAmd) options.push({ label: 'AMD AMF/VCE (legacy FFmpeg)', value: 'amdvce_legacy' }); } else if (platform === 'linux') { options.push( { label: 'NVIDIA NVENC', value: 'nvenc' }, @@ -334,17 +342,56 @@ export function getConfigSelectOptions( value: 'vbr_latency', }, { label: t('config.amd_rc_vbr_peak'), value: 'vbr_peak' }, + { label: translateOr(t, 'config.amd_rc_qvbr', 'QVBR'), value: 'qvbr' }, + { label: translateOr(t, 'config.amd_rc_hqvbr', 'HQ-VBR'), value: 'hqvbr' }, + { label: translateOr(t, 'config.amd_rc_hqcbr', 'HQ-CBR'), value: 'hqcbr' }, ]; return ensureIncludesCurrentValue(options, ctx.currentValue); } case 'amd_quality': { const options = [ + { label: translateOr(t, 'config.amd_opt_auto', 'Auto'), value: 'auto' }, { label: t('config.amd_quality_speed'), value: 'speed' }, { label: t('config.amd_quality_balanced'), value: 'balanced' }, { label: t('config.amd_quality_quality'), value: 'quality' }, ]; return ensureIncludesCurrentValue(options, ctx.currentValue); } + case 'amd_vbaq': { + const options = [ + { label: translateOr(t, 'config.amd_opt_auto', 'Auto'), value: 'auto' }, + { label: translateOr(t, 'config.amd_opt_enabled', 'Enabled'), value: 'enabled' }, + { label: translateOr(t, 'config.amd_opt_disabled', 'Disabled'), value: 'disabled' }, + ]; + return ensureIncludesCurrentValue(options, ctx.currentValue); + } + case 'amd_av1_screen_content': { + const options = [ + { label: translateOr(t, 'config.amd_opt_auto', 'Auto'), value: 'auto' }, + { label: translateOr(t, 'config.amd_opt_enabled', 'Enabled'), value: 'enabled' }, + { label: translateOr(t, 'config.amd_opt_disabled', 'Disabled'), value: 'disabled' }, + ]; + return ensureIncludesCurrentValue(options, ctx.currentValue); + } + case 'amd_av1_latency_mode': { + const options = [ + { label: translateOr(t, 'config.amd_opt_auto', 'Auto'), value: 'auto' }, + { label: translateOr(t, 'config.amd_av1_latency_none', 'None'), value: 'none' }, + { + label: translateOr(t, 'config.amd_av1_latency_power_saving', 'Power saving (real-time)'), + value: 'power_saving', + }, + { + label: translateOr(t, 'config.amd_av1_latency_realtime', 'Real-time'), + value: 'realtime', + }, + { + label: translateOr(t, 'config.amd_av1_latency_lowest', 'Lowest latency'), + value: 'lowest', + }, + ]; + return ensureIncludesCurrentValue(options, ctx.currentValue); + } case 'vt_software': { const options = [ { label: t('_common.auto'), value: 'auto' }, diff --git a/src_assets/common/assets/web/configs/tabs/encoders/AmdAmfEncoder.vue b/src_assets/common/assets/web/configs/tabs/encoders/AmdAmfEncoder.vue index 9528265cf..5fdf831dc 100644 --- a/src_assets/common/assets/web/configs/tabs/encoders/AmdAmfEncoder.vue +++ b/src_assets/common/assets/web/configs/tabs/encoders/AmdAmfEncoder.vue @@ -13,18 +13,24 @@ const config = store.config;

{{ $t('config.amd_section_title') }}

- +

{{ $t('config.amd_rc_group') }}

- + + +
@@ -35,20 +41,38 @@ const config = store.config; - + + + + - +
+

+ {{ $t('config.amd_native_group') }} +

+ + + +
diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json index 6d82e5717..a85463a3c 100644 --- a/src_assets/common/assets/web/public/assets/locale/en.json +++ b/src_assets/common/assets/web/public/assets/locale/en.json @@ -417,22 +417,39 @@ "amd_coder_desc": "Allows you to select the entropy encoding to prioritize quality or encoding speed. H.264 only.", "amd_enforce_hrd": "AMF Hypothetical Reference Decoder (HRD) Enforcement", "amd_enforce_hrd_desc": "Increases the constraints on rate control to meet HRD model requirements. This greatly reduces bitrate overflows, but may cause encoding artifacts or reduced quality on certain cards.", + "amd_native_group": "AV1 options - native amdvce", "amd_preanalysis": "AMF Preanalysis", "amd_preanalysis_desc": "This enables rate-control preanalysis, which may increase quality at the expense of increased encoding latency.", "amd_quality": "AMF Quality", - "amd_quality_balanced": "balanced -- balanced (default)", - "amd_quality_desc": "This controls the balance between encoding speed and quality.", - "amd_quality_group": "AMF Quality Settings", + "amd_quality_balanced": "balanced -- balance speed and quality", + "amd_quality_desc": "This controls the balance between encoding speed and quality. The default is Balanced. Auto leaves the property unset so the selected AMF usage preset can choose it.", + "amd_quality_group": "AMF Quality Settings (native + FFmpeg)", "amd_quality_quality": "quality -- prefer quality", "amd_quality_speed": "speed -- prefer speed", + "amd_qvbr_quality_level": "AMF QVBR Quality Level", + "amd_qvbr_quality_level_desc": "Target quality level for the 'qvbr' rate control method, from 1 (lowest quality) to 51 (highest quality). Leave at 0 to keep the encoder default. This setting has no effect unless AMF Rate Control is set to 'qvbr'.", "amd_rc": "AMF Rate Control", "amd_rc_cbr": "cbr -- constant bitrate (recommended if HRD is enabled)", "amd_rc_cqp": "cqp -- constant qp mode", "amd_rc_desc": "This controls the rate control method to ensure we are not exceeding the client bitrate target. 'cqp' is not suitable for bitrate targeting, and other options besides 'vbr_latency' depend on HRD Enforcement to help constrain bitrate overflows.", - "amd_rc_group": "AMF Rate Control Settings", + "amd_rc_group": "AMF Rate Control Settings (native + FFmpeg)", + "amd_rc_hqcbr": "hqcbr -- high quality constant bitrate", + "amd_rc_hqvbr": "hqvbr -- high quality variable bitrate", + "amd_rc_qvbr": "qvbr -- quality-defined variable bitrate (uses QVBR Quality Level)", "amd_rc_vbr_latency": "vbr_latency -- latency constrained variable bitrate (recommended if HRD is disabled; default)", "amd_rc_vbr_peak": "vbr_peak -- peak constrained variable bitrate", - "amd_usage": "AMF Usage", + "amd_av1_latency_lowest": "lowest -- lowest latency (highest power)", + "amd_av1_latency_mode": "AMF AV1 Encoding Latency Mode", + "amd_av1_latency_mode_desc": "Sets the AV1 encoding-latency tier on the native AMF encoder (amdvce). Lower tiers finish each frame faster at the cost of higher power draw. 'auto' leaves the driver default. AV1 only; only applies to the native amdvce encoder.", + "amd_av1_latency_none": "none -- balance latency and power", + "amd_av1_latency_power_saving": "power_saving -- real-time with lower power", + "amd_av1_latency_realtime": "realtime -- real-time", + "amd_av1_screen_content": "AMF AV1 Screen Content Tools", + "amd_av1_screen_content_desc": "Enables AV1 screen-content coding tools on the native AMF encoder (amdvce), which can improve efficiency and text/UI clarity for desktop and screen-heavy content. 'auto' leaves the driver default. AV1 only; only applies to the native amdvce encoder.", + "amd_opt_auto": "auto -- leave the AMF driver default", + "amd_opt_disabled": "disabled", + "amd_opt_enabled": "enabled", + "amd_usage": "AMF Usage (native + FFmpeg)", "amd_usage_desc": "This sets the base encoding profile. All options presented below will override a subset of the usage profile, but there are additional hidden settings applied that cannot be configured elsewhere.", "amd_usage_lowlatency": "lowlatency - low latency (fastest)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality - low latency, high quality (fast)", @@ -440,7 +457,7 @@ "amd_usage_ultralowlatency": "ultralowlatency - ultra low latency (fastest; default)", "amd_usage_webcam": "webcam -- webcam (slow)", "amd_vbaq": "AMF Variance Based Adaptive Quantization (VBAQ)", - "amd_vbaq_desc": "The human visual system is typically less sensitive to artifacts in highly textured areas. In VBAQ mode, pixel variance is used to indicate the complexity of spatial textures, allowing the encoder to allocate more bits to smoother areas. Enabling this feature leads to improvements in subjective visual quality with some content.", + "amd_vbaq_desc": "The human visual system is typically less sensitive to artifacts in highly textured areas. In VBAQ mode, pixel variance is used to indicate the complexity of spatial textures, allowing the encoder to allocate more bits to smoother areas. VBAQ is enabled by default. Auto leaves the property unset so the selected AMF usage preset can choose it.", "apply_note": "Click 'Apply' to restart Vibepollo and apply changes. This will terminate any running sessions.", "audio_sink": "Audio Sink", "audio_sink_desc_linux": "The name of the audio sink used for Audio Loopback. If you do not specify this variable, pulseaudio will select the default monitor device. You can find the name of the audio sink using either command:", diff --git a/src_assets/common/assets/web/public/assets/locale/en_GB.json b/src_assets/common/assets/web/public/assets/locale/en_GB.json index 203a85ee3..f4a4317e7 100644 --- a/src_assets/common/assets/web/public/assets/locale/en_GB.json +++ b/src_assets/common/assets/web/public/assets/locale/en_GB.json @@ -400,22 +400,39 @@ "amd_coder_desc": "Allows you to select the entropy encoding to prioritize quality or encoding speed. H.264 only.", "amd_enforce_hrd": "AMF Hypothetical Reference Decoder (HRD) Enforcement", "amd_enforce_hrd_desc": "Increases the constraints on rate control to meet HRD model requirements. This greatly reduces bitrate overflows, but may cause encoding artifacts or reduced quality on certain cards.", + "amd_native_group": "AV1 options - native amdvce", "amd_preanalysis": "AMF Preanalysis", "amd_preanalysis_desc": "This enables rate-control preanalysis, which may increase quality at the expense of increased encoding latency.", "amd_quality": "AMF Quality", - "amd_quality_balanced": "balanced -- balanced (default)", - "amd_quality_desc": "This controls the balance between encoding speed and quality.", - "amd_quality_group": "AMF Quality Settings", + "amd_quality_balanced": "balanced -- balance speed and quality", + "amd_quality_desc": "This controls the balance between encoding speed and quality. The default is Balanced. Auto leaves the property unset so the selected AMF usage preset can choose it.", + "amd_quality_group": "AMF Quality Settings (native + FFmpeg)", "amd_quality_quality": "quality -- prefer quality", "amd_quality_speed": "speed -- prefer speed", + "amd_qvbr_quality_level": "AMF QVBR Quality Level", + "amd_qvbr_quality_level_desc": "Target quality level for the 'qvbr' rate control method, from 1 (lowest quality) to 51 (highest quality). Leave at 0 to keep the encoder default. This setting has no effect unless AMF Rate Control is set to 'qvbr'.", "amd_rc": "AMF Rate Control", "amd_rc_cbr": "cbr -- constant bitrate", "amd_rc_cqp": "cqp -- constant qp mode", "amd_rc_desc": "This controls the rate control method to ensure we are not exceeding the client bitrate target. 'cqp' is not suitable for bitrate targeting, and other options besides 'vbr_latency' depend on HRD Enforcement to help constrain bitrate overflows.", - "amd_rc_group": "AMF Rate Control Settings", + "amd_rc_group": "AMF Rate Control Settings (native + FFmpeg)", + "amd_rc_hqcbr": "hqcbr -- high quality constant bitrate", + "amd_rc_hqvbr": "hqvbr -- high quality variable bitrate", + "amd_rc_qvbr": "qvbr -- quality-defined variable bitrate (uses QVBR Quality Level)", "amd_rc_vbr_latency": "vbr_latency -- latency constrained variable bitrate (default)", "amd_rc_vbr_peak": "vbr_peak -- peak constrained variable bitrate", - "amd_usage": "AMF Usage", + "amd_av1_latency_lowest": "lowest -- lowest latency (highest power)", + "amd_av1_latency_mode": "AMF AV1 Encoding Latency Mode", + "amd_av1_latency_mode_desc": "Sets the AV1 encoding-latency tier on the native AMF encoder (amdvce). Lower tiers finish each frame faster at the cost of higher power draw. 'auto' leaves the driver default. AV1 only; only applies to the native amdvce encoder.", + "amd_av1_latency_none": "none -- balance latency and power", + "amd_av1_latency_power_saving": "power_saving -- real-time with lower power", + "amd_av1_latency_realtime": "realtime -- real-time", + "amd_av1_screen_content": "AMF AV1 Screen Content Tools", + "amd_av1_screen_content_desc": "Enables AV1 screen-content coding tools on the native AMF encoder (amdvce), which can improve efficiency and text/UI clarity for desktop and screen-heavy content. 'auto' leaves the driver default. AV1 only; only applies to the native amdvce encoder.", + "amd_opt_auto": "auto -- leave the AMF driver default", + "amd_opt_disabled": "disabled", + "amd_opt_enabled": "enabled", + "amd_usage": "AMF Usage (native + FFmpeg)", "amd_usage_desc": "This sets the base encoding profile. All options presented below will override a subset of the usage profile, but there are additional hidden settings applied that cannot be configured elsewhere.", "amd_usage_lowlatency": "lowlatency - low latency (fast)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality - low latency, high quality (fast)", @@ -423,7 +440,7 @@ "amd_usage_ultralowlatency": "ultralowlatency - ultra low latency (fastest)", "amd_usage_webcam": "webcam -- webcam (slow)", "amd_vbaq": "AMF Variance Based Adaptive Quantization (VBAQ)", - "amd_vbaq_desc": "The human visual system is typically less sensitive to artifacts in highly textured areas. In VBAQ mode, pixel variance is used to indicate the complexity of spatial textures, allowing the encoder to allocate more bits to smoother areas. Enabling this feature leads to improvements in subjective visual quality with some content.", + "amd_vbaq_desc": "The human visual system is typically less sensitive to artifacts in highly textured areas. In VBAQ mode, pixel variance is used to indicate the complexity of spatial textures, allowing the encoder to allocate more bits to smoother areas. VBAQ is enabled by default. Auto leaves the property unset so the selected AMF usage preset can choose it.", "apply_note": "Click 'Apply' to restart Vibepollo and apply changes. This will terminate any running sessions.", "audio_sink": "Audio Sink", "audio_sink_desc_linux": "The name of the audio sink used for Audio Loopback. If you do not specify this variable, pulseaudio will select the default monitor device. You can find the name of the audio sink using either command:", diff --git a/src_assets/common/assets/web/public/assets/locale/en_US.json b/src_assets/common/assets/web/public/assets/locale/en_US.json index 8c58fe051..d8f7a758a 100644 --- a/src_assets/common/assets/web/public/assets/locale/en_US.json +++ b/src_assets/common/assets/web/public/assets/locale/en_US.json @@ -400,22 +400,39 @@ "amd_coder_desc": "Allows you to select the entropy encoding to prioritize quality or encoding speed. H.264 only.", "amd_enforce_hrd": "AMF Hypothetical Reference Decoder (HRD) Enforcement", "amd_enforce_hrd_desc": "Increases the constraints on rate control to meet HRD model requirements. This greatly reduces bitrate overflows, but may cause encoding artifacts or reduced quality on certain cards.", + "amd_native_group": "AV1 options - native amdvce", "amd_preanalysis": "AMF Preanalysis", "amd_preanalysis_desc": "This enables rate-control preanalysis, which may increase quality at the expense of increased encoding latency.", "amd_quality": "AMF Quality", - "amd_quality_balanced": "balanced -- balanced (default)", - "amd_quality_desc": "This controls the balance between encoding speed and quality.", - "amd_quality_group": "AMF Quality Settings", + "amd_quality_balanced": "balanced -- balance speed and quality", + "amd_quality_desc": "This controls the balance between encoding speed and quality. The default is Balanced. Auto leaves the property unset so the selected AMF usage preset can choose it.", + "amd_quality_group": "AMF Quality Settings (native + FFmpeg)", "amd_quality_quality": "quality -- prefer quality", "amd_quality_speed": "speed -- prefer speed", + "amd_qvbr_quality_level": "AMF QVBR Quality Level", + "amd_qvbr_quality_level_desc": "Target quality level for the 'qvbr' rate control method, from 1 (lowest quality) to 51 (highest quality). Leave at 0 to keep the encoder default. This setting has no effect unless AMF Rate Control is set to 'qvbr'.", "amd_rc": "AMF Rate Control", "amd_rc_cbr": "cbr -- constant bitrate (recommended if HRD is enabled)", "amd_rc_cqp": "cqp -- constant qp mode", "amd_rc_desc": "This controls the rate control method to ensure we are not exceeding the client bitrate target. 'cqp' is not suitable for bitrate targeting, and other options besides 'vbr_latency' depend on HRD Enforcement to help constrain bitrate overflows.", - "amd_rc_group": "AMF Rate Control Settings", + "amd_rc_group": "AMF Rate Control Settings (native + FFmpeg)", + "amd_rc_hqcbr": "hqcbr -- high quality constant bitrate", + "amd_rc_hqvbr": "hqvbr -- high quality variable bitrate", + "amd_rc_qvbr": "qvbr -- quality-defined variable bitrate (uses QVBR Quality Level)", "amd_rc_vbr_latency": "vbr_latency -- latency constrained variable bitrate (recommended if HRD is disabled; default)", "amd_rc_vbr_peak": "vbr_peak -- peak constrained variable bitrate", - "amd_usage": "AMF Usage", + "amd_av1_latency_lowest": "lowest -- lowest latency (highest power)", + "amd_av1_latency_mode": "AMF AV1 Encoding Latency Mode", + "amd_av1_latency_mode_desc": "Sets the AV1 encoding-latency tier on the native AMF encoder (amdvce). Lower tiers finish each frame faster at the cost of higher power draw. 'auto' leaves the driver default. AV1 only; only applies to the native amdvce encoder.", + "amd_av1_latency_none": "none -- balance latency and power", + "amd_av1_latency_power_saving": "power_saving -- real-time with lower power", + "amd_av1_latency_realtime": "realtime -- real-time", + "amd_av1_screen_content": "AMF AV1 Screen Content Tools", + "amd_av1_screen_content_desc": "Enables AV1 screen-content coding tools on the native AMF encoder (amdvce), which can improve efficiency and text/UI clarity for desktop and screen-heavy content. 'auto' leaves the driver default. AV1 only; only applies to the native amdvce encoder.", + "amd_opt_auto": "auto -- leave the AMF driver default", + "amd_opt_disabled": "disabled", + "amd_opt_enabled": "enabled", + "amd_usage": "AMF Usage (native + FFmpeg)", "amd_usage_desc": "This sets the base encoding profile. All options presented below will override a subset of the usage profile, but there are additional hidden settings applied that cannot be configured elsewhere.", "amd_usage_lowlatency": "lowlatency - low latency (fastest)", "amd_usage_lowlatency_high_quality": "lowlatency_high_quality - low latency, high quality (fast)", @@ -423,7 +440,7 @@ "amd_usage_ultralowlatency": "ultralowlatency - ultra low latency (fastest; default)", "amd_usage_webcam": "webcam -- webcam (slow)", "amd_vbaq": "AMF Variance Based Adaptive Quantization (VBAQ)", - "amd_vbaq_desc": "The human visual system is typically less sensitive to artifacts in highly textured areas. In VBAQ mode, pixel variance is used to indicate the complexity of spatial textures, allowing the encoder to allocate more bits to smoother areas. Enabling this feature leads to improvements in subjective visual quality with some content.", + "amd_vbaq_desc": "The human visual system is typically less sensitive to artifacts in highly textured areas. In VBAQ mode, pixel variance is used to indicate the complexity of spatial textures, allowing the encoder to allocate more bits to smoother areas. VBAQ is enabled by default. Auto leaves the property unset so the selected AMF usage preset can choose it.", "apply_note": "Click 'Apply' to restart Vibepollo and apply changes. This will terminate any running sessions.", "audio_sink": "Audio Sink", "audio_sink_desc_linux": "The name of the audio sink used for Audio Loopback. If you do not specify this variable, pulseaudio will select the default monitor device. You can find the name of the audio sink using either command:", diff --git a/src_assets/common/assets/web/stores/config.ts b/src_assets/common/assets/web/stores/config.ts index 637c00fc8..487c8b174 100644 --- a/src_assets/common/assets/web/stores/config.ts +++ b/src_assets/common/assets/web/stores/config.ts @@ -289,11 +289,14 @@ const defaultGroups = [ options: { amd_usage: 'ultralowlatency', amd_rc: 'vbr_latency', + amd_qvbr_quality_level: 0, amd_enforce_hrd: 'disabled', amd_quality: 'balanced', amd_preanalysis: 'disabled', amd_vbaq: 'enabled', amd_coder: 'auto', + amd_av1_screen_content: 'auto', + amd_av1_latency_mode: 'auto', }, }, { diff --git a/tests/frontend/config-field-schema.test.ts b/tests/frontend/config-field-schema.test.ts index 4f1748ad6..cd1bef557 100644 --- a/tests/frontend/config-field-schema.test.ts +++ b/tests/frontend/config-field-schema.test.ts @@ -53,4 +53,48 @@ describe('configFieldSchema', () => { }).kind, ).toBe('checkbox'); }); + + test.each([ + 'amd_quality', + 'amd_vbaq', + 'amd_av1_screen_content', + 'amd_av1_latency_mode', + ])('renders %s as an auto-capable select', (settingKey) => { + // currentValue stays undefined so ensureIncludesCurrentValue cannot append + // 'auto' as a fallback — the option must come from the select definition. + const field = getConfigFieldDefinition(settingKey, { + ...baseContext, + defaultValue: 'auto', + currentValue: undefined, + }); + + expect(field.kind).toBe('select'); + expect(field.options?.map(({ value }) => value)).toContain('auto'); + }); + + test('amd_rc has no auto option — the backend has no auto branch for it', () => { + const field = getConfigFieldDefinition('amd_rc', { + ...baseContext, + defaultValue: 'vbr_latency', + currentValue: undefined, + }); + + expect(field.kind).toBe('select'); + expect(field.options?.map(({ value }) => value)).not.toContain('auto'); + }); + + test('exposes every native AMF quality preset', () => { + const field = getConfigFieldDefinition('amd_quality', { + ...baseContext, + defaultValue: 'balanced', + currentValue: 'balanced', + }); + + expect(field.options?.map(({ value }) => value)).toEqual([ + 'auto', + 'speed', + 'balanced', + 'quality', + ]); + }); }); diff --git a/tests/unit/platform/windows/test_native_amf_review.cpp b/tests/unit/platform/windows/test_native_amf_review.cpp new file mode 100644 index 000000000..47240d21e --- /dev/null +++ b/tests/unit/platform/windows/test_native_amf_review.cpp @@ -0,0 +1,677 @@ +/** + * @file tests/unit/platform/windows/test_native_amf_review.cpp + * @brief Behavioral tests for native AMF ownership and lifecycle policy. + */ + +#include "src/amf/amf_lifecycle.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef SUNSHINE_AMF_LIFECYCLE_STANDALONE + #include "../../../tests_common.h" +#endif + +using namespace std::chrono_literals; + +namespace { + + enum class fake_amf_result_e { + ok, + input_full, + failed + }; + + bool synchronous_release_during_submit_is_reentrant_safe() { + std::mutex state_mutex; + amf::lifecycle::input_surface_state_t slot; + slot.state = amf::lifecycle::input_surface_state_e::reserved; + + bool observer_acquired_state = false; + const auto result = amf::lifecycle::submit_with_bounded_retry( + [&]() { + // Fake AMF invokes OnSurfaceDataRelease synchronously from SubmitInput. + if (state_mutex.try_lock()) { + observer_acquired_state = true; + amf::lifecycle::on_surface_released(slot); + state_mutex.unlock(); + } + return fake_amf_result_e::ok; + }, + []() { return false; }, + [](fake_amf_result_e value) { return value == fake_amf_result_e::input_full; }, + 20); + + std::lock_guard lock(state_mutex); + const bool immediately_reusable = amf::lifecycle::on_input_accepted(slot, 7); + return result == fake_amf_result_e::ok && observer_acquired_state && immediately_reusable && + slot.state == amf::lifecycle::input_surface_state_e::free && slot.frame_index == 0; + } + + bool backpressure_retries_the_same_submission_until_accepted() { + const std::vector responses { + fake_amf_result_e::input_full, + fake_amf_result_e::input_full, + fake_amf_result_e::ok, + }; + std::size_t submit_count = 0; + int wait_count = 0; + const auto result = amf::lifecycle::submit_with_bounded_retry( + [&]() { return responses.at(submit_count++); }, + [&]() { + ++wait_count; + return false; + }, + [](fake_amf_result_e value) { return value == fake_amf_result_e::input_full; }, + 20); + return result == fake_amf_result_e::ok && submit_count == 3 && wait_count == 2; + } + + bool exhausted_backpressure_reinitializes_without_owned_surfaces() { + using amf::lifecycle::submit_backpressure_requires_reinit; + return !submit_backpressure_requires_reinit(1, 120, false, 10s) && + !submit_backpressure_requires_reinit(119, 120, true, 1999ms) && + submit_backpressure_requires_reinit(120, 120, false, 0ms) && + submit_backpressure_requires_reinit(1, 120, true, 2s); + } + + bool recovery_state_changes_only_after_accepted_input() { + std::array slots_valid {true, true, false, false}; + std::array slot_frames {10, 20, 0, 0}; + int current_slot = 1; + bool rfi_pending = true; + uint64_t flagged_frame = 0; + + auto commit = [&](bool accepted) { + amf::lifecycle::commit_recovery_state( + accepted, + 2, + true, + 0, + -1, + 1, + true, + true, + 42, + slots_valid, + slot_frames, + current_slot, + rfi_pending, + [&](uint64_t frame_index) { flagged_frame = frame_index; }); + }; + + commit(false); + const bool unchanged_while_rejected = slots_valid[0] && slots_valid[1] && + slot_frames[0] == 10 && slot_frames[1] == 20 && + rfi_pending && flagged_frame == 0; + commit(true); + return unchanged_while_rejected && slots_valid[0] && !slots_valid[1] && + slot_frames[0] == 10 && slot_frames[1] == 0 && !rfi_pending && flagged_frame == 42; + } + + bool preanalysis_dependent_rate_control_is_planned_natively() { + const auto normal = amf::lifecycle::resolve_preanalysis(3, 0); + const auto explicit_pa = amf::lifecycle::resolve_preanalysis(3, 1); + const auto qvbr = amf::lifecycle::resolve_preanalysis(4, 0); + const auto hqvbr = amf::lifecycle::resolve_preanalysis(5, 0); + const auto hqcbr = amf::lifecycle::resolve_preanalysis(6, 0); + std::vector property_order; + const bool applied = amf::lifecycle::apply_rate_control_and_preanalysis( + 4, + true, + qvbr, + qvbr.lookahead_depth, + [&](int mode) { + property_order.push_back(100 + mode); + return true; + }, + [&](bool enabled) { + property_order.push_back(200 + static_cast(enabled)); + return true; + }, + [&](int depth) { + property_order.push_back(300 + depth); + return true; + }); + + return !normal.enabled && normal.lookahead_depth == 0 && + !amf::lifecycle::rate_control_supports_adaptive_quantization(0) && + amf::lifecycle::rate_control_supports_adaptive_quantization(3) && + amf::lifecycle::rate_control_supports_adaptive_quantization(std::nullopt) && + explicit_pa.enabled && explicit_pa.lookahead_depth == 1 && !explicit_pa.enabled_for_rate_control && + qvbr.enabled && qvbr.lookahead_depth == 1 && qvbr.enabled_for_rate_control && + hqvbr.enabled && hqvbr.lookahead_depth == 1 && hqvbr.enabled_for_rate_control && + hqcbr.enabled && hqcbr.lookahead_depth == 1 && hqcbr.enabled_for_rate_control && + applied && property_order == std::vector {104, 201, 301}; + } + + bool preanalysis_pipeline_primes_and_drains_in_order() { + struct fake_delayed_encoder_t { + explicit fake_delayed_encoder_t(int depth): + lookahead_depth(depth) { + } + + std::optional submit(uint64_t frame_index) { + pending.push_back(frame_index); + if (!amf::lifecycle::delayed_output_is_expected( + static_cast(pending.size()), + lookahead_depth)) { + return std::nullopt; + } + const auto output = pending.front(); + pending.pop_front(); + return output; + } + + std::vector drain() { + std::vector output; + while (!pending.empty()) { + output.push_back(pending.front()); + pending.pop_front(); + } + return output; + } + + int lookahead_depth; + std::deque pending; + }; + + fake_delayed_encoder_t encoder {amf::lifecycle::low_latency_preanalysis_lookahead_depth}; + const auto first = encoder.submit(100); + const auto second = encoder.submit(101); + const auto tail = encoder.drain(); + return !first && second && *second == 100 && tail == std::vector {101} && + !amf::lifecycle::delayed_output_is_expected(1, 1) && + amf::lifecycle::delayed_output_is_expected(2, 1) && + amf::lifecycle::delayed_output_is_expected(1, 0); + } + + bool automatic_h264_coder_preserves_driver_default() { + const auto automatic = amf::lifecycle::resolve_h264_cabac(0); + const auto cabac = amf::lifecycle::resolve_h264_cabac(1); + const auto cavlc = amf::lifecycle::resolve_h264_cabac(2); + return !automatic && cabac && *cabac == 1 && cavlc && *cavlc == 0; + } + + bool native_selection_never_routes_to_legacy() { + const auto automatic = amf::lifecycle::encoder_selection_policy(""); + const auto native = amf::lifecycle::encoder_selection_policy("amdvce"); + const auto legacy = amf::lifecycle::encoder_selection_policy("amdvce_legacy"); + + return !automatic.include_legacy && !automatic.fail_closed && + !native.include_legacy && native.fail_closed && + legacy.include_legacy && !legacy.fail_closed; + } + + bool xbox_intra_refresh_maps_to_native_amf() { + const auto disabled = amf::lifecycle::resolve_intra_refresh(false, 0, 1920, 1080); + const auto invalid = amf::lifecycle::resolve_intra_refresh(true, 3, 1920, 1080); + const auto h264 = amf::lifecycle::resolve_intra_refresh(true, 0, 1920, 1080); + const auto h264_odd = amf::lifecycle::resolve_intra_refresh(true, 0, 1919, 1079); + const auto hevc = amf::lifecycle::resolve_intra_refresh(true, 1, 1920, 1080); + const auto av1 = amf::lifecycle::resolve_intra_refresh(true, 2, 1920, 1080); + + return !disabled.enabled && !disabled.blocks_per_slot && + !invalid.enabled && + h264.enabled && h264.blocks_per_slot && *h264.blocks_per_slot == 28 && + h264.minimum_reference_frames == 2 && h264.disable_ltr && + h264_odd.blocks_per_slot && *h264_odd.blocks_per_slot == 28 && + hevc.enabled && hevc.blocks_per_slot && *hevc.blocks_per_slot == 2 && + hevc.minimum_reference_frames == 0 && !hevc.disable_ltr && + av1.enabled && !av1.blocks_per_slot && + av1.av1_mode && *av1.av1_mode == amf::lifecycle::av1_continuous_intra_refresh_mode && + av1.av1_cycle_frames && + *av1.av1_cycle_frames == amf::lifecycle::intra_refresh_period_frames; + } + + bool effective_reference_frame_limit_matches_configure_and_verify() { + using amf::lifecycle::effective_reference_frame_limit; + + // No client limit and no intra-refresh minimum: leave the driver default. + const auto untouched = effective_reference_frame_limit(0, 0); + const auto untouched_negative = effective_reference_frame_limit(-1, 0); + // "No limit" clients still get the H.264 intra-refresh floor. + const auto floored = effective_reference_frame_limit(0, 2); + // The non-RFI single-reference client is raised to the floor — and the + // post-Init verify must expect the raised value, not the client value. + const auto raised = effective_reference_frame_limit(1, 2); + // A client above the floor keeps its own limit. + const auto client_wins = effective_reference_frame_limit(4, 2); + // Without intra refresh the client limit passes through untouched. + const auto passthrough = effective_reference_frame_limit(3, 0); + + return !untouched && !untouched_negative && + floored && *floored == 2 && + raised && *raised == 2 && + client_wins && *client_wins == 4 && + passthrough && *passthrough == 3; + } + + bool repeated_input_rotates_away_from_a_lookahead_owned_surface() { + std::array slots; + slots[0].state = amf::lifecycle::input_surface_state_e::in_flight; + slots[1].state = amf::lifecycle::input_surface_state_e::free; + slots[2].state = amf::lifecycle::input_surface_state_e::in_flight; + const auto rotated = amf::lifecycle::select_repeat_surface(slots, 0, 1); + + slots[0].state = amf::lifecycle::input_surface_state_e::free; + const auto reused = amf::lifecycle::select_repeat_surface(slots, 0, 2); + + for (auto &slot : slots) slot.state = amf::lifecycle::input_surface_state_e::in_flight; + const auto unavailable = amf::lifecycle::select_repeat_surface(slots, 0, 1); + return rotated && *rotated == 1 && reused && *reused == 0 && !unavailable; + } + + bool surface_pool_can_prime_a_retaining_driver() { + constexpr auto active_slots = amf::lifecycle::initial_input_surface_count(1); + static_assert(active_slots == 6); + + std::array slots; + std::deque retained; + std::size_t accepted = 0; + std::size_t outputs = 0; + + // This fake driver retains four inputs before producing its first output. + // A three-surface pool deadlocks before submission four even without PA; the + // queue-aware pool must keep ownership exact and make enough forward progress. + for (uint64_t frame = 1; frame <= 8; ++frame) { + std::optional free_slot; + for (std::size_t slot = 0; slot < active_slots; ++slot) { + if (slots[slot].state == amf::lifecycle::input_surface_state_e::free) { + free_slot = slot; + break; + } + } + if (!free_slot) { + return false; + } + + auto &input = slots[*free_slot]; + input.state = amf::lifecycle::input_surface_state_e::reserved; + input.release_notified = false; + amf::lifecycle::on_input_accepted(input, frame); + retained.push_back(*free_slot); + ++accepted; + + if (retained.size() >= 4) { + auto &released = slots[retained.front()]; + retained.pop_front(); + if (!amf::lifecycle::on_surface_released(released)) { + return false; + } + ++outputs; + } + } + + return accepted == 8 && outputs == 5 && + amf::lifecycle::input_surface_count_for_lookahead(0) == 4 && + amf::lifecycle::input_surface_count_for_lookahead(2) == 8 && + amf::lifecycle::initial_input_surface_count(0) == 4 && + amf::lifecycle::initial_input_surface_count(1) == 6 && + amf::lifecycle::initial_input_surface_count(100) == + amf::lifecycle::maximum_input_surface_count; + } + + bool driver_submit_capacity_bounds_are_inclusive() { + return amf::lifecycle::driver_submit_capacity_available(4, 4) && + !amf::lifecycle::driver_submit_capacity_available(5, 4) && + amf::lifecycle::driver_submit_capacity_available(16, 16) && + !amf::lifecycle::driver_submit_capacity_available(17, 16); + } + + bool saturation_wait_requires_an_actual_surface_release() { + using amf::lifecycle::saturation_wait_should_finish; + return !saturation_wait_should_finish(false, false) && + saturation_wait_should_finish(false, true) && + saturation_wait_should_finish(true, false); + } + + bool output_poll_rearm_survives_concurrent_submission() { + const uint64_t queried_through = 10; + return amf::lifecycle::should_disarm_output_poll(queried_through, 10, false, 0) && + !amf::lifecycle::should_disarm_output_poll(queried_through, 10, false, 1) && + !amf::lifecycle::should_disarm_output_poll(queried_through, 11, false, 0) && + !amf::lifecycle::should_disarm_output_poll(queried_through, 10, true, 0); + } + + bool asynchronous_pipeline_catches_up_to_current_output() { + using amf::lifecycle::output_coalesce_budget; + using amf::lifecycle::driver_wait_budget; + using amf::lifecycle::output_coalesce_target_reached; + + return output_coalesce_budget(30) == std::chrono::milliseconds(32) && + output_coalesce_budget(60) == std::chrono::milliseconds(16) && + output_coalesce_budget(120) == std::chrono::milliseconds(8) && + output_coalesce_budget(240) == std::chrono::milliseconds(4) && + output_coalesce_budget(1000) == std::chrono::milliseconds(1) && + driver_wait_budget(30) == std::chrono::milliseconds(20) && + driver_wait_budget(60) == std::chrono::milliseconds(16) && + driver_wait_budget(120) == std::chrono::milliseconds(8) && + driver_wait_budget(240) == std::chrono::milliseconds(4) && + !output_coalesce_target_reached(11, 10, 10, 10) && + !output_coalesce_target_reached(11, 10, 11, 10) && + output_coalesce_target_reached(11, 10, 11, 11) && + output_coalesce_target_reached(11, 10, 12, 12) && + output_coalesce_target_reached(10, 10, 10, 10) && + !output_coalesce_target_reached(10, 10, 11, 9) && + output_coalesce_target_reached(10, 10, 11, 10) && + !amf::lifecycle::output_delivery_is_due(1, 0, 1, false) && + amf::lifecycle::output_delivery_is_due(2, 0, 1, false) && + !amf::lifecycle::output_delivery_is_due(2, 1, 1, false) && + amf::lifecycle::output_delivery_is_due(2, 1, 1, true) && + !amf::lifecycle::retained_preanalysis_tail_exists(1, 0, 0) && + amf::lifecycle::retained_preanalysis_tail_exists(1, 0, 1) && + amf::lifecycle::retained_preanalysis_tail_exists(2, 1, 1) && + !amf::lifecycle::retained_preanalysis_tail_exists(3, 1, 1) && + !amf::lifecycle::retained_preanalysis_tail_exists(2, 2, 1) && + amf::lifecycle::preanalysis_tail_flush_is_due(true, false) && + !amf::lifecycle::preanalysis_tail_flush_is_due(true, true) && + !amf::lifecycle::preanalysis_tail_flush_is_due(false, false); + } + + bool preanalysis_target_tracks_accepted_indices_with_gaps() { + std::deque accepted; + const auto first = amf::lifecycle::record_accepted_frame(accepted, 1, 1); + // Frame 2 was rejected and is intentionally absent from this sequence. + const auto third = amf::lifecycle::record_accepted_frame(accepted, 3, 1); + const auto fourth = amf::lifecycle::record_accepted_frame(accepted, 4, 1); + return !first && third && *third == 1 && fourth && *fourth == 3 && + amf::lifecycle::output_coalesce_target_reached(*third, 0, 1, 1) && + !amf::lifecycle::output_coalesce_target_reached(*fourth, 1, 2, 1) && + amf::lifecycle::output_coalesce_target_reached(*fourth, 1, 2, 3); + } + + bool teardown_timeout_returns_control_before_a_wedged_destructor() { + struct slow_resource_t { + std::atomic *destroyed; + ~slow_resource_t() { + std::this_thread::sleep_for(150ms); + destroyed->store(true, std::memory_order_release); + } + }; + + std::atomic destroyed {false}; + auto resource = std::make_unique(); + resource->destroyed = &destroyed; + const auto start = std::chrono::steady_clock::now(); + const bool completed = amf::lifecycle::run_with_timeout( + [resource = std::move(resource)]() mutable { resource.reset(); }, + 5ms); + const auto elapsed = std::chrono::steady_clock::now() - start; + + const auto cleanup_deadline = std::chrono::steady_clock::now() + 1s; + while (!destroyed.load(std::memory_order_acquire) && std::chrono::steady_clock::now() < cleanup_deadline) { + std::this_thread::sleep_for(1ms); + } + // Generous bound: the point is "returned promptly instead of blocking on the + // wedged destructor", not a scheduler-sensitive exact latency. + return !completed && elapsed < 2s && destroyed.load(std::memory_order_acquire); + } + + bool runtime_gate_fences_initialization_against_teardown_and_quarantine() { + amf::lifecycle::native_runtime_gate_t gate; + if (!gate.try_begin_initialization()) { + return false; + } + std::atomic teardown_entered {false}; + std::thread teardown {[&]() { + if (gate.begin_teardown()) { + teardown_entered.store(true, std::memory_order_release); + gate.finish_teardown(true); + } + }}; + std::this_thread::sleep_for(5ms); + if (teardown_entered.load(std::memory_order_acquire) || gate.runtime_is_idle()) { + gate.cancel_initialization(); + teardown.join(); + return false; + } + gate.cancel_initialization(); + teardown.join(); + if (!gate.runtime_is_idle() || !gate.try_begin_initialization()) { + return false; + } + gate.quarantine_initialization(); + return !gate.finish_initialization() && gate.is_quarantined() && + !gate.runtime_is_idle() && !gate.try_begin_initialization(); + } + + bool timed_out_teardown_quarantines_and_retains_the_runtime_fence() { + amf::lifecycle::native_runtime_gate_t gate; + if (!gate.begin_teardown()) return false; + gate.finish_teardown(false); + const auto start = std::chrono::steady_clock::now(); + const bool second_teardown_entered = gate.begin_teardown(); + return !second_teardown_entered && + std::chrono::steady_clock::now() - start < 50ms && + gate.is_quarantined() && gate.operation_in_progress() && + !gate.try_begin_initialization() && !gate.runtime_is_idle(); + } + + bool timed_out_worker_handoff_reaps_on_the_producer_thread() { + struct resource_t { + std::thread::id *destroyed_on; + ~resource_t() { *destroyed_on = std::this_thread::get_id(); } + }; + + auto handoff = std::make_shared>>(); + std::thread::id destroyed_on; + std::thread::id worker_id; + std::thread worker {[&]() { + worker_id = std::this_thread::get_id(); + std::this_thread::sleep_for(20ms); + auto resource = std::make_unique(); + resource->destroyed_on = &destroyed_on; + handoff->publish(std::move(resource)); + }}; + const auto accepted = handoff->accept_until( + std::chrono::steady_clock::now() + 2ms, + []() { return false; }); + worker.join(); + return !accepted && destroyed_on == worker_id; + } + + bool normal_cancellation_does_not_quarantine_the_runtime() { + amf::lifecycle::native_runtime_gate_t gate; + if (!gate.try_begin_initialization()) return false; + auto handoff = std::make_shared>(); + bool was_cancelled = false; + const auto accepted = handoff->accept_until( + std::chrono::steady_clock::now() + 1s, + []() { return true; }, + &was_cancelled); + gate.cancel_initialization(); + return !accepted && was_cancelled && !gate.is_quarantined() && + gate.runtime_is_idle(); + } + + bool teardown_gate_contention_respects_its_deadline() { + amf::lifecycle::native_runtime_gate_t gate; + if (!gate.try_begin_initialization()) return false; + const auto start = std::chrono::steady_clock::now(); + const bool entered = gate.begin_teardown_until(start + 2ms); + const auto elapsed = std::chrono::steady_clock::now() - start; + gate.cancel_initialization(); + return !entered && elapsed < 2s && !gate.is_quarantined() && + gate.runtime_is_idle(); + } + + bool smart_access_video_avoids_aggressive_low_latency_driver_path() { + const auto automatic = amf::lifecycle::resolve_smart_access_video(std::nullopt, std::nullopt); + const auto sav_only = amf::lifecycle::resolve_smart_access_video(true, std::nullopt); + const auto low_latency_only = amf::lifecycle::resolve_smart_access_video(std::nullopt, true); + const auto unsafe_combination = amf::lifecycle::resolve_smart_access_video(true, true); + const auto explicit_opt_out = amf::lifecycle::resolve_smart_access_video(false, true); + + return !automatic.enabled && !automatic.disabled_for_low_latency && + sav_only.enabled && *sav_only.enabled && !sav_only.disabled_for_low_latency && + !low_latency_only.enabled && !low_latency_only.disabled_for_low_latency && + unsafe_combination.enabled && !*unsafe_combination.enabled && unsafe_combination.disabled_for_low_latency && + explicit_opt_out.enabled && !*explicit_opt_out.enabled && !explicit_opt_out.disabled_for_low_latency; + } + + bool concurrent_native_session_remains_usable() { + const auto first = amf::lifecycle::resolve_concurrent_session_features(1, true, true); + const auto second = amf::lifecycle::resolve_concurrent_session_features(2, true, true); + const auto automatic = amf::lifecycle::resolve_concurrent_session_features( + 2, + std::nullopt, + std::nullopt); + const auto explicit_opt_out = amf::lifecycle::resolve_concurrent_session_features(2, false, false); + + return first.low_latency_mode && *first.low_latency_mode && + first.high_motion_quality_boost && *first.high_motion_quality_boost && + !first.overrides_suppressed && + !second.low_latency_mode && !second.high_motion_quality_boost && + second.overrides_suppressed && + !automatic.low_latency_mode && !automatic.high_motion_quality_boost && + !automatic.overrides_suppressed && + explicit_opt_out.low_latency_mode && !*explicit_opt_out.low_latency_mode && + explicit_opt_out.high_motion_quality_boost && !*explicit_opt_out.high_motion_quality_boost && + !explicit_opt_out.overrides_suppressed; + } + +} // namespace + +#ifdef SUNSHINE_AMF_LIFECYCLE_STANDALONE + +int main() { + return synchronous_release_during_submit_is_reentrant_safe() && + backpressure_retries_the_same_submission_until_accepted() && + exhausted_backpressure_reinitializes_without_owned_surfaces() && + recovery_state_changes_only_after_accepted_input() && + preanalysis_dependent_rate_control_is_planned_natively() && + preanalysis_pipeline_primes_and_drains_in_order() && + automatic_h264_coder_preserves_driver_default() && + native_selection_never_routes_to_legacy() && + xbox_intra_refresh_maps_to_native_amf() && + effective_reference_frame_limit_matches_configure_and_verify() && + repeated_input_rotates_away_from_a_lookahead_owned_surface() && + surface_pool_can_prime_a_retaining_driver() && + driver_submit_capacity_bounds_are_inclusive() && + saturation_wait_requires_an_actual_surface_release() && + output_poll_rearm_survives_concurrent_submission() && + asynchronous_pipeline_catches_up_to_current_output() && + preanalysis_target_tracks_accepted_indices_with_gaps() && + teardown_timeout_returns_control_before_a_wedged_destructor() && + runtime_gate_fences_initialization_against_teardown_and_quarantine() && + timed_out_teardown_quarantines_and_retains_the_runtime_fence() && + timed_out_worker_handoff_reaps_on_the_producer_thread() && + normal_cancellation_does_not_quarantine_the_runtime() && + teardown_gate_contention_respects_its_deadline() && + smart_access_video_avoids_aggressive_low_latency_driver_path() && + concurrent_native_session_remains_usable() ? + 0 : + 1; +} + +#else + +TEST(SunshineNativeAmfReview, SynchronousReleaseDuringSubmitIsReentrantSafe) { + EXPECT_TRUE(synchronous_release_during_submit_is_reentrant_safe()); +} + +TEST(SunshineNativeAmfReview, BackpressureRetriesUntilAccepted) { + EXPECT_TRUE(backpressure_retries_the_same_submission_until_accepted()); +} + +TEST(SunshineNativeAmfReview, ExhaustedBackpressureReinitializesWithoutOwnedSurfaces) { + EXPECT_TRUE(exhausted_backpressure_reinitializes_without_owned_surfaces()); +} + +TEST(SunshineNativeAmfReview, RecoveryStateChangesOnlyAfterAcceptance) { + EXPECT_TRUE(recovery_state_changes_only_after_accepted_input()); +} + +TEST(SunshineNativeAmfReview, PreAnalysisDependentRateControlIsPlannedNatively) { + EXPECT_TRUE(preanalysis_dependent_rate_control_is_planned_natively()); +} + +TEST(SunshineNativeAmfReview, PreAnalysisPipelinePrimesAndDrainsInOrder) { + EXPECT_TRUE(preanalysis_pipeline_primes_and_drains_in_order()); +} + +TEST(SunshineNativeAmfReview, AutomaticH264CoderPreservesDriverDefault) { + EXPECT_TRUE(automatic_h264_coder_preserves_driver_default()); +} + +TEST(SunshineNativeAmfReview, NativeSelectionNeverRoutesToLegacy) { + EXPECT_TRUE(native_selection_never_routes_to_legacy()); +} + +TEST(SunshineNativeAmfReview, XboxIntraRefreshMapsToNativeAmf) { + EXPECT_TRUE(xbox_intra_refresh_maps_to_native_amf()); +} + +TEST(SunshineNativeAmfReview, EffectiveReferenceFrameLimitMatchesConfigureAndVerify) { + EXPECT_TRUE(effective_reference_frame_limit_matches_configure_and_verify()); +} + + +TEST(SunshineNativeAmfReview, RepeatedInputRotatesAwayFromLookaheadOwnedSurface) { + EXPECT_TRUE(repeated_input_rotates_away_from_a_lookahead_owned_surface()); +} + +TEST(SunshineNativeAmfReview, SurfacePoolPrimesRetainingDriver) { + EXPECT_TRUE(surface_pool_can_prime_a_retaining_driver()); +} + +TEST(SunshineNativeAmfReview, PreanalysisPreservesLowLatencyQueue) { + EXPECT_TRUE(driver_submit_capacity_bounds_are_inclusive()); +} + +TEST(SunshineNativeAmfReview, SaturationWaitRequiresActualSurfaceRelease) { + EXPECT_TRUE(saturation_wait_requires_an_actual_surface_release()); +} + +TEST(SunshineNativeAmfReview, OutputPollRearmSurvivesConcurrentSubmission) { + EXPECT_TRUE(output_poll_rearm_survives_concurrent_submission()); +} + +TEST(SunshineNativeAmfReview, AsynchronousPipelineCatchesUpToCurrentOutput) { + EXPECT_TRUE(asynchronous_pipeline_catches_up_to_current_output()); +} + +TEST(SunshineNativeAmfReview, PreanalysisTargetTracksAcceptedIndicesWithGaps) { + EXPECT_TRUE(preanalysis_target_tracks_accepted_indices_with_gaps()); +} + +TEST(SunshineNativeAmfReview, TeardownTimeoutReturnsControl) { + EXPECT_TRUE(teardown_timeout_returns_control_before_a_wedged_destructor()); +} + +TEST(SunshineNativeAmfReview, RuntimeGateFencesInitializationAgainstTeardownAndQuarantine) { + EXPECT_TRUE(runtime_gate_fences_initialization_against_teardown_and_quarantine()); +} + +TEST(SunshineNativeAmfReview, TimedOutTeardownQuarantinesAndRetainsRuntimeFence) { + EXPECT_TRUE(timed_out_teardown_quarantines_and_retains_the_runtime_fence()); +} + +TEST(SunshineNativeAmfReview, TimedOutWorkerHandoffReapsOnProducerThread) { + EXPECT_TRUE(timed_out_worker_handoff_reaps_on_the_producer_thread()); +} + +TEST(SunshineNativeAmfReview, NormalCancellationDoesNotQuarantineRuntime) { + EXPECT_TRUE(normal_cancellation_does_not_quarantine_the_runtime()); +} + +TEST(SunshineNativeAmfReview, TeardownGateContentionRespectsDeadline) { + EXPECT_TRUE(teardown_gate_contention_respects_its_deadline()); +} + +TEST(SunshineNativeAmfReview, SmartAccessVideoAvoidsAggressiveLowLatencyDriverPath) { + EXPECT_TRUE(smart_access_video_avoids_aggressive_low_latency_driver_path()); +} + +TEST(SunshineNativeAmfReview, ConcurrentNativeSessionRemainsUsable) { + EXPECT_TRUE(concurrent_native_session_remains_usable()); +} + +#endif