diff --git a/.gitignore b/.gitignore index 30818d52ae3..563b837d93e 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ # VSCode IDE .vscode/ +.vs/ # build directories build/ diff --git a/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake index 7643d1d9efc..31c10aa18fd 100644 --- a/cmake/compile_definitions/windows.cmake +++ b/cmake/compile_definitions/windows.cmake @@ -51,6 +51,7 @@ set(PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/windows/display_vram.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/display_ram.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/display_wgc.cpp" + "${CMAKE_SOURCE_DIR}/src/platform/windows/display_amd.cpp" "${CMAKE_SOURCE_DIR}/src/platform/windows/audio.cpp" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/src/ViGEmClient.cpp" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Client.h" diff --git a/src/platform/windows/display.h b/src/platform/windows/display.h index 3e035490394..4e8e8aae76d 100644 --- a/src/platform/windows/display.h +++ b/src/platform/windows/display.h @@ -4,6 +4,8 @@ */ #pragma once +#include +#include #include #include #include @@ -31,6 +33,17 @@ namespace platf::dxgi { dxgi->Release(); } + /** + * Windows DLL loader function helper for AMD Display Capture + * @param item library dll + */ + inline + void + FreeLibraryHelper(void *item) { + FreeLibrary((HMODULE) item); + } + + using hmodule_t = util::safe_ptr; using factory1_t = util::safe_ptr>; using dxgi_t = util::safe_ptr>; using dxgi1_t = util::safe_ptr>; @@ -177,6 +190,8 @@ namespace platf::dxgi { int height_before_rotation; int client_frame_rate; + int adapter_index; + int output_index; DXGI_FORMAT capture_format; D3D_FEATURE_LEVEL feature_level; @@ -429,4 +444,46 @@ namespace platf::dxgi { capture_e release_snapshot() override; }; + + class amd_capture_t { + + public: + amd_capture_t(); + ~amd_capture_t(); + + int + init(display_base_t *display, const ::video::config_t &config, int output_index); + capture_e + next_frame(std::chrono::milliseconds timeout, amf::AMFData** out); + capture_e + release_frame(); + + hmodule_t amfrt_lib; + amf_uint64 amf_version; + amf::AMFFactory *amf_factory; + + amf::AMFContextPtr context; + amf::AMFComponentPtr captureComp; + amf::AMFSurfacePtr capturedSurface; + amf_int64 capture_format; + AMFSize resolution; + }; + + + /** + * Display backend that uses AMD Display Capture with a hardware encoder. + * Main purpose is to capture AMD Fluid Motion Frames (AFMF) + */ + class display_amd_vram_t: public display_vram_t { + amd_capture_t dup; + + public: + int + init(const ::video::config_t &config, const std::string &display_name); + capture_e + snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override; + capture_e + release_snapshot() override; + }; + } // namespace platf::dxgi diff --git a/src/platform/windows/display_amd.cpp b/src/platform/windows/display_amd.cpp new file mode 100644 index 00000000000..c497b7b3f85 --- /dev/null +++ b/src/platform/windows/display_amd.cpp @@ -0,0 +1,183 @@ +/** + * @file src/platform/windows/display_amd.cpp + * @brief Display capture implementation using AMD Direct Capture + */ + +extern "C" { +#include +#include +} + +#include "display.h" +#include "misc.h" +#include "src/config.h" +#include "src/main.h" +#include "src/video.h" + +#include +#include +#include + +namespace platf { + using namespace std::literals; +} + +namespace platf::dxgi { + amd_capture_t::amd_capture_t() { + } + + amd_capture_t::~amd_capture_t() { + AMF_RESULT result; + + // Before terminating the Display Capture component, we need to drain the remaining frames + result = captureComp->Drain(); + if (result == AMF_OK) { + do { + result = captureComp->QueryOutput((amf::AMFData**) &capturedSurface); + Sleep(1); + } while (result != AMF_EOF); + } + captureComp->Terminate(); + + context->Terminate(); + captureComp = nullptr; + context = nullptr; + capturedSurface = nullptr; + } + + capture_e + amd_capture_t::release_frame() { + if (capturedSurface != nullptr) + { + capturedSurface = nullptr; + } + + return capture_e::ok; + } + + /** + * @brief Get the next frame from the producer thread. + * If not available, the capture thread blocks until one is, or the wait times out. + * @param timeout how long to wait for the next frame + * @param out pointer to AMFSurfacePtr + */ + capture_e + amd_capture_t::next_frame(std::chrono::milliseconds timeout, amf::AMFData** out) { + release_frame(); + + AMF_RESULT result; + auto capture_start = std::chrono::steady_clock::now(); + do { + result = captureComp->QueryOutput(out); + if (result == AMF_REPEAT) { + if (std::chrono::steady_clock::now() - capture_start >= timeout) { + return platf::capture_e::timeout; + } + Sleep(1); + } + } while (result == AMF_REPEAT); + + if (result != AMF_OK) { + return capture_e::timeout; + } + return capture_e::ok; + } + + + int + amd_capture_t::init(display_base_t *display, const ::video::config_t &config, int output_index) { + // We have to load AMF before calling the base init() because we will need it loaded + // when our test_capture() function is called. + amfrt_lib.reset(LoadLibraryW(AMF_DLL_NAME)); + if (!amfrt_lib) { + // Probably not an AMD GPU system + return -1; + } + + auto fn_AMFQueryVersion = (AMFQueryVersion_Fn) GetProcAddress((HMODULE) amfrt_lib.get(), AMF_QUERY_VERSION_FUNCTION_NAME); + auto fn_AMFInit = (AMFInit_Fn) GetProcAddress((HMODULE) amfrt_lib.get(), AMF_INIT_FUNCTION_NAME); + + if (!fn_AMFQueryVersion || !fn_AMFInit) { + BOOST_LOG(error) << "Missing required AMF function!"sv; + return -1; + } + + auto result = fn_AMFQueryVersion(&amf_version); + if (result != AMF_OK) { + BOOST_LOG(error) << "AMFQueryVersion() failed: "sv << result; + return -1; + } + + // We don't support anything older than AMF 1.4.30. We'll gracefully fall back to DDAPI. + if (amf_version < AMF_MAKE_FULL_VERSION(1, 4, 30, 0)) { + BOOST_LOG(warning) << "AMD Direct Capture is not supported on AMF version"sv + << AMF_GET_MAJOR_VERSION(amf_version) << '.' + << AMF_GET_MINOR_VERSION(amf_version) << '.' + << AMF_GET_SUBMINOR_VERSION(amf_version) << '.' + << AMF_GET_BUILD_VERSION(amf_version); + BOOST_LOG(warning) << "Consider updating your AMD graphics driver for better capture performance!"sv; + return -1; + } + + // Initialize AMF library + result = fn_AMFInit(AMF_FULL_VERSION, &amf_factory); + if (result != AMF_OK) { + BOOST_LOG(error) << "AMFInit() failed: "sv << result; + return -1; + } + + DXGI_ADAPTER_DESC adapter_desc; + display->adapter->GetDesc(&adapter_desc); + + // Bail if this is not an AMD GPU + if (adapter_desc.VendorId != 0x1002) { + return -1; + } + + // Create the capture context + result = amf_factory->CreateContext(&context); + + if (result != AMF_OK) { + BOOST_LOG(error) << "CreateContext() failed: "sv << result; + return -1; + } + + // Associate the context with our ID3D11Device. This will enable multithread protection on the device. + result = context->InitDX11(display->device.get()); + if (result != AMF_OK) { + BOOST_LOG(error) << "InitDX11() failed: "sv << result; + return -1; + } + + display->capture_format = DXGI_FORMAT_UNKNOWN; + + // Create the DisplayCapture component + result = amf_factory->CreateComponent(context, AMFDisplayCapture, &(captureComp)); + if (result != AMF_OK) { + BOOST_LOG(error) << "CreateComponent(AMFDisplayCapture) failed: "sv << result; + return -1; + } + + // Set parameters for non-blocking capture + captureComp->SetProperty(AMF_DISPLAYCAPTURE_MONITOR_INDEX, output_index); + captureComp->SetProperty(AMF_DISPLAYCAPTURE_FRAMERATE, AMFConstructRate(config.framerate, 1)); + captureComp->SetProperty(AMF_DISPLAYCAPTURE_MODE, AMF_DISPLAYCAPTURE_MODE_WAIT_FOR_PRESENT); + captureComp->SetProperty(AMF_DISPLAYCAPTURE_DUPLICATEOUTPUT, true); + + // Initialize capture + result = captureComp->Init(amf::AMF_SURFACE_UNKNOWN, 0, 0); + if (result != AMF_OK) { + BOOST_LOG(error) << "DisplayCapture::Init() failed: "sv << result; + return -1; + } + + captureComp->GetProperty(AMF_DISPLAYCAPTURE_FORMAT, &(capture_format)); + captureComp->GetProperty(AMF_DISPLAYCAPTURE_RESOLUTION, &(resolution)); + BOOST_LOG(info) << "Desktop resolution ["sv << resolution.width << 'x' << resolution.height << ']'; + + BOOST_LOG(info) << "Using AMD Direct Capture API for display capture"sv; + + return 0; + } + +} // namespace platf::dxgi diff --git a/src/platform/windows/display_base.cpp b/src/platform/windows/display_base.cpp index d4731c0ec0b..61690877488 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -565,6 +565,7 @@ namespace platf::dxgi { } if (desc.AttachedToDesktop && test_dxgi_duplication(adapter_tmp, output_tmp, false)) { + output_index = y; output = std::move(output_tmp); offset_x = desc.DesktopCoordinates.left; @@ -593,6 +594,7 @@ namespace platf::dxgi { } if (output) { + adapter_index = x; adapter = std::move(adapter_tmp); break; } @@ -1052,6 +1054,16 @@ namespace platf { */ std::shared_ptr display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { + if (config::video.capture == "amd" || config::video.capture.empty()) { + if (hwdevice_type == mem_type_e::dxgi) { + auto disp = std::make_shared(); + + if (!disp->init(config, display_name)) { + return disp; + } + } + } + if (config::video.capture == "ddx" || config::video.capture.empty()) { if (hwdevice_type == mem_type_e::dxgi) { auto disp = std::make_shared(); diff --git a/src/platform/windows/display_vram.cpp b/src/platform/windows/display_vram.cpp index ba2b0685187..6fc75017f03 100644 --- a/src/platform/windows/display_vram.cpp +++ b/src/platform/windows/display_vram.cpp @@ -23,6 +23,7 @@ extern "C" { #include "src/video.h" #include +#include #include @@ -1629,6 +1630,94 @@ namespace platf::dxgi { return 0; } + + int + display_amd_vram_t::init(const ::video::config_t &config, const std::string &display_name) { + if (display_base_t::init(config, display_name) || dup.init(this, config, output_index)) + { + BOOST_LOG(error) << "AMD VRAM() failed"; + return -1; + } + + return 0; + } + + + /** + * @brief Get the next frame from the Windows.Graphics.Capture API and copy it into a new snapshot texture. + * @param pull_free_image_cb call this to get a new free image from the video subsystem. + * @param img_out the captured frame is returned here + * @param timeout how long to wait for the next frame + * @param cursor_visible whether to capture the cursor + */ + capture_e + display_amd_vram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) { + amf::AMFSurfacePtr output; + D3D11_TEXTURE2D_DESC desc; + + // Check for display configuration change + auto capture_status = dup.next_frame(timeout, (amf::AMFData **) &output); + if (capture_status != capture_e::ok) { + return capture_status; + } + dup.capturedSurface = output; + + texture2d_t src = (ID3D11Texture2D*) dup.capturedSurface->GetPlaneAt(0)->GetNative(); + src->GetDesc(&desc); + + // It's possible for our display enumeration to race with mode changes and result in + // mismatched image pool and desktop texture sizes. If this happens, just reinit again. + if (desc.Width != width_before_rotation || desc.Height != height_before_rotation) { + BOOST_LOG(info) << "Capture size changed ["sv << width << 'x' << height << " -> "sv << desc.Width << 'x' << desc.Height << ']'; + return capture_e::reinit; + } + + // If we don't know the capture format yet, grab it from this texture + if (capture_format == DXGI_FORMAT_UNKNOWN) { + capture_format = desc.Format; + BOOST_LOG(info) << "AMD Capture format ["sv << dxgi_format_to_string(capture_format) << ']'; + } + + // It's also possible for the capture format to change on the fly. If that happens, + // reinitialize capture to try format detection again and create new images. + if (capture_format != desc.Format) { + BOOST_LOG(info) << "AMD Capture format changed ["sv << dxgi_format_to_string(capture_format) << " -> "sv << dxgi_format_to_string(desc.Format) << ']'; + return capture_e::reinit; + } + + std::shared_ptr img; + if (!pull_free_image_cb(img)) + return capture_e::interrupted; + + auto d3d_img = std::static_pointer_cast(img); + d3d_img->blank = false; // image is always ready for capture + if (complete_img(d3d_img.get(), false) == 0) { + texture_lock_helper lock_helper(d3d_img->capture_mutex.get()); + if (lock_helper.lock()) { + device_ctx->CopyResource(d3d_img->capture_texture.get(), src.get()); + } + else { + return capture_e::error; + } + } + else { + return capture_e::error; + } + img_out = img; + if (img_out) { + img_out->frame_timestamp = std::chrono::steady_clock::now(); + } + + src.release(); + return capture_e::ok; + } + + capture_e + display_amd_vram_t::release_snapshot() { + dup.release_frame(); + return capture_e::ok; + } + /** * Get the next frame from the Windows.Graphics.Capture API and copy it into a new snapshot texture. * @param pull_free_image_cb call this to get a new free image from the video subsystem. diff --git a/src/video.cpp b/src/video.cpp index 5f810860441..5477dabbbde 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -709,6 +709,7 @@ namespace video { { "rc"s, &config::video.amd.amd_rc_av1 }, { "usage"s, &config::video.amd.amd_usage_av1 }, { "enforce_hrd"s, &config::video.amd.amd_enforce_hrd }, + { "loglevel"s, "debug"s}, }, {}, // SDR-specific options {}, // HDR-specific options @@ -734,6 +735,7 @@ namespace video { { "usage"s, &config::video.amd.amd_usage_hevc }, { "vbaq"s, &config::video.amd.amd_vbaq }, { "enforce_hrd"s, &config::video.amd.amd_enforce_hrd }, + { "loglevel"s, "debug"s}, }, {}, // SDR-specific options {}, // HDR-specific options @@ -757,6 +759,7 @@ namespace video { { "usage"s, &config::video.amd.amd_usage_h264 }, { "vbaq"s, &config::video.amd.amd_vbaq }, { "enforce_hrd"s, &config::video.amd.amd_enforce_hrd }, + { "loglevel"s, "debug"s}, }, {}, // SDR-specific options {}, // HDR-specific options diff --git a/src_assets/common/assets/web/configs/tabs/Advanced.vue b/src_assets/common/assets/web/configs/tabs/Advanced.vue index bd11adf2892..d5af93a8f5c 100644 --- a/src_assets/common/assets/web/configs/tabs/Advanced.vue +++ b/src_assets/common/assets/web/configs/tabs/Advanced.vue @@ -73,6 +73,7 @@ const config = ref(props.config)