diff --git a/SDL3-CS.Tests/MyWindow.cs b/SDL3-CS.Tests/MyWindow.cs new file mode 100644 index 0000000..e23e98b --- /dev/null +++ b/SDL3-CS.Tests/MyWindow.cs @@ -0,0 +1,238 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using osu.Framework.Allocation; +using SDL; +using static SDL.SDL3; + +namespace SDL3.Tests +{ + public sealed unsafe class MyWindow : IDisposable + { + private bool flash; + private ObjectHandle objectHandle { get; } + private SDL_Window* SDLWindowHandle; + private SDL_Renderer* Renderer; + private readonly bool initSuccess; + + private const SDL_InitFlags initFlags = SDL_InitFlags.SDL_INIT_VIDEO | SDL_InitFlags.SDL_INIT_GAMEPAD; + + public MyWindow() + { + if (SDL_InitSubSystem(initFlags) < 0) + throw new InvalidOperationException($"failed to initialise SDL. Error: {SDL_GetError()}"); + + initSuccess = true; + + objectHandle = new ObjectHandle(this, GCHandleType.Normal); + } + + public void Setup() + { + SDL_SetGamepadEventsEnabled(SDL_bool.SDL_TRUE); + SDL_SetEventFilter(&nativeFilter, objectHandle.Handle); + + if (OperatingSystem.IsWindows()) + SDL_SetWindowsMessageHook(&wndProc, objectHandle.Handle); + } + + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static SDL_bool wndProc(IntPtr userdata, MSG* message) + { + var handle = new ObjectHandle(userdata); + if (handle.GetTarget(out var window)) + { + Console.WriteLine($"from {window}, message: {message->message}"); + } + + return SDL_TRUE; // sample use of definition from SDL3 class, not SDL_bool enum + } + + // ReSharper disable once UseCollectionExpression + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] + private static int nativeFilter(IntPtr userdata, SDL_Event* e) + { + var handle = new ObjectHandle(userdata); + if (handle.GetTarget(out var window)) + return window.handleEventFromFilter(e); + + return 1; + } + + public Action? EventFilter; + + private int handleEventFromFilter(SDL_Event* e) + { + switch (e->type) + { + case SDL_EventType.SDL_EVENT_KEY_UP: + case SDL_EventType.SDL_EVENT_KEY_DOWN: + handleKeyFromFilter(e->key); + break; + + default: + EventFilter?.Invoke(*e); + break; + } + + return 1; + } + + private void handleKeyFromFilter(SDL_KeyboardEvent e) + { + if (e.keysym.sym == SDL_Keycode.SDLK_f) + { + flash = true; + } + } + + public void Create() + { + SDLWindowHandle = SDL_CreateWindow("hello"u8, 800, 600, SDL_WindowFlags.SDL_WINDOW_RESIZABLE | SDL_WindowFlags.SDL_WINDOW_HIGH_PIXEL_DENSITY); + Renderer = SDL_CreateRenderer(SDLWindowHandle, (byte*)null, SDL_RendererFlags.SDL_RENDERER_PRESENTVSYNC); + } + + private void handleEvent(SDL_Event e) + { + switch (e.type) + { + case SDL_EventType.SDL_EVENT_QUIT: + run = false; + break; + + case SDL_EventType.SDL_EVENT_KEY_DOWN: + switch (e.key.keysym.sym) + { + case SDL_Keycode.SDLK_r: + bool old = SDL_GetRelativeMouseMode() == SDL_bool.SDL_TRUE; + SDL_SetRelativeMouseMode(old ? SDL_bool.SDL_FALSE : SDL_bool.SDL_TRUE); + break; + + case SDL_Keycode.SDLK_v: + string? text = SDL_GetClipboardText(); + Console.WriteLine($"clipboard: {text}"); + break; + + case SDL_Keycode.SDLK_F10: + SDL_SetWindowFullscreen(SDLWindowHandle, SDL_bool.SDL_FALSE); + break; + + case SDL_Keycode.SDLK_F11: + SDL_SetWindowFullscreen(SDLWindowHandle, SDL_bool.SDL_TRUE); + break; + + case SDL_Keycode.SDLK_j: + { + using var gamepads = SDL_GetGamepads(); + + if (gamepads == null || gamepads.Count == 0) + break; + + var gamepad = SDL_OpenGamepad(gamepads[0]); + + int count; + var bindings = SDL_GetGamepadBindings(gamepad, &count); + for (int i = 0; i < count; i++) + { + var binding = *bindings[i]; + Console.WriteLine(binding.input_type); + Console.WriteLine(binding.output_type); + Console.WriteLine(); + } + + SDL_CloseGamepad(gamepad); + break; + } + + case SDL_Keycode.SDLK_F1: + SDL_StartTextInput(); + break; + + case SDL_Keycode.SDLK_F2: + SDL_StopTextInput(); + break; + + case SDL_Keycode.SDLK_m: + SDL_Keymod mod = e.key.keysym.Mod; + Console.WriteLine(mod); + break; + } + + break; + + case SDL_EventType.SDL_EVENT_TEXT_INPUT: + Console.WriteLine(e.text.GetText()); + break; + + case SDL_EventType.SDL_EVENT_GAMEPAD_ADDED: + Console.WriteLine($"gamepad added: {e.gdevice.which}"); + break; + + case SDL_EventType.SDL_EVENT_WINDOW_PEN_ENTER: + SDL_PenCapabilityInfo info; + var cap = (SDL_PEN_CAPABILITIES)SDL_GetPenCapabilities((SDL_PenID)e.window.data1, &info); + + if (cap.HasFlag(SDL_PEN_CAPABILITIES.SDL_PEN_AXIS_XTILT_MASK)) + Console.WriteLine("has pen xtilt axis"); + + Console.WriteLine(info.max_tilt); + break; + } + } + + private bool run = true; + + private const int events_per_peep = 64; + private readonly SDL_Event[] events = new SDL_Event[events_per_peep]; + + private void pollEvents() + { + SDL_PumpEvents(); + + int eventsRead; + + do + { + fixed (SDL_Event* buf = events) + eventsRead = SDL_PeepEvents(buf, events_per_peep, SDL_eventaction.SDL_GETEVENT, SDL_EventType.SDL_EVENT_FIRST, SDL_EventType.SDL_EVENT_LAST); + + for (int i = 0; i < eventsRead; i++) + handleEvent(events[i]); + } while (eventsRead == events_per_peep); + } + + private float frame; + + public void Run() + { + while (run) + { + if (flash) + { + flash = false; + Console.WriteLine("flash!"); + } + + pollEvents(); + + SDL_SetRenderDrawColorFloat(Renderer, SDL_sinf(frame) / 2 + 0.5f, SDL_cosf(frame) / 2 + 0.5f, 0.3f, 1.0f); + SDL_RenderClear(Renderer); + SDL_RenderPresent(Renderer); + + frame += 0.015f; + + Thread.Sleep(10); + } + } + + public void Dispose() + { + if (initSuccess) + SDL_QuitSubSystem(initFlags); + + objectHandle.Dispose(); + } + } +} diff --git a/SDL3-CS.Tests/ObjectHandle.cs b/SDL3-CS.Tests/ObjectHandle.cs new file mode 100644 index 0000000..4252799 --- /dev/null +++ b/SDL3-CS.Tests/ObjectHandle.cs @@ -0,0 +1,95 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable + +using System.Runtime.InteropServices; + +namespace osu.Framework.Allocation +{ + /// + /// Wrapper on that supports the pattern. + /// + public struct ObjectHandle : IDisposable + where T : class + { + /// + /// The pointer from the , if it is allocated. Otherwise . + /// + public IntPtr Handle => handle.IsAllocated ? GCHandle.ToIntPtr(handle) : IntPtr.Zero; + + /// + /// The address of target object, if it is allocated and pinned. Otherwise . + /// Note: This is not the same as the . + /// + public IntPtr Address => handle.IsAllocated ? handle.AddrOfPinnedObject() : IntPtr.Zero; + + public bool IsAllocated => handle.IsAllocated; + + private GCHandle handle; + + private readonly bool fromPointer; + + /// + /// Wraps the provided object with a , using the given . + /// + /// The target object to wrap. + /// The handle type to use. + public ObjectHandle(T target, GCHandleType handleType) + { + handle = GCHandle.Alloc(target, handleType); + fromPointer = false; + } + + /// + /// Recreates an based on the passed . + /// Disposing this object will not free the handle, the original object must be disposed instead. + /// + /// Handle. + public ObjectHandle(IntPtr handle) + { + this.handle = GCHandle.FromIntPtr(handle); + fromPointer = true; + } + + /// + /// Gets the object being referenced. + /// Returns true if successful and populates with the referenced object. + /// Returns false If the handle is not allocated or the target is not of type . + /// + /// Populates this parameter with the targeted object. + public bool GetTarget(out T target) + { + if (!handle.IsAllocated) + { + target = default; + return false; + } + + try + { + if (handle.Target is T value) + { + target = value; + return true; + } + } + catch (InvalidOperationException) + { + } + + target = default; + return false; + } + + #region IDisposable Support + + public void Dispose() + { + if (!fromPointer && handle.IsAllocated) + handle.Free(); + } + + #endregion + } +} diff --git a/SDL3-CS.Tests/Program.cs b/SDL3-CS.Tests/Program.cs new file mode 100644 index 0000000..045e97a --- /dev/null +++ b/SDL3-CS.Tests/Program.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using System.Text; +using SDL; +using static SDL.SDL3; + +namespace SDL3.Tests +{ + public static class Program + { + private static void Main() + { + Console.OutputEncoding = Encoding.UTF8; + + SDL_SetHint(SDL_HINT_WINDOWS_CLOSE_ON_ALT_F4, "null byte \0 in string"u8); + Debug.Assert(SDL_GetHint(SDL_HINT_WINDOWS_CLOSE_ON_ALT_F4) == "null byte "); + + SDL_SetHint(SDL_HINT_WINDOWS_CLOSE_ON_ALT_F4, "1"u8); + + using (var window = new MyWindow()) + { + Console.WriteLine($"SDL revision: {SDL_GetRevision()}"); + + printDisplays(); + + window.Setup(); + window.Create(); + + const SDL_Keymod state = SDL_Keymod.SDL_KMOD_CAPS | SDL_Keymod.SDL_KMOD_ALT; + SDL_SetModState(state); + Debug.Assert(SDL_GetModState() == state); + + window.Run(); + } + + SDL_Quit(); + } + + private static void printDisplays() + { + using var displays = SDL_GetDisplays(); + if (displays == null) + return; + + for (int i = 0; i < displays.Count; i++) + { + SDL_DisplayID id = displays[i]; + Console.WriteLine(id); + + using var modes = SDL_GetFullscreenDisplayModes(id); + if (modes == null) + continue; + + for (int j = 0; j < modes.Count; j++) + { + SDL_DisplayMode mode = modes[j]; + Console.WriteLine($"{mode.w}x{mode.h}@{mode.refresh_rate}"); + } + } + } + } +} diff --git a/SDL3-CS.Tests/SDL3-CS.Tests.csproj b/SDL3-CS.Tests/SDL3-CS.Tests.csproj new file mode 100644 index 0000000..0fe8dc8 --- /dev/null +++ b/SDL3-CS.Tests/SDL3-CS.Tests.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + SDL3.Tests + enable + enable + true + + + + + + + diff --git a/SDL3-CS.sln b/SDL3-CS.sln index e3d75c1..98f5ca6 100644 --- a/SDL3-CS.sln +++ b/SDL3-CS.sln @@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SDL3-CS", "SDL3-CS\SDL3-CS. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SDL3-CS.SourceGeneration", "SDL3-CS.SourceGeneration\SDL3-CS.SourceGeneration.csproj", "{432C86D0-D371-4D01-BFFE-01D2CDCCA7B8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SDL3-CS.Tests", "SDL3-CS.Tests\SDL3-CS.Tests.csproj", "{CF980481-8227-4BED-970E-6433C83F64CB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -34,5 +36,9 @@ Global {432C86D0-D371-4D01-BFFE-01D2CDCCA7B8}.Debug|Any CPU.Build.0 = Debug|Any CPU {432C86D0-D371-4D01-BFFE-01D2CDCCA7B8}.Release|Any CPU.ActiveCfg = Release|Any CPU {432C86D0-D371-4D01-BFFE-01D2CDCCA7B8}.Release|Any CPU.Build.0 = Release|Any CPU + {CF980481-8227-4BED-970E-6433C83F64CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF980481-8227-4BED-970E-6433C83F64CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF980481-8227-4BED-970E-6433C83F64CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF980481-8227-4BED-970E-6433C83F64CB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal