---
title: "Collect Vulkan validation messages in C#"
url: https://recursiveai.org/onegpu/cookbook/collect-gpu-validation-messages/
description: "Enable Vulkan validation in C#, collect messages before and after device disposal, and keep teardown leak diagnostics with a runnable OneGpu example."
version: "1.0.0"
---

# 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.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneGpu.Backends) · [GitHub](https://github.com/recursive-ai-oss/onegpu-csharp) · [Docs](https://recursiveai.org/onegpu/docs/) · [Download example (.zip)](https://recursiveai.org/onegpu/cookbook/collect-gpu-validation-messages/collect-gpu-validation-messages.zip)

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

```shell
dotnet add package \
    RecursiveAI.OneGpu.Backends
```

Requires [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) (net10.0); .NET 9 and earlier aren’t supported.

```csharp
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](https://recursiveai.org/onegpu/cookbook/collect-gpu-validation-messages/collect-gpu-validation-messages.zip), then `dotnet run`.

## The full program

```csharp
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:

```csharp
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:

```csharp
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.

## Related recipes

- [Choose a Vulkan device](https://recursiveai.org/onegpu/cookbook/pick-a-vulkan-device/)
- [Run a compute shader in C#](https://recursiveai.org/onegpu/cookbook/run-a-compute-shader-in-csharp/)
- [Chain dependent compute passes](https://recursiveai.org/onegpu/cookbook/chain-dependent-compute-passes/)

## 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](https://docs.vulkan.org/tutorial/latest/03_Drawing_a_triangle/00_Setup/02_Validation_layers.html), 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](https://docs.vulkan.org/tutorial/latest/03_Drawing_a_triangle/00_Setup/02_Validation_layers.html).

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](https://recursiveai.org/onegpu/docs/compute/#validation-messages) 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.
