[PR] avutil: direct GPU frame transfers between D3D11 and Vulkan/CUDA (PR #24316)

flowreen via ffmpeg-devel <[email protected]>
Newsgroups gmane.comp.video.ffmpeg.devel
Message-ID <[email protected]>
PR #24316 opened by flowreen
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24316
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24316.patch

This series adds device derivation for D3D11VA and direct GPU frame transfers between D3D11 and Vulkan, and between D3D11 and CUDA. It was sent to ffmpeg-devel as [PATCH 0/4] on 2026-08-25; opening it as a PR here per Philip Langdale's request in that thread. The branch is identical to the mailed series (tip 9f92c81a93, base ca99230f71).

Motivation: the end goal is letting mpv users on a Vulkan VO use the video processing features NVIDIA's driver exposes only through D3D11, RTX Video Super Resolution and RTX Video HDR, without paying for the transfers. mpv can already run D3D11 video processing under a Vulkan VO, or decode with nvdec and filter with D3D11, but today every such frame crosses through system memory twice per hop. With this series the transfer stays on the GPU. The mpv side of this work is under review in https://github.com/mpv-player/mpv/pull/18300 and https://github.com/mpv-player/mpv/pull/18301, and the FFmpeg design was discussed in that first thread from https://github.com/mpv-player/mpv/pull/18300#issuecomment-5124804191 onward. Philip Langdale's guidance there shaped both transfer implementations.

Commit 1 implements device_derive for D3D11VA by matching the source device's LUID, so a derived D3D11 device lands on the same adapter as a Vulkan or CUDA source.

Commit 2 imports single plane D3D11 textures into Vulkan as external memory and copies on the Vulkan queue, ordered against D3D11 work by a shared D3D11 fence imported as a timeline semaphore.

Commit 3 extends that to NV12 and P010. A two plane texture cannot be imported whole, so each plane moves through a shared single plane texture written and read by a compute shader on the D3D11 side.

Commit 4 adds CUDA transfers for D3D11 frames through CUDA's external memory and external semaphore import, fully GPU chained in both directions.

ffnvcodec currently lacks CU_EXTERNAL_MEMORY_HANDLE_TYPE_D3D11_RESOURCE and CUDA_EXTERNAL_MEMORY_DEDICATED, so commit 4 defines them locally. I am happy to send a nv-codec-headers patch adding them (and the D3D11 fence semaphore type) if that is preferred.

On the verification scope, stated plainly: this was developed and tested on one machine, Windows 11 with an RTX 5090. Testing covered byte exact transfer matrices in both directions for nv12, p010, bgra, x2bgr10 and rgbaf16 including 4K and odd sizes, texture arrays with nonzero slice indices, adversarial cases (mismatched formats, keyed mutex textures, stress and coherency loops), Vulkan validation layers, and mpv end to end route checks with screenshot comparison against the native D3D11 path. I have no AMD or Intel results. Rebased onto current master and re-verified before sending.

While re-verifying on master I hit an unrelated regression: since 4ec050211a ("avutil/hwcontext_vulkan: drop NVIDIA/MoltenVK host transfer blocklist"), allocating a Vulkan frame pool for any single plane RGB format fails on the NVIDIA proprietary driver with "No memory type found for flags 0x1" (bgra, rgba, x2bgr10le, rgbaf16le all fail, nv12 and p010 are unaffected). Bisected and reproduced on unpatched master; the frame pool images now carry VK_IMAGE_USAGE_HOST_TRANSFER_BIT and the driver then offers no device local memory type for them. With that commit reverted locally, the full matrix for this series passes on current master: the transfer matrix with zero failures and the adversarial suite 15 of 15. I am reporting that regression separately from this series; mentioning it here so a re
 viewer reproducing on NVIDIA does not misattribute the allocation failures to these patches.

Measured on this machine at 1080p, a direct hop is roughly 5x faster than the system memory path it replaces.

Disclosure: this series was written with AI assistance (Claude Code). The AI implemented the design following Philip Langdale's guidance from the mpv thread, probed the driver behavior and ran the verification on my machine. I have reviewed it, I understand what it changes and why, I take responsibility for it, and review responses will be written by me.

I have a lot of respect for this project and I do not claim this is the best possible design. If reviewers see a better path I am happy to rework the series, and I hope it is a useful starting point for getting these transfers into FFmpeg either way.


>From d25d588cac7382f320a14cc7fca2b0b240cb85ba Mon Sep 17 00:00:00 2001
From: flowreen <[email protected]>
Date: Sun, 26 Jul 2026 13:18:34 +0200
Subject: [PATCH 1/4] avutil/hwcontext_d3d11va: implement device derivation

D3D11VA implemented no device_derive, so av_hwdevice_ctx_create_derived()
targeting AV_HWDEVICE_TYPE_D3D11VA always failed with ENOSYS and callers
had to fall back to creating a standalone device.

A standalone device is created with a NULL DXGI adapter, which selects
whichever adapter enumerates first rather than the one the source device
runs on. On a multi-adapter system that can be a different GPU entirely,
so a caller ends up using a device on another GPU than the component that
provided the source device.

Derive by matching the source device's LUID against the AdapterLuid of
each DXGI adapter and creating the device on the one that matches. The
LUID comes from VkPhysicalDeviceIDProperties for a Vulkan source and from
cuDeviceGetLuid() for a CUDA source. This reuses d3d11va_device_create()
with the resulting adapter index, so the option, debug and multithread
handling is not duplicated.

cuDeviceGetLuid() is loaded optionally by ffnvcodec, so its absence on an
older driver is reported as ENOSYS rather than treated as an error.
---
 libavutil/hwcontext_d3d11va.c | 129 ++++++++++++++++++++++++++++++++++
 1 file changed, 129 insertions(+)

diff --git a/libavutil/hwcontext_d3d11va.c b/libavutil/hwcontext_d3d11va.c
index 834c2ce3dd..904ccd55d5 100644
--- a/libavutil/hwcontext_d3d11va.c
+++ b/libavutil/hwcontext_d3d11va.c
@@ -35,6 +35,14 @@
 #include "hwcontext.h"
 #include "hwcontext_d3d11va.h"
 #include "hwcontext_internal.h"
+#if CONFIG_CUDA
+#include "cuda_check.h"
+#include "hwcontext_cuda_internal.h"
+#define CHECK_CU(x) FF_CUDA_CHECK_DL(cuda_cu, cu, x)
+#endif
+#if CONFIG_VULKAN
+#include "hwcontext_vulkan.h"
+#endif
 #include "imgutils.h"
 #include "mem.h"
 #include "pixdesc.h"
@@ -729,6 +737,126 @@ static int d3d11va_device_create(AVHWDeviceContext *ctx, const char *device,
     return 0;
 }
 
+static int d3d11va_device_find_adapter_by_luid(AVHWDeviceContext *ctx,
+                                               const LUID *luid)
+{
+    HRESULT hr;
+    IDXGIAdapter *adapter = NULL;
+    IDXGIFactory2 *factory;
+    int adapter_id = 0;
+    int ret = -1;
+
+    hr = mCreateDXGIFactory(&IID_IDXGIFactory2, (void **)&factory);
+    if (FAILED(hr)) {
+        av_log(ctx, AV_LOG_ERROR, "CreateDXGIFactory returned error\n");
+        return -1;
+    }
+
+    while (IDXGIFactory2_EnumAdapters(factory, adapter_id++, &adapter) != DXGI_ERROR_NOT_FOUND) {
+        DXGI_ADAPTER_DESC adapter_desc;
+
+        hr = IDXGIAdapter2_GetDesc(adapter, &adapter_desc);
+        IDXGIAdapter_Release(adapter);
+        if (FAILED(hr)) {
+            av_log(ctx, AV_LOG_DEBUG, "IDXGIAdapter2_GetDesc returned error, try next adapter\n");
+            continue;
+        }
+
+        if (adapter_desc.AdapterLuid.LowPart  == luid->LowPart &&
+            adapter_desc.AdapterLuid.HighPart == luid->HighPart) {
+            ret = adapter_id - 1;
+            break;
+        }
+    }
+
+    IDXGIFactory2_Release(factory);
+    return ret;
+}
+
+static int d3d11va_device_derive(AVHWDeviceContext *ctx,
+                                 AVHWDeviceContext *src_ctx,
+                                 AVDictionary *opts, int flags)
+{
+    LUID luid;
+    int adapter, ret;
+    char adapter_str[16];
+
+    if ((ret = ff_thread_once(&functions_loaded, load_functions)) != 0)
+        return AVERROR_UNKNOWN;
+    if (!mD3D11CreateDevice || !mCreateDXGIFactory) {
+        av_log(ctx, AV_LOG_ERROR, "Failed to load D3D11 library or its functions\n");
+        return AVERROR_UNKNOWN;
+    }
+
+    switch (src_ctx->type) {
+#if CONFIG_VULKAN
+    case AV_HWDEVICE_TYPE_VULKAN: {
+        AVVulkanDeviceContext *src_hwctx = src_ctx->hwctx;
+        PFN_vkGetPhysicalDeviceProperties2 prop_fn;
+        VkPhysicalDeviceIDProperties vk_idp = {
+            .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES,
+        };
+        VkPhysicalDeviceProperties2 vk_dev_props = {
+            .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2,
+            .pNext = &vk_idp,
+        };
+
+        prop_fn = (PFN_vkGetPhysicalDeviceProperties2)
+            src_hwctx->get_proc_addr(src_hwctx->inst,
+                                     "vkGetPhysicalDeviceProperties2");
+        if (!prop_fn)
+            return AVERROR(ENOSYS);
+
+        prop_fn(src_hwctx->phys_dev, &vk_dev_props);
+        if (!vk_idp.deviceLUIDValid) {
+            av_log(ctx, AV_LOG_VERBOSE, "Source device does not expose a LUID\n");
+            return AVERROR(ENOSYS);
+        }
+
+        // VK_LUID_SIZE is defined as 8, which is also the size of a LUID.
+        memcpy(&luid, vk_idp.deviceLUID, sizeof(luid));
+        break;
+    }
+#endif
+#if CONFIG_CUDA
+    case AV_HWDEVICE_TYPE_CUDA: {
+        AVHWDeviceContext *cuda_cu = src_ctx;
+        AVCUDADeviceContext *src_hwctx = src_ctx->hwctx;
+        AVCUDADeviceContextInternal *cu_internal = src_hwctx->internal;
+        CudaFunctions *cu = cu_internal->cuda_dl;
+        unsigned int node_mask;
+
+        // Optionally loaded, so it can be absent on an older driver.
+        if (!cu->cuDeviceGetLuid) {
+            av_log(ctx, AV_LOG_VERBOSE, "cuDeviceGetLuid is unavailable\n");
+            return AVERROR(ENOSYS);
+        }
+
+        // The LUID is documented as 8 bytes, matching a Windows LUID.
+        ret = CHECK_CU(cu->cuDeviceGetLuid((char *)&luid, &node_mask,
+                                           cu_internal->cuda_device));
+        if (ret < 0) {
+            av_log(ctx, AV_LOG_ERROR, "Unable to get LUID from CUDA\n");
+            return AVERROR_EXTERNAL;
+        }
+        break;
+    }
+#endif
+    default:
+        return AVERROR(ENOSYS);
+    }
+
+    adapter = d3d11va_device_find_adapter_by_luid(ctx, &luid);
+    if (adapter < 0) {
+        av_log(ctx, AV_LOG_ERROR, "Failed to find a d3d11va adapter matching "
+               "the source device\n");
+        return AVERROR(ENODEV);
+    }
+
+    snprintf(adapter_str, sizeof(adapter_str), "%d", adapter);
+    return d3d11va_device_create(ctx, adapter_str, opts, flags);
+}
+
 const HWContextType ff_hwcontext_type_d3d11va = {
     .type                 = AV_HWDEVICE_TYPE_D3D11VA,
     .name                 = "D3D11VA",
@@ -737,6 +865,7 @@ const HWContextType ff_hwcontext_type_d3d11va = {
     .frames_hwctx_size    = sizeof(D3D11VAFramesContext),
 
     .device_create        = d3d11va_device_create,
+    .device_derive        = d3d11va_device_derive,
     .device_init          = d3d11va_device_init,
     .device_uninit        = d3d11va_device_uninit,
     .frames_get_constraints = d3d11va_frames_get_constraints,
-- 
2.52.0


>From 4ed6535306c4a599c0282c2414b818ea8decf85c Mon Sep 17 00:00:00 2001
From: flowreen <[email protected]>
Date: Sun, 26 Jul 2026 22:19:14 +0200
Subject: [PATCH 2/4] avutil/hwcontext_vulkan: add D3D11 to Vulkan frame
 transfers

Vulkan frames can now be transferred to and from single plane D3D11
textures. The texture is imported into Vulkan and the copy runs on the
GPU, ordered against D3D11 work by a shared D3D11 fence imported as a
timeline semaphore. Callers no longer need to route these frames
through system memory.

Unsupported cases report ENOSYS instead of failing. The d3d11va side
now reports it too. When that happens, av_hwframe_transfer_data()
tries the other frames context instead.
---
 libavutil/hwcontext_d3d11va.c |   8 +-
 libavutil/hwcontext_vulkan.c  | 676 ++++++++++++++++++++++++++++++++++
 libavutil/vulkan_functions.h  |   5 +-
 3 files changed, 687 insertions(+), 2 deletions(-)

diff --git a/libavutil/hwcontext_d3d11va.c b/libavutil/hwcontext_d3d11va.c
index 904ccd55d5..4efa302792 100644
--- a/libavutil/hwcontext_d3d11va.c
+++ b/libavutil/hwcontext_d3d11va.c
@@ -454,9 +454,15 @@ static int d3d11va_transfer_data(AVHWFramesContext *ctx, AVFrame *dst,
     HRESULT hr;
     int res;
 
-    if (frame->hw_frames_ctx->data != (uint8_t *)ctx || other->format != ctx->sw_format)
+    if (frame->hw_frames_ctx->data != (uint8_t *)ctx)
         return AVERROR(EINVAL);
 
+    /* Not a transfer to or from a software frame we can handle. Report this as
+     * unimplemented rather than invalid, so that a hardware to hardware
+     * transfer can still be tried from the other side. */
+    if (other->format != ctx->sw_format)
+        return AVERROR(ENOSYS);
+
     device_hwctx->lock(device_hwctx->lock_ctx);
 
     if (!s->staging_texture) {
diff --git a/libavutil/hwcontext_vulkan.c b/libavutil/hwcontext_vulkan.c
index 668b3c53d9..78b87570d3 100644
--- a/libavutil/hwcontext_vulkan.c
+++ b/libavutil/hwcontext_vulkan.c
@@ -25,6 +25,14 @@
 #include <windows.h> /* Included to prevent conflicts with CreateSemaphore */
 #include <versionhelpers.h>
 #include "compat/w32dlfcn.h"
+#if CONFIG_D3D11VA
+#define COBJMACROS
+#include <initguid.h>
+#include <d3d11.h>
+#include <d3d11_4.h>
+#include <dxgi1_2.h>
+#include "hwcontext_d3d11va.h"
+#endif
 #else
 #include <dlfcn.h>
 #include <unistd.h>
@@ -156,6 +164,12 @@ typedef struct VulkanDevicePriv {
     /* Opaque FD external semaphore properties */
     VkExternalSemaphoreProperties ext_sem_props_opaque;
 
+#ifdef _WIN32
+    /* D3D12 fence external semaphore properties for timeline semaphores.
+     * D3D11 fences are shared through the same handle type. */
+    VkExternalSemaphoreProperties ext_sem_props_d3d12_fence;
+#endif
+
     /* Enabled features */
     VulkanDeviceFeatures feats;
 
@@ -208,6 +222,14 @@ typedef struct VulkanFramesPriv {
 
     /* Set when physical device reports DEDICATED_ONLY for DMA-BUF export (try_export_flags) */
     int export_requires_dedicated;
+
+#if CONFIG_D3D11VA
+    /* Shared D3D11 fences imported as timeline semaphores, one for each D3D11
+     * device that frames have been transferred to or from */
+    pthread_mutex_t d3d11_sync_lock;
+    int d3d11_sync_lock_init;
+    struct D3D11SyncState *d3d11_sync;
+#endif
 } VulkanFramesPriv;
 
 typedef struct AVVkFrameInternal {
@@ -2026,6 +2048,23 @@ static int vulkan_device_init(AVHWDeviceContext *ctx)
                                                      &ext_sem_props_info,
                                                      &p->ext_sem_props_opaque);
 
+#ifdef _WIN32
+    /* D3D12 fence properties, queried for a timeline semaphore */
+    {
+        VkSemaphoreTypeCreateInfo timeline_info = {
+            .sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO,
+            .semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE,
+        };
+        ext_sem_props_info.pNext = &timeline_info;
+        ext_sem_props_info.handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_D3D12_FENCE_BIT;
+        p->ext_sem_props_d3d12_fence.sType = VK_STRUCTURE_TYPE_EXTERNAL_SEMAPHORE_PROPERTIES;
+        vk->GetPhysicalDeviceExternalSemaphoreProperties(hwctx->phys_dev,
+                                                         &ext_sem_props_info,
+                                                         &p->ext_sem_props_d3d12_fence);
+        ext_sem_props_info.pNext = NULL;
+    }
+#endif
+
     qf = av_malloc_array(qf_num, sizeof(VkQueueFamilyProperties2));
     if (!qf)
         return AVERROR(ENOMEM);
@@ -2972,6 +3011,46 @@ static void unlock_frame(AVHWFramesContext *fc, AVVkFrame *vkf)
     pthread_mutex_unlock(&vkf->internal->update_mutex);
 }
 
+#if CONFIG_D3D11VA
+/* A D3D11 fence shared with Vulkan as a timeline semaphore. D3D11 signals it
+ * on its immediate context after the commands that produce or consume a
+ * texture, and the transfer submission waits on the imported side, which
+ * orders the copy against D3D11 work entirely on the GPU. One state is kept
+ * for each D3D11 device that frames have been transferred to or from. */
+typedef struct D3D11SyncState {
+    struct D3D11SyncState *next;
+    ID3D11Device          *dev;   /* identity of the paired device */
+    ID3D11DeviceContext4  *ctx4;
+    ID3D11Fence           *fence;
+    VkSemaphore            sem;   /* the fence imported into Vulkan */
+    uint64_t               value; /* last signaled point, device lock held */
+} D3D11SyncState;
+
+static void d3d11_sync_states_free(AVHWFramesContext *hwfc)
+{
+    VulkanFramesPriv *fp = hwfc->hwctx;
+    VulkanDevicePriv *p = hwfc->device_ctx->hwctx;
+    AVVulkanDeviceContext *hwctx = &p->p;
+    FFVulkanFunctions *vk = &p->vkctx.vkfn;
+    D3D11SyncState *sync = fp->d3d11_sync;
+
+    while (sync) {
+        D3D11SyncState *next = sync->next;
+        if (sync->sem)
+            vk->DestroySemaphore(hwctx->act_dev, sync->sem, hwctx->alloc);
+        if (sync->fence)
+            ID3D11Fence_Release(sync->fence);
+        if (sync->ctx4)
+            ID3D11DeviceContext4_Release(sync->ctx4);
+        if (sync->dev)
+            ID3D11Device_Release(sync->dev);
+        av_free(sync);
+        sync = next;
+    }
+    fp->d3d11_sync = NULL;
+}
+#endif
+
 static void vulkan_frames_uninit(AVHWFramesContext *hwfc)
 {
     VulkanDevicePriv *p = hwfc->device_ctx->hwctx;
@@ -2987,6 +3066,14 @@ static void vulkan_frames_uninit(AVHWFramesContext *hwfc)
     ff_vk_exec_pool_free(&p->vkctx, &fp->upload_exec);
     ff_vk_exec_pool_free(&p->vkctx, &fp->download_exec);
 
+#if CONFIG_D3D11VA
+    /* After the exec pools have drained every submission that could still
+     * wait on the imported semaphores */
+    d3d11_sync_states_free(hwfc);
+    if (fp->d3d11_sync_lock_init)
+        pthread_mutex_destroy(&fp->d3d11_sync_lock);
+#endif
+
     av_refstruct_pool_uninit(&fp->tmp);
 }
 
@@ -3136,6 +3223,13 @@ static int vulkan_frames_init(AVHWFramesContext *hwfc)
                        ((hwctx->usage & VK_IMAGE_USAGE_VIDEO_DECODE_DPB_BIT_KHR) &&
                         !(hwctx->usage & VK_IMAGE_USAGE_VIDEO_DECODE_DST_BIT_KHR)));
 
+#if CONFIG_D3D11VA
+    err = pthread_mutex_init(&fp->d3d11_sync_lock, NULL);
+    if (err != 0)
+        return AVERROR(err);
+    fp->d3d11_sync_lock_init = 1;
+#endif
+
     /* Defaults */
     if (!hwctx->nb_layers)
         hwctx->nb_layers = 1;
@@ -5040,12 +5134,587 @@ end:
     return err;
 }
 
+#if CONFIG_D3D11VA
+
+static VkFormat d3d11_to_vulkan_fmt(DXGI_FORMAT f)
+{
+    switch (f) {
+    case DXGI_FORMAT_B8G8R8A8_UNORM:     return VK_FORMAT_B8G8R8A8_UNORM;
+    case DXGI_FORMAT_R8G8B8A8_UNORM:     return VK_FORMAT_R8G8B8A8_UNORM;
+    case DXGI_FORMAT_R10G10B10A2_UNORM:  return VK_FORMAT_A2B10G10R10_UNORM_PACK32;
+    case DXGI_FORMAT_R16G16B16A16_FLOAT: return VK_FORMAT_R16G16B16A16_SFLOAT;
+    default:                             return VK_FORMAT_UNDEFINED;
+    }
+}
+
+typedef struct D3D11Import {
+    VkImage        img;
+    VkDeviceMemory mem;
+} D3D11Import;
+
+static void d3d11_import_free(AVHWFramesContext *hwfc, D3D11Import *im)
+{
+    VulkanDevicePriv *p = hwfc->device_ctx->hwctx;
+    AVVulkanDeviceContext *hwctx = &p->p;
+    FFVulkanFunctions *vk = &p->vkctx.vkfn;
+
+    if (im->img)
+        vk->DestroyImage(hwctx->act_dev, im->img, hwctx->alloc);
+    if (im->mem)
+        vk->FreeMemory(hwctx->act_dev, im->mem, hwctx->alloc);
+    im->img = VK_NULL_HANDLE;
+    im->mem = VK_NULL_HANDLE;
+}
+
+/* Find or create the fence pair for this D3D11 device. Setups on which the
+ * fence cannot be created or imported report unimplemented, so the caller
+ * falls back to system memory. */
+static int d3d11_sync_state_get(AVHWFramesContext *hwfc,
+                                AVD3D11VADeviceContext *d3d_hw,
+                                D3D11SyncState **out)
+{
+    VulkanFramesPriv *fp = hwfc->hwctx;
+    VulkanDevicePriv *p = hwfc->device_ctx->hwctx;
+    AVVulkanDeviceContext *hwctx = &p->p;
+    FFVulkanFunctions *vk = &p->vkctx.vkfn;
+    VkSemaphoreTypeCreateInfo timeline_info = {
+        .sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO,
+        .semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE,
+    };
+    VkSemaphoreCreateInfo sem_info = {
+        .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
+        .pNext = &timeline_info,
+    };
+    VkImportSemaphoreWin32HandleInfoKHR imp;
+    D3D11SyncState *sync = NULL;
+    ID3D11Device5 *dev5 = NULL;
+    HANDLE handle = NULL;
+    VkResult ret;
+    HRESULT hr;
+    int err = AVERROR(ENOSYS);
+
+    pthread_mutex_lock(&fp->d3d11_sync_lock);
+
+    for (sync = fp->d3d11_sync; sync; sync = sync->next) {
+        if (sync->dev == d3d_hw->device) {
+            pthread_mutex_unlock(&fp->d3d11_sync_lock);
+            *out = sync;
+            return 0;
+        }
+    }
+
+    if (!(p->vkctx.extensions & FF_VK_EXT_EXTERNAL_WIN32_SEM) ||
+        !(p->ext_sem_props_d3d12_fence.externalSemaphoreFeatures &
+          VK_EXTERNAL_SEMAPHORE_FEATURE_IMPORTABLE_BIT)) {
+        av_log(hwfc, AV_LOG_DEBUG, "D3D12 fence import is not supported\n");
+        goto fail;
+    }
+
+    sync = av_mallocz(sizeof(*sync));
+    if (!sync) {
+        err = AVERROR(ENOMEM);
+        goto fail;
+    }
+
+    /* Fences arrived in D3D11.4, so a device that predates them has nothing
+     * the copy could synchronize against. */
+    hr = ID3D11Device_QueryInterface(d3d_hw->device, &IID_ID3D11Device5,
+                                     (void **)&dev5);
+    if (SUCCEEDED(hr))
+        hr = ID3D11DeviceContext_QueryInterface(d3d_hw->device_context,
+                                                &IID_ID3D11DeviceContext4,
+                                                (void **)&sync->ctx4);
+    if (SUCCEEDED(hr))
+        hr = ID3D11Device5_CreateFence(dev5, 0, D3D11_FENCE_FLAG_SHARED,
+                                       &IID_ID3D11Fence, (void **)&sync->fence);
+    if (SUCCEEDED(hr))
+        hr = ID3D11Fence_CreateSharedHandle(sync->fence, NULL, GENERIC_ALL,
+                                            NULL, &handle);
+    if (dev5)
+        ID3D11Device5_Release(dev5);
+    if (FAILED(hr) || !handle) {
+        av_log(hwfc, AV_LOG_DEBUG, "Unable to create a shared fence (%lx)\n",
+               (long)hr);
+        goto fail;
+    }
+
+    ret = vk->CreateSemaphore(hwctx->act_dev, &sem_info, hwctx->alloc, &sync->sem);
+    if (ret != VK_SUCCESS) {
+        av_log(hwfc, AV_LOG_ERROR, "Cannot create the import semaphore: %s\n",
+               ff_vk_ret2str(ret));
+        err = AVERROR_EXTERNAL;
+        goto fail;
+    }
+
+    imp = (VkImportSemaphoreWin32HandleInfoKHR) {
+        .sType      = VK_STRUCTURE_TYPE_IMPORT_SEMAPHORE_WIN32_HANDLE_INFO_KHR,
+        .semaphore  = sync->sem,
+        .handleType = VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_D3D12_FENCE_BIT,
+        .handle     = handle,
+    };
+    ret = vk->ImportSemaphoreWin32HandleKHR(hwctx->act_dev, &imp);
+    if (ret != VK_SUCCESS) {
+        av_log(hwfc, AV_LOG_DEBUG, "Cannot import the fence: %s\n",
+               ff_vk_ret2str(ret));
+        goto fail;
+    }
+    CloseHandle(handle);
+
+    ID3D11Device_AddRef(d3d_hw->device);
+    sync->dev  = d3d_hw->device;
+    sync->next = fp->d3d11_sync;
+    fp->d3d11_sync = sync;
+    pthread_mutex_unlock(&fp->d3d11_sync_lock);
+
+    *out = sync;
+    return 0;
+
+fail:
+    if (handle)
+        CloseHandle(handle);
+    if (sync) {
+        if (sync->sem)
+            vk->DestroySemaphore(hwctx->act_dev, sync->sem, hwctx->alloc);
+        if (sync->fence)
+            ID3D11Fence_Release(sync->fence);
+        if (sync->ctx4)
+            ID3D11DeviceContext4_Release(sync->ctx4);
+        av_free(sync);
+    }
+    pthread_mutex_unlock(&fp->d3d11_sync_lock);
+    return err;
+}
+
+/* Import a shared D3D11 texture as a VkImage aliasing the same memory. The
+ * texture must be single-subresource and created with D3D11_RESOURCE_MISC_SHARED. */
+static int d3d11_import(AVHWFramesContext *hwfc, ID3D11Texture2D *tex,
+                        D3D11Import *im)
+{
+    VulkanDevicePriv *p = hwfc->device_ctx->hwctx;
+    AVVulkanDeviceContext *hwctx = &p->p;
+    FFVulkanFunctions *vk = &p->vkctx.vkfn;
+    VkExternalMemoryHandleTypeFlagBits handle_type;
+    VkExternalMemoryImageCreateInfo ext_info;
+    VkMemoryWin32HandlePropertiesKHR hprops = {
+        .sType = VK_STRUCTURE_TYPE_MEMORY_WIN32_HANDLE_PROPERTIES_KHR,
+    };
+    VkImageMemoryRequirementsInfo2 req_info = {
+        .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_REQUIREMENTS_INFO_2,
+    };
+    VkMemoryRequirements2 req = {
+        .sType = VK_STRUCTURE_TYPE_MEMORY_REQUIREMENTS_2,
+    };
+    D3D11_TEXTURE2D_DESC desc;
+    HANDLE handle = NULL;
+    uint32_t bits;
+    int nt_handle = 0;
+    VkImageUsageFlags usage;
+    VkResult ret;
+    HRESULT hr;
+    int index, err = AVERROR_EXTERNAL;
+
+    memset(im, 0, sizeof(*im));
+
+    ID3D11Texture2D_GetDesc(tex, &desc);
+
+    if (desc.ArraySize != 1) {
+        av_log(hwfc, AV_LOG_DEBUG, "Cannot import a D3D11 texture array\n");
+        return AVERROR(ENOSYS);
+    }
+    /* The copy below covers the whole frame, so anything but an exact match
+     * would read or write outside one of the two images. */
+    if (desc.Width != hwfc->width || desc.Height != hwfc->height) {
+        av_log(hwfc, AV_LOG_DEBUG, "D3D11 texture is %ux%u, expected %dx%d\n",
+               (unsigned)desc.Width, (unsigned)desc.Height,
+               hwfc->width, hwfc->height);
+        return AVERROR(ENOSYS);
+    }
+    /* Keyed mutex sharing only guarantees coherency to a user that acquires the
+     * mutex around every access, which this code does not do. Such a texture
+     * imports without complaint and then reads back contents that predate what
+     * D3D11 wrote, so refuse it and let the caller use system memory. */
+    if (desc.MiscFlags & D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX) {
+        av_log(hwfc, AV_LOG_DEBUG, "D3D11 texture is shared through a keyed mutex\n");
+        return AVERROR(ENOSYS);
+    }
+    if (desc.MiscFlags & D3D11_RESOURCE_MISC_SHARED_NTHANDLE) {
+        nt_handle = 1;
+        handle_type = VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D11_TEXTURE_BIT;
+    } else if (desc.MiscFlags & D3D11_RESOURCE_MISC_SHARED) {
+        handle_type = VK_EXTERNAL_MEMORY_HANDLE_TYPE_D3D11_TEXTURE_KMT_BIT;
+    } else {
+        av_log(hwfc, AV_LOG_DEBUG, "D3D11 texture is not shared\n");
+        return AVERROR(ENOSYS);
+    }
+    if (d3d11_to_vulkan_fmt(desc.Format) == VK_FORMAT_UNDEFINED) {
+        av_log(hwfc, AV_LOG_DEBUG, "DXGI format %d cannot be imported\n",
+               (int)desc.Format);
+        return AVERROR(ENOSYS);
+    }
+
+    /* The layout a driver picks for an image can depend on how it may be
+     * used, so mirror the texture's bind flags. */
+    usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
+    if (desc.BindFlags & D3D11_BIND_SHADER_RESOURCE)
+        usage |= VK_IMAGE_USAGE_SAMPLED_BIT;
+    if (desc.BindFlags & D3D11_BIND_RENDER_TARGET)
+        usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
+    if (desc.BindFlags & D3D11_BIND_UNORDERED_ACCESS)
+        usage |= VK_IMAGE_USAGE_STORAGE_BIT;
+
+    /* Creating an external image is only defined for combinations the
+     * implementation reports as importable */
+    {
+        VkPhysicalDeviceExternalImageFormatInfo ext_fmt_info = {
+            .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_IMAGE_FORMAT_INFO,
+            .handleType = handle_type,
+        };
+        VkPhysicalDeviceImageFormatInfo2 fmt_info = {
+            .sType  = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_FORMAT_INFO_2,
+            .pNext  = &ext_fmt_info,
+            .format = d3d11_to_vulkan_fmt(desc.Format),
+            .type   = VK_IMAGE_TYPE_2D,
+            .tiling = VK_IMAGE_TILING_OPTIMAL,
+            .usage  = usage,
+        };
+        VkExternalImageFormatProperties ext_fmt_props = {
+            .sType = VK_STRUCTURE_TYPE_EXTERNAL_IMAGE_FORMAT_PROPERTIES,
+        };
+        VkImageFormatProperties2 fmt_props = {
+            .sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2,
+            .pNext = &ext_fmt_props,
+        };
+
+        ret = vk->GetPhysicalDeviceImageFormatProperties2(hwctx->phys_dev,
+                                                          &fmt_info, &fmt_props);
+        if (ret != VK_SUCCESS ||
+            !(ext_fmt_props.externalMemoryProperties.externalMemoryFeatures &
+              VK_EXTERNAL_MEMORY_FEATURE_IMPORTABLE_BIT)) {
+            av_log(hwfc, AV_LOG_DEBUG, "The image cannot import this handle type\n");
+            return AVERROR(ENOSYS);
+        }
+    }
+
+    if (nt_handle) {
+        IDXGIResource1 *res1 = NULL;
+        hr = ID3D11Texture2D_QueryInterface(tex, &IID_IDXGIResource1, (void **)&res1);
+        if (SUCCEEDED(hr)) {
+            hr = IDXGIResource1_CreateSharedHandle(res1, NULL,
+                                                   DXGI_SHARED_RESOURCE_READ |
+                                                   DXGI_SHARED_RESOURCE_WRITE,
+                                                   NULL, &handle);
+            IDXGIResource1_Release(res1);
+        }
+    } else {
+        IDXGIResource *res = NULL;
+        hr = ID3D11Texture2D_QueryInterface(tex, &IID_IDXGIResource, (void **)&res);
+        if (SUCCEEDED(hr)) {
+            hr = IDXGIResource_GetSharedHandle(res, &handle);
+            IDXGIResource_Release(res);
+        }
+    }
+    if (FAILED(hr) || !handle) {
+        av_log(hwfc, AV_LOG_ERROR, "Unable to get a shared handle (%lx)\n", (long)hr);
+        return AVERROR_EXTERNAL;
+    }
+
+    ext_info = (VkExternalMemoryImageCreateInfo) {
+        .sType       = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO,
+        .handleTypes = handle_type,
+    };
+
+    ret = vk->CreateImage(hwctx->act_dev, &(VkImageCreateInfo) {
+            .sType         = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
+            .pNext         = &ext_info,
+            .imageType     = VK_IMAGE_TYPE_2D,
+            .format        = d3d11_to_vulkan_fmt(desc.Format),
+            .extent        = { desc.Width, desc.Height, 1 },
+            .mipLevels     = 1,
+            .arrayLayers   = 1,
+            .samples       = VK_SAMPLE_COUNT_1_BIT,
+            .tiling        = VK_IMAGE_TILING_OPTIMAL,
+            .usage         = usage,
+            .sharingMode   = VK_SHARING_MODE_EXCLUSIVE,
+            .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
+        }, hwctx->alloc, &im->img);
+    if (ret != VK_SUCCESS) {
+        av_log(hwfc, AV_LOG_ERROR, "Cannot create the import image: %s\n",
+               ff_vk_ret2str(ret));
+        goto fail;
+    }
+
+    /* A handle this device cannot import at all, a texture belonging to
+     * another adapter being the likely reason, is a fall back rather than
+     * an error, so report it as unimplemented. */
+    ret = vk->GetMemoryWin32HandlePropertiesKHR(hwctx->act_dev, handle_type,
+                                                handle, &hprops);
+    if (ret != VK_SUCCESS) {
+        av_log(hwfc, AV_LOG_DEBUG, "Cannot query the handle properties: %s\n",
+               ff_vk_ret2str(ret));
+        err = AVERROR(ENOSYS);
+        goto fail;
+    }
+
+    req_info.image = im->img;
+    vk->GetImageMemoryRequirements2(hwctx->act_dev, &req_info, &req);
+
+    bits = req.memoryRequirements.memoryTypeBits & hprops.memoryTypeBits;
+    if (!bits) {
+        av_log(hwfc, AV_LOG_ERROR, "No memory type is common to the image "
+               "and the imported handle\n");
+        goto fail;
+    }
+    for (index = 0; !(bits & (1u << index)); index++)
+        ;
+
+    /* Imports of D3D11 textures are reported as dedicated-only */
+    ret = vk->AllocateMemory(hwctx->act_dev, &(VkMemoryAllocateInfo) {
+            .sType           = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO,
+            .pNext           = &(VkMemoryDedicatedAllocateInfo) {
+                .sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO,
+                .pNext = &(VkImportMemoryWin32HandleInfoKHR) {
+                    .sType      = VK_STRUCTURE_TYPE_IMPORT_MEMORY_WIN32_HANDLE_INFO_KHR,
+                    .handleType = handle_type,
+                    .handle     = handle,
+                },
+                .image = im->img,
+            },
+            .allocationSize  = req.memoryRequirements.size,
+            .memoryTypeIndex = index,
+        }, hwctx->alloc, &im->mem);
+    if (ret != VK_SUCCESS) {
+        av_log(hwfc, AV_LOG_ERROR, "Cannot import the texture memory: %s\n",
+               ff_vk_ret2str(ret));
+        goto fail;
+    }
+
+    ret = vk->BindImageMemory2(hwctx->act_dev, 1, &(VkBindImageMemoryInfo) {
+            .sType  = VK_STRUCTURE_TYPE_BIND_IMAGE_MEMORY_INFO,
+            .image  = im->img,
+            .memory = im->mem,
+        });
+    if (ret != VK_SUCCESS) {
+        av_log(hwfc, AV_LOG_ERROR, "Cannot bind the imported memory: %s\n",
+               ff_vk_ret2str(ret));
+        goto fail;
+    }
+
+    /* Importing an NT handle does not transfer ownership, so ours has to be
+     * closed. KMT handles are not reference counted and must not be closed. */
+    if (nt_handle)
+        CloseHandle(handle);
+
+    return 0;
+
+fail:
+    if (nt_handle)
+        CloseHandle(handle);
+    d3d11_import_free(hwfc, im);
+    return err;
+}
+
+/* Copy between a Vulkan frame and a D3D11 texture. D3D11 cannot import Vulkan
+ * memory, so the texture is always the side that gets imported, and the copy
+ * itself runs on the GPU. */
+static int vulkan_transfer_d3d11(AVHWFramesContext *hwfc, AVFrame *dst,
+                                 const AVFrame *src, int upload)
+{
+    VulkanDevicePriv *p = hwfc->device_ctx->hwctx;
+    VulkanFramesPriv *fp = hwfc->hwctx;
+    FFVulkanFunctions *vk = &p->vkctx.vkfn;
+    AVFrame *hwf = (AVFrame *)(upload ? dst : src);
+    const AVFrame *d3df = upload ? src : dst;
+    AVVkFrame *hwf_vk = (AVVkFrame *)hwf->data[0];
+    AVHWFramesContext *d3d_fc;
+    AVD3D11VADeviceContext *d3d_hw;
+    VkImageMemoryBarrier2 img_bar[AV_NUM_DATA_POINTERS + 1];
+    int nb_img_bar = 0;
+    VkImageCopy region;
+    D3D11SyncState *sync;
+    D3D11Import im;
+    uint64_t sync_point;
+    FFVkExecContext *exec;
+    VkCommandBuffer cmd_buf;
+    int err;
+
+    if ((intptr_t)d3df->data[1] != 0)
+        return AVERROR(ENOSYS); /* an index into a texture array */
+
+    /* Multi-planar textures cannot be imported: their imports are reported as
+     * dedicated-only, a dedicated import always maps the start of the
+     * resource, and no API reports where the other planes begin. Refuse them
+     * and the caller falls back to system memory. */
+    if (av_pix_fmt_count_planes(hwfc->sw_format) != 1)
+        return AVERROR(ENOSYS);
+
+    /* The copy moves bits, so the two sides have to agree on what the bits
+     * mean. Differing formats of the same size would silently reinterpret the
+     * pixels, and differing sizes are not a legal copy at all. */
+    if (!d3df->hw_frames_ctx ||
+        ((AVHWFramesContext *)d3df->hw_frames_ctx->data)->sw_format != hwfc->sw_format)
+        return AVERROR(ENOSYS);
+
+    d3d_fc = (AVHWFramesContext *)d3df->hw_frames_ctx->data;
+    d3d_hw = d3d_fc->device_ctx->hwctx;
+
+    err = d3d11_sync_state_get(hwfc, d3d_hw, &sync);
+    if (err < 0)
+        return err;
+
+    err = d3d11_import(hwfc, (ID3D11Texture2D *)d3df->data[0], &im);
+    if (err < 0)
+        return err;
+
+    /* Signal the fence after the D3D11 commands that produced the texture, or
+     * that may still be reading the one about to be overwritten. The copy
+     * below waits for that point on the imported side, so it is ordered
+     * against D3D11 work without blocking the CPU. Signals go only this way:
+     * a D3D11 wait for a fence value signaled from a Vulkan queue takes a
+     * scheduler timeout of more than a second to wake on current drivers. */
+    d3d_hw->lock(d3d_hw->lock_ctx);
+    sync_point = ++sync->value;
+    ID3D11DeviceContext4_Signal(sync->ctx4, sync->fence, sync_point);
+    ID3D11DeviceContext_Flush(d3d_hw->device_context);
+    d3d_hw->unlock(d3d_hw->lock_ctx);
+
+    exec = ff_vk_exec_get(&p->vkctx, upload ? &fp->upload_exec :
+                                              &fp->download_exec);
+    cmd_buf = exec->buf;
+    err = ff_vk_exec_start(&p->vkctx, exec);
+    if (err < 0) {
+        d3d11_import_free(hwfc, &im);
+        return err;
+    }
+
+    err = ff_vk_exec_add_dep_frame(&p->vkctx, exec, hwf,
+                                   VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
+                                   VK_PIPELINE_STAGE_2_TRANSFER_BIT);
+    if (err < 0)
+        goto fail;
+
+    /* ALL_COMMANDS rather than TRANSFER: the wait has to order the queue
+     * family acquire below too, and acquire operations happen in no defined
+     * stage. */
+    ff_vk_exec_add_dep_wait_sem(&p->vkctx, exec, sync->sem, sync_point,
+                                VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT);
+
+    ff_vk_frame_barrier(&p->vkctx, exec, hwf, img_bar, &nb_img_bar,
+                        VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
+                        VK_PIPELINE_STAGE_2_TRANSFER_BIT_KHR,
+                        upload ? VK_ACCESS_TRANSFER_WRITE_BIT :
+                                 VK_ACCESS_TRANSFER_READ_BIT,
+                        upload ? VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL :
+                                 VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+                        p->nb_img_qfs > 1 ? VK_QUEUE_FAMILY_IGNORED : p->img_qfs[0]);
+
+    /* Acquire the imported image from the external owner. When we are reading
+     * it, the contents D3D11 left behind have to be preserved, so it cannot
+     * be acquired from VK_IMAGE_LAYOUT_UNDEFINED. */
+    img_bar[nb_img_bar++] = (VkImageMemoryBarrier2) {
+        .sType         = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
+        .srcStageMask  = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
+        .dstStageMask  = VK_PIPELINE_STAGE_2_TRANSFER_BIT,
+        .srcAccessMask = 0,
+        .dstAccessMask = upload ? VK_ACCESS_2_TRANSFER_READ_BIT :
+                                  VK_ACCESS_2_TRANSFER_WRITE_BIT,
+        .oldLayout     = upload ? VK_IMAGE_LAYOUT_GENERAL :
+                                  VK_IMAGE_LAYOUT_UNDEFINED,
+        .newLayout     = upload ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL :
+                                  VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+        /* This is an ownership acquire, so the destination has to be the
+         * queue family the command buffer itself was allocated from. */
+        .srcQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL,
+        .dstQueueFamilyIndex = exec->qf,
+        .image               = im.img,
+        .subresourceRange    = {
+            .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+            .levelCount = 1,
+            .layerCount = 1,
+        },
+    };
+
+    vk->CmdPipelineBarrier2(cmd_buf, &(VkDependencyInfo) {
+            .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
+            .pImageMemoryBarriers    = img_bar,
+            .imageMemoryBarrierCount = nb_img_bar,
+        });
+
+    region = (VkImageCopy) {
+        .srcSubresource = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .layerCount = 1 },
+        .dstSubresource = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .layerCount = 1 },
+        .extent         = { hwfc->width, hwfc->height, 1 },
+    };
+    if (upload)
+        vk->CmdCopyImage(cmd_buf, im.img, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+                         hwf_vk->img[0], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+                         1, &region);
+    else
+        vk->CmdCopyImage(cmd_buf, hwf_vk->img[0], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+                         im.img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+                         1, &region);
+
+    /* Release the imported image back to the external owner, which makes the
+     * write available to D3D11 when this was a download. */
+    img_bar[0] = (VkImageMemoryBarrier2) {
+        .sType         = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
+        .srcStageMask  = VK_PIPELINE_STAGE_2_TRANSFER_BIT,
+        .dstStageMask  = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
+        .srcAccessMask = upload ? VK_ACCESS_2_TRANSFER_READ_BIT :
+                                  VK_ACCESS_2_TRANSFER_WRITE_BIT,
+        .dstAccessMask = 0,
+        .oldLayout     = upload ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL :
+                                  VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+        .newLayout     = VK_IMAGE_LAYOUT_GENERAL,
+        .srcQueueFamilyIndex = exec->qf,
+        .dstQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL,
+        .image               = im.img,
+        .subresourceRange    = {
+            .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+            .levelCount = 1,
+            .layerCount = 1,
+        },
+    };
+
+    vk->CmdPipelineBarrier2(cmd_buf, &(VkDependencyInfo) {
+            .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
+            .pImageMemoryBarriers    = img_bar,
+            .imageMemoryBarrierCount = 1,
+        });
+
+    err = ff_vk_exec_submit(&p->vkctx, exec);
+    if (err >= 0) {
+        /* The D3D11 side cannot wait for the copy on the GPU, see above, so
+         * the transfer completes before it returns, like the system memory
+         * paths do. */
+        ff_vk_exec_wait(&p->vkctx, exec);
+    }
+
+    d3d11_import_free(hwfc, &im);
+
+    return err;
+
+fail:
+    ff_vk_exec_discard(&p->vkctx, exec);
+    d3d11_import_free(hwfc, &im);
+    return err;
+}
+
+#endif /* CONFIG_D3D11VA */
+
 static int vulkan_transfer_data_to(AVHWFramesContext *hwfc, AVFrame *dst,
                                    const AVFrame *src)
 {
     av_unused VulkanDevicePriv *p = hwfc->device_ctx->hwctx;
 
     switch (src->format) {
+#if CONFIG_D3D11VA
+    case AV_PIX_FMT_D3D11:
+        if (p->vkctx.extensions & FF_VK_EXT_EXTERNAL_WIN32_MEMORY)
+            return vulkan_transfer_d3d11(hwfc, dst, src, 1);
+        /* Deliberately not falling through: the CUDA case below would then be
+         * reached with a D3D11 frame. The generic path rejects hw frames too. */
+        return AVERROR(ENOSYS);
+#endif
 #if CONFIG_CUDA
     case AV_PIX_FMT_CUDA:
 #ifdef _WIN32
@@ -5167,6 +5836,13 @@ static int vulkan_transfer_data_from(AVHWFramesContext *hwfc, AVFrame *dst,
     av_unused VulkanDevicePriv *p = hwfc->device_ctx->hwctx;
 
     switch (dst->format) {
+#if CONFIG_D3D11VA
+    case AV_PIX_FMT_D3D11:
+        if (p->vkctx.extensions & FF_VK_EXT_EXTERNAL_WIN32_MEMORY)
+            return vulkan_transfer_d3d11(hwfc, dst, src, 0);
+        /* Deliberately not falling through, as above. */
+        return AVERROR(ENOSYS);
+#endif
 #if CONFIG_CUDA
     case AV_PIX_FMT_CUDA:
 #ifdef _WIN32
diff --git a/libavutil/vulkan_functions.h b/libavutil/vulkan_functions.h
index 16398ab9d0..a3cff77bda 100644
--- a/libavutil/vulkan_functions.h
+++ b/libavutil/vulkan_functions.h
@@ -156,6 +156,7 @@ typedef uint64_t FFVulkanExtensions;
     MACRO(1, 1, FF_VK_EXT_NO_FLAG,              CmdPipelineBarrier)                      \
     MACRO(1, 1, FF_VK_EXT_NO_FLAG,              CmdCopyBufferToImage)                    \
     MACRO(1, 1, FF_VK_EXT_NO_FLAG,              CmdCopyImageToBuffer)                    \
+    MACRO(1, 1, FF_VK_EXT_NO_FLAG,              CmdCopyImage)                            \
     MACRO(1, 1, FF_VK_EXT_NO_FLAG,              CmdClearColorImage)                                    \
     MACRO(1, 1, FF_VK_EXT_NO_FLAG,              CmdCopyBuffer)                                         \
     MACRO(1, 1, FF_VK_EXT_NO_FLAG,              CmdUpdateBuffer)                                       \
@@ -265,7 +266,9 @@ typedef uint64_t FFVulkanExtensions;
 /* Macro containing every win32 specific function that we utilize in our codebase */
 #define FN_LIST_WIN32(MACRO)                                                             \
     MACRO(1, 1, FF_VK_EXT_EXTERNAL_WIN32_SEM,    GetSemaphoreWin32HandleKHR)             \
-    MACRO(1, 1, FF_VK_EXT_EXTERNAL_WIN32_MEMORY, GetMemoryWin32HandleKHR)
+    MACRO(1, 1, FF_VK_EXT_EXTERNAL_WIN32_SEM,    ImportSemaphoreWin32HandleKHR)          \
+    MACRO(1, 1, FF_VK_EXT_EXTERNAL_WIN32_MEMORY, GetMemoryWin32HandleKHR)                \
+    MACRO(1, 1, FF_VK_EXT_EXTERNAL_WIN32_MEMORY, GetMemoryWin32HandlePropertiesKHR)
 
 /* Macro to turn a function name into a definition */
 #define PFN_DEF(req_inst, req_dev, ext_flag, name) \
-- 
2.52.0


>From 7dd3510432a34e8f30408fb2fc1032e69615fa08 Mon Sep 17 00:00:00 2001
From: flowreen <[email protected]>
Date: Wed, 5 Aug 2026 20:47:51 +0200
Subject: [PATCH 3/4] avutil/hwcontext_vulkan: add two plane D3D11 transfers

NV12 and P010 frames now transfer on the GPU as well. A two plane
texture cannot be imported whole and D3D11 copies cannot address its
planes, so each plane moves through a shared single plane texture,
written and read by a compute shader on the D3D11 side and imported
into Vulkan. Texture arrays and padded decoder textures work too,
which the whole texture import could not. The bridge owning the plane
textures and shaders lives in hwcontext_d3d11va, where other interop
paths can share it.
---
 libavutil/Makefile                     |   3 +-
 libavutil/hwcontext_d3d11va.c          | 278 ++++++++++++++++++++++
 libavutil/hwcontext_d3d11va_internal.h |  79 +++++++
 libavutil/hwcontext_vulkan.c           | 311 +++++++++++++++++--------
 4 files changed, 579 insertions(+), 92 deletions(-)
 create mode 100644 libavutil/hwcontext_d3d11va_internal.h

diff --git a/libavutil/Makefile b/libavutil/Makefile
index 9cb3108b38..5c8af36625 100644
--- a/libavutil/Makefile
+++ b/libavutil/Makefile
@@ -240,7 +240,8 @@ SKIPHEADERS-$(CONFIG_ZLIB)             += zlib_utils.h
 SKIPHEADERS-$(HAVE_CUDA_H)             += hwcontext_cuda.h
 SKIPHEADERS-$(CONFIG_CUDA)             += hwcontext_cuda_internal.h     \
                                           cuda_check.h
-SKIPHEADERS-$(CONFIG_D3D11VA)          += hwcontext_d3d11va.h
+SKIPHEADERS-$(CONFIG_D3D11VA)          += hwcontext_d3d11va.h           \
+                                          hwcontext_d3d11va_internal.h
 SKIPHEADERS-$(CONFIG_D3D12VA)          += hwcontext_d3d12va.h
 SKIPHEADERS-$(CONFIG_DXVA2)            += hwcontext_dxva2.h
 SKIPHEADERS-$(CONFIG_AMF)              += hwcontext_amf.h               \
diff --git a/libavutil/hwcontext_d3d11va.c b/libavutil/hwcontext_d3d11va.c
index 4efa302792..023a8d7fe5 100644
--- a/libavutil/hwcontext_d3d11va.c
+++ b/libavutil/hwcontext_d3d11va.c
@@ -24,6 +24,7 @@
 
 #include <initguid.h>
 #include <d3d11.h>
+#include <d3dcompiler.h>
 #include <dxgi1_2.h>
 
 #if HAVE_DXGIDEBUG_H
@@ -34,6 +35,7 @@
 #include "common.h"
 #include "hwcontext.h"
 #include "hwcontext_d3d11va.h"
+#include "hwcontext_d3d11va_internal.h"
 #include "hwcontext_internal.h"
 #if CONFIG_CUDA
 #include "cuda_check.h"
@@ -519,6 +521,282 @@ map_failed:
     return AVERROR_UNKNOWN;
 }
 
+#if CONFIG_VULKAN || CONFIG_CUDA
+
+/* The plane bridge shared by the interop transfer paths, see
+ * hwcontext_d3d11va_internal.h for what it is and why. */
+
+static const char bridge_shader_r[] =
+    "Texture2D<float>   s : register(t0);\n"
+    "RWTexture2D<float> d : register(u0);\n"
+    "[numthreads(8, 8, 1)]\n"
+    "void main(uint3 t : SV_DispatchThreadID) { d[t.xy] = s[t.xy]; }\n";
+
+static const char bridge_shader_rg[] =
+    "Texture2D<float2>   s : register(t0);\n"
+    "RWTexture2D<float2> d : register(u0);\n"
+    "[numthreads(8, 8, 1)]\n"
+    "void main(uint3 t : SV_DispatchThreadID) { d[t.xy] = s[t.xy]; }\n";
+
+int ff_d3d11va_bridge_formats(enum AVPixelFormat sw, DXGI_FORMAT *tex,
+                              DXGI_FORMAT plane[2])
+{
+    switch (sw) {
+    case AV_PIX_FMT_NV12:
+        *tex = DXGI_FORMAT_NV12;
+        plane[0] = DXGI_FORMAT_R8_UNORM;
+        plane[1] = DXGI_FORMAT_R8G8_UNORM;
+        return 0;
+    case AV_PIX_FMT_P010:
+    case AV_PIX_FMT_P012:
+    case AV_PIX_FMT_P016:
+        *tex = sw == AV_PIX_FMT_P010 ? DXGI_FORMAT_P010 : DXGI_FORMAT_P016;
+        plane[0] = DXGI_FORMAT_R16_UNORM;
+        plane[1] = DXGI_FORMAT_R16G16_UNORM;
+        return 0;
+    default:
+        return AVERROR(ENOSYS);
+    }
+}
+
+void ff_d3d11va_bridge_free(FFD3D11PlaneBridge **bridge)
+{
+    FFD3D11PlaneBridge *b = *bridge;
+
+    if (!b)
+        return;
+    for (int i = 0; i < 2; i++) {
+        if (b->plane_uav[i])
+            ID3D11UnorderedAccessView_Release(b->plane_uav[i]);
+        if (b->plane_srv[i])
+            ID3D11ShaderResourceView_Release(b->plane_srv[i]);
+        if (b->planes[i])
+            ID3D11Texture2D_Release(b->planes[i]);
+        if (b->staging_uav[i])
+            ID3D11UnorderedAccessView_Release(b->staging_uav[i]);
+        if (b->staging_srv[i])
+            ID3D11ShaderResourceView_Release(b->staging_srv[i]);
+        if (b->cs[i])
+            ID3D11ComputeShader_Release(b->cs[i]);
+    }
+    if (b->staging)
+        ID3D11Texture2D_Release(b->staging);
+    if (b->compiler)
+        dlclose(b->compiler);
+    av_freep(bridge);
+}
+
+int ff_d3d11va_bridge_create(FFD3D11PlaneBridge **bridge, ID3D11Device *dev,
+                             int width, int height,
+                             enum AVPixelFormat sw_format, void *log_ctx)
+{
+    const char *cs_src[2] = { bridge_shader_r, bridge_shader_rg };
+    const AVPixFmtDescriptor *pixdesc = av_pix_fmt_desc_get(sw_format);
+    D3D11_TEXTURE2D_DESC desc;
+    DXGI_FORMAT fmt, pfmts[2];
+    FFD3D11PlaneBridge *b;
+    pD3DCompile compile;
+    HRESULT hr;
+    int err;
+
+    err = ff_d3d11va_bridge_formats(sw_format, &fmt, pfmts);
+    if (err < 0)
+        return err;
+
+    b = *bridge = av_mallocz(sizeof(*b));
+    if (!b)
+        return AVERROR(ENOMEM);
+    b->width      = width;
+    b->height     = height;
+    b->plane_w[0] = width;
+    b->plane_h[0] = height;
+    b->plane_w[1] = AV_CEIL_RSHIFT(width,  pixdesc->log2_chroma_w);
+    b->plane_h[1] = AV_CEIL_RSHIFT(height, pixdesc->log2_chroma_h);
+
+    /* The shaders are compiled at run time: libavutil cannot compile HLSL at
+     * build time, and d3dcompiler_47 ships with the OS. */
+    b->compiler = dlopen("d3dcompiler_47.dll", 0);
+    compile = b->compiler ? (pD3DCompile)dlsym(b->compiler, "D3DCompile")
+                          : NULL;
+    if (!compile) {
+        av_log(log_ctx, AV_LOG_DEBUG, "d3dcompiler_47 is not available\n");
+        err = AVERROR(ENOSYS);
+        goto fail;
+    }
+
+    for (int i = 0; i < 2; i++) {
+        ID3DBlob *code = NULL, *errors = NULL;
+        hr = compile(cs_src[i], strlen(cs_src[i]), NULL, NULL, NULL, "main",
+                     "cs_5_0", 0, 0, &code, &errors);
+        if (SUCCEEDED(hr))
+            hr = ID3D11Device_CreateComputeShader(dev,
+                                                  ID3D10Blob_GetBufferPointer(code),
+                                                  ID3D10Blob_GetBufferSize(code),
+                                                  NULL, &b->cs[i]);
+        if (code)
+            ID3D10Blob_Release(code);
+        if (errors)
+            ID3D10Blob_Release(errors);
+        if (FAILED(hr)) {
+            av_log(log_ctx, AV_LOG_DEBUG, "Cannot build the copy shader (%lx)\n",
+                   (long)hr);
+            err = AVERROR(ENOSYS);
+            goto fail;
+        }
+    }
+
+    desc = (D3D11_TEXTURE2D_DESC) {
+        .Width      = width,
+        .Height     = height,
+        .MipLevels  = 1,
+        .ArraySize  = 1,
+        .Format     = fmt,
+        .SampleDesc = { .Count = 1 },
+        .Usage      = D3D11_USAGE_DEFAULT,
+        .BindFlags  = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS,
+    };
+    hr = ID3D11Device_CreateTexture2D(dev, &desc, NULL, &b->staging);
+    if (FAILED(hr)) {
+        /* Without UAV support the plane textures cannot be assembled into the
+         * staging texture, but moves toward them only ever read it, so keep
+         * those working. */
+        desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
+        hr = ID3D11Device_CreateTexture2D(dev, &desc, NULL, &b->staging);
+    }
+    if (FAILED(hr)) {
+        av_log(log_ctx, AV_LOG_DEBUG, "Cannot create the staging texture (%lx)\n",
+               (long)hr);
+        err = AVERROR(ENOSYS);
+        goto fail;
+    }
+
+    for (int i = 0; i < 2; i++) {
+        D3D11_SHADER_RESOURCE_VIEW_DESC sd = {
+            .ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D,
+            .Texture2D     = { .MipLevels = 1 },
+        };
+        D3D11_UNORDERED_ACCESS_VIEW_DESC ud = {
+            .ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D,
+        };
+        D3D11_TEXTURE2D_DESC pd;
+
+        /* A view with a single-plane format selects that plane of the planar
+         * staging texture. */
+        sd.Format = pfmts[i];
+        ud.Format = pfmts[i];
+        hr = ID3D11Device_CreateShaderResourceView(dev,
+                                                   (ID3D11Resource *)b->staging,
+                                                   &sd, &b->staging_srv[i]);
+        if (FAILED(hr)) {
+            av_log(log_ctx, AV_LOG_DEBUG, "Cannot create a plane view (%lx)\n",
+                   (long)hr);
+            err = AVERROR(ENOSYS);
+            goto fail;
+        }
+        if (desc.BindFlags & D3D11_BIND_UNORDERED_ACCESS)
+            ID3D11Device_CreateUnorderedAccessView(dev,
+                                                   (ID3D11Resource *)b->staging,
+                                                   &ud, &b->staging_uav[i]);
+
+        pd = (D3D11_TEXTURE2D_DESC) {
+            .Width      = b->plane_w[i],
+            .Height     = b->plane_h[i],
+            .MipLevels  = 1,
+            .ArraySize  = 1,
+            .Format     = pfmts[i],
+            .SampleDesc = { .Count = 1 },
+            .Usage      = D3D11_USAGE_DEFAULT,
+            .BindFlags  = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS,
+            .MiscFlags  = D3D11_RESOURCE_MISC_SHARED,
+        };
+        hr = ID3D11Device_CreateTexture2D(dev, &pd, NULL, &b->planes[i]);
+        if (SUCCEEDED(hr))
+            hr = ID3D11Device_CreateShaderResourceView(dev,
+                                                       (ID3D11Resource *)b->planes[i],
+                                                       NULL, &b->plane_srv[i]);
+        if (SUCCEEDED(hr))
+            hr = ID3D11Device_CreateUnorderedAccessView(dev,
+                                                        (ID3D11Resource *)b->planes[i],
+                                                        NULL, &b->plane_uav[i]);
+        if (FAILED(hr)) {
+            av_log(log_ctx, AV_LOG_DEBUG, "Cannot create a plane texture (%lx)\n",
+                   (long)hr);
+            err = AVERROR(ENOSYS);
+            goto fail;
+        }
+    }
+
+    /* Assembling writes every staging plane, so all or nothing. */
+    if (!b->staging_uav[0] || !b->staging_uav[1]) {
+        for (int i = 0; i < 2; i++) {
+            if (b->staging_uav[i])
+                ID3D11UnorderedAccessView_Release(b->staging_uav[i]);
+            b->staging_uav[i] = NULL;
+        }
+    }
+
+    return 0;
+
+fail:
+    ff_d3d11va_bridge_free(bridge);
+    return err;
+}
+
+void ff_d3d11va_bridge_run(FFD3D11PlaneBridge *b, ID3D11DeviceContext *ctx,
+                           ID3D11Resource *tex, unsigned index, int to_planes)
+{
+    ID3D11ShaderResourceView *null_srv = NULL;
+    ID3D11UnorderedAccessView *null_uav = NULL;
+    ID3D11ComputeShader *prev_cs = NULL;
+    ID3D11ClassInstance *prev_inst[D3D11_SHADER_MAX_INTERFACES];
+    UINT prev_inst_n = FF_ARRAY_ELEMS(prev_inst);
+    ID3D11ShaderResourceView *prev_srv = NULL;
+    ID3D11UnorderedAccessView *prev_uav = NULL;
+    D3D11_BOX box = { 0, 0, 0, b->width, b->height, 1 };
+
+    /* The context belongs to the caller, so everything the passes below bind
+     * is saved here and put back at the end. */
+    ID3D11DeviceContext_CSGetShader(ctx, &prev_cs, prev_inst, &prev_inst_n);
+    ID3D11DeviceContext_CSGetShaderResources(ctx, 0, 1, &prev_srv);
+    ID3D11DeviceContext_CSGetUnorderedAccessViews(ctx, 0, 1, &prev_uav);
+
+    if (to_planes)
+        ID3D11DeviceContext_CopySubresourceRegion(ctx,
+            (ID3D11Resource *)b->staging, 0, 0, 0, 0, tex, index, &box);
+
+    for (int i = 0; i < 2; i++) {
+        ID3D11DeviceContext_CSSetShader(ctx, b->cs[i], NULL, 0);
+        ID3D11DeviceContext_CSSetShaderResources(ctx, 0, 1,
+            to_planes ? &b->staging_srv[i] : &b->plane_srv[i]);
+        ID3D11DeviceContext_CSSetUnorderedAccessViews(ctx, 0, 1,
+            to_planes ? &b->plane_uav[i] : &b->staging_uav[i], NULL);
+        ID3D11DeviceContext_Dispatch(ctx, (b->plane_w[i] + 7) / 8,
+                                     (b->plane_h[i] + 7) / 8, 1);
+        /* Unbind before the next pass: two views of one resource cannot be
+         * bound as input and output at the same time. */
+        ID3D11DeviceContext_CSSetShaderResources(ctx, 0, 1, &null_srv);
+        ID3D11DeviceContext_CSSetUnorderedAccessViews(ctx, 0, 1, &null_uav, NULL);
+    }
+
+    ID3D11DeviceContext_CSSetShader(ctx, prev_cs, prev_inst, prev_inst_n);
+    ID3D11DeviceContext_CSSetShaderResources(ctx, 0, 1, &prev_srv);
+    ID3D11DeviceContext_CSSetUnorderedAccessViews(ctx, 0, 1, &prev_uav, NULL);
+    if (prev_cs)
+        ID3D11ComputeShader_Release(prev_cs);
+    for (UINT i = 0; i < prev_inst_n; i++)
+        ID3D11ClassInstance_Release(prev_inst[i]);
+    if (prev_srv)
+        ID3D11ShaderResourceView_Release(prev_srv);
+    if (prev_uav)
+        ID3D11UnorderedAccessView_Release(prev_uav);
+
+    if (!to_planes)
+        ID3D11DeviceContext_CopySubresourceRegion(ctx, tex, index, 0, 0, 0,
+            (ID3D11Resource *)b->staging, 0, NULL);
+}
+
+#endif /* CONFIG_VULKAN || CONFIG_CUDA */
+
 static int d3d11va_device_init(AVHWDeviceContext *hwdev)
 {
     AVD3D11VADeviceContext *device_hwctx = hwdev->hwctx;
diff --git a/libavutil/hwcontext_d3d11va_internal.h b/libavutil/hwcontext_d3d11va_internal.h
new file mode 100644
index 0000000000..0b78aeac5a
--- /dev/null
+++ b/libavutil/hwcontext_d3d11va_internal.h
@@ -0,0 +1,79 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_HWCONTEXT_D3D11VA_INTERNAL_H
+#define AVUTIL_HWCONTEXT_D3D11VA_INTERNAL_H
+
+#include <d3d11.h>
+
+#include "pixfmt.h"
+
+/**
+ * A plane bridge for sharing two-plane textures with another API. Such a
+ * texture cannot be shared whole: its import is reported as dedicated-only,
+ * a dedicated import always maps the start of the resource, and no API
+ * reports where the second plane begins. What can be shared is one texture
+ * per plane, so the bridge keeps a shared single-plane texture for each
+ * plane and moves the data between them and a staging texture in the
+ * two-plane format. D3D11's copy functions cannot address the planes of a
+ * planar texture either, so that move is a compute shader reading and
+ * writing through views that each select one plane.
+ */
+typedef struct FFD3D11PlaneBridge {
+    void                      *compiler;   /* d3dcompiler_47.dll */
+    ID3D11ComputeShader       *cs[2];      /* one and two channel copies */
+    ID3D11Texture2D           *staging;    /* in the two-plane format */
+    ID3D11ShaderResourceView  *staging_srv[2];
+    ID3D11UnorderedAccessView *staging_uav[2]; /* missing without UAV support */
+    ID3D11Texture2D           *planes[2];  /* created with MISC_SHARED */
+    ID3D11ShaderResourceView  *plane_srv[2];
+    ID3D11UnorderedAccessView *plane_uav[2];
+    int                        width;
+    int                        height;
+    int                        plane_w[2]; /* in texels of the plane format */
+    int                        plane_h[2];
+} FFD3D11PlaneBridge;
+
+/**
+ * The DXGI format a two-plane sw format is represented by, and the single
+ * plane formats of its planes. Returns ENOSYS for everything else.
+ */
+int ff_d3d11va_bridge_formats(enum AVPixelFormat sw, DXGI_FORMAT *tex,
+                              DXGI_FORMAT plane[2]);
+
+/**
+ * Create a bridge for frames of the given size and sw format. On failure
+ * (including ENOSYS for an unhandled format) *bridge is left NULL.
+ */
+int ff_d3d11va_bridge_create(FFD3D11PlaneBridge **bridge, ID3D11Device *dev,
+                             int width, int height,
+                             enum AVPixelFormat sw_format, void *log_ctx);
+
+void ff_d3d11va_bridge_free(FFD3D11PlaneBridge **bridge);
+
+/**
+ * Move the frame-sized region of subresource index of tex into the plane
+ * textures (to_planes) or assemble the plane textures into it. Downloads
+ * from the plane textures require staging_uav, which the caller has to
+ * check. The device lock must be held; the context's compute state is
+ * saved and restored.
+ */
+void ff_d3d11va_bridge_run(FFD3D11PlaneBridge *b, ID3D11DeviceContext *ctx,
+                           ID3D11Resource *tex, unsigned index, int to_planes);
+
+#endif /* AVUTIL_HWCONTEXT_D3D11VA_INTERNAL_H */
diff --git a/libavutil/hwcontext_vulkan.c b/libavutil/hwcontext_vulkan.c
index 78b87570d3..caff8ee02a 100644
--- a/libavutil/hwcontext_vulkan.c
+++ b/libavutil/hwcontext_vulkan.c
@@ -32,6 +32,7 @@
 #include <d3d11_4.h>
 #include <dxgi1_2.h>
 #include "hwcontext_d3d11va.h"
+#include "hwcontext_d3d11va_internal.h"
 #endif
 #else
 #include <dlfcn.h>
@@ -3024,6 +3025,16 @@ typedef struct D3D11SyncState {
     ID3D11Fence           *fence;
     VkSemaphore            sem;   /* the fence imported into Vulkan */
     uint64_t               value; /* last signaled point, device lock held */
+
+    /* Plane bridge for two-plane formats, implemented by hwcontext_d3d11va
+     * (see hwcontext_d3d11va_internal.h): the bridge's shared plane textures
+     * are what Vulkan imports. All of it is created once and kept,
+     * bridge_lock serializes the transfers that use it. */
+    pthread_mutex_t     bridge_lock;
+    int                 bridge_status; /* 0 untried, 1 ready, else error */
+    FFD3D11PlaneBridge *bridge;
+    VkImage             plane_img[2];
+    VkDeviceMemory      plane_mem[2];
 } D3D11SyncState;
 
 static void d3d11_sync_states_free(AVHWFramesContext *hwfc)
@@ -3042,6 +3053,14 @@ static void d3d11_sync_states_free(AVHWFramesContext *hwfc)
             ID3D11Fence_Release(sync->fence);
         if (sync->ctx4)
             ID3D11DeviceContext4_Release(sync->ctx4);
+        for (int i = 0; i < 2; i++) {
+            if (sync->plane_img[i])
+                vk->DestroyImage(hwctx->act_dev, sync->plane_img[i], hwctx->alloc);
+            if (sync->plane_mem[i])
+                vk->FreeMemory(hwctx->act_dev, sync->plane_mem[i], hwctx->alloc);
+        }
+        ff_d3d11va_bridge_free(&sync->bridge);
+        pthread_mutex_destroy(&sync->bridge_lock);
         if (sync->dev)
             ID3D11Device_Release(sync->dev);
         av_free(sync);
@@ -5143,6 +5162,10 @@ static VkFormat d3d11_to_vulkan_fmt(DXGI_FORMAT f)
     case DXGI_FORMAT_R8G8B8A8_UNORM:     return VK_FORMAT_R8G8B8A8_UNORM;
     case DXGI_FORMAT_R10G10B10A2_UNORM:  return VK_FORMAT_A2B10G10R10_UNORM_PACK32;
     case DXGI_FORMAT_R16G16B16A16_FLOAT: return VK_FORMAT_R16G16B16A16_SFLOAT;
+    case DXGI_FORMAT_R8_UNORM:           return VK_FORMAT_R8_UNORM;
+    case DXGI_FORMAT_R8G8_UNORM:         return VK_FORMAT_R8G8_UNORM;
+    case DXGI_FORMAT_R16_UNORM:          return VK_FORMAT_R16_UNORM;
+    case DXGI_FORMAT_R16G16_UNORM:       return VK_FORMAT_R16G16_UNORM;
     default:                             return VK_FORMAT_UNDEFINED;
     }
 }
@@ -5215,6 +5238,11 @@ static int d3d11_sync_state_get(AVHWFramesContext *hwfc,
         err = AVERROR(ENOMEM);
         goto fail;
     }
+    if (pthread_mutex_init(&sync->bridge_lock, NULL)) {
+        av_freep(&sync);
+        err = AVERROR(ENOMEM);
+        goto fail;
+    }
 
     /* Fences arrived in D3D11.4, so a device that predates them has nothing
      * the copy could synchronize against. */
@@ -5279,6 +5307,7 @@ fail:
             ID3D11Fence_Release(sync->fence);
         if (sync->ctx4)
             ID3D11DeviceContext4_Release(sync->ctx4);
+        pthread_mutex_destroy(&sync->bridge_lock);
         av_free(sync);
     }
     pthread_mutex_unlock(&fp->d3d11_sync_lock);
@@ -5288,7 +5317,7 @@ fail:
 /* Import a shared D3D11 texture as a VkImage aliasing the same memory. The
  * texture must be single-subresource and created with D3D11_RESOURCE_MISC_SHARED. */
 static int d3d11_import(AVHWFramesContext *hwfc, ID3D11Texture2D *tex,
-                        D3D11Import *im)
+                        uint32_t width, uint32_t height, D3D11Import *im)
 {
     VulkanDevicePriv *p = hwfc->device_ctx->hwctx;
     AVVulkanDeviceContext *hwctx = &p->p;
@@ -5321,12 +5350,12 @@ static int d3d11_import(AVHWFramesContext *hwfc, ID3D11Texture2D *tex,
         av_log(hwfc, AV_LOG_DEBUG, "Cannot import a D3D11 texture array\n");
         return AVERROR(ENOSYS);
     }
-    /* The copy below covers the whole frame, so anything but an exact match
-     * would read or write outside one of the two images. */
-    if (desc.Width != hwfc->width || desc.Height != hwfc->height) {
-        av_log(hwfc, AV_LOG_DEBUG, "D3D11 texture is %ux%u, expected %dx%d\n",
+    /* The copy below covers the whole image, so anything but an exact match
+     * would read or write outside one of the two. */
+    if (desc.Width != width || desc.Height != height) {
+        av_log(hwfc, AV_LOG_DEBUG, "D3D11 texture is %ux%u, expected %ux%u\n",
                (unsigned)desc.Width, (unsigned)desc.Height,
-               hwfc->width, hwfc->height);
+               (unsigned)width, (unsigned)height);
         return AVERROR(ENOSYS);
     }
     /* Keyed mutex sharing only guarantees coherency to a user that acquires the
@@ -5513,6 +5542,37 @@ fail:
     return err;
 }
 
+/* The bridge outlives the transfer, so a failed creation attempt is not
+ * retried every frame, and whatever a failed attempt did create is released
+ * with the sync state. */
+static int d3d11_bridge_get(AVHWFramesContext *hwfc, D3D11SyncState *sync,
+                            AVD3D11VADeviceContext *d3d_hw)
+{
+    VulkanFramesPriv *fp = hwfc->hwctx;
+    int err;
+
+    pthread_mutex_lock(&fp->d3d11_sync_lock);
+    if (!sync->bridge_status) {
+        err = ff_d3d11va_bridge_create(&sync->bridge, d3d_hw->device,
+                                       hwfc->width, hwfc->height,
+                                       hwfc->sw_format, hwfc);
+        for (int i = 0; !err && i < 2; i++) {
+            D3D11Import im;
+            err = d3d11_import(hwfc, sync->bridge->planes[i],
+                               sync->bridge->plane_w[i],
+                               sync->bridge->plane_h[i], &im);
+            if (!err) {
+                sync->plane_img[i] = im.img;
+                sync->plane_mem[i] = im.mem;
+            }
+        }
+        sync->bridge_status = err < 0 ? err : 1;
+    }
+    err = sync->bridge_status > 0 ? 0 : sync->bridge_status;
+    pthread_mutex_unlock(&fp->d3d11_sync_lock);
+    return err;
+}
+
 /* Copy between a Vulkan frame and a D3D11 texture. D3D11 cannot import Vulkan
  * memory, so the texture is always the side that gets imported, and the copy
  * itself runs on the GPU. */
@@ -5525,28 +5585,21 @@ static int vulkan_transfer_d3d11(AVHWFramesContext *hwfc, AVFrame *dst,
     AVFrame *hwf = (AVFrame *)(upload ? dst : src);
     const AVFrame *d3df = upload ? src : dst;
     AVVkFrame *hwf_vk = (AVVkFrame *)hwf->data[0];
+    const int planes = av_pix_fmt_count_planes(hwfc->sw_format);
+    const int nb_images = ff_vk_count_images(hwf_vk);
     AVHWFramesContext *d3d_fc;
     AVD3D11VADeviceContext *d3d_hw;
-    VkImageMemoryBarrier2 img_bar[AV_NUM_DATA_POINTERS + 1];
+    VkImageMemoryBarrier2 img_bar[AV_NUM_DATA_POINTERS + 2];
     int nb_img_bar = 0;
-    VkImageCopy region;
     D3D11SyncState *sync;
-    D3D11Import im;
+    D3D11Import im = { 0 };
+    VkImage imp_img[2];
+    int bridged = 0;
     uint64_t sync_point;
     FFVkExecContext *exec;
     VkCommandBuffer cmd_buf;
     int err;
 
-    if ((intptr_t)d3df->data[1] != 0)
-        return AVERROR(ENOSYS); /* an index into a texture array */
-
-    /* Multi-planar textures cannot be imported: their imports are reported as
-     * dedicated-only, a dedicated import always maps the start of the
-     * resource, and no API reports where the other planes begin. Refuse them
-     * and the caller falls back to system memory. */
-    if (av_pix_fmt_count_planes(hwfc->sw_format) != 1)
-        return AVERROR(ENOSYS);
-
     /* The copy moves bits, so the two sides have to agree on what the bits
      * mean. Differing formats of the same size would silently reinterpret the
      * pixels, and differing sizes are not a legal copy at all. */
@@ -5561,9 +5614,49 @@ static int vulkan_transfer_d3d11(AVHWFramesContext *hwfc, AVFrame *dst,
     if (err < 0)
         return err;
 
-    err = d3d11_import(hwfc, (ID3D11Texture2D *)d3df->data[0], &im);
-    if (err < 0)
-        return err;
+    if (planes == 1) {
+        if ((intptr_t)d3df->data[1] != 0)
+            return AVERROR(ENOSYS); /* an index into a texture array */
+        err = d3d11_import(hwfc, (ID3D11Texture2D *)d3df->data[0],
+                           hwfc->width, hwfc->height, &im);
+        if (err < 0)
+            return err;
+        imp_img[0] = im.img;
+    } else if (planes == 2) {
+        D3D11_TEXTURE2D_DESC desc;
+        DXGI_FORMAT tex_fmt, plane_fmts[2];
+
+        ID3D11Texture2D_GetDesc((ID3D11Texture2D *)d3df->data[0], &desc);
+
+        /* The staging copies address the frame-sized region of the texture,
+         * which decoders often pad, and copies of video formats only accept
+         * aligned regions. The texture must really be in the format the sw
+         * format implies, or the copies would silently move nothing. A
+         * keyed-mutex texture is out for the same reason as above: nothing
+         * here acquires the mutex. */
+        if (((hwfc->width | hwfc->height) & 1) ||
+            desc.Width < hwfc->width || desc.Height < hwfc->height ||
+            desc.SampleDesc.Count != 1 ||
+            (desc.MiscFlags & D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX) ||
+            ff_d3d11va_bridge_formats(hwfc->sw_format, &tex_fmt, plane_fmts) < 0 ||
+            desc.Format != tex_fmt)
+            return AVERROR(ENOSYS);
+
+        err = d3d11_bridge_get(hwfc, sync, d3d_hw);
+        if (err < 0)
+            return err;
+        /* Downloads write the staging texture through views its format does
+         * not support everywhere. */
+        if (!upload && !sync->bridge->staging_uav[0])
+            return AVERROR(ENOSYS);
+
+        pthread_mutex_lock(&sync->bridge_lock);
+        bridged = 1;
+        imp_img[0] = sync->plane_img[0];
+        imp_img[1] = sync->plane_img[1];
+    } else {
+        return AVERROR(ENOSYS);
+    }
 
     /* Signal the fence after the D3D11 commands that produced the texture, or
      * that may still be reading the one about to be overwritten. The copy
@@ -5572,6 +5665,10 @@ static int vulkan_transfer_d3d11(AVHWFramesContext *hwfc, AVFrame *dst,
      * a D3D11 wait for a fence value signaled from a Vulkan queue takes a
      * scheduler timeout of more than a second to wake on current drivers. */
     d3d_hw->lock(d3d_hw->lock_ctx);
+    if (bridged && upload)
+        ff_d3d11va_bridge_run(sync->bridge, d3d_hw->device_context,
+                              (ID3D11Resource *)d3df->data[0],
+                              (UINT)(intptr_t)d3df->data[1], 1);
     sync_point = ++sync->value;
     ID3D11DeviceContext4_Signal(sync->ctx4, sync->fence, sync_point);
     ID3D11DeviceContext_Flush(d3d_hw->device_context);
@@ -5581,10 +5678,8 @@ static int vulkan_transfer_d3d11(AVHWFramesContext *hwfc, AVFrame *dst,
                                               &fp->download_exec);
     cmd_buf = exec->buf;
     err = ff_vk_exec_start(&p->vkctx, exec);
-    if (err < 0) {
-        d3d11_import_free(hwfc, &im);
-        return err;
-    }
+    if (err < 0)
+        goto end;
 
     err = ff_vk_exec_add_dep_frame(&p->vkctx, exec, hwf,
                                    VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
@@ -5607,31 +5702,33 @@ static int vulkan_transfer_d3d11(AVHWFramesContext *hwfc, AVFrame *dst,
                                  VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
                         p->nb_img_qfs > 1 ? VK_QUEUE_FAMILY_IGNORED : p->img_qfs[0]);
 
-    /* Acquire the imported image from the external owner. When we are reading
-     * it, the contents D3D11 left behind have to be preserved, so it cannot
-     * be acquired from VK_IMAGE_LAYOUT_UNDEFINED. */
-    img_bar[nb_img_bar++] = (VkImageMemoryBarrier2) {
-        .sType         = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
-        .srcStageMask  = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
-        .dstStageMask  = VK_PIPELINE_STAGE_2_TRANSFER_BIT,
-        .srcAccessMask = 0,
-        .dstAccessMask = upload ? VK_ACCESS_2_TRANSFER_READ_BIT :
-                                  VK_ACCESS_2_TRANSFER_WRITE_BIT,
-        .oldLayout     = upload ? VK_IMAGE_LAYOUT_GENERAL :
-                                  VK_IMAGE_LAYOUT_UNDEFINED,
-        .newLayout     = upload ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL :
-                                  VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
-        /* This is an ownership acquire, so the destination has to be the
-         * queue family the command buffer itself was allocated from. */
-        .srcQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL,
-        .dstQueueFamilyIndex = exec->qf,
-        .image               = im.img,
-        .subresourceRange    = {
-            .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
-            .levelCount = 1,
-            .layerCount = 1,
-        },
-    };
+    /* Acquire the imported images from the external owner. When we are
+     * reading them, the contents D3D11 left behind have to be preserved, so
+     * they cannot be acquired from VK_IMAGE_LAYOUT_UNDEFINED. */
+    for (int i = 0; i < planes; i++) {
+        img_bar[nb_img_bar++] = (VkImageMemoryBarrier2) {
+            .sType         = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
+            .srcStageMask  = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
+            .dstStageMask  = VK_PIPELINE_STAGE_2_TRANSFER_BIT,
+            .srcAccessMask = 0,
+            .dstAccessMask = upload ? VK_ACCESS_2_TRANSFER_READ_BIT :
+                                      VK_ACCESS_2_TRANSFER_WRITE_BIT,
+            .oldLayout     = upload ? VK_IMAGE_LAYOUT_GENERAL :
+                                      VK_IMAGE_LAYOUT_UNDEFINED,
+            .newLayout     = upload ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL :
+                                      VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+            /* This is an ownership acquire, so the destination has to be the
+             * queue family the command buffer itself was allocated from. */
+            .srcQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL,
+            .dstQueueFamilyIndex = exec->qf,
+            .image               = imp_img[i],
+            .subresourceRange    = {
+                .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+                .levelCount = 1,
+                .layerCount = 1,
+            },
+        };
+    }
 
     vk->CmdPipelineBarrier2(cmd_buf, &(VkDependencyInfo) {
             .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
@@ -5639,46 +5736,66 @@ static int vulkan_transfer_d3d11(AVHWFramesContext *hwfc, AVFrame *dst,
             .imageMemoryBarrierCount = nb_img_bar,
         });
 
-    region = (VkImageCopy) {
-        .srcSubresource = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .layerCount = 1 },
-        .dstSubresource = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .layerCount = 1 },
-        .extent         = { hwfc->width, hwfc->height, 1 },
-    };
-    if (upload)
-        vk->CmdCopyImage(cmd_buf, im.img, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
-                         hwf_vk->img[0], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
-                         1, &region);
-    else
-        vk->CmdCopyImage(cmd_buf, hwf_vk->img[0], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
-                         im.img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
-                         1, &region);
+    /* One copy per plane. The frame may hold one image per plane or a single
+     * multi-planar image addressed through plane aspects, so the aspect comes
+     * from the frame and the image index is clamped to what it has. */
+    for (int i = 0; i < planes; i++) {
+        const int img_idx = FFMIN(i, nb_images - 1);
+        VkImageAspectFlags aspect = ff_vk_aspect_flag(hwf, i);
+        uint32_t p_w, p_h;
+        VkImageCopy region;
 
-    /* Release the imported image back to the external owner, which makes the
-     * write available to D3D11 when this was a download. */
-    img_bar[0] = (VkImageMemoryBarrier2) {
-        .sType         = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
-        .srcStageMask  = VK_PIPELINE_STAGE_2_TRANSFER_BIT,
-        .dstStageMask  = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
-        .srcAccessMask = upload ? VK_ACCESS_2_TRANSFER_READ_BIT :
-                                  VK_ACCESS_2_TRANSFER_WRITE_BIT,
-        .dstAccessMask = 0,
-        .oldLayout     = upload ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL :
-                                  VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
-        .newLayout     = VK_IMAGE_LAYOUT_GENERAL,
-        .srcQueueFamilyIndex = exec->qf,
-        .dstQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL,
-        .image               = im.img,
-        .subresourceRange    = {
-            .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
-            .levelCount = 1,
-            .layerCount = 1,
-        },
-    };
+        get_plane_wh(&p_w, &p_h, hwfc->sw_format, hwfc->width, hwfc->height, i);
+
+        region = (VkImageCopy) {
+            .srcSubresource = { .layerCount = 1 },
+            .dstSubresource = { .layerCount = 1 },
+            .extent         = { p_w, p_h, 1 },
+        };
+        if (upload) {
+            region.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
+            region.dstSubresource.aspectMask = aspect;
+            vk->CmdCopyImage(cmd_buf, imp_img[i], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+                             hwf_vk->img[img_idx], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+                             1, &region);
+        } else {
+            region.srcSubresource.aspectMask = aspect;
+            region.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
+            vk->CmdCopyImage(cmd_buf, hwf_vk->img[img_idx], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+                             imp_img[i], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+                             1, &region);
+        }
+    }
+
+    /* Release the imported images back to the external owner, which makes the
+     * writes available to D3D11 when this was a download. */
+    nb_img_bar = 0;
+    for (int i = 0; i < planes; i++) {
+        img_bar[nb_img_bar++] = (VkImageMemoryBarrier2) {
+            .sType         = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
+            .srcStageMask  = VK_PIPELINE_STAGE_2_TRANSFER_BIT,
+            .dstStageMask  = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
+            .srcAccessMask = upload ? VK_ACCESS_2_TRANSFER_READ_BIT :
+                                      VK_ACCESS_2_TRANSFER_WRITE_BIT,
+            .dstAccessMask = 0,
+            .oldLayout     = upload ? VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL :
+                                      VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+            .newLayout     = VK_IMAGE_LAYOUT_GENERAL,
+            .srcQueueFamilyIndex = exec->qf,
+            .dstQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL,
+            .image               = imp_img[i],
+            .subresourceRange    = {
+                .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+                .levelCount = 1,
+                .layerCount = 1,
+            },
+        };
+    }
 
     vk->CmdPipelineBarrier2(cmd_buf, &(VkDependencyInfo) {
             .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
             .pImageMemoryBarriers    = img_bar,
-            .imageMemoryBarrierCount = 1,
+            .imageMemoryBarrierCount = nb_img_bar,
         });
 
     err = ff_vk_exec_submit(&p->vkctx, exec);
@@ -5687,15 +5804,27 @@ static int vulkan_transfer_d3d11(AVHWFramesContext *hwfc, AVFrame *dst,
          * the transfer completes before it returns, like the system memory
          * paths do. */
         ff_vk_exec_wait(&p->vkctx, exec);
+        /* Only now do the plane textures hold the frame, so a download
+         * finishes by assembling them into the destination texture. */
+        if (bridged && !upload) {
+            d3d_hw->lock(d3d_hw->lock_ctx);
+            ff_d3d11va_bridge_run(sync->bridge, d3d_hw->device_context,
+                                  (ID3D11Resource *)d3df->data[0],
+                                  (UINT)(intptr_t)d3df->data[1], 0);
+            d3d_hw->unlock(d3d_hw->lock_ctx);
+        }
     }
-
-    d3d11_import_free(hwfc, &im);
-
-    return err;
+    goto end;
 
 fail:
     ff_vk_exec_discard(&p->vkctx, exec);
-    d3d11_import_free(hwfc, &im);
+
+end:
+    if (bridged)
+        pthread_mutex_unlock(&sync->bridge_lock);
+    else
+        d3d11_import_free(hwfc, &im);
+
     return err;
 }
 
-- 
2.52.0


>From 9f92c81a93ad1fd12339816c649ef164d6a44898 Mon Sep 17 00:00:00 2001
From: flowreen <[email protected]>
Date: Fri, 7 Aug 2026 22:41:22 +0200
Subject: [PATCH 4/4] avutil/hwcontext_d3d11va: add CUDA frame transfers

Frames now cross between CUDA and D3D11 on the GPU instead of through
system memory. Each texture CUDA touches is imported once through the
external memory API, and a shared D3D11 fence, imported as a CUDA
external semaphore, chains the two queues on the GPU in both
directions, so no transfer waits on the CPU. CUDA cannot import the
KMT handles the bridge planes used to share, so they prefer NT handle
sharing now, with KMT kept for runtimes without it; Vulkan takes
either kind. No CUDA sharing API can address the planes of a two
plane texture, so those frames move through the plane bridge, and
single plane frames through an intermediate texture in their own
format, so the frame textures themselves, which may be recycled
decoder arrays, never have to be imported.
---
 libavutil/hwcontext_d3d11va.c          | 580 ++++++++++++++++++++++++-
 libavutil/hwcontext_d3d11va_internal.h |   2 +-
 2 files changed, 580 insertions(+), 2 deletions(-)

diff --git a/libavutil/hwcontext_d3d11va.c b/libavutil/hwcontext_d3d11va.c
index 023a8d7fe5..7b1bf6b130 100644
--- a/libavutil/hwcontext_d3d11va.c
+++ b/libavutil/hwcontext_d3d11va.c
@@ -38,6 +38,7 @@
 #include "hwcontext_d3d11va_internal.h"
 #include "hwcontext_internal.h"
 #if CONFIG_CUDA
+#include <d3d11_4.h>
 #include "cuda_check.h"
 #include "hwcontext_cuda_internal.h"
 #define CHECK_CU(x) FF_CUDA_CHECK_DL(cuda_cu, cu, x)
@@ -98,8 +99,20 @@ typedef struct D3D11VAFramesContext {
     DXGI_FORMAT format;
 
     ID3D11Texture2D *staging_texture;
+
+#if CONFIG_CUDA
+    pthread_mutex_t          cuda_lock;
+    int                      cuda_lock_init;
+    struct D3D11CudaInterop *cuda_interop;
+#endif
 } D3D11VAFramesContext;
 
+#if CONFIG_CUDA
+static void d3d11va_cuda_interops_free(AVHWFramesContext *ctx);
+static int d3d11va_cuda_transfer_data(AVHWFramesContext *ctx, AVFrame *dst,
+                                      const AVFrame *src);
+#endif
+
 static const struct {
     DXGI_FORMAT d3d_format;
     enum AVPixelFormat pix_fmt;
@@ -141,6 +154,14 @@ static void d3d11va_frames_uninit(AVHWFramesContext *ctx)
     D3D11VAFramesContext *s = ctx->hwctx;
     AVD3D11VAFramesContext *frames_hwctx = &s->p;
 
+#if CONFIG_CUDA
+    d3d11va_cuda_interops_free(ctx);
+    if (s->cuda_lock_init) {
+        pthread_mutex_destroy(&s->cuda_lock);
+        s->cuda_lock_init = 0;
+    }
+#endif
+
     if (frames_hwctx->texture)
         ID3D11Texture2D_Release(frames_hwctx->texture);
     frames_hwctx->texture = NULL;
@@ -289,6 +310,12 @@ static int d3d11va_frames_init(AVHWFramesContext *ctx)
     HRESULT hr;
     D3D11_TEXTURE2D_DESC texDesc;
 
+#if CONFIG_CUDA
+    if (pthread_mutex_init(&s->cuda_lock, NULL))
+        return AVERROR(ENOMEM);
+    s->cuda_lock_init = 1;
+#endif
+
     for (i = 0; i < FF_ARRAY_ELEMS(supported_formats); i++) {
         if (ctx->sw_format == supported_formats[i].pix_fmt) {
             s->format = supported_formats[i].d3d_format;
@@ -459,6 +486,11 @@ static int d3d11va_transfer_data(AVHWFramesContext *ctx, AVFrame *dst,
     if (frame->hw_frames_ctx->data != (uint8_t *)ctx)
         return AVERROR(EINVAL);
 
+#if CONFIG_CUDA
+    if (other->format == AV_PIX_FMT_CUDA)
+        return d3d11va_cuda_transfer_data(ctx, dst, src);
+#endif
+
     /* Not a transfer to or from a software frame we can handle. Report this as
      * unimplemented rather than invalid, so that a hardware to hardware
      * transfer can still be tried from the other side. */
@@ -707,9 +739,17 @@ int ff_d3d11va_bridge_create(FFD3D11PlaneBridge **bridge, ID3D11Device *dev,
             .SampleDesc = { .Count = 1 },
             .Usage      = D3D11_USAGE_DEFAULT,
             .BindFlags  = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_UNORDERED_ACCESS,
-            .MiscFlags  = D3D11_RESOURCE_MISC_SHARED,
+            /* NT handle sharing where the runtime has it: CUDA cannot
+             * import KMT handles, and Vulkan takes either kind. */
+            .MiscFlags  = D3D11_RESOURCE_MISC_SHARED |
+                          D3D11_RESOURCE_MISC_SHARED_NTHANDLE,
         };
         hr = ID3D11Device_CreateTexture2D(dev, &pd, NULL, &b->planes[i]);
+        if (FAILED(hr)) {
+            /* D3D11.0 runtimes reject NT handle sharing. */
+            pd.MiscFlags = D3D11_RESOURCE_MISC_SHARED;
+            hr = ID3D11Device_CreateTexture2D(dev, &pd, NULL, &b->planes[i]);
+        }
         if (SUCCEEDED(hr))
             hr = ID3D11Device_CreateShaderResourceView(dev,
                                                        (ID3D11Resource *)b->planes[i],
@@ -797,6 +837,544 @@ void ff_d3d11va_bridge_run(FFD3D11PlaneBridge *b, ID3D11DeviceContext *ctx,
 
 #endif /* CONFIG_VULKAN || CONFIG_CUDA */
 
+#if CONFIG_CUDA
+
+/* ffnvcodec does not carry these yet; the values come from cuda.h, where
+ * they have been since CUDA 10.0. */
+#define CU_EXTERNAL_MEMORY_HANDLE_TYPE_D3D11_RESOURCE \
+    ((CUexternalMemoryHandleType)6)
+#define CUDA_EXTERNAL_MEMORY_DEDICATED 0x1
+
+/* CUDA shares a D3D11 texture through the external memory API: each of the
+ * private textures below is imported once as a CUDA mipmapped array, and a
+ * shared D3D11 fence, imported as a CUDA external semaphore, chains the two
+ * queues on the GPU in both directions, so no transfer ever waits on the
+ * CPU. What no CUDA sharing API can do is reach the second plane of a
+ * two-plane texture, so those frames move through the plane bridge, and
+ * single-plane frames through an intermediate texture in their own format,
+ * so the frame textures themselves, which may be recycled decoder arrays,
+ * never have to be imported. One state is kept for each CUDA context frames
+ * have been transferred to or from, created once and kept; cuda_lock
+ * serializes the transfers, which keeps the shared textures owned by one
+ * transfer at a time and the fence values increasing in submission order,
+ * which monitored fences require of their signals. The copies ride the
+ * CUDA device's stream, so the D3D11 waits they end in also depend on
+ * whatever else the application keeps on that stream. */
+typedef struct D3D11CudaInterop {
+    struct D3D11CudaInterop *next;
+    AVBufferRef        *device_ref; /* keeps the device context and loader alive */
+    CUcontext           cuda_ctx;   /* identity of the pairing */
+    int                 status;     /* 0 untried, 1 ready, else error */
+    FFD3D11PlaneBridge *bridge;     /* two-plane formats */
+    ID3D11Texture2D    *tex;        /* single-plane intermediate */
+    ID3D11DeviceContext4 *ctx4;
+    ID3D11Fence        *fence;
+    CUexternalSemaphore sem;
+    CUexternalMemory    mem[2];
+    CUmipmappedArray    mip[2];
+    CUarray             arr[2];     /* level 0 of each texture */
+    int                 nb_planes;
+    uint64_t            fence_val;  /* last fence value handed out */
+    uint64_t            cuda_done;  /* last value CUDA was told to signal */
+} D3D11CudaInterop;
+
+static void d3d11va_cuda_interops_free(AVHWFramesContext *ctx)
+{
+    D3D11VAFramesContext *s = ctx->hwctx;
+    D3D11CudaInterop *ci = s->cuda_interop;
+
+    while (ci) {
+        D3D11CudaInterop *next = ci->next;
+        if (ci->device_ref) {
+            AVHWDeviceContext *dev_ctx = (AVHWDeviceContext *)ci->device_ref->data;
+            AVCUDADeviceContext *cu_hw = dev_ctx->hwctx;
+            CudaFunctions *cu = cu_hw->internal->cuda_dl;
+            CUcontext dummy;
+
+            if (cu->cuCtxPushCurrent(ci->cuda_ctx) == CUDA_SUCCESS) {
+                /* Transfers do not wait for their copies, so make sure none
+                 * is still using the imports about to go away. */
+                cu->cuStreamSynchronize(cu_hw->stream);
+                if (ci->sem)
+                    cu->cuDestroyExternalSemaphore(ci->sem);
+                for (int i = 0; i < FF_ARRAY_ELEMS(ci->mem); i++) {
+                    if (ci->mip[i])
+                        cu->cuMipmappedArrayDestroy(ci->mip[i]);
+                    if (ci->mem[i])
+                        cu->cuDestroyExternalMemory(ci->mem[i]);
+                }
+                cu->cuCtxPopCurrent(&dummy);
+            } else {
+                av_log(ctx, AV_LOG_WARNING, "The CUDA context is gone; its "
+                       "imports leak, and in-flight copies cannot be waited "
+                       "out\n");
+            }
+            av_buffer_unref(&ci->device_ref);
+        }
+        ff_d3d11va_bridge_free(&ci->bridge);
+        if (ci->tex)
+            ID3D11Texture2D_Release(ci->tex);
+        if (ci->fence)
+            ID3D11Fence_Release(ci->fence);
+        if (ci->ctx4)
+            ID3D11DeviceContext4_Release(ci->ctx4);
+        av_free(ci);
+        ci = next;
+    }
+    s->cuda_interop = NULL;
+}
+
+/* Texture layouts CUDA arrays can express. Packed formats without a
+ * matching array format alias one with the same texel size: the copies
+ * only move bytes. */
+static const struct {
+    DXGI_FORMAT d3d_format;
+    CUarray_format format;
+    int channels;
+} cuda_array_formats[] = {
+    { DXGI_FORMAT_R8_UNORM,           CU_AD_FORMAT_UNSIGNED_INT8,  1 },
+    { DXGI_FORMAT_R8G8_UNORM,         CU_AD_FORMAT_UNSIGNED_INT8,  2 },
+    { DXGI_FORMAT_R16_UNORM,          CU_AD_FORMAT_UNSIGNED_INT16, 1 },
+    { DXGI_FORMAT_R16G16_UNORM,       CU_AD_FORMAT_UNSIGNED_INT16, 2 },
+    { DXGI_FORMAT_B8G8R8A8_UNORM,     CU_AD_FORMAT_UNSIGNED_INT8,  4 },
+    { DXGI_FORMAT_R8G8B8A8_UNORM,     CU_AD_FORMAT_UNSIGNED_INT8,  4 },
+    { DXGI_FORMAT_R10G10B10A2_UNORM,  CU_AD_FORMAT_UNSIGNED_INT8,  4 },
+    { DXGI_FORMAT_R16G16B16A16_FLOAT, CU_AD_FORMAT_HALF,           4 },
+};
+
+/* Import one of our own textures, which is NT handle shared, as a CUDA
+ * array. Partial state is left in place for a retry, and freed with the
+ * interop state. */
+static int d3d11va_cuda_import_texture(CudaFunctions *cu,
+                                       ID3D11Texture2D *tex,
+                                       CUexternalMemory *mem,
+                                       CUmipmappedArray *mip, CUarray *arr)
+{
+    D3D11_TEXTURE2D_DESC desc;
+    CUarray_format format = 0;
+    int channels = 0;
+    CUresult ret;
+
+    ID3D11Texture2D_GetDesc(tex, &desc);
+    for (int i = 0; i < FF_ARRAY_ELEMS(cuda_array_formats); i++) {
+        if (cuda_array_formats[i].d3d_format == desc.Format) {
+            format   = cuda_array_formats[i].format;
+            channels = cuda_array_formats[i].channels;
+            break;
+        }
+    }
+    /* KMT handles import into CUDA without an error, and then every access
+     * faults. */
+    if (!channels || !(desc.MiscFlags & D3D11_RESOURCE_MISC_SHARED_NTHANDLE))
+        return AVERROR(ENOSYS);
+
+    if (!*mem) {
+        int texel = channels * (format == CU_AD_FORMAT_UNSIGNED_INT8 ? 1 : 2);
+        IDXGIResource1 *res1;
+        HANDLE handle;
+        HRESULT hr;
+        /* A dedicated import is bound to the resource, whose allocation
+         * size the driver knows; the size field only has to be plausible,
+         * which is fortunate, because D3D11 never reports it. */
+        CUDA_EXTERNAL_MEMORY_HANDLE_DESC mdesc = {
+            .type  = CU_EXTERNAL_MEMORY_HANDLE_TYPE_D3D11_RESOURCE,
+            .size  = (uint64_t)desc.Width * desc.Height * texel,
+            .flags = CUDA_EXTERNAL_MEMORY_DEDICATED,
+        };
+
+        hr = ID3D11Texture2D_QueryInterface(tex, &IID_IDXGIResource1,
+                                            (void **)&res1);
+        if (FAILED(hr))
+            return AVERROR(ENOSYS);
+        hr = IDXGIResource1_CreateSharedHandle(res1, NULL,
+                                               DXGI_SHARED_RESOURCE_READ |
+                                               DXGI_SHARED_RESOURCE_WRITE,
+                                               NULL, &handle);
+        IDXGIResource1_Release(res1);
+        if (FAILED(hr))
+            return AVERROR(ENOSYS);
+
+        mdesc.handle.win32.handle = handle;
+        ret = cu->cuImportExternalMemory(mem, &mdesc);
+        CloseHandle(handle);
+        if (ret != CUDA_SUCCESS)
+            return AVERROR(ENOSYS);
+    }
+
+    if (!*mip) {
+        CUDA_EXTERNAL_MEMORY_MIPMAPPED_ARRAY_DESC adesc = {
+            .arrayDesc = {
+                .Width       = desc.Width,
+                .Height      = desc.Height,
+                .Format      = format,
+                .NumChannels = channels,
+            },
+            .numLevels = 1,
+        };
+        ret = cu->cuExternalMemoryGetMappedMipmappedArray(mip, *mem, &adesc);
+        if (ret != CUDA_SUCCESS)
+            return AVERROR(ENOSYS);
+    }
+
+    ret = cu->cuMipmappedArrayGetLevel(arr, *mip, 0);
+    if (ret != CUDA_SUCCESS)
+        return AVERROR(ENOSYS);
+    return 0;
+}
+
+/* Find or create the state for this CUDA context, cuda_lock held. A failed
+ * creation attempt is kept, so it is not retried every frame, and whatever
+ * it did create is released with the frames context. */
+static int d3d11va_cuda_interop_get(AVHWFramesContext *ctx,
+                                    AVHWFramesContext *cuda_fc,
+                                    D3D11CudaInterop **out)
+{
+    D3D11VAFramesContext *s = ctx->hwctx;
+    AVD3D11VADeviceContext *hwctx = ctx->device_ctx->hwctx;
+    AVCUDADeviceContext *cu_hw = cuda_fc->device_ctx->hwctx;
+    CudaFunctions *cu = cu_hw->internal->cuda_dl;
+    const int planes = av_pix_fmt_count_planes(ctx->sw_format);
+    ID3D11Texture2D *plane_tex[2];
+    D3D11CudaInterop *ci;
+    CUcontext dummy;
+    HRESULT hr;
+    int err;
+
+    for (ci = s->cuda_interop; ci; ci = ci->next)
+        if (ci->cuda_ctx == cu_hw->cuda_ctx)
+            break;
+
+    if (!ci) {
+        ci = av_mallocz(sizeof(*ci));
+        if (!ci)
+            return AVERROR(ENOMEM);
+        ci->cuda_ctx   = cu_hw->cuda_ctx;
+        ci->device_ref = av_buffer_ref(cuda_fc->device_ref);
+        if (!ci->device_ref) {
+            av_free(ci);
+            return AVERROR(ENOMEM);
+        }
+        ci->next = s->cuda_interop;
+        s->cuda_interop = ci;
+    }
+    if (ci->status) {
+        *out = ci;
+        return ci->status > 0 ? 0 : ci->status;
+    }
+
+    if (planes == 2) {
+        if (!ci->bridge) {
+            err = ff_d3d11va_bridge_create(&ci->bridge, hwctx->device,
+                                           ctx->width, ctx->height,
+                                           ctx->sw_format, ctx);
+            if (err < 0)
+                goto fail;
+        }
+        plane_tex[0] = ci->bridge->planes[0];
+        plane_tex[1] = ci->bridge->planes[1];
+        ci->nb_planes = 2;
+    } else {
+        D3D11_TEXTURE2D_DESC desc = {
+            .Width      = ctx->width,
+            .Height     = ctx->height,
+            .MipLevels  = 1,
+            .ArraySize  = 1,
+            .Format     = s->format,
+            .SampleDesc = { .Count = 1 },
+            .Usage      = D3D11_USAGE_DEFAULT,
+            .MiscFlags  = D3D11_RESOURCE_MISC_SHARED |
+                          D3D11_RESOURCE_MISC_SHARED_NTHANDLE,
+        };
+        if (!ci->tex) {
+            hr = ID3D11Device_CreateTexture2D(hwctx->device, &desc, NULL,
+                                              &ci->tex);
+            if (FAILED(hr)) {
+                av_log(ctx, AV_LOG_DEBUG, "Cannot create the intermediate "
+                       "texture (%lx)\n", (long)hr);
+                err = AVERROR(ENOSYS);
+                goto fail;
+            }
+        }
+        plane_tex[0] = ci->tex;
+        ci->nb_planes = 1;
+    }
+
+    if (!ci->fence) {
+        ID3D11Device5 *dev5;
+        hr = ID3D11Device_QueryInterface(hwctx->device, &IID_ID3D11Device5,
+                                         (void **)&dev5);
+        if (SUCCEEDED(hr)) {
+            hr = ID3D11Device5_CreateFence(dev5, 0, D3D11_FENCE_FLAG_SHARED,
+                                           &IID_ID3D11Fence,
+                                           (void **)&ci->fence);
+            ID3D11Device5_Release(dev5);
+        }
+        if (FAILED(hr)) {
+            av_log(ctx, AV_LOG_DEBUG, "Cannot create a shared fence (%lx)\n",
+                   (long)hr);
+            err = AVERROR(ENOSYS);
+            goto fail;
+        }
+    }
+    if (!ci->ctx4) {
+        hr = ID3D11DeviceContext_QueryInterface(hwctx->device_context,
+                                                &IID_ID3D11DeviceContext4,
+                                                (void **)&ci->ctx4);
+        if (FAILED(hr)) {
+            err = AVERROR(ENOSYS);
+            goto fail;
+        }
+    }
+
+    /* The imports double as the capability probe: they fail when the CUDA
+     * context does not sit on the same device as the D3D11 one, and the
+     * caller falls back to system memory. */
+    if (cu->cuCtxPushCurrent(ci->cuda_ctx) != CUDA_SUCCESS) {
+        err = AVERROR_EXTERNAL;
+        goto fail;
+    }
+    err = 0;
+    if (!ci->sem) {
+        HANDLE handle;
+        hr = ID3D11Fence_CreateSharedHandle(ci->fence, NULL, GENERIC_ALL,
+                                            NULL, &handle);
+        if (SUCCEEDED(hr)) {
+            CUDA_EXTERNAL_SEMAPHORE_HANDLE_DESC sdesc = {
+                .type = CU_EXTERNAL_SEMAPHORE_HANDLE_TYPE_D3D12_FENCE,
+                .handle.win32.handle = handle,
+            };
+            if (cu->cuImportExternalSemaphore(&ci->sem, &sdesc) != CUDA_SUCCESS)
+                err = AVERROR(ENOSYS);
+            CloseHandle(handle);
+        } else {
+            err = AVERROR(ENOSYS);
+        }
+    }
+    for (int i = 0; err >= 0 && i < ci->nb_planes; i++)
+        err = d3d11va_cuda_import_texture(cu, plane_tex[i], &ci->mem[i],
+                                          &ci->mip[i], &ci->arr[i]);
+    cu->cuCtxPopCurrent(&dummy);
+    if (err < 0) {
+        av_log(ctx, AV_LOG_DEBUG, "Cannot import a texture into CUDA\n");
+        goto fail;
+    }
+
+    ci->status = 1;
+    *out = ci;
+    return 0;
+
+fail:
+    /* Only the capability probe is worth remembering: a transient failure
+     * like ENOMEM must not condemn the pairing for good. */
+    if (err == AVERROR(ENOSYS))
+        ci->status = err;
+    *out = ci;
+    return err;
+}
+
+static int d3d11va_cuda_transfer_data(AVHWFramesContext *ctx, AVFrame *dst,
+                                      const AVFrame *src)
+{
+    D3D11VAFramesContext *s = ctx->hwctx;
+    AVD3D11VADeviceContext *hwctx = ctx->device_ctx->hwctx;
+    const int to_cuda = dst->format == AV_PIX_FMT_CUDA;
+    const AVFrame *cudaf = to_cuda ? dst : src;
+    const AVFrame *d3df  = to_cuda ? src : dst;
+    ID3D11Resource *tex = (ID3D11Resource *)d3df->data[0];
+    UINT index = (UINT)(intptr_t)d3df->data[1];
+    const int planes = av_pix_fmt_count_planes(ctx->sw_format);
+    const AVPixFmtDescriptor *pixdesc = av_pix_fmt_desc_get(ctx->sw_format);
+    /* Like the system memory paths, only the common region is copied; with
+     * differing sizes the margin keeps whatever the long-lived intermediate
+     * textures held before, and those are sized by the frames context. */
+    int w = FFMIN3(dst->width,  src->width,  ctx->width);
+    int h = FFMIN3(dst->height, src->height, ctx->height);
+    AVHWFramesContext *cuda_fc;
+    AVHWDeviceContext *cuda_cu;
+    AVCUDADeviceContext *cu_hw;
+    CudaFunctions *cu;
+    D3D11CudaInterop *ci;
+    D3D11_TEXTURE2D_DESC desc;
+    D3D11_BOX box = { 0, 0, 0, ctx->width, ctx->height, 1 };
+    CUcontext dummy;
+    uint64_t v1, v2;
+    HRESULT hr;
+    int ret;
+
+    if (!cudaf->hw_frames_ctx)
+        return AVERROR(ENOSYS);
+    cuda_fc = (AVHWFramesContext *)cudaf->hw_frames_ctx->data;
+    if (cuda_fc->format != AV_PIX_FMT_CUDA)
+        return AVERROR(ENOSYS);
+    cuda_cu = cuda_fc->device_ctx;
+    cu_hw   = cuda_fc->device_ctx->hwctx;
+    cu      = cu_hw->internal->cuda_dl;
+
+    /* The copy moves bits, so the two sides have to agree on what the bits
+     * mean, and there is no way to address the planes of more of them.
+     * Packed subsampled formats are also out: their textures constrain the
+     * copy regions in ways this code does not track. */
+    if (cuda_fc->sw_format != ctx->sw_format || planes < 1 || planes > 2 ||
+        (planes == 1 && (pixdesc->log2_chroma_w || pixdesc->log2_chroma_h)))
+        return AVERROR(ENOSYS);
+
+    /* The staging copies address the frame-sized region of the texture,
+     * which decoders often pad, and copies of video formats only accept
+     * aligned regions. The texture must really be in the format the sw
+     * format implies, or the copies would silently move nothing. A
+     * keyed-mutex texture is only coherent for a user that acquires the
+     * mutex, which this code does not do. */
+    ID3D11Texture2D_GetDesc((ID3D11Texture2D *)tex, &desc);
+    if (desc.Width < ctx->width || desc.Height < ctx->height ||
+        desc.SampleDesc.Count != 1 ||
+        (desc.MiscFlags & D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX))
+        return AVERROR(ENOSYS);
+    if (planes == 2) {
+        DXGI_FORMAT tex_fmt, plane_fmts[2];
+        if (((ctx->width | ctx->height) & 1) ||
+            ff_d3d11va_bridge_formats(ctx->sw_format, &tex_fmt, plane_fmts) < 0 ||
+            desc.Format != tex_fmt)
+            return AVERROR(ENOSYS);
+    } else {
+        /* Formats no CUDA array layout matches are rejected before the
+         * interop state allocates anything for them. */
+        int i;
+        for (i = 0; i < FF_ARRAY_ELEMS(cuda_array_formats); i++)
+            if (cuda_array_formats[i].d3d_format == s->format)
+                break;
+        if (desc.Format != s->format ||
+            i == FF_ARRAY_ELEMS(cuda_array_formats))
+            return AVERROR(ENOSYS);
+    }
+
+    pthread_mutex_lock(&s->cuda_lock);
+
+    ret = d3d11va_cuda_interop_get(ctx, cuda_fc, &ci);
+    if (ret < 0)
+        goto end;
+    /* Assembling a frame writes the staging texture through views its format
+     * does not support everywhere. */
+    if (planes == 2 && !to_cuda && !ci->bridge->staging_uav[0]) {
+        ret = AVERROR(ENOSYS);
+        goto end;
+    }
+
+    v1 = ++ci->fence_val;
+    v2 = ++ci->fence_val;
+
+    hwctx->lock(hwctx->lock_ctx);
+    /* The last transfer returned while its copies could still be touching
+     * these textures; the wait orders whatever comes next after them, on
+     * the GPU. Failures have to surface before anything crosses the APIs:
+     * CUDA waiting on a signal that never got submitted would block its
+     * stream for good. */
+    hr = ID3D11DeviceContext4_Wait(ci->ctx4, ci->fence, ci->cuda_done);
+    if (SUCCEEDED(hr) && to_cuda) {
+        if (planes == 2)
+            ff_d3d11va_bridge_run(ci->bridge, hwctx->device_context, tex,
+                                  index, 1);
+        else
+            ID3D11DeviceContext_CopySubresourceRegion(hwctx->device_context,
+                (ID3D11Resource *)ci->tex, 0, 0, 0, 0, tex, index, &box);
+    }
+    /* The signal orders all D3D11 work issued so far ahead of the CUDA
+     * copies, whether it staged the frame or still reads the textures from
+     * an earlier download; the flush submits it, or CUDA would wait on
+     * work still sitting in the command buffer. */
+    if (SUCCEEDED(hr))
+        hr = ID3D11DeviceContext4_Signal(ci->ctx4, ci->fence, v1);
+    ID3D11DeviceContext_Flush(hwctx->device_context);
+    hwctx->unlock(hwctx->lock_ctx);
+    if (FAILED(hr)) {
+        ret = AVERROR_EXTERNAL;
+        goto end;
+    }
+
+    ret = CHECK_CU(cu->cuCtxPushCurrent(ci->cuda_ctx));
+    if (ret < 0)
+        goto end;
+
+    {
+        CUDA_EXTERNAL_SEMAPHORE_WAIT_PARAMS wait = {
+            .params.fence.value = v1,
+        };
+        ret = CHECK_CU(cu->cuWaitExternalSemaphoresAsync(&ci->sem, &wait, 1,
+                                                         cu_hw->stream));
+    }
+
+    for (int i = 0; ret >= 0 && i < planes; i++) {
+        CUDA_MEMCPY2D cpy = {
+            .WidthInBytes = av_image_get_linesize(ctx->sw_format, w, i),
+            .Height       = i ? AV_CEIL_RSHIFT(h, pixdesc->log2_chroma_h) : h,
+        };
+
+        if (to_cuda) {
+            cpy.srcMemoryType = CU_MEMORYTYPE_ARRAY;
+            cpy.srcArray      = ci->arr[i];
+            cpy.dstMemoryType = CU_MEMORYTYPE_DEVICE;
+            cpy.dstDevice     = (CUdeviceptr)(uintptr_t)cudaf->data[i];
+            cpy.dstPitch      = cudaf->linesize[i];
+        } else {
+            cpy.srcMemoryType = CU_MEMORYTYPE_DEVICE;
+            cpy.srcDevice     = (CUdeviceptr)(uintptr_t)cudaf->data[i];
+            cpy.srcPitch      = cudaf->linesize[i];
+            cpy.dstMemoryType = CU_MEMORYTYPE_ARRAY;
+            cpy.dstArray      = ci->arr[i];
+        }
+        ret = CHECK_CU(cu->cuMemcpy2DAsync(&cpy, cu_hw->stream));
+    }
+
+    if (ret >= 0) {
+        CUDA_EXTERNAL_SEMAPHORE_SIGNAL_PARAMS signal = {
+            .params.fence.value = v2,
+        };
+        ret = CHECK_CU(cu->cuSignalExternalSemaphoresAsync(&ci->sem, &signal,
+                                                           1, cu_hw->stream));
+    }
+    if (ret >= 0) {
+        /* No synchronize: the copies stay ordered on the stream for CUDA
+         * consumers, and behind the fence value for D3D11 ones. */
+        ci->cuda_done = v2;
+    } else {
+        /* v2 never signals now, and since cuda_done was not advanced,
+         * nothing will ever wait on it; the next transfer signals a higher
+         * value, which is all a monitored fence asks. */
+        CHECK_CU(cu->cuStreamSynchronize(cu_hw->stream));
+    }
+    CHECK_CU(cu->cuCtxPopCurrent(&dummy));
+    if (ret < 0)
+        goto end;
+
+    if (!to_cuda) {
+        /* Only past the fence value do the textures hold the frame; the
+         * wait keeps the assembly behind them, on the GPU. If it cannot be
+         * enqueued, the destination must not be assembled from stale
+         * planes. */
+        hwctx->lock(hwctx->lock_ctx);
+        hr = ID3D11DeviceContext4_Wait(ci->ctx4, ci->fence, v2);
+        if (SUCCEEDED(hr)) {
+            if (planes == 2)
+                ff_d3d11va_bridge_run(ci->bridge, hwctx->device_context, tex,
+                                      index, 0);
+            else
+                ID3D11DeviceContext_CopySubresourceRegion(hwctx->device_context,
+                    tex, index, 0, 0, 0, (ID3D11Resource *)ci->tex, 0, NULL);
+        }
+        hwctx->unlock(hwctx->lock_ctx);
+        if (FAILED(hr)) {
+            ret = AVERROR_EXTERNAL;
+            goto end;
+        }
+    }
+    ret = 0;
+
+end:
+    pthread_mutex_unlock(&s->cuda_lock);
+    return ret;
+}
+
+#endif /* CONFIG_CUDA */
+
 static int d3d11va_device_init(AVHWDeviceContext *hwdev)
 {
     AVD3D11VADeviceContext *device_hwctx = hwdev->hwctx;
diff --git a/libavutil/hwcontext_d3d11va_internal.h b/libavutil/hwcontext_d3d11va_internal.h
index 0b78aeac5a..3ceaa3d3ef 100644
--- a/libavutil/hwcontext_d3d11va_internal.h
+++ b/libavutil/hwcontext_d3d11va_internal.h
@@ -40,7 +40,7 @@ typedef struct FFD3D11PlaneBridge {
     ID3D11Texture2D           *staging;    /* in the two-plane format */
     ID3D11ShaderResourceView  *staging_srv[2];
     ID3D11UnorderedAccessView *staging_uav[2]; /* missing without UAV support */
-    ID3D11Texture2D           *planes[2];  /* created with MISC_SHARED */
+    ID3D11Texture2D           *planes[2];  /* shared, NT handle if possible */
     ID3D11ShaderResourceView  *plane_srv[2];
     ID3D11UnorderedAccessView *plane_uav[2];
     int                        width;
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.