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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Ahwoo.BRUTAL.Sdl" Version="2026.10.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Ahwoo.BRUTAL.Sdl" Version="2026.10.2" />
                    
Directory.Packages.props
<PackageReference Include="Ahwoo.BRUTAL.Sdl" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Ahwoo.BRUTAL.Sdl --version 2026.10.2
                    
#r "nuget: Ahwoo.BRUTAL.Sdl, 2026.10.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Ahwoo.BRUTAL.Sdl@2026.10.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Ahwoo.BRUTAL.Sdl&version=2026.10.2
                    
Install as a Cake Addin
#tool nuget:?package=Ahwoo.BRUTAL.Sdl&version=2026.10.2
                    
Install as a Cake Tool

Brutal Sdl

Build

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.

NuGet Packages

For Users

Prerequisites

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:

  • bool returns/parameters instead of SDL's 1-byte C bool (byte).
  • out T parameters for pure output pointers (SDL_GetWindowSize(window, out w, out h) instead of a fixed block).
  • Throwing entry points where a raw call would otherwise return false/null and expect you to check SDL_GetError yourself. Sdl.Init.Initialize throws SdlException; the ergonomic handle-returning creators (Sdl.Video.CreateWindow, Sdl.Mouse.CreateCursor, Sdl.Gamepad.OpenGamepad, Sdl.Render.CreateRenderer/CreateTexture/CreateSoftwareRenderer, ...) throw SdlException instead 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 an SdlCallbackException - 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; call Dispose() (or using) 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; check IsNull().
  • 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:
  • The vendored SDL3 source is a git submodule; clone with --recurse-submodules or run git submodule update --init src/Brutal.Sdl.Native/SDL after 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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)