feat: world state, multiple lights, textures, stdio MCP, Claude config

- Add get_world_state AI command that dumps ECS entities with Transform,
  Camera, Material, Light, and Mesh summaries.
- Add set_material AI command to update albedo/roughness/metallic/texture.
- Expose both commands as MCP HTTP tools and stdio tools.
- Add Light component and support up to 4 directional lights via a Vulkan
  uniform buffer (descriptor set 0) with std140 layout.
- Move per-frame lighting/camera data into the uniform buffer; push constants
  now carry only MVP + material properties (96 bytes).
- Add Texture class for PNG loading and Vulkan image/view/sampler creation.
- Add per-entity combined image sampler descriptor set (set 1) and use it
  for albedo texture sampling in the fragment shader.
- Generate a checkerboard floor texture in Program.cs.
- Add McpStdioServer for headless stdio MCP (Claude Desktop compatible).
- Add claude_desktop_config.json and scripts/start_mcp_engine.sh.
- Update CORTEX_ENGINE_ARCHITECTURE.md with runtime notes, CLI arguments,
  Vulkan pipeline details, and MCP client configuration.
- All configs (Debug/Release/ReleaseAOT) build successfully.
This commit is contained in:
emil28092005
2026-06-16 21:07:09 +03:00
parent 751e403c0c
commit 6d3b5cca37
23 changed files with 1577 additions and 79 deletions
+297 -12
View File
@@ -3,6 +3,8 @@ 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;
@@ -20,6 +22,13 @@ public sealed unsafe class MeshRenderer : IDisposable
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!;
@@ -28,18 +37,38 @@ public sealed unsafe class MeshRenderer : IDisposable
private Silk.NET.Vulkan.Fence[] _inFlightFences = null!;
private int _currentFrame;
[StructLayout(LayoutKind.Sequential)]
[StructLayout(LayoutKind.Sequential, Size = 96)]
private struct PushConstants
{
public Matrix4x4 Mvp;
public Vector3 LightDirection;
public float Pad1;
public Vector3 LightColor;
public float Pad2;
public Vector3 AmbientColor;
public float Pad3;
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 float Pad4;
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
@@ -67,6 +96,11 @@ public sealed unsafe class MeshRenderer : IDisposable
_screenshot = new ScreenshotCapture(context, swapchain);
_pipeline = new VulkanPipeline(context, swapchain);
_frameConstantsBuffer = new UniformBuffer(context, (ulong)sizeof(FrameConstants));
CreateFrameDescriptorPool();
CreateFrameDescriptorSet();
CreateTextureDescriptorPool();
CreateDefaultTexture();
CreateCommandPool();
CreateCommandBuffers();
CreateSyncObjects();
@@ -135,6 +169,120 @@ public sealed unsafe class MeshRenderer : IDisposable
}
}
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;
@@ -184,6 +332,8 @@ public sealed unsafe class MeshRenderer : IDisposable
_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);
@@ -195,6 +345,14 @@ public sealed unsafe class MeshRenderer : IDisposable
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))
@@ -208,14 +366,20 @@ public sealed unsafe class MeshRenderer : IDisposable
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,
LightDirection = new Vector3(0.5f, -1.0f, -0.5f),
LightColor = new Vector3(1.0f, 0.95f, 0.8f),
AmbientColor = new Vector3(0.15f, 0.15f, 0.2f),
CameraPosition = camera.Position
MaterialAlbedo = material.Albedo,
MaterialRoughness = material.Roughness,
MaterialMetallic = material.Metallic,
UseTexture = material.HasTexture ? 1u : 0u,
TextureIndex = 0,
Pad0 = 0
};
var pushSize = (uint)sizeof(PushConstants);
@@ -335,6 +499,117 @@ public sealed unsafe class MeshRenderer : IDisposable
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(
@@ -374,6 +649,16 @@ public sealed unsafe class MeshRenderer : IDisposable
}
_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();
}
+48 -17
View File
@@ -3,36 +3,67 @@
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 lightDirection;
float pad1;
vec3 lightColor;
float pad2;
vec3 ambientColor;
float pad3;
vec3 cameraPosition;
float pad4;
vec3 materialAlbedo;
float materialRoughness;
float materialMetallic;
uint useTexture;
uint textureIndex;
uint _pad0;
uint _pad1;
} push;
void main()
{
vec3 normal = normalize(fragNormal);
vec3 lightDir = normalize(-push.lightDirection);
vec3 viewDir = normalize(push.cameraPosition - fragWorldPos);
vec3 halfDir = normalize(lightDir + viewDir);
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);
float diff = max(dot(normal, lightDir), 0.0);
float spec = pow(max(dot(normal, halfDir), 0.0), 64.0) * 0.5;
vec3 result = frame.ambientColor * albedo;
vec3 diffuse = push.lightColor * diff;
vec3 specular = push.lightColor * spec;
vec3 ambient = push.ambientColor;
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;
}
vec3 result = (ambient + diffuse + specular) * fragColor;
outColor = vec4(result, 1.0);
}
Binary file not shown.
Binary file not shown.
+26 -8
View File
@@ -7,18 +7,35 @@ 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 lightDirection;
float pad1;
vec3 lightColor;
float pad2;
vec3 ambientColor;
float pad3;
vec3 cameraPosition;
float pad4;
vec3 materialAlbedo;
float materialRoughness;
float materialMetallic;
uint useTexture;
uint textureIndex;
uint _pad0;
uint _pad1;
} push;
void main()
@@ -27,4 +44,5 @@ void main()
fragColor = inColor;
fragNormal = inNormal;
fragWorldPos = inPosition;
fragUv = inPosition.xz * 0.5 + 0.5;
}
+330
View File
@@ -0,0 +1,330 @@
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);
}
}
+110
View File
@@ -0,0 +1,110 @@
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);
}
}
+20
View File
@@ -31,6 +31,7 @@ public sealed unsafe class VulkanContext : IDisposable
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)
{
@@ -42,6 +43,23 @@ public sealed unsafe class VulkanContext : IDisposable
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)
@@ -290,6 +308,8 @@ public sealed unsafe class VulkanContext : IDisposable
_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)
+58 -2
View File
@@ -15,6 +15,8 @@ public sealed unsafe class VulkanPipeline : IDisposable
public Pipeline Handle { get; }
public PipelineLayout Layout { get; }
public DescriptorSetLayout FrameDescriptorSetLayout { get; }
public DescriptorSetLayout TextureDescriptorSetLayout { get; }
private readonly ShaderModule _vertexModule;
private readonly ShaderModule _fragmentModule;
@@ -26,6 +28,8 @@ public sealed unsafe class VulkanPipeline : IDisposable
_vertexModule = CreateShaderModule("vertex.spv");
_fragmentModule = CreateShaderModule("fragment.spv");
FrameDescriptorSetLayout = CreateFrameDescriptorSetLayout();
TextureDescriptorSetLayout = CreateTextureDescriptorSetLayout();
Layout = CreatePipelineLayout();
Handle = CreateGraphicsPipeline();
}
@@ -53,19 +57,69 @@ public sealed unsafe class VulkanPipeline : IDisposable
}
}
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 = (uint)(32 * sizeof(float))
Size = 96
};
var setLayouts = stackalloc DescriptorSetLayout[] { FrameDescriptorSetLayout, TextureDescriptorSetLayout };
var createInfo = new PipelineLayoutCreateInfo
{
SType = StructureType.PipelineLayoutCreateInfo,
SetLayoutCount = 0,
SetLayoutCount = 2,
PSetLayouts = setLayouts,
PushConstantRangeCount = 1,
PPushConstantRanges = &pushConstantRange
};
@@ -244,6 +298,8 @@ public sealed unsafe class VulkanPipeline : IDisposable
_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);
}