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]
