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?)
This commit is contained in:
CasualPokePlayer 2024-05-20 14:53:20 -07:00
parent d9ac6fc455
commit ef05b6ec2f
10 changed files with 300 additions and 43 deletions

View File

@ -23,6 +23,7 @@
<PackageVersion Include="Silk.NET.OpenAL.Extensions.Enumeration" Version="2.21.0" />
<PackageVersion Include="Silk.NET.OpenAL.Extensions.EXT" Version="2.21.0" />
<PackageVersion Include="Silk.NET.OpenGL" Version="2.21.0" />
<PackageVersion Include="Silk.NET.WGL.Extensions.NV" Version="2.21.0" />
<PackageVersion Include="SQLitePCLRaw.provider.e_sqlite3" Version="2.1.8" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.435" /><!-- don't forget to update .stylecop.json at the same time -->
<PackageVersion Include="System.Collections.Immutable" Version="8.0.0" />

View File

@ -11,6 +11,7 @@
<PackageReference Include="Cyotek.Drawing.BitmapFont" />
<PackageReference Include="System.Drawing.Common" />
<PackageReference Include="Silk.NET.OpenGL" />
<PackageReference Include="Silk.NET.WGL.Extensions.NV" />
<PackageReference Include="ppy.SDL2-CS" ExcludeAssets="native;contentFiles" />
<PackageReference Include="Vortice.Direct3D11" />
<PackageReference Include="Vortice.D3DCompiler" />

View File

@ -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;
}
}
}

View File

@ -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,

View File

@ -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);
}

View File

@ -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<ID3D11Texture2D>(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)
{

View File

@ -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
/// </summary>
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<int, bool> _glSupport = new Dictionary<int, bool>();
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));
}
}
}

View File

@ -0,0 +1,23 @@
using System;
using static SDL2.SDL;
namespace BizHawk.Bizware.Graphics
{
/// <summary>
/// Helper ref struct for tempoarily saving the current OpenGL context and restoring it
/// </summary>
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);
}
}

View File

@ -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);
}
}

View File

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