diff mbox series

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

Message ID tencent_CC429311685FF3F0136C95C189166EA0E505@qq.com
State New
Headers show
Series [FFmpeg-devel,v5] fftools/ffplay: add hwaccel decoding support | 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. 30, 2023, 4:04 p.m. UTC
From: Zhao Zhili <zhilizhao@tencent.com>

Add vulkan renderer via libplacebo.

Simple usage:
$ ffplay -hwaccel vulkan foo.mp4

Use cuda to vulkan map:
$ ffplay -hwaccel cuda foo.mp4

Create vulkan instance by libplacebo, and enable debug:
$ ffplay -hwaccel vulkan \
	-vulkan_params create_by_placebo=1:debug=1 foo.mp4
---
v5:
    1. add vulkan_params option.
    2. vulkan instance can be create by hwcontext or libplacebo.

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

 configure                 |   2 +-
 doc/ffplay.texi           |  12 +
 fftools/Makefile          |   2 +
 fftools/ffplay.c          |  96 ++++-
 fftools/ffplay_renderer.c | 809 ++++++++++++++++++++++++++++++++++++++
 fftools/ffplay_renderer.h |  41 ++
 6 files changed, 956 insertions(+), 6 deletions(-)
 create mode 100644 fftools/ffplay_renderer.c
 create mode 100644 fftools/ffplay_renderer.h

Comments

Lynne Oct. 30, 2023, 5:05 p.m. UTC | #1
Oct 30, 2023, 17:05 by quinkblack@foxmail.com:

> From: Zhao Zhili <zhilizhao@tencent.com>
>
> Add vulkan renderer via libplacebo.
>
> Simple usage:
> $ ffplay -hwaccel vulkan foo.mp4
>
> Use cuda to vulkan map:
> $ ffplay -hwaccel cuda foo.mp4
>
> Create vulkan instance by libplacebo, and enable debug:
> $ ffplay -hwaccel vulkan \
>  -vulkan_params create_by_placebo=1:debug=1 foo.mp4
> ---
> v5:
>  1. add vulkan_params option.
>  2. vulkan instance can be create by hwcontext or libplacebo.
>
> v4: add more optional extensions
> v3: shared vulkan instance between libplacebo and hwcontext
>

You did it the other way. Instead of creating a device through libplacebo,
just create a Vulkan device via the hwcontext, and use it for libplacebo.
Zhao Zhili Oct. 30, 2023, 5:29 p.m. UTC | #2
On 2023/10/31 00:04, Zhao Zhili wrote:
> From: Zhao Zhili <zhilizhao@tencent.com>
>
> Add vulkan renderer via libplacebo.
>
> Simple usage:
> $ ffplay -hwaccel vulkan foo.mp4
>
> Use cuda to vulkan map:
> $ ffplay -hwaccel cuda foo.mp4
>
> Create vulkan instance by libplacebo, and enable debug:
> $ ffplay -hwaccel vulkan \
> 	-vulkan_params create_by_placebo=1:debug=1 foo.mp4

Some words about the patch.

I work on this in my spare time (weekend and midnight) for fun, like 
most of my

patches in FFmpeg and VLC. I learned a lot in this process from hwcontext_*,

libplacebo, and mpv. I'm not meant to reinvent the wheel and duplicate the

work of mpv or VLC, but enhance a simple test tool.

I do my best to do the test. I have tested on

1. Linux with cuda and vulkan decoder, and the slow path with vdpau

2. Windows with cuda, vulkan, d3d11va (copy to CPU)

3. MacOS with videotoolbox (map to CPU). Depends on another avutil patch

https://patchwork.ffmpeg.org/project/ffmpeg/patch/tencent_E37C7AAC8A39CDC271B63069AE01F9692A0A@qq.com/

Or let libplacebo create the vulkan instance.

I won't be surprised if it doesn't work on some hardware/software 
environment.

Please review and test the patch if you are interested on it.
Zhao Zhili Oct. 30, 2023, 5:35 p.m. UTC | #3
On 2023/10/31 01:05, Lynne wrote:
> Oct 30, 2023, 17:05 by quinkblack@foxmail.com:
>
>> From: Zhao Zhili <zhilizhao@tencent.com>
>>
>> Add vulkan renderer via libplacebo.
>>
>> Simple usage:
>> $ ffplay -hwaccel vulkan foo.mp4
>>
>> Use cuda to vulkan map:
>> $ ffplay -hwaccel cuda foo.mp4
>>
>> Create vulkan instance by libplacebo, and enable debug:
>> $ ffplay -hwaccel vulkan \
>>   -vulkan_params create_by_placebo=1:debug=1 foo.mp4
>> ---
>> v5:
>>   1. add vulkan_params option.
>>   2. vulkan instance can be create by hwcontext or libplacebo.
>>
>> v4: add more optional extensions
>> v3: shared vulkan instance between libplacebo and hwcontext
>>
> You did it the other way. Instead of creating a device through libplacebo,
> just create a Vulkan device via the hwcontext, and use it for libplacebo.

I have done both:

1. create vulkan device by hwcontext and import to placebo

2. create vulkan device by placebo and pass to hwcontext

It's controlled by -vulkan_params create_by_placebo=1(0). Default is the 
first behavior.

Did I miss something?

> _______________________________________________
> ffmpeg-devel mailing list
> ffmpeg-devel@ffmpeg.org
> https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
>
> To unsubscribe, visit link above, or email
> ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".
Lynne Oct. 31, 2023, 5:16 a.m. UTC | #4
Oct 30, 2023, 18:35 by quinkblack@foxmail.com:

>
> On 2023/10/31 01:05, Lynne wrote:
>
>> Oct 30, 2023, 17:05 by quinkblack@foxmail.com:
>>
>>> From: Zhao Zhili <zhilizhao@tencent.com>
>>>
>>> Add vulkan renderer via libplacebo.
>>>
>>> Simple usage:
>>> $ ffplay -hwaccel vulkan foo.mp4
>>>
>>> Use cuda to vulkan map:
>>> $ ffplay -hwaccel cuda foo.mp4
>>>
>>> Create vulkan instance by libplacebo, and enable debug:
>>> $ ffplay -hwaccel vulkan \
>>>  -vulkan_params create_by_placebo=1:debug=1 foo.mp4
>>> ---
>>> v5:
>>>  1. add vulkan_params option.
>>>  2. vulkan instance can be create by hwcontext or libplacebo.
>>>
>>> v4: add more optional extensions
>>> v3: shared vulkan instance between libplacebo and hwcontext
>>>
>> You did it the other way. Instead of creating a device through libplacebo,
>> just create a Vulkan device via the hwcontext, and use it for libplacebo.
>>
>
> I have done both:
>
> 1. create vulkan device by hwcontext and import to placebo
>
> 2. create vulkan device by placebo and pass to hwcontext
>
> It's controlled by -vulkan_params create_by_placebo=1(0). Default is the first behavior.
>
> Did I miss something?
>

Thanks, I overlooked that. Is there a reason for having a setting, and having that as the default?
Zhao Zhili Oct. 31, 2023, 8:40 a.m. UTC | #5
> On Oct 31, 2023, at 13:16, Lynne <dev@lynne.ee> wrote:
> 
> Oct 30, 2023, 18:35 by quinkblack@foxmail.com:
> 
>> 
>> On 2023/10/31 01:05, Lynne wrote:
>> 
>>> Oct 30, 2023, 17:05 by quinkblack@foxmail.com:
>>> 
>>>> From: Zhao Zhili <zhilizhao@tencent.com>
>>>> 
>>>> Add vulkan renderer via libplacebo.
>>>> 
>>>> Simple usage:
>>>> $ ffplay -hwaccel vulkan foo.mp4
>>>> 
>>>> Use cuda to vulkan map:
>>>> $ ffplay -hwaccel cuda foo.mp4
>>>> 
>>>> Create vulkan instance by libplacebo, and enable debug:
>>>> $ ffplay -hwaccel vulkan \
>>>> -vulkan_params create_by_placebo=1:debug=1 foo.mp4
>>>> ---
>>>> v5:
>>>> 1. add vulkan_params option.
>>>> 2. vulkan instance can be create by hwcontext or libplacebo.
>>>> 
>>>> v4: add more optional extensions
>>>> v3: shared vulkan instance between libplacebo and hwcontext
>>>> 
>>> You did it the other way. Instead of creating a device through libplacebo,
>>> just create a Vulkan device via the hwcontext, and use it for libplacebo.
>>> 
>> 
>> I have done both:
>> 
>> 1. create vulkan device by hwcontext and import to placebo
>> 
>> 2. create vulkan device by placebo and pass to hwcontext
>> 
>> It's controlled by -vulkan_params create_by_placebo=1(0). Default is the first behavior.
>> 
>> Did I miss something?
>> 
> 
> Thanks, I overlooked that. Is there a reason for having a setting, and having that as the default?

There is no particular reason for which one be the default. Let hwcontext create vulkan
device is easy, but the code path has been tested by fftools/ffmpeg. The second choice
covers another code path (av_hwdevice_ctx_alloc + av_hwdevice_ctx_init) and helps
finding bugs.

The macOS compatibility issue was found by the first choice. The memleak issue was found by
the second.

> _______________________________________________
> ffmpeg-devel mailing list
> ffmpeg-devel@ffmpeg.org
> https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
> 
> To unsubscribe, visit link above, or email
> ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".
Zhao Zhili Oct. 31, 2023, 9:51 a.m. UTC | #6
> 在 2023年10月31日,下午4:40,Zhao Zhili <quinkblack@foxmail.com> 写道:
> 
> 
>> 
>> On Oct 31, 2023, at 13:16, Lynne <dev@lynne.ee> wrote:
>> 
>> Oct 30, 2023, 18:35 by quinkblack@foxmail.com:
>> 
>>> 
>>>> On 2023/10/31 01:05, Lynne wrote:
>>> 
>>>> Oct 30, 2023, 17:05 by quinkblack@foxmail.com:
>>>> 
>>>>> From: Zhao Zhili <zhilizhao@tencent.com>
>>>>> 
>>>>> Add vulkan renderer via libplacebo.
>>>>> 
>>>>> Simple usage:
>>>>> $ ffplay -hwaccel vulkan foo.mp4
>>>>> 
>>>>> Use cuda to vulkan map:
>>>>> $ ffplay -hwaccel cuda foo.mp4
>>>>> 
>>>>> Create vulkan instance by libplacebo, and enable debug:
>>>>> $ ffplay -hwaccel vulkan \
>>>>> -vulkan_params create_by_placebo=1:debug=1 foo.mp4
>>>>> ---
>>>>> v5:
>>>>> 1. add vulkan_params option.
>>>>> 2. vulkan instance can be create by hwcontext or libplacebo.
>>>>> 
>>>>> v4: add more optional extensions
>>>>> v3: shared vulkan instance between libplacebo and hwcontext
>>>>> 
>>>> You did it the other way. Instead of creating a device through libplacebo,
>>>> just create a Vulkan device via the hwcontext, and use it for libplacebo.
>>>> 
>>> 
>>> I have done both:
>>> 
>>> 1. create vulkan device by hwcontext and import to placebo
>>> 
>>> 2. create vulkan device by placebo and pass to hwcontext
>>> 
>>> It's controlled by -vulkan_params create_by_placebo=1(0). Default is the first behavior.

I mean default is create_by_placebo=0.

>>> 
>>> Did I miss something?
>>> 
>> 
>> Thanks, I overlooked that. Is there a reason for having a setting, and having that as the default?
> 
> There is no particular reason for which one be the default. Let hwcontext create vulkan
> device is easy, but the code path has been tested by fftools/ffmpeg. The second choice
> covers another code path (av_hwdevice_ctx_alloc + av_hwdevice_ctx_init) and helps
> finding bugs.
> 
> The macOS compatibility issue was found by the first choice. The memleak issue was found by
> the second.
> 
>> _______________________________________________
>> ffmpeg-devel mailing list
>> ffmpeg-devel@ffmpeg.org
>> https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
>> 
>> To unsubscribe, visit link above, or email
>> ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".
> 
> _______________________________________________
> ffmpeg-devel mailing list
> ffmpeg-devel@ffmpeg.org
> https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
> 
> To unsubscribe, visit link above, or email
> ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".
diff mbox series

Patch

diff --git a/configure b/configure
index 1f0b9497cb..e7628f870a 100755
--- a/configure
+++ b/configure
@@ -3903,7 +3903,7 @@  ffmpeg_select="aformat_filter anull_filter atrim_filter format_filter
 ffmpeg_suggest="ole32 psapi shell32"
 ffplay_deps="avcodec avformat avfilter swscale swresample sdl2"
 ffplay_select="crop_filter transpose_filter hflip_filter vflip_filter rotate_filter"
-ffplay_suggest="shell32"
+ffplay_suggest="shell32 libplacebo vulkan"
 ffprobe_deps="avcodec avformat"
 ffprobe_suggest="shell32"
 
diff --git a/doc/ffplay.texi b/doc/ffplay.texi
index 5dd860b846..93f77eeece 100644
--- a/doc/ffplay.texi
+++ b/doc/ffplay.texi
@@ -196,6 +196,18 @@  will produce a thread pool with this many threads available for parallel
 processing. The default is 0 which means that the thread count will be
 determined by the number of available CPUs.
 
+@item -enable_vulkan
+Use vulkan renderer rather than SDL builtin renderer. Depends on libplacebo.
+
+@item -vulkan_params
+
+Vulkan configuration using a list of @var{key}=@var{value} pairs separated by
+":".
+
+@item -hwaccel
+Use HW accelerated decoding. Enable this option will enable vulkan renderer
+automatically.
+
 @end table
 
 @section While playing
diff --git a/fftools/Makefile b/fftools/Makefile
index 56820e6bc8..3c763e3db9 100644
--- a/fftools/Makefile
+++ b/fftools/Makefile
@@ -22,6 +22,8 @@  OBJS-ffmpeg +=                  \
     fftools/sync_queue.o        \
     fftools/thread_queue.o      \
 
+OBJS-ffplay += fftools/ffplay_renderer.o
+
 define DOFFTOOL
 OBJS-$(1) += fftools/cmdutils.o fftools/opt_common.o fftools/$(1).o $(OBJS-$(1)-yes)
 ifdef HAVE_GNU_WINDRES
diff --git a/fftools/ffplay.c b/fftools/ffplay.c
index d8c69e10bc..873ee8cc74 100644
--- a/fftools/ffplay.c
+++ b/fftools/ffplay.c
@@ -58,6 +58,7 @@ 
 #include <SDL_thread.h>
 
 #include "cmdutils.h"
+#include "ffplay_renderer.h"
 #include "opt_common.h"
 
 const char program_name[] = "ffplay";
@@ -350,6 +351,9 @@  static char *afilters = NULL;
 static int autorotate = 1;
 static int find_stream_info = 1;
 static int filter_nbthreads = 0;
+static int enable_vulkan = 0;
+static char *vulkan_params = NULL;
+static const char *hwaccel = NULL;
 
 /* current context */
 static int is_full_screen;
@@ -362,6 +366,8 @@  static SDL_Renderer *renderer;
 static SDL_RendererInfo renderer_info = {0};
 static SDL_AudioDeviceID audio_dev;
 
+static VkRenderer *vk_renderer;
+
 static const struct TextureFormatEntry {
     enum AVPixelFormat format;
     int texture_fmt;
@@ -954,6 +960,11 @@  static void video_image_display(VideoState *is)
     SDL_Rect rect;
 
     vp = frame_queue_peek_last(&is->pictq);
+    if (vk_renderer) {
+        vk_renderer_display(vk_renderer, vp->frame);
+        return;
+    }
+
     if (is->subtitle_st) {
         if (frame_queue_nb_remaining(&is->subpq) > 0) {
             sp = frame_queue_peek(&is->subpq);
@@ -1289,6 +1300,8 @@  static void do_exit(VideoState *is)
     }
     if (renderer)
         SDL_DestroyRenderer(renderer);
+    if (vk_renderer)
+        vk_renderer_destroy(vk_renderer);
     if (window)
         SDL_DestroyWindow(window);
     uninit_opts();
@@ -2546,6 +2559,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)
 {
@@ -2613,6 +2657,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;
     }
@@ -3447,6 +3497,8 @@  static void event_loop(VideoState *cur_stream)
                         SDL_DestroyTexture(cur_stream->vis_texture);
                         cur_stream->vis_texture = NULL;
                     }
+                    if (vk_renderer)
+                        vk_renderer_resize(vk_renderer, screen_width, screen_height);
                 case SDL_WINDOWEVENT_EXPOSED:
                     cur_stream->force_refresh = 1;
             }
@@ -3611,6 +3663,9 @@  static const OptionDef options[] = {
     { "find_stream_info", OPT_BOOL | OPT_INPUT | OPT_EXPERT, { &find_stream_info },
         "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 renderer" },
+    { "vulkan_params", HAS_ARG | OPT_STRING | OPT_EXPERT, { &vulkan_params }, "vulkan configuration using a list of key=value pairs separated by ':'" },
+    { "hwaccel", HAS_ARG | OPT_STRING | OPT_EXPERT, { &hwaccel }, "use HW accelerated decoding" },
     { NULL, },
 };
 
@@ -3725,9 +3780,40 @@  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) {
+#if SDL_VERSION_ATLEAST(2, 0, 6)
+                flags |= SDL_WINDOW_VULKAN;
+#endif
+            } else {
+                av_log(NULL, AV_LOG_WARNING, "Doesn't support vulkan renderer, fallback to SDL renderer\n");
+                enable_vulkan = 0;
+            }
+        }
         window = SDL_CreateWindow(program_name, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, default_width, default_height, flags);
         SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear");
-        if (window) {
+        if (!window) {
+            av_log(NULL, AV_LOG_FATAL, "Failed to create window: %s", SDL_GetError());
+            do_exit(NULL);
+        }
+
+        if (vk_renderer) {
+            AVDictionary *dict = NULL;
+
+            if (vulkan_params)
+                av_dict_parse_string(&dict, vulkan_params, "=", ":", 0);
+            ret = vk_renderer_create(vk_renderer, window, dict);
+            av_dict_free(&dict);
+            if (ret < 0) {
+                av_log(NULL, AV_LOG_FATAL, "Failed to create vulkan renderer, %s\n", av_err2str(ret));
+                do_exit(NULL);
+            }
+        } else {
             renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
             if (!renderer) {
                 av_log(NULL, AV_LOG_WARNING, "Failed to initialize a hardware accelerated renderer: %s\n", SDL_GetError());
@@ -3737,10 +3823,10 @@  int main(int argc, char **argv)
                 if (!SDL_GetRendererInfo(renderer, &renderer_info))
                     av_log(NULL, AV_LOG_VERBOSE, "Initialized %s renderer.\n", renderer_info.name);
             }
-        }
-        if (!window || !renderer || !renderer_info.num_texture_formats) {
-            av_log(NULL, AV_LOG_FATAL, "Failed to create window or renderer: %s", SDL_GetError());
-            do_exit(NULL);
+            if (!renderer || !renderer_info.num_texture_formats) {
+                av_log(NULL, AV_LOG_FATAL, "Failed to create window or renderer: %s", SDL_GetError());
+                do_exit(NULL);
+            }
         }
     }
 
diff --git a/fftools/ffplay_renderer.c b/fftools/ffplay_renderer.c
new file mode 100644
index 0000000000..f25796133e
--- /dev/null
+++ b/fftools/ffplay_renderer.c
@@ -0,0 +1,809 @@ 
+/*
+ * 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 VK_NO_PROTOTYPES
+#define VK_ENABLE_BETA_EXTENSIONS
+
+#include "config.h"
+#include "ffplay_renderer.h"
+
+#define HAVE_VULKAN_RENDERER (SDL_VERSION_ATLEAST(2, 0, 6) && CONFIG_LIBPLACEBO)
+
+#if HAVE_VULKAN_RENDERER
+
+#if defined(_WIN32) && !defined(VK_USE_PLATFORM_WIN32_KHR)
+#define VK_USE_PLATFORM_WIN32_KHR
+#endif
+
+#include <libplacebo/vulkan.h>
+#include <libplacebo/utils/frame_queue.h>
+#include <libplacebo/utils/libav.h>
+#include <SDL_vulkan.h>
+
+#include "libavutil/bprint.h"
+
+#endif
+
+struct VkRenderer {
+    const AVClass *class;
+
+    int (*create)(VkRenderer *renderer, SDL_Window *window, AVDictionary *dict);
+
+    int (*get_hw_dev)(VkRenderer *renderer, AVBufferRef **dev);
+
+    int (*display)(VkRenderer *renderer, AVFrame *frame);
+
+    int (*resize)(VkRenderer *renderer, int width, int height);
+
+    void (*destroy)(VkRenderer *renderer);
+};
+
+#if HAVE_VULKAN_RENDERER
+
+typedef struct RendererContext {
+    VkRenderer api;
+
+    // Can be NULL when vulkan instance is created by avutil
+    pl_vk_inst placebo_instance;
+    pl_vulkan placebo_vulkan;
+    pl_swapchain swapchain;
+    VkSurfaceKHR vk_surface;
+    pl_renderer renderer;
+    pl_tex tex[4];
+
+    pl_log vk_log;
+
+    AVBufferRef *hw_device_ref;
+    AVBufferRef *hw_frame_ref;
+    enum AVPixelFormat *transfer_formats;
+    AVHWFramesConstraints *constraints;
+
+    PFN_vkGetInstanceProcAddr get_proc_addr;
+    // This field is a copy from pl_vk_inst->instance or hw_device_ref instance.
+    VkInstance inst;
+
+    AVFrame *vk_frame;
+} RendererContext;
+
+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,
+            AV_LOG_ERROR,
+            AV_LOG_WARNING,
+            AV_LOG_INFO,
+            AV_LOG_DEBUG,
+            AV_LOG_TRACE,
+    };
+
+    if (level > 0 && level < FF_ARRAY_ELEMS(level_map))
+        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 inline int enable_debug(AVDictionary *opt)
+{
+    AVDictionaryEntry *entry = av_dict_get(opt, "debug", NULL, 0);
+    int debug = entry && strtol(entry->value, NULL, 10);
+    return debug;
+}
+
+static void hwctx_lock_queue(void *priv, uint32_t qf, uint32_t qidx)
+{
+    AVHWDeviceContext *avhwctx = priv;
+    const AVVulkanDeviceContext *hwctx = avhwctx->hwctx;
+    hwctx->lock_queue(avhwctx, qf, qidx);
+}
+
+static void hwctx_unlock_queue(void *priv, uint32_t qf, uint32_t qidx)
+{
+    AVHWDeviceContext *avhwctx = priv;
+    const AVVulkanDeviceContext *hwctx = avhwctx->hwctx;
+    hwctx->unlock_queue(avhwctx, qf, qidx);
+}
+
+static int add_instance_extension(const char **ext, unsigned num_ext,
+                                  const AVDictionary *opt,
+                                  AVDictionary **dict)
+{
+    const char *inst_ext_key = "instance_extensions";
+    AVDictionaryEntry *entry;
+    AVBPrint buf;
+    char *ext_list = NULL;
+    int ret;
+
+    av_bprint_init(&buf, 0, AV_BPRINT_SIZE_AUTOMATIC);
+    for (int i = 0; i < num_ext; i++) {
+        if (i)
+            av_bprintf(&buf, "+%s", ext[i]);
+        else
+            av_bprintf(&buf, "%s", ext[i]);
+    }
+
+    entry = av_dict_get(opt, inst_ext_key, NULL, 0);
+    if (entry && entry->value && entry->value[0]) {
+        if (num_ext)
+            av_bprintf(&buf, "+");
+        av_bprintf(&buf, "%s", entry->value);
+    }
+
+    ret = av_bprint_finalize(&buf, &ext_list);
+    if (ret < 0)
+        return ret;
+    return av_dict_set(dict, inst_ext_key, ext_list, AV_DICT_DONT_STRDUP_VAL);
+}
+
+static int add_device_extension(const AVDictionary *opt,
+                                AVDictionary **dict)
+{
+    const char *dev_ext_key = "device_extensions";
+    AVDictionaryEntry *entry;
+    AVBPrint buf;
+    char *ext_list = NULL;
+    int ret;
+
+    av_bprint_init(&buf, 0, AV_BPRINT_SIZE_AUTOMATIC);
+    av_bprintf(&buf, "%s", VK_KHR_SWAPCHAIN_EXTENSION_NAME);
+    for (int i = 0; i < pl_vulkan_num_recommended_extensions; i++)
+        av_bprintf(&buf, "+%s", pl_vulkan_recommended_extensions[i]);
+
+    entry = av_dict_get(opt, dev_ext_key, NULL, 0);
+    if (entry && entry->value && entry->value[0])
+        av_bprintf(&buf, "+%s", entry->value);
+
+    ret = av_bprint_finalize(&buf, &ext_list);
+    if (ret < 0)
+        return ret;
+    return av_dict_set(dict, dev_ext_key, ext_list, AV_DICT_DONT_STRDUP_VAL);
+}
+
+static int create_vk_by_hwcontext(VkRenderer *renderer,
+                                  const char **ext, unsigned num_ext,
+                                  const AVDictionary *opt)
+{
+    RendererContext *ctx = (RendererContext *) renderer;
+    AVHWDeviceContext *dev;
+    AVVulkanDeviceContext *hwctx;
+    AVDictionary *dict = NULL;
+    int ret;
+
+    ret = add_instance_extension(ext, num_ext, opt, &dict);
+    if (ret < 0)
+        return ret;
+    ret = add_device_extension(opt, &dict);
+    if (ret) {
+        av_dict_free(&dict);
+        return ret;
+    }
+
+    ret = av_hwdevice_ctx_create(&ctx->hw_device_ref, AV_HWDEVICE_TYPE_VULKAN,
+                           NULL, dict, 0);
+    av_dict_free(&dict);
+    if (ret < 0)
+        return ret;
+
+    dev = (AVHWDeviceContext *) ctx->hw_device_ref->data;
+    hwctx = dev->hwctx;
+
+    ctx->get_proc_addr = hwctx->get_proc_addr;
+    ctx->inst = hwctx->inst;
+    ctx->placebo_vulkan = pl_vulkan_import(ctx->vk_log,
+        pl_vulkan_import_params(
+            .instance = hwctx->inst,
+            .get_proc_addr = hwctx->get_proc_addr,
+            .phys_device = hwctx->phys_dev,
+            .device         = hwctx->act_dev,
+            .extensions     = hwctx->enabled_dev_extensions,
+            .num_extensions = hwctx->nb_enabled_dev_extensions,
+            .features       = &hwctx->device_features,
+            .lock_queue     = hwctx_lock_queue,
+            .unlock_queue   = hwctx_unlock_queue,
+            .queue_ctx      = dev,
+            .queue_graphics = {
+                .index = hwctx->queue_family_index,
+                .count = hwctx->nb_graphics_queues,
+            },
+            .queue_compute = {
+                .index = hwctx->queue_family_comp_index,
+                .count = hwctx->nb_comp_queues,
+            },
+            .queue_transfer = {
+                .index = hwctx->queue_family_tx_index,
+                .count = hwctx->nb_tx_queues,
+            },
+        ));
+    if (!ctx->placebo_vulkan)
+        return AVERROR_EXTERNAL;
+
+    return 0;
+}
+
+static void placebo_lock_queue(struct AVHWDeviceContext *dev_ctx,
+                       uint32_t queue_family, uint32_t index)
+{
+    RendererContext *ctx = dev_ctx->user_opaque;
+    pl_vulkan vk = ctx->placebo_vulkan;
+    vk->lock_queue(vk, queue_family, index);
+}
+
+static void placebo_unlock_queue(struct AVHWDeviceContext *dev_ctx,
+                         uint32_t queue_family,
+                         uint32_t index)
+{
+    RendererContext *ctx = dev_ctx->user_opaque;
+    pl_vulkan vk = ctx->placebo_vulkan;
+    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->get_proc_addr;
+
+    *index = -1;
+    *count = 0;
+    get_queue_family_prop = (PFN_vkGetPhysicalDeviceQueueFamilyProperties)
+            get_proc_addr(ctx->placebo_instance->instance,
+                          "vkGetPhysicalDeviceQueueFamilyProperties");
+    get_queue_family_prop(ctx->placebo_vulkan->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->placebo_vulkan->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 create_vk_by_placebo(VkRenderer *renderer,
+                                const char **ext, unsigned num_ext,
+                                const AVDictionary *opt)
+{
+    RendererContext *ctx = (RendererContext *) renderer;
+    AVHWDeviceContext *device_ctx;
+    AVVulkanDeviceContext *vk_dev_ctx;
+    int decode_index;
+    int decode_count;
+    int ret;
+
+    ctx->get_proc_addr = SDL_Vulkan_GetVkGetInstanceProcAddr();
+
+    ctx->placebo_instance = pl_vk_inst_create(ctx->vk_log, pl_vk_inst_params(
+            .get_proc_addr = ctx->get_proc_addr,
+            .debug = enable_debug(opt),
+            .extensions = ext,
+            .num_extensions = num_ext
+    ));
+    if (!ctx->placebo_instance) {
+        return AVERROR_EXTERNAL;
+    }
+    ctx->inst = ctx->placebo_instance->instance;
+
+    ctx->placebo_vulkan = pl_vulkan_create(ctx->vk_log, pl_vulkan_params(
+            .instance = ctx->placebo_instance->instance,
+            .get_proc_addr = ctx->placebo_instance->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->placebo_vulkan)
+        return AVERROR_EXTERNAL;
+    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 = placebo_lock_queue,
+            vk_dev_ctx->unlock_queue = placebo_unlock_queue;
+
+    vk_dev_ctx->get_proc_addr = ctx->placebo_instance->get_proc_addr;
+
+    vk_dev_ctx->inst = ctx->placebo_instance->instance;
+    vk_dev_ctx->phys_dev = ctx->placebo_vulkan->phys_device;
+    vk_dev_ctx->act_dev = ctx->placebo_vulkan->device;
+
+    vk_dev_ctx->device_features = *ctx->placebo_vulkan->features;
+
+    vk_dev_ctx->enabled_inst_extensions = ctx->placebo_instance->extensions;
+    vk_dev_ctx->nb_enabled_inst_extensions = ctx->placebo_instance->num_extensions;
+
+    vk_dev_ctx->enabled_dev_extensions = ctx->placebo_vulkan->extensions;
+    vk_dev_ctx->nb_enabled_dev_extensions = ctx->placebo_vulkan->num_extensions;
+
+    vk_dev_ctx->queue_family_index = ctx->placebo_vulkan->queue_graphics.index;
+    vk_dev_ctx->nb_graphics_queues = ctx->placebo_vulkan->queue_graphics.count;
+
+    vk_dev_ctx->queue_family_tx_index = ctx->placebo_vulkan->queue_transfer.index;
+    vk_dev_ctx->nb_tx_queues = ctx->placebo_vulkan->queue_transfer.count;
+
+    vk_dev_ctx->queue_family_comp_index = ctx->placebo_vulkan->queue_compute.index;
+    vk_dev_ctx->nb_comp_queues = ctx->placebo_vulkan->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;
+
+    return 0;
+}
+
+static int create(VkRenderer *renderer, SDL_Window *window, AVDictionary *opt)
+{
+    int ret = 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_DEBUG,
+            .log_priv = renderer,
+    };
+    RendererContext *ctx = (RendererContext *) renderer;
+    AVDictionaryEntry *entry;
+
+    ctx->vk_log = pl_log_create(PL_API_VER, &vk_log_params);
+
+    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(num_ext, sizeof(*ext));
+    if (!ext) {
+        ret = AVERROR(ENOMEM);
+        goto out;
+    }
+
+    SDL_Vulkan_GetInstanceExtensions(window, &num_ext, ext);
+
+    entry = av_dict_get(opt, "create_by_placebo", NULL, 0);
+    if (entry && strtol(entry->value, NULL, 10))
+        ret = create_vk_by_placebo(renderer, ext, num_ext, opt);
+    else
+        ret = create_vk_by_hwcontext(renderer, ext, num_ext, opt);
+    if (ret < 0)
+        goto out;
+
+    if (!SDL_Vulkan_CreateSurface(window, ctx->inst, &ctx->vk_surface)) {
+        ret = AVERROR_EXTERNAL;
+        goto out;
+    }
+
+    ctx->swapchain = pl_vulkan_create_swapchain(
+            ctx->placebo_vulkan,
+            pl_vulkan_swapchain_params(
+                    .surface = ctx->vk_surface,
+                    .present_mode = VK_PRESENT_MODE_FIFO_KHR));
+    if (!ctx->swapchain) {
+        ret = AVERROR_EXTERNAL;
+        goto out;
+    }
+
+    SDL_Vulkan_GetDrawableSize(window, &w, &h);
+    pl_swapchain_resize(ctx->swapchain, &w, &h);
+
+    ctx->renderer = pl_renderer_create(ctx->vk_log, ctx->placebo_vulkan->gpu);
+    if (!ctx->renderer) {
+        ret = AVERROR_EXTERNAL;
+        goto out;
+    }
+
+    ctx->vk_frame = av_frame_alloc();
+    if (!ctx->vk_frame) {
+        ret = AVERROR(ENOMEM);
+        goto out;
+    }
+
+    ret = 0;
+
+out:
+    av_free(ext);
+    return ret;
+}
+
+static int get_hw_dev(VkRenderer *renderer, AVBufferRef **dev)
+{
+    RendererContext *ctx = (RendererContext *) renderer;
+
+    *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 inline int move_to_output_frame(RendererContext *ctx, AVFrame *frame)
+{
+    int 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 map_frame(VkRenderer *renderer, AVFrame *frame, int use_hw_frame)
+{
+    RendererContext *ctx = (RendererContext *) renderer;
+    int ret;
+
+    if (use_hw_frame && !ctx->hw_frame_ref)
+        return AVERROR(ENOSYS);
+
+    // Try map data first
+    av_frame_unref(ctx->vk_frame);
+    if (use_hw_frame) {
+        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, 0);
+    if (!ret)
+        return move_to_output_frame(ctx, frame);
+
+    if (ret != AVERROR(ENOSYS))
+        av_log(NULL, AV_LOG_FATAL, "Map frame failed: %s\n", av_err2str(ret));
+    return ret;
+}
+
+static int transfer_frame(VkRenderer *renderer, AVFrame *frame, int use_hw_frame)
+{
+    RendererContext *ctx = (RendererContext *) renderer;
+    int ret;
+
+    if (use_hw_frame && !check_hw_transfer(ctx, frame))
+        return AVERROR(ENOSYS);
+
+    av_frame_unref(ctx->vk_frame);
+    if (use_hw_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)
+        return move_to_output_frame(ctx, frame);
+
+    if (ret != AVERROR(ENOSYS))
+        av_log(NULL, AV_LOG_FATAL, "Transfer frame failed: %s\n",
+               av_err2str(ret));
+    return ret;
+}
+
+static int convert_frame(VkRenderer *renderer, AVFrame *frame)
+{
+    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;
+
+    for (int use_hw = 1; use_hw >=0; use_hw--) {
+        ret = map_frame(renderer, frame, use_hw);
+        if (!ret)
+            return 0;
+        if (ret != AVERROR(ENOSYS))
+            return ret;
+
+        ret = transfer_frame(renderer, frame, use_hw);
+        if (!ret)
+            return 0;
+        if (ret != AVERROR(ENOSYS))
+            return ret;
+    }
+
+    return ret;
+}
+
+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;
+    int ret = 0;
+
+    ret = convert_frame(renderer, frame);
+    if (ret < 0)
+        return ret;
+
+    if (!pl_map_avframe_ex(ctx->placebo_vulkan->gpu, &pl_frame, pl_avframe_params(
+            .frame = frame,
+            .tex = ctx->tex))) {
+        av_log(NULL, AV_LOG_ERROR, "pl_map_avframe_ex failed\n");
+        return AVERROR_EXTERNAL;
+    }
+
+    if (!pl_swapchain_start_frame(ctx->swapchain, &swap_frame)) {
+        av_log(NULL, AV_LOG_ERROR, "start frame failed\n");
+        ret = AVERROR_EXTERNAL;
+        goto out;
+    }
+
+    pl_frame_from_swapchain(&target, &swap_frame);
+    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;
+    }
+
+    if (!pl_swapchain_submit_frame(ctx->swapchain)) {
+        av_log(NULL, AV_LOG_ERROR, "pl_swapchain_submit_frame failed\n");
+        ret = AVERROR_EXTERNAL;
+        goto out;
+    }
+    pl_swapchain_swap_buffers(ctx->swapchain);
+
+out:
+    pl_unmap_avframe(ctx->placebo_vulkan->gpu, &pl_frame);
+    return ret;
+}
+
+static int resize(VkRenderer *renderer, int width, int height)
+{
+    RendererContext *ctx = (RendererContext *) renderer;
+
+    if (!pl_swapchain_resize(ctx->swapchain, &width, &height))
+        return AVERROR_EXTERNAL;
+    return 0;
+}
+
+static void destroy(VkRenderer *renderer)
+{
+    RendererContext *ctx = (RendererContext *) renderer;
+    PFN_vkDestroySurfaceKHR vkDestroySurfaceKHR;
+
+    av_frame_free(&ctx->vk_frame);
+    av_freep(&ctx->transfer_formats);
+    av_hwframe_constraints_free(&ctx->constraints);
+    av_buffer_unref(&ctx->hw_frame_ref);
+
+    if (ctx->placebo_vulkan) {
+        for (int i = 0; i < FF_ARRAY_ELEMS(ctx->tex); i++)
+            pl_tex_destroy(ctx->placebo_vulkan->gpu, &ctx->tex[i]);
+        pl_renderer_destroy(&ctx->renderer);
+        pl_swapchain_destroy(&ctx->swapchain);
+        pl_vulkan_destroy(&ctx->placebo_vulkan);
+    }
+
+    if (ctx->vk_surface) {
+        vkDestroySurfaceKHR = (PFN_vkDestroySurfaceKHR)
+                ctx->get_proc_addr(ctx->inst, "vkDestroySurfaceKHR");
+        vkDestroySurfaceKHR(ctx->inst, ctx->vk_surface, NULL);
+        ctx->vk_surface = NULL;
+    }
+
+    av_buffer_unref(&ctx->hw_device_ref);
+    pl_vk_inst_destroy(&ctx->placebo_instance);
+
+    pl_log_destroy(&ctx->vk_log);
+}
+
+static const AVClass vulkan_renderer_class = {
+        .class_name = "Vulkan Renderer",
+        .item_name  = av_default_item_name,
+        .version    = LIBAVUTIL_VERSION_INT,
+};
+
+VkRenderer *vk_get_renderer(void)
+{
+    RendererContext *ctx = av_mallocz(sizeof(*ctx));
+    VkRenderer *renderer;
+
+    if (!ctx)
+        return NULL;
+
+    renderer = &ctx->api;
+    renderer->class = &vulkan_renderer_class;
+    renderer->get_hw_dev = get_hw_dev;
+    renderer->create = create;
+    renderer->display = display;
+    renderer->resize = resize;
+    renderer->destroy = destroy;
+
+    return renderer;
+}
+
+#else
+
+VkRenderer *vk_get_renderer(void)
+{
+    return NULL;
+}
+
+#endif
+
+int vk_renderer_create(VkRenderer *renderer, SDL_Window *window,
+                       AVDictionary *opt)
+{
+    return renderer->create(renderer, window, opt);
+}
+
+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);
+}
+
+int vk_renderer_resize(VkRenderer *renderer, int width, int height)
+{
+    return renderer->resize(renderer, width, height);
+}
+
+void vk_renderer_destroy(VkRenderer *renderer)
+{
+    renderer->destroy(renderer);
+}
diff --git a/fftools/ffplay_renderer.h b/fftools/ffplay_renderer.h
new file mode 100644
index 0000000000..dd8e9c06e7
--- /dev/null
+++ b/fftools/ffplay_renderer.h
@@ -0,0 +1,41 @@ 
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef FFTOOLS_FFPLAY_RENDERER_H
+#define FFTOOLS_FFPLAY_RENDERER_H
+
+#include <SDL.h>
+
+#include "libavutil/frame.h"
+
+typedef struct VkRenderer VkRenderer;
+
+VkRenderer *vk_get_renderer(void);
+
+int vk_renderer_create(VkRenderer *renderer, SDL_Window *window,
+                       AVDictionary *opt);
+
+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);
+
+void vk_renderer_destroy(VkRenderer *renderer);
+
+#endif /* FFTOOLS_FFPLAY_RENDERER_H */