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
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

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>
<ItemGroup>
<ProjectReference Include="..\Engine\Engine.csproj" />
<ProjectReference Include="..\Engine\Engine.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Serilog" Version="4.1.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="System.Drawing.Common" Version="8.0.10" />
<PackageReference Include="Serilog" Version="4.1.0"/>
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0"/>
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/>
<PackageReference Include="System.Drawing.Common" Version="8.0.10"/>
</ItemGroup>
</Project>

View File

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

View File

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

View File

@@ -10,31 +10,37 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="EngineTests"/>
<InternalsVisibleTo Include="PresenterConsole"/>
<InternalsVisibleTo Include="PresenterWpf"/>
<InternalsVisibleTo Include="PresenterNative"/>
<PackageReference Include="OpenTK" Version="4.8.2"/>
<PackageReference Include="Serilog" Version="4.1.0"/>
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0"/>
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/>
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0"/>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="src\Resource\ShaderResource.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Test.Designer.cs</LastGenOutput>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Update="src\Resource\ShaderResource.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Test.Designer.cs</LastGenOutput>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Compile Update="src\Resource\ShaderResource.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>ShaderResource.resx</DependentUpon>
</Compile>
<Compile Update="src\Resource\ShaderResource.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>ShaderResource.resx</DependentUpon>
</Compile>
</ItemGroup>
</Project>

View File

@@ -6,14 +6,16 @@ namespace Engine.Asset;
public class Image<T>(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()

View File

@@ -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<Engine>();
}
public Engine(int parWidth, int parHeight, Func<Engine, IPresenter> 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);
}
}

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>
{
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<QuadCommonVertex, QuadInstanceVert
protected override void SetAdditionalUniforms(Program parProgram)
{
var samplers = new int[_textureUnitMap.Size];
foreach (var (texture, unit) in _textureUnitMap)
foreach (var (texture, unit) in _textureUnitMap.Textures)
{
texture.BindUnit(unit);
samplers[unit] = unit;
_textureUnitIndices[unit] = unit;
}
parProgram.SetUniform("uTexture", samplers);
parProgram.SetUniform("uTexture", _textureUnitIndices);
}
public override void Reset()

View File

@@ -8,15 +8,16 @@ public class Renderer
{
internal Texture.Texture TextureInternal => _framebuffer.TextureInternal!;
public readonly QuadRenderer QuadRenderer;
public QuadRenderer QuadRenderer { get; }
private readonly Framebuffer.Framebuffer _framebuffer;
private readonly Queue<Action<Renderer>> _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<Renderer> 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();
}

View File

@@ -2,18 +2,13 @@
namespace Engine.Graphics.Texture;
public class TextureUnitMap : IEnumerable<KeyValuePair<Texture, int>>
public class TextureUnitMap(int parCapacity)
{
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 int _capacity;
public TextureUnitMap(int parCapacity)
{
_capacity = parCapacity;
}
public int GetUnit(Texture parTexture)
{
@@ -22,7 +17,7 @@ public class TextureUnitMap : IEnumerable<KeyValuePair<Texture, int>>
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<KeyValuePair<Texture, int>>
{
_textures.Clear();
}
public IEnumerator<KeyValuePair<Texture, int>> GetEnumerator()
{
return _textures.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

View File

@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<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>
</xsd:schema>
<resheader name="resmimetype">
@@ -13,10 +14,14 @@
<value>1.3</value>
</resheader>
<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 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>
<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)
{
GL.Viewport(0, 0, Width, Height);
GL.ClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

View File

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

View File

@@ -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<Transform>(), Is.True);
});
}
[Test]
public void Constructor_ShouldInitializeWithTransformComponent()
[Test]
public void AddComponent_ShouldThrowIfComponentAlreadyExists()
{
_gameObject.AddComponent<Transform>();
Assert.Throws<ArgumentException>(() => _gameObject.ProcessChanges());
}
[Test]
public void AddComponent_ShouldAddComponentToGameObject()
{
_gameObject.AddComponent<OrthographicCamera>();
_gameObject.ProcessChanges();
Assert.Multiple(() =>
{
Assert.Multiple(() =>
{
Assert.That(_gameObject.Transform, Is.Not.Null);
Assert.That(_gameObject.HasComponent<Transform>(), Is.True);
});
}
Assert.That(_gameObject.GetComponent<OrthographicCamera>(), Is.Not.Null);
Assert.That(_gameObject.HasComponent<OrthographicCamera>(), Is.True);
});
}
[Test]
public void AddComponent_ShouldThrowIfComponentAlreadyExists()
[Test]
public void AddComponent_ShouldAddComponentToGameObjectWithArgs()
{
_gameObject.AddComponent<PerspectiveCamera>(99, 0.2f, 1001f);
_gameObject.ProcessChanges();
var camera = _gameObject.GetComponent<PerspectiveCamera>()!;
Assert.Multiple(() =>
{
_gameObject.AddComponent<Transform>();
Assert.That(camera.FieldOfView, Is.EqualTo(99));
Assert.That(camera.NearPlane, Is.EqualTo(0.2f));
Assert.That(camera.FarPlane, Is.EqualTo(1001f));
});
}
Assert.Throws<ArgumentException>(() => _gameObject.ProcessChanges());
}
[Test]
public void AddComponent_ShouldThrowIfComponentBaseTypeAlreadyExists()
{
_gameObject.AddComponent<OrthographicCamera>();
_gameObject.AddComponent<PerspectiveCamera>();
[Test]
public void AddComponent_ShouldAddComponentToGameObject()
{
_gameObject.AddComponent<OrthographicCamera>();
_gameObject.ProcessChanges();
Assert.Multiple(() =>
{
Assert.That(_gameObject.GetComponent<OrthographicCamera>(), Is.Not.Null);
Assert.That(_gameObject.HasComponent<OrthographicCamera>(), Is.True);
});
}
[Test]
public void AddComponent_ShouldAddComponentToGameObjectWithArgs()
{
_gameObject.AddComponent<PerspectiveCamera>(99, 0.2f, 1001f);
_gameObject.ProcessChanges();
var camera = _gameObject.GetComponent<PerspectiveCamera>()!;
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<OrthographicCamera>();
_gameObject.AddComponent<PerspectiveCamera>();
Assert.Throws<ArgumentException>(() => _gameObject.ProcessChanges());
}
Assert.Throws<ArgumentException>(() => _gameObject.ProcessChanges());
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.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(() =>

View File

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

View File

@@ -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;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
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<ResizeEventArgs>? Resize;
private Engine.Engine _engine;
private readonly Engine.Engine _engine;
private Image<Rgb8>? _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)

View File

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