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 0000000..46c5033
Binary files /dev/null and b/ScreenCapture.NET.DX9/Resources/icon.png differ
diff --git a/ScreenCapture.NET.DX9/ScreenCapture.NET.DX9.csproj b/ScreenCapture.NET.DX9/ScreenCapture.NET.DX9.csproj
new file mode 100644
index 0000000..3db631c
--- /dev/null
+++ b/ScreenCapture.NET.DX9/ScreenCapture.NET.DX9.csproj
@@ -0,0 +1,75 @@
+
+
+ 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
+