mirror of
https://github.com/Blinue/Magpie.git
synced 2026-06-24 02:04:10 +00:00
feat: Graphics Capture
This commit is contained in:
parent
68b9656192
commit
3ed697cba0
11 changed files with 889 additions and 17 deletions
|
|
@ -16,8 +16,11 @@ static bool IsCandidateWindow(HWND hWnd) {
|
|||
return false;
|
||||
}
|
||||
|
||||
RECT frameRect;
|
||||
if (!Win32Utils::GetWindowFrameRect(hWnd, frameRect)) {
|
||||
RECT frameRect{};
|
||||
|
||||
HRESULT hr = DwmGetWindowAttribute(hWnd,
|
||||
DWMWA_EXTENDED_FRAME_BOUNDS, &frameRect, sizeof(frameRect));
|
||||
if (FAILED(hr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,4 +72,36 @@ bool DirectXHelper::IsDebugLayersAvailable() noexcept {
|
|||
#endif
|
||||
}
|
||||
|
||||
winrt::com_ptr<ID3D11Texture2D> DirectXHelper::CreateTexture2D(
|
||||
ID3D11Device* d3dDevice,
|
||||
DXGI_FORMAT format,
|
||||
UINT width,
|
||||
UINT height,
|
||||
UINT bindFlags,
|
||||
D3D11_USAGE usage,
|
||||
UINT miscFlags,
|
||||
const D3D11_SUBRESOURCE_DATA* pInitialData
|
||||
) noexcept {
|
||||
D3D11_TEXTURE2D_DESC desc{};
|
||||
desc.Format = format;
|
||||
desc.Width = width;
|
||||
desc.Height = height;
|
||||
desc.MipLevels = 1;
|
||||
desc.ArraySize = 1;
|
||||
desc.SampleDesc.Count = 1;
|
||||
desc.SampleDesc.Quality = 0;
|
||||
desc.BindFlags = bindFlags;
|
||||
desc.Usage = usage;
|
||||
desc.MiscFlags = miscFlags;
|
||||
|
||||
winrt::com_ptr<ID3D11Texture2D> result;
|
||||
HRESULT hr = d3dDevice->CreateTexture2D(&desc, pInitialData, result.put());
|
||||
if (FAILED(hr)) {
|
||||
Logger::Get().ComError("CreateTexture2D 失败", hr);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,17 @@ struct DirectXHelper {
|
|||
);
|
||||
|
||||
static bool IsDebugLayersAvailable() noexcept;
|
||||
|
||||
static winrt::com_ptr<ID3D11Texture2D> CreateTexture2D(
|
||||
ID3D11Device* d3dDevice,
|
||||
DXGI_FORMAT format,
|
||||
UINT width,
|
||||
UINT height,
|
||||
UINT bindFlags,
|
||||
D3D11_USAGE usage = D3D11_USAGE_DEFAULT,
|
||||
UINT miscFlags = 0,
|
||||
const D3D11_SUBRESOURCE_DATA* pInitialData = nullptr
|
||||
) noexcept;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
295
src/Magpie.Core/FrameSourceBase.cpp
Normal file
295
src/Magpie.Core/FrameSourceBase.cpp
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
#include "pch.h"
|
||||
#include "FrameSourceBase.h"
|
||||
#include "ScalingOptions.h"
|
||||
#include "Logger.h"
|
||||
#include "Win32Utils.h"
|
||||
#include "CommonSharedConstants.h"
|
||||
#include "Utils.h"
|
||||
#include "SmallVector.h"
|
||||
|
||||
namespace Magpie::Core {
|
||||
|
||||
FrameSourceBase::~FrameSourceBase() noexcept {
|
||||
// 还原窗口圆角
|
||||
if (_roundCornerDisabled) {
|
||||
_roundCornerDisabled = false;
|
||||
|
||||
INT attr = DWMWCP_DEFAULT;
|
||||
HRESULT hr = DwmSetWindowAttribute(_hwndSrc, DWMWA_WINDOW_CORNER_PREFERENCE, &attr, sizeof(attr));
|
||||
if (FAILED(hr)) {
|
||||
Logger::Get().ComError("取消禁用窗口圆角失败", hr);
|
||||
} else {
|
||||
Logger::Get().Info("已取消禁用窗口圆角");
|
||||
}
|
||||
}
|
||||
|
||||
// 还原窗口大小调整
|
||||
if (_windowResizingDisabled) {
|
||||
// 缩放 Magpie 主窗口时会在 SetWindowLongPtr 中卡住,似乎是 Win11 的 bug
|
||||
// 将在 MagService::_MagRuntime_IsRunningChanged 还原主窗口样式
|
||||
if (Win32Utils::GetWndClassName(_hwndSrc) != CommonSharedConstants::MAIN_WINDOW_CLASS_NAME) {
|
||||
LONG_PTR style = GetWindowLongPtr(_hwndSrc, GWL_STYLE);
|
||||
if (!(style & WS_THICKFRAME)) {
|
||||
if (SetWindowLongPtr(_hwndSrc, GWL_STYLE, style | WS_THICKFRAME)) {
|
||||
if (!SetWindowPos(_hwndSrc, 0, 0, 0, 0, 0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED)) {
|
||||
Logger::Get().Win32Error("SetWindowPos 失败");
|
||||
}
|
||||
|
||||
Logger::Get().Info("已取消禁用窗口大小调整");
|
||||
} else {
|
||||
Logger::Get().Win32Error("取消禁用窗口大小调整失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool FrameSourceBase::Initialize(HWND hwndSrc, HWND /*hwndScaling*/, const ScalingOptions& options, ID3D11Device5* d3dDevice) noexcept {
|
||||
_hwndSrc = hwndSrc;
|
||||
_d3dDevice = d3dDevice;
|
||||
|
||||
// 禁用窗口大小调整
|
||||
if (options.IsDisableWindowResizing()) {
|
||||
LONG_PTR style = GetWindowLongPtr(hwndSrc, GWL_STYLE);
|
||||
if (style & WS_THICKFRAME) {
|
||||
if (SetWindowLongPtr(hwndSrc, GWL_STYLE, style ^ WS_THICKFRAME)) {
|
||||
// 不重绘边框,以防某些窗口状态不正确
|
||||
// if (!SetWindowPos(hwndSrc, 0, 0, 0, 0, 0,
|
||||
// SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED)) {
|
||||
// SPDLOG_LOGGER_ERROR(logger, MakeWin32ErrorMsg("SetWindowPos 失败"));
|
||||
// }
|
||||
|
||||
Logger::Get().Info("已禁用窗口大小调整");
|
||||
_windowResizingDisabled = true;
|
||||
} else {
|
||||
Logger::Get().Win32Error("禁用窗口大小调整失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 禁用窗口圆角
|
||||
if (_HasRoundCornerInWin11()) {
|
||||
if (Win32Utils::GetOSVersion().IsWin11()) {
|
||||
INT attr = DWMWCP_DONOTROUND;
|
||||
HRESULT hr = DwmSetWindowAttribute(hwndSrc, DWMWA_WINDOW_CORNER_PREFERENCE, &attr, sizeof(attr));
|
||||
if (FAILED(hr)) {
|
||||
Logger::Get().ComError("禁用窗口圆角失败", hr);
|
||||
} else {
|
||||
Logger::Get().Info("已禁用窗口圆角");
|
||||
_roundCornerDisabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
struct EnumChildWndParam {
|
||||
const wchar_t* clientWndClassName = nullptr;
|
||||
SmallVector<HWND, 1> childWindows;
|
||||
};
|
||||
|
||||
static BOOL CALLBACK EnumChildProc(
|
||||
_In_ HWND hwnd,
|
||||
_In_ LPARAM lParam
|
||||
) {
|
||||
std::wstring className = Win32Utils::GetWndClassName(hwnd);
|
||||
|
||||
EnumChildWndParam* param = (EnumChildWndParam*)lParam;
|
||||
if (className == param->clientWndClassName) {
|
||||
param->childWindows.push_back(hwnd);
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static HWND FindClientWindowOfUWP(HWND hwndSrc, const wchar_t* clientWndClassName) {
|
||||
// 查找所有窗口类名为 ApplicationFrameInputSinkWindow 的子窗口
|
||||
// 该子窗口一般为客户区
|
||||
EnumChildWndParam param{};
|
||||
param.clientWndClassName = clientWndClassName;
|
||||
EnumChildWindows(hwndSrc, EnumChildProc, (LPARAM)¶m);
|
||||
|
||||
if (param.childWindows.empty()) {
|
||||
// 未找到符合条件的子窗口
|
||||
return hwndSrc;
|
||||
}
|
||||
|
||||
if (param.childWindows.size() == 1) {
|
||||
return param.childWindows[0];
|
||||
}
|
||||
|
||||
// 如果有多个匹配的子窗口,取最大的(一般不会出现)
|
||||
int maxSize = 0, maxIdx = 0;
|
||||
for (int i = 0; i < param.childWindows.size(); ++i) {
|
||||
RECT rect;
|
||||
if (!GetClientRect(param.childWindows[i], &rect)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int size = rect.right - rect.left + rect.bottom - rect.top;
|
||||
if (size > maxSize) {
|
||||
maxSize = size;
|
||||
maxIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
return param.childWindows[maxIdx];
|
||||
}
|
||||
|
||||
RECT FrameSourceBase::_GetSrcFrameRect(const Cropping& cropping, bool isCaptureTitleBar) noexcept {
|
||||
RECT result{};
|
||||
|
||||
if (isCaptureTitleBar && _CanCaptureTitleBar()) {
|
||||
HRESULT hr = DwmGetWindowAttribute(_hwndSrc,
|
||||
DWMWA_EXTENDED_FRAME_BOUNDS, &result, sizeof(result));
|
||||
if (FAILED(hr)) {
|
||||
Logger::Get().ComError("DwmGetWindowAttribute 失败", hr);
|
||||
}
|
||||
} else {
|
||||
std::wstring className = Win32Utils::GetWndClassName(_hwndSrc);
|
||||
if (className == L"ApplicationFrameWindow" || className == L"Windows.UI.Core.CoreWindow") {
|
||||
// "Modern App"
|
||||
// 客户区窗口类名为 ApplicationFrameInputSinkWindow
|
||||
HWND hwndClient = FindClientWindowOfUWP(_hwndSrc, L"ApplicationFrameInputSinkWindow");
|
||||
if (hwndClient) {
|
||||
if (!Win32Utils::GetClientScreenRect(hwndClient, result)) {
|
||||
Logger::Get().Win32Error("GetClientScreenRect 失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result == RECT{}) {
|
||||
if (!Win32Utils::GetClientScreenRect(_hwndSrc, result)) {
|
||||
Logger::Get().Win32Error("GetClientScreenRect 失败");
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
result = {
|
||||
std::lround(result.left + cropping.Left),
|
||||
std::lround(result.top + cropping.Top),
|
||||
std::lround(result.right - cropping.Right),
|
||||
std::lround(result.bottom - cropping.Bottom)
|
||||
};
|
||||
|
||||
if (result.right - result.left <= 0 || result.bottom - result.top <= 0) {
|
||||
Logger::Get().Error("裁剪窗口失败");
|
||||
return {};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool FrameSourceBase::_GetMapToOriginDPI(HWND hWnd, double& a, double& bx, double& by) noexcept {
|
||||
// HDC 中的 HBITMAP 尺寸为窗口的原始尺寸
|
||||
// 通过 GetWindowRect 获得的尺寸为窗口的 DPI 缩放后尺寸
|
||||
// 它们的商即为窗口的 DPI 缩放
|
||||
HDC hdcWindow = GetDCEx(hWnd, NULL, DCX_LOCKWINDOWUPDATE | DCX_WINDOW);
|
||||
if (!hdcWindow) {
|
||||
Logger::Get().Win32Error("GetDCEx 失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
HDC hdcClient = GetDCEx(hWnd, NULL, DCX_LOCKWINDOWUPDATE);
|
||||
if (!hdcClient) {
|
||||
Logger::Get().Win32Error("GetDCEx 失败");
|
||||
ReleaseDC(hWnd, hdcWindow);
|
||||
return false;
|
||||
}
|
||||
|
||||
Utils::ScopeExit se([hWnd, hdcWindow, hdcClient]() {
|
||||
ReleaseDC(hWnd, hdcWindow);
|
||||
ReleaseDC(hWnd, hdcClient);
|
||||
});
|
||||
|
||||
HGDIOBJ hBmpWindow = GetCurrentObject(hdcWindow, OBJ_BITMAP);
|
||||
if (!hBmpWindow) {
|
||||
Logger::Get().Win32Error("GetCurrentObject 失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GetObjectType(hBmpWindow) != OBJ_BITMAP) {
|
||||
Logger::Get().Error("无法获取窗口的重定向表面");
|
||||
return false;
|
||||
}
|
||||
|
||||
BITMAP bmp{};
|
||||
if (!GetObject(hBmpWindow, sizeof(bmp), &bmp)) {
|
||||
Logger::Get().Win32Error("GetObject 失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
RECT rect;
|
||||
if (!GetWindowRect(hWnd, &rect)) {
|
||||
Logger::Get().Win32Error("GetWindowRect 失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
a = bmp.bmWidth / double(rect.right - rect.left);
|
||||
|
||||
// 使用 DPI 缩放无法可靠计算出窗口客户区的位置
|
||||
// 这里使用窗口 HDC 和客户区 HDC 的原点坐标差值
|
||||
// GetDCOrgEx 获得的是 DC 原点的屏幕坐标
|
||||
|
||||
POINT ptClient{}, ptWindow{};
|
||||
if (!GetDCOrgEx(hdcClient, &ptClient)) {
|
||||
Logger::Get().Win32Error("GetDCOrgEx 失败");
|
||||
return false;
|
||||
}
|
||||
if (!GetDCOrgEx(hdcWindow, &ptWindow)) {
|
||||
Logger::Get().Win32Error("GetDCOrgEx 失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Win32Utils::GetClientScreenRect(hWnd, rect)) {
|
||||
Logger::Get().Error("GetClientScreenRect 失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 以窗口的客户区左上角为基准
|
||||
// 该点在坐标系 1 中坐标为 (rect.left, rect.top)
|
||||
// 在坐标系 2 中坐标为 (ptClient.x - ptWindow.x, ptClient.y - ptWindow.y)
|
||||
// 由此计算出 b
|
||||
bx = ptClient.x - ptWindow.x - rect.left * a;
|
||||
by = ptClient.y - ptWindow.y - rect.top * a;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FrameSourceBase::_CenterWindowIfNecessary(HWND hWnd, const RECT& rcWork) noexcept {
|
||||
RECT srcRect;
|
||||
if (!GetWindowRect(hWnd, &srcRect)) {
|
||||
Logger::Get().Win32Error("GetWindowRect 失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (srcRect.left < rcWork.left || srcRect.top < rcWork.top
|
||||
|| srcRect.right > rcWork.right || srcRect.bottom > rcWork.bottom) {
|
||||
// 源窗口超越边界,将源窗口移到屏幕中央
|
||||
SIZE srcSize = { srcRect.right - srcRect.left, srcRect.bottom - srcRect.top };
|
||||
SIZE rcWorkSize = { rcWork.right - rcWork.left, rcWork.bottom - rcWork.top };
|
||||
if (srcSize.cx > rcWorkSize.cx || srcSize.cy > rcWorkSize.cy) {
|
||||
// 源窗口无法被当前屏幕容纳,因此无法捕获
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SetWindowPos(
|
||||
hWnd,
|
||||
0,
|
||||
rcWork.left + (rcWorkSize.cx - srcSize.cx) / 2,
|
||||
rcWork.top + (rcWorkSize.cy - srcSize.cy) / 2,
|
||||
0,
|
||||
0,
|
||||
SWP_NOSIZE | SWP_NOZORDER
|
||||
)) {
|
||||
Logger::Get().Win32Error("SetWindowPos 失败");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
59
src/Magpie.Core/FrameSourceBase.h
Normal file
59
src/Magpie.Core/FrameSourceBase.h
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
#pragma once
|
||||
|
||||
namespace Magpie::Core {
|
||||
|
||||
struct ScalingOptions;
|
||||
struct Cropping;
|
||||
|
||||
class FrameSourceBase {
|
||||
public:
|
||||
FrameSourceBase() noexcept {}
|
||||
|
||||
virtual ~FrameSourceBase() noexcept;
|
||||
|
||||
// 不可复制,不可移动
|
||||
FrameSourceBase(const FrameSourceBase&) = delete;
|
||||
FrameSourceBase(FrameSourceBase&&) = delete;
|
||||
|
||||
virtual bool Initialize(HWND hwndSrc, HWND hwndScaling, const ScalingOptions& options, ID3D11Device5* d3dDevice) noexcept;
|
||||
|
||||
enum class UpdateState {
|
||||
NewFrame,
|
||||
Waiting,
|
||||
Error
|
||||
};
|
||||
|
||||
virtual UpdateState Update() noexcept = 0;
|
||||
|
||||
virtual const char* GetName() const noexcept = 0;
|
||||
|
||||
virtual bool IsScreenCapture() = 0;
|
||||
|
||||
protected:
|
||||
virtual bool _HasRoundCornerInWin11() noexcept = 0;
|
||||
|
||||
virtual bool _CanCaptureTitleBar() noexcept = 0;
|
||||
|
||||
RECT _GetSrcFrameRect(const Cropping& cropping, bool isCaptureTitleBar) noexcept;
|
||||
|
||||
// 获取坐标系 1 到坐标系 2 的映射关系
|
||||
// 坐标系 1:屏幕坐标系,即虚拟化后的坐标系。原点为屏幕左上角
|
||||
// 坐标系 2:虚拟化前的坐标系,即源窗口所见的坐标系,原点为窗口左上角
|
||||
// 两坐标系为线性映射,a 和 b 返回该映射的参数
|
||||
// 如果窗口本身支持高 DPI,则 a 为 1,否则 a 为 DPI 缩放的倒数
|
||||
// 此函数是为了将屏幕上的点映射到窗口坐标系中,并且无视 DPI 虚拟化
|
||||
// 坐标系 1 中的 (x1, y1) 映射到 (x1 * a + bx, x2 * a + by)
|
||||
static bool _GetMapToOriginDPI(HWND hWnd, double& a, double& bx, double& by) noexcept;
|
||||
|
||||
static bool _CenterWindowIfNecessary(HWND hWnd, const RECT& rcWork) noexcept;
|
||||
|
||||
HWND _hwndSrc = NULL;
|
||||
|
||||
ID3D11Device5* _d3dDevice = nullptr;
|
||||
winrt::com_ptr<ID3D11Texture2D> _output;
|
||||
|
||||
bool _roundCornerDisabled = false;
|
||||
bool _windowResizingDisabled = false;
|
||||
};
|
||||
|
||||
}
|
||||
414
src/Magpie.Core/GraphicsCaptureFrameSource.cpp
Normal file
414
src/Magpie.Core/GraphicsCaptureFrameSource.cpp
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
#include "pch.h"
|
||||
#include "GraphicsCaptureFrameSource.h"
|
||||
#include "StrUtils.h"
|
||||
#include "Utils.h"
|
||||
#include "DeviceResources.h"
|
||||
#include "Logger.h"
|
||||
#include <Windows.Graphics.DirectX.Direct3D11.interop.h>
|
||||
#include "Win32Utils.h"
|
||||
#include "DirectXHelper.h"
|
||||
#include "ScalingOptions.h"
|
||||
|
||||
namespace winrt {
|
||||
using namespace Windows::Graphics;
|
||||
using namespace Windows::Graphics::Capture;
|
||||
using namespace Windows::Graphics::DirectX;
|
||||
using namespace Windows::Graphics::DirectX::Direct3D11;
|
||||
}
|
||||
|
||||
namespace Magpie::Core {
|
||||
|
||||
bool GraphicsCaptureFrameSource::Initialize(HWND hwndSrc, HWND hwndScaling, const ScalingOptions& options, ID3D11Device5* d3dDevice) noexcept {
|
||||
if (!FrameSourceBase::Initialize(hwndSrc, hwndScaling, options, d3dDevice)) {
|
||||
Logger::Get().Error("初始化 FrameSourceBase 失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
HRESULT hr;
|
||||
|
||||
winrt::com_ptr<IGraphicsCaptureItemInterop> interop;
|
||||
try {
|
||||
if (!winrt::GraphicsCaptureSession::IsSupported()) {
|
||||
Logger::Get().Error("当前不支持 WinRT 捕获");
|
||||
return false;
|
||||
}
|
||||
|
||||
winrt::com_ptr<IDXGIDevice> dxgiDevice;
|
||||
_d3dDevice->QueryInterface<IDXGIDevice>(dxgiDevice.put());
|
||||
|
||||
hr = CreateDirect3D11DeviceFromDXGIDevice(
|
||||
dxgiDevice.get(),
|
||||
reinterpret_cast<::IInspectable**>(winrt::put_abi(_wrappedD3DDevice))
|
||||
);
|
||||
if (FAILED(hr)) {
|
||||
Logger::Get().ComError("创建 IDirect3DDevice 失败", hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 从窗口句柄获取 GraphicsCaptureItem
|
||||
interop = winrt::get_activation_factory<winrt::GraphicsCaptureItem, IGraphicsCaptureItemInterop>();
|
||||
if (!interop) {
|
||||
Logger::Get().Error("获取 IGraphicsCaptureItemInterop 失败");
|
||||
return false;
|
||||
}
|
||||
} catch (const winrt::hresult_error& e) {
|
||||
Logger::Get().Error(StrUtils::Concat("初始化 WinRT 失败:", StrUtils::UTF16ToUTF8(e.message())));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_CaptureWindow(interop.get(), options)) {
|
||||
Logger::Get().Info("窗口捕获失败,回落到屏幕捕获");
|
||||
|
||||
if (_CaptureMonitor(interop.get(), options, hwndScaling)) {
|
||||
_isScreenCapture = true;
|
||||
} else {
|
||||
Logger::Get().Error("屏幕捕获失败");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_output = DirectXHelper::CreateTexture2D(
|
||||
d3dDevice,
|
||||
DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||
_frameBox.right - _frameBox.left,
|
||||
_frameBox.bottom - _frameBox.top,
|
||||
D3D11_BIND_SHADER_RESOURCE
|
||||
);
|
||||
if (!_output) {
|
||||
Logger::Get().Error("创建纹理失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!StartCapture()) {
|
||||
Logger::Get().Error("_StartCapture 失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger::Get().Info("GraphicsCaptureFrameSource 初始化完成");
|
||||
return true;
|
||||
}
|
||||
|
||||
FrameSourceBase::UpdateState GraphicsCaptureFrameSource::Update() noexcept {
|
||||
if (!_captureSession) {
|
||||
return UpdateState::Waiting;
|
||||
}
|
||||
|
||||
winrt::Direct3D11CaptureFrame frame = _captureFramePool.TryGetNextFrame();
|
||||
if (!frame) {
|
||||
// 因为已通过 FrameArrived 注册回调,所以每当有新帧时会有新消息到达
|
||||
return UpdateState::Waiting;
|
||||
}
|
||||
|
||||
// 从帧获取 IDXGISurface
|
||||
winrt::IDirect3DSurface d3dSurface = frame.Surface();
|
||||
|
||||
winrt::com_ptr<::Windows::Graphics::DirectX::Direct3D11::IDirect3DDxgiInterfaceAccess> dxgiInterfaceAccess(
|
||||
d3dSurface.as<::Windows::Graphics::DirectX::Direct3D11::IDirect3DDxgiInterfaceAccess>()
|
||||
);
|
||||
|
||||
winrt::com_ptr<ID3D11Texture2D> withFrame;
|
||||
HRESULT hr = dxgiInterfaceAccess->GetInterface(IID_PPV_ARGS(&withFrame));
|
||||
if (FAILED(hr)) {
|
||||
Logger::Get().ComError("从获取 IDirect3DSurface 获取 ID3D11Texture2D 失败", hr);
|
||||
return UpdateState::Error;
|
||||
}
|
||||
|
||||
winrt::com_ptr<ID3D11DeviceContext> d3dDC;
|
||||
_d3dDevice->GetImmediateContext(d3dDC.put());
|
||||
|
||||
d3dDC->CopySubresourceRegion(_output.get(), 0, 0, 0, 0, withFrame.get(), 0, &_frameBox);
|
||||
|
||||
frame.Close();
|
||||
return UpdateState::NewFrame;
|
||||
}
|
||||
|
||||
bool GraphicsCaptureFrameSource::_CaptureWindow(IGraphicsCaptureItemInterop* interop, const ScalingOptions& options) {
|
||||
// DwmGetWindowAttribute 和 Graphics.Capture 无法应用于子窗口
|
||||
|
||||
// 包含边框的窗口尺寸
|
||||
RECT srcFrameBounds{};
|
||||
HRESULT hr = DwmGetWindowAttribute(_hwndSrc,
|
||||
DWMWA_EXTENDED_FRAME_BOUNDS, &srcFrameBounds, sizeof(srcFrameBounds));
|
||||
if (FAILED(hr)) {
|
||||
Logger::Get().ComError("DwmGetWindowAttribute 失败", hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
RECT srcFrameRect = _GetSrcFrameRect(options.cropping, options.IsCaptureTitleBar());
|
||||
if (srcFrameRect == RECT{}) {
|
||||
Logger::Get().Error("_GetSrcFrameRect 失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 在源窗口存在 DPI 缩放时有时会有一像素的偏移(取决于窗口在屏幕上的位置)
|
||||
// 可能是 DwmGetWindowAttribute 的 bug
|
||||
_frameBox = {
|
||||
UINT(srcFrameRect.left - srcFrameBounds.left),
|
||||
UINT(srcFrameRect.top - srcFrameBounds.top),
|
||||
0,
|
||||
UINT(srcFrameRect.right - srcFrameBounds.left),
|
||||
UINT(srcFrameRect.bottom - srcFrameBounds.top),
|
||||
1
|
||||
};
|
||||
|
||||
if (_TryCreateGraphicsCaptureItem(interop, _hwndSrc)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 尝试设置源窗口样式,因为 WGC 只能捕获位于 Alt+Tab 列表中的窗口
|
||||
LONG_PTR srcExStyle = GetWindowLongPtr(_hwndSrc, GWL_EXSTYLE);
|
||||
if ((srcExStyle & WS_EX_APPWINDOW) == 0) {
|
||||
// 添加 WS_EX_APPWINDOW 样式,确保源窗口可被 Alt+Tab 选中
|
||||
if (SetWindowLongPtr(_hwndSrc, GWL_EXSTYLE, srcExStyle | WS_EX_APPWINDOW)) {
|
||||
Logger::Get().Info("已改变源窗口样式");
|
||||
_originalSrcExStyle = srcExStyle;
|
||||
|
||||
if (_TryCreateGraphicsCaptureItem(interop, _hwndSrc)) {
|
||||
_RemoveOwnerFromAltTabList(_hwndSrc);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
Logger::Get().Win32Error("SetWindowLongPtr 失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 如果窗口使用 ITaskbarList 隐藏了任务栏图标也不会出现在 Alt+Tab 列表。这种情况很罕见
|
||||
_taskbarList = winrt::try_create_instance<ITaskbarList>(CLSID_TaskbarList);
|
||||
if (_taskbarList && SUCCEEDED(_taskbarList->HrInit())) {
|
||||
HRESULT hr = _taskbarList->AddTab(_hwndSrc);
|
||||
if (SUCCEEDED(hr)) {
|
||||
Logger::Get().Info("已添加任务栏图标");
|
||||
|
||||
if (_TryCreateGraphicsCaptureItem(interop, _hwndSrc)) {
|
||||
_RemoveOwnerFromAltTabList(_hwndSrc);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
_taskbarList = nullptr;
|
||||
Logger::Get().Error("ITaskbarList::AddTab 失败");
|
||||
}
|
||||
} else {
|
||||
_taskbarList = nullptr;
|
||||
Logger::Get().Error("创建 ITaskbarList 失败");
|
||||
}
|
||||
|
||||
// 上面的尝试失败了则还原更改
|
||||
if (_taskbarList) {
|
||||
_taskbarList->DeleteTab(_hwndSrc);
|
||||
_taskbarList = nullptr;
|
||||
}
|
||||
if (_originalSrcExStyle) {
|
||||
// 首先还原所有者窗口的样式以压制任务栏的动画
|
||||
if (_originalOwnerExStyle) {
|
||||
SetWindowLongPtr(GetWindowOwner(_hwndSrc), GWL_EXSTYLE, _originalOwnerExStyle);
|
||||
_originalOwnerExStyle = 0;
|
||||
}
|
||||
|
||||
SetWindowLongPtr(_hwndSrc, GWL_EXSTYLE, _originalSrcExStyle);
|
||||
_originalSrcExStyle = 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GraphicsCaptureFrameSource::_TryCreateGraphicsCaptureItem(IGraphicsCaptureItemInterop* interop, HWND hwndSrc) noexcept {
|
||||
try {
|
||||
HRESULT hr = interop->CreateForWindow(
|
||||
hwndSrc,
|
||||
winrt::guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(),
|
||||
winrt::put_abi(_captureItem)
|
||||
);
|
||||
if (FAILED(hr)) {
|
||||
Logger::Get().ComError("创建 GraphicsCaptureItem 失败", hr);
|
||||
return false;
|
||||
}
|
||||
} catch (const winrt::hresult_error& e) {
|
||||
Logger::Get().Info(StrUtils::Concat("源窗口无法使用窗口捕获:", StrUtils::UTF16ToUTF8(e.message())));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 部分使用 Kirikiri 引擎的游戏有着这样的架构:游戏窗口并非根窗口,它被一个尺寸为 0 的窗口
|
||||
// 所有。此时 Alt+Tab 列表中的窗口和任务栏图标实际上是所有者窗口,这会导致 WGC 捕获游戏窗
|
||||
// 口时失败。_CaptureWindow 在初次捕获失败后会将 WS_EX_APPWINDOW 样式添加到游戏窗口,这
|
||||
// 可以工作,但也导致所有者窗口和游戏窗口同时出现在 Alt+Tab 列表中,引起用户的困惑。
|
||||
//
|
||||
// 此函数检测这种情况并改变所有者窗口的样式将它从 Alt+Tab 列表中移除。
|
||||
void GraphicsCaptureFrameSource::_RemoveOwnerFromAltTabList(HWND hwndSrc) noexcept {
|
||||
HWND hwndOwner = GetWindowOwner(hwndSrc);
|
||||
if (!hwndOwner) {
|
||||
return;
|
||||
}
|
||||
|
||||
RECT ownerRect{};
|
||||
if (!GetWindowRect(hwndOwner, &ownerRect)) {
|
||||
Logger::Get().Win32Error("GetWindowRect 失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查所有者窗口尺寸
|
||||
if (ownerRect.right != ownerRect.left || ownerRect.bottom != ownerRect.top) {
|
||||
return;
|
||||
}
|
||||
|
||||
LONG_PTR ownerExStyle = GetWindowLongPtr(hwndOwner, GWL_EXSTYLE);
|
||||
if (ownerExStyle == 0) {
|
||||
Logger::Get().Win32Error("GetWindowLongPtr 失败");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SetWindowLongPtr(hwndOwner, GWL_EXSTYLE, ownerExStyle | WS_EX_TOOLWINDOW)) {
|
||||
Logger::Get().Win32Error("SetWindowLongPtr 失败");
|
||||
return;
|
||||
}
|
||||
|
||||
_originalOwnerExStyle = ownerExStyle;
|
||||
}
|
||||
|
||||
bool GraphicsCaptureFrameSource::_CaptureMonitor(IGraphicsCaptureItemInterop* interop, const ScalingOptions& options, HWND hwndScaling) {
|
||||
// Win10 无法隐藏黄色边框,因此只在 Win11 中回落到屏幕捕获
|
||||
if (!Win32Utils::GetOSVersion().IsWin11()) {
|
||||
Logger::Get().Error("无法使用屏幕捕获");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 使全屏窗口无法被捕获到
|
||||
// WDA_EXCLUDEFROMCAPTURE 只在 Win10 20H1 及更新版本中可用
|
||||
if (!SetWindowDisplayAffinity(hwndScaling, WDA_EXCLUDEFROMCAPTURE)) {
|
||||
Logger::Get().Win32Error("SetWindowDisplayAffinity 失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
HMONITOR hMonitor = MonitorFromWindow(_hwndSrc, MONITOR_DEFAULTTONEAREST);
|
||||
if (!hMonitor) {
|
||||
Logger::Get().Win32Error("MonitorFromWindow 失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
MONITORINFO mi{};
|
||||
mi.cbSize = sizeof(mi);
|
||||
if (!GetMonitorInfo(hMonitor, &mi)) {
|
||||
Logger::Get().Win32Error("GetMonitorInfo 失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 放在屏幕左上角而不是中间可以提高帧率,这里是为了和 DesktopDuplication 保持一致
|
||||
if (!_CenterWindowIfNecessary(_hwndSrc, mi.rcWork)) {
|
||||
Logger::Get().Error("居中源窗口失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
RECT srcFrameRect = _GetSrcFrameRect(options.cropping, options.IsCaptureTitleBar());
|
||||
if (srcFrameRect == RECT{}) {
|
||||
Logger::Get().Error("_GetSrcFrameRect 失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
_frameBox = {
|
||||
UINT(srcFrameRect.left - mi.rcMonitor.left),
|
||||
UINT(srcFrameRect.top - mi.rcMonitor.top),
|
||||
0,
|
||||
UINT(srcFrameRect.right - mi.rcMonitor.left),
|
||||
UINT(srcFrameRect.bottom - mi.rcMonitor.top),
|
||||
1
|
||||
};
|
||||
|
||||
try {
|
||||
HRESULT hr = interop->CreateForMonitor(
|
||||
hMonitor,
|
||||
winrt::guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(),
|
||||
winrt::put_abi(_captureItem)
|
||||
);
|
||||
if (FAILED(hr)) {
|
||||
Logger::Get().ComError("创建 GraphicsCaptureItem 失败", hr);
|
||||
return false;
|
||||
}
|
||||
} catch (const winrt::hresult_error& e) {
|
||||
Logger::Get().Info(StrUtils::Concat("捕获屏幕失败:", StrUtils::UTF16ToUTF8(e.message())));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GraphicsCaptureFrameSource::StartCapture() {
|
||||
if (_captureSession) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建帧缓冲池
|
||||
// 帧的尺寸和 _captureItem.Size() 不同
|
||||
_captureFramePool = winrt::Direct3D11CaptureFramePool::Create(
|
||||
_wrappedD3DDevice,
|
||||
winrt::DirectXPixelFormat::B8G8R8A8UIntNormalized,
|
||||
1, // 帧的缓存数量
|
||||
{ (int)_frameBox.right, (int)_frameBox.bottom } // 帧的尺寸为包含源窗口的最小尺寸
|
||||
);
|
||||
|
||||
// 注册回调是为了确保每当有新的帧时会向当前线程发送消息
|
||||
// 回调中什么也不做
|
||||
_captureFramePool.FrameArrived([](const auto&, const auto&) {});
|
||||
|
||||
_captureSession = _captureFramePool.CreateCaptureSession(_captureItem);
|
||||
|
||||
// 不捕获光标
|
||||
if (winrt::ApiInformation::IsPropertyPresent(
|
||||
winrt::name_of<winrt::GraphicsCaptureSession>(),
|
||||
L"IsCursorCaptureEnabled"
|
||||
)) {
|
||||
// 从 v2004 开始提供
|
||||
_captureSession.IsCursorCaptureEnabled(false);
|
||||
}
|
||||
|
||||
// 不显示黄色边框
|
||||
if (winrt::ApiInformation::IsPropertyPresent(
|
||||
winrt::name_of<winrt::GraphicsCaptureSession>(),
|
||||
L"IsBorderRequired"
|
||||
)) {
|
||||
// 从 Win10 v2104 开始提供
|
||||
// Win32 应用中无需请求权限
|
||||
_captureSession.IsBorderRequired(false);
|
||||
}
|
||||
|
||||
_captureSession.StartCapture();
|
||||
} catch (const winrt::hresult_error& e) {
|
||||
Logger::Get().Info(StrUtils::Concat("Graphics Capture 失败:", StrUtils::UTF16ToUTF8(e.message())));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void GraphicsCaptureFrameSource::StopCapture() {
|
||||
if (_captureSession) {
|
||||
_captureSession.Close();
|
||||
_captureSession = nullptr;
|
||||
}
|
||||
if (_captureFramePool) {
|
||||
_captureFramePool.Close();
|
||||
_captureFramePool = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
GraphicsCaptureFrameSource::~GraphicsCaptureFrameSource() {
|
||||
StopCapture();
|
||||
|
||||
if (_taskbarList) {
|
||||
_taskbarList->DeleteTab(_hwndSrc);
|
||||
}
|
||||
|
||||
// 还原源窗口样式
|
||||
if (_originalSrcExStyle) {
|
||||
// 首先还原所有者窗口的样式以压制任务栏的动画
|
||||
if (_originalOwnerExStyle) {
|
||||
SetWindowLongPtr(GetWindowOwner(_hwndSrc), GWL_EXSTYLE, _originalOwnerExStyle);
|
||||
}
|
||||
|
||||
SetWindowLongPtr(_hwndSrc, GWL_EXSTYLE, _originalSrcExStyle);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
65
src/Magpie.Core/GraphicsCaptureFrameSource.h
Normal file
65
src/Magpie.Core/GraphicsCaptureFrameSource.h
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
#pragma once
|
||||
#include "FrameSourceBase.h"
|
||||
#include <winrt/Windows.Graphics.Capture.h>
|
||||
#include <Windows.Graphics.Capture.Interop.h>
|
||||
|
||||
namespace Magpie::Core {
|
||||
|
||||
// 使用 Window Runtime 的 Windows.Graphics.Capture API 抓取窗口
|
||||
// 见 https://docs.microsoft.com/en-us/windows/uwp/audio-video-camera/screen-capture
|
||||
class GraphicsCaptureFrameSource : public FrameSourceBase {
|
||||
public:
|
||||
GraphicsCaptureFrameSource() {};
|
||||
virtual ~GraphicsCaptureFrameSource();
|
||||
|
||||
bool Initialize(HWND hwndSrc, HWND hwndScaling, const ScalingOptions& options, ID3D11Device5* d3dDevice) noexcept override;
|
||||
|
||||
UpdateState Update() noexcept override;
|
||||
|
||||
bool IsScreenCapture() override {
|
||||
return _isScreenCapture;
|
||||
}
|
||||
|
||||
const char* GetName() const noexcept override {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
bool StartCapture();
|
||||
|
||||
void StopCapture();
|
||||
|
||||
static constexpr const char* NAME = "Graphics Capture";
|
||||
|
||||
protected:
|
||||
bool _HasRoundCornerInWin11() noexcept override {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _CanCaptureTitleBar() noexcept override {
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
bool _CaptureWindow(IGraphicsCaptureItemInterop* interop, const ScalingOptions& options);
|
||||
|
||||
bool _CaptureMonitor(IGraphicsCaptureItemInterop* interop, const ScalingOptions& options, HWND hwndScaling);
|
||||
|
||||
bool _TryCreateGraphicsCaptureItem(IGraphicsCaptureItemInterop* interop, HWND hwndSrc) noexcept;
|
||||
|
||||
void _RemoveOwnerFromAltTabList(HWND hwndSrc) noexcept;
|
||||
|
||||
LONG_PTR _originalSrcExStyle = 0;
|
||||
LONG_PTR _originalOwnerExStyle = 0;
|
||||
winrt::com_ptr<ITaskbarList> _taskbarList;
|
||||
|
||||
D3D11_BOX _frameBox{};
|
||||
|
||||
bool _isScreenCapture = false;
|
||||
|
||||
winrt::Windows::Graphics::Capture::GraphicsCaptureItem _captureItem{ nullptr };
|
||||
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool _captureFramePool{ nullptr };
|
||||
winrt::Windows::Graphics::Capture::GraphicsCaptureSession _captureSession{ nullptr };
|
||||
winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice _wrappedD3DDevice{ nullptr };
|
||||
};
|
||||
|
||||
}
|
||||
|
|
@ -88,6 +88,8 @@
|
|||
<ClInclude Include="EffectDrawer.h" />
|
||||
<ClInclude Include="EffectHelper.h" />
|
||||
<ClInclude Include="ExportHelper.h" />
|
||||
<ClInclude Include="FrameSourceBase.h" />
|
||||
<ClInclude Include="GraphicsCaptureFrameSource.h" />
|
||||
<ClInclude Include="ImGuiFontsCacheManager.h" />
|
||||
<ClInclude Include="ImGuiHelper.h" />
|
||||
<ClInclude Include="include\Magpie.Core.h" />
|
||||
|
|
@ -109,6 +111,8 @@
|
|||
<ClCompile Include="EffectCacheManager.cpp" />
|
||||
<ClCompile Include="EffectCompiler.cpp" />
|
||||
<ClCompile Include="EffectDrawer.cpp" />
|
||||
<ClCompile Include="FrameSourceBase.cpp" />
|
||||
<ClCompile Include="GraphicsCaptureFrameSource.cpp" />
|
||||
<ClCompile Include="ImGuiFontsCacheManager.cpp" />
|
||||
<ClCompile Include="ImGuiHelper.cpp" />
|
||||
<ClCompile Include="LoggerHelper.cpp" />
|
||||
|
|
|
|||
|
|
@ -161,8 +161,10 @@ bool ScalingWindow::_CheckForeground(HWND hwndForeground) const noexcept {
|
|||
}*/
|
||||
|
||||
if (rectForground == RECT{}) {
|
||||
if (!Win32Utils::GetWindowFrameRect(hwndForeground, rectForground)) {
|
||||
Logger::Get().Error("GetWindowFrameRect 失败");
|
||||
HRESULT hr = DwmGetWindowAttribute(hwndForeground,
|
||||
DWMWA_EXTENDED_FRAME_BOUNDS, &rectForground, sizeof(rectForground));
|
||||
if (FAILED(hr)) {
|
||||
Logger::Get().ComError("DwmGetWindowAttribute 失败", hr);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,17 +105,6 @@ bool Win32Utils::GetClientScreenRect(HWND hWnd, RECT& rect) {
|
|||
return true;
|
||||
}
|
||||
|
||||
bool Win32Utils::GetWindowFrameRect(HWND hWnd, RECT& result) {
|
||||
HRESULT hr = DwmGetWindowAttribute(hWnd,
|
||||
DWMWA_EXTENDED_FRAME_BOUNDS, &result, sizeof(result));
|
||||
if (FAILED(hr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool Win32Utils::ReadFile(const wchar_t* fileName, std::vector<BYTE>& result) {
|
||||
Logger::Get().Info(StrUtils::Concat("读取文件:", StrUtils::UTF16ToUTF8(fileName)));
|
||||
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@ struct Win32Utils {
|
|||
|
||||
static bool GetClientScreenRect(HWND hWnd, RECT& rect);
|
||||
|
||||
static bool GetWindowFrameRect(HWND hWnd, RECT& result);
|
||||
|
||||
static bool ReadFile(const wchar_t* fileName, std::vector<BYTE>& result);
|
||||
|
||||
static bool ReadTextFile(const wchar_t* fileName, std::string& result);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue