feat: modular HAL, Raylib backend, PBR shading, textures, 60 unit tests
- Replace hardcoded SDL3 windowing with IWindow/IInputState/Key abstractions - Each render backend owns its window (Raylib GLFW, SDL3 for Vulkan) - Raylib backend: DrawModelEx, custom GLSL shader with Fresnel, ACES tonemapping, gamma correction, hemisphere ambient - Fix backface culling, mesh memory (NativeMemory.Alloc), texture loading - Camera controllers use backend-agnostic Key enum (inverted yaw/strafe) - Demo scene: 8 cubes, 7 spheres, torus knot OBJ with checker texture - Extract ProceduralMesh + MeshMath from Program.cs to Engine.Graphics - Vulkan backend deferred (compiles, untested, IWindow-compatible) - 60 unit tests: ObjLoader, camera controllers, AiCommandProcessor, RenderBackendFactory, Timing, ProceduralMesh, MeshMath, Transform - AGENTS.md for opencode integration
This commit is contained in:
@@ -18,16 +18,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Silk.NET.Vulkan" Version="2.21.0" />
|
||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.21.0" />
|
||||
<PackageReference Include="Flecs.NET.Debug" Version="4.0.4-build.546" Condition="'$(Configuration)' == 'Debug'" />
|
||||
<PackageReference Include="Flecs.NET.Release" Version="4.0.4-build.546" Condition="'$(Configuration)' == 'Release' OR '$(Configuration)' == 'ReleaseAOT'" />
|
||||
<PackageReference Include="SharpGLTF.Core" Version="1.0.6" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Shaders\*.spv" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using Engine.Core;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Abstraction over a graphics backend (Vulkan, Raylib, etc.).
|
||||
/// Each backend owns its window and surface. The application retrieves
|
||||
/// the window via <see cref="Window"/> for input and event polling.
|
||||
/// </summary>
|
||||
public interface IRenderContext : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The window owned by this backend. The application uses this for
|
||||
/// input polling, resize detection, and close requests.
|
||||
/// </summary>
|
||||
IWindow Window { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a renderer that can draw the ECS world using this backend.
|
||||
/// </summary>
|
||||
IRenderer CreateRenderer();
|
||||
|
||||
/// <summary>
|
||||
/// Notify the backend that the output surface has been resized.
|
||||
/// </summary>
|
||||
void Resize(int width, int height);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using Engine.Core;
|
||||
using Flecs.NET.Core;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Renders the ECS world and exposes screenshot capture.
|
||||
/// Implemented by concrete graphics backends.
|
||||
/// </summary>
|
||||
public interface IRenderer : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Render one frame of the ECS world and present it.
|
||||
/// </summary>
|
||||
void RenderWorld(World world);
|
||||
|
||||
/// <summary>
|
||||
/// Request a screenshot of the next rendered frame to be saved to disk.
|
||||
/// </summary>
|
||||
void RequestScreenshot(string outputPath);
|
||||
|
||||
/// <summary>
|
||||
/// True if a screenshot has been requested but not yet captured.
|
||||
/// </summary>
|
||||
bool IsScreenshotRequested { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Provider that can asynchronously capture the current frame to PNG bytes.
|
||||
/// </summary>
|
||||
IScreenshotProvider ScreenshotProvider { get; }
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
using System;
|
||||
using Silk.NET.Core;
|
||||
using Silk.NET.Vulkan;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// GPU index buffer for indexed draws.
|
||||
/// Uses 32-bit indices.
|
||||
/// </summary>
|
||||
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<byte> 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<byte> 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);
|
||||
}
|
||||
}
|
||||
@@ -81,14 +81,5 @@ public static class GltfLoader
|
||||
}
|
||||
|
||||
private static Vector3 ComputeFaceNormal(Vector3 a, Vector3 b, Vector3 c)
|
||||
{
|
||||
var ab = b - a;
|
||||
var ac = c - a;
|
||||
var normal = Vector3.Cross(ab, ac);
|
||||
if (normal.LengthSquared() > 0.00001f)
|
||||
normal = Vector3.Normalize(normal);
|
||||
else
|
||||
normal = Vector3.UnitY;
|
||||
return normal;
|
||||
}
|
||||
=> MeshMath.ComputeFaceNormal(a, b, c);
|
||||
}
|
||||
|
||||
@@ -84,14 +84,5 @@ public static class ObjLoader
|
||||
}
|
||||
|
||||
private static Vector3 ComputeFaceNormal(Vector3 a, Vector3 b, Vector3 c)
|
||||
{
|
||||
var ab = b - a;
|
||||
var ac = c - a;
|
||||
var normal = Vector3.Cross(ab, ac);
|
||||
if (normal.LengthSquared() > 0.00001f)
|
||||
normal = Vector3.Normalize(normal);
|
||||
else
|
||||
normal = Vector3.UnitY;
|
||||
return normal;
|
||||
}
|
||||
=> MeshMath.ComputeFaceNormal(a, b, c);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Shared mesh math utilities used by loaders and procedural generators.
|
||||
/// </summary>
|
||||
public static class MeshMath
|
||||
{
|
||||
/// <summary>
|
||||
/// Compute a flat face normal from three vertex positions.
|
||||
/// Falls back to Vector3.UnitY for degenerate (zero-area) triangles.
|
||||
/// </summary>
|
||||
public static Vector3 ComputeFaceNormal(Vector3 a, Vector3 b, Vector3 c)
|
||||
{
|
||||
var ab = b - a;
|
||||
var ac = c - a;
|
||||
var normal = Vector3.Cross(ab, ac);
|
||||
if (normal.LengthSquared() > 0.00001f)
|
||||
normal = Vector3.Normalize(normal);
|
||||
else
|
||||
normal = Vector3.UnitY;
|
||||
return normal;
|
||||
}
|
||||
}
|
||||
@@ -1,665 +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;
|
||||
|
||||
/// <summary>
|
||||
/// Renders indexed meshes attached to ECS entities.
|
||||
/// Uses Silk.NET.Vulkan and reads Mesh + Transform components from the ECS world.
|
||||
/// </summary>
|
||||
public sealed unsafe class MeshRenderer : IDisposable
|
||||
{
|
||||
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<string, Texture> _textures = new();
|
||||
private readonly Dictionary<Texture, DescriptorSet> _textureDescriptorSets = new();
|
||||
private Texture? _defaultTexture;
|
||||
private readonly Dictionary<Entity, MeshBuffers> _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 MeshRenderer(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<Rgba32>(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;
|
||||
|
||||
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();
|
||||
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<Material>() ? e.Get<Material>() : 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Engine.Core;
|
||||
using Engine.Core.Components;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Procedural mesh generators for common primitive shapes.
|
||||
/// All methods are pure CPU — no GPU/display dependencies.
|
||||
/// </summary>
|
||||
public static class ProceduralMesh
|
||||
{
|
||||
/// <summary>
|
||||
/// Generate a UV sphere mesh.
|
||||
/// </summary>
|
||||
/// <param name="radius">Sphere radius.</param>
|
||||
/// <param name="segments">Longitude segments (around the equator).</param>
|
||||
/// <param name="rings">Latitude rings (from pole to pole).</param>
|
||||
/// <param name="color">Vertex color applied to all vertices.</param>
|
||||
public static Mesh CreateSphere(float radius, int segments, int rings, Vector3 color)
|
||||
{
|
||||
var vertices = new List<Vertex>();
|
||||
var indices = new List<uint>();
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a ground grid mesh at Y=0, consisting of thin quads.
|
||||
/// </summary>
|
||||
/// <param name="lines">Number of grid lines on each side of the origin.</param>
|
||||
/// <param name="spacing">Distance between grid lines.</param>
|
||||
/// <param name="color">Vertex color applied to all vertices.</param>
|
||||
public static Mesh CreateGrid(int lines, float spacing, Vector3 color)
|
||||
{
|
||||
var vertices = new List<Vertex>();
|
||||
var indices = new List<uint>();
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Engine.Core;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static class RenderBackendFactory
|
||||
{
|
||||
private static readonly Dictionary<string, Func<int, int, bool, IRenderContext>> _registry
|
||||
= new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Register a backend implementation under the given name.
|
||||
/// The factory receives (width, height, enableValidation) and must create
|
||||
/// its own window and render context.
|
||||
/// </summary>
|
||||
public static void Register(string name, Func<int, int, bool, IRenderContext> factory)
|
||||
{
|
||||
_registry[name] = factory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a backend instance for the given name.
|
||||
/// The backend assembly must have registered itself before this is called.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Silk.NET.Core;
|
||||
using Silk.NET.Vulkan;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Captures the current swapchain image to a PNG file on disk.
|
||||
/// Used by AI agents to visually inspect the running engine.
|
||||
/// </summary>
|
||||
public sealed unsafe class ScreenshotCapture : IDisposable
|
||||
{
|
||||
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;
|
||||
|
||||
public ScreenshotCapture(VulkanContext context, Swapchain swapchain)
|
||||
{
|
||||
_context = context;
|
||||
_swapchain = swapchain;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request a screenshot to be captured on the next frame.
|
||||
/// </summary>
|
||||
public void Request(string outputPath)
|
||||
{
|
||||
_outputPath = outputPath;
|
||||
_requested = true;
|
||||
_ready = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if a screenshot has been requested but not yet saved.
|
||||
/// </summary>
|
||||
public bool IsRequested => _requested;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save the captured pixels to disk. Must be called after the command buffer containing the readback has finished.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
private void SavePixels(void* mappedData, uint width, uint height, uint rowPitch, Format format)
|
||||
{
|
||||
if (format == Format.B8G8R8A8Unorm || format == Format.B8G8R8A8Srgb)
|
||||
{
|
||||
SaveBgra(mappedData, width, height, rowPitch);
|
||||
return;
|
||||
}
|
||||
|
||||
if (format == Format.R8G8B8A8Unorm || format == Format.R8G8B8A8Srgb)
|
||||
{
|
||||
SaveRgba(mappedData, width, height, rowPitch);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new NotSupportedException($"Screenshot format {format} is not supported.");
|
||||
}
|
||||
|
||||
private void SaveBgra(void* mappedData, uint width, uint height, uint rowPitch)
|
||||
{
|
||||
using var image = new SixLabors.ImageSharp.Image<Rgba32>((int)width, (int)height);
|
||||
var src = (byte*)mappedData;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
image.SaveAsPng(_outputPath);
|
||||
}
|
||||
|
||||
private void SaveRgba(void* mappedData, uint width, uint height, uint rowPitch)
|
||||
{
|
||||
using var image = new SixLabors.ImageSharp.Image<Rgba32>((int)width, (int)height);
|
||||
var src = (byte*)mappedData;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
image.SaveAsPng(_outputPath);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Loads SPIR-V shader bytecode embedded in the assembly.
|
||||
/// </summary>
|
||||
public static class ShaderLoader
|
||||
{
|
||||
public static byte[] Load(string name)
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var resourceName = $"Engine.Graphics.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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -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;
|
||||
}
|
||||
@@ -1,446 +0,0 @@
|
||||
using System;
|
||||
using Silk.NET.Core;
|
||||
using Silk.NET.Vulkan;
|
||||
using Silk.NET.Vulkan.Extensions.KHR;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Manages the Vulkan swapchain, image views, render pass, and framebuffers.
|
||||
/// Uses Silk.NET.Vulkan.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,330 +0,0 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Silk.NET.Vulkan;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// A Vulkan texture: image, device memory, image view, and sampler.
|
||||
/// </summary>
|
||||
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<Rgba32>(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<CommandBuffer> 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);
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
using System;
|
||||
using Silk.NET.Vulkan;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// A host-visible, coherent Vulkan buffer for uniform data that is updated every frame.
|
||||
/// </summary>
|
||||
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<byte> 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);
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
using System;
|
||||
using Silk.NET.Core;
|
||||
using Silk.NET.Vulkan;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Interleaved vertex buffer: vec2 position + vec3 color.
|
||||
/// Uses Silk.NET.Vulkan.
|
||||
/// </summary>
|
||||
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<byte> 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<byte> 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<byte> 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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(Sdl3Window 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(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 = 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(Sdl3Window 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<PhysicalDevice>();
|
||||
|
||||
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<uint> { 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();
|
||||
}
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
using System;
|
||||
using Silk.NET.Core.Native;
|
||||
using Silk.NET.Vulkan;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Graphics pipeline for indexed meshes with push-constant MVP and depth testing.
|
||||
/// Uses Silk.NET.Vulkan.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user