Something went wrong. Try again.
Windows, a GPU renderer and text for Zig programs that draw their own interface.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The Vulkan instance and device every window draws with.//!//! The loader is opened at run time rather than linked, so that a machine//! without Vulkan gets a message rather than a binary that will not start.//! On Wayland no surface extensions are asked for, since there is no//! surface to make (see `Shm.zig` and `Dmabuf.zig`); on Windows frames go to//! a swapchain on a Win32 surface, so a device must be able to present to//! one.
const std = @import("std");const builtin = @import("builtin");const Allocator = std.mem.Allocator;const vk = @import("vk");
const dmabuf = @import("dmabuf.zig");const testing_ = @import("../testing.zig");
const Device = @This();
const log = std.log.scoped(.vulkan);
/// The Vulkan version asked for, and the least a device must have to be/// chosen.pub const api_version = vk.API_VERSION_1_2;
const windows = builtin.os.tag == .windows;
const loader_name = if (windows) "vulkan-1.dll" else "libvulkan.so.1";
/// The loader, opened as a library: `std.DynLib`, which has no Windows/// implementation, and on Windows `LoadLibraryW` itself.const Library = if (windows) struct { const win32 = @import("win32").everything;
module: win32.HINSTANCE,
fn open(comptime name: []const u8) !@This() { return .{ .module = win32.LoadLibraryW(std.unicode.utf8ToUtf16LeStringLiteral(name)) orelse return error.FileNotFound }; }
fn close(l: *@This()) void { _ = win32.FreeLibrary(l.module); }
fn lookup(l: *@This(), comptime T: type, name: [:0]const u8) ?T { return @ptrCast(win32.GetProcAddress(l.module, name) orelse return null); }} else struct { lib: std.DynLib,
fn open(comptime name: []const u8) !@This() { return .{ .lib = try std.DynLib.open(name) }; }
fn close(l: *@This()) void { l.lib.close(); }
fn lookup(l: *@This(), comptime T: type, name: [:0]const u8) ?T { return l.lib.lookup(T, name); }};
/// What the instance needs for the target windows present to.const instance_extensions: []const [*:0]const u8 = if (windows) &.{ vk.extensions.khr_surface.name, vk.extensions.khr_win_32_surface.name }else &.{};
/// What a device needs, whatever else it can do.const device_extensions: []const [*:0]const u8 = if (windows) &.{vk.extensions.khr_swapchain.name}else &.{};
gpa: Allocator,lib: Library,base: vk.BaseWrapper,instance_wrapper: *vk.InstanceWrapper,instance: vk.InstanceProxy,physical: vk.PhysicalDevice,properties: vk.PhysicalDeviceProperties,memory_properties: vk.PhysicalDeviceMemoryProperties,device_wrapper: *vk.DeviceWrapper,device: vk.DeviceProxy,queue: vk.Queue,queue_family: u32,/// What the device can do with dma-bufs, when it can share them at all.dmabuf_support: ?dmabuf.Support,
var get_instance_proc_addr: vk.PfnGetInstanceProcAddr = undefined;
fn getInstanceProcAddr(instance: vk.Instance, name: [*:0]const u8) callconv(vk.vulkan_call_conv) vk.PfnVoidFunction { return get_instance_proc_addr(instance, name);}
/// What `init` makes, and which device it chooses.pub const Options = struct { /// The `dev_t` of the device the compositor composites with, which is /// the one whose memory it can read without a copy. main_device: ?u64 = null, /// What the Vulkan instance says it is for, which tools and drivers /// may show. application_name: [*:0]const u8, /// Turn on the validation layer, where it is installed. validation: bool = false, /// Use the device whose name contains this, rather than choosing. name: ?[]const u8 = null,};
/// Opens the Vulkan loader, makes an instance, and makes a device on the/// best physical device there is: the one `options.name` names, if it/// names one; otherwise the compositor's own (`options.main_device`) where/// it can share dma-bufs, then a discrete GPU before an integrated one/// before anything else. One queue, which draws and, on Windows, presents.////// `error.NoVulkan` when there is no loader to open, and/// `error.NoUsableDevice` when no device has Vulkan 1.2 and a queue that/// draws (and on Windows, presents), or none has the name asked for. On the heap, since what is made/// from it keeps a pointer to it.pub fn init(gpa: Allocator, options: Options) !*Device { const d = try gpa.create(Device); errdefer gpa.destroy(d);
var lib = Library.open(loader_name) catch |err| { log.err("no Vulkan loader ({s}): {t}", .{ loader_name, err }); return error.NoVulkan; }; errdefer lib.close(); get_instance_proc_addr = lib.lookup(vk.PfnGetInstanceProcAddr, "vkGetInstanceProcAddr") orelse return error.NoVulkan;
const base = vk.BaseWrapper.load(getInstanceProcAddr);
var layers_buf: [1][*:0]const u8 = undefined; var layers: []const [*:0]const u8 = &.{}; if (options.validation) { const available = try base.enumerateInstanceLayerPropertiesAlloc(gpa); defer gpa.free(available); for (available) |l| if (std.mem.eql(u8, std.mem.sliceTo(&l.layer_name, 0), "VK_LAYER_KHRONOS_validation")) { layers_buf[0] = "VK_LAYER_KHRONOS_validation"; layers = &layers_buf; }; if (layers.len == 0) log.warn("validation asked for, but VK_LAYER_KHRONOS_validation is not installed", .{}); }
const instance_handle = try base.createInstance(&.{ .p_application_info = &.{ .p_application_name = options.application_name, .application_version = 0, .p_engine_name = "zig-aerie", .engine_version = 0, .api_version = api_version.toU32(), }, .enabled_layer_count = @intCast(layers.len), .pp_enabled_layer_names = layers.ptr, .enabled_extension_count = @intCast(instance_extensions.len), .pp_enabled_extension_names = instance_extensions.ptr, }, null); const instance_wrapper = try gpa.create(vk.InstanceWrapper); errdefer gpa.destroy(instance_wrapper); instance_wrapper.* = .load(instance_handle, getInstanceProcAddr); const instance = vk.InstanceProxy.init(instance_handle, instance_wrapper); errdefer instance.destroyInstance(null);
const chosen = try choose(gpa, instance, options.main_device, options.name); log.debug("drawing with {s}{s}", .{ std.mem.sliceTo(&chosen.properties.device_name, 0), if (chosen.support != null) ", sharing dma-bufs" else "", });
var ext_buf: [dmabuf.max_extensions + device_extensions.len][*:0]const u8 = undefined; @memcpy(ext_buf[0..device_extensions.len], device_extensions); const n_dmabuf = if (chosen.support) |s| s.extensions(ext_buf[device_extensions.len..][0..dmabuf.max_extensions]).len else 0; const extensions = ext_buf[0 .. device_extensions.len + n_dmabuf]; var timeline_features: vk.PhysicalDeviceTimelineSemaphoreFeatures = .{ .timeline_semaphore = .true }; const want_timelines = chosen.support != null and chosen.support.?.timelines; const priority: [1]f32 = .{1}; // Allocated before the device it dispatches for, which could not be // destroyed without it. const device_wrapper = try gpa.create(vk.DeviceWrapper); errdefer gpa.destroy(device_wrapper); const device_handle = try instance.createDevice(chosen.physical, &.{ .p_next = if (want_timelines) &timeline_features else null, .queue_create_info_count = 1, .p_queue_create_infos = &.{.{ .queue_family_index = chosen.queue_family, .queue_count = 1, .p_queue_priorities = &priority, }}, .enabled_extension_count = @intCast(extensions.len), .pp_enabled_extension_names = extensions.ptr, }, null); device_wrapper.* = .load(device_handle, instance_wrapper.dispatch.vkGetDeviceProcAddr.?); const device = vk.DeviceProxy.init(device_handle, device_wrapper); errdefer device.destroyDevice(null);
d.* = .{ .gpa = gpa, .lib = lib, .base = base, .instance_wrapper = instance_wrapper, .instance = instance, .physical = chosen.physical, .properties = chosen.properties, .memory_properties = instance.getPhysicalDeviceMemoryProperties(chosen.physical), .device_wrapper = device_wrapper, .device = device, .queue = device.getDeviceQueue(chosen.queue_family, 0), .queue_family = chosen.queue_family, .dmabuf_support = chosen.support, }; return d;}
test init { try testing_.live(); const device = try Device.init(std.testing.allocator, .{ .application_name = "aerie doctest" }); defer device.deinit(); try std.testing.expect(device.properties.api_version >= api_version.toU32());}
/// Waits for the device to be idle, then destroys it, the instance, and/// itself, and closes the loader. Everything made from the device must be/// gone first.pub fn deinit(d: *Device) void { d.device.deviceWaitIdle() catch {}; d.device.destroyDevice(null); d.instance.destroyInstance(null); d.gpa.destroy(d.device_wrapper); d.gpa.destroy(d.instance_wrapper); d.lib.close(); d.gpa.destroy(d);}
test deinit { try testing_.live(); const device = try Device.init(std.testing.allocator, .{ .application_name = "aerie doctest" }); device.deinit();}
const Chosen = struct { physical: vk.PhysicalDevice, properties: vk.PhysicalDeviceProperties, queue_family: u32, support: ?dmabuf.Support,};
/// The compositor's own device if it named one we can draw with; otherwise/// a discrete GPU before an integrated one before anything else.fn choose(gpa: Allocator, instance: vk.InstanceProxy, main_device: ?u64, name: ?[]const u8) !Chosen { const devices = try instance.enumeratePhysicalDevicesAlloc(gpa); defer gpa.free(devices);
var best: ?Chosen = null; var best_score: i32 = -1; for (devices) |pd| { const props = instance.getPhysicalDeviceProperties(pd); if (props.api_version < api_version.toU32()) continue; const family = graphicsFamily(gpa, instance, pd) orelse continue;
const extensions = try instance.enumerateDeviceExtensionPropertiesAlloc(pd, null, gpa); defer gpa.free(extensions); if (!hasAll(extensions, device_extensions)) continue; const support = dmabuf.probe(instance, pd, extensions);
var score: i32 = switch (props.device_type) { .discrete_gpu => 30, .integrated_gpu => 20, .virtual_gpu => 10, else => 0, }; if (support != null) score += 5; if (main_device) |dev| if (support) |s| if (s.isDevice(dev)) { score += 100; }; if (name) |want| { if (std.ascii.findIgnoreCase(std.mem.sliceTo(&props.device_name, 0), want) != null) score += 1000 else continue; } if (score > best_score) { best_score = score; best = .{ .physical = pd, .properties = props, .queue_family = family, .support = support }; } } if (best == null) if (name) |want| log.err("no Vulkan device's name contains \"{s}\"", .{want}); return best orelse error.NoUsableDevice;}
/// A queue family that draws; on Windows, one that can present to a window/// as well, so that one queue does both.fn graphicsFamily(gpa: Allocator, instance: vk.InstanceProxy, pd: vk.PhysicalDevice) ?u32 { const families = instance.getPhysicalDeviceQueueFamilyPropertiesAlloc(pd, gpa) catch return null; defer gpa.free(families); for (families, 0..) |f, i| { if (!f.queue_flags.graphics) continue; if (windows and instance.getPhysicalDeviceWin32PresentationSupportKHR(pd, @intCast(i)) != .true) continue; return @intCast(i); } return null;}
fn hasAll(available: []const vk.ExtensionProperties, wanted: []const [*:0]const u8) bool { for (wanted) |w| { const name = std.mem.span(w); for (available) |a| { if (std.mem.eql(u8, std.mem.sliceTo(&a.extension_name, 0), name)) break; } else return false; } return true;}
/// A memory type among `type_bits` with all of `required`.pub fn memoryType(d: *const Device, type_bits: u32, required: vk.MemoryPropertyFlags) ?u32 { const props = d.memory_properties; for (props.memory_types[0..props.memory_type_count], 0..) |t, i| { if (type_bits & (@as(u32, 1) << @intCast(i)) != 0 and t.property_flags.contains(required)) return @intCast(i); } return null;}
test memoryType { try testing_.live(); const device = try Device.init(std.testing.allocator, .{ .application_name = "aerie doctest" }); defer device.deinit(); // Any type at all will do, so long as the CPU can write it. const index = device.memoryType(~@as(u32, 0), .{ .host_visible = true }) orelse return error.NoHostMemory; try std.testing.expect(index < device.memory_properties.memory_type_count);}
/// A buffer with its own memory.pub const Buffer = struct { buffer: vk.Buffer = .null_handle, memory: vk.DeviceMemory = .null_handle, size: vk.DeviceSize = 0, /// Mapped for the buffer's life, when it is host-visible. mapped: ?[*]u8 = null,
/// Unmaps and frees the buffer and its memory, if it has any, and /// leaves it empty, so that destroying it twice is harmless. pub fn destroy(b: *Buffer, d: *const Device) void { if (b.mapped != null) d.device.unmapMemory(b.memory); if (b.buffer != .null_handle) d.device.destroyBuffer(b.buffer, null); if (b.memory != .null_handle) d.device.freeMemory(b.memory, null); b.* = .{}; }
test destroy { try testing_.live(); const device = try Device.init(std.testing.allocator, .{ .application_name = "aerie doctest" }); defer device.deinit(); var buffer = try device.hostBuffer(1024, .{ .transfer_src = true }); buffer.destroy(device); try std.testing.expectEqual(vk.Buffer.null_handle, buffer.buffer); buffer.destroy(device); }};
/// A host-visible, coherent buffer of `size` bytes for `usage`, mapped for/// its life. `error.NoHostMemory` when the device has no memory the CPU can/// see that suits it.pub fn hostBuffer(d: *const Device, size: vk.DeviceSize, usage: vk.BufferUsageFlags) !Buffer { const buffer = try d.device.createBuffer(&.{ .size = size, .usage = usage, .sharing_mode = .exclusive }, null); errdefer d.device.destroyBuffer(buffer, null); const req = d.device.getBufferMemoryRequirements(buffer); const mt = d.memoryType(req.memory_type_bits, .{ .host_visible = true, .host_coherent = true }) orelse return error.NoHostMemory; const memory = try d.device.allocateMemory(&.{ .allocation_size = req.size, .memory_type_index = mt }, null); errdefer d.device.freeMemory(memory, null); try d.device.bindBufferMemory(buffer, memory, 0); const mapped = try d.device.mapMemory(memory, 0, vk.WHOLE_SIZE, .{}); return .{ .buffer = buffer, .memory = memory, .size = size, .mapped = @ptrCast(mapped.?) };}
test hostBuffer { try testing_.live(); const device = try Device.init(std.testing.allocator, .{ .application_name = "aerie doctest" }); defer device.deinit(); var buffer = try device.hostBuffer(4096, .{ .vertex_buffer = true }); defer buffer.destroy(device); // Written straight through the mapping; it is coherent, so nothing // needs flushing before the GPU reads it. @memset(buffer.mapped.?[0..4096], 0);}
/// An image in device memory.pub const Image = struct { image: vk.Image = .null_handle, memory: vk.DeviceMemory = .null_handle, view: vk.ImageView = .null_handle, width: u32 = 0, height: u32 = 0,
/// Destroys the view, the image and its memory, if it has any, and /// leaves it empty, so that destroying it twice is harmless. pub fn destroy(i: *Image, d: *const Device) void { if (i.view != .null_handle) d.device.destroyImageView(i.view, null); if (i.image != .null_handle) d.device.destroyImage(i.image, null); if (i.memory != .null_handle) d.device.freeMemory(i.memory, null); i.* = .{}; }
test destroy { try testing_.live(); const device = try Device.init(std.testing.allocator, .{ .application_name = "aerie doctest" }); defer device.deinit(); var img = try device.image(16, 16, .r8g8b8a8_unorm, .{ .sampled = true, .transfer_dst = true }); img.destroy(device); try std.testing.expectEqual(vk.Image.null_handle, img.image); img.destroy(device); }};
/// A `width` by `height` 2D image of `format` for `usage`, optimally tiled/// in device-local memory where there is any, with a view of the whole of/// it. Its layout is UNDEFINED until something moves it.pub fn image(d: *const Device, width: u32, height: u32, format: vk.Format, usage: vk.ImageUsageFlags) !Image { const img = try d.device.createImage(&.{ .image_type = .@"2d", .format = format, .extent = .{ .width = width, .height = height, .depth = 1 }, .mip_levels = 1, .array_layers = 1, .samples = .{ .@"1" = true }, .tiling = .optimal, .usage = usage, .sharing_mode = .exclusive, .initial_layout = .undefined, }, null); errdefer d.device.destroyImage(img, null); const req = d.device.getImageMemoryRequirements(img); const mt = d.memoryType(req.memory_type_bits, .{ .device_local = true }) orelse d.memoryType(req.memory_type_bits, .{}) orelse return error.NoDeviceMemory; const memory = try d.device.allocateMemory(&.{ .allocation_size = req.size, .memory_type_index = mt }, null); errdefer d.device.freeMemory(memory, null); try d.device.bindImageMemory(img, memory, 0); const view = try d.device.createImageView(&.{ .image = img, .view_type = .@"2d", .format = format, .components = .{ .r = .identity, .g = .identity, .b = .identity, .a = .identity }, .subresource_range = color_range, }, null); return .{ .image = img, .memory = memory, .view = view, .width = width, .height = height };}
test image { try testing_.live(); const device = try Device.init(std.testing.allocator, .{ .application_name = "aerie doctest" }); defer device.deinit(); var img = try device.image(64, 32, .b8g8r8a8_unorm, .{ .color_attachment = true, .transfer_src = true }); defer img.destroy(device); try std.testing.expectEqual(64, img.width); try std.testing.expectEqual(32, img.height);}
/// The whole of a single-level, single-layer color image: every image here.pub const color_range: vk.ImageSubresourceRange = .{ .aspect_mask = .{ .color = true }, .base_mip_level = 0, .level_count = 1, .base_array_layer = 0, .layer_count = 1,};