diff --git a/Dashboard.Drawing.OpenGL/ContextExecutor.cs b/Dashboard.Drawing.OpenGL/ContextExecutor.cs
index 510a8aa..b744d70 100644
--- a/Dashboard.Drawing.OpenGL/ContextExecutor.cs
+++ b/Dashboard.Drawing.OpenGL/ContextExecutor.cs
@@ -51,6 +51,7 @@ namespace Dashboard.Drawing.OpenGL
ResourcePool.IncrementReference();
AddExecutor(new BaseCommandExecutor());
+ AddExecutor(new TextCommandExecutor());
}
~ContextExecutor()
diff --git a/Dashboard.Drawing.OpenGL/Dashboard.Drawing.OpenGL.csproj b/Dashboard.Drawing.OpenGL/Dashboard.Drawing.OpenGL.csproj
index 4c9837e..05c6aa9 100644
--- a/Dashboard.Drawing.OpenGL/Dashboard.Drawing.OpenGL.csproj
+++ b/Dashboard.Drawing.OpenGL/Dashboard.Drawing.OpenGL.csproj
@@ -8,6 +8,7 @@
+
@@ -18,6 +19,8 @@
+
+
diff --git a/Dashboard.Drawing.OpenGL/Executors/TextCommandExecutor.cs b/Dashboard.Drawing.OpenGL/Executors/TextCommandExecutor.cs
new file mode 100644
index 0000000..8b0da71
--- /dev/null
+++ b/Dashboard.Drawing.OpenGL/Executors/TextCommandExecutor.cs
@@ -0,0 +1,243 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+using BlurgText;
+using Dashboard.Drawing.OpenGL.Text;
+using OpenTK.Graphics.OpenGL;
+using OpenTK.Mathematics;
+
+namespace Dashboard.Drawing.OpenGL.Executors
+{
+ public class TextCommandExecutor : ICommandExecutor, IInitializer
+ {
+ public IEnumerable Extensions { get; } = new[] { "DB_Text" };
+ public IContextExecutor Executor { get; private set; }
+ private BlurgEngine Engine => Executor.ResourcePool.GetResourceManager();
+ public bool IsInitialized { get; private set; }
+
+ private DrawCallRecorder _recorder;
+ private int _program = 0;
+ private int _transformsLocation = -1;
+ private int _atlasLocation = -1;
+ private int _borderWidthLocation = -1;
+ private int _borderColorLocation = -1;
+ private int _fillColorLocation = -1;
+
+ public TextCommandExecutor()
+ {
+ Executor = null!;
+ _recorder = new DrawCallRecorder(this);
+ }
+
+ public void Initialize()
+ {
+ if (IsInitialized)
+ return;
+ IsInitialized = true;
+
+ Assembly self = typeof(TextCommandExecutor).Assembly;
+
+ using Stream vsource = self.GetManifestResourceStream("Dashboard.Drawing.OpenGL.Executors.text.vert")!;
+ using Stream fsource = self.GetManifestResourceStream("Dashboard.Drawing.OpenGL.Executors.text.frag")!;
+ int vs = ShaderUtil.CompileShader(ShaderType.VertexShader, vsource);
+ int fs = ShaderUtil.CompileShader(ShaderType.FragmentShader, fsource);
+ _program = ShaderUtil.LinkProgram(vs, fs, new []
+ {
+ "a_v3Position",
+ "a_v2TexCoords",
+ });
+ GL.DeleteShader(vs);
+ GL.DeleteShader(fs);
+
+ _transformsLocation = GL.GetUniformLocation(_program, "m4Transforms");
+ _atlasLocation = GL.GetUniformLocation(_program, "txAtlas");
+ _borderWidthLocation = GL.GetUniformLocation(_program, "fBorderWidth");
+ _borderColorLocation = GL.GetUniformLocation(_program, "v4BorderColor");
+ _fillColorLocation = GL.GetUniformLocation(_program, "v4FillColor");
+
+ _recorder.Initialize();
+ }
+
+ public void SetContextExecutor(IContextExecutor executor)
+ {
+ Executor = executor;
+ }
+
+ public void BeginFrame()
+ {
+ }
+
+ public void BeginDraw()
+ {
+ _recorder.Clear();
+ }
+
+ public void EndDraw()
+ {
+ GL.UseProgram(_program);
+ GL.Enable(EnableCap.Blend);
+ GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
+ _recorder.Execute();
+ GL.Disable(EnableCap.Blend);
+ }
+
+ public void EndFrame()
+ {
+ }
+
+ public void ProcessCommand(ICommandFrame frame)
+ {
+ switch (frame.Command.Name)
+ {
+ case "Text":
+ DrawText(frame);
+ break;
+ }
+ }
+
+ private void DrawText(ICommandFrame frame)
+ {
+ TextCommandArgs args = frame.GetParameter();
+ DbBlurgFont font = (DbBlurgFont)args.Font;
+
+ BlurgColor color;
+ switch (args.TextBrush)
+ {
+ case SolidBrush solid:
+ color = new BlurgColor()
+ {
+ R = solid.Color.R,
+ G = solid.Color.G,
+ B = solid.Color.B,
+ A = solid.Color.A,
+ };
+ break;
+ default:
+ color = new BlurgColor() { R = 255, G = 0, B = 255, A = 255 };
+ break;
+ }
+
+ BlurgResult? result = Engine.Blurg.BuildString(font.Font, font.Size, color, args.Text);
+
+ if (result == null)
+ return;
+
+ Vector3 position = new Vector3(args.Position.X, args.Position.Y, args.Position.Z);
+ ExecuteBlurgResult(result, position);
+
+ result.Dispose();
+ }
+
+ private void ExecuteBlurgResult(BlurgResult result, Vector3 position)
+ {
+ Matrix4 transforms = Executor.TransformStack.Top;
+
+ for (int i = 0; i < result.Count; i++)
+ {
+ BlurgRect rect = result[i];
+
+ int texture = (int)rect.UserData;
+ Vector4 color = new Vector4(rect.Color.R / 255f, rect.Color.G / 255f, rect.Color.B / 255f,
+ rect.Color.A / 255f);
+
+ if (i == 0)
+ {
+ _recorder.Begin(PrimitiveType.Triangles, new Call()
+ {
+ Texture = texture,
+ FillColor = color,
+ Transforms = transforms,
+ });
+ }
+ else if (
+ _recorder.CurrentCall.Texture != texture ||
+ _recorder.CurrentCall.FillColor != color)
+ {
+ _recorder.End();
+ Call call = new Call()
+ {
+ Texture = texture,
+ FillColor = color,
+ Transforms = transforms,
+ };
+ _recorder.Begin(PrimitiveType.Triangles, call);
+ }
+
+ Vector3 p00 = new Vector3(rect.X, rect.Y, 0) + position;
+ Vector3 p10 = p00 + new Vector3(rect.Width, 0, 0);
+ Vector3 p11 = p00 + new Vector3(rect.Width, rect.Height, 0);
+ Vector3 p01 = p00 + new Vector3(0, rect.Height, 0);
+
+ Vector2 uv00 = new Vector2(rect.U0, rect.V0);
+ Vector2 uv10 = new Vector2(rect.U1, rect.V0);
+ Vector2 uv11 = new Vector2(rect.U1, rect.V1);
+ Vector2 uv01 = new Vector2(rect.U0, rect.V1);
+
+ _recorder.Vertex(p00, uv00);
+ _recorder.Vertex(p10, uv10);
+ _recorder.Vertex(p11, uv11);
+
+ _recorder.Vertex(p00, uv00);
+ _recorder.Vertex(p11, uv11);
+ _recorder.Vertex(p01, uv01);
+ }
+ _recorder.End();
+ }
+
+ private struct Call
+ {
+ public Matrix4 Transforms = Matrix4.Identity;
+ public int Texture = 0;
+ public float BorderWidth = 0f;
+ public Vector4 FillColor = Vector4.One;
+ public Vector4 BorderColor = new Vector4(0,0,0,1);
+
+ public Call()
+ {
+ }
+ }
+
+ [StructLayout(LayoutKind.Explicit, Size = 8 * sizeof(float))]
+ private struct Vertex
+ {
+ [FieldOffset(0)]
+ public Vector3 Position;
+ [FieldOffset(4 * sizeof(float))]
+ public Vector2 TexCoords;
+ }
+
+ private class DrawCallRecorder : DrawCallRecorder
+ {
+ private TextCommandExecutor Executor { get; }
+
+ public DrawCallRecorder(TextCommandExecutor executor)
+ {
+ Executor = executor;
+ }
+
+ public void Vertex(Vector3 position, Vector2 texCoords)
+ {
+ Vertex(new Vertex(){Position = position, TexCoords = texCoords});
+ }
+
+ protected override void PrepareCall(in Call call)
+ {
+ Matrix4 transforms = call.Transforms;
+ GL.UniformMatrix4f(Executor._transformsLocation, 1, true, ref transforms);
+ GL.Uniform1f(Executor._borderWidthLocation, call.BorderWidth);
+ GL.Uniform4f(Executor._borderColorLocation, 1, in call.BorderColor);
+ GL.Uniform4f(Executor._fillColorLocation, 1, in call.FillColor);
+ GL.Uniform1i(Executor._atlasLocation, 0);
+ GL.ActiveTexture(TextureUnit.Texture0);
+ GL.BindTexture(TextureTarget.Texture2d, call.Texture);
+ }
+
+ protected override void SetVertexFormat()
+ {
+ GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, VertexSize, 0);
+ GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, VertexSize, 4*sizeof(float));
+ GL.EnableVertexAttribArray(0);
+ GL.EnableVertexAttribArray(1);
+ }
+ }
+ }
+}
diff --git a/Dashboard.Drawing.OpenGL/Executors/text.frag b/Dashboard.Drawing.OpenGL/Executors/text.frag
new file mode 100644
index 0000000..9914b22
--- /dev/null
+++ b/Dashboard.Drawing.OpenGL/Executors/text.frag
@@ -0,0 +1,21 @@
+#version 140
+
+in vec3 v_v3Position;
+in vec2 v_v2TexCoords;
+
+out vec4 f_Color;
+
+uniform sampler2D txAtlas;
+uniform float fBorderWidth;
+uniform vec4 v4BorderColor;
+uniform vec4 v4FillColor;
+
+void main() {
+ // For now just honor the fill color
+
+ vec4 color = texture(txAtlas, v_v2TexCoords) * v4FillColor;
+
+ if (color.a <= 0.1)
+ discard;
+ f_Color = color;
+}
diff --git a/Dashboard.Drawing.OpenGL/Executors/text.vert b/Dashboard.Drawing.OpenGL/Executors/text.vert
new file mode 100644
index 0000000..27ae926
--- /dev/null
+++ b/Dashboard.Drawing.OpenGL/Executors/text.vert
@@ -0,0 +1,18 @@
+#version 140
+
+in vec3 a_v3Position;
+in vec2 a_v2TexCoords;
+
+out vec3 v_v3Position;
+out vec2 v_v2TexCoords;
+
+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;
+}
diff --git a/Dashboard.Drawing.OpenGL/Text/BlurgEngine.cs b/Dashboard.Drawing.OpenGL/Text/BlurgEngine.cs
new file mode 100644
index 0000000..783261c
--- /dev/null
+++ b/Dashboard.Drawing.OpenGL/Text/BlurgEngine.cs
@@ -0,0 +1,126 @@
+using BlurgText;
+using OpenTK.Graphics.OpenGL;
+using OPENGL = OpenTK.Graphics.OpenGL;
+
+namespace Dashboard.Drawing.OpenGL.Text
+{
+ public class BlurgEngine : IResourceManager, IGLDisposable
+ {
+ public string Name { get; } = "BlurgEngine";
+ public Blurg Blurg { get; }
+ public bool SystemFontsEnabled { get; }
+
+ private readonly List _textures = new List();
+
+ public BlurgEngine()
+ {
+ Blurg = new Blurg(AllocateTexture, UpdateTexture);
+
+ SystemFontsEnabled = Blurg.EnableSystemFonts();
+ }
+
+ ~BlurgEngine()
+ {
+ Dispose(false, true);
+ }
+
+ public DbBlurgFont AddFont(string path)
+ {
+ BlurgFont? font = Blurg.AddFontFile(path) ?? throw new Exception("Failed to load the font file.");
+ return new DbBlurgFont(font, 12f);
+ }
+
+ public DbBlurgFont AddFont(Stream stream)
+ {
+ string path;
+ Stream dest;
+ for (int i = 0;; i++)
+ {
+ path = Path.GetTempFileName();
+ try
+ {
+ dest = File.Open(path, FileMode.CreateNew, FileAccess.Write, FileShare.None);
+ }
+ catch (IOException ex)
+ {
+ if (i < 3)
+ continue;
+ else
+ throw new Exception("Could not open a temporary file for writing the font.", ex);
+ }
+
+ break;
+ }
+
+ stream.CopyTo(dest);
+ dest.Dispose();
+
+ DbBlurgFont? font = AddFont(path);
+ File.Delete(path);
+ return font;
+ }
+
+ public DbBlurgFont? QueryFont(string family, FontWeight weight, FontSlant slant, FontStretch stretch)
+ {
+ // Ignore the stretch argument.
+ bool italic = slant != FontSlant.Normal;
+ BlurgFont? font = Blurg.QueryFont(family, new BlurgText.FontWeight((int)weight), italic);
+
+ if (font != null)
+ return new DbBlurgFont(font, 12f);
+ return null;
+ }
+
+ private void UpdateTexture(IntPtr texture, IntPtr buffer, int x, int y, int width, int height)
+ {
+ GL.BindTexture(TextureTarget.Texture2d, (int)texture);
+ GL.TexSubImage2D(TextureTarget.Texture2d, 0, x, y, width, height, OPENGL.PixelFormat.Rgba, PixelType.UnsignedByte, buffer);
+ // GL.TexSubImage2D(TextureTarget.Texture2d, 0, x, y, width, height, OPENGL.PixelFormat.Red, PixelType.Byte, buffer);
+ }
+
+ private IntPtr AllocateTexture(int width, int height)
+ {
+ int texture = GL.GenTexture();
+
+ GL.BindTexture(TextureTarget.Texture2d, texture);
+ GL.TexImage2D(TextureTarget.Texture2d, 0, InternalFormat.Rgba, width, height, 0, OPENGL.PixelFormat.Rgba, PixelType.UnsignedByte, IntPtr.Zero);
+ // GL.TexImage2D(TextureTarget.Texture2d, 0, InternalFormat.R8, width, height, 0, OPENGL.PixelFormat.Red, PixelType.Byte, IntPtr.Zero);
+
+ GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
+ GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
+ // GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureSwizzleR, (int)TextureSwizzle.One);
+ // GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureSwizzleG, (int)TextureSwizzle.One);
+ // GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureSwizzleB, (int)TextureSwizzle.One);
+ // GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureSwizzleA, (int)TextureSwizzle.Red);
+
+ _textures.Add(texture);
+
+ return texture;
+ }
+
+ private bool _isDisposed = false;
+
+ private void Dispose(bool disposing, bool safeExit)
+ {
+ if (_isDisposed)
+ return;
+ _isDisposed = true;
+
+ if (disposing)
+ {
+ Blurg.Dispose();
+ GC.SuppressFinalize(this);
+ }
+
+ if (safeExit)
+ {
+ foreach (int texture in _textures)
+ ContextCollector.Global.DeleteTexture(texture);
+ }
+ }
+
+ public void Dispose() => Dispose(true, true);
+
+ public void Dispose(bool safeExit) => Dispose(true, safeExit);
+ }
+}
diff --git a/Dashboard.Drawing.OpenGL/Text/BlurgFontExtension.cs b/Dashboard.Drawing.OpenGL/Text/BlurgFontExtension.cs
new file mode 100644
index 0000000..8ca8e6d
--- /dev/null
+++ b/Dashboard.Drawing.OpenGL/Text/BlurgFontExtension.cs
@@ -0,0 +1,13 @@
+namespace Dashboard.Drawing.OpenGL.Text
+{
+ public class BlurgFontExtension : IDrawExtension
+ {
+ public string Name { get; } = "BLURG_Font";
+ public IReadOnlyList Requires { get; } = new [] { FontExtension.Instance };
+ public IReadOnlyList Commands { get; } = new IDrawCommand[] { };
+
+ private BlurgFontExtension() {}
+
+ public static readonly BlurgFontExtension Instance = new BlurgFontExtension();
+ }
+}
diff --git a/Dashboard.Drawing.OpenGL/Text/DbBlurgFont.cs b/Dashboard.Drawing.OpenGL/Text/DbBlurgFont.cs
new file mode 100644
index 0000000..c0a5696
--- /dev/null
+++ b/Dashboard.Drawing.OpenGL/Text/DbBlurgFont.cs
@@ -0,0 +1,26 @@
+using BlurgText;
+
+namespace Dashboard.Drawing.OpenGL.Text
+{
+ public class DbBlurgFont : IFont
+ {
+ public IDrawExtension Kind { get; } = BlurgFontExtension.Instance;
+ public BlurgFont Font { get; }
+ public float Size { get; }
+ public string Family => Font.FamilyName;
+ public FontWeight Weight => (FontWeight)Font.Weight.Value;
+ public FontSlant Slant => Font.Italic ? FontSlant.Italic : FontSlant.Normal;
+ public FontStretch Stretch => FontStretch.Normal;
+
+ public DbBlurgFont(BlurgFont font, float size)
+ {
+ Font = font;
+ Size = size;
+ }
+
+ public DbBlurgFont WithSize(float size)
+ {
+ return new DbBlurgFont(Font, size);
+ }
+ }
+}
diff --git a/tests/Dashboard.TestApplication/Program.cs b/tests/Dashboard.TestApplication/Program.cs
index f8bbcd3..2567345 100644
--- a/tests/Dashboard.TestApplication/Program.cs
+++ b/tests/Dashboard.TestApplication/Program.cs
@@ -1,6 +1,8 @@
using Dashboard.Drawing;
using System.Drawing;
+using Dashboard;
using Dashboard.Drawing.OpenGL;
+using Dashboard.Drawing.OpenGL.Text;
using OpenTK.Graphics;
using OpenTK.Platform;
using OpenTK.Graphics.OpenGL;
@@ -96,6 +98,12 @@ TK.Window.SetMode(wnd, WindowMode.Normal);
List points = new List();
+IFont font = executor.ResourcePool.GetResourceManager()
+ .QueryFont("Nimbus Mono", FontWeight._500, FontSlant.Normal, FontStretch.Normal)!
+ .WithSize(12f);
+
+queue.Text(new sys.Vector3(96, 96, 0), bg, "Hello World!", font);
+queue.Text(new sys.Vector3(128, 12, 0), bg, "japenis too! uwa ~~~ アホ", font);
queue.Line(new sys.Vector2(64, 256), new sys.Vector2(64+256, 256), 0, 64f, fg);
queue.Rect(new sys.Vector2(16, 16), new sys.Vector2(96, 96), 0, fg, bg, 8f);