/* GStreamer
 * Copyright (C) 2024 Seungha Yang <seungha@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.
 */

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

#include "gstd3d12h264enc.h"
#include "gstd3d12encoder.h"
#include "gstd3d12dpbstorage.h"
#include "gstd3d12pluginutils.h"
#include <gst/base/gstqueuearray.h>
#include <gst/codecparsers/gsth264parser.h>
#include <gst/codecparsers/gsth264bitwriter.h>
#include <directx/d3dx12.h>
#include <wrl.h>
#include <string.h>
#include <cmath>
#include <vector>
#include <array>
#include <string>
#include <mutex>
#include <condition_variable>
#include <unordered_map>
#include <memory>

/* *INDENT-OFF* */
using namespace Microsoft::WRL;
/* *INDENT-ON* */

GST_DEBUG_CATEGORY_STATIC (gst_d3d12_h264_enc_debug);
#define GST_CAT_DEFAULT gst_d3d12_h264_enc_debug

enum
{
  PROP_0,
  PROP_RATE_CONTROL_SUPPORT,
  PROP_SLICE_MODE_SUPPORT,
  PROP_AUD,
  PROP_GOP_SIZE,
  PROP_REF_FRAMES,
  PROP_FRAME_ANALYSIS,
  PROP_RATE_CONTROL,
  PROP_BITRATE,
  PROP_MAX_BITRATE,
  PROP_QVBR_QUALITY,
  PROP_QP_INIT,
  PROP_QP_MIN,
  PROP_QP_MAX,
  PROP_QP_I,
  PROP_QP_P,
  PROP_QP_B,
  PROP_SLICE_MODE,
  PROP_SLICE_PARTITION,
  PROP_CC_INSERT,
};

#define DEFAULT_AUD TRUE
#define DEFAULT_FRAME_ANALYSIS FALSE
#define DEFAULT_GOP_SIZE 60
#define DEFAULT_RATE_CONTROL D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_VBR
#define DEFAULT_BITRATE 2000
#define DEFAULT_MAX_BITRATE 4000
#define DEFAULT_QVBR_QUALITY 23
#define DEFAULT_QP 0
#define DEFAULT_CQP 23
#define DEFAULT_REF_FRAMES 0
#define DEFAULT_SLICE_MODE D3D12_VIDEO_ENCODER_FRAME_SUBREGION_LAYOUT_MODE_FULL_FRAME
#define DEFAULT_SLICE_PARTITION 0
#define DEFAULT_CC_INSERT GST_D3D12_ENCODER_SEI_INSERT

struct GstD3D12H264EncClassData
{
  gint64 luid;
  guint device_id;
  guint vendor_id;
  gchar *description;
  GstCaps *sink_caps;
  GstCaps *src_caps;
  guint rc_support;
  guint slice_mode_support;
};

/* *INDENT-OFF* */
class GstD3D12H264EncGop
{
public:
  void Init (guint gop_length)
  {
    /* TODO: pic_order_cnt_type == 0 for b-frame  */
    gop_struct_.pic_order_cnt_type = 2;
    if (gop_length == 1)
      gop_struct_.PPicturePeriod = 0;
    else
      gop_struct_.PPicturePeriod = 1;

    /* 0 means infinite */
    if (gop_length == 0) {
      gop_struct_.GOPLength = 0;
      gop_struct_.log2_max_frame_num_minus4 = 12;
    } else {
      /* count bits */
      guint val = gop_length;
      guint num_bits = 0;
      while (val) {
        num_bits++;
        val >>= 1;
      }

      if (num_bits < 4)
        gop_struct_.log2_max_frame_num_minus4 = 0;
      else if (num_bits > 16)
        gop_struct_.log2_max_frame_num_minus4 = 12;
      else
        gop_struct_.log2_max_frame_num_minus4 = num_bits - 4;

      gop_struct_.GOPLength = gop_length;
    }

    MaxFrameNum_ = 1 << (gop_struct_.log2_max_frame_num_minus4 + 4);

    if (gop_struct_.pic_order_cnt_type == 2) {
      /* unused */
      gop_struct_.log2_max_pic_order_cnt_lsb_minus4 = 0;
      MaxPicOrderCnt_ = MaxFrameNum_ * 2;
    } else {
      guint log2_max_pic_order_cnt = gop_struct_.log2_max_frame_num_minus4 + 5;
      log2_max_pic_order_cnt = MIN (16, log2_max_pic_order_cnt);
      gop_struct_.log2_max_pic_order_cnt_lsb_minus4 = log2_max_pic_order_cnt - 4;

      MaxPicOrderCnt_ = 1 << log2_max_pic_order_cnt;
    }

    gop_start_ = true;
    frame_num_ = 0;
    encode_order_ = 0;
  }

  D3D12_VIDEO_ENCODER_SEQUENCE_GOP_STRUCTURE_H264
  GetGopStruct ()
  {
    return gop_struct_;
  }

  void
  FillPicCtrl (D3D12_VIDEO_ENCODER_PICTURE_CONTROL_CODEC_DATA_H264 & pic_ctrl)
  {
    if (gop_start_) {
      pic_ctrl.FrameType = D3D12_VIDEO_ENCODER_FRAME_TYPE_H264_IDR_FRAME;
      pic_ctrl.idr_pic_id = idr_pic_id_;
      pic_ctrl.FrameDecodingOrderNumber = 0;
      pic_ctrl.PictureOrderCountNumber = 0;
      pic_ctrl.TemporalLayerIndex = 0;
      idr_pic_id_++;
      gop_start_ = false;
    } else {
      pic_ctrl.FrameType = D3D12_VIDEO_ENCODER_FRAME_TYPE_H264_P_FRAME;
      pic_ctrl.idr_pic_id = idr_pic_id_;
      pic_ctrl.FrameDecodingOrderNumber = frame_num_;
      pic_ctrl.PictureOrderCountNumber = ((guint) frame_num_) * 2;
      pic_ctrl.TemporalLayerIndex = 0;
    }

    /* And increase frame num */
    /* FIXME: frame_num = (frame_num_of_pref_ref_pic + 1) % ...  */
    frame_num_ = (frame_num_ + 1) % MaxFrameNum_;
    encode_order_++;
    if (gop_struct_.GOPLength != 0 && encode_order_ >= gop_struct_.GOPLength) {
      frame_num_ = 0;
      encode_order_ = 0;
      gop_start_ = true;
    }
  }

  void ForceKeyUnit ()
  {
    frame_num_ = 0;
    encode_order_ = 0;
    gop_start_ = true;
  }

private:
  D3D12_VIDEO_ENCODER_SEQUENCE_GOP_STRUCTURE_H264 gop_struct_ = { };
  guint16 frame_num_ = 0;
  guint16 idr_pic_id_ = 0;
  guint32 MaxFrameNum_ = 16;
  guint32 MaxPicOrderCnt_ = 16;
  guint64 encode_order_ = 0;
  bool gop_start_ = false;
};

class GstD3D12H264EncDpb
{
public:
  GstD3D12H264EncDpb (GstD3D12Device * device, UINT width, UINT height,
       UINT max_dpb_size, bool array_of_textures)
  {
    max_dpb_size_ = max_dpb_size;
    if (max_dpb_size_ > 0) {
      storage_ = gst_d3d12_dpb_storage_new (device, max_dpb_size + 1,
          array_of_textures, DXGI_FORMAT_NV12, width, height,
          D3D12_RESOURCE_FLAG_VIDEO_ENCODE_REFERENCE_ONLY |
          D3D12_RESOURCE_FLAG_DENY_SHADER_RESOURCE);
    }
  }

  ~GstD3D12H264EncDpb ()
  {
    gst_clear_object (&storage_);
  }

  bool IsValid ()
  {
    if (max_dpb_size_ > 0 && !storage_)
      return false;

    return true;
  }

  bool StartFrame (bool is_reference,
      D3D12_VIDEO_ENCODER_PICTURE_CONTROL_CODEC_DATA_H264 * ctrl_data,
      D3D12_VIDEO_ENCODER_RECONSTRUCTED_PICTURE * recon_pic,
      D3D12_VIDEO_ENCODE_REFERENCE_FRAMES * ref_frames,
      UINT64 display_order)
  {
    ctrl_data_ = *ctrl_data;
    cur_display_order_ = display_order;
    cur_frame_is_ref_ = is_reference;

    recon_pic_.pReconstructedPicture = nullptr;
    recon_pic_.ReconstructedPictureSubresource = 0;

    if (max_dpb_size_ > 0 &&
      ctrl_data_.FrameType == D3D12_VIDEO_ENCODER_FRAME_TYPE_H264_IDR_FRAME) {
      ref_pic_desc_.clear();
      ref_pic_display_order_.clear ();
      gst_d3d12_dpb_storage_clear_dpb (storage_);
    }

    if (is_reference) {
      g_assert (max_dpb_size_ > 0);
      if (!gst_d3d12_dpb_storage_acquire_frame (storage_, &recon_pic_))
        return false;
    }

    *recon_pic = recon_pic_;

    switch (ctrl_data_.FrameType) {
      case D3D12_VIDEO_ENCODER_FRAME_TYPE_H264_P_FRAME:
      case D3D12_VIDEO_ENCODER_FRAME_TYPE_H264_B_FRAME:
        g_assert (max_dpb_size_ > 0);
        gst_d3d12_dpb_storage_get_reference_frames (storage_,
            ref_frames);
        break;
      default:
        ref_frames->NumTexture2Ds = 0;
        ref_frames->ppTexture2Ds = nullptr;
        ref_frames->pSubresources = nullptr;
        break;
    }

    list0_.clear ();
    list1_.clear ();

    bool build_l0 = (ctrl_data_.FrameType ==
      D3D12_VIDEO_ENCODER_FRAME_TYPE_H264_P_FRAME) ||
      (ctrl_data_.FrameType ==
      D3D12_VIDEO_ENCODER_FRAME_TYPE_H264_B_FRAME);
    bool build_l1 = ctrl_data_.FrameType ==
        D3D12_VIDEO_ENCODER_FRAME_TYPE_H264_B_FRAME;

    if (build_l0) {
      for (UINT i = 0; i < (UINT) ref_pic_display_order_.size (); i++) {
        if (ref_pic_display_order_[i] < display_order)
          list0_.push_back (i);
      }
    }

    if (build_l1) {
      for (UINT i = 0; i < (UINT) ref_pic_display_order_.size (); i++) {
        if (ref_pic_display_order_[i] > display_order)
          list1_.push_back (i);
      }
    }

    ctrl_data->List0ReferenceFramesCount = list0_.size ();
    ctrl_data->pList0ReferenceFrames =
        list0_.empty () ? nullptr : list0_.data ();

    ctrl_data->List1ReferenceFramesCount = list1_.size ();
    ctrl_data->pList1ReferenceFrames =
        list1_.empty () ? nullptr : list1_.data ();

    ctrl_data->ReferenceFramesReconPictureDescriptorsCount =
        ref_pic_desc_.size ();
    ctrl_data->pReferenceFramesReconPictureDescriptors =
        ref_pic_desc_.empty () ? nullptr : ref_pic_desc_.data ();

    return true;
  }

  void EndFrame ()
  {
    if (!cur_frame_is_ref_ || max_dpb_size_ == 0)
      return;

    if (gst_d3d12_dpb_storage_get_dpb_size (storage_) == max_dpb_size_) {
      gst_d3d12_dpb_storage_remove_oldest_frame (storage_);
      ref_pic_display_order_.pop_back ();
      ref_pic_desc_.pop_back ();
    }

    gst_d3d12_dpb_storage_add_frame (storage_, &recon_pic_);

    D3D12_VIDEO_ENCODER_REFERENCE_PICTURE_DESCRIPTOR_H264 desc = { };
    desc.ReconstructedPictureResourceIndex = 0;
    desc.IsLongTermReference = FALSE;
    desc.PictureOrderCountNumber = ctrl_data_.PictureOrderCountNumber;
    desc.FrameDecodingOrderNumber = ctrl_data_.FrameDecodingOrderNumber;
    desc.TemporalLayerIndex = 0;

    ref_pic_display_order_.insert (ref_pic_display_order_.begin (),
        cur_display_order_);
    ref_pic_desc_.insert (ref_pic_desc_.begin(), desc);
    for (UINT i = 1; i < ref_pic_desc_.size (); i++)
      ref_pic_desc_[i].ReconstructedPictureResourceIndex = i;
  }

private:
  std::vector<D3D12_VIDEO_ENCODER_REFERENCE_PICTURE_DESCRIPTOR_H264> ref_pic_desc_;
  std::vector<UINT64> ref_pic_display_order_;
  D3D12_VIDEO_ENCODER_RECONSTRUCTED_PICTURE recon_pic_ = { };
  D3D12_VIDEO_ENCODER_PICTURE_CONTROL_CODEC_DATA_H264 ctrl_data_ = { };
  std::vector<UINT> list0_;
  std::vector<UINT> list1_;
  UINT max_dpb_size_ = 0;
  UINT64 cur_display_order_ = 0;
  bool cur_frame_is_ref_ = false;
  GstD3D12DpbStorage *storage_ = nullptr;
};

struct GstD3D12H264SPS
{
  void Clear ()
  {
    memset (&sps, 0, sizeof (GstH264SPS));
    bytes.clear ();
  }

  GstH264SPS sps;
  std::vector<guint8> bytes;
};

struct GstD3D12H264PPS
{
  void Clear ()
  {
    memset (&pps, 0, sizeof (GstH264PPS));
    bytes.clear ();
  }

  GstH264PPS pps;
  std::vector<guint8> bytes;
};

struct GstD3D12H264EncPrivate
{
  GstD3D12H264EncPrivate ()
  {
    cc_sei = g_array_new (FALSE, FALSE, sizeof (GstH264SEIMessage));
    g_array_set_clear_func (cc_sei, (GDestroyNotify) gst_h264_sei_clear);
  }

  ~GstD3D12H264EncPrivate ()
  {
    g_array_unref (cc_sei);
  }

  GstVideoInfo info;
  GstD3D12H264SPS sps;
  std::vector<GstD3D12H264PPS> pps;
  GstH264Profile selected_profile;
  GstD3D12H264EncGop gop;
  std::unique_ptr<GstD3D12H264EncDpb> dpb;
  guint last_pps_id = 0;
  guint64 display_order = 0;
  GArray *cc_sei;

  std::mutex prop_lock;

  GstD3D12EncoderConfig encoder_config = { };

  D3D12_VIDEO_ENCODER_PROFILE_H264 profile_h264 =
      D3D12_VIDEO_ENCODER_PROFILE_H264_MAIN;
  D3D12_VIDEO_ENCODER_CODEC_CONFIGURATION_H264 config_h264 = { };
  D3D12_VIDEO_ENCODER_LEVELS_H264 level_h264;
  D3D12_VIDEO_ENCODER_PICTURE_CONTROL_SUBREGIONS_LAYOUT_DATA_SLICES
      layout_slices = { };
  D3D12_VIDEO_ENCODER_SEQUENCE_GOP_STRUCTURE_H264 gop_struct_h264 = { };
  D3D12_VIDEO_ENCODER_PICTURE_CONTROL_CODEC_DATA_H264 pic_control_h264 = { };

  D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE selected_rc_mode =
      D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_ABSOLUTE_QP_MAP;
  D3D12_VIDEO_ENCODER_FRAME_SUBREGION_LAYOUT_MODE selected_slice_mode =
      DEFAULT_SLICE_MODE;
  guint selected_ref_frames = 0;
  D3D12_VIDEO_ENCODER_CODEC_PICTURE_CONTROL_SUPPORT_H264 pic_ctrl_support = { };

  /* properties */
  gboolean aud = DEFAULT_AUD;

  /* gop struct related */
  guint gop_size = DEFAULT_GOP_SIZE;
  guint ref_frames = DEFAULT_REF_FRAMES;
  gboolean gop_updated = FALSE;

  /* rate control config */
  D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE rc_mode = DEFAULT_RATE_CONTROL;
  gboolean frame_analysis = DEFAULT_FRAME_ANALYSIS;
  gboolean rc_flag_updated = FALSE;
  guint bitrate = DEFAULT_BITRATE;
  guint max_bitrate = DEFAULT_MAX_BITRATE;
  guint qvbr_quality = DEFAULT_QVBR_QUALITY;
  guint qp_init = DEFAULT_QP;
  guint qp_min = DEFAULT_QP;
  guint qp_max = DEFAULT_QP;
  guint qp_i = DEFAULT_CQP;
  guint qp_p = DEFAULT_CQP;
  guint qp_b = DEFAULT_CQP;
  gboolean rc_updated = FALSE;

  /* slice mode */
  D3D12_VIDEO_ENCODER_FRAME_SUBREGION_LAYOUT_MODE slice_mode =
      DEFAULT_SLICE_MODE;
  guint slice_partition = DEFAULT_SLICE_PARTITION;
  gboolean slice_updated = FALSE;

  GstD3D12EncoderSeiInsertMode cc_insert = DEFAULT_CC_INSERT;
};
/* *INDENT-ON* */

struct GstD3D12H264Enc
{
  GstD3D12Encoder parent;

  GstD3D12H264EncPrivate *priv;
};

struct GstD3D12H264EncClass
{
  GstD3D12EncoderClass parent_class;

  GstD3D12H264EncClassData *cdata;
};

static inline GstD3D12H264Enc *
GST_D3D12_H264_ENC (gpointer ptr)
{
  return (GstD3D12H264Enc *) ptr;
}

static inline GstD3D12H264EncClass *
GST_D3D12_H264_ENC_GET_CLASS (gpointer ptr)
{
  return G_TYPE_INSTANCE_GET_CLASS (ptr, G_TYPE_FROM_INSTANCE (ptr),
      GstD3D12H264EncClass);
}

static void gst_d3d12_h264_enc_finalize (GObject * object);
static void gst_d3d12_h264_enc_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec);
static void gst_d3d12_h264_enc_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec);

static gboolean gst_d3d12_h264_enc_start (GstVideoEncoder * encoder);
static gboolean gst_d3d12_h264_enc_stop (GstVideoEncoder * encoder);
static gboolean gst_d3d12_h264_enc_transform_meta (GstVideoEncoder * encoder,
    GstVideoCodecFrame * frame, GstMeta * meta);
static gboolean gst_d3d12_h264_enc_new_sequence (GstD3D12Encoder * encoder,
    ID3D12VideoDevice * video_device, GstVideoCodecState * state,
    GstD3D12EncoderConfig * config);
static gboolean gst_d3d12_h264_enc_start_frame (GstD3D12Encoder * encoder,
    ID3D12VideoDevice * video_device, GstVideoCodecFrame * frame,
    D3D12_VIDEO_ENCODER_SEQUENCE_CONTROL_DESC * seq_ctrl,
    D3D12_VIDEO_ENCODER_PICTURE_CONTROL_DESC * picture_ctrl,
    D3D12_VIDEO_ENCODER_RECONSTRUCTED_PICTURE * recon_pic,
    GstD3D12EncoderConfig * config, gboolean * need_new_session);
static gboolean gst_d3d12_h264_enc_end_frame (GstD3D12Encoder * encoder);

static GstElementClass *parent_class = nullptr;

static void
gst_d3d12_h264_enc_class_init (GstD3D12H264EncClass * klass, gpointer data)
{
  auto object_class = G_OBJECT_CLASS (klass);
  auto element_class = GST_ELEMENT_CLASS (klass);
  auto encoder_class = GST_VIDEO_ENCODER_CLASS (klass);
  auto d3d12enc_class = GST_D3D12_ENCODER_CLASS (klass);
  auto cdata = (GstD3D12H264EncClassData *) data;
  GstPadTemplate *pad_templ;
  auto read_only_params = (GParamFlags) (GST_PARAM_DOC_SHOW_DEFAULT |
      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
  auto rw_params = (GParamFlags) (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

  parent_class = (GstElementClass *) g_type_class_peek_parent (klass);
  klass->cdata = cdata;

  object_class->finalize = gst_d3d12_h264_enc_finalize;
  object_class->set_property = gst_d3d12_h264_enc_set_property;
  object_class->get_property = gst_d3d12_h264_enc_get_property;

  g_object_class_install_property (object_class, PROP_RATE_CONTROL_SUPPORT,
      g_param_spec_flags ("rate-control-support", "Rate Control Support",
          "Supported rate control modes",
          GST_TYPE_D3D12_ENCODER_RATE_CONTROL_SUPPORT, 0, read_only_params));

  g_object_class_install_property (object_class, PROP_SLICE_MODE_SUPPORT,
      g_param_spec_flags ("slice-mode-support", "Slice Mode Support",
          "Supported slice partition modes",
          GST_TYPE_D3D12_ENCODER_SUBREGION_LAYOUT_SUPPORT, 1,
          read_only_params));

  g_object_class_install_property (object_class, PROP_AUD,
      g_param_spec_boolean ("aud", "AUD", "Use AU delimiter", DEFAULT_AUD,
          rw_params));

  g_object_class_install_property (object_class, PROP_GOP_SIZE,
      g_param_spec_uint ("gop-size", "GOP Size", "Size of GOP (0 = infinite)",
          0, G_MAXUINT32, DEFAULT_GOP_SIZE, rw_params));

  g_object_class_install_property (object_class, PROP_REF_FRAMES,
      g_param_spec_uint ("ref-frames", "Ref frames",
          "Preferred number of reference frames. Actual number of reference "
          "frames can be limited depending on hardware (0 = unspecified)",
          0, 16, DEFAULT_REF_FRAMES, rw_params));

  g_object_class_install_property (object_class, PROP_FRAME_ANALYSIS,
      g_param_spec_boolean ("frame-analysis", "Frame Analysis",
          "Enable 2 pass encoding if supported by hardware",
          DEFAULT_FRAME_ANALYSIS, rw_params));

  g_object_class_install_property (object_class, PROP_RATE_CONTROL,
      g_param_spec_enum ("rate-control", "Rate Control",
          "Rate Control Method", GST_TYPE_D3D12_ENCODER_RATE_CONTROL,
          DEFAULT_RATE_CONTROL, rw_params));

  g_object_class_install_property (object_class, PROP_BITRATE,
      g_param_spec_uint ("bitrate", "Bitrate",
          "Target bitrate in kbits/second. "
          "Used for \"cbr\", \"vbr\", and \"qvbr\" rate control",
          0, G_MAXUINT, DEFAULT_BITRATE, rw_params));

  g_object_class_install_property (object_class, PROP_MAX_BITRATE,
      g_param_spec_uint ("max-bitrate", "Max Bitrate",
          "Peak bitrate in kbits/second. "
          "Used for \"vbr\", and \"qvbr\" rate control",
          0, G_MAXUINT, DEFAULT_MAX_BITRATE, rw_params));

  g_object_class_install_property (object_class, PROP_QVBR_QUALITY,
      g_param_spec_uint ("qvbr-quality", "QVBR Quality",
          "Constant quality target value for \"qvbr\" rate control",
          0, 51, DEFAULT_QVBR_QUALITY, rw_params));

  g_object_class_install_property (object_class, PROP_QP_INIT,
      g_param_spec_uint ("qp-init", "QP Init",
          "Initial QP value. "
          "Used for \"cbr\", \"vbr\", and \"qvbr\" rate control",
          0, 51, DEFAULT_QP, rw_params));

  g_object_class_install_property (object_class, PROP_QP_MIN,
      g_param_spec_uint ("qp-min", "QP Min",
          "Minimum QP value for \"cbr\", \"vbr\", and \"qvbr\" rate control. "
          "To enable min/max QP setting, \"qp-max >= qp-min > 0\" "
          "condition should be satisfied", 0, 51, DEFAULT_QP, rw_params));

  g_object_class_install_property (object_class, PROP_QP_MAX,
      g_param_spec_uint ("qp-max", "QP Max",
          "Maximum QP value for \"cbr\", \"vbr\", and \"qvbr\" rate control. "
          "To enable min/max QP setting, \"qp-max >= qp-min > 0\" "
          "condition should be satisfied", 0, 51, DEFAULT_QP, rw_params));

  g_object_class_install_property (object_class, PROP_QP_I,
      g_param_spec_uint ("qp-i", "QP I",
          "Constant QP value for I frames. Used for \"cqp\" rate control",
          1, 51, DEFAULT_CQP, rw_params));

  g_object_class_install_property (object_class, PROP_QP_P,
      g_param_spec_uint ("qp-p", "QP P",
          "Constant QP value for P frames. Used for \"cqp\" rate control",
          1, 51, DEFAULT_CQP, rw_params));

  g_object_class_install_property (object_class, PROP_QP_I,
      g_param_spec_uint ("qp-b", "QP B",
          "Constant QP value for B frames. Used for \"cqp\" rate control",
          1, 51, DEFAULT_CQP, rw_params));

  g_object_class_install_property (object_class, PROP_SLICE_MODE,
      g_param_spec_enum ("slice-mode", "Slice Mode",
          "Slice partiton mode", GST_TYPE_D3D12_ENCODER_SUBREGION_LAYOUT,
          DEFAULT_SLICE_MODE, rw_params));

  g_object_class_install_property (object_class, PROP_SLICE_PARTITION,
      g_param_spec_uint ("slice-partition", "Slice partition",
          "Slice partition threshold interpreted depending on \"slice-mode\". "
          "If set zero, full frame encoding will be selected without "
          "partitioning regardless of requested \"slice-mode\"",
          0, G_MAXUINT, DEFAULT_SLICE_PARTITION, rw_params));

  g_object_class_install_property (object_class, PROP_CC_INSERT,
      g_param_spec_enum ("cc-insert", "Closed Caption Insert",
          "Closed Caption insert mode", GST_TYPE_D3D12_ENCODER_SEI_INSERT_MODE,
          DEFAULT_CC_INSERT, rw_params));

  std::string long_name = "Direct3D12 H.264 " + std::string (cdata->description)
      + " Encoder";
  gst_element_class_set_metadata (element_class, long_name.c_str (),
      "Codec/Encoder/Video/Hardware", "Direct3D12 H.264 Video Encoder",
      "Seungha Yang <seungha@centricular.com>");

  pad_templ = gst_pad_template_new ("sink",
      GST_PAD_SINK, GST_PAD_ALWAYS, cdata->sink_caps);
  gst_element_class_add_pad_template (element_class, pad_templ);

  pad_templ = gst_pad_template_new ("src",
      GST_PAD_SRC, GST_PAD_ALWAYS, cdata->src_caps);
  gst_element_class_add_pad_template (element_class, pad_templ);

  encoder_class->start = GST_DEBUG_FUNCPTR (gst_d3d12_h264_enc_start);
  encoder_class->stop = GST_DEBUG_FUNCPTR (gst_d3d12_h264_enc_stop);
  encoder_class->transform_meta =
      GST_DEBUG_FUNCPTR (gst_d3d12_h264_enc_transform_meta);

  d3d12enc_class->adapter_luid = cdata->luid;
  d3d12enc_class->device_id = cdata->device_id;
  d3d12enc_class->vendor_id = cdata->vendor_id;
  d3d12enc_class->new_sequence =
      GST_DEBUG_FUNCPTR (gst_d3d12_h264_enc_new_sequence);
  d3d12enc_class->start_frame =
      GST_DEBUG_FUNCPTR (gst_d3d12_h264_enc_start_frame);
  d3d12enc_class->end_frame = GST_DEBUG_FUNCPTR (gst_d3d12_h264_enc_end_frame);
}

static void
gst_d3d12_h264_enc_init (GstD3D12H264Enc * self)
{
  self->priv = new GstD3D12H264EncPrivate ();
}

static void
gst_d3d12_h264_enc_finalize (GObject * object)
{
  auto self = GST_D3D12_H264_ENC (object);

  delete self->priv;

  G_OBJECT_CLASS (parent_class)->finalize (object);
}

static void
gst_d3d12_h264_enc_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  auto self = GST_D3D12_H264_ENC (object);
  auto priv = self->priv;

  std::lock_guard < std::mutex > lk (priv->prop_lock);
  switch (prop_id) {
    case PROP_AUD:
      priv->aud = g_value_get_boolean (value);
      break;
    case PROP_GOP_SIZE:
    {
      auto gop_size = g_value_get_uint (value);
      if (gop_size != priv->gop_size) {
        priv->gop_size = gop_size;
        priv->gop_updated = TRUE;
      }
      break;
    }
    case PROP_REF_FRAMES:
    {
      auto ref_frames = g_value_get_uint (value);
      if (ref_frames != priv->ref_frames) {
        priv->ref_frames = ref_frames;
        priv->gop_updated = TRUE;
      }
      break;
    }
    case PROP_FRAME_ANALYSIS:
    {
      auto frame_analysis = g_value_get_boolean (value);
      if (frame_analysis != priv->frame_analysis) {
        priv->frame_analysis = frame_analysis;
        priv->rc_updated = TRUE;
      }
      break;
    }
    case PROP_RATE_CONTROL:
    {
      auto mode = (D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE)
          g_value_get_enum (value);
      if (mode != priv->rc_mode) {
        priv->rc_mode = mode;
        priv->rc_updated = TRUE;
      }
      break;
    }
    case PROP_BITRATE:
    {
      auto bitrate = g_value_get_uint (value);
      if (bitrate == 0)
        bitrate = DEFAULT_BITRATE;

      if (bitrate != priv->bitrate) {
        priv->bitrate = bitrate;
        if (priv->selected_rc_mode != D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_CQP)
          priv->rc_updated = TRUE;
      }
      break;
    }
    case PROP_MAX_BITRATE:
    {
      auto max_bitrate = g_value_get_uint (value);
      if (max_bitrate == 0)
        max_bitrate = DEFAULT_MAX_BITRATE;

      if (max_bitrate != priv->max_bitrate) {
        priv->max_bitrate = max_bitrate;
        switch (priv->selected_rc_mode) {
          case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_VBR:
          case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_QVBR:
            priv->rc_updated = TRUE;
            break;
          default:
            break;
        }
      }
      break;
    }
    case PROP_QVBR_QUALITY:
    {
      auto qvbr_quality = g_value_get_uint (value);
      if (qvbr_quality != priv->qvbr_quality) {
        priv->qvbr_quality = qvbr_quality;
        if (priv->selected_rc_mode ==
            D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_QVBR) {
          priv->rc_updated = TRUE;
        }
      }
      break;
    }
    case PROP_QP_INIT:
    {
      auto qp_init = g_value_get_uint (value);
      if (qp_init != priv->qp_init) {
        priv->qp_init = qp_init;
        switch (priv->selected_rc_mode) {
          case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_CBR:
          case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_VBR:
          case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_QVBR:
            priv->rc_updated = TRUE;
            break;
          default:
            break;
        }
      }
      break;
    }
    case PROP_QP_MIN:
    {
      auto qp_min = g_value_get_uint (value);
      if (qp_min != priv->qp_min) {
        priv->qp_min = qp_min;
        switch (priv->selected_rc_mode) {
          case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_CBR:
          case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_VBR:
          case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_QVBR:
            priv->rc_updated = TRUE;
            break;
          default:
            break;
        }
      }
      break;
    }
    case PROP_QP_MAX:
    {
      auto qp_max = g_value_get_uint (value);
      if (qp_max != priv->qp_max) {
        priv->qp_max = qp_max;
        switch (priv->selected_rc_mode) {
          case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_CBR:
          case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_VBR:
          case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_QVBR:
            priv->rc_updated = TRUE;
            break;
          default:
            break;
        }
      }
      break;
    }
    case PROP_QP_I:
    {
      auto qp_i = g_value_get_uint (value);
      if (qp_i != priv->qp_i) {
        priv->qp_i = qp_i;
        if (priv->selected_rc_mode == D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_CQP)
          priv->rc_updated = TRUE;
      }
      break;
    }
    case PROP_QP_P:
    {
      auto qp_p = g_value_get_uint (value);
      if (qp_p != priv->qp_p) {
        priv->qp_p = qp_p;
        if (priv->selected_rc_mode == D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_CQP)
          priv->rc_updated = TRUE;
      }
      break;
    }
    case PROP_QP_B:
    {
      auto qp_b = g_value_get_uint (value);
      if (qp_b != priv->qp_b) {
        priv->qp_b = qp_b;
        if (priv->selected_rc_mode == D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_CQP)
          priv->rc_updated = TRUE;
      }
      break;
    }
    case PROP_SLICE_MODE:
    {
      auto slice_mode = (D3D12_VIDEO_ENCODER_FRAME_SUBREGION_LAYOUT_MODE)
          g_value_get_enum (value);
      if (slice_mode != priv->slice_mode) {
        priv->slice_mode = slice_mode;
        if (priv->selected_slice_mode != slice_mode)
          priv->slice_updated = TRUE;
      }
      break;
    }
    case PROP_SLICE_PARTITION:
    {
      auto slice_partition = g_value_get_uint (value);
      if (slice_partition != priv->slice_partition) {
        priv->slice_partition = slice_partition;
        if (priv->selected_slice_mode !=
            D3D12_VIDEO_ENCODER_FRAME_SUBREGION_LAYOUT_MODE_FULL_FRAME) {
          priv->slice_updated = TRUE;
        }
      }
      break;
    }
    case PROP_CC_INSERT:
      priv->cc_insert = (GstD3D12EncoderSeiInsertMode) g_value_get_enum (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
gst_d3d12_h264_enc_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec)
{
  auto self = GST_D3D12_H264_ENC (object);
  auto priv = self->priv;
  auto klass = GST_D3D12_H264_ENC_GET_CLASS (self);
  const auto cdata = klass->cdata;

  std::lock_guard < std::mutex > lk (priv->prop_lock);
  switch (prop_id) {
    case PROP_RATE_CONTROL_SUPPORT:
      g_value_set_flags (value, cdata->rc_support);
      break;
    case PROP_SLICE_MODE_SUPPORT:
      g_value_set_flags (value, cdata->slice_mode_support);
      break;
    case PROP_AUD:
      g_value_set_boolean (value, priv->aud);
      break;
    case PROP_GOP_SIZE:
      g_value_set_uint (value, priv->gop_size);
      break;
    case PROP_REF_FRAMES:
      g_value_set_uint (value, priv->ref_frames);
      break;
    case PROP_FRAME_ANALYSIS:
      g_value_set_boolean (value, priv->frame_analysis);
      break;
    case PROP_RATE_CONTROL:
      g_value_set_enum (value, priv->rc_mode);
      break;
    case PROP_BITRATE:
      g_value_set_uint (value, priv->bitrate);
      break;
    case PROP_MAX_BITRATE:
      g_value_set_uint (value, priv->max_bitrate);
      break;
    case PROP_QVBR_QUALITY:
      g_value_set_uint (value, priv->qvbr_quality);
      break;
    case PROP_QP_INIT:
      g_value_set_uint (value, priv->qp_init);
      break;
    case PROP_QP_MIN:
      g_value_set_uint (value, priv->qp_min);
      break;
    case PROP_QP_MAX:
      g_value_set_uint (value, priv->qp_max);
      break;
    case PROP_QP_I:
      g_value_set_uint (value, priv->qp_i);
      break;
    case PROP_QP_P:
      g_value_set_uint (value, priv->qp_p);
      break;
    case PROP_QP_B:
      g_value_set_uint (value, priv->qp_p);
      break;
    case PROP_SLICE_MODE:
      g_value_set_enum (value, priv->slice_mode);
      break;
    case PROP_SLICE_PARTITION:
      g_value_set_uint (value, priv->slice_partition);
      break;
    case PROP_CC_INSERT:
      g_value_set_enum (value, priv->cc_insert);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static gboolean
gst_d3d12_h264_enc_start (GstVideoEncoder * encoder)
{
  auto self = GST_D3D12_H264_ENC (encoder);
  auto priv = self->priv;

  GST_DEBUG_OBJECT (self, "Start");

  priv->display_order = 0;

  return GST_VIDEO_ENCODER_CLASS (parent_class)->start (encoder);
}

static gboolean
gst_d3d12_h264_enc_stop (GstVideoEncoder * encoder)
{
  auto self = GST_D3D12_H264_ENC (encoder);
  auto priv = self->priv;

  GST_DEBUG_OBJECT (self, "Stop");

  priv->dpb = nullptr;

  return GST_VIDEO_ENCODER_CLASS (parent_class)->stop (encoder);
}

static gboolean
gst_d3d12_h264_enc_transform_meta (GstVideoEncoder * encoder,
    GstVideoCodecFrame * frame, GstMeta * meta)
{
  auto self = GST_D3D12_H264_ENC (encoder);
  auto priv = self->priv;

  if (meta->info->api == GST_VIDEO_CAPTION_META_API_TYPE) {
    std::lock_guard < std::mutex > lk (priv->prop_lock);
    if (priv->cc_insert == GST_D3D12_ENCODER_SEI_INSERT_AND_DROP) {
      auto cc_meta = (GstVideoCaptionMeta *) meta;
      if (cc_meta->caption_type == GST_VIDEO_CAPTION_TYPE_CEA708_RAW)
        return FALSE;
    }
  }

  return GST_VIDEO_ENCODER_CLASS (parent_class)->transform_meta (encoder,
      frame, meta);
}

static gboolean
gst_d3d12_h264_enc_build_sps (GstD3D12H264Enc * self, const GstVideoInfo * info,
    const D3D12_VIDEO_ENCODER_PICTURE_RESOLUTION_DESC * resolution,
    guint num_ref)
{
  auto priv = self->priv;
  guint8 sps_buf[4096] = { 0, };
  /* *INDENT-OFF* */
  static const std::unordered_map<D3D12_VIDEO_ENCODER_LEVELS_H264, guint8>
      level_map = {
    {D3D12_VIDEO_ENCODER_LEVELS_H264_1, GST_H264_LEVEL_L1},
    {D3D12_VIDEO_ENCODER_LEVELS_H264_1b, GST_H264_LEVEL_L1B},
    {D3D12_VIDEO_ENCODER_LEVELS_H264_11, GST_H264_LEVEL_L1_1},
    {D3D12_VIDEO_ENCODER_LEVELS_H264_12, GST_H264_LEVEL_L1_2},
    {D3D12_VIDEO_ENCODER_LEVELS_H264_13, GST_H264_LEVEL_L1_3},
    {D3D12_VIDEO_ENCODER_LEVELS_H264_2, GST_H264_LEVEL_L2},
    {D3D12_VIDEO_ENCODER_LEVELS_H264_21, GST_H264_LEVEL_L2_1},
    {D3D12_VIDEO_ENCODER_LEVELS_H264_22, GST_H264_LEVEL_L2_2},
    {D3D12_VIDEO_ENCODER_LEVELS_H264_3, GST_H264_LEVEL_L3},
    {D3D12_VIDEO_ENCODER_LEVELS_H264_31, GST_H264_LEVEL_L3_1},
    {D3D12_VIDEO_ENCODER_LEVELS_H264_32, GST_H264_LEVEL_L3_2},
    {D3D12_VIDEO_ENCODER_LEVELS_H264_4, GST_H264_LEVEL_L4},
    {D3D12_VIDEO_ENCODER_LEVELS_H264_41, GST_H264_LEVEL_L4_1},
    {D3D12_VIDEO_ENCODER_LEVELS_H264_42, GST_H264_LEVEL_L4_2},
    {D3D12_VIDEO_ENCODER_LEVELS_H264_5, GST_H264_LEVEL_L5},
    {D3D12_VIDEO_ENCODER_LEVELS_H264_51, GST_H264_LEVEL_L5_1},
    {D3D12_VIDEO_ENCODER_LEVELS_H264_52, GST_H264_LEVEL_L5_2},
    {D3D12_VIDEO_ENCODER_LEVELS_H264_6, GST_H264_LEVEL_L6},
    {D3D12_VIDEO_ENCODER_LEVELS_H264_61, GST_H264_LEVEL_L6_1},
    {D3D12_VIDEO_ENCODER_LEVELS_H264_62, GST_H264_LEVEL_L6_2},
  };
  static const std::array<std::pair<gint, gint>, 17> par_map {{
    {0, 0},
    {1, 1},
    {12, 11},
    {10, 11},
    {16, 11},
    {40, 33},
    {24, 11},
    {20, 11},
    {32, 11},
    {80, 33},
    {18, 11},
    {15, 11},
    {64, 33},
    {160, 99},
    {4, 3},
    {3, 2},
    {2, 1}
  }};
  /* *INDENT-ON* */

  priv->sps.Clear ();
  auto & sps = priv->sps.sps;
  sps.id = 0;
  sps.profile_idc = priv->selected_profile;
  switch (sps.profile_idc) {
    case GST_H264_PROFILE_BASELINE:
      sps.constraint_set0_flag = 1;
      sps.constraint_set1_flag = 1;
      break;
    case GST_H264_PROFILE_MAIN:
      sps.constraint_set1_flag = 1;
      break;
    default:
      break;
  }

  if (priv->level_h264 == D3D12_VIDEO_ENCODER_LEVELS_H264_1b)
    sps.constraint_set3_flag = 1;

  sps.level_idc = level_map.at (priv->level_h264);
  /* d3d12 supports only 4:2:0 encoding */
  sps.chroma_format_idc = 1;
  sps.separate_colour_plane_flag = 0;
  /* add high-10 profile support */
  sps.bit_depth_luma_minus8 = 0;
  sps.bit_depth_chroma_minus8 = 0;
  sps.qpprime_y_zero_transform_bypass_flag = 0;
  sps.scaling_matrix_present_flag = 0;
  sps.log2_max_frame_num_minus4 =
      priv->gop_struct_h264.log2_max_frame_num_minus4;
  sps.pic_order_cnt_type = priv->gop_struct_h264.pic_order_cnt_type;
  sps.log2_max_pic_order_cnt_lsb_minus4 =
      priv->gop_struct_h264.log2_max_pic_order_cnt_lsb_minus4;
  sps.num_ref_frames = num_ref;
  sps.gaps_in_frame_num_value_allowed_flag = 0;
  sps.pic_width_in_mbs_minus1 = (resolution->Width / 16) - 1;
  sps.pic_height_in_map_units_minus1 = (resolution->Height / 16) - 1;
  sps.frame_mbs_only_flag = 1;

  if (sps.profile_idc != GST_H264_PROFILE_BASELINE)
    sps.direct_8x8_inference_flag = 1;

  if (resolution->Width != (UINT) info->width ||
      resolution->Height != (UINT) info->height) {
    sps.frame_cropping_flag = 1;
    sps.frame_crop_left_offset = 0;
    sps.frame_crop_right_offset = (resolution->Width - info->width) / 2;
    sps.frame_crop_top_offset = 0;
    sps.frame_crop_bottom_offset = (resolution->Height - info->height) / 2;
  }

  sps.vui_parameters_present_flag = 1;
  auto & vui = sps.vui_parameters;
  const auto colorimetry = &info->colorimetry;

  if (info->par_n > 0 && info->par_d > 0) {
    const auto it = std::find_if (par_map.begin (),
        par_map.end (),[&](const auto & par) {
          return par.first == info->par_n && par.second == info->par_d;
        }
    );

    if (it != par_map.end ()) {
      vui.aspect_ratio_info_present_flag = 1;
      vui.aspect_ratio_idc = (guint8) std::distance (par_map.begin (), it);
    } else if (info->par_n <= G_MAXUINT16 && info->par_d <= G_MAXUINT16) {
      vui.aspect_ratio_info_present_flag = 1;
      vui.aspect_ratio_idc = 0xff;
      vui.sar_width = info->par_n;
      vui.sar_height = info->par_d;
    }
  }

  vui.video_signal_type_present_flag = 1;
  /* Unspecified */
  vui.video_format = 5;
  vui.video_full_range_flag =
      colorimetry->range == GST_VIDEO_COLOR_RANGE_0_255 ? 1 : 0;
  vui.colour_description_present_flag = 1;
  vui.colour_primaries =
      gst_video_color_primaries_to_iso (colorimetry->primaries);
  vui.transfer_characteristics =
      gst_video_transfer_function_to_iso (colorimetry->transfer);
  vui.matrix_coefficients = gst_video_color_matrix_to_iso (colorimetry->matrix);
  if (info->fps_n > 0 && info->fps_d > 0) {
    vui.timing_info_present_flag = 1;
    vui.num_units_in_tick = info->fps_d;
    vui.time_scale = 2 * (guint) info->fps_n;
  }

  /* TODO: pic_struct_present_flag = 1 for picture timeing SEI */

  guint nal_size = G_N_ELEMENTS (sps_buf);
  GstH264BitWriterResult write_ret =
      gst_h264_bit_writer_sps (&sps, TRUE, sps_buf, &nal_size);
  if (write_ret != GST_H264_BIT_WRITER_OK) {
    GST_ERROR_OBJECT (self, "Couldn't build SPS");
    return FALSE;
  }

  priv->sps.bytes.resize (G_N_ELEMENTS (sps_buf));
  guint written_size = priv->sps.bytes.size ();
  write_ret = gst_h264_bit_writer_convert_to_nal (4, FALSE, TRUE, FALSE,
      sps_buf, nal_size * 8, priv->sps.bytes.data (), &written_size);
  if (write_ret != GST_H264_BIT_WRITER_OK) {
    GST_ERROR_OBJECT (self, "Couldn't build SPS bytes");
    return FALSE;
  }
  priv->sps.bytes.resize (written_size);

  return TRUE;
}

static gboolean
gst_d3d12_h264_enc_build_pps (GstD3D12H264Enc * self, guint num_ref)
{
  auto priv = self->priv;

  /* Driver does not seem to use num_ref_idx_active_override_flag.
   * Needs multiple PPS to signal ref pics */
  /* TODO: make more PPS for L1 ref pics */
  guint num_pps = MAX (1, num_ref);
  priv->pps.resize (num_pps);
  for (size_t i = 0; i < priv->pps.size (); i++) {
    guint8 pps_buf[1024] = { 0, };
    auto & d3d12_pps = priv->pps[i];
    d3d12_pps.Clear ();

    auto & pps = d3d12_pps.pps;

    pps.id = i;
    pps.sequence = &priv->sps.sps;
    if ((priv->config_h264.ConfigurationFlags &
            D3D12_VIDEO_ENCODER_CODEC_CONFIGURATION_H264_FLAG_ENABLE_CABAC_ENCODING)
        != 0) {
      pps.entropy_coding_mode_flag = 1;
    } else {
      pps.entropy_coding_mode_flag = 0;
    }

    pps.pic_order_present_flag = 0;
    pps.num_slice_groups_minus1 = 0;
    pps.num_ref_idx_l0_active_minus1 = i;
    pps.num_ref_idx_l1_active_minus1 = 0;
    pps.weighted_pred_flag = 0;
    pps.weighted_bipred_idc = 0;
    pps.pic_init_qp_minus26 = 0;
    pps.pic_init_qs_minus26 = 0;
    pps.chroma_qp_index_offset = 0;
    pps.deblocking_filter_control_present_flag = 1;
    if ((priv->config_h264.ConfigurationFlags &
            D3D12_VIDEO_ENCODER_CODEC_CONFIGURATION_H264_FLAG_USE_CONSTRAINED_INTRAPREDICTION)
        != 0) {
      pps.constrained_intra_pred_flag = 1;
    } else {
      pps.constrained_intra_pred_flag = 0;
    }
    pps.redundant_pic_cnt_present_flag = 0;
    if ((priv->config_h264.ConfigurationFlags &
            D3D12_VIDEO_ENCODER_CODEC_CONFIGURATION_H264_FLAG_USE_ADAPTIVE_8x8_TRANSFORM)
        != 0) {
      /* double check. This is not allowed for baseline and main profiles */
      pps.transform_8x8_mode_flag = 1;
    } else {
      pps.transform_8x8_mode_flag = 0;
    }

    pps.pic_scaling_matrix_present_flag = 0;
    pps.second_chroma_qp_index_offset = 0;

    guint nal_size = G_N_ELEMENTS (pps_buf);
    d3d12_pps.bytes.resize (nal_size);
    GstH264BitWriterResult write_ret =
        gst_h264_bit_writer_pps (&pps, TRUE, pps_buf, &nal_size);
    if (write_ret != GST_H264_BIT_WRITER_OK) {
      GST_ERROR_OBJECT (self, "Couldn't build PPS");
      return FALSE;
    }

    guint written_size = d3d12_pps.bytes.size ();
    write_ret = gst_h264_bit_writer_convert_to_nal (4, FALSE, TRUE, FALSE,
        pps_buf, nal_size * 8, d3d12_pps.bytes.data (), &written_size);
    if (write_ret != GST_H264_BIT_WRITER_OK) {
      GST_ERROR_OBJECT (self, "Couldn't build PPS bytes");
      return FALSE;
    }

    d3d12_pps.bytes.resize (written_size);
  }

  return TRUE;
}

static gboolean
gst_d3d12_h264_enc_get_max_ref_frames (GstD3D12H264Enc * self)
{
  auto priv = self->priv;
  const auto & pic_ctrl_support = priv->pic_ctrl_support;

  guint max_ref_frames = MIN (pic_ctrl_support.MaxL0ReferencesForP,
      pic_ctrl_support.MaxDPBCapacity);
  guint ref_frames = priv->ref_frames;

  if (max_ref_frames == 0) {
    GST_INFO_OBJECT (self,
        "Hardware does not support inter prediction, forcing all-intra");
    ref_frames = 0;
  } else if (priv->gop_size == 1) {
    GST_INFO_OBJECT (self, "User requested all-intra coding");
    ref_frames = 0;
  } else {
    /* TODO: at least 2 ref frames if B frame is enabled */
    if (ref_frames != 0)
      ref_frames = MIN (ref_frames, max_ref_frames);
    else
      ref_frames = 1;
  }

  return ref_frames;
}

static gboolean
gst_d3d12_h264_enc_update_gop (GstD3D12H264Enc * self,
    ID3D12VideoDevice * video_device, GstD3D12EncoderConfig * config,
    D3D12_VIDEO_ENCODER_SEQUENCE_CONTROL_FLAGS * seq_flags)
{
  auto priv = self->priv;

  if (seq_flags && !priv->gop_updated)
    return TRUE;

  auto ref_frames = gst_d3d12_h264_enc_get_max_ref_frames (self);
  auto gop_size = priv->gop_size;
  if (ref_frames == 0)
    gop_size = 1;

  priv->last_pps_id = 0;

  auto prev_gop_struct = priv->gop.GetGopStruct ();
  auto prev_ref_frames = priv->selected_ref_frames;

  priv->selected_ref_frames = ref_frames;
  priv->gop.Init (gop_size);
  priv->gop_struct_h264 = priv->gop.GetGopStruct ();

  if (seq_flags) {
    if (prev_ref_frames != ref_frames ||
        memcmp (&prev_gop_struct, &priv->gop_struct_h264,
            sizeof (priv->gop_struct_h264)) != 0) {
      GST_DEBUG_OBJECT (self, "Gop struct updated");
      *seq_flags |=
          D3D12_VIDEO_ENCODER_SEQUENCE_CONTROL_FLAG_GOP_SEQUENCE_CHANGE;
    }
  }

  GST_DEBUG_OBJECT (self,
      "Configured GOP struct, GOPLength: %u, PPicturePeriod: %u, "
      "pic_order_cnt_type: %d, log2_max_frame_num_minus4: %d, "
      "log2_max_pic_order_cnt_lsb_minus4: %d", priv->gop_struct_h264.GOPLength,
      priv->gop_struct_h264.PPicturePeriod,
      priv->gop_struct_h264.pic_order_cnt_type,
      priv->gop_struct_h264.log2_max_frame_num_minus4,
      priv->gop_struct_h264.log2_max_pic_order_cnt_lsb_minus4);

  priv->gop_updated = FALSE;

  return TRUE;
}

/* called with prop_lock taken */
static gboolean
gst_d3d12_h264_enc_update_rate_control (GstD3D12H264Enc * self,
    ID3D12VideoDevice * video_device, GstD3D12EncoderConfig * config,
    D3D12_VIDEO_ENCODER_SEQUENCE_CONTROL_FLAGS * seq_flags)
{
  auto priv = self->priv;
  const D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE rc_modes[] = {
    D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_VBR,
    D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_QVBR,
    D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_CBR,
    D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_CQP,
  };

  if (seq_flags && !priv->rc_updated)
    return TRUE;

  GstD3D12EncoderConfig prev_config = *config;

  config->rate_control.Flags = D3D12_VIDEO_ENCODER_RATE_CONTROL_FLAG_NONE;
  UINT64 bitrate = priv->bitrate;
  if (bitrate == 0)
    bitrate = DEFAULT_BITRATE;

  UINT64 max_bitrate = priv->max_bitrate;
  if (max_bitrate < bitrate) {
    if (bitrate >= G_MAXUINT64 / 2)
      max_bitrate = bitrate;
    else
      max_bitrate = bitrate * 2;
  }

  /* Property uses kbps, and API uses bps */
  bitrate *= 1000;
  max_bitrate *= 1000;

  /* Fill every rate control struct and select later */
  config->cqp.ConstantQP_FullIntracodedFrame = priv->qp_i;
  config->cqp.ConstantQP_InterPredictedFrame_PrevRefOnly = priv->qp_p;
  config->cqp.ConstantQP_InterPredictedFrame_BiDirectionalRef = priv->qp_b;

  config->cbr.InitialQP = priv->qp_init;
  config->cbr.MinQP = priv->qp_min;
  config->cbr.MaxQP = priv->qp_max;
  config->cbr.TargetBitRate = bitrate;

  config->vbr.InitialQP = priv->qp_init;
  config->vbr.MinQP = priv->qp_min;
  config->vbr.MaxQP = priv->qp_max;
  config->vbr.TargetAvgBitRate = bitrate;
  config->vbr.PeakBitRate = max_bitrate;

  config->qvbr.InitialQP = priv->qp_init;
  config->qvbr.MinQP = priv->qp_min;
  config->qvbr.MaxQP = priv->qp_max;
  config->qvbr.TargetAvgBitRate = bitrate;
  config->qvbr.PeakBitRate = max_bitrate;
  config->qvbr.ConstantQualityTarget = priv->qvbr_quality;

  D3D12_FEATURE_DATA_VIDEO_ENCODER_RATE_CONTROL_MODE feature_data = { };
  feature_data.Codec = D3D12_VIDEO_ENCODER_CODEC_H264;
  feature_data.RateControlMode = priv->rc_mode;

  auto hr = video_device->CheckFeatureSupport
      (D3D12_FEATURE_VIDEO_ENCODER_RATE_CONTROL_MODE,
      &feature_data, sizeof (feature_data));
  if (SUCCEEDED (hr) && feature_data.IsSupported) {
    priv->selected_rc_mode = priv->rc_mode;
  } else {
    GST_INFO_OBJECT (self, "Requested rate control mode is not supported");

    for (guint i = 0; i < G_N_ELEMENTS (rc_modes); i++) {
      feature_data.RateControlMode = rc_modes[i];
      hr = video_device->CheckFeatureSupport
          (D3D12_FEATURE_VIDEO_ENCODER_RATE_CONTROL_MODE, &feature_data,
          sizeof (feature_data));
      if (SUCCEEDED (hr) && feature_data.IsSupported) {
        priv->selected_rc_mode = rc_modes[i];
        break;
      } else {
        feature_data.IsSupported = FALSE;
      }
    }

    if (!feature_data.IsSupported) {
      GST_ERROR_OBJECT (self, "Couldn't find support rate control mode");
      return FALSE;
    }
  }

  GST_INFO_OBJECT (self, "Requested rate control mode %d, selected %d",
      priv->rc_mode, priv->selected_rc_mode);

  config->rate_control.Mode = priv->selected_rc_mode;
  switch (priv->selected_rc_mode) {
    case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_CQP:
      config->rate_control.ConfigParams.DataSize = sizeof (config->cqp);
      config->rate_control.ConfigParams.pConfiguration_CQP = &config->cqp;
      break;
    case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_CBR:
      config->rate_control.ConfigParams.DataSize = sizeof (config->cbr);
      config->rate_control.ConfigParams.pConfiguration_CBR = &config->cbr;
      break;
    case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_VBR:
      config->rate_control.ConfigParams.DataSize = sizeof (config->vbr);
      config->rate_control.ConfigParams.pConfiguration_VBR = &config->vbr;
      break;
    case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_QVBR:
      config->rate_control.ConfigParams.DataSize = sizeof (config->qvbr);
      config->rate_control.ConfigParams.pConfiguration_QVBR = &config->qvbr;
      break;
    default:
      g_assert_not_reached ();
      return FALSE;
  }

  if (seq_flags) {
    if (prev_config.rate_control.Mode != config->rate_control.Mode) {
      GST_DEBUG_OBJECT (self, "Rate control mode changed %d -> %d",
          prev_config.rate_control.Mode, config->rate_control.Mode);
      *seq_flags |=
          D3D12_VIDEO_ENCODER_SEQUENCE_CONTROL_FLAG_RATE_CONTROL_CHANGE;
    } else {
      void *prev, *cur;
      switch (config->rate_control.Mode) {
        case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_CQP:
          prev = &prev_config.cqp;
          cur = &config->cqp;
          break;
        case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_CBR:
          prev = &prev_config.cbr;
          cur = &config->cbr;
          break;
        case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_VBR:
          prev = &prev_config.vbr;
          cur = &config->vbr;
          break;
        case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_QVBR:
          prev = &prev_config.qvbr;
          cur = &config->cbr;
          break;
        default:
          g_assert_not_reached ();
          return FALSE;
      }

      if (memcmp (prev, cur, config->rate_control.ConfigParams.DataSize) != 0) {
        GST_DEBUG_OBJECT (self, "Rate control params updated");
        *seq_flags |=
            D3D12_VIDEO_ENCODER_SEQUENCE_CONTROL_FLAG_RATE_CONTROL_CHANGE;
      }
    }
  }

  priv->rc_updated = FALSE;

  return TRUE;
}

static gboolean
gst_d3d12_h264_enc_update_slice (GstD3D12H264Enc * self,
    ID3D12VideoDevice * video_device, GstD3D12EncoderConfig * config,
    D3D12_VIDEO_ENCODER_SEQUENCE_CONTROL_FLAGS * seq_flags,
    D3D12_VIDEO_ENCODER_SUPPORT_FLAGS * support_flags)
{
  auto priv = self->priv;

  if (seq_flags && !priv->slice_updated)
    return TRUE;

  auto encoder = GST_D3D12_ENCODER (self);
  auto prev_mode = priv->selected_slice_mode;
  auto prev_slice = priv->layout_slices;

  priv->selected_slice_mode =
      D3D12_VIDEO_ENCODER_FRAME_SUBREGION_LAYOUT_MODE_FULL_FRAME;
  priv->layout_slices.NumberOfSlicesPerFrame = 1;
  config->max_subregions = 1;

  D3D12_FEATURE_DATA_VIDEO_ENCODER_SUPPORT support = { };
  D3D12_FEATURE_DATA_VIDEO_ENCODER_RESOLUTION_SUPPORT_LIMITS limits = { };
  D3D12_VIDEO_ENCODER_PROFILE_H264 suggested_profile = priv->profile_h264;
  D3D12_VIDEO_ENCODER_LEVELS_H264 suggested_level;

  support.Codec = D3D12_VIDEO_ENCODER_CODEC_H264;
  support.InputFormat = DXGI_FORMAT_NV12;
  support.CodecConfiguration = config->codec_config;
  support.CodecGopSequence = config->gop_struct;
  support.RateControl = config->rate_control;
  /* TODO: add intra-refresh support */
  support.IntraRefresh = D3D12_VIDEO_ENCODER_INTRA_REFRESH_MODE_NONE;
  support.ResolutionsListCount = 1;
  support.pResolutionList = &config->resolution;
  support.MaxReferenceFramesInDPB = priv->selected_ref_frames;
  support.pResolutionDependentSupport = &limits;
  support.SuggestedProfile.DataSize = sizeof (suggested_profile);
  support.SuggestedProfile.pH264Profile = &suggested_profile;
  support.SuggestedLevel.DataSize = sizeof (suggested_level);
  support.SuggestedLevel.pH264LevelSetting = &suggested_level;

  HRESULT hr;
  if (priv->slice_mode !=
      D3D12_VIDEO_ENCODER_FRAME_SUBREGION_LAYOUT_MODE_FULL_FRAME
      && priv->slice_partition > 0) {
    /* TODO: fallback to other mode if possible */
    D3D12_FEATURE_DATA_VIDEO_ENCODER_FRAME_SUBREGION_LAYOUT_MODE
        feature_layout = { };
    feature_layout.Codec = D3D12_VIDEO_ENCODER_CODEC_H264;
    feature_layout.Profile = config->profile_desc;
    feature_layout.Level = config->level;
    feature_layout.SubregionMode = priv->slice_mode;
    hr = video_device->CheckFeatureSupport
        (D3D12_FEATURE_VIDEO_ENCODER_FRAME_SUBREGION_LAYOUT_MODE,
        &feature_layout, sizeof (feature_layout));
    if (!gst_d3d12_result (hr, encoder->device) || !feature_layout.IsSupported) {
      GST_WARNING_OBJECT (self, "Requested slice mode is not supported");
    } else {
      support.SubregionFrameEncoding = priv->slice_mode;
      hr = video_device->CheckFeatureSupport
          (D3D12_FEATURE_VIDEO_ENCODER_SUPPORT, &support, sizeof (support));
      if (gst_d3d12_result (hr, encoder->device)
          && CHECK_SUPPORT_FLAG (support.SupportFlags, GENERAL_SUPPORT_OK)
          && support.ValidationFlags == D3D12_VIDEO_ENCODER_VALIDATION_FLAG_NONE
          && limits.MaxSubregionsNumber > 1
          && limits.SubregionBlockPixelsSize > 0) {
        switch (priv->slice_mode) {
          case D3D12_VIDEO_ENCODER_FRAME_SUBREGION_LAYOUT_MODE_BYTES_PER_SUBREGION:
            priv->selected_slice_mode =
                priv->slice_mode;
            /* Don't know how many slices would be generated */
            config->max_subregions = limits.MaxSubregionsNumber;
            *support_flags = support.SupportFlags;
            break;
          case D3D12_VIDEO_ENCODER_FRAME_SUBREGION_LAYOUT_MODE_SQUARE_UNITS_PER_SUBREGION_ROW_UNALIGNED:
          {
            auto total_mbs =
                (config->resolution.Width / limits.SubregionBlockPixelsSize) *
                (config->resolution.Height / limits.SubregionBlockPixelsSize);
            if (priv->slice_partition >= total_mbs) {
              GST_DEBUG_OBJECT (self,
                  "Requested MBs per slice exceeds total MBs per frame");
            } else {
              priv->selected_slice_mode = priv->slice_mode;

              auto min_mbs_per_slice = (guint) std::ceil ((float) total_mbs
                  / limits.MaxSubregionsNumber);

              if (min_mbs_per_slice > priv->slice_partition) {
                GST_WARNING_OBJECT (self, "Too small number of MBs per slice");
                priv->layout_slices.NumberOfCodingUnitsPerSlice =
                    min_mbs_per_slice;
                config->max_subregions = limits.MaxSubregionsNumber;
              } else {
                priv->layout_slices.NumberOfCodingUnitsPerSlice =
                    priv->slice_partition;
                config->max_subregions = std::ceil ((float) total_mbs
                    / priv->slice_partition);
              }

              *support_flags = support.SupportFlags;
            }
            break;
          }
          case D3D12_VIDEO_ENCODER_FRAME_SUBREGION_LAYOUT_MODE_UNIFORM_PARTITIONING_ROWS_PER_SUBREGION:
          {
            auto total_rows = config->resolution.Height /
                limits.SubregionBlockPixelsSize;
            if (priv->slice_partition >= total_rows) {
              GST_DEBUG_OBJECT (self,
                  "Requested rows per slice exceeds total rows per frame");
            } else {
              priv->selected_slice_mode = priv->slice_mode;

              auto min_rows_per_slice = (guint) std::ceil ((float) total_rows
                  / limits.MaxSubregionsNumber);

              if (min_rows_per_slice > priv->slice_partition) {
                GST_WARNING_OBJECT (self, "Too small number of rows per slice");
                priv->layout_slices.NumberOfRowsPerSlice = min_rows_per_slice;
                config->max_subregions = limits.MaxSubregionsNumber;
              } else {
                priv->layout_slices.NumberOfRowsPerSlice =
                    priv->slice_partition;
                config->max_subregions = (guint) std::ceil ((float) total_rows
                    / priv->slice_partition);
              }

              *support_flags = support.SupportFlags;
            }
            break;
          }
          case D3D12_VIDEO_ENCODER_FRAME_SUBREGION_LAYOUT_MODE_UNIFORM_PARTITIONING_SUBREGIONS_PER_FRAME:
          {
            if (priv->slice_partition > 1) {
              priv->selected_slice_mode = priv->slice_mode;

              if (priv->slice_partition > limits.MaxSubregionsNumber) {
                GST_WARNING_OBJECT (self, "Too many slices per frame");
                priv->layout_slices.NumberOfSlicesPerFrame =
                    limits.MaxSubregionsNumber;
                config->max_subregions = limits.MaxSubregionsNumber;
              } else {
                priv->layout_slices.NumberOfSlicesPerFrame =
                    priv->slice_partition;
                config->max_subregions = priv->slice_partition;
              }

              *support_flags = support.SupportFlags;
            }
            break;
          }
          default:
            break;
        }
      }
    }
  }

  if (seq_flags && (prev_mode != priv->selected_slice_mode ||
          prev_slice.NumberOfSlicesPerFrame !=
          priv->layout_slices.NumberOfSlicesPerFrame)) {
    GST_DEBUG_OBJECT (self, "Slice mode updated");
    *seq_flags |=
        D3D12_VIDEO_ENCODER_SEQUENCE_CONTROL_FLAG_SUBREGION_LAYOUT_CHANGE;
  }

  priv->slice_updated = FALSE;

  return TRUE;
}

static gboolean
gst_d3d12_h264_enc_reconfigure (GstD3D12H264Enc * self,
    ID3D12VideoDevice * video_device, GstD3D12EncoderConfig * config,
    D3D12_VIDEO_ENCODER_SEQUENCE_CONTROL_FLAGS * seq_flags)
{
  auto encoder = GST_D3D12_ENCODER (self);
  auto priv = self->priv;
  auto prev_config = *config;

  if (!gst_d3d12_h264_enc_update_gop (self, video_device, config, seq_flags))
    return FALSE;

  if (!gst_d3d12_h264_enc_update_rate_control (self,
          video_device, config, seq_flags)) {
    return FALSE;
  }

  D3D12_FEATURE_DATA_VIDEO_ENCODER_SUPPORT support = { };
  D3D12_FEATURE_DATA_VIDEO_ENCODER_RESOLUTION_SUPPORT_LIMITS limits = { };
  D3D12_VIDEO_ENCODER_PROFILE_H264 suggested_profile = priv->profile_h264;

  support.Codec = D3D12_VIDEO_ENCODER_CODEC_H264;
  support.InputFormat = DXGI_FORMAT_NV12;
  support.CodecConfiguration = config->codec_config;
  support.CodecGopSequence = config->gop_struct;
  support.RateControl = config->rate_control;
  /* TODO: add intra-refresh support */
  support.IntraRefresh = D3D12_VIDEO_ENCODER_INTRA_REFRESH_MODE_NONE;
  support.SubregionFrameEncoding = priv->selected_slice_mode;
  support.ResolutionsListCount = 1;
  support.pResolutionList = &config->resolution;
  support.MaxReferenceFramesInDPB = priv->selected_ref_frames;
  support.pResolutionDependentSupport = &limits;
  support.SuggestedProfile.DataSize = sizeof (suggested_profile);
  support.SuggestedProfile.pH264Profile = &suggested_profile;
  support.SuggestedLevel = config->level;

  auto hr = video_device->CheckFeatureSupport
      (D3D12_FEATURE_VIDEO_ENCODER_SUPPORT, &support, sizeof (support));

  /* This is our minimum/simplest configuration
   * TODO: negotiate again depending on validation flags */
  if (!gst_d3d12_result (hr, encoder->device) ||
      !CHECK_SUPPORT_FLAG (support.SupportFlags, GENERAL_SUPPORT_OK) ||
      (support.ValidationFlags != D3D12_VIDEO_ENCODER_VALIDATION_FLAG_NONE)) {
    GST_ERROR_OBJECT (self, "Couldn't query encoder support");
    return FALSE;
  }

  /* Update rate control flags based on support flags */
  if (priv->frame_analysis) {
    if (CHECK_SUPPORT_FLAG (support.SupportFlags,
            RATE_CONTROL_FRAME_ANALYSIS_AVAILABLE)) {
      GST_INFO_OBJECT (self, "Frame analysis is enabled as requested");
      config->rate_control.Flags |=
          D3D12_VIDEO_ENCODER_RATE_CONTROL_FLAG_ENABLE_FRAME_ANALYSIS;
    } else {
      GST_INFO_OBJECT (self, "Frame analysis is not supported");
    }
  }

  if (priv->qp_init > 0) {
    switch (priv->selected_rc_mode) {
      case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_CBR:
      case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_VBR:
      case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_QVBR:
        if (CHECK_SUPPORT_FLAG (support.SupportFlags,
                RATE_CONTROL_INITIAL_QP_AVAILABLE)) {
          GST_INFO_OBJECT (self, "Initial QP %d is enabled as requested",
              priv->qp_init);
          config->rate_control.Flags |=
              D3D12_VIDEO_ENCODER_RATE_CONTROL_FLAG_ENABLE_INITIAL_QP;
        } else {
          GST_INFO_OBJECT (self, "Initial QP is not supported");
        }
        break;
      default:
        break;
    }
  }

  if (priv->qp_max >= priv->qp_min && priv->qp_min > 0) {
    switch (priv->selected_rc_mode) {
      case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_CBR:
      case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_VBR:
      case D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_QVBR:
        if (CHECK_SUPPORT_FLAG (support.SupportFlags,
                RATE_CONTROL_ADJUSTABLE_QP_RANGE_AVAILABLE)) {
          GST_INFO_OBJECT (self, "QP range [%d, %d] is enabled as requested",
              priv->qp_min, priv->qp_max);
          config->rate_control.Flags |=
              D3D12_VIDEO_ENCODER_RATE_CONTROL_FLAG_ENABLE_QP_RANGE;
        } else {
          GST_INFO_OBJECT (self, "QP range is not supported");
        }
        break;
      default:
        break;
    }
  }

  if (seq_flags) {
    if (prev_config.rate_control.Flags != config->rate_control.Flags) {
      GST_DEBUG_OBJECT (self, "Rate control flag updated");
      *seq_flags |=
          D3D12_VIDEO_ENCODER_SEQUENCE_CONTROL_FLAG_RATE_CONTROL_CHANGE;
    }
  }

  if (!gst_d3d12_h264_enc_update_slice (self, video_device, config,
          seq_flags, &support.SupportFlags)) {
    return FALSE;
  }

  config->support_flags = support.SupportFlags;

  if (!seq_flags ||
      (*seq_flags &
          D3D12_VIDEO_ENCODER_SEQUENCE_CONTROL_FLAG_GOP_SEQUENCE_CHANGE) != 0) {
    priv->gop.ForceKeyUnit ();
    gst_d3d12_h264_enc_build_sps (self, &priv->info, &config->resolution,
        priv->selected_ref_frames);
    gst_d3d12_h264_enc_build_pps (self, priv->selected_ref_frames);

    bool array_of_textures = !CHECK_SUPPORT_FLAG (config->support_flags,
        RECONSTRUCTED_FRAMES_REQUIRE_TEXTURE_ARRAYS);
    auto dpb = std::make_unique < GstD3D12H264EncDpb > (encoder->device,
        config->resolution.Width, config->resolution.Height,
        priv->selected_ref_frames,
        array_of_textures);
    if (!dpb->IsValid ()) {
      GST_ERROR_OBJECT (self, "Couldn't create dpb");
      return FALSE;
    }

    GST_DEBUG_OBJECT (self, "New DPB configured");

    priv->dpb = nullptr;
    priv->dpb = std::move (dpb);
  }

  return TRUE;
}

static gboolean
gst_d3d12_h264_enc_new_sequence (GstD3D12Encoder * encoder,
    ID3D12VideoDevice * video_device, GstVideoCodecState * state,
    GstD3D12EncoderConfig * config)
{
  auto self = GST_D3D12_H264_ENC (encoder);
  auto priv = self->priv;
  auto info = &state->info;

  std::lock_guard < std::mutex > lk (priv->prop_lock);

  priv->dpb = nullptr;
  priv->info = state->info;

  config->profile_desc.DataSize = sizeof (D3D12_VIDEO_ENCODER_PROFILE_H264);
  config->profile_desc.pH264Profile = &priv->profile_h264;

  config->codec_config.DataSize =
      sizeof (D3D12_VIDEO_ENCODER_CODEC_CONFIGURATION_H264);
  config->codec_config.pH264Config = &priv->config_h264;

  config->level.DataSize = sizeof (D3D12_VIDEO_ENCODER_LEVELS_H264);
  config->level.pH264LevelSetting = &priv->level_h264;

  config->layout.DataSize =
      sizeof
      (D3D12_VIDEO_ENCODER_PICTURE_CONTROL_SUBREGIONS_LAYOUT_DATA_SLICES);
  config->layout.pSlicesPartition_H264 = &priv->layout_slices;

  config->gop_struct.DataSize =
      sizeof (D3D12_VIDEO_ENCODER_SEQUENCE_GOP_STRUCTURE_H264);
  config->gop_struct.pH264GroupOfPictures = &priv->gop_struct_h264;

  config->resolution.Width = GST_ROUND_UP_16 (info->width);
  config->resolution.Height = GST_ROUND_UP_16 (info->height);

  priv->selected_profile = GST_H264_PROFILE_MAIN;
  priv->profile_h264 = D3D12_VIDEO_ENCODER_PROFILE_H264_MAIN;

  auto allowed_caps =
      gst_pad_get_allowed_caps (GST_VIDEO_ENCODER_SRC_PAD (encoder));
  if (allowed_caps && !gst_caps_is_any (allowed_caps)) {
    allowed_caps = gst_caps_fixate (gst_caps_make_writable (allowed_caps));
    auto s = gst_caps_get_structure (allowed_caps, 0);
    auto profile_str = gst_structure_get_string (s, "profile");
    if (g_strcmp0 (profile_str, "high") == 0) {
      priv->selected_profile = GST_H264_PROFILE_HIGH;
      priv->profile_h264 = D3D12_VIDEO_ENCODER_PROFILE_H264_HIGH;
    } else if (g_strcmp0 (profile_str, "constrained-baseline") == 0) {
      priv->selected_profile = GST_H264_PROFILE_BASELINE;
      priv->profile_h264 = D3D12_VIDEO_ENCODER_PROFILE_H264_MAIN;
    } else {
      priv->selected_profile = GST_H264_PROFILE_MAIN;
      priv->profile_h264 = D3D12_VIDEO_ENCODER_PROFILE_H264_MAIN;
    }
  }

  gst_clear_caps (&allowed_caps);

  const gchar *profile_str = nullptr;
  switch (priv->selected_profile) {
    case GST_H264_PROFILE_BASELINE:
      profile_str = "constrained-baseline";
      break;
    case GST_H264_PROFILE_MAIN:
      profile_str = "main";
      break;
    case GST_H264_PROFILE_HIGH:
      profile_str = "high";
      break;
    default:
      g_assert_not_reached ();
      break;
  }

  auto caps = gst_caps_new_simple ("video/x-h264",
      "alignment", G_TYPE_STRING, "au", "profile", G_TYPE_STRING, profile_str,
      "stream-format", G_TYPE_STRING, "byte-stream", nullptr);
  auto output_state =
      gst_video_encoder_set_output_state (GST_VIDEO_ENCODER (self), caps,
      state);
  gst_video_codec_state_unref (output_state);

  D3D12_FEATURE_DATA_VIDEO_ENCODER_CODEC_PICTURE_CONTROL_SUPPORT
      feature_pic_ctrl = { };
  feature_pic_ctrl.Codec = D3D12_VIDEO_ENCODER_CODEC_H264;
  feature_pic_ctrl.Profile.DataSize = sizeof (priv->profile_h264);
  feature_pic_ctrl.Profile.pH264Profile = &priv->profile_h264;
  feature_pic_ctrl.PictureSupport.DataSize = sizeof (priv->pic_ctrl_support);
  feature_pic_ctrl.PictureSupport.pH264Support = &priv->pic_ctrl_support;
  auto hr = video_device->CheckFeatureSupport
      (D3D12_FEATURE_VIDEO_ENCODER_CODEC_PICTURE_CONTROL_SUPPORT,
      &feature_pic_ctrl, sizeof (feature_pic_ctrl));
  if (!gst_d3d12_result (hr, encoder->device) || !feature_pic_ctrl.IsSupported) {
    GST_ERROR_OBJECT (self, "Couldn't query picture control support");
    return FALSE;
  }

  if (info->fps_n > 0 && info->fps_d > 0) {
    config->rate_control.TargetFrameRate.Numerator = info->fps_n;
    config->rate_control.TargetFrameRate.Denominator = info->fps_d;
  } else {
    config->rate_control.TargetFrameRate.Numerator = 30;
    config->rate_control.TargetFrameRate.Denominator = 1;
  }

  priv->config_h264.ConfigurationFlags =
      D3D12_VIDEO_ENCODER_CODEC_CONFIGURATION_H264_FLAG_NONE;
  priv->config_h264.DirectModeConfig =
      D3D12_VIDEO_ENCODER_CODEC_CONFIGURATION_H264_DIRECT_MODES_DISABLED;
  priv->config_h264.DisableDeblockingFilterConfig =
      D3D12_VIDEO_ENCODER_CODEC_CONFIGURATION_H264_SLICES_DEBLOCKING_MODE_0_ALL_LUMA_CHROMA_SLICE_BLOCK_EDGES_ALWAYS_FILTERED;

  if (priv->selected_profile != GST_H264_PROFILE_BASELINE) {
    priv->config_h264.ConfigurationFlags |=
        D3D12_VIDEO_ENCODER_CODEC_CONFIGURATION_H264_FLAG_ENABLE_CABAC_ENCODING;
  }

  return gst_d3d12_h264_enc_reconfigure (self, video_device, config, nullptr);
}

static gboolean
gst_d3d12_h264_enc_foreach_caption_meta (GstBuffer * buffer, GstMeta ** meta,
    GArray * cc_sei)
{
  if ((*meta)->info->api != GST_VIDEO_CAPTION_META_API_TYPE)
    return TRUE;

  auto cc_meta = (GstVideoCaptionMeta *) (*meta);
  if (cc_meta->caption_type != GST_VIDEO_CAPTION_TYPE_CEA708_RAW)
    return TRUE;

  GstH264SEIMessage sei = { };
  sei.payloadType = GST_H264_SEI_REGISTERED_USER_DATA;
  auto rud = &sei.payload.registered_user_data;

  rud->country_code = 181;
  rud->size = cc_meta->size + 10;

  auto data = (guint8 *) g_malloc (rud->size);
  data[0] = 0;                  /* 16-bits itu_t_t35_provider_code */
  data[1] = 49;
  data[2] = 'G';                /* 32-bits ATSC_user_identifier */
  data[3] = 'A';
  data[4] = '9';
  data[5] = '4';
  data[6] = 3;                  /* 8-bits ATSC1_data_user_data_type_code */
  /* 8-bits:
   * 1 bit process_em_data_flag (0)
   * 1 bit process_cc_data_flag (1)
   * 1 bit additional_data_flag (0)
   * 5-bits cc_count
   */
  data[7] = ((cc_meta->size / 3) & 0x1f) | 0x40;
  data[8] = 255;                /* 8 bits em_data, unused */
  memcpy (data + 9, cc_meta->data, cc_meta->size);
  data[cc_meta->size + 9] = 255;        /* 8 marker bits */

  rud->data = data;

  g_array_append_val (cc_sei, sei);

  return TRUE;
}

static gboolean
gst_d3d12_h264_enc_start_frame (GstD3D12Encoder * encoder,
    ID3D12VideoDevice * video_device, GstVideoCodecFrame * frame,
    D3D12_VIDEO_ENCODER_SEQUENCE_CONTROL_DESC * seq_ctrl,
    D3D12_VIDEO_ENCODER_PICTURE_CONTROL_DESC * picture_ctrl,
    D3D12_VIDEO_ENCODER_RECONSTRUCTED_PICTURE * recon_pic,
    GstD3D12EncoderConfig * config, gboolean * need_new_session)
{
  auto self = GST_D3D12_H264_ENC (encoder);
  auto priv = self->priv;
  static guint8 aud_data[] = {
    0x00, 0x00, 0x00, 0x01, 0x09, 0xf0
  };

  *need_new_session = FALSE;

  std::lock_guard < std::mutex > lk (priv->prop_lock);
  seq_ctrl->Flags = D3D12_VIDEO_ENCODER_SEQUENCE_CONTROL_FLAG_NONE;

  /* Reset GOP struct on force-keyunit */
  if (GST_VIDEO_CODEC_FRAME_IS_FORCE_KEYFRAME (frame)) {
    GST_DEBUG_OBJECT (self, "Force keyframe requested");
    priv->gop.ForceKeyUnit ();
  }

  auto prev_level = priv->level_h264;
  if (!gst_d3d12_h264_enc_reconfigure (self, video_device, config,
          &seq_ctrl->Flags)) {
    GST_ERROR_OBJECT (self, "Reconfigure failed");
    return FALSE;
  }

  if (seq_ctrl->Flags != D3D12_VIDEO_ENCODER_SEQUENCE_CONTROL_FLAG_NONE) {
    *need_new_session =
        gst_d3d12_encoder_check_needs_new_session (config->support_flags,
        seq_ctrl->Flags);
  }

  if (priv->level_h264 != prev_level)
    *need_new_session = TRUE;

  if (*need_new_session) {
    seq_ctrl->Flags = D3D12_VIDEO_ENCODER_SEQUENCE_CONTROL_FLAG_NONE;

    GST_DEBUG_OBJECT (self, "Needs new session, forcing IDR");
    priv->gop.ForceKeyUnit ();
  }

  priv->gop.FillPicCtrl (priv->pic_control_h264);

  if (priv->pic_control_h264.FrameType ==
      D3D12_VIDEO_ENCODER_FRAME_TYPE_H264_IDR_FRAME) {
    GST_LOG_OBJECT (self, "Sync point at frame %" G_GUINT64_FORMAT,
        priv->display_order);
    GST_VIDEO_CODEC_FRAME_SET_SYNC_POINT (frame);
  }

  seq_ctrl->IntraRefreshConfig.Mode =
      D3D12_VIDEO_ENCODER_INTRA_REFRESH_MODE_NONE;
  seq_ctrl->IntraRefreshConfig.IntraRefreshDuration = 0;
  seq_ctrl->RateControl = config->rate_control;
  seq_ctrl->PictureTargetResolution = config->resolution;
  seq_ctrl->SelectedLayoutMode = priv->selected_slice_mode;
  seq_ctrl->FrameSubregionsLayoutData = config->layout;
  seq_ctrl->CodecGopSequence = config->gop_struct;

  picture_ctrl->IntraRefreshFrameIndex = 0;
  /* TODO: b frame can be non-reference picture */
  picture_ctrl->Flags = priv->selected_ref_frames > 0 ?
      D3D12_VIDEO_ENCODER_PICTURE_CONTROL_FLAG_USED_AS_REFERENCE_PICTURE :
      D3D12_VIDEO_ENCODER_PICTURE_CONTROL_FLAG_NONE;
  picture_ctrl->PictureControlCodecData.DataSize =
      sizeof (priv->pic_control_h264);
  picture_ctrl->PictureControlCodecData.pH264PicData = &priv->pic_control_h264;

  if (!priv->dpb->StartFrame (picture_ctrl->Flags ==
          D3D12_VIDEO_ENCODER_PICTURE_CONTROL_FLAG_USED_AS_REFERENCE_PICTURE,
          &priv->pic_control_h264, recon_pic, &picture_ctrl->ReferenceFrames,
          priv->display_order)) {
    GST_ERROR_OBJECT (self, "Start frame failed");
    return FALSE;
  }

  priv->display_order++;

  priv->pic_control_h264.Flags =
      D3D12_VIDEO_ENCODER_PICTURE_CONTROL_CODEC_DATA_H264_FLAG_NONE;
  /* FIXME: count L1 too */
  priv->pic_control_h264.pic_parameter_set_id =
      priv->pic_control_h264.List0ReferenceFramesCount > 1 ?
      priv->pic_control_h264.List0ReferenceFramesCount - 1 : 0;
  priv->pic_control_h264.adaptive_ref_pic_marking_mode_flag = 0;
  priv->pic_control_h264.RefPicMarkingOperationsCommandsCount = 0;
  priv->pic_control_h264.pRefPicMarkingOperationsCommands = nullptr;
  priv->pic_control_h264.List0RefPicModificationsCount = 0;
  priv->pic_control_h264.pList0RefPicModifications = nullptr;
  priv->pic_control_h264.List1RefPicModificationsCount = 0;
  priv->pic_control_h264.pList1RefPicModifications = nullptr;
  priv->pic_control_h264.QPMapValuesCount = 0;
  priv->pic_control_h264.pRateControlQPMap = nullptr;

  if (priv->pic_control_h264.FrameType ==
      D3D12_VIDEO_ENCODER_FRAME_TYPE_H264_IDR_FRAME) {
    auto buf_size = priv->sps.bytes.size () + priv->pps[0].bytes.size ();
    if (priv->aud)
      buf_size += sizeof (aud_data);

    auto output_buf = gst_buffer_new_and_alloc (buf_size);
    GstMapInfo map_info;
    gst_buffer_map (output_buf, &map_info, GST_MAP_WRITE);
    auto data = (guint8 *) map_info.data;

    if (priv->aud) {
      memcpy (data, aud_data, sizeof (aud_data));
      data += sizeof (aud_data);
    }

    memcpy (data, priv->sps.bytes.data (), priv->sps.bytes.size ());
    data += priv->sps.bytes.size ();

    memcpy (data, priv->pps[0].bytes.data (), priv->pps[0].bytes.size ());
    gst_buffer_unmap (output_buf, &map_info);
    frame->output_buffer = output_buf;

    priv->last_pps_id = 0;
  } else if (priv->pic_control_h264.pic_parameter_set_id != priv->last_pps_id) {
    const auto & cur_pps =
        priv->pps[priv->pic_control_h264.pic_parameter_set_id];
    auto buf_size = cur_pps.bytes.size ();

    if (priv->aud)
      buf_size += sizeof (aud_data);

    auto output_buf = gst_buffer_new_and_alloc (buf_size);
    GstMapInfo map_info;
    gst_buffer_map (output_buf, &map_info, GST_MAP_WRITE);
    auto data = (guint8 *) map_info.data;

    if (priv->aud) {
      memcpy (data, aud_data, sizeof (aud_data));
      data += sizeof (aud_data);
    }

    memcpy (data, cur_pps.bytes.data (), cur_pps.bytes.size ());
    gst_buffer_unmap (output_buf, &map_info);
    frame->output_buffer = output_buf;

    priv->last_pps_id = priv->pic_control_h264.pic_parameter_set_id;
  } else if (priv->aud) {
    auto buf_size = sizeof (aud_data);
    auto output_buf = gst_buffer_new_and_alloc (buf_size);
    GstMapInfo map_info;
    gst_buffer_map (output_buf, &map_info, GST_MAP_WRITE);
    memcpy (map_info.data, aud_data, sizeof (aud_data));
    gst_buffer_unmap (output_buf, &map_info);
    frame->output_buffer = output_buf;
  }

  if (priv->cc_insert != GST_D3D12_ENCODER_SEI_DISABLED) {
    g_array_set_size (priv->cc_sei, 0);
    gst_buffer_foreach_meta (frame->input_buffer,
        (GstBufferForeachMetaFunc) gst_d3d12_h264_enc_foreach_caption_meta,
        priv->cc_sei);
    if (priv->cc_sei->len > 0) {
      auto mem = gst_h264_create_sei_memory (4, priv->cc_sei);
      if (mem) {
        GST_TRACE_OBJECT (self, "Inserting CC SEI");

        if (!frame->output_buffer)
          frame->output_buffer = gst_buffer_new ();

        gst_buffer_append_memory (frame->output_buffer, mem);
      }
    }
  }

  return TRUE;
}

static gboolean
gst_d3d12_h264_enc_end_frame (GstD3D12Encoder * encoder)
{
  auto self = GST_D3D12_H264_ENC (encoder);
  auto priv = self->priv;

  priv->dpb->EndFrame ();

  return TRUE;
}

void
gst_d3d12_h264_enc_register (GstPlugin * plugin, GstD3D12Device * device,
    ID3D12VideoDevice * video_device, guint rank)
{
  HRESULT hr;
  std::vector < std::string > profiles;

  GST_DEBUG_CATEGORY_INIT (gst_d3d12_h264_enc_debug,
      "d3d12h264enc", 0, "d3d12h264enc");

  D3D12_FEATURE_DATA_VIDEO_ENCODER_CODEC feature_codec = { };
  feature_codec.Codec = D3D12_VIDEO_ENCODER_CODEC_H264;
  hr = video_device->CheckFeatureSupport (D3D12_FEATURE_VIDEO_ENCODER_CODEC,
      &feature_codec, sizeof (feature_codec));

  if (!gst_d3d12_result (hr, device) || !feature_codec.IsSupported) {
    GST_INFO_OBJECT (device, "Device does not support H.264 encoding");
    return;
  }

  D3D12_FEATURE_DATA_VIDEO_ENCODER_PROFILE_LEVEL feature_profile_level = { };
  D3D12_VIDEO_ENCODER_PROFILE_H264 profile_h264 =
      D3D12_VIDEO_ENCODER_PROFILE_H264_MAIN;
  D3D12_VIDEO_ENCODER_LEVELS_H264 level_h264_min;
  D3D12_VIDEO_ENCODER_LEVELS_H264 level_h264_max;

  feature_profile_level.Codec = D3D12_VIDEO_ENCODER_CODEC_H264;
  feature_profile_level.Profile.DataSize = sizeof (profile_h264);
  feature_profile_level.Profile.pH264Profile = &profile_h264;
  feature_profile_level.MinSupportedLevel.DataSize = sizeof (level_h264_min);
  feature_profile_level.MinSupportedLevel.pH264LevelSetting = &level_h264_min;
  feature_profile_level.MaxSupportedLevel.DataSize = sizeof (level_h264_max);
  feature_profile_level.MaxSupportedLevel.pH264LevelSetting = &level_h264_max;

  D3D12_FEATURE_DATA_VIDEO_ENCODER_INPUT_FORMAT feature_input_format = { };
  feature_input_format.Codec = D3D12_VIDEO_ENCODER_CODEC_H264;
  feature_input_format.Profile = feature_profile_level.Profile;

  hr = video_device->CheckFeatureSupport
      (D3D12_FEATURE_VIDEO_ENCODER_PROFILE_LEVEL, &feature_profile_level,
      sizeof (feature_profile_level));
  if (!gst_d3d12_result (hr, device) || !feature_profile_level.IsSupported) {
    GST_WARNING_OBJECT (device, "Main profile is not supported");
    return;
  }

  feature_input_format.Format = DXGI_FORMAT_NV12;
  hr = video_device->CheckFeatureSupport
      (D3D12_FEATURE_VIDEO_ENCODER_INPUT_FORMAT, &feature_input_format,
      sizeof (feature_input_format));
  if (!gst_d3d12_result (hr, device) || !feature_input_format.IsSupported) {
    GST_WARNING_OBJECT (device, "NV12 format is not supported");
    return;
  }

  profiles.push_back ("constrained-baseline");
  profiles.push_back ("main");
  GST_INFO_OBJECT (device, "Main profile is supported, level [%d, %d]",
      level_h264_min, level_h264_max);

  profile_h264 = D3D12_VIDEO_ENCODER_PROFILE_H264_HIGH;
  hr = video_device->CheckFeatureSupport
      (D3D12_FEATURE_VIDEO_ENCODER_PROFILE_LEVEL, &feature_profile_level,
      sizeof (feature_profile_level));
  if (gst_d3d12_result (hr, device) && feature_profile_level.IsSupported) {
    profiles.push_back ("high");
    GST_INFO_OBJECT (device, "High profile is supported, level [%d, %d]",
        level_h264_min, level_h264_max);
  }

  if (profiles.empty ()) {
    GST_WARNING_OBJECT (device, "Couldn't find supported profile");
    return;
  }

  D3D12_FEATURE_DATA_VIDEO_ENCODER_OUTPUT_RESOLUTION_RATIOS_COUNT ratios_count
      = { };
  ratios_count.Codec = D3D12_VIDEO_ENCODER_CODEC_H264;
  hr = video_device->CheckFeatureSupport
      (D3D12_FEATURE_VIDEO_ENCODER_OUTPUT_RESOLUTION_RATIOS_COUNT,
      &ratios_count, sizeof (ratios_count));
  if (!gst_d3d12_result (hr, device)) {
    GST_WARNING_OBJECT (device,
        "Couldn't query output resolution ratios count");
    return;
  }

  std::vector < D3D12_VIDEO_ENCODER_PICTURE_RESOLUTION_RATIO_DESC > ratios;

  D3D12_FEATURE_DATA_VIDEO_ENCODER_OUTPUT_RESOLUTION feature_resolution = { };
  feature_resolution.Codec = D3D12_VIDEO_ENCODER_CODEC_H264;
  feature_resolution.ResolutionRatiosCount = ratios_count.ResolutionRatiosCount;
  if (ratios_count.ResolutionRatiosCount > 0) {
    ratios.resize (ratios_count.ResolutionRatiosCount);
    feature_resolution.pResolutionRatios = &ratios[0];
  }

  hr = video_device->CheckFeatureSupport
      (D3D12_FEATURE_VIDEO_ENCODER_OUTPUT_RESOLUTION, &feature_resolution,
      sizeof (feature_resolution));
  if (!gst_d3d12_result (hr, device) || !feature_resolution.IsSupported) {
    GST_WARNING_OBJECT (device, "Couldn't query output resolution");
    return;
  }

  GST_INFO_OBJECT (device,
      "Device supported resolution %ux%u - %ux%u, align requirement %u, %u",
      feature_resolution.MinResolutionSupported.Width,
      feature_resolution.MinResolutionSupported.Height,
      feature_resolution.MaxResolutionSupported.Width,
      feature_resolution.MaxResolutionSupported.Height,
      feature_resolution.ResolutionWidthMultipleRequirement,
      feature_resolution.ResolutionHeightMultipleRequirement);

  guint rc_support = 0;
  D3D12_FEATURE_DATA_VIDEO_ENCODER_RATE_CONTROL_MODE feature_rate_control = { };
  feature_rate_control.Codec = D3D12_VIDEO_ENCODER_CODEC_H264;
  feature_rate_control.RateControlMode =
      D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_CQP;

  hr = video_device->CheckFeatureSupport
      (D3D12_FEATURE_VIDEO_ENCODER_RATE_CONTROL_MODE, &feature_rate_control,
      sizeof (feature_rate_control));
  if (SUCCEEDED (hr) && feature_rate_control.IsSupported) {
    GST_INFO_OBJECT (device, "CQP suported");
    rc_support |= (1 << D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_CQP);
  }

  feature_rate_control.RateControlMode =
      D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_CBR;
  hr = video_device->CheckFeatureSupport
      (D3D12_FEATURE_VIDEO_ENCODER_RATE_CONTROL_MODE, &feature_rate_control,
      sizeof (feature_rate_control));
  if (SUCCEEDED (hr) && feature_rate_control.IsSupported) {
    GST_INFO_OBJECT (device, "CBR suported");
    rc_support |= (1 << D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_CBR);
  }

  feature_rate_control.RateControlMode =
      D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_VBR;
  hr = video_device->CheckFeatureSupport
      (D3D12_FEATURE_VIDEO_ENCODER_RATE_CONTROL_MODE, &feature_rate_control,
      sizeof (feature_rate_control));
  if (SUCCEEDED (hr) && feature_rate_control.IsSupported) {
    GST_INFO_OBJECT (device, "VBR suported");
    rc_support |= (1 << D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_VBR);
  }

  feature_rate_control.RateControlMode =
      D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_QVBR;
  hr = video_device->CheckFeatureSupport
      (D3D12_FEATURE_VIDEO_ENCODER_RATE_CONTROL_MODE, &feature_rate_control,
      sizeof (feature_rate_control));
  if (SUCCEEDED (hr) && feature_rate_control.IsSupported) {
    GST_INFO_OBJECT (device, "VBR suported");
    rc_support |= (1 << D3D12_VIDEO_ENCODER_RATE_CONTROL_MODE_QVBR);
  }

  if (!rc_support) {
    GST_WARNING_OBJECT (device, "Couldn't find supported rate control mode");
    return;
  }

  profile_h264 = D3D12_VIDEO_ENCODER_PROFILE_H264_MAIN;
  D3D12_FEATURE_DATA_VIDEO_ENCODER_FRAME_SUBREGION_LAYOUT_MODE
      feature_layout = { };
  feature_layout.Codec = D3D12_VIDEO_ENCODER_CODEC_H264;
  feature_layout.Profile.DataSize = sizeof (profile_h264);
  feature_layout.Profile.pH264Profile = &profile_h264;
  feature_layout.Level.DataSize = sizeof (D3D12_VIDEO_ENCODER_LEVELS_H264);

  D3D12_VIDEO_ENCODER_FRAME_SUBREGION_LAYOUT_MODE layout_modes[] = {
    D3D12_VIDEO_ENCODER_FRAME_SUBREGION_LAYOUT_MODE_FULL_FRAME,
    D3D12_VIDEO_ENCODER_FRAME_SUBREGION_LAYOUT_MODE_BYTES_PER_SUBREGION,
    D3D12_VIDEO_ENCODER_FRAME_SUBREGION_LAYOUT_MODE_SQUARE_UNITS_PER_SUBREGION_ROW_UNALIGNED,
    D3D12_VIDEO_ENCODER_FRAME_SUBREGION_LAYOUT_MODE_UNIFORM_PARTITIONING_ROWS_PER_SUBREGION,
    D3D12_VIDEO_ENCODER_FRAME_SUBREGION_LAYOUT_MODE_UNIFORM_PARTITIONING_SUBREGIONS_PER_FRAME,
  };

  guint slice_mode_support = 0;
  for (guint i = 0; i < G_N_ELEMENTS (layout_modes); i++) {
    feature_layout.SubregionMode = layout_modes[i];
    for (guint level = (guint) level_h264_min; level <= (guint) level_h264_max;
        level++) {
      auto level_h264 = (D3D12_VIDEO_ENCODER_LEVELS_H264) level;
      feature_layout.Level.pH264LevelSetting = &level_h264;
      hr = video_device->CheckFeatureSupport
          (D3D12_FEATURE_VIDEO_ENCODER_FRAME_SUBREGION_LAYOUT_MODE,
          &feature_layout, sizeof (feature_layout));
      if (SUCCEEDED (hr) && feature_layout.IsSupported) {
        slice_mode_support |= (1 << layout_modes[i]);
        break;
      }
    }
  }

  if (!slice_mode_support) {
    GST_WARNING_OBJECT (device, "No supported subregion layout");
    return;
  }

  if (slice_mode_support & (1 <<
          D3D12_VIDEO_ENCODER_FRAME_SUBREGION_LAYOUT_MODE_FULL_FRAME)
      == 0) {
    GST_WARNING_OBJECT (device, "Full frame encoding is not supported");
    return;
  }

  auto subregions =
      g_flags_to_string (GST_TYPE_D3D12_ENCODER_SUBREGION_LAYOUT_SUPPORT,
      slice_mode_support);
  GST_INFO_OBJECT (device, "Supported subregion modes: \"%s\"", subregions);
  g_free (subregions);

  D3D12_VIDEO_ENCODER_CODEC_PICTURE_CONTROL_SUPPORT_H264 picture_ctrl_h264;
  D3D12_FEATURE_DATA_VIDEO_ENCODER_CODEC_PICTURE_CONTROL_SUPPORT
      feature_pic_ctrl = { };

  feature_pic_ctrl.Codec = D3D12_VIDEO_ENCODER_CODEC_H264;
  feature_pic_ctrl.Profile.DataSize = sizeof (profile_h264);
  feature_pic_ctrl.Profile.pH264Profile = &profile_h264;
  feature_pic_ctrl.PictureSupport.DataSize = sizeof (picture_ctrl_h264);
  feature_pic_ctrl.PictureSupport.pH264Support = &picture_ctrl_h264;

  hr = video_device->CheckFeatureSupport
      (D3D12_FEATURE_VIDEO_ENCODER_CODEC_PICTURE_CONTROL_SUPPORT,
      &feature_pic_ctrl, sizeof (feature_pic_ctrl));
  if (!gst_d3d12_result (hr, device) || !feature_pic_ctrl.IsSupported) {
    GST_WARNING_OBJECT (device, "Couldn't query picture control support");
    return;
  }

  GST_INFO_OBJECT (device, "MaxL0ReferencesForP: %u, MaxL0ReferencesForB: %u, "
      "MaxL1ReferencesForB: %u, MaxLongTermReferences: %u, MaxDPBCapacity %u",
      picture_ctrl_h264.MaxL0ReferencesForP,
      picture_ctrl_h264.MaxL0ReferencesForB,
      picture_ctrl_h264.MaxL1ReferencesForB,
      picture_ctrl_h264.MaxLongTermReferences,
      picture_ctrl_h264.MaxDPBCapacity);

  std::string resolution_str = "width = (int) [" +
      std::to_string (feature_resolution.MinResolutionSupported.Width) + ", " +
      std::to_string (feature_resolution.MaxResolutionSupported.Width) +
      "], height = (int) [" +
      std::to_string (feature_resolution.MinResolutionSupported.Height) + ", " +
      std::to_string (feature_resolution.MaxResolutionSupported.Height) + " ]";
  std::string sink_caps_str = "video/x-raw, format = (string) NV12, " +
      resolution_str + ", interlace-mode = (string) progressive";

  std::string src_caps_str = "video/x-h264, " + resolution_str +
      ", stream-format = (string) byte-stream, alignment = (string) au, ";
  if (profiles.size () == 1) {
    src_caps_str += "profile = (string) " + profiles[0];
  } else {
    src_caps_str += "profile = (string) { ";
    std::reverse (profiles.begin (), profiles.end ());
    for (size_t i = 0; i < profiles.size (); i++) {
      if (i != 0)
        src_caps_str += ", ";
      src_caps_str += profiles[i];
    }
    src_caps_str += " }";
  }

  auto sysmem_caps = gst_caps_from_string (sink_caps_str.c_str ());
  auto sink_caps = gst_caps_copy (sysmem_caps);
  gst_caps_set_features_simple (sink_caps,
      gst_caps_features_new (GST_CAPS_FEATURE_MEMORY_D3D12_MEMORY, nullptr));
  gst_caps_append (sink_caps, sysmem_caps);
  auto src_caps = gst_caps_from_string (src_caps_str.c_str ());

  GST_MINI_OBJECT_FLAG_SET (sink_caps, GST_MINI_OBJECT_FLAG_MAY_BE_LEAKED);
  GST_MINI_OBJECT_FLAG_SET (src_caps, GST_MINI_OBJECT_FLAG_MAY_BE_LEAKED);

  GstD3D12H264EncClassData *cdata = new GstD3D12H264EncClassData ();
  g_object_get (device, "adapter-luid", &cdata->luid,
      "device-id", &cdata->device_id, "vendor-id", &cdata->vendor_id,
      "description", &cdata->description, nullptr);
  cdata->sink_caps = sink_caps;
  cdata->src_caps = src_caps;
  cdata->rc_support = rc_support;
  cdata->slice_mode_support = slice_mode_support;

  GType type;
  gchar *type_name;
  gchar *feature_name;
  guint index = 0;
  GTypeInfo type_info = {
    sizeof (GstD3D12H264EncClass),
    nullptr,
    nullptr,
    (GClassInitFunc) gst_d3d12_h264_enc_class_init,
    nullptr,
    nullptr,
    sizeof (GstD3D12H264Enc),
    0,
    (GInstanceInitFunc) gst_d3d12_h264_enc_init,
  };

  type_info.class_data = cdata;

  type_name = g_strdup ("GstD3D12H264Enc");
  feature_name = g_strdup ("d3d12h264enc");
  while (g_type_from_name (type_name)) {
    index++;
    g_free (type_name);
    g_free (feature_name);
    type_name = g_strdup_printf ("GstD3D12H264Device%dEnc", index);
    feature_name = g_strdup_printf ("d3d12h264device%denc", index);
  }

  type = g_type_register_static (GST_TYPE_D3D12_ENCODER,
      type_name, &type_info, (GTypeFlags) 0);

  if (rank > 0 && index != 0)
    rank--;

  if (index != 0)
    gst_element_type_set_skip_documentation (type);

  if (!gst_element_register (plugin, feature_name, rank, type))
    GST_WARNING ("Failed to register plugin '%s'", type_name);

  g_free (type_name);
  g_free (feature_name);
}
