Files
Sunshine/src/platform/windows/display_wgc.cpp
2025-08-04 23:30:48 -04:00

336 lines
12 KiB
C++

/**
* @file src/platform/windows/display_wgc.cpp
* @brief Definitions for WinRT Windows.Graphics.Capture API
*/
// platform includes
#include <dxgi1_2.h>
// local includes
#include "display.h"
#include "misc.h"
#include "src/logging.h"
// Gross hack to work around MINGW-packages#22160
#define ____FIReference_1_boolean_INTERFACE_DEFINED__
#include <Windows.Graphics.Capture.Interop.h>
#include <winrt/windows.foundation.h>
#include <winrt/windows.foundation.metadata.h>
#include <winrt/windows.graphics.directx.direct3d11.h>
namespace platf {
using namespace std::literals;
}
namespace winrt {
using namespace Windows::Foundation;
using namespace Windows::Foundation::Metadata;
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;
}
/**
* @brief Initialize the Windows.Graphics.Capture backend.
* @return 0 on success, -1 on failure.
*/
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 {
if (winrt::ApiInformation::IsPropertyPresent(L"Windows.Graphics.Capture.GraphicsCaptureSession", L"IsBorderRequired")) {
capture_session.IsBorderRequired(false);
} else {
BOOST_LOG(warning) << "Can't disable colored border around capture area on this version of Windows";
}
} 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);
}
}
/**
* @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 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;
}
/**
* @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_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