diff --git a/libavfilter/Makefile b/libavfilter/Makefile
index d4e38bd4e8..2ae174a381 100644
--- a/libavfilter/Makefile
+++ b/libavfilter/Makefile
@@ -626,7 +626,8 @@ SKIPHEADERS-$(CONFIG_OPENCL)                 += opencl.h
 SKIPHEADERS-$(CONFIG_VAAPI)                  += vaapi_vpp.h
 SKIPHEADERS-$(CONFIG_VULKAN)                 += vulkan.h vulkan_filter.h
 
-TOOLS     = graph2dot
+TOOLS     = graph2dot                                                   \
+            spherical_compare
 TESTPROGS = drawutils filtfmts formats integral
 TESTPROGS-$(CONFIG_DNN) += dnn-layer-avgpool dnn-layer-conv2d dnn-layer-dense  \
                            dnn-layer-depth2space dnn-layer-mathbinary          \
diff --git a/tests/Makefile b/tests/Makefile
index 1d50e1d175..078d8aa3ce 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -259,7 +259,8 @@ FATE_EXTERN-$(CONFIG_FFMPEG)  += $(FATE_SAMPLES_AVCONV) $(FATE_SAMPLES_FFMPEG)
 FATE_EXTERN-$(CONFIG_FFPROBE) += $(FATE_SAMPLES_FFPROBE)
 FATE_SAMPLES_FFMPEG_FFPROBE   += $(FATE_SAMPLES_FFMPEG_FFPROBE-yes)
 FATE_EXTERN-$(call ALLYES, FFMPEG FFPROBE) += $(FATE_SAMPLES_FFMPEG_FFPROBE)
-FATE_EXTERN                   += $(FATE_EXTERN-yes) $(FATE_SAMPLES_FASTSTART)
+FATE_EXTERN                   += $(FATE_EXTERN-yes) $(FATE_SAMPLES_FASTSTART) \
+                                 $(FATE_SAMPLES_SPHERICAL_COMPARE)
 
 FATE += $(FATE-yes)
 
@@ -273,6 +274,7 @@ $(FATE_FFPROBE) $(FATE_FFMPEG_FFPROBE) $(FATE_SAMPLES_FFPROBE) $(FATE_SAMPLES_FF
 $(FATE_SAMPLES_FASTSTART): tools/qt-faststart$(EXESUF)
 $(FATE_SAMPLES_DUMP_DATA) $(FATE_SAMPLES_DUMP_DATA-yes): tools/venc_data_dump$(EXESUF)
 $(FATE_SAMPLES_SCALE_SLICE): tools/scale_slice_test$(EXESUF)
+$(FATE_SAMPLES_SPHERICAL_COMPARE): tools/spherical_compare$(EXESUF)
 
 ifdef SAMPLES
 FATE += $(FATE_EXTERN)
diff --git a/tests/fate-run.sh b/tests/fate-run.sh
index 61cc59acc0..41e6da5c5d 100755
--- a/tests/fate-run.sh
+++ b/tests/fate-run.sh
@@ -572,6 +572,12 @@ venc_data(){
     run tools/venc_data_dump${EXECSUF} ${file} ${stream} ${frames} ${threads} ${thread_type}
 }
 
+spherical_compare(){
+    file=$1
+    filterchain=$2
+    run tools/spherical_compare${EXECSUF} ${file} ${filterchain}
+}
+
 null(){
     :
 }
diff --git a/tests/fate/filter-video.mak b/tests/fate/filter-video.mak
index 63873a7a07..731d686cee 100644
--- a/tests/fate/filter-video.mak
+++ b/tests/fate/filter-video.mak
@@ -698,6 +698,25 @@ fate-filter-refcmp-blockdetect-yuv: CMD = cmp_metadata blockdetect yuv420p 0.015
 FATE_FILTER_CMP_METADATA-$(CONFIG_BLURDETECT_FILTER) += fate-filter-refcmp-blurdetect-yuv
 fate-filter-refcmp-blurdetect-yuv: CMD = cmp_metadata blurdetect yuv420p 0.015
 
+SPHERICAL_DEPS = MATROSKA_DEMUXER VP8_DECODER V360_FILTER SSIM360_FILTER
+SPHERICAL_SAMPLE = $(TARGET_SAMPLES)/spherical/Worlds_First_Live_360_Rocket_Launch-_Orbital_ATK_CRS-7_cut.mkv
+
+FATE_FILTER_SPHERICAL += fate-filter-spherical-c3x2
+fate-filter-spherical-c3x2: CMD = spherical_compare $(SPHERICAL_SAMPLE) \
+	"split[in][ref];[in]v360=input=e:output=c3x2[main];[main][ref]ssim360=main_projection=c3x2:ref_projection=e"
+
+FATE_FILTER_SPHERICAL += fate-filter-spherical-barrel
+fate-filter-spherical-barrel: CMD = spherical_compare $(SPHERICAL_SAMPLE) \
+	"split[in][ref];[in]v360=input=e:output=barrel[main];[main][ref]ssim360=main_projection=barrel:ref_projection=e"
+
+FATE_SAMPLES_SPHERICAL_COMPARE-$(call ALLYES, $(SPHERICAL_DEPS)) += $(FATE_FILTER_SPHERICAL)
+FATE_SAMPLES_SPHERICAL_COMPARE += $(FATE_SAMPLES_SPHERICAL_COMPARE-yes)
+
+$(FATE_SAMPLES_SPHERICAL_COMPARE): CMP = oneoff
+$(FATE_SAMPLES_SPHERICAL_COMPARE): CMP_UNIT = f32
+
+fate-filter-spherical: $(FATE_SAMPLES_SPHERICAL_COMPARE)
+
 FATE_FILTER_CMP_METADATA-$(CONFIG_SITI_FILTER) += fate-filter-refcmp-siti-yuv
 fate-filter-refcmp-siti-yuv: CMD = cmp_metadata siti yuv420p 0.015
 
diff --git a/tests/ref/fate/filter-spherical-barrel b/tests/ref/fate/filter-spherical-barrel
new file mode 100644
index 0000000000000000000000000000000000000000..11d36af7a1b26d4012a8ff5dd7973cb081a50af7
GIT binary patch
diff --git a/tests/ref/fate/filter-spherical-c3x2 b/tests/ref/fate/filter-spherical-c3x2
diff --git a/tools/Makefile b/tools/Makefile
index 4afa23342d..b8c88cd4d3 100644
--- a/tools/Makefile
+++ b/tools/Makefile
@@ -19,6 +19,7 @@ tools/target_io_dem_fuzzer.o: tools/target_dem_fuzzer.c
 
 tools/venc_data_dump$(EXESUF): tools/decode_simple.o
 tools/scale_slice_test$(EXESUF): tools/decode_simple.o
+tools/spherical_compare$(EXESUF): tools/decode_simple.o
 
 tools/decode_simple.o: | tools
 
diff --git a/tools/spherical_compare.c b/tools/spherical_compare.c
new file mode 100644
index 0000000000..59b1283a18
--- /dev/null
+++ b/tools/spherical_compare.c
@@ -0,0 +1,176 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+#include "decode_simple.h"
+
+#include "libavutil/avassert.h"
+#include "libavutil/avstring.h"
+#include "libavutil/common.h"
+#include "libavutil/dict.h"
+#include "libavutil/error.h"
+#include "libavutil/pixdesc.h"
+
+#include "libavformat/avformat.h"
+
+#include "libavcodec/avcodec.h"
+
+#include "libavfilter/avfilter.h"
+#include "libavfilter/buffersink.h"
+#include "libavfilter/buffersrc.h"
+
+typedef struct PrivData {
+    const char *filterchain;
+    AVFilterGraph   *fg;
+    AVFilterContext *src;
+    AVFilterContext *sink;
+    AVFrame         *frame;
+
+    uint64_t nb_frames;
+} PrivData;
+
+static int process_frame(DecodeContext *dc, AVFrame *frame)
+{
+    PrivData *pd = dc->opaque;
+    int ret;
+
+    if (!pd->fg) {
+        AVFilterInOut *inputs, *outputs;
+        char filterchain[1024];
+
+        if (!frame)
+            return 0;
+
+        snprintf(filterchain, sizeof(filterchain),
+                 "buffer@src=width=%d:height=%d:pix_fmt=%s:time_base=%d/%d,"
+                 "%s,buffersink@sink",
+                 frame->width, frame->height,
+                 av_get_pix_fmt_name(frame->format),
+                 dc->stream->time_base.num, dc->stream->time_base.den,
+                 pd->filterchain);
+
+        pd->fg = avfilter_graph_alloc();
+        if (!pd->fg)
+            return AVERROR(ENOMEM);
+
+        ret = avfilter_graph_parse2(pd->fg, filterchain, &inputs, &outputs);
+        if (ret < 0)
+            return ret;
+
+        av_assert0(!inputs && !outputs);
+
+        pd->src  = avfilter_graph_get_filter(pd->fg, "buffer@src");
+        pd->sink = avfilter_graph_get_filter(pd->fg, "buffersink@sink");
+        av_assert0(pd->src && pd->sink);
+
+        ret = avfilter_graph_config(pd->fg, pd->fg);
+        if (ret < 0)
+            return ret;
+
+        pd->frame = av_frame_alloc();
+        if (!pd->frame)
+            return AVERROR(ENOMEM);
+    }
+
+    ret = av_buffersrc_write_frame(pd->src, frame);
+    if (ret < 0)
+        return ret;
+
+    while (ret >= 0) {
+        static const char *keys[] = {
+            "lavfi.ssim360.Y",
+            "lavfi.ssim360.U",
+            "lavfi.ssim360.V",
+            "lavfi.ssim360.All",
+            "lavfi.ssim360.dB",
+        };
+
+        av_frame_unref(pd->frame);
+        ret = av_buffersink_get_frame(pd->sink, pd->frame);
+        if ((frame  && ret == AVERROR(EAGAIN)) ||
+            (!frame && ret == AVERROR_EOF))
+            return 0;
+        else if (ret < 0)
+            return ret;
+
+        fprintf(stderr, "frame %"PRIu64"\n", pd->nb_frames);
+
+        for (int i = 0; i < FF_ARRAY_ELEMS(keys); i++) {
+            const AVDictionaryEntry *t = av_dict_get(pd->frame->metadata, keys[i], NULL, 0);
+            float val;
+            size_t written;
+
+            if (!t) {
+                fprintf(stderr, "Metadata key '%s' not present in frame %"PRIu64"\n",
+                        keys[i], pd->nb_frames);
+                return AVERROR(EINVAL);
+            }
+            fprintf(stderr, "%s=%s\n", t->key, t->value);
+
+            val = strtof(t->value, NULL);
+
+            written = fwrite(&val, sizeof(val), 1, stdout);
+            if (written != 1) {
+                fprintf(stderr, "Error writing to stdout\n");
+                return AVERROR(EIO);
+            }
+        }
+
+        pd->nb_frames++;
+    }
+
+    return 0;
+}
+
+int main(int argc, char **argv)
+{
+    PrivData      pd;
+    DecodeContext dc;
+
+    const char *filename, *fc;
+    int ret = 0;
+
+    if (argc <= 2) {
+        fprintf(stderr, "Usage: %s <input file> <filterchain>\n", argv[0]);
+        return 0;
+    }
+
+    filename = argv[1];
+    fc       = argv[2];
+
+    memset(&pd, 0, sizeof(pd));
+    pd.filterchain = fc;
+
+    ret = ds_open(&dc, filename, 0);
+    if (ret < 0)
+        goto finish;
+
+    dc.process_frame = process_frame;
+    dc.opaque        = &pd;
+
+    ret = ds_run(&dc);
+
+finish:
+    avfilter_graph_free(&pd.fg);
+    av_frame_free(&pd.frame);
+    ds_free(&dc);
+    return ret;
+}
