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();