feat: basic sdl3 basic windowing

This commit is contained in:
2026-01-26 12:25:38 +03:00
parent 02130d8324
commit 18ac6b23fd
3 changed files with 408 additions and 0 deletions

View File

@@ -0,0 +1,284 @@
using System;
using Engine.Core;
using SDL3;
namespace Engine.Integration.SDL3;
public class Sdl3Window : Behaviour, IWindow, IUpdate, IDisposable, IEnterUniverse, IExitUniverse
{
public Event<IWindow, IWindow.StateChangedArguments> OnStateChanged { get; } = new();
public Event<IWindow, IWindow.FocusStateChangedArguments> OnFocusStateChanged { get; } = new();
public Event<IWindow, IWindow.ModeChangedArguments> OnModeChanged { get; } = new();
public Event<IWindow, IWindow.ShowStateChangedArguments> OnShowStateChanged { get; } = new();
public Event<IWindow, IWindow.ResizeModeChangedArguments> OnResizeModeChanged { get; } = new();
public Event<IWindow, IWindow.TitleChangedArguments> OnTitleChanged { get; } = new();
public Event<IWindow, IWindow.TargetFpsChangedArguments> OnTargetFpsChanged { get; } = new();
public Event<IWindow, IWindow.SizeChangedArguments> OnSizeChanged { get; } = new();
public Event<IWindow, IWindow.BackgroundColorChangedArguments> OnBackgroundColorChanged { get; } = new();
private IntPtr window = IntPtr.Zero;
private IntPtr renderer = IntPtr.Zero;
private ulong nextRenderTick = 0;
private ulong renderInterval = (ulong)(1f / 60f * 1000f);
public uint TargetFps
{
get => field;
set
{
if (value == field)
return;
uint previousTargetFps = field;
field = value;
renderInterval = value == 0 ? 0 : (ulong)(1f / value * 1000f);
OnTargetFpsChanged.Invoke(this, new(previousTargetFps));
}
} = 0;
public WindowState State
{
get => field;
set
{
if (value == field)
return;
switch (value)
{
case WindowState.Open: OpenWindow(); break;
case WindowState.Closed: default: CloseWindow(); break;
}
WindowState previousState = field;
field = value;
OnStateChanged.Invoke(this, new(previousState));
}
} = WindowState.Open;
public WindowFocusState FocusState
{
get => field;
private set
{
if (value == field)
return;
WindowFocusState previousFocusState = field;
field = value;
OnFocusStateChanged.Invoke(this, new(previousFocusState));
}
} = WindowFocusState.Unfocused;
public WindowMode Mode
{
get => field;
set
{
if (value == field)
return;
if (value is WindowMode.Windowed or WindowMode.BorderlessWindowed)
{
SDL.SetWindowFullscreen(window, false);
SDL.SyncWindow(window);
}
if (window != IntPtr.Zero)
switch (value)
{
case WindowMode.Windowed: SDL.SetWindowBordered(window, true); break;
case WindowMode.BorderlessWindowed: SDL.SetWindowBordered(window, false); break;
case WindowMode.BorderlessFullscreen: SDL.SetWindowFullscreen(window, true); break;
case WindowMode.Fullscreen: SDL.SetWindowFullscreen(window, true); break;
}
SDL.SyncWindow(window);
WindowMode previousMode = field;
field = value;
OnModeChanged.Invoke(this, new(previousMode));
}
} = WindowMode.Fullscreen;
public WindowShowState ShowState
{
get => field;
set
{
if (value == field)
return;
if (window != IntPtr.Zero)
switch (value)
{
case WindowShowState.Normal: SDL.RestoreWindow(window); break;
case WindowShowState.Minimized: SDL.MinimizeWindow(window); break;
case WindowShowState.Maximized: SDL.MaximizeWindow(window); break;
}
WindowShowState previousShowState = field;
field = value;
OnShowStateChanged.Invoke(this, new(previousShowState));
}
} = WindowShowState.Normal;
public WindowResizeMode ResizeMode
{
get => field;
set
{
if (value == field)
return;
if (window != IntPtr.Zero)
switch (value)
{
case WindowResizeMode.Fixed:
SDL.SetWindowResizable(window, false);
break;
case WindowResizeMode.Resizable:
case WindowResizeMode.AspectLocked:
SDL.SetWindowResizable(window, true);
break;
}
WindowResizeMode previousResizeMode = field;
field = value;
OnResizeModeChanged.Invoke(this, new(previousResizeMode));
}
} = WindowResizeMode.Fixed;
public ColorRGB BackgroundColor
{
get => field;
set
{
if (value == field)
return;
ColorRGB previousBackgroundColor = field;
field = value;
OnBackgroundColorChanged.Invoke(this, new(previousBackgroundColor));
}
} = new ColorRGB(35, 20, 35);
public string Title
{
get => field;
set
{
if (value == field)
return;
string previousTitle = field;
field = value;
OnTitleChanged.Invoke(this, new(previousTitle));
}
} = nameof(Sdl3Window);
public Vector2DInt Size
{
get => field;
set
{
if (value == field)
return;
if (value.Max(Vector2DInt.One) == Vector2DInt.One)
throw new Exception($"Window size can not be smaller than 1x1");
Vector2DInt previousSize = field;
field = value;
OnSizeChanged.Invoke(this, new(previousSize));
}
} = Vector2DInt.Zero;
public void Update()
{
while (SDL.PollEvent(out SDL.Event e))
ProcessInternalEvents((SDL.EventType)e.Type);
ulong currentTick = SDL.GetTicks();
if (currentTick >= nextRenderTick)
{
nextRenderTick = currentTick + renderInterval;
SDL.SetRenderDrawColor(renderer, BackgroundColor.R, BackgroundColor.G, BackgroundColor.B, 255);
SDL.RenderClear(renderer);
// TODO Render
SDL.RenderPresent(renderer);
}
}
public void ExitUniverse(IUniverse universe) => CloseWindow();
public void EnterUniverse(IUniverse universe)
{
if (State == WindowState.Open)
OpenWindow();
}
private void OpenWindow()
{
if (!UniverseObject.IsInUniverse)
throw new Exception($"{UniverseObject.Name} is not in a universe. The window can not be opened.");
SDL.WindowFlags windowFlags = 0;
switch (Mode)
{
case WindowMode.Windowed: break;
case WindowMode.Fullscreen: windowFlags |= SDL.WindowFlags.Fullscreen; break;
case WindowMode.BorderlessWindowed: windowFlags |= SDL.WindowFlags.Borderless; break;
case WindowMode.BorderlessFullscreen: windowFlags |= SDL.WindowFlags.Fullscreen | SDL.WindowFlags.Borderless; break;
}
if (!SDL.Init(SDL.InitFlags.Video))
throw new Exception($"SDL_Init failed: {SDL.GetError()}");
if (!SDL.CreateWindowAndRenderer(Title, Size.X, Size.Y, windowFlags, out window, out renderer))
throw new Exception($"SDL_CreateWindowAndRenderer failed: {SDL.GetError()}");
}
private void CloseWindow()
{
if (renderer != IntPtr.Zero) SDL.DestroyRenderer(renderer);
if (window != IntPtr.Zero) SDL.DestroyWindow(window);
renderer = IntPtr.Zero;
window = IntPtr.Zero;
}
private void ProcessInternalEvents(SDL.EventType eventType)
{
switch (eventType)
{
case SDL.EventType.WindowRestored: ShowState = WindowShowState.Normal; break;
case SDL.EventType.WindowMaximized: ShowState = WindowShowState.Maximized; break;
case SDL.EventType.WindowMinimized: ShowState = WindowShowState.Minimized; break;
case SDL.EventType.WindowFocusGained: FocusState = WindowFocusState.Focused; break;
case SDL.EventType.WindowFocusLost: FocusState = WindowFocusState.Unfocused; break;
case SDL.EventType.Quit:
State = WindowState.Closed;
BehaviourController.RemoveBehaviour(this);
break;
}
}
public void Dispose()
{
CloseWindow();
// SDL.Quit(); // TODO This possibly prevents multi window
GC.SuppressFinalize(this);
}
}

View File

@@ -0,0 +1,72 @@
using Engine.Core;
namespace Engine.Integration.SDL3;
public interface IWindow : IBehaviour
{
Event<IWindow, StateChangedArguments> OnStateChanged { get; }
Event<IWindow, FocusStateChangedArguments> OnFocusStateChanged { get; }
Event<IWindow, ModeChangedArguments> OnModeChanged { get; }
Event<IWindow, ShowStateChangedArguments> OnShowStateChanged { get; }
Event<IWindow, ResizeModeChangedArguments> OnResizeModeChanged { get; }
Event<IWindow, TitleChangedArguments> OnTitleChanged { get; }
Event<IWindow, TargetFpsChangedArguments> OnTargetFpsChanged { get; }
Event<IWindow, SizeChangedArguments> OnSizeChanged { get; }
Event<IWindow, BackgroundColorChangedArguments> OnBackgroundColorChanged { get; }
WindowState State { get; set; }
WindowFocusState FocusState { get; }
WindowMode Mode { get; set; }
WindowShowState ShowState { get; set; }
WindowResizeMode ResizeMode { get; set; }
string Title { get; set; }
uint TargetFps { get; set; }
Vector2DInt Size { get; set; }
ColorRGB BackgroundColor { get; set; }
readonly record struct StateChangedArguments(WindowState PreviousState);
readonly record struct FocusStateChangedArguments(WindowFocusState PreviousFocusState);
readonly record struct ModeChangedArguments(WindowMode PreviousMode);
readonly record struct ShowStateChangedArguments(WindowShowState PreviousShowState);
readonly record struct ResizeModeChangedArguments(WindowResizeMode PreviousResizeMode);
readonly record struct TitleChangedArguments(string PreviousTitle);
readonly record struct TargetFpsChangedArguments(uint PreviousTargetFps);
readonly record struct SizeChangedArguments(Vector2DInt PreviousSize);
readonly record struct BackgroundColorChangedArguments(ColorRGB PreviousColor);
}
public enum WindowFocusState
{
Focused,
Unfocused
}
public enum WindowState
{
Open,
Closed
}
public enum WindowMode
{
Windowed,
Fullscreen,
BorderlessFullscreen,
BorderlessWindowed
}
public enum WindowShowState
{
Normal,
Minimized,
Maximized
}
public enum WindowResizeMode
{
Fixed,
Resizable,
AspectLocked
}

View File

@@ -3,6 +3,7 @@
using Engine.Core;
using Engine.Core.Debug;
using Engine.Core.Serialization;
using Engine.Integration.SDL3;
using Engine.Serializers.Yaml;
using Engine.Systems.Network;
using Engine.Systems.Time;
@@ -22,6 +23,9 @@ universe.InstantiateUniverseObject().SetUniverseObject("Logger")
IUniverseObject desktopParent = universe.InstantiateUniverseObject().SetUniverseObject("Desktop");
// universe.InstantiateUniverseObject().SetUniverseObject("Desktop Inputs", desktopParent)
// .BehaviourController.AddBehaviour<DesktopInputs>();
universe.InstantiateUniverseObject().SetUniverseObject("Visual Managers", desktopParent)
.BehaviourController.AddBehaviour<DrawManager>();
@@ -36,8 +40,38 @@ MyUniverse.Shared.UniverseSource.ApplyUniverse(universe);
desktopParent.AddChild(universe.InstantiateUniverseObject().SetUniverseObject("Triangle Batcher").BehaviourController.AddBehaviour<Engine.Systems.Graphics.TriangleBatcher>().UniverseObject);
Sdl3Window window = Engine.Core.Factory.BehaviourFactory.Instantiate<Sdl3Window>();
window.Title = "MyUniverse";
window.Size = new(800, 600);
window.Mode = WindowMode.BorderlessWindowed;
IUniverseObject windowUO = universe.InstantiateUniverseObject().SetUniverseObject("SDL3 Window", desktopParent);
windowUO.BehaviourController.AddBehaviour(window);
// Testing Start
TickerStopwatch tickerStopwatch = windowUO.BehaviourController.AddBehaviour<TickerStopwatch>();
tickerStopwatch.Period = 5f;
tickerStopwatch.OnTick.AddListener(sender =>
{
if (window.IsInitialized)
windowUO.BehaviourController.RemoveBehaviour(window);
else
windowUO.BehaviourController.AddBehaviour(window);
});
tickerStopwatch.Start();
// Testing End
universe.Initialize();
// window.OnRender.AddListener((sender, args) => universe.Draw());
// window.OnUpdate.AddListener((sender, args) =>
// {
// timeSinceStart += args.TimeSpan;
// universe.Update(new(timeSinceStart, args.TimeSpan));
// });
// window.Run();
DateTime lastRun = DateTime.UtcNow;
TimeSpan timeSinceStart = new(0);
@@ -52,3 +86,21 @@ while (true)
lastRun = now;
System.Threading.Thread.Sleep(1);
}
// DateTime lastRun = DateTime.UtcNow;
// TimeSpan interval = new(0, 0, 0, 0, 16);
// TimeSpan timeSinceStart = new(0);
// universe.Initialize();
// while (true)
// {
// if (lastRun + interval <= DateTime.UtcNow)
// {
// lastRun += interval;
// timeSinceStart += interval;
// universe.Update(new(timeSinceStart, interval));
// }
// Thread.Sleep(1);
// }