Compare commits

..

No commits in common. "master" and "v1.3.1" have entirely different histories.

32 changed files with 809 additions and 2412 deletions

View File

@ -1,29 +0,0 @@
name: ScreenCapture.NET-Release
on:
release:
types: [published]
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: |
8.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore --configuration Release
- name: Test
run: dotnet test --no-build --verbosity normal --configuration Release
- name: Nuget Push
run: dotnet nuget push **\*.nupkg --skip-duplicate --api-key ${{ secrets.NUGET_TOKEN }} --source https://api.nuget.org/v3/index.json
- name: Symbols Push
run: dotnet nuget push **\*.snupkg --skip-duplicate --api-key ${{ secrets.NUGET_TOKEN }} --source https://api.nuget.org/v3/index.json

108
README.md
View File

@ -4,14 +4,6 @@
[![GitHub](https://img.shields.io/github/license/DarthAffe/ScreenCapture.NET?style=for-the-badge)](https://github.com/DarthAffe/ScreenCapture.NET/blob/master/LICENSE)
[![GitHub Repo stars](https://img.shields.io/github/stars/DarthAffe/ScreenCapture.NET?style=for-the-badge)](https://github.com/DarthAffe/ScreenCapture.NET/stargazers)
## NuGet-Packages:
| Package | Description |
|---------|-------------|
| [ScreenCapture.NET](https://www.nuget.org/packages/ScreenCapture.NET)| The core-package required to use ScreenCapture.NET captures or write your own. |
| [ScreenCapture.NET.DX11](https://www.nuget.org/packages/ScreenCapture.NET.DX11) | DirectX 11 based capturing. Fast and supports the whole set of features. **This should always be used if possible!** *Note: This might not work on windows hybrid systems (check [the docs](https://learn.microsoft.com/en-us/troubleshoot/windows-client/shell-experience/error-when-dda-capable-app-is-against-gpu) and [this issue](https://github.com/DarthAffe/ScreenCapture.NET/issues/39))*|
| [ScreenCapture.NET.DX9](https://www.nuget.org/packages/ScreenCapture.NET.DX9) | DirectX 9 based capturing. Slower then DX 11 and does not support rotated screens and GPU-accelerated downscaling. Only useful if the DX11 package can't be used for some reason. |
| [ScreenCapture.NET.X11](https://www.nuget.org/packages/ScreenCapture.NET.X11) | libX11 based capturing for the X-Window-System. Currently the only way to use ScreenCapture.NET on linux. Quite slow and can easily break depending on the X-Server config. Works on my machine, but it's not really a high proprity to support at the moment. Does not support rotated screens and GPU-accelerated downscaling. |
## Usage
```csharp
// Create a screen-capture service
@ -28,82 +20,48 @@ IScreenCapture screenCapture = screenCaptureService.GetScreenCapture(displays.Fi
// Register the regions you want to capture on the screen
// Capture the whole screen
ICaptureZone fullscreen = screenCapture.RegisterCaptureZone(0, 0, screenCapture.Display.Width, screenCapture.Display.Height);
CaptureZone fullscreen = screenCapture.RegisterCaptureZone(0, 0, screenCapture.Display.Width, screenCapture.Display.Height);
// Capture a 100x100 region at the top left and scale it down to 50x50
ICaptureZone topLeft = screenCapture.RegisterCaptureZone(0, 0, 100, 100, downscaleLevel: 1);
CaptureZone topLeft = screenCapture.RegisterCaptureZone(0, 0, 100, 100, downscaleLevel: 1);
// Capture the screen
// This should be done in a loop on a separate thread as CaptureScreen blocks if the screen is not updated (still image).
// This should be done in a loop on a seperate thread as CaptureScreen blocks if the screen is not updated (still image).
screenCapture.CaptureScreen();
// Do something with the captured image - e.g. access all pixels (same could be done with topLeft)
//Lock the zone to access the data. Remember to dispose the returned disposable to unlock again.
using (fullscreen.Lock())
// Locking is not neccessary in that case as we're capturing in the same thread,
// but when using a threaded-approach (which is recommended) it prevents potential tearing of the data in the buffer.
lock (fullscreen.Buffer)
{
// You have multiple options now:
// 1. Access the raw byte-data
ReadOnlySpan<byte> rawData = fullscreen.RawBuffer;
// Stride is the width in bytes of a row in the buffer (width in pixel * bytes per pixel)
int stride = fullscreen.Stride;
// 2. Use the provided abstraction to access pixels without having to care about low-level byte handling
// Get the image captured for the zone
IImage image = fullscreen.Image;
Span<byte> data = new(fullscreen.Buffer);
// Iterate all pixels of the image
foreach (IColor color in image)
Console.WriteLine($"A: {color.A}, R: {color.R}, G: {color.G}, B: {color.B}");
// Iterate all rows of the image
for (int y = 0; y < fullscreen.Height; y++)
{
// Select the actual data of the row
Span<byte> row = data.Slice(y * stride, stride);
// Get the pixel at location (x = 10, y = 20)
IColor imageColorExample = image[10, 20];
// Get the first row
IImageRow row = image.Rows[0];
// Get the 10th pixel of the row
IColor rowColorExample = row[10];
// Get the first column
IImageColumn column = image.Columns[0];
// Get the 10th pixel of the column
IColor columnColorExample = column[10];
// Cuts a rectangle out of the original image (x = 100, y = 150, width = 400, height = 300)
IImage subImage = image[100, 150, 400, 300];
// All of the things above (rows, columns, sub-images) do NOT allocate new memory so they are fast and memory efficient, but for that reason don't provide raw byte access.
}
```
If you know which Capture-provider you're using it performs a bit better to not use the abstraction but a more low-level approach instead.
This is the same example as above but without using the interfaces:
```csharp
DX11ScreenCaptureService screenCaptureService = new DX11ScreenCaptureService();
IEnumerable<GraphicsCard> graphicsCards = screenCaptureService.GetGraphicsCards();
IEnumerable<Display> displays = screenCaptureService.GetDisplays(graphicsCards.First());
DX11ScreenCapture screenCapture = screenCaptureService.GetScreenCapture(displays.First());
CaptureZone<ColorBGRA> fullscreen = screenCapture.RegisterCaptureZone(0, 0, screenCapture.Display.Width, screenCapture.Display.Height);
CaptureZone<ColorBGRA> topLeft = screenCapture.RegisterCaptureZone(0, 0, 100, 100, downscaleLevel: 1);
screenCapture.CaptureScreen();
using (fullscreen.Lock())
{
IImage<ColorBGRA> image = fullscreen.Image;
// You can also get a ref image which has a slight performance benefit in some cases
// RefImage<ColorBGRA> refImage = image.AsRefImage();
foreach (ColorBGRA color in image)
Console.WriteLine($"A: {color.A}, R: {color.R}, G: {color.G}, B: {color.B}");
ColorBGRA imageColorExample = image[10, 20];
ImageRow<ColorBGRA> row = image.Rows[0];
ColorBGRA rowColorExample = row[10];
ImageColumn<ColorBGRA> column = image.Columns[0];
ColorBGRA columnColorExample = column[10];
RefImage<ColorBGRA> subImage = image[100, 150, 400, 300];
// Iterate all pixels
for (int x = 0; x < row.Length; x += fullscreen.BytesPerPixel)
{
// Data is in BGRA format for the DX11ScreenCapture
byte b = row[x];
byte g = row[x + 1];
byte r = row[x + 2];
byte a = row[x + 3];
}
}
}
// Move the top left zone more towards the center
// Using the Update-method allows to move the zone without having to allocate
// new buffers and textures which yields a good performance gain if done at high framerates.
screenCapture.UpdateCaptureZone(topLeft, x: 100, y: 200);
// Note that resizing the zone is also possible but currently reinitializes the zone
// -> no performance gain compared to removing and readding the zone.
screenCapture.UpdateCaptureZone(topLeft, width: 20, height: 20);
```

View File

@ -1,516 +0,0 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using HPPH;
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 = [];
#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, 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(ReadOnlySpan<byte> source, int sourceStride, CaptureZone<ColorBGRA> captureZone, 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(ReadOnlySpan<byte> source, int sourceStride, CaptureZone<ColorBGRA> captureZone, 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(ReadOnlySpan<byte> source, int sourceStride, CaptureZone<ColorBGRA> captureZone, 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(ReadOnlySpan<byte> source, int sourceStride, CaptureZone<ColorBGRA> captureZone, 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(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)
{
foreach (ZoneTextures textures in _textures.Values)
textures.Dispose();
_textures.Clear();
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
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 705 B

View File

@ -1,72 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0</TargetFrameworks>
<RuntimeIdentifier>win-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.DX11</Title>
<AssemblyName>ScreenCapture.NET.DX11</AssemblyName>
<AssemblyTitle>ScreenCapture.NET.DX11</AssemblyTitle>
<PackageId>ScreenCapture.NET.DX11</PackageId>
<RootNamespace>ScreenCapture.NET</RootNamespace>
<Description>DirectX 11 based Screen-Capturing using the Desktop Duplication API</Description>
<Summary>DirectX 11 based Screen-Capturing using the Desktop Duplication API</Summary>
<Copyright>Copyright © Darth Affe 2024</Copyright>
<PackageCopyright>Copyright © Darth Affe 2024</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>3.0.0</Version>
<AssemblyVersion>3.0.0</AssemblyVersion>
<FileVersion>3.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.Direct3D11" Version="3.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ScreenCapture.NET\ScreenCapture.NET.csproj" />
</ItemGroup>
</Project>

View File

@ -1,2 +0,0 @@
<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

@ -1,184 +0,0 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using HPPH;
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, 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, Span<byte> buffer)
{
RefImage<ColorBGRA>.Wrap(_buffer, Display.Width, Display.Height, _stride)[captureZone.X, captureZone.Y, captureZone.Width, captureZone.Height]
.CopyTo(MemoryMarshal.Cast<byte, ColorBGRA>(buffer));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void DownscaleZone(CaptureZone<ColorBGRA> captureZone, Span<byte> buffer)
{
RefImage<ColorBGRA> source = RefImage<ColorBGRA>.Wrap(_buffer, Display.Width, Display.Height, _stride)[captureZone.X, captureZone.Y, captureZone.UnscaledWidth, captureZone.UnscaledHeight];
Span<ColorBGRA> target = MemoryMarshal.Cast<byte, ColorBGRA>(buffer);
int blockSize = 1 << captureZone.DownscaleLevel;
int width = captureZone.Width;
int height = captureZone.Height;
for (int y = 0; y < height; y++)
for (int x = 0; x < width; x++)
target[(y * width) + x] = source[x * blockSize, y * blockSize, blockSize, blockSize].Average();
}
/// <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

@ -1,97 +0,0 @@
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
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 705 B

View File

@ -1,72 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0</TargetFrameworks>
<RuntimeIdentifier>win-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.DX9</Title>
<AssemblyName>ScreenCapture.NET.DX9</AssemblyName>
<AssemblyTitle>ScreenCapture.NET.DX9</AssemblyTitle>
<PackageId>ScreenCapture.NET.DX9</PackageId>
<RootNamespace>ScreenCapture.NET</RootNamespace>
<Description>DirectX 9 based Screen-Capturing</Description>
<Summary>DirectX 9 based Screen-Capturing. Use DX11 if possible!</Summary>
<Copyright>Copyright © Darth Affe 2024</Copyright>
<PackageCopyright>Copyright © Darth Affe 2024</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>3.0.0</Version>
<AssemblyVersion>3.0.0</AssemblyVersion>
<FileVersion>3.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.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ScreenCapture.NET\ScreenCapture.NET.csproj" />
</ItemGroup>
</Project>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 705 B

View File

@ -1,68 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.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>libX11 based Screen-Capturing</Description>
<Summary>libX11 based Screen-Capturing</Summary>
<Copyright>Copyright © Darth Affe 2024</Copyright>
<PackageCopyright>Copyright © Darth Affe 2024</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>3.0.0</Version>
<AssemblyVersion>3.0.0</AssemblyVersion>
<FileVersion>3.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

@ -1,67 +0,0 @@
using System.Runtime.InteropServices;
namespace ScreenCapture.NET;
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
}
}

View File

@ -1,147 +0,0 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using HPPH;
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, 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, Span<byte> buffer)
{
RefImage<ColorBGRA>.Wrap(Data, Display.Width, Display.Height, _image.bytes_per_line)[captureZone.X, captureZone.Y, captureZone.Width, captureZone.Height]
.CopyTo(MemoryMarshal.Cast<byte, ColorBGRA>(buffer));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void DownscaleZone(CaptureZone<ColorBGRA> captureZone, Span<byte> buffer)
{
RefImage<ColorBGRA> source = RefImage<ColorBGRA>.Wrap(Data, Display.Width, Display.Height, _image.bytes_per_line)[captureZone.X, captureZone.Y, captureZone.UnscaledWidth, captureZone.UnscaledHeight];
Span<ColorBGRA> target = MemoryMarshal.Cast<byte, ColorBGRA>(buffer);
int blockSize = 1 << captureZone.DownscaleLevel;
int width = captureZone.Width;
int height = captureZone.Height;
for (int y = 0; y < height; y++)
for (int x = 0; x < width; x++)
target[(y * width) + x] = source[x * blockSize, y * blockSize, blockSize, blockSize].Average();
}
/// <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

@ -1,104 +0,0 @@
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,19 +1,9 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.6.33829.357
# Visual Studio Version 16
VisualStudioVersion = 16.0.31025.194
MinimumVisualStudioVersion = 10.0.40219.1
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}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScreenCapture.NET", "ScreenCapture.NET\ScreenCapture.NET.csproj", "{90596344-E012-4534-A933-3BD1B55469DC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -25,29 +15,10 @@ 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,5 +1,3 @@
<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,530 @@
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) = 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) = 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) CalculateScaledSize(int width, int height, int downscaleLevel)
{
if (downscaleLevel > 0)
for (int i = 0; i < downscaleLevel; i++)
{
width /= 2;
height /= 2;
}
if (width < 1) width = 1;
if (height < 1) height = 1;
return (width, height);
}
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

@ -12,9 +12,8 @@ public class DX11ScreenCaptureService : IScreenCaptureService
#region Properties & Fields
private readonly IDXGIFactory1 _factory;
private readonly Dictionary<Display, DX11ScreenCapture> _screenCaptures = new();
private bool _isDisposed;
private readonly Dictionary<Display, DX11ScreenCapture> _screenCaptures = new();
#endregion
@ -28,8 +27,6 @@ public class DX11ScreenCaptureService : IScreenCaptureService
DXGI.CreateDXGIFactory1(out _factory!).CheckError();
}
~DX11ScreenCaptureService() => Dispose();
#endregion
#region Methods
@ -37,8 +34,6 @@ 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)
{
@ -51,8 +46,6 @@ 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;
@ -66,7 +59,7 @@ public class DX11ScreenCaptureService : IScreenCaptureService
}
}
private static Rotation GetRotation(ModeRotation rotation) => rotation switch
private Rotation GetRotation(ModeRotation rotation) => rotation switch
{
ModeRotation.Rotate90 => Rotation.Rotation90,
ModeRotation.Rotate180 => Rotation.Rotation180,
@ -75,11 +68,8 @@ public class DX11ScreenCaptureService : IScreenCaptureService
};
/// <inheritdoc />
IScreenCapture IScreenCaptureService.GetScreenCapture(Display display) => GetScreenCapture(display);
public DX11ScreenCapture GetScreenCapture(Display display)
public IScreenCapture 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;
@ -88,17 +78,13 @@ public class DX11ScreenCaptureService : IScreenCaptureService
/// <inheritdoc />
public void Dispose()
{
if (_isDisposed) return;
foreach (DX11ScreenCapture screenCapture in _screenCaptures.Values)
screenCapture.Dispose();
_screenCaptures.Clear();
try { _factory.Dispose(); } catch { /**/ }
_factory.Dispose();
GC.SuppressFinalize(this);
_isDisposed = true;
}
#endregion

View File

@ -1,356 +0,0 @@
using HPPH;
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];
}
/// <summary>
/// Calculates the first row starting from the top with at least one non black pixel.
/// </summary>
/// <param name="image">The image to check.</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>
/// <returns>The row number of the first row with at least one non-black pixel.</returns>
public static int CalculateTop(IImage image, int threshold)
{
IImageRows rows = image.Rows;
for (int y = 0; y < rows.Count; y++)
{
IImageRow row = rows[y];
foreach (IColor color in row)
{
if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
return y;
}
}
return 0;
}
/// <summary>
/// Calculates the last row starting from the top with at least one non black pixel.
/// </summary>
/// <param name="image">The image to check.</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>
/// <returns>The row number of the last row with at least one non-black pixel.</returns>
public static int CalculateBottom(IImage image, int threshold)
{
IImageRows rows = image.Rows;
for (int y = rows.Count - 1; y >= 0; y--)
{
IImageRow row = rows[y];
foreach (IColor color in row)
{
if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
return y;
}
}
return rows.Count;
}
/// <summary>
/// Calculates the first column starting from the left with at least one non black pixel.
/// </summary>
/// <param name="image">The image to check.</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>
/// <returns>The column number of the first column with at least one non-black pixel.</returns>
public static int CalculateLeft(IImage image, int threshold)
{
IImageColumns columns = image.Columns;
for (int x = 0; x < columns.Count; x++)
{
IImageColumn column = columns[x];
foreach (IColor color in column)
{
if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
return x;
}
}
return 0;
}
/// <summary>
/// Calculates the last column starting from the top with at least one non black pixel.
/// </summary>
/// <param name="image">The image to check.</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>
/// <returns>The column number of the last column with at least one non-black pixel.</returns>
public static int CalculateRight(IImage image, int threshold)
{
IImageColumns columns = image.Columns;
for (int x = columns.Count - 1; x >= 0; x--)
{
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 Image
/// <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<T> RemoveBlackBars<T>(this IImage<T> image, int threshold = 0, bool removeTop = true, bool removeBottom = true, bool removeLeft = true, bool removeRight = true)
where T : 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];
}
/// <summary>
/// Calculates the first row starting from the top with at least one non black pixel.
/// </summary>
/// <param name="image">The image to check.</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>
/// <returns>The row number of the first row with at least one non-black pixel.</returns>
public static int CalculateTop<T>(IImage<T> image, int threshold)
where T : struct, IColor
{
ImageRows<T> rows = image.Rows;
for (int y = 0; y < rows.Count; y++)
{
ImageRow<T> row = rows[y];
foreach (T color in row)
{
if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
return y;
}
}
return 0;
}
/// <summary>
/// Calculates the last row starting from the top with at least one non black pixel.
/// </summary>
/// <param name="image">The image to check.</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>
/// <returns>The row number of the last row with at least one non-black pixel.</returns>
public static int CalculateBottom<T>(IImage<T> image, int threshold)
where T : struct, IColor
{
ImageRows<T> rows = image.Rows;
for (int y = rows.Count - 1; y >= 0; y--)
{
ImageRow<T> row = rows[y];
foreach (T color in row)
{
if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
return y;
}
}
return rows.Count;
}
/// <summary>
/// Calculates the first column starting from the left with at least one non black pixel.
/// </summary>
/// <param name="image">The image to check.</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>
/// <returns>The column number of the first column with at least one non-black pixel.</returns>
public static int CalculateLeft<T>(IImage<T> image, int threshold)
where T : struct, IColor
{
ImageColumns<T> columns = image.Columns;
for (int x = 0; x < columns.Count; x++)
{
ImageColumn<T> column = columns[x];
foreach (T color in column)
{
if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
return x;
}
}
return 0;
}
/// <summary>
/// Calculates the last column starting from the top with at least one non black pixel.
/// </summary>
/// <param name="image">The image to check.</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>
/// <returns>The column number of the last column with at least one non-black pixel.</returns>
public static int CalculateRight<T>(IImage<T> image, int threshold)
where T : struct, IColor
{
ImageColumns<T> columns = image.Columns;
for (int x = columns.Count - 1; x >= 0; x--)
{
ImageColumn<T> column = columns[x];
foreach (T 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 - 1;
int left = removeLeft ? CalculateLeft(image, threshold) : 0;
int right = removeRight ? CalculateRight(image, threshold) : image.Width - 1;
return image[left, top, (right - left) + 1, (bottom - top) + 1];
}
/// <summary>
/// Calculates the first row starting from the top with at least one non black pixel.
/// </summary>
/// <param name="image">The image to check.</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>
/// <returns>The row number of the first row with at least one non-black pixel.</returns>
public static int CalculateTop<TColor>(this RefImage<TColor> image, int threshold)
where TColor : struct, IColor
{
ImageRows<TColor> rows = image.Rows;
for (int y = 0; y < rows.Count; y++)
{
ImageRow<TColor> row = rows[y];
foreach (TColor color in row)
{
if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
return y;
}
}
return 0;
}
/// <summary>
/// Calculates the last row starting from the top with at least one non black pixel.
/// </summary>
/// <param name="image">The image to check.</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>
/// <returns>The row number of the last row with at least one non-black pixel.</returns>
public static int CalculateBottom<TColor>(this RefImage<TColor> image, int threshold)
where TColor : struct, IColor
{
ImageRows<TColor> rows = image.Rows;
for (int y = rows.Count - 1; y >= 0; y--)
{
ImageRow<TColor> row = rows[y];
foreach (TColor color in row)
{
if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
return y;
}
}
return rows.Count;
}
/// <summary>
/// Calculates the first column starting from the left with at least one non black pixel.
/// </summary>
/// <param name="image">The image to check.</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>
/// <returns>The column number of the first column with at least one non-black pixel.</returns>
public static int CalculateLeft<TColor>(this RefImage<TColor> image, int threshold)
where TColor : struct, IColor
{
ImageColumns<TColor> columns = image.Columns;
for (int x = 0; x < columns.Count; x++)
{
ImageColumn<TColor> column = columns[x];
foreach (TColor color in column)
{
if ((color.R > threshold) || (color.G > threshold) || (color.B > threshold))
return x;
}
}
return 0;
}
/// <summary>
/// Calculates the last column starting from the top with at least one non black pixel.
/// </summary>
/// <param name="image">The image to check.</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>
/// <returns>The column number of the last column with at least one non-black pixel.</returns>
public static int CalculateRight<TColor>(this RefImage<TColor> image, int threshold)
where TColor : struct, IColor
{
ImageColumns<TColor> columns = image.Columns;
for (int x = columns.Count - 1; x >= 0; x--)
{
ImageColumn<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

@ -1,248 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using HPPH;
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 ScreenCapture.
/// </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, 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="IDisposable.Dispose" />
protected virtual void Dispose(bool disposing) { }
#endregion
}

View File

@ -18,7 +18,7 @@ public interface IScreenCapture : IDisposable
event EventHandler<ScreenCaptureUpdatedEventArgs>? Updated;
/// <summary>
/// Attempts to capture the current frame showed on the <see cref="Display"/>.
/// Attemts to capture the current frame showed on the <see cref="Display"/>.
/// </summary>
/// <returns><c>true</c> if the current frame was captures successfully; otherwise, <c>false</c>.</returns>
bool CaptureScreen();
@ -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>
ICaptureZone RegisterCaptureZone(int x, int y, int width, int height, int downscaleLevel = 0);
CaptureZone 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(ICaptureZone captureZone);
bool UnregisterCaptureZone(CaptureZone captureZone);
/// <summary>
/// Updates the the given <see cref="CaptureScreen"/>.
@ -53,10 +53,10 @@ 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(ICaptureZone captureZone, int? x = null, int? y = null, int? width = null, int? height = null, int? downscaleLevel = null);
void UpdateCaptureZone(CaptureZone captureZone, int? x = null, int? y = null, int? width = null, int? height = null, int? downscaleLevel = null);
/// <summary>
/// Restarts the <see cref="IScreenCapture"/>.
/// </summary>
void Restart();
}
}

View File

@ -0,0 +1,141 @@
// 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,107 +1,93 @@
// ReSharper disable MemberCanBePrivate.Global
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using HPPH;
namespace ScreenCapture.NET;
/// <summary>
/// Represents a duplicated region on the screen.
/// </summary>
public sealed class CaptureZone<TColor> : ICaptureZone
where TColor : struct, IColor
public sealed class CaptureZone
{
#region Properties & Fields
private readonly object _lock = new();
/// <summary>
/// Gets the unique id of this <see cref="CaptureZone"/>.
/// </summary>
public int Id { get; }
/// <inheritdoc />
public Display Display { get; }
/// <inheritdoc />
public IColorFormat ColorFormat
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => TColor.ColorFormat;
}
/// <inheritdoc />
/// <summary>
/// Gets the x-location of the region on the screen.
/// </summary>
public int X { get; internal set; }
/// <inheritdoc />
/// <summary>
/// Gets the y-location of the region on the screen.
/// </summary>
public int Y { 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 width of the captured region.
/// </summary>
public int Width { get; internal set; }
/// <summary>
/// Gets the pixel-data of this zone.
/// Gets the height of the captured region.
/// </summary>
public ReadOnlySpan<TColor> Pixels
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => MemoryMarshal.Cast<byte, TColor>(RawBuffer);
}
public int Height { get; internal set; }
/// <summary>
/// Gets a <see cref="IImage{TColor}"/>.
/// 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>
public IImage<TColor> Image => Image<TColor>.Wrap(InternalBuffer, Width, Height, Stride);
/// <inheritdoc />
IImage ICaptureZone.Image => Image;
public int DownscaleLevel { get; internal set; }
/// <summary>
/// Gets a <see cref="RefImage{TColor}"/>.
/// Gets the original width of the region (this equals <see cref="Width"/> if <see cref="DownscaleLevel"/> is 0).
/// </summary>
public RefImage<TColor> RefImage
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => RefImage<TColor>.Wrap(RawBuffer, Width, Height, Stride);
}
public int UnscaledWidth { get; internal set; }
/// <inheritdoc />
/// <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>
public bool AutoUpdate { get; set; } = true;
/// <inheritdoc />
/// <summary>
/// Gets if an update for the <see cref="CaptureZone"/> is requested on the next captured frame.
/// </summary>
public bool IsUpdateRequested { get; private set; }
#endregion
#region Events
/// <inheritdoc />
/// <summary>
/// Occurs when the <see cref="CaptureZone"/> is updated.
/// </summary>
public event EventHandler? Updated;
#endregion
@ -109,9 +95,9 @@ public sealed class CaptureZone<TColor> : ICaptureZone
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="CaptureZone{T}"/> class.
/// Initializes a new instance of the <see cref="CaptureZone"/> class.
/// </summary>
/// <param name="id">The unique id of this <see cref="CaptureZone{T}"/>.</param>
/// <param name="id">The unique id of this <see cref="CaptureZone"/>.</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>
@ -121,97 +107,56 @@ public sealed class CaptureZone<TColor> : ICaptureZone
/// <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(Display display, int x, int y, int width, int height, int downscaleLevel, int unscaledWidth, int unscaledHeight)
internal CaptureZone(int id, int x, int y, int width, int height, int bytesPerPixel, int downscaleLevel, int unscaledWidth, int unscaledHeight, byte[] buffer)
{
this.Display = display;
this.Id = id;
this.X = x;
this.Y = y;
this.Width = width;
this.Height = height;
this.DownscaleLevel = downscaleLevel;
this.BytesPerPixel = bytesPerPixel;
this.UnscaledWidth = unscaledWidth;
this.UnscaledHeight = unscaledHeight;
this.DownscaleLevel = downscaleLevel;
this.Buffer = buffer;
InternalBuffer = new byte[Stride * Height];
BlackBars = new BlackBarDetection(this);
}
#endregion
#region Methods
/// <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 RefImage<T>.Wrap(RawBuffer, Width, Height, Stride);
}
/// <inheritdoc />
public IDisposable Lock()
{
Monitor.Enter(_lock);
return new UnlockDisposable(_lock);
}
/// <inheritdoc />
/// <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>
public void RequestUpdate() => IsUpdateRequested = true;
/// <summary>
/// Marks the <see cref="CaptureZone{T}"/> as updated.
/// Marks the <see cref="CaptureZone"/> as updated.
/// WARNING: This should not be called outside of an <see cref="IScreenCapture"/>!
/// </summary>
internal void SetUpdated()
public void SetUpdated()
{
IsUpdateRequested = false;
BlackBars.InvalidateCache();
Updated?.Invoke(this, EventArgs.Empty);
}
internal void Resize(int width, int height, int downscaleLevel, int unscaledWidth, int unscaledHeight)
{
Width = width;
Height = height;
DownscaleLevel = downscaleLevel;
UnscaledWidth = unscaledWidth;
UnscaledHeight = unscaledHeight;
/// <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;
int newBufferSize = Stride * Height;
if (newBufferSize != InternalBuffer.Length)
InternalBuffer = new byte[newBufferSize];
}
/// <inheritdoc />
public override bool Equals(object? obj) => obj is CaptureZone other && Equals(other);
/// <inheritdoc />
public override int GetHashCode() => Id;
#endregion
private class UnlockDisposable : IDisposable
{
#region Properties & Fields
private bool _disposed = false;
private readonly object _lock;
#endregion
#region Constructors
// ReSharper disable once ConvertToPrimaryConstructor
public UnlockDisposable(object @lock) => this._lock = @lock;
~UnlockDisposable() => Dispose();
#endregion
#region Methods
/// <inheritdoc />
public void Dispose()
{
ObjectDisposedException.ThrowIf(_disposed, this);
Monitor.Exit(_lock);
_disposed = true;
GC.SuppressFinalize(this);
}
#endregion
}
}

View File

@ -1,104 +0,0 @@
using System;
using HPPH;
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>
IColorFormat 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

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0</TargetFrameworks>
<TargetFrameworks>net7.0;net6.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@ -14,10 +14,10 @@
<AssemblyTitle>ScreenCapture.NET</AssemblyTitle>
<PackageId>ScreenCapture.NET</PackageId>
<RootNamespace>ScreenCapture.NET</RootNamespace>
<Description>Core functionality for Screen-Capturing</Description>
<Summary>Base package for ScreenCapture.NET projects</Summary>
<Copyright>Copyright © Darth Affe 2024</Copyright>
<PackageCopyright>Copyright © Darth Affe 2024</PackageCopyright>
<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>
@ -26,11 +26,12 @@
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageReleaseNotes>
- Fixed striding issue on downscaled captures introduced with 1.3.0
</PackageReleaseNotes>
<Version>3.0.0</Version>
<AssemblyVersion>3.0.0</AssemblyVersion>
<FileVersion>3.0.0</FileVersion>
<Version>1.3.1</Version>
<AssemblyVersion>1.3.1</AssemblyVersion>
<FileVersion>1.3.1</FileVersion>
<OutputPath>..\bin\</OutputPath>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
@ -61,7 +62,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="HPPH" Version="1.0.0" />
<PackageReference Include="Vortice.Direct3D11" Version="2.4.2" />
</ItemGroup>
</Project>

View File

@ -1,9 +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: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>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=model_005Ccolors/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=model/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -1,25 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0</TargetFrameworks>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.5.0" />
<PackageReference Include="MSTest.TestFramework" Version="3.5.0" />
<PackageReference Include="coverlet.collector" Version="6.0.2">
<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

@ -1,38 +0,0 @@
using System;
using System.Runtime.InteropServices;
using HPPH;
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, 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
}