From c6e6d087079fab71805b50c81bd88490753111ce Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Fri, 17 May 2024 11:59:06 +0800 Subject: [PATCH] capture HDR support --- .../Snap.Hutao/Control/Media/Rgba32.cs | 35 ---------- .../Snap.Hutao/Control/Media/Rgba64.cs | 14 ++++ .../Snap.Hutao/Properties/launchSettings.json | 2 +- .../ScreenCapture/GameScreenCaptureContext.cs | 6 +- .../ScreenCapture/GameScreenCaptureResult.cs | 29 ++++++++ .../ScreenCapture/GameScreenCaptureSession.cs | 67 +++++++++++++++---- .../Snap.Hutao/ViewModel/TestViewModel.cs | 65 ++++++++++++++++-- .../Direct3D11/ID3D11DeviceContext.cs | 8 ++- 8 files changed, 167 insertions(+), 59 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Control/Media/Rgba64.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Automation/ScreenCapture/GameScreenCaptureResult.cs diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Media/Rgba32.cs b/src/Snap.Hutao/Snap.Hutao/Control/Media/Rgba32.cs index a8c70b55..33aacc36 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Media/Rgba32.cs +++ b/src/Snap.Hutao/Snap.Hutao/Control/Media/Rgba32.cs @@ -8,45 +8,19 @@ using Windows.UI; namespace Snap.Hutao.Control.Media; -/// -/// RGBA 颜色 -/// [HighQuality] internal struct Rgba32 { - /// - /// R - /// public byte R; - - /// - /// G - /// public byte G; - - /// - /// B - /// public byte B; - - /// - /// A - /// public byte A; - /// - /// 构造一个新的 RGBA8 颜色 - /// - /// 色值字符串 public Rgba32(string hex) : this(hex.Length == 6 ? Convert.ToUInt32($"{hex}FF", 16) : Convert.ToUInt32(hex, 16)) { } - /// - /// 使用 RGBA 代码初始化新的结构 - /// - /// RGBA 代码 public unsafe Rgba32(uint xrgbaCode) { // uint layout: 0xRRGGBBAA is AABBGGRR @@ -80,11 +54,6 @@ internal struct Rgba32 return *(Color*)&rgba; } - /// - /// 从 HSL 颜色转换 - /// - /// HSL 颜色 - /// RGBA8颜色 public static Rgba32 FromHsl(Hsla32 hsl) { double chroma = (1 - Math.Abs((2 * hsl.L) - 1)) * hsl.S; @@ -138,10 +107,6 @@ internal struct Rgba32 return new(r, g, b, a); } - /// - /// 转换到 HSL 颜色 - /// - /// HSL 颜色 public readonly Hsla32 ToHsl() { const double toDouble = 1.0 / 255; diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Media/Rgba64.cs b/src/Snap.Hutao/Snap.Hutao/Control/Media/Rgba64.cs new file mode 100644 index 00000000..ba1d15b6 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Control/Media/Rgba64.cs @@ -0,0 +1,14 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. +// Some part of this file came from: +// https://github.com/xunkong/desktop/tree/main/src/Desktop/Desktop/Pages/CharacterInfoPage.xaml.cs + +namespace Snap.Hutao.Control.Media; + +internal struct Rgba64 +{ + public Half R; + public Half G; + public Half B; + public Half A; +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Properties/launchSettings.json b/src/Snap.Hutao/Snap.Hutao/Properties/launchSettings.json index 36a6b395..a5fb2a14 100644 --- a/src/Snap.Hutao/Snap.Hutao/Properties/launchSettings.json +++ b/src/Snap.Hutao/Snap.Hutao/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "Snap.Hutao": { "commandName": "MsixPackage", - "nativeDebugging": true, + "nativeDebugging": false, "doNotLaunchApp": false, "allowLocalNetworkLoopbackProperty": true }, diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Automation/ScreenCapture/GameScreenCaptureContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Automation/ScreenCapture/GameScreenCaptureContext.cs index d012a2f9..ccb0d299 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Automation/ScreenCapture/GameScreenCaptureContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Automation/ScreenCapture/GameScreenCaptureContext.cs @@ -52,19 +52,19 @@ internal readonly struct GameScreenCaptureContext { clientBox = default; - // Check if the window is minimized + // Ensure the window is not minimized if (IsIconic(hwnd)) { return false; } - // Check if the window is at least partially in the screen + // Ensure the window is at least partially in the screen if (!(GetClientRect(hwnd, out RECT clientRect) && (clientRect.right > 0) && (clientRect.bottom > 0))) { return false; } - // Make sure we get the window chrome rect + // Ensure we get the window chrome rect if (DwmGetWindowAttribute(hwnd, DWMWINDOWATTRIBUTE.DWMWA_EXTENDED_FRAME_BOUNDS, out RECT windowRect) != HRESULT.S_OK) { return false; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Automation/ScreenCapture/GameScreenCaptureResult.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Automation/ScreenCapture/GameScreenCaptureResult.cs new file mode 100644 index 00000000..8aa4ceb3 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Automation/ScreenCapture/GameScreenCaptureResult.cs @@ -0,0 +1,29 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Buffers; + +namespace Snap.Hutao.Service.Game.Automation.ScreenCapture; + +internal sealed class GameScreenCaptureResult : IDisposable +{ + private readonly IMemoryOwner rawPixelData; + private readonly int pixelWidth; + private readonly int pixelHeight; + + public GameScreenCaptureResult(IMemoryOwner rawPixelData, int pixelWidth, int pixelHeight) + { + this.rawPixelData = rawPixelData; + this.pixelWidth = pixelWidth; + this.pixelHeight = pixelHeight; + } + + public int PixelWidth { get => pixelWidth; } + + public int PixelHeight { get => pixelHeight; } + + public void Dispose() + { + rawPixelData.Dispose(); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Automation/ScreenCapture/GameScreenCaptureSession.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Automation/ScreenCapture/GameScreenCaptureSession.cs index 4a3d3bc9..af8f54a5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Automation/ScreenCapture/GameScreenCaptureSession.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Automation/ScreenCapture/GameScreenCaptureSession.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Control.Media; using Snap.Hutao.Core; using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Win32.Graphics.Direct3D11; @@ -8,7 +9,9 @@ using Snap.Hutao.Win32.Graphics.Dxgi; using Snap.Hutao.Win32.Graphics.Dxgi.Common; using Snap.Hutao.Win32.System.WinRT.Graphics.Capture; using System.Buffers; +using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Windows.Graphics; using Windows.Graphics.Capture; using Windows.Graphics.DirectX.Direct3D11; @@ -19,12 +22,14 @@ namespace Snap.Hutao.Service.Game.Automation.ScreenCapture; internal sealed class GameScreenCaptureSession : IDisposable { + private static readonly Half ByteMaxValue = 255; + private readonly GameScreenCaptureContext captureContext; private readonly Direct3D11CaptureFramePool framePool; private readonly GraphicsCaptureSession session; private readonly ILogger logger; - private TaskCompletionSource>? frameRawPixelDataTaskCompletionSource; + private TaskCompletionSource? frameRawPixelDataTaskCompletionSource; private bool isFrameRawPixelDataRequested; private SizeInt32 contentSize; @@ -47,7 +52,7 @@ internal sealed class GameScreenCaptureSession : IDisposable session.StartCapture(); } - public async ValueTask> RequestFrameRawPixelDataAsync() + public async ValueTask RequestFrameAsync() { if (Volatile.Read(ref isFrameRawPixelDataRequested)) { @@ -170,7 +175,7 @@ internal sealed class GameScreenCaptureSession : IDisposable if (boxAvailable) { - + pD3D11DeviceContext->CopySubresourceRegion((ID3D11Resource*)pD3D11Texture2D, 0U, 0U, 0U, 0U, pD3D11Resource, 0U, in clientBox); } else { @@ -190,17 +195,53 @@ internal sealed class GameScreenCaptureSession : IDisposable // │ Actual data │ Stride │ // │ │ │ // └────────────────────┴─────────┘ - ReadOnlySpan2D subresource = new(d3d11MappedSubresource.pData, (int)d3d11Texture2DDesc.Height, (int)d3d11MappedSubresource.RowPitch); - - int rowLength = contentSize.Width * 4; - IMemoryOwner buffer = GameScreenCaptureMemoryPool.Shared.Rent(contentSize.Height * rowLength); - - for (int row = 0; row < contentSize.Height; row++) - { - subresource[row][..rowLength].CopyTo(buffer.Memory.Span.Slice(row * rowLength, rowLength)); - } + ReadOnlySpan2D subresource = new(d3d11MappedSubresource.pData, (int)textureHeight, (int)d3d11MappedSubresource.RowPitch); ArgumentNullException.ThrowIfNull(frameRawPixelDataTaskCompletionSource); - frameRawPixelDataTaskCompletionSource.SetResult(buffer); + switch (dxgiSurfaceDesc.Format) + { + case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM: + { + int rowLength = (int)textureWidth * 4; + IMemoryOwner buffer = GameScreenCaptureMemoryPool.Shared.Rent((int)(textureHeight * textureWidth * 4)); + + for (int row = 0; row < textureHeight; row++) + { + subresource[row][..rowLength].CopyTo(buffer.Memory.Span.Slice(row * rowLength, rowLength)); + } + + frameRawPixelDataTaskCompletionSource.SetResult(new(buffer, (int)textureWidth, (int)textureHeight)); + return; + } + + case DXGI_FORMAT.DXGI_FORMAT_R16G16B16A16_FLOAT: + { + int rowLength = (int)textureWidth * 8; + IMemoryOwner buffer = GameScreenCaptureMemoryPool.Shared.Rent((int)(textureHeight * textureWidth * 4)); + Span pixelBuffer = MemoryMarshal.Cast(buffer.Memory.Span); + + for (int row = 0; row < textureHeight; row++) + { + ReadOnlySpan subresourceRow = MemoryMarshal.Cast(subresource[row][..rowLength]); + Span bufferRow = pixelBuffer.Slice(row * (int)textureWidth, (int)textureWidth); + for (int column = 0; column < textureWidth; column++) + { + ref readonly Rgba64 float16Pixel = ref subresourceRow[column]; + ref Bgra32 pixel = ref bufferRow[column]; + pixel.B = (byte)(float16Pixel.B * ByteMaxValue); + pixel.G = (byte)(float16Pixel.G * ByteMaxValue); + pixel.R = (byte)(float16Pixel.R * ByteMaxValue); + pixel.A = (byte)(float16Pixel.A * ByteMaxValue); + } + } + + frameRawPixelDataTaskCompletionSource.SetResult(new(buffer, (int)textureWidth, (int)textureHeight)); + return; + } + + default: + HutaoException.NotSupported($"Unexpected DXGI_FORMAT: {dxgiSurfaceDesc.Format}"); + return; + } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs index d1aeabe9..0ab90962 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs @@ -14,6 +14,7 @@ using Snap.Hutao.Web.Hutao.HutaoAsAService; using Snap.Hutao.Win32.Foundation; using Snap.Hutao.Win32.Graphics.Direct3D; using Snap.Hutao.Win32.Graphics.Direct3D11; +using Snap.Hutao.Win32.Graphics.Dwm; using Snap.Hutao.Win32.Graphics.Dxgi; using Snap.Hutao.Win32.Graphics.Dxgi.Common; using Snap.Hutao.Win32.System.Com; @@ -30,7 +31,9 @@ using Windows.Storage.Streams; using WinRT; using static Snap.Hutao.Win32.ConstValues; using static Snap.Hutao.Win32.D3D11; +using static Snap.Hutao.Win32.DwmApi; using static Snap.Hutao.Win32.Macros; +using static Snap.Hutao.Win32.User32; namespace Snap.Hutao.ViewModel; @@ -214,9 +217,14 @@ internal sealed partial class TestViewModel : Abstraction.ViewModel return; } + bool boxAvailable = TryGetClientBox(hwnd, surfaceDesc.Width, surfaceDesc.Height, out D3D11_BOX clientBox); + (uint textureWidth, uint textureHeight) = boxAvailable + ? (clientBox.right - clientBox.left, clientBox.bottom - clientBox.top) + : (surfaceDesc.Width, surfaceDesc.Height); + D3D11_TEXTURE2D_DESC texture2DDesc = default; - texture2DDesc.Width = surfaceDesc.Width; - texture2DDesc.Height = surfaceDesc.Height; + texture2DDesc.Width = textureWidth; + texture2DDesc.Height = textureHeight; texture2DDesc.ArraySize = 1; texture2DDesc.CPUAccessFlags = D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ; texture2DDesc.Format = DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM; @@ -240,15 +248,22 @@ internal sealed partial class TestViewModel : Abstraction.ViewModel } pD3D11Device->GetImmediateContext(out ID3D11DeviceContext* pDeviceContext); - pDeviceContext->CopyResource((ID3D11Resource*)pTexture2D, pD3D11Resource); + + if (boxAvailable) + { + pDeviceContext->CopySubresourceRegion((ID3D11Resource*)pTexture2D, 0, 0, 0, 0, pD3D11Resource, 0, in clientBox); + } + else + { + logger.LogInformation("Box not available"); + pDeviceContext->CopyResource((ID3D11Resource*)pTexture2D, pD3D11Resource); + } if (FAILED(pDeviceContext->Map((ID3D11Resource*)pTexture2D, 0, D3D11_MAP.D3D11_MAP_READ, 0, out D3D11_MAPPED_SUBRESOURCE mappedSubresource))) { return; } - int size = (int)(mappedSubresource.RowPitch * texture2DDesc.Height * 4); - SoftwareBitmap softwareBitmap = new(BitmapPixelFormat.Bgra8, (int)texture2DDesc.Width, (int)texture2DDesc.Height, BitmapAlphaMode.Premultiplied); using (BitmapBuffer bitmapBuffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Write)) { @@ -314,5 +329,45 @@ internal sealed partial class TestViewModel : Abstraction.ViewModel { logger.LogWarning("D3D11CreateDevice failed"); } + + static bool TryGetClientBox(HWND hwnd, uint width, uint height, out D3D11_BOX clientBox) + { + clientBox = default; + return false; + + // Ensure the window is not minimized + if (IsIconic(hwnd)) + { + return false; + } + + // Ensure the window is at least partially in the screen + if (!(GetClientRect(hwnd, out RECT clientRect) && (clientRect.right > 0) && (clientRect.bottom > 0))) + { + return false; + } + + // Ensure we get the window chrome rect + if (DwmGetWindowAttribute(hwnd, DWMWINDOWATTRIBUTE.DWMWA_EXTENDED_FRAME_BOUNDS, out RECT windowRect) != HRESULT.S_OK) + { + return false; + } + + // Provide a client side (0, 0) and translate to screen coordinates + POINT clientPoint = default; + if (!ClientToScreen(hwnd, ref clientPoint)) + { + return false; + } + + uint left = clientBox.left = clientPoint.x > windowRect.left ? (uint)(clientPoint.x - windowRect.left) : 0U; + uint top = clientBox.top = clientPoint.y > windowRect.top ? (uint)(clientPoint.y - windowRect.top) : 0U; + clientBox.right = left + (width > left ? (uint)Math.Min(width - left, clientRect.right) : 1U); + clientBox.bottom = top + (height > top ? (uint)Math.Min(height - top, clientRect.bottom) : 1U); + clientBox.front = 0U; + clientBox.back = 1U; + + return clientBox.right <= width && clientBox.bottom <= height; + } } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Win32/Graphics/Direct3D11/ID3D11DeviceContext.cs b/src/Snap.Hutao/Snap.Hutao/Win32/Graphics/Direct3D11/ID3D11DeviceContext.cs index 2dc71a1d..fd0ebe6e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Win32/Graphics/Direct3D11/ID3D11DeviceContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Win32/Graphics/Direct3D11/ID3D11DeviceContext.cs @@ -39,9 +39,13 @@ internal unsafe struct ID3D11DeviceContext ThisPtr->Unmap((ID3D11DeviceContext*)Unsafe.AsPointer(ref this), pResource, Subresource); } - public void CopySubresourceRegion(ID3D11Resource* pDstResource, uint DstSubresource, uint DstX, uint DstY, uint DstZ, ID3D11Resource* pSrcResource, uint SrcSubresource, D3D11_BOX* pSrcBox) + [SuppressMessage("", "SA1313")] + public unsafe void CopySubresourceRegion(ID3D11Resource* pDstResource, uint DstSubresource, uint DstX, uint DstY, uint DstZ, ID3D11Resource* pSrcResource, uint SrcSubresource, [AllowNull] ref readonly D3D11_BOX srcBox) { - ThisPtr->CopySubresourceRegion((ID3D11DeviceContext*)Unsafe.AsPointer(ref this), pDstResource, DstSubresource, DstX, DstY, DstZ, pSrcResource, SrcSubresource, pSrcBox); + fixed (D3D11_BOX* pSrcBox = &srcBox) + { + ThisPtr->CopySubresourceRegion((ID3D11DeviceContext*)Unsafe.AsPointer(ref this), pDstResource, DstSubresource, DstX, DstY, DstZ, pSrcResource, SrcSubresource, pSrcBox); + } } public void CopyResource(ID3D11Resource* pDstResource, ID3D11Resource* pSrcResource)