Skip to content

HardwareOpticalFlow.kt

src/jvmMain/kotlin/afield/media/vision/HardwareOpticalFlow.kt · 244 lines

package afield.media.vision

import afield.graphics.vulkan.device.vkLayout
import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.buffers.VulkanBuffer
import afield.graphics.vulkan.device.*
import afield.document.context.*
import afield.graphics.render.*
import afield.graphics.scene.*
import afield.graphics.shader.*
import afield.graphics.render.Module
import afield.graphics.vulkan.targets.RenderToTexture
import afield.graphics.texture.*
import afield.graphics.vulkan.util.vkCheck
import afield.graphics.vulkan.util.AutoDisposer
import afield.graphics.shader.fromViews
import org.lwjgl.system.MemoryStack
import org.lwjgl.vulkan.*
import org.lwjgl.vulkan.VK13.*
import org.lwjgl.vulkan.NVOpticalFlow.*

The hardware optical-flow backend, which drives the VK_NV_optical_flow engine on NVIDIA Ada and later GPUs.

HardwareOpticalFlow is what OpticalFlowBackend.AUTO picks when the device has that extension enabled.

class HardwareOpticalFlow(override val renderer: Renderer,override val first: HasImageView,override val second: HasImageView,

Dedicated hardware image correspondence. Call evaluate between Field frames, after producers have submitted. Images stay on the GPU. This first implementation waits for one final fence; it does not overlap pairs, and includes conversion/synchronization in milliseconds.

first -> second: output.xy is displacement in INPUT pixels, X right/Y down. One output texel covers grid input pixels. output.z is raw hardware cost (lower is better), NOT confidence. Inputs are opaque linear RGB; both are tone-mapped identically to BGRA8 for the motion engine. Concurrent-sharing private images and binary semaphores connect graphics and optical queues.

first and second are borrowed HasImageView providers from this renderer's device. A RenderToTexture supplies its first color attachment; use imageViewArrayindex` for another. Keep both input images alive and at the construction size until this operation is closed. [output](#L81) is an ownedRenderToTexture`, usable as an ordinary sampled texture after evaluate. The operation itself is not a texture and is not automatically scheduled by the renderer.

class HardwareOpticalFlow(override val renderer: Renderer,override val first: HasImageView,override val second: HasImageView,
    override val grid: Int=4,val quality: Quality=Quality.SLOW) : OpticalFlowOperation {

override val backend get()=OpticalFlowBackend.HARDWARE

enum class Quality(internal val vk: Int)

    enum class Quality(internal val vk: Int) { FAST(VK_OPTICAL_FLOW_PERFORMANCE_LEVEL_FAST_NV),MEDIUM(VK_OPTICAL_FLOW_PERFORMANCE_LEVEL_MEDIUM_NV),SLOW(VK_OPTICAL_FLOW_PERFORMANCE_LEVEL_SLOW_NV) }
    private val device=renderer.device.also {
        check(it.supportsOpticalFlow) { "VK_NV_optical_flow is not enabled" }
        require(grid in listOf(1,2,4,8))
        require(first.imageView.underlyingImage!=null && second.imageView.underlyingImage!=null)
    }

override val width=first.imageView.underlyingImage!!.imageData.width

override val height=first.imageView.underlyingImage!!.imageData.height

override val flowWidth=(width+grid-1)/grid

override val flowHeight=(height+grid-1)/grid

    private val family=(0 until device.physicalDevice.vkQueueFamilyProps.capacity()).firstOrNull {
        device.physicalDevice.vkQueueFamilyProps[it].queueFlags() and VK_QUEUE_OPTICAL_FLOW_BIT_NV!=0
    } ?: error("No optical-flow queue family")
    private val opticalQueue=Queue(device,family,0)

val opticalTimestampBits=device.physicalDevice.vkQueueFamilyProps[family].timestampValidBits()

    private val conversionTimer=afield.graphics.render.lighting.LightingGpuTimer(renderer)
    private val opticalTimer=afield.graphics.render.lighting.LightingGpuTimer(renderer,family)
    private val unpackTimer=afield.graphics.render.lighting.LightingGpuTimer(renderer)

override val gpuTimings get()

    override val gpuTimings get()=mapOf("conversion" to conversionTimer.samples.toList(),"opticalFlow" to opticalTimer.samples.toList(),"unpack" to unpackTimer.samples.toList())
    private val pool=CommandPool(device,family)
    private val commands=listOf(CommandBuffer(renderer.commandPool,true,true),CommandBuffer(pool,true,true),CommandBuffer(renderer.commandPool,true,true))
    private val ready=Semaphore(device,false)
    private val done=Semaphore(device,false)
    private val fence=Fence(device,false)
    private var session=0L
    private val nativeDisposer: AutoDisposer.Token
    private val inputs=mutableListOf<Image>()
    private val outputs=mutableListOf<Image>()
    private val views=mutableListOf<ImageView>()
    private val conversion=RenderToTexture(renderer) {
        this.width=this@HardwareOpticalFlow.width;this.height=this@HardwareOpticalFlow.height;colorAttachments=2;useDepth=false
        colorFormats=listOf(VK_FORMAT_B8G8R8A8_UNORM,VK_FORMAT_B8G8R8A8_UNORM);debugName="optical RGB conversion"
    }

override val output

Sampled RGBA32F texture: XY displacement in input pixels, Z matching cost, W = 1. Dimensions are ceil(input width/grid) by ceil(input height/grid), owned by this operation.

    override val output=RenderToTexture(renderer) { width=flowWidth;height=flowHeight;format=VK_FORMAT_R32G32B32A32_SFLOAT;useDepth=false;debugName="motion field and cost" }
    private val sampler=TextureSampler(renderer,1,false,filter=VK_FILTER_NEAREST)
    private val convert=Module(parameters(renderer) {
        name="optical input conversion";+conversion;vertex=VERTEX;fragment=CONVERT;depthTest=false;depthWrite=false;blend=false
        textureResolver={ t -> fromViews(listOf(if(t.name=="firstImage")first else second),t.name,sampler) }
    })
    private val packed=VulkanBuffer(renderer,flowWidth.toLong()*flowHeight*4,VK_BUFFER_USAGE_TRANSFER_DST_BIT or VK_BUFFER_USAGE_STORAGE_BUFFER_BIT or VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,needsDeviceAddress=true)
    private val costs=VulkanBuffer(renderer,flowWidth.toLong()*flowHeight*4,VK_BUFFER_USAGE_TRANSFER_DST_BIT or VK_BUFFER_USAGE_STORAGE_BUFFER_BIT or VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT,VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,needsDeviceAddress=true)
    private val resolve=Module(parameters(renderer) {name="optical unpack";+output;vertex=VERTEX;fragment=UNPACK;depthTest=false;depthWrite=false;blend=false})
    private var closed=false
    private var firstPair=true

override val milliseconds=ArrayDeque<Double>()

override val allocatedBytes get()

    override val allocatedBytes get()=inputs.sumOf { it.memorySize }+outputs.sumOf { it.memorySize }+
        conversion.colorImageRefs.sumOf { it?.memorySize ?: 0 }+output.colorImageRefs.sumOf { it?.memorySize ?: 0 }+packed.requestedSize+costs.requestedSize

init

body — 51 lines, from line 95
    init {
        check(device.supportsOpticalFlow) { "VK_NV_optical_flow is not enabled" }
        require(grid in listOf(1,2,4,8))
        require(second.imageView.underlyingImage!!.imageData.let { it.width==width && it.height==height })
        MemoryStack.stackPush().use { s ->
            val props=VkPhysicalDeviceOpticalFlowPropertiesNV.calloc(s).`sType$Default`()
            vkGetPhysicalDeviceProperties2(device.physicalDevice.vkPhysicalDevice,VkPhysicalDeviceProperties2.calloc(s).`sType$Default`().pNext(props.address()))
            require(width in props.minWidth()..props.maxWidth() && height in props.minHeight()..props.maxHeight())
            require(props.supportedOutputGridSizes() and gridFlag()!=0 && props.costSupported())
            fun supported(usage: Int,format: Int): Boolean {
                val info=VkOpticalFlowImageFormatInfoNV.calloc(s).`sType$Default`().usage(usage);val count=s.mallocInt(1)
                vkCheck(vkGetPhysicalDeviceOpticalFlowImageFormatsNV(device.physicalDevice.vkPhysicalDevice,info,count,null),"flow formats")
                val formats=VkOpticalFlowImageFormatPropertiesNV.calloc(count[0],s)
                for(i in 0 until count[0])formats[i].`sType$Default`()
                vkCheck(vkGetPhysicalDeviceOpticalFlowImageFormatsNV(device.physicalDevice.vkPhysicalDevice,info,count,formats),"flow formats")
                return (0 until count[0]).any { formats[it].format()==format }
            }
            check(supported(VK_OPTICAL_FLOW_USAGE_INPUT_BIT_NV,VK_FORMAT_B8G8R8A8_UNORM) &&
                supported(VK_OPTICAL_FLOW_USAGE_OUTPUT_BIT_NV,VK_FORMAT_R16G16_S10_5_NV) && supported(VK_OPTICAL_FLOW_USAGE_COST_BIT_NV,VK_FORMAT_R32_UINT)) { "Required optical-flow image formats unavailable" }
            val create=VkOpticalFlowSessionCreateInfoNV.calloc(s).`sType$Default`().width(width).height(height)
                .imageFormat(VK_FORMAT_B8G8R8A8_UNORM).flowVectorFormat(VK_FORMAT_R16G16_S10_5_NV).costFormat(VK_FORMAT_R32_UINT)
                .outputGridSize(gridFlag()).performanceLevel(quality.vk).flags(VK_OPTICAL_FLOW_SESSION_CREATE_ENABLE_COST_BIT_NV)
            val result=s.mallocLong(1);vkCheck(vkCreateOpticalFlowSessionNV(device.vkDevice,create,null,result),"create optical flow");session=result[0]
        }
        fun image(w: Int,h: Int,format: Int,usage: Int,opticalUsage: Int)=Image(device,Image.ImageData().format(format).width(w).height(h).usage(usage or VK_IMAGE_USAGE_SAMPLED_BIT).apply {
            queueFamilies=intArrayOf(renderer.graphQueue.queueFamilyIndex,family);opticalFlowUsage=opticalUsage
        })
        repeat(2){inputs+=image(width,height,VK_FORMAT_B8G8R8A8_UNORM,VK_IMAGE_USAGE_TRANSFER_DST_BIT,VK_OPTICAL_FLOW_USAGE_INPUT_BIT_NV)}
        outputs+=image(flowWidth,flowHeight,VK_FORMAT_R16G16_S10_5_NV,VK_IMAGE_USAGE_TRANSFER_SRC_BIT,VK_OPTICAL_FLOW_USAGE_OUTPUT_BIT_NV)
        outputs+=image(flowWidth,flowHeight,VK_FORMAT_R32_UINT,VK_IMAGE_USAGE_TRANSFER_SRC_BIT,VK_OPTICAL_FLOW_USAGE_COST_BIT_NV)
        (inputs+outputs).forEach { im -> views+=ImageView(device,im,ImageView.ImageViewData().format(im.format).aspectMask(VK_IMAGE_ASPECT_COLOR_BIT)) }
        listOf(VK_OPTICAL_FLOW_SESSION_BINDING_POINT_INPUT_NV,VK_OPTICAL_FLOW_SESSION_BINDING_POINT_REFERENCE_NV,
            VK_OPTICAL_FLOW_SESSION_BINDING_POINT_FLOW_VECTOR_NV,VK_OPTICAL_FLOW_SESSION_BINDING_POINT_COST_NV).forEachIndexed { i,binding ->
            vkCheck(vkBindOpticalFlowSessionImageNV(device.vkDevice,session,binding,views[i].vkImageView,
                VK_IMAGE_LAYOUT_GENERAL),"bind optical image")
        }
        check(convert.valid && resolve.valid)
        fun quad(module: Module,target: RenderToTexture): Mesh {val p=module.pointer();val m=Mesh(renderer,p);m.resize(3,0,p);module.meshes["quad"]=m;target.modules["quad"]=p;return m}
        quad(convert,conversion)
        quad(resolve,output).apply { push.setMembers(resolve.fi.pushConstants.single().members);push["flow"]=packed;push["cost"]=costs;push["width"]=flowWidth }
        // Unlike ordinary Field images/buffers, these extension/queue objects have no automatic
        // wrapper lifetime. Capture only native handles so shutdown also covers an unclosed view.
        val nativeDevice=device.vkDevice;val nativeSession=session;val nativePool=pool.vkCommandPool
        val nativeReady=ready.vkSemaphore;val nativeDone=done.vkSemaphore
        nativeDisposer=AutoDisposer.register(this,"optical flow session and queue",owner=device) {
            vkDestroyOpticalFlowSessionNV(nativeDevice,nativeSession,null)
            vkDestroyCommandPool(nativeDevice,nativePool,null)
            vkDestroySemaphore(nativeDevice,nativeReady,null);vkDestroySemaphore(nativeDevice,nativeDone,null)
        }
    }
    private fun gridFlag()=when(grid){1->VK_OPTICAL_FLOW_GRID_SIZE_1X1_BIT_NV;2->VK_OPTICAL_FLOW_GRID_SIZE_2X2_BIT_NV;4->VK_OPTICAL_FLOW_GRID_SIZE_4X4_BIT_NV;else->VK_OPTICAL_FLOW_GRID_SIZE_8X8_BIT_NV}

override fun evaluate(reset: Boolean)

Reset temporal hints for a cut, image-order change or nonconsecutive pair.

body — 54 lines, from line 147
    override fun evaluate(reset: Boolean) {
        check(!closed && renderer.currentCommandBuffer==null) { "Evaluate optical flow between frames, after input producers submit" }
        val start=System.nanoTime()
        val prep=commands[0];val flow=commands[1];val finish=commands[2]
        commands.forEach { vkResetCommandBuffer(it.vkCommandBuffer,0);it.beginRecording() }
        (first as? PreparesSampledImages)?.prepareSampledImagesFor(prep);(second as? PreparesSampledImages)?.prepareSampledImagesFor(prep)
        conversionTimer.begin(prep,0)
        conversion.render(prep,0)
        MemoryStack.stackPush().use { s ->
            for(i in 0..1) {
                val source=conversion.colorImageRefs[i]!!
                transition(prep,source,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
                transition(prep,inputs[i],VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
                val copy=VkImageCopy.calloc(1,s).srcSubresource { it.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT).layerCount(1) }
                    .dstSubresource { it.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT).layerCount(1) }.extent { it.set(width,height,1) }
                vkCmdCopyImage(prep.vkCommandBuffer,source.vkImage,vkLayout(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL),inputs[i].vkImage,vkLayout(VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL),copy)
                transition(prep,source,VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
                transition(prep,inputs[i],VK_IMAGE_LAYOUT_GENERAL)
            }
            outputs.forEach { transition(prep,it,VK_IMAGE_LAYOUT_GENERAL) }
            conversionTimer.end(prep,0);opticalTimer.begin(flow,0)
            vkCmdOpticalFlowExecuteNV(flow.vkCommandBuffer,session,VkOpticalFlowExecuteInfoNV.calloc(s).`sType$Default`()
                .flags(if(reset||firstPair)VK_OPTICAL_FLOW_EXECUTE_DISABLE_TEMPORAL_HINTS_BIT_NV else 0))
            opticalTimer.end(flow,0);unpackTimer.begin(finish,0)
            outputs.forEachIndexed { i,im ->
                transition(finish,im,VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
                val copy=VkBufferImageCopy.calloc(1,s).imageSubresource { it.aspectMask(VK_IMAGE_ASPECT_COLOR_BIT).layerCount(1) }.imageExtent { it.set(flowWidth,flowHeight,1) }
                vkCmdCopyImageToBuffer(finish.vkCommandBuffer,im.vkImage,vkLayout(VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL),if(i==0)packed.buffer else costs.buffer,copy)
            }
            val barrier=VkMemoryBarrier2.calloc(1,s).`sType$Default`().srcStageMask(VK_PIPELINE_STAGE_2_TRANSFER_BIT).srcAccessMask(VK_ACCESS_2_TRANSFER_WRITE_BIT)
                .dstStageMask(VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT).dstAccessMask(VK_ACCESS_2_SHADER_READ_BIT)
            vkCmdPipelineBarrier2(finish.vkCommandBuffer,VkDependencyInfo.calloc(s).`sType$Default`().pMemoryBarriers(barrier))
            output.render(finish,0)
            unpackTimer.end(finish,0)
        }
        commands.forEach { it.endRecording() }
        MemoryStack.stackPush().use { s ->
            fun submit(queue: Queue,command: CommandBuffer,wait: Semaphore?,signal: Semaphore?,last: Boolean) {
                val info=VkSubmitInfo2.calloc(1,s).`sType$Default`().pCommandBufferInfos(VkCommandBufferSubmitInfo.calloc(1,s).`sType$Default`().commandBuffer(command.vkCommandBuffer))
                if(wait!=null)info.pWaitSemaphoreInfos(VkSemaphoreSubmitInfo.calloc(1,s).`sType$Default`().semaphore(wait.vkSemaphore).stageMask(VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT))
                if(signal!=null)info.pSignalSemaphoreInfos(VkSemaphoreSubmitInfo.calloc(1,s).`sType$Default`().semaphore(signal.vkSemaphore).stageMask(VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT))
                synchronized(Queue.globalQueueLock){vkCheck(vkQueueSubmit2(queue.vkQueue,info,if(last)fence.vkFence else VK_NULL_HANDLE),"optical queue submit")}
            }
            submit(renderer.graphQueue,prep,null,ready,false);submit(opticalQueue,flow,ready,done,false);submit(renderer.graphQueue,finish,done,null,true)
        }
        fence.fenceWait();fence.reset();firstPair=false
        milliseconds+= (System.nanoTime()-start)/1e6;while(milliseconds.size>1024)milliseconds.removeFirst()
    }
    private fun transition(command: CommandBuffer,image: Image,layout: Int)=MemoryStack.stackPush().use { s ->
        val old=image.getLastKnownLayout() ?: VK_IMAGE_LAYOUT_UNDEFINED
        image_layout_transition(command.vkCommandBuffer,image,VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
            if(old==VK_IMAGE_LAYOUT_UNDEFINED)0 else VK_ACCESS_2_MEMORY_READ_BIT or VK_ACCESS_2_MEMORY_WRITE_BIT,
            VK_ACCESS_2_MEMORY_READ_BIT or VK_ACCESS_2_MEMORY_WRITE_BIT,old,layout,VkImageSubresourceRange.calloc(s).aspectMask(VK_IMAGE_ASPECT_COLOR_BIT).levelCount(1).layerCount(1))
    }

override fun close()

    override fun close() {
        if(closed)return;closed=true
        renderer.cleanup+=Runnable {
            commands.forEach { it.cleanup() }
            if(nativeDisposer.disable()) {
                vkDestroyOpticalFlowSessionNV(device.vkDevice,session,null)
                pool.cleanup();ready.cleanup();done.cleanup()
            }
            views.forEach { it.cleanup() };(inputs+outputs).forEach { it.cleanup() }
            fence.cleanup();packed.cleanup();costs.cleanup()
            convert.cleanup();resolve.cleanup();sampler.cleanup()
            conversionTimer.close();opticalTimer.close();unpackTimer.close()
            listOf(conversion,output).forEach { r -> r.imageViewArray.forEach { it.imageView.cleanup() };r.colorImageRefs.forEach { it?.cleanup() };r.defaultSampler?.cleanup() }
        }
    }

companion object

    companion object {

fun decodePacked(vector: Int): afield.math.linalg.Vector2f

CPU diagnostic counterpart to the GPU unpack pass; signed 16-bit lanes with five fraction bits.

        fun decodePacked(vector: Int): afield.math.linalg.Vector2f = afield.math.linalg.Vector2f(
            vector.toShort().toFloat()/32f,(vector shr 16).toShort().toFloat()/32f)
        private const val VERTEX="""#version 460
        void main(){vec2 p=vec2((gl_VertexIndex<<1)&2,gl_VertexIndex&2);gl_Position=vec4(p*2-1,0,1);}
        """
        private const val CONVERT="""#version 460
        layout(set=0,binding=0) uniform sampler2D firstImage;layout(set=0,binding=1) uniform sampler2D secondImage;
        layout(location=0) out vec4 a;layout(location=1) out vec4 b;
        vec4 encode(vec3 v){v=max(v,vec3(0));return vec4(pow(v/(1+v),vec3(1.0/2.2)),1);}
        void main(){ivec2 p=ivec2(gl_FragCoord.xy);a=encode(texelFetch(firstImage,p,0).rgb);b=encode(texelFetch(secondImage,p,0).rgb);}
        """
        private const val UNPACK="""#version 460
        #extension GL_EXT_buffer_reference : require
        #extension GL_EXT_scalar_block_layout : require
        layout(buffer_reference,std430) readonly buffer Words {uint data[];};
        layout(push_constant,scalar) uniform P {Words flow;Words cost;uint width;} p;
        layout(location=0) out vec4 result;
        void main(){uvec2 xy=uvec2(gl_FragCoord.xy);uint i=xy.y*p.width+xy.x,w=p.flow.data[i];
            result=vec4(float(int(w<<16)>>16)/32.0,float(int(w)>>16)/32.0,float(p.cost.data[i]),1);}
        """
    }
}

typealias OpticalFlow

The old name of HardwareOpticalFlow, which read as the general entry point; opticalFlow(...) is that.

@Deprecated("Renamed: this is the NVIDIA-hardware backend", ReplaceWith("HardwareOpticalFlow"))
typealias OpticalFlow = HardwareOpticalFlow