DescriptorSetLayout.kt
src/jvmMain/kotlin/afield/graphics/vulkan/device/DescriptorSetLayout.kt · 194 lines
package afield.graphics.vulkan.device
Descriptor set layouts for the renderer's shaders: DescriptorSetLayout owns a Vulkan layout handle and releases it with its device, and DescriptorSetLayout.SimpleDescriptorSetLayout builds one from parallel lists of bindings and descriptor types.
That builder supports partially bound arrays, one variable-count (bindless) binding, and update-after-bind. UpdatesDescriptorSet is the interface a bound resource implements to push its descriptor into a command buffer.
import afield.graphics.vulkan.buffers.BufferParameters
import afield.graphics.shader.Introspected
import afield.graphics.vulkan.util.vkCheck
import afield.graphics.vulkan.util.AutoDisposer
import org.lwjgl.system.MemoryStack
import org.lwjgl.vulkan.KHRPushDescriptor
import org.lwjgl.vulkan.VK10.*
import org.lwjgl.vulkan.VK12.* // descriptor indexing enums/structs promoted to core in 1.2
import org.lwjgl.vulkan.VkDescriptorSetLayoutBinding
import org.lwjgl.vulkan.VkDescriptorSetLayoutBindingFlagsCreateInfo
import org.lwjgl.vulkan.VkDescriptorSetLayoutCreateInfo
interface UpdatesDescriptorSet
val binding: Introspected.Bindable
val source: BufferParameters?
fun pushDescriptorSet
fun pushDescriptorSet(
command: CommandBuffer,
pipelineLayout: Long,
bindPoint: Int = VK_PIPELINE_BIND_POINT_GRAPHICS
)
}
abstract class DescriptorSetLayout protected constructor(private val device: Device)
var vkDescriptorLayout: Long
var vkDescriptorLayout: Long = 0
protected set
private val autoDisposeToken = AutoDisposer.register(this, "DescriptorSetLayout", owner = device) { }
protected fun registerCleanup()
protected fun registerCleanup() {
val dev = device.vkDevice
val layout = vkDescriptorLayout
autoDisposeToken.updateCleanup { vkDestroyDescriptorSetLayout(dev, layout, null) }
}
fun cleanup()
fun cleanup() {
if (!autoDisposeToken.disable()) return
vkDestroyDescriptorSetLayout(device.vkDevice, vkDescriptorLayout, null)
vkDescriptorLayout = 0L
}
open class SimpleDescriptorSetLayout
Simple layout with optional descriptor-indexing bells & whistles.
descriptorCountis the max count per binding (for arrays). Defaults to 1s.- If you want a runtime-sized/bindless binding, set
variableBindingIndexto the index in the lists above (not the binding number). That binding must be the numerically largest Vulkan binding number in this set (spec rule). enablePartiallyBoundadds PARTIALLY_BOUND on array bindings so you don't have to fill every slot.enableUpdateAfterBindturns on UPDATE_AFTER_BIND bits and will also set the layout create flag. (Remember to also create your pool with VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT.)immutableSamplersoptionally bakes one or more immutable samplers into a binding. The map keys are Vulkan binding numbers (not indices into the lists). Only valid for SAMPLER or COMBINED_IMAGE_SAMPLER descriptor types. The array length must equal the descriptorCount for that binding.
open class SimpleDescriptorSetLayout(
val device: Device,
val descriptorType: List<Int>,
val set: Int,
val binding: List<Int>,
val stage: Int,
val descriptorCount: List<Int> = List(binding.size) { 1 },
val enablePartiallyBound: Boolean = true,
val variableBindingIndex: Int = -1,
val enableUpdateAfterBind: Boolean = false,
val immutableSamplers: Map<Int, LongArray>? = null,
val pushDescriptor: Boolean = false
) : DescriptorSetLayout(device) {
init
body — 108 lines, from line 77
init {
require(descriptorType.size == binding.size && descriptorCount.size == binding.size) {
"descriptorType, binding, descriptorCount must have same size. ${descriptorType.size} != ${binding.size} != ${descriptorCount.size}"
}
if (variableBindingIndex >= 0) {
require(variableBindingIndex in binding.indices) {
"variableBindingIndex ($variableBindingIndex) is out of range 0..${binding.lastIndex}"
}
// Spec: the variable-count binding must be the numerically highest binding number
val maxBindingNumber = binding.maxOrNull()!!
require(binding[variableBindingIndex] == maxBindingNumber) {
"Variable descriptor binding must be the numerically largest binding in the set " +
"(have=${binding[variableBindingIndex]}, max=$maxBindingNumber)"
}
}
// Validate immutable sampler usage up-front (cheap checks).
immutableSamplers?.forEach { (vkBinding, samplers) ->
val idx = binding.indexOf(vkBinding)
require(idx >= 0) {
"immutableSamplers provided for binding=$vkBinding but that binding isn't in this layout"
}
val type = descriptorType[idx]
require(type == VK_DESCRIPTOR_TYPE_SAMPLER || type == VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER) {
"immutableSamplers only valid for SAMPLER or COMBINED_IMAGE_SAMPLER (binding=$vkBinding)"
}
val declaredCount = descriptorCount[idx]
require(samplers.size == declaredCount) {
"immutableSamplers count (${samplers.size}) must match descriptorCount ($declaredCount) for binding=$vkBinding"
}
}
if (pushDescriptor) {
require(device.supportsPushDescriptors) {
"VK_KHR_push_descriptor not supported on this device"
}
val totalDescriptors = descriptorCount.sum()
require(totalDescriptors <= device.pushDescriptorMax) {
"Requested $totalDescriptors push descriptors, but device maxPushDescriptors=${device.pushDescriptorMax}"
}
}
MemoryStack.stackPush().use { stack ->
// Per-binding core info
val layoutBindings = VkDescriptorSetLayoutBinding.calloc(binding.size, stack)
for (i in binding.indices) {
layoutBindings[i]
.binding(binding[i])
.descriptorType(descriptorType[i])
.descriptorCount(descriptorCount[i])
.stageFlags(stage)
// Optional immutable samplers for this binding (if any)
immutableSamplers?.get(binding[i])?.let { samplers ->
val buf = stack.mallocLong(samplers.size)
for (j in samplers.indices) buf.put(j, samplers[j])
layoutBindings[i].pImmutableSamplers(buf)
}
}
// Optional: per-binding flags (descriptor indexing)
val hasIndexingFlags =
enablePartiallyBound || variableBindingIndex >= 0 || enableUpdateAfterBind
val flagsBuf = if (hasIndexingFlags) {
val perBindingFlags = IntArray(binding.size) { 0 }
for (i in binding.indices) {
// Partially-bound helps for arrays (including bindless) so you can leave holes.
if (enablePartiallyBound && descriptorCount[i] > 1) {
perBindingFlags[i] = perBindingFlags[i] or VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT
}
// Variable descriptor count (bindless-style).
if (i == variableBindingIndex) {
perBindingFlags[i] = perBindingFlags[i] or VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT
}
// Update-after-bind if requested globally for this layout.
if (enableUpdateAfterBind) {
perBindingFlags[i] = perBindingFlags[i] or VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT
}
}
VkDescriptorSetLayoutBindingFlagsCreateInfo.calloc(stack).apply {
sType(VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO)
pBindingFlags(stack.ints(*perBindingFlags))
}
} else null
val layoutInfo = VkDescriptorSetLayoutCreateInfo.calloc(stack)
.sType(VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO)
.pBindings(layoutBindings)
if (enableUpdateAfterBind) {
layoutInfo.flags(layoutInfo.flags() or VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT)
}
if (pushDescriptor) {
layoutInfo.flags(layoutInfo.flags() or KHRPushDescriptor.VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR)
}
if (flagsBuf != null) {
layoutInfo.pNext(flagsBuf.address())
}
val pSetLayout = stack.mallocLong(1)
vkCheck(
vkCreateDescriptorSetLayout(device.vkDevice, layoutInfo, null, pSetLayout),
"Failed to create descriptor set layout"
)
super.vkDescriptorLayout = pSetLayout[0]
registerCleanup()
}
}
override fun toString(): String
override fun toString(): String {
return "SimpleDescriptorSetLayout(set=$set, binding=$binding, types=$descriptorType, " +
"count=$descriptorCount, varIdx=$variableBindingIndex, stage=$stage, " +
"updateAfterBind=$enableUpdateAfterBind, partiallyBound=$enablePartiallyBound, " +
"pushDescriptor=$pushDescriptor)"
}
}
}