diff --git a/Dashboard.Common/Drawing/DrawCall.cs b/Dashboard.Common/Drawing/DrawCall.cs new file mode 100644 index 0000000..33fb52c --- /dev/null +++ b/Dashboard.Common/Drawing/DrawCall.cs @@ -0,0 +1,210 @@ +using System.Diagnostics.CodeAnalysis; +using System.Drawing; + +namespace Dashboard.Drawing +{ + public enum MeshPrimitive + { + Point, + Line, + Triangle, + TriangleFan, + TriangleStrip, + } + + public enum WindingOrder + { + Clockwise, + Counterclockwise, + } + + public enum FaceCulling + { + None, + Front, + Back, + Both, + } + + public enum VertexAttributeType + { + UnsignedByte, + UnsignedShort, + UnsignedInt, + UnsignedLong, + Byte, + Short, + Int, + Long, + Half, + Float, + Double, + } + + public enum IndexType + { + UnsignedShort, + UnsignedInt, + UnsignedLong, + Short, + Int, + Long, + } + + public enum BlendFunction + { + One, + Zero, + SourceAlpha, + OneMinusSourceAlpha, + DestinationAlpha, + OneMinusDestinationAlpha, + SourceColor, + OneMinusSourceColor, + DestinationColor, + OneMinusDestinationColor, + ConstantColor, + OneMinusConstantColor, + ConstantAlpha, + OneMinusConstantAlpha, + } + + public enum BlendEquation + { + Add, + Subtract, + ReverseSubtract, + Min, + Max, + } + + public enum TestFunction + { + Never, + LessThan, + LessThanOrEqual, + Equal, + NotEqual, + GreaterThanOrEqual, + Greater, + Always, + } + + public enum StencilOperation + { + Keep, + Zero, + Replace, + Increment, + UncheckedIncrement, + Decrement, + UncheckedDecrement, + Invert, + } + + public enum UniformType + { + U1, + U2, + U3, + U4, + I1, + I2, + I3, + I4, + F1, + F2, + F3, + F4, + Mat2, + Mat3, + Mat4, + Buffer, + } + + public readonly record struct DepthMode(bool Test, bool Mask, TestFunction Function) + { + public static readonly DepthMode Off = new DepthMode(false, false, TestFunction.Always); + public static readonly DepthMode WriteOnly = new DepthMode(false, true, TestFunction.LessThan); + public static readonly DepthMode Equal = new DepthMode(true, false, TestFunction.Equal); + public static readonly DepthMode Enabled = new DepthMode(true, true, TestFunction.LessThan); + } + + public readonly record struct BlendFactor(BlendFunction Source, BlendFunction Destination) + { + public static readonly BlendFactor Default = + new BlendFactor(BlendFunction.SourceAlpha, BlendFunction.OneMinusSourceAlpha); + } + + public readonly record struct BlendMode(bool Enabled, BlendFactor Color, BlendFactor Alpha) + { + [MemberNotNullWhen(true, nameof(Unified))] + public bool IsSeparate => Color != Alpha; + public BlendFactor? Unified => IsSeparate ? null : Color; + public BlendEquation Equation { get; init; } = BlendEquation.Add; + public Color Constant { get; init; } = System.Drawing.Color.Black; + + public BlendMode(BlendFunction source, BlendFunction destination) + : this(true, new BlendFactor(source, destination), new BlendFactor(source, destination)) + { + } + + public static readonly BlendMode Off = new BlendMode(false, BlendFactor.Default, BlendFactor.Default); + + public static readonly BlendMode Normal = + new BlendMode(true, BlendFactor.Default, BlendFactor.Default); + } + + public readonly record struct StencilMode(bool Enabled, TestFunction Function) + { + public int Reference { get; init; } = 0; + public int Mask { get; init; } = ~0; + public StencilOperation Pass { get; init; } = StencilOperation.Keep; + public StencilOperation DepthFail { get; init; } = StencilOperation.Keep; + public StencilOperation Fail { get; init; } = StencilOperation.Keep; + + public static readonly StencilMode Off = new StencilMode(false, TestFunction.Always); + } + + public record struct VertexAttribute( + int Location, + int Components, + VertexAttributeType Type, + long Offset, + long Stride) + { + public int Divisor { get; init; } = 1; + } + + public record struct UniformDescriptor(UniformType Type, long Offset, long Size); + + public struct DrawCall(MeshPrimitive primitive, IBuffer vertexBuffer) + { + public IShader? ShaderPipeline { get; init; } + public Box2d ViewportRegion { get; init; } + public Box2d ScissorRegion { get; init; } + public WindingOrder FrontFace { get; init; } = WindingOrder.Counterclockwise; + public FaceCulling CullMode { get; init; } = FaceCulling.Back; + public BlendMode BlendMode { get; init; } = BlendMode.Normal; + public DepthMode DepthMode { get; init; } = DepthMode.Enabled; + public StencilMode StencilMode { get; init; } = StencilMode.Off; + public bool EnableRestart { get; init; } = false; + public long RestartIndex { get; init; } = -1; + public bool RedMask { get; init; } = true; + public bool GreenMask { get; init; } = true; + public bool BlueMask { get; init; } = true; + public bool AlphaMask { get; init; } = true; + public float PointSize { get; init; }= 1.0f; + public float LineWidth { get; init; }= 1.0f; + + public MeshPrimitive Primitive { get; init; } = primitive; + public List Textures { get; init; } = []; + public List Attributes { get; init; } = []; + public List Uniforms { get; init; } = []; + public IndexType IndexType { get; init; } = IndexType.UnsignedShort; + + public IBuffer VertexBuffer { get; init; } = vertexBuffer; + public IBuffer? ElementBuffer { get; init; } + public IBuffer? UniformBuffer { get; init; } + } +} diff --git a/Dashboard.Common/Drawing/IDirectRendering.cs b/Dashboard.Common/Drawing/IDirectRendering.cs new file mode 100644 index 0000000..e7383a1 --- /dev/null +++ b/Dashboard.Common/Drawing/IDirectRendering.cs @@ -0,0 +1,67 @@ +using Dashboard.Pal; + +namespace Dashboard.Drawing +{ + public interface IShader : IDisposable + { + } + + /// + /// Base interface for creating shader pipelines. + /// + public interface IShaderCreateInfo + { + } + + public enum BufferAccessPattern + { + /// + /// No hint given. Not recommended. + /// + DontCare, + + /// + /// For buffers which have a long lifetime, but the contents are initialized once. + /// + Static, + + /// + /// For buffers which have a long lifetime, but the contents can be updated every frame. + /// + Stream, + + /// + /// For buffers which have a short lifetime, used for streaming resources to the graphics hardware. + /// + Upload, + + /// + /// For buffers which have a short lifetime, used for streaming resources from the graphics hardware. + /// + Download, + } + + public interface IBuffer : IDisposable + { + public BufferAccessPattern AccessPattern { get; } + public long Size { get; } + + void Reallocate(long newSize); + Span Map(long offset, int count = -1) where T : unmanaged; + void Unmap(); + void Read(long offset, Span span) where T : unmanaged; + void Write(long offset, ReadOnlySpan span) where T : unmanaged; + } + + /// + /// This extension provides direct access to the backend rendering pipeline without writing with a higher level + /// abstraction than the rendering backend. + /// + public interface IDirectRendering : IDeviceContextExtension + { + public IShader CreatePipeline(T createInfo) where T : IShaderCreateInfo; + public IBuffer CreateBuffer(BufferAccessPattern pattern, long size); + + void Draw(DrawCall drawCall); + } +} diff --git a/Dashboard.Common/Drawing/KhronosShader.cs b/Dashboard.Common/Drawing/KhronosShader.cs new file mode 100644 index 0000000..c68a7d2 --- /dev/null +++ b/Dashboard.Common/Drawing/KhronosShader.cs @@ -0,0 +1,39 @@ +namespace Dashboard.Drawing +{ + public record GlslShaderCreateInfo() : IShaderCreateInfo + { + public required string VertexShader { get; init; } + public string? GeometryShader { get; init; } = null; + public string? TesselationControl { get; init; } = null; + public string? TesselationEvaluation { get; init; } = null; + public string? FragmentShader { get; init; } = null; + } + + public record GlslComputeShaderCreateInfo(string Source) : IShaderCreateInfo + { + } + + public record SpirvShaderCreateInfo(byte[] Binary) : IShaderCreateInfo + { + public int Format { get; init; } = Spirv; + + public SpirvShaderCreateInfo(Stream stream) : this(GetBinary(stream)) { } + + private static byte[] GetBinary(Stream stream) + { + //FIXME: this might actually be copying twice. + if (stream is MemoryStream ms) + { + return ms.ToArray(); + } + else + { + using MemoryStream memory = new MemoryStream(); + stream.CopyTo(memory); + return memory.ToArray(); + } + } + + private const int Spirv = 38225; + } +} diff --git a/Dashboard.Common/Drawing/ShaderBuilder.cs b/Dashboard.Common/Drawing/ShaderBuilder.cs new file mode 100644 index 0000000..f490e1e --- /dev/null +++ b/Dashboard.Common/Drawing/ShaderBuilder.cs @@ -0,0 +1,17 @@ +namespace Dashboard.Drawing +{ + public enum ShaderStage + { + Vertex, + Geometry, + TesselationEvaluation, + TesselationControl, + Fragment, + Compute, + } + + public abstract class ShaderBuilder(string language) + { + public string Language { get; } = language; + } +} diff --git a/Dashboard.OpenGL/Drawing/DirectRendering.cs b/Dashboard.OpenGL/Drawing/DirectRendering.cs new file mode 100644 index 0000000..f6c72e7 --- /dev/null +++ b/Dashboard.OpenGL/Drawing/DirectRendering.cs @@ -0,0 +1,57 @@ +using Dashboard.Drawing; +using Dashboard.Pal; +using OpenTK.Graphics.OpenGL; + +namespace Dashboard.OpenGL.Drawing +{ + public class DirectRendering : IDirectRendering + { + public DeviceContext Context { get; private set; } = null!; + public string DriverName { get; } = "Dashboard OpenGL"; + public string DriverVendor { get; } = "Dashboard"; + public Version DriverVersion { get; } = new Version(0, 1); + IContextBase IContextExtensionBase.Context => Context; + + public void Dispose() + { + GC.SuppressFinalize(this); + throw new NotImplementedException(); + } + + public void Require(DeviceContext context) => Context = context; + public void Require(IContextBase context) => Require((DeviceContext)context); + + public void Begin() + { + + } + + public void End() + { + + } + + public IShader CreatePipeline(T createInfo) where T : IShaderCreateInfo + { + int program = createInfo switch + { + GlslShaderCreateInfo glsl => glsl.CreateProgram(), + GlslComputeShaderCreateInfo compute => compute.CreateProgram(), + SpirvShaderCreateInfo spirv => spirv.CreateProgram(), + _ => throw new Exception($"Unsupported shader pipeline type {createInfo.GetType()}."), + }; + + return new GLShader((GLDeviceContext)Context, program); + } + + public IBuffer CreateBuffer(BufferAccessPattern pattern, long size) + { + return GLBuffer.Create((GLDeviceContext)Context, pattern, size); + } + + public void Draw(DrawCall drawCall) + { + throw new NotImplementedException(); + } + } +} diff --git a/Dashboard.OpenGL/Drawing/GLShader.cs b/Dashboard.OpenGL/Drawing/GLShader.cs new file mode 100644 index 0000000..2ed48f4 --- /dev/null +++ b/Dashboard.OpenGL/Drawing/GLShader.cs @@ -0,0 +1,15 @@ +using Dashboard.Drawing; + +namespace Dashboard.OpenGL.Drawing +{ + public class GLShader(GLDeviceContext context, int handle) : IShader + { + public GLDeviceContext Context { get; } = context; + public int Handle { get; } = handle; + + public void Dispose() + { + Context.Collector.DeleteProgram(Handle); + } + } +} diff --git a/Dashboard.OpenGL/Drawing/KhronosShaderHelper.cs b/Dashboard.OpenGL/Drawing/KhronosShaderHelper.cs new file mode 100644 index 0000000..e717ea3 --- /dev/null +++ b/Dashboard.OpenGL/Drawing/KhronosShaderHelper.cs @@ -0,0 +1,156 @@ +using System.Text; +using Dashboard.Drawing; +using OpenTK.Graphics.OpenGL; + +namespace Dashboard.OpenGL.Drawing +{ + public static class KhronosShaderHelper + { + public static int CompileStage(ShaderType type, string source, out string? log) + { + log = null; + int shader = GL.CreateShader(type); + + GL.ShaderSource(shader, source); + GL.CompileShader(shader); + + GL.GetShaderi(shader, ShaderParameterName.CompileStatus, out int flag); + + if (flag != 0) + return shader; + + GL.GetShaderInfoLog(shader, out log); + GL.DeleteShader(shader); + return 0; + } + + public static int LinkStages(ReadOnlySpan stages, out string? log) + { + log = null; + int program = GL.CreateProgram(); + + foreach (int stage in stages) + { + GL.AttachShader(program, stage); + } + + GL.LinkProgram(program); + GL.GetProgrami(program, ProgramProperty.LinkStatus, out int flag); + + if (flag == 0) + return program; + + GL.GetProgramInfoLog(program, out log); + GL.DeleteProgram(program); + return 0; + } + + public static int CreateProgram(this GlslShaderCreateInfo glsl) + { + Span stages = stackalloc int[5]; + int failed = 0; + StringBuilder builder = new StringBuilder(); + + int count = 0; + + Compile(ShaderType.VertexShader, glsl.VertexShader, "Vertex", stages); + Compile(ShaderType.FragmentShader, glsl.FragmentShader, "Fragment", stages); + Compile(ShaderType.GeometryShader, glsl.GeometryShader, "Geometry", stages); + Compile(ShaderType.TessControlShader, glsl.TesselationControl, "Tesselation Control", stages); + Compile(ShaderType.TessEvaluationShader, glsl.TesselationEvaluation, "Tesselation Evaluation", stages); + + if (failed > 0) + { + throw new Exception($"{failed} shader stage(s) failed to compile. See the exception data for details.") + { + Data = + { + ["Log"] = builder.ToString(), + }, + }; + } + + int program = KhronosShaderHelper.LinkStages(stages[..count], out string? log); + if (program == 0) + { + throw new Exception($"Shader program failed to link. See the exception data for details.") + { + Data = + { + ["Log"] = log, + }, + }; + } + + return program; + + void Compile(ShaderType type, string? source, string name, Span stages) + { + if (source == null) + return; + + int stage = CompileStage(type, source, out string? log); + if (stage == 0) + { + failed++; + builder.Append($"=== {name} Shader Compile Log Begin ===\n{log}\n=== {name} Shader Compile Log End ===\n\n"); + } + else + { + stages[count++] = stage; + } + } + } + + public static int CreateProgram(this GlslComputeShaderCreateInfo glsl) + { + int compute = CompileStage(ShaderType.ComputeShader, glsl.Source, out string? log); + if (compute == 0) + { + throw new Exception("Failed to compile compute shader. See the exception data for details.") + { + Data = + { + ["Log"] = log, + }, + }; + } + + int program = KhronosShaderHelper.LinkStages([compute], out log); + + if (program != 0) + return program; + + throw new Exception("Failed to link compute shader. See the exception data for details.") + { + Data = + { + ["Log"] = log, + }, + }; + } + + public static int CreateProgram(this SpirvShaderCreateInfo spirv) + { + int program = GL.CreateProgram(); + GL.ProgramBinary(program, (All)spirv.Format, spirv.Binary, spirv.Binary.Length); + + GL.GetProgrami(program, ProgramProperty.LinkStatus, out int flag); + if (flag == 0) + return program; + + GL.GetProgramInfoLog(program, out string? log); + GL.DeleteProgram(program); + + throw new Exception("Failed to load program binary. See the exception data for details.") + { + Data = + { + ["Log"] = log, + }, + }; + } + } + + +} diff --git a/Dashboard.OpenGL/GLBuffer.cs b/Dashboard.OpenGL/GLBuffer.cs new file mode 100644 index 0000000..69ce93f --- /dev/null +++ b/Dashboard.OpenGL/GLBuffer.cs @@ -0,0 +1,98 @@ +using System.Runtime.CompilerServices; +using Dashboard.Drawing; +using OpenTK.Graphics.OpenGL; + +namespace Dashboard.OpenGL +{ + public class GLBuffer(GLDeviceContext dc, BufferAccessPattern pattern, int handle, long size, long offset) : IBuffer + { + // TODO: At the moment, this class does no pooling. Convert it to a pool for better API usage. + + public BufferAccessPattern AccessPattern { get; } = pattern; + public long Size { get; private set; } = size; + public long Offset { get; private set; } = offset; + public int Handle { get; private set; } + + public void Reallocate(long newSize) + { + GL.CreateBuffer(out int handle); + GL.BindBuffer(BufferTarget.ArrayBuffer, handle); + GL.BufferData(BufferTarget.ArrayBuffer, (nint)newSize, (nint)0, AccessPattern switch + { + BufferAccessPattern.Stream => BufferUsage.StreamDraw, + BufferAccessPattern.Static => BufferUsage.StaticDraw, + BufferAccessPattern.Download => BufferUsage.DynamicCopy, + BufferAccessPattern.Upload => BufferUsage.DynamicCopy, + _ => BufferUsage.DynamicDraw, + }); + + if (Handle != -1) + GL.DeleteBuffer(Handle); + + Handle = handle; + Size = newSize; + Offset = 0; + } + + public unsafe Span Map(long offset, int count = -1) + where T : unmanaged + { + ArgumentOutOfRangeException.ThrowIfNegative(offset); + + if (count < 0) + count = (int)(Size / Unsafe.SizeOf()); + + long absOffset = Offset + offset; + long absCount = count * Unsafe.SizeOf(); + ArgumentOutOfRangeException.ThrowIfGreaterThan(offset + absCount, Size); + + GL.BindBuffer(BufferTarget.ArrayBuffer, Handle); + nint ptr = (nint)GL.MapBuffer(BufferTarget.ArrayBuffer, BufferAccess.ReadWrite); + + return new Span((T*)(ptr + absOffset), count); + } + + public void Unmap() + { + GL.BindBuffer(BufferTarget.ArrayBuffer, Handle); + GL.UnmapBuffer(BufferTarget.ArrayBuffer); + } + + public void Read(long offset, Span span) where T : unmanaged + { + long absCount = span.Length * Unsafe.SizeOf(); + ArgumentOutOfRangeException.ThrowIfGreaterThan(offset + absCount, Size); + + GL.BindBuffer(BufferTarget.ArrayBuffer, Handle); + GL.GetBufferSubData(BufferTarget.ArrayBuffer, (nint)(Offset + offset), (nint)absCount, span); + } + + public void Write(long offset, ReadOnlySpan span) where T : unmanaged + { + long absCount = span.Length * Unsafe.SizeOf(); + ArgumentOutOfRangeException.ThrowIfGreaterThan(offset + absCount, Size); + + GL.BindBuffer(BufferTarget.ArrayBuffer, Handle); + GL.BufferSubData(BufferTarget.ArrayBuffer, (nint)(Offset + offset), (nint)absCount, span); + } + + public void Dispose() + { + if (Handle == -1) + return; + + dc.Collector.DeleteBufffer(Handle); + Handle = -1; + + GC.SuppressFinalize(this); + } + + public static GLBuffer Create(GLDeviceContext dc, BufferAccessPattern pattern, long size) + { + GLBuffer buffer = new GLBuffer(dc, pattern, -1, 0, 0); + buffer.Reallocate(size); + + return buffer; + } + } +} diff --git a/Dashboard.OpenGL/GLDeviceContext.cs b/Dashboard.OpenGL/GLDeviceContext.cs index 241cf89..e3e6bfe 100644 --- a/Dashboard.OpenGL/GLDeviceContext.cs +++ b/Dashboard.OpenGL/GLDeviceContext.cs @@ -74,6 +74,7 @@ namespace Dashboard.OpenGL ExtensionPreload(); ExtensionPreload(); ExtensionPreload(); + ExtensionPreload(); } public bool IsGLExtensionAvailable(string name) @@ -154,12 +155,12 @@ namespace Dashboard.OpenGL public override void End() { - base.End(); - while (_afterDrawActions.TryDequeue(out Task? action)) { action.RunSynchronously(); } + + base.End(); } protected override void Dispose(bool isDisposing) diff --git a/tests/Dashboard.TestApplication/Program.cs b/tests/Dashboard.TestApplication/Program.cs index 4029a20..dbd44b2 100644 --- a/tests/Dashboard.TestApplication/Program.cs +++ b/tests/Dashboard.TestApplication/Program.cs @@ -2,6 +2,7 @@ using Dashboard.BlurgText.OpenGL; using Dashboard.Controls; using Dashboard.Drawing; +using Dashboard.Events; using Dashboard.OpenGL; using Dashboard.OpenTK.PAL2; using Dashboard.Pal; @@ -71,6 +72,34 @@ TK.Window.SetMode(window.WindowHandle, WindowMode.Normal); window.DeviceContext.ExtensionRequire().ScaleOverride = 1.5f; +IDirectRendering direct = window.DeviceContext.ExtensionRequire(); +IBuffer vertex = direct.CreateBuffer(BufferAccessPattern.Static, 1024); +float[] vertices = new float[] +{ // x, y, z, _, r, g, b, a + -0.5f, -0.5f, 0.0f, 0.0f, 1, 0, 0, 1, + +0.5f, -0.5f, 0.0f, 0.0f, 0, 1, 0, 1, + +0.0f, +0.5f, 0.0f, 0.0f, 0, 0, 1, 1, +}; +vertex.Write(0, vertices); + +DrawCall call = new DrawCall(MeshPrimitive.Triangle, vertex) +{ + ShaderPipeline = null, + Attributes = + [ + new VertexAttribute(0, 3, VertexAttributeType.Float, 0, 8 * sizeof(float)), + new VertexAttribute(1, 4, VertexAttributeType.Float, 4 * sizeof(float), 8 * sizeof(float)) + ], +}; + +window.EventRaised += (sender, eventArgs) => +{ + if (eventArgs is not PaintEventArgs paint) + return; + + paint.DeviceContext.ExtensionRequire().Draw(call); +}; + app.Run(true, source.Token);