diff mbox series

[FFmpeg-devel,v4,2/2] fftools/ffplay: add hwaccel decoding support

Message ID tencent_3475DE09054EBFA54F9306E856D3F6F36D05@qq.com
State New
Headers show
Series [FFmpeg-devel,v4,1/2] fftools/ffplay: add vulkan renderer via libplacebo | expand

Checks

Context Check Description
yinshiyou/make_loongarch64 success Make finished
yinshiyou/make_fate_loongarch64 success Make fate finished
andriy/make_x86 success Make finished
andriy/make_fate_x86 success Make fate finished

Commit Message

Zhao Zhili Oct. 29, 2023, 2:23 a.m. UTC
From: Zhao Zhili <zhilizhao@tencent.com>

---
v4: add more optional extensions
v3: shared vulkan instance between libplacebo and hwcontext

 fftools/ffplay.c          |  43 +++++
 fftools/ffplay_renderer.c | 393 ++++++++++++++++++++++++++++++++++++--
 fftools/ffplay_renderer.h |   2 +
 3 files changed, 418 insertions(+), 20 deletions(-)
diff mbox series

Patch

diff --git a/fftools/ffplay.c b/fftools/ffplay.c
index 305d72d8b8..29b37a5e46 100644
--- a/fftools/ffplay.c
+++ b/fftools/ffplay.c
@@ -352,6 +352,7 @@  static int autorotate = 1;
 static int find_stream_info = 1;
 static int filter_nbthreads = 0;
 static int enable_vulkan = 0;
+static const char *hwaccel = NULL;
 
 /* current context */
 static int is_full_screen;
@@ -2557,6 +2558,37 @@  static int audio_open(void *opaque, AVChannelLayout *wanted_channel_layout, int
     return spec.size;
 }
 
+static int create_hwaccel(AVBufferRef **device_ctx)
+{
+    enum AVHWDeviceType type;
+    int ret;
+    AVBufferRef *vk_dev;
+
+    *device_ctx = NULL;
+
+    if (!hwaccel)
+        return 0;
+
+    type = av_hwdevice_find_type_by_name(hwaccel);
+    if (type == AV_HWDEVICE_TYPE_NONE)
+        return AVERROR(ENOTSUP);
+
+    ret = vk_renderer_get_hw_dev(vk_renderer, &vk_dev);
+    if (ret < 0)
+        return ret;
+
+    ret = av_hwdevice_ctx_create_derived(device_ctx, type, vk_dev, 0);
+    if (!ret)
+        return 0;
+
+    if (ret != AVERROR(ENOSYS))
+        return ret;
+
+    av_log(NULL, AV_LOG_WARNING, "Derive %s from vulkan not supported.\n", hwaccel);
+    ret = av_hwdevice_ctx_create(device_ctx, type, NULL, NULL, 0);
+    return ret;
+}
+
 /* open a given stream. Return 0 if OK */
 static int stream_component_open(VideoState *is, int stream_index)
 {
@@ -2624,6 +2656,12 @@  static int stream_component_open(VideoState *is, int stream_index)
 
     av_dict_set(&opts, "flags", "+copy_opaque", AV_DICT_MULTIKEY);
 
+    if (avctx->codec_type == AVMEDIA_TYPE_VIDEO) {
+        ret = create_hwaccel(&avctx->hw_device_ctx);
+        if (ret < 0)
+            goto fail;
+    }
+
     if ((ret = avcodec_open2(avctx, codec, &opts)) < 0) {
         goto fail;
     }
@@ -3625,6 +3663,7 @@  static const OptionDef options[] = {
         "read and decode the streams to fill missing information with heuristics" },
     { "filter_threads", HAS_ARG | OPT_INT | OPT_EXPERT, { &filter_nbthreads }, "number of filter threads per graph" },
     { "enable_vulkan", OPT_BOOL, { &enable_vulkan }, "enable vulkan render" },
+    { "hwaccel", HAS_ARG | OPT_STRING | OPT_EXPERT, { &hwaccel }, "use HW accelerated decoding" },
     { NULL, },
 };
 
@@ -3739,6 +3778,10 @@  int main(int argc, char **argv)
 #ifdef SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR
         SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0");
 #endif
+        if (hwaccel && !enable_vulkan) {
+            av_log(NULL, AV_LOG_INFO, "Enable vulkan renderer to support hwaccel %s\n", hwaccel);
+            enable_vulkan = 1;
+        }
         if (enable_vulkan) {
             vk_renderer = vk_get_renderer();
             if (vk_renderer) {
diff --git a/fftools/ffplay_renderer.c b/fftools/ffplay_renderer.c
index 49e5516d5d..b898f69d26 100644
--- a/fftools/ffplay_renderer.c
+++ b/fftools/ffplay_renderer.c
@@ -16,6 +16,9 @@ 
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  */
 
+#define VK_NO_PROTOTYPES
+#define VK_ENABLE_BETA_EXTENSIONS
+
 #include "config.h"
 #include "ffplay_renderer.h"
 
@@ -35,6 +38,8 @@  struct VkRenderer {
 
     int (*create)(VkRenderer *renderer, SDL_Window *window);
 
+    int (*get_hw_dev)(VkRenderer *renderer, AVBufferRef **dev);
+
     int (*display)(VkRenderer *renderer, AVFrame *frame);
 
     int (*resize)(VkRenderer *renderer, int width, int height);
@@ -54,9 +59,18 @@  typedef struct RendererContext {
     pl_tex tex[4];
 
     pl_log vk_log;
+
+    AVBufferRef *hw_device_ref;
+    AVBufferRef *hw_frame_ref;
+    enum AVPixelFormat *transfer_formats;
+    AVHWFramesConstraints *constraints;
+
+    AVFrame *vk_frame;
 } RendererContext;
 
-static void vk_log_cb(void *log_priv, enum pl_log_level level, const char *msg) {
+static void vk_log_cb(void *log_priv, enum pl_log_level level,
+                      const char *msg)
+{
     static const int level_map[] = {
             AV_LOG_QUIET,
             AV_LOG_FATAL,
@@ -71,45 +85,77 @@  static void vk_log_cb(void *log_priv, enum pl_log_level level, const char *msg)
         av_log(log_priv, level_map[level], "%s\n", msg);
 }
 
+// Should keep sync with optional_device_exts inside hwcontext_vulkan.c
+static const char *optional_device_exts[] = {
+    /* Misc or required by other extensions */
+    VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME,
+    VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME,
+    VK_KHR_SAMPLER_YCBCR_CONVERSION_EXTENSION_NAME,
+    VK_EXT_DESCRIPTOR_BUFFER_EXTENSION_NAME,
+    VK_EXT_PHYSICAL_DEVICE_DRM_EXTENSION_NAME,
+    VK_EXT_SHADER_ATOMIC_FLOAT_EXTENSION_NAME,
+    VK_KHR_COOPERATIVE_MATRIX_EXTENSION_NAME,
+
+    /* Imports/exports */
+    VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME,
+    VK_EXT_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME,
+    VK_EXT_IMAGE_DRM_FORMAT_MODIFIER_EXTENSION_NAME,
+    VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME,
+    VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME,
+#ifdef _WIN32
+    VK_KHR_EXTERNAL_MEMORY_WIN32_EXTENSION_NAME,
+    VK_KHR_EXTERNAL_SEMAPHORE_WIN32_EXTENSION_NAME,
+#endif
+
+    /* Video encoding/decoding */
+    VK_KHR_VIDEO_QUEUE_EXTENSION_NAME,
+    VK_KHR_VIDEO_DECODE_QUEUE_EXTENSION_NAME,
+    VK_KHR_VIDEO_DECODE_H264_EXTENSION_NAME,
+    VK_KHR_VIDEO_DECODE_H265_EXTENSION_NAME,
+    "VK_MESA_video_decode_av1",
+};
+
 static int create(VkRenderer *renderer, SDL_Window *window)
 {
     int ret = 0;
-    unsigned ext_num = 0;
+    unsigned num_ext = 0;
     const char **ext = NULL;
     int w, h;
     struct pl_log_params vk_log_params = {
             .log_cb = vk_log_cb,
-            .log_level = PL_LOG_WARN,
+            .log_level = PL_LOG_DEBUG,
             .log_priv = renderer,
     };
-    RendererContext *ctx = (RendererContext *)renderer;
+    RendererContext *ctx = (RendererContext *) renderer;
 
-    if (!SDL_Vulkan_GetInstanceExtensions(window, &ext_num, NULL)) {
-        av_log(NULL, AV_LOG_FATAL, "Failed to get vulkan extensions: %s\n", SDL_GetError());
+    if (!SDL_Vulkan_GetInstanceExtensions(window, &num_ext, NULL)) {
+        av_log(NULL, AV_LOG_FATAL, "Failed to get vulkan extensions: %s\n",
+               SDL_GetError());
         return AVERROR_EXTERNAL;
     }
 
-    ext = av_calloc(ext_num, sizeof(*ext));
+    ext = av_calloc(num_ext, sizeof(*ext));
     if (!ext) {
         ret = AVERROR(ENOMEM);
         goto out;
     }
 
-    SDL_Vulkan_GetInstanceExtensions(window, &ext_num, ext);
+    SDL_Vulkan_GetInstanceExtensions(window, &num_ext, ext);
 
     ctx->vk_log = pl_log_create(PL_API_VER, &vk_log_params);
     ctx->vk_inst = pl_vk_inst_create(ctx->vk_log, pl_vk_inst_params(
             .get_proc_addr = SDL_Vulkan_GetVkGetInstanceProcAddr(),
             .debug = false,
             .extensions = ext,
-            .num_extensions = ext_num,
+            .num_extensions = num_ext
     ));
     if (!ctx->vk_inst) {
         ret = AVERROR_EXTERNAL;
         goto out;
     }
 
-    if (!SDL_Vulkan_CreateSurface(window, ctx->vk_inst->instance, &ctx->vk_surface)) {
+    if (!SDL_Vulkan_CreateSurface(window, ctx->vk_inst->instance,
+                                  &ctx->vk_surface)) {
         ret = AVERROR_EXTERNAL;
         goto out;
     }
@@ -119,15 +165,20 @@  static int create(VkRenderer *renderer, SDL_Window *window)
             .get_proc_addr = ctx->vk_inst->get_proc_addr,
             .surface = ctx->vk_surface,
             .allow_software = false,
+            .opt_extensions = optional_device_exts,
+            .num_opt_extensions = FF_ARRAY_ELEMS(optional_device_exts),
+            .extra_queues = VK_QUEUE_VIDEO_DECODE_BIT_KHR,
     ));
     if (!ctx->pl_vk) {
         ret = AVERROR_EXTERNAL;
         goto out;
     }
 
-    ctx->swapchain = pl_vulkan_create_swapchain(ctx->pl_vk,
-            pl_vulkan_swapchain_params(.surface = ctx->vk_surface,
-                                           .present_mode = VK_PRESENT_MODE_FIFO_KHR));
+    ctx->swapchain = pl_vulkan_create_swapchain(
+            ctx->pl_vk,
+            pl_vulkan_swapchain_params(
+                    .surface = ctx->vk_surface,
+                    .present_mode = VK_PRESENT_MODE_FIFO_KHR));
 
     SDL_Vulkan_GetDrawableSize(window, &w, &h);
     pl_swapchain_resize(ctx->swapchain, &w, &h);
@@ -137,21 +188,309 @@  static int create(VkRenderer *renderer, SDL_Window *window)
         ret = AVERROR_EXTERNAL;
         goto out;
     }
+
+    ctx->vk_frame = av_frame_alloc();
+    if (!ctx->vk_frame) {
+        ret = AVERROR(ENOMEM);
+        goto out;
+    }
+
     ret = 0;
 
-out:
+    out:
     av_free(ext);
     return ret;
 }
 
+static void lock_queue(struct AVHWDeviceContext *dev_ctx,
+                       uint32_t queue_family, uint32_t index)
+{
+    RendererContext *ctx = dev_ctx->user_opaque;
+    pl_vulkan vk = ctx->pl_vk;
+    vk->lock_queue(vk, queue_family, index);
+}
+
+static void unlock_queue(struct AVHWDeviceContext *dev_ctx,
+                         uint32_t queue_family,
+                         uint32_t index)
+{
+    RendererContext *ctx = dev_ctx->user_opaque;
+    pl_vulkan vk = ctx->pl_vk;
+    vk->unlock_queue(vk, queue_family, index);
+}
+
+static int get_decode_queue(VkRenderer *renderer, int *index, int *count)
+{
+    RendererContext *ctx = (RendererContext *) renderer;
+    VkQueueFamilyProperties *queue_family_prop = NULL;
+    uint32_t num_queue_family_prop = 0;
+    PFN_vkGetPhysicalDeviceQueueFamilyProperties get_queue_family_prop;
+    PFN_vkGetInstanceProcAddr get_proc_addr = ctx->vk_inst->get_proc_addr;
+
+    *index = -1;
+    *count = 0;
+    get_queue_family_prop = (PFN_vkGetPhysicalDeviceQueueFamilyProperties)
+            get_proc_addr(ctx->vk_inst->instance,
+                          "vkGetPhysicalDeviceQueueFamilyProperties");
+    get_queue_family_prop(ctx->pl_vk->phys_device,
+                          &num_queue_family_prop, NULL);
+    if (!num_queue_family_prop)
+        return AVERROR_EXTERNAL;
+
+    queue_family_prop = av_calloc(num_queue_family_prop,
+                                  sizeof(*queue_family_prop));
+    if (!queue_family_prop)
+        return AVERROR(ENOMEM);
+
+    get_queue_family_prop(ctx->pl_vk->phys_device,
+                          &num_queue_family_prop,
+                          queue_family_prop);
+
+    for (int i = 0; i < num_queue_family_prop; i++) {
+        if (queue_family_prop[i].queueFlags & VK_QUEUE_VIDEO_DECODE_BIT_KHR) {
+            *index = i;
+            *count = queue_family_prop[i].queueCount;
+            break;
+        }
+    }
+    av_free(queue_family_prop);
+
+    return 0;
+}
+
+static int get_hw_dev(VkRenderer *renderer, AVBufferRef **dev)
+{
+    RendererContext *ctx = (RendererContext *) renderer;
+    AVHWDeviceContext *device_ctx;
+    AVVulkanDeviceContext *vk_dev_ctx;
+    int decode_index;
+    int decode_count;
+    int ret;
+
+    *dev = NULL;
+    if (ctx->hw_device_ref) {
+        *dev = ctx->hw_device_ref;
+        return 0;
+    }
+
+    ctx->hw_device_ref = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VULKAN);
+    if (!ctx->hw_device_ref) {
+        return AVERROR(ENOMEM);
+    }
+
+    device_ctx = (AVHWDeviceContext *) ctx->hw_device_ref->data;
+    device_ctx->user_opaque = ctx;
+
+    vk_dev_ctx = device_ctx->hwctx;
+    vk_dev_ctx->lock_queue = lock_queue,
+            vk_dev_ctx->unlock_queue = unlock_queue;
+
+    vk_dev_ctx->get_proc_addr = ctx->vk_inst->get_proc_addr;
+
+    vk_dev_ctx->inst = ctx->vk_inst->instance;
+    vk_dev_ctx->phys_dev = ctx->pl_vk->phys_device;
+    vk_dev_ctx->act_dev = ctx->pl_vk->device;
+
+    vk_dev_ctx->device_features = *ctx->pl_vk->features;
+
+    vk_dev_ctx->enabled_inst_extensions = ctx->vk_inst->extensions;
+    vk_dev_ctx->nb_enabled_inst_extensions = ctx->vk_inst->num_extensions;
+
+    vk_dev_ctx->enabled_dev_extensions = ctx->pl_vk->extensions;
+    vk_dev_ctx->nb_enabled_dev_extensions = ctx->pl_vk->num_extensions;
+
+    vk_dev_ctx->queue_family_index = ctx->pl_vk->queue_graphics.index;
+    vk_dev_ctx->nb_graphics_queues = ctx->pl_vk->queue_graphics.count;
+
+    vk_dev_ctx->queue_family_tx_index = ctx->pl_vk->queue_transfer.index;
+    vk_dev_ctx->nb_tx_queues = ctx->pl_vk->queue_transfer.count;
+
+    vk_dev_ctx->queue_family_comp_index = ctx->pl_vk->queue_compute.index;
+    vk_dev_ctx->nb_comp_queues = ctx->pl_vk->queue_compute.count;
+
+    ret = get_decode_queue(renderer, &decode_index, &decode_count);
+    if (ret < 0)
+        return ret;
+
+    vk_dev_ctx->queue_family_decode_index = decode_index;
+    vk_dev_ctx->nb_decode_queues = decode_count;
+
+    ret = av_hwdevice_ctx_init(ctx->hw_device_ref);
+    if (ret < 0)
+        return ret;
+
+    *dev = ctx->hw_device_ref;
+    return 0;
+}
+
+static int create_hw_frame(VkRenderer *renderer, AVFrame *frame)
+{
+    RendererContext *ctx = (RendererContext *) renderer;
+    AVHWFramesContext *src_hw_frame = (AVHWFramesContext *)
+            frame->hw_frames_ctx->data;
+    AVHWFramesContext *hw_frame;
+    AVVulkanFramesContext *vk_frame_ctx;
+    int ret;
+
+    if (ctx->hw_frame_ref) {
+        hw_frame = (AVHWFramesContext *) ctx->hw_frame_ref->data;
+
+        if (hw_frame->width == frame->width &&
+            hw_frame->height == frame->height &&
+            hw_frame->sw_format == src_hw_frame->sw_format)
+            return 0;
+
+        av_buffer_unref(&ctx->hw_frame_ref);
+    }
+
+    if (!ctx->constraints) {
+        ctx->constraints = av_hwdevice_get_hwframe_constraints(
+                ctx->hw_device_ref, NULL);
+        if (!ctx->constraints)
+            return AVERROR(ENOMEM);
+    }
+
+    // Check constraints and skip create hwframe. Don't take it as error since
+    // we can fallback to memory copy from GPU to CPU.
+    if ((ctx->constraints->max_width &&
+         ctx->constraints->max_width < frame->width) ||
+        (ctx->constraints->max_height &&
+         ctx->constraints->max_height < frame->height) ||
+        (ctx->constraints->min_width &&
+         ctx->constraints->min_width > frame->width) ||
+        (ctx->constraints->min_height &&
+         ctx->constraints->min_height > frame->height))
+        return 0;
+
+    if (ctx->constraints->valid_sw_formats) {
+        enum AVPixelFormat *sw_formats = ctx->constraints->valid_sw_formats;
+        while (*sw_formats != AV_PIX_FMT_NONE) {
+            if (*sw_formats == src_hw_frame->sw_format)
+                break;
+            sw_formats++;
+        }
+        if (*sw_formats == AV_PIX_FMT_NONE)
+            return 0;
+    }
+
+    ctx->hw_frame_ref = av_hwframe_ctx_alloc(ctx->hw_device_ref);
+    if (!ctx->hw_frame_ref)
+        return AVERROR(ENOMEM);
+
+    hw_frame = (AVHWFramesContext *) ctx->hw_frame_ref->data;
+    hw_frame->format = AV_PIX_FMT_VULKAN;
+    hw_frame->sw_format = src_hw_frame->sw_format;
+    hw_frame->width = frame->width;
+    hw_frame->height = frame->height;
+
+    if (frame->format == AV_PIX_FMT_CUDA) {
+        vk_frame_ctx = hw_frame->hwctx;
+        vk_frame_ctx->flags = AV_VK_FRAME_FLAG_DISABLE_MULTIPLANE;
+    }
+
+    ret = av_hwframe_ctx_init(ctx->hw_frame_ref);
+    if (ret < 0) {
+        av_log(renderer, AV_LOG_ERROR, "Create hwframe context failed, %s\n",
+               av_err2str(ret));
+        return ret;
+    }
+
+    av_hwframe_transfer_get_formats(ctx->hw_frame_ref,
+                                    AV_HWFRAME_TRANSFER_DIRECTION_TO,
+                                    &ctx->transfer_formats, 0);
+
+    return 0;
+}
+
+static inline int check_hw_transfer(RendererContext *ctx, AVFrame *frame)
+{
+    if (!ctx->hw_frame_ref || !ctx->transfer_formats)
+        return 0;
+
+    for (int i = 0; ctx->transfer_formats[i] != AV_PIX_FMT_NONE; i++)
+        if (ctx->transfer_formats[i] == frame->format)
+            return 1;
+
+    return 0;
+}
+
+static int transfer_frame(VkRenderer *renderer, AVFrame *frame)
+{
+    RendererContext *ctx = (RendererContext *) renderer;
+    int ret;
+
+    if (!frame->hw_frames_ctx)
+        return 0;
+
+    if (frame->format == AV_PIX_FMT_VULKAN)
+        return 0;
+
+    ret = create_hw_frame(renderer, frame);
+    if (ret < 0)
+        return ret;
+
+    // Try map data first
+    av_frame_unref(ctx->vk_frame);
+    if (ctx->hw_frame_ref) {
+        ctx->vk_frame->hw_frames_ctx = av_buffer_ref(ctx->hw_frame_ref);
+        ctx->vk_frame->format = AV_PIX_FMT_VULKAN;
+    }
+    ret = av_hwframe_map(ctx->vk_frame, frame, AV_HWFRAME_MAP_READ);
+    if (!ret)
+        goto out;
+
+    if (ret != AVERROR(ENOSYS)) {
+        av_log(NULL, AV_LOG_FATAL, "Map data to vulkan failed: %s\n",
+               av_err2str(ret));
+        return ret;
+    }
+
+    // Try transfer to vulkan
+    if (check_hw_transfer(ctx, frame)) {
+        av_frame_unref(ctx->vk_frame);
+        av_hwframe_get_buffer(ctx->hw_frame_ref, ctx->vk_frame, 0);
+        ret = av_hwframe_transfer_data(ctx->vk_frame, frame, 1);
+        if (!ret)
+            goto out;
+
+        if (ret < 0 && ret != AVERROR(ENOSYS)) {
+            av_log(NULL, AV_LOG_FATAL, "Transfer data to vulkan failed: %s\n",
+                   av_err2str(ret));
+            return ret;
+        }
+    }
+
+    // Transfer to CPU
+    av_frame_unref(ctx->vk_frame);
+    ret = av_hwframe_transfer_data(ctx->vk_frame, frame, 0);
+    if (ret < 0) {
+        av_log(NULL, AV_LOG_FATAL, "Transfer data to CPU failed: %s\n",
+               av_err2str(ret));
+        return ret;
+    }
+
+    out:
+    ret = av_frame_copy_props(ctx->vk_frame, frame);
+    if (ret < 0)
+        return ret;
+    av_frame_unref(frame);
+    av_frame_move_ref(frame, ctx->vk_frame);
+
+    return 0;
+}
+
 static int display(VkRenderer *renderer, AVFrame *frame)
 {
     struct pl_swapchain_frame swap_frame = {0};
     struct pl_frame pl_frame = {0};
     struct pl_frame target = {0};
-    RendererContext *ctx = (RendererContext *)renderer;
+    RendererContext *ctx = (RendererContext *) renderer;
     int ret = 0;
 
+    ret = transfer_frame(renderer, frame);
+    if (ret < 0)
+        return ret;
+
     if (!pl_map_avframe_ex(ctx->pl_vk->gpu, &pl_frame, pl_avframe_params(
             .frame = frame,
             .tex = ctx->tex))) {
@@ -166,7 +505,8 @@  static int display(VkRenderer *renderer, AVFrame *frame)
     }
 
     pl_frame_from_swapchain(&target, &swap_frame);
-    if (!pl_render_image(ctx->renderer, &pl_frame, &target, &pl_render_default_params)) {
+    if (!pl_render_image(ctx->renderer, &pl_frame, &target,
+                         &pl_render_default_params)) {
         av_log(NULL, AV_LOG_ERROR, "pl_render_image failed\n");
         ret = AVERROR_EXTERNAL;
         goto out;
@@ -179,14 +519,14 @@  static int display(VkRenderer *renderer, AVFrame *frame)
     }
     pl_swapchain_swap_buffers(ctx->swapchain);
 
-out:
+    out:
     pl_unmap_avframe(ctx->pl_vk->gpu, &pl_frame);
     return ret;
 }
 
 static int resize(VkRenderer *renderer, int width, int height)
 {
-    RendererContext *ctx = (RendererContext *)renderer;
+    RendererContext *ctx = (RendererContext *) renderer;
 
     if (!pl_swapchain_resize(ctx->swapchain, &width, &height))
         return AVERROR_EXTERNAL;
@@ -196,7 +536,13 @@  static int resize(VkRenderer *renderer, int width, int height)
 static void destroy(VkRenderer *renderer)
 {
     PFN_vkDestroySurfaceKHR vkDestroySurfaceKHR;
-    RendererContext *ctx = (RendererContext *)renderer;
+    RendererContext *ctx = (RendererContext *) renderer;
+
+    av_freep(&ctx->transfer_formats);
+    av_hwframe_constraints_free(&ctx->constraints);
+    av_buffer_unref(&ctx->hw_frame_ref);
+    av_buffer_unref(&ctx->hw_device_ref);
+    av_frame_free(&ctx->vk_frame);
 
     if (ctx->pl_vk) {
         for (int i = 0; i < FF_ARRAY_ELEMS(ctx->tex); i++)
@@ -208,7 +554,8 @@  static void destroy(VkRenderer *renderer)
 
     if (ctx->vk_surface) {
         vkDestroySurfaceKHR = (PFN_vkDestroySurfaceKHR)
-                ctx->vk_inst->get_proc_addr(ctx->vk_inst->instance, "vkDestroySurfaceKHR");
+                ctx->vk_inst->get_proc_addr(ctx->vk_inst->instance,
+                                            "vkDestroySurfaceKHR");
         vkDestroySurfaceKHR(ctx->vk_inst->instance, ctx->vk_surface, NULL);
         ctx->vk_surface = NULL;
     }
@@ -233,6 +580,7 @@  VkRenderer *vk_get_renderer(void)
 
     renderer = &ctx->api;
     renderer->class = &vulkan_renderer_class;
+    renderer->get_hw_dev = get_hw_dev;
     renderer->create = create;
     renderer->display = display;
     renderer->resize = resize;
@@ -255,6 +603,11 @@  int vk_renderer_create(VkRenderer *renderer, SDL_Window *window)
     return renderer->create(renderer, window);
 }
 
+int vk_renderer_get_hw_dev(VkRenderer *renderer, AVBufferRef **dev)
+{
+    return renderer->get_hw_dev(renderer, dev);
+}
+
 int vk_renderer_display(VkRenderer *renderer, AVFrame *frame)
 {
     return renderer->display(renderer, frame);
diff --git a/fftools/ffplay_renderer.h b/fftools/ffplay_renderer.h
index 4029f154a9..b982ba0192 100644
--- a/fftools/ffplay_renderer.h
+++ b/fftools/ffplay_renderer.h
@@ -29,6 +29,8 @@  VkRenderer *vk_get_renderer(void);
 
 int vk_renderer_create(VkRenderer *renderer, SDL_Window *window);
 
+int vk_renderer_get_hw_dev(VkRenderer *renderer, AVBufferRef **dev);
+
 int vk_renderer_display(VkRenderer *renderer, AVFrame *frame);
 
 int vk_renderer_resize(VkRenderer *renderer, int width, int height);