diff --git a/src/GLWpfControl/AdapterMonitorNotFoundException.cs b/src/GLWpfControl/AdapterMonitorNotFoundException.cs new file mode 100644 index 0000000..d02f1e9 --- /dev/null +++ b/src/GLWpfControl/AdapterMonitorNotFoundException.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace OpenTK.Wpf +{ + + [Serializable] + class AdapterMonitorNotFoundException : Exception + { + public AdapterMonitorNotFoundException() { } + public AdapterMonitorNotFoundException(string message) : base(message) { } + public AdapterMonitorNotFoundException(string message, Exception inner) : base(message, inner) { } + protected AdapterMonitorNotFoundException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } +} diff --git a/src/GLWpfControl/DXGLContext.cs b/src/GLWpfControl/DXGLContext.cs index ecda7aa..d32977b 100644 --- a/src/GLWpfControl/DXGLContext.cs +++ b/src/GLWpfControl/DXGLContext.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Windows; using System.Windows.Interop; @@ -18,16 +20,16 @@ internal sealed class DxGlContext : IDisposable { /// The directX context. This is basically the root of all DirectX state. public IntPtr DxContextHandle { get; } - - /// The directX device handle. This is the graphics card we're running on. - public IntPtr DxDeviceHandle { get; } + + public D3DDevice Device { get; private set; } + + private readonly uint _adapterCount; + + private readonly List _devices; /// The OpenGL Context. This is basically the root of all OpenGL state. public IGraphicsContext GraphicsContext { get; } - /// An OpenGL handle to the DirectX device. Created and used by the WGL_dx_interop extension. - public IntPtr GlDeviceHandle { get; } - /// The shared context we (may) want to lazily create/use. private static IGraphicsContext _sharedContext; private static GLWpfControlSettings _sharedContextSettings; @@ -38,49 +40,27 @@ internal sealed class DxGlContext : IDisposable { public DxGlContext([NotNull] GLWpfControlSettings settings) { - DXInterop.Direct3DCreate9Ex(DXInterop.DefaultSdkVersion, out var dxContextHandle); - DxContextHandle = dxContextHandle; - - var deviceParameters = new PresentationParameters - { - Windowed = 1, - SwapEffect = SwapEffect.Discard, - DeviceWindowHandle = IntPtr.Zero, - PresentationInterval = 0, - BackBufferFormat = Format.X8R8G8B8, // this is like A8 R8 G8 B8, but avoids issues with Gamma correction being applied twice. - BackBufferWidth = 1, - BackBufferHeight = 1, - AutoDepthStencilFormat = Format.Unknown, - BackBufferCount = 1, - EnableAutoDepthStencil = 0, - Flags = 0, - FullScreen_RefreshRateInHz = 0, - MultiSampleQuality = 0, - MultiSampleType = MultisampleType.None - }; - - DXInterop.CreateDeviceEx( - dxContextHandle, - 0, - DeviceType.HAL, // use hardware rasterization - IntPtr.Zero, - CreateFlags.HardwareVertexProcessing | - CreateFlags.Multithreaded | - CreateFlags.PureDevice, - ref deviceParameters, - IntPtr.Zero, - out var dxDeviceHandle); - DxDeviceHandle = dxDeviceHandle; - + // if the graphics context is null, we use the shared context. - if (settings.ContextToUse != null) { + if (settings.ContextToUse != null) + { GraphicsContext = settings.ContextToUse; } - else { + else + { GraphicsContext = GetOrCreateSharedOpenGLContext(settings); } - GlDeviceHandle = Wgl.DXOpenDeviceNV(dxDeviceHandle); + DXInterop.Direct3DCreate9Ex(DXInterop.DefaultSdkVersion, out var dxContextHandle); + DxContextHandle = dxContextHandle; + + _adapterCount = DXInterop.GetAdapterCount(dxContextHandle); + + _devices = Enumerable.Range(0, (int)_adapterCount) + .Select(i => D3DDevice.CreateDevice(dxContextHandle, i)) + .ToList(); + + Device = _devices.First(); } private static IGraphicsContext GetOrCreateSharedOpenGLContext(GLWpfControlSettings settings) { @@ -130,7 +110,49 @@ private static IGraphicsContext GetOrCreateSharedOpenGLContext(GLWpfControlSetti return _sharedContext; } + public void SetDeviceFromMonitor(IntPtr monitor) + { + // Keep the default adapter device if monitor is null. + // In this (worst and unlikely) case, nothing will be drawn, but it won't lead in + // NullReference exceptions for null devices. + if(monitor == IntPtr.Zero) + { + Device = _devices[0]; + return; + } + + D3DDevice dev = null; + + for (int i = 0; i < _adapterCount; i++) + { + var d3dMonitor = DXInterop.GetAdapterMonitor(DxContextHandle, (uint)i); + if (d3dMonitor == monitor) + { + dev = _devices[i]; + break; + } + } + + if (dev == null) + { + // This can happen when the control runs on a laptop with an external display in duplicated mode + // and the user closes the lid (see issue #39). + // In this particular case, only recreating the context will be useful. + + throw new AdapterMonitorNotFoundException("Adapter was not found for given monitor handle"); + } + + Device = dev; + } + public void Dispose() { + + Device = null; + foreach(var dev in _devices) + { + dev.Dispose(); + } + // we only dispose of the graphics context if we're using the shared one. if (ReferenceEquals(_sharedContext, GraphicsContext)) { if (Interlocked.Decrement(ref _sharedContextReferenceCount) == 0) { diff --git a/src/GLWpfControl/DxGLFramebuffer.cs b/src/GLWpfControl/DxGLFramebuffer.cs index 111aec1..90d3972 100644 --- a/src/GLWpfControl/DxGLFramebuffer.cs +++ b/src/GLWpfControl/DxGLFramebuffer.cs @@ -17,7 +17,7 @@ namespace OpenTK.Wpf { /// Prior to releasing references. internal sealed class DxGLFramebuffer : IDisposable { - private DxGlContext DxGlContext { get; } + public D3DDevice Device { get; } /// The width of this buffer in pixels public int FramebufferWidth { get; } @@ -46,6 +46,10 @@ internal sealed class DxGLFramebuffer : IDisposable { /// Specific wgl_dx_interop handle that marks the framebuffer as ready for interop. public IntPtr DxInteropRegisteredHandle { get; } + public double DpiScaleX { get; } + + public double DpiScaleY { get; } + public D3DImage D3dImage { get; } @@ -53,16 +57,19 @@ internal sealed class DxGLFramebuffer : IDisposable { public ScaleTransform FlipYTransform { get; } - public DxGLFramebuffer([NotNull] DxGlContext context, int width, int height, double dpiScaleX, double dpiScaleY) { - DxGlContext = context; + public DxGLFramebuffer([NotNull] D3DDevice device, int width, int height, double dpiScaleX, double dpiScaleY) { + Device = device; Width = width; Height = height; + DpiScaleX = dpiScaleX; + DpiScaleY = dpiScaleY; + FramebufferWidth = (int)Math.Ceiling(width * dpiScaleX); FramebufferHeight = (int)Math.Ceiling(height * dpiScaleY); var dxSharedHandle = IntPtr.Zero; // Unused windows-vista legacy sharing handle. Must always be null. DXInterop.CreateRenderTarget( - context.DxDeviceHandle, + device.Handle, FramebufferWidth, FramebufferHeight, Format.X8R8G8B8,// this is like A8 R8 G8 B8, but avoids issues with Gamma correction being applied twice. @@ -80,7 +87,7 @@ public DxGLFramebuffer([NotNull] DxGlContext context, int width, int height, dou GLSharedTextureHandle = GL.GenTexture(); var genHandle = Wgl.DXRegisterObjectNV( - context.GlDeviceHandle, + device.GLDeviceHandle, dxRenderTargetHandle, (uint)GLSharedTextureHandle, (uint)TextureTarget.Texture2D, @@ -118,8 +125,12 @@ public void Dispose() { GL.DeleteFramebuffer(GLFramebufferHandle); GL.DeleteRenderbuffer(GLDepthRenderBufferHandle); GL.DeleteTexture(GLSharedTextureHandle); - Wgl.DXUnregisterObjectNV(DxGlContext.GlDeviceHandle, DxInteropRegisteredHandle); + Wgl.DXUnregisterObjectNV(Device.GLDeviceHandle, DxInteropRegisteredHandle); DXInterop.Release(DxRenderTargetHandle); + + D3dImage.Lock(); + D3dImage.SetBackBuffer(D3DResourceType.IDirect3DSurface9, IntPtr.Zero); + D3dImage.Unlock(); } } } diff --git a/src/GLWpfControl/GLWpfControl.cs b/src/GLWpfControl/GLWpfControl.cs index 9717666..6b49ad5 100644 --- a/src/GLWpfControl/GLWpfControl.cs +++ b/src/GLWpfControl/GLWpfControl.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Windows; using System.Windows.Media; +using System.Windows.Threading; using JetBrains.Annotations; namespace OpenTK.Wpf @@ -73,6 +74,8 @@ public bool RenderContinuously { private TimeSpan _lastRenderTime = TimeSpan.FromSeconds(-1); + private DispatcherTimer _controlLocationCheckTimer; + /// /// Used to create a new control. Before rendering can take place, must be called. /// @@ -103,9 +106,27 @@ public void Start(GLWpfControlSettings settings) InvalidateVisual(); }; Unloaded += (a, b) => OnUnloaded(); + + _controlLocationCheckTimer = new DispatcherTimer() + { + Interval = TimeSpan.FromMilliseconds(500) + }; + _controlLocationCheckTimer.Tick += OnLocationCheckTimerTick; + _controlLocationCheckTimer.Start(); + Ready?.Invoke(); } - + + private void OnLocationCheckTimerTick(object sender, EventArgs e) + { + var presentationSource = PresentationSource.FromVisual(this); + + if(presentationSource != null) + { + _renderer?.SetMonitorFromPoint(PointToScreen(new Point(0, 0))); + } + } + private void SetupRenderSize() { if (_renderer == null || _settings == null) { return; @@ -125,6 +146,7 @@ private void SetupRenderSize() { dpiScaleY = transformToDevice.M22; } } + _renderer?.SetSize((int) RenderSize.Width, (int) RenderSize.Height, dpiScaleX, dpiScaleY); } diff --git a/src/GLWpfControl/GLWpfControlRenderer.cs b/src/GLWpfControl/GLWpfControlRenderer.cs index 49840af..c674820 100644 --- a/src/GLWpfControl/GLWpfControlRenderer.cs +++ b/src/GLWpfControl/GLWpfControlRenderer.cs @@ -14,7 +14,8 @@ namespace OpenTK.Wpf internal sealed class GLWpfControlRenderer { private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); - private readonly DxGlContext _context; + private DxGlContext _context; + private readonly GLWpfControlSettings _settings; public event Action GLRender; public event Action GLAsyncRender; @@ -32,30 +33,44 @@ internal sealed class GLWpfControlRenderer { private TimeSpan _lastFrameStamp; + private IntPtr currentMonitor = IntPtr.Zero; public GLWpfControlRenderer(GLWpfControlSettings settings) { + _settings = settings; _context = new DxGlContext(settings); } + /// + /// Set the monitor on which the renderer will draw to, based on a screen position. + /// + /// + public void SetMonitorFromPoint(Point point) + { + currentMonitor = User32Interop.MonitorFromPoint(new POINT((int)point.X, (int)point.Y), MonitorOptions.MONITOR_DEFAULTTONULL); + } public void SetSize(int width, int height, double dpiScaleX, double dpiScaleY) { if (_framebuffer == null || _framebuffer.Width != width || _framebuffer.Height != height) { _framebuffer?.Dispose(); _framebuffer = null; if (width > 0 && height > 0) { - _framebuffer = new DxGLFramebuffer(_context, width, height, dpiScaleX, dpiScaleY); + EnsureContextIsCreated(); + _framebuffer = new DxGLFramebuffer(_context.Device, width, height, dpiScaleX, dpiScaleY); } } } public void Render(DrawingContext drawingContext) { - if (_framebuffer == null) { + + if (!CanRender()) { return; } + var curFrameStamp = _stopwatch.Elapsed; var deltaT = curFrameStamp - _lastFrameStamp; _lastFrameStamp = curFrameStamp; + PreRender(); GLRender?.Invoke(deltaT); GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0); @@ -79,7 +94,7 @@ public void Render(DrawingContext drawingContext) { private void PreRender() { _framebuffer.D3dImage.Lock(); - Wgl.DXLockObjectsNV(_context.GlDeviceHandle, 1, new [] {_framebuffer.DxInteropRegisteredHandle}); + Wgl.DXLockObjectsNV(_context.Device.GLDeviceHandle, 1, new [] {_framebuffer.DxInteropRegisteredHandle}); GL.BindFramebuffer(FramebufferTarget.Framebuffer, _framebuffer.GLFramebufferHandle); GL.Viewport(0, 0, _framebuffer.FramebufferWidth, _framebuffer.FramebufferHeight); } @@ -87,10 +102,77 @@ private void PreRender() /// Sets up the framebuffer and prepares stuff for usage in directx. private void PostRender() { - Wgl.DXUnlockObjectsNV(_context.GlDeviceHandle, 1, new [] {_framebuffer.DxInteropRegisteredHandle}); + Wgl.DXUnlockObjectsNV(_context.Device.GLDeviceHandle, 1, new [] {_framebuffer.DxInteropRegisteredHandle}); _framebuffer.D3dImage.SetBackBuffer(D3DResourceType.IDirect3DSurface9, _framebuffer.DxRenderTargetHandle); _framebuffer.D3dImage.AddDirtyRect(new Int32Rect(0, 0, _framebuffer.FramebufferWidth, _framebuffer.FramebufferHeight)); _framebuffer.D3dImage.Unlock(); } + + private void EnsureContextIsCreated() + { + if(_context == null) + { + _context = new DxGlContext(_settings); + } + } + + /// + /// This method performs different type of checks to ensure that the renderer is ready to draw a new frame. + /// + /// + private bool CanRender() + { + // If the renderer does not know on which monitor (adapter) it will be rendering onto, it can't draw + // (it's waiting on a SetMonitorFromPoint() call). + if (currentMonitor == IntPtr.Zero) + { + return false; + } + + // Drawing not possible if there's not a framebuffer (for example, if we're waiting on a SetSize() call). + if (_framebuffer == null) + { + return false; + } + + // Check if the current framebuffer belongs to the device associated with the current monitor. + + try + { + // check to set the device related to the adapter that is displaying the control + _context.SetDeviceFromMonitor(currentMonitor); + + // if the surface is related to a device that does not match the current adapter + if (_framebuffer != null && _framebuffer.Device != _context.Device) + { + // remove the framebuffer, so that it is created in following SetSize() calls + // with the correct device + _framebuffer.Dispose(); + _framebuffer = null; + + return false; + } + + return true; + } + catch (AdapterMonitorNotFoundException) + { + // No adapter was found that match the current monitor. + // Clear everything and do not draw a new frame. WPF doesn't like to do business with a surface + // belonging to this monitor. + // When the new context will be created, it will be able to detect the monitor and the adapters linked. + + _framebuffer?.Dispose(); + _framebuffer = null; + _context.Dispose(); + _context = null; + + // To save us from the possibility that currentMonitor holds an invalid handle, clear it and wait for + // the next SetMonitorFromPoint() call, which will give a valid pointer. + currentMonitor = IntPtr.Zero; + + return false; + } + } } } diff --git a/src/GLWpfControl/Interop/D3DDevice.cs b/src/GLWpfControl/Interop/D3DDevice.cs new file mode 100644 index 0000000..e67e460 --- /dev/null +++ b/src/GLWpfControl/Interop/D3DDevice.cs @@ -0,0 +1,74 @@ +using OpenTK.Graphics.Wgl; +using System; +using System.Collections.Generic; +using System.Text; + +namespace OpenTK.Wpf.Interop +{ + class D3DDevice : IDisposable + { + + public IntPtr Handle { get; } + + public IntPtr GLDeviceHandle { get; } + + public int Adapter { get; } + + private D3DDevice(IntPtr deviceHandle, int adapter, IntPtr glDeviceHandle) + { + Handle = deviceHandle; + Adapter = adapter; + GLDeviceHandle = glDeviceHandle; + + IsDeviceValid(); + } + + public static D3DDevice CreateDevice(IntPtr contextHandle, int adapter) + { + var deviceParameters = new PresentationParameters + { + Windowed = 1, + SwapEffect = SwapEffect.Discard, + DeviceWindowHandle = IntPtr.Zero, + PresentationInterval = 0, + BackBufferFormat = Format.X8R8G8B8, // this is like A8 R8 G8 B8, but avoids issues with Gamma correction being applied twice. + BackBufferWidth = 1, + BackBufferHeight = 1, + AutoDepthStencilFormat = Format.Unknown, + BackBufferCount = 1, + EnableAutoDepthStencil = 0, + Flags = 0, + FullScreen_RefreshRateInHz = 0, + MultiSampleQuality = 0, + MultiSampleType = MultisampleType.None + }; + + DXInterop.CreateDeviceEx( + contextHandle, + adapter, + DeviceType.HAL, // use hardware rasterization + IntPtr.Zero, + CreateFlags.HardwareVertexProcessing | + CreateFlags.Multithreaded | + CreateFlags.PureDevice, + ref deviceParameters, + IntPtr.Zero, + out var dxDeviceHandle); + + var glDeviceHandle = Wgl.DXOpenDeviceNV(dxDeviceHandle); + + return new D3DDevice(dxDeviceHandle, adapter, glDeviceHandle); + } + + public bool IsDeviceValid() + { + return DXInterop.TestCooperativeLevel(Handle); + } + + public void Dispose() + { + Wgl.DXCloseDeviceNV(Handle); + DXInterop.Release(Handle); + } + } +} diff --git a/src/GLWpfControl/Interop/DXInterop.cs b/src/GLWpfControl/Interop/DXInterop.cs index 94e0712..a0edd05 100644 --- a/src/GLWpfControl/Interop/DXInterop.cs +++ b/src/GLWpfControl/Interop/DXInterop.cs @@ -7,12 +7,20 @@ internal static class DXInterop { public const uint DefaultSdkVersion = 32; private const int CreateDeviceEx_Offset = 20; + private const int GetAdapterCount_Offset = 4; private const int CreateRenderTarget_Offset = 28; + private const int TestCooperativeLevel_Offset = 3; private const int Release_Offset = 2; + private const int GetAdapterMonitor_Offset = 15; + private const int CheckDeviceState_Offset = 128; private delegate int NativeCreateDeviceEx(IntPtr contextHandle, int adapter, DeviceType deviceType, IntPtr focusWindowHandle, CreateFlags behaviorFlags, ref PresentationParameters presentationParameters, IntPtr fullscreenDisplayMode, out IntPtr deviceHandle); + private delegate uint NativeGetAdapterCount(IntPtr contextHandle); private delegate int NativeCreateRenderTarget(IntPtr deviceHandle, int width, int height, Format format, MultisampleType multisample, int multisampleQuality, bool lockable, out IntPtr surfaceHandle, ref IntPtr sharedHandle); private delegate uint NativeRelease(IntPtr resourceHandle); + private delegate uint NativeTestCooperativeLevel(IntPtr deviceHandle); + private delegate IntPtr NativeGetAdapterMonitor(IntPtr contextHandle, uint index); + private delegate int NativeCheckDeviceState(IntPtr deviceHandle, IntPtr hwndDestinationWindow); [DllImport("d3d9.dll")] public static extern int Direct3DCreate9Ex(uint SdkVersion, out IntPtr ctx); @@ -41,6 +49,40 @@ public static uint Release(IntPtr resourceHandle) return method(resourceHandle); } + public static uint GetAdapterCount(IntPtr contextHandle) + { + IntPtr vTable = Marshal.ReadIntPtr(contextHandle, 0); + IntPtr functionPointer = Marshal.ReadIntPtr(vTable, GetAdapterCount_Offset * IntPtr.Size); + NativeGetAdapterCount method = Marshal.GetDelegateForFunctionPointer(functionPointer); + return method(contextHandle); + } + + public static bool TestCooperativeLevel(IntPtr deviceHandle) + { + IntPtr vTable = Marshal.ReadIntPtr(deviceHandle, 0); + IntPtr functionPointer = Marshal.ReadIntPtr(vTable, TestCooperativeLevel_Offset * IntPtr.Size); + NativeTestCooperativeLevel method = Marshal.GetDelegateForFunctionPointer(functionPointer); + var result = method(deviceHandle); + + return result == 0; + } + + public static IntPtr GetAdapterMonitor(IntPtr contextHandle, uint index) + { + IntPtr vTable = Marshal.ReadIntPtr(contextHandle, 0); + IntPtr functionPointer = Marshal.ReadIntPtr(vTable, GetAdapterMonitor_Offset * IntPtr.Size); + NativeGetAdapterMonitor method = Marshal.GetDelegateForFunctionPointer(functionPointer); + return method(contextHandle, index); + } + + public static int CheckDeviceState(IntPtr deviceHandle, IntPtr hwndWindow) + { + IntPtr vTable = Marshal.ReadIntPtr(deviceHandle, 0); + IntPtr functionPointer = Marshal.ReadIntPtr(vTable, CheckDeviceState_Offset * IntPtr.Size); + NativeCheckDeviceState method = Marshal.GetDelegateForFunctionPointer(functionPointer); + return method(deviceHandle, hwndWindow); + } + } } diff --git a/src/GLWpfControl/Interop/User32Interop.cs b/src/GLWpfControl/Interop/User32Interop.cs new file mode 100644 index 0000000..05c7f01 --- /dev/null +++ b/src/GLWpfControl/Interop/User32Interop.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; + +namespace OpenTK.Wpf.Interop +{ + [StructLayout(LayoutKind.Sequential)] + public struct POINT + { + public int X; + public int Y; + + public POINT(int x, int y) + { + this.X = x; + this.Y = y; + } + + public static implicit operator System.Drawing.Point(POINT p) + { + return new System.Drawing.Point(p.X, p.Y); + } + + public static implicit operator POINT(System.Drawing.Point p) + { + return new POINT(p.X, p.Y); + } + } + + enum MonitorOptions : uint + { + MONITOR_DEFAULTTONULL = 0x00000000, + MONITOR_DEFAULTTOPRIMARY = 0x00000001, + MONITOR_DEFAULTTONEAREST = 0x00000002 + } + + static class User32Interop + { + + [DllImport("user32.dll", SetLastError = true)] + public static extern IntPtr MonitorFromPoint(POINT pt, MonitorOptions dwFlags); + + } +}