Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 124 additions & 62 deletions HyperionScreenCap/Capture/Dx11ScreenCapture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@
using SharpDX.Direct3D11;
using SharpDX.DXGI;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using log4net;

namespace HyperionScreenCap
{
// Custom exception to indicate that capture is paused
public class CapturePausedException : Exception
{
public CapturePausedException(string message)
: base(message)
{
}
}

class DX11ScreenCapture : IScreenCapture
{
private int _adapterIndex;
Expand All @@ -38,23 +43,24 @@ class DX11ScreenCapture : IScreenCapture
private byte[] _lastCapturedFrame;
private int _minCaptureTime;
private Stopwatch _captureTimer;
private bool _desktopDuplicatorInvalid;
private bool _disposed;

public int CaptureWidth { get; private set; }
public int CaptureHeight { get; private set; }

private static readonly ILog LOG = LogManager.GetLogger(typeof(DX11ScreenCapture));

public static String GetAvailableMonitors()
{
StringBuilder response = new StringBuilder();
using ( Factory1 factory = new Factory1() )
using (Factory1 factory = new Factory1())
{
int adapterIndex = 0;
foreach(Adapter adapter in factory.Adapters)
foreach (Adapter adapter in factory.Adapters)
{
response.Append($"Adapter Index {adapterIndex++}: {adapter.Description.Description}\n");
int outputIndex = 0;
foreach(Output output in adapter.Outputs)
foreach (Output output in adapter.Outputs)
{
response.Append($"\tMonitor Index {outputIndex++}: {output.Description.DeviceName}");
var desktopBounds = output.Description.DesktopBounds;
Expand All @@ -78,18 +84,18 @@ public DX11ScreenCapture(int adapterIndex, int monitorIndex, int scalingFactor,

public void Initialize()
{
Dispose(); // Ensure previous resources are released

int mipLevels;
if ( _scalingFactor == 1 )
if (_scalingFactor == 1)
mipLevels = 1;
else if ( _scalingFactor > 0 && _scalingFactor % 2 == 0 )
else if (_scalingFactor > 0 && _scalingFactor % 2 == 0)
{
/// Mip level for a scaling factor other than one is computed as follows:
/// 2^n = 2 + n - 1 where LHS is the scaling factor and RHS is the MipLevels value.
_scalingFactorLog2 = Convert.ToInt32(Math.Log(_scalingFactor, 2));
mipLevels = 2 + _scalingFactorLog2 - 1;
}
else
throw new Exception("Invalid scaling factor. Allowed valued are 1, 2, 4, etc.");
throw new Exception("Invalid scaling factor. Allowed values are 1, 2, 4, etc.");

// Create DXGI Factory1
_factory = new Factory1();
Expand All @@ -100,7 +106,16 @@ public void Initialize()

// Get DXGI.Output
_output = _adapter.GetOutput(_monitorIndex);
if (_output == null)
{
throw new Exception($"Failed to get output for monitor index {_monitorIndex}.");
}

_output1 = _output.QueryInterface<Output1>();
if (_output1 == null)
{
throw new Exception("Failed to get Output1 interface.");
}

// Width/Height of desktop to capture
var desktopBounds = _output.Description.DesktopBounds;
Expand Down Expand Up @@ -152,71 +167,100 @@ public void Initialize()

private void InitDesktopDuplicator()
{
// Duplicate the output
_duplicatedOutput = _output1.DuplicateOutput(_device);
try
{
_duplicatedOutput?.Dispose();
_duplicatedOutput = null;

_desktopDuplicatorInvalid = false;
if (_output1 == null || _device == null)
{
throw new Exception("Output1 or Device is null. Cannot initialize desktop duplicator.");
}

_duplicatedOutput = _output1.DuplicateOutput(_device);
LOG.Debug("Desktop duplicator initialized successfully.");
}
catch (SharpDXException ex)
{
LOG.Error($"SharpDXException in InitDesktopDuplicator: {ex.Message}", ex);
throw;
}
}

public byte[] Capture()
{
if ( _desktopDuplicatorInvalid )
_captureTimer.Restart();
try
{
_duplicatedOutput?.Dispose();
InitDesktopDuplicator();
byte[] response = ManagedCapture();
_captureTimer.Stop();
return response;
}
catch (SharpDXException ex)
{
if (ex.ResultCode == SharpDX.DXGI.ResultCode.AccessLost ||
ex.ResultCode == SharpDX.DXGI.ResultCode.AccessDenied ||
ex.ResultCode == SharpDX.DXGI.ResultCode.SessionDisconnected ||
ex.ResultCode == SharpDX.DXGI.ResultCode.DeviceRemoved ||
ex.ResultCode == SharpDX.DXGI.ResultCode.InvalidCall)
{
LOG.Warn($"Capture failed due to device loss: {ex.Message}. Disposing and signaling for reinitialization.");
Dispose();
throw new CapturePausedException("Capture is paused due to device loss.");
}
else
{
LOG.Error($"Capture failed: {ex.Message}", ex);
throw;
}
}
catch (Exception ex)
{
LOG.Error($"Capture failed: {ex.Message}", ex);
throw;
}

_captureTimer.Restart();
byte[] response = ManagedCapture();
_captureTimer.Stop();

return response;
}

private byte[] ManagedCapture()
{
if (_duplicatedOutput == null)
{
throw new Exception("DuplicatedOutput is null. Cannot capture.");
}

SharpDX.DXGI.Resource screenResource = null;
OutputDuplicateFrameInformation duplicateFrameInformation;

try
{
try
{
// Try to get duplicated frame within given time
_duplicatedOutput.AcquireNextFrame(_frameCaptureTimeout, out duplicateFrameInformation, out screenResource);
// Try to get duplicated frame within given time
_duplicatedOutput.AcquireNextFrame(_frameCaptureTimeout, out duplicateFrameInformation, out screenResource);

if ( duplicateFrameInformation.LastPresentTime == 0 && _lastCapturedFrame != null )
return _lastCapturedFrame;
}
catch ( SharpDXException ex )
{
if ( ex.ResultCode.Code == SharpDX.DXGI.ResultCode.WaitTimeout.Code && _lastCapturedFrame != null )
return _lastCapturedFrame;

if ( ex.ResultCode.Code == SharpDX.DXGI.ResultCode.AccessLost.Code )
_desktopDuplicatorInvalid = true;

throw ex;
}
if (duplicateFrameInformation.LastPresentTime == 0 && _lastCapturedFrame != null)
return _lastCapturedFrame;

// Check if scaling is used
if ( CaptureWidth != _width )
if (CaptureWidth != _width)
{
// Copy resource into memory that can be accessed by the CPU
using ( var screenTexture2D = screenResource.QueryInterface<Texture2D>() )
using (var screenTexture2D = screenResource.QueryInterface<Texture2D>())
{
_device.ImmediateContext.CopySubresourceRegion(screenTexture2D, 0, null, _smallerTexture, 0);
}

// Generates the mipmap of the screen
_device.ImmediateContext.GenerateMips(_smallerTextureView);

// Copy the mipmap of smallerTexture (size/ scalingFactor) to the staging texture: 1 for /2, 2 for /4...etc
// Copy the mipmap of smallerTexture (size/ scalingFactor) to the staging texture
_device.ImmediateContext.CopySubresourceRegion(_smallerTexture, _scalingFactorLog2, null, _stagingTexture, 0);
}
else
{
// Copy resource into memory that can be accessed by the CPU
using ( var screenTexture2D = screenResource.QueryInterface<Texture2D>() )
using (var screenTexture2D = screenResource.QueryInterface<Texture2D>())
{
_device.ImmediateContext.CopyResource(screenTexture2D, _stagingTexture);
}
}

// Get the desktop capture texture
Expand All @@ -228,9 +272,9 @@ private byte[] ManagedCapture()
{
screenResource?.Dispose();
// Fixed OUT_OF_MEMORY issue on AMD Radeon cards. Ignoring all exceptions during unmapping.
try { _device.ImmediateContext.UnmapSubresource(_stagingTexture, 0); } catch { };
try { _device?.ImmediateContext?.UnmapSubresource(_stagingTexture, 0); } catch { };
// Ignore DXGI_ERROR_INVALID_CALL, DXGI_ERROR_ACCESS_LOST errors since capture is already complete
try { _duplicatedOutput.ReleaseFrame(); } catch { }
try { _duplicatedOutput?.ReleaseFrame(); } catch { }
}
}

Expand All @@ -245,15 +289,15 @@ private byte[] ToRGBArray(DataBox mapSource)
var sourcePtr = mapSource.DataPointer;
byte[] bytes = new byte[CaptureWidth * 3 * CaptureHeight];
int byteIndex = 0;
for ( int y = 0; y < CaptureHeight; y++ )
for (int y = 0; y < CaptureHeight; y++)
{
Int32[] rowData = new Int32[CaptureWidth];
Marshal.Copy(sourcePtr, rowData, 0, CaptureWidth);

foreach ( Int32 pixelData in rowData )
foreach (Int32 pixelData in rowData)
{
byte[] values = BitConverter.GetBytes(pixelData);
if ( BitConverter.IsLittleEndian )
if (BitConverter.IsLittleEndian)
{
// Byte order : bgra
bytes[byteIndex++] = values[2];
Expand All @@ -277,23 +321,41 @@ private byte[] ToRGBArray(DataBox mapSource)
public void DelayNextCapture()
{
int remainingFrameTime = _minCaptureTime - (int)_captureTimer.ElapsedMilliseconds;
if ( remainingFrameTime > 0 )
if (remainingFrameTime > 0)
{
Thread.Sleep(remainingFrameTime);
}
}

public void Dispose()
{
_duplicatedOutput?.Dispose();
_output1?.Dispose();
_output?.Dispose();
_stagingTexture?.Dispose();
_smallerTexture?.Dispose();
_smallerTextureView?.Dispose();
_device?.Dispose();
_adapter?.Dispose();
_factory?.Dispose();
try { _duplicatedOutput?.Dispose(); } catch (Exception ex) { LOG.Error("Exception during Dispose of _duplicatedOutput", ex); }
_duplicatedOutput = null;

try { _output1?.Dispose(); } catch (Exception ex) { LOG.Error("Exception during Dispose of _output1", ex); }
_output1 = null;

try { _output?.Dispose(); } catch (Exception ex) { LOG.Error("Exception during Dispose of _output", ex); }
_output = null;

try { _stagingTexture?.Dispose(); } catch (Exception ex) { LOG.Error("Exception during Dispose of _stagingTexture", ex); }
_stagingTexture = null;

try { _smallerTexture?.Dispose(); } catch (Exception ex) { LOG.Error("Exception during Dispose of _smallerTexture", ex); }
_smallerTexture = null;

try { _smallerTextureView?.Dispose(); } catch (Exception ex) { LOG.Error("Exception during Dispose of _smallerTextureView", ex); }
_smallerTextureView = null;

try { _device?.Dispose(); } catch (Exception ex) { LOG.Error("Exception during Dispose of _device", ex); }
_device = null;

try { _adapter?.Dispose(); } catch (Exception ex) { LOG.Error("Exception during Dispose of _adapter", ex); }
_adapter = null;

try { _factory?.Dispose(); } catch (Exception ex) { LOG.Error("Exception during Dispose of _factory", ex); }
_factory = null;

_lastCapturedFrame = null;
_disposed = true;
}
Expand Down
5 changes: 5 additions & 0 deletions HyperionScreenCap/Config/AppConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ class AppConstants
/// </summary>
public const int MAX_CAPTURE_ATTEMPTS = 45;

/// <summary>
/// Delay in milliseconds before retrying after a pause
/// </summary>
public const int CAPTURE_PAUSED_RETRY_DELAY = 3000;

/// <summary>
/// Number of screen capture failure attemps after which screen capture should be re-initialized.
/// </summary>
Expand Down
Loading