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
parent
4ceb5fb9bd
commit
b8cde26c89
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue