diff --git a/.codeql-prebuild-cpp-Windows.sh b/.codeql-prebuild-cpp-Windows.sh index 6259f493..ff2dc26d 100644 --- a/.codeql-prebuild-cpp-Windows.sh +++ b/.codeql-prebuild-cpp-Windows.sh @@ -13,6 +13,7 @@ pacman --noconfirm -S \ make \ mingw-w64-ucrt-x86_64-boost \ mingw-w64-ucrt-x86_64-cmake \ + mingw-w64-ucrt-x86_64-cppwinrt \ mingw-w64-ucrt-x86_64-curl \ mingw-w64-ucrt-x86_64-graphviz \ mingw-w64-ucrt-x86_64-miniupnpc \ diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index b9a2dd44..977eba51 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -942,6 +942,7 @@ jobs: git mingw-w64-ucrt-x86_64-boost mingw-w64-ucrt-x86_64-cmake + mingw-w64-ucrt-x86_64-cppwinrt mingw-w64-ucrt-x86_64-curl mingw-w64-ucrt-x86_64-graphviz mingw-w64-ucrt-x86_64-miniupnpc diff --git a/cmake/compile_definitions/windows.cmake b/cmake/compile_definitions/windows.cmake index 02807aed..fa5c0614 100644 --- a/cmake/compile_definitions/windows.cmake +++ b/cmake/compile_definitions/windows.cmake @@ -9,6 +9,9 @@ set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static") # gcc complains about misleading indentation in some mingw includes list(APPEND SUNSHINE_COMPILE_OPTIONS -Wno-misleading-indentation) +# see gcc bug 98723 +add_definitions(-DUSE_BOOST_REGEX) + # curl add_definitions(-DCURL_STATICLIB) include_directories(SYSTEM ${CURL_STATIC_INCLUDE_DIRS}) @@ -47,6 +50,7 @@ set(PLATFORM_TARGET_FILES "${CMAKE_SOURCE_DIR}/src/platform/windows/display_base.cpp" "${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/audio.cpp" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/src/ViGEmClient.cpp" "${CMAKE_SOURCE_DIR}/third-party/ViGEmClient/include/ViGEm/Client.h" diff --git a/cmake/targets/windows.cmake b/cmake/targets/windows.cmake index 341d7c2e..b7f8fbcf 100644 --- a/cmake/targets/windows.cmake +++ b/cmake/targets/windows.cmake @@ -3,4 +3,5 @@ set_target_properties(sunshine PROPERTIES LINK_SEARCH_START_STATIC 1) set(CMAKE_FIND_LIBRARY_SUFFIXES ".dll") find_library(ZLIB ZLIB1) list(APPEND SUNSHINE_EXTERNAL_LIBRARIES + Windowsapp.lib Wtsapi32.lib) diff --git a/docs/source/about/advanced_usage.rst b/docs/source/about/advanced_usage.rst index 12d1f2ff..1e606fa2 100644 --- a/docs/source/about/advanced_usage.rst +++ b/docs/source/about/advanced_usage.rst @@ -216,7 +216,7 @@ editing the `conf` file in a text editor. Use the examples as reference. .. code-block:: text gamepad = auto - + `ds4_back_as_touchpad_click `__ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -394,7 +394,7 @@ editing the `conf` file in a text editor. Use the examples as reference. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Description** - When enabled, Sunshine will pass through native pen/touch events from Moonlight clients. + When enabled, Sunshine will pass through native pen/touch events from Moonlight clients. This can be useful to disable for older applications without native pen/touch support. @@ -1113,25 +1113,25 @@ keybindings **Description** Force specific screen capture method. - .. caution:: Applies to Linux only. - **Choices** .. table:: :widths: auto - ========= =========== - Value Description - ========= =========== - nvfbc Use NVIDIA Frame Buffer Capture to capture direct to GPU memory. This is usually the fastest method for - NVIDIA cards. For GeForce cards it will only work with drivers patched with - `nvidia-patch `__ - or `nvlax `__. - wlr Capture for wlroots based Wayland compositors via DMA-BUF. - kms DRM/KMS screen capture from the kernel. This requires that sunshine has cap_sys_admin capability. - See :ref:`Linux Setup `. - x11 Uses XCB. This is the slowest and most CPU intensive so should be avoided if possible. - ========= =========== + ========= ======== =========== + Value Platform Description + ========= ======== =========== + nvfbc Linux Use NVIDIA Frame Buffer Capture to capture direct to GPU memory. This is usually the fastest method for + NVIDIA cards. For GeForce cards it will only work with drivers patched with + `nvidia-patch `__ + or `nvlax `__. + wlr Linux Capture for wlroots based Wayland compositors via DMA-BUF. + kms Linux DRM/KMS screen capture from the kernel. This requires that sunshine has cap_sys_admin capability. + See :ref:`Linux Setup `. + x11 Linux Uses XCB. This is the slowest and most CPU intensive so should be avoided if possible. + ddx Windows Use DirectX Desktop Duplication API to capture the display. This is well-supported on Windows machines. + wgc Windows (beta feature) Use Windows.Graphics.Capture to capture the display. + ========= ======== =========== **Default** Automatic. Sunshine will use the first capture method available in the order of the table above. diff --git a/docs/source/building/windows.rst b/docs/source/building/windows.rst index eb5444f4..084c492f 100644 --- a/docs/source/building/windows.rst +++ b/docs/source/building/windows.rst @@ -19,6 +19,7 @@ Install dependencies: git \ mingw-w64-ucrt-x86_64-boost \ mingw-w64-ucrt-x86_64-cmake \ + mingw-w64-ucrt-x86_64-cppwinrt \ mingw-w64-ucrt-x86_64-curl \ mingw-w64-ucrt-x86_64-graphviz \ mingw-w64-ucrt-x86_64-miniupnpc \ diff --git a/src/platform/windows/display.h b/src/platform/windows/display.h index 2d480c59..1a3e7b58 100644 --- a/src/platform/windows/display.h +++ b/src/platform/windows/display.h @@ -11,6 +11,9 @@ #include #include +#include +#include + #include "src/platform/common.h" #include "src/utility.h" #include "src/video.h" @@ -153,22 +156,6 @@ namespace platf::dxgi { bool visible; }; - class duplication_t { - public: - dup_t dup; - bool has_frame {}; - std::chrono::steady_clock::time_point last_protected_content_warning_time {}; - - capture_e - next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p); - capture_e - reset(dup_t::pointer dup_p = dup_t::pointer()); - capture_e - release_frame(); - - ~duplication_t(); - }; - class display_base_t: public display_t { public: int @@ -185,7 +172,6 @@ namespace platf::dxgi { output_t output; device_t device; device_ctx_t device_ctx; - duplication_t dup; DXGI_RATIONAL display_refresh_rate; int display_refresh_rate_rounded; @@ -253,30 +239,32 @@ namespace platf::dxgi { virtual bool get_hdr_metadata(SS_HDR_METADATA &metadata) override; + const char * + dxgi_format_to_string(DXGI_FORMAT format); + const char * + colorspace_to_string(DXGI_COLOR_SPACE_TYPE type); + virtual std::vector + get_supported_capture_formats() = 0; + protected: int get_pixel_pitch() { return (capture_format == DXGI_FORMAT_R16G16B16A16_FLOAT) ? 8 : 4; } - const char * - dxgi_format_to_string(DXGI_FORMAT format); - const char * - colorspace_to_string(DXGI_COLOR_SPACE_TYPE type); - virtual 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) = 0; + virtual capture_e + release_snapshot() = 0; virtual int complete_img(img_t *img, bool dummy) = 0; - virtual std::vector - get_supported_capture_formats() = 0; }; + /** + * Display component for devices that use software encoders. + */ class display_ram_t: public display_base_t { public: - virtual 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; - std::shared_ptr alloc_img() override; int @@ -286,22 +274,18 @@ namespace platf::dxgi { std::vector get_supported_capture_formats() override; - int - init(const ::video::config_t &config, const std::string &display_name); - std::unique_ptr make_avcodec_encode_device(pix_fmt_e pix_fmt) override; - cursor_t cursor; D3D11_MAPPED_SUBRESOURCE img_info; texture2d_t texture; }; + /** + * Display component for devices that use hardware encoders. + */ class display_vram_t: public display_base_t, public std::enable_shared_from_this { public: - virtual 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; - std::shared_ptr alloc_img() override; int @@ -311,9 +295,6 @@ namespace platf::dxgi { std::vector get_supported_capture_formats() override; - int - init(const ::video::config_t &config, const std::string &display_name); - bool is_codec_supported(std::string_view name, const ::video::config_t &config) override; @@ -323,6 +304,59 @@ namespace platf::dxgi { std::unique_ptr make_nvenc_encode_device(pix_fmt_e pix_fmt) override; + std::atomic next_image_id; + }; + + /** + * Display duplicator that uses the DirectX Desktop Duplication API. + */ + class duplication_t { + public: + dup_t dup; + bool has_frame {}; + std::chrono::steady_clock::time_point last_protected_content_warning_time {}; + + int + init(display_base_t *display, const ::video::config_t &config); + capture_e + next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p); + capture_e + reset(dup_t::pointer dup_p = dup_t::pointer()); + capture_e + release_frame(); + + ~duplication_t(); + }; + + /** + * Display backend that uses DDAPI with a software encoder. + */ + class display_ddup_ram_t: public display_ram_t { + 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; + + duplication_t dup; + cursor_t cursor; + }; + + /** + * Display backend that uses DDAPI with a hardware encoder. + */ + class display_ddup_vram_t: public display_vram_t { + 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; + + duplication_t dup; sampler_state_t sampler_linear; blend_t blend_alpha; @@ -338,7 +372,64 @@ namespace platf::dxgi { texture2d_t old_surface_delayed_destruction; std::chrono::steady_clock::time_point old_surface_timestamp; std::variant> last_frame_variant; + }; - std::atomic next_image_id; + /** + * Display duplicator that uses the Windows.Graphics.Capture API. + */ + class wgc_capture_t { + winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice uwp_device { nullptr }; + winrt::Windows::Graphics::Capture::GraphicsCaptureItem item { nullptr }; + winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool frame_pool { nullptr }; + winrt::Windows::Graphics::Capture::GraphicsCaptureSession capture_session { nullptr }; + winrt::Windows::Graphics::Capture::Direct3D11CaptureFrame produced_frame { nullptr }, consumed_frame { nullptr }; + SRWLOCK frame_lock = SRWLOCK_INIT; + CONDITION_VARIABLE frame_present_cv; + + void + on_frame_arrived(winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const &sender, winrt::Windows::Foundation::IInspectable const &); + + public: + wgc_capture_t(); + ~wgc_capture_t(); + + int + init(display_base_t *display, const ::video::config_t &config); + capture_e + next_frame(std::chrono::milliseconds timeout, ID3D11Texture2D **out, uint64_t &out_time); + capture_e + release_frame(); + int + set_cursor_visible(bool); + }; + + /** + * Display backend that uses Windows.Graphics.Capture with a software encoder. + */ + class display_wgc_ram_t: public display_ram_t { + wgc_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; + }; + + /** + * Display backend that uses Windows.Graphics.Capture with a hardware encoder. + */ + class display_wgc_vram_t: public display_vram_t { + wgc_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_base.cpp b/src/platform/windows/display_base.cpp index 227efe76..8d7eb36b 100644 --- a/src/platform/windows/display_base.cpp +++ b/src/platform/windows/display_base.cpp @@ -26,6 +26,91 @@ namespace platf { namespace platf::dxgi { namespace bp = boost::process; + /** + * DDAPI-specific initialization goes here. + */ + int + duplication_t::init(display_base_t *display, const ::video::config_t &config) { + HRESULT status; + + // Capture format will be determined from the first call to AcquireNextFrame() + display->capture_format = DXGI_FORMAT_UNKNOWN; + + // FIXME: Duplicate output on RX580 in combination with DOOM (2016) --> BSOD + { + // IDXGIOutput5 is optional, but can provide improved performance and wide color support + dxgi::output5_t output5 {}; + status = display->output->QueryInterface(IID_IDXGIOutput5, (void **) &output5); + if (SUCCEEDED(status)) { + // Ask the display implementation which formats it supports + auto supported_formats = display->get_supported_capture_formats(); + if (supported_formats.empty()) { + BOOST_LOG(warning) << "No compatible capture formats for this encoder"sv; + return -1; + } + + // We try this twice, in case we still get an error on reinitialization + for (int x = 0; x < 2; ++x) { + // Ensure we can duplicate the current display + syncThreadDesktop(); + + status = output5->DuplicateOutput1((IUnknown *) display->device.get(), 0, supported_formats.size(), supported_formats.data(), &dup); + if (SUCCEEDED(status)) { + break; + } + std::this_thread::sleep_for(200ms); + } + + // We don't retry with DuplicateOutput() because we can hit this codepath when we're racing + // with mode changes and we don't want to accidentally fall back to suboptimal capture if + // we get unlucky and succeed below. + if (FAILED(status)) { + BOOST_LOG(warning) << "DuplicateOutput1 Failed [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + } + else { + BOOST_LOG(warning) << "IDXGIOutput5 is not supported by your OS. Capture performance may be reduced."sv; + + dxgi::output1_t output1 {}; + status = display->output->QueryInterface(IID_IDXGIOutput1, (void **) &output1); + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to query IDXGIOutput1 from the output"sv; + return -1; + } + + for (int x = 0; x < 2; ++x) { + // Ensure we can duplicate the current display + syncThreadDesktop(); + + status = output1->DuplicateOutput((IUnknown *) display->device.get(), &dup); + if (SUCCEEDED(status)) { + break; + } + std::this_thread::sleep_for(200ms); + } + + if (FAILED(status)) { + BOOST_LOG(error) << "DuplicateOutput Failed [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + } + } + + DXGI_OUTDUPL_DESC dup_desc; + dup->GetDesc(&dup_desc); + + BOOST_LOG(info) << "Desktop resolution ["sv << dup_desc.ModeDesc.Width << 'x' << dup_desc.ModeDesc.Height << ']'; + BOOST_LOG(info) << "Desktop format ["sv << display->dxgi_format_to_string(dup_desc.ModeDesc.Format) << ']'; + + display->display_refresh_rate = dup_desc.ModeDesc.RefreshRate; + double display_refresh_rate_decimal = (double) display->display_refresh_rate.Numerator / display->display_refresh_rate.Denominator; + BOOST_LOG(info) << "Display refresh rate [" << display_refresh_rate_decimal << "Hz]"; + BOOST_LOG(info) << "Requested frame rate [" << display->client_frame_rate << "fps]"; + display->display_refresh_rate_rounded = lround(display_refresh_rate_decimal); + return 0; + } + capture_e duplication_t::next_frame(DXGI_OUTDUPL_FRAME_INFO &frame_info, std::chrono::milliseconds timeout, resource_t::pointer *res_p) { auto capture_status = release_frame(); @@ -255,7 +340,7 @@ namespace platf::dxgi { return status; } - status = dup.release_frame(); + status = release_snapshot(); if (status != platf::capture_e::ok) { return status; } @@ -694,81 +779,7 @@ namespace platf::dxgi { } } - // FIXME: Duplicate output on RX580 in combination with DOOM (2016) --> BSOD - { - // IDXGIOutput5 is optional, but can provide improved performance and wide color support - dxgi::output5_t output5 {}; - status = output->QueryInterface(IID_IDXGIOutput5, (void **) &output5); - if (SUCCEEDED(status)) { - // Ask the display implementation which formats it supports - auto supported_formats = get_supported_capture_formats(); - if (supported_formats.empty()) { - BOOST_LOG(warning) << "No compatible capture formats for this encoder"sv; - return -1; - } - - // We try this twice, in case we still get an error on reinitialization - for (int x = 0; x < 2; ++x) { - // Ensure we can duplicate the current display - syncThreadDesktop(); - - status = output5->DuplicateOutput1((IUnknown *) device.get(), 0, supported_formats.size(), supported_formats.data(), &dup.dup); - if (SUCCEEDED(status)) { - break; - } - std::this_thread::sleep_for(200ms); - } - - // We don't retry with DuplicateOutput() because we can hit this codepath when we're racing - // with mode changes and we don't want to accidentally fall back to suboptimal capture if - // we get unlucky and succeed below. - if (FAILED(status)) { - BOOST_LOG(warning) << "DuplicateOutput1 Failed [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - } - else { - BOOST_LOG(warning) << "IDXGIOutput5 is not supported by your OS. Capture performance may be reduced."sv; - - dxgi::output1_t output1 {}; - status = output->QueryInterface(IID_IDXGIOutput1, (void **) &output1); - if (FAILED(status)) { - BOOST_LOG(error) << "Failed to query IDXGIOutput1 from the output"sv; - return -1; - } - - for (int x = 0; x < 2; ++x) { - // Ensure we can duplicate the current display - syncThreadDesktop(); - - status = output1->DuplicateOutput((IUnknown *) device.get(), &dup.dup); - if (SUCCEEDED(status)) { - break; - } - std::this_thread::sleep_for(200ms); - } - - if (FAILED(status)) { - BOOST_LOG(error) << "DuplicateOutput Failed [0x"sv << util::hex(status).to_string_view() << ']'; - return -1; - } - } - } - - DXGI_OUTDUPL_DESC dup_desc; - dup.dup->GetDesc(&dup_desc); - - BOOST_LOG(info) << "Desktop resolution ["sv << dup_desc.ModeDesc.Width << 'x' << dup_desc.ModeDesc.Height << ']'; - BOOST_LOG(info) << "Desktop format ["sv << dxgi_format_to_string(dup_desc.ModeDesc.Format) << ']'; - - display_refresh_rate = dup_desc.ModeDesc.RefreshRate; - double display_refresh_rate_decimal = (double) display_refresh_rate.Numerator / display_refresh_rate.Denominator; - BOOST_LOG(info) << "Display refresh rate [" << display_refresh_rate_decimal << "Hz]"; - display_refresh_rate_rounded = lround(display_refresh_rate_decimal); - client_frame_rate = config.framerate; - BOOST_LOG(info) << "Requested frame rate [" << client_frame_rate << "fps]"; - dxgi::output6_t output6 {}; status = output->QueryInterface(IID_IDXGIOutput6, (void **) &output6); if (SUCCEEDED(status)) { @@ -788,9 +799,6 @@ namespace platf::dxgi { << "Max Full Luminance : "sv << desc1.MaxFullFrameLuminance << " nits"sv; } - // Capture format will be determined from the first call to AcquireNextFrame() - capture_format = DXGI_FORMAT_UNKNOWN; - // Use CREATE_WAITABLE_TIMER_HIGH_RESOLUTION if supported (Windows 10 1809+) timer.reset(CreateWaitableTimerEx(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS)); if (!timer) { @@ -1046,23 +1054,47 @@ namespace platf::dxgi { } // namespace platf::dxgi namespace platf { + /** + * Pick a display adapter and capture method. + * @param hwdevice_type enables possible use of hardware encoder + */ std::shared_ptr display(mem_type_e hwdevice_type, const std::string &display_name, const video::config_t &config) { - if (hwdevice_type == mem_type_e::dxgi) { - auto disp = std::make_shared(); + if (config::video.capture == "ddx" || 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 (!disp->init(config, display_name)) { + return disp; + } } - } - else if (hwdevice_type == mem_type_e::system) { - auto disp = std::make_shared(); + else if (hwdevice_type == mem_type_e::system) { + auto disp = std::make_shared(); - if (!disp->init(config, display_name)) { - return disp; + if (!disp->init(config, display_name)) { + return disp; + } } } + if (config::video.capture == "wgc" || config::video.capture.empty()) { + if (hwdevice_type == mem_type_e::dxgi) { + auto disp = std::make_shared(); + + if (!disp->init(config, display_name)) { + return disp; + } + } + else if (hwdevice_type == mem_type_e::system) { + auto disp = std::make_shared(); + + if (!disp->init(config, display_name)) { + return disp; + } + } + } + + // ddx and wgc failed return nullptr; } diff --git a/src/platform/windows/display_ram.cpp b/src/platform/windows/display_ram.cpp index cbe37edf..0a8e1a8b 100644 --- a/src/platform/windows/display_ram.cpp +++ b/src/platform/windows/display_ram.cpp @@ -177,9 +177,8 @@ namespace platf::dxgi { } capture_e - display_ram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) { + display_ddup_ram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) { HRESULT status; - DXGI_OUTDUPL_FRAME_INFO frame_info; resource_t::pointer res_p {}; @@ -326,6 +325,11 @@ namespace platf::dxgi { return capture_e::ok; } + capture_e + display_ddup_ram_t::release_snapshot() { + return dup.release_frame(); + } + std::shared_ptr display_ram_t::alloc_img() { auto img = std::make_shared(); @@ -382,8 +386,8 @@ namespace platf::dxgi { } int - display_ram_t::init(const ::video::config_t &config, const std::string &display_name) { - if (display_base_t::init(config, display_name)) { + display_ddup_ram_t::init(const ::video::config_t &config, const std::string &display_name) { + if (display_base_t::init(config, display_name) || dup.init(this, config)) { return -1; } diff --git a/src/platform/windows/display_vram.cpp b/src/platform/windows/display_vram.cpp index 4aa1800b..ae0e6407 100644 --- a/src/platform/windows/display_vram.cpp +++ b/src/platform/windows/display_vram.cpp @@ -945,9 +945,8 @@ namespace platf::dxgi { } capture_e - display_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) { + display_ddup_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) { HRESULT status; - DXGI_OUTDUPL_FRAME_INFO frame_info; resource_t::pointer res_p {}; @@ -1329,9 +1328,14 @@ namespace platf::dxgi { return capture_e::ok; } + capture_e + display_ddup_vram_t::release_snapshot() { + return dup.release_frame(); + } + int - display_vram_t::init(const ::video::config_t &config, const std::string &display_name) { - if (display_base_t::init(config, display_name)) { + display_ddup_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)) { return -1; } @@ -1410,6 +1414,80 @@ namespace platf::dxgi { return 0; } + /** + * 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 + */ + capture_e + display_wgc_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) { + texture2d_t src; + uint64_t frame_qpc; + dup.set_cursor_visible(cursor_visible); + auto capture_status = dup.next_frame(timeout, &src, frame_qpc); + if (capture_status != capture_e::ok) + return capture_status; + + auto frame_timestamp = std::chrono::steady_clock::now() - qpc_time_difference(qpc_counter(), frame_qpc); + D3D11_TEXTURE2D_DESC desc; + 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; + } + + // 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) << "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 { + BOOST_LOG(error) << "Failed to lock capture texture"; + return capture_e::error; + } + } + else { + return capture_e::error; + } + img_out = img; + if (img_out) { + img_out->frame_timestamp = frame_timestamp; + } + + return capture_e::ok; + } + + capture_e + display_wgc_vram_t::release_snapshot() { + return dup.release_frame(); + } + + int + display_wgc_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)) + return -1; + + return 0; + } + std::shared_ptr display_vram_t::alloc_img() { auto img = std::make_shared(); diff --git a/src/platform/windows/display_wgc.cpp b/src/platform/windows/display_wgc.cpp new file mode 100644 index 00000000..b77c600b --- /dev/null +++ b/src/platform/windows/display_wgc.cpp @@ -0,0 +1,325 @@ +/** + * @file src/platform/windows/display_wgc.cpp + * @brief WinRT Windows.Graphics.Capture API + */ +#include + +#include "display.h" + +#include "misc.h" +#include "src/logging.h" + +#include +#include +#include + +namespace platf { + using namespace std::literals; +} + +namespace winrt { + using namespace Windows::Foundation; + using namespace Windows::Graphics::Capture; + using namespace Windows::Graphics::DirectX::Direct3D11; + + extern "C" { + HRESULT __stdcall CreateDirect3D11DeviceFromDXGIDevice(::IDXGIDevice *dxgiDevice, ::IInspectable **graphicsDevice); + } + + /* Windows structures sometimes have compile-time GUIDs. GCC supports this, but in a roundabout way. + * If WINRT_IMPL_HAS_DECLSPEC_UUID is true, then the compiler supports adding this attribute to a struct. For example, Visual Studio. + * If not, then MinGW GCC has a workaround to assign a GUID to a structure. + */ + struct +#if WINRT_IMPL_HAS_DECLSPEC_UUID + __declspec(uuid("A9B3D012-3DF2-4EE3-B8D1-8695F457D3C1")) +#endif + IDirect3DDxgiInterfaceAccess: ::IUnknown { + virtual HRESULT __stdcall GetInterface(REFIID id, void **object) = 0; + }; +} // namespace winrt +#if !WINRT_IMPL_HAS_DECLSPEC_UUID +static constexpr GUID GUID__IDirect3DDxgiInterfaceAccess = { + 0xA9B3D012, 0x3DF2, 0x4EE3, { 0xB8, 0xD1, 0x86, 0x95, 0xF4, 0x57, 0xD3, 0xC1 } + // compare with __declspec(uuid(...)) for the struct above. +}; +template <> +constexpr auto +__mingw_uuidof() -> GUID const & { + return GUID__IDirect3DDxgiInterfaceAccess; +} +#endif + +namespace platf::dxgi { + wgc_capture_t::wgc_capture_t() { + InitializeConditionVariable(&frame_present_cv); + } + + wgc_capture_t::~wgc_capture_t() { + if (capture_session) + capture_session.Close(); + if (frame_pool) + frame_pool.Close(); + item = nullptr; + capture_session = nullptr; + frame_pool = nullptr; + } + + /** + * Initialize the Windows.Graphics.Capture backend. + * @return 0 on success + */ + int + wgc_capture_t::init(display_base_t *display, const ::video::config_t &config) { + HRESULT status; + dxgi::dxgi_t dxgi; + winrt::com_ptr<::IInspectable> d3d_comhandle; + try { + if (!winrt::GraphicsCaptureSession::IsSupported()) { + BOOST_LOG(error) << "Screen capture is not supported on this device for this release of Windows!"sv; + return -1; + } + if (FAILED(status = display->device->QueryInterface(IID_IDXGIDevice, (void **) &dxgi))) { + BOOST_LOG(error) << "Failed to query DXGI interface from device [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + if (FAILED(status = winrt::CreateDirect3D11DeviceFromDXGIDevice(*&dxgi, d3d_comhandle.put()))) { + BOOST_LOG(error) << "Failed to query WinRT DirectX interface from device [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + } + catch (winrt::hresult_error &e) { + BOOST_LOG(error) << "Screen capture is not supported on this device for this release of Windows: failed to acquire device: [0x"sv << util::hex(e.code()).to_string_view() << ']'; + return -1; + } + + DXGI_OUTPUT_DESC output_desc; + uwp_device = d3d_comhandle.as(); + display->output->GetDesc(&output_desc); + + auto monitor_factory = winrt::get_activation_factory(); + if (monitor_factory == nullptr || + FAILED(status = monitor_factory->CreateForMonitor(output_desc.Monitor, winrt::guid_of(), winrt::put_abi(item)))) { + BOOST_LOG(error) << "Screen capture is not supported on this device for this release of Windows: failed to acquire display: [0x"sv << util::hex(status).to_string_view() << ']'; + return -1; + } + + if (config.dynamicRange) + display->capture_format = DXGI_FORMAT_R16G16B16A16_FLOAT; + else + display->capture_format = DXGI_FORMAT_B8G8R8A8_UNORM; + + try { + frame_pool = winrt::Direct3D11CaptureFramePool::CreateFreeThreaded(uwp_device, static_cast(display->capture_format), 2, item.Size()); + capture_session = frame_pool.CreateCaptureSession(item); + frame_pool.FrameArrived({ this, &wgc_capture_t::on_frame_arrived }); + } + catch (winrt::hresult_error &e) { + BOOST_LOG(error) << "Screen capture is not supported on this device for this release of Windows: failed to create capture session: [0x"sv << util::hex(e.code()).to_string_view() << ']'; + return -1; + } + try { + capture_session.IsBorderRequired(false); + } + catch (winrt::hresult_error &e) { + BOOST_LOG(warning) << "Screen capture may not be fully supported on this device for this release of Windows: failed to disable border around capture area: [0x"sv << util::hex(e.code()).to_string_view() << ']'; + } + try { + capture_session.StartCapture(); + } + catch (winrt::hresult_error &e) { + BOOST_LOG(error) << "Screen capture is not supported on this device for this release of Windows: failed to start capture: [0x"sv << util::hex(e.code()).to_string_view() << ']'; + return -1; + } + return 0; + } + + /** + * This function runs in a separate thread spawned by the frame pool and is a producer of frames. + * To maintain parity with the original display interface, this frame will be consumed by the capture thread. + * Acquire a read-write lock, make the produced frame available to the capture thread, then wake the capture thread. + */ + void + wgc_capture_t::on_frame_arrived(winrt::Direct3D11CaptureFramePool const &sender, winrt::IInspectable const &) { + winrt::Windows::Graphics::Capture::Direct3D11CaptureFrame frame { nullptr }; + try { + frame = sender.TryGetNextFrame(); + } + catch (winrt::hresult_error &e) { + BOOST_LOG(warning) << "Failed to capture frame: "sv << e.code(); + return; + } + if (frame != nullptr) { + AcquireSRWLockExclusive(&frame_lock); + if (produced_frame) + produced_frame.Close(); + + produced_frame = frame; + ReleaseSRWLockExclusive(&frame_lock); + WakeConditionVariable(&frame_present_cv); + } + } + + /** + * 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 a texture containing the frame just captured + * @param out_time the timestamp of the frame just captured + */ + capture_e + wgc_capture_t::next_frame(std::chrono::milliseconds timeout, ID3D11Texture2D **out, uint64_t &out_time) { + // this CONSUMER runs in the capture thread + release_frame(); + + AcquireSRWLockExclusive(&frame_lock); + if (produced_frame == nullptr && SleepConditionVariableSRW(&frame_present_cv, &frame_lock, timeout.count(), 0) == 0) { + ReleaseSRWLockExclusive(&frame_lock); + if (GetLastError() == ERROR_TIMEOUT) + return capture_e::timeout; + else + return capture_e::error; + } + if (produced_frame) { + consumed_frame = produced_frame; + produced_frame = nullptr; + } + ReleaseSRWLockExclusive(&frame_lock); + if (consumed_frame == nullptr) // spurious wakeup + return capture_e::timeout; + + auto capture_access = consumed_frame.Surface().as(); + if (capture_access == nullptr) + return capture_e::error; + capture_access->GetInterface(IID_ID3D11Texture2D, (void **) out); + out_time = consumed_frame.SystemRelativeTime().count(); // raw ticks from query performance counter + return capture_e::ok; + } + + capture_e + wgc_capture_t::release_frame() { + if (consumed_frame != nullptr) { + consumed_frame.Close(); + consumed_frame = nullptr; + } + return capture_e::ok; + } + + int + wgc_capture_t::set_cursor_visible(bool x) { + try { + if (capture_session.IsCursorCaptureEnabled() != x) + capture_session.IsCursorCaptureEnabled(x); + return 0; + } + catch (winrt::hresult_error &) { + return -1; + } + } + + int + display_wgc_ram_t::init(const ::video::config_t &config, const std::string &display_name) { + if (display_base_t::init(config, display_name) || dup.init(this, config)) + return -1; + + texture.reset(); + return 0; + } + + /** + * 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 + */ + capture_e + display_wgc_ram_t::snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr &img_out, std::chrono::milliseconds timeout, bool cursor_visible) { + HRESULT status; + texture2d_t src; + uint64_t frame_qpc; + dup.set_cursor_visible(cursor_visible); + auto capture_status = dup.next_frame(timeout, &src, frame_qpc); + if (capture_status != capture_e::ok) + return capture_status; + + auto frame_timestamp = std::chrono::steady_clock::now() - qpc_time_difference(qpc_counter(), frame_qpc); + D3D11_TEXTURE2D_DESC desc; + src->GetDesc(&desc); + + // Create the staging texture if it doesn't exist. It should match the source in size and format. + if (texture == nullptr) { + capture_format = desc.Format; + BOOST_LOG(info) << "Capture format ["sv << dxgi_format_to_string(capture_format) << ']'; + + D3D11_TEXTURE2D_DESC t {}; + t.Width = width; + t.Height = height; + t.MipLevels = 1; + t.ArraySize = 1; + t.SampleDesc.Count = 1; + t.Usage = D3D11_USAGE_STAGING; + t.Format = capture_format; + t.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + + auto status = device->CreateTexture2D(&t, nullptr, &texture); + + if (FAILED(status)) { + BOOST_LOG(error) << "Failed to create staging texture [0x"sv << util::hex(status).to_string_view() << ']'; + return capture_e::error; + } + } + + // 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 || desc.Height != height) { + BOOST_LOG(info) << "Capture size changed ["sv << width << 'x' << height << " -> "sv << desc.Width << 'x' << desc.Height << ']'; + return capture_e::reinit; + } + // 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) << "Capture format changed ["sv << dxgi_format_to_string(capture_format) << " -> "sv << dxgi_format_to_string(desc.Format) << ']'; + return capture_e::reinit; + } + + // Copy from GPU to CPU + device_ctx->CopyResource(texture.get(), src.get()); + + if (!pull_free_image_cb(img_out)) { + return capture_e::interrupted; + } + auto img = (img_t *) img_out.get(); + + // Map the staging texture for CPU access (making it inaccessible for the GPU) + if (FAILED(status = device_ctx->Map(texture.get(), 0, D3D11_MAP_READ, 0, &img_info))) { + BOOST_LOG(error) << "Failed to map texture [0x"sv << util::hex(status).to_string_view() << ']'; + + return capture_e::error; + } + + // Now that we know the capture format, we can finish creating the image + if (complete_img(img, false)) { + device_ctx->Unmap(texture.get(), 0); + img_info.pData = nullptr; + return capture_e::error; + } + + std::copy_n((std::uint8_t *) img_info.pData, height * img_info.RowPitch, (std::uint8_t *) img->data); + + // Unmap the staging texture to allow GPU access again + device_ctx->Unmap(texture.get(), 0); + img_info.pData = nullptr; + + if (img) { + img->frame_timestamp = frame_timestamp; + } + + return capture_e::ok; + } + + capture_e + display_wgc_ram_t::release_snapshot() { + return dup.release_frame(); + } +} // namespace platf::dxgi diff --git a/src_assets/common/assets/web/configs/tabs/Advanced.vue b/src_assets/common/assets/web/configs/tabs/Advanced.vue index 9704c91b..bd11adf2 100644 --- a/src_assets/common/assets/web/configs/tabs/Advanced.vue +++ b/src_assets/common/assets/web/configs/tabs/Advanced.vue @@ -59,14 +59,22 @@ const config = ref(props.config) -
+
{{ $t('config.capture_desc') }}
diff --git a/src_assets/common/assets/web/public/assets/locale/en.json b/src_assets/common/assets/web/public/assets/locale/en.json index ac69860c..0735985b 100644 --- a/src_assets/common/assets/web/public/assets/locale/en.json +++ b/src_assets/common/assets/web/public/assets/locale/en.json @@ -3,6 +3,7 @@ "apply": "Apply", "auto": "Automatic", "autodetect": "Autodetect (recommended)", + "beta": "(beta)", "cancel": "Cancel", "disabled": "Disabled", "disabled_def": "Disabled (default)",