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); +}