Merge pull request #16 from DarthAffe/DataHandlingRework

Data handling rework
This commit is contained in:
DarthAffe 2023-09-10 23:07:27 +02:00 committed by GitHub
commit 610d9da25d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 4565 additions and 778 deletions

View File

@ -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;
/// <summary>
/// Represents a ScreenCapture using DirectX 11 desktop duplicaton.
/// https://docs.microsoft.com/en-us/windows/win32/direct3ddxgi/desktop-dup-api
/// </summary>
// ReSharper disable once InconsistentNaming
public sealed class DX11ScreenCapture : AbstractScreenCapture<ColorBGRA>
{
#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;
/// <summary>
/// Gets or sets the timeout in ms used for screen-capturing. (default 1000ms)
/// This is used in <see cref="PerformScreenCapture"/> https://docs.microsoft.com/en-us/windows/win32/api/dxgi1_2/nf-dxgi1_2-idxgioutputduplication-acquirenextframe
/// </summary>
// 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<CaptureZone<ColorBGRA>, ZoneTextures> _textures = new();
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="DX11ScreenCapture"/> class.
/// </summary>
/// <remarks>
/// Note that setting useNewDuplicationAdapter to true requires to call <c>DPIAwareness.Initalize();</c> and prevents the capture from running in a WPF-thread.
/// </remarks>
/// <param name="factory">The <see cref="IDXGIFactory1"/> used to create underlying objects.</param>
/// <param name="display">The <see cref="Display"/> to duplicate.</param>
/// <param name="useNewDuplicationAdapter">Indicates if the DuplicateOutput1 interface should be used instead of the older DuplicateOutput. Currently there's no real use in setting this to true.</param>
internal DX11ScreenCapture(IDXGIFactory1 factory, Display display, bool useNewDuplicationAdapter = false)
: base(display)
{
this._factory = factory;
this._useNewDuplicationAdapter = useNewDuplicationAdapter;
Restart();
}
#endregion
#region Methods
/// <inheritdoc />
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<ID3D11Texture2D>();
_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;
}
/// <inheritdoc />
protected override void PerformCaptureZoneUpdate(CaptureZone<ColorBGRA> captureZone, in Span<byte> 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<byte> 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<byte> source, int sourceStride, in CaptureZone<ColorBGRA> captureZone, in Span<byte> buffer)
{
int height = captureZone.Height;
int stride = captureZone.Stride;
Span<byte> 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<byte> source, int sourceStride, in CaptureZone<ColorBGRA> captureZone, in Span<byte> buffer)
{
int width = captureZone.Width;
int height = captureZone.Height;
int usedBytesPerLine = height * captureZone.ColorFormat.BytesPerPixel;
Span<ColorBGRA> target = MemoryMarshal.Cast<byte, ColorBGRA>(buffer);
for (int x = 0; x < width; x++)
{
ReadOnlySpan<ColorBGRA> src = MemoryMarshal.Cast<byte, ColorBGRA>(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<byte> source, int sourceStride, in CaptureZone<ColorBGRA> captureZone, in Span<byte> buffer)
{
int width = captureZone.Width;
int height = captureZone.Height;
int bpp = captureZone.ColorFormat.BytesPerPixel;
int usedBytesPerLine = width * bpp;
Span<ColorBGRA> target = MemoryMarshal.Cast<byte, ColorBGRA>(buffer);
for (int y = 0; y < height; y++)
{
ReadOnlySpan<ColorBGRA> src = MemoryMarshal.Cast<byte, ColorBGRA>(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<byte> source, int sourceStride, in CaptureZone<ColorBGRA> captureZone, in Span<byte> buffer)
{
int width = captureZone.Width;
int height = captureZone.Height;
int usedBytesPerLine = height * captureZone.ColorFormat.BytesPerPixel;
Span<ColorBGRA> target = MemoryMarshal.Cast<byte, ColorBGRA>(buffer);
for (int x = 0; x < width; x++)
{
ReadOnlySpan<ColorBGRA> src = MemoryMarshal.Cast<byte, ColorBGRA>(source.Slice(x * sourceStride, usedBytesPerLine));
for (int y = 0; y < src.Length; y++)
target[((height - y - 1) * width) + x] = src[y];
}
}
/// <inheritdoc />
public override CaptureZone<ColorBGRA> RegisterCaptureZone(int x, int y, int width, int height, int downscaleLevel = 0)
{
CaptureZone<ColorBGRA> captureZone = base.RegisterCaptureZone(x, y, width, height, downscaleLevel);
lock (_textures)
InitializeCaptureZone(captureZone);
return captureZone;
}
/// <inheritdoc />
public override bool UnregisterCaptureZone(CaptureZone<ColorBGRA> 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;
}
}
/// <inheritdoc />
public override void UpdateCaptureZone(CaptureZone<ColorBGRA> 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);
}
}
}
}
/// <inheritdoc />
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<ColorBGRA> 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);
}
/// <inheritdoc />
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<IDXGIOutput5>();
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<ColorBGRA> 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(); }
}
}
/// <inheritdoc />
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
}
}

View File

@ -12,9 +12,10 @@ public class DX11ScreenCaptureService : IScreenCaptureService
#region Properties & Fields
private readonly IDXGIFactory1 _factory;
private readonly Dictionary<Display, DX11ScreenCapture> _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
/// <inheritdoc />
public IEnumerable<GraphicsCard> 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
/// <inheritdoc />
public IEnumerable<Display> 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
};
/// <inheritdoc />
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
/// <inheritdoc />
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

View File

@ -0,0 +1,73 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net7.0-windows;net6.0-windows</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Authors>Darth Affe</Authors>
<Company>Wyrez</Company>
<Language>en-US</Language>
<NeutralLanguage>en-US</NeutralLanguage>
<Title>ScreenCapture.NET.DX11</Title>
<AssemblyName>ScreenCapture.NET.DX11</AssemblyName>
<AssemblyTitle>ScreenCapture.NET.DX11</AssemblyTitle>
<PackageId>ScreenCapture.NET.DX11</PackageId>
<RootNamespace>ScreenCapture.NET</RootNamespace>
<Description>Vortice based Screen-Capturing</Description>
<Summary>Vortice based Screen-Capturing using Desktop Duplication</Summary>
<Copyright>Copyright © Darth Affe 2023</Copyright>
<PackageCopyright>Copyright © Darth Affe 2023</PackageCopyright>
<PackageIcon>icon.png</PackageIcon>
<PackageProjectUrl>https://github.com/DarthAffe/ScreenCapture.NET</PackageProjectUrl>
<PackageLicenseExpression>LGPL-2.1-only</PackageLicenseExpression>
<RepositoryType>Github</RepositoryType>
<RepositoryUrl>https://github.com/DarthAffe/ScreenCapture.NET</RepositoryUrl>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageReleaseNotes>
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.
</PackageReleaseNotes>
<Version>1.3.2</Version>
<AssemblyVersion>1.3.2</AssemblyVersion>
<FileVersion>1.3.2</FileVersion>
<OutputPath>..\bin\</OutputPath>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IncludeSource>True</IncludeSource>
<IncludeSymbols>True</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<DefineConstants>$(DefineConstants);TRACE;DEBUG</DefineConstants>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<DebugType>portable</DebugType>
<Optimize>true</Optimize>
<NoWarn>$(NoWarn);CS1591;CS1572;CS1573</NoWarn>
<DefineConstants>$(DefineConstants);RELEASE</DefineConstants>
</PropertyGroup>
<ItemGroup>
<None Include="Resources\icon.png">
<Pack>True</Pack>
<PackagePath></PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Vortice.Direct3D11" Version="3.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ScreenCapture.NET\ScreenCapture.NET.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=helper/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -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;
/// <summary>
/// Represents a ScreenCapture using DirectX 9.
/// https://learn.microsoft.com/en-us/windows/win32/api/d3d9/nf-d3d9-idirect3ddevice9-getfrontbufferdata
/// </summary>
// ReSharper disable once InconsistentNaming
public sealed class DX9ScreenCapture : AbstractScreenCapture<ColorBGRA>
{
#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
/// <summary>
/// Initializes a new instance of the <see cref="DX9ScreenCapture"/> class.
/// </summary>
/// <param name="direct3D9">The D3D9 instance used.</param>
/// <param name="display">The <see cref="Display"/> to duplicate.</param>
internal DX9ScreenCapture(IDirect3D9 direct3D9, Display display)
: base(display)
{
this._direct3D9 = direct3D9;
Restart();
}
#endregion
#region Methods
/// <inheritdoc />
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;
}
/// <inheritdoc />
protected override void PerformCaptureZoneUpdate(CaptureZone<ColorBGRA> captureZone, in Span<byte> 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<ColorBGRA> captureZone, in Span<byte> buffer)
{
ReadOnlySpan<ColorBGRA> source = MemoryMarshal.Cast<byte, ColorBGRA>(_buffer);
Span<ColorBGRA> target = MemoryMarshal.Cast<byte, ColorBGRA>(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<ColorBGRA> captureZone, in Span<byte> buffer)
{
ReadOnlySpan<byte> source = _buffer;
Span<byte> 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<byte> scaleBuffer = stackalloc byte[bpp];
for (int y = 0; y < height; y++)
for (int x = 0; x < width; x++)
{
AverageByteSampler.Sample(new SamplerInfo<byte>((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];
}
}
/// <inheritdoc />
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();
}
}
}
/// <inheritdoc />
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
}

View File

@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using Vortice.Direct3D9;
namespace ScreenCapture.NET;
/// <summary>
/// Represents a <see cref="IScreenCaptureService"/> using the <see cref="DX9ScreenCapture"/>.
/// </summary>
public class DX9ScreenCaptureService : IScreenCaptureService
{
#region Properties & Fields
private readonly IDirect3D9 _direct3D9;
private readonly Dictionary<Display, DX9ScreenCapture> _screenCaptures = new();
private bool _isDisposed;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="DX9ScreenCaptureService"/> class.
/// </summary>
public DX9ScreenCaptureService()
{
_direct3D9 = D3D9.Direct3DCreate9();
}
~DX9ScreenCaptureService() => Dispose();
#endregion
#region Methods
/// <inheritdoc />
public IEnumerable<GraphicsCard> GetGraphicsCards()
{
if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
Dictionary<int, GraphicsCard> 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;
}
/// <inheritdoc />
public IEnumerable<Display> 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);
}
}
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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
}

View File

@ -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;
/// <summary>
/// Represents a sampled that averages multiple byte-data entries.
/// </summary>
internal static class AverageByteSampler
{
#region Constants
private static readonly int INT_VECTOR_LENGTH = Vector<uint>.Count;
#endregion
#region Methods
public static unsafe void Sample(in SamplerInfo<byte> info, in Span<byte> pixelData)
{
int count = info.Width * info.Height;
if (count == 0) return;
int dataLength = pixelData.Length;
Span<uint> sums = stackalloc uint[dataLength];
int elementsPerVector = Vector<byte>.Count / dataLength;
int valuesPerVector = elementsPerVector * dataLength;
if (Vector.IsHardwareAccelerated && (info.Height > 1) && (info.Width >= valuesPerVector) && (dataLength <= Vector<byte>.Count))
{
int chunks = info.Width / elementsPerVector;
Vector<uint> sum1 = Vector<uint>.Zero;
Vector<uint> sum2 = Vector<uint>.Zero;
Vector<uint> sum3 = Vector<uint>.Zero;
Vector<uint> sum4 = Vector<uint>.Zero;
for (int y = 0; y < info.Height; y++)
{
ReadOnlySpan<byte> data = info[y];
fixed (byte* colorPtr = data)
{
byte* current = colorPtr;
for (int i = 0; i < chunks; i++)
{
Vector<byte> bytes = *(Vector<byte>*)current;
Vector.Widen(bytes, out Vector<ushort> short1, out Vector<ushort> short2);
Vector.Widen(short1, out Vector<uint> int1, out Vector<uint> int2);
Vector.Widen(short2, out Vector<uint> int3, out Vector<uint> 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<byte> 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
}

View File

@ -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;
/// <summary>
/// Represents the information used to sample data.
/// </summary>
/// <typeparam name="T">The type of the data to sample.</typeparam>
internal readonly ref struct SamplerInfo<T>
{
#region Properties & Fields
private readonly ReadOnlySpan<T> _data;
private readonly int _x;
private readonly int _y;
private readonly int _stride;
private readonly int _dataPerPixel;
private readonly int _dataWidth;
/// <summary>
/// Gets the width of the region the data comes from.
/// </summary>
public readonly int Width;
/// <summary>
/// Gets the height of region the data comes from.
/// </summary>
public readonly int Height;
/// <summary>
/// Gets the data for the requested row.
/// </summary>
/// <param name="row">The row to get the data for.</param>
/// <returns>A readonly span containing the data of the row.</returns>
public ReadOnlySpan<T> this[int row] => _data.Slice((((_y + row) * _stride) + _x) * _dataPerPixel, _dataWidth);
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="SamplerInfo{T}" /> class.
/// </summary>
/// <param name="x">The X-location of the region the data comes from.</param>
/// <param name="y">The Y-location of the region the data comes from.</param>
/// <param name="width">The width of the region the data comes from.</param>
/// <param name="height">The height of region the data comes from.</param>
/// <param name="stride">The number of pixels in a row of data.</param>
/// <param name="dataPerPixel">The number of {T} representing a single pixel.</param>
/// <param name="data">The data to sample.</param>
public SamplerInfo(int x, int y, int width, int height, int stride, int dataPerPixel, in ReadOnlySpan<T> 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
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

View File

@ -0,0 +1,71 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net7.0-windows;net6.0-windows</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Authors>Darth Affe</Authors>
<Company>Wyrez</Company>
<Language>en-US</Language>
<NeutralLanguage>en-US</NeutralLanguage>
<Title>ScreenCapture.NET.DX9</Title>
<AssemblyName>ScreenCapture.NET.DX9</AssemblyName>
<AssemblyTitle>ScreenCapture.NET.DX9</AssemblyTitle>
<PackageId>ScreenCapture.NET.DX9</PackageId>
<RootNamespace>ScreenCapture.NET</RootNamespace>
<Description>Vortice based Screen-Capturing</Description>
<Summary>Vortice based Screen-Capturing using Desktop Duplication</Summary>
<Copyright>Copyright © Darth Affe 2023</Copyright>
<PackageCopyright>Copyright © Darth Affe 2023</PackageCopyright>
<PackageIcon>icon.png</PackageIcon>
<PackageProjectUrl>https://github.com/DarthAffe/ScreenCapture.NET</PackageProjectUrl>
<PackageLicenseExpression>LGPL-2.1-only</PackageLicenseExpression>
<RepositoryType>Github</RepositoryType>
<RepositoryUrl>https://github.com/DarthAffe/ScreenCapture.NET</RepositoryUrl>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageReleaseNotes>
</PackageReleaseNotes>
<Version>2.0.0</Version>
<AssemblyVersion>2.0.0</AssemblyVersion>
<FileVersion>2.0.0</FileVersion>
<OutputPath>..\bin\</OutputPath>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IncludeSource>True</IncludeSource>
<IncludeSymbols>True</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<DefineConstants>$(DefineConstants);TRACE;DEBUG</DefineConstants>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<DebugType>portable</DebugType>
<Optimize>true</Optimize>
<NoWarn>$(NoWarn);CS1591;CS1572;CS1573</NoWarn>
<DefineConstants>$(DefineConstants);RELEASE</DefineConstants>
</PropertyGroup>
<ItemGroup>
<None Include="Resources\icon.png">
<Pack>True</Pack>
<PackagePath></PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Vortice.Direct3D9" Version="3.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ScreenCapture.NET\ScreenCapture.NET.csproj" />
</ItemGroup>
</Project>

View File

@ -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;
/// <summary>
/// Represents a sampled that averages multiple byte-data entries.
/// </summary>
internal static class AverageByteSampler
{
#region Constants
private static readonly int INT_VECTOR_LENGTH = Vector<uint>.Count;
#endregion
#region Methods
public static unsafe void Sample(in SamplerInfo<byte> info, in Span<byte> pixelData)
{
int count = info.Width * info.Height;
if (count == 0) return;
int dataLength = pixelData.Length;
Span<uint> sums = stackalloc uint[dataLength];
int elementsPerVector = Vector<byte>.Count / dataLength;
int valuesPerVector = elementsPerVector * dataLength;
if (Vector.IsHardwareAccelerated && (info.Height > 1) && (info.Width >= valuesPerVector) && (dataLength <= Vector<byte>.Count))
{
int chunks = info.Width / elementsPerVector;
Vector<uint> sum1 = Vector<uint>.Zero;
Vector<uint> sum2 = Vector<uint>.Zero;
Vector<uint> sum3 = Vector<uint>.Zero;
Vector<uint> sum4 = Vector<uint>.Zero;
for (int y = 0; y < info.Height; y++)
{
ReadOnlySpan<byte> data = info[y];
fixed (byte* colorPtr = data)
{
byte* current = colorPtr;
for (int i = 0; i < chunks; i++)
{
Vector<byte> bytes = *(Vector<byte>*)current;
Vector.Widen(bytes, out Vector<ushort> short1, out Vector<ushort> short2);
Vector.Widen(short1, out Vector<uint> int1, out Vector<uint> int2);
Vector.Widen(short2, out Vector<uint> int3, out Vector<uint> 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<byte> 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
}

View File

@ -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;
/// <summary>
/// Represents the information used to sample data.
/// </summary>
/// <typeparam name="T">The type of the data to sample.</typeparam>
internal readonly ref struct SamplerInfo<T>
{
#region Properties & Fields
private readonly ReadOnlySpan<T> _data;
private readonly int _x;
private readonly int _y;
private readonly int _stride;
private readonly int _dataPerPixel;
private readonly int _dataWidth;
/// <summary>
/// Gets the width of the region the data comes from.
/// </summary>
public readonly int Width;
/// <summary>
/// Gets the height of region the data comes from.
/// </summary>
public readonly int Height;
/// <summary>
/// Gets the data for the requested row.
/// </summary>
/// <param name="row">The row to get the data for.</param>
/// <returns>A readonly span containing the data of the row.</returns>
public ReadOnlySpan<T> this[int row] => _data.Slice((((_y + row) * _stride) + _x) * _dataPerPixel, _dataWidth);
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="SamplerInfo{T}" /> class.
/// </summary>
/// <param name="x">The X-location of the region the data comes from.</param>
/// <param name="y">The Y-location of the region the data comes from.</param>
/// <param name="width">The width of the region the data comes from.</param>
/// <param name="height">The height of region the data comes from.</param>
/// <param name="stride">The number of pixels in a row of data.</param>
/// <param name="dataPerPixel">The number of {T} representing a single pixel.</param>
/// <param name="data">The data to sample.</param>
public SamplerInfo(int x, int y, int width, int height, int stride, int dataPerPixel, in ReadOnlySpan<T> 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
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

View File

@ -0,0 +1,68 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net7.0;net6.0</TargetFrameworks>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Authors>Darth Affe</Authors>
<Company>Wyrez</Company>
<Language>en-US</Language>
<NeutralLanguage>en-US</NeutralLanguage>
<Title>ScreenCapture.NET.X11</Title>
<AssemblyName>ScreenCapture.NET.X11</AssemblyName>
<AssemblyTitle>ScreenCapture.NET.X11</AssemblyTitle>
<PackageId>ScreenCapture.NET.X11</PackageId>
<RootNamespace>ScreenCapture.NET</RootNamespace>
<Description>Vortice based Screen-Capturing</Description>
<Summary>Vortice based Screen-Capturing using Desktop Duplication</Summary>
<Copyright>Copyright © Darth Affe 2023</Copyright>
<PackageCopyright>Copyright © Darth Affe 2023</PackageCopyright>
<PackageIcon>icon.png</PackageIcon>
<PackageProjectUrl>https://github.com/DarthAffe/ScreenCapture.NET</PackageProjectUrl>
<PackageLicenseExpression>LGPL-2.1-only</PackageLicenseExpression>
<RepositoryType>Github</RepositoryType>
<RepositoryUrl>https://github.com/DarthAffe/ScreenCapture.NET</RepositoryUrl>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageReleaseNotes>
</PackageReleaseNotes>
<Version>2.0.0</Version>
<AssemblyVersion>2.0.0</AssemblyVersion>
<FileVersion>2.0.0</FileVersion>
<OutputPath>..\bin\</OutputPath>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IncludeSource>True</IncludeSource>
<IncludeSymbols>True</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<DefineConstants>$(DefineConstants);TRACE;DEBUG</DefineConstants>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<DebugType>portable</DebugType>
<Optimize>true</Optimize>
<NoWarn>$(NoWarn);CS1591;CS1572;CS1573</NoWarn>
<DefineConstants>$(DefineConstants);RELEASE</DefineConstants>
</PropertyGroup>
<ItemGroup>
<None Include="Resources\icon.png">
<Pack>True</Pack>
<PackagePath></PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ScreenCapture.NET\ScreenCapture.NET.csproj" />
</ItemGroup>
</Project>

View File

@ -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

View File

@ -0,0 +1,187 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using ScreenCapture.NET.Downscale;
namespace ScreenCapture.NET;
/// <summary>
/// Represents a ScreenCapture using libX11.
/// https://x.org/releases/current/doc/libX11/libX11/libX11.html#XGetImage
/// </summary>
// ReSharper disable once InconsistentNaming
public sealed class X11ScreenCapture : AbstractScreenCapture<ColorBGRA>
{
#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<byte> Data => new(_image.data, _size);
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="X11ScreenCapture"/> class.
/// </summary>
/// <param name="display">The <see cref="Display"/> to duplicate.</param>
internal X11ScreenCapture(Display display)
: base(display)
{
Restart();
}
#endregion
#region Methods
/// <inheritdoc />
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;
}
}
/// <inheritdoc />
protected override void PerformCaptureZoneUpdate(CaptureZone<ColorBGRA> captureZone, in Span<byte> buffer)
{
using IDisposable @lock = captureZone.Lock();
{
if (captureZone.DownscaleLevel == 0)
CopyZone(captureZone, buffer);
else
DownscaleZone(captureZone, buffer);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void CopyZone(CaptureZone<ColorBGRA> captureZone, in Span<byte> buffer)
{
ReadOnlySpan<ColorBGRA> source = MemoryMarshal.Cast<byte, ColorBGRA>(Data);
Span<ColorBGRA> target = MemoryMarshal.Cast<byte, ColorBGRA>(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<ColorBGRA> captureZone, in Span<byte> buffer)
{
ReadOnlySpan<byte> source = Data;
Span<byte> 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<byte> scaleBuffer = stackalloc byte[bpp];
for (int y = 0; y < height; y++)
for (int x = 0; x < width; x++)
{
AverageByteSampler.Sample(new SamplerInfo<byte>((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];
}
}
/// <inheritdoc />
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<X11.XImage>(_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();
}
}
}
/// <inheritdoc />
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
}

View File

@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace ScreenCapture.NET;
/// <summary>
/// Represents a <see cref="IScreenCaptureService"/> using the <see cref="X11ScreenCapture"/>.
/// </summary>
public class X11ScreenCaptureService : IScreenCaptureService
{
#region Properties & Fields
private readonly Dictionary<Display, X11ScreenCapture> _screenCaptures = new();
private bool _isDisposed;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="X11ScreenCaptureService"/> class.
/// </summary>
public X11ScreenCaptureService()
{ }
~X11ScreenCaptureService() => Dispose();
#endregion
#region Methods
/// <inheritdoc />
public IEnumerable<GraphicsCard> 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);
}
}
/// <inheritdoc />
public IEnumerable<Display> 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);
}
}
/// <inheritdoc />
IScreenCapture IScreenCaptureService.GetScreenCapture(Display display) => GetScreenCapture(display);
/// <inheritdoc cref="IScreenCaptureService.GetScreenCapture"/>
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;
}
/// <inheritdoc />
public void Dispose()
{
if (_isDisposed) return;
foreach (X11ScreenCapture screenCapture in _screenCaptures.Values)
screenCapture.Dispose();
_screenCaptures.Clear();
GC.SuppressFinalize(this);
_isDisposed = true;
}
#endregion
}

View File

@ -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

View File

@ -1,3 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=BGR/@EntryIndexedValue">BGR</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=BGRA/@EntryIndexedValue">BGRA</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=DPI/@EntryIndexedValue">DPI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=DX/@EntryIndexedValue">DX</s:String></wpf:ResourceDictionary>

View File

@ -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;
/// <summary>
/// A <see langword="ref"/> <see langword="struct"/> that iterates readonly items from arbitrary memory locations.
/// </summary>
/// <typeparam name="T">The type of items to enumerate.</typeparam>
public readonly ref struct ReadOnlyRefEnumerable<T>
{
#region Properties & Fields
/// <summary>
/// The <see cref="ReadOnlySpan{T}"/> instance pointing to the first item in the target memory area.
/// </summary>
/// <remarks>The <see cref="ReadOnlySpan{T}.Length"/> field maps to the total available length.</remarks>
private readonly ReadOnlySpan<T> _span;
/// <summary>
/// The distance between items in the sequence to enumerate.
/// </summary>
/// <remarks>The distance refers to <typeparamref name="T"/> items, not byte offset.</remarks>
private readonly int _step;
/// <summary>
/// Gets the total available length for the sequence.
/// </summary>
public int Length
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _span.Length;
}
/// <summary>
/// Gets the element at the specified zero-based index.
/// </summary>
/// <param name="index">The zero-based index of the element.</param>
/// <returns>A reference to the element at the specified index.</returns>
/// <exception cref="IndexOutOfRangeException">
/// Thrown when <paramref name="index"/> is invalid.
/// </exception>
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;
}
}
/// <summary>
/// Gets the element at the specified zero-based index.
/// </summary>
/// <param name="index">The zero-based index of the element.</param>
/// <returns>A reference to the element at the specified index.</returns>
/// <exception cref="IndexOutOfRangeException">
/// Thrown when <paramref name="index"/> is invalid.
/// </exception>
public ref readonly T this[Index index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => ref this[index.GetOffset(Length)];
}
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="ReadOnlyRefEnumerable{T}"/> struct.
/// </summary>
/// <param name="reference">A reference to the first item of the sequence.</param>
/// <param name="length">The number of items in the sequence.</param>
/// <param name="step">The distance between items in the sequence to enumerate.</param>
[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
/// <inheritdoc cref="System.Collections.IEnumerable.GetEnumerator"/>
[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>();
T[] array = new T[length];
CopyTo(array);
return array;
}
/// <summary>
/// Copies the contents of this <see cref="ReadOnlyRefEnumerable{T}"/> into a destination <see cref="Span{T}"/> instance.
/// </summary>
/// <param name="destination">The destination <see cref="Span{T}"/> instance.</param>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="destination"/> is shorter than the source <see cref="ReadOnlyRefEnumerable{T}"/> instance.
/// </exception>
public void CopyTo(Span<T> 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);
}
/// <summary>
/// Attempts to copy the current <see cref="ReadOnlyRefEnumerable{T}"/> instance to a destination <see cref="Span{T}"/>.
/// </summary>
/// <param name="destination">The target <see cref="Span{T}"/> of the copy operation.</param>
/// <returns>Whether or not the operation was successful.</returns>
public bool TryCopyTo(Span<T> 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
/// <summary>
/// A custom enumerator type to traverse items within a <see cref="ReadOnlyRefEnumerable{T}"/> instance.
/// </summary>
public ref struct Enumerator
{
#region Properties & Fields
/// <inheritdoc cref="ReadOnlyRefEnumerable{T}._span"/>
private readonly ReadOnlySpan<T> _span;
/// <inheritdoc cref="ReadOnlyRefEnumerable{T}._step"/>
private readonly int _step;
/// <summary>
/// The current position in the sequence.
/// </summary>
private int _position;
/// <inheritdoc cref="System.Collections.Generic.IEnumerator{T}.Current"/>
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
/// <summary>
/// Initializes a new instance of the <see cref="Enumerator"/> struct.
/// </summary>
/// <param name="span">The <see cref="ReadOnlySpan{T}"/> instance with the info on the items to traverse.</param>
/// <param name="step">The distance between items in the sequence to enumerate.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal Enumerator(ReadOnlySpan<T> span, int step)
{
this._span = span;
this._step = step;
_position = -1;
}
#endregion
#region Methods
/// <inheritdoc cref="System.Collections.IEnumerator.MoveNext"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext() => ++_position < _span.Length;
#endregion
}
}

View File

@ -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;
/// <summary>
/// Represents a ScreenCapture using DirectX 11 desktop duplicaton.
/// https://docs.microsoft.com/en-us/windows/win32/direct3ddxgi/desktop-dup-api
/// </summary>
// 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;
/// <inheritdoc />
public Display Display { get; }
/// <summary>
/// Gets or sets the timeout in ms used for screen-capturing. (default 1000ms)
/// This is used in <see cref="CaptureScreen"/> https://docs.microsoft.com/en-us/windows/win32/api/dxgi1_2/nf-dxgi1_2-idxgioutputduplication-acquirenextframe
/// </summary>
// 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<CaptureZone, (ID3D11Texture2D stagingTexture, ID3D11Texture2D? scalingTexture, ID3D11ShaderResourceView? _scalingTextureView)> _captureZones = new();
#endregion
#region Events
/// <inheritdoc />
public event EventHandler<ScreenCaptureUpdatedEventArgs>? Updated;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="DX11ScreenCapture"/> class.
/// </summary>
/// <remarks>
/// Note that setting useNewDuplicationAdapter to true requires to call <c>DPIAwareness.Initalize();</c> and prevents the capture from running in a WPF-thread.
/// </remarks>
/// <param name="factory">The <see cref="IDXGIFactory1"/> used to create underlying objects.</param>
/// <param name="display">The <see cref="Display"/> to duplicate.</param>
/// <param name="useNewDuplicationAdapter">Indicates if the DuplicateOutput1 interface should be used instead of the older DuplicateOutput. Currently there's no real use in setting this to true.</param>
public DX11ScreenCapture(IDXGIFactory1 factory, Display display, bool useNewDuplicationAdapter = false)
{
this._factory = factory;
this.Display = display;
this._useNewDuplicationAdapter = useNewDuplicationAdapter;
Restart();
}
#endregion
#region Methods
/// <inheritdoc />
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<ID3D11Texture2D>();
_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<byte> 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<byte> source, int sourceStride, in CaptureZone captureZone)
{
int height = captureZone.Height;
int stride = captureZone.Stride;
Span<byte> 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<byte> source, int sourceStride, in CaptureZone captureZone)
{
int width = captureZone.Width;
int height = captureZone.Height;
Span<byte> 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<byte> source, int sourceStride, in CaptureZone captureZone)
{
int width = captureZone.Width;
int height = captureZone.Height;
int stride = captureZone.Stride;
Span<byte> 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<byte> source, int sourceStride, in CaptureZone captureZone)
{
int width = captureZone.Width;
int height = captureZone.Height;
Span<byte> 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];
}
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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;
}
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public void Restart()
{
lock (_captureLock)
{
try
{
List<CaptureZone> 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<IDXGIOutput5>();
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); }
}
}
/// <inheritdoc />
public void Dispose() => Dispose(true);
private void Dispose(bool removeCaptureZones)
{
try
{
lock (_captureLock)
{
try { _duplicatedOutput?.Dispose(); } catch { /**/ }
_duplicatedOutput = null;
try
{
if (removeCaptureZones)
{
List<CaptureZone> 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
}

View File

@ -0,0 +1,188 @@
namespace ScreenCapture.NET;
/// <summary>
/// Helper-class for black-bar removal.
/// </summary>
public static class BlackBarDetection
{
#region IImage
/// <summary>
/// Create an image with black bars removed
/// </summary>
/// <param name="image">The image the bars are removed from.</param>
/// <param name="threshold">The threshold of "blackness" used to detect black bars. (e. g. Threshold 5 will consider a pixel of color [5,5,5] as black.)</param>
/// <param name="removeTop">A bool indicating if black bars should be removed at the top of the image.</param>
/// <param name="removeBottom">A bool indicating if black bars should be removed at the bottom of the image.</param>
/// <param name="removeLeft">A bool indicating if black bars should be removed on the left side of the image.</param>
/// <param name="removeRight">A bool indicating if black bars should be removed on the right side of the image.</param>
/// <returns>The image with black bars removed.</returns>
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
/// <summary>
/// Create an image with black bars removed
/// </summary>
/// <param name="image">The image the bars are removed from.</param>
/// <param name="threshold">The threshold of "blackness" used to detect black bars. (e. g. Threshold 5 will consider a pixel of color [5,5,5] as black.)</param>
/// <param name="removeTop">A bool indicating if black bars should be removed at the top of the image.</param>
/// <param name="removeBottom">A bool indicating if black bars should be removed at the bottom of the image.</param>
/// <param name="removeLeft">A bool indicating if black bars should be removed on the left side of the image.</param>
/// <param name="removeRight">A bool indicating if black bars should be removed on the right side of the image.</param>
/// <returns>The image with black bars removed.</returns>
public static RefImage<TColor> RemoveBlackBars<TColor>(this RefImage<TColor> 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<TColor>(this RefImage<TColor> image, int threshold)
where TColor : struct, IColor
{
RefImage<TColor>.ImageRows rows = image.Rows;
for (int y = 0; y < rows.Count; y++)
{
ReadOnlyRefEnumerable<TColor> 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<TColor>(this RefImage<TColor> image, int threshold)
where TColor : struct, IColor
{
RefImage<TColor>.ImageRows rows = image.Rows;
for (int y = rows.Count - 1; y >= 0; y--)
{
ReadOnlyRefEnumerable<TColor> 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<TColor>(this RefImage<TColor> image, int threshold)
where TColor : struct, IColor
{
RefImage<TColor>.ImageColumns columns = image.Columns;
for (int x = 0; x < columns.Count; x++)
{
ReadOnlyRefEnumerable<TColor> 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<TColor>(this RefImage<TColor> image, int threshold)
where TColor : struct, IColor
{
RefImage<TColor>.ImageColumns columns = image.Columns;
for (int x = columns.Count - 1; x >= 0; x--)
{
ReadOnlyRefEnumerable<TColor> column = columns[x];
foreach (TColor color in column)
{
if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
return x;
}
}
return columns.Count;
}
#endregion
}

View File

@ -0,0 +1,247 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace ScreenCapture.NET;
/// <inheritdoc />
public abstract class AbstractScreenCapture<TColor> : IScreenCapture
where TColor : struct, IColor
{
#region Properties & Fields
private bool _isDisposed;
/// <summary>
/// Gets a list of <see cref="CaptureZone{TColol}"/> registered on this ScreenCature.
/// </summary>
protected HashSet<CaptureZone<TColor>> CaptureZones { get; } = new();
/// <inheritdoc />
public Display Display { get; }
#endregion
#region Events
/// <inheritdoc />
public event EventHandler<ScreenCaptureUpdatedEventArgs>? Updated;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="AbstractScreenCapture{T}"/> class.
/// </summary>
/// <param name="display">The <see cref="Display"/> to duplicate.</param>
protected AbstractScreenCapture(Display display)
{
this.Display = display;
}
~AbstractScreenCapture() => Dispose(false);
#endregion
#region Methods
/// <inheritdoc />
public virtual bool CaptureScreen()
{
if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
bool result;
try
{
result = PerformScreenCapture();
}
catch
{
result = false;
}
lock (CaptureZones)
foreach (CaptureZone<TColor> captureZone in CaptureZones.Where(x => x.AutoUpdate || x.IsUpdateRequested))
{
try
{
PerformCaptureZoneUpdate(captureZone, captureZone.InternalBuffer);
captureZone.SetUpdated();
}
catch { /* */ }
}
OnUpdated(result);
return result;
}
/// <summary>
/// Performs the actual screen capture.
/// </summary>
/// <returns><c>true</c> if the screen was captured sucessfully; otherwise, <c>false</c>.</returns>
protected abstract bool PerformScreenCapture();
/// <summary>
/// Performs an update of the given capture zone.
/// </summary>
/// <param name="captureZone">The capture zone to update.</param>
/// <param name="buffer">The buffer containing the current pixel-data of the capture zone.</param>
protected abstract void PerformCaptureZoneUpdate(CaptureZone<TColor> captureZone, in Span<byte> buffer);
/// <summary>
/// Raises the <see cref="Updated"/>-event.
/// </summary>
/// <param name="result">A bool indicating whether the update was successful or not.</param>
protected virtual void OnUpdated(bool result)
{
try
{
Updated?.Invoke(this, new ScreenCaptureUpdatedEventArgs(result));
}
catch { /**/ }
}
/// <inheritdoc />
ICaptureZone IScreenCapture.RegisterCaptureZone(int x, int y, int width, int height, int downscaleLevel) => RegisterCaptureZone(x, y, width, height, downscaleLevel);
/// <inheritdoc cref="IScreenCapture.RegisterCaptureZone" />
public virtual CaptureZone<TColor> 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<TColor> captureZone = new(Display, x, y, width, height, downscaleLevel, unscaledWidth, unscaledHeight);
CaptureZones.Add(captureZone);
return captureZone;
}
}
/// <summary>
/// Validates the given values of a capture zone.
/// </summary>
/// <param name="x">The X-location of the zone.</param>
/// <param name="y">The Y-location of the zone.</param>
/// <param name="width">The width of the zone.</param>
/// <param name="height">The height of the zone.</param>
/// <param name="downscaleLevel">The downscale-level of the zone.</param>
/// <exception cref="ArgumentException">Throws if some of the provided data is not valid.</exception>
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");
}
/// <summary>
/// Calculates the actual size when downscaling is used.
/// </summary>
/// <param name="width">The original width.</param>
/// <param name="height">The original height.</param>
/// <param name="downscaleLevel">The level of downscaling to be used.</param>
/// <returns>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.)</returns>
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);
}
/// <inheritdoc />
bool IScreenCapture.UnregisterCaptureZone(ICaptureZone captureZone) => UnregisterCaptureZone(captureZone as CaptureZone<TColor> ?? throw new ArgumentException("Invalid capture-zone."));
/// <inheritdoc cref="IScreenCapture.UnregisterCaptureZone" />
public virtual bool UnregisterCaptureZone(CaptureZone<TColor> captureZone)
{
if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
return CaptureZones.Remove(captureZone);
}
/// <inheritdoc />
void IScreenCapture.UpdateCaptureZone(ICaptureZone captureZone, int? x, int? y, int? width, int? height, int? downscaleLevel)
=> UpdateCaptureZone(captureZone as CaptureZone<TColor> ?? throw new ArgumentException("Invalid capture-zone."), x, y, width, height, downscaleLevel);
/// <inheritdoc cref="IScreenCapture.UpdateCaptureZone" />
public virtual void UpdateCaptureZone(CaptureZone<TColor> 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);
}
}
}
/// <inheritdoc />
public virtual void Restart()
{
if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
}
/// <inheritdoc />
public void Dispose()
{
if (_isDisposed) return;
try
{
Dispose(true);
}
catch { /* don't throw in dispose! */ }
GC.SuppressFinalize(this);
_isDisposed = true;
}
/// <inheritdoc cref="Dispose" />
protected virtual void Dispose(bool disposing) { }
#endregion
}

View File

@ -32,14 +32,14 @@ public interface IScreenCapture : IDisposable
/// <param name="height">The height of the region to capture (must be &gt;= 0 and this + y must be &lt;= screen-height).</param>
/// <param name="downscaleLevel">The level of downscaling applied to the image of this region before copying to local memory. The calculation is (width and height)/2^downscaleLevel.</param>
/// <returns>The new <see cref="CaptureScreen"/>.</returns>
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);
/// <summary>
/// Removes the given <see cref="CaptureScreen"/> from the <see cref="IScreenCapture"/>.
/// </summary>
/// <param name="captureZone">The previously registered <see cref="CaptureScreen"/>.</param>
/// <returns><c>true</c> if the <see cref="CaptureScreen"/> was successfully removed; otherwise, <c>false</c>.</returns>
bool UnregisterCaptureZone(CaptureZone captureZone);
bool UnregisterCaptureZone(ICaptureZone captureZone);
/// <summary>
/// Updates the the given <see cref="CaptureScreen"/>.
@ -53,7 +53,7 @@ public interface IScreenCapture : IDisposable
/// <param name="width">The width of the region to capture (must be &gt;= 0 and this + x must be &lt;= screen-width).</param>
/// <param name="height">The new height of the region to capture (must be &gt;= 0 and this + y must be &lt;= screen-height).</param>
/// <param name="downscaleLevel">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.</param>
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);
/// <summary>
/// Restarts the <see cref="IScreenCapture"/>.

View File

@ -1,141 +0,0 @@
// ReSharper disable MemberCanBePrivate.Global
using System;
namespace ScreenCapture.NET;
/// <summary>
/// Represents the configuration for the detection and removal of black bars around the screen image.
/// </summary>
public sealed class BlackBarDetection
{
#region Properties & Fields
private readonly CaptureZone _captureZone;
private int? _top;
/// <summary>
/// Gets the size of the detected black bar at the top of the image.
/// </summary>
public int Top => _top ??= CalculateTop();
private int? _bottom;
/// <summary>
/// Gets the size of the detected black bar at the bottom of the image.
/// </summary>
public int Bottom => _bottom ??= CalculateBottom();
private int? _left;
/// <summary>
/// Gets the size of the detected black bar at the left of the image.
/// </summary>
public int Left => _left ??= CalculateLeft();
private int? _right;
/// <summary>
/// Gets the size of the detected black bar at the right of the image.
/// </summary>
public int Right => _right ??= CalculateRight();
private int _theshold = 0;
/// <summary>
/// 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)
/// </summary>
public int Threshold
{
get => _theshold;
set
{
_theshold = value;
InvalidateCache();
}
}
#endregion
#region Constructors
internal BlackBarDetection(CaptureZone captureZone)
{
this._captureZone = captureZone;
}
#endregion
#region Methods
/// <summary>
/// Invalidates the cached values and recalculates <see cref="Top"/>, <see cref="Bottom"/>, <see cref="Left"/> and <see cref="Right"/>.
/// </summary>
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<byte> 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<byte> 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
}

View File

@ -1,93 +1,105 @@
// ReSharper disable MemberCanBePrivate.Global
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
namespace ScreenCapture.NET;
/// <summary>
/// Represents a duplicated region on the screen.
/// </summary>
public sealed class CaptureZone
public sealed class CaptureZone<TColor> : ICaptureZone
where TColor : struct, IColor
{
#region Properties & Fields
/// <summary>
/// Gets the unique id of this <see cref="CaptureZone"/>.
/// </summary>
public int Id { get; }
private readonly object _lock = new();
/// <summary>
/// Gets the x-location of the region on the screen.
/// </summary>
/// <inheritdoc />
public Display Display { get; }
/// <inheritdoc />
public ColorFormat ColorFormat
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => TColor.ColorFormat;
}
/// <inheritdoc />
public int X { get; internal set; }
/// <summary>
/// Gets the y-location of the region on the screen.
/// </summary>
/// <inheritdoc />
public int Y { get; internal set; }
/// <summary>
/// Gets the width of the captured region.
/// </summary>
public int Width { get; internal set; }
/// <inheritdoc />
public int Width { get; private set; }
/// <inheritdoc />
public int Height { get; private set; }
/// <inheritdoc />
public int Stride
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Width * ColorFormat.BytesPerPixel;
}
/// <inheritdoc />
public int DownscaleLevel { get; private set; }
/// <inheritdoc />
public int UnscaledWidth { get; private set; }
/// <inheritdoc />
public int UnscaledHeight { get; private set; }
internal byte[] InternalBuffer { get; set; }
/// <inheritdoc />
public ReadOnlySpan<byte> RawBuffer
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => InternalBuffer;
}
/// <summary>
/// Gets the height of the captured region.
/// Gets the pixel-data of this zone.
/// </summary>
public int Height { get; internal set; }
public ReadOnlySpan<TColor> Pixels
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => MemoryMarshal.Cast<byte, TColor>(RawBuffer);
}
/// <summary>
/// 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 <see cref="RefImage{TColor}"/>. Basically the same as <see cref="Image"/> but with better performance.
/// </summary>
public int DownscaleLevel { get; internal set; }
public RefImage<TColor> Image
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new(Pixels, 0, 0, Width, Height, Width);
}
/// <summary>
/// Gets the original width of the region (this equals <see cref="Width"/> if <see cref="DownscaleLevel"/> is 0).
/// </summary>
public int UnscaledWidth { get; internal set; }
/// <inheritdoc />
IImage ICaptureZone.Image
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new Image<TColor>(InternalBuffer, 0, 0, Width, Height, Width);
}
/// <summary>
/// Gets the original height of the region (this equals <see cref="Height"/> if <see cref="DownscaleLevel"/> is 0).
/// </summary>
public int UnscaledHeight { get; internal set; }
/// <summary>
/// Gets the amount of bytes per pixel in the image (most likely 3 [RGB] or 4 [ARGB]).
/// </summary>
public int BytesPerPixel { get; }
/// <summary>
/// Gets the size in bytes of a row in the region (<see cref="Width"/> * <see cref="BytesPerPixel"/>).
/// </summary>
public int Stride => Width * BytesPerPixel;
/// <summary>
/// Gets the buffer containing the image data. Format depends on the specific capture but is most likely BGRA32.
/// </summary>
public byte[] Buffer { get; internal set; }
/// <summary>
/// Gets the config for black-bar detection.
/// </summary>
public BlackBarDetection BlackBars { get; }
/// <summary>
/// Gets or sets if the <see cref="CaptureZone"/> should be automatically updated on every captured frame.
/// </summary>
/// <inheritdoc />
public bool AutoUpdate { get; set; } = true;
/// <summary>
/// Gets if an update for the <see cref="CaptureZone"/> is requested on the next captured frame.
/// </summary>
/// <inheritdoc />
public bool IsUpdateRequested { get; private set; }
#endregion
#region Events
/// <summary>
/// Occurs when the <see cref="CaptureZone"/> is updated.
/// </summary>
/// <inheritdoc />
public event EventHandler? Updated;
#endregion
@ -95,9 +107,9 @@ public sealed class CaptureZone
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="CaptureZone"/> class.
/// Initializes a new instance of the <see cref="CaptureZone{T}"/> class.
/// </summary>
/// <param name="id">The unique id of this <see cref="CaptureZone"/>.</param>
/// <param name="id">The unique id of this <see cref="CaptureZone{T}"/>.</param>
/// <param name="x">The x-location of the region on the screen.</param>
/// <param name="y">The y-location of the region on the screen.</param>
/// <param name="width">The width of the region on the screen.</param>
@ -107,56 +119,97 @@ public sealed class CaptureZone
/// <param name="unscaledWidth">The original width of the region.</param>
/// <param name="unscaledHeight">The original height of the region</param>
/// <param name="buffer">The buffer containing the image data.</param>
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
/// <summary>
/// Requests to update this <see cref="CaptureZone"/> when the next frame is captured.
/// Only necessary if <see cref="AutoUpdate"/> is set to <c>false</c>.
/// </summary>
/// <inheritdoc />
public RefImage<T> GetRefImage<T>()
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<T>(MemoryMarshal.Cast<byte, T>(RawBuffer), 0, 0, Width, Height, Width);
}
/// <inheritdoc />
public IDisposable Lock()
{
Monitor.Enter(_lock);
return new UnlockDisposable(_lock);
}
/// <inheritdoc />
public void RequestUpdate() => IsUpdateRequested = true;
/// <summary>
/// Marks the <see cref="CaptureZone"/> as updated.
/// WARNING: This should not be called outside of an <see cref="IScreenCapture"/>!
/// Marks the <see cref="CaptureZone{T}"/> as updated.
/// </summary>
public void SetUpdated()
internal void SetUpdated()
{
IsUpdateRequested = false;
BlackBars.InvalidateCache();
Updated?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Determines whether this <see cref="CaptureZone"/> equals the given one.
/// </summary>
/// <param name="other">The <see cref="CaptureZone"/> to compare.</param>
/// <returns><c>true</c> if the specified object is equal to the current object; otherwise, <c>false</c>.</returns>
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;
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
/// <inheritdoc />
public override bool Equals(object? obj) => obj is CaptureZone other && Equals(other);
public void Dispose()
{
if (_disposed) throw new ObjectDisposedException("The lock is already released");
/// <inheritdoc />
public override int GetHashCode() => Id;
Monitor.Exit(_lock);
_disposed = true;
GC.SuppressFinalize(this);
}
#endregion
}
}

View File

@ -0,0 +1,66 @@
// ReSharper disable ConvertToAutoProperty
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace ScreenCapture.NET;
/// <summary>
/// Represents a color in 32 bit ABGR-format.
/// </summary>
[DebuggerDisplay("[A: {A}, R: {R}, G: {G}, B: {B}]")]
[StructLayout(LayoutKind.Sequential)]
public readonly struct ColorABGR : IColor
{
#region Properties & Fields
/// <inheritdoc />
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
/// <inheritdoc />
public byte A => _a;
/// <inheritdoc />
public byte B => _b;
/// <inheritdoc />
public byte G => _g;
/// <inheritdoc />
public byte R => _r;
// ReSharper restore ConvertToAutoPropertyWhenPossible
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="ColorABGR"/> class.
/// </summary>
/// <param name="a">The alpha-component of the color.</param>
/// <param name="b">The blue-component of the color.</param>
/// <param name="g">The green-component of the color.</param>
/// <param name="r">The red-component of the color.</param>
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
/// <inheritdoc />
public override string ToString() => $"[A: {_a}, R: {_r}, G: {_g}, B: {_b}]";
#endregion
}

View File

@ -0,0 +1,66 @@
// ReSharper disable ConvertToAutoProperty
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace ScreenCapture.NET;
/// <summary>
/// Represents a color in 32 bit ARGB-format.
/// </summary>
[DebuggerDisplay("[A: {A}, R: {R}, G: {G}, B: {B}]")]
[StructLayout(LayoutKind.Sequential)]
public readonly struct ColorARGB : IColor
{
#region Properties & Fields
/// <inheritdoc />
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
/// <inheritdoc />
public byte A => _a;
/// <inheritdoc />
public byte R => _r;
/// <inheritdoc />
public byte G => _g;
/// <inheritdoc />
public byte B => _b;
// ReSharper restore ConvertToAutoPropertyWhenPossible
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="ColorARGB"/> class.
/// </summary>
/// <param name="a">The alpha-component of the color.</param>
/// <param name="r">The red-component of the color.</param>
/// <param name="g">The green-component of the color.</param>
/// <param name="b">The blue-component of the color.</param>
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
/// <inheritdoc />
public override string ToString() => $"[A: {_a}, R: {_r}, G: {_g}, B: {_b}]";
#endregion
}

View File

@ -0,0 +1,63 @@
// ReSharper disable ConvertToAutoProperty
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace ScreenCapture.NET;
/// <summary>
/// Represents a color in 24 bit BGR-format.
/// </summary>
[DebuggerDisplay("[A: {A}, R: {R}, G: {G}, B: {B}]")]
[StructLayout(LayoutKind.Sequential)]
public readonly struct ColorBGR : IColor
{
#region Properties & Fields
/// <inheritdoc />
public static ColorFormat ColorFormat => ColorFormat.BGR;
private readonly byte _b;
private readonly byte _g;
private readonly byte _r;
// ReSharper disable ConvertToAutoPropertyWhenPossible
/// <inheritdoc />
public byte A => byte.MaxValue;
/// <inheritdoc />
public byte B => _b;
/// <inheritdoc />
public byte G => _g;
/// <inheritdoc />
public byte R => _r;
// ReSharper restore ConvertToAutoPropertyWhenPossible
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="ColorBGR"/> class.
/// </summary>
/// <param name="b">The blue-component of the color.</param>
/// <param name="g">The green-component of the color.</param>
/// <param name="r">The red-component of the color.</param>
public ColorBGR(byte b, byte g, byte r)
{
this._b = b;
this._g = g;
this._r = r;
}
#endregion
#region Methods
/// <inheritdoc />
public override string ToString() => $"[A: {A}, R: {_r}, G: {_g}, B: {_b}]";
#endregion
}

View File

@ -0,0 +1,66 @@
// ReSharper disable ConvertToAutoProperty
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace ScreenCapture.NET;
/// <summary>
/// Represents a color in 32 bit BGRA-format.
/// </summary>
[DebuggerDisplay("[A: {A}, R: {R}, G: {G}, B: {B}]")]
[StructLayout(LayoutKind.Sequential)]
public readonly struct ColorBGRA : IColor
{
#region Properties & Fields
/// <inheritdoc />
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
/// <inheritdoc />
public byte B => _b;
/// <inheritdoc />
public byte G => _g;
/// <inheritdoc />
public byte R => _r;
/// <inheritdoc />
public byte A => _a;
// ReSharper restore ConvertToAutoPropertyWhenPossible
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="ColorBGRA"/> class.
/// </summary>
/// <param name="b">The blue-component of the color.</param>
/// <param name="g">The green-component of the color.</param>
/// <param name="r">The red-component of the color.</param>
/// <param name="a">The alpha-component of the color.</param>
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
/// <inheritdoc />
public override string ToString() => $"[A: {_a}, R: {_r}, G: {_g}, B: {_b}]";
#endregion
}

View File

@ -0,0 +1,58 @@
namespace ScreenCapture.NET;
/// <summary>
/// Represents a color format.
/// </summary>
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
/// <summary>
/// Gets the Id of the color-format.
/// </summary>
public readonly int Id;
/// <summary>
/// Gets the Bytes per pixel for this color-format.
/// </summary>
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
}

View File

@ -0,0 +1,63 @@
// ReSharper disable ConvertToAutoProperty
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace ScreenCapture.NET;
/// <summary>
/// Represents a color in 24 bit RGB-format.
/// </summary>
[DebuggerDisplay("[A: {A}, R: {R}, G: {G}, B: {B}]")]
[StructLayout(LayoutKind.Sequential)]
public readonly struct ColorRGB : IColor
{
#region Properties & Fields
/// <inheritdoc />
public static ColorFormat ColorFormat => ColorFormat.RGB;
private readonly byte _r;
private readonly byte _g;
private readonly byte _b;
// ReSharper disable ConvertToAutoPropertyWhenPossible
/// <inheritdoc />
public byte A => byte.MaxValue;
/// <inheritdoc />
public byte R => _r;
/// <inheritdoc />
public byte G => _g;
/// <inheritdoc />
public byte B => _b;
// ReSharper restore ConvertToAutoPropertyWhenPossible
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="ColorRGB"/> class.
/// </summary>
/// <param name="r">The red-component of the color.</param>
/// <param name="g">The green-component of the color.</param>
/// <param name="b">The blue-component of the color.</param>
public ColorRGB(byte r, byte g, byte b)
{
this._r = r;
this._g = g;
this._b = b;
}
#endregion
#region Methods
/// <inheritdoc />
public override string ToString() => $"[A: {A}, R: {_r}, G: {_g}, B: {_b}]";
#endregion
}

View File

@ -0,0 +1,66 @@
// ReSharper disable ConvertToAutoProperty
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace ScreenCapture.NET;
/// <summary>
/// Represents a color in 32 bit RGBA-format.
/// </summary>
[DebuggerDisplay("[A: {A}, R: {R}, G: {G}, B: {B}]")]
[StructLayout(LayoutKind.Sequential)]
public readonly struct ColorRGBA : IColor
{
#region Properties & Fields
/// <inheritdoc />
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
/// <inheritdoc />
public byte R => _r;
/// <inheritdoc />
public byte G => _g;
/// <inheritdoc />
public byte B => _b;
/// <inheritdoc />
public byte A => _a;
// ReSharper restore ConvertToAutoPropertyWhenPossible
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="ColorRGBA"/> class.
/// </summary>
/// <param name="r">The red-component of the color.</param>
/// <param name="g">The green-component of the color.</param>
/// <param name="b">The blue-component of the color.</param>
/// <param name="a">The alpha-component of the color.</param>
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
/// <inheritdoc />
public override string ToString() => $"[A: {_a}, R: {_r}, G: {_g}, B: {_b}]";
#endregion
}

View File

@ -0,0 +1,34 @@
using System;
namespace ScreenCapture.NET;
/// <summary>
/// Represents a generic color made of 4 bytes (alpha, red, green and blue)
/// </summary>
public interface IColor
{
/// <summary>
/// Gets the red-component of this color.
/// </summary>
byte R { get; }
/// <summary>
/// Gets the green-component of this color.
/// </summary>
byte G { get; }
/// <summary>
/// Gets the blue-component of this color.
/// </summary>
byte B { get; }
/// <summary>
/// Gets the alpha-component of this color.
/// </summary>
byte A { get; }
/// <summary>
/// Gets the color-format of this color.
/// </summary>
public static virtual ColorFormat ColorFormat => throw new NotSupportedException();
}

View File

@ -0,0 +1,103 @@
using System;
namespace ScreenCapture.NET;
/// <summary>
/// Represents a zone on the screen to be captured.
/// </summary>
public interface ICaptureZone
{
/// <summary>
/// Gets the display this zone is on.
/// </summary>
Display Display { get; }
/// <summary>
/// Gets the color-format used in the buffer of this zone.
/// </summary>
ColorFormat ColorFormat { get; }
/// <summary>
/// Gets the x-location of the region on the screen.
/// </summary>
int X { get; }
/// <summary>
/// Gets the y-location of the region on the screen.
/// </summary>
int Y { get; }
/// <summary>
/// Gets the width of the captured region.
/// </summary>
int Width { get; }
/// <summary>
/// Gets the height of the captured region.
/// </summary>
int Height { get; }
/// <summary>
/// Gets the stride of the buffer.
/// </summary>
int Stride { get; }
/// <summary>
/// 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.
/// </summary>
int DownscaleLevel { get; }
/// <summary>
/// Gets the original width of the region (this equals <see cref="Width"/> if <see cref="DownscaleLevel"/> is 0).
/// </summary>
int UnscaledWidth { get; }
/// <summary>
/// Gets the original height of the region (this equals <see cref="Height"/> if <see cref="DownscaleLevel"/> is 0).
/// </summary>
int UnscaledHeight { get; }
/// <summary>
/// Gets the raw buffer of this zone.
/// </summary>
ReadOnlySpan<byte> RawBuffer { get; }
/// <summary>
/// Gets the <see cref="IImage"/> represented by the buffer of this zone.
/// </summary>
IImage Image { get; }
/// <summary>
/// Gets or sets if the <see cref="ICaptureZone"/> should be automatically updated on every captured frame.
/// </summary>
bool AutoUpdate { get; set; }
/// <summary>
/// Gets if an update for the <see cref="ICaptureZone"/> is requested on the next captured frame.
/// </summary>
bool IsUpdateRequested { get; }
/// <summary>
/// Occurs when the <see cref="ICaptureZone"/> is updated.
/// </summary>
event EventHandler? Updated;
/// <summary>
/// Locks the image for use. Unlock by disposing the returned disposable.
/// </summary>
/// <returns>The disposable used to unlock the image.</returns>
IDisposable Lock();
/// <summary>
/// Requests to update this <see cref="ICaptureZone"/> when the next frame is captured.
/// Only necessary if <see cref="AutoUpdate"/> is set to <c>false</c>.
/// </summary>
void RequestUpdate();
/// <summary>
/// Gets a <see cref="RefImage{TColor}"/>. Basically the same as <see cref="Image"/> but with better performance if the color-layout is known.
/// </summary>
/// <typeparam name="TColor">The color used by the buffer of this zone.</typeparam>
/// <returns>The <see cref="RefImage{TColor}"/> representing this zone.</returns>
RefImage<TColor> GetRefImage<TColor>() where TColor : struct, IColor;
}

View File

@ -0,0 +1,119 @@
using System.Collections.Generic;
namespace ScreenCapture.NET;
/// <summary>
/// Represents a image.
/// </summary>
public interface IImage : IEnumerable<IColor>
{
/// <summary>
/// Gets the width of this image.
/// </summary>
int Width { get; }
/// <summary>
/// Gets the height of this image.
/// </summary>
int Height { get; }
/// <summary>
/// Gets the color at the specified location.
/// </summary>
/// <param name="x">The X-location to read.</param>
/// <param name="y">The Y-location to read.</param>
/// <returns>The color at the specified location.</returns>
IColor this[int x, int y] { get; }
/// <summary>
/// Gets an image representing the specified location.
/// </summary>
/// <param name="x">The X-location of the image.</param>
/// <param name="y">The Y-location of the image.</param>
/// <param name="width">The width of the sub-image.</param>
/// <param name="height"></param>
/// <returns></returns>
IImage this[int x, int y, int width, int height] { get; }
/// <summary>
/// Gets a list of all rows of this image.
/// </summary>
IImageRows Rows { get; }
/// <summary>
/// Gets a list of all columns of this image.
/// </summary>
IImageColumns Columns { get; }
/// <summary>
/// Represents a list of rows of an image.
/// </summary>
public interface IImageRows : IEnumerable<IImageRow>
{
/// <summary>
/// Gets the amount of rows in this list.
/// </summary>
int Count { get; }
/// <summary>
/// Gets a specific <see cref="IImageRow"/>.
/// </summary>
/// <param name="column">The ´row to get.</param>
/// <returns>The requested <see cref="IImageRow"/>.</returns>
IImageRow this[int column] { get; }
}
/// <summary>
/// Represents a list of columns of an image.
/// </summary>
public interface IImageColumns : IEnumerable<IImageColumn>
{
/// <summary>
/// Gets the amount of columns in this list.
/// </summary>
int Count { get; }
/// <summary>
/// Gets a specific <see cref="IImageColumn"/>.
/// </summary>
/// <param name="column">The column to get.</param>
/// <returns>The requested <see cref="IImageColumn"/>.</returns>
IImageColumn this[int column] { get; }
}
/// <summary>
/// Represents a single row of an image.
/// </summary>
public interface IImageRow : IEnumerable<IColor>
{
/// <summary>
/// Gets the length of the row.
/// </summary>
int Length { get; }
/// <summary>
/// Gets the <see cref="IColor"/> at the specified location.
/// </summary>
/// <param name="x">The location to get the color from.</param>
/// <returns>The <see cref="IColor"/> at the specified location.</returns>
IColor this[int x] { get; }
}
/// <summary>
/// Represents a single column of an image.
/// </summary>
public interface IImageColumn : IEnumerable<IColor>
{
/// <summary>
/// Gets the length of the column.
/// </summary>
int Length { get; }
/// <summary>
/// Gets the <see cref="IColor"/> at the specified location.
/// </summary>
/// <param name="y">The location to get the color from.</param>
/// <returns>The <see cref="IColor"/> at the specified location.</returns>
IColor this[int y] { get; }
}
}

View File

@ -0,0 +1,339 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace ScreenCapture.NET;
/// <inheritdoc />
public sealed class Image<TColor> : IImage
where TColor : struct, IColor
{
#region Properties & Fields
private readonly byte[] _buffer;
private readonly int _x;
private readonly int _y;
private readonly int _stride;
/// <inheritdoc />
public int Width { get; }
/// <inheritdoc />
public int Height { get; }
#endregion
#region Indexer
/// <inheritdoc />
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<byte, TColor>(_buffer)[((_y + y) * _stride) + (_x + x)];
}
}
/// <inheritdoc />
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<TColor>(_buffer, _x + x, _y + y, width, height, _stride);
}
}
/// <inheritdoc />
public IImage.IImageRows Rows
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new ImageRows(_buffer, _x, _y, Width, Height, _stride);
}
/// <inheritdoc />
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
/// <inheritdoc />
public IEnumerator<IColor> GetEnumerator()
{
for (int y = 0; y < Height; y++)
for (int x = 0; x < Width; x++)
yield return this[x, y];
}
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
#endregion
#region Indexer-Classes
/// <inheritdoc />
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;
/// <inheritdoc />
public int Count => _height;
#endregion
#region Indexer
/// <inheritdoc />
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
/// <inheritdoc />
public IEnumerator<IImage.IImageRow> GetEnumerator()
{
for (int y = 0; y < _height; y++)
yield return this[y];
}
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
#endregion
}
/// <inheritdoc />
private sealed class ImageRow : IImage.IImageRow
{
#region Properties & Fields
private readonly byte[] _buffer;
private readonly int _start;
private readonly int _length;
/// <inheritdoc />
public int Length => _length;
#endregion
#region Indexer
/// <inheritdoc />
public IColor this[int x]
{
get
{
if ((x < 0) || (x >= _length)) throw new IndexOutOfRangeException();
ReadOnlySpan<TColor> row = MemoryMarshal.Cast<byte, TColor>(_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
/// <inheritdoc />
public IEnumerator<IColor> GetEnumerator()
{
for (int x = 0; x < _length; x++)
yield return this[x];
}
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
#endregion
}
/// <inheritdoc />
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;
/// <inheritdoc />
public int Count => _width;
#endregion
#region Indexer
/// <inheritdoc />
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
/// <inheritdoc />
public IEnumerator<IImage.IImageColumn> GetEnumerator()
{
for (int y = 0; y < _height; y++)
yield return this[y];
}
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
#endregion
}
/// <inheritdoc />
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;
/// <inheritdoc />
public int Length => _length;
#endregion
#region Indexer
/// <inheritdoc />
public IColor this[int y]
{
get
{
if ((y < 0) || (y >= _length)) throw new IndexOutOfRangeException();
ReadOnlySpan<TColor> row = MemoryMarshal.Cast<byte, TColor>(_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
/// <inheritdoc />
public IEnumerator<IColor> GetEnumerator()
{
for (int y = 0; y < _length; y++)
yield return this[y];
}
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
#endregion
}
#endregion
}

View File

@ -0,0 +1,330 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace ScreenCapture.NET;
public readonly ref struct RefImage<TColor>
where TColor : struct, IColor
{
#region Properties & Fields
private readonly ReadOnlySpan<TColor> _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<TColor> 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<TColor>(_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<TColor> 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<TColor> 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<TColor> target = dest;
foreach (ReadOnlyRefEnumerable<TColor> row in rows)
{
row.CopyTo(target);
target = target[Width..];
}
}
public TColor[] ToArray()
{
TColor[] array = new TColor[Width * Height];
CopyTo(array);
return array;
}
/// <inheritdoc cref="System.Collections.IEnumerable.GetEnumerator"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ImageEnumerator GetEnumerator() => new(_pixels);
#endregion
public ref struct ImageEnumerator
{
#region Properties & Fields
private readonly ReadOnlySpan<TColor> _pixels;
private int _position;
/// <inheritdoc cref="System.Collections.Generic.IEnumerator{T}.Current"/>
public TColor Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _pixels[_position];
}
#endregion
#region Constructors
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal ImageEnumerator(ReadOnlySpan<TColor> pixels)
{
this._pixels = pixels;
_position = -1;
}
#endregion
#region Methods
/// <inheritdoc cref="System.Collections.IEnumerator.MoveNext"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext() => ++_position < _pixels.Length;
#endregion
}
#region Indexer-Structs
public readonly ref struct ImageRows
{
#region Properties & Fields
private readonly ReadOnlySpan<TColor> _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<TColor> 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<TColor>(rr, _width, 1);
}
}
#endregion
#region Constructors
public ImageRows(ReadOnlySpan<TColor> 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
/// <inheritdoc cref="System.Collections.IEnumerable.GetEnumerator"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ImageRowsEnumerator GetEnumerator() => new(this);
#endregion
public ref struct ImageRowsEnumerator
{
#region Properties & Fields
private readonly ImageRows _rows;
private int _position;
/// <inheritdoc cref="System.Collections.Generic.IEnumerator{T}.Current"/>
public ReadOnlyRefEnumerable<TColor> 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
/// <inheritdoc cref="System.Collections.IEnumerator.MoveNext"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext() => ++_position < _rows._height;
#endregion
}
}
public readonly ref struct ImageColumns
{
#region Properties & Fields
private readonly ReadOnlySpan<TColor> _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<TColor> 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<TColor>(rc, _height, _stride);
}
}
#endregion
#region Constructors
public ImageColumns(ReadOnlySpan<TColor> 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
/// <inheritdoc cref="System.Collections.IEnumerable.GetEnumerator"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ImageColumnsEnumerator GetEnumerator() => new(this);
#endregion
public ref struct ImageColumnsEnumerator
{
#region Properties & Fields
private readonly ImageColumns _columns;
private int _position;
/// <inheritdoc cref="System.Collections.Generic.IEnumerator{T}.Current"/>
public ReadOnlyRefEnumerable<TColor> 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
/// <inheritdoc cref="System.Collections.IEnumerator.MoveNext"/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool MoveNext() => ++_position < _columns._width;
#endregion
}
}
#endregion
}

View File

@ -14,8 +14,8 @@
<AssemblyTitle>ScreenCapture.NET</AssemblyTitle>
<PackageId>ScreenCapture.NET</PackageId>
<RootNamespace>ScreenCapture.NET</RootNamespace>
<Description>Vortice based Screen-Capturing</Description>
<Summary>Vortice based Screen-Capturing using Desktop Duplication</Summary>
<Description>Core functionality for Screen-Capturing</Description>
<Summary>Base package for ScreenCapture.NET projects</Summary>
<Copyright>Copyright © Darth Affe 2023</Copyright>
<PackageCopyright>Copyright © Darth Affe 2023</PackageCopyright>
<PackageIcon>icon.png</PackageIcon>
@ -62,8 +62,4 @@
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Vortice.Direct3D11" Version="2.4.2" />
</ItemGroup>
</Project>

View File

@ -1,5 +1,9 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=communitytoolkit_002Ehighperformance/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=directx/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=events/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=extensions/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=generic/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=helper/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=model/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=model/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=model_005Ccolors/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -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
}

View File

@ -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<ColorARGB>? _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<ColorARGB> 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<ColorARGB> 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<ColorARGB> 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<ColorARGB> 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<ColorARGB> 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<ColorARGB> image = _captureZone!.Image;
Assert.AreEqual(image.Height, image.Rows.Count);
for (int y = 0; y < image.Height; y++)
{
ReadOnlyRefEnumerable<ColorARGB> 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<ColorARGB> image = _captureZone!.Image;
int y = 0;
foreach (ReadOnlyRefEnumerable<ColorARGB> 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<ColorARGB> image = _captureZone!.Image;
Assert.AreEqual(image.Width, image.Columns.Count);
for (int x = 0; x < image.Width; x++)
{
ReadOnlyRefEnumerable<ColorARGB> 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<ColorARGB> image = _captureZone!.Image;
int x = 0;
foreach (ReadOnlyRefEnumerable<ColorARGB> column in image.Columns)
{
for (int y = 0; y < column.Length; y++)
Assert.AreEqual(TestScreenCapture.GetColorFromLocation(x, y), column[y]);
x++;
}
}
#endregion
}

View File

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ScreenCapture.NET\ScreenCapture.NET.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,37 @@
using System;
using System.Runtime.InteropServices;
namespace ScreenCapture.NET.Tests;
internal class TestScreenCapture : AbstractScreenCapture<ColorARGB>
{
#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<ColorARGB> captureZone, in Span<byte> buffer)
{
Span<ColorARGB> pixels = MemoryMarshal.Cast<byte, ColorARGB>(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
}