Compare commits

..

4 Commits

Author SHA1 Message Date
qhy040404
2284510852 fix #1588 2024-05-18 15:17:42 +08:00
Lightczx
7da778699b code style 2024-05-09 16:25:21 +08:00
Lightczx
5bfc790ea2 pooled frame 2024-05-08 17:30:04 +08:00
DismissedLight
fc13b85739 Merge pull request #1610 from DGP-Studio/fix/dailynotevmslim/metadata 2024-05-08 09:03:25 +08:00
12 changed files with 413 additions and 29 deletions

View File

@@ -48,6 +48,8 @@ public sealed partial class App : Application
/// <param name="serviceProvider">服务提供器</param>
public App(IServiceProvider serviceProvider)
{
// DispatcherShutdownMode = DispatcherShutdownMode.OnExplicitShutdown;
// Load app resource
InitializeComponent();
activation = serviceProvider.GetRequiredService<IActivation>();

View File

@@ -51,6 +51,12 @@ internal sealed class HutaoException : Exception
throw new InvalidCastException(message, innerException);
}
[DoesNotReturn]
public static InvalidOperationException InvalidOperation(string message, Exception? innerException = default)
{
throw new InvalidOperationException(message, innerException);
}
[DoesNotReturn]
public static NotSupportedException NotSupported(string? message = default, Exception? innerException = default)
{

View File

@@ -0,0 +1,27 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Core;
internal readonly ref struct ReadOnlySpan2D<T>
where T : unmanaged
{
private readonly ref T reference;
private readonly int length;
private readonly int columns;
public unsafe ReadOnlySpan2D(void* pointer, int length, int columns)
{
reference = ref *(T*)pointer;
this.length = length;
this.columns = columns;
}
public ReadOnlySpan<T> this[int row]
{
get => MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref reference, row * columns), columns);
}
}

View File

@@ -32,6 +32,7 @@ internal sealed partial class IdentifyMonitorWindow : Window
{
List<IdentifyMonitorWindow> windows = [];
// TODO: the order here is not sync with unity.
IReadOnlyList<DisplayArea> displayAreas = DisplayArea.FindAll();
for (int i = 0; i < displayAreas.Count; i++)
{

View File

@@ -0,0 +1,63 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Win32.Foundation;
using Snap.Hutao.Win32.Graphics.Gdi;
using Snap.Hutao.Win32.System.WinRT.Graphics.Capture;
using Windows.Graphics.Capture;
using Windows.Graphics.DirectX;
using Windows.Graphics.DirectX.Direct3D11;
using static Snap.Hutao.Win32.Gdi32;
using static Snap.Hutao.Win32.User32;
namespace Snap.Hutao.Service.Game.Automation.ScreenCapture;
internal readonly struct GameScreenCaptureContext
{
public readonly GraphicsCaptureItem Item;
private readonly IDirect3DDevice direct3DDevice;
private readonly HWND hwnd;
public GameScreenCaptureContext(IDirect3DDevice direct3DDevice, HWND hwnd)
{
this.direct3DDevice = direct3DDevice;
this.hwnd = hwnd;
GraphicsCaptureItem.As<IGraphicsCaptureItemInterop>().CreateForWindow(hwnd, out Item);
}
public Direct3D11CaptureFramePool CreatePool()
{
return Direct3D11CaptureFramePool.CreateFreeThreaded(direct3DDevice, DeterminePixelFormat(hwnd), 2, Item.Size);
}
public void RecreatePool(Direct3D11CaptureFramePool framePool)
{
framePool.Recreate(direct3DDevice, DeterminePixelFormat(hwnd), 2, Item.Size);
}
public GraphicsCaptureSession CreateSession(Direct3D11CaptureFramePool framePool)
{
GraphicsCaptureSession session = framePool.CreateCaptureSession(Item);
session.IsCursorCaptureEnabled = false;
session.IsBorderRequired = false;
return session;
}
private static DirectXPixelFormat DeterminePixelFormat(HWND hwnd)
{
HDC hdc = GetDC(hwnd);
if (hdc != HDC.NULL)
{
int bitsPerPixel = GetDeviceCaps(hdc, GET_DEVICE_CAPS_INDEX.BITSPIXEL);
_ = ReleaseDC(hwnd, hdc);
if (bitsPerPixel >= 32)
{
return DirectXPixelFormat.R16G16B16A16Float;
}
}
return DirectXPixelFormat.B8G8R8A8UIntNormalized;
}
}

View File

@@ -0,0 +1,82 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Core.ExceptionService;
using System.Buffers;
namespace Snap.Hutao.Service.Game.Automation.ScreenCapture;
internal sealed class GameScreenCaptureMemoryPool : MemoryPool<byte>
{
private static LazySlim<GameScreenCaptureMemoryPool> lazyShared = new(() => new());
private readonly object syncRoot = new();
private readonly LinkedList<GameScreenCaptureBuffer> unrentedBuffers = [];
private readonly LinkedList<GameScreenCaptureBuffer> rentedBuffers = [];
private int bufferCount;
public new static GameScreenCaptureMemoryPool Shared { get => lazyShared.Value; }
public override int MaxBufferSize { get => Array.MaxLength; }
public int BufferCount { get => bufferCount; }
public override IMemoryOwner<byte> Rent(int minBufferSize = -1)
{
ArgumentOutOfRangeException.ThrowIfLessThan(minBufferSize, 0);
lock (syncRoot)
{
foreach (GameScreenCaptureBuffer buffer in unrentedBuffers)
{
if (buffer.Memory.Length >= minBufferSize)
{
unrentedBuffers.Remove(buffer);
rentedBuffers.AddLast(buffer);
return buffer;
}
}
GameScreenCaptureBuffer newBuffer = new(this, minBufferSize);
rentedBuffers.AddLast(newBuffer);
++bufferCount;
return newBuffer;
}
}
protected override void Dispose(bool disposing)
{
lock (syncRoot)
{
if (rentedBuffers.Count > 0)
{
HutaoException.InvalidOperation("There are still rented buffers.");
}
}
}
internal sealed class GameScreenCaptureBuffer : IMemoryOwner<byte>
{
private readonly GameScreenCaptureMemoryPool pool;
private readonly byte[] buffer;
public GameScreenCaptureBuffer(GameScreenCaptureMemoryPool pool, int bufferSize)
{
this.pool = pool;
buffer = GC.AllocateUninitializedArray<byte>(bufferSize);
}
public Memory<byte> Memory { get => buffer.AsMemory(); }
public void Dispose()
{
lock (pool.syncRoot)
{
pool.rentedBuffers.Remove(this);
pool.unrentedBuffers.AddLast(this);
}
}
}
}

View File

@@ -7,10 +7,9 @@ using Snap.Hutao.Win32.Graphics.Direct3D;
using Snap.Hutao.Win32.Graphics.Direct3D11;
using Snap.Hutao.Win32.Graphics.Dxgi;
using Snap.Hutao.Win32.System.Com;
using Snap.Hutao.Win32.System.WinRT.Graphics.Capture;
using Windows.Graphics.Capture;
using Windows.Graphics.DirectX;
using Windows.Graphics.DirectX.Direct3D11;
using WinRT;
using static Snap.Hutao.Win32.ConstValues;
using static Snap.Hutao.Win32.D3D11;
using static Snap.Hutao.Win32.Macros;
@@ -18,7 +17,8 @@ using static Snap.Hutao.Win32.Macros;
namespace Snap.Hutao.Service.Game.Automation.ScreenCapture;
[ConstructorGenerated]
internal sealed partial class GameScreenCaptureService
[Injection(InjectAs.Singleton, typeof(IGameScreenCaptureService))]
internal sealed partial class GameScreenCaptureService : IGameScreenCaptureService
{
private readonly ILogger<GameScreenCaptureService> logger;
@@ -39,6 +39,7 @@ internal sealed partial class GameScreenCaptureService
return true;
}
[SuppressMessage("", "SH002")]
public unsafe bool TryStartCapture(HWND hwnd, [NotNullWhen(true)] out GameScreenCaptureSession? session)
{
session = default;
@@ -64,6 +65,8 @@ internal sealed partial class GameScreenCaptureService
return false;
}
IUnknownMarshal.Release(pDXGIDevice);
hr = CreateDirect3D11DeviceFromDXGIDevice(pDXGIDevice, out Win32.System.WinRT.IInspectable* inspectable);
if (FAILED(hr))
{
@@ -71,29 +74,13 @@ internal sealed partial class GameScreenCaptureService
return false;
}
IDirect3DDevice direct3DDevice = WinRT.IInspectable.FromAbi((nint)inspectable).ObjRef.AsInterface<IDirect3DDevice>();
GraphicsCaptureItem.As<IGraphicsCaptureItemInterop>().CreateForWindow(hwnd, out GraphicsCaptureItem item);
IUnknownMarshal.Release(inspectable);
// Note
Direct3D11CaptureFramePool framePool = Direct3D11CaptureFramePool.CreateFreeThreaded(direct3DDevice, DirectXPixelFormat.B8G8R8A8UIntNormalized, 2, item.Size);
IDirect3DDevice direct3DDevice = IInspectable.FromAbi((nint)inspectable).ObjRef.AsInterface<IDirect3DDevice>();
GameScreenCaptureContext captureContext = new(direct3DDevice, hwnd);
session = new(captureContext, logger);
IUnknownMarshal.Release(pDXGIDevice);
return true;
}
}
internal sealed class GameScreenCaptureSession : IDisposable
{
private readonly Direct3D11CaptureFramePool framePool;
private readonly GraphicsCaptureSession session;
public GameScreenCaptureSession(Direct3D11CaptureFramePool framePool)
{
this.session = session;
}
public void Dispose()
{
session.Dispose();
}
}

View File

@@ -0,0 +1,197 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Win32.Graphics.Direct3D11;
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.Runtime.CompilerServices;
using Windows.Graphics;
using Windows.Graphics.Capture;
using Windows.Graphics.DirectX.Direct3D11;
using WinRT;
using static Snap.Hutao.Win32.Macros;
namespace Snap.Hutao.Service.Game.Automation.ScreenCapture;
internal sealed class GameScreenCaptureSession : IDisposable
{
private readonly GameScreenCaptureContext captureContext;
private readonly Direct3D11CaptureFramePool framePool;
private readonly GraphicsCaptureSession session;
private readonly ILogger logger;
private TaskCompletionSource<IMemoryOwner<byte>>? frameRawPixelDataTaskCompletionSource;
private bool isFrameRawPixelDataRequested;
private SizeInt32 contentSize;
private bool isDisposed;
[SuppressMessage("", "SH002")]
public GameScreenCaptureSession(GameScreenCaptureContext captureContext, ILogger logger)
{
this.captureContext = captureContext;
this.logger = logger;
contentSize = captureContext.Item.Size;
captureContext.Item.Closed += OnItemClosed;
framePool = captureContext.CreatePool();
framePool.FrameArrived += OnFrameArrived;
session = captureContext.CreateSession(framePool);
session.StartCapture();
}
public async ValueTask<IMemoryOwner<byte>> RequestFrameRawPixelDataAsync()
{
if (Volatile.Read(ref isFrameRawPixelDataRequested))
{
HutaoException.InvalidOperation("The frame raw pixel data has already been requested.");
}
if (isDisposed)
{
HutaoException.InvalidOperation("The session has been disposed.");
}
frameRawPixelDataTaskCompletionSource = new();
Volatile.Write(ref isFrameRawPixelDataRequested, true);
return await frameRawPixelDataTaskCompletionSource.Task.ConfigureAwait(false);
}
public void Dispose()
{
if (isDisposed)
{
return;
}
session.Dispose();
framePool.Dispose();
isDisposed = true;
}
private void OnItemClosed(GraphicsCaptureItem sender, object args)
{
Dispose();
}
private unsafe void OnFrameArrived(Direct3D11CaptureFramePool sender, object args)
{
// Simply ignore the frame if the frame raw pixel data is not requested.
if (!Volatile.Read(ref isFrameRawPixelDataRequested))
{
return;
}
using (Direct3D11CaptureFrame? frame = sender.TryGetNextFrame())
{
if (frame is null)
{
return;
}
bool needsReset = false;
if (frame.ContentSize != contentSize)
{
needsReset = true;
contentSize = frame.ContentSize;
}
try
{
UnsafeProcessFrameSurface(frame.Surface);
}
catch (Exception ex) // TODO: test if it's device lost.
{
logger.LogError(ex, "Failed to process the frame surface.");
needsReset = true;
}
if (needsReset)
{
captureContext.RecreatePool(sender);
}
}
}
private unsafe void UnsafeProcessFrameSurface(IDirect3DSurface surface)
{
IDirect3DDxgiInterfaceAccess access = surface.As<IDirect3DDxgiInterfaceAccess>();
if (FAILED(access.GetInterface(in IDXGISurface.IID, out IDXGISurface* pDXGISurface)))
{
return;
}
if (FAILED(pDXGISurface->GetDesc(out DXGI_SURFACE_DESC dxgiSurfaceDesc)))
{
return;
}
// Should be the same device used to create the frame pool.
if (FAILED(pDXGISurface->GetDevice(in ID3D11Device.IID, out ID3D11Device* pD3D11Device)))
{
return;
}
D3D11_TEXTURE2D_DESC d3d11Texture2DDesc = default;
d3d11Texture2DDesc.Width = dxgiSurfaceDesc.Width;
d3d11Texture2DDesc.Height = dxgiSurfaceDesc.Height;
d3d11Texture2DDesc.ArraySize = 1;
// We have to copy out the resource to a CPU readable texture.
d3d11Texture2DDesc.CPUAccessFlags = D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_READ;
// DirectX will automatically convert any format to B8G8R8A8_UNORM.
d3d11Texture2DDesc.Format = DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM;
d3d11Texture2DDesc.MipLevels = 1;
d3d11Texture2DDesc.SampleDesc.Count = 1;
d3d11Texture2DDesc.Usage = D3D11_USAGE.D3D11_USAGE_STAGING;
if (FAILED(pD3D11Device->CreateTexture2D(ref d3d11Texture2DDesc, ref Unsafe.NullRef<D3D11_SUBRESOURCE_DATA>(), out ID3D11Texture2D* pD3D11Texture2D)))
{
return;
}
if (FAILED(access.GetInterface(in ID3D11Resource.IID, out ID3D11Resource* pD3D11Resource)))
{
return;
}
pD3D11Device->GetImmediateContext(out ID3D11DeviceContext* pD3D11DeviceContext);
pD3D11DeviceContext->CopyResource((ID3D11Resource*)pD3D11Texture2D, pD3D11Resource);
if (FAILED(pD3D11DeviceContext->Map((ID3D11Resource*)pD3D11Texture2D, 0U, D3D11_MAP.D3D11_MAP_READ, 0U, out D3D11_MAPPED_SUBRESOURCE d3d11MappedSubresource)))
{
return;
}
// The D3D11_MAPPED_SUBRESOURCE data is arranged as follows:
// |--------- Row pitch ----------|
// |---- Data width ----|- Blank -|
// ┌────────────────────┬─────────┐
// │ │ │
// │ 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));
}
ArgumentNullException.ThrowIfNull(frameRawPixelDataTaskCompletionSource);
frameRawPixelDataTaskCompletionSource.SetResult(buffer);
}
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Win32.Foundation;
namespace Snap.Hutao.Service.Game.Automation.ScreenCapture;
internal interface IGameScreenCaptureService
{
bool IsSupported();
bool TryStartCapture(HWND hwnd, [NotNullWhen(true)] out GameScreenCaptureSession? session);
}

View File

@@ -66,7 +66,7 @@ internal sealed partial class TypedWishSummary : Wish
/// </summary>
public string TotalOrangeFormatted
{
get => $"{TotalOrangePull} [{TotalOrangePercent,6:p2}]";
get => $"{TotalOrangePull} [{(TotalOrangePercent is double.NaN ? 0D : TotalOrangePercent),6:p2}]";
}
/// <summary>
@@ -74,7 +74,7 @@ internal sealed partial class TypedWishSummary : Wish
/// </summary>
public string TotalPurpleFormatted
{
get => $"{TotalPurplePull} [{TotalPurplePercent,6:p2}]";
get => $"{TotalPurplePull} [{(TotalPurplePercent is double.NaN ? 0D : TotalPurplePercent),6:p2}]";
}
/// <summary>
@@ -82,7 +82,7 @@ internal sealed partial class TypedWishSummary : Wish
/// </summary>
public string TotalBlueFormatted
{
get => $"{TotalBluePull} [{TotalBluePercent,6:p2}]";
get => $"{TotalBluePull} [{(TotalBluePercent is double.NaN ? 0D : TotalBluePercent),6:p2}]";
}
public ColorSegmentCollection PullPercentSegmentSource

View File

@@ -9,8 +9,6 @@ internal readonly struct HWND
public HWND(nint value) => Value = value;
public bool IsNull => Value is 0;
public static unsafe implicit operator HWND(nint value) => *(HWND*)&value;
public static unsafe implicit operator nint(HWND value) => *(nint*)&value;

View File

@@ -5,5 +5,13 @@ namespace Snap.Hutao.Win32.Graphics.Gdi;
internal readonly struct HDC
{
public static readonly HDC NULL = 0;
public readonly nint Value;
public static unsafe implicit operator HDC(nint value) => *(HDC*)&value;
public static unsafe bool operator ==(HDC left, HDC right) => *(nint*)&left == *(nint*)&right;
public static unsafe bool operator !=(HDC left, HDC right) => *(nint*)&left != *(nint*)&right;
}