Ctrl K
ComunidadGuías…

server/src/amf/amf_d3d11.cpp

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * @file src/amf/amf_d3d11.cpp
 * @brief Implementation of standalone AMF encoder with D3D11 texture input.
 */

#include "amf_d3d11.h"

#include <algorithm>
#include <chrono>
#include <exception>
#include <thread>

#include <AMF/components/ColorSpace.h>
#include <AMF/components/ComponentCaps.h>
#include <AMF/components/PreAnalysis.h>
#include <AMF/components/VideoEncoderAV1.h>
#include <AMF/components/VideoEncoderHEVC.h>
#include <AMF/components/VideoEncoderVCE.h>
#include <AMF/core/Surface.h>

#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"ArtLight ServerSlotGeneration";

  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<uint64_t>(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<ID3D11Texture2D>
  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<ID3D11Texture2D> 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<AMFQueryVersion_Fn>(GetProcAddress(amf_dll, AMF_QUERY_VERSION_FUNCTION_NAME));
    auto amf_init = reinterpret_cast<AMFInit_Fn>(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<int64_t>(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;
    max_au_size_supported = false;

    ::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;
    configured_rate_control_mode.reset();
    skip_frame_control_verified = false;

    const bool suppress_preanalysis_for_10bit = colorspace.bit_depth == 10;
    if (suppress_preanalysis_for_10bit &&
        (config.preanalysis.value_or(0) != 0 ||
         (config.rc_mode && lifecycle::rate_control_requires_preanalysis(*config.rc_mode)))) {
      BOOST_LOG(info) << "AMF: disabling PreAnalysis for a 10-bit encode session "
                      << "because the PreAnalysis component only consumes NV12";
    }
    // QVBR/HQVBR/HQCBR are documented as supported only while PreAnalysis is
    // enabled, so disabling PA alone still leaves Init returning
    // AMF_NOT_SUPPORTED on a 10-bit session. Demote the mode as well; SDR is
    // untouched because the suppression only triggers at bit_depth == 10.
    const auto effective_rc_mode = lifecycle::effective_rate_control(
      config.rc_mode,
      suppress_preanalysis_for_10bit);
    if (config.rc_mode && effective_rc_mode != config.rc_mode) {
      BOOST_LOG(warning) << "AMF: rate-control mode " << *config.rc_mode
                         << " requires PreAnalysis, which cannot run on a 10-bit (P010) pipeline;"
                         << " demoting to PEAK_CONSTRAINED_VBR (" << *effective_rc_mode
                         << ") so the HDR session can initialize";
    }
    const auto preanalysis_plan = lifecycle::resolve_preanalysis(
      config.rc_mode,
      config.preanalysis,
      suppress_preanalysis_for_10bit);
    // 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(effective_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;
    };
    enum class optional_property_result_e {
      applied,
      unavailable,
      failed,
    };
    auto is_definitely_unavailable = [](AMF_RESULT result) {
      return result == AMF_ACCESS_DENIED || result == AMF_NOT_IMPLEMENTED ||
             result == AMF_NOT_SUPPORTED || result == AMF_NOT_FOUND;
    };
    auto set_optional_property = [&](const wchar_t *property, auto requested, const char *label, bool invalid_arg_can_mean_unavailable) {
      const auto set_result = encoder->SetProperty(property, requested);
      decltype(requested) applied {};
      const auto get_result = encoder->GetProperty(property, &applied);
      // INVALID_ARG is ambiguous, so only classify it as an unavailable optional
      // property when both operations reject the property identically. This is
      // the old-VCE signature from the affected RX 580, not a blanket exemption
      // for bad values, types, modes, or lifecycle ordering.
      const bool invalid_arg_means_unavailable = invalid_arg_can_mean_unavailable &&
                                                 set_result == AMF_INVALID_ARG &&
                                                 get_result == AMF_INVALID_ARG;
      if (is_definitely_unavailable(set_result) || invalid_arg_means_unavailable) {
        BOOST_LOG(info) << "AMF: " << label << " is unavailable on this runtime"
                        << " (set=" << set_result << ", get=" << get_result
                        << "); continuing with the driver default";
        return optional_property_result_e::unavailable;
      }
      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 optional_property_result_e::failed;
      }
      return optional_property_result_e::applied;
    };
    auto set_optional_max_au_size = [&](const wchar_t *property, amf_int64 requested, const char *label) {
      const auto set_result = encoder->SetProperty(property, requested);
      // A runtime can refuse a supplemental property as private, unimplemented,
      // unsupported, or absent. Those outcomes mean the optional cap does not
      // exist for us; only other results indicate a genuine application failure.
      if (is_definitely_unavailable(set_result)) {
        BOOST_LOG(info) << "AMF: " << label << " is unavailable on this runtime"
                        << " (set=" << set_result << "); continuing without the optional cap";
        return true;
      }
      if (set_result != AMF_OK) {
        BOOST_LOG(warning) << "AMF: failed to apply " << label << " (requested=" << requested
                           << ", set=" << set_result << ')';
        return false;
      }

      amf_int64 applied = 0;
      const auto get_result = encoder->GetProperty(property, &applied);
      if (get_result != AMF_OK || applied != requested) {
        BOOST_LOG(warning) << "AMF: failed to verify " << label << " (requested=" << requested
                           << ", applied=" << applied << ", get=" << get_result << ')';
        return false;
      }

      max_au_size_supported = true;
      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<bool>(applied) != requested) {
        BOOST_LOG(warning) << "AMF: failed to apply " << label << " (requested=" << requested
                           << ", applied=" << static_cast<bool>(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<amf_int64>(*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<int>(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<int>(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<amf_int64>(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<amf_int64>(0));
        return;
      }

      max_ltr_frames = std::min({supported_frames, static_cast<int>(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, &current_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_optional_max_au_size(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<amf_int64>(*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) {
        const auto result = set_optional_property(
          AMF_VIDEO_ENCODER_HIGH_MOTION_QUALITY_BOOST_ENABLE,
          static_cast<amf_bool>(*config.high_motion_quality_boost_enable),
          "H.264 high-motion quality boost",
          true
        );
        if (result == optional_property_result_e::failed) {
          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_optional_max_au_size(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) {
        const auto result = set_optional_property(
          AMF_VIDEO_ENCODER_HEVC_HIGH_MOTION_QUALITY_BOOST_ENABLE,
          static_cast<amf_bool>(*config.high_motion_quality_boost_enable),
          "HEVC high-motion quality boost",
          true
        );
        if (result == optional_property_result_e::failed) {
          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_optional_max_au_size(
              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) {
        const auto result = set_optional_property(
          AMF_VIDEO_ENCODER_AV1_HIGH_MOTION_QUALITY_BOOST,
          static_cast<amf_bool>(*config.high_motion_quality_boost_enable),
          "AV1 high-motion quality boost",
          true
        );
        if (result == optional_property_result_e::failed) {
          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<amf_int64>(*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.
    // Older VCE runtimes can deny the default latency-constrained mode even though
    // their usage preset provides a working streaming rate control. In that case,
    // leave this optional override unset and verify only modes the runtime accepted.
    const int requested_depth = preanalysis_plan.enabled ?
                                  std::max(1, config.pa_lookahead_depth.value_or(preanalysis_plan.lookahead_depth)) :
                                  0;
    if (effective_rc_mode) {
      const auto requested_mode = *effective_rc_mode;
      const auto result = set_optional_property(
        rate_control_property,
        static_cast<amf_int64>(requested_mode),
        "rate-control mode",
        true
      );
      if (result == optional_property_result_e::failed) {
        return false;
      }
      if (result == optional_property_result_e::unavailable) {
        // PA-dependent modes are a coupled feature request and cannot be replaced
        // by an unknown driver default without changing their semantics.
        if (lifecycle::rate_control_requires_preanalysis(requested_mode)) {
          BOOST_LOG(warning) << "AMF: rate-control mode " << requested_mode
                             << " requires PreAnalysis and cannot fall back to the usage-preset default";
          return false;
        }
        user_configured_rate_control = false;
        BOOST_LOG(warning) << "AMF: rate-control mode " << requested_mode
                           << " is unavailable; using the encoder usage-preset default";
      } else {
        configured_rate_control_mode = requested_mode;
      }
    }
    if (preanalysis_plan.enabled) {
      if (!set_verified_bool(preanalysis_property, true, "PreAnalysis")) {
        return false;
      }
      if (!set_verified_int64(
            AMF_PA_LOOKAHEAD_BUFFER_DEPTH,
            requested_depth,
            "PreAnalysis lookahead depth"
          )) {
        return false;
      }
      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;
    const auto skip_frame_result = set_optional_property(
      skip_frame_property,
      static_cast<amf_bool>(false),
      "rate-control frame skipping",
      true
    );
    if (skip_frame_result == optional_property_result_e::failed) {
      return false;
    }
    skip_frame_control_verified = skip_frame_result == optional_property_result_e::applied;

    // Gate on the mode that was actually written: a 10-bit demotion away from
    // QVBR makes the quality level meaningless and some runtimes reject it.
    if (config.qvbr_quality_level && configured_rate_control_mode && *configured_rate_control_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<amf_int64>(*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;
    }

    // Preserve the finite GOP used by the Xbox HEVC workaround. Select the
    // codec explicitly: accepting a property is not a reliable codec probe.
    const auto gdr_ctbs = lifecycle::hevc_gdr_ctbs_per_slot(
      client_config.enableIntraRefresh == 1,
      video_format,
      client_config.width,
      client_config.height);
    if (gdr_ctbs) {
      if (!set_verified_int64(AMF_VIDEO_ENCODER_HEVC_GOP_SIZE, lifecycle::hevc_gdr_gop_frames, "HEVC GDR GOP size") ||
          !set_verified_int64(AMF_VIDEO_ENCODER_HEVC_INTRA_REFRESH_NUM_CTBS_PER_SLOT, *gdr_ctbs, "HEVC GDR CTBs per slot")) {
        return false;
      }
      BOOST_LOG(info) << "AMF: configured HEVC intra refresh with GOP=" << lifecycle::hevc_gdr_gop_frames
                      << ", CTBs per slot=" << *gdr_ctbs;
    }

    // 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;
    }

    // Some AMD drivers regress on pre-RDNA hardware and fail H.264 encoder
    // creation/initialization outright when USAGE is set to
    // ULTRA_LOW_LATENCY, even though the same GPU encodes fine at the less
    // aggressive LOW_LATENCY usage preset. This mirrors the FFmpeg AMF path's
    // "usage=2" fallback_options entry for h264_amf (see amdvce_legacy in
    // video.cpp / GPUOpen-LibrariesAndSDKs/AMF#410): retry once, forcing
    // USAGE = AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY, before giving up on H.264.
    constexpr amf_int64 kUsageLowLatency = 2;  // AMF_VIDEO_ENCODER_USAGE_LOW_LATENCY
    const bool allow_h264_usage_fallback =
      video_format == 0 && (!config.usage || *config.usage != kUsageLowLatency);
    const int max_attempts = allow_h264_usage_fallback ? 2 : 1;
 

    amf_config attempt_config = config;
    auto amf_format = get_amf_format(buffer_format, colorspace.bit_depth);
    for (int attempt = 0; attempt < max_attempts; ++attempt) {
      if (attempt > 0) {
        attempt_config = config;
        attempt_config.usage = kUsageLowLatency;
        BOOST_LOG(warning) << "AMF: H.264 encoder creation/initialization failed;"
                              " retrying with USAGE=LOW_LATENCY (pre-RDNA driver workaround)";
      }

      // 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;
        if (attempt + 1 < max_attempts) continue;
        return false;
      }

      // Configure encoder properties (before Init)
      if (!configure_encoder(attempt_config, client_config, colorspace)) {
        encoder->Terminate();
        encoder = nullptr;
        if (attempt + 1 < max_attempts) continue;
        return false;
      }

      // Initialize encoder
      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;
        encoder->Terminate();
        encoder = nullptr;
        if (attempt + 1 < max_attempts) continue;
        return false;
      }

      // Successfully created and initialized the encoder
      break;
    }

    // Some runtimes accept a property before Init but substitute a different
    // value while constructing the hardware pipeline. Verify only properties
    // that configure_encoder() successfully applied; unavailable old-VCE
    // controls were deliberately left at their usage-preset defaults.
    if (preanalysis_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<bool>(applied_preanalysis)) {
        BOOST_LOG(error) << "AMF: driver changed the requested PreAnalysis state after Init"
                         << " (requested=true"
                         << ", applied=" << static_cast<bool>(applied_preanalysis)
                         << ", result=" << preanalysis_result << ')';
        return false;
      }
      // 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 (configured_rate_control_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 != *configured_rate_control_mode) {
        BOOST_LOG(error) << "AMF: driver changed the requested rate-control mode after Init"
                         << " (requested=" << *configured_rate_control_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<amf_int64>(*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(configured_rate_control_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<bool>(applied_vbaq)) {
          BOOST_LOG(error) << "AMF: driver enabled VBAQ with CQP after Init"
                           << " (applied=" << static_cast<bool>(applied_vbaq)
                           << ", result=" << vbaq_result << ')';
          return false;
        }
      }
    }

    if (skip_frame_control_verified) {
      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<bool>(applied_skip_frame)) {
        BOOST_LOG(error) << "AMF: driver changed the requested rate-control frame-skipping state after Init"
                         << " (requested=false, applied=" << static_cast<bool>(applied_skip_frame)
                         << ", result=" << skip_frame_result << ')';
        return false;
      }
    }

    // Mirrors the configure-time gate: the QVBR quality level is only written,
    // and therefore only verifiable, when QVBR survived the 10-bit demotion.
    if (config.qvbr_quality_level && configured_rate_control_mode && *configured_rate_control_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<amf_int64>(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<std::size_t>(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;
    configured_rate_control_mode.reset();
    skip_frame_control_verified = 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<uint64_t>(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<uint8_t *>(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<std::size_t> 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<std::size_t> {
          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<ID3D11DeviceContext> 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<amf_pts>(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<amf_int64>(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<int>(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;
    };

    // Intra refresh must not intercept the initial or recovery IDR requested by
    // the client. Apply only the negotiated codec's per-frame properties.
    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<amf_int64>(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<amf_int64>(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<int>(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<int>(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<uint64_t>(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<int64_t>(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 && max_au_size_supported) {
        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 && max_au_size_supported) {
        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 && max_au_size_supported) {
        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<amf_hdr_metadata> &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<AMFHDRMetadata *>(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<amf_uint32>(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<std::size_t> {
      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<amf_d3d11>
  create_amf_d3d11(ID3D11Device *d3d_device) {
    if (!d3d_device) return nullptr;

    auto enc = std::make_unique<amf_d3d11>(d3d_device);
    return enc;
  }

}  // namespace amf