Files
RobustToolbox/Robust.Client/Graphics/Clyde/GLContext/GLContextAngle.cs
Pieter-Jan Briers cf230b3454 Warning fixes centered around Clyde
Pulling entity systems into direct fields in Clyde to make it now painful. This required adding an event to ClientEntityManager when these become available, as they are only available when the client is in a server/single player.
2024-07-23 17:49:56 +02:00

583 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 readonly ISawmill _sawmill;
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)
{
_sawmill = clyde._logManager.GetSawmill("clyde.ogl.angle");
}
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)
{
_sawmill.Error($"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));
_sawmill.Debug($"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));
_sawmill.Debug("EGL initialized!");
_sawmill.Debug($"EGL vendor: {vendor}!");
_sawmill.Debug($"EGL version: {version}!");
_sawmill.Debug($"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!");
_sawmill.Debug($"{numConfigs} EGL configs possible!");
for (var i = 0; i < numConfigs; i++)
{
_sawmill.Debug(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!");
_sawmill.Debug("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)
{
_sawmill.Warning($"Unable to find display adapter with requested name: {adapterName}");
}
_sawmill.Debug($"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 = ((ReadOnlySpan<char>)desc.Description).TrimEnd('\0');
_sawmill.Debug("Successfully created D3D11 device!");
_sawmill.Debug($"D3D11 Device Adapter: {descName.ToString()}");
_sawmill.Debug($"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 = ((ReadOnlySpan<char>)desc.Description);
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;
}
}
}
}