Recursive AI

Collect Vulkan validation messages in C#

OneGpu is a free, open-source GPU compute and rendering library for .NET from Recursive AI, on Vulkan, Metal and WebGPU. MIT or Apache-2.0, no bundled native binaries.

Collect Vulkan diagnostics before and after device disposal. Requires Windows or Linux, Vulkan 1.3 and the Khronos validation layer.

Terminal
dotnet add package \
    RecursiveAI.OneGpu.Backends

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

C#
using RecursiveAI.OneGpu;
 
var device = GpuBackends.CreateDevice(new()
{
    Adapter = "discrete", Validation = true,
    RequireValidation = true
});
using (var buffer =
    device.CreateBuffer<uint>(4)) { }
var before = device.TakeValidationMessages();
device.Dispose();
var after = device.TakeValidationMessages();
Console.WriteLine($"Messages: " +
    (before.Length + after.Length));
foreach (var message in before.Concat(after))
    Console.WriteLine(message);

Collects both batches of messages; this run prints:

Text
Messages: 0

Unzip the example, then dotnet run.

The full program

C#
using RecursiveAI.OneGpu;
 
var device = GpuBackends.CreateDevice(new()
{
    Adapter = "discrete",
    Validation = true,
    RequireValidation = true
});
try
{
    Console.WriteLine($"Adapter: {device.Adapter.Name}");
    using (var buffer = device.CreateBuffer<uint>(4))
    {
        buffer.Write([1u, 2u, 3u, 4u]);
    }
    var messages = device.TakeValidationMessages();
    Console.WriteLine($"Before disposal: {messages.Length}");
    foreach (var message in messages)
        Console.WriteLine(message);
}
finally
{
    device.Dispose();
    var messages = device.TakeValidationMessages();
    Console.WriteLine($"After disposal: {messages.Length}");
    foreach (var message in messages)
        Console.WriteLine(message);
}

Output on Linux with an NVIDIA GeForce RTX 4070 via Vulkan:

Text
Adapter: NVIDIA GeForce RTX 4070
Before disposal: 0
After disposal: 0

The full program disposes the buffer before the device, and collects teardown messages in finally. Without the required layer, device creation throws GpuUnsupportedException unless ONEGPU_VALIDATION_OPTIONAL=1 overrides the requirement; leave that override unset when collecting diagnostics.

Variations

To target a name rather than a device kind, replace the Adapter setting with:

C#
Adapter = "RTX",

On this machine, the full program still reports the NVIDIA GeForce RTX 4070 and zero messages before and after disposal. ONEGPU_ADAPTER, if set, overrides this selection.

To see teardown diagnostics, deliberately leave one buffer undisposed in a separate run:

C#
using RecursiveAI.OneGpu;
 
var device = GpuBackends.CreateDevice(new()
{
    Adapter = "discrete", Validation = true,
    RequireValidation = true
});
var buffer = device.CreateBuffer<uint>(4);
// Deliberately omit buffer.Dispose() for this diagnostic example.
device.Dispose();
var messages = device.TakeValidationMessages();
Console.WriteLine($"After disposal: {messages.Length}");
foreach (string message in messages)
    Console.WriteLine(message);
GC.KeepAlive(buffer);

Actual output with the installed Khronos validation layer on the same Vulkan device (object handles vary between runs):

Text
After disposal: 1
[Error] vkDestroyDevice(): Object Tracking - For VkDevice 0x5deca7c6bae0, VkBuffer 0xb000000000b has not been destroyed.
The Vulkan spec states: All child objects created on device that can be destroyed or freed must have been destroyed or freed prior to destroying device (https://docs.vulkan.org/spec/latest/chapters/devsandqueues.html#VUID-vkDestroyDevice-device-05137)

This variation intentionally leaks a buffer to demonstrate the message; keep the normal disposal pattern in application code.

The short answer

Set both Validation = true and RequireValidation = true when creating the OneGpu device. Dispose its resources, collect TakeValidationMessages(), dispose the device, and collect again. Keep both returned arrays. The second call matters because device destruction can produce diagnostics that did not exist during the first call.

A zero-length array means this collection received no messages. It does not prove that an algorithm produces the right result or that every possible API misuse was checked. Check computed outputs separately. The main example disposes its buffer correctly; the variation deliberately omits disposal to show a real teardown message.

How do you enable Vulkan validation layers in C#?

Install the Khronos validation layer separately from the NuGet package. As the Vulkan tutorial explains, layers are optional components that intercept API calls; the Vulkan API does not include them automatically. The standard layer is named VK_LAYER_KHRONOS_validation. A graphics driver alone is not evidence that this layer is installed.

On Windows, the layer comes with the LunarG Vulkan SDK.

On Linux, install your distribution’s Vulkan validation-layer package, or install the Vulkan SDK. For a layer in a nonstandard directory, point VK_ADD_LAYER_PATH at the directory containing its JSON manifest and make its shared library discoverable. Then run the example with a Vulkan-capable adapter. ONEGPU_BACKEND=vulkan selects this backend explicitly; the desktop default on Windows and Linux is Vulkan.

The two options do different jobs. Validation requests the layer. RequireValidation makes its absence an error instead of allowing a diagnostic run to continue without it. OneGpu also has the ONEGPU_VALIDATION_OPTIONAL environment override, so check the environment when a required-layer test unexpectedly continues. The snippet does not install anything or modify the machine’s layer configuration.

TakeValidationMessages() returns a string[]: each element is warning or error message text, not a record with structured fields. Severity, object handles and validation identifiers are part of the text when the layer supplies them; there are no separate fields for them.

The call drains the collected messages: save or print each returned array before collecting again. The device-attribution contract explains why a collection belongs to a device even though Vulkan devices share an instance and a callback. The backend tracks which device created each object. A message goes to the owner of the first object it names. Messages raised during device creation or destruction go to that device. A message with no object owned by any device goes to every live device rather than being discarded.

That last case matters when several sessions or devices run in one process. Do not assume that every line identifies a resource belonging exclusively to the device whose log you are reading. Preserve the message text and object information when investigating it, and do not deduplicate solely by which collection returned it.

Finally, collect after disposal. The layer can list leaked objects when vkDestroyDevice runs. A log taken only while the device is alive misses that point in the lifecycle. Use a finally block, as the full program does, so ordinary exceptions in the workload do not skip device disposal and the final collection. Dispose buffers, bind groups and other owned resources first; an explicit scope makes that order visible in the code.

All onegpu recipes