feat: Step 1 SDL3 window + Vulkan context + clear screen
This commit is contained in:
+26
@@ -0,0 +1,26 @@
|
|||||||
|
# Build outputs
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vs/
|
||||||
|
.vscode/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
|
||||||
|
# NuGet
|
||||||
|
*.nupkg
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# dotnet
|
||||||
|
*.dll
|
||||||
|
*.exe
|
||||||
|
*.pdb
|
||||||
|
*.cache
|
||||||
|
*.json
|
||||||
|
!*.csproj
|
||||||
|
!*.sln
|
||||||
|
!launchSettings.json
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Core", "src\Engine.Core\Engine.Core.csproj", "{11111111-1111-1111-1111-111111111111}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Graphics", "src\Engine.Graphics\Engine.Graphics.csproj", "{22222222-2222-2222-2222-222222222222}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CortexEngine.App", "src\CortexEngine.App\CortexEngine.App.csproj", "{33333333-3333-3333-3333-333333333333}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
ReleaseAOT|Any CPU = ReleaseAOT|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{11111111-1111-1111-1111-111111111111}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{11111111-1111-1111-1111-111111111111}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{11111111-1111-1111-1111-111111111111}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{11111111-1111-1111-1111-111111111111}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{11111111-1111-1111-1111-111111111111}.ReleaseAOT|Any CPU.ActiveCfg = ReleaseAOT|Any CPU
|
||||||
|
{11111111-1111-1111-1111-111111111111}.ReleaseAOT|Any CPU.Build.0 = ReleaseAOT|Any CPU
|
||||||
|
{22222222-2222-2222-2222-222222222222}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{22222222-2222-2222-2222-222222222222}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{22222222-2222-2222-2222-222222222222}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{22222222-2222-2222-2222-222222222222}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{22222222-2222-2222-2222-222222222222}.ReleaseAOT|Any CPU.ActiveCfg = ReleaseAOT|Any CPU
|
||||||
|
{22222222-2222-2222-2222-222222222222}.ReleaseAOT|Any CPU.Build.0 = ReleaseAOT|Any CPU
|
||||||
|
{33333333-3333-3333-3333-333333333333}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{33333333-3333-3333-3333-333333333333}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{33333333-3333-3333-3333-333333333333}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{33333333-3333-3333-3333-333333333333}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{33333333-3333-3333-3333-333333333333}.ReleaseAOT|Any CPU.ActiveCfg = ReleaseAOT|Any CPU
|
||||||
|
{33333333-3333-3333-3333-333333333333}.ReleaseAOT|Any CPU.Build.0 = ReleaseAOT|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
<PublishAot>false</PublishAot>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
|
||||||
|
<DefineConstants>DEV_MODE</DefineConstants>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)' == 'ReleaseAOT'">
|
||||||
|
<DefineConstants>RELEASE_AOT</DefineConstants>
|
||||||
|
<PublishAot>true</PublishAot>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Engine.Core\Engine.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\Engine.Graphics\Engine.Graphics.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System;
|
||||||
|
using Engine.Core;
|
||||||
|
using Engine.Graphics;
|
||||||
|
|
||||||
|
namespace CortexEngine.App;
|
||||||
|
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
static void Main(string[] args)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Cortex Engine Step 1 — Starting up...");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var window = new Sdl3Window("Cortex Engine — Step 1", 1280, 720);
|
||||||
|
var timing = new Timing();
|
||||||
|
var input = new InputMapping();
|
||||||
|
using var vulkan = new VulkanContext(window, enableValidation: true);
|
||||||
|
using var swapchain = new Swapchain(vulkan);
|
||||||
|
using var renderer = new ClearRenderer(vulkan, swapchain);
|
||||||
|
|
||||||
|
var frames = 0;
|
||||||
|
var lastFpsTime = 0.0;
|
||||||
|
|
||||||
|
while (!window.ShouldClose)
|
||||||
|
{
|
||||||
|
timing.Tick();
|
||||||
|
window.PumpEvents();
|
||||||
|
input.BeginFrame();
|
||||||
|
// Note: SDL events are already polled in PumpEvents.
|
||||||
|
// In a real engine, the window would expose an event iterator.
|
||||||
|
|
||||||
|
// Animate clear color over time.
|
||||||
|
var t = (float)timing.TotalTime;
|
||||||
|
var r = MathF.Sin(t * 0.5f) * 0.5f + 0.5f;
|
||||||
|
var g = MathF.Sin(t * 0.7f + 2.0f) * 0.5f + 0.5f;
|
||||||
|
var b = MathF.Sin(t * 0.9f + 4.0f) * 0.5f + 0.5f;
|
||||||
|
|
||||||
|
renderer.RenderFrame(r, g, b);
|
||||||
|
|
||||||
|
frames++;
|
||||||
|
if (timing.TotalTime - lastFpsTime >= 1.0)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"FPS: {frames}, Delta: {timing.DeltaTime * 1000.0:F2} ms");
|
||||||
|
frames = 0;
|
||||||
|
lastFpsTime = timing.TotalTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("Shutting down...");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Fatal error: {ex}");
|
||||||
|
Environment.Exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
<IsAotCompatible>true</IsAotCompatible>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
|
||||||
|
<DefineConstants>DEV_MODE</DefineConstants>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)' == 'ReleaseAOT'">
|
||||||
|
<DefineConstants>RELEASE_AOT</DefineConstants>
|
||||||
|
<PublishAot>true</PublishAot>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="ppy.SDL3-CS" Version="2026.520.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using SDL;
|
||||||
|
|
||||||
|
namespace Engine.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal snapshot of current input state.
|
||||||
|
/// Populated by polling SDL events once per frame.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class InputMapping
|
||||||
|
{
|
||||||
|
private readonly HashSet<SDL_Keycode> _keysPressed = new();
|
||||||
|
private readonly HashSet<SDL_Keycode> _keysDown = new();
|
||||||
|
private readonly HashSet<SDL_Keycode> _keysReleased = 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 void BeginFrame()
|
||||||
|
{
|
||||||
|
_keysPressed.Clear();
|
||||||
|
_keysReleased.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ProcessEvent(SDL_Event evt)
|
||||||
|
{
|
||||||
|
switch ((SDL_EventType)evt.Type)
|
||||||
|
{
|
||||||
|
case SDL_EventType.SDL_EVENT_KEY_DOWN:
|
||||||
|
if (!_keysDown.Contains((SDL_Keycode)evt.key.key))
|
||||||
|
_keysPressed.Add((SDL_Keycode)evt.key.key);
|
||||||
|
_keysDown.Add((SDL_Keycode)evt.key.key);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDL_EventType.SDL_EVENT_KEY_UP:
|
||||||
|
_keysDown.Remove((SDL_Keycode)evt.key.key);
|
||||||
|
_keysReleased.Add((SDL_Keycode)evt.key.key);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDL_EventType.SDL_EVENT_MOUSE_MOTION:
|
||||||
|
MouseX = (int)evt.motion.x;
|
||||||
|
MouseY = (int)evt.motion.y;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDL_EventType.SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||||
|
SetMouseButton(evt.button.button, true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDL_EventType.SDL_EVENT_MOUSE_BUTTON_UP:
|
||||||
|
SetMouseButton(evt.button.button, false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsKeyDown(SDL_Keycode key) => _keysDown.Contains(key);
|
||||||
|
public bool IsKeyPressed(SDL_Keycode key) => _keysPressed.Contains(key);
|
||||||
|
public bool IsKeyReleased(SDL_Keycode key) => _keysReleased.Contains(key);
|
||||||
|
|
||||||
|
private void SetMouseButton(byte button, bool pressed)
|
||||||
|
{
|
||||||
|
switch (button)
|
||||||
|
{
|
||||||
|
case 1: MouseLeft = pressed; break;
|
||||||
|
case 2: MouseMiddle = pressed; break;
|
||||||
|
case 3: MouseRight = pressed; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using SDL;
|
||||||
|
|
||||||
|
namespace Engine.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A thin, disposable wrapper around an SDL3 window.
|
||||||
|
/// Handles creation, Vulkan surface discovery, and event polling.
|
||||||
|
/// </summary>
|
||||||
|
public sealed unsafe class Sdl3Window : IDisposable
|
||||||
|
{
|
||||||
|
private readonly SDL_Window* _window;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public int Width { get; private set; }
|
||||||
|
public int Height { get; private set; }
|
||||||
|
public nint Handle => (nint)_window;
|
||||||
|
public bool ShouldClose { get; private set; }
|
||||||
|
|
||||||
|
public Sdl3Window(string title, int width, int height)
|
||||||
|
{
|
||||||
|
Width = width;
|
||||||
|
Height = height;
|
||||||
|
|
||||||
|
if (!SDL3.SDL_Init(SDL_InitFlags.SDL_INIT_VIDEO))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"SDL_Init failed: {SDL3.SDL_GetError()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var titleBytes = Encoding.UTF8.GetBytes(title + '\0');
|
||||||
|
fixed (byte* titlePtr = titleBytes)
|
||||||
|
{
|
||||||
|
_window = SDL3.SDL_CreateWindow(
|
||||||
|
titlePtr,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
SDL_WindowFlags.SDL_WINDOW_VULKAN | SDL_WindowFlags.SDL_WINDOW_RESIZABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_window == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"SDL_CreateWindow failed: {SDL3.SDL_GetError()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PumpEvents()
|
||||||
|
{
|
||||||
|
SDL_Event evt;
|
||||||
|
while (SDL3.SDL_PollEvent(&evt))
|
||||||
|
{
|
||||||
|
switch ((SDL_EventType)evt.Type)
|
||||||
|
{
|
||||||
|
case SDL_EventType.SDL_EVENT_QUIT:
|
||||||
|
ShouldClose = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDL_EventType.SDL_EVENT_WINDOW_RESIZED:
|
||||||
|
Width = evt.window.data1;
|
||||||
|
Height = evt.window.data2;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SDL_EventType.SDL_EVENT_KEY_DOWN:
|
||||||
|
if (evt.key.key == SDL_Keycode.SDLK_ESCAPE)
|
||||||
|
ShouldClose = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] GetRequiredInstanceExtensions()
|
||||||
|
{
|
||||||
|
uint count;
|
||||||
|
var extensionsPtr = SDL3.SDL_Vulkan_GetInstanceExtensions(&count);
|
||||||
|
if (extensionsPtr == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"SDL_Vulkan_GetInstanceExtensions failed: {SDL3.SDL_GetError()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new string[count];
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
result[i] = SDL3.PtrToStringUTF8(extensionsPtr[i]) ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
|
||||||
|
SDL3.SDL_DestroyWindow(_window);
|
||||||
|
SDL3.SDL_Quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Engine.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Frame timing and fixed-timestep helper.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class Timing
|
||||||
|
{
|
||||||
|
private readonly Stopwatch _stopwatch;
|
||||||
|
private double _lastTime;
|
||||||
|
private double _accumulator;
|
||||||
|
|
||||||
|
public double DeltaTime { get; private set; }
|
||||||
|
public double TotalTime { get; private set; }
|
||||||
|
public double FixedTimeStep { get; set; } = 1.0 / 60.0;
|
||||||
|
public double FixedTimeAccumulator => _accumulator;
|
||||||
|
|
||||||
|
public Timing()
|
||||||
|
{
|
||||||
|
_stopwatch = Stopwatch.StartNew();
|
||||||
|
_lastTime = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Tick()
|
||||||
|
{
|
||||||
|
var current = _stopwatch.Elapsed.TotalSeconds;
|
||||||
|
DeltaTime = current - _lastTime;
|
||||||
|
_lastTime = current;
|
||||||
|
TotalTime = current;
|
||||||
|
_accumulator += DeltaTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ConsumeFixedStep()
|
||||||
|
{
|
||||||
|
if (_accumulator < FixedTimeStep)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_accumulator -= FixedTimeStep;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetAccumulator()
|
||||||
|
{
|
||||||
|
if (_accumulator > FixedTimeStep * 5)
|
||||||
|
_accumulator = FixedTimeStep * 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
using System;
|
||||||
|
using Vortice.Vulkan;
|
||||||
|
|
||||||
|
namespace Engine.Graphics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimal renderer that clears the swapchain image to a solid color.
|
||||||
|
/// Serves as the foundational Step 1 rendering proof-of-concept.
|
||||||
|
/// </summary>
|
||||||
|
public sealed unsafe class ClearRenderer : IDisposable
|
||||||
|
{
|
||||||
|
private readonly VulkanContext _context;
|
||||||
|
private readonly Swapchain _swapchain;
|
||||||
|
private VkCommandPool _commandPool;
|
||||||
|
private VkCommandBuffer[] _commandBuffers;
|
||||||
|
private VkSemaphore[] _imageAvailableSemaphores;
|
||||||
|
private VkSemaphore[] _renderFinishedSemaphores;
|
||||||
|
private VkFence[] _inFlightFences;
|
||||||
|
private int _currentFrame;
|
||||||
|
|
||||||
|
public ClearRenderer(VulkanContext context, Swapchain swapchain)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_swapchain = swapchain;
|
||||||
|
CreateCommandPool();
|
||||||
|
CreateCommandBuffers();
|
||||||
|
CreateSyncObjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateCommandPool()
|
||||||
|
{
|
||||||
|
var createInfo = new VkCommandPoolCreateInfo
|
||||||
|
{
|
||||||
|
sType = VkStructureType.CommandPoolCreateInfo,
|
||||||
|
queueFamilyIndex = _context.GraphicsFamilyIndex,
|
||||||
|
flags = VkCommandPoolCreateFlags.ResetCommandBuffer
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _context.DeviceApi.vkCreateCommandPool(&createInfo, null, out _commandPool);
|
||||||
|
if (result != VkResult.Success)
|
||||||
|
throw new InvalidOperationException($"vkCreateCommandPool failed: {result}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateCommandBuffers()
|
||||||
|
{
|
||||||
|
_commandBuffers = new VkCommandBuffer[2];
|
||||||
|
for (var i = 0; i < _commandBuffers.Length; i++)
|
||||||
|
{
|
||||||
|
var allocInfo = new VkCommandBufferAllocateInfo
|
||||||
|
{
|
||||||
|
sType = VkStructureType.CommandBufferAllocateInfo,
|
||||||
|
commandPool = _commandPool,
|
||||||
|
level = VkCommandBufferLevel.Primary,
|
||||||
|
commandBufferCount = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _context.DeviceApi.vkAllocateCommandBuffer(&allocInfo, out _commandBuffers[i]);
|
||||||
|
if (result != VkResult.Success)
|
||||||
|
throw new InvalidOperationException($"vkAllocateCommandBuffers failed: {result}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateSyncObjects()
|
||||||
|
{
|
||||||
|
_imageAvailableSemaphores = new VkSemaphore[2];
|
||||||
|
_renderFinishedSemaphores = new VkSemaphore[2];
|
||||||
|
_inFlightFences = new VkFence[2];
|
||||||
|
|
||||||
|
var semaphoreInfo = new VkSemaphoreCreateInfo { sType = VkStructureType.SemaphoreCreateInfo };
|
||||||
|
var fenceInfo = new VkFenceCreateInfo
|
||||||
|
{
|
||||||
|
sType = VkStructureType.FenceCreateInfo,
|
||||||
|
flags = VkFenceCreateFlags.Signaled
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var i = 0; i < 2; i++)
|
||||||
|
{
|
||||||
|
_context.DeviceApi.vkCreateSemaphore(&semaphoreInfo, null, out _imageAvailableSemaphores[i]);
|
||||||
|
_context.DeviceApi.vkCreateSemaphore(&semaphoreInfo, null, out _renderFinishedSemaphores[i]);
|
||||||
|
_context.DeviceApi.vkCreateFence(&fenceInfo, null, out _inFlightFences[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RenderFrame(float r, float g, float b)
|
||||||
|
{
|
||||||
|
var frame = _currentFrame % 2;
|
||||||
|
|
||||||
|
_context.DeviceApi.vkWaitForFences(_inFlightFences[frame], true, ulong.MaxValue);
|
||||||
|
_context.DeviceApi.vkResetFences(_inFlightFences[frame]);
|
||||||
|
|
||||||
|
var result = _context.DeviceApi.vkAcquireNextImageKHR(
|
||||||
|
_swapchain.Handle,
|
||||||
|
ulong.MaxValue,
|
||||||
|
_imageAvailableSemaphores[frame],
|
||||||
|
VkFence.Null,
|
||||||
|
out var imageIndex);
|
||||||
|
|
||||||
|
if (result == VkResult.ErrorOutOfDateKHR)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd = _commandBuffers[frame];
|
||||||
|
_context.DeviceApi.vkResetCommandBuffer(cmd, VkCommandBufferResetFlags.None);
|
||||||
|
|
||||||
|
var beginInfo = new VkCommandBufferBeginInfo
|
||||||
|
{
|
||||||
|
sType = VkStructureType.CommandBufferBeginInfo,
|
||||||
|
flags = VkCommandBufferUsageFlags.OneTimeSubmit
|
||||||
|
};
|
||||||
|
_context.DeviceApi.vkBeginCommandBuffer(cmd, &beginInfo);
|
||||||
|
|
||||||
|
var clearColor = new VkClearValue(r, g, b, 1.0f);
|
||||||
|
|
||||||
|
var renderPassInfo = new VkRenderPassBeginInfo
|
||||||
|
{
|
||||||
|
sType = VkStructureType.RenderPassBeginInfo,
|
||||||
|
renderPass = _swapchain.RenderPass,
|
||||||
|
framebuffer = _swapchain.Framebuffers[imageIndex],
|
||||||
|
renderArea = new VkRect2D(0, 0, _swapchain.Extent.width, _swapchain.Extent.height),
|
||||||
|
clearValueCount = 1,
|
||||||
|
pClearValues = &clearColor
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.DeviceApi.vkCmdBeginRenderPass(cmd, &renderPassInfo, VkSubpassContents.Inline);
|
||||||
|
_context.DeviceApi.vkCmdEndRenderPass(cmd);
|
||||||
|
_context.DeviceApi.vkEndCommandBuffer(cmd);
|
||||||
|
|
||||||
|
var waitSemaphore = _imageAvailableSemaphores[frame];
|
||||||
|
var signalSemaphore = _renderFinishedSemaphores[frame];
|
||||||
|
var stageMask = VkPipelineStageFlags.ColorAttachmentOutput;
|
||||||
|
var submitInfo = new VkSubmitInfo
|
||||||
|
{
|
||||||
|
sType = VkStructureType.SubmitInfo,
|
||||||
|
waitSemaphoreCount = 1,
|
||||||
|
pWaitSemaphores = &waitSemaphore,
|
||||||
|
pWaitDstStageMask = &stageMask,
|
||||||
|
commandBufferCount = 1,
|
||||||
|
pCommandBuffers = &cmd,
|
||||||
|
signalSemaphoreCount = 1,
|
||||||
|
pSignalSemaphores = &signalSemaphore
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.DeviceApi.vkQueueSubmit(_context.GraphicsQueue, 1, &submitInfo, _inFlightFences[frame]);
|
||||||
|
|
||||||
|
var swapchain = _swapchain.Handle;
|
||||||
|
var presentInfo = new VkPresentInfoKHR
|
||||||
|
{
|
||||||
|
sType = VkStructureType.PresentInfoKHR,
|
||||||
|
waitSemaphoreCount = 1,
|
||||||
|
pWaitSemaphores = &signalSemaphore,
|
||||||
|
swapchainCount = 1,
|
||||||
|
pSwapchains = &swapchain,
|
||||||
|
pImageIndices = &imageIndex
|
||||||
|
};
|
||||||
|
|
||||||
|
var presentResult = _context.DeviceApi.vkQueuePresentKHR(_context.PresentQueue, &presentInfo);
|
||||||
|
if (presentResult == VkResult.ErrorOutOfDateKHR || presentResult == VkResult.SuboptimalKHR)
|
||||||
|
{
|
||||||
|
// Recreate handled externally.
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentFrame++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_context.DeviceApi.vkDeviceWaitIdle();
|
||||||
|
|
||||||
|
for (var i = 0; i < 2; i++)
|
||||||
|
{
|
||||||
|
if (_renderFinishedSemaphores[i] != VkSemaphore.Null)
|
||||||
|
_context.DeviceApi.vkDestroySemaphore(_renderFinishedSemaphores[i]);
|
||||||
|
if (_imageAvailableSemaphores[i] != VkSemaphore.Null)
|
||||||
|
_context.DeviceApi.vkDestroySemaphore(_imageAvailableSemaphores[i]);
|
||||||
|
if (_inFlightFences[i] != VkFence.Null)
|
||||||
|
_context.DeviceApi.vkDestroyFence(_inFlightFences[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_commandPool != VkCommandPool.Null)
|
||||||
|
_context.DeviceApi.vkDestroyCommandPool(_commandPool);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
<IsAotCompatible>true</IsAotCompatible>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
|
||||||
|
<DefineConstants>DEV_MODE</DefineConstants>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(Configuration)' == 'ReleaseAOT'">
|
||||||
|
<DefineConstants>RELEASE_AOT</DefineConstants>
|
||||||
|
<PublishAot>true</PublishAot>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Vortice.Vulkan" Version="3.2.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Engine.Core\Engine.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
using System;
|
||||||
|
using Vortice.Vulkan;
|
||||||
|
|
||||||
|
namespace Engine.Graphics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manages the Vulkan swapchain, image views, render pass, and framebuffers.
|
||||||
|
/// Recreates itself automatically when the window is resized.
|
||||||
|
/// </summary>
|
||||||
|
public sealed unsafe class Swapchain : IDisposable
|
||||||
|
{
|
||||||
|
private readonly VulkanContext _context;
|
||||||
|
private VkRenderPass _renderPass;
|
||||||
|
private VkSwapchainKHR _swapchain;
|
||||||
|
private VkImage[] _images;
|
||||||
|
private VkImageView[] _imageViews;
|
||||||
|
private VkFramebuffer[] _framebuffers;
|
||||||
|
private VkSurfaceFormatKHR _surfaceFormat;
|
||||||
|
private VkPresentModeKHR _presentMode;
|
||||||
|
private VkExtent2D _extent;
|
||||||
|
|
||||||
|
public VkRenderPass RenderPass => _renderPass;
|
||||||
|
public VkFramebuffer[] Framebuffers => _framebuffers;
|
||||||
|
public VkExtent2D Extent => _extent;
|
||||||
|
public VkSwapchainKHR Handle => _swapchain;
|
||||||
|
public uint ImageCount => (uint)_images.Length;
|
||||||
|
|
||||||
|
public Swapchain(VulkanContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_surfaceFormat = ChooseSurfaceFormat();
|
||||||
|
CreateRenderPass();
|
||||||
|
Recreate(1280, 720);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Recreate(int width, int height)
|
||||||
|
{
|
||||||
|
_context.DeviceApi.vkDeviceWaitIdle();
|
||||||
|
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 VkSwapchainCreateInfoKHR
|
||||||
|
{
|
||||||
|
sType = VkStructureType.SwapchainCreateInfoKHR,
|
||||||
|
surface = _context.Surface,
|
||||||
|
minImageCount = imageCount,
|
||||||
|
imageFormat = _surfaceFormat.format,
|
||||||
|
imageColorSpace = _surfaceFormat.colorSpace,
|
||||||
|
imageExtent = _extent,
|
||||||
|
imageArrayLayers = 1,
|
||||||
|
imageUsage = VkImageUsageFlags.ColorAttachment,
|
||||||
|
imageSharingMode = VkSharingMode.Exclusive,
|
||||||
|
preTransform = capabilities.currentTransform,
|
||||||
|
compositeAlpha = VkCompositeAlphaFlagsKHR.Opaque,
|
||||||
|
presentMode = _presentMode,
|
||||||
|
clipped = true,
|
||||||
|
oldSwapchain = _swapchain
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _context.DeviceApi.vkCreateSwapchainKHR(&createInfo, null, out _swapchain);
|
||||||
|
if (result != VkResult.Success)
|
||||||
|
throw new InvalidOperationException($"vkCreateSwapchainKHR failed: {result}");
|
||||||
|
|
||||||
|
_images = GetSwapchainImages();
|
||||||
|
_imageViews = new VkImageView[_images.Length];
|
||||||
|
_framebuffers = new VkFramebuffer[_images.Length];
|
||||||
|
|
||||||
|
for (var i = 0; i < _images.Length; i++)
|
||||||
|
{
|
||||||
|
_imageViews[i] = CreateImageView(_images[i], _surfaceFormat.format);
|
||||||
|
_framebuffers[i] = CreateFramebuffer(_imageViews[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private VkSurfaceCapabilitiesKHR GetSurfaceCapabilities()
|
||||||
|
{
|
||||||
|
var result = _context.InstanceApi.vkGetPhysicalDeviceSurfaceCapabilitiesKHR(_context.PhysicalDevice, _context.Surface, out var capabilities);
|
||||||
|
if (result != VkResult.Success)
|
||||||
|
throw new InvalidOperationException($"vkGetPhysicalDeviceSurfaceCapabilitiesKHR failed: {result}");
|
||||||
|
return capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VkImage[] GetSwapchainImages()
|
||||||
|
{
|
||||||
|
uint count = 0;
|
||||||
|
_context.DeviceApi.vkGetSwapchainImagesKHR(_swapchain, &count, null);
|
||||||
|
var images = new VkImage[count];
|
||||||
|
fixed (VkImage* p = images)
|
||||||
|
{
|
||||||
|
var result = _context.DeviceApi.vkGetSwapchainImagesKHR(_swapchain, &count, p);
|
||||||
|
if (result != VkResult.Success)
|
||||||
|
throw new InvalidOperationException($"vkGetSwapchainImagesKHR failed: {result}");
|
||||||
|
}
|
||||||
|
return images;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateRenderPass()
|
||||||
|
{
|
||||||
|
var colorAttachment = new VkAttachmentDescription
|
||||||
|
{
|
||||||
|
format = _surfaceFormat.format != VkFormat.Undefined ? _surfaceFormat.format : VkFormat.B8G8R8A8Unorm,
|
||||||
|
samples = VkSampleCountFlags.Count1,
|
||||||
|
loadOp = VkAttachmentLoadOp.Clear,
|
||||||
|
storeOp = VkAttachmentStoreOp.Store,
|
||||||
|
stencilLoadOp = VkAttachmentLoadOp.DontCare,
|
||||||
|
stencilStoreOp = VkAttachmentStoreOp.DontCare,
|
||||||
|
initialLayout = VkImageLayout.Undefined,
|
||||||
|
finalLayout = VkImageLayout.PresentSrcKHR
|
||||||
|
};
|
||||||
|
|
||||||
|
var colorAttachmentRef = new VkAttachmentReference
|
||||||
|
{
|
||||||
|
attachment = 0,
|
||||||
|
layout = VkImageLayout.ColorAttachmentOptimal
|
||||||
|
};
|
||||||
|
|
||||||
|
var subpass = new VkSubpassDescription
|
||||||
|
{
|
||||||
|
pipelineBindPoint = VkPipelineBindPoint.Graphics,
|
||||||
|
colorAttachmentCount = 1,
|
||||||
|
pColorAttachments = &colorAttachmentRef
|
||||||
|
};
|
||||||
|
|
||||||
|
var dependency = new VkSubpassDependency
|
||||||
|
{
|
||||||
|
srcSubpass = Vulkan.VK_SUBPASS_EXTERNAL,
|
||||||
|
dstSubpass = 0,
|
||||||
|
srcStageMask = VkPipelineStageFlags.ColorAttachmentOutput,
|
||||||
|
dstStageMask = VkPipelineStageFlags.ColorAttachmentOutput,
|
||||||
|
srcAccessMask = VkAccessFlags.None,
|
||||||
|
dstAccessMask = VkAccessFlags.ColorAttachmentWrite
|
||||||
|
};
|
||||||
|
|
||||||
|
var createInfo = new VkRenderPassCreateInfo
|
||||||
|
{
|
||||||
|
sType = VkStructureType.RenderPassCreateInfo,
|
||||||
|
attachmentCount = 1,
|
||||||
|
pAttachments = &colorAttachment,
|
||||||
|
subpassCount = 1,
|
||||||
|
pSubpasses = &subpass,
|
||||||
|
dependencyCount = 1,
|
||||||
|
pDependencies = &dependency
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _context.DeviceApi.vkCreateRenderPass(&createInfo, null, out _renderPass);
|
||||||
|
if (result != VkResult.Success)
|
||||||
|
throw new InvalidOperationException($"vkCreateRenderPass failed: {result}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private VkImageView CreateImageView(VkImage image, VkFormat format)
|
||||||
|
{
|
||||||
|
var createInfo = new VkImageViewCreateInfo
|
||||||
|
{
|
||||||
|
sType = VkStructureType.ImageViewCreateInfo,
|
||||||
|
image = image,
|
||||||
|
viewType = VkImageViewType.Image2D,
|
||||||
|
format = format,
|
||||||
|
components = new VkComponentMapping(VkComponentSwizzle.R, VkComponentSwizzle.G, VkComponentSwizzle.B, VkComponentSwizzle.A),
|
||||||
|
subresourceRange = new VkImageSubresourceRange(VkImageAspectFlags.Color, 0, 1, 0, 1)
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _context.DeviceApi.vkCreateImageView(&createInfo, null, out var imageView);
|
||||||
|
if (result != VkResult.Success)
|
||||||
|
throw new InvalidOperationException($"vkCreateImageView failed: {result}");
|
||||||
|
|
||||||
|
return imageView;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VkFramebuffer CreateFramebuffer(VkImageView imageView)
|
||||||
|
{
|
||||||
|
var createInfo = new VkFramebufferCreateInfo
|
||||||
|
{
|
||||||
|
sType = VkStructureType.FramebufferCreateInfo,
|
||||||
|
renderPass = _renderPass,
|
||||||
|
attachmentCount = 1,
|
||||||
|
pAttachments = &imageView,
|
||||||
|
width = _extent.width,
|
||||||
|
height = _extent.height,
|
||||||
|
layers = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _context.DeviceApi.vkCreateFramebuffer(&createInfo, null, out var framebuffer);
|
||||||
|
if (result != VkResult.Success)
|
||||||
|
throw new InvalidOperationException($"vkCreateFramebuffer failed: {result}");
|
||||||
|
|
||||||
|
return framebuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VkSurfaceFormatKHR ChooseSurfaceFormat()
|
||||||
|
{
|
||||||
|
var formats = GetSurfaceFormats();
|
||||||
|
foreach (var format in formats)
|
||||||
|
{
|
||||||
|
if (format.format == VkFormat.B8G8R8A8Unorm && format.colorSpace == VkColorSpaceKHR.SrgbNonLinear)
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
return formats[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private VkSurfaceFormatKHR[] GetSurfaceFormats()
|
||||||
|
{
|
||||||
|
uint count = 0;
|
||||||
|
_context.InstanceApi.vkGetPhysicalDeviceSurfaceFormatsKHR(_context.PhysicalDevice, _context.Surface, &count, null);
|
||||||
|
var formats = new VkSurfaceFormatKHR[count];
|
||||||
|
fixed (VkSurfaceFormatKHR* p = formats)
|
||||||
|
{
|
||||||
|
var result = _context.InstanceApi.vkGetPhysicalDeviceSurfaceFormatsKHR(_context.PhysicalDevice, _context.Surface, &count, p);
|
||||||
|
if (result != VkResult.Success)
|
||||||
|
throw new InvalidOperationException($"vkGetPhysicalDeviceSurfaceFormatsKHR failed: {result}");
|
||||||
|
}
|
||||||
|
return formats;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VkPresentModeKHR ChoosePresentMode()
|
||||||
|
{
|
||||||
|
var modes = GetSurfacePresentModes();
|
||||||
|
if (Array.Exists(modes, m => m == VkPresentModeKHR.Mailbox))
|
||||||
|
return VkPresentModeKHR.Mailbox;
|
||||||
|
return VkPresentModeKHR.Fifo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VkPresentModeKHR[] GetSurfacePresentModes()
|
||||||
|
{
|
||||||
|
uint count = 0;
|
||||||
|
_context.InstanceApi.vkGetPhysicalDeviceSurfacePresentModesKHR(_context.PhysicalDevice, _context.Surface, &count, null);
|
||||||
|
var modes = new VkPresentModeKHR[count];
|
||||||
|
fixed (VkPresentModeKHR* p = modes)
|
||||||
|
{
|
||||||
|
var result = _context.InstanceApi.vkGetPhysicalDeviceSurfacePresentModesKHR(_context.PhysicalDevice, _context.Surface, &count, p);
|
||||||
|
if (result != VkResult.Success)
|
||||||
|
throw new InvalidOperationException($"vkGetPhysicalDeviceSurfacePresentModesKHR failed: {result}");
|
||||||
|
}
|
||||||
|
return modes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VkExtent2D ChooseExtent(VkSurfaceCapabilitiesKHR capabilities, uint width, uint height)
|
||||||
|
{
|
||||||
|
if (capabilities.currentExtent.width != uint.MaxValue)
|
||||||
|
return capabilities.currentExtent;
|
||||||
|
|
||||||
|
var extent = new VkExtent2D
|
||||||
|
{
|
||||||
|
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 == VkDevice.Null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_framebuffers != null)
|
||||||
|
{
|
||||||
|
foreach (var fb in _framebuffers)
|
||||||
|
{
|
||||||
|
if (fb != VkFramebuffer.Null)
|
||||||
|
_context.DeviceApi.vkDestroyFramebuffer(fb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_imageViews != null)
|
||||||
|
{
|
||||||
|
foreach (var view in _imageViews)
|
||||||
|
{
|
||||||
|
if (view != VkImageView.Null)
|
||||||
|
_context.DeviceApi.vkDestroyImageView(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_swapchain != VkSwapchainKHR.Null)
|
||||||
|
_context.DeviceApi.vkDestroySwapchainKHR(_swapchain);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_context.DeviceApi.vkDeviceWaitIdle();
|
||||||
|
CleanupSwapchain();
|
||||||
|
|
||||||
|
if (_renderPass != VkRenderPass.Null)
|
||||||
|
_context.DeviceApi.vkDestroyRenderPass(_renderPass);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using Engine.Core;
|
||||||
|
using SDL;
|
||||||
|
using Vortice.Vulkan;
|
||||||
|
|
||||||
|
namespace Engine.Graphics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owns the Vulkan instance, physical device, logical device, queues, and API handles.
|
||||||
|
/// Created once per application lifetime.
|
||||||
|
/// </summary>
|
||||||
|
public sealed unsafe class VulkanContext : IDisposable
|
||||||
|
{
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public VkInstance Instance { get; private set; }
|
||||||
|
public VkInstanceApi InstanceApi { get; private set; }
|
||||||
|
public VkPhysicalDevice PhysicalDevice { get; private set; }
|
||||||
|
public VkDevice Device { get; private set; }
|
||||||
|
public VkDeviceApi DeviceApi { get; private set; }
|
||||||
|
public VkQueue GraphicsQueue { get; private set; }
|
||||||
|
public VkQueue PresentQueue { get; private set; }
|
||||||
|
public uint GraphicsFamilyIndex { get; private set; }
|
||||||
|
public uint PresentFamilyIndex { get; private set; }
|
||||||
|
public VkSurfaceKHR Surface { get; private set; }
|
||||||
|
|
||||||
|
public VulkanContext(Sdl3Window window, bool enableValidation = true)
|
||||||
|
{
|
||||||
|
CreateInstance(window, enableValidation);
|
||||||
|
InstanceApi = Vulkan.GetApi(Instance);
|
||||||
|
CreateSurface(window);
|
||||||
|
PickPhysicalDevice();
|
||||||
|
CreateLogicalDevice();
|
||||||
|
DeviceApi = Vulkan.GetApi(Instance, Device);
|
||||||
|
GetQueues();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateInstance(Sdl3Window window, bool enableValidation)
|
||||||
|
{
|
||||||
|
var requiredExtensions = new List<string>(window.GetRequiredInstanceExtensions());
|
||||||
|
if (enableValidation)
|
||||||
|
{
|
||||||
|
requiredExtensions.Add("VK_EXT_debug_utils");
|
||||||
|
}
|
||||||
|
|
||||||
|
var layerNames = enableValidation
|
||||||
|
? new[] { "VK_LAYER_KHRONOS_validation" }
|
||||||
|
: Array.Empty<string>();
|
||||||
|
|
||||||
|
var appName = VkStringInterop.ConvertToUnmanaged("Cortex Engine");
|
||||||
|
var engineName = VkStringInterop.ConvertToUnmanaged("CortexEngine");
|
||||||
|
|
||||||
|
var appInfo = new VkApplicationInfo
|
||||||
|
{
|
||||||
|
sType = VkStructureType.ApplicationInfo,
|
||||||
|
pApplicationName = appName,
|
||||||
|
pEngineName = engineName,
|
||||||
|
apiVersion = VkVersion.Version_1_3
|
||||||
|
};
|
||||||
|
|
||||||
|
using var extensionPin = new StringArrayPin(requiredExtensions);
|
||||||
|
using var layerPin = new StringArrayPin(layerNames);
|
||||||
|
|
||||||
|
{
|
||||||
|
var createInfo = new VkInstanceCreateInfo
|
||||||
|
{
|
||||||
|
sType = VkStructureType.InstanceCreateInfo,
|
||||||
|
pApplicationInfo = &appInfo,
|
||||||
|
enabledExtensionCount = (uint)requiredExtensions.Count,
|
||||||
|
ppEnabledExtensionNames = extensionPin.Pointers,
|
||||||
|
enabledLayerCount = (uint)layerNames.Length,
|
||||||
|
ppEnabledLayerNames = layerPin.Pointers
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = Vulkan.vkCreateInstance(&createInfo, null, out var instance);
|
||||||
|
if (result != VkResult.Success)
|
||||||
|
throw new InvalidOperationException($"vkCreateInstance failed: {result}");
|
||||||
|
Instance = instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
VkStringInterop.Free(appName);
|
||||||
|
VkStringInterop.Free(engineName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateSurface(Sdl3Window window)
|
||||||
|
{
|
||||||
|
var sdlInstance = (SDL.VkInstance_T*)Instance.Handle;
|
||||||
|
var sdlSurface = (SDL.VkSurfaceKHR_T*)null;
|
||||||
|
var result = SDL3.SDL_Vulkan_CreateSurface(
|
||||||
|
(SDL_Window*)window.Handle,
|
||||||
|
sdlInstance,
|
||||||
|
null,
|
||||||
|
&sdlSurface);
|
||||||
|
|
||||||
|
if (result != true)
|
||||||
|
throw new InvalidOperationException($"SDL_Vulkan_CreateSurface failed: {SDL3.SDL_GetError()}");
|
||||||
|
|
||||||
|
Surface = new VkSurfaceKHR((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 = InstanceApi.vkGetPhysicalDeviceProperties(device);
|
||||||
|
var queueFamilies = GetPhysicalDeviceQueueFamilyProperties(device);
|
||||||
|
|
||||||
|
var hasGraphics = false;
|
||||||
|
var hasPresent = false;
|
||||||
|
for (var i = 0; i < queueFamilies.Length; i++)
|
||||||
|
{
|
||||||
|
if (queueFamilies[i].queueFlags.HasFlag(VkQueueFlags.Graphics))
|
||||||
|
hasGraphics = true;
|
||||||
|
var supportResult = InstanceApi.vkGetPhysicalDeviceSurfaceSupportKHR(device, (uint)i, Surface, out VkBool32 supported);
|
||||||
|
if (supportResult == VkResult.Success && supported)
|
||||||
|
hasPresent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasGraphics && hasPresent)
|
||||||
|
{
|
||||||
|
PhysicalDevice = device;
|
||||||
|
if (properties.deviceType == VkPhysicalDeviceType.DiscreteGpu)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PhysicalDevice == VkPhysicalDevice.Null)
|
||||||
|
throw new InvalidOperationException("No suitable Vulkan physical device found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private VkPhysicalDevice[] EnumeratePhysicalDevices()
|
||||||
|
{
|
||||||
|
uint count = 0;
|
||||||
|
InstanceApi.vkEnumeratePhysicalDevices(&count, null);
|
||||||
|
if (count == 0)
|
||||||
|
return Array.Empty<VkPhysicalDevice>();
|
||||||
|
|
||||||
|
var devices = new VkPhysicalDevice[count];
|
||||||
|
fixed (VkPhysicalDevice* p = devices)
|
||||||
|
{
|
||||||
|
var result = InstanceApi.vkEnumeratePhysicalDevices(&count, p);
|
||||||
|
if (result != VkResult.Success)
|
||||||
|
throw new InvalidOperationException($"vkEnumeratePhysicalDevices failed: {result}");
|
||||||
|
}
|
||||||
|
return devices;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateLogicalDevice()
|
||||||
|
{
|
||||||
|
var queueFamilies = GetPhysicalDeviceQueueFamilyProperties(PhysicalDevice);
|
||||||
|
GraphicsFamilyIndex = FindQueueFamilyIndex(queueFamilies, VkQueueFlags.Graphics);
|
||||||
|
PresentFamilyIndex = FindPresentQueueFamilyIndex(queueFamilies);
|
||||||
|
|
||||||
|
var uniqueFamilies = new HashSet<uint> { GraphicsFamilyIndex, PresentFamilyIndex };
|
||||||
|
var queueCreateInfos = uniqueFamilies.Select(family => new VkDeviceQueueCreateInfo
|
||||||
|
{
|
||||||
|
sType = VkStructureType.DeviceQueueCreateInfo,
|
||||||
|
queueFamilyIndex = family,
|
||||||
|
queueCount = 1
|
||||||
|
}).ToArray();
|
||||||
|
|
||||||
|
var priorityHandles = new GCHandle[queueCreateInfos.Length];
|
||||||
|
var extensionNames = new[] { "VK_KHR_swapchain" };
|
||||||
|
using var extensionPin = new StringArrayPin(extensionNames);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var deviceFeatures = new VkPhysicalDeviceFeatures();
|
||||||
|
|
||||||
|
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 (VkDeviceQueueCreateInfo* pQueue = queueCreateInfos)
|
||||||
|
{
|
||||||
|
var createInfo = new VkDeviceCreateInfo
|
||||||
|
{
|
||||||
|
sType = VkStructureType.DeviceCreateInfo,
|
||||||
|
queueCreateInfoCount = (uint)queueCreateInfos.Length,
|
||||||
|
pQueueCreateInfos = pQueue,
|
||||||
|
pEnabledFeatures = &deviceFeatures,
|
||||||
|
enabledExtensionCount = 1,
|
||||||
|
ppEnabledExtensionNames = extensionPin.Pointers
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = InstanceApi.vkCreateDevice(PhysicalDevice, &createInfo, null, out var device);
|
||||||
|
if (result != VkResult.Success)
|
||||||
|
throw new InvalidOperationException($"vkCreateDevice failed: {result}");
|
||||||
|
Device = device;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
foreach (var handle in priorityHandles)
|
||||||
|
{
|
||||||
|
if (handle.IsAllocated)
|
||||||
|
handle.Free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GetQueues()
|
||||||
|
{
|
||||||
|
DeviceApi.vkGetDeviceQueue(GraphicsFamilyIndex, 0, out var graphicsQueue);
|
||||||
|
DeviceApi.vkGetDeviceQueue(PresentFamilyIndex, 0, out var presentQueue);
|
||||||
|
GraphicsQueue = graphicsQueue;
|
||||||
|
PresentQueue = presentQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private uint FindQueueFamilyIndex(VkQueueFamilyProperties[] properties, VkQueueFlags 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(VkQueueFamilyProperties[] properties)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < properties.Length; i++)
|
||||||
|
{
|
||||||
|
var supportResult = InstanceApi.vkGetPhysicalDeviceSurfaceSupportKHR(PhysicalDevice, (uint)i, Surface, out VkBool32 supported);
|
||||||
|
if (supportResult == VkResult.Success && supported)
|
||||||
|
return (uint)i;
|
||||||
|
}
|
||||||
|
throw new InvalidOperationException("No present queue family found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private VkQueueFamilyProperties[] GetPhysicalDeviceQueueFamilyProperties(VkPhysicalDevice device)
|
||||||
|
{
|
||||||
|
uint count = 0;
|
||||||
|
InstanceApi.vkGetPhysicalDeviceQueueFamilyProperties(device, &count, null);
|
||||||
|
var properties = new VkQueueFamilyProperties[count];
|
||||||
|
fixed (VkQueueFamilyProperties* p = properties)
|
||||||
|
{
|
||||||
|
InstanceApi.vkGetPhysicalDeviceQueueFamilyProperties(device, &count, p);
|
||||||
|
}
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed unsafe class StringArrayPin : IDisposable
|
||||||
|
{
|
||||||
|
public byte** Pointers;
|
||||||
|
private readonly GCHandle[] _handles;
|
||||||
|
|
||||||
|
public StringArrayPin(IReadOnlyList<string> strings)
|
||||||
|
{
|
||||||
|
if (strings.Count == 0)
|
||||||
|
{
|
||||||
|
Pointers = null;
|
||||||
|
_handles = Array.Empty<GCHandle>();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Pointers = (byte**)Marshal.AllocHGlobal(strings.Count * sizeof(byte*));
|
||||||
|
_handles = new GCHandle[strings.Count];
|
||||||
|
|
||||||
|
for (var i = 0; i < strings.Count; i++)
|
||||||
|
{
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(strings[i] + '\0');
|
||||||
|
_handles[i] = GCHandle.Alloc(bytes, GCHandleType.Pinned);
|
||||||
|
Pointers[i] = (byte*)_handles[i].AddrOfPinnedObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (Pointers == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var handle in _handles)
|
||||||
|
{
|
||||||
|
if (handle.IsAllocated)
|
||||||
|
handle.Free();
|
||||||
|
}
|
||||||
|
|
||||||
|
Marshal.FreeHGlobal((nint)Pointers);
|
||||||
|
Pointers = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
|
||||||
|
if (Device != VkDevice.Null)
|
||||||
|
DeviceApi.vkDestroyDevice();
|
||||||
|
if (Instance != VkInstance.Null && Surface != VkSurfaceKHR.Null)
|
||||||
|
InstanceApi.vkDestroySurfaceKHR(Surface);
|
||||||
|
if (Instance != VkInstance.Null)
|
||||||
|
InstanceApi.vkDestroyInstance();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user