diff --git a/tests/Avalonia.DmaBufInteropTests/Avalonia.DmaBufInteropTests.csproj b/tests/Avalonia.DmaBufInteropTests/Avalonia.DmaBufInteropTests.csproj
new file mode 100644
index 0000000000..77be796875
--- /dev/null
+++ b/tests/Avalonia.DmaBufInteropTests/Avalonia.DmaBufInteropTests.csproj
@@ -0,0 +1,19 @@
+
+
+
+ Exe
+ $(AvsCurrentTargetFramework)
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Avalonia.DmaBufInteropTests/DmaBufAllocator.cs b/tests/Avalonia.DmaBufInteropTests/DmaBufAllocator.cs
new file mode 100644
index 0000000000..99b8c9286f
--- /dev/null
+++ b/tests/Avalonia.DmaBufInteropTests/DmaBufAllocator.cs
@@ -0,0 +1,208 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using static Avalonia.DmaBufInteropTests.NativeInterop;
+
+namespace Avalonia.DmaBufInteropTests;
+
+///
+/// Allocates DMA-BUF buffers with known pixel content for testing.
+/// Prefers GBM when available, falls back to udmabuf.
+///
+internal sealed unsafe class DmaBufAllocator : IDisposable
+{
+ private readonly int _drmFd;
+ private readonly IntPtr _gbmDevice;
+
+ public bool IsAvailable => _gbmDevice != IntPtr.Zero;
+
+ public DmaBufAllocator()
+ {
+ // Try render nodes in order
+ foreach (var path in new[] { "/dev/dri/renderD128", "/dev/dri/renderD129" })
+ {
+ if (!File.Exists(path))
+ continue;
+ _drmFd = Open(path, O_RDWR);
+ if (_drmFd < 0)
+ continue;
+ _gbmDevice = GbmCreateDevice(_drmFd);
+ if (_gbmDevice != IntPtr.Zero)
+ return;
+ Close(_drmFd);
+ _drmFd = -1;
+ }
+ }
+
+ ///
+ /// Allocates a DMA-BUF with the specified format and fills it with a solid ARGB color.
+ ///
+ public DmaBufAllocation? AllocateLinear(uint width, uint height, uint format, uint color)
+ {
+ if (_gbmDevice == IntPtr.Zero)
+ return AllocateViaUdmabuf(width, height, format, color);
+
+ var bo = GbmBoCreate(_gbmDevice, width, height, format,
+ GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR);
+ if (bo == IntPtr.Zero)
+ return null;
+
+ var fd = GbmBoGetFd(bo);
+ var stride = GbmBoGetStride(bo);
+ var modifier = GbmBoGetModifier(bo);
+
+ // Map and fill with color
+ uint mapStride;
+ IntPtr mapData = IntPtr.Zero;
+ var mapped = GbmBoMap(bo, 0, 0, width, height, GBM_BO_TRANSFER_WRITE, &mapStride, &mapData);
+ if (mapped != IntPtr.Zero)
+ {
+ var pixels = (uint*)mapped;
+ var pixelsPerRow = mapStride / 4;
+ for (uint y = 0; y < height; y++)
+ for (uint x = 0; x < width; x++)
+ pixels[y * pixelsPerRow + x] = color;
+ GbmBoUnmap(bo, mapData);
+ }
+
+ return new DmaBufAllocation(fd, stride, modifier, width, height, format, bo);
+ }
+
+ ///
+ /// Allocates a DMA-BUF with tiled modifier (if the GPU supports it).
+ ///
+ public DmaBufAllocation? AllocateTiled(uint width, uint height, uint format, uint color)
+ {
+ if (_gbmDevice == IntPtr.Zero)
+ return null;
+
+ // Use GBM_BO_USE_RENDERING without LINEAR to let the driver choose tiling
+ var bo = GbmBoCreate(_gbmDevice, width, height, format, GBM_BO_USE_RENDERING);
+ if (bo == IntPtr.Zero)
+ return null;
+
+ var modifier = GbmBoGetModifier(bo);
+ // If the driver gave us linear anyway, this isn't a useful tiled test
+ if (modifier == 0) // DRM_FORMAT_MOD_LINEAR
+ {
+ GbmBoDestroy(bo);
+ return null;
+ }
+
+ var fd = GbmBoGetFd(bo);
+ var stride = GbmBoGetStride(bo);
+
+ // Map and fill — for tiled BOs, GBM handles the detiling in map
+ uint mapStride;
+ IntPtr mapData = IntPtr.Zero;
+ var mapped = GbmBoMap(bo, 0, 0, width, height, GBM_BO_TRANSFER_WRITE, &mapStride, &mapData);
+ if (mapped != IntPtr.Zero)
+ {
+ var pixels = (uint*)mapped;
+ var pixelsPerRow = mapStride / 4;
+ for (uint y = 0; y < height; y++)
+ for (uint x = 0; x < width; x++)
+ pixels[y * pixelsPerRow + x] = color;
+ GbmBoUnmap(bo, mapData);
+ }
+
+ return new DmaBufAllocation(fd, stride, modifier, width, height, format, bo);
+ }
+
+ private static DmaBufAllocation? AllocateViaUdmabuf(uint width, uint height, uint format, uint color)
+ {
+ if (!File.Exists("/dev/udmabuf"))
+ return null;
+
+ var stride = width * 4;
+ var size = (long)(stride * height);
+
+ var memfd = MemfdCreate("dmabuf-test", MFD_ALLOW_SEALING);
+ if (memfd < 0)
+ return null;
+
+ if (Ftruncate(memfd, size) != 0)
+ {
+ Close(memfd);
+ return null;
+ }
+
+ // Map and fill
+ var mapped = Mmap(IntPtr.Zero, (nuint)size, PROT_READ | PROT_WRITE, MAP_SHARED, memfd, 0);
+ if (mapped != IntPtr.Zero && mapped != new IntPtr(-1))
+ {
+ var pixels = (uint*)mapped;
+ var count = width * height;
+ for (uint i = 0; i < count; i++)
+ pixels[i] = color;
+ Munmap(mapped, (nuint)size);
+ }
+
+ // Create DMA-BUF via udmabuf
+ var udmabufFd = Open("/dev/udmabuf", O_RDWR);
+ if (udmabufFd < 0)
+ {
+ Close(memfd);
+ return null;
+ }
+
+ var createParams = new UdmabufCreate
+ {
+ Memfd = memfd,
+ Flags = 0,
+ Offset = 0,
+ Size = (ulong)size
+ };
+
+ var dmaBufFd = Ioctl(udmabufFd, UDMABUF_CREATE, &createParams);
+ Close(udmabufFd);
+ Close(memfd);
+
+ if (dmaBufFd < 0)
+ return null;
+
+ return new DmaBufAllocation(dmaBufFd, stride, 0 /* LINEAR */, width, height, format, IntPtr.Zero);
+ }
+
+ public void Dispose()
+ {
+ if (_gbmDevice != IntPtr.Zero)
+ GbmDeviceDestroy(_gbmDevice);
+ if (_drmFd >= 0)
+ Close(_drmFd);
+ }
+}
+
+internal sealed class DmaBufAllocation : IDisposable
+{
+ public int Fd { get; }
+ public uint Stride { get; }
+ public ulong Modifier { get; }
+ public uint Width { get; }
+ public uint Height { get; }
+ public uint DrmFourcc { get; }
+ private IntPtr _gbmBo;
+
+ public DmaBufAllocation(int fd, uint stride, ulong modifier, uint width, uint height, uint drmFourcc,
+ IntPtr gbmBo)
+ {
+ Fd = fd;
+ Stride = stride;
+ Modifier = modifier;
+ Width = width;
+ Height = height;
+ DrmFourcc = drmFourcc;
+ _gbmBo = gbmBo;
+ }
+
+ public void Dispose()
+ {
+ if (_gbmBo != IntPtr.Zero)
+ {
+ NativeInterop.GbmBoDestroy(_gbmBo);
+ _gbmBo = IntPtr.Zero;
+ }
+ if (Fd >= 0)
+ NativeInterop.Close(Fd);
+ }
+}
diff --git a/tests/Avalonia.DmaBufInteropTests/NativeInterop.cs b/tests/Avalonia.DmaBufInteropTests/NativeInterop.cs
new file mode 100644
index 0000000000..38cf15b061
--- /dev/null
+++ b/tests/Avalonia.DmaBufInteropTests/NativeInterop.cs
@@ -0,0 +1,294 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Avalonia.DmaBufInteropTests;
+
+///
+/// P/Invoke bindings for GBM, DRM, and Linux kernel interfaces used for test buffer allocation.
+///
+internal static unsafe partial class NativeInterop
+{
+ // GBM
+ private const string LibGbm = "libgbm.so.1";
+
+ [LibraryImport(LibGbm, EntryPoint = "gbm_create_device")]
+ public static partial IntPtr GbmCreateDevice(int fd);
+
+ [LibraryImport(LibGbm, EntryPoint = "gbm_device_destroy")]
+ public static partial void GbmDeviceDestroy(IntPtr gbm);
+
+ [LibraryImport(LibGbm, EntryPoint = "gbm_bo_create")]
+ public static partial IntPtr GbmBoCreate(IntPtr gbm, uint width, uint height, uint format, uint flags);
+
+ [LibraryImport(LibGbm, EntryPoint = "gbm_bo_create_with_modifiers")]
+ public static partial IntPtr GbmBoCreateWithModifiers(IntPtr gbm, uint width, uint height, uint format,
+ ulong* modifiers, uint count);
+
+ [LibraryImport(LibGbm, EntryPoint = "gbm_bo_destroy")]
+ public static partial void GbmBoDestroy(IntPtr bo);
+
+ [LibraryImport(LibGbm, EntryPoint = "gbm_bo_get_fd")]
+ public static partial int GbmBoGetFd(IntPtr bo);
+
+ [LibraryImport(LibGbm, EntryPoint = "gbm_bo_get_stride")]
+ public static partial uint GbmBoGetStride(IntPtr bo);
+
+ [LibraryImport(LibGbm, EntryPoint = "gbm_bo_get_modifier")]
+ public static partial ulong GbmBoGetModifier(IntPtr bo);
+
+ [LibraryImport(LibGbm, EntryPoint = "gbm_bo_get_width")]
+ public static partial uint GbmBoGetWidth(IntPtr bo);
+
+ [LibraryImport(LibGbm, EntryPoint = "gbm_bo_get_height")]
+ public static partial uint GbmBoGetHeight(IntPtr bo);
+
+ [LibraryImport(LibGbm, EntryPoint = "gbm_bo_get_format")]
+ public static partial uint GbmBoGetFormat(IntPtr bo);
+
+ [LibraryImport(LibGbm, EntryPoint = "gbm_bo_map")]
+ public static partial IntPtr GbmBoMap(IntPtr bo, uint x, uint y, uint width, uint height,
+ uint flags, uint* stride, IntPtr* mapData);
+
+ [LibraryImport(LibGbm, EntryPoint = "gbm_bo_unmap")]
+ public static partial void GbmBoUnmap(IntPtr bo, IntPtr mapData);
+
+ // GBM flags
+ public const uint GBM_BO_USE_RENDERING = 1 << 2;
+ public const uint GBM_BO_USE_LINEAR = 1 << 4;
+
+ // GBM_BO_TRANSFER flags for gbm_bo_map
+ public const uint GBM_BO_TRANSFER_WRITE = 2;
+ public const uint GBM_BO_TRANSFER_READ_WRITE = 3;
+
+ // DRM format codes
+ public const uint GBM_FORMAT_ARGB8888 = 0x34325241; // DRM_FORMAT_ARGB8888
+ public const uint GBM_FORMAT_XRGB8888 = 0x34325258;
+ public const uint GBM_FORMAT_ABGR8888 = 0x34324241;
+
+ // libc
+ private const string LibC = "libc";
+
+ [LibraryImport(LibC, EntryPoint = "open", StringMarshalling = StringMarshalling.Utf8)]
+ public static partial int Open(string path, int flags);
+
+ [LibraryImport(LibC, EntryPoint = "close")]
+ public static partial int Close(int fd);
+
+ [LibraryImport(LibC, EntryPoint = "mmap")]
+ public static partial IntPtr Mmap(IntPtr addr, nuint length, int prot, int flags, int fd, long offset);
+
+ [LibraryImport(LibC, EntryPoint = "munmap")]
+ public static partial int Munmap(IntPtr addr, nuint length);
+
+ [LibraryImport(LibC, EntryPoint = "memfd_create", StringMarshalling = StringMarshalling.Utf8)]
+ public static partial int MemfdCreate(string name, uint flags);
+
+ [LibraryImport(LibC, EntryPoint = "ftruncate")]
+ public static partial int Ftruncate(int fd, long length);
+
+ [LibraryImport(LibC, EntryPoint = "ioctl")]
+ public static partial int Ioctl(int fd, ulong request, void* arg);
+
+ public const int O_RDWR = 0x02;
+ public const int PROT_READ = 0x1;
+ public const int PROT_WRITE = 0x2;
+ public const int MAP_SHARED = 0x01;
+ public const uint MFD_ALLOW_SEALING = 0x0002;
+
+ // udmabuf
+ public const ulong UDMABUF_CREATE = 0x40187542; // _IOW('u', 0x42, struct udmabuf_create)
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct UdmabufCreate
+ {
+ public int Memfd;
+ public uint Flags;
+ public ulong Offset;
+ public ulong Size;
+ }
+
+ // EGL
+ private const string LibEgl = "libEGL.so.1";
+
+ [LibraryImport(LibEgl, EntryPoint = "eglGetProcAddress", StringMarshalling = StringMarshalling.Utf8)]
+ public static partial IntPtr EglGetProcAddress(string procname);
+
+ [LibraryImport(LibEgl, EntryPoint = "eglGetDisplay")]
+ public static partial IntPtr EglGetDisplay(IntPtr nativeDisplay);
+
+ [LibraryImport(LibEgl, EntryPoint = "eglInitialize")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static partial bool EglInitialize(IntPtr display, out int major, out int minor);
+
+ [LibraryImport(LibEgl, EntryPoint = "eglTerminate")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static partial bool EglTerminate(IntPtr display);
+
+ [LibraryImport(LibEgl, EntryPoint = "eglQueryString")]
+ public static partial IntPtr EglQueryStringNative(IntPtr display, int name);
+
+ public static string? EglQueryString(IntPtr display, int name)
+ {
+ var ptr = EglQueryStringNative(display, name);
+ return ptr == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(ptr);
+ }
+
+ [LibraryImport(LibEgl, EntryPoint = "eglBindAPI")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static partial bool EglBindApi(int api);
+
+ [LibraryImport(LibEgl, EntryPoint = "eglChooseConfig")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static partial bool EglChooseConfig(IntPtr display, int[] attribs, IntPtr* configs, int configSize,
+ out int numConfig);
+
+ [LibraryImport(LibEgl, EntryPoint = "eglCreateContext")]
+ public static partial IntPtr EglCreateContext(IntPtr display, IntPtr config, IntPtr shareContext, int[] attribs);
+
+ [LibraryImport(LibEgl, EntryPoint = "eglDestroyContext")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static partial bool EglDestroyContext(IntPtr display, IntPtr context);
+
+ [LibraryImport(LibEgl, EntryPoint = "eglMakeCurrent")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static partial bool EglMakeCurrent(IntPtr display, IntPtr draw, IntPtr read, IntPtr context);
+
+ [LibraryImport(LibEgl, EntryPoint = "eglCreatePbufferSurface")]
+ public static partial IntPtr EglCreatePbufferSurface(IntPtr display, IntPtr config, int[] attribs);
+
+ [LibraryImport(LibEgl, EntryPoint = "eglDestroySurface")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static partial bool EglDestroySurface(IntPtr display, IntPtr surface);
+
+ [LibraryImport(LibEgl, EntryPoint = "eglGetError")]
+ public static partial int EglGetError();
+
+ // EGL_KHR_platform_gbm
+ [LibraryImport(LibEgl, EntryPoint = "eglGetPlatformDisplayEXT")]
+ public static partial IntPtr EglGetPlatformDisplayExt(int platform, IntPtr nativeDisplay, int[]? attribs);
+
+ // EGL constants
+ public const int EGL_OPENGL_ES_API = 0x30A0;
+ public const int EGL_OPENGL_ES2_BIT = 0x0004;
+ public const int EGL_OPENGL_ES3_BIT = 0x0040;
+ public const int EGL_RENDERABLE_TYPE = 0x3040;
+ public const int EGL_SURFACE_TYPE = 0x3033;
+ public const int EGL_PBUFFER_BIT = 0x0001;
+ public const int EGL_RED_SIZE = 0x3024;
+ public const int EGL_GREEN_SIZE = 0x3023;
+ public const int EGL_BLUE_SIZE = 0x3022;
+ public const int EGL_ALPHA_SIZE = 0x3021;
+ public const int EGL_NONE = 0x3038;
+ public const int EGL_CONTEXT_MAJOR_VERSION = 0x3098;
+ public const int EGL_CONTEXT_MINOR_VERSION = 0x30FB;
+ public const int EGL_WIDTH = 0x3057;
+ public const int EGL_HEIGHT = 0x3056;
+ public const int EGL_EXTENSIONS = 0x3055;
+ public const int EGL_NO_IMAGE_KHR = 0;
+ public const int EGL_LINUX_DMA_BUF_EXT = 0x3270;
+ public const int EGL_LINUX_DRM_FOURCC_EXT = 0x3271;
+ public const int EGL_DMA_BUF_PLANE0_FD_EXT = 0x3272;
+ public const int EGL_DMA_BUF_PLANE0_OFFSET_EXT = 0x3273;
+ public const int EGL_DMA_BUF_PLANE0_PITCH_EXT = 0x3274;
+ public const int EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT = 0x3443;
+ public const int EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT = 0x3444;
+ public const int EGL_PLATFORM_GBM_KHR = 0x31D7;
+ public const int EGL_SYNC_NATIVE_FENCE_ANDROID = 0x3144;
+ public const int EGL_SYNC_NATIVE_FENCE_FD_ANDROID = 0x3145;
+ public const int EGL_NO_NATIVE_FENCE_FD_ANDROID = -1;
+ public const int EGL_SYNC_FLUSH_COMMANDS_BIT_KHR = 0x0001;
+ public const long EGL_FOREVER_KHR = unchecked((long)0xFFFFFFFFFFFFFFFF);
+ public const int EGL_CONDITION_SATISFIED_KHR = 0x30F6;
+
+ // GL
+ private const string LibGl = "libGLESv2.so.2";
+
+ [LibraryImport(LibGl, EntryPoint = "glGetError")]
+ public static partial int GlGetError();
+
+ [LibraryImport(LibGl, EntryPoint = "glGenTextures")]
+ public static partial void GlGenTextures(int n, int* textures);
+
+ [LibraryImport(LibGl, EntryPoint = "glDeleteTextures")]
+ public static partial void GlDeleteTextures(int n, int* textures);
+
+ [LibraryImport(LibGl, EntryPoint = "glBindTexture")]
+ public static partial void GlBindTexture(int target, int texture);
+
+ [LibraryImport(LibGl, EntryPoint = "glGenFramebuffers")]
+ public static partial void GlGenFramebuffers(int n, int* framebuffers);
+
+ [LibraryImport(LibGl, EntryPoint = "glDeleteFramebuffers")]
+ public static partial void GlDeleteFramebuffers(int n, int* framebuffers);
+
+ [LibraryImport(LibGl, EntryPoint = "glBindFramebuffer")]
+ public static partial void GlBindFramebuffer(int target, int framebuffer);
+
+ [LibraryImport(LibGl, EntryPoint = "glFramebufferTexture2D")]
+ public static partial void GlFramebufferTexture2D(int target, int attachment, int texTarget, int texture,
+ int level);
+
+ [LibraryImport(LibGl, EntryPoint = "glCheckFramebufferStatus")]
+ public static partial int GlCheckFramebufferStatus(int target);
+
+ [LibraryImport(LibGl, EntryPoint = "glReadPixels")]
+ public static partial void GlReadPixels(int x, int y, int width, int height, int format, int type, void* pixels);
+
+ [LibraryImport(LibGl, EntryPoint = "glFlush")]
+ public static partial void GlFlush();
+
+ [LibraryImport(LibGl, EntryPoint = "glFinish")]
+ public static partial void GlFinish();
+
+ public const int GL_TEXTURE_2D = 0x0DE1;
+ public const int GL_FRAMEBUFFER = 0x8D40;
+ public const int GL_COLOR_ATTACHMENT0 = 0x8CE0;
+ public const int GL_FRAMEBUFFER_COMPLETE = 0x8CD5;
+ public const int GL_RGBA = 0x1908;
+ public const int GL_BGRA = 0x80E1;
+ public const int GL_UNSIGNED_BYTE = 0x1401;
+
+ // Function pointer types for EGL extensions loaded via eglGetProcAddress
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate IntPtr EglCreateImageKHRDelegate(IntPtr dpy, IntPtr ctx, int target, IntPtr buffer,
+ int[] attribs);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public delegate bool EglDestroyImageKHRDelegate(IntPtr dpy, IntPtr image);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public delegate bool EglQueryDmaBufFormatsEXTDelegate(IntPtr dpy, int maxFormats,
+ [Out] int[]? formats, out int numFormats);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public delegate bool EglQueryDmaBufModifiersEXTDelegate(IntPtr dpy, int format, int maxModifiers,
+ [Out] long[]? modifiers, [Out] int[]? externalOnly, out int numModifiers);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate void GlEGLImageTargetTexture2DOESDelegate(int target, IntPtr image);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate IntPtr EglCreateSyncKHRDelegate(IntPtr dpy, int type, int[] attribs);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public delegate bool EglDestroySyncKHRDelegate(IntPtr dpy, IntPtr sync);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate int EglClientWaitSyncKHRDelegate(IntPtr dpy, IntPtr sync, int flags, long timeout);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate int EglWaitSyncKHRDelegate(IntPtr dpy, IntPtr sync, int flags);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ public delegate int EglDupNativeFenceFDANDROIDDelegate(IntPtr dpy, IntPtr sync);
+
+ public static T? LoadEglExtension(string name) where T : Delegate
+ {
+ var ptr = EglGetProcAddress(name);
+ return ptr == IntPtr.Zero ? null : Marshal.GetDelegateForFunctionPointer(ptr);
+ }
+}
diff --git a/tests/Avalonia.DmaBufInteropTests/Program.cs b/tests/Avalonia.DmaBufInteropTests/Program.cs
new file mode 100644
index 0000000000..2546ade6b1
--- /dev/null
+++ b/tests/Avalonia.DmaBufInteropTests/Program.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using Avalonia.DmaBufInteropTests;
+using Avalonia.DmaBufInteropTests.Tests;
+
+// Parse args
+var mode = "both";
+bool verbose = false;
+foreach (var arg in args)
+{
+ switch (arg)
+ {
+ case "--egl": mode = "egl"; break;
+ case "--vulkan": mode = "vulkan"; break;
+ case "--both": mode = "both"; break;
+ case "--logic": mode = "logic"; break;
+ case "-v" or "--verbose": verbose = true; break;
+ case "-h" or "--help":
+ PrintUsage();
+ return 0;
+ }
+}
+
+Console.WriteLine($"DMA-BUF Interop Tests — mode: {mode}");
+Console.WriteLine(new string('=', 60));
+
+var allResults = new List();
+var sw = Stopwatch.StartNew();
+
+// Pure logic tests (always run)
+RunSuite("DRM Format Mapping", DrmFormatMappingTests.Run(), allResults);
+
+if (mode is "egl" or "both")
+ RunSuite("EGL DMA-BUF Import", EglDmaBufImportTests.Run(), allResults);
+
+if (mode is "vulkan" or "both")
+ RunSuite("Vulkan DMA-BUF Import", VulkanDmaBufImportTests.Run(), allResults);
+
+sw.Stop();
+
+// Summary
+Console.WriteLine();
+Console.WriteLine(new string('=', 60));
+var passed = allResults.Count(r => r.Status == TestStatus.Passed);
+var failed = allResults.Count(r => r.Status == TestStatus.Failed);
+var skipped = allResults.Count(r => r.Status == TestStatus.Skipped);
+Console.WriteLine($"Total: {allResults.Count} | Passed: {passed} | Failed: {failed} | Skipped: {skipped} | Time: {sw.ElapsedMilliseconds}ms");
+
+if (failed > 0)
+{
+ Console.WriteLine();
+ Console.WriteLine("FAILURES:");
+ foreach (var f in allResults.Where(r => r.Status == TestStatus.Failed))
+ Console.WriteLine($" {f}");
+}
+
+return failed > 0 ? 1 : 0;
+
+void RunSuite(string name, IEnumerable results, List accumulator)
+{
+ Console.WriteLine();
+ Console.WriteLine($"--- {name} ---");
+ foreach (var result in results)
+ {
+ accumulator.Add(result);
+ if (verbose || result.Status != TestStatus.Passed)
+ Console.WriteLine($" {result}");
+ else
+ Console.Write(".");
+ }
+ if (!verbose)
+ Console.WriteLine();
+}
+
+void PrintUsage()
+{
+ Console.WriteLine("Usage: Avalonia.DmaBufInteropTests [options]");
+ Console.WriteLine();
+ Console.WriteLine("Options:");
+ Console.WriteLine(" --egl Run EGL tests only");
+ Console.WriteLine(" --vulkan Run Vulkan tests only");
+ Console.WriteLine(" --both Run both EGL and Vulkan tests (default)");
+ Console.WriteLine(" --logic Run only pure logic tests (no GPU)");
+ Console.WriteLine(" -v Verbose output (show passing tests)");
+ Console.WriteLine(" -h Show this help");
+}
diff --git a/tests/Avalonia.DmaBufInteropTests/TestResult.cs b/tests/Avalonia.DmaBufInteropTests/TestResult.cs
new file mode 100644
index 0000000000..cb85186a1f
--- /dev/null
+++ b/tests/Avalonia.DmaBufInteropTests/TestResult.cs
@@ -0,0 +1,24 @@
+namespace Avalonia.DmaBufInteropTests;
+
+public enum TestStatus
+{
+ Passed,
+ Failed,
+ Skipped
+}
+
+public record TestResult(string Name, TestStatus Status, string? Message = null)
+{
+ public override string ToString()
+ {
+ var tag = Status switch
+ {
+ TestStatus.Passed => "PASS",
+ TestStatus.Failed => "FAIL",
+ TestStatus.Skipped => "SKIP",
+ _ => "????"
+ };
+ var suffix = Message != null ? $" — {Message}" : "";
+ return $"[{tag}] {Name}{suffix}";
+ }
+}
diff --git a/tests/Avalonia.DmaBufInteropTests/Tests/DrmFormatMappingTests.cs b/tests/Avalonia.DmaBufInteropTests/Tests/DrmFormatMappingTests.cs
new file mode 100644
index 0000000000..7c8d91d305
--- /dev/null
+++ b/tests/Avalonia.DmaBufInteropTests/Tests/DrmFormatMappingTests.cs
@@ -0,0 +1,48 @@
+using System.Collections.Generic;
+using Avalonia.Platform;
+
+namespace Avalonia.DmaBufInteropTests.Tests;
+
+///
+/// Pure logic tests — no GPU needed.
+///
+internal static class DrmFormatMappingTests
+{
+ public static IEnumerable Run()
+ {
+ yield return Test("DrmFormatMapping_Argb8888_Maps_To_B8G8R8A8",
+ PlatformGraphicsDrmFormats.TryMapDrmFormat(PlatformGraphicsDrmFormats.DRM_FORMAT_ARGB8888)
+ == PlatformGraphicsExternalImageFormat.B8G8R8A8UNorm);
+
+ yield return Test("DrmFormatMapping_Xrgb8888_Maps_To_B8G8R8A8",
+ PlatformGraphicsDrmFormats.TryMapDrmFormat(PlatformGraphicsDrmFormats.DRM_FORMAT_XRGB8888)
+ == PlatformGraphicsExternalImageFormat.B8G8R8A8UNorm);
+
+ yield return Test("DrmFormatMapping_Abgr8888_Maps_To_R8G8B8A8",
+ PlatformGraphicsDrmFormats.TryMapDrmFormat(PlatformGraphicsDrmFormats.DRM_FORMAT_ABGR8888)
+ == PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm);
+
+ yield return Test("DrmFormatMapping_Xbgr8888_Maps_To_R8G8B8A8",
+ PlatformGraphicsDrmFormats.TryMapDrmFormat(PlatformGraphicsDrmFormats.DRM_FORMAT_XBGR8888)
+ == PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm);
+
+ yield return Test("DrmFormatMapping_Unknown_Returns_Null",
+ PlatformGraphicsDrmFormats.TryMapDrmFormat(0x00000000) == null);
+
+ yield return Test("DmaBufFileDescriptor_Is_DMABUF_FD",
+ KnownPlatformGraphicsExternalImageHandleTypes.DmaBufFileDescriptor == "DMABUF_FD");
+
+ yield return Test("SyncFileDescriptor_Is_SYNC_FD",
+ KnownPlatformGraphicsExternalSemaphoreHandleTypes.SyncFileDescriptor == "SYNC_FD");
+
+ yield return Test("DrmModLinear_Is_Zero",
+ PlatformGraphicsDrmFormats.DRM_FORMAT_MOD_LINEAR == 0);
+ }
+
+ private static TestResult Test(string name, bool condition)
+ {
+ return condition
+ ? new TestResult(name, TestStatus.Passed)
+ : new TestResult(name, TestStatus.Failed, "assertion failed");
+ }
+}
diff --git a/tests/Avalonia.DmaBufInteropTests/Tests/EglDmaBufImportTests.cs b/tests/Avalonia.DmaBufInteropTests/Tests/EglDmaBufImportTests.cs
new file mode 100644
index 0000000000..ebc50c4376
--- /dev/null
+++ b/tests/Avalonia.DmaBufInteropTests/Tests/EglDmaBufImportTests.cs
@@ -0,0 +1,366 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using static Avalonia.DmaBufInteropTests.NativeInterop;
+
+namespace Avalonia.DmaBufInteropTests.Tests;
+
+///
+/// Tests EGL DMA-BUF import path directly without Avalonia.
+///
+internal static unsafe class EglDmaBufImportTests
+{
+ public static List Run()
+ {
+ var results = new List();
+
+ using var allocator = new DmaBufAllocator();
+ if (!allocator.IsAvailable)
+ {
+ results.Add(new TestResult("Egl_DmaBuf_All", TestStatus.Skipped, "no render node available"));
+ return results;
+ }
+
+ var display = IntPtr.Zero;
+ var context = IntPtr.Zero;
+ var surface = IntPtr.Zero;
+
+ try
+ {
+ // Try GBM platform display first, fall back to default
+ var getPlatformDisplay = LoadEglExtension("eglGetPlatformDisplayEXT");
+ if (getPlatformDisplay != null)
+ {
+ int drmFd = -1;
+ IntPtr gbm = IntPtr.Zero;
+ foreach (var path in new[] { "/dev/dri/renderD128", "/dev/dri/renderD129" })
+ {
+ if (!System.IO.File.Exists(path)) continue;
+ drmFd = Open(path, O_RDWR);
+ if (drmFd >= 0)
+ {
+ gbm = GbmCreateDevice(drmFd);
+ if (gbm != IntPtr.Zero) break;
+ Close(drmFd);
+ drmFd = -1;
+ }
+ }
+ if (gbm != IntPtr.Zero)
+ display = getPlatformDisplay(EGL_PLATFORM_GBM_KHR, gbm, null);
+ }
+
+ if (display == IntPtr.Zero)
+ display = EglGetDisplay(IntPtr.Zero);
+
+ if (display == IntPtr.Zero)
+ {
+ results.Add(new TestResult("Egl_DmaBuf_All", TestStatus.Skipped, "cannot get EGL display"));
+ return results;
+ }
+
+ if (!EglInitialize(display, out _, out _))
+ {
+ results.Add(new TestResult("Egl_DmaBuf_All", TestStatus.Skipped, "eglInitialize failed"));
+ return results;
+ }
+
+ var extensions = EglQueryString(display, EGL_EXTENSIONS) ?? "";
+
+ results.Add(extensions.Contains("EGL_EXT_image_dma_buf_import")
+ ? new TestResult("Egl_DmaBuf_Extension_Availability", TestStatus.Passed)
+ : new TestResult("Egl_DmaBuf_Extension_Availability", TestStatus.Skipped,
+ "EGL_EXT_image_dma_buf_import not available"));
+
+ if (!extensions.Contains("EGL_EXT_image_dma_buf_import"))
+ {
+ EglTerminate(display);
+ return results;
+ }
+
+ // Create GL context — try pbuffer first, then surfaceless
+ EglBindApi(EGL_OPENGL_ES_API);
+ IntPtr config;
+ int numConfig;
+ bool useSurfaceless = false;
+
+ // Try pbuffer config first
+ var configAttribs = new[]
+ {
+ EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
+ EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
+ EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8,
+ EGL_NONE
+ };
+ EglChooseConfig(display, configAttribs, &config, 1, out numConfig);
+ if (numConfig == 0)
+ {
+ configAttribs[3] = EGL_OPENGL_ES2_BIT;
+ EglChooseConfig(display, configAttribs, &config, 1, out numConfig);
+ }
+
+ // Fall back to surfaceless (no surface type requirement)
+ if (numConfig == 0)
+ {
+ configAttribs = new[]
+ {
+ EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
+ EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8,
+ EGL_NONE
+ };
+ EglChooseConfig(display, configAttribs, &config, 1, out numConfig);
+ if (numConfig == 0)
+ {
+ configAttribs[1] = EGL_OPENGL_ES2_BIT;
+ EglChooseConfig(display, configAttribs, &config, 1, out numConfig);
+ }
+ useSurfaceless = numConfig > 0;
+ }
+
+ if (numConfig == 0)
+ {
+ results.Add(new TestResult("Egl_DmaBuf_All", TestStatus.Skipped, "no suitable EGL config"));
+ EglTerminate(display);
+ return results;
+ }
+
+ var ctxAttribs = new[] { EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 0, EGL_NONE };
+ context = EglCreateContext(display, config, IntPtr.Zero, ctxAttribs);
+ if (context == IntPtr.Zero)
+ {
+ ctxAttribs = new[] { EGL_CONTEXT_MAJOR_VERSION, 2, EGL_CONTEXT_MINOR_VERSION, 0, EGL_NONE };
+ context = EglCreateContext(display, config, IntPtr.Zero, ctxAttribs);
+ }
+
+ if (context == IntPtr.Zero)
+ {
+ results.Add(new TestResult("Egl_DmaBuf_All", TestStatus.Skipped, "cannot create EGL context"));
+ EglTerminate(display);
+ return results;
+ }
+
+ if (useSurfaceless)
+ {
+ // EGL_KHR_surfaceless_context: pass EGL_NO_SURFACE
+ EglMakeCurrent(display, IntPtr.Zero, IntPtr.Zero, context);
+ }
+ else
+ {
+ surface = EglCreatePbufferSurface(display, config, new[] { EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE });
+ EglMakeCurrent(display, surface, surface, context);
+ }
+
+ var eglCreateImageKHR = LoadEglExtension("eglCreateImageKHR");
+ var eglDestroyImageKHR = LoadEglExtension("eglDestroyImageKHR");
+ var glEGLImageTargetTexture2DOES =
+ LoadEglExtension("glEGLImageTargetTexture2DOES");
+
+ if (eglCreateImageKHR == null || eglDestroyImageKHR == null || glEGLImageTargetTexture2DOES == null)
+ {
+ results.Add(new TestResult("Egl_DmaBuf_Image_Import", TestStatus.Skipped,
+ "missing eglCreateImageKHR or glEGLImageTargetTexture2DOES"));
+ }
+ else
+ {
+ results.AddRange(TestFormatQuery(display, extensions));
+ results.Add(TestImageImportAndReadback(display, allocator, eglCreateImageKHR,
+ eglDestroyImageKHR, glEGLImageTargetTexture2DOES));
+ }
+
+ results.Add(TestSyncFence(display, extensions));
+ }
+ finally
+ {
+ if (context != IntPtr.Zero)
+ {
+ EglMakeCurrent(display, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
+ if (surface != IntPtr.Zero)
+ EglDestroySurface(display, surface);
+ EglDestroyContext(display, context);
+ }
+
+ if (display != IntPtr.Zero)
+ EglTerminate(display);
+ }
+
+ return results;
+ }
+
+ private static List TestFormatQuery(IntPtr display, string extensions)
+ {
+ if (!extensions.Contains("EGL_EXT_image_dma_buf_import_modifiers"))
+ return [new TestResult("Egl_DmaBuf_Format_Query", TestStatus.Skipped,
+ "EGL_EXT_image_dma_buf_import_modifiers not available")];
+
+ var queryFormats = LoadEglExtension("eglQueryDmaBufFormatsEXT");
+ var queryModifiers = LoadEglExtension("eglQueryDmaBufModifiersEXT");
+ if (queryFormats == null || queryModifiers == null)
+ return [new TestResult("Egl_DmaBuf_Format_Query", TestStatus.Skipped, "query functions not available")];
+
+ queryFormats(display, 0, null, out var numFormats);
+ if (numFormats == 0)
+ return [new TestResult("Egl_DmaBuf_Format_Query", TestStatus.Failed,
+ "eglQueryDmaBufFormatsEXT returned 0 formats")];
+
+ var formats = new int[numFormats];
+ queryFormats(display, numFormats, formats, out _);
+
+ bool hasArgb8888 = false;
+ foreach (var fmt in formats)
+ if ((uint)fmt == GBM_FORMAT_ARGB8888)
+ hasArgb8888 = true;
+
+ return [hasArgb8888
+ ? new TestResult("Egl_DmaBuf_Format_Query", TestStatus.Passed,
+ $"{numFormats} formats, ARGB8888 supported")
+ : new TestResult("Egl_DmaBuf_Format_Query", TestStatus.Failed,
+ $"{numFormats} formats but DRM_FORMAT_ARGB8888 not found")];
+ }
+
+ private static TestResult TestImageImportAndReadback(IntPtr display, DmaBufAllocator allocator,
+ EglCreateImageKHRDelegate eglCreateImageKHR, EglDestroyImageKHRDelegate eglDestroyImageKHR,
+ GlEGLImageTargetTexture2DOESDelegate glEGLImageTargetTexture2DOES)
+ {
+ const uint width = 64, height = 64;
+ const uint greenColor = 0xFF00FF00; // ARGB: full green
+
+ using var alloc = allocator.AllocateLinear(width, height, GBM_FORMAT_ARGB8888, greenColor);
+ if (alloc == null)
+ return new TestResult("Egl_DmaBuf_Image_Import_And_Readback", TestStatus.Skipped,
+ "could not allocate DMA-BUF");
+
+ var attribs = new[]
+ {
+ EGL_WIDTH, (int)width,
+ EGL_HEIGHT, (int)height,
+ EGL_LINUX_DRM_FOURCC_EXT, (int)GBM_FORMAT_ARGB8888,
+ EGL_DMA_BUF_PLANE0_FD_EXT, alloc.Fd,
+ EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0,
+ EGL_DMA_BUF_PLANE0_PITCH_EXT, (int)alloc.Stride,
+ EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, (int)(alloc.Modifier & 0xFFFFFFFF),
+ EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, (int)(alloc.Modifier >> 32),
+ EGL_NONE
+ };
+
+ var eglImage = eglCreateImageKHR(display, IntPtr.Zero, EGL_LINUX_DMA_BUF_EXT, IntPtr.Zero, attribs);
+ if (eglImage == IntPtr.Zero)
+ {
+ var err = EglGetError();
+ return new TestResult("Egl_DmaBuf_Image_Import_And_Readback", TestStatus.Failed,
+ $"eglCreateImageKHR failed with 0x{err:X}");
+ }
+
+ try
+ {
+ int texId;
+ GlGenTextures(1, &texId);
+ GlBindTexture(GL_TEXTURE_2D, texId);
+ glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, eglImage);
+
+ var glErr = GlGetError();
+ if (glErr != 0)
+ {
+ GlDeleteTextures(1, &texId);
+ return new TestResult("Egl_DmaBuf_Image_Import_And_Readback", TestStatus.Failed,
+ $"glEGLImageTargetTexture2DOES error: 0x{glErr:X}");
+ }
+
+ int fbo;
+ GlGenFramebuffers(1, &fbo);
+ GlBindFramebuffer(GL_FRAMEBUFFER, fbo);
+ GlFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texId, 0);
+
+ var status = GlCheckFramebufferStatus(GL_FRAMEBUFFER);
+ if (status != GL_FRAMEBUFFER_COMPLETE)
+ {
+ GlBindFramebuffer(GL_FRAMEBUFFER, 0);
+ GlDeleteFramebuffers(1, &fbo);
+ GlDeleteTextures(1, &texId);
+ return new TestResult("Egl_DmaBuf_Image_Import_And_Readback", TestStatus.Failed,
+ $"framebuffer incomplete: 0x{status:X}");
+ }
+
+ var pixels = new byte[width * height * 4];
+ fixed (byte* pPixels = pixels)
+ GlReadPixels(0, 0, (int)width, (int)height, GL_RGBA, GL_UNSIGNED_BYTE, pPixels);
+
+ GlBindFramebuffer(GL_FRAMEBUFFER, 0);
+ GlDeleteFramebuffers(1, &fbo);
+ GlDeleteTextures(1, &texId);
+
+ // Verify: DRM_FORMAT_ARGB8888 with green=0xFF → G=0xFF, A=0xFF after import
+ int correctPixels = 0;
+ for (int i = 0; i < width * height; i++)
+ {
+ var g = pixels[i * 4 + 1];
+ var a = pixels[i * 4 + 3];
+ if (g == 0xFF && a == 0xFF)
+ correctPixels++;
+ }
+
+ var correctRatio = (double)correctPixels / (width * height);
+ return correctRatio > 0.95
+ ? new TestResult("Egl_DmaBuf_Image_Import_And_Readback", TestStatus.Passed,
+ $"{correctRatio:P0} pixels correct")
+ : new TestResult("Egl_DmaBuf_Image_Import_And_Readback", TestStatus.Failed,
+ $"only {correctRatio:P0} pixels correct (expected >95%)");
+ }
+ finally
+ {
+ eglDestroyImageKHR(display, eglImage);
+ }
+ }
+
+ private static TestResult TestSyncFence(IntPtr display, string extensions)
+ {
+ if (!extensions.Contains("EGL_ANDROID_native_fence_sync"))
+ return new TestResult("Egl_SyncFence_RoundTrip", TestStatus.Skipped,
+ "EGL_ANDROID_native_fence_sync not available");
+
+ var createSync = LoadEglExtension("eglCreateSyncKHR");
+ var destroySync = LoadEglExtension("eglDestroySyncKHR");
+ var clientWait = LoadEglExtension("eglClientWaitSyncKHR");
+ var dupFence = LoadEglExtension("eglDupNativeFenceFDANDROID");
+
+ if (createSync == null || destroySync == null || clientWait == null || dupFence == null)
+ return new TestResult("Egl_SyncFence_RoundTrip", TestStatus.Skipped,
+ "sync fence functions not available");
+
+ GlFlush();
+
+ var sync = createSync(display, EGL_SYNC_NATIVE_FENCE_ANDROID,
+ new[] { EGL_SYNC_NATIVE_FENCE_FD_ANDROID, EGL_NO_NATIVE_FENCE_FD_ANDROID, EGL_NONE });
+ if (sync == IntPtr.Zero)
+ {
+ var err = EglGetError();
+ return new TestResult("Egl_SyncFence_RoundTrip", TestStatus.Failed,
+ $"eglCreateSyncKHR (export) failed with 0x{err:X}");
+ }
+
+ var fd = dupFence(display, sync);
+ destroySync(display, sync);
+
+ if (fd < 0)
+ return new TestResult("Egl_SyncFence_RoundTrip", TestStatus.Failed,
+ $"eglDupNativeFenceFDANDROID returned {fd}");
+
+ var importSync = createSync(display, EGL_SYNC_NATIVE_FENCE_ANDROID,
+ new[] { EGL_SYNC_NATIVE_FENCE_FD_ANDROID, fd, EGL_NONE });
+ if (importSync == IntPtr.Zero)
+ {
+ var err = EglGetError();
+ return new TestResult("Egl_SyncFence_RoundTrip", TestStatus.Failed,
+ $"eglCreateSyncKHR (import) failed with 0x{err:X}");
+ }
+
+ var waitResult = clientWait(display, importSync, EGL_SYNC_FLUSH_COMMANDS_BIT_KHR, EGL_FOREVER_KHR);
+ destroySync(display, importSync);
+
+ return waitResult == EGL_CONDITION_SATISFIED_KHR
+ ? new TestResult("Egl_SyncFence_RoundTrip", TestStatus.Passed)
+ : new TestResult("Egl_SyncFence_RoundTrip", TestStatus.Failed,
+ $"eglClientWaitSyncKHR returned 0x{waitResult:X}");
+ }
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ private delegate IntPtr EglGetPlatformDisplayDelegate(int platform, IntPtr nativeDisplay, int[]? attribs);
+}
diff --git a/tests/Avalonia.DmaBufInteropTests/Tests/VulkanDmaBufImportTests.cs b/tests/Avalonia.DmaBufInteropTests/Tests/VulkanDmaBufImportTests.cs
new file mode 100644
index 0000000000..c843cbb645
--- /dev/null
+++ b/tests/Avalonia.DmaBufInteropTests/Tests/VulkanDmaBufImportTests.cs
@@ -0,0 +1,790 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using static Avalonia.DmaBufInteropTests.NativeInterop;
+
+namespace Avalonia.DmaBufInteropTests.Tests;
+
+///
+/// Tests Vulkan DMA-BUF import path using raw Vulkan API via P/Invoke.
+///
+internal static unsafe partial class VulkanDmaBufImportTests
+{
+ // Vulkan constants
+ private const uint VK_API_VERSION_1_1 = (1u << 22) | (1u << 12);
+ private const int VK_SUCCESS = 0;
+ private const int VK_STRUCTURE_TYPE_APPLICATION_INFO = 0;
+ private const int VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO = 1;
+ private const int VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO = 3;
+ private const int VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO = 2;
+ private const int VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO = 5;
+ private const int VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO = 14;
+ private const int VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO = 15;
+ private const int VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR = 1000074000;
+ private const int VK_STRUCTURE_TYPE_MEMORY_FD_PROPERTIES_KHR = 1000074001;
+ private const int VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO = 1000127001;
+ private const int VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_EXPLICIT_CREATE_INFO_EXT = 1000158004;
+ private const int VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_PROPERTIES_EXT = 1000158005;
+ private const int VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_DRM_FORMAT_MODIFIER_INFO_EXT = 1000158003;
+ private const int VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO = 1000072001;
+ private const int VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_IMAGE_FORMAT_INFO = 1000071000;
+ private const int VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_FORMAT_INFO_2 = 1000059003;
+ private const int VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2 = 1000059003;
+ private const int VK_STRUCTURE_TYPE_DRM_FORMAT_MODIFIER_PROPERTIES_LIST_EXT = 1000158000;
+ private const int VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_2 = 1000059005;
+
+ private const int VK_IMAGE_TYPE_2D = 1;
+ private const int VK_FORMAT_B8G8R8A8_UNORM = 44;
+ private const int VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT = 1000158000;
+ private const int VK_IMAGE_TILING_OPTIMAL = 0;
+ private const int VK_IMAGE_USAGE_SAMPLED_BIT = 0x00000004;
+ private const int VK_IMAGE_USAGE_TRANSFER_SRC_BIT = 0x00000001;
+ private const int VK_SHARING_MODE_EXCLUSIVE = 0;
+ private const int VK_SAMPLE_COUNT_1_BIT = 0x00000001;
+ private const int VK_IMAGE_LAYOUT_UNDEFINED = 0;
+ private const int VK_IMAGE_VIEW_TYPE_2D = 1;
+ private const int VK_COMPONENT_SWIZZLE_IDENTITY = 0;
+ private const int VK_IMAGE_ASPECT_COLOR_BIT = 0x00000001;
+ private const uint VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT = 0x00000200;
+ private const uint VK_QUEUE_FAMILY_FOREIGN_EXT = ~0u;
+ private const uint VK_QUEUE_FAMILY_IGNORED = ~0u;
+
+ private const string LibVulkan = "libvulkan.so.1";
+
+ // Minimal Vulkan P/Invoke declarations
+ [LibraryImport(LibVulkan, EntryPoint = "vkCreateInstance")]
+ private static partial int VkCreateInstance(VkInstanceCreateInfo* createInfo, void* allocator, IntPtr* instance);
+
+ [LibraryImport(LibVulkan, EntryPoint = "vkDestroyInstance")]
+ private static partial void VkDestroyInstance(IntPtr instance, void* allocator);
+
+ [LibraryImport(LibVulkan, EntryPoint = "vkEnumeratePhysicalDevices")]
+ private static partial int VkEnumeratePhysicalDevices(IntPtr instance, uint* count, IntPtr* devices);
+
+ [LibraryImport(LibVulkan, EntryPoint = "vkGetPhysicalDeviceQueueFamilyProperties")]
+ private static partial void VkGetPhysicalDeviceQueueFamilyProperties(IntPtr physicalDevice, uint* count,
+ VkQueueFamilyProperties* properties);
+
+ [LibraryImport(LibVulkan, EntryPoint = "vkCreateDevice")]
+ private static partial int VkCreateDevice(IntPtr physicalDevice, VkDeviceCreateInfo* createInfo, void* allocator,
+ IntPtr* device);
+
+ [LibraryImport(LibVulkan, EntryPoint = "vkDestroyDevice")]
+ private static partial void VkDestroyDevice(IntPtr device, void* allocator);
+
+ [LibraryImport(LibVulkan, EntryPoint = "vkGetDeviceProcAddr", StringMarshalling = StringMarshalling.Utf8)]
+ private static partial IntPtr VkGetDeviceProcAddr(IntPtr device, string name);
+
+ [LibraryImport(LibVulkan, EntryPoint = "vkGetInstanceProcAddr", StringMarshalling = StringMarshalling.Utf8)]
+ private static partial IntPtr VkGetInstanceProcAddr(IntPtr instance, string name);
+
+ [LibraryImport(LibVulkan, EntryPoint = "vkEnumerateDeviceExtensionProperties")]
+ private static partial int VkEnumerateDeviceExtensionProperties(IntPtr physicalDevice, byte* layerName,
+ uint* count, VkExtensionProperties* properties);
+
+ [LibraryImport(LibVulkan, EntryPoint = "vkGetPhysicalDeviceMemoryProperties")]
+ private static partial void VkGetPhysicalDeviceMemoryProperties(IntPtr physicalDevice,
+ VkPhysicalDeviceMemoryProperties* memProperties);
+
+ [LibraryImport(LibVulkan, EntryPoint = "vkGetPhysicalDeviceFormatProperties2")]
+ private static partial void VkGetPhysicalDeviceFormatProperties2(IntPtr physicalDevice, int format,
+ VkFormatProperties2Native* formatProperties);
+
+ [LibraryImport(LibVulkan, EntryPoint = "vkCreateImage")]
+ private static partial int VkCreateImage(IntPtr device, VkImageCreateInfo* createInfo, void* allocator,
+ ulong* image);
+
+ [LibraryImport(LibVulkan, EntryPoint = "vkDestroyImage")]
+ private static partial void VkDestroyImage(IntPtr device, ulong image, void* allocator);
+
+ [LibraryImport(LibVulkan, EntryPoint = "vkGetImageMemoryRequirements")]
+ private static partial void VkGetImageMemoryRequirements(IntPtr device, ulong image,
+ VkMemoryRequirements* memRequirements);
+
+ [LibraryImport(LibVulkan, EntryPoint = "vkAllocateMemory")]
+ private static partial int VkAllocateMemory(IntPtr device, VkMemoryAllocateInfo* allocateInfo, void* allocator,
+ ulong* memory);
+
+ [LibraryImport(LibVulkan, EntryPoint = "vkFreeMemory")]
+ private static partial void VkFreeMemory(IntPtr device, ulong memory, void* allocator);
+
+ [LibraryImport(LibVulkan, EntryPoint = "vkBindImageMemory")]
+ private static partial int VkBindImageMemory(IntPtr device, ulong image, ulong memory, ulong offset);
+
+ [LibraryImport(LibVulkan, EntryPoint = "vkCreateImageView")]
+ private static partial int VkCreateImageView(IntPtr device, VkImageViewCreateInfo* createInfo, void* allocator,
+ ulong* imageView);
+
+ [LibraryImport(LibVulkan, EntryPoint = "vkDestroyImageView")]
+ private static partial void VkDestroyImageView(IntPtr device, ulong imageView, void* allocator);
+
+ // Function pointer delegate for vkGetMemoryFdPropertiesKHR
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+ private delegate int VkGetMemoryFdPropertiesKHRDelegate(IntPtr device, uint handleType, int fd,
+ VkMemoryFdPropertiesKHRNative* memoryFdProperties);
+
+ // Structures
+ [StructLayout(LayoutKind.Sequential)]
+ private struct VkApplicationInfo
+ {
+ public int sType;
+ public void* pNext;
+ public byte* pApplicationName;
+ public uint applicationVersion;
+ public byte* pEngineName;
+ public uint engineVersion;
+ public uint apiVersion;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct VkInstanceCreateInfo
+ {
+ public int sType;
+ public void* pNext;
+ public uint flags;
+ public VkApplicationInfo* pApplicationInfo;
+ public uint enabledLayerCount;
+ public byte** ppEnabledLayerNames;
+ public uint enabledExtensionCount;
+ public byte** ppEnabledExtensionNames;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct VkDeviceQueueCreateInfo
+ {
+ public int sType;
+ public void* pNext;
+ public uint flags;
+ public uint queueFamilyIndex;
+ public uint queueCount;
+ public float* pQueuePriorities;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct VkDeviceCreateInfo
+ {
+ public int sType;
+ public void* pNext;
+ public uint flags;
+ public uint queueCreateInfoCount;
+ public VkDeviceQueueCreateInfo* pQueueCreateInfos;
+ public uint enabledLayerCount;
+ public byte** ppEnabledLayerNames;
+ public uint enabledExtensionCount;
+ public byte** ppEnabledExtensionNames;
+ public void* pEnabledFeatures;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct VkExtensionProperties
+ {
+ public fixed byte extensionName[256];
+ public uint specVersion;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct VkQueueFamilyProperties
+ {
+ public uint queueFlags;
+ public uint queueCount;
+ public uint timestampValidBits;
+ public uint minImageTransferGranularity_width;
+ public uint minImageTransferGranularity_height;
+ public uint minImageTransferGranularity_depth;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct VkPhysicalDeviceMemoryProperties
+ {
+ public uint memoryTypeCount;
+ public VkMemoryType memoryTypes_0;
+ public VkMemoryType memoryTypes_1;
+ public VkMemoryType memoryTypes_2;
+ public VkMemoryType memoryTypes_3;
+ public VkMemoryType memoryTypes_4;
+ public VkMemoryType memoryTypes_5;
+ public VkMemoryType memoryTypes_6;
+ public VkMemoryType memoryTypes_7;
+ public VkMemoryType memoryTypes_8;
+ public VkMemoryType memoryTypes_9;
+ public VkMemoryType memoryTypes_10;
+ public VkMemoryType memoryTypes_11;
+ public VkMemoryType memoryTypes_12;
+ public VkMemoryType memoryTypes_13;
+ public VkMemoryType memoryTypes_14;
+ public VkMemoryType memoryTypes_15;
+ public VkMemoryType memoryTypes_16;
+ public VkMemoryType memoryTypes_17;
+ public VkMemoryType memoryTypes_18;
+ public VkMemoryType memoryTypes_19;
+ public VkMemoryType memoryTypes_20;
+ public VkMemoryType memoryTypes_21;
+ public VkMemoryType memoryTypes_22;
+ public VkMemoryType memoryTypes_23;
+ public VkMemoryType memoryTypes_24;
+ public VkMemoryType memoryTypes_25;
+ public VkMemoryType memoryTypes_26;
+ public VkMemoryType memoryTypes_27;
+ public VkMemoryType memoryTypes_28;
+ public VkMemoryType memoryTypes_29;
+ public VkMemoryType memoryTypes_30;
+ public VkMemoryType memoryTypes_31;
+ public uint memoryHeapCount;
+ public VkMemoryHeap memoryHeaps_0;
+ public VkMemoryHeap memoryHeaps_1;
+ public VkMemoryHeap memoryHeaps_2;
+ public VkMemoryHeap memoryHeaps_3;
+ public VkMemoryHeap memoryHeaps_4;
+ public VkMemoryHeap memoryHeaps_5;
+ public VkMemoryHeap memoryHeaps_6;
+ public VkMemoryHeap memoryHeaps_7;
+ public VkMemoryHeap memoryHeaps_8;
+ public VkMemoryHeap memoryHeaps_9;
+ public VkMemoryHeap memoryHeaps_10;
+ public VkMemoryHeap memoryHeaps_11;
+ public VkMemoryHeap memoryHeaps_12;
+ public VkMemoryHeap memoryHeaps_13;
+ public VkMemoryHeap memoryHeaps_14;
+ public VkMemoryHeap memoryHeaps_15;
+
+ public VkMemoryType GetMemoryType(int index)
+ {
+ // Use pointer arithmetic since we can't use fixed buffer of structs
+ fixed (VkMemoryType* p = &memoryTypes_0)
+ return p[index];
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct VkMemoryType
+ {
+ public uint propertyFlags;
+ public uint heapIndex;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct VkMemoryHeap
+ {
+ public ulong size;
+ public uint flags;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct VkImageCreateInfo
+ {
+ public int sType;
+ public void* pNext;
+ public uint flags;
+ public int imageType;
+ public int format;
+ public uint extent_width;
+ public uint extent_height;
+ public uint extent_depth;
+ public uint mipLevels;
+ public uint arrayLayers;
+ public int samples;
+ public int tiling;
+ public int usage;
+ public int sharingMode;
+ public uint queueFamilyIndexCount;
+ public uint* pQueueFamilyIndices;
+ public int initialLayout;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct VkExternalMemoryImageCreateInfo
+ {
+ public int sType;
+ public void* pNext;
+ public uint handleTypes;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct VkImageDrmFormatModifierExplicitCreateInfoEXT
+ {
+ public int sType;
+ public void* pNext;
+ public ulong drmFormatModifier;
+ public uint drmFormatModifierPlaneCount;
+ public VkSubresourceLayout* pPlaneLayouts;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct VkSubresourceLayout
+ {
+ public ulong offset;
+ public ulong size;
+ public ulong rowPitch;
+ public ulong arrayPitch;
+ public ulong depthPitch;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct VkMemoryRequirements
+ {
+ public ulong size;
+ public ulong alignment;
+ public uint memoryTypeBits;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct VkMemoryAllocateInfo
+ {
+ public int sType;
+ public void* pNext;
+ public ulong allocationSize;
+ public uint memoryTypeIndex;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct VkImportMemoryFdInfoKHR
+ {
+ public int sType;
+ public void* pNext;
+ public uint handleType;
+ public int fd;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct VkMemoryDedicatedAllocateInfo
+ {
+ public int sType;
+ public void* pNext;
+ public ulong image;
+ public ulong buffer;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct VkMemoryFdPropertiesKHRNative
+ {
+ public int sType;
+ public void* pNext;
+ public uint memoryTypeBits;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct VkImageViewCreateInfo
+ {
+ public int sType;
+ public void* pNext;
+ public uint flags;
+ public ulong image;
+ public int viewType;
+ public int format;
+ public int components_r;
+ public int components_g;
+ public int components_b;
+ public int components_a;
+ public int subresourceRange_aspectMask;
+ public uint subresourceRange_baseMipLevel;
+ public uint subresourceRange_levelCount;
+ public uint subresourceRange_baseArrayLayer;
+ public uint subresourceRange_layerCount;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct VkDrmFormatModifierPropertiesEXT
+ {
+ public ulong drmFormatModifier;
+ public uint drmFormatModifierPlaneCount;
+ public uint drmFormatModifierTilingFeatures;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct VkDrmFormatModifierPropertiesListEXT
+ {
+ public int sType;
+ public void* pNext;
+ public uint drmFormatModifierCount;
+ public VkDrmFormatModifierPropertiesEXT* pDrmFormatModifierProperties;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct VkFormatProperties2Native
+ {
+ public int sType;
+ public void* pNext;
+ public uint linearTilingFeatures;
+ public uint optimalTilingFeatures;
+ public uint bufferFeatures;
+ }
+
+ public static List Run()
+ {
+ var results = new List();
+ IntPtr instance = IntPtr.Zero;
+ IntPtr device = IntPtr.Zero;
+ IntPtr physicalDevice = IntPtr.Zero;
+
+ try
+ {
+ var appInfo = new VkApplicationInfo
+ {
+ sType = VK_STRUCTURE_TYPE_APPLICATION_INFO,
+ apiVersion = VK_API_VERSION_1_1
+ };
+
+ var instanceCreateInfo = new VkInstanceCreateInfo
+ {
+ sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
+ pApplicationInfo = &appInfo
+ };
+
+ var result = VkCreateInstance(&instanceCreateInfo, null, &instance);
+ if (result != VK_SUCCESS)
+ {
+ results.Add(new TestResult("Vulkan_DmaBuf_All", TestStatus.Skipped,
+ $"vkCreateInstance failed: {result}"));
+ return results;
+ }
+
+ uint deviceCount = 0;
+ VkEnumeratePhysicalDevices(instance, &deviceCount, null);
+ if (deviceCount == 0)
+ {
+ results.Add(new TestResult("Vulkan_DmaBuf_All", TestStatus.Skipped, "no Vulkan devices"));
+ return results;
+ }
+
+ var physDevices = stackalloc IntPtr[(int)deviceCount];
+ VkEnumeratePhysicalDevices(instance, &deviceCount, physDevices);
+ physicalDevice = physDevices[0];
+
+ uint extCount = 0;
+ VkEnumerateDeviceExtensionProperties(physicalDevice, null, &extCount, null);
+ var extensions = new VkExtensionProperties[extCount];
+ fixed (VkExtensionProperties* pExt = extensions)
+ VkEnumerateDeviceExtensionProperties(physicalDevice, null, &extCount, pExt);
+
+ var extNames = new HashSet();
+ fixed (VkExtensionProperties* pExts = extensions)
+ {
+ for (int i = 0; i < extCount; i++)
+ extNames.Add(Marshal.PtrToStringAnsi((IntPtr)pExts[i].extensionName) ?? "");
+ }
+
+ bool hasExternalMemory = extNames.Contains("VK_KHR_external_memory_fd");
+ bool hasDmaBuf = extNames.Contains("VK_EXT_external_memory_dma_buf");
+ bool hasDrmModifier = extNames.Contains("VK_EXT_image_drm_format_modifier");
+ bool hasQueueFamilyForeign = extNames.Contains("VK_EXT_queue_family_foreign");
+
+ results.Add(new TestResult("Vulkan_DmaBuf_Extension_Availability",
+ hasDmaBuf && hasDrmModifier ? TestStatus.Passed : TestStatus.Skipped,
+ $"external_memory_fd={hasExternalMemory}, dma_buf={hasDmaBuf}, drm_modifier={hasDrmModifier}, queue_family_foreign={hasQueueFamilyForeign}"));
+
+ if (!hasDmaBuf || !hasDrmModifier || !hasExternalMemory)
+ {
+ results.Add(new TestResult("Vulkan_DmaBuf_All", TestStatus.Skipped,
+ "required extensions not available"));
+ return results;
+ }
+
+ results.Add(TestModifierQuery(physicalDevice));
+
+ var requiredExtensions = new List
+ {
+ "VK_KHR_external_memory_fd",
+ "VK_EXT_external_memory_dma_buf",
+ "VK_EXT_image_drm_format_modifier"
+ };
+ if (hasQueueFamilyForeign)
+ requiredExtensions.Add("VK_EXT_queue_family_foreign");
+
+ uint queueFamilyCount = 0;
+ VkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, null);
+ var queueFamilies = stackalloc VkQueueFamilyProperties[(int)queueFamilyCount];
+ VkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueFamilyCount, queueFamilies);
+
+ uint graphicsQueueFamily = uint.MaxValue;
+ for (uint i = 0; i < queueFamilyCount; i++)
+ {
+ if ((queueFamilies[i].queueFlags & 0x01) != 0)
+ {
+ graphicsQueueFamily = i;
+ break;
+ }
+ }
+
+ if (graphicsQueueFamily == uint.MaxValue)
+ {
+ results.Add(new TestResult("Vulkan_DmaBuf_All", TestStatus.Skipped, "no graphics queue"));
+ return results;
+ }
+
+ var extNamePtrs = new IntPtr[requiredExtensions.Count];
+ for (int i = 0; i < requiredExtensions.Count; i++)
+ extNamePtrs[i] = Marshal.StringToHGlobalAnsi(requiredExtensions[i]);
+
+ try
+ {
+ float priority = 1.0f;
+ var queueCreateInfo = new VkDeviceQueueCreateInfo
+ {
+ sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
+ queueFamilyIndex = graphicsQueueFamily,
+ queueCount = 1,
+ pQueuePriorities = &priority
+ };
+
+ fixed (IntPtr* pExtNames = extNamePtrs)
+ {
+ var deviceCreateInfo = new VkDeviceCreateInfo
+ {
+ sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
+ queueCreateInfoCount = 1,
+ pQueueCreateInfos = &queueCreateInfo,
+ enabledExtensionCount = (uint)requiredExtensions.Count,
+ ppEnabledExtensionNames = (byte**)pExtNames
+ };
+
+ result = VkCreateDevice(physicalDevice, &deviceCreateInfo, null, &device);
+ }
+
+ if (result != VK_SUCCESS)
+ {
+ results.Add(new TestResult("Vulkan_DmaBuf_All", TestStatus.Failed,
+ $"vkCreateDevice failed: {result}"));
+ return results;
+ }
+
+ results.Add(TestDmaBufImageImport(physicalDevice, device, graphicsQueueFamily,
+ hasQueueFamilyForeign));
+ }
+ finally
+ {
+ for (int i = 0; i < extNamePtrs.Length; i++)
+ Marshal.FreeHGlobal(extNamePtrs[i]);
+ }
+ }
+ finally
+ {
+ if (device != IntPtr.Zero)
+ VkDestroyDevice(device, null);
+ if (instance != IntPtr.Zero)
+ VkDestroyInstance(instance, null);
+ }
+
+ return results;
+ }
+
+ private static TestResult TestModifierQuery(IntPtr physicalDevice)
+ {
+ var modList = new VkDrmFormatModifierPropertiesListEXT
+ {
+ sType = VK_STRUCTURE_TYPE_DRM_FORMAT_MODIFIER_PROPERTIES_LIST_EXT
+ };
+ var fmtProps = new VkFormatProperties2Native
+ {
+ sType = VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_2,
+ pNext = &modList
+ };
+
+ VkGetPhysicalDeviceFormatProperties2(physicalDevice, VK_FORMAT_B8G8R8A8_UNORM, &fmtProps);
+
+ if (modList.drmFormatModifierCount == 0)
+ return new TestResult("Vulkan_DmaBuf_Modifier_Query", TestStatus.Failed,
+ "no DRM format modifiers for B8G8R8A8_UNORM");
+
+ var modifiers = new VkDrmFormatModifierPropertiesEXT[modList.drmFormatModifierCount];
+ fixed (VkDrmFormatModifierPropertiesEXT* pMods = modifiers)
+ {
+ modList.pDrmFormatModifierProperties = pMods;
+ fmtProps.pNext = &modList;
+ VkGetPhysicalDeviceFormatProperties2(physicalDevice, VK_FORMAT_B8G8R8A8_UNORM, &fmtProps);
+ }
+
+ bool hasLinear = false;
+ foreach (var mod in modifiers)
+ if (mod.drmFormatModifier == 0)
+ hasLinear = true;
+
+ return new TestResult("Vulkan_DmaBuf_Modifier_Query", TestStatus.Passed,
+ $"{modList.drmFormatModifierCount} modifiers, linear={hasLinear}");
+ }
+
+ private static TestResult TestDmaBufImageImport(IntPtr physicalDevice, IntPtr device,
+ uint graphicsQueueFamily, bool hasQueueFamilyForeign)
+ {
+ const uint width = 64, height = 64;
+
+ using var allocator = new DmaBufAllocator();
+ if (!allocator.IsAvailable)
+ return new TestResult("Vulkan_DmaBuf_Image_Import", TestStatus.Skipped,
+ "no DMA-BUF allocator available");
+
+ using var alloc = allocator.AllocateLinear(width, height, GBM_FORMAT_ARGB8888, 0xFF00FF00);
+ if (alloc == null)
+ return new TestResult("Vulkan_DmaBuf_Image_Import", TestStatus.Skipped,
+ "could not allocate DMA-BUF");
+
+ var importFd = Dup(alloc.Fd);
+ if (importFd < 0)
+ return new TestResult("Vulkan_DmaBuf_Image_Import", TestStatus.Failed, "dup() failed");
+
+ ulong vkImage = 0;
+ ulong vkMemory = 0;
+ ulong vkImageView = 0;
+
+ try
+ {
+ var getMemFdPropsPtr = VkGetDeviceProcAddr(device, "vkGetMemoryFdPropertiesKHR");
+ if (getMemFdPropsPtr == IntPtr.Zero)
+ return new TestResult("Vulkan_DmaBuf_Image_Import", TestStatus.Failed,
+ "vkGetMemoryFdPropertiesKHR not found");
+
+ var getMemFdProps = Marshal.GetDelegateForFunctionPointer(getMemFdPropsPtr);
+
+ var fdProps = new VkMemoryFdPropertiesKHRNative
+ {
+ sType = VK_STRUCTURE_TYPE_MEMORY_FD_PROPERTIES_KHR
+ };
+ var result = getMemFdProps(device, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, importFd, &fdProps);
+ if (result != VK_SUCCESS)
+ return new TestResult("Vulkan_DmaBuf_Image_Import", TestStatus.Failed,
+ $"vkGetMemoryFdPropertiesKHR failed: {result}");
+
+ var externalMemoryInfo = new VkExternalMemoryImageCreateInfo
+ {
+ sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO,
+ handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT
+ };
+
+ var planeLayout = new VkSubresourceLayout
+ {
+ offset = 0,
+ rowPitch = alloc.Stride,
+ size = 0,
+ arrayPitch = 0,
+ depthPitch = 0
+ };
+
+ var drmModifierInfo = new VkImageDrmFormatModifierExplicitCreateInfoEXT
+ {
+ sType = VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_EXPLICIT_CREATE_INFO_EXT,
+ pNext = &externalMemoryInfo,
+ drmFormatModifier = alloc.Modifier,
+ drmFormatModifierPlaneCount = 1,
+ pPlaneLayouts = &planeLayout
+ };
+
+ var imageCreateInfo = new VkImageCreateInfo
+ {
+ sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
+ pNext = &drmModifierInfo,
+ imageType = VK_IMAGE_TYPE_2D,
+ format = VK_FORMAT_B8G8R8A8_UNORM,
+ extent_width = width,
+ extent_height = height,
+ extent_depth = 1,
+ mipLevels = 1,
+ arrayLayers = 1,
+ samples = VK_SAMPLE_COUNT_1_BIT,
+ tiling = VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT,
+ usage = VK_IMAGE_USAGE_SAMPLED_BIT,
+ sharingMode = VK_SHARING_MODE_EXCLUSIVE,
+ initialLayout = VK_IMAGE_LAYOUT_UNDEFINED
+ };
+
+ result = VkCreateImage(device, &imageCreateInfo, null, &vkImage);
+ if (result != VK_SUCCESS)
+ return new TestResult("Vulkan_DmaBuf_Image_Import", TestStatus.Failed,
+ $"vkCreateImage failed: {result}");
+
+ VkMemoryRequirements memReqs;
+ VkGetImageMemoryRequirements(device, vkImage, &memReqs);
+
+ VkPhysicalDeviceMemoryProperties memProps;
+ VkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProps);
+
+ uint memTypeIndex = uint.MaxValue;
+ uint compatBits = memReqs.memoryTypeBits & fdProps.memoryTypeBits;
+ for (uint i = 0; i < memProps.memoryTypeCount; i++)
+ {
+ if ((compatBits & (1u << (int)i)) != 0)
+ {
+ memTypeIndex = i;
+ break;
+ }
+ }
+
+ if (memTypeIndex == uint.MaxValue)
+ return new TestResult("Vulkan_DmaBuf_Image_Import", TestStatus.Failed,
+ $"no compatible memory type (image={memReqs.memoryTypeBits:X}, fd={fdProps.memoryTypeBits:X})");
+
+ var dedicatedInfo = new VkMemoryDedicatedAllocateInfo
+ {
+ sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO,
+ image = vkImage,
+ buffer = 0
+ };
+
+ var importFdInfo = new VkImportMemoryFdInfoKHR
+ {
+ sType = VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR,
+ pNext = &dedicatedInfo,
+ handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT,
+ fd = importFd
+ };
+
+ var allocInfo = new VkMemoryAllocateInfo
+ {
+ sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
+ pNext = &importFdInfo,
+ allocationSize = memReqs.size,
+ memoryTypeIndex = memTypeIndex
+ };
+
+ result = VkAllocateMemory(device, &allocInfo, null, &vkMemory);
+ if (result != VK_SUCCESS)
+ return new TestResult("Vulkan_DmaBuf_Image_Import", TestStatus.Failed,
+ $"vkAllocateMemory failed: {result}");
+
+ importFd = -1; // Vulkan took ownership
+
+ result = VkBindImageMemory(device, vkImage, vkMemory, 0);
+ if (result != VK_SUCCESS)
+ return new TestResult("Vulkan_DmaBuf_Image_Import", TestStatus.Failed,
+ $"vkBindImageMemory failed: {result}");
+
+ var viewCreateInfo = new VkImageViewCreateInfo
+ {
+ sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
+ image = vkImage,
+ viewType = VK_IMAGE_VIEW_TYPE_2D,
+ format = VK_FORMAT_B8G8R8A8_UNORM,
+ components_r = VK_COMPONENT_SWIZZLE_IDENTITY,
+ components_g = VK_COMPONENT_SWIZZLE_IDENTITY,
+ components_b = VK_COMPONENT_SWIZZLE_IDENTITY,
+ components_a = VK_COMPONENT_SWIZZLE_IDENTITY,
+ subresourceRange_aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ subresourceRange_baseMipLevel = 0,
+ subresourceRange_levelCount = 1,
+ subresourceRange_baseArrayLayer = 0,
+ subresourceRange_layerCount = 1
+ };
+
+ result = VkCreateImageView(device, &viewCreateInfo, null, &vkImageView);
+ if (result != VK_SUCCESS)
+ return new TestResult("Vulkan_DmaBuf_Image_Import", TestStatus.Failed,
+ $"vkCreateImageView failed: {result}");
+
+ return new TestResult("Vulkan_DmaBuf_Image_Import", TestStatus.Passed,
+ $"image + view created, memType={memTypeIndex}, modifier=0x{alloc.Modifier:X}");
+ }
+ finally
+ {
+ if (vkImageView != 0)
+ VkDestroyImageView(device, vkImageView, null);
+ if (vkMemory != 0)
+ VkFreeMemory(device, vkMemory, null);
+ if (vkImage != 0)
+ VkDestroyImage(device, vkImage, null);
+ if (importFd >= 0)
+ Close(importFd);
+ }
+ }
+
+ // We need dup() for duplicating the DMA-BUF fd
+ [LibraryImport("libc", EntryPoint = "dup")]
+ private static partial int Dup(int fd);
+}