diff --git a/Dashboard.Drawing/Dashboard.Drawing.csproj b/Dashboard.Drawing/Dashboard.Drawing.csproj
new file mode 100644
index 0000000..88b90bb
--- /dev/null
+++ b/Dashboard.Drawing/Dashboard.Drawing.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net8.0
+ disable
+ enable
+
+
+
diff --git a/Dashboard.Drawing/DbBaseCommands.cs b/Dashboard.Drawing/DbBaseCommands.cs
new file mode 100644
index 0000000..1d6d84f
--- /dev/null
+++ b/Dashboard.Drawing/DbBaseCommands.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Dashboard.Drawing
+{
+ public class DbBaseCommands : DrawExtension
+ {
+ public DrawCommand DrawPoint { get; }
+
+ private DbBaseCommands() : base("DB_base",
+ new[]
+ {
+ BrushExtension.Instance,
+ })
+ {
+ AddCommand(DrawPoint = new DrawCommand("Point", this, PointCommandArgs.CommandSize));
+ }
+
+ public static readonly DbBaseCommands Instance = new DbBaseCommands();
+ }
+
+ public struct PointCommandArgs : IParameterSerializer
+ {
+ public Vector3 Position { get; private set; }
+ public float Size { get; private set; }
+ public IBrush? Brush { get; private set; }
+
+ public PointCommandArgs(Vector3 position, float size, IBrush brush)
+ {
+ Position = position;
+ Brush = brush;
+ Size = size;
+ }
+
+ public int Serialize(DrawQueue queue, Span bytes)
+ {
+ if (bytes.Length < CommandSize)
+ return CommandSize;
+
+ Span value = stackalloc Value[]
+ {
+ new Value(Position, Size, queue.RequireResource(Brush!))
+ };
+
+ MemoryMarshal.AsBytes(value).CopyTo(bytes);
+ return CommandSize;
+ }
+
+ [MemberNotNull(nameof(Brush))]
+ public void Deserialize(DrawQueue queue, ReadOnlySpan bytes)
+ {
+ if (bytes.Length < CommandSize)
+ throw new Exception("Not enough bytes");
+
+ Value value = MemoryMarshal.AsRef(bytes);
+
+ Position = value.Position;
+ Size = value.Size;
+ Brush = (IBrush)queue.Resources[value.BrushIndex];
+ }
+
+ private record struct Value(Vector3 Position, float Size, int BrushIndex);
+
+ public static readonly int CommandSize = Unsafe.SizeOf();
+ }
+}
diff --git a/Dashboard.Drawing/DrawCommand.cs b/Dashboard.Drawing/DrawCommand.cs
new file mode 100644
index 0000000..683ae5a
--- /dev/null
+++ b/Dashboard.Drawing/DrawCommand.cs
@@ -0,0 +1,88 @@
+using System;
+
+namespace Dashboard.Drawing
+{
+ public interface IDrawCommand
+ {
+ ///
+ /// Name of the command.
+ ///
+ string Name { get; }
+
+ ///
+ /// The draw extension that defines this command.
+ ///
+ IDrawExtension Extension { get; }
+
+ ///
+ /// The length of the command data segment, in bytes.
+ ///
+ ///
+ /// Must be 0 for simple commands. For commands that are variadic, the
+ /// value must be less than 0. Any other positive value, otherwise.
+ ///
+ int Length { get; }
+
+ ///
+ /// Get the parameters object for this command.
+ ///
+ /// The parameter array.
+ /// The parameters object.
+ object? GetParams(DrawQueue queue, ReadOnlySpan param);
+ }
+
+ public interface IDrawCommand : IDrawCommand
+ {
+ ///
+ /// Get the parameters object for this command.
+ ///
+ /// The parameter array.
+ /// The parameters object.
+ new T? GetParams(DrawQueue queue, ReadOnlySpan param);
+ }
+
+ public sealed class DrawCommand : IDrawCommand
+ {
+ public string Name { get; }
+ public IDrawExtension Extension { get; }
+ public int Length { get; } = 0;
+
+ public DrawCommand(string name, IDrawExtension extension)
+ {
+ Name = name;
+ Extension = extension;
+ }
+
+ public object? GetParams(DrawQueue queue, ReadOnlySpan param)
+ {
+ return null;
+ }
+ }
+
+ public sealed class DrawCommand : IDrawCommand
+ where T : IParameterSerializer, new()
+ {
+ public string Name { get; }
+ public IDrawExtension Extension { get; }
+ public int Length { get; }
+
+ public DrawCommand(string name, IDrawExtension extension, int length)
+ {
+ Name = name;
+ Extension = extension;
+ Length = length;
+ }
+
+ public T? GetParams(DrawQueue queue, ReadOnlySpan param)
+ {
+ T t = new T();
+ t.Deserialize(queue, param);
+ return t;
+ }
+
+ object? IDrawCommand.GetParams(DrawQueue queue, ReadOnlySpan param)
+ {
+ return GetParams(queue, param);
+ }
+ }
+}
diff --git a/Dashboard.Drawing/DrawExtension.cs b/Dashboard.Drawing/DrawExtension.cs
new file mode 100644
index 0000000..fd3fe7e
--- /dev/null
+++ b/Dashboard.Drawing/DrawExtension.cs
@@ -0,0 +1,72 @@
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Numerics;
+
+namespace Dashboard.Drawing
+{
+ ///
+ /// Interface for all drawing extensions.
+ ///
+ public interface IDrawExtension
+ {
+ ///
+ /// Name of this extension.
+ ///
+ public string Name { get; }
+
+ public IReadOnlyList Requires { get; }
+
+ ///
+ /// The list of commands this extension defines, if any.
+ ///
+ public IReadOnlyList Commands { get; }
+ }
+
+ ///
+ /// A simple draw extension.
+ ///
+ public class DrawExtension : IDrawExtension
+ {
+ private readonly List _drawCommands = new List();
+
+ public string Name { get; }
+
+ public IReadOnlyList Commands { get; }
+
+ public IReadOnlyList Requires { get; }
+
+ public DrawExtension(string name, IEnumerable? requires = null)
+ {
+ Name = name;
+ Commands = _drawCommands.AsReadOnly();
+ Requires = (requires ?? Enumerable.Empty()).ToImmutableList();
+ }
+
+ protected void AddCommand(IDrawCommand command)
+ {
+ _drawCommands.Add(command);
+ }
+ }
+
+ public static class DrawExtensionClass
+ {
+ ///
+ /// Get the draw controller for the given queue.
+ ///
+ /// The extension instance.
+ /// The draw queue.
+ /// The draw controller for this queue.
+ public static IDrawController GetController(this IDrawExtension extension, DrawQueue queue)
+ {
+ return queue.GetController(extension);
+ }
+
+ public static void Point(this DrawQueue queue, Vector3 position, float size, IBrush brush)
+ {
+ var controller = queue.GetController(DbBaseCommands.Instance);
+ controller.EnsureSize(position);
+ controller.Write(DbBaseCommands.Instance.DrawPoint, new PointCommandArgs(position, size, brush));
+ }
+ }
+}
diff --git a/Dashboard.Drawing/DrawQueue.cs b/Dashboard.Drawing/DrawQueue.cs
new file mode 100644
index 0000000..53b9f8f
--- /dev/null
+++ b/Dashboard.Drawing/DrawQueue.cs
@@ -0,0 +1,263 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Numerics;
+
+namespace Dashboard.Drawing
+{
+ public class DrawQueue : IDisposable
+ {
+ private readonly HashList _extensions = new HashList();
+ private readonly HashList _commands = new HashList();
+ private readonly HashList _resources = new HashList();
+ private readonly DrawController _controller;
+ private readonly MemoryStream _commandStream = new MemoryStream();
+
+ ///
+ /// Size of the image in points.
+ ///
+ public Vector3 Size { get; private set; } = Vector3.Zero;
+
+ ///
+ /// The extensions required to draw the image.
+ ///
+ public IReadOnlyList Extensions => _extensions;
+
+ ///
+ /// The resources used by this draw queue.
+ ///
+ public IReadOnlyList Resources => _resources;
+
+ ///
+ /// The list of commands used by the extension.
+ ///
+ public IReadOnlyList Command => _commands;
+
+ public DrawQueue()
+ {
+ _controller = new DrawController(this);
+ }
+
+ ///
+ /// Clear the queue.
+ ///
+ public void Clear()
+ {
+ _resources.Clear();
+ _commands.Clear();
+ _extensions.Clear();
+ _commandStream.Capacity = 0;
+ }
+
+ public int RequireExtension(IDrawExtension extension)
+ {
+ foreach (IDrawExtension super in extension.Requires)
+ RequireExtension(super);
+
+ return _extensions.Intern(extension);
+ }
+
+ public int RequireResource(IDrawResource resource)
+ {
+ RequireExtension(resource.Kind);
+ return _resources.Intern(resource);
+ }
+
+ internal IDrawController GetController(IDrawExtension extension)
+ {
+ _extensions.Intern(extension);
+ return _controller;
+ }
+
+ private void Write(IDrawCommand command)
+ {
+ if (command.Length > 0)
+ throw new InvalidOperationException("This command has a finite length argument.");
+
+ int cmdIndex = _commands.Intern(command);
+
+ Span cmd = stackalloc byte[6];
+ int sz;
+
+ if (command.Length == 0)
+ {
+ // Write a fixed command.
+ sz = ToVlq(cmdIndex, cmd);
+ }
+ else
+ {
+ // Write a variadic with zero length.
+ sz = ToVlq(cmdIndex, cmd);
+ cmd[sz++] = 0;
+ }
+
+ _commandStream.Write(cmd[..sz]);
+ }
+
+ private void Write(IDrawCommand command, ReadOnlySpan param)
+ {
+ if (command.Length < 0)
+ {
+ Span cmd = stackalloc byte[10];
+ int cmdIndex = _commands.Intern(command);
+ int sz = ToVlq(cmdIndex, cmd);
+ sz += ToVlq(param.Length, cmd[sz..]);
+ _commandStream.Write(cmd[..sz]);
+ }
+ else
+ {
+ if (command.Length != param.Length)
+ throw new ArgumentOutOfRangeException(nameof(param.Length), "Length of the parameter does not match the command.");
+
+ Span cmd = stackalloc byte[5];
+ int cmdIndex = _commands.Intern(command);
+ int sz = ToVlq(cmdIndex, cmd);
+
+ _commandStream.Write(cmd[..sz]);
+ _commandStream.Write(param);
+ }
+ }
+
+ private static int ToVlq(int value, Span bytes)
+ {
+ if (value < 0)
+ throw new ArgumentOutOfRangeException(nameof(value), "Must be a positive integer.");
+ else if (bytes.Length < 5)
+ throw new ArgumentOutOfRangeException(nameof(bytes), "Must at least be five bytes long.");
+
+ int i;
+
+ for (i = 0; i < 5 && value != 0; i++, value >>= 7)
+ {
+ if (i > 0)
+ bytes[i - 1] |= 1 << 7;
+
+ bytes[i] = (byte)(value & 0x7F);
+ }
+
+ return i;
+ }
+
+ private static int FromVlq(ReadOnlySpan bytes)
+ {
+ int value = 0;
+
+ for (int i = 0; i < bytes.Length; i++)
+ {
+ byte b = bytes[i];
+
+ value = (value << 7) | b;
+
+ if ((b & (1 << 7)) == 0)
+ break;
+ }
+
+ return value;
+ }
+
+ public void Dispose()
+ {
+ throw new NotImplementedException();
+ }
+
+ private class DrawController(DrawQueue Queue) : IDrawController
+ {
+ public void EnsureSize(Vector3 size)
+ {
+ Queue.Size = Vector3.Max(Queue.Size, size);
+ }
+
+ public int Require(IDrawExtension extension)
+ {
+ return Queue.RequireExtension(extension);
+ }
+
+ public int GetResourceIndex(IDrawResource resource)
+ {
+ return Queue._resources.Intern(resource);
+ }
+
+ public void Write(IDrawCommand command)
+ {
+ Queue.Write(command);
+ }
+
+ public void Write(IDrawCommand command, ReadOnlySpan bytes)
+ {
+ Queue.Write(command, bytes);
+ }
+
+ public void Write(IDrawCommand command, T param) where T : IParameterSerializer
+ {
+ int length = param.Serialize(Queue, Span.Empty);
+ Span bytes = stackalloc byte[length];
+
+ param.Serialize(Queue, bytes);
+ Write(command, bytes);
+ }
+ }
+
+ private class HashList : IReadOnlyList
+ where T : notnull
+ {
+ private readonly List _list = new List();
+ private readonly Dictionary _map = new Dictionary();
+
+ public T this[int index] => _list[index];
+
+ public int Count => _list.Count;
+
+ public int Intern(T value)
+ {
+ if (_map.TryGetValue(value, out int index))
+ return index;
+
+ index = Count;
+
+ _list.Add(value);
+ _map.Add(value, index);
+
+ return index;
+ }
+
+ public void Clear()
+ {
+ _list.Clear();
+ _map.Clear();
+ }
+
+ public IEnumerator GetEnumerator() => _list.GetEnumerator();
+ IEnumerator IEnumerable.GetEnumerator() => _list.GetEnumerator();
+ }
+ }
+
+ public interface IDrawController
+ {
+ ///
+ /// Ensures that the canvas is at least a certain size.
+ ///
+ /// The minimum size.
+ void EnsureSize(Vector3 size);
+
+ ///
+ /// Write into the command stream.
+ ///
+ /// The command to write.
+ void Write(IDrawCommand command);
+
+ ///
+ /// Write into the command stream.
+ ///
+ /// The command to write.
+ /// Any data associated with the command.
+ void Write(IDrawCommand command, ReadOnlySpan param);
+
+ ///
+ /// Write into the command stream.
+ ///
+ /// The command to write.
+ /// Any data associated with the command.
+ void Write(IDrawCommand command, T param) where T : IParameterSerializer;
+ }
+}
diff --git a/Dashboard.Drawing/IDrawResource.cs b/Dashboard.Drawing/IDrawResource.cs
new file mode 100644
index 0000000..2a57ced
--- /dev/null
+++ b/Dashboard.Drawing/IDrawResource.cs
@@ -0,0 +1,13 @@
+namespace Dashboard.Drawing
+{
+ ///
+ /// Interface for draw resources.
+ ///
+ public interface IDrawResource
+ {
+ ///
+ /// The extension for this kind of resource.
+ ///
+ IDrawExtension Kind { get; }
+ }
+}
diff --git a/Dashboard.Drawing/IParameterSerializer.cs b/Dashboard.Drawing/IParameterSerializer.cs
new file mode 100644
index 0000000..5ae0bf6
--- /dev/null
+++ b/Dashboard.Drawing/IParameterSerializer.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Dashboard.Drawing
+{
+ public interface IParameterSerializer
+ {
+ int Serialize(DrawQueue queue, Span bytes);
+ void Deserialize(DrawQueue queue, ReadOnlySpan bytes);
+ }
+}
diff --git a/Dashboard.Drawing/SolidBrush.cs b/Dashboard.Drawing/SolidBrush.cs
new file mode 100644
index 0000000..1273653
--- /dev/null
+++ b/Dashboard.Drawing/SolidBrush.cs
@@ -0,0 +1,33 @@
+using System.Drawing;
+
+namespace Dashboard.Drawing
+{
+ public class BrushExtension : DrawExtension
+ {
+ private BrushExtension() : base("DB_Brush") { }
+
+ public static readonly BrushExtension Instance = new BrushExtension();
+ }
+
+ public interface IBrush : IDrawResource
+ {
+ }
+
+ public readonly struct SolidBrush : IBrush
+ {
+ public IDrawExtension Kind { get; } = SolidBrushExtension.Instance;
+ public Color Color { get; }
+
+ public SolidBrush(Color color)
+ {
+ Color = color;
+ }
+ }
+
+ public class SolidBrushExtension : DrawExtension
+ {
+ private SolidBrushExtension() : base("DB_Brush_solid", new[] { BrushExtension.Instance }) { }
+
+ public static readonly SolidBrushExtension Instance = new SolidBrushExtension();
+ }
+}
diff --git a/Dashboard.sln b/Dashboard.sln
new file mode 100644
index 0000000..5b6b711
--- /dev/null
+++ b/Dashboard.sln
@@ -0,0 +1,43 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dashboard", "Dashboard\Dashboard.csproj", "{49A62F46-AC1C-4240-8615-020D4FBBF964}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dashboard.Drawing", "Dashboard.Drawing\Dashboard.Drawing.csproj", "{1BDFEF50-C907-42C8-B63B-E4F6F585CFB5}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9D6CCC74-4DF3-47CB-B9B2-6BB75DF2BC40}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dashboard.TestApplication", "tests\Dashboard.TestApplication\Dashboard.TestApplication.csproj", "{7C90B90B-DF31-439B-9080-CD805383B014}"
+ ProjectSection(ProjectDependencies) = postProject
+ {1BDFEF50-C907-42C8-B63B-E4F6F585CFB5} = {1BDFEF50-C907-42C8-B63B-E4F6F585CFB5}
+ {49A62F46-AC1C-4240-8615-020D4FBBF964} = {49A62F46-AC1C-4240-8615-020D4FBBF964}
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {49A62F46-AC1C-4240-8615-020D4FBBF964}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {49A62F46-AC1C-4240-8615-020D4FBBF964}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {49A62F46-AC1C-4240-8615-020D4FBBF964}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {49A62F46-AC1C-4240-8615-020D4FBBF964}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1BDFEF50-C907-42C8-B63B-E4F6F585CFB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1BDFEF50-C907-42C8-B63B-E4F6F585CFB5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1BDFEF50-C907-42C8-B63B-E4F6F585CFB5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1BDFEF50-C907-42C8-B63B-E4F6F585CFB5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7C90B90B-DF31-439B-9080-CD805383B014}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7C90B90B-DF31-439B-9080-CD805383B014}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7C90B90B-DF31-439B-9080-CD805383B014}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7C90B90B-DF31-439B-9080-CD805383B014}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {7C90B90B-DF31-439B-9080-CD805383B014} = {9D6CCC74-4DF3-47CB-B9B2-6BB75DF2BC40}
+ EndGlobalSection
+EndGlobal
diff --git a/Dashboard/Class1.cs b/Dashboard/Class1.cs
new file mode 100644
index 0000000..70484e5
--- /dev/null
+++ b/Dashboard/Class1.cs
@@ -0,0 +1,6 @@
+namespace Dashboard;
+
+public class Class1
+{
+
+}
diff --git a/Dashboard/Dashboard.csproj b/Dashboard/Dashboard.csproj
new file mode 100644
index 0000000..88b90bb
--- /dev/null
+++ b/Dashboard/Dashboard.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net8.0
+ disable
+ enable
+
+
+
diff --git a/tests/Dashboard.TestApplication/Dashboard.TestApplication.csproj b/tests/Dashboard.TestApplication/Dashboard.TestApplication.csproj
new file mode 100644
index 0000000..80bbb87
--- /dev/null
+++ b/tests/Dashboard.TestApplication/Dashboard.TestApplication.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/tests/Dashboard.TestApplication/Program.cs b/tests/Dashboard.TestApplication/Program.cs
new file mode 100644
index 0000000..6dd4b01
--- /dev/null
+++ b/tests/Dashboard.TestApplication/Program.cs
@@ -0,0 +1,10 @@
+using Dashboard.Drawing;
+using System.Diagnostics;
+using System.Drawing;
+using System.Numerics;
+
+DrawQueue queue = new DrawQueue();
+
+queue.Point(Vector3.Zero, 2, new SolidBrush(Color.White));
+
+Debugger.Break();