Skip to content

Image.kt

src/jvmMain/kotlin/afield/graphics/texture/Image.kt · 605 lines

Image wraps a VkImage and its memory, created from an Image.ImageData description, including the external-memory exports used to hand frames to other processes.

Alongside ordinary device-local images, an Image can be allocated for export as an opaque fd (CUDA/NVENC on Linux, see VulkanExternalMemory), as a dma-buf with a DRM format modifier (see LinuxDmaBufExport) or as a Metal IOSurface (see MetalIOSurface). The file also holds the format helpers vkFormatBytesPerPixel and vkFormatBytesPerPixelOrNull, and image_layout_transition for recording layout barriers.

package afield.graphics.texture

import afield.graphics.vulkan.device.vkLayout
import afield.graphics.vulkan.device.nameObject
import org.lwjgl.system.MemoryStack
import org.lwjgl.vulkan.*
import afield.graphics.vulkan.device.Device
import afield.graphics.vulkan.util.memoryTypeFromProperties
import afield.graphics.vulkan.util.vkCheck
import afield.graphics.vulkan.util.AutoDisposer
import org.lwjgl.vulkan.VK10.VK_QUEUE_FAMILY_IGNORED
import org.lwjgl.vulkan.VK13.VK_STRUCTURE_TYPE_DEPENDENCY_INFO
import org.lwjgl.vulkan.VK13.VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2
import org.lwjgl.vulkan.VK13.vkCmdPipelineBarrier2

class Image(private val device: Device, val imageData: ImageData)

class Image(private val device: Device, val imageData: ImageData) {

var format: Int = 0

The VkFormat the image was created with, copied from the Image.ImageData it was built from so it can be read back without that description to hand.

var mipLevels: Int = 0

How many mip levels the image has, copied from its Image.ImageData. The levels are allocated; filling them is the caller's own blit chain.

var vkImage: Long = 0

The VkImage handle. Zero until the constructor has allocated, and zero again after cleanup.

var vkMemory: Long = 0

The VkDeviceMemory allocated for vkImage and bound to it. Bound even when the pixels live in an imported IOSurface, where MoltenVK may not need it.

var memorySize: Long

The size the driver asked for to back this image, which is the allocation size CUDA's cuImportExternalMemory has to be given when the image is exported as an opaque fd (the Linux NVENC path, see VulkanExternalMemory). Read from the image's memory requirements, so it is the real figure rather than a computed one, and it is set for every image.

    var memorySize: Long = 0
        private set

var linearRowPitch: Long

The row pitch of the image's linear layout, for NVENC's NV_ENC_REGISTER_RESOURCE.pitch. Queried from the image rather than computed as width times bytes per pixel, because the driver may pad a row out to an alignment. Set only for an image whose Image.ImageData asked for exportOpaqueFd, and zero otherwise.

    var linearRowPitch: Long = 0
        private set

var drmFormatModifier: Long

The DRM format modifier the driver chose, which is the tiling an importing process has to be told about before it can read the dma-buf. The driver picks from the modifiers Field offers, so the choice is read back rather than assumed. Set only for an image whose Image.ImageData asked for exportDmaBuf — see LinuxDmaBufExport.

    var drmFormatModifier: Long = 0
        private set

var drmPlaneLayouts: List<DmaBufPlaneLayout>

The offset and pitch of each memory plane under the modifier the driver chose, read back alongside drmFormatModifier and sent with the handle so an importer can address the planes.

    var drmPlaneLayouts: List<DmaBufPlaneLayout> = emptyList()
        private set

    private val autoDisposeToken = AutoDisposer.register(this, "Image(format=${imageData.format}, ${imageData.width}x${imageData.height})", owner = device) { /* set after init */ }

init

body — 200 lines, from line 72
    init {
        MemoryStack.stackPush().use { stack ->
            this.format = imageData.format
            this.mipLevels = imageData.mipLevels

            val imageCreateInfo = VkImageCreateInfo.calloc(stack)
                .sType(VK10.VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO)
                .flags(
                    imageData.flags or
                        if (imageData.aliasSingleLayerDescriptors && device.supportsMaintenance11) {
                            KHRMaintenance11.VK_IMAGE_CREATE_ALIAS_SINGLE_LAYER_DESCRIPTOR_BIT_KHR
                        } else {
                            0
                        }
                )
                .imageType(VK10.VK_IMAGE_TYPE_2D)
                .format(format)
                .extent { it: VkExtent3D ->
                    it
                        .width(imageData.width)
                        .height(imageData.height)
                        .depth(1)
                }
                .mipLevels(mipLevels)
                .arrayLayers(imageData.arrayLayers)
                .samples(imageData.sampleCount)
                .initialLayout(VK10.VK_IMAGE_LAYOUT_UNDEFINED)
                .sharingMode(VK10.VK_SHARING_MODE_EXCLUSIVE)
                // NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR (the Linux capture path — see
                // VulkanExternalMemory.kt / native/field-encode-linux) needs a flat linear buffer +
                // row pitch, not NVIDIA's private optimal-tiling layout, so exported images use
                // LINEAR tiling. Costs a little render throughput vs OPTIMAL; irrelevant at the
                // resolutions/framerates this streams (see dev_docs/vision_frontend.md's "ship the
                // fence first" posture).
                .tiling(
                    when {
                        imageData.exportOpaqueFd -> VK10.VK_IMAGE_TILING_LINEAR
                        // A dma-buf is only interpretable by another driver if a DRM format
                        // modifier describes its tiling, and that tiling mode is how the image
                        // acquires one. Unlike LINEAR above this costs no render throughput: the
                        // modifier the driver picks is normally its own optimal layout.
                        imageData.exportDmaBuf -> EXTImageDrmFormatModifier.VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT
                        else -> VK10.VK_IMAGE_TILING_OPTIMAL
                    }
                )
                .usage(imageData.usage)
            if(imageData.queueFamilies.distinct().size>1)imageCreateInfo
                .sharingMode(VK10.VK_SHARING_MODE_CONCURRENT).pQueueFamilyIndices(stack.ints(*imageData.queueFamilies))

            if(imageData.opticalFlowUsage!=0)imageCreateInfo.pNext(VkOpticalFlowImageFormatInfoNV.calloc(stack).`sType$Default`().usage(imageData.opticalFlowUsage).address())

            // Back the image with an externally-created IOSurface (MoltenVK), or make its backing
            // MTLTexture IOSurface-exportable. Both are for zero-copy hand-off to a hardware video
            // encoder; import wins because a surface born via IOSurfaceCreate carries a real
            // pixelFormat (CoreVideo-wrappable), whereas MoltenVK's export leaves pixelFormat = 0.
            if (imageData.importMetalIOSurface != 0L || imageData.exportMetalIOSurface) {
                device.requireMetalObjects("An IOSurface-backed image")
            }
            if (imageData.importMetalIOSurface != 0L) {
                val importInfo = VkImportMetalIOSurfaceInfoEXT.calloc(stack)
                    .sType(EXTMetalObjects.VK_STRUCTURE_TYPE_IMPORT_METAL_IO_SURFACE_INFO_EXT)
                    .ioSurface(imageData.importMetalIOSurface)
                imageCreateInfo.pNext(importInfo.address())
            } else if (imageData.exportMetalIOSurface) {
                val exportInfo = VkExportMetalObjectCreateInfoEXT.calloc(stack)
                    .sType(EXTMetalObjects.VK_STRUCTURE_TYPE_EXPORT_METAL_OBJECT_CREATE_INFO_EXT)
                    .exportObjectType(EXTMetalObjects.VK_EXPORT_METAL_OBJECT_TYPE_METAL_IOSURFACE_BIT_EXT)
                imageCreateInfo.pNext(exportInfo.address())
            } else if (imageData.exportOpaqueFd) {
                // Linux/NVENC path: VK_KHR_external_memory_fd, same-vendor "opaque fd" handle type
                // (not VK_EXT_external_memory_dma_buf's generic cross-vendor DMA_BUF_BIT — that one
                // needs a DRM format modifier to describe tiling to a non-NVIDIA importer; opaque_fd
                // is the NVIDIA-Vulkan-to-NVIDIA-CUDA same-GPU handshake and needs none of that).
                val extImageInfo = VkExternalMemoryImageCreateInfo.calloc(stack)
                    .sType(VK11.VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO)
                    .handleTypes(VK11.VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT)
                imageCreateInfo.pNext(extImageInfo.address())
            } else if (imageData.exportDmaBuf) {
                // Linux/dma-buf path: the generic cross-vendor handle type a browser compositor
                // publishes (see LinuxDmaBufExport.kt). Offer every modifier this device can both
                // build a `usage` image from and share as a dma-buf, and let the driver choose —
                // naming one modifier here would be guessing on the importer's behalf.
                val modifiers = LinuxDmaBufExport.shareableModifiers(device, format, imageData.usage)
                check(modifiers.isNotEmpty()) {
                    "no DRM format modifier on this device can export format $format with usage 0x${imageData.usage.toString(16)} as a dma-buf"
                }
                val modifierList = VkImageDrmFormatModifierListCreateInfoEXT.calloc(stack)
                    .sType(EXTImageDrmFormatModifier.VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_LIST_CREATE_INFO_EXT)
                    .pDrmFormatModifiers(stack.longs(*modifiers.toLongArray()))
                val extImageInfo = VkExternalMemoryImageCreateInfo.calloc(stack)
                    .sType(VK11.VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO)
                    .handleTypes(EXTExternalMemoryDmaBuf.VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT)
                    .pNext(modifierList.address())
                imageCreateInfo.pNext(extImageInfo.address())
            }

            val lp = stack.mallocLong(1)
            vkCheck(
                VK10.vkCreateImage(device.vkDevice, imageCreateInfo, null, lp),
                "Failed to create image"
            )
            vkImage = lp[0]

            // Get memory requirements for this object
            val memReqs = VkMemoryRequirements.calloc(stack)
            VK10.vkGetImageMemoryRequirements(device.vkDevice, vkImage, memReqs)
            memorySize = memReqs.size()

            // For an imported IOSurface, MoltenVK backs the MTLTexture's pixels with the surface, so
            // this allocate+bind may be redundant. Log the reported size once so we can decide
            // whether to skip it (0 ⇒ safe to skip; non-zero ⇒ MoltenVK still wants bound memory).
            if (imageData.importMetalIOSurface != 0L) {
                afield.app.diagnostics.logging.lprintln("info,field-stream",
                    "imported-IOSurface image: memReqs.size=${memReqs.size()} (binding anyway; pixels live in the IOSurface)")
            }

            // Select memory size and type
            val memAlloc = VkMemoryAllocateInfo.calloc(stack)
                .sType(VK10.VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO)
                .allocationSize(memReqs.size())
                .memoryTypeIndex(
                    memoryTypeFromProperties(
                        device.physicalDevice,
                        memReqs.memoryTypeBits(),
                        imageData.memoryPropertyFlags
                    )
                )

            // The image-create-info chain above only declares the image *shareable*; the backing
            // memory's own allocation must separately opt in to export, or vkGetMemoryFdKHR fails.
            if (imageData.exportOpaqueFd) {
                // Dedicated allocation is required here, not optional: NVIDIA's Vulkan driver
                // internally tags opaque-fd memory born from an image export as dedicated, and
                // cuImportExternalMemory (CU_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD) rejects the
                // import with CUDA_ERROR_UNKNOWN (999) if the Vulkan side didn't also declare it
                // dedicated via this struct — verified by hitting exactly that error without it.
                val dedicatedAllocInfo = VkMemoryDedicatedAllocateInfo.calloc(stack)
                    .sType(VK11.VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO)
                    .image(vkImage)
                val exportAllocInfo = VkExportMemoryAllocateInfo.calloc(stack)
                    .sType(VK11.VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO)
                    .handleTypes(VK11.VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT)
                    .pNext(dedicatedAllocInfo.address())
                memAlloc.pNext(exportAllocInfo.address())
            } else if (imageData.exportDmaBuf) {
                // Dedicated is not optional here either: NVIDIA reports its block-linear modifiers
                // as VK_EXTERNAL_MEMORY_FEATURE_DEDICATED_ONLY_BIT for the dma-buf handle type, so
                // a suballocated export would be invalid on exactly the modifiers worth using.
                val dedicatedAllocInfo = VkMemoryDedicatedAllocateInfo.calloc(stack)
                    .sType(VK11.VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO)
                    .image(vkImage)
                val exportAllocInfo = VkExportMemoryAllocateInfo.calloc(stack)
                    .sType(VK11.VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO)
                    .handleTypes(EXTExternalMemoryDmaBuf.VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT)
                    .pNext(dedicatedAllocInfo.address())
                memAlloc.pNext(exportAllocInfo.address())
            }

            // Allocate memory
            vkCheck(VK10.vkAllocateMemory(device.vkDevice, memAlloc, null, lp), "Failed to allocate memory")
            vkMemory = lp[0]

            // Bind memory
            vkCheck(
                VK10.vkBindImageMemory(device.vkDevice, vkImage, vkMemory, 0),
                "Failed to bind image memory"
            )

            if (imageData.exportOpaqueFd) {
                val subresource = VkImageSubresource.calloc(stack)
                    .aspectMask(VK10.VK_IMAGE_ASPECT_COLOR_BIT)
                    .mipLevel(0)
                    .arrayLayer(0)
                val layout = VkSubresourceLayout.calloc(stack)
                VK10.vkGetImageSubresourceLayout(device.vkDevice, vkImage, subresource, layout)
                linearRowPitch = layout.rowPitch()
            }

            if (imageData.exportDmaBuf) {
                val chosen = VkImageDrmFormatModifierPropertiesEXT.calloc(stack)
                    .sType(EXTImageDrmFormatModifier.VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_PROPERTIES_EXT)
                vkCheck(
                    EXTImageDrmFormatModifier.vkGetImageDrmFormatModifierPropertiesEXT(
                        device.vkDevice,
                        vkImage,
                        chosen
                    ),
                    "vkGetImageDrmFormatModifierPropertiesEXT"
                )
                drmFormatModifier = chosen.drmFormatModifier()
                // shareableModifiers only offers single-memory-plane layouts, so the driver cannot
                // have chosen one that needs more.
                drmPlaneLayouts = LinuxDmaBufExport.planeLayouts(device, vkImage, planeCount = 1)
            }
        }
        imageData.debugName.takeIf { it.isNotBlank() }?.let {
            device.nameObject(vkImage, VK10.VK_OBJECT_TYPE_IMAGE, it)
        }
        autoDisposeToken.updateCleanup(snapshotCleanup())
    }

fun cleanup()

    fun cleanup() {
        if (!autoDisposeToken.disable()) return
        // cachedOpaqueFd (if any) is intentionally not close()'d here: plain JVM code has no public
        // way to close a raw fd number (FileDescriptor has no public int constructor pre-reflection
        // hacks). Ring sizes are 2-3 per streamer, so this leaks a handful of fds for the process's
        // lifetime rather than per-stream-restart — acceptable; revisit only if that stops being true.
        cachedOpaqueFd = null
        VK10.vkDestroyImage(device.vkDevice, vkImage, null)
        VK10.vkFreeMemory(device.vkDevice, vkMemory, null)
    }

    private fun snapshotCleanup(): () -> Unit {
        val dev = device.vkDevice
        val img = vkImage
        val mem = vkMemory
        return {
            VK10.vkDestroyImage(dev, img, null)
            VK10.vkFreeMemory(dev, mem, null)
        }
    }

Per-command-buffer layout tracking (optional; best-effort)

    @Volatile
    private var layoutByCommandBuffer = java.util.concurrent.ConcurrentHashMap<Long, Int>()

Vulkan image layout is global to the image subresource, not to a command buffer. Keep the most recently recorded transition as well so consumers recording on another command buffer (notably OpenXR) can transition from the producer's actual layout.

    @Volatile
    private var lastKnownLayout: Int? = null

fun setLayoutFor(commandBufferHandle: Long, layout: Int)

    fun setLayoutFor(commandBufferHandle: Long, layout: Int) {
        layoutByCommandBuffer[commandBufferHandle] = layout
        lastKnownLayout = layout
    }

fun getLayoutFor(commandBufferHandle: Long): Int? = layoutByCommandBuffer[commandBufferHandle]

fun getLastKnownLayout(): Int? = lastKnownLayout

fun exportMetalIOSurfaceRef(): Long

Export the IOSurfaceRef backing this image's MTLTexture (MoltenVK, VK_EXT_metal_objects). Requires exportMetalIOSurface set on the Image.ImageData it was created from, and the device to have enabled VK_EXT_metal_objects. Returns the IOSurfaceRef pointer (pass to afield.graphics.texture.MetalIOSurface.ioSurfaceId), or 0 if the surface could not be exported.

    fun exportMetalIOSurfaceRef(): Long {
        device.requireMetalObjects("Exporting an image's IOSurface")
        MemoryStack.stackPush().use { stack ->
            val ioInfo = VkExportMetalIOSurfaceInfoEXT.calloc(stack)
                .sType(EXTMetalObjects.VK_STRUCTURE_TYPE_EXPORT_METAL_IO_SURFACE_INFO_EXT)
                .image(vkImage)
            val objects = VkExportMetalObjectsInfoEXT.calloc(stack)
                .sType(EXTMetalObjects.VK_STRUCTURE_TYPE_EXPORT_METAL_OBJECTS_INFO_EXT)
                .pNext(ioInfo.address())
            EXTMetalObjects.vkExportMetalObjectsEXT(device.vkDevice, objects)
            return ioInfo.ioSurface()
        }
    }

    private var cachedOpaqueFd: Int? = null

fun exportOpaqueFd(): Int

Export this image's backing memory as an opaque POSIX fd (Linux, VK_KHR_external_memory_fd, see VulkanExternalMemory). Requires exportOpaqueFd set on the Image.ImageData it was created from. Per the spec each call to vkGetMemoryFdKHR transfers ownership of a new fd to the caller, so this caches and returns the same fd on repeated calls — call close to release it, don't just drop the value. The fd stays valid (and open in this JVM process) for the streamer's whole session; the helper process only ever gets a duplicate via /proc/<pid>/fd/<fd> (see WIRE_FORMAT.md's Linux addendum), never this one.

    fun exportOpaqueFd(): Int {
        cachedOpaqueFd?.let { return it }
        MemoryStack.stackPush().use { stack ->
            val info = VkMemoryGetFdInfoKHR.calloc(stack)
                .sType(KHRExternalMemoryFd.VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR)
                .memory(vkMemory)
                .handleType(VK11.VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT)
            val fd = IntArray(1)
            vkCheck(KHRExternalMemoryFd.vkGetMemoryFdKHR(device.vkDevice, info, fd), "vkGetMemoryFdKHR")
            cachedOpaqueFd = fd[0]
            return fd[0]
        }
    }

fun exportDmaBufFd(): Int

Export this image's backing memory as a POSIX dma-buf fd (Linux, VK_EXT_external_memory_dma_buf, see LinuxDmaBufExport). Requires exportDmaBuf set on the Image.ImageData it was created from.

Unlike exportOpaqueFd this deliberately does not cache. A dma-buf fd is normally handed to an importer that consumes ownership of it, so one cached fd shared between two imports would be a double free; each call instead returns a fresh fd the caller owns. Pair it with drmFormatModifier and drmPlaneLayouts, without which no other driver can interpret the bytes.

    fun exportDmaBufFd(): Int {
        require(imageData.exportDmaBuf) { "exportDmaBufFd requires ImageData.exportDmaBuf = true" }
        return LinuxDmaBufExport.exportFd(device, vkMemory)
    }

class ImageData

var arrayLayers: Int = 1

internal var format: Int

var flags: Int = 0

var height: Int = 0

var aliasSingleLayerDescriptors: Boolean = false

var debugName: String = ""

var mipLevels: Int = 1

var memoryPropertyFlags: Int = VK10.VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT

var sampleCount: Int = 1

var usage: Int = 0

var queueFamilies: IntArray = intArrayOf()

Concurrent ownership for explicit cross-queue adapters; empty keeps exclusive sharing.

var opticalFlowUsage: Int = 0

Optional VK_NV_optical_flow format usage, independent of ordinary image usage.

var exportMetalIOSurface: Boolean = false

MoltenVK only: create the backing MTLTexture as IOSurface-exportable (chains VkExportMetalObjectCreateInfoEXT). Enables Image.exportMetalIOSurfaceRef.

var importMetalIOSurface: Long = 0

MoltenVK only: back this image with the given externally-created IOSurface (its IOSurfaceRef pointer), via VkImportMetalIOSurfaceInfoEXT. Takes precedence over exportMetalIOSurface. The IOSurface's format/size must match this image's.

var exportOpaqueFd: Boolean = false

Linux/NVENC only: create this image's backing memory as exportable via VK_KHR_external_memory_fd (opaque-fd handle type), forces VK_IMAGE_TILING_LINEAR (see Image.linearRowPitch), and enables Image.exportOpaqueFd. Analogous to exportMetalIOSurface on the Mac path — see VulkanExternalMemory.kt.

var exportDmaBuf: Boolean = false

Linux only: create this image with a driver-chosen DRM format modifier and dma-buf exportable backing memory (VK_EXT_image_drm_format_modifier + VK_EXT_external_memory_dma_buf), and enable Image.exportDmaBufFd. This is the handle a browser compositor publishes, and the one the external-browser receiver imports; see LinuxDmaBufExport.kt for why it is not the same thing as exportOpaqueFd.

var width: Int = 0

init

        init {
            this.format = VK10.VK_FORMAT_R8G8B8A8_SRGB
        }

fun arrayLayers(arrayLayers: Int): ImageData

        fun arrayLayers(arrayLayers: Int): ImageData {
            this.arrayLayers = arrayLayers
            return this
        }

fun format(format: Int): ImageData

        fun format(format: Int): ImageData {
            this.format = format
            return this
        }

fun flags(flags: Int): ImageData

        fun flags(flags: Int): ImageData {
            this.flags = flags
            return this
        }

fun height(height: Int): ImageData

        fun height(height: Int): ImageData {
            this.height = height
            return this
        }

fun debugName(debugName: String): ImageData

        fun debugName(debugName: String): ImageData {
            this.debugName = debugName
            return this
        }

fun aliasSingleLayerDescriptors(aliasSingleLayerDescriptors: Boolean = true): ImageData

        fun aliasSingleLayerDescriptors(aliasSingleLayerDescriptors: Boolean = true): ImageData {
            this.aliasSingleLayerDescriptors = aliasSingleLayerDescriptors
            return this
        }

fun mipLevels(mipLevels: Int): ImageData

        fun mipLevels(mipLevels: Int): ImageData {
            this.mipLevels = mipLevels
            return this
        }

fun memoryPropertyFlags(memoryPropertyFlags: Int): ImageData

        fun memoryPropertyFlags(memoryPropertyFlags: Int): ImageData {
            this.memoryPropertyFlags = memoryPropertyFlags
            return this
        }

fun sampleCount(sampleCount: Int): ImageData

        fun sampleCount(sampleCount: Int): ImageData {
            this.sampleCount = sampleCount
            return this
        }

fun usage(usage: Int): ImageData

        fun usage(usage: Int): ImageData {
            this.usage = usage
            return this
        }

fun width(width: Int): ImageData

        fun width(width: Int): ImageData {
            this.width = width
            return this
        }

val bytesPerPixel: Int

        val bytesPerPixel: Int
            get() = vkFormatBytesPerPixel(format, debugName.ifEmpty { "ImageData" })
    }
}

format size

fun vkFormatBytesPerPixelOrNull(format: Int): Int?

Bytes occupied by one pixel of format, or null if this table does not know the format.

Null means "no answer", not "zero": block-compressed formats (BC/ETC/ASTC) have no per-pixel size at all, and depth/stencil formats are not addressable by the flat width * height * bytesPerPixel arithmetic every caller here performs. Both must fall through rather than be given a plausible-looking number.

This is the single source of truth. Image.ImageData.bytesPerPixel, GpuImageCapture and VideoTexture's plane staging each carried their own copy of this table, and they had already drifted apart once — the capture copy was missing every one- and two-channel float format, VideoTexture's was the only one that knew R16G16_UNORM.

fun vkFormatBytesPerPixelOrNull(format: Int): Int? = when (format) {
    VK10.VK_FORMAT_R8_UNORM -> 1
    VK10.VK_FORMAT_R8G8_UNORM -> 2
    VK10.VK_FORMAT_R16_SFLOAT,
    VK10.VK_FORMAT_R16_UNORM,
    VK10.VK_FORMAT_R16_SNORM -> 2
    VK10.VK_FORMAT_R8G8B8_UNORM,
    VK10.VK_FORMAT_B8G8R8_UNORM -> 3
    VK10.VK_FORMAT_R8G8B8A8_UNORM,
    VK10.VK_FORMAT_R8G8B8A8_SRGB,
    VK10.VK_FORMAT_B8G8R8A8_UNORM,
    VK10.VK_FORMAT_B8G8R8A8_SRGB -> 4
    VK10.VK_FORMAT_R16G16_SFLOAT,
    VK10.VK_FORMAT_R16G16_UNORM,
    VK10.VK_FORMAT_R16G16_SNORM,
    VK10.VK_FORMAT_R32_SFLOAT -> 4
    VK10.VK_FORMAT_R16G16B16_SFLOAT -> 6
    VK10.VK_FORMAT_R16G16B16A16_SFLOAT,
    VK10.VK_FORMAT_R32G32_SFLOAT -> 8
    VK10.VK_FORMAT_R32G32B32_SFLOAT -> 12
    VK10.VK_FORMAT_R32G32B32A32_SFLOAT -> 16
    else -> null
}

fun vkFormatBytesPerPixel(format: Int, usage: String): Int

vkFormatBytesPerPixelOrNull, throwing for formats it does not know. usage names the caller so the message says which capture or upload failed.

Callers all size a staging buffer or validate a source buffer against the result, so a default is worse than a failure: too small a guess lets vkCmdCopyImageToBuffer write past the end of the allocation, and the corruption surfaces somewhere unrelated to the format that caused it.

fun vkFormatBytesPerPixel(format: Int, usage: String): Int =
    vkFormatBytesPerPixelOrNull(format)
        ?: throw IllegalArgumentException("$usage: no bytes-per-pixel known for VkFormat $format")

barrier util (now sync2)

fun image_layout_transition

fun image_layout_transition(
    command_buffer: VkCommandBuffer,
    image: Long,
    src_stage_mask: Long,
    dst_stage_mask: Long,
    src_access_mask: Long,
    dst_access_mask: Long,
    old_layout: Int,
    new_layout: Int,
    sub_resource_range: VkImageSubresourceRange,
    src_queue_family_index: Int = VK_QUEUE_FAMILY_IGNORED,
    dst_queue_family_index: Int = VK_QUEUE_FAMILY_IGNORED
) {
    MemoryStack.stackPush().use { stack ->
        val barrier2 = VkImageMemoryBarrier2.calloc(1, stack)
        barrier2[0]
            .sType(VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2)
            .srcStageMask(src_stage_mask)
            .srcAccessMask(src_access_mask)
            .dstStageMask(dst_stage_mask)
            .dstAccessMask(dst_access_mask)
            .oldLayout(vkLayout(old_layout))
            .newLayout(vkLayout(new_layout))
            .srcQueueFamilyIndex(src_queue_family_index)
            .dstQueueFamilyIndex(dst_queue_family_index)
            .image(image)
            .subresourceRange(sub_resource_range)

        val dep = VkDependencyInfo.calloc(stack)
            .sType(VK_STRUCTURE_TYPE_DEPENDENCY_INFO)
            .pImageMemoryBarriers(barrier2)

        vkCmdPipelineBarrier2(command_buffer, dep)
    }
}

fun image_layout_transition

fun image_layout_transition(
    command_buffer: VkCommandBuffer,
    image: Image,
    src_stage_mask: Long,
    dst_stage_mask: Long,
    src_access_mask: Long,
    dst_access_mask: Long,
    old_layout: Int,
    new_layout: Int,
    sub_resource_range: VkImageSubresourceRange,
    src_queue_family_index: Int = VK_QUEUE_FAMILY_IGNORED,
    dst_queue_family_index: Int = VK_QUEUE_FAMILY_IGNORED
) {
    image_layout_transition(
        command_buffer,
        image.vkImage,
        src_stage_mask,
        dst_stage_mask,
        src_access_mask,
        dst_access_mask,
        old_layout,
        new_layout,
        sub_resource_range,
        src_queue_family_index,
        dst_queue_family_index
    )
    image.setLayoutFor(command_buffer.address(), new_layout)
}