Added DX9 capture

This commit is contained in:
Darth Affe 2023-09-07 23:59:52 +02:00
parent 6919cf7b60
commit 2667f84c42
8 changed files with 616 additions and 5 deletions

View File

@ -0,0 +1,218 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using ScreenCapture.NET.Downscale;
using SharpGen.Runtime;
using Vortice.Direct3D9;
namespace ScreenCapture.NET;
/// <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 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="display">The <see cref="Display"/> to duplicate.</param>
public DX9ScreenCapture(IDirect3D9 direct3D9, Display display)
: base(display)
{
this._direct3D9 = direct3D9;
Restart();
}
#endregion
#region Methods
protected override bool PerformScreenCapture()
{
bool result = false;
lock (_captureLock)
{
if ((_device == null) || (_surface == null) || (_buffer == null))
{
Restart();
return false;
}
try
{
_device.GetFrontBufferData(0, _surface);
LockedRectangle dr = _surface.LockRect(LockFlags.NoSystemLock | LockFlags.ReadOnly);
nint ptr = dr.DataPointer;
for (int y = 0; y < Display.Height; y++)
{
Marshal.Copy(ptr, _buffer, y * _stride, _stride);
ptr += dr.Pitch;
}
_surface.UnlockRect();
result = true;
}
catch (SharpGenException dxException)
{
if (dxException.ResultCode == Result.AccessDenied)
{
try
{
Restart();
}
catch { Thread.Sleep(100); }
}
}
}
return result;
}
protected override void PerformCaptureZoneUpdate(CaptureZone<ColorBGRA> captureZone, in Span<byte> buffer)
{
if (_buffer == null) return;
using IDisposable @lock = captureZone.Lock();
{
if (captureZone.DownscaleLevel == 0)
CopyZone(captureZone, buffer);
else
DownscaleZone(captureZone, buffer);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void CopyZone(CaptureZone<ColorBGRA> captureZone, in Span<byte> buffer)
{
ReadOnlySpan<ColorBGRA> source = MemoryMarshal.Cast<byte, ColorBGRA>(_buffer);
Span<ColorBGRA> target = MemoryMarshal.Cast<byte, ColorBGRA>(buffer);
int offsetX = captureZone.X;
int offsetY = captureZone.Y;
int width = captureZone.Width;
int height = captureZone.Height;
for (int y = 0; y < height; y++)
{
int sourceOffset = ((y + offsetY) * Display.Width) + offsetX;
int targetOffset = y * width;
source.Slice(sourceOffset, width).CopyTo(target.Slice(targetOffset, width));
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void DownscaleZone(CaptureZone<ColorBGRA> captureZone, in Span<byte> buffer)
{
ReadOnlySpan<byte> source = _buffer;
Span<byte> target = buffer;
int blockSize = captureZone.DownscaleLevel switch
{
1 => 2,
2 => 4,
3 => 8,
4 => 16,
5 => 32,
6 => 64,
7 => 128,
8 => 256,
_ => (int)Math.Pow(2, captureZone.DownscaleLevel),
};
int offsetX = captureZone.X;
int offsetY = captureZone.Y;
int width = captureZone.Width;
int height = captureZone.Height;
int stride = captureZone.Stride;
int bpp = captureZone.ColorFormat.BytesPerPixel;
int unscaledWith = captureZone.UnscaledWidth;
Span<byte> scaleBuffer = stackalloc byte[bpp];
for (int y = 0; y < height; y++)
for (int x = 0; x < width; x++)
{
AverageByteSampler.Sample(new SamplerInfo<byte>((x + offsetX) * blockSize, (y + offsetY) * blockSize, blockSize, blockSize, unscaledWith, bpp, source), scaleBuffer);
int targetOffset = (y * stride) + (x * bpp);
// DarthAffe 07.09.2023: Unroll as optimization since we know it's always 4 bpp - not ideal but it does quite a lot
target[targetOffset] = scaleBuffer[0];
target[targetOffset + 1] = scaleBuffer[1];
target[targetOffset + 2] = scaleBuffer[2];
target[targetOffset + 3] = scaleBuffer[3];
}
}
/// <inheritdoc />
public override void Restart()
{
base.Restart();
lock (_captureLock)
{
DisposeDX();
try
{
PresentParameters presentParameters = new()
{
BackBufferWidth = Display.Width,
BackBufferHeight = Display.Height,
Windowed = true,
SwapEffect = SwapEffect.Discard
};
_device = _direct3D9.CreateDevice(Display.Index, DeviceType.Hardware, IntPtr.Zero, CreateFlags.SoftwareVertexProcessing, presentParameters);
_surface = _device.CreateOffscreenPlainSurface(Display.Width, Display.Height, Format.A8R8G8B8, Pool.Scratch);
_stride = Display.Width * ColorBGRA.ColorFormat.BytesPerPixel;
_buffer = new byte[Display.Height * _stride];
}
catch
{
DisposeDX();
}
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
DisposeDX();
}
private void DisposeDX()
{
try
{
try { _surface?.Dispose(); } catch { /**/}
try { _device?.Dispose(); } catch { /**/}
_buffer = null;
_device = null;
_surface = null;
}
catch { /**/ }
}
#endregion
}

View File

@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using Vortice.Direct3D9;
namespace ScreenCapture.NET;
/// <summary>
/// Represents a <see cref="IScreenCaptureService"/> using the <see cref="DX9ScreenCapture"/>.
/// </summary>
public class DX9ScreenCaptureService : IScreenCaptureService
{
#region Properties & Fields
private readonly IDirect3D9 _direct3D9;
private readonly Dictionary<Display, DX9ScreenCapture> _screenCaptures = new();
private bool _isDisposed;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="DX9ScreenCaptureService"/> class.
/// </summary>
public DX9ScreenCaptureService()
{
_direct3D9 = D3D9.Direct3DCreate9();
}
~DX9ScreenCaptureService() => Dispose();
#endregion
#region Methods
/// <inheritdoc />
public IEnumerable<GraphicsCard> GetGraphicsCards()
{
if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
Dictionary<int, GraphicsCard> graphicsCards = new();
for (int i = 0; i < _direct3D9.AdapterCount; i++)
{
AdapterIdentifier adapterIdentifier = _direct3D9.GetAdapterIdentifier(i);
if (!graphicsCards.ContainsKey(adapterIdentifier.DeviceId))
graphicsCards.Add(adapterIdentifier.DeviceId, new GraphicsCard(i, adapterIdentifier.Description, adapterIdentifier.VendorId, adapterIdentifier.DeviceId));
}
return graphicsCards.Values;
}
/// <inheritdoc />
public IEnumerable<Display> GetDisplays(GraphicsCard graphicsCard)
{
if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
for (int i = 0; i < _direct3D9.AdapterCount; i++)
{
AdapterIdentifier adapterIdentifier = _direct3D9.GetAdapterIdentifier(i);
if (adapterIdentifier.DeviceId == graphicsCard.DeviceId)
{
DisplayMode displayMode = _direct3D9.GetAdapterDisplayMode(i);
yield return new Display(i, adapterIdentifier.DeviceName, displayMode.Width, displayMode.Height, Rotation.None, graphicsCard);
}
}
}
/// <inheritdoc />
IScreenCapture IScreenCaptureService.GetScreenCapture(Display display) => GetScreenCapture(display);
public DX9ScreenCapture GetScreenCapture(Display display)
{
if (_isDisposed) throw new ObjectDisposedException(GetType().FullName);
if (!_screenCaptures.TryGetValue(display, out DX9ScreenCapture? screenCapture))
_screenCaptures.Add(display, screenCapture = new DX9ScreenCapture(_direct3D9, display));
return screenCapture;
}
/// <inheritdoc />
public void Dispose()
{
if (_isDisposed) return;
foreach (DX9ScreenCapture screenCapture in _screenCaptures.Values)
screenCapture.Dispose();
_screenCaptures.Clear();
try { _direct3D9.Dispose(); } catch { /**/ }
GC.SuppressFinalize(this);
_isDisposed = true;
}
#endregion
}

View File

@ -0,0 +1,149 @@
// DarthAffe 07.09.2023: Copied from https://github.com/DarthAffe/RGB.NET/blob/2e0754f474b82ed4d0cae5c6c44378d234f1321b/RGB.NET.Presets/Textures/Sampler/AverageByteSampler.cs
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
namespace ScreenCapture.NET.Downscale;
/// <summary>
/// Represents a sampled that averages multiple byte-data entries.
/// </summary>
internal static class AverageByteSampler
{
#region Constants
private static readonly int INT_VECTOR_LENGTH = Vector<uint>.Count;
#endregion
#region Methods
public static unsafe void Sample(in SamplerInfo<byte> info, in Span<byte> pixelData)
{
int count = info.Width * info.Height;
if (count == 0) return;
int dataLength = pixelData.Length;
Span<uint> sums = stackalloc uint[dataLength];
int elementsPerVector = Vector<byte>.Count / dataLength;
int valuesPerVector = elementsPerVector * dataLength;
if (Vector.IsHardwareAccelerated && (info.Height > 1) && (info.Width >= valuesPerVector) && (dataLength <= Vector<byte>.Count))
{
int chunks = info.Width / elementsPerVector;
Vector<uint> sum1 = Vector<uint>.Zero;
Vector<uint> sum2 = Vector<uint>.Zero;
Vector<uint> sum3 = Vector<uint>.Zero;
Vector<uint> sum4 = Vector<uint>.Zero;
for (int y = 0; y < info.Height; y++)
{
ReadOnlySpan<byte> data = info[y];
fixed (byte* colorPtr = data)
{
byte* current = colorPtr;
for (int i = 0; i < chunks; i++)
{
Vector<byte> bytes = *(Vector<byte>*)current;
Vector.Widen(bytes, out Vector<ushort> short1, out Vector<ushort> short2);
Vector.Widen(short1, out Vector<uint> int1, out Vector<uint> int2);
Vector.Widen(short2, out Vector<uint> int3, out Vector<uint> int4);
sum1 = Vector.Add(sum1, int1);
sum2 = Vector.Add(sum2, int2);
sum3 = Vector.Add(sum3, int3);
sum4 = Vector.Add(sum4, int4);
current += valuesPerVector;
}
}
int missingElements = data.Length - (chunks * valuesPerVector);
int offset = chunks * valuesPerVector;
for (int i = 0; i < missingElements; i += dataLength)
for (int j = 0; j < sums.Length; j++)
sums[j] += data[offset + i + j];
}
int value = 0;
int sumIndex = 0;
for (int j = 0; (j < INT_VECTOR_LENGTH) && (value < valuesPerVector); j++)
{
sums[sumIndex] += sum1[j];
++sumIndex;
++value;
if (sumIndex >= dataLength)
sumIndex = 0;
}
for (int j = 0; (j < INT_VECTOR_LENGTH) && (value < valuesPerVector); j++)
{
sums[sumIndex] += sum2[j];
++sumIndex;
++value;
if (sumIndex >= dataLength)
sumIndex = 0;
}
for (int j = 0; (j < INT_VECTOR_LENGTH) && (value < valuesPerVector); j++)
{
sums[sumIndex] += sum3[j];
++sumIndex;
++value;
if (sumIndex >= dataLength)
sumIndex = 0;
}
for (int j = 0; (j < INT_VECTOR_LENGTH) && (value < valuesPerVector); j++)
{
sums[sumIndex] += sum4[j];
++sumIndex;
++value;
if (sumIndex >= dataLength)
sumIndex = 0;
}
}
else
{
for (int y = 0; y < info.Height; y++)
{
ReadOnlySpan<byte> data = info[y];
for (int i = 0; i < data.Length; i += dataLength)
for (int j = 0; j < sums.Length; j++)
sums[j] += data[i + j];
}
}
float divisor = count * byte.MaxValue;
for (int i = 0; i < pixelData.Length; i++)
pixelData[i] = (sums[i] / divisor).GetByteValueFromPercentage();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static byte GetByteValueFromPercentage(this float percentage)
{
if (float.IsNaN(percentage)) return 0;
percentage = percentage.Clamp(0, 1.0f);
return (byte)(percentage >= 1.0f ? 255 : percentage * 256.0f);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float Clamp(this float value, float min, float max)
{
// ReSharper disable ConvertIfStatementToReturnStatement - I'm not sure why, but inlining this statement reduces performance by ~10%
if (value < min) return min;
if (value > max) return max;
return value;
// ReSharper restore ConvertIfStatementToReturnStatement
}
#endregion
}

View File

@ -0,0 +1,63 @@
// DarthAffe 07.09.2023: Copied from https://github.com/DarthAffe/RGB.NET/blob/2e0754f474b82ed4d0cae5c6c44378d234f1321b/RGB.NET.Core/Rendering/Textures/Sampler/SamplerInfo.cs
using System;
namespace ScreenCapture.NET.Downscale;
/// <summary>
/// Represents the information used to sample data.
/// </summary>
/// <typeparam name="T">The type of the data to sample.</typeparam>
internal readonly ref struct SamplerInfo<T>
{
#region Properties & Fields
private readonly ReadOnlySpan<T> _data;
private readonly int _x;
private readonly int _y;
private readonly int _stride;
private readonly int _dataPerPixel;
private readonly int _dataWidth;
/// <summary>
/// Gets the width of the region the data comes from.
/// </summary>
public readonly int Width;
/// <summary>
/// Gets the height of region the data comes from.
/// </summary>
public readonly int Height;
/// <summary>
/// Gets the data for the requested row.
/// </summary>
/// <param name="row">The row to get the data for.</param>
/// <returns>A readonly span containing the data of the row.</returns>
public ReadOnlySpan<T> this[int row] => _data.Slice((((_y + row) * _stride) + _x) * _dataPerPixel, _dataWidth);
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="SamplerInfo{T}" /> class.
/// </summary>
/// <param name="width">The width of the region the data comes from.</param>
/// <param name="height">The height of region the data comes from.</param>
/// <param name="data">The data to sample.</param>
public SamplerInfo(int x, int y, int width, int height, int stride, int dataPerPixel, in ReadOnlySpan<T> data)
{
this._x = x;
this._y = y;
this._data = data;
this._stride = stride;
this._dataPerPixel = dataPerPixel;
this.Width = width;
this.Height = height;
_dataWidth = width * dataPerPixel;
}
#endregion
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

View File

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

View File

@ -9,7 +9,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScreenCapture.NET.DX11", "S
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{CF7A1475-3A44-4870-A80F-5988DA25418B}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{CF7A1475-3A44-4870-A80F-5988DA25418B}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScreenCapture.NET.Tests", "Tests\ScreenCapture.NET.Tests\ScreenCapture.NET.Tests.csproj", "{AA1829BB-EFA7-4BB8-8041-76374659A42B}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScreenCapture.NET.Tests", "Tests\ScreenCapture.NET.Tests\ScreenCapture.NET.Tests.csproj", "{AA1829BB-EFA7-4BB8-8041-76374659A42B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScreenCapture.NET.DX9", "ScreenCapture.NET.DX9\ScreenCapture.NET.DX9.csproj", "{27EB5B17-2F83-43BA-A21F-06D93948B8BF}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -29,6 +31,10 @@ Global
{AA1829BB-EFA7-4BB8-8041-76374659A42B}.Debug|Any CPU.Build.0 = Debug|Any CPU {AA1829BB-EFA7-4BB8-8041-76374659A42B}.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.ActiveCfg = Release|Any CPU
{AA1829BB-EFA7-4BB8-8041-76374659A42B}.Release|Any CPU.Build.0 = Release|Any CPU {AA1829BB-EFA7-4BB8-8041-76374659A42B}.Release|Any CPU.Build.0 = Release|Any CPU
{27EB5B17-2F83-43BA-A21F-06D93948B8BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{27EB5B17-2F83-43BA-A21F-06D93948B8BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{27EB5B17-2F83-43BA-A21F-06D93948B8BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{27EB5B17-2F83-43BA-A21F-06D93948B8BF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@ -9,10 +9,13 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" /> <PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" /> <PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
<PackageReference Include="coverlet.collector" Version="3.2.0" /> <PackageReference Include="coverlet.collector" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>