/*
 * This file is part of FFmpeg.
 *
 * FFmpeg is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * FFmpeg is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with FFmpeg; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include "libavutil/avassert.h"
#include "libavutil/random_seed.h"
#include "libavutil/opt.h"
#include "libavutil/vulkan_spirv.h"
#include "vulkan_filter.h"
#include "filters.h"
#include "video.h"

#define IN_A  0
#define IN_B  1
#define IN_NB 2

typedef struct XFadeParameters {
    float progress;
} XFadeParameters;

typedef struct XFadeVulkanContext {
    FFVulkanContext     vkctx;

    int                 transition;
    int64_t             duration;
    int64_t             offset;

    int                 initialized;
    FFVkExecPool        e;
    AVVulkanDeviceQueueFamily *qf;
    FFVulkanShader      shd;
    VkSampler           sampler;

    // PTS when the fade should start (in IN_A timebase)
    int64_t             start_pts;

    // PTS offset between IN_A and IN_B
    int64_t             inputs_offset_pts;

    // Duration of the transition
    int64_t             duration_pts;

    // Current PTS of the first input (IN_A)
    int64_t             pts;

    // If frames are currently just passed through
    // unmodified, like before and after the actual
    // transition.
    int                 passthrough;

    int                 status[IN_NB];
} XFadeVulkanContext;

enum XFadeTransitions {
    FADE,
    WIPELEFT,
    WIPERIGHT,
    WIPEUP,
    WIPEDOWN,
    SLIDEDOWN,
    SLIDEUP,
    SLIDELEFT,
    SLIDERIGHT,
    CIRCLEOPEN,
    CIRCLECLOSE,
    DISSOLVE,
    PIXELIZE,
    WIPETL,
    WIPETR,
    WIPEBL,
    WIPEBR,
    NB_TRANSITIONS,
};

static const char transition_fade[] = {
    C(0, void transition(int idx, ivec2 pos, float progress)                   )
    C(0, {                                                                     )
    C(1,     vec4 a = texture(a_images[idx], pos);                             )
    C(1,     vec4 b = texture(b_images[idx], pos);                             )
    C(1,     imageStore(output_images[idx], pos, mix(a, b, progress));         )
    C(0, }                                                                     )
};

static const char transition_wipeleft[] = {
    C(0, void transition(int idx, ivec2 pos, float progress)                   )
    C(0, {                                                                     )
    C(1,     ivec2 size = imageSize(output_images[idx]);                       )
    C(1,     int  s = int(size.x * (1.0 - progress));                          )
    C(1,     vec4 a = texture(a_images[idx], pos);                             )
    C(1,     vec4 b = texture(b_images[idx], pos);                             )
    C(1,     imageStore(output_images[idx], pos, pos.x > s ? b : a);           )
    C(0, }                                                                     )
};

static const char transition_wiperight[] = {
    C(0, void transition(int idx, ivec2 pos, float progress)                   )
    C(0, {                                                                     )
    C(1,     ivec2 size = imageSize(output_images[idx]);                       )
    C(1,     int  s = int(size.x * progress);                                  )
    C(1,     vec4 a = texture(a_images[idx], pos);                             )
    C(1,     vec4 b = texture(b_images[idx], pos);                             )
    C(1,     imageStore(output_images[idx], pos, pos.x > s ? a : b);           )
    C(0, }                                                                     )
};

static const char transition_wipeup[] = {
    C(0, void transition(int idx, ivec2 pos, float progress)                   )
    C(0, {                                                                     )
    C(1,     ivec2 size = imageSize(output_images[idx]);                       )
    C(1,     int  s = int(size.y * (1.0 - progress));                          )
    C(1,     vec4 a = texture(a_images[idx], pos);                             )
    C(1,     vec4 b = texture(b_images[idx], pos);                             )
    C(1,     imageStore(output_images[idx], pos, pos.y > s ? b : a);           )
    C(0, }                                                                     )
};

static const char transition_wipedown[] = {
    C(0, void transition(int idx, ivec2 pos, float progress)                   )
    C(0, {                                                                     )
    C(1,     ivec2 size = imageSize(output_images[idx]);                       )
    C(1,     int  s = int(size.y * progress);                                  )
    C(1,     vec4 a = texture(a_images[idx], pos);                             )
    C(1,     vec4 b = texture(b_images[idx], pos);                             )
    C(1,     imageStore(output_images[idx], pos, pos.y > s ? a : b);           )
    C(0, }                                                                     )
};

#define SHADER_SLIDE_COMMON                                                              \
    C(0, void slide(int idx, ivec2 pos, float progress, ivec2 direction)               ) \
    C(0, {                                                                             ) \
    C(1,     ivec2 size = imageSize(output_images[idx]);                               ) \
    C(1,     ivec2 pi = ivec2(progress * size);                                        ) \
    C(1,     ivec2 p = pos + pi * direction;                                           ) \
    C(1,     ivec2 f = p % size;                                                       ) \
    C(1,     f = f + size * ivec2(f.x < 0, f.y < 0);                                   ) \
    C(1,     vec4 a = texture(a_images[idx], f);                                       ) \
    C(1,     vec4 b = texture(b_images[idx], f);                                       ) \
    C(1,     vec4 r = (p.y >= 0 && p.x >= 0 && size.y > p.y &&  size.x > p.x) ? a : b; ) \
    C(1,     imageStore(output_images[idx], pos, r);                                   ) \
    C(0, }                                                                             )

static const char transition_slidedown[] = {
    SHADER_SLIDE_COMMON
    C(0, void transition(int idx, ivec2 pos, float progress)                   )
    C(0, {                                                                     )
    C(1,     slide(idx, pos, progress, ivec2(0, -1));                          )
    C(0, }                                                                     )
};

static const char transition_slideup[] = {
    SHADER_SLIDE_COMMON
    C(0, void transition(int idx, ivec2 pos, float progress)                   )
    C(0, {                                                                     )
    C(1,     slide(idx, pos, progress, ivec2(0, +1));                          )
    C(0, }                                                                     )
};

static const char transition_slideleft[] = {
    SHADER_SLIDE_COMMON
    C(0, void transition(int idx, ivec2 pos, float progress)                   )
    C(0, {                                                                     )
    C(1,     slide(idx, pos, progress, ivec2(+1, 0));                          )
    C(0, }                                                                     )
};

static const char transition_slideright[] = {
    SHADER_SLIDE_COMMON
    C(0, void transition(int idx, ivec2 pos, float progress)                   )
    C(0, {                                                                     )
    C(1,     slide(idx, pos, progress, ivec2(-1, 0));                          )
    C(0, }                                                                     )
};

#define SHADER_CIRCLE_COMMON                                                     \
    C(0, void circle(int idx, ivec2 pos, float progress, bool open)            ) \
    C(0, {                                                                     ) \
    C(1,     const ivec2 half_size = imageSize(output_images[idx]) / 2;        ) \
    C(1,     const float z = dot(half_size, half_size);                        ) \
    C(1,     float p = ((open ? (1.0 - progress) : progress) - 0.5) * 3.0;     ) \
    C(1,     ivec2 dsize = pos - half_size;                                    ) \
    C(1,     float sm = dot(dsize, dsize) / z + p;                             ) \
    C(1,     vec4 a = texture(a_images[idx], pos);                             ) \
    C(1,     vec4 b = texture(b_images[idx], pos);                             ) \
    C(1,     imageStore(output_images[idx], pos, \
                        mix(open ? b : a, open ? a : b, \
                            smoothstep(0.f, 1.f, sm)));                        ) \
    C(0, }                                                                     )

static const char transition_circleopen[] = {
    SHADER_CIRCLE_COMMON
    C(0, void transition(int idx, ivec2 pos, float progress)                   )
    C(0, {                                                                     )
    C(1,     circle(idx, pos, progress, true);                                 )
    C(0, }                                                                     )
};

static const char transition_circleclose[] = {
    SHADER_CIRCLE_COMMON
    C(0, void transition(int idx, ivec2 pos, float progress)                   )
    C(0, {                                                                     )
    C(1,     circle(idx, pos, progress, false);                                )
    C(0, }                                                                     )
};

#define SHADER_FRAND_FUNC                                                        \
    C(0, float frand(vec2 v)                                                   ) \
    C(0, {                                                                     ) \
    C(1,     return fract(sin(dot(v, vec2(12.9898, 78.233))) * 43758.545);     ) \
    C(0, }                                                                     )

static const char transition_dissolve[] = {
    SHADER_FRAND_FUNC
    C(0, void transition(int idx, ivec2 pos, float progress)                   )
    C(0, {                                                                     )
    C(1,     float sm = frand(pos) * 2.0 + (1.0 - progress) * 2.0 - 1.5;       )
    C(1,     vec4 a = texture(a_images[idx], pos);                             )
    C(1,     vec4 b = texture(b_images[idx], pos);                             )
    C(1,     imageStore(output_images[idx], pos, sm >= 0.5 ? a : b);           )
    C(0, }                                                                     )
};

static const char transition_pixelize[] = {
    C(0, void transition(int idx, ivec2 pos, float progress)                                  )
    C(0, {                                                                                    )
    C(1,     ivec2 size = imageSize(output_images[idx]);                                      )
    C(1,     float d = min(progress, 1.0 - progress);                                         )
    C(1,     float dist = ceil(d * 50.0) / 50.0;                                              )
    C(1,     float sq = 2.0 * dist * min(size.x, size.y) / 20.0;                              )
    C(1,     float sx = dist > 0.0 ? min((floor(pos.x / sq) + 0.5) * sq, size.x - 1) : pos.x; )
    C(1,     float sy = dist > 0.0 ? min((floor(pos.y / sq) + 0.5) * sq, size.y - 1) : pos.y; )
    C(1,     vec4 a = texture(a_images[idx], vec2(sx, sy));                                   )
    C(1,     vec4 b = texture(b_images[idx], vec2(sx, sy));                                   )
    C(1,     imageStore(output_images[idx], pos, mix(a, b, progress));                        )
    C(0, }                                                                                    )
};

static const char transition_wipetl[] = {
    C(0, void transition(int idx, ivec2 pos, float progress)                                  )
    C(0, {                                                                                    )
    C(1,     ivec2 size = imageSize(output_images[idx]);                                      )
    C(1,     float zw = size.x * (1.0 - progress);                                            )
    C(1,     float zh = size.y * (1.0 - progress);                                            )
    C(1,     vec4 a = texture(a_images[idx], pos);                                            )
    C(1,     vec4 b = texture(b_images[idx], pos);                                            )
    C(1,     imageStore(output_images[idx], pos, (pos.y <= zh && pos.x <= zw) ? a : b);       )
    C(0, }                                                                                    )
};

static const char transition_wipetr[] = {
    C(0, void transition(int idx, ivec2 pos, float progress)                                  )
    C(0, {                                                                                    )
    C(1,     ivec2 size = imageSize(output_images[idx]);                                      )
    C(1,     float zw = size.x * (progress);                                                  )
    C(1,     float zh = size.y * (1.0 - progress);                                            )
    C(1,     vec4 a = texture(a_images[idx], pos);                                            )
    C(1,     vec4 b = texture(b_images[idx], pos);                                            )
    C(1,     imageStore(output_images[idx], pos, (pos.y <= zh && pos.x > zw) ? a : b);        )
    C(0, }                                                                                    )
};

static const char transition_wipebl[] = {
    C(0, void transition(int idx, ivec2 pos, float progress)                                  )
    C(0, {                                                                                    )
    C(1,     ivec2 size = imageSize(output_images[idx]);                                      )
    C(1,     float zw = size.x * (1.0 - progress);                                            )
    C(1,     float zh = size.y * (progress);                                                  )
    C(1,     vec4 a = texture(a_images[idx], pos);                                            )
    C(1,     vec4 b = texture(b_images[idx], pos);                                            )
    C(1,     imageStore(output_images[idx], pos, (pos.y > zh && pos.x <= zw) ? a : b);        )
    C(0, }                                                                                    )
};

static const char transition_wipebr[] = {
    C(0, void transition(int idx, ivec2 pos, float progress)                                  )
    C(0, {                                                                                    )
    C(1,     ivec2 size = imageSize(output_images[idx]);                                      )
    C(1,     float zw = size.x * (progress);                                                  )
    C(1,     float zh = size.y * (progress);                                                  )
    C(1,     vec4 a = texture(a_images[idx], pos);                                            )
    C(1,     vec4 b = texture(b_images[idx], pos);                                            )
    C(1,     imageStore(output_images[idx], pos, (pos.y > zh && pos.x > zw) ? a : b);         )
    C(0, }                                                                                    )
};

static const char* transitions_map[NB_TRANSITIONS] = {
    [FADE]          = transition_fade,
    [WIPELEFT]      = transition_wipeleft,
    [WIPERIGHT]     = transition_wiperight,
    [WIPEUP]        = transition_wipeup,
    [WIPEDOWN]      = transition_wipedown,
    [SLIDEDOWN]     = transition_slidedown,
    [SLIDEUP]       = transition_slideup,
    [SLIDELEFT]     = transition_slideleft,
    [SLIDERIGHT]    = transition_slideright,
    [CIRCLEOPEN]    = transition_circleopen,
    [CIRCLECLOSE]   = transition_circleclose,
    [DISSOLVE]      = transition_dissolve,
    [PIXELIZE]      = transition_pixelize,
    [WIPETL]        = transition_wipetl,
    [WIPETR]        = transition_wipetr,
    [WIPEBL]        = transition_wipebl,
    [WIPEBR]        = transition_wipebr,
};

static av_cold int init_vulkan(AVFilterContext *avctx)
{
    int err = 0;
    uint8_t *spv_data;
    size_t spv_len;
    void *spv_opaque = NULL;
    XFadeVulkanContext *s = avctx->priv;
    FFVulkanContext *vkctx = &s->vkctx;
    const int planes = av_pix_fmt_count_planes(s->vkctx.output_format);
    FFVulkanShader *shd = &s->shd;
    FFVkSPIRVCompiler *spv;
    FFVulkanDescriptorSetBinding *desc;

    spv = ff_vk_spirv_init();
    if (!spv) {
        av_log(avctx, AV_LOG_ERROR, "Unable to initialize SPIR-V compiler!\n");
        return AVERROR_EXTERNAL;
    }

    s->qf = ff_vk_qf_find(vkctx, VK_QUEUE_COMPUTE_BIT, 0);
    if (!s->qf) {
        av_log(avctx, AV_LOG_ERROR, "Device has no compute queues\n");
        err = AVERROR(ENOTSUP);
        goto fail;
    }

    RET(ff_vk_exec_pool_init(vkctx, s->qf, &s->e, s->qf->num*4, 0, 0, 0, NULL));
    RET(ff_vk_init_sampler(vkctx, &s->sampler, 1, VK_FILTER_NEAREST));
    RET(ff_vk_shader_init(vkctx, &s->shd, "xfade",
                          VK_SHADER_STAGE_COMPUTE_BIT,
                          NULL, 0,
                          32, 32, 1,
                          0));

    desc = (FFVulkanDescriptorSetBinding []) {
        {
            .name       = "a_images",
            .type       = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
            .dimensions = 2,
            .elems      = planes,
            .stages     = VK_SHADER_STAGE_COMPUTE_BIT,
            .samplers   = DUP_SAMPLER(s->sampler),
        },
        {
            .name       = "b_images",
            .type       = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
            .dimensions = 2,
            .elems      = planes,
            .stages     = VK_SHADER_STAGE_COMPUTE_BIT,
            .samplers   = DUP_SAMPLER(s->sampler),
        },
        {
            .name       = "output_images",
            .type       = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
            .mem_layout = ff_vk_shader_rep_fmt(s->vkctx.output_format, FF_VK_REP_FLOAT),
            .mem_quali  = "writeonly",
            .dimensions = 2,
            .elems      = planes,
            .stages     = VK_SHADER_STAGE_COMPUTE_BIT,
        },
    };

    RET(ff_vk_shader_add_descriptor_set(vkctx, &s->shd, desc, 3, 0, 0));

    GLSLC(0, layout(push_constant, std430) uniform pushConstants {                 );
    GLSLC(1,    float progress;                                                    );
    GLSLC(0, };                                                                    );

    ff_vk_shader_add_push_const(&s->shd, 0, sizeof(XFadeParameters),
                                VK_SHADER_STAGE_COMPUTE_BIT);

    // Add the right transition type function to the shader
    GLSLD(transitions_map[s->transition]);

    GLSLC(0, void main()                                                  );
    GLSLC(0, {                                                            );
    GLSLC(1,     ivec2 pos = ivec2(gl_GlobalInvocationID.xy);             );
    GLSLF(1,     int planes = %i;                                  ,planes);
    GLSLC(1,     for (int i = 0; i < planes; i++) {                       );
    GLSLC(2,        transition(i, pos, progress);                         );
    GLSLC(1,     }                                                        );
    GLSLC(0, }                                                            );

    RET(spv->compile_shader(vkctx, spv, shd, &spv_data, &spv_len, "main",
                            &spv_opaque));
    RET(ff_vk_shader_link(vkctx, shd, spv_data, spv_len, "main"));

    RET(ff_vk_shader_register_exec(vkctx, &s->e, &s->shd));

    s->initialized = 1;

fail:
    if (spv_opaque)
        spv->free_shader(spv, &spv_opaque);
    if (spv)
        spv->uninit(&spv);

    return err;
}

static int xfade_frame(AVFilterContext *avctx, AVFrame *frame_a, AVFrame *frame_b)
{
    int err;
    AVFilterLink *outlink = avctx->outputs[0];
    XFadeVulkanContext *s = avctx->priv;
    float progress;

    AVFrame *output = ff_get_video_buffer(outlink, outlink->w, outlink->h);
    if (!output) {
        err = AVERROR(ENOMEM);
        goto fail;
    }

    if (!s->initialized) {
        AVHWFramesContext *a_fc = (AVHWFramesContext*)frame_a->hw_frames_ctx->data;
        AVHWFramesContext *b_fc = (AVHWFramesContext*)frame_b->hw_frames_ctx->data;
        if (a_fc->sw_format != b_fc->sw_format) {
            av_log(avctx, AV_LOG_ERROR,
                   "Currently the sw format of the first input needs to match the second!\n");
            return AVERROR(EINVAL);
        }
        RET(init_vulkan(avctx));
    }

    RET(av_frame_copy_props(output, frame_a));
    output->pts = s->pts;

    progress = av_clipf((float)(s->pts - s->start_pts) / s->duration_pts,
                        0.f, 1.f);

    RET(ff_vk_filter_process_Nin(&s->vkctx, &s->e, &s->shd, output,
                                 (AVFrame *[]){ frame_a, frame_b }, 2, s->sampler,
                                 &(XFadeParameters){ progress }, sizeof(XFadeParameters)));

    return ff_filter_frame(outlink, output);

fail:
    av_frame_free(&output);
    return err;
}

static int config_props_output(AVFilterLink *outlink)
{
    int err;
    AVFilterContext *avctx = outlink->src;
    XFadeVulkanContext *s = avctx->priv;
    AVFilterLink *inlink_a = avctx->inputs[IN_A];
    AVFilterLink *inlink_b = avctx->inputs[IN_B];
    FilterLink *il = ff_filter_link(inlink_a);
    FilterLink *ol = ff_filter_link(outlink);

    if (inlink_a->w != inlink_b->w || inlink_a->h != inlink_b->h) {
        av_log(avctx, AV_LOG_ERROR, "First input link %s parameters "
               "(size %dx%d) do not match the corresponding "
               "second input link %s parameters (size %dx%d)\n",
               avctx->input_pads[IN_A].name, inlink_a->w, inlink_a->h,
               avctx->input_pads[IN_B].name, inlink_b->w, inlink_b->h);
        return AVERROR(EINVAL);
    }

    if (inlink_a->time_base.num != inlink_b->time_base.num ||
        inlink_a->time_base.den != inlink_b->time_base.den) {
        av_log(avctx, AV_LOG_ERROR, "First input link %s timebase "
               "(%d/%d) does not match the corresponding "
               "second input link %s timebase (%d/%d)\n",
               avctx->input_pads[IN_A].name, inlink_a->time_base.num, inlink_a->time_base.den,
               avctx->input_pads[IN_B].name, inlink_b->time_base.num, inlink_b->time_base.den);
        return AVERROR(EINVAL);
    }

    s->start_pts = s->inputs_offset_pts = AV_NOPTS_VALUE;

    outlink->time_base = inlink_a->time_base;
    ol->frame_rate = il->frame_rate;
    outlink->sample_aspect_ratio = inlink_a->sample_aspect_ratio;

    if (s->duration)
        s->duration_pts = av_rescale_q(s->duration, AV_TIME_BASE_Q, inlink_a->time_base);
    RET(ff_vk_filter_config_output(outlink));

fail:
    return err;
}

static int forward_frame(XFadeVulkanContext *s,
                         AVFilterLink *inlink, AVFilterLink *outlink)
{
    int64_t status_pts;
    int ret = 0, status;
    AVFrame *frame = NULL;

    ret = ff_inlink_consume_frame(inlink, &frame);
    if (ret < 0)
        return ret;

    if (ret > 0) {
        // If we do not have an offset yet, it's because we
        // never got a first input. Just offset to 0
        if (s->inputs_offset_pts == AV_NOPTS_VALUE)
            s->inputs_offset_pts = -frame->pts;

        // We got a frame, nothing to do other than adjusting the timestamp
        frame->pts += s->inputs_offset_pts;
        return ff_filter_frame(outlink, frame);
    }

    // Forward status with our timestamp
    if (ff_inlink_acknowledge_status(inlink, &status, &status_pts)) {
        if (s->inputs_offset_pts == AV_NOPTS_VALUE)
            s->inputs_offset_pts = -status_pts;

        ff_outlink_set_status(outlink, status, status_pts + s->inputs_offset_pts);
        return 0;
    }

    // No frame available, request one if needed
    if (ff_outlink_frame_wanted(outlink))
        ff_inlink_request_frame(inlink);

    return 0;
}

static int activate(AVFilterContext *avctx)
{
    XFadeVulkanContext *s = avctx->priv;
    AVFilterLink *in_a = avctx->inputs[IN_A];
    AVFilterLink *in_b = avctx->inputs[IN_B];
    AVFilterLink *outlink = avctx->outputs[0];
    int64_t status_pts;

    FF_FILTER_FORWARD_STATUS_BACK_ALL(outlink, avctx);

    // Check if we already transitioned or IN_A ended prematurely,
    // in which case just forward the frames from IN_B with adjusted
    // timestamps until EOF.
    if (s->status[IN_A] && !s->status[IN_B])
        return forward_frame(s, in_b, outlink);

    // We did not finish transitioning yet and the first stream
    // did not end either, so check if there are more frames to consume.
    if (ff_inlink_check_available_frame(in_a)) {
        AVFrame *peeked_frame = ff_inlink_peek_frame(in_a, 0);
        s->pts = peeked_frame->pts;

        if (s->start_pts == AV_NOPTS_VALUE)
            s->start_pts =
                s->pts + av_rescale_q(s->offset, AV_TIME_BASE_Q, in_a->time_base);

        // Check if we are not yet transitioning, in which case
        // just request and forward the input frame.
        if (s->start_pts > s->pts) {
            AVFrame *frame_a = NULL;
            s->passthrough = 1;
            ff_inlink_consume_frame(in_a, &frame_a);
            return ff_filter_frame(outlink, frame_a);
        }
        s->passthrough = 0;

        // We are transitioning, so we need a frame from IN_B
        if (ff_inlink_check_available_frame(in_b)) {
            int ret;
            AVFrame *frame_a = NULL, *frame_b = NULL;
            ff_inlink_consume_frame(avctx->inputs[IN_A], &frame_a);
            ff_inlink_consume_frame(avctx->inputs[IN_B], &frame_b);

            // Calculate PTS offset to first input
            if (s->inputs_offset_pts == AV_NOPTS_VALUE)
                s->inputs_offset_pts = s->pts - frame_b->pts;

            // Check if we finished transitioning, in which case we
            // report back EOF to IN_A as it is no longer needed.
            if (s->pts - s->start_pts > s->duration_pts) {
                s->status[IN_A] = AVERROR_EOF;
                ff_inlink_set_status(in_a, AVERROR_EOF);
                s->passthrough = 1;
            }
            ret = xfade_frame(avctx, frame_a, frame_b);
            av_frame_free(&frame_a);
            av_frame_free(&frame_b);
            return ret;
        }

        // We did not get a frame from IN_B, check its status.
        if (ff_inlink_acknowledge_status(in_b, &s->status[IN_B], &status_pts)) {
            // We should transition, but IN_B is EOF so just report EOF output now.
            ff_outlink_set_status(outlink, s->status[IN_B], s->pts);
            return 0;
        }

        // We did not get a frame for IN_B but no EOF either, so just request more.
        if (ff_outlink_frame_wanted(outlink)) {
            ff_inlink_request_frame(in_b);
            return 0;
        }
    }

    // We did not get a frame from IN_A, check its status.
    if (ff_inlink_acknowledge_status(in_a, &s->status[IN_A], &status_pts)) {
        // No more frames from IN_A, do not report EOF though, we will just
        // forward the IN_B frames in the next activate calls.
        s->passthrough = 1;
        ff_filter_set_ready(avctx, 100);
        return 0;
    }

    // We have no frames yet from IN_A and no EOF, so request some.
    if (ff_outlink_frame_wanted(outlink)) {
        ff_inlink_request_frame(in_a);
        return 0;
    }

    return FFERROR_NOT_READY;
}

static av_cold void uninit(AVFilterContext *avctx)
{
    XFadeVulkanContext *s = avctx->priv;
    FFVulkanContext *vkctx = &s->vkctx;
    FFVulkanFunctions *vk = &vkctx->vkfn;

    ff_vk_exec_pool_free(vkctx, &s->e);
    ff_vk_shader_free(vkctx, &s->shd);

    if (s->sampler)
        vk->DestroySampler(vkctx->hwctx->act_dev, s->sampler,
                           vkctx->hwctx->alloc);

    ff_vk_uninit(&s->vkctx);

    s->initialized = 0;
}

static AVFrame *get_video_buffer(AVFilterLink *inlink, int w, int h)
{
    XFadeVulkanContext *s = inlink->dst->priv;

    return s->passthrough ?
        ff_null_get_video_buffer   (inlink, w, h) :
        ff_default_get_video_buffer(inlink, w, h);
}

#define OFFSET(x) offsetof(XFadeVulkanContext, x)
#define FLAGS (AV_OPT_FLAG_FILTERING_PARAM | AV_OPT_FLAG_VIDEO_PARAM)

static const AVOption xfade_vulkan_options[] = {
    { "transition", "set cross fade transition", OFFSET(transition), AV_OPT_TYPE_INT, {.i64=FADE}, 0, NB_TRANSITIONS-1, FLAGS, .unit = "transition" },
        { "fade", "fade transition", 0, AV_OPT_TYPE_CONST, {.i64=FADE}, 0, 0, FLAGS, .unit = "transition" },
        { "wipeleft", "wipe left transition", 0, AV_OPT_TYPE_CONST, {.i64=WIPELEFT}, 0, 0, FLAGS, .unit = "transition" },
        { "wiperight", "wipe right transition", 0, AV_OPT_TYPE_CONST, {.i64=WIPERIGHT}, 0, 0, FLAGS, .unit = "transition" },
        { "wipeup", "wipe up transition", 0, AV_OPT_TYPE_CONST, {.i64=WIPEUP}, 0, 0, FLAGS, .unit = "transition" },
        { "wipedown", "wipe down transition", 0, AV_OPT_TYPE_CONST, {.i64=WIPEDOWN}, 0, 0, FLAGS, .unit = "transition" },
        { "slidedown", "slide down transition", 0, AV_OPT_TYPE_CONST, {.i64=SLIDEDOWN}, 0, 0, FLAGS, .unit = "transition" },
        { "slideup", "slide up transition", 0, AV_OPT_TYPE_CONST, {.i64=SLIDEUP}, 0, 0, FLAGS, .unit = "transition" },
        { "slideleft", "slide left transition", 0, AV_OPT_TYPE_CONST, {.i64=SLIDELEFT}, 0, 0, FLAGS, .unit = "transition" },
        { "slideright", "slide right transition", 0, AV_OPT_TYPE_CONST, {.i64=SLIDERIGHT}, 0, 0, FLAGS, .unit = "transition" },
        { "circleopen", "circleopen transition", 0, AV_OPT_TYPE_CONST, {.i64=CIRCLEOPEN}, 0, 0, FLAGS, .unit = "transition" },
        { "circleclose", "circleclose transition", 0, AV_OPT_TYPE_CONST, {.i64=CIRCLECLOSE}, 0, 0, FLAGS, .unit = "transition" },
        { "dissolve", "dissolve transition", 0, AV_OPT_TYPE_CONST, {.i64=DISSOLVE}, 0, 0, FLAGS, .unit = "transition" },
        { "pixelize", "pixelize transition", 0, AV_OPT_TYPE_CONST, {.i64=PIXELIZE}, 0, 0, FLAGS, .unit = "transition" },
        { "wipetl", "wipe top left transition", 0, AV_OPT_TYPE_CONST, {.i64=WIPETL}, 0, 0, FLAGS, .unit = "transition" },
        { "wipetr", "wipe top right transition", 0, AV_OPT_TYPE_CONST, {.i64=WIPETR}, 0, 0, FLAGS, .unit = "transition" },
        { "wipebl", "wipe bottom left transition", 0, AV_OPT_TYPE_CONST, {.i64=WIPEBL}, 0, 0, FLAGS, .unit = "transition" },
        { "wipebr", "wipe bottom right transition", 0, AV_OPT_TYPE_CONST, {.i64=WIPEBR}, 0, 0, FLAGS, .unit = "transition" },
    { "duration", "set cross fade duration", OFFSET(duration), AV_OPT_TYPE_DURATION, {.i64=1000000}, 0, 60000000, FLAGS },
    { "offset",   "set cross fade start relative to first input stream", OFFSET(offset), AV_OPT_TYPE_DURATION, {.i64=0}, INT64_MIN, INT64_MAX, FLAGS },
    { NULL }
};

AVFILTER_DEFINE_CLASS(xfade_vulkan);

static const AVFilterPad xfade_vulkan_inputs[] = {
    {
        .name             = "main",
        .type             = AVMEDIA_TYPE_VIDEO,
        .get_buffer.video = &get_video_buffer,
        .config_props     = &ff_vk_filter_config_input,
    },
    {
        .name             = "xfade",
        .type             = AVMEDIA_TYPE_VIDEO,
        .get_buffer.video = &get_video_buffer,
        .config_props     = &ff_vk_filter_config_input,
    },
};

static const AVFilterPad xfade_vulkan_outputs[] = {
    {
        .name          = "default",
        .type          = AVMEDIA_TYPE_VIDEO,
        .config_props  = &config_props_output,
    },
};

const FFFilter ff_vf_xfade_vulkan = {
    .p.name          = "xfade_vulkan",
    .p.description   = NULL_IF_CONFIG_SMALL("Cross fade one video with another video."),
    .p.priv_class    = &xfade_vulkan_class,
    .p.flags         = AVFILTER_FLAG_HWDEVICE,
    .priv_size       = sizeof(XFadeVulkanContext),
    .init            = &ff_vk_filter_init,
    .uninit          = &uninit,
    .activate        = &activate,
    FILTER_INPUTS(xfade_vulkan_inputs),
    FILTER_OUTPUTS(xfade_vulkan_outputs),
    FILTER_SINGLE_PIXFMT(AV_PIX_FMT_VULKAN),
    .flags_internal  = FF_FILTER_FLAG_HWFRAME_AWARE,
};
