Commit dce56e75eb for ffmpeg

commit dce56e75ebd74296009b33502f7e997f0e3b8c2b
Author: TADANO Tokumei <aimingoff@pc.nifty.jp>
Date:   Tue Sep 22 01:19:32 2026 +0900

    avfilter/v360_vulkan: support GoPro Max inputs

    Add GoPro Max 360 projection for input.
    Add tentative Equi-Angular Cubemap (EAC) projection for output
    (for testing).

    Signed-off-by: TADANO Tokumei <aimingoff@pc.nifty.jp>

diff --git a/libavfilter/Makefile b/libavfilter/Makefile
index ac194bead7..15d0b23af9 100644
--- a/libavfilter/Makefile
+++ b/libavfilter/Makefile
@@ -560,7 +560,7 @@ OBJS-$(CONFIG_UNSHARP_OPENCL_FILTER)         += vf_unsharp_opencl.o opencl.o \
 OBJS-$(CONFIG_UNTILE_FILTER)                 += vf_untile.o
 OBJS-$(CONFIG_USPP_FILTER)                   += vf_uspp.o qp_table.o
 OBJS-$(CONFIG_V360_FILTER)                   += vf_v360.o
-OBJS-$(CONFIG_V360_VULKAN_FILTER)            += vf_v360_vulkan.o
+OBJS-$(CONFIG_V360_VULKAN_FILTER)            += vf_v360_vulkan.o framesync.o
 OBJS-$(CONFIG_VAGUEDENOISER_FILTER)          += vf_vaguedenoiser.o
 OBJS-$(CONFIG_VARBLUR_FILTER)                += vf_varblur.o framesync.o
 OBJS-$(CONFIG_VECTORSCOPE_FILTER)            += vf_vectorscope.o
diff --git a/libavfilter/v360.h b/libavfilter/v360.h
index b3660c234c..f62f37df08 100644
--- a/libavfilter/v360.h
+++ b/libavfilter/v360.h
@@ -55,6 +55,7 @@ enum Projections {
     ORTHOGRAPHIC,
     OCTAHEDRON,
     CYLINDRICALEA,
+    GOPROMAX,
     NB_PROJECTIONS,
 };

diff --git a/libavfilter/vf_v360_vulkan.c b/libavfilter/vf_v360_vulkan.c
index 85b4c6bfb5..1780f7c106 100644
--- a/libavfilter/vf_v360_vulkan.c
+++ b/libavfilter/vf_v360_vulkan.c
@@ -22,6 +22,7 @@
 #include "v360.h"
 #include "filters.h"
 #include "video.h"
+#include "framesync.h"

 extern const unsigned char ff_v360_comp_spv_data[];
 extern const unsigned int ff_v360_comp_spv_len;
@@ -35,6 +36,7 @@ struct PushData {

 typedef struct V360ulkanContext {
     FFVulkanContext vkctx;
+    FFFrameSync fs;

     int initialized;
     FFVkExecPool e;
@@ -51,6 +53,7 @@ typedef struct V360ulkanContext {
     float yaw, pitch, roll;
     char *rorder;
     int   rotation_order[3];
+    int   overlap;
 } V360VulkanContext;

 static int get_rorder(char c)
@@ -198,6 +201,7 @@ static void config_params(AVFilterContext *ctx, AVFilterLink *inlink)
             s->iv_fov = 180.f;
         break;
     case EQUIRECTANGULAR: /* unchangeable */
+    case GOPROMAX:
         s->ih_fov = 360.f;
         s->iv_fov = 180.f;
         break;
@@ -335,6 +339,9 @@ static av_cold int calculate_output_size(AVFilterContext *ctx)
         case DUAL_FISHEYE:
             hf = wf * sar / 2.f;
             break;
+        case EQUIANGULAR:
+            hf = wf * sar / 3.f * 2.f;
+            break;
         case STEREOGRAPHIC:
         case FISHEYE:
             hf = wf * sar;
@@ -353,6 +360,9 @@ static av_cold int calculate_output_size(AVFilterContext *ctx)
             case FLAT:
                  hf = (float)inlink->h / s->pd.iflat_range[1] / 2.f;
                 break;
+            case GOPROMAX:
+                hf = (float)inlink->h * 2.f;
+                break;
             default:
                 hf = (float)inlink->h;
                 break;
@@ -376,6 +386,9 @@ static av_cold int calculate_output_size(AVFilterContext *ctx)
         case DUAL_FISHEYE:
             wf = hf * 2.f;
             break;
+        case EQUIANGULAR:
+            wf = hf * 3.f / 2.f;
+            break;
         case STEREOGRAPHIC:
         case FISHEYE:
             wf = hf;
@@ -404,6 +417,10 @@ static av_cold int calculate_output_size(AVFilterContext *ctx)
         min_w = 2;
         min_h = 1;
         break;
+    case EQUIANGULAR:
+        min_w = 3;
+        min_h = 2;
+        break;
     default:
         min_w = 1;
         min_h = 1;
@@ -438,7 +455,7 @@ static av_cold int init_filter(AVFilterContext *ctx, AVFrame *in)

     RET(ff_vk_exec_pool_init(vkctx, s->qf, &s->e, FF_VK_DEFAULT_EXEC_CONTEXTS, 0, 0, 0, NULL));

-    SPEC_LIST_CREATE(sl, 10, 7*sizeof(int) + 3*sizeof(float))
+    SPEC_LIST_CREATE(sl, 17, 14*sizeof(int) + 3*sizeof(float))
     SPEC_LIST_ADD(sl, 0, 32, s->out);
     SPEC_LIST_ADD(sl, 1, 32, s->in);

@@ -453,6 +470,19 @@ static av_cold int init_filter(AVFilterContext *ctx, AVFrame *in)
     SPEC_LIST_ADD(sl, 8, 32, FF_CEIL_RSHIFT(in->width, desc->log2_chroma_w));
     SPEC_LIST_ADD(sl, 9, 32, FF_CEIL_RSHIFT(in->height, desc->log2_chroma_h));

+    if (s->in == GOPROMAX) {
+        int cube_size = in->height;
+        int gopro_cube_width = (in->width - cube_size) / 2;
+
+        SPEC_LIST_ADD(sl, 10, 32, cube_size);
+        SPEC_LIST_ADD(sl, 11, 32, gopro_cube_width);
+        SPEC_LIST_ADD(sl, 12, 32, s->overlap);
+        SPEC_LIST_ADD(sl, 13, 32, gopro_cube_width + cube_size);
+        SPEC_LIST_ADD(sl, 14, 32, gopro_cube_width / 2 - s->overlap);
+        SPEC_LIST_ADD(sl, 15, 32, gopro_cube_width / 2);
+        SPEC_LIST_ADD(sl, 16, 32, gopro_cube_width / 2 + s->overlap);
+    }
+
     ff_vk_shader_load(&s->shd, VK_SHADER_STAGE_COMPUTE_BIT,
                       sl, (uint32_t []) { 16, 16, 1 }, 0);

@@ -460,19 +490,25 @@ static av_cold int init_filter(AVFilterContext *ctx, AVFrame *in)
                                 VK_SHADER_STAGE_COMPUTE_BIT);

     const FFVulkanDescriptorSetBinding desc_set[] = {
+        { /* output_img */
+            .type   = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
+            .stages = VK_SHADER_STAGE_COMPUTE_BIT,
+            .elems  = planes,
+        },
         { /* input_img */
             .type     = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
             .stages   = VK_SHADER_STAGE_COMPUTE_BIT,
             .elems    = planes,
             .samplers = DUP_SAMPLER(s->sampler),
         },
-        { /* output_img */
-            .type   = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
-            .stages = VK_SHADER_STAGE_COMPUTE_BIT,
-            .elems  = planes,
+        { /* input2_img */
+            .type     = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
+            .stages   = VK_SHADER_STAGE_COMPUTE_BIT,
+            .elems    = planes,
+            .samplers = DUP_SAMPLER(s->sampler),
         },
     };
-    ff_vk_shader_add_descriptor_set(vkctx, &s->shd, desc_set, 2, 0);
+    ff_vk_shader_add_descriptor_set(vkctx, &s->shd, desc_set, 3, 0);

     RET(ff_vk_shader_link(vkctx, &s->shd,
                           ff_v360_comp_spv_data,
@@ -486,13 +522,20 @@ fail:
     return err;
 }

-static int v360_vulkan_filter_frame(AVFilterLink *link, AVFrame *in)
+static int v360_vulkan_filter_frame(FFFrameSync *fs)
 {
     int err;
-    AVFrame *out = NULL;
-    AVFilterContext *ctx = link->dst;
-    V360VulkanContext *s = ctx->priv;
+    AVFilterContext  *ctx = fs->parent;
+    V360VulkanContext  *s = ctx->priv;
     AVFilterLink *outlink = ctx->outputs[0];
+    AVFrame *in[2] = { NULL };
+    AVFrame *out = NULL;
+
+    for (int i = 0; i < ctx->nb_inputs; i++) {
+        RET(ff_framesync_get_frame(fs, i, &in[i], 0));
+        if (!in[i])
+            return 0;
+    }

     out = ff_get_video_buffer(outlink, outlink->w, outlink->h);
     if (!out) {
@@ -500,27 +543,37 @@ static int v360_vulkan_filter_frame(AVFilterLink *link, AVFrame *in)
         goto fail;
     }

-    if (!s->initialized)
-        RET(init_filter(ctx, in));
-
-    RET(ff_vk_filter_process_simple(&s->vkctx, &s->e, &s->shd,
-                                    out, in, s->sampler, 1,
-                                    &s->pd, sizeof(s->pd)));
-
-    err = av_frame_copy_props(out, in);
-    if (err < 0)
-        goto fail;
+    if (!s->initialized) {
+        AVHWFramesContext *fc0 = (AVHWFramesContext*)in[0]->hw_frames_ctx->data;
+        for (int i = 1; i < ctx->nb_inputs; i++) {
+            AVHWFramesContext *fci = (AVHWFramesContext*)in[i]->hw_frames_ctx->data;
+            if (fc0->sw_format != fci->sw_format) {
+                av_log(ctx, AV_LOG_ERROR, "Input %d has a different format "
+                                          "from input 0\n", i);
+                err = AVERROR(EINVAL);
+                goto fail;
+            }
+        }
+        RET(init_filter(ctx, in[0]));
+    }

-    av_frame_free(&in);
+    RET(ff_vk_filter_process_Nin(&s->vkctx, &s->e, &s->shd, out,
+                                 in, ctx->nb_inputs, s->sampler, 1,
+                                 &s->pd, sizeof(s->pd)));
+    RET(av_frame_copy_props(out, in[0]));

     return ff_filter_frame(outlink, out);

 fail:
-    av_frame_free(&in);
     av_frame_free(&out);
     return err;
 }

+static int v360_vulkan_activate(AVFilterContext *ctx)
+{
+    V360VulkanContext *s = ctx->priv;
+    return ff_framesync_activate(&s->fs);
+}

 static int process_command(AVFilterContext *ctx, const char *cmd, const char *args,
                            char *res, int res_len, int flags)
@@ -539,13 +592,80 @@ static av_cold int v360_vulkan_config_output(AVFilterLink *outlink)
 {
     int err;
     AVFilterContext *ctx = outlink->src;
+    V360VulkanContext *s = ctx->priv;
     AVFilterLink *inlink = ctx->inputs[0];

+    if (s->in == GOPROMAX) {
+        if (s->overlap == 0) {
+            if (inlink->h <= 960) {
+                s->overlap = 32;
+            }
+            else if (inlink->h < 1920) {
+                s->overlap = 64;
+            }
+            else
+                s->overlap = 96;
+        }
+        if (ctx->nb_inputs != 2) {
+            av_log(ctx, AV_LOG_ERROR, "GoPro Max. requires 2 input streams.\n");
+            return AVERROR(EINVAL);
+        }
+        if ((inlink->w != ctx->inputs[1]->w) ||
+            (inlink->h != ctx->inputs[1]->h) ||
+            (inlink->w < inlink->h * 3) ||
+            (inlink->w > inlink->h * 3 + s->overlap * 2)) {
+            av_log(ctx, AV_LOG_ERROR, "Incompatible inputs for GoPro Max. (%dx%d + %dx%d)\n",
+                                      inlink->w, inlink->h, ctx->inputs[1]->w, ctx->inputs[1]->h);
+            return AVERROR(EINVAL);
+       }
+    }
+    else if (ctx->nb_inputs != 1) {
+        av_log(ctx, AV_LOG_ERROR, "Too many inputs (%d)\n", ctx->nb_inputs);
+        return AVERROR(EINVAL);
+    }
+
     config_params(ctx, inlink);
     RET(calculate_output_size(ctx));
-
     RET(ff_vk_filter_config_output(outlink));

+    RET(ff_framesync_init(&s->fs, ctx, ctx->nb_inputs));
+    for (int i = 0; i < ctx->nb_inputs; i++) {
+        s->fs.in[i].time_base = ctx->inputs[i]->time_base;
+        s->fs.in[i].sync      = 1;
+        s->fs.in[i].before    = EXT_STOP;
+        s->fs.in[i].after     = EXT_STOP;
+    }
+    s->fs.on_event = &v360_vulkan_filter_frame;
+
+    RET(ff_framesync_configure(&s->fs));
+    outlink->time_base = s->fs.time_base;
+
+fail:
+    return err;
+}
+
+static av_cold int v360_vulkan_init(AVFilterContext *avctx)
+{
+    int err;
+    V360VulkanContext *s = avctx->priv;
+    AVFilterPad pad = {
+        .type         = AVMEDIA_TYPE_VIDEO,
+        .config_props = ff_vk_filter_config_input,
+    };
+
+    if (s->in == GOPROMAX) {
+        pad.name = "front";
+        RET(ff_append_inpad(avctx, &pad));
+        pad.name = "rear";
+        RET(ff_append_inpad(avctx, &pad));
+    }
+    else {
+        pad.name = "default";
+        RET(ff_append_inpad(avctx, &pad));
+    }
+
+    return ff_vk_filter_init(avctx);
+
 fail:
     return err;
 }
@@ -556,6 +676,7 @@ static void v360_vulkan_uninit(AVFilterContext *avctx)
     FFVulkanContext *vkctx = &s->vkctx;
     FFVulkanFunctions *vk = &vkctx->vkfn;

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

@@ -579,10 +700,12 @@ static const AVOption v360_vulkan_options[] = {
     {  "dfisheye", "dual fisheye",                                 0, AV_OPT_TYPE_CONST,  {.i64=DUAL_FISHEYE},    0,                   0,   FLAGS, "in" },
     {        "sg", "stereographic",                                0, AV_OPT_TYPE_CONST,  {.i64=STEREOGRAPHIC},   0,                   0,   FLAGS, "in" },
     {   "fisheye", "fisheye",                                      0, AV_OPT_TYPE_CONST,  {.i64=FISHEYE},         0,                   0,   FLAGS, "in" },
+    {  "gopromax", "GoPro Max (two inputs)",                       0, AV_OPT_TYPE_CONST,  {.i64=GOPROMAX},        0,                   0,   FLAGS, "in" },

     {    "output", "set output projection",              OFFSET(out), AV_OPT_TYPE_INT,    {.i64=FLAT},            0,    NB_PROJECTIONS-1,   FLAGS, "out" },
     {         "e", "equirectangular",                              0, AV_OPT_TYPE_CONST,  {.i64=EQUIRECTANGULAR}, 0,                   0,   FLAGS, "out" },
     {  "equirect", "equirectangular",                              0, AV_OPT_TYPE_CONST,  {.i64=EQUIRECTANGULAR}, 0,                   0,   FLAGS, "out" },
+    {       "eac", "equi-angular cubemap",                         0, AV_OPT_TYPE_CONST,  {.i64=EQUIANGULAR},     0,                   0,   FLAGS, "out" },
     {      "flat", "regular video",                                0, AV_OPT_TYPE_CONST,  {.i64=FLAT},            0,                   0,   FLAGS, "out" },
     {  "dfisheye", "dual fisheye",                                 0, AV_OPT_TYPE_CONST,  {.i64=DUAL_FISHEYE},    0,                   0,   FLAGS, "out" },
     {        "sg", "stereographic",                                0, AV_OPT_TYPE_CONST,  {.i64=STEREOGRAPHIC},   0,                   0,   FLAGS, "out" },
@@ -598,26 +721,18 @@ static const AVOption v360_vulkan_options[] = {
     {     "v_fov", "set output vertical FOV angle",    OFFSET(v_fov), AV_OPT_TYPE_FLOAT,  {.dbl = 0.0f},       0.0f,              360.0f, DYNAMIC, "v_fov" },
     {    "ih_fov", "set input horizontal FOV angle",  OFFSET(ih_fov), AV_OPT_TYPE_FLOAT,  {.dbl = 0.0f},       0.0f,              360.0f, DYNAMIC, "ih_fov" },
     {    "iv_fov", "set input vertical FOV angle",    OFFSET(iv_fov), AV_OPT_TYPE_FLOAT,  {.dbl = 0.0f},       0.0f,              360.0f, DYNAMIC, "iv_fov" },
+    {   "overlap", "overlapped pixels for GoPro Max", OFFSET(overlap), AV_OPT_TYPE_INT,    {.i64 = 0},            0,                1024,   FLAGS, "overlap" },

     { NULL },
 };

-AVFILTER_DEFINE_CLASS(v360_vulkan);
-
-static const AVFilterPad v360_vulkan_inputs[] = {
-    {
-        .name         = "default",
-        .type         = AVMEDIA_TYPE_VIDEO,
-        .filter_frame = &v360_vulkan_filter_frame,
-        .config_props = &ff_vk_filter_config_input,
-    },
-};
+FRAMESYNC_DEFINE_CLASS(v360_vulkan, V360VulkanContext, fs);

 static const AVFilterPad v360_vulkan_outputs[] = {
     {
         .name = "default",
         .type = AVMEDIA_TYPE_VIDEO,
-        .config_props = &v360_vulkan_config_output,
+        .config_props = v360_vulkan_config_output,
     },
 };

@@ -625,13 +740,14 @@ const FFFilter ff_vf_v360_vulkan = {
     .p.name         = "v360_vulkan",
     .p.description  = NULL_IF_CONFIG_SMALL("Convert 360 projection of video."),
     .p.priv_class   = &v360_vulkan_class,
-    .p.flags        = AVFILTER_FLAG_HWDEVICE,
+    .p.flags        = AVFILTER_FLAG_HWDEVICE | AVFILTER_FLAG_DYNAMIC_INPUTS,
     .priv_size      = sizeof(V360VulkanContext),
-    .init           = &ff_vk_filter_init,
-    .uninit         = &v360_vulkan_uninit,
-    FILTER_INPUTS(v360_vulkan_inputs),
+    .preinit        = v360_vulkan_framesync_preinit,
+    .init           = v360_vulkan_init,
+    .uninit         = v360_vulkan_uninit,
+    .activate       = v360_vulkan_activate,
     FILTER_OUTPUTS(v360_vulkan_outputs),
     FILTER_SINGLE_PIXFMT(AV_PIX_FMT_VULKAN),
     .flags_internal = FF_FILTER_FLAG_HWFRAME_AWARE,
-    .process_command = &process_command,
+    .process_command = process_command,
 };
diff --git a/libavfilter/vulkan/v360.comp.glsl b/libavfilter/vulkan/v360.comp.glsl
index 131b10e3b2..982555f8d4 100644
--- a/libavfilter/vulkan/v360.comp.glsl
+++ b/libavfilter/vulkan/v360.comp.glsl
@@ -25,10 +25,12 @@
 layout (local_size_x_id = 253, local_size_y_id = 254, local_size_z_id = 255) in;

 #define EQUIRECTANGULAR  0
+#define EQUIANGULAR      3
 #define FLAT             4
 #define DUAL_FISHEYE     5
 #define STEREOGRAPHIC    8
 #define FISHEYE         13
+#define GOPROMAX        25
 layout (constant_id = 0) const int out_transform = 0;
 layout (constant_id = 1) const int in_transform = 0;

@@ -42,8 +44,17 @@ layout (constant_id = 7) const int in_h = 0;
 layout (constant_id = 8) const int in_chroma_w = 0;
 layout (constant_id = 9) const int in_chroma_h = 0;

-layout (set = 0, binding = 0) uniform sampler2D input_img[];
-layout (set = 0, binding = 1) uniform writeonly image2D output_img[];
+layout (constant_id = 10) const int in_cube_size = 0;
+layout (constant_id = 11) const int gopro_cube_width = 0;
+layout (constant_id = 12) const int gopro_overlap = 0;
+layout (constant_id = 13) const int gopro_face_offset_2 = 0;
+layout (constant_id = 14) const int gopro_blend_x_0 = 0;
+layout (constant_id = 15) const int gopro_blend_x_1 = 0;
+layout (constant_id = 16) const int gopro_blend_x_2 = 0;
+
+layout (set = 0, binding = 0) uniform writeonly image2D output_img[];
+layout (set = 0, binding = 1) uniform sampler2D input_img[];
+layout (set = 0, binding = 2) uniform sampler2D input2_img[];

 layout (push_constant, scalar) uniform pushConstants {
     mat4 rot_mat;
@@ -58,8 +69,27 @@ ivec2 in_img_size[4] = {
     ivec2(in_w, in_h)
 };

+ivec3 gopro_face_offset_x = ivec3(0,
+                                  gopro_cube_width,
+                                  gopro_face_offset_2);
+ivec3 gopro_face_size_x = ivec3(gopro_cube_width,
+                                in_cube_size,
+                                gopro_cube_width);
+ivec3 gopro_blend_x = ivec3(gopro_blend_x_0,
+                            gopro_blend_x_1,
+                            gopro_blend_x_2);
+
 #define IS_WITHIN(v1, v2) any(lessThan(v1, v2))

+float atan2(in float y, in float x)
+{
+    return abs(x) < 1e-6 ? sign(y) * m_pi2 : atan(y, x);
+}
+vec2 atan2(in vec2 v, in float z)
+{
+    return abs(z) < 1e-6 ? sign(v) * m_pi2 : atan(v, vec2(z));
+}
+
 void xyz_to_flat(uint idx, in vec3 v, in ivec2 pos, in ivec2 in_size)
 {
     const float r = tan(acos(v[2]));
@@ -166,6 +196,133 @@ void xyz_to_dfisheye(uint idx, in vec3 v, in ivec2 pos, in ivec2 in_size)
     imageStore(output_img[idx], pos, res);
 }

+// FIXME: no padding
+vec3 eac_to_xyz(in ivec2 out_size, in ivec2 pos)
+{
+    const ivec2 cube_size = out_size / ivec2(3, 2);
+    const ivec2 face = pos / cube_size;
+    const vec2 e_uv = vec2(pos % cube_size) / vec2(cube_size);
+    const int f = face.y * 3 + face.x;
+    vec3 v;
+    vec2 uv;
+
+    uv.x = (e_uv.x >= 1.0) ? 1.0 : tan((e_uv.x - 0.5) * m_pi2);
+    uv.y = (e_uv.y >= 1.0) ? 1.0 : tan((e_uv.y - 0.5) * m_pi2);
+
+    // limited cube_to_xyz
+    switch (f) {
+    case 0:
+        v = vec3(-1.0, uv.y, uv.x);
+        break;
+    case 1:
+        v = vec3(uv.x, uv.y, 1.0);
+        break;
+    case 2:
+        v = vec3(1.0, uv.y, -uv.x);
+        break;
+    case 3:
+        v = vec3(-uv.y, 1.0, -uv.x);
+        break;
+    case 4:
+        v = vec3(-uv.y, -uv.x, -1.0);
+        break;
+    case 5:
+        v = vec3(-uv.y, -1.0, uv.x);
+        break;
+    }
+
+    return normalize(v);
+}
+
+// face order : LEFT, FRONT, RIGHT, DOWN, BACK, UP
+vec3 xzy_to_eac_cube(in vec3 v)
+{
+    vec3 a = abs(v);
+    int face;
+    vec3 view;
+    vec2 uv;
+
+    if (a.x >= a.y && a.x >= a.z) {
+        if (v.x >= 0.0) { // +X
+            face = 2;
+            view = vec3(-v.z,  v.y, a.x);
+        }
+        else { // -X
+            face = 0;
+            view = vec3( v.z,  v.y, a.x);
+        }
+    }
+    else if (a.y >= a.z) {
+        if (v.y >= 0.0) { // +Y
+            face = 3;
+            view = vec3(-v.z, -v.x, a.y);
+        }
+        else { // -Y
+            face = 5;
+            view = vec3( v.z, -v.x, a.y);
+        }
+    }
+    else {
+        if (v.z >= 0.0) { // +Z
+            face = 1;
+            view = vec3( v.x,  v.y, a.z);
+        }
+        else { // -Z
+            face = 4;
+            view = vec3(-v.y, -v.x, a.z);
+        }
+    }
+    uv = atan2(view.xy, view.z) / m_pi2 + 0.5;
+
+    return vec3(uv.x, uv.y, float(face));
+}
+
+void xyz_to_gopro(uint idx, in vec3 v, in ivec2 pos, in ivec2 in_size)
+{
+    vec3 uvf = xzy_to_eac_cube(v);
+    vec2 uv = vec2(uvf.xy);
+    ivec2 grid = ivec2(int(uvf.z) % 3, int(uvf.z) / 3);
+    vec3 face_offset = vec3(gopro_face_offset_x) / float(in_img_size[0].x);
+    vec2 face_size = vec2(float(gopro_face_size_x[grid.x]) / float(in_img_size[0].x), 1.0);
+    vec2 uv_start = vec2(face_offset[grid.x], 0.0);
+    vec2 uv_end = uv_start + face_size;
+    vec4 res;
+
+    if (grid.x == 1) {
+        res = (grid.y == 0) ? texture(input_img[idx], mix(uv_start, uv_end, uv))
+                            : texture(input2_img[idx], mix(uv_start, uv_end, uv));
+    } else {
+        float cube_width = float(gopro_cube_width);
+        float overlap = float(gopro_overlap);
+        float gratio = (cube_width - overlap) / cube_width;
+        float blend_width = overlap / (cube_width - overlap);
+        vec3 blend_u = vec3(gopro_blend_x) / cube_width;
+        vec2 uv1 = vec2(uv.x * gratio, uv.y);
+        vec2 uv2 = vec2((uv.x + blend_width) * gratio, uv.y);
+        if (uv1.x < blend_u[0]) {
+            res = (grid.y == 0) ? texture(input_img[idx], mix(uv_start, uv_end, uv1))
+                                : texture(input2_img[idx], mix(uv_start, uv_end, uv1));
+        } else if (uv2.x > blend_u[2]) {
+            res = (grid.y == 0) ? texture(input_img[idx], mix(uv_start, uv_end, uv2))
+                                : texture(input2_img[idx], mix(uv_start, uv_end, uv2));
+        } else {
+            vec4 res2;
+            if (grid.y == 0) {
+                res = texture(input_img[idx], mix(uv_start, uv_end, uv1));
+                res2 = texture(input_img[idx], mix(uv_start, uv_end, uv2));
+            }
+            else {
+                res = texture(input2_img[idx], mix(uv_start, uv_end, uv1));
+                res2 = texture(input2_img[idx], mix(uv_start, uv_end, uv2));
+            }
+            float a = (uv1.x - blend_u[0]) / (blend_u[1] - blend_u[0]);
+            res = mix(res, res2, a);
+        }
+    }
+
+    imageStore(output_img[idx], pos, res);
+}
+
 void main()
 {
     ivec2 pos = ivec2(gl_GlobalInvocationID.xy);
@@ -179,6 +336,9 @@ void main()
         case EQUIRECTANGULAR:
 	        v = equirect_to_xyz(out_size, pos);
 	        break;
+        case EQUIANGULAR:
+	        v = eac_to_xyz(out_size, pos);
+	        break;
 	    case FLAT:
 	        v = flat_to_xyz(out_size, pos);
 	        break;
@@ -211,6 +371,9 @@ void main()
 	    case FISHEYE:
 	        xyz_to_fisheye(i, v, pos, in_size);
 	        break;
+	    case GOPROMAX:
+	        xyz_to_gopro(i, v, pos, in_size);
+	        break;
 	    }
     }
 }