renderer/vulkan: multi-set descriptors + cell_bg pipeline

Refactor `Pipeline` and `RenderPass.step` to support pipelines that
use more than one descriptor set, then wire up cell_bg as the first
multi-set consumer.

Pipeline changes:

- `descriptor_set_layouts` is now an indexed slice of
  `?vk.VkDescriptorSetLayout` (element i == set i in the shader).
  Null entries are placeholders for unused sets; the caller passes
  `empty_set_layout` (a 0-binding DSL) to fill them. Vulkan rejects
  VK_NULL_HANDLE in `pSetLayouts`, so this is the contract that
  lets a pipeline use, e.g., sets 0 and 2 without set 1.
- One descriptor set is allocated per non-null layout entry and
  stored in `descriptor_sets[i]`. `set_count` tracks the
  one-past-the-last-used index so RenderPass can iterate without
  re-counting.
- MAX_DESCRIPTOR_SETS = 3, matching the preprocessor's UBO=0,
  sampler=1, storage=2 buckets.

RenderPass.step changes:

- Resource → (set, binding) mapping follows the preprocessor's
  scheme directly:
    `uniforms`     → set 0, binding pipeline.uniforms_binding (UBO)
    `textures[i]` + `samplers[i]`
                   → set 1, binding i (combined image sampler)
    `buffers[i]`   → set 2, binding i (storage buffer)
- Descriptor sets get bound in maximal contiguous runs (one
  `cmdBindDescriptorSets` per run). Lets cell_bg's set=0 and set=2
  bind correctly when set=1 is null.

Shaders changes:

- Build a 0-binding `empty_set_layout` once and reuse it for every
  pipeline's unused set slots.
- Track all created DSLs in a fixed-size `set_layouts` array;
  `deinit` walks it to destroy. Drops the old per-pipeline
  `bg_color_set_layout` / `bg_color_set` fields that didn't scale.
- New `createSingleBindingDsl` helper — every per-set layout we
  build today has exactly one binding (Globals UBO, bg_cells SSBO,
  individual atlas sampler).
- bg_color pipeline migrated to the new API.
- cell_bg pipeline built. Uses set 0 (UBO at binding 1) and set 2
  (bg_cells storage buffer at binding 1). Blending enabled (unlike
  bg_color) because cell_bg discards out-of-grid pixels and blends
  per-cell colors over bg_color's output.

Visual check: empty terminal still paints the configured theme
background. cell_bg runs cleanly without validation errors;
discriminating its output from bg_color's would need a populated
grid, which arrives with the cell_text pipeline.

Co-Authored-By: claude-flow <ruv@ruv.net>
pull/12846/head
Nathan 2026-05-24 14:55:25 -05:00
parent 4ceb5fb9bd
commit b8cde26c89
3 changed files with 368 additions and 120 deletions

View File

@ -54,14 +54,20 @@ pub const VertexInput = struct {
attributes: []const vk.VkVertexInputAttributeDescription,
};
/// Maximum descriptor sets a single pipeline can address. The
/// preprocessor in `shaders.zig` bins resources into 3 sets (UBO=0,
/// sampler=1, storage=2), so 3 is sufficient. Bump if/when a fourth
/// resource class is introduced.
pub const MAX_DESCRIPTOR_SETS: usize = 3;
pub const Options = struct {
device: *const Device,
/// Optional descriptor pool. If provided alongside a non-empty
/// `descriptor_set_layouts` slice, `Pipeline.init` allocates one
/// descriptor set against the first layout and stores it on
/// `Pipeline.descriptor_set` so `RenderPass.step` can bind it
/// without a separate plumbing step.
/// Optional descriptor pool. If provided, `Pipeline.init`
/// allocates one descriptor set per non-null entry in
/// `descriptor_set_layouts` and stores them on
/// `Pipeline.descriptor_sets[i]`, indexed by set number.
/// `RenderPass.step` updates + binds them per frame.
descriptor_pool: ?*DescriptorPool = null,
/// Shader modules. The caller owns these Pipeline does not
@ -73,8 +79,18 @@ pub const Options = struct {
/// Optional vertex input. `null` no vertex bindings.
vertex_input: ?VertexInput = null,
/// Descriptor set layouts referenced by the shaders.
descriptor_set_layouts: []const vk.VkDescriptorSetLayout = &.{},
/// Per-set descriptor layouts. Element i corresponds to `set = i`
/// in the shader. `null` slots are placeholders for sets the
/// pipeline doesn't actually use Vulkan requires the pipeline
/// layout's `pSetLayouts` to be contiguous up to the max used
/// set number, so we substitute `empty_set_layout` for nulls.
descriptor_set_layouts: []const ?vk.VkDescriptorSetLayout = &.{},
/// 0-binding placeholder layout used to fill `null` entries in
/// `descriptor_set_layouts`. Required when any entry is null;
/// can stay null when every entry is non-null. Owned by the
/// caller (`Shaders.init` caches one and reuses it).
empty_set_layout: vk.VkDescriptorSetLayout = null,
/// Push constant ranges referenced by the shaders.
push_constant_ranges: []const vk.VkPushConstantRange = &.{},
@ -103,38 +119,59 @@ device: *const Device,
pipeline: vk.VkPipeline,
layout: vk.VkPipelineLayout,
/// Cached copy of the single `VkDescriptorSetLayout` this pipeline
/// was built with (when one was provided). `Shaders.init` owns the
/// layout's lifetime; storing the handle here lets `RenderPass.step`
/// allocate descriptor sets matching this pipeline without threading
/// the layout separately.
descriptor_set_layout: vk.VkDescriptorSetLayout = null,
/// Descriptor sets allocated from `opts.descriptor_pool`, indexed by
/// set number. `descriptor_sets[i]` is the set bound at `set = i` in
/// the shader; `null` means the pipeline doesn't use that set (so
/// `RenderPass.step` skips updating/binding it). `set_count` is one
/// past the last non-null index, matching what
/// `vkCmdBindDescriptorSets` needs as `setCount`.
descriptor_sets: [MAX_DESCRIPTOR_SETS]vk.VkDescriptorSet = .{ null, null, null },
set_count: u32 = 0,
/// Optional descriptor set bundled with this pipeline. When set,
/// `RenderPass.step` updates it with the Step's `uniforms`/textures
/// and binds it before drawing. Allocated from a pool at
/// `Pipeline.init` time when `opts.descriptor_pool` is provided.
/// Null for pipelines that take no descriptor inputs (e.g. the
/// smoke-test's solid-color pipeline).
descriptor_set: vk.VkDescriptorSet = null,
/// Binding number that `uniforms` writes to. Defaults to 1 to match
/// the GLSL `layout(binding = 1)` on the Globals UBO. Override per
/// pipeline if/when glslang's auto-map picks a different slot.
/// Binding number that `Step.uniforms` writes to within set 0.
/// Defaults to 1 to match `common.glsl`'s
/// `layout(binding = 1, std140) uniform Globals`. Override per
/// pipeline if a different shader uses a different slot.
uniforms_binding: u32 = 1,
pub fn init(opts: Options) Error!Self {
const dev = opts.device;
if (opts.descriptor_set_layouts.len > MAX_DESCRIPTOR_SETS) {
log.err(
"Pipeline.init: {} descriptor sets exceeds MAX_DESCRIPTOR_SETS={}",
.{ opts.descriptor_set_layouts.len, MAX_DESCRIPTOR_SETS },
);
return error.VulkanFailed;
}
// ---- pipeline layout ---------------------------------------
//
// Build a flat array of VkDescriptorSetLayout where index i is
// the layout for set=i. Null entries in `opts.descriptor_set_layouts`
// get substituted with `opts.empty_set_layout` Vulkan rejects
// VK_NULL_HANDLE in `pSetLayouts`. `Shaders.init` always supplies
// an empty layout when any null appears.
var flat_dsls: [MAX_DESCRIPTOR_SETS]vk.VkDescriptorSetLayout = .{ null, null, null };
for (opts.descriptor_set_layouts, 0..) |maybe_dsl, i| {
if (maybe_dsl) |dsl| {
flat_dsls[i] = dsl;
} else if (opts.empty_set_layout != null) {
flat_dsls[i] = opts.empty_set_layout;
} else {
log.err(
"Pipeline.init: set {} is null but no empty_set_layout was provided",
.{i},
);
return error.VulkanFailed;
}
}
const layout_info: vk.VkPipelineLayoutCreateInfo = .{
.sType = vk.VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
.pNext = null,
.flags = 0,
.setLayoutCount = @intCast(opts.descriptor_set_layouts.len),
.pSetLayouts = if (opts.descriptor_set_layouts.len > 0)
opts.descriptor_set_layouts.ptr
else
null,
.pSetLayouts = if (opts.descriptor_set_layouts.len > 0) &flat_dsls else null,
.pushConstantRangeCount = @intCast(opts.push_constant_ranges.len),
.pPushConstantRanges = if (opts.push_constant_ranges.len > 0)
opts.push_constant_ranges.ptr
@ -339,16 +376,21 @@ pub fn init(opts: Options) Error!Self {
}
}
const dsl_first: vk.VkDescriptorSetLayout =
if (opts.descriptor_set_layouts.len > 0) opts.descriptor_set_layouts[0] else null;
var dset: vk.VkDescriptorSet = null;
// Allocate one descriptor set per non-null entry in
// `opts.descriptor_set_layouts`. Null entries are placeholder
// (the shader's set=i isn't actually used) nothing to allocate.
var dsets: [MAX_DESCRIPTOR_SETS]vk.VkDescriptorSet = .{ null, null, null };
if (opts.descriptor_pool) |pool_ptr| {
if (dsl_first != null) {
dset = pool_ptr.allocate(dsl_first) catch |err| {
log.err("Pipeline.init: descriptor set allocation failed: {}", .{err});
return error.VulkanFailed;
};
for (opts.descriptor_set_layouts, 0..) |maybe_dsl, i| {
if (maybe_dsl) |dsl| {
dsets[i] = pool_ptr.allocate(dsl) catch |err| {
log.err(
"Pipeline.init: descriptor set {} allocation failed: {}",
.{ i, err },
);
return error.VulkanFailed;
};
}
}
}
@ -356,8 +398,8 @@ pub fn init(opts: Options) Error!Self {
.device = dev,
.pipeline = pipeline,
.layout = layout,
.descriptor_set_layout = dsl_first,
.descriptor_set = dset,
.descriptor_sets = dsets,
.set_count = @intCast(opts.descriptor_set_layouts.len),
};
}

View File

@ -213,25 +213,35 @@ pub fn begin(opts: Options) Self {
/// Record one step of the pass.
///
/// Skips silently when the pipeline isn't yet real (`VkPipeline ==
/// null`) `Shaders.init` only constructs bg_color so far; the
/// other 4 pipeline slots are default-undefined and we filter them
/// out here rather than crashing on a null handle.
/// Updates the pipeline's descriptor sets from the Step's resources
/// and emits the draw call. Resource (set, binding) mapping
/// matches the `vulkanizeGlsl` preprocessor's bucketing scheme:
///
/// - `uniforms` set 0, binding `pipeline.uniforms_binding`
/// (UBO; the Globals block from `common.glsl`)
/// - `buffers[i]` set 2, binding `i` (storage buffer)
/// - `textures[i]` + `samplers[i]`
/// set 1, binding `i` (combined image sampler)
///
/// Skips silently when the pipeline hasn't been constructed yet
/// (`VkPipeline == null`) pipelines for shaders we haven't wired
/// up are default-null and we filter them out instead of crashing
/// on a null handle.
pub fn step(self: *Self, s: Step) void {
// Skip pipelines that haven't been constructed yet only
// `bg_color` is real today; the other 4 slots in
// `PipelineCollection` are default-initialized (VkPipeline ==
// null) and we filter them out instead of crashing on a null
// handle.
if (s.pipeline.pipeline == null) return;
if (s.draw.vertex_count == 0) return;
const dev = self.device;
// Update + bind the pipeline's descriptor set if it has one
// AND the step is passing a uniforms buffer. Today this only
// fires for the bg_color path.
if (s.pipeline.descriptor_set != null) if (s.uniforms) |ubo_buffer| {
// ---- update descriptor sets ---------------------------------
//
// We do one vkUpdateDescriptorSets call per descriptor write to
// keep the code straightforward; the total writes per frame are
// tiny (1 UBO + a handful of storage buffers + a handful of
// samplers) so batching wouldn't move the needle.
// UBO (set 0)
if (s.pipeline.descriptor_sets[0] != null) if (s.uniforms) |ubo_buffer| {
const buffer_info: vk.VkDescriptorBufferInfo = .{
.buffer = ubo_buffer,
.offset = 0,
@ -240,7 +250,7 @@ pub fn step(self: *Self, s: Step) void {
const write: vk.VkWriteDescriptorSet = .{
.sType = vk.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
.pNext = null,
.dstSet = s.pipeline.descriptor_set,
.dstSet = s.pipeline.descriptor_sets[0],
.dstBinding = s.pipeline.uniforms_binding,
.dstArrayElement = 0,
.descriptorCount = 1,
@ -250,19 +260,88 @@ pub fn step(self: *Self, s: Step) void {
.pTexelBufferView = null,
};
dev.dispatch.updateDescriptorSets(dev.device, 1, &write, 0, null);
};
var sets = [_]vk.VkDescriptorSet{s.pipeline.descriptor_set};
// Samplers (set 1)
if (s.pipeline.descriptor_sets[1] != null) {
const slot_count = @max(s.textures.len, s.samplers.len);
for (0..slot_count) |slot| {
const tex_opt: ?Texture = if (slot < s.textures.len) s.textures[slot] else null;
const samp_opt: ?Sampler = if (slot < s.samplers.len) s.samplers[slot] else null;
const tex = tex_opt orelse continue;
const samp = samp_opt orelse continue;
const image_info: vk.VkDescriptorImageInfo = .{
.sampler = samp.sampler,
.imageView = tex.view,
.imageLayout = vk.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
};
const write: vk.VkWriteDescriptorSet = .{
.sType = vk.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
.pNext = null,
.dstSet = s.pipeline.descriptor_sets[1],
.dstBinding = @intCast(slot),
.dstArrayElement = 0,
.descriptorCount = 1,
.descriptorType = vk.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
.pImageInfo = &image_info,
.pBufferInfo = null,
.pTexelBufferView = null,
};
dev.dispatch.updateDescriptorSets(dev.device, 1, &write, 0, null);
}
}
// Storage buffers (set 2)
if (s.pipeline.descriptor_sets[2] != null) {
for (s.buffers, 0..) |maybe_buf, slot| {
const buf = maybe_buf orelse continue;
const buffer_info: vk.VkDescriptorBufferInfo = .{
.buffer = buf,
.offset = 0,
.range = vk.VK_WHOLE_SIZE,
};
const write: vk.VkWriteDescriptorSet = .{
.sType = vk.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
.pNext = null,
.dstSet = s.pipeline.descriptor_sets[2],
.dstBinding = @intCast(slot),
.dstArrayElement = 0,
.descriptorCount = 1,
.descriptorType = vk.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
.pImageInfo = null,
.pBufferInfo = &buffer_info,
.pTexelBufferView = null,
};
dev.dispatch.updateDescriptorSets(dev.device, 1, &write, 0, null);
}
}
// ---- bind descriptor sets -----------------------------------
//
// `cmdBindDescriptorSets` only accepts contiguous, non-null
// handles starting at `firstSet`. To handle the cell_bg case
// (sets 0 and 2, no set 1), we make one call per maximal
// contiguous run of non-null sets.
var start: usize = 0;
while (start < s.pipeline.set_count) {
if (s.pipeline.descriptor_sets[start] == null) {
start += 1;
continue;
}
var end = start + 1;
while (end < s.pipeline.set_count and s.pipeline.descriptor_sets[end] != null) : (end += 1) {}
dev.dispatch.cmdBindDescriptorSets(
self.cb,
vk.VK_PIPELINE_BIND_POINT_GRAPHICS,
s.pipeline.layout,
0, // first set
1, // set count
&sets,
0, // dynamic offset count
@intCast(start),
@intCast(end - start),
&s.pipeline.descriptor_sets[start],
0,
null,
);
};
start = end;
}
dev.dispatch.cmdBindPipeline(
self.cb,

View File

@ -593,12 +593,20 @@ pub const Shaders = struct {
/// the next frame begins).
descriptor_pool: ?DescriptorPool = null,
/// One descriptor set + layout per pipeline. The layout is also
/// stored on `Pipeline.descriptor_set_layout` so `RenderPass.step`
/// can re-fetch from `step.pipeline`; the set lives here because
/// it's allocated once and updated per-frame.
bg_color_set_layout: vk.VkDescriptorSetLayout = null,
bg_color_set: vk.VkDescriptorSet = null,
/// Descriptor set layouts created by `init`, kept alive for the
/// lifetime of `Shaders` and destroyed in `deinit`. Each pipeline
/// holds raw `VkDescriptorSetLayout` handles into this array
/// `Shaders` owns the lifetime so individual pipelines don't have
/// to. Fixed-size because the pipeline set is small and known.
set_layouts: [16]vk.VkDescriptorSetLayout = [_]vk.VkDescriptorSetLayout{null} ** 16,
set_layouts_len: usize = 0,
/// 0-binding placeholder descriptor set layout. Vulkan requires
/// `pSetLayouts[i]` in the pipeline layout to be non-null for
/// every set up to the max used. When a pipeline uses sets 0 and
/// 2 but not 1, we substitute this layout for the set-1 slot.
/// Also tracked in `set_layouts` for deinit.
empty_set_layout: vk.VkDescriptorSetLayout = null,
defunct: bool = false,
@ -665,18 +673,68 @@ pub const Shaders = struct {
}
}
// Build a descriptor pool sized for one descriptor set per
// pipeline (we currently only construct bg_color; size for the
// full set so adding new pipelines doesn't require pool
// resizing).
// Descriptor pool. Each pipeline allocates one set per
// resource bucket it uses (UBO / sampler / storage). Size
// generously these are tiny and rebuilding the pool would
// force us to recreate all the sets too.
var pool = try DescriptorPool.init(.{
.device = device,
.max_sets = 5,
.uniform_buffers = 5,
.combined_image_samplers = 8,
.max_sets = 32,
.uniform_buffers = 16,
.combined_image_samplers = 16,
.storage_buffers = 16,
});
errdefer pool.deinit();
// ---- 0-binding placeholder DSL ---------------------------
//
// Used to fill `pSetLayouts[i]` for set indices a pipeline
// doesn't actually use (e.g. cell_bg uses set 0 and set 2,
// so set 1 needs a non-null placeholder).
const empty_dsl_info: vk.VkDescriptorSetLayoutCreateInfo = .{
.sType = vk.VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
.pNext = null,
.flags = 0,
.bindingCount = 0,
.pBindings = null,
};
var empty_dsl: vk.VkDescriptorSetLayout = undefined;
if (device.dispatch.createDescriptorSetLayout(
device.device,
&empty_dsl_info,
null,
&empty_dsl,
) != vk.VK_SUCCESS) {
return error.VulkanFailed;
}
// Layout tracker captures every DSL we create so deinit
// can tear them down without per-pipeline bookkeeping.
var set_layouts: [16]vk.VkDescriptorSetLayout = [_]vk.VkDescriptorSetLayout{null} ** 16;
var set_layouts_len: usize = 0;
set_layouts[set_layouts_len] = empty_dsl;
set_layouts_len += 1;
errdefer {
for (set_layouts[0..set_layouts_len]) |dsl| {
if (dsl != null) device.dispatch.destroyDescriptorSetLayout(
device.device,
dsl,
null,
);
}
}
// Helper: track + return.
const Tracker = struct {
arr: *[16]vk.VkDescriptorSetLayout,
len: *usize,
fn track(t: @This(), dsl: vk.VkDescriptorSetLayout) void {
t.arr.*[t.len.*] = dsl;
t.len.* += 1;
}
};
const tracker = Tracker{ .arr = &set_layouts, .len = &set_layouts_len };
// ---- bg_color pipeline -----------------------------------
//
// Full-screen fragment shader that reads the bg color out of
@ -684,90 +742,159 @@ pub const Shaders = struct {
// synthesizes a covering triangle from `gl_VertexIndex`, so
// there's no vertex input.
//
// Descriptor set layout: one UBO binding for Globals. The
// existing OpenGL shader declares it at `binding = 1`; with
// glslang's `setAutoMapBindings(true)` (in our shim) the
// binding may be remapped, but for v1 we declare it at
// binding 1 to match. Layout fragment-stage only the
// vertex shader for bg_color doesn't use the UBO.
const bg_color_bindings = [_]vk.VkDescriptorSetLayoutBinding{.{
.binding = 1,
.descriptorType = vk.VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
.descriptorCount = 1,
.stageFlags = vk.VK_SHADER_STAGE_FRAGMENT_BIT,
.pImmutableSamplers = null,
}};
const bg_color_dsl_info: vk.VkDescriptorSetLayoutCreateInfo = .{
.sType = vk.VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
.pNext = null,
.flags = 0,
.bindingCount = bg_color_bindings.len,
.pBindings = &bg_color_bindings,
};
var bg_color_dsl: vk.VkDescriptorSetLayout = undefined;
if (device.dispatch.createDescriptorSetLayout(
device.device,
&bg_color_dsl_info,
null,
&bg_color_dsl,
) != vk.VK_SUCCESS) {
return error.VulkanFailed;
}
errdefer device.dispatch.destroyDescriptorSetLayout(device.device, bg_color_dsl, null);
const bg_color_dsls = [_]vk.VkDescriptorSetLayout{bg_color_dsl};
// After `vulkanizeGlsl`, the Globals UBO lives at set=0,
// binding=1. bg_color doesn't use samplers or storage
// buffers, so the pipeline needs only one descriptor set
// layout.
const bg_color_ubo_dsl = try createSingleBindingDsl(
device,
1,
vk.VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
vk.VK_SHADER_STAGE_FRAGMENT_BIT,
);
tracker.track(bg_color_ubo_dsl);
const bg_color_pipeline = try Pipeline.init(.{
.device = device,
.descriptor_pool = &pool,
.vertex_module = modules.full_screen_vert.handle,
.fragment_module = modules.bg_color_frag.handle,
.vertex_input = null,
.descriptor_set_layouts = &bg_color_dsls,
.descriptor_set_layouts = &.{bg_color_ubo_dsl},
.empty_set_layout = empty_dsl,
.color_format = vk.VK_FORMAT_B8G8R8A8_SRGB,
.blending_enabled = false,
.topology = vk.VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
});
errdefer bg_color_pipeline.deinit();
// ---- cell_bg pipeline ------------------------------------
//
// Full-screen fragment shader that reads per-cell background
// colors out of `bg_cells` (storage buffer) and the Globals
// UBO. After `vulkanizeGlsl`:
//
// set 0 binding 1 Globals UBO (fragment stage)
// set 2 binding 1 bg_cells storage buffer (fragment stage)
//
// Set 1 is unused the empty DSL fills the slot so the
// pipeline layout's `pSetLayouts` is contiguous.
const cell_bg_ubo_dsl = try createSingleBindingDsl(
device,
1,
vk.VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
vk.VK_SHADER_STAGE_FRAGMENT_BIT,
);
tracker.track(cell_bg_ubo_dsl);
const cell_bg_storage_dsl = try createSingleBindingDsl(
device,
1,
vk.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
vk.VK_SHADER_STAGE_FRAGMENT_BIT,
);
tracker.track(cell_bg_storage_dsl);
const cell_bg_pipeline = try Pipeline.init(.{
.device = device,
.descriptor_pool = &pool,
.vertex_module = modules.full_screen_vert.handle,
.fragment_module = modules.cell_bg_frag.handle,
.vertex_input = null,
.descriptor_set_layouts = &.{ cell_bg_ubo_dsl, null, cell_bg_storage_dsl },
.empty_set_layout = empty_dsl,
.color_format = vk.VK_FORMAT_B8G8R8A8_SRGB,
.blending_enabled = true,
.topology = vk.VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
});
errdefer cell_bg_pipeline.deinit();
var pipelines: PipelineCollection = .{};
pipelines.bg_color = bg_color_pipeline;
pipelines.cell_bg = cell_bg_pipeline;
return .{
.pipelines = pipelines,
.post_pipelines = &.{},
.modules = modules,
.descriptor_pool = pool,
.bg_color_set_layout = bg_color_dsl,
.bg_color_set = bg_color_pipeline.descriptor_set,
.set_layouts = set_layouts,
.set_layouts_len = set_layouts_len,
.empty_set_layout = empty_dsl,
};
}
/// Construct a single-binding `VkDescriptorSetLayout`. The vast
/// majority of our per-set layouts have exactly one binding
/// (Globals UBO, bg_cells SSBO, individual sampler) so a helper
/// keeps the call sites short.
fn createSingleBindingDsl(
device: *const @import("Device.zig"),
binding: u32,
descriptor_type: vk.VkDescriptorType,
stage_flags: vk.VkShaderStageFlags,
) !vk.VkDescriptorSetLayout {
const bindings = [_]vk.VkDescriptorSetLayoutBinding{.{
.binding = binding,
.descriptorType = descriptor_type,
.descriptorCount = 1,
.stageFlags = stage_flags,
.pImmutableSamplers = null,
}};
const info: vk.VkDescriptorSetLayoutCreateInfo = .{
.sType = vk.VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
.pNext = null,
.flags = 0,
.bindingCount = bindings.len,
.pBindings = &bindings,
};
var dsl: vk.VkDescriptorSetLayout = undefined;
if (device.dispatch.createDescriptorSetLayout(
device.device,
&info,
null,
&dsl,
) != vk.VK_SUCCESS) {
return error.VulkanFailed;
}
return dsl;
}
pub fn deinit(self: *Shaders, alloc: Allocator) void {
_ = alloc;
if (self.defunct) return;
self.defunct = true;
// Real pipeline (bg_color) destroy first since it
// references the descriptor set layout.
const bg_color_real = self.pipelines.bg_color.pipeline != null;
if (bg_color_real) self.pipelines.bg_color.deinit();
// Pipelines first each holds a VkPipelineLayout that
// references the descriptor set layouts we're about to
// destroy. Skip default-null sentinel slots.
inline for (.{
&self.pipelines.bg_color,
&self.pipelines.bg_image,
&self.pipelines.cell_bg,
&self.pipelines.cell_text,
&self.pipelines.image,
}) |p_ptr| {
if (p_ptr.pipeline != null) p_ptr.deinit();
}
// The descriptor pool reclaims all sets allocated from it,
// including `bg_color_set`. Destroy the standalone layout
// separately.
// Descriptor pool reclaims every set allocated from it
// (including the per-pipeline sets); the standalone layouts
// are tracked separately in `set_layouts`.
if (self.descriptor_pool) |*p| p.deinit();
if (self.bg_color_set_layout != null) {
self.modules.bg_color_frag.device.dispatch.destroyDescriptorSetLayout(
self.modules.bg_color_frag.device.device,
self.bg_color_set_layout,
// Destroy every descriptor set layout we created. The empty
// placeholder is one of the entries.
const dev = self.modules.full_screen_vert.device;
for (self.set_layouts[0..self.set_layouts_len]) |dsl| {
if (dsl != null) dev.dispatch.destroyDescriptorSetLayout(
dev.device,
dsl,
null,
);
}
// Destroy every compiled module. Modules whose handle is
// null (not compiled in v1) skip destruction vkDestroy*
// is null-safe per the Vulkan spec but we check explicitly
// so we don't even pass null through the dispatch.
// null skip destruction vkDestroy* is null-safe per the
// Vulkan spec but we check explicitly so we don't pass null
// through the dispatch.
inline for (.{
&self.modules.bg_color_frag,
&self.modules.bg_image_frag,