![]() |
VOOZH | about |
dotnet add package SdlVulkan.Renderer --version 6.3.1121
NuGet\Install-Package SdlVulkan.Renderer -Version 6.3.1121
<PackageReference Include="SdlVulkan.Renderer" Version="6.3.1121" />
<PackageVersion Include="SdlVulkan.Renderer" Version="6.3.1121" />Directory.Packages.props
<PackageReference Include="SdlVulkan.Renderer" />Project file
paket add SdlVulkan.Renderer --version 6.3.1121
#r "nuget: SdlVulkan.Renderer, 6.3.1121"
#:package SdlVulkan.Renderer@6.3.1121
#addin nuget:?package=SdlVulkan.Renderer&version=6.3.1121Install as a Cake Addin
#tool nuget:?package=SdlVulkan.Renderer&version=6.3.1121Install as a Cake Tool
SDL3 + Vortice.Vulkan rendering library built on DIR.Lib primitives.
SdlVulkanWindow — SDL3 window with Vulkan instance and surface lifecycle. Creates maximized, resizable windows with Vulkan surface.VulkanContext — Vulkan device, command buffers, sync management. MaxFramesInFlight = 2 with per-frame vertex buffers. Two construction modes:
VulkanContext.Create(instance, surface, w, h, ...) — on-screen path with a swapchain tied to an SdlVulkanWindow.VulkanContext.CreateOffscreen(instance, w, h, ...) — headless path rendering to a standalone VkImage; no surface, no swapchain, no SDL window. See Headless / offscreen rendering below.VkRenderer — Renderer<VulkanContext> implementation with FillRectangle, DrawRectangle, FillEllipse, DrawText, plus batched glyph and persistent-vertex-buffer draw APIs. Exposes FontAtlasDirty so callers can trigger redraws after glyph rasterization. Has BeginFrame / BeginOffscreenFrame variants that match the two VulkanContext modes.VkPipelineSet — GLSL 450 shader compilation and Vulkan pipeline creation (flat, textured, ellipse, stroke, SDF, blend variants).VkFontAtlas — Dynamic bitmap glyph atlas with ManagedFontRasterizer (from DIR.Lib) rasterization and Vulkan texture upload. Supports grow (512→4096), deferred eviction, and skipUnflushed to prevent sampling stale GPU texture data.VkSdfFontAtlas — Signed-distance-field glyph atlas side-car for resolution-independent text. SdfRasterSize = 128, fwidth-driven AA in the fragment shader auto-tunes to ±0.5 screen pixels at any zoom. Single-channel R8_Unorm texture, keyed on (font, size, character, charCode) so CID subset fonts don't collide.Per frame:
BeginFrame() / BeginOffscreenFrame() — handles deferred eviction, runs OnPreFlush (pre-warm callback), calls Flush(cmd) on both atlases, runs OnPreRenderPass (texture uploads), then BeginRenderPass.Flush(cmd) — uploads dirty staging region to GPU via vkCmdCopyBufferToImage.DrawText(...) → GetGlyph(...) — cache hit returns UV coords; miss rasterizes into staging.GetGlyph(..., skipUnflushed: true) — in draw loops, returns zero-width for glyphs not yet uploaded. Pair with PreWarmGlyph in OnPreFlush if drawing a glyph that wasn't shown last frame (first-frame glyph flicker). For SDF text, PreWarmSdfGlyph is the equivalent; PreWarmSdfGlyphBatch warms many glyphs in one call with parallel rasterization — preferred when a page introduces tens-to-hundreds of unique glyphs.Thread safety: vkDeviceWaitIdle() before reusing the shared upload buffer (prevents race with MaxFramesInFlight = 2).
Diagnostic logging (Console.Error): [FontAtlas] / [VkRenderer] prefixed lines for Flush, Grow, EvictAll, cache miss, Resize. Capture with 2>stderr.log.
using SdlVulkan.Renderer;
using var window = SdlVulkanWindow.Create("My App", 800, 600);
window.GetSizeInPixels(out var w, out var h);
var ctx = VulkanContext.Create(window.Instance, window.Surface, (uint)w, (uint)h);
var renderer = new VkRenderer(ctx, (uint)w, (uint)h);
while (running)
{
if (!renderer.BeginFrame(bgColor)) { renderer.Resize(w, h); continue; }
renderer.FillRectangle(rect, color);
renderer.DrawText("Hello", fontPath, 14f, white, layout);
renderer.EndFrame();
if (renderer.FontAtlasDirty) needsRedraw = true;
}
VulkanContext.CreateOffscreen builds a context that renders to a single VkImage instead of a swapchain. No VkSurfaceKHR, no SDL window, no VK_KHR_swapchain device extension requested — useful for tests, thumbnail / raster workers, CI without a display server, and server-side rendering.
using SdlVulkan.Renderer;
using Vortice.Vulkan;
using static Vortice.Vulkan.Vulkan;
// Minimal instance — no windowing extensions needed.
vkInitialize().CheckResult();
VkInstanceCreateInfo ici = new();
vkCreateInstance(&ici, null, out var instance).CheckResult();
const uint W = 1920, H = 1080;
using var ctx = VulkanContext.CreateOffscreen(instance, W, H);
using var renderer = new VkRenderer(ctx, W, H);
renderer.BeginOffscreenFrame(new DIR.Lib.RGBAColor32(255, 255, 255, 255));
renderer.FillRectangle(new RectInt(new(100, 100), new(400, 300)), red);
renderer.DrawText("Hello", fontPath, 14f, black, layout);
renderer.EndOffscreenFrame();
ctx.WaitOffscreenFrameComplete();
byte[] rgba = ctx.ReadbackOffscreenRgba(); // top-down, 4 bytes per pixel
// Pipe rgba into PNG encoder / image library of choice.
The offscreen path reuses all existing pipelines, MSAA slots, sync objects, and vertex ring buffers. Only the swapchain acquire/present is replaced by vkCmdCopyImageToBuffer into a host-visible staging buffer.
At runtime you need the Vulkan loader plus an ICD (driver). Nothing else: no X11 / Wayland, no SDL native, no DirectX.
vulkan-1.dll (shipped with any modern GPU driver; also installable via the Vulkan SDK).libvulkan.so.1 + an ICD. Options in increasing order of portability:
radv / intel_anv / NVIDIA proprietary) — fastest.lavapipe / llvmpipe — software rasterizer. 5–20× slower but fully headless. Apt: mesa-vulkan-drivers.SDL3-CS remains a package reference because the on-screen path uses it. Its P/Invokes are lazy — libSDL3.so / SDL3.dll is never loaded if SdlVulkanWindow is not instantiated, so the offscreen path has no SDL runtime dependency.
On a fresh Ubuntu runner (GitHub Actions ubuntu-latest, Azure Pipelines, GitLab):
- run: sudo apt-get update && sudo apt-get install -y libvulkan1 mesa-vulkan-drivers vulkan-tools
- run: dotnet test
vulkan-tools is optional but gives you vulkaninfo --summary for sanity-checking which ICD the runner loaded. For deterministic behaviour across runners, pin to lavapipe:
- run: echo "VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json" >> $GITHUB_ENV
Containers: add the same two packages to your image. The offscreen path doesn't require a tty, display variable, or privileged mode.
Host a native browser view inside an SdlVulkanWindow. The window system composites it
over the Vulkan swapchain as a child surface — it does not go through the Vulkan render
pass. Shipped as separate packages so core renderer consumers pull no webview dependency:
| Package | Purpose |
|---|---|
SdlVulkan.Renderer.WebView |
The INativeWebView abstraction + platform backends. Multi-targets net10.0 (interface, factory + the Linux/WebKitGTK backend) and net10.0-windows (adds the Windows/WebView2 backend + its native dependency). |
SdlVulkan.Renderer.WebView.Native |
WebView2Loader.dll native assets per Windows RID (x64 / arm64 / x86). Pulled in transitively on Windows — no need to reference it directly. |
dotnet add package SdlVulkan.Renderer.WebView
Backends:
Win32WebView, WebView2 (Edge/Chromium) via the WebView2Aot Native-AOT bindings. Needs the Microsoft Edge WebView2 Runtime installed: that runtime is a system component and is not redistributed here; only the loader DLL ships (in the .Native package).GtkWebView, WebKitGTK 4.1 embedded via X11 (XReparentWindow into SDL's window; GTK runs its own loop on a dedicated thread). Requires the SDL window on the X11 driver — run with SDL_VIDEODRIVER=x11 (works under Wayland sessions via XWayland) — and the WebKitGTK 4.1 + GTK3 runtime (apt install libwebkit2gtk-4.1-0 on Debian/Ubuntu; present on most desktops).WKWebView; backend is present but not yet implemented.using SdlVulkan.Renderer;
using SdlVulkan.Renderer.WebView;
using DIR.Lib;
using var window = SdlVulkanWindow.Create("App", 1280, 800);
using var web = NativeWebView.Create(); // platform backend via the factory
web.NavigationCompleted += url => Console.WriteLine($"loaded {url}");
web.ConsoleMessage += (level, text) => Console.WriteLine($"[{level}] {text}");
web.PageError += err => Console.WriteLine($"JS error: {err}");
web.AttachToWindow(window); // parents the webview into the window's HWND
web.Navigate("https://example.com");
window.GetSizeInPixels(out var w, out var h);
web.SetBounds(new RectInt(new PointInt(w, h), new PointInt(0, 0))); // window pixel coords
string href = await web.ExecuteScriptAsync("location.href"); // JSON-encoded result
For two-way native↔web interaction, MessageReceived surfaces the page's
window.chrome.webview.postMessage(...) calls (as raw JSON) and PostMessage(json) sends the
other way (the page receives it on chrome.webview's message event) — build whatever
request/response protocol you want on top. The Linux backend injects a small window.chrome.webview
shim at document-start (mapping to WebKit's messageHandlers), so the same page JS API works on
both backends.
SetBounds is host→browser (you position the webview). For the reverse direction — letting the
page's content drive the webview's size — wrap it in a WebViewContentSizer. It injects a small
ResizeObserver reporter after each navigation, surfaces the page's content size (in device
pixels, ready for SetBounds) as ContentSizeChanged, and can drive the bounds for you:
using var web = NativeWebView.Create();
using var sizer = new WebViewContentSizer(web); // construct before navigating
// Grow/shrink the webview's height to fit the page; width stays fixed at `w`, top-left at (0,0).
sizer.EnableAutoSize(origin: new PointInt(0, 0), fixedExtent: new PointInt(w, 0), axis: AutoSizeAxis.Height);
sizer.ContentSizeChanged += size => Console.WriteLine($"content is {size.X}x{size.Y} device px");
web.AttachToWindow(window);
web.NavigateToString("<h1>I size myself</h1>");
AutoSizeAxis.Height is the default and the safe choice: a fixed width never reflows the page, so it
can't oscillate. Width/Both track the other axis but are prone to reflow feedback. The __sdlLayout
message key is reserved for this protocol — layout envelopes are consumed by the sizer and never
reach its MessageReceived, so subscribe to sizer.MessageReceived to get only your app's messages.
No INativeWebView change is involved; the sizer is built entirely on the existing message bridge.
Beyond navigation, both backends surface diagnostics: Trace (redirect/load chain), ConsoleMessage
(console.*), and PageError (uncaught JS exceptions, with stack). On Windows these come from the
WebView2 DevTools Protocol; on Linux from in-page hooks forwarded over dedicated script-message
channels.
Backend notes:
g_idle_add). Events (MessageReceived,
TitleChanged, …) are raised on the GTK thread, so handlers must be thread-safe or marshal back
themselves.MIT
This library exists because the TianWen project needed a path to HDR display output (HDR10, scRGB, wide color gamut) that its previous Silk.NET + OpenGL stack could not deliver. The investigation below documents the alternatives considered and why SDL3 (via edwardgushchin/SDL3-CS) + Vortice.Vulkan won. This is decision rationale, not documentation of current features — check the types list above for what the library actually does today.
GLFW_FLOAT_PIXEL_TYPE patch was
never merged.WindowHintBool ends at SrgbCapable / DoubleBuffer — no float pixel
type or HDR color space.Vulkan supports HDR output via VK_EXT_swapchain_colorspace + HDR10 surface formats.
| Platform | OpenGL | Vulkan | HDR possible? |
|---|---|---|---|
| Windows | Native | Native | Yes (Vulkan HDR swapchain) |
| Linux | Native | Native | Yes (if compositor supports) |
| macOS | Deprecated (frozen at 4.1) | MoltenVK | No (Metal HDR needs separate path) |
| Android | OpenGL ES | Native | Yes (Android 10+) |
| iOS | OpenGL ES (deprecated) | MoltenVK | No (same as macOS) |
| Web/WASM | WebGL | No | No |
Shader migration effort: low. GLSL shaders compile to SPIR-V with minimal mechanical
changes (#version 330 core to #version 450, uniforms packed into UBOs, explicit
layout(binding=N), build-time compile via glslc / glslangValidator or runtime
via Vortice.ShaderCompiler). Shader math (MTF stretch, Hermite soft-knee, WCS
deprojection, histogram) stays identical.
API migration effort: high. The real work was replacing ~2000 lines of OpenGL API calls with swapchain setup, descriptor sets, pipeline objects, command buffers, and synchronization.
develop/3.0 branch exists, tracking issue #209
open since June 2020 (5.5+ years). Complete rewrite of bindings generation, no release
date, lead developer (Perksey) less active, WebGPU bindings planned.GpuInterop sample with Vulkan demo via CompositionDrawingSurface.SDL_GPU abstraction (Vulkan/D3D12/Metal
with automatic shader cross-compilation).SDL_COLORSPACE_SRGB.delegate* unmanaged
function pointers, no P/Invoke). IsAotCompatible = true.edwardgushchin/SDL3-CS uses LibraryImport (source-generated, AOT-safe) — preferred
over ppy/SDL3-CS which uses old DllImport. SDL3-CS.Native NuGet ships desktop
natives; Android works but needs manual lib bundling.
| Platform | Vulkan | SDL3 native | AOT | HDR |
|---|---|---|---|---|
| Windows x64 | Native | NuGet | Yes | Yes (Vulkan HDR swapchain) |
| Windows ARM64 | Native | NuGet | Yes | Yes |
| Linux x64 | Native (Mesa/NVIDIA) | NuGet | Yes | Possible (Wayland + Vulkan) |
| Linux ARM64 | Native (Mesa) | NuGet | Yes | Limited |
| macOS x64 | MoltenVK | NuGet | Yes | MoltenVK limitations |
| macOS ARM64 | MoltenVK | NuGet | Yes | MoltenVK limitations |
| Android | Native | Manual bundling | Partial | Yes |
| iOS | MoltenVK (must bundle) | Not shipped | Yes | Limited |
SDL3 HDR support: SDL.window.HDR_enabled, SDL.window.SDR_white_level,
SDL.window.HDR_headroom display properties, plus PQ (ST 2084) and HLG transfer
characteristics. Combined with Vulkan VK_COLOR_SPACE_HDR10_ST2084_EXT swapchain,
full HDR output is achievable.
SDL3 Vulkan surface creation: SDL.VulkanLoadLibrary() auto-finds MoltenVK on macOS;
SDL.VulkanCreateSurface() returns a VkSurfaceKHR that pairs directly with
Vortice.Vulkan.
GLFW 3.4 changed Vulkan detection on macOS; glfwVulkanSupported() can't find the
Vulkan loader even though Silk.NET ships it (Silk.NET.Vulkan.Loader.Native). GLFW 3.4
added glfwInitVulkanLoader() which could solve this.
Possible fixes: call glfwInitVulkanLoader() with a custom vkGetInstanceProcAddr
before glfwInit(); set VK_ICD_FILENAMES at the bundled MoltenVK ICD; ensure the
Vulkan loader is on DYLD_LIBRARY_PATH.
Status: no PRs submitted, zero maintainer engagement on the issue. Silk.NET 2.x is in maintenance mode (14-month gap between 2.22 and 2.23), team focused on 3.0. Trivial PRs merge in 0-11 days; no evidence of substantive external feature PRs merging recently.
| Path | macOS fix | HDR | Effort | Risk |
|---|---|---|---|---|
| Fix Silk.NET upstream | Small PR, may wait months | Blocked by GLFW | Low for macOS, impossible for HDR | PR rot |
| Vortice.Vulkan + SDL3-CS | SDL3 auto-detects MoltenVK | Full HDR built into SDL3 | High (rewrite renderer) | Two active projects |
| Option | Maintenance | Vulkan | HDR | AOT | Migration | Shaders kept? |
|---|---|---|---|---|---|---|
| Silk.NET 2.x (stay) | Moderate | Via 3.0 someday | No | Yes | None | Yes |
| Silk.NET 2.x + macOS PR | Moderate | Yes (with fix) | No | Yes | None | Yes |
| SDL3 + OpenGL | Excellent | Surface only | No | Yes | Medium | Yes |
| SDL3 + SDL_GPU | Excellent | Under the hood | Possible | Yes | Medium-high | Rewrite to SDL_GPU |
| Vortice.Vulkan + SDL3 (chosen) | Good | Full | Yes | Yes | Very high | GLSL to SPIR-V |
| Evergine Vulkan.NET + SDL3 | Excellent | Full | Yes | Yes | Very high | GLSL to SPIR-V |
| Avalonia + Vulkan interop | Excellent | Yes (interop) | No | Improving | Very high | Rewrite |
| WebGPU/wgpu | Weak (.NET) | Under the hood | Not yet | Possible | High | GLSL to WGSL |
SDL3 + OpenGL HDR was corrected to No during evaluation: SDL3's OpenGL renderer
hardcodes SDL_COLORSPACE_SRGB as the only accepted output. No float pixel formats,
no scRGB, no HDR10 via OpenGL on any platform.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 net10.0 is compatible. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
Showing the top 1 NuGet packages that depend on SdlVulkan.Renderer:
| Package | Downloads |
|---|---|
|
SdlVulkan.Renderer.WebView
Cross-platform native WebView (WebView2 / WebKitGTK / WKWebView) hosted inside an SdlVulkan.Renderer window. |
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 6.3.1121 | 37 | 6/15/2026 |
| 6.2.1111 | 38 | 6/13/2026 |
| 6.1.1091 | 32 | 6/13/2026 |
| 6.0.1071 | 71 | 6/11/2026 |
| 6.0.1051 | 29 | 6/11/2026 |
| 6.0.1031 | 28 | 6/11/2026 |
| 6.0.1011 | 35 | 6/10/2026 |
| 6.0.991 | 31 | 6/9/2026 |
| 6.0.971 | 26 | 6/9/2026 |
| 6.0.951 | 37 | 6/8/2026 |
| 6.0.931 | 32 | 6/8/2026 |
| 6.0.911 | 35 | 6/8/2026 |
| 6.0.891 | 32 | 6/8/2026 |
| 6.0.871 | 32 | 6/8/2026 |
| 6.0.851 | 32 | 6/7/2026 |
| 6.0.781 | 33 | 6/7/2026 |
| 6.0.761 | 27 | 6/7/2026 |
| 6.0.741 | 32 | 6/7/2026 |
| 6.0.721 | 31 | 6/3/2026 |
| 5.1.702 | 31 | 6/1/2026 |