From ebe48bd4cd1be830eef0445b0b579bb93f8d5bd6 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 24 May 2026 09:53:21 -0500 Subject: [PATCH] renderer/vulkan: render target with dmabuf export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `vulkan/Target.zig` — the linchpin of the zero-copy presentation path. Creates an exportable `VkImage` backed by linear- tiled `VkDeviceMemory` whose dmabuf fd is the payload of `ghostty_platform_vulkan_s.present`. The Vulkan side: - `VkExternalMemoryImageCreateInfo` chained on `VkImageCreateInfo` via `pNext` declares the image as externally shareable with `VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT`. - `VkExportMemoryAllocateInfo` chained on `VkMemoryAllocateInfo` declares the backing memory as exportable. - `vkGetMemoryFdKHR` extracts the dmabuf fd post-bind. - `vkGetImageSubresourceLayout` gives us the driver's actual row stride (which may include alignment padding) for the host. What gets handed to the host through the platform callback: - dmabuf fd (a borrow; valid for the duration of the call) - DRM fourcc (derived from VkFormat by `vkFormatToDrmFourcc` — the common formats the renderer uses; Vulkan & DRM disagree on byte order naming, the mapping comments call this out) - DRM modifier (currently always `DRM_FORMAT_MOD_LINEAR = 0`) - width / height (pixels) - stride (bytes per row, from VkSubresourceLayout) Linear vs DRM format modifier tiling: this commit uses `VK_IMAGE_TILING_LINEAR` for v1. Cross-driver safe and every dmabuf consumer (Qt RHI, Wayland compositors) accepts it without modifier negotiation. The cost is reduced rasterization performance vs `VK_IMAGE_TILING_OPTIMAL`. The driver-chosen modifier path via `VK_EXT_image_drm_format_modifier` is a contained follow-up — for now that extension is removed from `Device.REQUIRED_DEVICE_EXTENSIONS` so the host doesn't have to enable it. Ownership & lifetime: - libghostty owns the image, memory, and fd for the lifetime of the `Target`. - `deinit` destroys the view + image, frees the memory, and closes the fd. - The fd handed via `present` is a borrow — the host must `dup()` if it needs to hold it past the call. - `Target.present(self)` is a small helper that routes through the platform callback in one place. Dispatch additions: `vkGetMemoryFdKHR` (extension function, needed to export the fd) and `vkGetImageSubresourceLayout` (for the row stride). Other resource functions reuse what `Texture.zig` already loaded (`vkCreateImage`, `vkAllocateMemory`, `vkBindImageMemory`, view creation, etc.). Verification: temp-switch compile-check; only the expected downstream `DerivedConfig` error from the stub substitution. Reverted. OpenGL build still silent / clean. Co-Authored-By: claude-flow --- src/renderer/Vulkan.zig | 1 + src/renderer/vulkan/Device.zig | 19 +- src/renderer/vulkan/Target.zig | 319 +++++++++++++++++++++++++++++++++ 3 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 src/renderer/vulkan/Target.zig diff --git a/src/renderer/Vulkan.zig b/src/renderer/Vulkan.zig index 916c17632..524aaeaf0 100644 --- a/src/renderer/Vulkan.zig +++ b/src/renderer/Vulkan.zig @@ -66,6 +66,7 @@ pub const Device = @import("vulkan/Device.zig"); pub const Sampler = @import("vulkan/Sampler.zig"); pub const Texture = @import("vulkan/Texture.zig"); +pub const Target = @import("vulkan/Target.zig"); pub const CommandPool = @import("vulkan/CommandPool.zig"); pub const Pipeline = @import("vulkan/Pipeline.zig"); pub const shaders = @import("vulkan/shaders.zig"); diff --git a/src/renderer/vulkan/Device.zig b/src/renderer/vulkan/Device.zig index 9009fc93e..019237275 100644 --- a/src/renderer/vulkan/Device.zig +++ b/src/renderer/vulkan/Device.zig @@ -47,10 +47,15 @@ pub const MIN_API_VERSION = vk.VK_API_VERSION_1_3; /// Device extensions libghostty enables on top of the host's /// VkDevice setup. The host must have created its VkDevice with /// these enabled; we only verify availability here. +/// +/// Note: `VK_EXT_image_drm_format_modifier` is intentionally NOT +/// required yet — `vulkan/Target.zig` currently uses +/// `VK_IMAGE_TILING_LINEAR` for dmabuf export, which only needs the +/// two extensions below. When the driver-chosen modifier path lands, +/// add the modifier extension back here. pub const REQUIRED_DEVICE_EXTENSIONS = [_][:0]const u8{ "VK_KHR_external_memory_fd", "VK_EXT_external_memory_dma_buf", - "VK_EXT_image_drm_format_modifier", }; /// Errors that can come out of `init`. @@ -136,6 +141,12 @@ pub const Dispatch = struct { destroyPipelineLayout: std.meta.Child(vk.PFN_vkDestroyPipelineLayout), createGraphicsPipelines: std.meta.Child(vk.PFN_vkCreateGraphicsPipelines), destroyPipeline: std.meta.Child(vk.PFN_vkDestroyPipeline), + + // External memory fd export — used by `vulkan/Target.zig`. + // `vkGetMemoryFdKHR` is from `VK_KHR_external_memory_fd`; needs + // device-level resolution like any other device function. + getMemoryFdKHR: std.meta.Child(vk.PFN_vkGetMemoryFdKHR), + getImageSubresourceLayout: std.meta.Child(vk.PFN_vkGetImageSubresourceLayout), }; // ---- fields --------------------------------------------------------- @@ -364,6 +375,10 @@ pub fn init( try dl.load(vk.PFN_vkCreateGraphicsPipelines, "vkCreateGraphicsPipelines"); const destroy_pipeline = try dl.load(vk.PFN_vkDestroyPipeline, "vkDestroyPipeline"); + const get_memory_fd_khr = + try dl.load(vk.PFN_vkGetMemoryFdKHR, "vkGetMemoryFdKHR"); + const get_image_subresource_layout = + try dl.load(vk.PFN_vkGetImageSubresourceLayout, "vkGetImageSubresourceLayout"); return .{ .platform = platform, @@ -414,6 +429,8 @@ pub fn init( .destroyPipelineLayout = destroy_pipeline_layout, .createGraphicsPipelines = create_graphics_pipelines, .destroyPipeline = destroy_pipeline, + .getMemoryFdKHR = get_memory_fd_khr, + .getImageSubresourceLayout = get_image_subresource_layout, }, }; } diff --git a/src/renderer/vulkan/Target.zig b/src/renderer/vulkan/Target.zig new file mode 100644 index 000000000..83f0ca086 --- /dev/null +++ b/src/renderer/vulkan/Target.zig @@ -0,0 +1,319 @@ +//! Render target: an exportable `VkImage` backed by linear-tiled, +//! externally-shareable `VkDeviceMemory` whose dmabuf fd is the +//! payload of `ghostty_platform_vulkan_s.present`. +//! +//! This is what makes the whole Vulkan port worthwhile: instead of +//! reading the frame back into a `QImage` like the OpenGL path does, +//! the host (Qt RHI via `QRhiTexture`) imports our memory directly +//! and composites it in-GPU. Zero-copy, no readback. +//! +//! Layout: **linear tiling** for v1. Linear is the safest cross- +//! driver choice for dmabuf consumers — every Wayland compositor, +//! every Qt RHI backend, every reader can accept linear without +//! modifier negotiation. The cost is reduced rasterization perf vs +//! `VK_IMAGE_TILING_OPTIMAL`. For a terminal at ~60Hz with a few +//! megapixels of fill, linear is fine. Driver-chosen DRM format +//! modifiers (the "optimal+exportable" path via +//! `VK_EXT_image_drm_format_modifier`) is a contained follow-up. +//! +//! Ownership: libghostty owns the `VkImage`, `VkDeviceMemory`, and +//! the dmabuf fd for the lifetime of the `Target`. The fd is passed +//! to the host via `present` as a borrow; the host must `dup()` if +//! it needs to hold it past the call. `deinit` closes the fd and +//! frees the memory. +//! +//! Counterpart: `src/renderer/opengl/Target.zig`. + +const Self = @This(); + +const std = @import("std"); +const vk = @import("vulkan").c; + +const Device = @import("Device.zig"); + +const log = std.log.scoped(.vulkan); + +/// DRM modifier sentinel for "linear, no tiling". Matches +/// `DRM_FORMAT_MOD_LINEAR` from ``. Hardcoded so we +/// don't pull in libdrm headers just for a single constant. +pub const DRM_FORMAT_MOD_LINEAR: u64 = 0; + +pub const Options = struct { + device: *const Device, + + /// Color format. The DRM fourcc the host receives is derived + /// from this — see `vkFormatToDrmFourcc` below. + format: vk.VkFormat, + + /// Render target dimensions, in pixels. + width: u32, + height: u32, + + /// Extra `VkImageUsageFlagBits` beyond the defaults + /// (`COLOR_ATTACHMENT_BIT | SAMPLED_BIT`). Rarely needed; left + /// as an escape hatch for things like a transfer source for + /// debug captures. + extra_usage: vk.VkImageUsageFlags = 0, +}; + +pub const Error = error{ + /// A `vkCreate*` / `vkAllocate*` / `vkBind*` / `vkGetMemoryFdKHR` + /// returned a non-success status. + VulkanFailed, + /// `Device.findMemoryType` couldn't find a memory type matching + /// the image's requirements and the export memory flag bit. + NoSuitableMemoryType, + /// The provided `VkFormat` doesn't map to a known DRM fourcc. + /// Currently the renderer only ever uses + /// `VK_FORMAT_B8G8R8A8_UNORM` / `_R8G8B8A8_UNORM` so this is a + /// guard against config drift rather than a real failure mode. + UnsupportedFormat, +}; + +device: *const Device, + +image: vk.VkImage, +memory: vk.VkDeviceMemory, +view: vk.VkImageView, + +format: vk.VkFormat, +width: u32, +height: u32, + +/// dmabuf fd. Owned by `Target` until `deinit`; the host must +/// `dup()` if it wants to hold it past a `present` call. +fd: i32, + +/// DRM fourcc the host should interpret the dmabuf as. Derived from +/// `format` at construction time so the apprt callback can pass it +/// straight through. +drm_format: u32, + +/// DRM modifier. Always `DRM_FORMAT_MOD_LINEAR` for v1. +drm_modifier: u64, + +/// Row stride in bytes — `vkGetImageSubresourceLayout` tells us the +/// driver's actual rowPitch (which may include alignment padding). +/// The host needs this for the dmabuf import. +stride: u32, + +/// Current image layout, mirroring the same field on `Texture`. +/// Starts at `UNDEFINED`; the renderer transitions it as needed +/// across the frame. +layout: vk.VkImageLayout = vk.VK_IMAGE_LAYOUT_UNDEFINED, + +pub fn init(opts: Options) Error!Self { + const dev = opts.device; + const drm_format = try vkFormatToDrmFourcc(opts.format); + + const usage = @as(vk.VkImageUsageFlags, vk.VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) | + vk.VK_IMAGE_USAGE_SAMPLED_BIT | + opts.extra_usage; + + // ---- 1. VkImage (with external-memory chain) ---------------- + const external_memory_image_info: vk.VkExternalMemoryImageCreateInfo = .{ + .sType = vk.VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO, + .pNext = null, + .handleTypes = vk.VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, + }; + const image_info: vk.VkImageCreateInfo = .{ + .sType = vk.VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .pNext = &external_memory_image_info, + .flags = 0, + .imageType = vk.VK_IMAGE_TYPE_2D, + .format = opts.format, + .extent = .{ .width = opts.width, .height = opts.height, .depth = 1 }, + .mipLevels = 1, + .arrayLayers = 1, + .samples = vk.VK_SAMPLE_COUNT_1_BIT, + .tiling = vk.VK_IMAGE_TILING_LINEAR, + .usage = usage, + .sharingMode = vk.VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = null, + .initialLayout = vk.VK_IMAGE_LAYOUT_UNDEFINED, + }; + var image: vk.VkImage = undefined; + { + const r = dev.dispatch.createImage(dev.device, &image_info, null, &image); + if (r != vk.VK_SUCCESS) { + log.err("vkCreateImage (Target) failed: result={}", .{r}); + return error.VulkanFailed; + } + } + errdefer dev.dispatch.destroyImage(dev.device, image, null); + + // ---- 2. VkDeviceMemory (with export chain) ------------------ + var reqs: vk.VkMemoryRequirements = undefined; + dev.dispatch.getImageMemoryRequirements(dev.device, image, &reqs); + + // DEVICE_LOCAL is preferred but not required for linear export + // memory — some drivers only expose HOST_VISIBLE memory types + // matching the requirements bitmask for linear tiling. We don't + // care which heap as long as it's exportable. + const memory_type_index = dev.findMemoryType(reqs.memoryTypeBits, 0) orelse { + log.err( + "no exportable memory type for Target (typeBits=0x{x})", + .{reqs.memoryTypeBits}, + ); + return error.NoSuitableMemoryType; + }; + + const export_info: vk.VkExportMemoryAllocateInfo = .{ + .sType = vk.VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO, + .pNext = null, + .handleTypes = vk.VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, + }; + const alloc_info: vk.VkMemoryAllocateInfo = .{ + .sType = vk.VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .pNext = &export_info, + .allocationSize = reqs.size, + .memoryTypeIndex = memory_type_index, + }; + var memory: vk.VkDeviceMemory = undefined; + { + const r = dev.dispatch.allocateMemory(dev.device, &alloc_info, null, &memory); + if (r != vk.VK_SUCCESS) { + log.err("vkAllocateMemory (Target) failed: result={}", .{r}); + return error.VulkanFailed; + } + } + errdefer dev.dispatch.freeMemory(dev.device, memory, null); + + { + const r = dev.dispatch.bindImageMemory(dev.device, image, memory, 0); + if (r != vk.VK_SUCCESS) { + log.err("vkBindImageMemory (Target) failed: result={}", .{r}); + return error.VulkanFailed; + } + } + + // ---- 3. Export the dmabuf fd -------------------------------- + const fd_info: vk.VkMemoryGetFdInfoKHR = .{ + .sType = vk.VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR, + .pNext = null, + .memory = memory, + .handleType = vk.VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, + }; + var fd: c_int = -1; + { + const r = dev.dispatch.getMemoryFdKHR(dev.device, &fd_info, &fd); + if (r != vk.VK_SUCCESS or fd < 0) { + log.err("vkGetMemoryFdKHR failed: result={} fd={}", .{ r, fd }); + return error.VulkanFailed; + } + } + errdefer std.posix.close(fd); + + // ---- 4. Stride from the driver's subresource layout --------- + const subresource: vk.VkImageSubresource = .{ + .aspectMask = vk.VK_IMAGE_ASPECT_COLOR_BIT, + .mipLevel = 0, + .arrayLayer = 0, + }; + var sub_layout: vk.VkSubresourceLayout = undefined; + dev.dispatch.getImageSubresourceLayout(dev.device, image, &subresource, &sub_layout); + + // ---- 5. VkImageView ----------------------------------------- + const view_info: vk.VkImageViewCreateInfo = .{ + .sType = vk.VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .pNext = null, + .flags = 0, + .image = image, + .viewType = vk.VK_IMAGE_VIEW_TYPE_2D, + .format = opts.format, + .components = .{ + .r = vk.VK_COMPONENT_SWIZZLE_IDENTITY, + .g = vk.VK_COMPONENT_SWIZZLE_IDENTITY, + .b = vk.VK_COMPONENT_SWIZZLE_IDENTITY, + .a = vk.VK_COMPONENT_SWIZZLE_IDENTITY, + }, + .subresourceRange = .{ + .aspectMask = vk.VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }; + var view: vk.VkImageView = undefined; + { + const r = dev.dispatch.createImageView(dev.device, &view_info, null, &view); + if (r != vk.VK_SUCCESS) { + log.err("vkCreateImageView (Target) failed: result={}", .{r}); + return error.VulkanFailed; + } + } + + return .{ + .device = dev, + .image = image, + .memory = memory, + .view = view, + .format = opts.format, + .width = opts.width, + .height = opts.height, + .fd = fd, + .drm_format = drm_format, + .drm_modifier = DRM_FORMAT_MOD_LINEAR, + .stride = @intCast(sub_layout.rowPitch), + }; +} + +pub fn deinit(self: *Self) void { + const dev = self.device; + dev.dispatch.destroyImageView(dev.device, self.view, null); + dev.dispatch.destroyImage(dev.device, self.image, null); + dev.dispatch.freeMemory(dev.device, self.memory, null); + if (self.fd >= 0) std.posix.close(self.fd); + self.* = undefined; +} + +/// Hand the target's dmabuf fd to the host's `present` callback. The +/// fd is a temporary borrow valid only until this call returns; the +/// host must `dup()` if it needs to hold it past then. The +/// underlying memory remains owned by libghostty. +pub fn present(self: *const Self) void { + self.device.platform.present( + self.device.platform.userdata, + self.fd, + self.drm_format, + self.drm_modifier, + self.width, + self.height, + self.stride, + ); +} + +/// Map a `VkFormat` to its DRM fourcc. Vulkan and DRM disagree on +/// byte order naming: Vulkan format names are in memory order, DRM +/// names are little-endian from MSB. The mapping table here covers +/// the formats the renderer actually targets — extend as new ones +/// are added. +fn vkFormatToDrmFourcc(format: vk.VkFormat) Error!u32 { + // DRM fourcc helpers — packing 4 ASCII chars LSB-first. + const fourcc = struct { + fn make(a: u8, b: u8, c: u8, d: u8) u32 { + return (@as(u32, a)) | + (@as(u32, b) << 8) | + (@as(u32, c) << 16) | + (@as(u32, d) << 24); + } + }; + return switch (format) { + // Vulkan B,G,R,A in memory = DRM_FORMAT_ARGB8888 ("AR24"). + // This is what Wayland compositors prefer. + vk.VK_FORMAT_B8G8R8A8_UNORM, + vk.VK_FORMAT_B8G8R8A8_SRGB, + => fourcc.make('A', 'R', '2', '4'), + // Vulkan R,G,B,A in memory = DRM_FORMAT_ABGR8888 ("AB24"). + vk.VK_FORMAT_R8G8B8A8_UNORM, + vk.VK_FORMAT_R8G8B8A8_SRGB, + => fourcc.make('A', 'B', '2', '4'), + else => error.UnsupportedFormat, + }; +} + +test { + std.testing.refAllDecls(@This()); +}