diff --git a/src/renderer/Vulkan.zig b/src/renderer/Vulkan.zig index e682de91c..9728dc3ea 100644 --- a/src/renderer/Vulkan.zig +++ b/src/renderer/Vulkan.zig @@ -66,3 +66,6 @@ pub const Device = @import("vulkan/Device.zig"); pub const Sampler = @import("vulkan/Sampler.zig"); pub const Texture = @import("vulkan/Texture.zig"); + +const bufferpkg = @import("vulkan/buffer.zig"); +pub const Buffer = bufferpkg.Buffer; diff --git a/src/renderer/vulkan/Device.zig b/src/renderer/vulkan/Device.zig index 579010291..41fd201a9 100644 --- a/src/renderer/vulkan/Device.zig +++ b/src/renderer/vulkan/Device.zig @@ -100,6 +100,15 @@ pub const Dispatch = struct { bindImageMemory: std.meta.Child(vk.PFN_vkBindImageMemory), createImageView: std.meta.Child(vk.PFN_vkCreateImageView), destroyImageView: std.meta.Child(vk.PFN_vkDestroyImageView), + + // Buffer (host-visible vertex / uniform / cell-data storage) — + // used by `vulkan/buffer.zig`. + createBuffer: std.meta.Child(vk.PFN_vkCreateBuffer), + destroyBuffer: std.meta.Child(vk.PFN_vkDestroyBuffer), + getBufferMemoryRequirements: std.meta.Child(vk.PFN_vkGetBufferMemoryRequirements), + bindBufferMemory: std.meta.Child(vk.PFN_vkBindBufferMemory), + mapMemory: std.meta.Child(vk.PFN_vkMapMemory), + unmapMemory: std.meta.Child(vk.PFN_vkUnmapMemory), }; // ---- fields --------------------------------------------------------- @@ -280,6 +289,18 @@ pub fn init( try dl.load(vk.PFN_vkCreateImageView, "vkCreateImageView"); const destroy_image_view = try dl.load(vk.PFN_vkDestroyImageView, "vkDestroyImageView"); + const create_buffer = + try dl.load(vk.PFN_vkCreateBuffer, "vkCreateBuffer"); + const destroy_buffer = + try dl.load(vk.PFN_vkDestroyBuffer, "vkDestroyBuffer"); + const get_buffer_memory_requirements = + try dl.load(vk.PFN_vkGetBufferMemoryRequirements, "vkGetBufferMemoryRequirements"); + const bind_buffer_memory = + try dl.load(vk.PFN_vkBindBufferMemory, "vkBindBufferMemory"); + const map_memory = + try dl.load(vk.PFN_vkMapMemory, "vkMapMemory"); + const unmap_memory = + try dl.load(vk.PFN_vkUnmapMemory, "vkUnmapMemory"); return .{ .platform = platform, @@ -306,6 +327,12 @@ pub fn init( .bindImageMemory = bind_image_memory, .createImageView = create_image_view, .destroyImageView = destroy_image_view, + .createBuffer = create_buffer, + .destroyBuffer = destroy_buffer, + .getBufferMemoryRequirements = get_buffer_memory_requirements, + .bindBufferMemory = bind_buffer_memory, + .mapMemory = map_memory, + .unmapMemory = unmap_memory, }, }; } diff --git a/src/renderer/vulkan/buffer.zig b/src/renderer/vulkan/buffer.zig new file mode 100644 index 000000000..8a3cbaa40 --- /dev/null +++ b/src/renderer/vulkan/buffer.zig @@ -0,0 +1,244 @@ +//! Host-coherent `VkBuffer` wrapper, generic over element type. +//! +//! Mirrors `src/renderer/opengl/buffer.zig`: `Buffer(T)` returns a +//! struct that holds one buffer's worth of `T`s, with init / initFill +//! / sync / syncFromArrayLists semantics that match the OpenGL +//! contract. +//! +//! Storage strategy: `HOST_VISIBLE | HOST_COHERENT` memory. +//! - HOST_VISIBLE lets us `vkMapMemory` the buffer and write directly. +//! - HOST_COHERENT means the writes are visible to the GPU without a +//! `vkFlushMappedMemoryRanges` round-trip. +//! - This is the simplest "dynamic" buffer pattern in Vulkan. It does +//! pay a small cost over device-local + staging on discrete GPUs, +//! but the renderer's per-frame buffer payloads are KBs (cell +//! instances + uniforms), not bandwidth-bound. The OpenGL backend +//! uses `dynamic_draw` for the same buffers, which behaves +//! similarly on most drivers. +//! +//! Growth policy: matches the OpenGL backend — `sync` doubles the +//! buffer when content outgrows it, with no shrink. The buffer is +//! recreated (destroy/create) on growth because Vulkan buffers are +//! immutable in size. + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const vk = @import("vulkan").c; + +const Device = @import("Device.zig"); + +const log = std.log.scoped(.vulkan); + +/// Buffer construction parameters. The OpenGL backend's `target` / +/// `usage` enums don't map to Vulkan — `target` (vertex vs element +/// binding point) is replaced by descriptor binding at draw time, and +/// `usage` (static_draw / dynamic_draw / etc.) is implicit in our +/// host-coherent allocation strategy. What's left is the Vulkan +/// `VkBufferUsageFlags` bitmask, which the renderer's `api.*BufferOptions` +/// methods will return differently per buffer kind (VERTEX_BUFFER_BIT +/// for instance buffers, UNIFORM_BUFFER_BIT for uniforms, etc.). +pub const Options = struct { + device: *const Device, + /// `VkBufferUsageFlagBits` for the buffer. + usage: vk.VkBufferUsageFlags, +}; + +pub const Error = error{ + /// A `vkCreate*` / `vkAllocateMemory` / `vkBindBufferMemory` / + /// `vkMapMemory` returned a non-success status. + VulkanFailed, + /// `Device.findMemoryType` couldn't find a `HOST_VISIBLE | HOST_COHERENT` + /// memory type matching the buffer's requirements. Unlikely on any + /// real driver but worth flagging distinctly. + NoSuitableMemoryType, +}; + +/// `Buffer(T)`: a `VkBuffer` + backing `VkDeviceMemory` typed to hold +/// some number of `T`s. Mirrors `opengl/buffer.zig`'s `Buffer(T)` so +/// the renderer's call sites don't need a per-backend branch. +pub fn Buffer(comptime T: type) type { + return struct { + const Self = @This(); + + /// Underlying `VkBuffer` handle. + buffer: vk.VkBuffer, + /// Backing memory. Host-coherent; mappable directly. + memory: vk.VkDeviceMemory, + /// Options this buffer was allocated with. + opts: Options, + /// Current capacity, in number of `T`s. + len: usize, + + /// Initialize a buffer with capacity for `len` `T`s. Contents + /// are uninitialized; call `sync` to populate. + pub fn init(opts: Options, len: usize) Error!Self { + return try create(opts, len); + } + + /// Initialize a buffer pre-filled with the provided data. + pub fn initFill(opts: Options, data: []const T) Error!Self { + var self = try create(opts, data.len); + errdefer self.deinit(); + try self.write(0, data); + return self; + } + + pub fn deinit(self: Self) void { + const dev = self.opts.device; + dev.dispatch.destroyBuffer(dev.device, self.buffer, null); + dev.dispatch.freeMemory(dev.device, self.memory, null); + } + + /// Replace the buffer's contents. Grows (doubles) if needed — + /// matches the OpenGL backend's behavior. Data shorter than + /// the current capacity leaves the trailing slots untouched. + pub fn sync(self: *Self, data: []const T) Error!void { + if (data.len > self.len) try self.grow(data.len * 2); + try self.write(0, data); + } + + /// Like `sync` but pulls from multiple `ArrayList`s in + /// sequence; returns the total number of elements written. + pub fn syncFromArrayLists( + self: *Self, + lists: []const std.ArrayListUnmanaged(T), + ) Error!usize { + var total: usize = 0; + for (lists) |list| total += list.items.len; + + if (total > self.len) try self.grow(total * 2); + + var off: usize = 0; + for (lists) |list| { + if (list.items.len == 0) continue; + try self.write(off, list.items); + off += list.items.len; + } + return total; + } + + // ---- internals ------------------------------------------- + + fn create(opts: Options, len: usize) Error!Self { + const dev = opts.device; + // Vulkan requires `size > 0` for buffer creation. Round up + // a zero request to 1 so the buffer exists and can be + // grown later via `sync`. (OpenGL silently accepts size=0.) + const byte_size: u64 = @max(1, len * @sizeOf(T)); + + const info: vk.VkBufferCreateInfo = .{ + .sType = vk.VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + .pNext = null, + .flags = 0, + .size = byte_size, + .usage = opts.usage, + .sharingMode = vk.VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = null, + }; + var buffer: vk.VkBuffer = undefined; + { + const r = dev.dispatch.createBuffer(dev.device, &info, null, &buffer); + if (r != vk.VK_SUCCESS) { + log.err("vkCreateBuffer failed: result={}", .{r}); + return error.VulkanFailed; + } + } + errdefer dev.dispatch.destroyBuffer(dev.device, buffer, null); + + var reqs: vk.VkMemoryRequirements = undefined; + dev.dispatch.getBufferMemoryRequirements(dev.device, buffer, &reqs); + + const type_index = dev.findMemoryType( + reqs.memoryTypeBits, + vk.VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | + vk.VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, + ) orelse { + log.err( + "no HOST_VISIBLE|HOST_COHERENT memory type for buffer (typeBits=0x{x})", + .{reqs.memoryTypeBits}, + ); + return error.NoSuitableMemoryType; + }; + + const alloc_info: vk.VkMemoryAllocateInfo = .{ + .sType = vk.VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .pNext = null, + .allocationSize = reqs.size, + .memoryTypeIndex = 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 (buffer) failed: result={}", .{r}); + return error.VulkanFailed; + } + } + errdefer dev.dispatch.freeMemory(dev.device, memory, null); + + { + const r = dev.dispatch.bindBufferMemory(dev.device, buffer, memory, 0); + if (r != vk.VK_SUCCESS) { + log.err("vkBindBufferMemory failed: result={}", .{r}); + return error.VulkanFailed; + } + } + + return .{ + .buffer = buffer, + .memory = memory, + .opts = opts, + .len = len, + }; + } + + /// Grow the buffer to hold at least `new_len` Ts. Destroys + /// and recreates the underlying VkBuffer (Vulkan buffers are + /// immutable in size). Contents are discarded — callers + /// always `sync` immediately after `grow` returns. + fn grow(self: *Self, new_len: usize) Error!void { + const dev = self.opts.device; + dev.dispatch.destroyBuffer(dev.device, self.buffer, null); + dev.dispatch.freeMemory(dev.device, self.memory, null); + const replacement = try create(self.opts, new_len); + self.* = replacement; + } + + /// Copy `data` into the buffer starting at element offset + /// `elem_off`. Host-coherent memory means the GPU sees the + /// writes without an explicit flush. + fn write(self: *const Self, elem_off: usize, data: []const T) Error!void { + if (data.len == 0) return; + const dev = self.opts.device; + const byte_off: u64 = elem_off * @sizeOf(T); + const byte_size: u64 = data.len * @sizeOf(T); + var mapped: ?*anyopaque = null; + { + const r = dev.dispatch.mapMemory( + dev.device, + self.memory, + byte_off, + byte_size, + 0, + &mapped, + ); + if (r != vk.VK_SUCCESS) { + log.err("vkMapMemory failed: result={}", .{r}); + return error.VulkanFailed; + } + } + defer dev.dispatch.unmapMemory(dev.device, self.memory); + + const dst: [*]u8 = @ptrCast(mapped.?); + const src: [*]const u8 = @ptrCast(data.ptr); + @memcpy(dst[0..byte_size], src[0..byte_size]); + } + }; +} + +test { + // Exercise top-level decls of a representative instantiation so + // type errors in the generic body surface during compile-check. + std.testing.refAllDecls(Buffer(u32)); +}