Files
RobustToolbox/Robust.Client/Graphics/Clyde/GLContext/GLContextAngle.cs
2022-05-19 07:55:36 +10:00

581 lines
22 KiB
C#

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using OpenToolkit.Graphics.OpenGL4;
using Robust.Shared;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows;
using static Robust.Client.Graphics.Clyde.Egl;
using static TerraFX.Interop.DirectX.D3D_DRIVER_TYPE;
using static TerraFX.Interop.DirectX.D3D_FEATURE_LEVEL;
using static TerraFX.Interop.DirectX.DXGI_FORMAT;
using static TerraFX.Interop.DirectX.DXGI_SWAP_EFFECT;
using static TerraFX.Interop.Windows.Windows;
using static TerraFX.Interop.DirectX.DirectX;
using static TerraFX.Interop.DirectX.D3D11;
using static TerraFX.Interop.DirectX.DXGI;
using GL = OpenToolkit.Graphics.OpenGL4.GL;
namespace Robust.Client.Graphics.Clyde
{
internal partial class Clyde
{
/// <summary>
/// Explicit ANGLE GL context with manual DXGI/D3D device and swap chain management.
/// </summary>
private sealed unsafe class GLContextAngle : GLContextBase
{
// Thanks to mpv's implementation of context_angle for inspiration/hints.
// https://github.com/mpv-player/mpv/blob/f8e62d3d82dd0a3d06f9a557d756f0ad78118cc7/video/out/opengl/context_angle.c
// NOTE: This class only handles GLES3/D3D11.
// For anything lower we just let ANGLE fall back and do the work 100%.
private IDXGIFactory1* _factory;
private IDXGIAdapter1* _adapter;
private ID3D11Device* _device;
private D3D_FEATURE_LEVEL _deviceFl;
private void* _eglDevice;
private void* _eglDisplay;
private void* _eglContext;
private void* _eglConfig;
private bool _es3;
private uint _swapInterval;
private readonly Dictionary<WindowId, WindowData> _windowData = new();
public override GLContextSpec[] SpecsToTry => Array.Empty<GLContextSpec>();
public override bool RequireWindowGL => false;
public override bool EarlyContextInit => true;
public override bool HasBrokenWindowSrgb => false;
public GLContextAngle(Clyde clyde) : base(clyde)
{
}
public override GLContextSpec? SpecWithOpenGLVersion(RendererOpenGLVersion version)
{
// Do not initialize GL context on the window directly, we use ANGLE.
return null;
}
public override void UpdateVSync()
{
_swapInterval = (uint) (Clyde._vSync ? 1 : 0);
}
public override void WindowCreated(GLContextSpec? spec, WindowReg reg)
{
var data = new WindowData
{
Reg = reg
};
_windowData[reg.Id] = data;
var hWnd = (HWND) Clyde._windowing!.WindowGetWin32Window(reg)!.Value;
// todo: exception management.
CreateSwapChain1(hWnd, data);
_factory->MakeWindowAssociation(hWnd, DXGI_MWA_NO_ALT_ENTER);
var rt = Clyde.RtToLoaded(reg.RenderTarget);
rt.FlipY = true;
if (reg.IsMainWindow)
{
UpdateVSync();
eglMakeCurrent(_eglDisplay, data.EglBackbuffer, data.EglBackbuffer, _eglContext);
}
}
private void DestroyBackbuffer(WindowData data)
{
if (data.EglBackbuffer != null)
{
if (data.Reg.IsMainWindow)
eglMakeCurrent(_eglDisplay, null, null, null);
eglDestroySurface(_eglDisplay, data.EglBackbuffer);
data.EglBackbuffer = null;
}
data.Backbuffer->Release();
data.Backbuffer = null;
}
private void SetupBackbuffer(WindowData data)
{
DebugTools.Assert(data.Backbuffer == null, "Backbuffer must have been released!");
DebugTools.Assert(data.EglBackbuffer == null, "EGL Backbuffer must have been released!");
fixed (ID3D11Texture2D** texPtr = &data.Backbuffer)
{
ThrowIfFailed("GetBuffer", data.SwapChain->GetBuffer(0, __uuidof<ID3D11Texture2D>(), (void**) texPtr));
}
var attributes = stackalloc int[]
{
EGL_TEXTURE_FORMAT, EGL_TEXTURE_RGBA,
EGL_TEXTURE_TARGET, EGL_TEXTURE_2D,
EGL_NONE
};
data.EglBackbuffer = eglCreatePbufferFromClientBuffer(
_eglDisplay,
EGL_D3D_TEXTURE_ANGLE,
data.Backbuffer,
_eglConfig,
attributes);
}
private void CreateSwapChain1(HWND hWnd, WindowData data)
{
var desc = new DXGI_SWAP_CHAIN_DESC
{
BufferDesc =
{
Width = (uint) data.Reg.FramebufferSize.X,
Height = (uint) data.Reg.FramebufferSize.Y,
Format = Clyde._hasGLSrgb ? DXGI_FORMAT_R8G8B8A8_UNORM_SRGB : DXGI_FORMAT_R8G8B8A8_UNORM
},
SampleDesc =
{
Count = 1
},
OutputWindow = hWnd,
BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT | DXGI_USAGE_SHADER_INPUT,
BufferCount = 2,
SwapEffect = DXGI_SWAP_EFFECT_DISCARD,
Windowed = 1
};
fixed (IDXGISwapChain** swapPtr = &data.SwapChain)
{
ThrowIfFailed("CreateSwapChain", _factory->CreateSwapChain(
(IUnknown*) _device,
&desc,
swapPtr
));
}
SetupBackbuffer(data);
}
public override void WindowDestroyed(WindowReg reg)
{
var data = _windowData[reg.Id];
DestroyBackbuffer(data);
data.SwapChain->Release();
_windowData.Remove(reg.Id);
}
public bool TryInitialize()
{
try
{
TryInitializeCore();
}
catch (Exception e)
{
Logger.ErrorS("clyde.ogl.angle", $"Failed to initialize custom ANGLE: {e}");
Shutdown();
return false;
}
return true;
}
public void EarlyInit()
{
// Early GL context init so that feature detection runs before window creation,
// and so that we can know _hasGLSrgb in window creation.
eglMakeCurrent(_eglDisplay, null, null, _eglContext);
Clyde.InitOpenGL();
Clyde._earlyGLInit = true;
}
private void TryInitializeCore()
{
var extensions = Marshal.PtrToStringUTF8((nint) eglQueryString(null, EGL_EXTENSIONS));
Logger.DebugS("clyde.ogl.angle", $"EGL client extensions: {extensions}!");
CreateD3D11Device();
CreateEglContext();
}
private void CreateEglContext()
{
_eglDevice = eglCreateDeviceANGLE(EGL_D3D11_DEVICE_ANGLE, _device, null);
if (_eglDevice == (void*) EGL_NO_DEVICE_EXT)
throw new Exception("eglCreateDeviceANGLE failed.");
_eglDisplay = eglGetPlatformDisplayEXT(EGL_PLATFORM_DEVICE_EXT, _eglDevice, null);
if (_eglDisplay == null)
throw new Exception("eglGetPlatformDisplayEXT failed.");
int major;
int minor;
if (eglInitialize(_eglDisplay, &major, &minor) == EGL_FALSE)
throw new Exception("eglInitialize failed.");
var vendor = Marshal.PtrToStringUTF8((nint) eglQueryString(_eglDisplay, EGL_VENDOR));
var version = Marshal.PtrToStringUTF8((nint) eglQueryString(_eglDisplay, EGL_VERSION));
var extensions = Marshal.PtrToStringUTF8((nint) eglQueryString(_eglDisplay, EGL_EXTENSIONS));
Logger.DebugS("clyde.ogl.angle", "EGL initialized!");
Logger.DebugS("clyde.ogl.angle", $"EGL vendor: {vendor}!");
Logger.DebugS("clyde.ogl.angle", $"EGL version: {version}!");
Logger.DebugS("clyde.ogl.angle", $"EGL extensions: {extensions}!");
if (eglBindAPI(EGL_OPENGL_ES_API) != EGL_TRUE)
throw new Exception("eglBindAPI failed.");
var attribs = stackalloc int[]
{
// EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, 8,
EGL_STENCIL_SIZE, 8,
EGL_NONE
};
var numConfigs = 0;
if (eglChooseConfig(_eglDisplay, attribs, null, 0, &numConfigs) == EGL_FALSE)
throw new Exception("eglChooseConfig failed.");
var configs = stackalloc void*[numConfigs];
if (eglChooseConfig(_eglDisplay, attribs, configs, numConfigs, &numConfigs) == EGL_FALSE)
throw new Exception("eglChooseConfig failed.");
if (numConfigs == 0)
throw new Exception("No compatible EGL configurations returned!");
Logger.DebugS("clyde.ogl.angle", $"{numConfigs} EGL configs possible!");
for (var i = 0; i < numConfigs; i++)
{
Logger.DebugS("clyde.ogl.angle", DumpEglConfig(_eglDisplay, configs[i]));
}
_eglConfig = configs[0];
int supportedRenderableTypes;
eglGetConfigAttrib(_eglDisplay, _eglConfig, EGL_RENDERABLE_TYPE, &supportedRenderableTypes);
_es3 = (supportedRenderableTypes & EGL_OPENGL_ES3_BIT) != 0;
var createAttribs = stackalloc int[]
{
EGL_CONTEXT_CLIENT_VERSION, _es3 ? 3 : 2,
EGL_NONE
};
_eglContext = eglCreateContext(_eglDisplay, _eglConfig, null, createAttribs);
if (_eglContext == (void*) EGL_NO_CONTEXT)
throw new Exception("eglCreateContext failed!");
Logger.DebugS("clyde.ogl.angle", "EGL context created!");
Clyde._openGLVersion = _es3 ? RendererOpenGLVersion.GLES3 : RendererOpenGLVersion.GLES2;
}
private void CreateD3D11Device()
{
IDXGIDevice1* dxgiDevice = null;
try
{
fixed (IDXGIFactory1** ptr = &_factory)
{
ThrowIfFailed(nameof(CreateDXGIFactory1), CreateDXGIFactory1(__uuidof<IDXGIFactory1>(), (void**) ptr));
}
// Try to find the correct adapter if specified.
var adapterName = Clyde._cfg.GetCVar(CVars.DisplayAdapter);
if (adapterName != "")
{
_adapter = TryFindAdapterWithName(adapterName);
if (_adapter == null)
{
Logger.WarningS("clyde.ogl.angle",
$"Unable to find display adapter with requested name: {adapterName}");
}
Logger.DebugS("clyde.ogl.angle", $"Found display adapter with name: {adapterName}");
}
#pragma warning disable CA1416
#pragma warning disable CS0162
IDXGIFactory6* factory6;
if (_adapter == null && _factory->QueryInterface(__uuidof<IDXGIFactory6>(), (void**) &factory6) == 0)
{
var gpuPref = (DXGI_GPU_PREFERENCE) Clyde._cfg.GetCVar(CVars.DisplayGpuPreference);
IDXGIAdapter1* adapter;
for (var adapterIndex = 0u;
factory6->EnumAdapterByGpuPreference(
adapterIndex,
gpuPref,
__uuidof<IDXGIAdapter1>(),
(void**)&adapter) != DXGI_ERROR_NOT_FOUND;
adapterIndex++)
{
/*
DXGI_ADAPTER_DESC1 aDesc;
ThrowIfFailed("GetDesc1", adapter->GetDesc1(&aDesc));
var aDescName = new ReadOnlySpan<char>(aDesc.Description, 128);
Logger.DebugS("clyde.ogl.angle", aDescName.ToString());
adapter->Release();
*/
_adapter = adapter;
break;
}
factory6->Release();
}
#pragma warning restore CS0162
#pragma warning restore CA1416
Span<D3D_FEATURE_LEVEL> featureLevels = stackalloc D3D_FEATURE_LEVEL[]
{
// 11_0 can do GLES3
D3D_FEATURE_LEVEL_11_0,
// 9_3 can do GLES2
D3D_FEATURE_LEVEL_9_3,
// If we get a 9_1 FL we can't do D3D11 based ANGLE,
// but ANGLE can do it manually via the D3D9 renderer.
// In this case, abort custom swap chain and let ANGLE handle everything.
D3D_FEATURE_LEVEL_9_1
};
if (Clyde._cfg.GetCVar(CVars.DisplayAngleForceEs2))
{
featureLevels = stackalloc D3D_FEATURE_LEVEL[]
{
// Don't allow FL 11_0 so ANGLE is forced to init GLES2.
D3D_FEATURE_LEVEL_9_3,
D3D_FEATURE_LEVEL_9_1
};
}
if (Clyde._cfg.GetCVar(CVars.DisplayAngleForce10_0))
{
featureLevels = stackalloc D3D_FEATURE_LEVEL[]
{
D3D_FEATURE_LEVEL_10_0,
};
}
fixed (ID3D11Device** device = &_device)
fixed (D3D_FEATURE_LEVEL* fl = &featureLevels[0])
{
ThrowIfFailed("D3D11CreateDevice", D3D11CreateDevice(
(IDXGIAdapter*) _adapter,
_adapter == null ? D3D_DRIVER_TYPE_HARDWARE : D3D_DRIVER_TYPE_UNKNOWN,
HMODULE.NULL,
0,
fl,
(uint) featureLevels.Length,
D3D11_SDK_VERSION,
device,
null,
null
));
}
// Get adapter from the device.
ThrowIfFailed("QueryInterface", _device->QueryInterface(__uuidof<IDXGIDevice1>(), (void**) &dxgiDevice));
fixed (IDXGIAdapter1** ptrAdapter = &_adapter)
{
ThrowIfFailed("GetParent", dxgiDevice->GetParent(__uuidof<IDXGIAdapter1>(), (void**) ptrAdapter));
}
_deviceFl = _device->GetFeatureLevel();
DXGI_ADAPTER_DESC1 desc;
ThrowIfFailed("GetDesc1", _adapter->GetDesc1(&desc));
var descName = new ReadOnlySpan<char>(desc.Description, 128).TrimEnd('\0');
Logger.DebugS("clyde.ogl.angle", "Successfully created D3D11 device!");
Logger.DebugS("clyde.ogl.angle", $"D3D11 Device Adapter: {descName.ToString()}");
Logger.DebugS("clyde.ogl.angle", $"D3D11 Device FL: {_deviceFl}");
if (_deviceFl == D3D_FEATURE_LEVEL_9_1)
{
throw new Exception(
"D3D11 device has too low FL (need at least 9_3). Aborting custom swap chain!");
}
}
finally
{
if (dxgiDevice != null)
dxgiDevice->Release();
}
}
public override void Shutdown()
{
// Shut down ANGLE.
if (_eglDisplay != null)
eglTerminate(_eglDisplay);
if (_eglDevice != null)
eglReleaseDeviceANGLE(_eglDevice);
// Shut down D3D11/DXGI
if (_factory != null)
_factory->Release();
if (_adapter != null)
_adapter->Release();
if (_device != null)
_device->Release();
}
public override void SwapAllBuffers()
{
foreach (var data in _windowData.Values)
{
data.SwapChain->Present(_swapInterval, 0);
}
}
public override void WindowResized(WindowReg reg, Vector2i oldSize)
{
var data = _windowData[reg.Id];
DestroyBackbuffer(data);
ThrowIfFailed("ResizeBuffers", data.SwapChain->ResizeBuffers(
2,
(uint) reg.FramebufferSize.X, (uint) reg.FramebufferSize.Y,
Clyde._hasGLSrgb ? DXGI_FORMAT_R8G8B8A8_UNORM_SRGB : DXGI_FORMAT_R8G8B8A8_UNORM,
0));
SetupBackbuffer(data);
if (reg.IsMainWindow)
eglMakeCurrent(_eglDisplay, data.EglBackbuffer, data.EglBackbuffer, _eglContext);
}
private IDXGIAdapter1* TryFindAdapterWithName(string name)
{
uint idx = 0;
while (true)
{
IDXGIAdapter1* adapter;
var hr = _factory->EnumAdapters1(idx++, &adapter);
if (hr == DXGI_ERROR_NOT_FOUND)
break;
ThrowIfFailed("EnumAdapters1", hr);
DXGI_ADAPTER_DESC1 desc;
ThrowIfFailed("GetDesc1", adapter->GetDesc1(&desc));
var descName = new ReadOnlySpan<char>(desc.Description, 128);
if (descName.StartsWith(name))
return adapter;
adapter->Release();
}
return null;
}
public override void* GetProcAddress(string name)
{
Span<byte> buf = stackalloc byte[128];
var len = Encoding.UTF8.GetBytes(name, buf);
buf[len] = 0;
fixed (byte* ptr = &buf[0])
{
return eglGetProcAddress(ptr);
}
}
public override void BindWindowRenderTarget(WindowId rtWindowId)
{
var data = _windowData[rtWindowId];
var result = eglMakeCurrent(_eglDisplay, data.EglBackbuffer, data.EglBackbuffer, _eglContext);
if (result == EGL_FALSE)
throw new Exception("eglMakeCurrent failed.");
GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
Clyde.CheckGlError();
}
private static void ThrowIfFailed(string methodName, HRESULT hr)
{
if (FAILED(hr))
{
Marshal.ThrowExceptionForHR(hr);
}
}
private static string DumpEglConfig(void* display, void* config)
{
var sb = new StringBuilder();
sb.Append($"cfg: {Get(EGL_CONFIG_ID):000} | ");
sb.AppendFormat(
"R/G/B/A/D/S: {0}/{1}/{2}/{3}/{4:00}/{5} | ",
Get(EGL_RED_SIZE), Get(EGL_GREEN_SIZE), Get(EGL_BLUE_SIZE), Get(EGL_ALPHA_SIZE),
Get(EGL_DEPTH_SIZE), Get(EGL_STENCIL_SIZE));
// COLOR_BUFFER_TYPE
sb.Append($"CBT: {Get(EGL_COLOR_BUFFER_TYPE)} | ");
sb.Append($"CC: {Get(EGL_CONFIG_CAVEAT)} | ");
sb.Append($"CONF: {Get(EGL_CONFORMANT)} | ");
sb.Append($"NAT: {Get(EGL_NATIVE_VISUAL_ID)} | ");
sb.Append($"SAMPLES: {Get(EGL_SAMPLES)} | ");
sb.Append($"SAMPLE_BUFFERS: {Get(EGL_SAMPLE_BUFFERS)} | ");
sb.Append($"ORIENTATION: {Get(EGL_OPTIMAL_SURFACE_ORIENTATION_ANGLE)} | ");
sb.Append($"RENDERABLE: {Get(EGL_RENDERABLE_TYPE)}");
return sb.ToString();
int Get(int attrib)
{
int value;
if (eglGetConfigAttrib(display, config, attrib, &value) == EGL_FALSE)
throw new Exception("eglGetConfigAttrib failed!");
return value;
}
}
private sealed class WindowData
{
public WindowReg Reg = default!;
public IDXGISwapChain* SwapChain;
public ID3D11Texture2D* Backbuffer;
public void* EglBackbuffer;
}
}
}
}