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
//