From 0bea2d095a9eef76fd653a5485210df2c8c96b60 Mon Sep 17 00:00:00 2001 From: Shatyuka Date: Sun, 11 May 2025 01:17:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=88=AA=E5=9B=BE=E4=BC=98=E5=8C=96=20(#1480)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * BitBlt 优化 * BitBlt恢复Top-down * 渲染时翻转图像 * CaptureSession引用计数 * 恢复成无拷贝Mat * 合法性检查 * 优化截图预览窗口 * 保存截图文件必要时需要克隆一份Mat * BitBlt内存池 * 返回拷贝就不用对Session做引用计数了 * 移除CaptureImageRes * 优化DirectX * 更好地处理padding * BitBlt去掉padding 1920*1080的游戏窗口是4字节对齐的,因此不会有性能影响。这里主要用于测试。 * 修复修改窗口大小 * 合并CreateStagingTexture * 修复设备丢失崩溃 * WGC截图支持HDR * fix typo * CodeQA * 去掉1px窗口边框 * DirectX截图去掉A通道 * HDR转换使用GPU加速 --------- Co-authored-by: 辉鸭蛋 --- .../AutoFishing/BehaviourTreeExtensions.cs | 27 +- .../GameTask/CaptureContent.cs | 14 +- .../GameTask/Common/TaskControl.cs | 4 +- .../GameTask/Model/Area/DesktopRegion.cs | 2 +- .../GameTask/Model/Area/GameCaptureRegion.cs | 13 +- .../GameTask/Model/Area/ImageRegion.cs | 17 - .../GameTask/TaskTriggerDispatcher.cs | 11 +- .../Helpers/Extensions/BitmapExtension.cs | 43 +++ .../Helpers/Extensions/EnumExtensions.cs | 18 +- BetterGenshinImpact/Model/EnumItem.cs | 8 +- .../Notification/NotificationService.cs | 15 +- .../View/CaptureTestWindow.xaml | 1 - .../View/CaptureTestWindow.xaml.cs | 70 +++- BetterGenshinImpact/View/Pages/HomePage.xaml | 2 +- Fischless.GameCapture/BitBlt/BitBltCapture.cs | 53 ++- Fischless.GameCapture/BitBlt/BitBltMat.cs | 32 ++ .../BitBlt/BitBltOldCapture.cs | 70 ---- Fischless.GameCapture/BitBlt/BitBltSession.cs | 172 +++++----- Fischless.GameCapture/CaptureImageRes.cs | 76 ----- Fischless.GameCapture/CaptureModes.cs | 22 +- .../DwmSharedSurface/Helpers/NativeMethods.cs | 18 - .../DwmSharedSurface/SharedSurfaceCapture.cs | 211 ++++++------ .../Fischless.GameCapture.csproj | 1 + Fischless.GameCapture/GameCaptureFactory.cs | 4 +- .../Graphics/GraphicsCapture.cs | 318 ++++++++---------- .../Graphics/Helpers/Direct3D11Helper.cs | 91 +++-- .../Graphics/Helpers/HdrToSdrShader.cs | 27 ++ .../Graphics/Helpers/Texture2DExtensions.cs | 87 +---- Fischless.GameCapture/IGameCapture.cs | 2 +- 29 files changed, 670 insertions(+), 759 deletions(-) create mode 100644 Fischless.GameCapture/BitBlt/BitBltMat.cs delete mode 100644 Fischless.GameCapture/BitBlt/BitBltOldCapture.cs delete mode 100644 Fischless.GameCapture/CaptureImageRes.cs delete mode 100644 Fischless.GameCapture/DwmSharedSurface/Helpers/NativeMethods.cs create mode 100644 Fischless.GameCapture/Graphics/Helpers/HdrToSdrShader.cs diff --git a/BetterGenshinImpact/GameTask/AutoFishing/BehaviourTreeExtensions.cs b/BetterGenshinImpact/GameTask/AutoFishing/BehaviourTreeExtensions.cs index 1106a51d..deec3f59 100644 --- a/BetterGenshinImpact/GameTask/AutoFishing/BehaviourTreeExtensions.cs +++ b/BetterGenshinImpact/GameTask/AutoFishing/BehaviourTreeExtensions.cs @@ -166,7 +166,6 @@ namespace BetterGenshinImpact.GameTask.AutoFishing Directory.CreateDirectory(path); } - var bitmap = imageRegion.SrcBitmap; if (String.IsNullOrWhiteSpace(name)) { name = $@"{DateTime.Now:yyyyMMddHHmmssffff}.png"; @@ -176,17 +175,25 @@ namespace BetterGenshinImpact.GameTask.AutoFishing var mat = imageRegion.SrcMat; if (TaskContext.Instance().Config.CommonConfig.ScreenshotUidCoverEnabled) { - var assetScale = TaskContext.Instance().SystemInfo.ScaleTo1080PRatio; - var rect = new Rect((int)(mat.Width - MaskWindowConfig.UidCoverRightBottomRect.X * assetScale), - (int)(mat.Height - MaskWindowConfig.UidCoverRightBottomRect.Y * assetScale), - (int)(MaskWindowConfig.UidCoverRightBottomRect.Width * assetScale), - (int)(MaskWindowConfig.UidCoverRightBottomRect.Height * assetScale)); - mat.Rectangle(rect, Scalar.White, -1); + new Task(() => + { + using var mat2 = mat.Clone(); + var assetScale = TaskContext.Instance().SystemInfo.ScaleTo1080PRatio; + var rect = new Rect((int)(mat.Width - MaskWindowConfig.UidCoverRightBottomRect.X * assetScale), + (int)(mat.Height - MaskWindowConfig.UidCoverRightBottomRect.Y * assetScale), + (int)(MaskWindowConfig.UidCoverRightBottomRect.Width * assetScale), + (int)(MaskWindowConfig.UidCoverRightBottomRect.Height * assetScale)); + mat2.Rectangle(rect, Scalar.White, -1); + Cv2.ImWrite(savePath, mat2); + }).Start(); } - new Task(() => + else { - Cv2.ImWrite(savePath, mat); - }).Start(); + new Task(() => + { + Cv2.ImWrite(savePath, mat); + }).Start(); + } } public void Reset() diff --git a/BetterGenshinImpact/GameTask/CaptureContent.cs b/BetterGenshinImpact/GameTask/CaptureContent.cs index e7d644b5..5f238b59 100644 --- a/BetterGenshinImpact/GameTask/CaptureContent.cs +++ b/BetterGenshinImpact/GameTask/CaptureContent.cs @@ -1,7 +1,5 @@ using BetterGenshinImpact.GameTask.Model.Area; using System; -using System.Drawing; -using Fischless.GameCapture; using OpenCvSharp; namespace BetterGenshinImpact.GameTask; @@ -18,9 +16,9 @@ public class CaptureContent : IDisposable public int FrameRate => (int)(1000 / TimerInterval); - public ImageRegion CaptureRectArea { get; private set; } + public ImageRegion CaptureRectArea { get; } - public CaptureContent(CaptureImageRes image, int frameIndex, double interval) + public CaptureContent(Mat image, int frameIndex, double interval) { FrameIndex = frameIndex; TimerInterval = interval; @@ -42,5 +40,11 @@ public class CaptureContent : IDisposable public void Dispose() { CaptureRectArea.Dispose(); + GC.SuppressFinalize(this); } -} \ No newline at end of file + + ~CaptureContent() + { + Dispose(); + } +} diff --git a/BetterGenshinImpact/GameTask/Common/TaskControl.cs b/BetterGenshinImpact/GameTask/Common/TaskControl.cs index 19819251..a23092fa 100644 --- a/BetterGenshinImpact/GameTask/Common/TaskControl.cs +++ b/BetterGenshinImpact/GameTask/Common/TaskControl.cs @@ -182,7 +182,7 @@ public class TaskControl } } - public static CaptureImageRes CaptureGameImage(IGameCapture? gameCapture) + public static Mat CaptureGameImage(IGameCapture? gameCapture) { var image = gameCapture?.Capture(); if (image == null) @@ -208,7 +208,7 @@ public class TaskControl } } - public static CaptureImageRes? CaptureGameImageNoRetry(IGameCapture? gameCapture) + public static Mat? CaptureGameImageNoRetry(IGameCapture? gameCapture) { return gameCapture?.Capture(); } diff --git a/BetterGenshinImpact/GameTask/Model/Area/DesktopRegion.cs b/BetterGenshinImpact/GameTask/Model/Area/DesktopRegion.cs index 651460e2..f16b13b9 100644 --- a/BetterGenshinImpact/GameTask/Model/Area/DesktopRegion.cs +++ b/BetterGenshinImpact/GameTask/Model/Area/DesktopRegion.cs @@ -69,7 +69,7 @@ public class DesktopRegion : Region Simulation.SendInput.Mouse.MoveMouseBy((int)dx, (int)dy); } - public GameCaptureRegion Derive(CaptureImageRes captureMat, int x, int y) + public GameCaptureRegion Derive(Mat captureMat, int x, int y) { return new GameCaptureRegion(captureMat, x, y, this, new TranslationConverter(x, y)); } diff --git a/BetterGenshinImpact/GameTask/Model/Area/GameCaptureRegion.cs b/BetterGenshinImpact/GameTask/Model/Area/GameCaptureRegion.cs index 5e4def3d..65898223 100644 --- a/BetterGenshinImpact/GameTask/Model/Area/GameCaptureRegion.cs +++ b/BetterGenshinImpact/GameTask/Model/Area/GameCaptureRegion.cs @@ -12,19 +12,8 @@ namespace BetterGenshinImpact.GameTask.Model.Area; /// 游戏捕获区域类 /// 主要用于转换到遮罩窗口的坐标 /// -public class GameCaptureRegion : ImageRegion +public class GameCaptureRegion(Mat mat, int initX, int initY, Region? owner = null, INodeConverter? converter = null, DrawContent? drawContent = null) : ImageRegion(mat, initX, initY, owner, converter, drawContent) { - public GameCaptureRegion(Mat mat, int initX, int initY, Region? owner = null, INodeConverter? converter = null, - DrawContent? drawContent = null) : base(mat, initX, initY, owner, converter, drawContent) - { - } - - public GameCaptureRegion(CaptureImageRes image, int initX, int initY, Region? owner = null, - INodeConverter? converter = null, DrawContent? drawContent = null) : base(image, initX, initY, owner, converter, drawContent) - { - } - - /// /// 在游戏捕获图像的坐标维度进行转换到遮罩窗口的坐标维度 /// diff --git a/BetterGenshinImpact/GameTask/Model/Area/ImageRegion.cs b/BetterGenshinImpact/GameTask/Model/Area/ImageRegion.cs index a57e7ff7..f3aa4601 100644 --- a/BetterGenshinImpact/GameTask/Model/Area/ImageRegion.cs +++ b/BetterGenshinImpact/GameTask/Model/Area/ImageRegion.cs @@ -78,23 +78,6 @@ public class ImageRegion : Region _srcMat = mat; } - public ImageRegion(CaptureImageRes image, int x, int y, Region? owner = null, INodeConverter? converter = null, - DrawContent? drawContent = null) : base(x, y, image.Width, image.Height, owner, converter, drawContent) - { - if (image.Bitmap != null) - { - _srcBitmap = image.Bitmap; - } - else if (image.Mat != null) - { - _srcMat = image.Mat; - } - else - { - throw new Exception("ImageRegion的构造函数参数错误"); - } - } - private bool HasImage() { return _srcBitmap != null || _srcMat != null; diff --git a/BetterGenshinImpact/GameTask/TaskTriggerDispatcher.cs b/BetterGenshinImpact/GameTask/TaskTriggerDispatcher.cs index 4b256e49..bdab1edf 100644 --- a/BetterGenshinImpact/GameTask/TaskTriggerDispatcher.cs +++ b/BetterGenshinImpact/GameTask/TaskTriggerDispatcher.cs @@ -386,9 +386,12 @@ namespace BetterGenshinImpact.GameTask Directory.CreateDirectory(path); } - var image = TaskControl.CaptureGameImage(GameCapture); - var mat = image.ForceGetMat(); - if (mat == null) + Mat mat; + try + { + mat = TaskControl.CaptureGameImage(GameCapture); + } + catch (Exception) { _logger.LogInformation("截图失败,未获取到图像"); return; @@ -410,6 +413,8 @@ namespace BetterGenshinImpact.GameTask Cv2.ImWrite(savePath, mat); } + mat.Dispose(); + _logger.LogInformation("截图已保存: {Name}", name); } catch (Exception e) diff --git a/BetterGenshinImpact/Helpers/Extensions/BitmapExtension.cs b/BetterGenshinImpact/Helpers/Extensions/BitmapExtension.cs index 81896c80..54214dd2 100644 --- a/BetterGenshinImpact/Helpers/Extensions/BitmapExtension.cs +++ b/BetterGenshinImpact/Helpers/Extensions/BitmapExtension.cs @@ -1,12 +1,55 @@ using OpenCvSharp; +using System; using System.Drawing; using System.IO; +using System.Windows.Media; using System.Windows.Media.Imaging; +using Color = System.Drawing.Color; namespace BetterGenshinImpact.Helpers.Extensions { public static class BitmapExtension { + public static BitmapSource ToBitmapSource(this Bitmap bitmap) + { + return bitmap.ToBitmapSource(out _); + } + + public static BitmapSource ToBitmapSource(this Bitmap bitmap, out bool bottomUp) + { + var bitmapData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), + System.Drawing.Imaging.ImageLockMode.ReadOnly, bitmap.PixelFormat); + + var stride = bitmapData.Stride; + var buffer = bitmapData.Scan0; + if (stride < 0) + { + bottomUp = true; + stride = -stride; + buffer -= stride * (bitmapData.Height - 1); + } + else + { + bottomUp = false; + } + + var pixelFormat = bitmap.PixelFormat switch + { + System.Drawing.Imaging.PixelFormat.Format24bppRgb => PixelFormats.Bgr24, + System.Drawing.Imaging.PixelFormat.Format32bppArgb => PixelFormats.Bgra32, + _ => throw new NotSupportedException($"Unsupported pixel format {bitmap.PixelFormat}") + }; + + var bitmapSource = BitmapSource.Create( + bitmapData.Width, bitmapData.Height, + bitmap.HorizontalResolution, bitmap.VerticalResolution, + pixelFormat, null, + buffer, stride * bitmapData.Height, stride); + + bitmap.UnlockBits(bitmapData); + + return bitmapSource; + } public static BitmapImage ToBitmapImage(this Bitmap bitmap) { diff --git a/BetterGenshinImpact/Helpers/Extensions/EnumExtensions.cs b/BetterGenshinImpact/Helpers/Extensions/EnumExtensions.cs index 64e61239..6e5d5cb5 100644 --- a/BetterGenshinImpact/Helpers/Extensions/EnumExtensions.cs +++ b/BetterGenshinImpact/Helpers/Extensions/EnumExtensions.cs @@ -17,11 +17,23 @@ public static class EnumExtensions .FirstOrDefault() ?.Description ?? value.ToString(); } - + + public static int GetOrder(this Enum value) + { + return value.GetType() + .GetField(value.ToString()) + ?.GetCustomAttributes(typeof(DefaultValueAttribute), false) + .Cast() + .FirstOrDefault() + ?.Value as int? ?? 0; + } + public static IEnumerable> ToEnumItems() where T : Enum { return Enum.GetValues(typeof(T)) .Cast() - .Select(EnumItem.Create); + .Select(EnumItem.Create) + .OrderBy(x => x.Order) + .ThenBy(x => x.Value); } -} \ No newline at end of file +} diff --git a/BetterGenshinImpact/Model/EnumItem.cs b/BetterGenshinImpact/Model/EnumItem.cs index 43043a29..7daf5c4b 100644 --- a/BetterGenshinImpact/Model/EnumItem.cs +++ b/BetterGenshinImpact/Model/EnumItem.cs @@ -7,6 +7,7 @@ public class EnumItem where T : Enum { public T Value { get; set; } public string DisplayName { get; set; } + public int Order { get; set; } public string EnumName => Value.ToString(); // 提供一个静态工厂方法来创建实例 @@ -15,7 +16,10 @@ public class EnumItem where T : Enum return new EnumItem { Value = value, - DisplayName = value.GetDescription() // 使用扩展方法获取Description + DisplayName = value.GetDescription(), // 使用扩展方法获取Description + Order = value.GetOrder() }; } -} \ No newline at end of file + + +} diff --git a/BetterGenshinImpact/Service/Notification/NotificationService.cs b/BetterGenshinImpact/Service/Notification/NotificationService.cs index 47b0bd05..b6dff1ca 100644 --- a/BetterGenshinImpact/Service/Notification/NotificationService.cs +++ b/BetterGenshinImpact/Service/Notification/NotificationService.cs @@ -406,19 +406,8 @@ public class NotificationService : IHostedService, IDisposable try { - var image = TaskControl.CaptureGameImageNoRetry(TaskTriggerDispatcher.GlobalGameCapture); - - if (image != null) - { - if (image.Bitmap != null) - { - notificationData.Screenshot = image.Bitmap; - } - else if (image.Mat != null) - { - notificationData.Screenshot = image.Mat.ToBitmap(); - } - } + var mat = TaskControl.CaptureGameImageNoRetry(TaskTriggerDispatcher.GlobalGameCapture); + if (mat != null) notificationData.Screenshot = mat.ToBitmap(); } catch (Exception ex) { diff --git a/BetterGenshinImpact/View/CaptureTestWindow.xaml b/BetterGenshinImpact/View/CaptureTestWindow.xaml index 2dc8eece..139ffd05 100644 --- a/BetterGenshinImpact/View/CaptureTestWindow.xaml +++ b/BetterGenshinImpact/View/CaptureTestWindow.xaml @@ -12,6 +12,5 @@ - diff --git a/BetterGenshinImpact/View/CaptureTestWindow.xaml.cs b/BetterGenshinImpact/View/CaptureTestWindow.xaml.cs index 62d66548..6831a6ec 100644 --- a/BetterGenshinImpact/View/CaptureTestWindow.xaml.cs +++ b/BetterGenshinImpact/View/CaptureTestWindow.xaml.cs @@ -1,20 +1,22 @@ using BetterGenshinImpact.GameTask; -using BetterGenshinImpact.Helpers.Extensions; using Fischless.GameCapture; using System; using System.Collections.Generic; using System.Diagnostics; using System.Windows; using System.Windows.Media; +using System.Windows.Media.Imaging; using BetterGenshinImpact.Helpers; -using OpenCvSharp.WpfExtensions; +using OpenCvSharp; using Wpf.Ui.Violeta.Controls; +using Size = OpenCvSharp.Size; namespace BetterGenshinImpact.View; -public partial class CaptureTestWindow : Window +public partial class CaptureTestWindow { private IGameCapture? _capture; + private Size _cacheSize; private long _captureTime; private long _transferTime; @@ -57,23 +59,75 @@ public partial class CaptureTestWindow : Window CompositionTarget.Rendering += Loop; } + private static WriteableBitmap ToWriteableBitmap(Mat mat) + { + PixelFormat pixelFormat; + var type = mat.Type(); + if (type == MatType.CV_8UC3) + { + pixelFormat = PixelFormats.Bgr24; + } + else if (type == MatType.CV_8UC4) + { + pixelFormat = PixelFormats.Bgra32; + } + else + { + throw new NotSupportedException($"Unsupported pixel format {type}"); + } + + var bitmap = new WriteableBitmap(mat.Width, mat.Height, 96, 96, pixelFormat, null); + UpdateWriteableBitmap(bitmap, mat); + + return bitmap; + } + + private static unsafe void UpdateWriteableBitmap(WriteableBitmap bitmap, Mat mat) + { + bitmap.Lock(); + var stride = bitmap.BackBufferStride; + var step = mat.Step(); + if (stride == step) + { + var length = stride * bitmap.PixelHeight; + Buffer.MemoryCopy(mat.Data.ToPointer(), bitmap.BackBuffer.ToPointer(), length, length); + } + else + { + var length = Math.Min(stride, step); + for (var i = 0; i < bitmap.PixelHeight; i++) + { + Buffer.MemoryCopy((void*)(mat.Data + i * step), (void*)(bitmap.BackBuffer + i * stride), length, length); + } + } + bitmap.AddDirtyRect(new Int32Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight)); + bitmap.Unlock(); + } + private void Loop(object? sender, EventArgs e) { var sw = new Stopwatch(); sw.Start(); - var image = _capture?.Capture(); + using var mat = _capture?.Capture(); sw.Stop(); Debug.WriteLine("截图耗时:" + sw.ElapsedMilliseconds); _captureTime += sw.ElapsedMilliseconds; - var bitmap = image?.ForceGetBitmap(); - if (bitmap != null) + if (mat != null) { - Debug.WriteLine($"Bitmap:{bitmap.Width}x{bitmap.Height}"); + Debug.WriteLine($"Bitmap:{mat.Width}x{mat.Height}"); _captureCount++; sw.Reset(); sw.Start(); - DisplayCaptureResultImage.Source = bitmap.ToBitmapSource(); + if (_cacheSize != mat.Size()) + { + DisplayCaptureResultImage.Source = ToWriteableBitmap(mat); + _cacheSize = mat.Size(); + } + else + { + UpdateWriteableBitmap((WriteableBitmap)DisplayCaptureResultImage.Source, mat); + } sw.Stop(); Debug.WriteLine("转换耗时:" + sw.ElapsedMilliseconds); _transferTime += sw.ElapsedMilliseconds; diff --git a/BetterGenshinImpact/View/Pages/HomePage.xaml b/BetterGenshinImpact/View/Pages/HomePage.xaml index 3ece3250..7fcf220b 100644 --- a/BetterGenshinImpact/View/Pages/HomePage.xaml +++ b/BetterGenshinImpact/View/Pages/HomePage.xaml @@ -132,7 +132,7 @@ Stop(); + public void Dispose() + { + Stop(); + GC.SuppressFinalize(this); + } - public CaptureImageRes? Capture() => Capture(false); + public Mat? Capture() => Capture(false); public void Start(nint hWnd, Dictionary? settings = null) { @@ -50,9 +54,8 @@ public class BitBltCapture : IGameCapture /// - /// 检查窗口大小,如果改变则更新截图尺寸。返回是否成功。 + /// 检查窗口大小,如果改变则更新截图尺寸。 /// - /// private void CheckSession() { if (_lockSlim.WaitingWriteCount > 0 || !_lockSlim.TryEnterWriteLock(TimeSpan.FromSeconds(0.5))) @@ -66,7 +69,7 @@ public class BitBltCapture : IGameCapture { // 窗口状态变化可能会导致会话失效 // 上次截图失败则重置会话,避免一直截图失败 - if (_session is not null && (_session.IsInvalid() || _lastCaptureFailed)) + if (_session is not null && (_session.Invalid || _lastCaptureFailed)) { _session.Dispose(); _session = null; @@ -84,15 +87,18 @@ public class BitBltCapture : IGameCapture var width = windowRect.right - windowRect.left; var height = windowRect.bottom - windowRect.top; - _session ??= new BitBltSession(_hWnd, width, height); - if (_session.Width == width && _session.Height == height) + if (_session != null) { - // 窗口大小没有改变 - return; + if (_session.Width == width && _session.Height == height) + { + // 窗口大小没有改变 + return; + } + + // 窗口尺寸被改变,释放资源,然后重新创建会话 + _session.Dispose(); } - // 窗口尺寸被改变,释放资源,然后重新创建会话 - _session.Dispose(); _session = new BitBltSession(_hWnd, width, height); } catch (Exception e) @@ -110,7 +116,7 @@ public class BitBltCapture : IGameCapture /// /// 递归标志 /// 截图 - private CaptureImageRes? Capture(bool recursive) + private Mat? Capture(bool recursive) { if (_hWnd == IntPtr.Zero) { @@ -139,13 +145,13 @@ public class BitBltCapture : IGameCapture { // 成功截图 _lastCaptureFailed = false; - return CaptureImageRes.BuildNullable(result); + return result; } - else if (result is null) + else { - if (_lastCaptureFailed) return CaptureImageRes.BuildNullable(result); // 这不是首次失败,不再进行尝试 + if (_lastCaptureFailed) return result; // 这不是首次失败,不再进行尝试 _lastCaptureFailed = true; // 设置失败标志 - if (recursive) return CaptureImageRes.BuildNullable(result); // 已设置递归标志,说明也不是首次失败 + if (recursive) return result; // 已设置递归标志,说明也不是首次失败 } } finally @@ -165,24 +171,15 @@ public class BitBltCapture : IGameCapture /// 截图功能的实现。需要加锁后调用,一般只由 Capture 方法调用。 /// /// - private Bitmap? Capture0() + private Mat? Capture0() { - Bitmap? bitmap = null; try { - if (_session is null) - { - // 没有成功创建会话,直接返回空 - return null; - } - bitmap = _session.GetImage(); - return bitmap; + return _session?.GetImage(); } catch (Exception e) { // 理论这里不应出现异常,除非窗口不存在了或者有什么bug - // 出现异常的时候释放内存 - bitmap?.Dispose(); Error.WriteLine("[BitBlt]Failed to capture image {0}", e); return null; } @@ -208,4 +205,4 @@ public class BitBltCapture : IGameCapture IsCapturing = false; } -} \ No newline at end of file +} diff --git a/Fischless.GameCapture/BitBlt/BitBltMat.cs b/Fischless.GameCapture/BitBlt/BitBltMat.cs new file mode 100644 index 00000000..f2ed0c80 --- /dev/null +++ b/Fischless.GameCapture/BitBlt/BitBltMat.cs @@ -0,0 +1,32 @@ +using OpenCvSharp; +using OpenCvSharp.Internal; + +namespace Fischless.GameCapture.BitBlt; + +public class BitBltMat : Mat +{ + private readonly BitBltSession _session; + private readonly IntPtr _data; + + private BitBltMat(IntPtr ptr, BitBltSession session, IntPtr data) + { + if (ptr == IntPtr.Zero) + throw new OpenCvSharpException("Native object address is NULL"); + this.ptr = ptr; + _session = session; + _data = data; + } + + public static Mat FromPixelData(BitBltSession session, int rows, int cols, MatType type, IntPtr data, long step = 0) + { + NativeMethods.HandleException( + NativeMethods.core_Mat_new8(rows, cols, type, data, new IntPtr(step), out var ptr)); + return new BitBltMat(ptr, session, data); + } + + protected override void DisposeUnmanaged() + { + base.DisposeUnmanaged(); + _session.ReleaseBuffer(_data); + } +} diff --git a/Fischless.GameCapture/BitBlt/BitBltOldCapture.cs b/Fischless.GameCapture/BitBlt/BitBltOldCapture.cs deleted file mode 100644 index 64875683..00000000 --- a/Fischless.GameCapture/BitBlt/BitBltOldCapture.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Diagnostics; -using Vanara.PInvoke; - -namespace Fischless.GameCapture.BitBlt; - -public class BitBltOldCapture : IGameCapture -{ - private nint _hWnd; - - public static object LockObject { get; } = new(); - - public bool IsCapturing { get; private set; } - - public void Dispose() => Stop(); - - public void Start(nint hWnd, Dictionary? settings = null) - { - _hWnd = hWnd; - IsCapturing = true; - if (settings != null && settings.TryGetValue("autoFixWin11BitBlt", out var value)) - { - if (value is true) - { - BitBltRegistryHelper.SetDirectXUserGlobalSettings(); - } - } - } - - public CaptureImageRes? Capture() - { - if (_hWnd == IntPtr.Zero) - { - return null; - } - - try - { - lock (LockObject) - { - User32.GetClientRect(_hWnd, out var windowRect); - int x = default, y = default; - var width = windowRect.right - windowRect.left; - var height = windowRect.bottom - windowRect.top; - - Bitmap bitmap = new(width, height); - using System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(bitmap); - var hdcDest = g.GetHdc(); - var hdcSrc = User32.GetDC(_hWnd == IntPtr.Zero ? User32.GetDesktopWindow() : _hWnd); - Gdi32.StretchBlt(hdcDest, 0, 0, width, height, hdcSrc, x, y, width, height, - Gdi32.RasterOperationMode.SRCCOPY); - g.ReleaseHdc(); - Gdi32.DeleteDC(hdcDest); - Gdi32.DeleteDC(hdcSrc); - return CaptureImageRes.BuildNullable(bitmap); - } - } - catch (Exception e) - { - Debug.WriteLine(e); - } - - return null!; - } - - public void Stop() - { - _hWnd = IntPtr.Zero; - IsCapturing = false; - } -} \ No newline at end of file diff --git a/Fischless.GameCapture/BitBlt/BitBltSession.cs b/Fischless.GameCapture/BitBlt/BitBltSession.cs index 3714d6d1..79520926 100644 --- a/Fischless.GameCapture/BitBlt/BitBltSession.cs +++ b/Fischless.GameCapture/BitBlt/BitBltSession.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Diagnostics; using System.Runtime.InteropServices; using OpenCvSharp; @@ -7,17 +8,6 @@ namespace Fischless.GameCapture.BitBlt; public class BitBltSession : IDisposable { - // 用于过滤alpha通道 - private static readonly Gdi32.BLENDFUNCTION BlendFunction = new() - { - BlendOp = 0, //Gdi32.BlendOperation.AC_SRC_OVER, - BlendFlags = 0, - SourceConstantAlpha = 255, - AlphaFormat = 0 - }; - - private readonly int _height; - // 窗口句柄 private readonly HWND _hWnd; @@ -25,13 +15,17 @@ public class BitBltSession : IDisposable // 大小计算好的宽高,截图用这个不会爆炸 private readonly int _width; - - // 位图指针,这个指针会在位图释放时自动释放 - private IntPtr _bitsPtr; + private readonly int _height; // 位图句柄 private Gdi32.SafeHBITMAP _hBitmap; + // 位图数据指针,这个指针会在位图释放时自动释放 + private IntPtr _bitsPtr; + + // 位图数据一行字节数 + private readonly int _stride; + // 缓冲区 CompatibleDC private Gdi32.SafeHDC _hdcDest; @@ -41,17 +35,24 @@ public class BitBltSession : IDisposable // 旧位图,析构时一起释放掉 private HGDIOBJ _oldBitmap; + // Bitmap buffer 大小 + private readonly int _bufferSize; + + // Bitmap 内存池 + private readonly ConcurrentStack _bufferPool = []; + + // 窗口原宽高 + public int Width { get; } + public int Height { get; } + + /// + /// 不是所有的失效情况都能被检测到 + /// + /// + public bool Invalid => _hWnd.IsNull || _hdcSrc.IsInvalid || _hdcDest.IsInvalid || _hBitmap.IsInvalid || _bitsPtr == 0; public BitBltSession(HWND hWnd, int w, int h) { - // 分配全空对象 避免 System.NullReferenceException - // 不能直接在上面写,不然zz编译器会报warning,感觉抑制warning也不优雅 - _oldBitmap = Gdi32.SafeHBITMAP.Null; - _hBitmap = Gdi32.SafeHBITMAP.Null; - _hdcDest = Gdi32.SafeHDC.Null; - _hdcSrc = User32.SafeReleaseHDC.Null; - _bitsPtr = IntPtr.Zero; - if (hWnd.IsNull) throw new Exception("hWnd is invalid"); _hWnd = hWnd; @@ -67,7 +68,12 @@ public class BitBltSession : IDisposable try { _hdcSrc = User32.GetDC(_hWnd); - if (_hdcSrc.IsInvalid) throw new Exception("Failed to get DC for {_hWnd}"); + if (_hdcSrc.IsInvalid) throw new Exception($"Failed to get DC for {_hWnd}"); + + var hdcRasterCaps = Gdi32.GetDeviceCaps(_hdcSrc, Gdi32.DeviceCap.RASTERCAPS); + if ((hdcRasterCaps | 0x800) == 0) // RC_STRETCHBLT + // 设备不支持 BitBlt + throw new Exception("BitBlt not supported"); var hdcSrcPixel = Gdi32.GetDeviceCaps(_hdcSrc, Gdi32.DeviceCap.BITSPIXEL); // 颜色位数 @@ -87,9 +93,14 @@ public class BitBltSession : IDisposable throw new Exception("Device does not support clipping"); _hdcDest = Gdi32.CreateCompatibleDC(_hdcSrc); + if (_hdcDest.IsInvalid) + { + Debug.Fail("Failed to create CompatibleDC"); + throw new Exception($"Failed to create CompatibleDC for {_hWnd}"); + } + var maxW = Gdi32.GetDeviceCaps(_hdcDest, Gdi32.DeviceCap.HORZRES); var maxH = Gdi32.GetDeviceCaps(_hdcDest, Gdi32.DeviceCap.VERTRES); - if (maxW <= 0 || maxH <= 0) // 显示器不见啦 throw new Exception("Can not get display size"); @@ -98,12 +109,6 @@ public class BitBltSession : IDisposable _width = Math.Min(maxW, Width); _height = Math.Min(maxH, Height); - if (_hdcSrc.IsInvalid) - { - Debug.Fail("Failed to create CompatibleDC"); - throw new Exception("Failed to create CompatibleDC for {_hWnd}"); - } - var bmi = new Gdi32.BITMAPINFO { @@ -114,21 +119,27 @@ public class BitBltSession : IDisposable biHeight = -_height, // Top-down image biPlanes = (ushort)hdcSrcPlanes, biBitCount = (ushort)(hdcSrcPixel - 8), //RGBA->RGB - biCompression = 0, // BI_RGB + biCompression = Gdi32.BitmapCompressionMode.BI_RGB, biSizeImage = 0 } }; - _hBitmap = Gdi32.CreateDIBSection(_hdcDest, bmi, Gdi32.DIBColorMode.DIB_RGB_COLORS, out var bits, + _hBitmap = Gdi32.CreateDIBSection(_hdcDest, bmi, Gdi32.DIBColorMode.DIB_RGB_COLORS, out _bitsPtr, IntPtr.Zero); - if (_hBitmap.IsInvalid || bits == 0) + if (_hBitmap.IsInvalid || _bitsPtr == 0) { if (!_hBitmap.IsInvalid) Gdi32.DeleteObject(_hBitmap); - throw new Exception($"Failed to create dIB section for {_hWnd}"); + throw new Exception($"Failed to create dIB section for {_hdcDest}"); } - _bitsPtr = bits; + var bitmap = Gdi32.GetObject(_hBitmap); + if (bitmap.bmPlanes != 1 || bitmap.bmBitsPixel != 24) + throw new Exception("Unsupported bitmap format"); + + _stride = bitmap.bmWidthBytes; + _bufferSize = bitmap.bmWidth * bitmap.bmHeight * 3; + _oldBitmap = Gdi32.SelectObject(_hdcDest, _hBitmap); if (_oldBitmap.IsNull) throw new Exception("Failed to select object"); } @@ -144,73 +155,58 @@ public class BitBltSession : IDisposable } } - //原宽高,用来喂上层 - public int Width { get; } - public int Height { get; } - - public void Dispose() { lock (_lockObject) { ReleaseResources(); - GC.SuppressFinalize(this); } + GC.SuppressFinalize(this); } - /// - /// 调用GDI复制到缓冲区并返回新Image + /// 调用GDI复制到缓冲区并返回新Mat /// - public Bitmap? GetImage() + public unsafe Mat? GetImage() { lock (_lockObject) { - Gdi32.GdiFlush(); - if (_hBitmap.IsInvalid) - // 位图指针无效 - return null; - - Gdi32.BITMAP bitmap; - try - { - // 获取位图详情,这应该能抵挡住奇奇怪怪的bug - bitmap = Gdi32.GetObject(_hBitmap); - } - catch (ArgumentException) - { - // 可能是位图已经被释放了 - return null; - } - - // 这个是bitmap的结构信息,不用手动释放 - if (bitmap.bmPlanes != 1 || bitmap.bmBitsPixel != 24) - // 不支持的位图格式 - return null; - // 直接返回转换后的位图 - var success = Gdi32.AlphaBlend(_hdcDest, 0, 0, _width, _height, _hdcSrc, 0, 0, - _width, _height, BlendFunction); + // 截图 + var success = Gdi32.StretchBlt(_hdcDest, 0, 0, _width, _height, + _hdcSrc, 0, 0, _width, _height, Gdi32.RasterOperationMode.SRCCOPY); if (!success || !Gdi32.GdiFlush()) return null; - // return Mat.FromPixelData(bitmap.bmHeight, bitmap.bmWidth, MatType.CV_8UC3, bitmap.bmBits,bitmap.bmWidthBytes); - return _hBitmap.ToBitmap(); - - // 原始宏 ((((biWidth * biBitCount) + 31) & ~31) >> 3) => (biWidth * biBitCount + 3) & ~3) (在总位数是8的倍数时,两者等价) - // 对齐 https://learn.microsoft.com/zh-cn/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader - - // 适用于32位(RGBA) - // return Gdi32.BitBlt(_hdcDest, 0, 0, Width, Height, _hdcSrc, 0, 0, Gdi32.RasterOperationMode.SRCCOPY ) ? Mat.FromPixelData(Height, Width, MatType.CV_8UC3, _bitsPtr) : null; + // 新Mat + var buffer = AcquireBuffer(); + var step = _width * 3; + if (_stride == step) + { + Buffer.MemoryCopy(_bitsPtr.ToPointer(), buffer.ToPointer(), _bufferSize, _bufferSize); + } + else + { + for (var i = 0; i < _height; i++) + { + Buffer.MemoryCopy((void*)(_bitsPtr + _stride * i), (void*)(buffer + step * i), step, step); + } + } + return BitBltMat.FromPixelData(this, _height, _width, MatType.CV_8UC3, buffer, step); } } - - /// - /// 不是所有的失效情况都能被检测到 - /// - /// - public bool IsInvalid() + private IntPtr AcquireBuffer() { - return _hWnd.IsNull || _hdcSrc.IsInvalid || _hdcDest.IsInvalid || _hBitmap.IsInvalid || _bitsPtr == IntPtr.Zero; + if (!_bufferPool.TryPop(out var buffer)) + { + buffer = Marshal.AllocHGlobal(_bufferSize); + } + + return buffer; + } + + public void ReleaseBuffer(IntPtr buffer) + { + _bufferPool.Push(buffer); } /// @@ -218,6 +214,8 @@ public class BitBltSession : IDisposable /// private void ReleaseResources() { + Gdi32.GdiFlush(); + if (!_oldBitmap.IsNull) { // 先选回资源再释放_hBitmap @@ -245,7 +243,11 @@ public class BitBltSession : IDisposable _hdcSrc = User32.SafeReleaseHDC.Null; } - Gdi32.GdiFlush(); _bitsPtr = IntPtr.Zero; + + foreach (var buffer in _bufferPool) + { + Marshal.FreeHGlobal(buffer); + } } -} \ No newline at end of file +} diff --git a/Fischless.GameCapture/CaptureImageRes.cs b/Fischless.GameCapture/CaptureImageRes.cs deleted file mode 100644 index 412de1ce..00000000 --- a/Fischless.GameCapture/CaptureImageRes.cs +++ /dev/null @@ -1,76 +0,0 @@ -using OpenCvSharp; -using OpenCvSharp.Extensions; - -namespace Fischless.GameCapture; - -/// -/// 捕获的图像 -/// -public class CaptureImageRes : IDisposable -{ - public Bitmap? Bitmap { get; set; } - public Mat? Mat { get; set; } - - public int Width => Mat?.Width ?? Bitmap?.Width ?? 0; - public int Height => Mat?.Height ?? Bitmap?.Height ?? 0; - - public CaptureImageRes(Mat mat) - { - Mat = mat; - } - - public CaptureImageRes(Bitmap bitmap) - { - Bitmap = bitmap; - } - - public static CaptureImageRes? BuildNullable(Bitmap? bitmap) - { - if (bitmap == null) - { - return null; - } - return new CaptureImageRes(bitmap); - } - - public static CaptureImageRes? BuildNullable(Mat? mat) - { - if (mat == null) - { - return null; - } - return new CaptureImageRes(mat); - } - - /// - /// 非特殊情况不要使用这个方法,会造成额外的性能消耗 - /// - /// - public Mat? ForceGetMat() - { - if (Mat == null) - { - Mat = Bitmap?.ToMat(); - } - return Mat; - } - - /// - /// 非特殊情况不要使用这个方法,会造成额外的性能消耗 - /// - /// - public Bitmap? ForceGetBitmap() - { - if (Bitmap == null) - { - Bitmap = Mat?.ToBitmap(); - } - return Bitmap; - } - - public void Dispose() - { - Bitmap?.Dispose(); - Mat?.Dispose(); - } -} \ No newline at end of file diff --git a/Fischless.GameCapture/CaptureModes.cs b/Fischless.GameCapture/CaptureModes.cs index eeade079..a383e892 100644 --- a/Fischless.GameCapture/CaptureModes.cs +++ b/Fischless.GameCapture/CaptureModes.cs @@ -4,15 +4,19 @@ namespace Fischless.GameCapture; public enum CaptureModes { - // [Description("BitBlt(稳定)")] - // BitBlt, - [Description("BitBlt")] - BitBlt, - - [Description("WindowsGraphicsCapture")] - WindowsGraphicsCapture, - + [DefaultValue(0)] + BitBlt = 0, + [Description("DwmGetDxSharedSurface")] - DwmGetDxSharedSurface + [DefaultValue(1)] + DwmGetDxSharedSurface = 2, + + [Description("WindowsGraphicsCapture")] + [DefaultValue(2)] + WindowsGraphicsCapture = 1, + + [Description("WindowsGraphicsCapture(HDR)")] + [DefaultValue(3)] + WindowsGraphicsCaptureHdr = 3, } diff --git a/Fischless.GameCapture/DwmSharedSurface/Helpers/NativeMethods.cs b/Fischless.GameCapture/DwmSharedSurface/Helpers/NativeMethods.cs deleted file mode 100644 index adfc4e44..00000000 --- a/Fischless.GameCapture/DwmSharedSurface/Helpers/NativeMethods.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Runtime.InteropServices; -using Vanara.PInvoke; - -namespace Fischless.GameCapture.DwmSharedSurface.Helpers; - -internal class NativeMethods -{ - - public delegate bool DwmGetDxSharedSurfaceDelegate(IntPtr hWnd, out IntPtr phSurface, out long pAdapterLuid, out long pFmtWindow, out long pPresentFlags, out long pWin32KUpdateId); - - public static DwmGetDxSharedSurfaceDelegate DwmGetDxSharedSurface; - - static NativeMethods() - { - var ptr = Kernel32.GetProcAddress(Kernel32.GetModuleHandle("user32"), "DwmGetDxSharedSurface"); - DwmGetDxSharedSurface = Marshal.GetDelegateForFunctionPointer(ptr); - } -} \ No newline at end of file diff --git a/Fischless.GameCapture/DwmSharedSurface/SharedSurfaceCapture.cs b/Fischless.GameCapture/DwmSharedSurface/SharedSurfaceCapture.cs index 0ca6ec43..86cbfbcf 100644 --- a/Fischless.GameCapture/DwmSharedSurface/SharedSurfaceCapture.cs +++ b/Fischless.GameCapture/DwmSharedSurface/SharedSurfaceCapture.cs @@ -1,128 +1,143 @@ -using Fischless.GameCapture.DwmSharedSurface.Helpers; -using Fischless.GameCapture.Graphics.Helpers; +using Fischless.GameCapture.Graphics.Helpers; using SharpDX.Direct3D11; -using SharpDX.DXGI; using System.Diagnostics; +using System.Runtime.InteropServices; using OpenCvSharp; -using OpenCvSharp.Extensions; +using SharpDX; using Vanara.PInvoke; using Device = SharpDX.Direct3D11.Device; -namespace Fischless.GameCapture.DwmSharedSurface +namespace Fischless.GameCapture.DwmSharedSurface; + +public partial class SharedSurfaceCapture : IGameCapture { - public class SharedSurfaceCapture : IGameCapture + // 窗口句柄 + private nint _hWnd; + + private static readonly object LockObject = new(); + + // D3D 设备 + private Device? _d3dDevice; + + // 截图区域 + private ResourceRegion? _region; + + // 暂存贴图 + private Texture2D? _stagingTexture; + + // Surface 大小 + private int _surfaceWidth; + private int _surfaceHeight; + + public bool IsCapturing { get; private set; } + + [LibraryImport("user32.dll", EntryPoint = "DwmGetDxSharedSurface", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool DwmGetDxSharedSurface(IntPtr hWnd, out IntPtr phSurface, out long pAdapterLuid, out long pFmtWindow, out long pPresentFlags, out long pWin32KUpdateId); + + public void Dispose() { - private nint _hWnd; - private static readonly object LockObject = new object(); - private Device? _d3dDevice; + Stop(); + GC.SuppressFinalize(this); + } - public void Dispose() => Stop(); + public void Start(nint hWnd, Dictionary? settings = null) + { + _hWnd = hWnd; + User32.ShowWindow(hWnd, ShowWindowCommand.SW_RESTORE); + _region = GetGameScreenRegion(hWnd); + _d3dDevice = new Device(SharpDX.Direct3D.DriverType.Hardware, DeviceCreationFlags.BgraSupport); // Software/Hardware - public bool IsCapturing { get; private set; } + IsCapturing = true; + } - private ResourceRegion? _region; - - - public void Start(nint hWnd, Dictionary? settings = null) + /// + /// 从 GetWindowRect 的带窗口阴影面积矩形 截取出 GetClientRect的矩形(游戏区域) + /// + /// + /// + private ResourceRegion? GetGameScreenRegion(nint hWnd) + { + var exStyle = User32.GetWindowLong(hWnd, User32.WindowLongFlags.GWL_EXSTYLE); + if ((exStyle & (int)User32.WindowStylesEx.WS_EX_TOPMOST) != 0) { - _hWnd = hWnd; - User32.ShowWindow(hWnd, ShowWindowCommand.SW_RESTORE); - User32.SetForegroundWindow(hWnd); - _region = GetGameScreenRegion(hWnd); - _d3dDevice = new Device(SharpDX.Direct3D.DriverType.Hardware, DeviceCreationFlags.BgraSupport); // Software/Hardware - - //var device = Direct3D11Helper.CreateDevice(); - //_d3dDevice = Direct3D11Helper.CreateSharpDXDevice(device); - - IsCapturing = true; + return null; } - /// - /// 从 GetWindowRect 的带窗口阴影面积矩形 截取出 GetClientRect的矩形(游戏区域) - /// - /// - /// - private ResourceRegion? GetGameScreenRegion(nint hWnd) + ResourceRegion region = new(); + User32.GetWindowRect(hWnd, out var windowWithShadowRect); + DwmApi.DwmGetWindowAttribute(hWnd, DwmApi.DWMWINDOWATTRIBUTE.DWMWA_EXTENDED_FRAME_BOUNDS, out var windowRect); + User32.GetClientRect(_hWnd, out var clientRect); + + region.Left = windowRect.Left - windowWithShadowRect.Left; + // 标题栏 windowRect.Height - clientRect.Height 上阴影 windowRect.Top - windowWithShadowRect.Top + region.Top = windowRect.Height - clientRect.Height + windowRect.Top - windowWithShadowRect.Top; + region.Right = region.Left + clientRect.Width; + region.Bottom = region.Top + clientRect.Height; + region.Front = 0; + region.Back = 1; + + return region; + } + + public Mat? Capture() + { + lock (LockObject) { - var exStyle = User32.GetWindowLong(hWnd, User32.WindowLongFlags.GWL_EXSTYLE); - if ((exStyle & (int)User32.WindowStylesEx.WS_EX_TOPMOST) != 0) + if (_d3dDevice == null) + { + Debug.WriteLine("D3Device is null."); + return null; + } + + if (!DwmGetDxSharedSurface(_hWnd, out var phSurface, out _, out _, out _, out _)) + { + return null; + } + if (phSurface == 0) { return null; } - ResourceRegion region = new(); - User32.GetWindowRect(hWnd, out var windowWithShadowRect); - DwmApi.DwmGetWindowAttribute(hWnd, DwmApi.DWMWINDOWATTRIBUTE.DWMWA_EXTENDED_FRAME_BOUNDS, out var windowRect); - User32.GetClientRect(_hWnd, out var clientRect); - - region.Left = windowRect.Left - windowWithShadowRect.Left; - // 标题栏 windowRect.Height - clientRect.Height 上阴影 windowRect.Top - windowWithShadowRect.Top - region.Top = windowRect.Height - clientRect.Height + windowRect.Top - windowWithShadowRect.Top; - region.Right = region.Left + clientRect.Width; - region.Bottom = region.Top + clientRect.Height; - region.Front = 0; - region.Back = 1; - - return region; - } - - public CaptureImageRes? Capture() - { - if (_hWnd == nint.Zero) + try { - return null; - } - - lock (LockObject) - { - NativeMethods.DwmGetDxSharedSurface(_hWnd, out var phSurface, out _, out _, out _, out _); - if (phSurface == nint.Zero) - { - return null; - } - - - if (_d3dDevice == null) - { - Debug.WriteLine("D3Device is null."); - return null; - } - using var surfaceTexture = _d3dDevice.OpenSharedResource(phSurface); - using var stagingTexture = CreateStagingTexture(surfaceTexture, _d3dDevice); - var mat = stagingTexture.CreateMat(_d3dDevice, surfaceTexture, _region); - if (mat == null) + + if (_stagingTexture == null || _surfaceWidth != surfaceTexture.Description.Width || + _surfaceHeight != surfaceTexture.Description.Height) { - return null; + _stagingTexture?.Dispose(); + _stagingTexture = null; + _surfaceWidth = surfaceTexture.Description.Width; + _surfaceHeight = surfaceTexture.Description.Height; + _region = GetGameScreenRegion(_hWnd); } - var bgrMat = new Mat(); - Cv2.CvtColor(mat, bgrMat, ColorConversionCodes.BGRA2BGR); - return CaptureImageRes.BuildNullable(bgrMat); + + _stagingTexture ??= Direct3D11Helper.CreateStagingTexture(_d3dDevice, _surfaceWidth, _surfaceHeight, _region); + var mat = _stagingTexture.CreateMat(_d3dDevice, surfaceTexture, _region); + return mat; } - } - - - private Texture2D CreateStagingTexture(Texture2D surfaceTexture, Device device) - { - return new Texture2D(device, new Texture2DDescription + catch (SharpDXException e) { - Width = _region == null ? surfaceTexture.Description.Width : _region.Value.Right - _region.Value.Left, - Height = _region == null ? surfaceTexture.Description.Height : _region.Value.Bottom - _region.Value.Top, - MipLevels = 1, - ArraySize = 1, - Format = Format.B8G8R8A8_UNorm, - Usage = ResourceUsage.Staging, - SampleDescription = new SampleDescription(1, 0), - BindFlags = BindFlags.None, - CpuAccessFlags = CpuAccessFlags.Read, - OptionFlags = ResourceOptionFlags.None - }); - } + Debug.WriteLine($"SharpDXException: {e.Descriptor}"); + _d3dDevice?.Dispose(); + _d3dDevice = new Device(SharpDX.Direct3D.DriverType.Hardware, DeviceCreationFlags.BgraSupport); + } - public void Stop() + return null; + } + } + + public void Stop() + { + lock (LockObject) { - _hWnd = nint.Zero; + _stagingTexture?.Dispose(); + _stagingTexture = null; + _d3dDevice?.Dispose(); + _d3dDevice = null; + _hWnd = 0; IsCapturing = false; } } -} \ No newline at end of file +} diff --git a/Fischless.GameCapture/Fischless.GameCapture.csproj b/Fischless.GameCapture/Fischless.GameCapture.csproj index aba49767..2e118352 100644 --- a/Fischless.GameCapture/Fischless.GameCapture.csproj +++ b/Fischless.GameCapture/Fischless.GameCapture.csproj @@ -12,6 +12,7 @@ + diff --git a/Fischless.GameCapture/GameCaptureFactory.cs b/Fischless.GameCapture/GameCaptureFactory.cs index 5e3baa3a..b330a35d 100644 --- a/Fischless.GameCapture/GameCaptureFactory.cs +++ b/Fischless.GameCapture/GameCaptureFactory.cs @@ -12,9 +12,9 @@ public class GameCaptureFactory return mode switch { CaptureModes.BitBlt => new BitBlt.BitBltCapture(), - // CaptureModes.BitBlt => new BitBlt.BitBltOldCapture(), - CaptureModes.WindowsGraphicsCapture => new Graphics.GraphicsCapture(), CaptureModes.DwmGetDxSharedSurface => new DwmSharedSurface.SharedSurfaceCapture(), + CaptureModes.WindowsGraphicsCapture => new Graphics.GraphicsCapture(), + CaptureModes.WindowsGraphicsCaptureHdr => new Graphics.GraphicsCapture(true), _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null), }; } diff --git a/Fischless.GameCapture/Graphics/GraphicsCapture.cs b/Fischless.GameCapture/Graphics/GraphicsCapture.cs index 8cb1b33f..2a003429 100644 --- a/Fischless.GameCapture/Graphics/GraphicsCapture.cs +++ b/Fischless.GameCapture/Graphics/GraphicsCapture.cs @@ -7,31 +7,31 @@ using Windows.Graphics.Capture; using Windows.Graphics.DirectX; using Windows.Graphics.DirectX.Direct3D11; using OpenCvSharp; -using SharpDX.DXGI; -using Device = SharpDX.Direct3D11.Device; -using MapFlags = SharpDX.Direct3D11.MapFlags; +using SharpDX; +using SharpDX.D3DCompiler; namespace Fischless.GameCapture.Graphics; -public class GraphicsCapture : IGameCapture +public class GraphicsCapture(bool captureHdr = false) : IGameCapture { private nint _hWnd; - private Direct3D11CaptureFramePool _captureFramePool = null!; - private GraphicsCaptureItem _captureItem = null!; + private Direct3D11CaptureFramePool? _captureFramePool; + private GraphicsCaptureItem? _captureItem; - private GraphicsCaptureSession _captureSession = null!; + private GraphicsCaptureSession? _captureSession; - private IDirect3DDevice _d3dDevice = null!; + private IDirect3DDevice? _d3dDevice; public bool IsCapturing { get; private set; } private ResourceRegion? _region; // HDR相关 - private bool _isHdrEnabled; - private DirectXPixelFormat _pixelFormat = DirectXPixelFormat.B8G8R8A8UIntNormalized; - + private bool _isHdrEnabled = captureHdr; + private DirectXPixelFormat _pixelFormat; + private Texture2D? _hdrOutputTexture; + private ComputeShader? _hdrComputeShader; // 最新帧的存储 private Mat? _latestFrame; @@ -40,11 +40,15 @@ public class GraphicsCapture : IGameCapture // 用于获取帧数据的临时纹理和暂存资源 private Texture2D? _stagingTexture; - private long _lastFrameTime = 0; + private long _lastFrameTime; - private readonly Stopwatch _frameTimer = new Stopwatch(); + private readonly Stopwatch _frameTimer = new(); - public void Dispose() => Stop(); + public void Dispose() + { + Stop(); + GC.SuppressFinalize(this); + } public void Start(nint hWnd, Dictionary? settings = null) { @@ -65,16 +69,33 @@ public class GraphicsCapture : IGameCapture // 创建D3D设备 _d3dDevice = Direct3D11Helper.CreateDevice(); - // 检测HDR状态 - _isHdrEnabled = false; - _pixelFormat = _isHdrEnabled ? DirectXPixelFormat.R16G16B16A16Float : DirectXPixelFormat.B8G8R8A8UIntNormalized; - // 创建帧池 - _captureFramePool = Direct3D11CaptureFramePool.Create( - _d3dDevice, - _pixelFormat, - 2, - _captureItem.Size); + try + { + if (!_isHdrEnabled) + { + // 不处理 HDR,直接抛异常走 SDR 分支 + throw new Exception(); + } + + _pixelFormat = DirectXPixelFormat.R16G16B16A16Float; + _captureFramePool = Direct3D11CaptureFramePool.CreateFreeThreaded( + _d3dDevice, + _pixelFormat, + 2, + _captureItem.Size); + } + catch (Exception) + { + // Fallback + _pixelFormat = DirectXPixelFormat.B8G8R8A8UIntNormalized; + _captureFramePool = Direct3D11CaptureFramePool.CreateFreeThreaded( + _d3dDevice, + _pixelFormat, + 2, + _captureItem.Size); + _isHdrEnabled = false; + } _captureItem.Closed += CaptureItemOnClosed; @@ -82,13 +103,13 @@ public class GraphicsCapture : IGameCapture _captureSession = _captureFramePool.CreateCaptureSession(_captureItem); if (ApiInformation.IsPropertyPresent("Windows.Graphics.Capture.GraphicsCaptureSession", - "IsCursorCaptureEnabled")) + nameof(GraphicsCaptureSession.IsCursorCaptureEnabled))) { _captureSession.IsCursorCaptureEnabled = false; } if (ApiInformation.IsWriteablePropertyPresent("Windows.Graphics.Capture.GraphicsCaptureSession", - "IsBorderRequired")) + nameof(GraphicsCaptureSession.IsBorderRequired))) { _captureSession.IsBorderRequired = false; } @@ -111,186 +132,124 @@ public class GraphicsCapture : IGameCapture return null; } - ResourceRegion region = new(); DwmApi.DwmGetWindowAttribute(hWnd, DwmApi.DWMWINDOWATTRIBUTE.DWMWA_EXTENDED_FRAME_BOUNDS, out var windowRect); User32.GetClientRect(_hWnd, out var clientRect); - //POINT point = default; // 这个点和 DwmGetWindowAttribute 结果差1 - //User32.ClientToScreen(hWnd, ref point); + POINT point = default; + User32.ClientToScreen(_hWnd, ref point); - region.Left = 0; - region.Top = windowRect.Height - clientRect.Height; - region.Right = clientRect.Width; - region.Bottom = windowRect.Height; + region.Left = point.X > windowRect.Left ? point.X - windowRect.Left : 0; + region.Top = point.Y > windowRect.Top ? point.Y - windowRect.Top : 0; + region.Right = region.Left + clientRect.Width; + region.Bottom = region.Top + clientRect.Height; region.Front = 0; region.Back = 1; return region; } - public static bool IsHdrEnabled(nint hWnd) + private Texture2D ProcessHdrTexture(Texture2D hdrTexture) { - try - { - var hdc = User32.GetDC(hWnd); - if (hdc != IntPtr.Zero) - { - int bitsPerPixel = Gdi32.GetDeviceCaps(hdc, Gdi32.DeviceCap.BITSPIXEL); - User32.ReleaseDC(hWnd, hdc); + var device = hdrTexture.Device; + var context = device.ImmediateContext; - // 如果位深大于等于32位,认为支持HDR - return bitsPerPixel >= 32; - } + var width = hdrTexture.Description.Width; + var height = hdrTexture.Description.Height; - return false; - } - catch - { - return false; - } - } + _hdrOutputTexture ??= Direct3D11Helper.CreateOutputTexture(device, width, height); + _hdrComputeShader ??= new ComputeShader(device, ShaderBytecode.Compile(HdrToSdrShader.Content, "CS_HDRtoSDR", "cs_5_0")); - private Texture2D CreateStagingTexture(Direct3D11CaptureFrame frame, Device device) - { - // 创建可以用于CPU读取的暂存纹理 - var textureDesc = new Texture2DDescription - { - CpuAccessFlags = CpuAccessFlags.Read, - BindFlags = BindFlags.None, - Format = _isHdrEnabled ? Format.R16G16B16A16_Float : Format.B8G8R8A8_UNorm, - Width = _region == null ? frame.ContentSize.Width : _region.Value.Right - _region.Value.Left, - Height = _region == null ? frame.ContentSize.Height : _region.Value.Bottom - _region.Value.Top, - OptionFlags = ResourceOptionFlags.None, - MipLevels = 1, - ArraySize = 1, - SampleDescription = { Count = 1, Quality = 0 }, - Usage = ResourceUsage.Staging - }; + using var inputSrv = new ShaderResourceView(device, hdrTexture); + using var outputUav = new UnorderedAccessView(device, _hdrOutputTexture); - return new Texture2D(device, textureDesc); + context.ComputeShader.Set(_hdrComputeShader); + context.ComputeShader.SetShaderResource(0, inputSrv); + context.ComputeShader.SetUnorderedAccessView(0, outputUav); + + var threadGroupCountX = (int)Math.Ceiling(width / 16.0); + var threadGroupCountY = (int)Math.Ceiling(height / 16.0); + + context.Dispatch(threadGroupCountX, threadGroupCountY, 1); + + return _hdrOutputTexture; } private void OnFrameArrived(Direct3D11CaptureFramePool sender, object args) { - using var frame = sender.TryGetNextFrame(); - if (frame == null) - { - return; - } - - // 限制最高处理帧率为62fps - if (_frameTimer.ElapsedMilliseconds - _lastFrameTime < 16) - { - return; - } - _lastFrameTime = _frameTimer.ElapsedMilliseconds; - - var frameSize = _captureItem.Size; - - // 检查帧大小是否变化 // 不会被访问到的代码 - if (frameSize.Width != frame.ContentSize.Width || - frameSize.Height != frame.ContentSize.Height) - { - frameSize = frame.ContentSize; - _captureFramePool.Recreate( - _d3dDevice, - _pixelFormat, - 2, - frameSize - ); - _stagingTexture = null; - } - - // 从捕获的帧创建一个可以被访问的纹理 - using var surfaceTexture = Direct3D11Helper.CreateSharpDXTexture2D(frame.Surface); - var d3dDevice = surfaceTexture.Device; - - _stagingTexture ??= CreateStagingTexture(frame, d3dDevice); - var stagingTexture = _stagingTexture; - - // 将捕获的纹理复制到暂存纹理 - if (_region != null) - { - d3dDevice.ImmediateContext.CopySubresourceRegion(surfaceTexture, 0, _region, stagingTexture, 0); - } - else - { - d3dDevice.ImmediateContext.CopyResource(surfaceTexture, stagingTexture); - } - - - // 映射纹理以便CPU读取 - var dataBox = d3dDevice.ImmediateContext.MapSubresource( - stagingTexture, - 0, - MapMode.Read, - MapFlags.None); - + // 使用写锁更新最新帧 + _frameAccessLock.EnterWriteLock(); try { - // 创建一个新的Mat - var newFrame = Mat.FromPixelData(stagingTexture.Description.Height, stagingTexture.Description.Width, - _isHdrEnabled ? MatType.MakeType(7, 4) : MatType.CV_8UC4, dataBox.DataPointer); - - // 如果是HDR,进行HDR到SDR的转换 - if (_isHdrEnabled) + if (_hWnd == 0) { - // rgb -> bgr - newFrame = ConvertHdrToSdr(newFrame); + return; + } + + using var frame = sender.TryGetNextFrame(); + if (frame == null) + { + return; + } + + // 限制最高处理帧率为62fps + if (_frameTimer.ElapsedMilliseconds - _lastFrameTime < 16) + { + return; + } + _lastFrameTime = _frameTimer.ElapsedMilliseconds; + + var frameSize = _captureItem!.Size; + + // 检查帧大小是否变化 + if (frameSize.Width != frame.ContentSize.Width || frameSize.Height != frame.ContentSize.Height) + { + frameSize = frame.ContentSize; + _captureFramePool!.Recreate( + _d3dDevice, + _pixelFormat, + 2, + frameSize + ); + _stagingTexture?.Dispose(); + _stagingTexture = null; + _region = GetGameScreenRegion(_hWnd); } - // 使用写锁更新最新帧 - _frameAccessLock.EnterWriteLock(); try { - // 释放之前的帧 + // 从捕获的帧创建一个可以被访问的纹理 + using var surfaceTexture = Direct3D11Helper.CreateSharpDXTexture2D(frame.Surface); + var sourceTexture = _isHdrEnabled ? ProcessHdrTexture(surfaceTexture) : surfaceTexture; + var d3dDevice = surfaceTexture.Device; + + _stagingTexture ??= Direct3D11Helper.CreateStagingTexture(d3dDevice, frame.ContentSize.Width, frame.ContentSize.Height, _region); + var mat = _stagingTexture.CreateMat(d3dDevice, sourceTexture, _region); + + // 释放之前的帧,然后更新 _latestFrame?.Dispose(); - // 克隆新帧以保持对其的引用(因为dataBox.DataPointer将被释放) - _latestFrame = newFrame.Clone(); + _latestFrame = mat; } - finally + catch (SharpDXException e) { - newFrame.Dispose(); - _frameAccessLock.ExitWriteLock(); + Debug.WriteLine($"SharpDXException: {e.Descriptor}"); + _latestFrame = null; } } finally { - // 取消映射纹理 - d3dDevice.ImmediateContext.UnmapSubresource(surfaceTexture, 0); + _frameAccessLock.ExitWriteLock(); } } - private static Mat ConvertHdrToSdr(Mat hdrMat) - { - // 创建一个目标 8UC4 Mat - Mat sdkMat = new Mat(hdrMat.Size(), MatType.CV_8UC4); - - // 将 32FC4 缩放到 0-255 范围并转换为 8UC4 - // 注意:这种简单缩放可能不会保留 HDR 的所有细节 - hdrMat.ConvertTo(sdkMat, MatType.CV_8UC4, 255.0); - - // 将 HDR 的 RGB 通道转换为 BGR - Cv2.CvtColor(sdkMat, sdkMat, ColorConversionCodes.RGBA2BGRA); - - return sdkMat; - } - - public CaptureImageRes? Capture() + public Mat? Capture() { // 使用读锁获取最新帧 _frameAccessLock.EnterReadLock(); try { - // 如果没有可用帧则返回null - if (_latestFrame == null) - { - return null; - } - // 返回最新帧的副本(这里我们必须克隆,因为Mat是不线程安全的) - return CaptureImageRes.BuildNullable(_latestFrame.Clone()); + return _latestFrame?.Clone(); } finally { @@ -300,20 +259,33 @@ public class GraphicsCapture : IGameCapture public void Stop() { - _captureSession?.Dispose(); - _captureFramePool?.Dispose(); - _captureSession = null!; - _captureFramePool = null!; - _captureItem = null!; - _stagingTexture?.Dispose(); - _d3dDevice?.Dispose(); - - _hWnd = IntPtr.Zero; - IsCapturing = false; + _frameAccessLock.EnterWriteLock(); + try + { + _captureSession?.Dispose(); + _captureSession = null; + _captureFramePool?.Dispose(); + _captureFramePool = null; + _captureItem = null; + _stagingTexture?.Dispose(); + _stagingTexture = null; + _hdrOutputTexture?.Dispose(); + _hdrOutputTexture = null; + _hdrComputeShader?.Dispose(); + _hdrComputeShader = null; + _d3dDevice?.Dispose(); + _d3dDevice = null; + _hWnd = 0; + IsCapturing = false; + } + finally + { + _frameAccessLock.ExitWriteLock(); + } } private void CaptureItemOnClosed(GraphicsCaptureItem sender, object args) { Stop(); } -} \ No newline at end of file +} diff --git a/Fischless.GameCapture/Graphics/Helpers/Direct3D11Helper.cs b/Fischless.GameCapture/Graphics/Helpers/Direct3D11Helper.cs index ca1f0449..98be8218 100644 --- a/Fischless.GameCapture/Graphics/Helpers/Direct3D11Helper.cs +++ b/Fischless.GameCapture/Graphics/Helpers/Direct3D11Helper.cs @@ -1,15 +1,14 @@ using System.Runtime.InteropServices; using Windows.Graphics.DirectX.Direct3D11; +using SharpDX.Direct3D11; +using SharpDX.DXGI; using WinRT; +using Device = SharpDX.Direct3D11.Device; namespace Fischless.GameCapture.Graphics.Helpers; public static class Direct3D11Helper { - internal static Guid IInspectable = new("AF86E2E0-B12D-4c6a-9C5A-D7AA65101E90"); - internal static Guid ID3D11Resource = new("dc8e63f3-d12b-4952-b47b-5e45026a862d"); - internal static Guid IDXGIAdapter3 = new("645967A4-1392-4310-A798-8053CE3E93FD"); - internal static Guid ID3D11Device = new("db6f6ddb-ac77-4e88-8253-819df9bbf140"); internal static Guid ID3D11Texture2D = new("6f15aaf2-d208-4e89-9ab4-489535d34f9c"); [ComImport] @@ -31,33 +30,23 @@ public static class Direct3D11Helper )] static extern uint CreateDirect3D11DeviceFromDXGIDevice(nint dxgiDevice, out nint graphicsDevice); - [DllImport( - "d3d11.dll", - EntryPoint = "CreateDirect3D11SurfaceFromDXGISurface", - SetLastError = true, - CharSet = CharSet.Unicode, - ExactSpelling = true, - CallingConvention = CallingConvention.StdCall - )] - static extern uint CreateDirect3D11SurfaceFromDXGISurface(nint dxgiSurface, out nint graphicsSurface); - public static IDirect3DDevice CreateDevice() { return CreateDevice(false); } - private static SharpDX.Direct3D11.Device? d3dDevice; + private static Device? d3dDevice; public static IDirect3DDevice CreateDevice(bool useWARP) { - d3dDevice ??= new SharpDX.Direct3D11.Device( + d3dDevice ??= new Device( useWARP ? SharpDX.Direct3D.DriverType.Software : SharpDX.Direct3D.DriverType.Hardware, - SharpDX.Direct3D11.DeviceCreationFlags.BgraSupport); + DeviceCreationFlags.BgraSupport); var device = CreateDirect3DDeviceFromSharpDXDevice(d3dDevice); return device; } - public static IDirect3DDevice CreateDirect3DDeviceFromSharpDXDevice(SharpDX.Direct3D11.Device d3dDevice) + public static IDirect3DDevice CreateDirect3DDeviceFromSharpDXDevice(Device d3dDevice) { IDirect3DDevice device = default!; @@ -77,39 +66,45 @@ public static class Direct3D11Helper return device; } - public static IDirect3DSurface CreateDirect3DSurfaceFromSharpDXTexture(SharpDX.Direct3D11.Texture2D texture) - { - IDirect3DSurface surface = default!; - - // Acquire the DXGI interface for the Direct3D surface. - using (var dxgiSurface = texture.QueryInterface()) - { - // Wrap the native device using a WinRT interop object. - uint hr = CreateDirect3D11SurfaceFromDXGISurface(dxgiSurface.NativePointer, out nint pUnknown); - - if (hr == 0) - { - surface = Marshal.GetObjectForIUnknown(pUnknown) as IDirect3DSurface; - Marshal.Release(pUnknown); - } - } - - return surface; - } - - public static SharpDX.Direct3D11.Device CreateSharpDXDevice(IDirect3DDevice device) - { - var access = device.As(); - var d3dPointer = access.GetInterface(ID3D11Device); - var d3dDevice = new SharpDX.Direct3D11.Device(d3dPointer); - return d3dDevice; - } - - public static SharpDX.Direct3D11.Texture2D CreateSharpDXTexture2D(IDirect3DSurface surface) + public static Texture2D CreateSharpDXTexture2D(IDirect3DSurface surface) { var access = surface.As(); var d3dPointer = access.GetInterface(ID3D11Texture2D); - var d3dSurface = new SharpDX.Direct3D11.Texture2D(d3dPointer); + var d3dSurface = new Texture2D(d3dPointer); return d3dSurface; } + + public static Texture2D CreateStagingTexture(Device device, int width, int height, ResourceRegion? region) + { + return new Texture2D(device, new Texture2DDescription + { + Width = region == null ? width : region.Value.Right - region.Value.Left, + Height = region == null ? height : region.Value.Bottom - region.Value.Top, + MipLevels = 1, + ArraySize = 1, + Format = Format.B8G8R8A8_UNorm, + Usage = ResourceUsage.Staging, + SampleDescription = new SampleDescription(1, 0), + BindFlags = BindFlags.None, + CpuAccessFlags = CpuAccessFlags.Read, + OptionFlags = ResourceOptionFlags.None + }); + } + + public static Texture2D CreateOutputTexture(Device device, int width, int height) + { + return new Texture2D(device, new Texture2DDescription + { + Width = width, + Height = height, + MipLevels = 1, + ArraySize = 1, + Format = Format.B8G8R8A8_UNorm, + Usage = ResourceUsage.Default, + SampleDescription = new SampleDescription(1, 0), + BindFlags = BindFlags.UnorderedAccess | BindFlags.ShaderResource, + CpuAccessFlags = CpuAccessFlags.None, + OptionFlags = ResourceOptionFlags.None + }); + } } diff --git a/Fischless.GameCapture/Graphics/Helpers/HdrToSdrShader.cs b/Fischless.GameCapture/Graphics/Helpers/HdrToSdrShader.cs new file mode 100644 index 00000000..410a9399 --- /dev/null +++ b/Fischless.GameCapture/Graphics/Helpers/HdrToSdrShader.cs @@ -0,0 +1,27 @@ +namespace Fischless.GameCapture.Graphics.Helpers; + +public static class HdrToSdrShader +{ + public static string Content => +""" +// HLSL Compute Shader +Texture2D hdrTexture : register(t0); +RWTexture2D sdrTexture : register(u0); + +[numthreads(16, 16, 1)] +void CS_HDRtoSDR(uint3 id : SV_DispatchThreadID) +{ + // Load color + half4 hdrColor = hdrTexture[id.xy]; + + // HDR -> SDR (exposure) + float4 exposedColor = float4(hdrColor.rgb * 0.25, hdrColor.a); + + // Linear RGB -> sRGB + float4 srgbColor = float4(pow(exposedColor.rgb, 1 / 2.2), exposedColor.a); + + // Store color + sdrTexture[id.xy] = (unorm float4)saturate(srgbColor); +} +"""; +} diff --git a/Fischless.GameCapture/Graphics/Helpers/Texture2DExtensions.cs b/Fischless.GameCapture/Graphics/Helpers/Texture2DExtensions.cs index 2957a74b..ab932c97 100644 --- a/Fischless.GameCapture/Graphics/Helpers/Texture2DExtensions.cs +++ b/Fischless.GameCapture/Graphics/Helpers/Texture2DExtensions.cs @@ -1,75 +1,12 @@ -using SharpDX; -using SharpDX.Direct3D11; -using SharpDX.DXGI; +using SharpDX.Direct3D11; using System.Diagnostics; -using System.Drawing.Imaging; -using Windows.Graphics.Capture; using OpenCvSharp; namespace Fischless.GameCapture.Graphics.Helpers; public static class Texture2DExtensions { - public static Bitmap? ToBitmap(this Direct3D11CaptureFrame frame, ResourceRegion? region = null) - { - var texture2dBitmap = Direct3D11Helper.CreateSharpDXTexture2D(frame.Surface); - - var d3dDevice = texture2dBitmap.Device; - - // Create texture copy - var staging = new Texture2D(d3dDevice, new Texture2DDescription - { - Width = region == null ? frame.ContentSize.Width : region.Value.Right - region.Value.Left, - Height = region == null ? frame.ContentSize.Height : region.Value.Bottom - region.Value.Top, - MipLevels = 1, - ArraySize = 1, - Format = texture2dBitmap.Description.Format, - Usage = ResourceUsage.Staging, - SampleDescription = new SampleDescription(1, 0), - BindFlags = BindFlags.None, - CpuAccessFlags = CpuAccessFlags.Read, - OptionFlags = ResourceOptionFlags.None - }); - - return staging.CreateBitmap(d3dDevice, texture2dBitmap, region); - } - - public static Bitmap? CreateBitmap(this Texture2D staging, SharpDX.Direct3D11.Device d3dDevice, Texture2D surfaceTexture, ResourceRegion? region = null) - { - try - { - // Copy data - if (region != null) - { - d3dDevice.ImmediateContext.CopySubresourceRegion(surfaceTexture, 0, region, staging, 0); - } - else - { - d3dDevice.ImmediateContext.CopyResource(surfaceTexture, staging); - } - - var dataBox = d3dDevice.ImmediateContext.MapSubresource(staging, 0, 0, MapMode.Read, - SharpDX.Direct3D11.MapFlags.None, - out DataStream stream); - - var bitmap = new Bitmap(staging.Description.Width, staging.Description.Height, dataBox.RowPitch, - PixelFormat.Format32bppArgb, dataBox.DataPointer); - - return bitmap; - } - catch (Exception e) - { - Debug.WriteLine("Failed to copy texture to bitmap."); - Debug.WriteLine(e.StackTrace); - return null; - } - finally - { - staging.Dispose(); - } - } - - public static Mat? CreateMat(this Texture2D staging, SharpDX.Direct3D11.Device d3dDevice, Texture2D surfaceTexture, ResourceRegion? region = null) + public static Mat? CreateMat(this Texture2D staging, Device d3dDevice, Texture2D surfaceTexture, ResourceRegion? region = null) { try { @@ -88,10 +25,18 @@ public static class Texture2DExtensions staging, 0, MapMode.Read, - SharpDX.Direct3D11.MapFlags.None); + MapFlags.None); - var mat = Mat.FromPixelData(staging.Description.Height, staging.Description.Width, MatType.CV_8UC4, dataBox.DataPointer); - return mat; + try + { + using var mat = Mat.FromPixelData(staging.Description.Height, staging.Description.Width, + MatType.CV_8UC4, dataBox.DataPointer, dataBox.RowPitch); + return mat.CvtColor(ColorConversionCodes.BGRA2BGR); + } + finally + { + d3dDevice.ImmediateContext.UnmapSubresource(staging, 0); + } } catch (Exception e) { @@ -99,9 +44,5 @@ public static class Texture2DExtensions Debug.WriteLine(e.StackTrace); return null; } - finally - { - staging.Dispose(); - } } -} \ No newline at end of file +} diff --git a/Fischless.GameCapture/IGameCapture.cs b/Fischless.GameCapture/IGameCapture.cs index 07dbf145..c2783fd9 100644 --- a/Fischless.GameCapture/IGameCapture.cs +++ b/Fischless.GameCapture/IGameCapture.cs @@ -8,7 +8,7 @@ public interface IGameCapture : IDisposable public void Start(nint hWnd, Dictionary? settings = null); - public CaptureImageRes? Capture(); + public Mat? Capture(); public void Stop(); }