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; /// /// Represents a ScreenCapture using DirectX 11 desktop duplicaton. /// https://docs.microsoft.com/en-us/windows/win32/direct3ddxgi/desktop-dup-api /// // ReSharper disable once InconsistentNaming public sealed class DX11ScreenCapture : AbstractScreenCapture { #region Constants private static readonly FeatureLevel[] FEATURE_LEVELS = [ FeatureLevel.Level_11_1, FeatureLevel.Level_11_0, FeatureLevel.Level_10_1, FeatureLevel.Level_10_0 ]; #endregion #region Properties & Fields private readonly object _captureLock = new(); private readonly bool _useNewDuplicationAdapter; /// /// Gets or sets the timeout in ms used for screen-capturing. (default 1000ms) /// This is used in https://docs.microsoft.com/en-us/windows/win32/api/dxgi1_2/nf-dxgi1_2-idxgioutputduplication-acquirenextframe /// // ReSharper disable once MemberCanBePrivate.Global public int Timeout { get; set; } = 1000; private readonly IDXGIFactory1 _factory; private IDXGIOutput? _output; private IDXGIOutputDuplication? _duplicatedOutput; private ID3D11Device? _device; private ID3D11DeviceContext? _context; private ID3D11Texture2D? _captureTexture; private readonly Dictionary, ZoneTextures> _textures = []; #endregion #region Constructors /// /// Initializes a new instance of the class. /// /// /// Note that setting useNewDuplicationAdapter to true requires to call DPIAwareness.Initalize(); and prevents the capture from running in a WPF-thread. /// /// The used to create underlying objects. /// The to duplicate. /// Indicates if the DuplicateOutput1 interface should be used instead of the older DuplicateOutput. Currently there's no real use in setting this to true. internal DX11ScreenCapture(IDXGIFactory1 factory, Display display, bool useNewDuplicationAdapter = false) : base(display) { this._factory = factory; this._useNewDuplicationAdapter = useNewDuplicationAdapter; Restart(); } #endregion #region Methods /// protected override bool PerformScreenCapture() { bool result = false; lock (_captureLock) { if ((_context == null) || (_duplicatedOutput == null) || (_captureTexture == null)) { Restart(); return false; } try { IDXGIResource? screenResource = null; try { _duplicatedOutput.AcquireNextFrame(Timeout, out OutduplFrameInfo duplicateFrameInformation, out screenResource).CheckError(); if ((screenResource == null) || (duplicateFrameInformation.LastPresentTime == 0)) return false; using ID3D11Texture2D screenTexture = screenResource.QueryInterface(); _context.CopySubresourceRegion(_captureTexture, 0, 0, 0, 0, screenTexture, 0); } finally { try { screenResource?.Dispose(); _duplicatedOutput?.ReleaseFrame(); } catch { /**/ } } result = true; } catch (SharpGenException dxException) { if ((dxException.ResultCode == ResultCode.AccessLost) || (dxException.ResultCode == ResultCode.AccessDenied) || (dxException.ResultCode == ResultCode.InvalidCall)) { try { Restart(); } catch { Thread.Sleep(100); } } } } return result; } /// protected override void PerformCaptureZoneUpdate(CaptureZone captureZone, Span buffer) { if (_context == null) return; lock (_textures) { if (!_textures.TryGetValue(captureZone, out ZoneTextures? textures)) return; if (textures.ScalingTexture != null) { _context.CopySubresourceRegion(textures.ScalingTexture, 0, 0, 0, 0, _captureTexture, 0, new Box(textures.X, textures.Y, 0, textures.X + textures.UnscaledWidth, textures.Y + textures.UnscaledHeight, 1)); _context.GenerateMips(textures.ScalingTextureView); _context.CopySubresourceRegion(textures.StagingTexture, 0, 0, 0, 0, textures.ScalingTexture, captureZone.DownscaleLevel); } else _context.CopySubresourceRegion(textures.StagingTexture, 0, 0, 0, 0, _captureTexture, 0, new Box(textures.X, textures.Y, 0, textures.X + textures.UnscaledWidth, textures.Y + textures.UnscaledHeight, 1)); MappedSubresource mapSource = _context.Map(textures.StagingTexture, 0, MapMode.Read, MapFlags.None); using IDisposable @lock = captureZone.Lock(); { ReadOnlySpan source = mapSource.AsSpan(mapSource.RowPitch * textures.Height); switch (Display.Rotation) { case Rotation.Rotation90: CopyRotate90(source, mapSource.RowPitch, captureZone, buffer); break; case Rotation.Rotation180: CopyRotate180(source, mapSource.RowPitch, captureZone, buffer); break; case Rotation.Rotation270: CopyRotate270(source, mapSource.RowPitch, captureZone, buffer); break; default: CopyRotate0(source, mapSource.RowPitch, captureZone, buffer); break; } } _context.Unmap(textures.StagingTexture, 0); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CopyRotate0(ReadOnlySpan source, int sourceStride, CaptureZone captureZone, Span buffer) { int height = captureZone.Height; int stride = captureZone.Stride; Span target = buffer; for (int y = 0; y < height; y++) { int sourceOffset = y * sourceStride; int targetOffset = y * stride; source.Slice(sourceOffset, stride).CopyTo(target.Slice(targetOffset, stride)); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CopyRotate90(ReadOnlySpan source, int sourceStride, CaptureZone captureZone, Span buffer) { int width = captureZone.Width; int height = captureZone.Height; int usedBytesPerLine = height * captureZone.ColorFormat.BytesPerPixel; Span target = MemoryMarshal.Cast(buffer); for (int x = 0; x < width; x++) { ReadOnlySpan src = MemoryMarshal.Cast(source.Slice(x * sourceStride, usedBytesPerLine)); for (int y = 0; y < src.Length; y++) target[(y * width) + (width - x - 1)] = src[y]; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CopyRotate180(ReadOnlySpan source, int sourceStride, CaptureZone captureZone, Span buffer) { int width = captureZone.Width; int height = captureZone.Height; int bpp = captureZone.ColorFormat.BytesPerPixel; int usedBytesPerLine = width * bpp; Span target = MemoryMarshal.Cast(buffer); for (int y = 0; y < height; y++) { ReadOnlySpan src = MemoryMarshal.Cast(source.Slice(y * sourceStride, usedBytesPerLine)); for (int x = 0; x < src.Length; x++) target[((height - y - 1) * width) + (width - x - 1)] = src[x]; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CopyRotate270(ReadOnlySpan source, int sourceStride, CaptureZone captureZone, Span buffer) { int width = captureZone.Width; int height = captureZone.Height; int usedBytesPerLine = height * captureZone.ColorFormat.BytesPerPixel; Span target = MemoryMarshal.Cast(buffer); for (int x = 0; x < width; x++) { ReadOnlySpan src = MemoryMarshal.Cast(source.Slice(x * sourceStride, usedBytesPerLine)); for (int y = 0; y < src.Length; y++) target[((height - y - 1) * width) + x] = src[y]; } } /// public override CaptureZone RegisterCaptureZone(int x, int y, int width, int height, int downscaleLevel = 0) { CaptureZone captureZone = base.RegisterCaptureZone(x, y, width, height, downscaleLevel); lock (_textures) InitializeCaptureZone(captureZone); return captureZone; } /// public override bool UnregisterCaptureZone(CaptureZone captureZone) { if (!base.UnregisterCaptureZone(captureZone)) return false; lock (_textures) { if (_textures.TryGetValue(captureZone, out ZoneTextures? textures)) { textures.Dispose(); _textures.Remove(captureZone); return true; } return false; } } /// public override void UpdateCaptureZone(CaptureZone captureZone, int? x = null, int? y = null, int? width = null, int? height = null, int? downscaleLevel = null) { base.UpdateCaptureZone(captureZone, x, y, width, height, downscaleLevel); //TODO DarthAffe 01.05.2022: For now just reinitialize the zone in that case, but this could be optimized to only recreate the textures needed. if ((width != null) || (height != null) || (downscaleLevel != null)) { lock (_textures) { if (_textures.TryGetValue(captureZone, out ZoneTextures? textures)) { textures.Dispose(); InitializeCaptureZone(captureZone); } } } } /// protected override void ValidateCaptureZoneAndThrow(int x, int y, int width, int height, int downscaleLevel) { if (_device == null) throw new ApplicationException("ScreenCapture isn't initialized."); base.ValidateCaptureZoneAndThrow(x, y, width, height, downscaleLevel); } private void InitializeCaptureZone(CaptureZone captureZone) { int x; int y; int width; int height; int unscaledWidth; int unscaledHeight; if (captureZone.Display.Rotation is Rotation.Rotation90 or Rotation.Rotation270) { x = captureZone.Y; y = captureZone.X; width = captureZone.Height; height = captureZone.Width; unscaledWidth = captureZone.UnscaledHeight; unscaledHeight = captureZone.UnscaledWidth; } else { x = captureZone.X; y = captureZone.Y; width = captureZone.Width; height = captureZone.Height; unscaledWidth = captureZone.UnscaledWidth; unscaledHeight = captureZone.UnscaledHeight; } Texture2DDescription stagingTextureDesc = new() { CPUAccessFlags = CpuAccessFlags.Read, BindFlags = BindFlags.None, Format = Format.B8G8R8A8_UNorm, Width = width, Height = height, MiscFlags = ResourceOptionFlags.None, MipLevels = 1, ArraySize = 1, SampleDescription = { Count = 1, Quality = 0 }, Usage = ResourceUsage.Staging }; ID3D11Texture2D stagingTexture = _device!.CreateTexture2D(stagingTextureDesc); ID3D11Texture2D? scalingTexture = null; ID3D11ShaderResourceView? scalingTextureView = null; if (captureZone.DownscaleLevel > 0) { Texture2DDescription scalingTextureDesc = new() { CPUAccessFlags = CpuAccessFlags.None, BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource, Format = Format.B8G8R8A8_UNorm, Width = unscaledWidth, Height = unscaledHeight, MiscFlags = ResourceOptionFlags.GenerateMips, MipLevels = captureZone.DownscaleLevel + 1, ArraySize = 1, SampleDescription = { Count = 1, Quality = 0 }, Usage = ResourceUsage.Default }; scalingTexture = _device!.CreateTexture2D(scalingTextureDesc); scalingTextureView = _device.CreateShaderResourceView(scalingTexture); } _textures[captureZone] = new ZoneTextures(x, y, width, height, unscaledWidth, unscaledHeight, stagingTexture, scalingTexture, scalingTextureView); } /// public override void Restart() { base.Restart(); lock (_captureLock) lock (_textures) { try { foreach (ZoneTextures textures in _textures.Values) textures.Dispose(); _textures.Clear(); DisposeDX(); using IDXGIAdapter1 adapter = _factory.GetAdapter1(Display.GraphicsCard.Index) ?? throw new ApplicationException("Couldn't create DirectX-Adapter."); D3D11.D3D11CreateDevice(adapter, DriverType.Unknown, DeviceCreationFlags.None, FEATURE_LEVELS, out _device).CheckError(); _context = _device!.ImmediateContext; _output = adapter.GetOutput(Display.Index) ?? throw new ApplicationException("Couldn't get DirectX-Output."); using IDXGIOutput5 output = _output.QueryInterface(); int width = Display.Width; int height = Display.Height; if (Display.Rotation is Rotation.Rotation90 or Rotation.Rotation270) (width, height) = (height, width); Texture2DDescription captureTextureDesc = new() { CPUAccessFlags = CpuAccessFlags.None, BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource, Format = Format.B8G8R8A8_UNorm, Width = width, Height = height, MiscFlags = ResourceOptionFlags.None, MipLevels = 1, ArraySize = 1, SampleDescription = { Count = 1, Quality = 0 }, Usage = ResourceUsage.Default }; _captureTexture = _device.CreateTexture2D(captureTextureDesc); foreach (CaptureZone captureZone in CaptureZones) InitializeCaptureZone(captureZone); if (_useNewDuplicationAdapter) _duplicatedOutput = output.DuplicateOutput1(_device, new[] { Format.B8G8R8A8_UNorm }); // DarthAffe 27.02.2021: This prepares for the use of 10bit color depth else _duplicatedOutput = output.DuplicateOutput(_device); } catch { DisposeDX(); } } } /// protected override void Dispose(bool disposing) { base.Dispose(disposing); lock (_captureLock) { 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 } }