feat: modular HAL, Raylib backend, PBR shading, textures, 60 unit tests

- Replace hardcoded SDL3 windowing with IWindow/IInputState/Key abstractions
- Each render backend owns its window (Raylib GLFW, SDL3 for Vulkan)
- Raylib backend: DrawModelEx, custom GLSL shader with Fresnel, ACES
  tonemapping, gamma correction, hemisphere ambient
- Fix backface culling, mesh memory (NativeMemory.Alloc), texture loading
- Camera controllers use backend-agnostic Key enum (inverted yaw/strafe)
- Demo scene: 8 cubes, 7 spheres, torus knot OBJ with checker texture
- Extract ProceduralMesh + MeshMath from Program.cs to Engine.Graphics
- Vulkan backend deferred (compiles, untested, IWindow-compatible)
- 60 unit tests: ObjLoader, camera controllers, AiCommandProcessor,
  RenderBackendFactory, Timing, ProceduralMesh, MeshMath, Transform
- AGENTS.md for opencode integration
This commit is contained in:
emil28092005
2026-06-17 13:49:12 +03:00
parent 61f8c7065e
commit fb6e26a268
62 changed files with 22259 additions and 395 deletions
@@ -0,0 +1,173 @@
using System.Numerics;
using Engine.Core;
using Engine.Core.Components;
using Engine.AI;
using Flecs.NET.Core;
namespace Engine.Tests;
public class AiCommandProcessorTests
{
private static (AiCommandProcessor, World) CreateProcessor()
{
var world = World.Create();
var dummyMesh = new Mesh(
new[] { new Vertex(new Vector3(0, 0, 0), Vector3.One, Vector3.UnitY) },
new uint[] { 0 });
var processor = new AiCommandProcessor(
world,
_ => dummyMesh,
_ => { });
return (processor, world);
}
[Fact]
public void SpawnModel_Creates_Entity_With_Transform()
{
var (processor, world) = CreateProcessor();
var result = processor.Process("""
{ "type": "spawn_model", "name": "TestCube", "modelPath": "fake.obj", "position": [1, 2, 3] }
""");
Assert.True(result.Success);
var entity = world.Lookup("TestCube");
Assert.True((ulong)entity.Id != 0);
var transform = entity.Get<Transform>();
Assert.Equal(new Vector3(1, 2, 3), transform.Position);
}
[Fact]
public void SetTransform_Updates_Position()
{
var (processor, world) = CreateProcessor();
processor.Process("""{ "type": "spawn_model", "name": "Test", "modelPath": "x.obj" }""");
var result = processor.Process("""
{ "type": "set_transform", "name": "Test", "position": [5, 5, 5] }
""");
Assert.True(result.Success);
var transform = world.Lookup("Test").Get<Transform>();
Assert.Equal(new Vector3(5, 5, 5), transform.Position);
}
[Fact]
public void SetTransform_Partial_Update_Keeps_Other_Fields()
{
var (processor, world) = CreateProcessor();
processor.Process("""{ "type": "spawn_model", "name": "Test", "modelPath": "x.obj", "position": [1, 1, 1], "scale": [2, 2, 2] }""");
processor.Process("""{ "type": "set_transform", "name": "Test", "position": [9, 9, 9] }""");
var transform = world.Lookup("Test").Get<Transform>();
Assert.Equal(new Vector3(9, 9, 9), transform.Position);
Assert.Equal(new Vector3(2, 2, 2), transform.Scale);
}
[Fact]
public void SetMaterial_Updates_Albedo_And_Roughness()
{
var (processor, world) = CreateProcessor();
processor.Process("""{ "type": "spawn_model", "name": "Test", "modelPath": "x.obj" }""");
var result = processor.Process("""
{ "type": "set_material", "name": "Test", "albedo": [1, 0, 0], "roughness": 0.8 }
""");
Assert.True(result.Success);
var mat = world.Lookup("Test").Get<Material>();
Assert.Equal(new Vector3(1, 0, 0), mat.Albedo);
Assert.Equal(0.8f, mat.Roughness);
}
[Fact]
public void DeleteEntity_Removes_Entity()
{
var (processor, world) = CreateProcessor();
processor.Process("""{ "type": "spawn_model", "name": "ToDelete", "modelPath": "x.obj" }""");
var result = processor.Process("""{ "type": "delete_entity", "name": "ToDelete" }""");
Assert.True(result.Success);
Assert.True((ulong)world.Lookup("ToDelete").Id == 0);
}
[Fact]
public void ListEntities_Returns_Names()
{
var (processor, world) = CreateProcessor();
processor.Process("""{ "type": "spawn_model", "name": "Alpha", "modelPath": "x.obj" }""");
processor.Process("""{ "type": "spawn_model", "name": "Beta", "modelPath": "x.obj" }""");
var result = processor.Process("""{ "type": "list_entities" }""");
Assert.True(result.Success);
Assert.Contains("Alpha", result.Message);
Assert.Contains("Beta", result.Message);
}
[Fact]
public void CaptureScreenshot_Calls_Callback()
{
var world = World.Create();
var capturedPath = "";
var processor = new AiCommandProcessor(
world,
_ => new Mesh(new[] { new Vertex(Vector3.Zero, Vector3.One, Vector3.UnitY) }, new uint[] { 0 }),
path => capturedPath = path);
var result = processor.Process("""{ "type": "capture_screenshot", "outputPath": "test.png" }""");
Assert.True(result.Success);
Assert.Equal("test.png", capturedPath);
}
[Fact]
public void GetWorldState_Returns_Json()
{
var (processor, world) = CreateProcessor();
processor.Process("""{ "type": "spawn_model", "name": "StateTest", "modelPath": "x.obj", "position": [1, 2, 3] }""");
var result = processor.Process("""{ "type": "get_world_state" }""");
Assert.True(result.Success);
Assert.Contains("StateTest", result.Message);
Assert.Contains("position", result.Message);
}
[Fact]
public void SetTransform_On_Nonexistent_Entity_Returns_Error()
{
var (processor, world) = CreateProcessor();
var result = processor.Process("""{ "type": "set_transform", "name": "Ghost", "position": [0, 0, 0] }""");
Assert.False(result.Success);
}
[Fact]
public void Invalid_JSON_Returns_Error()
{
var (processor, world) = CreateProcessor();
var result = processor.Process("not valid json");
Assert.False(result.Success);
}
[Fact]
public void ProcessBatch_Handles_Multiple_Commands()
{
var (processor, world) = CreateProcessor();
var results = processor.ProcessBatch("""
{ "type": "spawn_model", "name": "A", "modelPath": "x.obj" }
{ "type": "spawn_model", "name": "B", "modelPath": "x.obj" }
""");
Assert.Equal(2, results.Length);
Assert.True(results[0].Success);
Assert.True(results[1].Success);
}
}
+235
View File
@@ -0,0 +1,235 @@
using System.Numerics;
using Engine.Core;
using Engine.Core.Components;
using Flecs.NET.Core;
namespace Engine.Tests;
/// <summary>
/// Test double for IInputState — set properties before calling controller.Update().
/// </summary>
internal sealed class FakeInputState : IInputState
{
private readonly HashSet<Key> _down = new();
private readonly HashSet<Key> _pressed = new();
public int MouseX { get; set; }
public int MouseY { get; set; }
public bool MouseLeft { get; set; }
public bool MouseRight { get; set; }
public bool MouseMiddle { get; set; }
public float MouseWheelDelta { get; set; }
public void SetKeyDown(Key key) => _down.Add(key);
public void SetKeyPressed(Key key)
{
_down.Add(key);
_pressed.Add(key);
}
public bool IsKeyDown(Key key) => _down.Contains(key);
public bool IsKeyPressed(Key key) => _pressed.Contains(key);
public bool IsKeyReleased(Key key) => false;
public void BeginFrame()
{
_pressed.Clear();
MouseWheelDelta = 0;
}
}
public class FreeFlyCameraControllerTests
{
private static (FreeFlyCameraController, Entity) CreateController(float yaw = 0f, float pitch = 0f)
{
var world = World.Create();
var pos = new Vector3(0, 1, -10);
var dir = new Vector3(
MathF.Cos(pitch) * MathF.Sin(yaw),
-MathF.Sin(pitch),
MathF.Cos(pitch) * MathF.Cos(yaw));
var cam = new Camera(pos, pos + dir, Vector3.UnitY);
var entity = world.Entity("TestCamera").Set(cam);
return (new FreeFlyCameraController(entity), entity);
}
[Fact]
public void W_Moves_Forward()
{
var (controller, entity) = CreateController(yaw: 0f);
var input = new FakeInputState();
input.SetKeyDown(Key.W);
controller.Update(input, 1.0f);
var cam = entity.Get<Camera>();
Assert.True(cam.Position.Z > -10f);
}
[Fact]
public void S_Moves_Backward()
{
var (controller, entity) = CreateController(yaw: 0f);
var input = new FakeInputState();
input.SetKeyDown(Key.S);
controller.Update(input, 1.0f);
var cam = entity.Get<Camera>();
Assert.True(cam.Position.Z < -10f);
}
[Fact]
public void Shift_Boosts_Speed()
{
var (controller, entity) = CreateController(yaw: 0f);
var inputNormal = new FakeInputState();
inputNormal.SetKeyDown(Key.W);
controller.Update(inputNormal, 1.0f);
var normalPos = entity.Get<Camera>().Position;
var (controller2, entity2) = CreateController(yaw: 0f);
var inputBoost = new FakeInputState();
inputBoost.SetKeyDown(Key.W);
inputBoost.SetKeyDown(Key.LeftShift);
controller2.Update(inputBoost, 1.0f);
var boostPos = entity2.Get<Camera>().Position;
Assert.True(boostPos.Z > normalPos.Z);
}
[Fact]
public void Q_Moves_Down()
{
var (controller, entity) = CreateController();
var input = new FakeInputState();
input.SetKeyDown(Key.Q);
controller.Update(input, 1.0f);
var cam = entity.Get<Camera>();
Assert.True(cam.Position.Y < 1f);
}
[Fact]
public void E_Moves_Up()
{
var (controller, entity) = CreateController();
var input = new FakeInputState();
input.SetKeyDown(Key.E);
controller.Update(input, 1.0f);
var cam = entity.Get<Camera>();
Assert.True(cam.Position.Y > 1f);
}
[Fact]
public void Mouse_Wheel_Adjusts_Speed()
{
var (controller, entity) = CreateController(yaw: 0f);
var input = new FakeInputState();
input.SetKeyDown(Key.W);
input.MouseWheelDelta = 10f;
controller.Update(input, 0.1f);
input.BeginFrame();
input.SetKeyDown(Key.W);
controller.Update(input, 1.0f);
var cam = entity.Get<Camera>();
// With boosted speed, movement should be much larger than default 3 units
Assert.True(cam.Position.Z > -7f);
}
[Fact]
public void Target_Follows_Position()
{
var (controller, entity) = CreateController(yaw: 0f);
var input = new FakeInputState();
input.SetKeyDown(Key.W);
controller.Update(input, 1.0f);
var cam = entity.Get<Camera>();
var dir = Vector3.Normalize(cam.Target - cam.Position);
Assert.Equal(0f, dir.X, 0.01f);
Assert.Equal(0f, dir.Y, 0.01f);
}
}
public class OrbitCameraControllerTests
{
private static (OrbitCameraController, Entity) CreateController()
{
var world = World.Create();
var target = new Vector3(0, 0.5f, 0);
var pos = new Vector3(0, 0.5f, -10);
var cam = new Camera(pos, target, Vector3.UnitY);
var entity = world.Entity("TestOrbitCamera").Set(cam);
return (new OrbitCameraController(entity, target), entity);
}
[Fact]
public void W_Moves_Target_Forward()
{
var (controller, entity) = CreateController();
var input = new FakeInputState();
input.SetKeyDown(Key.W);
controller.Update(input, 1.0f);
var cam = entity.Get<Camera>();
Assert.True(cam.Target.Z > 0f);
}
[Fact]
public void Zoom_Decreases_Distance()
{
var (controller, entity) = CreateController();
var input = new FakeInputState();
input.MouseWheelDelta = 1f;
controller.Update(input, 0.1f);
var cam = entity.Get<Camera>();
var dist = Vector3.Distance(cam.Position, cam.Target);
Assert.True(dist < 10f);
}
[Fact]
public void Camera_Position_Orbits_Target()
{
var (controller, entity) = CreateController();
var input = new FakeInputState();
input.MouseRight = true;
input.MouseX = 100;
input.MouseY = 100;
controller.Update(input, 0.1f);
input.BeginFrame();
input.MouseRight = true;
input.MouseX = 200;
input.MouseY = 100;
controller.Update(input, 0.1f);
var cam = entity.Get<Camera>();
var dist = Vector3.Distance(cam.Position, cam.Target);
Assert.Equal(10f, dist, 1f);
}
[Fact]
public void Target_Stays_At_Ground_Level_With_WASD()
{
var (controller, entity) = CreateController();
var input = new FakeInputState();
input.SetKeyDown(Key.W);
controller.Update(input, 1.0f);
var cam = entity.Get<Camera>();
Assert.Equal(0.5f, cam.Target.Y, 0.001f);
}
}
+65
View File
@@ -0,0 +1,65 @@
using System.Numerics;
using Engine.Core.Components;
namespace Engine.Tests;
public class CameraTests
{
[Fact]
public void View_Matrix_Transforms_Position_To_Origin()
{
var cam = new Camera(
new Vector3(0, 0, 10),
new Vector3(0, 0, 0),
Vector3.UnitY,
MathF.PI / 4f,
16f / 9f,
0.1f,
100f);
var view = cam.GetViewMatrix();
var originInCameraSpace = Vector3.Transform(new Vector3(0, 0, 0), view);
Assert.Equal(0f, originInCameraSpace.X, 0.001f);
Assert.Equal(0f, originInCameraSpace.Y, 0.001f);
Assert.Equal(-10f, originInCameraSpace.Z, 0.001f);
}
[Fact]
public void Projection_Matrix_Has_Correct_Aspect_Ratio()
{
var cam = new Camera(
Vector3.Zero,
Vector3.UnitZ,
Vector3.UnitY,
MathF.PI / 4f,
16f / 9f,
0.1f,
100f);
var proj = cam.GetProjectionMatrix();
Assert.True(proj.M11 > 0);
Assert.True(proj.M22 > 0);
Assert.Equal(0f, proj.M41, 0.001f);
}
[Fact]
public void Default_Material_Has_Expected_Values()
{
var mat = Material.Default;
Assert.Equal(0.5f, mat.Roughness);
Assert.Equal(0.0f, mat.Metallic);
Assert.False(mat.HasTexture);
}
[Fact]
public void Material_With_Texture_Path_Has_Texture_Flag()
{
var mat = new Material(texturePath: "Content/test.png");
Assert.True(mat.HasTexture);
Assert.Equal("Content/test.png", mat.TexturePath);
}
}
+27
View File
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Engine.Core\Engine.Core.csproj" />
<ProjectReference Include="..\..\src\Engine.Graphics\Engine.Graphics.csproj" />
<ProjectReference Include="..\..\src\Engine.AI\Engine.AI.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>
+40
View File
@@ -0,0 +1,40 @@
using System.Numerics;
using Engine.Core;
using Engine.Core.Components;
namespace Engine.Tests;
public class MeshAndLightTests
{
[Fact]
public void Mesh_Stores_Vertices_And_Indices()
{
var vertices = new[]
{
new Vertex(new Vector3(0, 0, 0), Vector3.One, Vector3.UnitY),
new Vertex(new Vector3(1, 0, 0), Vector3.One, Vector3.UnitY),
new Vertex(new Vector3(1, 1, 0), Vector3.One, Vector3.UnitY),
};
var indices = new uint[] { 0, 1, 2 };
var mesh = new Mesh(vertices, indices);
Assert.Equal(3, mesh.Vertices.Length);
Assert.Equal(3, mesh.Indices.Length);
}
[Fact]
public void Light_Direction_Is_Normalized()
{
var light = new Light(new Vector3(0, 2, 0), Vector3.One, 1.0f);
Assert.Equal(1f, light.Direction.Length(), 0.001f);
}
[Fact]
public void Light_With_Zero_Direction_Defaults_To_UnitY()
{
var light = new Light(Vector3.Zero, Vector3.One, 1.0f);
Assert.Equal(Vector3.UnitY, light.Direction);
}
}
@@ -0,0 +1,119 @@
using System.Numerics;
using Engine.Core;
using Engine.Graphics;
namespace Engine.Tests;
public class MeshMathTests
{
[Fact]
public void Computes_Normal_For_CCW_Triangle()
{
var n = MeshMath.ComputeFaceNormal(
new Vector3(0, 0, 0),
new Vector3(1, 0, 0),
new Vector3(0, 1, 0));
Assert.Equal(0f, n.X, 0.001f);
Assert.Equal(0f, n.Y, 0.001f);
Assert.Equal(1f, n.Z, 0.001f);
}
[Fact]
public void Normal_Is_Unit_Length()
{
var n = MeshMath.ComputeFaceNormal(
new Vector3(0, 0, 0),
new Vector3(3, 0, 0),
new Vector3(0, 4, 0));
Assert.Equal(1f, n.Length(), 0.001f);
}
[Fact]
public void Degenerate_Triangle_Falls_Back_To_UnitY()
{
var n = MeshMath.ComputeFaceNormal(
new Vector3(0, 0, 0),
new Vector3(1, 0, 0),
new Vector3(2, 0, 0));
Assert.Equal(Vector3.UnitY, n);
}
}
public class ProceduralMeshTests
{
[Fact]
public void Sphere_Has_Correct_Vertex_Count()
{
var mesh = ProceduralMesh.CreateSphere(1f, 16, 8, Vector3.One);
Assert.Equal((8 + 1) * (16 + 1), mesh.Vertices.Length);
}
[Fact]
public void Sphere_Has_Correct_Index_Count()
{
var mesh = ProceduralMesh.CreateSphere(1f, 16, 8, Vector3.One);
Assert.Equal(8 * 16 * 6, mesh.Indices.Length);
}
[Fact]
public void Sphere_Vertices_Lie_On_Surface()
{
const float radius = 2.5f;
var mesh = ProceduralMesh.CreateSphere(radius, 8, 4, Vector3.One);
foreach (var v in mesh.Vertices)
Assert.Equal(radius, v.Position.Length(), 0.001f);
}
[Fact]
public void Sphere_Normals_Are_Unit_Length()
{
var mesh = ProceduralMesh.CreateSphere(1f, 8, 4, Vector3.One);
foreach (var v in mesh.Vertices)
Assert.Equal(1f, v.Normal.Length(), 0.001f);
}
[Fact]
public void Sphere_Top_Pole_At_Positive_Y()
{
var mesh = ProceduralMesh.CreateSphere(1f, 8, 4, Vector3.One);
Assert.Equal(1f, mesh.Vertices[0].Position.Y, 0.001f);
Assert.Equal(0f, mesh.Vertices[0].Position.X, 0.001f);
Assert.Equal(0f, mesh.Vertices[0].Position.Z, 0.001f);
}
[Fact]
public void Grid_Has_Correct_Vertex_Count()
{
var mesh = ProceduralMesh.CreateGrid(5, 1f, Vector3.One);
var expectedLines = 2 * 5 + 1;
Assert.Equal(expectedLines * 4 * 2, mesh.Vertices.Length);
}
[Fact]
public void Grid_All_Normals_Point_Up()
{
var mesh = ProceduralMesh.CreateGrid(3, 1f, Vector3.One);
foreach (var v in mesh.Vertices)
Assert.Equal(Vector3.UnitY, v.Normal);
}
[Fact]
public void Grid_Extent_Matches_Lines_And_Spacing()
{
var mesh = ProceduralMesh.CreateGrid(10, 2f, Vector3.One);
var maxPos = 10f * 2f;
Assert.True(mesh.Vertices.Any(v => v.Position.X <= -maxPos));
Assert.True(mesh.Vertices.Any(v => v.Position.X >= maxPos));
}
}
+146
View File
@@ -0,0 +1,146 @@
using System.IO;
using System.Numerics;
using Engine.Core;
using Engine.Core.Components;
using Engine.Graphics.Loaders;
namespace Engine.Tests;
public class ObjLoaderTests
{
private static readonly string TempDir = Path.Combine(Path.GetTempPath(), "CortexEngineTests");
private static string WriteTempObj(string content)
{
Directory.CreateDirectory(TempDir);
var path = Path.Combine(TempDir, $"test_{Guid.NewGuid():N}.obj");
File.WriteAllText(path, content);
return path;
}
[Fact]
public void Loads_Single_Triangle()
{
var path = WriteTempObj("""
v 0 0 0
v 1 0 0
v 0 1 0
f 1 2 3
""");
var mesh = ObjLoader.Load(path, new Vector3(1, 1, 1));
Assert.Equal(3, mesh.Vertices.Length);
Assert.Equal(3, mesh.Indices.Length);
}
[Fact]
public void Triangulates_Quad_As_Fan()
{
var path = WriteTempObj("""
v 0 0 0
v 1 0 0
v 1 1 0
v 0 1 0
f 1 2 3 4
""");
var mesh = ObjLoader.Load(path);
Assert.Equal(6, mesh.Vertices.Length);
Assert.Equal(6, mesh.Indices.Length);
}
[Fact]
public void Parses_Face_With_Texcoord_Format()
{
var path = WriteTempObj("""
v 0 0 0
v 1 0 0
v 0 1 0
vt 0 0
vt 1 0
vt 0 1
f 1/1 2/2 3/3
""");
var mesh = ObjLoader.Load(path);
Assert.Equal(3, mesh.Vertices.Length);
}
[Fact]
public void Parses_Face_With_Normal_Format()
{
var path = WriteTempObj("""
v 0 0 0
v 1 0 0
v 0 1 0
vn 0 0 1
f 1//1 2//1 3//1
""");
var mesh = ObjLoader.Load(path);
Assert.Equal(3, mesh.Vertices.Length);
}
[Fact]
public void Computes_Face_Normal_For_Triangle()
{
var path = WriteTempObj("""
v 0 0 0
v 1 0 0
v 0 1 0
f 1 2 3
""");
var mesh = ObjLoader.Load(path);
var normal = mesh.Vertices[0].Normal;
Assert.Equal(0f, normal.X, 0.001f);
Assert.Equal(0f, normal.Y, 0.001f);
Assert.Equal(1f, normal.Z, 0.001f);
}
[Fact]
public void Skips_Comments_And_Blank_Lines()
{
var path = WriteTempObj("""
# This is a comment
v 0 0 0
# Another comment
v 1 0 0
v 0 1 0
f 1 2 3
""");
var mesh = ObjLoader.Load(path);
Assert.Equal(3, mesh.Vertices.Length);
}
[Fact]
public void Throws_On_Empty_File()
{
var path = WriteTempObj("# just a comment\n");
Assert.Throws<InvalidOperationException>(() => ObjLoader.Load(path));
}
[Fact]
public void Default_Color_When_Not_Specified()
{
var path = WriteTempObj("""
v 0 0 0
v 1 0 0
v 0 1 0
f 1 2 3
""");
var mesh = ObjLoader.Load(path);
Assert.Equal(0.7f, mesh.Vertices[0].Color.X, 0.001f);
}
}
@@ -0,0 +1,41 @@
using Engine.Core;
using Engine.Graphics;
namespace Engine.Tests;
public class RenderBackendFactoryTests
{
private sealed class FakeRenderContext : IRenderContext
{
public IWindow Window => null!;
public IRenderer CreateRenderer() => null!;
public void Resize(int width, int height) { }
public void Dispose() { }
}
[Fact]
public void Create_Returns_Registered_Backend()
{
RenderBackendFactory.Register("fake-test", (_, _, _) => new FakeRenderContext());
var ctx = RenderBackendFactory.Create("fake-test", 800, 600, false);
Assert.IsType<FakeRenderContext>(ctx);
}
[Fact]
public void Create_Throws_For_Unknown_Backend()
{
Assert.Throws<NotSupportedException>(() =>
RenderBackendFactory.Create("nonexistent", 800, 600, false));
}
[Fact]
public void Register_Is_Case_Insensitive()
{
RenderBackendFactory.Register("CaseTest", (_, _, _) => new FakeRenderContext());
var ctx = RenderBackendFactory.Create("casetest", 1, 1, false);
Assert.IsType<FakeRenderContext>(ctx);
}
}
+65
View File
@@ -0,0 +1,65 @@
using Engine.Core;
namespace Engine.Tests;
public class TimingTests
{
[Fact]
public void Tick_Updates_DeltaTime()
{
var timing = new Timing();
timing.Tick();
Assert.True(timing.DeltaTime > 0);
}
[Fact]
public void Tick_Updates_TotalTime()
{
var timing = new Timing();
timing.Tick();
var time1 = timing.TotalTime;
timing.Tick();
var time2 = timing.TotalTime;
Assert.True(time2 > time1);
}
[Fact]
public void ConsumeFixedStep_Returns_True_When_Accumulator_Exceeds_Step()
{
var timing = new Timing { FixedTimeStep = 0.001 };
timing.Tick();
Thread.Sleep(5);
timing.Tick();
Assert.True(timing.ConsumeFixedStep());
}
[Fact]
public void ConsumeFixedStep_Returns_False_When_Below_Step()
{
var timing = new Timing { FixedTimeStep = 100.0 };
timing.Tick();
Assert.False(timing.ConsumeFixedStep());
}
[Fact]
public void ResetAccumulator_Clamps_To_Max()
{
var timing = new Timing { FixedTimeStep = 0.1 };
// Simulate a huge delta by ticking many times without consuming
for (var i = 0; i < 1000; i++)
timing.Tick();
timing.ResetAccumulator();
Assert.True(timing.FixedTimeAccumulator <= timing.FixedTimeStep * 5 + 0.001f);
}
}
+52
View File
@@ -0,0 +1,52 @@
using System.Numerics;
using Engine.Core.Components;
namespace Engine.Tests;
public class TransformTests
{
[Fact]
public void Identity_Transform_Produces_Identity_Matrix()
{
var t = new Transform(Vector3.Zero, Quaternion.Identity, Vector3.One);
var m = t.GetMatrix();
Assert.Equal(Matrix4x4.Identity, m);
}
[Fact]
public void Translation_Appears_In_Matrix()
{
var t = new Transform(new Vector3(1, 2, 3), Quaternion.Identity, Vector3.One);
var m = t.GetMatrix();
Assert.Equal(1f, m.M41);
Assert.Equal(2f, m.M42);
Assert.Equal(3f, m.M43);
}
[Fact]
public void Scale_Affects_Matrix_Diagonal()
{
var t = new Transform(Vector3.Zero, Quaternion.Identity, new Vector3(2, 3, 4));
var m = t.GetMatrix();
Assert.Equal(2f, m.M11);
Assert.Equal(3f, m.M22);
Assert.Equal(4f, m.M33);
}
[Fact]
public void Rotation_Around_Y_Rotates_X_Axis()
{
var angle = MathF.PI / 2f;
var rot = Quaternion.CreateFromAxisAngle(Vector3.UnitY, angle);
var t = new Transform(Vector3.Zero, rot, Vector3.One);
var m = t.GetMatrix();
var xAxis = new Vector3(m.M11, m.M21, m.M31);
Assert.Equal(0f, xAxis.X, 0.001f);
Assert.Equal(0f, xAxis.Y, 0.001f);
Assert.Equal(1f, xAxis.Z, 0.001f);
}
}