PR #24363 opened by TADANO Tokumei (aimoff)
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24363
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24363.patch

# Summary of changes
This PR is continuation of PR #22725 and #21598.

This patch enhances v360_vulkan code:
- Default FOV parameters are determined for each projection.
- Calculate output size for specified projection with proper aspect retio.
- Resize output if `w` or `h` options are specified. (Current code ignores them)
- Reduce size of PushConstant and don't calculate it at every frame processing.
- Add GoPro Max projection for input.  
  Ref: https://gopro.com/news/max-tech-specs-stitching-resolution
- Add tentative Equi-Angular Cubemap (EAC) projection for output. (no padding)
- Add document.

Special notes exist for GoPro Max projection for input:
GoPro Max's .360 video file contains two streams.
PR #21598 treats them as dual input and use framesync to processing.
This code expects that the two streams are combined by `vstack` or similar 
filter as preprocessing.
Example:
```
ffmpeg -init_hw_device vulkan -i INPUT.360 -filter_complex 
"[0:0][0:5]vstack,format=yuv420p,hwupload, v360_vulkan=input=gopro:output=e, 
hwdownload,format=yuv420p" OUTPUT.mkv
```


>From 2666d50a315fdf831f31e5def697bda419ab4cca Mon Sep 17 00:00:00 2001
From: TADANO Tokumei <[email protected]>
Date: Sun, 23 Aug 2026 00:10:50 +0900
Subject: [PATCH 1/6] avfilter/v360_vulkan: referctoring with few fixes

Referctoring for later enhancements.
There are 2 fixes with the referctoring.
* Move PushData in context.
  Parameters are not recalculated in every v360_vulkan_filter_frame().
  Dynamically changed parameters are reflected by process_command().
* Avoid infinity on calculate_iflat_range() and calculate_flat_range().

Signed-off-by: TADANO Tokumei <[email protected]>
---
 libavfilter/vf_v360_vulkan.c | 274 +++++++++++++++++++----------------
 1 file changed, 153 insertions(+), 121 deletions(-)

diff --git a/libavfilter/vf_v360_vulkan.c b/libavfilter/vf_v360_vulkan.c
index a88fc6f0a6..d34a226dd8 100644
--- a/libavfilter/vf_v360_vulkan.c
+++ b/libavfilter/vf_v360_vulkan.c
@@ -26,6 +26,14 @@
 extern const unsigned char ff_v360_comp_spv_data[];
 extern const unsigned int ff_v360_comp_spv_len;
 
+/* Push constants */
+struct PushData {
+    float rot_mat[4][4];
+    int in_img_size[4][2];
+    float iflat_range[2];
+    float flat_range[2];
+};
+
 typedef struct V360ulkanContext {
     FFVulkanContext vkctx;
 
@@ -34,6 +42,7 @@ typedef struct V360ulkanContext {
     AVVulkanDeviceQueueFamily *qf;
     FFVulkanShader shd;
     VkSampler sampler;
+    struct PushData pd;
 
     /* Options */
     int   planewidth[4], planeheight[4];
@@ -47,14 +56,6 @@ typedef struct V360ulkanContext {
     int   rotation_order[3];
 } V360VulkanContext;
 
-/* Push constants */
-struct PushData {
-    float rot_mat[4][4];
-    int in_img_size[4][2];
-    float iflat_range[2];
-    float flat_range[2];
-};
-
 static int get_rorder(char c)
 {
     switch (c) {
@@ -72,12 +73,99 @@ static int get_rorder(char c)
     }
 }
 
-static av_cold int init_filter(AVFilterContext *ctx, AVFrame *in)
+static void multiply_matrix(float c[4][4], const float a[4][4], const float 
b[4][4])
+{
+    for (int i = 0; i < 3; i++) {
+        for (int j = 0; j < 3; j++) {
+            float sum = 0.0f;
+            for (int k = 0; k < 3; k++)
+                sum += a[i][k] * b[k][j];
+            c[i][j] = sum;
+        }
+    }
+}
+
+static inline void calculate_iflat_range(int in, float ih_fov, float iv_fov,
+                                         float *iflat_range)
+{
+    switch (in) {
+    case FLAT:
+        iflat_range[0] = tanf(0.5f * FFMIN(ih_fov, 179.f) * M_PI / 180.f);
+        iflat_range[1] = tanf(0.5f * FFMIN(iv_fov, 179.f) * M_PI / 180.f);
+        break;
+    case STEREOGRAPHIC:
+        iflat_range[0] = tanf(FFMIN(ih_fov, 359.f) * M_PI / 720.f);
+        iflat_range[1] = tanf(FFMIN(iv_fov, 359.f) * M_PI / 720.f);
+        break;
+    case DUAL_FISHEYE:
+    case FISHEYE:
+        iflat_range[0] = ih_fov / 180.f;
+        iflat_range[1] = iv_fov / 180.f;
+        break;
+    default:
+        break;
+    }
+}
+
+static inline void calculate_flat_range(int out, float h_fov, float v_fov,
+                                        float *flat_range)
+{
+    switch (out) {
+    case FLAT:
+        flat_range[0] = tanf(0.5f * FFMIN(h_fov, 179.f) * M_PI / 180.f);
+        flat_range[1] = tanf(0.5f * FFMIN(v_fov, 179.f) * M_PI / 180.f);
+        break;
+    case STEREOGRAPHIC:
+        flat_range[0] = tanf(FFMIN(h_fov, 359.f) * M_PI / 720.f);
+        flat_range[1] = tanf(FFMIN(v_fov, 359.f) * M_PI / 720.f);
+        break;
+    case DUAL_FISHEYE:
+    case FISHEYE:
+        flat_range[0] = h_fov / 180.f;
+        flat_range[1] = v_fov / 180.f;
+        break;
+    default:
+        break;
+    }
+}
+
+static inline void calculate_rotation_matrix(float yaw, float pitch, float 
roll,
+                                             float rot_mat[4][4],
+                                             const int rotation_order[3])
+{
+    const float yaw_rad   = yaw   * M_PI / 180.f;
+    const float pitch_rad = pitch * M_PI / 180.f;
+    const float roll_rad  = roll  * M_PI / 180.f;
+
+    const float sin_yaw   = sinf(yaw_rad);
+    const float cos_yaw   = cosf(yaw_rad);
+    const float sin_pitch = sinf(pitch_rad);
+    const float cos_pitch = cosf(pitch_rad);
+    const float sin_roll  = sinf(roll_rad);
+    const float cos_roll  = cosf(roll_rad);
+
+    float m[3][4][4];
+    float temp[4][4];
+
+    m[0][0][0] =  cos_yaw;  m[0][0][1] = 0;          m[0][0][2] =  sin_yaw;
+    m[0][1][0] =  0;        m[0][1][1] = 1;          m[0][1][2] =  0;
+    m[0][2][0] = -sin_yaw;  m[0][2][1] = 0;          m[0][2][2] =  cos_yaw;
+
+    m[1][0][0] = 1;         m[1][0][1] = 0;          m[1][0][2] =  0;
+    m[1][1][0] = 0;         m[1][1][1] = cos_pitch;  m[1][1][2] = -sin_pitch;
+    m[1][2][0] = 0;         m[1][2][1] = sin_pitch;  m[1][2][2] =  cos_pitch;
+
+    m[2][0][0] = cos_roll;  m[2][0][1] = -sin_roll;  m[2][0][2] =  0;
+    m[2][1][0] = sin_roll;  m[2][1][1] =  cos_roll;  m[2][1][2] =  0;
+    m[2][2][0] = 0;         m[2][2][1] =  0;         m[2][2][2] =  1;
+
+    multiply_matrix(temp, m[rotation_order[0]], m[rotation_order[1]]);
+    multiply_matrix(rot_mat, temp, m[rotation_order[2]]);
+}
+
+static void config_params(AVFilterContext *ctx)
 {
-    int err;
     V360VulkanContext *s = ctx->priv;
-    FFVulkanContext *vkctx = &s->vkctx;
-    const int planes = av_pix_fmt_count_planes(s->vkctx.output_format);
 
     for (int order = 0; order < NB_RORDERS; order++) {
         const char c = s->rorder[order];
@@ -107,6 +195,29 @@ static av_cold int init_filter(AVFilterContext *ctx, 
AVFrame *in)
         s->rotation_order[order] = rorder;
     }
 
+    calculate_iflat_range(s->in, s->ih_fov, s->iv_fov, s->pd.iflat_range);
+    calculate_flat_range(s->out, s->h_fov, s->v_fov, s->pd.flat_range);
+    calculate_rotation_matrix(s->yaw, s->pitch, s->roll,
+                              s->pd.rot_mat, s->rotation_order);
+
+    return;
+}
+
+static av_cold int init_filter(AVFilterContext *ctx, AVFrame *in)
+{
+    int err;
+    V360VulkanContext *s = ctx->priv;
+    FFVulkanContext *vkctx = &s->vkctx;
+    const AVPixFmtDescriptor *desc = 
av_pix_fmt_desc_get(s->vkctx.output_format);
+    const int planes = av_pix_fmt_count_planes(s->vkctx.output_format);
+
+    s->pd.in_img_size[0][0] = s->pd.in_img_size[3][0] = in->width;
+    s->pd.in_img_size[0][1] = s->pd.in_img_size[3][1] = in->height;
+    s->pd.in_img_size[1][0] = s->pd.in_img_size[2][0] =
+        FF_CEIL_RSHIFT(in->width, desc->log2_chroma_w);
+    s->pd.in_img_size[1][1] = s->pd.in_img_size[2][1] =
+        FF_CEIL_RSHIFT(in->height, desc->log2_chroma_h);
+
     RET(ff_vk_init_sampler(vkctx, &s->sampler, 0, VK_FILTER_LINEAR));
 
     s->qf = ff_vk_qf_find(vkctx, VK_QUEUE_COMPUTE_BIT, 0);
@@ -159,96 +270,6 @@ fail:
     return err;
 }
 
-static void multiply_matrix(float c[4][4], const float a[4][4], const float 
b[4][4])
-{
-    for (int i = 0; i < 3; i++) {
-        for (int j = 0; j < 3; j++) {
-            float sum = 0.0f;
-            for (int k = 0; k < 3; k++)
-                sum += a[i][k] * b[k][j];
-            c[i][j] = sum;
-        }
-    }
-}
-
-static inline void calculate_iflat_range(int in, float ih_fov, float iv_fov,
-                                         float *iflat_range)
-{
-    switch (in) {
-    case FLAT:
-        iflat_range[0] = tanf(0.5f * ih_fov * M_PI / 180.f);
-        iflat_range[1] = tanf(0.5f * iv_fov * M_PI / 180.f);
-        break;
-    case STEREOGRAPHIC:
-        iflat_range[0] = tanf(FFMIN(ih_fov, 359.f) * M_PI / 720.f);
-        iflat_range[1] = tanf(FFMIN(iv_fov, 359.f) * M_PI / 720.f);
-        break;
-    case DUAL_FISHEYE:
-    case FISHEYE:
-        iflat_range[0] = ih_fov / 180.f;
-        iflat_range[1] = iv_fov / 180.f;
-        break;
-    default:
-        break;
-    }
-}
-
-static inline void calculate_flat_range(int out, float h_fov, float v_fov,
-                                        float *flat_range)
-{
-    switch (out) {
-    case FLAT:
-        flat_range[0] = tanf(0.5f * h_fov * M_PI / 180.f);
-        flat_range[1] = tanf(0.5f * v_fov * M_PI / 180.f);
-        break;
-    case STEREOGRAPHIC:
-        flat_range[0] = tanf(FFMIN(h_fov, 359.f) * M_PI / 720.f);
-        flat_range[1] = tanf(FFMIN(v_fov, 359.f) * M_PI / 720.f);
-        break;
-    case DUAL_FISHEYE:
-    case FISHEYE:
-        flat_range[0] = h_fov / 180.f;
-        flat_range[1] = v_fov / 180.f;
-        break;
-    default:
-        break;
-    }
-}
-
-static inline void calculate_rotation_matrix(float yaw, float pitch, float 
roll,
-                                             float rot_mat[4][4],
-                                             const int rotation_order[3])
-{
-    const float yaw_rad   = yaw   * M_PI / 180.f;
-    const float pitch_rad = pitch * M_PI / 180.f;
-    const float roll_rad  = roll  * M_PI / 180.f;
-
-    const float sin_yaw   = sinf(yaw_rad);
-    const float cos_yaw   = cosf(yaw_rad);
-    const float sin_pitch = sinf(pitch_rad);
-    const float cos_pitch = cosf(pitch_rad);
-    const float sin_roll  = sinf(roll_rad);
-    const float cos_roll  = cosf(roll_rad);
-
-    float m[3][4][4];
-    float temp[4][4];
-
-    m[0][0][0] =  cos_yaw;  m[0][0][1] = 0;          m[0][0][2] =  sin_yaw;
-    m[0][1][0] =  0;        m[0][1][1] = 1;          m[0][1][2] =  0;
-    m[0][2][0] = -sin_yaw;  m[0][2][1] = 0;          m[0][2][2] =  cos_yaw;
-
-    m[1][0][0] = 1;         m[1][0][1] = 0;          m[1][0][2] =  0;
-    m[1][1][0] = 0;         m[1][1][1] = cos_pitch;  m[1][1][2] = -sin_pitch;
-    m[1][2][0] = 0;         m[1][2][1] = sin_pitch;  m[1][2][2] =  cos_pitch;
-
-    m[2][0][0] = cos_roll;  m[2][0][1] = -sin_roll;  m[2][0][2] =  0;
-    m[2][1][0] = sin_roll;  m[2][1][1] =  cos_roll;  m[2][1][2] =  0;
-    m[2][2][0] = 0;         m[2][2][1] =  0;         m[2][2][2] =  1;
-
-    multiply_matrix(temp, m[rotation_order[0]], m[rotation_order[1]]);
-    multiply_matrix(rot_mat, temp, m[rotation_order[2]]);
-}
-
 static int v360_vulkan_filter_frame(AVFilterLink *link, AVFrame *in)
 {
     int err;
@@ -266,25 +287,9 @@ static int v360_vulkan_filter_frame(AVFilterLink *link, 
AVFrame *in)
     if (!s->initialized)
         RET(init_filter(ctx, in));
 
-    /* Push constants */
-    struct PushData pd = { 0 };
-
-    const AVPixFmtDescriptor *desc = 
av_pix_fmt_desc_get(s->vkctx.input_format);
-    pd.in_img_size[0][0] = pd.in_img_size[3][0] = in->width;
-    pd.in_img_size[0][1] = pd.in_img_size[3][1] = in->height;
-    pd.in_img_size[1][0] = pd.in_img_size[2][0] =
-        FF_CEIL_RSHIFT(in->width, desc->log2_chroma_w);
-    pd.in_img_size[1][1] = pd.in_img_size[2][1] =
-        FF_CEIL_RSHIFT(in->height, desc->log2_chroma_h);
-
-    calculate_iflat_range(s->in, s->ih_fov, s->iv_fov, pd.iflat_range);
-    calculate_flat_range(s->out, s->h_fov, s->v_fov, pd.flat_range);
-    calculate_rotation_matrix(s->yaw, s->pitch, s->roll,
-                              pd.rot_mat, s->rotation_order);
-
     RET(ff_vk_filter_process_simple(&s->vkctx, &s->e, &s->shd,
                                     out, in, s->sampler, 1,
-                                    &pd, sizeof(pd)));
+                                    &s->pd, sizeof(s->pd)));
 
     err = av_frame_copy_props(out, in);
     if (err < 0)
@@ -300,6 +305,32 @@ fail:
     return err;
 }
 
+
+static int process_command(AVFilterContext *ctx, const char *cmd, const char 
*args,
+                           char *res, int res_len, int flags)
+{
+    int err;
+
+    RET(ff_filter_process_command(ctx, cmd, args, res, res_len, flags));
+    config_params(ctx);
+
+fail:
+    return err;
+}
+
+static av_cold int v360_vulkan_config_output(AVFilterLink *outlink)
+{
+    int err;
+    AVFilterContext *ctx = outlink->src;
+
+    config_params(ctx);
+
+    RET(ff_vk_filter_config_output(outlink));
+
+fail:
+    return err;
+}
+
 static void v360_vulkan_uninit(AVFilterContext *avctx)
 {
     V360VulkanContext *s = avctx->priv;
@@ -343,7 +374,7 @@ static const AVOption v360_vulkan_options[] = {
     {       "yaw", "yaw rotation",                       OFFSET(yaw), 
AV_OPT_TYPE_FLOAT,  {.dbl = 0.0f},     -180.f,               180.f, DYNAMIC, 
"yaw" },
     {     "pitch", "pitch rotation",                   OFFSET(pitch), 
AV_OPT_TYPE_FLOAT,  {.dbl = 0.0f},     -180.f,               180.f, DYNAMIC, 
"pitch" },
     {      "roll", "roll rotation",                     OFFSET(roll), 
AV_OPT_TYPE_FLOAT,  {.dbl = 0.0f},     -180.f,               180.f, DYNAMIC, 
"roll" },
-    {    "rorder", "rotation order",                  OFFSET(rorder), 
AV_OPT_TYPE_STRING, {.str = "ypr"},         0,                   0,   FLAGS, 
"rorder" },
+    {    "rorder", "rotation order",                  OFFSET(rorder), 
AV_OPT_TYPE_STRING, {.str = "ypr"},         0,                   0, DYNAMIC, 
"rorder" },
     {     "h_fov", "set output horizontal FOV angle",  OFFSET(h_fov), 
AV_OPT_TYPE_FLOAT,  {.dbl = 90.0f},  0.00001f,              360.0f, DYNAMIC, 
"h_fov" },
     {     "v_fov", "set output vertical FOV angle",    OFFSET(v_fov), 
AV_OPT_TYPE_FLOAT,  {.dbl = 45.0f},  0.00001f,              360.0f, DYNAMIC, 
"v_fov" },
     {    "ih_fov", "set input horizontal FOV angle",  OFFSET(ih_fov), 
AV_OPT_TYPE_FLOAT,  {.dbl = 90.0f},  0.00001f,              360.0f, DYNAMIC, 
"ih_fov" },
@@ -367,7 +398,7 @@ static const AVFilterPad v360_vulkan_outputs[] = {
     {
         .name = "default",
         .type = AVMEDIA_TYPE_VIDEO,
-        .config_props = &ff_vk_filter_config_output,
+        .config_props = &v360_vulkan_config_output,
     },
 };
 
@@ -383,4 +414,5 @@ const FFFilter ff_vf_v360_vulkan = {
     FILTER_OUTPUTS(v360_vulkan_outputs),
     FILTER_SINGLE_PIXFMT(AV_PIX_FMT_VULKAN),
     .flags_internal = FF_FILTER_FLAG_HWFRAME_AWARE,
+    .process_command = &process_command,
 };
-- 
2.52.0


>From 1dffe79d3388bec910d87014cc27eda5f3716554 Mon Sep 17 00:00:00 2001
From: TADANO Tokumei <[email protected]>
Date: Sun, 23 Aug 2026 01:23:56 +0900
Subject: [PATCH 2/6] avfilter/v360_vulkan: adjust fov parameters

The default values of former code for h_fov, v_fov, ih_fov, iv_fov
are constantly defined for any projection.
This fix treats the default values as similar to v360 filter.
It calculate the default values as suitable for each projection.

Signed-off-by: TADANO Tokumei <[email protected]>
---
 libavfilter/vf_v360_vulkan.c | 110 ++++++++++++++++++++++++++++++++---
 1 file changed, 103 insertions(+), 7 deletions(-)

diff --git a/libavfilter/vf_v360_vulkan.c b/libavfilter/vf_v360_vulkan.c
index d34a226dd8..956371cc89 100644
--- a/libavfilter/vf_v360_vulkan.c
+++ b/libavfilter/vf_v360_vulkan.c
@@ -85,6 +85,9 @@ static void multiply_matrix(float c[4][4], const float 
a[4][4], const float b[4]
     }
 }
 
+#define degree2radian(degree) ((degree) * M_PI / 180.f)
+#define radian2degree(radian) ((radian) * 180.f / M_PI)
+
 static inline void calculate_iflat_range(int in, float ih_fov, float iv_fov,
                                          float *iflat_range)
 {
@@ -163,10 +166,101 @@ static inline void calculate_rotation_matrix(float yaw, 
float pitch, float roll,
     multiply_matrix(rot_mat, temp, m[rotation_order[2]]);
 }
 
-static void config_params(AVFilterContext *ctx)
+static void config_params(AVFilterContext *ctx, AVFilterLink *inlink)
 {
     V360VulkanContext *s = ctx->priv;
 
+    switch (s->in) {
+    case FLAT:
+        float sar = inlink->sample_aspect_ratio.num ?
+                    (float) inlink->sample_aspect_ratio.num / 
inlink->sample_aspect_ratio.den : 1;
+        if (s->ih_fov == 0.f && s->iv_fov == 0.f) {
+            s->ih_fov = 90.f;
+            s->iv_fov = radian2degree(2.f * atanf((float)inlink->h / sar / 
inlink->w));
+        }
+        else if (s->ih_fov == 0.f || s->ih_fov >= 180.f) {
+            if (s->iv_fov >= 180.f)
+                s->ih_fov = s->iv_fov = 180.f;
+            else
+                s->iv_fov = radian2degree(2.f * atanf((float)inlink->h / sar / 
inlink->w * tanf(degree2radian(s->ih_fov) / 2.f)));
+        }
+        else if (s->iv_fov == 0.f || s->iv_fov >= 180.f) {
+            if (s->ih_fov >= 180.f)
+                s->ih_fov = s->iv_fov = 180.f;
+            else
+                s->ih_fov = radian2degree(2.f * atanf((float)inlink->w * sar / 
inlink->h * tanf(degree2radian(s->iv_fov) / 2.f)));
+        }
+        break;
+    case STEREOGRAPHIC:
+    case DUAL_FISHEYE:
+    case FISHEYE:
+        if (s->ih_fov == 0.f)
+            s->ih_fov = 180.f;
+        if (s->iv_fov == 0.f)
+            s->iv_fov = 180.f;
+        break;
+    case EQUIRECTANGULAR: /* unchangeable */
+        s->ih_fov = 360.f;
+        s->iv_fov = 180.f;
+        break;
+    default:
+        if (s->ih_fov == 0.f)
+            s->ih_fov = 360.f;
+        if (s->iv_fov == 0.f)
+            s->iv_fov = 180.f;
+        break;
+    }
+
+    switch (s->out) {
+    case FLAT:
+        if (s->width > 0 && s->height > 0 &&
+            (s->h_fov == 0.f || s->h_fov >= 180.f || s->v_fov == 0.f || 
s->v_fov >= 180.f)) {
+            if (s->h_fov == 0.f && s->v_fov == 0.f) {
+                s->h_fov = 90.f;
+                s->v_fov = radian2degree(2.f * atanf((float)s->height / 
s->width));
+            }
+            else if (s->h_fov == 0.f || s->h_fov >= 180.f) {
+                if (s->v_fov >= 180.f)
+                    s->h_fov = s->v_fov = 180.f;
+                else
+                    s->v_fov = radian2degree(2.f * atanf((float)s->height / 
s->width * tanf(degree2radian(s->h_fov) / 2.f)));
+            }
+            else if (s->v_fov == 0.f || s->v_fov >= 180.f) {
+                if (s->h_fov >= 180.f)
+                    s->h_fov = s->v_fov = 180.f;
+                else
+                    s->h_fov = radian2degree(2.f * atanf((float)s->width / 
s->height * tanf(degree2radian(s->v_fov) / 2.f)));
+            }
+        }
+        else {
+            if (s->h_fov >= 180.f || s->v_fov >= 180.f) {
+                s->h_fov = 180.f;
+                s->v_fov = 180.f;
+            }
+            else {
+                if (s->h_fov == 0.f)
+                    s->h_fov = 90.f;
+                if (s->v_fov == 0.f)
+                    s->v_fov = 45.f;
+            }
+        }
+        break;
+    case STEREOGRAPHIC:
+    case DUAL_FISHEYE:
+    case FISHEYE:
+        if (s->h_fov == 0.f)
+            s->h_fov = 180.f;
+        if (s->v_fov == 0.f)
+            s->v_fov = 180.f;
+        break;
+    default:
+        if (s->h_fov == 0.f)
+            s->h_fov = 360.f;
+        if (s->v_fov == 0.f)
+            s->v_fov = 180.f;
+        break;
+    }
+
     for (int order = 0; order < NB_RORDERS; order++) {
         const char c = s->rorder[order];
         int rorder;
@@ -310,9 +404,10 @@ static int process_command(AVFilterContext *ctx, const 
char *cmd, const char *ar
                            char *res, int res_len, int flags)
 {
     int err;
+    AVFilterLink *inlink = ctx->inputs[0];
 
     RET(ff_filter_process_command(ctx, cmd, args, res, res_len, flags));
-    config_params(ctx);
+    config_params(ctx, inlink);
 
 fail:
     return err;
@@ -322,8 +417,9 @@ static av_cold int v360_vulkan_config_output(AVFilterLink 
*outlink)
 {
     int err;
     AVFilterContext *ctx = outlink->src;
+    AVFilterLink *inlink = ctx->inputs[0];
 
-    config_params(ctx);
+    config_params(ctx, inlink);
 
     RET(ff_vk_filter_config_output(outlink));
 
@@ -375,10 +471,10 @@ static const AVOption v360_vulkan_options[] = {
     {     "pitch", "pitch rotation",                   OFFSET(pitch), 
AV_OPT_TYPE_FLOAT,  {.dbl = 0.0f},     -180.f,               180.f, DYNAMIC, 
"pitch" },
     {      "roll", "roll rotation",                     OFFSET(roll), 
AV_OPT_TYPE_FLOAT,  {.dbl = 0.0f},     -180.f,               180.f, DYNAMIC, 
"roll" },
     {    "rorder", "rotation order",                  OFFSET(rorder), 
AV_OPT_TYPE_STRING, {.str = "ypr"},         0,                   0, DYNAMIC, 
"rorder" },
-    {     "h_fov", "set output horizontal FOV angle",  OFFSET(h_fov), 
AV_OPT_TYPE_FLOAT,  {.dbl = 90.0f},  0.00001f,              360.0f, DYNAMIC, 
"h_fov" },
-    {     "v_fov", "set output vertical FOV angle",    OFFSET(v_fov), 
AV_OPT_TYPE_FLOAT,  {.dbl = 45.0f},  0.00001f,              360.0f, DYNAMIC, 
"v_fov" },
-    {    "ih_fov", "set input horizontal FOV angle",  OFFSET(ih_fov), 
AV_OPT_TYPE_FLOAT,  {.dbl = 90.0f},  0.00001f,              360.0f, DYNAMIC, 
"ih_fov" },
-    {    "iv_fov", "set input vertical FOV angle",    OFFSET(iv_fov), 
AV_OPT_TYPE_FLOAT,  {.dbl = 45.0f},  0.00001f,              360.0f, DYNAMIC, 
"iv_fov" },
+    {     "h_fov", "set output horizontal FOV angle",  OFFSET(h_fov), 
AV_OPT_TYPE_FLOAT,  {.dbl = 0.0f},       0.0f,              360.0f, DYNAMIC, 
"h_fov" },
+    {     "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" },
 
     { NULL },
 };
-- 
2.52.0


>From f73d39c67ece49e995d9a2471da11259079e428b Mon Sep 17 00:00:00 2001
From: TADANO Tokumei <[email protected]>
Date: Sun, 23 Aug 2026 03:34:47 +0900
Subject: [PATCH 3/6] avfilter/v360_vulkan: calculate output size

The former code ignores w and h options, and doesn't resize.
This fix add resizing feature with proper aspect ratio.

Signed-off-by: TADANO Tokumei <[email protected]>
---
 libavfilter/vf_v360_vulkan.c | 123 +++++++++++++++++++++++++++++++++++
 1 file changed, 123 insertions(+)

diff --git a/libavfilter/vf_v360_vulkan.c b/libavfilter/vf_v360_vulkan.c
index 956371cc89..3357a7be45 100644
--- a/libavfilter/vf_v360_vulkan.c
+++ b/libavfilter/vf_v360_vulkan.c
@@ -297,6 +297,128 @@ static void config_params(AVFilterContext *ctx, 
AVFilterLink *inlink)
     return;
 }
 
+static av_cold int calculate_output_size(AVFilterContext *ctx)
+{
+    V360VulkanContext   *s = ctx->priv;
+    FFVulkanContext *vkctx = &s->vkctx;
+    AVFilterLink   *inlink = ctx->inputs[0];
+    const AVPixFmtDescriptor *desc = 
av_pix_fmt_desc_get(s->vkctx.output_format);
+    float sar = inlink->sample_aspect_ratio.num ?
+                (float) inlink->sample_aspect_ratio.num / 
inlink->sample_aspect_ratio.den : 1;
+    float  wf = (float) s->width;
+    float  hf = (float) s->height;
+    int min_w, min_h, pw, ph;
+
+    if (s->width > 0 && s->height > 0) {
+        if (sar == 1)
+            vkctx->output_width = s->width;
+        else {
+            vkctx->output_width = lrint(wf / sar);
+            if (vkctx->output_width % 2 != 0)
+                vkctx->output_width++;
+        }
+        vkctx->output_height = s->height;
+    }
+    else if (s->width > 0 && s->height == 0) {
+        if (sar == 1)
+            vkctx->output_width = s->width;
+        else {
+            vkctx->output_width = lrint(wf / sar);
+            if (vkctx->output_width % 2 != 0)
+                vkctx->output_width++;
+        }
+        switch (s->out) {
+        case FLAT:
+            hf = wf * s->pd.iflat_range[1] * 2.f;
+            break;
+        case EQUIRECTANGULAR:
+        case DUAL_FISHEYE:
+            hf = wf * sar / 2.f;
+            break;
+        case STEREOGRAPHIC:
+        case FISHEYE:
+            hf = wf * sar;
+            break;
+        default:
+            break;
+        }
+        vkctx->output_height = lrint(hf);
+        if (vkctx->output_height % 2 != 0)
+            vkctx->output_height++;
+    }
+    else { /* s->width == 0 */
+        vkctx->output_height = s->height;
+        if (s->height == 0) {
+            switch (s->in) {
+            case FLAT:
+                 hf = (float)inlink->h / s->pd.iflat_range[1] / 2.f;
+                break;
+            default:
+                hf = (float)inlink->h;
+                break;
+            }
+            switch (s->out) {
+            case FLAT:
+                hf = (float)inlink->h * s->pd.flat_range[1] * 2.f;
+                break;
+            default:
+                break;
+            }
+            vkctx->output_height = lrint(hf);
+            if (vkctx->output_height % 2 != 0)
+                vkctx->output_height++;
+        }
+        switch (s->out) {
+        case FLAT:
+            wf = hf * s->pd.flat_range[0] * 2.f;
+            break;
+        case EQUIRECTANGULAR:
+        case DUAL_FISHEYE:
+            wf = hf * 2.f;
+            break;
+        case STEREOGRAPHIC:
+        case FISHEYE:
+            wf = hf;
+            break;
+        default:
+            break;
+        }
+        vkctx->output_width = lrint(wf / sar);
+        if (vkctx->output_width % 2 != 0)
+            vkctx->output_width++;
+    }
+
+    if (vkctx->output_width < 1  || vkctx->output_width > INT16_MAX ||
+        vkctx->output_height < 1 || vkctx->output_height > INT16_MAX) {
+        av_log(ctx, AV_LOG_ERROR,
+               "Output dimensions %dx%d are outside the allowed range [1, 
%d].\n",
+               vkctx->output_height, vkctx->output_height, INT16_MAX);
+        return AVERROR(EINVAL);
+    }
+
+    pw = AV_CEIL_RSHIFT(vkctx->output_width, desc->log2_chroma_w);
+    ph = AV_CEIL_RSHIFT(vkctx->output_height, desc->log2_chroma_h);
+    switch (s->out) {
+    case EQUIRECTANGULAR:
+    case DUAL_FISHEYE:
+        min_w = 2;
+        min_h = 1;
+        break;
+    default:
+        min_w = 1;
+        min_h = 1;
+        break;
+    }
+    if (pw < min_w || ph < min_h) {
+        av_log(ctx, AV_LOG_ERROR,
+               "Output %dx%d is too small for the output projection "
+               "(requires at least %dx%d per plane).\n", pw, ph, min_w, min_h);
+        return AVERROR(EINVAL);
+    }
+
+    return 0;
+}
+
 static av_cold int init_filter(AVFilterContext *ctx, AVFrame *in)
 {
     int err;
@@ -420,6 +542,7 @@ static av_cold int v360_vulkan_config_output(AVFilterLink 
*outlink)
     AVFilterLink *inlink = ctx->inputs[0];
 
     config_params(ctx, inlink);
+    RET(calculate_output_size(ctx));
 
     RET(ff_vk_filter_config_output(outlink));
 
-- 
2.52.0


>From 5a3f61527ec013db2f71aa4358e92e8a48cdaab5 Mon Sep 17 00:00:00 2001
From: TADANO Tokumei <[email protected]>
Date: Sun, 23 Aug 2026 21:59:05 +0900
Subject: [PATCH 4/6] avfilter/v360_vulkan: reduce size of Push Constants

Remove `in_img_size[][]` from PushData.
Even though it is constant through a filter process, it occupies
128 bites of Push Constants. It may exceed limit of some hardware.
The information is passed via Specialization Constants.

In addition to `in_img_size`, number of planes is passed via
Specialization Constants, that was hard-coded with magic number.
`m_pi4` is for later enhancement.

Signed-off-by: TADANO Tokumei <[email protected]>
---
 libavfilter/vf_v360_vulkan.c      | 21 ++++++++++-----------
 libavfilter/vulkan/v360.comp.glsl | 17 +++++++++++++++--
 2 files changed, 25 insertions(+), 13 deletions(-)

diff --git a/libavfilter/vf_v360_vulkan.c b/libavfilter/vf_v360_vulkan.c
index 3357a7be45..ef3b58228f 100644
--- a/libavfilter/vf_v360_vulkan.c
+++ b/libavfilter/vf_v360_vulkan.c
@@ -29,7 +29,6 @@ extern const unsigned int ff_v360_comp_spv_len;
 /* Push constants */
 struct PushData {
     float rot_mat[4][4];
-    int in_img_size[4][2];
     float iflat_range[2];
     float flat_range[2];
 };
@@ -427,13 +426,6 @@ static av_cold int init_filter(AVFilterContext *ctx, 
AVFrame *in)
     const AVPixFmtDescriptor *desc = 
av_pix_fmt_desc_get(s->vkctx.output_format);
     const int planes = av_pix_fmt_count_planes(s->vkctx.output_format);
 
-    s->pd.in_img_size[0][0] = s->pd.in_img_size[3][0] = in->width;
-    s->pd.in_img_size[0][1] = s->pd.in_img_size[3][1] = in->height;
-    s->pd.in_img_size[1][0] = s->pd.in_img_size[2][0] =
-        FF_CEIL_RSHIFT(in->width, desc->log2_chroma_w);
-    s->pd.in_img_size[1][1] = s->pd.in_img_size[2][1] =
-        FF_CEIL_RSHIFT(in->height, desc->log2_chroma_h);
-
     RET(ff_vk_init_sampler(vkctx, &s->sampler, 0, VK_FILTER_LINEAR));
 
     s->qf = ff_vk_qf_find(vkctx, VK_QUEUE_COMPUTE_BIT, 0);
@@ -443,15 +435,22 @@ static av_cold int init_filter(AVFilterContext *ctx, 
AVFrame *in)
         goto fail;
     }
 
-    RET(ff_vk_exec_pool_init(vkctx, s->qf, &s->e, 2, 0, 0, 0, NULL));
+    RET(ff_vk_exec_pool_init(vkctx, s->qf, &s->e, s->qf->num*4, 0, 0, 0, 
NULL));
 
-    SPEC_LIST_CREATE(sl, 4, 2*sizeof(int) + 2*sizeof(float))
+    SPEC_LIST_CREATE(sl, 10, 7*sizeof(int) + 3*sizeof(float))
     SPEC_LIST_ADD(sl, 0, 32, s->out);
     SPEC_LIST_ADD(sl, 1, 32, s->in);
 
-    const float m_pi = M_PI, m_pi2 = M_PI_2;
+    const float m_pi = M_PI, m_pi2 = M_PI_2, m_pi4 = M_PI_4;
     SPEC_LIST_ADD(sl, 2, 32, av_float2int(m_pi));
     SPEC_LIST_ADD(sl, 3, 32, av_float2int(m_pi2));
+    SPEC_LIST_ADD(sl, 4, 32, av_float2int(m_pi4));
+
+    SPEC_LIST_ADD(sl, 5, 32, planes);
+    SPEC_LIST_ADD(sl, 6, 32, in->width);
+    SPEC_LIST_ADD(sl, 7, 32, in->height);
+    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));
 
     ff_vk_shader_load(&s->shd, VK_SHADER_STAGE_COMPUTE_BIT,
                       sl, (uint32_t []) { 16, 16, 1 }, 0);
diff --git a/libavfilter/vulkan/v360.comp.glsl 
b/libavfilter/vulkan/v360.comp.glsl
index 74b83b232f..131b10e3b2 100644
--- a/libavfilter/vulkan/v360.comp.glsl
+++ b/libavfilter/vulkan/v360.comp.glsl
@@ -34,17 +34,30 @@ layout (constant_id = 1) const int in_transform = 0;
 
 layout (constant_id = 2) const float m_pi = 0;
 layout (constant_id = 3) const float m_pi2 = 0;
+layout (constant_id = 4) const float m_pi4 = 0;
+
+layout (constant_id = 5) const int planes = 0;
+layout (constant_id = 6) const int in_w = 0;
+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 (push_constant, scalar) uniform pushConstants {
     mat4 rot_mat;
-    ivec2 in_img_size[4];
     vec2 iflat_range;
     vec2 flat_range;
 };
 
+ivec2 in_img_size[4] = {
+    ivec2(in_w, in_h),
+    ivec2(in_chroma_w, in_chroma_h),
+    ivec2(in_chroma_w, in_chroma_h),
+    ivec2(in_w, in_h)
+};
+
 #define IS_WITHIN(v1, v2) any(lessThan(v1, v2))
 
 void xyz_to_flat(uint idx, in vec3 v, in ivec2 pos, in ivec2 in_size)
@@ -157,7 +170,7 @@ void main()
 {
     ivec2 pos = ivec2(gl_GlobalInvocationID.xy);
 
-    for (int i = 0; i < 3; i++) {
+    for (int i = 0; i < planes; i++) {
         ivec2 in_size = in_img_size[i];
         ivec2 out_size = imageSize(output_img[i]);
 
-- 
2.52.0


>From 664f9d395cea5ba3ee204c98ca5d641bff7cc1af Mon Sep 17 00:00:00 2001
From: TADANO Tokumei <[email protected]>
Date: Thu, 3 Sep 2026 01:29:50 +0900
Subject: [PATCH 5/6] 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 <[email protected]>
---
 libavfilter/v360.h                |   1 +
 libavfilter/vf_v360_vulkan.c      |  35 ++++++-
 libavfilter/vulkan/v360.comp.glsl | 148 ++++++++++++++++++++++++++++++
 3 files changed, 183 insertions(+), 1 deletion(-)

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 ef3b58228f..f482cfcd9b 100644
--- a/libavfilter/vf_v360_vulkan.c
+++ b/libavfilter/vf_v360_vulkan.c
@@ -53,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)
@@ -199,6 +200,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;
@@ -334,6 +336,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;
@@ -375,6 +380,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;
@@ -403,6 +411,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;
@@ -415,6 +427,15 @@ static av_cold int calculate_output_size(AVFilterContext 
*ctx)
         return AVERROR(EINVAL);
     }
 
+    if (s->in == GOPROMAX && s->overlap == 0) {
+        if (inlink->h <= 1920)
+            s->overlap = 32;
+        else if (inlink->h < 3840)
+            s->overlap = 64;
+        else
+            s->overlap = 96;
+    }
+
     return 0;
 }
 
@@ -437,7 +458,7 @@ static av_cold int init_filter(AVFilterContext *ctx, 
AVFrame *in)
 
     RET(ff_vk_exec_pool_init(vkctx, s->qf, &s->e, s->qf->num*4, 0, 0, 0, 
NULL));
 
-    SPEC_LIST_CREATE(sl, 10, 7*sizeof(int) + 3*sizeof(float))
+    SPEC_LIST_CREATE(sl, 13, 10*sizeof(int) + 3*sizeof(float))
     SPEC_LIST_ADD(sl, 0, 32, s->out);
     SPEC_LIST_ADD(sl, 1, 32, s->in);
 
@@ -452,6 +473,15 @@ 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 / 2;
+        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);
+    }
+
     ff_vk_shader_load(&s->shd, VK_SHADER_STAGE_COMPUTE_BIT,
                       sl, (uint32_t []) { 16, 16, 1 }, 0);
 
@@ -578,10 +608,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" },
+    {     "gopro", "gopro max",                                    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" },
@@ -597,6 +629,7 @@ 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 pixcels for GoPro Max", OFFSET(overlap), 
AV_OPT_TYPE_INT,    {.i64 = 0},            0,                1024,   FLAGS, 
"overlap" },
 
     { NULL },
 };
diff --git a/libavfilter/vulkan/v360.comp.glsl 
b/libavfilter/vulkan/v360.comp.glsl
index 131b10e3b2..09e099d07d 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,6 +44,10 @@ 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 (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 (set = 0, binding = 0) uniform sampler2D input_img[];
 layout (set = 0, binding = 1) uniform writeonly image2D output_img[];
 
@@ -58,8 +64,27 @@ ivec2 in_img_size[4] = {
     ivec2(in_w, in_h)
 };
 
+ivec3 gopro_face_offset_x = ivec3(0,
+                                  gopro_cube_width,
+                                  gopro_cube_width + in_cube_size);
+ivec3 gopro_face_size_x = ivec3(gopro_cube_width,
+                                in_cube_size,
+                                gopro_cube_width);
+ivec3 gopro_blend_x = ivec3(gopro_cube_width / 2 - gopro_overlap,
+                            gopro_cube_width / 2,
+                            gopro_cube_width / 2 + gopro_overlap);
+
 #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 +191,123 @@ 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), 0.5);
+    vec2 uv_start = vec2(face_offset[grid.x], float(grid.y) * face_size.y);
+    vec2 uv_end = uv_start + face_size;
+    vec4 res;
+
+    if (grid.x == 1)
+        res = texture(input_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 = texture(input_img[idx], mix(uv_start, uv_end, uv1));
+        else if (uv2.x > blend_u[2])
+            res = texture(input_img[idx], mix(uv_start, uv_end, uv2));
+        else {
+            res = texture(input_img[idx], mix(uv_start, uv_end, uv1));
+            vec4 res2 = texture(input_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 +321,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 +356,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;
            }
     }
 }
-- 
2.52.0


>From ee621a4053598eac8e886f793af42409c3ce166e Mon Sep 17 00:00:00 2001
From: TADANO Tokumei <[email protected]>
Date: Thu, 3 Sep 2026 19:21:18 +0900
Subject: [PATCH 6/6] doc/filters: add v360_vulkan filter document

Signed-off-by: TADANO Tokumei <[email protected]>
---
 doc/filters.texi | 108 +++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 108 insertions(+)

diff --git a/doc/filters.texi b/doc/filters.texi
index b7379d3060..8429db40b3 100644
--- a/doc/filters.texi
+++ b/doc/filters.texi
@@ -24941,6 +24941,7 @@ from the video stream (if available).
 Use specified codec instead of snow.
 @end table
 
+@anchor{v360}
 @section v360
 
 Convert 360 videos between various formats.
@@ -29846,6 +29847,113 @@ Preserve landscape geometry (when @var{width} >= 
@var{height}).
 
 @end table
 
+@section v360_vulkan
+
+Convert 360 videos between various formats.
+Supported formats (projections) are subset of @ref{v360} filter,
+that are most popular 360-degree video formats.
+
+The filter accepts subset of @ref{v360} options:
+
+@table @option
+
+@item input
+@item output
+Set format of the input/output video.
+
+@table @samp
+
+@item e
+@item equirect
+Equirectangular projection.
+
+@item eac
+Equi-Angular Cubemap. (Output only)
+
+@item flat
+Regular video.
+
+@item dfisheye
+Dual fisheye.
+
+@item sg
+Stereographic format.
+
+@item fisheye
+Fisheye projection.
+
+@item gopro
+GoPro Max .360 video format. (Input only)
+
+The .360 video file contains two video streams.
+It is required to combine the two streams to single stream by another filter
+like @ref{vstack} before processing this filter.
+
+Format specific option:
+
+@table @option
+@item overlap
+Set number of overlapped pixels on input .360 video.
+
+The GoPro Max.360 video contains overlapped area.
+(See @url{https://gopro.com/news/max-tech-specs-stitching-resolution})
+The filter blends overlapped images in these two areas.
+
+The number of pixels of overlapped ares is depending on video resolutions.
+Most of cases, the value is automatically calculated based on input video size.
+But it may be changed on GoPro Max2 or future models.
+The option is to adjust for unknown video resolutions.
+@end table
+
+@end table
+
+@item w
+@item h
+Set the output video resolution.
+See @ref{v360} options.
+
+@item yaw
+@item pitch
+@item roll
+@item rorder
+Set rotation for the output video.
+See @ref{v360} options.
+
+@item h_fov
+@item v_fov
+Set output horizontal/vertical field of view.
+See @ref{v360} options.
+
+@item ih_fov
+@item iv_fov
+Set input horizontal/vertical field of view.
+See @ref{v360} options.
+
+@end table
+
+@subsection Examples
+Special notes exist for @code{gopro} input.
+
+Following examples specify @ref{vstack} filter to combine GoPro Max's two 
video streams to single stream.
+The stream number (e.g., @code{[0:5]}) depends on the model and shooting mode.
+Please check actual stream number with @code{ffprobe} command.
+
+@itemize
+@item
+Convert GoPro Max (8bit color depth) .360 to Equirectangular projection.
+GoPro Max uses obsolete yuvj420p pixel format, thus convert the format to 
yuv420p before this filter.
+@example
+ffmpeg -init_hw_device vulkan -i INPUT.360 -filter_complex 
"[0:0][0:5]vstack,format=yuv420p,hwupload, v360_vulkan=input=gopro:output=e, 
hwdownload,format=yuv420p" OUTPUT.mkv
+@end example
+
+@item
+Convert GoPro Max2 (10bit color depth) .360 to Dual fisheye projection.
+GoPro Max2 uses yuv420p10le pixel format.
+@example
+ffmpeg  -init_hw_device vulkan -i INPUT.360 -filter_complex 
"[0:0][0:4]vstack,hwupload, v360_vulkan=input=gopro:output=dfisheye, 
hwdownload,format=yuv420p10le" OUTPUT.mkv
+@end example
+@end itemize
+
 @c man end VULKAN VIDEO FILTERS
 
 @chapter QSV Video Filters
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to