Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ea5c3882e | ||
|
|
eec26d60f4 |
@@ -100,7 +100,7 @@ src/
|
||||
|
||||
### 3.1 Initialization
|
||||
|
||||
1. **Load `libvulkan.so.1`** via `NativeLibrary.Load()`
|
||||
1. **Load the platform Vulkan loader** via `NativeLibrary.Load()` (`vulkan-1.dll` on Windows, `libvulkan.so.1` on Linux)
|
||||
2. **`vkGetInstanceProcAddr`** — only directly-loaded function
|
||||
3. **Create instance** with SDL3 extensions + `VK_EXT_debug_utils` (if validation available)
|
||||
4. **Pick physical device** — prefer `DiscreteGpu`
|
||||
|
||||
@@ -16,7 +16,7 @@ AI-Native 3D game engine with pure P/Invoke Vulkan 1.3 render backend.
|
||||
- **ECS** — Flecs.NET with Transform, Mesh, Material, Light, Camera, RigidBody components
|
||||
- **SDL3 Window** — cross-platform, Vulkan surface
|
||||
|
||||
## Quick Start
|
||||
## Quick Start — Linux (main platform)
|
||||
|
||||
```bash
|
||||
# Build
|
||||
@@ -38,6 +38,33 @@ dotnet build CORTEX_ENGINE.sln -c Debug
|
||||
dotnet test tests/Engine.Tests/Engine.Tests.csproj -c Debug
|
||||
```
|
||||
|
||||
## Quick Start — Windows x64
|
||||
|
||||
PowerShell scripts select the Windows native runtime assets explicitly; Bash is
|
||||
not required:
|
||||
|
||||
```powershell
|
||||
# Build and restore Windows native dependencies
|
||||
dotnet restore .\CORTEX_ENGINE.sln -r win-x64
|
||||
dotnet build .\CORTEX_ENGINE.sln -c Debug -r win-x64 --no-restore
|
||||
|
||||
# Run the engine
|
||||
.\scripts\run.ps1
|
||||
|
||||
# Run a demo scene
|
||||
.\scripts\run.ps1 --scene shooter
|
||||
|
||||
# Run with AI/MCP server
|
||||
.\scripts\start_mcp_engine.ps1 -Port 5000
|
||||
|
||||
# Run tests
|
||||
dotnet test .\tests\Engine.Tests\Engine.Tests.csproj -c Debug -r win-x64
|
||||
```
|
||||
|
||||
The project keeps Linux as the default RID on Linux hosts and selects `win-x64`
|
||||
automatically on Windows. Use `-r win-x64` when cross-publishing from another
|
||||
OS.
|
||||
|
||||
## Controls
|
||||
|
||||
- **WASD** — move camera
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(ValueFromRemainingArguments = $true)]
|
||||
[string[]] $EngineArgs
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$engineDir = Split-Path -Parent $PSScriptRoot
|
||||
$project = Join-Path $engineDir 'src/CortexEngine.App/CortexEngine.App.csproj'
|
||||
|
||||
Push-Location $engineDir
|
||||
try {
|
||||
& dotnet run --project $project -c Debug -r win-x64 -- @EngineArgs
|
||||
$exitCode = $LASTEXITCODE
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
exit $exitCode
|
||||
@@ -0,0 +1,19 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[int] $Port = 5000
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$engineDir = Split-Path -Parent $PSScriptRoot
|
||||
$project = Join-Path $engineDir 'src/CortexEngine.App/CortexEngine.App.csproj'
|
||||
|
||||
Push-Location $engineDir
|
||||
try {
|
||||
& dotnet run --project $project -c Debug -r win-x64 -- --mcp-port $Port
|
||||
$exitCode = $LASTEXITCODE
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
exit $exitCode
|
||||
@@ -11,6 +11,15 @@
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
|
||||
<DefineConstants>DEV_MODE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Keep Linux as the default development platform while selecting the
|
||||
correct native runtime assets automatically on Windows. An explicit
|
||||
-r/-p:RuntimeIdentifier always takes precedence for cross-publishing. -->
|
||||
<PropertyGroup Condition="'$(RuntimeIdentifier)' == '' AND '$(OS)' == 'Windows_NT'">
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(RuntimeIdentifier)' == '' AND '$(OS)' != 'Windows_NT'">
|
||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
+179
-17
@@ -83,6 +83,8 @@ class Program
|
||||
|
||||
var lastWidth = window.Width;
|
||||
var lastHeight = window.Height;
|
||||
var lastDrawableWidth = window.DrawableWidth;
|
||||
var lastDrawableHeight = window.DrawableHeight;
|
||||
var frames = 0;
|
||||
var lastFpsTime = 0.0;
|
||||
var timing = new Timing();
|
||||
@@ -132,13 +134,17 @@ class Program
|
||||
}
|
||||
#endif
|
||||
|
||||
if (window.Width != lastWidth || window.Height != lastHeight)
|
||||
if (window.Width != lastWidth || window.Height != lastHeight ||
|
||||
window.DrawableWidth != lastDrawableWidth || window.DrawableHeight != lastDrawableHeight)
|
||||
{
|
||||
lastWidth = window.Width;
|
||||
lastHeight = window.Height;
|
||||
if (lastWidth > 0 && lastHeight > 0)
|
||||
lastDrawableWidth = window.DrawableWidth;
|
||||
lastDrawableHeight = window.DrawableHeight;
|
||||
if (lastWidth > 0 && lastHeight > 0 &&
|
||||
lastDrawableWidth > 0 && lastDrawableHeight > 0)
|
||||
{
|
||||
renderContext.Resize(lastWidth, lastHeight);
|
||||
renderContext.Resize(lastDrawableWidth, lastDrawableHeight);
|
||||
ref var cam = ref cameraEntity.Ensure<Camera>();
|
||||
cam.AspectRatio = (float)lastWidth / lastHeight;
|
||||
cameraEntity.Set(cam);
|
||||
@@ -215,14 +221,21 @@ class Program
|
||||
|
||||
if (hasImGui)
|
||||
{
|
||||
var displayWidth = Math.Max(window.Width, 1);
|
||||
var displayHeight = Math.Max(window.Height, 1);
|
||||
var drawableWidth = Math.Max(window.DrawableWidth, 1);
|
||||
var drawableHeight = Math.Max(window.DrawableHeight, 1);
|
||||
var framebufferScaleX = (float)drawableWidth / displayWidth;
|
||||
var framebufferScaleY = (float)drawableHeight / displayHeight;
|
||||
var io = ImGui.GetIO();
|
||||
io.DisplaySize = new System.Numerics.Vector2(window.Width, window.Height);
|
||||
io.MousePos = new System.Numerics.Vector2(input.MouseX, input.MouseY);
|
||||
io.MouseDown[0] = input.MouseLeft;
|
||||
io.MouseDown[1] = input.MouseRight;
|
||||
io.MouseDown[2] = input.MouseMiddle;
|
||||
UpdateImGuiInput(io, input);
|
||||
|
||||
renderer.BeginImGuiFrame();
|
||||
renderer.BeginImGuiFrame(
|
||||
displayWidth,
|
||||
displayHeight,
|
||||
framebufferScaleX,
|
||||
framebufferScaleY,
|
||||
(float)timing.DeltaTime);
|
||||
|
||||
ImGui.Begin("Cortex Engine Debug");
|
||||
ImGui.Text($"FPS: {frames}");
|
||||
@@ -388,7 +401,7 @@ class Program
|
||||
}
|
||||
|
||||
var paramsText = sb.ToString();
|
||||
ImGui.SetClipboardText(paramsText);
|
||||
window.SetClipboardText(paramsText);
|
||||
Console.WriteLine("[App] Parameters copied to clipboard:");
|
||||
Console.WriteLine(paramsText);
|
||||
}
|
||||
@@ -406,20 +419,41 @@ class Program
|
||||
recordingPath = $"Videos/cortex_{timestamp}.mp4";
|
||||
Directory.CreateDirectory("Videos");
|
||||
|
||||
var w = window.Width;
|
||||
var h = window.Height;
|
||||
var ffmpegArgs = $"-y -f rawvideo -pixel_format bgra -video_size {w}x{h} -framerate 60 -i - -c:v libx264 -preset fast -crf 23 -pix_fmt yuv420p -vf fps=30 {recordingPath}";
|
||||
var w = window.DrawableWidth;
|
||||
var h = window.DrawableHeight;
|
||||
var outputPath = recordingPath!;
|
||||
|
||||
// ArgumentList avoids shell-specific quoting rules and works
|
||||
// for paths on both Windows and Linux.
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "ffmpeg",
|
||||
Arguments = ffmpegArgs,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
psi.ArgumentList.Add("-y");
|
||||
psi.ArgumentList.Add("-f");
|
||||
psi.ArgumentList.Add("rawvideo");
|
||||
psi.ArgumentList.Add("-pixel_format");
|
||||
psi.ArgumentList.Add("bgra");
|
||||
psi.ArgumentList.Add("-video_size");
|
||||
psi.ArgumentList.Add($"{w}x{h}");
|
||||
psi.ArgumentList.Add("-framerate");
|
||||
psi.ArgumentList.Add("60");
|
||||
psi.ArgumentList.Add("-i");
|
||||
psi.ArgumentList.Add("-");
|
||||
psi.ArgumentList.Add("-c:v");
|
||||
psi.ArgumentList.Add("libx264");
|
||||
psi.ArgumentList.Add("-preset");
|
||||
psi.ArgumentList.Add("fast");
|
||||
psi.ArgumentList.Add("-crf");
|
||||
psi.ArgumentList.Add("23");
|
||||
psi.ArgumentList.Add("-pix_fmt");
|
||||
psi.ArgumentList.Add("yuv420p");
|
||||
psi.ArgumentList.Add("-vf");
|
||||
psi.ArgumentList.Add("fps=30");
|
||||
psi.ArgumentList.Add(outputPath);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -431,7 +465,7 @@ class Program
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[App] Failed to start FFmpeg: {ex.Message}");
|
||||
Console.WriteLine("[App] Install FFmpeg: sudo apt install ffmpeg");
|
||||
Console.WriteLine("[App] Install FFmpeg and ensure the ffmpeg executable is available on PATH.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -519,6 +553,134 @@ class Program
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static void UpdateImGuiInput(ImGuiIOPtr io, IInputState input)
|
||||
{
|
||||
io.AddMousePosEvent(input.MouseX, input.MouseY);
|
||||
|
||||
foreach (var inputEvent in input.Events)
|
||||
{
|
||||
switch (inputEvent.Type)
|
||||
{
|
||||
case InputEventType.KeyDown:
|
||||
case InputEventType.KeyUp:
|
||||
if (TryMapImGuiKey(inputEvent.Key, out var imguiKey))
|
||||
io.AddKeyEvent(imguiKey, inputEvent.Type == InputEventType.KeyDown);
|
||||
break;
|
||||
|
||||
case InputEventType.TextInput:
|
||||
if (!string.IsNullOrEmpty(inputEvent.Text))
|
||||
io.AddInputCharactersUTF8(inputEvent.Text);
|
||||
break;
|
||||
|
||||
case InputEventType.MouseButtonDown:
|
||||
case InputEventType.MouseButtonUp:
|
||||
io.AddMouseButtonEvent(
|
||||
inputEvent.MouseButton,
|
||||
inputEvent.Type == InputEventType.MouseButtonDown);
|
||||
break;
|
||||
|
||||
case InputEventType.MouseWheel:
|
||||
io.AddMouseWheelEvent(inputEvent.WheelX, inputEvent.WheelY);
|
||||
break;
|
||||
|
||||
case InputEventType.FocusGained:
|
||||
io.AddFocusEvent(true);
|
||||
break;
|
||||
|
||||
case InputEventType.FocusLost:
|
||||
io.AddFocusEvent(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ImGui uses separate virtual modifier keys for shortcuts and navigation.
|
||||
io.AddKeyEvent(ImGuiKey.ModCtrl,
|
||||
input.IsKeyDown(Key.LeftControl) || input.IsKeyDown(Key.RightControl));
|
||||
io.AddKeyEvent(ImGuiKey.ModShift,
|
||||
input.IsKeyDown(Key.LeftShift) || input.IsKeyDown(Key.RightShift));
|
||||
io.AddKeyEvent(ImGuiKey.ModAlt,
|
||||
input.IsKeyDown(Key.LeftAlt) || input.IsKeyDown(Key.RightAlt));
|
||||
}
|
||||
|
||||
private static bool TryMapImGuiKey(Key key, out ImGuiKey imguiKey)
|
||||
{
|
||||
imguiKey = key switch
|
||||
{
|
||||
Key.Space => ImGuiKey.Space,
|
||||
Key.Escape => ImGuiKey.Escape,
|
||||
Key.Enter => ImGuiKey.Enter,
|
||||
Key.Tab => ImGuiKey.Tab,
|
||||
Key.Backspace => ImGuiKey.Backspace,
|
||||
Key.Insert => ImGuiKey.Insert,
|
||||
Key.Delete => ImGuiKey.Delete,
|
||||
Key.Home => ImGuiKey.Home,
|
||||
Key.End => ImGuiKey.End,
|
||||
Key.PageUp => ImGuiKey.PageUp,
|
||||
Key.PageDown => ImGuiKey.PageDown,
|
||||
Key.Left => ImGuiKey.LeftArrow,
|
||||
Key.Right => ImGuiKey.RightArrow,
|
||||
Key.Up => ImGuiKey.UpArrow,
|
||||
Key.Down => ImGuiKey.DownArrow,
|
||||
Key.A => ImGuiKey.A,
|
||||
Key.B => ImGuiKey.B,
|
||||
Key.C => ImGuiKey.C,
|
||||
Key.D => ImGuiKey.D,
|
||||
Key.E => ImGuiKey.E,
|
||||
Key.F => ImGuiKey.F,
|
||||
Key.G => ImGuiKey.G,
|
||||
Key.H => ImGuiKey.H,
|
||||
Key.I => ImGuiKey.I,
|
||||
Key.J => ImGuiKey.J,
|
||||
Key.K => ImGuiKey.K,
|
||||
Key.L => ImGuiKey.L,
|
||||
Key.M => ImGuiKey.M,
|
||||
Key.N => ImGuiKey.N,
|
||||
Key.O => ImGuiKey.O,
|
||||
Key.P => ImGuiKey.P,
|
||||
Key.Q => ImGuiKey.Q,
|
||||
Key.R => ImGuiKey.R,
|
||||
Key.S => ImGuiKey.S,
|
||||
Key.T => ImGuiKey.T,
|
||||
Key.U => ImGuiKey.U,
|
||||
Key.V => ImGuiKey.V,
|
||||
Key.W => ImGuiKey.W,
|
||||
Key.X => ImGuiKey.X,
|
||||
Key.Y => ImGuiKey.Y,
|
||||
Key.Z => ImGuiKey.Z,
|
||||
Key.Zero => ImGuiKey._0,
|
||||
Key.One => ImGuiKey._1,
|
||||
Key.Two => ImGuiKey._2,
|
||||
Key.Three => ImGuiKey._3,
|
||||
Key.Four => ImGuiKey._4,
|
||||
Key.Five => ImGuiKey._5,
|
||||
Key.Six => ImGuiKey._6,
|
||||
Key.Seven => ImGuiKey._7,
|
||||
Key.Eight => ImGuiKey._8,
|
||||
Key.Nine => ImGuiKey._9,
|
||||
Key.F1 => ImGuiKey.F1,
|
||||
Key.F2 => ImGuiKey.F2,
|
||||
Key.F3 => ImGuiKey.F3,
|
||||
Key.F4 => ImGuiKey.F4,
|
||||
Key.F5 => ImGuiKey.F5,
|
||||
Key.F6 => ImGuiKey.F6,
|
||||
Key.F7 => ImGuiKey.F7,
|
||||
Key.F8 => ImGuiKey.F8,
|
||||
Key.F9 => ImGuiKey.F9,
|
||||
Key.F10 => ImGuiKey.F10,
|
||||
Key.F11 => ImGuiKey.F11,
|
||||
Key.F12 => ImGuiKey.F12,
|
||||
Key.LeftShift => ImGuiKey.LeftShift,
|
||||
Key.RightShift => ImGuiKey.RightShift,
|
||||
Key.LeftControl => ImGuiKey.LeftCtrl,
|
||||
Key.RightControl => ImGuiKey.RightCtrl,
|
||||
Key.LeftAlt => ImGuiKey.LeftAlt,
|
||||
Key.RightAlt => ImGuiKey.RightAlt,
|
||||
_ => ImGuiKey.None,
|
||||
};
|
||||
|
||||
return imguiKey != ImGuiKey.None;
|
||||
}
|
||||
|
||||
static int ParseMcpPort(string[] args)
|
||||
{
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
|
||||
@@ -13,6 +13,12 @@ public interface IInputState
|
||||
bool MouseMiddle { get; }
|
||||
float MouseWheelDelta { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Events received since the last <see cref="BeginFrame"/> call.
|
||||
/// Backends that do not expose an event queue may return an empty list.
|
||||
/// </summary>
|
||||
IReadOnlyList<InputEvent> Events => Array.Empty<InputEvent>();
|
||||
|
||||
bool IsKeyDown(Key key);
|
||||
bool IsKeyPressed(Key key);
|
||||
bool IsKeyReleased(Key key);
|
||||
|
||||
@@ -9,6 +9,14 @@ public interface IWindow : IDisposable
|
||||
{
|
||||
int Width { get; }
|
||||
int Height { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Size of the drawable Vulkan framebuffer in physical pixels. This can
|
||||
/// differ from <see cref="Width"/>/<see cref="Height"/> on high-DPI displays.
|
||||
/// </summary>
|
||||
int DrawableWidth => Width;
|
||||
int DrawableHeight => Height;
|
||||
|
||||
bool ShouldClose { get; }
|
||||
|
||||
/// <summary>
|
||||
@@ -27,6 +35,11 @@ public interface IWindow : IDisposable
|
||||
/// </summary>
|
||||
void Close();
|
||||
|
||||
/// <summary>
|
||||
/// Places UTF-8 text on the platform clipboard when supported.
|
||||
/// </summary>
|
||||
void SetClipboardText(string text) { }
|
||||
|
||||
/// <summary>
|
||||
/// Native window handle (e.g. <c>SDL_Window*</c>). Used by backends that need
|
||||
/// the raw OS handle for surface creation. Returns 0 if not applicable.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace Engine.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Backend-agnostic input event captured during the current frame.
|
||||
/// </summary>
|
||||
public readonly record struct InputEvent(
|
||||
InputEventType Type,
|
||||
Key Key = Key.Unknown,
|
||||
bool IsRepeat = false,
|
||||
int MouseButton = -1,
|
||||
float WheelX = 0,
|
||||
float WheelY = 0,
|
||||
string? Text = null);
|
||||
|
||||
public enum InputEventType
|
||||
{
|
||||
KeyDown,
|
||||
KeyUp,
|
||||
TextInput,
|
||||
MouseButtonDown,
|
||||
MouseButtonUp,
|
||||
MouseWheel,
|
||||
FocusGained,
|
||||
FocusLost,
|
||||
}
|
||||
@@ -7,11 +7,12 @@ namespace Engine.Core;
|
||||
/// SDL3-backed implementation of <see cref="IInputState"/>.
|
||||
/// Populated by polling SDL events via <see cref="ProcessEvent"/> once per frame.
|
||||
/// </summary>
|
||||
public sealed class InputMapping : IInputState
|
||||
public sealed unsafe class InputMapping : IInputState
|
||||
{
|
||||
private readonly HashSet<Key> _keysPressed = new();
|
||||
private readonly HashSet<Key> _keysDown = new();
|
||||
private readonly HashSet<Key> _keysReleased = new();
|
||||
private readonly List<InputEvent> _events = new();
|
||||
|
||||
public int MouseX { get; private set; }
|
||||
public int MouseY { get; private set; }
|
||||
@@ -19,11 +20,13 @@ public sealed class InputMapping : IInputState
|
||||
public bool MouseRight { get; private set; }
|
||||
public bool MouseMiddle { get; private set; }
|
||||
public float MouseWheelDelta { get; private set; }
|
||||
public IReadOnlyList<InputEvent> Events => _events;
|
||||
|
||||
public void BeginFrame()
|
||||
{
|
||||
_keysPressed.Clear();
|
||||
_keysReleased.Clear();
|
||||
_events.Clear();
|
||||
MouseWheelDelta = 0;
|
||||
}
|
||||
|
||||
@@ -38,6 +41,10 @@ public sealed class InputMapping : IInputState
|
||||
if (!_keysDown.Contains(key))
|
||||
_keysPressed.Add(key);
|
||||
_keysDown.Add(key);
|
||||
_events.Add(new InputEvent(
|
||||
InputEventType.KeyDown,
|
||||
Key: key,
|
||||
IsRepeat: evt.key.repeat));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -47,6 +54,7 @@ public sealed class InputMapping : IInputState
|
||||
if (key == Key.Unknown) break;
|
||||
_keysDown.Remove(key);
|
||||
_keysReleased.Add(key);
|
||||
_events.Add(new InputEvent(InputEventType.KeyUp, Key: key));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -57,14 +65,46 @@ public sealed class InputMapping : IInputState
|
||||
|
||||
case SDL_EventType.SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||
SetMouseButton(evt.button.button, true);
|
||||
if (evt.button.button is >= 1 and <= 3)
|
||||
_events.Add(new InputEvent(
|
||||
InputEventType.MouseButtonDown,
|
||||
MouseButton: evt.button.button - 1));
|
||||
break;
|
||||
|
||||
case SDL_EventType.SDL_EVENT_MOUSE_BUTTON_UP:
|
||||
SetMouseButton(evt.button.button, false);
|
||||
if (evt.button.button is >= 1 and <= 3)
|
||||
_events.Add(new InputEvent(
|
||||
InputEventType.MouseButtonUp,
|
||||
MouseButton: evt.button.button - 1));
|
||||
break;
|
||||
|
||||
case SDL_EventType.SDL_EVENT_MOUSE_WHEEL:
|
||||
MouseWheelDelta += evt.wheel.y;
|
||||
_events.Add(new InputEvent(
|
||||
InputEventType.MouseWheel,
|
||||
WheelX: evt.wheel.x,
|
||||
WheelY: evt.wheel.y));
|
||||
break;
|
||||
|
||||
case SDL_EventType.SDL_EVENT_TEXT_INPUT:
|
||||
{
|
||||
var text = SDL3.PtrToStringUTF8(evt.text.text);
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
_events.Add(new InputEvent(InputEventType.TextInput, Text: text));
|
||||
break;
|
||||
}
|
||||
|
||||
case SDL_EventType.SDL_EVENT_WINDOW_FOCUS_GAINED:
|
||||
_events.Add(new InputEvent(InputEventType.FocusGained));
|
||||
break;
|
||||
|
||||
case SDL_EventType.SDL_EVENT_WINDOW_FOCUS_LOST:
|
||||
_keysDown.Clear();
|
||||
MouseLeft = false;
|
||||
MouseRight = false;
|
||||
MouseMiddle = false;
|
||||
_events.Add(new InputEvent(InputEventType.FocusLost));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,16 +18,25 @@ public sealed unsafe class Sdl3Window : IWindow
|
||||
|
||||
public int Width { get; private set; }
|
||||
public int Height { get; private set; }
|
||||
public int DrawableWidth { get; private set; }
|
||||
public int DrawableHeight { get; private set; }
|
||||
public bool ShouldClose { get; private set; }
|
||||
public IInputState Input => _input;
|
||||
public nint Handle => (nint)_window;
|
||||
|
||||
public void Close() => ShouldClose = true;
|
||||
|
||||
public void SetClipboardText(string text)
|
||||
{
|
||||
SDL3.SDL_SetClipboardText(text);
|
||||
}
|
||||
|
||||
public Sdl3Window(string title, int width, int height, bool vulkanSurface = true)
|
||||
{
|
||||
Width = width;
|
||||
Height = height;
|
||||
DrawableWidth = width;
|
||||
DrawableHeight = height;
|
||||
|
||||
if (!SDL3.SDL_Init(SDL_InitFlags.SDL_INIT_VIDEO))
|
||||
throw new InvalidOperationException($"SDL_Init failed: {SDL3.SDL_GetError()}");
|
||||
@@ -49,6 +58,8 @@ public sealed unsafe class Sdl3Window : IWindow
|
||||
|
||||
SDL3.SDL_ShowWindow(_window);
|
||||
SDL3.SDL_RaiseWindow(_window);
|
||||
UpdateDrawableSize();
|
||||
SDL3.SDL_StartTextInput(_window);
|
||||
|
||||
// On Wayland, the compositor needs an event round-trip before mapping
|
||||
// the window. Pump events to flush the show request without blocking.
|
||||
@@ -80,6 +91,16 @@ public sealed unsafe class Sdl3Window : IWindow
|
||||
case SDL_EventType.SDL_EVENT_WINDOW_RESIZED:
|
||||
Width = evt.window.data1;
|
||||
Height = evt.window.data2;
|
||||
UpdateDrawableSize();
|
||||
break;
|
||||
|
||||
case SDL_EventType.SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
|
||||
DrawableWidth = evt.window.data1;
|
||||
DrawableHeight = evt.window.data2;
|
||||
break;
|
||||
|
||||
case SDL_EventType.SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED:
|
||||
UpdateDrawableSize();
|
||||
break;
|
||||
|
||||
case SDL_EventType.SDL_EVENT_KEY_DOWN:
|
||||
@@ -104,11 +125,25 @@ public sealed unsafe class Sdl3Window : IWindow
|
||||
return result;
|
||||
}
|
||||
|
||||
private void UpdateDrawableSize()
|
||||
{
|
||||
if (_window == null) return;
|
||||
|
||||
var pixelWidth = Width;
|
||||
var pixelHeight = Height;
|
||||
if (SDL3.SDL_GetWindowSizeInPixels(_window, &pixelWidth, &pixelHeight))
|
||||
{
|
||||
DrawableWidth = pixelWidth;
|
||||
DrawableHeight = pixelHeight;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
SDL3.SDL_StopTextInput(_window);
|
||||
SDL3.SDL_DestroyWindow(_window);
|
||||
SDL3.SDL_Quit();
|
||||
}
|
||||
|
||||
@@ -447,11 +447,13 @@ internal sealed unsafe class VulkanImGui : IDisposable
|
||||
_indexMemory = im;
|
||||
}
|
||||
|
||||
public void NewFrame()
|
||||
public void NewFrame(float displayWidth, float displayHeight,
|
||||
float framebufferScaleX, float framebufferScaleY, float deltaTime)
|
||||
{
|
||||
var io = ImGui.GetIO();
|
||||
io.DisplaySize = new System.Numerics.Vector2(1280, 720);
|
||||
io.DeltaTime = 0.016f;
|
||||
io.DisplaySize = new Vector2(displayWidth, displayHeight);
|
||||
io.DisplayFramebufferScale = new Vector2(framebufferScaleX, framebufferScaleY);
|
||||
io.DeltaTime = MathF.Max(deltaTime, 1f / 1000f);
|
||||
ImGui.NewFrame();
|
||||
}
|
||||
|
||||
@@ -520,13 +522,21 @@ internal sealed unsafe class VulkanImGui : IDisposable
|
||||
{
|
||||
var pcmd = list.CmdBuffer[i];
|
||||
var clip = pcmd.ClipRect;
|
||||
var framebufferScale = io.DisplayFramebufferScale;
|
||||
var clipMinX = MathF.Max(clip.X * framebufferScale.X, 0f);
|
||||
var clipMinY = MathF.Max(clip.Y * framebufferScale.Y, 0f);
|
||||
var clipMaxX = MathF.Min(clip.Z * framebufferScale.X, width);
|
||||
var clipMaxY = MathF.Min(clip.W * framebufferScale.Y, height);
|
||||
if (clipMaxX <= clipMinX || clipMaxY <= clipMinY)
|
||||
continue;
|
||||
|
||||
var scissor = new VkRect2D
|
||||
{
|
||||
Offset = new VkOffset2D { X = (int)clip.X, Y = (int)clip.Y },
|
||||
Offset = new VkOffset2D { X = (int)clipMinX, Y = (int)clipMinY },
|
||||
Extent = new VkExtent2D
|
||||
{
|
||||
Width = (uint)(clip.Z - clip.X),
|
||||
Height = (uint)(clip.W - clip.Y),
|
||||
Width = (uint)(clipMaxX - clipMinX),
|
||||
Height = (uint)(clipMaxY - clipMinY),
|
||||
},
|
||||
};
|
||||
Vk.vkCmdSetScissor(cmd, 0, 1, &scissor);
|
||||
|
||||
@@ -23,7 +23,7 @@ internal sealed class VulkanRenderContext : IRenderContext{
|
||||
};
|
||||
|
||||
_swapchain = new VulkanSwapchain(_ctx.Device, _ctx.PhysicalDevice, _ctx.Surface,
|
||||
surfaceFormat, window.Width, window.Height, _ctx);
|
||||
surfaceFormat, window.DrawableWidth, window.DrawableHeight, _ctx);
|
||||
}
|
||||
|
||||
public IRenderer CreateRenderer()
|
||||
|
||||
@@ -179,9 +179,11 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh
|
||||
return result;
|
||||
}
|
||||
|
||||
public void BeginImGuiFrame()
|
||||
public void BeginImGuiFrame(float displayWidth, float displayHeight,
|
||||
float framebufferScaleX, float framebufferScaleY, float deltaTime)
|
||||
{
|
||||
_imGui?.NewFrame();
|
||||
_imGui?.NewFrame(displayWidth, displayHeight,
|
||||
framebufferScaleX, framebufferScaleY, deltaTime);
|
||||
}
|
||||
|
||||
public void EndImGuiFrame()
|
||||
@@ -296,7 +298,7 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh
|
||||
|
||||
// A minimized window has no drawable surface. Keep the frame fence signaled and wait
|
||||
// for the window event loop to provide a non-zero extent before trying to acquire.
|
||||
if (_window.Width <= 0 || _window.Height <= 0)
|
||||
if (_window.DrawableWidth <= 0 || _window.DrawableHeight <= 0)
|
||||
return;
|
||||
|
||||
// Read captured frame from previous render (GPU has finished by now)
|
||||
@@ -324,7 +326,7 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh
|
||||
// from this frame would leave the next WaitForFrame blocked forever.
|
||||
if (acquireResult == VkResult.ErrorOutOfDateKHR)
|
||||
{
|
||||
RecreateSwapchain(_window.Width, _window.Height);
|
||||
RecreateSwapchain(_window.DrawableWidth, _window.DrawableHeight);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -657,7 +659,7 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh
|
||||
|
||||
if (presentResult == VkResult.ErrorOutOfDateKHR || presentResult == VkResult.SuboptimalKHR)
|
||||
{
|
||||
RecreateSwapchain(_window.Width, _window.Height);
|
||||
RecreateSwapchain(_window.DrawableWidth, _window.DrawableHeight);
|
||||
}
|
||||
|
||||
_frameIndex = (_frameIndex + 1) % VulkanFrameResources.MaxFramesInFlight;
|
||||
|
||||
@@ -17,8 +17,11 @@ public interface IRenderer : IDisposable
|
||||
/// <summary>
|
||||
/// Called before RenderWorld to begin a new ImGui frame.
|
||||
/// Null if the backend doesn't support ImGui.
|
||||
/// <paramref name="displayWidth"/> and <paramref name="displayHeight"/> are
|
||||
/// logical window coordinates; framebuffer scale maps them to pixels.
|
||||
/// </summary>
|
||||
void BeginImGuiFrame() { }
|
||||
void BeginImGuiFrame(float displayWidth, float displayHeight,
|
||||
float framebufferScaleX, float framebufferScaleY, float deltaTime) { }
|
||||
|
||||
/// <summary>
|
||||
/// Called after RenderWorld to render ImGui draw data.
|
||||
|
||||
Reference in New Issue
Block a user