SharedGpuSlot.kt
src/jvmMain/kotlin/afield/media/video/diffusion/SharedGpuSlot.kt · 139 lines
package afield.media.video.diffusion
import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.device.CommandBuffer
import afield.graphics.texture.UnixFdPass
import afield.graphics.vulkan.util.memoryTypeFromProperties
import afield.graphics.vulkan.util.vkCheck
import org.lwjgl.system.MemoryStack
import org.lwjgl.vulkan.*
import org.lwjgl.vulkan.VK13.*
One slot of the GPU memory a diffusion session shares with its worker: a device-local buffer and a timeline semaphore, both exported to the worker as file descriptors.
A StreamDiffusionSession allocates a ring of these for the frames it sends and another for the results it reads back, passes their descriptors over a Unix socket, and afterwards exchanges only slot indices and semaphore values with the worker. Pixels are written and read by the GPU at both ends: a slot is a dedicated device-local allocation and is never mapped for the CPU. Its barrier moves the buffer between Field's graphics queue and the external queue CUDA works on, and the device UUID it reports names the GPU the worker is to import onto. Construction asks the driver whether it can export this buffer and this kind of semaphore, and fails rather than handing over a descriptor the worker cannot use.
internal class SharedGpuSlot(private val renderer: Renderer, val bytes: Long) : AutoCloseable
One dedicated device-local buffer and one exportable timeline semaphore. Never host mapped.
val device = renderer.device
var buffer = 0L; private set
private var memory = 0L
var semaphore = 0L; private set
var allocationSize = 0L; private set
@Volatile var value = 0L
var used = false
private var closed = false
init
body — 49 lines, from line 37
init {
check(device.supportsExternalMemoryFd && device.supportsExternalSemaphoreFd) {
"StreamDiffusion requires Linux Vulkan external memory and semaphore FD support"
}
require(bytes > 0)
try {
MemoryStack.stackPush().use { s ->
val usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT or VK_BUFFER_USAGE_TRANSFER_DST_BIT
val capabilities = VkExternalBufferProperties.calloc(s).`sType$Default`()
vkGetPhysicalDeviceExternalBufferProperties(device.physicalDevice.vkPhysicalDevice,
VkPhysicalDeviceExternalBufferInfo.calloc(s).`sType$Default`().usage(usage)
.handleType(VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT), capabilities)
check(capabilities.externalMemoryProperties().externalMemoryFeatures() and VK_EXTERNAL_MEMORY_FEATURE_EXPORTABLE_BIT != 0)
val timeline = VkSemaphoreTypeCreateInfo.calloc(s).`sType$Default`()
.semaphoreType(VK_SEMAPHORE_TYPE_TIMELINE).initialValue(0)
val semCaps = VkExternalSemaphoreProperties.calloc(s).`sType$Default`()
vkGetPhysicalDeviceExternalSemaphoreProperties(device.physicalDevice.vkPhysicalDevice,
VkPhysicalDeviceExternalSemaphoreInfo.calloc(s).`sType$Default`()
.pNext(timeline.address()).handleType(VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT), semCaps)
check(semCaps.externalSemaphoreFeatures() and VK_EXTERNAL_SEMAPHORE_FEATURE_EXPORTABLE_BIT != 0) {
"Driver cannot export a timeline semaphore for CUDA"
}
val exportBuffer = VkExternalMemoryBufferCreateInfo.calloc(s).`sType$Default`()
.handleTypes(VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT)
val ci = VkBufferCreateInfo.calloc(s).`sType$Default`().pNext(exportBuffer.address())
.size(bytes).usage(usage).sharingMode(VK_SHARING_MODE_EXCLUSIVE)
val out = s.mallocLong(1)
vkCheck(vkCreateBuffer(device.vkDevice, ci, null, out), "Create shared GPU buffer")
buffer = out[0]
val req = VkMemoryRequirements.calloc(s)
vkGetBufferMemoryRequirements(device.vkDevice, buffer, req)
allocationSize = req.size()
val dedicated = VkMemoryDedicatedAllocateInfo.calloc(s).`sType$Default`().buffer(buffer)
val exportMemory = VkExportMemoryAllocateInfo.calloc(s).`sType$Default`()
.handleTypes(VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT).pNext(dedicated.address())
val ai = VkMemoryAllocateInfo.calloc(s).`sType$Default`().pNext(exportMemory.address())
.allocationSize(allocationSize).memoryTypeIndex(memoryTypeFromProperties(
device.physicalDevice, req.memoryTypeBits(), VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT))
vkCheck(vkAllocateMemory(device.vkDevice, ai, null, out), "Allocate shared GPU memory")
memory = out[0]
vkCheck(vkBindBufferMemory(device.vkDevice, buffer, memory, 0), "Bind shared GPU buffer")
val exportSem = VkExportSemaphoreCreateInfo.calloc(s).`sType$Default`()
.handleTypes(VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT).pNext(timeline.address())
vkCheck(vkCreateSemaphore(device.vkDevice, VkSemaphoreCreateInfo.calloc(s).`sType$Default`()
.pNext(exportSem.address()), null, out), "Create shared GPU semaphore")
semaphore = out[0]
}
} catch (e: Throwable) { close(); throw e }
}
fun exportFds(): IntArray
Fresh descriptors; caller closes after SCM_RIGHTS duplication, including failure paths.
fun exportFds(): IntArray = MemoryStack.stackPush().use { s ->
check(UnixFdPass.available)
val out = s.mallocInt(1)
vkCheck(KHRExternalMemoryFd.vkGetMemoryFdKHR(device.vkDevice,
VkMemoryGetFdInfoKHR.calloc(s).`sType$Default`().memory(memory)
.handleType(VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT), out), "Export GPU memory")
val memoryFd = out[0]
try {
vkCheck(KHRExternalSemaphoreFd.vkGetSemaphoreFdKHR(device.vkDevice,
VkSemaphoreGetFdInfoKHR.calloc(s).`sType$Default`().semaphore(semaphore)
.handleType(VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT), out), "Export GPU semaphore")
intArrayOf(memoryFd, out[0])
} catch (e: Throwable) { UnixFdPass.closeFd(memoryFd); throw e }
}
fun barrier(command: CommandBuffer, acquire: Boolean, access: Long)
Ownership transfer between the graphics queue and CUDA's external queue.
fun barrier(command: CommandBuffer, acquire: Boolean, access: Long) {
MemoryStack.stackPush().use { s ->
val b = VkBufferMemoryBarrier2.calloc(1, s).`sType$Default`()
.buffer(buffer).offset(0).size(bytes)
.srcQueueFamilyIndex(if (acquire) VK_QUEUE_FAMILY_EXTERNAL else renderer.graphQueue.queueFamilyIndex)
.dstQueueFamilyIndex(if (acquire) renderer.graphQueue.queueFamilyIndex else VK_QUEUE_FAMILY_EXTERNAL)
.srcStageMask(if (acquire) VK_PIPELINE_STAGE_2_NONE else VK_PIPELINE_STAGE_2_COPY_BIT)
.dstStageMask(if (acquire) VK_PIPELINE_STAGE_2_COPY_BIT else VK_PIPELINE_STAGE_2_NONE)
.srcAccessMask(if (acquire) 0 else access).dstAccessMask(if (acquire) access else 0)
vkCmdPipelineBarrier2(command.vkCommandBuffer, VkDependencyInfo.calloc(s).`sType$Default`().pBufferMemoryBarriers(b))
}
}
fun copyRegion(s: MemoryStack, width: Int, height: Int): VkBufferImageCopy.Buffer
fun copyRegion(s: MemoryStack, width: Int, height: Int): VkBufferImageCopy.Buffer =
VkBufferImageCopy.calloc(1, s).bufferOffset(0).bufferRowLength(0).bufferImageHeight(0)
.imageSubresource { it.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT).mipLevel(0).baseArrayLayer(0).layerCount(1) }
.imageExtent { it.width(width).height(height).depth(1) }
override fun close()
override fun close() {
if (closed) return
closed = true
if (semaphore != 0L) vkDestroySemaphore(device.vkDevice, semaphore, null)
if (buffer != 0L) vkDestroyBuffer(device.vkDevice, buffer, null)
if (memory != 0L) vkFreeMemory(device.vkDevice, memory, null)
}
companion object
fun deviceUuid(renderer: Renderer): String
fun deviceUuid(renderer: Renderer): String = MemoryStack.stackPush().use { s ->
val id = VkPhysicalDeviceIDProperties.calloc(s).`sType$Default`()
vkGetPhysicalDeviceProperties2(renderer.physicalDevice.vkPhysicalDevice,
VkPhysicalDeviceProperties2.calloc(s).`sType$Default`().pNext(id.address()))
(0 until VK_UUID_SIZE).joinToString("") { "%02x".format(id.deviceUUID(it).toInt() and 255) }
}
}
}