Compare commits
No commits in common. "master" and "dashboard2" have entirely different histories.
master
...
dashboard2
264
Dashboard.BlurgText.OpenGL/BlurgGLExtension.cs
Normal file
264
Dashboard.BlurgText.OpenGL/BlurgGLExtension.cs
Normal file
@ -0,0 +1,264 @@
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using BlurgText;
|
||||
using Dashboard.Drawing;
|
||||
using Dashboard.OpenGL;
|
||||
using OpenTK.Graphics.OpenGL;
|
||||
using OPENGL = OpenTK.Graphics.OpenGL;
|
||||
|
||||
namespace Dashboard.BlurgText.OpenGL
|
||||
{
|
||||
public class BlurgGLExtension : BlurgDcExtension
|
||||
{
|
||||
private readonly List<int> _textures = new List<int>();
|
||||
private int _program = 0;
|
||||
private int _transformsLocation = -1;
|
||||
private int _atlasLocation = -1;
|
||||
private int _borderWidthLocation = -1;
|
||||
private int _borderColorLocation = -1;
|
||||
private int _fillColorLocation = -1;
|
||||
private int _vertexArray = 0;
|
||||
|
||||
public override Blurg Blurg { get; }
|
||||
public bool SystemFontsEnabled { get; set; }
|
||||
public bool IsDisposed { get; private set; } = false;
|
||||
|
||||
public override string DriverName => "BlurgText";
|
||||
public override string DriverVendor => "Dashboard and BlurgText";
|
||||
public override Version DriverVersion => new Version(1, 0);
|
||||
|
||||
private new GLDeviceContext Context => (GLDeviceContext)base.Context;
|
||||
|
||||
public BlurgGLExtension()
|
||||
{
|
||||
Blurg = new Blurg(AllocateTexture, UpdateTexture);
|
||||
SystemFontsEnabled = Blurg.EnableSystemFonts();
|
||||
}
|
||||
|
||||
~BlurgGLExtension()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public BlurgFontProxy InternFont(IFont font)
|
||||
{
|
||||
BlurgTextExtension appExtension = Application.ExtensionRequire<BlurgTextExtension>();
|
||||
if (font is FontInfo fontInfo)
|
||||
{
|
||||
return (BlurgFontProxy)appExtension.Load(fontInfo);
|
||||
}
|
||||
else if (font is BlurgFontProxy blurgFont)
|
||||
{
|
||||
if (blurgFont.Owner != Blurg)
|
||||
{
|
||||
if (blurgFont.Path == null)
|
||||
return (BlurgFontProxy)appExtension.Load(new FontInfo(blurgFont.Family, blurgFont.Weight,
|
||||
blurgFont.Slant,
|
||||
blurgFont.Stretch));
|
||||
else
|
||||
return (BlurgFontProxy)appExtension.Load(blurgFont.Path);
|
||||
}
|
||||
else
|
||||
{
|
||||
return blurgFont;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Unsupported font resource.");
|
||||
}
|
||||
}
|
||||
|
||||
private void UseProgram()
|
||||
{
|
||||
if (_program != 0)
|
||||
{
|
||||
GL.UseProgram(_program);
|
||||
return;
|
||||
}
|
||||
|
||||
Assembly self = typeof(BlurgGLExtension).Assembly;
|
||||
|
||||
using Stream vsource = self.GetManifestResourceStream("Dashboard.BlurgText.OpenGL.text.vert")!;
|
||||
using Stream fsource = self.GetManifestResourceStream("Dashboard.BlurgText.OpenGL.text.frag")!;
|
||||
int vs = ShaderUtil.CompileShader(ShaderType.VertexShader, vsource);
|
||||
int fs = ShaderUtil.CompileShader(ShaderType.FragmentShader, fsource);
|
||||
_program = ShaderUtil.LinkProgram(vs, fs, [
|
||||
"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");
|
||||
|
||||
GL.UseProgram(_program);
|
||||
GL.Uniform1i(_atlasLocation, 0);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return;
|
||||
IsDisposed = true;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
foreach (int texture in _textures)
|
||||
{
|
||||
Context.Collector.DeleteTexture(texture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void DrawBlurgResult(BlurgResult result, Vector3 position)
|
||||
{
|
||||
if (result.Count == 0)
|
||||
return;
|
||||
|
||||
Matrix4x4 view = Matrix4x4.CreateTranslation(position) * Context.ExtensionRequire<IDeviceContextBase>().Transforms;
|
||||
|
||||
List<DrawCall> drawCalls = new List<DrawCall>();
|
||||
List<Vertex> vertices = new List<Vertex>();
|
||||
List<ushort> indices = new List<ushort>();
|
||||
int offset = 0;
|
||||
int count = 0;
|
||||
|
||||
DrawCall prototype = default;
|
||||
for (int i = 0; i < result.Count; i++)
|
||||
{
|
||||
BlurgRect rect = result[i];
|
||||
|
||||
int texture = (int)rect.UserData;
|
||||
Vector4 fillColor = new Vector4(rect.Color.R / 255f, rect.Color.G / 255f, rect.Color.B / 255f,
|
||||
rect.Color.A / 255f);
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
prototype = new DrawCall(0, 0, texture, fillColor);
|
||||
}
|
||||
else if (prototype.Texture != texture || prototype.FillColor != fillColor)
|
||||
{
|
||||
drawCalls.Add(prototype with { Count = count, Offset = offset });
|
||||
prototype = new DrawCall(0, 0, texture, fillColor);
|
||||
offset += count;
|
||||
count = 0;
|
||||
}
|
||||
|
||||
vertices.Add(new Vertex(rect.X, rect.Y, 0, rect.U0, rect.V0));
|
||||
vertices.Add(new Vertex(rect.X + rect.Width, rect.Y, 0, rect.U1, rect.V0));
|
||||
vertices.Add(new Vertex(rect.X + rect.Width, rect.Y + rect.Height, 0, rect.U1, rect.V1));
|
||||
vertices.Add(new Vertex(rect.X, rect.Y + rect.Height, 0, rect.U0, rect.V1));
|
||||
|
||||
indices.Add((ushort)(vertices.Count - 4));
|
||||
indices.Add((ushort)(vertices.Count - 3));
|
||||
indices.Add((ushort)(vertices.Count - 2));
|
||||
indices.Add((ushort)(vertices.Count - 4));
|
||||
indices.Add((ushort)(vertices.Count - 2));
|
||||
indices.Add((ushort)(vertices.Count - 1));
|
||||
|
||||
count += 6;
|
||||
}
|
||||
drawCalls.Add(prototype with { Count = count, Offset = offset });
|
||||
|
||||
if (_vertexArray == 0)
|
||||
{
|
||||
_vertexArray = GL.GenVertexArray();
|
||||
}
|
||||
GL.BindVertexArray(_vertexArray);
|
||||
|
||||
Span<int> buffers = stackalloc int[2];
|
||||
GL.GenBuffers(2, buffers);
|
||||
GL.BindBuffer(BufferTarget.ArrayBuffer, buffers[0]);
|
||||
GL.BufferData(BufferTarget.ArrayBuffer, vertices.Count * Vertex.Size, (ReadOnlySpan<Vertex>)CollectionsMarshal.AsSpan(vertices), BufferUsage.StaticRead);
|
||||
|
||||
GL.BindBuffer(BufferTarget.ElementArrayBuffer, buffers[1]);
|
||||
GL.BufferData(BufferTarget.ElementArrayBuffer, indices.Count * sizeof(ushort), (ReadOnlySpan<ushort>)CollectionsMarshal.AsSpan(indices), BufferUsage.StaticRead);
|
||||
|
||||
GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, Vertex.Size, Vertex.PositionOffset);
|
||||
GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, Vertex.Size, Vertex.TexCoordOffset);
|
||||
GL.EnableVertexAttribArray(0); GL.EnableVertexAttribArray(1);
|
||||
|
||||
UseProgram();
|
||||
|
||||
GL.UniformMatrix4f(_transformsLocation, 1, true, in view);
|
||||
GL.ActiveTexture(TextureUnit.Texture0);
|
||||
|
||||
foreach (DrawCall call in drawCalls)
|
||||
{
|
||||
GL.BindTexture(TextureTarget.Texture2d, call.Texture);
|
||||
GL.Uniform4f(_fillColorLocation, call.FillColor.X, call.FillColor.Y, call.FillColor.Z,
|
||||
call.FillColor.W);
|
||||
GL.DrawElements(PrimitiveType.Triangles, call.Count, DrawElementsType.UnsignedShort, call.Offset);
|
||||
}
|
||||
|
||||
GL.DeleteBuffers(2, buffers);
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Size = Size)]
|
||||
private struct Vertex(Vector3 position, Vector2 texCoord)
|
||||
{
|
||||
[FieldOffset(PositionOffset)]
|
||||
public Vector3 Position = position;
|
||||
|
||||
[FieldOffset(TexCoordOffset)]
|
||||
public Vector2 TexCoord = texCoord;
|
||||
|
||||
public Vertex(float x, float y, float z, float u, float v)
|
||||
: this(new Vector3(x, y, z), new Vector2(u, v))
|
||||
{
|
||||
}
|
||||
|
||||
public const int Size = 8 * sizeof(float);
|
||||
public const int PositionOffset = 0 * sizeof(float);
|
||||
public const int TexCoordOffset = 4 * sizeof(float);
|
||||
}
|
||||
|
||||
private struct DrawCall(int offset, int count, int texture, Vector4 fillColor)
|
||||
{
|
||||
public int Offset = offset;
|
||||
public int Count = count;
|
||||
public int Texture = texture;
|
||||
public Vector4 FillColor = fillColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Dashboard.BlurgText.OpenGL/Dashboard.BlurgText.OpenGL.csproj
Normal file
20
Dashboard.BlurgText.OpenGL/Dashboard.BlurgText.OpenGL.csproj
Normal file
@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../Dashboard.BlurgText/Dashboard.BlurgText.csproj"/>
|
||||
<ProjectReference Include="..\Dashboard.OpenGL\Dashboard.OpenGL.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="text.frag" />
|
||||
<EmbeddedResource Include="text.vert" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
21
Dashboard.BlurgText.OpenGL/text.frag
Normal file
21
Dashboard.BlurgText.OpenGL/text.frag
Normal file
@ -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.01)
|
||||
discard;
|
||||
f_Color = color;
|
||||
}
|
||||
18
Dashboard.BlurgText.OpenGL/text.vert
Normal file
18
Dashboard.BlurgText.OpenGL/text.vert
Normal file
@ -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;
|
||||
}
|
||||
21
Dashboard.BlurgText/BlurgFontProxy.cs
Normal file
21
Dashboard.BlurgText/BlurgFontProxy.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using BlurgText;
|
||||
using Dashboard.Drawing;
|
||||
|
||||
namespace Dashboard.BlurgText
|
||||
{
|
||||
public class BlurgFontProxy(Blurg owner, BlurgFont font) : IFont
|
||||
{
|
||||
public Blurg Owner { get; } = owner;
|
||||
public BlurgFont Font { get; } = font;
|
||||
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 string? Path { get; init; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
165
Dashboard.BlurgText/BlurgTextExtension.cs
Normal file
165
Dashboard.BlurgText/BlurgTextExtension.cs
Normal file
@ -0,0 +1,165 @@
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using BlurgText;
|
||||
using Dashboard.Drawing;
|
||||
using Dashboard.Pal;
|
||||
|
||||
namespace Dashboard.BlurgText
|
||||
{
|
||||
public interface IBlurgDcExtensionFactory
|
||||
{
|
||||
public BlurgDcExtension CreateExtension(BlurgTextExtension appExtension, DeviceContext dc);
|
||||
}
|
||||
|
||||
public class BlurgTextExtension(IBlurgDcExtensionFactory dcExtensionFactory) : IApplicationExtension, IFontLoader
|
||||
{
|
||||
private readonly Blurg _blurg = new Blurg(GlobalTextureAllocation, GlobalTextureUpdate);
|
||||
|
||||
public Application Context { get; private set; } = null!;
|
||||
public string DriverName { get; } = "BlurgText";
|
||||
public string DriverVendor { get; } = "Dashbord and BlurgText";
|
||||
public Version DriverVersion { get; } = new Version(1, 0);
|
||||
public IBlurgDcExtensionFactory DcExtensionFactory { get; } = dcExtensionFactory;
|
||||
IContextBase IContextExtensionBase.Context => Context;
|
||||
public bool IsDisposed { get; private set; } = false;
|
||||
|
||||
public void Require(Application context)
|
||||
{
|
||||
Context = context;
|
||||
context.DeviceContextCreated += OnDeviceContextCreated;
|
||||
}
|
||||
|
||||
void IContextExtensionBase.Require(IContextBase context) => Require((Application)context);
|
||||
|
||||
private void RequireDeviceContextExtension(DeviceContext dc)
|
||||
{
|
||||
dc.ExtensionPreload<BlurgDcExtension>(() => DcExtensionFactory.CreateExtension(this, dc));
|
||||
}
|
||||
|
||||
private void OnDeviceContextCreated(object? sender, DeviceContext dc)
|
||||
{
|
||||
RequireDeviceContextExtension(dc);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (IsDisposed)
|
||||
return;
|
||||
IsDisposed = true;
|
||||
|
||||
_blurg.Dispose();
|
||||
Context.DeviceContextCreated -= OnDeviceContextCreated;
|
||||
}
|
||||
|
||||
private static void GlobalTextureUpdate(IntPtr userdata, IntPtr buffer, int x, int y, int width, int height)
|
||||
{
|
||||
// Report the user error.
|
||||
Debug.WriteLine("Attempt to create or update a texture from the global BlurgText.", "Dashboard/BlurgEngine");
|
||||
}
|
||||
|
||||
private static IntPtr GlobalTextureAllocation(int width, int height)
|
||||
{
|
||||
Debug.WriteLine("Attempt to create or update a texture from the global BlurgText.", "Dashboard/BlurgEngine");
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
public IFont Load(FontInfo info)
|
||||
{
|
||||
BlurgFont? font = _blurg.QueryFont(
|
||||
info.Family,
|
||||
info.Weight switch
|
||||
{
|
||||
FontWeight._100 => global::BlurgText.FontWeight.Thin,
|
||||
FontWeight._200 => global::BlurgText.FontWeight.ExtraLight,
|
||||
FontWeight._300 => global::BlurgText.FontWeight.Light,
|
||||
FontWeight._500 => global::BlurgText.FontWeight.Medium,
|
||||
FontWeight._600 => global::BlurgText.FontWeight.SemiBold,
|
||||
FontWeight._700 => global::BlurgText.FontWeight.Bold,
|
||||
FontWeight._800 => global::BlurgText.FontWeight.ExtraBold,
|
||||
FontWeight._900 => global::BlurgText.FontWeight.Black,
|
||||
_ => global::BlurgText.FontWeight.Regular,
|
||||
},
|
||||
info.Slant switch
|
||||
{
|
||||
FontSlant.Oblique or FontSlant.Italic => true,
|
||||
_ => false,
|
||||
});
|
||||
|
||||
if (font == null)
|
||||
throw new Exception("Font not found.");
|
||||
|
||||
return new BlurgFontProxy(_blurg, font);
|
||||
}
|
||||
|
||||
public IFont Load(string path)
|
||||
{
|
||||
BlurgFont? font = _blurg.AddFontFile(path);
|
||||
|
||||
if (font == null)
|
||||
throw new Exception("Font not found.");
|
||||
|
||||
return new BlurgFontProxy(_blurg, font);
|
||||
}
|
||||
|
||||
public IFont Load(Stream stream)
|
||||
{
|
||||
string path;
|
||||
Stream dest;
|
||||
for (int i = 0;; i++)
|
||||
{
|
||||
path = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
dest = File.Open(path, FileMode.OpenOrCreate, 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();
|
||||
|
||||
return Load(path);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class BlurgDcExtension : IDeviceContextExtension
|
||||
{
|
||||
public abstract Blurg Blurg { get; }
|
||||
public abstract string DriverName { get; }
|
||||
public abstract string DriverVendor { get; }
|
||||
public abstract Version DriverVersion { get; }
|
||||
|
||||
public Application Application => Context.Application;
|
||||
public DeviceContext Context { get; private set; } = null!;
|
||||
|
||||
public abstract void DrawBlurgResult(BlurgResult result, Vector3 position);
|
||||
|
||||
public void DrawBlurgFormattedText(BlurgFormattedText text, Vector3 position, float width = 0f)
|
||||
{
|
||||
BlurgResult? result = Blurg.BuildFormattedText(text, maxWidth: width);
|
||||
if (result == null)
|
||||
throw new Exception("Could not build formatted text result.");
|
||||
|
||||
DrawBlurgResult(result, position);
|
||||
}
|
||||
|
||||
public abstract void Dispose();
|
||||
|
||||
IContextBase IContextExtensionBase.Context => Context;
|
||||
|
||||
public void Require(DeviceContext context)
|
||||
{
|
||||
Context = context;
|
||||
}
|
||||
|
||||
void IContextExtensionBase.Require(IContextBase context) => Require((DeviceContext)context);
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BlurgText" Version="0.1.0-nightly-4" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BlurgText" Version="0.1.0-nightly-19" />
|
||||
<ProjectReference Include="..\Dashboard.Common\Dashboard.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
15
Dashboard.Common/Anchor.cs
Normal file
15
Dashboard.Common/Anchor.cs
Normal file
@ -0,0 +1,15 @@
|
||||
namespace Dashboard
|
||||
{
|
||||
[Flags]
|
||||
public enum Anchor
|
||||
{
|
||||
Auto = 0,
|
||||
Right = (1 << 0),
|
||||
Left = (1 << 1),
|
||||
HCenter = Left | Right,
|
||||
Top = (1 << 2),
|
||||
Bottom = (1 << 3),
|
||||
VCenter = Top | Bottom,
|
||||
Middle = HCenter | VCenter,
|
||||
}
|
||||
}
|
||||
65
Dashboard.Common/Box2d.cs
Normal file
65
Dashboard.Common/Box2d.cs
Normal file
@ -0,0 +1,65 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Dashboard
|
||||
{
|
||||
public readonly record struct Box2d(Vector2 Min, Vector2 Max)
|
||||
{
|
||||
public float Left => Min.X;
|
||||
public float Right => Max.X;
|
||||
public float Top => Min.Y;
|
||||
public float Bottom => Max.Y;
|
||||
|
||||
public Vector2 Size => Max - Min;
|
||||
public Vector2 Center => (Min + Max) * 0.5f;
|
||||
|
||||
public Box2d(RectangleF rectangle)
|
||||
: this(new Vector2(rectangle.Left, rectangle.Top), new Vector2(rectangle.Right, rectangle.Bottom))
|
||||
{
|
||||
}
|
||||
|
||||
public Box2d(float x0, float y0, float x1, float y1)
|
||||
: this(new Vector2(x0, y0), new Vector2(x1, y1))
|
||||
{
|
||||
}
|
||||
|
||||
public static Box2d FromPositionAndSize(Vector2 position, Vector2 size, Origin anchor = Origin.Center)
|
||||
{
|
||||
Vector2 half = size * 0.5f;
|
||||
switch (anchor)
|
||||
{
|
||||
case Origin.Center:
|
||||
return new Box2d(position - half, position + half);
|
||||
case Origin.TopLeft:
|
||||
return new Box2d(position, position + size);
|
||||
default:
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public static Box2d Union(Box2d left, Box2d right)
|
||||
{
|
||||
Vector2 min = Vector2.Min(left.Min, right.Min);
|
||||
Vector2 max = Vector2.Max(left.Max, right.Max);
|
||||
return new Box2d(min, max);
|
||||
}
|
||||
|
||||
public static Box2d Intersect(Box2d left, Box2d right)
|
||||
{
|
||||
Vector2 min = Vector2.Max(left.Min, right.Min);
|
||||
Vector2 max = Vector2.Min(left.Max, right.Max);
|
||||
return new Box2d(min, max);
|
||||
}
|
||||
|
||||
public static explicit operator RectangleF(Box2d box2d)
|
||||
{
|
||||
return new RectangleF((PointF)box2d.Center, (SizeF)box2d.Size);
|
||||
}
|
||||
|
||||
public static explicit operator Box2d(RectangleF rectangle)
|
||||
{
|
||||
return new Box2d(rectangle);
|
||||
}
|
||||
}
|
||||
}
|
||||
46
Dashboard.Common/Box3d.cs
Normal file
46
Dashboard.Common/Box3d.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Dashboard
|
||||
{
|
||||
public readonly record struct Box3d(Vector3 Min, Vector3 Max)
|
||||
{
|
||||
public float Left => Min.X;
|
||||
public float Right => Max.X;
|
||||
public float Top => Min.Y;
|
||||
public float Bottom => Max.Y;
|
||||
public float Far => Min.Z;
|
||||
public float Near => Max.Z;
|
||||
|
||||
public Vector3 Size => Max - Min;
|
||||
public Vector3 Center => Min + Size * 0.5f;
|
||||
|
||||
public static Box3d Union(Box3d left, Box3d right)
|
||||
{
|
||||
Vector3 min = Vector3.Min(left.Min, right.Min);
|
||||
Vector3 max = Vector3.Max(left.Max, right.Max);
|
||||
return new Box3d(min, max);
|
||||
}
|
||||
|
||||
public static Box3d Union(Box3d box, Box2d bounds, float depth)
|
||||
{
|
||||
Vector3 min = Vector3.Min(box.Min, new Vector3(bounds.Left, bounds.Top, depth));
|
||||
Vector3 max = Vector3.Max(box.Max, new Vector3(bounds.Right, bounds.Bottom, depth));
|
||||
return new Box3d(min, max);
|
||||
}
|
||||
|
||||
public static Box3d Intersect(Box3d left, Box3d right)
|
||||
{
|
||||
Vector3 min = Vector3.Max(left.Min, right.Min);
|
||||
Vector3 max = Vector3.Min(left.Max, right.Max);
|
||||
return new Box3d(min, max);
|
||||
}
|
||||
|
||||
public static Box3d Intersect(Box3d box, Box2d bounds, float depth)
|
||||
{
|
||||
Vector3 min = Vector3.Max(box.Min, new Vector3(bounds.Min, depth));
|
||||
Vector3 max = Vector3.Min(box.Max, new Vector3(bounds.Max, depth));
|
||||
return new Box3d(min, max);
|
||||
}
|
||||
}
|
||||
}
|
||||
95
Dashboard.Common/Collections/TypeAtom.cs
Normal file
95
Dashboard.Common/Collections/TypeAtom.cs
Normal file
@ -0,0 +1,95 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Dashboard.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for better type access performance.
|
||||
/// </summary>
|
||||
public record TypeAtom
|
||||
{
|
||||
|
||||
public Type Type { get; }
|
||||
public int Id { get; }
|
||||
public ImmutableHashSet<TypeAtom> Ancestors { get; }
|
||||
|
||||
// Makes it so TypeAtom doesn't get pissed at me
|
||||
private protected TypeAtom()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
private TypeAtom(Type type)
|
||||
{
|
||||
Type = type;
|
||||
|
||||
HashSet<TypeAtom> ancestors = new HashSet<TypeAtom>();
|
||||
FindAncestors(Type, ancestors);
|
||||
ancestors.Add(this);
|
||||
Ancestors = ancestors.ToImmutableHashSet();
|
||||
|
||||
lock (s_lockObject)
|
||||
{
|
||||
Id = s_counter++;
|
||||
s_atoms.Add(Id, this);
|
||||
s_types.Add(Type, this);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly object s_lockObject = new object();
|
||||
private static readonly Dictionary<int, TypeAtom> s_atoms = new Dictionary<int, TypeAtom>();
|
||||
private static readonly Dictionary<Type, TypeAtom> s_types = new Dictionary<Type, TypeAtom>();
|
||||
|
||||
private static int s_counter = 0;
|
||||
|
||||
public static TypeAtom? Get(int id) => s_atoms.GetValueOrDefault(id);
|
||||
public static TypeAtom Get(Type type)
|
||||
{
|
||||
if (s_types.TryGetValue(type, out TypeAtom? id))
|
||||
return id;
|
||||
|
||||
// Type is not registered, try to acquire lock.
|
||||
lock (s_lockObject)
|
||||
{
|
||||
// Maybe somebody else registered this type whilst acquiring the lock.
|
||||
if (s_types.TryGetValue(type, out id))
|
||||
return id;
|
||||
|
||||
// Register the type if applicable and leave.
|
||||
return new TypeAtom(type);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static void FindAncestors(Type type, HashSet<TypeAtom> destination)
|
||||
{
|
||||
// Traverse the object tree for all possible aliases.
|
||||
if (type.BaseType != null)
|
||||
{
|
||||
foreach (TypeAtom ancestor in Get(type.BaseType).Ancestors)
|
||||
{
|
||||
destination.Add(ancestor);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Type trait in type.GetInterfaces())
|
||||
{
|
||||
TypeAtom atom = Get(trait);
|
||||
destination.Add(atom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for better type access performance.
|
||||
/// </summary>
|
||||
public sealed record TypeAtom<T> : TypeAtom
|
||||
{
|
||||
public static TypeAtom Atom { get; } = Get(typeof(T));
|
||||
public new static int Id => Atom.Id;
|
||||
public new static Type Type => Atom.Type;
|
||||
public new static ImmutableHashSet<TypeAtom> Ancestors => Atom.Ancestors;
|
||||
|
||||
private TypeAtom() { }
|
||||
}
|
||||
}
|
||||
157
Dashboard.Common/Collections/TypeDictionary.cs
Normal file
157
Dashboard.Common/Collections/TypeDictionary.cs
Normal file
@ -0,0 +1,157 @@
|
||||
using System.Collections;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Dashboard.Collections
|
||||
{
|
||||
public class TypeDictionary<T>(bool hierarchical = false) : IEnumerable<T>
|
||||
{
|
||||
private readonly Dictionary<int, T> _objects = new Dictionary<int, T>();
|
||||
|
||||
public bool Contains<T2>() where T2 : T => _objects.ContainsKey(TypeAtom<T2>.Id);
|
||||
|
||||
public bool Add<T2>(T2 value) where T2 : T
|
||||
{
|
||||
TypeAtom atom = TypeAtom<T2>.Atom;
|
||||
|
||||
if (!_objects.TryAdd(atom.Id, value))
|
||||
return false;
|
||||
|
||||
if (!hierarchical)
|
||||
return true;
|
||||
|
||||
foreach (TypeAtom ancestor in atom.Ancestors)
|
||||
{
|
||||
_objects[ancestor.Id] = value;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public T2 Get<T2>() where T2 : T => TryGet(out T2? value) ? value : throw new KeyNotFoundException();
|
||||
|
||||
public void Set<T2>(T2 value) where T2 : T
|
||||
{
|
||||
TypeAtom atom = TypeAtom<T2>.Atom;
|
||||
_objects[atom.Id] = value;
|
||||
|
||||
if (!hierarchical)
|
||||
return;
|
||||
|
||||
foreach (TypeAtom ancestor in atom.Ancestors)
|
||||
{
|
||||
_objects[ancestor.Id] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove<T2>() where T2 : T => Remove<T>(out _);
|
||||
|
||||
public bool Remove<T2>([NotNullWhen(true)] out T2? value) where T2 : T
|
||||
{
|
||||
TypeAtom atom = TypeAtom<T2>.Atom;
|
||||
|
||||
if (!_objects.Remove(atom.Id, out T? subValue))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = (T2?)subValue;
|
||||
|
||||
if (hierarchical)
|
||||
{
|
||||
foreach (TypeAtom ancestor in atom.Ancestors)
|
||||
{
|
||||
_objects.Remove(ancestor.Id);
|
||||
}
|
||||
}
|
||||
|
||||
return value != null;
|
||||
}
|
||||
|
||||
public bool TryGet<T2>([NotNullWhen(true)] out T2? value) where T2 : T
|
||||
{
|
||||
if (!_objects.TryGetValue(TypeAtom<T2>.Id, out T? subValue))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = (T2?)subValue;
|
||||
return value != null;
|
||||
}
|
||||
|
||||
public void Clear() => _objects.Clear();
|
||||
|
||||
public IEnumerator<T> GetEnumerator() => _objects.Values.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
|
||||
public class TypeDictionary<TKey, TValue>(bool hierarchical = false) : IEnumerable<KeyValuePair<TypeAtom, TValue>>
|
||||
{
|
||||
private readonly Dictionary<int, TValue> _objects = new Dictionary<int, TValue>();
|
||||
|
||||
public bool Contains<TKey2>() where TKey2 : TKey => _objects.ContainsKey(TypeAtom<TKey2>.Id);
|
||||
|
||||
public bool Add<TKey2>(TValue value) where TKey2 : TKey
|
||||
{
|
||||
TypeAtom atom = TypeAtom<TKey2>.Atom;
|
||||
|
||||
if (!_objects.TryAdd(atom.Id, value))
|
||||
return false;
|
||||
|
||||
if (!hierarchical)
|
||||
return true;
|
||||
|
||||
foreach (TypeAtom ancestor in atom.Ancestors)
|
||||
{
|
||||
_objects[ancestor.Id] = value;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public TValue Get<TKey2>() where TKey2 : TKey => (TValue)_objects[TypeAtom<TKey2>.Id]!;
|
||||
|
||||
public void Set<TKey2>(TValue value) where TKey2 : TKey
|
||||
{
|
||||
TypeAtom atom = TypeAtom<TKey2>.Atom;
|
||||
_objects[atom.Id] = value;
|
||||
|
||||
if (!hierarchical)
|
||||
return;
|
||||
|
||||
foreach (TypeAtom ancestor in atom.Ancestors)
|
||||
{
|
||||
_objects[ancestor.Id] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove<TKey2>() where TKey2 : TKey => Remove<TKey2>(out _);
|
||||
public bool Remove<TKey2>([MaybeNullWhen(false)] out TValue? value) where TKey2 : TKey
|
||||
{
|
||||
TypeAtom atom = TypeAtom<TKey2>.Atom;
|
||||
|
||||
if (!_objects.Remove(atom.Id, out value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hierarchical)
|
||||
{
|
||||
foreach (TypeAtom ancestor in atom.Ancestors)
|
||||
{
|
||||
_objects.Remove(ancestor.Id);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGet<TKey2>([NotNullWhen(true)] out TValue? value) where TKey2 : TKey => _objects.TryGetValue(TypeAtom<TKey2>.Id, out value);
|
||||
|
||||
public void Clear() => _objects.Clear();
|
||||
|
||||
public IEnumerator<KeyValuePair<TypeAtom, TValue>> GetEnumerator() => _objects.Select(x => new KeyValuePair<TypeAtom, TValue>(TypeAtom.Get(x.Key)!, x.Value)).GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
||||
44
Dashboard.Common/Collections/TypeHashSet.cs
Normal file
44
Dashboard.Common/Collections/TypeHashSet.cs
Normal file
@ -0,0 +1,44 @@
|
||||
using System.Collections;
|
||||
|
||||
namespace Dashboard.Collections
|
||||
{
|
||||
public class TypeHashSet(bool hierarchical = false) : IEnumerable<TypeAtom>
|
||||
{
|
||||
private readonly HashSet<int> _set = new HashSet<int>();
|
||||
|
||||
public bool Contains<T>() => _set.Contains(TypeAtom<T>.Id);
|
||||
|
||||
public bool Set<T>()
|
||||
{
|
||||
if (!_set.Add(TypeAtom<T>.Id))
|
||||
return false;
|
||||
|
||||
if (hierarchical)
|
||||
foreach (TypeAtom ancestor in TypeAtom<T>.Ancestors)
|
||||
{
|
||||
_set.Add(ancestor.Id);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Reset<T>()
|
||||
{
|
||||
if (!_set.Remove(TypeAtom<T>.Id))
|
||||
return false;
|
||||
|
||||
if (hierarchical)
|
||||
foreach (TypeAtom ancestor in TypeAtom<T>.Ancestors)
|
||||
{
|
||||
_set.Remove(ancestor.Id);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Clear() => _set.Clear();
|
||||
|
||||
public IEnumerator<TypeAtom> GetEnumerator() => _set.Select(x => TypeAtom.Get(x)!).GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
||||
10
Dashboard.Common/Dashboard.Common.csproj
Normal file
10
Dashboard.Common/Dashboard.Common.csproj
Normal file
@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Dashboard</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
20
Dashboard.Common/Drawing/IDeviceContextBase.cs
Normal file
20
Dashboard.Common/Drawing/IDeviceContextBase.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
using Dashboard.Pal;
|
||||
|
||||
namespace Dashboard.Drawing
|
||||
{
|
||||
public interface IDeviceContextBase : IDeviceContextExtension
|
||||
{
|
||||
RectangleF ClipRegion { get; }
|
||||
Matrix4x4 Transforms { get; }
|
||||
|
||||
void ResetClip();
|
||||
void PushClip(RectangleF clipRegion);
|
||||
void PopClip();
|
||||
|
||||
void ResetTransforms();
|
||||
void PushTransforms(in Matrix4x4 matrix);
|
||||
void PopTransforms();
|
||||
}
|
||||
}
|
||||
25
Dashboard.Common/Drawing/IFontLoader.cs
Normal file
25
Dashboard.Common/Drawing/IFontLoader.cs
Normal file
@ -0,0 +1,25 @@
|
||||
namespace Dashboard.Drawing
|
||||
{
|
||||
public interface IFont : IDisposable
|
||||
{
|
||||
string Family { get; }
|
||||
FontWeight Weight { get; }
|
||||
FontSlant Slant { get; }
|
||||
FontStretch Stretch { get; }
|
||||
}
|
||||
|
||||
public record struct FontInfo(string Family, FontWeight Weight = FontWeight.Normal,
|
||||
FontSlant Slant = FontSlant.Normal, FontStretch Stretch = FontStretch.Normal) : IFont
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public interface IFontLoader
|
||||
{
|
||||
IFont Load(FontInfo info);
|
||||
IFont Load(string path);
|
||||
IFont Load(Stream stream);
|
||||
}
|
||||
}
|
||||
61
Dashboard.Common/Drawing/IImageLoader.cs
Normal file
61
Dashboard.Common/Drawing/IImageLoader.cs
Normal file
@ -0,0 +1,61 @@
|
||||
using Dashboard.Pal;
|
||||
|
||||
namespace Dashboard.Drawing
|
||||
{
|
||||
public record ImageData(
|
||||
TextureType Type,
|
||||
PixelFormat Format,
|
||||
int Width,
|
||||
int Height,
|
||||
byte[] Bitmap)
|
||||
{
|
||||
public int Depth { get; init; } = 1;
|
||||
public int Levels { get; init; } = 1;
|
||||
public bool Premultiplied { get; init; } = false;
|
||||
public int Alignment { get; init; } = 4;
|
||||
|
||||
public long GetLevelOffset(int level)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(level, 0, nameof(level));
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThan(level, Levels, nameof(level));
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThan(level, Math.ILogB(Math.Max(Width, Height)));
|
||||
|
||||
long offset = 0;
|
||||
|
||||
long row = Width * Format switch
|
||||
{
|
||||
PixelFormat.R8I => 1,
|
||||
PixelFormat.R16F => 2,
|
||||
PixelFormat.Rg8I => 2,
|
||||
PixelFormat.Rg16F => 4,
|
||||
PixelFormat.Rgb8I => 3,
|
||||
PixelFormat.Rgb16F => 6,
|
||||
PixelFormat.Rgba8I => 4,
|
||||
PixelFormat.Rgba16F => 8,
|
||||
};
|
||||
|
||||
row += Alignment - (row % Alignment);
|
||||
long plane = row * Height;
|
||||
long volume = plane * Depth;
|
||||
|
||||
for (int i = 0; i < level; i++)
|
||||
{
|
||||
if (Depth == 1)
|
||||
{
|
||||
offset += plane / (1 << i) / (1 << i);
|
||||
}
|
||||
else
|
||||
{
|
||||
offset += volume / (1 << i) / (1 << i) / (1 << i);
|
||||
}
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IImageLoader : IApplicationExtension
|
||||
{
|
||||
public ImageData LoadImageData(Stream stream);
|
||||
}
|
||||
}
|
||||
17
Dashboard.Common/Drawing/IImmediateMode.cs
Normal file
17
Dashboard.Common/Drawing/IImmediateMode.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
using Dashboard.Pal;
|
||||
|
||||
namespace Dashboard.Drawing
|
||||
{
|
||||
public interface IImmediateMode : IDeviceContextExtension
|
||||
{
|
||||
void ClearColor(Color color);
|
||||
|
||||
|
||||
|
||||
void Line(Vector2 a, Vector2 b, float width, float depth, Vector4 color);
|
||||
void Rectangle(Box2d rectangle, float depth, Vector4 color);
|
||||
void Image(Box2d rectangle, Box2d uv, float depth, ITexture texture);
|
||||
}
|
||||
}
|
||||
12
Dashboard.Common/Drawing/ITextRenderer.cs
Normal file
12
Dashboard.Common/Drawing/ITextRenderer.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using System.Numerics;
|
||||
using Dashboard.Pal;
|
||||
|
||||
namespace Dashboard.Drawing
|
||||
{
|
||||
|
||||
public interface ITextRenderer : IDeviceContextExtension
|
||||
{
|
||||
Box2d MeasureText(IFont font, float size, string text);
|
||||
void DrawText(Vector2 position, Vector4 color, float size, IFont font, string text);
|
||||
}
|
||||
}
|
||||
84
Dashboard.Common/Drawing/ITextureExtension.cs
Normal file
84
Dashboard.Common/Drawing/ITextureExtension.cs
Normal file
@ -0,0 +1,84 @@
|
||||
using System.Drawing;
|
||||
using Dashboard.Pal;
|
||||
|
||||
namespace Dashboard.Drawing
|
||||
{
|
||||
public interface ITextureExtension : IDeviceContextExtension
|
||||
{
|
||||
ITexture CreateTexture(TextureType type);
|
||||
}
|
||||
|
||||
public enum TextureType
|
||||
{
|
||||
Texture1D,
|
||||
Texture2D,
|
||||
Texture2DArray,
|
||||
Texture2DCube,
|
||||
Texture3D,
|
||||
}
|
||||
|
||||
public enum TextureFilter
|
||||
{
|
||||
Nearest,
|
||||
Linear,
|
||||
NearestMipmapNearest,
|
||||
LinearMipmapNearest,
|
||||
NearestMipmapLinear,
|
||||
LinearMipmapLinear,
|
||||
Anisotropic,
|
||||
}
|
||||
|
||||
public enum TextureRepeat
|
||||
{
|
||||
Repeat,
|
||||
MirroredRepeat,
|
||||
ClampToEdge,
|
||||
ClampToBorder,
|
||||
MirrorClampToEdge,
|
||||
}
|
||||
|
||||
public enum CubeMapFace
|
||||
{
|
||||
PositiveX,
|
||||
PositiveY,
|
||||
PositiveZ,
|
||||
NegativeX,
|
||||
NegativeY,
|
||||
NegativeZ,
|
||||
}
|
||||
|
||||
public interface ITexture : IDisposable
|
||||
{
|
||||
public TextureType Type { get; }
|
||||
public PixelFormat Format { get; }
|
||||
|
||||
public int Width { get; }
|
||||
public int Height { get; }
|
||||
public int Depth { get; }
|
||||
public int Levels { get; }
|
||||
|
||||
public bool Premultiplied { get; set; }
|
||||
|
||||
public ColorSwizzle Swizzle { get; set; }
|
||||
|
||||
public TextureFilter MinifyFilter { get; set; }
|
||||
|
||||
public TextureFilter MagnifyFilter { get; set; }
|
||||
|
||||
public Color BorderColor { get; set; }
|
||||
|
||||
public TextureRepeat RepeatS { get; set; }
|
||||
|
||||
public TextureRepeat RepeatT { get; set; }
|
||||
|
||||
public TextureRepeat RepeatR { get; set; }
|
||||
public int Anisotropy { get; set; }
|
||||
|
||||
void SetStorage(PixelFormat format, int width, int height, int depth, int levels);
|
||||
void Read<T>(Span<T> buffer, int level = 0, int align = 0) where T : unmanaged;
|
||||
void Write<T>(PixelFormat format, ReadOnlySpan<T> buffer, int level = 0, int align = 4) where T : unmanaged;
|
||||
void Premultiply();
|
||||
void Unmultiply();
|
||||
void GenerateMipmaps();
|
||||
}
|
||||
}
|
||||
47
Dashboard.Common/Events/KeyboardEvents.cs
Normal file
47
Dashboard.Common/Events/KeyboardEvents.cs
Normal file
@ -0,0 +1,47 @@
|
||||
namespace Dashboard.Events
|
||||
{
|
||||
[Flags]
|
||||
public enum ModifierKeys
|
||||
{
|
||||
None = 0,
|
||||
LeftBitPos = 8,
|
||||
RightBitPos = 16,
|
||||
|
||||
Shift = (1 << 0),
|
||||
Control = (1 << 1),
|
||||
Alt = (1 << 2),
|
||||
Meta = (1 << 3),
|
||||
|
||||
NumLock = (1 << 4),
|
||||
CapsLock = (1 << 5),
|
||||
ScrollLock = (1 << 6),
|
||||
|
||||
LeftShift = (Shift << LeftBitPos),
|
||||
LeftControl = (Control << LeftBitPos),
|
||||
LeftAlt = (Alt << LeftBitPos),
|
||||
LeftMeta = (Meta << LeftBitPos),
|
||||
|
||||
RightShift = (Shift << RightBitPos),
|
||||
RightControl = (Control << RightBitPos),
|
||||
RightAlt = (Alt << RightBitPos),
|
||||
RightMeta = (Meta << RightBitPos),
|
||||
}
|
||||
|
||||
public enum KeyCode
|
||||
{
|
||||
// TODO:
|
||||
}
|
||||
|
||||
public enum ScanCode
|
||||
{
|
||||
// TODO:
|
||||
}
|
||||
|
||||
public class KeyboardButtonEventArgs(KeyCode keyCode, ScanCode scanCode, ModifierKeys modifierKeys, bool up)
|
||||
: UiEventArgs(up ? UiEventType.KeyUp : UiEventType.KeyDown)
|
||||
{
|
||||
public KeyCode KeyCode { get; } = keyCode;
|
||||
public ScanCode ScanCode { get; } = scanCode;
|
||||
public ModifierKeys ModifierKeys { get; } = modifierKeys;
|
||||
}
|
||||
}
|
||||
43
Dashboard.Common/Events/MouseEvents.cs
Normal file
43
Dashboard.Common/Events/MouseEvents.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Dashboard.Events
|
||||
{
|
||||
[Flags]
|
||||
public enum MouseButtons
|
||||
{
|
||||
M1 = 1 << 0,
|
||||
M2 = 1 << 1,
|
||||
M3 = 1 << 2,
|
||||
M4 = 1 << 3,
|
||||
M5 = 1 << 4,
|
||||
M6 = 1 << 5,
|
||||
M7 = 1 << 6,
|
||||
M8 = 1 << 7,
|
||||
|
||||
Left = M1,
|
||||
Right = M2,
|
||||
Middle = M3,
|
||||
}
|
||||
|
||||
public sealed class MouseMoveEventArgs(Vector2 clientPosition, Vector2 delta) : UiEventArgs(UiEventType.MouseMove)
|
||||
{
|
||||
public Vector2 ClientPosition { get; } = clientPosition;
|
||||
public Vector2 Delta { get; } = delta;
|
||||
}
|
||||
|
||||
public sealed class MouseButtonEventArgs(Vector2 clientPosition, MouseButtons buttons, ModifierKeys modifierKeys, bool up)
|
||||
: UiEventArgs(up ? UiEventType.MouseButtonUp : UiEventType.MouseButtonDown)
|
||||
{
|
||||
public ModifierKeys ModifierKeys { get; } = modifierKeys;
|
||||
public Vector2 ClientPosition { get; } = clientPosition;
|
||||
public MouseButtons Buttons { get; } = buttons;
|
||||
}
|
||||
|
||||
public sealed class MouseScrollEventArgs(Vector2 clientPosition, Vector2 scrollDelta)
|
||||
: UiEventArgs(UiEventType.MouseScroll)
|
||||
{
|
||||
public Vector2 ClientPosition { get; } = clientPosition;
|
||||
public Vector2 ScrollDelta { get; } = scrollDelta;
|
||||
}
|
||||
}
|
||||
15
Dashboard.Common/Events/TickEventArgs.cs
Normal file
15
Dashboard.Common/Events/TickEventArgs.cs
Normal file
@ -0,0 +1,15 @@
|
||||
namespace Dashboard.Events
|
||||
{
|
||||
public class TickEventArgs : UiEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Animation delta time in seconds.
|
||||
/// </summary>
|
||||
public float Delta { get; }
|
||||
|
||||
public TickEventArgs(float delta) : base(UiEventType.AnimationTick)
|
||||
{
|
||||
Delta = delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
83
Dashboard.Common/Events/UiEventArgs.cs
Normal file
83
Dashboard.Common/Events/UiEventArgs.cs
Normal file
@ -0,0 +1,83 @@
|
||||
using System.Numerics;
|
||||
using Dashboard.Pal;
|
||||
|
||||
namespace Dashboard.Events
|
||||
{
|
||||
public enum UiEventType
|
||||
{
|
||||
None,
|
||||
AnimationTick, // Generic timer event.
|
||||
Paint, // Generic paint event.
|
||||
|
||||
// Text input related events.
|
||||
KeyDown, // Keyboard key down.
|
||||
KeyUp, // Keyboard key up.
|
||||
TextInput, // Non-IME text event.
|
||||
TextEdit, // IME text event.
|
||||
TextCandidates, // IME text candidate list.
|
||||
TextLanguage, // Keyboard language changed event.
|
||||
|
||||
// Mouse & touch related events
|
||||
MouseButtonDown, // Mouse button down.
|
||||
MouseButtonUp, // Mouse button up.
|
||||
MouseMove, // Mouse moved.
|
||||
MouseScroll, // Mouse scrolled.
|
||||
|
||||
// Reserved event names
|
||||
StylusEnter, // The stylus has entered the hover region.
|
||||
StylusLeave, // The stylus has left the hover region.
|
||||
StylusMove, // The stylus has moved.
|
||||
StylusDown, // The stylus is touching.
|
||||
StylusUp, // The stylus is no longer touching.
|
||||
StylusButtonUp, // Stylus button up.
|
||||
StylusButtonDown, // Stylus button down.
|
||||
StylusAxes, // Extra stylus axes data.
|
||||
|
||||
// Window & Control Events
|
||||
ControlInvalidateVisual, // Force rendering the control again.
|
||||
ControlStateChanged, // Control state changed.
|
||||
ControlMoved, // Control moved.
|
||||
ControlResized, // Control resized.
|
||||
ControlEnter, // The pointing device entered the control.
|
||||
ControlLeave, // The pointing device left the control.
|
||||
ControlFocusGet, // The control acquired focus.
|
||||
ControlFocusLost, // The control lost focus.
|
||||
WindowClose, // The window closed.
|
||||
|
||||
UserRangeStart = 1 << 12,
|
||||
}
|
||||
|
||||
public class UiEventArgs : EventArgs
|
||||
{
|
||||
public UiEventType Type { get; }
|
||||
|
||||
public UiEventArgs(UiEventType type)
|
||||
{
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public static readonly UiEventArgs None = new UiEventArgs(UiEventType.None);
|
||||
}
|
||||
|
||||
public class PaintEventArgs(DeviceContext dc) : UiEventArgs(UiEventType.Paint)
|
||||
{
|
||||
public DeviceContext DeviceContext { get; } = dc;
|
||||
}
|
||||
|
||||
public class ControlMovedEventArgs : UiEventArgs
|
||||
{
|
||||
public Vector2 OldPosition { get; }
|
||||
public Vector2 NewPosition { get; }
|
||||
|
||||
public ControlMovedEventArgs(Vector2 oldPosition, Vector2 newPosition) : base(UiEventType.ControlMoved)
|
||||
{
|
||||
OldPosition = oldPosition;
|
||||
NewPosition = newPosition;
|
||||
}
|
||||
}
|
||||
|
||||
public class ControlResizedEventArgs
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
7
Dashboard.Common/Events/WindowEvent.cs
Normal file
7
Dashboard.Common/Events/WindowEvent.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Dashboard.Events
|
||||
{
|
||||
public class WindowCloseEvent() : UiEventArgs(UiEventType.WindowClose)
|
||||
{
|
||||
public bool Cancel { get; set; } = false;
|
||||
}
|
||||
}
|
||||
41
Dashboard.Common/FontProperties.cs
Normal file
41
Dashboard.Common/FontProperties.cs
Normal file
@ -0,0 +1,41 @@
|
||||
namespace Dashboard
|
||||
{
|
||||
public enum FontWeight
|
||||
{
|
||||
_100 = 100,
|
||||
_200 = 200,
|
||||
_300 = 300,
|
||||
_400 = 400,
|
||||
_500 = 500,
|
||||
_600 = 600,
|
||||
_700 = 700,
|
||||
_800 = 800,
|
||||
_900 = 900,
|
||||
|
||||
Thin = _100,
|
||||
Normal = _400,
|
||||
Bold = _600,
|
||||
Heavy = _900,
|
||||
}
|
||||
|
||||
public enum FontSlant
|
||||
{
|
||||
Normal,
|
||||
Italic,
|
||||
Oblique,
|
||||
}
|
||||
|
||||
public enum FontStretch
|
||||
{
|
||||
UltraCondensed = 500,
|
||||
ExtraCondensed = 625,
|
||||
Condensed = 750,
|
||||
SemiCondensed = 875,
|
||||
Normal = 1000,
|
||||
SemiExpanded = 1125,
|
||||
Expanded = 1250,
|
||||
ExtraExpanded = 1500,
|
||||
UltraExpanded = 2000,
|
||||
}
|
||||
|
||||
}
|
||||
227
Dashboard.Common/Gradient.cs
Normal file
227
Dashboard.Common/Gradient.cs
Normal file
@ -0,0 +1,227 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Dashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// Enumeration of the kinds of gradients available.
|
||||
/// </summary>
|
||||
public enum GradientType
|
||||
{
|
||||
/// <summary>
|
||||
/// A gradient which transitions over a set axis.
|
||||
/// </summary>
|
||||
Axial,
|
||||
/// <summary>
|
||||
/// A gradient which transitions along elliptical curves.
|
||||
/// </summary>
|
||||
Radial,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single gradient stop.
|
||||
/// </summary>
|
||||
/// <param name="Position">The position of the gradient stop. Must be [0,1].</param>
|
||||
/// <param name="Color">The color value for the stop.</param>
|
||||
public record struct GradientStop(float Position, Color Color);
|
||||
|
||||
/// <summary>
|
||||
/// Represents a linear gradient.
|
||||
/// </summary>
|
||||
public struct Gradient : ICollection<GradientStop>, ICloneable, IEquatable<Gradient>
|
||||
{
|
||||
private readonly List<GradientStop> _stops = new List<GradientStop>();
|
||||
|
||||
/// <summary>
|
||||
/// Gradient type.
|
||||
/// </summary>
|
||||
public GradientType Type { get; set; } = GradientType.Axial;
|
||||
|
||||
/// <summary>
|
||||
/// First gradient control point.
|
||||
/// </summary>
|
||||
public Vector2 C0 { get; set; } = Vector2.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Second gradient control point.
|
||||
/// </summary>
|
||||
public Vector2 C1 { get; set; } = Vector2.One;
|
||||
|
||||
/// <summary>
|
||||
/// Number of stops in a gradient.
|
||||
/// </summary>
|
||||
public int Count => _stops.Count;
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
/// <summary>
|
||||
/// Get a gradient control point.
|
||||
/// </summary>
|
||||
/// <param name="index">The index to get the control point for.</param>
|
||||
public GradientStop this[int index]
|
||||
{
|
||||
get => _stops[index];
|
||||
set
|
||||
{
|
||||
RemoveAt(index);
|
||||
Add(value);
|
||||
}
|
||||
}
|
||||
|
||||
public Gradient()
|
||||
{
|
||||
}
|
||||
|
||||
public Gradient(Color a, Color b)
|
||||
{
|
||||
Add(new GradientStop(0, a));
|
||||
Add(new GradientStop(1, b));
|
||||
}
|
||||
|
||||
public Gradient(IEnumerable<GradientStop> stops)
|
||||
{
|
||||
_stops.AddRange(stops);
|
||||
|
||||
if (_stops.Any(x => x.Position < 0 || x.Position > 1))
|
||||
throw new Exception("Gradient stop positions must be in the range [0, 1].");
|
||||
|
||||
_stops.Sort((a, b) => a.Position.CompareTo(b.Position));
|
||||
}
|
||||
|
||||
public Color GetColor(float position)
|
||||
{
|
||||
if (Count == 0)
|
||||
return Color.Black;
|
||||
else if (Count == 1)
|
||||
return _stops[0].Color;
|
||||
|
||||
int pivot = _stops.FindIndex(x => x.Position < position);
|
||||
|
||||
GradientStop left, right;
|
||||
if (pivot == -1)
|
||||
{
|
||||
left = right = _stops[^1];
|
||||
}
|
||||
else if (pivot == 0)
|
||||
{
|
||||
left = right = _stops[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
left = _stops[pivot-1];
|
||||
right = _stops[pivot];
|
||||
}
|
||||
|
||||
float weight = (position - left.Position) / (right.Position - left.Position);
|
||||
|
||||
Vector4 lcolor = new Vector4(left.Color.R, left.Color.G, left.Color.B, left.Color.A) * (1-weight);
|
||||
Vector4 rcolor = new Vector4(right.Color.R, right.Color.G, right.Color.B, right.Color.A) * weight;
|
||||
Vector4 color = lcolor + rcolor;
|
||||
|
||||
return Color.FromArgb((byte)color.W, (byte)color.X, (byte)color.Y, (byte)color.Z);
|
||||
}
|
||||
|
||||
public Gradient Clone()
|
||||
{
|
||||
Gradient gradient = new Gradient()
|
||||
{
|
||||
Type = Type,
|
||||
C0 = C0,
|
||||
C1 = C1,
|
||||
};
|
||||
|
||||
foreach (GradientStop stop in _stops)
|
||||
{
|
||||
gradient.Add(stop);
|
||||
}
|
||||
|
||||
return gradient;
|
||||
}
|
||||
|
||||
object ICloneable.Clone()
|
||||
{
|
||||
return Clone();
|
||||
}
|
||||
|
||||
public IEnumerator<GradientStop> GetEnumerator()
|
||||
{
|
||||
return _stops.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return ((IEnumerable)_stops).GetEnumerator();
|
||||
}
|
||||
|
||||
public void Add(GradientStop item)
|
||||
{
|
||||
if (item.Position < 0 || item.Position > 1)
|
||||
throw new Exception("Gradient stop positions must be in the range [0, 1].");
|
||||
|
||||
int index = _stops.FindIndex(x => x.Position > item.Position);
|
||||
if (index == -1)
|
||||
index = _stops.Count;
|
||||
|
||||
_stops.Insert(index, item);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_stops.Clear();
|
||||
}
|
||||
|
||||
public bool Contains(GradientStop item)
|
||||
{
|
||||
return _stops.Contains(item);
|
||||
}
|
||||
|
||||
public void CopyTo(GradientStop[] array, int arrayIndex)
|
||||
{
|
||||
_stops.CopyTo(array, arrayIndex);
|
||||
}
|
||||
|
||||
public bool Remove(GradientStop item)
|
||||
{
|
||||
return _stops.Remove(item);
|
||||
}
|
||||
|
||||
public void RemoveAt(int index)
|
||||
{
|
||||
_stops.RemoveAt(index);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
HashCode code = new HashCode();
|
||||
code.Add(Count);
|
||||
foreach (GradientStop item in this)
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Dashboard.Common/HashList.cs
Normal file
38
Dashboard.Common/HashList.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Dashboard
|
||||
{
|
||||
public class HashList<T> : IReadOnlyList<T>
|
||||
where T : notnull
|
||||
{
|
||||
private readonly List<T> _list = new List<T>();
|
||||
private readonly Dictionary<T, int> _map = new Dictionary<T, int>();
|
||||
|
||||
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<T> GetEnumerator() => _list.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => _list.GetEnumerator();
|
||||
}
|
||||
}
|
||||
211
Dashboard.Common/ImageProperties.cs
Normal file
211
Dashboard.Common/ImageProperties.cs
Normal file
@ -0,0 +1,211 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Dashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// Pixel format for images.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum PixelFormat
|
||||
{
|
||||
None = 0,
|
||||
|
||||
R8I = R | I8,
|
||||
Rg8I = Rg | I8,
|
||||
Rgb8I = Rgb | I8,
|
||||
Rgba8I = Rgba | I8,
|
||||
R16F = R | F16,
|
||||
Rg16F = Rg | F16,
|
||||
Rgb16F = Rgb | F16,
|
||||
Rgba16F = Rgba | F16,
|
||||
|
||||
// Channels
|
||||
R = 0x01,
|
||||
Rg = 0x02,
|
||||
Rgb = 0x03,
|
||||
Rgba = 0x04,
|
||||
A = 0x05,
|
||||
ColorMask = 0x0F,
|
||||
|
||||
I8 = 0x10,
|
||||
F16 = 0x20,
|
||||
TypeMask = 0xF0,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Color channels for images.
|
||||
/// </summary>
|
||||
public enum ColorChannel
|
||||
{
|
||||
/// <summary>
|
||||
/// The zero channel. Used for swizzle masks.
|
||||
/// </summary>
|
||||
Zero = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The one channel. Used for swizzle masks.
|
||||
/// </summary>
|
||||
One = 1,
|
||||
|
||||
/// <summary>
|
||||
/// An invalid swizzle mask.
|
||||
/// </summary>
|
||||
Unknown = 2,
|
||||
|
||||
/// <summary>
|
||||
/// The red channel.
|
||||
/// </summary>
|
||||
Red = 4,
|
||||
|
||||
/// <summary>
|
||||
/// The green channel.
|
||||
/// </summary>
|
||||
Green = 5,
|
||||
|
||||
/// <summary>
|
||||
/// The blue channel.
|
||||
/// </summary>
|
||||
Blue = 6,
|
||||
|
||||
/// <summary>
|
||||
/// The alpha channel.
|
||||
/// </summary>
|
||||
Alpha = 7,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines a image swizzle mask.
|
||||
/// </summary>
|
||||
public struct ColorSwizzle : IEquatable<ColorSwizzle>
|
||||
{
|
||||
public short Mask;
|
||||
|
||||
private const int MASK = 7;
|
||||
private const int RBIT = 0;
|
||||
private const int GBIT = 3;
|
||||
private const int BBIT = 6;
|
||||
private const int ABIT = 9;
|
||||
|
||||
/// <summary>
|
||||
/// Swizzle the red channel.
|
||||
/// </summary>
|
||||
public ColorChannel R
|
||||
{
|
||||
get => (ColorChannel)((Mask >> RBIT) & MASK);
|
||||
set => Mask = (short)(((int)value << RBIT) | (Mask & ~(MASK << RBIT)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Swizzle the green channel.
|
||||
/// </summary>
|
||||
public ColorChannel G
|
||||
{
|
||||
get => (ColorChannel)((Mask >> GBIT) & MASK);
|
||||
set => Mask = (short)(((int)value << GBIT) | (Mask & ~(MASK << GBIT)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Swizzle the blue channel.
|
||||
/// </summary>
|
||||
public ColorChannel B
|
||||
{
|
||||
get => (ColorChannel)((Mask >> BBIT) & MASK);
|
||||
set => Mask = (short)(((int)value << BBIT) | (Mask & ~(MASK << BBIT)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Swizzle the alpha channel.
|
||||
/// </summary>
|
||||
public ColorChannel A
|
||||
{
|
||||
get => (ColorChannel)((Mask >> ABIT) & MASK);
|
||||
set => Mask = (short)(((int)value << ABIT) | (Mask & ~(MASK << ABIT)));
|
||||
}
|
||||
|
||||
public ColorSwizzle(short mask)
|
||||
{
|
||||
Mask = mask;
|
||||
}
|
||||
|
||||
public ColorSwizzle(ColorChannel r, ColorChannel g, ColorChannel b, ColorChannel a)
|
||||
{
|
||||
Mask = (short)(((int)r << RBIT) | ((int)g << GBIT) | ((int)b << BBIT) | ((int)a << ABIT));
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{GetChannelChar(R)}{GetChannelChar(G)}{GetChannelChar(B)}{GetChannelChar(A)}";
|
||||
|
||||
char GetChannelChar(ColorChannel channel) => channel switch
|
||||
{
|
||||
ColorChannel.Zero => '0',
|
||||
ColorChannel.Red => 'R',
|
||||
ColorChannel.Green => 'G',
|
||||
ColorChannel.Blue => 'B',
|
||||
ColorChannel.Alpha => 'A',
|
||||
ColorChannel.One => '1',
|
||||
_ => '?',
|
||||
};
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Mask.GetHashCode();
|
||||
}
|
||||
|
||||
public override bool Equals([NotNullWhen(true)] object? obj)
|
||||
{
|
||||
return obj is ColorSwizzle other && Equals(other);
|
||||
}
|
||||
|
||||
public bool Equals(ColorSwizzle other)
|
||||
{
|
||||
return Mask == other.Mask;
|
||||
}
|
||||
|
||||
public static readonly ColorSwizzle Default = Parse("RGBA");
|
||||
public static readonly ColorSwizzle White = Parse("1111");
|
||||
public static readonly ColorSwizzle Black = Parse("0001");
|
||||
public static readonly ColorSwizzle Transparent = Parse("0000");
|
||||
public static readonly ColorSwizzle RedToGrayscale = Parse("RRR1");
|
||||
public static readonly ColorSwizzle RedToWhiteAlpha = Parse("111A");
|
||||
|
||||
public static bool TryParse(ReadOnlySpan<char> str, out ColorSwizzle value)
|
||||
{
|
||||
if (str.Length < 4)
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
ColorChannel r = GetChannelFromChar(str[0]);
|
||||
ColorChannel g = GetChannelFromChar(str[1]);
|
||||
ColorChannel b = GetChannelFromChar(str[2]);
|
||||
ColorChannel a = GetChannelFromChar(str[3]);
|
||||
|
||||
value = new ColorSwizzle(r, g, b, a);
|
||||
return true;
|
||||
|
||||
ColorChannel GetChannelFromChar(char chr) => chr switch
|
||||
{
|
||||
'0' => ColorChannel.Zero,
|
||||
'R' => ColorChannel.Red,
|
||||
'G' => ColorChannel.Green,
|
||||
'B' => ColorChannel.Blue,
|
||||
'A' => ColorChannel.Alpha,
|
||||
'1' => ColorChannel.One,
|
||||
_ => ColorChannel.Unknown,
|
||||
};
|
||||
}
|
||||
|
||||
public static ColorSwizzle Parse(ReadOnlySpan<char> str) =>
|
||||
TryParse(str, out ColorSwizzle value) ? value : throw new FormatException(nameof(str));
|
||||
|
||||
public static bool operator ==(ColorSwizzle left, ColorSwizzle right) =>
|
||||
left.Mask == right.Mask;
|
||||
|
||||
public static bool operator !=(ColorSwizzle left, ColorSwizzle right) =>
|
||||
left.Mask != right.Mask;
|
||||
}
|
||||
}
|
||||
23
Dashboard.Common/LineProperties.cs
Normal file
23
Dashboard.Common/LineProperties.cs
Normal file
@ -0,0 +1,23 @@
|
||||
namespace Dashboard
|
||||
{
|
||||
public enum BorderKind
|
||||
{
|
||||
Inset = -1,
|
||||
Center = 0,
|
||||
Outset = 1,
|
||||
}
|
||||
|
||||
public enum CapType
|
||||
{
|
||||
None,
|
||||
Circular,
|
||||
Rectangular,
|
||||
}
|
||||
|
||||
public enum CuspType
|
||||
{
|
||||
None,
|
||||
Circular,
|
||||
Rectangular,
|
||||
}
|
||||
}
|
||||
86
Dashboard.Common/Measure.cs
Normal file
86
Dashboard.Common/Measure.cs
Normal file
@ -0,0 +1,86 @@
|
||||
namespace Dashboard
|
||||
{
|
||||
public enum MeasurementUnit
|
||||
{
|
||||
/// <summary>
|
||||
/// The default unit. A size of a single picture element.
|
||||
/// </summary>
|
||||
Pixel,
|
||||
/// <summary>
|
||||
/// 1/72th of an inch traditional in graphics design.
|
||||
/// </summary>
|
||||
Point,
|
||||
/// <summary>
|
||||
/// The universal length unit for small distances.
|
||||
/// </summary>
|
||||
Millimeter,
|
||||
/// <summary>
|
||||
/// An inverse proportional unit with respect to the container size.
|
||||
/// </summary>
|
||||
Star,
|
||||
/// <summary>
|
||||
/// A directly proportional unit with respect to the container size.
|
||||
/// </summary>
|
||||
Percent,
|
||||
}
|
||||
|
||||
public record struct AdvancedMetric(MeasurementUnit Unit, float Value)
|
||||
{
|
||||
public AdvancedMetric Convert(MeasurementUnit target, float dpi, float rel, int stars)
|
||||
{
|
||||
if (Unit == target)
|
||||
return this;
|
||||
|
||||
float pixels = Unit switch {
|
||||
MeasurementUnit.Pixel => Value,
|
||||
MeasurementUnit.Point => Value * (72f / dpi),
|
||||
MeasurementUnit.Millimeter => Value * (28.3464566929f / dpi),
|
||||
MeasurementUnit.Star => Value * rel / stars,
|
||||
MeasurementUnit.Percent => Value * rel / 100,
|
||||
_ => throw new Exception(),
|
||||
};
|
||||
|
||||
float value = target switch {
|
||||
MeasurementUnit.Pixel => pixels,
|
||||
MeasurementUnit.Point => Value * (dpi / 72f),
|
||||
// MeasurementUnit.Millimeter =>
|
||||
};
|
||||
|
||||
return new AdvancedMetric(target, value);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Value} {Unit.ToShortString()}";
|
||||
}
|
||||
|
||||
public static bool TryParse(ReadOnlySpan<char> str, out AdvancedMetric metric)
|
||||
{
|
||||
metric = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static AdvancedMetric Parse(ReadOnlySpan<char> str) =>
|
||||
TryParse(str, out AdvancedMetric metric)
|
||||
? metric
|
||||
: throw new Exception($"Could not parse the value '{str}'.");
|
||||
}
|
||||
|
||||
public static class MeasurementExtensions
|
||||
{
|
||||
public static bool IsRelative(this MeasurementUnit unit) => unit switch {
|
||||
MeasurementUnit.Star or MeasurementUnit.Percent => true,
|
||||
_ => false,
|
||||
};
|
||||
public static bool IsAbsolute(this MeasurementUnit unit) => !IsRelative(unit);
|
||||
|
||||
public static string ToShortString(this MeasurementUnit unit) => unit switch {
|
||||
MeasurementUnit.Pixel => "px",
|
||||
MeasurementUnit.Point => "pt",
|
||||
MeasurementUnit.Millimeter => "mm",
|
||||
MeasurementUnit.Star => "*",
|
||||
MeasurementUnit.Percent => "%",
|
||||
_ => throw new Exception("Unknown unit."),
|
||||
};
|
||||
}
|
||||
}
|
||||
17
Dashboard.Common/Origin.cs
Normal file
17
Dashboard.Common/Origin.cs
Normal file
@ -0,0 +1,17 @@
|
||||
namespace Dashboard
|
||||
{
|
||||
public enum Origin
|
||||
{
|
||||
Center = 0,
|
||||
|
||||
Left = (1 << 0),
|
||||
Top = (1 << 1),
|
||||
Right = (1 << 2),
|
||||
Bottom = (1 << 3),
|
||||
|
||||
TopLeft = Top | Left,
|
||||
BottomLeft = Bottom | Left,
|
||||
BottomRight = Bottom | Right,
|
||||
TopRight = Top | Right,
|
||||
}
|
||||
}
|
||||
195
Dashboard.Common/Pal/Application.cs
Normal file
195
Dashboard.Common/Pal/Application.cs
Normal file
@ -0,0 +1,195 @@
|
||||
using Dashboard.Collections;
|
||||
using Dashboard.Windowing;
|
||||
using BindingFlags = System.Reflection.BindingFlags;
|
||||
|
||||
namespace Dashboard.Pal
|
||||
{
|
||||
public abstract class Application : IContextBase<Application, IApplicationExtension>
|
||||
{
|
||||
public abstract string DriverName { get; }
|
||||
public abstract string DriverVendor { get; }
|
||||
public abstract Version DriverVersion { get; }
|
||||
|
||||
public virtual string AppTitle { get; set; } = "Dashboard Application";
|
||||
|
||||
public bool IsInitialized { get; private set; } = false;
|
||||
public bool IsDisposed { get; private set; } = false;
|
||||
public IContextDebugger? Debugger { get; set; }
|
||||
|
||||
private readonly TypeDictionary<IApplicationExtension> _extensions =
|
||||
new TypeDictionary<IApplicationExtension>(true);
|
||||
private readonly TypeDictionary<IApplicationExtension, Func<IApplicationExtension>> _preloadedExtensions =
|
||||
new TypeDictionary<IApplicationExtension, Func<IApplicationExtension>>(true);
|
||||
|
||||
public event EventHandler<DeviceContext>? DeviceContextCreated;
|
||||
|
||||
public Application()
|
||||
{
|
||||
Current = this;
|
||||
}
|
||||
|
||||
~Application()
|
||||
{
|
||||
InvokeDispose(false);
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
if (IsInitialized)
|
||||
return;
|
||||
|
||||
IsInitialized = true;
|
||||
|
||||
InitializeInternal();
|
||||
}
|
||||
|
||||
protected virtual void InitializeInternal()
|
||||
{
|
||||
}
|
||||
|
||||
protected internal virtual void OnDeviceContextCreated(DeviceContext dc)
|
||||
{
|
||||
DeviceContextCreated?.Invoke(this, dc);
|
||||
}
|
||||
|
||||
public virtual void RunEvents(bool wait)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
Initialize();
|
||||
}
|
||||
|
||||
public void Run() => Run(true, CancellationToken.None);
|
||||
|
||||
public void Run(bool wait) => Run(wait, CancellationToken.None);
|
||||
|
||||
public void Run(bool waitForEvents, CancellationToken token)
|
||||
{
|
||||
InitializeInternal();
|
||||
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
RunEvents(waitForEvents);
|
||||
}
|
||||
}
|
||||
|
||||
#region Window API
|
||||
/// <summary>
|
||||
/// Creates a window. It could be a virtual window, or a physical window.
|
||||
/// </summary>
|
||||
/// <returns>A window.</returns>
|
||||
public abstract IWindow CreateWindow();
|
||||
|
||||
/// <summary>
|
||||
/// Always creates a physical window.
|
||||
/// </summary>
|
||||
/// <returns>A physical window.</returns>
|
||||
public abstract IPhysicalWindow CreatePhysicalWindow();
|
||||
|
||||
/// <summary>
|
||||
/// Create a physical window with a window manager.
|
||||
/// </summary>
|
||||
/// <returns>A physical window with the given window manager.</returns>
|
||||
public IPhysicalWindow CreatePhysicalWindow(IWindowManager wm)
|
||||
{
|
||||
IPhysicalWindow window = CreatePhysicalWindow();
|
||||
window.WindowManager = wm;
|
||||
return window;
|
||||
}
|
||||
|
||||
public IWindow CreateDialogWindow(IWindow? parent = null)
|
||||
{
|
||||
if (parent is IVirtualWindow virtualWindow)
|
||||
{
|
||||
IWindow? window = virtualWindow.WindowManager?.CreateWindow();
|
||||
|
||||
if (window != null)
|
||||
return window;
|
||||
}
|
||||
|
||||
return CreatePhysicalWindow();
|
||||
}
|
||||
#endregion
|
||||
|
||||
public bool IsExtensionAvailable<T>() where T : IApplicationExtension
|
||||
{
|
||||
return _extensions.Contains<T>() || _preloadedExtensions.Contains<T>();
|
||||
}
|
||||
|
||||
public bool ExtensionPreload<T>(Func<IApplicationExtension> loader) where T : IApplicationExtension
|
||||
{
|
||||
return _preloadedExtensions.Add<T>(loader);
|
||||
}
|
||||
|
||||
public bool ExtensionPreload<T>() where T : IApplicationExtension, new()
|
||||
{
|
||||
return _preloadedExtensions.Add<T>(() => new T());
|
||||
}
|
||||
|
||||
public T ExtensionRequire<T>() where T : IApplicationExtension
|
||||
{
|
||||
T? extension = default;
|
||||
|
||||
if (_extensions.TryGet(out extension))
|
||||
return extension;
|
||||
|
||||
lock (_extensions)
|
||||
{
|
||||
if (_extensions.TryGet(out extension))
|
||||
return extension;
|
||||
|
||||
if (_preloadedExtensions.Remove<T>(out Func<IApplicationExtension>? loader))
|
||||
{
|
||||
extension = (T)loader!();
|
||||
}
|
||||
else
|
||||
{
|
||||
extension = Activator.CreateInstance<T>();
|
||||
}
|
||||
|
||||
_extensions.Add(extension);
|
||||
extension.Require(this);
|
||||
}
|
||||
|
||||
return extension;
|
||||
}
|
||||
|
||||
public bool ExtensionLoad<T>(T instance) where T : IApplicationExtension
|
||||
{
|
||||
if (_extensions.Contains(instance))
|
||||
return false;
|
||||
|
||||
_extensions.Add(instance);
|
||||
instance.Require(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool isDisposing)
|
||||
{
|
||||
if (!isDisposing) return;
|
||||
|
||||
foreach (IApplicationExtension extension in _extensions)
|
||||
{
|
||||
extension.Dispose();
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void InvokeDispose(bool isDisposing)
|
||||
{
|
||||
if (IsDisposed) return;
|
||||
IsDisposed = true;
|
||||
|
||||
Dispose(isDisposing);
|
||||
}
|
||||
|
||||
public void Dispose() => InvokeDispose(true);
|
||||
|
||||
[ThreadStatic] private static Application _current;
|
||||
public static Application Current
|
||||
{
|
||||
get => _current ?? throw new InvalidOperationException("There is currently no current application.");
|
||||
set => _current = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
149
Dashboard.Common/Pal/DeviceContext.cs
Normal file
149
Dashboard.Common/Pal/DeviceContext.cs
Normal file
@ -0,0 +1,149 @@
|
||||
using Dashboard.Collections;
|
||||
using Dashboard.Windowing;
|
||||
using BindingFlags = System.Reflection.BindingFlags;
|
||||
|
||||
namespace Dashboard.Pal
|
||||
{
|
||||
public abstract class DeviceContext : IContextBase<DeviceContext, IDeviceContextExtension>
|
||||
{
|
||||
private readonly TypeDictionary<IDeviceContextExtension> _extensions =
|
||||
new TypeDictionary<IDeviceContextExtension>(true);
|
||||
private readonly TypeDictionary<IDeviceContextExtension, Func<IDeviceContextExtension>> _preloadedExtensions =
|
||||
new TypeDictionary<IDeviceContextExtension, Func<IDeviceContextExtension>>(true);
|
||||
|
||||
private readonly Dictionary<string, object> _attributes = new Dictionary<string, object>();
|
||||
|
||||
|
||||
public Application Application { get; }
|
||||
public IWindow? Window { get; }
|
||||
public abstract string DriverName { get; }
|
||||
public abstract string DriverVendor { get; }
|
||||
public abstract Version DriverVersion { get; }
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional debugging object for your pleasure.
|
||||
/// </summary>
|
||||
public IContextDebugger? Debugger { get; set; }
|
||||
|
||||
protected DeviceContext(Application app, IWindow? window)
|
||||
{
|
||||
Application = app;
|
||||
Window = window;
|
||||
app.OnDeviceContextCreated(this);
|
||||
}
|
||||
|
||||
~DeviceContext()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public virtual void Begin() { }
|
||||
|
||||
// public abstract void Paint(object renderbuffer);
|
||||
|
||||
public virtual void End() { }
|
||||
|
||||
public bool IsExtensionAvailable<T>() where T : IDeviceContextExtension
|
||||
{
|
||||
return _extensions.Contains<T>() || _preloadedExtensions.Contains<T>();
|
||||
}
|
||||
|
||||
public bool ExtensionPreload<T>(Func<IDeviceContextExtension> loader) where T : IDeviceContextExtension
|
||||
{
|
||||
return _preloadedExtensions.Add<T>(loader);
|
||||
}
|
||||
|
||||
public bool ExtensionPreload<T>() where T : IDeviceContextExtension, new()
|
||||
{
|
||||
return _preloadedExtensions.Add<T>(() => new T());
|
||||
}
|
||||
|
||||
public T ExtensionRequire<T>() where T : IDeviceContextExtension
|
||||
{
|
||||
T? extension = default;
|
||||
|
||||
if (_extensions.TryGet(out extension))
|
||||
return extension;
|
||||
|
||||
lock (_extensions)
|
||||
{
|
||||
if (_extensions.TryGet(out extension))
|
||||
return extension;
|
||||
|
||||
if (_preloadedExtensions.Remove<T>(out Func<IDeviceContextExtension>? loader))
|
||||
{
|
||||
extension = (T)loader!();
|
||||
}
|
||||
else
|
||||
{
|
||||
extension = Activator.CreateInstance<T>();
|
||||
}
|
||||
|
||||
_extensions.Add(extension);
|
||||
extension.Require(this);
|
||||
}
|
||||
|
||||
return extension;
|
||||
}
|
||||
|
||||
public bool ExtensionLoad<T>(T instance) where T : IDeviceContextExtension
|
||||
{
|
||||
if (_extensions.Contains(instance))
|
||||
return false;
|
||||
|
||||
_extensions.Add(instance);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SetAttribute(string name, object? v)
|
||||
{
|
||||
if (v != null)
|
||||
_attributes[name] = v;
|
||||
else
|
||||
_attributes.Remove(name);
|
||||
}
|
||||
|
||||
public void SetAttribute<T>(string name, T v) => SetAttribute(name, (object?)v);
|
||||
|
||||
public object? GetAttribute(string name)
|
||||
{
|
||||
return _attributes.GetValueOrDefault(name);
|
||||
}
|
||||
|
||||
public T? GetAttribute<T>(string name)
|
||||
{
|
||||
object? o = GetAttribute(name);
|
||||
if (o != null)
|
||||
return (T?)o;
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implement your dispose in this function.
|
||||
/// </summary>
|
||||
/// <param name="isDisposing">True if disposing, false otherwise.</param>
|
||||
protected virtual void Dispose(bool isDisposing)
|
||||
{
|
||||
if (!isDisposing) return;
|
||||
|
||||
foreach (IDeviceContextExtension extension in _extensions)
|
||||
{
|
||||
extension.Dispose();
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void InvokeDispose(bool isDisposing)
|
||||
{
|
||||
if (IsDisposed) return;
|
||||
IsDisposed = true;
|
||||
|
||||
Dispose(isDisposing);
|
||||
}
|
||||
|
||||
public void Dispose() => InvokeDispose(true);
|
||||
}
|
||||
}
|
||||
7
Dashboard.Common/Pal/IApplicationExtension.cs
Normal file
7
Dashboard.Common/Pal/IApplicationExtension.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Dashboard.Pal
|
||||
{
|
||||
public interface IApplicationExtension : IContextExtensionBase<Application>
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
122
Dashboard.Common/Pal/IContextBase.cs
Normal file
122
Dashboard.Common/Pal/IContextBase.cs
Normal file
@ -0,0 +1,122 @@
|
||||
namespace Dashboard.Pal
|
||||
{
|
||||
/// <summary>
|
||||
/// Information about this context interface.
|
||||
/// </summary>
|
||||
public interface IContextInterfaceInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of this driver.
|
||||
/// </summary>
|
||||
string DriverName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The vendor for this driver.
|
||||
/// </summary>
|
||||
string DriverVendor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The version of this driver.
|
||||
/// </summary>
|
||||
Version DriverVersion { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The base context interface.
|
||||
/// </summary>
|
||||
public interface IContextBase : IContextInterfaceInfo, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The debugger for this context.
|
||||
/// </summary>
|
||||
IContextDebugger? Debugger { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The base context interface.
|
||||
/// </summary>
|
||||
/// <typeparam name="TContext">The context type.</typeparam>
|
||||
/// <typeparam name="TExtension">The extension type, if used.</typeparam>
|
||||
public interface IContextBase<TContext, in TExtension> : IContextBase
|
||||
where TContext : IContextBase<TContext, TExtension>
|
||||
where TExtension : IContextExtensionBase<TContext>
|
||||
{
|
||||
/// <summary>
|
||||
/// Is such an extension available?
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The extension to check.</typeparam>
|
||||
/// <returns>True if the extension is available.</returns>
|
||||
bool IsExtensionAvailable<T>() where T : TExtension;
|
||||
|
||||
/// <summary>
|
||||
/// Preload extensions, to be lazy loaded when required.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The extension to preload.</typeparam>
|
||||
/// <returns>
|
||||
/// True if the extension was added to the preload set. Otherwise, already loaded or another extension
|
||||
/// exists which provides this.
|
||||
/// </returns>
|
||||
bool ExtensionPreload<T>() where T : TExtension, new();
|
||||
|
||||
/// <summary>
|
||||
/// Preload extensions, to be lazy loaded when required.
|
||||
/// </summary>
|
||||
/// <param name="loader">The loader delegate.</param>
|
||||
/// <typeparam name="T">The extension to preload.</typeparam>
|
||||
/// <returns>
|
||||
/// True if the extension was added to the preload set. Otherwise, already loaded or another extension
|
||||
/// exists which provides this.
|
||||
/// </returns>
|
||||
bool ExtensionPreload<T>(Func<TExtension> loader) where T : TExtension;
|
||||
|
||||
/// <summary>
|
||||
/// Require an extension.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The extension to require.</typeparam>
|
||||
/// <returns>The extension instance.</returns>
|
||||
T ExtensionRequire<T>() where T : TExtension;
|
||||
|
||||
/// <summary>
|
||||
/// Load an extension.
|
||||
/// </summary>
|
||||
/// <param name="instance">The extension instance.</param>
|
||||
/// <typeparam name="T">The extension to require.</typeparam>
|
||||
/// <returns>True if the extension was loaded, false if there was already one.</returns>
|
||||
bool ExtensionLoad<T>(T instance) where T : TExtension;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base interface for all context extensions.
|
||||
/// </summary>
|
||||
public interface IContextExtensionBase : IContextInterfaceInfo, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The context that loaded this extension.
|
||||
/// </summary>
|
||||
IContextBase Context { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Require this extension.
|
||||
/// </summary>
|
||||
/// <param name="context">The context that required this extension.</param>
|
||||
void Require(IContextBase context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base interface for all context extensions.
|
||||
/// </summary>
|
||||
public interface IContextExtensionBase<TContext> : IContextExtensionBase
|
||||
where TContext : IContextBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The context that loaded this extension.
|
||||
/// </summary>
|
||||
new TContext Context { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Require this extension.
|
||||
/// </summary>
|
||||
/// <param name="context">The context that required this extension.</param>
|
||||
void Require(TContext context);
|
||||
}
|
||||
}
|
||||
10
Dashboard.Common/Pal/IContextDebugger.cs
Normal file
10
Dashboard.Common/Pal/IContextDebugger.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Dashboard.Pal
|
||||
{
|
||||
public interface IContextDebugger : IDisposable
|
||||
{
|
||||
void LogDebug(string message);
|
||||
void LogInfo(string message);
|
||||
void LogWarning(string message);
|
||||
void LogError(string message);
|
||||
}
|
||||
}
|
||||
6
Dashboard.Common/Pal/IDeviceContextExtension.cs
Normal file
6
Dashboard.Common/Pal/IDeviceContextExtension.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Dashboard.Pal
|
||||
{
|
||||
public interface IDeviceContextExtension : IContextExtensionBase<DeviceContext>
|
||||
{
|
||||
}
|
||||
}
|
||||
43
Dashboard.Common/Windowing/ICompositor.cs
Normal file
43
Dashboard.Common/Windowing/ICompositor.cs
Normal file
@ -0,0 +1,43 @@
|
||||
namespace Dashboard.Windowing
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for a class that composites multiple windows together.
|
||||
/// </summary>
|
||||
public interface ICompositor : IDisposable
|
||||
{
|
||||
void Composite(IPhysicalWindow window, IEnumerable<IVirtualWindow> windows);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for classes that implement a window manager.
|
||||
/// </summary>
|
||||
public interface IWindowManager : IEnumerable<IVirtualWindow>, IEventListener
|
||||
{
|
||||
/// <summary>
|
||||
/// The physical window that this window manager is associated with.
|
||||
/// </summary>
|
||||
IPhysicalWindow PhysicalWindow { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The compositor that will composite all virtual windows.
|
||||
/// </summary>
|
||||
ICompositor Compositor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The window that is currently focused.
|
||||
/// </summary>
|
||||
IVirtualWindow? FocusedWindow { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a virtual window.
|
||||
/// </summary>
|
||||
/// <returns>Virtual window handle.</returns>
|
||||
IVirtualWindow CreateWindow();
|
||||
|
||||
/// <summary>
|
||||
/// Focus a virtual window, if it is owned by this window manager.
|
||||
/// </summary>
|
||||
/// <param name="window">The window to focus.</param>
|
||||
void Focus(IVirtualWindow window);
|
||||
}
|
||||
}
|
||||
20
Dashboard.Common/Windowing/IDeviceContext.cs
Normal file
20
Dashboard.Common/Windowing/IDeviceContext.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System.Drawing;
|
||||
|
||||
namespace Dashboard.Windowing
|
||||
{
|
||||
/// <summary>
|
||||
/// Generic interface for the rendering system present in a <see cref="IPhysicalWindow"/>
|
||||
/// </summary>
|
||||
public interface IDeviceContext
|
||||
{
|
||||
/// <summary>
|
||||
/// The swap group for this device context.
|
||||
/// </summary>
|
||||
ISwapGroup SwapGroup { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The size of the window framebuffer in pixels.
|
||||
/// </summary>
|
||||
Size FramebufferSize { get; }
|
||||
}
|
||||
}
|
||||
14
Dashboard.Common/Windowing/IEventListener.cs
Normal file
14
Dashboard.Common/Windowing/IEventListener.cs
Normal file
@ -0,0 +1,14 @@
|
||||
namespace Dashboard.Windowing
|
||||
{
|
||||
public interface IEventListener
|
||||
{
|
||||
event EventHandler? EventRaised;
|
||||
|
||||
/// <summary>
|
||||
/// Send an event to this windowing object.
|
||||
/// </summary>
|
||||
/// <param name="sender">The object which generated the event.</param>
|
||||
/// <param name="args">The event arguments sent.</param>
|
||||
void SendEvent(object? sender, EventArgs args);
|
||||
}
|
||||
}
|
||||
8
Dashboard.Common/Windowing/IForm.cs
Normal file
8
Dashboard.Common/Windowing/IForm.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Dashboard.Windowing
|
||||
{
|
||||
public interface IForm : IEventListener, IDisposable
|
||||
{
|
||||
public IWindow Window { get; }
|
||||
public string Title { get; }
|
||||
}
|
||||
}
|
||||
12
Dashboard.Common/Windowing/IPaintable.cs
Normal file
12
Dashboard.Common/Windowing/IPaintable.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace Dashboard.Windowing
|
||||
{
|
||||
public interface IPaintable
|
||||
{
|
||||
event EventHandler Painting;
|
||||
|
||||
/// <summary>
|
||||
/// Paint this paintable object.
|
||||
/// </summary>
|
||||
void Paint();
|
||||
}
|
||||
}
|
||||
18
Dashboard.Common/Windowing/ISwapGroup.cs
Normal file
18
Dashboard.Common/Windowing/ISwapGroup.cs
Normal file
@ -0,0 +1,18 @@
|
||||
namespace Dashboard.Windowing
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface that is used to swap the buffers of windows
|
||||
/// </summary>
|
||||
public interface ISwapGroup
|
||||
{
|
||||
/// <summary>
|
||||
/// The swap interval for this swap group.
|
||||
/// </summary>
|
||||
public int SwapInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Swap buffers.
|
||||
/// </summary>
|
||||
void Swap();
|
||||
}
|
||||
}
|
||||
93
Dashboard.Common/Windowing/IWindow.cs
Normal file
93
Dashboard.Common/Windowing/IWindow.cs
Normal file
@ -0,0 +1,93 @@
|
||||
using System.Drawing;
|
||||
using Dashboard.Pal;
|
||||
|
||||
namespace Dashboard.Windowing
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class of all Dashboard windows.
|
||||
/// </summary>
|
||||
public interface IWindow : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The application for this window.
|
||||
/// </summary>
|
||||
Application Application { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the window.
|
||||
/// </summary>
|
||||
string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The size of the window that includes the window extents.
|
||||
/// </summary>
|
||||
SizeF OuterSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The size of the window that excludes the window extents.
|
||||
/// </summary>
|
||||
SizeF ClientSize { get; set; }
|
||||
|
||||
IForm? Form { get; set; }
|
||||
|
||||
public event EventHandler? EventRaised;
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to events from this window.
|
||||
/// </summary>
|
||||
/// <param name="listener">The event listener instance.</param>
|
||||
/// <returns>An unsubscription token.</returns>
|
||||
public void SubcribeEvent(IEventListener listener);
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe from events in from this window.
|
||||
/// </summary>
|
||||
/// <param name="listener">The event listener to unsubscribe.</param>
|
||||
public void UnsubscribeEvent(IEventListener listener);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all Dashboard windows that are DPI-aware.
|
||||
/// </summary>
|
||||
public interface IDpiAwareWindow : IWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// DPI of the window.
|
||||
/// </summary>
|
||||
float Dpi { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Scale of the window.
|
||||
/// </summary>
|
||||
float Scale { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An object that represents a window in a virtual space, usually another window or a rendering system.
|
||||
/// </summary>
|
||||
public interface IVirtualWindow : IWindow, IEventListener
|
||||
{
|
||||
IWindowManager? WindowManager { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An object that represents a native operating system window.
|
||||
/// </summary>
|
||||
public interface IPhysicalWindow : IWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// The device context for this window.
|
||||
/// </summary>
|
||||
DeviceContext DeviceContext { get; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the window is double buffered.
|
||||
/// </summary>
|
||||
public bool DoubleBuffered { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The window manager for this physical window.
|
||||
/// </summary>
|
||||
public IWindowManager? WindowManager { get; set; }
|
||||
}
|
||||
}
|
||||
48
Dashboard.Drawing.OpenGL/CommandInfo.cs
Normal file
48
Dashboard.Drawing.OpenGL/CommandInfo.cs
Normal file
@ -0,0 +1,48 @@
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Dashboard.Drawing.OpenGL
|
||||
{
|
||||
public enum SimpleDrawCommand : int
|
||||
{
|
||||
Point = 1,
|
||||
Line = 2,
|
||||
Rect = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Make sure your custom commands have values higher than this if you plan on using the default command
|
||||
/// buffer.
|
||||
/// </summary>
|
||||
CustomCommandStart = 4096
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Size = 64)]
|
||||
public struct CommandInfo
|
||||
{
|
||||
[FieldOffset(0)]
|
||||
public SimpleDrawCommand Type;
|
||||
|
||||
[FieldOffset(4)]
|
||||
public int Flags;
|
||||
|
||||
[FieldOffset(8)]
|
||||
public float Arg0;
|
||||
[FieldOffset(12)]
|
||||
public float Arg1;
|
||||
|
||||
[FieldOffset(16)]
|
||||
public int FgGradientIndex;
|
||||
[FieldOffset(20)]
|
||||
public int FgGradientCount;
|
||||
[FieldOffset(24)]
|
||||
public int BgGradientIndex;
|
||||
[FieldOffset(28)]
|
||||
public int BgGradientCount;
|
||||
|
||||
[FieldOffset(32)]
|
||||
public Vector4 FgColor;
|
||||
|
||||
[FieldOffset(48)]
|
||||
public Vector4 BgColor;
|
||||
}
|
||||
}
|
||||
184
Dashboard.Drawing.OpenGL/ContextExecutor.cs
Normal file
184
Dashboard.Drawing.OpenGL/ContextExecutor.cs
Normal file
@ -0,0 +1,184 @@
|
||||
using System.Drawing;
|
||||
using Dashboard.Drawing.OpenGL.Executors;
|
||||
using Dashboard.OpenGL;
|
||||
using OpenTK.Mathematics;
|
||||
|
||||
namespace Dashboard.Drawing.OpenGL
|
||||
{
|
||||
public interface ICommandExecutor
|
||||
{
|
||||
IEnumerable<string> Extensions { get; }
|
||||
IContextExecutor Executor { get; }
|
||||
|
||||
void SetContextExecutor(IContextExecutor executor);
|
||||
|
||||
void BeginFrame();
|
||||
|
||||
void BeginDraw();
|
||||
|
||||
void EndDraw();
|
||||
|
||||
void EndFrame();
|
||||
|
||||
void ProcessCommand(ICommandFrame frame);
|
||||
}
|
||||
|
||||
public interface IContextExecutor : IInitializer, IGLDisposable
|
||||
{
|
||||
GLEngine Engine { get; }
|
||||
IGLContext Context { get; }
|
||||
ContextResourcePool ResourcePool { get; }
|
||||
TransformStack TransformStack { get; }
|
||||
}
|
||||
|
||||
public class ContextExecutor : IContextExecutor
|
||||
{
|
||||
public GLEngine Engine { get; }
|
||||
public IGLContext Context { get; }
|
||||
public ContextResourcePool ResourcePool { get; }
|
||||
public TransformStack TransformStack { get; } = new TransformStack();
|
||||
protected bool IsDisposed { get; private set; } = false;
|
||||
public bool IsInitialized { get; private set; } = false;
|
||||
|
||||
private readonly List<ICommandExecutor> _executorsList = new List<ICommandExecutor>();
|
||||
|
||||
private readonly Dictionary<string, ICommandExecutor> _executorsMap = new Dictionary<string, ICommandExecutor>();
|
||||
|
||||
public ContextExecutor(GLEngine engine, IGLContext context)
|
||||
{
|
||||
Engine = engine;
|
||||
Context = context;
|
||||
|
||||
ResourcePool = Engine.ResourcePoolManager.Get(context);
|
||||
ResourcePool.IncrementReference();
|
||||
|
||||
AddExecutor(new BaseCommandExecutor());
|
||||
AddExecutor(new TextCommandExecutor());
|
||||
}
|
||||
|
||||
~ContextExecutor()
|
||||
{
|
||||
DisposeInvoker(true, false);
|
||||
}
|
||||
|
||||
public void AddExecutor(ICommandExecutor executor, bool overwrite = false)
|
||||
{
|
||||
if (IsInitialized)
|
||||
throw new Exception("This context executor is already initialized. Cannot add new command executors.");
|
||||
|
||||
IInitializer? initializer = executor as IInitializer;
|
||||
|
||||
if (initializer?.IsInitialized == true)
|
||||
throw new InvalidOperationException("This command executor has already been initialized, cannot add here.");
|
||||
|
||||
if (!overwrite)
|
||||
{
|
||||
foreach (string extension in executor.Extensions)
|
||||
{
|
||||
if (_executorsMap.ContainsKey(extension))
|
||||
throw new InvalidOperationException("An executor already handles this extension.");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string extension in executor.Extensions)
|
||||
{
|
||||
_executorsMap[extension] = executor;
|
||||
}
|
||||
_executorsList.Add(executor);
|
||||
|
||||
executor.SetContextExecutor(this);
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
if (IsInitialized)
|
||||
return;
|
||||
IsInitialized = true;
|
||||
|
||||
foreach (ICommandExecutor executor in _executorsList)
|
||||
{
|
||||
if (executor is IInitializer initializer)
|
||||
initializer.Initialize();
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void BeginFrame()
|
||||
{
|
||||
foreach (ICommandExecutor executor in _executorsList)
|
||||
executor.BeginFrame();
|
||||
}
|
||||
|
||||
protected virtual void BeginDraw()
|
||||
{
|
||||
foreach (ICommandExecutor executor in _executorsList)
|
||||
executor.BeginDraw();
|
||||
}
|
||||
|
||||
protected virtual void EndDraw()
|
||||
{
|
||||
foreach (ICommandExecutor executor in _executorsList)
|
||||
executor.EndDraw();
|
||||
}
|
||||
|
||||
public virtual void EndFrame()
|
||||
{
|
||||
ResourcePool.Collector.Dispose();
|
||||
TransformStack.Clear();
|
||||
|
||||
foreach (ICommandExecutor executor in _executorsList)
|
||||
executor.EndFrame();
|
||||
}
|
||||
|
||||
public void Draw(DrawQueue drawqueue) => Draw(drawqueue, new RectangleF(new PointF(0f,0f), Context.FramebufferSize));
|
||||
|
||||
public virtual void Draw(DrawQueue drawQueue, RectangleF bounds, float scale = 1.0f)
|
||||
{
|
||||
BeginDraw();
|
||||
|
||||
if (scale != 1.0f)
|
||||
TransformStack.Push(Matrix4.CreateScale(scale, scale, 1));
|
||||
|
||||
foreach (ICommandFrame frame in drawQueue)
|
||||
{
|
||||
if (_executorsMap.TryGetValue(frame.Command.Extension.Name, out ICommandExecutor? executor))
|
||||
executor.ProcessCommand(frame);
|
||||
}
|
||||
|
||||
EndDraw();
|
||||
}
|
||||
|
||||
private void DisposeInvoker(bool safeExit, bool disposing)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
return;
|
||||
|
||||
IsDisposed = true;
|
||||
|
||||
if (disposing)
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
Dispose(safeExit, disposing);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool safeExit, bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
foreach (ICommandExecutor executor in _executorsList)
|
||||
{
|
||||
if (executor is IGLDisposable glDisposable)
|
||||
glDisposable.Dispose(safeExit);
|
||||
else if (executor is IDisposable disposable)
|
||||
disposable.Dispose();
|
||||
}
|
||||
|
||||
if (ResourcePool.DecrementReference())
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => DisposeInvoker(true, true);
|
||||
|
||||
public void Dispose(bool safeExit) => DisposeInvoker(safeExit, true);
|
||||
}
|
||||
}
|
||||
133
Dashboard.Drawing.OpenGL/ContextResourcePool.cs
Normal file
133
Dashboard.Drawing.OpenGL/ContextResourcePool.cs
Normal file
@ -0,0 +1,133 @@
|
||||
using Dashboard.OpenGL;
|
||||
|
||||
namespace Dashboard.Drawing.OpenGL
|
||||
{
|
||||
public class ContextResourcePoolManager
|
||||
{
|
||||
private readonly Dictionary<IGLContext, ContextResourcePool> _unique = new Dictionary<IGLContext, ContextResourcePool>();
|
||||
private readonly Dictionary<int, ContextResourcePool> _shared = new Dictionary<int, ContextResourcePool>();
|
||||
|
||||
public ContextResourcePool Get(IGLContext context)
|
||||
{
|
||||
if (context.ContextGroup == -1)
|
||||
{
|
||||
if (!_unique.TryGetValue(context, out ContextResourcePool? pool))
|
||||
{
|
||||
pool = new ContextResourcePool(this, context);
|
||||
_unique.Add(context, pool);
|
||||
}
|
||||
|
||||
return pool;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_shared.TryGetValue(context.ContextGroup, out ContextResourcePool? pool))
|
||||
{
|
||||
pool = new ContextResourcePool(this, context.ContextGroup);
|
||||
_shared.Add(context.ContextGroup, pool);
|
||||
}
|
||||
|
||||
return pool;
|
||||
}
|
||||
}
|
||||
|
||||
internal void Disposed(ContextResourcePool pool)
|
||||
{
|
||||
// TODO:
|
||||
}
|
||||
}
|
||||
|
||||
public class ContextResourcePool : IGLDisposable, IArc
|
||||
{
|
||||
private int _references = 0;
|
||||
private bool _isDisposed = false;
|
||||
private readonly Dictionary<int, IResourceManager> _managers = new Dictionary<int, IResourceManager>();
|
||||
|
||||
public ContextResourcePoolManager Manager { get; }
|
||||
public IGLContext? Context { get; private set; } = null;
|
||||
public int ContextGroup { get; private set; } = -1;
|
||||
public int References => _references;
|
||||
public ContextCollector Collector { get; } = new ContextCollector();
|
||||
|
||||
internal ContextResourcePool(ContextResourcePoolManager manager, int contextGroup)
|
||||
{
|
||||
Manager = manager;
|
||||
ContextGroup = contextGroup;
|
||||
}
|
||||
|
||||
internal ContextResourcePool(ContextResourcePoolManager manager, IGLContext context)
|
||||
{
|
||||
Manager = manager;
|
||||
Context = context;
|
||||
}
|
||||
|
||||
public T GetResourceManager<T>(bool init = true) where T : IResourceManager, new()
|
||||
{
|
||||
int index = ManagerAtom<T>.Atom;
|
||||
|
||||
if (!_managers.TryGetValue(index, out IResourceManager? resourceClass))
|
||||
{
|
||||
_managers[index] = resourceClass = new T();
|
||||
}
|
||||
|
||||
if (init && resourceClass is IInitializer initializer)
|
||||
{
|
||||
initializer.Initialize();
|
||||
}
|
||||
|
||||
return (T)resourceClass;
|
||||
}
|
||||
|
||||
~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)
|
||||
{
|
||||
if (_isDisposed)
|
||||
return;
|
||||
_isDisposed = true;
|
||||
|
||||
Manager.Disposed(this);
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
foreach ((int _, IResourceManager manager) in _managers)
|
||||
{
|
||||
if (manager is IGLDisposable glDisposable)
|
||||
glDisposable.Dispose(safeExit);
|
||||
else if (manager is IDisposable disposable)
|
||||
disposable.Dispose();
|
||||
}
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void IncrementReference()
|
||||
{
|
||||
Interlocked.Increment(ref _references);
|
||||
}
|
||||
|
||||
public bool DecrementReference()
|
||||
{
|
||||
return Interlocked.Decrement(ref _references) == 0;
|
||||
}
|
||||
|
||||
|
||||
private class ManagerAtom
|
||||
{
|
||||
private static int _counter = -1;
|
||||
|
||||
protected static int Acquire() => Interlocked.Increment(ref _counter);
|
||||
}
|
||||
private class ManagerAtom<T> : ManagerAtom where T : IResourceManager
|
||||
{
|
||||
public static int Atom { get; } = Acquire();
|
||||
}
|
||||
}
|
||||
}
|
||||
30
Dashboard.Drawing.OpenGL/Dashboard.Drawing.OpenGL.csproj
Normal file
30
Dashboard.Drawing.OpenGL/Dashboard.Drawing.OpenGL.csproj
Normal file
@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BlurgText" Version="0.1.0-nightly-19" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Dashboard.Drawing\Dashboard.Drawing.csproj" />
|
||||
<ProjectReference Include="..\Dashboard.OpenGL\Dashboard.OpenGL.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Executors\simple.frag" />
|
||||
<EmbeddedResource Include="Executors\simple.vert" />
|
||||
<EmbeddedResource Include="Executors\text.vert" />
|
||||
<EmbeddedResource Include="Executors\text.frag" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Text\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
468
Dashboard.Drawing.OpenGL/DrawCallRecorder.cs
Normal file
468
Dashboard.Drawing.OpenGL/DrawCallRecorder.cs
Normal file
@ -0,0 +1,468 @@
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Dashboard.OpenGL;
|
||||
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<DrawVertex> _vertices = new List<DrawVertex>();
|
||||
private readonly List<DrawCall> _calls = new List<DrawCall>();
|
||||
|
||||
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<DrawVertex> vertices = CollectionsMarshal.AsSpan(_vertices);
|
||||
GL.BufferData(BufferTarget.ArrayBuffer, _vertices.Count * Unsafe.SizeOf<DrawVertex>(), 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A customizable immediate mode draw call queue, for the modern OpenGL user.
|
||||
/// </summary>
|
||||
/// <typeparam name="TCall">The call info type.</typeparam>
|
||||
/// <typeparam name="TVertex">The vertex structure.</typeparam>
|
||||
public abstract class DrawCallRecorder<TCall, TVertex> : IGLDisposable, IInitializer
|
||||
where TVertex : unmanaged
|
||||
{
|
||||
/// <summary>
|
||||
/// The vertex array for this queue.
|
||||
/// </summary>
|
||||
public int Vao { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The vertex buffer for this queue.
|
||||
/// </summary>
|
||||
public int Vbo { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of calls recorded in this queue.
|
||||
/// </summary>
|
||||
public int CallCount => Calls.Count;
|
||||
|
||||
/// <summary>
|
||||
/// The number of total vertices recorded.
|
||||
/// </summary>
|
||||
public int TotalVertices => Vertices.Count;
|
||||
|
||||
/// <summary>
|
||||
/// The latest draw call info.
|
||||
/// </summary>
|
||||
public ref TCall CurrentCall => ref _currentCall;
|
||||
|
||||
/// <summary>
|
||||
/// The latest vertex emitted.
|
||||
/// </summary>
|
||||
public ref TVertex CurrentVertex => ref _currentVertex;
|
||||
|
||||
/// <summary>
|
||||
/// True if currently recording a draw call.
|
||||
/// </summary>
|
||||
public bool InCall => _primitiveMode != 0;
|
||||
|
||||
/// <summary>
|
||||
/// Size of one vertex.
|
||||
/// </summary>
|
||||
protected int VertexSize => Unsafe.SizeOf<TVertex>();
|
||||
|
||||
/// <summary>
|
||||
/// The list of draw calls.
|
||||
/// </summary>
|
||||
protected List<DrawCall> Calls { get; } = new List<DrawCall>();
|
||||
|
||||
/// <summary>
|
||||
/// The list of all vertices.
|
||||
/// </summary>
|
||||
protected List<TVertex> Vertices { get; } = new List<TVertex>();
|
||||
|
||||
/// <summary>
|
||||
/// The value to write for the draw call info at the start of a call.
|
||||
/// </summary>
|
||||
[Pure] protected virtual TCall DefaultCall => default(TCall);
|
||||
|
||||
/// <summary>
|
||||
/// The value to write for last vertex at the start of a call.
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record a draw call directly.
|
||||
/// </summary>
|
||||
/// <param name="type">The primitive type to use.</param>
|
||||
/// <param name="callInfo">The call info structure to use</param>
|
||||
/// <param name="vertices">The list of vertices to use.</param>
|
||||
/// <exception cref="InvalidOperationException">You attempted to use this function during another draw call.</exception>
|
||||
public void DrawArrays(PrimitiveType type, in TCall callInfo, ReadOnlySpan<TVertex> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start a draw call.
|
||||
/// </summary>
|
||||
/// <param name="type">The primitive type for the call.</param>
|
||||
public void Begin(PrimitiveType type) => Begin(type, DefaultCall);
|
||||
|
||||
/// <summary>
|
||||
/// Start a draw call.
|
||||
/// </summary>
|
||||
/// <param name="type">The primitive type for the call.</param>
|
||||
/// <param name="callInfo">The call info.</param>
|
||||
/// <exception cref="InvalidOperationException">You attempted to create a draw call within a draw call.</exception>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emit the latest or modified vertex.
|
||||
/// </summary>
|
||||
public void Vertex()
|
||||
{
|
||||
Vertex(CurrentVertex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emit a vertex.
|
||||
/// </summary>
|
||||
/// <param name="vertex">The vertex to emit.</param>
|
||||
public void Vertex(in TVertex vertex)
|
||||
{
|
||||
Vertices.Add(CurrentVertex = vertex);
|
||||
_count++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// End the current call.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">You tried to end a call that you didn't begin recording.</exception>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the execution engine before a draw call is executed.
|
||||
/// </summary>
|
||||
/// <param name="call">The call to prepare.</param>
|
||||
protected abstract void PrepareCall(in TCall call);
|
||||
|
||||
/// <summary>
|
||||
/// Set the vertex format for the <see cref="Vao"/> and <see cref="Vbo"/> created by the recorder.
|
||||
/// </summary>
|
||||
protected abstract void SetVertexFormat();
|
||||
|
||||
/// <summary>
|
||||
/// Execute all the recorded draw calls.
|
||||
/// </summary>
|
||||
public void Execute()
|
||||
{
|
||||
GL.BindVertexArray(Vao);
|
||||
GL.BindBuffer(BufferTarget.ArrayBuffer, Vbo);
|
||||
|
||||
ReadOnlySpan<TVertex> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear the draw call queue.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
333
Dashboard.Drawing.OpenGL/Executors/BaseCommandExecutor.cs
Normal file
333
Dashboard.Drawing.OpenGL/Executors/BaseCommandExecutor.cs
Normal file
@ -0,0 +1,333 @@
|
||||
using System.Drawing;
|
||||
using OpenTK.Graphics.OpenGL;
|
||||
using System.Numerics;
|
||||
using Dashboard.OpenGL;
|
||||
using OTK = OpenTK.Mathematics;
|
||||
|
||||
namespace Dashboard.Drawing.OpenGL.Executors
|
||||
{
|
||||
public class BaseCommandExecutor : IInitializer, ICommandExecutor
|
||||
{
|
||||
private int _program = 0;
|
||||
private readonly MappableBumpAllocator<CommandInfo> _commands = new MappableBumpAllocator<CommandInfo>();
|
||||
private readonly DrawCallRecorder _calls = new DrawCallRecorder();
|
||||
|
||||
public bool IsInitialized { get; private set; }
|
||||
public IEnumerable<string> Extensions { get; } = new[] { "DB_base" };
|
||||
public IContextExecutor Executor { get; private set; } = null!;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
if (IsInitialized) return;
|
||||
|
||||
if (Executor == null)
|
||||
throw new Exception("Executor has not been set.");
|
||||
|
||||
IsInitialized = true;
|
||||
|
||||
LoadShaders();
|
||||
}
|
||||
|
||||
public void SetContextExecutor(IContextExecutor executor)
|
||||
{
|
||||
Executor = executor;
|
||||
}
|
||||
|
||||
public void BeginFrame()
|
||||
{
|
||||
}
|
||||
|
||||
public void BeginDraw()
|
||||
{
|
||||
_commands.Initialize();
|
||||
_calls.Initialize();
|
||||
|
||||
Size size = Executor.Context.FramebufferSize;
|
||||
|
||||
Executor.TransformStack.Push(OTK.Matrix4.CreateOrthographicOffCenter(
|
||||
0,
|
||||
size.Width,
|
||||
size.Height,
|
||||
0,
|
||||
1,
|
||||
-1));
|
||||
|
||||
GL.Viewport(0, 0, size.Width, size.Height);
|
||||
}
|
||||
|
||||
public void EndDraw()
|
||||
{
|
||||
_commands.Unmap();
|
||||
GL.UseProgram(_program);
|
||||
_calls.CommandBuffer = _commands.Handle;
|
||||
_calls.Execute();
|
||||
}
|
||||
|
||||
public void EndFrame()
|
||||
{
|
||||
_commands.Clear();
|
||||
_calls.Clear();
|
||||
}
|
||||
|
||||
public void ProcessCommand(ICommandFrame frame)
|
||||
{
|
||||
switch (frame.Command.Name)
|
||||
{
|
||||
case "Point":
|
||||
DrawBasePoint(frame);
|
||||
break;
|
||||
case "Line":
|
||||
DrawBaseLine(frame);
|
||||
break;
|
||||
case "RectF":
|
||||
case "RectS":
|
||||
case "RectFS":
|
||||
DrawRect(frame);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawBasePoint(ICommandFrame frame)
|
||||
{
|
||||
ref CommandInfo info = ref _commands.Take(out int index);
|
||||
|
||||
PointCommandArgs args = frame.GetParameter<PointCommandArgs>();
|
||||
|
||||
info = new CommandInfo()
|
||||
{
|
||||
Type = SimpleDrawCommand.Point,
|
||||
Arg0 = args.Size,
|
||||
};
|
||||
|
||||
SetCommandCommonBrush(ref info, args.Brush, args.Brush);
|
||||
|
||||
_calls.Transforms(Executor.TransformStack.Top);
|
||||
_calls.Begin(PrimitiveType.Triangles);
|
||||
_calls.CommandIndex(index);
|
||||
DrawPoint(args.Position, args.Depth, args.Size);
|
||||
_calls.End();
|
||||
}
|
||||
|
||||
private void DrawPoint(Vector2 position, float depth, float diameter)
|
||||
{
|
||||
// Draw a point as a isocles triangle.
|
||||
const float adjust = 1.1f;
|
||||
const float cos30 = 0.8660254038f;
|
||||
Vector2 top = adjust * new Vector2(0, -cos30);
|
||||
Vector2 left = adjust * new Vector2(-cos30, 0.5f);
|
||||
Vector2 right = adjust * new Vector2(cos30, 0.5f);
|
||||
|
||||
_calls.TexCoords2(top);
|
||||
_calls.Vertex3(new Vector3(position + top * diameter, depth));
|
||||
_calls.TexCoords2(left);
|
||||
_calls.Vertex3(new Vector3(position + left * diameter, depth));
|
||||
_calls.TexCoords2(right);
|
||||
_calls.Vertex3(new Vector3(position + right * diameter, depth));
|
||||
}
|
||||
|
||||
private void DrawBaseLine(ICommandFrame frame)
|
||||
{
|
||||
ref CommandInfo info = ref _commands.Take(out int index);
|
||||
|
||||
LineCommandArgs args = frame.GetParameter<LineCommandArgs>();
|
||||
|
||||
info = new CommandInfo()
|
||||
{
|
||||
Type = SimpleDrawCommand.Line,
|
||||
Arg0 = 0.5f * args.Size / (args.End - args.Start).Length(),
|
||||
};
|
||||
|
||||
SetCommandCommonBrush(ref info, args.Brush, args.Brush);
|
||||
|
||||
_calls.Transforms(Executor.TransformStack.Top);
|
||||
_calls.Begin(PrimitiveType.Triangles);
|
||||
|
||||
_calls.CommandIndex(index);
|
||||
|
||||
DrawLine(args.Start, args.End, args.Depth, args.Size);
|
||||
|
||||
_calls.End();
|
||||
}
|
||||
|
||||
private void DrawLine(Vector2 start, Vector2 end, float depth, float width)
|
||||
{
|
||||
float radius = 0.5f * width;
|
||||
Vector2 segment = end - start;
|
||||
float length = segment.Length();
|
||||
float ratio = radius / length;
|
||||
Vector2 n = ratio * segment;
|
||||
Vector2 t = new Vector2(-n.Y, n.X);
|
||||
|
||||
Vector2 t00 = new Vector2(-ratio, -ratio);
|
||||
Vector2 t10 = new Vector2(1+ratio, -ratio);
|
||||
Vector2 t01 = new Vector2(-ratio, +ratio);
|
||||
Vector2 t11 = new Vector2(1+ratio, +ratio);
|
||||
|
||||
Vector3 x00 = new Vector3(start - n - t, depth);
|
||||
Vector3 x10 = new Vector3(end + n - t, depth);
|
||||
Vector3 x01 = new Vector3(start - n + t, depth);
|
||||
Vector3 x11 = new Vector3(end + n + t, depth);
|
||||
|
||||
_calls.TexCoords2(t00);
|
||||
_calls.Vertex3(x00);
|
||||
_calls.TexCoords2(t01);
|
||||
_calls.Vertex3(x01);
|
||||
_calls.TexCoords2(t11);
|
||||
_calls.Vertex3(x11);
|
||||
|
||||
_calls.TexCoords2(t00);
|
||||
_calls.Vertex3(x00);
|
||||
_calls.TexCoords2(t11);
|
||||
_calls.Vertex3(x11);
|
||||
_calls.TexCoords2(t10);
|
||||
_calls.Vertex3(x10);
|
||||
}
|
||||
|
||||
private void DrawRect(ICommandFrame frame)
|
||||
{
|
||||
ref CommandInfo info = ref _commands.Take(out int index);
|
||||
|
||||
RectCommandArgs args = frame.GetParameter<RectCommandArgs>();
|
||||
|
||||
Vector2 size = Vector2.Abs(args.End - args.Start);
|
||||
float aspect = size.X / size.Y;
|
||||
float border = args.StrikeSize;
|
||||
float normRad = args.StrikeSize / size.Y;
|
||||
float wideRad = aspect * normRad;
|
||||
|
||||
int flags = 0;
|
||||
|
||||
switch (frame.Command.Name)
|
||||
{
|
||||
case "RectF":
|
||||
flags |= 1;
|
||||
break;
|
||||
case "RectS":
|
||||
flags |= 2;
|
||||
break;
|
||||
case "RectFS":
|
||||
flags |= 3;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (args.BorderKind)
|
||||
{
|
||||
case BorderKind.Inset:
|
||||
flags |= 2 << 2;
|
||||
break;
|
||||
case BorderKind.Outset:
|
||||
flags |= 1 << 2;
|
||||
break;
|
||||
}
|
||||
|
||||
info = new CommandInfo()
|
||||
{
|
||||
Type = SimpleDrawCommand.Rect,
|
||||
Flags = flags,
|
||||
Arg0 = aspect,
|
||||
Arg1 = normRad,
|
||||
};
|
||||
|
||||
SetCommandCommonBrush(ref info, args.FillBrush, args.StrikeBrush);
|
||||
|
||||
_calls.Transforms(Executor.TransformStack.Top);
|
||||
_calls.Begin(PrimitiveType.Triangles);
|
||||
|
||||
_calls.CommandIndex(index);
|
||||
|
||||
Vector2 t00 = new Vector2(-wideRad, -normRad);
|
||||
Vector2 t10 = new Vector2(1+wideRad, -normRad);
|
||||
Vector2 t01 = new Vector2(-wideRad, 1+normRad);
|
||||
Vector2 t11 = new Vector2(1+wideRad, 1+normRad);
|
||||
|
||||
Vector3 x00 = new Vector3(args.Start.X - border, args.Start.Y - border, args.Depth);
|
||||
Vector3 x10 = new Vector3(args.End.X + border, args.Start.Y - border, args.Depth);
|
||||
Vector3 x01 = new Vector3(args.Start.X - border, args.End.Y + border, args.Depth);
|
||||
Vector3 x11 = new Vector3(args.End.X + border, args.End.Y + border, args.Depth);
|
||||
|
||||
_calls.TexCoords2(t00);
|
||||
_calls.Vertex3(x00);
|
||||
_calls.TexCoords2(t01);
|
||||
_calls.Vertex3(x01);
|
||||
_calls.TexCoords2(t11);
|
||||
_calls.Vertex3(x11);
|
||||
|
||||
_calls.TexCoords2(t00);
|
||||
_calls.Vertex3(x00);
|
||||
_calls.TexCoords2(t11);
|
||||
_calls.Vertex3(x11);
|
||||
_calls.TexCoords2(t10);
|
||||
_calls.Vertex3(x10);
|
||||
|
||||
_calls.End();
|
||||
}
|
||||
|
||||
protected void SetCommandCommonBrush(ref CommandInfo info, IBrush? fill, IBrush? border)
|
||||
{
|
||||
switch (fill?.Kind.Name)
|
||||
{
|
||||
case "DB_Brush_solid":
|
||||
SolidBrush solid = (SolidBrush)fill;
|
||||
Vector4 color = new Vector4(solid.Color.R/255f, solid.Color.G/255f, solid.Color.B/255f, solid.Color.A/255f);
|
||||
info.FgColor = color;
|
||||
break;
|
||||
case "DB_Brush_gradient":
|
||||
GradientBrush gradient = (GradientBrush)fill;
|
||||
GradientUniformBuffer gradients = Executor.ResourcePool.GetResourceManager<GradientUniformBuffer>();
|
||||
gradients.Initialize();
|
||||
GradientUniformBuffer.Entry entry = gradients.InternGradient(gradient.Gradient);
|
||||
info.FgGradientIndex = entry.Offset;
|
||||
info.FgGradientCount = entry.Count;
|
||||
break;
|
||||
case null:
|
||||
// Craete a magenta brush for this.
|
||||
info.FgColor = new Vector4(1, 0, 1, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (border?.Kind.Name)
|
||||
{
|
||||
case "DB_Brush_solid":
|
||||
SolidBrush solid = (SolidBrush)border;
|
||||
Vector4 color = new Vector4(solid.Color.R/255f, solid.Color.G/255f, solid.Color.B/255f, solid.Color.A/255f);
|
||||
info.BgColor = color;
|
||||
break;
|
||||
case "DB_Brush_gradient":
|
||||
GradientBrush gradient = (GradientBrush)border;
|
||||
GradientUniformBuffer gradients = Executor.ResourcePool.GetResourceManager<GradientUniformBuffer>();
|
||||
gradients.Initialize();
|
||||
GradientUniformBuffer.Entry entry = gradients.InternGradient(gradient.Gradient);
|
||||
info.BgGradientIndex = entry.Offset;
|
||||
info.BgGradientCount = entry.Count;
|
||||
break;
|
||||
case null:
|
||||
// Craete a magenta brush for this.
|
||||
info.BgColor = new Vector4(1, 0, 1, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadShaders()
|
||||
{
|
||||
using Stream vsource = FetchEmbeddedResource("Dashboard.Drawing.OpenGL.Executors.simple.vert");
|
||||
using Stream fsource = FetchEmbeddedResource("Dashboard.Drawing.OpenGL.Executors.simple.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",
|
||||
"a_iCmdIndex",
|
||||
});
|
||||
GL.DeleteShader(vs);
|
||||
GL.DeleteShader(fs);
|
||||
|
||||
GL.UniformBlockBinding(_program, GL.GetUniformBlockIndex(_program, "CommandBlock"), 0);
|
||||
}
|
||||
|
||||
private static Stream FetchEmbeddedResource(string name)
|
||||
{
|
||||
return typeof(BaseCommandExecutor).Assembly.GetManifestResourceStream(name)!;
|
||||
}
|
||||
}
|
||||
}
|
||||
243
Dashboard.Drawing.OpenGL/Executors/TextCommandExecutor.cs
Normal file
243
Dashboard.Drawing.OpenGL/Executors/TextCommandExecutor.cs
Normal file
@ -0,0 +1,243 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using BlurgText;
|
||||
using Dashboard.OpenGL;
|
||||
using OpenTK.Graphics.OpenGL;
|
||||
using OpenTK.Mathematics;
|
||||
|
||||
namespace Dashboard.Drawing.OpenGL.Executors
|
||||
{
|
||||
public class TextCommandExecutor : ICommandExecutor, IInitializer
|
||||
{
|
||||
public IEnumerable<string> Extensions { get; } = new[] { "DB_Text" };
|
||||
public IContextExecutor Executor { get; private set; }
|
||||
// private BlurgEngine Engine => Executor.ResourcePool.GetResourceManager<BlurgEngine>();
|
||||
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<TextCommandArgs>();
|
||||
// DbBlurgFont font = Engine.InternFont(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<Call, Vertex>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
55
Dashboard.Drawing.OpenGL/Executors/gradient.glsl
Normal file
55
Dashboard.Drawing.OpenGL/Executors/gradient.glsl
Normal file
@ -0,0 +1,55 @@
|
||||
#ifndef _GRADIENT_GLSL_
|
||||
#define _GRADIENT_GLSL_
|
||||
|
||||
#define DB_GRADIENT_MAX 16
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
#endif
|
||||
224
Dashboard.Drawing.OpenGL/Executors/simple.frag
Normal file
224
Dashboard.Drawing.OpenGL/Executors/simple.frag
Normal file
@ -0,0 +1,224 @@
|
||||
#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 STRIKE_CENTER 0
|
||||
#define STRIKE_OUTSET 1
|
||||
#define STRIKE_INSET 2
|
||||
|
||||
in vec3 v_v3Position;
|
||||
in vec2 v_v2TexCoords;
|
||||
flat in int v_iCmdIndex;
|
||||
|
||||
out vec4 f_Color;
|
||||
|
||||
uniform sampler2D txForeground;
|
||||
uniform sampler2D txBackground;
|
||||
|
||||
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 CommandInfo_t {
|
||||
int iCommand;
|
||||
int iFlags;
|
||||
float fArg0;
|
||||
float fArg1;
|
||||
|
||||
int iFgGradientIndex;
|
||||
int iFgGradientCount;
|
||||
int iBgGradientIndex;
|
||||
int iBgGradientCount;
|
||||
|
||||
vec4 v4FgColor;
|
||||
vec4 v4BgColor;
|
||||
};
|
||||
|
||||
uniform CommandBlock
|
||||
{
|
||||
CommandInfo_t vstCommandInfo[DB_COMMAND_MAX];
|
||||
};
|
||||
|
||||
CommandInfo_t getCommandInfo()
|
||||
{
|
||||
return vstCommandInfo[v_iCmdIndex];
|
||||
}
|
||||
|
||||
vec4 fgColor()
|
||||
{
|
||||
return getCommandInfo().v4FgColor;
|
||||
}
|
||||
|
||||
vec4 bgColor()
|
||||
{
|
||||
return getCommandInfo().v4BgColor;
|
||||
}
|
||||
|
||||
void Point(void)
|
||||
{
|
||||
vec4 fg = fgColor();
|
||||
|
||||
if (dot(v_v2TexCoords, v_v2TexCoords) <= 0.25)
|
||||
f_Color = fg;
|
||||
else
|
||||
discard;
|
||||
}
|
||||
|
||||
#define LINE_NORMALIZED_RADIUS(cmd) cmd.fArg0
|
||||
void Line(void)
|
||||
{
|
||||
vec4 fg = fgColor();
|
||||
CommandInfo_t cmd = getCommandInfo();
|
||||
|
||||
float t = clamp(v_v2TexCoords.x, 0, 1);
|
||||
vec2 dv = v_v2TexCoords - vec2(t, 0);
|
||||
float d = dot(dv, dv);
|
||||
|
||||
float lim = LINE_NORMALIZED_RADIUS(cmd);
|
||||
lim *= lim;
|
||||
|
||||
if (d <= lim)
|
||||
f_Color = fg;
|
||||
else
|
||||
discard;
|
||||
}
|
||||
|
||||
#define RECT_ASPECT_RATIO(cmd) (cmd.fArg0)
|
||||
#define RECT_BORDER_WIDTH(cmd) (cmd.fArg1)
|
||||
#define RECT_FILL(cmd) ((cmd.iFlags & (1 << 0)) != 0)
|
||||
#define RECT_BORDER(cmd) ((cmd.iFlags & (1 << 1)) != 0)
|
||||
#define RECT_STRIKE_MASK 3
|
||||
#define RECT_STRIKE_SHIFT 2
|
||||
#define RECT_STRIKE_KIND(cmd) ((cmd.iFlags & RECT_STRIKE_MASK) >> RECT_STRIKE_SHIFT)
|
||||
void Rect(void)
|
||||
{
|
||||
vec4 fg = fgColor();
|
||||
vec4 bg = bgColor();
|
||||
|
||||
CommandInfo_t cmd = getCommandInfo();
|
||||
float aspect = RECT_ASPECT_RATIO(cmd);
|
||||
float border = RECT_BORDER_WIDTH(cmd);
|
||||
int strikeKind = RECT_STRIKE_KIND(cmd);
|
||||
|
||||
vec2 p = abs(2*v_v2TexCoords - vec2(1));
|
||||
p.x = p.x/aspect;
|
||||
|
||||
float m0;
|
||||
float m1;
|
||||
if (!RECT_BORDER(cmd))
|
||||
{
|
||||
m0 = 1;
|
||||
m1 = 1;
|
||||
}
|
||||
else if (strikeKind == STRIKE_OUTSET)
|
||||
{
|
||||
m0 = 1;
|
||||
m1 = border;
|
||||
}
|
||||
else if (strikeKind == STRIKE_INSET)
|
||||
{
|
||||
m0 = 1-border;
|
||||
m1 = 1;
|
||||
}
|
||||
else // strikeKind == STRIKE_CENTER
|
||||
{
|
||||
float h = 0.5 * border;
|
||||
m0 = 1-border;
|
||||
m1 = 1+border;
|
||||
}
|
||||
|
||||
if (p.x > m1*aspect || p.y > m1)
|
||||
{
|
||||
discard;
|
||||
}
|
||||
|
||||
if (RECT_FILL(cmd))
|
||||
{
|
||||
if (p.x <= 1 && p.y <= 1)
|
||||
{
|
||||
f_Color = fg;
|
||||
}
|
||||
}
|
||||
|
||||
if (RECT_BORDER(cmd))
|
||||
{
|
||||
float x = clamp(p.x, aspect*m0, aspect*m1);
|
||||
float y = clamp(p.y, m0, m1);
|
||||
|
||||
if (p.x == x || p.y == y)
|
||||
{
|
||||
f_Color = bg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void main(void)
|
||||
{
|
||||
switch (getCommandInfo().iCommand)
|
||||
{
|
||||
case CMD_POINT:
|
||||
Point();
|
||||
break;
|
||||
case CMD_LINE:
|
||||
Line();
|
||||
break;
|
||||
case CMD_RECT:
|
||||
Rect();
|
||||
break;
|
||||
|
||||
default:
|
||||
// Unimplemented value.
|
||||
f_Color = vec4(1, 0, 1, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
21
Dashboard.Drawing.OpenGL/Executors/simple.vert
Normal file
21
Dashboard.Drawing.OpenGL/Executors/simple.vert
Normal file
@ -0,0 +1,21 @@
|
||||
#version 140
|
||||
|
||||
in vec3 a_v3Position;
|
||||
in vec2 a_v2TexCoords;
|
||||
in int a_iCmdIndex;
|
||||
|
||||
out vec3 v_v3Position;
|
||||
out vec2 v_v2TexCoords;
|
||||
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_iCmdIndex = a_iCmdIndex;
|
||||
}
|
||||
21
Dashboard.Drawing.OpenGL/Executors/text.frag
Normal file
21
Dashboard.Drawing.OpenGL/Executors/text.frag
Normal file
@ -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;
|
||||
}
|
||||
18
Dashboard.Drawing.OpenGL/Executors/text.vert
Normal file
18
Dashboard.Drawing.OpenGL/Executors/text.vert
Normal file
@ -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;
|
||||
}
|
||||
40
Dashboard.Drawing.OpenGL/GLEngine.cs
Normal file
40
Dashboard.Drawing.OpenGL/GLEngine.cs
Normal file
@ -0,0 +1,40 @@
|
||||
// using Dashboard.Drawing.OpenGL.Text;
|
||||
using Dashboard.OpenGL;
|
||||
using OpenTK;
|
||||
using OpenTK.Graphics;
|
||||
|
||||
namespace Dashboard.Drawing.OpenGL
|
||||
{
|
||||
public class GLEngine
|
||||
{
|
||||
private readonly Dictionary<IGLContext, ContextExecutor> _executors = new Dictionary<IGLContext, ContextExecutor>();
|
||||
|
||||
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);
|
||||
|
||||
// Typesetter.Backend = BlurgEngine.Global;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
90
Dashboard.Drawing.OpenGL/GradientUniformBuffer.cs
Normal file
90
Dashboard.Drawing.OpenGL/GradientUniformBuffer.cs
Normal file
@ -0,0 +1,90 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Dashboard.OpenGL;
|
||||
using OpenTK.Mathematics;
|
||||
|
||||
namespace Dashboard.Drawing.OpenGL
|
||||
{
|
||||
public class GradientUniformBuffer : IInitializer, IGLDisposable, IResourceManager
|
||||
{
|
||||
private bool _isDisposed;
|
||||
private int _top = 0;
|
||||
private readonly MappableBuffer<GradientUniformStruct> _buffer = new MappableBuffer<GradientUniformStruct>();
|
||||
private readonly Dictionary<Gradient, Entry> _entries = new Dictionary<Gradient, Entry>();
|
||||
|
||||
public bool IsInitialized { get; private set; } = false;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
if (IsInitialized)
|
||||
return;
|
||||
|
||||
IsInitialized = true;
|
||||
|
||||
_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<GradientUniformStruct> 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);
|
||||
|
||||
public void Dispose() => Dispose(true);
|
||||
|
||||
public void Dispose(bool safeExit)
|
||||
{
|
||||
if (_isDisposed)
|
||||
return;
|
||||
_isDisposed = true;
|
||||
|
||||
_buffer.Dispose(safeExit);
|
||||
}
|
||||
|
||||
string IResourceManager.Name { get; } = nameof(GradientUniformBuffer);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Size = 8 * sizeof(float))]
|
||||
public struct GradientUniformStruct
|
||||
{
|
||||
[FieldOffset(0 * sizeof(float))]
|
||||
public float Position;
|
||||
|
||||
[FieldOffset(4 * sizeof(float))]
|
||||
public Vector4 Color;
|
||||
}
|
||||
}
|
||||
24
Dashboard.Drawing.OpenGL/IArc.cs
Normal file
24
Dashboard.Drawing.OpenGL/IArc.cs
Normal file
@ -0,0 +1,24 @@
|
||||
namespace Dashboard.Drawing.OpenGL
|
||||
{
|
||||
/// <summary>
|
||||
/// Atomic reference counter.
|
||||
/// </summary>
|
||||
public interface IArc : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The number of references to this.
|
||||
/// </summary>
|
||||
int References { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Increment the number of references.
|
||||
/// </summary>
|
||||
void IncrementReference();
|
||||
|
||||
/// <summary>
|
||||
/// Decrement the number of references.
|
||||
/// </summary>
|
||||
/// <returns>True if this was the last reference.</returns>
|
||||
bool DecrementReference();
|
||||
}
|
||||
}
|
||||
9
Dashboard.Drawing.OpenGL/IInitializer.cs
Normal file
9
Dashboard.Drawing.OpenGL/IInitializer.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Dashboard.Drawing.OpenGL
|
||||
{
|
||||
public interface IInitializer
|
||||
{
|
||||
bool IsInitialized { get; }
|
||||
|
||||
void Initialize();
|
||||
}
|
||||
}
|
||||
7
Dashboard.Drawing.OpenGL/IResourceManager.cs
Normal file
7
Dashboard.Drawing.OpenGL/IResourceManager.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Dashboard.Drawing.OpenGL
|
||||
{
|
||||
public interface IResourceManager
|
||||
{
|
||||
public string Name { get; }
|
||||
}
|
||||
}
|
||||
168
Dashboard.Drawing.OpenGL/MappableBuffer.cs
Normal file
168
Dashboard.Drawing.OpenGL/MappableBuffer.cs
Normal file
@ -0,0 +1,168 @@
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Dashboard.OpenGL;
|
||||
using OpenTK.Graphics.OpenGL;
|
||||
|
||||
namespace Dashboard.Drawing.OpenGL
|
||||
{
|
||||
public class MappableBuffer<T> : IInitializer, IGLDisposable where T : struct
|
||||
{
|
||||
public int Handle { get; private set; } = 0;
|
||||
public int Capacity { get; set; } = BASE_CAPACITY;
|
||||
public IntPtr Pointer { get; private set; } = IntPtr.Zero;
|
||||
|
||||
public bool IsInitialized => Handle != 0;
|
||||
|
||||
private bool _isDisposed = false;
|
||||
private const int BASE_CAPACITY = 4 << 10; // 4 KiB
|
||||
private const int MAX_INCREMENT = 4 << 20; // 4 MiB
|
||||
|
||||
~MappableBuffer()
|
||||
{
|
||||
Dispose(true, false);
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
if (IsInitialized)
|
||||
return;
|
||||
|
||||
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;
|
||||
|
||||
SetSize(count, false);
|
||||
}
|
||||
|
||||
public void SetSize(int count, bool clear = false)
|
||||
{
|
||||
AssertInitialized();
|
||||
Unmap();
|
||||
|
||||
int sz = Unsafe.SizeOf<T>();
|
||||
int oldsize = Capacity * sz;
|
||||
int request = count * sz;
|
||||
int newsize;
|
||||
|
||||
if (request < BASE_CAPACITY)
|
||||
request = BASE_CAPACITY;
|
||||
|
||||
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();
|
||||
|
||||
if (clear)
|
||||
{
|
||||
GL.BindBuffer(BufferTarget.ArrayBuffer, dest);
|
||||
GL.BufferData(BufferTarget.ArrayBuffer, newsize, IntPtr.Zero, BufferUsage.DynamicDraw);
|
||||
}
|
||||
else
|
||||
{
|
||||
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, Math.Min(newsize, oldsize));
|
||||
}
|
||||
|
||||
GL.DeleteBuffer(Handle);
|
||||
Handle = dest;
|
||||
Capacity = newsize / Unsafe.SizeOf<T>();
|
||||
}
|
||||
|
||||
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);
|
||||
Pointer = IntPtr.Zero;
|
||||
}
|
||||
|
||||
public unsafe Span<T> AsSpan()
|
||||
{
|
||||
if (Pointer == IntPtr.Zero)
|
||||
throw new InvalidOperationException("The buffer is not currently mapped.");
|
||||
|
||||
AssertInitialized();
|
||||
|
||||
return new Span<T>(Pointer.ToPointer(), Capacity);
|
||||
}
|
||||
|
||||
private void AssertInitialized()
|
||||
{
|
||||
if (Handle == 0)
|
||||
throw new InvalidOperationException("The buffer is not initialized.");
|
||||
}
|
||||
|
||||
private void Dispose(bool safeExit, bool disposing)
|
||||
{
|
||||
if (_isDisposed)
|
||||
return;
|
||||
_isDisposed = true;
|
||||
|
||||
if (disposing)
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
if (safeExit)
|
||||
ContextCollector.Global.DeleteBufffer(Handle);
|
||||
}
|
||||
|
||||
public void Dispose() => Dispose(true, true);
|
||||
public void Dispose(bool safeExit) => Dispose(safeExit, true);
|
||||
}
|
||||
|
||||
public class MappableBumpAllocator<T> : MappableBuffer<T> where T : struct
|
||||
{
|
||||
private int _top = 0;
|
||||
private int _previousTop = 0;
|
||||
|
||||
public ref T Take(out int index)
|
||||
{
|
||||
index = _top;
|
||||
EnsureCapacity(++_top);
|
||||
Map();
|
||||
|
||||
return ref AsSpan()[index];
|
||||
}
|
||||
|
||||
public ref T Take() => ref Take(out _);
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
SetSize(0, true);
|
||||
_previousTop = _top;
|
||||
_top = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
Dashboard.Drawing.OpenGL/TransformStack.cs
Normal file
51
Dashboard.Drawing.OpenGL/TransformStack.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using OpenTK.Mathematics;
|
||||
|
||||
namespace Dashboard.Drawing.OpenGL
|
||||
{
|
||||
/// <summary>
|
||||
/// The current stack of transformations.
|
||||
/// </summary>
|
||||
public class TransformStack
|
||||
{
|
||||
private Matrix4 _top = Matrix4.Identity;
|
||||
private readonly Stack<Matrix4> _stack = new Stack<Matrix4>();
|
||||
|
||||
/// <summary>
|
||||
/// The top-most transform matrix.
|
||||
/// </summary>
|
||||
public ref readonly Matrix4 Top => ref _top;
|
||||
|
||||
/// <summary>
|
||||
/// The number of matrices in the stack.
|
||||
/// </summary>
|
||||
public int Count => _stack.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Push a transform.
|
||||
/// </summary>
|
||||
/// <param name="transform">The transform to push.</param>
|
||||
public void Push(in Matrix4 transform)
|
||||
{
|
||||
_stack.Push(_top);
|
||||
_top = transform * _top;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pop a transform.
|
||||
/// </summary>
|
||||
public void Pop()
|
||||
{
|
||||
if (!_stack.TryPop(out _top))
|
||||
_top = Matrix4.Identity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear the stack of transformations.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_stack.Clear();
|
||||
_top = Matrix4.Identity;
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Dashboard.Drawing/Brush.cs
Normal file
52
Dashboard.Drawing/Brush.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using System;
|
||||
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(Color color) : IBrush
|
||||
{
|
||||
public IDrawExtension Kind { get; } = SolidBrushExtension.Instance;
|
||||
public Color Color { get; } = color;
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Kind, Color);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct GradientBrush(Gradient gradient) : IBrush
|
||||
{
|
||||
public IDrawExtension Kind { get; } = GradientBrushExtension.Instance;
|
||||
public Gradient Gradient { get; } = gradient;
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Kind, Gradient);
|
||||
}
|
||||
}
|
||||
|
||||
public class SolidBrushExtension : DrawExtension
|
||||
{
|
||||
private SolidBrushExtension() : base("DB_Brush_solid", new[] { BrushExtension.Instance }) { }
|
||||
|
||||
public static readonly SolidBrushExtension Instance = new SolidBrushExtension();
|
||||
}
|
||||
|
||||
public class GradientBrushExtension : DrawExtension
|
||||
{
|
||||
private GradientBrushExtension() : base("DB_Brush_gradient", new[] { BrushExtension.Instance }) { }
|
||||
|
||||
public static readonly GradientBrushExtension Instance = new GradientBrushExtension();
|
||||
}
|
||||
}
|
||||
13
Dashboard.Drawing/Dashboard.Drawing.csproj
Normal file
13
Dashboard.Drawing/Dashboard.Drawing.csproj
Normal file
@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Dashboard.Common\Dashboard.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
292
Dashboard.Drawing/DbBaseCommands.cs
Normal file
292
Dashboard.Drawing/DbBaseCommands.cs
Normal file
@ -0,0 +1,292 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Dashboard.Drawing
|
||||
{
|
||||
public class DbBaseCommands : DrawExtension
|
||||
{
|
||||
public DrawCommand<PointCommandArgs> DrawPoint { get; }
|
||||
public DrawCommand<LineCommandArgs> DrawLine { get; }
|
||||
public RectCommand DrawRectF { get; }
|
||||
public RectCommand DrawRectS { get; }
|
||||
public RectCommand DrawRectFS { get; }
|
||||
|
||||
private DbBaseCommands() : base("DB_base",
|
||||
new[]
|
||||
{
|
||||
BrushExtension.Instance,
|
||||
})
|
||||
{
|
||||
AddCommand(DrawPoint = new DrawCommand<PointCommandArgs>("Point", this, PointCommandArgs.CommandSize));
|
||||
AddCommand(DrawLine = new DrawCommand<LineCommandArgs>("Line", this, LineCommandArgs.CommandSize));
|
||||
AddCommand(DrawRectF = new RectCommand(this, RectCommand.Mode.Fill));
|
||||
AddCommand(DrawRectS = new RectCommand(this, RectCommand.Mode.Strike));
|
||||
AddCommand(DrawRectFS = new RectCommand(this, RectCommand.Mode.FillStrike));
|
||||
}
|
||||
|
||||
public static readonly DbBaseCommands Instance = new DbBaseCommands();
|
||||
}
|
||||
|
||||
public struct PointCommandArgs : IParameterSerializer<PointCommandArgs>
|
||||
{
|
||||
public Vector2 Position { get; private set; }
|
||||
public float Depth { get; private set; }
|
||||
public float Size { get; private set; }
|
||||
public IBrush? Brush { get; private set; }
|
||||
|
||||
public PointCommandArgs(Vector2 position, float depth, float size, IBrush brush)
|
||||
{
|
||||
Position = position;
|
||||
Depth = depth;
|
||||
Brush = brush;
|
||||
Size = size;
|
||||
}
|
||||
|
||||
public int Serialize(DrawQueue queue, Span<byte> bytes)
|
||||
{
|
||||
if (bytes.Length < CommandSize)
|
||||
return CommandSize;
|
||||
|
||||
Span<Value> value = stackalloc Value[]
|
||||
{
|
||||
new Value(Position, Depth, Size, queue.RequireResource(Brush!))
|
||||
};
|
||||
|
||||
MemoryMarshal.AsBytes(value).CopyTo(bytes);
|
||||
return CommandSize;
|
||||
}
|
||||
|
||||
[MemberNotNull(nameof(Brush))]
|
||||
public void Deserialize(DrawQueue queue, ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
if (bytes.Length < CommandSize)
|
||||
throw new Exception("Not enough bytes");
|
||||
|
||||
Value value = MemoryMarshal.AsRef<Value>(bytes);
|
||||
|
||||
Position = value.Position;
|
||||
Depth = value.Depth;
|
||||
Size = value.Size;
|
||||
Brush = (IBrush)queue.Resources[value.BrushIndex];
|
||||
}
|
||||
|
||||
private record struct Value(Vector2 Position, float Depth, float Size, int BrushIndex);
|
||||
|
||||
public static readonly int CommandSize = Unsafe.SizeOf<Value>();
|
||||
}
|
||||
|
||||
public struct LineCommandArgs : IParameterSerializer<LineCommandArgs>
|
||||
{
|
||||
public Vector2 Start { get; private set; }
|
||||
public Vector2 End { get; private set; }
|
||||
public float Depth { get; private set; }
|
||||
public float Size { get; private set; }
|
||||
public IBrush? Brush { get; private set; }
|
||||
|
||||
public LineCommandArgs(Vector2 start, Vector2 end, float depth, float size, IBrush brush)
|
||||
{
|
||||
Start = start;
|
||||
End = end;
|
||||
Depth = depth;
|
||||
Size = size;
|
||||
Brush = brush;
|
||||
}
|
||||
|
||||
public int Serialize(DrawQueue queue, Span<byte> bytes)
|
||||
{
|
||||
if (bytes.Length < CommandSize)
|
||||
return CommandSize;
|
||||
|
||||
Span<Value> value = stackalloc Value[]
|
||||
{
|
||||
new Value(Start, End, Depth, Size, queue.RequireResource(Brush!))
|
||||
};
|
||||
|
||||
MemoryMarshal.AsBytes(value).CopyTo(bytes);
|
||||
return CommandSize;
|
||||
}
|
||||
|
||||
public void Deserialize(DrawQueue queue, ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
if (bytes.Length < CommandSize)
|
||||
throw new Exception("Not enough bytes");
|
||||
|
||||
Value value = MemoryMarshal.AsRef<Value>(bytes);
|
||||
|
||||
Start = value.Start;
|
||||
End = value.End;
|
||||
Depth = value.Depth;
|
||||
Size = value.Size;
|
||||
Brush = (IBrush)queue.Resources[value.BrushIndex];
|
||||
}
|
||||
|
||||
private record struct Value(Vector2 Start, Vector2 End, float Depth, float Size, int BrushIndex);
|
||||
|
||||
public static readonly int CommandSize = Unsafe.SizeOf<Value>();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public class RectCommand : IDrawCommand<RectCommandArgs>
|
||||
{
|
||||
private readonly Mode _mode;
|
||||
public string Name { get; }
|
||||
public IDrawExtension Extension { get; }
|
||||
public int Length { get; }
|
||||
|
||||
public RectCommand(IDrawExtension extension, Mode mode)
|
||||
{
|
||||
Extension = extension;
|
||||
_mode = mode;
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case Mode.Fill:
|
||||
Name = "RectF";
|
||||
Length = Unsafe.SizeOf<RectF>();
|
||||
break;
|
||||
case Mode.Strike:
|
||||
Name = "RectS";
|
||||
Length = Unsafe.SizeOf<RectS>();
|
||||
break;
|
||||
default:
|
||||
Name = "RectFS";
|
||||
Length = Unsafe.SizeOf<RectFS>();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
object? IDrawCommand.GetParams(DrawQueue queue, ReadOnlySpan<byte> param)
|
||||
{
|
||||
return GetParams(queue, param);
|
||||
}
|
||||
|
||||
public RectCommandArgs GetParams(DrawQueue queue, ReadOnlySpan<byte> param)
|
||||
{
|
||||
if (param.Length < Length)
|
||||
throw new Exception("Not enough bytes");
|
||||
|
||||
RectCommandArgs args;
|
||||
|
||||
switch (_mode)
|
||||
{
|
||||
case Mode.Fill:
|
||||
ref readonly RectF f = ref MemoryMarshal.AsRef<RectF>(param);
|
||||
args = new RectCommandArgs(f.Start, f.End, f.Depth, (IBrush)queue.Resources[f.FillBrushIndex]);
|
||||
break;
|
||||
case Mode.Strike:
|
||||
ref readonly RectS s = ref MemoryMarshal.AsRef<RectS>(param);
|
||||
args = new RectCommandArgs(s.Start, s.End, s.Depth, (IBrush)queue.Resources[s.StrikeBrushIndex], s.StrikeSize, s.BorderKind);
|
||||
break;
|
||||
default:
|
||||
ref readonly RectFS fs = ref MemoryMarshal.AsRef<RectFS>(param);
|
||||
args = new RectCommandArgs(fs.Start, fs.End, fs.Depth, (IBrush)queue.Resources[fs.FillBrushIndex],
|
||||
(IBrush)queue.Resources[fs.StrikeBrushIndex], fs.StrikeSize, fs.BorderKind);
|
||||
break;
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
public int WriteParams(DrawQueue queue, object? obj, Span<byte> param)
|
||||
{
|
||||
return WriteParams(queue, (RectCommandArgs)obj, param);
|
||||
}
|
||||
|
||||
public int WriteParams(DrawQueue queue, RectCommandArgs obj, Span<byte> param)
|
||||
{
|
||||
if (param.Length < Length)
|
||||
return Length;
|
||||
|
||||
switch (_mode)
|
||||
{
|
||||
case Mode.Fill:
|
||||
ref RectF f = ref MemoryMarshal.AsRef<RectF>(param);
|
||||
f.Start = obj.Start;
|
||||
f.End = obj.End;
|
||||
f.Depth = obj.Depth;
|
||||
f.FillBrushIndex = queue.RequireResource(obj.FillBrush!);
|
||||
break;
|
||||
case Mode.Strike:
|
||||
ref RectS s = ref MemoryMarshal.AsRef<RectS>(param);
|
||||
s.Start = obj.Start;
|
||||
s.End = obj.End;
|
||||
s.Depth = obj.Depth;
|
||||
s.StrikeBrushIndex = queue.RequireResource(obj.StrikeBrush!);
|
||||
s.StrikeSize = obj.StrikeSize;
|
||||
s.BorderKind = obj.BorderKind;
|
||||
break;
|
||||
default:
|
||||
ref RectFS fs = ref MemoryMarshal.AsRef<RectFS>(param);
|
||||
fs.Start = obj.Start;
|
||||
fs.End = obj.End;
|
||||
fs.Depth = obj.Depth;
|
||||
fs.FillBrushIndex = queue.RequireResource(obj.FillBrush!);
|
||||
fs.StrikeBrushIndex = queue.RequireResource(obj.StrikeBrush!);
|
||||
fs.StrikeSize = obj.StrikeSize;
|
||||
fs.BorderKind = obj.BorderKind;
|
||||
break;
|
||||
}
|
||||
|
||||
return Length;
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum Mode
|
||||
{
|
||||
Fill = 1,
|
||||
Strike = 2,
|
||||
FillStrike = Fill | Strike,
|
||||
}
|
||||
|
||||
private record struct RectF(Vector2 Start, Vector2 End, float Depth, int FillBrushIndex);
|
||||
private record struct RectS(Vector2 Start, Vector2 End, float Depth, int StrikeBrushIndex, float StrikeSize, BorderKind BorderKind);
|
||||
private record struct RectFS(Vector2 Start, Vector2 End, float Depth, int FillBrushIndex, int StrikeBrushIndex, float StrikeSize, BorderKind BorderKind);
|
||||
}
|
||||
|
||||
public struct RectCommandArgs
|
||||
{
|
||||
public Vector2 Start { get; private set; }
|
||||
public Vector2 End { get; private set; }
|
||||
public float Depth { get; private set; }
|
||||
public float StrikeSize { get; private set; } = 0f;
|
||||
public BorderKind BorderKind { get; private set; } = BorderKind.Center;
|
||||
public IBrush? FillBrush { get; private set; } = null;
|
||||
public IBrush? StrikeBrush { get; private set; } = null;
|
||||
public bool IsStruck => StrikeSize != 0;
|
||||
|
||||
public RectCommandArgs(Vector2 start, Vector2 end, float depth, IBrush fillBrush)
|
||||
{
|
||||
Start = start;
|
||||
End = end;
|
||||
Depth = depth;
|
||||
FillBrush = fillBrush;
|
||||
}
|
||||
|
||||
public RectCommandArgs(Vector2 start, Vector2 end, float depth, IBrush strikeBrush, float strikeSize, BorderKind borderKind)
|
||||
{
|
||||
Start = start;
|
||||
End = end;
|
||||
Depth = depth;
|
||||
StrikeBrush = strikeBrush;
|
||||
StrikeSize = strikeSize;
|
||||
BorderKind = borderKind;
|
||||
}
|
||||
|
||||
public RectCommandArgs(Vector2 start, Vector2 end, float depth, IBrush fillBrush, IBrush strikeBrush, float strikeSize,
|
||||
BorderKind borderKind)
|
||||
{
|
||||
Start = start;
|
||||
End = end;
|
||||
Depth = depth;
|
||||
FillBrush = fillBrush;
|
||||
StrikeBrush = strikeBrush;
|
||||
StrikeSize = strikeSize;
|
||||
BorderKind = borderKind;
|
||||
}
|
||||
}
|
||||
}
|
||||
26
Dashboard.Drawing/DrawBuffer.cs
Normal file
26
Dashboard.Drawing/DrawBuffer.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace Dashboard.Drawing
|
||||
{
|
||||
public enum DrawPrimitive
|
||||
{
|
||||
Point,
|
||||
Line,
|
||||
LineStrip,
|
||||
Triangle,
|
||||
TriangleFan,
|
||||
TriangleStrip
|
||||
}
|
||||
|
||||
public record struct DrawVertex(Vector3 Position, Vector3 TextureCoordinate, Vector4 Color);
|
||||
|
||||
public record DrawInfo(DrawPrimitive Primitive, int Count)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class DrawBuffer
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
107
Dashboard.Drawing/DrawCommand.cs
Normal file
107
Dashboard.Drawing/DrawCommand.cs
Normal file
@ -0,0 +1,107 @@
|
||||
using System;
|
||||
|
||||
namespace Dashboard.Drawing
|
||||
{
|
||||
public interface IDrawCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the command.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The draw extension that defines this command.
|
||||
/// </summary>
|
||||
IDrawExtension Extension { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The length of the command data segment, in bytes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Must be 0 for simple commands. For commands that are variadic, the
|
||||
/// value must be less than 0. Any other positive value, otherwise.
|
||||
/// </remarks>
|
||||
int Length { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the parameters object for this command.
|
||||
/// </summary>
|
||||
/// <param name="param">The parameter array.</param>
|
||||
/// <returns>The parameters object.</returns>
|
||||
object? GetParams(DrawQueue queue, ReadOnlySpan<byte> param);
|
||||
|
||||
int WriteParams(DrawQueue queue, object? obj, Span<byte> param);
|
||||
}
|
||||
|
||||
public interface IDrawCommand<T> : IDrawCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the parameters object for this command.
|
||||
/// </summary>
|
||||
/// <param name="param">The parameter array.</param>
|
||||
/// <returns>The parameters object.</returns>
|
||||
new T? GetParams(DrawQueue queue, ReadOnlySpan<byte> param);
|
||||
|
||||
new int WriteParams(DrawQueue queue, T? obj, Span<byte> 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<byte> param)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public int WriteParams(DrawQueue queue, object? obj, Span<byte> param)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DrawCommand<T> : IDrawCommand<T>
|
||||
where T : IParameterSerializer<T>, 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<byte> param)
|
||||
{
|
||||
T t = new T();
|
||||
t.Deserialize(queue, param);
|
||||
return t;
|
||||
}
|
||||
|
||||
public int WriteParams(DrawQueue queue, T? obj, Span<byte> param)
|
||||
{
|
||||
return obj!.Serialize(queue, param);
|
||||
}
|
||||
|
||||
int IDrawCommand.WriteParams(DrawQueue queue, object? obj, Span<byte> param)
|
||||
{
|
||||
return WriteParams(queue, (T?)obj, param);
|
||||
}
|
||||
|
||||
object? IDrawCommand.GetParams(DrawQueue queue, ReadOnlySpan<byte> param)
|
||||
{
|
||||
return GetParams(queue, param);
|
||||
}
|
||||
}
|
||||
}
|
||||
141
Dashboard.Drawing/DrawExtension.cs
Normal file
141
Dashboard.Drawing/DrawExtension.cs
Normal file
@ -0,0 +1,141 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Dashboard.Drawing
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for all drawing extensions.
|
||||
/// </summary>
|
||||
public interface IDrawExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of this extension.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
public IReadOnlyList<IDrawExtension> Requires { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of commands this extension defines, if any.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IDrawCommand> Commands { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A simple draw extension.
|
||||
/// </summary>
|
||||
public class DrawExtension : IDrawExtension
|
||||
{
|
||||
private readonly List<IDrawCommand> _drawCommands = new List<IDrawCommand>();
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public IReadOnlyList<IDrawCommand> Commands { get; }
|
||||
|
||||
public IReadOnlyList<IDrawExtension> Requires { get; }
|
||||
|
||||
public DrawExtension(string name, IEnumerable<IDrawExtension>? requires = null)
|
||||
{
|
||||
Name = name;
|
||||
Commands = _drawCommands.AsReadOnly();
|
||||
Requires = (requires ?? Enumerable.Empty<IDrawExtension>()).ToImmutableList();
|
||||
}
|
||||
|
||||
protected void AddCommand(IDrawCommand command)
|
||||
{
|
||||
_drawCommands.Add(command);
|
||||
}
|
||||
}
|
||||
|
||||
public static class DrawExtensionClass
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the draw controller for the given queue.
|
||||
/// </summary>
|
||||
/// <param name="extension">The extension instance.</param>
|
||||
/// <param name="queue">The draw queue.</param>
|
||||
/// <returns>The draw controller for this queue.</returns>
|
||||
public static IDrawController GetController(this IDrawExtension extension, DrawQueue queue)
|
||||
{
|
||||
return queue.GetController(extension);
|
||||
}
|
||||
|
||||
public static void Point(this DrawQueue queue, Vector2 position, float depth, float size, IBrush brush)
|
||||
{
|
||||
Vector2 radius = new Vector2(0.5f * size);
|
||||
Box2d bounds = new Box2d(position - radius, position + radius);
|
||||
|
||||
IDrawController controller = queue.GetController(DbBaseCommands.Instance);
|
||||
controller.EnsureBounds(bounds, depth);
|
||||
controller.Write(DbBaseCommands.Instance.DrawPoint, new PointCommandArgs(position, depth, size, brush));
|
||||
}
|
||||
|
||||
public static void Line(this DrawQueue queue, Vector2 start, Vector2 end, float depth, float size, IBrush brush)
|
||||
{
|
||||
Vector2 radius = new Vector2(size / 2f);
|
||||
Vector2 min = Vector2.Min(start, end) - radius;
|
||||
Vector2 max = Vector2.Max(start, end) + radius;
|
||||
Box2d bounds = new Box2d(min, max);
|
||||
|
||||
IDrawController controller = queue.GetController(DbBaseCommands.Instance);
|
||||
controller.EnsureBounds(bounds, depth);
|
||||
controller.Write(DbBaseCommands.Instance.DrawLine, new LineCommandArgs(start, end, depth, size, brush));
|
||||
}
|
||||
|
||||
public static void Rect(this DrawQueue queue, Vector2 start, Vector2 end, float depth, IBrush fillBrush)
|
||||
{
|
||||
IDrawController controller = queue.GetController(DbBaseCommands.Instance);
|
||||
Vector2 min = Vector2.Min(start, end);
|
||||
Vector2 max = Vector2.Max(start, end);
|
||||
controller.EnsureBounds(new Box2d(min, max), depth);
|
||||
controller.Write(DbBaseCommands.Instance.DrawRectF, new RectCommandArgs(start, end, depth, fillBrush));
|
||||
}
|
||||
|
||||
public static void Rect(this DrawQueue queue, Vector2 start, Vector2 end, float depth, IBrush strikeBrush, float strikeSize,
|
||||
BorderKind kind = BorderKind.Center)
|
||||
{
|
||||
IDrawController controller = queue.GetController(DbBaseCommands.Instance);
|
||||
Vector2 min = Vector2.Min(start, end);
|
||||
Vector2 max = Vector2.Max(start, end);
|
||||
controller.EnsureBounds(new Box2d(min, max), depth);
|
||||
controller.Write(DbBaseCommands.Instance.DrawRectS, new RectCommandArgs(start, end, depth, strikeBrush, strikeSize, kind));
|
||||
}
|
||||
|
||||
public static void Rect(this DrawQueue queue, Vector2 start, Vector2 end, float depth, IBrush fillBrush, IBrush strikeBrush,
|
||||
float strikeSize, BorderKind kind = BorderKind.Center)
|
||||
{
|
||||
IDrawController controller = queue.GetController(DbBaseCommands.Instance);
|
||||
Vector2 min = Vector2.Min(start, end);
|
||||
Vector2 max = Vector2.Max(start, end);
|
||||
controller.EnsureBounds(new Box2d(min, max), depth);
|
||||
controller.Write(DbBaseCommands.Instance.DrawRectFS, new RectCommandArgs(start, end, depth, fillBrush, strikeBrush, strikeSize, kind));
|
||||
}
|
||||
|
||||
public static void Text(this DrawQueue queue, Vector3 position, IBrush brush, string text, IFont font,
|
||||
Anchor anchor = Anchor.Left)
|
||||
{
|
||||
IDrawController controller = queue.GetController(DbBaseCommands.Instance);
|
||||
SizeF size = Typesetter.MeasureString(font, text);
|
||||
controller.EnsureBounds(new Box2d(position.X, position.Y, position.X + size.Width, position.Y + size.Height), position.Z);
|
||||
controller.Write(TextExtension.Instance.TextCommand, new TextCommandArgs(font, brush, anchor, position, text));
|
||||
}
|
||||
|
||||
public static void Text(this DrawQueue queue, Vector3 position, IBrush textBrush, IBrush borderBrush,
|
||||
float borderRadius, string text, IFont font, Anchor anchor = Anchor.Left, BorderKind borderKind = BorderKind.Outset)
|
||||
{
|
||||
IDrawController controller = queue.GetController(DbBaseCommands.Instance);
|
||||
SizeF size = Typesetter.MeasureString(font, text);
|
||||
controller.EnsureBounds(new Box2d(position.X, position.Y, position.X + size.Width, position.Y + size.Height), position.Z);
|
||||
controller.Write(TextExtension.Instance.TextCommand, new TextCommandArgs(font, textBrush, anchor, position, text)
|
||||
{
|
||||
BorderBrush = borderBrush,
|
||||
BorderRadius = borderRadius,
|
||||
BorderKind = borderKind,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
368
Dashboard.Drawing/DrawQueue.cs
Normal file
368
Dashboard.Drawing/DrawQueue.cs
Normal file
@ -0,0 +1,368 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
|
||||
namespace Dashboard.Drawing
|
||||
{
|
||||
public class DrawQueue : IEnumerable<ICommandFrame>, IDisposable
|
||||
{
|
||||
private readonly HashList<IDrawExtension> _extensions = new HashList<IDrawExtension>();
|
||||
private readonly HashList<IDrawCommand> _commands = new HashList<IDrawCommand>();
|
||||
private readonly HashList<IDrawResource> _resources = new HashList<IDrawResource>();
|
||||
private readonly DrawController _controller;
|
||||
private readonly MemoryStream _commandStream = new MemoryStream();
|
||||
|
||||
/// <summary>
|
||||
/// The absolute boundary of all graphics objects.
|
||||
/// </summary>
|
||||
public Box3d Bounds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The extensions required to draw the image.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IDrawExtension> Extensions => _extensions;
|
||||
|
||||
/// <summary>
|
||||
/// The resources used by this draw queue.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IDrawResource> Resources => _resources;
|
||||
|
||||
/// <summary>
|
||||
/// The list of commands used by the extension.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IDrawCommand> Command => _commands;
|
||||
|
||||
public DrawQueue()
|
||||
{
|
||||
_controller = new DrawController(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear the queue.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_resources.Clear();
|
||||
_commands.Clear();
|
||||
_extensions.Clear();
|
||||
_commandStream.SetLength(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<byte> 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<byte> param)
|
||||
{
|
||||
if (command.Length < 0)
|
||||
{
|
||||
Span<byte> cmd = stackalloc byte[10];
|
||||
int cmdIndex = _commands.Intern(command);
|
||||
int sz = ToVlq(cmdIndex, cmd);
|
||||
sz += ToVlq(param.Length, cmd[sz..]);
|
||||
_commandStream.Write(cmd[..sz]);
|
||||
_commandStream.Write(param);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (command.Length != param.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(param.Length), "Length of the parameter does not match the command.");
|
||||
|
||||
Span<byte> cmd = stackalloc byte[5];
|
||||
int cmdIndex = _commands.Intern(command);
|
||||
int sz = ToVlq(cmdIndex, cmd);
|
||||
|
||||
_commandStream.Write(cmd[..sz]);
|
||||
_commandStream.Write(param);
|
||||
}
|
||||
}
|
||||
|
||||
public Enumerator GetEnumerator() => new Enumerator(this);
|
||||
IEnumerator<ICommandFrame> IEnumerable<ICommandFrame>.GetEnumerator() => GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
private static int ToVlq(int value, Span<byte> 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.");
|
||||
|
||||
if (value == 0)
|
||||
{
|
||||
bytes[0] = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
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<byte> bytes, out int value)
|
||||
{
|
||||
value = 0;
|
||||
|
||||
int i;
|
||||
for (i = 0; i < bytes.Length; i++)
|
||||
{
|
||||
byte b = bytes[i];
|
||||
|
||||
value |= (b & 0x7F) << (7*i);
|
||||
|
||||
if ((b & (1 << 7)) == 0)
|
||||
{
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private class DrawController(DrawQueue Queue) : IDrawController
|
||||
{
|
||||
public void EnsureBounds(Box2d bounds, float depth)
|
||||
{
|
||||
Queue.Bounds = Box3d.Union(Queue.Bounds, bounds, depth);
|
||||
}
|
||||
|
||||
public void Write(IDrawCommand command)
|
||||
{
|
||||
Queue.Write(command);
|
||||
}
|
||||
|
||||
public void Write(IDrawCommand command, ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
Queue.Write(command, bytes);
|
||||
}
|
||||
|
||||
public void Write<T>(IDrawCommand command, T param) where T : IParameterSerializer<T>
|
||||
{
|
||||
int length = param.Serialize(Queue, Span<byte>.Empty);
|
||||
Span<byte> bytes = stackalloc byte[length];
|
||||
|
||||
param.Serialize(Queue, bytes);
|
||||
Write(command, bytes);
|
||||
}
|
||||
|
||||
public void Write<T1, T2>(T2 command, T1 param) where T2 : IDrawCommand<T1>
|
||||
{
|
||||
int length = command.WriteParams(Queue, param, Span<byte>.Empty);
|
||||
Span<byte> bytes = stackalloc byte[length];
|
||||
|
||||
command.WriteParams(Queue, param, bytes);
|
||||
Write(command, bytes);
|
||||
}
|
||||
}
|
||||
|
||||
public class Enumerator : ICommandFrame, IEnumerator<ICommandFrame>
|
||||
{
|
||||
private readonly DrawQueue _queue;
|
||||
private readonly byte[] _stream;
|
||||
private int _length;
|
||||
private int _index = -1;
|
||||
private int _paramsIndex = -1;
|
||||
private int _paramLength = 0;
|
||||
private IDrawCommand? _current = null;
|
||||
|
||||
public ICommandFrame Current => this;
|
||||
|
||||
object? IEnumerator.Current => Current;
|
||||
|
||||
public IDrawCommand Command => _current ?? throw new InvalidOperationException();
|
||||
|
||||
public bool HasParameters { get; private set; }
|
||||
|
||||
public Enumerator(DrawQueue queue)
|
||||
{
|
||||
_queue = queue;
|
||||
_stream = queue._commandStream.GetBuffer();
|
||||
_length = (int)queue._commandStream.Length;
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (_index == -1)
|
||||
_index = 0;
|
||||
|
||||
if (_index >= _length)
|
||||
return false;
|
||||
|
||||
|
||||
_index += FromVlq(_stream[_index .. (_index + 5)], out int command);
|
||||
_current = _queue.Command[command];
|
||||
|
||||
HasParameters = _current.Length != 0;
|
||||
|
||||
if (!HasParameters)
|
||||
{
|
||||
_paramsIndex = -1;
|
||||
return true;
|
||||
}
|
||||
|
||||
int length;
|
||||
if (_current.Length < 0)
|
||||
{
|
||||
_index += FromVlq(_stream[_index .. (_index + 5)], out length);
|
||||
}
|
||||
else
|
||||
{
|
||||
length = _current.Length;
|
||||
}
|
||||
|
||||
_paramsIndex = _index;
|
||||
_paramLength = length;
|
||||
_index += length;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_index = -1;
|
||||
_current = null;
|
||||
}
|
||||
|
||||
public object? GetParameter()
|
||||
{
|
||||
return _current?.GetParams(_queue, _stream.AsSpan(_paramsIndex, _paramLength));
|
||||
}
|
||||
|
||||
public T GetParameter<T>()
|
||||
{
|
||||
if (_current is IDrawCommand<T> command)
|
||||
{
|
||||
return command.GetParams(_queue, _stream.AsSpan(_paramsIndex, _paramLength))!;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetParameter<T>([NotNullWhen(true)] out T? parameter)
|
||||
{
|
||||
if (_current is IDrawCommand<T> command)
|
||||
{
|
||||
parameter = command.GetParams(_queue, _stream.AsSpan(_paramsIndex, _paramLength))!;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
parameter = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface ICommandFrame
|
||||
{
|
||||
public IDrawCommand Command { get; }
|
||||
|
||||
public bool HasParameters { get; }
|
||||
|
||||
public object? GetParameter();
|
||||
|
||||
public T GetParameter<T>();
|
||||
|
||||
public bool TryGetParameter<T>([NotNullWhen(true)] out T? parameter);
|
||||
}
|
||||
|
||||
|
||||
public interface IDrawController
|
||||
{
|
||||
/// <summary>
|
||||
/// Ensures that the canvas is at least a certain size.
|
||||
/// </summary>
|
||||
/// <param name="bounds">The bounding box.</param>
|
||||
void EnsureBounds(Box2d bounds, float depth);
|
||||
|
||||
/// <summary>
|
||||
/// Write into the command stream.
|
||||
/// </summary>
|
||||
/// <param name="command">The command to write.</param>
|
||||
void Write(IDrawCommand command);
|
||||
|
||||
/// <summary>
|
||||
/// Write into the command stream.
|
||||
/// </summary>
|
||||
/// <param name="command">The command to write.</param>
|
||||
/// <param name="param">Any data associated with the command.</param>
|
||||
void Write(IDrawCommand command, ReadOnlySpan<byte> param);
|
||||
|
||||
/// <summary>
|
||||
/// Write into the command stream.
|
||||
/// </summary>
|
||||
/// <param name="command">The command to write.</param>
|
||||
/// <param name="param">Any data associated with the command.</param>
|
||||
void Write<T>(IDrawCommand command, T param) where T : IParameterSerializer<T>;
|
||||
|
||||
/// <summary>
|
||||
/// Write into the command stream.
|
||||
/// </summary>
|
||||
/// <param name="command">The command to write.</param>
|
||||
/// <param name="param">Any data associated with the command.</param>
|
||||
void Write<T1, T2>(T2 command, T1 param) where T2 : IDrawCommand<T1>;
|
||||
}
|
||||
}
|
||||
9
Dashboard.Drawing/IDrawQueuePaintable.cs
Normal file
9
Dashboard.Drawing/IDrawQueuePaintable.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using Dashboard.Windowing;
|
||||
|
||||
namespace Dashboard.Drawing
|
||||
{
|
||||
public interface IDrawQueuePaintable : IPaintable
|
||||
{
|
||||
DrawQueue DrawQueue { get; }
|
||||
}
|
||||
}
|
||||
13
Dashboard.Drawing/IDrawResource.cs
Normal file
13
Dashboard.Drawing/IDrawResource.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace Dashboard.Drawing
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for draw resources.
|
||||
/// </summary>
|
||||
public interface IDrawResource
|
||||
{
|
||||
/// <summary>
|
||||
/// The extension for this kind of resource.
|
||||
/// </summary>
|
||||
IDrawExtension Kind { get; }
|
||||
}
|
||||
}
|
||||
14
Dashboard.Drawing/IParameterSerializer.cs
Normal file
14
Dashboard.Drawing/IParameterSerializer.cs
Normal file
@ -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<T>
|
||||
{
|
||||
int Serialize(DrawQueue queue, Span<byte> bytes);
|
||||
void Deserialize(DrawQueue queue, ReadOnlySpan<byte> bytes);
|
||||
}
|
||||
}
|
||||
140
Dashboard.Drawing/TextExtension.cs
Normal file
140
Dashboard.Drawing/TextExtension.cs
Normal file
@ -0,0 +1,140 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Dashboard.Drawing
|
||||
{
|
||||
public class TextExtension : DrawExtension
|
||||
{
|
||||
public TextCommand TextCommand { get; }
|
||||
|
||||
private TextExtension() : base("DB_Text", new [] { BrushExtension.Instance })
|
||||
{
|
||||
TextCommand = new TextCommand(this);
|
||||
}
|
||||
|
||||
public static readonly TextExtension Instance = new TextExtension();
|
||||
}
|
||||
|
||||
public class TextCommand : IDrawCommand<TextCommandArgs>
|
||||
{
|
||||
public string Name { get; } = "Text";
|
||||
public IDrawExtension Extension { get; }
|
||||
public int Length { get; } = -1;
|
||||
|
||||
public TextCommand(TextExtension ext)
|
||||
{
|
||||
Extension = ext;
|
||||
}
|
||||
|
||||
public int WriteParams(DrawQueue queue, TextCommandArgs obj, Span<byte> param)
|
||||
{
|
||||
int size = Unsafe.SizeOf<Header>() + obj.Text.Length * sizeof(char) + sizeof(char);
|
||||
|
||||
if (param.Length < size)
|
||||
return size;
|
||||
|
||||
ref Header header = ref MemoryMarshal.Cast<byte, Header>(param[0..Unsafe.SizeOf<Header>()])[0];
|
||||
Span<char> text = MemoryMarshal.Cast<byte, char>(param[Unsafe.SizeOf<Header>()..]);
|
||||
|
||||
header = new Header()
|
||||
{
|
||||
// Font = queue.RequireResource(obj.Font),
|
||||
TextBrush = queue.RequireResource(obj.TextBrush),
|
||||
BorderBrush = (obj.BorderBrush != null) ? queue.RequireResource(obj.BorderBrush) : -1,
|
||||
BorderRadius = (obj.BorderBrush != null) ? obj.BorderRadius : 0f,
|
||||
Anchor = obj.Anchor,
|
||||
Position = obj.Position,
|
||||
BorderKind = obj.BorderKind,
|
||||
};
|
||||
obj.Text.CopyTo(text);
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
public TextCommandArgs GetParams(DrawQueue queue, ReadOnlySpan<byte> param)
|
||||
{
|
||||
Header header = MemoryMarshal.Cast<byte, Header>(param[0..Unsafe.SizeOf<Header>()])[0];
|
||||
ReadOnlySpan<char> text = MemoryMarshal.Cast<byte, char>(param[Unsafe.SizeOf<Header>()..]);
|
||||
|
||||
if (header.BorderBrush != -1 && header.BorderRadius != 0)
|
||||
{
|
||||
return new TextCommandArgs(
|
||||
(IFont)queue.Resources[header.Font],
|
||||
(IBrush)queue.Resources[header.TextBrush],
|
||||
header.Anchor,
|
||||
header.Position,
|
||||
text.ToString())
|
||||
{
|
||||
BorderBrush = (IBrush)queue.Resources[header.BorderBrush],
|
||||
BorderRadius = header.BorderRadius,
|
||||
BorderKind = header.BorderKind,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return new TextCommandArgs(
|
||||
(IFont)queue.Resources[header.Font],
|
||||
(IBrush)queue.Resources[header.TextBrush],
|
||||
header.Anchor,
|
||||
header.Position,
|
||||
text.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
int IDrawCommand.WriteParams(DrawQueue queue, object? obj, Span<byte> param)
|
||||
{
|
||||
return WriteParams(queue, (TextCommandArgs)obj!, param);
|
||||
}
|
||||
|
||||
object? IDrawCommand.GetParams(DrawQueue queue, ReadOnlySpan<byte> param)
|
||||
{
|
||||
return GetParams(queue, param);
|
||||
}
|
||||
|
||||
private struct Header
|
||||
{
|
||||
private int _flags;
|
||||
public int Font;
|
||||
public int TextBrush;
|
||||
public int BorderBrush;
|
||||
public Vector3 Position;
|
||||
public float BorderRadius;
|
||||
|
||||
public Anchor Anchor
|
||||
{
|
||||
get => (Anchor)(_flags & 0xF);
|
||||
set => _flags = (_flags & ~0xF) | (int)value;
|
||||
}
|
||||
|
||||
public BorderKind BorderKind
|
||||
{
|
||||
get => (_flags & INSET) switch
|
||||
{
|
||||
OUTSET => BorderKind.Outset,
|
||||
INSET => BorderKind.Inset,
|
||||
_ => BorderKind.Center,
|
||||
};
|
||||
set => _flags = value switch
|
||||
{
|
||||
BorderKind.Outset => (_flags & ~INSET) | OUTSET,
|
||||
BorderKind.Inset => (_flags & ~INSET) | INSET,
|
||||
_ => (_flags & ~INSET) | CENTER,
|
||||
};
|
||||
}
|
||||
|
||||
private const int INSET = 0x30;
|
||||
private const int CENTER = 0x00;
|
||||
private const int OUTSET = 0x10;
|
||||
}
|
||||
}
|
||||
|
||||
public record struct TextCommandArgs(IFont Font, IBrush TextBrush, Anchor Anchor, Vector3 Position, string Text)
|
||||
{
|
||||
public IBrush? BorderBrush { get; init; } = null;
|
||||
public float BorderRadius { get; init; } = 0;
|
||||
|
||||
public BorderKind BorderKind { get; init; } = BorderKind.Center;
|
||||
}
|
||||
}
|
||||
105
Dashboard.Drawing/Typesetter.cs
Normal file
105
Dashboard.Drawing/Typesetter.cs
Normal file
@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Reflection.PortableExecutable;
|
||||
|
||||
namespace Dashboard.Drawing
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for registered typesetters.
|
||||
/// </summary>
|
||||
public interface ITypeSetter
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the typesetter.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
SizeF MeasureString(IFont font, string value);
|
||||
|
||||
IFont LoadFont(Stream stream);
|
||||
IFont LoadFont(string path);
|
||||
// IFont LoadFont(NamedFont font);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class for typesetting related functions.
|
||||
/// </summary>
|
||||
public static class Typesetter
|
||||
{
|
||||
/// <summary>
|
||||
/// The typesetting backend for this instance.
|
||||
/// </summary>
|
||||
public static ITypeSetter Backend { get; set; } = new UndefinedTypeSetter();
|
||||
|
||||
public static string Name => Backend.Name;
|
||||
|
||||
public static SizeF MeasureString(IFont font, string value)
|
||||
{
|
||||
return Backend.MeasureString(font, value);
|
||||
}
|
||||
|
||||
public static IFont LoadFont(Stream stream)
|
||||
{
|
||||
return Backend.LoadFont(stream);
|
||||
}
|
||||
|
||||
public static IFont LoadFont(string path)
|
||||
{
|
||||
return Backend.LoadFont(path);
|
||||
}
|
||||
|
||||
public static IFont LoadFont(FileInfo file)
|
||||
{
|
||||
return Backend.LoadFont(file.FullName);
|
||||
}
|
||||
|
||||
// public static IFont LoadFont(NamedFont font)
|
||||
// {
|
||||
// return Backend.LoadFont(font);
|
||||
// }
|
||||
|
||||
public static IFont LoadFont(string family, float size, FontWeight weight = FontWeight.Normal,
|
||||
FontSlant slant = FontSlant.Normal, FontStretch stretch = FontStretch.Normal)
|
||||
{
|
||||
// return LoadFont(new NamedFont(family, size, weight, slant, stretch));
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
private class UndefinedTypeSetter : ITypeSetter
|
||||
{
|
||||
public string Name { get; } = "Undefined";
|
||||
|
||||
[DoesNotReturn]
|
||||
private void Except()
|
||||
{
|
||||
throw new InvalidOperationException("No typesetting backend is loaded.");
|
||||
}
|
||||
|
||||
public SizeF MeasureString(IFont font, string value)
|
||||
{
|
||||
Except();
|
||||
return default;
|
||||
}
|
||||
|
||||
public IFont LoadFont(Stream stream)
|
||||
{
|
||||
Except();
|
||||
return default;
|
||||
}
|
||||
|
||||
public IFont LoadFont(string path)
|
||||
{
|
||||
Except();
|
||||
return default;
|
||||
}
|
||||
|
||||
// public IFont LoadFont(NamedFont font)
|
||||
// {
|
||||
// Except();
|
||||
// return default;
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Dashboard.ImmediateUI/Dashboard.ImmediateUI.csproj
Normal file
9
Dashboard.ImmediateUI/Dashboard.ImmediateUI.csproj
Normal file
@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
234
Dashboard.ImmediateUI/DimUI.cs
Normal file
234
Dashboard.ImmediateUI/DimUI.cs
Normal file
@ -0,0 +1,234 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using Dashboard.Drawing;
|
||||
|
||||
namespace Dashboard.ImmediateUI
|
||||
{
|
||||
public class DimUIConfig
|
||||
{
|
||||
public Vector2 Margin = new Vector2(8, 4);
|
||||
public Vector2 Padding = new Vector2(4);
|
||||
public required IFont Font { get; init; }
|
||||
|
||||
public IBrush TextBrush = new SolidBrush(Color.Black);
|
||||
public IBrush DisabledText = new SolidBrush(Color.Gray);
|
||||
|
||||
public float ButtonBorderSize = 2f;
|
||||
public IBrush ButtonBorderBrush = new SolidBrush(Color.SteelBlue);
|
||||
public IBrush ButtonFillBrush = new SolidBrush(Color.SlateGray);
|
||||
public IBrush? ButtonShadowBrush = new SolidBrush(Color.FromArgb(32, Color.LightSteelBlue));
|
||||
public float ButtonShadowOffset = 2f;
|
||||
|
||||
public float InputBorderSize = 2f;
|
||||
public IBrush InputPlaceholderTextBrush = new SolidBrush(Color.SteelBlue);
|
||||
public IBrush InputBorderBrush = new SolidBrush(Color.SlateGray);
|
||||
public IBrush InputFillBrush = new SolidBrush(Color.LightGray);
|
||||
public IBrush? InputShadowBrush = new SolidBrush(Color.FromArgb(32, Color.LightSteelBlue));
|
||||
public float InputShadowOffset = -2f;
|
||||
|
||||
public float MenuBorderSize = 2f;
|
||||
public IBrush MenuBorderBrush = new SolidBrush(Color.SteelBlue);
|
||||
public IBrush MenuFillBrush = new SolidBrush(Color.SlateGray);
|
||||
public IBrush? MenuShadowBrush = new SolidBrush(Color.FromArgb(32, Color.LightSteelBlue));
|
||||
public float MenuShadowOffset = 2f;
|
||||
}
|
||||
|
||||
public class DimUI
|
||||
{
|
||||
private readonly DimUIConfig _config;
|
||||
private Vector2 _pen;
|
||||
private Box2d _bounds;
|
||||
private bool _firstLine = false;
|
||||
private bool _sameLine = false;
|
||||
private float _z = -1;
|
||||
private float _lineHeight;
|
||||
private DrawQueue _queue;
|
||||
|
||||
public DimUI(DimUIConfig config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
[MemberNotNull(nameof(_queue))]
|
||||
public void Begin(Box2d bounds, DrawQueue queue)
|
||||
{
|
||||
_bounds = bounds;
|
||||
_pen = _bounds.Min;
|
||||
_queue = queue;
|
||||
_firstLine = true;
|
||||
_lineHeight = 0;
|
||||
_z = -1;
|
||||
}
|
||||
|
||||
public void SameLine()
|
||||
{
|
||||
_sameLine = true;
|
||||
}
|
||||
|
||||
private void Line()
|
||||
{
|
||||
if (!_firstLine && !_sameLine)
|
||||
{
|
||||
_pen = new Vector2(_bounds.Left, _pen.Y + _lineHeight);
|
||||
}
|
||||
else
|
||||
{
|
||||
_firstLine = false;
|
||||
_sameLine = false;
|
||||
}
|
||||
|
||||
_pen.X += _config.Margin.X;
|
||||
_lineHeight = 0;
|
||||
}
|
||||
|
||||
private float Z()
|
||||
{
|
||||
return _z += 0.001f;
|
||||
}
|
||||
|
||||
public void Text(string text)
|
||||
{
|
||||
Line();
|
||||
|
||||
SizeF sz = Typesetter.MeasureString(_config.Font, text);
|
||||
float z = Z();
|
||||
float h = _config.Margin.Y * 2 + sz.Height;
|
||||
_queue.Text(new Vector3(_pen + new Vector2(0, _config.Margin.X), z), _config.TextBrush, text, _config.Font);
|
||||
|
||||
_lineHeight = Math.Max(_lineHeight, h);
|
||||
_pen.X += sz.Width;
|
||||
}
|
||||
|
||||
public void DrawBox(
|
||||
Vector2 position,
|
||||
Vector2 size,
|
||||
IBrush fill,
|
||||
IBrush border, float borderWidth,
|
||||
IBrush? shadow, float offset)
|
||||
{
|
||||
float z = Z();
|
||||
|
||||
if (shadow != null)
|
||||
{
|
||||
if (offset >= 0)
|
||||
{
|
||||
_queue.Rect(position + new Vector2(offset), position + size + new Vector2(offset + borderWidth), z, shadow);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Inset shadows are draw a bit weirdly.
|
||||
_queue.Rect(position, position + new Vector2(offset, size.Y), z, shadow);
|
||||
_queue.Rect(position + new Vector2(offset, 0), position + new Vector2(size.X - offset, offset), z, shadow);
|
||||
}
|
||||
}
|
||||
|
||||
_queue.Rect(position, position + size, z, fill, border, borderWidth, BorderKind.Outset);
|
||||
}
|
||||
|
||||
public bool Button(string label)
|
||||
{
|
||||
Line();
|
||||
|
||||
SizeF sz = Typesetter.MeasureString(_config.Font, label);
|
||||
|
||||
float h = _config.Margin.Y * 2 + _config.Padding.Y * 2 + sz.Height;
|
||||
DrawBox(
|
||||
_pen + new Vector2(0, _config.Margin.Y),
|
||||
new Vector2(sz.Width + 2 * _config.Padding.X, sz.Height + 2 * _config.Padding.Y),
|
||||
_config.ButtonFillBrush,
|
||||
_config.ButtonBorderBrush,
|
||||
_config.ButtonBorderSize,
|
||||
_config.ButtonShadowBrush,
|
||||
_config.ButtonShadowOffset);
|
||||
|
||||
float z = Z();
|
||||
|
||||
_queue.Text(new Vector3(_pen + new Vector2(_config.Padding.X, _config.Margin.Y + _config.Padding.Y), z),
|
||||
_config.TextBrush, label, _config.Font);
|
||||
_lineHeight = Math.Max(_lineHeight, h);
|
||||
|
||||
_pen.X += sz.Width + 2 * _config.Padding.X;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool Input(string placeholder, StringBuilder value)
|
||||
{
|
||||
Line();
|
||||
|
||||
IBrush textBrush;
|
||||
string str;
|
||||
if (value.Length == 0)
|
||||
{
|
||||
textBrush = _config.DisabledText;
|
||||
str = placeholder;
|
||||
}
|
||||
else
|
||||
{
|
||||
textBrush = _config.TextBrush;
|
||||
str = value.ToString();
|
||||
}
|
||||
|
||||
SizeF sz = Typesetter.MeasureString(_config.Font, str);
|
||||
|
||||
float h = _config.Margin.Y * 2 + _config.Padding.Y * 2 + sz.Height;
|
||||
DrawBox(
|
||||
_pen + new Vector2(0, _config.Margin.Y),
|
||||
new Vector2(sz.Width + 2 * _config.Padding.X, sz.Height + 2 * _config.Padding.Y),
|
||||
_config.InputFillBrush,
|
||||
_config.InputBorderBrush,
|
||||
_config.InputBorderSize,
|
||||
_config.InputShadowBrush,
|
||||
_config.InputShadowOffset);
|
||||
|
||||
float z = Z();
|
||||
|
||||
_queue.Text(new Vector3(_pen + new Vector2(_config.Padding.X, _config.Margin.Y + _config.Padding.Y), z),
|
||||
textBrush, str, _config.Font);
|
||||
_lineHeight = Math.Max(_lineHeight, h);
|
||||
|
||||
_pen.X += sz.Width + 2 * _config.Padding.X;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void BeginMenu()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public bool MenuItem(string name)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public void EndMenu()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public int Id(ReadOnlySpan<char> str)
|
||||
{
|
||||
// Uses the FVN-1A algorithm in 32-bit mode.
|
||||
const int PRIME = 0x01000193;
|
||||
const int BASIS = unchecked((int)0x811c9dc5);
|
||||
|
||||
int hash = BASIS;
|
||||
for (int i = 0; i < str.Length; i++)
|
||||
{
|
||||
hash ^= str[i] & 0xFF;
|
||||
hash *= PRIME;
|
||||
hash ^= str[i] >> 8;
|
||||
hash *= PRIME;
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
public void Finish()
|
||||
{
|
||||
// TODO:
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<LanguageVersion>7.3</LanguageVersion>
|
||||
<Nullable>disable</Nullable>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ReFuel.FreeType" Version="0.1.0-rc.5" />
|
||||
<PackageReference Include="ReFuel.StbImage" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Dashboard\Dashboard.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -1,13 +0,0 @@
|
||||
namespace Dashboard.Media.Defaults
|
||||
{
|
||||
internal static class EnvironmentVariables
|
||||
{
|
||||
public const string SerifFont = "QUIK_SERIF_FONT";
|
||||
public const string SansFont = "QUIK_SANS_FONT";
|
||||
public const string MonospaceFont = "QUIK_MONOSPACE_FONT";
|
||||
public const string CursiveFont = "QUIK_CURSIVE_FONT";
|
||||
public const string FantasyFont = "QUIK_FANTASY_FONT";
|
||||
|
||||
public const string FallbackFontDatabase = "QUIK_FALLBACK_FONT_DB";
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
using System;
|
||||
using ReFuel.FreeType;
|
||||
|
||||
namespace Dashboard.Media.Defaults
|
||||
{
|
||||
public static class FTProvider
|
||||
{
|
||||
private static FTLibrary _ft;
|
||||
public static FTLibrary Ft => _ft;
|
||||
|
||||
static FTProvider()
|
||||
{
|
||||
FT.InitFreeType(out _ft);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,269 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json;
|
||||
using ReFuel.FreeType;
|
||||
using Dashboard.Media.Font;
|
||||
using Dashboard.PAL;
|
||||
using Dashboard.Media.Defaults.Linux;
|
||||
|
||||
namespace Dashboard.Media.Defaults.Fallback
|
||||
{
|
||||
public class FallbackFontDatabase : IFontDataBase
|
||||
{
|
||||
private readonly string DbPath =
|
||||
Environment.GetEnvironmentVariable(EnvironmentVariables.FallbackFontDatabase) ??
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "QUIK/fontdb.json");
|
||||
|
||||
private Dictionary<FontFace, FileInfo> FilesMap { get; } = new Dictionary<FontFace, FileInfo>();
|
||||
private Dictionary<string, List<FontFace>> ByFamily { get; } = new Dictionary<string, List<FontFace>>();
|
||||
private Dictionary<SystemFontFamily, FontFace> SystemFonts { get; } = new Dictionary<SystemFontFamily, FontFace>();
|
||||
private List<FontFace> All { get; } = new List<FontFace>();
|
||||
|
||||
IEnumerable<FontFace> IFontDataBase.All => this.All;
|
||||
|
||||
public FallbackFontDatabase(bool rebuild = false)
|
||||
{
|
||||
// Load existing database if desired.
|
||||
List<DbEntry> database;
|
||||
|
||||
if(!rebuild)
|
||||
{
|
||||
database = LoadDatabase();
|
||||
}
|
||||
else
|
||||
{
|
||||
database = new List<DbEntry>();
|
||||
}
|
||||
|
||||
VerifyDatabase(database);
|
||||
FlushDatabase(database);
|
||||
|
||||
database.ForEach(x => AddFont(x.Face, new FileInfo(x.FilePath)));
|
||||
|
||||
(FontFace, FileInfo) serif = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.SerifFont, LinuxFonts.DefaultSerifFamilies, this);
|
||||
SystemFonts[SystemFontFamily.Serif] = serif.Item1;
|
||||
(FontFace, FileInfo) sans = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.SansFont, LinuxFonts.DefaultSansFamilies, this);
|
||||
SystemFonts[SystemFontFamily.Sans] = sans.Item1;
|
||||
(FontFace, FileInfo) mono = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.MonospaceFont, LinuxFonts.DefaultMonospaceFamilies, this);
|
||||
SystemFonts[SystemFontFamily.Monospace] = mono.Item1;
|
||||
(FontFace, FileInfo) cursive = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.CursiveFont, LinuxFonts.DefaultCursiveFamilies, this);
|
||||
SystemFonts[SystemFontFamily.Cursive] = cursive.Item1;
|
||||
(FontFace, FileInfo) fantasy = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.FantasyFont, LinuxFonts.DefaultFantasyFamilies, this);
|
||||
SystemFonts[SystemFontFamily.Fantasy] = fantasy.Item1;
|
||||
}
|
||||
|
||||
public FileInfo FontFileInfo(FontFace face)
|
||||
{
|
||||
if (FilesMap.TryGetValue(face, out FileInfo info))
|
||||
return info;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
public Stream Open(FontFace face)
|
||||
{
|
||||
return FontFileInfo(face)?.OpenRead() ?? throw new FileNotFoundException();
|
||||
}
|
||||
|
||||
public IEnumerable<FontFace> Search(FontFace prototype, FontMatchCriteria criteria = FontMatchCriteria.All)
|
||||
{
|
||||
// A bit scuffed and LINQ heavy but it should work.
|
||||
IEnumerable<FontFace> candidates;
|
||||
|
||||
if (criteria.HasFlag(FontMatchCriteria.Family))
|
||||
{
|
||||
List<FontFace> siblings;
|
||||
|
||||
if (!ByFamily.TryGetValue(prototype.Family, out siblings))
|
||||
{
|
||||
return Enumerable.Empty<FontFace>();
|
||||
}
|
||||
|
||||
candidates = siblings;
|
||||
}
|
||||
else
|
||||
{
|
||||
candidates = All;
|
||||
}
|
||||
|
||||
return
|
||||
candidates
|
||||
.Where(x =>
|
||||
implies(criteria.HasFlag(FontMatchCriteria.Slant), prototype.Slant == x.Slant) ||
|
||||
implies(criteria.HasFlag(FontMatchCriteria.Weight), prototype.Weight == x.Weight) ||
|
||||
implies(criteria.HasFlag(FontMatchCriteria.Stretch), prototype.Stretch == x.Stretch)
|
||||
)
|
||||
.OrderByDescending(x =>
|
||||
|
||||
(prototype.Slant == x.Slant ? 1 : 0) +
|
||||
(prototype.Weight == x.Weight ? 1 : 0) +
|
||||
(prototype.Stretch == x.Stretch ? 1 : 0) +
|
||||
confidence(prototype.Family, x.Family) * 3
|
||||
);
|
||||
|
||||
// a => b = a'+b
|
||||
static bool implies(bool a, bool b)
|
||||
{
|
||||
return !a || b;
|
||||
}
|
||||
|
||||
static int confidence(string target, string testee)
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < target.Length && i < testee.Length && target[i] == testee[i]; i++);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
public FontFace GetSystemFontFace(SystemFontFamily family)
|
||||
{
|
||||
return SystemFonts[family];
|
||||
}
|
||||
|
||||
private void AddFont(FontFace face, FileInfo file)
|
||||
{
|
||||
if (!All.Contains(face))
|
||||
All.Add(face);
|
||||
|
||||
FilesMap.TryAdd(face, file);
|
||||
|
||||
if (!ByFamily.TryGetValue(face.Family, out List<FontFace> siblings))
|
||||
{
|
||||
siblings = new List<FontFace>();
|
||||
ByFamily.Add(face.Family, siblings);
|
||||
}
|
||||
|
||||
if (!siblings.Contains(face))
|
||||
siblings.Add(face);
|
||||
}
|
||||
|
||||
private List<DbEntry> LoadDatabase()
|
||||
{
|
||||
FileInfo info = new FileInfo(DbPath);
|
||||
|
||||
if (!info.Exists)
|
||||
return new List<DbEntry>();
|
||||
|
||||
using Stream str = info.OpenRead();
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<List<DbEntry>>(str);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new List<DbEntry>();
|
||||
}
|
||||
}
|
||||
|
||||
private void VerifyDatabase(List<DbEntry> db)
|
||||
{
|
||||
// Very slow way to do this but how many fonts could a system have on average?
|
||||
Dictionary<string, DbEntry> entires = new Dictionary<string, DbEntry>();
|
||||
|
||||
foreach (DbEntry entry in db)
|
||||
{
|
||||
FileInfo info = new FileInfo(entry.FilePath);
|
||||
|
||||
// Reprocess fonts that appear like this.
|
||||
if (!info.Exists) continue;
|
||||
else if (info.LastWriteTime > entry.AccessTime) continue;
|
||||
}
|
||||
|
||||
string fontpath = null;
|
||||
try
|
||||
{
|
||||
fontpath = Environment.GetFolderPath(Environment.SpecialFolder.Fonts);
|
||||
if (string.IsNullOrEmpty(fontpath))
|
||||
throw new Exception();
|
||||
}
|
||||
catch
|
||||
{
|
||||
foreach (string path in FontPaths)
|
||||
{
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
fontpath = path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// rip
|
||||
if (string.IsNullOrEmpty(fontpath))
|
||||
return;
|
||||
}
|
||||
|
||||
SearchPathForFonts(entires, fontpath);
|
||||
|
||||
db.Clear();
|
||||
db.AddRange(entires.Values);
|
||||
}
|
||||
|
||||
private static void SearchPathForFonts(Dictionary<string, DbEntry> entries, string path)
|
||||
{
|
||||
DirectoryInfo dir = new DirectoryInfo(path);
|
||||
|
||||
foreach (FileInfo file in dir.EnumerateFiles())
|
||||
{
|
||||
SearchFileForFonts(entries, file);
|
||||
}
|
||||
|
||||
foreach (DirectoryInfo directory in dir.EnumerateDirectories())
|
||||
{
|
||||
SearchPathForFonts(entries, directory.FullName);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SearchFileForFonts(Dictionary<string, DbEntry> entries, FileInfo file)
|
||||
{
|
||||
if (entries.ContainsKey(file.FullName))
|
||||
return;
|
||||
|
||||
if (FT.NewFace(FTProvider.Ft, file.FullName, 0, out FTFace face) != FTError.None)
|
||||
return;
|
||||
|
||||
FontFace facename = FontFace.Parse(face.FamilyName, face.StyleName);
|
||||
|
||||
DbEntry entry = new DbEntry(facename, file.FullName);
|
||||
entries.Add(file.FullName, entry);
|
||||
FT.DoneFace(face);
|
||||
}
|
||||
|
||||
private void FlushDatabase(List<DbEntry> db)
|
||||
{
|
||||
FileInfo info = new FileInfo(DbPath);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(DbPath));
|
||||
using Stream str = info.Open(FileMode.Create);
|
||||
JsonSerializer.Serialize(str, db);
|
||||
}
|
||||
|
||||
private static readonly string[] FontPaths = new string[] {
|
||||
"/usr/share/fonts",
|
||||
};
|
||||
|
||||
[JsonSerializable(typeof(DbEntry))]
|
||||
private class DbEntry
|
||||
{
|
||||
[JsonIgnore] public FontFace Face => new FontFace(Family, Slant, Weight, Stretch);
|
||||
public string Family { get; set; }
|
||||
public FontSlant Slant { get; set; }
|
||||
public FontWeight Weight { get; set; }
|
||||
public FontStretch Stretch { get; set; }
|
||||
public string FilePath { get; set; }
|
||||
public DateTime AccessTime { get; set; }
|
||||
|
||||
public DbEntry() {}
|
||||
public DbEntry(FontFace face, string path)
|
||||
{
|
||||
Family = face.Family;
|
||||
Slant = face.Slant;
|
||||
Weight = face.Weight;
|
||||
Stretch = face.Stretch;
|
||||
FilePath = path;
|
||||
AccessTime = DateTime.Now;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,93 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using ReFuel.FreeType;
|
||||
using Dashboard.Media.Defaults.Fallback;
|
||||
using Dashboard.Media.Defaults.Linux;
|
||||
using Dashboard.Media.Font;
|
||||
using Dashboard.PAL;
|
||||
|
||||
namespace Dashboard.Media.Defaults
|
||||
{
|
||||
public static class FontDataBaseProvider
|
||||
{
|
||||
public static IFontDataBase Instance { get; }
|
||||
|
||||
static FontDataBaseProvider()
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO: add as other operating systems are supported.
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
Instance = new FontConfigFontDatabase();
|
||||
}
|
||||
else
|
||||
{
|
||||
Instance = new FallbackFontDatabase();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new NotSupportedException("Could not load a suitable font database implementation.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static (FontFace, FileInfo) ResolveSystemFont(string envVar, string defaults, IFontDataBase db)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
string user = Environment.GetEnvironmentVariable(envVar);
|
||||
if (user != null)
|
||||
{
|
||||
builder.Append(user);
|
||||
builder.Append(':');
|
||||
}
|
||||
|
||||
builder.Append(defaults);
|
||||
|
||||
string[] list = builder.ToString().Split(':');
|
||||
|
||||
foreach (string item in list)
|
||||
{
|
||||
if (File.Exists(item))
|
||||
{
|
||||
// Process file.
|
||||
if (FT.NewFace(FTProvider.Ft, item, 0, out FTFace ftface) != FTError.None)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FontFace face = FontFace.Parse(ftface.FamilyName, ftface.StyleName);
|
||||
FT.DoneFace(ftface);
|
||||
|
||||
return (face, new FileInfo(item));
|
||||
}
|
||||
else
|
||||
{
|
||||
IEnumerable<FontFace> faces = db.Search(
|
||||
new FontFace(item, FontSlant.Normal, FontWeight.Normal, FontStretch.Normal),
|
||||
FontMatchCriteria.Family);
|
||||
|
||||
if (faces.Any())
|
||||
{
|
||||
FontFace face = faces.First();
|
||||
return (face, db.FontFileInfo(face));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
FontFace face = db.GetSystemFontFace(SystemFontFamily.Sans);
|
||||
return (face, db.FontFileInfo(face));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new NotImplementedException("No fallback font yet.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using Dashboard.PAL;
|
||||
|
||||
namespace Dashboard.Media.Defaults
|
||||
{
|
||||
public class FreeTypeFontFactory : IFontFactory
|
||||
{
|
||||
public bool TryOpen(Stream stream, [NotNullWhen(true)] out QFont font)
|
||||
{
|
||||
try
|
||||
{
|
||||
font = new QFontFreeType(stream);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
font = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,230 +0,0 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using Dashboard;
|
||||
|
||||
namespace Dashboard.Media.Defaults
|
||||
{
|
||||
public static unsafe class FontConfig
|
||||
{
|
||||
private const string fontconfig = "fontconfig";
|
||||
|
||||
public static bool Exists { get; }
|
||||
|
||||
public static IntPtr FAMILY { get; } = Marshal.StringToHGlobalAnsi("family");
|
||||
public static IntPtr STYLE { get; } = Marshal.StringToHGlobalAnsi("style");
|
||||
public static IntPtr FILE { get; } = Marshal.StringToHGlobalAnsi("file");
|
||||
public static IntPtr WEIGHT { get; } = Marshal.StringToHGlobalAnsi("weight");
|
||||
public static IntPtr SLANT { get; } = Marshal.StringToHGlobalAnsi("slant");
|
||||
|
||||
|
||||
static FontConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (FcInitLoadConfigAndFonts() == null)
|
||||
{
|
||||
Exists = false;
|
||||
}
|
||||
Exists = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Exists = false;
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport(fontconfig, EntryPoint = "FcInitLoadConfigAndFonts")]
|
||||
public static extern FcConfig* FcInitLoadConfigAndFonts();
|
||||
|
||||
[DllImport(fontconfig, EntryPoint = "FcConfigGetCurrent")]
|
||||
public static extern FcConfig ConfigGetCurrent();
|
||||
|
||||
[DllImport(fontconfig, EntryPoint = "FcPatternCreate")]
|
||||
public static extern FcPattern PatternCreate();
|
||||
|
||||
[DllImport(fontconfig, EntryPoint = "FcPatternCreate")]
|
||||
public static extern bool FcPatternAdd(FcPattern pattern, IntPtr what, FcValue value, bool append);
|
||||
|
||||
[DllImport(fontconfig, EntryPoint = "FcObjectSetBuild", CallingConvention = CallingConvention.Cdecl)]
|
||||
public static extern FcObjectSet ObjectSetBuild(IntPtr i1, IntPtr i2, IntPtr i3, IntPtr i4, IntPtr i5, IntPtr i6);
|
||||
|
||||
public static FcObjectSet ObjectSetBuild(IntPtr i1)
|
||||
{
|
||||
return ObjectSetBuild(i1, IntPtr.Zero);
|
||||
}
|
||||
|
||||
public static FcObjectSet ObjectSetBuild(IntPtr i1, IntPtr i2)
|
||||
{
|
||||
return ObjectSetBuild(i1, i2, IntPtr.Zero);
|
||||
}
|
||||
public static FcObjectSet ObjectSetBuild(IntPtr i1, IntPtr i2, IntPtr i3)
|
||||
{
|
||||
return ObjectSetBuild(i1, i2, i3, IntPtr.Zero);
|
||||
}
|
||||
public static FcObjectSet ObjectSetBuild(IntPtr i1, IntPtr i2, IntPtr i3, IntPtr i4)
|
||||
{
|
||||
return ObjectSetBuild(i1, i2, i3, i4, IntPtr.Zero);
|
||||
}
|
||||
public static FcObjectSet ObjectSetBuild(IntPtr i1, IntPtr i2, IntPtr i3, IntPtr i4, IntPtr i5)
|
||||
{
|
||||
return ObjectSetBuild(i1, i2, i3, i4, i5, IntPtr.Zero);
|
||||
}
|
||||
|
||||
[DllImport(fontconfig, EntryPoint = "FcFontList")]
|
||||
public static extern FcFontSet FontList(FcConfig config, FcPattern pattern, FcObjectSet objectSet);
|
||||
|
||||
[DllImport(fontconfig, EntryPoint = "FcNameUnparse")]
|
||||
public static extern IntPtr NameUnparse(FcPattern pat);
|
||||
|
||||
public static string NameUnparseStr(FcPattern pat) => Marshal.PtrToStringAnsi(NameUnparse(pat));
|
||||
|
||||
[DllImport(fontconfig, EntryPoint = "FcPatternGetString")]
|
||||
public static extern FcResult PatternGetString(FcPattern p, IntPtr what, int n, out IntPtr val);
|
||||
|
||||
public static FcResult PatternGetString(FcPattern p, IntPtr what, out string str)
|
||||
{
|
||||
FcResult i = PatternGetString(p, what, 0, out IntPtr ptr);
|
||||
|
||||
if (i == FcResult.Match)
|
||||
{
|
||||
str = Marshal.PtrToStringAnsi(ptr);
|
||||
}
|
||||
else
|
||||
{
|
||||
str = null;
|
||||
}
|
||||
|
||||
Marshal.FreeHGlobal(ptr);
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
[DllImport(fontconfig, EntryPoint = "FcPatternGet")]
|
||||
public static extern FcResult PatternGet(FcPattern p, IntPtr what, int id, out FcValue value);
|
||||
|
||||
[DllImport(fontconfig, EntryPoint = "FcFontSetDestroy")]
|
||||
public static extern void FontSetDestroy(FcFontSet fs);
|
||||
|
||||
[DllImport(fontconfig, EntryPoint = "FcObjectSetDestroy")]
|
||||
public static extern void ObjectSetDestroy (FcObjectSet os);
|
||||
|
||||
[DllImport(fontconfig, EntryPoint = "FcConfigDestroy")]
|
||||
public static extern void ConfigDestroy (FcConfig cfg);
|
||||
|
||||
[DllImport(fontconfig, EntryPoint = "FcPatternDestroy")]
|
||||
public static extern void PatternDestroy (FcPattern os);
|
||||
|
||||
#region Range
|
||||
|
||||
[DllImport(fontconfig, EntryPoint = "FcRangeCreateDouble")]
|
||||
public static extern IntPtr RangeCreateDouble(double begin, double end);
|
||||
|
||||
[DllImport(fontconfig, EntryPoint = "FcRangeCreateInteger")]
|
||||
public static extern IntPtr RangeCreateInteger (int begin, int end);
|
||||
|
||||
[DllImport(fontconfig, EntryPoint = "FcRangeDestroy")]
|
||||
public static extern void RangeDestroy(IntPtr range);
|
||||
|
||||
[DllImport(fontconfig, EntryPoint = "FcRangeCopy")]
|
||||
public static extern IntPtr RangeCopy (IntPtr range);
|
||||
|
||||
[DllImport(fontconfig, EntryPoint = "FcRangeGetDouble")]
|
||||
public static extern bool RangeGetDouble(IntPtr range, out double start, out double end);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public enum FcResult
|
||||
{
|
||||
Match,
|
||||
NoMatch,
|
||||
TypeMismatch,
|
||||
NoId,
|
||||
OutOfMemory
|
||||
}
|
||||
|
||||
public struct FcConfig
|
||||
{
|
||||
public readonly IntPtr Handle;
|
||||
}
|
||||
|
||||
public struct FcPattern
|
||||
{
|
||||
public readonly IntPtr Handle;
|
||||
}
|
||||
|
||||
public unsafe struct FcObjectSet
|
||||
{
|
||||
public readonly IntPtr Handle;
|
||||
|
||||
private Accessor* AsPtr => (Accessor*)Handle;
|
||||
|
||||
public int NObject => AsPtr->nobject;
|
||||
|
||||
public int SObject => AsPtr->sobject;
|
||||
|
||||
#pragma warning disable CS0649 // Will always have default value.
|
||||
private struct Accessor
|
||||
{
|
||||
public int nobject;
|
||||
public int sobject;
|
||||
public byte** objects;
|
||||
}
|
||||
#pragma warning restore CS0649
|
||||
}
|
||||
|
||||
public unsafe struct FcFontSet
|
||||
{
|
||||
public readonly IntPtr Handle;
|
||||
private Accessor* AsPtr => (Accessor*)Handle;
|
||||
|
||||
public int NFont => AsPtr->nfont;
|
||||
public int SFont => AsPtr->sfont;
|
||||
|
||||
public FcPattern this[int i]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (i < 0 || i >= NFont)
|
||||
throw new IndexOutOfRangeException();
|
||||
|
||||
return AsPtr->fonts[i];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable CS0649 // Will always have default value.
|
||||
private struct Accessor
|
||||
{
|
||||
public int nfont;
|
||||
public int sfont;
|
||||
public FcPattern* fonts;
|
||||
}
|
||||
#pragma warning restore CS0649
|
||||
}
|
||||
|
||||
public enum FcType
|
||||
{
|
||||
Unknown = -1,
|
||||
Void,
|
||||
Integer,
|
||||
Double,
|
||||
String,
|
||||
Bool,
|
||||
Matrix,
|
||||
CharSet,
|
||||
FTFace,
|
||||
LangSet,
|
||||
Range
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
public readonly struct FcValue
|
||||
{
|
||||
[FieldOffset(0)] public readonly FcType Type;
|
||||
[FieldOffset(sizeof(FcType))] public readonly IntPtr Pointer;
|
||||
[FieldOffset(sizeof(FcType))] public readonly int Int;
|
||||
[FieldOffset(sizeof(FcType))] public readonly double Double;
|
||||
}
|
||||
}
|
||||
@ -1,168 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using ReFuel.FreeType;
|
||||
using Dashboard.Media.Font;
|
||||
using Dashboard.PAL;
|
||||
|
||||
namespace Dashboard.Media.Defaults.Linux
|
||||
{
|
||||
/// <summary>
|
||||
/// Font database for Linux libfontconfig systems.
|
||||
/// </summary>
|
||||
public class FontConfigFontDatabase : IFontDataBase
|
||||
{
|
||||
private Dictionary<FontFace, FileInfo> FilesMap { get; } = new Dictionary<FontFace, FileInfo>();
|
||||
private Dictionary<string, List<FontFace>> ByFamily { get; } = new Dictionary<string, List<FontFace>>();
|
||||
private Dictionary<SystemFontFamily, FontFace> SystemFonts { get; } = new Dictionary<SystemFontFamily, FontFace>();
|
||||
private List<FontFace> All { get; } = new List<FontFace>();
|
||||
|
||||
IEnumerable<FontFace> IFontDataBase.All => this.All;
|
||||
|
||||
public FontConfigFontDatabase()
|
||||
{
|
||||
if (!FontConfig.Exists)
|
||||
{
|
||||
throw new NotSupportedException("This host doesn't have fontconfig installed.");
|
||||
}
|
||||
|
||||
FcConfig config = FontConfig.ConfigGetCurrent();
|
||||
FcPattern pattern = FontConfig.PatternCreate();
|
||||
FcObjectSet os = FontConfig.ObjectSetBuild(FontConfig.FAMILY, FontConfig.STYLE, FontConfig.FILE);
|
||||
FcFontSet fs = FontConfig.FontList(config, pattern, os);
|
||||
|
||||
for (int i = 0; i < fs.NFont; i++)
|
||||
{
|
||||
FcPattern current = fs[i];
|
||||
|
||||
if (
|
||||
FontConfig.PatternGetString(current, FontConfig.FAMILY, 0, out IntPtr pFamily) != FcResult.Match ||
|
||||
FontConfig.PatternGetString(current, FontConfig.STYLE, 0, out IntPtr pStyle) != FcResult.Match)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string family = Marshal.PtrToStringUTF8(pFamily);
|
||||
string style = Marshal.PtrToStringUTF8(pStyle);
|
||||
|
||||
FontFace face = FontFace.Parse(family, style);
|
||||
|
||||
FontConfig.PatternGetString(current, FontConfig.FILE, 0, out IntPtr pFile);
|
||||
string file = Marshal.PtrToStringAnsi(pFile);
|
||||
|
||||
AddFont(face, new FileInfo(file));
|
||||
}
|
||||
|
||||
FontConfig.FontSetDestroy(fs);
|
||||
FontConfig.ObjectSetDestroy(os);
|
||||
FontConfig.PatternDestroy(pattern);
|
||||
|
||||
(FontFace, FileInfo) serif = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.SerifFont, LinuxFonts.DefaultSerifFamilies, this);
|
||||
SystemFonts[SystemFontFamily.Serif] = serif.Item1;
|
||||
(FontFace, FileInfo) sans = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.SansFont, LinuxFonts.DefaultSansFamilies, this);
|
||||
SystemFonts[SystemFontFamily.Sans] = sans.Item1;
|
||||
(FontFace, FileInfo) mono = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.MonospaceFont, LinuxFonts.DefaultMonospaceFamilies, this);
|
||||
SystemFonts[SystemFontFamily.Monospace] = mono.Item1;
|
||||
(FontFace, FileInfo) cursive = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.CursiveFont, LinuxFonts.DefaultCursiveFamilies, this);
|
||||
SystemFonts[SystemFontFamily.Cursive] = cursive.Item1;
|
||||
(FontFace, FileInfo) fantasy = FontDataBaseProvider.ResolveSystemFont(EnvironmentVariables.FantasyFont, LinuxFonts.DefaultFantasyFamilies, this);
|
||||
SystemFonts[SystemFontFamily.Fantasy] = fantasy.Item1;
|
||||
|
||||
AddFont(serif.Item1, serif.Item2);
|
||||
AddFont(sans.Item1, sans.Item2);
|
||||
AddFont(mono.Item1, mono.Item2);
|
||||
AddFont(cursive.Item1, cursive.Item2);
|
||||
AddFont(fantasy.Item1, fantasy.Item2);
|
||||
|
||||
}
|
||||
|
||||
private void AddFont(FontFace face, FileInfo file)
|
||||
{
|
||||
if (!All.Contains(face))
|
||||
All.Add(face);
|
||||
|
||||
FilesMap.TryAdd(face, file);
|
||||
|
||||
if (!ByFamily.TryGetValue(face.Family, out List<FontFace> siblings))
|
||||
{
|
||||
siblings = new List<FontFace>();
|
||||
ByFamily.Add(face.Family, siblings);
|
||||
}
|
||||
|
||||
if (!siblings.Contains(face))
|
||||
siblings.Add(face);
|
||||
}
|
||||
|
||||
public IEnumerable<FontFace> Search(FontFace prototype, FontMatchCriteria criteria = FontMatchCriteria.All)
|
||||
{
|
||||
// A bit scuffed and LINQ heavy but it should work.
|
||||
IEnumerable<FontFace> candidates;
|
||||
|
||||
if (criteria.HasFlag(FontMatchCriteria.Family))
|
||||
{
|
||||
List<FontFace> siblings;
|
||||
|
||||
if (!ByFamily.TryGetValue(prototype.Family, out siblings))
|
||||
{
|
||||
return Enumerable.Empty<FontFace>();
|
||||
}
|
||||
|
||||
candidates = siblings;
|
||||
}
|
||||
else
|
||||
{
|
||||
candidates = All;
|
||||
}
|
||||
|
||||
return
|
||||
candidates
|
||||
.Where(x =>
|
||||
implies(criteria.HasFlag(FontMatchCriteria.Slant), prototype.Slant == x.Slant) ||
|
||||
implies(criteria.HasFlag(FontMatchCriteria.Weight), prototype.Weight == x.Weight) ||
|
||||
implies(criteria.HasFlag(FontMatchCriteria.Stretch), prototype.Stretch == x.Stretch)
|
||||
)
|
||||
.OrderByDescending(x =>
|
||||
|
||||
(prototype.Slant == x.Slant ? 1 : 0) +
|
||||
(prototype.Weight == x.Weight ? 1 : 0) +
|
||||
(prototype.Stretch == x.Stretch ? 1 : 0) +
|
||||
confidence(prototype.Family, x.Family) * 3
|
||||
);
|
||||
|
||||
// a => b = a'+b
|
||||
static bool implies(bool a, bool b)
|
||||
{
|
||||
return !a || b;
|
||||
}
|
||||
|
||||
static int confidence(string target, string testee)
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < target.Length && i < testee.Length && target[i] == testee[i]; i++);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
public FileInfo FontFileInfo(FontFace face)
|
||||
{
|
||||
if (FilesMap.TryGetValue(face, out FileInfo info))
|
||||
return info;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
public Stream Open(FontFace face)
|
||||
{
|
||||
return FontFileInfo(face)?.OpenRead() ?? throw new FileNotFoundException();
|
||||
}
|
||||
|
||||
public FontFace GetSystemFontFace(SystemFontFamily family)
|
||||
{
|
||||
return SystemFonts[family];
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
namespace Dashboard.Media.Defaults.Linux
|
||||
{
|
||||
internal static class LinuxFonts
|
||||
{
|
||||
public const string DefaultSerifFamilies = "Noto Serif:Nimbus Roman:Liberation Serif:FreeSerif:Times:Times New Roman";
|
||||
public const string DefaultSansFamilies = "Noto Sans:Nimbus Sans:Droid Sans:Liberation Sans:FreeSans:Helvetica Neue:Helvetica:Arial";
|
||||
public const string DefaultMonospaceFamilies = "Noto Mono:Nimbus Mono PS:Liberation Mono:DejaVu Mono:FreeMono:Lucida Console:Consolas:Courier:Courier New";
|
||||
public const string DefaultCursiveFamilies = "";
|
||||
public const string DefaultFantasyFamilies = "";
|
||||
}
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
using ReFuel.FreeType;
|
||||
using Dashboard.Media.Color;
|
||||
using Dashboard.Media.Font;
|
||||
|
||||
namespace Dashboard.Media.Defaults
|
||||
{
|
||||
public class QFontFreeType : QFont
|
||||
{
|
||||
private MemoryStream ms;
|
||||
private FTFace face;
|
||||
|
||||
public override FontFace Face => throw new NotImplementedException();
|
||||
|
||||
public QFontFreeType(Stream stream)
|
||||
{
|
||||
ms = new MemoryStream();
|
||||
stream.CopyTo(ms);
|
||||
|
||||
FTError e = FT.NewMemoryFace(Ft, ms.GetBuffer(), ms.Length, 0, out face);
|
||||
if (e != FTError.None)
|
||||
{
|
||||
throw new Exception("Could not load font face from stream.");
|
||||
}
|
||||
}
|
||||
|
||||
public override bool HasRune(int rune)
|
||||
{
|
||||
return FT.GetCharIndex(face, (ulong)rune) != 0;
|
||||
}
|
||||
|
||||
protected override QImage Render(out QGlyphMetrics metrics, int codepoint, float size, in FontRasterizerOptions options)
|
||||
{
|
||||
FT.SetCharSize(face, 0, (long)Math.Round(64*size), 0, (uint)Math.Round(options.Resolution));
|
||||
|
||||
uint index = FT.GetCharIndex(face, (ulong)codepoint);
|
||||
FT.LoadGlyph(face, index, FTLoadFlags.Default);
|
||||
|
||||
ref readonly FTGlyphMetrics ftmetrics = ref face.Glyph.Metrics;
|
||||
metrics = new QGlyphMetrics(codepoint,
|
||||
new QVec2(ftmetrics.Width/64f, ftmetrics.Height/64f),
|
||||
new QVec2(ftmetrics.HorizontalBearingX/64f, ftmetrics.HorizontalBearingY/64f),
|
||||
new QVec2(ftmetrics.VerticalBearingX/64f, ftmetrics.VerticalBearingY/64f),
|
||||
new QVec2(ftmetrics.HorizontalAdvance/64f, ftmetrics.VerticalAdvance/64f)
|
||||
);
|
||||
|
||||
FT.RenderGlyph(face.Glyph, options.Sdf ? FTRenderMode.Sdf : FTRenderMode.Normal);
|
||||
ref readonly FTBitmap bitmap = ref face.Glyph.Bitmap;
|
||||
|
||||
if (bitmap.Width == 0 || bitmap.Pitch == 0 || bitmap.Buffer == IntPtr.Zero)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
QImageBuffer image = new QImageBuffer(QImageFormat.AlphaU8, (int)bitmap.Width, (int)bitmap.Rows);
|
||||
image.LockBits2d(out QImageLock lk, QImageLockOptions.Default);
|
||||
|
||||
unsafe
|
||||
{
|
||||
Buffer.MemoryCopy((void*)bitmap.Buffer, (void*)lk.ImagePtr, lk.Width * lk.Height, bitmap.Width * bitmap.Rows);
|
||||
}
|
||||
|
||||
image.UnlockBits();
|
||||
return image;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
ms.Dispose();
|
||||
}
|
||||
|
||||
FT.DoneFace(face);
|
||||
}
|
||||
|
||||
private static FTLibrary Ft => FTProvider.Ft;
|
||||
}
|
||||
}
|
||||
@ -1,98 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Dashboard.Media.Color;
|
||||
using ReFuel.Stb;
|
||||
|
||||
namespace Dashboard.Media.Defaults
|
||||
{
|
||||
public unsafe class QImageStbi : QImage
|
||||
{
|
||||
private readonly StbImage image;
|
||||
private QImageBuffer buffer;
|
||||
private bool isSdf = false;
|
||||
|
||||
public override int Width => image.Width;
|
||||
|
||||
public override int Height => image.Height;
|
||||
|
||||
public override int Depth => 1;
|
||||
public override bool IsSdf => isSdf;
|
||||
public override QImageFormat InternalFormat => Stb2QImageFormat(image.Format);
|
||||
|
||||
public QImageStbi(Stream source)
|
||||
{
|
||||
// According to the stbi documentation, only a specific type of PNG
|
||||
// files are premultiplied out of the box (iPhone PNG). Take the
|
||||
// precision loss L and move on.
|
||||
StbImage.FlipVerticallyOnLoad = true;
|
||||
StbImage.UnpremultiplyOnLoad = true;
|
||||
|
||||
image = StbImage.Load(source);
|
||||
}
|
||||
|
||||
public static QImageFormat Stb2QImageFormat(StbiImageFormat src)
|
||||
{
|
||||
switch (src)
|
||||
{
|
||||
case StbiImageFormat.Grey: return QImageFormat.RedU8;
|
||||
case StbiImageFormat.Rgb: return QImageFormat.RgbU8;
|
||||
case StbiImageFormat.Rgba: return QImageFormat.RgbaU8;
|
||||
case StbiImageFormat.GreyAlpha: return QImageFormat.RaU8;
|
||||
default: return QImageFormat.Undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public override void LockBits2d(out QImageLock imageLock, QImageLockOptions options)
|
||||
{
|
||||
if (options.MipLevel > 0) throw new Exception("This image has no mip levels.");
|
||||
|
||||
buffer?.Dispose();
|
||||
buffer = new QImageBuffer(options.Format, Width, Height);
|
||||
buffer.LockBits2d(out QImageLock dst, QImageLockOptions.Default);
|
||||
|
||||
byte *srcPtr = (byte*)image.ImagePointer;
|
||||
QImageLock src = new QImageLock(InternalFormat, Width, Height, 1, (IntPtr)srcPtr);
|
||||
FormatConvert.Convert(dst, src);
|
||||
|
||||
if (options.Premultiply)
|
||||
{
|
||||
FormatConvert.Premultiply(dst);
|
||||
}
|
||||
|
||||
imageLock = dst;
|
||||
}
|
||||
|
||||
public override void LockBits3d(out QImageLock imageLock, QImageLockOptions options)
|
||||
{
|
||||
LockBits2d(out imageLock, options);
|
||||
}
|
||||
|
||||
public override void LockBits3d(out QImageLock imageLock, QImageLockOptions options, int depth)
|
||||
{
|
||||
if (depth != 1) throw new ArgumentOutOfRangeException(nameof(depth));
|
||||
|
||||
LockBits2d(out imageLock, options);
|
||||
}
|
||||
|
||||
public override void UnlockBits()
|
||||
{
|
||||
buffer.UnlockBits();
|
||||
}
|
||||
|
||||
public void SdfHint(bool value = true)
|
||||
{
|
||||
isSdf = value;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
buffer?.Dispose();
|
||||
image.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,155 +0,0 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using Dashboard.Media.Font;
|
||||
|
||||
// WebRequest is obsolete but runs on .NET framework.
|
||||
#pragma warning disable SYSLIB0014
|
||||
|
||||
namespace Dashboard.Media.Defaults
|
||||
{
|
||||
public class StbMediaLoader : MediaLoader<string>, MediaLoader<Uri>, MediaLoader<FileInfo>, MediaLoader<FontFace>
|
||||
{
|
||||
public bool AllowRemoteTransfers { get; set; } = false;
|
||||
private readonly ArrayPool<byte> ByteArrays = ArrayPool<byte>.Create();
|
||||
|
||||
public IDisposable GetMedia(object key, MediaHint hint)
|
||||
{
|
||||
Type t = key.GetType();
|
||||
/**/ if (t == typeof(string))
|
||||
{
|
||||
return GetMedia((string)key, hint);
|
||||
}
|
||||
else if (t == typeof(Uri))
|
||||
{
|
||||
return GetMedia((Uri)key, hint);
|
||||
}
|
||||
else if (t == typeof(FileInfo))
|
||||
{
|
||||
return GetMedia((FileInfo)key, hint);
|
||||
}
|
||||
else if (t == typeof(FontFace))
|
||||
{
|
||||
return GetMedia((FontFace)key, hint);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public IDisposable GetMedia(Uri uri, MediaHint hint)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IDisposable GetMedia(string str, MediaHint hint)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IDisposable GetMedia(FileInfo file, MediaHint hint)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IDisposable GetMedia(FontFace key, MediaHint hint)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Stream OpenResource(FileInfo file)
|
||||
{
|
||||
if (file.Exists)
|
||||
{
|
||||
return file.Open(FileMode.Open);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Stream OpenResource(Uri uri)
|
||||
{
|
||||
switch (uri.Scheme)
|
||||
{
|
||||
case "http":
|
||||
case "https":
|
||||
if (!AllowRemoteTransfers) return null;
|
||||
|
||||
try
|
||||
{
|
||||
WebRequest request = HttpWebRequest.Create(uri);
|
||||
WebResponse response = request.GetResponse();
|
||||
MemoryStream stream = new MemoryStream();
|
||||
|
||||
response.GetResponseStream().CopyTo(stream);
|
||||
response.Close();
|
||||
|
||||
stream.Position = 0;
|
||||
return stream;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
case "file":
|
||||
return OpenResource(new FileInfo(uri.AbsolutePath));
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Stream OpenResource(string key)
|
||||
{
|
||||
if (File.Exists(key))
|
||||
{
|
||||
return File.Open(key, FileMode.Open);
|
||||
}
|
||||
else if (Uri.TryCreate(key, UriKind.RelativeOrAbsolute, out Uri uri))
|
||||
{
|
||||
return OpenResource(uri);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
MediaHint InferMedia(Stream str, MediaHint hint)
|
||||
{
|
||||
if (hint != MediaHint.None)
|
||||
{
|
||||
return hint;
|
||||
}
|
||||
|
||||
byte[] array = ByteArrays.Rent(4);
|
||||
str.Read(array, 0, 4);
|
||||
str.Position = 0;
|
||||
|
||||
foreach (var(type, seq) in MediaTypes)
|
||||
{
|
||||
if (seq.SequenceEqual(array))
|
||||
return hint;
|
||||
}
|
||||
|
||||
return MediaHint.None;
|
||||
}
|
||||
|
||||
private readonly (MediaHint, byte[])[] MediaTypes = new (MediaHint, byte[])[] {
|
||||
(MediaHint.Image, new byte[] { 0x42, 0x4d }), /* .bmp `BM` */
|
||||
(MediaHint.Image, new byte[] { 0x47, 0x49, 0x46, 0x38 }), /* .gif `GIF8` */
|
||||
(MediaHint.Image, new byte[] { 0xff, 0xd8, 0xff, 0xe0 }), /* .jpg (JFIF) */
|
||||
(MediaHint.Image, new byte[] { 0xff, 0xd8, 0xff, 0xe1 }), /* .jpg (EXIF) */
|
||||
(MediaHint.Image, new byte[] { 0x89, 0x50, 0x4e, 0x47 }), /* .png `.PNG `*/
|
||||
(MediaHint.Image, new byte[] { 0x4d, 0x4d, 0x00, 0x2a }), /* .tif (motorola) */
|
||||
(MediaHint.Image, new byte[] { 0x49, 0x49, 0x2a, 0x00 }), /* .tif (intel) */
|
||||
(MediaHint.Font, new byte[] { 0x00, 0x01, 0x00, 0x00 }), /* .ttf */
|
||||
(MediaHint.Font, new byte[] { 0x4F, 0x54, 0x54, 0x4F }), /* .otf */
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,151 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#if false
|
||||
namespace Quik.Media.Defaults.Win32
|
||||
{
|
||||
|
||||
public class EnumerateFonts
|
||||
{
|
||||
private const byte DEFAULT_CHARSET = 1;
|
||||
|
||||
public static void Enumerate(FontFace font)
|
||||
{
|
||||
/* It's windows, just borrow the desktop window. */
|
||||
IntPtr hdc = GetDC(GetDesktopWindow());
|
||||
|
||||
List<(LogFontA, TextMetricA)> list = new List<(LogFontA, TextMetricA)>();
|
||||
|
||||
LogFontA font2 = new LogFontA()
|
||||
{
|
||||
//FaceName = font.Family,
|
||||
Weight = ((font.Style & FontStyle.Bold) != 0) ? FontWeight.Bold : FontWeight.Regular,
|
||||
Italic = (font.Style & FontStyle.Italic) != 0,
|
||||
CharSet = DEFAULT_CHARSET
|
||||
};
|
||||
|
||||
Console.WriteLine(font2.FaceName);
|
||||
|
||||
EnumFontFamiliesExProc proc = (in LogFontA font, in TextMetricA metric, int type, IntPtr lparam) =>
|
||||
{
|
||||
list.Add((font, metric));
|
||||
return 0;
|
||||
};
|
||||
|
||||
EnumFontFamiliesExA(hdc, font2, proc, IntPtr.Zero, 0);
|
||||
}
|
||||
|
||||
private const string gdi32 = "Gdi32.dll";
|
||||
private const string user32 = "User32.dll";
|
||||
|
||||
[DllImport(gdi32)]
|
||||
private static extern int EnumFontFamiliesExA(
|
||||
IntPtr hdc,
|
||||
in LogFontA font,
|
||||
[MarshalAs(UnmanagedType.FunctionPtr)] EnumFontFamiliesExProc proc,
|
||||
IntPtr lparam,
|
||||
int flags /* Should be zero. */);
|
||||
|
||||
[DllImport(user32)]
|
||||
private static extern IntPtr /* HWND */ GetDesktopWindow();
|
||||
|
||||
[DllImport(user32)]
|
||||
private static extern IntPtr /* HDC */ GetDC(IntPtr hwnd);
|
||||
|
||||
private delegate int EnumFontFamiliesExProc(in LogFontA font, in TextMetricA metric, int fontType, IntPtr lParam);
|
||||
|
||||
private struct LogFontA
|
||||
{
|
||||
public long Height;
|
||||
public long Width;
|
||||
public long Escapement;
|
||||
public long Orientation;
|
||||
public FontWeight Weight;
|
||||
[MarshalAs(UnmanagedType.U1)]
|
||||
public bool Italic;
|
||||
[MarshalAs(UnmanagedType.U1)]
|
||||
public bool Underline;
|
||||
[MarshalAs(UnmanagedType.U1)]
|
||||
public bool StrikeOut;
|
||||
public byte CharSet;
|
||||
public byte OutPrecision;
|
||||
public byte ClipPrecision;
|
||||
public byte PitchAndFamily;
|
||||
private unsafe fixed byte aFaceName[32];
|
||||
public unsafe string FaceName
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (byte* str = aFaceName)
|
||||
{
|
||||
int len = 0;
|
||||
for (; str[len] != 0 && len < 32; len++) ;
|
||||
return Encoding.UTF8.GetString(str, len);
|
||||
}
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
fixed (byte *str = aFaceName)
|
||||
{
|
||||
Span<byte> span = new Span<byte>(str, 32);
|
||||
Encoding.UTF8.GetBytes(value, span);
|
||||
span[31] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct TextMetricA
|
||||
{
|
||||
public long Height;
|
||||
public long Ascent;
|
||||
public long Descent;
|
||||
public long InternalLeading;
|
||||
public long ExternalLeading;
|
||||
public long AveCharWidth;
|
||||
public long MaxCharWidth;
|
||||
public long Weight;
|
||||
public long Overhang;
|
||||
public long DigitizedAspectX;
|
||||
public long DigitizedAspectY;
|
||||
public byte FirstChar;
|
||||
public byte LastChar;
|
||||
public byte DefaultChar;
|
||||
public byte BreakChar;
|
||||
public byte Italic;
|
||||
public byte Underlined;
|
||||
public byte StruckOut;
|
||||
public byte PitchAndFamily;
|
||||
public byte CharSet;
|
||||
}
|
||||
|
||||
private enum FontWeight : long
|
||||
{
|
||||
DontCare = 0,
|
||||
Thin = 100,
|
||||
ExtraLight = 200,
|
||||
UltraLight = 200,
|
||||
Light = 300,
|
||||
Normal = 400,
|
||||
Regular = 400,
|
||||
Medium = 500,
|
||||
Semibold = 600,
|
||||
Demibold = 600,
|
||||
Bold = 700,
|
||||
Extrabold = 800,
|
||||
Ultrabold = 800,
|
||||
Heavy = 900,
|
||||
Black = 900
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
#endif
|
||||
77
Dashboard.OpenGL/ContextCollector.cs
Normal file
77
Dashboard.OpenGL/ContextCollector.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using System.Collections.Concurrent;
|
||||
using OpenTK.Graphics.OpenGL;
|
||||
|
||||
namespace Dashboard.OpenGL
|
||||
{
|
||||
public class ContextCollector : IDisposable
|
||||
{
|
||||
private readonly ConcurrentQueue<GLObject> _disposedObjects = new ConcurrentQueue<GLObject>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
22
Dashboard.OpenGL/Dashboard.OpenGL.csproj
Normal file
22
Dashboard.OpenGL/Dashboard.OpenGL.csproj
Normal file
@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OpenTK.Graphics" Version="[5.0.0-pre.*,5.1)" />
|
||||
<ProjectReference Include="..\Dashboard.Common\Dashboard.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Drawing\immediate.frag" />
|
||||
<EmbeddedResource Include="Drawing\immediate.frag" />
|
||||
<None Remove="Drawing\immediate.vert" />
|
||||
<EmbeddedResource Include="Drawing\immediate.vert" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
97
Dashboard.OpenGL/Drawing/DeviceContextBase.cs
Normal file
97
Dashboard.OpenGL/Drawing/DeviceContextBase.cs
Normal file
@ -0,0 +1,97 @@
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
using Dashboard.Drawing;
|
||||
using Dashboard.Pal;
|
||||
using OpenTK.Graphics.OpenGL;
|
||||
using OpenTK.Graphics.Wgl;
|
||||
using OpenTK.Mathematics;
|
||||
|
||||
namespace Dashboard.OpenGL.Drawing
|
||||
{
|
||||
public class DeviceContextBase : IDeviceContextBase
|
||||
{
|
||||
private readonly Stack<Matrix4x4> _transforms = new Stack<Matrix4x4>();
|
||||
private readonly Stack<RectangleF> _clipRegions = new Stack<RectangleF>();
|
||||
|
||||
public DeviceContext Context { get; private set; } = null!;
|
||||
IContextBase IContextExtensionBase.Context => Context;
|
||||
public string DriverName => "Dashboard OpenGL Device Context";
|
||||
public string DriverVendor => "Dashboard";
|
||||
public Version DriverVersion => new Version(0, 1);
|
||||
public RectangleF ClipRegion => _clipRegions.Peek();
|
||||
public Matrix4x4 Transforms => _transforms.Peek();
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public void Require(DeviceContext context)
|
||||
{
|
||||
Context = context;
|
||||
|
||||
ResetClip();
|
||||
ResetTransforms();
|
||||
}
|
||||
|
||||
|
||||
void IContextExtensionBase.Require(IContextBase context) => Require((DeviceContext)context);
|
||||
|
||||
public void ResetClip()
|
||||
{
|
||||
_clipRegions.Clear();
|
||||
|
||||
SizeF size = ((GLDeviceContext)Context).GLContext.FramebufferSize;
|
||||
_clipRegions.Push(new RectangleF(0,0, size.Width, size.Height));
|
||||
|
||||
SetClip(ClipRegion);
|
||||
}
|
||||
|
||||
public void PushClip(RectangleF clipRegion)
|
||||
{
|
||||
clipRegion = new RectangleF(ClipRegion.X + clipRegion.X, ClipRegion.Y + clipRegion.Y,
|
||||
Math.Min(ClipRegion.Right - clipRegion.X, clipRegion.Width),
|
||||
Math.Min(ClipRegion.Bottom - clipRegion.Y, clipRegion.Height));
|
||||
_clipRegions.Push(clipRegion);
|
||||
|
||||
SetClip(clipRegion);
|
||||
}
|
||||
|
||||
public void PopClip()
|
||||
{
|
||||
_clipRegions.Pop();
|
||||
SetClip(ClipRegion);
|
||||
}
|
||||
|
||||
private void SetClip(RectangleF rect)
|
||||
{
|
||||
SizeF size = ((GLDeviceContext)Context).GLContext.FramebufferSize;
|
||||
GL.Viewport(
|
||||
(int)Math.Round(rect.X),
|
||||
(int)Math.Round(size.Height - rect.Y - rect.Height),
|
||||
(int)Math.Round(rect.Width),
|
||||
(int)Math.Round(rect.Height));
|
||||
}
|
||||
|
||||
public void ResetTransforms()
|
||||
{
|
||||
SizeF size = ((GLDeviceContext)Context).GLContext.FramebufferSize;
|
||||
Matrix4x4 m = Matrix4x4.CreateOrthographicOffCenterLeftHanded(0, size.Width, size.Height, 0, 1, -1);
|
||||
|
||||
_transforms.Clear();
|
||||
_transforms.Push(m);
|
||||
}
|
||||
|
||||
public void PushTransforms(in Matrix4x4 matrix)
|
||||
{
|
||||
Matrix4x4 result = matrix * Transforms;
|
||||
_transforms.Push(result);
|
||||
}
|
||||
|
||||
public void PopTransforms()
|
||||
{
|
||||
_transforms.Pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
228
Dashboard.OpenGL/Drawing/ImmediateMode.cs
Normal file
228
Dashboard.OpenGL/Drawing/ImmediateMode.cs
Normal file
@ -0,0 +1,228 @@
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Dashboard.Drawing;
|
||||
using Dashboard.Pal;
|
||||
using OpenTK.Graphics.OpenGL;
|
||||
|
||||
namespace Dashboard.OpenGL.Drawing
|
||||
{
|
||||
public class ImmediateMode : IImmediateMode
|
||||
{
|
||||
public string DriverName => "Dashboard OpenGL Immediate Mode";
|
||||
public string DriverVendor => "Dashboard";
|
||||
public Version DriverVersion { get; } = new Version(1, 0);
|
||||
|
||||
public DeviceContext Context { get; private set; } = null!;
|
||||
|
||||
private int _program;
|
||||
private uint _program_apos;
|
||||
private uint _program_atexcoord;
|
||||
private uint _program_acolor;
|
||||
private int _program_transforms;
|
||||
private int _program_image;
|
||||
private int _vao;
|
||||
private int _white;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public void Require(DeviceContext context)
|
||||
{
|
||||
Context = context;
|
||||
|
||||
_program = GL.CreateProgram();
|
||||
|
||||
int vs = GL.CreateShader(ShaderType.VertexShader);
|
||||
|
||||
using (StreamReader reader = new StreamReader(GetType().Assembly
|
||||
.GetManifestResourceStream("Dashboard.OpenGL.Drawing.immediate.vert")!))
|
||||
{
|
||||
GL.ShaderSource(vs, reader.ReadToEnd());
|
||||
}
|
||||
GL.CompileShader(vs);
|
||||
GL.AttachShader(_program, vs);
|
||||
|
||||
int fs = GL.CreateShader(ShaderType.FragmentShader);
|
||||
|
||||
using (StreamReader reader = new StreamReader(GetType().Assembly
|
||||
.GetManifestResourceStream("Dashboard.OpenGL.Drawing.immediate.frag")!))
|
||||
{
|
||||
GL.ShaderSource(fs, reader.ReadToEnd());
|
||||
}
|
||||
|
||||
GL.CompileShader(fs);
|
||||
GL.AttachShader(_program, fs);
|
||||
|
||||
GL.LinkProgram(_program);
|
||||
GL.DeleteShader(vs); GL.DeleteShader(fs);
|
||||
|
||||
_program_apos = (uint)GL.GetAttribLocation(_program, "aPos");
|
||||
_program_atexcoord = (uint)GL.GetAttribLocation(_program, "aTexCoords");
|
||||
_program_acolor = (uint)GL.GetAttribLocation(_program, "aColor");
|
||||
|
||||
_program_transforms = GL.GetUniformLocation(_program, "transforms");
|
||||
_program_image = GL.GetUniformLocation(_program, "image");
|
||||
|
||||
GL.GenTexture(out _white);
|
||||
GL.BindTexture(TextureTarget.Texture2d, _white);
|
||||
GL.TexImage2D(TextureTarget.Texture2d, 0, InternalFormat.Rgb, 1, 1, 0, OpenTK.Graphics.OpenGL.PixelFormat.Rgb, PixelType.Byte, IntPtr.Zero);
|
||||
GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureSwizzleA, (int)All.One);
|
||||
GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureSwizzleR, (int)All.One);
|
||||
GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureSwizzleG, (int)All.One);
|
||||
GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureSwizzleB, (int)All.One);
|
||||
GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
|
||||
GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest);
|
||||
|
||||
GL.GenVertexArray(out _vao);
|
||||
}
|
||||
|
||||
public void ClearColor(Color color)
|
||||
{
|
||||
GL.ClearColor(color.R / 255f, color.G / 255f, color.B / 255f, color.A / 255f);
|
||||
GL.Clear(ClearBufferMask.ColorBufferBit);
|
||||
}
|
||||
|
||||
public void Line(Vector2 a, Vector2 b, float width, float depth, Vector4 color)
|
||||
{
|
||||
Vector2 normal = Vector2.Normalize(b - a);
|
||||
Vector2 tangent = new Vector2(-normal.Y, normal.X) * width;
|
||||
Span<ImmediateVertex> vertices =
|
||||
[
|
||||
new ImmediateVertex(new Vector3(a-tangent, depth), Vector2.Zero, color),
|
||||
new ImmediateVertex(new Vector3(b-tangent, depth), Vector2.Zero, color),
|
||||
new ImmediateVertex(new Vector3(b+tangent, depth), Vector2.Zero, color),
|
||||
new ImmediateVertex(new Vector3(a-tangent, depth), Vector2.Zero, color),
|
||||
new ImmediateVertex(new Vector3(b+tangent, depth), Vector2.Zero, color),
|
||||
new ImmediateVertex(new Vector3(a+tangent, depth), Vector2.Zero, color),
|
||||
];
|
||||
|
||||
int buffer = GL.GenBuffer();
|
||||
GL.BindBuffer(BufferTarget.ArrayBuffer, buffer);
|
||||
GL.BufferData(BufferTarget.ArrayBuffer, vertices.Length * ImmediateVertex.Size, ref vertices[0], BufferUsage.StreamDraw);
|
||||
|
||||
GL.BindVertexArray(_vao);
|
||||
GL.VertexAttribPointer(_program_apos, 3, VertexAttribPointerType.Float, false, ImmediateVertex.Size, ImmediateVertex.PosOffset);
|
||||
GL.EnableVertexAttribArray(_program_apos);
|
||||
GL.VertexAttribPointer(_program_atexcoord, 2, VertexAttribPointerType.Float, false, ImmediateVertex.Size, ImmediateVertex.TexCoordsOffset);
|
||||
GL.EnableVertexAttribArray(_program_atexcoord);
|
||||
GL.VertexAttribPointer(_program_acolor, 4, VertexAttribPointerType.Float, false, ImmediateVertex.Size, ImmediateVertex.ColorOffset);
|
||||
GL.EnableVertexAttribArray(_program_acolor);
|
||||
|
||||
Size size = ((GLDeviceContext)Context).GLContext.FramebufferSize;
|
||||
Matrix4x4 view = Context.ExtensionRequire<IDeviceContextBase>().Transforms;
|
||||
|
||||
GL.UseProgram(_program);
|
||||
|
||||
GL.ActiveTexture(TextureUnit.Texture0);
|
||||
GL.BindTexture(TextureTarget.Texture2d, _white);
|
||||
|
||||
GL.UniformMatrix4f(_program_transforms, 1, true, ref view);
|
||||
GL.Uniform1i(_program_image, 0);
|
||||
|
||||
GL.DrawArrays(PrimitiveType.Triangles, 0, 6);
|
||||
|
||||
GL.DeleteBuffer(buffer);
|
||||
}
|
||||
|
||||
public void Rectangle(Box2d rectangle, float depth, Vector4 color)
|
||||
{
|
||||
Span<ImmediateVertex> vertices =
|
||||
[
|
||||
new ImmediateVertex(new Vector3(rectangle.Min.X, rectangle.Min.Y, depth), Vector2.Zero, color),
|
||||
new ImmediateVertex(new Vector3(rectangle.Max.X, rectangle.Min.Y, depth), Vector2.Zero, color),
|
||||
new ImmediateVertex(new Vector3(rectangle.Max.X, rectangle.Max.Y, depth), Vector2.Zero, color),
|
||||
new ImmediateVertex(new Vector3(rectangle.Min.X, rectangle.Min.Y, depth), Vector2.Zero, color),
|
||||
new ImmediateVertex(new Vector3(rectangle.Max.X, rectangle.Max.Y, depth), Vector2.Zero, color),
|
||||
new ImmediateVertex(new Vector3(rectangle.Min.X, rectangle.Max.Y, depth), Vector2.Zero, color),
|
||||
];
|
||||
|
||||
int buffer = GL.GenBuffer();
|
||||
GL.BindBuffer(BufferTarget.ArrayBuffer, buffer);
|
||||
GL.BufferData(BufferTarget.ArrayBuffer, vertices.Length * ImmediateVertex.Size, ref vertices[0], BufferUsage.StreamDraw);
|
||||
|
||||
GL.BindVertexArray(_vao);
|
||||
GL.VertexAttribPointer(_program_apos, 3, VertexAttribPointerType.Float, false, ImmediateVertex.Size, ImmediateVertex.PosOffset);
|
||||
GL.EnableVertexAttribArray(_program_apos);
|
||||
GL.VertexAttribPointer(_program_atexcoord, 2, VertexAttribPointerType.Float, false, ImmediateVertex.Size, ImmediateVertex.TexCoordsOffset);
|
||||
GL.EnableVertexAttribArray(_program_atexcoord);
|
||||
GL.VertexAttribPointer(_program_acolor, 4, VertexAttribPointerType.Float, false, ImmediateVertex.Size, ImmediateVertex.ColorOffset);
|
||||
GL.EnableVertexAttribArray(_program_acolor);
|
||||
|
||||
Size size = ((GLDeviceContext)Context).GLContext.FramebufferSize;
|
||||
Matrix4x4 view = Context.ExtensionRequire<IDeviceContextBase>().Transforms;
|
||||
|
||||
GL.UseProgram(_program);
|
||||
|
||||
GL.ActiveTexture(TextureUnit.Texture0);
|
||||
GL.BindTexture(TextureTarget.Texture2d, _white);
|
||||
|
||||
GL.UniformMatrix4f(_program_transforms, 1, true, ref view);
|
||||
GL.Uniform1i(_program_image, 0);
|
||||
|
||||
GL.DrawArrays(PrimitiveType.Triangles, 0, 6);
|
||||
|
||||
GL.DeleteBuffer(buffer);
|
||||
}
|
||||
|
||||
public void Image(Box2d rectangle, Box2d uv, float depth, ITexture texture)
|
||||
{
|
||||
Span<ImmediateVertex> vertices =
|
||||
[
|
||||
new ImmediateVertex(new Vector3(rectangle.Min.X, rectangle.Min.Y, depth), new Vector2(uv.Min.X, uv.Min.Y), Vector4.One),
|
||||
new ImmediateVertex(new Vector3(rectangle.Max.X, rectangle.Min.Y, depth), new Vector2(uv.Max.X, uv.Min.Y), Vector4.One),
|
||||
new ImmediateVertex(new Vector3(rectangle.Max.X, rectangle.Max.Y, depth), new Vector2(uv.Max.X, uv.Max.Y), Vector4.One),
|
||||
new ImmediateVertex(new Vector3(rectangle.Min.X, rectangle.Min.Y, depth), new Vector2(uv.Min.X, uv.Min.Y), Vector4.One),
|
||||
new ImmediateVertex(new Vector3(rectangle.Max.X, rectangle.Max.Y, depth), new Vector2(uv.Max.X, uv.Max.Y), Vector4.One),
|
||||
new ImmediateVertex(new Vector3(rectangle.Min.X, rectangle.Max.Y, depth), new Vector2(uv.Min.X, uv.Max.Y), Vector4.One),
|
||||
];
|
||||
|
||||
int buffer = GL.GenBuffer();
|
||||
GL.BindBuffer(BufferTarget.ArrayBuffer, buffer);
|
||||
GL.BufferData(BufferTarget.ArrayBuffer, vertices.Length * ImmediateVertex.Size, ref vertices[0], BufferUsage.StreamDraw);
|
||||
|
||||
GL.BindVertexArray(_vao);
|
||||
GL.VertexAttribPointer(_program_apos, 3, VertexAttribPointerType.Float, false, ImmediateVertex.Size, ImmediateVertex.PosOffset);
|
||||
GL.EnableVertexAttribArray(_program_apos);
|
||||
GL.VertexAttribPointer(_program_atexcoord, 2, VertexAttribPointerType.Float, false, ImmediateVertex.Size, ImmediateVertex.TexCoordsOffset);
|
||||
GL.EnableVertexAttribArray(_program_atexcoord);
|
||||
GL.VertexAttribPointer(_program_acolor, 4, VertexAttribPointerType.Float, false, ImmediateVertex.Size, ImmediateVertex.ColorOffset);
|
||||
GL.EnableVertexAttribArray(_program_acolor);
|
||||
Size size = ((GLDeviceContext)Context).GLContext.FramebufferSize;
|
||||
Matrix4x4 view = Context.ExtensionRequire<IDeviceContextBase>().Transforms;
|
||||
|
||||
GL.UseProgram(_program);
|
||||
|
||||
GL.ActiveTexture(TextureUnit.Texture0);
|
||||
GL.BindTexture(TextureTarget.Texture2d, ((GLTexture)texture).Handle);
|
||||
|
||||
GL.UniformMatrix4f(_program_transforms, 1, true, ref view);
|
||||
GL.Uniform1i(_program_image, 0);
|
||||
|
||||
GL.DrawArrays(PrimitiveType.Triangles, 0, 6);
|
||||
|
||||
GL.DeleteBuffer(buffer);
|
||||
}
|
||||
|
||||
IContextBase IContextExtensionBase.Context => Context;
|
||||
|
||||
void IContextExtensionBase.Require(IContextBase context)
|
||||
{
|
||||
Require((DeviceContext)context);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = sizeof(float) * 4, Size = Size)]
|
||||
private struct ImmediateVertex(Vector3 position, Vector2 texCoords, Vector4 color)
|
||||
{
|
||||
[FieldOffset(PosOffset)] public Vector3 Position = position;
|
||||
[FieldOffset(TexCoordsOffset)] public Vector2 TexCoords = texCoords;
|
||||
[FieldOffset(ColorOffset)] public Vector4 Color = color;
|
||||
|
||||
public const int Size = 16 * sizeof(float);
|
||||
public const int PosOffset = 0 * sizeof(float);
|
||||
public const int TexCoordsOffset = 4 * sizeof(float);
|
||||
public const int ColorOffset = 8 * sizeof(float);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Dashboard.OpenGL/Drawing/immediate.frag
Normal file
12
Dashboard.OpenGL/Drawing/immediate.frag
Normal file
@ -0,0 +1,12 @@
|
||||
#version 130
|
||||
|
||||
uniform sampler2D image;
|
||||
|
||||
in vec2 vTexCoords;
|
||||
in vec4 vColor;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
void main() {
|
||||
fragColor = vColor * texture(image, vTexCoords);
|
||||
}
|
||||
18
Dashboard.OpenGL/Drawing/immediate.vert
Normal file
18
Dashboard.OpenGL/Drawing/immediate.vert
Normal file
@ -0,0 +1,18 @@
|
||||
#version 130
|
||||
|
||||
uniform mat4 transforms;
|
||||
|
||||
in vec3 aPos;
|
||||
in vec2 aTexCoords;
|
||||
in vec4 aColor;
|
||||
|
||||
out vec2 vTexCoords;
|
||||
out vec4 vColor;
|
||||
|
||||
void main() {
|
||||
vec4 position = vec4(aPos, 1.0) * transforms;
|
||||
gl_Position = position;
|
||||
|
||||
vTexCoords = aTexCoords;
|
||||
vColor = aColor;
|
||||
}
|
||||
169
Dashboard.OpenGL/GLDeviceContext.cs
Normal file
169
Dashboard.OpenGL/GLDeviceContext.cs
Normal file
@ -0,0 +1,169 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using Dashboard.Drawing;
|
||||
using Dashboard.OpenGL.Drawing;
|
||||
using Dashboard.Pal;
|
||||
using Dashboard.Windowing;
|
||||
using OpenTK;
|
||||
using OpenTK.Graphics;
|
||||
using OpenTK.Graphics.OpenGL;
|
||||
|
||||
namespace Dashboard.OpenGL
|
||||
{
|
||||
internal class GLContextBindingsContext(IGLContext context) : IBindingsContext
|
||||
{
|
||||
public IntPtr GetProcAddress(string procName)
|
||||
{
|
||||
return context.GetProcAddress(procName);
|
||||
}
|
||||
}
|
||||
|
||||
public class GLDeviceContext : DeviceContext
|
||||
{
|
||||
public IGLContext GLContext { get; }
|
||||
|
||||
public ContextCollector Collector { get; } = new ContextCollector();
|
||||
|
||||
public override string DriverName => "Dashboard OpenGL Device Context";
|
||||
public override string DriverVendor => "Dashboard";
|
||||
public override Version DriverVersion => new Version(0, 1, 0);
|
||||
|
||||
public Version GLVersion { get; }
|
||||
public string GLRenderer { get; }
|
||||
public string GLVendor { get; }
|
||||
public ImmutableHashSet<string> Extensions { get; }
|
||||
|
||||
public Thread RendererThread { get; } = Thread.CurrentThread;
|
||||
public bool IsRenderThread => RendererThread == Thread.CurrentThread;
|
||||
|
||||
private readonly ConcurrentQueue<Task> _beforeDrawActions = new ConcurrentQueue<Task>();
|
||||
private readonly ConcurrentQueue<Task> _afterDrawActions = new ConcurrentQueue<Task>();
|
||||
|
||||
public GLDeviceContext(Application app, IWindow? window, IGLContext context) : base(app, window)
|
||||
{
|
||||
GLContext = context;
|
||||
context.MakeCurrent();
|
||||
GLLoader.LoadBindings(new GLContextBindingsContext(context));
|
||||
|
||||
context.Disposed += Dispose;
|
||||
|
||||
GL.GetInteger(GetPName.MajorVersion, out int major);
|
||||
GL.GetInteger(GetPName.MinorVersion, out int minor);
|
||||
GLVersion = new Version(major, minor);
|
||||
|
||||
GLRenderer = GL.GetString(StringName.Renderer) ?? string.Empty;
|
||||
GLVendor = GL.GetString(StringName.Vendor) ?? string.Empty;
|
||||
|
||||
HashSet<string> extensions = new HashSet<string>();
|
||||
GL.GetInteger(GetPName.NumExtensions, out int extensionCount);
|
||||
for (uint i = 0; i < extensionCount; i++)
|
||||
{
|
||||
string? ext = GL.GetStringi(StringName.Extensions, i);
|
||||
if (ext != null)
|
||||
extensions.Add(ext);
|
||||
}
|
||||
|
||||
Extensions = extensions.ToImmutableHashSet();
|
||||
|
||||
ExtensionPreload<DeviceContextBase>();
|
||||
ExtensionPreload<GLTextureExtension>();
|
||||
ExtensionPreload<ImmediateMode>();
|
||||
}
|
||||
|
||||
public bool IsGLExtensionAvailable(string name)
|
||||
{
|
||||
return Extensions.Contains(name);
|
||||
}
|
||||
|
||||
public void AssertGLExtension(string name)
|
||||
{
|
||||
if (IsGLExtensionAvailable(name))
|
||||
return;
|
||||
|
||||
throw new NotSupportedException($"The OpenGL extension \"{name}\" is not supported by this context.");
|
||||
}
|
||||
|
||||
public void InvokeBeforeDraw(Task task) => _beforeDrawActions.Enqueue(task);
|
||||
|
||||
public void InvokeAfterDraw(Task task) => _afterDrawActions.Enqueue(task);
|
||||
|
||||
public Task InvokeBeforeDraw(Action action)
|
||||
{
|
||||
Task task = new Task(action);
|
||||
_beforeDrawActions.Enqueue(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
public Task InvokeAfterDraw(Action action)
|
||||
{
|
||||
Task task = new Task(action);
|
||||
_afterDrawActions.Enqueue(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
public Task<T> InvokeBeforeDraw<T>(Func<T> function)
|
||||
{
|
||||
Task<T> task = new Task<T>(function);
|
||||
_beforeDrawActions.Enqueue(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
public Task<T> InvokeAfterDraw<T>(Func<T> function)
|
||||
{
|
||||
Task<T> task = new Task<T>(function);
|
||||
_afterDrawActions.Enqueue(task);
|
||||
return task;
|
||||
}
|
||||
|
||||
public Task InvokeOnRenderThread(Action action)
|
||||
{
|
||||
if (IsRenderThread)
|
||||
{
|
||||
action();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return InvokeBeforeDraw(action);
|
||||
}
|
||||
|
||||
public Task<T> InvokeOnRenderThread<T>(Func<T> function)
|
||||
{
|
||||
return IsRenderThread ? Task.FromResult(function()) : InvokeBeforeDraw(function);
|
||||
}
|
||||
|
||||
public override void Begin()
|
||||
{
|
||||
base.Begin();
|
||||
|
||||
GLContext.MakeCurrent();
|
||||
IDeviceContextBase dc = ExtensionRequire<IDeviceContextBase>();
|
||||
dc.ResetClip();
|
||||
dc.ResetTransforms();
|
||||
|
||||
while (_beforeDrawActions.TryDequeue(out Task? action))
|
||||
{
|
||||
action.RunSynchronously();
|
||||
}
|
||||
}
|
||||
|
||||
public override void End()
|
||||
{
|
||||
base.End();
|
||||
|
||||
while (_afterDrawActions.TryDequeue(out Task? action))
|
||||
{
|
||||
action.RunSynchronously();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (isDisposing)
|
||||
{
|
||||
GLContext.Disposed -= Dispose;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
307
Dashboard.OpenGL/GLTextureExtension.cs
Normal file
307
Dashboard.OpenGL/GLTextureExtension.cs
Normal file
@ -0,0 +1,307 @@
|
||||
using System.Drawing;
|
||||
using Dashboard.Drawing;
|
||||
using Dashboard.Pal;
|
||||
using OpenTK.Graphics.OpenGL;
|
||||
using OGL = OpenTK.Graphics.OpenGL;
|
||||
|
||||
namespace Dashboard.OpenGL
|
||||
{
|
||||
public class GLTextureExtension : ITextureExtension, IContextExtensionBase<GLDeviceContext>
|
||||
{
|
||||
public string DriverName => "Dashboard OpenGL Texture Extension";
|
||||
public string DriverVendor => "Dashboard";
|
||||
public Version DriverVersion => new Version(0, 1, 0);
|
||||
public GLDeviceContext Context { get; private set; } = null!;
|
||||
public bool SupportsArbTextureStorage { get; private set; }
|
||||
public bool SupportsAnisotropy { get; private set; }
|
||||
|
||||
IContextBase IContextExtensionBase.Context => Context;
|
||||
DeviceContext IContextExtensionBase<DeviceContext>.Context => Context;
|
||||
|
||||
private List<GLTexture> _textures = new List<GLTexture>();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
|
||||
public void Require(GLDeviceContext context)
|
||||
{
|
||||
Context = context;
|
||||
|
||||
SupportsArbTextureStorage = Context.DriverVersion >= new Version(4, 2) ||
|
||||
Context.IsGLExtensionAvailable("GL_ARB_texture_storage");
|
||||
SupportsAnisotropy = Context.DriverVersion >= new Version() ||
|
||||
Context.IsGLExtensionAvailable("GL_EXT_texture_filter_anisotropic") ||
|
||||
Context.IsGLExtensionAvailable("GL_ARB_texture_filter_anisotropic");
|
||||
}
|
||||
public void Require(DeviceContext context) => Require((GLDeviceContext)context);
|
||||
public void Require(IContextBase context) => Require((GLDeviceContext)context);
|
||||
|
||||
|
||||
|
||||
public GLTexture CreateTexture(TextureType type)
|
||||
{
|
||||
GLTexture texture = new GLTexture(this, type);
|
||||
lock (_textures) _textures.Add(texture);
|
||||
return texture;
|
||||
}
|
||||
|
||||
internal void TextureDisposed(GLTexture texture)
|
||||
{
|
||||
lock (_textures) _textures.Remove(texture);
|
||||
}
|
||||
|
||||
ITexture ITextureExtension.CreateTexture(TextureType type) => CreateTexture(type);
|
||||
}
|
||||
|
||||
public class GLTexture(GLTextureExtension extension, TextureType type) : ITexture
|
||||
{
|
||||
public int Handle { get; private set; } = 0;
|
||||
public bool IsValid => Handle != 0;
|
||||
|
||||
public TextureType Type { get; } = type;
|
||||
public PixelFormat Format { get; private set; } = PixelFormat.Rgba8I;
|
||||
public ColorSwizzle Swizzle { get; set; } = ColorSwizzle.Default;
|
||||
public TextureFilter MinifyFilter { get; set; } = TextureFilter.Linear;
|
||||
public TextureFilter MagnifyFilter { get; set; } = TextureFilter.Linear;
|
||||
public Color BorderColor { get; set; } = Color.White;
|
||||
public TextureRepeat RepeatS { get; set; } = TextureRepeat.Repeat;
|
||||
public TextureRepeat RepeatT { get; set; } = TextureRepeat.Repeat;
|
||||
public TextureRepeat RepeatR { get; set; } = TextureRepeat.Repeat;
|
||||
public int Anisotropy { get; set; } = 0;
|
||||
|
||||
public int Width { get; private set; } = 0;
|
||||
public int Height { get; private set; } = 0;
|
||||
public int Depth { get; private set; } = 0;
|
||||
public int Levels { get; private set; } = 0;
|
||||
public bool Premultiplied { get; set; } = false;
|
||||
|
||||
private TextureTarget Target { get; } = type switch
|
||||
{
|
||||
TextureType.Texture1D => TextureTarget.Texture1d,
|
||||
TextureType.Texture2D => TextureTarget.Texture2d,
|
||||
TextureType.Texture3D => TextureTarget.Texture3d,
|
||||
TextureType.Texture2DArray => TextureTarget.Texture2dArray,
|
||||
TextureType.Texture2DCube => TextureTarget.TextureCubeMap,
|
||||
_ => throw new NotSupportedException()
|
||||
};
|
||||
|
||||
private GLTextureExtension Extension { get; } = extension;
|
||||
private GLDeviceContext Context => Extension.Context;
|
||||
|
||||
~GLTexture()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public void SetStorage(PixelFormat format, int width, int height, int depth, int levels)
|
||||
{
|
||||
if (!Context.IsRenderThread)
|
||||
{
|
||||
Context.InvokeBeforeDraw(() => SetStorage(format, width, height, depth, levels)).Wait();
|
||||
return;
|
||||
}
|
||||
|
||||
if (levels == 0)
|
||||
{
|
||||
levels = Math.Max(Math.ILogB(width), Math.ILogB(height));
|
||||
}
|
||||
|
||||
Bind();
|
||||
SizedInternalFormat glFormat = GetFormat(format);
|
||||
if (Extension.SupportsArbTextureStorage)
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case TextureType.Texture1D:
|
||||
GL.TexStorage1D(Target, levels, glFormat, width);
|
||||
break;
|
||||
case TextureType.Texture2D:
|
||||
GL.TexStorage2D(Target, levels, glFormat, width, height);
|
||||
break;
|
||||
case TextureType.Texture3D:
|
||||
case TextureType.Texture2DArray:
|
||||
case TextureType.Texture2DCube:
|
||||
GL.TexStorage3D(Target, levels, glFormat, width, height, depth);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case TextureType.Texture1D:
|
||||
GL.TexImage1D(Target, 0, (InternalFormat)glFormat, width, 0, (OGL.PixelFormat)glFormat, PixelType.UnsignedByte, IntPtr.Zero);
|
||||
break;
|
||||
case TextureType.Texture2D:
|
||||
GL.TexImage2D(Target, 0, (InternalFormat)glFormat, width, height, 0, (OGL.PixelFormat)glFormat, PixelType.UnsignedByte, IntPtr.Zero);
|
||||
break;
|
||||
case TextureType.Texture3D:
|
||||
case TextureType.Texture2DArray:
|
||||
case TextureType.Texture2DCube:
|
||||
GL.TexImage3D(Target, 0, (InternalFormat)glFormat, width, height, depth, 0, (OGL.PixelFormat)glFormat, PixelType.UnsignedByte, IntPtr.Zero);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Width = width;
|
||||
Height = height;
|
||||
Depth = depth;
|
||||
Levels = levels;
|
||||
}
|
||||
|
||||
public void Read<T>(Span<T> buffer, int level = 0, int align = 0) where T : unmanaged
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
void ITexture.Write<T>(PixelFormat format, ReadOnlySpan<T> buffer, int level, int align) => Write(format, buffer, level, align, null);
|
||||
|
||||
public unsafe void Write<T>(PixelFormat format, ReadOnlySpan<T> buffer, int level = 0, int align = 4, TimeSpan? timeout = null) where T : unmanaged
|
||||
{
|
||||
if (!Context.IsRenderThread)
|
||||
{
|
||||
T[] bufferArray = buffer.ToArray();
|
||||
Task task = Context.InvokeBeforeDraw(() => Write<T>(format, bufferArray, level, align));
|
||||
|
||||
if (timeout.HasValue)
|
||||
task.Wait(timeout.Value);
|
||||
else
|
||||
task.Wait();
|
||||
}
|
||||
|
||||
Bind();
|
||||
OGL::PixelFormat glFormat = format switch
|
||||
{
|
||||
PixelFormat.R8I or PixelFormat.R16F => OGL.PixelFormat.Red,
|
||||
PixelFormat.Rg8I or PixelFormat.Rg16F => OGL.PixelFormat.Rg,
|
||||
PixelFormat.Rgb8I or PixelFormat.Rgb16F => OGL.PixelFormat.Rgb,
|
||||
PixelFormat.Rgba8I or PixelFormat.Rgba16F => OGL.PixelFormat.Rgba,
|
||||
_ => throw new NotSupportedException()
|
||||
};
|
||||
|
||||
PixelType glType = format switch
|
||||
{
|
||||
PixelFormat.R8I or PixelFormat.Rg8I or PixelFormat.Rgb8I or PixelFormat.Rgba8I => PixelType.UnsignedByte,
|
||||
PixelFormat.R16F or PixelFormat.Rg16F or PixelFormat.Rgb16F or PixelFormat.Rgba16F => PixelType.HalfFloat,
|
||||
_ => throw new NotSupportedException()
|
||||
};
|
||||
|
||||
GL.PixelStorei(PixelStoreParameter.UnpackAlignment, align);
|
||||
fixed (T* ptr = buffer)
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case TextureType.Texture1D:
|
||||
GL.TexSubImage1D(Target, level, 0, Width, glFormat, glType, ptr);
|
||||
break;
|
||||
case TextureType.Texture2D:
|
||||
GL.TexSubImage2D(Target, level, 0, 0, Width, Height, glFormat, glType, ptr);
|
||||
break;
|
||||
case TextureType.Texture2DCube:
|
||||
case TextureType.Texture3D:
|
||||
case TextureType.Texture2DArray:
|
||||
GL.TexSubImage3D(Target, level, 0, 0, 0, Width, Height, Depth, glFormat, glType, ptr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Premultiply()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Unmultiply()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void GenerateMipmaps()
|
||||
{
|
||||
if (!Context.IsRenderThread)
|
||||
{
|
||||
Context.InvokeBeforeDraw(GenerateMipmaps).Wait();
|
||||
return;
|
||||
}
|
||||
|
||||
Bind();
|
||||
GL.GenerateMipmap(Target);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return;
|
||||
IsDisposed = true;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
if (Thread.CurrentThread != Context.RendererThread)
|
||||
{
|
||||
Context.Collector.DeleteTexture(Handle);
|
||||
}
|
||||
else
|
||||
{
|
||||
GL.DeleteTexture(Handle);
|
||||
}
|
||||
|
||||
Handle = 0;
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
Context.Collector.DeleteTexture(Handle);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public void Dispose() => Dispose(false);
|
||||
|
||||
private void Bind()
|
||||
{
|
||||
if (Handle == 0)
|
||||
{
|
||||
Handle = GL.GenTexture();
|
||||
}
|
||||
|
||||
GL.BindTexture(Target, Handle);
|
||||
}
|
||||
|
||||
private static SizedInternalFormat GetFormat(PixelFormat format)
|
||||
{
|
||||
return format switch
|
||||
{
|
||||
PixelFormat.R8I => SizedInternalFormat.R8,
|
||||
PixelFormat.R16F => SizedInternalFormat.R16f,
|
||||
PixelFormat.Rg8I => SizedInternalFormat.Rg8,
|
||||
PixelFormat.Rg16F => SizedInternalFormat.Rg16f,
|
||||
PixelFormat.Rgb8I => SizedInternalFormat.Rgb8,
|
||||
PixelFormat.Rgb16F => SizedInternalFormat.Rgb16f,
|
||||
PixelFormat.Rgba8I => SizedInternalFormat.Rgba8,
|
||||
PixelFormat.Rgba16F => SizedInternalFormat.Rgba16f,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
|
||||
private static PixelFormat GetFormat(SizedInternalFormat format)
|
||||
{
|
||||
return format switch
|
||||
{
|
||||
SizedInternalFormat.R8 => PixelFormat.R8I,
|
||||
SizedInternalFormat.R16f => PixelFormat.R16F,
|
||||
SizedInternalFormat.Rg8 => PixelFormat.Rg8I,
|
||||
SizedInternalFormat.Rg16f => PixelFormat.Rg16F,
|
||||
SizedInternalFormat.Rgb8 => PixelFormat.Rgb8I,
|
||||
SizedInternalFormat.Rgb16f => PixelFormat.Rgb16F,
|
||||
SizedInternalFormat.Rgba8 => PixelFormat.Rgba8I,
|
||||
SizedInternalFormat.Rgba16f => PixelFormat.Rgba16F,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Dashboard.OpenGL/IGLContext.cs
Normal file
34
Dashboard.OpenGL/IGLContext.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System.Drawing;
|
||||
using Dashboard.Windowing;
|
||||
|
||||
namespace Dashboard.OpenGL
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for GL context operations
|
||||
/// </summary>
|
||||
public interface IGLContext : IDeviceContext
|
||||
{
|
||||
/// <summary>
|
||||
/// The associated group for context sharing.
|
||||
/// </summary>
|
||||
/// <remarks>-1 assigns no group.</remarks>
|
||||
public int ContextGroup { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The size of the framebuffer in pixels.
|
||||
/// </summary>
|
||||
public Size FramebufferSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Called when the context is disposed.
|
||||
/// </summary>
|
||||
event Action Disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Activate this OpenGL Context.
|
||||
/// </summary>
|
||||
void MakeCurrent();
|
||||
|
||||
IntPtr GetProcAddress(string procName);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user