From 44f8c860fd4b89310f69b918287e96a1a44fdfd7 Mon Sep 17 00:00:00 2001 From: Marco Todisco Date: Sat, 5 Dec 2020 12:27:24 +0100 Subject: [PATCH 1/7] Initial support for multi-adapter d3d context - separated device properties from dxglcontext - added check for device associated with monitor during render phase - framebuffer associated with device rather than context --- src/GLWpfControl/DXGLContext.cs | 85 +++++++++++------------ src/GLWpfControl/DxGLFramebuffer.cs | 19 +++-- src/GLWpfControl/GLWpfControl.cs | 2 +- src/GLWpfControl/GLWpfControlRenderer.cs | 27 +++++-- src/GLWpfControl/Interop/D3DDevice.cs | 74 ++++++++++++++++++++ src/GLWpfControl/Interop/DXInterop.cs | 32 +++++++++ src/GLWpfControl/Interop/User32Interop.cs | 45 ++++++++++++ 7 files changed, 228 insertions(+), 56 deletions(-) create mode 100644 src/GLWpfControl/Interop/D3DDevice.cs create mode 100644 src/GLWpfControl/Interop/User32Interop.cs diff --git a/src/GLWpfControl/DXGLContext.cs b/src/GLWpfControl/DXGLContext.cs index ecda7aa..97acc76 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,6 +110,25 @@ private static IGraphicsContext GetOrCreateSharedOpenGLContext(GLWpfControlSetti return _sharedContext; } + public void SetDeviceFromCurrentAdapter(Point point) + { + var monitor = User32Interop.MonitorFromPoint(new POINT((int)point.X, (int)point.Y), MonitorOptions.MONITOR_DEFAULTTONULL); + + D3DDevice dev = null; + + for(int i = 0; i < _adapterCount; i++) + { + var d3dMonitor = DXInterop.GetAdapterMonitor(DxContextHandle, (uint)i); + if(d3dMonitor == monitor) + { + dev = _devices[i]; + break; + } + } + + Device = dev; + } + public void Dispose() { // we only dispose of the graphics context if we're using the shared one. if (ReferenceEquals(_sharedContext, GraphicsContext)) { diff --git a/src/GLWpfControl/DxGLFramebuffer.cs b/src/GLWpfControl/DxGLFramebuffer.cs index 111aec1..0ed54f4 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,7 +125,7 @@ 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); } } diff --git a/src/GLWpfControl/GLWpfControl.cs b/src/GLWpfControl/GLWpfControl.cs index 9cd2df7..55d2f2c 100644 --- a/src/GLWpfControl/GLWpfControl.cs +++ b/src/GLWpfControl/GLWpfControl.cs @@ -143,7 +143,7 @@ protected override void OnRender(DrawingContext drawingContext) { DesignTimeHelper.DrawDesignTimeHelper(this, drawingContext); } else { - _renderer?.Render(drawingContext); + _renderer?.Render(drawingContext, PointToScreen(new Point(0,0))); } base.OnRender(drawingContext); } diff --git a/src/GLWpfControl/GLWpfControlRenderer.cs b/src/GLWpfControl/GLWpfControlRenderer.cs index 0859d26..da13fc3 100644 --- a/src/GLWpfControl/GLWpfControlRenderer.cs +++ b/src/GLWpfControl/GLWpfControlRenderer.cs @@ -44,19 +44,20 @@ public void SetSize(int width, int height, double dpiScaleX, double dpiScaleY) { _framebuffer?.Dispose(); _framebuffer = null; if (width > 0 && height > 0) { - _framebuffer = new DxGLFramebuffer(_context, width, height, dpiScaleX, dpiScaleY); + _framebuffer = new DxGLFramebuffer(_context.Device, width, height, dpiScaleX, dpiScaleY); } } } - public void Render(DrawingContext drawingContext) { + public void Render(DrawingContext drawingContext, Point point) { if (_framebuffer == null) { return; } var curFrameStamp = _stopwatch.Elapsed; var deltaT = curFrameStamp - _lastFrameStamp; _lastFrameStamp = curFrameStamp; - PreRender(); + + PreRender(point); GLRender?.Invoke(deltaT); GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0); GL.Flush(); @@ -76,10 +77,24 @@ public void Render(DrawingContext drawingContext) { } /// Sets up the framebuffer, directx stuff for rendering. - private void PreRender() + private void PreRender(Point point) { + // TODO: checking for the correct adapter is expensive and should not be made during the Render phase. + // It's safe to do it periodically (eg: a timer). + + // check to set the device related to the adapter that is displaying the control + _context.SetDeviceFromCurrentAdapter(point); + + if(_framebuffer.Device != _context.Device) // if the surface is related to a device that does not match the current adapter + { + // recreate a framebuffer with the current device and the same size as the previous one + var newFramebuffer = new DxGLFramebuffer(_context.Device, _framebuffer.Width, _framebuffer.Height, _framebuffer.DpiScaleX, _framebuffer.DpiScaleY); + _framebuffer.Dispose(); + _framebuffer = newFramebuffer; + } + _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,7 +102,7 @@ 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(); 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..094a10b 100644 --- a/src/GLWpfControl/Interop/DXInterop.cs +++ b/src/GLWpfControl/Interop/DXInterop.cs @@ -7,12 +7,18 @@ 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 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); [DllImport("d3d9.dll")] public static extern int Direct3DCreate9Ex(uint SdkVersion, out IntPtr ctx); @@ -41,6 +47,32 @@ 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); + } + } } 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); + + } +} From d3b0aca2cd8a25dbca2123a109fc6d60d12bbc91 Mon Sep 17 00:00:00 2001 From: Marco Todisco Date: Thu, 7 Jan 2021 10:44:08 +0100 Subject: [PATCH 2/7] Fixed compilation error after merge --- src/GLWpfControl/GLWpfControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GLWpfControl/GLWpfControl.cs b/src/GLWpfControl/GLWpfControl.cs index 9717666..9fab833 100644 --- a/src/GLWpfControl/GLWpfControl.cs +++ b/src/GLWpfControl/GLWpfControl.cs @@ -160,7 +160,7 @@ protected override void OnRender(DrawingContext drawingContext) { } else if(_renderer != null) { SetupRenderSize(); - _renderer?.Render(drawingContext); + _renderer?.Render(drawingContext, PointToScreen(new Point(0, 0))); } else { UnstartedControlHelper.DrawUnstartedControlHelper(this, drawingContext); From 04269d8a388f7d96fe08421df2eb0031ab394365 Mon Sep 17 00:00:00 2001 From: Marco Todisco Date: Sat, 9 Jan 2021 11:32:01 +0100 Subject: [PATCH 3/7] Moved adapter point check outside of Render - added fallback for point not matching any device --- src/GLWpfControl/DXGLContext.cs | 8 +++-- src/GLWpfControl/GLWpfControl.cs | 4 ++- src/GLWpfControl/GLWpfControlRenderer.cs | 40 ++++++++++++++---------- 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/GLWpfControl/DXGLContext.cs b/src/GLWpfControl/DXGLContext.cs index 97acc76..840e891 100644 --- a/src/GLWpfControl/DXGLContext.cs +++ b/src/GLWpfControl/DXGLContext.cs @@ -113,8 +113,12 @@ private static IGraphicsContext GetOrCreateSharedOpenGLContext(GLWpfControlSetti public void SetDeviceFromCurrentAdapter(Point point) { var monitor = User32Interop.MonitorFromPoint(new POINT((int)point.X, (int)point.Y), MonitorOptions.MONITOR_DEFAULTTONULL); - - D3DDevice dev = null; + + // Keep the default adapter device if no adapter is displaying the given point. + // In this (worst and unlikely) case, nothing will be drawn, but it won't lead in + // NullReference exceptions for null devices. + + D3DDevice dev = _devices[0]; for(int i = 0; i < _adapterCount; i++) { diff --git a/src/GLWpfControl/GLWpfControl.cs b/src/GLWpfControl/GLWpfControl.cs index 9fab833..5e22835 100644 --- a/src/GLWpfControl/GLWpfControl.cs +++ b/src/GLWpfControl/GLWpfControl.cs @@ -125,6 +125,8 @@ private void SetupRenderSize() { dpiScaleY = transformToDevice.M22; } } + + _renderer?.SetActiveDeviceFromLocation(PointToScreen(new Point(0, 0))); _renderer?.SetSize((int) RenderSize.Width, (int) RenderSize.Height, dpiScaleX, dpiScaleY); } @@ -160,7 +162,7 @@ protected override void OnRender(DrawingContext drawingContext) { } else if(_renderer != null) { SetupRenderSize(); - _renderer?.Render(drawingContext, PointToScreen(new Point(0, 0))); + _renderer?.Render(drawingContext); } else { UnstartedControlHelper.DrawUnstartedControlHelper(this, drawingContext); diff --git a/src/GLWpfControl/GLWpfControlRenderer.cs b/src/GLWpfControl/GLWpfControlRenderer.cs index cc66695..3798994 100644 --- a/src/GLWpfControl/GLWpfControlRenderer.cs +++ b/src/GLWpfControl/GLWpfControlRenderer.cs @@ -38,6 +38,26 @@ public GLWpfControlRenderer(GLWpfControlSettings settings) _context = new DxGlContext(settings); } + /// + /// Informs the renderer with the location it is drawing onto. + /// Since a device is created for each adapter, this is needed in order to select + /// the device associated with the adapter that displays the control. + /// + /// + public void SetActiveDeviceFromLocation(Point point) + { + // check to set the device related to the adapter that is displaying the control + _context.SetDeviceFromCurrentAdapter(point); + + // 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; + } + } public void SetSize(int width, int height, double dpiScaleX, double dpiScaleY) { if (_framebuffer == null || _framebuffer.Width != width || _framebuffer.Height != height) { @@ -49,7 +69,7 @@ public void SetSize(int width, int height, double dpiScaleX, double dpiScaleY) { } } - public void Render(DrawingContext drawingContext, Point point) { + public void Render(DrawingContext drawingContext) { if (_framebuffer == null) { return; } @@ -57,7 +77,7 @@ public void Render(DrawingContext drawingContext, Point point) { var deltaT = curFrameStamp - _lastFrameStamp; _lastFrameStamp = curFrameStamp; - PreRender(point); + PreRender(); GLRender?.Invoke(deltaT); GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0); GL.Flush(); @@ -77,22 +97,8 @@ public void Render(DrawingContext drawingContext, Point point) { } /// Sets up the framebuffer, directx stuff for rendering. - private void PreRender(Point point) + private void PreRender() { - // TODO: checking for the correct adapter is expensive and should not be made during the Render phase. - // It's safe to do it periodically (eg: a timer). - - // check to set the device related to the adapter that is displaying the control - _context.SetDeviceFromCurrentAdapter(point); - - if(_framebuffer.Device != _context.Device) // if the surface is related to a device that does not match the current adapter - { - // recreate a framebuffer with the current device and the same size as the previous one - var newFramebuffer = new DxGLFramebuffer(_context.Device, _framebuffer.Width, _framebuffer.Height, _framebuffer.DpiScaleX, _framebuffer.DpiScaleY); - _framebuffer.Dispose(); - _framebuffer = newFramebuffer; - } - _framebuffer.D3dImage.Lock(); Wgl.DXLockObjectsNV(_context.Device.GLDeviceHandle, 1, new [] {_framebuffer.DxInteropRegisteredHandle}); GL.BindFramebuffer(FramebufferTarget.Framebuffer, _framebuffer.GLFramebufferHandle); From 254cf797ee37f7da4e2bbbdf17fccc3a67b837b6 Mon Sep 17 00:00:00 2001 From: Marco Todisco Date: Sat, 9 Jan 2021 11:36:06 +0100 Subject: [PATCH 4/7] Moved location check to dedicated timer --- src/GLWpfControl/GLWpfControl.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/GLWpfControl/GLWpfControl.cs b/src/GLWpfControl/GLWpfControl.cs index 5e22835..c6c058e 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,22 @@ 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) + { + _renderer?.SetActiveDeviceFromLocation(PointToScreen(new Point(0, 0))); + } + private void SetupRenderSize() { if (_renderer == null || _settings == null) { return; @@ -126,7 +142,6 @@ private void SetupRenderSize() { } } - _renderer?.SetActiveDeviceFromLocation(PointToScreen(new Point(0, 0))); _renderer?.SetSize((int) RenderSize.Width, (int) RenderSize.Height, dpiScaleX, dpiScaleY); } From 3625dad3ccf8c9a9e979caa04b98ba6dd9d5172c Mon Sep 17 00:00:00 2001 From: Marco Todisco Date: Sun, 14 Mar 2021 15:06:34 +0100 Subject: [PATCH 5/7] Bugfix: crash when closing lid of laptop with duplicated displays - renderer retains the handle of the current monitor and is updated periodically - in case it is not found before rendering, clear the context and wait for another valid monitor handle before rendering again --- .../AdapterMonitorNotFoundException.cs | 18 ++++ src/GLWpfControl/DXGLContext.cs | 26 +++-- src/GLWpfControl/DxGLFramebuffer.cs | 4 + src/GLWpfControl/GLWpfControl.cs | 2 +- src/GLWpfControl/GLWpfControlRenderer.cs | 95 +++++++++++++++---- src/GLWpfControl/Interop/DXInterop.cs | 10 ++ 6 files changed, 130 insertions(+), 25 deletions(-) create mode 100644 src/GLWpfControl/AdapterMonitorNotFoundException.cs 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 840e891..52d57cb 100644 --- a/src/GLWpfControl/DXGLContext.cs +++ b/src/GLWpfControl/DXGLContext.cs @@ -110,26 +110,38 @@ private static IGraphicsContext GetOrCreateSharedOpenGLContext(GLWpfControlSetti return _sharedContext; } - public void SetDeviceFromCurrentAdapter(Point point) + public void SetDeviceFromMonitor(IntPtr monitor) { - var monitor = User32Interop.MonitorFromPoint(new POINT((int)point.X, (int)point.Y), MonitorOptions.MONITOR_DEFAULTTONULL); - - // Keep the default adapter device if no adapter is displaying the given point. + // 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 = _devices[0]; + D3DDevice dev = null; - for(int i = 0; i < _adapterCount; i++) + for (int i = 0; i < _adapterCount; i++) { var d3dMonitor = DXInterop.GetAdapterMonitor(DxContextHandle, (uint)i); - if(d3dMonitor == monitor) + 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; } diff --git a/src/GLWpfControl/DxGLFramebuffer.cs b/src/GLWpfControl/DxGLFramebuffer.cs index 0ed54f4..90d3972 100644 --- a/src/GLWpfControl/DxGLFramebuffer.cs +++ b/src/GLWpfControl/DxGLFramebuffer.cs @@ -127,6 +127,10 @@ public void Dispose() { GL.DeleteTexture(GLSharedTextureHandle); 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 c6c058e..89c006d 100644 --- a/src/GLWpfControl/GLWpfControl.cs +++ b/src/GLWpfControl/GLWpfControl.cs @@ -119,7 +119,7 @@ public void Start(GLWpfControlSettings settings) private void OnLocationCheckTimerTick(object sender, EventArgs e) { - _renderer?.SetActiveDeviceFromLocation(PointToScreen(new Point(0, 0))); + _renderer?.SetMonitorFromPoint(PointToScreen(new Point(0, 0))); } private void SetupRenderSize() { diff --git a/src/GLWpfControl/GLWpfControlRenderer.cs b/src/GLWpfControl/GLWpfControlRenderer.cs index 3798994..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,31 +33,21 @@ internal sealed class GLWpfControlRenderer { private TimeSpan _lastFrameStamp; + private IntPtr currentMonitor = IntPtr.Zero; public GLWpfControlRenderer(GLWpfControlSettings settings) { + _settings = settings; _context = new DxGlContext(settings); } /// - /// Informs the renderer with the location it is drawing onto. - /// Since a device is created for each adapter, this is needed in order to select - /// the device associated with the adapter that displays the control. + /// Set the monitor on which the renderer will draw to, based on a screen position. /// /// - public void SetActiveDeviceFromLocation(Point point) + public void SetMonitorFromPoint(Point point) { - // check to set the device related to the adapter that is displaying the control - _context.SetDeviceFromCurrentAdapter(point); - - // 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; - } + 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) { @@ -64,15 +55,18 @@ public void SetSize(int width, int height, double dpiScaleX, double dpiScaleY) { _framebuffer?.Dispose(); _framebuffer = null; if (width > 0 && height > 0) { + 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; @@ -113,5 +107,72 @@ private void PostRender() _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/DXInterop.cs b/src/GLWpfControl/Interop/DXInterop.cs index 094a10b..a0edd05 100644 --- a/src/GLWpfControl/Interop/DXInterop.cs +++ b/src/GLWpfControl/Interop/DXInterop.cs @@ -12,6 +12,7 @@ internal static class DXInterop 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); @@ -19,6 +20,7 @@ internal static class DXInterop 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); @@ -73,6 +75,14 @@ public static IntPtr GetAdapterMonitor(IntPtr contextHandle, uint index) 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); + } + } } From 8d2229efc7c51644e79037ff581102afa0f02d46 Mon Sep 17 00:00:00 2001 From: Marco Todisco Date: Sun, 14 Mar 2021 15:11:06 +0100 Subject: [PATCH 6/7] Bugfix: check for element loaded before setting current monitor --- src/GLWpfControl/GLWpfControl.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/GLWpfControl/GLWpfControl.cs b/src/GLWpfControl/GLWpfControl.cs index 89c006d..6b49ad5 100644 --- a/src/GLWpfControl/GLWpfControl.cs +++ b/src/GLWpfControl/GLWpfControl.cs @@ -119,7 +119,12 @@ public void Start(GLWpfControlSettings settings) private void OnLocationCheckTimerTick(object sender, EventArgs e) { - _renderer?.SetMonitorFromPoint(PointToScreen(new Point(0, 0))); + var presentationSource = PresentationSource.FromVisual(this); + + if(presentationSource != null) + { + _renderer?.SetMonitorFromPoint(PointToScreen(new Point(0, 0))); + } } private void SetupRenderSize() { From 55c9380ab22a9b9161a00d9b103f6b80dd3d0f11 Mon Sep 17 00:00:00 2001 From: Marco Todisco Date: Sun, 14 Mar 2021 15:25:14 +0100 Subject: [PATCH 7/7] Clear all the devices in Dispose of the context --- src/GLWpfControl/DXGLContext.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/GLWpfControl/DXGLContext.cs b/src/GLWpfControl/DXGLContext.cs index 52d57cb..d32977b 100644 --- a/src/GLWpfControl/DXGLContext.cs +++ b/src/GLWpfControl/DXGLContext.cs @@ -146,6 +146,13 @@ public void SetDeviceFromMonitor(IntPtr monitor) } 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) {