capture HDR support

This commit is contained in:
Lightczx
2024-05-17 11:59:06 +08:00
parent 4323ced7dc
commit c6e6d08707
8 changed files with 167 additions and 59 deletions

View File

@@ -8,45 +8,19 @@ using Windows.UI;
namespace Snap.Hutao.Control.Media;
/// <summary>
/// RGBA 颜色
/// </summary>
[HighQuality]
internal struct Rgba32
{
/// <summary>
/// R
/// </summary>
public byte R;
/// <summary>
/// G
/// </summary>
public byte G;
/// <summary>
/// B
/// </summary>
public byte B;
/// <summary>
/// A
/// </summary>
public byte A;
/// <summary>
/// 构造一个新的 RGBA8 颜色
/// </summary>
/// <param name="hex">色值字符串</param>
public Rgba32(string hex)
: this(hex.Length == 6 ? Convert.ToUInt32($"{hex}FF", 16) : Convert.ToUInt32(hex, 16))
{
}
/// <summary>
/// 使用 RGBA 代码初始化新的结构
/// </summary>
/// <param name="xrgbaCode">RGBA 代码</param>
public unsafe Rgba32(uint xrgbaCode)
{
// uint layout: 0xRRGGBBAA is AABBGGRR
@@ -80,11 +54,6 @@ internal struct Rgba32
return *(Color*)&rgba;
}
/// <summary>
/// 从 HSL 颜色转换
/// </summary>
/// <param name="hsl">HSL 颜色</param>
/// <returns>RGBA8颜色</returns>
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);
}
/// <summary>
/// 转换到 HSL 颜色
/// </summary>
/// <returns>HSL 颜色</returns>
public readonly Hsla32 ToHsl()
{
const double toDouble = 1.0 / 255;

View File

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

View File

@@ -2,7 +2,7 @@
"profiles": {
"Snap.Hutao": {
"commandName": "MsixPackage",
"nativeDebugging": true,
"nativeDebugging": false,
"doNotLaunchApp": false,
"allowLocalNetworkLoopbackProperty": true
},

View File

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

View File

@@ -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<byte> rawPixelData;
private readonly int pixelWidth;
private readonly int pixelHeight;
public GameScreenCaptureResult(IMemoryOwner<byte> 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();
}
}

View File

@@ -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<IMemoryOwner<byte>>? frameRawPixelDataTaskCompletionSource;
private TaskCompletionSource<GameScreenCaptureResult>? frameRawPixelDataTaskCompletionSource;
private bool isFrameRawPixelDataRequested;
private SizeInt32 contentSize;
@@ -47,7 +52,7 @@ internal sealed class GameScreenCaptureSession : IDisposable
session.StartCapture();
}
public async ValueTask<IMemoryOwner<byte>> RequestFrameRawPixelDataAsync()
public async ValueTask<GameScreenCaptureResult> 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<byte> subresource = new(d3d11MappedSubresource.pData, (int)d3d11Texture2DDesc.Height, (int)d3d11MappedSubresource.RowPitch);
int rowLength = contentSize.Width * 4;
IMemoryOwner<byte> 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<byte> 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<byte> 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<byte> buffer = GameScreenCaptureMemoryPool.Shared.Rent((int)(textureHeight * textureWidth * 4));
Span<Bgra32> pixelBuffer = MemoryMarshal.Cast<byte, Bgra32>(buffer.Memory.Span);
for (int row = 0; row < textureHeight; row++)
{
ReadOnlySpan<Rgba64> subresourceRow = MemoryMarshal.Cast<byte, Rgba64>(subresource[row][..rowLength]);
Span<Bgra32> 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;
}
}
}

View File

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

View File

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