diff --git a/ScreenCapture.NET/DirectX/DX11CompatibilityExtensions.cs b/ScreenCapture.NET.DX11/DX11CompatibilityExtensions.cs
similarity index 100%
rename from ScreenCapture.NET/DirectX/DX11CompatibilityExtensions.cs
rename to ScreenCapture.NET.DX11/DX11CompatibilityExtensions.cs
diff --git a/ScreenCapture.NET.DX11/DX11ScreenCapture.cs b/ScreenCapture.NET.DX11/DX11ScreenCapture.cs
new file mode 100644
index 0000000..618711d
--- /dev/null
+++ b/ScreenCapture.NET.DX11/DX11ScreenCapture.cs
@@ -0,0 +1,509 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Threading;
+using SharpGen.Runtime;
+using Vortice.Direct3D;
+using Vortice.Direct3D11;
+using Vortice.DXGI;
+using Vortice.Mathematics;
+using MapFlags = Vortice.Direct3D11.MapFlags;
+using ResultCode = Vortice.DXGI.ResultCode;
+
+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 DX11ScreenCapture : AbstractScreenCapture
+{
+ #region Constants
+
+ private static readonly FeatureLevel[] FEATURE_LEVELS =
+ {
+ FeatureLevel.Level_11_1,
+ FeatureLevel.Level_11_0,
+ FeatureLevel.Level_10_1,
+ FeatureLevel.Level_10_0
+ };
+
+ #endregion
+
+ #region Properties & Fields
+
+ private readonly object _captureLock = new();
+
+ private readonly bool _useNewDuplicationAdapter;
+
+ ///
+ /// Gets or sets the timeout in ms used for screen-capturing. (default 1000ms)
+ /// This is used in https://docs.microsoft.com/en-us/windows/win32/api/dxgi1_2/nf-dxgi1_2-idxgioutputduplication-acquirenextframe
+ ///
+ // ReSharper disable once MemberCanBePrivate.Global
+ public int Timeout { get; set; } = 1000;
+
+ private readonly IDXGIFactory1 _factory;
+
+ private IDXGIOutput? _output;
+ private IDXGIOutputDuplication? _duplicatedOutput;
+ private ID3D11Device? _device;
+ private ID3D11DeviceContext? _context;
+ private ID3D11Texture2D? _captureTexture;
+
+ private readonly Dictionary, ZoneTextures> _textures = new();
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// Note that setting useNewDuplicationAdapter to true requires to call DPIAwareness.Initalize(); and prevents the capture from running in a WPF-thread.
+ ///
+ /// The used to create underlying objects.
+ /// The to duplicate.
+ /// Indicates if the DuplicateOutput1 interface should be used instead of the older DuplicateOutput. Currently there's no real use in setting this to true.
+ internal DX11ScreenCapture(IDXGIFactory1 factory, Display display, bool useNewDuplicationAdapter = false)
+ : base(display)
+ {
+ this._factory = factory;
+ this._useNewDuplicationAdapter = useNewDuplicationAdapter;
+
+ Restart();
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ protected override bool PerformScreenCapture()
+ {
+ bool result = false;
+ lock (_captureLock)
+ {
+ if ((_context == null) || (_duplicatedOutput == null) || (_captureTexture == null))
+ {
+ Restart();
+ return false;
+ }
+
+ try
+ {
+ IDXGIResource? screenResource = null;
+ try
+ {
+ _duplicatedOutput.AcquireNextFrame(Timeout, out OutduplFrameInfo duplicateFrameInformation, out screenResource).CheckError();
+ if ((screenResource == null) || (duplicateFrameInformation.LastPresentTime == 0)) return false;
+
+ using ID3D11Texture2D screenTexture = screenResource.QueryInterface();
+ _context.CopySubresourceRegion(_captureTexture, 0, 0, 0, 0, screenTexture, 0);
+ }
+ finally
+ {
+ try
+ {
+ screenResource?.Dispose();
+ _duplicatedOutput?.ReleaseFrame();
+ }
+ catch { /**/ }
+ }
+
+ result = true;
+ }
+ catch (SharpGenException dxException)
+ {
+ if ((dxException.ResultCode == ResultCode.AccessLost)
+ || (dxException.ResultCode == ResultCode.AccessDenied)
+ || (dxException.ResultCode == ResultCode.InvalidCall))
+ {
+ try
+ {
+ Restart();
+ }
+ catch { Thread.Sleep(100); }
+ }
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ protected override void PerformCaptureZoneUpdate(CaptureZone captureZone, in Span buffer)
+ {
+ if (_context == null) return;
+
+ lock (_textures)
+ {
+ if (!_textures.TryGetValue(captureZone, out ZoneTextures? textures)) return;
+
+ if (textures.ScalingTexture != null)
+ {
+ _context.CopySubresourceRegion(textures.ScalingTexture, 0, 0, 0, 0, _captureTexture, 0,
+ new Box(textures.X, textures.Y, 0,
+ textures.X + textures.UnscaledWidth,
+ textures.Y + textures.UnscaledHeight, 1));
+ _context.GenerateMips(textures.ScalingTextureView);
+ _context.CopySubresourceRegion(textures.StagingTexture, 0, 0, 0, 0, textures.ScalingTexture, captureZone.DownscaleLevel);
+ }
+ else
+ _context.CopySubresourceRegion(textures.StagingTexture, 0, 0, 0, 0, _captureTexture, 0,
+ new Box(textures.X, textures.Y, 0,
+ textures.X + textures.UnscaledWidth,
+ textures.Y + textures.UnscaledHeight, 1));
+
+ MappedSubresource mapSource = _context.Map(textures.StagingTexture, 0, MapMode.Read, MapFlags.None);
+
+ using IDisposable @lock = captureZone.Lock();
+ {
+ ReadOnlySpan source = mapSource.AsSpan(mapSource.RowPitch * textures.Height);
+ switch (Display.Rotation)
+ {
+ case Rotation.Rotation90:
+ CopyRotate90(source, mapSource.RowPitch, captureZone, buffer);
+ break;
+
+ case Rotation.Rotation180:
+ CopyRotate180(source, mapSource.RowPitch, captureZone, buffer);
+ break;
+
+ case Rotation.Rotation270:
+ CopyRotate270(source, mapSource.RowPitch, captureZone, buffer);
+ break;
+
+ default:
+ CopyRotate0(source, mapSource.RowPitch, captureZone, buffer);
+ break;
+ }
+ }
+
+ _context.Unmap(textures.StagingTexture, 0);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void CopyRotate0(in ReadOnlySpan source, int sourceStride, in CaptureZone captureZone, in Span buffer)
+ {
+ int height = captureZone.Height;
+ int stride = captureZone.Stride;
+ Span target = buffer;
+
+ for (int y = 0; y < height; y++)
+ {
+ int sourceOffset = y * sourceStride;
+ int targetOffset = y * stride;
+
+ source.Slice(sourceOffset, stride).CopyTo(target.Slice(targetOffset, stride));
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void CopyRotate90(in ReadOnlySpan source, int sourceStride, in CaptureZone captureZone, in Span buffer)
+ {
+ int width = captureZone.Width;
+ int height = captureZone.Height;
+ int usedBytesPerLine = height * captureZone.ColorFormat.BytesPerPixel;
+ Span target = MemoryMarshal.Cast(buffer);
+
+ for (int x = 0; x < width; x++)
+ {
+ ReadOnlySpan src = MemoryMarshal.Cast(source.Slice(x * sourceStride, usedBytesPerLine));
+ for (int y = 0; y < src.Length; y++)
+ target[(y * width) + (width - x - 1)] = src[y];
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void CopyRotate180(in ReadOnlySpan source, int sourceStride, in CaptureZone captureZone, in Span buffer)
+ {
+ int width = captureZone.Width;
+ int height = captureZone.Height;
+ int bpp = captureZone.ColorFormat.BytesPerPixel;
+ int usedBytesPerLine = width * bpp;
+ Span target = MemoryMarshal.Cast(buffer);
+
+ for (int y = 0; y < height; y++)
+ {
+ ReadOnlySpan src = MemoryMarshal.Cast(source.Slice(y * sourceStride, usedBytesPerLine));
+ for (int x = 0; x < src.Length; x++)
+ target[((height - y - 1) * width) + (width - x - 1)] = src[x];
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void CopyRotate270(in ReadOnlySpan source, int sourceStride, in CaptureZone captureZone, in Span buffer)
+ {
+ int width = captureZone.Width;
+ int height = captureZone.Height;
+ int usedBytesPerLine = height * captureZone.ColorFormat.BytesPerPixel;
+ Span target = MemoryMarshal.Cast(buffer);
+
+ for (int x = 0; x < width; x++)
+ {
+ ReadOnlySpan src = MemoryMarshal.Cast(source.Slice(x * sourceStride, usedBytesPerLine));
+ for (int y = 0; y < src.Length; y++)
+ target[((height - y - 1) * width) + x] = src[y];
+ }
+ }
+
+ ///
+ public override CaptureZone RegisterCaptureZone(int x, int y, int width, int height, int downscaleLevel = 0)
+ {
+ CaptureZone captureZone = base.RegisterCaptureZone(x, y, width, height, downscaleLevel);
+
+ lock (_textures)
+ InitializeCaptureZone(captureZone);
+
+ return captureZone;
+ }
+
+ ///
+ public override bool UnregisterCaptureZone(CaptureZone captureZone)
+ {
+ if (!base.UnregisterCaptureZone(captureZone)) return false;
+
+ lock (_textures)
+ {
+ if (_textures.TryGetValue(captureZone, out ZoneTextures? textures))
+ {
+ textures.Dispose();
+ _textures.Remove(captureZone);
+
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ ///
+ public override void UpdateCaptureZone(CaptureZone captureZone, int? x = null, int? y = null, int? width = null, int? height = null, int? downscaleLevel = null)
+ {
+ base.UpdateCaptureZone(captureZone, x, y, width, height, downscaleLevel);
+
+ //TODO DarthAffe 01.05.2022: For now just reinitialize the zone in that case, but this could be optimized to only recreate the textures needed.
+ if ((width != null) || (height != null) || (downscaleLevel != null))
+ {
+ lock (_textures)
+ {
+ if (_textures.TryGetValue(captureZone, out ZoneTextures? textures))
+ {
+ textures.Dispose();
+ InitializeCaptureZone(captureZone);
+ }
+ }
+ }
+ }
+
+ ///
+ protected override void ValidateCaptureZoneAndThrow(int x, int y, int width, int height, int downscaleLevel)
+ {
+ if (_device == null) throw new ApplicationException("ScreenCapture isn't initialized.");
+
+ base.ValidateCaptureZoneAndThrow(x, y, width, height, downscaleLevel);
+ }
+
+ private void InitializeCaptureZone(in CaptureZone captureZone)
+ {
+ int x;
+ int y;
+ int width;
+ int height;
+ int unscaledWidth;
+ int unscaledHeight;
+
+ if (captureZone.Display.Rotation is Rotation.Rotation90 or Rotation.Rotation270)
+ {
+ x = captureZone.Y;
+ y = captureZone.X;
+ width = captureZone.Height;
+ height = captureZone.Width;
+ unscaledWidth = captureZone.UnscaledHeight;
+ unscaledHeight = captureZone.UnscaledWidth;
+ }
+ else
+ {
+ x = captureZone.X;
+ y = captureZone.Y;
+ width = captureZone.Width;
+ height = captureZone.Height;
+ unscaledWidth = captureZone.UnscaledWidth;
+ unscaledHeight = captureZone.UnscaledHeight;
+ }
+
+ Texture2DDescription stagingTextureDesc = new()
+ {
+ CPUAccessFlags = CpuAccessFlags.Read,
+ BindFlags = BindFlags.None,
+ Format = Format.B8G8R8A8_UNorm,
+ Width = width,
+ Height = height,
+ MiscFlags = ResourceOptionFlags.None,
+ MipLevels = 1,
+ ArraySize = 1,
+ SampleDescription = { Count = 1, Quality = 0 },
+ Usage = ResourceUsage.Staging
+ };
+ ID3D11Texture2D stagingTexture = _device!.CreateTexture2D(stagingTextureDesc);
+
+ ID3D11Texture2D? scalingTexture = null;
+ ID3D11ShaderResourceView? scalingTextureView = null;
+ if (captureZone.DownscaleLevel > 0)
+ {
+ Texture2DDescription scalingTextureDesc = new()
+ {
+ CPUAccessFlags = CpuAccessFlags.None,
+ BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource,
+ Format = Format.B8G8R8A8_UNorm,
+ Width = unscaledWidth,
+ Height = unscaledHeight,
+ MiscFlags = ResourceOptionFlags.GenerateMips,
+ MipLevels = captureZone.DownscaleLevel + 1,
+ ArraySize = 1,
+ SampleDescription = { Count = 1, Quality = 0 },
+ Usage = ResourceUsage.Default
+ };
+ scalingTexture = _device!.CreateTexture2D(scalingTextureDesc);
+ scalingTextureView = _device.CreateShaderResourceView(scalingTexture);
+ }
+
+ _textures[captureZone] = new ZoneTextures(x, y, width, height, unscaledWidth, unscaledHeight, stagingTexture, scalingTexture, scalingTextureView);
+ }
+
+ ///
+ public override void Restart()
+ {
+ base.Restart();
+
+ lock (_captureLock)
+ lock (_textures)
+ {
+ try
+ {
+ foreach (ZoneTextures textures in _textures.Values)
+ textures.Dispose();
+ _textures.Clear();
+
+ DisposeDX();
+
+ using IDXGIAdapter1 adapter = _factory.GetAdapter1(Display.GraphicsCard.Index) ?? throw new ApplicationException("Couldn't create DirectX-Adapter.");
+
+ D3D11.D3D11CreateDevice(adapter, DriverType.Unknown, DeviceCreationFlags.None, FEATURE_LEVELS, out _device).CheckError();
+ _context = _device!.ImmediateContext;
+
+ _output = adapter.GetOutput(Display.Index) ?? throw new ApplicationException("Couldn't get DirectX-Output.");
+ using IDXGIOutput5 output = _output.QueryInterface();
+
+ int width = Display.Width;
+ int height = Display.Height;
+ if (Display.Rotation is Rotation.Rotation90 or Rotation.Rotation270)
+ (width, height) = (height, width);
+
+ Texture2DDescription captureTextureDesc = new()
+ {
+ CPUAccessFlags = CpuAccessFlags.None,
+ BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource,
+ Format = Format.B8G8R8A8_UNorm,
+ Width = width,
+ Height = height,
+ MiscFlags = ResourceOptionFlags.None,
+ MipLevels = 1,
+ ArraySize = 1,
+ SampleDescription = { Count = 1, Quality = 0 },
+ Usage = ResourceUsage.Default
+ };
+ _captureTexture = _device.CreateTexture2D(captureTextureDesc);
+
+ foreach (CaptureZone captureZone in CaptureZones)
+ InitializeCaptureZone(captureZone);
+
+ if (_useNewDuplicationAdapter)
+ _duplicatedOutput = output.DuplicateOutput1(_device, new[] { Format.B8G8R8A8_UNorm }); // DarthAffe 27.02.2021: This prepares for the use of 10bit color depth
+ else
+ _duplicatedOutput = output.DuplicateOutput(_device);
+ }
+ catch { DisposeDX(); }
+ }
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ lock (_captureLock)
+ DisposeDX();
+ }
+
+ private void DisposeDX()
+ {
+ try
+ {
+ try { _duplicatedOutput?.Dispose(); } catch { /**/ }
+ try { _output?.Dispose(); } catch { /**/ }
+ try { _context?.Dispose(); } catch { /**/ }
+ try { _device?.Dispose(); } catch { /**/ }
+ try { _captureTexture?.Dispose(); } catch { /**/ }
+
+ _duplicatedOutput = null;
+ _context = null;
+ _captureTexture = null;
+ }
+ catch { /**/ }
+ }
+
+ #endregion
+
+ private sealed class ZoneTextures : IDisposable
+ {
+ #region Properties & Fields
+
+ public int X { get; }
+ public int Y { get; }
+ public int Width { get; }
+ public int Height { get; }
+ public int UnscaledWidth { get; }
+ public int UnscaledHeight { get; }
+
+ public ID3D11Texture2D StagingTexture { get; }
+ public ID3D11Texture2D? ScalingTexture { get; }
+ public ID3D11ShaderResourceView? ScalingTextureView { get; }
+
+ #endregion
+
+ #region Constructors
+
+ public ZoneTextures(int x, int y, int width, int height, int unscaledWidth, int unscaledHeight,
+ ID3D11Texture2D stagingTexture, ID3D11Texture2D? scalingTexture, ID3D11ShaderResourceView? scalingTextureView)
+ {
+ this.X = x;
+ this.Y = y;
+ this.Width = width;
+ this.Height = height;
+ this.UnscaledWidth = unscaledWidth;
+ this.UnscaledHeight = unscaledHeight;
+ this.StagingTexture = stagingTexture;
+ this.ScalingTexture = scalingTexture;
+ this.ScalingTextureView = scalingTextureView;
+ }
+
+ #endregion
+
+ #region Methods
+
+ public void Dispose()
+ {
+ StagingTexture.Dispose();
+ ScalingTexture?.Dispose();
+ ScalingTextureView?.Dispose();
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/ScreenCapture.NET/DirectX/DX11ScreenCaptureService.cs b/ScreenCapture.NET.DX11/DX11ScreenCaptureService.cs
similarity index 79%
rename from ScreenCapture.NET/DirectX/DX11ScreenCaptureService.cs
rename to ScreenCapture.NET.DX11/DX11ScreenCaptureService.cs
index 79a023e..4193647 100644
--- a/ScreenCapture.NET/DirectX/DX11ScreenCaptureService.cs
+++ b/ScreenCapture.NET.DX11/DX11ScreenCaptureService.cs
@@ -12,9 +12,10 @@ public class DX11ScreenCaptureService : IScreenCaptureService
#region Properties & Fields
private readonly IDXGIFactory1 _factory;
-
private readonly Dictionary _screenCaptures = new();
+ private bool _isDisposed;
+
#endregion
#region Constructors
@@ -27,6 +28,8 @@ public class DX11ScreenCaptureService : IScreenCaptureService
DXGI.CreateDXGIFactory1(out _factory!).CheckError();
}
+ ~DX11ScreenCaptureService() => Dispose();
+
#endregion
#region Methods
@@ -34,6 +37,8 @@ public class DX11ScreenCaptureService : IScreenCaptureService
///
public IEnumerable GetGraphicsCards()
{
+ if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
+
int i = 0;
while (_factory.EnumAdapters1(i, out IDXGIAdapter1 adapter).Success)
{
@@ -46,6 +51,8 @@ public class DX11ScreenCaptureService : IScreenCaptureService
///
public IEnumerable GetDisplays(GraphicsCard graphicsCard)
{
+ if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
+
using IDXGIAdapter1? adapter = _factory.GetAdapter1(graphicsCard.Index);
int i = 0;
@@ -59,7 +66,7 @@ public class DX11ScreenCaptureService : IScreenCaptureService
}
}
- private Rotation GetRotation(ModeRotation rotation) => rotation switch
+ private static Rotation GetRotation(ModeRotation rotation) => rotation switch
{
ModeRotation.Rotate90 => Rotation.Rotation90,
ModeRotation.Rotate180 => Rotation.Rotation180,
@@ -68,8 +75,11 @@ public class DX11ScreenCaptureService : IScreenCaptureService
};
///
- public IScreenCapture GetScreenCapture(Display display)
+ IScreenCapture IScreenCaptureService.GetScreenCapture(Display display) => GetScreenCapture(display);
+ public DX11ScreenCapture GetScreenCapture(Display display)
{
+ if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
+
if (!_screenCaptures.TryGetValue(display, out DX11ScreenCapture? screenCapture))
_screenCaptures.Add(display, screenCapture = new DX11ScreenCapture(_factory, display));
return screenCapture;
@@ -78,13 +88,17 @@ public class DX11ScreenCaptureService : IScreenCaptureService
///
public void Dispose()
{
+ if (_isDisposed) return;
+
foreach (DX11ScreenCapture screenCapture in _screenCaptures.Values)
screenCapture.Dispose();
_screenCaptures.Clear();
- _factory.Dispose();
+ try { _factory.Dispose(); } catch { /**/ }
GC.SuppressFinalize(this);
+
+ _isDisposed = true;
}
#endregion
diff --git a/ScreenCapture.NET/Helper/DPIAwareness.cs b/ScreenCapture.NET.DX11/Helper/DPIAwareness.cs
similarity index 100%
rename from ScreenCapture.NET/Helper/DPIAwareness.cs
rename to ScreenCapture.NET.DX11/Helper/DPIAwareness.cs
diff --git a/ScreenCapture.NET.DX11/Resources/icon.png b/ScreenCapture.NET.DX11/Resources/icon.png
new file mode 100644
index 0000000..46c5033
Binary files /dev/null and b/ScreenCapture.NET.DX11/Resources/icon.png differ
diff --git a/ScreenCapture.NET.DX11/ScreenCapture.NET.DX11.csproj b/ScreenCapture.NET.DX11/ScreenCapture.NET.DX11.csproj
new file mode 100644
index 0000000..b13b988
--- /dev/null
+++ b/ScreenCapture.NET.DX11/ScreenCapture.NET.DX11.csproj
@@ -0,0 +1,73 @@
+
+
+ net7.0-windows;net6.0-windows
+ latest
+ enable
+ true
+
+ Darth Affe
+ Wyrez
+ en-US
+ en-US
+ ScreenCapture.NET.DX11
+ ScreenCapture.NET.DX11
+ ScreenCapture.NET.DX11
+ ScreenCapture.NET.DX11
+ 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
+
+
+ The downscale-level is now automatically limited if it would scale the image below a size of 1x1 px.
+ This is change that can potentially break existing behavior but should only affect cases that are expected to crash with earlier versions.
+
+
+ 1.3.2
+ 1.3.2
+ 1.3.2
+
+ ..\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.DX11/ScreenCapture.NET.DX11.csproj.DotSettings b/ScreenCapture.NET.DX11/ScreenCapture.NET.DX11.csproj.DotSettings
new file mode 100644
index 0000000..bdcca3e
--- /dev/null
+++ b/ScreenCapture.NET.DX11/ScreenCapture.NET.DX11.csproj.DotSettings
@@ -0,0 +1,2 @@
+
+ True
\ No newline at end of file
diff --git a/ScreenCapture.NET.DX9/DX9ScreenCapture.cs b/ScreenCapture.NET.DX9/DX9ScreenCapture.cs
new file mode 100644
index 0000000..cb7aec6
--- /dev/null
+++ b/ScreenCapture.NET.DX9/DX9ScreenCapture.cs
@@ -0,0 +1,224 @@
+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 9.
+/// https://learn.microsoft.com/en-us/windows/win32/api/d3d9/nf-d3d9-idirect3ddevice9-getfrontbufferdata
+///
+// 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 D3D9 instance used.
+ /// The to duplicate.
+ internal 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);
+
+ lock (_captureLock)
+ 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..1862de2
--- /dev/null
+++ b/ScreenCapture.NET.DX9/Downscale/SamplerInfo.cs
@@ -0,0 +1,67 @@
+// 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 X-location of the region the data comes from.
+ /// The Y-location of the region the data comes from.
+ /// The width of the region the data comes from.
+ /// The height of region the data comes from.
+ /// The number of pixels in a row of data.
+ /// The number of {T} representing a single pixel.
+ /// 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..caceecc
--- /dev/null
+++ b/ScreenCapture.NET.DX9/ScreenCapture.NET.DX9.csproj
@@ -0,0 +1,71 @@
+
+
+ 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.X11/Downscale/AverageByteSampler.cs b/ScreenCapture.NET.X11/Downscale/AverageByteSampler.cs
new file mode 100644
index 0000000..e8e7423
--- /dev/null
+++ b/ScreenCapture.NET.X11/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.X11/Downscale/SamplerInfo.cs b/ScreenCapture.NET.X11/Downscale/SamplerInfo.cs
new file mode 100644
index 0000000..1862de2
--- /dev/null
+++ b/ScreenCapture.NET.X11/Downscale/SamplerInfo.cs
@@ -0,0 +1,67 @@
+// 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 X-location of the region the data comes from.
+ /// The Y-location of the region the data comes from.
+ /// The width of the region the data comes from.
+ /// The height of region the data comes from.
+ /// The number of pixels in a row of data.
+ /// The number of {T} representing a single pixel.
+ /// 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.X11/Resources/icon.png b/ScreenCapture.NET.X11/Resources/icon.png
new file mode 100644
index 0000000..46c5033
Binary files /dev/null and b/ScreenCapture.NET.X11/Resources/icon.png differ
diff --git a/ScreenCapture.NET.X11/ScreenCapture.NET.X11.csproj b/ScreenCapture.NET.X11/ScreenCapture.NET.X11.csproj
new file mode 100644
index 0000000..b7cbb84
--- /dev/null
+++ b/ScreenCapture.NET.X11/ScreenCapture.NET.X11.csproj
@@ -0,0 +1,68 @@
+
+
+ net7.0;net6.0
+ linux-x64
+ latest
+ enable
+ true
+
+ Darth Affe
+ Wyrez
+ en-US
+ en-US
+ ScreenCapture.NET.X11
+ ScreenCapture.NET.X11
+ ScreenCapture.NET.X11
+ ScreenCapture.NET.X11
+ 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.X11/X11.cs b/ScreenCapture.NET.X11/X11.cs
new file mode 100644
index 0000000..85b8ec3
--- /dev/null
+++ b/ScreenCapture.NET.X11/X11.cs
@@ -0,0 +1,133 @@
+using System.Runtime.InteropServices;
+
+namespace ScreenCapture.NET;
+
+#if NET7_0_OR_GREATER
+internal static partial class X11
+{
+ internal const nint DISPLAY_NAME = 0;
+
+ internal const long ALL_PLANES = -1;
+ internal const int ZPIXMAP = 2;
+
+ [LibraryImport("libX11.so.6")]
+ internal static partial nint XOpenDisplay(nint displayName);
+
+ [LibraryImport("libX11.so.6")]
+ internal static partial int XScreenCount(nint display);
+
+ [LibraryImport("libX11.so.6")]
+ internal static partial nint XScreenOfDisplay(nint display, int screeenNumber);
+
+ [LibraryImport("libX11.so.6")]
+ internal static partial int XWidthOfScreen(nint screen);
+
+ [LibraryImport("libX11.so.6")]
+ internal static partial int XHeightOfScreen(nint screen);
+
+ [LibraryImport("libX11.so.6")]
+ internal static partial nint XRootWindowOfScreen(nint screen);
+
+ [LibraryImport("libX11.so.6")]
+ internal static partial nint XGetImage(nint display, nint drawable, int x, int y, uint width, uint height, long planeMask, int format);
+
+ [LibraryImport("libX11.so.6")]
+ internal static partial nint XGetSubImage(nint display, nint drawable, int x, int y, uint width, uint height, long planeMask, int format, nint image, int destX, int dextY);
+
+ [LibraryImport("libX11.so.6")]
+ internal static partial void XDestroyImage(nint image);
+
+ [LibraryImport("libX11.so.6")]
+ internal static partial nint XDisplayString(nint display);
+
+ [LibraryImport("libX11.so.6")]
+ internal static partial void XCloseDisplay(nint display);
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal unsafe struct XImage
+ {
+ // ReSharper disable MemberCanBePrivate.Global
+ public int width;
+ public int height;
+ public int xoffset;
+ public int format;
+ public byte* data;
+ public int byte_order;
+ public int bitmap_unit;
+ public int bitmap_bit_order;
+ public int bitmap_pad;
+ public int depth;
+ public int bytes_per_line;
+ public int bits_per_pixel;
+ public uint red_mask;
+ public uint green_mask;
+ public uint blue_mask;
+ public nint obdata;
+ // ReSharper restore MemberCanBePrivate.Global
+ }
+}
+#else
+internal static class X11
+{
+ internal const nint DISPLAY_NAME = 0;
+
+ internal const long ALL_PLANES = -1;
+ internal const int ZPIXMAP = 2;
+
+ [DllImport("libX11.so.6")]
+ internal static extern nint XOpenDisplay(nint displayName);
+
+ [DllImport("libX11.so.6")]
+ internal static extern int XScreenCount(nint display);
+
+ [DllImport("libX11.so.6")]
+ internal static extern nint XScreenOfDisplay(nint display, int screeenNumber);
+
+ [DllImport("libX11.so.6")]
+ internal static extern int XWidthOfScreen(nint screen);
+
+ [DllImport("libX11.so.6")]
+ internal static extern int XHeightOfScreen(nint screen);
+
+ [DllImport("libX11.so.6")]
+ internal static extern nint XRootWindowOfScreen(nint screen);
+
+ [DllImport("libX11.so.6")]
+ internal static extern nint XGetImage(nint display, nint drawable, int x, int y, uint width, uint height, long planeMask, int format);
+
+ [DllImport("libX11.so.6")]
+ internal static extern nint XGetSubImage(nint display, nint drawable, int x, int y, uint width, uint height, long planeMask, int format, nint image, int destX, int dextY);
+
+ [DllImport("libX11.so.6")]
+ internal static extern void XDestroyImage(nint image);
+
+ [DllImport("libX11.so.6")]
+ internal static extern nint XDisplayString(nint display);
+
+ [DllImport("libX11.so.6")]
+ internal static extern void XCloseDisplay(nint display);
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal unsafe struct XImage
+ {
+ // ReSharper disable MemberCanBePrivate.Global
+ public int width;
+ public int height;
+ public int xoffset;
+ public int format;
+ public byte* data;
+ public int byte_order;
+ public int bitmap_unit;
+ public int bitmap_bit_order;
+ public int bitmap_pad;
+ public int depth;
+ public int bytes_per_line;
+ public int bits_per_pixel;
+ public uint red_mask;
+ public uint green_mask;
+ public uint blue_mask;
+ public nint obdata;
+ // ReSharper restore MemberCanBePrivate.Global
+ }
+}
+#endif
\ No newline at end of file
diff --git a/ScreenCapture.NET.X11/X11ScreenCapture.cs b/ScreenCapture.NET.X11/X11ScreenCapture.cs
new file mode 100644
index 0000000..c35339e
--- /dev/null
+++ b/ScreenCapture.NET.X11/X11ScreenCapture.cs
@@ -0,0 +1,187 @@
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using ScreenCapture.NET.Downscale;
+
+namespace ScreenCapture.NET;
+
+///
+/// Represents a ScreenCapture using libX11.
+/// https://x.org/releases/current/doc/libX11/libX11/libX11.html#XGetImage
+///
+// ReSharper disable once InconsistentNaming
+public sealed class X11ScreenCapture : AbstractScreenCapture
+{
+ #region Properties & Fields
+
+ private readonly object _captureLock = new();
+
+ private nint _display;
+ private nint _drawable;
+ private nint _imageHandle;
+ private X11.XImage _image;
+ private int _size;
+ private unsafe ReadOnlySpan Data => new(_image.data, _size);
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The to duplicate.
+ internal X11ScreenCapture(Display display)
+ : base(display)
+ {
+ Restart();
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ protected override bool PerformScreenCapture()
+ {
+ lock (_captureLock)
+ {
+ if ((_display == 0) || (_imageHandle == 0))
+ {
+ Restart();
+ return false;
+ }
+
+ X11.XGetSubImage(_display, _drawable, 0, 0, (uint)Display.Width, (uint)Display.Height, X11.ALL_PLANES, X11.ZPIXMAP, _imageHandle, 0, 0);
+
+ return true;
+ }
+ }
+
+ ///
+ protected override void PerformCaptureZoneUpdate(CaptureZone captureZone, in Span buffer)
+ {
+ 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(Data);
+ Span target = MemoryMarshal.Cast(buffer);
+
+ int offsetX = captureZone.X;
+ int offsetY = captureZone.Y;
+ int width = captureZone.Width;
+ int height = captureZone.Height;
+ int sourceStride = _image.bytes_per_line / captureZone.ColorFormat.BytesPerPixel;
+
+ for (int y = 0; y < height; y++)
+ {
+ int sourceOffset = ((y + offsetY) * sourceStride) + 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 = Data;
+ 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 sourceStride = _image.bytes_per_line / bpp;
+
+ 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, sourceStride, bpp, source), scaleBuffer);
+
+ int targetOffset = (y * stride) + (x * bpp);
+
+ // DarthAffe 09.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)
+ {
+ DisposeDisplay();
+ try
+ {
+ _display = X11.XOpenDisplay(X11.DISPLAY_NAME);
+
+ nint screen = X11.XScreenOfDisplay(_display, Display.Index);
+ _drawable = X11.XRootWindowOfScreen(screen);
+ _imageHandle = X11.XGetImage(_display, _drawable, 0, 0, (uint)Display.Width, (uint)Display.Height, X11.ALL_PLANES, X11.ZPIXMAP);
+ _image = Marshal.PtrToStructure(_imageHandle);
+ _size = _image.bytes_per_line * _image.height;
+
+ if (_image.bits_per_pixel != (ColorBGRA.ColorFormat.BytesPerPixel * 8)) throw new NotSupportedException("The X-Server is configured to a not supported pixel-format. Needs to be 32 bit per pixel BGR.");
+ }
+ catch
+ {
+ DisposeDisplay();
+ }
+ }
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ lock (_captureLock)
+ {
+ try { DisposeDisplay(); }
+ catch { /**/ }
+ }
+ }
+
+ private void DisposeDisplay()
+ {
+ if (_imageHandle != 0)
+ try { X11.XDestroyImage(_imageHandle); } catch { /**/ }
+
+ _image = default;
+
+ if (_display != 0)
+ try { X11.XCloseDisplay(_display); } catch { /**/ }
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/ScreenCapture.NET.X11/X11ScreenCaptureService.cs b/ScreenCapture.NET.X11/X11ScreenCaptureService.cs
new file mode 100644
index 0000000..2e39c27
--- /dev/null
+++ b/ScreenCapture.NET.X11/X11ScreenCaptureService.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+namespace ScreenCapture.NET;
+
+///
+/// Represents a using the .
+///
+public class X11ScreenCaptureService : IScreenCaptureService
+{
+ #region Properties & Fields
+
+ private readonly Dictionary _screenCaptures = new();
+
+ private bool _isDisposed;
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public X11ScreenCaptureService()
+ { }
+
+ ~X11ScreenCaptureService() => Dispose();
+
+ #endregion
+
+ #region Methods
+
+ ///
+ public IEnumerable GetGraphicsCards()
+ {
+ if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
+
+ nint display = X11.XOpenDisplay(X11.DISPLAY_NAME);
+ try
+ {
+ string name = Marshal.PtrToStringAnsi(X11.XDisplayString(display)) ?? string.Empty;
+ yield return new GraphicsCard(0, name, 0, 0);
+ }
+ finally
+ {
+ X11.XCloseDisplay(display);
+ }
+ }
+
+ ///
+ public IEnumerable GetDisplays(GraphicsCard graphicsCard)
+ {
+ if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
+
+ nint display = X11.XOpenDisplay(X11.DISPLAY_NAME);
+ try
+ {
+ int screenCount = X11.XScreenCount(display);
+ for (int screenNumber = 0; screenNumber < screenCount; screenNumber++)
+ {
+ nint screen = X11.XScreenOfDisplay(display, screenNumber);
+ int screenWidth = X11.XWidthOfScreen(screen);
+ int screenHeight = X11.XHeightOfScreen(screen);
+
+ // DarthAffe 10.09.2023: Emulate DX-Displaynames for no real reason ¯\(°_o)/¯
+ yield return new Display(screenNumber, @$"\\.\DISPLAY{screenNumber + 1}", screenWidth, screenHeight, Rotation.None, graphicsCard);
+ }
+ }
+ finally
+ {
+ X11.XCloseDisplay(display);
+ }
+ }
+
+ ///
+ IScreenCapture IScreenCaptureService.GetScreenCapture(Display display) => GetScreenCapture(display);
+
+ ///
+ public X11ScreenCapture GetScreenCapture(Display display)
+ {
+ if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
+
+ if (!_screenCaptures.TryGetValue(display, out X11ScreenCapture? screenCapture))
+ _screenCaptures.Add(display, screenCapture = new X11ScreenCapture(display));
+ return screenCapture;
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (_isDisposed) return;
+
+ foreach (X11ScreenCapture screenCapture in _screenCaptures.Values)
+ screenCapture.Dispose();
+ _screenCaptures.Clear();
+
+ GC.SuppressFinalize(this);
+
+ _isDisposed = true;
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/ScreenCapture.NET.sln b/ScreenCapture.NET.sln
index 3888c2c..b91b878 100644
--- a/ScreenCapture.NET.sln
+++ b/ScreenCapture.NET.sln
@@ -1,9 +1,19 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.31025.194
+# Visual Studio Version 17
+VisualStudioVersion = 17.6.33829.357
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScreenCapture.NET", "ScreenCapture.NET\ScreenCapture.NET.csproj", "{90596344-E012-4534-A933-3BD1B55469DC}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScreenCapture.NET", "ScreenCapture.NET\ScreenCapture.NET.csproj", "{90596344-E012-4534-A933-3BD1B55469DC}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScreenCapture.NET.DX11", "ScreenCapture.NET.DX11\ScreenCapture.NET.DX11.csproj", "{58A09AD8-D66F-492E-8BC7-62BDB85E57EC}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{CF7A1475-3A44-4870-A80F-5988DA25418B}"
+EndProject
+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
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScreenCapture.NET.X11", "ScreenCapture.NET.X11\ScreenCapture.NET.X11.csproj", "{F81562C8-2035-4FB9-9547-C51F9D343BDF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -15,10 +25,29 @@ Global
{90596344-E012-4534-A933-3BD1B55469DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{90596344-E012-4534-A933-3BD1B55469DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{90596344-E012-4534-A933-3BD1B55469DC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {58A09AD8-D66F-492E-8BC7-62BDB85E57EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {58A09AD8-D66F-492E-8BC7-62BDB85E57EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {58A09AD8-D66F-492E-8BC7-62BDB85E57EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {58A09AD8-D66F-492E-8BC7-62BDB85E57EC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AA1829BB-EFA7-4BB8-8041-76374659A42B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {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
+ {F81562C8-2035-4FB9-9547-C51F9D343BDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F81562C8-2035-4FB9-9547-C51F9D343BDF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F81562C8-2035-4FB9-9547-C51F9D343BDF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F81562C8-2035-4FB9-9547-C51F9D343BDF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {AA1829BB-EFA7-4BB8-8041-76374659A42B} = {CF7A1475-3A44-4870-A80F-5988DA25418B}
+ EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B5111031-6E65-4331-9E6E-A07165289860}
EndGlobalSection
diff --git a/ScreenCapture.NET.sln.DotSettings b/ScreenCapture.NET.sln.DotSettings
index 8e3f720..4061c9f 100644
--- a/ScreenCapture.NET.sln.DotSettings
+++ b/ScreenCapture.NET.sln.DotSettings
@@ -1,3 +1,5 @@
+ BGR
+ BGRA
DPI
DX
\ No newline at end of file
diff --git a/ScreenCapture.NET/CommunityToolkit.HighPerformance/ReadOnlyRefEnumerable.cs b/ScreenCapture.NET/CommunityToolkit.HighPerformance/ReadOnlyRefEnumerable.cs
new file mode 100644
index 0000000..6dda976
--- /dev/null
+++ b/ScreenCapture.NET/CommunityToolkit.HighPerformance/ReadOnlyRefEnumerable.cs
@@ -0,0 +1,266 @@
+// DarthAffe 05.09.2023: Based on https://github.com/CommunityToolkit/dotnet/blob/b0d6c4f9c0cfb5d860400abb00b0ca1b3e94dfa4/src/CommunityToolkit.HighPerformance/Enumerables/ReadOnlyRefEnumerable%7BT%7D.cs
+
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace ScreenCapture.NET;
+
+///
+/// A that iterates readonly items from arbitrary memory locations.
+///
+/// The type of items to enumerate.
+public readonly ref struct ReadOnlyRefEnumerable
+{
+ #region Properties & Fields
+
+ ///
+ /// The instance pointing to the first item in the target memory area.
+ ///
+ /// The field maps to the total available length.
+ private readonly ReadOnlySpan _span;
+
+ ///
+ /// The distance between items in the sequence to enumerate.
+ ///
+ /// The distance refers to items, not byte offset.
+ private readonly int _step;
+
+ ///
+ /// Gets the total available length for the sequence.
+ ///
+ public int Length
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _span.Length;
+ }
+
+ ///
+ /// Gets the element at the specified zero-based index.
+ ///
+ /// The zero-based index of the element.
+ /// A reference to the element at the specified index.
+ ///
+ /// Thrown when is invalid.
+ ///
+ public ref readonly T this[int index]
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ if ((uint)index >= (uint)Length) throw new IndexOutOfRangeException();
+
+ ref T r0 = ref MemoryMarshal.GetReference(_span);
+ nint offset = (nint)(uint)index * (nint)(uint)_step;
+ ref T ri = ref Unsafe.Add(ref r0, offset);
+
+ return ref ri;
+ }
+ }
+
+ ///
+ /// Gets the element at the specified zero-based index.
+ ///
+ /// The zero-based index of the element.
+ /// A reference to the element at the specified index.
+ ///
+ /// Thrown when is invalid.
+ ///
+ public ref readonly T this[Index index]
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => ref this[index.GetOffset(Length)];
+ }
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// A reference to the first item of the sequence.
+ /// The number of items in the sequence.
+ /// The distance between items in the sequence to enumerate.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal ReadOnlyRefEnumerable(in T reference, int length, int step)
+ {
+ this._step = step;
+
+ _span = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef(reference), length);
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Enumerator GetEnumerator() => new(_span, _step);
+
+ public T[] ToArray()
+ {
+ int length = _span.Length;
+
+ // Empty array if no data is mapped
+ if (length == 0)
+ return Array.Empty();
+
+ T[] array = new T[length];
+ CopyTo(array);
+
+ return array;
+ }
+
+ ///
+ /// Copies the contents of this into a destination instance.
+ ///
+ /// The destination instance.
+ ///
+ /// Thrown when is shorter than the source instance.
+ ///
+ public void CopyTo(Span destination)
+ {
+ if (_step == 1)
+ {
+ _span.CopyTo(destination);
+ return;
+ }
+
+ ref T sourceRef = ref MemoryMarshal.GetReference(_span);
+ int length = _span.Length;
+ if ((uint)destination.Length < (uint)length)
+ throw new ArgumentException("The target span is too short to copy all the current items to.");
+
+ ref T destinationRef = ref MemoryMarshal.GetReference(destination);
+
+ CopyTo(ref sourceRef, ref destinationRef, (nint)(uint)length, (nint)(uint)_step);
+ }
+
+ ///
+ /// Attempts to copy the current instance to a destination .
+ ///
+ /// The target of the copy operation.
+ /// Whether or not the operation was successful.
+ public bool TryCopyTo(Span destination)
+ {
+ if (destination.Length >= _span.Length)
+ {
+ CopyTo(destination);
+ return true;
+ }
+
+ return false;
+ }
+
+ private static void CopyTo(ref T sourceRef, ref T destinationRef, nint length, nint sourceStep)
+ {
+ nint sourceOffset = 0;
+ nint destinationOffset = 0;
+
+ while (length >= 8)
+ {
+ Unsafe.Add(ref destinationRef, destinationOffset + 0) = Unsafe.Add(ref sourceRef, sourceOffset);
+ Unsafe.Add(ref destinationRef, destinationOffset + 1) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep);
+ Unsafe.Add(ref destinationRef, destinationOffset + 2) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep);
+ Unsafe.Add(ref destinationRef, destinationOffset + 3) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep);
+ Unsafe.Add(ref destinationRef, destinationOffset + 4) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep);
+ Unsafe.Add(ref destinationRef, destinationOffset + 5) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep);
+ Unsafe.Add(ref destinationRef, destinationOffset + 6) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep);
+ Unsafe.Add(ref destinationRef, destinationOffset + 7) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep);
+
+ length -= 8;
+ sourceOffset += sourceStep;
+ destinationOffset += 8;
+ }
+
+ if (length >= 4)
+ {
+ Unsafe.Add(ref destinationRef, destinationOffset + 0) = Unsafe.Add(ref sourceRef, sourceOffset);
+ Unsafe.Add(ref destinationRef, destinationOffset + 1) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep);
+ Unsafe.Add(ref destinationRef, destinationOffset + 2) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep);
+ Unsafe.Add(ref destinationRef, destinationOffset + 3) = Unsafe.Add(ref sourceRef, sourceOffset += sourceStep);
+
+ length -= 4;
+ sourceOffset += sourceStep;
+ destinationOffset += 4;
+ }
+
+ while (length > 0)
+ {
+ Unsafe.Add(ref destinationRef, destinationOffset) = Unsafe.Add(ref sourceRef, sourceOffset);
+
+ length -= 1;
+ sourceOffset += sourceStep;
+ destinationOffset += 1;
+ }
+ }
+
+ #endregion
+
+ ///
+ /// A custom enumerator type to traverse items within a instance.
+ ///
+ public ref struct Enumerator
+ {
+ #region Properties & Fields
+
+ ///
+ private readonly ReadOnlySpan _span;
+
+ ///
+ private readonly int _step;
+
+ ///
+ /// The current position in the sequence.
+ ///
+ private int _position;
+
+ ///
+ public readonly ref readonly T Current
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ ref T r0 = ref MemoryMarshal.GetReference(_span);
+
+ nint offset = (nint)(uint)_position * (nint)(uint)_step;
+ ref T ri = ref Unsafe.Add(ref r0, offset);
+
+ return ref ri;
+ }
+ }
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The instance with the info on the items to traverse.
+ /// The distance between items in the sequence to enumerate.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal Enumerator(ReadOnlySpan span, int step)
+ {
+ this._span = span;
+ this._step = step;
+
+ _position = -1;
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool MoveNext() => ++_position < _span.Length;
+
+ #endregion
+ }
+}
diff --git a/ScreenCapture.NET/DirectX/DX11ScreenCapture.cs b/ScreenCapture.NET/DirectX/DX11ScreenCapture.cs
deleted file mode 100644
index 8444acc..0000000
--- a/ScreenCapture.NET/DirectX/DX11ScreenCapture.cs
+++ /dev/null
@@ -1,536 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Runtime.CompilerServices;
-using System.Threading;
-using SharpGen.Runtime;
-using Vortice.Direct3D;
-using Vortice.Direct3D11;
-using Vortice.DXGI;
-using Vortice.Mathematics;
-using MapFlags = Vortice.Direct3D11.MapFlags;
-using ResultCode = Vortice.DXGI.ResultCode;
-
-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 DX11ScreenCapture : IScreenCapture
-{
- #region Constants
-
- private static readonly FeatureLevel[] FEATURE_LEVELS =
- {
- FeatureLevel.Level_11_1,
- FeatureLevel.Level_11_0,
- FeatureLevel.Level_10_1,
- FeatureLevel.Level_10_0
- };
-
- private const int BPP = 4;
-
- #endregion
-
- #region Properties & Fields
-
- private readonly object _captureLock = new();
-
- private readonly bool _useNewDuplicationAdapter;
- private int _indexCounter = 0;
-
- ///
- public Display Display { get; }
-
- ///
- /// Gets or sets the timeout in ms used for screen-capturing. (default 1000ms)
- /// This is used in https://docs.microsoft.com/en-us/windows/win32/api/dxgi1_2/nf-dxgi1_2-idxgioutputduplication-acquirenextframe
- ///
- // ReSharper disable once MemberCanBePrivate.Global
- public int Timeout { get; set; } = 1000;
-
- private readonly IDXGIFactory1 _factory;
-
- private IDXGIOutput? _output;
- private IDXGIOutputDuplication? _duplicatedOutput;
- private ID3D11Device? _device;
- private ID3D11DeviceContext? _context;
- private ID3D11Texture2D? _captureTexture;
-
- private readonly Dictionary _captureZones = new();
-
- #endregion
-
- #region Events
-
- ///
- public event EventHandler? Updated;
-
- #endregion
-
- #region Constructors
-
- ///
- /// Initializes a new instance of the class.
- ///
- ///
- /// Note that setting useNewDuplicationAdapter to true requires to call DPIAwareness.Initalize(); and prevents the capture from running in a WPF-thread.
- ///
- /// The used to create underlying objects.
- /// The to duplicate.
- /// Indicates if the DuplicateOutput1 interface should be used instead of the older DuplicateOutput. Currently there's no real use in setting this to true.
- public DX11ScreenCapture(IDXGIFactory1 factory, Display display, bool useNewDuplicationAdapter = false)
- {
- this._factory = factory;
- this.Display = display;
- this._useNewDuplicationAdapter = useNewDuplicationAdapter;
-
- Restart();
- }
-
- #endregion
-
- #region Methods
-
- ///
- public bool CaptureScreen()
- {
- bool result = false;
- lock (_captureLock)
- {
- if ((_context == null) || (_duplicatedOutput == null) || (_captureTexture == null))
- {
- Restart();
- return false;
- }
-
- try
- {
- IDXGIResource? screenResource = null;
- try
- {
- _duplicatedOutput.AcquireNextFrame(Timeout, out OutduplFrameInfo duplicateFrameInformation, out screenResource).CheckError();
- if ((screenResource == null) || (duplicateFrameInformation.LastPresentTime == 0)) return false;
-
- using ID3D11Texture2D screenTexture = screenResource.QueryInterface();
- _context.CopySubresourceRegion(_captureTexture, 0, 0, 0, 0, screenTexture, 0);
- }
- finally
- {
- try
- {
- screenResource?.Dispose();
- _duplicatedOutput?.ReleaseFrame();
- }
- catch { /**/ }
- }
-
- result = true;
- }
- catch (SharpGenException dxException)
- {
- if ((dxException.ResultCode == ResultCode.AccessLost)
- || (dxException.ResultCode == ResultCode.AccessDenied)
- || (dxException.ResultCode == ResultCode.InvalidCall))
- {
- try
- {
- Restart();
- }
- catch { Thread.Sleep(100); }
- }
- }
- catch { /**/ }
-
- try
- {
- UpdateZones();
- }
- catch { /**/ }
-
- try
- {
- Updated?.Invoke(this, new ScreenCaptureUpdatedEventArgs(result));
- }
- catch { /**/ }
-
- return result;
- }
- }
-
- private void UpdateZones()
- {
- if (_context == null) return;
-
- lock (_captureZones)
- {
- foreach ((CaptureZone captureZone, (ID3D11Texture2D stagingTexture, ID3D11Texture2D? scalingTexture, ID3D11ShaderResourceView? scalingTextureView)) in _captureZones.Where(z => z.Key.AutoUpdate || z.Key.IsUpdateRequested))
- {
- if (scalingTexture != null)
- {
- _context.CopySubresourceRegion(scalingTexture, 0, 0, 0, 0, _captureTexture, 0,
- new Box(captureZone.X, captureZone.Y, 0,
- captureZone.X + captureZone.UnscaledWidth,
- captureZone.Y + captureZone.UnscaledHeight, 1));
- _context.GenerateMips(scalingTextureView);
- _context.CopySubresourceRegion(stagingTexture, 0, 0, 0, 0, scalingTexture, captureZone.DownscaleLevel);
- }
- else
- _context.CopySubresourceRegion(stagingTexture, 0, 0, 0, 0, _captureTexture, 0,
- new Box(captureZone.X, captureZone.Y, 0,
- captureZone.X + captureZone.UnscaledWidth,
- captureZone.Y + captureZone.UnscaledHeight, 1));
-
- MappedSubresource mapSource = _context.Map(stagingTexture, 0, MapMode.Read, MapFlags.None);
- lock (captureZone.Buffer)
- {
- Span source = mapSource.AsSpan(mapSource.RowPitch * captureZone.Height);
- switch (Display.Rotation)
- {
- case Rotation.Rotation90:
- CopyRotate90(source, mapSource.RowPitch, captureZone);
- break;
-
- case Rotation.Rotation180:
- CopyRotate180(source, mapSource.RowPitch, captureZone);
- break;
-
- case Rotation.Rotation270:
- CopyRotate270(source, mapSource.RowPitch, captureZone);
- break;
-
- default:
- CopyRotate0(source, mapSource.RowPitch, captureZone);
- break;
- }
- }
-
- _context.Unmap(stagingTexture, 0);
- captureZone.SetUpdated();
- }
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void CopyRotate0(in Span source, int sourceStride, in CaptureZone captureZone)
- {
- int height = captureZone.Height;
- int stride = captureZone.Stride;
- Span target = captureZone.Buffer.AsSpan();
-
- for (int y = 0; y < height; y++)
- {
- int sourceOffset = y * sourceStride;
- int targetOffset = y * stride;
-
- source.Slice(sourceOffset, stride).CopyTo(target.Slice(targetOffset, stride));
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void CopyRotate90(in Span source, int sourceStride, in CaptureZone captureZone)
- {
- int width = captureZone.Width;
- int height = captureZone.Height;
- Span target = captureZone.Buffer.AsSpan();
-
- for (int y = 0; y < height; y++)
- for (int x = 0; x < width; x++)
- {
- int sourceOffset = ((y * sourceStride) + (x * BPP));
- int targetOffset = ((x * height) + ((height - 1) - y)) * BPP;
-
- target[targetOffset] = source[sourceOffset];
- target[targetOffset + 1] = source[sourceOffset + 1];
- target[targetOffset + 2] = source[sourceOffset + 2];
- target[targetOffset + 3] = source[sourceOffset + 3];
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void CopyRotate180(in Span source, int sourceStride, in CaptureZone captureZone)
- {
- int width = captureZone.Width;
- int height = captureZone.Height;
- int stride = captureZone.Stride;
- Span target = captureZone.Buffer.AsSpan();
-
- for (int y = 0; y < height; y++)
- for (int x = 0; x < width; x++)
- {
- int sourceOffset = ((y * sourceStride) + (x * BPP));
- int targetOffset = target.Length - ((y * stride) + (x * BPP)) - 1;
-
- target[targetOffset - 3] = source[sourceOffset];
- target[targetOffset - 2] = source[sourceOffset + 1];
- target[targetOffset - 1] = source[sourceOffset + 2];
- target[targetOffset] = source[sourceOffset + 3];
- }
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void CopyRotate270(in Span source, int sourceStride, in CaptureZone captureZone)
- {
- int width = captureZone.Width;
- int height = captureZone.Height;
- Span target = captureZone.Buffer.AsSpan();
-
- for (int y = 0; y < height; y++)
- for (int x = 0; x < width; x++)
- {
- int sourceOffset = ((y * sourceStride) + (x * BPP));
- int targetOffset = ((((width - 1) - x) * height) + y) * BPP;
-
- target[targetOffset] = source[sourceOffset];
- target[targetOffset + 1] = source[sourceOffset + 1];
- target[targetOffset + 2] = source[sourceOffset + 2];
- target[targetOffset + 3] = source[sourceOffset + 3];
- }
- }
-
- ///
- public CaptureZone RegisterCaptureZone(int x, int y, int width, int height, int downscaleLevel = 0)
- {
- ValidateCaptureZoneAndThrow(x, y, width, height);
-
- if (Display.Rotation is Rotation.Rotation90 or Rotation.Rotation270)
- (x, y, width, height) = (y, x, height, width);
-
- int unscaledWidth = width;
- int unscaledHeight = height;
- (width, height, downscaleLevel) = CalculateScaledSize(unscaledWidth, unscaledHeight, downscaleLevel);
-
- byte[] buffer = new byte[width * height * BPP];
-
- CaptureZone captureZone = new(_indexCounter++, x, y, width, height, BPP, downscaleLevel, unscaledWidth, unscaledHeight, buffer);
- lock (_captureZones)
- InitializeCaptureZone(captureZone);
-
- return captureZone;
- }
-
- ///
- public bool UnregisterCaptureZone(CaptureZone captureZone)
- {
- lock (_captureZones)
- {
- if (_captureZones.TryGetValue(captureZone, out (ID3D11Texture2D stagingTexture, ID3D11Texture2D? scalingTexture, ID3D11ShaderResourceView? _scalingTextureView) data))
- {
- _captureZones.Remove(captureZone);
- data.stagingTexture.Dispose();
- data.scalingTexture?.Dispose();
- data._scalingTextureView?.Dispose();
-
- return true;
- }
-
- return false;
- }
- }
-
- ///
- public void UpdateCaptureZone(CaptureZone captureZone, int? x = null, int? y = null, int? width = null, int? height = null, int? downscaleLevel = null)
- {
- lock (_captureZones)
- if (!_captureZones.ContainsKey(captureZone))
- throw new ArgumentException("The capture zone is not registered to this ScreenCapture", nameof(captureZone));
-
- int newX = x ?? captureZone.X;
- int newY = y ?? captureZone.Y;
- int newUnscaledWidth = width ?? captureZone.UnscaledWidth;
- int newUnscaledHeight = height ?? captureZone.UnscaledHeight;
- int newDownscaleLevel = downscaleLevel ?? captureZone.DownscaleLevel;
-
- ValidateCaptureZoneAndThrow(newX, newY, newUnscaledWidth, newUnscaledHeight);
-
- if (Display.Rotation is Rotation.Rotation90 or Rotation.Rotation270)
- (newX, newY, newUnscaledWidth, newUnscaledHeight) = (newY, newX, newUnscaledHeight, newUnscaledWidth);
-
- captureZone.X = newX;
- captureZone.Y = newY;
-
- //TODO DarthAffe 01.05.2022: For now just reinitialize the zone in that case, but this could be optimized to only recreate the textures needed.
- if ((width != null) || (height != null) || (downscaleLevel != null))
- {
- (int newWidth, int newHeight, newDownscaleLevel) = CalculateScaledSize(newUnscaledWidth, newUnscaledHeight, newDownscaleLevel);
- lock (_captureZones)
- {
- UnregisterCaptureZone(captureZone);
-
- captureZone.UnscaledWidth = newUnscaledWidth;
- captureZone.UnscaledHeight = newUnscaledHeight;
- captureZone.Width = newWidth;
- captureZone.Height = newHeight;
- captureZone.DownscaleLevel = newDownscaleLevel;
- captureZone.Buffer = new byte[newWidth * newHeight * BPP];
-
- InitializeCaptureZone(captureZone);
- }
- }
- }
-
- private (int width, int height, int downscaleLevel) CalculateScaledSize(int width, int height, int downscaleLevel)
- {
- if (downscaleLevel > 0)
- for (int i = 0; i < downscaleLevel; i++)
- {
- if ((width <= 1) && (height <= 1))
- {
- downscaleLevel = i;
- break;
- }
-
- width /= 2;
- height /= 2;
- }
-
- if (width < 1) width = 1;
- if (height < 1) height = 1;
-
- return (width, height, downscaleLevel);
- }
-
- private void ValidateCaptureZoneAndThrow(int x, int y, int width, int height)
- {
- if (_device == null) throw new ApplicationException("ScreenCapture isn't initialized.");
-
- if (x < 0) throw new ArgumentException("x < 0");
- if (y < 0) throw new ArgumentException("y < 0");
- if (width <= 0) throw new ArgumentException("with <= 0");
- if (height <= 0) throw new ArgumentException("height <= 0");
- if ((x + width) > Display.Width) throw new ArgumentException("x + width > Display width");
- if ((y + height) > Display.Height) throw new ArgumentException("y + height > Display height");
- }
-
- private void InitializeCaptureZone(in CaptureZone captureZone)
- {
- Texture2DDescription stagingTextureDesc = new()
- {
- CPUAccessFlags = CpuAccessFlags.Read,
- BindFlags = BindFlags.None,
- Format = Format.B8G8R8A8_UNorm,
- Width = captureZone.Width,
- Height = captureZone.Height,
- MiscFlags = ResourceOptionFlags.None,
- MipLevels = 1,
- ArraySize = 1,
- SampleDescription = { Count = 1, Quality = 0 },
- Usage = ResourceUsage.Staging
- };
- ID3D11Texture2D stagingTexture = _device!.CreateTexture2D(stagingTextureDesc);
-
- ID3D11Texture2D? scalingTexture = null;
- ID3D11ShaderResourceView? scalingTextureView = null;
- if (captureZone.DownscaleLevel > 0)
- {
- Texture2DDescription scalingTextureDesc = new()
- {
- CPUAccessFlags = CpuAccessFlags.None,
- BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource,
- Format = Format.B8G8R8A8_UNorm,
- Width = captureZone.UnscaledWidth,
- Height = captureZone.UnscaledHeight,
- MiscFlags = ResourceOptionFlags.GenerateMips,
- MipLevels = captureZone.DownscaleLevel + 1,
- ArraySize = 1,
- SampleDescription = { Count = 1, Quality = 0 },
- Usage = ResourceUsage.Default
- };
- scalingTexture = _device!.CreateTexture2D(scalingTextureDesc);
- scalingTextureView = _device.CreateShaderResourceView(scalingTexture);
- }
-
- _captureZones[captureZone] = (stagingTexture, scalingTexture, scalingTextureView);
- }
-
- ///
- public void Restart()
- {
- lock (_captureLock)
- {
- try
- {
- List captureZones = _captureZones.Keys.ToList();
- Dispose();
-
- using IDXGIAdapter1 adapter = _factory.GetAdapter1(Display.GraphicsCard.Index) ?? throw new ApplicationException("Couldn't create DirectX-Adapter.");
-
- D3D11.D3D11CreateDevice(adapter, DriverType.Unknown, DeviceCreationFlags.None, FEATURE_LEVELS, out _device).CheckError();
- _context = _device!.ImmediateContext;
-
- _output = adapter.GetOutput(Display.Index) ?? throw new ApplicationException("Couldn't get DirectX-Output.");
- using IDXGIOutput5 output = _output.QueryInterface();
-
- int width = Display.Width;
- int height = Display.Height;
- if (Display.Rotation is Rotation.Rotation90 or Rotation.Rotation270)
- (width, height) = (height, width);
-
- Texture2DDescription captureTextureDesc = new()
- {
- CPUAccessFlags = CpuAccessFlags.None,
- BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource,
- Format = Format.B8G8R8A8_UNorm,
- Width = width,
- Height = height,
- MiscFlags = ResourceOptionFlags.None,
- MipLevels = 1,
- ArraySize = 1,
- SampleDescription = { Count = 1, Quality = 0 },
- Usage = ResourceUsage.Default
- };
- _captureTexture = _device.CreateTexture2D(captureTextureDesc);
-
- lock (_captureZones)
- {
- foreach (CaptureZone captureZone in captureZones)
- InitializeCaptureZone(captureZone);
- }
-
- if (_useNewDuplicationAdapter)
- _duplicatedOutput = output.DuplicateOutput1(_device, new[] { Format.B8G8R8A8_UNorm }); // DarthAffe 27.02.2021: This prepares for the use of 10bit color depth
- else
- _duplicatedOutput = output.DuplicateOutput(_device);
- }
- catch { Dispose(false); }
- }
- }
-
- ///
- public void Dispose() => Dispose(true);
-
- private void Dispose(bool removeCaptureZones)
- {
- try
- {
- lock (_captureLock)
- {
- try { _duplicatedOutput?.Dispose(); } catch { /**/ }
- _duplicatedOutput = null;
-
- try
- {
- if (removeCaptureZones)
- {
- List captureZones = _captureZones.Keys.ToList();
- foreach (CaptureZone captureZone in captureZones)
- UnregisterCaptureZone(captureZone);
- }
- }
- catch { /**/ }
-
- try { _output?.Dispose(); } catch { /**/ }
- try { _context?.Dispose(); } catch { /**/ }
- try { _device?.Dispose(); } catch { /**/ }
- try { _captureTexture?.Dispose(); } catch { /**/ }
- _context = null;
- _captureTexture = null;
- }
- }
- catch { /**/ }
- }
-
- #endregion
-}
\ No newline at end of file
diff --git a/ScreenCapture.NET/Extensions/BlackBarDetection.cs b/ScreenCapture.NET/Extensions/BlackBarDetection.cs
new file mode 100644
index 0000000..4042fee
--- /dev/null
+++ b/ScreenCapture.NET/Extensions/BlackBarDetection.cs
@@ -0,0 +1,188 @@
+namespace ScreenCapture.NET;
+
+///
+/// Helper-class for black-bar removal.
+///
+public static class BlackBarDetection
+{
+ #region IImage
+
+ ///
+ /// Create an image with black bars removed
+ ///
+ /// The image the bars are removed from.
+ /// The threshold of "blackness" used to detect black bars. (e. g. Threshold 5 will consider a pixel of color [5,5,5] as black.)
+ /// A bool indicating if black bars should be removed at the top of the image.
+ /// A bool indicating if black bars should be removed at the bottom of the image.
+ /// A bool indicating if black bars should be removed on the left side of the image.
+ /// A bool indicating if black bars should be removed on the right side of the image.
+ /// The image with black bars removed.
+ public static IImage RemoveBlackBars(this IImage image, int threshold = 0, bool removeTop = true, bool removeBottom = true, bool removeLeft = true, bool removeRight = true)
+ {
+ int top = removeTop ? CalculateTop(image, threshold) : 0;
+ int bottom = removeBottom ? CalculateBottom(image, threshold) : image.Height;
+ int left = removeLeft ? CalculateLeft(image, threshold) : 0;
+ int right = removeRight ? CalculateRight(image, threshold) : image.Width;
+
+ return image[left, top, right - left, bottom - top];
+ }
+
+ private static int CalculateTop(IImage image, int threshold)
+ {
+ IImage.IImageRows rows = image.Rows;
+ for (int y = 0; y < rows.Count; y++)
+ {
+ IImage.IImageRow row = rows[y];
+ foreach (IColor color in row)
+ {
+ if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
+ return y;
+ }
+ }
+
+ return 0;
+ }
+
+ private static int CalculateBottom(IImage image, int threshold)
+ {
+ IImage.IImageRows rows = image.Rows;
+ for (int y = rows.Count - 1; y >= 0; y--)
+ {
+ IImage.IImageRow row = rows[y];
+ foreach (IColor color in row)
+ {
+ if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
+ return y;
+ }
+ }
+
+ return rows.Count;
+ }
+
+ private static int CalculateLeft(IImage image, int threshold)
+ {
+ IImage.IImageColumns columns = image.Columns;
+ for (int x = 0; x < columns.Count; x++)
+ {
+ IImage.IImageColumn column = columns[x];
+ foreach (IColor color in column)
+ {
+ if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
+ return x;
+ }
+ }
+
+ return 0;
+ }
+
+ private static int CalculateRight(IImage image, int threshold)
+ {
+ IImage.IImageColumns columns = image.Columns;
+ for (int x = columns.Count - 1; x >= 0; x--)
+ {
+ IImage.IImageColumn column = columns[x];
+ foreach (IColor color in column)
+ {
+ if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
+ return x;
+ }
+ }
+
+ return columns.Count;
+ }
+
+ #endregion
+
+ #region RefImage
+
+ ///
+ /// Create an image with black bars removed
+ ///
+ /// The image the bars are removed from.
+ /// The threshold of "blackness" used to detect black bars. (e. g. Threshold 5 will consider a pixel of color [5,5,5] as black.)
+ /// A bool indicating if black bars should be removed at the top of the image.
+ /// A bool indicating if black bars should be removed at the bottom of the image.
+ /// A bool indicating if black bars should be removed on the left side of the image.
+ /// A bool indicating if black bars should be removed on the right side of the image.
+ /// The image with black bars removed.
+ public static RefImage RemoveBlackBars(this RefImage image, int threshold = 0, bool removeTop = true, bool removeBottom = true, bool removeLeft = true, bool removeRight = true)
+ where TColor : struct, IColor
+ {
+ int top = removeTop ? CalculateTop(image, threshold) : 0;
+ int bottom = removeBottom ? CalculateBottom(image, threshold) : image.Height;
+ int left = removeLeft ? CalculateLeft(image, threshold) : 0;
+ int right = removeRight ? CalculateRight(image, threshold) : image.Width;
+
+ return image[left, top, right - left, bottom - top];
+ }
+
+ private static int CalculateTop(this RefImage image, int threshold)
+ where TColor : struct, IColor
+ {
+ RefImage.ImageRows rows = image.Rows;
+ for (int y = 0; y < rows.Count; y++)
+ {
+ ReadOnlyRefEnumerable row = rows[y];
+ foreach (TColor color in row)
+ {
+ if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
+ return y;
+ }
+ }
+
+ return 0;
+ }
+
+ private static int CalculateBottom(this RefImage image, int threshold)
+ where TColor : struct, IColor
+ {
+ RefImage.ImageRows rows = image.Rows;
+ for (int y = rows.Count - 1; y >= 0; y--)
+ {
+ ReadOnlyRefEnumerable row = rows[y];
+ foreach (TColor color in row)
+ {
+ if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
+ return y;
+ }
+ }
+
+ return rows.Count;
+ }
+
+ private static int CalculateLeft(this RefImage image, int threshold)
+ where TColor : struct, IColor
+ {
+ RefImage.ImageColumns columns = image.Columns;
+ for (int x = 0; x < columns.Count; x++)
+ {
+ ReadOnlyRefEnumerable column = columns[x];
+ foreach (TColor color in column)
+ {
+ if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
+ return x;
+ }
+ }
+
+ return 0;
+ }
+
+ private static int CalculateRight(this RefImage image, int threshold)
+ where TColor : struct, IColor
+ {
+ RefImage.ImageColumns columns = image.Columns;
+ for (int x = columns.Count - 1; x >= 0; x--)
+ {
+ ReadOnlyRefEnumerable column = columns[x];
+ foreach (TColor color in column)
+ {
+ if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
+ return x;
+ }
+ }
+
+ return columns.Count;
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/ScreenCapture.NET/Generic/AbstractScreenCapture.cs b/ScreenCapture.NET/Generic/AbstractScreenCapture.cs
new file mode 100644
index 0000000..297d7e6
--- /dev/null
+++ b/ScreenCapture.NET/Generic/AbstractScreenCapture.cs
@@ -0,0 +1,247 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace ScreenCapture.NET;
+
+///
+public abstract class AbstractScreenCapture : IScreenCapture
+ where TColor : struct, IColor
+{
+ #region Properties & Fields
+
+ private bool _isDisposed;
+
+ ///
+ /// Gets a list of registered on this ScreenCature.
+ ///
+ protected HashSet> CaptureZones { get; } = new();
+
+ ///
+ public Display Display { get; }
+
+ #endregion
+
+ #region Events
+
+ ///
+ public event EventHandler? Updated;
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The to duplicate.
+ protected AbstractScreenCapture(Display display)
+ {
+ this.Display = display;
+ }
+
+ ~AbstractScreenCapture() => Dispose(false);
+
+ #endregion
+
+ #region Methods
+
+ ///
+ public virtual bool CaptureScreen()
+ {
+ if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
+
+ bool result;
+
+ try
+ {
+ result = PerformScreenCapture();
+ }
+ catch
+ {
+ result = false;
+ }
+
+ lock (CaptureZones)
+ foreach (CaptureZone captureZone in CaptureZones.Where(x => x.AutoUpdate || x.IsUpdateRequested))
+ {
+ try
+ {
+ PerformCaptureZoneUpdate(captureZone, captureZone.InternalBuffer);
+ captureZone.SetUpdated();
+ }
+ catch { /* */ }
+ }
+
+ OnUpdated(result);
+
+ return result;
+ }
+
+ ///
+ /// Performs the actual screen capture.
+ ///
+ /// true if the screen was captured sucessfully; otherwise, false.
+ protected abstract bool PerformScreenCapture();
+
+ ///
+ /// Performs an update of the given capture zone.
+ ///
+ /// The capture zone to update.
+ /// The buffer containing the current pixel-data of the capture zone.
+ protected abstract void PerformCaptureZoneUpdate(CaptureZone captureZone, in Span buffer);
+
+ ///
+ /// Raises the -event.
+ ///
+ /// A bool indicating whether the update was successful or not.
+ protected virtual void OnUpdated(bool result)
+ {
+ try
+ {
+ Updated?.Invoke(this, new ScreenCaptureUpdatedEventArgs(result));
+ }
+ catch { /**/ }
+ }
+
+ ///
+ ICaptureZone IScreenCapture.RegisterCaptureZone(int x, int y, int width, int height, int downscaleLevel) => RegisterCaptureZone(x, y, width, height, downscaleLevel);
+
+ ///
+ public virtual CaptureZone RegisterCaptureZone(int x, int y, int width, int height, int downscaleLevel = 0)
+ {
+ if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
+
+ lock (CaptureZones)
+ {
+ ValidateCaptureZoneAndThrow(x, y, width, height, downscaleLevel);
+
+ int unscaledWidth = width;
+ int unscaledHeight = height;
+ (width, height, downscaleLevel) = CalculateScaledSize(unscaledWidth, unscaledHeight, downscaleLevel);
+
+ CaptureZone captureZone = new(Display, x, y, width, height, downscaleLevel, unscaledWidth, unscaledHeight);
+ CaptureZones.Add(captureZone);
+
+ return captureZone;
+ }
+ }
+
+ ///
+ /// Validates the given values of a capture zone.
+ ///
+ /// The X-location of the zone.
+ /// The Y-location of the zone.
+ /// The width of the zone.
+ /// The height of the zone.
+ /// The downscale-level of the zone.
+ /// Throws if some of the provided data is not valid.
+ protected virtual void ValidateCaptureZoneAndThrow(int x, int y, int width, int height, int downscaleLevel)
+ {
+ if (x < 0) throw new ArgumentException("x < 0");
+ if (y < 0) throw new ArgumentException("y < 0");
+ if (width <= 0) throw new ArgumentException("with <= 0");
+ if (height <= 0) throw new ArgumentException("height <= 0");
+ if ((x + width) > Display.Width) throw new ArgumentException("x + width > Display width");
+ if ((y + height) > Display.Height) throw new ArgumentException("y + height > Display height");
+ }
+
+ ///
+ /// Calculates the actual size when downscaling is used.
+ ///
+ /// The original width.
+ /// The original height.
+ /// The level of downscaling to be used.
+ /// A tuple containing the scaled width, the scaled height and the downscale-level used. (This can be smaller then the one provided if the image is not big enough to scale down that often.)
+ protected virtual (int width, int height, int downscaleLevel) CalculateScaledSize(int width, int height, int downscaleLevel)
+ {
+ if (downscaleLevel > 0)
+ for (int i = 0; i < downscaleLevel; i++)
+ {
+ if ((width <= 1) && (height <= 1))
+ {
+ downscaleLevel = i;
+ break;
+ }
+
+ width /= 2;
+ height /= 2;
+ }
+
+ if (width < 1) width = 1;
+ if (height < 1) height = 1;
+
+ return (width, height, downscaleLevel);
+ }
+
+ ///
+ bool IScreenCapture.UnregisterCaptureZone(ICaptureZone captureZone) => UnregisterCaptureZone(captureZone as CaptureZone ?? throw new ArgumentException("Invalid capture-zone."));
+
+ ///
+ public virtual bool UnregisterCaptureZone(CaptureZone captureZone)
+ {
+ if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
+
+ return CaptureZones.Remove(captureZone);
+ }
+
+ ///
+ void IScreenCapture.UpdateCaptureZone(ICaptureZone captureZone, int? x, int? y, int? width, int? height, int? downscaleLevel)
+ => UpdateCaptureZone(captureZone as CaptureZone ?? throw new ArgumentException("Invalid capture-zone."), x, y, width, height, downscaleLevel);
+
+ ///
+ public virtual void UpdateCaptureZone(CaptureZone captureZone, int? x = null, int? y = null, int? width = null, int? height = null, int? downscaleLevel = null)
+ {
+ if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
+
+ lock (CaptureZones)
+ {
+ if (!CaptureZones.Contains(captureZone))
+ throw new ArgumentException("The capture zone is not registered to this ScreenCapture", nameof(captureZone));
+
+ int newX = x ?? captureZone.X;
+ int newY = y ?? captureZone.Y;
+ int newUnscaledWidth = width ?? captureZone.UnscaledWidth;
+ int newUnscaledHeight = height ?? captureZone.UnscaledHeight;
+ int newDownscaleLevel = downscaleLevel ?? captureZone.DownscaleLevel;
+
+ ValidateCaptureZoneAndThrow(newX, newY, newUnscaledWidth, newUnscaledHeight, newDownscaleLevel);
+
+ captureZone.X = newX;
+ captureZone.Y = newY;
+
+ if ((width != null) || (height != null) || (downscaleLevel != null))
+ {
+ (int newWidth, int newHeight, newDownscaleLevel) = CalculateScaledSize(newUnscaledWidth, newUnscaledHeight, newDownscaleLevel);
+ captureZone.Resize(newWidth, newHeight, newDownscaleLevel, newUnscaledWidth, newUnscaledHeight);
+ }
+ }
+ }
+
+ ///
+ public virtual void Restart()
+ {
+ if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (_isDisposed) return;
+
+ try
+ {
+ Dispose(true);
+ }
+ catch { /* don't throw in dispose! */ }
+
+ GC.SuppressFinalize(this);
+
+ _isDisposed = true;
+ }
+
+ ///
+ protected virtual void Dispose(bool disposing) { }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/ScreenCapture.NET/IScreenCapture.cs b/ScreenCapture.NET/Generic/IScreenCapture.cs
similarity index 90%
rename from ScreenCapture.NET/IScreenCapture.cs
rename to ScreenCapture.NET/Generic/IScreenCapture.cs
index 362836f..cc423bd 100644
--- a/ScreenCapture.NET/IScreenCapture.cs
+++ b/ScreenCapture.NET/Generic/IScreenCapture.cs
@@ -32,14 +32,14 @@ public interface IScreenCapture : IDisposable
/// The height of the region to capture (must be >= 0 and this + y must be <= screen-height).
/// The level of downscaling applied to the image of this region before copying to local memory. The calculation is (width and height)/2^downscaleLevel.
/// The new .
- CaptureZone RegisterCaptureZone(int x, int y, int width, int height, int downscaleLevel = 0);
+ ICaptureZone RegisterCaptureZone(int x, int y, int width, int height, int downscaleLevel = 0);
///
/// Removes the given from the .
///
/// The previously registered .
/// true if the was successfully removed; otherwise, false.
- bool UnregisterCaptureZone(CaptureZone captureZone);
+ bool UnregisterCaptureZone(ICaptureZone captureZone);
///
/// Updates the the given .
@@ -53,7 +53,7 @@ public interface IScreenCapture : IDisposable
/// The width of the region to capture (must be >= 0 and this + x must be <= screen-width).
/// The new height of the region to capture (must be >= 0 and this + y must be <= screen-height).
/// The new level of downscaling applied to the image of this region before copying to local memory. The calculation is (width and height)/2^downscaleLevel.
- void UpdateCaptureZone(CaptureZone captureZone, int? x = null, int? y = null, int? width = null, int? height = null, int? downscaleLevel = null);
+ void UpdateCaptureZone(ICaptureZone captureZone, int? x = null, int? y = null, int? width = null, int? height = null, int? downscaleLevel = null);
///
/// Restarts the .
diff --git a/ScreenCapture.NET/IScreenCaptureService.cs b/ScreenCapture.NET/Generic/IScreenCaptureService.cs
similarity index 100%
rename from ScreenCapture.NET/IScreenCaptureService.cs
rename to ScreenCapture.NET/Generic/IScreenCaptureService.cs
diff --git a/ScreenCapture.NET/Model/BlackBarDetection.cs b/ScreenCapture.NET/Model/BlackBarDetection.cs
deleted file mode 100644
index 254a57c..0000000
--- a/ScreenCapture.NET/Model/BlackBarDetection.cs
+++ /dev/null
@@ -1,141 +0,0 @@
-// ReSharper disable MemberCanBePrivate.Global
-
-using System;
-
-namespace ScreenCapture.NET;
-
-///
-/// Represents the configuration for the detection and removal of black bars around the screen image.
-///
-public sealed class BlackBarDetection
-{
- #region Properties & Fields
-
- private readonly CaptureZone _captureZone;
-
- private int? _top;
- ///
- /// Gets the size of the detected black bar at the top of the image.
- ///
- public int Top => _top ??= CalculateTop();
-
- private int? _bottom;
- ///
- /// Gets the size of the detected black bar at the bottom of the image.
- ///
- public int Bottom => _bottom ??= CalculateBottom();
-
- private int? _left;
- ///
- /// Gets the size of the detected black bar at the left of the image.
- ///
- public int Left => _left ??= CalculateLeft();
-
- private int? _right;
- ///
- /// Gets the size of the detected black bar at the right of the image.
- ///
- public int Right => _right ??= CalculateRight();
-
- private int _theshold = 0;
- ///
- /// Gets or sets the threshold of "blackness" used to detect black bars. (e. g. Threshold 5 will consider a pixel of color [5,5,5] as black.) (default 0)
- ///
- public int Threshold
- {
- get => _theshold;
- set
- {
- _theshold = value;
- InvalidateCache();
- }
- }
-
- #endregion
-
- #region Constructors
-
- internal BlackBarDetection(CaptureZone captureZone)
- {
- this._captureZone = captureZone;
- }
-
- #endregion
-
- #region Methods
-
- ///
- /// Invalidates the cached values and recalculates , , and .
- ///
- public void InvalidateCache()
- {
- _top = null;
- _bottom = null;
- _left = null;
- _right = null;
- }
-
- private int CalculateTop()
- {
- int threshold = Threshold;
- int stride = _captureZone.Stride;
- for (int row = 0; row < _captureZone.Height; row++)
- {
- Span data = new(_captureZone.Buffer, row * stride, stride);
- for (int i = 0; i < data.Length; i += 4)
- if ((data[i] > threshold) || (data[i + 1] > threshold) || (data[i + 2] > threshold))
- return row;
- }
-
- return 0;
- }
-
- private int CalculateBottom()
- {
- int threshold = Threshold;
- int stride = _captureZone.Stride;
- for (int row = _captureZone.Height - 1; row >= 0; row--)
- {
- Span data = new(_captureZone.Buffer, row * stride, stride);
- for (int i = 0; i < data.Length; i += 4)
- if ((data[i] > threshold) || (data[i + 1] > threshold) || (data[i + 2] > threshold))
- return (_captureZone.Height - 1) - row;
- }
-
- return 0;
- }
-
- private int CalculateLeft()
- {
- int threshold = Threshold;
- int stride = _captureZone.Stride;
- byte[] buffer = _captureZone.Buffer;
- for (int column = 0; column < _captureZone.Width; column++)
- for (int row = 0; row < _captureZone.Height; row++)
- {
- int offset = (stride * row) + (column * 4);
- if ((buffer[offset] > threshold) || (buffer[offset + 1] > threshold) || (buffer[offset + 2] > threshold))
- return column;
- }
-
- return 0;
- }
-
- private int CalculateRight()
- {
- int threshold = Threshold;
- int stride = _captureZone.Stride;
- byte[] buffer = _captureZone.Buffer;
- for (int column = _captureZone.Width - 1; column >= 0; column--)
- for (int row = 0; row < _captureZone.Height; row++)
- {
- int offset = (stride * row) + (column * 4);
- if ((buffer[offset] > threshold) || (buffer[offset + 1] > threshold) || (buffer[offset + 2] > threshold))
- return (_captureZone.Width - 1) - column;
- }
-
- return 0;
- }
-
- #endregion
-}
\ No newline at end of file
diff --git a/ScreenCapture.NET/Model/CaptureZone.cs b/ScreenCapture.NET/Model/CaptureZone.cs
index e33d59e..fca089c 100644
--- a/ScreenCapture.NET/Model/CaptureZone.cs
+++ b/ScreenCapture.NET/Model/CaptureZone.cs
@@ -1,93 +1,105 @@
// ReSharper disable MemberCanBePrivate.Global
using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Threading;
namespace ScreenCapture.NET;
///
/// Represents a duplicated region on the screen.
///
-public sealed class CaptureZone
+public sealed class CaptureZone : ICaptureZone
+ where TColor : struct, IColor
{
#region Properties & Fields
- ///
- /// Gets the unique id of this .
- ///
- public int Id { get; }
+ private readonly object _lock = new();
- ///
- /// Gets the x-location of the region on the screen.
- ///
+ ///
+ public Display Display { get; }
+
+ ///
+ public ColorFormat ColorFormat
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => TColor.ColorFormat;
+ }
+
+ ///
public int X { get; internal set; }
- ///
- /// Gets the y-location of the region on the screen.
- ///
+ ///
public int Y { get; internal set; }
- ///
- /// Gets the width of the captured region.
- ///
- public int Width { get; internal set; }
+ ///
+ public int Width { get; private set; }
+
+ ///
+ public int Height { get; private set; }
+
+ ///
+ public int Stride
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => Width * ColorFormat.BytesPerPixel;
+ }
+
+ ///
+ public int DownscaleLevel { get; private set; }
+
+ ///
+ public int UnscaledWidth { get; private set; }
+
+ ///
+ public int UnscaledHeight { get; private set; }
+
+ internal byte[] InternalBuffer { get; set; }
+
+ ///
+ public ReadOnlySpan RawBuffer
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => InternalBuffer;
+ }
///
- /// Gets the height of the captured region.
+ /// Gets the pixel-data of this zone.
///
- public int Height { get; internal set; }
+ public ReadOnlySpan Pixels
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => MemoryMarshal.Cast(RawBuffer);
+ }
///
- /// Gets the level of downscaling applied to the image of this region before copying to local memory. The calculation is (width and height)/2^downscaleLevel.
+ /// Gets a . Basically the same as but with better performance.
///
- public int DownscaleLevel { get; internal set; }
+ public RefImage Image
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => new(Pixels, 0, 0, Width, Height, Width);
+ }
- ///
- /// Gets the original width of the region (this equals if is 0).
- ///
- public int UnscaledWidth { get; internal set; }
+ ///
+ IImage ICaptureZone.Image
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => new Image(InternalBuffer, 0, 0, Width, Height, Width);
+ }
- ///
- /// Gets the original height of the region (this equals if is 0).
- ///
- public int UnscaledHeight { get; internal set; }
-
- ///
- /// Gets the amount of bytes per pixel in the image (most likely 3 [RGB] or 4 [ARGB]).
- ///
- public int BytesPerPixel { get; }
-
- ///
- /// Gets the size in bytes of a row in the region ( * ).
- ///
- public int Stride => Width * BytesPerPixel;
-
- ///
- /// Gets the buffer containing the image data. Format depends on the specific capture but is most likely BGRA32.
- ///
- public byte[] Buffer { get; internal set; }
-
- ///
- /// Gets the config for black-bar detection.
- ///
- public BlackBarDetection BlackBars { get; }
-
- ///
- /// Gets or sets if the should be automatically updated on every captured frame.
- ///
+ ///
public bool AutoUpdate { get; set; } = true;
- ///
- /// Gets if an update for the is requested on the next captured frame.
- ///
+ ///
public bool IsUpdateRequested { get; private set; }
#endregion
#region Events
- ///
- /// Occurs when the is updated.
- ///
+ ///
public event EventHandler? Updated;
#endregion
@@ -95,9 +107,9 @@ public sealed class CaptureZone
#region Constructors
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
- /// The unique id of this .
+ /// The unique id of this .
/// The x-location of the region on the screen.
/// The y-location of the region on the screen.
/// The width of the region on the screen.
@@ -107,56 +119,97 @@ public sealed class CaptureZone
/// The original width of the region.
/// The original height of the region
/// The buffer containing the image data.
- internal CaptureZone(int id, int x, int y, int width, int height, int bytesPerPixel, int downscaleLevel, int unscaledWidth, int unscaledHeight, byte[] buffer)
+ internal CaptureZone(Display display, int x, int y, int width, int height, int downscaleLevel, int unscaledWidth, int unscaledHeight)
{
- this.Id = id;
+ this.Display = display;
this.X = x;
this.Y = y;
this.Width = width;
this.Height = height;
- this.BytesPerPixel = bytesPerPixel;
+ this.DownscaleLevel = downscaleLevel;
this.UnscaledWidth = unscaledWidth;
this.UnscaledHeight = unscaledHeight;
- this.DownscaleLevel = downscaleLevel;
- this.Buffer = buffer;
- BlackBars = new BlackBarDetection(this);
+ InternalBuffer = new byte[Stride * Height];
}
#endregion
#region Methods
- ///
- /// Requests to update this when the next frame is captured.
- /// Only necessary if is set to false.
- ///
+ ///
+ public RefImage GetRefImage()
+ where T : struct, IColor
+ {
+ if (typeof(T) != typeof(TColor)) throw new ArgumentException("The requested Color-Format does not match the data.", nameof(T));
+
+ return new RefImage(MemoryMarshal.Cast(RawBuffer), 0, 0, Width, Height, Width);
+ }
+
+ ///
+ public IDisposable Lock()
+ {
+ Monitor.Enter(_lock);
+ return new UnlockDisposable(_lock);
+ }
+
+ ///
public void RequestUpdate() => IsUpdateRequested = true;
///
- /// Marks the as updated.
- /// WARNING: This should not be called outside of an !
+ /// Marks the as updated.
///
- public void SetUpdated()
+ internal void SetUpdated()
{
IsUpdateRequested = false;
- BlackBars.InvalidateCache();
Updated?.Invoke(this, EventArgs.Empty);
}
- ///
- /// Determines whether this equals the given one.
- ///
- /// The to compare.
- /// true if the specified object is equal to the current object; otherwise, false.
- public bool Equals(CaptureZone other) => Id == other.Id;
+ internal void Resize(int width, int height, int downscaleLevel, int unscaledWidth, int unscaledHeight)
+ {
+ Width = width;
+ Height = height;
+ DownscaleLevel = downscaleLevel;
+ UnscaledWidth = unscaledWidth;
+ UnscaledHeight = unscaledHeight;
- ///
- public override bool Equals(object? obj) => obj is CaptureZone other && Equals(other);
-
- ///
- public override int GetHashCode() => Id;
+ int newBufferSize = Stride * Height;
+ if (newBufferSize != InternalBuffer.Length)
+ InternalBuffer = new byte[newBufferSize];
+ }
#endregion
+
+ private class UnlockDisposable : IDisposable
+ {
+ #region Properties & Fields
+
+ private bool _disposed = false;
+ private readonly object _lock;
+
+ #endregion
+
+ #region Constructors
+
+ public UnlockDisposable(object @lock) => this._lock = @lock;
+ ~UnlockDisposable() => Dispose();
+
+ #endregion
+
+ #region Methods
+
+ ///
+ public void Dispose()
+ {
+ if (_disposed) throw new ObjectDisposedException("The lock is already released");
+
+ Monitor.Exit(_lock);
+ _disposed = true;
+
+ GC.SuppressFinalize(this);
+ }
+
+ #endregion
+ }
}
\ No newline at end of file
diff --git a/ScreenCapture.NET/Model/Colors/ColorABGR.cs b/ScreenCapture.NET/Model/Colors/ColorABGR.cs
new file mode 100644
index 0000000..00cfa36
--- /dev/null
+++ b/ScreenCapture.NET/Model/Colors/ColorABGR.cs
@@ -0,0 +1,66 @@
+// ReSharper disable ConvertToAutoProperty
+
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace ScreenCapture.NET;
+
+///
+/// Represents a color in 32 bit ABGR-format.
+///
+[DebuggerDisplay("[A: {A}, R: {R}, G: {G}, B: {B}]")]
+[StructLayout(LayoutKind.Sequential)]
+public readonly struct ColorABGR : IColor
+{
+ #region Properties & Fields
+
+ ///
+ public static ColorFormat ColorFormat => ColorFormat.ABGR;
+
+ private readonly byte _a;
+ private readonly byte _b;
+ private readonly byte _g;
+ private readonly byte _r;
+
+ // ReSharper disable ConvertToAutoPropertyWhenPossible
+ ///
+ public byte A => _a;
+
+ ///
+ public byte B => _b;
+
+ ///
+ public byte G => _g;
+
+ ///
+ public byte R => _r;
+ // ReSharper restore ConvertToAutoPropertyWhenPossible
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The alpha-component of the color.
+ /// The blue-component of the color.
+ /// The green-component of the color.
+ /// The red-component of the color.
+ public ColorABGR(byte a, byte b, byte g, byte r)
+ {
+ this._a = a;
+ this._b = b;
+ this._g = g;
+ this._r = r;
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ public override string ToString() => $"[A: {_a}, R: {_r}, G: {_g}, B: {_b}]";
+
+ #endregion
+}
\ No newline at end of file
diff --git a/ScreenCapture.NET/Model/Colors/ColorARGB.cs b/ScreenCapture.NET/Model/Colors/ColorARGB.cs
new file mode 100644
index 0000000..2758be5
--- /dev/null
+++ b/ScreenCapture.NET/Model/Colors/ColorARGB.cs
@@ -0,0 +1,66 @@
+// ReSharper disable ConvertToAutoProperty
+
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace ScreenCapture.NET;
+
+///
+/// Represents a color in 32 bit ARGB-format.
+///
+[DebuggerDisplay("[A: {A}, R: {R}, G: {G}, B: {B}]")]
+[StructLayout(LayoutKind.Sequential)]
+public readonly struct ColorARGB : IColor
+{
+ #region Properties & Fields
+
+ ///
+ public static ColorFormat ColorFormat => ColorFormat.ARGB;
+
+ private readonly byte _a;
+ private readonly byte _r;
+ private readonly byte _g;
+ private readonly byte _b;
+
+ // ReSharper disable ConvertToAutoPropertyWhenPossible
+ ///
+ public byte A => _a;
+
+ ///
+ public byte R => _r;
+
+ ///
+ public byte G => _g;
+
+ ///
+ public byte B => _b;
+ // ReSharper restore ConvertToAutoPropertyWhenPossible
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The alpha-component of the color.
+ /// The red-component of the color.
+ /// The green-component of the color.
+ /// The blue-component of the color.
+ public ColorARGB(byte a, byte r, byte g, byte b)
+ {
+ this._a = a;
+ this._r = r;
+ this._g = g;
+ this._b = b;
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ public override string ToString() => $"[A: {_a}, R: {_r}, G: {_g}, B: {_b}]";
+
+ #endregion
+}
\ No newline at end of file
diff --git a/ScreenCapture.NET/Model/Colors/ColorBGR.cs b/ScreenCapture.NET/Model/Colors/ColorBGR.cs
new file mode 100644
index 0000000..e6f1a12
--- /dev/null
+++ b/ScreenCapture.NET/Model/Colors/ColorBGR.cs
@@ -0,0 +1,63 @@
+// ReSharper disable ConvertToAutoProperty
+
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace ScreenCapture.NET;
+
+///
+/// Represents a color in 24 bit BGR-format.
+///
+[DebuggerDisplay("[A: {A}, R: {R}, G: {G}, B: {B}]")]
+[StructLayout(LayoutKind.Sequential)]
+public readonly struct ColorBGR : IColor
+{
+ #region Properties & Fields
+
+ ///
+ public static ColorFormat ColorFormat => ColorFormat.BGR;
+
+ private readonly byte _b;
+ private readonly byte _g;
+ private readonly byte _r;
+
+ // ReSharper disable ConvertToAutoPropertyWhenPossible
+ ///
+ public byte A => byte.MaxValue;
+
+ ///
+ public byte B => _b;
+
+ ///
+ public byte G => _g;
+
+ ///
+ public byte R => _r;
+ // ReSharper restore ConvertToAutoPropertyWhenPossible
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The blue-component of the color.
+ /// The green-component of the color.
+ /// The red-component of the color.
+ public ColorBGR(byte b, byte g, byte r)
+ {
+ this._b = b;
+ this._g = g;
+ this._r = r;
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ public override string ToString() => $"[A: {A}, R: {_r}, G: {_g}, B: {_b}]";
+
+ #endregion
+}
\ No newline at end of file
diff --git a/ScreenCapture.NET/Model/Colors/ColorBGRA.cs b/ScreenCapture.NET/Model/Colors/ColorBGRA.cs
new file mode 100644
index 0000000..1db7ea3
--- /dev/null
+++ b/ScreenCapture.NET/Model/Colors/ColorBGRA.cs
@@ -0,0 +1,66 @@
+// ReSharper disable ConvertToAutoProperty
+
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace ScreenCapture.NET;
+
+///
+/// Represents a color in 32 bit BGRA-format.
+///
+[DebuggerDisplay("[A: {A}, R: {R}, G: {G}, B: {B}]")]
+[StructLayout(LayoutKind.Sequential)]
+public readonly struct ColorBGRA : IColor
+{
+ #region Properties & Fields
+
+ ///
+ public static ColorFormat ColorFormat => ColorFormat.BGRA;
+
+ private readonly byte _b;
+ private readonly byte _g;
+ private readonly byte _r;
+ private readonly byte _a;
+
+ // ReSharper disable ConvertToAutoPropertyWhenPossible
+ ///
+ public byte B => _b;
+
+ ///
+ public byte G => _g;
+
+ ///
+ public byte R => _r;
+
+ ///
+ public byte A => _a;
+ // ReSharper restore ConvertToAutoPropertyWhenPossible
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The blue-component of the color.
+ /// The green-component of the color.
+ /// The red-component of the color.
+ /// The alpha-component of the color.
+ public ColorBGRA(byte b, byte g, byte r, byte a)
+ {
+ this._b = b;
+ this._g = g;
+ this._r = r;
+ this._a = a;
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ public override string ToString() => $"[A: {_a}, R: {_r}, G: {_g}, B: {_b}]";
+
+ #endregion
+}
\ No newline at end of file
diff --git a/ScreenCapture.NET/Model/Colors/ColorFormat.cs b/ScreenCapture.NET/Model/Colors/ColorFormat.cs
new file mode 100644
index 0000000..a9f08e6
--- /dev/null
+++ b/ScreenCapture.NET/Model/Colors/ColorFormat.cs
@@ -0,0 +1,58 @@
+namespace ScreenCapture.NET;
+
+///
+/// Represents a color format.
+///
+public readonly struct ColorFormat
+{
+ #region Instances
+
+ public static readonly ColorFormat BGRA = new(1, 4);
+ public static readonly ColorFormat ABGR = new(2, 4);
+ public static readonly ColorFormat RGBA = new(3, 4);
+ public static readonly ColorFormat ARGB = new(4, 4);
+ public static readonly ColorFormat BGR = new(5, 3);
+ public static readonly ColorFormat RGB = new(6, 3);
+
+ #endregion
+
+ #region Properties & Fields
+
+ ///
+ /// Gets the Id of the color-format.
+ ///
+ public readonly int Id;
+
+ ///
+ /// Gets the Bytes per pixel for this color-format.
+ ///
+ public readonly int BytesPerPixel;
+
+ #endregion
+
+ #region Constructors
+
+ private ColorFormat(int id, int bytesPerPixel)
+ {
+ this.Id = id;
+ this.BytesPerPixel = bytesPerPixel;
+ }
+
+ #endregion
+
+ #region Methods
+
+ public bool Equals(ColorFormat other) => Id == other.Id;
+ public override bool Equals(object? obj) => obj is ColorFormat other && Equals(other);
+
+ public override int GetHashCode() => Id;
+
+ #endregion
+
+ #region Operators
+
+ public static bool operator ==(ColorFormat left, ColorFormat right) => left.Equals(right);
+ public static bool operator !=(ColorFormat left, ColorFormat right) => !(left == right);
+
+ #endregion
+}
\ No newline at end of file
diff --git a/ScreenCapture.NET/Model/Colors/ColorRGB.cs b/ScreenCapture.NET/Model/Colors/ColorRGB.cs
new file mode 100644
index 0000000..36dd932
--- /dev/null
+++ b/ScreenCapture.NET/Model/Colors/ColorRGB.cs
@@ -0,0 +1,63 @@
+// ReSharper disable ConvertToAutoProperty
+
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace ScreenCapture.NET;
+
+///
+/// Represents a color in 24 bit RGB-format.
+///
+[DebuggerDisplay("[A: {A}, R: {R}, G: {G}, B: {B}]")]
+[StructLayout(LayoutKind.Sequential)]
+public readonly struct ColorRGB : IColor
+{
+ #region Properties & Fields
+
+ ///
+ public static ColorFormat ColorFormat => ColorFormat.RGB;
+
+ private readonly byte _r;
+ private readonly byte _g;
+ private readonly byte _b;
+
+ // ReSharper disable ConvertToAutoPropertyWhenPossible
+ ///
+ public byte A => byte.MaxValue;
+
+ ///
+ public byte R => _r;
+
+ ///
+ public byte G => _g;
+
+ ///
+ public byte B => _b;
+ // ReSharper restore ConvertToAutoPropertyWhenPossible
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The red-component of the color.
+ /// The green-component of the color.
+ /// The blue-component of the color.
+ public ColorRGB(byte r, byte g, byte b)
+ {
+ this._r = r;
+ this._g = g;
+ this._b = b;
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ public override string ToString() => $"[A: {A}, R: {_r}, G: {_g}, B: {_b}]";
+
+ #endregion
+}
\ No newline at end of file
diff --git a/ScreenCapture.NET/Model/Colors/ColorRGBA.cs b/ScreenCapture.NET/Model/Colors/ColorRGBA.cs
new file mode 100644
index 0000000..d51a7a1
--- /dev/null
+++ b/ScreenCapture.NET/Model/Colors/ColorRGBA.cs
@@ -0,0 +1,66 @@
+// ReSharper disable ConvertToAutoProperty
+
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace ScreenCapture.NET;
+
+///
+/// Represents a color in 32 bit RGBA-format.
+///
+[DebuggerDisplay("[A: {A}, R: {R}, G: {G}, B: {B}]")]
+[StructLayout(LayoutKind.Sequential)]
+public readonly struct ColorRGBA : IColor
+{
+ #region Properties & Fields
+
+ ///
+ public static ColorFormat ColorFormat => ColorFormat.RGBA;
+
+ private readonly byte _r;
+ private readonly byte _g;
+ private readonly byte _b;
+ private readonly byte _a;
+
+ // ReSharper disable ConvertToAutoPropertyWhenPossible
+ ///
+ public byte R => _r;
+
+ ///
+ public byte G => _g;
+
+ ///
+ public byte B => _b;
+
+ ///
+ public byte A => _a;
+ // ReSharper restore ConvertToAutoPropertyWhenPossible
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The red-component of the color.
+ /// The green-component of the color.
+ /// The blue-component of the color.
+ /// The alpha-component of the color.
+ public ColorRGBA(byte r, byte g, byte b, byte a)
+ {
+ this._r = r;
+ this._g = g;
+ this._b = b;
+ this._a = a;
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ public override string ToString() => $"[A: {_a}, R: {_r}, G: {_g}, B: {_b}]";
+
+ #endregion
+}
\ No newline at end of file
diff --git a/ScreenCapture.NET/Model/Colors/IColor.cs b/ScreenCapture.NET/Model/Colors/IColor.cs
new file mode 100644
index 0000000..826c8db
--- /dev/null
+++ b/ScreenCapture.NET/Model/Colors/IColor.cs
@@ -0,0 +1,34 @@
+using System;
+
+namespace ScreenCapture.NET;
+
+///
+/// Represents a generic color made of 4 bytes (alpha, red, green and blue)
+///
+public interface IColor
+{
+ ///
+ /// Gets the red-component of this color.
+ ///
+ byte R { get; }
+
+ ///
+ /// Gets the green-component of this color.
+ ///
+ byte G { get; }
+
+ ///
+ /// Gets the blue-component of this color.
+ ///
+ byte B { get; }
+
+ ///
+ /// Gets the alpha-component of this color.
+ ///
+ byte A { get; }
+
+ ///
+ /// Gets the color-format of this color.
+ ///
+ public static virtual ColorFormat ColorFormat => throw new NotSupportedException();
+}
\ No newline at end of file
diff --git a/ScreenCapture.NET/Model/ICaptureZone.cs b/ScreenCapture.NET/Model/ICaptureZone.cs
new file mode 100644
index 0000000..915da3a
--- /dev/null
+++ b/ScreenCapture.NET/Model/ICaptureZone.cs
@@ -0,0 +1,103 @@
+using System;
+
+namespace ScreenCapture.NET;
+
+///
+/// Represents a zone on the screen to be captured.
+///
+public interface ICaptureZone
+{
+ ///
+ /// Gets the display this zone is on.
+ ///
+ Display Display { get; }
+
+ ///
+ /// Gets the color-format used in the buffer of this zone.
+ ///
+ ColorFormat ColorFormat { get; }
+
+ ///
+ /// Gets the x-location of the region on the screen.
+ ///
+ int X { get; }
+
+ ///
+ /// Gets the y-location of the region on the screen.
+ ///
+ int Y { get; }
+
+ ///
+ /// Gets the width of the captured region.
+ ///
+ int Width { get; }
+
+ ///
+ /// Gets the height of the captured region.
+ ///
+ int Height { get; }
+
+ ///
+ /// Gets the stride of the buffer.
+ ///
+ int Stride { get; }
+
+ ///
+ /// Gets the level of downscaling applied to the image of this region before copying to local memory. The calculation is (width and height)/2^downscaleLevel.
+ ///
+ int DownscaleLevel { get; }
+
+ ///
+ /// Gets the original width of the region (this equals if is 0).
+ ///
+ int UnscaledWidth { get; }
+
+ ///
+ /// Gets the original height of the region (this equals if is 0).
+ ///
+ int UnscaledHeight { get; }
+
+ ///
+ /// Gets the raw buffer of this zone.
+ ///
+ ReadOnlySpan RawBuffer { get; }
+
+ ///
+ /// Gets the represented by the buffer of this zone.
+ ///
+ IImage Image { get; }
+
+ ///
+ /// Gets or sets if the should be automatically updated on every captured frame.
+ ///
+ bool AutoUpdate { get; set; }
+
+ ///
+ /// Gets if an update for the is requested on the next captured frame.
+ ///
+ bool IsUpdateRequested { get; }
+
+ ///
+ /// Occurs when the is updated.
+ ///
+ event EventHandler? Updated;
+
+ ///
+ /// Locks the image for use. Unlock by disposing the returned disposable.
+ ///
+ /// The disposable used to unlock the image.
+ IDisposable Lock();
+
+ ///
+ /// Requests to update this when the next frame is captured.
+ /// Only necessary if is set to false.
+ ///
+ void RequestUpdate();
+
+ ///
+ /// Gets a . Basically the same as but with better performance if the color-layout is known.
+ ///
+ /// The color used by the buffer of this zone.
+ /// The representing this zone.
+ RefImage GetRefImage() where TColor : struct, IColor;
+}
diff --git a/ScreenCapture.NET/Model/IImage.cs b/ScreenCapture.NET/Model/IImage.cs
new file mode 100644
index 0000000..a406a1f
--- /dev/null
+++ b/ScreenCapture.NET/Model/IImage.cs
@@ -0,0 +1,119 @@
+using System.Collections.Generic;
+
+namespace ScreenCapture.NET;
+
+///
+/// Represents a image.
+///
+public interface IImage : IEnumerable
+{
+ ///
+ /// Gets the width of this image.
+ ///
+ int Width { get; }
+
+ ///
+ /// Gets the height of this image.
+ ///
+ int Height { get; }
+
+ ///
+ /// Gets the color at the specified location.
+ ///
+ /// The X-location to read.
+ /// The Y-location to read.
+ /// The color at the specified location.
+ IColor this[int x, int y] { get; }
+
+ ///
+ /// Gets an image representing the specified location.
+ ///
+ /// The X-location of the image.
+ /// The Y-location of the image.
+ /// The width of the sub-image.
+ ///
+ ///
+ IImage this[int x, int y, int width, int height] { get; }
+
+ ///
+ /// Gets a list of all rows of this image.
+ ///
+ IImageRows Rows { get; }
+
+ ///
+ /// Gets a list of all columns of this image.
+ ///
+ IImageColumns Columns { get; }
+
+ ///
+ /// Represents a list of rows of an image.
+ ///
+ public interface IImageRows : IEnumerable
+ {
+ ///
+ /// Gets the amount of rows in this list.
+ ///
+ int Count { get; }
+
+ ///
+ /// Gets a specific .
+ ///
+ /// The ´row to get.
+ /// The requested .
+ IImageRow this[int column] { get; }
+ }
+
+ ///
+ /// Represents a list of columns of an image.
+ ///
+ public interface IImageColumns : IEnumerable
+ {
+ ///
+ /// Gets the amount of columns in this list.
+ ///
+ int Count { get; }
+
+ ///
+ /// Gets a specific .
+ ///
+ /// The column to get.
+ /// The requested .
+ IImageColumn this[int column] { get; }
+ }
+
+ ///
+ /// Represents a single row of an image.
+ ///
+ public interface IImageRow : IEnumerable
+ {
+ ///
+ /// Gets the length of the row.
+ ///
+ int Length { get; }
+
+ ///
+ /// Gets the at the specified location.
+ ///
+ /// The location to get the color from.
+ /// The at the specified location.
+ IColor this[int x] { get; }
+ }
+
+ ///
+ /// Represents a single column of an image.
+ ///
+ public interface IImageColumn : IEnumerable
+ {
+ ///
+ /// Gets the length of the column.
+ ///
+ int Length { get; }
+
+ ///
+ /// Gets the at the specified location.
+ ///
+ /// The location to get the color from.
+ /// The at the specified location.
+ IColor this[int y] { get; }
+ }
+}
\ No newline at end of file
diff --git a/ScreenCapture.NET/Model/Image.cs b/ScreenCapture.NET/Model/Image.cs
new file mode 100644
index 0000000..bb3c1d2
--- /dev/null
+++ b/ScreenCapture.NET/Model/Image.cs
@@ -0,0 +1,339 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace ScreenCapture.NET;
+
+///
+public sealed class Image : IImage
+ where TColor : struct, IColor
+{
+ #region Properties & Fields
+
+ private readonly byte[] _buffer;
+
+ private readonly int _x;
+ private readonly int _y;
+ private readonly int _stride;
+
+ ///
+ public int Width { get; }
+
+ ///
+ public int Height { get; }
+
+ #endregion
+
+ #region Indexer
+
+ ///
+ public IColor this[int x, int y]
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ if ((x < 0) || (y < 0) || (x >= Width) || (y >= Height)) throw new IndexOutOfRangeException();
+
+ return MemoryMarshal.Cast(_buffer)[((_y + y) * _stride) + (_x + x)];
+ }
+ }
+
+ ///
+ public IImage this[int x, int y, int width, int height]
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ if ((x < 0) || (y < 0) || (width <= 0) || (height <= 0) || ((x + width) > Width) || ((y + height) > Height)) throw new IndexOutOfRangeException();
+
+ return new Image(_buffer, _x + x, _y + y, width, height, _stride);
+ }
+ }
+
+ ///
+ public IImage.IImageRows Rows
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => new ImageRows(_buffer, _x, _y, Width, Height, _stride);
+ }
+
+ ///
+ public IImage.IImageColumns Columns
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => new ImageColumns(_buffer, _x, _y, Width, Height, _stride);
+ }
+
+ #endregion
+
+ #region Constructors
+
+ internal Image(byte[] buffer, int x, int y, int width, int height, int stride)
+ {
+ this._buffer = buffer;
+ this._x = x;
+ this._y = y;
+ this.Width = width;
+ this.Height = height;
+ this._stride = stride;
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ public IEnumerator GetEnumerator()
+ {
+ for (int y = 0; y < Height; y++)
+ for (int x = 0; x < Width; x++)
+ yield return this[x, y];
+ }
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ #endregion
+
+ #region Indexer-Classes
+
+ ///
+ private sealed class ImageRows : IImage.IImageRows
+ {
+ #region Properties & Fields
+
+ private readonly byte[] _buffer;
+ private readonly int _x;
+ private readonly int _y;
+ private readonly int _width;
+ private readonly int _height;
+ private readonly int _stride;
+
+ ///
+ public int Count => _height;
+
+ #endregion
+
+ #region Indexer
+
+ ///
+ public IImage.IImageRow this[int row]
+ {
+ get
+ {
+ if ((row < 0) || (row >= _height)) throw new IndexOutOfRangeException();
+
+ return new ImageRow(_buffer, (((row + _y) * _stride) + _x), _width);
+ }
+ }
+
+ #endregion
+
+ #region Constructors
+
+ internal ImageRows(byte[] buffer, int x, int y, int width, int height, int stride)
+ {
+ this._buffer = buffer;
+ this._x = x;
+ this._y = y;
+ this._width = width;
+ this._height = height;
+ this._stride = stride;
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ public IEnumerator GetEnumerator()
+ {
+ for (int y = 0; y < _height; y++)
+ yield return this[y];
+ }
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ #endregion
+ }
+
+ ///
+ private sealed class ImageRow : IImage.IImageRow
+ {
+ #region Properties & Fields
+
+ private readonly byte[] _buffer;
+ private readonly int _start;
+ private readonly int _length;
+
+ ///
+ public int Length => _length;
+
+ #endregion
+
+ #region Indexer
+
+ ///
+ public IColor this[int x]
+ {
+ get
+ {
+ if ((x < 0) || (x >= _length)) throw new IndexOutOfRangeException();
+
+ ReadOnlySpan row = MemoryMarshal.Cast(_buffer)[_start..];
+ return row[x];
+ }
+ }
+
+ #endregion
+
+ #region Constructors
+
+ internal ImageRow(byte[] buffer, int start, int length)
+ {
+ this._buffer = buffer;
+ this._start = start;
+ this._length = length;
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ public IEnumerator GetEnumerator()
+ {
+ for (int x = 0; x < _length; x++)
+ yield return this[x];
+ }
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ #endregion
+ }
+
+ ///
+ private sealed class ImageColumns : IImage.IImageColumns
+ {
+ #region Properties & Fields
+
+ private readonly byte[] _buffer;
+ private readonly int _x;
+ private readonly int _y;
+ private readonly int _width;
+ private readonly int _height;
+ private readonly int _stride;
+
+ ///
+ public int Count => _width;
+
+ #endregion
+
+ #region Indexer
+
+ ///
+ public IImage.IImageColumn this[int column]
+ {
+ get
+ {
+ if ((column < 0) || (column >= _width)) throw new IndexOutOfRangeException();
+
+ return new ImageColumn(_buffer, (_y * _stride) + _x + column, _height, _stride);
+ }
+ }
+
+ #endregion
+
+ #region Constructors
+
+ internal ImageColumns(byte[] buffer, int x, int y, int width, int height, int stride)
+ {
+ this._buffer = buffer;
+ this._x = x;
+ this._y = y;
+ this._width = width;
+ this._height = height;
+ this._stride = stride;
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ public IEnumerator GetEnumerator()
+ {
+ for (int y = 0; y < _height; y++)
+ yield return this[y];
+ }
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ #endregion
+ }
+
+ ///
+ private sealed class ImageColumn : IImage.IImageColumn
+ {
+ #region Properties & Fields
+
+ private readonly byte[] _buffer;
+ private readonly int _start;
+ private readonly int _length;
+ private readonly int _step;
+
+ ///
+ public int Length => _length;
+
+ #endregion
+
+ #region Indexer
+
+ ///
+ public IColor this[int y]
+ {
+ get
+ {
+ if ((y < 0) || (y >= _length)) throw new IndexOutOfRangeException();
+
+ ReadOnlySpan row = MemoryMarshal.Cast(_buffer)[_start..];
+ return row[y * _step];
+ }
+ }
+
+ #endregion
+
+ #region Constructors
+
+ internal ImageColumn(byte[] buffer, int start, int length, int step)
+ {
+ this._buffer = buffer;
+ this._start = start;
+ this._length = length;
+ this._step = step;
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ public IEnumerator GetEnumerator()
+ {
+ for (int y = 0; y < _length; y++)
+ yield return this[y];
+ }
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ #endregion
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/ScreenCapture.NET/Model/RefImage.cs b/ScreenCapture.NET/Model/RefImage.cs
new file mode 100644
index 0000000..e15b0a1
--- /dev/null
+++ b/ScreenCapture.NET/Model/RefImage.cs
@@ -0,0 +1,330 @@
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace ScreenCapture.NET;
+
+public readonly ref struct RefImage
+ where TColor : struct, IColor
+{
+ #region Properties & Fields
+
+ private readonly ReadOnlySpan _pixels;
+
+ private readonly int _x;
+ private readonly int _y;
+ private readonly int _stride;
+
+ public int Width { get; }
+ public int Height { get; }
+
+ #endregion
+
+ #region Indexer
+
+ public TColor this[int x, int y]
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ if ((x < 0) || (y < 0) || (x >= Width) || (y >= Height)) throw new IndexOutOfRangeException();
+
+ return _pixels[((_y + y) * _stride) + (_x + x)];
+ }
+ }
+
+ public RefImage this[int x, int y, int width, int height]
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ if ((x < 0) || (y < 0) || (width <= 0) || (height <= 0) || ((x + width) > Width) || ((y + height) > Height)) throw new IndexOutOfRangeException();
+
+ return new RefImage(_pixels, _x + x, _y + y, width, height, _stride);
+ }
+ }
+
+ public ImageRows Rows
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => new(_pixels, _x, _y, Width, Height, _stride);
+ }
+ public ImageColumns Columns
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => new(_pixels, _x, _y, Width, Height, _stride);
+ }
+
+ #endregion
+
+ #region Constructors
+
+ internal RefImage(ReadOnlySpan pixels, int x, int y, int width, int height, int stride)
+ {
+ this._pixels = pixels;
+ this._x = x;
+ this._y = y;
+ this.Width = width;
+ this.Height = height;
+ this._stride = stride;
+ }
+
+ #endregion
+
+ #region Methods
+
+ public void CopyTo(in Span dest)
+ {
+ if (dest == null) throw new ArgumentNullException(nameof(dest));
+ if (dest.Length < (Width * Height)) throw new ArgumentException("The destination is too small to fit this image.", nameof(dest));
+
+ ImageRows rows = Rows;
+ Span target = dest;
+ foreach (ReadOnlyRefEnumerable row in rows)
+ {
+ row.CopyTo(target);
+ target = target[Width..];
+ }
+ }
+
+ public TColor[] ToArray()
+ {
+ TColor[] array = new TColor[Width * Height];
+ CopyTo(array);
+ return array;
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ImageEnumerator GetEnumerator() => new(_pixels);
+
+ #endregion
+
+ public ref struct ImageEnumerator
+ {
+ #region Properties & Fields
+
+ private readonly ReadOnlySpan _pixels;
+ private int _position;
+
+ ///
+ public TColor Current
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _pixels[_position];
+ }
+
+ #endregion
+
+ #region Constructors
+
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal ImageEnumerator(ReadOnlySpan pixels)
+ {
+ this._pixels = pixels;
+
+ _position = -1;
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool MoveNext() => ++_position < _pixels.Length;
+
+ #endregion
+ }
+
+ #region Indexer-Structs
+
+ public readonly ref struct ImageRows
+ {
+ #region Properties & Fields
+
+ private readonly ReadOnlySpan _pixels;
+ private readonly int _x;
+ private readonly int _y;
+ private readonly int _width;
+ private readonly int _height;
+ private readonly int _stride;
+
+ public int Count => _height;
+
+ #endregion
+
+ #region Indexer
+
+ public readonly ReadOnlyRefEnumerable this[int row]
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ if ((row < 0) || (row > _height)) throw new IndexOutOfRangeException();
+
+ ref TColor r0 = ref MemoryMarshal.GetReference(_pixels);
+ ref TColor rr = ref Unsafe.Add(ref r0, (nint)(uint)(((row + _y) * _stride) + _x));
+
+ return new ReadOnlyRefEnumerable(rr, _width, 1);
+ }
+ }
+
+ #endregion
+
+ #region Constructors
+
+ public ImageRows(ReadOnlySpan pixels, int x, int y, int width, int height, int stride)
+ {
+ this._pixels = pixels;
+ this._x = x;
+ this._y = y;
+ this._width = width;
+ this._height = height;
+ this._stride = stride;
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ImageRowsEnumerator GetEnumerator() => new(this);
+
+ #endregion
+
+ public ref struct ImageRowsEnumerator
+ {
+ #region Properties & Fields
+
+ private readonly ImageRows _rows;
+ private int _position;
+
+ ///
+ public ReadOnlyRefEnumerable Current
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _rows[_position];
+ }
+
+ #endregion
+
+ #region Constructors
+
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal ImageRowsEnumerator(ImageRows rows)
+ {
+ this._rows = rows;
+
+ _position = -1;
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool MoveNext() => ++_position < _rows._height;
+
+ #endregion
+ }
+ }
+
+ public readonly ref struct ImageColumns
+ {
+ #region Properties & Fields
+
+ private readonly ReadOnlySpan _pixels;
+ private readonly int _x;
+ private readonly int _y;
+ private readonly int _width;
+ private readonly int _height;
+ private readonly int _stride;
+
+ public int Count => _width;
+
+ #endregion
+
+ #region Indexer
+
+ public ReadOnlyRefEnumerable this[int column]
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ if ((column < 0) || (column > _width)) throw new IndexOutOfRangeException();
+
+ ref TColor r0 = ref MemoryMarshal.GetReference(_pixels);
+ ref TColor rc = ref Unsafe.Add(ref r0, (nint)(uint)((_y * _stride) + (column + _x)));
+
+ return new ReadOnlyRefEnumerable(rc, _height, _stride);
+ }
+ }
+
+ #endregion
+
+ #region Constructors
+
+ public ImageColumns(ReadOnlySpan pixels, int x, int y, int width, int height, int stride)
+ {
+ this._pixels = pixels;
+ this._x = x;
+ this._y = y;
+ this._width = width;
+ this._height = height;
+ this._stride = stride;
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ImageColumnsEnumerator GetEnumerator() => new(this);
+
+ #endregion
+
+ public ref struct ImageColumnsEnumerator
+ {
+ #region Properties & Fields
+
+ private readonly ImageColumns _columns;
+ private int _position;
+
+ ///
+ public ReadOnlyRefEnumerable Current
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _columns[_position];
+ }
+
+ #endregion
+
+ #region Constructors
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal ImageColumnsEnumerator(ImageColumns columns)
+ {
+ this._columns = columns;
+ this._position = -1;
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool MoveNext() => ++_position < _columns._width;
+
+ #endregion
+ }
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/ScreenCapture.NET/ScreenCapture.NET.csproj b/ScreenCapture.NET/ScreenCapture.NET.csproj
index 49dfad2..63094cd 100644
--- a/ScreenCapture.NET/ScreenCapture.NET.csproj
+++ b/ScreenCapture.NET/ScreenCapture.NET.csproj
@@ -14,8 +14,8 @@
ScreenCapture.NET
ScreenCapture.NET
ScreenCapture.NET
- Vortice based Screen-Capturing
- Vortice based Screen-Capturing using Desktop Duplication
+ Core functionality for Screen-Capturing
+ Base package for ScreenCapture.NET projects
Copyright © Darth Affe 2023
Copyright © Darth Affe 2023
icon.png
@@ -62,8 +62,4 @@
-
-
-
-
diff --git a/ScreenCapture.NET/ScreenCapture.NET.csproj.DotSettings b/ScreenCapture.NET/ScreenCapture.NET.csproj.DotSettings
index 5cd2a56..560de9f 100644
--- a/ScreenCapture.NET/ScreenCapture.NET.csproj.DotSettings
+++ b/ScreenCapture.NET/ScreenCapture.NET.csproj.DotSettings
@@ -1,5 +1,9 @@
+ True
True
True
+ True
+ True
True
- True
\ No newline at end of file
+ True
+ True
\ No newline at end of file
diff --git a/Tests/ScreenCapture.NET.Tests/ImageTest.cs b/Tests/ScreenCapture.NET.Tests/ImageTest.cs
new file mode 100644
index 0000000..5e5694b
--- /dev/null
+++ b/Tests/ScreenCapture.NET.Tests/ImageTest.cs
@@ -0,0 +1,165 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace ScreenCapture.NET.Tests;
+
+[TestClass]
+public class ImageTest
+{
+ #region Properties & Fields
+
+ private static IScreenCapture? _screenCapture;
+ private static ICaptureZone? _captureZone;
+
+ #endregion
+
+ #region Methods
+
+ [ClassInitialize]
+ public static void ClassInit(TestContext _)
+ {
+ _screenCapture = new TestScreenCapture();
+ _captureZone = _screenCapture.RegisterCaptureZone(0, 0, _screenCapture.Display.Width, _screenCapture.Display.Height);
+ _screenCapture.CaptureScreen();
+ }
+
+ [ClassCleanup]
+ public static void ClassCleanup()
+ {
+ _screenCapture?.Dispose();
+ _screenCapture = null;
+ }
+
+ [TestMethod]
+ public void TestImageFullScreen()
+ {
+ IImage image = _captureZone!.Image;
+
+ Assert.AreEqual(_captureZone.Width, image.Width);
+ Assert.AreEqual(_captureZone.Height, image.Height);
+
+ for (int y = 0; y < image.Height; y++)
+ for (int x = 0; x < image.Width; x++)
+ Assert.AreEqual(TestScreenCapture.GetColorFromLocation(x, y), image[x, y]);
+ }
+
+ [TestMethod]
+ public void TestImageInnerFull()
+ {
+ IImage image = _captureZone!.Image;
+ image = image[0, 0, image.Width, image.Height];
+
+ for (int y = 0; y < image.Height; y++)
+ for (int x = 0; x < image.Width; x++)
+ Assert.AreEqual(TestScreenCapture.GetColorFromLocation(x, y), image[x, y]);
+ }
+
+ [TestMethod]
+ public void TestImageEnumerator()
+ {
+ IImage image = _captureZone!.Image;
+
+ int counter = 0;
+ foreach (IColor color in image)
+ {
+ int x = counter % image.Width;
+ int y = counter / image.Width;
+
+ Assert.AreEqual(TestScreenCapture.GetColorFromLocation(x, y), color);
+
+ counter++;
+ }
+ }
+
+ [TestMethod]
+ public void TestImageInnerPartial()
+ {
+ IImage image = _captureZone!.Image;
+ image = image[163, 280, 720, 13];
+
+ Assert.AreEqual(720, image.Width);
+ Assert.AreEqual(13, image.Height);
+
+ for (int y = 0; y < image.Height; y++)
+ for (int x = 0; x < image.Width; x++)
+ Assert.AreEqual(TestScreenCapture.GetColorFromLocation(163 + x, 280 + y), image[x, y]);
+ }
+
+ [TestMethod]
+ public void TestImageInnerInnerPartial()
+ {
+ IImage image = _captureZone!.Image;
+ image = image[163, 280, 720, 13];
+ image = image[15, 2, 47, 8];
+
+ Assert.AreEqual(47, image.Width);
+ Assert.AreEqual(8, image.Height);
+
+ for (int y = 0; y < image.Height; y++)
+ for (int x = 0; x < image.Width; x++)
+ Assert.AreEqual(TestScreenCapture.GetColorFromLocation(178 + x, 282 + y), image[x, y]);
+ }
+
+ [TestMethod]
+ public void TestImageRowIndexer()
+ {
+ IImage image = _captureZone!.Image;
+
+ Assert.AreEqual(image.Height, image.Rows.Count);
+
+ for (int y = 0; y < image.Height; y++)
+ {
+ IImage.IImageRow row = image.Rows[y];
+ Assert.AreEqual(image.Width, row.Length);
+ for (int x = 0; x < row.Length; x++)
+ Assert.AreEqual(TestScreenCapture.GetColorFromLocation(x, y), row[x]);
+ }
+ }
+
+ [TestMethod]
+ public void TestImageRowEnumerator()
+ {
+ IImage image = _captureZone!.Image;
+
+ int y = 0;
+ foreach (IImage.IImageRow row in image.Rows)
+ {
+ for (int x = 0; x < row.Length; x++)
+ Assert.AreEqual(TestScreenCapture.GetColorFromLocation(x, y), row[x]);
+
+ y++;
+ }
+ }
+
+ [TestMethod]
+ public void TestImageColumnIndexer()
+ {
+ IImage image = _captureZone!.Image;
+
+ Assert.AreEqual(image.Width, image.Columns.Count);
+
+ for (int x = 0; x < image.Width; x++)
+ {
+ IImage.IImageColumn column = image.Columns[x];
+ Assert.AreEqual(image.Height, column.Length);
+ for (int y = 0; y < column.Length; y++)
+ Assert.AreEqual(TestScreenCapture.GetColorFromLocation(x, y), column[y]);
+ }
+ }
+
+ [TestMethod]
+ public void TestImageColumnEnumerator()
+ {
+ IImage image = _captureZone!.Image;
+
+ int x = 0;
+ foreach (IImage.IImageColumn column in image.Columns)
+ {
+ for (int y = 0; y < column.Length; y++)
+ Assert.AreEqual(TestScreenCapture.GetColorFromLocation(x, y), column[y]);
+
+ x++;
+ }
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/Tests/ScreenCapture.NET.Tests/RefImageTest.cs b/Tests/ScreenCapture.NET.Tests/RefImageTest.cs
new file mode 100644
index 0000000..dae38bd
--- /dev/null
+++ b/Tests/ScreenCapture.NET.Tests/RefImageTest.cs
@@ -0,0 +1,165 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace ScreenCapture.NET.Tests;
+
+[TestClass]
+public class RefImageTest
+{
+ #region Properties & Fields
+
+ private static TestScreenCapture? _screenCapture;
+ private static CaptureZone? _captureZone;
+
+ #endregion
+
+ #region Methods
+
+ [ClassInitialize]
+ public static void ClassInit(TestContext _)
+ {
+ _screenCapture = new TestScreenCapture();
+ _captureZone = _screenCapture.RegisterCaptureZone(0, 0, _screenCapture.Display.Width, _screenCapture.Display.Height);
+ _screenCapture.CaptureScreen();
+ }
+
+ [ClassCleanup]
+ public static void ClassCleanup()
+ {
+ _screenCapture?.Dispose();
+ _screenCapture = null;
+ }
+
+ [TestMethod]
+ public void TestImageFullScreen()
+ {
+ RefImage image = _captureZone!.Image;
+
+ Assert.AreEqual(_captureZone.Width, image.Width);
+ Assert.AreEqual(_captureZone.Height, image.Height);
+
+ for (int y = 0; y < image.Height; y++)
+ for (int x = 0; x < image.Width; x++)
+ Assert.AreEqual(TestScreenCapture.GetColorFromLocation(x, y), image[x, y]);
+ }
+
+ [TestMethod]
+ public void TestImageInnerFull()
+ {
+ RefImage image = _captureZone!.Image;
+ image = image[0, 0, image.Width, image.Height];
+
+ for (int y = 0; y < image.Height; y++)
+ for (int x = 0; x < image.Width; x++)
+ Assert.AreEqual(TestScreenCapture.GetColorFromLocation(x, y), image[x, y]);
+ }
+
+ [TestMethod]
+ public void TestImageEnumerator()
+ {
+ RefImage image = _captureZone!.Image;
+
+ int counter = 0;
+ foreach (ColorARGB color in image)
+ {
+ int x = counter % image.Width;
+ int y = counter / image.Width;
+
+ Assert.AreEqual(TestScreenCapture.GetColorFromLocation(x, y), color);
+
+ counter++;
+ }
+ }
+
+ [TestMethod]
+ public void TestImageInnerPartial()
+ {
+ RefImage image = _captureZone!.Image;
+ image = image[163, 280, 720, 13];
+
+ Assert.AreEqual(720, image.Width);
+ Assert.AreEqual(13, image.Height);
+
+ for (int y = 0; y < image.Height; y++)
+ for (int x = 0; x < image.Width; x++)
+ Assert.AreEqual(TestScreenCapture.GetColorFromLocation(163 + x, 280 + y), image[x, y]);
+ }
+
+ [TestMethod]
+ public void TestImageInnerInnerPartial()
+ {
+ RefImage image = _captureZone!.Image;
+ image = image[163, 280, 720, 13];
+ image = image[15, 2, 47, 8];
+
+ Assert.AreEqual(47, image.Width);
+ Assert.AreEqual(8, image.Height);
+
+ for (int y = 0; y < image.Height; y++)
+ for (int x = 0; x < image.Width; x++)
+ Assert.AreEqual(TestScreenCapture.GetColorFromLocation(178 + x, 282 + y), image[x, y]);
+ }
+
+ [TestMethod]
+ public void TestImageRowIndexer()
+ {
+ RefImage image = _captureZone!.Image;
+
+ Assert.AreEqual(image.Height, image.Rows.Count);
+
+ for (int y = 0; y < image.Height; y++)
+ {
+ ReadOnlyRefEnumerable row = image.Rows[y];
+ Assert.AreEqual(image.Width, row.Length);
+ for (int x = 0; x < row.Length; x++)
+ Assert.AreEqual(TestScreenCapture.GetColorFromLocation(x, y), row[x]);
+ }
+ }
+
+ [TestMethod]
+ public void TestImageRowEnumerator()
+ {
+ RefImage image = _captureZone!.Image;
+
+ int y = 0;
+ foreach (ReadOnlyRefEnumerable row in image.Rows)
+ {
+ for (int x = 0; x < row.Length; x++)
+ Assert.AreEqual(TestScreenCapture.GetColorFromLocation(x, y), row[x]);
+
+ y++;
+ }
+ }
+
+ [TestMethod]
+ public void TestImageColumnIndexer()
+ {
+ RefImage image = _captureZone!.Image;
+
+ Assert.AreEqual(image.Width, image.Columns.Count);
+
+ for (int x = 0; x < image.Width; x++)
+ {
+ ReadOnlyRefEnumerable