This commit is contained in:
2024-12-14 11:17:47 +03:00
parent fa7b12c88c
commit ef922486eb
30 changed files with 1135 additions and 949 deletions

View File

@@ -10,6 +10,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PresenterWpf", "PresenterWp
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PresenterConsole", "PresenterConsole\PresenterConsole.csproj", "{85AA55C5-F8AF-4C38-8874-702684BCAFBB}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PresenterConsole", "PresenterConsole\PresenterConsole.csproj", "{85AA55C5-F8AF-4C38-8874-702684BCAFBB}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PresenterNative", "PresenterNative\PresenterNative.csproj", "{3B8B7867-5B38-4013-A366-159083F5C095}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{85AA55C5-F8AF-4C38-8874-702684BCAFBB}.Release|Any CPU.Build.0 = 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 EndGlobalSection
EndGlobal EndGlobal

View File

@@ -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);
}

View File

@@ -8,14 +8,14 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Engine\Engine.csproj" /> <ProjectReference Include="..\Engine\Engine.csproj"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog" Version="4.1.0" /> <PackageReference Include="Serilog" Version="4.1.0"/>
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" /> <PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0"/>
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/>
<PackageReference Include="System.Drawing.Common" Version="8.0.10" /> <PackageReference Include="System.Drawing.Common" Version="8.0.10"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -6,15 +6,7 @@ namespace DoomDeathmatch;
internal abstract class Program 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) public static void Main(string[] args)
{ {
var engine = new Engine.Engine(480, 270, false, "Doom Deathmatch");
engine.Run();
} }
} }

View File

@@ -7,15 +7,11 @@ namespace DoomDeathmatch;
public struct QuadVertex : IVertex public struct QuadVertex : IVertex
{ {
[Vertex(VertexAttribType.Float, 2)] [Vertex(VertexAttribType.Float, 2)] public Vector2 Position2;
public Vector2 Position2;
[Vertex(VertexAttribType.Float, 2)] [Vertex(VertexAttribType.Float, 2)] public Vector2 Position;
public Vector2 Position;
[Vertex(VertexAttribType.Float, 2)] [Vertex(VertexAttribType.Float, 2)] public Vector2 Position4;
public Vector2 Position4;
[Vertex(VertexAttribType.Float, 2)] [Vertex(VertexAttribType.Float, 2)] public Vector2 Position3;
public Vector2 Position3;
} }

View File

@@ -10,15 +10,21 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<InternalsVisibleTo Include="EngineTests"/> <InternalsVisibleTo Include="EngineTests"/>
<InternalsVisibleTo Include="PresenterConsole"/> <InternalsVisibleTo Include="PresenterConsole"/>
<InternalsVisibleTo Include="PresenterWpf"/> <InternalsVisibleTo Include="PresenterWpf"/>
<InternalsVisibleTo Include="PresenterNative"/>
<PackageReference Include="OpenTK" Version="4.8.2"/> <PackageReference Include="OpenTK" Version="4.8.2"/>
<PackageReference Include="Serilog" Version="4.1.0"/> <PackageReference Include="Serilog" Version="4.1.0"/>
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0"/> <PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0"/>
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/> <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/>
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -6,14 +6,16 @@ namespace Engine.Asset;
public class Image<T>(T[,] parPixels) public class Image<T>(T[,] parPixels)
where T : struct, IPixel where T : struct, IPixel
{ {
public int Width { get; } = parPixels.GetLength(1); public int Width { get; }
public int Height { get; } = parPixels.GetLength(0); public int Height { get; }
public T[,] Pixels { get; } = parPixels; public T[,] Pixels { get; } = parPixels;
public T this[int parY, int parX] => Pixels[parY, parX]; public T this[int parY, int parX] => Pixels[parY, parX];
public Image(int parWidth, int parHeight) : this(new T[parHeight, parWidth]) public Image(int parWidth, int parHeight) : this(new T[parHeight, parWidth])
{ {
Width = parWidth;
Height = parHeight;
} }
public DynamicTexture ToDynamicTexture() public DynamicTexture ToDynamicTexture()

View File

@@ -7,25 +7,49 @@ using OpenTK.Mathematics;
using OpenTK.Windowing.Common; using OpenTK.Windowing.Common;
using OpenTK.Windowing.Desktop; using OpenTK.Windowing.Desktop;
using Serilog; using Serilog;
using Serilog.Sinks.SystemConsole.Themes;
namespace Engine; namespace Engine;
public sealed class Engine public sealed class Engine
{ {
public Renderer Renderer => _renderer; public Renderer Renderer { get; }
public SceneManager SceneManager => _sceneManager; 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 ILogger _logger;
private readonly IInputHandler _inputHandler;
private readonly IPresenter _presenter; private IInputHandler? _inputHandler;
private IPresenter? _presenter;
private Thread? _updateThread; 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 var settings = new NativeWindowSettings
{ {
@@ -33,41 +57,16 @@ public sealed class Engine
Title = parTitle, Title = parTitle,
StartVisible = !parHeadless, StartVisible = !parHeadless,
APIVersion = new Version(4, 6), APIVersion = new Version(4, 6),
Profile = ContextProfile.Compatability Profile = ContextProfile.Core
}; };
_window = new Window(this, new NativeWindow(settings), parHeadless); Window = new Window(this, new NativeWindow(settings), parHeadless);
_renderer = new Renderer(parWidth, parHeight); 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();
Log.Logger = parLogger;
_logger = Log.ForContext<Engine>(); _logger = Log.ForContext<Engine>();
} }
public Engine(int parWidth, int parHeight, Func<Engine, IPresenter> parPresenter) : this(parWidth, parHeight, true)
{
_presenter = parPresenter(this);
}
public void Run() public void Run()
{ {
_updateThread = new Thread(RunUpdate); _updateThread = new Thread(RunUpdate);
@@ -75,23 +74,24 @@ public sealed class Engine
var timer = Stopwatch.StartNew(); var timer = Stopwatch.StartNew();
var deltaTime = 0.0; var deltaTime = 0.0;
while (!_presenter.IsExiting) while (!Presenter?.IsExiting ?? false)
{ {
var time = deltaTime; Renderer.StartFrame();
_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);
_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; deltaTime = timer.Elapsed.TotalSeconds;
} }
@@ -102,11 +102,19 @@ public sealed class Engine
{ {
var timer = Stopwatch.StartNew(); var timer = Stopwatch.StartNew();
var deltaTime = 0.0; var deltaTime = 0.0;
while (!_presenter.IsExiting) while (!Presenter?.IsExiting ?? false)
{ {
_sceneManager.Update(deltaTime); SceneManager.Update(deltaTime);
timer.Restart(); timer.Restart();
deltaTime = timer.Elapsed.TotalSeconds; deltaTime = timer.Elapsed.TotalSeconds;
} }
} }
private void PresenterResize(ResizeEventArgs parEventArgs)
{
if (parEventArgs.Width == 0 || parEventArgs.Height == 0)
return;
Renderer.Resize(parEventArgs.Width, parEventArgs.Height);
}
} }

151
Engine/src/EngineBuilder.cs Normal file
View File

@@ -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<Engine, IPresenter>? _presenterFunc;
private Func<Engine, IInputHandler>? _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<Engine, IPresenter> parPresenterFunc)
{
_presenterFunc = parPresenterFunc;
return this;
}
public EngineBuilder InputHandler(IInputHandler parInputHandler)
{
_inputHandlerFunc = _ => parInputHandler;
return this;
}
public EngineBuilder InputHandler(Func<Engine, IInputHandler> 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();
}
}

View File

@@ -8,6 +8,7 @@ namespace Engine.Graphics.Render.Quad;
public class QuadRenderer : InstancedRenderer<QuadCommonVertex, QuadInstanceVertex> public class QuadRenderer : InstancedRenderer<QuadCommonVertex, QuadInstanceVertex>
{ {
private readonly TextureUnitMap _textureUnitMap = new(16); private readonly TextureUnitMap _textureUnitMap = new(16);
private readonly int[] _textureUnitIndices = new int[16];
public QuadRenderer(Renderer parRenderer, int parInstanceCount) public QuadRenderer(Renderer parRenderer, int parInstanceCount)
: base(parRenderer, PrimitiveType.Triangles, parInstanceCount, [0, 1, 2, 2, 3, 0], [ : base(parRenderer, PrimitiveType.Triangles, parInstanceCount, [0, 1, 2, 2, 3, 0], [
@@ -41,14 +42,13 @@ public class QuadRenderer : InstancedRenderer<QuadCommonVertex, QuadInstanceVert
protected override void SetAdditionalUniforms(Program parProgram) protected override void SetAdditionalUniforms(Program parProgram)
{ {
var samplers = new int[_textureUnitMap.Size]; foreach (var (texture, unit) in _textureUnitMap.Textures)
foreach (var (texture, unit) in _textureUnitMap)
{ {
texture.BindUnit(unit); texture.BindUnit(unit);
samplers[unit] = unit; _textureUnitIndices[unit] = unit;
} }
parProgram.SetUniform("uTexture", samplers); parProgram.SetUniform("uTexture", _textureUnitIndices);
} }
public override void Reset() public override void Reset()

View File

@@ -8,15 +8,16 @@ public class Renderer
{ {
internal Texture.Texture TextureInternal => _framebuffer.TextureInternal!; internal Texture.Texture TextureInternal => _framebuffer.TextureInternal!;
public readonly QuadRenderer QuadRenderer; public QuadRenderer QuadRenderer { get; }
private readonly Framebuffer.Framebuffer _framebuffer; private readonly Framebuffer.Framebuffer _framebuffer;
private readonly Queue<Action<Renderer>> _renderActions = new();
private readonly Thread _renderThread; private readonly Thread _renderThread;
public Renderer(int parWidth, int parHeight) public Renderer(int parWidth, int parHeight)
{ {
Thread.CurrentThread.Name = "RendererThread";
InitializeOpenGl(parWidth, parHeight); InitializeOpenGl(parWidth, parHeight);
_renderThread = Thread.CurrentThread; _renderThread = Thread.CurrentThread;
@@ -37,7 +38,7 @@ public class Renderer
GL.Enable(EnableCap.DepthTest); GL.Enable(EnableCap.DepthTest);
GL.Enable(EnableCap.FramebufferSrgb); // GL.Enable(EnableCap.FramebufferSrgb);
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
GL.Enable(EnableCap.Blend); GL.Enable(EnableCap.Blend);
@@ -47,29 +48,27 @@ public class Renderer
public void EnsureRenderThread() public void EnsureRenderThread()
{ {
#if DEBUG
if (Thread.CurrentThread == _renderThread) if (Thread.CurrentThread == _renderThread)
{ {
return; return;
} }
throw new InvalidOperationException("Renderer is not on render thread"); throw new InvalidOperationException("Renderer is not on render thread");
#endif
} }
internal void Commit(Action<Renderer> parRenderAction) internal void StartFrame()
{
_renderActions.Enqueue(parRenderAction);
}
internal void Render()
{ {
EnsureRenderThread(); EnsureRenderThread();
_framebuffer.Bind(); _framebuffer.Bind();
while (_renderActions.TryDequeue(out var renderAction))
{
renderAction(this);
} }
internal void EndFrame()
{
EnsureRenderThread();
_framebuffer.Unbind(); _framebuffer.Unbind();
} }

View File

@@ -2,18 +2,13 @@
namespace Engine.Graphics.Texture; namespace Engine.Graphics.Texture;
public class TextureUnitMap : IEnumerable<KeyValuePair<Texture, int>> public class TextureUnitMap(int parCapacity)
{ {
public int Size => _textures.Count; public int Size => _textures.Count;
public int Capacity => _capacity; public int Capacity => parCapacity;
public IReadOnlyDictionary<Texture, int> Textures => _textures;
private readonly Dictionary<Texture, int> _textures = new(); private readonly Dictionary<Texture, int> _textures = new();
private readonly int _capacity;
public TextureUnitMap(int parCapacity)
{
_capacity = parCapacity;
}
public int GetUnit(Texture parTexture) public int GetUnit(Texture parTexture)
{ {
@@ -22,7 +17,7 @@ public class TextureUnitMap : IEnumerable<KeyValuePair<Texture, int>>
return unit; return unit;
} }
if (_textures.Count >= _capacity) if (_textures.Count >= parCapacity)
{ {
throw new InvalidOperationException("Texture unit map is full"); throw new InvalidOperationException("Texture unit map is full");
} }
@@ -37,14 +32,4 @@ public class TextureUnitMap : IEnumerable<KeyValuePair<Texture, int>>
{ {
_textures.Clear(); _textures.Clear();
} }
public IEnumerator<KeyValuePair<Texture, int>> GetEnumerator()
{
return _textures.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
} }

View File

@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true"> <xsd:element name="root" msdata:IsDataSet="true">
</xsd:element> </xsd:element>
@@ -13,10 +14,14 @@
<value>1.3</value> <value>1.3</value>
</resheader> </resheader>
<resheader name="reader"> <resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader> </resheader>
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader> </resheader>
<data name="Mesh" type="System.Resources.ResXFileRef" xml:space="preserve"> <data name="Mesh" type="System.Resources.ResXFileRef" xml:space="preserve">

View File

@@ -51,8 +51,6 @@ public class Window : IPresenter
public void Present(IConstTexture parTexture) public void Present(IConstTexture parTexture)
{ {
GL.Viewport(0, 0, Width, Height);
GL.ClearColor(0.0f, 0.0f, 0.0f, 1.0f); GL.ClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

View File

@@ -22,11 +22,11 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Engine\Engine.csproj" /> <ProjectReference Include="..\Engine\Engine.csproj"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="src\" /> <Folder Include="src\"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -9,7 +9,7 @@ public class HierarchyTests
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
_hierarchy = new Hierarchy<object>(); _hierarchy = [];
} }
[Test] [Test]

View File

@@ -5,6 +5,7 @@
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -17,68 +17,106 @@ void main()
precision highp float; precision highp float;
// All components are in the range [0…1], including hue. // ConsoleColor vec3 palette (normalized)
vec3 rgb2hsv(vec3 c) const vec3 ConsoleColorVec3[16] = vec3[](
{ vec3(0.0, 0.0, 0.0), // Black
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); vec3(0.0, 0.0, 0.5), // DarkBlue
vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g)); vec3(0.0, 0.5, 0.0), // DarkGreen
vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r)); vec3(0.0, 0.5, 0.5), // DarkCyan
vec3(0.5, 0.0, 0.0), // DarkRed
float d = q.x - min(q.w, q.y); vec3(0.5, 0.0, 0.5), // DarkMagenta
float e = 1.0e-10; vec3(0.5, 0.5, 0.0), // DarkYellow
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); vec3(0.5, 0.5, 0.5), // Gray
} vec3(0.25, 0.25, 0.25), // DarkGray
vec3(0.0, 0.0, 1.0), // Blue
// 16 Windows color palette (approximated) vec3(0.0, 1.0, 0.0), // Green
const vec3 windowsPalette[7] = vec3[]( vec3(0.0, 1.0, 1.0), // Cyan
vec3(rgb2hsv(vec3(0.0, 0.0, 1.0)).rg, 0.0), // Blue (FOREGROUND_BLUE) vec3(1.0, 0.0, 0.0), // Red
vec3(rgb2hsv(vec3(0.0, 1.0, 0.0)).rg, 0.0), // Green (FOREGROUND_GREEN) vec3(1.0, 0.0, 1.0), // Magenta
vec3(rgb2hsv(vec3(0.0, 1.0, 1.0)).rg, 0.0), // Cyan (FOREGROUND_GREEN | FOREGROUND_BLUE) vec3(1.0, 1.0, 0.0), // Yellow
vec3(rgb2hsv(vec3(1.0, 0.0, 0.0)).rg, 0.0), // Red (FOREGROUND_RED) vec3(1.0, 1.0, 1.0) // White
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)
); );
// Find the closest color in the Windows palette vec3 f(vec3 x) {
int findClosestColor(vec3 color) { const float epsilon = 0.008856;
int closestIndex = 0; const float k = 903.3;
float minDistance = distance(color, windowsPalette[0]); vec3 fx;
fx.x = x.x > epsilon ? pow(x.x, 1.0 / 3.0) : (k * x.x + 16.0) / 116.0;
for (int i = 1; i < 7; i++) { fx.y = x.y > epsilon ? pow(x.y, 1.0 / 3.0) : (k * x.y + 16.0) / 116.0;
float dist = distance(color, windowsPalette[i]); fx.z = x.z > epsilon ? pow(x.z, 1.0 / 3.0) : (k * x.z + 16.0) / 116.0;
if (dist < minDistance) { return fx;
minDistance = dist;
closestIndex = i;
}
}
return closestIndex + 1;
} }
float calculateLuminosity(vec3 color) { // Perceptually weighted color difference (CIEDE2000-inspired)
// Standard luminosity calculation float perceptualColorDistance(vec3 color1, vec3 color2) {
float luminosity = dot(color, vec3(0.299, 0.587, 0.114)); // RGB to LAB conversion (simplified approximation)
return clamp(luminosity, 0.0, 1.0);
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 bestMatchIndex;
}
// 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 sampler2D uInputTexture;
uniform vec2 uResolution;
layout (location = 0) in vec2 iUV; layout (location = 0) in vec2 iUV;
layout (location = 0) out vec4 FragColor; layout (location = 0) out vec4 FragColor;
void main() { void main() {
vec3 pixelColor = texture(uInputTexture, iUV).rgb; vec3 pixelColor = texture(uInputTexture, iUV).rgb;
vec3 pixelColorHsv = rgb2hsv(pixelColor);
// Find closest color index (4 bits) // Find most perceptually accurate console color
int colorIndex = findClosestColor(vec3(pixelColorHsv.rg, 0)); int colorIndex = findMostPerceptuallyAccurateColor(pixelColor);
// Calculate luminosity (4 bits) // Calculate perceptual luminance with gamma correction
float luminosity = calculateLuminosity(pixelColor); float luminance = calculatePerceptualLuminance(pixelColor);
// Combine into a single byte-like value // Output with high precision color mapping
// High 4 bits: color index FragColor = vec4(
// Low 4 bits: luminosity luminance, // Red: Perceptual luminance
FragColor = vec4(luminosity, colorIndex / 255.0, 0, 1); float(colorIndex) / 15.0, // Green: Normalized color index
0.0, // Blue: Unused
1.0 // Alpha
);
} }

View File

@@ -11,8 +11,8 @@ public struct AsciiPixel : IPixel
public PixelInternalFormat InternalFormat => PixelInternalFormat.Rg8; public PixelInternalFormat InternalFormat => PixelInternalFormat.Rg8;
public SizedInternalFormat SizedInternalFormat => SizedInternalFormat.Rg8; public SizedInternalFormat SizedInternalFormat => SizedInternalFormat.Rg8;
public byte LightnessIndex => R; public byte Luminance => R;
public byte ColorIndex => G; public byte Color => G;
public byte R; public byte R;
public byte G; public byte G;

View File

@@ -1,158 +1,71 @@
using System.Runtime.InteropServices; using System.Runtime.CompilerServices;
using Microsoft.Win32.SafeHandles; using Microsoft.Win32.SafeHandles;
namespace PresenterConsole; namespace PresenterConsole;
public class ConsoleFastOutput public sealed class ConsoleFastOutput : IDisposable
{ {
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] private const uint GENERIC_WRITE = 0x40000000;
static extern SafeFileHandle CreateFile( private const int FILE_SHARE_WRITE = 0x2;
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 readonly SafeFileHandle _handle; private readonly SafeFileHandle _handle;
private CharInfo[] _buffer; private readonly WindowsFFI.Coord _bufferCoord = new(0, 0);
private SmallRect _region;
private int _width; private int _width;
private int _height; private int _height;
private WindowsFFI.CharInfo[] _buffer;
private WindowsFFI.Coord _bufferSize;
public ConsoleFastOutput(int parWidth, int parHeight) public ConsoleFastOutput(int parWidth, int parHeight)
{ {
_width = parWidth; ArgumentOutOfRangeException.ThrowIfNegativeOrZero(parWidth);
_height = parHeight; ArgumentOutOfRangeException.ThrowIfNegativeOrZero(parHeight);
_handle = CreateFile("CONOUT$", 0x40000000, 2, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero);
_handle = WindowsFFI.CreateFile("CONOUT$", GENERIC_WRITE, FILE_SHARE_WRITE,
IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero);
if (_handle.IsInvalid) if (_handle.IsInvalid)
{ throw new InvalidOperationException("Failed to open console handle");
throw new Exception("Failed to open console");
Resize(parWidth, parHeight);
} }
_buffer = new CharInfo[parWidth * parHeight]; [MethodImpl(MethodImplOptions.AggressiveInlining)]
_region = new SmallRect { Left = 0, Top = 0, Right = (short)_width, Bottom = (short)_height }; public void WriteChar(char parCharacter, int parX, int parY, ConsoleColor parForeground, ConsoleColor parBackground)
}
public void WriteChar(char parChar, short parX, short parY, ushort parForeground, ushort parBackground)
{ {
var index = parX + parY * _width; if (parX < 0 || parX >= _width || parY < 0 || parY >= _height)
if (index >= _buffer.Length)
return; return;
_buffer[parX + parY * _width].Char.UnicodeChar = parChar; var index = parX + parY * _width;
_buffer[parX + parY * _width].Attributes = (short)(parForeground | (parBackground << 4)); ref var charInfo = ref _buffer[index];
charInfo.Char.UnicodeChar = parCharacter;
charInfo.Attributes = (short)((int)parForeground | ((int)parBackground << 4));
} }
public void Flush() public void Flush()
{ {
var regions = SplitRegion(_region, 1, 1); var writeRegion = new WindowsFFI.SmallRect { Left = 0, Top = 0, Right = (short)_width, Bottom = (short)_height };
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 bufferSize = new Coord(_region.Right, _region.Bottom); WindowsFFI.WriteConsoleOutput(_handle, _buffer, _bufferSize, _bufferCoord, ref writeRegion);
// 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;
} }
public void Resize(int parWidth, int parHeight) public void Resize(int parWidth, int parHeight)
{ {
if (parWidth <= 0 || parHeight <= 0) ArgumentOutOfRangeException.ThrowIfNegativeOrZero(parWidth);
{ ArgumentOutOfRangeException.ThrowIfNegativeOrZero(parHeight);
throw new ArgumentException("Width and height must be greater than 0");
}
if (parWidth == _width && parHeight == _height) if (parWidth == _width && parHeight == _height)
{
return; return;
}
_width = parWidth; _width = parWidth;
_height = parHeight; _height = parHeight;
_buffer = new WindowsFFI.CharInfo[parWidth * parHeight];
_bufferSize = new WindowsFFI.Coord((short)_width, (short)_height);
}
_buffer = new CharInfo[parWidth * parHeight]; public void Dispose()
_region = new SmallRect { Left = 0, Top = 0, Right = (short)parWidth, Bottom = (short)parHeight }; {
_handle.Dispose();
} }
} }

View File

@@ -31,6 +31,7 @@ public class ConsolePresenter : IPresenter
private readonly VertexArray _vertexArray; private readonly VertexArray _vertexArray;
private readonly ConsoleFastOutput _consoleOutput; private readonly ConsoleFastOutput _consoleOutput;
private static readonly char[] LIGHTMAP = " .:-+=#%@".Reverse().ToArray();
public ConsolePresenter(Engine.Engine parEngine) public ConsolePresenter(Engine.Engine parEngine)
{ {
@@ -41,17 +42,18 @@ public class ConsolePresenter : IPresenter
.AddColorAttachment<AsciiPixel>() .AddColorAttachment<AsciiPixel>()
.Build(); .Build();
_indexBuffer = new IndexBuffer([0, 1, 2, 2, 3, 0], BufferStorageFlags.MapReadBit); _indexBuffer = new IndexBuffer([0, 1, 2, 2, 3, 0]);
_vertexBuffer = new VertexBuffer<AsciiVertex>([ _vertexBuffer = new VertexBuffer<AsciiVertex>([
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(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, 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(1.0f, 1.0f) },
new AsciiVertex { _position = new Vector2(-1.0f, 1.0f), _uv = new Vector2(0.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 = new VertexArray();
_vertexArray.BindIndexBuffer(_indexBuffer); _vertexArray.BindIndexBuffer(_indexBuffer);
_vertexArray.BindVertexBuffer(_vertexBuffer); _vertexArray.BindVertexBuffer(_vertexBuffer);
Console.CursorVisible = false;
_consoleOutput = new ConsoleFastOutput(Width, Height); _consoleOutput = new ConsoleFastOutput(Width, Height);
} }
@@ -87,15 +89,15 @@ public class ConsolePresenter : IPresenter
private void Output(Image<AsciiPixel> parImage) private void Output(Image<AsciiPixel> parImage)
{ {
var lightmap = " .-:=+*#%@".Reverse().ToArray();
for (var y = 0; y < parImage.Height; y++) for (var y = 0; y < parImage.Height; y++)
{ {
for (var x = 0; x < parImage.Width; x++) for (var x = 0; x < parImage.Width; x++)
{ {
var pixel = parImage[y, x]; var pixel = parImage[y, x];
var lightnessIndex = (int)(pixel.LightnessIndex / 255.0f * (lightmap.Length - 1)); var lightnessIndex = (int)(pixel.Luminance / 255.0f * (LIGHTMAP.Length - 1));
_consoleOutput.WriteChar(lightmap[lightnessIndex], (short)x, (short)y, 0, var colorIndex = (int)(pixel.Color / 255.0f * 15.0f);
pixel.ColorIndex); _consoleOutput.WriteChar(LIGHTMAP[lightnessIndex], (short)x, (short)y, 0,
(ConsoleColor)colorIndex);
} }
} }
@@ -109,8 +111,9 @@ public class ConsolePresenter : IPresenter
_cachedWidth = Width; _cachedWidth = Width;
_cachedHeight = Height; _cachedHeight = Height;
Resize?.Invoke(new ResizeEventArgs(Width, Height));
_framebuffer.Resize(Width, Height); _framebuffer.Resize(Width, Height);
_engine.Renderer.Resize(Width, Height);
_consoleOutput.Resize(Width, Height); _consoleOutput.Resize(Width, Height);
} }
} }

View File

@@ -1,12 +1,20 @@
// See https://aka.ms/new-console-template for more information // 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(); engine.Run();
} }
} }

View File

@@ -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
);
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Engine\Engine.csproj"/>
</ItemGroup>
</Project>

View File

@@ -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();
}
}

View File

@@ -1,9 +1,11 @@
using System.Configuration; using System.Configuration;
using System.Data; using System.Data;
using System.Windows; using System.Windows;
using Engine;
using Engine.Graphics; using Engine.Graphics;
using Engine.Graphics.Texture; using Engine.Graphics.Texture;
using OpenTK.Windowing.Common; using OpenTK.Windowing.Common;
using Serilog.Events;
namespace PresenterWpf; namespace PresenterWpf;
@@ -13,10 +15,16 @@ namespace PresenterWpf;
public partial class App : Application public partial class App : Application
{ {
// Hijack the default startup event to start the engine // 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 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 // Since engine claims current thread for rendering, we need to create a new thread to run WPF
var thread = new Thread(() => var thread = new Thread(() =>

View File

@@ -7,7 +7,6 @@
mc:Ignorable="d" mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800" Closing="MainWindow_OnClosing"> Title="MainWindow" Height="450" Width="800" Closing="MainWindow_OnClosing">
<Grid> <Grid>
<Image x:Name="Image" Width="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=ActualWidth}" <Image x:Name="Image" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Stretch="Fill" />
Height="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=ActualHeight}" />
</Grid> </Grid>
</Window> </Window>

View File

@@ -1,42 +1,29 @@
using System.ComponentModel; using System.Windows;
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.Media; using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
using Engine.Asset; using Engine.Asset;
using Engine.Graphics; using Engine.Graphics;
using Engine.Graphics.Pixel; using Engine.Graphics.Pixel;
using Engine.Graphics.Texture; using Engine.Graphics.Texture;
using OpenTK.Windowing.Common; using OpenTK.Windowing.Common;
using CancelEventArgs = System.ComponentModel.CancelEventArgs;
namespace PresenterWpf; namespace PresenterWpf;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, IPresenter public partial class MainWindow : Window, IPresenter
{ {
public bool IsExiting { get; set; } public bool IsExiting { get; private set; }
public int Width => (int)_cachedWidth; public new int Width { get; private set; }
public int Height => (int)_cachedHeight; public new int Height { get; private set; }
private bool _scheduledResize;
public event Action<ResizeEventArgs>? Resize; public event Action<ResizeEventArgs>? Resize;
private readonly Engine.Engine _engine;
private Engine.Engine _engine;
private Image<Rgb8>? _image; private Image<Rgb8>? _image;
private WriteableBitmap? _bitmap; private WriteableBitmap? _bitmap;
private int _cachedWidth;
private int _cachedHeight;
public MainWindow(Engine.Engine parEngine) public MainWindow(Engine.Engine parEngine)
{ {
InitializeComponent(); InitializeComponent();
@@ -48,16 +35,21 @@ public partial class MainWindow : Window, IPresenter
{ {
Dispatcher.Invoke(() => Dispatcher.Invoke(() =>
{ {
if ((int)Image.Width != _cachedWidth || (int)Image.Height != _cachedHeight) if ((int)Image.ActualWidth != Width || (int)Image.ActualHeight != Height)
{ {
_cachedWidth = (int)Image.Width; Width = (int)Image.ActualWidth;
_cachedHeight = (int)Image.Height; Height = (int)Image.ActualHeight;
_scheduledResize = true;
} }
}); });
// Resizes are lazy so resizing only happens when the window's size actually changes // Resizes are lazy so resizing only happens when the window's size actually changes
if (Width != 0 && Height != 0) if (_scheduledResize)
_engine.Renderer.Resize(Width, Height); {
_scheduledResize = false;
Resize?.Invoke(new ResizeEventArgs(Width, Height));
}
} }
public void Present(IConstTexture parTexture) public void Present(IConstTexture parTexture)

View File

@@ -1,5 +0,0 @@
namespace PresenterWpf;
internal class Program
{
}