[vlc-commits] vout: add new vulkan/libplacebo vout

Niklas Haas git at videolan.org
Wed Nov 7 17:09:47 CET 2018


vlc | branch: master | Niklas Haas <git at haasn.xyz> | Mon Oct  1 16:14:07 2018 +0200| [1386b85049c7a2bca6a7ab1512fa68889121d9fa] | committer: Thomas Guillem

vout: add new vulkan/libplacebo vout

This uses libplacebo's rendering helpers for all video output, on top of
the vulkan graphics API. Some notes:

- The existing fourcc/chroma helpers don't really line up with what the
  libplacebo API expects, or in some cases return values that just don't
  seem to make sense. I was advised against touching them for fear of
  breaking the rest of VLC - so we add our own helpers that give us the
  information in the format we need for libplacebo.

- Not all libplacebo options are mapped. There's no ability to create
  custom filter functions (which libplacebo/mpv support), and there's
  also no support for ICC profiles / 3DLUTs (which libplacebo supports)
  nor for the new color blindness simulation parameters in libplacebo
  v0.6. We also don't map the VLC brightness/hue/gamma/etc. options to
  the libplacebo structs - we could do it for free as part of the video
  decode matrix, rather than needing to insert a CPU filter for it.

- How to create the vulkan surface will depend on the platform (much
  like in opengl), so we move context, surface and device creation into
  a single module (`vulkan/surface.c`) which will be conditionally
  compiled depending on the platform in order to provide support for
  multiple surfaces side-by-side (e.g. x11 and wayland). This does mean
  that the context/device-related options end up being separate per
  platform, but OTOH this is not that bad since different platforms
  might want different e.g. swapchain modes (an example being wayland,
  which can make better use of mailbox rather than fifo).

- libplacebo doesn't have a "configure" step, instead all rendering
  parameters are fully dynamic. So we could call UpdateParams() in our
  module at any point in time when the config values change.
  Unfortunately, there's no easy way for us to find out when this is the
  case, so right now changing the vulkan module options requires a
  module reinit to take effect. In theory we could change this. (As an
  aside: calling var_Inherit* per frame does work to get us the changes
  in "realtime", as soon as the user clicks "save", but this may block
  for arbitrary amounts of time so I was advised against doing it)

Due to the new functions, structs and enum members used, the minimum
libplacebo version has been bumped up to v0.5.0. In theory we could also
try and support v0.4.0 with some #ifdefs, but v0.5.0 has been out for
several months now so it should be a safe requirement.

> http://git.videolan.org/gitweb.cgi/vlc.git/?a=commit;h=1386b85049c7a2bca6a7ab1512fa68889121d9fa
---

 configure.ac                           |  66 ++-
 modules/video_output/Makefile.am       |  22 +
 modules/video_output/placebo_utils.c   | 364 ++++++++++++++++
 modules/video_output/placebo_utils.h   | 295 +++++++++++++
 modules/video_output/vulkan/display.c  | 764 +++++++++++++++++++++++++++++++++
 modules/video_output/vulkan/instance.c |  77 ++++
 modules/video_output/vulkan/instance.h |  56 +++
 modules/video_output/vulkan/platform.h |  36 ++
 modules/video_output/vulkan/surface.c  | 186 ++++++++
 po/POTFILES.in                         |   1 +
 10 files changed, 1848 insertions(+), 19 deletions(-)

diff --git a/configure.ac b/configure.ac
index fc8ba64cb0..fa093bbee3 100644
--- a/configure.ac
+++ b/configure.ac
@@ -2988,6 +2988,27 @@ dnl
 EXTEND_HELP_STRING([Video plugins:])
 
 dnl
+dnl  libplacebo support
+dnl
+AC_ARG_ENABLE([libplacebo],
+  AS_HELP_STRING([--disable-libplacebo],
+      [disable libplacebo support (default auto)]))
+
+AS_IF([test "$enable_libplacebo" != "no"], [
+  PKG_CHECK_MODULES([LIBPLACEBO], [libplacebo >= 0.5], [
+    AC_DEFINE([HAVE_LIBPLACEBO], [1], [Define to 1 if libplacebo is enabled.])
+    PKG_CHECK_MODULES([LIBPLACEBO], [libplacebo >= 1.7],
+      [have_libplacebo_vk="yes"], [have_libplacebo_vk="no"])
+  ], [
+    AS_IF([test -n "${enable_libplacebo}"], [
+      AC_MSG_ERROR([${LIBPLACEBO_PKG_ERRORS}.])
+    ])
+    enable_libplacebo="no"
+  ])
+])
+AM_CONDITIONAL([HAVE_LIBPLACEBO], [test "$enable_libplacebo" != "no"])
+
+dnl
 dnl  OpenGL
 dnl
 PKG_CHECK_MODULES([EGL], [egl], [
@@ -3029,6 +3050,32 @@ dnl OpenGL ES 2: depends on EGL 1.1
 PKG_ENABLE_MODULES_VLC([GLES2], [], [glesv2], [OpenGL ES v2 support], [disabled])
 
 dnl
+dnl Vulkan
+dnl
+AC_ARG_ENABLE(vulkan,
+  [AS_HELP_STRING([--disable-vulkan],
+      [disable Vulkan support (default auto)])])
+
+AS_IF([test "$enable_vulkan" != "no"], [
+  AS_IF([test "$have_libplacebo_vk" = "yes"], [
+    PKG_CHECK_MODULES([VULKAN], [vulkan >= 1.0.26], [
+      AC_DEFINE([HAVE_VULKAN], [1], [Define to 1 if vulkan is enabled.])
+    ], [
+      AS_IF([test -n "${enable_vulkan}"], [
+        AC_MSG_ERROR([${VULKAN_PKG_ERRORS}.])
+      ])
+      enable_vulkan="no"
+    ])
+  ], [
+    AS_IF([test -n "${enable_vulkan}"], [
+      AC_MSG_ERROR([Vulkan support requires libplacebo 1.7 as well. Do not use --disable-libplacebo.])
+    ])
+    enable_vulkan="no"
+  ])
+])
+AM_CONDITIONAL(HAVE_VULKAN, [test "$enable_vulkan" != "no"])
+
+dnl
 dnl  Xlib
 dnl
 
@@ -4142,25 +4189,6 @@ dnl Libnotify notification plugin
 dnl
 PKG_ENABLE_MODULES_VLC([NOTIFY], [], [libnotify gtk+-3.0], [libnotify notification], [auto])
 
-dnl
-dnl  libplacebo support
-dnl
-AC_ARG_ENABLE([libplacebo],
-  AS_HELP_STRING([--disable-libplacebo],
-      [disable libplacebo support (default auto)]))
-
-AS_IF([test "$enable_libplacebo" != "no"], [
-  PKG_CHECK_MODULES([LIBPLACEBO], [libplacebo >= 0.2.1], [
-    AC_DEFINE([HAVE_LIBPLACEBO], [1], [Define to 1 if libplacebo is enabled.])
-  ], [
-    AS_IF([test -n "${enable_libplacebo}"], [
-      AC_MSG_ERROR([${LIBPLACEBO_PKG_ERRORS}.])
-    ])
-    enable_libplacebo="no"
-  ])
-])
-AM_CONDITIONAL([HAVE_LIBPLACEBO], [test "$enable_libplacebo" != "no"])
-
 PKG_ENABLE_MODULES_VLC([MEDIALIBRARY], [medialibrary], [medialibrary], (medialibrary support), [auto])
 
 dnl
diff --git a/modules/video_output/Makefile.am b/modules/video_output/Makefile.am
index ebd1e998de..73196fc7cc 100644
--- a/modules/video_output/Makefile.am
+++ b/modules/video_output/Makefile.am
@@ -124,6 +124,28 @@ vout_LTLIBRARIES += libglconv_vdpau_plugin.la
 endif
 endif # HAVE_GL
 
+### Vulkan ###
+
+VULKAN_COMMONSOURCES = video_output/vulkan/instance.c \
+	video_output/vulkan/instance.h \
+	video_output/vulkan/platform.h \
+	video_output/placebo_utils.c \
+	video_output/placebo_utils.h
+
+# Trigger the c++ linker because of glslang dependency of libplacebo
+VULKAN_COMMONSOURCES += dummy.cpp
+
+VULKAN_COMMONCFLAGS = $(VULKAN_CFLAGS) $(LIBPLACEBO_CFLAGS)
+VULKAN_COMMONLIBS = $(VULKAN_LIBS) $(LIBPLACEBO_LIBS)
+
+libvk_plugin_la_SOURCES = $(VULKAN_COMMONSOURCES) video_output/vulkan/display.c
+libvk_plugin_la_CFLAGS = $(AM_CFLAGS) $(VULKAN_COMMONCFLAGS)
+libvk_plugin_la_LIBADD = $(VULKAN_COMMONLIBS)
+
+if HAVE_VULKAN
+vout_LTLIBRARIES += libvk_plugin.la
+endif
+
 ### XCB ###
 libvlc_xcb_events_la_SOURCES = \
 	video_output/xcb/events.c video_output/xcb/events.h
diff --git a/modules/video_output/placebo_utils.c b/modules/video_output/placebo_utils.c
index 2515dc0bef..90edf77be1 100644
--- a/modules/video_output/placebo_utils.c
+++ b/modules/video_output/placebo_utils.c
@@ -52,6 +52,318 @@ struct pl_context *vlc_placebo_Create(vlc_object_t *obj)
     });
 }
 
+struct plane_desc {
+    int components;
+    size_t pixel_size;
+    int comp_bits[4];
+    int comp_map[4];
+    int w_denom;
+    int h_denom;
+};
+
+struct fmt_desc {
+    enum pl_fmt_type type;
+    struct plane_desc planes[4];
+    int num_planes;
+    int color_bits; // relevant bits, or 0 if the same as the texture depth
+};
+
+#define SIZE(n, bits, pad) (((n) * (bits) + (pad) + 7) / 8)
+#define COMPS(...) {__VA_ARGS__}
+
+#define PLANE(n, bits, map, wd, hd, pad)            \
+  { .components = n,                                \
+    .pixel_size = SIZE(n, bits, pad),               \
+    .comp_bits = {bits, bits, bits, bits},          \
+    .comp_map = map,                                \
+    .w_denom = wd,                                  \
+    .h_denom = hd,                                  \
+  }
+
+#define _PLANAR(n, bits, wd, hd)          \
+  .type = PL_FMT_UNORM,                   \
+  .num_planes = n,                        \
+  .planes = {                             \
+      PLANE(1, bits, {0},  1,  1, 0),     \
+      PLANE(1, bits, {1}, wd, hd, 0),     \
+      PLANE(1, bits, {2}, wd, hd, 0),     \
+      PLANE(1, bits, {3},  1,  1, 0),     \
+  }
+
+#define _SEMIPLANAR(n, bits, wd, hd)           \
+  .type = PL_FMT_UNORM,                        \
+  .num_planes = n,                             \
+  .planes = {                                  \
+      PLANE(1, bits, {0},          1,  1, 0),  \
+      PLANE(2, bits, COMPS(1, 2), wd, hd, 0),  \
+      PLANE(1, bits, {3},          1,  1, 0),  \
+  }
+
+#define PACKED(n, bits, pad)                          \
+  .type = PL_FMT_UNORM,                               \
+  .num_planes = 1,                                    \
+  .planes = {                                         \
+      PLANE(n, bits, COMPS(0, 1, 2, 3), 1, 1, pad),   \
+  }
+
+#define SINGLE(t, bits)                   \
+  .type = PL_FMT_##t,                     \
+  .num_planes = 1,                        \
+  .planes = {                             \
+      PLANE(1, bits, {0}, 1, 1, 0),       \
+  }
+
+#define PLANAR(...) _PLANAR(__VA_ARGS__)
+#define SEMIPLANAR(...) _SEMIPLANAR(__VA_ARGS__)
+
+#define _410 4, 2
+#define _411 4, 1
+#define _420 2, 2
+#define _422 2, 1
+#define _440 1, 2
+#define _444 1, 1
+
+// NOTE: This list contains some special formats that don't follow the normal
+// rules, but which are included regardless. The corrections for these
+// exceptions happen below, in the function vlc_placebo_PlaneFormat!
+static const struct { vlc_fourcc_t fcc; struct fmt_desc desc; } formats[] = {
+    { VLC_CODEC_YV9,            {PLANAR(3,  8, _410)} },
+    { VLC_CODEC_I410,           {PLANAR(3,  8, _410)} },
+    { VLC_CODEC_I411,           {PLANAR(3,  8, _411)} },
+    { VLC_CODEC_I440,           {PLANAR(3,  8, _440)} },
+    { VLC_CODEC_J440,           {PLANAR(3,  8, _440)} },
+    { VLC_CODEC_GREY,           {PLANAR(1,  8, _444)} },
+
+    { VLC_CODEC_I420,           {PLANAR(3,  8, _420)} },
+    { VLC_CODEC_J420,           {PLANAR(3,  8, _420)} },
+#ifdef WORDS_BIGENDIAN
+    { VLC_CODEC_I420_9B,        {PLANAR(3, 16, _420), .color_bits = 9} },
+    { VLC_CODEC_I420_10B,       {PLANAR(3, 16, _420), .color_bits = 10} },
+    { VLC_CODEC_I420_12B,       {PLANAR(3, 16, _420), .color_bits = 12} },
+    { VLC_CODEC_I420_16B,       {PLANAR(3, 16, _420), .color_bits = 16} },
+#else
+    { VLC_CODEC_I420_9L,        {PLANAR(3, 16, _420), .color_bits = 9} },
+    { VLC_CODEC_I420_10L,       {PLANAR(3, 16, _420), .color_bits = 10} },
+    { VLC_CODEC_I420_12L,       {PLANAR(3, 16, _420), .color_bits = 12} },
+    { VLC_CODEC_I420_16L,       {PLANAR(3, 16, _420), .color_bits = 16} },
+#endif
+
+    { VLC_CODEC_I422,           {PLANAR(3,  8, _422)} },
+    { VLC_CODEC_J422,           {PLANAR(3,  8, _422)} },
+#ifdef WORDS_BIGENDIAN
+    { VLC_CODEC_I422_9B,        {PLANAR(3, 16, _422), .color_bits = 9} },
+    { VLC_CODEC_I422_10B,       {PLANAR(3, 16, _422), .color_bits = 10} },
+    { VLC_CODEC_I422_12B,       {PLANAR(3, 16, _422), .color_bits = 12} },
+#else
+    { VLC_CODEC_I422_9L,        {PLANAR(3, 16, _422), .color_bits = 9} },
+    { VLC_CODEC_I422_10L,       {PLANAR(3, 16, _422), .color_bits = 10} },
+    { VLC_CODEC_I422_12L,       {PLANAR(3, 16, _422), .color_bits = 12} },
+#endif
+
+    { VLC_CODEC_I444,           {PLANAR(3,  8, _444)} },
+    { VLC_CODEC_J444,           {PLANAR(3,  8, _444)} },
+#ifdef WORDS_BIGENDIAN
+    { VLC_CODEC_I444_9B,        {PLANAR(3, 16, _444), .color_bits = 9} },
+    { VLC_CODEC_I444_10B,       {PLANAR(3, 16, _444), .color_bits = 10} },
+    { VLC_CODEC_I444_12B,       {PLANAR(3, 16, _444), .color_bits = 12} },
+    { VLC_CODEC_I444_16B,       {PLANAR(3, 16, _444), .color_bits = 16} },
+#else
+    { VLC_CODEC_I444_9L,        {PLANAR(3, 16, _444), .color_bits = 9} },
+    { VLC_CODEC_I444_10L,       {PLANAR(3, 16, _444), .color_bits = 10} },
+    { VLC_CODEC_I444_12L,       {PLANAR(3, 16, _444), .color_bits = 12} },
+    { VLC_CODEC_I444_16L,       {PLANAR(3, 16, _444), .color_bits = 16} },
+#endif
+
+    { VLC_CODEC_YUVA,           {PLANAR(4,  8, _444)} },
+    { VLC_CODEC_YUV422A,        {PLANAR(4,  8, _422)} },
+#ifdef WORDS_BIGENDIAN
+    { VLC_CODEC_YUVA_444_10B,   {PLANAR(4, 16, _444), .color_bits = 10} },
+#else
+    { VLC_CODEC_YUVA_444_10L,   {PLANAR(4, 16, _444), .color_bits = 10} },
+#endif
+
+    { VLC_CODEC_NV12,           {SEMIPLANAR(2,  8, _420)} },
+    { VLC_CODEC_NV21,           {SEMIPLANAR(2,  8, _420)} },
+    { VLC_CODEC_P010,           {SEMIPLANAR(2, 16, _420)} },
+    { VLC_CODEC_NV16,           {SEMIPLANAR(2,  8, _422)} },
+    { VLC_CODEC_NV61,           {SEMIPLANAR(2,  8, _422)} },
+    { VLC_CODEC_NV24,           {SEMIPLANAR(2,  8, _444)} },
+    { VLC_CODEC_NV42,           {SEMIPLANAR(2,  8, _444)} },
+
+    { VLC_CODEC_RGB8,           {PACKED(3, 2, 2)} },
+    { VLC_CODEC_RGB12,          {PACKED(3, 4, 4)} },
+    { VLC_CODEC_RGB15,          {PACKED(3, 5, 1)} },
+    { VLC_CODEC_RGB16,          {PACKED(3, 5, 1)} },
+    { VLC_CODEC_RGB24,          {PACKED(3, 8, 0)} },
+    { VLC_CODEC_RGB32,          {PACKED(3, 8, 8)} },
+    { VLC_CODEC_RGBA,           {PACKED(4, 8, 0)} },
+    { VLC_CODEC_BGRA,           {PACKED(4, 8, 0)} },
+
+    { VLC_CODEC_GBR_PLANAR,     {PLANAR(3,  8, _444)} },
+#ifdef WORDS_BIGENDIAN
+    { VLC_CODEC_GBR_PLANAR_9B,  {PLANAR(3, 16, _444), .color_bits = 9} },
+    { VLC_CODEC_GBR_PLANAR_10B, {PLANAR(3, 16, _444), .color_bits = 10} },
+    { VLC_CODEC_GBR_PLANAR_16B, {PLANAR(3, 16, _444), .color_bits = 16} },
+#else
+    { VLC_CODEC_GBR_PLANAR_9L,  {PLANAR(3, 16, _444), .color_bits = 9} },
+    { VLC_CODEC_GBR_PLANAR_10L, {PLANAR(3, 16, _444), .color_bits = 10} },
+    { VLC_CODEC_GBR_PLANAR_16L, {PLANAR(3, 16, _444), .color_bits = 16} },
+#endif
+
+    { VLC_CODEC_U8,             {SINGLE(UNORM,  8)} },
+    { VLC_CODEC_S8,             {SINGLE(SNORM,  8)} },
+    { VLC_CODEC_U16N,           {SINGLE(UNORM, 16)} },
+    { VLC_CODEC_S16N,           {SINGLE(SNORM, 16)} },
+    { VLC_CODEC_U24N,           {SINGLE(UNORM, 24)} },
+    { VLC_CODEC_S24N,           {SINGLE(SNORM, 24)} },
+    { VLC_CODEC_U32N,           {SINGLE(UNORM, 32)} },
+    { VLC_CODEC_S32N,           {SINGLE(SNORM, 32)} },
+    { VLC_CODEC_FL32,           {SINGLE(FLOAT, 32)} },
+    { VLC_CODEC_FL64,           {SINGLE(FLOAT, 64)} },
+
+    { 0 },
+};
+
+static const struct fmt_desc *FindDesc(vlc_fourcc_t fcc)
+{
+    for (int i = 0; formats[i].fcc; i++) {
+        if (formats[i].fcc == fcc) {
+            return &formats[i].desc;
+        }
+    }
+
+    return NULL;
+}
+
+// This fills everything except width/height, which are left as 1
+static void FillDesc(vlc_fourcc_t fcc, const struct fmt_desc *desc,
+                     struct pl_plane_data data[4])
+{
+    assert(desc->num_planes <= 4);
+    for (int i = 0; i < desc->num_planes; i++) {
+        const struct plane_desc *p = &desc->planes[i];
+
+        data[i] = (struct pl_plane_data) {
+            .type   = desc->type,
+            .width  = 1,
+            .height = 1,
+            .pixel_stride = p->pixel_size,
+        };
+
+        for (int c = 0; c < p->components; c++) {
+            data[i].component_size[c] = p->comp_bits[c];
+            data[i].component_map[c] = p->comp_map[c];
+        }
+    }
+
+    // Exceptions to the rule
+    switch (fcc) {
+    case VLC_CODEC_YV9:
+    case VLC_CODEC_YV12:
+        // Planar Y:V:U
+        data[1].component_map[0] = 2;
+        data[2].component_map[0] = 1;
+        break;
+
+    case VLC_CODEC_RGB32:
+        // XRGB instead of RGBX
+        data[0].component_map[0] = -1;
+        data[1].component_map[0] = 0;
+        data[2].component_map[0] = 1;
+        data[3].component_map[0] = 2;
+        break;
+
+    case VLC_CODEC_BGRA:
+        // Packed BGR
+        data[0].component_map[0] = 2;
+        data[0].component_map[1] = 1;
+        data[0].component_map[2] = 0;
+        break;
+
+    case VLC_CODEC_GBR_PLANAR:
+    case VLC_CODEC_GBR_PLANAR_9L:
+    case VLC_CODEC_GBR_PLANAR_10L:
+    case VLC_CODEC_GBR_PLANAR_16L:
+    case VLC_CODEC_GBR_PLANAR_9B:
+    case VLC_CODEC_GBR_PLANAR_10B:
+    case VLC_CODEC_GBR_PLANAR_16B:
+        // Planar GBR
+        data[0].component_map[0] = 1;
+        data[1].component_map[0] = 2;
+        data[2].component_map[0] = 0;
+        break;
+
+    case VLC_CODEC_RGB16:
+        // 5:6:5 instead of 5:5:5
+        data[0].component_size[1] += 1;
+        break;
+
+    case VLC_CODEC_RGB8:
+        // 3:3:2 instead of 2:2:2
+        data[0].component_size[0] += 1;
+        data[0].component_size[1] += 1;
+        break;
+
+    default: break;
+    }
+}
+
+int vlc_placebo_PlaneFormat(const video_format_t *fmt, struct pl_plane_data data[4])
+{
+    const struct fmt_desc *desc = FindDesc(fmt->i_chroma);
+    if (!desc)
+        return 0;
+
+    FillDesc(fmt->i_chroma, desc, data);
+    for (int i = 0; i < desc->num_planes; i++) {
+        const struct plane_desc *p = &desc->planes[i];
+        data[i].width  = (fmt->i_visible_width  + p->w_denom - 1) / p->w_denom;
+        data[i].height = (fmt->i_visible_height + p->h_denom - 1) / p->h_denom;
+    }
+
+    return desc->num_planes;
+}
+
+int vlc_placebo_PlaneData(const picture_t *pic, struct pl_plane_data data[4],
+                          const struct pl_buf *buf)
+{
+    int planes = vlc_placebo_PlaneFormat(&pic->format, data);
+    if (!planes)
+        return 0;
+
+    assert(planes == pic->i_planes);
+    for (int i = 0; i < planes; i++) {
+        assert(data[i].height == pic->p[i].i_visible_lines);
+        data[i].row_stride = pic->p[i].i_pitch;
+        if (buf) {
+            assert(buf->data);
+            assert(pic->p[i].p_pixels <= buf->data + buf->params.size);
+            data[i].buf = buf;
+            data[i].buf_offset = (uintptr_t) pic->p[i].p_pixels - (ptrdiff_t) buf->data;
+        } else {
+            data[i].pixels = pic->p[i].p_pixels;
+        }
+    }
+
+    return planes;
+}
+
+bool vlc_placebo_FormatSupported(const struct pl_gpu *gpu, vlc_fourcc_t fcc)
+{
+    const struct fmt_desc *desc = FindDesc(fcc);
+    if (!desc)
+        return false;
+
+    struct pl_plane_data data[4];
+    FillDesc(fcc, desc, data);
+    for (int i = 0; i < desc->num_planes; i++) {
+        if (!pl_plane_find_fmt(gpu, NULL, &data[i]))
+            return false;
+    }
+
+    return true;
+}
+
 struct pl_color_space vlc_placebo_ColorSpace(const video_format_t *fmt)
 {
     static const enum pl_color_primaries primaries[COLOR_PRIMARIES_MAX+1] = {
@@ -100,3 +412,55 @@ struct pl_color_space vlc_placebo_ColorSpace(const video_format_t *fmt)
         .sig_avg   = sig_avg,
     };
 }
+
+struct pl_color_repr vlc_placebo_ColorRepr(const video_format_t *fmt)
+{
+    static const enum pl_color_system yuv_systems[COLOR_SPACE_MAX+1] = {
+        [COLOR_SPACE_UNDEF]     = PL_COLOR_SYSTEM_BT_709, // _UNKNOWN is RGB
+        [COLOR_SPACE_BT601]     = PL_COLOR_SYSTEM_BT_601,
+        [COLOR_SPACE_BT709]     = PL_COLOR_SYSTEM_BT_709,
+        [COLOR_SPACE_BT2020]    = PL_COLOR_SYSTEM_BT_2020_NC,
+    };
+
+    // fmt->space describes the YCbCr type only, it does not distinguish
+    // between YUV, XYZ, RGB and the likes!
+    enum pl_color_system sys;
+    if (likely(vlc_fourcc_IsYUV(fmt->i_chroma))) {
+        sys = yuv_systems[fmt->space];
+    } else if (unlikely(fmt->i_chroma == VLC_CODEC_XYZ12)) {
+        sys = PL_COLOR_SYSTEM_XYZ;
+    } else {
+        sys = PL_COLOR_SYSTEM_RGB;
+    }
+
+    const struct fmt_desc *desc = FindDesc(fmt->i_chroma);
+    int sample_depth = desc->planes[0].comp_bits[0]; // just use first component
+
+    return (struct pl_color_repr) {
+        .sys        = sys,
+        .alpha      = PL_ALPHA_PREMULTIPLIED,
+        .levels     = unlikely(fmt->b_color_range_full)
+                        ? PL_COLOR_LEVELS_PC
+                        : PL_COLOR_LEVELS_TV,
+        .bits = {
+            .sample_depth   = sample_depth,
+            .color_depth    = desc->color_bits ? desc->color_bits : sample_depth,
+            .bit_shift      = 0,
+        },
+    };
+}
+
+enum pl_chroma_location vlc_placebo_ChromaLoc(const video_format_t *fmt)
+{
+    static const enum pl_chroma_location locs[CHROMA_LOCATION_MAX+1] = {
+        [CHROMA_LOCATION_UNDEF]         = PL_CHROMA_UNKNOWN,
+        [CHROMA_LOCATION_LEFT]          = PL_CHROMA_LEFT,
+        [CHROMA_LOCATION_CENTER]        = PL_CHROMA_CENTER,
+        [CHROMA_LOCATION_TOP_LEFT]      = PL_CHROMA_TOP_LEFT,
+        [CHROMA_LOCATION_TOP_CENTER]    = PL_CHROMA_TOP_CENTER,
+        [CHROMA_LOCATION_BOTTOM_LEFT]   = PL_CHROMA_BOTTOM_LEFT,
+        [CHROMA_LOCATION_BOTTOM_CENTER] = PL_CHROMA_BOTTOM_CENTER,
+    };
+
+    return locs[fmt->chroma_location];
+}
diff --git a/modules/video_output/placebo_utils.h b/modules/video_output/placebo_utils.h
index 2933996255..ab7caf4e1c 100644
--- a/modules/video_output/placebo_utils.h
+++ b/modules/video_output/placebo_utils.h
@@ -26,12 +26,25 @@
 
 #include <libplacebo/colorspace.h>
 #include <libplacebo/shaders/colorspace.h>
+#include <libplacebo/utils/upload.h>
 
 // Create a libplacebo context, hooked up to the log system; or NULL on OOM
 VLC_API struct pl_context *vlc_placebo_Create(vlc_object_t *);
 
 // Turn a video_format_t into the equivalent libplacebo values
 VLC_API struct pl_color_space vlc_placebo_ColorSpace(const video_format_t *);
+VLC_API struct pl_color_repr vlc_placebo_ColorRepr(const video_format_t *);
+VLC_API enum pl_chroma_location vlc_placebo_ChromaLoc(const video_format_t *);
+
+// Fill a pl_plane_data array with various data. Returns the number of planes,
+// or 0 if the format is unsupported by the libplacebo API. If `buf` is set,
+// then all addresses of the picture_t must lie within `buf`'s mapped memory.
+VLC_API int vlc_placebo_PlaneFormat(const video_format_t *, struct pl_plane_data[4]);
+VLC_API int vlc_placebo_PlaneData(const picture_t *, struct pl_plane_data[4],
+                                  const struct pl_buf *buf);
+
+// See if a given FourCC is physically supported by a given GPU
+VLC_API bool vlc_placebo_FormatSupported(const struct pl_gpu *, vlc_fourcc_t);
 
 // Shared options strings/structs for libplacebo options
 
@@ -153,6 +166,15 @@ static const char * const tone_text[] = {
 #define GAMUT_WARN_TEXT "Highlight clipped pixels"
 #define GAMUT_WARN_LONGTEXT "Debugging tool to indicate which pixels were clipped as part of the tone mapping process."
 
+#define PEAK_FRAMES_TEXT "HDR peak detection buffer size"
+#define PEAK_FRAMES_LONGTEXT "How many input frames to consider when determining the brightness of HDR signals. Higher values result in a slower/smoother response to brightness level changes. Setting this to 0 disables peak detection entirely."
+
+#define TARGET_AVG_TEXT "Target peak brightness average"
+#define TARGET_AVG_LONGTEXT "If the source frame has an average brightness exceeding this number, the frame will be automatically darkened to compensate. This feature only works when peak detection is enabled."
+
+#define SCENE_THRESHOLD_TEXT "HDR peak scene change threshold"
+#define SCENE_THRESHOLD_LONGTEXT "When using HDR peak detection, this sets a threshold for sudden brightness changes that should be considered as scene changes. This will result in the detected peak being immediately updated to the new value, rather than gradually being adjusted. Setting this to 0 disables this feature."
+
 #define DITHER_TEXT "Dithering algorithm"
 #define DITHER_LONGTEXT "The algorithm to use when dithering to a lower bit depth."
 
@@ -172,7 +194,280 @@ static const char * const dither_text[] = {
     "White noise (fast but low quality)",
 };
 
+#define DITHER_SIZE_TEXT "Dither LUT size (log 2)"
+#define DITHER_SIZE_LONGTEXT "Controls the size of the dither matrix, as a power of two (e.g. the default of 6 corresponds to a 64x64 matrix). Does not affect all algorithms."
+
+#define TEMPORAL_DITHER_TEXT "Temporal dithering"
+#define TEMPORAL_DITHER_LONGTEXT "Enables perturbing the dither matrix across frames. This reduces the persistence of dithering artifacts, but can cause flickering on some (cheap) LCD screens."
+
 #define DITHER_DEPTH_TEXT "Dither depth override (0 = auto)"
 #define DITHER_DEPTH_LONGTEXT "Overrides the detected framebuffer depth. Useful to dither to lower bit depths than otherwise required."
 
+enum {
+    SCALE_BUILTIN = 0,
+    SCALE_SPLINE16,
+    SCALE_SPLINE36,
+    SCALE_SPLINE64,
+    SCALE_MITCHELL,
+    SCALE_BICUBIC,
+    SCALE_EWA_LANCZOS,
+    SCALE_NEAREST,
+    SCALE_BILINEAR,
+    SCALE_GAUSSIAN,
+    SCALE_LANCZOS,
+    SCALE_GINSENG,
+    SCALE_EWA_GINSENG,
+    SCALE_EWA_HANN,
+    SCALE_HAASNSOFT,
+    SCALE_CATMULL_ROM,
+    SCALE_ROBIDOUX,
+    SCALE_ROBIDOUXSHARP,
+    SCALE_EWA_ROBIDOUX,
+    SCALE_EWA_ROBIDOUXSHARP,
+    SCALE_SINC,
+    SCALE_EWA_JINC,
+    SCALE_CUSTOM,
+};
+
+static const int scale_values[] = {
+    SCALE_BUILTIN,
+    SCALE_SPLINE16,
+    SCALE_SPLINE36,
+    SCALE_SPLINE64,
+    SCALE_MITCHELL,
+    SCALE_BICUBIC,
+    SCALE_EWA_LANCZOS,
+    SCALE_NEAREST,
+    SCALE_BILINEAR,
+    SCALE_GAUSSIAN,
+    SCALE_LANCZOS,
+    SCALE_GINSENG,
+    SCALE_EWA_GINSENG,
+    SCALE_EWA_HANN,
+    SCALE_HAASNSOFT,
+    SCALE_CATMULL_ROM,
+    SCALE_ROBIDOUX,
+    SCALE_ROBIDOUXSHARP,
+    SCALE_EWA_ROBIDOUX,
+    SCALE_EWA_ROBIDOUXSHARP,
+    SCALE_SINC,
+    SCALE_EWA_JINC,
+    SCALE_CUSTOM,
+};
+
+static const char * const scale_text[] = {
+    "Built-in / fixed function (fast)",
+    "Spline 2 taps",
+    "Spline 3 taps (recommended upscaler)",
+    "Spline 4 taps",
+    "Mitchell-Netravali (recommended downscaler)",
+    "Bicubic",
+    "Jinc / EWA Lanczos 3 taps (high quality, slow)",
+    "Nearest neighbour",
+    "Bilinear",
+    "Gaussian",
+    "Lanczos 3 taps",
+    "Ginseng 3 taps",
+    "EWA Ginseng",
+    "EWA Hann",
+    "HaasnSoft (blurred EWA Hann)",
+    "Catmull-Rom",
+    "Robidoux",
+    "RobidouxSharp",
+    "EWA Robidoux",
+    "EWA RobidouxSharp",
+    "Unwindowed sinc (clipped)",
+    "Unwindowed EWA Jinc (clipped)",
+    "Custom (see below)",
+};
+
+static const struct pl_filter_config *scale_config[] = {
+    [SCALE_BUILTIN]             = NULL,
+    [SCALE_SPLINE16]            = &pl_filter_spline16,
+    [SCALE_SPLINE36]            = &pl_filter_spline36,
+    [SCALE_SPLINE64]            = &pl_filter_spline64,
+    [SCALE_NEAREST]             = &pl_filter_box,
+    [SCALE_BILINEAR]            = &pl_filter_triangle,
+    [SCALE_GAUSSIAN]            = &pl_filter_gaussian,
+    [SCALE_SINC]                = &pl_filter_sinc,
+    [SCALE_LANCZOS]             = &pl_filter_lanczos,
+    [SCALE_GINSENG]             = &pl_filter_ginseng,
+    [SCALE_EWA_JINC]            = &pl_filter_ewa_jinc,
+    [SCALE_EWA_LANCZOS]         = &pl_filter_ewa_lanczos,
+    [SCALE_EWA_GINSENG]         = &pl_filter_ewa_ginseng,
+    [SCALE_EWA_HANN]            = &pl_filter_ewa_hann,
+    [SCALE_HAASNSOFT]           = &pl_filter_haasnsoft,
+    [SCALE_BICUBIC]             = &pl_filter_bicubic,
+    [SCALE_CATMULL_ROM]         = &pl_filter_catmull_rom,
+    [SCALE_MITCHELL]            = &pl_filter_mitchell,
+    [SCALE_ROBIDOUX]            = &pl_filter_robidoux,
+    [SCALE_ROBIDOUXSHARP]       = &pl_filter_robidouxsharp,
+    [SCALE_EWA_ROBIDOUX]        = &pl_filter_robidoux,
+    [SCALE_EWA_ROBIDOUXSHARP]   = &pl_filter_robidouxsharp,
+    [SCALE_CUSTOM]              = NULL,
+};
+
+#define UPSCALER_PRESET_TEXT "Upscaler preset"
+#define DOWNSCALER_PRESET_TEXT "Downscaler preset"
+#define SCALER_PRESET_LONGTEXT "Choose from one of the built-in scaler presets. If set to custom, you can choose your own combination of kernel/window functions."
+
+#define LUT_ENTRIES_TEXT "Scaler LUT size"
+#define LUT_ENTRIES_LONGTEXT "Size of the LUT texture used for up/downscalers that require one. Reducing this may boost performance at the cost of quality."
+
+#define ANTIRING_TEXT "Anti-ringing strength"
+#define ANTIRING_LONGTEXT "Enables anti-ringing for non-polar filters. A value of 1.0 completely removes ringing, a value of 0.0 is a no-op."
+
+enum {
+    FILTER_NONE = 0,
+    FILTER_BOX,
+    FILTER_TRIANGLE,
+    FILTER_HANN,
+    FILTER_HAMMING,
+    FILTER_WELCH,
+    FILTER_KAISER,
+    FILTER_BLACKMAN,
+    FILTER_GAUSSIAN,
+    FILTER_SINC,
+    FILTER_JINC,
+    FILTER_SPHINX,
+    FILTER_BCSPLINE,
+    FILTER_CATMULL_ROM,
+    FILTER_MITCHELL,
+    FILTER_ROBIDOUX,
+    FILTER_ROBIDOUXSHARP,
+    FILTER_BICUBIC,
+    FILTER_SPLINE16,
+    FILTER_SPLINE36,
+    FILTER_SPLINE64,
+};
+
+static const int filter_values[] = {
+    FILTER_NONE,
+    FILTER_BOX,
+    FILTER_TRIANGLE,
+    FILTER_HANN,
+    FILTER_HAMMING,
+    FILTER_WELCH,
+    FILTER_KAISER,
+    FILTER_BLACKMAN,
+    FILTER_GAUSSIAN,
+    FILTER_SINC,
+    FILTER_JINC,
+    FILTER_SPHINX,
+    FILTER_BCSPLINE,
+    FILTER_CATMULL_ROM,
+    FILTER_MITCHELL,
+    FILTER_ROBIDOUX,
+    FILTER_ROBIDOUXSHARP,
+    FILTER_BICUBIC,
+    FILTER_SPLINE16,
+    FILTER_SPLINE36,
+    FILTER_SPLINE64,
+};
+
+static const char * const filter_text[] = {
+    "None",
+    "Box / Nearest",
+    "Triangle / Linear",
+    "Hann",
+    "Hamming",
+    "Welch",
+    "Kaiser",
+    "Blackman",
+    "Gaussian",
+    "Sinc",
+    "Jinc",
+    "Sphinx",
+    "BC spline",
+    "Catmull-Rom",
+    "Mitchell-Netravali",
+    "Robidoux",
+    "RobidouxSharp",
+    "Bicubic",
+    "Spline16",
+    "Spline36",
+    "Spline64",
+};
+
+static const struct pl_filter_function *filter_fun[] = {
+    [FILTER_NONE]           = NULL,
+    [FILTER_BOX]            = &pl_filter_function_box,
+    [FILTER_TRIANGLE]       = &pl_filter_function_triangle,
+    [FILTER_HANN]           = &pl_filter_function_hann,
+    [FILTER_HAMMING]        = &pl_filter_function_hamming,
+    [FILTER_WELCH]          = &pl_filter_function_welch,
+    [FILTER_KAISER]         = &pl_filter_function_kaiser,
+    [FILTER_BLACKMAN]       = &pl_filter_function_blackman,
+    [FILTER_GAUSSIAN]       = &pl_filter_function_gaussian,
+    [FILTER_SINC]           = &pl_filter_function_sinc,
+    [FILTER_JINC]           = &pl_filter_function_jinc,
+    [FILTER_SPHINX]         = &pl_filter_function_sphinx,
+    [FILTER_BCSPLINE]       = &pl_filter_function_bcspline,
+    [FILTER_CATMULL_ROM]    = &pl_filter_function_catmull_rom,
+    [FILTER_MITCHELL]       = &pl_filter_function_mitchell,
+    [FILTER_ROBIDOUX]       = &pl_filter_function_robidoux,
+    [FILTER_ROBIDOUXSHARP]  = &pl_filter_function_robidouxsharp,
+    [FILTER_BICUBIC]        = &pl_filter_function_bicubic,
+    [FILTER_SPLINE16]       = &pl_filter_function_spline16,
+    [FILTER_SPLINE36]       = &pl_filter_function_spline36,
+    [FILTER_SPLINE64]       = &pl_filter_function_spline64,
+};
+
+#define KERNEL_TEXT "Kernel function"
+#define KERNEL_LONGTEXT "Main function defining the filter kernel."
+
+#define WINDOW_TEXT "Window function"
+#define WINDOW_LONGTEXT "Window the kernel by an additional function. (Optional)"
+
+#define CLAMP_TEXT "Clamping coefficient"
+#define CLAMP_LONGTEXT "If 1.0, clamp the kernel to only allow non-negative coefficients. If 0.0, no clamping is performed. Values in between are linear."
+
+#define BLUR_TEXT "Blur/Sharpen coefficient"
+#define BLUR_LONGTEXT "If 1.0, no change is performed. Values below 1.0 sharpen/narrow the kernel, values above 1.0 blur/widen the kernel. Avoid setting too low values!"
+
+#define TAPER_TEXT "Taper width"
+#define TAPER_LONGTEXT "Taper the kernel - all inputs within the range [0, taper] will return 1.0, and the rest of the kernel is squished into (taper, radius]."
+
+#define POLAR_TEXT "Use as EWA / Polar filter"
+#define POLAR_LONGTEXT "EWA/Polar filters are much slower but higher quality. Not all functions are good candidates. It's recommended to use jinc as the kernel."
+
+#define DEBAND_TEXT "Enable debanding"
+#define DEBAND_LONGTEXT "Turns on the debanding step. This algorithm can be further tuned with the iterations and grain options."
+
+#define DEBAND_ITER_TEXT "Debanding iterations"
+#define DEBAND_ITER_LONGTEXT "The number of debanding steps to perform per sample. Each step reduces a bit more banding, but takes time to compute. Note that the strength of each step falls off very quickly, so high numbers (>4) are practically useless. A value of 0 is a no-op."
+
+#define DEBAND_THRESH_TEXT "Gradient threshold"
+#define DEBAND_THRESH_LONGTEXT "The debanding filter's cut-off threshold. Higher numbers increase the debanding strength dramatically, but progressively diminish image details."
+
+#define DEBAND_RADIUS_TEXT "Search radius"
+#define DEBAND_RADIUS_LONGTEXT "The debanding filter's initial radius. The radius increases linearly for each iteration. A higher radius will find more gradients, but a lower radius will smooth more aggressively."
+
+#define DEBAND_GRAIN_TEXT "Grain strength"
+#define DEBAND_GRAIN_LONGTEXT "Add some extra noise to the image. This significantly helps cover up remaining quantization artifacts. Higher numbers add more noise."
+
+#define SIGMOID_TEXT "Use sigmoidization when upscaling"
+#define SIGMOID_LONGTEXT "If true, sigmoidizes the signal before upscaling. This helps prevent ringing artifacts. Not always in effect, even if enabled."
+
+#define SIGMOID_CENTER_TEXT "Sigmoid center"
+#define SIGMOID_CENTER_LONGTEXT "The center (bias) of the sigmoid curve."
+
+#define SIGMOID_SLOPE_TEXT "Sigmoid slope"
+#define SIGMOID_SLOPE_LONGTEXT "The slope (steepness) of the sigmoid curve."
+
+#define POLAR_CUTOFF_TEXT "Cut-off value for polar samplers"
+#define POLAR_CUTOFF_LONGTEXT "As a micro-optimization, all samples with a weight below this value will be ignored. This reduces the need to perform unnecessary work that doesn't noticeably change the resulting image. Setting it to a value of 0.0 disables this optimization."
+
+#define SKIP_AA_TEXT "Disable anti-aliasing when downscaling"
+#define SKIP_AA_LONGTEXT "This will result in moiré artifacts and nasty, jagged pixels when downscaling, except for some very limited special cases (e.g. bilinear downsampling to exactly 0.5x). Significantly speeds up downscaling with high downscaling ratios."
+
+#define OVERLAY_DIRECT_TEXT "Force GPU built-in sampling for overlay textures"
+#define OVERLAY_DIRECT_LONGTEXT "Normally, the configured up/downscalers will be used when overlay textures (such as subtitles) need to be scaled up or down. Enabling this option overrides this behavior and forces overlay textures to go through the GPU's built-in sampling instead (typically bilinear)."
+
+#define DISABLE_LINEAR_TEXT "Don't linearize before scaling"
+#define DISABLE_LINEAR_LONGTEXT "Normally, the image is converted to linear light before scaling (under certain conditions). Enabling this option disables this behavior."
+
+#define FORCE_GENERAL_TEXT "Force the use of general-purpose scalers"
+#define FORCE_GENERAL_LONGTEXT "Normally, certain special scalers will be replaced by faster versions instead of going through the general scaler architecture. Enabling this option disables these optimizations."
+
 #endif // VLC_PLACEBO_UTILS_H
diff --git a/modules/video_output/vulkan/display.c b/modules/video_output/vulkan/display.c
new file mode 100644
index 0000000000..58313c9773
--- /dev/null
+++ b/modules/video_output/vulkan/display.c
@@ -0,0 +1,764 @@
+/**
+ * @file display.c
+ * @brief Vulkan video output module
+ */
+/*****************************************************************************
+ * Copyright © 2018 Niklas Haas
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#include <stdlib.h>
+#include <assert.h>
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+#include <vlc_common.h>
+#include <vlc_plugin.h>
+#include <vlc_vout_display.h>
+
+#include "../placebo_utils.h"
+#include "instance.h"
+
+#include <libplacebo/renderer.h>
+#include <libplacebo/utils/upload.h>
+#include <libplacebo/swapchain.h>
+#include <libplacebo/vulkan.h>
+
+#define VLCVK_MAX_BUFFERS 128
+
+struct vout_display_sys_t
+{
+    vlc_vk_t *vk;
+    const struct pl_tex *plane_tex[4];
+    struct pl_renderer *renderer;
+    picture_pool_t *pool;
+    bool pool_dr;
+
+    // Pool of textures for the subpictures
+    struct pl_overlay *overlays;
+    const struct pl_tex **overlay_tex;
+    int num_overlays;
+
+    // Dynamic during rendering
+    vout_display_place_t place;
+    uint64_t counter;
+
+    // Mapped buffers
+    picture_t *pics[VLCVK_MAX_BUFFERS];
+    unsigned long long list; // bitset of available pictures
+
+    // Storage for rendering parameters
+    struct pl_filter_config upscaler;
+    struct pl_filter_config downscaler;
+    struct pl_deband_params deband;
+    struct pl_sigmoid_params sigmoid;
+    struct pl_color_map_params color_map;
+    struct pl_dither_params dither;
+    struct pl_render_params params;
+    struct pl_color_space target;
+    int dither_depth;
+};
+
+struct picture_sys
+{
+    vlc_vk_t *vk;
+    unsigned index;
+    const struct pl_buf *buf;
+};
+
+// Display callbacks
+static picture_pool_t *Pool(vout_display_t *, unsigned);
+static void PictureRender(vout_display_t *, picture_t *, subpicture_t *, mtime_t);
+static void PictureDisplay(vout_display_t *, picture_t *);
+static int Control(vout_display_t *, int, va_list);
+static void PollBuffers(vout_display_t *);
+static void UpdateParams(vout_display_t *);
+
+// Allocates a Vulkan surface and instance for video output.
+static int Open(vlc_object_t *obj)
+{
+    vout_display_t *vd = (vout_display_t *) obj;
+    vout_display_sys_t *sys = vd->sys = vlc_obj_calloc(obj, 1, sizeof (*sys));
+    if (unlikely(sys == NULL))
+        return VLC_ENOMEM;
+
+    vout_window_t *window = vd->cfg->window;
+    if (window == NULL)
+    {
+        msg_Err(vd, "parent window not available");
+        goto error;
+    }
+
+    sys->vk = vlc_vk_Create(window, NULL);
+    if (sys->vk == NULL)
+        goto error;
+
+    const struct pl_gpu *gpu = sys->vk->vulkan->gpu;
+    sys->renderer = pl_renderer_create(sys->vk->ctx, gpu);
+    if (!sys->renderer)
+        goto error;
+
+    // Attempt using the input format as the display format
+    if (vlc_placebo_FormatSupported(gpu, vd->source.i_chroma)) {
+        vd->fmt.i_chroma = vd->source.i_chroma;
+    } else {
+        const vlc_fourcc_t *fcc;
+        for (fcc = vlc_fourcc_GetFallback(vd->source.i_chroma); *fcc; fcc++) {
+            if (vlc_placebo_FormatSupported(gpu, *fcc)) {
+                vd->fmt.i_chroma = *fcc;
+                break;
+            }
+        }
+
+        if (!vd->fmt.i_chroma) {
+            vd->fmt.i_chroma = VLC_CODEC_RGBA;
+            msg_Warn(vd, "Failed picking any suitable input format, falling "
+                     "back to RGBA for sanity!");
+        }
+    }
+
+    // Hard-coded list of supported subtitle chromas (non-planar only!)
+    static const vlc_fourcc_t subfmts[] = {
+        VLC_CODEC_RGBA,
+        VLC_CODEC_BGRA,
+        VLC_CODEC_RGB8,
+        VLC_CODEC_RGB12,
+        VLC_CODEC_RGB15,
+        VLC_CODEC_RGB16,
+        VLC_CODEC_RGB24,
+        VLC_CODEC_RGB32,
+        VLC_CODEC_GREY,
+        0
+    };
+
+    vd->info.subpicture_chromas = subfmts;
+
+    vd->pool = Pool;
+    vd->prepare = PictureRender;
+    vd->display = PictureDisplay;
+    vd->control = Control;
+
+    UpdateParams(vd);
+    return VLC_SUCCESS;
+
+error:
+    pl_renderer_destroy(&sys->renderer);
+    if (sys->vk != NULL)
+        vlc_vk_Release(sys->vk);
+    return VLC_EGENERIC;
+}
+
+static void Close(vlc_object_t *obj)
+{
+    vout_display_t *vd = (vout_display_t *)obj;
+    vout_display_sys_t *sys = vd->sys;
+    const struct pl_gpu *gpu = sys->vk->vulkan->gpu;
+
+    for (int i = 0; i < 4; i++)
+        pl_tex_destroy(gpu, &sys->plane_tex[i]);
+    for (int i = 0; i < sys->num_overlays; i++)
+        pl_tex_destroy(gpu, &sys->overlay_tex[i]);
+
+    if (sys->overlays) {
+        free(sys->overlays);
+        free(sys->overlay_tex);
+    }
+
+    pl_renderer_destroy(&sys->renderer);
+    for (int i = 0; i < VLCVK_MAX_BUFFERS; i++) {
+        if (sys->pics[i])
+            picture_Release(sys->pics[i]);
+    }
+    if (sys->pool)
+        picture_pool_Release(sys->pool);
+
+    vlc_vk_Release(sys->vk);
+}
+
+static void DestroyPicture(picture_t *pic)
+{
+    struct picture_sys *picsys = pic->p_sys;
+    const struct pl_gpu *gpu = picsys->vk->vulkan->gpu;
+
+    pl_buf_destroy(gpu, &picsys->buf);
+    vlc_vk_Release(picsys->vk);
+    free(picsys);
+    free(pic);
+}
+
+static picture_t *CreatePicture(vout_display_t *vd)
+{
+    vout_display_sys_t *sys = vd->sys;
+    const struct pl_gpu *gpu = sys->vk->vulkan->gpu;
+
+    struct picture_sys *picsys = calloc(1, sizeof(*picsys));
+    if (unlikely(picsys == NULL))
+        return NULL;
+
+    picture_t *pic = picture_NewFromResource(&vd->fmt, &(picture_resource_t) {
+        .p_sys = picsys,
+        .pf_destroy = DestroyPicture,
+    });
+
+    if (!pic) {
+        free(picsys);
+        return NULL;
+    }
+
+    picsys->vk = sys->vk;
+    vlc_vk_Hold(picsys->vk);
+
+    // XXX: needed since picture_NewFromResource override pic planes
+    // cf. opengl display.c
+    if (picture_Setup(pic, &vd->fmt) != VLC_SUCCESS) {
+        picture_Release(pic);
+        return NULL;
+    }
+
+    size_t buf_size = 0;
+    size_t offsets[PICTURE_PLANE_MAX];
+    for (int i = 0; i < pic->i_planes; i++)
+    {
+        const plane_t *p = &pic->p[i];
+
+        if (p->i_pitch < 0 || p->i_lines <= 0 ||
+            (size_t) p->i_pitch > SIZE_MAX/p->i_lines)
+        {
+            picture_Release(pic);
+            return NULL;
+        }
+        offsets[i] = buf_size;
+        buf_size += p->i_pitch * p->i_lines;
+    }
+
+    picsys->buf = pl_buf_create(gpu, &(struct pl_buf_params) {
+        .type = PL_BUF_TEX_TRANSFER,
+        .size = buf_size,
+        .host_mapped = true,
+    });
+
+    if (!picsys->buf) {
+        picture_Release(pic);
+        return NULL;
+    }
+
+    for (int i = 0; i < pic->i_planes; ++i)
+    {
+        pic->p[i].p_pixels = (void *) &picsys->buf->data[offsets[i]];
+    }
+
+    return pic;
+}
+
+static picture_pool_t *Pool(vout_display_t *vd, unsigned requested_count)
+{
+    assert(requested_count <= VLCVK_MAX_BUFFERS);
+    vout_display_sys_t *sys = vd->sys;
+    if (sys->pool)
+        return sys->pool;
+
+    if (var_InheritBool(vd, "disable-dr"))
+        goto fallback;
+
+    unsigned count;
+    picture_t *pictures[VLCVK_MAX_BUFFERS];
+    for (count = 0; count < requested_count; count++)
+    {
+        pictures[count] = CreatePicture(vd);
+        if (!pictures[count])
+            break;
+
+        struct picture_sys *picsys = pictures[count]->p_sys;
+        picsys->index = count;
+    }
+
+    if (count <= 1)
+        goto error;
+
+    sys->pool = picture_pool_New(count, pictures);
+    if (!sys->pool)
+        goto error;
+
+    sys->pool_dr = true;
+    return sys->pool;
+
+error:
+    for (unsigned i = 0; i < count; i++) {
+        picture_Release(pictures[i]);
+        sys->pics[i] = NULL;
+    }
+
+fallback:
+    // Fallback to a regular memory pool
+    sys->pool = picture_pool_NewFromFormat(&vd->fmt, requested_count);
+    sys->pool_dr = false;
+    return sys->pool;
+}
+
+// Garbage collect all buffers that can be re-used
+static void PollBuffers(vout_display_t *vd)
+{
+    vout_display_sys_t *sys = vd->sys;
+    const struct pl_gpu *gpu = sys->vk->vulkan->gpu;
+    unsigned long long list = sys->list;
+
+    // Release all pictures that are not used by the GPU anymore
+    while (list != 0) {
+        int i = ctz(list);
+        picture_t *pic = sys->pics[i];
+        assert(pic);
+        struct picture_sys *picsys = pic->p_sys;
+        assert(picsys);
+
+        if (!pl_buf_poll(gpu, picsys->buf, 0)) {
+            sys->list &= ~(1ULL << i);
+            sys->pics[i] = NULL;
+            picture_Release(pic);
+        }
+
+        list &= ~(1ULL << i);
+    }
+}
+
+static void PictureRender(vout_display_t *vd, picture_t *pic,
+                          subpicture_t *subpicture, mtime_t date)
+{
+    VLC_UNUSED(date);
+    vout_display_sys_t *sys = vd->sys;
+    const struct pl_gpu *gpu = sys->vk->vulkan->gpu;
+    bool failed = false;
+
+    struct pl_swapchain_frame frame;
+    if (!pl_swapchain_start_frame(sys->vk->swapchain, &frame))
+        return; // Probably benign error, ignore it
+
+    struct pl_image img = {
+        .signature  = sys->counter++,
+        .num_planes = pic->i_planes,
+        .width      = pic->format.i_visible_width,
+        .height     = pic->format.i_visible_height,
+        .color      = vlc_placebo_ColorSpace(&vd->fmt),
+        .repr       = vlc_placebo_ColorRepr(&vd->fmt),
+        .src_rect = {
+            .x0 = pic->format.i_x_offset,
+            .y0 = pic->format.i_y_offset,
+            .x1 = pic->format.i_x_offset + pic->format.i_visible_width,
+            .y1 = pic->format.i_y_offset + pic->format.i_visible_height,
+        },
+    };
+
+    // Upload the image data for each plane
+    struct pl_plane_data data[4];
+    struct picture_sys *picsys = sys->pool_dr ? pic->p_sys : NULL;
+    if (!vlc_placebo_PlaneData(pic, data, picsys ? picsys->buf : NULL)) {
+        // This should never happen, in theory
+        assert(!"Failed processing the picture_t into pl_plane_data!?");
+    }
+
+    for (int i = 0; i < pic->i_planes; i++) {
+        struct pl_plane *plane = &img.planes[i];
+        if (!pl_upload_plane(gpu, plane, &sys->plane_tex[i], &data[i])) {
+            msg_Err(vd, "Failed uploading image data!");
+            failed = true;
+            goto done;
+        }
+
+        // Matches only the chroma planes, never luma or alpha
+        if (vlc_fourcc_IsYUV(vd->fmt.i_chroma) && i != 0 && i != 3) {
+            enum pl_chroma_location loc = vlc_placebo_ChromaLoc(&vd->fmt);
+            pl_chroma_location_offset(loc, &plane->shift_x, &plane->shift_y);
+        }
+    }
+
+    // If this was a mapped buffer, mark it as in use by the GPU
+    if (picsys) {
+        unsigned index = picsys->index;
+        if (sys->pics[index] == NULL) {
+            sys->list |= 1ULL << index;
+            sys->pics[index] = pic;
+            picture_Hold(pic);
+        }
+    }
+
+    // Garbage collect all previously used mapped buffers
+    PollBuffers(vd);
+
+    struct pl_render_target target;
+    pl_render_target_from_swapchain(&target, &frame);
+    target.dst_rect = (struct pl_rect2d) {
+        .x0 = sys->place.x,
+        .y0 = sys->place.y,
+        .x1 = sys->place.x + sys->place.width,
+        .y1 = sys->place.y + sys->place.height,
+    };
+
+    // Override the target colorimetry only if the user requests it
+    if (sys->target.primaries)
+        target.color.primaries = sys->target.primaries;
+    if (sys->target.transfer) {
+        target.color.transfer = sys->target.transfer;
+        target.color.light = PL_COLOR_LIGHT_UNKNOWN; // re-infer
+    }
+    if (sys->target.sig_avg > 0.0)
+        target.color.sig_avg = sys->target.sig_avg;
+    if (sys->dither_depth > 0) {
+        // override the sample depth without affecting the color encoding
+        struct pl_bit_encoding *bits = &target.repr.bits;
+        float scale = bits->color_depth / bits->sample_depth;
+        bits->sample_depth = sys->dither_depth;
+        bits->color_depth = scale * sys->dither_depth;
+    }
+
+    if (subpicture) {
+        int num_regions = 0;
+        for (subpicture_region_t *r = subpicture->p_region; r; r = r->p_next)
+            num_regions++;
+
+        // Grow the overlays array if needed
+        if (num_regions > sys->num_overlays) {
+            sys->overlays = realloc(sys->overlays, num_regions * sizeof(struct pl_overlay));
+            sys->overlay_tex = realloc(sys->overlay_tex, num_regions * sizeof(struct pl_tex *));
+            if (!sys->overlays || !sys->overlay_tex) {
+                // Unlikely OOM, just do whatever
+                sys->num_overlays = 0;
+                failed = true;
+                goto done;
+            }
+            // Clear the newly added texture pointers for pl_upload_plane
+            for (int i = sys->num_overlays; i < num_regions; i++)
+                sys->overlay_tex[i] = NULL;
+            sys->num_overlays = num_regions;
+        }
+
+        // Upload all of the regions
+        subpicture_region_t *r = subpicture->p_region;
+        for (int i = 0; i < num_regions; i++) {
+            assert(r->p_picture->i_planes == 1);
+            struct pl_plane_data subdata;
+            if (!vlc_placebo_PlaneData(r->p_picture, &subdata, NULL))
+                assert(!"Failed processing the subpicture_t into pl_plane_data!?");
+
+            struct pl_overlay *overlay = &sys->overlays[i];
+            *overlay = (struct pl_overlay) {
+                .rect = {
+                    .x0 = target.dst_rect.x0 + r->i_x,
+                    .y0 = target.dst_rect.y0 + r->i_y,
+                    .x1 = target.dst_rect.x0 + r->i_x + r->fmt.i_visible_width,
+                    .y1 = target.dst_rect.y0 + r->i_y + r->fmt.i_visible_height,
+                },
+                .mode = PL_OVERLAY_NORMAL,
+                .color = vlc_placebo_ColorSpace(&r->fmt),
+                .repr  = vlc_placebo_ColorRepr(&r->fmt),
+            };
+
+            if (!pl_upload_plane(gpu, &overlay->plane, &sys->overlay_tex[i], &subdata)) {
+                msg_Err(vd, "Failed uploading subpicture region!");
+                num_regions = i; // stop here
+                break;
+            }
+        }
+
+        // Update the target information to reference the subpictures
+        target.overlays = sys->overlays;
+        target.num_overlays = num_regions;
+    }
+
+    // If we don't cover the entire output, clear it first
+    struct pl_rect2d full = {0, 0, frame.fbo->params.w, frame.fbo->params.h };
+    if (!pl_rect2d_eq(target.dst_rect, full)) {
+        // TODO: make background color configurable?
+        pl_tex_clear(gpu, frame.fbo, (float[4]){ 0.0, 0.0, 0.0, 0.0 });
+    }
+
+    // Dispatch the actual image rendering with the pre-configured parameters
+    if (!pl_render_image(sys->renderer, &img, &target, &sys->params)) {
+        msg_Err(vd, "Failed rendering frame!");
+        failed = true;
+        goto done;
+    }
+
+done:
+
+    if (failed)
+        pl_tex_clear(gpu, frame.fbo, (float[4]){ 1.0, 0.0, 0.0, 1.0 });
+
+    if (!pl_swapchain_submit_frame(sys->vk->swapchain)) {
+        msg_Err(vd, "Failed rendering frame!");
+        return;
+    }
+}
+
+static void PictureDisplay(vout_display_t *vd, picture_t *pic)
+{
+    VLC_UNUSED(pic);
+    vout_display_sys_t *sys = vd->sys;
+    pl_swapchain_swap_buffers(sys->vk->swapchain);
+}
+
+static int Control(vout_display_t *vd, int query, va_list ap)
+{
+    vout_display_sys_t *sys = vd->sys;
+
+    switch (query)
+    {
+    case VOUT_DISPLAY_RESET_PICTURES:
+        assert(!"VOUT_DISPLAY_RESET_PICTURES");
+
+    case VOUT_DISPLAY_CHANGE_DISPLAY_SIZE:
+    case VOUT_DISPLAY_CHANGE_DISPLAY_FILLED:
+    case VOUT_DISPLAY_CHANGE_ZOOM: {
+        vout_display_cfg_t cfg = *va_arg (ap, const vout_display_cfg_t *);
+        vout_display_PlacePicture(&sys->place, &vd->source, &cfg, false);
+        return VLC_SUCCESS;
+    }
+
+    case VOUT_DISPLAY_CHANGE_SOURCE_ASPECT:
+    case VOUT_DISPLAY_CHANGE_SOURCE_CROP:
+        vout_display_PlacePicture(&sys->place, &vd->source, vd->cfg, false);
+        return VLC_SUCCESS;
+
+    default:
+        msg_Err (vd, "Unknown request %d", query);
+    }
+
+    return VLC_EGENERIC;
+}
+
+// Options
+
+#define VK_TEXT N_("Vulkan surface extension")
+#define PROVIDER_LONGTEXT N_( \
+    "Extension which provides the Vulkan surface to use.")
+
+#define DISABLE_DR_TEXT "Disable direct rendering / zero-copy upload"
+#define DISABLE_DR_LONGTEXT "Direct rendering is a technique where image data is uploaded via a mapped buffer instead of via memcpy. On some platforms this might be very slow (due to poor readback performance from mapped memory), in which cases this flag would help."
+
+vlc_module_begin () set_shortname ("Vulkan")
+    set_description (N_("Vulkan video output"))
+    set_category (CAT_VIDEO)
+    set_subcategory (SUBCAT_VIDEO_VOUT)
+    set_capability ("vout display", 0)
+    set_callbacks (Open, Close)
+    add_shortcut ("vulkan", "vk")
+    add_module ("vk", "vulkan", NULL, VK_TEXT, PROVIDER_LONGTEXT)
+
+    set_section("Scaling", NULL)
+    add_integer("upscaler-preset", SCALE_BUILTIN,
+            UPSCALER_PRESET_TEXT, SCALER_PRESET_LONGTEXT, false)
+            change_integer_list(scale_values, scale_text)
+    add_integer("downscaler-preset", SCALE_BUILTIN,
+            DOWNSCALER_PRESET_TEXT, SCALER_PRESET_LONGTEXT, false)
+            change_integer_list(scale_values, scale_text)
+    add_integer_with_range("lut-entries", 64, 16, 256,
+            LUT_ENTRIES_TEXT, LUT_ENTRIES_LONGTEXT, false)
+    add_float_with_range("antiringing", 0.0,
+            0.0, 1.0, ANTIRING_TEXT, ANTIRING_LONGTEXT, false)
+    add_bool("sigmoid", !!pl_render_default_params.sigmoid_params,
+            SIGMOID_TEXT, SIGMOID_LONGTEXT, true)
+    add_float_with_range("sigmoid-center", pl_sigmoid_default_params.center,
+            0., 1., SIGMOID_CENTER_TEXT, SIGMOID_CENTER_LONGTEXT, true)
+    add_float_with_range("sigmoid-slope", pl_sigmoid_default_params.slope,
+            1., 20., SIGMOID_SLOPE_TEXT, SIGMOID_SLOPE_LONGTEXT, true)
+
+    set_section("Debanding", NULL)
+    add_bool("debanding", false, DEBAND_TEXT, DEBAND_LONGTEXT, false)
+    add_integer("iterations", pl_deband_default_params.iterations,
+            DEBAND_ITER_TEXT, DEBAND_ITER_LONGTEXT, false)
+    add_float("threshold", pl_deband_default_params.threshold,
+            DEBAND_THRESH_TEXT, DEBAND_THRESH_LONGTEXT, false)
+    add_float("radius", pl_deband_default_params.radius,
+            DEBAND_RADIUS_TEXT, DEBAND_RADIUS_LONGTEXT, false)
+    add_float("grain", pl_deband_default_params.grain,
+            DEBAND_GRAIN_TEXT, DEBAND_GRAIN_LONGTEXT, false)
+
+    set_section("Colorspace conversion", NULL)
+    add_integer("intent", pl_color_map_default_params.intent,
+            RENDER_INTENT_TEXT, RENDER_INTENT_LONGTEXT, false)
+            change_integer_list(intent_values, intent_text)
+    add_integer("target-prim", PL_COLOR_PRIM_UNKNOWN, PRIM_TEXT, PRIM_LONGTEXT, false) \
+            change_integer_list(prim_values, prim_text) \
+    add_integer("target-trc", PL_COLOR_TRC_UNKNOWN, TRC_TEXT, TRC_LONGTEXT, false) \
+            change_integer_list(trc_values, trc_text) \
+
+    // TODO: support for ICC profiles / 3DLUTs.. we will need some way of loading
+    // this from the operating system / user
+
+    set_section("Tone mapping", NULL)
+    add_integer("tone-mapping", pl_color_map_default_params.tone_mapping_algo,
+            TONEMAPPING_TEXT, TONEMAPPING_LONGTEXT, false)
+            change_integer_list(tone_values, tone_text)
+    add_float("tone-mapping-param", pl_color_map_default_params.tone_mapping_param,
+            TONEMAP_PARAM_TEXT, TONEMAP_PARAM_LONGTEXT, true)
+    add_float("tone-mapping-desat", pl_color_map_default_params.tone_mapping_desaturate,
+            TONEMAP_DESAT_TEXT, TONEMAP_DESAT_LONGTEXT, false)
+    add_bool("gamut-warning", false, GAMUT_WARN_TEXT, GAMUT_WARN_LONGTEXT, true)
+    add_integer_with_range("peak-frames", pl_color_map_default_params.peak_detect_frames,
+            0, 255, PEAK_FRAMES_TEXT, PEAK_FRAMES_LONGTEXT, false)
+    add_float_with_range("target-avg", 0.25,
+            0.0, 1.0, TARGET_AVG_TEXT, TARGET_AVG_LONGTEXT, false)
+    add_float_with_range("scene-threshold", pl_color_map_default_params.scene_threshold,
+            0., 10., SCENE_THRESHOLD_TEXT, SCENE_THRESHOLD_LONGTEXT, false)
+
+    set_section("Dithering", NULL)
+    add_integer("dither", -1,
+            DITHER_TEXT, DITHER_LONGTEXT, false)
+            change_integer_list(dither_values, dither_text)
+    add_integer_with_range("dither-size", pl_dither_default_params.lut_size,
+            1, 8, DITHER_SIZE_TEXT, DITHER_SIZE_LONGTEXT, false)
+    add_bool("temporal-dither", pl_dither_default_params.temporal,
+            TEMPORAL_DITHER_TEXT, TEMPORAL_DITHER_LONGTEXT, false)
+    add_integer_with_range("dither-depth", 0,
+            0, 16, DITHER_DEPTH_TEXT, DITHER_DEPTH_LONGTEXT, false)
+
+    set_section("Custom upscaler (when preset = custom)", NULL)
+    add_integer("upscaler-kernel", FILTER_BOX,
+            KERNEL_TEXT, KERNEL_LONGTEXT, true)
+            change_integer_list(filter_values, filter_text)
+    add_integer("upscaler-window", FILTER_NONE,
+            WINDOW_TEXT, WINDOW_LONGTEXT, true)
+            change_integer_list(filter_values, filter_text)
+    add_bool("upscaler-polar", false, POLAR_TEXT, POLAR_LONGTEXT, true)
+    add_float_with_range("upscaler-clamp", 0.0,
+            0.0, 1.0, CLAMP_TEXT, CLAMP_LONGTEXT, true)
+    add_float_with_range("upscaler-blur", 1.0,
+            0.0, 100.0, BLUR_TEXT, BLUR_LONGTEXT, true)
+    add_float_with_range("upscaler-taper", 0.0,
+            0.0, 10.0, TAPER_TEXT, TAPER_LONGTEXT, true)
+
+    set_section("Custom downscaler (when preset = custom)", NULL)
+    add_integer("downscaler-kernel", FILTER_BOX,
+            KERNEL_TEXT, KERNEL_LONGTEXT, true)
+            change_integer_list(filter_values, filter_text)
+    add_integer("downscaler-window", FILTER_NONE,
+            WINDOW_TEXT, WINDOW_LONGTEXT, true)
+            change_integer_list(filter_values, filter_text)
+    add_bool("downscaler-polar", false, POLAR_TEXT, POLAR_LONGTEXT, true)
+    add_float_with_range("downscaler-clamp", 0.0,
+            0.0, 1.0, CLAMP_TEXT, CLAMP_LONGTEXT, true)
+    add_float_with_range("downscaler-blur", 1.0,
+            0.0, 100.0, BLUR_TEXT, BLUR_LONGTEXT, true)
+    add_float_with_range("downscaler-taper", 0.0,
+            0.0, 10.0, TAPER_TEXT, TAPER_LONGTEXT, true)
+
+    set_section("Performance tweaks / debugging", NULL)
+    add_bool("disable-dr", false, DISABLE_DR_TEXT, DISABLE_DR_LONGTEXT, false)
+    add_bool("skip-aa", false, SKIP_AA_TEXT, SKIP_AA_LONGTEXT, false)
+    add_float_with_range("polar-cutoff", 0.001,
+            0., 1., POLAR_CUTOFF_TEXT, POLAR_CUTOFF_LONGTEXT, false)
+    add_bool("overlay-direct", false, OVERLAY_DIRECT_TEXT, OVERLAY_DIRECT_LONGTEXT, false)
+    add_bool("disable-linear", false, DISABLE_LINEAR_TEXT, DISABLE_LINEAR_LONGTEXT, false)
+    add_bool("force-general", false, FORCE_GENERAL_TEXT, FORCE_GENERAL_LONGTEXT, false)
+
+vlc_module_end ()
+
+// Update the renderer settings based on the current configuration.
+//
+// XXX: This could be called every time the parameters change, but currently
+// VLC does not allow that - so we're stuck with doing it once on Open().
+// Should be changed as soon as it's possible!
+static void UpdateParams(vout_display_t *vd)
+{
+    vout_display_sys_t *sys = vd->sys;
+
+    sys->deband = pl_deband_default_params;
+    sys->deband.iterations = var_InheritInteger(vd, "iterations");
+    sys->deband.threshold = var_InheritFloat(vd, "threshold");
+    sys->deband.radius = var_InheritFloat(vd, "radius");
+    sys->deband.grain = var_InheritFloat(vd, "grain");
+    bool use_deband = sys->deband.iterations > 0 || sys->deband.grain > 0;
+    use_deband &= var_InheritBool(vd, "debanding");
+
+    sys->sigmoid = pl_sigmoid_default_params;
+    sys->sigmoid.center = var_InheritFloat(vd, "sigmoid-center");
+    sys->sigmoid.slope = var_InheritFloat(vd, "sigmoid-slope");
+    bool use_sigmoid = var_InheritBool(vd, "sigmoid");
+
+    sys->color_map = pl_color_map_default_params;
+    sys->color_map.intent = var_InheritInteger(vd, "intent");
+    sys->color_map.tone_mapping_algo = var_InheritInteger(vd, "tone-mapping");
+    sys->color_map.tone_mapping_param = var_InheritFloat(vd, "tone-mapping-param");
+    sys->color_map.tone_mapping_desaturate = var_InheritFloat(vd, "tone-mapping-desat");
+    sys->color_map.gamut_warning = var_InheritBool(vd, "gamut-warning");
+    sys->color_map.peak_detect_frames = var_InheritInteger(vd, "peak-frames");
+    sys->color_map.scene_threshold = var_InheritFloat(vd, "scene-threshold");
+
+    sys->dither = pl_dither_default_params;
+    int method = var_InheritInteger(vd, "dither");
+    bool use_dither = method >= 0;
+    sys->dither.method = use_dither ? method : 0;
+    sys->dither.lut_size = var_InheritInteger(vd, "dither-size");
+    sys->dither.temporal = var_InheritBool(vd, "temporal-dither");
+
+    sys->params = pl_render_default_params;
+    sys->params.deband_params = use_deband ? &sys->deband : NULL;
+    sys->params.sigmoid_params = use_sigmoid ? &sys->sigmoid : NULL;
+    sys->params.color_map_params = &sys->color_map;
+    sys->params.dither_params = use_dither ? &sys->dither : NULL;
+    sys->params.lut_entries = var_InheritInteger(vd, "lut-entries");
+    sys->params.antiringing_strength = var_InheritFloat(vd, "antiringing");
+    sys->params.skip_anti_aliasing = var_InheritBool(vd, "skip-aa");
+    sys->params.polar_cutoff = var_InheritFloat(vd, "polar-cutoff");
+    sys->params.disable_overlay_sampling = var_InheritBool(vd, "overlay-direct");
+    sys->params.disable_linear_scaling = var_InheritBool(vd, "disable-linear");
+    sys->params.disable_builtin_scalers = var_InheritBool(vd, "force-general");
+
+    int preset = var_InheritInteger(vd, "upscaler-preset");
+    sys->params.upscaler = scale_config[preset];
+    if (preset == SCALE_CUSTOM) {
+        sys->params.upscaler = &sys->upscaler;
+        sys->upscaler = (struct pl_filter_config) {
+            .kernel = filter_fun[var_InheritInteger(vd, "upscaler-kernel")],
+            .window = filter_fun[var_InheritInteger(vd, "upscaler-window")],
+            .clamp  = var_InheritFloat(vd, "upscaler-clamp"),
+            .blur   = var_InheritFloat(vd, "upscaler-blur"),
+            .taper  = var_InheritFloat(vd, "upscaler-taper"),
+            .polar  = var_InheritBool(vd, "upscaler-polar"),
+        };
+
+        if (!sys->upscaler.kernel) {
+            msg_Err(vd, "Tried specifying a custom upscaler with no kernel!");
+            sys->params.upscaler = NULL;
+        }
+    };
+
+    preset = var_InheritInteger(vd, "downscaler-preset");
+    sys->params.downscaler = scale_config[preset];
+    if (preset == SCALE_CUSTOM) {
+        sys->params.downscaler = &sys->downscaler;
+        sys->downscaler = (struct pl_filter_config) {
+            .kernel = filter_fun[var_InheritInteger(vd, "downscaler-kernel")],
+            .window = filter_fun[var_InheritInteger(vd, "downscaler-window")],
+            .clamp  = var_InheritFloat(vd, "downscaler-clamp"),
+            .blur   = var_InheritFloat(vd, "downscaler-blur"),
+            .taper  = var_InheritFloat(vd, "downscaler-taper"),
+            .polar  = var_InheritBool(vd, "downscaler-polar"),
+        };
+
+        if (!sys->downscaler.kernel) {
+            msg_Err(vd, "Tried specifying a custom downscaler with no kernel!");
+            sys->params.downscaler = NULL;
+        }
+    };
+
+    sys->dither_depth = var_InheritInteger(vd, "dither-depth");
+    sys->target = (struct pl_color_space) {
+        .primaries = var_InheritInteger(vd, "target-prim"),
+        .transfer = var_InheritInteger(vd, "target-trc"),
+        .sig_avg = var_InheritFloat(vd, "target-avg"),
+    };
+}
diff --git a/modules/video_output/vulkan/instance.c b/modules/video_output/vulkan/instance.c
new file mode 100644
index 0000000000..d749e0d34b
--- /dev/null
+++ b/modules/video_output/vulkan/instance.c
@@ -0,0 +1,77 @@
+/*****************************************************************************
+ * instance.c: Vulkan instance abstraction
+ *****************************************************************************
+ * Copyright (C) 2018 Niklas Haas
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#include <assert.h>
+#include <stdlib.h>
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <vlc_common.h>
+#include <vlc_modules.h>
+#include <vlc_atomic.h>
+
+#include "instance.h"
+
+/**
+ * Creates a Vulkan surface (and its underlying instance).
+ *
+ * @param wnd window to use as Vulkan surface
+ * @param name module name (or NULL for auto)
+ * @return a new context, or NULL on failure
+ */
+vlc_vk_t *vlc_vk_Create(struct vout_window_t *wnd, const char *name)
+{
+    vlc_object_t *parent = (vlc_object_t *) wnd;
+    struct vlc_vk_t *vk;
+
+    vk = vlc_object_create(parent, sizeof (*vk));
+    if (unlikely(vk == NULL))
+        return NULL;
+
+    vk->ctx = NULL;
+    vk->instance = NULL;
+    vk->surface = (VkSurfaceKHR) NULL;
+
+    vk->window = wnd;
+    vk->module = module_need(vk, "vulkan", name, true);
+    if (vk->module == NULL)
+    {
+        vlc_object_release(vk);
+        return NULL;
+    }
+    vlc_atomic_rc_init(&vk->ref_count);
+
+    return vk;
+}
+
+void vlc_vk_Hold(vlc_vk_t *vk)
+{
+    vlc_atomic_rc_inc(&vk->ref_count);
+}
+
+void vlc_vk_Release(vlc_vk_t *vk)
+{
+    if (!vlc_atomic_rc_dec(&vk->ref_count))
+        return;
+    module_unneed(vk, vk->module);
+    vlc_object_release(vk);
+}
diff --git a/modules/video_output/vulkan/instance.h b/modules/video_output/vulkan/instance.h
new file mode 100644
index 0000000000..f4830c86e8
--- /dev/null
+++ b/modules/video_output/vulkan/instance.h
@@ -0,0 +1,56 @@
+/*****************************************************************************
+ * instance.h: Vulkan instance abstraction
+ *****************************************************************************
+ * Copyright (C) 2018 Niklas Haas
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#ifndef VLC_VULKAN_INSTANCE_H
+#define VLC_VULKAN_INSTANCE_H
+
+#include <vlc_common.h>
+#include <vlc_atomic.h>
+#include <vlc_vout_window.h>
+
+#include <vulkan/vulkan.h>
+#include <libplacebo/vulkan.h>
+
+struct vout_window_t;
+struct vout_window_cfg_t;
+
+// Shared struct for vulkan instance / surface / device state
+typedef struct vlc_vk_t
+{
+    // fields internal to instance.c, should not be touched
+    struct vlc_common_members obj;
+    module_t *module;
+    vlc_atomic_rc_t ref_count;
+    void *platform_sys;
+
+    // these should be initialized by the surface module (i.e. surface.c)
+    struct pl_context *ctx;
+    const struct pl_vk_inst *instance;
+    const struct pl_vulkan *vulkan;
+    const struct pl_swapchain *swapchain;
+    VkSurfaceKHR surface;
+    struct vout_window_t *window;
+} vlc_vk_t;
+
+vlc_vk_t *vlc_vk_Create(struct vout_window_t *, const char *) VLC_USED;
+void vlc_vk_Release(vlc_vk_t *);
+void vlc_vk_Hold(vlc_vk_t *);
+
+#endif // VLC_VULKAN_INSTANCE_H
diff --git a/modules/video_output/vulkan/platform.h b/modules/video_output/vulkan/platform.h
new file mode 100644
index 0000000000..fb7a9923ff
--- /dev/null
+++ b/modules/video_output/vulkan/platform.h
@@ -0,0 +1,36 @@
+/*****************************************************************************
+ * platform.h: Vulkan platform-specific functions
+ *****************************************************************************
+ * Copyright (C) 2018 Niklas Haas
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#ifndef VLC_VULKAN_PLATFORM_H
+#define VLC_VULKAN_PLATFORM_H
+
+#include "instance.h"
+
+// Initializes a platform-specific context to vk->platform_sys
+int vlc_vk_InitPlatform(vlc_vk_t *);
+void vlc_vk_ClosePlatform(vlc_vk_t *);
+
+// Contains the required platform-specific instance extension
+extern const char * const vlc_vk_PlatformExt;
+
+// Create a vulkan surface to vk->surface
+int vlc_vk_CreateSurface(vlc_vk_t *);
+
+#endif // VLC_VULKAN_PLATFORM_H
diff --git a/modules/video_output/vulkan/surface.c b/modules/video_output/vulkan/surface.c
new file mode 100644
index 0000000000..64fd5d8bde
--- /dev/null
+++ b/modules/video_output/vulkan/surface.c
@@ -0,0 +1,186 @@
+/**
+ * @file surface.c
+ * @brief Vulkan platform-specific surface extension module
+ */
+/*****************************************************************************
+ * Copyright © 2018 Niklas Haas
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#include <stdlib.h>
+#include <assert.h>
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <vlc_common.h>
+#include <vlc_plugin.h>
+#include <vlc_vout_window.h>
+
+#include "../placebo_utils.h"
+#include "instance.h"
+#include "platform.h"
+
+static int Open (vlc_object_t *obj)
+{
+    vlc_vk_t *vk = (vlc_vk_t *) obj;
+
+    if (vlc_vk_InitPlatform(vk) != VLC_SUCCESS)
+        goto error;
+
+    // Initialize Vulkan instance
+    vk->ctx = vlc_placebo_Create(VLC_OBJECT(vk));
+    if (!vk->ctx)
+        goto error;
+
+    vk->instance = pl_vk_inst_create(vk->ctx, &(struct pl_vk_inst_params) {
+        .debug = var_InheritBool(vk, "vk-debug"),
+        .extensions = (const char *[]) {
+            VK_KHR_SURFACE_EXTENSION_NAME,
+            vlc_vk_PlatformExt,
+        },
+        .num_extensions = 2,
+    });
+    if (!vk->instance)
+        goto error;
+
+    // Create the platform-specific surface object
+    if (vlc_vk_CreateSurface(vk) != VLC_SUCCESS)
+        goto error;
+
+    // Create vulkan device
+    char *device_name = var_InheritString(vk, "vk-device");
+    vk->vulkan = pl_vulkan_create(vk->ctx, &(struct pl_vulkan_params) {
+        .instance = vk->instance->instance,
+        .surface = vk->surface,
+        .device_name = device_name,
+        .allow_software = var_InheritBool(vk, "allow-sw"),
+        .async_transfer = var_InheritBool(vk, "async-xfer"),
+        .async_compute = var_InheritBool(vk, "async-comp"),
+        .queue_count = var_InheritInteger(vk, "queue-count"),
+    });
+    free(device_name);
+    if (!vk->vulkan)
+        goto error;
+
+    // Create swapchain for this surface
+    struct pl_vulkan_swapchain_params swap_params = {
+        .surface = vk->surface,
+        .present_mode = var_InheritInteger(vk, "present-mode"),
+        .swapchain_depth = var_InheritInteger(vk, "queue-depth"),
+    };
+
+    vk->swapchain = pl_vulkan_create_swapchain(vk->vulkan, &swap_params);
+    if (!vk->swapchain)
+        goto error;
+
+    return VLC_SUCCESS;
+
+error:
+    pl_swapchain_destroy(&vk->swapchain);
+    if (vk->surface)
+        vkDestroySurfaceKHR(vk->instance->instance, vk->surface, NULL);
+
+    pl_vulkan_destroy(&vk->vulkan);
+    pl_vk_inst_destroy(&vk->instance);
+    pl_context_destroy(&vk->ctx);
+    vlc_vk_ClosePlatform(vk);
+
+    return VLC_EGENERIC;
+}
+
+static void Close (vlc_object_t *obj)
+{
+    vlc_vk_t *vk = (vlc_vk_t *) obj;
+
+    pl_swapchain_destroy(&vk->swapchain);
+    vkDestroySurfaceKHR(vk->instance->instance, vk->surface, NULL);
+    pl_vulkan_destroy(&vk->vulkan);
+    pl_vk_inst_destroy(&vk->instance);
+    pl_context_destroy(&vk->ctx);
+    vlc_vk_ClosePlatform(vk);
+}
+
+#define DEBUG_TEXT "Enable API debugging"
+#define DEBUG_LONGTEXT "This loads the vulkan standard validation layers, which can help catch API usage errors. Comes at a small performance penalty."
+
+#define DEVICE_TEXT "Device name override"
+#define DEVICE_LONGTEXT "If set to something non-empty, only a device with this exact name will be used. To see a list of devices and their names, run vlc -v with this module active."
+
+#define ALLOWSW_TEXT "Allow software devices"
+#define ALLOWSW_LONGTEXT "If enabled, allow the use of software emulation devices, which are not real devices and therefore typically very slow. (This option has no effect if forcing a specific device name)"
+
+#define ASYNC_XFER_TEXT "Allow asynchronous transfer"
+#define ASYNC_XFER_LONGTEXT "Allows the use of an asynchronous transfer queue if the device has one. Typically this maps to a DMA engine, which can perform texture uploads/downloads without blocking the GPU's compute units. Highly recommended for 4K and above."
+
+#define ASYNC_COMP_TEXT "Allow asynchronous compute"
+#define ASYNC_COMP_LONGTEXT "Allows the use of dedicated compute queue families if the device has one. Sometimes these will schedule concurrent compute work better than the main graphics queue. Turn this off if you have any issues."
+
+#define QUEUE_COUNT_TEXT "Queue count"
+#define QUEUE_COUNT_LONGTEXT "How many queues to use on the device. Increasing this might improve rendering throughput for GPUs capable of concurrent scheduling. Increasing this past the driver's limit has no effect."
+
+#define QUEUE_DEPTH_TEXT "Maximum frame latency"
+#define QUEUE_DEPTH_LONGTEXT "Affects how many frames to render/present in advance. Increasing this can improve performance at the cost of latency, by allowing better pipelining between frames. May have no effect, depending on the VLC clock settings."
+
+static const int present_values[] = {
+    VK_PRESENT_MODE_IMMEDIATE_KHR,
+    VK_PRESENT_MODE_MAILBOX_KHR,
+    VK_PRESENT_MODE_FIFO_KHR,
+    VK_PRESENT_MODE_FIFO_RELAXED_KHR,
+};
+
+static const char * const present_text[] = {
+    "Immediate (non-blocking, tearing)",
+    "Mailbox (non-blocking, non-tearing)",
+    "FIFO (blocking, non-tearing)",
+    "Relaxed FIFO (blocking, tearing)",
+};
+
+#define PRESENT_MODE_TEXT "Preferred present mode"
+#define PRESENT_MODE_LONGTEXT "Which present mode to use when creating the swapchain. If the chosen mode is not supported, VLC will fall back to using FIFO."
+
+#define XSTR(x) #x
+#define STR(x) XSTR(x)
+
+vlc_module_begin ()
+    set_shortname ("Vulkan/" STR(PLATFORM_NAME))
+    set_description ("Vulkan context (" STR(PLATFORM_NAME) ")")
+    set_category (CAT_VIDEO)
+    set_subcategory (SUBCAT_VIDEO_VOUT)
+    set_capability ("vulkan", 10)
+    set_callbacks (Open, Close)
+
+    set_section("Device selection", NULL)
+    add_bool("vk-debug", false, DEBUG_TEXT, DEBUG_LONGTEXT, false)
+    add_string("vk-device", "", DEVICE_TEXT, DEVICE_LONGTEXT, false)
+    add_bool("allow-sw", pl_vulkan_default_params.allow_software,
+            ALLOWSW_TEXT, ALLOWSW_LONGTEXT, false)
+
+    set_section("Performance tuning", NULL)
+    add_bool("async-xfer", pl_vulkan_default_params.async_transfer,
+            ASYNC_XFER_TEXT, ASYNC_XFER_LONGTEXT, false)
+    add_bool("async-comp", pl_vulkan_default_params.async_compute,
+            ASYNC_COMP_TEXT, ASYNC_COMP_LONGTEXT, false)
+    add_integer_with_range("queue-count", pl_vulkan_default_params.queue_count,
+            1, 8, QUEUE_COUNT_TEXT, QUEUE_COUNT_LONGTEXT, false)
+    add_integer_with_range("queue-depth", 3,
+            1, 8, QUEUE_DEPTH_TEXT, QUEUE_DEPTH_LONGTEXT, false)
+    add_integer("present-mode", VK_PRESENT_MODE_FIFO_KHR,
+            PRESENT_MODE_TEXT, PRESENT_MODE_LONGTEXT, false)
+            change_integer_list(present_values, present_text)
+
+vlc_module_end ()
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 7bcd2ffd0e..de741c37ff 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -1171,6 +1171,7 @@ modules/video_output/macosx.m
 modules/video_output/opengl/display.c
 modules/video_output/opengl/egl.c
 modules/video_output/opengl/vout_helper.h
+modules/video_output/vulkan/display.c
 modules/video_output/win32/direct3d9.c
 modules/video_output/win32/direct3d11.c
 modules/video_output/win32/directdraw.c



More information about the vlc-commits mailing list