From ef922486eb37f3c3ab78f265050adb84ebe012fe Mon Sep 17 00:00:00 2001 From: lionarius Date: Sat, 14 Dec 2024 11:17:47 +0300 Subject: [PATCH] . --- DoomDeathmatch.sln | 6 + DoomDeathmatch/Assets/test.shader | 19 - DoomDeathmatch/DoomDeathmatch.csproj | 10 +- DoomDeathmatch/src/Program.cs | 14 +- DoomDeathmatch/src/QuadVertex.cs | 12 +- Engine/Engine.csproj | 26 +- Engine/src/Asset/Image.cs | 6 +- Engine/src/Engine.cs | 118 +- Engine/src/EngineBuilder.cs | 151 +++ .../src/Graphics/Render/Quad/QuadRenderer.cs | 8 +- Engine/src/Graphics/Renderer.cs | 25 +- Engine/src/Graphics/Texture/TextureUnitMap.cs | 23 +- Engine/src/Resource/ShaderResource.resx | 13 +- Engine/src/Window.cs | 2 - EngineTests/EngineTests.csproj | 4 +- EngineTests/src/Scene/GameObjectTests.cs | 110 +- EngineTests/src/Scene/HierarchyTests.cs | 1050 ++++++++--------- PresenterConsole/PresenterConsole.csproj | 1 + PresenterConsole/assets/shader/ascii.shader | 126 +- PresenterConsole/src/AsciiPixel.cs | 4 +- PresenterConsole/src/ConsoleFastOutput.cs | 157 +-- PresenterConsole/src/ConsolePresenter.cs | 17 +- PresenterConsole/src/Program.cs | 16 +- PresenterConsole/src/WindowsFFI.cs | 63 + PresenterNative/PresenterNative.csproj | 14 + PresenterNative/src/Program.cs | 23 + PresenterWpf/src/App.xaml.cs | 12 +- PresenterWpf/src/MainWindow.xaml | 3 +- PresenterWpf/src/MainWindow.xaml.cs | 46 +- PresenterWpf/src/Program.cs | 5 - 30 files changed, 1135 insertions(+), 949 deletions(-) delete mode 100644 DoomDeathmatch/Assets/test.shader create mode 100644 Engine/src/EngineBuilder.cs create mode 100644 PresenterConsole/src/WindowsFFI.cs create mode 100644 PresenterNative/PresenterNative.csproj create mode 100644 PresenterNative/src/Program.cs delete mode 100644 PresenterWpf/src/Program.cs diff --git a/DoomDeathmatch.sln b/DoomDeathmatch.sln index d20fae8..15066e3 100644 --- a/DoomDeathmatch.sln +++ b/DoomDeathmatch.sln @@ -10,6 +10,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PresenterWpf", "PresenterWp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PresenterConsole", "PresenterConsole\PresenterConsole.csproj", "{85AA55C5-F8AF-4C38-8874-702684BCAFBB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PresenterNative", "PresenterNative\PresenterNative.csproj", "{3B8B7867-5B38-4013-A366-159083F5C095}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -36,5 +38,9 @@ Global {85AA55C5-F8AF-4C38-8874-702684BCAFBB}.Debug|Any CPU.Build.0 = Debug|Any CPU {85AA55C5-F8AF-4C38-8874-702684BCAFBB}.Release|Any CPU.ActiveCfg = Release|Any CPU {85AA55C5-F8AF-4C38-8874-702684BCAFBB}.Release|Any CPU.Build.0 = Release|Any CPU + {3B8B7867-5B38-4013-A366-159083F5C095}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3B8B7867-5B38-4013-A366-159083F5C095}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3B8B7867-5B38-4013-A366-159083F5C095}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3B8B7867-5B38-4013-A366-159083F5C095}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/DoomDeathmatch/Assets/test.shader b/DoomDeathmatch/Assets/test.shader deleted file mode 100644 index 464f8b5..0000000 --- a/DoomDeathmatch/Assets/test.shader +++ /dev/null @@ -1,19 +0,0 @@ -#shader vertex -#version 330 core - -layout(location = 0) in vec2 aPos; - -void main() -{ - gl_Position = vec4(aPos, 0.0, 1.0); -} - -#shader fragment -#version 330 core - -out vec4 FragColor; - -void main() -{ - FragColor = vec4(1.0, 1.0, 0.0, 1.0); -} \ No newline at end of file diff --git a/DoomDeathmatch/DoomDeathmatch.csproj b/DoomDeathmatch/DoomDeathmatch.csproj index 90e66bb..025d582 100644 --- a/DoomDeathmatch/DoomDeathmatch.csproj +++ b/DoomDeathmatch/DoomDeathmatch.csproj @@ -8,14 +8,14 @@ - + - - - - + + + + diff --git a/DoomDeathmatch/src/Program.cs b/DoomDeathmatch/src/Program.cs index 9e8ab43..5f53be4 100644 --- a/DoomDeathmatch/src/Program.cs +++ b/DoomDeathmatch/src/Program.cs @@ -6,15 +6,7 @@ namespace DoomDeathmatch; internal abstract class Program { - [DllImport("kernel32.dll")] - static extern IntPtr GetConsoleWindow(); - - [DllImport("User32.dll", SetLastError = true)] - public static extern IntPtr GetDC(IntPtr hwnd); - - public static void Main(string[] args) - { - var engine = new Engine.Engine(480, 270, false, "Doom Deathmatch"); - engine.Run(); - } + public static void Main(string[] args) + { + } } \ No newline at end of file diff --git a/DoomDeathmatch/src/QuadVertex.cs b/DoomDeathmatch/src/QuadVertex.cs index b29ae81..f26909f 100644 --- a/DoomDeathmatch/src/QuadVertex.cs +++ b/DoomDeathmatch/src/QuadVertex.cs @@ -7,15 +7,11 @@ namespace DoomDeathmatch; public struct QuadVertex : IVertex { - [Vertex(VertexAttribType.Float, 2)] - public Vector2 Position2; + [Vertex(VertexAttribType.Float, 2)] public Vector2 Position2; - [Vertex(VertexAttribType.Float, 2)] - public Vector2 Position; + [Vertex(VertexAttribType.Float, 2)] public Vector2 Position; - [Vertex(VertexAttribType.Float, 2)] - public Vector2 Position4; + [Vertex(VertexAttribType.Float, 2)] public Vector2 Position4; - [Vertex(VertexAttribType.Float, 2)] - public Vector2 Position3; + [Vertex(VertexAttribType.Float, 2)] public Vector2 Position3; } \ No newline at end of file diff --git a/Engine/Engine.csproj b/Engine/Engine.csproj index efd2e0e..d1889fc 100644 --- a/Engine/Engine.csproj +++ b/Engine/Engine.csproj @@ -10,31 +10,37 @@ true + + true + + + + - - ResXFileCodeGenerator - Test.Designer.cs - PreserveNewest - + + ResXFileCodeGenerator + Test.Designer.cs + PreserveNewest + - - True - True - ShaderResource.resx - + + True + True + ShaderResource.resx + diff --git a/Engine/src/Asset/Image.cs b/Engine/src/Asset/Image.cs index 8b1ee4b..9fc6581 100644 --- a/Engine/src/Asset/Image.cs +++ b/Engine/src/Asset/Image.cs @@ -6,14 +6,16 @@ namespace Engine.Asset; public class Image(T[,] parPixels) where T : struct, IPixel { - public int Width { get; } = parPixels.GetLength(1); - public int Height { get; } = parPixels.GetLength(0); + public int Width { get; } + public int Height { get; } public T[,] Pixels { get; } = parPixels; public T this[int parY, int parX] => Pixels[parY, parX]; public Image(int parWidth, int parHeight) : this(new T[parHeight, parWidth]) { + Width = parWidth; + Height = parHeight; } public DynamicTexture ToDynamicTexture() diff --git a/Engine/src/Engine.cs b/Engine/src/Engine.cs index 853b80d..1ec77b1 100644 --- a/Engine/src/Engine.cs +++ b/Engine/src/Engine.cs @@ -7,25 +7,49 @@ using OpenTK.Mathematics; using OpenTK.Windowing.Common; using OpenTK.Windowing.Desktop; using Serilog; -using Serilog.Sinks.SystemConsole.Themes; namespace Engine; public sealed class Engine { - public Renderer Renderer => _renderer; - public SceneManager SceneManager => _sceneManager; + public Renderer Renderer { get; } + public SceneManager SceneManager { get; } = new(); + + internal Window Window { get; } + + internal IPresenter? Presenter + { + get => _presenter; + set + { + if (_presenter != null) + { + _presenter.Resize -= PresenterResize; + } + + _presenter = value; + + if (_presenter != null) + { + _presenter.Resize += PresenterResize; + } + } + } + + internal IInputHandler? InputHandler + { + get => _inputHandler; + set => _inputHandler = value; + } - private readonly Window _window; - private readonly Renderer _renderer; - private readonly SceneManager _sceneManager = new(); private readonly ILogger _logger; - private readonly IInputHandler _inputHandler; - private readonly IPresenter _presenter; + + private IInputHandler? _inputHandler; + private IPresenter? _presenter; private Thread? _updateThread; - public Engine(int parWidth, int parHeight, bool parHeadless = false, string parTitle = "") + public Engine(int parWidth, int parHeight, bool parHeadless, string parTitle, ILogger parLogger) { var settings = new NativeWindowSettings { @@ -33,41 +57,16 @@ public sealed class Engine Title = parTitle, StartVisible = !parHeadless, APIVersion = new Version(4, 6), - Profile = ContextProfile.Compatability + Profile = ContextProfile.Core }; - _window = new Window(this, new NativeWindow(settings), parHeadless); - _renderer = new Renderer(parWidth, parHeight); - if (!parHeadless) - { - _window.Resize += parArgs => - { - _renderer.Resize(parArgs.Width, parArgs.Height); - }; - _presenter = _window; - } - - Thread.CurrentThread.Name = "RendererThread"; - - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Verbose() - .Enrich.WithThreadName() - .Enrich.WithThreadId() - .Enrich.FromLogContext() - .WriteTo.Console( - outputTemplate: - "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level:u3}] [{ThreadName,-15:l}:{ThreadId,-4:d4}] [{SourceContext:l}] {Message:lj}{NewLine}{Exception}", - theme: AnsiConsoleTheme.Literate) - .CreateLogger(); + Window = new Window(this, new NativeWindow(settings), parHeadless); + Renderer = new Renderer(parWidth, parHeight); + Log.Logger = parLogger; _logger = Log.ForContext(); } - public Engine(int parWidth, int parHeight, Func parPresenter) : this(parWidth, parHeight, true) - { - _presenter = parPresenter(this); - } - public void Run() { _updateThread = new Thread(RunUpdate); @@ -75,23 +74,24 @@ public sealed class Engine var timer = Stopwatch.StartNew(); var deltaTime = 0.0; - while (!_presenter.IsExiting) + while (!Presenter?.IsExiting ?? false) { - var time = deltaTime; - _renderer.Commit(_ => GL.ClearColor(0.6f, 0.0f, 0.6f, 1.0f)); - _renderer.Commit(_ => GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit)); - _renderer.Commit(parRenderer => - { - parRenderer.QuadRenderer.Commit( - Matrix4.CreateScale(MathF.Sin((float)time) * 2) * Matrix4.CreateRotationZ(MathF.Sin((float)time)), - new Vector4((MathF.Sin((float)time * 3) + 1.0f) / 2, (MathF.Sin((float)time) + 1.0f) / 2, (MathF.Sin((float)time * 5) + 1.0f) / 2, 1.0f)); - parRenderer.QuadRenderer.Render(Matrix4.Identity, Matrix4.Identity); - parRenderer.QuadRenderer.Reset(); - }); - _renderer.Render(); - _presenter.Present(_renderer.TextureInternal); + Renderer.StartFrame(); - _presenter.Update(deltaTime); + GL.ClearColor(0.6f, 0.0f, 0.6f, 1.0f); + GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); + + Renderer.QuadRenderer.Commit( + Matrix4.CreateRotationZ((float)deltaTime), + new Vector4((MathF.Sin((float)deltaTime) + 1.0f) / 2, (MathF.Sin((float)deltaTime) + 1.0f) / 2, + (MathF.Sin((float)deltaTime) + 1.0f) / 2, 1.0f)); + Renderer.QuadRenderer.Render(Matrix4.Identity, Matrix4.Identity); + Renderer.QuadRenderer.Reset(); + + Renderer.EndFrame(); + + Presenter!.Present(Renderer.TextureInternal); + Presenter!.Update(deltaTime); deltaTime = timer.Elapsed.TotalSeconds; } @@ -102,11 +102,19 @@ public sealed class Engine { var timer = Stopwatch.StartNew(); var deltaTime = 0.0; - while (!_presenter.IsExiting) + while (!Presenter?.IsExiting ?? false) { - _sceneManager.Update(deltaTime); + SceneManager.Update(deltaTime); timer.Restart(); deltaTime = timer.Elapsed.TotalSeconds; } } + + private void PresenterResize(ResizeEventArgs parEventArgs) + { + if (parEventArgs.Width == 0 || parEventArgs.Height == 0) + return; + + Renderer.Resize(parEventArgs.Width, parEventArgs.Height); + } } \ No newline at end of file diff --git a/Engine/src/EngineBuilder.cs b/Engine/src/EngineBuilder.cs new file mode 100644 index 0000000..264a3b2 --- /dev/null +++ b/Engine/src/EngineBuilder.cs @@ -0,0 +1,151 @@ +using Engine.Graphics; +using Engine.Input; +using Serilog; +using Serilog.Core; +using Serilog.Events; +using Serilog.Sinks.SystemConsole.Themes; + +namespace Engine; + +public sealed class EngineBuilder +{ + private string _title = ""; + private bool _headless; + private int _width = 1; + private int _height = 1; + + private Func? _presenterFunc; + private Func? _inputHandlerFunc; + + // Logging + private bool _logToConsole; + private bool _logToFile; + private string? _logFilePath; + private LogEventLevel _logLevel = LogEventLevel.Information; + + public EngineBuilder() { } + + public EngineBuilder Title(string parTitle) + { + _title = parTitle; + return this; + } + + public EngineBuilder Headless(bool parHeadless = true) + { + _headless = parHeadless; + return this; + } + + public EngineBuilder Width(int parWidth) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(parWidth); + + _width = parWidth; + return this; + } + + public EngineBuilder Height(int parHeight) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(parHeight); + + _height = parHeight; + return this; + } + + public EngineBuilder Presenter(IPresenter parPresenter) + { + _presenterFunc = _ => parPresenter; + return this; + } + + public EngineBuilder Presenter(Func parPresenterFunc) + { + _presenterFunc = parPresenterFunc; + return this; + } + + public EngineBuilder InputHandler(IInputHandler parInputHandler) + { + _inputHandlerFunc = _ => parInputHandler; + return this; + } + + public EngineBuilder InputHandler(Func parInputHandlerFunc) + { + _inputHandlerFunc = parInputHandlerFunc; + return this; + } + + public EngineBuilder LogToConsole(bool parLogToConsole = true) + { + _logToConsole = parLogToConsole; + return this; + } + + public EngineBuilder LogToFile(bool parLogToFile = true, string? parLogFilePath = null) + { + if (parLogToFile && parLogFilePath == null) + throw new ArgumentNullException(nameof(parLogFilePath)); + + _logToFile = parLogToFile; + _logFilePath = parLogFilePath; + return this; + } + + public EngineBuilder LogLevel(LogEventLevel parLogLevel) + { + _logLevel = parLogLevel; + return this; + } + + public Engine Build() + { + var logger = BuildLogger(); + var engine = new Engine(_width, _height, _headless, _title, logger); + + var presenter = _presenterFunc?.Invoke(engine); + if (presenter != null) + { + engine.Presenter = presenter; + } + + var inputHandler = _inputHandlerFunc?.Invoke(engine); + if (inputHandler != null) + { + engine.InputHandler = inputHandler; + } + + return engine; + } + + private Logger BuildLogger() + { + const string template = + "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level:u3}] [{ThreadName,-15:l}:{ThreadId,-4:d4}] [{SourceContext:l}] {Message:lj}{NewLine}{Exception}"; + + var loggerConfiguration = new LoggerConfiguration() + .MinimumLevel.Is(_logLevel) + .Enrich.WithThreadName() + .Enrich.WithThreadId() + .Enrich.FromLogContext(); + + if (_logToConsole) + { + loggerConfiguration.WriteTo.Console( + outputTemplate: template, + theme: AnsiConsoleTheme.Literate + ); + } + + if (_logToFile) + { + loggerConfiguration.WriteTo.File( + _logFilePath!, + outputTemplate: template + ); + } + + return loggerConfiguration.CreateLogger(); + } +} \ No newline at end of file diff --git a/Engine/src/Graphics/Render/Quad/QuadRenderer.cs b/Engine/src/Graphics/Render/Quad/QuadRenderer.cs index 1657552..832a8bf 100644 --- a/Engine/src/Graphics/Render/Quad/QuadRenderer.cs +++ b/Engine/src/Graphics/Render/Quad/QuadRenderer.cs @@ -8,6 +8,7 @@ namespace Engine.Graphics.Render.Quad; public class QuadRenderer : InstancedRenderer { private readonly TextureUnitMap _textureUnitMap = new(16); + private readonly int[] _textureUnitIndices = new int[16]; public QuadRenderer(Renderer parRenderer, int parInstanceCount) : base(parRenderer, PrimitiveType.Triangles, parInstanceCount, [0, 1, 2, 2, 3, 0], [ @@ -41,14 +42,13 @@ public class QuadRenderer : InstancedRenderer _framebuffer.TextureInternal!; - public readonly QuadRenderer QuadRenderer; + public QuadRenderer QuadRenderer { get; } private readonly Framebuffer.Framebuffer _framebuffer; - private readonly Queue> _renderActions = new(); private readonly Thread _renderThread; public Renderer(int parWidth, int parHeight) { + Thread.CurrentThread.Name = "RendererThread"; + InitializeOpenGl(parWidth, parHeight); _renderThread = Thread.CurrentThread; @@ -37,7 +38,7 @@ public class Renderer GL.Enable(EnableCap.DepthTest); - GL.Enable(EnableCap.FramebufferSrgb); + // GL.Enable(EnableCap.FramebufferSrgb); GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); GL.Enable(EnableCap.Blend); @@ -47,28 +48,26 @@ public class Renderer public void EnsureRenderThread() { +#if DEBUG if (Thread.CurrentThread == _renderThread) { return; } throw new InvalidOperationException("Renderer is not on render thread"); +#endif } - internal void Commit(Action parRenderAction) - { - _renderActions.Enqueue(parRenderAction); - } - - internal void Render() + internal void StartFrame() { EnsureRenderThread(); _framebuffer.Bind(); - while (_renderActions.TryDequeue(out var renderAction)) - { - renderAction(this); - } + } + + internal void EndFrame() + { + EnsureRenderThread(); _framebuffer.Unbind(); } diff --git a/Engine/src/Graphics/Texture/TextureUnitMap.cs b/Engine/src/Graphics/Texture/TextureUnitMap.cs index eda650a..df1eea0 100644 --- a/Engine/src/Graphics/Texture/TextureUnitMap.cs +++ b/Engine/src/Graphics/Texture/TextureUnitMap.cs @@ -2,18 +2,13 @@ namespace Engine.Graphics.Texture; -public class TextureUnitMap : IEnumerable> +public class TextureUnitMap(int parCapacity) { public int Size => _textures.Count; - public int Capacity => _capacity; + public int Capacity => parCapacity; + public IReadOnlyDictionary Textures => _textures; private readonly Dictionary _textures = new(); - private readonly int _capacity; - - public TextureUnitMap(int parCapacity) - { - _capacity = parCapacity; - } public int GetUnit(Texture parTexture) { @@ -22,7 +17,7 @@ public class TextureUnitMap : IEnumerable> return unit; } - if (_textures.Count >= _capacity) + if (_textures.Count >= parCapacity) { throw new InvalidOperationException("Texture unit map is full"); } @@ -37,14 +32,4 @@ public class TextureUnitMap : IEnumerable> { _textures.Clear(); } - - public IEnumerator> GetEnumerator() - { - return _textures.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } } \ No newline at end of file diff --git a/Engine/src/Resource/ShaderResource.resx b/Engine/src/Resource/ShaderResource.resx index a854c31..a78eb25 100644 --- a/Engine/src/Resource/ShaderResource.resx +++ b/Engine/src/Resource/ShaderResource.resx @@ -1,9 +1,10 @@  - + - + @@ -13,10 +14,14 @@ 1.3 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + diff --git a/Engine/src/Window.cs b/Engine/src/Window.cs index 93e586e..d315603 100644 --- a/Engine/src/Window.cs +++ b/Engine/src/Window.cs @@ -51,8 +51,6 @@ public class Window : IPresenter public void Present(IConstTexture parTexture) { - GL.Viewport(0, 0, Width, Height); - GL.ClearColor(0.0f, 0.0f, 0.0f, 1.0f); GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); diff --git a/EngineTests/EngineTests.csproj b/EngineTests/EngineTests.csproj index fccc8f0..a5e5c2e 100644 --- a/EngineTests/EngineTests.csproj +++ b/EngineTests/EngineTests.csproj @@ -22,11 +22,11 @@ - + - + diff --git a/EngineTests/src/Scene/GameObjectTests.cs b/EngineTests/src/Scene/GameObjectTests.cs index c559edf..b186134 100644 --- a/EngineTests/src/Scene/GameObjectTests.cs +++ b/EngineTests/src/Scene/GameObjectTests.cs @@ -5,67 +5,67 @@ namespace EngineTests.Scene; public class GameObjectTests { - private GameObject _gameObject; + private GameObject _gameObject; - [SetUp] - public void Setup() + [SetUp] + public void Setup() + { + _gameObject = new GameObject(); + } + + [Test] + public void Constructor_ShouldInitializeWithTransformComponent() + { + Assert.Multiple(() => { - _gameObject = new GameObject(); - } + Assert.That(_gameObject.Transform, Is.Not.Null); + Assert.That(_gameObject.HasComponent(), Is.True); + }); + } - [Test] - public void Constructor_ShouldInitializeWithTransformComponent() + [Test] + public void AddComponent_ShouldThrowIfComponentAlreadyExists() + { + _gameObject.AddComponent(); + + Assert.Throws(() => _gameObject.ProcessChanges()); + } + + [Test] + public void AddComponent_ShouldAddComponentToGameObject() + { + _gameObject.AddComponent(); + _gameObject.ProcessChanges(); + + Assert.Multiple(() => { - Assert.Multiple(() => - { - Assert.That(_gameObject.Transform, Is.Not.Null); - Assert.That(_gameObject.HasComponent(), Is.True); - }); - } + Assert.That(_gameObject.GetComponent(), Is.Not.Null); + Assert.That(_gameObject.HasComponent(), Is.True); + }); + } - [Test] - public void AddComponent_ShouldThrowIfComponentAlreadyExists() + [Test] + public void AddComponent_ShouldAddComponentToGameObjectWithArgs() + { + _gameObject.AddComponent(99, 0.2f, 1001f); + _gameObject.ProcessChanges(); + + var camera = _gameObject.GetComponent()!; + + Assert.Multiple(() => { - _gameObject.AddComponent(); + Assert.That(camera.FieldOfView, Is.EqualTo(99)); + Assert.That(camera.NearPlane, Is.EqualTo(0.2f)); + Assert.That(camera.FarPlane, Is.EqualTo(1001f)); + }); + } - Assert.Throws(() => _gameObject.ProcessChanges()); - } + [Test] + public void AddComponent_ShouldThrowIfComponentBaseTypeAlreadyExists() + { + _gameObject.AddComponent(); + _gameObject.AddComponent(); - [Test] - public void AddComponent_ShouldAddComponentToGameObject() - { - _gameObject.AddComponent(); - _gameObject.ProcessChanges(); - - Assert.Multiple(() => - { - Assert.That(_gameObject.GetComponent(), Is.Not.Null); - Assert.That(_gameObject.HasComponent(), Is.True); - }); - } - - [Test] - public void AddComponent_ShouldAddComponentToGameObjectWithArgs() - { - _gameObject.AddComponent(99, 0.2f, 1001f); - _gameObject.ProcessChanges(); - - var camera = _gameObject.GetComponent()!; - - Assert.Multiple(() => - { - Assert.That(camera.FieldOfView, Is.EqualTo(99)); - Assert.That(camera.NearPlane, Is.EqualTo(0.2f)); - Assert.That(camera.FarPlane, Is.EqualTo(1001f)); - }); - } - - [Test] - public void AddComponent_ShouldThrowIfComponentBaseTypeAlreadyExists() - { - _gameObject.AddComponent(); - _gameObject.AddComponent(); - - Assert.Throws(() => _gameObject.ProcessChanges()); - } + Assert.Throws(() => _gameObject.ProcessChanges()); + } } \ No newline at end of file diff --git a/EngineTests/src/Scene/HierarchyTests.cs b/EngineTests/src/Scene/HierarchyTests.cs index 2b9e426..508bf98 100644 --- a/EngineTests/src/Scene/HierarchyTests.cs +++ b/EngineTests/src/Scene/HierarchyTests.cs @@ -4,563 +4,563 @@ namespace EngineTests.Scene; public class HierarchyTests { - private Hierarchy _hierarchy; + private Hierarchy _hierarchy; - [SetUp] - public void Setup() + [SetUp] + public void Setup() + { + _hierarchy = []; + } + + [Test] + public void AddToRoot() + { + var obj = new object(); + _hierarchy.Add(obj); + _hierarchy.ProcessChanges(); + + Assert.Multiple(() => { - _hierarchy = new Hierarchy(); + Assert.That(_hierarchy.GetChildren().Count(), Is.EqualTo(1)); + Assert.That(_hierarchy.GetParent(obj), Is.Null); + }); + } + + [Test] + public void AddToChild() + { + var parent = new object(); + var child = new object(); + + _hierarchy.Add(parent); + _hierarchy.Add(child); + + _hierarchy.AddChild(parent, child); + _hierarchy.ProcessChanges(); + + Assert.Multiple(() => + { + Assert.That(_hierarchy.GetChildren(parent).Count(), Is.EqualTo(1)); + Assert.That(_hierarchy.GetParent(parent), Is.Null); + Assert.That(_hierarchy.GetParent(child), Is.EqualTo(parent)); + }); + } + + [Test] + public void RemoveChild() + { + var parent = new object(); + var child = new object(); + + _hierarchy.Add(parent); + _hierarchy.Add(child); + + _hierarchy.AddChild(parent, child); + _hierarchy.ProcessChanges(); + + _hierarchy.Remove(child); + _hierarchy.ProcessChanges(); + + Assert.Multiple(() => + { + Assert.That(_hierarchy.GetChildren(parent).Count(), Is.EqualTo(0)); + Assert.That(_hierarchy.Contains(child), Is.False); + }); + } + + [Test] + public void RemoveParentAndAllChildren() + { + var parent = new object(); + var child1 = new object(); + var child2 = new object(); + + _hierarchy.Add(parent); + _hierarchy.Add(child1); + _hierarchy.Add(child2); + + _hierarchy.AddChild(parent, child1); + _hierarchy.AddChild(parent, child2); + _hierarchy.ProcessChanges(); + + _hierarchy.Remove(parent); + _hierarchy.ProcessChanges(); + + Assert.Multiple(() => + { + Assert.That(_hierarchy.GetChildren().Count(), Is.EqualTo(0)); + Assert.That(_hierarchy.Contains(child1), Is.False); + Assert.That(_hierarchy.Contains(child2), Is.False); + Assert.That(_hierarchy.Contains(parent), Is.False); + }); + } + + [Test] + public void DetectCycleInHierarchy() + { + var parent = new object(); + var child = new object(); + + _hierarchy.Add(parent); + _hierarchy.AddChild(parent, child); + _hierarchy.AddChild(child, parent); + + Assert.Throws(() => _hierarchy.ProcessChanges()); + } + + [Test] + public void IsInHierarchy_PositiveCase() + { + var ancestor = new object(); + var child = new object(); + + _hierarchy.Add(ancestor); + _hierarchy.Add(child); + + _hierarchy.AddChild(ancestor, child); + _hierarchy.ProcessChanges(); + + Assert.That(_hierarchy.IsInHierarchy(ancestor, child), Is.True); + } + + [Test] + public void IsInHierarchy_NegativeCase() + { + var obj1 = new object(); + var obj2 = new object(); + + _hierarchy.Add(obj1); + _hierarchy.Add(obj2); + _hierarchy.ProcessChanges(); + + Assert.That(_hierarchy.IsInHierarchy(obj1, obj2), Is.False); + } + + [Test] + public void MultipleChildrenHandling() + { + var parent = new object(); + var child1 = new object(); + var child2 = new object(); + + _hierarchy.Add(parent); + _hierarchy.Add(child1); + _hierarchy.Add(child2); + + _hierarchy.AddChild(parent, child1); + _hierarchy.AddChild(parent, child2); + _hierarchy.ProcessChanges(); + + Assert.Multiple(() => + { + Assert.That(_hierarchy.GetChildren(parent).Count(), Is.EqualTo(2)); + Assert.That(_hierarchy.GetParent(child1), Is.EqualTo(parent)); + Assert.That(_hierarchy.GetParent(child2), Is.EqualTo(parent)); + }); + } + + [Test] + public void GetChildren_EmptyCase() + { + var obj = new object(); + _hierarchy.Add(obj); + _hierarchy.ProcessChanges(); + + Assert.That(_hierarchy.GetChildren(obj).Count(), Is.EqualTo(0)); + } + + [Test] + public void AddDuplicateObjectThrows() + { + var obj = new object(); + + _hierarchy.Add(obj); + _hierarchy.Add(obj); + + Assert.Throws(() => _hierarchy.ProcessChanges()); + } + + [Test] + public void AddChild_ParentNotInHierarchyThrows() + { + var parent = new object(); + var child = new object(); + + _hierarchy.Add(child); + _hierarchy.AddChild(parent, child); + + Assert.Throws(() => _hierarchy.ProcessChanges()); + } + + [Test] + public void GetParent_ObjectNotInHierarchyThrows() + { + var obj = new object(); + + Assert.That(_hierarchy.Contains(obj), Is.False); + } + + [Test] + public void NestedHierarchyTest() + { + var grandParent = new object(); + var parent = new object(); + var child = new object(); + + _hierarchy.Add(grandParent); + _hierarchy.Add(parent); + _hierarchy.Add(child); + + _hierarchy.AddChild(grandParent, parent); + _hierarchy.AddChild(parent, child); + _hierarchy.ProcessChanges(); + + Assert.Multiple(() => + { + Assert.That(_hierarchy.GetParent(parent), Is.EqualTo(grandParent)); + Assert.That(_hierarchy.GetParent(child), Is.EqualTo(parent)); + Assert.That(_hierarchy.IsInHierarchy(grandParent, child), Is.True); + }); + } + + [Test] + public void ReparentObjectTest() + { + var parent1 = new object(); + var parent2 = new object(); + var child = new object(); + + _hierarchy.Add(parent1); + _hierarchy.Add(parent2); + _hierarchy.Add(child); + + _hierarchy.AddChild(parent1, child); + _hierarchy.ProcessChanges(); + + // Reparent the child to parent2 + _hierarchy.AddChild(parent2, child); + _hierarchy.ProcessChanges(); + + Assert.Multiple(() => + { + Assert.That(_hierarchy.GetChildren(parent1).Count(), Is.EqualTo(0)); + Assert.That(_hierarchy.GetChildren(parent2).Count(), Is.EqualTo(1)); + Assert.That(_hierarchy.GetParent(child), Is.EqualTo(parent2)); + }); + } + + [Test] + public void RemoveRootObjectsTest() + { + var obj1 = new object(); + var obj2 = new object(); + + _hierarchy.Add(obj1); + _hierarchy.Add(obj2); + _hierarchy.ProcessChanges(); + + _hierarchy.Remove(obj1); + _hierarchy.ProcessChanges(); + + Assert.Multiple(() => + { + Assert.That(_hierarchy.GetChildren().Count(), Is.EqualTo(1)); + Assert.That(_hierarchy.Contains(obj1), Is.False); + }); + } + + [Test] + public void IsInHierarchy_SelfTest() + { + var obj = new object(); + _hierarchy.Add(obj); + _hierarchy.ProcessChanges(); + + Assert.That(_hierarchy.IsInHierarchy(obj, obj), Is.True); + } + + [Test] + public void ComplexHierarchyRemovalTest() + { + var grandParent = new object(); + var parent1 = new object(); + var parent2 = new object(); + var child1 = new object(); + var child2 = new object(); + + _hierarchy.Add(grandParent); + _hierarchy.Add(parent1); + _hierarchy.Add(parent2); + _hierarchy.Add(child1); + _hierarchy.Add(child2); + + _hierarchy.AddChild(grandParent, parent1); + _hierarchy.AddChild(grandParent, parent2); + _hierarchy.AddChild(parent1, child1); + _hierarchy.AddChild(parent2, child2); + _hierarchy.ProcessChanges(); + + _hierarchy.Remove(parent1); + _hierarchy.ProcessChanges(); + + Assert.Multiple(() => + { + Assert.That(_hierarchy.GetChildren(grandParent).Count(), Is.EqualTo(1)); + Assert.That(_hierarchy.Contains(child1), Is.False); + Assert.That(_hierarchy.Contains(parent1), Is.False); + }); + } + + [Test] + public void AddChild_SelfAsParentThrowsException() + { + var obj = new object(); + _hierarchy.Add(obj); + + Assert.Throws(() => _hierarchy.AddChild(obj, obj)); + } + + [Test] + public void MultiLevelReparentingTest() + { + var grandParent = new object(); + var parent = new object(); + var child1 = new object(); + var child2 = new object(); + + _hierarchy.Add(grandParent); + _hierarchy.Add(parent); + _hierarchy.Add(child1); + _hierarchy.Add(child2); + + // Initial hierarchy setup + _hierarchy.AddChild(grandParent, parent); + _hierarchy.AddChild(parent, child1); + _hierarchy.AddChild(parent, child2); + _hierarchy.ProcessChanges(); + + // Reparent child1 to child2 + _hierarchy.AddChild(child2, child1); + _hierarchy.ProcessChanges(); + + Assert.Multiple(() => + { + Assert.That(_hierarchy.GetChildren(parent).Count(), Is.EqualTo(1)); + Assert.That(_hierarchy.GetParent(child1), Is.EqualTo(child2)); + Assert.That(_hierarchy.IsInHierarchy(grandParent, child1), Is.True); + }); + } + + [Test] + public void DeepHierarchyTest() + { + const int hierarchyDepth = 100; + var objects = new object[hierarchyDepth]; + + // Create objects + for (var i = 0; i < hierarchyDepth; i++) + { + objects[i] = new object(); + _hierarchy.Add(objects[i]); } - [Test] - public void AddToRoot() + // Create a deep hierarchy + for (var i = 1; i < hierarchyDepth; i++) { - var obj = new object(); - _hierarchy.Add(obj); - _hierarchy.ProcessChanges(); - - Assert.Multiple(() => - { - Assert.That(_hierarchy.GetChildren().Count(), Is.EqualTo(1)); - Assert.That(_hierarchy.GetParent(obj), Is.Null); - }); + _hierarchy.AddChild(objects[i - 1], objects[i]); } - [Test] - public void AddToChild() + _hierarchy.ProcessChanges(); + + Assert.Multiple(() => { - var parent = new object(); - var child = new object(); + // Verify parent-child relationships + for (var i = 1; i < hierarchyDepth; i++) + { + Assert.That(_hierarchy.GetParent(objects[i]), Is.EqualTo(objects[i - 1])); + Assert.That(_hierarchy.IsInHierarchy(objects[0], objects[i]), Is.True); + } - _hierarchy.Add(parent); - _hierarchy.Add(child); + // Verify children + for (var i = 0; i < hierarchyDepth - 1; i++) + { + var children = _hierarchy.GetChildren(objects[i]).ToList(); + Assert.That(children, Has.Count.EqualTo(1)); + Assert.That(children[0], Is.EqualTo(objects[i + 1])); + } + }); + } - _hierarchy.AddChild(parent, child); - _hierarchy.ProcessChanges(); + [Test] + public void RemoveFromMiddleOfHierarchyTest() + { + var grandParent = new object(); + var parent = new object(); + var child1 = new object(); + var child2 = new object(); - Assert.Multiple(() => - { - Assert.That(_hierarchy.GetChildren(parent).Count(), Is.EqualTo(1)); - Assert.That(_hierarchy.GetParent(parent), Is.Null); - Assert.That(_hierarchy.GetParent(child), Is.EqualTo(parent)); - }); - } + _hierarchy.Add(grandParent); + _hierarchy.Add(parent); + _hierarchy.Add(child1); + _hierarchy.Add(child2); - [Test] - public void RemoveChild() + _hierarchy.AddChild(grandParent, parent); + _hierarchy.AddChild(parent, child1); + _hierarchy.AddChild(parent, child2); + _hierarchy.ProcessChanges(); + + // Remove parent, which will also remove its children + _hierarchy.Remove(parent); + _hierarchy.ProcessChanges(); + + Assert.Multiple(() => { - var parent = new object(); - var child = new object(); + Assert.That(_hierarchy.GetChildren(grandParent).Count(), Is.EqualTo(0)); + Assert.That(_hierarchy.Contains(child1), Is.False); + Assert.That(_hierarchy.Contains(child2), Is.False); + Assert.That(_hierarchy.Contains(parent), Is.False); + }); + } - _hierarchy.Add(parent); - _hierarchy.Add(child); + [Test] + public void ConcurrentModificationTest() + { + var parent = new object(); + var children = Enumerable.Range(0, 10) + .Select(_ => new object()) + .ToList(); - _hierarchy.AddChild(parent, child); - _hierarchy.ProcessChanges(); + _hierarchy.Add(parent); + children.ForEach(child => _hierarchy.Add(child)); - _hierarchy.Remove(child); - _hierarchy.ProcessChanges(); + // Simulate concurrent child additions + children.AsParallel().ForAll(child => { _hierarchy.AddChild(parent, child); }); - Assert.Multiple(() => - { - Assert.That(_hierarchy.GetChildren(parent).Count(), Is.EqualTo(0)); - Assert.That(_hierarchy.Contains(child), Is.False); - }); - } + _hierarchy.ProcessChanges(); - [Test] - public void RemoveParentAndAllChildren() + Assert.That(_hierarchy.GetChildren(parent).Count(), Is.EqualTo(children.Count)); + } + + [Test] + public void IsInHierarchy_NullInputs() + { + Assert.Multiple(() => { - var parent = new object(); - var child1 = new object(); - var child2 = new object(); + // Null child + Assert.That(_hierarchy.IsInHierarchy(new object(), null), Is.False); - _hierarchy.Add(parent); - _hierarchy.Add(child1); - _hierarchy.Add(child2); + // Null ancestor + Assert.That(_hierarchy.IsInHierarchy(null, new object()), Is.False); - _hierarchy.AddChild(parent, child1); - _hierarchy.AddChild(parent, child2); - _hierarchy.ProcessChanges(); + // Both null + Assert.That(_hierarchy.IsInHierarchy(null, null), Is.False); + }); + } - _hierarchy.Remove(parent); - _hierarchy.ProcessChanges(); + [Test] + public void IsInHierarchy_SameObject() + { + var obj = new object(); + _hierarchy.Add(obj); + _hierarchy.ProcessChanges(); - Assert.Multiple(() => - { - Assert.That(_hierarchy.GetChildren().Count(), Is.EqualTo(0)); - Assert.That(_hierarchy.Contains(child1), Is.False); - Assert.That(_hierarchy.Contains(child2), Is.False); - Assert.That(_hierarchy.Contains(parent), Is.False); - }); - } + Assert.That(_hierarchy.IsInHierarchy(obj, obj), Is.True); + } - [Test] - public void DetectCycleInHierarchy() + [Test] + public void IsInHierarchy_DirectParentChild() + { + var parent = new object(); + var child = new object(); + + _hierarchy.Add(parent); + _hierarchy.Add(child); + _hierarchy.AddChild(parent, child); + _hierarchy.ProcessChanges(); + + Assert.Multiple(() => { - var parent = new object(); - var child = new object(); + Assert.That(_hierarchy.IsInHierarchy(parent, child), Is.True); + Assert.That(_hierarchy.IsInHierarchy(child, parent), Is.False); + }); + } - _hierarchy.Add(parent); - _hierarchy.AddChild(parent, child); - _hierarchy.AddChild(child, parent); + [Test] + public void IsInHierarchy_DeepHierarchy() + { + var grandParent = new object(); + var parent = new object(); + var child = new object(); + var grandChild = new object(); - Assert.Throws(() => _hierarchy.ProcessChanges()); - } + _hierarchy.Add(grandParent); + _hierarchy.Add(parent); + _hierarchy.Add(child); + _hierarchy.Add(grandChild); - [Test] - public void IsInHierarchy_PositiveCase() + _hierarchy.AddChild(grandParent, parent); + _hierarchy.AddChild(parent, child); + _hierarchy.AddChild(child, grandChild); + _hierarchy.ProcessChanges(); + + Assert.Multiple(() => { - var ancestor = new object(); - var child = new object(); + // Verify deep hierarchy relationships + Assert.That(_hierarchy.IsInHierarchy(grandParent, grandChild), Is.True); + Assert.That(_hierarchy.IsInHierarchy(parent, grandChild), Is.True); + Assert.That(_hierarchy.IsInHierarchy(child, grandChild), Is.True); - _hierarchy.Add(ancestor); - _hierarchy.Add(child); + // Verify inverse relationships are false + Assert.That(_hierarchy.IsInHierarchy(grandChild, grandParent), Is.False); + Assert.That(_hierarchy.IsInHierarchy(grandChild, parent), Is.False); + Assert.That(_hierarchy.IsInHierarchy(grandChild, child), Is.False); + }); + } - _hierarchy.AddChild(ancestor, child); - _hierarchy.ProcessChanges(); + [Test] + public void IsInHierarchy_SeparateBranches() + { + var root = new object(); + var branch1Parent = new object(); + var branch1Child = new object(); + var branch2Parent = new object(); + var branch2Child = new object(); - Assert.That(_hierarchy.IsInHierarchy(ancestor, child), Is.True); - } + _hierarchy.Add(root); + _hierarchy.Add(branch1Parent); + _hierarchy.Add(branch1Child); + _hierarchy.Add(branch2Parent); + _hierarchy.Add(branch2Child); - [Test] - public void IsInHierarchy_NegativeCase() + _hierarchy.AddChild(root, branch1Parent); + _hierarchy.AddChild(root, branch2Parent); + _hierarchy.AddChild(branch1Parent, branch1Child); + _hierarchy.AddChild(branch2Parent, branch2Child); + _hierarchy.ProcessChanges(); + + Assert.Multiple(() => { - var obj1 = new object(); - var obj2 = new object(); - - _hierarchy.Add(obj1); - _hierarchy.Add(obj2); - _hierarchy.ProcessChanges(); - - Assert.That(_hierarchy.IsInHierarchy(obj1, obj2), Is.False); - } - - [Test] - public void MultipleChildrenHandling() - { - var parent = new object(); - var child1 = new object(); - var child2 = new object(); - - _hierarchy.Add(parent); - _hierarchy.Add(child1); - _hierarchy.Add(child2); - - _hierarchy.AddChild(parent, child1); - _hierarchy.AddChild(parent, child2); - _hierarchy.ProcessChanges(); - - Assert.Multiple(() => - { - Assert.That(_hierarchy.GetChildren(parent).Count(), Is.EqualTo(2)); - Assert.That(_hierarchy.GetParent(child1), Is.EqualTo(parent)); - Assert.That(_hierarchy.GetParent(child2), Is.EqualTo(parent)); - }); - } - - [Test] - public void GetChildren_EmptyCase() - { - var obj = new object(); - _hierarchy.Add(obj); - _hierarchy.ProcessChanges(); - - Assert.That(_hierarchy.GetChildren(obj).Count(), Is.EqualTo(0)); - } - - [Test] - public void AddDuplicateObjectThrows() - { - var obj = new object(); - - _hierarchy.Add(obj); - _hierarchy.Add(obj); - - Assert.Throws(() => _hierarchy.ProcessChanges()); - } - - [Test] - public void AddChild_ParentNotInHierarchyThrows() - { - var parent = new object(); - var child = new object(); - - _hierarchy.Add(child); - _hierarchy.AddChild(parent, child); - - Assert.Throws(() => _hierarchy.ProcessChanges()); - } - - [Test] - public void GetParent_ObjectNotInHierarchyThrows() - { - var obj = new object(); - - Assert.That(_hierarchy.Contains(obj), Is.False); - } - - [Test] - public void NestedHierarchyTest() - { - var grandParent = new object(); - var parent = new object(); - var child = new object(); - - _hierarchy.Add(grandParent); - _hierarchy.Add(parent); - _hierarchy.Add(child); - - _hierarchy.AddChild(grandParent, parent); - _hierarchy.AddChild(parent, child); - _hierarchy.ProcessChanges(); - - Assert.Multiple(() => - { - Assert.That(_hierarchy.GetParent(parent), Is.EqualTo(grandParent)); - Assert.That(_hierarchy.GetParent(child), Is.EqualTo(parent)); - Assert.That(_hierarchy.IsInHierarchy(grandParent, child), Is.True); - }); - } - - [Test] - public void ReparentObjectTest() - { - var parent1 = new object(); - var parent2 = new object(); - var child = new object(); - - _hierarchy.Add(parent1); - _hierarchy.Add(parent2); - _hierarchy.Add(child); - - _hierarchy.AddChild(parent1, child); - _hierarchy.ProcessChanges(); - - // Reparent the child to parent2 - _hierarchy.AddChild(parent2, child); - _hierarchy.ProcessChanges(); - - Assert.Multiple(() => - { - Assert.That(_hierarchy.GetChildren(parent1).Count(), Is.EqualTo(0)); - Assert.That(_hierarchy.GetChildren(parent2).Count(), Is.EqualTo(1)); - Assert.That(_hierarchy.GetParent(child), Is.EqualTo(parent2)); - }); - } - - [Test] - public void RemoveRootObjectsTest() - { - var obj1 = new object(); - var obj2 = new object(); - - _hierarchy.Add(obj1); - _hierarchy.Add(obj2); - _hierarchy.ProcessChanges(); - - _hierarchy.Remove(obj1); - _hierarchy.ProcessChanges(); - - Assert.Multiple(() => - { - Assert.That(_hierarchy.GetChildren().Count(), Is.EqualTo(1)); - Assert.That(_hierarchy.Contains(obj1), Is.False); - }); - } - - [Test] - public void IsInHierarchy_SelfTest() - { - var obj = new object(); - _hierarchy.Add(obj); - _hierarchy.ProcessChanges(); - - Assert.That(_hierarchy.IsInHierarchy(obj, obj), Is.True); - } - - [Test] - public void ComplexHierarchyRemovalTest() - { - var grandParent = new object(); - var parent1 = new object(); - var parent2 = new object(); - var child1 = new object(); - var child2 = new object(); - - _hierarchy.Add(grandParent); - _hierarchy.Add(parent1); - _hierarchy.Add(parent2); - _hierarchy.Add(child1); - _hierarchy.Add(child2); - - _hierarchy.AddChild(grandParent, parent1); - _hierarchy.AddChild(grandParent, parent2); - _hierarchy.AddChild(parent1, child1); - _hierarchy.AddChild(parent2, child2); - _hierarchy.ProcessChanges(); - - _hierarchy.Remove(parent1); - _hierarchy.ProcessChanges(); - - Assert.Multiple(() => - { - Assert.That(_hierarchy.GetChildren(grandParent).Count(), Is.EqualTo(1)); - Assert.That(_hierarchy.Contains(child1), Is.False); - Assert.That(_hierarchy.Contains(parent1), Is.False); - }); - } - - [Test] - public void AddChild_SelfAsParentThrowsException() - { - var obj = new object(); - _hierarchy.Add(obj); - - Assert.Throws(() => _hierarchy.AddChild(obj, obj)); - } - - [Test] - public void MultiLevelReparentingTest() - { - var grandParent = new object(); - var parent = new object(); - var child1 = new object(); - var child2 = new object(); - - _hierarchy.Add(grandParent); - _hierarchy.Add(parent); - _hierarchy.Add(child1); - _hierarchy.Add(child2); - - // Initial hierarchy setup - _hierarchy.AddChild(grandParent, parent); - _hierarchy.AddChild(parent, child1); - _hierarchy.AddChild(parent, child2); - _hierarchy.ProcessChanges(); - - // Reparent child1 to child2 - _hierarchy.AddChild(child2, child1); - _hierarchy.ProcessChanges(); - - Assert.Multiple(() => - { - Assert.That(_hierarchy.GetChildren(parent).Count(), Is.EqualTo(1)); - Assert.That(_hierarchy.GetParent(child1), Is.EqualTo(child2)); - Assert.That(_hierarchy.IsInHierarchy(grandParent, child1), Is.True); - }); - } - - [Test] - public void DeepHierarchyTest() - { - const int hierarchyDepth = 100; - var objects = new object[hierarchyDepth]; - - // Create objects - for (var i = 0; i < hierarchyDepth; i++) - { - objects[i] = new object(); - _hierarchy.Add(objects[i]); - } - - // Create a deep hierarchy - for (var i = 1; i < hierarchyDepth; i++) - { - _hierarchy.AddChild(objects[i - 1], objects[i]); - } - - _hierarchy.ProcessChanges(); - - Assert.Multiple(() => - { - // Verify parent-child relationships - for (var i = 1; i < hierarchyDepth; i++) - { - Assert.That(_hierarchy.GetParent(objects[i]), Is.EqualTo(objects[i - 1])); - Assert.That(_hierarchy.IsInHierarchy(objects[0], objects[i]), Is.True); - } - - // Verify children - for (var i = 0; i < hierarchyDepth - 1; i++) - { - var children = _hierarchy.GetChildren(objects[i]).ToList(); - Assert.That(children, Has.Count.EqualTo(1)); - Assert.That(children[0], Is.EqualTo(objects[i + 1])); - } - }); - } - - [Test] - public void RemoveFromMiddleOfHierarchyTest() - { - var grandParent = new object(); - var parent = new object(); - var child1 = new object(); - var child2 = new object(); - - _hierarchy.Add(grandParent); - _hierarchy.Add(parent); - _hierarchy.Add(child1); - _hierarchy.Add(child2); - - _hierarchy.AddChild(grandParent, parent); - _hierarchy.AddChild(parent, child1); - _hierarchy.AddChild(parent, child2); - _hierarchy.ProcessChanges(); - - // Remove parent, which will also remove its children - _hierarchy.Remove(parent); - _hierarchy.ProcessChanges(); - - Assert.Multiple(() => - { - Assert.That(_hierarchy.GetChildren(grandParent).Count(), Is.EqualTo(0)); - Assert.That(_hierarchy.Contains(child1), Is.False); - Assert.That(_hierarchy.Contains(child2), Is.False); - Assert.That(_hierarchy.Contains(parent), Is.False); - }); - } - - [Test] - public void ConcurrentModificationTest() - { - var parent = new object(); - var children = Enumerable.Range(0, 10) - .Select(_ => new object()) - .ToList(); - - _hierarchy.Add(parent); - children.ForEach(child => _hierarchy.Add(child)); - - // Simulate concurrent child additions - children.AsParallel().ForAll(child => { _hierarchy.AddChild(parent, child); }); - - _hierarchy.ProcessChanges(); - - Assert.That(_hierarchy.GetChildren(parent).Count(), Is.EqualTo(children.Count)); - } - - [Test] - public void IsInHierarchy_NullInputs() - { - Assert.Multiple(() => - { - // Null child - Assert.That(_hierarchy.IsInHierarchy(new object(), null), Is.False); - - // Null ancestor - Assert.That(_hierarchy.IsInHierarchy(null, new object()), Is.False); - - // Both null - Assert.That(_hierarchy.IsInHierarchy(null, null), Is.False); - }); - } - - [Test] - public void IsInHierarchy_SameObject() - { - var obj = new object(); - _hierarchy.Add(obj); - _hierarchy.ProcessChanges(); - - Assert.That(_hierarchy.IsInHierarchy(obj, obj), Is.True); - } - - [Test] - public void IsInHierarchy_DirectParentChild() - { - var parent = new object(); - var child = new object(); - - _hierarchy.Add(parent); - _hierarchy.Add(child); - _hierarchy.AddChild(parent, child); - _hierarchy.ProcessChanges(); - - Assert.Multiple(() => - { - Assert.That(_hierarchy.IsInHierarchy(parent, child), Is.True); - Assert.That(_hierarchy.IsInHierarchy(child, parent), Is.False); - }); - } - - [Test] - public void IsInHierarchy_DeepHierarchy() - { - var grandParent = new object(); - var parent = new object(); - var child = new object(); - var grandChild = new object(); - - _hierarchy.Add(grandParent); - _hierarchy.Add(parent); - _hierarchy.Add(child); - _hierarchy.Add(grandChild); - - _hierarchy.AddChild(grandParent, parent); - _hierarchy.AddChild(parent, child); - _hierarchy.AddChild(child, grandChild); - _hierarchy.ProcessChanges(); - - Assert.Multiple(() => - { - // Verify deep hierarchy relationships - Assert.That(_hierarchy.IsInHierarchy(grandParent, grandChild), Is.True); - Assert.That(_hierarchy.IsInHierarchy(parent, grandChild), Is.True); - Assert.That(_hierarchy.IsInHierarchy(child, grandChild), Is.True); - - // Verify inverse relationships are false - Assert.That(_hierarchy.IsInHierarchy(grandChild, grandParent), Is.False); - Assert.That(_hierarchy.IsInHierarchy(grandChild, parent), Is.False); - Assert.That(_hierarchy.IsInHierarchy(grandChild, child), Is.False); - }); - } - - [Test] - public void IsInHierarchy_SeparateBranches() - { - var root = new object(); - var branch1Parent = new object(); - var branch1Child = new object(); - var branch2Parent = new object(); - var branch2Child = new object(); - - _hierarchy.Add(root); - _hierarchy.Add(branch1Parent); - _hierarchy.Add(branch1Child); - _hierarchy.Add(branch2Parent); - _hierarchy.Add(branch2Child); - - _hierarchy.AddChild(root, branch1Parent); - _hierarchy.AddChild(root, branch2Parent); - _hierarchy.AddChild(branch1Parent, branch1Child); - _hierarchy.AddChild(branch2Parent, branch2Child); - _hierarchy.ProcessChanges(); - - Assert.Multiple(() => - { - // Verify within same branch - Assert.That(_hierarchy.IsInHierarchy(root, branch1Parent), Is.True); - Assert.That(_hierarchy.IsInHierarchy(root, branch1Child), Is.True); - Assert.That(_hierarchy.IsInHierarchy(branch1Parent, branch1Child), Is.True); - - // Verify between different branches - Assert.That(_hierarchy.IsInHierarchy(branch1Parent, branch2Child), Is.False); - Assert.That(_hierarchy.IsInHierarchy(branch2Parent, branch1Child), Is.False); - }); - } - - [Test] - public void IsInHierarchy_ObjectsNotInHierarchy() - { - var obj1 = new object(); - var obj2 = new object(); - - _hierarchy.Add(obj1); - _hierarchy.Add(obj2); - _hierarchy.ProcessChanges(); - - Assert.That(_hierarchy.IsInHierarchy(obj1, obj2), Is.False); - } + // Verify within same branch + Assert.That(_hierarchy.IsInHierarchy(root, branch1Parent), Is.True); + Assert.That(_hierarchy.IsInHierarchy(root, branch1Child), Is.True); + Assert.That(_hierarchy.IsInHierarchy(branch1Parent, branch1Child), Is.True); + + // Verify between different branches + Assert.That(_hierarchy.IsInHierarchy(branch1Parent, branch2Child), Is.False); + Assert.That(_hierarchy.IsInHierarchy(branch2Parent, branch1Child), Is.False); + }); + } + + [Test] + public void IsInHierarchy_ObjectsNotInHierarchy() + { + var obj1 = new object(); + var obj2 = new object(); + + _hierarchy.Add(obj1); + _hierarchy.Add(obj2); + _hierarchy.ProcessChanges(); + + Assert.That(_hierarchy.IsInHierarchy(obj1, obj2), Is.False); + } } \ No newline at end of file diff --git a/PresenterConsole/PresenterConsole.csproj b/PresenterConsole/PresenterConsole.csproj index 426a52e..be125a4 100644 --- a/PresenterConsole/PresenterConsole.csproj +++ b/PresenterConsole/PresenterConsole.csproj @@ -5,6 +5,7 @@ net8.0 enable enable + true diff --git a/PresenterConsole/assets/shader/ascii.shader b/PresenterConsole/assets/shader/ascii.shader index d0ba834..2bce69c 100644 --- a/PresenterConsole/assets/shader/ascii.shader +++ b/PresenterConsole/assets/shader/ascii.shader @@ -17,68 +17,106 @@ void main() precision highp float; -// All components are in the range [0…1], including hue. -vec3 rgb2hsv(vec3 c) -{ - vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); - vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g)); - vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r)); - - float d = q.x - min(q.w, q.y); - float e = 1.0e-10; - return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); -} - -// 16 Windows color palette (approximated) -const vec3 windowsPalette[7] = vec3[]( -vec3(rgb2hsv(vec3(0.0, 0.0, 1.0)).rg, 0.0), // Blue (FOREGROUND_BLUE) -vec3(rgb2hsv(vec3(0.0, 1.0, 0.0)).rg, 0.0), // Green (FOREGROUND_GREEN) -vec3(rgb2hsv(vec3(0.0, 1.0, 1.0)).rg, 0.0), // Cyan (FOREGROUND_GREEN | FOREGROUND_BLUE) -vec3(rgb2hsv(vec3(1.0, 0.0, 0.0)).rg, 0.0), // Red (FOREGROUND_RED) -vec3(rgb2hsv(vec3(1.0, 0.0, 1.0)).rg, 0.0), // Magenta (FOREGROUND_RED | FOREGROUND_BLUE) -vec3(rgb2hsv(vec3(1.0, 1.0, 0.0)).rg, 0.0), // Yellow (FOREGROUND_RED | FOREGROUND_GREEN) -vec3(rgb2hsv(vec3(1.0, 1.0, 1.0)).rg, 0.0) // White (all colors set) +// ConsoleColor vec3 palette (normalized) +const vec3 ConsoleColorVec3[16] = vec3[]( + vec3(0.0, 0.0, 0.0), // Black + vec3(0.0, 0.0, 0.5), // DarkBlue + vec3(0.0, 0.5, 0.0), // DarkGreen + vec3(0.0, 0.5, 0.5), // DarkCyan + vec3(0.5, 0.0, 0.0), // DarkRed + vec3(0.5, 0.0, 0.5), // DarkMagenta + vec3(0.5, 0.5, 0.0), // DarkYellow + vec3(0.5, 0.5, 0.5), // Gray + vec3(0.25, 0.25, 0.25), // DarkGray + vec3(0.0, 0.0, 1.0), // Blue + vec3(0.0, 1.0, 0.0), // Green + vec3(0.0, 1.0, 1.0), // Cyan + vec3(1.0, 0.0, 0.0), // Red + vec3(1.0, 0.0, 1.0), // Magenta + vec3(1.0, 1.0, 0.0), // Yellow + vec3(1.0, 1.0, 1.0) // White ); -// Find the closest color in the Windows palette -int findClosestColor(vec3 color) { - int closestIndex = 0; - float minDistance = distance(color, windowsPalette[0]); +vec3 f(vec3 x) { + const float epsilon = 0.008856; + const float k = 903.3; + vec3 fx; + fx.x = x.x > epsilon ? pow(x.x, 1.0 / 3.0) : (k * x.x + 16.0) / 116.0; + fx.y = x.y > epsilon ? pow(x.y, 1.0 / 3.0) : (k * x.y + 16.0) / 116.0; + fx.z = x.z > epsilon ? pow(x.z, 1.0 / 3.0) : (k * x.z + 16.0) / 116.0; + return fx; +} - for (int i = 1; i < 7; i++) { - float dist = distance(color, windowsPalette[i]); - if (dist < minDistance) { - minDistance = dist; - closestIndex = i; +// Perceptually weighted color difference (CIEDE2000-inspired) +float perceptualColorDistance(vec3 color1, vec3 color2) { + // RGB to LAB conversion (simplified approximation) + + vec3 lab1 = f(color1); + vec3 lab2 = f(color2); + + // Compute LAB-like color difference with perceptual weighting + float deltaL = lab1.x - lab2.x; + float deltaA = lab1.y - lab2.y; + float deltaB = lab1.z - lab2.z; + + // Perceptual weighting + return deltaL * deltaL * 1.0 + // Lightness difference + deltaA * deltaA * 1.5 + // Green-Red difference + deltaB * deltaB * 1.5; // Blue-Yellow difference +} + +// Advanced color matching considering multiple color attributes +int findMostPerceptuallyAccurateColor(vec3 color) { + int bestMatchIndex = 0; + float minDistance = perceptualColorDistance(color, ConsoleColorVec3[0]); + + for (int i = 1; i < 16; i++) { + float currentDistance = perceptualColorDistance(color, ConsoleColorVec3[i]); + if (currentDistance < minDistance) { + minDistance = currentDistance; + bestMatchIndex = i; } } - return closestIndex + 1; + return bestMatchIndex; } -float calculateLuminosity(vec3 color) { - // Standard luminosity calculation - float luminosity = dot(color, vec3(0.299, 0.587, 0.114)); - return clamp(luminosity, 0.0, 1.0); +// Enhanced luminosity calculation considering human perception +float calculatePerceptualLuminance(vec3 color) { + // BT.709 luminance coefficients with slight adjustment + return pow( + 0.2126 * pow(color.r, 2.2) + + 0.7152 * pow(color.g, 2.2) + + 0.0722 * pow(color.b, 2.2), + 1.0 / 2.2 + ); +} + +// Dithering function to reduce color banding +float interleavedGradientNoise(vec2 pixel) { + return fract(52.982919 * fract(0.06711056 * pixel.x + 0.00583715 * pixel.y)); } uniform sampler2D uInputTexture; +uniform vec2 uResolution; layout (location = 0) in vec2 iUV; layout (location = 0) out vec4 FragColor; void main() { vec3 pixelColor = texture(uInputTexture, iUV).rgb; - vec3 pixelColorHsv = rgb2hsv(pixelColor); - // Find closest color index (4 bits) - int colorIndex = findClosestColor(vec3(pixelColorHsv.rg, 0)); + // Find most perceptually accurate console color + int colorIndex = findMostPerceptuallyAccurateColor(pixelColor); - // Calculate luminosity (4 bits) - float luminosity = calculateLuminosity(pixelColor); + // Calculate perceptual luminance with gamma correction + float luminance = calculatePerceptualLuminance(pixelColor); - // Combine into a single byte-like value - // High 4 bits: color index - // Low 4 bits: luminosity - FragColor = vec4(luminosity, colorIndex / 255.0, 0, 1); + // Output with high precision color mapping + FragColor = vec4( + luminance, // Red: Perceptual luminance + float(colorIndex) / 15.0, // Green: Normalized color index + 0.0, // Blue: Unused + 1.0 // Alpha + ); } \ No newline at end of file diff --git a/PresenterConsole/src/AsciiPixel.cs b/PresenterConsole/src/AsciiPixel.cs index 09a058a..378cd68 100644 --- a/PresenterConsole/src/AsciiPixel.cs +++ b/PresenterConsole/src/AsciiPixel.cs @@ -11,8 +11,8 @@ public struct AsciiPixel : IPixel public PixelInternalFormat InternalFormat => PixelInternalFormat.Rg8; public SizedInternalFormat SizedInternalFormat => SizedInternalFormat.Rg8; - public byte LightnessIndex => R; - public byte ColorIndex => G; + public byte Luminance => R; + public byte Color => G; public byte R; public byte G; diff --git a/PresenterConsole/src/ConsoleFastOutput.cs b/PresenterConsole/src/ConsoleFastOutput.cs index ca340d0..c2d2b9f 100644 --- a/PresenterConsole/src/ConsoleFastOutput.cs +++ b/PresenterConsole/src/ConsoleFastOutput.cs @@ -1,158 +1,71 @@ -using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; using Microsoft.Win32.SafeHandles; namespace PresenterConsole; -public class ConsoleFastOutput +public sealed class ConsoleFastOutput : IDisposable { - [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] - static extern SafeFileHandle CreateFile( - string fileName, - [MarshalAs(UnmanagedType.U4)] uint fileAccess, - [MarshalAs(UnmanagedType.U4)] uint fileShare, - IntPtr securityAttributes, - [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition, - [MarshalAs(UnmanagedType.U4)] int flags, - IntPtr template); - - [DllImport("kernel32.dll", SetLastError = true)] - static extern bool WriteConsoleOutput( - SafeFileHandle hConsoleOutput, - CharInfo[] lpBuffer, - Coord dwBufferSize, - Coord dwBufferCoord, - ref SmallRect lpWriteRegion); - - [StructLayout(LayoutKind.Sequential)] - public struct Coord - { - public short X; - public short Y; - - public Coord(short X, short Y) - { - this.X = X; - this.Y = Y; - } - }; - - [StructLayout(LayoutKind.Explicit)] - public struct CharUnion - { - [FieldOffset(0)] public char UnicodeChar; - [FieldOffset(0)] public byte AsciiChar; - } - - [StructLayout(LayoutKind.Explicit)] - public struct CharInfo - { - [FieldOffset(0)] public CharUnion Char; - [FieldOffset(2)] public short Attributes; - } - - [StructLayout(LayoutKind.Sequential)] - public struct SmallRect - { - public short Left; - public short Top; - public short Right; - public short Bottom; - } + private const uint GENERIC_WRITE = 0x40000000; + private const int FILE_SHARE_WRITE = 0x2; private readonly SafeFileHandle _handle; - private CharInfo[] _buffer; - private SmallRect _region; + private readonly WindowsFFI.Coord _bufferCoord = new(0, 0); + private int _width; private int _height; + private WindowsFFI.CharInfo[] _buffer; + private WindowsFFI.Coord _bufferSize; + public ConsoleFastOutput(int parWidth, int parHeight) { - _width = parWidth; - _height = parHeight; - _handle = CreateFile("CONOUT$", 0x40000000, 2, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero); - if (_handle.IsInvalid) - { - throw new Exception("Failed to open console"); - } + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(parWidth); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(parHeight); - _buffer = new CharInfo[parWidth * parHeight]; - _region = new SmallRect { Left = 0, Top = 0, Right = (short)_width, Bottom = (short)_height }; + _handle = WindowsFFI.CreateFile("CONOUT$", GENERIC_WRITE, FILE_SHARE_WRITE, + IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero); + + if (_handle.IsInvalid) + throw new InvalidOperationException("Failed to open console handle"); + + Resize(parWidth, parHeight); } - public void WriteChar(char parChar, short parX, short parY, ushort parForeground, ushort parBackground) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteChar(char parCharacter, int parX, int parY, ConsoleColor parForeground, ConsoleColor parBackground) { - var index = parX + parY * _width; - if (index >= _buffer.Length) + if (parX < 0 || parX >= _width || parY < 0 || parY >= _height) return; - _buffer[parX + parY * _width].Char.UnicodeChar = parChar; - _buffer[parX + parY * _width].Attributes = (short)(parForeground | (parBackground << 4)); + var index = parX + parY * _width; + ref var charInfo = ref _buffer[index]; + charInfo.Char.UnicodeChar = parCharacter; + charInfo.Attributes = (short)((int)parForeground | ((int)parBackground << 4)); } public void Flush() { - var regions = SplitRegion(_region, 1, 1); - for (var i = 0; i < regions.Length; i++) - { - var region = regions[i]; - var bufferSize = new Coord(region.Right, region.Bottom); - var bufferCoord = new Coord(region.Left, region.Top); - WriteConsoleOutput(_handle, _buffer, bufferSize, bufferCoord, ref region); - } + var writeRegion = new WindowsFFI.SmallRect { Left = 0, Top = 0, Right = (short)_width, Bottom = (short)_height }; - // var bufferSize = new Coord(_region.Right, _region.Bottom); - // var bufferCoord = new Coord(0, 0); - // WriteConsoleOutput(_handle, _buffer, bufferSize, bufferCoord, ref _region); - // - // _region.Left = 0; - // _region.Top = 0; - // _region.Right = (short)_width; - // _region.Bottom = (short)_height; - - // run in parallel - // Parallel.ForEach(regions, region => - // { - // var bufferSize = new Coord(_region.Right, _region.Bottom); - // var bufferCoord = new Coord(region.Left, region.Top); - // WriteConsoleOutput(_handle, _buffer, bufferSize, bufferCoord, ref region); - // }); - } - - private SmallRect[] SplitRegion(SmallRect parRegion, int countX, int countY) - { - var regions = new SmallRect[countX * countY]; - for (var y = 0; y < countY; y++) - { - for (var x = 0; x < countX; x++) - { - var region = parRegion; - region.Left = (short)(parRegion.Left + x * parRegion.Right / countX); - region.Right = (short)(parRegion.Left + (x + 1) * parRegion.Right / countX); - region.Top = (short)(parRegion.Top + y * parRegion.Bottom / countY); - region.Bottom = (short)(parRegion.Top + (y + 1) * parRegion.Bottom / countY); - regions[y * countX + x] = region; - } - } - - return regions; + WindowsFFI.WriteConsoleOutput(_handle, _buffer, _bufferSize, _bufferCoord, ref writeRegion); } public void Resize(int parWidth, int parHeight) { - if (parWidth <= 0 || parHeight <= 0) - { - throw new ArgumentException("Width and height must be greater than 0"); - } + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(parWidth); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(parHeight); if (parWidth == _width && parHeight == _height) - { return; - } _width = parWidth; _height = parHeight; + _buffer = new WindowsFFI.CharInfo[parWidth * parHeight]; + _bufferSize = new WindowsFFI.Coord((short)_width, (short)_height); + } - _buffer = new CharInfo[parWidth * parHeight]; - _region = new SmallRect { Left = 0, Top = 0, Right = (short)parWidth, Bottom = (short)parHeight }; + public void Dispose() + { + _handle.Dispose(); } } \ No newline at end of file diff --git a/PresenterConsole/src/ConsolePresenter.cs b/PresenterConsole/src/ConsolePresenter.cs index 2ff51a7..56e9965 100644 --- a/PresenterConsole/src/ConsolePresenter.cs +++ b/PresenterConsole/src/ConsolePresenter.cs @@ -31,6 +31,7 @@ public class ConsolePresenter : IPresenter private readonly VertexArray _vertexArray; private readonly ConsoleFastOutput _consoleOutput; + private static readonly char[] LIGHTMAP = " .:-+=#%@".Reverse().ToArray(); public ConsolePresenter(Engine.Engine parEngine) { @@ -41,17 +42,18 @@ public class ConsolePresenter : IPresenter .AddColorAttachment() .Build(); - _indexBuffer = new IndexBuffer([0, 1, 2, 2, 3, 0], BufferStorageFlags.MapReadBit); + _indexBuffer = new IndexBuffer([0, 1, 2, 2, 3, 0]); _vertexBuffer = new VertexBuffer([ new AsciiVertex { _position = new Vector2(-1.0f, -1.0f), _uv = new Vector2(0.0f, 0.0f) }, new AsciiVertex { _position = new Vector2(1.0f, -1.0f), _uv = new Vector2(1.0f, 0.0f) }, new AsciiVertex { _position = new Vector2(1.0f, 1.0f), _uv = new Vector2(1.0f, 1.0f) }, new AsciiVertex { _position = new Vector2(-1.0f, 1.0f), _uv = new Vector2(0.0f, 1.0f) } - ], BufferStorageFlags.MapReadBit); + ]); _vertexArray = new VertexArray(); _vertexArray.BindIndexBuffer(_indexBuffer); _vertexArray.BindVertexBuffer(_vertexBuffer); + Console.CursorVisible = false; _consoleOutput = new ConsoleFastOutput(Width, Height); } @@ -87,15 +89,15 @@ public class ConsolePresenter : IPresenter private void Output(Image parImage) { - var lightmap = " .-:=+*#%@".Reverse().ToArray(); for (var y = 0; y < parImage.Height; y++) { for (var x = 0; x < parImage.Width; x++) { var pixel = parImage[y, x]; - var lightnessIndex = (int)(pixel.LightnessIndex / 255.0f * (lightmap.Length - 1)); - _consoleOutput.WriteChar(lightmap[lightnessIndex], (short)x, (short)y, 0, - pixel.ColorIndex); + var lightnessIndex = (int)(pixel.Luminance / 255.0f * (LIGHTMAP.Length - 1)); + var colorIndex = (int)(pixel.Color / 255.0f * 15.0f); + _consoleOutput.WriteChar(LIGHTMAP[lightnessIndex], (short)x, (short)y, 0, + (ConsoleColor)colorIndex); } } @@ -109,8 +111,9 @@ public class ConsolePresenter : IPresenter _cachedWidth = Width; _cachedHeight = Height; + Resize?.Invoke(new ResizeEventArgs(Width, Height)); + _framebuffer.Resize(Width, Height); - _engine.Renderer.Resize(Width, Height); _consoleOutput.Resize(Width, Height); } } diff --git a/PresenterConsole/src/Program.cs b/PresenterConsole/src/Program.cs index 8d99222..3ea092b 100644 --- a/PresenterConsole/src/Program.cs +++ b/PresenterConsole/src/Program.cs @@ -1,12 +1,20 @@ // See https://aka.ms/new-console-template for more information -using PresenterConsole; +using Serilog.Events; -internal class Program +namespace PresenterConsole; + +internal static class Program { - public static void Main(string[] args) + public static void Main(string[] parArgs) { - var engine = new Engine.Engine(240, 135, (parEngine) => new ConsolePresenter(parEngine)); + var engine = new Engine.EngineBuilder() + .Headless() + .LogToFile(true, "log.txt") + .LogLevel(LogEventLevel.Debug) + .Presenter(parEngine => new ConsolePresenter(parEngine)) + .Build(); + engine.Run(); } } \ No newline at end of file diff --git a/PresenterConsole/src/WindowsFFI.cs b/PresenterConsole/src/WindowsFFI.cs new file mode 100644 index 0000000..c43f868 --- /dev/null +++ b/PresenterConsole/src/WindowsFFI.cs @@ -0,0 +1,63 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; +using Microsoft.Win32.SafeHandles; + +[assembly: DisableRuntimeMarshalling] + +namespace PresenterConsole; + +public static partial class WindowsFFI +{ + [StructLayout(LayoutKind.Sequential)] + public struct Coord(short parX, short parY) + { + public short X = parX; + public short Y = parY; + } + + [StructLayout(LayoutKind.Explicit)] + public struct CharUnion + { + [FieldOffset(0)] public char UnicodeChar; + [FieldOffset(0)] public byte AsciiChar; + } + + [StructLayout(LayoutKind.Explicit)] + public struct CharInfo + { + [FieldOffset(0)] public CharUnion Char; + [FieldOffset(2)] public short Attributes; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SmallRect + { + public short Left; + public short Top; + public short Right; + public short Bottom; + } + + [LibraryImport("kernel32.dll", SetLastError = true, EntryPoint = "CreateFileW")] + public static partial SafeFileHandle CreateFile( + [MarshalAs(UnmanagedType.LPWStr)] string parFileName, + uint parFileAccess, + uint parFileShare, + IntPtr parSecurityAttributes, + FileMode parCreationDisposition, + int parFlags, + IntPtr parTemplate + ); + + [LibraryImport("kernel32.dll", SetLastError = true, EntryPoint = "WriteConsoleOutputW")] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool WriteConsoleOutput( + SafeFileHandle parHConsoleOutput, + [MarshalUsing(CountElementName = nameof(parDwBufferSize))] + CharInfo[] parLpBuffer, + Coord parDwBufferSize, + Coord parDwBufferCoord, + ref SmallRect parLpWriteRegion + ); +} \ No newline at end of file diff --git a/PresenterNative/PresenterNative.csproj b/PresenterNative/PresenterNative.csproj new file mode 100644 index 0000000..7e4bc5f --- /dev/null +++ b/PresenterNative/PresenterNative.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/PresenterNative/src/Program.cs b/PresenterNative/src/Program.cs new file mode 100644 index 0000000..84c59bc --- /dev/null +++ b/PresenterNative/src/Program.cs @@ -0,0 +1,23 @@ +using Engine; +using Serilog.Events; + +namespace PresenterNative; + +internal static class Program +{ + public static void Main(string[] parArgs) + { + var engine = new EngineBuilder() + .Headless(false) + .Width(1280) + .Height(720) + .Title("Doom Deathmatch") + .LogToConsole() + .LogToFile(true, "log.txt") + .LogLevel(LogEventLevel.Debug) + .Presenter(parEngine => parEngine.Window) + .Build(); + + engine.Run(); + } +} \ No newline at end of file diff --git a/PresenterWpf/src/App.xaml.cs b/PresenterWpf/src/App.xaml.cs index c8c74ac..bb13be4 100644 --- a/PresenterWpf/src/App.xaml.cs +++ b/PresenterWpf/src/App.xaml.cs @@ -1,9 +1,11 @@ using System.Configuration; using System.Data; using System.Windows; +using Engine; using Engine.Graphics; using Engine.Graphics.Texture; using OpenTK.Windowing.Common; +using Serilog.Events; namespace PresenterWpf; @@ -13,10 +15,16 @@ namespace PresenterWpf; public partial class App : Application { // Hijack the default startup event to start the engine - protected override void OnStartup(StartupEventArgs e) + protected override void OnStartup(StartupEventArgs parEventArgs) { var presenter = new PresenterWrapper(); - var engine = new Engine.Engine(1, 1, _ => presenter); + var engine = new EngineBuilder() + .Headless() + .Presenter(_ => presenter) + .LogToConsole() + .LogToFile(true, "log.txt") + .LogLevel(LogEventLevel.Debug) + .Build(); // Since engine claims current thread for rendering, we need to create a new thread to run WPF var thread = new Thread(() => diff --git a/PresenterWpf/src/MainWindow.xaml b/PresenterWpf/src/MainWindow.xaml index 91633ef..ef8cd4d 100644 --- a/PresenterWpf/src/MainWindow.xaml +++ b/PresenterWpf/src/MainWindow.xaml @@ -7,7 +7,6 @@ mc:Ignorable="d" Title="MainWindow" Height="450" Width="800" Closing="MainWindow_OnClosing"> - + \ No newline at end of file diff --git a/PresenterWpf/src/MainWindow.xaml.cs b/PresenterWpf/src/MainWindow.xaml.cs index f8e8420..8a8736f 100644 --- a/PresenterWpf/src/MainWindow.xaml.cs +++ b/PresenterWpf/src/MainWindow.xaml.cs @@ -1,42 +1,29 @@ -using System.ComponentModel; -using System.IO; -using System.Text; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; +using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; -using System.Windows.Threading; using Engine.Asset; using Engine.Graphics; using Engine.Graphics.Pixel; using Engine.Graphics.Texture; using OpenTK.Windowing.Common; +using CancelEventArgs = System.ComponentModel.CancelEventArgs; namespace PresenterWpf; -/// -/// Interaction logic for MainWindow.xaml -/// public partial class MainWindow : Window, IPresenter { - public bool IsExiting { get; set; } - public int Width => (int)_cachedWidth; - public int Height => (int)_cachedHeight; + public bool IsExiting { get; private set; } + public new int Width { get; private set; } + public new int Height { get; private set; } + + private bool _scheduledResize; + public event Action? Resize; - - private Engine.Engine _engine; + private readonly Engine.Engine _engine; private Image? _image; private WriteableBitmap? _bitmap; - private int _cachedWidth; - private int _cachedHeight; - public MainWindow(Engine.Engine parEngine) { InitializeComponent(); @@ -48,16 +35,21 @@ public partial class MainWindow : Window, IPresenter { Dispatcher.Invoke(() => { - if ((int)Image.Width != _cachedWidth || (int)Image.Height != _cachedHeight) + if ((int)Image.ActualWidth != Width || (int)Image.ActualHeight != Height) { - _cachedWidth = (int)Image.Width; - _cachedHeight = (int)Image.Height; + Width = (int)Image.ActualWidth; + Height = (int)Image.ActualHeight; + + _scheduledResize = true; } }); // Resizes are lazy so resizing only happens when the window's size actually changes - if (Width != 0 && Height != 0) - _engine.Renderer.Resize(Width, Height); + if (_scheduledResize) + { + _scheduledResize = false; + Resize?.Invoke(new ResizeEventArgs(Width, Height)); + } } public void Present(IConstTexture parTexture) diff --git a/PresenterWpf/src/Program.cs b/PresenterWpf/src/Program.cs deleted file mode 100644 index bd8481d..0000000 --- a/PresenterWpf/src/Program.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace PresenterWpf; - -internal class Program -{ -} \ No newline at end of file