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 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() + { + RefImage image = _captureZone!.Image; + + int x = 0; + foreach (ReadOnlyRefEnumerable 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/ScreenCapture.NET.Tests.csproj b/Tests/ScreenCapture.NET.Tests/ScreenCapture.NET.Tests.csproj new file mode 100644 index 0000000..b4b9251 --- /dev/null +++ b/Tests/ScreenCapture.NET.Tests/ScreenCapture.NET.Tests.csproj @@ -0,0 +1,25 @@ + + + + net7.0 + enable + + false + true + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/Tests/ScreenCapture.NET.Tests/TestScreenCapture.cs b/Tests/ScreenCapture.NET.Tests/TestScreenCapture.cs new file mode 100644 index 0000000..e1de0e5 --- /dev/null +++ b/Tests/ScreenCapture.NET.Tests/TestScreenCapture.cs @@ -0,0 +1,37 @@ +using System; +using System.Runtime.InteropServices; + +namespace ScreenCapture.NET.Tests; + +internal class TestScreenCapture : AbstractScreenCapture +{ + #region Constructors + + public TestScreenCapture(Rotation rotation = Rotation.None) + : base(new Display(0, "Test", 1920, 1080, rotation, new GraphicsCard(0, "Test", 0, 0))) + { } + + #endregion + + #region Methods + + protected override bool PerformScreenCapture() => true; + + protected override void PerformCaptureZoneUpdate(CaptureZone captureZone, in Span buffer) + { + Span pixels = MemoryMarshal.Cast(buffer); + + for (int y = 0; y < captureZone.Height; y++) + for (int x = 0; x < captureZone.Width; x++) + pixels[(y * captureZone.Width) + x] = GetColorFromLocation(x, y); + } + + public static ColorARGB GetColorFromLocation(int x, int y) + { + byte[] xBytes = BitConverter.GetBytes((short)x); + byte[] yBytes = BitConverter.GetBytes((short)y); + return new ColorARGB(xBytes[0], xBytes[1], yBytes[0], yBytes[1]); + } + + #endregion +} \ No newline at end of file