using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using OpenTK.Graphics.OpenGL; using OpenTK.Mathematics; using Vector2 = System.Numerics.Vector2; using Vector3 = System.Numerics.Vector3; namespace Dashboard.Drawing.OpenGL { public class DrawCallRecorder : IGLDisposable, IInitializer { private int _vao = 0; private int _vbo = 0; private readonly List _vertices = new List(); private readonly List _calls = new List(); private int _start = 0; private int _count = 0; private int _primitives = 0; private Vector3 _charCoords; private int _cmdIndex; private int _texture0, _texture1, _texture2, _texture3; private TextureTarget _target0, _target1, _target2, _target3; private Matrix4 _transforms = Matrix4.Identity; public int CommandModulus = 64; public int CommandBuffer = 0; public int CommandSize = 64; private int CommandByteSize => CommandModulus * CommandSize; public int TransformsLocation { get; set; } public void Transforms(in Matrix4 transforms) { _transforms = transforms; } public void Begin(PrimitiveType type) { if (_primitives != 0) throw new InvalidOperationException("Attempt to begin new draw call before finishing previous one."); _primitives = (int)type; _start = _vertices.Count; _count = 0; } public void TexCoords2(Vector2 texCoords) { _charCoords = new Vector3(texCoords, 0); } public void CharCoords(Vector3 charCoords) { _charCoords = charCoords; } public void CommandIndex(int index) { _cmdIndex = index; } public void Vertex3(Vector3 vertex) { _vertices.Add(new DrawVertex() { Position = vertex, CharCoords = _charCoords, CmdIndex = _cmdIndex % CommandModulus, }); _count++; } public void End() { if (_primitives == 0) throw new InvalidOperationException("Attempt to end draw call before starting one."); _calls.Add( new DrawCall() { Type = (PrimitiveType)_primitives, Start = _start, Count = _count, CmdIndex = _cmdIndex, Target0 = _target0, Target1 = _target1, Target2 = _target2, Target3 = _target3, Texture0 = _texture0, Texture1 = _texture1, Texture2 = _texture2, Texture3 = _texture3, Transforms = _transforms, }); _primitives = 0; } public void BindTexture(TextureTarget target, int texture) => BindTexture(target, 0, texture); public void BindTexture(TextureTarget target, int unit, int texture) { switch (unit) { case 0: _texture0 = 0; _target0 = target; break; case 1: _texture1 = 0; _target1 = target; break; case 2: _texture2 = 0; _target2 = target; break; case 3: _texture3 = 0; _target3 = target; break; default: throw new ArgumentOutOfRangeException(nameof(unit), "I did not write support for more than 4 textures."); } } public void DrawArrays(PrimitiveType type, int first, int count) { throw new NotImplementedException(); } public void Execute() { GL.BindVertexArray(_vao); GL.BindBuffer(BufferTarget.ArrayBuffer, _vbo); ReadOnlySpan vertices = CollectionsMarshal.AsSpan(_vertices); GL.BufferData(BufferTarget.ArrayBuffer, _vertices.Count * Unsafe.SizeOf(), vertices, BufferUsage.DynamicDraw); foreach (DrawCall call in _calls) { GL.BindBufferRange(BufferTarget.UniformBuffer, 0, CommandBuffer, call.CmdIndex / CommandModulus * CommandByteSize, CommandByteSize); GL.ActiveTexture(TextureUnit.Texture0); GL.BindTexture(call.Target0, call.Texture0); GL.ActiveTexture(TextureUnit.Texture1); GL.BindTexture(call.Target1, call.Texture1); GL.ActiveTexture(TextureUnit.Texture2); GL.BindTexture(call.Target2, call.Texture2); GL.ActiveTexture(TextureUnit.Texture3); GL.BindTexture(call.Target3, call.Texture3); Matrix4 transforms = call.Transforms; GL.UniformMatrix4f(TransformsLocation, 1, true, ref transforms); GL.DrawArrays(call.Type, call.Start, call.Count); } } public void Clear() { _vertices.Clear(); _calls.Clear(); } public void Dispose() { throw new NotImplementedException(); } public void Dispose(bool safeExit) { throw new NotImplementedException(); } public bool IsInitialized { get; private set; } public void Initialize() { if (IsInitialized) return; IsInitialized = true; _vao = GL.CreateVertexArray(); _vbo = GL.CreateBuffer(); GL.BindVertexArray(_vao); GL.BindBuffer(BufferTarget.ArrayBuffer, _vbo); GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 32, 0); GL.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, 32, 16); GL.VertexAttribIPointer(2, 1, VertexAttribIType.Int, 32, 28); GL.EnableVertexAttribArray(0); GL.EnableVertexAttribArray(1); GL.EnableVertexAttribArray(2); } private struct DrawCall { public PrimitiveType Type; public int Start; public int Count; public int CmdIndex; public int Texture0; public int Texture1; public int Texture2; public int Texture3; public TextureTarget Target0; public TextureTarget Target1; public TextureTarget Target2; public TextureTarget Target3; public Matrix4 Transforms; } [StructLayout(LayoutKind.Explicit, Size = 32)] private struct DrawVertex { [FieldOffset(0)] public Vector3 Position; [FieldOffset(16)] public Vector3 CharCoords; [FieldOffset(28)] public int CmdIndex; } } /// /// A customizable immediate mode draw call queue, for the modern OpenGL user. /// /// The call info type. /// The vertex structure. public abstract class DrawCallRecorder : IGLDisposable, IInitializer where TVertex : unmanaged { /// /// The vertex array for this queue. /// public int Vao { get; private set; } /// /// The vertex buffer for this queue. /// public int Vbo { get; private set; } /// /// Number of calls recorded in this queue. /// public int CallCount => Calls.Count; /// /// The number of total vertices recorded. /// public int TotalVertices => Vertices.Count; /// /// The latest draw call info. /// public ref TCall CurrentCall => ref _currentCall; /// /// The latest vertex emitted. /// public ref TVertex CurrentVertex => ref _currentVertex; /// /// True if currently recording a draw call. /// public bool InCall => _primitiveMode != 0; /// /// Size of one vertex. /// protected int VertexSize => Unsafe.SizeOf(); /// /// The list of draw calls. /// protected List Calls { get; } = new List(); /// /// The list of all vertices. /// protected List Vertices { get; } = new List(); /// /// The value to write for the draw call info at the start of a call. /// [Pure] protected virtual TCall DefaultCall => default(TCall); /// /// The value to write for last vertex at the start of a call. /// [Pure] protected virtual TVertex DefaultVertex => default; private int _start = 0; private int _count = 0; private int _primitiveMode = 0; private TCall _currentCall; private TVertex _currentVertex; protected DrawCallRecorder() { _currentCall = DefaultCall; _currentVertex = DefaultVertex; } /// /// Record a draw call directly. /// /// The primitive type to use. /// The call info structure to use /// The list of vertices to use. /// You attempted to use this function during another draw call. public void DrawArrays(PrimitiveType type, in TCall callInfo, ReadOnlySpan vertices) { if (InCall) throw new InvalidOperationException("Cannot use draw arrays in the middle of an ongoing immediate-mode call."); DrawCall call = new DrawCall() { Type = type, Start = Vertices.Count, Count = vertices.Length, CallInfo = callInfo, }; Vertices.AddRange(vertices); Calls.Add(call); } /// /// Start a draw call. /// /// The primitive type for the call. public void Begin(PrimitiveType type) => Begin(type, DefaultCall); /// /// Start a draw call. /// /// The primitive type for the call. /// The call info. /// You attempted to create a draw call within a draw call. public void Begin(PrimitiveType type, TCall callInfo) { if (InCall) throw new InvalidOperationException("Attempt to begin new draw call before finishing previous one."); _primitiveMode = (int)type; _start = Vertices.Count; _count = 0; CurrentCall = callInfo; CurrentVertex = DefaultVertex; } /// /// Emit the latest or modified vertex. /// public void Vertex() { Vertex(CurrentVertex); } /// /// Emit a vertex. /// /// The vertex to emit. public void Vertex(in TVertex vertex) { Vertices.Add(CurrentVertex = vertex); _count++; } /// /// End the current call. /// /// You tried to end a call that you didn't begin recording. public void End() { if (!InCall) throw new InvalidOperationException("Attempt to end draw call before starting one."); Calls.Add(new DrawCall() { Start = _start, Count = _count, Type = (PrimitiveType)_primitiveMode, CallInfo = CurrentCall, }); _primitiveMode = 0; } /// /// Called by the execution engine before a draw call is executed. /// /// The call to prepare. protected abstract void PrepareCall(in TCall call); /// /// Set the vertex format for the and created by the recorder. /// protected abstract void SetVertexFormat(); /// /// Execute all the recorded draw calls. /// public void Execute() { GL.BindVertexArray(Vao); GL.BindBuffer(BufferTarget.ArrayBuffer, Vbo); ReadOnlySpan vertices = CollectionsMarshal.AsSpan(Vertices); GL.BufferData(BufferTarget.ArrayBuffer, Vertices.Count * VertexSize, vertices, BufferUsage.DynamicDraw); foreach (DrawCall call in Calls) { PrepareCall(call.CallInfo); GL.DrawArrays(call.Type, call.Start, call.Count); } } /// /// Clear the draw call queue. /// public void Clear() { Vertices.Clear(); Calls.Clear(); } public void Dispose() { throw new NotImplementedException(); } public void Dispose(bool safeExit) { throw new NotImplementedException(); } public bool IsInitialized { get; private set; } public void Initialize() { if (IsInitialized) return; IsInitialized = true; Vao = GL.CreateVertexArray(); Vbo = GL.CreateBuffer(); GL.BindVertexArray(Vao); GL.BindBuffer(BufferTarget.ArrayBuffer, Vbo); SetVertexFormat(); } protected struct DrawCall { public PrimitiveType Type; public int Start; public int Count; public TCall CallInfo; } } }