mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
capture HDR support
This commit is contained in:
@@ -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;
|
||||
|
||||
14
src/Snap.Hutao/Snap.Hutao/Control/Media/Rgba64.cs
Normal file
14
src/Snap.Hutao/Snap.Hutao/Control/Media/Rgba64.cs
Normal 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;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"profiles": {
|
||||
"Snap.Hutao": {
|
||||
"commandName": "MsixPackage",
|
||||
"nativeDebugging": true,
|
||||
"nativeDebugging": false,
|
||||
"doNotLaunchApp": false,
|
||||
"allowLocalNetworkLoopbackProperty": true
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user