/*
 * GStreamer
 * Copyright (C) 2022 Matthew Waters <matthew@centricular.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

/**
 * SECTION:element-vulkanoverlaycompositor
 * @title: vulkanoverlaycompositor
 *
 * `vulkanoverlaycompositor` overlays upstream `GstVideoOverlayCompositonMeta`
 * onto the video stream.
 *
 * Since: 1.22
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <string.h>

#include "gstvulkanelements.h"
#include "vkoverlaycompositor.h"

#include "shaders/identity.vert.h"
#include "shaders/swizzle.frag.h"

GST_DEBUG_CATEGORY (gst_debug_vulkan_overlay_compositor);
#define GST_CAT_DEFAULT gst_debug_vulkan_overlay_compositor

struct vk_overlay
{
  GstVideoOverlayRectangle *rectangle;
  GstVulkanFullScreenQuad *quad;
};

static void
vk_overlay_free (struct vk_overlay *overlay)
{
  gst_video_overlay_rectangle_unref (overlay->rectangle);
  gst_clear_object (&overlay->quad);
  g_free (overlay);
}

static struct vk_overlay *
vk_overlay_new (GstVulkanQueue * queue, GstBuffer * buffer,
    GstVideoOverlayRectangle * rectangle, GstVulkanHandle * vert,
    GstVulkanHandle * frag)
{
  struct vk_overlay *overlay = g_new0 (struct vk_overlay, 1);
  GstVideoOverlayFormatFlags flags;

  memset (overlay, 0, sizeof (*overlay));

  flags = gst_video_overlay_rectangle_get_flags (rectangle);

  overlay->rectangle = gst_video_overlay_rectangle_ref (rectangle);
  overlay->quad = gst_vulkan_full_screen_quad_new (queue);
  gst_vulkan_full_screen_quad_enable_clear (overlay->quad, FALSE);
  gst_vulkan_full_screen_quad_set_shaders (overlay->quad, vert, frag);
  gst_vulkan_full_screen_quad_enable_blend (overlay->quad, TRUE);
  gst_vulkan_full_screen_quad_set_blend_operation (overlay->quad,
      VK_BLEND_OP_ADD, VK_BLEND_OP_ADD);
  if (flags & GST_VIDEO_OVERLAY_FORMAT_FLAG_PREMULTIPLIED_ALPHA) {
    gst_vulkan_full_screen_quad_set_blend_factors (overlay->quad,
        VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
        VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA);
  } else {
    gst_vulkan_full_screen_quad_set_blend_factors (overlay->quad,
        VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA,
        VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA);
  }

  return overlay;
}

struct Vertex
{
  float x, y, z;
  float s, t;
};

struct swizzle_uniforms
{
  int in_reorder_index[4];
  int out_reorder_index[4];
};

static gboolean
vk_overlay_upload (struct vk_overlay *overlay, GstVideoInfo * out_info,
    GError ** error)
{
  GstBuffer *overlay_buffer, *vk_gst_buffer = NULL;
  GstVideoMeta *vmeta;
  GstVideoInfo vinfo;
  GstVideoFrame vframe;
  GstVulkanBufferMemory *buf_mem;
  GstVulkanImageMemory *img_mem;
  GstMemory *vkbuffer = NULL, *vkimage = NULL, *vkvertices = NULL;
  GstMemory *vkuniforms = NULL;
  VkFormat vk_format;
  GstMapInfo map_info;
  GstVulkanFence *fence = NULL;
  GstVulkanCommandBuffer *cmd_buf = NULL;
  VkBufferMemoryBarrier buffer_memory_barrier;
  VkImageMemoryBarrier image_memory_barrier;
  VkBufferImageCopy region;
  VkResult err;
  struct Vertex vertices[4];
  struct swizzle_uniforms uniforms;

  overlay_buffer =
      gst_video_overlay_rectangle_get_pixels_unscaled_argb
      (overlay->rectangle, GST_VIDEO_OVERLAY_FORMAT_FLAG_NONE);

  vmeta = gst_buffer_get_video_meta (overlay_buffer);
  gst_video_info_set_format (&vinfo, vmeta->format, vmeta->width,
      vmeta->height);
  vinfo.stride[0] = vmeta->stride[0];

  if (!gst_vulkan_full_screen_quad_set_info (overlay->quad, out_info, out_info))
    goto error;

  if (!gst_video_frame_map (&vframe, &vinfo, overlay_buffer, GST_MAP_READ)) {
    g_set_error_literal (error, GST_TYPE_RESOURCE_ERROR,
        GST_RESOURCE_ERROR_READ, "Cannot map overlay buffer for reading");
    return FALSE;
  }

  vkbuffer =
      gst_vulkan_buffer_memory_alloc (overlay->quad->queue->device,
      GST_VIDEO_INFO_COMP_STRIDE (&vinfo, 0) *
      GST_VIDEO_INFO_COMP_HEIGHT (&vinfo, 0),
      VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
      VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT);
  buf_mem = (GstVulkanBufferMemory *) vkbuffer;

  if (!gst_memory_map (vkbuffer, &map_info, GST_MAP_WRITE)) {
    g_set_error_literal (error, GST_TYPE_RESOURCE_ERROR,
        GST_RESOURCE_ERROR_WRITE,
        "Cannot map staging vulkan buffer for writing");
    gst_video_frame_unmap (&vframe);
    goto error;
  }

  memcpy (map_info.data, vframe.data[0], vframe.info.size);

  gst_memory_unmap (vkbuffer, &map_info);
  gst_video_frame_unmap (&vframe);

  vk_format = gst_vulkan_format_from_video_info (&vinfo, 0);
  vkimage =
      gst_vulkan_image_memory_alloc (overlay->quad->queue->device, vk_format,
      GST_VIDEO_INFO_COMP_WIDTH (&vinfo, 0),
      GST_VIDEO_INFO_COMP_HEIGHT (&vinfo, 0),
      VK_IMAGE_TILING_OPTIMAL,
      VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT |
      VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
  img_mem = (GstVulkanImageMemory *) vkimage;

  /* *INDENT-OFF* */
  region = (VkBufferImageCopy) {
      .bufferOffset = 0,
      .bufferRowLength = GST_VIDEO_INFO_COMP_WIDTH (&vinfo, 0),
      .bufferImageHeight = GST_VIDEO_INFO_COMP_HEIGHT (&vinfo, 0),
      .imageSubresource = {
          .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
          .mipLevel = 0,
          .baseArrayLayer = 0,
          .layerCount = 1,
      },
      .imageOffset = { .x = 0, .y = 0, .z = 0, },
      .imageExtent = {
          .width = GST_VIDEO_INFO_COMP_WIDTH (&vinfo, 0),
          .height = GST_VIDEO_INFO_COMP_HEIGHT (&vinfo, 0),
          .depth = 1,
      }
  };

  buffer_memory_barrier = (VkBufferMemoryBarrier) {
      .sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER,
      .pNext = NULL,
      .srcAccessMask = buf_mem->barrier.parent.access_flags,
      .dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT,
      /* FIXME: implement exclusive transfers */
      .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
      .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
      .buffer = buf_mem->buffer,
      .offset = region.bufferOffset,
      .size = region.bufferRowLength * region.bufferImageHeight,
  };

  image_memory_barrier = (VkImageMemoryBarrier) {
      .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
      .pNext = NULL,
      .srcAccessMask = img_mem->barrier.parent.access_flags,
      .dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT,
      .oldLayout = img_mem->barrier.image_layout,
      .newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
      /* FIXME: implement exclusive transfers */
      .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
      .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
      .image = img_mem->image,
      .subresourceRange = img_mem->barrier.subresource_range,
  };
  /* *INDENT-ON* */

  if (!(cmd_buf =
          gst_vulkan_command_pool_create (overlay->quad->cmd_pool, error)))
    goto error;

  {
    /* *INDENT-OFF* */
    VkCommandBufferBeginInfo cmd_buf_info = {
        .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
        .pNext = NULL,
        .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
        .pInheritanceInfo = NULL
    };
    /* *INDENT-ON* */

    gst_vulkan_command_buffer_lock (cmd_buf);
    err = vkBeginCommandBuffer (cmd_buf->cmd, &cmd_buf_info);
    if (gst_vulkan_error_to_g_error (err, error, "vkBeginCommandBuffer") < 0) {
      gst_vulkan_command_buffer_unlock (cmd_buf);
      goto error;
    }
  }

  vkCmdPipelineBarrier (cmd_buf->cmd,
      buf_mem->barrier.parent.pipeline_stages | img_mem->barrier.
      parent.pipeline_stages, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, NULL, 1,
      &buffer_memory_barrier, 1, &image_memory_barrier);

  buf_mem->barrier.parent.pipeline_stages = VK_PIPELINE_STAGE_TRANSFER_BIT;
  buf_mem->barrier.parent.access_flags = buffer_memory_barrier.dstAccessMask;

  img_mem->barrier.parent.pipeline_stages = VK_PIPELINE_STAGE_TRANSFER_BIT;
  img_mem->barrier.parent.access_flags = image_memory_barrier.dstAccessMask;
  img_mem->barrier.image_layout = image_memory_barrier.newLayout;

  vkCmdCopyBufferToImage (cmd_buf->cmd, buf_mem->buffer, img_mem->image,
      img_mem->barrier.image_layout, 1, &region);

  err = vkEndCommandBuffer (cmd_buf->cmd);
  gst_vulkan_command_buffer_unlock (cmd_buf);
  if (gst_vulkan_error_to_g_error (err, error, "vkEndCommandBuffer") < 0) {
    goto error;
  }

  {
    VkSubmitInfo submit_info = { 0, };

    /* *INDENT-OFF* */
    submit_info = (VkSubmitInfo) {
        .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
        .pNext = NULL,
        .waitSemaphoreCount = 0,
        .pWaitSemaphores = NULL,
        .pWaitDstStageMask = NULL,
        .commandBufferCount = 1,
        .pCommandBuffers = &cmd_buf->cmd,
        .signalSemaphoreCount = 0,
        .pSignalSemaphores = NULL,
    };
    /* *INDENT-ON* */

    fence =
        gst_vulkan_device_create_fence (overlay->quad->queue->device, error);
    if (!fence)
      goto error;

    gst_vulkan_queue_submit_lock (overlay->quad->queue);
    err =
        vkQueueSubmit (overlay->quad->queue->queue, 1, &submit_info,
        GST_VULKAN_FENCE_FENCE (fence));
    gst_vulkan_queue_submit_unlock (overlay->quad->queue);
    if (gst_vulkan_error_to_g_error (err, error, "vkQueueSubmit") < 0)
      goto error;

    gst_vulkan_trash_list_add (overlay->quad->trash_list,
        gst_vulkan_trash_list_acquire (overlay->quad->trash_list, fence,
            gst_vulkan_trash_mini_object_unref,
            GST_MINI_OBJECT_CAST (cmd_buf)));
    cmd_buf = NULL;
    gst_vulkan_trash_list_add (overlay->quad->trash_list,
        gst_vulkan_trash_list_acquire (overlay->quad->trash_list, fence,
            gst_vulkan_trash_mini_object_unref,
            GST_MINI_OBJECT_CAST (vkbuffer)));
    vkbuffer = NULL;
    gst_vulkan_trash_list_add (overlay->quad->trash_list,
        gst_vulkan_trash_list_acquire (overlay->quad->trash_list, fence,
            gst_vulkan_trash_mini_object_unref,
            GST_MINI_OBJECT_CAST (gst_memory_ref (vkimage))));
    gst_vulkan_trash_list_gc (overlay->quad->trash_list);
    gst_vulkan_fence_unref (fence);
    fence = NULL;
  }

  vk_gst_buffer = gst_buffer_new ();
  gst_buffer_append_memory (vk_gst_buffer, vkimage);
  vkimage = NULL;

  if (!gst_vulkan_full_screen_quad_set_input_buffer (overlay->quad,
          vk_gst_buffer, error))
    goto error;

  gst_clear_buffer (&vk_gst_buffer);

  {
    int xpos, ypos;
    guint width, height, out_width, out_height;
    float xl, xr, yt, yb;

    if (!gst_video_overlay_rectangle_get_render_rectangle (overlay->rectangle,
            &xpos, &ypos, &width, &height))
      goto error;

    out_width = GST_VIDEO_INFO_WIDTH (out_info);
    out_height = GST_VIDEO_INFO_HEIGHT (out_info);

    xl = 2.0 * (float) xpos / (float) out_width - 1.0;
    yt = 2.0 * (float) ypos / (float) out_height - 1.0;
    xr = xl + 2.0 * (float) width / (float) out_width;
    yb = yt + 2.0 * (float) height / (float) out_height;

    GST_LOG_OBJECT (overlay->quad, "rectangle %ux%u+%d,%d placed in %ux%u at "
        "%fx%f+%f,%f", width, height, xpos, ypos, out_width, out_height,
        xr - xl, yb - yt, xl, yt);

    /* top-left */
    vertices[0].x = xl;
    vertices[0].y = yt;
    vertices[0].z = 0.0;
    vertices[0].s = 0.0;
    vertices[0].t = 0.0;
    /* top-right */
    vertices[1].x = xr;
    vertices[1].y = yt;
    vertices[1].z = 0.0;
    vertices[1].s = 1.0;
    vertices[1].t = 0.0;
    /* bottom-right */
    vertices[2].x = xr;
    vertices[2].y = yb;
    vertices[2].z = 0.0;
    vertices[2].s = 1.0;
    vertices[2].t = 1.0;
    /* bottom-left */
    vertices[3].x = xl;
    vertices[3].y = yb;
    vertices[3].z = 0.0;
    vertices[3].s = 0.0;
    vertices[3].t = 1.0;
  }

  vkvertices =
      gst_vulkan_buffer_memory_alloc (overlay->quad->queue->device,
      sizeof (vertices),
      VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT,
      VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
      VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);

  if (!gst_memory_map (vkvertices, &map_info, GST_MAP_WRITE))
    goto error;
  memcpy (map_info.data, vertices, sizeof (vertices));
  gst_memory_unmap (vkvertices, &map_info);

  if (!gst_vulkan_full_screen_quad_set_vertex_buffer (overlay->quad,
          vkvertices, error))
    goto error;

  gst_clear_mini_object ((GstMiniObject **) & vkvertices);

  uniforms.in_reorder_index[0] = 0;
  uniforms.in_reorder_index[1] = 1;
  uniforms.in_reorder_index[2] = 2;
  uniforms.in_reorder_index[3] = 3;
  uniforms.out_reorder_index[0] = 0;
  uniforms.out_reorder_index[1] = 1;
  uniforms.out_reorder_index[2] = 2;
  uniforms.out_reorder_index[3] = 3;

  vkuniforms =
      gst_vulkan_buffer_memory_alloc (overlay->quad->queue->device,
      sizeof (uniforms),
      VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
      VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
      VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);

  if (!gst_memory_map (vkuniforms, &map_info, GST_MAP_WRITE))
    goto error;
  memcpy (map_info.data, &uniforms, sizeof (uniforms));
  gst_memory_unmap (vkuniforms, &map_info);

  if (!gst_vulkan_full_screen_quad_set_uniform_buffer (overlay->quad,
          vkuniforms, error))
    goto error;
  gst_clear_mini_object ((GstMiniObject **) & vkuniforms);

  return TRUE;

error:
  gst_clear_mini_object ((GstMiniObject **) & vkimage);
  gst_clear_mini_object ((GstMiniObject **) & vkbuffer);
  gst_clear_mini_object ((GstMiniObject **) & vkvertices);
  gst_clear_mini_object ((GstMiniObject **) & vkuniforms);
  if (cmd_buf)
    gst_vulkan_command_buffer_unref (cmd_buf);
  if (fence)
    gst_vulkan_fence_unref (fence);
  gst_clear_buffer (&vk_gst_buffer);
  gst_clear_buffer (&overlay_buffer);

  return FALSE;
}

static gboolean gst_vulkan_overlay_compositor_start (GstBaseTransform * bt);
static gboolean gst_vulkan_overlay_compositor_stop (GstBaseTransform * bt);
static GstCaps *gst_vulkan_overlay_compositor_transform_caps (GstBaseTransform *
    bt, GstPadDirection direction, GstCaps * caps, GstCaps * filter);
static gboolean gst_vulkan_overlay_compositor_set_caps (GstBaseTransform *
    bt, GstCaps * incaps, GstCaps * outcaps);
static GstFlowReturn
gst_vulkan_overlay_compositor_transform_ip (GstBaseTransform * bt,
    GstBuffer * inbuf);

#define IMAGE_FORMATS " { BGRA, RGBA }"

static GstStaticPadTemplate gst_vulkan_sink_template =
    GST_STATIC_PAD_TEMPLATE ("sink",
    GST_PAD_SINK,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE_WITH_FEATURES
        (GST_CAPS_FEATURE_MEMORY_VULKAN_IMAGE ","
            GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION,
            IMAGE_FORMATS) "; "
        GST_VIDEO_CAPS_MAKE_WITH_FEATURES (GST_CAPS_FEATURE_MEMORY_VULKAN_IMAGE,
            IMAGE_FORMATS) "; " GST_VIDEO_CAPS_MAKE_WITH_FEATURES ("ANY",
            IMAGE_FORMATS)));

static GstStaticPadTemplate gst_vulkan_src_template =
    GST_STATIC_PAD_TEMPLATE ("src",
    GST_PAD_SRC,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE_WITH_FEATURES
        (GST_CAPS_FEATURE_MEMORY_VULKAN_IMAGE ","
            GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION,
            IMAGE_FORMATS) "; "
        GST_VIDEO_CAPS_MAKE_WITH_FEATURES (GST_CAPS_FEATURE_MEMORY_VULKAN_IMAGE,
            IMAGE_FORMATS) "; " GST_VIDEO_CAPS_MAKE_WITH_FEATURES ("ANY",
            IMAGE_FORMATS)));

enum
{
  PROP_0,
};

typedef struct _GstVulkanOverlayCompositor GstVulkanOverlayCompositor;

struct _GstVulkanOverlayCompositor
{
  GstVulkanVideoFilter parent;

  GstVulkanHandle *vert;
  GstVulkanHandle *frag;
  GQueue overlays;

  gboolean render_overlays;
};

#define gst_vulkan_overlay_compositor_parent_class parent_class
G_DEFINE_TYPE_WITH_CODE (GstVulkanOverlayCompositor,
    gst_vulkan_overlay_compositor, GST_TYPE_VULKAN_VIDEO_FILTER,
    GST_DEBUG_CATEGORY_INIT (gst_debug_vulkan_overlay_compositor,
        "vulkanoverlaycompositor", 0, "Vulkan Overlay Compositor"));
GST_ELEMENT_REGISTER_DEFINE_WITH_CODE (vulkanoverlaycompositor,
    "vulkanoverlaycompositor", GST_RANK_NONE,
    GST_TYPE_VULKAN_OVERLAY_COMPOSITOR, vulkan_element_init (plugin));

static void
gst_vulkan_overlay_compositor_class_init (GstVulkanOverlayCompositorClass *
    klass)
{
  GstElementClass *gstelement_class;
  GstBaseTransformClass *gstbasetransform_class;

  gstelement_class = (GstElementClass *) klass;
  gstbasetransform_class = (GstBaseTransformClass *) klass;

  gst_element_class_set_static_metadata (gstelement_class,
      "Vulkan Overlay Compositor", "Filter/Video",
      "Vulkan Overlay Composition element",
      "Matthew Waters <matthew@centricular.com>");

  gst_element_class_add_static_pad_template (gstelement_class,
      &gst_vulkan_sink_template);
  gst_element_class_add_static_pad_template (gstelement_class,
      &gst_vulkan_src_template);

  gstbasetransform_class->start =
      GST_DEBUG_FUNCPTR (gst_vulkan_overlay_compositor_start);
  gstbasetransform_class->stop =
      GST_DEBUG_FUNCPTR (gst_vulkan_overlay_compositor_stop);
  gstbasetransform_class->transform_caps =
      GST_DEBUG_FUNCPTR (gst_vulkan_overlay_compositor_transform_caps);
  gstbasetransform_class->set_caps =
      GST_DEBUG_FUNCPTR (gst_vulkan_overlay_compositor_set_caps);
  gstbasetransform_class->transform_ip =
      GST_DEBUG_FUNCPTR (gst_vulkan_overlay_compositor_transform_ip);
}

static void
gst_vulkan_overlay_compositor_init (GstVulkanOverlayCompositor * vk_overlay)
{
}

static gboolean
gst_vulkan_overlay_compositor_start (GstBaseTransform * bt)
{
  GstVulkanOverlayCompositor *vk_overlay = GST_VULKAN_OVERLAY_COMPOSITOR (bt);
  GstVulkanVideoFilter *vfilter = GST_VULKAN_VIDEO_FILTER (vk_overlay);
  GError *error = NULL;

  if (!GST_BASE_TRANSFORM_CLASS (parent_class)->start (bt))
    return FALSE;

  if (!(vk_overlay->vert =
          gst_vulkan_create_shader (vfilter->device, identity_vert,
              identity_vert_size, &error)))
    goto error;
  if (!(vk_overlay->frag =
          gst_vulkan_create_shader (vfilter->device, swizzle_frag,
              swizzle_frag_size, &error))) {
    gst_clear_vulkan_handle (&vk_overlay->vert);
    goto error;
  }

  g_queue_init (&vk_overlay->overlays);

  return TRUE;

error:
  GST_ELEMENT_ERROR (bt, RESOURCE, NOT_FOUND, ("%s", error->message), (NULL));
  return FALSE;
}

static gboolean
gst_vulkan_overlay_compositor_stop (GstBaseTransform * bt)
{
  GstVulkanOverlayCompositor *vk_overlay = GST_VULKAN_OVERLAY_COMPOSITOR (bt);

  g_queue_clear_full (&vk_overlay->overlays, (GDestroyNotify) vk_overlay_free);

  gst_clear_vulkan_handle (&vk_overlay->vert);
  gst_clear_vulkan_handle (&vk_overlay->frag);

  return GST_BASE_TRANSFORM_CLASS (parent_class)->stop (bt);
}

static GstCaps *
gst_vulkan_overlay_compositor_transform_caps (GstBaseTransform * bt,
    GstPadDirection direction, GstCaps * caps, GstCaps * filter)
{
  GstCaps *ret;

  /* add/remove the composition overlay meta as necessary */
  if (direction == GST_PAD_SRC) {
    GstCaps *composition_caps;
    int i;

    composition_caps = gst_caps_copy (caps);

    for (i = 0; i < gst_caps_get_size (composition_caps); i++) {
      GstCapsFeatures *f = gst_caps_get_features (composition_caps, i);
      if (!gst_caps_features_is_any (f))
        gst_caps_features_add (f,
            GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION);
    }

    ret = gst_caps_merge (composition_caps, gst_caps_copy (caps));
  } else {
    guint i, n;
    GstCaps *removed;

    ret = gst_caps_copy (caps);
    removed = gst_caps_copy (caps);
    n = gst_caps_get_size (removed);
    for (i = 0; i < n; i++) {
      GstCapsFeatures *feat = gst_caps_get_features (removed, i);

      if (feat && gst_caps_features_contains (feat,
              GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION)) {
        feat = gst_caps_features_copy (feat);
        /* prefer the passthrough case */
        gst_caps_features_remove (feat,
            GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION);
        gst_caps_set_features (removed, i, feat);
      }
    }

    ret = gst_caps_merge (ret, removed);
  }

  if (filter) {
    GstCaps *tmp = gst_caps_intersect (ret, filter);
    gst_clear_caps (&ret);
    ret = tmp;
  }

  return ret;
}

static gboolean
gst_vulkan_overlay_compositor_set_caps (GstBaseTransform * bt, GstCaps * incaps,
    GstCaps * outcaps)
{
  GstVulkanOverlayCompositor *vk_overlay = GST_VULKAN_OVERLAY_COMPOSITOR (bt);
  GstCapsFeatures *in_features, *out_features;

  GST_DEBUG_OBJECT (bt, " incaps %" GST_PTR_FORMAT, incaps);
  GST_DEBUG_OBJECT (bt, "outcaps %" GST_PTR_FORMAT, outcaps);

  if (!GST_BASE_TRANSFORM_CLASS (parent_class)->set_caps (bt, incaps, outcaps))
    return FALSE;

  in_features = gst_caps_get_features (incaps, 0);
  out_features = gst_caps_get_features (outcaps, 0);

  if (gst_caps_features_contains (in_features,
          GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION) &&
      !gst_caps_features_contains (out_features,
          GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION)) {
    GST_INFO_OBJECT (bt, "caps say to render GstVideoOverlayCompositionMeta");
    vk_overlay->render_overlays = TRUE;
  } else {
    GST_INFO_OBJECT (bt,
        "caps say to not render GstVideoOverlayCompositionMeta");
    vk_overlay->render_overlays = FALSE;
  }

  return TRUE;
}

static gint
_find_overlay_cmp (gconstpointer item, gconstpointer user_data)
{
  struct vk_overlay *overlay = (struct vk_overlay *) item;
  GstVideoOverlayRectangle *rectangle = (GstVideoOverlayRectangle *) user_data;
  return overlay->rectangle == rectangle ? 0 : 1;
}

static gboolean
remove_overlay_meta_foreach (GstBuffer * buffer, GstMeta ** meta,
    gpointer user_data)
{
  if ((*meta)->info->api == GST_VIDEO_OVERLAY_COMPOSITION_META_API_TYPE)
    *meta = NULL;

  return TRUE;
}

static GstFlowReturn
gst_vulkan_overlay_compositor_transform_ip (GstBaseTransform * bt,
    GstBuffer * buffer)
{
  GstVulkanOverlayCompositor *vk_overlay = GST_VULKAN_OVERLAY_COMPOSITOR (bt);
  GError *error = NULL;

  if (!vk_overlay->render_overlays) {
    GST_LOG_OBJECT (bt,
        "caps don't say to render GstVideoOverlayCompositionMeta, passthrough");
    return GST_FLOW_OK;
  }

  /* Steal previous list of overlays */
  GList *overlays = vk_overlay->overlays.head;
  g_queue_init (&vk_overlay->overlays);

  gpointer state = NULL;
  GstMeta *meta;
  while ((meta =
          gst_buffer_iterate_meta_filtered (buffer, &state,
              GST_VIDEO_OVERLAY_COMPOSITION_META_API_TYPE)) != NULL) {
    GstVideoOverlayCompositionMeta *ometa =
        (GstVideoOverlayCompositionMeta *) meta;
    guint n = gst_video_overlay_composition_n_rectangles (ometa->overlay);

    for (int i = 0; i < n; i++) {
      GstVideoOverlayRectangle *rectangle =
          gst_video_overlay_composition_get_rectangle (ometa->overlay, i);
      struct vk_overlay *over;

      GList *l = g_list_find_custom (overlays, rectangle, _find_overlay_cmp);
      if (l == NULL) {
        over = vk_overlay_new (vk_overlay->parent.queue, buffer,
            rectangle, vk_overlay->vert, vk_overlay->frag);
        if (!vk_overlay_upload (over, &vk_overlay->parent.out_info, &error)) {
          vk_overlay_free (over);
          goto error;
        }
        g_queue_push_tail (&vk_overlay->overlays, over);
      } else {
        over = l->data;
        overlays = g_list_remove_link (overlays, l);
        g_queue_push_tail_link (&vk_overlay->overlays, l);
      }

      if (!gst_vulkan_full_screen_quad_set_output_buffer (over->quad, buffer,
              &error))
        goto error;

      if (!gst_vulkan_full_screen_quad_draw (over->quad, &error))
        goto error;
    }
  }

  /* Remove all composition metas, otherwise downstream might render them twice. */
  gst_buffer_foreach_meta (buffer, remove_overlay_meta_foreach, NULL);

  /* Free any previous overlays that are not in use anymore */
  g_list_free_full (overlays, (GDestroyNotify) vk_overlay_free);

  return GST_FLOW_OK;

error:
  GST_ELEMENT_ERROR (bt, LIBRARY, FAILED, ("%s", error->message), (NULL));
  g_clear_error (&error);
  return GST_FLOW_ERROR;
}
