Skip to content

ComputeModule.kt

src/jvmMain/kotlin/afield/graphics/render/ComputeModule.kt · 685 lines

package afield.graphics.render

import afield.app.diagnostics.LatencySpans
import afield.graphics.scene.main

import afield.graphics.shader.Introspected
import afield.graphics.shader.Introspection
import afield.graphics.shader.ShaderKind
import afield.graphics.shader.shaderInterfaceTransition

import afield.graphics.vulkan.Target
import afield.graphics.vulkan.buffers.FrameBufferGroup
import afield.graphics.vulkan.buffers.MissingRefException
import afield.graphics.vulkan.buffers.Structure
import afield.graphics.vulkan.buffers.VulkanBuffer
import afield.graphics.vulkan.buffers.buffer
import afield.graphics.vulkan.device.*
import afield.graphics.vulkan.device.DescriptorSetLayout.SimpleDescriptorSetLayout
import afield.graphics.render.Module.*
import afield.graphics.vulkan.util.AutoDisposer
import afield.graphics.vulkan.util.FIELD_PROBE_BUFFER_NAME
import afield.graphics.vulkan.util.FIELD_STATS_PARTIAL_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.KHRAccelerationStructure.VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR
import org.lwjgl.vulkan.VK13.*
import java.nio.ByteBuffer

A compute pipeline as a renderable module: ComputeModule compiles and introspects a compute shader, owns its descriptor sets, and dispatches the ComputeTasks registered in its meshes.

Boxes hold it through a ComputeModulePointer, which swaps in a rebuilt module when the shader is reloaded and carries live values across the interface change.

class ComputeModulePointer(var current: ComputeModule) : Renderable

class ComputeModulePointer(var current: ComputeModule) : 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

override fun render(command: CommandBuffer, bounds: Rectanglef)

    override fun render(command: CommandBuffer, bounds: Rectanglef) {
        if (current.valid) current.render(command, bounds)
        else {
//            println("module '${current.parameters.name}' not valid / not rendering")
        }
    }

enum class ReloadOutcome

What a reloadOutcome did.

    enum class ReloadOutcome {

The parameters were already the compiled ones; nothing happened.

        UNCHANGED,

A new module compiled and replaced the old one.

        RELOADED,

The new parameters did not compile; the old module keeps running.

        FAILED,
    }

fun reload(p: Parameters, onError: (String, Throwable?) -> Unit

True when the module was replaced. False covers both "already compiled" and "did not compile"; reloadOutcome tells them apart.

    fun reload(p: Parameters, onError: (String, Throwable?) -> Unit = p.errorHandler): Boolean =
        reloadOutcome(p, onError) == ReloadOutcome.RELOADED

fun reloadOutcome(p: Parameters, onError: (String, Throwable?) -> Unit

reload, saying which of the three things happened.

body — 54 lines, from line 71
    fun reloadOutcome(p: Parameters, onError: (String, Throwable?) -> Unit = p.errorHandler): ReloadOutcome {
        if (current.valid && p == current.parameters) return ReloadOutcome.UNCHANGED

        p.errorHandler = onError

        val nc = p.computeIntrospection
        val before = if (nc != null) runCatching {
            shaderInterfaceTransition(listOf("compute" to current.ci), listOf("compute" to nc))
        }.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 ->
            try {
                val tp = current.uniforms[it.name] as 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 = ComputeModule(p)
        if (m.valid) {
            val previous = current
            m.copyMeshesFrom(previous)
            m.enabled = previous.enabled
            val transition = before ?: runCatching {
                shaderInterfaceTransition(listOf("compute" to previous.ci), listOf("compute" to m.ci))
            }.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 ReloadOutcome.RELOADED
        }
        m.cleanup()
        return ReloadOutcome.FAILED
    }

operator fun plusAssign(m: Pair<String, ComputeTask>)

    operator fun plusAssign(m: Pair<String, ComputeTask>) {
        current.meshes[m.first] = m.second
        (m.second as? ComputeMesh)?.let { it.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 zeroBefore(name: String, buffer: FrameBufferGroup)

    fun zeroBefore(name: String, buffer: FrameBufferGroup) {
        current.zeroBefore[name] = buffer
    }

fun zeroBefore(name: String, buffer: VulkanBuffer)

    fun zeroBefore(name: String, buffer: VulkanBuffer) {
        current.zeroBefore[name] = FrameBufferGroup(buffer.renderer, n = 1, buffers = mutableListOf(buffer))
    }

fun enable()

    fun enable() {
        current.enable()
    }

fun disable()

    fun disable() {
        current.disable()
    }
}

fun ComputeModule.pointer() = ComputeModulePointer(this)

class ComputeModule(val parameters: Parameters) : Renderable

class ComputeModule(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

val meshes = linkedMapOf<String, ComputeTask>()

val before = linkedMapOf<String, Target>()

val zeroBefore = linkedMapOf<String, FrameBufferGroup>()

val after = linkedMapOf<String, Target>()

val ci

    val ci = parameters.computeIntrospection
        ?: Introspection(
            parameters.compute,
            ShaderKind.COMPUTE_SHADER,
            errorHandler = parameters.errorHandler,
            sourceName = parameters.computeSourceName,
            includeResolver = parameters.computeIncludeResolver
        )

var csm: Long = 0L

val pushConstantSize = ci.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, "ComputePipeline(${parameters.name})", owner = parameters.renderer.device) { }

data class PipelineUpdate

    data class PipelineUpdate(
        val computeIntrospection: Introspection,
        val shaderModule: Long,
        val pipeline: Long
    )

init

body — 240 lines, from line 199
    init {
        try {
            if (!ci.valid) throw IllegalStateException("Can't make a module without valid programs")

            csm = createShaderModule(ci.code.bytecode()!!).also {
                parameters.renderer.device.nameObject(it, VK10.VK_OBJECT_TYPE_SHADER_MODULE, "${parameters.name} CS")
            }

            // Build per-module sets for all shader-declared sets (including set 0)
            val setsNeeded = mutableMapOf<Int, MutableList<Int>>()
            ci.ubos.forEach     { setsNeeded.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }
            ci.ssbo.forEach     { setsNeeded.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }
            ci.textures.forEach { setsNeeded.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }
            ci.images.forEach   { setsNeeded.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }
            ci.accelerationStructures.forEach { setsNeeded.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }
            require(ci.accelerationStructures.isEmpty() || parameters.renderer.device.supportsRayQuery) {
                "Compute acceleration structures require enabled KHR ray queries"
            }

            setsNeeded.keys.toList().forEach { s ->
                setsNeeded[s] = setsNeeded[s]!!.distinct().sorted().toMutableList()
            }

            uniforms = mutableMapOf()
            liveUniformStructures = mutableMapOf()

            ci.ubos.forEach  { liveUniformStructures[it.name] = Structure().setMembers(it.members).reportingTo(parameters) }
            ci.ssbo.forEach  { liveUniformStructures[it.name] = Structure().setMembers(it.members).reportingTo(parameters) }
            ModuleRebuild.created(liveUniformStructures)

            val layoutMap = mutableMapOf<Int, SimpleDescriptorSetLayout>()

            // Build push-descriptor set layouts
            setsNeeded.entries.forEach { (set, bindingList) ->
                var stages = 0
                val kinds = mutableListOf<Int>()
                val lens  = mutableListOf<Int>()

                bindingList.forEach { bind ->
                    var kind = 0
                    ci.ubos.forEach {
                        if (it.set == set && it.binding == bind) {
                            stages = stages or VK_SHADER_STAGE_COMPUTE_BIT
                            kind = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER
                            lens.add(1)
                        }
                    }
                    ci.ssbo.forEach {
                        if (it.set == set && it.binding == bind) {
                            stages = stages or VK_SHADER_STAGE_COMPUTE_BIT
                            kind = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER
                            lens.add(1)
                        }
                    }
                    ci.textures.forEach {
                        if (it.set == set && it.binding == bind) {
                            stages = stages or VK_SHADER_STAGE_COMPUTE_BIT
                            kind = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER
                            lens.add(maxOf(1, it.arrayCount))
                        }
                    }
                    ci.images.forEach {
                        if (it.set == set && it.binding == bind) {
                            stages = stages or VK_SHADER_STAGE_COMPUTE_BIT
                            kind = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE
                            lens.add(1)
                        }
                    }

                    ci.accelerationStructures.forEach {
                        if (it.set == set && it.binding == bind) {
                            stages = stages or VK_SHADER_STAGE_COMPUTE_BIT
                            kind = VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR
                            lens.add(1)
                        }
                    }

                    if (kind == 0) throw IllegalArgumentException(" couldn't find binding $bind for set $set in $bindingList")
                    kinds.add(kind)
                }

                val layout = DescriptorSetLayout.SimpleDescriptorSetLayout(
                    parameters.renderer.device, kinds, set, bindingList, stages, lens, pushDescriptor = true
                )
                layoutMap[set] = layout
                ownedDescriptorLayouts += layout
            }

            // UBOs
            setsNeeded.entries.forEach { (set, _) ->
                var kind = 0
                val tag  = mutableListOf<Introspected.Bindable>()
                ci.ubos.forEach { if (it.set == set) { kind = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; tag.add(it) } }
                if (kind == 0) return@forEach

                tag.forEach {
                    var sz = (it as Introspected.UBO).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] = Uniform(it.name, it, source = buf, buf.toBuffer(parameters.renderer), kind)
                }
            }

            // SSBOs
            setsNeeded.entries.forEach { (set, _) ->
                var kind = 0
                val tag  = mutableListOf<Introspected.Bindable>()
                ci.ssbo.forEach { if (it.set == set) { kind = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; tag.add(it) } }
                if (kind == 0) return@forEach

                tag.forEach {
                    var sz = (it as Introspected.UBO).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] = Uniform(it.name, it, source = buf, buf.toBuffer(parameters.renderer), kind)
                }
            }

            // Sampled textures
            setsNeeded.entries.forEach { (set, _) ->
                var kind = 0
                val tag  = mutableListOf<Introspected.Bindable>()
                ci.textures.forEach { if (it.set == set) { kind = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; tag.add(it) } }
                if (kind == 0) return@forEach

                tag.forEach {
                    val (texture, sampler) = parameters.textureResolver(it as Introspected.Texture)!!
                    val views = texture.imageViewArray
                    require(views.isNotEmpty()) { "Texture ${it.name} resolved to empty image view array" }

                    if (it.arrayCount == 0) {
                        uniforms[it.name] = 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] = TextureArrayPointer(it.name, it, source = null, texture, sampler)
                    }
                }
            }

            // Storage images
            setsNeeded.entries.forEach { (set, _) ->
                var kind = 0
                val tag  = mutableListOf<Introspected.Bindable>()
                ci.images.forEach { if (it.set == set) { kind = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; tag.add(it) } }
                if (kind == 0) return@forEach

                tag.forEach {
                    val image = parameters.imageResolver(it as Introspected.Image)!!
                    uniforms[it.name] = ImagePointer(it.name, it, source = null, image)
                }
            }

            ci.accelerationStructures.forEach { accel ->
                val resolved = parameters.accelerationStructureResolver(accel)
                    ?: throw IllegalArgumentException("cannot link acceleration structure ${accel.name}")
                uniforms[accel.name] = RayTracingModule.AccelerationStructurePointer(accel.name, accel, resolved)
            }

            // --- Pipeline creation
            MemoryStack.stackPush().use { stack ->
                val stage = VkPipelineShaderStageCreateInfo.calloc(1, stack)
                stage[0].sType(VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO)
                    .stage(VK_SHADER_STAGE_COMPUTE_BIT)
                    .pName(stack.UTF8("main"))
                    .module(csm)

                var vpcr: VkPushConstantRange.Buffer? = null
                if (pushConstantSize > 0) {
                    vpcr = VkPushConstantRange.calloc(1, stack)
                        .stageFlags(VK_SHADER_STAGE_COMPUTE_BIT)
                        .offset(0)
                        .size(pushConstantSize)
                }

                val highestUsedSet = layoutMap.keys.maxOrNull() ?: -1
                val ppLayout = if (highestUsedSet >= 0) stack.callocLong(highestUsedSet + 1) else null
                if (highestUsedSet >= 0) {
                    ppLayout?.let { buf ->
                        for (s in 0..highestUsedSet) {
                            val layout = layoutMap[s] ?: throw IllegalArgumentException(
                                "Pipeline layout is missing descriptor set $s. Make 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 cp = VkComputePipelineCreateInfo.calloc(1, stack)
                    .sType(VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO)
                    .stage(stage[0])
                    .layout(pipelineLayout)

                LatencySpans.time("pipeline.create", detail = "compute") {
                    vkCheck(
                        vkCreateComputePipelines(
                            parameters.renderer.device.vkDevice,
                            parameters.renderer.pipelineCache.vkPipelineCache,
                            cp, null, lp
                        ),
                        "Error creating compute pipeline"
                    )
                }
                this.pipeline = lp[0]
                pipelineDisposeToken.updateCleanup(snapshotPipelineCleanup())
                parameters.renderer.device.nameObject(
                    pipeline, VK10.VK_OBJECT_TYPE_PIPELINE, "${parameters.name} Pipeline"
                )

                liveUniformStructures.forEach { (name, structure) ->
                    val target = (uniforms[name] as Uniform).buffer
                    structure.proxying = target
                }
            }

            valid = true
        } catch (e: Exception) {
            parameters.errorHandler("error `${e.message}`", e)
        }
    }

fun buildPipelineUpdate(newCompute: Introspection): PipelineUpdate

body — 45 lines, from line 440
    fun buildPipelineUpdate(newCompute: Introspection): PipelineUpdate {
        var newCsm = 0L
        var newPipeline = 0L
        try {
            newCsm = createShaderModule(newCompute.code.bytecode()!!).also {
                parameters.renderer.device.nameObject(it, VK10.VK_OBJECT_TYPE_SHADER_MODULE, "${parameters.name} CS update")
            }

            MemoryStack.stackPush().use { stack ->
                val stage = VkPipelineShaderStageCreateInfo.calloc(1, stack)
                stage[0].sType(VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO)
                    .stage(VK_SHADER_STAGE_COMPUTE_BIT)
                    .pName(stack.UTF8("main"))
                    .module(newCsm)

                val cp = VkComputePipelineCreateInfo.calloc(1, stack)
                    .sType(VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO)
                    .stage(stage[0])
                    .layout(pipelineLayout)

                val lp = stack.mallocLong(1)
                synchronized(parameters.renderer.pipelineCache) {
                    LatencySpans.time("pipeline.update", detail = "compute") {
                        vkCheck(
                            vkCreateComputePipelines(
                                parameters.renderer.device.vkDevice,
                                parameters.renderer.pipelineCache.vkPipelineCache,
                                cp, null, lp
                            ),
                            "Error creating compute pipeline update"
                        )
                    }
                }
                newPipeline = lp[0]
                parameters.renderer.device.nameObject(
                    newPipeline, VK10.VK_OBJECT_TYPE_PIPELINE, "${parameters.name} Pipeline update"
                )
            }

            return PipelineUpdate(newCompute, newCsm, newPipeline)
        } catch (t: Throwable) {
            destroyPipelineHandles(newPipeline, 0L, newCsm)
            throw t
        }
    }

fun installPipelineUpdate(update: PipelineUpdate)

    fun installPipelineUpdate(update: PipelineUpdate) {
        val oldPipeline = pipeline
        val oldCsm = csm

        pipeline = update.pipeline
        csm = update.shaderModule

        parameters.computeIntrospection = update.computeIntrospection

        pipelineDisposeToken.updateCleanup(snapshotPipelineCleanup())

        parameters.renderer.cleanup.add(Runnable {
            destroyPipelineHandles(oldPipeline, 0L, oldCsm)
        })
    }

fun destroyUnusedPipelineUpdate(update: PipelineUpdate)

    fun destroyUnusedPipelineUpdate(update: PipelineUpdate) {
        destroyPipelineHandles(update.pipeline, 0L, update.shaderModule)
    }

override fun render(command: CommandBuffer, bounds: Rectanglef)

body — 84 lines, from line 506
    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)

            try {
                val cmd = command.vkCommandBuffer
                val frameIndex = parameters.renderer.currentImage
                val profiler = parameters.renderer.getProfiler()
                val fieldProbe = parameters.fieldProbe?.takeIf { FIELD_PROBE_BUFFER_NAME in uniforms }
                val fieldStats = parameters.fieldStats
                    ?.takeIf { it.producer && FIELD_STATS_PARTIAL_BUFFER_NAME in uniforms }

                val marks = if (profiler != null) afield.graphics.vulkan.Renderer.ModuleMarks(parameters.name) else null
                var psScope: GpuProfiler.PipeStatScope? = null
                var perfScope: GpuProfiler.PerfScope? = null

                if (profiler != null) {
                    marks!!.tsBegin = profiler.writeTimestamp2(cmd, frameIndex, VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT)
                    psScope = profiler.beginPipelineStats(cmd, frameIndex)
                    perfScope = profiler.beginPerfCounters(cmd, frameIndex)
                }

                fieldProbe?.beforeShaderUse(parameters.renderer, frameIndex)

                before.values.forEach { target ->
                    target.render(command, parameters.renderer.currentImage)
                }

                fieldStats?.stats?.beforeProducer(command, parameters.renderer)

                zeroBefore.values.forEach { buffer ->
                    buffer.get(write = true).zero(command)
                }

                VK10.vkCmdBindPipeline(cmd, VK10.VK_PIPELINE_BIND_POINT_COMPUTE, pipeline)

                val pushConstantBuffer = if (pushConstantSize > 0) stack.calloc(pushConstantSize) else stack.malloc(0)

                liveUniformStructures.forEach { (name, structure) ->
                    val target = (uniforms[name] as Uniform).buffer
                    structure.proxying = target
                    structure.writeIfStale(target, command) {
                        throw MissingRefException(afield.graphics.vulkan.buffers.unassignedMemberMessage(it))
                    }
                    target.clean(false, command)
                }

                uniforms.values.forEach { upd ->
                    upd.pushDescriptorSet(command, pipelineLayout, VK10.VK_PIPELINE_BIND_POINT_COMPUTE)
                }

                meshes.values.forEach { mesh ->
                    mesh.compute(this, command, pushConstantBuffer)
                }

                fieldStats?.stats?.afterProducer(command, parameters.renderer)

                after.values.forEach { target ->
                    target.render(command, parameters.renderer.currentImage)
                }

                fieldProbe?.let {
                    hostReadBarrier(command, it.bufferForImage(parameters.renderer, frameIndex))
                    it.afterComputeShaderUse(parameters.renderer, frameIndex)
                }

                if (profiler != null) {
                    profiler.endPipelineStats(cmd, psScope)
                    profiler.endPerfCounters(cmd, frameIndex, perfScope)
                    marks!!.tsEnd = profiler.writeTimestamp2(cmd, frameIndex, VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT)
                    psScope?.let { marks.pipeStatsIndex = it.index }
                    perfScope?.let { marks.perfIndex = it.index }
                    parameters.renderer.registerModuleMarks(frameIndex, marks)
                }
            } finally {
                command.endLabel()
            }
        }
    }

fun enable()

    fun enable() {
        enabled = true
    }

fun disable()

    fun disable() {
        enabled = false
    }

fun copyMeshesFrom(old: ComputeModule)

    fun copyMeshesFrom(old: ComputeModule) {
        old.meshes.entries.removeIf {
            meshes[it.key] = it.value
            (it.value as? ComputeMesh)?.let { cm ->
                cm.inside = this.pointer()
                // Reloaded bindings are available immediately, before the next dispatch.
                cm.push.setMembers(ci.pushConstants.flatMap { block -> block.members })
            }
            true
        }
        before.putAll(old.before)
        zeroBefore.putAll(old.zeroBefore)
        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 csmHandle = csm
        return {
            if (p != 0L) VK10.vkDestroyPipeline(dev, p, null)
            if (layout != 0L) VK10.vkDestroyPipelineLayout(dev, layout, null)
            if (csmHandle != 0L) VK10.vkDestroyShaderModule(dev, csmHandle, null)
            sets.forEach { it.cleanup() }
        }
    }

fun cleanup()

Release this module's pipeline, layout, shader module and descriptor layouts. It stops dispatching at once; while its renderer is alive the handles go through the retirement queue, as for Module.cleanup, and at once after the renderer has closed.

body — 43 lines, from line 633
    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() {
        destroyPipelineHandles(pipeline, pipelineLayout, csm)
        pipeline = 0L
        pipelineLayout = 0L
        csm = 0L
        ownedDescriptorLayouts.forEach { it.cleanup() }
        ownedDescriptorLayouts.clear()
    }

    private fun destroyPipelineHandles(pipelineHandle: Long, pipelineLayoutHandle: Long, shaderModule: Long) {
        if (pipelineHandle != 0L) {
            VK10.vkDestroyPipeline(parameters.renderer.device.vkDevice, pipelineHandle, null)
        }
        if (pipelineLayoutHandle != 0L) {
            VK10.vkDestroyPipelineLayout(parameters.renderer.device.vkDevice, pipelineLayoutHandle, null)
        }
        if (shaderModule != 0L) {
            VK10.vkDestroyShaderModule(parameters.renderer.device.vkDevice, shaderModule, null)
        }
    }

    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]
        }
    }

fun zeroBefore(name: String, buffer: FrameBufferGroup)

    fun zeroBefore(name: String, buffer: FrameBufferGroup) {
        zeroBefore[name] = buffer
    }

fun zeroBefore(name: String, buffer: VulkanBuffer)

    fun zeroBefore(name: String, buffer: VulkanBuffer) {
        zeroBefore[name] = FrameBufferGroup(buffer.renderer, n = 1, buffers = mutableListOf(buffer))
    }
}