diff --git a/docs/class-registration.md b/docs/class-registration.md index f915509f..fb9deee3 100644 --- a/docs/class-registration.md +++ b/docs/class-registration.md @@ -71,7 +71,7 @@ Use when: Registers an already-constructed instance. No source generator required. ```csharp -services.AddSingleton(new AppConfig { Width = 1280, Height = 720 }); +services.AddSingleton(new WindowOptions(Size: (1280, 720), Title: "Game")); services.AddSingleton(new ConsoleLogger()); ``` @@ -360,7 +360,7 @@ A child provider flattens the parent's singleton service array and registration ```csharp ServiceCollection rootCollection = new(); -rootCollection.AddSingleton(new AppConfig()); +rootCollection.AddSingleton(new GameKitConfig()); ServiceProvider root = rootCollection.BuildServiceProvider(); ServiceCollection stageCollection = root.CreateServiceCollection(); @@ -368,7 +368,7 @@ stageCollection.AddSingleton(new GameplayView()); ServiceProvider stage = stageCollection.BuildServiceProvider(); // stage can resolve both its own and parent services -AppConfig config = stage.GetRequiredService(); +GameKitConfig config = stage.GetRequiredService(); IView view = stage.GetRequiredService(); ``` diff --git a/docs/subrenderers.md b/docs/subrenderers.md index dff1721b..a8af29cb 100644 --- a/docs/subrenderers.md +++ b/docs/subrenderers.md @@ -3,7 +3,7 @@ Subrenderers are renderers that receive an existing RenderPass instead of creating their own. They're used for internal composition within an `IRenderPhase`. ``` -DefaultRenderManager +DefaultRenderCoordinator └─ IRenderPhase[] (geometry, lighting, post-process phases) └─ Subrenderers (multiple renderers sharing the same RenderPass) ``` @@ -129,7 +129,7 @@ public class GeometryPhase : IRenderPhase } ``` -**Note:** `IRenderPhase` is managed by `DefaultRenderManager`, which orchestrates multiple phases (geometry, lighting, post-process) in order. +**Note:** `IRenderPhase` is coordinated by `DefaultRenderCoordinator`, which executes multiple phases (geometry, lighting, post-process) in order. ## Key Points diff --git a/src/GameKit.Pencuil/Pencil.cs b/src/GameKit.Pencuil/Pencil.cs index d70cdefe..c25345fd 100644 --- a/src/GameKit.Pencuil/Pencil.cs +++ b/src/GameKit.Pencuil/Pencil.cs @@ -120,16 +120,11 @@ public void UpdateCursor(Vector2Int position, bool pressed) internal bool FocusClaimedThisFrame; internal TextFieldEditingState? EditingState; - public Pencil(IFontSystem fontSystem, IClipboardService clipboardService, GuiStyle guiStyle, AppConfig appConfig) + public Pencil(IFontSystem fontSystem, IClipboardService clipboardService, GuiStyle guiStyle) { _fontSystem = fontSystem; _clipboardService = clipboardService; Style = guiStyle; - if (appConfig.Size is { } size) - { - _viewportWidth = (int)size.Width; - _viewportHeight = (int)size.Height; - } } public void AddHoverTest(Rectangle test) diff --git a/src/GameKit.Pencuil/PencilSystem.cs b/src/GameKit.Pencuil/PencilSystem.cs index 336754be..f7f3ee8c 100644 --- a/src/GameKit.Pencuil/PencilSystem.cs +++ b/src/GameKit.Pencuil/PencilSystem.cs @@ -74,7 +74,7 @@ public PencilSystem(Pencil pencil, ViewRegistry viewRegistry, Window window, IMo public void Update() { - ShortSize renderSize = _window.RenderSizeInPixels; + ShortSize renderSize = _window.RequireActivation().RenderSizeInPixels; _pencil.UpdateViewport(renderSize.Width, renderSize.Height); bool needsBuild = _pencil.NeedsUpdate | _viewRegistry.ConsumeDirty(); diff --git a/src/GameKit.Pencuil/PencuilRenderer.cs b/src/GameKit.Pencuil/PencuilRenderer.cs index 59d1bb91..dbda2109 100644 --- a/src/GameKit.Pencuil/PencuilRenderer.cs +++ b/src/GameKit.Pencuil/PencuilRenderer.cs @@ -32,12 +32,13 @@ public class PencuilRenderer private int _maxDepthValue; + // TODO: This constructor performs substantial GPU resource creation and may become a static factory method. public PencuilRenderer( GraphicsPipelineBuilder graphicsPipelineBuilder, GpuMemorySystem gpuMemorySystem, ShaderLoader shaderLoader, GpuDevice gpuDevice, - WindowManager windowManager) + Window window) { ReadOnlySpan quad = [ @@ -54,8 +55,9 @@ public PencuilRenderer( FragmentShader tintedTextureFragmentShader = shaderLoader.LoadFragmentShader("shaders/pencuil_tinted_texture_fragment"); FragmentShader textureFragmentShader = shaderLoader.LoadFragmentShader("shaders/pencuil_texture_fragment"); - TextureFormat colorTargetFormat = windowManager.PrimaryWindow.ColorTargetFormat; - ShortSize renderSize = windowManager.PrimaryWindow.RenderSizeInPixels; + ActivationWindow activation = window.RequireActivation(); + TextureFormat colorTargetFormat = activation.ColorTargetFormat; + ShortSize renderSize = activation.RenderSizeInPixels; _colorPipeline = graphicsPipelineBuilder .SetPrimitiveType(PrimitiveType.TriangleStrip) diff --git a/src/GameKit.RenderOrchestration/DefaultRenderContextProvider.cs b/src/GameKit.RenderOrchestration/DefaultRenderContextProvider.cs index 81d002b5..f4c77b92 100644 --- a/src/GameKit.RenderOrchestration/DefaultRenderContextProvider.cs +++ b/src/GameKit.RenderOrchestration/DefaultRenderContextProvider.cs @@ -9,12 +9,12 @@ namespace GameKit.RenderOrchestration; /// public class DefaultRenderContextProvider : IRenderContextProvider { - private readonly WindowManager _windowManager; + private readonly Window _window; private readonly GpuDevice _gpuDevice; - public DefaultRenderContextProvider(WindowManager windowManager, GpuDevice gpuDevice) + public DefaultRenderContextProvider(Window window, GpuDevice gpuDevice) { - _windowManager = windowManager; + _window = window; _gpuDevice = gpuDevice; } @@ -27,7 +27,9 @@ public bool TryProvide([NotNullWhen(true)] out DefaultRenderContext? renderConte { CommandBuffer renderCommandBuffer = _gpuDevice.AcquireCommandBuffer(); - if (!_windowManager.PrimaryWindow.TryWaitAndAcquireSwapchainTexture(renderCommandBuffer, out SwapchainTexture swapchainTexture)) + ActivationWindow? activation = _window.Activation; + if (activation == null || + !activation.TryWaitAndAcquireSwapchainTexture(renderCommandBuffer, out SwapchainTexture swapchainTexture)) { renderContext = null; renderCommandBuffer.Dispose(); diff --git a/src/GameKit.RenderOrchestration/DefaultRenderManager.cs b/src/GameKit.RenderOrchestration/DefaultRenderCoordinator.cs similarity index 83% rename from src/GameKit.RenderOrchestration/DefaultRenderManager.cs rename to src/GameKit.RenderOrchestration/DefaultRenderCoordinator.cs index 12241aeb..1596abf5 100644 --- a/src/GameKit.RenderOrchestration/DefaultRenderManager.cs +++ b/src/GameKit.RenderOrchestration/DefaultRenderCoordinator.cs @@ -4,20 +4,22 @@ namespace GameKit.RenderOrchestration; /// -/// Manages the overall rendering process by coordinating multiple render phases. +/// Coordinates rendering across multiple render phases. /// /// The type of the render context used by the render phases. -public class DefaultRenderManager : IRenderManager +public class DefaultRenderCoordinator : RenderCoordinator where TRenderContext: IRenderContext { private readonly GpuMemorySystem _gpuMemorySystem; private readonly IRenderContextProvider _renderContextProvider; private readonly RenderPhaseRegistry _renderPhaseRegistry; - internal DefaultRenderManager( + internal DefaultRenderCoordinator( + Window window, GpuMemorySystem gpuMemorySystem, IRenderContextProvider renderContextProvider, RenderPhaseRegistry renderPhaseRegistry) + : base(window) { _gpuMemorySystem = gpuMemorySystem; _renderContextProvider = renderContextProvider; @@ -27,7 +29,7 @@ internal DefaultRenderManager( /// /// Executes the rendering pipeline for a single frame. /// - public void Execute() + public override void Execute() { if (!_renderContextProvider.TryProvide(out TRenderContext? renderContext)) { @@ -37,7 +39,7 @@ public void Execute() using (renderContext) { _renderPhaseRegistry.Render(renderContext); - + // submit all pending changes before renderContext is disposed _gpuMemorySystem.Submit(); } diff --git a/src/GameKit.RenderOrchestration/GameKitAppBuilderExtensions.cs b/src/GameKit.RenderOrchestration/GameKitAppBuilderExtensions.cs index 005282ee..27e07b45 100644 --- a/src/GameKit.RenderOrchestration/GameKitAppBuilderExtensions.cs +++ b/src/GameKit.RenderOrchestration/GameKitAppBuilderExtensions.cs @@ -6,33 +6,63 @@ namespace GameKit.RenderOrchestration; public static class GameKitAppBuilderExtensions { - public static GameKitAppBuilder UseDefaultRenderManager(this GameKitAppBuilder builder) where TRenderContext: IRenderContext + public static GameKitAppBuilder UseDefaultRenderCoordinator( + this GameKitAppBuilder builder) + where TRenderContext : IRenderContext + { + ConfigureDefaultRenderCoordinator(builder); + return builder; + } + + public static ServiceCollection UseDefaultRenderCoordinator( + this ServiceCollection services) + where TRenderContext : IRenderContext + { + ConfigureDefaultRenderCoordinator(services); + return services; + } + + public static GameKitAppBuilder UseDefaultRenderCoordinator(this GameKitAppBuilder builder) + { + ConfigureDefaultRenderContext(builder); + return builder; + } + + public static ServiceCollection UseDefaultRenderCoordinator(this ServiceCollection services) + { + ConfigureDefaultRenderContext(services); + return services; + } + + private static void ConfigureDefaultRenderContext(ServiceCollection services) + { + services.AddSingleton, DefaultRenderContextProvider>(); + ConfigureDefaultRenderCoordinator(services); + } + + private static void ConfigureDefaultRenderCoordinator(ServiceCollection services) + where TRenderContext : IRenderContext { RenderPhaseRegistry renderPhaseRegistry = new(); - builder.OnActivated((instance, _) => + services.OnActivated((instance, _) => { if (instance is IRenderPhase renderPhase) { renderPhaseRegistry.Register(renderPhase); } }); - builder.OnDisposing((instance, _) => + services.OnDisposing((instance, _) => { if (instance is IRenderPhase renderPhase) { renderPhaseRegistry.Unregister(renderPhase); } }); - builder.AddSingleton(sp => new DefaultRenderManager( - sp.GetRequiredService(), - sp.GetRequiredService>(), - renderPhaseRegistry)); - return builder; - } - - public static GameKitAppBuilder UseDefaultRenderManager(this GameKitAppBuilder builder) - { - builder.AddSingleton, DefaultRenderContextProvider>(); - return builder.UseDefaultRenderManager(); + services.AddSingleton(provider => + new DefaultRenderCoordinator( + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService>(), + renderPhaseRegistry)); } } diff --git a/src/GameKit.RenderOrchestration/IRenderContextProvider.cs b/src/GameKit.RenderOrchestration/IRenderContextProvider.cs index eb453186..7403b523 100644 --- a/src/GameKit.RenderOrchestration/IRenderContextProvider.cs +++ b/src/GameKit.RenderOrchestration/IRenderContextProvider.cs @@ -6,7 +6,8 @@ namespace GameKit.RenderOrchestration; /// Defines a provider that creates and supplies a render context for a single frame. /// /// The type of the render context to provide. -public interface IRenderContextProvider where TRenderContext: IRenderContext +public interface IRenderContextProvider + where TRenderContext : IRenderContext { /// /// Attempts to create and provide a render context. @@ -14,4 +15,4 @@ public interface IRenderContextProvider where TRenderContext: IR /// When this method returns, contains the created render context, or null if creation failed. /// True if the render context was successfully provided, false otherwise. public bool TryProvide([NotNullWhen(true)] out TRenderContext? renderContext); -} \ No newline at end of file +} diff --git a/src/GameKit.RenderOrchestration/IRenderPhase.cs b/src/GameKit.RenderOrchestration/IRenderPhase.cs index a3c6cfb4..8ab0215b 100644 --- a/src/GameKit.RenderOrchestration/IRenderPhase.cs +++ b/src/GameKit.RenderOrchestration/IRenderPhase.cs @@ -5,8 +5,13 @@ namespace GameKit.RenderOrchestration; /// culling, shadow map generation, deferred shading (e.g., lighting, ambient occlusion), /// post-processing, and UI rendering. /// +/// +/// Register a phase in the same service container as its default render coordinator or in a descendant +/// container. Phase discovery follows service activation: a phase already activated in an ancestor +/// is not retroactively attached to a render coordinator created by a child container. +/// /// The type of the render context required by this phase. -public interface IRenderPhase: IOrderable +public interface IRenderPhase : IOrderable { /// /// Executes the rendering logic for this phase. diff --git a/src/GameKit.Utils/OrthographicCameraFactory.cs b/src/GameKit.Utils/OrthographicCameraFactory.cs index 885988fa..6c3135cd 100644 --- a/src/GameKit.Utils/OrthographicCameraFactory.cs +++ b/src/GameKit.Utils/OrthographicCameraFactory.cs @@ -6,7 +6,7 @@ public static class OrthographicCameraFactory { public static Camera Create(Window window, IViewConfiguration viewConfiguration) { - ShortSize windowSize = window.RenderSizeInPixels; + ShortSize windowSize = window.RequireActivation().RenderSizeInPixels; float width = windowSize.Width / viewConfiguration.PixelsPerUnit; float height = windowSize.Height / viewConfiguration.PixelsPerUnit; @@ -19,4 +19,4 @@ public static Camera Create(Window window, IViewConfiguration viewConfiguration) FarPlane = 1000f }; } -} \ No newline at end of file +} diff --git a/src/GameKit/ActivationWindow.cs b/src/GameKit/ActivationWindow.cs new file mode 100644 index 00000000..4c643685 --- /dev/null +++ b/src/GameKit/ActivationWindow.cs @@ -0,0 +1,102 @@ +using GameKit.Gpu; +using GameKit.Utilities; +using SDL; + +namespace GameKit; + +public sealed class ActivationWindow : IDisposable +{ + internal Pointer SdlGpuDevice { get; } + internal Pointer SdlWindow { get; private set; } + + public uint Id { get; } + + internal ActivationWindow( + Pointer sdlWindow, + Pointer sdlGpuDevice, + uint id) + { + SdlWindow = sdlWindow; + SdlGpuDevice = sdlGpuDevice; + Id = id; + } + + public ShortSize RenderSizeInPixels + { + get + { + int width; + int height; + unsafe + { + SDL3.SDL_GetWindowSizeInPixels(SdlWindow, &width, &height); + } + + return new ShortSize((ushort)width, (ushort)height); + } + } + + public TextureFormat ColorTargetFormat + { + get + { + unsafe + { + return (TextureFormat)SDL3.SDL_GetGPUSwapchainTextureFormat(SdlGpuDevice, SdlWindow); + } + } + } + + public bool TryWaitAndAcquireSwapchainTexture( + CommandBuffer commandBuffer, + out SwapchainTexture swapchainTexture) + { + swapchainTexture = default!; + uint width; + uint height; + + unsafe + { + SDL_GPUTexture* swapchainTexturePointer; + if (!SDL3.SDL_WaitAndAcquireGPUSwapchainTexture( + commandBuffer.SdlGpuCommandBuffer, + SdlWindow, + &swapchainTexturePointer, + &width, + &height)) + { + throw new GameKitException( + $"SDL_WaitAndAcquireGPUSwapchainTexture failed: {SDL3.SDL_GetError()}"); + } + + if (swapchainTexturePointer == null) + { + return false; + } + + TextureFormat textureFormat = + (TextureFormat)SDL3.SDL_GetGPUSwapchainTextureFormat(SdlGpuDevice, SdlWindow); + swapchainTexture = new SwapchainTexture( + swapchainTexturePointer, + new ShortSize((ushort)width, (ushort)height), + textureFormat); + } + + return true; + } + + public void Dispose() + { + if (SdlWindow.IsNull) + { + return; + } + + unsafe + { + SDL3.SDL_ReleaseWindowFromGPUDevice(SdlGpuDevice, SdlWindow); + SDL3.SDL_DestroyWindow(SdlWindow); + SdlWindow = Pointer.Null; + } + } +} diff --git a/src/GameKit/App/GameKitApp.cs b/src/GameKit/App/GameKitApp.cs index ff0c0577..e8bcbe4b 100644 --- a/src/GameKit/App/GameKitApp.cs +++ b/src/GameKit/App/GameKitApp.cs @@ -5,6 +5,8 @@ namespace GameKit.App; public class GameKitApp : IGameKitApp { + private bool _disposed; + public ServiceProvider ServiceProvider { get; } internal GameKitApp(ServiceProvider serviceProvider) @@ -12,6 +14,11 @@ internal GameKitApp(ServiceProvider serviceProvider) ServiceProvider = serviceProvider; } + public ServiceCollection CreateServiceCollection() + { + return ServiceProvider.CreateServiceCollection(); + } + public T GetRequiredService<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] T>() where T : class { return ServiceProvider.GetRequiredService(); @@ -19,22 +26,30 @@ internal GameKitApp(ServiceProvider serviceProvider) public int Run() { + ObjectDisposedException.ThrowIf(_disposed, this); + GameKitFrameContext frameContext = ServiceProvider.GetRequiredService(); EventService eventService = ServiceProvider.GetRequiredService(); AppControl appControl = ServiceProvider.GetRequiredService(); - IRenderManager rootRenderer = ServiceProvider.GetRequiredService(); + ServiceRegistry renderCoordinators = + ServiceProvider.GetRequiredService>(); ServiceRegistry updatables = ServiceProvider.GetRequiredService>(); StageManager stageManager = ServiceProvider.GetRequiredService(); + WindowManager windowManager = ServiceProvider.GetRequiredService(); while (true) { // start the frame before applying queued stage transitions frameContext.StartFrame(); stageManager.ApplyPendingTransition(); + windowManager.ApplyPendingDisposals(); // then process events eventService.Process(); - Update(updatables); + foreach (IUpdatable updatable in updatables) + { + updatable.Update(); + } if (appControl.QuitRequested) { @@ -42,20 +57,21 @@ public int Run() } // finally render - rootRenderer.Execute(); + foreach (RenderCoordinator renderCoordinator in renderCoordinators) + { + renderCoordinator.Execute(); + } } } public void Dispose() { - ServiceProvider.Dispose(); - } - - private static void Update(ServiceRegistry updatables) - { - foreach (IUpdatable updatable in updatables) + if (_disposed) { - updatable.Update(); + return; } + + _disposed = true; + ServiceProvider.Dispose(); } } diff --git a/src/GameKit/App/GameKitAppBuilder.cs b/src/GameKit/App/GameKitAppBuilder.cs index d7db3ea1..46efce2d 100644 --- a/src/GameKit/App/GameKitAppBuilder.cs +++ b/src/GameKit/App/GameKitAppBuilder.cs @@ -19,6 +19,7 @@ public GameKitAppBuilder() int rightOrder = right is IOrderable rightOrderable ? rightOrderable.Order : 0; return leftOrder.CompareTo(rightOrder); }); + AddRegistry(); } public GameKitAppBuilder AddContentFromDirectory(string directory) @@ -63,34 +64,20 @@ public IGameKitApp Build() { AddSingleton(new GameKitConfig()); } - if (!IsRegistered()) - { - AddSingleton(new AppConfig()); - } AddSingleton(); AddSingleton(); - AddSingleton(); - AddSingleton(static sp => sp.GetRequiredService().PrimaryWindow); - AddSingleton(); AddSingleton(); - AddSingleton(); - AddAlias(); + AddSingleton(); AddSingleton(); AddAlias(); - AddSingleton(); - AddAlias(); - - AddSingleton(); - AddAlias(); - AddSingleton(); AddAlias(); @@ -103,7 +90,11 @@ public IGameKitApp Build() AddSingleton(); - AddSingleton(); + // An inherited transient is constructed by the requesting window provider, so Window resolves locally. + AddTransient(static provider => new GraphicsPipelineBuilder( + provider.GetRequiredService(), + provider.GetService(), + provider.GetRequiredService())); AddSingleton(); diff --git a/src/GameKit/App/IGameKitApp.cs b/src/GameKit/App/IGameKitApp.cs index 90bf120a..1b7524c0 100644 --- a/src/GameKit/App/IGameKitApp.cs +++ b/src/GameKit/App/IGameKitApp.cs @@ -6,6 +6,7 @@ namespace GameKit.App; public interface IGameKitApp : IDisposable { ServiceProvider ServiceProvider { get; } + ServiceCollection CreateServiceCollection(); T GetRequiredService<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] T>() where T : class; int Run(); } diff --git a/src/GameKit/App/IRenderManager.cs b/src/GameKit/App/IRenderManager.cs deleted file mode 100644 index cf3b6463..00000000 --- a/src/GameKit/App/IRenderManager.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace GameKit.App; - -public interface IRenderManager -{ - void Execute(); -} - -public sealed class NullRenderManager: IRenderManager -{ - public void Execute() - { - } -} diff --git a/src/GameKit/App/RenderCoordinator.cs b/src/GameKit/App/RenderCoordinator.cs new file mode 100644 index 00000000..4eb58fb4 --- /dev/null +++ b/src/GameKit/App/RenderCoordinator.cs @@ -0,0 +1,20 @@ +namespace GameKit.App; + +/// +/// Coordinates rendering for exactly one logical window. The required +/// makes that association part of the type and prevents a render coordinator from being constructed +/// by a windowless service container. +/// +public abstract class RenderCoordinator +{ + protected RenderCoordinator(Window window) + { + ArgumentNullException.ThrowIfNull(window); + Window = window; + } + + /// The logical window whose rendering is coordinated by this instance. + protected Window Window { get; } + + public abstract void Execute(); +} diff --git a/src/GameKit/AppConfig.cs b/src/GameKit/AppConfig.cs deleted file mode 100644 index 3deda1c2..00000000 --- a/src/GameKit/AppConfig.cs +++ /dev/null @@ -1,5 +0,0 @@ -using GameKit.Gpu; - -namespace GameKit; - -public sealed record AppConfig(Size? Size = null, string? Title = null, FColor? ClearColor = null, bool Fullscreen = false, bool Resizable = false, bool Transparent = false, bool Borderless = false, bool AlwaysOnTop = false); diff --git a/src/GameKit/EventService.cs b/src/GameKit/EventService.cs index f051c50b..8c4a9eae 100644 --- a/src/GameKit/EventService.cs +++ b/src/GameKit/EventService.cs @@ -3,22 +3,17 @@ namespace GameKit; -public class EventService +public sealed class EventService { - private readonly KeyboardService _keyboardService; private readonly GamepadService _gamepadService; - private readonly MouseService _mouseService; - private readonly TextInputService _textInputService; - private readonly WindowManager _windowManager; private readonly AppControl _appControl; + private (uint WindowId, WindowEventService Service)[] _windowEventServices = []; - internal EventService(KeyboardService keyboardService, GamepadService gamepadService, MouseService mouseService, TextInputService textInputService, WindowManager windowManager, AppControl appControl) + internal EventService( + GamepadService gamepadService, + AppControl appControl) { - _keyboardService = keyboardService; _gamepadService = gamepadService; - _mouseService = mouseService; - _textInputService = textInputService; - _windowManager = windowManager; _appControl = appControl; } @@ -27,100 +22,106 @@ public void Process() unsafe { SDL_Event evt; - while (SDL3.SDL_PollEvent(&evt) == true) + while (SDL3.SDL_PollEvent(&evt)) { - if (evt.Type == SDL_EventType.SDL_EVENT_KEY_DOWN) - { - _keyboardService.OnKeyEvent(evt.key); - } - else if (evt.Type == SDL_EventType.SDL_EVENT_KEY_UP) - { - _keyboardService.OnKeyEvent(evt.key); - } - else if (evt.Type == SDL_EventType.SDL_EVENT_GAMEPAD_ADDED) - { - _gamepadService.OnGamepadAdded(evt.gdevice.which); - } - else if (evt.Type == SDL_EventType.SDL_EVENT_GAMEPAD_REMOVED) - { - _gamepadService.OnGamepadRemoved(evt.gdevice.which); - } - else if (evt.Type == SDL_EventType.SDL_EVENT_GAMEPAD_AXIS_MOTION) - { - _gamepadService.OnGamepadStickMotion(in evt.gaxis); - } - else if (evt.Type == SDL_EventType.SDL_EVENT_GAMEPAD_BUTTON_DOWN) - { - _gamepadService.OnGamepadButtonPressed(evt.gbutton); - } - else if (evt.Type == SDL_EventType.SDL_EVENT_GAMEPAD_BUTTON_UP) - { - _gamepadService.OnGamepadButtonReleased(evt.gbutton); - } - else if (evt.Type == SDL_EventType.SDL_EVENT_MOUSE_BUTTON_DOWN) - { - _mouseService.OnMouseButtonEvent(evt.button); - } - else if (evt.Type == SDL_EventType.SDL_EVENT_MOUSE_BUTTON_UP) - { - _mouseService.OnMouseButtonEvent(evt.button); - } - else if (evt.Type == SDL_EventType.SDL_EVENT_MOUSE_MOTION) - { - _mouseService.OnMouseMotionEvent(evt.motion); - } - else if (evt.Type == SDL_EventType.SDL_EVENT_MOUSE_WHEEL) - { - _mouseService.OnMouseWheelEvent(evt.wheel); - } - else if (evt.Type == SDL_EventType.SDL_EVENT_WINDOW_MOUSE_ENTER) - { - if ((uint)evt.window.windowID == _windowManager.PrimaryWindow.Id) - { - _mouseService.OnMouseWindowPresenceEvent(evt.window, true); - } - } - else if (evt.Type == SDL_EventType.SDL_EVENT_WINDOW_MOUSE_LEAVE) - { - if ((uint)evt.window.windowID == _windowManager.PrimaryWindow.Id) - { - _mouseService.OnMouseWindowPresenceEvent(evt.window, false); - } - } - else if (evt.Type == SDL_EventType.SDL_EVENT_TEXT_INPUT) - { - _textInputService.OnTextInputEvent(evt.text); - } - else if (evt.Type == SDL_EventType.SDL_EVENT_TEXT_EDITING) - { - _textInputService.OnTextEditingEvent(evt.edit); - } - else if (evt.Type == SDL_EventType.SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED) - { - if (_windowManager.TryGetWindow((uint)evt.window.windowID, out Window pixelSizeWindow)) - { - pixelSizeWindow.OnPixelSizeChanged(evt.window.timestamp); - } - } - else if (evt.Type == SDL_EventType.SDL_EVENT_WINDOW_CLOSE_REQUESTED) - { - if (_windowManager.TryGetWindow((uint)evt.window.windowID, out Window closedWindow)) - { - if (closedWindow == _windowManager.PrimaryWindow) - { - _appControl.Quit(); - } - else - { - _windowManager.DestroyWindow(closedWindow); - } - } - } - else if (evt.Type == SDL_EventType.SDL_EVENT_QUIT) - { - _appControl.Quit(); - } + Process(&evt); } } } + + private unsafe void Process(SDL_Event* eventPointer) + { + ref SDL_Event evt = ref *eventPointer; + + if (evt.Type == SDL_EventType.SDL_EVENT_GAMEPAD_ADDED) + { + _gamepadService.OnGamepadAdded(evt.gdevice.which); + } + else if (evt.Type == SDL_EventType.SDL_EVENT_GAMEPAD_REMOVED) + { + _gamepadService.OnGamepadRemoved(evt.gdevice.which); + } + else if (evt.Type == SDL_EventType.SDL_EVENT_GAMEPAD_AXIS_MOTION) + { + _gamepadService.OnGamepadStickMotion(in evt.gaxis); + } + else if (evt.Type == SDL_EventType.SDL_EVENT_GAMEPAD_BUTTON_DOWN) + { + _gamepadService.OnGamepadButtonPressed(evt.gbutton); + } + else if (evt.Type == SDL_EventType.SDL_EVENT_GAMEPAD_BUTTON_UP) + { + _gamepadService.OnGamepadButtonReleased(evt.gbutton); + } + else if (evt.Type == SDL_EventType.SDL_EVENT_QUIT) + { + _appControl.Quit(); + } + else + { + SDL_Window* sdlWindow = SDL3.SDL_GetWindowFromEvent(eventPointer); + if (sdlWindow != null && + TryGetWindowEventService( + (uint)SDL3.SDL_GetWindowID(sdlWindow), + out WindowEventService windowEvents)) + { + windowEvents.Process(in evt); + } + } + } + + internal void Attach(WindowEventService windowEvents) + { + uint windowId = windowEvents.Window.RequireActivation().Id; + for (int i = 0; i < _windowEventServices.Length; i++) + { + if (_windowEventServices[i].WindowId == windowId) + { + throw new InvalidOperationException($"SDL window ID {windowId} is already attached."); + } + } + + _windowEventServices = [.. _windowEventServices, (windowId, windowEvents)]; + } + + internal void Detach(WindowEventService windowEvents) + { + int index = -1; + for (int i = 0; i < _windowEventServices.Length; i++) + { + if (ReferenceEquals(_windowEventServices[i].Service, windowEvents)) + { + index = i; + break; + } + } + + if (index < 0) + { + return; + } + + int itemsToMove = _windowEventServices.Length - index - 1; + if (itemsToMove > 0) + { + Array.Copy(_windowEventServices, index + 1, _windowEventServices, index, itemsToMove); + } + + Array.Resize(ref _windowEventServices, _windowEventServices.Length - 1); + } + + internal bool TryGetWindowEventService(uint windowId, out WindowEventService windowEvents) + { + for (int i = 0; i < _windowEventServices.Length; i++) + { + if (_windowEventServices[i].WindowId == windowId) + { + windowEvents = _windowEventServices[i].Service; + return true; + } + } + + windowEvents = null!; + return false; + } } diff --git a/src/GameKit/GameKitFactory.cs b/src/GameKit/GameKitFactory.cs index 4a0cb820..f0c72b6d 100644 --- a/src/GameKit/GameKitFactory.cs +++ b/src/GameKit/GameKitFactory.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Runtime.InteropServices; using GameKit.Gpu; using GameKit.Input; @@ -6,7 +7,7 @@ namespace GameKit; -public class GameKitFactory: IDisposable +public class GameKitFactory : IDisposable { private static readonly Size DefaultSize = (640, 480); internal const string GpuBackendEnvironmentVariable = "GK_GRAPHICS"; @@ -56,19 +57,37 @@ internal PlatformInfo CreatePlatformInfo() return new PlatformInfo(GetCurrentVideoDriver()); } - internal Window CreateWindow(GpuDevice gpuDevice, GameKitFrameContext frameContext, AppConfig config, PlatformInfo platformInfo) + internal ActivationWindow CreateActivationWindow( + GpuDevice gpuDevice, + WindowOptions options) { - return CreateWindow(gpuDevice, frameContext, platformInfo, config.Size, config.Title, config.Fullscreen, config.Resizable, config.Transparent, config.Borderless, config.AlwaysOnTop); + return CreateActivationWindow( + gpuDevice, + options.Size, + options.Title, + options.Fullscreen, + options.Resizable, + options.Transparent, + options.Borderless, + options.AlwaysOnTop); } - private Window CreateWindow(GpuDevice gpuDevice, GameKitFrameContext frameContext, PlatformInfo platformInfo, Size? size = null, string? title = null, bool fullscreen = false, bool resizable = false, bool transparent = false, bool borderless = false, bool alwaysOnTop = false) + private ActivationWindow CreateActivationWindow( + GpuDevice gpuDevice, + Size? size = null, + string? title = null, + bool fullscreen = false, + bool resizable = false, + bool transparent = false, + bool borderless = false, + bool alwaysOnTop = false) { EnsureSdlInitialized(); string windowTitle; if (title == null) { - using var process = System.Diagnostics.Process.GetCurrentProcess(); + using Process process = Process.GetCurrentProcess(); windowTitle = process.ProcessName; } else @@ -133,7 +152,7 @@ private Window CreateWindow(GpuDevice gpuDevice, GameKitFrameContext frameContex } } - return new Window(sdlWindow, gpuDevice.SdlGpuDevice, sdlWindowId, frameContext, platformInfo); + return new ActivationWindow(sdlWindow, gpuDevice.SdlGpuDevice, sdlWindowId); } internal GpuDevice CreateGpuDevice() @@ -250,11 +269,11 @@ internal GamepadService CreateGamepadService() return gamepadService; } - internal MouseService CreateMouseService(WindowManager windowManager) + internal MouseService CreateMouseService(Window window) { EnsureSdlInitialized(); - return new MouseService(IsMouseInWindow(windowManager.PrimaryWindow)); + return new MouseService(IsMouseInWindow(window)); } private static bool IsMouseInWindow(Window window) @@ -268,22 +287,24 @@ private static bool IsMouseInWindow(Window window) return false; } - return (uint)SDL3.SDL_GetWindowID(mouseFocusWindow) == window.Id; + return (uint)SDL3.SDL_GetWindowID(mouseFocusWindow) == window.RequireActivation().Id; } } - internal TextInputService CreateTextInputService(WindowManager windowManager) + internal TextInputService CreateTextInputService(Window window) { EnsureSdlInitialized(); - return new TextInputService(windowManager); + return new TextInputService(window); } - internal EventService CreateEventService(KeyboardService keyboardService, GamepadService gamepadService, MouseService mouseService, TextInputService textInputService, WindowManager windowManager, AppControl appControl) + internal EventService CreateEventService( + GamepadService gamepadService, + AppControl appControl) { EnsureSdlInitialized(); - return new EventService(keyboardService, gamepadService, mouseService, textInputService, windowManager, appControl); + return new EventService(gamepadService, appControl); } public GameKitFrameContext CreateFrameContext() diff --git a/src/GameKit/Gpu/GraphicsPipelineBuilder.cs b/src/GameKit/Gpu/GraphicsPipelineBuilder.cs index 810510b5..8b60b391 100644 --- a/src/GameKit/Gpu/GraphicsPipelineBuilder.cs +++ b/src/GameKit/Gpu/GraphicsPipelineBuilder.cs @@ -114,7 +114,7 @@ public void Reset() public class GraphicsPipelineBuilder { private readonly GpuDevice _gpuDevice; - private readonly WindowManager _windowManager; + private readonly Window? _window; private readonly IShaderLoader _shaderLoader; private PipelineBuilderInfo _info = new(); @@ -123,23 +123,29 @@ public class GraphicsPipelineBuilder /// public IShaderLoader ShaderLoader => _shaderLoader; - internal GraphicsPipelineBuilder(GpuDevice gpuDevice, WindowManager windowManager, IShaderLoader shaderLoader) + internal GraphicsPipelineBuilder(GpuDevice gpuDevice, Window? window, IShaderLoader shaderLoader) { _gpuDevice = gpuDevice; - _windowManager = windowManager; + _window = window; _shaderLoader = shaderLoader; } public GraphicsPipelineBuilder AddColorFormatFromDisplay(in BlendingState? blendingState = null, ColorComponentFlags? colorWriteMask = null) { - AddColorTarget(_windowManager.PrimaryWindow.ColorTargetFormat, blendingState, colorWriteMask); + if (_window == null) + { + throw new InvalidOperationException( + "AddColorFormatFromDisplay requires a reachable active Window. Supply a texture format explicitly from a windowless service container."); + } + + AddColorTarget(_window.RequireActivation().ColorTargetFormat, blendingState, colorWriteMask); return this; } public GraphicsPipelineBuilder AddColorFormatFromDisplay(Window window, in BlendingState? blendingState = null, ColorComponentFlags? colorWriteMask = null) { - AddColorTarget(window.ColorTargetFormat, blendingState, colorWriteMask); + AddColorTarget(window.RequireActivation().ColorTargetFormat, blendingState, colorWriteMask); return this; } diff --git a/src/GameKit/Input/TextInputService.cs b/src/GameKit/Input/TextInputService.cs index cd400014..a0eb2c01 100644 --- a/src/GameKit/Input/TextInputService.cs +++ b/src/GameKit/Input/TextInputService.cs @@ -26,20 +26,20 @@ public class TextEditingEventArgs public class TextInputService : ITextInputService { - private readonly WindowManager _windowManager; + private readonly Window _window; private readonly TextInputEventArgs _textInputEventArgs = new(); private readonly TextEditingEventArgs _textEditingEventArgs = new(); private readonly PriorityEventHandlers _textInputHandlers = new(); private readonly PriorityEventHandlers _textEditingHandlers = new(); - public bool IsActive => IsActiveFor(_windowManager.PrimaryWindow); + public bool IsActive => IsActiveFor(_window); public bool IsActiveFor(Window window) { unsafe { - return SDL3.SDL_TextInputActive(window.SdlWindow); + return SDL3.SDL_TextInputActive(window.RequireActivation().SdlWindow); } } @@ -67,33 +67,33 @@ public void SubscribeTextEditing(int priority, TextEditingHandler handler) public void Start() { - Start(_windowManager.PrimaryWindow); + Start(_window); } public void Start(Window window) { unsafe { - SDL3.SDL_StartTextInput(window.SdlWindow); + SDL3.SDL_StartTextInput(window.RequireActivation().SdlWindow); } } public void Stop() { - Stop(_windowManager.PrimaryWindow); + Stop(_window); } public void Stop(Window window) { unsafe { - SDL3.SDL_StopTextInput(window.SdlWindow); + SDL3.SDL_StopTextInput(window.RequireActivation().SdlWindow); } } - internal TextInputService(WindowManager windowManager) + internal TextInputService(Window window) { - _windowManager = windowManager; + _window = window; } internal void OnTextInputEvent(in SDL_TextInputEvent textInputEvent) diff --git a/src/GameKit/Window.cs b/src/GameKit/Window.cs index a0178ded..58da6d47 100644 --- a/src/GameKit/Window.cs +++ b/src/GameKit/Window.cs @@ -1,7 +1,8 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using GameKit.App; using GameKit.Content; -using GameKit.Gpu; +using GameKit.DependencyInjection; using GameKit.Utilities; using SDL; @@ -11,54 +12,65 @@ namespace GameKit; public delegate void ResolutionChangedHandler(ResolutionChangedEventArgs eventArgs); -public class Window : IDisposable +public sealed class Window : IDisposable { - internal Pointer SdlGpuDevice { get; } - internal Pointer SdlWindow { get; private set; } private readonly GameKitFrameContext _frameContext; private readonly PlatformInfo _platformInfo; - - public uint Id { get; } + private readonly WindowManager _windowManager; private ShortSize _lastSize; public event ResolutionChangedHandler? ResolutionChanged; - - internal Window(Pointer sdlWindow, Pointer sdlSdlGpuDevice, uint id, GameKitFrameContext frameContext, PlatformInfo platformInfo) + public ActivationWindow? Activation { get; internal set; } + public bool StopGameOnClose { get; set; } + + internal Window( + ActivationWindow activation, + GameKitFrameContext frameContext, + PlatformInfo platformInfo, + WindowManager windowManager, + bool stopGameOnClose) { - SdlGpuDevice = sdlSdlGpuDevice; - SdlWindow = sdlWindow; - Id = id; + Activation = activation; _frameContext = frameContext; _platformInfo = platformInfo; - _lastSize = RenderSizeInPixels; + _windowManager = windowManager; + _lastSize = activation.RenderSizeInPixels; + StopGameOnClose = stopGameOnClose; + } + + internal static Window Create( + ServiceProvider serviceProvider, + ActivationWindow activation, + GameKitFrameContext frameContext, + PlatformInfo platformInfo, + WindowManager windowManager, + WindowOptions options) + { + Window window = new( + activation, + frameContext, + platformInfo, + windowManager, + options.StopGameOnClose); + windowManager.Attach(window, serviceProvider); + return window; } internal void OnPixelSizeChanged(ulong timestamp) { - ShortSize newSize = RenderSizeInPixels; + ShortSize newSize = RequireActivation().RenderSizeInPixels; ShortSize oldSize = _lastSize; - if (newSize == oldSize) return; + if (newSize == oldSize) + { + return; + } _lastSize = newSize; ResolutionChanged?.Invoke(new ResolutionChangedEventArgs(oldSize, newSize, timestamp)); } - public ShortSize RenderSizeInPixels - { - get - { - int width, height; - unsafe - { - SDL3.SDL_GetWindowSizeInPixels(SdlWindow, &width, &height); - } - - return new ShortSize((ushort)width, (ushort)height); - } - } - public Size Size { get @@ -66,7 +78,7 @@ public Size Size int width, height; unsafe { - SDL3.SDL_GetWindowSize(SdlWindow, &width, &height); + SDL3.SDL_GetWindowSize(RequireActivation().SdlWindow, &width, &height); } return new Size((uint)width, (uint)height); } @@ -74,18 +86,7 @@ public Size Size { unsafe { - SDL3.SDL_SetWindowSize(SdlWindow, (int)value.Width, (int)value.Height); - } - } - } - - public TextureFormat ColorTargetFormat - { - get - { - unsafe - { - return (TextureFormat)SDL3.SDL_GetGPUSwapchainTextureFormat(SdlGpuDevice, SdlWindow); + SDL3.SDL_SetWindowSize(RequireActivation().SdlWindow, (int)value.Width, (int)value.Height); } } } @@ -96,14 +97,14 @@ public bool MouseGrab { unsafe { - return SDL3.SDL_GetWindowMouseGrab(SdlWindow); + return SDL3.SDL_GetWindowMouseGrab(RequireActivation().SdlWindow); } } set { unsafe { - SDL3.SDL_SetWindowMouseGrab(SdlWindow, value); + SDL3.SDL_SetWindowMouseGrab(RequireActivation().SdlWindow, value); } } } @@ -114,14 +115,14 @@ public bool WindowRelativeMouseMode { unsafe { - return SDL3.SDL_GetWindowRelativeMouseMode(SdlWindow); + return SDL3.SDL_GetWindowRelativeMouseMode(RequireActivation().SdlWindow); } } set { unsafe { - SDL3.SDL_SetWindowRelativeMouseMode(SdlWindow, value); + SDL3.SDL_SetWindowRelativeMouseMode(RequireActivation().SdlWindow, value); } } } @@ -156,14 +157,14 @@ public bool AlwaysOnTop { unsafe { - return (SDL3.SDL_GetWindowFlags(SdlWindow) & SDL_WindowFlags.SDL_WINDOW_ALWAYS_ON_TOP) != 0; + return (SDL3.SDL_GetWindowFlags(RequireActivation().SdlWindow) & SDL_WindowFlags.SDL_WINDOW_ALWAYS_ON_TOP) != 0; } } set { unsafe { - if (SDL3.SDL_SetWindowAlwaysOnTop(SdlWindow, value) == false) + if (SDL3.SDL_SetWindowAlwaysOnTop(RequireActivation().SdlWindow, value) == false) { throw new GameKitException($"SDL_SetWindowAlwaysOnTop failed: {SDL3.SDL_GetError()}"); } @@ -185,11 +186,11 @@ public bool Draggable if (value) { ClearHitTestCallback(); - SDL3.SDL_SetWindowHitTest(SdlWindow, &HitTestDraggable, IntPtr.Zero); + SDL3.SDL_SetWindowHitTest(RequireActivation().SdlWindow, &HitTestDraggable, IntPtr.Zero); } else { - SDL3.SDL_SetWindowHitTest(SdlWindow, null, IntPtr.Zero); + SDL3.SDL_SetWindowHitTest(RequireActivation().SdlWindow, null, IntPtr.Zero); } } _draggable = value; @@ -205,8 +206,8 @@ public void SetHitTest(Func? callback) { unsafe { - SDL3.SDL_SetWindowHitTest(SdlWindow, null, IntPtr.Zero); - SDL3.SDL_SetWindowShape(SdlWindow, null); + SDL3.SDL_SetWindowHitTest(RequireActivation().SdlWindow, null, IntPtr.Zero); + SDL3.SDL_SetWindowShape(RequireActivation().SdlWindow, null); } return; } @@ -216,7 +217,7 @@ public void SetHitTest(Func? callback) _hitTestHandle = GCHandle.Alloc(this); unsafe { - SDL3.SDL_SetWindowHitTest(SdlWindow, &HitTestCallback, GCHandle.ToIntPtr(_hitTestHandle)); + SDL3.SDL_SetWindowHitTest(RequireActivation().SdlWindow, &HitTestCallback, GCHandle.ToIntPtr(_hitTestHandle)); } } @@ -239,7 +240,7 @@ private unsafe void ApplyHitTestShape(Func callback) } } - SDL3.SDL_SetWindowShape(SdlWindow, surface); + SDL3.SDL_SetWindowShape(RequireActivation().SdlWindow, surface); SDL3.SDL_DestroySurface(surface); } @@ -275,7 +276,7 @@ public Vector2Int Position int x, y; unsafe { - SDL3.SDL_GetWindowPosition(SdlWindow, &x, &y); + SDL3.SDL_GetWindowPosition(RequireActivation().SdlWindow, &x, &y); } return new Vector2Int(x, y); } @@ -283,42 +284,16 @@ public Vector2Int Position { unsafe { - SDL3.SDL_SetWindowPosition(SdlWindow, value.X, value.Y); - } - } - } - - public bool TryWaitAndAcquireSwapchainTexture(CommandBuffer commandBuffer, out SwapchainTexture swapchainTexture) - { - swapchainTexture = default!; - uint width, height; - - unsafe - { - SDL_GPUTexture* swapchainTexturePointer; - if (SDL3.SDL_WaitAndAcquireGPUSwapchainTexture(commandBuffer.SdlGpuCommandBuffer, SdlWindow, &swapchainTexturePointer, &width, &height) == false) - { - throw new GameKitInitializationException($"SDL_WaitAndAcquireGPUSwapchainTexture failed: {SDL3.SDL_GetError()}"); - } - - if (swapchainTexturePointer == null) - { - return false; + SDL3.SDL_SetWindowPosition(RequireActivation().SdlWindow, value.X, value.Y); } - - TextureFormat textureFormat = (TextureFormat)SDL3.SDL_GetGPUSwapchainTextureFormat(SdlGpuDevice, SdlWindow); - - swapchainTexture = new SwapchainTexture(swapchainTexturePointer, new ShortSize((ushort)width, (ushort)height), textureFormat); } - - return true; } public void SetFullscreenBorderless(bool fullscreen) { unsafe { - SDL3.SDL_SetWindowFullscreen(SdlWindow, fullscreen); + SDL3.SDL_SetWindowFullscreen(RequireActivation().SdlWindow, fullscreen); } } @@ -347,7 +322,7 @@ public void SetIcon(Image icon) try { - if (!SDL3.SDL_SetWindowIcon(SdlWindow, surface)) + if (!SDL3.SDL_SetWindowIcon(RequireActivation().SdlWindow, surface)) { throw new GameKitException($"SDL_SetWindowIcon failed: {SDL3.SDL_GetError()}"); } @@ -418,7 +393,7 @@ private unsafe FileDialogResult ShowModalFileDialog( SDL3.SDL_ShowOpenFileDialog( &OnFileDialogCompleted, userdata, - SdlWindow, + RequireActivation().SdlWindow, actualFiltersPointer, nativeFilters.Filters.Length, (byte*)defaultLocationPointer, @@ -429,7 +404,7 @@ private unsafe FileDialogResult ShowModalFileDialog( SDL3.SDL_ShowSaveFileDialog( &OnFileDialogCompleted, userdata, - SdlWindow, + RequireActivation().SdlWindow, actualFiltersPointer, nativeFilters.Filters.Length, (byte*)defaultLocationPointer); @@ -490,20 +465,17 @@ private static unsafe FileDialogResult CreateFileDialogResult(byte** fileList) return FileDialogResult.Accepted(paths); } - public override int GetHashCode() + public void Dispose() { - return Id.GetHashCode(); + _windowManager?.Detach(this); + ClearHitTestCallback(); + Activation = null; } - public void Dispose() + // TODO: Replace active-only callers with skip, retained-state, or explicit-failure semantics when window deactivation is implemented. + public ActivationWindow RequireActivation() { - ClearHitTestCallback(); - unsafe - { - SDL3.SDL_ReleaseWindowFromGPUDevice(SdlGpuDevice, SdlWindow); - SDL3.SDL_DestroyWindow(SdlWindow); - SdlWindow = null; - } + return Activation ?? throw new InvalidOperationException("The window is not active."); } private sealed class ModalFileDialogState diff --git a/src/GameKit/WindowEventService.cs b/src/GameKit/WindowEventService.cs new file mode 100644 index 00000000..2df28fed --- /dev/null +++ b/src/GameKit/WindowEventService.cs @@ -0,0 +1,113 @@ +using GameKit.Input; +using SDL; + +namespace GameKit; + +internal sealed class WindowEventService : IDisposable +{ + private readonly KeyboardService _keyboard; + private readonly MouseService _mouse; + private readonly TextInputService _textInput; + private readonly AppControl _appControl; + private readonly EventService _eventService; + private readonly WindowManager _windowManager; + + internal WindowEventService( + Window window, + KeyboardService keyboard, + MouseService mouse, + TextInputService textInput, + WindowManager windowManager, + AppControl appControl, + EventService eventService) + { + Window = window; + _keyboard = keyboard; + _mouse = mouse; + _textInput = textInput; + _windowManager = windowManager; + _appControl = appControl; + _eventService = eventService; + } + + internal Window Window { get; } + + internal static WindowEventService Create( + Window window, + KeyboardService keyboard, + MouseService mouse, + TextInputService textInput, + WindowManager windowManager, + AppControl appControl, + EventService eventService) + { + WindowEventService windowEvents = new( + window, + keyboard, + mouse, + textInput, + windowManager, + appControl, + eventService); + eventService.Attach(windowEvents); + return windowEvents; + } + + internal void Process(in SDL_Event evt) + { + if (evt.Type == SDL_EventType.SDL_EVENT_KEY_DOWN || + evt.Type == SDL_EventType.SDL_EVENT_KEY_UP) + { + _keyboard.OnKeyEvent(evt.key); + } + else if (evt.Type == SDL_EventType.SDL_EVENT_MOUSE_BUTTON_DOWN || + evt.Type == SDL_EventType.SDL_EVENT_MOUSE_BUTTON_UP) + { + _mouse.OnMouseButtonEvent(evt.button); + } + else if (evt.Type == SDL_EventType.SDL_EVENT_MOUSE_MOTION) + { + _mouse.OnMouseMotionEvent(evt.motion); + } + else if (evt.Type == SDL_EventType.SDL_EVENT_MOUSE_WHEEL) + { + _mouse.OnMouseWheelEvent(evt.wheel); + } + else if (evt.Type == SDL_EventType.SDL_EVENT_WINDOW_MOUSE_ENTER) + { + _mouse.OnMouseWindowPresenceEvent(evt.window, true); + } + else if (evt.Type == SDL_EventType.SDL_EVENT_WINDOW_MOUSE_LEAVE) + { + _mouse.OnMouseWindowPresenceEvent(evt.window, false); + } + else if (evt.Type == SDL_EventType.SDL_EVENT_TEXT_INPUT) + { + _textInput.OnTextInputEvent(evt.text); + } + else if (evt.Type == SDL_EventType.SDL_EVENT_TEXT_EDITING) + { + _textInput.OnTextEditingEvent(evt.edit); + } + else if (evt.Type == SDL_EventType.SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED) + { + Window.OnPixelSizeChanged(evt.window.timestamp); + } + else if (evt.Type == SDL_EventType.SDL_EVENT_WINDOW_CLOSE_REQUESTED) + { + if (Window.StopGameOnClose) + { + _appControl.Quit(); + } + else + { + _windowManager.DestroyWindow(Window); + } + } + } + + public void Dispose() + { + _eventService.Detach(this); + } +} diff --git a/src/GameKit/WindowManager.cs b/src/GameKit/WindowManager.cs index ef97d662..46ad5ac7 100644 --- a/src/GameKit/WindowManager.cs +++ b/src/GameKit/WindowManager.cs @@ -1,59 +1,114 @@ -using GameKit.Gpu; +using GameKit.DependencyInjection; namespace GameKit; -public class WindowManager : IDisposable +public sealed class WindowManager { - private readonly GameKitFactory _factory; - private readonly GpuDevice _gpuDevice; - private readonly GameKitFrameContext _frameContext; - private readonly PlatformInfo _platformInfo; - private readonly Dictionary _windowsById = new(); + private readonly ServiceProvider _applicationProvider; + private (Window Window, ServiceProvider Provider)[] _windowOwners = []; private readonly List _windows = new(); + private readonly List _pendingDisposals = new(); + + internal WindowManager(ServiceProvider applicationProvider) + { + _applicationProvider = applicationProvider; + } - public Window PrimaryWindow { get; } public IReadOnlyList Windows => _windows; - public WindowManager(GameKitFactory factory, GpuDevice gpuDevice, GameKitFrameContext frameContext, AppConfig config, PlatformInfo platformInfo) + public void DestroyWindow(Window window) { - _factory = factory; - _gpuDevice = gpuDevice; - _frameContext = frameContext; - _platformInfo = platformInfo; - - PrimaryWindow = factory.CreateWindow(gpuDevice, frameContext, config, platformInfo); - _windows.Add(PrimaryWindow); - _windowsById.Add(PrimaryWindow.Id, PrimaryWindow); + ArgumentNullException.ThrowIfNull(window); + + ServiceProvider? provider = null; + for (int i = 0; i < _windowOwners.Length; i++) + { + if (ReferenceEquals(_windowOwners[i].Window, window)) + { + provider = _windowOwners[i].Provider; + break; + } + } + + if (provider == null) + { + throw new InvalidOperationException("The window is not attached to the application."); + } + + if (ReferenceEquals(provider, _applicationProvider)) + { + throw new InvalidOperationException( + "A window owned by the application service container cannot be closed independently. Use StopGame close behavior."); + } + + QueueDisposal(provider); } - public Window CreateWindow(WindowOptions options) + internal void Attach(Window window, ServiceProvider provider) { - AppConfig config = new(options.Size, options.Title, null, options.Fullscreen, options.Resizable, options.Transparent, options.Borderless, options.AlwaysOnTop); - Window window = _factory.CreateWindow(_gpuDevice, _frameContext, config, _platformInfo); + for (int i = 0; i < _windowOwners.Length; i++) + { + if (ReferenceEquals(_windowOwners[i].Window, window)) + { + throw new InvalidOperationException("The window is already attached."); + } + } + + _windowOwners = [.. _windowOwners, (window, provider)]; _windows.Add(window); - _windowsById.Add(window.Id, window); - return window; } - public void DestroyWindow(Window window) + internal void Detach(Window window) { - if (window == PrimaryWindow) + int index = -1; + for (int i = 0; i < _windowOwners.Length; i++) + { + if (ReferenceEquals(_windowOwners[i].Window, window)) + { + index = i; + break; + } + } + + if (index < 0) { - throw new InvalidOperationException("Cannot destroy the primary window."); + return; } - _windowsById.Remove(window.Id); + int itemsToMove = _windowOwners.Length - index - 1; + if (itemsToMove > 0) + { + Array.Copy(_windowOwners, index + 1, _windowOwners, index, itemsToMove); + } + + Array.Resize(ref _windowOwners, _windowOwners.Length - 1); _windows.Remove(window); - window.Dispose(); } - internal bool TryGetWindow(uint windowId, out Window window) + internal void ApplyPendingDisposals() { - return _windowsById.TryGetValue(windowId, out window!); + if (_pendingDisposals.Count > 0) + { + ServiceProvider[] providers = _pendingDisposals.ToArray(); + _pendingDisposals.Clear(); + + for (int i = 0; i < providers.Length; i++) + { + providers[i].Dispose(); + } + } } - public void Dispose() + internal void QueueDisposal(ServiceProvider provider) { - PrimaryWindow.Dispose(); + for (int i = 0; i < _pendingDisposals.Count; i++) + { + if (ReferenceEquals(_pendingDisposals[i], provider)) + { + return; + } + } + + _pendingDisposals.Add(provider); } } diff --git a/src/GameKit/WindowOptions.cs b/src/GameKit/WindowOptions.cs index 6554e19c..70bdcb7b 100644 --- a/src/GameKit/WindowOptions.cs +++ b/src/GameKit/WindowOptions.cs @@ -7,4 +7,5 @@ public sealed record WindowOptions( bool Resizable = false, bool Transparent = false, bool Borderless = false, - bool AlwaysOnTop = false); + bool AlwaysOnTop = false, + bool StopGameOnClose = true); diff --git a/src/GameKit/WindowServiceCollectionExtensions.cs b/src/GameKit/WindowServiceCollectionExtensions.cs new file mode 100644 index 00000000..ab7e1fc6 --- /dev/null +++ b/src/GameKit/WindowServiceCollectionExtensions.cs @@ -0,0 +1,49 @@ +using GameKit.App; +using GameKit.DependencyInjection; +using GameKit.Input; + +namespace GameKit; + +public static class WindowServiceCollectionExtensions +{ + public static ServiceCollection AddWindow( + this ServiceCollection services, + WindowOptions options) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(options); + + // TODO: Future window deactivation may allow application-owned windows to close independently. + if (services is GameKitAppBuilder && + !options.StopGameOnClose) + { + throw new InvalidOperationException( + "A window owned by the application service container cannot be closed independently. Set StopGameOnClose to true."); + } + + if (services.IsRegistered()) + { + throw new InvalidOperationException( + "A window is already registered in this service hierarchy."); + } + + AddWindowServices(services, options); + return services; + } + + private static void AddWindowServices(ServiceCollection services, WindowOptions options) + { + services.AddSingleton(options); + services.AddSingleton(); + services.AddSingleton(Window.Create); + + services.AddSingleton(); + services.AddAlias(); + services.AddSingleton(); + services.AddAlias(); + services.AddSingleton(); + services.AddAlias(); + + services.AddSingleton(WindowEventService.Create); + } +} diff --git a/tests/GameKit.Tests/DefaultRenderManagerTests.cs b/tests/GameKit.Tests/DefaultRenderCoordinatorTests.cs similarity index 75% rename from tests/GameKit.Tests/DefaultRenderManagerTests.cs rename to tests/GameKit.Tests/DefaultRenderCoordinatorTests.cs index 96e38a4e..c4d17160 100644 --- a/tests/GameKit.Tests/DefaultRenderManagerTests.cs +++ b/tests/GameKit.Tests/DefaultRenderCoordinatorTests.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using GameKit.App; using GameKit.DependencyInjection; using GameKit.Gpu; @@ -6,16 +7,16 @@ namespace GameKit.Tests; -public class DefaultRenderManagerTests +public class DefaultRenderCoordinatorTests { [Test] public void Execute_WithNoRenderPhases_DoesNotThrow() { GameKitAppBuilder builder = CreateBuilder(new List()); ServiceProvider provider = builder.BuildServiceProvider(); - IRenderManager renderManager = provider.GetRequiredService(); + RenderCoordinator renderCoordinator = provider.GetRequiredService(); - Assert.DoesNotThrow(renderManager.Execute); + Assert.DoesNotThrow(renderCoordinator.Execute); } [Test] @@ -24,31 +25,53 @@ public void ChildProviderRenderPhase_IsRenderedAfterChildBuild() List calls = new(); GameKitAppBuilder builder = CreateBuilder(calls); ServiceProvider parent = builder.BuildServiceProvider(); - IRenderManager renderManager = parent.GetRequiredService(); + RenderCoordinator renderCoordinator = parent.GetRequiredService(); ServiceCollection childCollection = parent.CreateServiceCollection(); childCollection.AddSingleton>(new TestRenderPhase("child", calls)); using ServiceProvider child = childCollection.BuildServiceProvider(); - renderManager.Execute(); + renderCoordinator.Execute(); Assert.That(calls, Is.EqualTo(new[] { "child" })); } + [Test] + public void AncestorProviderRenderPhase_IsNotAttachedToChildRenderCoordinator() + { + List calls = new(); + ServiceCollection ancestorServices = new(); + ancestorServices.AddSingleton>( + new TestRenderPhase("ancestor", calls)); + using ServiceProvider ancestor = ancestorServices.BuildServiceProvider(); + + ServiceCollection windowServices = ancestor.CreateServiceCollection(); + windowServices.AddSingleton((Window)RuntimeHelpers.GetUninitializedObject(typeof(Window))); + windowServices.UseDefaultRenderCoordinator(); + windowServices.AddSingleton>( + new TestRenderContextProvider()); + windowServices.AddSingleton(new GpuMemorySystem(null!)); + using ServiceProvider windowProvider = windowServices.BuildServiceProvider(); + + windowProvider.GetRequiredService().Execute(); + + Assert.That(calls, Is.Empty); + } + [Test] public void ChildProviderRenderPhase_IsRemovedWhenChildProviderIsDisposed() { List calls = new(); GameKitAppBuilder builder = CreateBuilder(calls); ServiceProvider parent = builder.BuildServiceProvider(); - IRenderManager renderManager = parent.GetRequiredService(); + RenderCoordinator renderCoordinator = parent.GetRequiredService(); ServiceCollection childCollection = parent.CreateServiceCollection(); childCollection.AddSingleton>(new TestRenderPhase("child", calls)); ServiceProvider child = childCollection.BuildServiceProvider(); child.Dispose(); - renderManager.Execute(); + renderCoordinator.Execute(); Assert.That(calls, Is.Empty); } @@ -60,13 +83,13 @@ public void DynamicRenderPhases_AreRenderedInOrder() GameKitAppBuilder builder = CreateBuilder(calls); builder.AddSingleton>(new TestRenderPhase("root", calls, 10)); ServiceProvider parent = builder.BuildServiceProvider(); - IRenderManager renderManager = parent.GetRequiredService(); + RenderCoordinator renderCoordinator = parent.GetRequiredService(); ServiceCollection childCollection = parent.CreateServiceCollection(); childCollection.AddSingleton>(new TestRenderPhase("child", calls, 5)); using ServiceProvider child = childCollection.BuildServiceProvider(); - renderManager.Execute(); + renderCoordinator.Execute(); Assert.That(calls, Is.EqualTo(new[] { "child", "root" })); } @@ -78,14 +101,14 @@ public void ChildProviderDisposeDuringRender_DoesNotSkipRemainingRootRenderPhase GameKitAppBuilder builder = CreateBuilder(calls); builder.AddSingleton>(new TestRenderPhase("root", calls, 10)); ServiceProvider parent = builder.BuildServiceProvider(); - IRenderManager renderManager = parent.GetRequiredService(); + RenderCoordinator renderCoordinator = parent.GetRequiredService(); ServiceProvider? child = null; ServiceCollection childCollection = parent.CreateServiceCollection(); childCollection.AddSingleton>(new DisposingRenderPhase("child", calls, () => child!, 0)); child = childCollection.BuildServiceProvider(); - renderManager.Execute(); + renderCoordinator.Execute(); Assert.That(calls, Is.EqualTo(new[] { "child", "root" })); } @@ -98,14 +121,14 @@ public void ChildProviderBuildDuringRender_AddsRenderPhaseForNextFrame() ServiceProvider? parent = null; builder.AddSingleton>(new ChildProviderBuildingRenderPhase("root", calls, () => parent!, 0)); parent = builder.BuildServiceProvider(); - IRenderManager renderManager = parent.GetRequiredService(); + RenderCoordinator renderCoordinator = parent.GetRequiredService(); - renderManager.Execute(); + renderCoordinator.Execute(); Assert.That(calls, Is.EqualTo(new[] { "root" })); calls.Clear(); - renderManager.Execute(); + renderCoordinator.Execute(); Assert.That(calls, Is.EqualTo(new[] { "child", "root" })); } @@ -113,7 +136,8 @@ public void ChildProviderBuildDuringRender_AddsRenderPhaseForNextFrame() private static GameKitAppBuilder CreateBuilder(List calls) { GameKitAppBuilder builder = new(); - builder.UseDefaultRenderManager(); + builder.AddSingleton((Window)RuntimeHelpers.GetUninitializedObject(typeof(Window))); + builder.UseDefaultRenderCoordinator(); builder.AddSingleton>(new TestRenderContextProvider()); builder.AddSingleton(new GpuMemorySystem(null!)); builder.AddSingleton(calls); diff --git a/tests/GameKit.Tests/EventServiceTests.cs b/tests/GameKit.Tests/EventServiceTests.cs new file mode 100644 index 00000000..f4837323 --- /dev/null +++ b/tests/GameKit.Tests/EventServiceTests.cs @@ -0,0 +1,152 @@ +using System.Runtime.CompilerServices; +using GameKit.DependencyInjection; +using GameKit.Input; + +namespace GameKit.Tests; + +public sealed class EventServiceTests +{ + [Test] + public void WindowEventService_CreateAndDispose_AttachesAndDetaches() + { + using ServiceProvider provider = new ServiceCollection().BuildServiceProvider(); + WindowManager windowManager = new(provider); + EventService eventService = new(new GamepadService(), new AppControl()); + Window window = CreateWindow(42); + AppControl appControl = new(); + + WindowEventService windowEvents = WindowEventService.Create( + window, + new KeyboardService(appControl), + new MouseService(), + new TextInputService(window), + windowManager, + appControl, + eventService); + + Assert.That( + eventService.TryGetWindowEventService(42, out WindowEventService attached), + Is.True); + Assert.That(attached, Is.SameAs(windowEvents)); + + windowEvents.Dispose(); + + Assert.That(eventService.TryGetWindowEventService(42, out _), Is.False); + } + + [Test] + public void TryGetWindowEventService_WithSparseWindowIds_FindsAttachedServices() + { + using ServiceProvider provider = new ServiceCollection().BuildServiceProvider(); + WindowManager windowManager = new(provider); + EventService eventService = new(new GamepadService(), new AppControl()); + WindowEventService first = CreateWindowEventService(eventService, windowManager, 3); + WindowEventService second = CreateWindowEventService(eventService, windowManager, 1_000_000_000); + + eventService.Attach(first); + eventService.Attach(second); + + Assert.Multiple(() => + { + Assert.That( + eventService.TryGetWindowEventService(3, out WindowEventService foundFirst), + Is.True); + Assert.That(foundFirst, Is.SameAs(first)); + Assert.That( + eventService.TryGetWindowEventService( + 1_000_000_000, + out WindowEventService foundSecond), + Is.True); + Assert.That(foundSecond, Is.SameAs(second)); + Assert.That(eventService.TryGetWindowEventService(4, out _), Is.False); + }); + } + + [Test] + public void Detach_RemovesOnlyMatchingWindowEventService() + { + using ServiceProvider provider = new ServiceCollection().BuildServiceProvider(); + WindowManager windowManager = new(provider); + EventService eventService = new(new GamepadService(), new AppControl()); + WindowEventService first = CreateWindowEventService(eventService, windowManager, 10); + WindowEventService second = CreateWindowEventService(eventService, windowManager, 20); + WindowEventService third = CreateWindowEventService(eventService, windowManager, 30); + eventService.Attach(first); + eventService.Attach(second); + eventService.Attach(third); + + eventService.Detach(second); + + Assert.Multiple(() => + { + Assert.That( + eventService.TryGetWindowEventService(10, out WindowEventService foundFirst), + Is.True); + Assert.That(foundFirst, Is.SameAs(first)); + Assert.That(eventService.TryGetWindowEventService(20, out _), Is.False); + Assert.That( + eventService.TryGetWindowEventService(30, out WindowEventService foundThird), + Is.True); + Assert.That(foundThird, Is.SameAs(third)); + }); + } + + [Test] + public void Attach_WithDuplicateWindowId_Throws() + { + using ServiceProvider provider = new ServiceCollection().BuildServiceProvider(); + WindowManager windowManager = new(provider); + EventService eventService = new(new GamepadService(), new AppControl()); + WindowEventService first = CreateWindowEventService(eventService, windowManager, 10); + WindowEventService second = CreateWindowEventService(eventService, windowManager, 10); + eventService.Attach(first); + + InvalidOperationException exception = Assert.Throws(() => + eventService.Attach(second))!; + + Assert.That(exception.Message, Does.Contain("10")); + } + + [Test] + public void WindowManager_AttachAndDetach_TracksWindowOwnership() + { + using ServiceProvider provider = new ServiceCollection().BuildServiceProvider(); + WindowManager windowManager = new(provider); + EventService eventService = new(new GamepadService(), new AppControl()); + WindowEventService windowEvents = CreateWindowEventService(eventService, windowManager, 10); + + windowManager.Attach(windowEvents.Window, provider); + + Assert.That(windowManager.Windows, Is.EqualTo(new[] { windowEvents.Window })); + + windowManager.Detach(windowEvents.Window); + + Assert.That(windowManager.Windows, Is.Empty); + } + + private static WindowEventService CreateWindowEventService( + EventService eventService, + WindowManager windowManager, + uint windowId) + { + Window window = CreateWindow(windowId); + AppControl appControl = new(); + + return new WindowEventService( + window, + new KeyboardService(appControl), + new MouseService(), + new TextInputService(window), + windowManager, + appControl, + eventService); + } + + private static Window CreateWindow(uint windowId) + { + ActivationWindow activation = new(default, default, windowId); + Window window = (Window)RuntimeHelpers.GetUninitializedObject(typeof(Window)); + window.Activation = activation; + return window; + } +} diff --git a/tests/GameKit.Tests/MultiWindowContainerTests.cs b/tests/GameKit.Tests/MultiWindowContainerTests.cs new file mode 100644 index 00000000..93a95e7c --- /dev/null +++ b/tests/GameKit.Tests/MultiWindowContainerTests.cs @@ -0,0 +1,143 @@ +using System.Runtime.CompilerServices; +using GameKit.App; +using GameKit.DependencyInjection; +using GameKit.Gpu; +using GameKit.RenderOrchestration; + +namespace GameKit.Tests; + +public sealed class MultiWindowContainerTests +{ + [Test] + public void WindowOptions_StopGameOnCloseByDefault() + { + WindowOptions options = new(); + + Assert.That(options.StopGameOnClose, Is.True); + } + + [Test] + public void RootWindow_WithoutStopGameOnClose_ThrowsDuringRegistration() + { + GameKitAppBuilder builder = new(); + WindowOptions options = new(StopGameOnClose: false); + + InvalidOperationException exception = Assert.Throws(() => + builder.AddWindow(options))!; + + Assert.That(exception.Message, Does.Contain(nameof(WindowOptions.StopGameOnClose))); + } + + [Test] + public void WindowlessRoot_WithDefaultRenderCoordinator_ThrowsBeforeSdlInitialization() + { + GameKitAppBuilder builder = new GameKitAppBuilder() + .UseDefaultRenderCoordinator(); + + InvalidOperationException exception = Assert.Throws(() => builder.Build())!; + + Assert.That(exception.Message, Does.Contain(nameof(Window))); + } + + [Test] + public void AddWindow_WithSecondLocalWindow_ThrowsDuringRegistration() + { + ServiceCollection services = new(); + services.AddWindow(new WindowOptions()); + + InvalidOperationException exception = Assert.Throws(() => + services.AddWindow(new WindowOptions()))!; + + Assert.That(exception.Message, Does.Contain("already registered")); + } + + [Test] + public void AddWindow_WithInheritedWindow_ThrowsDuringRegistration() + { + ServiceCollection rootServices = new(); + rootServices.AddSingleton((Window)RuntimeHelpers.GetUninitializedObject(typeof(Window))); + using ServiceProvider rootProvider = rootServices.BuildServiceProvider(); + + ServiceCollection childServices = rootProvider.CreateServiceCollection(); + + InvalidOperationException exception = Assert.Throws(() => + childServices.AddWindow(new WindowOptions()))!; + + Assert.That(exception.Message, Does.Contain("already registered")); + } + + [Test] + public void BuildServiceProvider_WithApp_UsesAndIsOwnedByAppProvider() + { + ServiceCollection rootServices = new(); + RootService rootService = new(); + rootServices.AddSingleton(rootService); + ServiceProvider rootProvider = rootServices.BuildServiceProvider(); + FakeApp app = new(rootProvider); + + ServiceCollection childServices = app.CreateServiceCollection(); + DisposableChildService childService = new(); + childServices.AddSingleton(childService); + ServiceProvider childProvider = childServices.BuildServiceProvider(); + + Assert.That(childProvider.GetRequiredService(), Is.SameAs(rootService)); + + app.Dispose(); + + Assert.That(childService.IsDisposed, Is.True); + } + + private sealed class RootService + { + } + + private sealed class DisposableChildService : IDisposable + { + public bool IsDisposed { get; private set; } + + public void Dispose() + { + IsDisposed = true; + } + } + + private sealed class FakeApp : IGameKitApp + { + public FakeApp(ServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + + public ServiceProvider ServiceProvider { get; } + + public ServiceCollection CreateServiceCollection() + { + return ServiceProvider.CreateServiceCollection(); + } + + public T GetRequiredService() where T : class + { + return ServiceProvider.GetRequiredService(); + } + + public int Run() + { + throw new NotSupportedException(); + } + + public void Dispose() + { + ServiceProvider.Dispose(); + } + } + + private sealed class TestRenderContext : IRenderContext + { + public CommandBuffer CommandBuffer => null!; + public Texture ColorTarget => null!; + + public void Dispose() + { + } + } +} diff --git a/tests/GameKit.Tests/PencilTextTests.cs b/tests/GameKit.Tests/PencilTextTests.cs index 1347b323..1ea42608 100644 --- a/tests/GameKit.Tests/PencilTextTests.cs +++ b/tests/GameKit.Tests/PencilTextTests.cs @@ -34,7 +34,7 @@ public void MeasureText_WithEmptyString_ReturnsZeroAndDoesNotCallFontSystem() private static Pencil CreatePencil() { - return new Pencil(new ThrowingFontSystem(), new TestClipboardService(), GuiStyles.Style, new AppConfig()); + return new Pencil(new ThrowingFontSystem(), new TestClipboardService(), GuiStyles.Style); } private sealed class ThrowingFontSystem : IFontSystem diff --git a/tests/GameKit.Tests/PencilViewportTests.cs b/tests/GameKit.Tests/PencilViewportTests.cs index 3d82832a..c2240a86 100644 --- a/tests/GameKit.Tests/PencilViewportTests.cs +++ b/tests/GameKit.Tests/PencilViewportTests.cs @@ -45,7 +45,7 @@ public void MarkInstructionsCompleted_TracksViewportUsedByCompletedInstructions( private static Pencil CreatePencil() { - return new Pencil(new ThrowingFontSystem(), new TestClipboardService(), GuiStyles.Style, new AppConfig()); + return new Pencil(new ThrowingFontSystem(), new TestClipboardService(), GuiStyles.Style); } private sealed class TestClipboardService : IClipboardService diff --git a/tests/GameKit.Tests/StageManagerTests.cs b/tests/GameKit.Tests/StageManagerTests.cs index 52eb0c74..1d831e17 100644 --- a/tests/GameKit.Tests/StageManagerTests.cs +++ b/tests/GameKit.Tests/StageManagerTests.cs @@ -162,23 +162,24 @@ public void Load_RegistersStageServicesViaParentCallbacksOnPendingTransition() public void Load_StageServicesCanResolveRootServices() { ServiceCollection rootCollection = new(); - rootCollection.AddSingleton(new AppConfig { Title = "test" }); + RootService rootService = new("test"); + rootCollection.AddSingleton(rootService); ServiceProvider root = rootCollection.BuildServiceProvider(); StageManager stageManager = new(root); - AppConfig? resolved = null; + RootService? resolved = null; stageManager.Load(services => { services.AddSingleton(sp => { - resolved = sp.GetRequiredService(); + resolved = sp.GetRequiredService(); return new TestView("stage"); }); }); stageManager.ApplyPendingTransition(); Assert.That(resolved, Is.Not.Null); - Assert.That(resolved!.Title, Is.EqualTo("test")); + Assert.That(resolved!.Value, Is.EqualTo("test")); } [Test] @@ -258,4 +259,6 @@ public void Dispose() IsDisposed = true; } } + + private sealed record RootService(string Value); } diff --git a/tutorials/GameKit.Tutorials.Audio/Program.cs b/tutorials/GameKit.Tutorials.Audio/Program.cs index 7fb64745..7fbfe58a 100644 --- a/tutorials/GameKit.Tutorials.Audio/Program.cs +++ b/tutorials/GameKit.Tutorials.Audio/Program.cs @@ -17,10 +17,10 @@ static int Main(string[] args) { GameKitAppBuilder builder = new GameKitAppBuilder() .AddContentFromProjectDirectory("Content") - .UseDefaultRenderManager() + .UseDefaultRenderCoordinator() .RegisterAudio(); - builder.AddSingleton(new AppConfig { Size = (640, 480), Title = "Audio Tutorial" }); + builder.AddWindow(new WindowOptions(Size: (640, 480), Title: "Audio Tutorial")); builder.AddSingleton, NullRenderPhase>(); builder.OnStart((IAudioSystem audioSystem, IKeyboardService keyboardService, AppControl appControl) => diff --git a/tutorials/GameKit.Tutorials.ClickThrough/Program.cs b/tutorials/GameKit.Tutorials.ClickThrough/Program.cs index a9ac65e6..4a0686fc 100644 --- a/tutorials/GameKit.Tutorials.ClickThrough/Program.cs +++ b/tutorials/GameKit.Tutorials.ClickThrough/Program.cs @@ -15,20 +15,17 @@ static int Main(string[] args) { GameKitAppBuilder builder = new GameKitAppBuilder() .AddContentFromProjectDirectory("Content") - .UseDefaultRenderManager(); + .UseDefaultRenderCoordinator(); - builder.AddSingleton(new AppConfig - { - Size = (400, 400), - Title = "Click Through", - Borderless = true, - ClearColor = FColors.Black - }); + builder.AddWindow(new WindowOptions( + Size: (400, 400), + Title: "Click Through", + Borderless: true)); builder.AddSingleton>(ClickThroughRenderer.Create); - builder.OnStart((WindowManager windowManager, IKeyboardService keyboardService, AppControl appControl) => + builder.OnStart((Window window, IKeyboardService keyboardService, AppControl appControl) => { - windowManager.PrimaryWindow.SetHitTest(point => InteractiveRegion.Intersects(point) ? HitTestResult.Normal : HitTestResult.Miss); + window.SetHitTest(point => InteractiveRegion.Intersects(point) ? HitTestResult.Normal : HitTestResult.Miss); keyboardService.KeyDown += (Keyboard keyboard, KeyEventArgs e) => { diff --git a/tutorials/GameKit.Tutorials.ComputeShader/Program.cs b/tutorials/GameKit.Tutorials.ComputeShader/Program.cs index 32ad941b..5f2bfe95 100644 --- a/tutorials/GameKit.Tutorials.ComputeShader/Program.cs +++ b/tutorials/GameKit.Tutorials.ComputeShader/Program.cs @@ -9,9 +9,9 @@ static int Main(string[] args) { var builder = new GameKitAppBuilder() .AddContentFromProjectDirectory("Content") - .UseDefaultRenderManager(); + .UseDefaultRenderCoordinator(); - builder.AddSingleton(new AppConfig { Size = (800, 600), Title = "Compute Shader Demo" }); + builder.AddWindow(new WindowOptions(Size: (800, 600), Title: "Compute Shader Demo")); builder.AddSingleton(ComputeRenderer.Create); builder.AddAlias, ComputeRenderer>(); diff --git a/tutorials/GameKit.Tutorials.DepthOnly/Program.cs b/tutorials/GameKit.Tutorials.DepthOnly/Program.cs index 9913531b..b7222801 100644 --- a/tutorials/GameKit.Tutorials.DepthOnly/Program.cs +++ b/tutorials/GameKit.Tutorials.DepthOnly/Program.cs @@ -11,9 +11,9 @@ static int Main(string[] args) var builder = new GameKitAppBuilder() .AddVertexShaderOnlySupport() .AddContentFromProjectDirectory("Content") - .UseDefaultRenderManager(); + .UseDefaultRenderCoordinator(); - builder.AddSingleton(new AppConfig { Size = (800, 600), Title = "Depth-Only Pipeline Test" }); + builder.AddWindow(new WindowOptions(Size: (800, 600), Title: "Depth-Only Pipeline Test")); builder.AddSingleton(DepthOnlyRenderer.Create); builder.AddAlias, DepthOnlyRenderer>(); diff --git a/tutorials/GameKit.Tutorials.FileDialogs/FileDialogsView.cs b/tutorials/GameKit.Tutorials.FileDialogs/FileDialogsView.cs index 50164cdd..c0aa7764 100644 --- a/tutorials/GameKit.Tutorials.FileDialogs/FileDialogsView.cs +++ b/tutorials/GameKit.Tutorials.FileDialogs/FileDialogsView.cs @@ -59,10 +59,10 @@ public class FileDialogsView : View private readonly Window _window; private readonly Font _font; - public FileDialogsView(FileDialogsViewModel viewModel, WindowManager windowManager, IFontSystem fontSystem) + public FileDialogsView(FileDialogsViewModel viewModel, Window window, IFontSystem fontSystem) : base(viewModel) { - _window = windowManager.PrimaryWindow; + _window = window; _font = fontSystem.Load("fonts/GohuFont-Medium.ttf", 16); } diff --git a/tutorials/GameKit.Tutorials.FileDialogs/Program.cs b/tutorials/GameKit.Tutorials.FileDialogs/Program.cs index 6f5b820e..e264cab6 100644 --- a/tutorials/GameKit.Tutorials.FileDialogs/Program.cs +++ b/tutorials/GameKit.Tutorials.FileDialogs/Program.cs @@ -9,11 +9,11 @@ static class Program static int Main(string[] args) { GameKitAppBuilder builder = new GameKitAppBuilder() - .UseDefaultRenderManager() + .UseDefaultRenderCoordinator() .UsePencuil() .AddContentFromProjectDirectory("../GameKit.Tutorials.Hotbar/Content"); - builder.AddSingleton(new AppConfig { Size = (960, 540), Title = "File Dialogs" }); + builder.AddWindow(new WindowOptions(Size: (960, 540), Title: "File Dialogs")); builder.AddSingleton(new FileDialogsViewModel()); builder.AddSingleton(); diff --git a/tutorials/GameKit.Tutorials.Gamepad/Program.cs b/tutorials/GameKit.Tutorials.Gamepad/Program.cs index 620eb19a..014f9631 100644 --- a/tutorials/GameKit.Tutorials.Gamepad/Program.cs +++ b/tutorials/GameKit.Tutorials.Gamepad/Program.cs @@ -10,9 +10,9 @@ static class Program static int Main(string[] args) { GameKitAppBuilder builder = new GameKitAppBuilder() - .UseDefaultRenderManager(); + .UseDefaultRenderCoordinator(); - builder.AddSingleton(new AppConfig { Size = (640, 480), Title = "Gamepad Tutorial" }); + builder.AddWindow(new WindowOptions(Size: (640, 480), Title: "Gamepad Tutorial")); builder.AddSingleton, NullRenderPhase>(); builder.OnStart((IGamepadService gamepadService) => diff --git a/tutorials/GameKit.Tutorials.Hotbar/Program.cs b/tutorials/GameKit.Tutorials.Hotbar/Program.cs index 99f6cdb7..2d90b24b 100644 --- a/tutorials/GameKit.Tutorials.Hotbar/Program.cs +++ b/tutorials/GameKit.Tutorials.Hotbar/Program.cs @@ -9,11 +9,11 @@ static class Program static int Main(string[] args) { var builder = new GameKitAppBuilder() - .UseDefaultRenderManager() + .UseDefaultRenderCoordinator() .UsePencuil() .AddContentFromProjectDirectory("Content"); - builder.AddSingleton(new AppConfig { Size = (1280, 720), Title = "Hotbar" }); + builder.AddWindow(new WindowOptions(Size: (1280, 720), Title: "Hotbar")); builder.AddSingleton(new HotbarViewModel()); builder.AddSingleton(); diff --git a/tutorials/GameKit.Tutorials.ImageLoading/Program.cs b/tutorials/GameKit.Tutorials.ImageLoading/Program.cs index f24ce51d..1a98b78b 100644 --- a/tutorials/GameKit.Tutorials.ImageLoading/Program.cs +++ b/tutorials/GameKit.Tutorials.ImageLoading/Program.cs @@ -9,9 +9,9 @@ static int Main(string[] args) { var builder = new GameKitAppBuilder() .AddContentFromProjectDirectory("Content") - .UseDefaultRenderManager(); + .UseDefaultRenderCoordinator(); - builder.AddSingleton(new AppConfig { Size = (443, 410), Title = "Image Loading Demo" }); + builder.AddWindow(new WindowOptions(Size: (443, 410), Title: "Image Loading Demo")); builder.AddSingleton(ImageLoadingRenderer.Create); builder.AddAlias, ImageLoadingRenderer>(); diff --git a/tutorials/GameKit.Tutorials.IndexBuffer/Program.cs b/tutorials/GameKit.Tutorials.IndexBuffer/Program.cs index d2d11af3..a24299b5 100644 --- a/tutorials/GameKit.Tutorials.IndexBuffer/Program.cs +++ b/tutorials/GameKit.Tutorials.IndexBuffer/Program.cs @@ -9,9 +9,9 @@ static int Main(string[] args) { GameKitAppBuilder builder = new GameKitAppBuilder() .AddContentFromProjectDirectory("Content") - .UseDefaultRenderManager(); + .UseDefaultRenderCoordinator(); - builder.AddSingleton(new AppConfig { Size = (1280, 720), Title = "Index Buffer" }); + builder.AddWindow(new WindowOptions(Size: (1280, 720), Title: "Index Buffer")); builder.AddSingleton>(IndexBufferRenderer.Create); using IGameKitApp gameKitApp = builder.Build(); diff --git a/tutorials/GameKit.Tutorials.IndexedRenderPass/Program.cs b/tutorials/GameKit.Tutorials.IndexedRenderPass/Program.cs index 6ecadaf6..bc97477b 100644 --- a/tutorials/GameKit.Tutorials.IndexedRenderPass/Program.cs +++ b/tutorials/GameKit.Tutorials.IndexedRenderPass/Program.cs @@ -9,9 +9,9 @@ static int Main(string[] args) { GameKitAppBuilder builder = new GameKitAppBuilder() .AddContentFromProjectDirectory("Content") - .UseDefaultRenderManager(); + .UseDefaultRenderCoordinator(); - builder.AddSingleton(new AppConfig { Size = (1280, 720), Title = "Indexed Render Pass" }); + builder.AddWindow(new WindowOptions(Size: (1280, 720), Title: "Indexed Render Pass")); builder.AddSingleton>(IndexedRenderPassRenderer.Create); using IGameKitApp gameKitApp = builder.Build(); diff --git a/tutorials/GameKit.Tutorials.Instancing/Program.cs b/tutorials/GameKit.Tutorials.Instancing/Program.cs index ab54b059..5f294c73 100644 --- a/tutorials/GameKit.Tutorials.Instancing/Program.cs +++ b/tutorials/GameKit.Tutorials.Instancing/Program.cs @@ -9,9 +9,9 @@ static int Main(string[] args) { var builder = new GameKitAppBuilder() .AddContentFromProjectDirectory("Content") - .UseDefaultRenderManager(); + .UseDefaultRenderCoordinator(); - builder.AddSingleton(new AppConfig { Size = (800, 600), Title = "Instancing Demo" }); + builder.AddWindow(new WindowOptions(Size: (800, 600), Title: "Instancing Demo")); builder.AddSingleton(InstancingRenderer.Create); builder.AddAlias, InstancingRenderer>(); diff --git a/tutorials/GameKit.Tutorials.Logging/Program.cs b/tutorials/GameKit.Tutorials.Logging/Program.cs index 87654a06..d89a2888 100644 --- a/tutorials/GameKit.Tutorials.Logging/Program.cs +++ b/tutorials/GameKit.Tutorials.Logging/Program.cs @@ -11,7 +11,7 @@ static class Program static int Main(string[] args) { GameKitAppBuilder builder = new GameKitAppBuilder() - .UseDefaultRenderManager(); + .UseDefaultRenderCoordinator(); builder.AddZLogger(logging => { @@ -36,7 +36,7 @@ static int Main(string[] args) #endif }); builder.AddSingleton(PlayerInputService.Create); - builder.AddSingleton(new AppConfig { Size = (1280, 720), Title = "Logging" }); + builder.AddWindow(new WindowOptions(Size: (1280, 720), Title: "Logging")); builder.AddSingleton, NullRenderPhase>(); using IGameKitApp gameKitApp = builder.Build(); diff --git a/tutorials/GameKit.Tutorials.MouseWindowPresence/Program.cs b/tutorials/GameKit.Tutorials.MouseWindowPresence/Program.cs index 55afd455..34b534ad 100644 --- a/tutorials/GameKit.Tutorials.MouseWindowPresence/Program.cs +++ b/tutorials/GameKit.Tutorials.MouseWindowPresence/Program.cs @@ -9,9 +9,9 @@ static class Program static int Main(string[] args) { GameKitAppBuilder builder = new GameKitAppBuilder() - .UseDefaultRenderManager(); + .UseDefaultRenderCoordinator(); - builder.AddSingleton(new AppConfig { Size = (640, 480), Title = "Mouse Window Presence" }); + builder.AddWindow(new WindowOptions(Size: (640, 480), Title: "Mouse Window Presence")); builder.AddSingleton, NullRenderPhase>(); builder.OnStart((IMouseService mouseService) => diff --git a/tutorials/GameKit.Tutorials.MultiWindow/PrimaryRenderer.cs b/tutorials/GameKit.Tutorials.MultiWindow/PrimaryRenderer.cs index 1d24889f..bc30869b 100644 --- a/tutorials/GameKit.Tutorials.MultiWindow/PrimaryRenderer.cs +++ b/tutorials/GameKit.Tutorials.MultiWindow/PrimaryRenderer.cs @@ -1,6 +1,5 @@ using GameKit.Gpu; using GameKit.RenderOrchestration; -using GameKit.Shaders; namespace GameKit.Tutorials.MultiWindow; @@ -28,10 +27,10 @@ public void Render(DefaultRenderContext renderContext) renderPass.DrawPrimitive(); } - public static PrimaryRenderer Create(ShaderLoader shaderLoader, GraphicsPipelineBuilder graphicsPipelineBuilder, GpuMemorySystem gpuMemorySystem) + public static PrimaryRenderer Create( + GraphicsPipelineBuilder graphicsPipelineBuilder, + GpuVertexBuffer vertexBuffer) { - GpuVertexBuffer vertexBuffer = gpuMemorySystem.CreateVertexBuffer(PositionShapes.VerticalQuad); - GraphicsPipeline graphicsPipeline = graphicsPipelineBuilder .SetPrimitiveType(PrimitiveType.TriangleStrip) .AddVertexBufferConfig() diff --git a/tutorials/GameKit.Tutorials.MultiWindow/Program.cs b/tutorials/GameKit.Tutorials.MultiWindow/Program.cs index 80cf3fea..3e6cce85 100644 --- a/tutorials/GameKit.Tutorials.MultiWindow/Program.cs +++ b/tutorials/GameKit.Tutorials.MultiWindow/Program.cs @@ -1,4 +1,6 @@ using GameKit.App; +using GameKit.DependencyInjection; +using GameKit.Gpu; using GameKit.RenderOrchestration; namespace GameKit.Tutorials.MultiWindow; @@ -8,14 +10,30 @@ static class Program static int Main(string[] args) { GameKitAppBuilder builder = new GameKitAppBuilder() - .AddContentFromProjectDirectory("Content") - .UseDefaultRenderManager(); + .AddContentFromProjectDirectory("Content"); - builder.AddSingleton(new AppConfig { Size = (640, 480), Title = "Primary Window" }); - builder.AddSingleton>(PrimaryRenderer.Create); - builder.AddSingleton(SecondaryWindowRenderer.Create); + builder.AddSingleton>(static (GpuMemorySystem gpuMemorySystem) => + gpuMemorySystem.CreateVertexBuffer(PositionShapes.VerticalQuad)); using IGameKitApp gameKitApp = builder.Build(); + + ServiceCollection primaryWindowServices = gameKitApp.CreateServiceCollection(); + primaryWindowServices.AddWindow(new WindowOptions( + Size: new Size(640, 480), + Title: "Primary Window")); + primaryWindowServices.UseDefaultRenderCoordinator(); + primaryWindowServices.AddSingleton>(PrimaryRenderer.Create); + primaryWindowServices.BuildServiceProvider(); + + ServiceCollection secondaryWindowServices = gameKitApp.CreateServiceCollection(); + secondaryWindowServices.AddWindow(new WindowOptions( + Size: new Size(480, 360), + Title: "Secondary Window", + StopGameOnClose: false)); + secondaryWindowServices.UseDefaultRenderCoordinator(); + secondaryWindowServices.AddSingleton>(SecondaryWindowRenderer.Create); + secondaryWindowServices.BuildServiceProvider(); + return gameKitApp.Run(); } } diff --git a/tutorials/GameKit.Tutorials.MultiWindow/SecondaryWindowRenderer.cs b/tutorials/GameKit.Tutorials.MultiWindow/SecondaryWindowRenderer.cs index 455ae15d..8fc6ebb1 100644 --- a/tutorials/GameKit.Tutorials.MultiWindow/SecondaryWindowRenderer.cs +++ b/tutorials/GameKit.Tutorials.MultiWindow/SecondaryWindowRenderer.cs @@ -1,94 +1,45 @@ using GameKit.Gpu; -using GameKit.Shaders; +using GameKit.RenderOrchestration; namespace GameKit.Tutorials.MultiWindow; -public class SecondaryWindowRenderer : IUpdatable, IDisposable +public sealed class SecondaryWindowRenderer : IRenderPhase { - private readonly WindowManager _windowManager; - private readonly GpuDevice _gpuDevice; private readonly GraphicsPipeline _graphicsPipeline; private readonly GpuVertexBuffer _vertexBuffer; - private Window? _secondaryWindow; public SecondaryWindowRenderer( - WindowManager windowManager, - GpuDevice gpuDevice, GraphicsPipeline graphicsPipeline, - GpuVertexBuffer vertexBuffer, - Window secondaryWindow) + GpuVertexBuffer vertexBuffer) { - _windowManager = windowManager; - _gpuDevice = gpuDevice; _graphicsPipeline = graphicsPipeline; _vertexBuffer = vertexBuffer; - _secondaryWindow = secondaryWindow; } - public void Update() + public void Render(DefaultRenderContext renderContext) { - if (_secondaryWindow == null) - { - return; - } - - if (!_windowManager.Windows.Contains(_secondaryWindow)) - { - _secondaryWindow = null; - return; - } - - CommandBuffer commandBuffer = _gpuDevice.AcquireCommandBuffer(); - if (!_secondaryWindow.TryWaitAndAcquireSwapchainTexture(commandBuffer, out SwapchainTexture swapchainTexture)) - { - commandBuffer.Dispose(); - return; - } - - commandBuffer.PushFragmentUniformData(0, FColors.Coral); - using (IRenderPass renderPass = new RenderPassBuilder(commandBuffer) - .AddColorTarget(swapchainTexture) - .SetSharedColorTargetSettings(ColorTargetSettings.Clear) - .Build()) - { - renderPass.BindGraphicsPipeline(_graphicsPipeline); - renderPass.BindVertexBuffer(_vertexBuffer); - renderPass.DrawPrimitive(); - } - - commandBuffer.Submit(); - } - - public void Dispose() - { - if (_secondaryWindow != null && _windowManager.Windows.Contains(_secondaryWindow)) - { - _windowManager.DestroyWindow(_secondaryWindow); - } + renderContext.CommandBuffer.PushFragmentUniformData(0, FColors.Coral); + using IRenderPass renderPass = new RenderPassBuilder(renderContext.CommandBuffer) + .AddColorTarget(renderContext.SwapchainTexture) + .SetSharedColorTargetSettings(ColorTargetSettings.Clear) + .Build(); - _secondaryWindow = null; + renderPass.BindGraphicsPipeline(_graphicsPipeline); + renderPass.BindVertexBuffer(_vertexBuffer); + renderPass.DrawPrimitive(); } public static SecondaryWindowRenderer Create( - WindowManager windowManager, - GpuDevice gpuDevice, - ShaderLoader shaderLoader, GraphicsPipelineBuilder graphicsPipelineBuilder, - GpuMemorySystem gpuMemorySystem) + GpuVertexBuffer vertexBuffer) { - Window secondaryWindow = windowManager.CreateWindow(new WindowOptions( - Size: new Size(480, 360), - Title: "Secondary Window")); - - GpuVertexBuffer vertexBuffer = gpuMemorySystem.CreateVertexBuffer(PositionShapes.VerticalQuad); - GraphicsPipeline graphicsPipeline = graphicsPipelineBuilder .SetPrimitiveType(PrimitiveType.TriangleStrip) .AddVertexBufferConfig() .SetShaders("shaders/vertex", "shaders/fragment") - .AddColorFormatFromDisplay(secondaryWindow) + .AddColorFormatFromDisplay() .Build(); - return new SecondaryWindowRenderer(windowManager, gpuDevice, graphicsPipeline, vertexBuffer, secondaryWindow); + return new SecondaryWindowRenderer(graphicsPipeline, vertexBuffer); } } diff --git a/tutorials/GameKit.Tutorials.StageSwitching/Program.cs b/tutorials/GameKit.Tutorials.StageSwitching/Program.cs index e18c5680..aeb24cdd 100644 --- a/tutorials/GameKit.Tutorials.StageSwitching/Program.cs +++ b/tutorials/GameKit.Tutorials.StageSwitching/Program.cs @@ -9,11 +9,11 @@ static class Program static int Main(string[] args) { GameKitAppBuilder builder = new GameKitAppBuilder() - .UseDefaultRenderManager() + .UseDefaultRenderCoordinator() .UsePencuil() .AddContentFromProjectDirectory("../GameKit.Tutorials.Hotbar/Content"); - builder.AddSingleton(new AppConfig { Size = (960, 540), Title = "Stage Switching" }); + builder.AddWindow(new WindowOptions(Size: (960, 540), Title: "Stage Switching")); builder.AddSingleton(); using IGameKitApp gameKitApp = builder.Build(); diff --git a/tutorials/GameKit.Tutorials.StencilBuffer/Program.cs b/tutorials/GameKit.Tutorials.StencilBuffer/Program.cs index adc01f13..d82f0def 100644 --- a/tutorials/GameKit.Tutorials.StencilBuffer/Program.cs +++ b/tutorials/GameKit.Tutorials.StencilBuffer/Program.cs @@ -9,9 +9,9 @@ static int Main(string[] args) { var builder = new GameKitAppBuilder() .AddContentFromProjectDirectory("Content") - .UseDefaultRenderManager(); + .UseDefaultRenderCoordinator(); - builder.AddSingleton(new AppConfig { Size = (1280, 720), Title = "Stencil Buffer" }); + builder.AddWindow(new WindowOptions(Size: (1280, 720), Title: "Stencil Buffer")); builder.AddSingleton(StencilBufferRenderer.Create); builder.AddAlias, StencilBufferRenderer>(); diff --git a/tutorials/GameKit.Tutorials.StorageBuffer/Program.cs b/tutorials/GameKit.Tutorials.StorageBuffer/Program.cs index fd1f0590..02d88d71 100644 --- a/tutorials/GameKit.Tutorials.StorageBuffer/Program.cs +++ b/tutorials/GameKit.Tutorials.StorageBuffer/Program.cs @@ -9,9 +9,9 @@ static int Main(string[] args) { var builder = new GameKitAppBuilder() .AddContentFromProjectDirectory("Content") - .UseDefaultRenderManager(); + .UseDefaultRenderCoordinator(); - builder.AddSingleton(new AppConfig { Size = (800, 600), Title = "Storage Buffer Demo" }); + builder.AddWindow(new WindowOptions(Size: (800, 600), Title: "Storage Buffer Demo")); builder.AddSingleton(StorageBufferRenderer.Create); builder.AddAlias, StorageBufferRenderer>(); diff --git a/tutorials/GameKit.Tutorials.TextInput/Program.cs b/tutorials/GameKit.Tutorials.TextInput/Program.cs index da79305a..5ba5e54e 100644 --- a/tutorials/GameKit.Tutorials.TextInput/Program.cs +++ b/tutorials/GameKit.Tutorials.TextInput/Program.cs @@ -9,11 +9,11 @@ static class Program static int Main(string[] args) { GameKitAppBuilder builder = new GameKitAppBuilder() - .UseDefaultRenderManager() + .UseDefaultRenderCoordinator() .UsePencuil() .AddContentFromProjectDirectory("../GameKit.Tutorials.Hotbar/Content"); - builder.AddSingleton(new AppConfig { Size = (640, 440), Title = "Text Input" }); + builder.AddWindow(new WindowOptions(Size: (640, 440), Title: "Text Input")); builder.AddSingleton(); builder.AddSingleton(); diff --git a/tutorials/GameKit.Tutorials.TextureArray/Program.cs b/tutorials/GameKit.Tutorials.TextureArray/Program.cs index c32f4c17..2ab5afca 100644 --- a/tutorials/GameKit.Tutorials.TextureArray/Program.cs +++ b/tutorials/GameKit.Tutorials.TextureArray/Program.cs @@ -9,13 +9,13 @@ static int Main(string[] args) { var builder = new GameKitAppBuilder() .AddContentFromProjectDirectory("Content") - .UseDefaultRenderManager(); + .UseDefaultRenderCoordinator(); - builder.AddSingleton(new AppConfig { Size = (800, 600), Title = "Texture Array Demo" }); + builder.AddWindow(new WindowOptions(Size: (800, 600), Title: "Texture Array Demo")); builder.AddSingleton(TextureArrayRenderer.Create); builder.AddAlias, TextureArrayRenderer>(); using IGameKitApp gameKitApp = builder.Build(); return gameKitApp.Run(); } -} \ No newline at end of file +} diff --git a/tutorials/GameKit.Tutorials.TransparentWindow/Program.cs b/tutorials/GameKit.Tutorials.TransparentWindow/Program.cs index 0f281238..836597c7 100644 --- a/tutorials/GameKit.Tutorials.TransparentWindow/Program.cs +++ b/tutorials/GameKit.Tutorials.TransparentWindow/Program.cs @@ -17,16 +17,13 @@ static int Main(string[] args) { GameKitAppBuilder builder = new GameKitAppBuilder() .AddContentFromProjectDirectory("Content") - .UseDefaultRenderManager(); + .UseDefaultRenderCoordinator(); - builder.AddSingleton(new AppConfig - { - Size = (800, 600), - Title = "Transparent Window", - Transparent = true, - Borderless = true, - ClearColor = FColors.Transparent - }); + builder.AddWindow(new WindowOptions( + Size: (800, 600), + Title: "Transparent Window", + Transparent: true, + Borderless: true)); if (OperatingSystem.IsWindows()) { builder.AddSingleton(new GameKitConfig(GpuBackend: GpuBackend.Vulkan)); diff --git a/tutorials/GameKit.Tutorials.Triangle/Program.cs b/tutorials/GameKit.Tutorials.Triangle/Program.cs index 7b632f45..1f1152e4 100644 --- a/tutorials/GameKit.Tutorials.Triangle/Program.cs +++ b/tutorials/GameKit.Tutorials.Triangle/Program.cs @@ -10,9 +10,9 @@ static int Main(string[] args) GameKitAppBuilder builder = new GameKitAppBuilder() //.AddContentFromZipPattern("data*.pak") .AddContentFromProjectDirectory("Content") - .UseDefaultRenderManager(); + .UseDefaultRenderCoordinator(); - builder.AddSingleton(new AppConfig { Size = (1280, 720), Title = "Game" }); + builder.AddWindow(new WindowOptions(Size: (1280, 720), Title: "Game")); builder.AddSingleton>(TriangleRenderer.Create); using IGameKitApp gameKitApp = builder.Build(); diff --git a/tutorials/GameKit.Tutorials.WindowConfiguration/Program.cs b/tutorials/GameKit.Tutorials.WindowConfiguration/Program.cs index b67ee586..fbb18103 100644 --- a/tutorials/GameKit.Tutorials.WindowConfiguration/Program.cs +++ b/tutorials/GameKit.Tutorials.WindowConfiguration/Program.cs @@ -12,20 +12,17 @@ static class Program static int Main(string[] args) { GameKitAppBuilder builder = new GameKitAppBuilder() - .UseDefaultRenderManager(); + .UseDefaultRenderCoordinator(); - builder.AddSingleton(new AppConfig - { - Size = (800, 600), - Title = "Window Configuration Demo", - AlwaysOnTop = true - }); + builder.AddWindow(new WindowOptions( + Size: (800, 600), + Title: "Window Configuration Demo", + AlwaysOnTop: true)); builder.AddSingleton, NullRenderPhase>(); - builder.OnStart((WindowManager windowManager, IKeyboardService keyboardService, PlatformInfo platformInfo) => + builder.OnStart((Window window, IKeyboardService keyboardService, PlatformInfo platformInfo) => { - Window window = windowManager.PrimaryWindow; using RawImage icon = CreateIcon(32, 32); window.SetIcon(icon); diff --git a/tutorials/GameKit.Tutorials.WindowCreation/Program.cs b/tutorials/GameKit.Tutorials.WindowCreation/Program.cs index 1a1c5c9e..a0c89f73 100644 --- a/tutorials/GameKit.Tutorials.WindowCreation/Program.cs +++ b/tutorials/GameKit.Tutorials.WindowCreation/Program.cs @@ -10,9 +10,9 @@ static int Main(string[] args) GameKitAppBuilder builder = new GameKitAppBuilder() //.AddContentFromZipPattern("data*.pak") //.AddContentFromProjectDirectory("_Content") - .UseDefaultRenderManager(); + .UseDefaultRenderCoordinator(); - builder.AddSingleton(new AppConfig { Size = (1280, 720), Title = "Game" }); + builder.AddWindow(new WindowOptions(Size: (1280, 720), Title: "Game")); builder.AddSingleton, NullRenderPhase>(); using IGameKitApp gameKitApp = builder.Build(); diff --git a/tutorials/GameKit.Tutorials.WindowDragging/Program.cs b/tutorials/GameKit.Tutorials.WindowDragging/Program.cs index cde16389..c0d513b5 100644 --- a/tutorials/GameKit.Tutorials.WindowDragging/Program.cs +++ b/tutorials/GameKit.Tutorials.WindowDragging/Program.cs @@ -12,21 +12,17 @@ static class Program static int Main(string[] args) { GameKitAppBuilder builder = new GameKitAppBuilder() - .UseDefaultRenderManager(); + .UseDefaultRenderCoordinator(); - builder.AddSingleton(new AppConfig - { - Size = (400, 400), - Title = "Window Dragging", - Borderless = true - }); + builder.AddWindow(new WindowOptions( + Size: (400, 400), + Title: "Window Dragging", + Borderless: true)); builder.AddSingleton>(static () => new ClearRenderPhase(FColors.SkyBlue)); - builder.OnStart((WindowManager windowManager, IMouseService mouseService, IKeyboardService keyboardService, UpdateSystem updateSystem, AppControl appControl) => + builder.OnStart((Window window, IMouseService mouseService, IKeyboardService keyboardService, UpdateSystem updateSystem, AppControl appControl) => { - Window window = windowManager.PrimaryWindow; - if (window.SupportsSetWindowPosition) { Console.WriteLine("Active window dragging path: programmatic positioning");