Compare commits

2 Commits
Author SHA1 Message Date
emil28092005 8ea5c3882e fix: wire complete ImGui window input 2026-08-26 17:04:37 +03:00
emil28092005 eec26d60f4 feat: add Windows runtime support 2026-08-26 16:50:15 +03:00
15 changed files with 404 additions and 33 deletions
+1 -1
View File
@@ -100,7 +100,7 @@ src/
### 3.1 Initialization ### 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 2. **`vkGetInstanceProcAddr`** — only directly-loaded function
3. **Create instance** with SDL3 extensions + `VK_EXT_debug_utils` (if validation available) 3. **Create instance** with SDL3 extensions + `VK_EXT_debug_utils` (if validation available)
4. **Pick physical device** — prefer `DiscreteGpu` 4. **Pick physical device** — prefer `DiscreteGpu`
+28 -1
View File
@@ -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 - **ECS** — Flecs.NET with Transform, Mesh, Material, Light, Camera, RigidBody components
- **SDL3 Window** — cross-platform, Vulkan surface - **SDL3 Window** — cross-platform, Vulkan surface
## Quick Start ## Quick Start — Linux (main platform)
```bash ```bash
# Build # Build
@@ -38,6 +38,33 @@ dotnet build CORTEX_ENGINE.sln -c Debug
dotnet test tests/Engine.Tests/Engine.Tests.csproj -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 ## Controls
- **WASD** — move camera - **WASD** — move camera
+20
View File
@@ -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
+19
View File
@@ -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'"> <PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<DefineConstants>DEV_MODE</DefineConstants> <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> <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
</PropertyGroup> </PropertyGroup>
+179 -17
View File
@@ -83,6 +83,8 @@ class Program
var lastWidth = window.Width; var lastWidth = window.Width;
var lastHeight = window.Height; var lastHeight = window.Height;
var lastDrawableWidth = window.DrawableWidth;
var lastDrawableHeight = window.DrawableHeight;
var frames = 0; var frames = 0;
var lastFpsTime = 0.0; var lastFpsTime = 0.0;
var timing = new Timing(); var timing = new Timing();
@@ -132,13 +134,17 @@ class Program
} }
#endif #endif
if (window.Width != lastWidth || window.Height != lastHeight) if (window.Width != lastWidth || window.Height != lastHeight ||
window.DrawableWidth != lastDrawableWidth || window.DrawableHeight != lastDrawableHeight)
{ {
lastWidth = window.Width; lastWidth = window.Width;
lastHeight = window.Height; 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>(); ref var cam = ref cameraEntity.Ensure<Camera>();
cam.AspectRatio = (float)lastWidth / lastHeight; cam.AspectRatio = (float)lastWidth / lastHeight;
cameraEntity.Set(cam); cameraEntity.Set(cam);
@@ -215,14 +221,21 @@ class Program
if (hasImGui) 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(); var io = ImGui.GetIO();
io.DisplaySize = new System.Numerics.Vector2(window.Width, window.Height); UpdateImGuiInput(io, input);
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;
renderer.BeginImGuiFrame(); renderer.BeginImGuiFrame(
displayWidth,
displayHeight,
framebufferScaleX,
framebufferScaleY,
(float)timing.DeltaTime);
ImGui.Begin("Cortex Engine Debug"); ImGui.Begin("Cortex Engine Debug");
ImGui.Text($"FPS: {frames}"); ImGui.Text($"FPS: {frames}");
@@ -388,7 +401,7 @@ class Program
} }
var paramsText = sb.ToString(); var paramsText = sb.ToString();
ImGui.SetClipboardText(paramsText); window.SetClipboardText(paramsText);
Console.WriteLine("[App] Parameters copied to clipboard:"); Console.WriteLine("[App] Parameters copied to clipboard:");
Console.WriteLine(paramsText); Console.WriteLine(paramsText);
} }
@@ -406,20 +419,41 @@ class Program
recordingPath = $"Videos/cortex_{timestamp}.mp4"; recordingPath = $"Videos/cortex_{timestamp}.mp4";
Directory.CreateDirectory("Videos"); Directory.CreateDirectory("Videos");
var w = window.Width; var w = window.DrawableWidth;
var h = window.Height; var h = window.DrawableHeight;
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 outputPath = recordingPath!;
// ArgumentList avoids shell-specific quoting rules and works
// for paths on both Windows and Linux.
var psi = new ProcessStartInfo var psi = new ProcessStartInfo
{ {
FileName = "ffmpeg", FileName = "ffmpeg",
Arguments = ffmpegArgs,
UseShellExecute = false, UseShellExecute = false,
RedirectStandardInput = true, RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = 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 try
{ {
@@ -431,7 +465,7 @@ class Program
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"[App] Failed to start FFmpeg: {ex.Message}"); 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; 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) static int ParseMcpPort(string[] args)
{ {
for (var i = 0; i < args.Length; i++) for (var i = 0; i < args.Length; i++)
+6
View File
@@ -13,6 +13,12 @@ public interface IInputState
bool MouseMiddle { get; } bool MouseMiddle { get; }
float MouseWheelDelta { 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 IsKeyDown(Key key);
bool IsKeyPressed(Key key); bool IsKeyPressed(Key key);
bool IsKeyReleased(Key key); bool IsKeyReleased(Key key);
+13
View File
@@ -9,6 +9,14 @@ public interface IWindow : IDisposable
{ {
int Width { get; } int Width { get; }
int Height { 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; } bool ShouldClose { get; }
/// <summary> /// <summary>
@@ -27,6 +35,11 @@ public interface IWindow : IDisposable
/// </summary> /// </summary>
void Close(); void Close();
/// <summary>
/// Places UTF-8 text on the platform clipboard when supported.
/// </summary>
void SetClipboardText(string text) { }
/// <summary> /// <summary>
/// Native window handle (e.g. <c>SDL_Window*</c>). Used by backends that need /// 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. /// the raw OS handle for surface creation. Returns 0 if not applicable.
+25
View File
@@ -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,
}
+41 -1
View File
@@ -7,11 +7,12 @@ namespace Engine.Core;
/// SDL3-backed implementation of <see cref="IInputState"/>. /// SDL3-backed implementation of <see cref="IInputState"/>.
/// Populated by polling SDL events via <see cref="ProcessEvent"/> once per frame. /// Populated by polling SDL events via <see cref="ProcessEvent"/> once per frame.
/// </summary> /// </summary>
public sealed class InputMapping : IInputState public sealed unsafe class InputMapping : IInputState
{ {
private readonly HashSet<Key> _keysPressed = new(); private readonly HashSet<Key> _keysPressed = new();
private readonly HashSet<Key> _keysDown = new(); private readonly HashSet<Key> _keysDown = new();
private readonly HashSet<Key> _keysReleased = new(); private readonly HashSet<Key> _keysReleased = new();
private readonly List<InputEvent> _events = new();
public int MouseX { get; private set; } public int MouseX { get; private set; }
public int MouseY { 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 MouseRight { get; private set; }
public bool MouseMiddle { get; private set; } public bool MouseMiddle { get; private set; }
public float MouseWheelDelta { get; private set; } public float MouseWheelDelta { get; private set; }
public IReadOnlyList<InputEvent> Events => _events;
public void BeginFrame() public void BeginFrame()
{ {
_keysPressed.Clear(); _keysPressed.Clear();
_keysReleased.Clear(); _keysReleased.Clear();
_events.Clear();
MouseWheelDelta = 0; MouseWheelDelta = 0;
} }
@@ -38,6 +41,10 @@ public sealed class InputMapping : IInputState
if (!_keysDown.Contains(key)) if (!_keysDown.Contains(key))
_keysPressed.Add(key); _keysPressed.Add(key);
_keysDown.Add(key); _keysDown.Add(key);
_events.Add(new InputEvent(
InputEventType.KeyDown,
Key: key,
IsRepeat: evt.key.repeat));
break; break;
} }
@@ -47,6 +54,7 @@ public sealed class InputMapping : IInputState
if (key == Key.Unknown) break; if (key == Key.Unknown) break;
_keysDown.Remove(key); _keysDown.Remove(key);
_keysReleased.Add(key); _keysReleased.Add(key);
_events.Add(new InputEvent(InputEventType.KeyUp, Key: key));
break; break;
} }
@@ -57,14 +65,46 @@ public sealed class InputMapping : IInputState
case SDL_EventType.SDL_EVENT_MOUSE_BUTTON_DOWN: case SDL_EventType.SDL_EVENT_MOUSE_BUTTON_DOWN:
SetMouseButton(evt.button.button, true); 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; break;
case SDL_EventType.SDL_EVENT_MOUSE_BUTTON_UP: case SDL_EventType.SDL_EVENT_MOUSE_BUTTON_UP:
SetMouseButton(evt.button.button, false); 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; break;
case SDL_EventType.SDL_EVENT_MOUSE_WHEEL: case SDL_EventType.SDL_EVENT_MOUSE_WHEEL:
MouseWheelDelta += evt.wheel.y; 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; break;
} }
} }
+35
View File
@@ -18,16 +18,25 @@ public sealed unsafe class Sdl3Window : IWindow
public int Width { get; private set; } public int Width { get; private set; }
public int Height { 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 bool ShouldClose { get; private set; }
public IInputState Input => _input; public IInputState Input => _input;
public nint Handle => (nint)_window; public nint Handle => (nint)_window;
public void Close() => ShouldClose = true; 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) public Sdl3Window(string title, int width, int height, bool vulkanSurface = true)
{ {
Width = width; Width = width;
Height = height; Height = height;
DrawableWidth = width;
DrawableHeight = height;
if (!SDL3.SDL_Init(SDL_InitFlags.SDL_INIT_VIDEO)) if (!SDL3.SDL_Init(SDL_InitFlags.SDL_INIT_VIDEO))
throw new InvalidOperationException($"SDL_Init failed: {SDL3.SDL_GetError()}"); 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_ShowWindow(_window);
SDL3.SDL_RaiseWindow(_window); SDL3.SDL_RaiseWindow(_window);
UpdateDrawableSize();
SDL3.SDL_StartTextInput(_window);
// On Wayland, the compositor needs an event round-trip before mapping // On Wayland, the compositor needs an event round-trip before mapping
// the window. Pump events to flush the show request without blocking. // 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: case SDL_EventType.SDL_EVENT_WINDOW_RESIZED:
Width = evt.window.data1; Width = evt.window.data1;
Height = evt.window.data2; 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; break;
case SDL_EventType.SDL_EVENT_KEY_DOWN: case SDL_EventType.SDL_EVENT_KEY_DOWN:
@@ -104,11 +125,25 @@ public sealed unsafe class Sdl3Window : IWindow
return result; 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() public void Dispose()
{ {
if (_disposed) return; if (_disposed) return;
_disposed = true; _disposed = true;
SDL3.SDL_StopTextInput(_window);
SDL3.SDL_DestroyWindow(_window); SDL3.SDL_DestroyWindow(_window);
SDL3.SDL_Quit(); SDL3.SDL_Quit();
} }
+16 -6
View File
@@ -447,11 +447,13 @@ internal sealed unsafe class VulkanImGui : IDisposable
_indexMemory = im; _indexMemory = im;
} }
public void NewFrame() public void NewFrame(float displayWidth, float displayHeight,
float framebufferScaleX, float framebufferScaleY, float deltaTime)
{ {
var io = ImGui.GetIO(); var io = ImGui.GetIO();
io.DisplaySize = new System.Numerics.Vector2(1280, 720); io.DisplaySize = new Vector2(displayWidth, displayHeight);
io.DeltaTime = 0.016f; io.DisplayFramebufferScale = new Vector2(framebufferScaleX, framebufferScaleY);
io.DeltaTime = MathF.Max(deltaTime, 1f / 1000f);
ImGui.NewFrame(); ImGui.NewFrame();
} }
@@ -520,13 +522,21 @@ internal sealed unsafe class VulkanImGui : IDisposable
{ {
var pcmd = list.CmdBuffer[i]; var pcmd = list.CmdBuffer[i];
var clip = pcmd.ClipRect; 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 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 Extent = new VkExtent2D
{ {
Width = (uint)(clip.Z - clip.X), Width = (uint)(clipMaxX - clipMinX),
Height = (uint)(clip.W - clip.Y), Height = (uint)(clipMaxY - clipMinY),
}, },
}; };
Vk.vkCmdSetScissor(cmd, 0, 1, &scissor); Vk.vkCmdSetScissor(cmd, 0, 1, &scissor);
@@ -23,7 +23,7 @@ internal sealed class VulkanRenderContext : IRenderContext{
}; };
_swapchain = new VulkanSwapchain(_ctx.Device, _ctx.PhysicalDevice, _ctx.Surface, _swapchain = new VulkanSwapchain(_ctx.Device, _ctx.PhysicalDevice, _ctx.Surface,
surfaceFormat, window.Width, window.Height, _ctx); surfaceFormat, window.DrawableWidth, window.DrawableHeight, _ctx);
} }
public IRenderer CreateRenderer() public IRenderer CreateRenderer()
+7 -5
View File
@@ -179,9 +179,11 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh
return result; 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() 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 // 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. // 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; return;
// Read captured frame from previous render (GPU has finished by now) // 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. // from this frame would leave the next WaitForFrame blocked forever.
if (acquireResult == VkResult.ErrorOutOfDateKHR) if (acquireResult == VkResult.ErrorOutOfDateKHR)
{ {
RecreateSwapchain(_window.Width, _window.Height); RecreateSwapchain(_window.DrawableWidth, _window.DrawableHeight);
return; return;
} }
@@ -657,7 +659,7 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh
if (presentResult == VkResult.ErrorOutOfDateKHR || presentResult == VkResult.SuboptimalKHR) if (presentResult == VkResult.ErrorOutOfDateKHR || presentResult == VkResult.SuboptimalKHR)
{ {
RecreateSwapchain(_window.Width, _window.Height); RecreateSwapchain(_window.DrawableWidth, _window.DrawableHeight);
} }
_frameIndex = (_frameIndex + 1) % VulkanFrameResources.MaxFramesInFlight; _frameIndex = (_frameIndex + 1) % VulkanFrameResources.MaxFramesInFlight;
+4 -1
View File
@@ -17,8 +17,11 @@ public interface IRenderer : IDisposable
/// <summary> /// <summary>
/// Called before RenderWorld to begin a new ImGui frame. /// Called before RenderWorld to begin a new ImGui frame.
/// Null if the backend doesn't support ImGui. /// 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> /// </summary>
void BeginImGuiFrame() { } void BeginImGuiFrame(float displayWidth, float displayHeight,
float framebufferScaleX, float framebufferScaleY, float deltaTime) { }
/// <summary> /// <summary>
/// Called after RenderWorld to render ImGui draw data. /// Called after RenderWorld to render ImGui draw data.