PR #24523 opened by Steven Xiao (younengxiao)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24523
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24523.patch

This commit enables DNN video filter to run entirely on the GPU
for the full D3D12 pipeline — decode, AI inference, and encode
— without ever copying frames back to system memory, making it
significantly faster on D3D12 hardware.

Add dnn_onnx_d3d12, a helper that binds the DirectML execution
provider to a D3D12VA input frame's own device and does NV12
texture<->tensor conversion on the GPU (CopyTextureRegion + compute
shaders), avoiding a host round-trip.

dnn_processing negotiates this automatically for D3D12 NV12 input
with device=dml, falling back to the host path otherwise. Currently
limited to single-channel FLOAT/NCHW models; chroma bypasses the
model and is copied/upscaled on the GPU.

Example usage:
  - Zero-copy pipeline
  ffmpeg.exe -y -loglevel info \
  -hwaccel d3d12va -hwaccel_output_format d3d12 \
  -i \input-1920x1080.mp4 \
  -vf 
"dnn_processing=dnn_backend=onnx:device=dml:model=ecbsrnetv2_kmv6_y_fused.onnx:input=input:output=output"
 \
  -c:v h264_d3d12va -bf 0 -c:a copy \
  output_2x_sr_zero_copy.mp4

  - CPU round-trip pipeline
  ffmpeg.exe -y -loglevel info \
  -init_hw_device d3d12va=dev -filter_hw_device dev -hwaccel d3d12va \
  -i BL3-Seq1-1920x1080.mp4 \
  -vf 
"dnn_processing=dnn_backend=onnx:device=dml:device_id=0:model=ecbsrnetv2_kmv6_y_fused.onnx:input=input:output=output,hwupload"
 \
  -c:v h264_d3d12va -bf 0 -c:a copy \
  output_2x_sr_round_trip.mp4



From 01f3cbcd5579c727ac974b68107d92e13a2b9701 Mon Sep 17 00:00:00 2001
From: Steven Xiao <[email protected]>
Date: Tue, 15 Sep 2026 16:40:24 -0400
Subject: [PATCH] avfilter/dnn: implement D3D12 DirectML zero-copy for ONNX
 Runtime backend
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This commit enables DNN video filter to run entirely on the GPU
for the full D3D12 pipeline — decode, AI inference, and encode
— without ever copying frames back to system memory, making it
significantly faster on D3D12 hardware.

Add dnn_onnx_d3d12, a helper that binds the DirectML execution
provider to a D3D12VA input frame's own device and does NV12
texture<->tensor conversion on the GPU (CopyTextureRegion + compute
shaders), avoiding a host round-trip.

dnn_processing negotiates this automatically for D3D12 NV12 input
with device=dml, falling back to the host path otherwise. Currently
limited to single-channel FLOAT/NCHW models; chroma bypasses the
model and is copied/upscaled on the GPU.

Example usage:
  # Zero-copy pipeline
  ffmpeg.exe -y -loglevel info \
  -hwaccel d3d12va -hwaccel_output_format d3d12 \
  -i \input-1920x1080.mp4 \
  -vf 
"dnn_processing=dnn_backend=onnx:device=dml:model=ecbsrnetv2_kmv6_y_fused.onnx:input=input:output=output"
 \
  -c:v h264_d3d12va -bf 0 -c:a copy \
  output_2x_sr_zero_copy.mp4

  # CPU round-trip pipeline
  ffmpeg.exe -y -loglevel info \
  -init_hw_device d3d12va=dev -filter_hw_device dev -hwaccel d3d12va \
  -i BL3-Seq1-1920x1080.mp4 \
  -vf 
"dnn_processing=dnn_backend=onnx:device=dml:device_id=0:model=ecbsrnetv2_kmv6_y_fused.onnx:input=input:output=output,hwupload"
 \
  -c:v h264_d3d12va -bf 0 -c:a copy \
  output_2x_sr_round_trip.mp4
---
 Changelog                          |    1 +
 configure                          |    3 +
 doc/filters.texi                   |   24 +
 libavfilter/dnn/Makefile           |    1 +
 libavfilter/dnn/dnn_backend_onnx.c |  138 +++-
 libavfilter/dnn/dnn_onnx_d3d12.c   | 1152 ++++++++++++++++++++++++++++
 libavfilter/dnn/dnn_onnx_d3d12.h   |  158 ++++
 libavfilter/dnn_filter_common.c    |   59 ++
 libavfilter/dnn_filter_common.h    |    4 +
 libavfilter/dnn_interface.h        |    3 +
 libavfilter/vf_dnn_processing.c    |  141 +++-
 11 files changed, 1681 insertions(+), 3 deletions(-)
 create mode 100644 libavfilter/dnn/dnn_onnx_d3d12.c
 create mode 100644 libavfilter/dnn/dnn_onnx_d3d12.h

diff --git a/Changelog b/Changelog
index 1cfa9db1b6..433eee3d34 100644
--- a/Changelog
+++ b/Changelog
@@ -14,6 +14,7 @@ version <next>:
 - NVIDIA optical flow accelerated interpolation filter (vf_fruc_vulkan)
 - H.264 data partitioning support
 - DSD (dsd_msbf) encoder
+- D3D12/DirectML zero-copy inference for the ONNX Runtime DNN backend
 
 
 version 9.0:
diff --git a/configure b/configure
index 046a0bd005..213eb4d6b1 100755
--- a/configure
+++ b/configure
@@ -2719,6 +2719,7 @@ CONFIG_EXTRA="
     deflate_wrapper
     dirac_parse
     dnn
+    dnn_onnx_d3d12
     dovi_rpudec
     dovi_rpuenc
     dvprofile
@@ -3567,6 +3568,7 @@ scale_d3d11_filter_deps="d3d11va"
 scale_d3d12_filter_deps="d3d12va ID3D12VideoProcessor"
 deinterlace_d3d12_filter_deps="d3d12va ID3D12VideoProcessor"
 mestimate_d3d12_filter_deps="d3d12va ID3D12VideoMotionEstimator 
d3d12_motion_estimator"
+dnn_onnx_d3d12_deps="libonnxruntime d3d12va"
 
 amf_deps_any="libdl LoadLibrary"
 nvenc_deps="ffnvcodec"
@@ -7279,6 +7281,7 @@ enabled libmpeghdec       && require_pkg_config 
libmpeghdec "mpeghdec >= 3.0.0"
 enabled libmysofa         && { check_pkg_config libmysofa libmysofa mysofa.h 
mysofa_neighborhood_init_withstepdefine ||
                                require libmysofa mysofa.h 
mysofa_neighborhood_init_withstepdefine -lmysofa $zlib_extralibs; }
 enabled libonnxruntime    && require libonnxruntime onnxruntime_c_api.h 
OrtGetApiBase -lonnxruntime
+enabled libonnxruntime && enabled ID3D12Device && enable dnn_onnx_d3d12
 enabled libopencore_amrnb && { check_pkg_config libopencore_amrnb 
opencore-amrnb opencore-amrnb/interf_dec.h Decoder_Interface_init ||
                                require libopencore_amrnb 
opencore-amrnb/interf_dec.h Decoder_Interface_init -lopencore-amrnb; }
 enabled libopencore_amrwb && { check_pkg_config libopencore_amrwb 
opencore-amrwb opencore-amrwb/dec_if.h D_IF_init ||
diff --git a/doc/filters.texi b/doc/filters.texi
index b7379d3060..a4dc28baab 100644
--- a/doc/filters.texi
+++ b/doc/filters.texi
@@ -12328,6 +12328,18 @@ inference request. The shared @option{async} and 
@option{nireq} options
 therefore have no effect for @code{dnn_backend=onnx}; inference always
 runs synchronously regardless of their values.
 
+When FFmpeg is built with Direct3D 12 support and this build's ONNX
+Runtime provides a working DirectML execution provider, setting
+@option{device=dml} together with @code{AV_PIX_FMT_D3D12} NV12 hardware
+frames as input (e.g. via @code{-hwaccel d3d12va -hwaccel_output_format
+d3d12}) keeps frames in VRAM for the whole filter, from the decoder's
+output through DNN inference and back out, without a host round-trip.
+This path is negotiated automatically when the input qualifies;
+otherwise, the filter fails at configuration time with a descriptive
+error. Currently, only single-channel (luma-only), @code{FLOAT}/NCHW
+models are supported; the chroma plane is copied or bilinearly upscaled
+on the GPU without ever going through the model.
+
 @end table
 
 @item model
@@ -12421,6 +12433,18 @@ throughout the pipeline:
   -y output.mp4
 @end example
 
+@item
+Run a single-channel (luma-only) super-resolution model on D3D12
+hardware frames with the ONNX Runtime backend, keeping frames in VRAM
+from decode through inference (Windows only, requires a build with
+Direct3D 12 support and @code{--enable-libonnxruntime} against a
+DirectML-capable ONNX Runtime):
+@example
+./ffmpeg -hwaccel d3d12va -hwaccel_output_format d3d12 -i input.mp4 \
+  -vf dnn_processing=dnn_backend=onnx:model=sr_model.onnx:device=dml \
+  -y output.mp4
+@end example
+
 @item
 Process rgb24 frames with a TorchScript model using the Libtorch backend:
 @example
diff --git a/libavfilter/dnn/Makefile b/libavfilter/dnn/Makefile
index 7c5d7d8ab6..fe86199e10 100644
--- a/libavfilter/dnn/Makefile
+++ b/libavfilter/dnn/Makefile
@@ -8,5 +8,6 @@ DNN-OBJS-$(CONFIG_LIBTENSORFLOW)             += 
dnn/dnn_backend_tf.o
 DNN-OBJS-$(CONFIG_LIBOPENVINO)               += dnn/dnn_backend_openvino.o
 DNN-OBJS-$(CONFIG_LIBTORCH)                  += dnn/dnn_backend_torch.o
 DNN-OBJS-$(CONFIG_LIBONNXRUNTIME)            += dnn/dnn_backend_onnx.o
+DNN-OBJS-$(CONFIG_DNN_ONNX_D3D12)            += dnn/dnn_onnx_d3d12.o
 
 OBJS-$(CONFIG_DNN)                           += $(DNN-OBJS-yes)
diff --git a/libavfilter/dnn/dnn_backend_onnx.c 
b/libavfilter/dnn/dnn_backend_onnx.c
index 009387b36d..b9793a6d4f 100644
--- a/libavfilter/dnn/dnn_backend_onnx.c
+++ b/libavfilter/dnn/dnn_backend_onnx.c
@@ -40,6 +40,12 @@
 #include <stdio.h>
 #include <string.h>
 
+#if CONFIG_DNN_ONNX_D3D12
+#include "libavutil/hwcontext.h"
+#include "libavutil/hwcontext_d3d12va.h"
+#include "dnn_onnx_d3d12.h"
+#endif
+
 typedef struct ONNXModel {
     DNNModel model;
     DnnContext *ctx;
@@ -53,6 +59,9 @@ typedef struct ONNXModel {
     DNNData input_info;
     int     input_resolved;
     int     output_resolved;
+#if CONFIG_DNN_ONNX_D3D12
+    DnnOnnxD3D12Ctx *hw_ctx;
+#endif
 } ONNXModel;
 
 typedef struct ONNXInferRequest {
@@ -60,6 +69,10 @@ typedef struct ONNXInferRequest {
     OrtValue **output_tensors;
     uint32_t   nb_outputs;
     void      *input_data;
+#if CONFIG_DNN_ONNX_D3D12
+    void     *hw_in_tensor;
+    void     *hw_out_tensor;
+#endif
 } ONNXInferRequest;
 
 typedef struct ONNXRequestItem {
@@ -137,6 +150,11 @@ static void onnx_free_request(ONNXInferRequest *request)
         av_freep(&request->output_tensors);
     }
     request->nb_outputs = 0;
+#if CONFIG_DNN_ONNX_D3D12
+    /* Borrowed references owned by ONNXModel.hw_ctx; never release here. */
+    request->hw_in_tensor  = NULL;
+    request->hw_out_tensor = NULL;
+#endif
 }
 
 static inline void destroy_request_item(ONNXRequestItem **arg)
@@ -188,10 +206,60 @@ static void dnn_free_model_onnx(DNNModel **model)
     if (onnx_model->env)
         g_ort->ReleaseEnv(onnx_model->env);
 
+#if CONFIG_DNN_ONNX_D3D12
+    /* Must be released after the session and session_options above have
+     * let go of the DirectML device and shared command queue it wraps. */
+    ff_dnn_onnx_d3d12_free(&onnx_model->hw_ctx);
+#endif
+
     av_freep(&onnx_model);
     *model = NULL;
 }
 
+#if CONFIG_DNN_ONNX_D3D12
+static void append_d3d12_dml_ep(ONNXModel *onnx_model)
+{
+    DnnContext *ctx = onnx_model->ctx;
+    AVHWFramesContext *frames_ctx;
+    AVHWDeviceContext *dev_ctx;
+    AVD3D12VADeviceContext *d3d12_hwctx;
+    int is_uma = 0;
+
+    if (!ctx->hw_frames_ctx)
+        return;
+
+    frames_ctx = (AVHWFramesContext *)ctx->hw_frames_ctx->data;
+    dev_ctx = frames_ctx->device_ctx;
+    if (dev_ctx->type != AV_HWDEVICE_TYPE_D3D12VA) {
+        av_log(ctx, AV_LOG_WARNING,
+               "dnn_onnx_d3d12: hw_frames_ctx is not backed by a D3D12VA "
+               "device; falling back to host I/O\n");
+        return;
+    }
+    d3d12_hwctx = (AVD3D12VADeviceContext *)dev_ctx->hwctx;
+
+    if (ff_dnn_onnx_d3d12_probe(d3d12_hwctx->device, &is_uma, ctx) < 0)
+        return;
+
+    onnx_model->hw_ctx = ff_dnn_onnx_d3d12_create(d3d12_hwctx->device, ctx);
+    if (!onnx_model->hw_ctx)
+        return;
+
+    if (ff_dnn_onnx_d3d12_append_ep(onnx_model->hw_ctx, 
onnx_model->session_options) < 0) {
+        av_log(ctx, AV_LOG_WARNING,
+               "Failed to register the DirectML EP on the frames' D3D12 "
+               "device; falling back to the default-device DirectML EP\n");
+        ff_dnn_onnx_d3d12_free(&onnx_model->hw_ctx);
+        return;
+    }
+
+    ctx->zero_copy_negotiated = 1;
+    av_log(ctx, AV_LOG_INFO,
+           "Using DirectML execution provider on the frames' own D3D12 
device%s\n",
+           is_uma ? " (UMA)" : "");
+}
+#endif
+
 static int get_input_onnx(DNNModel *model, DNNData *input, const char 
*input_name)
 {
     ONNXModel  *onnx_model = (ONNXModel *)model;
@@ -395,6 +463,31 @@ static int fill_model_input_onnx(ONNXModel *onnx_model, 
ONNXRequestItem *request
     input_shape[2] = input.dims[height_idx];
     input_shape[3] = input.dims[width_idx];
 
+#if CONFIG_DNN_ONNX_D3D12
+    /*
+     * task->in_frame may lack hw_frames_ctx (e.g. get_output_onnx()'s
+     * shape probe); such frames fall through to the host-memory tensor
+     * path, which still works via the DML EP from
+     * ff_dnn_onnx_d3d12_append_ep(), just without zero-copy.
+     */
+    if (ctx->zero_copy_negotiated && task->in_frame->hw_frames_ctx) {
+        ret = ff_dnn_onnx_d3d12_config(onnx_model->hw_ctx,
+                                        task->in_frame->width, 
task->in_frame->height,
+                                        task->out_frame->width, 
task->out_frame->height,
+                                        input.dt, input.layout, 
input.dims[channel_idx]);
+        if (ret < 0)
+            goto err;
+
+        ret = ff_dnn_onnx_d3d12_fill_input(onnx_model->hw_ctx, task->in_frame,
+                                            &infer_request->hw_in_tensor,
+                                            &infer_request->hw_out_tensor);
+        if (ret < 0)
+            goto err;
+
+        return 0;
+    }
+#endif
+
     /*
      * Build the byte count with checked size_t multiplications instead of
      * multiplying four int64_t shape values in one expression.
@@ -498,6 +591,14 @@ static int onnx_start_inference(void *args)
         return AVERROR(EINVAL);
     }
 
+#if CONFIG_DNN_ONNX_D3D12
+    if (infer_request->hw_in_tensor) {
+        if (!infer_request->hw_out_tensor) {
+            av_log(ctx, AV_LOG_ERROR, "D3D12 output tensor is NULL\n");
+            return DNN_GENERIC_ERROR;
+        }
+    } else
+#endif
     if (!infer_request->input_tensor) {
         av_log(ctx, AV_LOG_ERROR, "Input tensor is NULL\n");
         return DNN_GENERIC_ERROR;
@@ -544,6 +645,15 @@ static int onnx_start_inference(void *args)
 
     input_names[0] = task->input_name;
 
+#if CONFIG_DNN_ONNX_D3D12
+    if (infer_request->hw_in_tensor) {
+        return ff_dnn_onnx_d3d12_run(onnx_model->hw_ctx, onnx_model->session,
+                                      input_names[0], task->output_names[0],
+                                      infer_request->hw_in_tensor,
+                                      infer_request->hw_out_tensor);
+    }
+#endif
+
     /* ORT writes task->nb_output result handles into this array; it must be
      * allocated (and NULL-initialised) before Run() so ORT owns each slot. */
     av_freep(&infer_request->output_tensors);
@@ -590,6 +700,17 @@ static void infer_completion_callback(void *args)
     OrtStatus *status;
     int ret;
 
+#if CONFIG_DNN_ONNX_D3D12
+    if (infer_request->hw_in_tensor) {
+        ret = ff_dnn_onnx_d3d12_writeback(onnx_model->hw_ctx, task->out_frame);
+        if (ret < 0)
+            av_log(ctx, AV_LOG_ERROR, "D3D12 writeback failed (%d)\n", ret);
+        else
+            task->inference_done++;
+        goto err;
+    }
+#endif
+
     outputs = av_calloc(infer_request->nb_outputs, sizeof(*outputs));
     if (!outputs) {
         av_log(ctx, AV_LOG_ERROR, "Failed to allocate output DNNData array\n");
@@ -879,6 +1000,12 @@ static DNNModel *dnn_load_model_onnx(DnnContext *ctx, 
DNNFunctionType func_type,
     }
     g_ort->SetSessionGraphOptimizationLevel(onnx_model->session_options, 
ORT_ENABLE_ALL);
 
+    /*
+     * device=dml is registered below exactly like cuda/vitisai, but with
+     * one extra detail: if ctx->hw_frames_ctx is already set,
+     * append_d3d12_dml_ep() binds the DirectML EP directly to that D3D12
+     * device, instead of letting ONNX Runtime pick its own default one.
+     */
     if (ctx->device && av_strcasecmp(ctx->device, "cpu") != 0) {
         if (av_strcasecmp(ctx->device, "cuda") == 0) {
             if (g_ort->SessionOptionsAppendExecutionProvider_CUDA) {
@@ -916,7 +1043,16 @@ static DNNModel *dnn_load_model_onnx(DnnContext *ctx, 
DNNFunctionType func_type,
             if (status)
                 g_ort->ReleaseStatus(status);
 
-            if (g_ort->SessionOptionsAppendExecutionProvider) {
+#if CONFIG_DNN_ONNX_D3D12
+            /* No-op unless the filter already set ctx->hw_frames_ctx via
+             * ff_dnn_set_hw_frames_ctx(); if set, binds the DirectML EP to
+             * that device so tensor I/O can stay in VRAM. */
+            append_d3d12_dml_ep(onnx_model);
+#endif
+
+            if (ctx->zero_copy_negotiated) {
+                /* Device-bound DirectML EP already appended above. */
+            } else if (g_ort->SessionOptionsAppendExecutionProvider) {
                 status = g_ort->SessionOptionsAppendExecutionProvider(
                     onnx_model->session_options, "DML",
                     dml_options_keys, dml_options_values, 1);
diff --git a/libavfilter/dnn/dnn_onnx_d3d12.c b/libavfilter/dnn/dnn_onnx_d3d12.c
new file mode 100644
index 0000000000..f3837191de
--- /dev/null
+++ b/libavfilter/dnn/dnn_onnx_d3d12.c
@@ -0,0 +1,1152 @@
+/*
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ *
+ * 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
+ */
+
+#define COBJMACROS
+#include <windows.h>
+#include <initguid.h>
+#include <d3d12.h>
+#include <d3dcompiler.h>
+
+#include "onnxruntime_c_api.h"
+#include "dml_provider_factory.h"
+
+DEFINE_GUID(IID_IDMLDevice, 0x6dbd6437, 0x96fd, 0x423f,
+            0xa9, 0x8c, 0xae, 0x5e, 0x7c, 0x2a, 0x57, 0x3f);
+typedef HRESULT(WINAPI *PFN_DMLCreateDevice)(ID3D12Device *, UINT, REFIID, 
void **);
+
+#include "config.h"
+#include "libavutil/error.h"
+#include "libavutil/fifo.h"
+#include "libavutil/hwcontext_d3d12va.h"
+#include "libavutil/log.h"
+#include "libavutil/macros.h"
+#include "libavutil/mem.h"
+#include "dnn_onnx_d3d12.h"
+
+#include <stdint.h>
+#include <string.h>
+
+static inline HMODULE onnx_d3d12_dlopen(const char *name)
+{
+    return LoadLibraryExA(name, NULL,
+                           LOAD_LIBRARY_SEARCH_SYSTEM32 | 
LOAD_LIBRARY_SEARCH_APPLICATION_DIR);
+}
+
+/*
+ * All three compute shaders share one root signature: 8 32-bit root
+ * constants at b0 (each shader only names/uses the subset it needs; the
+ * layout must stay identical across all of them so a single
+ * ID3D12RootSignature works for all three ID3D12PipelineState objects), plus
+ * two UAV root descriptors (u0, u1) bound directly to a buffer's GPU virtual
+ * address -- no descriptor heap is needed since every buffer is consumed as
+ * a raw RWByteAddressBuffer.
+ *
+ * The shader bodies below are HLSL source, stringized via the
+ * HLSL_SHADER() macro (the same trick used in vsrc_gfxcapture_shader.h) so
+ * they can be written as plain, syntax-highlightable HLSL instead of
+ * manually escaped C string literals.
+ */
+#define HLSL_SHADER(shader) #shader
+
+static const char *kShaderCompactY = HLSL_SHADER(
+    cbuffer Params : register(b0) {
+        uint width; uint height; uint src_pitch; uint dst_pitch;
+        uint pad4; uint pad5; uint pad6; uint pad7;
+    };
+    RWByteAddressBuffer SrcBuf : register(u0); // padded byte plane, 1 Bpp
+    RWByteAddressBuffer DstBuf : register(u1); // compact FP32 tensor
+    [numthreads(8, 8, 1)]
+    void CSCompactY(uint3 id : SV_DispatchThreadID)
+    {
+        if (id.x >= width || id.y >= height) return;
+        uint src_off = id.y * src_pitch + id.x;
+        uint aligned = src_off & ~3u;
+        uint shift = (src_off & 3u) * 8u;
+        uint byte_val = (SrcBuf.Load(aligned) >> shift) & 0xFFu;
+        float v = float(byte_val) * (1.0f / 255.0f);
+        uint dst_off = (id.y * width + id.x) * 4u;
+        DstBuf.Store(dst_off, asuint(v));
+    }
+);
+
+static const char *kShaderExpandY = HLSL_SHADER(
+    cbuffer Params : register(b0) {
+        uint width; uint height; uint src_pitch; uint dst_pitch;
+        uint pad4; uint pad5; uint pad6; uint pad7;
+    };
+    RWByteAddressBuffer SrcBuf : register(u0); // compact FP32 tensor
+    RWByteAddressBuffer DstBuf : register(u1); // padded byte plane, 1 Bpp
+    [numthreads(8, 8, 1)]
+    void CSExpandY(uint3 id : SV_DispatchThreadID)
+    {
+        if (id.x >= width || id.y >= height) return;
+        uint src_off = (id.y * width + id.x) * 4u;
+        float v = asfloat(SrcBuf.Load(src_off));
+        float scaled = clamp(v * 255.0f, 0.0f, 255.0f);
+        uint byte_val = (uint)round(scaled);
+        uint dst_off = id.y * dst_pitch + id.x;
+        uint aligned = dst_off & ~3u;
+        uint shift = (dst_off & 3u) * 8u;
+        uint mask = 0xFFu << shift;
+        uint prev;
+        DstBuf.InterlockedAnd(aligned, ~mask, prev);
+        DstBuf.InterlockedOr(aligned, (byte_val & 0xFFu) << shift, prev);
+    }
+);
+
+/* Params here means (dst_width, dst_height, src_width, src_height, src_pitch,
+ * dst_pitch, -, -) instead of (width, height, src_pitch, dst_pitch, ...): the
+ * root signature layout (8 constants) is shared, the meaning is shader-local. 
*/
+static const char *kShaderUpscaleUV = HLSL_SHADER(
+    cbuffer Params : register(b0) {
+        uint dst_width; uint dst_height; uint src_width; uint src_height;
+        uint src_pitch; uint dst_pitch; uint pad6; uint pad7;
+    };
+    RWByteAddressBuffer SrcBuf : register(u0); // padded interleaved UV plane, 
2 Bpp
+    RWByteAddressBuffer DstBuf : register(u1); // padded interleaved UV plane, 
2 Bpp
+    uint LoadUV(uint x, uint y, uint comp)
+    {
+        x = min(x, src_width - 1); y = min(y, src_height - 1);
+        uint off = y * src_pitch + x * 2u + comp;
+        uint aligned = off & ~3u; uint shift = (off & 3u) * 8u;
+        return (SrcBuf.Load(aligned) >> shift) & 0xFFu;
+    }
+    void StoreUV(uint x, uint y, uint comp, uint value)
+    {
+        uint off = y * dst_pitch + x * 2u + comp;
+        uint aligned = off & ~3u; uint shift = (off & 3u) * 8u;
+        uint mask = 0xFFu << shift; uint prev;
+        DstBuf.InterlockedAnd(aligned, ~mask, prev);
+        DstBuf.InterlockedOr(aligned, (value & 0xFFu) << shift, prev);
+    }
+    [numthreads(8, 8, 1)]
+    void CSUpscaleUV(uint3 id : SV_DispatchThreadID)
+    {
+        if (id.x >= dst_width || id.y >= dst_height) return;
+        float sx = (float(id.x) + 0.5f) * float(src_width)  / float(dst_width) 
 - 0.5f;
+        float sy = (float(id.y) + 0.5f) * float(src_height) / 
float(dst_height) - 0.5f;
+        int x0 = (int)floor(sx);
+        int y0 = (int)floor(sy);
+        float fx = sx - float(x0);
+        float fy = sy - float(y0);
+        uint x0c = (uint)clamp(x0,     0, (int)src_width  - 1);
+        uint x1c = (uint)clamp(x0 + 1, 0, (int)src_width  - 1);
+        uint y0c = (uint)clamp(y0,     0, (int)src_height - 1);
+        uint y1c = (uint)clamp(y0 + 1, 0, (int)src_height - 1);
+        for (uint comp = 0; comp < 2; comp++) {
+            float v00 = float(LoadUV(x0c, y0c, comp));
+            float v10 = float(LoadUV(x1c, y0c, comp));
+            float v01 = float(LoadUV(x0c, y1c, comp));
+            float v11 = float(LoadUV(x1c, y1c, comp));
+            float v = lerp(lerp(v00, v10, fx), lerp(v01, v11, fx), fy);
+            uint bv = (uint)round(clamp(v, 0.0f, 255.0f));
+            StoreUV(id.x, id.y, comp, bv);
+        }
+    }
+);
+
+#undef HLSL_SHADER
+
+typedef struct DnnOnnxD3D12CmdAlloc {
+    ID3D12CommandAllocator *alloc;
+    UINT64 fence_value;
+} DnnOnnxD3D12CmdAlloc;
+
+struct DnnOnnxD3D12Ctx {
+    void *log_ctx;
+
+    ID3D12Device *device;
+    IDMLDevice *dml_device;
+    /* DIRECT queue, shared with the DirectML EP */
+    ID3D12CommandQueue *queue;
+    const OrtApi *ort;
+    const OrtDmlApi *dml_api;
+
+    HMODULE directml_dll;
+    HMODULE d3dcompiler_dll;
+    HMODULE d3d12_dll;
+    pD3DCompile compile_fn;
+
+    PFN_D3D12_SERIALIZE_ROOT_SIGNATURE serialize_root_sig_fn;
+
+    ID3D12RootSignature *root_sig;
+    ID3D12PipelineState *pso_compact_y;
+    ID3D12PipelineState *pso_expand_y;
+    ID3D12PipelineState *pso_upscale_uv;
+
+    AVFifo *alloc_queue;
+    ID3D12GraphicsCommandList *cmd_list;
+
+    ID3D12Fence *fence;
+    UINT64 fence_value;
+    HANDLE fence_event;
+
+    /* the two compact tensors DML actually touches */
+    ID3D12Resource *in_buf;
+    ID3D12Resource *out_buf;
+
+    /* padded transit buffers whose row pitch matches CopyTextureRegion's
+     * 256-byte-aligned requirement.
+     * pad_y_src/pad_y_dst additionally bridge to the tight (unpadded)
+     * in_buf/out_buf tensors above via CSCompactY/CSExpandY; pad_uv_src and
+     * pad_uv_dst hold the interleaved-UV (R8G8) bytes directly and are
+     * read/written between each other by CSUpscaleUV */
+    ID3D12Resource *pad_y_src;
+    ID3D12Resource *pad_y_dst;
+    ID3D12Resource *pad_uv_src;
+    ID3D12Resource *pad_uv_dst;
+
+    void *dml_in_alloc;
+    void *dml_out_alloc;
+    OrtMemoryInfo *dml_mem_info;
+    OrtValue *in_tensor;
+    OrtValue *out_tensor;
+
+    int configured;
+    int in_w, in_h, out_w, out_h, channels;
+    DNNDataType dt;
+    DNNLayout layout;
+    int uv_passthrough;
+
+    UINT y_src_pitch, y_dst_pitch, uv_src_pitch, uv_dst_pitch;
+    UINT y_src_rows, y_dst_rows, uv_src_rows, uv_dst_rows;
+};
+
+static void transition(ID3D12GraphicsCommandList *cl, ID3D12Resource *res, 
UINT subresource,
+                        D3D12_RESOURCE_STATES before, D3D12_RESOURCE_STATES 
after)
+{
+    D3D12_RESOURCE_BARRIER barrier = { 0 };
+    barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
+    barrier.Transition.pResource = res;
+    barrier.Transition.Subresource = subresource;
+    barrier.Transition.StateBefore = before;
+    barrier.Transition.StateAfter = after;
+    ID3D12GraphicsCommandList_ResourceBarrier(cl, 1, &barrier);
+}
+
+static void uav_barrier(ID3D12GraphicsCommandList *cl, ID3D12Resource *res)
+{
+    D3D12_RESOURCE_BARRIER barrier = { 0 };
+    barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_UAV;
+    barrier.UAV.pResource = res;
+    ID3D12GraphicsCommandList_ResourceBarrier(cl, 1, &barrier);
+}
+
+static void wait_for_fence(DnnOnnxD3D12Ctx *s)
+{
+    if (s->fence_value > 0 && ID3D12Fence_GetCompletedValue(s->fence) < 
s->fence_value) {
+        DWORD wait_result;
+        ID3D12Fence_SetEventOnCompletion(s->fence, s->fence_value, 
s->fence_event);
+        do {
+            wait_result = WaitForSingleObject(s->fence_event, 5000);
+            if (wait_result == WAIT_TIMEOUT) {
+                HRESULT removed = 
ID3D12Device_GetDeviceRemovedReason(s->device);
+                av_log(s->log_ctx, AV_LOG_WARNING,
+                       "dnn_onnx_d3d12: still waiting on GPU fence after 5s 
(want=%llu, completed=%llu), "
+                       "GetDeviceRemovedReason=0x%08x\n",
+                       (unsigned long long)s->fence_value,
+                       (unsigned long 
long)ID3D12Fence_GetCompletedValue(s->fence),
+                       (unsigned int)removed);
+            }
+        } while (wait_result == WAIT_TIMEOUT);
+    }
+}
+
+static int create_uav_buffer(ID3D12Device *device, UINT64 size, ID3D12Resource 
**out)
+{
+    D3D12_HEAP_PROPERTIES heap_props = { 0 };
+    D3D12_RESOURCE_DESC desc = { 0 };
+    HRESULT hr;
+
+    heap_props.Type = D3D12_HEAP_TYPE_DEFAULT;
+
+    desc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
+    desc.Width = size;
+    desc.Height = 1;
+    desc.DepthOrArraySize = 1;
+    desc.MipLevels = 1;
+    desc.Format = DXGI_FORMAT_UNKNOWN;
+    desc.SampleDesc.Count = 1;
+    desc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
+    desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS;
+
+    hr = ID3D12Device_CreateCommittedResource(device, &heap_props, 
D3D12_HEAP_FLAG_NONE, &desc,
+                                               
D3D12_RESOURCE_STATE_UNORDERED_ACCESS, NULL,
+                                               &IID_ID3D12Resource, (void 
**)out);
+    return SUCCEEDED(hr) ? 0 : AVERROR(ENOMEM);
+}
+
+static void release_buffers(DnnOnnxD3D12Ctx *s)
+{
+    OrtStatus *status;
+
+    if (s->in_tensor) {
+        s->ort->ReleaseValue(s->in_tensor);
+        s->in_tensor = NULL;
+    }
+    if (s->out_tensor) {
+        s->ort->ReleaseValue(s->out_tensor);
+        s->out_tensor = NULL;
+    }
+
+    /* DML allocations must be freed before the D3D12 resources they wrap. */
+    if (s->dml_in_alloc) {
+        status = s->dml_api->FreeGPUAllocation(s->dml_in_alloc);
+        if (status) s->ort->ReleaseStatus(status);
+        s->dml_in_alloc = NULL;
+    }
+    if (s->dml_out_alloc) {
+        status = s->dml_api->FreeGPUAllocation(s->dml_out_alloc);
+        if (status) s->ort->ReleaseStatus(status);
+        s->dml_out_alloc = NULL;
+    }
+    if (s->dml_mem_info) {
+        s->ort->ReleaseMemoryInfo(s->dml_mem_info);
+        s->dml_mem_info = NULL;
+    }
+
+    if (s->in_buf) {
+        ID3D12Resource_Release(s->in_buf);
+        s->in_buf = NULL;
+    }
+    if (s->out_buf) {
+        ID3D12Resource_Release(s->out_buf);
+        s->out_buf = NULL;
+    }
+    if (s->pad_y_src) {
+        ID3D12Resource_Release(s->pad_y_src);
+        s->pad_y_src = NULL;
+    }
+    if (s->pad_y_dst) {
+        ID3D12Resource_Release(s->pad_y_dst);
+        s->pad_y_dst = NULL;
+    }
+    if (s->pad_uv_src) {
+        ID3D12Resource_Release(s->pad_uv_src);
+        s->pad_uv_src = NULL;
+    }
+    if (s->pad_uv_dst) {
+        ID3D12Resource_Release(s->pad_uv_dst);
+        s->pad_uv_dst = NULL;
+    }
+
+    s->configured = 0;
+}
+
+static int create_root_signature(DnnOnnxD3D12Ctx *s)
+{
+    D3D12_ROOT_PARAMETER params[3] = { 0 };
+    D3D12_ROOT_SIGNATURE_DESC desc = { 0 };
+    ID3DBlob *sig_blob = NULL, *err_blob = NULL;
+    HRESULT hr;
+
+    /*The 3 compute shaders HLSL use the exact same root signature:
+     * the 3 registers are:
+     * 0: Constants (8 32-bit values)
+     * 1: UAV (Unordered Access View)
+     * 2: UAV (Unordered Access View)
+     */
+    params[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS;
+    params[0].Constants.ShaderRegister = 0;
+    params[0].Constants.Num32BitValues = 8;
+    params[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;
+
+    params[1].ParameterType = D3D12_ROOT_PARAMETER_TYPE_UAV;
+    params[1].Descriptor.ShaderRegister = 0;
+    params[1].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;
+
+    params[2].ParameterType = D3D12_ROOT_PARAMETER_TYPE_UAV;
+    params[2].Descriptor.ShaderRegister = 1;
+    params[2].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;
+
+    desc.NumParameters = 3;
+    desc.pParameters = params;
+    desc.Flags = D3D12_ROOT_SIGNATURE_FLAG_NONE;
+
+    hr = s->serialize_root_sig_fn(&desc, D3D_ROOT_SIGNATURE_VERSION_1, 
&sig_blob, &err_blob);
+    if (FAILED(hr)) {
+        av_log(s->log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: root signature 
serialize failed: %s\n",
+               err_blob ? (const char *)ID3D10Blob_GetBufferPointer(err_blob) 
: "unknown error");
+        if (err_blob) ID3D10Blob_Release(err_blob);
+        return AVERROR_EXTERNAL;
+    }
+    if (err_blob) ID3D10Blob_Release(err_blob);
+
+    hr = ID3D12Device_CreateRootSignature(s->device, 0, 
ID3D10Blob_GetBufferPointer(sig_blob),
+                                           ID3D10Blob_GetBufferSize(sig_blob),
+                                           &IID_ID3D12RootSignature, (void 
**)&s->root_sig);
+    ID3D10Blob_Release(sig_blob);
+    if (FAILED(hr)) {
+        av_log(s->log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: CreateRootSignature 
failed: 0x%lX\n", (long)hr);
+        return AVERROR_EXTERNAL;
+    }
+    return 0;
+}
+
+static int compile_and_create_pso(DnnOnnxD3D12Ctx *s, const char *src, const 
char *entry,
+                                   ID3D12PipelineState **out)
+{
+    ID3DBlob *code = NULL, *err = NULL;
+    D3D12_COMPUTE_PIPELINE_STATE_DESC desc = { 0 };
+    HRESULT hr;
+
+    hr = s->compile_fn(src, strlen(src), NULL, NULL, NULL, entry, "cs_5_0", 0, 
0, &code, &err);
+    if (FAILED(hr)) {
+        av_log(s->log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: failed to compile 
%s: %s\n", entry,
+               err ? (const char *)ID3D10Blob_GetBufferPointer(err) : "unknown 
error");
+        if (err) ID3D10Blob_Release(err);
+        return AVERROR_EXTERNAL;
+    }
+    if (err) ID3D10Blob_Release(err);
+
+    desc.pRootSignature = s->root_sig;
+    desc.CS.pShaderBytecode = ID3D10Blob_GetBufferPointer(code);
+    desc.CS.BytecodeLength = ID3D10Blob_GetBufferSize(code);
+
+    hr = ID3D12Device_CreateComputePipelineState(s->device, &desc, 
&IID_ID3D12PipelineState, (void **)out);
+    ID3D10Blob_Release(code);
+    if (FAILED(hr)) {
+        av_log(s->log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: 
CreateComputePipelineState(%s) failed: 0x%lX\n",
+               entry, (long)hr);
+        return AVERROR_EXTERNAL;
+    }
+    return 0;
+}
+
+int ff_dnn_onnx_d3d12_probe(void *d3d12_device_void, int *is_uma, void 
*log_ctx)
+{
+    ID3D12Device *device = (ID3D12Device *)d3d12_device_void;
+    HMODULE dml_dll, compiler_dll;
+
+    if (is_uma)
+        *is_uma = 0;
+
+    dml_dll = onnx_d3d12_dlopen("DirectML.dll");
+    if (!dml_dll) {
+        av_log(log_ctx, AV_LOG_VERBOSE, "dnn_onnx_d3d12: DirectML.dll not 
found, D3D12 hardware-frame path unavailable\n");
+        return AVERROR(ENOSYS);
+    }
+    compiler_dll = onnx_d3d12_dlopen("d3dcompiler_47.dll");
+    if (!compiler_dll) {
+        FreeLibrary(dml_dll);
+        av_log(log_ctx, AV_LOG_VERBOSE, "dnn_onnx_d3d12: d3dcompiler_47.dll 
not found, D3D12 hardware-frame path unavailable\n");
+        return AVERROR(ENOSYS);
+    }
+    FreeLibrary(compiler_dll);
+    FreeLibrary(dml_dll);
+
+    if (is_uma && device) {
+        D3D12_FEATURE_DATA_ARCHITECTURE1 arch = { 0 };
+        if (SUCCEEDED(ID3D12Device_CheckFeatureSupport(device, 
D3D12_FEATURE_ARCHITECTURE1, &arch, sizeof(arch))))
+            *is_uma = arch.UMA || arch.CacheCoherentUMA;
+    }
+    return 0;
+}
+
+static int get_valid_command_allocator(DnnOnnxD3D12Ctx *s, 
ID3D12CommandAllocator **out)
+{
+    DnnOnnxD3D12CmdAlloc entry;
+    HRESULT hr;
+
+    if (av_fifo_peek(s->alloc_queue, &entry, 1, 0) >= 0) {
+        UINT64 completed = ID3D12Fence_GetCompletedValue(s->fence);
+        if (completed >= entry.fence_value) {
+            *out = entry.alloc;
+            av_fifo_read(s->alloc_queue, &entry, 1);
+            return 0;
+        }
+    }
+
+    hr = ID3D12Device_CreateCommandAllocator(s->device, 
D3D12_COMMAND_LIST_TYPE_DIRECT,
+                                              &IID_ID3D12CommandAllocator, 
(void **)out);
+    if (FAILED(hr)) {
+        av_log(s->log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: 
CreateCommandAllocator failed: 0x%lX\n", (long)hr);
+        return AVERROR_EXTERNAL;
+    }
+    return 0;
+}
+
+static int discard_command_allocator(DnnOnnxD3D12Ctx *s, 
ID3D12CommandAllocator *alloc, UINT64 fence_value)
+{
+    DnnOnnxD3D12CmdAlloc entry = { .alloc = alloc, .fence_value = fence_value 
};
+
+    if (av_fifo_write(s->alloc_queue, &entry, 1) < 0) {
+        ID3D12CommandAllocator_Release(alloc);
+        return AVERROR(ENOMEM);
+    }
+    return 0;
+}
+
+DnnOnnxD3D12Ctx *ff_dnn_onnx_d3d12_create(void *d3d12_device_void, void 
*log_ctx)
+{
+    ID3D12Device *device = (ID3D12Device *)d3d12_device_void;
+    DnnOnnxD3D12Ctx *s = (DnnOnnxD3D12Ctx *)av_mallocz(sizeof(*s));
+    D3D12_COMMAND_QUEUE_DESC qdesc = { 0 };
+    PFN_DMLCreateDevice create_dml_device;
+    HRESULT hr;
+
+    if (!s)
+        return NULL;
+    s->log_ctx = log_ctx;
+    s->device = device;
+
+    s->ort = OrtGetApiBase()->GetApi(ORT_API_VERSION);
+    if (!s->ort) {
+        av_log(log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: failed to get 
OrtApi\n");
+        goto fail;
+    }
+
+    s->directml_dll = onnx_d3d12_dlopen("DirectML.dll");
+    if (!s->directml_dll) {
+        av_log(log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: failed to load 
DirectML.dll\n");
+        goto fail;
+    }
+    create_dml_device = (PFN_DMLCreateDevice)(void 
*)GetProcAddress(s->directml_dll, "DMLCreateDevice");
+    if (!create_dml_device) {
+        av_log(log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: DMLCreateDevice not 
found in DirectML.dll\n");
+        goto fail;
+    }
+    hr = create_dml_device(device, 0 /* DML_CREATE_DEVICE_FLAG_NONE */,
+                            &IID_IDMLDevice, (void **)&s->dml_device);
+    if (FAILED(hr)) {
+        av_log(log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: DMLCreateDevice failed: 
0x%lX\n", (long)hr);
+        goto fail;
+    }
+
+    s->d3dcompiler_dll = onnx_d3d12_dlopen("d3dcompiler_47.dll");
+    if (!s->d3dcompiler_dll) {
+        av_log(log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: failed to load 
d3dcompiler_47.dll\n");
+        goto fail;
+    }
+    s->compile_fn = (pD3DCompile)(void *)GetProcAddress(s->d3dcompiler_dll, 
"D3DCompile");
+    if (!s->compile_fn) {
+        av_log(log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: D3DCompile not found in 
d3dcompiler_47.dll\n");
+        goto fail;
+    }
+
+    s->d3d12_dll = onnx_d3d12_dlopen("d3d12.dll");
+    if (!s->d3d12_dll) {
+        av_log(log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: failed to load 
d3d12.dll\n");
+        goto fail;
+    }
+    s->serialize_root_sig_fn = (PFN_D3D12_SERIALIZE_ROOT_SIGNATURE)(void *)
+        GetProcAddress(s->d3d12_dll, "D3D12SerializeRootSignature");
+    if (!s->serialize_root_sig_fn) {
+        av_log(log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: 
D3D12SerializeRootSignature not found in d3d12.dll\n");
+        goto fail;
+    }
+
+    qdesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
+    qdesc.Priority = D3D12_COMMAND_QUEUE_PRIORITY_NORMAL;
+    hr = ID3D12Device_CreateCommandQueue(device, &qdesc, 
&IID_ID3D12CommandQueue, (void **)&s->queue);
+    if (FAILED(hr)) {
+        av_log(log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: CreateCommandQueue 
failed: 0x%lX\n", (long)hr);
+        goto fail;
+    }
+
+    hr = ID3D12Device_CreateFence(device, 0, D3D12_FENCE_FLAG_NONE, 
&IID_ID3D12Fence, (void **)&s->fence);
+    if (FAILED(hr))
+        goto fail;
+    s->fence_event = CreateEvent(NULL, FALSE, FALSE, NULL);
+    if (!s->fence_event)
+        goto fail;
+
+    s->alloc_queue = av_fifo_alloc2(2, sizeof(DnnOnnxD3D12CmdAlloc), 
AV_FIFO_FLAG_AUTO_GROW);
+    if (!s->alloc_queue)
+        goto fail;
+
+    {
+        ID3D12CommandAllocator *alloc = NULL;
+        if (get_valid_command_allocator(s, &alloc) < 0)
+            goto fail;
+        hr = ID3D12Device_CreateCommandList(device, 0, 
D3D12_COMMAND_LIST_TYPE_DIRECT, alloc, NULL,
+                                             &IID_ID3D12GraphicsCommandList, 
(void **)&s->cmd_list);
+        if (FAILED(hr)) {
+            ID3D12CommandAllocator_Release(alloc);
+            goto fail;
+        }
+        ID3D12GraphicsCommandList_Close(s->cmd_list);
+        if (discard_command_allocator(s, alloc, 0) < 0)
+            goto fail;
+    }
+
+    if (create_root_signature(s) < 0)
+        goto fail;
+    if (compile_and_create_pso(s, kShaderCompactY, "CSCompactY", 
&s->pso_compact_y) < 0)
+        goto fail;
+    if (compile_and_create_pso(s, kShaderExpandY, "CSExpandY", 
&s->pso_expand_y) < 0)
+        goto fail;
+    if (compile_and_create_pso(s, kShaderUpscaleUV, "CSUpscaleUV", 
&s->pso_upscale_uv) < 0)
+        goto fail;
+
+    return s;
+
+fail:
+    ff_dnn_onnx_d3d12_free(&s);
+    return NULL;
+}
+
+void ff_dnn_onnx_d3d12_free(DnnOnnxD3D12Ctx **ctx)
+{
+    DnnOnnxD3D12Ctx *s = *ctx;
+
+    if (!s)
+        return;
+
+    if (s->queue && s->fence && s->fence_event) {
+        UINT64 v = s->fence_value + 1;
+        if (SUCCEEDED(ID3D12CommandQueue_Signal(s->queue, s->fence, v)) &&
+            ID3D12Fence_GetCompletedValue(s->fence) < v) {
+            ID3D12Fence_SetEventOnCompletion(s->fence, v, s->fence_event);
+            WaitForSingleObject(s->fence_event, INFINITE);
+        }
+    }
+
+    if (s->ort)
+        release_buffers(s);
+
+    if (s->pso_compact_y) {
+        ID3D12PipelineState_Release(s->pso_compact_y);
+        s->pso_compact_y = NULL;
+    }
+    if (s->pso_expand_y) {
+        ID3D12PipelineState_Release(s->pso_expand_y);
+        s->pso_expand_y = NULL;
+    }
+    if (s->pso_upscale_uv) {
+        ID3D12PipelineState_Release(s->pso_upscale_uv);
+        s->pso_upscale_uv = NULL;
+    }
+    if (s->root_sig) {
+        ID3D12RootSignature_Release(s->root_sig);
+        s->root_sig = NULL;
+    }
+    if (s->cmd_list) {
+        ID3D12GraphicsCommandList_Release(s->cmd_list);
+        s->cmd_list = NULL;
+    }
+    if (s->alloc_queue) {
+        DnnOnnxD3D12CmdAlloc entry;
+        while (av_fifo_read(s->alloc_queue, &entry, 1) >= 0)
+            ID3D12CommandAllocator_Release(entry.alloc);
+        av_fifo_freep2(&s->alloc_queue);
+    }
+    if (s->fence_event) {
+        CloseHandle(s->fence_event);
+        s->fence_event = NULL;
+    }
+    if (s->fence) {
+        ID3D12Fence_Release(s->fence);
+        s->fence = NULL;
+    }
+    if (s->queue) {
+        ID3D12CommandQueue_Release(s->queue);
+        s->queue = NULL;
+    }
+    if (s->dml_device) {
+        IUnknown_Release((IUnknown *)s->dml_device);
+        s->dml_device = NULL;
+    }
+
+    if (s->d3dcompiler_dll) {
+        FreeLibrary(s->d3dcompiler_dll);
+        s->d3dcompiler_dll = NULL;
+    }
+    if (s->d3d12_dll) {
+        FreeLibrary(s->d3d12_dll);
+        s->d3d12_dll = NULL;
+    }
+    if (s->directml_dll) {
+        FreeLibrary(s->directml_dll);
+        s->directml_dll = NULL;
+    }
+
+    av_free(s);
+    *ctx = NULL;
+}
+
+int ff_dnn_onnx_d3d12_append_ep(DnnOnnxD3D12Ctx *ctx, void 
*session_options_void)
+{
+    OrtSessionOptions *session_options = (OrtSessionOptions 
*)session_options_void;
+    OrtStatus *status;
+
+    if (!ctx->dml_api) {
+        status = ctx->ort->GetExecutionProviderApi("DML", ORT_API_VERSION, 
(const void **)&ctx->dml_api);
+        if (status != NULL) {
+            av_log(ctx->log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: failed to get 
OrtDmlApi: %s\n",
+                   ctx->ort->GetErrorMessage(status));
+            ctx->ort->ReleaseStatus(status);
+            ctx->dml_api = NULL;
+            return AVERROR(ENOSYS);
+        }
+    }
+
+    /* Same DirectML constraints as the string-based EP path in 
dnn_backend_onnx.c:
+     * sequential execution only, and the memory-pattern optimizer must be 
off. */
+    status = ctx->ort->SetSessionExecutionMode(session_options, 
ORT_SEQUENTIAL);
+    if (status) ctx->ort->ReleaseStatus(status);
+    status = ctx->ort->DisableMemPattern(session_options);
+    if (status) ctx->ort->ReleaseStatus(status);
+
+    status = 
ctx->dml_api->SessionOptionsAppendExecutionProvider_DML1(session_options, 
ctx->dml_device, ctx->queue);
+    if (status != NULL) {
+        av_log(ctx->log_ctx, AV_LOG_ERROR,
+               "dnn_onnx_d3d12: failed to append DirectML EP on shared 
device/queue: %s\n",
+               ctx->ort->GetErrorMessage(status));
+        ctx->ort->ReleaseStatus(status);
+        return AVERROR_EXTERNAL;
+    }
+    return 0;
+}
+
+static int check_mult(uint64_t a, uint64_t b, uint64_t *out)
+{
+    if (a != 0 && b > UINT64_MAX / a)
+        return AVERROR(ERANGE);
+    *out = a * b;
+    return 0;
+}
+
+int ff_dnn_onnx_d3d12_config(DnnOnnxD3D12Ctx *ctx, int in_w, int in_h, int 
out_w, int out_h,
+                              DNNDataType dt, DNNLayout layout, int channels)
+{
+    D3D12_RESOURCE_DESC nv12_desc = { 0 };
+    D3D12_PLACED_SUBRESOURCE_FOOTPRINT footprints[2];
+    UINT num_rows[2];
+    UINT64 row_sizes[2], total_bytes;
+    uint64_t in_pixels, out_pixels, tmp;
+    OrtStatus *status;
+    int64_t in_shape[4], out_shape[4];
+
+    if (channels != 1) {
+        av_log(ctx->log_ctx, AV_LOG_ERROR,
+               "dnn_onnx_d3d12: only single-channel models are supported in 
this phase "
+               "(got channels=%d); multi-channel support is second-phase 
work\n", channels);
+        return AVERROR(ENOSYS);
+    }
+    if (dt != DNN_FLOAT) {
+        av_log(ctx->log_ctx, AV_LOG_ERROR,
+               "dnn_onnx_d3d12: only FLOAT tensors are supported in this 
phase; "
+               "uint8 tensor support is second-phase work\n");
+        return AVERROR(ENOSYS);
+    }
+    if (layout != DL_NCHW) {
+        av_log(ctx->log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: only NCHW layout 
is supported\n");
+        return AVERROR(ENOSYS);
+    }
+    if (in_w <= 0 || in_h <= 0 || out_w <= 0 || out_h <= 0) {
+        av_log(ctx->log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: invalid tensor 
dimensions %dx%d -> %dx%d\n",
+               in_w, in_h, out_w, out_h);
+        return AVERROR(EINVAL);
+    }
+
+    if (ctx->configured && ctx->in_w == in_w && ctx->in_h == in_h && 
ctx->out_w == out_w &&
+        ctx->out_h == out_h && ctx->channels == channels && ctx->dt == dt && 
ctx->layout == layout)
+        return 0;
+
+    wait_for_fence(ctx);
+    release_buffers(ctx);
+
+    if (check_mult((uint64_t)in_w, (uint64_t)in_h, &in_pixels) < 0 ||
+        check_mult(in_pixels, sizeof(float), &tmp) < 0 ||
+        check_mult((uint64_t)out_w, (uint64_t)out_h, &out_pixels) < 0 ||
+        check_mult(out_pixels, sizeof(float), &tmp) < 0) {
+        av_log(ctx->log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: tensor size 
overflow\n");
+        return AVERROR(ERANGE);
+    }
+
+     /* CopyTextureRegion() needs to know the exact row pitch it expects for
+     * a texture of these dimensions, so we ask the driver once here via
+     * GetCopyableFootprints(), instead of re-deriving the alignment rule
+     * (and re-querying it) on every frame. */
+    nv12_desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
+    nv12_desc.Width = in_w;
+    nv12_desc.Height = in_h;
+    nv12_desc.DepthOrArraySize = 1;
+    nv12_desc.MipLevels = 1;
+    nv12_desc.Format = DXGI_FORMAT_NV12;
+    nv12_desc.SampleDesc.Count = 1;
+    nv12_desc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
+    ID3D12Device_GetCopyableFootprints(ctx->device, &nv12_desc, 0, 2, 0, 
footprints, num_rows, row_sizes, &total_bytes);
+    ctx->y_src_pitch = footprints[0].Footprint.RowPitch;
+    ctx->y_src_rows = (UINT)in_h;
+    ctx->uv_src_pitch = footprints[1].Footprint.RowPitch;
+    ctx->uv_src_rows = (UINT)((in_h + 1) / 2);
+
+    nv12_desc.Width = out_w;
+    nv12_desc.Height = out_h;
+    ID3D12Device_GetCopyableFootprints(ctx->device, &nv12_desc, 0, 2, 0, 
footprints, num_rows, row_sizes, &total_bytes);
+    ctx->y_dst_pitch = footprints[0].Footprint.RowPitch;
+    ctx->y_dst_rows = (UINT)out_h;
+    ctx->uv_dst_pitch = footprints[1].Footprint.RowPitch;
+    ctx->uv_dst_rows = (UINT)((out_h + 1) / 2);
+
+    if (create_uav_buffer(ctx->device, (UINT64)in_w * in_h * sizeof(float), 
&ctx->in_buf) < 0 ||
+        create_uav_buffer(ctx->device, (UINT64)out_w * out_h * sizeof(float), 
&ctx->out_buf) < 0 ||
+        create_uav_buffer(ctx->device, (UINT64)ctx->y_src_pitch * 
ctx->y_src_rows, &ctx->pad_y_src) < 0 ||
+        create_uav_buffer(ctx->device, (UINT64)ctx->y_dst_pitch * 
ctx->y_dst_rows, &ctx->pad_y_dst) < 0 ||
+        create_uav_buffer(ctx->device, (UINT64)ctx->uv_src_pitch * 
ctx->uv_src_rows, &ctx->pad_uv_src) < 0 ||
+        create_uav_buffer(ctx->device, (UINT64)ctx->uv_dst_pitch * 
ctx->uv_dst_rows, &ctx->pad_uv_dst) < 0) {
+        av_log(ctx->log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: failed to allocate 
GPU buffers\n");
+        release_buffers(ctx);
+        return AVERROR(ENOMEM);
+    }
+
+    if (!ctx->dml_api) {
+        status = ctx->ort->GetExecutionProviderApi("DML", ORT_API_VERSION, 
(const void **)&ctx->dml_api);
+        if (status) {
+            ctx->ort->ReleaseStatus(status);
+            ctx->dml_api = NULL;
+        }
+    }
+    if (!ctx->dml_api) {
+        av_log(ctx->log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: OrtDmlApi 
unavailable\n");
+        release_buffers(ctx);
+        return AVERROR(ENOSYS);
+    }
+
+    status = ctx->ort->CreateMemoryInfo("DML", OrtDeviceAllocator, 0, 
OrtMemTypeDefault, &ctx->dml_mem_info);
+    if (status) {
+        av_log(ctx->log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: 
CreateMemoryInfo(DML) failed: %s\n",
+               ctx->ort->GetErrorMessage(status));
+        ctx->ort->ReleaseStatus(status);
+        release_buffers(ctx);
+        return AVERROR_EXTERNAL;
+    }
+
+    status = ctx->dml_api->CreateGPUAllocationFromD3DResource(ctx->in_buf, 
&ctx->dml_in_alloc);
+    if (status) {
+        av_log(ctx->log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: 
CreateGPUAllocationFromD3DResource(in) failed: %s\n",
+               ctx->ort->GetErrorMessage(status));
+        ctx->ort->ReleaseStatus(status);
+        release_buffers(ctx);
+        return AVERROR_EXTERNAL;
+    }
+    status = ctx->dml_api->CreateGPUAllocationFromD3DResource(ctx->out_buf, 
&ctx->dml_out_alloc);
+    if (status) {
+        av_log(ctx->log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: 
CreateGPUAllocationFromD3DResource(out) failed: %s\n",
+               ctx->ort->GetErrorMessage(status));
+        ctx->ort->ReleaseStatus(status);
+        release_buffers(ctx);
+        return AVERROR_EXTERNAL;
+    }
+
+    /* NCHW layout */
+    in_shape[0]  = 1; in_shape[1]  = 1; in_shape[2]  = in_h;  in_shape[3]  = 
in_w;
+    out_shape[0] = 1; out_shape[1] = 1; out_shape[2] = out_h; out_shape[3] = 
out_w;
+
+    status = ctx->ort->CreateTensorWithDataAsOrtValue(ctx->dml_mem_info, 
ctx->dml_in_alloc,
+                                                     (size_t)in_w * in_h * 
sizeof(float),
+                                                     in_shape, 4, 
ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT,
+                                                     &ctx->in_tensor);
+    if (status) {
+        av_log(ctx->log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: 
CreateTensorWithDataAsOrtValue(in) failed: %s\n",
+               ctx->ort->GetErrorMessage(status));
+        ctx->ort->ReleaseStatus(status);
+        release_buffers(ctx);
+        return AVERROR_EXTERNAL;
+    }
+    status = ctx->ort->CreateTensorWithDataAsOrtValue(ctx->dml_mem_info, 
ctx->dml_out_alloc,
+                                                     (size_t)out_w * out_h * 
sizeof(float),
+                                                     out_shape, 4, 
ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT,
+                                                     &ctx->out_tensor);
+    if (status) {
+        av_log(ctx->log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: 
CreateTensorWithDataAsOrtValue(out) failed: %s\n",
+               ctx->ort->GetErrorMessage(status));
+        ctx->ort->ReleaseStatus(status);
+        release_buffers(ctx);
+        return AVERROR_EXTERNAL;
+    }
+
+    ctx->in_w = in_w; ctx->in_h = in_h; ctx->out_w = out_w; ctx->out_h = out_h;
+    ctx->channels = channels; ctx->dt = dt; ctx->layout = layout;
+    ctx->uv_passthrough = (in_w == out_w && in_h == out_h);
+    ctx->configured = 1;
+    return 0;
+}
+
+int ff_dnn_onnx_d3d12_fill_input(DnnOnnxD3D12Ctx *ctx, const AVFrame *in, void 
**in_tensor, void **out_tensor)
+{
+    AVD3D12VAFrame *iframe;
+    D3D12_TEXTURE_COPY_LOCATION dst_loc = { 0 }, src_loc = { 0 };
+    ID3D12CommandList *lists[1];
+    ID3D12CommandAllocator *alloc = NULL;
+    UINT consts[8] = { 0 };
+    HRESULT hr;
+    int ret;
+
+    if (!ctx->configured) {
+        av_log(ctx->log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: fill_input() 
called before config()\n");
+        return AVERROR(EINVAL);
+    }
+    if (!in->hw_frames_ctx || !in->data[0]) {
+        av_log(ctx->log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: input frame has no 
D3D12 hw data\n");
+        return AVERROR(EINVAL);
+    }
+    iframe = (AVD3D12VAFrame *)in->data[0];
+    if (iframe->subresource_index != 0) {
+        av_log(ctx->log_ctx, AV_LOG_ERROR,
+               "dnn_onnx_d3d12: texture-array frames are not supported yet 
(subresource_index=%d)\n",
+               iframe->subresource_index);
+        return AVERROR(ENOSYS);
+    }
+
+    /* An AVD3D12VAFrame's texture is what the D3D12VA decoder wrote
+     * directly: an opaque, tiled/swizzled NV12 surface, not something a
+     * compute shader can index into. Turning it into DML's compact FP32
+     * tensor takes two real per-frame byte copies in fill_input():
+     * 1. CopyTextureRegion() flattens the opaque texture into a linear,
+     *    correctly-pitched staging buffer.
+     * 2. CSCompactY normalizes those linear bytes into the tight in_buf 
tensor.
+    */
+    ret = get_valid_command_allocator(ctx, &alloc);
+    if (ret < 0) return ret;
+    hr = ID3D12CommandAllocator_Reset(alloc);
+    if (FAILED(hr)) { discard_command_allocator(ctx, alloc, 0); return 
AVERROR_EXTERNAL; }
+    hr = ID3D12GraphicsCommandList_Reset(ctx->cmd_list, alloc, NULL);
+    if (FAILED(hr)) { discard_command_allocator(ctx, alloc, 0); return 
AVERROR_EXTERNAL; }
+
+    /* Flatten the decoder's opaque tiled/swizzled NV12 texture into linear,
+     * correctly-pitched staging buffers via CopyTextureRegion() */
+    transition(ctx->cmd_list, iframe->texture, 0, D3D12_RESOURCE_STATE_COMMON, 
D3D12_RESOURCE_STATE_COPY_SOURCE);
+    transition(ctx->cmd_list, iframe->texture, 1, D3D12_RESOURCE_STATE_COMMON, 
D3D12_RESOURCE_STATE_COPY_SOURCE);
+    transition(ctx->cmd_list, ctx->pad_y_src, 0, 
D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_COPY_DEST);
+    transition(ctx->cmd_list, ctx->pad_uv_src, 0, 
D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_COPY_DEST);
+
+    dst_loc.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
+    dst_loc.PlacedFootprint.Footprint.Depth = 1;
+    src_loc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
+    src_loc.pResource = iframe->texture;
+
+    dst_loc.pResource = ctx->pad_y_src;
+    dst_loc.PlacedFootprint.Footprint.Format = DXGI_FORMAT_R8_UNORM;
+    dst_loc.PlacedFootprint.Footprint.Width = (UINT)ctx->in_w;
+    dst_loc.PlacedFootprint.Footprint.Height = (UINT)ctx->in_h;
+    dst_loc.PlacedFootprint.Footprint.RowPitch = ctx->y_src_pitch;
+    src_loc.SubresourceIndex = 0;
+    ID3D12GraphicsCommandList_CopyTextureRegion(ctx->cmd_list, &dst_loc, 0, 0, 
0, &src_loc, NULL);
+
+    dst_loc.pResource = ctx->pad_uv_src;
+    dst_loc.PlacedFootprint.Footprint.Format = DXGI_FORMAT_R8G8_UNORM;
+    dst_loc.PlacedFootprint.Footprint.Width = (UINT)((ctx->in_w + 1) / 2);
+    dst_loc.PlacedFootprint.Footprint.Height = (UINT)((ctx->in_h + 1) / 2);
+    dst_loc.PlacedFootprint.Footprint.RowPitch = ctx->uv_src_pitch;
+    src_loc.SubresourceIndex = 1;
+    ID3D12GraphicsCommandList_CopyTextureRegion(ctx->cmd_list, &dst_loc, 0, 0, 
0, &src_loc, NULL);
+
+    transition(ctx->cmd_list, iframe->texture, 0, 
D3D12_RESOURCE_STATE_COPY_SOURCE, D3D12_RESOURCE_STATE_COMMON);
+    transition(ctx->cmd_list, iframe->texture, 1, 
D3D12_RESOURCE_STATE_COPY_SOURCE, D3D12_RESOURCE_STATE_COMMON);
+    transition(ctx->cmd_list, ctx->pad_y_src, 0, 
D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
+    transition(ctx->cmd_list, ctx->pad_uv_src, 0, 
D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
+
+    /* Normalize the linear Y bytes just staged above into the tight in_buf
+     * tensor DML expects, by dispatching CSCompactY. (The UV planes take a
+     * different path, only the Y plane goes through a compute shader here.) */
+    consts[0] = (UINT)ctx->in_w;
+    consts[1] = (UINT)ctx->in_h;
+    consts[2] = ctx->y_src_pitch;
+    ID3D12GraphicsCommandList_SetComputeRootSignature(ctx->cmd_list, 
ctx->root_sig);
+    ID3D12GraphicsCommandList_SetPipelineState(ctx->cmd_list, 
ctx->pso_compact_y);
+    ID3D12GraphicsCommandList_SetComputeRoot32BitConstants(ctx->cmd_list, 0, 
8, consts, 0);
+    ID3D12GraphicsCommandList_SetComputeRootUnorderedAccessView(
+        ctx->cmd_list, 1, ID3D12Resource_GetGPUVirtualAddress(ctx->pad_y_src));
+    ID3D12GraphicsCommandList_SetComputeRootUnorderedAccessView(
+        ctx->cmd_list, 2, ID3D12Resource_GetGPUVirtualAddress(ctx->in_buf));
+    ID3D12GraphicsCommandList_Dispatch(ctx->cmd_list, ((UINT)ctx->in_w + 7) / 
8, ((UINT)ctx->in_h + 7) / 8, 1);
+    uav_barrier(ctx->cmd_list, ctx->in_buf);
+
+    hr = ID3D12GraphicsCommandList_Close(ctx->cmd_list);
+    if (FAILED(hr)) { discard_command_allocator(ctx, alloc, 0); return 
AVERROR_EXTERNAL; }
+
+    if (iframe->sync_ctx.fence && iframe->sync_ctx.fence_value > 0 &&
+        ID3D12Fence_GetCompletedValue(iframe->sync_ctx.fence) < 
iframe->sync_ctx.fence_value)
+        ID3D12CommandQueue_Wait(ctx->queue, iframe->sync_ctx.fence, 
iframe->sync_ctx.fence_value);
+
+    lists[0] = (ID3D12CommandList *)ctx->cmd_list;
+    ID3D12CommandQueue_ExecuteCommandLists(ctx->queue, 1, lists);
+    ctx->fence_value++;
+    hr = ID3D12CommandQueue_Signal(ctx->queue, ctx->fence, ctx->fence_value);
+    if (FAILED(hr)) { discard_command_allocator(ctx, alloc, ctx->fence_value); 
return AVERROR_EXTERNAL; }
+
+    if (discard_command_allocator(ctx, alloc, ctx->fence_value) < 0)
+        return AVERROR(ENOMEM);
+
+    if (in_tensor) *in_tensor = ctx->in_tensor;
+    if (out_tensor) *out_tensor = ctx->out_tensor;
+    return 0;
+}
+
+int ff_dnn_onnx_d3d12_run(DnnOnnxD3D12Ctx *ctx, void *session_void, const char 
*in_name, const char *out_name,
+                          void *in_tensor_void, void *out_tensor_void)
+{
+    OrtSession *session = (OrtSession *)session_void;
+    OrtValue *in_tensor = (OrtValue *)in_tensor_void;
+    OrtValue *out_tensor = (OrtValue *)out_tensor_void;
+    OrtIoBinding *binding = NULL;
+    OrtStatus *status;
+    int ret = 0;
+
+    status = ctx->ort->CreateIoBinding(session, &binding);
+    if (status) {
+        av_log(ctx->log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: CreateIoBinding 
failed: %s\n",
+               ctx->ort->GetErrorMessage(status));
+        ctx->ort->ReleaseStatus(status);
+        return DNN_GENERIC_ERROR;
+    }
+
+    status = ctx->ort->BindInput(binding, in_name, in_tensor);
+    if (!status)
+        status = ctx->ort->BindOutput(binding, out_name, out_tensor);
+    if (!status)
+        status = ctx->ort->RunWithBinding(session, NULL, binding);
+
+    if (status) {
+        av_log(ctx->log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: DirectML inference 
failed: %s\n",
+               ctx->ort->GetErrorMessage(status));
+        ctx->ort->ReleaseStatus(status);
+        ret = DNN_GENERIC_ERROR;
+    }
+
+    ctx->ort->ReleaseIoBinding(binding);
+    return ret;
+}
+
+int ff_dnn_onnx_d3d12_writeback(DnnOnnxD3D12Ctx *ctx, AVFrame *out)
+{
+    AVD3D12VAFrame *oframe;
+    D3D12_TEXTURE_COPY_LOCATION dst_loc = { 0 }, src_loc = { 0 };
+    ID3D12Resource *uv_src_buf;
+    ID3D12CommandList *lists[1];
+    ID3D12CommandAllocator *alloc = NULL;
+    UINT consts[8] = { 0 };
+    UINT uv_dst_w = (UINT)((ctx->out_w + 1) / 2), uv_dst_h = 
(UINT)((ctx->out_h + 1) / 2);
+    HRESULT hr;
+    int ret;
+
+    if (!ctx->configured)
+        return AVERROR(EINVAL);
+    if (!out->data[0]) {
+        av_log(ctx->log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: output frame has 
no D3D12 hw data\n");
+        return AVERROR(EINVAL);
+    }
+
+    oframe = (AVD3D12VAFrame *)out->data[0];
+    if (oframe->subresource_index != 0) {
+        av_log(ctx->log_ctx, AV_LOG_ERROR, "dnn_onnx_d3d12: texture-array 
frames are not supported yet\n");
+        return AVERROR(ENOSYS);
+    }
+
+    ret = get_valid_command_allocator(ctx, &alloc);
+    if (ret < 0) return ret;
+    hr = ID3D12CommandAllocator_Reset(alloc);
+    if (FAILED(hr)) { discard_command_allocator(ctx, alloc, 0); return 
AVERROR_EXTERNAL; }
+    hr = ID3D12GraphicsCommandList_Reset(ctx->cmd_list, alloc, NULL);
+    if (FAILED(hr)) { discard_command_allocator(ctx, alloc, 0); return 
AVERROR_EXTERNAL; }
+
+    uav_barrier(ctx->cmd_list, ctx->out_buf);
+
+    /* Denormalize the compact FP32 out_buf tensor DML produced back into
+     * linear luma bytes, into the padded pad_y_dst staging buffer, by
+     * dispatching CSExpandY (the reverse of fill_input()'s CSCompactY). */
+    consts[0] = (UINT)ctx->out_w;
+    consts[1] = (UINT)ctx->out_h;
+    consts[3] = ctx->y_dst_pitch;
+    ID3D12GraphicsCommandList_SetComputeRootSignature(ctx->cmd_list, 
ctx->root_sig);
+    ID3D12GraphicsCommandList_SetPipelineState(ctx->cmd_list, 
ctx->pso_expand_y);
+    ID3D12GraphicsCommandList_SetComputeRoot32BitConstants(ctx->cmd_list, 0, 
8, consts, 0);
+    ID3D12GraphicsCommandList_SetComputeRootUnorderedAccessView(
+        ctx->cmd_list, 1, ID3D12Resource_GetGPUVirtualAddress(ctx->out_buf));
+    ID3D12GraphicsCommandList_SetComputeRootUnorderedAccessView(
+        ctx->cmd_list, 2, ID3D12Resource_GetGPUVirtualAddress(ctx->pad_y_dst));
+    ID3D12GraphicsCommandList_Dispatch(ctx->cmd_list, ((UINT)ctx->out_w + 7) / 
8, ((UINT)ctx->out_h + 7) / 8, 1);
+    uav_barrier(ctx->cmd_list, ctx->pad_y_dst);
+
+    /* Chroma: never goes through a DML tensor. Either reuse what
+     * fill_input() already staged in pad_uv_src (1:1, no resize needed),
+     * or resize it into pad_uv_dst via the CSUpscaleUV compute shader. */
+    if (ctx->uv_passthrough) {
+        /* chroma bypasses the model entirely: reuse what fill_input() already 
staged */
+        uv_src_buf = ctx->pad_uv_src;
+    } else {
+        UINT uv_src_w = (UINT)((ctx->in_w + 1) / 2), uv_src_h = 
(UINT)((ctx->in_h + 1) / 2);
+        UINT uv_consts[8] = { uv_dst_w, uv_dst_h, uv_src_w, uv_src_h, 
ctx->uv_src_pitch, ctx->uv_dst_pitch };
+        ID3D12GraphicsCommandList_SetPipelineState(ctx->cmd_list, 
ctx->pso_upscale_uv);
+        ID3D12GraphicsCommandList_SetComputeRoot32BitConstants(ctx->cmd_list, 
0, 8, uv_consts, 0);
+        ID3D12GraphicsCommandList_SetComputeRootUnorderedAccessView(
+            ctx->cmd_list, 1, 
ID3D12Resource_GetGPUVirtualAddress(ctx->pad_uv_src));
+        ID3D12GraphicsCommandList_SetComputeRootUnorderedAccessView(
+            ctx->cmd_list, 2, 
ID3D12Resource_GetGPUVirtualAddress(ctx->pad_uv_dst));
+        ID3D12GraphicsCommandList_Dispatch(ctx->cmd_list, (uv_dst_w + 7) / 8, 
(uv_dst_h + 7) / 8, 1);
+        uav_barrier(ctx->cmd_list, ctx->pad_uv_dst);
+        uv_src_buf = ctx->pad_uv_dst;
+    }
+
+    /* Flatten the padded linear Y/UV buffers just produced above back into
+     * the decoder-format opaque tiled/swizzled NV12 output texture, via
+     * CopyTextureRegion() -- the reverse of fill_input()'s flatten step. */
+    transition(ctx->cmd_list, ctx->pad_y_dst, 0, 
D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_COPY_SOURCE);
+    transition(ctx->cmd_list, uv_src_buf, 0, 
D3D12_RESOURCE_STATE_UNORDERED_ACCESS, D3D12_RESOURCE_STATE_COPY_SOURCE);
+    transition(ctx->cmd_list, oframe->texture, 0, D3D12_RESOURCE_STATE_COMMON, 
D3D12_RESOURCE_STATE_COPY_DEST);
+    transition(ctx->cmd_list, oframe->texture, 1, D3D12_RESOURCE_STATE_COMMON, 
D3D12_RESOURCE_STATE_COPY_DEST);
+
+    src_loc.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
+    src_loc.PlacedFootprint.Footprint.Depth = 1;
+    dst_loc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
+    dst_loc.pResource = oframe->texture;
+
+    src_loc.pResource = ctx->pad_y_dst;
+    src_loc.PlacedFootprint.Footprint.Format = DXGI_FORMAT_R8_UNORM;
+    src_loc.PlacedFootprint.Footprint.Width = (UINT)ctx->out_w;
+    src_loc.PlacedFootprint.Footprint.Height = (UINT)ctx->out_h;
+    src_loc.PlacedFootprint.Footprint.RowPitch = ctx->y_dst_pitch;
+    dst_loc.SubresourceIndex = 0;
+    ID3D12GraphicsCommandList_CopyTextureRegion(ctx->cmd_list, &dst_loc, 0, 0, 
0, &src_loc, NULL);
+
+    src_loc.pResource = uv_src_buf;
+    src_loc.PlacedFootprint.Footprint.Format = DXGI_FORMAT_R8G8_UNORM;
+    src_loc.PlacedFootprint.Footprint.Width = uv_dst_w;
+    src_loc.PlacedFootprint.Footprint.Height = uv_dst_h;
+    src_loc.PlacedFootprint.Footprint.RowPitch = ctx->uv_dst_pitch;
+    dst_loc.SubresourceIndex = 1;
+    ID3D12GraphicsCommandList_CopyTextureRegion(ctx->cmd_list, &dst_loc, 0, 0, 
0, &src_loc, NULL);
+
+    transition(ctx->cmd_list, ctx->pad_y_dst, 0, 
D3D12_RESOURCE_STATE_COPY_SOURCE, D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
+    transition(ctx->cmd_list, uv_src_buf, 0, D3D12_RESOURCE_STATE_COPY_SOURCE, 
D3D12_RESOURCE_STATE_UNORDERED_ACCESS);
+    transition(ctx->cmd_list, oframe->texture, 0, 
D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_COMMON);
+    transition(ctx->cmd_list, oframe->texture, 1, 
D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_COMMON);
+
+    hr = ID3D12GraphicsCommandList_Close(ctx->cmd_list);
+    if (FAILED(hr)) { discard_command_allocator(ctx, alloc, 0); return 
AVERROR_EXTERNAL; }
+
+    lists[0] = (ID3D12CommandList *)ctx->cmd_list;
+    ID3D12CommandQueue_ExecuteCommandLists(ctx->queue, 1, lists);
+    ctx->fence_value++;
+    hr = ID3D12CommandQueue_Signal(ctx->queue, ctx->fence, ctx->fence_value);
+    if (FAILED(hr)) { discard_command_allocator(ctx, alloc, ctx->fence_value); 
return AVERROR_EXTERNAL; }
+
+    if (discard_command_allocator(ctx, alloc, ctx->fence_value) < 0)
+        return AVERROR(ENOMEM);
+
+    hr = ID3D12CommandQueue_Signal(ctx->queue, oframe->sync_ctx.fence, 
++oframe->sync_ctx.fence_value);
+    if (FAILED(hr))
+        return AVERROR_EXTERNAL;
+
+    return 0;
+}
diff --git a/libavfilter/dnn/dnn_onnx_d3d12.h b/libavfilter/dnn/dnn_onnx_d3d12.h
new file mode 100644
index 0000000000..377bde521f
--- /dev/null
+++ b/libavfilter/dnn/dnn_onnx_d3d12.h
@@ -0,0 +1,158 @@
+/*
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ *
+ * 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
+ */
+
+/**
+ * @file
+ * Pure C interface to the D3D12/DirectML GPU pipeline helper used by the
+ * ONNX Runtime backend and the dnn_processing filter.
+ *
+ * This header intentionally never mentions DirectML, OrtDmlApi, or any
+ * other execution-provider-specific type: DnnOnnxD3D12Ctx is an opaque
+ * handle, and every D3D12/DML/HLSL detail is confined to
+ * dnn_onnx_d3d12.c, the only translation unit in the tree that includes
+ * the D3D12 and DirectML headers.
+ */
+
+#ifndef AVFILTER_DNN_DNN_ONNX_D3D12_H
+#define AVFILTER_DNN_DNN_ONNX_D3D12_H
+
+#include "libavutil/frame.h"
+#include "../dnn_interface.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct DnnOnnxD3D12Ctx DnnOnnxD3D12Ctx;
+
+/**
+ * Check whether the D3D12/DirectML path is usable on d3d12_device, without
+ * creating any lasting state. Call from a filter's config_output() on
+ * every negotiation.
+ *
+ * @param d3d12_device an ID3D12Device*
+ * @param is_uma if non-NULL, set to 1 if the device reports a UMA
+ *               (integrated/APU-style) memory architecture via
+ *               D3D12_FEATURE_ARCHITECTURE1, 0 otherwise.
+ * @param log_ctx an AVClass-compatible pointer to use for av_log(), or NULL
+ * @return 0 if the path is usable, otherwise a negative AVERROR (e.g.
+ *         AVERROR(ENOSYS)).
+ */
+int ff_dnn_onnx_d3d12_probe(void *d3d12_device, int *is_uma, void *log_ctx);
+
+/**
+ * Create a helper context bound to d3d12_device: a DirectML device and a
+ * single dedicated DIRECT command queue;
+ * Both our own compute shader dispatches and the DirectML execution
+ * provider submit their work to this one queue, so the GPU simply runs
+ * everything in submission order -- no extra cross-queue fences are needed
+ * to keep the two in sync.
+ *
+ * @param d3d12_device an ID3D12Device*; the caller retains ownership and
+ *                      must keep it alive for the lifetime of the returned
+ *                      context.
+ * @param log_ctx an AVClass-compatible pointer to use for av_log(), or NULL
+ * @return a new context, or NULL on failure (logged via log_ctx).
+ */
+DnnOnnxD3D12Ctx *ff_dnn_onnx_d3d12_create(void *d3d12_device, void *log_ctx);
+
+/**
+ * Release a context created by ff_dnn_onnx_d3d12_create(), including any
+ * buffers configured via ff_dnn_onnx_d3d12_config(). Waits for the shared
+ * command queue to drain first. *ctx is set to NULL. No-op if *ctx is NULL.
+ */
+void ff_dnn_onnx_d3d12_free(DnnOnnxD3D12Ctx **ctx);
+
+/**
+ * Append the DirectML execution provider to session_options, built on top
+ * of ctx's DirectML device and shared command queue instead of a default
+ * device ORT would otherwise create internally. Must be called before the
+ * OrtSession is created from session_options.
+ *
+ * @param session_options an OrtSessionOptions*
+ * @return 0 on success, negative AVERROR on failure.
+ */
+int ff_dnn_onnx_d3d12_append_ep(DnnOnnxD3D12Ctx *ctx, void *session_options);
+
+/**
+ * Configure ctx's buffers for the given model input/output tensor.
+ *
+ * Current gate: only channels == 1, dt == DNN_FLOAT and
+ * layout == DL_NCHW are accepted; anything else fails with a clear
+ * AVERROR(ENOSYS) log line naming the unsupported combination rather than
+ * a bare AVERROR(EINVAL).
+ *
+ * @return 0 on success, negative AVERROR on failure.
+ */
+int ff_dnn_onnx_d3d12_config(DnnOnnxD3D12Ctx *ctx, int in_w, int in_h,
+                              int out_w, int out_h,
+                              DNNDataType dt, DNNLayout layout, int channels);
+
+/**
+ * Step 1/3 of the per-frame pipeline: extract the luma plane of the D3D12
+ * NV12 input frame into ctx's compact GPU tensor buffer (normalizing to
+ * [0,1] as it goes), waiting on the frame's decode fence first.
+ *
+ * @param in the input AVFrame; in->data[0] must be an AVD3D12VAFrame* and
+ *           in->hw_frames_ctx's device must be the same one ctx was created
+ *           with.
+ * @param in_tensor  if non-NULL, set to the opaque OrtValue* wrapping the
+ *                   input tensor for this frame.
+ * @param out_tensor if non-NULL, set to the opaque OrtValue* wrapping the
+ *                   output tensor for this frame.
+ * @return 0 on success, negative AVERROR on failure.
+ */
+int ff_dnn_onnx_d3d12_fill_input(DnnOnnxD3D12Ctx *ctx, const AVFrame *in,
+                                  void **in_tensor, void **out_tensor);
+
+/**
+ * Step 2/3 of the per-frame pipeline: run the model against the tensors
+ * produced by ff_dnn_onnx_d3d12_fill_input(), using IoBinding so that the
+ * DirectML EP consumes/produces the GPU tensors directly instead of
+ * copying the data out to host (CPU) memory and back.
+ *
+ * @param session an OrtSession* whose session_options had
+ *                 ff_dnn_onnx_d3d12_append_ep() applied.
+ * @param in_tensor/out_tensor the opaque OrtValue* pointers returned by
+ *                 ff_dnn_onnx_d3d12_fill_input().
+ * @return 0 on success, negative AVERROR on failure.
+ */
+int ff_dnn_onnx_d3d12_run(DnnOnnxD3D12Ctx *ctx, void *session,
+                           const char *in_name, const char *out_name,
+                           void *in_tensor, void *out_tensor);
+
+/**
+ * Step 3/3 of the per-frame pipeline: write ctx's compact GPU output tensor
+ * back into the luma plane of the D3D12 NV12 output frame (denormalizing
+ * from [0,1] with round-to-nearest, matching swscale's GRAYF32->GRAY8), and
+ * copy or upscale the chroma planes.
+ * Signals out's sync fence with the work submitted by this call and by the
+ * preceding fill_input()/run().
+ *
+ * @param out the output AVFrame; out->data[0] must be an AVD3D12VAFrame*.
+ * @return 0 on success, negative AVERROR on failure.
+ */
+int ff_dnn_onnx_d3d12_writeback(DnnOnnxD3D12Ctx *ctx, AVFrame *out);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* AVFILTER_DNN_DNN_ONNX_D3D12_H */
diff --git a/libavfilter/dnn_filter_common.c b/libavfilter/dnn_filter_common.c
index 392cca4d38..8b19f4db7d 100644
--- a/libavfilter/dnn_filter_common.c
+++ b/libavfilter/dnn_filter_common.c
@@ -22,6 +22,7 @@
 #include "libavutil/mem.h"
 #include "libavutil/opt.h"
 #include "libavutil/hwcontext.h"
+#include "libavutil/pixdesc.h"
 
 #define MAX_SUPPORTED_OUTPUTS_NB 4
 
@@ -189,6 +190,17 @@ int ff_dnn_get_output(DnnContext *ctx, int input_width, 
int input_height, int *o
                                   (const char *)output_name, output_width, 
output_height);
 }
 
+int ff_dnn_set_hw_frames_ctx(DnnContext *ctx, AVBufferRef *hw_frames_ctx)
+{
+    av_buffer_unref(&ctx->hw_frames_ctx);
+    if (hw_frames_ctx) {
+        ctx->hw_frames_ctx = av_buffer_ref(hw_frames_ctx);
+        if (!ctx->hw_frames_ctx)
+            return AVERROR(ENOMEM);
+    }
+    return 0;
+}
+
 int ff_dnn_execute_model(DnnContext *ctx, AVFrame *in_frame, AVFrame 
*out_frame)
 {
     DNNExecBaseParams exec_params = {
@@ -237,6 +249,7 @@ void ff_dnn_uninit(DnnContext *ctx)
 
         av_freep(&ctx->model_outputnames);
     }
+    av_buffer_unref(&ctx->hw_frames_ctx);
 }
 
 #if CONFIG_CUDA
@@ -279,3 +292,49 @@ int ff_dnn_zero_copy_supported_cuda(DnnContext *ctx, const 
AVFilterLink *inlink)
     return 0;
 }
 #endif
+
+#if CONFIG_DNN_ONNX_D3D12
+int ff_dnn_zero_copy_supported_d3d12(DnnContext *ctx, const AVFilterLink 
*inlink)
+{
+    AVBufferRef *hw_frames_ref;
+    AVHWFramesContext *hw_frames_ctx;
+
+    if (ctx->backend_type != DNN_ONNX) {
+        av_log(inlink->dst, AV_LOG_ERROR,
+               "D3D12 zero-copy requires the ONNX Runtime backend 
(dnn_backend=onnx).\n");
+        return AVERROR(EINVAL);
+    }
+
+    if (ctx->batch_size > 1) {
+        av_log(inlink->dst, AV_LOG_ERROR, "D3D12 zero-copy currently does not 
support batching.\n");
+        return AVERROR(EINVAL);
+    }
+
+    hw_frames_ref = avfilter_link_get_hw_frames_ctx((AVFilterLink *)inlink);
+    if (!hw_frames_ref) {
+        av_log(inlink->dst, AV_LOG_ERROR, "D3D12 input has no 
hw_frames_ctx.\n");
+        return AVERROR(EINVAL);
+    }
+    hw_frames_ctx = (AVHWFramesContext *)hw_frames_ref->data;
+
+    if (hw_frames_ctx->sw_format != AV_PIX_FMT_NV12) {
+        av_log(inlink->dst, AV_LOG_ERROR,
+               "D3D12 zero-copy currently only supports NV12 hardware frames, 
got %s.\n",
+               av_get_pix_fmt_name(hw_frames_ctx->sw_format));
+        av_buffer_unref(&hw_frames_ref);
+        return AVERROR(EINVAL);
+    }
+
+    av_buffer_unref(&hw_frames_ref);
+
+    if (!ctx->zero_copy_negotiated) {
+        av_log(inlink->dst, AV_LOG_ERROR,
+               "D3D12 zero-copy was not negotiated by the ONNX Runtime 
backend. "
+               "Check that device=dml is set and that this build's ONNX 
Runtime "
+               "provides a working DirectML execution provider.\n");
+        return AVERROR(ENOSYS);
+    }
+
+    return 0;
+}
+#endif
diff --git a/libavfilter/dnn_filter_common.h b/libavfilter/dnn_filter_common.h
index 1e89ab42ae..40fb87e864 100644
--- a/libavfilter/dnn_filter_common.h
+++ b/libavfilter/dnn_filter_common.h
@@ -57,6 +57,7 @@ int ff_dnn_set_detect_post_proc(DnnContext *ctx, 
DetectPostProc post_proc);
 int ff_dnn_set_classify_post_proc(DnnContext *ctx, ClassifyPostProc post_proc);
 int ff_dnn_get_input(DnnContext *ctx, DNNData *input);
 int ff_dnn_get_output(DnnContext *ctx, int input_width, int input_height, int 
*output_width, int *output_height);
+int ff_dnn_set_hw_frames_ctx(DnnContext *ctx, AVBufferRef *hw_frames_ctx);
 int ff_dnn_execute_model(DnnContext *ctx, AVFrame *in_frame, AVFrame 
*out_frame);
 int ff_dnn_execute_model_classification(DnnContext *ctx, AVFrame *in_frame, 
AVFrame *out_frame, const char *target);
 DNNAsyncStatusType ff_dnn_get_result(DnnContext *ctx, AVFrame **in_frame, 
AVFrame **out_frame);
@@ -65,5 +66,8 @@ void ff_dnn_uninit(DnnContext *ctx);
 #if CONFIG_CUDA
 int ff_dnn_zero_copy_supported_cuda(DnnContext *ctx, const AVFilterLink 
*inlink);
 #endif
+#if CONFIG_DNN_ONNX_D3D12
+int ff_dnn_zero_copy_supported_d3d12(DnnContext *ctx, const AVFilterLink 
*inlink);
+#endif
 
 #endif
diff --git a/libavfilter/dnn_interface.h b/libavfilter/dnn_interface.h
index 632ae6be84..29ba2870b2 100644
--- a/libavfilter/dnn_interface.h
+++ b/libavfilter/dnn_interface.h
@@ -168,6 +168,9 @@ typedef struct DnnContext {
     char *device;
     int device_id;
 
+    AVBufferRef *hw_frames_ctx;
+    int zero_copy_negotiated;
+
 #if CONFIG_LIBTENSORFLOW
     TFOptions tf_option;
 #endif
diff --git a/libavfilter/vf_dnn_processing.c b/libavfilter/vf_dnn_processing.c
index 16904a2c20..83aabaa185 100644
--- a/libavfilter/vf_dnn_processing.c
+++ b/libavfilter/vf_dnn_processing.c
@@ -28,6 +28,7 @@
 #include "libavutil/pixdesc.h"
 #include "libavutil/avassert.h"
 #include "libavutil/imgutils.h"
+#include "libavutil/hwcontext.h"
 #include "filters.h"
 #include "formats.h"
 #include "dnn_filter_common.h"
@@ -35,11 +36,16 @@
 #include "libswscale/swscale.h"
 #include "libavutil/time.h"
 
+#if CONFIG_DNN_ONNX_D3D12
+#include "libavutil/hwcontext_d3d12va.h"
+#endif
+
 typedef struct DnnProcessingContext {
     const AVClass *class;
     DnnContext dnnctx;
     struct SwsContext *sws_uv_scale;
     int sws_uv_height;
+    AVBufferRef *hw_frames_ctx_out;
 } DnnProcessingContext;
 
 #define OFFSET(x) offsetof(DnnProcessingContext, dnnctx.x)
@@ -65,8 +71,13 @@ AVFILTER_DNN_DEFINE_CLASS(dnn_processing, DNN_TF | DNN_OV | 
DNN_TH | DNN_ONNX);
 
 static av_cold int init(AVFilterContext *context)
 {
-    DnnProcessingContext *ctx = context->priv;
-    return ff_dnn_init(&ctx->dnnctx, DFT_PROCESS_FRAME, context);
+    /*
+     * Model loading is deferred to config_input(): the ONNX Runtime
+     * backend's D3D12/DirectML path must bind its execution provider to
+     * the input's D3D12 device before the ORT session is created, and
+     * hw_frames_ctx is not available yet here.
+     */
+    return 0;
 }
 
 static const enum AVPixelFormat pix_fmts[] = {
@@ -77,6 +88,9 @@ static const enum AVPixelFormat pix_fmts[] = {
     AV_PIX_FMT_NV12,
 #if CONFIG_CUDA
     AV_PIX_FMT_CUDA,
+#endif
+#if CONFIG_DNN_ONNX_D3D12
+    AV_PIX_FMT_D3D12,
 #endif
     AV_PIX_FMT_NONE
 };
@@ -143,6 +157,17 @@ static int check_modelinput_inlink(const DNNData 
*model_input, const AVFilterLin
         DnnProcessingContext *dnn_ctx = ctx->priv;
         return ff_dnn_zero_copy_supported_cuda(&dnn_ctx->dnnctx, inlink);
     }
+#endif
+#if CONFIG_DNN_ONNX_D3D12
+    case AV_PIX_FMT_D3D12:
+    {
+        DnnProcessingContext *dnn_ctx = ctx->priv;
+        if 
(model_input->dims[dnn_get_channel_idx_by_layout(model_input->layout)] != 1) {
+            LOG_FORMAT_CHANNEL_MISMATCH();
+            return AVERROR(EIO);
+        }
+        return ff_dnn_zero_copy_supported_d3d12(&dnn_ctx->dnnctx, inlink);
+    }
 #endif
     default:
         avpriv_report_missing_feature(ctx, "%s", av_get_pix_fmt_name(fmt));
@@ -160,6 +185,45 @@ static int config_input(AVFilterLink *inlink)
     DNNData model_input = { 0 };
     int check;
 
+#if CONFIG_DNN_ONNX_D3D12
+    if (inlink->format == AV_PIX_FMT_D3D12) {
+        FilterLink *inl = ff_filter_link(inlink);
+        AVHWFramesContext *frames_ctx;
+
+        if (!inl->hw_frames_ctx) {
+            av_log(context, AV_LOG_ERROR,
+                   "No hw_frames_ctx on the input link; D3D12 input requires "
+                   "D3D12VA hardware frames (e.g. via hwupload or a "
+                   "D3D12VA-accelerated decoder)\n");
+            return AVERROR(EINVAL);
+        }
+
+        frames_ctx = (AVHWFramesContext *)inl->hw_frames_ctx->data;
+        if (frames_ctx->sw_format != AV_PIX_FMT_NV12) {
+            av_log(context, AV_LOG_ERROR,
+                   "D3D12 zero-copy currently only supports NV12 hardware "
+                   "frames, got %s\n", 
av_get_pix_fmt_name(frames_ctx->sw_format));
+            return AVERROR(EINVAL);
+        }
+
+        result = ff_dnn_set_hw_frames_ctx(&ctx->dnnctx, inl->hw_frames_ctx);
+        if (result < 0)
+            return result;
+    }
+#endif
+
+    /*
+     * Deferred from init(): the DirectML EP must bind to
+     * ctx->dnnctx.hw_frames_ctx (set above for D3D12 input) before
+     * ff_dnn_init() creates the ORT session, but hw_frames_ctx isn't
+     * available until config_input() runs..
+     */
+    if (!ctx->dnnctx.model) {
+        result = ff_dnn_init(&ctx->dnnctx, DFT_PROCESS_FRAME, context);
+        if (result < 0)
+            return result;
+    }
+
     result = ff_dnn_get_input(&ctx->dnnctx, &model_input);
     if (result != 0) {
         av_log(ctx, AV_LOG_ERROR, "could not get input from the model\n");
@@ -212,12 +276,71 @@ static int prepare_uv_scale(AVFilterLink *outlink)
     return 0;
 }
 
+#if CONFIG_DNN_ONNX_D3D12
+static int config_output_d3d12(AVFilterLink *outlink)
+{
+    AVFilterContext *context = outlink->src;
+    DnnProcessingContext *ctx = context->priv;
+    AVFilterLink *inlink = context->inputs[0];
+    FilterLink *inl = ff_filter_link(inlink);
+    FilterLink *outl = ff_filter_link(outlink);
+    AVHWFramesContext *in_frames_ctx = (AVHWFramesContext 
*)inl->hw_frames_ctx->data;
+    AVHWFramesContext *frames_ctx;
+    AVD3D12VAFramesContext *frames_hwctx;
+    int ret;
+
+    if (!ctx->dnnctx.zero_copy_negotiated) {
+        av_log(context, AV_LOG_ERROR,
+               "Failed to negotiate the D3D12/DirectML zero-copy path for "
+               "this model; dnn_processing cannot fall back to host-memory "
+               "processing on hardware frames. Check that device=dml, that "
+               "the input is a D3D12VA hwframe, and that FFmpeg was built "
+               "with the DirectML-enabled ONNX Runtime (see dnn_onnx_d3d12 "
+               "in configure).\n");
+        return AVERROR(ENOSYS);
+    }
+
+    av_buffer_unref(&ctx->hw_frames_ctx_out);
+
+    ctx->hw_frames_ctx_out = av_hwframe_ctx_alloc(in_frames_ctx->device_ref);
+    if (!ctx->hw_frames_ctx_out)
+        return AVERROR(ENOMEM);
+
+    frames_ctx = (AVHWFramesContext *)ctx->hw_frames_ctx_out->data;
+    frames_ctx->format            = AV_PIX_FMT_D3D12;
+    frames_ctx->sw_format         = AV_PIX_FMT_NV12;
+    frames_ctx->width             = outlink->w;
+    frames_ctx->height            = outlink->h;
+    frames_ctx->initial_pool_size = 10;
+    if (context->extra_hw_frames > 0)
+        frames_ctx->initial_pool_size += context->extra_hw_frames;
+
+    frames_hwctx = frames_ctx->hwctx;
+    frames_hwctx->format = DXGI_FORMAT_NV12;
+
+    ret = av_hwframe_ctx_init(ctx->hw_frames_ctx_out);
+    if (ret < 0) {
+        av_buffer_unref(&ctx->hw_frames_ctx_out);
+        return ret;
+    }
+
+    av_buffer_unref(&outl->hw_frames_ctx);
+    outl->hw_frames_ctx = av_buffer_ref(ctx->hw_frames_ctx_out);
+    if (!outl->hw_frames_ctx)
+        return AVERROR(ENOMEM);
+
+    return 0;
+}
+#endif
+
 static int config_output(AVFilterLink *outlink)
 {
     AVFilterContext *context = outlink->src;
     DnnProcessingContext *ctx = context->priv;
     int result;
     AVFilterLink *inlink = context->inputs[0];
+    FilterLink *inl = ff_filter_link(inlink);
+    FilterLink *outl = ff_filter_link(outlink);
 
     // have a try run in case that the dnn model resize the frame
     result = ff_dnn_get_output(&ctx->dnnctx, inlink->w, inlink->h, 
&outlink->w, &outlink->h);
@@ -228,6 +351,17 @@ static int config_output(AVFilterLink *outlink)
 
     prepare_uv_scale(outlink);
 
+    if (inl->hw_frames_ctx) {
+#if CONFIG_DNN_ONNX_D3D12
+    /* D3D12 needs its own pool sized to the model's output. */
+    if (inlink->format == AV_PIX_FMT_D3D12)
+            return config_output_d3d12(outlink);
+#endif
+        outl->hw_frames_ctx = av_buffer_ref(inl->hw_frames_ctx);
+        if (!outl->hw_frames_ctx)
+            return AVERROR(ENOMEM);
+    }
+
     return 0;
 }
 
@@ -364,6 +498,7 @@ static av_cold void uninit(AVFilterContext *ctx)
 
     sws_freeContext(context->sws_uv_scale);
     ff_dnn_uninit(&context->dnnctx);
+    av_buffer_unref(&context->hw_frames_ctx_out);
 }
 
 static const AVFilterPad dnn_processing_inputs[] = {
@@ -394,4 +529,6 @@ const FFFilter ff_vf_dnn_processing = {
     FILTER_OUTPUTS(dnn_processing_outputs),
     FILTER_PIXFMTS_ARRAY(pix_fmts),
     .activate      = activate,
+    .p.flags       = AVFILTER_FLAG_HWDEVICE,
+    .flags_internal = FF_FILTER_FLAG_HWFRAME_AWARE,
 };
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to