diff --git a/HyperionScreenCap/Capture/Dx11ScreenCapture.cs b/HyperionScreenCap/Capture/Dx11ScreenCapture.cs index 16ac024..4207d81 100644 --- a/HyperionScreenCap/Capture/Dx11ScreenCapture.cs +++ b/HyperionScreenCap/Capture/Dx11ScreenCapture.cs @@ -13,8 +13,21 @@ using System.Text; using System.Threading; + + namespace HyperionScreenCap { + + static class MathExt + { + public static T Clamp(this T val, T min, T max) where T : IComparable + { + if (val.CompareTo(min) < 0) return min; + else if (val.CompareTo(max) > 0) return max; + else return val; + } + } + class DX11ScreenCapture : IScreenCapture { private int _adapterIndex; @@ -26,12 +39,21 @@ class DX11ScreenCapture : IScreenCapture private Factory1 _factory; private Adapter _adapter; private Output _output; - private Output1 _output1; + private Output5 _output5; private SharpDX.Direct3D11.Device _device; private Texture2D _stagingTexture; private Texture2D _smallerTexture; private ShaderResourceView _smallerTextureView; private OutputDuplication _duplicatedOutput; + //private IDXGIFactory2 _factory; + //private IDXGIAdapter1 _adapter; + //private IDXGIOutput _output; + //private IDXGIOutput5 _output5; + //private ID3D11Device _device; + //private ID3D11Texture2D _stagingTexture; + //private ID3D11Texture2D _smallerTexture; + //private ID3D11ShaderResourceView _smallerTextureView; + //private IDXGIOutputDuplication _duplicatedOutput; private int _scalingFactorLog2; private int _width; private int _height; @@ -40,6 +62,10 @@ class DX11ScreenCapture : IScreenCapture private Stopwatch _captureTimer; private bool _desktopDuplicatorInvalid; private bool _disposed; + //private static readonly FeatureLevel[] s_featureLevels = new[] + //{ + //FeatureLevel.Level_11_0 + //}; public int CaptureWidth { get; private set; } public int CaptureHeight { get; private set; } @@ -78,18 +104,6 @@ public DX11ScreenCapture(int adapterIndex, int monitorIndex, int scalingFactor, public void Initialize() { - int mipLevels; - if ( _scalingFactor == 1 ) - mipLevels = 1; - 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."); // Create DXGI Factory1 _factory = new Factory1(); @@ -100,7 +114,7 @@ public void Initialize() // Get DXGI.Output _output = _adapter.GetOutput(_monitorIndex); - _output1 = _output.QueryInterface(); + _output5 = _output.QueryInterface(); // Width/Height of desktop to capture var desktopBounds = _output.Description.DesktopBounds; @@ -109,13 +123,48 @@ public void Initialize() CaptureWidth = _width / _scalingFactor; CaptureHeight = _height / _scalingFactor; + + // Initialize duplicator so we can see what the output format is + InitDesktopDuplicator(); + + + _minCaptureTime = 1000 / _maxFps; + _captureTimer = new Stopwatch(); + _disposed = false; + } + + private void InitDesktopDuplicator() + { + // We're potentially reinitializing the duplicator, which could change output format + // So make sure we reinitialize our textures + _stagingTexture?.Dispose(); + _smallerTexture?.Dispose(); + _smallerTextureView?.Dispose(); + + // Duplicate the output + Format[] DesktopFormats = { Format.R16G16B16A16_Float, Format.B8G8R8A8_UNorm, Format.R10G10B10A2_UNorm}; + _duplicatedOutput = _output5.DuplicateOutput1(_device, 0, DesktopFormats.Count(), DesktopFormats); + + // Calculate miplevels + int mipLevels; + if ( _scalingFactor == 1 ) + mipLevels = 1; + 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."); // Create Staging texture CPU-accessible var stagingTextureDesc = new Texture2DDescription { CpuAccessFlags = CpuAccessFlags.Read, BindFlags = BindFlags.None, - Format = Format.B8G8R8A8_UNorm, + Format = _duplicatedOutput.Description.ModeDescription.Format, Width = CaptureWidth, Height = CaptureHeight, OptionFlags = ResourceOptionFlags.None, @@ -131,7 +180,7 @@ public void Initialize() { CpuAccessFlags = CpuAccessFlags.None, BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource, - Format = Format.B8G8R8A8_UNorm, + Format = _duplicatedOutput.Description.ModeDescription.Format, Width = _width, Height = _height, OptionFlags = ResourceOptionFlags.GenerateMipMaps, @@ -143,18 +192,6 @@ public void Initialize() _smallerTexture = new Texture2D(_device, smallerTextureDesc); _smallerTextureView = new ShaderResourceView(_device, _smallerTexture); - _minCaptureTime = 1000 / _maxFps; - _captureTimer = new Stopwatch(); - _disposed = false; - - InitDesktopDuplicator(); - } - - private void InitDesktopDuplicator() - { - // Duplicate the output - _duplicatedOutput = _output1.DuplicateOutput(_device); - _desktopDuplicatorInvalid = false; } @@ -221,7 +258,7 @@ private byte[] ManagedCapture() // Get the desktop capture texture var mapSource = _device.ImmediateContext.MapSubresource(_stagingTexture, 0, MapMode.Read, SharpDX.Direct3D11.MapFlags.None); - _lastCapturedFrame = ToRGBArray(mapSource); + _lastCapturedFrame = ToRGBArray(mapSource, _stagingTexture.Description.Format); return _lastCapturedFrame; } finally @@ -234,42 +271,132 @@ private byte[] ManagedCapture() } } + public static float Parse16BitFloat(byte Hi, byte Lo) + { + // From https://stackoverflow.com/questions/6162651/half-precision-floating-point-in-java/6162687#6162687 + + int fullFloat = ((Hi << 8) | Lo); + int mant = fullFloat & 0x03ff; // 10 bits mantissa + int exp = fullFloat & 0x7c00; // 5 bits exponent + if( exp == 0x7c00 ) // NaN/Inf + exp = 0x3fc00; // -> NaN/Inf + else if( exp != 0 ) // normalized value + { + exp += 0x1c000; // exp - 15 + 127 + if( mant == 0 && exp > 0x1c400 ) // smooth transition + return BitConverter.ToSingle(BitConverter.GetBytes( ( fullFloat & 0x8000 ) << 16 + | exp << 13 | 0x3ff ), 0); + } + else if( mant != 0 ) // && exp==0 -> subnormal + { + exp = 0x1c400; // make it normal + do { + mant <<= 1; // mantissa * 2 + exp -= 0x400; // decrease exp by 1 + } while( ( mant & 0x400 ) == 0 ); // while not normal + mant &= 0x3ff; // discard subnormal bit + } // else +/-0 -> +/-0 + return BitConverter.ToSingle(BitConverter.GetBytes( // combine all parts + ( fullFloat & 0x8000 ) << 16 // sign << ( 31 - 15 ) + | ( exp | mant ) << 13 ), 0); // value << ( 23 - 10 ) + } /// /// Reads from the memory locations pointed to by the DataBox and saves it into a byte array /// ignoring the alpha component of each pixel. /// /// /// - private byte[] ToRGBArray(DataBox mapSource) + private byte[] ToRGBArray(DataBox mapSource, Format format) { - var sourcePtr = mapSource.DataPointer; byte[] bytes = new byte[CaptureWidth * 3 * CaptureHeight]; int byteIndex = 0; - for ( int y = 0; y < CaptureHeight; y++ ) - { - Int32[] rowData = new Int32[CaptureWidth]; - Marshal.Copy(sourcePtr, rowData, 0, CaptureWidth); - foreach ( Int32 pixelData in rowData ) + if (format == Format.R16G16B16A16_Float) + { + unsafe { - byte[] values = BitConverter.GetBytes(pixelData); - if ( BitConverter.IsLittleEndian ) + byte* ptr = (byte*)mapSource.DataPointer; + byte* rowptr = ptr; + for (int y = 0; y < CaptureHeight; y++) { - // Byte order : bgra - bytes[byteIndex++] = values[2]; - bytes[byteIndex++] = values[1]; - bytes[byteIndex++] = values[0]; + byte* pixelptr = rowptr; + for (int x = 0; x < CaptureWidth; x++) + { + for (int comp = 0; comp < 3; comp++) + { + byte lo = *pixelptr++; + byte hi = *pixelptr++; + + // No idea why these values range from 4.6 to 0 instead of 1 to 0 + // f(x) = sqrt(x/4.6) seems to approximate what the values should be. + bytes[byteIndex++] = (byte)(MathExt.Clamp(Math.Sqrt(Parse16BitFloat(hi, lo) / 4.6), 0, 1) * 255); + } + pixelptr += 2; //skip alpha + } + rowptr += mapSource.RowPitch; } - else + } + } + else if (format == Format.B8G8R8A8_UNorm) + { + IntPtr sourcePtr = mapSource.DataPointer; + + for ( int y = 0; y < CaptureHeight; y++ ) + { + Int32[] rowData = new Int32[CaptureWidth]; + Marshal.Copy(sourcePtr, rowData, 0, CaptureWidth); + + foreach ( Int32 pixelData in rowData ) { - // Byte order : argb - bytes[byteIndex++] = values[1]; - bytes[byteIndex++] = values[2]; - bytes[byteIndex++] = values[3]; + byte[] values = BitConverter.GetBytes(pixelData); + if ( BitConverter.IsLittleEndian ) + { + // Byte order : bgra + bytes[byteIndex++] = values[2]; + bytes[byteIndex++] = values[1]; + bytes[byteIndex++] = values[0]; + } + else + { + // Byte order : argb + bytes[byteIndex++] = values[1]; + bytes[byteIndex++] = values[2]; + bytes[byteIndex++] = values[3]; + } } + + sourcePtr = IntPtr.Add(sourcePtr, mapSource.RowPitch); } + } + else if (format == Format.R10G10B10A2_UNorm) + { + IntPtr sourcePtr = mapSource.DataPointer; - sourcePtr = IntPtr.Add(sourcePtr, mapSource.RowPitch); + for ( int y = 0; y < CaptureHeight; y++ ) + { + Int32[] rowData = new Int32[CaptureWidth]; + Marshal.Copy(sourcePtr, rowData, 0, CaptureWidth); + + // From http://threadlocalmutex.com/?page_id=60, 10bit to 8bit conversion: (x * 1021 + 2048) >> 12 + Func convert10bitTo8bit = (x) => (byte)MathExt.Clamp((x * 1021 + 2048) >> 12, 0, 255); + + foreach ( Int32 pixelData in rowData ) + { + Int32 r = pixelData & 0x3FF; + Int32 g = (pixelData >> 10) & 0x3FF; + Int32 b = (pixelData >> 20) & 0x3FF; + + bytes[byteIndex++] = convert10bitTo8bit(r); + bytes[byteIndex++] = convert10bitTo8bit(g); + bytes[byteIndex++] = convert10bitTo8bit(b); + } + + sourcePtr = IntPtr.Add(sourcePtr, mapSource.RowPitch); + } + } + else + { + throw new NotImplementedException($"Texture format {format.ToString()} is not supported."); } return bytes; } @@ -286,7 +413,7 @@ public void DelayNextCapture() public void Dispose() { _duplicatedOutput?.Dispose(); - _output1?.Dispose(); + _output5?.Dispose(); _output?.Dispose(); _stagingTexture?.Dispose(); _smallerTexture?.Dispose(); diff --git a/HyperionScreenCap/Helper/HyperionTask.cs b/HyperionScreenCap/Helper/HyperionTask.cs index d7780fd..9a8d9e7 100644 --- a/HyperionScreenCap/Helper/HyperionTask.cs +++ b/HyperionScreenCap/Helper/HyperionTask.cs @@ -139,8 +139,10 @@ private void TransmitNextFrame() try { byte[] imageData = _screenCapture.Capture(); - hyperionClient.SendImageData(imageData, _screenCapture.CaptureWidth, _screenCapture.CaptureHeight); - + if (imageData != null) + { + hyperionClient.SendImageData(imageData, _screenCapture.CaptureWidth, _screenCapture.CaptureHeight); + } // Uncomment the following to enable debugging // MiscUtils.SaveRGBArrayToImageFile(imageData, _screenCapture.CaptureWidth, _screenCapture.CaptureHeight, AppConstants.DEBUG_IMAGE_FILE_NAME); } diff --git a/HyperionScreenCap/HyperionScreenCap.csproj b/HyperionScreenCap/HyperionScreenCap.csproj index b45527b..cf11d6c 100644 --- a/HyperionScreenCap/HyperionScreenCap.csproj +++ b/HyperionScreenCap/HyperionScreenCap.csproj @@ -78,6 +78,7 @@ prompt MinimumRecommendedRules.ruleset true + true @@ -122,6 +123,7 @@ ..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll + ..\packages\RestSharp.106.11.7\lib\net452\RestSharp.dll @@ -163,8 +165,9 @@ - - ..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll + + + ..\packages\System.Memory.4.5.5\lib\net461\System.Memory.dll @@ -190,6 +193,7 @@ + @@ -382,6 +386,9 @@ + + + diff --git a/HyperionScreenCap/Program.cs b/HyperionScreenCap/Program.cs index bba3c43..160d1e5 100644 --- a/HyperionScreenCap/Program.cs +++ b/HyperionScreenCap/Program.cs @@ -6,7 +6,9 @@ using System.IO; using System.Linq; using System.Net; +using System.Management; using System.Windows.Forms; +using System.Runtime.InteropServices; namespace HyperionScreenCap { @@ -19,9 +21,61 @@ static Program() { } - [System.Runtime.InteropServices.DllImport("user32.dll")] + [DllImport("user32.dll")] private static extern bool SetProcessDPIAware(); + [DllImport("User32.dll")] + public static extern bool SetProcessDpiAwarenessContext(int dpiFlag); + + [DllImport("SHCore.dll")] + public static extern bool SetProcessDpiAwareness(PROCESS_DPI_AWARENESS awareness); + + public enum PROCESS_DPI_AWARENESS + { + Process_DPI_Unaware = 0, + Process_System_DPI_Aware = 1, + Process_Per_Monitor_DPI_Aware = 2 + } + + public enum DPI_AWARENESS_CONTEXT + { + DPI_AWARENESS_CONTEXT_UNAWARE = 16, + DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = 17, + DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = 18, + DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = 34 + } + + + + /// + private static void SetDpiAwareness() + { + // Get windows version to set the correct dpi awareness context + // This is needed for DXGI 1.5 OutputDuplicate1 to work + var query = "SELECT * FROM Win32_OperatingSystem"; + var searcher = new ManagementObjectSearcher(query); + var info = searcher.Get().Cast().FirstOrDefault(); + var version = info.Properties["Version"].Value.ToString(); + Version winVersion = new Version(version); + // Windows 8.1 added support for per monitor DPI + if ( winVersion >= new Version(6, 3, 0)) + { + // Windows 10 creators update added support for per monitor v2 + if ( winVersion >= new Version(10, 0, 15063)) + { + SetProcessDpiAwarenessContext((int)DPI_AWARENESS_CONTEXT.DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + } + else + { + SetProcessDpiAwareness(PROCESS_DPI_AWARENESS.Process_Per_Monitor_DPI_Aware); + } + } + else + { + SetProcessDPIAware(); + }; + } + /// /// The main entry point for the application. /// @@ -35,7 +89,7 @@ private static void Main() LOG.Info("**********************************************************"); // Set DPI awareness - SetProcessDPIAware(); + SetDpiAwareness(); // Check if already running and exit if that's the case if (IsProgramRunning("hyperionscreencap", 0) > 1) diff --git a/HyperionScreenCap/Properties/AssemblyInfo.cs b/HyperionScreenCap/Properties/AssemblyInfo.cs index f80e7b0..df27e7f 100644 --- a/HyperionScreenCap/Properties/AssemblyInfo.cs +++ b/HyperionScreenCap/Properties/AssemblyInfo.cs @@ -1,5 +1,6 @@ using System.Reflection; using System.Runtime.InteropServices; +using System.Windows.Media; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information @@ -33,3 +34,6 @@ // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("2.9.0.0")] //[assembly: AssemblyFileVersion("2.0.0.0")] Commented out so that it will be generated automatically + +// Needed for DuplicateOuput1 to work? :/ +[assembly: DisableDpiAwareness] diff --git a/HyperionScreenCap/app.config b/HyperionScreenCap/app.config index 36c3a75..4170a72 100644 --- a/HyperionScreenCap/app.config +++ b/HyperionScreenCap/app.config @@ -132,6 +132,14 @@ + + + + + + + + diff --git a/HyperionScreenCap/packages.config b/HyperionScreenCap/packages.config index e3389c4..48bf75b 100644 --- a/HyperionScreenCap/packages.config +++ b/HyperionScreenCap/packages.config @@ -28,7 +28,7 @@ - +