From dd105ea4af47256e4c1a6b8dff33486a0f19a362 Mon Sep 17 00:00:00 2001 From: emil28092005 Date: Wed, 17 Jun 2026 22:12:51 +0300 Subject: [PATCH] =?UTF-8?q?chore:=20remove=20all=20graphics=20backends=20(?= =?UTF-8?q?Raylib,=20OpenTK,=20Vulkan/Silk.NET)=20=E2=80=94=20prepare=20fo?= =?UTF-8?q?r=20pure=20Vulkan=20P/Invoke=20rewrite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VULKAN_IMPLEMENTATION_PLAN.md | 257 +++++++ .../Engine.Graphics.OpenTK.csproj | 21 - .../OpenTKBackendRegistrar.cs | 16 - .../OpenTKInputState.cs | 104 --- .../OpenTKRenderContext.cs | 47 -- src/Engine.Graphics.OpenTK/OpenTKRenderer.cs | 495 ------------ src/Engine.Graphics.OpenTK/OpenTKWindow.cs | 52 -- .../Engine.Graphics.Raylib.csproj | 34 - src/Engine.Graphics.Raylib/ImGuiLayer.cs | 335 -------- .../RaylibBackendRegistrar.cs | 20 - .../RaylibInputState.cs | 156 ---- .../RaylibRenderContext.cs | 28 - src/Engine.Graphics.Raylib/RaylibRenderer.cs | 723 ------------------ src/Engine.Graphics.Raylib/RaylibWindow.cs | 54 -- .../Engine.Graphics.Vulkan.csproj | 40 - src/Engine.Graphics.Vulkan/IndexBuffer.cs | 113 --- .../ScreenshotCapture.cs | 310 -------- src/Engine.Graphics.Vulkan/ShaderLoader.cs | 24 - .../Shaders/fragment.frag | 69 -- .../Shaders/fragment.spv | Bin 4804 -> 0 bytes src/Engine.Graphics.Vulkan/Shaders/vertex.spv | Bin 2652 -> 0 bytes .../Shaders/vertex.vert | 48 -- src/Engine.Graphics.Vulkan/Swapchain.cs | 446 ----------- src/Engine.Graphics.Vulkan/Texture.cs | 330 -------- src/Engine.Graphics.Vulkan/UniformBuffer.cs | 110 --- src/Engine.Graphics.Vulkan/VertexBuffer.cs | 119 --- .../VulkanBackendRegistrar.cs | 20 - src/Engine.Graphics.Vulkan/VulkanContext.cs | 321 -------- src/Engine.Graphics.Vulkan/VulkanPipeline.cs | 306 -------- .../VulkanRenderContext.cs | 35 - src/Engine.Graphics.Vulkan/VulkanRenderer.cs | 673 ---------------- src/Engine.Graphics/Engine.Graphics.csproj | 30 - src/Engine.Graphics/IRenderContext.cs | 27 - src/Engine.Graphics/IRenderer.cs | 31 - src/Engine.Graphics/Loaders/GltfLoader.cs | 196 ----- src/Engine.Graphics/Loaders/ObjLoader.cs | 88 --- src/Engine.Graphics/MeshMath.cs | 26 - src/Engine.Graphics/ProceduralMesh.cs | 101 --- src/Engine.Graphics/RenderBackendFactory.cs | 36 - src/Engine.Graphics/SceneSerializer.cs | 300 -------- 40 files changed, 257 insertions(+), 5884 deletions(-) create mode 100644 VULKAN_IMPLEMENTATION_PLAN.md delete mode 100644 src/Engine.Graphics.OpenTK/Engine.Graphics.OpenTK.csproj delete mode 100644 src/Engine.Graphics.OpenTK/OpenTKBackendRegistrar.cs delete mode 100644 src/Engine.Graphics.OpenTK/OpenTKInputState.cs delete mode 100644 src/Engine.Graphics.OpenTK/OpenTKRenderContext.cs delete mode 100644 src/Engine.Graphics.OpenTK/OpenTKRenderer.cs delete mode 100644 src/Engine.Graphics.OpenTK/OpenTKWindow.cs delete mode 100644 src/Engine.Graphics.Raylib/Engine.Graphics.Raylib.csproj delete mode 100644 src/Engine.Graphics.Raylib/ImGuiLayer.cs delete mode 100644 src/Engine.Graphics.Raylib/RaylibBackendRegistrar.cs delete mode 100644 src/Engine.Graphics.Raylib/RaylibInputState.cs delete mode 100644 src/Engine.Graphics.Raylib/RaylibRenderContext.cs delete mode 100644 src/Engine.Graphics.Raylib/RaylibRenderer.cs delete mode 100644 src/Engine.Graphics.Raylib/RaylibWindow.cs delete mode 100644 src/Engine.Graphics.Vulkan/Engine.Graphics.Vulkan.csproj delete mode 100644 src/Engine.Graphics.Vulkan/IndexBuffer.cs delete mode 100644 src/Engine.Graphics.Vulkan/ScreenshotCapture.cs delete mode 100644 src/Engine.Graphics.Vulkan/ShaderLoader.cs delete mode 100644 src/Engine.Graphics.Vulkan/Shaders/fragment.frag delete mode 100644 src/Engine.Graphics.Vulkan/Shaders/fragment.spv delete mode 100644 src/Engine.Graphics.Vulkan/Shaders/vertex.spv delete mode 100644 src/Engine.Graphics.Vulkan/Shaders/vertex.vert delete mode 100644 src/Engine.Graphics.Vulkan/Swapchain.cs delete mode 100644 src/Engine.Graphics.Vulkan/Texture.cs delete mode 100644 src/Engine.Graphics.Vulkan/UniformBuffer.cs delete mode 100644 src/Engine.Graphics.Vulkan/VertexBuffer.cs delete mode 100644 src/Engine.Graphics.Vulkan/VulkanBackendRegistrar.cs delete mode 100644 src/Engine.Graphics.Vulkan/VulkanContext.cs delete mode 100644 src/Engine.Graphics.Vulkan/VulkanPipeline.cs delete mode 100644 src/Engine.Graphics.Vulkan/VulkanRenderContext.cs delete mode 100644 src/Engine.Graphics.Vulkan/VulkanRenderer.cs delete mode 100644 src/Engine.Graphics/Engine.Graphics.csproj delete mode 100644 src/Engine.Graphics/IRenderContext.cs delete mode 100644 src/Engine.Graphics/IRenderer.cs delete mode 100644 src/Engine.Graphics/Loaders/GltfLoader.cs delete mode 100644 src/Engine.Graphics/Loaders/ObjLoader.cs delete mode 100644 src/Engine.Graphics/MeshMath.cs delete mode 100644 src/Engine.Graphics/ProceduralMesh.cs delete mode 100644 src/Engine.Graphics/RenderBackendFactory.cs delete mode 100644 src/Engine.Graphics/SceneSerializer.cs diff --git a/VULKAN_IMPLEMENTATION_PLAN.md b/VULKAN_IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..a811228 --- /dev/null +++ b/VULKAN_IMPLEMENTATION_PLAN.md @@ -0,0 +1,257 @@ +# CORTEX ENGINE — VULKAN RENDERER IMPLEMENTATION PLAN + +## Project State (June 2026) + +### What Exists +- **Engine.Core** — Sdl3Window (SDL3, Vulkan surface ready), IWindow, IInputState, Key enum, InputMapping, + camera controllers (FreeFly, Orbit), components (Transform, Mesh, Material, Light, Camera, RigidBody), + Vertex struct (Position, Color, Normal — 9 floats), Timing, IScreenshotProvider +- **Engine.Physics** — JoltPhysicsSharp 2.21.0, PhysicsWorld wrapper, RigidBody component +- **Engine.AI** — AiCommandProcessor (7 commands), MCP HTTP + stdio servers, AiCommandQueue +- **CortexEngine.App** — main loop (broken, references deleted graphics projects) +- **tests/Engine.Tests** — 66 tests (broken, reference Engine.Graphics) +- **Content/** — cube.obj, torusknot.obj, checker.png + +### What Was Deleted +- Engine.Graphics (interfaces + loaders + factory) +- Engine.Graphics.Raylib +- Engine.Graphics.OpenTK +- Engine.Graphics.Vulkan (Silk.NET version) + +### Environment +- .NET 9 SDK at $HOME/.dotnet +- Vulkan 1.4.329, NVIDIA RTX 2080 Ti, validation layers available +- SDL3 (ppy.SDL3-CS 2026.520.0) — window + Vulkan surface +- glslangValidator NOT installed (need: sudo apt install glslang-tools) +- Linux (X11), cross-platform target (Windows: vulkan-1.dll, Linux: libvulkan.so.1) + +### Key Architecture Decisions +- NO wrapper libraries (no Silk.NET, no Vortice, no OpenTK for Vulkan) +- Pure P/Invoke to libvulkan.so.1 / vulkan-1.dll +- SDL3 for windowing (Sdl3Window already works, creates Vulkan surface) +- ImGui planned (later phase) +- Shadow mapping planned (Vulkan gives full control) +- Validation layers for debugging + +## Implementation Plan + +### Phase 1: Restore Engine.Graphics (interfaces + loaders) + +Create `src/Engine.Graphics/` with: +- `IRenderContext.cs` — interface: Window, CreateRenderer(), Resize(), Dispose +- `IRenderer.cs` — interface: RenderWorld(World), RequestScreenshot, IsScreenshotRequested, ScreenshotProvider, Dispose +- `RenderBackendFactory.cs` — static registry: Register(name, factory), Create(name, w, h, validation) +- `IScreenshotProvider.cs` already in Engine.Core +- `Loaders/ObjLoader.cs` — parse .obj files → Mesh component +- `Loaders/GltfLoader.cs` — parse .gltf/.glb → Mesh component +- `MeshMath.cs` — ComputeFaceNormal(a, b, c) +- `ProceduralMesh.cs` — CreateSphere(), CreateGrid() +- `SceneSerializer.cs` — save/load ECS world to JSON + +csproj: references Engine.Core, Flecs.NET, SharpGLTF.Core + +### Phase 2: Vulkan P/Invoke Layer + +Create `src/Engine.Graphics.Vulkan/` with: +- `VulkanNative.cs` — all P/Invoke declarations: + - Library loading: `const string VulkanLib = OperatingSystem.IsWindows() ? "vulkan-1.dll" : "libvulkan.so.1"` + - ~80 Vulkan functions (vkCreateInstance through vkQueuePresentKHR) + - ~50 structs (InstanceCreateInfo, DeviceCreateInfo, SwapchainCreateInfoKHR, etc.) + - ~20 enums (Result, Format, ImageLayout, PipelineStageFlags, etc.) + - Extension function loading via vkGetInstanceProcAddr/vkGetDeviceProcAddr + - SDL_Vulkan_CreateSurface via SDL3 (already in Sdl3Window) + +- `VulkanContext.cs` — Instance + PhysicalDevice + Device + Queues + Surface: + - CreateInstance with SDL3 extensions + validation layers + - PickPhysicalDevice (prefer discrete GPU) + - CreateLogicalDevice with VK_KHR_swapchain + - CreateSurface via SDL_Vulkan_CreateSurface + - Get graphics + present queues + +- `VulkanSwapchain.cs` — Swapchain + image views + depth + render pass + framebuffers: + - Query surface capabilities + - Create swapchain (format, extent, present mode) + - Create image views + - Create depth image + view (D32_SFLOAT) + - Create render pass (color + depth attachments) + - Create framebuffers + +- `VulkanPipeline.cs` — Graphics pipeline: + - Load SPIR-V shader modules (vertex + fragment) + - Vertex input description (Position vec3, Normal vec3, Color vec4) + - Descriptor set layouts (frame UBO + texture sampler) + - Pipeline layout + graphics pipeline + - Push constants for MVP matrix + material params + +- `VulkanBuffer.cs` — Buffer management: + - CreateBuffer (vertex/index/uniform) + - AllocateMemory + bind + - Map/unmap for writing + - FindMemoryType + - Staging buffer for copy + +- `VulkanRenderer.cs` — Render loop: + - Command pool + command buffers (2 frames in flight) + - Semaphores + fences for sync + - Descriptor pools + sets + - RenderWorld(World): + - Get camera from ECS + - Collect lights from ECS + - Update frame UBO (camera pos, lights) + - For each Mesh+Transform entity: upload/cache vertex+index buffers, + set push constants (MVP + material), draw indexed + - Screenshot capture (copy image to staging buffer → PNG) + - Present + +- `VulkanBackendRegistrar.cs` — Register("vulkan", factory) + +csproj: references Engine.Core, Engine.Graphics, Flecs.NET +NO external Vulkan packages — pure P/Invoke + +### Phase 3: Shaders + +GLSL → SPIR-V shaders (compiled with glslangValidator): +- `Shaders/vertex.vert` — #version 450, position/normal/color inputs, MVP+model uniforms, outputs +- `Shaders/fragment.frag` — #version 450, PBR lighting (Fresnel, ACES, gamma), directional + point lights +- Compile: `glslangValidator -V vertex.vert -o vertex.spv && glslangValidator -V fragment.frag -o fragment.spv` +- Embed .spv files as project resources or copy to output directory + +### Phase 4: App Integration + +- Fix `CortexEngine.App.csproj` — remove deleted project refs, add Engine.Graphics + Engine.Graphics.Vulkan +- Fix `Program.cs`: + - Remove all Raylib/OpenTK/old OpenGL imports + - Remove ImGuiLayer, ObjectManipulator (Raylib-specific, will reimplement later) + - Use `RenderBackendFactory.Create("vulkan", 1280, 720, enableValidation: true)` + - Keep: physics, camera controllers, AI commands, tour mode, scene setup + - Sdl3Window creates Vulkan surface automatically (vulkanSurface: true) + +### Phase 5: Fix Tests + +- Update `Engine.Tests.csproj` — reference restored Engine.Graphics +- Tests that reference Engine.Graphics: ObjLoaderTests, RenderBackendFactoryTests, + SceneSerializerTests, MeshMathAndProceduralTests +- All 66 tests should pass after Engine.Graphics is restored + +### Phase 6: ImGui (later) + +- ImGui.NET NuGet + Vulkan ImGui backend +- ImGui_ImplVulkan for rendering +- Entity inspector, hierarchy, debug overlay + +### Phase 7: Shadow Mapping (later) + +- Depth-only render pass from light's POV +- Shadow image (depth texture, 2048x2048) +- Shadow matrix (lightViewProj) in push constants +- PCF sampling in fragment shader + +## Cross-Platform Notes + +- Vulkan P/Invoke: only difference is library name (vulkan-1.dll vs libvulkan.so.1) +- SDL3: already cross-platform (ppy.SDL3-CS) +- SPIR-V: binary format, works everywhere +- .NET 9: NativeLibrary.Load for dynamic resolution if needed + +## File Layout + +``` +src/ +├── Engine.Core/ (exists, unchanged) +├── Engine.Graphics/ (new — interfaces + loaders) +│ ├── Engine.Graphics.csproj +│ ├── IRenderContext.cs +│ ├── IRenderer.cs +│ ├── RenderBackendFactory.cs +│ ├── MeshMath.cs +│ ├── ProceduralMesh.cs +│ ├── SceneSerializer.cs +│ └── Loaders/ +│ ├── ObjLoader.cs +│ └── GltfLoader.cs +├── Engine.Graphics.Vulkan/ (new — pure Vulkan P/Invoke) +│ ├── Engine.Graphics.Vulkan.csproj +│ ├── VulkanNative.cs (~800 lines) +│ ├── VulkanContext.cs (~300 lines) +│ ├── VulkanSwapchain.cs (~250 lines) +│ ├── VulkanPipeline.cs (~200 lines) +│ ├── VulkanBuffer.cs (~150 lines) +│ ├── VulkanRenderer.cs (~400 lines) +│ ├── VulkanBackendRegistrar.cs +│ └── Shaders/ +│ ├── vertex.vert +│ ├── fragment.frag +│ ├── vertex.spv +│ └── fragment.spv +├── Engine.Physics/ (exists, unchanged) +├── Engine.AI/ (exists, unchanged) +└── CortexEngine.App/ (fix references) +``` + +## Solution Update + +Remove from solution: +- Engine.Graphics.Raylib (deleted) +- Engine.Graphics.OpenTK (deleted) +- Engine.Graphics.Vulkan (old Silk.NET, deleted) + +Add to solution: +- Engine.Graphics (new) +- Engine.Graphics.Vulkan (new, pure P/Invoke) + +## Key Technical Details + +### Matrix Layout +- System.Numerics.Matrix4x4 is row-major +- Vulkan expects column-major in shaders (layout(row_major) or transpose) +- Solution: use `layout(row_major) uniform mat4` in GLSL → no transpose needed +- OR transpose in C# before writing to uniform buffer + +### Vertex Layout +``` +Vertex struct (9 floats, 36 bytes): + Position: vec3 (offset 0) + Color: vec3 (offset 12) + Normal: vec3 (offset 24) +``` + +### Push Constants (96 bytes max) +``` +offset 0: mat4 MVP (64 bytes) +offset 64: vec3 materialAlbedo + float roughness (16 bytes) +offset 80: float metallic + uint useTexture + uint pad + uint pad (16 bytes) +``` + +### Frame UBO (224 bytes) +``` +offset 0: vec3 cameraPosition + uint lightCount (16 bytes) +offset 16: vec3 ambientColor + float pad (16 bytes) +offset 32: Light[4] — each 48 bytes (vec3 direction + float intensity + vec3 color + float pad) +``` + +### Light Struct (48 bytes) +``` +vec3 direction (12 bytes) +float intensity (4 bytes) +vec3 color (12 bytes) +float padding (4 bytes) +``` + +### Validation Layers +```csharp +string[] layers = enableValidation + ? new[] { "VK_LAYER_KHRONOS_validation" } + : Array.Empty(); +``` +Validation errors print to stderr — use for debugging. + +### Memory Allocation +Simple approach (no VMA): +1. vkGetPhysicalDeviceMemoryProperties +2. Find memory type with VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | HOST_COHERENT_BIT +3. vkAllocateMemory + vkBindBufferMemory +4. vkMapMemory for writing, vkUnmapMemory + +For GPU-only buffers (vertex/index): +1. Find memory type with DEVICE_LOCAL_BIT +2. Use staging buffer (host visible) + vkCmdCopyBuffer diff --git a/src/Engine.Graphics.OpenTK/Engine.Graphics.OpenTK.csproj b/src/Engine.Graphics.OpenTK/Engine.Graphics.OpenTK.csproj deleted file mode 100644 index 8439a8d..0000000 --- a/src/Engine.Graphics.OpenTK/Engine.Graphics.OpenTK.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - net9.0 - enable - enable - true - - - - - - - - - - - - - - diff --git a/src/Engine.Graphics.OpenTK/OpenTKBackendRegistrar.cs b/src/Engine.Graphics.OpenTK/OpenTKBackendRegistrar.cs deleted file mode 100644 index 052bf07..0000000 --- a/src/Engine.Graphics.OpenTK/OpenTKBackendRegistrar.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Engine.Graphics; - -namespace Engine.Graphics.OpenTK; - -/// -/// Triggers registration of the OpenTK backend with the HAL factory. -/// -public static class OpenTKBackendRegistrar -{ - static OpenTKBackendRegistrar() - { - RenderBackendFactory.Register("opentk", (width, height, _) => new OpenTKRenderContext(width, height)); - } - - public static void EnsureRegistered() { } -} diff --git a/src/Engine.Graphics.OpenTK/OpenTKInputState.cs b/src/Engine.Graphics.OpenTK/OpenTKInputState.cs deleted file mode 100644 index abf486c..0000000 --- a/src/Engine.Graphics.OpenTK/OpenTKInputState.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System.Collections.Generic; -using Engine.Core; -using OpenTK.Windowing.GraphicsLibraryFramework; -using EngineKey = Engine.Core.Key; -using OpenTKKey = OpenTK.Windowing.GraphicsLibraryFramework.Keys; - -namespace Engine.Graphics.OpenTK; - -/// -/// OpenTK input-backed implementation of IInputState. -/// -public sealed class OpenTKInputState : IInputState -{ - private readonly HashSet _keysDown = new(); - private readonly HashSet _keysPressed = new(); - private readonly HashSet _keysReleased = new(); - private readonly HashSet _prevDown = new(); - - public int MouseX { get; private set; } - public int MouseY { get; private set; } - public bool MouseLeft { get; private set; } - public bool MouseRight { get; private set; } - public bool MouseMiddle { get; private set; } - public float MouseWheelDelta { get; private set; } - - public void Update(KeyboardState kb, MouseState ms) - { - MouseX = (int)ms.Position.X; - MouseY = (int)ms.Position.Y; - MouseLeft = ms.IsButtonDown(MouseButton.Left); - MouseRight = ms.IsButtonDown(MouseButton.Right); - MouseMiddle = ms.IsButtonDown(MouseButton.Middle); - MouseWheelDelta = (float)ms.ScrollDelta.Y; - - // Compute pressed/released edges - _keysPressed.Clear(); - _keysReleased.Clear(); - - var currentKeys = new HashSet(); - foreach (var openTkKey in AllKeys) - { - if (kb.IsKeyDown(openTkKey)) - { - var k = MapKey(openTkKey); - if (k == EngineKey.Unknown) continue; - currentKeys.Add(k); - if (!_prevDown.Contains(k)) - _keysPressed.Add(k); - } - } - - foreach (var k in _prevDown) - if (!currentKeys.Contains(k)) - _keysReleased.Add(k); - - _keysDown.Clear(); - _keysDown.UnionWith(currentKeys); - _prevDown.Clear(); - _prevDown.UnionWith(currentKeys); - } - - public void BeginFrame() - { - _keysPressed.Clear(); - _keysReleased.Clear(); - MouseWheelDelta = 0; - } - - public bool IsKeyDown(EngineKey key) => _keysDown.Contains(key); - public bool IsKeyPressed(EngineKey key) => _keysPressed.Contains(key); - public bool IsKeyReleased(EngineKey key) => _keysReleased.Contains(key); - - private static readonly OpenTKKey[] AllKeys = - { - OpenTKKey.W, OpenTKKey.A, OpenTKKey.S, OpenTKKey.D, - OpenTKKey.Q, OpenTKKey.E, OpenTKKey.F, - OpenTKKey.LeftShift, - OpenTKKey.Space, OpenTKKey.Escape, OpenTKKey.Enter, - OpenTKKey.Tab, OpenTKKey.Backspace, - OpenTKKey.Up, OpenTKKey.Down, OpenTKKey.Left, OpenTKKey.Right, - }; - - private static EngineKey MapKey(OpenTKKey key) => key switch - { - OpenTKKey.W => EngineKey.W, - OpenTKKey.A => EngineKey.A, - OpenTKKey.S => EngineKey.S, - OpenTKKey.D => EngineKey.D, - OpenTKKey.Q => EngineKey.Q, - OpenTKKey.E => EngineKey.E, - OpenTKKey.F => EngineKey.F, - OpenTKKey.LeftShift => EngineKey.LeftShift, - OpenTKKey.Space => EngineKey.Space, - OpenTKKey.Escape => EngineKey.Escape, - OpenTKKey.Enter => EngineKey.Enter, - OpenTKKey.Tab => EngineKey.Tab, - OpenTKKey.Backspace => EngineKey.Backspace, - OpenTKKey.Up => EngineKey.Up, - OpenTKKey.Down => EngineKey.Down, - OpenTKKey.Left => EngineKey.Left, - OpenTKKey.Right => EngineKey.Right, - _ => EngineKey.Unknown, - }; -} diff --git a/src/Engine.Graphics.OpenTK/OpenTKRenderContext.cs b/src/Engine.Graphics.OpenTK/OpenTKRenderContext.cs deleted file mode 100644 index 0ea43a9..0000000 --- a/src/Engine.Graphics.OpenTK/OpenTKRenderContext.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Engine.Core; -using Engine.Graphics; - -namespace Engine.Graphics.OpenTK; - -/// -/// OpenTK render context — owns the GameWindow and creates the renderer. -/// -public sealed class OpenTKRenderContext : IRenderContext -{ - private readonly OpenTKWindow _window; - private OpenTKRenderer? _renderer; - private bool _disposed; - - public IWindow Window => _window; - - public OpenTKRenderContext(int width, int height, bool enableValidation = false) - { - _window = new OpenTKWindow("Cortex Engine (OpenTK)", width, height); - // GameWindow creates OpenGL context in constructor — MakeCurrent is called automatically - _window.MakeCurrent(); - } - - public IRenderer CreateRenderer() - { - _renderer = new OpenTKRenderer(); - return _renderer; - } - - public void Resize(int width, int height) - { - _renderer?.SetScreenSize(width, height); - } - - /// - /// Swap buffers — called after RenderWorld in the main loop. - /// - public void SwapBuffers() => _window.SwapBuffers(); - - public void Dispose() - { - if (_disposed) return; - _disposed = true; - _renderer?.Dispose(); - _window.Dispose(); - } -} diff --git a/src/Engine.Graphics.OpenTK/OpenTKRenderer.cs b/src/Engine.Graphics.OpenTK/OpenTKRenderer.cs deleted file mode 100644 index dd373ec..0000000 --- a/src/Engine.Graphics.OpenTK/OpenTKRenderer.cs +++ /dev/null @@ -1,495 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Numerics; -using Engine.Core; -using Engine.Core.Components; -using Engine.Graphics; -using Flecs.NET.Core; -using OpenTK.Graphics.OpenGL4; -using OTKMatrix = OpenTK.Mathematics.Matrix4; -using OTKVector3 = OpenTK.Mathematics.Vector3; -using EngineMaterial = Engine.Core.Components.Material; -using EngineMesh = Engine.Core.Components.Mesh; -using EngineTransform = Engine.Core.Components.Transform; - -namespace Engine.Graphics.OpenTK; - -public sealed class OpenTKRenderer : IRenderer -{ - private const int ShadowMapSize = 2048; - - private int _program; - private int _shadowProgram; - private int _shadowFbo; - private int _shadowTexture; - private readonly Dictionary _meshCache = new(); - - private readonly float[] _matrixBuf = new float[16]; - - // Uniform locations - private int _uMVP, _uModel, _uViewPos, _uMaterialColor, _uRoughness, _uMetallic; - private int _uAmbient, _uLightCount, _uLightDirs, _uLightIntensities, _uLightColors; - private int _uLightPositions, _uLightTypes, _uLightRanges; - private int _uLightViewProj, _uShadowMap, _uUseTexture; - - // Light data - private readonly float[] _lightDirs = new float[12]; - private readonly float[] _lightPositions = new float[12]; - private readonly float[] _lightIntensities = new float[4]; - private readonly float[] _lightColors = new float[12]; - private readonly int[] _lightTypes = new int[4]; - private readonly float[] _lightRanges = new float[4]; - private int _lightCount; - - private int _screenW = 1280, _screenH = 720; - private bool _disposed; - - public OpenTKRenderer() - { - // Compile shaders - _program = CreateProgram(VertexSrc, FragmentSrc); - _shadowProgram = CreateProgram(ShadowVertSrc, ShadowFragSrc); - - // Get uniform locations - _uMVP = GL.GetUniformLocation(_program, "mvp"); - _uModel = GL.GetUniformLocation(_program, "model"); - _uViewPos = GL.GetUniformLocation(_program, "viewPos"); - _uMaterialColor = GL.GetUniformLocation(_program, "materialColor"); - _uRoughness = GL.GetUniformLocation(_program, "roughness"); - _uMetallic = GL.GetUniformLocation(_program, "metallic"); - _uAmbient = GL.GetUniformLocation(_program, "ambientColor"); - _uLightCount = GL.GetUniformLocation(_program, "lightCount"); - _uLightDirs = GL.GetUniformLocation(_program, "lightDirs"); - _uLightIntensities = GL.GetUniformLocation(_program, "lightIntensities"); - _uLightColors = GL.GetUniformLocation(_program, "lightColors"); - _uLightPositions = GL.GetUniformLocation(_program, "lightPositions"); - _uLightTypes = GL.GetUniformLocation(_program, "lightTypes"); - _uLightRanges = GL.GetUniformLocation(_program, "lightRanges"); - _uLightViewProj = GL.GetUniformLocation(_program, "lightViewProj"); - _uShadowMap = GL.GetUniformLocation(_program, "shadowMap"); - _uUseTexture = GL.GetUniformLocation(_program, "useTexture"); - - // Shadow FBO - _shadowFbo = GL.GenFramebuffer(); - GL.BindFramebuffer(FramebufferTarget.Framebuffer, _shadowFbo); - - _shadowTexture = GL.GenTexture(); - GL.BindTexture(TextureTarget.Texture2D, _shadowTexture); - GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.DepthComponent, - ShadowMapSize, ShadowMapSize, 0, PixelFormat.DepthComponent, PixelType.Float, IntPtr.Zero); - GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest); - GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest); - GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge); - GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge); - GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.DepthAttachment, - TextureTarget.Texture2D, _shadowTexture, 0); - GL.DrawBuffer(DrawBufferMode.None); - GL.ReadBuffer(ReadBufferMode.None); - GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0); - - // GL state - GL.Enable(EnableCap.DepthTest); - GL.Disable(EnableCap.CullFace); - } - - public void SetScreenSize(int w, int h) { _screenW = w; _screenH = h; } - - public void RequestScreenshot(string path) { } - public bool IsScreenshotRequested => false; - public IScreenshotProvider ScreenshotProvider => new DummyScreenshotProvider(); - - public void RenderWorld(World world) - { - var camera = GetCamera(world); - CollectLights(world); - - var hasDirLight = _lightCount > 0 && _lightTypes[0] == (int)LightType.Directional; - OTKMatrix lightVP = OTKMatrix.Identity; - - // === PASS 1: Shadow === - if (hasDirLight) - { - var lightDir = new Vector3(_lightDirs[0], _lightDirs[1], _lightDirs[2]); - var center = new Vector3(0, 0.5f, 0); - var lightPos = center - lightDir * 30f; - var up = MathF.Abs(Vector3.Dot(lightDir, Vector3.UnitY)) > 0.99f ? Vector3.UnitZ : Vector3.UnitY; - - lightVP = OTKMatrix.CreateOrthographicOffCenter(-15, 15, -15, 15, 1, 80) - * OTKMatrix.LookAt(ToV3(lightPos), ToV3(center), ToV3(up)); - - GL.Viewport(0, 0, ShadowMapSize, ShadowMapSize); - GL.BindFramebuffer(FramebufferTarget.Framebuffer, _shadowFbo); - GL.Clear(ClearBufferMask.DepthBufferBit); - GL.UseProgram(_shadowProgram); - GL.CullFace(CullFaceMode.Front); - - int sMvp = GL.GetUniformLocation(_shadowProgram, "mvp"); - - world.Each((Entity e, ref EngineMesh mesh, ref EngineTransform t) => - { - if (e.Name() == "Grid" || e.Name() == "Floor") return; - var gm = GetOrUploadMesh(e, mesh); - var model = ToM4(t.GetMatrix()); - var mvp = lightVP * model; - SetUniformMat4(sMvp, mvp); - DrawMesh(gm); - }); - - GL.CullFace(CullFaceMode.Back); - GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0); - } - - // === PASS 2: Main === - GL.Viewport(0, 0, _screenW, _screenH); - GL.ClearColor(0.098f, 0.118f, 0.157f, 1f); - GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); - GL.UseProgram(_program); - - var view = OTKMatrix.LookAt(ToV3(camera.Position), ToV3(camera.Target), ToV3(camera.Up)); - var proj = OTKMatrix.CreatePerspectiveFieldOfView(camera.FieldOfView, camera.AspectRatio, camera.NearPlane, camera.FarPlane); - - GL.Uniform3(_uViewPos, camera.Position.X, camera.Position.Y, camera.Position.Z); - GL.Uniform3(_uAmbient, 0.35f, 0.35f, 0.4f); - GL.Uniform1(_uLightCount, _lightCount); - GL.Uniform3(_uLightDirs, 4, _lightDirs); - GL.Uniform1(_uLightIntensities, 4, _lightIntensities); - GL.Uniform3(_uLightColors, 4, _lightColors); - GL.Uniform3(_uLightPositions, 4, _lightPositions); - GL.Uniform1(_uLightTypes, 4, _lightTypes); - GL.Uniform1(_uLightRanges, 4, _lightRanges); - - if (hasDirLight) - { - SetUniformMat4(_uLightViewProj, lightVP); - GL.ActiveTexture(TextureUnit.Texture1); - GL.BindTexture(TextureTarget.Texture2D, _shadowTexture); - GL.Uniform1(_uShadowMap, 1); - GL.ActiveTexture(TextureUnit.Texture0); - } - - world.Each((Entity e, ref EngineMesh mesh, ref EngineTransform t) => - { - if (e.Name() == "Grid") return; - var mat = e.Has() ? e.Get() : EngineMaterial.Default; - var gm = GetOrUploadMesh(e, mesh); - var model = ToM4(t.GetMatrix()); - var mvp = proj * view * model; - - SetUniformMat4(_uMVP, mvp); - SetUniformMat4(_uModel, model); - GL.Uniform4(_uMaterialColor, mat.Albedo.X, mat.Albedo.Y, mat.Albedo.Z, 1f); - GL.Uniform1(_uRoughness, mat.Roughness); - GL.Uniform1(_uMetallic, mat.Metallic); - GL.Uniform1(_uUseTexture, 0); - - DrawMesh(gm); - }); - } - - private void DrawMesh(GLMesh m) - { - GL.BindVertexArray(m.Vao); - GL.DrawElements(PrimitiveType.Triangles, m.Count, DrawElementsType.UnsignedInt, 0); - GL.BindVertexArray(0); - } - - private void SetUniformMat4(int loc, OTKMatrix mat) - { - // OpenTK Matrix4 fields: M11,M12,M13,M14, M21..M24, M31..M34, M41..M44 - // These are stored in the struct as row-major (M11=row1col1). - // BUT OpenTK's Matrix4 in memory is actually column-major per its design - // (M11,M21,M31,M41 is the first column in memory). - // So we copy field-by-field and pass with transpose=false. - _matrixBuf[0] = mat.M11; _matrixBuf[1] = mat.M21; _matrixBuf[2] = mat.M31; _matrixBuf[3] = mat.M41; - _matrixBuf[4] = mat.M12; _matrixBuf[5] = mat.M22; _matrixBuf[6] = mat.M32; _matrixBuf[7] = mat.M42; - _matrixBuf[8] = mat.M13; _matrixBuf[9] = mat.M23; _matrixBuf[10] = mat.M33; _matrixBuf[11] = mat.M43; - _matrixBuf[12] = mat.M14; _matrixBuf[13] = mat.M24; _matrixBuf[14] = mat.M34; _matrixBuf[15] = mat.M44; - GL.UniformMatrix4(loc, 1, false, _matrixBuf); - } - - private GLMesh GetOrUploadMesh(Entity e, EngineMesh mesh) - { - if (_meshCache.TryGetValue(e, out var existing)) - return existing; - - int vao = GL.GenVertexArray(); - GL.BindVertexArray(vao); - - // Position - float[] pos = new float[mesh.Vertices.Length * 3]; - for (int i = 0; i < mesh.Vertices.Length; i++) - { - pos[i*3] = mesh.Vertices[i].Position.X; - pos[i*3+1] = mesh.Vertices[i].Position.Y; - pos[i*3+2] = mesh.Vertices[i].Position.Z; - } - int vboPos = GL.GenBuffer(); - GL.BindBuffer(BufferTarget.ArrayBuffer, vboPos); - GL.BufferData(BufferTarget.ArrayBuffer, pos.Length * sizeof(float), pos, BufferUsageHint.StaticDraw); - GL.EnableVertexAttribArray(0); - GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 0, 0); - - // Normal - float[] nrm = new float[mesh.Vertices.Length * 3]; - for (int i = 0; i < mesh.Vertices.Length; i++) - { - nrm[i*3] = mesh.Vertices[i].Normal.X; - nrm[i*3+1] = mesh.Vertices[i].Normal.Y; - nrm[i*3+2] = mesh.Vertices[i].Normal.Z; - } - int vboNrm = GL.GenBuffer(); - GL.BindBuffer(BufferTarget.ArrayBuffer, vboNrm); - GL.BufferData(BufferTarget.ArrayBuffer, nrm.Length * sizeof(float), nrm, BufferUsageHint.StaticDraw); - GL.EnableVertexAttribArray(1); - GL.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, 0, 0); - - // Color - float[] col = new float[mesh.Vertices.Length * 4]; - for (int i = 0; i < mesh.Vertices.Length; i++) - { - col[i*4] = mesh.Vertices[i].Color.X; - col[i*4+1] = mesh.Vertices[i].Color.Y; - col[i*4+2] = mesh.Vertices[i].Color.Z; - col[i*4+3] = 1f; - } - int vboCol = GL.GenBuffer(); - GL.BindBuffer(BufferTarget.ArrayBuffer, vboCol); - GL.BufferData(BufferTarget.ArrayBuffer, col.Length * sizeof(float), col, BufferUsageHint.StaticDraw); - GL.EnableVertexAttribArray(2); - GL.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, false, 0, 0); - - // Indices - int ebo = GL.GenBuffer(); - GL.BindBuffer(BufferTarget.ElementArrayBuffer, ebo); - GL.BufferData(BufferTarget.ElementArrayBuffer, mesh.Indices.Length * sizeof(uint), mesh.Indices, BufferUsageHint.StaticDraw); - - GL.BindVertexArray(0); - - var gm = new GLMesh(vao, mesh.Indices.Length); - _meshCache[e] = gm; - return gm; - } - - private void CollectLights(World world) - { - int count = 0; - world.Each((Entity e, ref Light light) => - { - if (count >= 4) return; - _lightDirs[count*3] = light.Direction.X; - _lightDirs[count*3+1] = light.Direction.Y; - _lightDirs[count*3+2] = light.Direction.Z; - _lightPositions[count*3] = light.Position.X; - _lightPositions[count*3+1] = light.Position.Y; - _lightPositions[count*3+2] = light.Position.Z; - _lightIntensities[count] = light.Intensity; - _lightColors[count*3] = light.Color.X; - _lightColors[count*3+1] = light.Color.Y; - _lightColors[count*3+2] = light.Color.Z; - _lightTypes[count] = (int)light.Type; - _lightRanges[count] = light.Range; - count++; - }); - if (count == 0) - { - _lightDirs[0] = 0.5f; _lightDirs[1] = -1; _lightDirs[2] = -0.5f; - _lightIntensities[0] = 1; _lightColors[0] = 1; _lightColors[1] = 0.95f; _lightColors[2] = 0.8f; - _lightTypes[0] = (int)LightType.Directional; _lightRanges[0] = 20; - count = 1; - } - for (int i = count; i < 4; i++) { _lightIntensities[i] = 0; _lightTypes[i] = 0; } - _lightCount = count; - } - - private Camera GetCamera(World world) - { - var cam = new Camera(new Vector3(0, 0.75f, -30), new Vector3(0, 0.5f, 0), Vector3.UnitY, MathF.PI/12, 16f/9f, 0.1f, 100f); - world.Each((Entity e, ref Camera c) => cam = c); - return cam; - } - - private static OTKMatrix ToM4(System.Numerics.Matrix4x4 m) => new( - m.M11, m.M12, m.M13, m.M14, - m.M21, m.M22, m.M23, m.M24, - m.M31, m.M32, m.M33, m.M34, - m.M41, m.M42, m.M43, m.M44); - - private static OTKVector3 ToV3(Vector3 v) => new(v.X, v.Y, v.Z); - - private static int CreateProgram(string vsSrc, string fsSrc) - { - int vs = GL.CreateShader(ShaderType.VertexShader); - GL.ShaderSource(vs, vsSrc); - GL.CompileShader(vs); - GL.GetShader(vs, ShaderParameter.CompileStatus, out int vsOk); - if (vsOk == 0) throw new Exception($"VS compile: {GL.GetShaderInfoLog(vs)}"); - - int fs = GL.CreateShader(ShaderType.FragmentShader); - GL.ShaderSource(fs, fsSrc); - GL.CompileShader(fs); - GL.GetShader(fs, ShaderParameter.CompileStatus, out int fsOk); - if (fsOk == 0) throw new Exception($"FS compile: {GL.GetShaderInfoLog(fs)}"); - - int prog = GL.CreateProgram(); - GL.AttachShader(prog, vs); - GL.AttachShader(prog, fs); - GL.LinkProgram(prog); - GL.GetProgram(prog, GetProgramParameterName.LinkStatus, out int linkOk); - if (linkOk == 0) throw new Exception($"Link: {GL.GetProgramInfoLog(prog)}"); - - GL.DeleteShader(vs); - GL.DeleteShader(fs); - return prog; - } - - public void Dispose() - { - if (_disposed) return; - _disposed = true; - foreach (var m in _meshCache.Values) GL.DeleteVertexArray(m.Vao); - _meshCache.Clear(); - GL.DeleteProgram(_program); - GL.DeleteProgram(_shadowProgram); - GL.DeleteFramebuffer(_shadowFbo); - GL.DeleteTexture(_shadowTexture); - } - - // === Shaders === - private const string ShadowVertSrc = @"#version 330 core -layout(location=0) in vec3 aPos; -uniform mat4 mvp; -void main(){ gl_Position = mvp * vec4(aPos, 1.0); }"; - - private const string ShadowFragSrc = @"#version 330 core -void main(){}"; - - private const string VertexSrc = @"#version 330 core -layout(location=0) in vec3 aPos; -layout(location=1) in vec3 aNormal; -layout(location=2) in vec4 aColor; -uniform mat4 mvp; -uniform mat4 model; -out vec3 vNormal; -out vec3 vWorldPos; -out vec4 vColor; -void main() -{ - vec4 wp = model * vec4(aPos, 1.0); - vWorldPos = wp.xyz; - vNormal = mat3(transpose(inverse(model))) * aNormal; - vColor = aColor; - gl_Position = mvp * vec4(aPos, 1.0); -}"; - - private const string FragmentSrc = @"#version 330 core -in vec3 vNormal; -in vec3 vWorldPos; -in vec4 vColor; -out vec4 finalColor; -uniform vec4 materialColor; -uniform float roughness; -uniform float metallic; -uniform vec3 viewPos; -uniform vec3 ambientColor; -uniform int lightCount; -uniform vec3 lightDirs[4]; -uniform vec3 lightPositions[4]; -uniform float lightIntensities[4]; -uniform vec3 lightColors[4]; -uniform int lightTypes[4]; -uniform float lightRanges[4]; -uniform mat4 lightViewProj; -uniform sampler2D shadowMap; -uniform int useTexture; - -vec3 ACESFilm(vec3 x) -{ - const float a=2.51, b=0.03, c=2.43, d=0.59, e=0.14; - return clamp((x*(a*x+b))/(x*(c*x+d)+e), 0.0, 1.0); -} - -float Attenuation(float dist, float range) -{ - float r = max(range, 0.001); - float d = max(dist, 0.001); - float x = d/r, x2 = x*x, x4 = x2*x2; - return clamp(1.0/(1.0+25.0*x4), 0.0, 1.0) * smoothstep(1.0, 0.0, x); -} - -float CalculateShadow(vec3 worldPos) -{ - vec4 lp = lightViewProj * vec4(worldPos, 1.0); - vec3 ndc = lp.xyz / lp.w; - vec3 uvw = ndc * 0.5 + 0.5; - if (uvw.x < 0.0 || uvw.x > 1.0 || uvw.y < 0.0 || uvw.y > 1.0 || uvw.z > 1.0) - return 1.0; - float bias = 0.005; - vec2 ts = vec2(1.0 / 2048.0); - float s = 0.0; - for (int x = -1; x <= 1; x++) { - for (int y = -1; y <= 1; y++) { - float dpt = texture(shadowMap, uvw.xy + vec2(x, y) * ts).r; - s += (uvw.z - bias > dpt) ? 0.3 : 1.0; - } - } - return s / 9.0; -} - -void main() -{ - vec3 normal = normalize(vNormal); - vec3 albedo = pow(vColor.rgb * materialColor.rgb, vec3(2.2)); - vec3 viewDir = normalize(viewPos - vWorldPos); - float rough = clamp(roughness, 0.05, 1.0); - float metal = clamp(metallic, 0.0, 1.0); - - vec3 skyColor = ambientColor; - vec3 groundColor = ambientColor * 0.2; - float hemisphere = 0.5 + 0.5 * normal.y; - vec3 result = albedo * mix(groundColor, skyColor, hemisphere) * 0.4; - - float shadow = 1.0; - if (lightCount > 0 && lightTypes[0] == 0) - shadow = CalculateShadow(vWorldPos); - - vec3 F0 = mix(vec3(0.04), albedo, metal); - float shininess = mix(8.0, 256.0, 1.0 - rough); - - for (int i = 0; i < lightCount; i++) - { - vec3 L; - float atten = 1.0; - if (lightTypes[i] == 1) { - vec3 toLight = lightPositions[i] - vWorldPos; - float dist = length(toLight); - L = toLight / max(dist, 0.001); - atten = Attenuation(dist, lightRanges[i]); - } else { - L = normalize(-lightDirs[i]); - } - float lightShadow = (i == 0 && lightTypes[0] == 0) ? shadow : 1.0; - vec3 H = normalize(L + viewDir); - float NdotL = max(dot(normal, L), 0.0); - float NdotH = max(dot(normal, H), 0.0); - float HdotV = max(dot(H, viewDir), 0.0); - float spec = pow(NdotH, shininess); - vec3 fresnel = F0 + (1.0 - F0) * pow(1.0 - HdotV, 5.0); - vec3 specColor = mix(fresnel, albedo * fresnel, metal); - vec3 diffuse = albedo * lightColors[i] * NdotL * lightIntensities[i] * atten * 1.5 * lightShadow; - vec3 specular = specColor * spec * lightIntensities[i] * atten * lightShadow; - diffuse *= (1.0 - fresnel * (1.0 - metal * 0.5)); - result += diffuse + specular; - } - - result = ACESFilm(result * 1.2); - result = pow(result, vec3(1.0 / 2.2)); - finalColor = vec4(result, 1.0); -}"; - - private readonly record struct GLMesh(int Vao, int Count); - - private sealed class DummyScreenshotProvider : IScreenshotProvider - { - public Task CaptureAsync(string outputPath) => Task.FromResult(Array.Empty()); - } -} diff --git a/src/Engine.Graphics.OpenTK/OpenTKWindow.cs b/src/Engine.Graphics.OpenTK/OpenTKWindow.cs deleted file mode 100644 index b93b334..0000000 --- a/src/Engine.Graphics.OpenTK/OpenTKWindow.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using Engine.Core; -using OpenTK.Windowing.Common; -using OpenTK.Windowing.Desktop; - -namespace Engine.Graphics.OpenTK; - -public sealed class OpenTKWindow : GameWindow, IWindow, IDisposable -{ - private readonly OpenTKInputState _input = new(); - private bool _shouldClose; - - public new int Width => Size.X; - public new int Height => Size.Y; - public bool ShouldClose => _shouldClose || IsExiting; - IInputState IWindow.Input => _input; - public nint Handle => 0; - - public OpenTKWindow(string title, int width, int height) - : base(GameWindowSettings.Default, new NativeWindowSettings - { - Title = title, - Size = (width, height), - APIVersion = new Version(3, 3), - Profile = ContextProfile.Core, - Flags = ContextFlags.ForwardCompatible, - Vsync = VSyncMode.Off, - NumberOfSamples = 0, - }) - { - // GameWindow creates the GL context in the base constructor. - // MakeCurrent is called automatically by OpenTK on first ProcessEvents. - // We call it explicitly here to ensure it's ready before renderer creation. - MakeCurrent(); - } - - public void PumpEvents() - { - ProcessEvents(0); - _input.Update(KeyboardState, MouseState); - } - - void IWindow.Close() => _shouldClose = true; - - public string[] GetRequiredVulkanExtensions() => Array.Empty(); - - public new void Dispose() - { - Close(); - base.Dispose(); - } -} diff --git a/src/Engine.Graphics.Raylib/Engine.Graphics.Raylib.csproj b/src/Engine.Graphics.Raylib/Engine.Graphics.Raylib.csproj deleted file mode 100644 index dc584c6..0000000 --- a/src/Engine.Graphics.Raylib/Engine.Graphics.Raylib.csproj +++ /dev/null @@ -1,34 +0,0 @@ - - - - net9.0 - enable - enable - true - false - Engine.Graphics.Raylib - Engine.Graphics.Raylib - - - - DEV_MODE - - - - RELEASE_AOT - false - - - - - - - - - - - - - - - diff --git a/src/Engine.Graphics.Raylib/ImGuiLayer.cs b/src/Engine.Graphics.Raylib/ImGuiLayer.cs deleted file mode 100644 index 899d589..0000000 --- a/src/Engine.Graphics.Raylib/ImGuiLayer.cs +++ /dev/null @@ -1,335 +0,0 @@ -using System.Numerics; -using Engine.Core; -using Flecs.NET.Core; -using ImGuiNET; -using rlImGui_cs; -using EngineTransform = Engine.Core.Components.Transform; -using EngineMaterial = Engine.Core.Components.Material; -using EngineLight = Engine.Core.Components.Light; -using EngineCamera = Engine.Core.Components.Camera; - -namespace Engine.Graphics.RaylibBackend; - -/// -/// Dear ImGui integration for the Raylib backend. -/// Provides an entity inspector, hierarchy panel, and debug overlay. -/// -public sealed class ImGuiLayer : IDisposable -{ - private bool _initialized; - private bool _disposed; - private string _selectedEntity = ""; - private float[] _fpsHistory = new float[120]; - private int _fpsHistoryIndex; - - private World? _world; - private Timing? _timing; - private int _fps; - - /// - /// Set the selected entity name from external (e.g. ObjectManipulator). - /// - public void SetSelectedEntity(string name) { _selectedEntity = name; } - - /// - /// Get the currently selected entity name. - /// - public string GetSelectedEntity() => _selectedEntity; - - /// - /// Set per-frame data before calling RenderImGuiUI. - /// - public void SetFrameData(World world, Timing timing, int fps) - { - _world = world; - _timing = timing; - _fps = fps; - } - - /// - /// Render the ImGui UI. Called internally by RaylibRenderer between Begin() and End(). - /// - internal void RenderImGuiUI() - { - if (!_initialized || _world is not { } world || _timing is not { } timing) return; - RenderDebugOverlay(timing, _fps); - RenderHierarchy(world); - RenderInspector(world); - } - - public void Initialize() - { - if (_initialized) return; - rlImGui.Setup(true); - _initialized = true; - } - - /// - /// Call at the start of the frame (after EndMode3D, before EndDrawing). - /// Begins the ImGui render pass. - /// - public void Begin() - { - if (!_initialized) return; - rlImGui.Begin(); - } - - /// - /// Call at the end of the frame (before EndDrawing). - /// Ends the ImGui render pass and renders all ImGui draw data. - /// - public void End() - { - if (!_initialized) return; - rlImGui.End(); - } - - private void RenderDebugOverlay(Timing timing, int fps) - { - ImGui.SetNextWindowPos(new Vector2(10, 10), ImGuiCond.Always); - ImGui.SetNextWindowBgAlpha(0.7f); - - if (!ImGui.Begin("Debug", ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.AlwaysAutoResize)) - { - ImGui.End(); - return; - } - - ImGui.Text($"FPS: {fps}"); - ImGui.Text($"Frame: {timing.DeltaTime * 1000.0f:F2} ms"); - ImGui.Text($"Time: {timing.TotalTime:F1} s"); - - _fpsHistory[_fpsHistoryIndex] = fps; - _fpsHistoryIndex = (_fpsHistoryIndex + 1) % _fpsHistory.Length; - - ImGui.PlotLines("##fps", ref _fpsHistory[0], _fpsHistory.Length, _fpsHistoryIndex, "", 0, 200, new Vector2(200, 40)); - - ImGui.End(); - } - - private void RenderHierarchy(World world) - { - ImGui.SetNextWindowPos(new Vector2(10, 120), ImGuiCond.FirstUseEver); - ImGui.SetNextWindowSize(new Vector2(250, 400), ImGuiCond.FirstUseEver); - - if (!ImGui.Begin("Hierarchy")) - { - ImGui.End(); - return; - } - - world.Each((Entity e, ref EngineTransform _) => - { - var name = e.Name(); - if (string.IsNullOrEmpty(name)) - return; - - var isSelected = name == _selectedEntity; - if (ImGui.Selectable(name, isSelected)) - _selectedEntity = name; - }); - - ImGui.End(); - } - - private void RenderInspector(World world) - { - ImGui.SetNextWindowPos(new Vector2(270, 120), ImGuiCond.FirstUseEver); - ImGui.SetNextWindowSize(new Vector2(300, 400), ImGuiCond.FirstUseEver); - - if (!ImGui.Begin("Inspector")) - { - ImGui.End(); - return; - } - - if (string.IsNullOrEmpty(_selectedEntity)) - { - ImGui.TextDisabled("Select an entity from the Hierarchy"); - ImGui.End(); - return; - } - - var entity = world.Lookup(_selectedEntity); - if ((ulong)entity.Id == 0) - { - ImGui.TextDisabled($"Entity '{_selectedEntity}' not found"); - ImGui.End(); - return; - } - - ImGui.Text($"Entity: {_selectedEntity}"); - ImGui.Separator(); - - if (entity.Has()) - { - var t = entity.Get(); - - if (ImGui.CollapsingHeader("Transform", ImGuiTreeNodeFlags.DefaultOpen)) - { - var pos = t.Position; - if (ImGui.DragFloat3("Position", ref pos, 0.1f)) - { - t.Position = pos; - entity.Set(t); - } - - var scale = t.Scale; - if (ImGui.DragFloat3("Scale", ref scale, 0.1f, 0.01f, 100f)) - { - t.Scale = scale; - entity.Set(t); - } - - var euler = ToEuler(t.Rotation); - if (ImGui.DragFloat3("Rotation", ref euler, 1.0f, -180f, 180f)) - { - t.Rotation = FromEuler(euler); - entity.Set(t); - } - } - } - - if (entity.Has()) - { - var m = entity.Get(); - - if (ImGui.CollapsingHeader("Material", ImGuiTreeNodeFlags.DefaultOpen)) - { - var albedo = m.Albedo; - if (ImGui.ColorEdit3("Albedo", ref albedo)) - { - m.Albedo = albedo; - entity.Set(m); - } - - var rough = m.Roughness; - if (ImGui.SliderFloat("Roughness", ref rough, 0.0f, 1.0f)) - { - m.Roughness = rough; - entity.Set(m); - } - - var metal = m.Metallic; - if (ImGui.SliderFloat("Metallic", ref metal, 0.0f, 1.0f)) - { - m.Metallic = metal; - entity.Set(m); - } - - if (m.HasTexture) - ImGui.Text($"Texture: {m.TexturePath}"); - else - ImGui.TextDisabled("No texture"); - } - } - - if (entity.Has()) - { - var l = entity.Get(); - - if (ImGui.CollapsingHeader("Light", ImGuiTreeNodeFlags.DefaultOpen)) - { - var type = (int)l.Type; - if (ImGui.Combo("Type", ref type, "Directional\0Point\0")) - { - l.Type = (Engine.Core.Components.LightType)type; - entity.Set(l); - } - - var color = l.Color; - if (ImGui.ColorEdit3("Color", ref color)) - { - l.Color = color; - entity.Set(l); - } - - var intensity = l.Intensity; - if (ImGui.SliderFloat("Intensity", ref intensity, 0.0f, 10.0f)) - { - l.Intensity = intensity; - entity.Set(l); - } - - if (l.Type == Engine.Core.Components.LightType.Point) - { - var pos = l.Position; - if (ImGui.DragFloat3("Position", ref pos, 0.1f)) - { - l.Position = pos; - entity.Set(l); - } - - var range = l.Range; - if (ImGui.DragFloat("Range", ref range, 0.5f, 1f, 100f)) - { - l.Range = range; - entity.Set(l); - } - } - else - { - var dir = l.Direction; - if (ImGui.DragFloat3("Direction", ref dir, 0.01f, -1f, 1f)) - { - l.Direction = dir; - entity.Set(l); - } - } - } - } - - if (entity.Has()) - { - var c = entity.Get(); - - if (ImGui.CollapsingHeader("Camera")) - { - var pos = c.Position; - if (ImGui.DragFloat3("Position", ref pos, 0.1f)) - { - c.Position = pos; - entity.Set(c); - } - - var target = c.Target; - if (ImGui.DragFloat3("Target", ref target, 0.1f)) - { - c.Target = target; - entity.Set(c); - } - - var fov = c.FieldOfView * 180.0f / MathF.PI; - if (ImGui.SliderFloat("FOV", ref fov, 5f, 120f)) - { - c.FieldOfView = fov * MathF.PI / 180.0f; - entity.Set(c); - } - } - } - - ImGui.End(); - } - - private static Vector3 ToEuler(Quaternion q) - { - var pitch = MathF.Atan2(2 * (q.W * q.X + q.Y * q.Z), 1 - 2 * (q.X * q.X + q.Y * q.Y)); - var yaw = MathF.Asin(Math.Clamp(2 * (q.W * q.Y - q.Z * q.X), -1f, 1f)); - var roll = MathF.Atan2(2 * (q.W * q.Z + q.X * q.Y), 1 - 2 * (q.Y * q.Y + q.Z * q.Z)); - return new Vector3(pitch * 180f / MathF.PI, yaw * 180f / MathF.PI, roll * 180f / MathF.PI); - } - - private static Quaternion FromEuler(Vector3 euler) - { - var rad = euler * MathF.PI / 180f; - return Quaternion.CreateFromYawPitchRoll(rad.Y, rad.X, rad.Z); - } - - public void Dispose() - { - if (_disposed) return; - _disposed = true; - if (_initialized) - rlImGui.Shutdown(); - } -} diff --git a/src/Engine.Graphics.Raylib/RaylibBackendRegistrar.cs b/src/Engine.Graphics.Raylib/RaylibBackendRegistrar.cs deleted file mode 100644 index 2bb8e4f..0000000 --- a/src/Engine.Graphics.Raylib/RaylibBackendRegistrar.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Engine.Graphics; - -namespace Engine.Graphics.RaylibBackend; - -/// -/// Triggers registration of the Raylib backend with the HAL factory. -/// -public static class RaylibBackendRegistrar -{ - static RaylibBackendRegistrar() - { - RenderBackendFactory.Register("raylib", (width, height, _) => new RaylibRenderContext(width, height)); - } - - /// - /// No-op method that forces the static constructor to run. - /// Call this before using . - /// - public static void EnsureRegistered() { } -} diff --git a/src/Engine.Graphics.Raylib/RaylibInputState.cs b/src/Engine.Graphics.Raylib/RaylibInputState.cs deleted file mode 100644 index 83ca180..0000000 --- a/src/Engine.Graphics.Raylib/RaylibInputState.cs +++ /dev/null @@ -1,156 +0,0 @@ -using System; -using System.Collections.Generic; -using Engine.Core; -using Raylib_cs; - -namespace Engine.Graphics.RaylibBackend; - -/// -/// Raylib-backed implementation of . -/// Queries Raylib's input functions directly each frame. -/// -public sealed class RaylibInputState : IInputState -{ - private static readonly Key[] _allKeys = (Key[])Enum.GetValues(typeof(Key)); - - private readonly HashSet _keysDown = new(); - private readonly HashSet _keysPressed = new(); - private readonly HashSet _keysReleased = new(); - - private float _mouseWheelDelta; - private bool _wheelConsumed; - - public int MouseX => Raylib.GetMouseX(); - public int MouseY => Raylib.GetMouseY(); - public bool MouseLeft => Raylib.IsMouseButtonDown(MouseButton.Left); - public bool MouseRight => Raylib.IsMouseButtonDown(MouseButton.Right); - public bool MouseMiddle => Raylib.IsMouseButtonDown(MouseButton.Middle); - - public float MouseWheelDelta - { - get - { - if (!_wheelConsumed) - { - _mouseWheelDelta = Raylib.GetMouseWheelMove(); - _wheelConsumed = true; - } - return _mouseWheelDelta; - } - } - - public void BeginFrame() - { - _keysPressed.Clear(); - _keysReleased.Clear(); - _mouseWheelDelta = 0; - _wheelConsumed = false; - } - - /// - /// Poll Raylib input and update edge state. Called by . - /// - public void Poll() - { - _keysPressed.Clear(); - _keysReleased.Clear(); - - foreach (var key in _allKeys) - { - if (key == Key.Unknown) continue; - var rlKey = ToRaylibKey(key); - if (rlKey == KeyboardKey.Null) continue; - - var isDown = Raylib.IsKeyDown(rlKey); - var wasDown = _keysDown.Contains(key); - - if (isDown && !wasDown) - _keysPressed.Add(key); - if (!isDown && wasDown) - _keysReleased.Add(key); - - if (isDown) - _keysDown.Add(key); - else - _keysDown.Remove(key); - } - } - - public bool IsKeyDown(Key key) => _keysDown.Contains(key); - public bool IsKeyPressed(Key key) => _keysPressed.Contains(key); - public bool IsKeyReleased(Key key) => _keysReleased.Contains(key); - - private static KeyboardKey ToRaylibKey(Key key) => key switch - { - Key.Space => KeyboardKey.Space, - Key.Escape => KeyboardKey.Escape, - Key.Enter => KeyboardKey.Enter, - Key.Tab => KeyboardKey.Tab, - Key.Backspace => KeyboardKey.Backspace, - Key.Insert => KeyboardKey.Insert, - Key.Delete => KeyboardKey.Delete, - Key.Home => KeyboardKey.Home, - Key.End => KeyboardKey.End, - Key.PageUp => KeyboardKey.PageUp, - Key.PageDown => KeyboardKey.PageDown, - Key.Left => KeyboardKey.Left, - Key.Right => KeyboardKey.Right, - Key.Up => KeyboardKey.Up, - Key.Down => KeyboardKey.Down, - Key.A => KeyboardKey.A, - Key.B => KeyboardKey.B, - Key.C => KeyboardKey.C, - Key.D => KeyboardKey.D, - Key.E => KeyboardKey.E, - Key.F => KeyboardKey.F, - Key.G => KeyboardKey.G, - Key.H => KeyboardKey.H, - Key.I => KeyboardKey.I, - Key.J => KeyboardKey.J, - Key.K => KeyboardKey.K, - Key.L => KeyboardKey.L, - Key.M => KeyboardKey.M, - Key.N => KeyboardKey.N, - Key.O => KeyboardKey.O, - Key.P => KeyboardKey.P, - Key.Q => KeyboardKey.Q, - Key.R => KeyboardKey.R, - Key.S => KeyboardKey.S, - Key.T => KeyboardKey.T, - Key.U => KeyboardKey.U, - Key.V => KeyboardKey.V, - Key.W => KeyboardKey.W, - Key.X => KeyboardKey.X, - Key.Y => KeyboardKey.Y, - Key.Z => KeyboardKey.Z, - Key.Zero => KeyboardKey.Zero, - Key.One => KeyboardKey.One, - Key.Two => KeyboardKey.Two, - Key.Three => KeyboardKey.Three, - Key.Four => KeyboardKey.Four, - Key.Five => KeyboardKey.Five, - Key.Six => KeyboardKey.Six, - Key.Seven => KeyboardKey.Seven, - Key.Eight => KeyboardKey.Eight, - Key.Nine => KeyboardKey.Nine, - Key.F1 => KeyboardKey.F1, - Key.F2 => KeyboardKey.F2, - Key.F3 => KeyboardKey.F3, - Key.F4 => KeyboardKey.F4, - Key.F5 => KeyboardKey.F5, - Key.F6 => KeyboardKey.F6, - Key.F7 => KeyboardKey.F7, - Key.F8 => KeyboardKey.F8, - Key.F9 => KeyboardKey.F9, - Key.F10 => KeyboardKey.F10, - Key.F11 => KeyboardKey.F11, - Key.F12 => KeyboardKey.F12, - Key.LeftShift => KeyboardKey.LeftShift, - Key.LeftControl => KeyboardKey.LeftControl, - Key.LeftAlt => KeyboardKey.LeftAlt, - Key.RightShift => KeyboardKey.RightShift, - Key.RightControl => KeyboardKey.RightControl, - Key.RightAlt => KeyboardKey.RightAlt, - _ => KeyboardKey.Null, - }; -} diff --git a/src/Engine.Graphics.Raylib/RaylibRenderContext.cs b/src/Engine.Graphics.Raylib/RaylibRenderContext.cs deleted file mode 100644 index a18cc2f..0000000 --- a/src/Engine.Graphics.Raylib/RaylibRenderContext.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Engine.Core; -using Engine.Graphics; -using Raylib_cs; - -namespace Engine.Graphics.RaylibBackend; - -/// -/// Raylib implementation of the render HAL context. -/// Creates and owns a (GLFW-based). -/// No SDL3 dependency — the Raylib window handles both rendering and input. -/// -public sealed class RaylibRenderContext : IRenderContext -{ - private readonly RaylibWindow _window; - - public IWindow Window => _window; - - public RaylibRenderContext(int width, int height, bool enableValidation = false) - { - _window = new RaylibWindow("Cortex Engine", width, height); - } - - public IRenderer CreateRenderer() => new RaylibRenderer(); - - public void Resize(int width, int height) => Raylib.SetWindowSize(width, height); - - public void Dispose() => _window.Dispose(); -} diff --git a/src/Engine.Graphics.Raylib/RaylibRenderer.cs b/src/Engine.Graphics.Raylib/RaylibRenderer.cs deleted file mode 100644 index d148bb1..0000000 --- a/src/Engine.Graphics.Raylib/RaylibRenderer.cs +++ /dev/null @@ -1,723 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Numerics; -using System.Runtime.InteropServices; -using System.Threading.Tasks; -using Engine.Core; -using Engine.Core.Components; -using EngineMaterial = Engine.Core.Components.Material; -using EngineMesh = Engine.Core.Components.Mesh; -using EngineTransform = Engine.Core.Components.Transform; -using Flecs.NET.Core; -using Raylib_cs; - -namespace Engine.Graphics.RaylibBackend; - -/// -/// Raylib implementation of the ECS world renderer. -/// Renders Mesh + Transform + Material entities with up to four directional lights. -/// -public sealed class RaylibRenderer : IRenderer -{ - private const int ShadowMapSize = 2048; - - private readonly Shader _shader; - private readonly Shader _shadowShader; - private readonly uint _shadowFbo; - private readonly uint _shadowDepthTex; - private readonly Dictionary _modelCache = new(); - private readonly Dictionary _textureCache = new(); - private readonly int _materialColorLoc; - private readonly int _useTextureLoc; - private readonly int _roughnessLoc; - private readonly int _metallicLoc; - private readonly int _ambientLoc; - private readonly int _viewPosLoc; - private readonly int _lightCountLoc; - private readonly int _lightDirLoc; - private readonly int _lightIntensityLoc; - private readonly int _lightColorLoc; - private readonly int _lightPosLoc; - private readonly int _lightTypeLoc; - private readonly int _lightRangeLoc; - private readonly int _lightViewProjLoc; - private readonly int _shadowMapLoc; - private readonly float[] _lightDirs = new float[12]; - private readonly float[] _lightPositions = new float[12]; - private readonly float[] _lightRanges = new float[4]; - private readonly int[] _lightTypes = new int[4]; - private readonly float[] _lightIntensities = new float[4]; - private readonly float[] _lightColors = new float[12]; - private int _lightCount; - - private ScreenshotRequest? _pendingScreenshot; - private int _frameCount; - private bool _disposed; - - /// - /// ImGui editor layer. Accessible so the app can feed world/timing data. - /// - public ImGuiLayer? ImGuiLayer { get; set; } - - public RaylibRenderer() - { - _shader = LoadShader(); - _shadowShader = LoadShadowShader(); - - // Create depth-only FBO for shadow mapping (per official raylib example) - _shadowFbo = Rlgl.LoadFramebuffer(); - _shadowDepthTex = Rlgl.LoadTextureDepth(ShadowMapSize, ShadowMapSize, false); - Rlgl.FramebufferAttach(_shadowFbo, _shadowDepthTex, FramebufferAttachType.Depth, FramebufferAttachTextureType.Texture2D, 0); - Rlgl.FramebufferComplete(_shadowFbo); - - // Main shader uniform locations - _materialColorLoc = Raylib.GetShaderLocation(_shader, "materialColor"); - _useTextureLoc = Raylib.GetShaderLocation(_shader, "useTexture"); - _roughnessLoc = Raylib.GetShaderLocation(_shader, "roughness"); - _metallicLoc = Raylib.GetShaderLocation(_shader, "metallic"); - _ambientLoc = Raylib.GetShaderLocation(_shader, "ambientColor"); - _viewPosLoc = Raylib.GetShaderLocation(_shader, "viewPos"); - _lightCountLoc = Raylib.GetShaderLocation(_shader, "lightCount"); - _lightDirLoc = Raylib.GetShaderLocation(_shader, "lightDirs"); - _lightIntensityLoc = Raylib.GetShaderLocation(_shader, "lightIntensities"); - _lightColorLoc = Raylib.GetShaderLocation(_shader, "lightColors"); - _lightPosLoc = Raylib.GetShaderLocation(_shader, "lightPositions"); - _lightTypeLoc = Raylib.GetShaderLocation(_shader, "lightTypes"); - _lightRangeLoc = Raylib.GetShaderLocation(_shader, "lightRanges"); - _lightViewProjLoc = Raylib.GetShaderLocation(_shader, "lightViewProj"); - _shadowMapLoc = Raylib.GetShaderLocation(_shader, "shadowMap"); - } - - public void RequestScreenshot(string outputPath) - { - _pendingScreenshot = new ScreenshotRequest(outputPath, null); - } - - public bool IsScreenshotRequested => _pendingScreenshot != null; - - public IScreenshotProvider ScreenshotProvider => new RaylibScreenshotProvider(this); - - public void RenderWorld(World world) - { - var camera = GetCamera(world); - CollectLights(world); - - var hasDirectionalLight = _lightCount > 0 && _lightTypes[0] == (int)LightType.Directional; - Matrix4x4 lightViewProj = Matrix4x4.Identity; - - // === PASS 1: Shadow map (render depth from light's POV) === - if (hasDirectionalLight) - { - var lightDir = new Vector3(_lightDirs[0], _lightDirs[1], _lightDirs[2]); - var sceneCenter = new Vector3(0, 0.5f, 0); - var lightPos = sceneCenter - lightDir * 30f; - var up = MathF.Abs(Vector3.Dot(lightDir, Vector3.UnitY)) > 0.99f - ? Vector3.UnitZ : Vector3.UnitY; - - var shadowCamera = new Camera3D - { - Position = lightPos, - Target = sceneCenter, - Up = up, - FovY = 0, - Projection = CameraProjection.Orthographic - }; - - // Use BeginTextureMode with our custom depth FBO - Rlgl.EnableFramebuffer(_shadowFbo); - Rlgl.Viewport(0, 0, ShadowMapSize, ShadowMapSize); - Rlgl.ClearColor(255, 255, 255, 255); - Rlgl.ClearScreenBuffers(); - - Raylib.BeginMode3D(shadowCamera); - - // Grab light view/proj matrices AFTER BeginMode3D (like official example) - // SetShaderValueMatrix passes Matrix4x4 as-is to glUniformMatrix4fv(transpose=false) - // which interprets row-major System.Numerics as column-major = effectively transposed. - // So we just multiply directly (no manual transpose needed). - lightViewProj = Rlgl.GetMatrixModelview() * Rlgl.GetMatrixProjection(); - - // Draw all shadow casters with the simple shadow shader - world.Each((Entity e, ref EngineMesh mesh, ref EngineTransform transform) => - { - if (e.Name() == "Grid" || e.Name() == "Floor") - return; - - var model = GetOrUploadModel(e, mesh); - var modelMatrix = transform.GetMatrix(); - - if (Matrix4x4.Decompose(modelMatrix, out var scale, out var rotation, out var position)) - { - var (axis, angle) = QuaternionToAxisAngle(rotation); - unsafe - { - var origShader = model.Materials[0].Shader; - model.Materials[0].Shader = _shadowShader; - Raylib.DrawModelEx(model, position, axis, angle * 180.0f / MathF.PI, scale, Color.White); - model.Materials[0].Shader = origShader; - } - } - }); - - Raylib.EndMode3D(); - Rlgl.DisableFramebuffer(); - Rlgl.Viewport(0, 0, Raylib.GetScreenWidth(), Raylib.GetScreenHeight()); - } - - // === PASS 2: Main render with shadow sampling === - Raylib.BeginDrawing(); - Raylib.ClearBackground(new Color(25, 30, 40, 255)); - Raylib.BeginMode3D(ToRaylib(camera)); - - CollectLights(world); - SetFrameLights(); - Raylib.SetShaderValue(_shader, _viewPosLoc, new float[] { camera.Position.X, camera.Position.Y, camera.Position.Z }, ShaderUniformDataType.Vec3); - - // Set shadow uniforms on the main shader - if (hasDirectionalLight && _lightViewProjLoc >= 0) - { - Raylib.SetShaderValueMatrix(_shader, _lightViewProjLoc, lightViewProj); - } - - Rlgl.DisableBackfaceCulling(); - - var shadowTex = new Texture2D { Id = _shadowDepthTex, Width = ShadowMapSize, Height = ShadowMapSize, Mipmaps = 1, Format = PixelFormat.UncompressedR16 }; - - world.Each((Entity e, ref EngineMesh mesh, ref EngineTransform transform) => - { - if (e.Name() == "Grid") - return; - - var material = e.Has() ? e.Get() : EngineMaterial.Default; - var model = GetOrUploadModel(e, mesh); - var modelMatrix = transform.GetMatrix(); - - if (Matrix4x4.Decompose(modelMatrix, out var scale, out var rotation, out var position)) - { - var (axis, angle) = QuaternionToAxisAngle(rotation); - SetMaterialUniforms(material, model); - - // Bind shadow map on Emission slot (texture unit 1) - if (hasDirectionalLight && _shadowMapLoc >= 0) - { - unsafe { Raylib.SetMaterialTexture(ref model.Materials[0], MaterialMapIndex.Emission, shadowTex); } - } - - // Use BeginShaderMode to lock our shader, then bind shadow map on unit 1 - // BEFORE DrawModelEx so it's active during the draw - if (hasDirectionalLight && _shadowMapLoc >= 0) - { - Raylib.BeginShaderMode(_shader); - Rlgl.ActiveTextureSlot(1); - Rlgl.EnableTexture(_shadowDepthTex); - Raylib.SetShaderValue(_shader, _shadowMapLoc, 1, ShaderUniformDataType.Int); - Rlgl.ActiveTextureSlot(0); - } - - Raylib.DrawModelEx(model, position, axis, angle * 180.0f / MathF.PI, scale, Color.White); - - if (hasDirectionalLight && _shadowMapLoc >= 0) - { - Raylib.EndShaderMode(); - } - } - }); - - // Reset texture unit 0 after shadow binding - Rlgl.ActiveTextureSlot(0); - - Rlgl.EnableBackfaceCulling(); - - Raylib.DrawGrid(20, 1.0f); - - Raylib.EndMode3D(); - - - // ImGui renders on top of the 3D scene, before EndDrawing. - if (ImGuiLayer != null) - { - ImGuiLayer.Begin(); - ImGuiLayer.RenderImGuiUI(); - ImGuiLayer.End(); - } - - Raylib.EndDrawing(); - - // Defer the first screenshot by a few frames. Raylib may return a blank image - // if the window/GPU has not finished presenting the first frame. - if (_pendingScreenshot is { } request && _frameCount >= 10) - { - CaptureScreenshot(request); - _pendingScreenshot = null; - } - - _frameCount++; - } - - private static (Vector3 axis, float angle) QuaternionToAxisAngle(Quaternion q) - { - if (MathF.Abs(q.W) > 0.9999999f) - return (Vector3.UnitY, 0.0f); - - var angle = 2.0f * MathF.Acos(Math.Clamp(q.W, -1.0f, 1.0f)); - var s = MathF.Sqrt(1.0f - q.W * q.W); - var axis = s > 0.0001f - ? new Vector3(q.X / s, q.Y / s, q.Z / s) - : new Vector3(q.X, q.Y, q.Z); - return (axis, angle); - } - - private Camera3D ToRaylib(Camera camera) - { - return new Camera3D - { - Position = camera.Position, - Target = camera.Target, - Up = camera.Up, - FovY = camera.FieldOfView * 180.0f / MathF.PI, - Projection = CameraProjection.Perspective - }; - } - - private Camera GetCamera(World world) - { - var width = Raylib.GetScreenWidth(); - var height = Raylib.GetScreenHeight(); - var aspect = height > 0 ? (float)width / height : 16f / 9f; - - var camera = new Camera( - new Vector3(0.0f, 0.75f, -30.0f), - new Vector3(0.0f, 0.5f, 0.0f), - Vector3.UnitY, - MathF.PI / 12.0f, - aspect, - 0.1f, - 100.0f); - - world.Each((Entity e, ref Camera cam) => - { - camera = cam; - }); - - camera.AspectRatio = aspect; - return camera; - } - - private void CollectLights(World world) - { - var count = 0; - world.Each((Entity e, ref Light light) => - { - if (count >= 4) - return; - _lightDirs[count * 3 + 0] = light.Direction.X; - _lightDirs[count * 3 + 1] = light.Direction.Y; - _lightDirs[count * 3 + 2] = light.Direction.Z; - _lightPositions[count * 3 + 0] = light.Position.X; - _lightPositions[count * 3 + 1] = light.Position.Y; - _lightPositions[count * 3 + 2] = light.Position.Z; - _lightIntensities[count] = light.Intensity; - _lightColors[count * 3 + 0] = light.Color.X; - _lightColors[count * 3 + 1] = light.Color.Y; - _lightColors[count * 3 + 2] = light.Color.Z; - _lightTypes[count] = (int)light.Type; - _lightRanges[count] = light.Range; - count++; - }); - - if (count == 0) - { - _lightDirs[0] = 0.5f; _lightDirs[1] = -1.0f; _lightDirs[2] = -0.5f; - _lightIntensities[0] = 1.0f; - _lightColors[0] = 1.0f; _lightColors[1] = 0.95f; _lightColors[2] = 0.8f; - _lightTypes[0] = (int)LightType.Directional; - _lightRanges[0] = 20f; - count = 1; - } - - for (var i = count; i < 4; i++) - { - _lightDirs[i * 3 + 0] = 0; - _lightDirs[i * 3 + 1] = 0; - _lightDirs[i * 3 + 2] = 0; - _lightPositions[i * 3 + 0] = 0; - _lightPositions[i * 3 + 1] = 0; - _lightPositions[i * 3 + 2] = 0; - _lightIntensities[i] = 0.0f; - _lightColors[i * 3 + 0] = 0; - _lightColors[i * 3 + 1] = 0; - _lightColors[i * 3 + 2] = 0; - _lightTypes[i] = 0; - _lightRanges[i] = 0; - } - - _lightCount = count; - Raylib.SetShaderValue(_shader, _lightCountLoc, count, ShaderUniformDataType.Int); - Raylib.SetShaderValueV(_shader, _lightDirLoc, _lightDirs, ShaderUniformDataType.Vec3, 4); - Raylib.SetShaderValueV(_shader, _lightIntensityLoc, _lightIntensities, ShaderUniformDataType.Float, 4); - Raylib.SetShaderValueV(_shader, _lightColorLoc, _lightColors, ShaderUniformDataType.Vec3, 4); - Raylib.SetShaderValueV(_shader, _lightPosLoc, _lightPositions, ShaderUniformDataType.Vec3, 4); - Raylib.SetShaderValueV(_shader, _lightTypeLoc, _lightTypes, ShaderUniformDataType.Int, 4); - Raylib.SetShaderValueV(_shader, _lightRangeLoc, _lightRanges, ShaderUniformDataType.Float, 4); - } - - private void SetFrameLights() - { - Raylib.SetShaderValue(_shader, _ambientLoc, new float[] { 0.35f, 0.35f, 0.4f }, ShaderUniformDataType.Vec3); - } - - private unsafe void SetMaterialUniforms(EngineMaterial material, Raylib_cs.Model model) - { - Raylib.SetShaderValue(_shader, _materialColorLoc, new float[] { material.Albedo.X, material.Albedo.Y, material.Albedo.Z, 1.0f }, ShaderUniformDataType.Vec4); - Raylib.SetShaderValue(_shader, _roughnessLoc, material.Roughness, ShaderUniformDataType.Float); - Raylib.SetShaderValue(_shader, _metallicLoc, material.Metallic, ShaderUniformDataType.Float); - - if (material.HasTexture && File.Exists(material.TexturePath!)) - { - Raylib.SetShaderValue(_shader, _useTextureLoc, 1, ShaderUniformDataType.Int); - var texture = GetOrLoadTexture(material.TexturePath!); - Raylib.SetMaterialTexture(ref model.Materials[0], MaterialMapIndex.Albedo, texture); - } - else - { - Raylib.SetShaderValue(_shader, _useTextureLoc, 0, ShaderUniformDataType.Int); - } - } - - private unsafe Raylib_cs.Model GetOrUploadModel(Entity e, EngineMesh mesh) - { - if (_modelCache.TryGetValue(e, out var model)) - return model; - - // Use Raylib's native mesh generation when possible — the manual UploadMesh - // + LoadModelFromMesh path is unreliable for larger meshes because - // LoadModelFromMesh reads CPU-side vertex pointers after UploadMesh. - // For custom meshes (from OBJ/GLTF loaders), keep the CPU data alive. - var raylibMesh = UploadRaylibMesh(mesh); - model = Raylib.LoadModelFromMesh(raylibMesh); - - for (var i = 0; i < model.MaterialCount; i++) - { - model.Materials[i].Shader = _shader; - } - _modelCache[e] = model; - return model; - } - - private unsafe Raylib_cs.Mesh UploadRaylibMesh(EngineMesh mesh) - { - var vertexCount = mesh.Vertices.Length; - var triangleCount = mesh.Indices.Length / 3; - - var raylibMesh = new Raylib_cs.Mesh - { - VertexCount = vertexCount, - TriangleCount = triangleCount - }; - - var positionSize = vertexCount * 3 * sizeof(float); - var normalSize = vertexCount * 3 * sizeof(float); - var colorSize = vertexCount * 4; - var texcoordSize = vertexCount * 2 * sizeof(float); - var indexSize = mesh.Indices.Length * sizeof(ushort); - - // Use NativeMemory.Alloc so Raylib's UnloadMesh can free with RL_FREE (free). - var positionPtr = (float*)NativeMemory.Alloc((nuint)positionSize, 4); - var normalPtr = (float*)NativeMemory.Alloc((nuint)normalSize, 4); - var colorPtr = (byte*)NativeMemory.Alloc((nuint)colorSize, 1); - var texcoordPtr = (float*)NativeMemory.Alloc((nuint)texcoordSize, 4); - var indexPtr = (ushort*)NativeMemory.Alloc((nuint)indexSize, 2); - - for (var i = 0; i < vertexCount; i++) - { - var v = mesh.Vertices[i]; - positionPtr[i * 3 + 0] = v.Position.X; - positionPtr[i * 3 + 1] = v.Position.Y; - positionPtr[i * 3 + 2] = v.Position.Z; - - normalPtr[i * 3 + 0] = v.Normal.X; - normalPtr[i * 3 + 1] = v.Normal.Y; - normalPtr[i * 3 + 2] = v.Normal.Z; - - colorPtr[i * 4 + 0] = (byte)Math.Clamp(v.Color.X * 255.0f, 0.0f, 255.0f); - colorPtr[i * 4 + 1] = (byte)Math.Clamp(v.Color.Y * 255.0f, 0.0f, 255.0f); - colorPtr[i * 4 + 2] = (byte)Math.Clamp(v.Color.Z * 255.0f, 0.0f, 255.0f); - colorPtr[i * 4 + 3] = 255; - - texcoordPtr[i * 2 + 0] = v.Position.X; - texcoordPtr[i * 2 + 1] = v.Position.Z; - } - - for (var i = 0; i < mesh.Indices.Length; i++) - indexPtr[i] = (ushort)mesh.Indices[i]; - - raylibMesh.Vertices = positionPtr; - raylibMesh.Normals = normalPtr; - raylibMesh.Colors = colorPtr; - raylibMesh.TexCoords = texcoordPtr; - raylibMesh.Indices = indexPtr; - - Raylib.UploadMesh(ref raylibMesh, false); - - // Keep CPU-side data alive — LoadModelFromMesh reads these pointers - // to compute the bounding box. They will be freed when the model is unloaded. - return raylibMesh; - } - - private Texture2D GetOrLoadTexture(string path) - { - if (_textureCache.TryGetValue(path, out var texture)) - return texture; - - texture = Raylib.LoadTexture(path); - Raylib.SetTextureWrap(texture, TextureWrap.Repeat); - Raylib.SetTextureFilter(texture, TextureFilter.Trilinear); - _textureCache[path] = texture; - return texture; - } - - private unsafe void CaptureScreenshot(ScreenshotRequest request) - { - var image = Raylib.LoadImageFromScreen(); - try - { - var directory = Path.GetDirectoryName(request.Path); - if (!string.IsNullOrEmpty(directory)) - Directory.CreateDirectory(directory); - - Raylib.ExportImage(image, request.Path); - - if (request.Tcs != null) - { - var size = 0; - var fileType = stackalloc byte[] { (byte)'.', (byte)'p', (byte)'n', (byte)'g', 0 }; - var data = Raylib.ExportImageToMemory(image, (sbyte*)fileType, &size); - var bytes = new byte[size]; - fixed (byte* p = bytes) - { - Buffer.MemoryCopy(data, p, size, size); - } - Raylib.MemFree(data); - request.Tcs.TrySetResult(bytes); - } - - Console.WriteLine($"Screenshot saved: {request.Path}"); - } - finally - { - Raylib.UnloadImage(image); - } - } - - private Task CaptureAsync(string outputPath) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - _pendingScreenshot = new ScreenshotRequest(outputPath, tcs); - return tcs.Task; - } - - - - - - - private static Shader LoadShadowShader() - { - const string VertexSource = @"#version 330 core -in vec3 vertexPosition; -uniform mat4 mvp; -void main() -{ - gl_Position = mvp * vec4(vertexPosition, 1.0); -}"; - - const string FragmentSource = @"#version 330 core -void main() {}"; - - return Raylib.LoadShaderFromMemory(VertexSource, FragmentSource); - } - - private static Shader LoadShader() - { - const string VertexSource = @"#version 330 core -in vec3 vertexPosition; -in vec2 vertexTexCoord; -in vec3 vertexNormal; -in vec4 vertexColor; -uniform mat4 mvp; -uniform mat4 matModel; -out vec3 vNormal; -out vec3 vWorldPos; -out vec4 vColor; -out vec2 vTexCoord; -void main() -{ - vec4 worldPos = matModel * vec4(vertexPosition, 1.0); - vWorldPos = worldPos.xyz; - vNormal = mat3(transpose(inverse(matModel))) * vertexNormal; - vColor = vertexColor; - vTexCoord = vertexTexCoord; - gl_Position = mvp * vec4(vertexPosition, 1.0); -}"; - - const string FragmentSource = @"#version 330 core -in vec3 vNormal; -in vec3 vWorldPos; -in vec4 vColor; -in vec2 vTexCoord; -out vec4 finalColor; -uniform vec4 materialColor; -uniform int useTexture; -uniform sampler2D texture0; -uniform float roughness; -uniform float metallic; -uniform vec3 viewPos; -uniform vec3 ambientColor; -uniform int lightCount; -uniform vec3 lightDirs[4]; -uniform vec3 lightPositions[4]; -uniform float lightIntensities[4]; -uniform vec3 lightColors[4]; -uniform int lightTypes[4]; -uniform float lightRanges[4]; -uniform mat4 lightViewProj; -uniform sampler2D shadowMap; - -vec3 ACESFilm(vec3 x) -{ - const float a = 2.51; const float b = 0.03; const float c = 2.43; const float d = 0.59; const float e = 0.14; - return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0); -} - -float Attenuation(float dist, float range) -{ - float r = max(range, 0.001); - float d = max(dist, 0.001); - float x = d / r; - float x2 = x * x; - float x4 = x2 * x2; - return clamp(1.0 / (1.0 + 25.0 * x4), 0.0, 1.0) * smoothstep(1.0, 0.0, x); -} - -float CalculateShadow(vec3 worldPos) -{ - vec4 lp = lightViewProj * vec4(worldPos, 1.0); - vec3 ndc = lp.xyz / lp.w; - vec3 uvw = ndc * 0.5 + 0.5; - if (uvw.x < 0.0 || uvw.x > 1.0 || uvw.y < 0.0 || uvw.y > 1.0 || uvw.z > 1.0) - return 1.0; - float bias = 0.005; - vec2 ts = vec2(1.0 / 2048.0); - float s = 0.0; - for (int x = -1; x <= 1; x++) { - for (int y = -1; y <= 1; y++) { - float d = texture(shadowMap, uvw.xy + vec2(x, y) * ts).r; - s += (uvw.z - bias > d) ? 0.3 : 1.0; - } - } - return s / 9.0; -} - -void main() -{ - vec3 normal = normalize(vNormal); - vec3 albedo = pow(vColor.rgb * materialColor.rgb, vec3(2.2)); - if (useTexture != 0) - { - vec2 uv = vTexCoord * 4.0; - vec3 texColor = pow(texture(texture0, uv).rgb, vec3(2.2)); - albedo *= texColor; - } - - vec3 viewDir = normalize(viewPos - vWorldPos); - float rough = clamp(roughness, 0.05, 1.0); - float metal = clamp(metallic, 0.0, 1.0); - - vec3 skyColor = ambientColor; - vec3 groundColor = ambientColor * 0.2; - float hemisphere = 0.5 + 0.5 * normal.y; - vec3 result = albedo * mix(groundColor, skyColor, hemisphere) * 0.4; - - // Shadow factor for first directional light - float shadow = 1.0; - if (lightCount > 0 && lightTypes[0] == 0) - shadow = CalculateShadow(vWorldPos); - - vec3 F0 = mix(vec3(0.04), albedo, metal); - float shininess = mix(8.0, 256.0, 1.0 - rough); - - for (int i = 0; i < lightCount; i++) - { - vec3 L; - float atten = 1.0; - - if (lightTypes[i] == 1) - { - vec3 toLight = lightPositions[i] - vWorldPos; - float dist = length(toLight); - L = toLight / max(dist, 0.001); - atten = Attenuation(dist, lightRanges[i]); - } - else - { - L = normalize(-lightDirs[i]); - } - - float lightShadow = (i == 0 && lightTypes[0] == 0) ? shadow : 1.0; - - vec3 H = normalize(L + viewDir); - float NdotL = max(dot(normal, L), 0.0); - float NdotH = max(dot(normal, H), 0.0); - float HdotV = max(dot(H, viewDir), 0.0); - float diff = NdotL; - float spec = pow(NdotH, shininess); - vec3 fresnel = F0 + (1.0 - F0) * pow(1.0 - HdotV, 5.0); - vec3 specularColor = mix(fresnel, albedo * fresnel, metal); - vec3 diffuse = albedo * lightColors[i] * diff * lightIntensities[i] * atten * 1.5 * lightShadow; - vec3 specular = specularColor * spec * lightIntensities[i] * atten * lightShadow; - diffuse *= (1.0 - fresnel * (1.0 - metal * 0.5)); - result += diffuse + specular; - } - - result = ACESFilm(result * 1.2); - result = pow(result, vec3(1.0 / 2.2)); - finalColor = vec4(result, 1.0); -}"; - - return Raylib.LoadShaderFromMemory(VertexSource, FragmentSource); - } - - public void Dispose() - { - if (_disposed) return; - _disposed = true; - - foreach (var model in _modelCache.Values) - Raylib.UnloadModel(model); - _modelCache.Clear(); - - foreach (var texture in _textureCache.Values) - Raylib.UnloadTexture(texture); - _textureCache.Clear(); - - Raylib.UnloadShader(_shadowShader); - Rlgl.UnloadTexture(_shadowDepthTex); - Rlgl.UnloadFramebuffer(_shadowFbo); - Raylib.UnloadShader(_shader); - } - - private readonly record struct ScreenshotRequest(string Path, TaskCompletionSource? Tcs); - - private sealed class RaylibScreenshotProvider : IScreenshotProvider - { - private readonly RaylibRenderer _renderer; - - public RaylibScreenshotProvider(RaylibRenderer renderer) - { - _renderer = renderer; - } - - public Task CaptureAsync(string outputPath) => _renderer.CaptureAsync(outputPath); - } -} diff --git a/src/Engine.Graphics.Raylib/RaylibWindow.cs b/src/Engine.Graphics.Raylib/RaylibWindow.cs deleted file mode 100644 index 1588542..0000000 --- a/src/Engine.Graphics.Raylib/RaylibWindow.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using Engine.Core; -using Raylib_cs; - -namespace Engine.Graphics.RaylibBackend; - -/// -/// Raylib-backed implementation of . -/// Wraps Raylib's GLFW window creation, event polling, and input. -/// -public sealed class RaylibWindow : IWindow -{ - private readonly RaylibInputState _input = new(); - private bool _shouldClose; - private bool _disposed; - - public int Width => Raylib.GetScreenWidth(); - public int Height => Raylib.GetScreenHeight(); - public bool ShouldClose => _shouldClose; - public IInputState Input => _input; - public nint Handle => 0; - - public RaylibWindow(string title, int width, int height) - { - Raylib.SetConfigFlags(ConfigFlags.VSyncHint); - Raylib.InitWindow(width, height, title); - Raylib.SetTargetFPS(0); - - // Present a blank frame so the window is visible immediately. - Raylib.BeginDrawing(); - Raylib.ClearBackground(new Color(25, 30, 40, 255)); - Raylib.EndDrawing(); - } - - public void PumpEvents() - { - _input.Poll(); - _shouldClose = Raylib.WindowShouldClose() || _shouldClose; - - if (Raylib.IsKeyPressed(KeyboardKey.Escape)) - _shouldClose = true; - } - - public void Close() => _shouldClose = true; - - public string[] GetRequiredVulkanExtensions() => Array.Empty(); - - public void Dispose() - { - if (_disposed) return; - _disposed = true; - Raylib.CloseWindow(); - } -} diff --git a/src/Engine.Graphics.Vulkan/Engine.Graphics.Vulkan.csproj b/src/Engine.Graphics.Vulkan/Engine.Graphics.Vulkan.csproj deleted file mode 100644 index 9ee4295..0000000 --- a/src/Engine.Graphics.Vulkan/Engine.Graphics.Vulkan.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - - net9.0 - enable - enable - true - true - Engine.Graphics.Vulkan - Engine.Graphics.Vulkan - - - - DEV_MODE - - - - RELEASE_AOT - true - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Engine.Graphics.Vulkan/IndexBuffer.cs b/src/Engine.Graphics.Vulkan/IndexBuffer.cs deleted file mode 100644 index a2336ae..0000000 --- a/src/Engine.Graphics.Vulkan/IndexBuffer.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using Silk.NET.Core; -using Silk.NET.Vulkan; - -namespace Engine.Graphics; - -/// -/// GPU index buffer for indexed draws. -/// Uses 32-bit indices. -/// -public sealed unsafe class IndexBuffer : IDisposable -{ - private readonly VulkanContext _context; - public Silk.NET.Vulkan.Buffer Buffer { get; } - public DeviceMemory Memory { get; } - public ulong Size { get; } - public uint Count { get; } - - public IndexBuffer(VulkanContext context, ReadOnlySpan data, uint count) - { - _context = context; - Size = (ulong)data.Length; - Count = count; - - Buffer = CreateBuffer(Size, BufferUsageFlags.IndexBufferBit); - var memoryRequirements = GetMemoryRequirements(Buffer); - Memory = AllocateMemory(memoryRequirements, MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit); - - var result = _context.Vk.BindBufferMemory(_context.Device, Buffer, Memory, 0); - if (result != Result.Success) - throw new InvalidOperationException($"vkBindBufferMemory failed: {result}"); - - CopyData(data); - } - - private Silk.NET.Vulkan.Buffer CreateBuffer(ulong size, BufferUsageFlags usage) - { - var createInfo = new BufferCreateInfo - { - SType = StructureType.BufferCreateInfo, - Size = size, - Usage = usage, - SharingMode = SharingMode.Exclusive - }; - - Silk.NET.Vulkan.Buffer buffer; - var result = _context.Vk.CreateBuffer(_context.Device, &createInfo, null, &buffer); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreateBuffer failed: {result}"); - return buffer; - } - - private MemoryRequirements GetMemoryRequirements(Silk.NET.Vulkan.Buffer buffer) - { - MemoryRequirements requirements; - _context.Vk.GetBufferMemoryRequirements(_context.Device, buffer, &requirements); - return requirements; - } - - private DeviceMemory AllocateMemory(MemoryRequirements requirements, MemoryPropertyFlags properties) - { - var memoryTypeIndex = FindMemoryType(requirements.MemoryTypeBits, properties); - var allocateInfo = new MemoryAllocateInfo - { - SType = StructureType.MemoryAllocateInfo, - AllocationSize = requirements.Size, - MemoryTypeIndex = memoryTypeIndex - }; - - DeviceMemory memory; - var result = _context.Vk.AllocateMemory(_context.Device, &allocateInfo, null, &memory); - if (result != Result.Success) - throw new InvalidOperationException($"vkAllocateMemory failed: {result}"); - return memory; - } - - private uint FindMemoryType(uint typeFilter, MemoryPropertyFlags properties) - { - PhysicalDeviceMemoryProperties memoryProperties; - _context.Vk.GetPhysicalDeviceMemoryProperties(_context.PhysicalDevice, &memoryProperties); - for (var i = 0; i < memoryProperties.MemoryTypeCount; i++) - { - if ((typeFilter & (1u << i)) != 0 && - (memoryProperties.MemoryTypes[i].PropertyFlags & properties) == properties) - { - return (uint)i; - } - } - throw new InvalidOperationException("Failed to find suitable memory type."); - } - - private void CopyData(ReadOnlySpan data) - { - void* mappedData; - var result = _context.Vk.MapMemory(_context.Device, Memory, 0, Size, MemoryMapFlags.None, &mappedData); - if (result != Result.Success) - throw new InvalidOperationException($"vkMapMemory failed: {result}"); - - fixed (byte* src = data) - { - global::System.Buffer.MemoryCopy(src, mappedData, (long)Size, data.Length); - } - - _context.Vk.UnmapMemory(_context.Device, Memory); - } - - public void Dispose() - { - _context.Vk.DeviceWaitIdle(_context.Device); - _context.Vk.DestroyBuffer(_context.Device, Buffer, null); - _context.Vk.FreeMemory(_context.Device, Memory, null); - } -} diff --git a/src/Engine.Graphics.Vulkan/ScreenshotCapture.cs b/src/Engine.Graphics.Vulkan/ScreenshotCapture.cs deleted file mode 100644 index 90953ef..0000000 --- a/src/Engine.Graphics.Vulkan/ScreenshotCapture.cs +++ /dev/null @@ -1,310 +0,0 @@ -using System; -using System.IO; -using Engine.Core; -using Silk.NET.Core; -using Silk.NET.Vulkan; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; - -namespace Engine.Graphics; - -/// -/// Captures the current swapchain image to a PNG file on disk. -/// Used by AI agents to visually inspect the running engine. -/// -public sealed unsafe class ScreenshotCapture : IDisposable, IScreenshotProvider -{ - private readonly VulkanContext _context; - private readonly Swapchain _swapchain; - private Silk.NET.Vulkan.Buffer _stagingBuffer; - private DeviceMemory _stagingMemory; - private ulong _stagingSize; - private bool _requested; - private string _outputPath = string.Empty; - private bool _ready; - private bool _captureToMemory; - private MemoryStream? _memoryOutput; - private TaskCompletionSource? _captureTcs; - - public ScreenshotCapture(VulkanContext context, Swapchain swapchain) - { - _context = context; - _swapchain = swapchain; - } - - /// - /// Request a screenshot to be captured on the next frame and saved to disk. - /// - public void Request(string outputPath) - { - _outputPath = outputPath; - _requested = true; - _ready = false; - _captureToMemory = false; - _memoryOutput = null; - _captureTcs = null; - } - - /// - /// Request a screenshot of the next rendered frame. The returned task completes once the - /// PNG bytes are available. The image is also saved to on disk. - /// - public Task CaptureAsync(string outputPath) - { - _outputPath = outputPath; - _requested = true; - _ready = false; - _captureToMemory = true; - _memoryOutput = new MemoryStream(); - _captureTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - return _captureTcs.Task; - } - - /// - /// True if a screenshot has been requested but not yet saved. - /// - public bool IsRequested => _requested; - - /// - /// Records the image readback commands into the given command buffer. - /// Must be called after the render pass has ended and before the image is presented. - /// - public void RecordReadback(CommandBuffer cmd, Silk.NET.Vulkan.Image sourceImage, uint width, uint height, Format format) - { - if (!_requested) - return; - - var pixelSize = GetPixelSize(format); - var rowPitch = width * pixelSize; - var imageSize = rowPitch * height; - - EnsureStagingBuffer(imageSize); - - // Transition from present layout to transfer source. - var barrier = new ImageMemoryBarrier - { - SType = StructureType.ImageMemoryBarrier, - OldLayout = ImageLayout.PresentSrcKhr, - NewLayout = ImageLayout.TransferSrcOptimal, - SrcAccessMask = AccessFlags.None, - DstAccessMask = AccessFlags.TransferReadBit, - Image = sourceImage, - SubresourceRange = new ImageSubresourceRange(ImageAspectFlags.ColorBit, 0, 1, 0, 1) - }; - - _context.Vk.CmdPipelineBarrier(cmd, PipelineStageFlags.TransferBit, PipelineStageFlags.TransferBit, 0, 0, null, 0, null, 1, &barrier); - - var copyRegion = new BufferImageCopy - { - BufferOffset = 0, - BufferRowLength = 0, - BufferImageHeight = 0, - ImageSubresource = new ImageSubresourceLayers(ImageAspectFlags.ColorBit, 0, 0, 1), - ImageOffset = new Offset3D(0, 0, 0), - ImageExtent = new Extent3D(width, height, 1) - }; - - _context.Vk.CmdCopyImageToBuffer(cmd, sourceImage, ImageLayout.TransferSrcOptimal, _stagingBuffer, 1, ©Region); - - // Transition back to present layout. - barrier.OldLayout = ImageLayout.TransferSrcOptimal; - barrier.NewLayout = ImageLayout.PresentSrcKhr; - barrier.SrcAccessMask = AccessFlags.TransferReadBit; - barrier.DstAccessMask = AccessFlags.None; - - _context.Vk.CmdPipelineBarrier(cmd, PipelineStageFlags.TransferBit, PipelineStageFlags.TransferBit, 0, 0, null, 0, null, 1, &barrier); - - _ready = true; - } - - /// - /// Save the captured pixels to disk. Must be called after the command buffer containing the readback has finished. - /// If the request was made with the PNG bytes are also written to memory and the task is completed. - /// - public void Save(uint width, uint height, Format format) - { - if (!_ready) - return; - - var pixelSize = GetPixelSize(format); - var rowPitch = width * pixelSize; - var imageSize = rowPitch * height; - - void* mappedData; - var result = _context.Vk.MapMemory(_context.Device, _stagingMemory, 0, imageSize, MemoryMapFlags.None, &mappedData); - if (result != Result.Success) - throw new InvalidOperationException($"vkMapMemory failed: {result}"); - - try - { - var directory = Path.GetDirectoryName(_outputPath); - if (!string.IsNullOrEmpty(directory)) - Directory.CreateDirectory(directory); - - SavePixels(mappedData, width, height, rowPitch, format); - } - finally - { - _context.Vk.UnmapMemory(_context.Device, _stagingMemory); - } - - Console.WriteLine($"Screenshot saved: {_outputPath}"); - _requested = false; - _ready = false; - _captureToMemory = false; - _memoryOutput = null; - _captureTcs = null; - } - - private void SavePixels(void* mappedData, uint width, uint height, uint rowPitch, Format format) - { - using var image = CreateImage(mappedData, width, height, rowPitch, format); - image.SaveAsPng(_outputPath); - - if (_captureToMemory && _memoryOutput != null) - { - image.SaveAsPng(_memoryOutput); - var bytes = _memoryOutput.ToArray(); - _captureTcs?.TrySetResult(bytes); - } - } - - private SixLabors.ImageSharp.Image CreateImage(void* mappedData, uint width, uint height, uint rowPitch, Format format) - { - var image = new SixLabors.ImageSharp.Image((int)width, (int)height); - var src = (byte*)mappedData; - - if (format == Format.B8G8R8A8Unorm || format == Format.B8G8R8A8Srgb) - { - for (var y = 0; y < height; y++) - { - var rowStart = src + y * rowPitch; - for (var x = 0; x < width; x++) - { - var b = rowStart[x * 4 + 0]; - var g = rowStart[x * 4 + 1]; - var r = rowStart[x * 4 + 2]; - var a = rowStart[x * 4 + 3]; - image[x, y] = new Rgba32(r, g, b, a); - } - } - return image; - } - - if (format == Format.R8G8B8A8Unorm || format == Format.R8G8B8A8Srgb) - { - for (var y = 0; y < height; y++) - { - var rowStart = src + y * rowPitch; - for (var x = 0; x < width; x++) - { - var r = rowStart[x * 4 + 0]; - var g = rowStart[x * 4 + 1]; - var b = rowStart[x * 4 + 2]; - var a = rowStart[x * 4 + 3]; - image[x, y] = new Rgba32(r, g, b, a); - } - } - return image; - } - - image.Dispose(); - throw new NotSupportedException($"Screenshot format {format} is not supported."); - } - - private void EnsureStagingBuffer(ulong size) - { - if (_stagingSize >= size) - return; - - if (_stagingBuffer.Handle != 0) - { - _context.Vk.DestroyBuffer(_context.Device, _stagingBuffer, null); - _context.Vk.FreeMemory(_context.Device, _stagingMemory, null); - } - - _stagingSize = size; - _stagingBuffer = CreateBuffer(size, BufferUsageFlags.TransferDstBit); - var requirements = GetMemoryRequirements(_stagingBuffer); - _stagingMemory = AllocateMemory(requirements, MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit); - - var result = _context.Vk.BindBufferMemory(_context.Device, _stagingBuffer, _stagingMemory, 0); - if (result != Result.Success) - throw new InvalidOperationException($"vkBindBufferMemory failed: {result}"); - } - - private Silk.NET.Vulkan.Buffer CreateBuffer(ulong size, BufferUsageFlags usage) - { - var createInfo = new BufferCreateInfo - { - SType = StructureType.BufferCreateInfo, - Size = size, - Usage = usage, - SharingMode = SharingMode.Exclusive - }; - - Silk.NET.Vulkan.Buffer buffer; - var result = _context.Vk.CreateBuffer(_context.Device, &createInfo, null, &buffer); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreateBuffer failed: {result}"); - return buffer; - } - - private MemoryRequirements GetMemoryRequirements(Silk.NET.Vulkan.Buffer buffer) - { - MemoryRequirements requirements; - _context.Vk.GetBufferMemoryRequirements(_context.Device, buffer, &requirements); - return requirements; - } - - private DeviceMemory AllocateMemory(MemoryRequirements requirements, MemoryPropertyFlags properties) - { - var memoryTypeIndex = FindMemoryType(requirements.MemoryTypeBits, properties); - var allocInfo = new MemoryAllocateInfo - { - SType = StructureType.MemoryAllocateInfo, - AllocationSize = requirements.Size, - MemoryTypeIndex = memoryTypeIndex - }; - - DeviceMemory memory; - var result = _context.Vk.AllocateMemory(_context.Device, &allocInfo, null, &memory); - if (result != Result.Success) - throw new InvalidOperationException($"vkAllocateMemory failed: {result}"); - return memory; - } - - private uint FindMemoryType(uint typeFilter, MemoryPropertyFlags properties) - { - PhysicalDeviceMemoryProperties memoryProperties; - _context.Vk.GetPhysicalDeviceMemoryProperties(_context.PhysicalDevice, &memoryProperties); - for (var i = 0; i < memoryProperties.MemoryTypeCount; i++) - { - if ((typeFilter & (1u << i)) != 0 && - (memoryProperties.MemoryTypes[i].PropertyFlags & properties) == properties) - { - return (uint)i; - } - } - throw new InvalidOperationException("Failed to find suitable memory type."); - } - - private static uint GetPixelSize(Format format) - { - return format switch - { - Format.B8G8R8A8Unorm or Format.B8G8R8A8Srgb or Format.R8G8B8A8Unorm or Format.R8G8B8A8Srgb => 4, - _ => throw new NotSupportedException($"Format {format} is not supported for screenshots.") - }; - } - - public void Dispose() - { - if (_stagingBuffer.Handle != 0) - { - _context.Vk.DeviceWaitIdle(_context.Device); - _context.Vk.DestroyBuffer(_context.Device, _stagingBuffer, null); - _context.Vk.FreeMemory(_context.Device, _stagingMemory, null); - } - } -} diff --git a/src/Engine.Graphics.Vulkan/ShaderLoader.cs b/src/Engine.Graphics.Vulkan/ShaderLoader.cs deleted file mode 100644 index 27cc0a2..0000000 --- a/src/Engine.Graphics.Vulkan/ShaderLoader.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.IO; -using System.Reflection; - -namespace Engine.Graphics; - -/// -/// Loads SPIR-V shader bytecode embedded in the assembly. -/// -public static class ShaderLoader -{ - public static byte[] Load(string name) - { - var assembly = Assembly.GetExecutingAssembly(); - var resourceName = $"Engine.Graphics.Vulkan.Shaders.{name}"; - - using var stream = assembly.GetManifestResourceStream(resourceName) - ?? throw new InvalidOperationException($"Embedded shader resource not found: {resourceName}"); - - using var memory = new MemoryStream(); - stream.CopyTo(memory); - return memory.ToArray(); - } -} diff --git a/src/Engine.Graphics.Vulkan/Shaders/fragment.frag b/src/Engine.Graphics.Vulkan/Shaders/fragment.frag deleted file mode 100644 index 1372e2e..0000000 --- a/src/Engine.Graphics.Vulkan/Shaders/fragment.frag +++ /dev/null @@ -1,69 +0,0 @@ -#version 450 - -layout(location = 0) in vec3 fragColor; -layout(location = 1) in vec3 fragNormal; -layout(location = 2) in vec3 fragWorldPos; -layout(location = 3) in vec2 fragUv; - -layout(location = 0) out vec4 outColor; - -struct Light -{ - vec3 direction; - float intensity; - vec3 color; - float _pad; -}; - -layout(set = 0, binding = 0) uniform FrameConstants -{ - vec3 cameraPosition; - uint lightCount; - vec3 ambientColor; - float _pad; - Light lights[4]; -} frame; - -layout(set = 1, binding = 0) uniform sampler2D albedoTexture; - -layout(push_constant) uniform PushConstants -{ - mat4 mvp; - vec3 materialAlbedo; - float materialRoughness; - float materialMetallic; - uint useTexture; - uint textureIndex; - uint _pad0; - uint _pad1; -} push; - -void main() -{ - vec3 normal = normalize(fragNormal); - vec3 viewDir = normalize(frame.cameraPosition - fragWorldPos); - vec3 albedo = fragColor * push.materialAlbedo; - if (push.useTexture != 0u) - { - albedo *= texture(albedoTexture, fragUv).rgb; - } - float roughness = clamp(push.materialRoughness, 0.05, 1.0); - float metallic = clamp(push.materialMetallic, 0.0, 1.0); - - vec3 result = frame.ambientColor * albedo; - - for (uint i = 0u; i < frame.lightCount; i++) - { - vec3 lightDir = normalize(-frame.lights[i].direction); - vec3 halfDir = normalize(lightDir + viewDir); - float diff = max(dot(normal, lightDir), 0.0); - float spec = pow(max(dot(normal, halfDir), 0.0), mix(8.0, 128.0, 1.0 - roughness)) * mix(0.5, 1.0, metallic); - - vec3 diffuse = frame.lights[i].color * diff * frame.lights[i].intensity; - vec3 specular = frame.lights[i].color * spec * frame.lights[i].intensity; - - result += diffuse * albedo + specular; - } - - outColor = vec4(result, 1.0); -} diff --git a/src/Engine.Graphics.Vulkan/Shaders/fragment.spv b/src/Engine.Graphics.Vulkan/Shaders/fragment.spv deleted file mode 100644 index f24c63f86b32c56ede278ecf16749d2a6d7f344e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4804 zcmZ9OX?GM=5Qdv1lZZwUMNmN;0s;!+zKaM!0SOW|K~!XtOvvagaTdT8a79JjaNltK z;un8|pY`v!o#Qz^Pfr*2NpmVy^;X@t?yaudLvPR0fh3ugT#%fX{FHQ`xk(R50@k1E zO(VNT*0kCaYi_;mMl&u-decO6<|OAQeQ2dts?-f!fGk9c$i2u}thc|a=21Hva!-kIWEQ@sT`VWqx<=%?=w+p zmdD$bh8qjqeOK!3a=le)Kbzu(G~YdqH>!PM0Q7)ag$yML*aq)yl(-daGTk zw_EZKq_{cbgqo$%2GnU$Yr4joRrfdC=+xO{tg&XPHdZOuiF8X7E2L}kwF9$PDGzn+ z)n)kfrdCF~Kl^U4_BNW;3ATZo=3R|0RmaK`4gEs;Zp@3Ku3bjXXs0!mI~r(Tipx_w zJdHn)`ktp&YL}apQgwaC+s_Qi;h7?~v(Y&;RWG;PNW?u?#J82(rE0Y@4n(}W2hZ4P zm3NnqwmVG@7Bvgt?Jl^bK2bi}T~kPF_T%`T98i1ZKAbhNo6z=ZIeMD&q44{YJJH>q zoGX>)IkWa=&*5&bhVh%ZbJ)MFV71)d<==(!KKWW=Gi!;>tRC&BUA=A7{;a;X6ywgTV4<5{on4pIaMe z8p&aCrqLYMhb`ak9OfRx_T@19EcSQ~b8lh?5zpsb=lh{=Kir>j=i7etX8zgCNAbRT z(mpLAYTW(L$BOs6fc4hXH(ta)4=twXpO3bd{ymxR42oTbKL=ezd=|rGUxPFBsw{6V zF?+L!zs{MtVvAYfe3&m^tTV?w%tO0(bKHZzb0X#*=A(;sDlm!r*J^XdGv7M;%QGzYxdPvPn7=ZM z$s6(2*w!_EeHOP)^z=q-pLv|nZ{rnIybbN1C=3_tZ zM$2ap&A$gNm$>=*&Y_s`4e01ioMR(ctbYL8zQ}1@y&3uaw~@8sZ^2KJdnNi>WI3_|iJa>){I#ssDty<8 z%jr82lf!$r29aYmB8Rhe10w!2Io`dSGt4{rCmgq;ef}Ze*e!^d^Pw$|ckV94`=;-m zT8ns}_QE@az7H`+d^;0mZ`L8=ijO_%LE?P(V|#`;-+FAZ?z-;z#QtVg>`Co;eUA?! zp4WRTXFqxqB0k6-^dCa3YoCm5M!dK7&lxrzeA}?)8$p8aVQlf>8^sPjW80D7+lp;m zKIdsC;ylIv?8&YSj~?vC&-Y+YmLu*Sy+>l&-XY(c{n&^09jX7|?EBG=AwI?*M;ljL z%UO5=k;DBRKtGANzZFR29K;s?g&gOlgt+b?;*97&idav6?N~dG-Q^!h&p&}Lzw>A; zf3M2;#*e{iUnUW;xI>4q?X&OMnCBlui%0J&*ka*7g?$lng1yW6G-8f;@K&*d*SNfX zd&GmMjy*f)H?Xa923}`s8Zk$FkV<0DAo6;bjXM`|*U-j-yM>+SZsS`&xI5V9hzIv! z?BF&ow|v%p7IBXK9_c@aY(d0hW}nA){Tk#5auhKy_+P*_ekEcp^Mn5-Z09<-j$sGa zi`dpO$2)BOFoW!j%|*3 z@V|i_{3o&BM1tSg3MBa6%HqNIHolzV?)?N>EV$pn4sP%FX(YIf<@f$>7LQu*;oFO- z^***a;=%s`cJRCR50T(Emf!oCEFOFx;maxR-cO;$_jEVV8dJOG1L+yI!;VZ;5ALpIwf1P2T)%C{2 z^54Au_)-5`Y-{Yu=I=XfF`vM`#}=EF!+yxH9`q<38bbewxZZr@&bE6vi#GNz40j^` E0g+;O2><{9 diff --git a/src/Engine.Graphics.Vulkan/Shaders/vertex.spv b/src/Engine.Graphics.Vulkan/Shaders/vertex.spv deleted file mode 100644 index dd808e8f6788764d8f5032480d45d5ce012d3636..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2652 zcmZ9NX>Zd&5Qa^2Q0}8ZIZH~p3#H{g#8CxNkP3vG4@kvLOl!%pE5|{=C-?(M{3U)B zUyyj8wWnRN)ns?xnVoreW|G!MrZxw`reIqz8r%uWwI>)65(sO$ePQv{;>@7v&0M`U zi^tBO<{a_a73^^5x<311oT7jnlAM&BlAMv8m7JH%NNV~U(f(Tj8bMuu8+RMZxlQ^? zveb$4w^3e1U!)2xan9JXL0rUHDs;2+hl|7Gw0ISNi|{iWxnyf6@!G?9P=skWTDS2M zY{Mj3w>8ACd=>nf!Cv`vhJ)31mU6ooxY%`v>lpMuueo^m!J{7*Q67iMe6k$%GI)$S zUwD}9Wj0({O`|~>tF*(#>`$X2Op>@ONcT$))M+?~UPoVwVXhbQK5{lBDih$7v}g5d zI9pS;rM0j}c10 zJ^GB#vn=n2Ng0d2$EhJ;O*aSlz02~Xr!;aAUU#zr{^oP3UlYCsj)S;}%bKHTwh!v~DMx;Ux?b+|2Eb?7t13%bGgftZhF<;=3(Ft~uR z8^#?lavH`NjGV+~9?IAo^UOTqkB!B=?3Vc06b3EB$P4zs!-xyE=wajs>v$Mkz+QV8 z9Khat7_$ua!NcGN_EEB+SMj?uAz`7P)b8u2DjmMqnOW?nGq31y$Ifq-uk-tbUX$>4 zn@KpZ46}M{_b{u6)snl!;qFlh`EF?Eoh_%=+F0lV|f%&;Cp4Gl3 zJNcQtNeRpJ6QvrADJpR%6^U4!59nlk$9126pc8k$K1a2;B&_L5-=Z^|k4oRF9e=P1 zotRCFfemTNk2;bQ`<+VXHyVsQJGHkY#3c{&J~P+&f=%dk%p?BP*Y4p1hg;g=gAW|gnM<(i`W~@Fu>0U@XIq=?q5g4CA{G28WK`L7$FGxQ{<}`uR(7%N~}w z@qyo@_7f6j7k>Bj0`!)|xSiISo?@fd=w>^oGyTLypV7@Wtut@JMvu|WcB!&G7b|)@ pD*;1TyyvI*VY@8BJ}2o^*p*5L`%z(6b;cJPIhhl%zpB_x$v=ZF%mM%a diff --git a/src/Engine.Graphics.Vulkan/Shaders/vertex.vert b/src/Engine.Graphics.Vulkan/Shaders/vertex.vert deleted file mode 100644 index 7344767..0000000 --- a/src/Engine.Graphics.Vulkan/Shaders/vertex.vert +++ /dev/null @@ -1,48 +0,0 @@ -#version 450 - -layout(location = 0) in vec3 inPosition; -layout(location = 1) in vec3 inColor; -layout(location = 2) in vec3 inNormal; - -layout(location = 0) out vec3 fragColor; -layout(location = 1) out vec3 fragNormal; -layout(location = 2) out vec3 fragWorldPos; -layout(location = 3) out vec2 fragUv; - -struct Light -{ - vec3 direction; - float intensity; - vec3 color; - float _pad; -}; - -layout(set = 0, binding = 0) uniform FrameConstants -{ - vec3 cameraPosition; - uint lightCount; - vec3 ambientColor; - float _pad; - Light lights[4]; -} frame; - -layout(push_constant) uniform PushConstants -{ - mat4 mvp; - vec3 materialAlbedo; - float materialRoughness; - float materialMetallic; - uint useTexture; - uint textureIndex; - uint _pad0; - uint _pad1; -} push; - -void main() -{ - gl_Position = push.mvp * vec4(inPosition, 1.0); - fragColor = inColor; - fragNormal = inNormal; - fragWorldPos = inPosition; - fragUv = inPosition.xz * 0.5 + 0.5; -} diff --git a/src/Engine.Graphics.Vulkan/Swapchain.cs b/src/Engine.Graphics.Vulkan/Swapchain.cs deleted file mode 100644 index 08924b0..0000000 --- a/src/Engine.Graphics.Vulkan/Swapchain.cs +++ /dev/null @@ -1,446 +0,0 @@ -using System; -using Silk.NET.Core; -using Silk.NET.Vulkan; -using Silk.NET.Vulkan.Extensions.KHR; - -namespace Engine.Graphics; - -/// -/// Manages the Vulkan swapchain, image views, render pass, and framebuffers. -/// Uses Silk.NET.Vulkan. -/// -public sealed unsafe class Swapchain : IDisposable -{ - private readonly VulkanContext _context; - private RenderPass _renderPass; - private SwapchainKHR _swapchain; - private Image[] _images = null!; - private ImageView[] _imageViews = null!; - private Framebuffer[] _framebuffers = null!; - private Image _depthImage; - private DeviceMemory _depthMemory; - private ImageView _depthImageView; - private Format _depthFormat; - private SurfaceFormatKHR _surfaceFormat; - private PresentModeKHR _presentMode; - private Extent2D _extent; - - public RenderPass RenderPass => _renderPass; - public Framebuffer[] Framebuffers => _framebuffers; - public Extent2D Extent => _extent; - public SwapchainKHR Handle => _swapchain; - public uint ImageCount => (uint)_images.Length; - public Format SurfaceFormat => _surfaceFormat.Format; - - public Image GetImage(uint index) => _images[index]; - - public Swapchain(VulkanContext context) - { - _context = context; - _surfaceFormat = ChooseSurfaceFormat(); - _depthFormat = FindDepthFormat(); - CreateRenderPass(); - Recreate(1280, 720); - } - - public void Recreate(int width, int height) - { - _context.Vk.DeviceWaitIdle(_context.Device); - CleanupSwapchain(); - - var capabilities = GetSurfaceCapabilities(); - _surfaceFormat = ChooseSurfaceFormat(); - _presentMode = ChoosePresentMode(); - _extent = ChooseExtent(capabilities, (uint)width, (uint)height); - - var imageCount = capabilities.MinImageCount + 1; - if (capabilities.MaxImageCount > 0 && imageCount > capabilities.MaxImageCount) - imageCount = capabilities.MaxImageCount; - - var createInfo = new SwapchainCreateInfoKHR - { - SType = StructureType.SwapchainCreateInfoKhr, - Surface = _context.Surface, - MinImageCount = imageCount, - ImageFormat = _surfaceFormat.Format, - ImageColorSpace = _surfaceFormat.ColorSpace, - ImageExtent = _extent, - ImageArrayLayers = 1, - ImageUsage = ImageUsageFlags.ColorAttachmentBit, - ImageSharingMode = SharingMode.Exclusive, - PreTransform = capabilities.CurrentTransform, - CompositeAlpha = CompositeAlphaFlagsKHR.OpaqueBitKhr, - PresentMode = _presentMode, - Clipped = true, - OldSwapchain = _swapchain - }; - - SwapchainKHR swapchain; - var result = _context.KhrSwapchain!.CreateSwapchain(_context.Device, &createInfo, null, &swapchain); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreateSwapchainKHR failed: {result}"); - _swapchain = swapchain; - - _images = GetSwapchainImages(); - _imageViews = new ImageView[_images.Length]; - _framebuffers = new Framebuffer[_images.Length]; - - CreateDepthResources(); - - for (var i = 0; i < _images.Length; i++) - { - _imageViews[i] = CreateImageView(_images[i], _surfaceFormat.Format); - _framebuffers[i] = CreateFramebuffer(_imageViews[i]); - } - } - - private SurfaceCapabilitiesKHR GetSurfaceCapabilities() - { - SurfaceCapabilitiesKHR capabilities; - var result = _context.KhrSurface!.GetPhysicalDeviceSurfaceCapabilities(_context.PhysicalDevice, _context.Surface, &capabilities); - if (result != Result.Success) - throw new InvalidOperationException($"vkGetPhysicalDeviceSurfaceCapabilitiesKHR failed: {result}"); - return capabilities; - } - - private Image[] GetSwapchainImages() - { - uint count = 0; - _context.KhrSwapchain!.GetSwapchainImages(_context.Device, _swapchain, &count, null); - var images = new Image[count]; - fixed (Image* p = images) - { - var result = _context.KhrSwapchain!.GetSwapchainImages(_context.Device, _swapchain, &count, p); - if (result != Result.Success) - throw new InvalidOperationException($"vkGetSwapchainImagesKHR failed: {result}"); - } - return images; - } - - private Format FindDepthFormat() - { - var candidates = new[] { Format.D32Sfloat, Format.D32SfloatS8Uint, Format.D24UnormS8Uint }; - foreach (var format in candidates) - { - FormatProperties props; - _context.Vk.GetPhysicalDeviceFormatProperties(_context.PhysicalDevice, format, &props); - if ((props.OptimalTilingFeatures & FormatFeatureFlags.DepthStencilAttachmentBit) != 0) - return format; - } - throw new InvalidOperationException("No supported depth format found."); - } - - private uint FindMemoryType(uint typeFilter, MemoryPropertyFlags properties) - { - PhysicalDeviceMemoryProperties memoryProperties; - _context.Vk.GetPhysicalDeviceMemoryProperties(_context.PhysicalDevice, &memoryProperties); - for (var i = 0; i < memoryProperties.MemoryTypeCount; i++) - { - if ((typeFilter & (1u << i)) != 0 && - (memoryProperties.MemoryTypes[i].PropertyFlags & properties) == properties) - { - return (uint)i; - } - } - throw new InvalidOperationException("Failed to find suitable memory type."); - } - - private void CreateDepthResources() - { - CreateDepthImage(); - CreateDepthImageView(); - } - - private void CreateDepthImage() - { - var createInfo = new ImageCreateInfo - { - SType = StructureType.ImageCreateInfo, - ImageType = ImageType.Type2D, - Extent = new Extent3D(_extent.Width, _extent.Height, 1), - MipLevels = 1, - ArrayLayers = 1, - Format = _depthFormat, - Tiling = ImageTiling.Optimal, - InitialLayout = ImageLayout.Undefined, - Usage = ImageUsageFlags.DepthStencilAttachmentBit, - Samples = SampleCountFlags.Count1Bit, - SharingMode = SharingMode.Exclusive - }; - - Image image; - var result = _context.Vk.CreateImage(_context.Device, &createInfo, null, &image); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreateImage failed: {result}"); - _depthImage = image; - - MemoryRequirements memRequirements; - _context.Vk.GetImageMemoryRequirements(_context.Device, image, &memRequirements); - - var memoryTypeIndex = FindMemoryType(memRequirements.MemoryTypeBits, MemoryPropertyFlags.DeviceLocalBit); - var allocInfo = new MemoryAllocateInfo - { - SType = StructureType.MemoryAllocateInfo, - AllocationSize = memRequirements.Size, - MemoryTypeIndex = memoryTypeIndex - }; - - DeviceMemory memory; - result = _context.Vk.AllocateMemory(_context.Device, &allocInfo, null, &memory); - if (result != Result.Success) - throw new InvalidOperationException($"vkAllocateMemory failed: {result}"); - _depthMemory = memory; - - result = _context.Vk.BindImageMemory(_context.Device, image, memory, 0); - if (result != Result.Success) - throw new InvalidOperationException($"vkBindImageMemory failed: {result}"); - } - - private void CreateDepthImageView() - { - var createInfo = new ImageViewCreateInfo - { - SType = StructureType.ImageViewCreateInfo, - Image = _depthImage, - ViewType = ImageViewType.Type2D, - Format = _depthFormat, - SubresourceRange = new ImageSubresourceRange(ImageAspectFlags.DepthBit, 0, 1, 0, 1) - }; - - ImageView imageView; - var result = _context.Vk.CreateImageView(_context.Device, &createInfo, null, &imageView); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreateImageView failed: {result}"); - _depthImageView = imageView; - } - - private void CleanupDepthResources() - { - if (_depthImageView.Handle != 0) - _context.Vk.DestroyImageView(_context.Device, _depthImageView, null); - if (_depthImage.Handle != 0) - _context.Vk.DestroyImage(_context.Device, _depthImage, null); - if (_depthMemory.Handle != 0) - _context.Vk.FreeMemory(_context.Device, _depthMemory, null); - } - - private void CreateRenderPass() - { - var colorAttachment = new AttachmentDescription - { - Format = _surfaceFormat.Format != Format.Undefined ? _surfaceFormat.Format : Format.B8G8R8A8Unorm, - Samples = SampleCountFlags.Count1Bit, - LoadOp = AttachmentLoadOp.Clear, - StoreOp = AttachmentStoreOp.Store, - StencilLoadOp = AttachmentLoadOp.DontCare, - StencilStoreOp = AttachmentStoreOp.DontCare, - InitialLayout = ImageLayout.Undefined, - FinalLayout = ImageLayout.PresentSrcKhr - }; - - var depthAttachment = new AttachmentDescription - { - Format = _depthFormat, - Samples = SampleCountFlags.Count1Bit, - LoadOp = AttachmentLoadOp.Clear, - StoreOp = AttachmentStoreOp.DontCare, - StencilLoadOp = AttachmentLoadOp.DontCare, - StencilStoreOp = AttachmentStoreOp.DontCare, - InitialLayout = ImageLayout.Undefined, - FinalLayout = ImageLayout.DepthStencilAttachmentOptimal - }; - - var attachments = new[] { colorAttachment, depthAttachment }; - - var colorAttachmentRef = new AttachmentReference - { - Attachment = 0, - Layout = ImageLayout.ColorAttachmentOptimal - }; - - var depthAttachmentRef = new AttachmentReference - { - Attachment = 1, - Layout = ImageLayout.DepthStencilAttachmentOptimal - }; - - var subpass = new SubpassDescription - { - PipelineBindPoint = PipelineBindPoint.Graphics, - ColorAttachmentCount = 1, - PColorAttachments = &colorAttachmentRef, - PDepthStencilAttachment = &depthAttachmentRef - }; - - var dependency = new SubpassDependency - { - SrcSubpass = ~0u, - DstSubpass = 0, - SrcStageMask = PipelineStageFlags.ColorAttachmentOutputBit, - DstStageMask = PipelineStageFlags.ColorAttachmentOutputBit, - SrcAccessMask = AccessFlags.None, - DstAccessMask = AccessFlags.ColorAttachmentWriteBit - }; - - fixed (AttachmentDescription* pAttachments = attachments) - { - var createInfo = new RenderPassCreateInfo - { - SType = StructureType.RenderPassCreateInfo, - AttachmentCount = (uint)attachments.Length, - PAttachments = pAttachments, - SubpassCount = 1, - PSubpasses = &subpass, - DependencyCount = 1, - PDependencies = &dependency - }; - - RenderPass renderPass; - var result = _context.Vk.CreateRenderPass(_context.Device, &createInfo, null, &renderPass); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreateRenderPass failed: {result}"); - _renderPass = renderPass; - } - } - - private ImageView CreateImageView(Image image, Format format) - { - var createInfo = new ImageViewCreateInfo - { - SType = StructureType.ImageViewCreateInfo, - Image = image, - ViewType = ImageViewType.Type2D, - Format = format, - Components = new ComponentMapping(ComponentSwizzle.R, ComponentSwizzle.G, ComponentSwizzle.B, ComponentSwizzle.A), - SubresourceRange = new ImageSubresourceRange(ImageAspectFlags.ColorBit, 0, 1, 0, 1) - }; - - ImageView imageView; - var result = _context.Vk.CreateImageView(_context.Device, &createInfo, null, &imageView); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreateImageView failed: {result}"); - return imageView; - } - - private Framebuffer CreateFramebuffer(ImageView imageView) - { - var attachments = new[] { imageView, _depthImageView }; - fixed (ImageView* pAttachments = attachments) - { - var createInfo = new FramebufferCreateInfo - { - SType = StructureType.FramebufferCreateInfo, - RenderPass = _renderPass, - AttachmentCount = (uint)attachments.Length, - PAttachments = pAttachments, - Width = _extent.Width, - Height = _extent.Height, - Layers = 1 - }; - - Framebuffer framebuffer; - var result = _context.Vk.CreateFramebuffer(_context.Device, &createInfo, null, &framebuffer); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreateFramebuffer failed: {result}"); - return framebuffer; - } - } - - private SurfaceFormatKHR ChooseSurfaceFormat() - { - var formats = GetSurfaceFormats(); - foreach (var format in formats) - { - if (format.Format == Format.B8G8R8A8Unorm && format.ColorSpace == ColorSpaceKHR.SpaceSrgbNonlinearKhr) - return format; - } - return formats[0]; - } - - private SurfaceFormatKHR[] GetSurfaceFormats() - { - uint count = 0; - _context.KhrSurface!.GetPhysicalDeviceSurfaceFormats(_context.PhysicalDevice, _context.Surface, &count, null); - var formats = new SurfaceFormatKHR[count]; - fixed (SurfaceFormatKHR* p = formats) - { - var result = _context.KhrSurface!.GetPhysicalDeviceSurfaceFormats(_context.PhysicalDevice, _context.Surface, &count, p); - if (result != Result.Success) - throw new InvalidOperationException($"vkGetPhysicalDeviceSurfaceFormatsKHR failed: {result}"); - } - return formats; - } - - private PresentModeKHR ChoosePresentMode() - { - var modes = GetSurfacePresentModes(); - if (Array.Exists(modes, m => m == PresentModeKHR.MailboxKhr)) - return PresentModeKHR.MailboxKhr; - return PresentModeKHR.FifoKhr; - } - - private PresentModeKHR[] GetSurfacePresentModes() - { - uint count = 0; - _context.KhrSurface!.GetPhysicalDeviceSurfacePresentModes(_context.PhysicalDevice, _context.Surface, &count, null); - var modes = new PresentModeKHR[count]; - fixed (PresentModeKHR* p = modes) - { - var result = _context.KhrSurface!.GetPhysicalDeviceSurfacePresentModes(_context.PhysicalDevice, _context.Surface, &count, p); - if (result != Result.Success) - throw new InvalidOperationException($"vkGetPhysicalDeviceSurfacePresentModesKHR failed: {result}"); - } - return modes; - } - - private Extent2D ChooseExtent(SurfaceCapabilitiesKHR capabilities, uint width, uint height) - { - if (capabilities.CurrentExtent.Width != uint.MaxValue) - return capabilities.CurrentExtent; - - var extent = new Extent2D - { - Width = Math.Clamp(width, capabilities.MinImageExtent.Width, capabilities.MaxImageExtent.Width), - Height = Math.Clamp(height, capabilities.MinImageExtent.Height, capabilities.MaxImageExtent.Height) - }; - return extent; - } - - private void CleanupSwapchain() - { - if (_context.Device.Handle == 0) - return; - - if (_framebuffers != null) - { - foreach (var fb in _framebuffers) - { - if (fb.Handle != 0) - _context.Vk.DestroyFramebuffer(_context.Device, fb, null); - } - } - - CleanupDepthResources(); - - if (_imageViews != null) - { - foreach (var view in _imageViews) - { - if (view.Handle != 0) - _context.Vk.DestroyImageView(_context.Device, view, null); - } - } - - if (_swapchain.Handle != 0) - _context.KhrSwapchain!.DestroySwapchain(_context.Device, _swapchain, null); - } - - public void Dispose() - { - _context.Vk.DeviceWaitIdle(_context.Device); - CleanupSwapchain(); - - if (_renderPass.Handle != 0) - _context.Vk.DestroyRenderPass(_context.Device, _renderPass, null); - } -} diff --git a/src/Engine.Graphics.Vulkan/Texture.cs b/src/Engine.Graphics.Vulkan/Texture.cs deleted file mode 100644 index b8e6cbb..0000000 --- a/src/Engine.Graphics.Vulkan/Texture.cs +++ /dev/null @@ -1,330 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Silk.NET.Vulkan; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; - -namespace Engine.Graphics; - -/// -/// A Vulkan texture: image, device memory, image view, and sampler. -/// -public sealed unsafe class Texture : IDisposable -{ - private readonly VulkanContext _context; - public Silk.NET.Vulkan.Image Image { get; } - public DeviceMemory Memory { get; } - public ImageView View { get; } - public Sampler Sampler { get; } - public uint Width { get; } - public uint Height { get; } - - public Texture(VulkanContext context, string path) - { - _context = context; - - using var image = SixLabors.ImageSharp.Image.Load(path); - Width = (uint)image.Width; - Height = (uint)image.Height; - - var pixels = new byte[Width * Height * 4]; - image.CopyPixelDataTo(pixels); - - Image = CreateImage(Width, Height); - var memoryRequirements = GetImageMemoryRequirements(Image); - Memory = AllocateMemory(memoryRequirements, MemoryPropertyFlags.DeviceLocalBit); - - var bindResult = _context.Vk.BindImageMemory(_context.Device, Image, Memory, 0); - if (bindResult != Result.Success) - throw new InvalidOperationException($"vkBindImageMemory failed: {bindResult}"); - - UploadPixels(pixels); - - View = CreateImageView(Image); - Sampler = CreateSampler(); - } - - private Silk.NET.Vulkan.Image CreateImage(uint width, uint height) - { - var createInfo = new ImageCreateInfo - { - SType = StructureType.ImageCreateInfo, - ImageType = ImageType.Type2D, - Extent = new Extent3D(width, height, 1), - MipLevels = 1, - ArrayLayers = 1, - Format = Format.R8G8B8A8Srgb, - Tiling = ImageTiling.Optimal, - InitialLayout = ImageLayout.Undefined, - Usage = ImageUsageFlags.TransferDstBit | ImageUsageFlags.SampledBit, - SharingMode = SharingMode.Exclusive, - Samples = SampleCountFlags.Count1Bit - }; - - Silk.NET.Vulkan.Image image; - var result = _context.Vk.CreateImage(_context.Device, &createInfo, null, &image); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreateImage failed: {result}"); - return image; - } - - private MemoryRequirements GetImageMemoryRequirements(Silk.NET.Vulkan.Image image) - { - MemoryRequirements requirements; - _context.Vk.GetImageMemoryRequirements(_context.Device, image, &requirements); - return requirements; - } - - private DeviceMemory AllocateMemory(MemoryRequirements requirements, MemoryPropertyFlags properties) - { - var memoryTypeIndex = FindMemoryType(requirements.MemoryTypeBits, properties); - var allocateInfo = new MemoryAllocateInfo - { - SType = StructureType.MemoryAllocateInfo, - AllocationSize = requirements.Size, - MemoryTypeIndex = memoryTypeIndex - }; - - DeviceMemory memory; - var result = _context.Vk.AllocateMemory(_context.Device, &allocateInfo, null, &memory); - if (result != Result.Success) - throw new InvalidOperationException($"vkAllocateMemory failed: {result}"); - return memory; - } - - private uint FindMemoryType(uint typeFilter, MemoryPropertyFlags properties) - { - PhysicalDeviceMemoryProperties memoryProperties; - _context.Vk.GetPhysicalDeviceMemoryProperties(_context.PhysicalDevice, &memoryProperties); - for (var i = 0; i < memoryProperties.MemoryTypeCount; i++) - { - if ((typeFilter & (1u << i)) != 0 && - (memoryProperties.MemoryTypes[i].PropertyFlags & properties) == properties) - { - return (uint)i; - } - } - throw new InvalidOperationException("Failed to find suitable memory type for texture."); - } - - private void UploadPixels(byte[] pixels) - { - var imageSize = (ulong)pixels.Length; - - var stagingBuffer = CreateBuffer(imageSize, BufferUsageFlags.TransferSrcBit); - var stagingMemory = AllocateStagingMemory(stagingBuffer); - - var bindResult = _context.Vk.BindBufferMemory(_context.Device, stagingBuffer, stagingMemory, 0); - if (bindResult != Result.Success) - throw new InvalidOperationException($"vkBindBufferMemory for staging failed: {bindResult}"); - - void* mappedData; - var mapResult = _context.Vk.MapMemory(_context.Device, stagingMemory, 0, imageSize, MemoryMapFlags.None, &mappedData); - if (mapResult != Result.Success) - throw new InvalidOperationException($"vkMapMemory failed: {mapResult}"); - - fixed (byte* src = pixels) - { - global::System.Buffer.MemoryCopy(src, mappedData, (long)imageSize, pixels.Length); - } - - _context.Vk.UnmapMemory(_context.Device, stagingMemory); - - ExecuteOneTimeCommand(cmd => - { - TransitionImageLayout(cmd, Image, ImageLayout.Undefined, ImageLayout.TransferDstOptimal); - - var bufferCopy = new BufferImageCopy - { - BufferOffset = 0, - BufferRowLength = 0, - BufferImageHeight = 0, - ImageSubresource = new ImageSubresourceLayers - { - AspectMask = ImageAspectFlags.ColorBit, - MipLevel = 0, - BaseArrayLayer = 0, - LayerCount = 1 - }, - ImageOffset = new Offset3D(0, 0, 0), - ImageExtent = new Extent3D(Width, Height, 1) - }; - - _context.Vk.CmdCopyBufferToImage(cmd, stagingBuffer, Image, ImageLayout.TransferDstOptimal, 1, &bufferCopy); - - TransitionImageLayout(cmd, Image, ImageLayout.TransferDstOptimal, ImageLayout.ShaderReadOnlyOptimal); - }); - - _context.Vk.FreeMemory(_context.Device, stagingMemory, null); - _context.Vk.DestroyBuffer(_context.Device, stagingBuffer, null); - } - - private Silk.NET.Vulkan.Buffer CreateBuffer(ulong size, BufferUsageFlags usage) - { - var createInfo = new BufferCreateInfo - { - SType = StructureType.BufferCreateInfo, - Size = size, - Usage = usage, - SharingMode = SharingMode.Exclusive - }; - - Silk.NET.Vulkan.Buffer buffer; - var result = _context.Vk.CreateBuffer(_context.Device, &createInfo, null, &buffer); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreateBuffer failed: {result}"); - return buffer; - } - - private DeviceMemory AllocateStagingMemory(Silk.NET.Vulkan.Buffer buffer) - { - var requirements = GetBufferMemoryRequirements(buffer); - return AllocateMemory(requirements, MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit); - } - - private MemoryRequirements GetBufferMemoryRequirements(Silk.NET.Vulkan.Buffer buffer) - { - MemoryRequirements requirements; - _context.Vk.GetBufferMemoryRequirements(_context.Device, buffer, &requirements); - return requirements; - } - - private void TransitionImageLayout(CommandBuffer cmd, Silk.NET.Vulkan.Image image, ImageLayout oldLayout, ImageLayout newLayout) - { - var barrier = new ImageMemoryBarrier - { - SType = StructureType.ImageMemoryBarrier, - OldLayout = oldLayout, - NewLayout = newLayout, - SrcQueueFamilyIndex = uint.MaxValue, - DstQueueFamilyIndex = uint.MaxValue, - Image = image, - SubresourceRange = new ImageSubresourceRange - { - AspectMask = ImageAspectFlags.ColorBit, - BaseMipLevel = 0, - LevelCount = 1, - BaseArrayLayer = 0, - LayerCount = 1 - } - }; - - var srcStage = PipelineStageFlags.TopOfPipeBit; - var dstStage = PipelineStageFlags.TransferBit; - AccessFlags srcAccessMask = 0; - AccessFlags dstAccessMask = AccessFlags.TransferWriteBit; - - if (oldLayout == ImageLayout.TransferDstOptimal && newLayout == ImageLayout.ShaderReadOnlyOptimal) - { - srcStage = PipelineStageFlags.TransferBit; - dstStage = PipelineStageFlags.FragmentShaderBit; - srcAccessMask = AccessFlags.TransferWriteBit; - dstAccessMask = AccessFlags.ShaderReadBit; - } - - barrier.SrcAccessMask = srcAccessMask; - barrier.DstAccessMask = dstAccessMask; - - _context.Vk.CmdPipelineBarrier(cmd, srcStage, dstStage, 0, 0, null, 0, null, 1, &barrier); - } - - private void ExecuteOneTimeCommand(Action action) - { - var allocInfo = new CommandBufferAllocateInfo - { - SType = StructureType.CommandBufferAllocateInfo, - CommandPool = _context.CommandPool, - Level = CommandBufferLevel.Primary, - CommandBufferCount = 1 - }; - - CommandBuffer commandBuffer; - var result = _context.Vk.AllocateCommandBuffers(_context.Device, &allocInfo, &commandBuffer); - if (result != Result.Success) - throw new InvalidOperationException($"vkAllocateCommandBuffers failed: {result}"); - - var beginInfo = new CommandBufferBeginInfo - { - SType = StructureType.CommandBufferBeginInfo, - Flags = CommandBufferUsageFlags.OneTimeSubmitBit - }; - _context.Vk.BeginCommandBuffer(commandBuffer, &beginInfo); - - action(commandBuffer); - - _context.Vk.EndCommandBuffer(commandBuffer); - - var submitInfo = new SubmitInfo - { - SType = StructureType.SubmitInfo, - CommandBufferCount = 1, - PCommandBuffers = &commandBuffer - }; - - _context.Vk.QueueSubmit(_context.GraphicsQueue, 1, &submitInfo, new Fence()); - _context.Vk.QueueWaitIdle(_context.GraphicsQueue); - - _context.Vk.FreeCommandBuffers(_context.Device, _context.CommandPool, 1, &commandBuffer); - } - - private ImageView CreateImageView(Silk.NET.Vulkan.Image image) - { - var createInfo = new ImageViewCreateInfo - { - SType = StructureType.ImageViewCreateInfo, - Image = image, - ViewType = ImageViewType.Type2D, - Format = Format.R8G8B8A8Srgb, - SubresourceRange = new ImageSubresourceRange - { - AspectMask = ImageAspectFlags.ColorBit, - BaseMipLevel = 0, - LevelCount = 1, - BaseArrayLayer = 0, - LayerCount = 1 - } - }; - - ImageView view; - var result = _context.Vk.CreateImageView(_context.Device, &createInfo, null, &view); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreateImageView failed: {result}"); - return view; - } - - private Sampler CreateSampler() - { - var createInfo = new SamplerCreateInfo - { - SType = StructureType.SamplerCreateInfo, - MagFilter = Filter.Linear, - MinFilter = Filter.Linear, - AddressModeU = SamplerAddressMode.Repeat, - AddressModeV = SamplerAddressMode.Repeat, - AddressModeW = SamplerAddressMode.Repeat, - AnisotropyEnable = false, - BorderColor = BorderColor.IntOpaqueBlack, - UnnormalizedCoordinates = false, - CompareEnable = false, - MipmapMode = SamplerMipmapMode.Linear, - MipLodBias = 0.0f, - MinLod = 0.0f, - MaxLod = 1.0f - }; - - Sampler sampler; - var result = _context.Vk.CreateSampler(_context.Device, &createInfo, null, &sampler); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreateSampler failed: {result}"); - return sampler; - } - - public void Dispose() - { - _context.Vk.DeviceWaitIdle(_context.Device); - _context.Vk.DestroySampler(_context.Device, Sampler, null); - _context.Vk.DestroyImageView(_context.Device, View, null); - _context.Vk.DestroyImage(_context.Device, Image, null); - _context.Vk.FreeMemory(_context.Device, Memory, null); - } -} diff --git a/src/Engine.Graphics.Vulkan/UniformBuffer.cs b/src/Engine.Graphics.Vulkan/UniformBuffer.cs deleted file mode 100644 index 8b99512..0000000 --- a/src/Engine.Graphics.Vulkan/UniformBuffer.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using Silk.NET.Vulkan; - -namespace Engine.Graphics; - -/// -/// A host-visible, coherent Vulkan buffer for uniform data that is updated every frame. -/// -public sealed unsafe class UniformBuffer : IDisposable -{ - private readonly VulkanContext _context; - public Silk.NET.Vulkan.Buffer Buffer { get; } - public DeviceMemory Memory { get; } - public ulong Size { get; } - - public UniformBuffer(VulkanContext context, ulong size) - { - _context = context; - Size = size; - - Buffer = CreateBuffer(Size, BufferUsageFlags.UniformBufferBit); - var memoryRequirements = GetMemoryRequirements(Buffer); - Memory = AllocateMemory(memoryRequirements, MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit); - - var result = _context.Vk.BindBufferMemory(_context.Device, Buffer, Memory, 0); - if (result != Result.Success) - throw new InvalidOperationException($"vkBindBufferMemory failed: {result}"); - } - - private Silk.NET.Vulkan.Buffer CreateBuffer(ulong size, BufferUsageFlags usage) - { - var createInfo = new BufferCreateInfo - { - SType = StructureType.BufferCreateInfo, - Size = size, - Usage = usage, - SharingMode = SharingMode.Exclusive - }; - - Silk.NET.Vulkan.Buffer buffer; - var result = _context.Vk.CreateBuffer(_context.Device, &createInfo, null, &buffer); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreateBuffer failed: {result}"); - return buffer; - } - - private MemoryRequirements GetMemoryRequirements(Silk.NET.Vulkan.Buffer buffer) - { - MemoryRequirements requirements; - _context.Vk.GetBufferMemoryRequirements(_context.Device, buffer, &requirements); - return requirements; - } - - private DeviceMemory AllocateMemory(MemoryRequirements requirements, MemoryPropertyFlags properties) - { - var memoryTypeIndex = FindMemoryType(requirements.MemoryTypeBits, properties); - var allocateInfo = new MemoryAllocateInfo - { - SType = StructureType.MemoryAllocateInfo, - AllocationSize = requirements.Size, - MemoryTypeIndex = memoryTypeIndex - }; - - DeviceMemory memory; - var result = _context.Vk.AllocateMemory(_context.Device, &allocateInfo, null, &memory); - if (result != Result.Success) - throw new InvalidOperationException($"vkAllocateMemory failed: {result}"); - return memory; - } - - private uint FindMemoryType(uint typeFilter, MemoryPropertyFlags properties) - { - PhysicalDeviceMemoryProperties memoryProperties; - _context.Vk.GetPhysicalDeviceMemoryProperties(_context.PhysicalDevice, &memoryProperties); - for (var i = 0; i < memoryProperties.MemoryTypeCount; i++) - { - if ((typeFilter & (1u << i)) != 0 && - (memoryProperties.MemoryTypes[i].PropertyFlags & properties) == properties) - { - return (uint)i; - } - } - throw new InvalidOperationException("Failed to find suitable memory type for uniform buffer."); - } - - public void Update(ReadOnlySpan data) - { - if ((ulong)data.Length != Size) - throw new ArgumentException($"Uniform buffer update size mismatch: {data.Length} != {Size}"); - - void* mappedData; - var result = _context.Vk.MapMemory(_context.Device, Memory, 0, Size, MemoryMapFlags.None, &mappedData); - if (result != Result.Success) - throw new InvalidOperationException($"vkMapMemory failed: {result}"); - - fixed (byte* src = data) - { - global::System.Buffer.MemoryCopy(src, mappedData, (long)Size, data.Length); - } - - _context.Vk.UnmapMemory(_context.Device, Memory); - } - - public void Dispose() - { - _context.Vk.DeviceWaitIdle(_context.Device); - _context.Vk.DestroyBuffer(_context.Device, Buffer, null); - _context.Vk.FreeMemory(_context.Device, Memory, null); - } -} diff --git a/src/Engine.Graphics.Vulkan/VertexBuffer.cs b/src/Engine.Graphics.Vulkan/VertexBuffer.cs deleted file mode 100644 index b58d353..0000000 --- a/src/Engine.Graphics.Vulkan/VertexBuffer.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using Silk.NET.Core; -using Silk.NET.Vulkan; - -namespace Engine.Graphics; - -/// -/// Interleaved vertex buffer: vec2 position + vec3 color. -/// Uses Silk.NET.Vulkan. -/// -public sealed unsafe class VertexBuffer : IDisposable -{ - private readonly VulkanContext _context; - public Silk.NET.Vulkan.Buffer Buffer { get; } - public DeviceMemory Memory { get; } - public ulong Size { get; } - - public VertexBuffer(VulkanContext context, ReadOnlySpan data) - { - _context = context; - Size = (ulong)data.Length; - - Buffer = CreateBuffer(Size, BufferUsageFlags.VertexBufferBit); - var memoryRequirements = GetMemoryRequirements(Buffer); - Memory = AllocateMemory(memoryRequirements, MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit); - - var result = _context.Vk.BindBufferMemory(_context.Device, Buffer, Memory, 0); - if (result != Result.Success) - throw new InvalidOperationException($"vkBindBufferMemory failed: {result}"); - - CopyData(data); - } - - private Silk.NET.Vulkan.Buffer CreateBuffer(ulong size, BufferUsageFlags usage) - { - var createInfo = new BufferCreateInfo - { - SType = StructureType.BufferCreateInfo, - Size = size, - Usage = usage, - SharingMode = SharingMode.Exclusive - }; - - Silk.NET.Vulkan.Buffer buffer; - var result = _context.Vk.CreateBuffer(_context.Device, &createInfo, null, &buffer); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreateBuffer failed: {result}"); - return buffer; - } - - private MemoryRequirements GetMemoryRequirements(Silk.NET.Vulkan.Buffer buffer) - { - MemoryRequirements requirements; - _context.Vk.GetBufferMemoryRequirements(_context.Device, buffer, &requirements); - return requirements; - } - - private DeviceMemory AllocateMemory(MemoryRequirements requirements, MemoryPropertyFlags properties) - { - var memoryTypeIndex = FindMemoryType(requirements.MemoryTypeBits, properties); - var allocateInfo = new MemoryAllocateInfo - { - SType = StructureType.MemoryAllocateInfo, - AllocationSize = requirements.Size, - MemoryTypeIndex = memoryTypeIndex - }; - - DeviceMemory memory; - var result = _context.Vk.AllocateMemory(_context.Device, &allocateInfo, null, &memory); - if (result != Result.Success) - throw new InvalidOperationException($"vkAllocateMemory failed: {result}"); - return memory; - } - - private uint FindMemoryType(uint typeFilter, MemoryPropertyFlags properties) - { - PhysicalDeviceMemoryProperties memoryProperties; - _context.Vk.GetPhysicalDeviceMemoryProperties(_context.PhysicalDevice, &memoryProperties); - for (var i = 0; i < memoryProperties.MemoryTypeCount; i++) - { - if ((typeFilter & (1u << i)) != 0 && - (memoryProperties.MemoryTypes[i].PropertyFlags & properties) == properties) - { - return (uint)i; - } - } - throw new InvalidOperationException("Failed to find suitable memory type."); - } - - private void CopyData(ReadOnlySpan data) - { - void* mappedData; - var result = _context.Vk.MapMemory(_context.Device, Memory, 0, Size, MemoryMapFlags.None, &mappedData); - if (result != Result.Success) - throw new InvalidOperationException($"vkMapMemory failed: {result}"); - - fixed (byte* src = data) - { - global::System.Buffer.MemoryCopy(src, mappedData, (long)Size, data.Length); - } - - _context.Vk.UnmapMemory(_context.Device, Memory); - } - - public void Update(ReadOnlySpan data) - { - if ((ulong)data.Length != Size) - throw new ArgumentException($"Vertex buffer update size mismatch: {data.Length} != {Size}"); - - CopyData(data); - } - - public void Dispose() - { - _context.Vk.DeviceWaitIdle(_context.Device); - _context.Vk.DestroyBuffer(_context.Device, Buffer, null); - _context.Vk.FreeMemory(_context.Device, Memory, null); - } -} diff --git a/src/Engine.Graphics.Vulkan/VulkanBackendRegistrar.cs b/src/Engine.Graphics.Vulkan/VulkanBackendRegistrar.cs deleted file mode 100644 index a1b82bb..0000000 --- a/src/Engine.Graphics.Vulkan/VulkanBackendRegistrar.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Engine.Graphics; - -namespace Engine.Graphics.Vulkan; - -/// -/// Triggers registration of the Vulkan backend with the HAL factory. -/// -public static class VulkanBackendRegistrar -{ - static VulkanBackendRegistrar() - { - RenderBackendFactory.Register("vulkan", (width, height, enableValidation) => new VulkanRenderContext(width, height, enableValidation)); - } - - /// - /// No-op method that forces the static constructor to run. - /// Call this before using . - /// - public static void EnsureRegistered() { } -} diff --git a/src/Engine.Graphics.Vulkan/VulkanContext.cs b/src/Engine.Graphics.Vulkan/VulkanContext.cs deleted file mode 100644 index e9dc9db..0000000 --- a/src/Engine.Graphics.Vulkan/VulkanContext.cs +++ /dev/null @@ -1,321 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using Engine.Core; -using SDL; -using Silk.NET.Core; -using Silk.NET.Core.Native; -using Silk.NET.Vulkan; -using Silk.NET.Vulkan.Extensions.KHR; - -namespace Engine.Graphics; - -/// -/// Owns the Vulkan instance, physical device, logical device, queues, and surface. -/// Uses Silk.NET.Vulkan because Vortice.Vulkan's loader segfaulted on this Kubuntu setup. -/// -public sealed unsafe class VulkanContext : IDisposable -{ - private bool _disposed; - - public Vk Vk { get; } - public KhrSurface? KhrSurface { get; private set; } - public KhrSwapchain? KhrSwapchain { get; private set; } - public Instance Instance { get; private set; } - public PhysicalDevice PhysicalDevice { get; private set; } - public Device Device { get; private set; } - public Queue GraphicsQueue { get; private set; } - public Queue PresentQueue { get; private set; } - public SurfaceKHR Surface { get; private set; } - public uint GraphicsFamilyIndex { get; private set; } - public uint PresentFamilyIndex { get; private set; } - public CommandPool CommandPool { get; private set; } - - public VulkanContext(IWindow window, bool enableValidation = true) - { - Vk = Vk.GetApi(); - CreateInstance(window, enableValidation); - LoadInstanceExtensions(); - CreateSurface(window); - PickPhysicalDevice(); - CreateLogicalDevice(); - LoadDeviceExtensions(); - GetQueues(); - CreateCommandPool(); - } - - private void CreateCommandPool() - { - var createInfo = new CommandPoolCreateInfo - { - SType = StructureType.CommandPoolCreateInfo, - QueueFamilyIndex = GraphicsFamilyIndex, - Flags = CommandPoolCreateFlags.ResetCommandBufferBit - }; - - CommandPool commandPool; - var result = Vk.CreateCommandPool(Device, &createInfo, null, &commandPool); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreateCommandPool failed: {result}"); - CommandPool = commandPool; - } - - private void CreateInstance(IWindow window, bool enableValidation) - { - var requiredExtensions = new List(window.GetRequiredVulkanExtensions()); - if (enableValidation) - { - requiredExtensions.Add("VK_EXT_debug_utils"); - } - - var layerNames = enableValidation - ? new[] { "VK_LAYER_KHRONOS_validation" } - : Array.Empty(); - - var appName = SilkMarshal.StringToMemory("Cortex Engine", NativeStringEncoding.UTF8); - var engineName = SilkMarshal.StringToMemory("CortexEngine", NativeStringEncoding.UTF8); - var extensionMemory = SilkMarshal.StringArrayToMemory(requiredExtensions, NativeStringEncoding.UTF8); - var layerMemory = SilkMarshal.StringArrayToMemory(layerNames, NativeStringEncoding.UTF8); - - try - { - var appInfo = new ApplicationInfo - { - SType = StructureType.ApplicationInfo, - PApplicationName = (byte*)appName.Handle, - PEngineName = (byte*)engineName.Handle, - ApiVersion = Vk.Version13 - }; - - var createInfo = new InstanceCreateInfo - { - SType = StructureType.InstanceCreateInfo, - PApplicationInfo = &appInfo, - EnabledExtensionCount = (uint)requiredExtensions.Count, - PpEnabledExtensionNames = (byte**)extensionMemory.Handle, - EnabledLayerCount = (uint)layerNames.Length, - PpEnabledLayerNames = (byte**)layerMemory.Handle - }; - - Instance instance; - var result = Vk.CreateInstance(&createInfo, null, &instance); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreateInstance failed: {result}"); - Instance = instance; - } - finally - { - appName.Dispose(); - engineName.Dispose(); - extensionMemory.Dispose(); - layerMemory.Dispose(); - } - } - - private void LoadInstanceExtensions() - { - if (!Vk.TryGetInstanceExtension(Instance, out KhrSurface khrSurface)) - throw new InvalidOperationException("VK_KHR_surface not available."); - KhrSurface = khrSurface; - } - - private void LoadDeviceExtensions() - { - if (!Vk.TryGetDeviceExtension(Instance, Device, out KhrSwapchain khrSwapchain)) - throw new InvalidOperationException("VK_KHR_swapchain not available."); - KhrSwapchain = khrSwapchain; - } - - private void CreateSurface(IWindow window) - { - var sdlInstance = (SDL.VkInstance_T*)Instance.Handle; - var sdlSurface = (SDL.VkSurfaceKHR_T*)null; - var sdlResult = SDL3.SDL_Vulkan_CreateSurface( - (SDL_Window*)window.Handle, - sdlInstance, - null, - &sdlSurface); - - if (sdlResult != true) - throw new InvalidOperationException($"SDL_Vulkan_CreateSurface failed: {SDL3.SDL_GetError()}"); - - Surface = new SurfaceKHR((ulong)sdlSurface); - } - - private void PickPhysicalDevice() - { - var devices = EnumeratePhysicalDevices(); - if (devices.Length == 0) - throw new InvalidOperationException("No Vulkan physical devices found."); - - foreach (var device in devices) - { - var properties = Vk.GetPhysicalDeviceProperties(device); - var queueFamilies = GetPhysicalDeviceQueueFamilyProperties(device); - - var hasGraphics = false; - var hasPresent = false; - for (var i = 0; i < queueFamilies.Length; i++) - { - if (queueFamilies[i].QueueFlags.HasFlag(QueueFlags.GraphicsBit)) - hasGraphics = true; - - Bool32 supported; - KhrSurface!.GetPhysicalDeviceSurfaceSupport(device, (uint)i, Surface, &supported); - if (supported) - hasPresent = true; - } - - if (hasGraphics && hasPresent) - { - PhysicalDevice = device; - if (properties.DeviceType == PhysicalDeviceType.DiscreteGpu) - break; - } - } - - if (PhysicalDevice.Handle == 0) - throw new InvalidOperationException("No suitable Vulkan physical device found."); - } - - private PhysicalDevice[] EnumeratePhysicalDevices() - { - uint count = 0; - Vk.EnumeratePhysicalDevices(Instance, &count, null); - if (count == 0) - return Array.Empty(); - - var devices = new PhysicalDevice[count]; - fixed (PhysicalDevice* p = devices) - { - var result = Vk.EnumeratePhysicalDevices(Instance, &count, p); - if (result != Result.Success) - throw new InvalidOperationException($"vkEnumeratePhysicalDevices failed: {result}"); - } - return devices; - } - - private QueueFamilyProperties[] GetPhysicalDeviceQueueFamilyProperties(PhysicalDevice device) - { - uint count = 0; - Vk.GetPhysicalDeviceQueueFamilyProperties(device, &count, null); - var properties = new QueueFamilyProperties[count]; - fixed (QueueFamilyProperties* p = properties) - { - Vk.GetPhysicalDeviceQueueFamilyProperties(device, &count, p); - } - return properties; - } - - private void CreateLogicalDevice() - { - var queueFamilies = GetPhysicalDeviceQueueFamilyProperties(PhysicalDevice); - GraphicsFamilyIndex = FindQueueFamilyIndex(queueFamilies, QueueFlags.GraphicsBit); - PresentFamilyIndex = FindPresentQueueFamilyIndex(queueFamilies); - - var uniqueFamilies = new HashSet { GraphicsFamilyIndex, PresentFamilyIndex }; - var queueCreateInfos = uniqueFamilies.Select(family => new DeviceQueueCreateInfo - { - SType = StructureType.DeviceQueueCreateInfo, - QueueFamilyIndex = family, - QueueCount = 1 - }).ToArray(); - - var extensionNames = new[] { "VK_KHR_swapchain" }; - var extensionMemory = SilkMarshal.StringArrayToMemory(extensionNames, NativeStringEncoding.UTF8); - - var priorityHandles = new GCHandle[queueCreateInfos.Length]; - try - { - var deviceFeatures = new PhysicalDeviceFeatures(); - - for (var i = 0; i < queueCreateInfos.Length; i++) - { - var priority = new[] { 1.0f }; - var handle = GCHandle.Alloc(priority, GCHandleType.Pinned); - priorityHandles[i] = handle; - queueCreateInfos[i].PQueuePriorities = (float*)handle.AddrOfPinnedObject(); - } - - fixed (DeviceQueueCreateInfo* pQueue = queueCreateInfos) - { - var createInfo = new DeviceCreateInfo - { - SType = StructureType.DeviceCreateInfo, - QueueCreateInfoCount = (uint)queueCreateInfos.Length, - PQueueCreateInfos = pQueue, - PEnabledFeatures = &deviceFeatures, - EnabledExtensionCount = 1, - PpEnabledExtensionNames = (byte**)extensionMemory.Handle - }; - - Device device; - var result = Vk.CreateDevice(PhysicalDevice, &createInfo, null, &device); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreateDevice failed: {result}"); - Device = device; - } - } - finally - { - foreach (var handle in priorityHandles) - { - if (handle.IsAllocated) - handle.Free(); - } - extensionMemory.Dispose(); - } - } - - private void GetQueues() - { - Queue graphicsQueue; - Vk.GetDeviceQueue(Device, GraphicsFamilyIndex, 0, &graphicsQueue); - GraphicsQueue = graphicsQueue; - - Queue presentQueue; - Vk.GetDeviceQueue(Device, PresentFamilyIndex, 0, &presentQueue); - PresentQueue = presentQueue; - } - - private uint FindQueueFamilyIndex(QueueFamilyProperties[] properties, QueueFlags flags) - { - for (var i = 0; i < properties.Length; i++) - { - if (properties[i].QueueFlags.HasFlag(flags)) - return (uint)i; - } - throw new InvalidOperationException($"No queue family with flags {flags} found."); - } - - private uint FindPresentQueueFamilyIndex(QueueFamilyProperties[] properties) - { - for (var i = 0; i < properties.Length; i++) - { - Bool32 supported; - KhrSurface!.GetPhysicalDeviceSurfaceSupport(PhysicalDevice, (uint)i, Surface, &supported); - if (supported) - return (uint)i; - } - throw new InvalidOperationException("No present queue family found."); - } - - public void Dispose() - { - if (_disposed) return; - _disposed = true; - - Vk.DeviceWaitIdle(Device); - if (CommandPool.Handle != 0) - Vk.DestroyCommandPool(Device, CommandPool, null); - if (Device.Handle != 0) - Vk.DestroyDevice(Device, null); - if (Surface.Handle != 0) - KhrSurface?.DestroySurface(Instance, Surface, null); - if (Instance.Handle != 0) - Vk.DestroyInstance(Instance, null); - Vk.Dispose(); - } -} diff --git a/src/Engine.Graphics.Vulkan/VulkanPipeline.cs b/src/Engine.Graphics.Vulkan/VulkanPipeline.cs deleted file mode 100644 index 1d15b06..0000000 --- a/src/Engine.Graphics.Vulkan/VulkanPipeline.cs +++ /dev/null @@ -1,306 +0,0 @@ -using System; -using Silk.NET.Core.Native; -using Silk.NET.Vulkan; - -namespace Engine.Graphics; - -/// -/// Graphics pipeline for indexed meshes with push-constant MVP and depth testing. -/// Uses Silk.NET.Vulkan. -/// -public sealed unsafe class VulkanPipeline : IDisposable -{ - private readonly VulkanContext _context; - private readonly Swapchain _swapchain; - - public Pipeline Handle { get; } - public PipelineLayout Layout { get; } - public DescriptorSetLayout FrameDescriptorSetLayout { get; } - public DescriptorSetLayout TextureDescriptorSetLayout { get; } - private readonly ShaderModule _vertexModule; - private readonly ShaderModule _fragmentModule; - - public VulkanPipeline(VulkanContext context, Swapchain swapchain) - { - _context = context; - _swapchain = swapchain; - - _vertexModule = CreateShaderModule("vertex.spv"); - _fragmentModule = CreateShaderModule("fragment.spv"); - - FrameDescriptorSetLayout = CreateFrameDescriptorSetLayout(); - TextureDescriptorSetLayout = CreateTextureDescriptorSetLayout(); - Layout = CreatePipelineLayout(); - Handle = CreateGraphicsPipeline(); - } - - private ShaderModule CreateShaderModule(string resourceName) - { - var code = ShaderLoader.Load(resourceName); - if (code.Length % 4 != 0) - throw new InvalidOperationException($"Shader {resourceName} size is not a multiple of 4."); - - fixed (byte* pCode = code) - { - var createInfo = new ShaderModuleCreateInfo - { - SType = StructureType.ShaderModuleCreateInfo, - CodeSize = (nuint)code.Length, - PCode = (uint*)pCode - }; - - ShaderModule module; - var result = _context.Vk.CreateShaderModule(_context.Device, &createInfo, null, &module); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreateShaderModule failed for {resourceName}: {result}"); - return module; - } - } - - private DescriptorSetLayout CreateFrameDescriptorSetLayout() - { - var binding = new DescriptorSetLayoutBinding - { - Binding = 0, - DescriptorType = DescriptorType.UniformBuffer, - DescriptorCount = 1, - StageFlags = ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit - }; - - var createInfo = new DescriptorSetLayoutCreateInfo - { - SType = StructureType.DescriptorSetLayoutCreateInfo, - BindingCount = 1, - PBindings = &binding - }; - - DescriptorSetLayout layout; - var result = _context.Vk.CreateDescriptorSetLayout(_context.Device, &createInfo, null, &layout); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreateDescriptorSetLayout failed: {result}"); - return layout; - } - - private DescriptorSetLayout CreateTextureDescriptorSetLayout() - { - var binding = new DescriptorSetLayoutBinding - { - Binding = 0, - DescriptorType = DescriptorType.CombinedImageSampler, - DescriptorCount = 1, - StageFlags = ShaderStageFlags.FragmentBit - }; - - var createInfo = new DescriptorSetLayoutCreateInfo - { - SType = StructureType.DescriptorSetLayoutCreateInfo, - BindingCount = 1, - PBindings = &binding - }; - - DescriptorSetLayout layout; - var result = _context.Vk.CreateDescriptorSetLayout(_context.Device, &createInfo, null, &layout); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreateDescriptorSetLayout (texture) failed: {result}"); - return layout; - } - - private PipelineLayout CreatePipelineLayout() - { - var pushConstantRange = new PushConstantRange - { - StageFlags = ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit, - Offset = 0, - Size = 96 - }; - - var setLayouts = stackalloc DescriptorSetLayout[] { FrameDescriptorSetLayout, TextureDescriptorSetLayout }; - var createInfo = new PipelineLayoutCreateInfo - { - SType = StructureType.PipelineLayoutCreateInfo, - SetLayoutCount = 2, - PSetLayouts = setLayouts, - PushConstantRangeCount = 1, - PPushConstantRanges = &pushConstantRange - }; - - PipelineLayout layout; - var result = _context.Vk.CreatePipelineLayout(_context.Device, &createInfo, null, &layout); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreatePipelineLayout failed: {result}"); - return layout; - } - - private Pipeline CreateGraphicsPipeline() - { - var entryName = SilkMarshal.StringToPtr("main", NativeStringEncoding.UTF8); - var stages = new[] - { - new PipelineShaderStageCreateInfo - { - SType = StructureType.PipelineShaderStageCreateInfo, - Stage = ShaderStageFlags.VertexBit, - Module = _vertexModule, - PName = (byte*)entryName - }, - new PipelineShaderStageCreateInfo - { - SType = StructureType.PipelineShaderStageCreateInfo, - Stage = ShaderStageFlags.FragmentBit, - Module = _fragmentModule, - PName = (byte*)entryName - } - }; - - var bindingDescription = new VertexInputBindingDescription - { - Binding = 0, - Stride = (uint)(9 * sizeof(float)), - InputRate = VertexInputRate.Vertex - }; - - var attributeDescriptions = new[] - { - new VertexInputAttributeDescription - { - Binding = 0, - Location = 0, - Format = Format.R32G32B32Sfloat, - Offset = 0 - }, - new VertexInputAttributeDescription - { - Binding = 0, - Location = 1, - Format = Format.R32G32B32Sfloat, - Offset = (uint)(3 * sizeof(float)) - }, - new VertexInputAttributeDescription - { - Binding = 0, - Location = 2, - Format = Format.R32G32B32Sfloat, - Offset = (uint)(6 * sizeof(float)) - } - }; - - PipelineVertexInputStateCreateInfo vertexInputInfo; - fixed (VertexInputAttributeDescription* pAttributes = attributeDescriptions) - { - vertexInputInfo = new PipelineVertexInputStateCreateInfo - { - SType = StructureType.PipelineVertexInputStateCreateInfo, - VertexBindingDescriptionCount = 1, - PVertexBindingDescriptions = &bindingDescription, - VertexAttributeDescriptionCount = (uint)attributeDescriptions.Length, - PVertexAttributeDescriptions = pAttributes - }; - } - - var inputAssembly = new PipelineInputAssemblyStateCreateInfo - { - SType = StructureType.PipelineInputAssemblyStateCreateInfo, - Topology = PrimitiveTopology.TriangleList, - PrimitiveRestartEnable = false - }; - - var viewportState = new PipelineViewportStateCreateInfo - { - SType = StructureType.PipelineViewportStateCreateInfo, - ViewportCount = 1, - ScissorCount = 1 - }; - - var rasterizer = new PipelineRasterizationStateCreateInfo - { - SType = StructureType.PipelineRasterizationStateCreateInfo, - PolygonMode = PolygonMode.Fill, - CullMode = CullModeFlags.None, - FrontFace = FrontFace.Clockwise, - LineWidth = 1.0f - }; - - var multisampling = new PipelineMultisampleStateCreateInfo - { - SType = StructureType.PipelineMultisampleStateCreateInfo, - RasterizationSamples = SampleCountFlags.Count1Bit, - SampleShadingEnable = false - }; - - var colorBlendAttachment = new PipelineColorBlendAttachmentState - { - ColorWriteMask = ColorComponentFlags.RBit | ColorComponentFlags.GBit | ColorComponentFlags.BBit | ColorComponentFlags.ABit - }; - - var colorBlending = new PipelineColorBlendStateCreateInfo - { - SType = StructureType.PipelineColorBlendStateCreateInfo, - AttachmentCount = 1, - PAttachments = &colorBlendAttachment - }; - - var depthStencil = new PipelineDepthStencilStateCreateInfo - { - SType = StructureType.PipelineDepthStencilStateCreateInfo, - DepthTestEnable = true, - DepthWriteEnable = true, - DepthCompareOp = CompareOp.Less, - DepthBoundsTestEnable = false, - StencilTestEnable = false, - Back = new StencilOpState(), - Front = new StencilOpState() - }; - - var dynamicStates = new[] { DynamicState.Viewport, DynamicState.Scissor }; - PipelineDynamicStateCreateInfo dynamicState; - fixed (DynamicState* pDynamic = dynamicStates) - { - dynamicState = new PipelineDynamicStateCreateInfo - { - SType = StructureType.PipelineDynamicStateCreateInfo, - DynamicStateCount = (uint)dynamicStates.Length, - PDynamicStates = pDynamic - }; - } - - Pipeline pipeline; - fixed (PipelineShaderStageCreateInfo* pStages = stages) - { - var createInfo = new GraphicsPipelineCreateInfo - { - SType = StructureType.GraphicsPipelineCreateInfo, - StageCount = (uint)stages.Length, - PStages = pStages, - PVertexInputState = &vertexInputInfo, - PInputAssemblyState = &inputAssembly, - PViewportState = &viewportState, - PRasterizationState = &rasterizer, - PMultisampleState = &multisampling, - PDepthStencilState = &depthStencil, - PColorBlendState = &colorBlending, - PDynamicState = &dynamicState, - Layout = Layout, - RenderPass = _swapchain.RenderPass, - Subpass = 0 - }; - - var result = _context.Vk.CreateGraphicsPipelines(_context.Device, default, 1, &createInfo, null, &pipeline); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreateGraphicsPipelines failed: {result}"); - } - - SilkMarshal.FreeString(entryName, NativeStringEncoding.UTF8); - return pipeline; - } - - public void Dispose() - { - _context.Vk.DeviceWaitIdle(_context.Device); - _context.Vk.DestroyPipeline(_context.Device, Handle, null); - _context.Vk.DestroyPipelineLayout(_context.Device, Layout, null); - _context.Vk.DestroyDescriptorSetLayout(_context.Device, FrameDescriptorSetLayout, null); - _context.Vk.DestroyDescriptorSetLayout(_context.Device, TextureDescriptorSetLayout, null); - _context.Vk.DestroyShaderModule(_context.Device, _vertexModule, null); - _context.Vk.DestroyShaderModule(_context.Device, _fragmentModule, null); - } -} diff --git a/src/Engine.Graphics.Vulkan/VulkanRenderContext.cs b/src/Engine.Graphics.Vulkan/VulkanRenderContext.cs deleted file mode 100644 index 201cd70..0000000 --- a/src/Engine.Graphics.Vulkan/VulkanRenderContext.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Engine.Core; -using Engine.Graphics; - -namespace Engine.Graphics.Vulkan; - -/// -/// Vulkan implementation of the render HAL context. -/// Creates and owns an for the Vulkan surface. -/// -public sealed class VulkanRenderContext : IRenderContext -{ - private readonly Sdl3Window _window; - private readonly VulkanContext _context; - private readonly Swapchain _swapchain; - - public IWindow Window => _window; - - public VulkanRenderContext(int width, int height, bool enableValidation) - { - _window = new Sdl3Window("Cortex Engine", width, height, vulkanSurface: true); - _context = new VulkanContext(_window, enableValidation); - _swapchain = new Swapchain(_context); - } - - public IRenderer CreateRenderer() => new VulkanRenderer(_context, _swapchain); - - public void Resize(int width, int height) => _swapchain.Recreate(width, height); - - public void Dispose() - { - _swapchain.Dispose(); - _context.Dispose(); - _window.Dispose(); - } -} diff --git a/src/Engine.Graphics.Vulkan/VulkanRenderer.cs b/src/Engine.Graphics.Vulkan/VulkanRenderer.cs deleted file mode 100644 index 7c20b50..0000000 --- a/src/Engine.Graphics.Vulkan/VulkanRenderer.cs +++ /dev/null @@ -1,673 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Numerics; -using System.Runtime.InteropServices; -using Flecs.NET.Core; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; -using Silk.NET.Core; -using Silk.NET.Vulkan; -using Engine.Core; -using Engine.Core.Components; - -namespace Engine.Graphics; - -/// -/// Vulkan implementation of the ECS world renderer. -/// Uses Silk.NET.Vulkan and reads Mesh + Transform components from the ECS world. -/// -public sealed unsafe class VulkanRenderer : IRenderer -{ - private readonly VulkanContext _context; - private readonly Swapchain _swapchain; - private readonly VulkanPipeline _pipeline; - private readonly ScreenshotCapture _screenshot; - private readonly UniformBuffer _frameConstantsBuffer; - private DescriptorPool _frameDescriptorPool; - private DescriptorSet _frameDescriptorSet; - private DescriptorPool _textureDescriptorPool; - private readonly Dictionary _textures = new(); - private readonly Dictionary _textureDescriptorSets = new(); - private Texture? _defaultTexture; - private readonly Dictionary _buffers = new(); - private CommandPool _commandPool; - private CommandBuffer[] _commandBuffers = null!; - private Silk.NET.Vulkan.Semaphore[] _imageAvailableSemaphores = null!; - private Silk.NET.Vulkan.Semaphore[] _renderFinishedSemaphores = null!; - private Silk.NET.Vulkan.Fence[] _inFlightFences = null!; - private int _currentFrame; - - [StructLayout(LayoutKind.Sequential, Size = 96)] - private struct PushConstants - { - public Matrix4x4 Mvp; - public Vector3 MaterialAlbedo; - public float MaterialRoughness; - public float MaterialMetallic; - public uint UseTexture; - public uint TextureIndex; - public uint Pad0; - } - - [StructLayout(LayoutKind.Sequential, Size = 48)] - private struct GpuLight - { - public Vector3 Direction; - public float Intensity; - public Vector3 Color; - public float Padding; - } - - [StructLayout(LayoutKind.Sequential, Size = 224)] - private struct FrameConstants - { - public Vector3 CameraPosition; - public uint LightCount; - public Vector3 AmbientColor; - public float AmbientPadding; - public GpuLight Light0; - public GpuLight Light1; - public GpuLight Light2; - public GpuLight Light3; - } - - private sealed class MeshBuffers : IDisposable - { - public VertexBuffer VertexBuffer; - public IndexBuffer IndexBuffer; - - public MeshBuffers(VertexBuffer vertexBuffer, IndexBuffer indexBuffer) - { - VertexBuffer = vertexBuffer; - IndexBuffer = indexBuffer; - } - - public void Dispose() - { - VertexBuffer.Dispose(); - IndexBuffer.Dispose(); - } - } - - public VulkanRenderer(VulkanContext context, Swapchain swapchain) - { - _context = context; - _swapchain = swapchain; - _screenshot = new ScreenshotCapture(context, swapchain); - - _pipeline = new VulkanPipeline(context, swapchain); - _frameConstantsBuffer = new UniformBuffer(context, (ulong)sizeof(FrameConstants)); - CreateFrameDescriptorPool(); - CreateFrameDescriptorSet(); - CreateTextureDescriptorPool(); - CreateDefaultTexture(); - CreateCommandPool(); - CreateCommandBuffers(); - CreateSyncObjects(); - } - - private void CreateCommandPool() - { - var createInfo = new CommandPoolCreateInfo - { - SType = StructureType.CommandPoolCreateInfo, - QueueFamilyIndex = _context.GraphicsFamilyIndex, - Flags = CommandPoolCreateFlags.ResetCommandBufferBit - }; - - CommandPool commandPool; - var result = _context.Vk.CreateCommandPool(_context.Device, &createInfo, null, &commandPool); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreateCommandPool failed: {result}"); - _commandPool = commandPool; - } - - private void CreateCommandBuffers() - { - _commandBuffers = new CommandBuffer[2]; - for (var i = 0; i < _commandBuffers.Length; i++) - { - var allocInfo = new CommandBufferAllocateInfo - { - SType = StructureType.CommandBufferAllocateInfo, - CommandPool = _commandPool, - Level = CommandBufferLevel.Primary, - CommandBufferCount = 1 - }; - - CommandBuffer cmd; - var result = _context.Vk.AllocateCommandBuffers(_context.Device, &allocInfo, &cmd); - if (result != Result.Success) - throw new InvalidOperationException($"vkAllocateCommandBuffers failed: {result}"); - _commandBuffers[i] = cmd; - } - } - - private void CreateSyncObjects() - { - _imageAvailableSemaphores = new Silk.NET.Vulkan.Semaphore[2]; - _renderFinishedSemaphores = new Silk.NET.Vulkan.Semaphore[2]; - _inFlightFences = new Silk.NET.Vulkan.Fence[2]; - - var semaphoreInfo = new SemaphoreCreateInfo { SType = StructureType.SemaphoreCreateInfo }; - var fenceInfo = new FenceCreateInfo - { - SType = StructureType.FenceCreateInfo, - Flags = FenceCreateFlags.SignaledBit - }; - - for (var i = 0; i < 2; i++) - { - Silk.NET.Vulkan.Semaphore imageAvailable, renderFinished; - Silk.NET.Vulkan.Fence fence; - _context.Vk.CreateSemaphore(_context.Device, &semaphoreInfo, null, &imageAvailable); - _context.Vk.CreateSemaphore(_context.Device, &semaphoreInfo, null, &renderFinished); - _context.Vk.CreateFence(_context.Device, &fenceInfo, null, &fence); - _imageAvailableSemaphores[i] = imageAvailable; - _renderFinishedSemaphores[i] = renderFinished; - _inFlightFences[i] = fence; - } - } - - private void CreateFrameDescriptorPool() - { - var poolSize = new DescriptorPoolSize - { - Type = DescriptorType.UniformBuffer, - DescriptorCount = 1 - }; - - var createInfo = new DescriptorPoolCreateInfo - { - SType = StructureType.DescriptorPoolCreateInfo, - MaxSets = 1, - PoolSizeCount = 1, - PPoolSizes = &poolSize - }; - - DescriptorPool descriptorPool; - var result = _context.Vk.CreateDescriptorPool(_context.Device, &createInfo, null, &descriptorPool); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreateDescriptorPool failed: {result}"); - _frameDescriptorPool = descriptorPool; - } - - private void CreateFrameDescriptorSet() - { - var layout = _pipeline.FrameDescriptorSetLayout; - var allocInfo = new DescriptorSetAllocateInfo - { - SType = StructureType.DescriptorSetAllocateInfo, - DescriptorPool = _frameDescriptorPool, - DescriptorSetCount = 1, - PSetLayouts = &layout - }; - - DescriptorSet descriptorSet; - var result = _context.Vk.AllocateDescriptorSets(_context.Device, &allocInfo, &descriptorSet); - if (result != Result.Success) - throw new InvalidOperationException($"vkAllocateDescriptorSets failed: {result}"); - _frameDescriptorSet = descriptorSet; - - var bufferInfo = new DescriptorBufferInfo - { - Buffer = _frameConstantsBuffer.Buffer, - Offset = 0, - Range = (ulong)sizeof(FrameConstants) - }; - - var write = new WriteDescriptorSet - { - SType = StructureType.WriteDescriptorSet, - DstSet = _frameDescriptorSet, - DstBinding = 0, - DstArrayElement = 0, - DescriptorType = DescriptorType.UniformBuffer, - DescriptorCount = 1, - PBufferInfo = &bufferInfo - }; - - _context.Vk.UpdateDescriptorSets(_context.Device, 1, &write, 0, null); - } - - private void CreateTextureDescriptorPool() - { - var poolSize = new DescriptorPoolSize - { - Type = DescriptorType.CombinedImageSampler, - DescriptorCount = 16 - }; - - var createInfo = new DescriptorPoolCreateInfo - { - SType = StructureType.DescriptorPoolCreateInfo, - MaxSets = 16, - PoolSizeCount = 1, - PPoolSizes = &poolSize - }; - - DescriptorPool descriptorPool; - var result = _context.Vk.CreateDescriptorPool(_context.Device, &createInfo, null, &descriptorPool); - if (result != Result.Success) - throw new InvalidOperationException($"vkCreateDescriptorPool (texture) failed: {result}"); - _textureDescriptorPool = descriptorPool; - } - - private void CreateDefaultTexture() - { - var whitePixel = new byte[] { 255, 255, 255, 255 }; - _defaultTexture = CreateTextureFromBytes("__default__", whitePixel, 1, 1); - } - - private Texture CreateTextureFromBytes(string key, byte[] rgbaPixels, uint width, uint height) - { - var path = $"/tmp/cortex_texture_{key}.png"; - System.IO.File.WriteAllBytes(path, EncodePng(rgbaPixels, width, height)); - var texture = new Texture(_context, path); - try - { - System.IO.File.Delete(path); - } - catch - { - // Ignore cleanup failure. - } - return texture; - } - - private static byte[] EncodePng(byte[] rgbaPixels, uint width, uint height) - { - using var image = SixLabors.ImageSharp.Image.LoadPixelData(rgbaPixels, (int)width, (int)height); - using var stream = new System.IO.MemoryStream(); - image.SaveAsPng(stream); - return stream.ToArray(); - } - - public void RequestScreenshot(string outputPath) => _screenshot.Request(outputPath); - - public bool IsScreenshotRequested => _screenshot.IsRequested; - - /// - /// Provider that can asynchronously capture the current frame to a PNG byte array. - /// - public IScreenshotProvider ScreenshotProvider => _screenshot; - - public void RenderWorld(World world) - { - var frame = _currentFrame % 2; - - var fence = _inFlightFences[frame]; - _context.Vk.WaitForFences(_context.Device, 1, &fence, true, ulong.MaxValue); - _context.Vk.ResetFences(_context.Device, 1, &fence); - - uint imageIndex; - var result = _context.KhrSwapchain!.AcquireNextImage(_context.Device, _swapchain.Handle, ulong.MaxValue, _imageAvailableSemaphores[frame], new Silk.NET.Vulkan.Fence(), &imageIndex); - if (result == Result.ErrorOutOfDateKhr) - return; - - var cmd = _commandBuffers[frame]; - _context.Vk.ResetCommandBuffer(cmd, CommandBufferResetFlags.None); - - var beginInfo = new CommandBufferBeginInfo - { - SType = StructureType.CommandBufferBeginInfo, - Flags = CommandBufferUsageFlags.OneTimeSubmitBit - }; - _context.Vk.BeginCommandBuffer(cmd, &beginInfo); - - var clearValues = new[] - { - new ClearValue(new ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f)), - new ClearValue { DepthStencil = new ClearDepthStencilValue(1.0f, 0) } - }; - - var renderPassInfo = new RenderPassBeginInfo - { - SType = StructureType.RenderPassBeginInfo, - RenderPass = _swapchain.RenderPass, - Framebuffer = _swapchain.Framebuffers[imageIndex], - RenderArea = new Rect2D(new Offset2D(0, 0), _swapchain.Extent), - ClearValueCount = (uint)clearValues.Length - }; - - fixed (ClearValue* pClearValues = clearValues) - { - renderPassInfo.PClearValues = pClearValues; - } - - _context.Vk.CmdBeginRenderPass(cmd, &renderPassInfo, SubpassContents.Inline); - _context.Vk.CmdBindPipeline(cmd, PipelineBindPoint.Graphics, _pipeline.Handle); - var frameDescriptorSet = _frameDescriptorSet; - _context.Vk.CmdBindDescriptorSets(cmd, PipelineBindPoint.Graphics, _pipeline.Layout, 0, 1, &frameDescriptorSet, 0, null); - - var viewport = new Viewport(0, 0, _swapchain.Extent.Width, _swapchain.Extent.Height, 0, 1); - var scissor = new Rect2D(new Offset2D(0, 0), _swapchain.Extent); - _context.Vk.CmdSetViewport(cmd, 0, 1, &viewport); - _context.Vk.CmdSetScissor(cmd, 0, 1, &scissor); - - var camera = GetCamera(world); - var view = camera.GetViewMatrix(); - var proj = camera.GetProjectionMatrix(); - // Vulkan NDC Y points down; .NET's projection matrix assumes Y up, so flip Y. - proj.M22 = -proj.M22; - var drawCmd = cmd; - - - var frameConstants = BuildFrameConstants(world, camera); - var frameConstantsBytes = new byte[sizeof(FrameConstants)]; - fixed (byte* p = frameConstantsBytes) - { - *(FrameConstants*)p = frameConstants; - } - _frameConstantsBuffer.Update(frameConstantsBytes); - - world.Each((Entity e, ref Mesh mesh, ref Transform transform) => - { - if (!_buffers.TryGetValue(e, out var buffers)) - { - buffers = CreateMeshBuffers(mesh); - _buffers[e] = buffers; - } - - var material = e.Has() ? e.Get() : Material.Default; - var bytes = BuildMeshVertices(mesh, transform, material); - buffers.VertexBuffer.Update(bytes); - - var mvp = Matrix4x4.Transpose(Matrix4x4.Multiply(view, proj)); - var texture = GetTexture(material); - var textureDescriptorSet = GetTextureDescriptorSet(texture); - var textureSet = textureDescriptorSet; - _context.Vk.CmdBindDescriptorSets(drawCmd, PipelineBindPoint.Graphics, _pipeline.Layout, 1, 1, &textureSet, 0, null); - - var push = new PushConstants - { - Mvp = mvp, - MaterialAlbedo = material.Albedo, - MaterialRoughness = material.Roughness, - MaterialMetallic = material.Metallic, - UseTexture = material.HasTexture ? 1u : 0u, - TextureIndex = 0, - Pad0 = 0 - }; - - var pushSize = (uint)sizeof(PushConstants); - _context.Vk.CmdPushConstants(drawCmd, _pipeline.Layout, ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit, 0, pushSize, &push); - - var vertexBuffer = buffers.VertexBuffer.Buffer; - var offset = 0ul; - _context.Vk.CmdBindVertexBuffers(drawCmd, 0, 1, &vertexBuffer, &offset); - _context.Vk.CmdBindIndexBuffer(drawCmd, buffers.IndexBuffer.Buffer, 0, IndexType.Uint32); - _context.Vk.CmdDrawIndexed(drawCmd, buffers.IndexBuffer.Count, 1, 0, 0, 0); - }); - - _context.Vk.CmdEndRenderPass(cmd); - - var swapchainImage = _swapchain.GetImage(imageIndex); - _screenshot.RecordReadback(cmd, swapchainImage, _swapchain.Extent.Width, _swapchain.Extent.Height, _swapchain.SurfaceFormat); - - _context.Vk.EndCommandBuffer(cmd); - - var waitSemaphore = _imageAvailableSemaphores[frame]; - var signalSemaphore = _renderFinishedSemaphores[frame]; - var stageMask = PipelineStageFlags.ColorAttachmentOutputBit; - var submitInfo = new SubmitInfo - { - SType = StructureType.SubmitInfo, - WaitSemaphoreCount = 1, - PWaitSemaphores = &waitSemaphore, - PWaitDstStageMask = &stageMask, - CommandBufferCount = 1, - PCommandBuffers = &cmd, - SignalSemaphoreCount = 1, - PSignalSemaphores = &signalSemaphore - }; - - _context.Vk.QueueSubmit(_context.GraphicsQueue, 1, &submitInfo, _inFlightFences[frame]); - - var swapchain = _swapchain.Handle; - var presentInfo = new PresentInfoKHR - { - SType = StructureType.PresentInfoKhr, - WaitSemaphoreCount = 1, - PWaitSemaphores = &signalSemaphore, - SwapchainCount = 1, - PSwapchains = &swapchain, - PImageIndices = &imageIndex - }; - - _context.KhrSwapchain!.QueuePresent(_context.PresentQueue, &presentInfo); - - // If a screenshot was requested, wait for the GPU to finish the readback and save the file. - if (_screenshot.IsRequested) - { - _context.Vk.WaitForFences(_context.Device, 1, &fence, true, ulong.MaxValue); - _screenshot.Save(_swapchain.Extent.Width, _swapchain.Extent.Height, _swapchain.SurfaceFormat); - } - - _currentFrame++; - } - - private MeshBuffers CreateMeshBuffers(Mesh mesh) - { - var vertexBytes = new byte[mesh.Vertices.Length * 9 * sizeof(float)]; - fixed (byte* p = vertexBytes) - { - var dst = (float*)p; - for (var i = 0; i < mesh.Vertices.Length; i++) - { - var v = mesh.Vertices[i]; - dst[i * 9 + 0] = v.Position.X; - dst[i * 9 + 1] = v.Position.Y; - dst[i * 9 + 2] = v.Position.Z; - dst[i * 9 + 3] = v.Color.X; - dst[i * 9 + 4] = v.Color.Y; - dst[i * 9 + 5] = v.Color.Z; - dst[i * 9 + 6] = v.Normal.X; - dst[i * 9 + 7] = v.Normal.Y; - dst[i * 9 + 8] = v.Normal.Z; - } - } - - var indexBytes = new byte[mesh.Indices.Length * sizeof(uint)]; - fixed (byte* p = indexBytes) - fixed (uint* src = mesh.Indices) - { - global::System.Buffer.MemoryCopy(src, p, indexBytes.Length, mesh.Indices.Length * sizeof(uint)); - } - - return new MeshBuffers( - new VertexBuffer(_context, vertexBytes), - new IndexBuffer(_context, indexBytes, (uint)mesh.Indices.Length)); - } - - private byte[] BuildMeshVertices(Mesh mesh, Transform transform, Material material) - { - var matrix = transform.GetMatrix(); - var bytes = new byte[mesh.Vertices.Length * 9 * sizeof(float)]; - fixed (byte* p = bytes) - { - var dst = (float*)p; - for (var i = 0; i < mesh.Vertices.Length; i++) - { - var v = mesh.Vertices[i]; - var worldPos = Vector3.Transform(v.Position, matrix); - var normal = transform.TransformNormal(v.Normal); - var color = v.Color * material.Albedo; - dst[i * 9 + 0] = worldPos.X; - dst[i * 9 + 1] = worldPos.Y; - dst[i * 9 + 2] = worldPos.Z; - dst[i * 9 + 3] = color.X; - dst[i * 9 + 4] = color.Y; - dst[i * 9 + 5] = color.Z; - dst[i * 9 + 6] = normal.X; - dst[i * 9 + 7] = normal.Y; - dst[i * 9 + 8] = normal.Z; - } - } - return bytes; - } - - private Texture GetTexture(Material material) - { - if (!material.HasTexture) - return _defaultTexture!; - - if (_textures.TryGetValue(material.TexturePath!, out var texture)) - return texture; - - if (!System.IO.File.Exists(material.TexturePath!)) - return _defaultTexture!; - - texture = new Texture(_context, material.TexturePath!); - _textures[material.TexturePath!] = texture; - return texture; - } - - private DescriptorSet GetTextureDescriptorSet(Texture texture) - { - if (_textureDescriptorSets.TryGetValue(texture, out var descriptorSet)) - return descriptorSet; - - var layout = _pipeline.TextureDescriptorSetLayout; - var allocInfo = new DescriptorSetAllocateInfo - { - SType = StructureType.DescriptorSetAllocateInfo, - DescriptorPool = _textureDescriptorPool, - DescriptorSetCount = 1, - PSetLayouts = &layout - }; - - DescriptorSet set; - var result = _context.Vk.AllocateDescriptorSets(_context.Device, &allocInfo, &set); - if (result != Result.Success) - throw new InvalidOperationException($"vkAllocateDescriptorSets (texture) failed: {result}"); - - var imageInfo = new DescriptorImageInfo - { - ImageLayout = ImageLayout.ShaderReadOnlyOptimal, - ImageView = texture.View, - Sampler = texture.Sampler - }; - - var write = new WriteDescriptorSet - { - SType = StructureType.WriteDescriptorSet, - DstSet = set, - DstBinding = 0, - DstArrayElement = 0, - DescriptorType = DescriptorType.CombinedImageSampler, - DescriptorCount = 1, - PImageInfo = &imageInfo - }; - - _context.Vk.UpdateDescriptorSets(_context.Device, 1, &write, 0, null); - _textureDescriptorSets[texture] = set; - return set; - } - - private FrameConstants BuildFrameConstants(World world, Camera camera) - { - var frameConstants = new FrameConstants - { - CameraPosition = camera.Position, - LightCount = 0, - AmbientColor = new Vector3(0.4f, 0.4f, 0.45f), - AmbientPadding = 0 - }; - - world.Each((Entity e, ref Light light) => - { - if (frameConstants.LightCount >= 4) - return; - - var index = (int)frameConstants.LightCount; - frameConstants.LightCount++; - SetLight(ref frameConstants, index, new GpuLight - { - Direction = light.Direction, - Intensity = light.Intensity, - Color = light.Color, - Padding = 0 - }); - }); - - // Fallback: if no light components exist, add a default directional light. - if (frameConstants.LightCount == 0) - { - frameConstants.LightCount = 1; - SetLight(ref frameConstants, 0, new GpuLight - { - Direction = new Vector3(0.5f, -1.0f, -0.5f), - Intensity = 1.0f, - Color = new Vector3(1.0f, 0.95f, 0.8f), - Padding = 0 - }); - } - - return frameConstants; - } - - private static void SetLight(ref FrameConstants frameConstants, int index, GpuLight light) - { - switch (index) - { - case 0: frameConstants.Light0 = light; break; - case 1: frameConstants.Light1 = light; break; - case 2: frameConstants.Light2 = light; break; - case 3: frameConstants.Light3 = light; break; - } - } - - private Camera GetCamera(World world) - { - var camera = new Camera( - new Vector3(0.0f, 0.0f, -2.0f), - Vector3.Zero, - Vector3.UnitY, - MathF.PI / 4.0f, - (float)_swapchain.Extent.Width / _swapchain.Extent.Height, - 0.1f, - 100.0f); - - world.Each((Entity e, ref Camera cam) => - { - camera = cam; - }); - - // Always keep the aspect ratio in sync with the swapchain. - camera.AspectRatio = (float)_swapchain.Extent.Width / _swapchain.Extent.Height; - return camera; - } - - public void Dispose() - { - _context.Vk.DeviceWaitIdle(_context.Device); - - _screenshot.Dispose(); - - foreach (var buffers in _buffers.Values) - buffers.Dispose(); - _buffers.Clear(); - - for (var i = 0; i < 2; i++) - { - _context.Vk.DestroySemaphore(_context.Device, _renderFinishedSemaphores[i], null); - _context.Vk.DestroySemaphore(_context.Device, _imageAvailableSemaphores[i], null); - _context.Vk.DestroyFence(_context.Device, _inFlightFences[i], null); - } - - _context.Vk.DestroyCommandPool(_context.Device, _commandPool, null); - _context.Vk.DestroyDescriptorPool(_context.Device, _textureDescriptorPool, null); - _context.Vk.DestroyDescriptorPool(_context.Device, _frameDescriptorPool, null); - - foreach (var texture in _textures.Values) - texture.Dispose(); - _textures.Clear(); - - _defaultTexture?.Dispose(); - - _frameConstantsBuffer.Dispose(); - - _pipeline.Dispose(); - } -} diff --git a/src/Engine.Graphics/Engine.Graphics.csproj b/src/Engine.Graphics/Engine.Graphics.csproj deleted file mode 100644 index dc79689..0000000 --- a/src/Engine.Graphics/Engine.Graphics.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - net9.0 - enable - enable - true - true - - - - DEV_MODE - - - - RELEASE_AOT - true - - - - - - - - - - - - - diff --git a/src/Engine.Graphics/IRenderContext.cs b/src/Engine.Graphics/IRenderContext.cs deleted file mode 100644 index 3ac5fca..0000000 --- a/src/Engine.Graphics/IRenderContext.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Engine.Core; - -namespace Engine.Graphics; - -/// -/// Abstraction over a graphics backend (Vulkan, Raylib, etc.). -/// Each backend owns its window and surface. The application retrieves -/// the window via for input and event polling. -/// -public interface IRenderContext : IDisposable -{ - /// - /// The window owned by this backend. The application uses this for - /// input polling, resize detection, and close requests. - /// - IWindow Window { get; } - - /// - /// Create a renderer that can draw the ECS world using this backend. - /// - IRenderer CreateRenderer(); - - /// - /// Notify the backend that the output surface has been resized. - /// - void Resize(int width, int height); -} diff --git a/src/Engine.Graphics/IRenderer.cs b/src/Engine.Graphics/IRenderer.cs deleted file mode 100644 index 601d6c0..0000000 --- a/src/Engine.Graphics/IRenderer.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Engine.Core; -using Flecs.NET.Core; - -namespace Engine.Graphics; - -/// -/// Renders the ECS world and exposes screenshot capture. -/// Implemented by concrete graphics backends. -/// -public interface IRenderer : IDisposable -{ - /// - /// Render one frame of the ECS world and present it. - /// - void RenderWorld(World world); - - /// - /// Request a screenshot of the next rendered frame to be saved to disk. - /// - void RequestScreenshot(string outputPath); - - /// - /// True if a screenshot has been requested but not yet captured. - /// - bool IsScreenshotRequested { get; } - - /// - /// Provider that can asynchronously capture the current frame to PNG bytes. - /// - IScreenshotProvider ScreenshotProvider { get; } -} diff --git a/src/Engine.Graphics/Loaders/GltfLoader.cs b/src/Engine.Graphics/Loaders/GltfLoader.cs deleted file mode 100644 index 636280a..0000000 --- a/src/Engine.Graphics/Loaders/GltfLoader.cs +++ /dev/null @@ -1,196 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Numerics; -using Engine.Core; -using EngineCoreMaterial = Engine.Core.Components.Material; -using EngineMesh = Engine.Core.Components.Mesh; -using SharpGLTF.Schema2; - -namespace Engine.Graphics.Loaders; - -/// -/// glTF/glTF binary loader using SharpGLTF.Core. -/// Loads all primitives across all meshes, extracting: -/// - Positions, normals (from file or computed), texcoords -/// - PBR material: albedo, roughness, metallic, base color texture -/// -public static class GltfLoader -{ - /// - /// Load a glTF/GLB file and return the combined mesh plus extracted materials. - /// - public static EngineMesh Load(string path, Vector3? defaultColor = null) - { - var (mesh, _) = LoadWithMaterials(path, defaultColor); - return mesh; - } - - /// - /// Load a glTF/GLB file and return the combined mesh plus a list of - /// (primitive index, material) pairs. Textures are extracted to a - /// temp directory next to the source file. - /// - public static (EngineMesh Mesh, List Materials) LoadWithMaterials( - string path, Vector3? defaultColor = null) - { - var color = defaultColor ?? new Vector3(0.7f, 0.7f, 0.7f); - var textureDir = Path.Combine(Path.GetDirectoryName(path) ?? ".", "extracted_textures"); - var model = ModelRoot.Load(path); - - if (model.LogicalMeshes.Count == 0) - throw new InvalidOperationException($"glTF file has no meshes: {path}"); - - var vertices = new List(); - var indices = new List(); - var materials = new List(); - - foreach (var mesh in model.LogicalMeshes) - { - foreach (var primitive in mesh.Primitives) - { - if (!primitive.VertexAccessors.TryGetValue("POSITION", out var positionAccessor)) - continue; - - var positions = positionAccessor.AsVector3Array(); - var normals = primitive.VertexAccessors.TryGetValue("NORMAL", out var normalAccessor) - ? normalAccessor.AsVector3Array() - : null; - var uvs = primitive.VertexAccessors.TryGetValue("TEXCOORD_0", out var uvAccessor) - ? uvAccessor.AsVector2Array() - : null; - - var primIndices = GetIndices(primitive, positions.Count); - var material = ExtractMaterial(primitive, color, textureDir); - materials.Add(material); - - var vertexBase = (uint)vertices.Count; - - for (var i = 0; i < primIndices.Length; i += 3) - { - var i0 = (int)primIndices[i]; - var i1 = (int)primIndices[i + 1]; - var i2 = (int)primIndices[i + 2]; - - var v0 = ToVertex(positions, normals, uvs, i0, color); - var v1 = ToVertex(positions, normals, uvs, i1, color); - var v2 = ToVertex(positions, normals, uvs, i2, color); - - if (normals == null) - { - var n = MeshMath.ComputeFaceNormal(v0.Position, v1.Position, v2.Position); - v0.Normal = n; - v1.Normal = n; - v2.Normal = n; - } - - indices.Add(vertexBase + (uint)i0); - indices.Add(vertexBase + (uint)i1); - indices.Add(vertexBase + (uint)i2); - - if (i == 0) - { - vertices.AddRange(new[] { v0, v1, v2 }); - } - } - - if (normals != null || uvs != null) - { - for (var i = 0; i < positions.Count; i++) - vertices.Add(ToVertex(positions, normals, uvs, i, color)); - } - - vertexBase = (uint)vertices.Count; - } - } - - return (new EngineMesh(vertices.ToArray(), indices.ToArray()), materials); - } - - private static Vertex ToVertex( - IReadOnlyList positions, - IReadOnlyList? normals, - IReadOnlyList? uvs, - int index, - Vector3 color) - { - var pos = new Vector3(positions[index].X, positions[index].Y, positions[index].Z); - var normal = normals != null - ? Vector3.Normalize(new Vector3(normals[index].X, normals[index].Y, normals[index].Z)) - : Vector3.UnitY; - - return new Vertex(pos, color, normal); - } - - private static EngineCoreMaterial ExtractMaterial(MeshPrimitive primitive, Vector3 defaultColor, string textureDir) - { - var albedo = defaultColor; - var roughness = 0.5f; - var metallic = 0.0f; - string? texturePath = null; - - var gltfMat = primitive.Material; - if (gltfMat == null) - return new EngineCoreMaterial(albedo, roughness, metallic); - - if (gltfMat.FindChannel("BaseColor") is { } baseColor) - { - foreach (var param in baseColor.Parameters) - { - if (param.Name == "BaseColorFactor" && param.Value is Vector4 factor) - { - albedo = new Vector3(factor.X, factor.Y, factor.Z); - } - } - - if (baseColor.Texture?.PrimaryImage is { } img) - { - var mem = img.Content; - if (!string.IsNullOrEmpty(mem.SourcePath) && File.Exists(mem.SourcePath)) - { - texturePath = mem.SourcePath; - } - else if (mem.IsValid) - { - Directory.CreateDirectory(textureDir); - var ext = string.IsNullOrEmpty(mem.FileExtension) ? ".png" : mem.FileExtension; - texturePath = Path.Combine(textureDir, $"tex_{Guid.NewGuid():N}{ext}"); - mem.SaveToFile(texturePath); - } - } - } - - if (gltfMat.FindChannel("MetallicRoughness") is { } mr) - { - foreach (var param in mr.Parameters) - { - if (param.Name == "MetallicFactor" && param.Value is float mf) - metallic = mf; - if (param.Name == "RoughnessFactor" && param.Value is float rf) - roughness = rf; - } - } - - return new EngineCoreMaterial(albedo, roughness, metallic, texturePath); - } - - private static uint[] GetIndices(MeshPrimitive primitive, int positionCount) - { - if (primitive.IndexAccessor != null) - { - var idx = primitive.IndexAccessor.AsIndexArray(); - var indices = new uint[idx.Count]; - for (var i = 0; i < idx.Count; i++) - indices[i] = idx[i]; - return indices; - } - - var auto = new uint[positionCount]; - for (var i = 0; i < positionCount; i++) - auto[i] = (uint)i; - return auto; - } - - private static Vector3 ComputeFaceNormal(Vector3 a, Vector3 b, Vector3 c) - => MeshMath.ComputeFaceNormal(a, b, c); -} diff --git a/src/Engine.Graphics/Loaders/ObjLoader.cs b/src/Engine.Graphics/Loaders/ObjLoader.cs deleted file mode 100644 index abcc180..0000000 --- a/src/Engine.Graphics/Loaders/ObjLoader.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Numerics; -using Engine.Core; -using Engine.Core.Components; - -namespace Engine.Graphics.Loaders; - -/// -/// Minimal .obj loader. -/// Supports vertices (v) and faces (f). Creates per-face normals for flat shading. -/// Produces a colored Mesh component. -/// -public static class ObjLoader -{ - public static Mesh Load(string path, Vector3? defaultColor = null) - { - var color = defaultColor ?? new Vector3(0.7f, 0.7f, 0.7f); - - var positions = new List(); - var vertices = new List(); - var indices = new List(); - - foreach (var rawLine in File.ReadLines(path)) - { - var line = rawLine.Trim(); - if (string.IsNullOrEmpty(line) || line.StartsWith("#")) - continue; - - var parts = line.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length == 0) - continue; - - switch (parts[0]) - { - case "v" when parts.Length >= 4: - positions.Add(new Vector3( - float.Parse(parts[1]), - float.Parse(parts[2]), - float.Parse(parts[3]))); - break; - - case "f" when parts.Length >= 4: - // Triangulate the face as a fan. - var baseIndex = ParseFaceIndex(parts[1]); - for (var i = 2; i < parts.Length - 1; i++) - { - var i0 = baseIndex; - var i1 = ParseFaceIndex(parts[i]); - var i2 = ParseFaceIndex(parts[i + 1]); - - var v0 = positions[(int)i0]; - var v1 = positions[(int)i1]; - var v2 = positions[(int)i2]; - var normal = ComputeFaceNormal(v0, v1, v2); - - var vertexBase = (uint)vertices.Count; - indices.Add(vertexBase); - indices.Add(vertexBase + 1); - indices.Add(vertexBase + 2); - - vertices.Add(new Vertex(v0, color, normal)); - vertices.Add(new Vertex(v1, color, normal)); - vertices.Add(new Vertex(v2, color, normal)); - } - break; - } - } - - if (vertices.Count == 0) - throw new InvalidOperationException($"OBJ file has no vertices: {path}"); - - return new Mesh(vertices.ToArray(), indices.ToArray()); - } - - private static uint ParseFaceIndex(string part) - { - // Formats: v, v/vt, v/vt/vn, v//vn - var slashIndex = part.IndexOf('/'); - var indexStr = slashIndex == -1 ? part : part.Substring(0, slashIndex); - var index = int.Parse(indexStr); - return (uint)(index - 1); // OBJ indices are 1-based - } - - private static Vector3 ComputeFaceNormal(Vector3 a, Vector3 b, Vector3 c) - => MeshMath.ComputeFaceNormal(a, b, c); -} diff --git a/src/Engine.Graphics/MeshMath.cs b/src/Engine.Graphics/MeshMath.cs deleted file mode 100644 index 08f6295..0000000 --- a/src/Engine.Graphics/MeshMath.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Numerics; - -namespace Engine.Graphics; - -/// -/// Shared mesh math utilities used by loaders and procedural generators. -/// -public static class MeshMath -{ - /// - /// Compute a flat face normal from three vertex positions. - /// Falls back to Vector3.UnitY for degenerate (zero-area) triangles. - /// - public static Vector3 ComputeFaceNormal(Vector3 a, Vector3 b, Vector3 c) - { - var ab = b - a; - var ac = c - a; - // Cross(ac, ab) instead of Cross(ab, ac) to match CW winding in typical OBJ files - var normal = Vector3.Cross(ac, ab); - if (normal.LengthSquared() > 0.00001f) - normal = Vector3.Normalize(normal); - else - normal = Vector3.UnitY; - return normal; - } -} diff --git a/src/Engine.Graphics/ProceduralMesh.cs b/src/Engine.Graphics/ProceduralMesh.cs deleted file mode 100644 index 0486b6a..0000000 --- a/src/Engine.Graphics/ProceduralMesh.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.Collections.Generic; -using System.Numerics; -using Engine.Core; -using Engine.Core.Components; - -namespace Engine.Graphics; - -/// -/// Procedural mesh generators for common primitive shapes. -/// All methods are pure CPU — no GPU/display dependencies. -/// -public static class ProceduralMesh -{ - /// - /// Generate a UV sphere mesh. - /// - /// Sphere radius. - /// Longitude segments (around the equator). - /// Latitude rings (from pole to pole). - /// Vertex color applied to all vertices. - public static Mesh CreateSphere(float radius, int segments, int rings, Vector3 color) - { - var vertices = new List(); - var indices = new List(); - - for (var ring = 0; ring <= rings; ring++) - { - var phi = MathF.PI * ring / rings; - var sinPhi = MathF.Sin(phi); - var cosPhi = MathF.Cos(phi); - - for (var seg = 0; seg <= segments; seg++) - { - var theta = 2.0f * MathF.PI * seg / segments; - var sinTheta = MathF.Sin(theta); - var cosTheta = MathF.Cos(theta); - - var x = radius * sinPhi * cosTheta; - var y = radius * cosPhi; - var z = radius * sinPhi * sinTheta; - var normal = Vector3.Normalize(new Vector3(x, y, z)); - - vertices.Add(new Vertex(new Vector3(x, y, z), color, normal)); - } - } - - for (var ring = 0; ring < rings; ring++) - { - for (var seg = 0; seg < segments; seg++) - { - var i0 = (uint)(ring * (segments + 1) + seg); - var i1 = i0 + 1; - var i2 = i0 + (uint)(segments + 1); - var i3 = i2 + 1; - - indices.Add(i0); indices.Add(i1); indices.Add(i2); - indices.Add(i1); indices.Add(i3); indices.Add(i2); - } - } - - return new Mesh(vertices.ToArray(), indices.ToArray()); - } - - /// - /// Generate a ground grid mesh at Y=0, consisting of thin quads. - /// - /// Number of grid lines on each side of the origin. - /// Distance between grid lines. - /// Vertex color applied to all vertices. - public static Mesh CreateGrid(int lines, float spacing, Vector3 color) - { - var vertices = new List(); - var indices = new List(); - var extent = lines * spacing; - var normal = Vector3.UnitY; - var halfWidth = 0.02f; - - for (var i = -lines; i <= lines; i++) - { - var offset = i * spacing; - - var baseIndex = (uint)vertices.Count; - vertices.Add(new Vertex(new Vector3(-extent, 0, offset - halfWidth), color, normal)); - vertices.Add(new Vertex(new Vector3(extent, 0, offset - halfWidth), color, normal)); - vertices.Add(new Vertex(new Vector3(extent, 0, offset + halfWidth), color, normal)); - vertices.Add(new Vertex(new Vector3(-extent, 0, offset + halfWidth), color, normal)); - indices.Add(baseIndex); indices.Add(baseIndex + 1); indices.Add(baseIndex + 2); - indices.Add(baseIndex); indices.Add(baseIndex + 2); indices.Add(baseIndex + 3); - - baseIndex = (uint)vertices.Count; - vertices.Add(new Vertex(new Vector3(offset - halfWidth, 0, -extent), color, normal)); - vertices.Add(new Vertex(new Vector3(offset + halfWidth, 0, -extent), color, normal)); - vertices.Add(new Vertex(new Vector3(offset + halfWidth, 0, extent), color, normal)); - vertices.Add(new Vertex(new Vector3(offset - halfWidth, 0, extent), color, normal)); - indices.Add(baseIndex); indices.Add(baseIndex + 1); indices.Add(baseIndex + 2); - indices.Add(baseIndex); indices.Add(baseIndex + 2); indices.Add(baseIndex + 3); - } - - return new Mesh(vertices.ToArray(), indices.ToArray()); - } -} diff --git a/src/Engine.Graphics/RenderBackendFactory.cs b/src/Engine.Graphics/RenderBackendFactory.cs deleted file mode 100644 index 972070b..0000000 --- a/src/Engine.Graphics/RenderBackendFactory.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Engine.Core; - -namespace Engine.Graphics; - -/// -/// Factory for creating concrete graphics backends by name. -/// Backends register themselves so the app only depends on the HAL interfaces. -/// Each backend creates and owns its own window. -/// -public static class RenderBackendFactory -{ - private static readonly Dictionary> _registry - = new(StringComparer.OrdinalIgnoreCase); - - /// - /// Register a backend implementation under the given name. - /// The factory receives (width, height, enableValidation) and must create - /// its own window and render context. - /// - public static void Register(string name, Func factory) - { - _registry[name] = factory; - } - - /// - /// Create a backend instance for the given name. - /// The backend assembly must have registered itself before this is called. - /// - public static IRenderContext Create(string name, int width, int height, bool enableValidation) - { - if (!_registry.TryGetValue(name, out var factory)) - throw new NotSupportedException($"No graphics backend named '{name}' is registered."); - - return factory(width, height, enableValidation); - } -} diff --git a/src/Engine.Graphics/SceneSerializer.cs b/src/Engine.Graphics/SceneSerializer.cs deleted file mode 100644 index 5de8d6d..0000000 --- a/src/Engine.Graphics/SceneSerializer.cs +++ /dev/null @@ -1,300 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Numerics; -using System.Text.Json; -using System.Text.Json.Serialization; -using Engine.Core.Components; -using Flecs.NET.Core; - -namespace Engine.Graphics; - -/// -/// Scene serialization — saves and loads the ECS world to/from JSON. -/// Uses manual serialization for named entities with Transform, Material, Light, Camera components. -/// -public static class SceneSerializer -{ - private static readonly JsonSerializerOptions JsonOptions = new() - { - PropertyNameCaseInsensitive = true, - WriteIndented = true, - Converters = - { - new Vector3JsonConverter(), - new QuaternionJsonConverter() - } - }; - - /// - /// Serialize all named entities with their components to a JSON string. - /// - public static string SaveToString(World world) - { - var entities = new List(); - var processedNames = new HashSet(); - - world.Each((Entity e, ref Transform _) => - { - var name = e.Name(); - if (string.IsNullOrEmpty(name)) - return; - - if (processedNames.Contains(name)) - return; - processedNames.Add(name); - - var entry = new SceneEntity { Name = name }; - - if (e.Has()) - { - var t = e.Get(); - entry.Transform = new SceneTransform - { - Position = t.Position, - Rotation = t.Rotation, - Scale = t.Scale - }; - } - - if (e.Has()) - { - var m = e.Get(); - entry.Material = new SceneMaterial - { - Albedo = m.Albedo, - Roughness = m.Roughness, - Metallic = m.Metallic, - TexturePath = m.TexturePath - }; - } - - if (e.Has()) - { - var l = e.Get(); - entry.Light = new SceneLight - { - Type = l.Type, - Direction = l.Direction, - Position = l.Position, - Color = l.Color, - Intensity = l.Intensity, - Range = l.Range - }; - } - - if (e.Has()) - { - var c = e.Get(); - entry.Camera = new SceneCamera - { - Position = c.Position, - Target = c.Target, - Up = c.Up, - FieldOfView = c.FieldOfView, - NearPlane = c.NearPlane, - FarPlane = c.FarPlane - }; - } - - entities.Add(entry); - }); - - return JsonSerializer.Serialize(entities, JsonOptions); - } - - /// - /// Save the world to a JSON file. - /// - public static void SaveToFile(World world, string path) - { - var json = SaveToString(world); - var dir = Path.GetDirectoryName(path); - if (!string.IsNullOrEmpty(dir)) - Directory.CreateDirectory(dir); - File.WriteAllText(path, json); - } - - /// - /// Load entities from a JSON string into the world. - /// Returns the number of entities loaded. - /// - public static int LoadFromString(World world, string json) - { - var entities = JsonSerializer.Deserialize>(json, JsonOptions); - if (entities == null) return 0; - - foreach (var entry in entities) - { - var entity = world.Entity(entry.Name); - - if (entry.Transform != null) - { - entity.Set(new Transform( - entry.Transform.Position, - entry.Transform.Rotation, - entry.Transform.Scale)); - } - - if (entry.Material != null) - { - entity.Set(new Material( - entry.Material.Albedo, - entry.Material.Roughness, - entry.Material.Metallic, - entry.Material.TexturePath)); - } - - if (entry.Light != null) - { - if (entry.Light.Type == LightType.Point) - { - entity.Set(Light.Point( - entry.Light.Position, - entry.Light.Color, - entry.Light.Intensity, - entry.Light.Range)); - } - else - { - entity.Set(Light.Directional( - entry.Light.Direction, - entry.Light.Color, - entry.Light.Intensity)); - } - } - - if (entry.Camera != null) - { - entity.Set(new Camera( - entry.Camera.Position, - entry.Camera.Target, - entry.Camera.Up, - entry.Camera.FieldOfView, - 16f / 9f, - entry.Camera.NearPlane, - entry.Camera.FarPlane)); - } - } - - return entities.Count; - } - - /// - /// Load entities from a JSON file into the world. - /// Returns the number of entities loaded. - /// - public static int LoadFromFile(World world, string path) - { - if (!File.Exists(path)) - throw new FileNotFoundException($"Scene file not found: {path}"); - - var json = File.ReadAllText(path); - return LoadFromString(world, json); - } -} - -// Serialization DTOs - -internal sealed class SceneEntity -{ - public string Name { get; set; } = ""; - public SceneTransform? Transform { get; set; } - public SceneMaterial? Material { get; set; } - public SceneLight? Light { get; set; } - public SceneCamera? Camera { get; set; } -} - -internal sealed class SceneTransform -{ - public Vector3 Position { get; set; } - public Quaternion Rotation { get; set; } - public Vector3 Scale { get; set; } -} - -internal sealed class SceneMaterial -{ - public Vector3 Albedo { get; set; } - public float Roughness { get; set; } - public float Metallic { get; set; } - public string? TexturePath { get; set; } -} - -internal sealed class SceneLight -{ - public LightType Type { get; set; } - public Vector3 Direction { get; set; } - public Vector3 Position { get; set; } - public Vector3 Color { get; set; } - public float Intensity { get; set; } - public float Range { get; set; } -} - -internal sealed class SceneCamera -{ - public Vector3 Position { get; set; } - public Vector3 Target { get; set; } - public Vector3 Up { get; set; } - public float FieldOfView { get; set; } - public float NearPlane { get; set; } - public float FarPlane { get; set; } -} - -internal sealed class Vector3JsonConverter : JsonConverter -{ - public override Vector3 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.StartArray) - throw new JsonException("Expected array for Vector3"); - reader.Read(); - var x = reader.GetSingle(); - reader.Read(); - var y = reader.GetSingle(); - reader.Read(); - var z = reader.GetSingle(); - reader.Read(); - if (reader.TokenType != JsonTokenType.EndArray) - throw new JsonException("Expected 3 elements for Vector3"); - return new Vector3(x, y, z); - } - - public override void Write(Utf8JsonWriter writer, Vector3 value, JsonSerializerOptions options) - { - writer.WriteStartArray(); - writer.WriteNumberValue(value.X); - writer.WriteNumberValue(value.Y); - writer.WriteNumberValue(value.Z); - writer.WriteEndArray(); - } -} - -internal sealed class QuaternionJsonConverter : JsonConverter -{ - public override Quaternion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.StartArray) - throw new JsonException("Expected array for Quaternion"); - reader.Read(); - var x = reader.GetSingle(); - reader.Read(); - var y = reader.GetSingle(); - reader.Read(); - var z = reader.GetSingle(); - reader.Read(); - var w = reader.GetSingle(); - reader.Read(); - if (reader.TokenType != JsonTokenType.EndArray) - throw new JsonException("Expected 4 elements for Quaternion"); - return new Quaternion(x, y, z, w); - } - - public override void Write(Utf8JsonWriter writer, Quaternion value, JsonSerializerOptions options) - { - writer.WriteStartArray(); - writer.WriteNumberValue(value.X); - writer.WriteNumberValue(value.Y); - writer.WriteNumberValue(value.Z); - writer.WriteNumberValue(value.W); - writer.WriteEndArray(); - } -}