diff --git a/Dashboard.Common/Gradient.cs b/Dashboard.Common/Gradient.cs
index f9f98bc..cf27537 100644
--- a/Dashboard.Common/Gradient.cs
+++ b/Dashboard.Common/Gradient.cs
@@ -30,7 +30,7 @@ namespace Dashboard
///
/// Represents a linear gradient.
///
- public struct Gradient : ICollection, ICloneable
+ public struct Gradient : ICollection, ICloneable, IEquatable
{
private readonly List _stops = new List();
@@ -199,5 +199,29 @@ namespace Dashboard
code.Add(item.GetHashCode());
return code.ToHashCode();
}
+
+ public bool Equals(Gradient other)
+ {
+ return
+ Type == other.Type &&
+ C0 == other.C0 &&
+ C1 == other.C1 &&
+ _stops.Equals(other._stops);
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is Gradient other && Equals(other);
+ }
+
+ public static bool operator ==(Gradient left, Gradient right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(Gradient left, Gradient right)
+ {
+ return !left.Equals(right);
+ }
}
}
diff --git a/Dashboard.Drawing.OpenGL/ContextCollector.cs b/Dashboard.Drawing.OpenGL/ContextCollector.cs
new file mode 100644
index 0000000..b5ccde8
--- /dev/null
+++ b/Dashboard.Drawing.OpenGL/ContextCollector.cs
@@ -0,0 +1,77 @@
+using System.Collections.Concurrent;
+using OpenTK.Graphics.OpenGL;
+
+namespace Dashboard.Drawing.OpenGL
+{
+ public class ContextCollector : IDisposable
+ {
+ private readonly ConcurrentQueue _disposedObjects = new ConcurrentQueue();
+
+ public void Dispose()
+ {
+ while (_disposedObjects.TryDequeue(out GLObject obj))
+ {
+ obj.Dispose();
+ }
+ }
+
+ void DeleteObject(ObjectIdentifier identifier, int handle) => _disposedObjects.Enqueue(new GLObject(identifier, handle));
+
+ public void DeleteTexture(int texture) => DeleteObject(ObjectIdentifier.Texture, texture);
+ public void DeleteBufffer(int buffer) => DeleteObject(ObjectIdentifier.Buffer, buffer);
+ public void DeleteFramebuffer(int framebuffer) => DeleteObject(ObjectIdentifier.Framebuffer, framebuffer);
+ public void DeleteRenderBuffer(int renderbuffer) => DeleteObject(ObjectIdentifier.Renderbuffer, renderbuffer);
+ public void DeleteSampler(int sampler) => DeleteObject(ObjectIdentifier.Sampler, sampler);
+ public void DeleteShader(int shader) => DeleteObject(ObjectIdentifier.Shader, shader);
+ public void DeleteProgram(int program) => DeleteObject(ObjectIdentifier.Program, program);
+ public void DeleteVertexArray(int vertexArray) => DeleteObject(ObjectIdentifier.VertexArray, vertexArray);
+ public void DeleteQuery(int query) => DeleteObject(ObjectIdentifier.Query, query);
+ public void DeleteProgramPipeline(int programPipeline) => DeleteObject(ObjectIdentifier.ProgramPipeline, programPipeline);
+ public void DeleteTransformFeedback(int transformFeedback) => DeleteObject(ObjectIdentifier.TransformFeedback, transformFeedback);
+
+ private readonly record struct GLObject(ObjectIdentifier Type, int Handle)
+ {
+ public void Dispose()
+ {
+ switch (Type)
+ {
+ case ObjectIdentifier.Texture:
+ GL.DeleteTexture(Handle);
+ break;
+ case ObjectIdentifier.Buffer:
+ GL.DeleteBuffer(Handle);
+ break;
+ case ObjectIdentifier.Framebuffer:
+ GL.DeleteFramebuffer(Handle);
+ break;
+ case ObjectIdentifier.Renderbuffer:
+ GL.DeleteRenderbuffer(Handle);
+ break;
+ case ObjectIdentifier.Sampler:
+ GL.DeleteSampler(Handle);
+ break;
+ case ObjectIdentifier.Shader:
+ GL.DeleteShader(Handle);
+ break;
+ case ObjectIdentifier.VertexArray:
+ GL.DeleteVertexArray(Handle);
+ break;
+ case ObjectIdentifier.Program:
+ GL.DeleteProgram(Handle);
+ break;
+ case ObjectIdentifier.Query:
+ GL.DeleteQuery(Handle);
+ break;
+ case ObjectIdentifier.ProgramPipeline:
+ GL.DeleteProgramPipeline(Handle);
+ break;
+ case ObjectIdentifier.TransformFeedback:
+ GL.DeleteTransformFeedback(Handle);
+ break;
+ }
+ }
+ }
+
+ public static readonly ContextCollector Global = new ContextCollector();
+ }
+}
diff --git a/Dashboard.Drawing.OpenGL/ContextExecutor.cs b/Dashboard.Drawing.OpenGL/ContextExecutor.cs
new file mode 100644
index 0000000..4d59991
--- /dev/null
+++ b/Dashboard.Drawing.OpenGL/ContextExecutor.cs
@@ -0,0 +1,134 @@
+using System.Drawing;
+using OpenTK.Graphics.OpenGL;
+
+namespace Dashboard.Drawing.OpenGL
+{
+ public class ContextExecutor : IDisposable
+ {
+ public GLEngine Engine { get; }
+ public IGLContext Context { get; }
+ public ContextResourcePool ResourcePool { get; }
+ protected bool IsDisposed { get; private set; } = false;
+
+ public bool IsInitialized { get; private set; } = false;
+
+ private int _program = 0;
+ public ContextExecutor(GLEngine engine, IGLContext context)
+ {
+ Engine = engine;
+ Context = context;
+
+ ResourcePool = Engine.ResourcePoolManager.Get(context);
+ ResourcePool.IncrementReference();
+ }
+
+ ~ContextExecutor()
+ {
+ DisposeInvoker(false);
+ }
+
+ public void Initialize()
+ {
+ if (IsInitialized)
+ return;
+ IsInitialized = true;
+
+ LoadShaders();
+ }
+
+ private void LoadShaders()
+ {
+ using Stream vsource = FetchEmbeddedResource("Dashboard.Drawing.OpenGL.default.vert");
+ using Stream fsource = FetchEmbeddedResource("Dashboard.Drawing.OpenGL.default.frag");
+ int vs = CompileShader(ShaderType.VertexShader, vsource);
+ int fs = CompileShader(ShaderType.FragmentShader, fsource);
+ _program = LinkProgram(vs, fs);
+ GL.DeleteShader(vs);
+ GL.DeleteShader(fs);
+ }
+
+ public void Draw(DrawQueue drawqueue) => Draw(drawqueue, new RectangleF(new PointF(0f,0f), Context.FramebufferSize));
+ public virtual void Draw(DrawQueue drawQueue, RectangleF bounds)
+ {
+
+ }
+
+ public virtual void EndFrame()
+ {
+
+ }
+
+ private void DisposeInvoker(bool disposing)
+ {
+ if (!IsDisposed)
+ return;
+
+ IsDisposed = true;
+
+ if (disposing)
+ GC.SuppressFinalize(this);
+
+ Dispose(disposing);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ ContextCollector.Global.DeleteProgram(_program);
+ }
+
+ public void Dispose() => DisposeInvoker(true);
+
+ private static int CompileShader(ShaderType type, string source)
+ {
+ int shader = GL.CreateShader(type);
+
+ GL.ShaderSource(shader, source);
+ GL.CompileShader(shader);
+
+ int compileStatus = 0;
+ GL.GetShaderi(shader, ShaderParameterName.CompileStatus, out compileStatus);
+
+ if (compileStatus == 0)
+ {
+ GL.GetShaderInfoLog(shader, out string log);
+ GL.DeleteShader(shader);
+ throw new Exception($"{type} Shader compilation failed: " + log);
+ }
+
+ return shader;
+ }
+
+ private static int CompileShader(ShaderType type, Stream stream)
+ {
+ using StreamReader reader = new StreamReader(stream, leaveOpen: true);
+
+ return CompileShader(type, reader.ReadToEnd());
+ }
+
+ private static int LinkProgram(int s1, int s2)
+ {
+ int program = GL.CreateProgram();
+
+ GL.AttachShader(program, s1);
+ GL.AttachShader(program, s2);
+
+ GL.LinkProgram(program);
+
+ int linkStatus = 0;
+ GL.GetProgrami(program, ProgramProperty.LinkStatus, out linkStatus);
+ if (linkStatus == 0)
+ {
+ GL.GetProgramInfoLog(program, out string log);
+ GL.DeleteProgram(program);
+ throw new Exception("Shader program linking failed: " + log);
+ }
+
+ return program;
+ }
+
+ private static Stream FetchEmbeddedResource(string name)
+ {
+ return typeof(ContextExecutor).Assembly.GetManifestResourceStream(name)!;
+ }
+ }
+}
diff --git a/Dashboard.Drawing.OpenGL/ContextResourcePool.cs b/Dashboard.Drawing.OpenGL/ContextResourcePool.cs
new file mode 100644
index 0000000..ddfc556
--- /dev/null
+++ b/Dashboard.Drawing.OpenGL/ContextResourcePool.cs
@@ -0,0 +1,81 @@
+namespace Dashboard.Drawing.OpenGL
+{
+ public class ContextResourcePoolManager
+ {
+ private readonly Dictionary _unique = new Dictionary();
+ private readonly Dictionary _shared = new Dictionary();
+
+ public ContextResourcePool Get(IGLContext context)
+ {
+ if (context.ContextGroup == -1)
+ {
+ if (!_unique.TryGetValue(context, out ContextResourcePool? pool))
+ {
+ pool = new ContextResourcePool(this, context);
+ }
+
+ return pool;
+ }
+ else
+ {
+ if (!_shared.TryGetValue(context.ContextGroup, out ContextResourcePool? pool))
+ {
+ pool = new ContextResourcePool(this, context.ContextGroup);
+ }
+
+ return pool;
+ }
+ }
+
+ internal void Disposed(ContextResourcePool pool)
+ {
+ // TODO:
+ }
+ }
+
+ public class ContextResourcePool : IGLDisposable, IArc
+ {
+ private int _references = 0;
+
+ public ContextResourcePoolManager Manager { get; }
+ public IGLContext? Context { get; private set; } = null;
+ public int ContextGroup { get; private set; } = -1;
+ public int References => _references;
+
+ internal ContextResourcePool(ContextResourcePoolManager manager, int contextGroup)
+ {
+ Manager = manager;
+ ContextGroup = contextGroup;
+ }
+
+ internal ContextResourcePool(ContextResourcePoolManager manager, IGLContext context)
+ {
+ Manager = manager;
+ Context = context;
+ }
+
+ ~ContextResourcePool()
+ {
+ Dispose(true, false);
+ }
+
+ public void Dispose() => Dispose(true, false);
+
+ public void Dispose(bool safeExit) => Dispose(safeExit, true);
+
+ private void Dispose(bool safeExit, bool disposing)
+ {
+ Manager.Disposed(this);
+ }
+
+ public void IncrementReference()
+ {
+ Interlocked.Increment(ref _references);
+ }
+
+ public bool DecrementReference()
+ {
+ return Interlocked.Decrement(ref _references) == 0;
+ }
+ }
+}
diff --git a/Dashboard.Drawing.OpenGL/Dashboard.Drawing.OpenGL.csproj b/Dashboard.Drawing.OpenGL/Dashboard.Drawing.OpenGL.csproj
new file mode 100644
index 0000000..a094872
--- /dev/null
+++ b/Dashboard.Drawing.OpenGL/Dashboard.Drawing.OpenGL.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Dashboard.Drawing.OpenGL/GLEngine.cs b/Dashboard.Drawing.OpenGL/GLEngine.cs
new file mode 100644
index 0000000..7f84aff
--- /dev/null
+++ b/Dashboard.Drawing.OpenGL/GLEngine.cs
@@ -0,0 +1,36 @@
+using OpenTK;
+using OpenTK.Graphics;
+
+namespace Dashboard.Drawing.OpenGL
+{
+ public class GLEngine
+ {
+ private readonly Dictionary _executors = new Dictionary();
+
+ public bool IsInitialized { get; private set; } = false;
+ public ContextResourcePoolManager ResourcePoolManager { get; private set; } = new ContextResourcePoolManager();
+
+ public void Initialize(IBindingsContext? bindingsContext = null)
+ {
+ if (IsInitialized)
+ return;
+ IsInitialized = true;
+
+ if (bindingsContext != null)
+ GLLoader.LoadBindings(bindingsContext);
+ }
+
+ public ContextExecutor GetExecutor(IGLContext glContext)
+ {
+ if (!_executors.TryGetValue(glContext, out ContextExecutor? executor))
+ {
+ executor = new ContextExecutor(this, glContext);
+ executor.Initialize();
+
+ _executors.Add(glContext, executor);
+ }
+
+ return executor;
+ }
+ }
+}
diff --git a/Dashboard.Drawing.OpenGL/GradientUniformBuffer.cs b/Dashboard.Drawing.OpenGL/GradientUniformBuffer.cs
new file mode 100644
index 0000000..478616d
--- /dev/null
+++ b/Dashboard.Drawing.OpenGL/GradientUniformBuffer.cs
@@ -0,0 +1,68 @@
+using System.Runtime.InteropServices;
+using OpenTK.Mathematics;
+
+namespace Dashboard.Drawing.OpenGL
+{
+ public class GradientUniformBuffer
+ {
+ private int _top = 0;
+ private readonly MappableBuffer _buffer = new MappableBuffer();
+ private readonly Dictionary _entries = new Dictionary();
+
+ public void Initialize()
+ {
+ _buffer.Initialize();
+ }
+
+ public Entry InternGradient(Gradient gradient)
+ {
+ if (_entries.TryGetValue(gradient, out Entry entry))
+ return entry;
+
+ int count = gradient.Count;
+ int offset = _top;
+ _top += count;
+
+ _buffer.EnsureCapacity(_top);
+ _buffer.Map();
+ Span span = _buffer.AsSpan()[offset.._top];
+
+ for (int i = 0; i < count; i++)
+ {
+ GradientStop stop = gradient[i];
+ span[i] = new GradientUniformStruct()
+ {
+ Position = stop.Position,
+ Color = new Vector4(
+ stop.Color.R / 255f,
+ stop.Color.G / 255f,
+ stop.Color.B / 255f,
+ stop.Color.A / 255f),
+ };
+ }
+
+ entry = new Entry(offset, count);
+ _entries.Add(gradient, entry);
+
+ return entry;
+ }
+
+ public void Clear()
+ {
+ _entries.Clear();
+ _top = 0;
+ }
+
+ public record struct Entry(int Offset, int Count);
+ }
+
+ [StructLayout(LayoutKind.Explicit, Size = 8 * sizeof(float))]
+ public struct GradientUniformStruct
+ {
+ [FieldOffset(0 * sizeof(float))]
+ public float Position;
+
+ [FieldOffset(4 * sizeof(float))]
+ public Vector4 Color;
+ }
+}
diff --git a/Dashboard.Drawing.OpenGL/IArc.cs b/Dashboard.Drawing.OpenGL/IArc.cs
new file mode 100644
index 0000000..1bfc99c
--- /dev/null
+++ b/Dashboard.Drawing.OpenGL/IArc.cs
@@ -0,0 +1,24 @@
+namespace Dashboard.Drawing.OpenGL
+{
+ ///
+ /// Atomic reference counter.
+ ///
+ public interface IArc : IDisposable
+ {
+ ///
+ /// The number of references to this.
+ ///
+ int References { get; }
+
+ ///
+ /// Increment the number of references.
+ ///
+ void IncrementReference();
+
+ ///
+ /// Decrement the number of references.
+ ///
+ /// True if this was the last reference.
+ bool DecrementReference();
+ }
+}
diff --git a/Dashboard.Drawing.OpenGL/IGLContext.cs b/Dashboard.Drawing.OpenGL/IGLContext.cs
new file mode 100644
index 0000000..b326622
--- /dev/null
+++ b/Dashboard.Drawing.OpenGL/IGLContext.cs
@@ -0,0 +1,26 @@
+using System.Drawing;
+
+namespace Dashboard.Drawing.OpenGL
+{
+ ///
+ /// Interface for GL context operations
+ ///
+ public interface IGLContext
+ {
+ ///
+ /// The associated group for context sharing.
+ ///
+ /// -1 assigns no group.
+ public int ContextGroup { get; }
+
+ ///
+ /// The size of the framebuffer in pixels.
+ ///
+ public Size FramebufferSize { get; }
+
+ ///
+ /// Called when the context is disposed.
+ ///
+ event Action Disposed;
+ }
+}
diff --git a/Dashboard.Drawing.OpenGL/IGLDisposable.cs b/Dashboard.Drawing.OpenGL/IGLDisposable.cs
new file mode 100644
index 0000000..c1d9aba
--- /dev/null
+++ b/Dashboard.Drawing.OpenGL/IGLDisposable.cs
@@ -0,0 +1,16 @@
+namespace Dashboard.Drawing.OpenGL
+{
+ ///
+ /// Interface much like except GL resources are dropped.
+ ///
+ ///
+ /// The main reason this interface exists is that you need a way to differentiate
+ /// when a context is deleted, versus when the context is to remain present.
+ ///
+ public interface IGLDisposable : IDisposable
+ {
+ ///
+ /// Set to true to spend the time clearing out GL objects.
+ void Dispose(bool safeExit);
+ }
+}
diff --git a/Dashboard.Drawing.OpenGL/MappableBuffer.cs b/Dashboard.Drawing.OpenGL/MappableBuffer.cs
new file mode 100644
index 0000000..8081dea
--- /dev/null
+++ b/Dashboard.Drawing.OpenGL/MappableBuffer.cs
@@ -0,0 +1,118 @@
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using OpenTK.Graphics.OpenGL;
+
+namespace Dashboard.Drawing.OpenGL
+{
+ public class MappableBuffer : IDisposable
+ {
+ public int Handle { get; private set; } = 0;
+ public int Capacity { get; set; } = BASE_CAPACITY;
+ public IntPtr Pointer { get; private set; } = IntPtr.Zero;
+
+ private bool _isDisposed = false;
+ private const int BASE_CAPACITY = 4 << 10; // 4 KiB
+ private const int MAX_INCREMENT = 4 << 20; // 4 MiB
+
+ ~MappableBuffer()
+ {
+ Dispose(false);
+ }
+
+ public void Initialize()
+ {
+ Handle = GL.GenBuffer();
+ GL.BindBuffer(BufferTarget.ArrayBuffer, Handle);
+ GL.BufferData(BufferTarget.ArrayBuffer, Capacity, IntPtr.Zero, BufferUsage.DynamicDraw);
+ }
+
+ public void EnsureCapacity(int count)
+ {
+ if (count < 0)
+ throw new ArgumentOutOfRangeException(nameof(count));
+
+ if (Capacity >= count)
+ return;
+
+ AssertInitialized();
+ Unmap();
+
+ int sz = Unsafe.SizeOf();
+ int oldsize = Capacity * sz;
+ int request = count * sz;
+ int newsize;
+ if (request > MAX_INCREMENT)
+ {
+ newsize = ((request + MAX_INCREMENT - 1) / MAX_INCREMENT) * MAX_INCREMENT;
+ }
+ else
+ {
+ newsize = checked((int)BitOperations.RoundUpToPowerOf2((ulong)request));
+ }
+
+ int dest = GL.GenBuffer();
+
+ GL.BindBuffer(BufferTarget.CopyWriteBuffer, dest);
+ GL.BindBuffer(BufferTarget.CopyReadBuffer, Handle);
+
+ GL.BufferData(BufferTarget.CopyWriteBuffer, newsize, IntPtr.Zero, BufferUsage.DynamicDraw);
+ GL.CopyBufferSubData(CopyBufferSubDataTarget.CopyReadBuffer, CopyBufferSubDataTarget.CopyWriteBuffer, 0, 0, oldsize);
+
+ GL.DeleteBuffer(Handle);
+ Handle = dest;
+ Capacity = newsize;
+ }
+
+ public unsafe void Map()
+ {
+ if (Pointer != IntPtr.Zero)
+ return;
+
+ AssertInitialized();
+
+ GL.BindBuffer(BufferTarget.ArrayBuffer, Handle);
+ Pointer = (IntPtr)GL.MapBuffer(BufferTarget.ArrayBuffer, BufferAccess.ReadWrite);
+ }
+
+ public void Unmap()
+ {
+ if (Pointer == IntPtr.Zero)
+ return;
+
+ AssertInitialized();
+
+ GL.BindBuffer(BufferTarget.ArrayBuffer, Handle);
+ GL.UnmapBuffer(BufferTarget.ArrayBuffer);
+ }
+
+ public unsafe Span AsSpan()
+ {
+ if (Pointer == IntPtr.Zero)
+ throw new InvalidOperationException("The buffer is not currently mapped.");
+
+ AssertInitialized();
+
+ return new Span(Pointer.ToPointer(), Capacity / Unsafe.SizeOf());
+ }
+
+ private void AssertInitialized()
+ {
+ if (Handle == 0)
+ throw new InvalidOperationException("The buffer is not initialized.");
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (_isDisposed)
+ return;
+ _isDisposed = true;
+
+ if (disposing)
+ GC.SuppressFinalize(this);
+
+ ContextCollector.Global.DeleteBufffer(Handle);
+ }
+
+ public void Dispose() => Dispose(true);
+ }
+}
diff --git a/Dashboard.Drawing.OpenGL/TransformStack.cs b/Dashboard.Drawing.OpenGL/TransformStack.cs
new file mode 100644
index 0000000..000fe44
--- /dev/null
+++ b/Dashboard.Drawing.OpenGL/TransformStack.cs
@@ -0,0 +1,51 @@
+using OpenTK.Mathematics;
+
+namespace Dashboard.Drawing.OpenGL
+{
+ ///
+ /// The current stack of transformations.
+ ///
+ public class TransformStack
+ {
+ private Matrix4 _top = Matrix4.Identity;
+ private readonly Stack _stack = new Stack();
+
+ ///
+ /// The top-most transform matrix.
+ ///
+ public ref readonly Matrix4 Top => ref _top;
+
+ ///
+ /// The number of matrices in the stack.
+ ///
+ public int Count => _stack.Count;
+
+ ///
+ /// Push a transform.
+ ///
+ /// The transform to push.
+ public void Push(in Matrix4 transform)
+ {
+ _stack.Push(_top);
+ _top = transform * _top;
+ }
+
+ ///
+ /// Pop a transform.
+ ///
+ public void Pop()
+ {
+ if (!_stack.TryPop(out _top))
+ _top = Matrix4.Identity;
+ }
+
+ ///
+ /// Clear the stack of transformations.
+ ///
+ public void Clear()
+ {
+ _stack.Clear();
+ _top = Matrix4.Identity;
+ }
+ }
+}
diff --git a/Dashboard.Drawing.OpenGL/default.frag b/Dashboard.Drawing.OpenGL/default.frag
new file mode 100644
index 0000000..2e3d45a
--- /dev/null
+++ b/Dashboard.Drawing.OpenGL/default.frag
@@ -0,0 +1,97 @@
+#version 140
+
+#define DB_GRADIENT_MAX 16
+#define DB_COMMAND_MAX 64
+
+#define CMD_POINT 1
+#define CMD_LINE 2
+#define CMD_RECT 3
+#define CMD_TEXT 4
+
+#define STRIKE_INSET -1
+#define STRIKE_CENTER 0
+#define STRIKE_OUTSET 1
+
+in vec3 v_v3Position;
+in vec2 v_v2TexCoords;
+in int v_iCmdIndex;
+
+out vec4 f_Color;
+
+uniform sampler2D txForeground;
+uniform sampler2D txBackground;
+uniform sampler2DArray txCharacters;
+
+struct Gradient_t {
+ float fPosition;
+ float pad0;
+ float pad1;
+ float pad2;
+ vec4 v4Color;
+};
+
+uniform GradientBlock
+{
+ Gradient_t vstGradientStops[DB_GRADIENT_MAX];
+};
+
+vec4 getGradientColor(float position, int index, int count)
+{
+ position = clamp(position, 0, 1);
+
+ int i0 = 0;
+ float p0 = vstGradientStops[index + i0].fPosition;
+
+ int i1 = count - 1;
+ float p1 = vstGradientStops[index + i1].fPosition;
+
+ for (int i = 0; i < count; i++)
+ {
+ float px = vstGradientStops[index + i].fPosition;
+
+ if (px > p0 && px <= position)
+ {
+ p0 = px;
+ i0 = i;
+ }
+
+ if (px < p1 && px >= position)
+ {
+ p1 = px;
+ i1 = i;
+ }
+ }
+
+ vec4 c0 = vstGradientStops[index + i0].v4Color;
+ vec4 c1 = vstGradientStops[index + i1].v4Color;
+
+ float l = p1 - p0;
+ float w = (l > 0) ? (position - p0) / (p1 - p0) : 0;
+
+ return mix(c0, c1, w);
+}
+
+struct CommandCommon {
+ int iCommand;
+ int iStrikeKind;
+ float fBorderRadius;
+ int _padding;
+
+ int iFgGradientIndex;
+ int iFgGradientCount;
+ int iBgGradientIndex;
+ int iBgGradientCount;
+
+ vec4 v4FillColor;
+ vec4 v4BorderColor;
+};
+
+uniform CommandBlock
+{
+ CommandCommon vstCommands[DB_COMMAND_MAX];
+};
+
+void main(void)
+{
+
+}
diff --git a/Dashboard.Drawing.OpenGL/default.vert b/Dashboard.Drawing.OpenGL/default.vert
new file mode 100644
index 0000000..019c07d
--- /dev/null
+++ b/Dashboard.Drawing.OpenGL/default.vert
@@ -0,0 +1,24 @@
+#version 140
+
+in vec3 a_v3Position;
+in vec2 a_v2TexCoords;
+in vec3 a_v3CharCoords;
+in int a_iCmdIndex;
+
+out vec3 v_v3Position;
+out vec2 v_v2TexCoords;
+out vec3 v_v3CharCoords;
+flat out int v_iCmdIndex;
+
+uniform mat4 m4Transforms;
+
+void main(void)
+{
+ vec4 position = vec4(a_v3Position, 1) * m4Transforms;
+ gl_Position = position;
+ v_v3Position = position.xyz/position.w;
+
+ v_v2TexCoords = a_v2TexCoords;
+ v_v3CharCoords = a_v3CharCoords;
+ v_iCmdIndex = a_iCmdIndex;
+}
diff --git a/Dashboard.Drawing/DrawQueue.cs b/Dashboard.Drawing/DrawQueue.cs
index 9f04a20..6df93e2 100644
--- a/Dashboard.Drawing/DrawQueue.cs
+++ b/Dashboard.Drawing/DrawQueue.cs
@@ -49,7 +49,7 @@ namespace Dashboard.Drawing
_resources.Clear();
_commands.Clear();
_extensions.Clear();
- _commandStream.Capacity = 0;
+ _commandStream.SetLength(0);
}
public int RequireExtension(IDrawExtension extension)
diff --git a/Dashboard.sln b/Dashboard.sln
index cbcbcb5..aeb4144 100644
--- a/Dashboard.sln
+++ b/Dashboard.sln
@@ -17,6 +17,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dashboard.TestApplication",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dashboard.Common", "Dashboard.Common\Dashboard.Common.csproj", "{C77CDD2B-2482-45F9-B330-47A52F5F13C0}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dashboard.Drawing.OpenGL", "Dashboard.Drawing.OpenGL\Dashboard.Drawing.OpenGL.csproj", "{454198BA-CB95-41C5-A934-B1C8FDA35A6B}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -39,6 +41,10 @@ Global
{C77CDD2B-2482-45F9-B330-47A52F5F13C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C77CDD2B-2482-45F9-B330-47A52F5F13C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C77CDD2B-2482-45F9-B330-47A52F5F13C0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {454198BA-CB95-41C5-A934-B1C8FDA35A6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {454198BA-CB95-41C5-A934-B1C8FDA35A6B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {454198BA-CB95-41C5-A934-B1C8FDA35A6B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {454198BA-CB95-41C5-A934-B1C8FDA35A6B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/tests/Dashboard.TestApplication/Dashboard.TestApplication.csproj b/tests/Dashboard.TestApplication/Dashboard.TestApplication.csproj
index 80bbb87..e7a339c 100644
--- a/tests/Dashboard.TestApplication/Dashboard.TestApplication.csproj
+++ b/tests/Dashboard.TestApplication/Dashboard.TestApplication.csproj
@@ -8,7 +8,12 @@
+
+
+
+
+
diff --git a/tests/Dashboard.TestApplication/Program.cs b/tests/Dashboard.TestApplication/Program.cs
index 1295240..ab954ee 100644
--- a/tests/Dashboard.TestApplication/Program.cs
+++ b/tests/Dashboard.TestApplication/Program.cs
@@ -1,23 +1,131 @@
using Dashboard.Drawing;
-using System.Diagnostics;
using System.Drawing;
-using System.Numerics;
+using Dashboard.Drawing.OpenGL;
+using OpenTK.Graphics;
+using OpenTK.Platform;
+using OpenTK.Graphics.OpenGL;
+using OpenTK.Mathematics;
+using TK = OpenTK.Platform.Toolkit;
+using Vector3 = System.Numerics.Vector3;
+
+TK.Init(new ToolkitOptions()
+{
+ ApplicationName = "Dashboard Test Application",
+ Windows = new ToolkitOptions.WindowsOptions()
+ {
+ EnableVisualStyles = true,
+ IsDPIAware = true,
+ }
+});
+
+WindowHandle wnd = TK.Window.Create(new OpenGLGraphicsApiHints()
+{
+ Version = new Version(3, 2),
+ ForwardCompatibleFlag = true,
+ DebugFlag = true,
+ Profile = OpenGLProfile.Core,
+ sRGBFramebuffer = true,
+
+ SwapMethod = ContextSwapMethod.Undefined,
+
+ RedColorBits = 8,
+ GreenColorBits = 8,
+ BlueColorBits = 8,
+ AlphaColorBits = 8,
+
+ SupportTransparentFramebufferX11 = true,
+});
+TK.Window.SetTitle(wnd, "Dashboard Test Application");
+TK.Window.SetMinClientSize(wnd, 300, 200);
+TK.Window.SetClientSize(wnd, new Vector2i(320, 240));
+TK.Window.SetBorderStyle(wnd, WindowBorderStyle.ResizableBorder);
+
+OpenGLContextHandle context = TK.OpenGL.CreateFromWindow(wnd);
+
+TK.OpenGL.SetCurrentContext(context);
+TK.OpenGL.SetSwapInterval(1);
+
+GLLoader.LoadBindings(new Pal2BindingsContext(TK.OpenGL, context));
DrawQueue queue = new DrawQueue();
-
SolidBrush fg = new SolidBrush(Color.Black);
SolidBrush bg = new SolidBrush(Color.White);
+bool shouldExit = false;
-queue.Point(Vector3.Zero, 2f, fg);
-queue.Line(Vector3.Zero, Vector3.One * 2, 2f, bg);
-queue.Rect(Vector3.UnitX, 2 * Vector3.UnitX + Vector3.UnitY, fg, bg, 2f);
+GLEngine engine = new GLEngine();
+engine.Initialize();
-foreach (ICommandFrame frame in queue)
+GlContext dbGlContext = new GlContext(wnd, context);
+ContextExecutor executor = engine.GetExecutor(dbGlContext);
+
+EventQueue.EventRaised += (handle, type, eventArgs) =>
{
- Console.WriteLine("{0}", frame.Command.Name);
+ if (handle != wnd)
+ return;
- if (frame.HasParameters)
- Console.WriteLine("Param: {0}", frame.GetParameter());
+ switch (type)
+ {
+ case PlatformEventType.Close:
+ shouldExit = true;
+ break;
+ }
+};
+
+TK.Window.SetMode(wnd, WindowMode.Normal);
+
+while (!shouldExit)
+{
+ TK.Window.ProcessEvents(true);
+
+ TK.Window.GetFramebufferSize(wnd, out Vector2i framebufferSize);
+
+ queue.Clear();
+ queue.Point(Vector3.Zero, 2f, fg);
+ queue.Line(Vector3.Zero, Vector3.One * 2, 2f, bg);
+ queue.Rect(Vector3.UnitX, 2 * Vector3.UnitX + Vector3.UnitY, fg, bg, 2f);
+
+ GL.Viewport(0, 0, framebufferSize.X, framebufferSize.Y);
+ GL.ClearColor(0.3f, 0.3f, 0.3f, 1.0f);
+ GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
+
+ executor.Draw(queue);
+ executor.EndFrame();
+
+ TK.OpenGL.SwapBuffers(context);
}
-Debugger.Break();
+class GlContext : IGLContext
+{
+ public WindowHandle Window { get; }
+ public OpenGLContextHandle Context { get; }
+ public int ContextGroup { get; } = -1;
+
+ public Size FramebufferSize
+ {
+ get
+ {
+ TK.Window.GetFramebufferSize(Window, out Vector2i framebufferSize);
+ return new Size(framebufferSize.X, framebufferSize.Y);
+ }
+ }
+
+ public event Action? Disposed;
+
+ public GlContext(WindowHandle window, OpenGLContextHandle context)
+ {
+ Window = window;
+ Context = context;
+
+ OpenGLContextHandle? shared = TK.OpenGL.GetSharedContext(context);
+ if (shared != null)
+ {
+ ContextGroup = _contexts.IndexOf(shared);
+ }
+ else
+ {
+ _contexts.Add(context);
+ }
+ }
+
+ private static readonly List _contexts = new List();
+}