/**
 * @file src/platform/linux/vulkan_encode.cpp
 * @brief Vulkan-native encoder: DMA-BUF -> Vulkan compute (RGB->YUV) -> Vulkan Video encode.
 *        No EGL/GL dependency — all GPU work stays in a single Vulkan queue.
 */
#include <array>
#include <cstdint>
#include <drm_fourcc.h>
#include <sys/stat.h>
#if defined(__FreeBSD__)
  #include <sys/types.h>
#else
  #include <sys/sysmacros.h>
#endif
#include <vector>
#include <vulkan/vulkan.h>

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/hwcontext.h>
#include <libavutil/hwcontext_vulkan.h>
}

#include "graphics.h"
#include "src/config.h"
#include "src/logging.h"
#include "src/video_colorspace.h"
#include "vulkan_encode.h"

// SPIR-V data generated at build time
static const std::vector<uint32_t> rgb2yuv_comp_spv_data
#include "shaders/rgb2yuv.spv.inc"
  ;
static const size_t rgb2yuv_comp_spv_size = rgb2yuv_comp_spv_data.size() * sizeof(uint32_t);

using namespace std::literals;

namespace vk {

  // Match a DRI render node path to a Vulkan device index via VK_EXT_physical_device_drm.
  // Returns the index as a string (e.g. "1"), or empty string if no match.
  static std::string find_vulkan_index_for_render_node(const char *render_path) {
    struct stat node_stat;
    if (stat(render_path, &node_stat) < 0) {
      return {};
    }

    auto target_major = major(node_stat.st_rdev);
    auto target_minor = minor(node_stat.st_rdev);

    VkApplicationInfo app = {VK_STRUCTURE_TYPE_APPLICATION_INFO};
    app.apiVersion = VK_API_VERSION_1_1;

    static const std::array<const char *, 1> instance_exts = {VK_EXT_PHYSICAL_DEVICE_DRM_EXTENSION_NAME};
    VkInstanceCreateInfo ci = {VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
    ci.pApplicationInfo = &app;
    ci.enabledExtensionCount = instance_exts.size();
    ci.ppEnabledExtensionNames = instance_exts.data();
    VkInstance inst = VK_NULL_HANDLE;
    if (vkCreateInstance(&ci, nullptr, &inst) != VK_SUCCESS) {
      // Retry without the extension for loaders that don't support it
      ci.enabledExtensionCount = 0;
      ci.ppEnabledExtensionNames = nullptr;
      if (vkCreateInstance(&ci, nullptr, &inst) != VK_SUCCESS) {
        return {};
      }
    }

    uint32_t count = 0;
    vkEnumeratePhysicalDevices(inst, &count, nullptr);
    std::vector<VkPhysicalDevice> devs(count);
    vkEnumeratePhysicalDevices(inst, &count, devs.data());

    std::string result;
    for (uint32_t i = 0; i < count; i++) {
      VkPhysicalDeviceDrmPropertiesEXT drm = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRM_PROPERTIES_EXT};
      VkPhysicalDeviceProperties2 props2 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2};
      props2.pNext = &drm;
      vkGetPhysicalDeviceProperties2(devs[i], &props2);
      if (drm.hasRender && drm.renderMajor == (int64_t) target_major && drm.renderMinor == (int64_t) target_minor) {
        result = std::to_string(i);
        break;
      }
    }
    vkDestroyInstance(inst, nullptr);
    return result;
  }

  static int create_vulkan_hwdevice(AVBufferRef **hw_device_buf) {
    // Resolve render device path to Vulkan device index
    if (auto render_path = platf::resolve_render_device(); render_path[0] == '/') {
      if (auto idx = find_vulkan_index_for_render_node(render_path.c_str()); !idx.empty() && av_hwdevice_ctx_create(hw_device_buf, AV_HWDEVICE_TYPE_VULKAN, idx.c_str(), nullptr, 0) >= 0) {
        return 0;
      }
    } else {
      // Non-path: treat as device name substring or numeric index
      if (av_hwdevice_ctx_create(hw_device_buf, AV_HWDEVICE_TYPE_VULKAN, render_path.c_str(), nullptr, 0) >= 0) {
        return 0;
      }
    }
    // Final fallback: let FFmpeg pick default
    if (av_hwdevice_ctx_create(hw_device_buf, AV_HWDEVICE_TYPE_VULKAN, nullptr, nullptr, 0) >= 0) {
      return 0;
    }
    return -1;
  }

  struct PushConstants {
    std::array<float, 4> color_vec_y;
    std::array<float, 4> color_vec_u;
    std::array<float, 4> color_vec_v;
    std::array<float, 2> range_y;
    std::array<float, 2> range_uv;
    std::array<int32_t, 2> src_offset;
    std::array<int32_t, 2> src_size;
    std::array<int32_t, 2> dst_size;
    std::array<int32_t, 2> cursor_pos;
    std::array<int32_t, 2> cursor_size;
    int32_t y_invert;
  };

// Helper to check VkResult
#define VK_CHECK(expr) \
  do { \
    VkResult _r = (expr); \
    if (_r != VK_SUCCESS) { \
      BOOST_LOG(error) << #expr << " failed: " << _r; \
      return -1; \
    } \
  } while (0)
#define VK_CHECK_BOOL(expr) \
  do { \
    VkResult _r = (expr); \
    if (_r != VK_SUCCESS) { \
      BOOST_LOG(error) << #expr << " failed: " << _r; \
      return false; \
    } \
  } while (0)

  class vk_vram_t: public platf::avcodec_encode_device_t {
  public:
    ~vk_vram_t() override {
      cleanup_pipeline();
    }

    int init(int in_width, int in_height, int in_offset_x = 0, int in_offset_y = 0) {
      width = in_width;
      height = in_height;
      offset_x = in_offset_x;
      offset_y = in_offset_y;
      this->data = (void *) &init_hw_device;
      return 0;
    }

    void init_codec_options(AVCodecContext *ctx, AVDictionary **options) override {
      // When VBR mode is selected (rc_mode=4), don't pin rc_min_rate to the target bitrate.
      // Having rc_min_rate == rc_max_rate == bit_rate in VBR mode prevents the encoder from
      // undershooting on simple frames, which builds up headroom that causes large overshoots
      // on complex frames.
      if (config::video.vk.rc_mode == 4) {
        ctx->rc_min_rate = 0;
      }
    }

    int set_frame(AVFrame *new_frame, AVBufferRef *hw_frames_ctx_buf) override {
      this->hwframe.reset(new_frame);
      this->frame = new_frame;
      this->hw_frames_ctx = hw_frames_ctx_buf;

      auto *frames_ctx = (AVHWFramesContext *) hw_frames_ctx_buf->data;
      auto *dev_ctx = (AVHWDeviceContext *) frames_ctx->device_ref->data;
      vk_dev.ctx = (AVVulkanDeviceContext *) dev_ctx->hwctx;
      vk_dev.dev = vk_dev.ctx->act_dev;
      vk_dev.phys_dev = vk_dev.ctx->phys_dev;
      is_10bit = (frames_ctx->sw_format == AV_PIX_FMT_P010);

      {
        VkPhysicalDeviceProperties p;
        vkGetPhysicalDeviceProperties(vk_dev.phys_dev, &p);
        BOOST_LOG(info) << "Vulkan encode using GPU: " << p.deviceName;
      }

      // Find a compute-capable queue family from FFmpeg's context
      vk_dev.compute_qf = -1;
      for (int i = 0; i < vk_dev.ctx->nb_qf; i++) {
        if (vk_dev.ctx->qf[i].flags & VK_QUEUE_COMPUTE_BIT) {
          vk_dev.compute_qf = vk_dev.ctx->qf[i].idx;
          break;
        }
      }
      if (vk_dev.compute_qf < 0) {
        BOOST_LOG(error) << "No compute queue family in Vulkan device"sv;
        return -1;
      }

      vkGetDeviceQueue(vk_dev.dev, vk_dev.compute_qf, 0, &vk_dev.compute_queue);

      // Load extension functions
      vk_dev.getMemoryFdProperties = (PFN_vkGetMemoryFdPropertiesKHR)
        vkGetDeviceProcAddr(vk_dev.dev, "vkGetMemoryFdPropertiesKHR");

      if (!create_compute_pipeline()) {
        return -1;
      }
      if (!create_command_resources()) {
        return -1;
      }

      return 0;
    }

    void apply_colorspace() override {
      auto *colors = video::color_vectors_from_colorspace(colorspace, true);
      if (colors) {
        memcpy(push.color_vec_y.data(), colors->color_vec_y, sizeof(push.color_vec_y));
        memcpy(push.color_vec_u.data(), colors->color_vec_u, sizeof(push.color_vec_u));
        memcpy(push.color_vec_v.data(), colors->color_vec_v, sizeof(push.color_vec_v));
        memcpy(push.range_y.data(), colors->range_y, sizeof(push.range_y));
        memcpy(push.range_uv.data(), colors->range_uv, sizeof(push.range_uv));
      }
    }

    void init_hwframes(AVHWFramesContext *frames) override {
      frames->initial_pool_size = 4;
      auto *vk_frames = (AVVulkanFramesContext *) frames->hwctx;
      vk_frames->tiling = VK_IMAGE_TILING_OPTIMAL;
      vk_frames->usage = (VkImageUsageFlagBits) (VK_IMAGE_USAGE_STORAGE_BIT |
                                                 VK_IMAGE_USAGE_TRANSFER_DST_BIT |
                                                 VK_IMAGE_USAGE_SAMPLED_BIT |
                                                 VK_IMAGE_USAGE_VIDEO_ENCODE_SRC_BIT_KHR);
    }

    int convert(platf::img_t &img) override {
      auto &descriptor = (egl::img_descriptor_t &) img;

      // Get encoder target frame
      if (!frame->buf[0]) {
        if (av_hwframe_get_buffer(hw_frames_ctx, frame, 0) < 0) {
          BOOST_LOG(error) << "Failed to get Vulkan frame"sv;
          return -1;
        }
      }

      // Import new DMA-BUF as VkImage when capture sequence changes
      if (descriptor.sequence == 0) {
        // Dummy frame — clear the target
        return 0;
      }

      if (descriptor.sequence > sequence) {
        sequence = descriptor.sequence;
        if (!import_dmabuf(descriptor.sd)) {
          BOOST_LOG(error) << "Failed to import DMA-BUF"sv;
          return -1;
        }
        descriptors_dirty = true;
      }

      if (src.image == VK_NULL_HANDLE) {
        return -1;
      }

      // Setup Y/UV image views for the encoder target (once)
      if (!target.views_created) {
        if (!create_target_views()) {
          return -1;
        }
        target.views_created = true;
        descriptors_dirty = true;
      }

      // Update descriptor set only when source or target changed
      if (descriptors_dirty) {
        update_descriptors();
        descriptors_dirty = false;
      }

      if (descriptor.data && descriptor.serial != cursor_serial) {
        cursor_serial = descriptor.serial;
        if (!create_cursor_image(descriptor.src_w, descriptor.src_h, descriptor.data)) {
          return -1;
        }
        update_descriptors();
        descriptors_dirty = false;
      }

      // Fill push constants
      push.src_offset[0] = offset_x;
      push.src_offset[1] = offset_y;
      push.src_size[0] = width;
      push.src_size[1] = height;
      push.dst_size[0] = frame->width;
      push.dst_size[1] = frame->height;
      push.y_invert = descriptor.y_invert ? 1 : 0;

      if (descriptor.data) {
        float scale_x = (float) frame->width / width;
        float scale_y = (float) frame->height / height;
        push.cursor_pos[0] = (int32_t) ((descriptor.x - offset_x) * scale_x);
        push.cursor_pos[1] = (int32_t) ((descriptor.y - offset_y) * scale_y);
        push.cursor_size[0] = (int32_t) (descriptor.width * scale_x);
        push.cursor_size[1] = (int32_t) (descriptor.height * scale_y);
      } else {
        push.cursor_size[0] = 0;
      }

      // Record and submit compute dispatch
      return dispatch_compute();
    }

  private:
    bool create_compute_pipeline() {
      // Shader module
      VkShaderModuleCreateInfo shader_ci = {VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
      shader_ci.codeSize = rgb2yuv_comp_spv_size;
      shader_ci.pCode = rgb2yuv_comp_spv_data.data();
      VK_CHECK_BOOL(vkCreateShaderModule(vk_dev.dev, &shader_ci, nullptr, &compute.shader_module));

      // Descriptor set layout: binding 0=sampler, 1=Y storage, 2=UV storage, 3=cursor sampler
      std::array<VkDescriptorSetLayoutBinding, 4> bindings = {};
      bindings[0] = {0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr};
      bindings[1] = {1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr};
      bindings[2] = {2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr};
      bindings[3] = {3, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr};

      VkDescriptorSetLayoutCreateInfo ds_layout_ci = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
      ds_layout_ci.bindingCount = bindings.size();
      ds_layout_ci.pBindings = bindings.data();
      VK_CHECK_BOOL(vkCreateDescriptorSetLayout(vk_dev.dev, &ds_layout_ci, nullptr, &compute.ds_layout));

      // Push constant range
      VkPushConstantRange pc_range = {VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(PushConstants)};

      VkPipelineLayoutCreateInfo pl_ci = {VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
      pl_ci.setLayoutCount = 1;
      pl_ci.pSetLayouts = &compute.ds_layout;
      pl_ci.pushConstantRangeCount = 1;
      pl_ci.pPushConstantRanges = &pc_range;
      VK_CHECK_BOOL(vkCreatePipelineLayout(vk_dev.dev, &pl_ci, nullptr, &compute.pipeline_layout));

      // Compute pipeline
      VkComputePipelineCreateInfo comp_ci = {VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
      comp_ci.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
      comp_ci.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
      comp_ci.stage.module = compute.shader_module;
      comp_ci.stage.pName = "main";
      comp_ci.layout = compute.pipeline_layout;
      VK_CHECK_BOOL(vkCreateComputePipelines(vk_dev.dev, VK_NULL_HANDLE, 1, &comp_ci, nullptr, &compute.pipeline));

      // Descriptor pool
      std::array<VkDescriptorPoolSize, 2> pool_sizes = {{
        {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 2},
        {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 2},
      }};
      VkDescriptorPoolCreateInfo pool_ci = {VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
      pool_ci.maxSets = 1;
      pool_ci.poolSizeCount = pool_sizes.size();
      pool_ci.pPoolSizes = pool_sizes.data();
      VK_CHECK_BOOL(vkCreateDescriptorPool(vk_dev.dev, &pool_ci, nullptr, &compute.desc_pool));

      VkDescriptorSetAllocateInfo alloc_info = {VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
      alloc_info.descriptorPool = compute.desc_pool;
      alloc_info.descriptorSetCount = 1;
      alloc_info.pSetLayouts = &compute.ds_layout;
      VK_CHECK_BOOL(vkAllocateDescriptorSets(vk_dev.dev, &alloc_info, &compute.desc_set));

      // Sampler for source image
      VkSamplerCreateInfo sampler_ci = {VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
      sampler_ci.magFilter = VK_FILTER_LINEAR;
      sampler_ci.minFilter = VK_FILTER_LINEAR;
      sampler_ci.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
      sampler_ci.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
      VK_CHECK_BOOL(vkCreateSampler(vk_dev.dev, &sampler_ci, nullptr, &compute.sampler));

      if (!create_cursor_image(1, 1, nullptr)) {
        return false;
      }

      return true;
    }

    bool create_command_resources() {
      VkCommandPoolCreateInfo pool_ci = {VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
      pool_ci.queueFamilyIndex = vk_dev.compute_qf;
      pool_ci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
      VK_CHECK_BOOL(vkCreateCommandPool(vk_dev.dev, &pool_ci, nullptr, &cmd.pool));

      VkCommandBufferAllocateInfo alloc_ci = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
      alloc_ci.commandPool = cmd.pool;
      alloc_ci.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
      alloc_ci.commandBufferCount = CMD_RING_SIZE;
      VK_CHECK_BOOL(vkAllocateCommandBuffers(vk_dev.dev, &alloc_ci, cmd.ring.data()));

      return true;
    }

    struct drm_format_info {
      VkFormat format;
      VkComponentMapping swizzle;
    };

    static drm_format_info drm_fourcc_to_vk_format(uint32_t fourcc) {
      static constexpr VkComponentMapping identity = {
        VK_COMPONENT_SWIZZLE_IDENTITY,
        VK_COMPONENT_SWIZZLE_IDENTITY,
        VK_COMPONENT_SWIZZLE_IDENTITY,
        VK_COMPONENT_SWIZZLE_IDENTITY,
      };
      static constexpr VkComponentMapping bgr_swap = {
        VK_COMPONENT_SWIZZLE_B,
        VK_COMPONENT_SWIZZLE_G,
        VK_COMPONENT_SWIZZLE_R,
        VK_COMPONENT_SWIZZLE_A,
      };

      switch (fourcc) {
        case DRM_FORMAT_XRGB8888:
        case DRM_FORMAT_ARGB8888:
          return {VK_FORMAT_B8G8R8A8_UNORM, identity};
        case DRM_FORMAT_XBGR8888:
        case DRM_FORMAT_ABGR8888:
          return {VK_FORMAT_R8G8B8A8_UNORM, identity};
        case DRM_FORMAT_XRGB2101010:
        case DRM_FORMAT_ARGB2101010:
          return {VK_FORMAT_A2R10G10B10_UNORM_PACK32, identity};
        case DRM_FORMAT_XBGR2101010:
        case DRM_FORMAT_ABGR2101010:
          return {VK_FORMAT_A2B10G10R10_UNORM_PACK32, identity};
        case DRM_FORMAT_XBGR16161616:
        case DRM_FORMAT_ABGR16161616:
          return {VK_FORMAT_R16G16B16A16_UNORM, identity};
        case DRM_FORMAT_XRGB16161616:
        case DRM_FORMAT_ARGB16161616:
          return {VK_FORMAT_R16G16B16A16_UNORM, bgr_swap};
        case DRM_FORMAT_XBGR16161616F:
        case DRM_FORMAT_ABGR16161616F:
          return {VK_FORMAT_R16G16B16A16_SFLOAT, identity};
        case DRM_FORMAT_XRGB16161616F:
        case DRM_FORMAT_ARGB16161616F:
          return {VK_FORMAT_R16G16B16A16_SFLOAT, bgr_swap};
        default:
          BOOST_LOG(warning) << "Unknown DRM fourcc 0x" << std::hex << fourcc << std::dec << ", assuming B8G8R8A8";
          return {VK_FORMAT_B8G8R8A8_UNORM, identity};
      }
    }

    /**
     * @brief Query the driver-expected plane count for a format+modifier pair.
     * @return Expected plane count, or 0 if unknown.
     */
    int query_modifier_plane_count(VkFormat format, uint64_t modifier) {
      VkDrmFormatModifierPropertiesListEXT mod_list = {VK_STRUCTURE_TYPE_DRM_FORMAT_MODIFIER_PROPERTIES_LIST_EXT};
      VkFormatProperties2 fmt_props2 = {VK_STRUCTURE_TYPE_FORMAT_PROPERTIES_2};
      fmt_props2.pNext = &mod_list;
      vkGetPhysicalDeviceFormatProperties2(vk_dev.phys_dev, format, &fmt_props2);
      std::vector<VkDrmFormatModifierPropertiesEXT> mod_props(mod_list.drmFormatModifierCount);
      mod_list.pDrmFormatModifierProperties = mod_props.data();
      vkGetPhysicalDeviceFormatProperties2(vk_dev.phys_dev, format, &fmt_props2);
      for (const auto &mp : mod_props) {
        if (mp.drmFormatModifier == modifier) {
          return mp.drmFormatModifierPlaneCount;
        }
      }
      return 0;
    }

    bool import_dmabuf(const egl::surface_descriptor_t &sd) {
      destroy_src_image();

      int fd = dup(sd.fds[0]);
      if (fd < 0) {
        return false;
      }

      // Query memory requirements for this DMA-BUF
      VkMemoryFdPropertiesKHR fd_props = {VK_STRUCTURE_TYPE_MEMORY_FD_PROPERTIES_KHR};
      if (vk_dev.getMemoryFdProperties) {
        vk_dev.getMemoryFdProperties(vk_dev.dev, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, fd, &fd_props);
      }

      // Create VkImage for the DMA-BUF
      VkExternalMemoryImageCreateInfo ext_ci = {VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO};
      ext_ci.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT;

      std::array<VkSubresourceLayout, 4> drm_layouts = {};
      VkImageDrmFormatModifierExplicitCreateInfoEXT drm_ci = {
        VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_EXPLICIT_CREATE_INFO_EXT
      };
      VkImageTiling tiling;

      auto [vk_format, vk_swizzle] = drm_fourcc_to_vk_format(sd.fourcc);

      if (sd.modifier != DRM_FORMAT_MOD_INVALID) {
        int dmabuf_planes = 0;
        for (int i = 0; i < 4 && sd.fds[i] >= 0; ++i) {
          dmabuf_planes++;
        }

        // Query driver for the expected plane count for this format+modifier.
        // DMA-BUF exports may include extra metadata planes (e.g. AMD DCC).
        int expected = query_modifier_plane_count(vk_format, sd.modifier);
        int plane_count = (expected > 0 && expected <= dmabuf_planes) ? expected : dmabuf_planes;

        for (int i = 0; i < plane_count; ++i) {
          drm_layouts[i].offset = sd.offsets[i];
          drm_layouts[i].rowPitch = sd.pitches[i];
        }
        drm_ci.drmFormatModifier = sd.modifier;
        drm_ci.drmFormatModifierPlaneCount = plane_count;
        drm_ci.pPlaneLayouts = drm_layouts.data();
        ext_ci.pNext = &drm_ci;
        tiling = VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT;
      } else {
        tiling = VK_IMAGE_TILING_LINEAR;
      }

      VkImageCreateInfo img_ci = {VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
      img_ci.pNext = &ext_ci;
      img_ci.imageType = VK_IMAGE_TYPE_2D;
      img_ci.format = vk_format;
      img_ci.extent = {(uint32_t) sd.width, (uint32_t) sd.height, 1};
      img_ci.mipLevels = 1;
      img_ci.arrayLayers = 1;
      img_ci.samples = VK_SAMPLE_COUNT_1_BIT;
      img_ci.tiling = tiling;
      img_ci.usage = VK_IMAGE_USAGE_SAMPLED_BIT;
      img_ci.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

      auto res = vkCreateImage(vk_dev.dev, &img_ci, nullptr, &src.image);
      if (res != VK_SUCCESS) {
        close(fd);
        BOOST_LOG(error) << "vkCreateImage for DMA-BUF failed: " << res
                         << " (modifier=0x" << std::hex << sd.modifier << std::dec
                         << ", pitch=" << sd.pitches[0] << ", offset=" << sd.offsets[0] << ")";
        return false;
      }

      // Bind imported DMA-BUF memory
      VkMemoryRequirements mem_req;
      vkGetImageMemoryRequirements(vk_dev.dev, src.image, &mem_req);

      VkImportMemoryFdInfoKHR import_fd = {VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR};
      import_fd.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT;
      import_fd.fd = fd;  // Vulkan takes ownership

      VkMemoryAllocateInfo alloc_info = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
      alloc_info.pNext = &import_fd;
      alloc_info.allocationSize = mem_req.size;
      alloc_info.memoryTypeIndex = find_memory_type(
        fd_props.memoryTypeBits ? fd_props.memoryTypeBits : mem_req.memoryTypeBits,
        VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
      );

      VkDeviceMemory src_mem = VK_NULL_HANDLE;
      res = vkAllocateMemory(vk_dev.dev, &alloc_info, nullptr, &src_mem);
      if (res != VK_SUCCESS) {
        BOOST_LOG(error) << "vkAllocateMemory for DMA-BUF failed: " << res;
        vkDestroyImage(vk_dev.dev, src.image, nullptr);
        src.image = VK_NULL_HANDLE;
        return false;
      }

      vkBindImageMemory(vk_dev.dev, src.image, src_mem, 0);

      // Create image view
      VkImageViewCreateInfo view_ci = {VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
      view_ci.image = src.image;
      view_ci.viewType = VK_IMAGE_VIEW_TYPE_2D;
      view_ci.format = vk_format;
      view_ci.components = vk_swizzle;
      view_ci.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
      VK_CHECK_BOOL(vkCreateImageView(vk_dev.dev, &view_ci, nullptr, &src.view));

      src.mem = src_mem;
      return true;
    }

    bool create_cursor_image(int w, int h, const uint8_t *pixels) {
      destroy_cursor_image();

      VkImageCreateInfo img_ci = {VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
      img_ci.imageType = VK_IMAGE_TYPE_2D;
      img_ci.format = VK_FORMAT_B8G8R8A8_UNORM;
      img_ci.extent = {(uint32_t) w, (uint32_t) h, 1};
      img_ci.mipLevels = 1;
      img_ci.arrayLayers = 1;
      img_ci.samples = VK_SAMPLE_COUNT_1_BIT;
      img_ci.tiling = VK_IMAGE_TILING_LINEAR;
      img_ci.usage = VK_IMAGE_USAGE_SAMPLED_BIT;
      img_ci.initialLayout = VK_IMAGE_LAYOUT_PREINITIALIZED;
      VK_CHECK_BOOL(vkCreateImage(vk_dev.dev, &img_ci, nullptr, &cursor.image));

      VkMemoryRequirements mem_req;
      vkGetImageMemoryRequirements(vk_dev.dev, cursor.image, &mem_req);
      VkMemoryAllocateInfo alloc = {VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
      alloc.allocationSize = mem_req.size;
      alloc.memoryTypeIndex = find_memory_type(mem_req.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
      VK_CHECK_BOOL(vkAllocateMemory(vk_dev.dev, &alloc, nullptr, &cursor.mem));
      VK_CHECK_BOOL(vkBindImageMemory(vk_dev.dev, cursor.image, cursor.mem, 0));

      if (pixels) {
        void *mapped;
        VK_CHECK_BOOL(vkMapMemory(vk_dev.dev, cursor.mem, 0, VK_WHOLE_SIZE, 0, &mapped));
        VkImageSubresource subres = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0};
        VkSubresourceLayout layout;
        vkGetImageSubresourceLayout(vk_dev.dev, cursor.image, &subres, &layout);
        for (int y = 0; y < h; y++) {
          memcpy((uint8_t *) mapped + layout.offset + y * layout.rowPitch, pixels + y * w * 4, w * 4);
        }
        vkUnmapMemory(vk_dev.dev, cursor.mem);
      }

      VkImageViewCreateInfo view_ci = {VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
      view_ci.image = cursor.image;
      view_ci.viewType = VK_IMAGE_VIEW_TYPE_2D;
      view_ci.format = VK_FORMAT_B8G8R8A8_UNORM;
      view_ci.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
      VK_CHECK_BOOL(vkCreateImageView(vk_dev.dev, &view_ci, nullptr, &cursor.view));

      cursor.needs_transition = true;
      descriptors_dirty = true;
      return true;
    }

    void destroy_cursor_image() {
      if (cursor.view) {
        vkDestroyImageView(vk_dev.dev, cursor.view, nullptr);
        cursor.view = VK_NULL_HANDLE;
      }
      if (cursor.image) {
        vkDestroyImage(vk_dev.dev, cursor.image, nullptr);
        cursor.image = VK_NULL_HANDLE;
      }
      if (cursor.mem) {
        vkFreeMemory(vk_dev.dev, cursor.mem, nullptr);
        cursor.mem = VK_NULL_HANDLE;
      }
    }

    bool create_target_views() {
      auto *vk_frame = (AVVkFrame *) frame->data[0];
      if (!vk_frame) {
        return false;
      }

      auto y_fmt = is_10bit ? VK_FORMAT_R16_UNORM : VK_FORMAT_R8_UNORM;
      auto uv_fmt = is_10bit ? VK_FORMAT_R16G16_UNORM : VK_FORMAT_R8G8_UNORM;

      // Detect multiplane vs multi-image layout
      int num_imgs = 0;
      for (int i = 0; i < AV_NUM_DATA_POINTERS && vk_frame->img[i]; i++) {
        num_imgs++;
      }

      if (num_imgs == 1) {
        // Single multiplane image — create plane views
        VkImageViewCreateInfo view_ci = {VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
        view_ci.image = vk_frame->img[0];
        view_ci.viewType = VK_IMAGE_VIEW_TYPE_2D;

        // Y plane
        view_ci.format = y_fmt;
        view_ci.subresourceRange = {VK_IMAGE_ASPECT_PLANE_0_BIT, 0, 1, 0, 1};
        VK_CHECK_BOOL(vkCreateImageView(vk_dev.dev, &view_ci, nullptr, &target.y_view));

        // UV plane
        view_ci.format = uv_fmt;
        view_ci.subresourceRange = {VK_IMAGE_ASPECT_PLANE_1_BIT, 0, 1, 0, 1};
        VK_CHECK_BOOL(vkCreateImageView(vk_dev.dev, &view_ci, nullptr, &target.uv_view));
      } else {
        // Separate images per plane
        VkImageViewCreateInfo view_ci = {VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
        view_ci.viewType = VK_IMAGE_VIEW_TYPE_2D;
        view_ci.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};

        view_ci.image = vk_frame->img[0];
        view_ci.format = y_fmt;
        VK_CHECK_BOOL(vkCreateImageView(vk_dev.dev, &view_ci, nullptr, &target.y_view));

        view_ci.image = vk_frame->img[1];
        view_ci.format = uv_fmt;
        VK_CHECK_BOOL(vkCreateImageView(vk_dev.dev, &view_ci, nullptr, &target.uv_view));
      }
      return true;
    }

    void update_descriptors() {
      VkDescriptorImageInfo src_info = {compute.sampler, src.view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL};
      VkDescriptorImageInfo y_info = {VK_NULL_HANDLE, target.y_view, VK_IMAGE_LAYOUT_GENERAL};
      VkDescriptorImageInfo uv_info = {VK_NULL_HANDLE, target.uv_view, VK_IMAGE_LAYOUT_GENERAL};
      VkDescriptorImageInfo cursor_info = {compute.sampler, cursor.view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL};

      std::array<VkWriteDescriptorSet, 4> writes = {};
      writes[0] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, compute.desc_set, 0, 0, 1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, &src_info, nullptr, nullptr};
      writes[1] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, compute.desc_set, 1, 0, 1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, &y_info, nullptr, nullptr};
      writes[2] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, compute.desc_set, 2, 0, 1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, &uv_info, nullptr, nullptr};
      writes[3] = {VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr, compute.desc_set, 3, 0, 1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, &cursor_info, nullptr, nullptr};
      vkUpdateDescriptorSets(vk_dev.dev, writes.size(), writes.data(), 0, nullptr);
    }

    int dispatch_compute() {
      auto *vk_frame = (AVVkFrame *) frame->data[0];
      int num_imgs = 0;
      for (int i = 0; i < AV_NUM_DATA_POINTERS && vk_frame->img[i]; i++) {
        num_imgs++;
      }

      // Rotate to next command buffer. With CMD_RING_SIZE slots, the buffer
      // we're about to reuse was submitted CMD_RING_SIZE frames ago.
      // At 60fps that's ~50ms for a <1ms compute dispatch — always complete.
      // No fences, no semaphore waits, no CPU blocking.
      auto cmd_buf = cmd.ring[cmd.ring_idx];
      cmd.ring_idx = (cmd.ring_idx + 1) % CMD_RING_SIZE;

      VkCommandBufferBeginInfo begin_ci = {VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
      begin_ci.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
      VK_CHECK(vkBeginCommandBuffer(cmd_buf, &begin_ci));

      // Transition source image to SHADER_READ_ONLY
      VkImageMemoryBarrier src_barrier = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
      src_barrier.srcAccessMask = 0;
      src_barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
      src_barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
      src_barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
      src_barrier.image = src.image;
      src_barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
      src_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL;
      src_barrier.dstQueueFamilyIndex = vk_dev.compute_qf;

      vkCmdPipelineBarrier(cmd_buf, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &src_barrier);

      // Transition cursor image if needed
      if (cursor.needs_transition) {
        VkImageMemoryBarrier cursor_barrier = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
        cursor_barrier.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT;
        cursor_barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
        cursor_barrier.oldLayout = VK_IMAGE_LAYOUT_PREINITIALIZED;
        cursor_barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
        cursor_barrier.image = cursor.image;
        cursor_barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
        cursor_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
        cursor_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
        vkCmdPipelineBarrier(cmd_buf, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &cursor_barrier);
        cursor.needs_transition = false;
      }

      // Transition target planes to GENERAL for storage writes
      std::array<VkImageMemoryBarrier, 2> dst_barriers = {};
      int num_dst_barriers = (num_imgs == 1) ? 1 : 2;
      for (int i = 0; i < num_dst_barriers; i++) {
        dst_barriers[i] = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
        dst_barriers[i].srcAccessMask = target.initialized ? VK_ACCESS_SHADER_READ_BIT : 0;
        dst_barriers[i].dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
        dst_barriers[i].oldLayout = target.initialized ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_UNDEFINED;
        dst_barriers[i].newLayout = VK_IMAGE_LAYOUT_GENERAL;
        dst_barriers[i].image = vk_frame->img[num_imgs == 1 ? 0 : i];
        dst_barriers[i].subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
        dst_barriers[i].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
        dst_barriers[i].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
      }

      vkCmdPipelineBarrier(cmd_buf, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0, nullptr, 0, nullptr, num_dst_barriers, dst_barriers.data());

      // Bind pipeline and dispatch
      vkCmdBindPipeline(cmd_buf, VK_PIPELINE_BIND_POINT_COMPUTE, compute.pipeline);
      vkCmdBindDescriptorSets(cmd_buf, VK_PIPELINE_BIND_POINT_COMPUTE, compute.pipeline_layout, 0, 1, &compute.desc_set, 0, nullptr);
      vkCmdPushConstants(cmd_buf, compute.pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(PushConstants), &push);

      uint32_t gx = (frame->width + 15) / 16;
      uint32_t gy = (frame->height + 15) / 16;
      vkCmdDispatch(cmd_buf, gx, gy, 1);

      VK_CHECK(vkEndCommandBuffer(cmd_buf));

      // Submit with timeline semaphore signaling for FFmpeg
      VkTimelineSemaphoreSubmitInfo timeline_info = {VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO};
      std::array<VkSemaphore, AV_NUM_DATA_POINTERS> wait_sems = {};
      std::array<VkSemaphore, AV_NUM_DATA_POINTERS> signal_sems = {};
      std::array<uint64_t, AV_NUM_DATA_POINTERS> wait_vals = {};
      std::array<uint64_t, AV_NUM_DATA_POINTERS> signal_vals = {};
      std::array<VkPipelineStageFlags, AV_NUM_DATA_POINTERS> wait_stages = {};
      int sem_count = 0;

      for (int i = 0; i < AV_NUM_DATA_POINTERS && vk_frame->sem[i]; i++) {
        wait_sems[sem_count] = vk_frame->sem[i];
        wait_vals[sem_count] = vk_frame->sem_value[i];
        wait_stages[sem_count] = VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;

        signal_sems[sem_count] = vk_frame->sem[i];
        signal_vals[sem_count] = vk_frame->sem_value[i] + 1;
        vk_frame->sem_value[i]++;
        sem_count++;
      }

      timeline_info.waitSemaphoreValueCount = sem_count;
      timeline_info.pWaitSemaphoreValues = wait_vals.data();
      timeline_info.signalSemaphoreValueCount = sem_count;
      timeline_info.pSignalSemaphoreValues = signal_vals.data();

      VkSubmitInfo submit = {VK_STRUCTURE_TYPE_SUBMIT_INFO};
      submit.pNext = &timeline_info;
      submit.waitSemaphoreCount = sem_count;
      submit.pWaitSemaphores = wait_sems.data();
      submit.pWaitDstStageMask = wait_stages.data();
      submit.commandBufferCount = 1;
      submit.pCommandBuffers = &cmd_buf;
      submit.signalSemaphoreCount = sem_count;
      submit.pSignalSemaphores = signal_sems.data();

      auto res = vkQueueSubmit(vk_dev.compute_queue, 1, &submit, VK_NULL_HANDLE);

      if (res != VK_SUCCESS) {
        BOOST_LOG(error) << "vkQueueSubmit failed: " << res;
        return -1;
      }

      // Update frame layouts for FFmpeg
      for (int i = 0; i < AV_NUM_DATA_POINTERS && vk_frame->img[i]; i++) {
        vk_frame->layout[i] = VK_IMAGE_LAYOUT_GENERAL;
        vk_frame->access[i] = VK_ACCESS_SHADER_WRITE_BIT;
        vk_frame->queue_family[i] = vk_dev.compute_qf;
      }

      target.initialized = true;

      return 0;
    }

    uint32_t find_memory_type(uint32_t type_bits, VkMemoryPropertyFlags props) {
      VkPhysicalDeviceMemoryProperties mem_props;
      vkGetPhysicalDeviceMemoryProperties(vk_dev.phys_dev, &mem_props);
      for (uint32_t i = 0; i < mem_props.memoryTypeCount; i++) {
        if ((type_bits & (1 << i)) && (mem_props.memoryTypes[i].propertyFlags & props) == props) {
          return i;
        }
      }
      // Fallback: any matching type bit
      for (uint32_t i = 0; i < mem_props.memoryTypeCount; i++) {
        if (type_bits & (1 << i)) {
          return i;
        }
      }
      return 0;
    }

    void destroy_src_image() {
      if (src.image) {
        // Defer destruction — the GPU may still be using this image.
        // By the time we wrap around (4 frames later), it's guaranteed done.
        auto &slot = defer_ring[defer_idx];
        if (slot.view) {
          vkDestroyImageView(vk_dev.dev, slot.view, nullptr);
        }
        if (slot.image) {
          vkDestroyImage(vk_dev.dev, slot.image, nullptr);
        }
        if (slot.mem) {
          vkFreeMemory(vk_dev.dev, slot.mem, nullptr);
        }
        slot = src;
        defer_idx = (defer_idx + 1) % DEFER_RING_SIZE;
      }
      src = {};
    }

    void cleanup_pipeline() {
      if (!vk_dev.dev) {
        return;
      }
      vkDeviceWaitIdle(vk_dev.dev);
      destroy_src_image();
      // Flush deferred destroys
      for (auto &slot : defer_ring) {
        if (slot.view) {
          vkDestroyImageView(vk_dev.dev, slot.view, nullptr);
        }
        if (slot.image) {
          vkDestroyImage(vk_dev.dev, slot.image, nullptr);
        }
        if (slot.mem) {
          vkFreeMemory(vk_dev.dev, slot.mem, nullptr);
        }
        slot = {};
      }
      if (target.y_view) {
        vkDestroyImageView(vk_dev.dev, target.y_view, nullptr);
      }
      if (target.uv_view) {
        vkDestroyImageView(vk_dev.dev, target.uv_view, nullptr);
      }
      destroy_cursor_image();
      if (cmd.pool) {
        vkDestroyCommandPool(vk_dev.dev, cmd.pool, nullptr);
      }
      if (compute.sampler) {
        vkDestroySampler(vk_dev.dev, compute.sampler, nullptr);
      }
      if (compute.desc_pool) {
        vkDestroyDescriptorPool(vk_dev.dev, compute.desc_pool, nullptr);
      }
      if (compute.pipeline) {
        vkDestroyPipeline(vk_dev.dev, compute.pipeline, nullptr);
      }
      if (compute.pipeline_layout) {
        vkDestroyPipelineLayout(vk_dev.dev, compute.pipeline_layout, nullptr);
      }
      if (compute.ds_layout) {
        vkDestroyDescriptorSetLayout(vk_dev.dev, compute.ds_layout, nullptr);
      }
      if (compute.shader_module) {
        vkDestroyShaderModule(vk_dev.dev, compute.shader_module, nullptr);
      }
    }

    static int init_hw_device(platf::avcodec_encode_device_t *, AVBufferRef **hw_device_buf) {
      return create_vulkan_hwdevice(hw_device_buf);
    }

    // Dimensions
    int width = 0;
    int height = 0;
    int offset_x = 0;
    int offset_y = 0;
    bool is_10bit = false;
    AVBufferRef *hw_frames_ctx = nullptr;
    frame_t hwframe;
    std::uint64_t sequence = 0;

    // Vulkan device (from FFmpeg)
    struct vk_device_t {
      VkDevice dev = VK_NULL_HANDLE;
      VkPhysicalDevice phys_dev = VK_NULL_HANDLE;
      AVVulkanDeviceContext *ctx = nullptr;
      int compute_qf = -1;
      VkQueue compute_queue = VK_NULL_HANDLE;
      PFN_vkGetMemoryFdPropertiesKHR getMemoryFdProperties = nullptr;
    };

    vk_device_t vk_dev = {};

    // Compute pipeline
    struct compute_pipeline_t {
      VkShaderModule shader_module = VK_NULL_HANDLE;
      VkDescriptorSetLayout ds_layout = VK_NULL_HANDLE;
      VkPipelineLayout pipeline_layout = VK_NULL_HANDLE;
      VkPipeline pipeline = VK_NULL_HANDLE;
      VkDescriptorPool desc_pool = VK_NULL_HANDLE;
      VkDescriptorSet desc_set = VK_NULL_HANDLE;
      VkSampler sampler = VK_NULL_HANDLE;
    };

    compute_pipeline_t compute = {};

    // Command submission — ring of buffers to avoid reuse while in-flight.
    // No CPU waits: by the time we wrap around, the old submission is long done.
    static constexpr int CMD_RING_SIZE = 3;

    struct cmd_submission_t {
      VkCommandPool pool = VK_NULL_HANDLE;
      std::array<VkCommandBuffer, CMD_RING_SIZE> ring = {};
      int ring_idx = 0;
    };

    cmd_submission_t cmd = {};

    // Source DMA-BUF image with deferred destruction
    struct src_image_t {
      VkImage image = VK_NULL_HANDLE;
      VkDeviceMemory mem = VK_NULL_HANDLE;
      VkImageView view = VK_NULL_HANDLE;
    };

    src_image_t src = {};
    static constexpr int DEFER_RING_SIZE = 4;
    std::array<src_image_t, DEFER_RING_SIZE> defer_ring = {};
    int defer_idx = 0;

    // Target NV12 plane views
    struct target_state_t {
      VkImageView y_view = VK_NULL_HANDLE;
      VkImageView uv_view = VK_NULL_HANDLE;
      bool views_created = false;
      bool initialized = false;
    };

    target_state_t target = {};

    bool descriptors_dirty = false;

    // Cursor image
    struct {
      VkImage image = VK_NULL_HANDLE;
      VkDeviceMemory mem = VK_NULL_HANDLE;
      VkImageView view = VK_NULL_HANDLE;
      bool needs_transition = false;
    } cursor = {};

    unsigned long cursor_serial = 0;

    // Push constants (color matrix)
    PushConstants push = {};
  };

  // Free functions

  int vulkan_init_avcodec_hardware_input_buffer(platf::avcodec_encode_device_t *, AVBufferRef **hw_device_buf) {
    return create_vulkan_hwdevice(hw_device_buf);
  }

  bool validate() {
    if (!avcodec_find_encoder_by_name("h264_vulkan") && !avcodec_find_encoder_by_name("hevc_vulkan")) {
      return false;
    }
    AVBufferRef *dev = nullptr;
    if (create_vulkan_hwdevice(&dev) < 0) {
      return false;
    }
    av_buffer_unref(&dev);
    return true;
  }

  std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_encode_device_vram(int w, int h, int offset_x, int offset_y) {
    auto dev = std::make_unique<vk_vram_t>();
    if (dev->init(w, h, offset_x, offset_y) < 0) {
      return nullptr;
    }
    return dev;
  }

  std::unique_ptr<platf::avcodec_encode_device_t> make_avcodec_encode_device_ram(int, int) {
    return nullptr;
  }

}  // namespace vk
