diff --git a/Dashboard.Drawing.OpenGL/CommandInfo.cs b/Dashboard.Drawing.OpenGL/CommandInfo.cs new file mode 100644 index 0000000..2e14aa4 --- /dev/null +++ b/Dashboard.Drawing.OpenGL/CommandInfo.cs @@ -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, + + /// + /// Make sure your custom commands have values higher than this if you plan on using the default command + /// buffer. + /// + 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; + } +} diff --git a/Dashboard.Drawing.OpenGL/ContextExecutor.cs b/Dashboard.Drawing.OpenGL/ContextExecutor.cs index 4d59991..510a8aa 100644 --- a/Dashboard.Drawing.OpenGL/ContextExecutor.cs +++ b/Dashboard.Drawing.OpenGL/ContextExecutor.cs @@ -1,18 +1,47 @@ using System.Drawing; -using OpenTK.Graphics.OpenGL; +using Dashboard.Drawing.OpenGL.Executors; namespace Dashboard.Drawing.OpenGL { - public class ContextExecutor : IDisposable + public interface ICommandExecutor + { + IEnumerable 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 int _program = 0; + private readonly List _executorsList = new List(); + + private readonly Dictionary _executorsMap = new Dictionary(); + public ContextExecutor(GLEngine engine, IGLContext context) { Engine = engine; @@ -20,11 +49,41 @@ namespace Dashboard.Drawing.OpenGL ResourcePool = Engine.ResourcePoolManager.Get(context); ResourcePool.IncrementReference(); + + AddExecutor(new BaseCommandExecutor()); } ~ContextExecutor() { - DisposeInvoker(false); + 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() @@ -33,32 +92,56 @@ namespace Dashboard.Drawing.OpenGL return; IsInitialized = true; - LoadShaders(); + foreach (ICommandExecutor executor in _executorsList) + { + if (executor is IInitializer initializer) + initializer.Initialize(); + } } - private void LoadShaders() + public virtual void BeginFrame() { - using Stream vsource = FetchEmbeddedResource("Dashboard.Drawing.OpenGL.default.vert"); - using Stream fsource = FetchEmbeddedResource("Dashboard.Drawing.OpenGL.default.frag"); - int vs = CompileShader(ShaderType.VertexShader, vsource); - int fs = CompileShader(ShaderType.FragmentShader, fsource); - _program = LinkProgram(vs, fs); - GL.DeleteShader(vs); - GL.DeleteShader(fs); + foreach (ICommandExecutor executor in _executorsList) + executor.BeginFrame(); } - public void Draw(DrawQueue drawqueue) => Draw(drawqueue, new RectangleF(new PointF(0f,0f), Context.FramebufferSize)); - public virtual void Draw(DrawQueue drawQueue, RectangleF bounds) + 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(); } - private void DisposeInvoker(bool disposing) + public void Draw(DrawQueue drawqueue) => Draw(drawqueue, new RectangleF(new PointF(0f,0f), Context.FramebufferSize)); + + public virtual void Draw(DrawQueue drawQueue, RectangleF bounds) + { + BeginDraw(); + + 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; @@ -68,67 +151,28 @@ namespace Dashboard.Drawing.OpenGL if (disposing) GC.SuppressFinalize(this); - Dispose(disposing); + Dispose(safeExit, disposing); } - protected virtual void Dispose(bool disposing) + protected virtual void Dispose(bool safeExit, bool disposing) { - ContextCollector.Global.DeleteProgram(_program); - } - - public void Dispose() => DisposeInvoker(true); - - private static int CompileShader(ShaderType type, string source) - { - int shader = GL.CreateShader(type); - - GL.ShaderSource(shader, source); - GL.CompileShader(shader); - - int compileStatus = 0; - GL.GetShaderi(shader, ShaderParameterName.CompileStatus, out compileStatus); - - if (compileStatus == 0) + if (disposing) { - GL.GetShaderInfoLog(shader, out string log); - GL.DeleteShader(shader); - throw new Exception($"{type} Shader compilation failed: " + log); + 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(); } - - return shader; } - private static int CompileShader(ShaderType type, Stream stream) - { - using StreamReader reader = new StreamReader(stream, leaveOpen: true); + public void Dispose() => DisposeInvoker(true, true); - return CompileShader(type, reader.ReadToEnd()); - } - - private static int LinkProgram(int s1, int s2) - { - int program = GL.CreateProgram(); - - GL.AttachShader(program, s1); - GL.AttachShader(program, s2); - - GL.LinkProgram(program); - - int linkStatus = 0; - GL.GetProgrami(program, ProgramProperty.LinkStatus, out linkStatus); - if (linkStatus == 0) - { - GL.GetProgramInfoLog(program, out string log); - GL.DeleteProgram(program); - throw new Exception("Shader program linking failed: " + log); - } - - return program; - } - - private static Stream FetchEmbeddedResource(string name) - { - return typeof(ContextExecutor).Assembly.GetManifestResourceStream(name)!; - } + public void Dispose(bool safeExit) => DisposeInvoker(safeExit, true); } } diff --git a/Dashboard.Drawing.OpenGL/Dashboard.Drawing.OpenGL.csproj b/Dashboard.Drawing.OpenGL/Dashboard.Drawing.OpenGL.csproj index a094872..4c9837e 100644 --- a/Dashboard.Drawing.OpenGL/Dashboard.Drawing.OpenGL.csproj +++ b/Dashboard.Drawing.OpenGL/Dashboard.Drawing.OpenGL.csproj @@ -16,10 +16,8 @@ - - - - + + diff --git a/Dashboard.Drawing.OpenGL/DrawCallRecorder.cs b/Dashboard.Drawing.OpenGL/DrawCallRecorder.cs new file mode 100644 index 0000000..5b6a482 --- /dev/null +++ b/Dashboard.Drawing.OpenGL/DrawCallRecorder.cs @@ -0,0 +1,227 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +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 _vertices = new List(); + private readonly List _calls = new List(); + + 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 vertices = CollectionsMarshal.AsSpan(_vertices); + GL.BufferData(BufferTarget.ArrayBuffer, _vertices.Count * Unsafe.SizeOf(), 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; + } + } +} diff --git a/Dashboard.Drawing.OpenGL/Executors/BaseCommandExecutor.cs b/Dashboard.Drawing.OpenGL/Executors/BaseCommandExecutor.cs new file mode 100644 index 0000000..6ee7c1a --- /dev/null +++ b/Dashboard.Drawing.OpenGL/Executors/BaseCommandExecutor.cs @@ -0,0 +1,332 @@ +using System.Drawing; +using OpenTK.Graphics.OpenGL; +using System.Numerics; +using OTK = OpenTK.Mathematics; + +namespace Dashboard.Drawing.OpenGL.Executors +{ + public class BaseCommandExecutor : IInitializer, ICommandExecutor + { + private int _program = 0; + private readonly MappableBumpAllocator _commands = new MappableBumpAllocator(); + private readonly DrawCallRecorder _calls = new DrawCallRecorder(); + + public bool IsInitialized { get; private set; } + public IEnumerable 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(); + + 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(); + + 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(); + + 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(); + 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(); + 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)!; + } + } +} diff --git a/Dashboard.Drawing.OpenGL/Executors/simple.frag b/Dashboard.Drawing.OpenGL/Executors/simple.frag new file mode 100644 index 0000000..9271218 --- /dev/null +++ b/Dashboard.Drawing.OpenGL/Executors/simple.frag @@ -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; + } +} diff --git a/Dashboard.Drawing.OpenGL/default.vert b/Dashboard.Drawing.OpenGL/Executors/simple.vert similarity index 82% rename from Dashboard.Drawing.OpenGL/default.vert rename to Dashboard.Drawing.OpenGL/Executors/simple.vert index 019c07d..89248f0 100644 --- a/Dashboard.Drawing.OpenGL/default.vert +++ b/Dashboard.Drawing.OpenGL/Executors/simple.vert @@ -2,12 +2,10 @@ in vec3 a_v3Position; in vec2 a_v2TexCoords; -in vec3 a_v3CharCoords; in int a_iCmdIndex; out vec3 v_v3Position; out vec2 v_v2TexCoords; -out vec3 v_v3CharCoords; flat out int v_iCmdIndex; uniform mat4 m4Transforms; @@ -19,6 +17,5 @@ void main(void) v_v3Position = position.xyz/position.w; v_v2TexCoords = a_v2TexCoords; - v_v3CharCoords = a_v3CharCoords; v_iCmdIndex = a_iCmdIndex; } diff --git a/Dashboard.Drawing.OpenGL/ShaderUtil.cs b/Dashboard.Drawing.OpenGL/ShaderUtil.cs new file mode 100644 index 0000000..a835e15 --- /dev/null +++ b/Dashboard.Drawing.OpenGL/ShaderUtil.cs @@ -0,0 +1,60 @@ +using OpenTK.Graphics.OpenGL; + +namespace Dashboard.Drawing.OpenGL +{ + public static class ShaderUtil + { + public static int CompileShader(ShaderType type, string source) + { + int shader = GL.CreateShader(type); + + GL.ShaderSource(shader, source); + GL.CompileShader(shader); + + int compileStatus = 0; + GL.GetShaderi(shader, ShaderParameterName.CompileStatus, out compileStatus); + + if (compileStatus == 0) + { + GL.GetShaderInfoLog(shader, out string log); + GL.DeleteShader(shader); + throw new Exception($"{type} Shader compilation failed: " + log); + } + + return shader; + } + + public static int CompileShader(ShaderType type, Stream stream) + { + using StreamReader reader = new StreamReader(stream, leaveOpen: true); + + return CompileShader(type, reader.ReadToEnd()); + } + + public static int LinkProgram(int s1, int s2, IReadOnlyList? attribLocations = null) + { + int program = GL.CreateProgram(); + + GL.AttachShader(program, s1); + GL.AttachShader(program, s2); + + for (int i = 0; i < attribLocations?.Count; i++) + { + GL.BindAttribLocation(program, (uint)i, attribLocations[i]); + } + + GL.LinkProgram(program); + + int linkStatus = 0; + GL.GetProgrami(program, ProgramProperty.LinkStatus, out linkStatus); + if (linkStatus == 0) + { + GL.GetProgramInfoLog(program, out string log); + GL.DeleteProgram(program); + throw new Exception("Shader program linking failed: " + log); + } + + return program; + } + } +} diff --git a/Dashboard.Drawing.OpenGL/default.frag b/Dashboard.Drawing.OpenGL/default.frag deleted file mode 100644 index 2e3d45a..0000000 --- a/Dashboard.Drawing.OpenGL/default.frag +++ /dev/null @@ -1,97 +0,0 @@ -#version 140 - -#define DB_GRADIENT_MAX 16 -#define DB_COMMAND_MAX 64 - -#define CMD_POINT 1 -#define CMD_LINE 2 -#define CMD_RECT 3 -#define CMD_TEXT 4 - -#define STRIKE_INSET -1 -#define STRIKE_CENTER 0 -#define STRIKE_OUTSET 1 - -in vec3 v_v3Position; -in vec2 v_v2TexCoords; -in int v_iCmdIndex; - -out vec4 f_Color; - -uniform sampler2D txForeground; -uniform sampler2D txBackground; -uniform sampler2DArray txCharacters; - -struct Gradient_t { - float fPosition; - float pad0; - float pad1; - float pad2; - vec4 v4Color; -}; - -uniform GradientBlock -{ - Gradient_t vstGradientStops[DB_GRADIENT_MAX]; -}; - -vec4 getGradientColor(float position, int index, int count) -{ - position = clamp(position, 0, 1); - - int i0 = 0; - float p0 = vstGradientStops[index + i0].fPosition; - - int i1 = count - 1; - float p1 = vstGradientStops[index + i1].fPosition; - - for (int i = 0; i < count; i++) - { - float px = vstGradientStops[index + i].fPosition; - - if (px > p0 && px <= position) - { - p0 = px; - i0 = i; - } - - if (px < p1 && px >= position) - { - p1 = px; - i1 = i; - } - } - - vec4 c0 = vstGradientStops[index + i0].v4Color; - vec4 c1 = vstGradientStops[index + i1].v4Color; - - float l = p1 - p0; - float w = (l > 0) ? (position - p0) / (p1 - p0) : 0; - - return mix(c0, c1, w); -} - -struct CommandCommon { - int iCommand; - int iStrikeKind; - float fBorderRadius; - int _padding; - - int iFgGradientIndex; - int iFgGradientCount; - int iBgGradientIndex; - int iBgGradientCount; - - vec4 v4FillColor; - vec4 v4BorderColor; -}; - -uniform CommandBlock -{ - CommandCommon vstCommands[DB_COMMAND_MAX]; -}; - -void main(void) -{ - -} diff --git a/Dashboard.Drawing/DrawExtension.cs b/Dashboard.Drawing/DrawExtension.cs index f95fbfd..55a2d3e 100644 --- a/Dashboard.Drawing/DrawExtension.cs +++ b/Dashboard.Drawing/DrawExtension.cs @@ -102,7 +102,7 @@ namespace Dashboard.Drawing 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, strikeBrush, strikeSize, kind)); + 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, @@ -112,7 +112,7 @@ namespace Dashboard.Drawing 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, strikeBrush, strikeSize, kind)); + 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, diff --git a/tests/Dashboard.TestApplication/Program.cs b/tests/Dashboard.TestApplication/Program.cs index ab954ee..f8bbcd3 100644 --- a/tests/Dashboard.TestApplication/Program.cs +++ b/tests/Dashboard.TestApplication/Program.cs @@ -6,11 +6,12 @@ using OpenTK.Platform; using OpenTK.Graphics.OpenGL; using OpenTK.Mathematics; using TK = OpenTK.Platform.Toolkit; -using Vector3 = System.Numerics.Vector3; +using sys = System.Numerics; +using otk = OpenTK.Mathematics; TK.Init(new ToolkitOptions() { - ApplicationName = "Dashboard Test Application", + ApplicationName = "Paper Punch Out!", Windows = new ToolkitOptions.WindowsOptions() { EnableVisualStyles = true, @@ -32,13 +33,15 @@ WindowHandle wnd = TK.Window.Create(new OpenGLGraphicsApiHints() GreenColorBits = 8, BlueColorBits = 8, AlphaColorBits = 8, + Multisamples = 0, SupportTransparentFramebufferX11 = true, }); -TK.Window.SetTitle(wnd, "Dashboard Test Application"); +TK.Window.SetTitle(wnd, "Paper Punch Out!"); TK.Window.SetMinClientSize(wnd, 300, 200); TK.Window.SetClientSize(wnd, new Vector2i(320, 240)); TK.Window.SetBorderStyle(wnd, WindowBorderStyle.ResizableBorder); +TK.Window.SetTransparencyMode(wnd, WindowTransparencyMode.TransparentFramebuffer, 0.1f); OpenGLContextHandle context = TK.OpenGL.CreateFromWindow(wnd); @@ -48,8 +51,8 @@ TK.OpenGL.SetSwapInterval(1); GLLoader.LoadBindings(new Pal2BindingsContext(TK.OpenGL, context)); DrawQueue queue = new DrawQueue(); -SolidBrush fg = new SolidBrush(Color.Black); -SolidBrush bg = new SolidBrush(Color.White); +SolidBrush fg = new SolidBrush(Color.FromArgb(0, 0, 0, 0)); +SolidBrush bg = new SolidBrush(Color.Black); bool shouldExit = false; GLEngine engine = new GLEngine(); @@ -58,6 +61,8 @@ engine.Initialize(); GlContext dbGlContext = new GlContext(wnd, context); ContextExecutor executor = engine.GetExecutor(dbGlContext); +Vector2 mousePos = Vector2.Zero; +Random r = new Random(); EventQueue.EventRaised += (handle, type, eventArgs) => { if (handle != wnd) @@ -68,25 +73,49 @@ EventQueue.EventRaised += (handle, type, eventArgs) => case PlatformEventType.Close: shouldExit = true; break; + case PlatformEventType.MouseMove: + mousePos = ((MouseMoveEventArgs)eventArgs).ClientPosition; + break; + case PlatformEventType.MouseUp: + MouseButtonUpEventArgs mouseUp = (MouseButtonUpEventArgs)eventArgs; + if (mouseUp.Button == MouseButton.Button1) + { + SolidBrush brush = new SolidBrush(Color.FromArgb(r.Next(256), r.Next(256), r.Next(256), 0)); + queue.Point(new sys::Vector2(mousePos.X, mousePos.Y), 0f, 24f, brush); + } + break; + case PlatformEventType.KeyDown: + KeyDownEventArgs keyDown = (KeyDownEventArgs)eventArgs; + if (keyDown.Key == Key.Escape || keyDown.Key == Key.Delete || keyDown.Key == Key.Backspace) + queue.Clear(); + break; } }; TK.Window.SetMode(wnd, WindowMode.Normal); +List points = new List(); + +queue.Line(new sys.Vector2(64, 256), new sys.Vector2(64+256, 256), 0, 64f, fg); +queue.Rect(new sys.Vector2(16, 16), new sys.Vector2(96, 96), 0, fg, bg, 8f); + while (!shouldExit) { TK.Window.ProcessEvents(true); TK.Window.GetFramebufferSize(wnd, out Vector2i framebufferSize); + executor.BeginFrame(); - queue.Clear(); - queue.Point(Vector3.Zero, 2f, fg); - queue.Line(Vector3.Zero, Vector3.One * 2, 2f, bg); - queue.Rect(Vector3.UnitX, 2 * Vector3.UnitX + Vector3.UnitY, fg, bg, 2f); + // queue.Line(Vector3.Zero, new Vector3(System.Numerics.Vector2.One * 256f, 0), 4f, bg); + // queue.Rect(Vector3.UnitX, 2 * Vector3.UnitX + Vector3.UnitY, fg, bg, 2f); GL.Viewport(0, 0, framebufferSize.X, framebufferSize.Y); - GL.ClearColor(0.3f, 0.3f, 0.3f, 1.0f); + GL.ClearColor(0.9f, 0.9f, 0.7f, 1.0f); GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); + GL.Disable(EnableCap.DepthTest); + // GL.Enable(EnableCap.Blend); + // GL.BlendFuncSeparate(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha, BlendingFactor.One, BlendingFactor.Zero); + GL.ColorMask(true, true, true, true); executor.Draw(queue); executor.EndFrame();