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:
@@ -3,8 +3,12 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using Engine.AI;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
#if !RELEASE_AOT
|
||||
using Engine.AI.Mcp;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
#endif
|
||||
using Engine.Core;
|
||||
using Engine.Core.Components;
|
||||
@@ -22,6 +26,12 @@ class Program
|
||||
|
||||
try
|
||||
{
|
||||
if (args.Contains("--mcp-stdio"))
|
||||
{
|
||||
RunMcpStdioServer();
|
||||
return;
|
||||
}
|
||||
|
||||
using var world = World.Create();
|
||||
using var window = new Sdl3Window("Cortex Engine", 1280, 720);
|
||||
var timing = new Timing();
|
||||
@@ -37,16 +47,35 @@ class Program
|
||||
var queue = new AiCommandQueue(processor);
|
||||
|
||||
var cameraEntity = world.Entity("Camera")
|
||||
.Set(new Transform(new Vector3(0.0f, 2.5f, -4.0f), Quaternion.Identity, Vector3.One))
|
||||
.Set(new Camera(
|
||||
new Vector3(0.0f, 1.5f, -3.0f),
|
||||
Vector3.Zero,
|
||||
new Vector3(0.0f, 2.5f, -4.0f),
|
||||
new Vector3(0.0f, 0.5f, 0.0f),
|
||||
Vector3.UnitY,
|
||||
MathF.PI / 4.0f,
|
||||
1280.0f / 720.0f,
|
||||
0.1f,
|
||||
100.0f));
|
||||
|
||||
var orbit = new OrbitCameraController(cameraEntity, Vector3.Zero);
|
||||
world.Entity("MainLight")
|
||||
.Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f), Quaternion.Identity, Vector3.One))
|
||||
.Set(new Light(new Vector3(0.5f, -1.0f, -0.5f), new Vector3(1.0f, 0.95f, 0.8f), 1.0f));
|
||||
|
||||
world.Entity("FillLight")
|
||||
.Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f), Quaternion.Identity, Vector3.One))
|
||||
.Set(new Light(new Vector3(-0.8f, -0.6f, 0.3f), new Vector3(0.3f, 0.4f, 0.6f), 0.6f));
|
||||
|
||||
world.Entity("FrontLight")
|
||||
.Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f), Quaternion.Identity, Vector3.One))
|
||||
.Set(new Light(new Vector3(0.0f, -0.3f, -1.0f), new Vector3(0.8f, 0.8f, 0.9f), 0.4f));
|
||||
|
||||
world.Entity("GroundLight")
|
||||
.Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f), Quaternion.Identity, Vector3.One))
|
||||
.Set(new Light(new Vector3(0.0f, 1.0f, 0.0f), new Vector3(0.15f, 0.15f, 0.2f), 0.3f));
|
||||
|
||||
var orbit = new OrbitCameraController(cameraEntity, new Vector3(0.0f, 0.5f, 0.0f));
|
||||
|
||||
var texturePath = GenerateCheckerboardTexture("Content/checkerboard.png", 256);
|
||||
|
||||
var model = world.Entity("Model")
|
||||
.Set(new Transform(new Vector3(0.0f, 0.5f, 0.0f), Quaternion.Identity, new Vector3(0.5f)))
|
||||
@@ -55,8 +84,8 @@ class Program
|
||||
|
||||
var floor = world.Entity("Floor")
|
||||
.Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f), Quaternion.Identity, Vector3.One))
|
||||
.Set(CreateFloorMesh(20.0f, new Vector3(0.08f, 0.08f, 0.1f)))
|
||||
.Set(new Material(new Vector3(0.08f, 0.08f, 0.1f), roughness: 0.9f, metallic: 0.0f));
|
||||
.Set(CreateFloorMesh(20.0f, new Vector3(0.8f, 0.8f, 0.85f)))
|
||||
.Set(new Material(new Vector3(0.8f, 0.8f, 0.85f), roughness: 0.9f, metallic: 0.0f, texturePath: texturePath));
|
||||
|
||||
var grid = world.Entity("Grid")
|
||||
.Set(new Transform(new Vector3(0.0f, 0.01f, 0.0f), Quaternion.Identity, Vector3.One))
|
||||
@@ -74,24 +103,37 @@ class Program
|
||||
secondCube.Set(new Material(new Vector3(0.3f, 0.7f, 0.9f), roughness: 0.3f, metallic: 0.2f));
|
||||
|
||||
Console.WriteLine(processor.Process("""{ "type": "list_entities" }""").Message);
|
||||
Console.WriteLine(processor.Process("""{ "type": "get_world_state" }""").Message);
|
||||
Console.WriteLine(processor.Process("""{ "type": "capture_screenshot", "outputPath": "Screenshots/demo.png" }""").Message);
|
||||
|
||||
#if !RELEASE_AOT
|
||||
// Start the MCP server in the background so AI agents can connect via HTTP.
|
||||
var mcpApp = McpEngineServerHost.Create(args, queue, port: mcpPort);
|
||||
var mcpTask = mcpApp.RunAsync();
|
||||
_ = mcpTask.ContinueWith(t =>
|
||||
WebApplication? mcpApp = null;
|
||||
Task? mcpTask = null;
|
||||
|
||||
if (mcpPort > 0)
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
Console.WriteLine($"MCP server error: {t.Exception?.GetBaseException().Message}");
|
||||
else if (t.IsCanceled)
|
||||
Console.WriteLine("MCP server canceled.");
|
||||
else
|
||||
Console.WriteLine("MCP server stopped.");
|
||||
}, TaskScheduler.Default);
|
||||
Console.WriteLine($"MCP server starting on http://localhost:{mcpPort}");
|
||||
// Start the MCP server in the background so AI agents can connect via HTTP.
|
||||
mcpApp = McpEngineServerHost.Create(args, queue, port: mcpPort);
|
||||
mcpTask = mcpApp.RunAsync();
|
||||
_ = mcpTask.ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
Console.WriteLine($"MCP server error: {t.Exception?.GetBaseException().Message}");
|
||||
else if (t.IsCanceled)
|
||||
Console.WriteLine("MCP server canceled.");
|
||||
else
|
||||
Console.WriteLine("MCP server stopped.");
|
||||
}, TaskScheduler.Default);
|
||||
|
||||
Console.WriteLine($"MCP HTTP server listening on http://localhost:{mcpPort}/ (SSE)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("MCP server disabled (--mcp-port 0).");
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
var frames = 0;
|
||||
var lastFpsTime = 0.0;
|
||||
var lastWidth = window.Width;
|
||||
@@ -138,10 +180,10 @@ class Program
|
||||
|
||||
Console.WriteLine("Shutting down...");
|
||||
#if !RELEASE_AOT
|
||||
await mcpApp.StopAsync();
|
||||
await mcpTask;
|
||||
#else
|
||||
await Task.CompletedTask;
|
||||
if (mcpApp != null)
|
||||
await mcpApp.StopAsync();
|
||||
if (mcpTask != null)
|
||||
await mcpTask;
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -258,4 +300,35 @@ class Program
|
||||
|
||||
throw new FileNotFoundException("No model file found. Pass a .obj/.gltf/.glb path as argument or place Content/cube.obj next to the executable.");
|
||||
}
|
||||
|
||||
private static string GenerateCheckerboardTexture(string path, int size)
|
||||
{
|
||||
var tileSize = size / 8;
|
||||
using var image = new Image<Rgba32>(size, size);
|
||||
for (var y = 0; y < size; y++)
|
||||
{
|
||||
for (var x = 0; x < size; x++)
|
||||
{
|
||||
var tileX = x / tileSize;
|
||||
var tileY = y / tileSize;
|
||||
var isDark = (tileX + tileY) % 2 == 0;
|
||||
image[x, y] = isDark
|
||||
? new Rgba32(60, 60, 70, 255)
|
||||
: new Rgba32(160, 160, 170, 255);
|
||||
}
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
image.SaveAsPng(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
private static void RunMcpStdioServer()
|
||||
{
|
||||
Console.WriteLine("Starting headless stdio MCP server...");
|
||||
using var world = World.Create();
|
||||
var processor = new AiCommandProcessor(world, LoadModel, _ => { });
|
||||
var server = new Engine.AI.Stdio.McpStdioServer(processor);
|
||||
server.Run();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Engine.AI.Commands;
|
||||
@@ -55,6 +56,8 @@ public sealed class AiCommandProcessor
|
||||
DeleteEntityCommand c => DeleteEntity(c),
|
||||
ListEntitiesCommand => ListEntities(),
|
||||
CaptureScreenshotCommand c => CaptureScreenshot(c),
|
||||
GetWorldStateCommand => GetWorldState(),
|
||||
SetMaterialCommand c => SetMaterial(c),
|
||||
_ => AiCommandResult.Error($"Unknown command type: {command.Type}")
|
||||
};
|
||||
}
|
||||
@@ -132,4 +135,128 @@ public sealed class AiCommandProcessor
|
||||
_requestScreenshot(path);
|
||||
return AiCommandResult.Ok($"Screenshot requested: {path}");
|
||||
}
|
||||
|
||||
private AiCommandResult SetMaterial(SetMaterialCommand command)
|
||||
{
|
||||
var entity = _world.Lookup(command.Name);
|
||||
if ((ulong)entity.Id == 0)
|
||||
return AiCommandResult.Error($"Entity '{command.Name}' not found.");
|
||||
|
||||
ref var material = ref entity.Ensure<Material>();
|
||||
|
||||
if (command.Albedo.HasValue)
|
||||
material.Albedo = command.Albedo.Value;
|
||||
if (command.Roughness.HasValue)
|
||||
material.Roughness = command.Roughness.Value;
|
||||
if (command.Metallic.HasValue)
|
||||
material.Metallic = command.Metallic.Value;
|
||||
if (command.TexturePath is not null)
|
||||
material.TexturePath = command.TexturePath;
|
||||
|
||||
return AiCommandResult.Ok($"Updated material for entity '{command.Name}'.");
|
||||
}
|
||||
|
||||
private AiCommandResult GetWorldState()
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = false }))
|
||||
{
|
||||
writer.WriteStartArray();
|
||||
|
||||
_world.Each((Entity e, ref Transform _) =>
|
||||
{
|
||||
var name = e.Name();
|
||||
if (string.IsNullOrEmpty(name))
|
||||
return;
|
||||
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("name", name);
|
||||
writer.WriteNumber("id", (ulong)e.Id);
|
||||
|
||||
writer.WriteStartObject("components");
|
||||
|
||||
if (e.Has<Transform>())
|
||||
{
|
||||
ref var transform = ref e.Ensure<Transform>();
|
||||
writer.WriteStartObject("Transform");
|
||||
WriteVector3(writer, "position", transform.Position);
|
||||
WriteQuaternion(writer, "rotation", transform.Rotation);
|
||||
WriteVector3(writer, "scale", transform.Scale);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
if (e.Has<Camera>())
|
||||
{
|
||||
ref var camera = ref e.Ensure<Camera>();
|
||||
writer.WriteStartObject("Camera");
|
||||
writer.WriteNumber("fieldOfView", camera.FieldOfView);
|
||||
writer.WriteNumber("aspectRatio", camera.AspectRatio);
|
||||
writer.WriteNumber("nearPlane", camera.NearPlane);
|
||||
writer.WriteNumber("farPlane", camera.FarPlane);
|
||||
WriteVector3(writer, "position", camera.Position);
|
||||
WriteVector3(writer, "target", camera.Target);
|
||||
WriteVector3(writer, "up", camera.Up);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
if (e.Has<Material>())
|
||||
{
|
||||
ref var material = ref e.Ensure<Material>();
|
||||
writer.WriteStartObject("Material");
|
||||
WriteVector3(writer, "albedo", material.Albedo);
|
||||
writer.WriteNumber("roughness", material.Roughness);
|
||||
writer.WriteNumber("metallic", material.Metallic);
|
||||
if (material.HasTexture)
|
||||
writer.WriteString("texturePath", material.TexturePath);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
if (e.Has<Mesh>())
|
||||
{
|
||||
ref var mesh = ref e.Ensure<Mesh>();
|
||||
writer.WriteStartObject("Mesh");
|
||||
writer.WriteNumber("vertexCount", mesh.Vertices.Length);
|
||||
writer.WriteNumber("indexCount", mesh.Indices.Length);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
if (e.Has<Light>())
|
||||
{
|
||||
ref var light = ref e.Ensure<Light>();
|
||||
writer.WriteStartObject("Light");
|
||||
WriteVector3(writer, "direction", light.Direction);
|
||||
WriteVector3(writer, "color", light.Color);
|
||||
writer.WriteNumber("intensity", light.Intensity);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
writer.WriteEndObject();
|
||||
writer.WriteEndObject();
|
||||
});
|
||||
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
|
||||
var json = System.Text.Encoding.UTF8.GetString(stream.ToArray());
|
||||
return AiCommandResult.Ok(json);
|
||||
}
|
||||
|
||||
private static void WriteVector3(Utf8JsonWriter writer, string propertyName, Vector3 value)
|
||||
{
|
||||
writer.WriteStartArray(propertyName);
|
||||
writer.WriteNumberValue(value.X);
|
||||
writer.WriteNumberValue(value.Y);
|
||||
writer.WriteNumberValue(value.Z);
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
|
||||
private static void WriteQuaternion(Utf8JsonWriter writer, string propertyName, Quaternion value)
|
||||
{
|
||||
writer.WriteStartArray(propertyName);
|
||||
writer.WriteNumberValue(value.X);
|
||||
writer.WriteNumberValue(value.Y);
|
||||
writer.WriteNumberValue(value.Z);
|
||||
writer.WriteNumberValue(value.W);
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace Engine.AI.Commands;
|
||||
[JsonDerivedType(typeof(DeleteEntityCommand), "delete_entity")]
|
||||
[JsonDerivedType(typeof(ListEntitiesCommand), "list_entities")]
|
||||
[JsonDerivedType(typeof(CaptureScreenshotCommand), "capture_screenshot")]
|
||||
[JsonDerivedType(typeof(GetWorldStateCommand), "get_world_state")]
|
||||
[JsonDerivedType(typeof(SetMaterialCommand), "set_material")]
|
||||
public abstract record AiCommand
|
||||
{
|
||||
public string Type => GetType().Name.Replace("Command", "").ToLowerInvariant();
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Engine.AI.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Dump the current ECS world state as JSON for AI analysis.
|
||||
/// </summary>
|
||||
public sealed record GetWorldStateCommand : AiCommand;
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace Engine.AI.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Update the Material component of an existing entity by name.
|
||||
/// </summary>
|
||||
public sealed record SetMaterialCommand : AiCommand
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public Vector3? Albedo { get; init; }
|
||||
public float? Roughness { get; init; }
|
||||
public float? Metallic { get; init; }
|
||||
public string? TexturePath { get; init; }
|
||||
}
|
||||
@@ -77,6 +77,33 @@ public sealed class EngineMcpTools
|
||||
return EnqueueAndReturnMessage(cmd);
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Dump the current ECS world state as JSON, including Transform, Camera, Material, Light, and Mesh component summaries.")]
|
||||
public Task<string> GetWorldState()
|
||||
{
|
||||
var cmd = new GetWorldStateCommand();
|
||||
return EnqueueAndReturnMessage(cmd);
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Update the material of an existing entity by name.")]
|
||||
public Task<string> SetMaterial(
|
||||
string name,
|
||||
[Description("Optional albedo color as [r, g, b] (0-1)")] IReadOnlyList<double>? albedo = null,
|
||||
[Description("Optional roughness value (0-1)")] float? roughness = null,
|
||||
[Description("Optional metallic value (0-1)")] float? metallic = null,
|
||||
[Description("Optional path to a PNG texture file")] string? texturePath = null)
|
||||
{
|
||||
var cmd = new SetMaterialCommand
|
||||
{
|
||||
Name = name,
|
||||
Albedo = ToVector3(albedo),
|
||||
Roughness = roughness,
|
||||
Metallic = metallic,
|
||||
TexturePath = texturePath
|
||||
};
|
||||
|
||||
return EnqueueAndReturnMessage(cmd);
|
||||
}
|
||||
|
||||
private async Task<string> EnqueueAndReturnMessage(AiCommand command)
|
||||
{
|
||||
var result = await _queue.EnqueueAsync(command).ConfigureAwait(false);
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Engine.AI.Commands;
|
||||
|
||||
namespace Engine.AI.Stdio;
|
||||
|
||||
/// <summary>
|
||||
/// A minimal stdio MCP server that routes JSON-RPC requests to an <see cref="AiCommandProcessor"/>.
|
||||
/// This is suitable for clients like Claude Desktop that speak MCP over stdio.
|
||||
/// </summary>
|
||||
public sealed class McpStdioServer
|
||||
{
|
||||
private readonly AiCommandProcessor _processor;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private readonly Dictionary<string, ToolDefinition> _tools;
|
||||
|
||||
public McpStdioServer(AiCommandProcessor processor)
|
||||
{
|
||||
_processor = processor;
|
||||
_jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||||
_tools = new Dictionary<string, ToolDefinition>
|
||||
{
|
||||
["SpawnModel"] = new(
|
||||
"Spawn a 3D model entity in the engine world.",
|
||||
new JsonSchemaBuilder()
|
||||
.AddRequiredString("name")
|
||||
.AddRequiredString("modelPath")
|
||||
.AddOptionalArray("position", "number", 3)
|
||||
.AddOptionalArray("rotation", "number", 4)
|
||||
.AddOptionalArray("scale", "number", 3)
|
||||
.Build()),
|
||||
["SetTransform"] = new(
|
||||
"Update the transform of an existing entity by name.",
|
||||
new JsonSchemaBuilder()
|
||||
.AddRequiredString("name")
|
||||
.AddOptionalArray("position", "number", 3)
|
||||
.AddOptionalArray("rotation", "number", 4)
|
||||
.AddOptionalArray("scale", "number", 3)
|
||||
.Build()),
|
||||
["SetMaterial"] = new(
|
||||
"Update the material of an existing entity by name.",
|
||||
new JsonSchemaBuilder()
|
||||
.AddRequiredString("name")
|
||||
.AddOptionalArray("albedo", "number", 3)
|
||||
.AddOptionalNumber("roughness")
|
||||
.AddOptionalNumber("metallic")
|
||||
.AddOptionalString("texturePath")
|
||||
.Build()),
|
||||
["DeleteEntity"] = new(
|
||||
"Delete an entity by name.",
|
||||
new JsonSchemaBuilder()
|
||||
.AddRequiredString("name")
|
||||
.Build()),
|
||||
["ListEntities"] = new(
|
||||
"List all named entities in the ECS world.",
|
||||
new JsonSchemaBuilder().Build()),
|
||||
["CaptureScreenshot"] = new(
|
||||
"Capture a screenshot of the current rendered frame and save it to disk.",
|
||||
new JsonSchemaBuilder()
|
||||
.AddOptionalString("outputPath")
|
||||
.Build()),
|
||||
["GetWorldState"] = new(
|
||||
"Dump the current ECS world state as JSON.",
|
||||
new JsonSchemaBuilder().Build())
|
||||
};
|
||||
}
|
||||
|
||||
public void Run()
|
||||
{
|
||||
var input = Console.OpenStandardInput();
|
||||
var output = Console.OpenStandardOutput();
|
||||
using var reader = new StreamReader(input, Encoding.UTF8);
|
||||
using var writer = new StreamWriter(output, Encoding.UTF8) { AutoFlush = true };
|
||||
|
||||
while (true)
|
||||
{
|
||||
var line = reader.ReadLine();
|
||||
if (line == null)
|
||||
break;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
var response = HandleMessage(line);
|
||||
if (response != null)
|
||||
{
|
||||
writer.WriteLine(JsonSerializer.Serialize(response, _jsonOptions));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private JsonElement? HandleMessage(string line)
|
||||
{
|
||||
using var document = JsonDocument.Parse(line);
|
||||
var root = document.RootElement;
|
||||
|
||||
var method = root.GetProperty("method").GetString();
|
||||
var id = root.TryGetProperty("id", out var idProp) ? (JsonElement?)idProp : null;
|
||||
|
||||
switch (method)
|
||||
{
|
||||
case "initialize":
|
||||
return MakeResponse(id, new
|
||||
{
|
||||
protocolVersion = "2024-11-05",
|
||||
capabilities = new { tools = new { } },
|
||||
serverInfo = new { name = "CortexEngine", version = "0.1.0" }
|
||||
});
|
||||
|
||||
case "notifications/initialized":
|
||||
return null;
|
||||
|
||||
case "tools/list":
|
||||
return MakeResponse(id, new { tools = _tools.Select(t => new { type = "function", function = new { name = t.Key, description = t.Value.Description, parameters = t.Value.Parameters } }).ToList() });
|
||||
|
||||
case "tools/call":
|
||||
return HandleToolCall(id, root.GetProperty("params"));
|
||||
|
||||
case "ping":
|
||||
return MakeResponse(id, new { });
|
||||
|
||||
default:
|
||||
return MakeError(id, -32601, $"Method not found: {method}");
|
||||
}
|
||||
}
|
||||
|
||||
private JsonElement? HandleToolCall(JsonElement? id, JsonElement paramsElement)
|
||||
{
|
||||
var name = paramsElement.GetProperty("name").GetString();
|
||||
var arguments = paramsElement.GetProperty("arguments");
|
||||
|
||||
if (!_tools.TryGetValue(name ?? string.Empty, out _))
|
||||
return MakeError(id, -32601, $"Tool not found: {name}");
|
||||
|
||||
var command = BuildCommand(name!, arguments);
|
||||
var result = _processor.Process(command);
|
||||
|
||||
return MakeResponse(id, new { content = new[] { new { type = "text", text = result.Message } }, isError = !result.Success });
|
||||
}
|
||||
|
||||
private string BuildCommand(string toolName, JsonElement arguments)
|
||||
{
|
||||
var type = toolName switch
|
||||
{
|
||||
"SpawnModel" => "spawn_model",
|
||||
"SetTransform" => "set_transform",
|
||||
"SetMaterial" => "set_material",
|
||||
"DeleteEntity" => "delete_entity",
|
||||
"ListEntities" => "list_entities",
|
||||
"CaptureScreenshot" => "capture_screenshot",
|
||||
"GetWorldState" => "get_world_state",
|
||||
_ => toolName.ToLowerInvariant()
|
||||
};
|
||||
|
||||
var dict = new Dictionary<string, object> { ["type"] = type };
|
||||
foreach (var property in arguments.EnumerateObject())
|
||||
dict[property.Name] = ConvertArgument(property.Value);
|
||||
|
||||
return JsonSerializer.Serialize(dict, _jsonOptions);
|
||||
}
|
||||
|
||||
private static object ConvertArgument(JsonElement element)
|
||||
{
|
||||
return element.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => element.GetString()!,
|
||||
JsonValueKind.Number => element.GetDouble(),
|
||||
JsonValueKind.True => true,
|
||||
JsonValueKind.False => false,
|
||||
JsonValueKind.Array => element.EnumerateArray().Select(ConvertArgument).ToList(),
|
||||
JsonValueKind.Object => element.EnumerateObject().ToDictionary(p => p.Name, p => ConvertArgument(p.Value)),
|
||||
_ => element.GetRawText()
|
||||
};
|
||||
}
|
||||
|
||||
private JsonElement? MakeResponse(JsonElement? id, object result)
|
||||
{
|
||||
if (id == null)
|
||||
return null;
|
||||
|
||||
var json = JsonSerializer.Serialize(new { jsonrpc = "2.0", result, id = id.Value }, _jsonOptions);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
return doc.RootElement.Clone();
|
||||
}
|
||||
|
||||
private JsonElement? MakeError(JsonElement? id, int code, string message)
|
||||
{
|
||||
if (id == null)
|
||||
return null;
|
||||
|
||||
var json = JsonSerializer.Serialize(new { jsonrpc = "2.0", error = new { code, message }, id = id.Value }, _jsonOptions);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
return doc.RootElement.Clone();
|
||||
}
|
||||
|
||||
private sealed record ToolDefinition(string Description, JsonElement Parameters);
|
||||
|
||||
private sealed class JsonSchemaBuilder
|
||||
{
|
||||
private readonly Dictionary<string, object> _properties = new();
|
||||
private readonly List<string> _required = new();
|
||||
private readonly string _type = "object";
|
||||
|
||||
public JsonSchemaBuilder AddRequiredString(string name)
|
||||
{
|
||||
_properties[name] = new { type = "string" };
|
||||
_required.Add(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonSchemaBuilder AddOptionalString(string name)
|
||||
{
|
||||
_properties[name] = new { type = "string" };
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonSchemaBuilder AddOptionalNumber(string name)
|
||||
{
|
||||
_properties[name] = new { type = "number" };
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonSchemaBuilder AddOptionalArray(string name, string itemType, int? minItems = null)
|
||||
{
|
||||
_properties[name] = new { type = "array", items = new { type = itemType }, minItems };
|
||||
return this;
|
||||
}
|
||||
|
||||
public JsonElement Build()
|
||||
{
|
||||
var dict = new Dictionary<string, object>
|
||||
{
|
||||
["type"] = _type,
|
||||
["properties"] = _properties,
|
||||
["required"] = _required
|
||||
};
|
||||
var json = JsonSerializer.Serialize(dict);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
return doc.RootElement.Clone();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace Engine.Core.Components;
|
||||
|
||||
/// <summary>
|
||||
/// A directional light component for the ECS.
|
||||
/// </summary>
|
||||
public record struct Light
|
||||
{
|
||||
public Vector3 Direction;
|
||||
public Vector3 Color;
|
||||
public float Intensity;
|
||||
|
||||
public Light(Vector3 direction, Vector3 color, float intensity = 1.0f)
|
||||
{
|
||||
Direction = Vector3.Normalize(direction);
|
||||
Color = color;
|
||||
Intensity = intensity;
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,16 @@ public record struct Material
|
||||
public Vector3 Albedo;
|
||||
public float Roughness;
|
||||
public float Metallic;
|
||||
public string? TexturePath;
|
||||
|
||||
public Material(Vector3? albedo = null, float roughness = 0.5f, float metallic = 0.0f)
|
||||
public Material(Vector3? albedo = null, float roughness = 0.5f, float metallic = 0.0f, string? texturePath = null)
|
||||
{
|
||||
Albedo = albedo ?? new Vector3(0.7f, 0.6f, 0.5f);
|
||||
Roughness = roughness;
|
||||
Metallic = metallic;
|
||||
TexturePath = texturePath;
|
||||
}
|
||||
|
||||
public static Material Default => new(new Vector3(0.7f, 0.6f, 0.5f), 0.5f, 0.0f);
|
||||
public bool HasTexture => !string.IsNullOrEmpty(TexturePath);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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.
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user