feat(win): new capture method, Windows.Graphics.Capture (#2580)

This commit is contained in:
Tejas Rao 2024-05-27 11:16:14 -07:00 committed by GitHub
parent 2d706d3104
commit 287ac4c0fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 701 additions and 154 deletions

View File

@ -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 \

View File

@ -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

View File

@ -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"

View File

@ -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)

View File

@ -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 <https://localhost:47990/config/#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 <https://github.com/keylase/nvidia-patch/>`__
or `nvlax <https://github.com/illnyang/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 <about/setup:install>`.
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 <https://github.com/keylase/nvidia-patch/>`__
or `nvlax <https://github.com/illnyang/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 <about/setup:install>`.
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.

View File

@ -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 \

View File

@ -11,6 +11,9 @@
#include <dxgi.h>
#include <dxgi1_6.h>
#include <Unknwn.h>
#include <winrt/Windows.Graphics.Capture.h>
#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<DXGI_FORMAT>
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<platf::img_t> &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<DXGI_FORMAT>
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<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override;
std::shared_ptr<img_t>
alloc_img() override;
int
@ -286,22 +274,18 @@ namespace platf::dxgi {
std::vector<DXGI_FORMAT>
get_supported_capture_formats() override;
int
init(const ::video::config_t &config, const std::string &display_name);
std::unique_ptr<avcodec_encode_device_t>
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<display_vram_t> {
public:
virtual capture_e
snapshot(const pull_free_image_cb_t &pull_free_image_cb, std::shared_ptr<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override;
std::shared_ptr<img_t>
alloc_img() override;
int
@ -311,9 +295,6 @@ namespace platf::dxgi {
std::vector<DXGI_FORMAT>
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<nvenc_encode_device_t>
make_nvenc_encode_device(pix_fmt_e pix_fmt) override;
std::atomic<uint32_t> 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<platf::img_t> &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<platf::img_t> &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<std::monostate, texture2d_t, std::shared_ptr<platf::img_t>> last_frame_variant;
};
std::atomic<uint32_t> 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<platf::img_t> &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<platf::img_t> &img_out, std::chrono::milliseconds timeout, bool cursor_visible) override;
capture_e
release_snapshot() override;
};
} // namespace platf::dxgi

View File

@ -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_t>
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<dxgi::display_vram_t>();
if (config::video.capture == "ddx" || config::video.capture.empty()) {
if (hwdevice_type == mem_type_e::dxgi) {
auto disp = std::make_shared<dxgi::display_ddup_vram_t>();
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<dxgi::display_ram_t>();
else if (hwdevice_type == mem_type_e::system) {
auto disp = std::make_shared<dxgi::display_ddup_ram_t>();
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<dxgi::display_wgc_vram_t>();
if (!disp->init(config, display_name)) {
return disp;
}
}
else if (hwdevice_type == mem_type_e::system) {
auto disp = std::make_shared<dxgi::display_wgc_ram_t>();
if (!disp->init(config, display_name)) {
return disp;
}
}
}
// ddx and wgc failed
return nullptr;
}

View File

@ -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<platf::img_t> &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<platf::img_t> &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<platf::img_t>
display_ram_t::alloc_img() {
auto img = std::make_shared<img_t>();
@ -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;
}

View File

@ -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<platf::img_t> &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<platf::img_t> &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<platf::img_t> &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<platf::img_t> img;
if (!pull_free_image_cb(img))
return capture_e::interrupted;
auto d3d_img = std::static_pointer_cast<img_d3d_t>(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<platf::img_t>
display_vram_t::alloc_img() {
auto img = std::make_shared<img_d3d_t>();

View File

@ -0,0 +1,325 @@
/**
* @file src/platform/windows/display_wgc.cpp
* @brief WinRT Windows.Graphics.Capture API
*/
#include <dxgi1_2.h>
#include "display.h"
#include "misc.h"
#include "src/logging.h"
#include <windows.graphics.capture.interop.h>
#include <winrt/windows.foundation.h>
#include <winrt/windows.graphics.directx.direct3d11.h>
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<winrt::IDirect3DDxgiInterfaceAccess>() -> 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<winrt::IDirect3DDevice>();
display->output->GetDesc(&output_desc);
auto monitor_factory = winrt::get_activation_factory<winrt::GraphicsCaptureItem, IGraphicsCaptureItemInterop>();
if (monitor_factory == nullptr ||
FAILED(status = monitor_factory->CreateForMonitor(output_desc.Monitor, winrt::guid_of<winrt::IGraphicsCaptureItem>(), 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<winrt::Windows::Graphics::DirectX::DirectXPixelFormat>(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<winrt::IDirect3DDxgiInterfaceAccess>();
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<platf::img_t> &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

View File

@ -59,14 +59,22 @@ const config = ref(props.config)
</div>
<!-- Capture -->
<div class="mb-3" v-if="platform === 'linux'">
<div class="mb-3" v-if="platform !== 'macos'">
<label for="capture" class="form-label">{{ $t('config.capture') }}</label>
<select id="capture" class="form-select" v-model="config.capture">
<option value="">{{ $t('_common.autodetect') }}</option>
<option value="nvfbc">NvFBC</option>
<option value="wlr">wlroots</option>
<option value="kms">KMS</option>
<option value="x11">X11</option>
<PlatformLayout :platform="platform">
<template #linux>
<option value="nvfbc">NvFBC</option>
<option value="wlr">wlroots</option>
<option value="kms">KMS</option>
<option value="x11">X11</option>
</template>
<template #windows>
<option value="ddx">Desktop Duplication API</option>
<option value="wgc">Windows.Graphics.Capture {{ $t('_common.beta') }}</option>
</template>
</PlatformLayout>
</select>
<div class="form-text">{{ $t('config.capture_desc') }}</div>
</div>

View File

@ -3,6 +3,7 @@
"apply": "Apply",
"auto": "Automatic",
"autodetect": "Autodetect (recommended)",
"beta": "(beta)",
"cancel": "Cancel",
"disabled": "Disabled",
"disabled_def": "Disabled (default)",