From edc85c3f2438a35693e99e51324c6b4351e35784 Mon Sep 17 00:00:00 2001 From: "H. Utku Maden" Date: Tue, 5 Aug 2025 23:50:29 +0300 Subject: [PATCH] Create a new context system for dashboard. --- Dashboard.Common/ImageProperties.cs | 31 ++- Dashboard.Common/Pal/AppContext.cs | 136 +++++++++ Dashboard.Common/Pal/DeviceContext.cs | 98 +++++++ Dashboard.Common/Pal/IAppContextExtension.cs | 7 + Dashboard.Common/Pal/IContextBase.cs | 103 +++++++ Dashboard.Common/Pal/IContextDebugger.cs | 10 + .../Pal/IDeviceContextExtension.cs | 6 + Dashboard.Common/Pal/ITextureExtension.cs | 83 ++++++ Dashboard.Drawing.OpenGL/ContextExecutor.cs | 6 +- .../Pal/GLDeviceContext.cs | 160 +++++++++++ .../Pal/GLTextureExtension.cs | 257 ++++++++++++++++++ Dashboard.OpenGL/IGLContext.cs | 2 + Dashboard.OpenTK/PAL2/OpenGLDeviceContext.cs | 5 + Dashboard.OpenTK/PAL2/Pal2AppContext.cs | 56 ++++ Dashboard.OpenTK/PAL2/Pal2DashboardBackend.cs | 50 ---- Dashboard.OpenTK/PAL2/PhysicalWindow.cs | 19 +- Dashboard/Application.cs | 131 --------- Dashboard/IDashboardBackend.cs | 29 -- tests/Dashboard.TestApplication/Program.cs | 192 ++++++------- 19 files changed, 1064 insertions(+), 317 deletions(-) create mode 100644 Dashboard.Common/Pal/AppContext.cs create mode 100644 Dashboard.Common/Pal/DeviceContext.cs create mode 100644 Dashboard.Common/Pal/IAppContextExtension.cs create mode 100644 Dashboard.Common/Pal/IContextBase.cs create mode 100644 Dashboard.Common/Pal/IContextDebugger.cs create mode 100644 Dashboard.Common/Pal/IDeviceContextExtension.cs create mode 100644 Dashboard.Common/Pal/ITextureExtension.cs create mode 100644 Dashboard.Drawing.OpenGL/Pal/GLDeviceContext.cs create mode 100644 Dashboard.Drawing.OpenGL/Pal/GLTextureExtension.cs create mode 100644 Dashboard.OpenTK/PAL2/Pal2AppContext.cs delete mode 100644 Dashboard.OpenTK/PAL2/Pal2DashboardBackend.cs delete mode 100644 Dashboard/Application.cs delete mode 100644 Dashboard/IDashboardBackend.cs diff --git a/Dashboard.Common/ImageProperties.cs b/Dashboard.Common/ImageProperties.cs index a7be347..0317116 100644 --- a/Dashboard.Common/ImageProperties.cs +++ b/Dashboard.Common/ImageProperties.cs @@ -6,16 +6,31 @@ namespace Dashboard /// /// Pixel format for images. /// + [Flags] public enum PixelFormat { - R8I, - Rg8I, - Rgb8I, - Rgba8I, - R16F, - Rg816F, - Rgb16F, - Rgba16F, + 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, } /// diff --git a/Dashboard.Common/Pal/AppContext.cs b/Dashboard.Common/Pal/AppContext.cs new file mode 100644 index 0000000..fe4f1f7 --- /dev/null +++ b/Dashboard.Common/Pal/AppContext.cs @@ -0,0 +1,136 @@ +using Dashboard.Collections; +using Dashboard.Windowing; + +namespace Dashboard.Pal +{ + public abstract class AppContext : IContextBase + { + 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 _extensions = + new TypeDictionary(true); + private readonly TypeDictionary> _preloadedExtensions = + new TypeDictionary>(true); + + ~AppContext() + { + InvokeDispose(false); + } + + public void Initialize() + { + if (IsInitialized) + return; + + IsInitialized = true; + + InitializeInternal(); + } + + protected virtual void InitializeInternal() + { + } + + 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 + /// + /// Creates a window. It could be a virtual window, or a physical window. + /// + /// A window. + public abstract IWindow CreateWindow(); + + /// + /// Always creates a physical window. + /// + /// A physical window. + public abstract IPhysicalWindow CreatePhysicalWindow(); + #endregion + + public bool IsExtensionAvailable() where T : IAppContextExtension + { + return _extensions.Contains() || _preloadedExtensions.Contains(); + } + + public bool ExtensionPreload() where T : IAppContextExtension, new() + { + return _preloadedExtensions.Add(() => new T()); + } + + public T ExtensionRequire() where T : IAppContextExtension, new() + { + T? extension = default; + + if (_extensions.TryGet(out extension)) + return extension; + + lock (_extensions) + { + if (_extensions.TryGet(out extension)) + return extension; + + if (_preloadedExtensions.Remove(out Func? loader)) + { + extension = (T)loader!(); + } + else + { + extension = new T(); + } + + _extensions.Add(extension); + extension.Require(this); + } + + return extension; + } + + protected virtual void Dispose(bool isDisposing) + { + if (!isDisposing) return; + + foreach (IAppContextExtension 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); + } +} diff --git a/Dashboard.Common/Pal/DeviceContext.cs b/Dashboard.Common/Pal/DeviceContext.cs new file mode 100644 index 0000000..a15c87f --- /dev/null +++ b/Dashboard.Common/Pal/DeviceContext.cs @@ -0,0 +1,98 @@ +using Dashboard.Collections; + +namespace Dashboard.Pal +{ + public abstract class DeviceContext : IContextBase + { + private readonly TypeDictionary _extensions = + new TypeDictionary(true); + private readonly TypeDictionary> _preloadedExtensions = + new TypeDictionary>(true); + + public abstract string DriverName { get; } + public abstract string DriverVendor { get; } + public abstract Version DriverVersion { get; } + + public bool IsDisposed { get; private set; } + + /// + /// Optional debugging object for your pleasure. + /// + public IContextDebugger? Debugger { get; set; } + + ~DeviceContext() + { + Dispose(false); + } + + public virtual void Begin() { } + + // public abstract void Paint(object renderbuffer); + + public virtual void End() { } + + public bool IsExtensionAvailable() where T : IDeviceContextExtension + { + return _extensions.Contains() || _preloadedExtensions.Contains(); + } + + public bool ExtensionPreload() where T : IDeviceContextExtension, new() + { + return _preloadedExtensions.Add(() => new T()); + } + + public T ExtensionRequire() where T : IDeviceContextExtension, new() + { + T? extension = default; + + if (_extensions.TryGet(out extension)) + return extension; + + lock (_extensions) + { + if (_extensions.TryGet(out extension)) + return extension; + + if (_preloadedExtensions.Remove(out Func? loader)) + { + extension = (T)loader!(); + } + else + { + extension = new T(); + } + + _extensions.Add(extension); + extension.Require(this); + } + + return extension; + } + + /// + /// Implement your dispose in this function. + /// + /// True if disposing, false otherwise. + 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); + } +} diff --git a/Dashboard.Common/Pal/IAppContextExtension.cs b/Dashboard.Common/Pal/IAppContextExtension.cs new file mode 100644 index 0000000..da27c13 --- /dev/null +++ b/Dashboard.Common/Pal/IAppContextExtension.cs @@ -0,0 +1,7 @@ +namespace Dashboard.Pal +{ + public interface IAppContextExtension : IContextExtensionBase + { + + } +} diff --git a/Dashboard.Common/Pal/IContextBase.cs b/Dashboard.Common/Pal/IContextBase.cs new file mode 100644 index 0000000..f1a8b01 --- /dev/null +++ b/Dashboard.Common/Pal/IContextBase.cs @@ -0,0 +1,103 @@ +namespace Dashboard.Pal +{ + /// + /// Information about this context interface. + /// + public interface IContextInterfaceInfo + { + /// + /// Name of this driver. + /// + string DriverName { get; } + + /// + /// The vendor for this driver. + /// + string DriverVendor { get; } + + /// + /// The version of this driver. + /// + Version DriverVersion { get; } + } + + /// + /// The base context interface. + /// + public interface IContextBase : IContextInterfaceInfo, IDisposable + { + /// + /// The debugger for this context. + /// + IContextDebugger? Debugger { get; set; } + } + + /// + /// The base context interface. + /// + /// The context type. + /// The extension type, if used. + public interface IContextBase : IContextBase + where TContext : IContextBase + where TExtension : IContextExtensionBase + { + /// + /// Is such an extension available? + /// + /// The extension to check. + /// True if the extension is available. + bool IsExtensionAvailable() where T : TExtension; + + /// + /// Preload extensions, to be lazy loaded when required. + /// + /// The extension to preload. + /// + /// True if the extension was added to the preload set. Otherwise, already loaded or another extension + /// exists which provides this. + /// + bool ExtensionPreload() where T : TExtension, new(); + + /// + /// Require an extension. + /// + /// The extension to require. + /// The extension instance. + T ExtensionRequire() where T : TExtension, new(); + } + + /// + /// Base interface for all context extensions. + /// + public interface IContextExtensionBase : IContextInterfaceInfo, IDisposable + { + /// + /// The context that loaded this extension. + /// + IContextBase Context { get; } + + /// + /// Require this extension. + /// + /// The context that required this extension. + void Require(IContextBase context); + } + + /// + /// Base interface for all context extensions. + /// + public interface IContextExtensionBase : IContextExtensionBase + where TContext : IContextBase + { + /// + /// The context that loaded this extension. + /// + new TContext Context { get; } + + /// + /// Require this extension. + /// + /// The context that required this extension. + void Require(TContext context); + } +} diff --git a/Dashboard.Common/Pal/IContextDebugger.cs b/Dashboard.Common/Pal/IContextDebugger.cs new file mode 100644 index 0000000..6706d57 --- /dev/null +++ b/Dashboard.Common/Pal/IContextDebugger.cs @@ -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); + } +} diff --git a/Dashboard.Common/Pal/IDeviceContextExtension.cs b/Dashboard.Common/Pal/IDeviceContextExtension.cs new file mode 100644 index 0000000..87f7a8e --- /dev/null +++ b/Dashboard.Common/Pal/IDeviceContextExtension.cs @@ -0,0 +1,6 @@ +namespace Dashboard.Pal +{ + public interface IDeviceContextExtension : IContextExtensionBase + { + } +} diff --git a/Dashboard.Common/Pal/ITextureExtension.cs b/Dashboard.Common/Pal/ITextureExtension.cs new file mode 100644 index 0000000..5432375 --- /dev/null +++ b/Dashboard.Common/Pal/ITextureExtension.cs @@ -0,0 +1,83 @@ +using System.Drawing; + +namespace Dashboard.Pal +{ + 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(Span buffer, int level = 0, int align = 0) where T : unmanaged; + void Write(PixelFormat format, ReadOnlySpan buffer, int level = 0, int align = 4) where T : unmanaged; + void Premultiply(); + void Unmultiply(); + void GenerateMipmaps(); + } +} diff --git a/Dashboard.Drawing.OpenGL/ContextExecutor.cs b/Dashboard.Drawing.OpenGL/ContextExecutor.cs index 55513be..5ecb66b 100644 --- a/Dashboard.Drawing.OpenGL/ContextExecutor.cs +++ b/Dashboard.Drawing.OpenGL/ContextExecutor.cs @@ -1,6 +1,7 @@ using System.Drawing; using Dashboard.Drawing.OpenGL.Executors; using Dashboard.OpenGL; +using OpenTK.Mathematics; namespace Dashboard.Drawing.OpenGL { @@ -130,10 +131,13 @@ namespace Dashboard.Drawing.OpenGL public void Draw(DrawQueue drawqueue) => Draw(drawqueue, new RectangleF(new PointF(0f,0f), Context.FramebufferSize)); - public virtual void Draw(DrawQueue drawQueue, RectangleF bounds) + 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)) diff --git a/Dashboard.Drawing.OpenGL/Pal/GLDeviceContext.cs b/Dashboard.Drawing.OpenGL/Pal/GLDeviceContext.cs new file mode 100644 index 0000000..29c489e --- /dev/null +++ b/Dashboard.Drawing.OpenGL/Pal/GLDeviceContext.cs @@ -0,0 +1,160 @@ +using System.Collections.Concurrent; +using System.Collections.Immutable; +using Dashboard.OpenGL; +using Dashboard.Pal; +using OpenTK; +using OpenTK.Graphics; +using OpenTK.Graphics.OpenGL; + +namespace Dashboard.Drawing.OpenGL.Pal +{ + internal class GLContextBindingsContext(IGLContext context) : IBindingsContext + { + public IntPtr GetProcAddress(string procName) + { + return context.GetProcAddress(procName); + } + } + + public class GLDeviceContext : DeviceContext + { + public IGLContext GLContext { get; } + + 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 Extensions { get; } + + public Thread RendererThread { get; } = Thread.CurrentThread; + public bool IsRenderThread => RendererThread == Thread.CurrentThread; + + private readonly ConcurrentQueue _beforeDrawActions = new ConcurrentQueue(); + private readonly ConcurrentQueue _afterDrawActions = new ConcurrentQueue(); + + public GLDeviceContext(IGLContext context) + { + 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 extensions = new HashSet(); + 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(); + } + + 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 InvokeBeforeDraw(Func function) + { + Task task = new Task(function); + _beforeDrawActions.Enqueue(task); + return task; + } + + public Task InvokeAfterDraw(Func function) + { + Task task = new Task(function); + _afterDrawActions.Enqueue(task); + return task; + } + + public Task InvokeOnRenderThread(Action action) + { + if (IsRenderThread) + { + action(); + return Task.CompletedTask; + } + + return InvokeBeforeDraw(action); + } + + public Task InvokeOnRenderThread(Func function) + { + return IsRenderThread ? Task.FromResult(function()) : InvokeBeforeDraw(function); + } + + public override void Begin() + { + base.Begin(); + + GLContext.MakeCurrent(); + + 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; + } + } + } +} diff --git a/Dashboard.Drawing.OpenGL/Pal/GLTextureExtension.cs b/Dashboard.Drawing.OpenGL/Pal/GLTextureExtension.cs new file mode 100644 index 0000000..2050b60 --- /dev/null +++ b/Dashboard.Drawing.OpenGL/Pal/GLTextureExtension.cs @@ -0,0 +1,257 @@ +using System.Drawing; +using Dashboard.Pal; +using OpenTK.Graphics.OpenGL; +using OGL = OpenTK.Graphics.OpenGL; + +namespace Dashboard.Drawing.OpenGL.Pal +{ + public class GLTextureExtension : ITextureExtension, IContextExtensionBase + { + 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.Context => Context; + + private List _textures = new List(); + + 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; + + 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; + } + + 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.Byte, IntPtr.Zero); + break; + case TextureType.Texture2D: + GL.TexImage2D(Target, 0, (InternalFormat)glFormat, width, height, 0, (OGL.PixelFormat)glFormat, PixelType.Byte, 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.Byte, IntPtr.Zero); + break; + } + } + } + + public void Read(Span buffer, int level = 0, int align = 0) where T : unmanaged + { + throw new NotImplementedException(); + } + + public unsafe void Write(PixelFormat format, ReadOnlySpan buffer, int level = 0, int align = 4) where T : unmanaged + { + if (!Context.IsRenderThread) + { + throw new NotImplementedException(); + } + + 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.Byte, + 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); + } + + public void Dispose() + { + throw new NotImplementedException(); + } + + 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() + }; + } + } +} diff --git a/Dashboard.OpenGL/IGLContext.cs b/Dashboard.OpenGL/IGLContext.cs index 00f4605..9ab3b0a 100644 --- a/Dashboard.OpenGL/IGLContext.cs +++ b/Dashboard.OpenGL/IGLContext.cs @@ -28,5 +28,7 @@ namespace Dashboard.OpenGL /// Activate this OpenGL Context. /// void MakeCurrent(); + + IntPtr GetProcAddress(string procName); } } diff --git a/Dashboard.OpenTK/PAL2/OpenGLDeviceContext.cs b/Dashboard.OpenTK/PAL2/OpenGLDeviceContext.cs index 6d20797..100314d 100644 --- a/Dashboard.OpenTK/PAL2/OpenGLDeviceContext.cs +++ b/Dashboard.OpenTK/PAL2/OpenGLDeviceContext.cs @@ -40,6 +40,11 @@ namespace Dashboard.OpenTK.PAL2 TK.OpenGL.SetCurrentContext(ContextHandle); } + public IntPtr GetProcAddress(string procName) + { + return TK.OpenGL.GetProcedureAddress(ContextHandle, procName); + } + private bool _isDisposed = false; public void Dispose() => Dispose(true); diff --git a/Dashboard.OpenTK/PAL2/Pal2AppContext.cs b/Dashboard.OpenTK/PAL2/Pal2AppContext.cs new file mode 100644 index 0000000..c603c9a --- /dev/null +++ b/Dashboard.OpenTK/PAL2/Pal2AppContext.cs @@ -0,0 +1,56 @@ +using Dashboard.Windowing; +using OpenTK.Graphics; +using OpenTK.Platform; +using AppContext = Dashboard.Pal.AppContext; +using TK = OpenTK.Platform.Toolkit; + +namespace Dashboard.OpenTK.PAL2 +{ + public class Pal2AppContext : AppContext + { + public override string DriverName => "Dashboard OpenTK PAL2.0 Driver"; + public override string DriverVendor => "Dashboard"; + public override Version DriverVersion => new Version(0, 1); + public GraphicsApiHints GraphicsApiHints { get; set; } = new OpenGLGraphicsApiHints(); + public bool OpenGLBindingsInitialized { get; private set; } = false; + + private readonly List _windows = new List(); + + public override IPhysicalWindow CreatePhysicalWindow() + { + PhysicalWindow window = new PhysicalWindow(GraphicsApiHints); + _windows.Add(window); + + if (!OpenGLBindingsInitialized) + { + OpenGLBindingsInitialized = true; + GLLoader.LoadBindings( + new Pal2BindingsContext(TK.OpenGL, + ((OpenGLDeviceContext)window.DeviceContext).ContextHandle)); + } + + return window; + } + + public override IWindow CreateWindow() + { + return CreatePhysicalWindow(); + } + + public override void RunEvents(bool wait) + { + TK.Window.ProcessEvents(wait); + + for (int i = 0; i < _windows.Count; i++) + { + if (_windows[i].IsDisposed) + { + _windows.RemoveAt(i); + continue; + } + + _windows[i].Paint(); + } + } + } +} diff --git a/Dashboard.OpenTK/PAL2/Pal2DashboardBackend.cs b/Dashboard.OpenTK/PAL2/Pal2DashboardBackend.cs deleted file mode 100644 index 61b13b4..0000000 --- a/Dashboard.OpenTK/PAL2/Pal2DashboardBackend.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Dashboard.Windowing; -using OpenTK.Graphics; -using OpenTK.Platform; -using TK = OpenTK.Platform.Toolkit; - -namespace Dashboard.OpenTK.PAL2 -{ - public class Pal2DashboardBackend : IDashboardBackend - { - public GraphicsApiHints GraphicsApiHints { get; set; } = new OpenGLGraphicsApiHints(); - public bool OpenGLBindingsInitialized { get; set; } = false; - - public IPhysicalWindow CreatePhysicalWindow() - { - PhysicalWindow window = new PhysicalWindow(GraphicsApiHints); - - if (!OpenGLBindingsInitialized) - { - OpenGLBindingsInitialized = true; - GLLoader.LoadBindings( - new Pal2BindingsContext(TK.OpenGL, - ((OpenGLDeviceContext)window.DeviceContext).ContextHandle)); - } - - return window; - } - - public virtual IWindow CreateWindow() - { - return CreatePhysicalWindow(); - } - - public void Dispose() - { - } - - public void Initialize() - { - } - - public void Leave() - { - } - - public void RunEvents(bool wait) - { - TK.Window.ProcessEvents(wait); - } - } -} \ No newline at end of file diff --git a/Dashboard.OpenTK/PAL2/PhysicalWindow.cs b/Dashboard.OpenTK/PAL2/PhysicalWindow.cs index 569f63f..d467408 100644 --- a/Dashboard.OpenTK/PAL2/PhysicalWindow.cs +++ b/Dashboard.OpenTK/PAL2/PhysicalWindow.cs @@ -10,7 +10,7 @@ using TK = OpenTK.Platform.Toolkit; namespace Dashboard.OpenTK.PAL2 { - public class PhysicalWindow : IPhysicalWindow, IDrawQueuePaintable, IEventListener + public class PhysicalWindow : IPhysicalWindow, IDrawQueuePaintable, IEventListener, IDpiAwareWindow { public DrawQueue DrawQueue { get; } = new DrawQueue(); public WindowHandle WindowHandle { get; } @@ -86,11 +86,11 @@ namespace Dashboard.OpenTK.PAL2 } } - private bool _isDisposed = false; + public bool IsDisposed { get; private set; } = false; public void Dispose() { - if (_isDisposed) return; - _isDisposed = true; + if (IsDisposed) return; + IsDisposed = true; RemoveWindow(this); @@ -160,5 +160,16 @@ namespace Dashboard.OpenTK.PAL2 break; } } + + public float Dpi => Scale * 96f; + + public float Scale + { + get + { + TK.Window.GetScaleFactor(WindowHandle, out float x, out float y); + return Math.Max(x, y); + } + } } } diff --git a/Dashboard/Application.cs b/Dashboard/Application.cs deleted file mode 100644 index 62db7dc..0000000 --- a/Dashboard/Application.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using Dashboard.Windowing; - -namespace Dashboard -{ - public class Application : IDisposable - { - public IDashboardBackend Backend { get; } - public IWindowFactory WindowFactory { get; set; } - - public string Name { get; } = "Dashboard Application"; - - protected bool IsInitialized { get; private set; } = false; - protected bool IsDisposed { get; private set; } = false; - - private readonly List _physicalWindows = new List(); - - public event EventHandler? PreInitializing; - public event EventHandler? Initializing; - public event EventHandler? PostInitializing; - public event EventHandler? Leaving; - - public Application(IDashboardBackend backend) - { - Backend = backend; - WindowFactory = backend; - } - - protected virtual void PreInitialize() - { - PreInitializing?.Invoke(this, EventArgs.Empty); - } - - public virtual void Initialize() - { - Backend.Initialize(); - Initializing?.Invoke(this, EventArgs.Empty); - } - - protected virtual void PostInitialize() - { - PostInitializing?.Invoke(this, EventArgs.Empty); - } - - private void InitializeInternal() - { - if (IsInitialized) - return; - - IsInitialized = true; - - PreInitialize(); - Initialize(); - PostInitialize(); - } - - public virtual void RunEvents(bool wait) - { - if (!IsInitialized) - throw new InvalidOperationException("The application is not initialized. Cannot run events at this time."); - - Backend.RunEvents(wait); - } - - public virtual void Leave() - { - Backend.Leave(); - Leaving?.Invoke(this, EventArgs.Empty); - } - - 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); - - foreach (IPhysicalWindow window in _physicalWindows) - { - window.Paint(); - } - } - - Leave(); - } - - /// - public IWindow CreateWindow() - { - IWindow window = WindowFactory.CreateWindow(); - - if (window is IPhysicalWindow physical) - _physicalWindows.Add(physical); - - return window; - } - - /// - public IPhysicalWindow CreatePhysicalWindow() - { - IPhysicalWindow window = WindowFactory.CreatePhysicalWindow(); - _physicalWindows.Add(window); - return window; - } - - protected virtual void Dispose(bool disposing) - { - } - - protected void InvokeDispose(bool disposing) - { - if (IsDisposed) - return; - - IsDisposed = true; - - Dispose(disposing); - - if (disposing) - GC.SuppressFinalize(this); - } - - public void Dispose() => InvokeDispose(true); - } -} \ No newline at end of file diff --git a/Dashboard/IDashboardBackend.cs b/Dashboard/IDashboardBackend.cs deleted file mode 100644 index f56ecb4..0000000 --- a/Dashboard/IDashboardBackend.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using Dashboard.Windowing; - -namespace Dashboard -{ - public interface IWindowFactory - { - /// - /// Creates a window. It could be a virtual window, or a physical window. - /// - /// A window. - IWindow CreateWindow(); - - /// - /// Always creates a physical window. - /// - /// A physical window. - IPhysicalWindow CreatePhysicalWindow(); - } - - public interface IDashboardBackend : IDisposable, IWindowFactory - { - void Initialize(); - - void RunEvents(bool wait); - - void Leave(); - } -} \ No newline at end of file diff --git a/tests/Dashboard.TestApplication/Program.cs b/tests/Dashboard.TestApplication/Program.cs index 1b174b9..4a2786c 100644 --- a/tests/Dashboard.TestApplication/Program.cs +++ b/tests/Dashboard.TestApplication/Program.cs @@ -10,6 +10,9 @@ using OpenTK.Mathematics; using Box2d = Dashboard.Box2d; using TK = OpenTK.Platform.Toolkit; using Dashboard; +using Dashboard.Windowing; +using OpenTK.Windowing.GraphicsLibraryFramework; +using AppContext = Dashboard.Pal.AppContext; TK.Init(new ToolkitOptions() { @@ -18,11 +21,12 @@ TK.Init(new ToolkitOptions() { EnableVisualStyles = true, IsDPIAware = true, - } + }, + FeatureFlags = ToolkitFlags.EnableOpenGL, }); -Application app = new Application(new Pal2DashboardBackend() -{ +AppContext app = new Pal2AppContext() +{ GraphicsApiHints = new OpenGLGraphicsApiHints() { Version = new Version(3, 2), @@ -41,7 +45,7 @@ Application app = new Application(new Pal2DashboardBackend() SupportTransparentFramebufferX11 = true, } -}); +}; PhysicalWindow window; SolidBrush fg = new SolidBrush(Color.FromArgb(0, 0, 0, 0)); @@ -56,121 +60,121 @@ List points = new List(); IFont font; StringBuilder builder = new StringBuilder(); -app.PostInitializing += (sender, ea) => { - window = (PhysicalWindow)app.CreatePhysicalWindow(); +app.Initialize(); - window.Title = "DashTerm"; - TK.Window.SetMinClientSize(window.WindowHandle, 300, 200); - TK.Window.SetClientSize(window.WindowHandle, new Vector2i(320, 240)); - TK.Window.SetBorderStyle(window.WindowHandle, WindowBorderStyle.ResizableBorder); - // TK.Window.SetTransparencyMode(wnd, WindowTransparencyMode.TransparentFramebuffer, 0.1f); +window = (PhysicalWindow)app.CreatePhysicalWindow(); - OpenGLDeviceContext context = (OpenGLDeviceContext)window.DeviceContext; +window.Title = "DashTerm"; +TK.Window.SetMinClientSize(window.WindowHandle, 300, 200); +TK.Window.SetClientSize(window.WindowHandle, new Vector2i(320, 240)); +TK.Window.SetBorderStyle(window.WindowHandle, WindowBorderStyle.ResizableBorder); +// TK.Window.SetTransparencyMode(wnd, WindowTransparencyMode.TransparentFramebuffer, 0.1f); - context.MakeCurrent(); - context.SwapGroup.SwapInterval = 1; +OpenGLDeviceContext context = (OpenGLDeviceContext)window.DeviceContext; - engine = new GLEngine(); - engine.Initialize(); - - executor = engine.GetExecutor(context); +context.MakeCurrent(); +context.SwapGroup.SwapInterval = 1; - dimUI = new DimUI(new DimUIConfig() +engine = new GLEngine(); +engine.Initialize(); + +executor = engine.GetExecutor(context); + +dimUI = new DimUI(new DimUIConfig() +{ + Font = new NamedFont("Noto Sans", 9f), +}); + +EventQueue.EventRaised += (handle, type, eventArgs) => +{ + if (handle != window.WindowHandle) + return; + + switch (type) { - Font = new NamedFont("Noto Sans", 9f), - }); + case PlatformEventType.Close: + source.Cancel(); + break; + case PlatformEventType.MouseMove: + mousePos = ((MouseMoveEventArgs)eventArgs).ClientPosition; + break; + } +}; - EventQueue.EventRaised += (handle, type, eventArgs) => - { - if (handle != window.WindowHandle) - return; +TK.Window.SetMode(window.WindowHandle, WindowMode.Normal); +font = Typesetter.LoadFont("Nimbus Mono", 12f); - switch (type) +window.Painting += (sender, ea) => { + TK.Window.GetSize(context.WindowHandle, out Vector2i size); + executor.BeginFrame(); + + dimUI.Begin(new Box2d(0, 0, size.X, size.Y), window.DrawQueue); + dimUI.Text("Hello World!"); + dimUI.Button("Cancel"); dimUI.SameLine(); + dimUI.Button("OK"); + + dimUI.Input("type me!", builder); + + dimUI.BeginMenu(); + + if (dimUI.MenuItem("File")) { - case PlatformEventType.Close: - source.Cancel(); - break; - case PlatformEventType.MouseMove: - mousePos = ((MouseMoveEventArgs)eventArgs).ClientPosition; - break; - } - }; - - TK.Window.SetMode(window.WindowHandle, WindowMode.Normal); - font = Typesetter.LoadFont("Nimbus Mono", 12f); - - window.Painting += (sender, ea) => { - TK.Window.GetFramebufferSize(context.WindowHandle, out Vector2i framebufferSize); - executor.BeginFrame(); - - dimUI.Begin(new Box2d(0, 0, framebufferSize.X, framebufferSize.Y), window.DrawQueue); - dimUI.Text("Hello World!"); - dimUI.Button("Cancel"); dimUI.SameLine(); - dimUI.Button("OK"); - - dimUI.Input("type me!", builder); - dimUI.BeginMenu(); + dimUI.MenuItem("New Window"); + dimUI.MenuItem("Preferences"); + dimUI.MenuItem("Exit"); + dimUI.EndMenu(); + } - if (dimUI.MenuItem("File")) + if (dimUI.MenuItem("Edit")) + { + dimUI.BeginMenu(); + dimUI.MenuItem("Cut"); + dimUI.MenuItem("Copy"); + dimUI.MenuItem("Paste"); + + if (dimUI.MenuItem("Send Char")) { dimUI.BeginMenu(); - dimUI.MenuItem("New Window"); - dimUI.MenuItem("Preferences"); - dimUI.MenuItem("Exit"); dimUI.EndMenu(); } - if (dimUI.MenuItem("Edit")) + dimUI.EndMenu(); + } + + if (dimUI.MenuItem("View")) + { + dimUI.BeginMenu(); + dimUI.MenuItem("Clear"); + + if (dimUI.MenuItem("Set Size")) { dimUI.BeginMenu(); - dimUI.MenuItem("Cut"); - dimUI.MenuItem("Copy"); - dimUI.MenuItem("Paste"); - - if (dimUI.MenuItem("Send Char")) - { - dimUI.BeginMenu(); - dimUI.EndMenu(); - } - + dimUI.MenuItem("24 x 40"); + dimUI.MenuItem("25 x 40"); + dimUI.MenuItem("24 x 80"); + dimUI.MenuItem("25 x 80"); + dimUI.MenuItem("25 x 120"); dimUI.EndMenu(); } - if (dimUI.MenuItem("View")) - { - dimUI.BeginMenu(); - dimUI.MenuItem("Clear"); + dimUI.EndMenu(); + } - if (dimUI.MenuItem("Set Size")) - { - dimUI.BeginMenu(); - dimUI.MenuItem("24 x 40"); - dimUI.MenuItem("25 x 40"); - dimUI.MenuItem("24 x 80"); - dimUI.MenuItem("25 x 80"); - dimUI.MenuItem("25 x 120"); - dimUI.EndMenu(); - } + dimUI.Finish(); - dimUI.EndMenu(); - } + GL.Viewport(0, 0, size.X, size.Y); + GL.ClearColor(0.3f, 0.3f, 0.3f, 1.0f); + GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); + GL.Disable(EnableCap.DepthTest); + GL.Enable(EnableCap.Blend); + GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); + GL.ColorMask(true, true, true, true); - dimUI.Finish(); + executor.Draw(window.DrawQueue, new RectangleF(0, 0, size.X, size.Y), 1.5f /*(window as IDpiAwareWindow)?.Scale ?? 1*/); + executor.EndFrame(); - GL.Viewport(0, 0, framebufferSize.X, framebufferSize.Y); - GL.ClearColor(0.3f, 0.3f, 0.3f, 1.0f); - GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); - GL.Disable(EnableCap.DepthTest); - GL.Enable(EnableCap.Blend); - GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); - GL.ColorMask(true, true, true, true); - - executor.Draw(window.DrawQueue); - executor.EndFrame(); - - context.SwapGroup.Swap(); - }; + context.SwapGroup.Swap(); }; app.Run(true, source.Token);