mirror-ghostty/src/renderer/vulkan/Target.zig

320 lines
11 KiB
Zig

//! 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 `<drm/drm_fourcc.h>`. 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());
}