From 2667f84c429ffcd9c071149fff7e8ca35207ce08 Mon Sep 17 00:00:00 2001 From: Darth Affe Date: Thu, 7 Sep 2023 23:59:52 +0200 Subject: [PATCH] Added DX9 capture --- ScreenCapture.NET.DX9/DX9ScreenCapture.cs | 218 ++++++++++++++++++ .../DX9ScreenCaptureService.cs | 97 ++++++++ .../Downscale/AverageByteSampler.cs | 149 ++++++++++++ .../Downscale/SamplerInfo.cs | 63 +++++ ScreenCapture.NET.DX9/Resources/icon.png | Bin 0 -> 705 bytes .../ScreenCapture.NET.DX9.csproj | 75 ++++++ ScreenCapture.NET.sln | 8 +- .../ScreenCapture.NET.Tests.csproj | 11 +- 8 files changed, 616 insertions(+), 5 deletions(-) create mode 100644 ScreenCapture.NET.DX9/DX9ScreenCapture.cs create mode 100644 ScreenCapture.NET.DX9/DX9ScreenCaptureService.cs create mode 100644 ScreenCapture.NET.DX9/Downscale/AverageByteSampler.cs create mode 100644 ScreenCapture.NET.DX9/Downscale/SamplerInfo.cs create mode 100644 ScreenCapture.NET.DX9/Resources/icon.png create mode 100644 ScreenCapture.NET.DX9/ScreenCapture.NET.DX9.csproj diff --git a/ScreenCapture.NET.DX9/DX9ScreenCapture.cs b/ScreenCapture.NET.DX9/DX9ScreenCapture.cs new file mode 100644 index 0000000..bf057b6 --- /dev/null +++ b/ScreenCapture.NET.DX9/DX9ScreenCapture.cs @@ -0,0 +1,218 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using ScreenCapture.NET.Downscale; +using SharpGen.Runtime; +using Vortice.Direct3D9; + +namespace ScreenCapture.NET; + +/// +/// Represents a ScreenCapture using DirectX 11 desktop duplicaton. +/// https://docs.microsoft.com/en-us/windows/win32/direct3ddxgi/desktop-dup-api +/// +// ReSharper disable once InconsistentNaming +public sealed class DX9ScreenCapture : AbstractScreenCapture +{ + #region Properties & Fields + + private readonly object _captureLock = new(); + + private readonly IDirect3D9 _direct3D9; + private IDirect3DDevice9? _device; + private IDirect3DSurface9? _surface; + private byte[]? _buffer; + private int _stride; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The to duplicate. + public DX9ScreenCapture(IDirect3D9 direct3D9, Display display) + : base(display) + { + this._direct3D9 = direct3D9; + + Restart(); + } + + #endregion + + #region Methods + + protected override bool PerformScreenCapture() + { + bool result = false; + lock (_captureLock) + { + if ((_device == null) || (_surface == null) || (_buffer == null)) + { + Restart(); + return false; + } + + try + { + _device.GetFrontBufferData(0, _surface); + + LockedRectangle dr = _surface.LockRect(LockFlags.NoSystemLock | LockFlags.ReadOnly); + + nint ptr = dr.DataPointer; + for (int y = 0; y < Display.Height; y++) + { + Marshal.Copy(ptr, _buffer, y * _stride, _stride); + ptr += dr.Pitch; + } + + _surface.UnlockRect(); + + result = true; + } + catch (SharpGenException dxException) + { + if (dxException.ResultCode == Result.AccessDenied) + { + try + { + Restart(); + } + catch { Thread.Sleep(100); } + } + } + } + + return result; + } + + protected override void PerformCaptureZoneUpdate(CaptureZone captureZone, in Span buffer) + { + if (_buffer == null) return; + + using IDisposable @lock = captureZone.Lock(); + { + if (captureZone.DownscaleLevel == 0) + CopyZone(captureZone, buffer); + else + DownscaleZone(captureZone, buffer); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CopyZone(CaptureZone captureZone, in Span buffer) + { + ReadOnlySpan source = MemoryMarshal.Cast(_buffer); + Span target = MemoryMarshal.Cast(buffer); + + int offsetX = captureZone.X; + int offsetY = captureZone.Y; + int width = captureZone.Width; + int height = captureZone.Height; + + for (int y = 0; y < height; y++) + { + int sourceOffset = ((y + offsetY) * Display.Width) + offsetX; + int targetOffset = y * width; + + source.Slice(sourceOffset, width).CopyTo(target.Slice(targetOffset, width)); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DownscaleZone(CaptureZone captureZone, in Span buffer) + { + ReadOnlySpan source = _buffer; + Span target = buffer; + + int blockSize = captureZone.DownscaleLevel switch + { + 1 => 2, + 2 => 4, + 3 => 8, + 4 => 16, + 5 => 32, + 6 => 64, + 7 => 128, + 8 => 256, + _ => (int)Math.Pow(2, captureZone.DownscaleLevel), + }; + + int offsetX = captureZone.X; + int offsetY = captureZone.Y; + int width = captureZone.Width; + int height = captureZone.Height; + int stride = captureZone.Stride; + int bpp = captureZone.ColorFormat.BytesPerPixel; + int unscaledWith = captureZone.UnscaledWidth; + + Span scaleBuffer = stackalloc byte[bpp]; + for (int y = 0; y < height; y++) + for (int x = 0; x < width; x++) + { + AverageByteSampler.Sample(new SamplerInfo((x + offsetX) * blockSize, (y + offsetY) * blockSize, blockSize, blockSize, unscaledWith, bpp, source), scaleBuffer); + + int targetOffset = (y * stride) + (x * bpp); + + // DarthAffe 07.09.2023: Unroll as optimization since we know it's always 4 bpp - not ideal but it does quite a lot + target[targetOffset] = scaleBuffer[0]; + target[targetOffset + 1] = scaleBuffer[1]; + target[targetOffset + 2] = scaleBuffer[2]; + target[targetOffset + 3] = scaleBuffer[3]; + } + } + + /// + public override void Restart() + { + base.Restart(); + + lock (_captureLock) + { + DisposeDX(); + + try + { + PresentParameters presentParameters = new() + { + BackBufferWidth = Display.Width, + BackBufferHeight = Display.Height, + Windowed = true, + SwapEffect = SwapEffect.Discard + }; + _device = _direct3D9.CreateDevice(Display.Index, DeviceType.Hardware, IntPtr.Zero, CreateFlags.SoftwareVertexProcessing, presentParameters); + _surface = _device.CreateOffscreenPlainSurface(Display.Width, Display.Height, Format.A8R8G8B8, Pool.Scratch); + _stride = Display.Width * ColorBGRA.ColorFormat.BytesPerPixel; + _buffer = new byte[Display.Height * _stride]; + } + catch + { + DisposeDX(); + } + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + DisposeDX(); + } + + private void DisposeDX() + { + try + { + try { _surface?.Dispose(); } catch { /**/} + try { _device?.Dispose(); } catch { /**/} + _buffer = null; + _device = null; + _surface = null; + } + catch { /**/ } + } + + #endregion +} \ No newline at end of file diff --git a/ScreenCapture.NET.DX9/DX9ScreenCaptureService.cs b/ScreenCapture.NET.DX9/DX9ScreenCaptureService.cs new file mode 100644 index 0000000..8a725da --- /dev/null +++ b/ScreenCapture.NET.DX9/DX9ScreenCaptureService.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using Vortice.Direct3D9; + +namespace ScreenCapture.NET; + +/// +/// Represents a using the . +/// +public class DX9ScreenCaptureService : IScreenCaptureService +{ + #region Properties & Fields + + private readonly IDirect3D9 _direct3D9; + private readonly Dictionary _screenCaptures = new(); + + private bool _isDisposed; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + public DX9ScreenCaptureService() + { + _direct3D9 = D3D9.Direct3DCreate9(); + } + + ~DX9ScreenCaptureService() => Dispose(); + + #endregion + + #region Methods + + /// + public IEnumerable GetGraphicsCards() + { + if (_isDisposed) throw new ObjectDisposedException(GetType().FullName); + + Dictionary graphicsCards = new(); + for (int i = 0; i < _direct3D9.AdapterCount; i++) + { + AdapterIdentifier adapterIdentifier = _direct3D9.GetAdapterIdentifier(i); + if (!graphicsCards.ContainsKey(adapterIdentifier.DeviceId)) + graphicsCards.Add(adapterIdentifier.DeviceId, new GraphicsCard(i, adapterIdentifier.Description, adapterIdentifier.VendorId, adapterIdentifier.DeviceId)); + } + + return graphicsCards.Values; + } + + /// + public IEnumerable GetDisplays(GraphicsCard graphicsCard) + { + if (_isDisposed) throw new ObjectDisposedException(GetType().FullName); + + for (int i = 0; i < _direct3D9.AdapterCount; i++) + { + AdapterIdentifier adapterIdentifier = _direct3D9.GetAdapterIdentifier(i); + if (adapterIdentifier.DeviceId == graphicsCard.DeviceId) + { + DisplayMode displayMode = _direct3D9.GetAdapterDisplayMode(i); + yield return new Display(i, adapterIdentifier.DeviceName, displayMode.Width, displayMode.Height, Rotation.None, graphicsCard); + } + } + } + + /// + IScreenCapture IScreenCaptureService.GetScreenCapture(Display display) => GetScreenCapture(display); + public DX9ScreenCapture GetScreenCapture(Display display) + { + if (_isDisposed) throw new ObjectDisposedException(GetType().FullName); + + if (!_screenCaptures.TryGetValue(display, out DX9ScreenCapture? screenCapture)) + _screenCaptures.Add(display, screenCapture = new DX9ScreenCapture(_direct3D9, display)); + return screenCapture; + } + + /// + public void Dispose() + { + if (_isDisposed) return; + + foreach (DX9ScreenCapture screenCapture in _screenCaptures.Values) + screenCapture.Dispose(); + _screenCaptures.Clear(); + + try { _direct3D9.Dispose(); } catch { /**/ } + + GC.SuppressFinalize(this); + + _isDisposed = true; + } + + #endregion +} \ No newline at end of file diff --git a/ScreenCapture.NET.DX9/Downscale/AverageByteSampler.cs b/ScreenCapture.NET.DX9/Downscale/AverageByteSampler.cs new file mode 100644 index 0000000..e8e7423 --- /dev/null +++ b/ScreenCapture.NET.DX9/Downscale/AverageByteSampler.cs @@ -0,0 +1,149 @@ +// DarthAffe 07.09.2023: Copied from https://github.com/DarthAffe/RGB.NET/blob/2e0754f474b82ed4d0cae5c6c44378d234f1321b/RGB.NET.Presets/Textures/Sampler/AverageByteSampler.cs + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace ScreenCapture.NET.Downscale; + +/// +/// Represents a sampled that averages multiple byte-data entries. +/// +internal static class AverageByteSampler +{ + #region Constants + + private static readonly int INT_VECTOR_LENGTH = Vector.Count; + + #endregion + + #region Methods + + public static unsafe void Sample(in SamplerInfo info, in Span pixelData) + { + int count = info.Width * info.Height; + if (count == 0) return; + + int dataLength = pixelData.Length; + Span sums = stackalloc uint[dataLength]; + + int elementsPerVector = Vector.Count / dataLength; + int valuesPerVector = elementsPerVector * dataLength; + if (Vector.IsHardwareAccelerated && (info.Height > 1) && (info.Width >= valuesPerVector) && (dataLength <= Vector.Count)) + { + int chunks = info.Width / elementsPerVector; + + Vector sum1 = Vector.Zero; + Vector sum2 = Vector.Zero; + Vector sum3 = Vector.Zero; + Vector sum4 = Vector.Zero; + + for (int y = 0; y < info.Height; y++) + { + ReadOnlySpan data = info[y]; + + fixed (byte* colorPtr = data) + { + byte* current = colorPtr; + for (int i = 0; i < chunks; i++) + { + Vector bytes = *(Vector*)current; + Vector.Widen(bytes, out Vector short1, out Vector short2); + Vector.Widen(short1, out Vector int1, out Vector int2); + Vector.Widen(short2, out Vector int3, out Vector int4); + + sum1 = Vector.Add(sum1, int1); + sum2 = Vector.Add(sum2, int2); + sum3 = Vector.Add(sum3, int3); + sum4 = Vector.Add(sum4, int4); + + current += valuesPerVector; + } + } + + int missingElements = data.Length - (chunks * valuesPerVector); + int offset = chunks * valuesPerVector; + for (int i = 0; i < missingElements; i += dataLength) + for (int j = 0; j < sums.Length; j++) + sums[j] += data[offset + i + j]; + } + + int value = 0; + int sumIndex = 0; + for (int j = 0; (j < INT_VECTOR_LENGTH) && (value < valuesPerVector); j++) + { + sums[sumIndex] += sum1[j]; + ++sumIndex; + ++value; + + if (sumIndex >= dataLength) + sumIndex = 0; + } + + for (int j = 0; (j < INT_VECTOR_LENGTH) && (value < valuesPerVector); j++) + { + sums[sumIndex] += sum2[j]; + ++sumIndex; + ++value; + + if (sumIndex >= dataLength) + sumIndex = 0; + } + + for (int j = 0; (j < INT_VECTOR_LENGTH) && (value < valuesPerVector); j++) + { + sums[sumIndex] += sum3[j]; + ++sumIndex; + ++value; + + if (sumIndex >= dataLength) + sumIndex = 0; + } + + for (int j = 0; (j < INT_VECTOR_LENGTH) && (value < valuesPerVector); j++) + { + sums[sumIndex] += sum4[j]; + ++sumIndex; + ++value; + + if (sumIndex >= dataLength) + sumIndex = 0; + } + } + else + { + for (int y = 0; y < info.Height; y++) + { + ReadOnlySpan data = info[y]; + for (int i = 0; i < data.Length; i += dataLength) + for (int j = 0; j < sums.Length; j++) + sums[j] += data[i + j]; + } + } + + float divisor = count * byte.MaxValue; + for (int i = 0; i < pixelData.Length; i++) + pixelData[i] = (sums[i] / divisor).GetByteValueFromPercentage(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte GetByteValueFromPercentage(this float percentage) + { + if (float.IsNaN(percentage)) return 0; + + percentage = percentage.Clamp(0, 1.0f); + return (byte)(percentage >= 1.0f ? 255 : percentage * 256.0f); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float Clamp(this float value, float min, float max) + { + // ReSharper disable ConvertIfStatementToReturnStatement - I'm not sure why, but inlining this statement reduces performance by ~10% + if (value < min) return min; + if (value > max) return max; + return value; + // ReSharper restore ConvertIfStatementToReturnStatement + } + + #endregion +} \ No newline at end of file diff --git a/ScreenCapture.NET.DX9/Downscale/SamplerInfo.cs b/ScreenCapture.NET.DX9/Downscale/SamplerInfo.cs new file mode 100644 index 0000000..97081e3 --- /dev/null +++ b/ScreenCapture.NET.DX9/Downscale/SamplerInfo.cs @@ -0,0 +1,63 @@ +// DarthAffe 07.09.2023: Copied from https://github.com/DarthAffe/RGB.NET/blob/2e0754f474b82ed4d0cae5c6c44378d234f1321b/RGB.NET.Core/Rendering/Textures/Sampler/SamplerInfo.cs + +using System; + +namespace ScreenCapture.NET.Downscale; + +/// +/// Represents the information used to sample data. +/// +/// The type of the data to sample. +internal readonly ref struct SamplerInfo +{ + #region Properties & Fields + + private readonly ReadOnlySpan _data; + private readonly int _x; + private readonly int _y; + private readonly int _stride; + private readonly int _dataPerPixel; + private readonly int _dataWidth; + + /// + /// Gets the width of the region the data comes from. + /// + public readonly int Width; + + /// + /// Gets the height of region the data comes from. + /// + public readonly int Height; + + /// + /// Gets the data for the requested row. + /// + /// The row to get the data for. + /// A readonly span containing the data of the row. + public ReadOnlySpan this[int row] => _data.Slice((((_y + row) * _stride) + _x) * _dataPerPixel, _dataWidth); + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The width of the region the data comes from. + /// The height of region the data comes from. + /// The data to sample. + public SamplerInfo(int x, int y, int width, int height, int stride, int dataPerPixel, in ReadOnlySpan data) + { + this._x = x; + this._y = y; + this._data = data; + this._stride = stride; + this._dataPerPixel = dataPerPixel; + this.Width = width; + this.Height = height; + + _dataWidth = width * dataPerPixel; + } + + #endregion +} \ No newline at end of file diff --git a/ScreenCapture.NET.DX9/Resources/icon.png b/ScreenCapture.NET.DX9/Resources/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..46c50338bdcd4535ff63138ab7030dab6b11e747 GIT binary patch literal 705 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSY)RhkE(}r(cNo+^b@?U(MK}vQ zB8wRq_zr_Gjld?%{_ ztr9WvFf!(ud)_hqTTgv1lK=yg0|Q4xQ(Jdkg4JB9rJ^Rjo9|v{Of#O&87B4MV8Tqx z4J--_EDa10g$#`NIO`H_d}WsCKYsQ3gY4<$_kLD0Pp^0{GYzUeVYwf(_)H(Qv`p5; z7c+MIGuOJDO*?*FD1jft^xe4+S2zAkKQD-8--fqso^qvsB>H@v%YPLtyk9P!U(~eM zu*KE;fR(M!C!>@57OoXPuv_IkZ|2#}4d!b5->DVYpZxAC`z@F0_kPYj*=M(deB*G^ zH{q7=1KrsNX3ajJQaS6%@`vkov(%jXmobia3;O{% zghLi|+^bJ?xonb_*<4kxD&Wn)1Pnm9boPaI?Z*D<4-)s(cGjN#{!{m)Hs`m)h0mrh zS^xQ3_j$KXED=m)r~azw7k%Bn+TNq+^DOz-k(2!Yf2@8#xn|0{{e6@Eym}CD@95hT zCGlkuDUbB;@%jF~)BCL;O1EM2x1H1Pv)uD83NZN7{B5T-w*!L+--1u#(!YX^mkIcB z + + net7.0-windows;net6.0-windows + latest + enable + true + + Darth Affe + Wyrez + en-US + en-US + ScreenCapture.NET.DX9 + ScreenCapture.NET.DX9 + ScreenCapture.NET.DX9 + ScreenCapture.NET.DX9 + ScreenCapture.NET + Vortice based Screen-Capturing + Vortice based Screen-Capturing using Desktop Duplication + Copyright © Darth Affe 2023 + Copyright © Darth Affe 2023 + icon.png + https://github.com/DarthAffe/ScreenCapture.NET + LGPL-2.1-only + Github + https://github.com/DarthAffe/ScreenCapture.NET + True + + + + + 2.0.0 + 2.0.0 + 2.0.0 + + ..\bin\ + true + True + True + snupkg + + + + $(DefineConstants);TRACE;DEBUG + true + full + false + + + + portable + true + $(NoWarn);CS1591;CS1572;CS1573 + $(DefineConstants);RELEASE + + + + + True + + + + + + + + + + + + + + + + + diff --git a/ScreenCapture.NET.sln b/ScreenCapture.NET.sln index abd9a1d..e927c38 100644 --- a/ScreenCapture.NET.sln +++ b/ScreenCapture.NET.sln @@ -9,7 +9,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScreenCapture.NET.DX11", "S EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{CF7A1475-3A44-4870-A80F-5988DA25418B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScreenCapture.NET.Tests", "Tests\ScreenCapture.NET.Tests\ScreenCapture.NET.Tests.csproj", "{AA1829BB-EFA7-4BB8-8041-76374659A42B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScreenCapture.NET.Tests", "Tests\ScreenCapture.NET.Tests\ScreenCapture.NET.Tests.csproj", "{AA1829BB-EFA7-4BB8-8041-76374659A42B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScreenCapture.NET.DX9", "ScreenCapture.NET.DX9\ScreenCapture.NET.DX9.csproj", "{27EB5B17-2F83-43BA-A21F-06D93948B8BF}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -29,6 +31,10 @@ Global {AA1829BB-EFA7-4BB8-8041-76374659A42B}.Debug|Any CPU.Build.0 = Debug|Any CPU {AA1829BB-EFA7-4BB8-8041-76374659A42B}.Release|Any CPU.ActiveCfg = Release|Any CPU {AA1829BB-EFA7-4BB8-8041-76374659A42B}.Release|Any CPU.Build.0 = Release|Any CPU + {27EB5B17-2F83-43BA-A21F-06D93948B8BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27EB5B17-2F83-43BA-A21F-06D93948B8BF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27EB5B17-2F83-43BA-A21F-06D93948B8BF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27EB5B17-2F83-43BA-A21F-06D93948B8BF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Tests/ScreenCapture.NET.Tests/ScreenCapture.NET.Tests.csproj b/Tests/ScreenCapture.NET.Tests/ScreenCapture.NET.Tests.csproj index fbf4484..b4b9251 100644 --- a/Tests/ScreenCapture.NET.Tests/ScreenCapture.NET.Tests.csproj +++ b/Tests/ScreenCapture.NET.Tests/ScreenCapture.NET.Tests.csproj @@ -9,10 +9,13 @@ - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive +