Ahwoo.BRUTAL.Sdl
2026.10.2
Prefix Reserved
dotnet add package Ahwoo.BRUTAL.Sdl --version 2026.10.2
NuGet\Install-Package Ahwoo.BRUTAL.Sdl -Version 2026.10.2
<PackageReference Include="Ahwoo.BRUTAL.Sdl" Version="2026.10.2" />
<PackageVersion Include="Ahwoo.BRUTAL.Sdl" Version="2026.10.2" />
<PackageReference Include="Ahwoo.BRUTAL.Sdl" />
paket add Ahwoo.BRUTAL.Sdl --version 2026.10.2
#r "nuget: Ahwoo.BRUTAL.Sdl, 2026.10.2"
#:package Ahwoo.BRUTAL.Sdl@2026.10.2
#addin nuget:?package=Ahwoo.BRUTAL.Sdl&version=2026.10.2
#tool nuget:?package=Ahwoo.BRUTAL.Sdl&version=2026.10.2
Brutal Sdl
Modern C# bindings for SDL3, the cross-platform library for windowing, input, audio, and multimedia, for use with the BRUTAL framework.
Brutal.Sdl generates a complete, strongly-typed P/Invoke surface directly from the SDL3 headers -
every public SDL_* function, enum, struct, and constant is reachable from C# - and then layers a
hand-curated ergonomic API on top of the subsystems most programs touch first: initialization,
windowing, the event queue, keyboard, mouse, gamepad, joystick, and 2D rendering. Use the raw layer
when you need the full surface (audio, the GPU API, cameras, haptics, sensors, and everything else
SDL exposes); use the ergonomic layer when you want bool/out/string/typed-handle sugar over
the subsystems it curates. Both layers call the same underlying native library, so you can freely
mix them in one program.
Platform Support
- Windows (x64)
- Linux (x64)
- macOS (Apple Silicon)
Brutal.Sdl vendors and builds SDL 3.4.12 as a git submodule; the native shared library is built from source for each platform via CMake, not downloaded as a prebuilt binary.
Links
NuGet Packages
- Ahwoo.BRUTAL.Sdl - C# bindings for SDL3
- Ahwoo.BRUTAL.Sdl.Image - C# bindings for SDL_image
- Ahwoo.BRUTAL.Sdl.Ttf - C# bindings for SDL_ttf
- Ahwoo.BRUTAL.Sdl.Mixer - C# bindings for SDL_mixer
For Users
Prerequisites
- .NET SDK (version 10.0 or later)
Installation
RocketWerkz.Brutal.* packages are published to the RocketWerkz GitHub Packages feed, which
requires an authenticated NuGet source even for read access. Add the feed to a nuget.config in
your solution (or user-level %APPDATA%\NuGet\NuGet.Config / ~/.nuget/NuGet/NuGet.Config),
using a personal access token with the read:packages scope
as the password:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="RocketWerkz" value="https://nuget.pkg.github.com/RocketWerkz/index.json" />
</packageSources>
<packageSourceCredentials>
<RocketWerkz>
<add key="Username" value="YOUR_GITHUB_USERNAME" />
<add key="ClearTextPassword" value="YOUR_GITHUB_PAT" />
</RocketWerkz>
</packageSourceCredentials>
</configuration>
Then install the package(s) you need:
dotnet add package Ahwoo.BRUTAL.Sdl
Add the SDL_image, SDL_ttf, and/or SDL_mixer satellite packages as needed - each is optional
and independent of the others:
dotnet add package Ahwoo.BRUTAL.Sdl.Image
dotnet add package Ahwoo.BRUTAL.Sdl.Ttf
dotnet add package Ahwoo.BRUTAL.Sdl.Mixer
Quick Start
Initialize SDL, create a window, and run a basic event loop:
using Brutal.SdlApi;
Sdl.Init.Initialize(SdlInitFlags.Video);
var window = Sdl.Video.CreateWindow("Hello Window", 1024, 768, SdlWindowFlags.Resizable);
var running = true;
while (running)
{
while (Sdl.Events.PollEvent(out var ev))
{
var type = ev.type;
if (type is SdlEventType.Quit or SdlEventType.WindowCloseRequested)
running = false;
else if (type == SdlEventType.KeyDown && ev.key.scancode == SdlScancode.Escape)
running = false;
}
}
window.Dispose();
Sdl.Init.Quit();
See samples/ for ten runnable scenarios (QuickStart, KeyboardInput, MouseInput,
GamepadInput, MultipleWindows, RendererDrawing, ImageViewSample, TtfTextSample,
MixerPlaybackSample, VulkanSurfaceSample) - run one with just sample <Name>.
VulkanSurfaceSample is the odd one out: it demonstrates cross-package interop with
Ahwoo.BRUTAL.Vulkan - an SDL window created with SDL_WINDOW_VULKAN, a VkInstance created
by Brutal.Vulkan using the extensions SDL_Vulkan_GetInstanceExtensions reports, and a
VkSurfaceKHR created/destroyed through SDL's own SDL_Vulkan_CreateSurface/
SDL_Vulkan_DestroySurface, with only a raw pointer-sized handle crossing the package boundary. It
is headless-aware: with no Vulkan driver reachable (SDL_VIDEODRIVER=dummy, or a real driver with no
Vulkan loader discoverable) it prints a message and exits 0 instead of failing.
Please refer to src/Brutal.Sdl/Manual/Libraries/ for topic-by-topic guides
(Initialization,
WindowLifecycle,
EventLoop,
Input), the
SdlImage guide for the SDL_image package, the
SdlTtf guide for the SDL_ttf package, the
SdlMixer guide for the SDL_mixer package, and the
BRUTAL Documentation for the full API reference.
Raw vs. ergonomic APIs
Every one of SDL3's 1,200+ public functions is bound as a strongly-typed (but otherwise 1:1) raw
P/Invoke wrapper under Sdl.PInvoke.* - strings, enums, and opaque pointers already come through
as string/SdlEnum/typed handle, but the shape of each call otherwise matches its SDL_*
counterpart exactly (including out-free pointer parameters where SDL itself takes a pointer).
On top of that, eight subsystems get an additional ergonomic layer - nested classes such as
Sdl.Video and Sdl.Keyboard that add:
boolreturns/parameters instead of SDL's 1-byte Cbool(byte).out Tparameters for pure output pointers (SDL_GetWindowSize(window, out w, out h)instead of afixedblock).- Throwing entry points where a raw call would otherwise return
false/null and expect you to checkSDL_GetErroryourself.Sdl.Init.InitializethrowsSdlException; the ergonomic handle-returning creators (Sdl.Video.CreateWindow,Sdl.Mouse.CreateCursor,Sdl.Gamepad.OpenGamepad,Sdl.Render.CreateRenderer/CreateTexture/CreateSoftwareRenderer, ...) throwSdlExceptioninstead of returning a null handle; and every ergonomic event-pump entry point (Sdl.Events.PollEvent,PumpEvents,WaitEvent,WaitEventTimeout,PushEvent) rethrows exceptions raised inside event-watch callbacks as anSdlCallbackException- see EventLoop.
A handful of shapes stay on the raw layer even within a curated subsystem - most notably every
SDL_Get*s(int* count) enumeration idiom (GetGamepads, GetJoysticks, GetDisplays, ...),
because turning a T*-with-count return into something safer requires span curation this package
does not yet do. You always have Sdl.PInvoke.* as the fallback, in every subsystem, ergonomic or
not.
Every ergonomic static whose first parameter is one of the nine disposable handle types is also
projected as an instance method on that handle's struct - window.Show() calls
Sdl.Video.ShowWindow(window) verbatim, window.GetSize(out w, out h) calls
Sdl.Video.GetWindowSize(window, out w, out h), and so on. Statics are untouched (nothing removed
or renamed); the instance method is purely an additive, same-body alias, so either spelling is
equally correct and you can mix them freely.
Keyboard and mouse events also carry typed enums rather than raw integers: SdlKeyboardEvent.key is
a plain SdlKeycode enum (not an untyped SDL_Keycode/uint), and Sdl.Mouse.GetMouseState/mouse
button state is the [Flags] SdlMouseButtonFlags enum rather than a raw bitmask.
Handle ownership, disposal, and aliasing
Opaque SDL pointers that own a resource (SDL_Window, SDL_Renderer, SDL_Texture,
SDL_Gamepad, SDL_Joystick, SDL_Cursor, SDL_AudioStream, SDL_GPUDevice, SDL_Camera) are
exposed as strongly-typed, disposable handle structs (SdlWindow, SdlRenderer, ...). Three rules
matter:
- Dispose what you create. Handles returned by a
Create*/Open*ergonomic creator are owned by you; callDispose()(orusing) exactly once to release them.Dispose()on a null/default handle is a guarded no-op, so it is safe to dispose an uninitialized handle. - Do not dispose borrowed handles. Handles returned by a
Get*lookup (Sdl.Video.GetWindowFromID,Sdl.Gamepad.GetGamepadJoystick,Sdl.Mouse.GetCursor,Sdl.Render.GetRenderer, ...) are borrowed - owned by SDL or by another object - and are documented with a "Borrowed handle" warning. Disposing one frees a resource you do not own and corrupts SDL's state. These lookups return a null handle (not an exception) on failure; checkIsNull(). - Handles are values; aliasing is your responsibility. A handle is a thin wrapper over a raw pointer with value semantics: copying one produces two handles referring to the same native object. Disposing one and then using (or disposing) the copy is a use-after-free / double-free. Keep a single owning copy; do not dispose the same underlying resource twice. This trade-off is shared with the rest of the BRUTAL interop family and is not reshaped for v1.
Threading
SDL is not free-threaded. Initialize SDL, create/destroy windows, and pump the event queue from your process's main thread - macOS (Cocoa) requires it, and there is no runtime guard in v1, so crossing threads is undefined behaviour rather than a caught error. See Initialization.
Feature matrix
| Subsystem | Ergonomic wrapper (Sdl.*) |
Raw only (Sdl.PInvoke.*) |
|---|---|---|
| Init/shutdown, app metadata | Sdl.Init |
- |
| Windowing, displays, OpenGL context | Sdl.Video |
- |
| Event queue, watches | Sdl.Events |
- |
| Keyboard, text input | Sdl.Keyboard (typed SdlKeycode enum) |
- |
| Mouse, cursors | Sdl.Mouse (typed SdlMouseButtonFlags flags enum) |
- |
| Gamepad | Sdl.Gamepad |
Device enumeration (GetGamepads) |
| Joystick | Sdl.Joystick |
Device enumeration (GetJoysticks) |
| Logging | Sdl.Log |
Priority/category configuration (SDL_log.h) |
| Audio | - | Everything (SDL_audio.h) |
GPU (SDL_gpu.h) |
- | Everything |
2D rendering (SDL_render.h) |
Sdl.Render |
Texture palette access (GetTexturePalette), Metal layer/command-encoder queries, RenderReadPixels, custom GPU render state (SDL_GPURenderState) |
Vulkan surface support (SDL_vulkan.h) |
Sdl.Vulkan[^vulkan] |
SDL_Vulkan_LoadLibrary/GetVkGetInstanceProcAddr/UnloadLibrary/GetPresentationSupport |
| Camera | - | Everything |
| Haptics | - | Everything |
| Sensors | - | Everything |
| Clipboard, filesystem, storage, process, power, locale, timers, threads, and everything else | - | Everything |
Image loading/saving (SDL_image, separate Ahwoo.BRUTAL.Sdl.Image package)[^image] |
SdlImage.* |
In-memory XPM array loaders (ReadXPMFromArray/ReadXPMFromArrayToRGB888) |
Font rendering (SDL_ttf, separate Ahwoo.BRUTAL.Sdl.Ttf package)[^ttf] |
SdlTtf.* |
GPU/renderer-backed text engines (TTF_CreateRendererTextEngine, TTF_CreateGPUTextEngine) |
Audio mixing (SDL_mixer, separate Ahwoo.BRUTAL.Sdl.Mixer package)[^mixer] |
SdlMixer.* |
- |
Every subsystem - curated or not - is fully usable today through Sdl.PInvoke.*; "raw only" means
no bool/out/throwing sugar has been added yet, not that the functionality is unavailable.
[^image]: SDL_image ships as its own package/assembly rather than a subsystem of core Sdl, so its
ergonomic entry point is SdlImage.* (with SdlImage.PInvoke.* as its raw layer), not Sdl.*. Its
build enables the dependency-free codecs (PNG, JPG, BMP, GIF, QOI, TGA, and others) and disables the
ones that pull in large third-party codec libraries (WEBP, AVIF, JXL, TIFF) - see
src/Brutal.Sdl.Image/Brutal.Sdl.Image.csproj.
[^ttf]: SDL_ttf likewise ships as its own package/assembly, so its ergonomic entry point is
SdlTtf.* (with SdlTtf.PInvoke.* as its raw layer), not Sdl.*. Its build vendors and statically
links FreeType (rasterization) and HarfBuzz (Unicode text shaping); PlutoSVG (color-emoji glyphs) is
not enabled in v1 - see src/Brutal.Sdl.Ttf/Brutal.Sdl.Ttf.csproj. Rendering to an SdlSurface*
(RenderText*/the surface text engine) needs no SDL video subsystem at all; the renderer- and
GPU-backed text engines are bound on the raw layer but need a live SdlRenderer/GPU device, so they
are exercised only by hand, not in this repo's headless CI test suite.
[^mixer]: SDL_mixer likewise ships as its own package/assembly, so its ergonomic entry point is
SdlMixer.* (with SdlMixer.PInvoke.* as its raw layer), not Sdl.*. It binds SDL_mixer 3.2's
rewritten object-model API (MIX_Mixer/MIX_Track/MIX_Audio/MIX_Group/MIX_AudioDecoder) and
compiles in WAV/AIFF/VOC/AU natively plus the header-only stb_vorbis (OGG), dr_flac (FLAC), and
dr_mp3 (MP3) decoders - no vendored/system codec library required - see
src/Brutal.Sdl.Mixer/Brutal.Sdl.Mixer.csproj. Unlike SdlImage/SdlTtf, most of this package is
device-free: MIX_Init() + MIX_CreateMixer() need no SDL_INIT_AUDIO at all, rendering
instead into a caller-owned memory buffer via MIX_Generate - see
the SdlMixer guide. MIX_CreateMixerDevice, the one
device-touching creator, opens a real playback device when the platform has one, but the Linux
packages this project's CI currently builds against ship SDL without a real audio backend
available (no ALSA/PulseAudio/PipeWire device on the runner), so MIX_CreateMixerDevice reliably
fails there; MixerPlaybackSample and this package's test suite are both written to work correctly
either way (falling back to the device-free MIX_Generate path), but real-device playback itself is
not currently exercised on Linux CI.
[^vulkan]: Sdl.Vulkan covers only the three entry points needed to create/destroy a VkSurfaceKHR
for an SdlWindow (SDL_Vulkan_GetInstanceExtensions, SDL_Vulkan_CreateSurface,
SDL_Vulkan_DestroySurface) - see src/Brutal.Sdl/Manual/Libraries/Sdl.Vulkan.cs. SDL_vulkan.h is
not part of the generator's input header, so unlike the rest of this table these are hand-written,
not generated; Vulkan's opaque handles (VkInstance, VkSurfaceKHR) are spelled as nint rather
than referencing Ahwoo.BRUTAL.Vulkan's own types of the same name, since Brutal.Sdl takes
no dependency on Vulkan - see VulkanSurfaceSample for how a consumer bridges the two packages'
handles.
For Developers
Prerequisites
- .NET SDK (version 10.0 or later)
- Just
- CMake
- Ninja
- A C++ compiler:
- Windows: Visual Studio 2022 (x64)
- Linux: GCC (x64)
- Mac: Xcode and Xcode Command Line Tools (Apple Silicon)
- The vendored SDL3 source is a git submodule; clone with
--recurse-submodulesor rungit submodule update --init src/Brutal.Sdl.Native/SDLafter cloning.
Compilation and Testing
To build the solution locally, run the following command in the repository root:
just build
To run the tests, run the following command in the repository root:
just test
To try the samples:
just sample QuickStart
Acknowledgments
Ahwoo.BRUTAL.Sdl.Ttf vendors and statically links FreeType 2.13.2 and
HarfBuzz as part of its SDL_ttf build.
See src/Brutal.Sdl.Ttf/LICENSES/BRUTAL-SDL-TTF-THIRD-PARTY-NOTICES.txt for the full third-party
license texts and credits (FreeType, HarfBuzz, and SDL_ttf itself).
Contributing
If you find a bug or have a feature request, please open an issue on GitHub. If you'd like to contribute code, feel free to fork the repository and submit a pull request.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Ahwoo.BRUTAL.Core (>= 2026.10.1)
- Ahwoo.BRUTAL.Interop (>= 2026.10.1)
- Ahwoo.BRUTAL.Logging (>= 2026.10.2)
NuGet packages (4)
Showing the top 4 NuGet packages that depend on Ahwoo.BRUTAL.Sdl:
| Package | Downloads |
|---|---|
|
Ahwoo.BRUTAL.Sdl.Ttf
Modern C# bindings for SDL_ttf, SDL3's TrueType font rendering library. |
|
|
Ahwoo.BRUTAL.Sdl.Mixer
Modern C# bindings for SDL_mixer, SDL3's audio mixing library. |
|
|
Ahwoo.BRUTAL.Sdl.Image
Modern C# bindings for SDL_image, SDL3's image loading library. |
|
|
com.IvanMurzak.Brutal.MCP.Sdl
Brutal-MCP extension for Ahwoo.BRUTAL.Sdl: exposes the SDL windows of the game (title, position, size, flags) to AI agents through RQL. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2026.10.2 | 1,175 | 10/15/2026 |
Rebuilds against Strings 2026.10.1 and the packages released on it. The previous 2026.10 release still pulled Brutal.T4 in through Strings 2026.10.0; this one no longer does.
No public API changes.
**Full Changelog:** [v2026.10.1...v2026.10.2](https://github.com/RocketWerkz/brutal.sdl/compare/v2026.10.1...v2026.10.2)