/** * @file src/amf/amf_d3d11.h * @brief Declarations for AMF D3D11 encoder. */ #pragma once #include "amf_encoder.h" #include "amf_lifecycle.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace amf { /** * @brief AMF encoder using D3D11 for texture input. */ class amf_d3d11: public amf_encoder { public: explicit amf_d3d11(ID3D11Device *d3d_device); ~amf_d3d11(); bool create_encoder(const amf_config &config, const video::config_t &client_config, const video::sunshine_colorspace_t &colorspace, platf::pix_fmt_e buffer_format) override; void destroy_encoder() override; amf_encode_result encode_frame(uint64_t frame_index, bool force_idr) override; amf_encode_result drain_output(std::chrono::milliseconds timeout) override; bool has_output_due() override; bool has_completed_output() override; bool has_retained_preanalysis_tail() override; bool begin_drain() override; bool invalidate_ref_frames(uint64_t first_frame, uint64_t last_frame) override; bool set_bitrate(int bitrate_kbps) override; bool set_hdr_metadata(const std::optional &metadata) override; void * get_input_texture() override; ID3D11Texture2D * acquire_input_texture_for_render(); void cancel_input_texture_for_render(); private: bool init_amf_library(); bool configure_encoder(const amf_config &config, const video::config_t &client_config, const video::sunshine_colorspace_t &colorspace); AMF_SURFACE_FORMAT get_amf_format(platf::pix_fmt_e buffer_format, int bit_depth); const wchar_t * get_codec_id(); amf_encoded_frame extract_encoded_frame(const ::amf::AMFDataPtr &output_data); void output_pump(std::stop_token stop_token) noexcept; void on_input_surface_released(std::size_t slot_index, uint64_t generation) noexcept; bool ensure_input_surface_count(std::size_t count); // Safe to call with state_mutex released: device and input_surface_desc are // immutable while the encode thread runs. Pool expansion allocates through // this so a multi-MB CreateTexture2D never stalls the pump or AMF's // release observers behind the lock. Microsoft::WRL::ComPtr create_input_surface_texture(); ID3D11Device *device = nullptr; ::amf::AMFFactory *factory = nullptr; ::amf::AMFContextPtr context; ::amf::AMFComponentPtr encoder; HMODULE amf_dll = nullptr; // The converter renders directly into a reserved pool surface. AMF may retain an // accepted native surface while encoding it, so a slot is recycled exclusively by // AMFSurfaceObserver. The pool starts at the queue/lookahead-aware working depth // and allocates additional slots lazily during a transient driver backlog. static constexpr std::size_t INPUT_SURFACE_RING_SIZE = lifecycle::maximum_input_surface_count; using input_surface_state_e = lifecycle::input_surface_state_e; struct input_surface_release_observer_t final: ::amf::AMFSurfaceObserver { amf_d3d11 *owner = nullptr; std::size_t slot_index = 0; void AMF_STD_CALL OnSurfaceDataRelease(::amf::AMFSurface *surface) override; }; struct input_surface_slot_t: lifecycle::input_surface_state_t { Microsoft::WRL::ComPtr texture; // Bumped on every reservation; each AMF wrapper is stamped with the value // it was created under so a stale OnSurfaceDataRelease from a previous // wrapper can never recycle the slot's next occupant. uint64_t generation = 0; }; std::array input_surface_ring; std::array input_surface_release_observers; std::size_t active_input_surface_count = lifecycle::minimum_input_surface_count; std::size_t encoder_input_queue_size = lifecycle::default_amf_input_queue_size; D3D11_TEXTURE2D_DESC input_surface_desc {}; std::size_t next_input_surface_slot = 0; std::optional prepared_input_surface_slot; std::optional last_rendered_input_surface_slot; // Encoder state video::config_t current_config {}; int video_format = 0; // 0=H264, 1=HEVC, 2=AV1 AMF_SURFACE_FORMAT surface_format = AMF_SURFACE_NV12; int encode_width = 0; int encode_height = 0; bool rfi_pending = false; uint64_t last_rfi_ltr_index = 0; int max_ltr_frames = 0; bool rfi_enabled = false; // Current LTR state for RFI. // Slot 0 is reserved as the IDR baseline (set on every IDR, never overwritten by // periodic marks) so RFI always has a known-good fallback even when every recent // periodic-marked frame was inside a packet-loss window. Slots 1..N-1 form a // sliding window of more recent anchors. static constexpr int MAX_LTR_SLOTS = 4; static constexpr uint64_t LTR_MARK_INTERVAL = 4; // Mark LTR every N frames int effective_ltr_slots = 0; // Clamped to min(max_ltr_frames, MAX_LTR_SLOTS) int current_ltr_slot = 0; // Which LTR slot to mark next std::array ltr_slots_valid {}; std::array ltr_slot_frame_index {}; // Frame index when each LTR slot was marked // QueryOutput is owned by a dedicated pump. This keeps AMF driver completion work // off Sunshine's latency-critical encode thread and gives input-slot ownership one // synchronization boundary. std::mutex state_mutex; std::condition_variable state_cv; std::jthread output_thread; std::deque completed_outputs; std::unordered_map frame_rfi_flags; uint64_t last_completed_frame_index = 0; uint64_t last_submitted_frame_index = 0; bool output_fatal = false; bool drain_requested = false; bool drain_complete = false; bool output_poll_requested = false; std::size_t active_output_poll_waiters = 0; // Input ownership and output production are deliberately tracked separately. // AMF explicitly does not guarantee a one-to-one input/output relationship; // AMFSurfaceObserver owns the first count while QueryOutput owns the totals. std::size_t input_surfaces_in_flight = 0; uint64_t accepted_input_count = 0; uint64_t completed_output_count = 0; std::deque accepted_frame_indices; bool preanalysis_enabled = false; int preanalysis_lookahead_depth = 0; bool query_timeout_supported = false; bool user_configured_rate_control = false; std::optional configured_rate_control_mode; bool skip_frame_control_verified = false; bool enforce_hrd_enabled = false; bool max_au_size_supported = false; // Statistics feedback state bool statistics_enabled = false; bool psnr_enabled = false; bool ssim_enabled = false; // Runtime fault watchdog: count consecutive failures so we can signal // a fatal error to the upper layer (triggering a real reinit) instead // of silently producing no output forever. Threshold is derived from // client framerate in create_encoder() so the watchdog fires after // roughly the same wall-clock time regardless of fps. int consecutive_submit_failures = 0; int consecutive_surface_failures = 0; int consecutive_query_failures = 0; int consecutive_output_failures = 0; // Consecutive submissions whose bounded coalescing target was not reached. int consecutive_catchup_misses = 0; uint64_t catchup_batch_count = 0; int max_consecutive_failures = 60; // Set to ~1s of frames in create_encoder() std::chrono::steady_clock::time_point last_output_progress {}; std::chrono::steady_clock::time_point submit_backpressure_started {}; std::string last_error_string; }; /** * @brief Create an AMF D3D11 encoder instance. * @param d3d_device The D3D11 device to use. * @return AMF encoder or nullptr on failure. */ std::unique_ptr create_amf_d3d11(ID3D11Device *d3d_device); } // namespace amf