Skip to content

Module.kt

src/jvmMain/kotlin/afield/graphics/render/Module.kt · 1463 lines

The rasterising Module: one graphics pipeline built from vertex/fragment (or task/mesh) GLSL, the Meshes it draws, and the uniforms, textures and push constants bound to it.

Parameters carries the shader sources and pipeline settings, and parameters builds one against the renderer found by fromContext(). A ModulePointer stands in front of a module so a box can recompile its shaders and swap in the new Module without the renderer losing its reference; Module.pointer makes one. before wraps a command-buffer callback as a target that runs ahead of the module's draws, and parseCompilerException splits a shader compiler error into file, line and message.

Box code usually reaches this through moduleHere rather than constructing a Module directly; the graphics lessons in public/docs/assets/canonical_graphics/LESSONS.md show that shape.

package and imports — 49 lines, from line 17
package afield.graphics.render

import afield.app.diagnostics.LatencySpans
import afield.graphics.vulkan.targets.HasRenderToTexture
import afield.graphics.scene.main

import afield.document.context.fromContext
import afield.document.context.fromContextWithNameOrNull
import afield.graphics.shader.CompilerError
import afield.graphics.shader.Introspected
import afield.graphics.shader.Introspection
import afield.graphics.shader.ShaderIncludeResolver
import afield.graphics.shader.ShaderKind
import afield.graphics.shader.graphicsShaderInterfaceTransition

import afield.graphics.vulkan.device.vkLayout
import afield.app.diagnostics.Named
import afield.app.protocol.Rect
import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.Target
import afield.graphics.vulkan.buffers.*
import afield.graphics.vulkan.device.*
import afield.graphics.vulkan.device.DescriptorSetLayout.SimpleDescriptorSetLayout
import afield.graphics.shader.Introspected.Bindable
import afield.graphics.vulkan.targets.MultiPingPongRenderToTexture
import afield.graphics.vulkan.targets.RenderToTexture
import afield.graphics.texture.HasDefaultTextureSampler
import afield.graphics.texture.HasImageView
import afield.graphics.texture.HasImageViewArray
import afield.graphics.texture.ImageView
import afield.graphics.texture.PreparesSampledImages
import afield.graphics.texture.TextureSampler
import afield.graphics.vulkan.util.AutoDisposer
import afield.graphics.vulkan.util.FIELD_PROBE_BUFFER_NAME
import afield.graphics.vulkan.util.Options
import afield.graphics.vulkan.util.ShaderFieldStatsBinding
import afield.graphics.vulkan.util.ShaderFieldProbe
import afield.graphics.vulkan.util.vkCheck
import afield.app.diagnostics.logging.lprintln
import afield.math.linalg.Vec4
import afield.math.linalg.f
import afield.math.linalg.Rectanglef
import org.lwjgl.system.MemoryStack
import org.lwjgl.system.MemoryUtil
import org.lwjgl.vulkan.*
import org.lwjgl.vulkan.KHRPushDescriptor.vkCmdPushDescriptorSetKHR
import org.lwjgl.vulkan.VK13.*
import java.nio.ByteBuffer
import kotlin.reflect.KProperty

data class Parameters

Shader sources and pipeline settings shared by Module and RayTracingModule.

declaration — 105 lines, from line 68
data class Parameters(
    val renderer: Renderer,
    var vertex: String = "",
    var fragment: String = "",
    var compute: String = "",
    var mesh: String = "",
    var task: String = "",
    var raygen: String = "",
    var miss: String = "",
    var closesthit: String = "",
    var anyhit: String = "",
    var intersection: String = "",
    var callable: String = "",
    var vertexIntrospection: Introspection? = null,
    var fragmentIntrospection: Introspection? = null,
    var computeIntrospection: Introspection? = null,
    var meshIntrospection: Introspection? = null,
    var taskIntrospection: Introspection? = null,
    var raygenIntrospection: Introspection? = null,
    var missIntrospection: Introspection? = null,
    var closesthitIntrospection: Introspection? = null,
    var anyhitIntrospection: Introspection? = null,
    var intersectionIntrospection: Introspection? = null,
    var callableIntrospection: Introspection? = null,
    var vertexSourceName: String = "vertex.glsl",
    var fragmentSourceName: String = "fragment.glsl",
    var computeSourceName: String = "compute.glsl",
    var meshSourceName: String = "mesh.glsl",
    var taskSourceName: String = "task.glsl",
    var raygenSourceName: String = "raygen.rgen",
    var missSourceName: String = "miss.rmiss",
    var closesthitSourceName: String = "closesthit.rchit",
    var anyhitSourceName: String = "anyhit.rahit",
    var intersectionSourceName: String = "intersection.rint",
    var callableSourceName: String = "callable.rcall",
    var vertexIncludeResolver: ShaderIncludeResolver? = null,
    var fragmentIncludeResolver: ShaderIncludeResolver? = null,
    var computeIncludeResolver: ShaderIncludeResolver? = null,
    var meshIncludeResolver: ShaderIncludeResolver? = null,
    var taskIncludeResolver: ShaderIncludeResolver? = null,
    var raygenIncludeResolver: ShaderIncludeResolver? = null,
    var missIncludeResolver: ShaderIncludeResolver? = null,
    var closesthitIncludeResolver: ShaderIncludeResolver? = null,
    var anyhitIncludeResolver: ShaderIncludeResolver? = null,
    var intersectionIncludeResolver: ShaderIncludeResolver? = null,
    var callableIncludeResolver: ShaderIncludeResolver? = null,
    var color: Int = 1,
    var colorFormats: List<Int> = emptyList(),
    var depth: Int = 1,
    var depthFormat: Int = Options.defaultDepthFormat,
    var primitiveType: Int = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
    var textureResolver: (Introspected.Texture) -> Pair<HasImageViewArray, TextureSampler>? = {
        lprintln("warning,graphics", "Cannot resolve texture ${it.name}")
        null
    },
    var uboResolver: (BufferParameters) -> BufferParameters? = {
        val lb = linkBuffer(it.name, it)
        lb.second
    },
    var ssboResolver: (BufferParameters) -> BufferParameters? = {
        val lb = linkBuffer(it.name, it)
        lb.second
    },
    var ssboSizer: (Introspected.UBO) -> Int? = { 0 },
    var imageResolver: (Introspected.Image) -> ImageView? = {
        lprintln("warning,graphics", "Cannot resolve image ${it.name}")
//        throw Exception("Cannot resolve image ${it.name}")
        null
    },
    var accelerationStructureResolver: (Introspected.AccelerationStructure) -> HasAccelerationStructure? = {
        val found = fromContextWithNameOrNull<HasAccelerationStructure>(it.name)
        if (found == null) lprintln("warning,graphics", "Cannot resolve acceleration structure ${it.name}")
        found
    },
    var rayTracingGroups: RayTracingGroups? = null,
    var maxRayRecursionDepth: Int = 1,
    var rayTracingPipelineFlags: Int = 0,
    /** Explicit pipeline opt-in for NV cluster geometry. Baseline pipelines need no extension. */
    var allowClusterAccelerationStructure: Boolean = false,
    var name: String = "unnamed module",
    var errorHandler: (String, Throwable?) -> Unit = { desc, it ->
        lprintln("error,graphics", "error $desc while creating Module: $it")
        it?.let { ex -> lprintln("error,graphics", ex.stackTraceToString()) }
    },
    var colorMask: Array<Boolean> = arrayOf(true, true, true, true),
    var blend: Boolean = true,
    var srcColor: Int = VK_BLEND_FACTOR_SRC_ALPHA,
    var dstColor: Int = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
    var colorBlendOp: Int = VK_BLEND_OP_ADD,
    var srcAlpha: Int = VK_BLEND_FACTOR_SRC_ALPHA,
    var dstAlpha: Int = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
    var alphaBlendOp: Int = VK_BLEND_OP_ADD,
    var depthTest: Boolean = true,
    var depthWrite: Boolean = true,
    var depthCompareOp: Int = VK_COMPARE_OP_LESS_OR_EQUAL,
    var viewport: Rect? = null,
    var viewportCount: Int = 1,
    var viewMask: Int = 0,
    var fillMode: Int = VK_POLYGON_MODE_FILL,
    var imageFormat: Int = renderer.swapChain.surfaceFormat.imageFormat,
    val labelColor: Vec4 = Vec4(0.15f, 0.45f, 0.85f, 1f),
    var fieldProbe: ShaderFieldProbe? = null,
    var fieldStats: ShaderFieldStatsBinding? = null

) {

val declaredMeshes = mutableListOf<DeclaredMesh>()

Meshes declared with +Mesh("name") { ... } inside a moduleHere { } block. A mesh cannot be sized before its module exists, and this block runs first, so they wait here and the module builder creates, adds and fills them once it has built the module.

fun Mesh(name: String, build: Mesh.() -> Unit = {}): DeclaredMesh = DeclaredMesh(name, build)

Inside moduleHere { }: a mesh called name for this module, filled by build once the module exists. Add it with a leading +: +Mesh("my mesh") { set(3, 3) { ... } }. Elsewhere Mesh() is the ordinary constructor.

operator fun DeclaredMesh.unaryPlus()

Adds a Mesh declared in this block to the module.

    operator fun DeclaredMesh.unaryPlus() {
        declaredMeshes += this
    }

val triangles = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST

val lines = VK_PRIMITIVE_TOPOLOGY_LINE_LIST

val points = VK_PRIMITIVE_TOPOLOGY_POINT_LIST

val triangle_strip = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP

val line_strip = VK_PRIMITIVE_TOPOLOGY_LINE_STRIP

val filled = VK_POLYGON_MODE_FILL

val wireframe = VK_POLYGON_MODE_LINE

val depth_never = VK_COMPARE_OP_NEVER

val depth_less = VK_COMPARE_OP_LESS

val depth_equal = VK_COMPARE_OP_EQUAL

val depth_less_or_equal = VK_COMPARE_OP_LESS_OR_EQUAL

val depth_greater = VK_COMPARE_OP_GREATER

val depth_not_equal = VK_COMPARE_OP_NOT_EQUAL

val depth_greater_or_equal = VK_COMPARE_OP_GREATER_OR_EQUAL

val depth_always = VK_COMPARE_OP_ALWAYS

val pipelineDepthFormat: Int get() = if (depth > 0) depthFormat else VK_FORMAT_UNDEFINED

The depth format the pipeline is built for: depthFormat when the module has a depth attachment (depth > 0), otherwise VK_FORMAT_UNDEFINED. depth = 0 alone leaves depthFormat at its default, and a pipeline declaring that format would not match a pass that supplies no depth view.

operator fun RenderToTexture.unaryPlus()

Inside moduleHere { }: this module draws into this target. +target gives the module the target's colour formats, depth and viewports, which it needs before it can be attached with target += module.

    operator fun RenderToTexture.unaryPlus() {
        this@Parameters.color = colorFormats.size
        this@Parameters.colorFormats = colorFormats
        this@Parameters.depth = if (parameters.useDepth) 1 else 0
        this@Parameters.depthFormat = if (parameters.useDepth) parameters.depthFormat else VK_FORMAT_UNDEFINED
        this@Parameters.viewportCount = parameters.viewports.size.coerceAtLeast(1)
        this@Parameters.viewMask = parameters.multiviewViewMask
    }

operator fun HasRenderToTexture.unaryPlus() = +rtt

Inside moduleHere { }: this module draws into the target this object owns, e.g. +view for an ElectronRTTView.

operator fun MultiPingPongRenderToTexture.unaryPlus()

Inside moduleHere { }: this module draws into this ping-pong target, as +target does for a RenderToTexture.

    operator fun MultiPingPongRenderToTexture.unaryPlus() {
        val resolvedColorFormat = if (this.parameters.colorFormats.isNotEmpty()) this.parameters.colorFormats.first() else this.parameters.format
        this@Parameters.color = 1
        this@Parameters.colorFormats = listOf(resolvedColorFormat)
        this@Parameters.depth = if (parameters.useDepth) 1 else 0
        this@Parameters.depthFormat = if (parameters.useDepth) parameters.depthFormat else VK_FORMAT_UNDEFINED
        this@Parameters.viewportCount = parameters.viewports.size.coerceAtLeast(1)
        this@Parameters.viewMask = parameters.multiviewViewMask
    }

fun textures(vararg t: Pair<String, HasImageView>)

    fun textures(vararg t: Pair<String, HasImageView>) {
        val m = t.associate { it }
        this.textureResolver = {
            val found = m[it.name]
            if (found != null) {
                val provider = object : HasImageViewArray {
                    override val imageViewArray = listOf(found)
                }
                val sampler = (found as? HasDefaultTextureSampler)?.defaultSampler ?: TextureSampler(
                    renderer = fromContext(), sampledTextureMipLevels(found), true
                )
                provider to sampler
            } else null
        }
    }

fun textureArrays(vararg t: Pair<String, HasImageViewArray>)

Bind providers whose image views can change between frames (video ring buffers, ping-pong targets, and similar resources). Unlike textures, this keeps the provider itself live instead of capturing one of its current image views while the module is configured.

    fun textureArrays(vararg t: Pair<String, HasImageViewArray>) {
        val m = t.associate { it }
        this.textureResolver = { texture ->
            val provider = m[texture.name]
            if (provider != null) {
                val first = provider.imageViewArray.firstOrNull()
                    ?: error("Texture provider '${texture.name}' has no image views")
                val sampler = (provider as? HasDefaultTextureSampler)?.defaultSampler ?: TextureSampler(
                    renderer = fromContext(), sampledTextureMipLevels(first), true
                )
                provider to sampler
            } else null
        }
    }
}

private fun sampledTextureMipLevels(view: HasImageView): Int {
    return (view as? RenderToTexture)?.mipLevels
        ?: view.imageView.imageViewData.mipLevels
}

class DeclaredMesh(val name: String, val build: Mesh.() -> Unit)

A mesh declared inside moduleHere { +Mesh("name") { ... } }; see Parameters.declaredMeshes.

fun attachDeclaredMeshes(module: ModulePointer, p: Parameters)

Creates each mesh declared in p's block, adds it to module under its name, then runs its build block, in declaration order. Re-running the box replaces a mesh of the same name.

fun attachDeclaredMeshes(module: ModulePointer, p: Parameters) {
    p.declaredMeshes.forEach { declared ->
        val mesh = Mesh()
        module += declared.name to mesh
        declared.build(mesh)
    }
}

fun parameters(renderer: Renderer = fromContext(), build: Parameters.() -> Unit): Parameters {

fun parameters(renderer: Renderer = fromContext(), build: Parameters.() -> Unit): Parameters {
    val p = Parameters(renderer)
    p.build()
    return p
}

internal fun Renderer.formatSupportsColorAttachmentBlend(format: Int): Boolean

internal fun Renderer.formatSupportsColorAttachmentBlend(format: Int): Boolean {
    MemoryStack.stackPush().use { stack ->
        val props = VkFormatProperties.calloc(stack)
        VK10.vkGetPhysicalDeviceFormatProperties(device.physicalDevice.vkPhysicalDevice, format, props)
        return (props.optimalTilingFeatures() and VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT) != 0
    }
}

interface Renderable

fun render(command: CommandBuffer, bounds: Rectanglef)

class FromUniform

operator fun getValue(inside: ModulePointer, property: KProperty<*>): Structure

    operator fun getValue(inside: ModulePointer, property: KProperty<*>): Structure {
        return ModuleRebuild.access(inside, property.name, inside.current.liveUniformStructures[property.name] ?: throw IllegalArgumentException(
            " can't find '${property.name}' in this module, options are ${inside.current.liveUniformStructures.keys}"
        ))
    }
}

private fun sampledImageLayout(view: HasImageView): Int {
    val aspectMask = view.imageView.aspectMask
    val depthOrStencil = (aspectMask and (VK_IMAGE_ASPECT_DEPTH_BIT or VK_IMAGE_ASPECT_STENCIL_BIT)) != 0
    return if (depthOrStencil) VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL
    else VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
}

internal fun prepareSampledImagesForUniforms

internal fun prepareSampledImagesForUniforms(
    uniforms: Iterable<UpdatesDescriptorSet>,
    command: CommandBuffer
) {
    uniforms.forEach { uniform ->
        when (uniform) {
            is Module.TexturePointer -> (uniform.texture as? PreparesSampledImages)?.prepareSampledImagesFor(command)
            is Module.TextureArrayPointer -> (uniform.texture as? PreparesSampledImages)?.prepareSampledImagesFor(command)
        }
    }
}

class ModulePointer(var current: Module) : Renderable

class ModulePointer(var current: Module) : 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 {
         //   println("module '${current.parameters.name}' not valid / not rendering")
        }
    }

fun reload

body — 73 lines, from line 365
    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

        // Known before the build when the caller introspected (every box reload does), so a renamed block
        // can keep its buffer; otherwise worked out from the built module below.
        val nv = p.vertexIntrospection
        val nf = p.fragmentIntrospection
        val before = if (nv != null && nf != null) runCatching {
            graphicsShaderInterfaceTransition(current.vi, current.fi, nv, 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) {
                val oo = oTextureResolver(it)
                if (oo != null) oo
                else when (val uniform = current.uniforms[it.name]) {
                    is Module.TexturePointer -> uniform.texture to uniform.sampler
                    is Module.TextureArrayPointer -> uniform.texture to uniform.sampler
                    else -> {
                        lprintln("warning,graphics", "couldn't transfer over texture ${it.name}, falling back to originating resolution")
                        oTextureResolver(it)
                    }
                }
            } else try {
                when (val uniform = current.uniforms[it.name]) {
                    is Module.TexturePointer -> uniform.texture to uniform.sampler
                    is Module.TextureArrayPointer -> uniform.texture to uniform.sampler
                    else -> throw IllegalStateException("unsupported texture uniform type ${uniform?.javaClass}")
                }
            } catch (_: Exception) {
                lprintln("warning,graphics", "couldn't transfer over texture ${it.name}, falling back to originating resolution")
                oTextureResolver(it)
            }
        }

        val m = Module(p)
        if (m.valid) {
            val previous = current
            m.copyMeshesFrom(previous)
            m.enabled = previous.enabled
            val transition = before ?: runCatching { graphicsShaderInterfaceTransition(previous.vi, previous.fi, m.vi, m.fi) }.getOrNull()
            lastReloadReport = ModuleReloadReport(
                transition,
                ReloadCarryOver.finishAndNotify(this, transition, previous.uniforms.values, m.uniforms, previous.liveUniformStructures, m.liveUniformStructures)
            )
            current = m
            // Retire the module this one replaced now rather than when the GC finds it. cleanup() stops it
            // rendering at once and defers the handle destruction through the renderer's retirement queue,
            // so a frame still recording or executing the old pipeline is not pulled out from under.
            previous.cleanup()
            return true
        }
        // A module that failed part-way may already hold a shader module or descriptor layout.
        m.cleanup()
        return false
    }

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

    operator fun plusAssign(m: Pair<String, Mesh>) {
        current.meshes[m.first] = m.second
        m.second.inside = this
    }

operator fun plusAssign(m: Named<Mesh>)

    operator fun plusAssign(m: Named<Mesh>) {
        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: Named<Target>)

    @JvmName("plusAssign_namedTarget")
    operator fun plusAssign(m: Named<Target>) {
        current.after[m.name] = m.value
    }

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

    @JvmName("plusAssign_beforeTarget")
    operator fun plusAssign(m: Pair<String, BeforeTarget>) {
        current.before[m.first] = m.second.target
    }

operator fun plusAssign(m: Named<BeforeTarget>)

    @JvmName("plusAssign_namedBeforeTarget")
    operator fun plusAssign(m: Named<BeforeTarget>) {
        current.before[m.name] = m.value.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 Module.pointer() = ModulePointer(this)

class Module(val parameters: Parameters) : Renderable

class Module(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, Mesh>()

var before = linkedMapOf<String, Target>()

var after = linkedMapOf<String, Target>()

val vi

    val vi = parameters.vertexIntrospection
        ?: Introspection(
            parameters.vertex,
            ShaderKind.VERTEX_SHADER,
            errorHandler = parameters.errorHandler,
            sourceName = parameters.vertexSourceName,
            includeResolver = parameters.vertexIncludeResolver
        )

val fi

    val fi = parameters.fragmentIntrospection
        ?: Introspection(
            parameters.fragment,
            ShaderKind.FRAGMENT_SHADER,
            errorHandler = parameters.errorHandler,
            sourceName = parameters.fragmentSourceName,
            includeResolver = parameters.fragmentIncludeResolver
        )

val namedInputs = vi.inputs.associateBy { it.name }

val pushConstantSize = (vi.pushConstants + fi.pushConstants).maxOfOrNull { it.size } ?: 0

var uniforms: MutableMap<String, UpdatesDescriptorSet> = mutableMapOf()

var liveUniformStructures: MutableMap<String, Structure> = mutableMapOf()

class Uniform

    class Uniform(
        val name: String,
        override val binding: Bindable,
        override var source: BufferParameters? = null,
        val buffer: FrameBufferGroup,
        val kind: Int = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
    ) : UpdatesDescriptorSet {

override fun pushDescriptorSet

        override fun pushDescriptorSet(
            command: CommandBuffer, pipelineLayout: Long, bindPoint: Int
        ) {
            buffer.clean(false, command)
            MemoryStack.stackPush().use { stack ->
                // One read of handle + range, refused by name if the buffer was released or its
                // renderer disposed: MoltenVK dereferences a pushed handle without validating it.
                val bound = buffer.get().descriptorBinding()
                val bufferInfo = VkDescriptorBufferInfo.calloc(1, stack).buffer(bound.buffer).offset(0)
                    .range(bound.range)

                val write = VkWriteDescriptorSet.calloc(1, stack).sType(VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET)
                    .dstBinding(binding.binding).dstArrayElement(0).descriptorType(kind).descriptorCount(1)
                    .pBufferInfo(bufferInfo)

                vkCmdPushDescriptorSetKHR(
                    command.vkCommandBuffer, bindPoint, pipelineLayout, binding.set, write
                )
            }
        }
    }

class TexturePointer

    class TexturePointer(
        val name: String,
        override val binding: Bindable,
        override var source: BufferParameters? = null,
        val texture: HasImageViewArray,
        val sampler: TextureSampler,
    ) : UpdatesDescriptorSet {

override fun pushDescriptorSet

        override fun pushDescriptorSet(
            command: CommandBuffer, pipelineLayout: Long, bindPoint: Int
        ) {
            if (!command.inRenderPassInstance) {
                (texture as? PreparesSampledImages)?.prepareSampledImagesFor(command)
            }
            MemoryStack.stackPush().use { stack ->
                val views = texture.imageViewArray
                require(views.isNotEmpty()) { "TexturePointer ${name} has no image views" }

                val imageInfo = VkDescriptorImageInfo.calloc(1, stack)
                    .imageLayout(vkLayout(sampledImageLayout(views.first())))
                    .imageView(views.first().imageView.vkImageView)
                    .sampler(sampler.vkSampler)

                val write = VkWriteDescriptorSet.calloc(1, stack).sType(VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET)
                    .dstBinding(binding.binding).dstArrayElement(0)
                    .descriptorType(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER).descriptorCount(1).pImageInfo(imageInfo)

                vkCmdPushDescriptorSetKHR(
                    command.vkCommandBuffer, bindPoint, pipelineLayout, binding.set, write
                )
            }
        }
    }

class TextureArrayPointer

    class TextureArrayPointer(
        val name: String,
        override val binding: Bindable,
        override var source: BufferParameters? = null,
        val texture: HasImageViewArray,
        val sampler: TextureSampler,
    ) : UpdatesDescriptorSet {

override fun pushDescriptorSet

        override fun pushDescriptorSet(
            command: CommandBuffer, pipelineLayout: Long, bindPoint: Int
        ) {
            if (!command.inRenderPassInstance) {
                (texture as? PreparesSampledImages)?.prepareSampledImagesFor(command)
            }
            MemoryStack.stackPush().use { stack ->
                val views = texture.imageViewArray
                require(views.isNotEmpty()) { "TextureArrayPointer ${name} has no image views" }

                val infos = VkDescriptorImageInfo.calloc(views.size, stack)
                for (i in views.indices) {
                    infos[i].imageLayout(vkLayout(sampledImageLayout(views[i])))
                        .imageView(views[i].imageView.vkImageView).sampler(sampler.vkSampler)
                }

                val write = VkWriteDescriptorSet.calloc(1, stack).sType(VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET)
                    .dstBinding(binding.binding).dstArrayElement(0)
                    .descriptorType(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER).descriptorCount(views.size)
                    .pImageInfo(infos)

                vkCmdPushDescriptorSetKHR(
                    command.vkCommandBuffer, bindPoint, pipelineLayout, binding.set, write
                )
            }
        }
    }

class ImagePointer

    class ImagePointer(
        val name: String,
        override val binding: Bindable,
        override var source: BufferParameters? = null,
        val texture: HasImageView,
    ) : UpdatesDescriptorSet {

override fun pushDescriptorSet

        override fun pushDescriptorSet(
            command: CommandBuffer, pipelineLayout: Long, bindPoint: Int
        ) {
            MemoryStack.stackPush().use { stack ->
                val imageInfo = VkDescriptorImageInfo.calloc(1, stack).imageLayout(vkLayout(VK_IMAGE_LAYOUT_GENERAL))
                    .imageView(texture.imageView.vkImageView)

                val write = VkWriteDescriptorSet.calloc(1, stack).sType(VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET)
                    .dstBinding(binding.binding).dstArrayElement(0).descriptorType(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE)
                    .descriptorCount(1).pImageInfo(imageInfo)

                vkCmdPushDescriptorSetKHR(
                    command.vkCommandBuffer, bindPoint, pipelineLayout, binding.set, write
                )
            }
        }
    }

lateinit var vertexInputAttributes: VkVertexInputAttributeDescription.Buffer

lateinit var vertexInputBindings: VkVertexInputBindingDescription.Buffer

lateinit var vertexInputState: VkPipelineVertexInputStateCreateInfo

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

var vsm: Long = 0L

var fsm: Long = 0L

data class PipelineUpdate

    data class PipelineUpdate(
        val vertexIntrospection: Introspection,
        val fragmentIntrospection: Introspection,
        val vertexShaderModule: Long,
        val fragmentShaderModule: Long,
        val pipeline: Long
    )

init

body — 414 lines, from line 656
    init {
        try {
            if (!vi.valid || !fi.valid) throw IllegalStateException("Can't make a module without valid programs")

            vsm = createShaderModule(vi.code.bytecode()!!)
            fsm = createShaderModule(fi.code.bytecode()!!)

            // Name objects for tooling (if EXT_debug_utils is present)
            parameters.renderer.device.nameObject(vsm, VK10.VK_OBJECT_TYPE_SHADER_MODULE, "${parameters.name} VS")
            parameters.renderer.device.nameObject(fsm, VK10.VK_OBJECT_TYPE_SHADER_MODULE, "${parameters.name} FS")

            val setsNeeded = mutableMapOf<Int, MutableList<Int>>()

            // Build per-module sets for all sets used (including set 0)
            vi.ubos.forEach { setsNeeded.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }
            fi.ubos.forEach { setsNeeded.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }
            vi.ssbo.forEach { setsNeeded.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }
            fi.ssbo.forEach { setsNeeded.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }
            vi.textures.forEach { setsNeeded.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }
            fi.textures.forEach { setsNeeded.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }
            vi.images.forEach { setsNeeded.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }
            fi.images.forEach { setsNeeded.computeIfAbsent(it.set) { mutableListOf() }.add(it.binding) }

            // --- Dedup and sort per-set bindings (fix #2)
            if (setsNeeded.isNotEmpty()) {
                val keys = setsNeeded.keys.toList()
                for (k in keys) {
                    setsNeeded[k] = setsNeeded.getValue(k).distinct().sorted().toMutableList()
                }
            }

            uniforms = mutableMapOf()
            liveUniformStructures = mutableMapOf()

            vi.ubos.forEach { liveUniformStructures[it.name] = Structure().setMembers(it.members).reportingTo(parameters) }
            fi.ubos.forEach { liveUniformStructures[it.name] = Structure().setMembers(it.members).reportingTo(parameters) }
            vi.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>()

            // Create layouts/sets for each set >= 1
            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)
                    }

                    vi.ubos.forEach {
                        if (it.set == set && it.binding == bind) {
                            noteBinding(VK_SHADER_STAGE_VERTEX_BIT, 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)
                        }
                    }

                    vi.ssbo.forEach {
                        if (it.set == set && it.binding == bind) {
                            noteBinding(VK_SHADER_STAGE_VERTEX_BIT, 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)
                        }
                    }

                    vi.textures.forEach {
                        if (it.set == set && it.binding == bind) {
                            noteBinding(
                                VK_SHADER_STAGE_VERTEX_BIT,
                                VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
                                maxOf(1, it.arrayCount)
                            )
                        }
                    }
                    fi.textures.forEach {                      // fix #1
                        if (it.set == set && it.binding == bind) {
                            noteBinding(
                                VK_SHADER_STAGE_FRAGMENT_BIT,
                                VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
                                maxOf(1, it.arrayCount)
                            )
                        }
                    }

                    vi.images.forEach {                        // fix #1
                        if (it.set == set && it.binding == bind) {
                            noteBinding(VK_SHADER_STAGE_VERTEX_BIT, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE)
                        }
                    }
                    fi.images.forEach {                        // fix #1
                        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 binding $bind for set $set in $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 kind = 0
                val tag = mutableListOf<Introspected.Bindable>()
                vi.ubos.forEach {
                    if (it.set == set) {
                        kind = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; tag.add(it)
                    }
                }
                fi.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}")
                    try {
                        uniforms[it.name] = Uniform(it.name, it, source = buf, buf.toBuffer(parameters.renderer), kind)
                    } catch (_: NullPointerException) {
                        throw IllegalArgumentException("UBO ${it.name}, no longer resolves")
                    }
                }
            }

            // SSBOs (sets >= 1)
            setsNeeded.entries.forEach { (set, _) ->
                var kind = 0
                val tag = mutableListOf<Introspected.Bindable>()
                vi.ssbo.forEach {
                    if (it.set == set) {
                        kind = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; tag.add(it)
                    }
                }
                fi.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")
                    try {
                        uniforms[it.name] = Uniform(it.name, it, source = buf, buf.toBuffer(parameters.renderer), kind)
                    } catch (_: NullPointerException) {
                        throw IllegalArgumentException("buffer ${it.name} resolved to null")
                    }
                }
            }

            // Sampled textures (sets >= 1, combined image sampler is fine for local sets)
            setsNeeded.entries.forEach { (set, _) ->
                var kind = 0
                val tag = mutableListOf<Introspected.Bindable>()
                vi.textures.forEach {
                    if (it.set == set) {
                        kind = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; tag.add(it)
                    }
                }
                fi.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 expects ${it.arrayCount}"
                        }
                        uniforms[it.name] = TextureArrayPointer(it.name, it, source = null, texture, sampler)
                    }
                }
            }

            // Storage images (sets >= 1)
            setsNeeded.entries.forEach { (set, _) ->
                var kind = 0
                val tag = mutableListOf<Introspected.Bindable>()
                vi.images.forEach {
                    if (it.set == set) {
                        kind = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; tag.add(it)
                    }
                }
                fi.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)
                }
            }

            // --- Vertex input (unchanged)
            vertexInputAttributes = VkVertexInputAttributeDescription.calloc(vi.inputs.size)
            vertexInputBindings = VkVertexInputBindingDescription.calloc(vi.inputs.size)

            vi.inputs.forEachIndexed { index, input ->
                vertexInputAttributes[index].binding(index).location(input.location)
                    .format(vi.inputs[index].type.leftOrNull()!!.bufferFormat).offset(0)
                vertexInputBindings[index].binding(index).stride(vi.inputs[index].sizeInBytes.toInt())
                    .inputRate(VK_VERTEX_INPUT_RATE_VERTEX)
            }

            vertexInputState = VkPipelineVertexInputStateCreateInfo.calloc()
                .sType(VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO)
                .pVertexBindingDescriptions(vertexInputBindings).pVertexAttributeDescriptions(vertexInputAttributes)

            MemoryStack.stackPush().use { stack ->
                val shaderStages = VkPipelineShaderStageCreateInfo.calloc(2, stack)
                shaderStages[0].sType(VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO)
                    .stage(VK_SHADER_STAGE_VERTEX_BIT).pName(stack.UTF8("main")).module(vsm)
                shaderStages[1].sType(VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO)
                    .stage(VK_SHADER_STAGE_FRAGMENT_BIT).pName(stack.UTF8("main")).module(fsm)

                val ia = VkPipelineInputAssemblyStateCreateInfo.calloc(stack)
                    .sType(VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO)
                    .topology(parameters.primitiveType)
                when (parameters.primitiveType) {
                    VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_LINE_STRIP, VK_PRIMITIVE_TOPOLOGY_LINE_STRIP_WITH_ADJACENCY, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY -> ia.primitiveRestartEnable(
                        true
                    )
                }

                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).dstAlphaBlendFactor(parameters.dstAlpha)   // fix #3
                        .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, 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_VERTEX_BIT or VK_SHADER_STAGE_FRAGMENT_BIT).offset(0)
                        .size(pushConstantSize)
                }

                // ---- Build pipeline layout for module-owned sets (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 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).pVertexInputState(vertexInputState).pInputAssemblyState(ia)
                        .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"
                )

                liveUniformStructures.forEach { (name, structure) ->
                    val target = (uniforms[name] as 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)
        }
    }

fun buildPipelineUpdate

body — 138 lines, from line 1071
    fun buildPipelineUpdate(
        newVertex: Introspection,
        newFragment: Introspection
    ): PipelineUpdate {
        var newVsm = 0L
        var newFsm = 0L
        var newPipeline = 0L
        try {
            newVsm = createShaderModule(newVertex.code.bytecode()!!)
            newFsm = createShaderModule(newFragment.code.bytecode()!!)

            parameters.renderer.device.nameObject(newVsm, VK10.VK_OBJECT_TYPE_SHADER_MODULE, "${parameters.name} VS update")
            parameters.renderer.device.nameObject(newFsm, VK10.VK_OBJECT_TYPE_SHADER_MODULE, "${parameters.name} FS update")

            MemoryStack.stackPush().use { stack ->
                val shaderStages = VkPipelineShaderStageCreateInfo.calloc(2, stack)
                shaderStages[0].sType(VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO)
                    .stage(VK_SHADER_STAGE_VERTEX_BIT).pName(stack.UTF8("main")).module(newVsm)
                shaderStages[1].sType(VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO)
                    .stage(VK_SHADER_STAGE_FRAGMENT_BIT).pName(stack.UTF8("main")).module(newFsm)

                val ia = VkPipelineInputAssemblyStateCreateInfo.calloc(stack)
                    .sType(VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO)
                    .topology(parameters.primitiveType)
                when (parameters.primitiveType) {
                    VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN,
                    VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP,
                    VK_PRIMITIVE_TOPOLOGY_LINE_STRIP,
                    VK_PRIMITIVE_TOPOLOGY_LINE_STRIP_WITH_ADJACENCY,
                    VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY -> ia.primitiveRestartEnable(true)
                }

                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).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, 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)

                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).pVertexInputState(vertexInputState).pInputAssemblyState(ia)
                        .pViewportState(vp).pRasterizationState(rs).pMultisampleState(ms).pColorBlendState(cb)
                        .pDynamicState(dyn).layout(pipelineLayout).renderPass(0).pNext(renderInfo.address())

                ds?.let { gp.pDepthStencilState(it) }

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

            return PipelineUpdate(newVertex, newFragment, newVsm, newFsm, newPipeline)
        } catch (t: Throwable) {
            destroyPipelineHandles(newPipeline, 0L, newVsm, newFsm)
            throw t
        }
    }

fun installPipelineUpdate(update: PipelineUpdate)

    fun installPipelineUpdate(update: PipelineUpdate) {
        val oldPipeline = pipeline
        val oldVsm = vsm
        val oldFsm = fsm

        pipeline = update.pipeline
        vsm = update.vertexShaderModule
        fsm = update.fragmentShaderModule

        parameters.vertexIntrospection = update.vertexIntrospection
        parameters.fragmentIntrospection = update.fragmentIntrospection

        pipelineDisposeToken.updateCleanup(snapshotPipelineCleanup())

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

fun destroyUnusedPipelineUpdate(update: PipelineUpdate)

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

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

body — 84 lines, from line 1233
    override fun render(command: CommandBuffer, bounds: Rectanglef) {
        if (!valid || !enabled) return
        MemoryStack.stackPush().use { stack ->
            val cmd = command.vkCommandBuffer
            val renderer = parameters.renderer
            val frameIndex = renderer.currentImage
            val profiler = renderer.getProfiler()
            val fieldProbe = parameters.fieldProbe?.takeIf { FIELD_PROBE_BUFFER_NAME in uniforms }

            fieldProbe?.beforeShaderUse(renderer, frameIndex)

            // Begin label for the whole module render (if EXT_debug_utils is available)
            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 marks = if (profiler != null) 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)
                }

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

                VK13.vkCmdSetDepthTestEnable(cmd, if (parameters.depth == 0) false else parameters.depthTest)
                if (renderer.device.supportsExtendedDynamicState) {
                    VK13.vkCmdSetPrimitiveTopology(cmd, parameters.primitiveType)
                    VK13.vkCmdSetCullMode(cmd, VK_CULL_MODE_NONE)
                    VK13.vkCmdSetFrontFace(cmd, VK_FRONT_FACE_CLOCKWISE)
                }
                if (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)
                }

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

                meshes.forEach { (_, mesh) ->
                    mesh.draw(this, command, pushConstantBuffer)
                }

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

                fieldProbe?.let {
                    graphicsHostReadBarrier(command, it.bufferForImage(renderer, frameIndex))
                    it.afterShaderUse(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 }
                    renderer.registerModuleMarks(frameIndex, marks)
                }
            } finally {
                command.endLabel() // close the module render region
            }
        }
    }

fun enable()

    fun enable() {
        enabled = true
    }

fun disable()

    fun disable() {
        enabled = false
    }

fun copyMeshesFrom(old: Module)

    fun copyMeshesFrom(old: Module) {
        old.meshes.entries.removeIf {
            meshes[it.key] = it.value
            it.value.resize(it.value.numVert, it.value.numTriangle, 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 vsmHandle = vsm
        val fsmHandle = fsm
        return {
            if (p != 0L) VK10.vkDestroyPipeline(dev, p, null)
            if (layout != 0L) VK10.vkDestroyPipelineLayout(dev, layout, null)
            if (vsmHandle != 0L) VK10.vkDestroyShaderModule(dev, vsmHandle, null)
            if (fsmHandle != 0L) VK10.vkDestroyShaderModule(dev, fsmHandle, null)
            sets.forEach { it.cleanup() }
        }
    }

fun cleanup()

Release this module's pipeline, pipeline layout, shader modules and descriptor layouts.

The module stops rendering at once (valid goes false, so no new frame binds it), but while its renderer is alive the handles are destroyed through the renderer's retirement queue, after the frames that may have recorded or be executing them have retired. Destroying them here, as this used to, was a SIGSEGV on a render worker still recording a bind (MVKCmdBindGraphicsPipeline, reproduced 1 of 1), and a VUID-vkDestroyPipeline-pipeline-00765 for frames in flight. After the renderer has closed (its device idle) they go at once. See dev_docs/renderer_teardown_order.md, "Immediate cleanup mid-frame".

    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, vsm, fsm)
        pipeline = 0L
        pipelineLayout = 0L
        vsm = 0L
        fsm = 0L
        ownedDescriptorLayouts.forEach { it.cleanup() }
        ownedDescriptorLayouts.clear()
    }

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

Hands SPIR-V to Vulkan without staging it on LWJGL's 64 KiB MemoryStack.

Generated shaders routinely cross that boundary. Shaderc already returns a native, word-aligned buffer, so the common path can point Vulkan at it directly. A non-direct caller is copied into explicitly managed native memory; only the two small Vulkan structs remain on the stack.

    private fun createShaderModule(code: ByteBuffer): Long {
        val direct = if (code.isDirect) null else MemoryUtil.memAlloc(code.remaining()).put(code.duplicate()).flip()
        try {
            MemoryStack.stackPush().use { stack ->
                val ci = VkShaderModuleCreateInfo.calloc(stack)
                    .sType(VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO)
                    .pCode(direct ?: code)
                val lp = stack.mallocLong(1)
                vkCheck(
                    vkCreateShaderModule(parameters.renderer.device.vkDevice, ci, null, lp),
                    "Failed to create shader module"
                )
                return lp[0]
            }
        } finally {
            if (direct != null) MemoryUtil.memFree(direct)
        }
    }

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

    operator fun plusAssign(m: Pair<String, Mesh>) {
        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
    }

}

value class BeforeTarget(val target: Target)

@JvmInline
value class BeforeTarget(val target: Target)

fun before(a: (CommandBuffer, Int) -> Unit) = BeforeTarget(object : Target

fun before(a: (CommandBuffer, Int) -> Unit) = BeforeTarget(object : Target {
    override fun render(command: CommandBuffer, index: Int) = a(command, index)
})

helper (unchanged)

fun parseCompilerException(exception: CompilerError, accept: (String, Int, String) -> Unit) {

fun parseCompilerException(exception: CompilerError, accept: (String, Int, String) -> Unit) {
    val message = exception.message
    val regex = """:(\d+):""".toRegex()
    message.split("\n").forEach {
        val f = regex.find(it)
        if (f != null) {
            val line = f.groupValues[1].toInt()
            val (file, msg) = it.split(regex)
            accept(file.split("/").last(), line, msg)
        } else {
            if (it.trim().isNotEmpty()) accept("", -1, it)
        }
    }
}