From fafd928a80d83e87b14f97e3acdf3e2817d09b57 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 24 May 2026 09:25:31 -0500 Subject: [PATCH] renderer/vulkan: VkCommandPool wrapper + one-shot helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `vulkan/CommandPool.zig` — the missing piece between `Texture.zig` having an image handle and actually being able to upload pixels to it. Provides the `init` / `deinit` lifecycle plus a `beginOneShot` → `OneShot.endAndSubmit` helper that runs a caller-recorded command buffer to completion. Pool flags: `TRANSIENT_BIT | RESET_COMMAND_BUFFER_BIT`. The transient hint matches our usage pattern (every CB allocated here is short-lived); the reset bit lets us free individual buffers without dropping the whole pool. One-shot semantics: alloc → begin → caller records → end → submit → `vkQueueWaitIdle` → free. The wait is acceptable here because the only consumer for now is atlas / texture upload, which is rare and naturally synchronous (the renderer wants the texture populated before sampling it the next frame). Per-frame command submission will land separately with fence-based pacing — never `queueWaitIdle`. Dispatch additions: 10 new entries for the full one-shot path — `vkCreateCommandPool`, `vkDestroyCommandPool`, `vkAllocateCommandBuffers`, `vkFreeCommandBuffers`, `vkBeginCommandBuffer`, `vkEndCommandBuffer`, `vkQueueSubmit`, `vkQueueWaitIdle`, `vkCmdPipelineBarrier`, `vkCmdCopyBufferToImage`. The last two are loaded here (rather than in the upcoming texture- upload commit) because they're command-buffer-recording functions and naturally belong with the rest of the command-buffer surface. Verification: temp-switch compile-check; only the expected downstream `DerivedConfig` error surfaced. Reverted. OpenGL build still silent / clean. Co-Authored-By: claude-flow --- src/renderer/Vulkan.zig | 1 + src/renderer/vulkan/CommandPool.zig | 160 ++++++++++++++++++++++++++++ src/renderer/vulkan/Device.zig | 44 ++++++++ 3 files changed, 205 insertions(+) create mode 100644 src/renderer/vulkan/CommandPool.zig diff --git a/src/renderer/Vulkan.zig b/src/renderer/Vulkan.zig index 9728dc3ea..9e906d077 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 CommandPool = @import("vulkan/CommandPool.zig"); const bufferpkg = @import("vulkan/buffer.zig"); pub const Buffer = bufferpkg.Buffer; diff --git a/src/renderer/vulkan/CommandPool.zig b/src/renderer/vulkan/CommandPool.zig new file mode 100644 index 000000000..426336526 --- /dev/null +++ b/src/renderer/vulkan/CommandPool.zig @@ -0,0 +1,160 @@ +//! Wrapper for `VkCommandPool` with a one-shot command-buffer helper. +//! +//! Initially used by `vulkan/Texture.zig` for staging-buffer uploads: +//! allocate a transient command buffer, record an upload + layout +//! barriers, submit, wait for completion, free. +//! +//! Eventually the renderer will grow a separate per-frame command +//! pool for the main draw stream; this pool stays around for +//! infrequent operations like atlas uploads where blocking the +//! caller is fine. The choice keeps the API small and avoids the +//! complication of multi-frame fence tracking for resources that +//! will outlive the upload. + +const Self = @This(); + +const std = @import("std"); +const vk = @import("vulkan").c; + +const Device = @import("Device.zig"); + +const log = std.log.scoped(.vulkan); + +pub const Error = error{ + /// A `vkCreateCommandPool` / `vkAllocateCommandBuffers` / + /// `vkBeginCommandBuffer` / `vkEndCommandBuffer` / `vkQueueSubmit` + /// returned a non-success status. Logged with the raw `VkResult`. + VulkanFailed, +}; + +device: *const Device, +pool: vk.VkCommandPool, + +/// Create a command pool on the device's graphics queue family. The +/// pool is created with `TRANSIENT_BIT | RESET_COMMAND_BUFFER_BIT` +/// because every command buffer we allocate here is short-lived and +/// freed (or reset) immediately after submit. +pub fn init(device: *const Device) Error!Self { + const info: vk.VkCommandPoolCreateInfo = .{ + .sType = vk.VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, + .pNext = null, + .flags = vk.VK_COMMAND_POOL_CREATE_TRANSIENT_BIT | + vk.VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, + .queueFamilyIndex = device.queue_family_index, + }; + var pool: vk.VkCommandPool = undefined; + const r = device.dispatch.createCommandPool(device.device, &info, null, &pool); + if (r != vk.VK_SUCCESS) { + log.err("vkCreateCommandPool failed: result={}", .{r}); + return error.VulkanFailed; + } + return .{ .device = device, .pool = pool }; +} + +pub fn deinit(self: *Self) void { + self.device.dispatch.destroyCommandPool(self.device.device, self.pool, null); + self.* = undefined; +} + +/// A one-shot recording session. Yielded from `beginOneShot`, drives +/// `endAndSubmit` when the caller is done recording. +pub const OneShot = struct { + pool: *Self, + cb: vk.VkCommandBuffer, + + /// Record any commands directly via `cb` and the device dispatch + /// table (e.g. `pool.device.dispatch.cmdPipelineBarrier(cb, …)`). + /// Then call `endAndSubmit`. The command buffer is freed by the + /// time this returns. + pub fn endAndSubmit(self: OneShot) Error!void { + const dev = self.pool.device; + + { + const r = dev.dispatch.endCommandBuffer(self.cb); + if (r != vk.VK_SUCCESS) { + log.err("vkEndCommandBuffer failed: result={}", .{r}); + return error.VulkanFailed; + } + } + + const submit_info: vk.VkSubmitInfo = .{ + .sType = vk.VK_STRUCTURE_TYPE_SUBMIT_INFO, + .pNext = null, + .waitSemaphoreCount = 0, + .pWaitSemaphores = null, + .pWaitDstStageMask = null, + .commandBufferCount = 1, + .pCommandBuffers = &self.cb, + .signalSemaphoreCount = 0, + .pSignalSemaphores = null, + }; + { + const r = dev.dispatch.queueSubmit(dev.queue, 1, &submit_info, null); + if (r != vk.VK_SUCCESS) { + log.err("vkQueueSubmit failed: result={}", .{r}); + return error.VulkanFailed; + } + } + + // Block until the submit completes. Acceptable for one-shot + // uploads (atlas resizes are rare and the caller is willing + // to stall). Per-frame command submission will use fences + // and never queueWaitIdle. + { + const r = dev.dispatch.queueWaitIdle(dev.queue); + if (r != vk.VK_SUCCESS) { + log.err("vkQueueWaitIdle failed: result={}", .{r}); + return error.VulkanFailed; + } + } + + // Free the command buffer. The pool itself stays around so + // back-to-back uploads can reuse it without re-allocating + // VkCommandPool. + const cb_local = self.cb; + dev.dispatch.freeCommandBuffers(dev.device, self.pool.pool, 1, &cb_local); + } +}; + +/// Allocate + begin a transient command buffer for a one-shot +/// upload. Pair with `OneShot.endAndSubmit`. +pub fn beginOneShot(self: *Self) Error!OneShot { + const dev = self.device; + + const alloc_info: vk.VkCommandBufferAllocateInfo = .{ + .sType = vk.VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, + .pNext = null, + .commandPool = self.pool, + .level = vk.VK_COMMAND_BUFFER_LEVEL_PRIMARY, + .commandBufferCount = 1, + }; + var cb: vk.VkCommandBuffer = undefined; + { + const r = dev.dispatch.allocateCommandBuffers(dev.device, &alloc_info, &cb); + if (r != vk.VK_SUCCESS) { + log.err("vkAllocateCommandBuffers failed: result={}", .{r}); + return error.VulkanFailed; + } + } + errdefer dev.dispatch.freeCommandBuffers(dev.device, self.pool, 1, &cb); + + const begin_info: vk.VkCommandBufferBeginInfo = .{ + .sType = vk.VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, + .pNext = null, + .flags = vk.VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, + .pInheritanceInfo = null, + }; + { + const r = dev.dispatch.beginCommandBuffer(cb, &begin_info); + if (r != vk.VK_SUCCESS) { + log.err("vkBeginCommandBuffer failed: result={}", .{r}); + return error.VulkanFailed; + } + } + + return .{ .pool = self, .cb = cb }; +} + +test { + std.testing.refAllDecls(@This()); +} diff --git a/src/renderer/vulkan/Device.zig b/src/renderer/vulkan/Device.zig index 41fd201a9..108059d2b 100644 --- a/src/renderer/vulkan/Device.zig +++ b/src/renderer/vulkan/Device.zig @@ -109,6 +109,20 @@ pub const Dispatch = struct { bindBufferMemory: std.meta.Child(vk.PFN_vkBindBufferMemory), mapMemory: std.meta.Child(vk.PFN_vkMapMemory), unmapMemory: std.meta.Child(vk.PFN_vkUnmapMemory), + + // Command pool / buffer + queue submit + recording — + // used by `vulkan/CommandPool.zig` and (later) per-frame command + // recording in `vulkan/Frame.zig`. + createCommandPool: std.meta.Child(vk.PFN_vkCreateCommandPool), + destroyCommandPool: std.meta.Child(vk.PFN_vkDestroyCommandPool), + allocateCommandBuffers: std.meta.Child(vk.PFN_vkAllocateCommandBuffers), + freeCommandBuffers: std.meta.Child(vk.PFN_vkFreeCommandBuffers), + beginCommandBuffer: std.meta.Child(vk.PFN_vkBeginCommandBuffer), + endCommandBuffer: std.meta.Child(vk.PFN_vkEndCommandBuffer), + queueSubmit: std.meta.Child(vk.PFN_vkQueueSubmit), + queueWaitIdle: std.meta.Child(vk.PFN_vkQueueWaitIdle), + cmdPipelineBarrier: std.meta.Child(vk.PFN_vkCmdPipelineBarrier), + cmdCopyBufferToImage: std.meta.Child(vk.PFN_vkCmdCopyBufferToImage), }; // ---- fields --------------------------------------------------------- @@ -301,6 +315,26 @@ pub fn init( try dl.load(vk.PFN_vkMapMemory, "vkMapMemory"); const unmap_memory = try dl.load(vk.PFN_vkUnmapMemory, "vkUnmapMemory"); + const create_command_pool = + try dl.load(vk.PFN_vkCreateCommandPool, "vkCreateCommandPool"); + const destroy_command_pool = + try dl.load(vk.PFN_vkDestroyCommandPool, "vkDestroyCommandPool"); + const allocate_command_buffers = + try dl.load(vk.PFN_vkAllocateCommandBuffers, "vkAllocateCommandBuffers"); + const free_command_buffers = + try dl.load(vk.PFN_vkFreeCommandBuffers, "vkFreeCommandBuffers"); + const begin_command_buffer = + try dl.load(vk.PFN_vkBeginCommandBuffer, "vkBeginCommandBuffer"); + const end_command_buffer = + try dl.load(vk.PFN_vkEndCommandBuffer, "vkEndCommandBuffer"); + const queue_submit = + try dl.load(vk.PFN_vkQueueSubmit, "vkQueueSubmit"); + const queue_wait_idle = + try dl.load(vk.PFN_vkQueueWaitIdle, "vkQueueWaitIdle"); + const cmd_pipeline_barrier = + try dl.load(vk.PFN_vkCmdPipelineBarrier, "vkCmdPipelineBarrier"); + const cmd_copy_buffer_to_image = + try dl.load(vk.PFN_vkCmdCopyBufferToImage, "vkCmdCopyBufferToImage"); return .{ .platform = platform, @@ -333,6 +367,16 @@ pub fn init( .bindBufferMemory = bind_buffer_memory, .mapMemory = map_memory, .unmapMemory = unmap_memory, + .createCommandPool = create_command_pool, + .destroyCommandPool = destroy_command_pool, + .allocateCommandBuffers = allocate_command_buffers, + .freeCommandBuffers = free_command_buffers, + .beginCommandBuffer = begin_command_buffer, + .endCommandBuffer = end_command_buffer, + .queueSubmit = queue_submit, + .queueWaitIdle = queue_wait_idle, + .cmdPipelineBarrier = cmd_pipeline_barrier, + .cmdCopyBufferToImage = cmd_copy_buffer_to_image, }, }; }