/**
 * Copyright (C) 2026 Lynne
 *
 * 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/mem.h"
#include "libavutil/refstruct.h"

#include "../graph.h"
#include "../ops_internal.h"
#include "../swscale_internal.h"

#include "ops.h"

#if HAVE_SPIRV_HEADERS_SPIRV_H || HAVE_SPIRV_UNIFIED1_SPIRV_H
#include "spvasm.h"
#endif

static void ff_sws_vk_uninit(AVRefStructOpaque opaque, void *obj)
{
    FFVulkanOpsCtx *s = obj;

    ff_vk_uninit(&s->vkctx);
}

int ff_sws_vk_init(SwsContext *sws, AVBufferRef *dev_ref)
{
    int err;
    SwsInternal *c = sws_internal(sws);

    if (!c->hw_priv) {
        c->hw_priv = av_refstruct_alloc_ext(sizeof(FFVulkanOpsCtx), 0, NULL,
                                            ff_sws_vk_uninit);
        if (!c->hw_priv)
            return AVERROR(ENOMEM);
    }

    FFVulkanOpsCtx *s = c->hw_priv;
    if (s->vkctx.device_ref && s->vkctx.device_ref->data != dev_ref->data) {
        /* Reinitialize with new context */
        ff_vk_uninit(&s->vkctx);
    } else if (s->vkctx.device_ref && s->vkctx.device_ref->data == dev_ref->data) {
        return 0;
    }

    err = ff_vk_init(&s->vkctx, sws, dev_ref, NULL);
    if (err < 0)
        return err;

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

    return 0;
}

AVBufferRef *ff_sws_vk_device_ref(SwsContext *sws)
{
    SwsInternal *c = sws_internal(sws);
    FFVulkanOpsCtx *s = c->hw_priv;
    return s ? s->vkctx.device_ref : NULL;
}

#define MAX_DITHER_BUFS 4
#define MAX_FILT_BUFS 4
#define MAX_DATA_BUFS (MAX_DITHER_BUFS + MAX_FILT_BUFS*4)

typedef struct VulkanPriv {
    FFVulkanOpsCtx *s;
    FFVkExecPool e;
    FFVulkanShader shd;
    FFVkBuffer data_bufs[MAX_DATA_BUFS];
    int nb_data_bufs;
    enum FFVkShaderRepFormat src_rep;
    enum FFVkShaderRepFormat dst_rep;
    int interlaced;
} VulkanPriv;

static void process(const SwsFrame *dst, const SwsFrame *src, int y, int h,
                    const SwsPass *pass)
{
    VulkanPriv *p = (VulkanPriv *) pass->priv;
    FFVkExecContext *ec = ff_vk_exec_get(&p->s->vkctx, &p->e);
    FFVulkanFunctions *vk = &p->s->vkctx.vkfn;
    ff_vk_exec_start(&p->s->vkctx, ec);

    AVFrame *src_f = (AVFrame *) src->avframe;
    AVFrame *dst_f = (AVFrame *) dst->avframe;
    ff_vk_exec_add_dep_frame(&p->s->vkctx, ec, src_f,
                             VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
                             VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT);
    ff_vk_exec_add_dep_frame(&p->s->vkctx, ec, dst_f,
                             VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
                             VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT);

    VkImageView src_views[AV_NUM_DATA_POINTERS];
    VkImageView dst_views[AV_NUM_DATA_POINTERS];
    ff_vk_create_imageviews(&p->s->vkctx, ec, src_views, src_f, p->src_rep);
    ff_vk_create_imageviews(&p->s->vkctx, ec, dst_views, dst_f, p->dst_rep);

    ff_vk_shader_update_img_array(&p->s->vkctx, ec, &p->shd, src_f, src_views,
                                  0, 0, VK_IMAGE_LAYOUT_GENERAL, VK_NULL_HANDLE);
    ff_vk_shader_update_img_array(&p->s->vkctx, ec, &p->shd, dst_f, dst_views,
                                  0, 1, VK_IMAGE_LAYOUT_GENERAL, VK_NULL_HANDLE);

    int nb_img_bar = 0;
    VkImageMemoryBarrier2 img_bar[8];
    ff_vk_frame_barrier(&p->s->vkctx, ec, src_f, img_bar, &nb_img_bar,
                        VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
                        VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
                        VK_ACCESS_SHADER_READ_BIT,
                        VK_IMAGE_LAYOUT_GENERAL,
                        VK_QUEUE_FAMILY_IGNORED);
    ff_vk_frame_barrier(&p->s->vkctx, ec, dst_f, img_bar, &nb_img_bar,
                        VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
                        VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT,
                        VK_ACCESS_SHADER_WRITE_BIT,
                        VK_IMAGE_LAYOUT_GENERAL,
                        VK_QUEUE_FAMILY_IGNORED);
    vk->CmdPipelineBarrier2(ec->buf, &(VkDependencyInfo) {
        .sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO,
        .pImageMemoryBarriers = img_bar,
        .imageMemoryBarrierCount = nb_img_bar,
    });

    if (p->interlaced) {
        uint32_t field = pass->graph ? pass->graph->dst.field : 0;
        ff_vk_shader_update_push_const(&p->s->vkctx, ec, &p->shd,
                                       VK_SHADER_STAGE_COMPUTE_BIT,
                                       0, sizeof(field), &field);
    }

    ff_vk_exec_bind_shader(&p->s->vkctx, ec, &p->shd);

    vk->CmdDispatch(ec->buf,
                    FFALIGN(dst->width, p->shd.lg_size[0])/p->shd.lg_size[0],
                    FFALIGN(dst->height, p->shd.lg_size[1])/p->shd.lg_size[1],
                    1);

    ff_vk_exec_submit(&p->s->vkctx, ec);
    ff_vk_exec_wait(&p->s->vkctx, ec);
}

static void free_fn(void *priv)
{
    VulkanPriv *p = priv;
    ff_vk_exec_pool_free(&p->s->vkctx, &p->e);
    ff_vk_shader_free(&p->s->vkctx, &p->shd);
    for (int i = 0; i < p->nb_data_bufs; i++)
        ff_vk_free_buf(&p->s->vkctx, &p->data_bufs[i]);
    av_refstruct_unref(&p->s);
    av_free(priv);
}

static int create_filter_buf(FFVulkanOpsCtx *s, VulkanPriv *p,
                             const SwsFilterWeights *wd, FFVkBuffer *buf)
{
    int err;

    /* Weights */
    err = ff_vk_create_buf(&s->vkctx, buf,
                           wd->num_weights*sizeof(float) +
                           wd->dst_size*sizeof(int32_t), NULL, NULL,
                           VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                           VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT |
                           VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT);
    if (err < 0)
        goto fail;

    float *weights_data;
    err = ff_vk_map_buffer(&s->vkctx, buf,
                           (uint8_t **)&weights_data, 0);
    if (err < 0)
        goto fail;
    for (int i = 0; i < wd->num_weights; i++)
        weights_data[i] = (float) wd->weights[i] / SWS_FILTER_SCALE;

    memcpy(weights_data + wd->num_weights,
           wd->offsets, wd->dst_size*sizeof(int32_t));

    ff_vk_unmap_buffer(&s->vkctx, buf, 1);

    return 0;

fail:
    ff_vk_free_buf(&p->s->vkctx, buf);
    return 0;
}

static int create_dither_buf(FFVulkanOpsCtx *s, VulkanPriv *p,
                             const SwsDitherOp *dd, FFVkBuffer *buf)
{
    int err;

    int size = (1 << dd->size_log2);
    err = ff_vk_create_buf(&s->vkctx, buf,
                           size*size*sizeof(float), NULL, NULL,
                           VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
                           VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT |
                           VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT);
    if (err < 0)
        return err;

    float *dither_data;
    err = ff_vk_map_buffer(&s->vkctx, buf, (uint8_t **)&dither_data, 0);
    if (err < 0)
        goto fail;

    for (int i = 0; i < size; i++) {
        for (int j = 0; j < size; j++) {
            const AVRational64 r = dd->matrix[i*size + j];
            dither_data[i*size + j] = r.num/(float)r.den;
        }
    }

    ff_vk_unmap_buffer(&s->vkctx, buf, 1);

    return 0;

fail:
    ff_vk_free_buf(&p->s->vkctx, buf);
    return err;
}

static int create_bufs(FFVulkanOpsCtx *s, VulkanPriv *p, const SwsOpList *ops)
{
    int err;
    p->nb_data_bufs = 0;
    for (int n = 0; n < ops->num_ops; n++) {
        const SwsOp *op = &ops->ops[n];
        if (op->op == SWS_OP_DITHER) {
            av_assert0(p->nb_data_bufs + 1 <= FF_ARRAY_ELEMS(p->data_bufs));
            err = create_dither_buf(s, p, &op->dither,
                                    &p->data_bufs[p->nb_data_bufs]);
            if (err < 0)
                goto fail;
            p->nb_data_bufs++;
        } else if (op->op == SWS_OP_FILTER_H || op->op == SWS_OP_FILTER_V) {
            av_assert0(p->nb_data_bufs + 1 <= FF_ARRAY_ELEMS(p->data_bufs));
            err = create_filter_buf(s, p, op->filter.kernel,
                                    &p->data_bufs[p->nb_data_bufs]);
            if (err < 0)
                goto fail;
            p->nb_data_bufs++;
        } else if ((op->op == SWS_OP_READ ||
                    op->op == SWS_OP_WRITE) && op->rw.filter.op) {
            av_assert0(p->nb_data_bufs + 1 <= FF_ARRAY_ELEMS(p->data_bufs));
            err = create_filter_buf(s, p, op->rw.filter.kernel,
                                    &p->data_bufs[p->nb_data_bufs]);
            if (err < 0)
                goto fail;
            p->nb_data_bufs++;
        }
    }

    return 0;

fail:
    for (int i = 0; i < p->nb_data_bufs; i++)
        ff_vk_free_buf(&p->s->vkctx, &p->data_bufs[i]);
    return err;
}

#if HAVE_SPIRV_HEADERS_SPIRV_H || HAVE_SPIRV_UNIFIED1_SPIRV_H
struct DitherData {
        int size;
        int arr_1d_id;
        int arr_2d_id;
        int struct_id;
        int struct_ptr_id;
        int id;
        int mask_id;
        int binding;
};

struct FilterData {
    SwsOpType filter;
    int filter_size;
    int dst_size;
    int num_weights;

    int arr_w_in_id;
    int arr_w_out_id;
    int arr_o_id;
    int struct_id;
    int struct_ptr_id;

    int id; /* buffer ID */
    int binding; /* descriptor idx in desc set 1 */

    int tap_const_base;
};

typedef struct SPIRVIDs {
    int in_vars[3 + MAX_DATA_BUFS + 1];

    int glfn;
    int ep;

    /* Types */
    int void_type;
    int b_type;
    int u32_type;
    int i32_type;
    int f32_type;
    int void_fn_type;

    /* Define vector types */
    int bvec2_type;
    int u32vec2_type;
    int i32vec2_type;

    int u32vec3_type;

    int u32vec4_type;
    int f32vec4_type;
    int f32mat4_type;

    /* Constants */
    int u32_p;
    int f32_p;
    int f32_0;
    int u32_cid[5];

    int const_ids[128];
    int nb_const_ids;

    int linear_deco_off[16];
    int linear_deco_ops[16];
    int nb_linear_ops;

    struct DitherData dither[MAX_DITHER_BUFS];
    int dither_ptr_elem_id;
    int nb_dither_bufs;

    struct FilterData filt[MAX_FILT_BUFS];
    int filt_o_ptr_id;
    int nb_filter_bufs;

    int out_img_type;
    int out_img_array_id;

    int in_img_type;
    int in_img_array_id;

    /* Pointer types for images */
    int u32vec3_tptr;
    int out_img_tptr;
    int out_img_sptr;

    int in_img_tptr;
    int in_img_sptr;

    /* Interlaced handling */
    int interlaced;
    int push_const_struct_id;
    int push_const_ptr_id;
    int push_const_elem_ptr_id;
    int push_const_var_id;
    int field_i32;
} SPIRVIDs;

/* Section 1: Function to define all shader header data, and decorations */
static void define_shader_header(SwsContext *sws, FFVulkanShader *shd,
                                 const SwsOpList *ops, SPICtx *spi, SPIRVIDs *id)
{
    spi_OpCapability(spi, SpvCapabilityShader); /* Shader type */

    /* Declare required capabilities */
    spi_OpCapability(spi, SpvCapabilityInt16);
    spi_OpCapability(spi, SpvCapabilityInt8);
    spi_OpCapability(spi, SpvCapabilityImageQuery);
    spi_OpCapability(spi, SpvCapabilityStorageImageReadWithoutFormat);
    spi_OpCapability(spi, SpvCapabilityStorageImageWriteWithoutFormat);
    spi_OpCapability(spi, SpvCapabilityStorageBuffer8BitAccess);
    /* Import the GLSL set of functions (used for min/max) */
    id->glfn = spi_OpExtInstImport(spi, "GLSL.std.450");

    /* Next section starts here */
    spi_OpMemoryModel(spi, SpvAddressingModelLogical, SpvMemoryModelGLSL450);

    /* Entrypoint */
    id->ep = spi_OpEntryPoint(spi, SpvExecutionModelGLCompute, "main",
                                id->in_vars,
                                3 + id->nb_dither_bufs + id->nb_filter_bufs +
                                (id->interlaced ? 1 : 0));
    spi_OpExecutionMode(spi, id->ep, SpvExecutionModeLocalSize,
                        shd->lg_size, 3);

    if (id->interlaced) {
        spi_OpDecorate(spi, id->push_const_struct_id, SpvDecorationBlock);
        spi_OpMemberDecorate(spi, id->push_const_struct_id, 0,
                             SpvDecorationOffset, 0);
    }

    /* gl_GlobalInvocationID descriptor decorations */
    spi_OpDecorate(spi, id->in_vars[0], SpvDecorationBuiltIn,
                   SpvBuiltInGlobalInvocationId);

    /* Input image descriptor decorations */
    spi_OpDecorate(spi, id->in_vars[1], SpvDecorationNonWritable);
    spi_OpDecorate(spi, id->in_vars[1], SpvDecorationDescriptorSet, 0);
    spi_OpDecorate(spi, id->in_vars[1], SpvDecorationBinding, 0);

    /* Output image descriptor decorations */
    spi_OpDecorate(spi, id->in_vars[2], SpvDecorationNonReadable);
    spi_OpDecorate(spi, id->in_vars[2], SpvDecorationDescriptorSet, 0);
    spi_OpDecorate(spi, id->in_vars[2], SpvDecorationBinding, 1);

    for (int i = 0; i < id->nb_dither_bufs; i++) {
        spi_OpDecorate(spi, id->dither[i].arr_1d_id, SpvDecorationArrayStride,
                       sizeof(float));
        spi_OpDecorate(spi, id->dither[i].arr_2d_id, SpvDecorationArrayStride,
                       id->dither[i].size*sizeof(float));
        spi_OpDecorate(spi, id->dither[i].struct_id, SpvDecorationBlock);
        spi_OpMemberDecorate(spi, id->dither[i].struct_id, 0, SpvDecorationOffset, 0);
        spi_OpDecorate(spi, id->dither[i].id, SpvDecorationDescriptorSet, 1);
        spi_OpDecorate(spi, id->dither[i].id, SpvDecorationBinding,
                       id->dither[i].binding);
    }

    for (int i = 0; i < id->nb_filter_bufs; i++) {
        struct FilterData *f = &id->filt[i];
        spi_OpDecorate(spi, f->arr_w_in_id, SpvDecorationArrayStride,
                       sizeof(float));
        spi_OpDecorate(spi, f->arr_w_out_id, SpvDecorationArrayStride,
                       f->filter_size*sizeof(float));
        spi_OpDecorate(spi, f->arr_o_id, SpvDecorationArrayStride,
                       sizeof(int32_t));
        spi_OpDecorate(spi, f->struct_id, SpvDecorationBlock);
        spi_OpMemberDecorate(spi, f->struct_id, 0, SpvDecorationOffset, 0);
        spi_OpMemberDecorate(spi, f->struct_id, 1, SpvDecorationOffset,
                             f->num_weights*sizeof(float));
        spi_OpDecorate(spi, f->id, SpvDecorationDescriptorSet, 1);
        spi_OpDecorate(spi, f->id, SpvDecorationBinding, f->binding);
    }

    if (!(sws->flags & SWS_BITEXACT))
        return;

    /* All linear arithmetic ops must be decorated with NoContraction */
    for (int n = 0; n < ops->num_ops; n++) {
        const SwsOp *op = &ops->ops[n];
        if (op->op != SWS_OP_LINEAR)
            continue;
        av_assert0((id->nb_linear_ops + 1) <= FF_ARRAY_ELEMS(id->linear_deco_off));

        int nb_ops = 0;
        for (int j = 0; j < 4; j++) {
            nb_ops += !!op->lin.m[j][0].num;
            nb_ops += op->lin.m[j][0].num && op->lin.m[j][4].num;
            for (int i = 1; i < 4; i++) {
                nb_ops += !!op->lin.m[j][i].num;
                nb_ops += op->lin.m[j][i].num &&
                          (op->lin.m[j][0].num || op->lin.m[j][4].num);
            }
        }

        id->linear_deco_off[id->nb_linear_ops] = spi_reserve(spi, nb_ops*4*3);
        id->linear_deco_ops[id->nb_linear_ops] = nb_ops;
        id->nb_linear_ops++;
    }
}

/* Section 2: Define all types and constants */
static void define_shader_consts(SwsContext *sws, const SwsOpList *ops,
                                 SPICtx *spi, SPIRVIDs *id)
{
    /* Define scalar types */
    id->void_type    = spi_OpTypeVoid(spi);
    id->b_type       = spi_OpTypeBool(spi);
    int u32_type =
        id->u32_type = spi_OpTypeInt(spi, 32, 0);
    id->i32_type     = spi_OpTypeInt(spi, 32, 1);
    int f32_type =
        id->f32_type = spi_OpTypeFloat(spi, 32);
    id->void_fn_type = spi_OpTypeFunction(spi, id->void_type, NULL, 0);

    /* Define vector types */
    id->bvec2_type   = spi_OpTypeVector(spi, id->b_type,   2);
    id->u32vec2_type = spi_OpTypeVector(spi, u32_type, 2);
    id->i32vec2_type = spi_OpTypeVector(spi, id->i32_type, 2);

    id->u32vec3_type = spi_OpTypeVector(spi, u32_type, 3);

    id->u32vec4_type = spi_OpTypeVector(spi, u32_type, 4);
    id->f32vec4_type = spi_OpTypeVector(spi, f32_type, 4);
    id->f32mat4_type = spi_OpTypeMatrix(spi, id->f32vec4_type, 4);

    /* Constants */
    id->u32_p = spi_OpUndef(spi, u32_type);
    id->f32_p = spi_OpUndef(spi, f32_type);
    id->f32_0 = spi_OpConstantFloat(spi, f32_type, 0);
    for (int i = 0; i < 5; i++)
        id->u32_cid[i] = spi_OpConstantUInt(spi, u32_type, i);

    /* Operation constants */
    id->nb_const_ids = 0;
    for (int n = 0; n < ops->num_ops; n++) {
        /* Make sure there's always enough space for the maximum number of
         * constants a single operation needs (currently linear, 31 consts). */
        av_assert0((id->nb_const_ids + 31) <= FF_ARRAY_ELEMS(id->const_ids));
        const SwsOp *op = &ops->ops[n];
        switch (op->op) {
        case SWS_OP_CONVERT:
            if (ff_sws_pixel_type_is_int(op->convert.to) && op->convert.expand) {
                AVRational64 m = ff_sws_pixel_expand(op->type, op->convert.to);
                int tmp = spi_OpConstantUInt(spi, id->u32_type, m.num);
                tmp = spi_OpConstantComposite(spi, id->u32vec4_type,
                                              tmp, tmp, tmp, tmp);
                id->const_ids[id->nb_const_ids++] = tmp;
            }
            break;
        case SWS_OP_CLEAR:
            for (int i = 0; i < 4; i++) {
                if (!SWS_COMP_TEST(op->clear.mask, i))
                    continue;
                AVRational64 cv = op->clear.value[i];
                if (op->type == SWS_PIXEL_F32) {
                    float q = (float)cv.num/cv.den;
                    id->const_ids[id->nb_const_ids++] =
                        spi_OpConstantFloat(spi, f32_type, q);
                } else {
                    av_assert0(cv.den == 1);
                    id->const_ids[id->nb_const_ids++] =
                        spi_OpConstantUInt(spi, u32_type, cv.num);
                }
            }
            break;
        case SWS_OP_LSHIFT:
        case SWS_OP_RSHIFT: {
            int tmp = spi_OpConstantUInt(spi, u32_type, op->shift.amount);
            tmp = spi_OpConstantComposite(spi, id->u32vec4_type,
                                          tmp, tmp, tmp, tmp);
            id->const_ids[id->nb_const_ids++] = tmp;
            break;
        }
        case SWS_OP_SCALE: {
            int tmp;
            if (op->type == SWS_PIXEL_F32) {
                float q = op->scale.factor.num/(float)op->scale.factor.den;
                tmp = spi_OpConstantFloat(spi, f32_type, q);
                tmp = spi_OpConstantComposite(spi, id->f32vec4_type,
                                              tmp, tmp, tmp, tmp);
            } else {
                av_assert0(op->scale.factor.den == 1);
                tmp = spi_OpConstantUInt(spi, u32_type, op->scale.factor.num);
                tmp = spi_OpConstantComposite(spi, id->u32vec4_type,
                                              tmp, tmp, tmp, tmp);
            }
            id->const_ids[id->nb_const_ids++] = tmp;
            break;
        }
        case SWS_OP_MIN:
        case SWS_OP_MAX:
            for (int i = 0; i < 4; i++) {
                int tmp;
                AVRational64 cl = op->clamp.limit[i];
                if (!op->clamp.limit[i].den) {
                    continue;
                } else if (op->type == SWS_PIXEL_F32) {
                    float q = (float)cl.num/((float)cl.den);
                    tmp = spi_OpConstantFloat(spi, f32_type, q);
                } else {
                    av_assert0(cl.den == 1);
                    tmp = spi_OpConstantUInt(spi, u32_type, cl.num);
                }
                id->const_ids[id->nb_const_ids++] = tmp;
            }
            break;
        case SWS_OP_DITHER:
            for (int i = 0; i < 4; i++) {
                if (op->dither.y_offset[i] < 0)
                    continue;
                int tmp = spi_OpConstantUInt(spi, u32_type, op->dither.y_offset[i]);
                id->const_ids[id->nb_const_ids++] = tmp;
            }
            break;
        case SWS_OP_LINEAR: {
            int tmp;
            float val;
            for (int i = 0; i < 4; i++) {
                for (int j = 0; j < 4; j++) {
                    int k = sws->flags & SWS_BITEXACT ? i : j;
                    int l = sws->flags & SWS_BITEXACT ? j : i;
                    val = op->lin.m[k][l].num/(float)op->lin.m[k][l].den;
                    id->const_ids[id->nb_const_ids++] =
                        spi_OpConstantFloat(spi, f32_type, val);
                }
                tmp = spi_OpConstantComposite(spi, id->f32vec4_type,
                                              id->const_ids[id->nb_const_ids - 4],
                                              id->const_ids[id->nb_const_ids - 3],
                                              id->const_ids[id->nb_const_ids - 2],
                                              id->const_ids[id->nb_const_ids - 1]);
                id->const_ids[id->nb_const_ids++] = tmp;
            }

            tmp = spi_OpConstantComposite(spi, id->f32mat4_type,
                                          id->const_ids[id->nb_const_ids - 5*4 + 4],
                                          id->const_ids[id->nb_const_ids - 5*3 + 4],
                                          id->const_ids[id->nb_const_ids - 5*2 + 4],
                                          id->const_ids[id->nb_const_ids - 5*1 + 4]);
            id->const_ids[id->nb_const_ids++] = tmp;

            for (int i = 0; i < 4; i++) {
                val = op->lin.m[i][4].num/(float)op->lin.m[i][4].den;
                id->const_ids[id->nb_const_ids++] =
                    spi_OpConstantFloat(spi, f32_type, val);
            }

            tmp = spi_OpConstantComposite(spi, id->f32vec4_type,
                                          id->const_ids[id->nb_const_ids - 4],
                                          id->const_ids[id->nb_const_ids - 3],
                                          id->const_ids[id->nb_const_ids - 2],
                                          id->const_ids[id->nb_const_ids - 1]);
            id->const_ids[id->nb_const_ids++] = tmp;
            break;
        }
        default:
            break;
        }
    }
}

/* Section 3: Define bindings */
static void define_shader_bindings(const SwsOpList *ops, SPICtx *spi, SPIRVIDs *id,
                                   int in_img_count, int out_img_count)
{
    id->dither_ptr_elem_id = spi_OpTypePointer(spi, SpvStorageClassUniform,
                                                 id->f32_type);

    struct DitherData *dither = id->dither;
    for (int i = 0; i < id->nb_dither_bufs; i++) {
        int size_id = spi_OpConstantUInt(spi, id->u32_type, dither[i].size);
        dither[i].mask_id = spi_OpConstantUInt(spi, id->u32_type, dither[i].size - 1);
        spi_OpTypeArray(spi, id->f32_type, dither[i].arr_1d_id, size_id);
        spi_OpTypeArray(spi, dither[i].arr_1d_id, dither[i].arr_2d_id, size_id);
        spi_OpTypeStruct(spi, dither[i].struct_id, dither[i].arr_2d_id);
        dither[i].struct_ptr_id = spi_OpTypePointer(spi, SpvStorageClassUniform,
                                                    dither[i].struct_id);
        dither[i].id = spi_OpVariable(spi, dither[i].id, dither[i].struct_ptr_id,
                                      SpvStorageClassUniform, 0);
    }

    /* Filter buffers: struct { float w[dst_size][filter_size]; int o[dst_size]; } */
    id->filt_o_ptr_id = 0;
    if (id->nb_filter_bufs)
        id->filt_o_ptr_id = spi_OpTypePointer(spi, SpvStorageClassUniform,
                                              id->i32_type);

    for (int i = 0; i < id->nb_filter_bufs; i++) {
        struct FilterData *f = &id->filt[i];
        int fs_id = spi_OpConstantUInt(spi, id->u32_type, f->filter_size);
        int ds_id = spi_OpConstantUInt(spi, id->u32_type, f->dst_size);

        spi_OpTypeArray(spi, id->f32_type, f->arr_w_in_id, fs_id);
        spi_OpTypeArray(spi, f->arr_w_in_id, f->arr_w_out_id, ds_id);
        spi_OpTypeArray(spi, id->i32_type, f->arr_o_id, ds_id);
        spi_OpTypeStruct(spi, f->struct_id, f->arr_w_out_id, f->arr_o_id);
        f->struct_ptr_id = spi_OpTypePointer(spi, SpvStorageClassUniform,
                                             f->struct_id);
        f->id = spi_OpVariable(spi, f->id, f->struct_ptr_id,
                               SpvStorageClassUniform, 0);

        /* Signed tap-index constants 0..filter_size-1 (consecutive <id>s) */
        f->tap_const_base = spi_OpConstantInt(spi, id->i32_type, 0);
        for (int t = 1; t < f->filter_size; t++)
            spi_OpConstantInt(spi, id->i32_type, t);
    }

    const SwsOp *op_w = ff_sws_op_list_output(ops);
    const SwsOp *op_r = ff_sws_op_list_input(ops);

    /* Define image types for descriptors */
    id->out_img_type = spi_OpTypeImage(spi,
                                       op_w->type == SWS_PIXEL_F32 ?
                                       id->f32_type : id->u32_type,
                                       2, 0, 0, 0, 2, SpvImageFormatUnknown);
    id->out_img_array_id = spi_OpTypeArray(spi, id->out_img_type, spi_get_id(spi),
                                           id->u32_cid[out_img_count]);

    id->in_img_type = 0;
    id->in_img_array_id = 0;
    if (op_r) {
        /* If the formats match, we have to reuse the types due to SPIR-V not
         * allowing redundant type defines */
        int match = ((op_w->type == SWS_PIXEL_F32) ==
                     (op_r->type == SWS_PIXEL_F32));
        id->in_img_type = match ? id->out_img_type :
                            spi_OpTypeImage(spi,
                                            op_r->type == SWS_PIXEL_F32 ?
                                            id->f32_type : id->u32_type,
                                            2, 0, 0, 0, 2, SpvImageFormatUnknown);
        id->in_img_array_id = spi_OpTypeArray(spi, id->in_img_type, spi_get_id(spi),
                                          id->u32_cid[in_img_count]);
    }

    /* Pointer types for images */
    id->u32vec3_tptr = spi_OpTypePointer(spi, SpvStorageClassInput,
                                         id->u32vec3_type);
    id->out_img_tptr = spi_OpTypePointer(spi, SpvStorageClassUniformConstant,
                                         id->out_img_array_id);
    id->out_img_sptr = spi_OpTypePointer(spi, SpvStorageClassUniformConstant,
                                         id->out_img_type);

    id->in_img_tptr = 0;
    id->in_img_sptr = 0;
    if (op_r) {
        id->in_img_tptr= spi_OpTypePointer(spi, SpvStorageClassUniformConstant,
                                           id->in_img_array_id);
        id->in_img_sptr= spi_OpTypePointer(spi, SpvStorageClassUniformConstant,
                                           id->in_img_type);
    }

    /* Define inputs */
    spi_OpVariable(spi, id->in_vars[0], id->u32vec3_tptr,
                   SpvStorageClassInput, 0);
    if (op_r) {
        spi_OpVariable(spi, id->in_vars[1], id->in_img_tptr,
                       SpvStorageClassUniformConstant, 0);
    }
    spi_OpVariable(spi, id->in_vars[2], id->out_img_tptr,
                   SpvStorageClassUniformConstant, 0);

    if (id->interlaced) {
        spi_OpTypeStruct(spi, id->push_const_struct_id, id->u32_type);
        id->push_const_ptr_id = spi_OpTypePointer(spi, SpvStorageClassPushConstant,
                                                  id->push_const_struct_id);
        id->push_const_elem_ptr_id = spi_OpTypePointer(spi, SpvStorageClassPushConstant,
                                                       id->u32_type);
        spi_OpVariable(spi, id->push_const_var_id, id->push_const_ptr_id,
                       SpvStorageClassPushConstant, 0);
    }
}

static int insert_vmat_linear(const SwsOp *op, SPICtx *spi, SPIRVIDs *id,
                              int data, int const_off)
{
    data =  spi_OpMatrixTimesVector(spi, id->f32vec4_type,
                                    id->const_ids[const_off + 4*5],
                                    data);
    return spi_OpFAdd(spi, id->f32vec4_type,
                      id->const_ids[const_off + 4*5 + 1 + 4], data);
}

static int insert_bitexact_linear(const SwsOp *op, SPICtx *spi, SPIRVIDs *id,
                                  int data, int linear_ops_idx, int const_off)
{
    int type_s = op->type == SWS_PIXEL_F32 ? id->f32_type : id->u32_type;
    int type_v = op->type == SWS_PIXEL_F32 ? id->f32vec4_type : id->u32vec4_type;

    int tmp[4];
    tmp[0] = spi_OpCompositeExtract(spi, type_s, data, 0);
    tmp[1] = spi_OpCompositeExtract(spi, type_s, data, 1);
    tmp[2] = spi_OpCompositeExtract(spi, type_s, data, 2);
    tmp[3] = spi_OpCompositeExtract(spi, type_s, data, 3);

    int off = spi_reserve(spi, 0); /* Current offset */
    spi->off = id->linear_deco_off[linear_ops_idx];
    for (int i = 0; i < id->linear_deco_ops[linear_ops_idx]; i++)
        spi_OpDecorate(spi, spi->id + i, SpvDecorationNoContraction);
    spi->off = off;

    int res[4];
    for (int j = 0; j < 4; j++) {
        res[j] = op->type == SWS_PIXEL_F32 ? id->f32_0 : id->u32_cid[0];
        if (op->lin.m[j][0].num)
            res[j] = spi_OpFMul(spi, type_s, tmp[0],
                                id->const_ids[const_off + j*5 + 0]);

        if (op->lin.m[j][0].num && op->lin.m[j][4].num)
            res[j] = spi_OpFAdd(spi, type_s,
                                id->const_ids[const_off + 4*5 + 1 + j], res[j]);
        else if (op->lin.m[j][4].num)
            res[j] = id->const_ids[const_off + 4*5 + 1 + j];

        for (int i = 1; i < 4; i++) {
            if (!op->lin.m[j][i].num)
                continue;

            int v = spi_OpFMul(spi, type_s, tmp[i],
                               id->const_ids[const_off + j*5 + i]);
            if (op->lin.m[j][0].num || op->lin.m[j][4].num)
                res[j] = spi_OpFAdd(spi, type_s, res[j], v);
            else
                res[j] = v;
        }
    }

    return spi_OpCompositeConstruct(spi, type_v,
                                    res[0], res[1], res[2], res[3]);
}

static int read_filtered(SPICtx *spi, SPIRVIDs *id, const SwsOpList *ops,
                         const SwsOp *op, const struct FilterData *f,
                         const int *in_img, int gid, int gi2)
{
    const int is_h = f->filter == SWS_OP_FILTER_H;
    const int src_interlaced = ops->src.interlaced;

    const int src_float = op->type == SWS_PIXEL_F32;
    const int read_vtype = src_float ? id->f32vec4_type : id->u32vec4_type;

    /* Buffer array index along the filtered axis: pos.x (H) or pos.y (V) */
    int axis = spi_OpCompositeExtract(spi, id->u32_type, gid, is_h ? 0 : 1);

    /* int o = filter_o[axis]; */
    int o_ptr = spi_OpAccessChain(spi, id->filt_o_ptr_id, f->id,
                                  id->u32_cid[1], axis);
    int o = spi_OpLoad(spi, id->i32_type, o_ptr, SpvMemoryAccessMaskNone, 0);

    /* Signed pixel position, for the non-filtered coordinate axis */
    int pos_x = spi_OpCompositeExtract(spi, id->i32_type, gi2, 0);
    int pos_y = spi_OpCompositeExtract(spi, id->i32_type, gi2, 1);

    /* For interlaced horizontal filtering, the y coordinate of every tap is
     * the (constant) destination y mapped into the source image. */
    if (src_interlaced && is_h) {
        pos_y = spi_OpShiftLeftLogical(spi, id->i32_type, pos_y, id->u32_cid[1]);
        pos_y = spi_OpIAdd(spi, id->i32_type, pos_y, id->field_i32);
    }

    /* Accumulators, initialized to zero */
    int acc_s[4] = { id->f32_0, id->f32_0, id->f32_0, id->f32_0 };
    int acc_v = id->f32_0;
    if (op->rw.mode == SWS_RW_PACKED)
        acc_v = spi_OpCompositeConstruct(spi, id->f32vec4_type,
                                         id->f32_0, id->f32_0,
                                         id->f32_0, id->f32_0);

    for (int t = 0; t < f->filter_size; t++) {
        /* float w = filter_w[axis][t]; */
        int w_ptr = spi_OpAccessChain(spi, id->dither_ptr_elem_id, f->id,
                                      id->u32_cid[0], axis,
                                      f->tap_const_base + t);
        int w = spi_OpLoad(spi, id->f32_type, w_ptr,
                           SpvMemoryAccessMaskNone, 0);

        /* Source coordinate, filtered axis offset by the tap index */
        int c = t ? spi_OpIAdd(spi, id->i32_type, o, f->tap_const_base + t) : o;
        /* For interlaced vertical filtering, the per-tap source row is
         * field-local; map it to the actual image row. */
        if (src_interlaced && !is_h) {
            c = spi_OpShiftLeftLogical(spi, id->i32_type, c, id->u32_cid[1]);
            c = spi_OpIAdd(spi, id->i32_type, c, id->field_i32);
        }
        int coord = is_h ?
            spi_OpCompositeConstruct(spi, id->i32vec2_type, c, pos_y) :
            spi_OpCompositeConstruct(spi, id->i32vec2_type, pos_x, c);

        if (op->rw.mode == SWS_RW_PACKED) {
            int px = spi_OpImageRead(spi, read_vtype,
                                     in_img[ops->plane_src[0]], coord,
                                     SpvImageOperandsMaskNone);
            if (!src_float)
                px = spi_OpConvertUToF(spi, id->f32vec4_type, px);
            px = spi_OpVectorTimesScalar(spi, id->f32vec4_type, px, w);
            acc_v = spi_OpFAdd(spi, id->f32vec4_type, acc_v, px);
        } else {
            for (int e = 0; e < op->rw.elems; e++) {
                int px = spi_OpImageRead(spi, read_vtype,
                                         in_img[ops->plane_src[e]], coord,
                                         SpvImageOperandsMaskNone);
                if (src_float) {
                    px = spi_OpCompositeExtract(spi, id->f32_type, px, 0);
                } else {
                    px = spi_OpCompositeExtract(spi, id->u32_type, px, 0);
                    px = spi_OpConvertUToF(spi, id->f32_type, px);
                }
                px = spi_OpFMul(spi, id->f32_type, w, px);
                acc_s[e] = spi_OpFAdd(spi, id->f32_type, acc_s[e], px);
            }
        }
    }

    if (op->rw.mode == SWS_RW_PACKED)
        return acc_v;
    return spi_OpCompositeConstruct(spi, id->f32vec4_type,
                                    acc_s[0], acc_s[1], acc_s[2], acc_s[3]);
}

/* Plane indices refer to actual frame planes, so the image handle arrays
 * have to cover the highest plane referenced, not just the plane count. */
static int rw_op_img_count(const SwsOp *op, const uint8_t *planes)
{
    int count = 0;
    for (int i = 0; i < ff_sws_rw_op_planes(op); i++)
        count = FFMAX(count, planes[i] + 1);
    return count;
}

static int add_ops_spirv(SwsContext *sws, VulkanPriv *p, FFVulkanOpsCtx *s,
                         const SwsOpList *ops, FFVulkanShader *shd)
{
    uint8_t spvbuf[1024*16];
    SPICtx spi_context = { 0 }, *spi = &spi_context;
    SPIRVIDs spid_data = { 0 }, *id = &spid_data;
    spi_init(spi, spvbuf, sizeof(spvbuf));

    id->interlaced = ops->src.interlaced || ops->dst.interlaced;
    p->interlaced = id->interlaced;

    ff_vk_shader_load(shd, VK_SHADER_STAGE_COMPUTE_BIT, NULL,
                      (uint32_t []) { 32, 32, 1 }, 0);
    shd->precompiled = 0;

    if (id->interlaced)
        ff_vk_shader_add_push_const(shd, 0, sizeof(uint32_t),
                                    VK_SHADER_STAGE_COMPUTE_BIT);

    /* Image ops, to determine types */
    const SwsOp *op_w = ff_sws_op_list_output(ops);
    int out_img_count = rw_op_img_count(op_w, ops->plane_dst);
    p->dst_rep = op_w->type == SWS_PIXEL_F32 ? FF_VK_REP_FLOAT : FF_VK_REP_UINT;

    const SwsOp *op_r = ff_sws_op_list_input(ops);
    int in_img_count = op_r ? rw_op_img_count(op_r, ops->plane_src) : 0;
    if (op_r)
        p->src_rep = op_r->type == SWS_PIXEL_F32 ? FF_VK_REP_FLOAT : FF_VK_REP_UINT;

    FFVulkanDescriptorSetBinding desc_set[MAX_DATA_BUFS] = {
        {
            .type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
            .stages = VK_SHADER_STAGE_COMPUTE_BIT,
            .elems = 4,
        },
        {
            .type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
            .stages = VK_SHADER_STAGE_COMPUTE_BIT,
            .elems = 4,
        },
    };
    ff_vk_shader_add_descriptor_set(&s->vkctx, shd, desc_set, 2, 0);

    /* Create dither buffers */
    int err = create_bufs(s, p, ops);
    if (err < 0)
        return err;

    /* Entrypoint inputs; gl_GlobalInvocationID, input and output images, dither */
    id->in_vars[0] = spi_get_id(spi);
    id->in_vars[1] = spi_get_id(spi);
    id->in_vars[2] = spi_get_id(spi);

    /* Create dither and filter buffer descriptor set. Both are collected in
     * op order, so the bindings match the buffer order from create_bufs().*/
    id->nb_dither_bufs = 0;
    id->nb_filter_bufs = 0;
    int nb_data_bufs = 0;
    for (int n = 0; n < ops->num_ops; n++) {
        const SwsOp *op = &ops->ops[n];
        int var_id = 0;

        if (op->op == SWS_OP_DITHER) {
            if (id->nb_dither_bufs >= MAX_DITHER_BUFS)
                return AVERROR(ENOTSUP);
            struct DitherData *d = &id->dither[id->nb_dither_bufs++];
            d->size      = 1 << op->dither.size_log2;
            d->arr_1d_id = spi_get_id(spi);
            d->arr_2d_id = spi_get_id(spi);
            d->struct_id = spi_get_id(spi);
            d->id        = spi_get_id(spi);
            d->binding   = nb_data_bufs;
            var_id       = d->id;
        } else if (op->op == SWS_OP_READ && op->rw.filter.op) {
            if (id->nb_filter_bufs >= MAX_FILT_BUFS)
                return AVERROR(ENOTSUP);
            const SwsFilterWeights *wd = op->rw.filter.kernel;
            struct FilterData *f = &id->filt[id->nb_filter_bufs++];
            f->filter       = op->rw.filter.op;
            f->filter_size  = wd->filter_size;
            f->dst_size     = wd->dst_size;
            f->num_weights  = wd->num_weights;
            f->arr_w_in_id  = spi_get_id(spi);
            f->arr_w_out_id = spi_get_id(spi);
            f->arr_o_id     = spi_get_id(spi);
            f->struct_id    = spi_get_id(spi);
            f->id           = spi_get_id(spi);
            f->binding      = nb_data_bufs;
            var_id          = f->id;
        } else {
            continue;
        }

        id->in_vars[3 + nb_data_bufs] = var_id;
        desc_set[nb_data_bufs++] = (FFVulkanDescriptorSetBinding) {
            .type        = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
            .stages      = VK_SHADER_STAGE_COMPUTE_BIT,
        };
    }
    if (nb_data_bufs)
        ff_vk_shader_add_descriptor_set(&s->vkctx, shd, desc_set,
                                        nb_data_bufs, 1);

    if (id->interlaced) {
        id->push_const_struct_id = spi_get_id(spi);
        id->push_const_var_id = spi_get_id(spi);
        id->in_vars[3 + id->nb_dither_bufs + id->nb_filter_bufs] =
            id->push_const_var_id;
    }

    /* Define shader header sections */
    define_shader_header(sws, shd, ops, spi, id);
    define_shader_consts(sws, ops, spi, id);
    define_shader_bindings(ops, spi, id, in_img_count, out_img_count);

    /* Main function starts here */
    spi_OpFunction(spi, id->ep, id->void_type, 0, id->void_fn_type);
    spi_OpLabel(spi, spi_get_id(spi));

    /* Load input image handles */
    int in_img[4] = { 0 };
    for (int i = 0; i < in_img_count; i++) {
        /* Deref array and then the pointer */
        int img = spi_OpAccessChain(spi, id->in_img_sptr,
                                    id->in_vars[1], id->u32_cid[i]);
        in_img[i] = spi_OpLoad(spi, id->in_img_type, img,
                               SpvMemoryAccessMaskNone, 0);
    }

    /* Load output image handles */
    int out_img[4] = { 0 };
    for (int i = 0; i < out_img_count; i++) {
        int img = spi_OpAccessChain(spi, id->out_img_sptr,
                                    id->in_vars[2], id->u32_cid[i]);
        out_img[i] = spi_OpLoad(spi, id->out_img_type, img,
                                SpvMemoryAccessMaskNone, 0);
    }

    /* Load gl_GlobalInvocationID */
    int gid = spi_OpLoad(spi, id->u32vec3_type, id->in_vars[0],
                         SpvMemoryAccessMaskNone, 0);

    /* ivec2(gl_GlobalInvocationID.xy) */
    gid = spi_OpVectorShuffle(spi, id->u32vec2_type, gid, gid, 0, 1);
    int gi2 = spi_OpBitcast(spi, id->i32vec2_type, gid);

    /* For interlaced sources/destinations the shader operates on field-local
     * coordinates, while images contain the full frame. Map the y axis to the
     * actual image row: image_y = field_y * 2 + field. */
    int dst_gid = gid, dst_gi2 = gi2;
    int src_gid = gid;
    if (id->interlaced) {
        int field_u32_ptr = spi_OpAccessChain(spi, id->push_const_elem_ptr_id,
                                              id->push_const_var_id,
                                              id->u32_cid[0]);
        int field_u32 = spi_OpLoad(spi, id->u32_type, field_u32_ptr,
                                   SpvMemoryAccessMaskNone, 0);
        id->field_i32 = spi_OpBitcast(spi, id->i32_type, field_u32);

        int img_y_i32 = spi_OpShiftLeftLogical(spi, id->i32_type,
                                               spi_OpCompositeExtract(spi, id->i32_type, gi2, 1),
                                               id->u32_cid[1]);
        img_y_i32 = spi_OpIAdd(spi, id->i32_type, img_y_i32, id->field_i32);

        int gi2_x = spi_OpCompositeExtract(spi, id->i32_type, gi2, 0);
        int mapped_gi2 = spi_OpCompositeConstruct(spi, id->i32vec2_type,
                                                  gi2_x, img_y_i32);
        int mapped_gid = spi_OpBitcast(spi, id->u32vec2_type, mapped_gi2);

        if (ops->src.interlaced)
            src_gid = mapped_gid;
        if (ops->dst.interlaced) {
            dst_gid = mapped_gid;
            dst_gi2 = mapped_gi2;
        }
    }

    /* imageSize(out_img[0]); */
    int img1_s = spi_OpImageQuerySize(spi, id->i32vec2_type, out_img[0]);
    int scmp = spi_OpSGreaterThanEqual(spi, id->bvec2_type, dst_gi2, img1_s);
    scmp = spi_OpAny(spi, id->b_type, scmp);

    /* if (out of bounds) return */
    int quit_label = spi_get_id(spi), merge_label = spi_get_id(spi);
    spi_OpSelectionMerge(spi, merge_label, SpvSelectionControlMaskNone);
    spi_OpBranchConditional(spi, scmp, quit_label, merge_label, 0);

    spi_OpLabel(spi, quit_label);
    spi_OpReturn(spi); /* Quit if out of bounds here */
    spi_OpLabel(spi, merge_label);

    /* Initialize main data state */
    int data;
    if (ops->ops[0].type == SWS_PIXEL_F32)
        data = spi_OpCompositeConstruct(spi, id->f32vec4_type,
                                        id->f32_p, id->f32_p,
                                        id->f32_p, id->f32_p);
    else
        data = spi_OpCompositeConstruct(spi, id->u32vec4_type,
                                        id->u32_p, id->u32_p,
                                        id->u32_p, id->u32_p);

    /* Keep track of which constant/buffer to use */
    int nb_const_ids = 0;
    int nb_dither_bufs = 0;
    int nb_linear_ops = 0;
    int nb_filter_used = 0;

    /* Operations */
    for (int n = 0; n < ops->num_ops; n++) {
        const SwsOp *op = &ops->ops[n];
        SwsPixelType cur_type = op->op == SWS_OP_CONVERT ?
                                op->convert.to : op->type;
        int type_v = cur_type == SWS_PIXEL_F32 ?
                     id->f32vec4_type : id->u32vec4_type;
        int type_s = cur_type == SWS_PIXEL_F32 ?
                     id->f32_type : id->u32_type;
        int uid = cur_type == SWS_PIXEL_F32 ?
                  id->f32_p : id->u32_p;

        switch (op->op) {
        case SWS_OP_READ:
            if (op->rw.frac) {
                return AVERROR(ENOTSUP);
            } else if (op->rw.filter.op) {
                av_assert0(op->rw.mode != SWS_RW_PALETTE);
                data = read_filtered(spi, id, ops, op,
                                     &id->filt[nb_filter_used++],
                                     in_img, gid, gi2);
            } else if (op->rw.mode == SWS_RW_PACKED) {
                data = spi_OpImageRead(spi, type_v, in_img[ops->plane_src[0]],
                                       src_gid, SpvImageOperandsMaskNone);
            } else if (op->rw.mode == SWS_RW_PLANAR) {
                int tmp[4] = { uid, uid, uid, uid };
                for (int i = 0; i < op->rw.elems; i++) {
                    tmp[i] = spi_OpImageRead(spi, type_v,
                                             in_img[ops->plane_src[i]], src_gid,
                                             SpvImageOperandsMaskNone);
                    tmp[i] = spi_OpCompositeExtract(spi, type_s, tmp[i], 0);
                }
                data = spi_OpCompositeConstruct(spi, type_v,
                                                tmp[0], tmp[1], tmp[2], tmp[3]);
            } else {
                return AVERROR(ENOTSUP);
            }
            break;
        case SWS_OP_WRITE:
            if (op->rw.frac || op->rw.filter.op) {
                return AVERROR(ENOTSUP);
            } else if (op->rw.mode == SWS_RW_PACKED) {
                spi_OpImageWrite(spi, out_img[ops->plane_dst[0]], dst_gid, data,
                                 SpvImageOperandsMaskNone);
            } else {
                for (int i = 0; i < op->rw.elems; i++) {
                    int tmp = spi_OpCompositeExtract(spi, type_s, data, i);
                    tmp = spi_OpCompositeConstruct(spi, type_v, tmp, tmp, tmp, tmp);
                    spi_OpImageWrite(spi, out_img[ops->plane_dst[i]], dst_gid, tmp,
                                     SpvImageOperandsMaskNone);
                }
            }
            break;
        case SWS_OP_CLEAR:
            for (int i = 0; i < 4; i++) {
                if (!SWS_COMP_TEST(op->clear.mask, i))
                    continue;
                data = spi_OpCompositeInsert(spi, type_v,
                                             id->const_ids[nb_const_ids++],
                                             data, i);
            }
            break;
        case SWS_OP_SWIZZLE:
            data = spi_OpVectorShuffle(spi, type_v, data, data,
                                       op->swizzle.in[0],
                                       op->swizzle.in[1],
                                       op->swizzle.in[2],
                                       op->swizzle.in[3]);
            break;
        case SWS_OP_CONVERT:
            if (ff_sws_pixel_type_is_int(cur_type) && op->convert.expand)
                data = spi_OpIMul(spi, type_v, data, id->const_ids[nb_const_ids++]);
            else if (op->type == SWS_PIXEL_F32 && type_s == id->u32_type)
                data = spi_OpConvertFToU(spi, type_v, data);
            else if (op->type != SWS_PIXEL_F32 && type_s == id->f32_type)
                data = spi_OpConvertUToF(spi, type_v, data);
            break;
        case SWS_OP_LSHIFT:
            data = spi_OpShiftLeftLogical(spi, type_v, data,
                                          id->const_ids[nb_const_ids++]);
            break;
        case SWS_OP_RSHIFT:
            data = spi_OpShiftRightLogical(spi, type_v, data,
                                           id->const_ids[nb_const_ids++]);
            break;
        case SWS_OP_SCALE:
            if (op->type == SWS_PIXEL_F32)
                data = spi_OpFMul(spi, type_v, data,
                                  id->const_ids[nb_const_ids++]);
            else
                data = spi_OpIMul(spi, type_v, data,
                                  id->const_ids[nb_const_ids++]);
            break;
        case SWS_OP_MIN:
        case SWS_OP_MAX: {
            int t = op->type == SWS_PIXEL_F32 ?
                    op->op == SWS_OP_MIN ? GLSLstd450FMin : GLSLstd450FMax :
                    op->op == SWS_OP_MIN ? GLSLstd450UMin : GLSLstd450UMax;
            for (int i = 0; i < 4; i++) {
                if (!op->clamp.limit[i].den)
                    continue;
                int tmp = spi_OpCompositeExtract(spi, type_s, data, i);
                tmp = spi_OpExtInst(spi, type_s, id->glfn, t,
                                    tmp, id->const_ids[nb_const_ids++]);
                data = spi_OpCompositeInsert(spi, type_v, tmp, data, i);
            }
            break;
        }
        case SWS_OP_DITHER: {
            int did = nb_dither_bufs++;
            int x_id = spi_OpCompositeExtract(spi, id->u32_type, gid, 0);
            int y_pos = spi_OpCompositeExtract(spi, id->u32_type, gid, 1);
            x_id = spi_OpBitwiseAnd(spi, id->u32_type, x_id,
                                    id->dither[did].mask_id);
            for (int i = 0; i < 4; i++) {
                if (op->dither.y_offset[i] < 0)
                    continue;

                int y_id = spi_OpIAdd(spi, id->u32_type, y_pos,
                                      id->const_ids[nb_const_ids++]);
                y_id = spi_OpBitwiseAnd(spi, id->u32_type, y_id,
                                        id->dither[did].mask_id);

                int ptr = spi_OpAccessChain(spi, id->dither_ptr_elem_id,
                                            id->dither[did].id, id->u32_cid[0],
                                            y_id, x_id);
                int val = spi_OpLoad(spi, id->f32_type, ptr,
                                     SpvMemoryAccessMaskNone, 0);

                int tmp = spi_OpCompositeExtract(spi, type_s, data, i);
                tmp = spi_OpFAdd(spi, type_s, tmp, val);
                data = spi_OpCompositeInsert(spi, type_v, tmp, data, i);
            }
            break;
        }
        case SWS_OP_LINEAR: {
            if (sws->flags & SWS_BITEXACT)
                data = insert_bitexact_linear(op, spi, id, data, nb_linear_ops, nb_const_ids);
            else
                data = insert_vmat_linear(op, spi, id, data, nb_const_ids);
            nb_linear_ops++;
            nb_const_ids += 5*5 + 1;
            break;
        }
        case SWS_OP_UNPACK:
            if (ops->src.format == AV_PIX_FMT_X2BGR10)
                data = spi_OpVectorShuffle(spi, type_v, data, data, 3, 2, 1, 0);
            else
                data = spi_OpVectorShuffle(spi, type_v, data, data, 3, 0, 1, 2);
            break;
        case SWS_OP_PACK:
            if (ops->dst.format == AV_PIX_FMT_X2BGR10)
                data = spi_OpVectorShuffle(spi, type_v, data, data, 3, 2, 1, 0);
            else
                data = spi_OpVectorShuffle(spi, type_v, data, data, 1, 2, 3, 0);
            break;
        default:
            return AVERROR(ENOTSUP);
        }
    }

    /* Return and finalize */
    spi_OpReturn(spi);
    spi_OpFunctionEnd(spi);

    int len = spi_end(spi);
    if (len < 0)
        return AVERROR_INVALIDDATA;

    return ff_vk_shader_link(&s->vkctx, shd, spvbuf, len, "main");
}
#endif

static int compile(SwsContext *sws, const SwsOpList *ops, SwsCompiledOp *out)
{
    int err;
    SwsInternal *c = sws_internal(sws);
    FFVulkanOpsCtx *s = c->hw_priv;
    if (!s)
        return AVERROR(ENOTSUP);

    VulkanPriv *p = av_mallocz(sizeof(*p));
    if (!p)
        return AVERROR(ENOMEM);
    p->s = av_refstruct_ref(c->hw_priv);

    err = ff_vk_exec_pool_init(&s->vkctx, s->qf, &p->e, 1,
                               0, 0, 0, NULL);
    if (err < 0)
        goto fail;

    err = AVERROR(ENOTSUP);
#if HAVE_SPIRV_HEADERS_SPIRV_H || HAVE_SPIRV_UNIFIED1_SPIRV_H
    err = add_ops_spirv(sws, p, s, ops, &p->shd);
#endif
    if (err < 0)
        goto fail;

    err = ff_vk_shader_register_exec(&s->vkctx, &p->e, &p->shd);
    if (err < 0)
        goto fail;

    for (int i = 0; i < p->nb_data_bufs; i++)
        ff_vk_shader_update_desc_buffer(&s->vkctx, &p->e.contexts[0], &p->shd,
                                        1, i, 0, &p->data_bufs[i],
                                        0, VK_WHOLE_SIZE, VK_FORMAT_UNDEFINED);

    *out = (SwsCompiledOp) {
        .opaque      = true,
        .func_opaque = process,
        .priv        = p,
        .free        = free_fn,
    };

    return 0;

fail:
    free_fn(p);
    return err;
}

#if HAVE_SPIRV_HEADERS_SPIRV_H || HAVE_SPIRV_UNIFIED1_SPIRV_H
static int compile_spirv(SwsContext *sws, const SwsOpList *ops,
                         SwsCompiledOp *out)
{
    return compile(sws, ops, out);
}

const SwsOpBackend backend_spirv = {
    .name      = "spirv",
    .flags     = SWS_BACKEND_SPIRV,
    .compile   = compile_spirv,
    .hw_format = AV_PIX_FMT_VULKAN,
};
#endif
