MeshModule.kt
src/jvmMain/kotlin/afield/graphics/render/MeshModule.kt · 839 lines
package afield.graphics.render
import afield.app.diagnostics.LatencySpans
import afield.graphics.scene.main
import afield.graphics.shader.Introspection
import afield.graphics.shader.ShaderKind
import afield.graphics.shader.shaderInterfaceTransition
import afield.app.diagnostics.Named
import afield.graphics.vulkan.Target
import afield.graphics.vulkan.buffers.MissingRefException
import afield.graphics.vulkan.buffers.Structure
import afield.graphics.vulkan.buffers.buffer
import afield.graphics.vulkan.device.*
import afield.graphics.vulkan.device.DescriptorSetLayout.SimpleDescriptorSetLayout
import afield.graphics.vulkan.util.AutoDisposer
import afield.graphics.vulkan.util.FIELD_PROBE_BUFFER_NAME
import afield.graphics.vulkan.util.vkCheck
import afield.app.diagnostics.logging.lprintln
import afield.math.linalg.f
import afield.math.linalg.Rectanglef
import org.lwjgl.system.MemoryStack
import org.lwjgl.vulkan.*
import org.lwjgl.vulkan.EXTMeshShader.VK_SHADER_STAGE_MESH_BIT_EXT
import org.lwjgl.vulkan.EXTMeshShader.VK_SHADER_STAGE_TASK_BIT_EXT
import org.lwjgl.vulkan.VK13.*
import java.nio.ByteBuffer
A task/mesh-shader pipeline as a renderable module: MeshModule compiles and introspects task and mesh shaders and owns their descriptor sets.
Boxes hold it through a MeshModulePointer, which swaps in a rebuilt module when a shader is reloaded and carries live values across the interface change.
class MeshModulePointer(var current: MeshModule) : Renderable
class MeshModulePointer(var current: MeshModule) : Renderable {
var lastReloadReport: ModuleReloadReport?
What the last successful reload did to the interface and which values it could not carry.
var lastReloadReport: ModuleReloadReport? = null
internal set
fun prepareSampledImagesFor(command: CommandBuffer)
fun prepareSampledImagesFor(command: CommandBuffer) {
if (current.valid && current.enabled) {
prepareSampledImagesForUniforms(current.uniforms.values, command)
}
}
override fun render(command: CommandBuffer, bounds: Rectanglef)
override fun render(command: CommandBuffer, bounds: Rectanglef) {
if (current.valid && current.enabled) current.render(command, bounds)
else lprintln("warning,graphics", "module '${current.parameters.name}' not valid / not rendering")
}
fun reload
body — 57 lines, from line 54
fun reload(
p: Parameters,
forceReresolve: Boolean = true,
onError: (String, Throwable?) -> Unit = p.errorHandler
): Boolean {
if (current.valid && p == current.parameters && !forceReresolve) return false
p.errorHandler = onError
val nt = p.taskIntrospection
val nm = p.meshIntrospection
val nf = p.fragmentIntrospection
val before = if (nt != null && nm != null && nf != null) runCatching {
shaderInterfaceTransition(current.stages(), listOf("task" to nt, "mesh" to nm, "fragment" to nf))
}.getOrNull() else null
p.uboResolver = reloadWrap(p.uboResolver) { oUboResolver, it ->
ReloadCarryOver.ubo(current.uniforms, before, it, oUboResolver)
}
p.ssboResolver = reloadWrap(p.ssboResolver) { oSsboResolver, it ->
val prev = current.uniforms[it.name]
if (prev == null) oSsboResolver(it) else {
val prevSrc = prev.source
if (prevSrc == null) oSsboResolver(it)
else if (prevSrc.checkCompatibility(it) == false) oSsboResolver(it)
else oSsboResolver(prevSrc)
}
}
p.textureResolver = reloadWrap(p.textureResolver) { oTextureResolver, it ->
if (forceReresolve) oTextureResolver(it) else try {
val tp = current.uniforms[it.name] as Module.TexturePointer
tp.texture to tp.sampler
} catch (_: Exception) {
lprintln("warning,graphics", "couldn't transfer over texture ${it.name}, falling back to originating resolution")
oTextureResolver(it)
}
}
val m = MeshModule(p)
if (m.valid) {
val previous = current
m.copyMeshesFrom(previous)
m.enabled = previous.enabled
val transition = before ?: runCatching { shaderInterfaceTransition(previous.stages(), m.stages()) }.getOrNull()
lastReloadReport = ModuleReloadReport(
transition,
ReloadCarryOver.finishAndNotify(this, transition, previous.uniforms.values, m.uniforms, previous.liveUniformStructures, m.liveUniformStructures)
)
current = m
// Retired through the renderer's deferred queue, as in ModulePointer.reload.
previous.cleanup()
return true
}
m.cleanup()
return false
}
operator fun plusAssign(m: Pair<String, MeshMesh>)
operator fun plusAssign(m: Pair<String, MeshMesh>) {
current.meshes[m.first] = m.second
m.second.inside = this
}
operator fun plusAssign(m: Named<MeshMesh>)
operator fun plusAssign(m: Named<MeshMesh>) {
current.meshes[m.name] = m.value
m.value.inside = this
}
operator fun plusAssign(m: Pair<String, Target>)
@JvmName("plusAssign_target")
operator fun plusAssign(m: Pair<String, Target>) {
current.after[m.first] = m.second
}
operator fun plusAssign(m: Pair<String, BeforeTarget>)
@JvmName("plusAssign_beforeTarget")
operator fun plusAssign(m: Pair<String, BeforeTarget>) {
current.before[m.first] = m.second.target
}
fun enable()
fun enable() {
current.enable()
}
fun disable()
fun disable() {
current.disable()
}
override fun toString(): String = if (!current.valid) "Invalid Module" else "Module"
fun MeshModule.pointer() = MeshModulePointer(this)
class MeshModule(val parameters: Parameters) : Renderable
class MeshModule(val parameters: Parameters) : Renderable {
var valid
Volatile: cleanup() clears it on a box thread while render workers read it.
@Volatile
var valid = false
var enabled = true
var meshes = linkedMapOf<String, MeshMesh>()
var before = linkedMapOf<String, Target>()
var after = linkedMapOf<String, Target>()
val ti
val ti = parameters.taskIntrospection
?: Introspection(
parameters.task,
ShaderKind.TASK_SHADER,
errorHandler = parameters.errorHandler,
sourceName = parameters.taskSourceName,
includeResolver = parameters.taskIncludeResolver
)
val mi
val mi = parameters.meshIntrospection
?: Introspection(
parameters.mesh,
ShaderKind.MESH_SHADER,
errorHandler = parameters.errorHandler,
sourceName = parameters.meshSourceName,
includeResolver = parameters.meshIncludeResolver
)
val fi
val fi = parameters.fragmentIntrospection
?: Introspection(
parameters.fragment,
ShaderKind.FRAGMENT_SHADER,
errorHandler = parameters.errorHandler,
sourceName = parameters.fragmentSourceName,
includeResolver = parameters.fragmentIncludeResolver
)
internal fun stages(): List<Pair<String, Introspection>>
The module's stages, named, for shaderInterfaceTransition.
internal fun stages(): List<Pair<String, Introspection>> = listOf("task" to ti, "mesh" to mi, "fragment" to fi)
val pushConstantSize
val pushConstantSize = (ti.pushConstants + mi.pushConstants + fi.pushConstants).maxOfOrNull { it.size } ?: 0
var uniforms: MutableMap<String, UpdatesDescriptorSet> = mutableMapOf()
var liveUniformStructures: MutableMap<String, Structure> = mutableMapOf()
Push descriptors still reference these layouts while the pipeline is in use.
private val ownedDescriptorLayouts = mutableListOf<DescriptorSetLayout.SimpleDescriptorSetLayout>()
var pipelineLayout: Long = 0L
var pipeline: Long = 0L
private val pipelineDisposeToken = AutoDisposer.register(this, "MeshPipeline(${parameters.name})", owner = parameters.renderer.device) { }
var tsm: Long = 0L
var msm: Long = 0L
var fsm: Long = 0L
init
body — 476 lines, from line 198
init {
try {
if (!ti.valid || !mi.valid || !fi.valid) throw IllegalStateException("Can't make a module without valid programs")
tsm = createShaderModule(ti.code.bytecode()!!)
msm = createShaderModule(mi.code.bytecode()!!)
fsm = createShaderModule(fi.code.bytecode()!!)
parameters.renderer.device.nameObject(tsm, VK10.VK_OBJECT_TYPE_SHADER_MODULE, "${parameters.name} TS")
parameters.renderer.device.nameObject(msm, VK10.VK_OBJECT_TYPE_SHADER_MODULE, "${parameters.name} MS")
parameters.renderer.device.nameObject(fsm, VK10.VK_OBJECT_TYPE_SHADER_MODULE, "${parameters.name} FS")
// Collect sets -> bindings (may contain dups from multiple stages)
val setsNeededRaw = mutableMapOf<Int, MutableList<Int>>()
ti.ubos.forEach { setsNeededRaw.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }
mi.ubos.forEach { setsNeededRaw.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }
fi.ubos.forEach { setsNeededRaw.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }
ti.ssbo.forEach { setsNeededRaw.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }
mi.ssbo.forEach { setsNeededRaw.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }
fi.ssbo.forEach { setsNeededRaw.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }
ti.textures.forEach { setsNeededRaw.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }
mi.textures.forEach { setsNeededRaw.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }
fi.textures.forEach { setsNeededRaw.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }
ti.images.forEach { setsNeededRaw.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }
mi.images.forEach { setsNeededRaw.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }
fi.images.forEach { setsNeededRaw.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }
// (2) Dedup + sort per-set binding lists so DescriptorSetLayout is deterministic & gap-free
val setsNeeded = setsNeededRaw.mapValues { (_, v) -> v.distinct().sorted().toMutableList() }
uniforms = mutableMapOf()
liveUniformStructures = mutableMapOf()
ti.ubos.forEach { liveUniformStructures[it.name] = Structure().setMembers(it.members).reportingTo(parameters) }
mi.ubos.forEach { liveUniformStructures[it.name] = Structure().setMembers(it.members).reportingTo(parameters) }
fi.ubos.forEach { liveUniformStructures[it.name] = Structure().setMembers(it.members).reportingTo(parameters) }
ti.ssbo.forEach { liveUniformStructures[it.name] = Structure().setMembers(it.members).reportingTo(parameters) }
mi.ssbo.forEach { liveUniformStructures[it.name] = Structure().setMembers(it.members).reportingTo(parameters) }
fi.ssbo.forEach { liveUniformStructures[it.name] = Structure().setMembers(it.members).reportingTo(parameters) }
ModuleRebuild.created(liveUniformStructures)
val layouts = mutableMapOf<Int, SimpleDescriptorSetLayout>()
// Build layouts for all sets used by the shader
setsNeeded.entries.forEach { (set, bindingList) ->
var where = 0
val kinds = mutableListOf<Int>()
val lens = mutableListOf<Int>()
bindingList.forEach { bind ->
var kind = 0
var descriptorCount = 1
fun noteBinding(stageFlag: Int, descriptorType: Int, count: Int = 1) {
where = where or stageFlag
if (kind == 0) kind = descriptorType
else require(kind == descriptorType) {
"binding $bind in set $set resolves to multiple descriptor types"
}
descriptorCount = maxOf(descriptorCount, count)
}
ti.ubos.forEach {
if (it.set == set && it.binding == bind) {
noteBinding(VK_SHADER_STAGE_TASK_BIT_EXT, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER)
}
}
mi.ubos.forEach {
if (it.set == set && it.binding == bind) {
noteBinding(VK_SHADER_STAGE_MESH_BIT_EXT, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER)
}
}
fi.ubos.forEach {
if (it.set == set && it.binding == bind) {
noteBinding(VK_SHADER_STAGE_FRAGMENT_BIT, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER)
}
}
ti.ssbo.forEach {
if (it.set == set && it.binding == bind) {
noteBinding(VK_SHADER_STAGE_TASK_BIT_EXT, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER)
}
}
mi.ssbo.forEach {
if (it.set == set && it.binding == bind) {
noteBinding(VK_SHADER_STAGE_MESH_BIT_EXT, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER)
}
}
fi.ssbo.forEach {
if (it.set == set && it.binding == bind) {
noteBinding(VK_SHADER_STAGE_FRAGMENT_BIT, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER)
}
}
// (1) Match on binding for textures/images too
ti.textures.forEach {
if (it.set == set && it.binding == bind) {
noteBinding(
VK_SHADER_STAGE_TASK_BIT_EXT,
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
maxOf(1, it.arrayCount)
)
}
}
mi.textures.forEach {
if (it.set == set && it.binding == bind) {
noteBinding(
VK_SHADER_STAGE_MESH_BIT_EXT,
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
maxOf(1, it.arrayCount)
)
}
}
fi.textures.forEach {
if (it.set == set && it.binding == bind) {
noteBinding(
VK_SHADER_STAGE_FRAGMENT_BIT,
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
maxOf(1, it.arrayCount)
)
}
}
ti.images.forEach {
if (it.set == set && it.binding == bind) {
noteBinding(VK_SHADER_STAGE_TASK_BIT_EXT, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE)
}
}
mi.images.forEach {
if (it.set == set && it.binding == bind) {
noteBinding(VK_SHADER_STAGE_MESH_BIT_EXT, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE)
}
}
fi.images.forEach {
if (it.set == set && it.binding == bind) {
noteBinding(VK_SHADER_STAGE_FRAGMENT_BIT, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE)
}
}
if (kind == 0) throw IllegalArgumentException(" couldn't find $bind for $bindingList")
kinds.add(kind)
lens.add(descriptorCount)
}
val layout = DescriptorSetLayout.SimpleDescriptorSetLayout(
parameters.renderer.device, kinds, set, bindingList, where, lens, pushDescriptor = true
)
layouts[set] = layout
ownedDescriptorLayouts += layout
}
// UBOs (sets >= 1)
setsNeeded.entries.forEach { (set, _) ->
var found = false
ti.ubos.forEach { if (it.set == set) {
found = true
val sz = it.size.toLong()
val bp = buffer(it, it.name) { size = sz; source = it; uniform(); hosted() }
val buf = ReloadCarryOver.resolve(bp, parameters.uboResolver) ?: throw IllegalArgumentException("can't link UBO ${it.name}")
uniforms[it.name] = Module.Uniform(it.name, it, source = buf, buf.toBuffer(parameters.renderer), VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER)
} }
mi.ubos.forEach { if (it.set == set) {
found = true
val sz = it.size.toLong()
val bp = buffer(it, it.name) { size = sz; source = it; uniform(); hosted() }
val buf = ReloadCarryOver.resolve(bp, parameters.uboResolver) ?: throw IllegalArgumentException("can't link UBO ${it.name}")
uniforms[it.name] = Module.Uniform(it.name, it, source = buf, buf.toBuffer(parameters.renderer), VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER)
} }
fi.ubos.forEach { if (it.set == set) {
found = true
val sz = it.size.toLong()
val bp = buffer(it, it.name) { size = sz; source = it; uniform(); hosted() }
val buf = ReloadCarryOver.resolve(bp, parameters.uboResolver) ?: throw IllegalArgumentException("can't link UBO ${it.name}")
uniforms[it.name] = Module.Uniform(it.name, it, source = buf, buf.toBuffer(parameters.renderer), VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER)
} }
if (!found) return@forEach
}
// SSBOs (sets >= 1)
setsNeeded.entries.forEach { (set, _) ->
var found = false
ti.ssbo.forEach { if (it.set == set) {
found = true
var sz = it.size.toLong()
if (sz == 0L) sz = parameters.ssboSizer(it)?.toLong() ?: 0L
val bp = buffer(it, it.name) { size = sz; source = it; storage(); hosted() }
val buf = ReloadCarryOver.resolve(bp, parameters.ssboResolver) ?: throw IllegalArgumentException("can't link SSBO buffer ${it.name}, resolver returned null")
uniforms[it.name] = Module.Uniform(it.name, it, source = buf, buf.buffer, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER)
} }
mi.ssbo.forEach { if (it.set == set) {
found = true
var sz = it.size.toLong()
if (sz == 0L) sz = parameters.ssboSizer(it)?.toLong() ?: 0L
val bp = buffer(it, it.name) { size = sz; source = it; storage(); hosted() }
val buf = ReloadCarryOver.resolve(bp, parameters.ssboResolver) ?: throw IllegalArgumentException("can't link SSBO buffer ${it.name}, resolver returned null")
uniforms[it.name] = Module.Uniform(it.name, it, source = buf, buf.buffer, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER)
} }
fi.ssbo.forEach { if (it.set == set) {
found = true
var sz = it.size.toLong()
if (sz == 0L) sz = parameters.ssboSizer(it)?.toLong() ?: 0L
val bp = buffer(it, it.name) { size = sz; source = it; storage(); hosted() }
val buf = ReloadCarryOver.resolve(bp, parameters.ssboResolver) ?: throw IllegalArgumentException("can't link SSBO buffer ${it.name}, resolver returned null")
uniforms[it.name] = Module.Uniform(it.name, it, source = buf, buf.buffer, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER)
} }
if (!found) return@forEach
}
// Sampled textures (sets >= 1)
setsNeeded.entries.forEach { (set, _) ->
var found = false
ti.textures.forEach { if (it.set == set) {
found = true
val (texture, sampler) = parameters.textureResolver(it)!!
val views = texture.imageViewArray
require(views.isNotEmpty()) { "Texture ${it.name} resolved to empty image view array" }
if (it.arrayCount == 0) {
uniforms[it.name] = Module.TexturePointer(it.name, it, source = null, texture, sampler)
} else {
require(views.size == it.arrayCount) {
"can't link texture resource ${it.name} of length ${views.size} but the shader is expecting ${it.arrayCount}"
}
uniforms[it.name] = Module.TextureArrayPointer(it.name, it, source = null, texture, sampler)
}
} }
mi.textures.forEach { if (it.set == set) {
found = true
val (texture, sampler) = parameters.textureResolver(it)!!
val views = texture.imageViewArray
require(views.isNotEmpty()) { "Texture ${it.name} resolved to empty image view array" }
if (it.arrayCount == 0) {
uniforms[it.name] = Module.TexturePointer(it.name, it, source = null, texture, sampler)
} else {
require(views.size == it.arrayCount) {
"can't link texture resource ${it.name} of length ${views.size} but the shader is expecting ${it.arrayCount}"
}
uniforms[it.name] = Module.TextureArrayPointer(it.name, it, source = null, texture, sampler)
}
} }
fi.textures.forEach { if (it.set == set) {
found = true
val (texture, sampler) = parameters.textureResolver(it)!!
val views = texture.imageViewArray
require(views.isNotEmpty()) { "Texture ${it.name} resolved to empty image view array" }
if (it.arrayCount == 0) {
uniforms[it.name] = Module.TexturePointer(it.name, it, source = null, texture, sampler)
} else {
require(views.size == it.arrayCount) {
"can't link texture resource ${it.name} of length ${views.size} but the shader is expecting ${it.arrayCount}"
}
uniforms[it.name] = Module.TextureArrayPointer(it.name, it, source = null, texture, sampler)
}
} }
if (!found) return@forEach
}
// Storage images (sets >= 1)
setsNeeded.entries.forEach { (set, _) ->
var found = false
ti.images.forEach { if (it.set == set) {
found = true
val image = parameters.imageResolver(it)!!
uniforms[it.name] = Module.ImagePointer(it.name, it, source = null, image)
} }
mi.images.forEach { if (it.set == set) {
found = true
val image = parameters.imageResolver(it)!!
uniforms[it.name] = Module.ImagePointer(it.name, it, source = null, image)
} }
fi.images.forEach { if (it.set == set) {
found = true
val image = parameters.imageResolver(it)!!
uniforms[it.name] = Module.ImagePointer(it.name, it, source = null, image)
} }
if (!found) return@forEach
}
MemoryStack.stackPush().use { stack ->
val shaderStages = VkPipelineShaderStageCreateInfo.calloc(3, stack)
shaderStages[0].sType(VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO)
.stage(VK_SHADER_STAGE_TASK_BIT_EXT)
.pName(stack.UTF8("main"))
.module(tsm)
shaderStages[1].sType(VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO)
.stage(VK_SHADER_STAGE_MESH_BIT_EXT)
.pName(stack.UTF8("main"))
.module(msm)
shaderStages[2].sType(VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO)
.stage(VK_SHADER_STAGE_FRAGMENT_BIT)
.pName(stack.UTF8("main"))
.module(fsm)
val dynamicViewportCount = parameters.viewportCount.coerceAtLeast(1)
val vp = VkPipelineViewportStateCreateInfo.calloc(stack)
.sType(VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO)
.viewportCount(dynamicViewportCount)
.scissorCount(dynamicViewportCount)
val rs = VkPipelineRasterizationStateCreateInfo.calloc(stack)
.sType(VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO)
.polygonMode(parameters.fillMode)
.cullMode(VK_CULL_MODE_NONE)
.frontFace(VK_FRONT_FACE_CLOCKWISE)
.lineWidth(1.0f)
val ms = VkPipelineMultisampleStateCreateInfo.calloc(stack)
.sType(VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO)
.rasterizationSamples(VK_SAMPLE_COUNT_1_BIT)
var ds: VkPipelineDepthStencilStateCreateInfo? = null
if (parameters.depth > 0) {
ds = VkPipelineDepthStencilStateCreateInfo.calloc(stack)
.sType(VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO)
.depthTestEnable(true)
.depthWriteEnable(parameters.depthWrite)
.depthCompareOp(parameters.depthCompareOp)
.depthBoundsTestEnable(false)
.stencilTestEnable(false)
}
val colorFormats = if (parameters.colorFormats.isNotEmpty()) parameters.colorFormats
else List(parameters.color) { parameters.imageFormat }
require(colorFormats.isNotEmpty()) { "Must specify at least one color attachment" }
val colorCount = colorFormats.size
val blendAtt = VkPipelineColorBlendAttachmentState.calloc(colorCount, stack)
for (i in 0 until colorCount) {
val attachmentBlend = parameters.blend &&
parameters.renderer.formatSupportsColorAttachmentBlend(colorFormats[i])
blendAtt[i]
.colorWriteMask(
(if (parameters.colorMask[0]) VK_COLOR_COMPONENT_R_BIT else 0) or
(if (parameters.colorMask[1]) VK_COLOR_COMPONENT_G_BIT else 0) or
(if (parameters.colorMask[2]) VK_COLOR_COMPONENT_B_BIT else 0) or
(if (parameters.colorMask[3]) VK_COLOR_COMPONENT_A_BIT else 0)
)
.blendEnable(attachmentBlend)
.srcColorBlendFactor(parameters.srcColor)
.dstColorBlendFactor(parameters.dstColor)
.colorBlendOp(parameters.colorBlendOp)
.srcAlphaBlendFactor(parameters.srcAlpha)
// (3) BUGFIX: use dstAlpha, not dstColor
.dstAlphaBlendFactor(parameters.dstAlpha)
.alphaBlendOp(parameters.alphaBlendOp)
}
val cb = VkPipelineColorBlendStateCreateInfo.calloc(stack)
.sType(VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO)
.pAttachments(blendAtt)
val dynList = mutableListOf(
VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_SCISSOR,
VK_DYNAMIC_STATE_DEPTH_TEST_ENABLE
)
if (parameters.renderer.device.supportsExtendedDynamicState) {
dynList += listOf(
// VK_DYNAMIC_STATE_PRIMITIVE_TOPOLOGY, // not allowed for meshShaders
VK_DYNAMIC_STATE_CULL_MODE,
VK_DYNAMIC_STATE_FRONT_FACE
)
}
if (parameters.renderer.device.supportsExtendedDynamicState3) {
dynList += EXTExtendedDynamicState3.VK_DYNAMIC_STATE_DEPTH_CLAMP_ENABLE_EXT
}
val dynStates = stack.ints(*dynList.toIntArray())
val dyn = VkPipelineDynamicStateCreateInfo.calloc(stack)
.sType(VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO)
.pDynamicStates(dynStates)
var vpcr: VkPushConstantRange.Buffer? = null
if (pushConstantSize > 0) {
vpcr = VkPushConstantRange.calloc(1, stack)
.stageFlags(VK_SHADER_STAGE_TASK_BIT_EXT or VK_SHADER_STAGE_MESH_BIT_EXT or VK_SHADER_STAGE_FRAGMENT_BIT)
.offset(0)
.size(pushConstantSize)
}
// ---- Pipeline layout includes every shader-declared set, starting at 0
val highestSet = layouts.keys.maxOrNull() ?: -1
val ppLayout = if (highestSet >= 0) stack.callocLong(highestSet + 1) else null
if (highestSet >= 0) {
ppLayout?.let { buf ->
for (s in 0..highestSet) {
val layout = layouts[s]
?: throw IllegalArgumentException(
"Pipeline layout is missing descriptor set $s. Make your module sets contiguous starting at set=0."
)
buf.put(s, layout.vkDescriptorLayout)
}
}
}
val pli = VkPipelineLayoutCreateInfo.calloc(stack)
.sType(VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO)
ppLayout?.let { pli.pSetLayouts(it) }
vpcr?.let { pli.pPushConstantRanges(it) }
val lp = stack.mallocLong(1)
vkCheck(
vkCreatePipelineLayout(parameters.renderer.device.vkDevice, pli, null, lp),
"Failed to create pipeline layout"
)
pipelineLayout = lp[0]
parameters.renderer.device.nameObject(
pipelineLayout, VK10.VK_OBJECT_TYPE_PIPELINE_LAYOUT, "${parameters.name} PipelineLayout"
)
val renderInfo = VkPipelineRenderingCreateInfoKHR.calloc(1, stack)
.sType(KHRDynamicRendering.VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR)
val colorAttachmentFormats = stack.callocInt(colorCount)
colorFormats.forEach { colorAttachmentFormats.put(it) }
colorAttachmentFormats.flip()
renderInfo
.colorAttachmentCount(colorCount)
.pColorAttachmentFormats(colorAttachmentFormats)
.depthAttachmentFormat(parameters.pipelineDepthFormat)
.stencilAttachmentFormat(0)
.viewMask(parameters.viewMask)
val gp = VkGraphicsPipelineCreateInfo.calloc(1, stack)
.sType(VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO)
.pStages(shaderStages)
.pViewportState(vp)
.pRasterizationState(rs)
.pMultisampleState(ms)
.pColorBlendState(cb)
.pDynamicState(dyn)
.layout(pipelineLayout)
.renderPass(0)
.pNext(renderInfo.address())
ds?.let { gp.pDepthStencilState(it) }
LatencySpans.time("pipeline.create", detail = "graphics") {
vkCheck(
vkCreateGraphicsPipelines(
parameters.renderer.device.vkDevice,
parameters.renderer.pipelineCache.vkPipelineCache,
gp, null, lp
),
"Error creating graphics pipeline"
)
}
this.pipeline = lp[0]
pipelineDisposeToken.updateCleanup(snapshotPipelineCleanup())
parameters.renderer.device.nameObject(
pipeline, VK10.VK_OBJECT_TYPE_PIPELINE, "${parameters.name} Pipeline"
)
// hook structures to backing buffers
liveUniformStructures.forEach { (name, structure) ->
val target = (uniforms[name] as Module.Uniform).buffer
structure.proxying = target
}
}
valid = true
} catch (e: Exception) {
// A Metal compile failure just before this is the real reason; say it here, where the box reads it.
parameters.errorHandler("Module error: ${e.message}" + (afield.graphics.shader.MetalShaderErrors.recent()?.let { "\n$it" } ?: ""), e)
}
}
override fun render(command: CommandBuffer, bounds: Rectanglef)
body — 59 lines, from line 675
override fun render(command: CommandBuffer, bounds: Rectanglef) {
if (!valid || !enabled) return
MemoryStack.stackPush().use { stack ->
val labelColor = stack.floats(
parameters.labelColor.x.f, parameters.labelColor.y.f, parameters.labelColor.z.f, parameters.labelColor.w.f
)
command.beginLabel(parameters.name, labelColor)
val cmd = command.vkCommandBuffer
val frameIndex = parameters.renderer.currentImage
val fieldProbe = parameters.fieldProbe?.takeIf { FIELD_PROBE_BUFFER_NAME in uniforms }
fieldProbe?.beforeShaderUse(parameters.renderer, frameIndex)
VK10.vkCmdBindPipeline(cmd, VK10.VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline)
VK13.vkCmdSetDepthTestEnable(cmd, if (parameters.depth == 0) false else parameters.depthTest)
if (parameters.renderer.device.supportsExtendedDynamicState) {
VK13.vkCmdSetPrimitiveTopology(cmd, parameters.primitiveType)
VK13.vkCmdSetCullMode(cmd, VK_CULL_MODE_NONE)
VK13.vkCmdSetFrontFace(cmd, VK_FRONT_FACE_CLOCKWISE)
}
if (parameters.renderer.device.supportsExtendedDynamicState3) {
EXTExtendedDynamicState3.vkCmdSetDepthClampEnableEXT(cmd, false)
}
val pushConstantBuffer = if (pushConstantSize > 0) stack.calloc(pushConstantSize) else stack.malloc(0)
uniforms.values.forEach { upd ->
upd.pushDescriptorSet(command, pipelineLayout, VK_PIPELINE_BIND_POINT_GRAPHICS)
}
// Update dirty UBO/SSBOs
liveUniformStructures.forEach { (name, structure) ->
val target = (uniforms[name] as Module.Uniform).buffer
structure.proxying = target
structure.writeIfStale(target, command) {
throw MissingRefException(afield.graphics.vulkan.buffers.unassignedMemberMessage(it))
}
target.clean(false, command)
}
// Dispatch per-mesh work
meshes.forEach { (_, mesh) ->
mesh.compute(this, command, pushConstantBuffer)
}
after.forEach { (_, target) ->
target.render(command, parameters.renderer.currentImage)
}
fieldProbe?.let {
graphicsHostReadBarrier(command, it.bufferForImage(parameters.renderer, frameIndex))
it.afterShaderUse(parameters.renderer, frameIndex)
}
command.endLabel()
}
}
fun enable()
fun enable() {
enabled = true
}
fun disable()
fun disable() {
enabled = false
}
fun copyMeshesFrom(old: MeshModule)
fun copyMeshesFrom(old: MeshModule) {
old.meshes.entries.removeIf {
meshes[it.key] = it.value
it.value.inside = this.pointer()
true
}
before.putAll(old.before)
after.putAll(old.after)
}
private fun snapshotPipelineCleanup(): () -> Unit {
val dev = parameters.renderer.device.vkDevice
val p = pipeline
val layout = pipelineLayout
val sets = ownedDescriptorLayouts.toList()
val tsmHandle = tsm
val msmHandle = msm
val fsmHandle = fsm
return {
if (p != 0L) VK10.vkDestroyPipeline(dev, p, null)
if (layout != 0L) VK10.vkDestroyPipelineLayout(dev, layout, null)
if (tsmHandle != 0L) VK10.vkDestroyShaderModule(dev, tsmHandle, null)
if (msmHandle != 0L) VK10.vkDestroyShaderModule(dev, msmHandle, null)
if (fsmHandle != 0L) VK10.vkDestroyShaderModule(dev, fsmHandle, null)
sets.forEach { it.cleanup() }
}
}
fun cleanup()
Release this module's pipeline, layout, shader modules and descriptor layouts. It stops drawing at once; while its renderer is alive the handles go through the retirement queue, as for Module.cleanup — a full reload retires the module it replaces while frames that recorded it may still be in flight — and at once after the renderer has closed.
body — 45 lines, from line 777
fun cleanup() {
if (!pipelineDisposeToken.disable()) return
valid = false
val renderer = parameters.renderer
if (renderer.isClosed) destroyPipelineNow()
else renderer.cleanup.add(Runnable { destroyPipelineNow() })
}
private fun destroyPipelineNow() {
if (pipeline != 0L) {
VK10.vkDestroyPipeline(parameters.renderer.device.vkDevice, pipeline, null)
pipeline = 0L
}
if (pipelineLayout != 0L) {
VK10.vkDestroyPipelineLayout(parameters.renderer.device.vkDevice, pipelineLayout, null)
pipelineLayout = 0L
}
if (msm != 0L) {
VK10.vkDestroyShaderModule(parameters.renderer.device.vkDevice, msm, null)
msm = 0L
}
if (tsm != 0L) {
VK10.vkDestroyShaderModule(parameters.renderer.device.vkDevice, tsm, null)
tsm = 0L
}
if (fsm != 0L) {
VK10.vkDestroyShaderModule(parameters.renderer.device.vkDevice, fsm, null)
fsm = 0L
}
ownedDescriptorLayouts.forEach { it.cleanup() }
ownedDescriptorLayouts.clear()
}
private fun createShaderModule(code: ByteBuffer): Long {
MemoryStack.stackPush().use { stack ->
val pCode = stack.malloc(code.limit()).put(code).flip()
val ci = VkShaderModuleCreateInfo.calloc(stack)
.sType(VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO)
.pCode(pCode)
val lp = stack.mallocLong(1)
vkCheck(vkCreateShaderModule(parameters.renderer.device.vkDevice, ci, null, lp),
"Failed to create shader module")
return lp[0]
}
}
operator fun plusAssign(m: Pair<String, MeshMesh>)
operator fun plusAssign(m: Pair<String, MeshMesh>) {
meshes[m.first] = m.second
m.second.inside = this.pointer()
}
operator fun plusAssign(m: Pair<String, Target>)
@JvmName("plusAssign_target")
operator fun plusAssign(m: Pair<String, Target>) {
after[m.first] = m.second
}
operator fun plusAssign(m: Pair<String, BeforeTarget>)
@JvmName("plusAssign_beforeTarget")
operator fun plusAssign(m: Pair<String, BeforeTarget>) {
before[m.first] = m.second.target
}
}