From ef05b6ec2fa8f84346a1766abad995b1d93626e2 Mon Sep 17 00:00:00 2001 From: CasualPokePlayer <50538166+CasualPokePlayer@users.noreply.github.com> Date: Mon, 20 May 2024 14:53:20 -0700 Subject: [PATCH] Add OpenGL <-> D3D11 interop handling Lets D3D11 display method take a wrapping GL tex id fast path, avoiding a CPU readback for GL cores. Requires the WGL_NV_DX_interop2 extension (which is probably somewhat well supported by GPUs?) --- Directory.Packages.props | 1 + .../BizHawk.Bizware.Graphics.csproj | 1 + .../D3D11/D3D11GLInterop.cs | 242 ++++++++++++++++++ .../D3D11/D3D11Resources.cs | 11 +- .../D3D11/D3D11Texture2D.cs | 13 +- .../D3D11/IGL_D3D11.cs | 18 +- .../OpenGL/OpenGLVersion.cs | 22 +- .../OpenGL/SavedOpenGLContext.cs | 23 ++ .../DisplayManager/DisplayManagerBase.cs | 8 +- .../config/DisplayConfig.Designer.cs | 4 +- 10 files changed, 300 insertions(+), 43 deletions(-) create mode 100644 src/BizHawk.Bizware.Graphics/D3D11/D3D11GLInterop.cs create mode 100644 src/BizHawk.Bizware.Graphics/OpenGL/SavedOpenGLContext.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 58df2be9fc..506b93fb4e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -23,6 +23,7 @@ + diff --git a/src/BizHawk.Bizware.Graphics/BizHawk.Bizware.Graphics.csproj b/src/BizHawk.Bizware.Graphics/BizHawk.Bizware.Graphics.csproj index 1d0a172c1f..5b0bce4b69 100644 --- a/src/BizHawk.Bizware.Graphics/BizHawk.Bizware.Graphics.csproj +++ b/src/BizHawk.Bizware.Graphics/BizHawk.Bizware.Graphics.csproj @@ -11,6 +11,7 @@ + diff --git a/src/BizHawk.Bizware.Graphics/D3D11/D3D11GLInterop.cs b/src/BizHawk.Bizware.Graphics/D3D11/D3D11GLInterop.cs new file mode 100644 index 0000000000..c38ca16408 --- /dev/null +++ b/src/BizHawk.Bizware.Graphics/D3D11/D3D11GLInterop.cs @@ -0,0 +1,242 @@ +using System; + +using Silk.NET.Core.Contexts; +using Silk.NET.OpenGL; +using Silk.NET.WGL.Extensions.NV; + +using Vortice.Direct3D; +using Vortice.Direct3D11; + +using static SDL2.SDL; + +namespace BizHawk.Bizware.Graphics +{ + internal sealed class D3D11GLInterop : IDisposable + { + public static readonly bool IsAvailable; + + // we use glCopyImageSubData in order to copy an external gl texture to our wrapped gl texture + // this is only guaranteed for GL 4.3 however, so we want to handle grabbing the ARB_copy_image or NV_copy_image extension variants + private static IntPtr GetGLProcAddress(string proc) + { + var ret = SDL2OpenGLContext.GetGLProcAddress(proc); + if (ret == IntPtr.Zero && proc == "glCopyImageSubData") + { + // note that both core and ARB_copy_image use the same proc name + // however, NV_copy_image uses a different name + ret = SDL2OpenGLContext.GetGLProcAddress("glCopyImageSubDataNV"); + } + + return ret; + } + + private static readonly GL GL; + private static readonly NVDXInterop NVDXInterop; + + static D3D11GLInterop() + { + using (new SavedOpenGLContext()) + { + try + { + // from Kronos: + // "WGL function retrieval does require an active, current context. However, the use of WGL functions do not. + // Therefore, you can destroy the context after querying all of the WGL extension functions." + + // from Microsoft: + // "The extension function addresses are unique for each pixel format. + // All rendering contexts of a given pixel format share the same extension function addresses." + + var (majorVersion, minorVersion) = OpenGLVersion.SupportsVersion(4, 3) ? (4, 3) : (2, 1); + using var glContext = new SDL2OpenGLContext(majorVersion, minorVersion, true, false); + + GL = GL.GetApi(GetGLProcAddress); + if (GL.CurrentVTable.Load("glCopyImageSubData") == IntPtr.Zero + || GL.CurrentVTable.Load("glGenTextures") == IntPtr.Zero + || GL.CurrentVTable.Load("glDeleteTextures") == IntPtr.Zero) + { + return; + } + + // note: Silk.NET's WGL.TryGetExtension function seemed to be bugged and just result in NREs... + NVDXInterop = new(new LamdaNativeContext(SDL2OpenGLContext.GetGLProcAddress)); + if (NVDXInterop.CurrentVTable.Load("wglDXOpenDeviceNV") == IntPtr.Zero + || NVDXInterop.CurrentVTable.Load("wglDXCloseDeviceNV") == IntPtr.Zero + || NVDXInterop.CurrentVTable.Load("wglDXRegisterObjectNV") == IntPtr.Zero + || NVDXInterop.CurrentVTable.Load("wglDXUnregisterObjectNV") == IntPtr.Zero + || NVDXInterop.CurrentVTable.Load("wglDXLockObjectsNV") == IntPtr.Zero + || NVDXInterop.CurrentVTable.Load("wglDXUnlockObjectsNV") == IntPtr.Zero) + { + return; + } + + ID3D11Device device = null; + var dxInteropDevice = IntPtr.Zero; + try + { + D3D11.D3D11CreateDevice( + adapter: null, + DriverType.Hardware, + DeviceCreationFlags.BgraSupport, + null!, + out device, + out var context).CheckError(); + context.Dispose(); + + unsafe + { + dxInteropDevice = NVDXInterop.DxopenDevice((void*)device!.NativePointer); + } + + // TODO: test interop harder? + IsAvailable = dxInteropDevice != IntPtr.Zero; + } + finally + { + if (dxInteropDevice != IntPtr.Zero) + { + NVDXInterop.DxcloseDevice(dxInteropDevice); + } + + device?.Dispose(); + + if (!IsAvailable) + { + GL?.Dispose(); + GL = null; + NVDXInterop?.Dispose(); + NVDXInterop = null; + } + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + } + } + + private readonly D3D11Resources _resources; + + private IntPtr _dxInteropDevice; + private IntPtr _lastGLContext; + private D3D11Texture2D _wrappedGLTexture; + private IntPtr _wrappedGLTexInteropHandle; + private uint _wrappedGLTexID; + + public D3D11GLInterop(D3D11Resources resources) + { + using (new SavedOpenGLContext()) + { + _resources = resources; + try + { + OpenInteropDevice(); + } + catch + { + Dispose(); + throw; + } + } + } + + public void OpenInteropDevice() + { + unsafe + { + _dxInteropDevice = NVDXInterop.DxopenDevice((void*)_resources.Device.NativePointer); + } + + if (_dxInteropDevice == IntPtr.Zero) + { + throw new InvalidOperationException("Failed to open DX interop device"); + } + } + + public D3D11Texture2D WrapGLTexture(int glTexId, int width, int height) + { + var glContext = SDL_GL_GetCurrentContext(); + if (glContext == IntPtr.Zero) + { + // can't do much without a context... + return null; + } + + if (_lastGLContext != glContext) + { + DestroyWrappedTexture(hasActiveContext: false); + _lastGLContext = glContext; + } + + if (_wrappedGLTexture == null || _wrappedGLTexture.Width != width || _wrappedGLTexture.Height != height) + { + DestroyWrappedTexture(hasActiveContext: true); + CreateWrappedTexture(width, height); + } + + unsafe + { + var wrappedGLTexInteropHandle = _wrappedGLTexInteropHandle; + NVDXInterop.DxlockObjects(hDevice: _dxInteropDevice, 1, &wrappedGLTexInteropHandle); + + GL.CopyImageSubData((uint)glTexId, CopyImageSubDataTarget.Texture2D, 0, 0, 0, 0, + _wrappedGLTexID, CopyImageSubDataTarget.Texture2D, 0, 0, 0, 0, (uint)width, (uint)height, 1); + + NVDXInterop.DxunlockObjects(hDevice: _dxInteropDevice, 1, &wrappedGLTexInteropHandle); + } + + return _wrappedGLTexture; + } + + private void CreateWrappedTexture(int width, int height) + { + _wrappedGLTexID = GL.GenTexture(); + _wrappedGLTexture = new(_resources, BindFlags.ShaderResource, ResourceUsage.Default, CpuAccessFlags.None, width, height, wrapped: true); + unsafe + { + _wrappedGLTexInteropHandle = NVDXInterop.DxregisterObject( + hDevice: _dxInteropDevice, + dxObject: (void*)_wrappedGLTexture.Texture.NativePointer, + name: _wrappedGLTexID, + type: (NV)GLEnum.Texture2D, + access: NV.AccessWriteDiscardNV); + } + } + + private void DestroyWrappedTexture(bool hasActiveContext = false) + { + if (_wrappedGLTexInteropHandle != IntPtr.Zero) + { + NVDXInterop.DxunregisterObject(_dxInteropDevice, _wrappedGLTexInteropHandle); + _wrappedGLTexInteropHandle = IntPtr.Zero; + } + + // this gl tex id is owned by the external context, which might be unavailable + // if it's unavailable, we assume that context was just destroyed + // therefore, assume the texture was already destroy in that case + if (hasActiveContext && _wrappedGLTexID != 0) + { + GL.DeleteTexture(_wrappedGLTexID); + } + + _wrappedGLTexID = 0; + + _wrappedGLTexture?.Dispose(); + _wrappedGLTexture = null; + } + + public void Dispose() + { + DestroyWrappedTexture(hasActiveContext: false); + + if (_dxInteropDevice != IntPtr.Zero) + { + NVDXInterop.DxcloseDevice(_dxInteropDevice); + _dxInteropDevice = IntPtr.Zero; + } + + _lastGLContext = IntPtr.Zero; + } + } +} diff --git a/src/BizHawk.Bizware.Graphics/D3D11/D3D11Resources.cs b/src/BizHawk.Bizware.Graphics/D3D11/D3D11Resources.cs index ae1fab3ff8..9053eeb496 100644 --- a/src/BizHawk.Bizware.Graphics/D3D11/D3D11Resources.cs +++ b/src/BizHawk.Bizware.Graphics/D3D11/D3D11Resources.cs @@ -44,11 +44,18 @@ namespace BizHawk.Bizware.Graphics // use this to debug D3D11 calls // note debug layer requires extra steps to use: https://learn.microsoft.com/en-us/windows/win32/direct3d11/overviews-direct3d-11-devices-layers#debug-layer // also debug output will only be present with a "native debugger" attached (pure managed debugger can't see this output) - const DeviceCreationFlags creationFlags = DeviceCreationFlags.Singlethreaded | DeviceCreationFlags.BgraSupport | DeviceCreationFlags.Debug; + var creationFlags = DeviceCreationFlags.Singlethreaded | DeviceCreationFlags.BgraSupport | DeviceCreationFlags.Debug; #else // IGL is not thread safe, so let's not bother making this implementation thread safe - const DeviceCreationFlags creationFlags = DeviceCreationFlags.Singlethreaded | DeviceCreationFlags.BgraSupport; + var creationFlags = DeviceCreationFlags.Singlethreaded | DeviceCreationFlags.BgraSupport; #endif + + // GL interop doesn't support the single threaded flag + if (D3D11GLInterop.IsAvailable) + { + creationFlags &= ~DeviceCreationFlags.Singlethreaded; + } + D3D11.D3D11CreateDevice( adapter: null, DriverType.Hardware, diff --git a/src/BizHawk.Bizware.Graphics/D3D11/D3D11Texture2D.cs b/src/BizHawk.Bizware.Graphics/D3D11/D3D11Texture2D.cs index 130e5e61cd..74f957ff75 100644 --- a/src/BizHawk.Bizware.Graphics/D3D11/D3D11Texture2D.cs +++ b/src/BizHawk.Bizware.Graphics/D3D11/D3D11Texture2D.cs @@ -21,15 +21,15 @@ namespace BizHawk.Bizware.Graphics private ID3D11Texture2D StagingTexture; - protected ID3D11Texture2D Texture; + public ID3D11Texture2D Texture; public ID3D11ShaderResourceView SRV; public bool LinearFiltering; public int Width { get; } public int Height { get; } - public bool IsUpsideDown => false; + public bool IsUpsideDown { get; } - public D3D11Texture2D(D3D11Resources resources, BindFlags bindFlags, ResourceUsage usage, CpuAccessFlags cpuAccessFlags, int width, int height) + public D3D11Texture2D(D3D11Resources resources, BindFlags bindFlags, ResourceUsage usage, CpuAccessFlags cpuAccessFlags, int width, int height, bool wrapped = false) { _resources = resources; _bindFlags = bindFlags; @@ -37,6 +37,7 @@ namespace BizHawk.Bizware.Graphics _cpuAccessFlags = cpuAccessFlags; Width = width; Height = height; + IsUpsideDown = wrapped; // ReSharper disable once VirtualMemberCallInConstructor CreateTexture(); Textures.Add(this); @@ -50,8 +51,10 @@ namespace BizHawk.Bizware.Graphics public virtual void CreateTexture() { + // wrapped textures are R8G8B8A8 rather than B8G8R8A8... + var format = IsUpsideDown ? Format.R8G8B8A8_UNorm : Format.B8G8R8A8_UNorm; Texture = Device.CreateTexture2D( - Format.B8G8R8A8_UNorm, + format, Width, Height, mipLevels: 1, @@ -59,7 +62,7 @@ namespace BizHawk.Bizware.Graphics usage: _usage, cpuAccessFlags: _cpuAccessFlags); - var srvd = new ShaderResourceViewDescription(ShaderResourceViewDimension.Texture2D, Format.B8G8R8A8_UNorm, mostDetailedMip: 0, mipLevels: 1); + var srvd = new ShaderResourceViewDescription(ShaderResourceViewDimension.Texture2D, format, mostDetailedMip: 0, mipLevels: 1); SRV = Device.CreateShaderResourceView(Texture, srvd); } diff --git a/src/BizHawk.Bizware.Graphics/D3D11/IGL_D3D11.cs b/src/BizHawk.Bizware.Graphics/D3D11/IGL_D3D11.cs index 5e9415ac05..a4c53dbb93 100644 --- a/src/BizHawk.Bizware.Graphics/D3D11/IGL_D3D11.cs +++ b/src/BizHawk.Bizware.Graphics/D3D11/IGL_D3D11.cs @@ -1,17 +1,10 @@ using System; -using System.Collections.Generic; using System.Drawing; -using System.Linq; -using System.Runtime.InteropServices; using System.Numerics; using BizHawk.Common; -using BizHawk.Common.StringExtensions; -using Vortice.D3DCompiler; -using Vortice.Direct3D; using Vortice.Direct3D11; -using Vortice.Direct3D11.Shader; using Vortice.DXGI; namespace BizHawk.Bizware.Graphics @@ -39,6 +32,7 @@ namespace BizHawk.Bizware.Graphics private D3D11Pipeline CurPipeline => _resources.CurPipeline; private D3D11SwapChain.SwapChainResources _controlSwapChain; + private readonly D3D11GLInterop _glInterop; public IGL_D3D11() { @@ -49,6 +43,11 @@ namespace BizHawk.Bizware.Graphics _resources = new(); _resources.CreateResources(); + + if (D3D11GLInterop.IsAvailable) + { + _glInterop = new(_resources); + } } private IDXGISwapChain CreateDXGISwapChain(D3D11SwapChain.ControlParameters cp) @@ -115,8 +114,10 @@ namespace BizHawk.Bizware.Graphics _controlSwapChain.Dispose(); Context.Flush(); // important to immediately dispose of the swapchain (if it's still around, we can't recreate it) + _glInterop?.Dispose(); _resources.DestroyResources(); _resources.CreateResources(); + _glInterop?.OpenInteropDevice(); var swapChain = CreateDXGISwapChain(cp); var bbTex = swapChain.GetBuffer(0); @@ -209,9 +210,8 @@ namespace BizHawk.Bizware.Graphics public ITexture2D CreateTexture(int width, int height) => new D3D11Texture2D(_resources, BindFlags.ShaderResource, ResourceUsage.Dynamic, CpuAccessFlags.Write, width, height); - // not used for non-GL backends public ITexture2D WrapGLTexture2D(int glTexId, int width, int height) - => null; + => _glInterop?.WrapGLTexture(glTexId, width, height); public Matrix4x4 CreateGuiProjectionMatrix(int width, int height) { diff --git a/src/BizHawk.Bizware.Graphics/OpenGL/OpenGLVersion.cs b/src/BizHawk.Bizware.Graphics/OpenGL/OpenGLVersion.cs index 8728afc433..f42a8e95b2 100644 --- a/src/BizHawk.Bizware.Graphics/OpenGL/OpenGLVersion.cs +++ b/src/BizHawk.Bizware.Graphics/OpenGL/OpenGLVersion.cs @@ -1,10 +1,8 @@ -using System; using System.Collections.Generic; -using BizHawk.Common.CollectionExtensions; using Silk.NET.OpenGL; -using static SDL2.SDL; +using BizHawk.Common.CollectionExtensions; namespace BizHawk.Bizware.Graphics { @@ -13,22 +11,6 @@ namespace BizHawk.Bizware.Graphics /// public static class OpenGLVersion { - private readonly ref struct SavedOpenGLContext - { - private readonly IntPtr _sdlWindow, _glContext; - - public SavedOpenGLContext() - { - _sdlWindow = SDL_GL_GetCurrentWindow(); - _glContext = SDL_GL_GetCurrentContext(); - } - - public void Dispose() - { - _ = SDL_GL_MakeCurrent(_sdlWindow, _glContext); - } - } - private static readonly IDictionary _glSupport = new Dictionary(); private static int PackGLVersion(int major, int minor) @@ -61,4 +43,4 @@ namespace BizHawk.Bizware.Graphics => _glSupport.GetValueOrPut(PackGLVersion(major, minor), static version => CheckVersion(version / 10, version % 10)); } -} \ No newline at end of file +} diff --git a/src/BizHawk.Bizware.Graphics/OpenGL/SavedOpenGLContext.cs b/src/BizHawk.Bizware.Graphics/OpenGL/SavedOpenGLContext.cs new file mode 100644 index 0000000000..aa965a4071 --- /dev/null +++ b/src/BizHawk.Bizware.Graphics/OpenGL/SavedOpenGLContext.cs @@ -0,0 +1,23 @@ +using System; + +using static SDL2.SDL; + +namespace BizHawk.Bizware.Graphics +{ + /// + /// Helper ref struct for tempoarily saving the current OpenGL context and restoring it + /// + public readonly ref struct SavedOpenGLContext + { + private readonly IntPtr _sdlWindow, _glContext; + + public SavedOpenGLContext() + { + _sdlWindow = SDL_GL_GetCurrentWindow(); + _glContext = SDL_GL_GetCurrentContext(); + } + + public void Dispose() + => _ = SDL_GL_MakeCurrent(_sdlWindow, _glContext); + } +} diff --git a/src/BizHawk.Client.Common/DisplayManager/DisplayManagerBase.cs b/src/BizHawk.Client.Common/DisplayManager/DisplayManagerBase.cs index b5e6f53ad8..00a67d1aed 100644 --- a/src/BizHawk.Client.Common/DisplayManager/DisplayManagerBase.cs +++ b/src/BizHawk.Client.Common/DisplayManager/DisplayManagerBase.cs @@ -798,12 +798,13 @@ namespace BizHawk.Client.Common ITexture2D videoTexture = null; if (!simulate) { - if (videoProvider is IGLTextureProvider glTextureProvider && _gl.DispMethodEnum == EDispMethod.OpenGL) + if (videoProvider is IGLTextureProvider glTextureProvider) { // FYI: this is a million years from happening on n64, since it's all geriatric non-FBO code videoTexture = _gl.WrapGLTexture2D(glTextureProvider.GetGLTexture(), bufferWidth, bufferHeight); } - else + + if (videoTexture == null) { // wrap the VideoProvider data in a BitmapBuffer (no point to refactoring that many IVideoProviders) bb = new(bufferWidth, bufferHeight, videoProvider.GetVideoBuffer()); @@ -811,9 +812,6 @@ namespace BizHawk.Client.Common //now, acquire the data sent from the videoProvider into a texture videoTexture = _videoTextureFrugalizer.Get(bb); - - // lets not use this. lets define BizwareGL to make clamp by default (TBD: check opengl) - // _gl.SetTextureWrapMode(videoTexture, true); } } diff --git a/src/BizHawk.Client.EmuHawk/config/DisplayConfig.Designer.cs b/src/BizHawk.Client.EmuHawk/config/DisplayConfig.Designer.cs index 5661250feb..6ce2c78078 100644 --- a/src/BizHawk.Client.EmuHawk/config/DisplayConfig.Designer.cs +++ b/src/BizHawk.Client.EmuHawk/config/DisplayConfig.Designer.cs @@ -446,7 +446,7 @@ // this.label5.Location = new System.Drawing.Point(21, 123); this.label5.Name = "label5"; - this.label5.Text = " • May malfunction on some systems.\r\n • Will have increased performance for OpenGL" + + this.label5.Text = " • May malfunction on some systems.\r\n • May have increased performance for OpenGL" + "-based emulation cores.\r\n • May have reduced performance on some systems.\r\n"; // // tabControl1 @@ -667,7 +667,7 @@ // this.label8.Location = new System.Drawing.Point(21, 30); this.label8.Name = "label8"; - this.label8.Text = " • Best compatibility\r\n • Decreased performance for OpenGL-based cores (NDS, 3DS)\r\n"; + this.label8.Text = " • Best compatibility\r\n • May have decreased performance for OpenGL-based cores (NDS, 3DS)\r\n"; // // rbD3D11 //