Files
RobustToolbox/SS14.Client/Graphics/Clyde/Clyde.cs
2019-02-21 18:13:03 +01:00

643 lines
23 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Text;
using System.Threading;
using JetBrains.Annotations;
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL4;
using SS14.Client.Input;
using SS14.Client.Interfaces.Graphics;
using SS14.Client.Interfaces.Graphics.ClientEye;
using SS14.Client.Interfaces.Graphics.Overlays;
using SS14.Client.Interfaces.ResourceManagement;
using SS14.Client.Interfaces.UserInterface;
using SS14.Client.ResourceManagement;
using SS14.Shared.Configuration;
using SS14.Shared.Interfaces.GameObjects;
using SS14.Shared.Interfaces.Map;
using SS14.Shared.IoC;
using SS14.Shared.Log;
using SS14.Shared.Maths;
using SS14.Shared.Utility;
using Matrix3 = SS14.Shared.Maths.Matrix3;
using Vector2 = SS14.Shared.Maths.Vector2;
using Vector3 = SS14.Shared.Maths.Vector3;
using Vector4 = SS14.Shared.Maths.Vector4;
namespace SS14.Client.Graphics.Clyde
{
/// <summary>
/// Responsible for most things rendering on OpenGL mode.
/// </summary>
internal sealed partial class Clyde : DisplayManager, IDisplayManagerOpenGL, IDisposable
{
[Dependency] private readonly IResourceCache _resourceCache;
[Dependency] private readonly IEyeManager _eyeManager;
[Dependency] private readonly IMapManager _mapManager;
[Dependency] private readonly IOverlayManager _overlayManager;
[Dependency] private readonly IEntityManager _entityManager;
[Dependency] private readonly IUserInterfaceManagerInternal _userInterfaceManager;
private Vector2i _windowSize;
private GameWindow _window;
private const int ProjViewBindingIndex = 0;
private Buffer ProjViewUBO;
private Buffer BatchVBO;
private Buffer BatchEBO;
private OGLHandle BatchVAO;
private OGLHandle BatchArrayedVAO;
// VBO to draw a single quad.
private Buffer QuadVBO;
private OGLHandle QuadVAO;
private ShaderProgram Vertex2DProgram;
private ShaderProgram Vertex2DArrayProgram;
private const string UniModUV = "modifyUV";
private const string UniModArrayIndex = "modifyArrayIndex";
private const string UniModelMatrix = "modelMatrix";
private const string UniModulate = "modulate";
// Thread the window is instantiated on.
// OpenGL is allergic to multi threading so we need to check this.
private Thread _mainThread;
private bool _drawingSplash;
private ShaderProgram _currentProgram;
// Following fields are performance tweaks mostly.
/// <summary>
/// If true, re-allocate buffer objects with BufferData instead of using BufferSubData.
/// </summary>
private bool _reallocateBuffers = false;
public override Vector2i ScreenSize => new Vector2i(_window.Width, _window.Height);
private readonly HashSet<string> OpenGLExtensions = new HashSet<string>();
private bool HasKHRDebug => HasExtension("GL_KHR_debug");
public override void SetWindowTitle(string title)
{
_window.Title = title;
}
public override void Initialize()
{
_initWindow();
ReloadConfig();
}
public void ProcessInput(FrameEventArgs frameEventArgs)
{
_window.ProcessEvents();
}
public void Ready()
{
_drawingSplash = false;
}
public Vector2 MouseScreenPosition { get; private set; }
public override void ReloadConfig()
{
base.ReloadConfig();
_window.VSync = VSync ? VSyncMode.On : VSyncMode.Off;
_window.WindowState = WindowMode == WindowMode.Fullscreen ? WindowState.Fullscreen : WindowState.Normal;
}
public override event Action<WindowResizedEventArgs> OnWindowResized;
private void _initWindow()
{
_window = new GameWindow(
800,
600,
GraphicsMode.Default,
"Space Station 14",
GameWindowFlags.Default,
DisplayDevice.Default,
4, 1,
#if DEBUG
GraphicsContextFlags.Debug | GraphicsContextFlags.ForwardCompatible
#else
GraphicsContextFlags.ForwardCompatible
#endif
)
{
Visible = true
};
_windowSize = new Vector2i(_window.Width, _window.Height);
_mainThread = Thread.CurrentThread;
_window.KeyDown += (sender, eventArgs) =>
{
_gameController.GameController.KeyDown((KeyEventArgs) eventArgs);
};
_window.KeyUp += (sender, eventArgs) => { _gameController.GameController.KeyUp((KeyEventArgs) eventArgs); };
_window.Closed += (sender, eventArgs) => { _gameController.GameController.Shutdown("Window closed"); };
_window.Resize += (sender, eventArgs) =>
{
var oldSize = _windowSize;
_windowSize = new Vector2i(_window.Width, _window.Height);
GL.Viewport(0, 0, _window.Width, _window.Height);
OnWindowResized?.Invoke(new WindowResizedEventArgs(oldSize, _windowSize));
};
_window.MouseDown += (sender, eventArgs) =>
{
var mouseButtonEventArgs = (MouseButtonEventArgs) eventArgs;
_gameController.GameController.MouseDown(mouseButtonEventArgs);
if (!mouseButtonEventArgs.Handled)
{
_gameController.GameController.KeyDown((KeyEventArgs) eventArgs);
}
};
_window.MouseUp += (sender, eventArgs) =>
{
var mouseButtonEventArgs = (MouseButtonEventArgs) eventArgs;
_gameController.GameController.MouseUp(mouseButtonEventArgs);
if (!mouseButtonEventArgs.Handled)
{
_gameController.GameController.KeyUp((KeyEventArgs) eventArgs);
}
};
_window.MouseMove += (sender, eventArgs) =>
{
MouseScreenPosition = new Vector2(eventArgs.X, eventArgs.Y);
_gameController.GameController.MouseMove((MouseMoveEventArgs) eventArgs);
};
_window.MouseWheel += (sender, eventArgs) =>
{
_gameController.GameController.MouseWheel((MouseWheelEventArgs) eventArgs);
};
_window.KeyPress += (sender, eventArgs) =>
{
// If this is a surrogate it has to be specifically handled and I'm not doing that yet.
DebugTools.Assert(!char.IsSurrogate(eventArgs.KeyChar));
_gameController.GameController.TextEntered(new TextEventArgs(eventArgs.KeyChar));
};
_initOpenGL();
}
private void _initOpenGL()
{
_loadExtensions();
GL.Enable(EnableCap.Blend);
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
Logger.DebugS("ogl", "OpenGL Vendor: {0}", GL.GetString(StringName.Vendor));
Logger.DebugS("ogl", "OpenGL Version: {0}", GL.GetString(StringName.Version));
#if DEBUG
_hijackDebugCallback();
#endif
_loadStockTextures();
Vertex2DProgram = _compileProgram(
new ResourcePath("/Shaders/Internal/sprite.vert"),
new ResourcePath("/Shaders/Internal/sprite.frag"),
"Vertex2DProgram");
Vertex2DProgram.BindBlock("projectionViewMatrices", ProjViewBindingIndex);
Vertex2DArrayProgram = _compileProgram(
new ResourcePath("/Shaders/Internal/sprite-arrayed.vert"),
new ResourcePath("/Shaders/Internal/sprite-arrayed.frag"),
"Vertex2DArrayProgram");
Vertex2DArrayProgram.BindBlock("projectionViewMatrices", ProjViewBindingIndex);
var quadVertices = new[]
{
new Vertex2D(1, 0, 1, 1, 1),
new Vertex2D(0, 0, 0, 1, 1),
new Vertex2D(1, 1, 1, 0, 1),
new Vertex2D(0, 1, 0, 0, 1),
};
QuadVBO = new Buffer<Vertex2D>(this, BufferTarget.ArrayBuffer, BufferUsageHint.StaticDraw, quadVertices,
"QuadVBO");
QuadVAO = new OGLHandle(GL.GenVertexArray());
GL.BindVertexArray(QuadVAO.Handle);
_objectLabelMaybe(ObjectLabelIdentifier.VertexArray, QuadVAO, "QuadVAO");
// Vertex Coords
GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, Vertex2D.SizeOf, 0);
GL.EnableVertexAttribArray(0);
// Texture Coords.
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, Vertex2D.SizeOf, 2 * sizeof(float));
GL.EnableVertexAttribArray(1);
// Texture Array Index.
GL.VertexAttribPointer(2, 1, VertexAttribPointerType.Float, false, Vertex2D.SizeOf, 4 * sizeof(float));
GL.EnableVertexAttribArray(2);
BatchVBO = new Buffer(this, BufferTarget.ArrayBuffer, BufferUsageHint.DynamicDraw,
Vertex2D.SizeOf * BatchVertexData.Length, "BatchVBO");
BatchVAO = new OGLHandle(GL.GenVertexArray());
GL.BindVertexArray(BatchVAO.Handle);
_objectLabelMaybe(ObjectLabelIdentifier.VertexArray, BatchVAO, "BatchVAO");
// Vertex Coords
GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, Vertex2D.SizeOf, 0);
GL.EnableVertexAttribArray(0);
// Texture Coords.
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, Vertex2D.SizeOf, 2 * sizeof(float));
GL.EnableVertexAttribArray(1);
BatchEBO = new Buffer(this, BufferTarget.ElementArrayBuffer, BufferUsageHint.DynamicDraw,
sizeof(ushort) * BatchIndexData.Length, "BatchEBO");
BatchArrayedVAO = new OGLHandle(GL.GenVertexArray());
BatchVBO.Use();
BatchEBO.Use();
GL.BindVertexArray(BatchArrayedVAO.Handle);
_objectLabelMaybe(ObjectLabelIdentifier.VertexArray, BatchArrayedVAO, "BatchArrayedVAO");
// Vertex Coords
GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, Vertex2D.SizeOf, 0);
GL.EnableVertexAttribArray(0);
// Texture Coords.
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, Vertex2D.SizeOf, 2 * sizeof(float));
GL.EnableVertexAttribArray(1);
// Texture Array Index.
GL.VertexAttribPointer(2, 1, VertexAttribPointerType.Float, false, Vertex2D.SizeOf, 4 * sizeof(float));
GL.EnableVertexAttribArray(2);
ProjViewUBO = new Buffer(this, BufferTarget.UniformBuffer, BufferUsageHint.StreamDraw, "ProjViewUBO");
unsafe
{
ProjViewUBO.Reallocate(sizeof(ProjViewMatrices));
}
GL.BindBufferBase(BufferRangeTarget.UniformBuffer, ProjViewBindingIndex, ProjViewUBO.Handle);
_drawingSplash = true;
_renderHandle = new RenderHandle(this);
Render(null);
}
private void _displaySplash(IRenderHandle handle)
{
var texture = _resourceCache.GetResource<TextureResource>("/Textures/Logo/logo.png").Texture;
var drawHandle = handle.CreateHandleScreen();
drawHandle.DrawTexture(texture, (ScreenSize - texture.Size) / 2);
}
private void _hijackDebugCallback()
{
if (!HasKHRDebug)
{
Logger.DebugS("ogl", "KHR_debug not present, OpenGL debug logging not enabled.");
return;
}
GL.Enable(EnableCap.DebugOutput);
GL.Enable(EnableCap.DebugOutputSynchronous);
GCHandle.Alloc(_debugMessageCallbackInstance);
// See https://github.com/opentk/opentk/issues/880
var type = typeof(GL);
// ReSharper disable once PossibleNullReferenceException
var entryPoints = (IntPtr[]) type.GetField("EntryPoints", BindingFlags.Static | BindingFlags.NonPublic)
.GetValue(null);
var ep = entryPoints[184];
var d = Marshal.GetDelegateForFunctionPointer<DebugMessageCallbackDelegate>(ep);
d(_debugMessageCallbackInstance, new IntPtr(0x3005));
}
private ShaderProgram _compileProgram(ResourcePath vertex, ResourcePath fragment, string name = null)
{
string vertexSource;
string fragmentSource;
using (var vertexReader = new StreamReader(_resourceCache.ContentFileRead(vertex), Encoding.UTF8))
{
vertexSource = vertexReader.ReadToEnd();
}
using (var fragmentReader = new StreamReader(_resourceCache.ContentFileRead(fragment), Encoding.UTF8))
{
fragmentSource = fragmentReader.ReadToEnd();
}
Shader vertexShader = null;
Shader fragmentShader = null;
try
{
try
{
vertexShader = new Shader(this, ShaderType.VertexShader, vertexSource, vertex.ToString());
}
catch (ShaderCompilationException e)
{
throw new ShaderCompilationException(
$"Failed to compile vertex shader {vertex}, see inner for details.", e);
}
try
{
fragmentShader = new Shader(this, ShaderType.FragmentShader, fragmentSource, fragment.ToString());
}
catch (ShaderCompilationException e)
{
throw new ShaderCompilationException(
$"Failed to compile fragment shader {fragment}, see inner for details.", e);
}
var program = new ShaderProgram(this, name);
program.Add(vertexShader);
program.Add(fragmentShader);
try
{
program.Link();
}
catch (ShaderCompilationException e)
{
program.Delete();
throw new ShaderCompilationException(
$"Failed to link shaders. vert: {vertex}, frag: {fragment}, see inner for details.", e);
}
return program;
}
finally
{
vertexShader?.Delete();
fragmentShader?.Delete();
}
}
private delegate void DebugMessageCallbackDelegate([MarshalAs(UnmanagedType.FunctionPtr)] DebugProc proc,
IntPtr userParam);
private static void _debugMessageCallback(DebugSource source, DebugType type, int id, DebugSeverity severity,
int length, IntPtr message, IntPtr userParam)
{
var contents = $"{source}: " + Marshal.PtrToStringAnsi(message, length);
var category = "ogl.debug";
switch (type)
{
case DebugType.DebugTypePerformance:
category += ".performance";
break;
case DebugType.DebugTypeOther:
category += ".other";
break;
case DebugType.DebugTypeError:
category += ".error";
break;
case DebugType.DebugTypeDeprecatedBehavior:
category += ".deprecated";
break;
case DebugType.DebugTypeUndefinedBehavior:
category += ".ub";
break;
case DebugType.DebugTypePortability:
category += ".portability";
break;
case DebugType.DebugTypeMarker:
case DebugType.DebugTypePushGroup:
case DebugType.DebugTypePopGroup:
// These are inserted by our own code so I imagine they're not necessary to log?
return;
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
switch (severity)
{
case DebugSeverity.DontCare:
Logger.InfoS(category, contents);
break;
case DebugSeverity.DebugSeverityNotification:
Logger.InfoS(category, contents);
break;
case DebugSeverity.DebugSeverityHigh:
Logger.ErrorS(category, contents);
break;
case DebugSeverity.DebugSeverityMedium:
Logger.ErrorS(category, contents);
break;
case DebugSeverity.DebugSeverityLow:
Logger.WarningS(category, contents);
break;
default:
throw new ArgumentOutOfRangeException(nameof(severity), severity, null);
}
}
private static readonly DebugProc _debugMessageCallbackInstance = _debugMessageCallback;
private void _loadExtensions()
{
var count = GL.GetInteger(GetPName.NumExtensions);
for (var i = 0; i < count; i++)
{
var extension = GL.GetString(StringNameIndexed.Extensions, i);
OpenGLExtensions.Add(extension);
}
}
private bool HasExtension(string extensionName)
{
return OpenGLExtensions.Contains(extensionName);
}
[Conditional("DEBUG")]
private void _objectLabelMaybe(ObjectLabelIdentifier identifier, int name, string label)
{
DebugTools.Assert(label != null);
if (!HasKHRDebug)
{
return;
}
GL.ObjectLabel(identifier, name, label.Length, label);
}
[Conditional("DEBUG")]
private void _objectLabelMaybe(ObjectLabelIdentifier identifier, OGLHandle name, string label)
{
_objectLabelMaybe(identifier, name.Handle, label);
}
[Conditional("DEBUG")]
private void _pushDebugGroupMaybe(in (uint, string) group)
{
if (!HasKHRDebug)
{
return;
}
var (id, name) = group;
GL.PushDebugGroup(DebugSourceExternal.DebugSourceApplication, id, name.Length, name);
}
[Conditional("DEBUG")]
private void _popDebugGroupMaybe()
{
if (!HasKHRDebug)
{
return;
}
GL.PopDebugGroup();
}
public void Dispose()
{
_window.Dispose();
}
[StructLayout(LayoutKind.Sequential)]
[PublicAPI]
private readonly struct Vertex2D
{
public static readonly int SizeOf;
public readonly Vector2 Position;
public readonly Vector2 TextureCoordinates;
public readonly float ArrayIndex;
static Vertex2D()
{
unsafe
{
SizeOf = sizeof(Vertex2D);
}
}
public Vertex2D(Vector2 position, Vector2 textureCoordinates, int arrayIndex)
{
Position = position;
TextureCoordinates = textureCoordinates;
ArrayIndex = arrayIndex;
}
public Vertex2D(float x, float y, float u, float v, int arrayIndex)
: this(new Vector2(x, y), new Vector2(u, v), arrayIndex)
{
}
public Vertex2D(Vector2 position, float u, float v, int arrayIndex)
: this(position, new Vector2(u, v), arrayIndex)
{
}
public override string ToString()
{
return $"Vertex2D: {Position}, {TextureCoordinates}";
}
}
// Go through the commit log if you wanna find why this struct exists.
// And why there's no implicit operator.
/// <summary>
/// Basically just a handle around the integer object handles returned by OpenGL.
/// </summary>
[PublicAPI]
private struct OGLHandle
{
public readonly int Handle;
public OGLHandle(int handle)
{
Handle = handle;
}
public bool Equals(OGLHandle other)
{
return Handle == other.Handle;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
return obj is OGLHandle other && Equals(other);
}
public override int GetHashCode()
{
return Handle;
}
public override string ToString()
{
return $"{nameof(Handle)}: {Handle}";
}
}
[StructLayout(LayoutKind.Explicit)]
[PublicAPI]
private struct ProjViewMatrices
{
[FieldOffset(0 * sizeof(float))] public Vector3 ProjMatrixC0;
[FieldOffset(4 * sizeof(float))] public Vector3 ProjMatrixC1;
[FieldOffset(8 * sizeof(float))] public Vector3 ProjMatrixC2;
[FieldOffset(12 * sizeof(float))] public Vector3 ViewMatrixC0;
[FieldOffset(16 * sizeof(float))] public Vector3 ViewMatrixC1;
[FieldOffset(20 * sizeof(float))] public Vector3 ViewMatrixC2;
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
[FieldOffset(24 * sizeof(float))] private readonly Vector4 _pad;
public ProjViewMatrices(in Matrix3 projMatrix, in Matrix3 viewMatrix)
{
_pad = Vector4.Zero;
ProjMatrixC0 = new Vector3(projMatrix.R0C0, projMatrix.R1C0, projMatrix.R2C0);
ProjMatrixC1 = new Vector3(projMatrix.R0C1, projMatrix.R1C1, projMatrix.R2C1);
ProjMatrixC2 = new Vector3(projMatrix.R0C2, projMatrix.R1C2, projMatrix.R2C2);
ViewMatrixC0 = new Vector3(viewMatrix.R0C0, viewMatrix.R1C0, viewMatrix.R2C0);
ViewMatrixC1 = new Vector3(viewMatrix.R0C1, viewMatrix.R1C1, viewMatrix.R2C1);
ViewMatrixC2 = new Vector3(viewMatrix.R0C2, viewMatrix.R1C2, viewMatrix.R2C2);
}
}
}
[Serializable]
[PublicAPI]
internal class ShaderCompilationException : Exception
{
public ShaderCompilationException()
{
}
public ShaderCompilationException(string message) : base(message)
{
}
public ShaderCompilationException(string message, Exception inner) : base(message, inner)
{
}
protected ShaderCompilationException(
SerializationInfo info,
StreamingContext context) : base(info, context)
{
}
}
}