mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
4 Commits
fix/dailyn
...
fix/1588
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2284510852 | ||
|
|
7da778699b | ||
|
|
5bfc790ea2 | ||
|
|
fc13b85739 |
@@ -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>();
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
27
src/Snap.Hutao/Snap.Hutao/Core/ReadOnlySpan2D.cs
Normal file
27
src/Snap.Hutao/Snap.Hutao/Core/ReadOnlySpan2D.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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++)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user