Skip to content

ComputeMesh.kt

src/jvmMain/kotlin/afield/graphics/render/ComputeMesh.kt · 459 lines

ComputeMesh.kt (sync2 version)

package afield.graphics.render

import afield.document.context.fromContext
import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.buffers.MissingRefException
import afield.graphics.vulkan.buffers.Structure
import afield.graphics.vulkan.buffers.VulkanBuffer
import afield.graphics.vulkan.buffers.bindPushConstant
import afield.graphics.vulkan.buffers.getBoundPushConstant
import afield.graphics.vulkan.buffers.getPushConstant
import afield.graphics.vulkan.buffers.setPushConstant
import afield.graphics.vulkan.device.CommandBuffer
import afield.app.diagnostics.logging.lprintln
import org.lwjgl.system.MemoryStack
import org.lwjgl.vulkan.VK10
import org.lwjgl.vulkan.VK10.*
import org.lwjgl.vulkan.VK13.*
import org.lwjgl.vulkan.VkBufferMemoryBarrier2
import org.lwjgl.vulkan.VkDependencyInfo
import org.lwjgl.vulkan.VkMemoryBarrier2
import java.nio.ByteBuffer
import kotlin.reflect.KProperty

ComputeMesh is a compute dispatch with its own push constants inside a ComputeModule; the rest of the file is the barriers that order compute work against transfers, graphics and host reads.

Any ComputeTask can be added to a module, and computeTask wraps a lambda as one. computeBarrier and zeroBuffer are ready-made tasks; computeMemoryBarrier, graphicsMemoryBarrier, transferMemoryBarrier, hostReadBarrier and graphicsHostReadBarrier record a Vulkan synchronization-2 barrier into a command buffer.

interface ComputeTask

fun compute(inside: ComputeModule, cmd: CommandBuffer, pushConstantBuffer: ByteBuffer)

fun computeTask(c: (inside: ComputeModule, cmd: CommandBuffer, pushConstantBuffer: ByteBuffer) -> Unit): ComputeTask {

fun computeTask(c: (inside: ComputeModule, cmd: CommandBuffer, pushConstantBuffer: ByteBuffer) -> Unit): ComputeTask {
    return object : ComputeTask {
        override fun compute(
            inside: ComputeModule,
            cmd: CommandBuffer,
            pushConstantBuffer: ByteBuffer
        ) {
            c(inside, cmd, pushConstantBuffer)
        }
    }
}

fun computeBarrier(graphics: Boolean = false, buffer: VulkanBuffer? = null): ComputeTask

Publish preceding compute shader writes to subsequent compute work on the same queue. With graphics true, also publish to graphics shader reads/writes. Optional buffer limits the dependency to that buffer; null is a global memory dependency. This does not transfer ownership between queues and does not make results visible to host reads.

fun computeBarrier(graphics: Boolean = false, buffer: VulkanBuffer? = null): ComputeTask =
    computeTask { _, cmd, _ ->
        computeMemoryBarrier(cmd, buffer)
        if (graphics) graphicsMemoryBarrier(cmd, buffer)
    }

fun zeroBuffer(v: VulkanBuffer, barrier: Boolean = true)

fun zeroBuffer(v: VulkanBuffer, barrier: Boolean = true) = computeTask { _, cmd, _ ->
    v.zero(cmd)
    transferMemoryBarrier(cmd, v)
}

fun transferMemoryBarrier(cmd: CommandBuffer, buffer: VulkanBuffer? = null)

TRANSFER->SHADER-READ (including task/mesh, fragment and compute). Falls back to a MemoryBarrier2 if buffer==null.

body — 43 lines, from line 70
fun transferMemoryBarrier(cmd: CommandBuffer, buffer: VulkanBuffer? = null) {
    MemoryStack.stackPush().use { stack ->
        val dep = VkDependencyInfo.calloc(stack).sType(VK_STRUCTURE_TYPE_DEPENDENCY_INFO)

        if (buffer != null) {
            val b = VkBufferMemoryBarrier2.calloc(1, stack)
            b[0]
                .sType(VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2)
                .srcStageMask(VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT)
                .srcAccessMask(VK_ACCESS_2_TRANSFER_WRITE_BIT)
                .dstStageMask(
                    VK_PIPELINE_STAGE_2_PRE_RASTERIZATION_SHADERS_BIT or
                            VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT or
                            VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT
                )
                .dstAccessMask(VK_ACCESS_2_SHADER_READ_BIT)
                .srcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
                .dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
                .buffer(buffer.buffer)
                .offset(0L)
                .size(buffer.requestedSize)

            dep.pBufferMemoryBarriers(b)
        } else {
            // Generic memory dependency (no specific buffer)
            val m = VkMemoryBarrier2.calloc(1, stack)
            m
                .sType(VK_STRUCTURE_TYPE_MEMORY_BARRIER_2)
                .srcStageMask(VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT)
                .srcAccessMask(VK_ACCESS_2_TRANSFER_WRITE_BIT)
                .dstStageMask(
                    VK_PIPELINE_STAGE_2_PRE_RASTERIZATION_SHADERS_BIT or
                            VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT or
                            VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT
                )
                .dstAccessMask(VK_ACCESS_2_SHADER_READ_BIT)

            dep.pMemoryBarriers(m)
        }

        vkCmdPipelineBarrier2(cmd.vkCommandBuffer, dep)
    }
}

fun computeMemoryBarrier(cmd: CommandBuffer, buffer: VulkanBuffer? = null)

COMPUTE write -> COMPUTE read / write.

fun computeMemoryBarrier(cmd: CommandBuffer, buffer: VulkanBuffer? = null) {
    MemoryStack.stackPush().use { stack ->
        val dep = VkDependencyInfo.calloc(stack).sType(VK_STRUCTURE_TYPE_DEPENDENCY_INFO)
        if (buffer != null) {
            val b = VkBufferMemoryBarrier2.calloc(1, stack)
            b[0]
                .sType(VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2)
                .srcStageMask(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT)
                .srcAccessMask(VK_ACCESS_2_SHADER_WRITE_BIT)
                .dstStageMask(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT)
                .dstAccessMask(VK_ACCESS_2_SHADER_READ_BIT or VK_ACCESS_2_SHADER_WRITE_BIT)
                .srcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
                .dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
                .buffer(buffer.buffer)
                .offset(0L)
                .size(buffer.requestedSize)

            dep.pBufferMemoryBarriers(b)
        } else {
            val m = VkMemoryBarrier2.calloc(1, stack)
            m[0]
                .sType(VK_STRUCTURE_TYPE_MEMORY_BARRIER_2)
                .srcStageMask(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT)
                .srcAccessMask(VK_ACCESS_2_SHADER_WRITE_BIT)
                .dstStageMask(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT)
                .dstAccessMask(VK_ACCESS_2_SHADER_READ_BIT)

            dep.pMemoryBarriers(m)
        }
        vkCmdPipelineBarrier2(cmd.vkCommandBuffer, dep)
    }
}

fun graphicsMemoryBarrier(cmd: CommandBuffer, buffer: VulkanBuffer? = null)

COMPUTE write -> graphics shader read / write.

fun graphicsMemoryBarrier(cmd: CommandBuffer, buffer: VulkanBuffer? = null) {
    MemoryStack.stackPush().use { stack ->
        val dep = VkDependencyInfo.calloc(stack).sType(VK_STRUCTURE_TYPE_DEPENDENCY_INFO)
        if (buffer != null) {
            val b = VkBufferMemoryBarrier2.calloc(1, stack)
            b[0]
                .sType(VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2)
                .srcStageMask(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT)
                .srcAccessMask(VK_ACCESS_2_SHADER_WRITE_BIT)
                .dstStageMask(VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT)
                .dstAccessMask(VK_ACCESS_2_SHADER_READ_BIT or VK_ACCESS_2_SHADER_WRITE_BIT)
                .srcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
                .dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
                .buffer(buffer.buffer)
                .offset(0L)
                .size(buffer.requestedSize)

            dep.pBufferMemoryBarriers(b)
        } else {
            val m = VkMemoryBarrier2.calloc(1, stack)
            m[0]
                .sType(VK_STRUCTURE_TYPE_MEMORY_BARRIER_2)
                .srcStageMask(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT)
                .srcAccessMask(VK_ACCESS_2_SHADER_WRITE_BIT)
                .dstStageMask(VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT)
                .dstAccessMask(VK_ACCESS_2_SHADER_READ_BIT or VK_ACCESS_2_SHADER_WRITE_BIT)

            dep.pMemoryBarriers(m)
        }
        vkCmdPipelineBarrier2(cmd.vkCommandBuffer, dep)
    }
}

fun computeMemoryBarrier(cmd: CommandBuffer, buffer: Long? = null, size: Long = VK_WHOLE_SIZE)

fun computeMemoryBarrier(cmd: CommandBuffer, buffer: Long? = null, size: Long = VK_WHOLE_SIZE) {
    MemoryStack.stackPush().use { stack ->
        val dep = VkDependencyInfo.calloc(stack).sType(VK_STRUCTURE_TYPE_DEPENDENCY_INFO)
        if (buffer != null) {
            val b = VkBufferMemoryBarrier2.calloc(1, stack)
            b[0]
                .sType(VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2)
                .srcStageMask(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT)
                .srcAccessMask(VK_ACCESS_2_SHADER_WRITE_BIT)
                .dstStageMask(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT)
                .dstAccessMask(VK_ACCESS_2_SHADER_READ_BIT or VK_ACCESS_2_SHADER_WRITE_BIT)
                .srcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
                .dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
                .buffer(buffer)
                .offset(0L)
                .size(size)

            dep.pBufferMemoryBarriers(b)
        } else {
            val m = VkMemoryBarrier2.calloc(1, stack)
            m[0]
                .sType(VK_STRUCTURE_TYPE_MEMORY_BARRIER_2)
                .srcStageMask(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT)
                .srcAccessMask(VK_ACCESS_2_SHADER_WRITE_BIT)
                .dstStageMask(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT)
                .dstAccessMask(VK_ACCESS_2_SHADER_READ_BIT)

            dep.pMemoryBarriers(m)
        }
        vkCmdPipelineBarrier2(cmd.vkCommandBuffer, dep)
    }
}

fun graphicsMemoryBarrier(cmd: CommandBuffer, buffer: Long? = null, size: Long = VK_WHOLE_SIZE)

fun graphicsMemoryBarrier(cmd: CommandBuffer, buffer: Long? = null, size: Long = VK_WHOLE_SIZE) {
    MemoryStack.stackPush().use { stack ->
        val dep = VkDependencyInfo.calloc(stack).sType(VK_STRUCTURE_TYPE_DEPENDENCY_INFO)
        if (buffer != null) {
            val b = VkBufferMemoryBarrier2.calloc(1, stack)
            b[0]
                .sType(VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2)
                .srcStageMask(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT)
                .srcAccessMask(VK_ACCESS_2_SHADER_WRITE_BIT)
                .dstStageMask(VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT)
                .dstAccessMask(VK_ACCESS_2_SHADER_READ_BIT or VK_ACCESS_2_SHADER_WRITE_BIT)
                .srcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
                .dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
                .buffer(buffer)
                .offset(0L)
                .size(size)

            dep.pBufferMemoryBarriers(b)
        } else {
            val m = VkMemoryBarrier2.calloc(1, stack)
            m[0]
                .sType(VK_STRUCTURE_TYPE_MEMORY_BARRIER_2)
                .srcStageMask(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT)
                .srcAccessMask(VK_ACCESS_2_SHADER_WRITE_BIT)
                .dstStageMask(VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT)
                .dstAccessMask(VK_ACCESS_2_SHADER_READ_BIT or VK_ACCESS_2_SHADER_WRITE_BIT)

            dep.pMemoryBarriers(m)
        }
        vkCmdPipelineBarrier2(cmd.vkCommandBuffer, dep)
    }
}

fun hostReadBarrier(cmd: CommandBuffer, buffer: VulkanBuffer? = null)

COMPUTE write -> HOST read.

fun hostReadBarrier(cmd: CommandBuffer, buffer: VulkanBuffer? = null) {
    MemoryStack.stackPush().use { stack ->
        val dep = VkDependencyInfo.calloc(stack).sType(VK_STRUCTURE_TYPE_DEPENDENCY_INFO)
        if (buffer != null) {
            val b = VkBufferMemoryBarrier2.calloc(1, stack)
            b[0]
                .sType(VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2)
                .srcStageMask(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT)
                .srcAccessMask(VK_ACCESS_2_SHADER_WRITE_BIT)
                .dstStageMask(VK_PIPELINE_STAGE_2_HOST_BIT)
                .dstAccessMask(VK_ACCESS_2_HOST_READ_BIT)
                .srcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
                .dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
                .buffer(buffer.buffer)
                .offset(0L)
                .size(buffer.requestedSize)

            dep.pBufferMemoryBarriers(b)
        } else {
            val m = VkMemoryBarrier2.calloc(1, stack)
            m[0]
                .sType(VK_STRUCTURE_TYPE_MEMORY_BARRIER_2)
                .srcStageMask(VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT)
                .srcAccessMask(VK_ACCESS_2_SHADER_WRITE_BIT)
                .dstStageMask(VK_PIPELINE_STAGE_2_HOST_BIT)
                .dstAccessMask(VK_ACCESS_2_HOST_READ_BIT)
            dep.pMemoryBarriers(m)
        }
        vkCmdPipelineBarrier2(cmd.vkCommandBuffer, dep)
    }
}

fun graphicsHostReadBarrier(cmd: CommandBuffer, buffer: VulkanBuffer? = null)

GRAPHICS write -> HOST read.

fun graphicsHostReadBarrier(cmd: CommandBuffer, buffer: VulkanBuffer? = null) {
    if (buffer != null && cmd.inRenderPassInstance) {
        cmd.deferGraphicsHostReadBarrier(buffer)
        return
    }
    MemoryStack.stackPush().use { stack ->
        val dep = VkDependencyInfo.calloc(stack).sType(VK_STRUCTURE_TYPE_DEPENDENCY_INFO)
        if (buffer != null) {
            val b = VkBufferMemoryBarrier2.calloc(1, stack)
            b[0]
                .sType(VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2)
                .srcStageMask(VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT)
                .srcAccessMask(VK_ACCESS_2_SHADER_WRITE_BIT)
                .dstStageMask(VK_PIPELINE_STAGE_2_HOST_BIT)
                .dstAccessMask(VK_ACCESS_2_HOST_READ_BIT)
                .srcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
                .dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
                .buffer(buffer.buffer)
                .offset(0L)
                .size(buffer.requestedSize)

            dep.pBufferMemoryBarriers(b)
        } else {
            val m = VkMemoryBarrier2.calloc(1, stack)
            m[0]
                .sType(VK_STRUCTURE_TYPE_MEMORY_BARRIER_2)
                .srcStageMask(VK_PIPELINE_STAGE_2_ALL_GRAPHICS_BIT)
                .srcAccessMask(VK_ACCESS_2_SHADER_WRITE_BIT)
                .dstStageMask(VK_PIPELINE_STAGE_2_HOST_BIT)
                .dstAccessMask(VK_ACCESS_2_HOST_READ_BIT)
            dep.pMemoryBarriers(m)
        }
        vkCmdPipelineBarrier2(cmd.vkCommandBuffer, dep)
    }
}

class ComputeMesh(var renderer: Renderer = fromContext()) : ComputeTask

this has to agree with the vertexbufferstructure that was used for this pipeline (right now)

class ComputeMesh(var renderer: Renderer = fromContext()) : ComputeTask {

var indirect: afield.graphics.vulkan.buffers.FrameBufferGroup? = null

Optional GPU-written VkDispatchIndirectCommand; no CPU count readback.

var indirectOffset: Long = 0L

class FromUniform

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

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

class BoundFromStructure<T>

operator fun setValue(inside: ComputeMesh, property: KProperty<*>, any: () -> T) {

        operator fun setValue(inside: ComputeMesh, property: KProperty<*>, any: () -> T) {
            if (inside.push.offsets.isEmpty()) inside.initializePushConstantsFromModule(inside.inside)
            inside.push.bindPushConstant(property.name, any) {
                throw IllegalArgumentException(" can't find ${property.name} in mesh ${inside.inside.current.parameters.name} as a push_constant")
            }
        }

operator fun getValue(inside: ComputeMesh, property: KProperty<*>): () -> T {

        operator fun getValue(inside: ComputeMesh, property: KProperty<*>): () -> T {
            if (inside.push.offsets.isEmpty()) inside.initializePushConstantsFromModule(inside.inside)
            return inside.push.getBoundPushConstant(property.name) {
                throw IllegalArgumentException(" can't find ${property.name} in mesh ${inside.inside.current.parameters.name} as a push_constant or a UBO structure")
            }
        }
    }

constructor(renderer: Renderer = fromContext(), pointer: ComputeModulePointer = fromContext()) : this(renderer)

    constructor(renderer: Renderer = fromContext(), pointer: ComputeModulePointer = fromContext()) : this(renderer) {
        this.inside = pointer
    }

lateinit var inside: ComputeModulePointer

var update = afield.graphics.vulkan.util.MapOfSequences<String>()

Named sequence hooks advanced once per compute call, before push constants and dispatch.

var push = Structure()

Push-constant values and bindings written to the buffer before each dispatch.

var x = 1

var y = 1

var z = 1

internal fun initializePushConstantsFromModule(inside: ComputeModulePointer)

    internal fun initializePushConstantsFromModule(inside: ComputeModulePointer) {
        for (pc in inside.current.ci.pushConstants) {
            push.setMembers(pc.members)
        }
    }

class FromStructure<T>

operator fun setValue(inside: ComputeMesh, property: KProperty<*>, any: T)

        operator fun setValue(inside: ComputeMesh, property: KProperty<*>, any: T) {
            if (inside.push.offsets.isEmpty()) inside.initializePushConstantsFromModule(inside.inside)
            inside.push.setPushConstant(property.name, any) {
                throw IllegalArgumentException(" can't find ${property.name} in mesh ${inside.inside.current.parameters.name} as a push_constant")
            }
        }

operator fun getValue(inside: ComputeMesh, property: KProperty<*>): T

        operator fun getValue(inside: ComputeMesh, property: KProperty<*>): T {
            if (inside.push.offsets.isEmpty()) inside.initializePushConstantsFromModule(inside.inside)
            return inside.push.getPushConstant(property.name) {
                throw IllegalArgumentException(" can't find ${property.name} in mesh ${inside.inside.current.parameters.name} as a push_constant or a UBO structure")
            }
        }
    }

override fun compute(inside: ComputeModule, cmd: CommandBuffer, pushConstantBuffer: ByteBuffer)

body — 75 lines, from line 384
    override fun compute(inside: ComputeModule, cmd: CommandBuffer, pushConstantBuffer: ByteBuffer) {
        if (inside != this.inside.current) lprintln("warning,graphics", "ComputeMesh was built in a different module $inside != ${this.inside.current}")
        update.update()

        MemoryStack.stackPush().use { _ ->
            fun pushConstantsForCurrentDispatch(): Boolean {
                if (pushConstantBuffer.limit() <= 0) return true
                for (pc in inside.ci.pushConstants) {
                    push.setMembers(pc.members)
                }
                try {
                    pushConstantBuffer.clear()
                    push.writeToBuffer(pushConstantBuffer, cmd) {
                        throw MissingRefException(afield.graphics.vulkan.buffers.unassignedMemberMessage(it))
                    }
                } catch (e: Exception) {
                    inside.parameters.errorHandler(e.message ?: "push constant error", e)
                    lprintln("error,graphics", "push constant error, not dispatching compute mesh : ${e.message}")
                    return false
                }
                pushConstantBuffer.clear()
                vkCmdPushConstants(
                    cmd.vkCommandBuffer,
                    inside.pipelineLayout,
                    VK10.VK_SHADER_STAGE_COMPUTE_BIT,
                    0,
                    pushConstantBuffer
                )
                return true
            }

            fun dispatchForCurrentWriter(dispatchX: Int?, dispatchY: Int?, dispatchZ: Int?) {
                indirect?.let {
                    require(indirectOffset >= 0L && indirectOffset % 4L == 0L)
                    it.getGPUAddress(cmd)
                    require(indirectOffset + 12L <= it.getLastWritten().requestedSize)
                    vkCmdDispatchIndirect(cmd.vkCommandBuffer, it.getLastWritten().buffer, indirectOffset)
                    return
                }
                val xx = dispatchX ?: x
                val yy = dispatchY ?: y
                val zz = dispatchZ ?: z
                if (xx * yy * zz > 0) {
                    vkCmdDispatch(cmd.vkCommandBuffer, xx, yy, zz)
                } else {
                    lprintln("warning,graphics", "ComputeMesh has no work to do $xx $yy $zz")
                }
            }

            try {
                forEachFanOutDispatch(
                    renderer = renderer,
                    push = push,
                    onNoFanOut = {
                        if (pushConstantsForCurrentDispatch()) {
                            dispatchForCurrentWriter(null, null, null)
                        }
                    },
                    onWriter = { writer ->
                        if (pushConstantsForCurrentDispatch()) {
                            dispatchForCurrentWriter(
                                dispatchX = writer.publication.dispatchX,
                                dispatchY = writer.publication.dispatchY,
                                dispatchZ = writer.publication.dispatchZ
                            )
                        }
                    }
                )
            } catch (e: Exception) {
                inside.parameters.errorHandler(e.message ?: "fan-out dispatch error", e)
                lprintln("error,graphics", "fan-out dispatch error, not dispatching compute mesh : ${e.message}")
            }
        }
    }
}